diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,15 +2,19 @@ # KDE Application Version, managed by release script set (KDE_APPLICATIONS_VERSION_MAJOR "19") -set (KDE_APPLICATIONS_VERSION_MINOR "08") -set (KDE_APPLICATIONS_VERSION_MICRO "2") +set (KDE_APPLICATIONS_VERSION_MINOR "11") +set (KDE_APPLICATIONS_VERSION_MICRO "70") set (KDE_APPLICATIONS_VERSION "${KDE_APPLICATIONS_VERSION_MAJOR}.${KDE_APPLICATIONS_VERSION_MINOR}.${KDE_APPLICATIONS_VERSION_MICRO}") project(okular VERSION 1.8.${KDE_APPLICATIONS_VERSION_MICRO}) set(QT_REQUIRED_VERSION "5.9.0") set(KF5_REQUIRED_VERSION "5.44.0") +if (ANDROID) + set(QT_REQUIRED_VERSION "5.13.0") +endif() + find_package(ECM 5.33.0 CONFIG REQUIRED) set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${ECM_MODULE_PATH}) @@ -40,6 +44,10 @@ add_definitions(-DHAVE_SPEECH) endif() +if(ANDROID) + find_package(Qt5 ${QT_REQUIRED_VERSION} CONFIG REQUIRED COMPONENTS AndroidExtras) +endif() + if(NOT CMAKE_VERSION VERSION_LESS "3.10.0") # CMake 3.9+ warns about automoc on files without Q_OBJECT, and doesn't know about other macros. # 3.10+ lets us provide more macro names that require automoc. @@ -304,13 +312,15 @@ core/script/kjs_app.cpp core/script/kjs_console.cpp core/script/kjs_data.cpp + core/script/kjs_display.cpp core/script/kjs_document.cpp core/script/kjs_field.cpp core/script/kjs_fullscreen.cpp core/script/kjs_field.cpp core/script/kjs_spell.cpp core/script/kjs_util.cpp core/script/kjs_event.cpp + core/script/kjs_ocg.cpp ) target_link_libraries(okularcore PRIVATE KF5::JS KF5::JSApi) endif() @@ -346,6 +356,7 @@ part.cpp extensions.cpp ui/embeddedfilesdialog.cpp + ui/annotationactionhandler.cpp ui/annotwindow.cpp ui/annotationmodel.cpp ui/annotationpopup.cpp @@ -385,7 +396,7 @@ ui/thumbnaillist.cpp ui/toc.cpp ui/tocmodel.cpp - ui/toolaction.cpp + ui/toggleactionmenu.cpp ui/videowidget.cpp ui/layers.cpp ui/signatureguiutils.cpp @@ -448,7 +459,7 @@ ########### install files ############### -install(FILES okular.upd DESTINATION ${KDE_INSTALL_DATADIR}/kconf_update) +install(FILES okular.upd DESTINATION ${KDE_INSTALL_KCONFUPDATEDIR}) install( FILES okular_part.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR} ) install( FILES part.rc part-viewermode.rc DESTINATION ${KDE_INSTALL_KXMLGUI5DIR}/okular ) diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -17,6 +17,16 @@ TEST_NAME "visibilitytest" LINK_LIBRARIES Qt5::Widgets Qt5::Test okularcore ) + + ecm_add_test(kjsfunctionstest.cpp + TEST_NAME "kjsfunctionstest" + LINK_LIBRARIES Qt5::Widgets Qt5::Test okularcore + ) + + ecm_add_test(formattest.cpp + TEST_NAME "formattest" + LINK_LIBRARIES Qt5::Widgets Qt5::Test okularcore + ) endif() ecm_add_test(documenttest.cpp diff --git a/autotests/data/formattest.pdf b/autotests/data/formattest.pdf new file mode 100644 index 0000000000000000000000000000000000000000..c3138bebbf4b9d23b3060723a5daefa570623eb8 GIT binary patch literal 37060 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 DSEReq literal 0 Hc$@beABZpfm!4AV{~M2#Ay-pmcXCAs{6oNcx{4 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 literal 0 Hc$@page( 0 )->annotations().first()->uniqueName(), QString("testannot") ); // Check that we detect that it must be migrated - QCOMPARE( m_document->isDocdataMigrationNeeded(), true ); + QVERIFY( m_document->isDocdataMigrationNeeded() ); m_document->closeDocument(); // Reopen the document and check that the annotation is still present // (because we have not migrated) QCOMPARE( m_document->openDocument( testFilePath, testFileUrl, mime ), Okular::Document::OpenSuccess ); QCOMPARE( m_document->page( 0 )->annotations().size(), 1 ); QCOMPARE( m_document->page( 0 )->annotations().first()->uniqueName(), QString("testannot") ); - QCOMPARE( m_document->isDocdataMigrationNeeded(), true ); + QVERIFY( m_document->isDocdataMigrationNeeded() ); // Do the migration QTemporaryFile migratedSaveFile( QStringLiteral( "%1/okrXXXXXX.pdf" ).arg( QDir::tempPath() ) ); QVERIFY( migratedSaveFile.open() ); migratedSaveFile.close(); QVERIFY( m_document->saveChanges( migratedSaveFile.fileName() ) ); m_document->docdataMigrationDone(); - QCOMPARE( m_document->isDocdataMigrationNeeded(), false ); + QVERIFY( !m_document->isDocdataMigrationNeeded() ); m_document->closeDocument(); // Now the docdata file should have no annotations, let's check QCOMPARE( m_document->openDocument( testFilePath, testFileUrl, mime ), Okular::Document::OpenSuccess ); QCOMPARE( m_document->page( 0 )->annotations().size(), 0 ); - QCOMPARE( m_document->isDocdataMigrationNeeded(), false ); + QVERIFY( !m_document->isDocdataMigrationNeeded() ); m_document->closeDocument(); // And the new file should have 1 annotation, let's check QCOMPARE( m_document->openDocument( migratedSaveFile.fileName(), QUrl::fromLocalFile(migratedSaveFile.fileName()), mime ), Okular::Document::OpenSuccess ); QCOMPARE( m_document->page( 0 )->annotations().size(), 1 ); - QCOMPARE( m_document->isDocdataMigrationNeeded(), false ); + QVERIFY( !m_document->isDocdataMigrationNeeded() ); m_document->closeDocument(); delete m_document; diff --git a/autotests/formattest.cpp b/autotests/formattest.cpp new file mode 100644 --- /dev/null +++ b/autotests/formattest.cpp @@ -0,0 +1,189 @@ +/*************************************************************************** + * 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() ) + { + 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 formmated 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 new file mode 100644 --- /dev/null +++ b/autotests/kjsfunctionstest.cpp @@ -0,0 +1,390 @@ +/*************************************************************************** + * 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() + { + QVERIFY(m_clicked); + } + +private slots: + void closeMessageBox() + { + QWidgetList allToplevelWidgets = QApplication::topLevelWidgets(); + QMessageBox *mb = nullptr; + foreach ( 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(); +#endif +private: + Okular::Document *m_document; + QMap m_fields; +}; + +#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() ) + { + 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 --- a/autotests/parttest.cpp +++ b/autotests/parttest.cpp @@ -19,17 +19,21 @@ #include "../ui/toc.h" #include "../ui/sidebar.h" #include "../ui/pageview.h" +#include "../ui/presentationwidget.h" +#include "../settings.h" #include "../generators/poppler/config-okular-poppler.h" #include #include #include +#include #include #include #include #include +#include #include #include #include @@ -42,7 +46,12 @@ Q_OBJECT public: - CloseDialogHelper(Okular::Part *p, QDialogButtonBox::StandardButton b) : m_part(p), m_button(b), m_clicked(false) + CloseDialogHelper(Okular::Part *p, QDialogButtonBox::StandardButton b) : m_widget(p->widget()), m_button(b), m_clicked(false) + { + QTimer::singleShot(0, this, &CloseDialogHelper::closeDialog); + } + + CloseDialogHelper(QWidget *w, QDialogButtonBox::StandardButton b) : m_widget(w), m_button(b), m_clicked(false) { QTimer::singleShot(0, this, &CloseDialogHelper::closeDialog); } @@ -55,7 +64,7 @@ private slots: void closeDialog() { - QDialog *dialog = m_part->widget()->findChild(); + QDialog *dialog = m_widget->findChild(); if (!dialog) { QTimer::singleShot(0, this, &CloseDialogHelper::closeDialog); return; @@ -66,7 +75,7 @@ } private: - Okular::Part *m_part; + QWidget *m_widget; QDialogButtonBox::StandardButton m_button; bool m_clicked; }; @@ -100,6 +109,7 @@ void testSaveAsToSymlink(); void testSaveIsSymlink(); void testSidebarItemAfterSaving(); + void testViewModeSavingPerFile(); void testSaveAsUndoStackAnnotations(); void testSaveAsUndoStackAnnotations_data(); void testSaveAsUndoStackForms(); @@ -119,6 +129,7 @@ void testAnnotWindow(); void testAdditionalActionTriggers(); void testJumpToPage(); + void testTabletProximityBehavior(); private: void simulateMouseSelection(double startX, double startY, double endX, double endY, QWidget *target); @@ -1049,6 +1060,36 @@ 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); @@ -1611,8 +1652,8 @@ auto widget = win2->window()->childAt(win2->mapTo(win2->window(), QPoint(10, 10))); QTest::mouseMove(win2->window(), win2->mapTo(win2->window(), QPoint(10, 10))); QTest::mouseClick(widget, Qt::LeftButton, Qt::NoModifier, widget->mapFrom(win2, QPoint(10, 10))); - QVERIFY( win1->visibleRegion().rects().count() == 3); - QVERIFY( win2->visibleRegion().rects().count() == 4); + QVERIFY( win1->visibleRegion().rectCount() == 3); + QVERIFY( win2->visibleRegion().rectCount() == 4); } // Helper for testAdditionalActionTriggers @@ -1782,6 +1823,76 @@ QCOMPARE(part.m_pageView->verticalScrollBar()->value(), pageWithSpaceTop - 4); } +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[...]" + 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 ) ); +} + } // namespace Okular int main(int argc, char *argv[]) diff --git a/autotests/testingutils.h b/autotests/testingutils.h --- a/autotests/testingutils.h +++ b/autotests/testingutils.h @@ -30,7 +30,7 @@ * Returns true if the pairwise comparison coordinates of points in @p points1 and @p points2 are almost * equal (according to qFuzzyCompare) */ - bool pointListsAlmostEqual( QLinkedList< Okular::NormalizedPoint > points1, QLinkedList< Okular::NormalizedPoint > points2 ); + bool pointListsAlmostEqual( const QLinkedList< Okular::NormalizedPoint > &points1, const QLinkedList< Okular::NormalizedPoint > &points2 ); /* * The AnnotationDisposeWatcher class provides a static disposeAnnotation function diff --git a/autotests/testingutils.cpp b/autotests/testingutils.cpp --- a/autotests/testingutils.cpp +++ b/autotests/testingutils.cpp @@ -21,7 +21,7 @@ return annotXmlString; } - bool pointListsAlmostEqual( QLinkedList< Okular::NormalizedPoint > points1, QLinkedList< Okular::NormalizedPoint > points2 ) { + bool pointListsAlmostEqual( const QLinkedList< Okular::NormalizedPoint > &points1, const QLinkedList< Okular::NormalizedPoint > &points2 ) { QLinkedListIterator it1( points1 ); QLinkedListIterator it2( points2 ); diff --git a/autotests/visibilitytest.cpp b/autotests/visibilitytest.cpp --- a/autotests/visibilitytest.cpp +++ b/autotests/visibilitytest.cpp @@ -141,7 +141,7 @@ { if ( ff->name().startsWith( QStringLiteral( "Target" ) ) ) { - QCOMPARE( ff->isVisible(), false ); + QVERIFY( !ff->isVisible() ); anyChecked = true; } } diff --git a/conf/dlgaccessibility.cpp b/conf/dlgaccessibility.cpp --- a/conf/dlgaccessibility.cpp +++ b/conf/dlgaccessibility.cpp @@ -11,6 +11,12 @@ #include "ui_dlgaccessibilitybase.h" +#include "settings.h" + +#ifdef HAVE_SPEECH +#include +#endif + DlgAccessibility::DlgAccessibility( QWidget * parent ) : QWidget( parent ), m_selected( 0 ) { @@ -28,6 +34,17 @@ page->hide(); m_color_pages[ m_selected ]->show(); +#ifdef HAVE_SPEECH + // Populate tts engines + const QStringList engines = QTextToSpeech::availableEngines(); + for (const QString &engine: engines) { + m_dlg->kcfg_ttsEngine->addItem (engine); + } + m_dlg->kcfg_ttsEngine->setProperty("kcfg_property", QByteArray("currentText")); +#else + m_dlg->speechBox->hide(); +#endif + connect(m_dlg->kcfg_RenderMode, static_cast(&KComboBox::currentIndexChanged), this, &DlgAccessibility::slotColorMode); } diff --git a/conf/dlgaccessibilitybase.ui b/conf/dlgaccessibilitybase.ui --- a/conf/dlgaccessibilitybase.ui +++ b/conf/dlgaccessibilitybase.ui @@ -6,7 +6,7 @@ 0 0 374 - 327 + 479 @@ -351,6 +351,25 @@ + + + + Speech + + + + + + Engine + + + + + + + + + diff --git a/conf/dlgannotations.cpp b/conf/dlgannotations.cpp --- a/conf/dlgannotations.cpp +++ b/conf/dlgannotations.cpp @@ -20,9 +20,9 @@ Ui_DlgAnnotationsBase dlg; dlg.setupUi( this ); - WidgetAnnotTools * kcfg_AnnotationTools = new WidgetAnnotTools( dlg.annotToolsGroup ); - dlg.annotToolsPlaceholder->addWidget( kcfg_AnnotationTools ); - kcfg_AnnotationTools->setObjectName( QStringLiteral("kcfg_AnnotationTools") ); + WidgetAnnotTools * kcfg_QuickAnnotationTools = new WidgetAnnotTools( dlg.annotToolsGroup ); + dlg.annotToolsPlaceholder->addWidget( kcfg_QuickAnnotationTools ); + kcfg_QuickAnnotationTools->setObjectName( QStringLiteral("kcfg_QuickAnnotationTools") ); KConfigDialogManager::changedMap()->insert( QStringLiteral("WidgetAnnotTools") , SIGNAL(changed()) ); } diff --git a/conf/dlgannotationsbase.ui b/conf/dlgannotationsbase.ui --- a/conf/dlgannotationsbase.ui +++ b/conf/dlgannotationsbase.ui @@ -84,7 +84,7 @@ - Annotation tools + Quick annotation tools diff --git a/conf/dlgdebug.cpp b/conf/dlgdebug.cpp --- a/conf/dlgdebug.cpp +++ b/conf/dlgdebug.cpp @@ -23,7 +23,7 @@ : QWidget( parent ) { QVBoxLayout * lay = new QVBoxLayout( this ); - lay->setMargin( 0 ); + lay->setContentsMargins( 0, 0, 0, 0 ); DEBUG_SIMPLE_BOOL( "DebugDrawBoundaries", lay ); DEBUG_SIMPLE_BOOL( "DebugDrawAnnotationRect", lay ); diff --git a/conf/editannottooldialog.h b/conf/editannottooldialog.h --- a/conf/editannottooldialog.h +++ b/conf/editannottooldialog.h @@ -43,7 +43,9 @@ ToolTypewriter }; - explicit EditAnnotToolDialog( QWidget *parent = nullptr, const QDomElement &initialState = QDomElement() ); + explicit EditAnnotToolDialog( QWidget *parent = nullptr, + const QDomElement &initialState = QDomElement(), + bool builtinTool = false ); ~EditAnnotToolDialog(); QString name() const; QDomDocument toolXml() const; @@ -63,6 +65,8 @@ Okular::Annotation *m_stubann; AnnotationWidget *m_annotationWidget; + bool m_builtinTool; + private Q_SLOTS: void slotTypeChanged(); void slotDataChanged(); diff --git a/conf/editannottooldialog.cpp b/conf/editannottooldialog.cpp --- a/conf/editannottooldialog.cpp +++ b/conf/editannottooldialog.cpp @@ -34,9 +34,11 @@ #include "ui/pageviewannotator.h" -EditAnnotToolDialog::EditAnnotToolDialog( QWidget *parent, const QDomElement &initialState ) +EditAnnotToolDialog::EditAnnotToolDialog( QWidget *parent, const QDomElement &initialState, bool builtinTool ) : QDialog( parent ), m_stubann( nullptr ), m_annotationWidget( nullptr ) { + m_builtinTool = builtinTool; + QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel); QVBoxLayout *mainLayout = new QVBoxLayout; setLayout(mainLayout); @@ -54,21 +56,23 @@ mainLayout->addWidget(widget); mainLayout->addWidget(buttonBox); - m_name = new KLineEdit( widget ); + m_name->setReadOnly( m_builtinTool ); mainLayout->addWidget(m_name); tmplabel = new QLabel( i18n( "&Name:" ), widget ); mainLayout->addWidget(tmplabel); tmplabel->setBuddy( m_name ); widgetLayout->addWidget( tmplabel, 0, 0, Qt::AlignRight ); widgetLayout->addWidget( m_name, 0, 1 ); m_type = new KComboBox( false, widget ); + m_type->setVisible( !m_builtinTool ); mainLayout->addWidget(m_type); connect(m_type, static_cast(&KComboBox::currentIndexChanged), this, &EditAnnotToolDialog::slotTypeChanged); tmplabel = new QLabel( i18n( "&Type:" ), widget ); mainLayout->addWidget(tmplabel); tmplabel->setBuddy( m_type ); + tmplabel->setVisible( !m_builtinTool ); widgetLayout->addWidget( tmplabel, 1, 0, Qt::AlignRight ); widgetLayout->addWidget( m_type, 1, 1 ); @@ -84,15 +88,15 @@ widgetLayout->addWidget( m_appearanceBox, 2, 0, 1, 3 ); // Populate combobox with annotation types - m_type->addItem( i18n("Pop-up Note"), qVariantFromValue( ToolNoteLinked ) ); - m_type->addItem( i18n("Inline Note"), qVariantFromValue( ToolNoteInline ) ); - m_type->addItem( i18n("Freehand Line"), qVariantFromValue( ToolInk ) ); - m_type->addItem( i18n("Straight Line"), qVariantFromValue( ToolStraightLine ) ); - m_type->addItem( i18n("Polygon"), qVariantFromValue( ToolPolygon ) ); - m_type->addItem( i18n("Text markup"), qVariantFromValue( ToolTextMarkup ) ); - m_type->addItem( i18n("Geometrical shape"), qVariantFromValue( ToolGeometricalShape ) ); - m_type->addItem( i18n("Stamp"), qVariantFromValue( ToolStamp ) ); - m_type->addItem( i18n("Typewriter"), qVariantFromValue( ToolTypewriter ) ); + m_type->addItem( i18n("Pop-up Note"), QVariant::fromValue( ToolNoteLinked ) ); + m_type->addItem( i18n("Inline Note"), QVariant::fromValue( ToolNoteInline ) ); + m_type->addItem( i18n("Freehand Line"), QVariant::fromValue( ToolInk ) ); + m_type->addItem( i18n("Straight Line"), QVariant::fromValue( ToolStraightLine ) ); + m_type->addItem( i18n("Polygon"), QVariant::fromValue( ToolPolygon ) ); + m_type->addItem( i18n("Text markup"), QVariant::fromValue( ToolTextMarkup ) ); + m_type->addItem( i18n("Geometrical shape"), QVariant::fromValue( ToolGeometricalShape ) ); + m_type->addItem( i18n("Stamp"), QVariant::fromValue( ToolStamp ) ); + m_type->addItem( i18n("Typewriter"), QVariant::fromValue( ToolTypewriter ) ); createStubAnnotation(); @@ -381,6 +385,7 @@ } m_annotationWidget = AnnotationWidgetFactory::widgetFor( m_stubann ); + m_annotationWidget->setAnnotTypeEditable( !m_builtinTool ); m_appearanceBox->layout()->addWidget( m_annotationWidget->appearanceWidget() ); connect(m_annotationWidget, &AnnotationWidget::dataChanged, this, &EditAnnotToolDialog::slotDataChanged); diff --git a/conf/okular.kcfg b/conf/okular.kcfg --- a/conf/okular.kcfg +++ b/conf/okular.kcfg @@ -117,6 +117,47 @@ annotationTools + + + QStringList quickAnnotationTools; + // load the default tool list from the 'xml tools definition' file + QFile quickAnnFile( QStandardPaths::locate(QStandardPaths::GenericDataLocation, "okular/toolsQuick.xml") ); + if ( quickAnnFile.exists() && quickAnnFile.open( QIODevice::ReadOnly ) ) + { + QDomDocument doc; + if ( doc.setContent( &quickAnnFile ) ) + { + QDomElement toolsDefinition = doc.elementsByTagName("quickAnnotatingTools").item( 0 ).toElement(); + // create the quickAnnotationTools list from the XML dom tree + QDomNode toolDescription = toolsDefinition.firstChild(); + while ( toolDescription.isElement() ) + { + QDomElement toolElement = toolDescription.toElement(); + if ( toolElement.tagName() == "tool" ) + { + QDomDocument temp; + temp.appendChild( temp.importNode( toolElement, true) ); + // add each <tool>...</tool> as XML string + quickAnnotationTools << temp.toString(-1); + } + toolDescription = toolDescription.nextSibling(); + } + } + else + { + qWarning() << "QuickAnnotatingTools XML file seems to be damaged"; + } + } + else + { + qWarning() << "Unable to open QuickAnnotatingTools XML definition"; + } + + quickAnnotationTools + + + true + @@ -145,6 +186,9 @@ + + speechd + true diff --git a/conf/widgetannottools.cpp b/conf/widgetannottools.cpp --- a/conf/widgetannottools.cpp +++ b/conf/widgetannottools.cpp @@ -102,7 +102,7 @@ if ( itemText.isEmpty() ) itemText = PageViewAnnotator::defaultToolName( toolElement ); QListWidgetItem * listEntry = new QListWidgetItem( itemText, m_list ); - listEntry->setData( ToolXmlRole, qVariantFromValue(toolXml) ); + listEntry->setData( ToolXmlRole, QVariant::fromValue(toolXml) ); listEntry->setIcon( PageViewAnnotator::makeToolPixmap( toolElement ) ); } } @@ -136,7 +136,7 @@ // Edit list entry and attach XML string as data listEntry->setText( itemText ); - listEntry->setData( ToolXmlRole, qVariantFromValue( doc.toString(-1) ) ); + listEntry->setData( ToolXmlRole, QVariant::fromValue( doc.toString(-1) ) ); listEntry->setIcon( PageViewAnnotator::makeToolPixmap( toolElement ) ); // Select and scroll @@ -166,7 +166,7 @@ // Create list entry and attach XML string as data QListWidgetItem * listEntry = new QListWidgetItem( itemText, m_list ); - listEntry->setData( ToolXmlRole, qVariantFromValue( rootDoc.toString(-1) ) ); + listEntry->setData( ToolXmlRole, QVariant::fromValue( rootDoc.toString(-1) ) ); listEntry->setIcon( PageViewAnnotator::makeToolPixmap( toolElement ) ); // Select and scroll diff --git a/conf/widgetdrawingtools.cpp b/conf/widgetdrawingtools.cpp --- a/conf/widgetdrawingtools.cpp +++ b/conf/widgetdrawingtools.cpp @@ -95,7 +95,7 @@ itemText = name; QListWidgetItem * listEntry = new QListWidgetItem( itemText, m_list ); - listEntry->setData( ToolXmlRole, qVariantFromValue( toolXml ) ); + listEntry->setData( ToolXmlRole, QVariant::fromValue( toolXml ) ); listEntry->setData( Qt::DecorationRole, colorDecorationFromToolDescription( toolXml ) ); } } @@ -157,7 +157,7 @@ // Create list entry and attach XML string as data const QString toolXml = rootDoc.toString( -1 ); QListWidgetItem * listEntry = new QListWidgetItem( itemText, m_list ); - listEntry->setData( ToolXmlRole, qVariantFromValue( toolXml ) ); + listEntry->setData( ToolXmlRole, QVariant::fromValue( toolXml ) ); listEntry->setData( Qt::DecorationRole, colorDecorationFromToolDescription( toolXml ) ); // Select and scroll @@ -201,7 +201,7 @@ // Edit list entry and attach XML string as data const QString toolXml = doc.toString( -1 ); listEntry->setText( itemText ); - listEntry->setData( ToolXmlRole, qVariantFromValue( toolXml ) ); + listEntry->setData( ToolXmlRole, QVariant::fromValue( toolXml ) ); listEntry->setData( Qt::DecorationRole, colorDecorationFromToolDescription( toolXml ) ); // Select and scroll diff --git a/core/annotations.h b/core/annotations.h --- a/core/annotations.h +++ b/core/annotations.h @@ -1153,8 +1153,18 @@ HighlightType highlightType() const; /** - * The Quad class contains 8 coordinates and style definitions - * which describe a line part of the whole highlight annotation. + * @short Describes a highlight quad of a text markup annotation. + * + * The Quad is a closed path of 4 NormalizedPoints. + * Another set of 4 NormalizedPoints can be generated with transform(), + * e. g. to get highlighting coordinates on a rotated PageViewItem. + * Additionally, Quad stores some geometry related style attributes. + * + * To enable correct rendering of the annotation, + * the points 0 and 1 must describe the bottom edge of the quad + * (relative to the text orientation). + * + * @see NormalizedPoint */ class OKULARCORE_EXPORT Quad { @@ -1226,6 +1236,9 @@ /** * Transforms the quad coordinates with the transformation defined * by @p matrix. + * + * The transformed coordinates will be accessible with transformedPoint(). + * The coordinates returned by point() are not affected. */ void transform( const QTransform &matrix ); diff --git a/core/area.h b/core/area.h --- a/core/area.h +++ b/core/area.h @@ -32,24 +32,95 @@ /** * NormalizedPoint is a helper class which stores the coordinates - * of a normalized point. Normalized means that the coordinates are - * between 0 and 1 so that it is page size independent. + * of a normalized point. * - * Example: - * The normalized point is (0.5, 0.3) + * @par Normalized Coordinate System + * @parblock + * Normalized means that the coordinates are always between 0 and 1, + * unless the point shall be outside of the reference area. * - * If you want to draw it on a 800x600 page, just multiply the x coordinate (0.5) with - * the page width (800) and the y coordinate (0.3) with the page height (600), so - * the point will be drawn on the page at (400, 180). + * The reference area is a rectangle, and all normalized points + * with coordinates of 0 or 1 describe its edges. * - * That allows you to zoom the page by just multiplying the normalized points with the - * zoomed page size. + * This allows to locate things on a reference area without knowing its + * (current or future) actual size. When the reference area is resized, + * all things which are described in normalized coordinates keep their + * proportional position on the area. + * @endparblock + * + * @par Transformation to and from Normalized Coordinates + * @parblock + * To transform normalized coordinates to coordinates on the reference area, + * just multiply them with the size of the reference area. + * + * To get normalized coordinates from a point on the reference area, + * just divide its coordinates with the size of the reference area. + * + * Many methods have parameters @c xScale and @c yScale, + * these are equal to the size of the reference area. + * @endparblock + * + * @par Normalized Coordinate System Applied to Pages + * @parblock + * Okular uses a normalized coordinate system mainly to describe + * positions on pages. + * This is useful because pages can be shown in different sizes (zoom), + * but all objects shall keep their proportional position on the page. + * + * Okular maps from page to normalized coordinates as follows: + * * Left edge of the page: x = 0 + * * Right edge of the page: x = 1 + * * Top edge of the page: y = 0 + * * Bottom edge of the page: y = 1 + * @endparblock + * + * @par Example: Draw a Point on a Page + * @parblock + * The point is given in normalized coordinates (0.5, 0.3). + * + * If you want to draw it on a 800x600 page, + * just multiply the x coordinate (0.5) with the page width (800), + * and the y coordinate (0.3) with the page height (600). + * So, the point will be drawn on the page at (400, 180). + * + * That allows you to zoom the page by just multiplying the normalized points with the + * zoomed page size. + * @endparblock + * + * @par Example: Select Text on a Page using Mouse Events + * @parblock + * The position of all glyphs and words is stored in normalized coordinates. + * (This is what TextPage actually does.) + * Mouse press and release events are given in page coordinates (400, 180) and (600, 450), + * while the page has a size of 800x600. + * + * If you want to search all text between the mouse click and release event, + * you need their normalized coordinates. + * Just divide the x coordinates (400 and 600) by the page width (800), + * and the y coordinates (180 and 450) by the page height (600). + * So, you have to search for all glyphs between (0.5, 0.3) and (0.75, 0.75). + * + * That allows you to process all glyphs and words without + * having to keep any of their positions in sync with the page. + * @endparblock + * + * @par Geometric operations + * @parblock + * NormalizedPoint supports basic geometric operations. + * * You can transform it with a QTransform matrix. + * * With the size of the reference area, you can calculate the squared + * absolute distance to another NormalizedPoint or a line of two NormalizedPoints. + * + * NormalizedRect provides additional geometric operations for rectangles. + * @endparblock + * + * @see NormalizedRect */ class OKULARCORE_EXPORT NormalizedPoint { public: /** - * Creates a new empty normalized point. + * Creates a normalized point at (0, 0). */ NormalizedPoint(); @@ -59,8 +130,8 @@ NormalizedPoint( double x, double y ); /** - * Creates a new normalized point with the coordinates (@p x, @p y) which are normalized - * by the scaling factors @p xScale and @p yScale. + * Creates a new normalized point from an absolute point (@p x, @p y) + * on a reference area of size @p xScale x @p yScale. */ NormalizedPoint( int x, int y, int xScale, int yScale ); @@ -78,14 +149,16 @@ void transform( const QTransform &matrix ); /** - * Returns squared distance to point @p x @p y @p xScale @p yScale + * Returns squared distance to normalized point (@p x, @p y) + * on a reference area of size @p xScale x @p yScale. * @since 0.17 (KDE 4.11) */ double distanceSqr( double x, double y, double xScale, double yScale ) const; - /** - * @brief Calculates distance of the point @p x @p y @p xScale @p yScale to the line segment from @p start to @p end + * Returns squared distance of the normalized point (@p x, @p y) + * to the line segment from @p start to @p end + * on a reference area of size @p xScale x @p yScale. * @since 0.17 (KDE 4.11) */ static double distanceSqr( double x, double y, double xScale, double yScale, const NormalizedPoint& start, const NormalizedPoint& end ); @@ -103,8 +176,18 @@ /** - * NormalizedRect is a helper class which stores the coordinates - * of a normalized rect, which is a rectangle of @see NormalizedPoints. + * A NormalizedRect is a rectangle which can be defined by two NormalizedPoints. + * + * It describes a rectangular area on a reference area of undefined size. + * For more information about the normalized coordinate system, see NormalizedPoint. + * + * In Okular, NormalizedRect can be used e. g. to describe bounding boxes of TextEntity objects, + * and the highlight area of text selections. + * + * If you need to describe an area which consists of multiple rectangles, + * you can use RegularAreaRect instead. + * + * @see NormalizedPoint, RegularAreaRect, TextEntity */ class OKULARCORE_EXPORT NormalizedRect { @@ -126,12 +209,22 @@ * @li y = top * @li width = right - left * @li height = bottom - top + * + * @note + * The coordinates for @p left and @p top should be lower than + * @p right and @p bottom, respectively. + * At negative width or height the behaviour of some operations is undefined. */ NormalizedRect( double left, double top, double right, double bottom ); /** - * Creates a normalized rectangle of the given @p rectangle which is normalized - * by the scaling factors @p xScale and @p yScale. + * Creates a normalized rectangle from the given @p rectangle + * on a reference area of size @p xScale x @p yScale. + * + * @note + * The rectangle should have positive width and height. + * You can use e. g. QRect::normalize() to ensure this. + * At negative width or height the behaviour of some operations is undefined. */ NormalizedRect( const QRect &rectangle, double xScale, double yScale ); @@ -148,7 +241,7 @@ ~NormalizedRect(); /** - * Build a normalized rect from a QRectF. + * Build a normalized rect from a QRectF, which already has normalized coordinates. */ static NormalizedRect fromQRectF( const QRectF &rect ); @@ -158,8 +251,8 @@ bool isNull() const; /** - * Returns whether the normalized rectangle contains the normalized coordinates - * @p x and @p y. + * Returns whether the normalized rectangle contains the normalized point + * (@p x, @p y). */ bool contains( double x, double y ) const; @@ -182,13 +275,13 @@ bool intersects( double left, double top, double right, double bottom ) const; /** - * Returns the rectangle that accrues when the normalized rectangle is multiplyed - * with the scaling @p xScale and @p yScale. + * Returns the rectangle mapped to a reference area of @p xScale x @p yScale. */ QRect geometry( int xScale, int yScale ) const; /** * Same functionality as geometry, but the output is now rounded before typecasting to int + * * @since 0.14 (KDE 4.8) */ QRect roundedGeometry( int xScale, int yScale ) const; @@ -207,7 +300,7 @@ /** * Returns the intersection of this normalized rectangle with the specified - * @p other. If the rects do not intersect then the result is null. + * @p other. If the rects do not intersect then the result is a null rectangle. * * @since 0.7 (KDE 4.1) */ @@ -231,62 +324,63 @@ void transform( const QTransform &matrix ); /** - * Returns true if the point pt is located to the bottom of the rectangle + * Returns true if the point @p pt is located below the bottom of the rectangle * @since 0.14 (KDE 4.8) */ bool isBottom(const NormalizedPoint& pt) const { return bottom < pt.y; } /** - * Returns true if the point pt is located on the top of the rectangle + * Returns true if the point @p pt is located above the top of the rectangle * @since 0.14 (KDE 4.8) */ bool isTop(const NormalizedPoint& pt) const { return top > pt.y; } /** - * Returns true if the point pt is located under the top of the rectangle + * Returns true if the point @p pt is located below the top of the rectangle * @since 0.14 (KDE 4.8) */ bool isBottomOrLevel(const NormalizedPoint& pt) const { return top < pt.y; } /** - * Returns true if the point pt is located above the bottom of the rectangle + * Returns true if the point @p pt is located above the bottom of the rectangle * @since 0.14 (KDE 4.8) */ bool isTopOrLevel(const NormalizedPoint& pt) const { return bottom > pt.y; } /** - * Returns true if the point pt is located to the right of the left arm of rectangle + * Returns true if the point @p pt is located to the right of the left edge of the rectangle * @since 0.14 (KDE 4.8) */ bool isLeft(const NormalizedPoint& pt) const { return left < pt.x; } /** - * Returns true if the point pt is located to the left of the right arm of rectangle + * Returns true if the point @p pt is located to the left of the right edge of the rectangle * @since 0.14 (KDE 4.8) */ bool isRight(const NormalizedPoint& pt) const { return right > pt.x; } /** - * Returns the distance of the point @p x @p y @p xScale @p yScale to the closest - * edge or 0 if the point is within the rectangle + * Returns the squared distance of the normalized point (@p x, @p y) + * to the closest edge, or 0 if the point is within the rectangle; + * using a reference area of size @p xScale x @p yScale * @since 0.17 (KDE 4.11) */ double distanceSqr(double x, double y, double xScale, double yScale) const @@ -340,17 +434,22 @@ //KDE_DUMMY_QHASH_FUNCTION(NormalizedRect) /** - * @short NormalizedRect that contains a reference to an object. + * @short An area with normalized coordinates that contains a reference to an object. * - * These rects contains a pointer to a okular object (such as an action or something - * like that). The pointer is read and stored as 'void pointer' so cast is + * These areas ("rects") contain a pointer to a document object + * (such as a hyperlink, an action, or something like that). + * The pointer is read and stored as 'void pointer' so cast is * performed by accessors based on the value returned by objectType(). Objects * are reparented to this class. * * Type / Class correspondence tab: * - Action : class Action: description of an action * - Image : class Image : description of an image (n/a) * - Annotation: class Annotation: description of an annotation + * + * For more information about the normalized coordinate system, see NormalizedPoint. + * + * @see NormalizedPoint */ class OKULARCORE_EXPORT ObjectRect { @@ -417,8 +516,8 @@ virtual QRect boundingRect( double xScale, double yScale ) const; /** - * Returns whether the object rectangle contains the point @p x, @p y for the - * scaling factor @p xScale and @p yScale. + * Returns whether the object rectangle contains the point with absolute coordinates + * (@p x, @p y) at a page size of @p xScale x @p yScale. */ virtual bool contains( double x, double y, double xScale, double yScale ) const; @@ -428,8 +527,10 @@ virtual void transform( const QTransform &matrix ); /** - * Returns the square of the distance between the object and the point @p x, @p y - * for the scaling factor @p xScale and @p yScale. + * Returns the squared distance between the object + * and the point with + * normalized coordinates (@p x, @p y) + * at a page size of @p xScale x @p yScale. * * @since 0.8.2 (KDE 4.2.2) */ @@ -547,32 +648,40 @@ /// @endcond /** - * @short A regular area of NormalizedShape which normalizes a Shape + * @short An area with normalized coordinates, consisting of NormalizedShape objects. + * + * This is a template class to describe an area which consists of + * multiple shapes of the same type, intersecting or non-intersecting. + * The coordinates are normalized, and can be mapped to a reference area of defined size. + * For more information about the normalized coordinate system, see NormalizedPoint. * * Class NormalizedShape \b must have the following functions/operators defined: - * - bool contains( double, double ) + * - bool contains( double, double ), whether it contains the given NormalizedPoint * - bool intersects( NormalizedShape ) * - bool isNull() - * - Shape geometry( int, int ) - * - operator|=( NormalizedShape ) which unite two NormalizedShape's + * - Shape geometry( int, int ), which maps to the reference area + * - operator|=( NormalizedShape ), which unites two NormalizedShape's + * + * @see RegularAreaRect, NormalizedPoint */ template class RegularArea : public QList { public: /** - * Returns whether the regular area contains the - * normalized point @p x, @p y. + * Returns whether this area contains the normalized point (@p x, @p y). */ bool contains( double x, double y ) const; /** - * Returns whether the regular area contains the - * given @p shape. + * Returns whether this area contains a NormalizedShape object that equals @p shape. + * + * @note + * The original NormalizedShape objects can be lost if simplify() was called. */ bool contains( const NormalizedShape& shape ) const; /** - * Returns whether the regular area intersects with the given @p area. + * Returns whether this area intersects with the given @p area. */ bool intersects( const RegularArea *area ) const; @@ -582,17 +691,18 @@ bool intersects( const NormalizedShape& shape ) const; /** - * Appends the given @p area to the regular area. + * Appends the given @p area to this area. */ void appendArea( const RegularArea *area ); /** - * Appends the given @p shape to the regular area. + * Appends the given @p shape to this area. */ void appendShape( const NormalizedShape& shape, MergeSide side = MergeAll ); /** - * Simplifies the regular area by merging its intersecting subareas. + * Simplifies this regular area by merging its intersecting subareas. + * This might change the effective geometry of this area. */ void simplify(); @@ -602,8 +712,9 @@ bool isNull() const; /** - * Returns the subareas of the regular areas as shapes for the given scaling factor - * @p xScale and @p yScale, translated by @p dx and @p dy. + * Returns the subareas of this regular area + * mapped to a reference area of size @p xScale x @p yScale, + * then translated by @p dx and @p dy. */ QList geometry( int xScale, int yScale, int dx = 0, int dy = 0 ) const; @@ -819,6 +930,18 @@ givePtr( (*this)[i] )->transform( matrix ); } +/** + * This is a list of NormalizedRect, to describe an area consisting of + * multiple rectangles using normalized coordinates. + * + * This area can be mapped to a reference area, resulting in a list of QRects. + * For more information about the normalized coordinate system, see NormalizedPoint. + * + * Okular uses this area e. g. to describe a text highlight area, + * which consists of multiple, intersecting or non-intersecting rectangles. + * + * @see NormalizedRect, NormalizedPoint + */ class OKULARCORE_EXPORT RegularAreaRect : public RegularArea< NormalizedRect, QRect > { public: @@ -834,8 +957,8 @@ }; /** - * This class stores the coordinates of a highlighting area - * together with the id of the highlight owner and the color. + * This class stores the geometry of a highlighting area in normalized coordinates, + * together with highlighting specific information. */ class HighlightAreaRect : public RegularAreaRect { diff --git a/core/document.h b/core/document.h --- a/core/document.h +++ b/core/document.h @@ -668,6 +668,34 @@ */ void processAction( const Action *action ); + /** + * Processes the given format @p action on @p field. + * + * @since 1.9 + */ + void processFormatAction( const Action *action, Okular::FormFieldText *field ); + + /** + * Processes the given keystroke @p action on @p field. + * + * @since 1.9 + */ + void processKeystrokeAction( const Action *action, Okular::FormFieldText *field, bool &returnCode ); + + /** + * Processes the given focus action on the field. + * + * @since 1.9 + */ + void processFocusAction( const Action *action, Okular::FormField *field ); + + /** + * Processes the given keystroke @p action on @p field. + * + * @since 1.9 + */ + void processValidateAction( const Action *action, Okular::FormFieldText *field, bool &returnCode ); + /** * Returns a list of the bookmarked.pages */ diff --git a/core/document.cpp b/core/document.cpp --- a/core/document.cpp +++ b/core/document.cpp @@ -697,6 +697,45 @@ 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(); } @@ -723,6 +762,37 @@ 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 @@ -1151,8 +1221,16 @@ if ( newVal != oldVal ) { fft->setText( newVal ); - emit m_parent->refreshFormWidget( fft ); - pageNeedsRefresh = true; + 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; + } } } } @@ -2204,6 +2282,36 @@ 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 ) ) { @@ -4292,6 +4400,123 @@ } } +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 ) @@ -5115,6 +5340,13 @@ return data; } +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 ) @@ -5259,7 +5491,7 @@ { Rotation rotation = (Rotation)r; if ( !m_generator || ( m_rotation == rotation ) ) - return; + return; // tell the pages to rotate QVector< Okular::Page * >::const_iterator pIt = m_pagesVector.constBegin(); diff --git a/core/document_p.h b/core/document_p.h --- a/core/document_p.h +++ b/core/document_p.h @@ -12,8 +12,10 @@ #define _OKULAR_DOCUMENT_P_H_ #include "document.h" +#include "script/event_p.h" #include "synctex/synctex_parser.h" +#include // qt/kde/system includes #include @@ -41,6 +43,7 @@ struct RunningSearch; namespace Okular { +class ScriptAction; class ConfigInterface; class PageController; class SaveInterface; @@ -191,6 +194,13 @@ void doProcessSearchMatch( RegularAreaRect *match, RunningSearch *search, QSet< int > *pagesToNotify, int currentPage, int searchID, bool moveViewport, const QColor & color ); + /** + * Executes a JavaScript script from the setInterval function. + * + * @since 1.9 + */ + void executeScript( const QString &function ); + // generators stuff /** * This method is used by the generators to signal the finish of @@ -220,6 +230,17 @@ void clearAndWaitForRequests(); + + /* + * Executes a ScriptAction with the event passed as parameter. + */ + void executeScriptEvent( const std::shared_ptr< Event > &event, const Okular::ScriptAction * linkscript ); + + /* + * Find the corresponding page number for the form field passed as parameter. + */ + int findFieldPageNumber( Okular::FormField *field ); + // member variables Document *m_parent; QPointer m_widget; diff --git a/core/fontinfo.h b/core/fontinfo.h --- a/core/fontinfo.h +++ b/core/fontinfo.h @@ -84,6 +84,16 @@ */ void setName( const QString& name ); + /** + * Returns the substitute name for the font. + */ + QString substituteName() const; + + /** + * Sets a new substitute name for the font. + */ + void setSubstituteName( const QString& substituteName ); + /** * Returns the type of the font. */ diff --git a/core/fontinfo.cpp b/core/fontinfo.cpp --- a/core/fontinfo.cpp +++ b/core/fontinfo.cpp @@ -27,13 +27,15 @@ bool operator==( const FontInfoPrivate &rhs ) const { return name == rhs.name && + substituteName == rhs.substituteName && type == rhs.type && embedType == rhs.embedType && file == rhs.file && canBeExtracted == rhs.canBeExtracted; } QString name; + QString substituteName; FontInfo::FontType type; FontInfo::EmbedType embedType; bool canBeExtracted; @@ -66,6 +68,16 @@ d->name = name; } +QString FontInfo::substituteName() const +{ + return d->substituteName; +} + +void FontInfo::setSubstituteName( const QString& substituteName ) +{ + d->substituteName = substituteName; +} + FontInfo::FontType FontInfo::type() const { return d->type; diff --git a/core/form.h b/core/form.h --- a/core/form.h +++ b/core/form.h @@ -86,6 +86,14 @@ */ virtual QString uiName() const = 0; + /** + * The fully qualified name of the field, is used in the JavaScript + * scripts. + * + * @since 1.9 + */ + virtual QString fullyQualifiedName() const = 0; + /** * Whether the field is read-only. */ @@ -110,6 +118,20 @@ */ virtual void setVisible( bool value ); + /** + Whether this field is printable. + + @since 1.9 + */ + virtual bool isPrintable() const; + + /** + Set this field printable + + @since 1.9 + */ + virtual void setPrintable( bool value ); + Action* activationAction() const; /** @@ -207,6 +229,13 @@ */ virtual QList< int > siblings() const = 0; + /** + * Sets the icon of the Button to the Icon of the field parameter. + * + * @since 1.9 + */ + virtual void setIcon( Okular::FormField *field ); + protected: FormFieldButton(); @@ -288,6 +317,13 @@ */ virtual bool canBeSpellChecked() const; + /** + * Set the text which should be rendered by the PDF. + * + * @since 1.9 + */ + virtual void setAppearanceText( const QString &text ) = 0; + protected: FormFieldText(); diff --git a/core/form.cpp b/core/form.cpp --- a/core/form.cpp +++ b/core/form.cpp @@ -70,6 +70,15 @@ { } +bool FormField::isPrintable() const +{ + return true; +} + +void FormField::setPrintable( bool ) +{ +} + Action* FormField::activationAction() const { Q_D( const FormField ); @@ -129,7 +138,7 @@ QString value() const override { Q_Q( const FormFieldButton ); - return qVariantFromValue( q->state() ).toString(); + return QVariant::fromValue( q->state() ).toString(); } }; @@ -147,6 +156,10 @@ { } +void FormFieldButton::setIcon( Okular::FormField * ) +{ +} + class Okular::FormFieldTextPrivate : public Okular::FormFieldPrivate { @@ -210,7 +223,6 @@ return false; } - class Okular::FormFieldChoicePrivate : public Okular::FormFieldPrivate { public: diff --git a/core/page.cpp b/core/page.cpp --- a/core/page.cpp +++ b/core/page.cpp @@ -579,9 +579,7 @@ if ( d->m_text ) { d->m_text->d->m_page = this; - /** - * Correct text order for before text selection - */ + // Correct/optimize text order for search and text selection d->m_text->d->correctTextOrder(); } } diff --git a/core/script/builtin.js b/core/script/builtin.js --- a/core/script/builtin.js +++ b/core/script/builtin.js @@ -60,3 +60,190 @@ event.value = ret; } + +/** AFTime_Format + * + * Formats event.value based on parameters. + * + * Parameter description based on Acrobat Help: + * + * ptf is the number which should be used to format the time, is one of: + * 0 = 24HR_MM [ 14:30 ] + * 1 = 12HR_MM [ 2:30 PM ] + * 2 = 24HR_MM_SS [ 14:30:15 ] + * 3 = 12HR_MM_SS [ 2:30:15 PM ] + */ +function AFTime_Format( ptf ) +{ + if( !event.value ) + { + return; + } + var tokens = event.value.split( /\D/ ); + var invalidDate = false; + + // Remove empty elements of the array + tokens = tokens.filter(Boolean); + + if( tokens.length < 2 ) + invalidDate = true; + + // Check if every number is valid + for( i = 0 ; i < tokens.length ; ++i ) + { + if( isNaN( tokens[i] ) ) + { + invalidDate = true; + break; + } + switch( i ) + { + case 0: + { + if( tokens[i] > 23 || tokens[i] < 0 ) + invalidDate = true; + break; + } + case 1: + case 2: + { + if( tokens[i] > 59 || tokens[i] < 0 ) + invalidDate = true; + break; + } + } + } + if( invalidDate ) + { + event.value = ""; + return; + } + + // Make it of lenght 3, since we use hh, mm, ss + while( tokens.length < 3 ) + tokens.push( 0 ); + + // We get pm string in the user locale to search. + var dummyPm = util.printd( 'ap', new Date( 2018, 5, 11, 23, 11, 11) ).toLocaleLowerCase(); + // Add 12 to time if it's PM and less than 12 + if( event.value.toLocaleLowerCase().search( dummyPm ) !== -1 && Number( tokens[0] ) < 12 ) + tokens[0] = Number( tokens[0] ) + 12; + + // We use a random date, because we only care about time. + var date = new Date( 2019, 7, 12, tokens[0], tokens[1], tokens[2] ); + var ret; + switch( ptf ) + { + case 0: + ret = util.printd( "hh:MM", date ); + break; + case 1: + ret = util.printd( "h:MM ap", date ); + break; + case 2: + ret = util.printd( "hh:MM:ss", date ); + break; + case 3: + ret = util.printd( "h:MM:ss ap", date ); + break; + } + event.value = ret; +} + +/** AFTime_Keystroke + * + * Checks if the string in event.value is valid. Not used. + */ +function AFTime_Keystroke( ptf ) +{ + return; +} + +/** AFSpecial_Format + * psf is the type of formatting to use: + * 0 = zip code + * 1 = zip + 4 + * 2 = phone + * 3 = SSN + * + * These are all in the US format. +*/ +function AFSpecial_Format( psf ) +{ + if( !event.value || psf == 0 ) + { + return; + } + + var ret = event.value; + + if( psf === 1 ) + ret = ret.substr( 0, 5 ) + '-' + ret.substr( 5, 4 ); + + else if( psf === 2 ) + ret = '(' + ret.substr( 0, 3 ) + ') ' + ret.substr( 3, 3 ) + '-' + ret.substr( 6, 4 ); + + else if( psf === 3 ) + ret = ret.substr( 0, 3 ) + '-' + ret.substr( 3, 2 ) + '-' + ret.substr( 5, 4 ); + + event.value = ret; +} + +/** AFSpecial_Keystroke + * + * Checks if the String in event.value is valid. + * + * Parameter description based on Acrobat Help: + * + * psf is the type of formatting to use: + * 0 = zip code + * 1 = zip + 4 + * 2 = phone + * 3 = SSN + * + * These are all in the US format. We check to see if only numbers are inserted and the length of the string. +*/ +function AFSpecial_Keystroke( psf ) +{ + if ( !event.value ) + { + return; + } + + var str = event.value; + if( psf === 0 ) + { + if( str.length > 5 ) + { + event.rc = false; + return; + } + } + + else if( psf === 1 || psf === 3 ) + { + if( str.length > 9 ) + { + event.rc = false; + return; + } + } + + else if( psf === 2 ) + { + if( str.length > 10 ) + { + event.rc = false; + return; + } + } + + for( i = 0 ; i < str.length ; ++i ) + { + if( !( str[i] <= '9' && str[i] >= '0' ) ) + { + event.rc = false; + return; + } + } +} diff --git a/core/script/event.cpp b/core/script/event.cpp --- a/core/script/event.cpp +++ b/core/script/event.cpp @@ -7,6 +7,7 @@ * (at your option) any later version. * ***************************************************************************/ +#include #include "event_p.h" #include "../form.h" @@ -20,7 +21,9 @@ m_targetPage( nullptr ), m_source( nullptr ), m_sourcePage( nullptr ), - m_eventType( eventType ) + m_eventType( eventType ), + m_returnCode( false ), + m_shiftModifier( false ) { } @@ -31,6 +34,8 @@ EventType m_eventType; QString m_targetName; QVariant m_value; + bool m_returnCode; + bool m_shiftModifier; }; Event::Event(): d( new Private( UnknownEvent ) ) @@ -53,6 +58,14 @@ { case ( FieldCalculate ): return QStringLiteral( "Calculate" ); + case ( FieldFormat ): + return QStringLiteral( "Format" ); + case ( FieldKeystroke ): + return QStringLiteral( "Keystroke" ); + case ( FieldFocus ): + return QStringLiteral( "Focus" ); + case ( FieldValidate ): + return QStringLiteral( "Validate" ); case ( UnknownEvent ): default: return QStringLiteral( "Unknown" ); @@ -64,6 +77,10 @@ switch ( d->m_eventType ) { case ( FieldCalculate ): + case ( FieldFormat ): + case ( FieldKeystroke ): + case ( FieldFocus ): + case ( FieldValidate ): return QStringLiteral( "Field" ); case ( UnknownEvent ): default: @@ -136,6 +153,26 @@ d->m_value = val; } +bool Event::returnCode() const +{ + return d->m_returnCode; +} + +void Event::setReturnCode( bool returnCode ) +{ + d->m_returnCode = returnCode; +} + +bool Event::shiftModifier() const +{ + return d->m_shiftModifier; +} + +void Event::setShiftModifier( bool shiftModifier ) +{ + d->m_shiftModifier = shiftModifier; +} + // static std::shared_ptr Event::createFormCalculateEvent( FormField *target, Page *targetPage, @@ -157,3 +194,74 @@ } return ret; } + +// static +std::shared_ptr Event::createFormatEvent( FormField *target, + Page *targetPage, + const QString &targetName ) +{ + std::shared_ptr ret( new Event( Event::FieldFormat ) ); + ret->setTarget( target ); + ret->setTargetPage( targetPage ); + ret->setTargetName( targetName ); + + FormFieldText *fft = dynamic_cast< FormFieldText * >(target); + if ( fft ) + { + ret->setValue( QVariant( fft->text() ) ); + } + return ret; +} + +// static +std::shared_ptr Event::createKeystrokeEvent( FormField *target, Page *targetPage ) +{ + std::shared_ptr ret( new Event( Event::FieldKeystroke ) ); + ret->setTarget( target ); + ret->setTargetPage( targetPage ); + + FormFieldText *fft = dynamic_cast< FormFieldText * >(target); + if ( fft ) + { + ret->setReturnCode( true ); + ret->setValue( QVariant( fft->text() ) ); + } + return ret; +} + +std::shared_ptr Event::createFormFocusEvent( FormField *target, + Page *targetPage, + const QString &targetName ) +{ + std::shared_ptr ret( new Event( Event::FieldFocus ) ); + ret->setTarget( target ); + ret->setTargetPage( targetPage ); + ret->setTargetName( targetName ); + ret->setShiftModifier( QApplication::keyboardModifiers() & Qt::ShiftModifier ); + + FormFieldText *fft = dynamic_cast< FormFieldText * >(target); + if ( fft ) + { + ret->setValue( QVariant( fft->text() ) ); + } + return ret; +} + +std::shared_ptr Event::createFormValidateEvent( FormField *target, + Page *targetPage, + const QString &targetName ) +{ + std::shared_ptr ret( new Event( Event::FieldValidate ) ); + ret->setTarget( target ); + ret->setTargetPage( targetPage ); + ret->setTargetName( targetName ); + ret->setShiftModifier( QApplication::keyboardModifiers() & Qt::ShiftModifier ); + + FormFieldText *fft = dynamic_cast< FormFieldText * >(target); + if ( fft ) + { + ret->setValue( QVariant( fft->text() ) ); + ret->setReturnCode( true ); + } + return ret; +} diff --git a/core/script/event_p.h b/core/script/event_p.h --- a/core/script/event_p.h +++ b/core/script/event_p.h @@ -56,14 +56,18 @@ ExternalExec, /// < Not implemented. FieldBlur, /// < Not implemented. FieldCalculate, /// < This event is defined in a field re-calculation. - FieldFocus, /// < Not implemented. - FieldFormat, /// < Not implemented. - FieldKeystroke, /// < Not implemented. + FieldFocus, /// < This event is defined when the field gains or loses focus. + FieldFormat, /// < When a format action is executed + FieldKeystroke, /// < Checks if the entered value is valid. FieldMouseDown, /// < Not implemented. FieldMouseEnter, /// < Not implemented. FieldMouseExit, /// < Not implemented. FieldMouseUp, /// < Not implemented. - FieldValidate, /// < Not implemented. + /* Validates the field after every change is committed + * (clicked outside or tabbed to another field). + * The enter event is not handled + */ + FieldValidate, LinkMouseUp, /// < Not implemented. MenuExec, /// < Not implemented. PageOpen, /// < Not implemented. @@ -97,11 +101,27 @@ QVariant value() const; void setValue(const QVariant &val); + bool returnCode() const; + void setReturnCode(bool returnCode); + + // Checks if the shift key was down when creating the event. + bool shiftModifier() const; + void setShiftModifier(bool shiftModifier); + static std::shared_ptr createFormCalculateEvent( FormField *target, Page *targetPage, FormField *source = nullptr, Page *sourcePage = nullptr, const QString &targetName = QString() ); + static std::shared_ptr createFormatEvent( FormField *target, Page *targetPage, + const QString &targetName = QString() ); + static std::shared_ptr createKeystrokeEvent( FormField *target, Page *targetPage ); + static std::shared_ptr createFormFocusEvent( FormField *target, + Page *targetPage, + const QString &targetName = QString() ); + static std::shared_ptr createFormValidateEvent( FormField *target, + Page *targetPage, + const QString &targetName = QString() ); private: class Private; std::shared_ptr d; diff --git a/core/script/executor_kjs.cpp b/core/script/executor_kjs.cpp --- a/core/script/executor_kjs.cpp +++ b/core/script/executor_kjs.cpp @@ -24,10 +24,12 @@ #include "kjs_app_p.h" #include "kjs_console_p.h" #include "kjs_data_p.h" +#include "kjs_display_p.h" #include "kjs_document_p.h" #include "kjs_event_p.h" #include "kjs_field_p.h" #include "kjs_fullscreen_p.h" +#include "kjs_ocg_p.h" #include "kjs_spell_p.h" #include "kjs_util_p.h" @@ -64,15 +66,19 @@ JSFullscreen::initType( ctx ); JSConsole::initType( ctx ); JSData::initType( ctx ); + JSDisplay::initType( ctx ); JSDocument::initType( ctx ); JSEvent::initType( ctx ); JSField::initType( ctx ); + JSOCG::initType( ctx ); JSSpell::initType( ctx ); JSUtil::initType( ctx ); m_docObject.setProperty( ctx, QStringLiteral("app"), JSApp::object( ctx, m_doc ) ); m_docObject.setProperty( ctx, QStringLiteral("console"), JSConsole::object( ctx ) ); m_docObject.setProperty( ctx, QStringLiteral("Doc"), m_docObject ); + m_docObject.setProperty( ctx, QStringLiteral("display"), JSDisplay::object( ctx ) ); + m_docObject.setProperty( ctx, QStringLiteral("OCG"), JSOCG::object( ctx ) ); m_docObject.setProperty( ctx, QStringLiteral("spell"), JSSpell::object( ctx ) ); m_docObject.setProperty( ctx, QStringLiteral("util"), JSUtil::object( ctx ) ); } @@ -84,6 +90,9 @@ ExecutorKJS::~ExecutorKJS() { + JSField::clearCachedFields(); + JSApp::clearCachedFields(); + JSOCG::clearCachedFields(); delete d; } @@ -106,6 +115,7 @@ KJSResult result = d->m_interpreter->evaluate( QStringLiteral("okular.js"), 1, script, &d->m_docObject ); + if ( result.isException() || ctx->hasException() ) { qCDebug(OkularCoreDebug) << "JS exception" << result.errorMessage(); @@ -120,5 +130,4 @@ << event->type() << "value:" << event->value(); } } - JSField::clearCachedFields(); } diff --git a/core/script/kjs_app.cpp b/core/script/kjs_app.cpp --- a/core/script/kjs_app.cpp +++ b/core/script/kjs_app.cpp @@ -17,15 +17,23 @@ #include #include +#include #include +#include +#include #include "../document_p.h" +#include "../scripter.h" #include "kjs_fullscreen_p.h" using namespace Okular; +#define OKULAR_TIMERID QStringLiteral( "okular_timerID" ) + static KJSPrototype *g_appProto; +typedef QHash< int, QTimer * > TimerCache; +Q_GLOBAL_STATIC( TimerCache, g_timerCache ) // the acrobat version we fake static const double fake_acroversion = 8.00; @@ -150,6 +158,113 @@ return KJSNumber( fake_acroversion ); } +/* + Alert function defined in the reference, it shows a Dialog Box with options. + app.alert() +*/ +static KJSObject appAlert( KJSContext *context, void *, + const KJSArguments &arguments ) +{ + if ( arguments.count() < 1 ) + { + return context->throwException( i18n( "Missing alert type") ); + } + QString cMsg = arguments.at( 0 ).toString( context ); + int nIcon = 0; + int nType = 0; + QString cTitle = "Okular"; + + if( arguments.count() >= 2 ) + nIcon = arguments.at( 1 ).toInt32( context ); + if( arguments.count() >= 3 ) + nType = arguments.at( 2 ).toInt32( context ); + if( arguments.count() >= 4 ) + cTitle = arguments.at( 3 ).toString( context ); + + QMessageBox::Icon icon; + switch( nIcon ) + { + case 0: + icon = QMessageBox::Critical; + break; + case 1: + icon = QMessageBox::Warning; + break; + case 2: + icon = QMessageBox::Question; + break; + case 3: + icon = QMessageBox::Information; + break; + } + + QMessageBox box( icon, cTitle, cMsg ); + + switch( nType ) + { + case 0: + box.setStandardButtons( QMessageBox::Ok ); + break; + case 1: + box.setStandardButtons( QMessageBox::Ok | QMessageBox::Cancel ); + break; + case 2: + box.setStandardButtons( QMessageBox::Yes | QMessageBox::No ); + break; + case 3: + box.setStandardButtons( QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel ); + break; + } + + QCheckBox *checkBox = nullptr; + KJSObject oCheckbox; + if( arguments.count() >= 6 ) + { + oCheckbox = arguments.at( 5 ); + KJSObject oMsg = oCheckbox.property( context, "cMsg" ); + QString msg = i18n( "Do not show this message again" ); + + if( oMsg.isString() ) + msg = oMsg.toString( context ); + + bool bInitialValue = false; + KJSObject value = oCheckbox.property( context, "bInitialValue" ); + if( value.isBoolean() ) + bInitialValue = value.toBoolean( context ); + checkBox = new QCheckBox( msg ); + checkBox->setChecked( bInitialValue ); + box.setCheckBox( checkBox ); + + } + + int button = box.exec(); + + int ret; + + switch( button ) + { + case QMessageBox::Ok: + ret = 1; + break; + case QMessageBox::Cancel: + ret = 2; + break; + case QMessageBox::No: + ret = 3; + break; + case QMessageBox::Yes: + ret = 4; + break; + } + + if( arguments.count() >= 6 ) + oCheckbox.setProperty( context, QStringLiteral( "bAfterValue" ), checkBox->isChecked() ); + + delete checkBox; + + return KJSNumber( ret ); +} + static KJSObject appBeep( KJSContext *context, void *, const KJSArguments &arguments ) { @@ -199,6 +314,77 @@ return KJSUndefined(); } +// app.setInterval() +static KJSObject appSetInterval( KJSContext *ctx, void *object, + const KJSArguments &arguments ) +{ + DocumentPrivate *doc = reinterpret_cast< DocumentPrivate * >( object ); + const QString function = arguments.at( 0 ).toString( ctx ) + ';'; + const int interval = arguments.at( 1 ).toInt32( ctx ); + + QTimer *timer = new QTimer(); + + QObject::connect( timer, &QTimer::timeout, doc->m_parent, [=](){ doc->executeScript( function ); } ); + + timer->start( interval ); + + return JSApp::wrapTimer( ctx, timer ); +} + +// app.clearInterval() +static KJSObject appClearInterval( KJSContext *ctx, void *, + const KJSArguments &arguments ) +{ + KJSObject timerObject = arguments.at( 0 ); + const int timerId = timerObject.property( ctx, OKULAR_TIMERID ).toInt32( ctx ); + QTimer *timer = g_timerCache->value( timerId ); + if( timer != nullptr ) + { + timer->stop(); + g_timerCache->remove( timerId ); + delete timer; + } + + return KJSUndefined(); +} + +// app.setTimeOut() +static KJSObject appSetTimeOut( KJSContext *ctx, void *object, + const KJSArguments &arguments ) +{ + DocumentPrivate *doc = reinterpret_cast< DocumentPrivate * >( object ); + const QString function = arguments.at( 0 ).toString( ctx ) + ';'; + const int interval = arguments.at( 1 ).toInt32( ctx ); + + QTimer *timer = new QTimer(); + timer->setSingleShot( true ); + + QObject::connect( timer, &QTimer::timeout, doc->m_parent, [=](){ doc->executeScript( function ); } ); + + timer->start( interval ); + + return JSApp::wrapTimer( ctx, timer ); +} + +// app.clearTimeOut() +static KJSObject appClearTimeOut( KJSContext *ctx, void *, + const KJSArguments &arguments ) +{ + KJSObject timerObject = arguments.at( 0 ); + const int timerId = timerObject.property( ctx, OKULAR_TIMERID ).toInt32( ctx ); + QTimer *timer = g_timerCache->value( timerId ); + + if( timer != nullptr ) + { + timer->stop(); + g_timerCache->remove( timerId ); + delete timer; + } + + return KJSUndefined(); +} + + void JSApp::initType( KJSContext *ctx ) { static bool initialized = false; @@ -219,13 +405,37 @@ g_appProto->defineProperty( ctx, QStringLiteral("viewerVariation"), appGetViewerVariation ); g_appProto->defineProperty( ctx, QStringLiteral("viewerVersion"), appGetViewerVersion ); + g_appProto->defineFunction( ctx, QStringLiteral("alert"), appAlert ); g_appProto->defineFunction( ctx, QStringLiteral("beep"), appBeep ); g_appProto->defineFunction( ctx, QStringLiteral("getNthPlugInName"), appGetNthPlugInName ); g_appProto->defineFunction( ctx, QStringLiteral("goBack"), appGoBack ); g_appProto->defineFunction( ctx, QStringLiteral("goForward"), appGoForward ); + g_appProto->defineFunction( ctx, QStringLiteral("setInterval"), appSetInterval ); + g_appProto->defineFunction( ctx, QStringLiteral("clearInterval"), appClearInterval ); + g_appProto->defineFunction( ctx, QStringLiteral("setTimeOut"), appSetTimeOut ); + g_appProto->defineFunction( ctx, QStringLiteral("clearTimeOut"), appClearTimeOut ); } KJSObject JSApp::object( KJSContext *ctx, DocumentPrivate *doc ) { return g_appProto->constructObject( ctx, doc ); } + +KJSObject JSApp::wrapTimer( KJSContext *ctx, QTimer *timer) +{ + KJSObject timerObject = g_appProto->constructObject( ctx, timer ); + timerObject.setProperty( ctx, OKULAR_TIMERID, timer->timerId() ); + + g_timerCache->insert( timer->timerId(), timer ); + + return timerObject; +} + +void JSApp::clearCachedFields() +{ + if ( g_timerCache ) + { + qDeleteAll( g_timerCache->begin(), g_timerCache->end() ); + g_timerCache->clear(); + } +} diff --git a/core/script/kjs_app_p.h b/core/script/kjs_app_p.h --- a/core/script/kjs_app_p.h +++ b/core/script/kjs_app_p.h @@ -13,6 +13,7 @@ class KJSContext; class KJSObject; +class QTimer; namespace Okular { @@ -23,6 +24,8 @@ public: static void initType( KJSContext *ctx ); static KJSObject object( KJSContext *ctx, DocumentPrivate *doc ); + static KJSObject wrapTimer( KJSContext *ctx, QTimer *timer ); + static void clearCachedFields(); }; } diff --git a/core/script/kjs_console.cpp b/core/script/kjs_console.cpp --- a/core/script/kjs_console.cpp +++ b/core/script/kjs_console.cpp @@ -42,7 +42,7 @@ g_jsConsoleWindow->setButtonGuiItem( KDialog::User1, KStandardGuiItem::clear() ); QVBoxLayout *mainLay = new QVBoxLayout( g_jsConsoleWindow->mainWidget() ); - mainLay->setMargin( 0 ); + mainLay->setContentsMargins( 0, 0, 0, 0 ); g_jsConsoleLog = new QPlainTextEdit( g_jsConsoleWindow->mainWidget() ); g_jsConsoleLog->setReadOnly( true ); mainLay->addWidget( g_jsConsoleLog ); diff --git a/core/script/kjs_display.cpp b/core/script/kjs_display.cpp new file mode 100644 --- /dev/null +++ b/core/script/kjs_display.cpp @@ -0,0 +1,64 @@ +/*************************************************************************** + * 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 "kjs_display_p.h" +#include "../form.h" + +#include +#include + +#include + +using namespace Okular; + +static KJSPrototype *g_displayProto; + +// display.hidden +static KJSObject displayGetHidden( KJSContext *, void * ) +{ + return KJSNumber( FormDisplay::FormHidden ); +} + +// display.visible +static KJSObject displayGetVisible( KJSContext *, void * ) +{ + return KJSNumber( FormDisplay::FormVisible ); +} + +// display.noView +static KJSObject displayGetNoView( KJSContext *, void * ) +{ + return KJSNumber( FormDisplay::FormNoView ); +} + +// display.noPrint +static KJSObject displayGetNoPrint( KJSContext *, void * ) +{ + return KJSNumber( FormDisplay::FormNoPrint ); +} + +void JSDisplay::initType( KJSContext *ctx ) +{ + static bool initialized = false; + if ( initialized ) + return; + initialized = true; + + g_displayProto = new KJSPrototype(); + + g_displayProto->defineProperty( ctx, QStringLiteral("hidden"), displayGetHidden ); + g_displayProto->defineProperty( ctx, QStringLiteral("visible"), displayGetVisible ); + g_displayProto->defineProperty( ctx, QStringLiteral("noView"), displayGetNoView ); + g_displayProto->defineProperty( ctx, QStringLiteral("noPrint"), displayGetNoPrint ); +} + +KJSObject JSDisplay::object( KJSContext *ctx ) +{ + return g_displayProto->constructObject( ctx, nullptr ); +} diff --git a/core/script/kjs_app_p.h b/core/script/kjs_display_p.h copy from core/script/kjs_app_p.h copy to core/script/kjs_display_p.h --- a/core/script/kjs_app_p.h +++ b/core/script/kjs_display_p.h @@ -1,28 +1,36 @@ /*************************************************************************** - * Copyright (C) 2008 by Pino Toscano * - * Copyright (C) 2008 by Harri Porten * + * 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. * ***************************************************************************/ -#ifndef OKULAR_SCRIPT_KJS_APP_P_H -#define OKULAR_SCRIPT_KJS_APP_P_H +#ifndef OKULAR_SCRIPT_KJS_DISPLAY_P_H +#define OKULAR_SCRIPT_KJS_DISPLAY_P_H class KJSContext; class KJSObject; namespace Okular { -class DocumentPrivate; +/** + * The display types of the field. +*/ +enum FormDisplay +{ + FormVisible, + FormHidden, + FormNoPrint, + FormNoView +}; -class JSApp +class JSDisplay { public: static void initType( KJSContext *ctx ); - static KJSObject object( KJSContext *ctx, DocumentPrivate *doc ); + static KJSObject object( KJSContext *ctx ); }; } diff --git a/core/script/kjs_document.cpp b/core/script/kjs_document.cpp --- a/core/script/kjs_document.cpp +++ b/core/script/kjs_document.cpp @@ -24,6 +24,7 @@ #include "../form.h" #include "kjs_data_p.h" #include "kjs_field_p.h" +#include "kjs_ocg_p.h" using namespace Okular; @@ -129,6 +130,21 @@ return KJSBoolean( !isShell ); } +// Document.numFields +static KJSObject docGetNumFields( KJSContext *, void *object ) +{ + const DocumentPrivate *doc = reinterpret_cast< DocumentPrivate* >( object ); + + unsigned int numFields = 0; + + for ( const Page * pIt : qAsConst(doc->m_pagesVector) ) + { + numFields += pIt->formFields().size(); + } + + return KJSNumber( numFields ); +} + static KJSObject docGetInfo( KJSContext *ctx, void *object ) { @@ -195,7 +211,7 @@ QLinkedList< Okular::FormField * >::const_iterator ffIt = pageFields.constBegin(), ffEnd = pageFields.constEnd(); for ( ; ffIt != ffEnd; ++ffIt ) { - if ( (*ffIt)->name() == cName ) + if ( (*ffIt)->fullyQualifiedName() == cName ) { return JSField::wrapField( context, *ffIt, *pIt ); } @@ -248,6 +264,55 @@ return KJSUndefined(); } +// Document.getNthFieldName +static KJSObject docGetNthFieldName( KJSContext *ctx, void *object, + const KJSArguments &arguments ) +{ + const DocumentPrivate *doc = reinterpret_cast< DocumentPrivate* >( object ); + + int numField = arguments.at( 0 ).toInt32( ctx ); + + for ( const Page * pIt : qAsConst(doc->m_pagesVector) ) + { + const QLinkedList< Okular::FormField * > pageFields = pIt->formFields(); + + if(numField < pageFields.size()) + { + const auto ffIt = pageFields.begin() + numField; + + return KJSString( (*ffIt)->fullyQualifiedName() ); + } + + numField -= pageFields.size(); + } + + return KJSUndefined(); +} + +static KJSObject docGetOCGs( KJSContext *ctx, void *object, + const KJSArguments & ) +{ + const DocumentPrivate *doc = reinterpret_cast< DocumentPrivate* >( object ); + + QAbstractItemModel * model = doc->m_parent->layersModel(); + + KJSArray array( ctx, model->rowCount() ); + + for(int i = 0;i < model->rowCount();++i){ + for(int j = 0;j < model->columnCount();++j){ + const QModelIndex index = model->index( i, j ); + + KJSObject item = JSOCG::wrapOCGObject( ctx, model, i, j ); + item.setProperty( ctx, QStringLiteral("name"), model->data( index , Qt::DisplayRole ).toString() ); + item.setProperty( ctx, QStringLiteral("initState"), model->data( index , Qt::CheckStateRole ).toBool() ); + + array.setProperty( ctx, QString::number( i ), item ); + } + } + + return array; +} + void JSDocument::initType( KJSContext *ctx ) { assert( g_docProto ); @@ -266,6 +331,7 @@ g_docProto->defineProperty( ctx, QStringLiteral("permStatusReady"), docGetPermStatusReady ); g_docProto->defineProperty( ctx, QStringLiteral("dataObjects"), docGetDataObjects ); g_docProto->defineProperty( ctx, QStringLiteral("external"), docGetExternal ); + g_docProto->defineProperty( ctx, QStringLiteral("numFields"), docGetNumFields ); // info properties g_docProto->defineProperty( ctx, QStringLiteral("info"), docGetInfo ); @@ -281,6 +347,8 @@ g_docProto->defineFunction( ctx, QStringLiteral("getPageRotation"), docGetPageRotation ); g_docProto->defineFunction( ctx, QStringLiteral("gotoNamedDest"), docGotoNamedDest ); g_docProto->defineFunction( ctx, QStringLiteral("syncAnnotScan"), docSyncAnnotScan ); + g_docProto->defineFunction( ctx, QStringLiteral("getNthFieldName"), docGetNthFieldName ); + g_docProto->defineFunction( ctx, QStringLiteral("getOCGs"), docGetOCGs ); } KJSGlobalObject JSDocument::wrapDocument( DocumentPrivate *doc ) diff --git a/core/script/kjs_event.cpp b/core/script/kjs_event.cpp --- a/core/script/kjs_event.cpp +++ b/core/script/kjs_event.cpp @@ -49,6 +49,13 @@ event->setTargetName ( value.toString ( ctx ) ); } +// Event.shift +static KJSObject eventGetShift( KJSContext *, void *object ) +{ + const Event *event = reinterpret_cast< Event * >( object ); + return KJSBoolean( event->shiftModifier() ); +} + // Event.source static KJSObject eventGetSource( KJSContext *ctx, void *object ) { @@ -66,11 +73,19 @@ static KJSObject eventGetTarget( KJSContext *ctx, void *object ) { const Event *event = reinterpret_cast< Event * >( object ); - if ( event->eventType() == Event::FieldCalculate ) + switch( event->eventType() ) { - FormField *target = static_cast< FormField * >( event->target() ); - if ( target ) - return JSField::wrapField( ctx, target, event->targetPage() ); + case Event::FieldCalculate: + case Event::FieldFormat: + case Event::FieldKeystroke: + case Event::FieldFocus: + case Event::FieldValidate: + { + FormField *target = static_cast< FormField * >( event->target() ); + if ( target ) + return JSField::wrapField( ctx, target, event->targetPage() ); + break; + } } return KJSUndefined(); } @@ -89,6 +104,20 @@ event->setValue ( QVariant( value.toString ( ctx ) ) ); } +// Event.rc (getter) +static KJSObject eventGetReturnCode( KJSContext *, void *object ) +{ + const Event *event = reinterpret_cast< Event * >( object ); + return KJSBoolean( event->returnCode() ); +} + +// Event.rc (setter) +static void eventSetReturnCode( KJSContext *ctx, void *object, KJSObject value ) +{ + Event *event = reinterpret_cast< Event * >( object ); + event->setReturnCode ( value.toBoolean ( ctx ) ); +} + void JSEvent::initType( KJSContext *ctx ) { static bool initialized = false; @@ -103,9 +132,11 @@ g_eventProto->defineProperty( ctx, QStringLiteral( "type" ), eventGetType ); g_eventProto->defineProperty( ctx, QStringLiteral( "targetName" ), eventGetTargetName, eventSetTargetName ); + g_eventProto->defineProperty( ctx, QStringLiteral( "shift" ), eventGetShift ); g_eventProto->defineProperty( ctx, QStringLiteral( "source" ), eventGetSource ); g_eventProto->defineProperty( ctx, QStringLiteral( "target" ), eventGetTarget ); g_eventProto->defineProperty( ctx, QStringLiteral( "value" ), eventGetValue, eventSetValue ); + g_eventProto->defineProperty( ctx, QStringLiteral( "rc" ), eventGetReturnCode, eventSetReturnCode ); } KJSObject JSEvent::wrapEvent( KJSContext *ctx, Event *event ) diff --git a/core/script/kjs_field.cpp b/core/script/kjs_field.cpp --- a/core/script/kjs_field.cpp +++ b/core/script/kjs_field.cpp @@ -23,13 +23,18 @@ #include "../form.h" #include "../page.h" #include "../page_p.h" +#include "kjs_display_p.h" using namespace Okular; +#define OKULAR_NAME QStringLiteral("okular_name") + static KJSPrototype *g_fieldProto; typedef QHash< FormField *, Page * > FormCache; Q_GLOBAL_STATIC( FormCache, g_fieldCache ) +typedef QHash< QString, FormField * > ButtonCache; +Q_GLOBAL_STATIC( ButtonCache, g_buttonCache ) // Helper for modified fields @@ -58,7 +63,7 @@ static KJSObject fieldGetName( KJSContext *, void *object ) { const FormField *field = reinterpret_cast< FormField * >( object ); - return KJSString( field->name() ); + return KJSString( field->fullyQualifiedName() ); } // Field.readonly (getter) @@ -224,6 +229,79 @@ updateField( field ); } +// Field.display (getter) +static KJSObject fieldGetDisplay( KJSContext *, void *object ) +{ + const FormField *field = reinterpret_cast< FormField * >( object ); + bool visible = field->isVisible(); + if( visible ) + { + return KJSNumber( field->isPrintable() ? FormDisplay::FormVisible : FormDisplay::FormNoPrint ); + } + return KJSNumber( field->isPrintable() ? FormDisplay::FormNoView : FormDisplay::FormHidden ); +} + +// Field.display (setter) +static void fieldSetDisplay( KJSContext *context, void *object, KJSObject value ) +{ + FormField *field = reinterpret_cast< FormField * >( object ); + const unsigned int b = value.toInt32( context ); + switch( b ) + { + case FormDisplay::FormVisible: + field->setVisible( true ); + field->setPrintable( true ); + break; + case FormDisplay::FormHidden: + field->setVisible( false ); + field->setPrintable( false ); + break; + case FormDisplay::FormNoPrint: + field->setVisible( true ); + field->setPrintable( false ); + break; + case FormDisplay::FormNoView: + field->setVisible( false ); + field->setPrintable( true ); + break; + } + updateField( field ); +} + +// Instead of getting the Icon, we pick the field. +static KJSObject fieldButtonGetIcon( KJSContext *ctx, void *object, + const KJSArguments & ) +{ + FormField *field = reinterpret_cast< FormField * >( object ); + + KJSObject fieldObject; + fieldObject.setProperty( ctx, OKULAR_NAME, field->fullyQualifiedName() ); + g_buttonCache->insert( field->fullyQualifiedName(), field ); + + return fieldObject; +} + +/* +* Now we send to the button what Icon should be drawn on it +*/ +static KJSObject fieldButtonSetIcon( KJSContext *ctx, void *object, + const KJSArguments &arguments ) +{ + FormField *field = reinterpret_cast< FormField * >( object ); + + const QString fieldName = arguments.at( 0 ).property( ctx, OKULAR_NAME ).toString( ctx ); + + if( field->type() == Okular::FormField::FormButton ) + { + FormFieldButton *button = static_cast< FormFieldButton * >( field ); + button->setIcon( g_buttonCache->value( fieldName ) ); + } + + updateField( field ); + + return KJSUndefined(); +} + void JSField::initType( KJSContext *ctx ) { static bool initialized = false; @@ -241,6 +319,10 @@ g_fieldProto->defineProperty( ctx, QStringLiteral("type"), fieldGetType ); g_fieldProto->defineProperty( ctx, QStringLiteral("value"), fieldGetValue, fieldSetValue ); g_fieldProto->defineProperty( ctx, QStringLiteral("hidden"), fieldGetHidden, fieldSetHidden ); + g_fieldProto->defineProperty( ctx, QStringLiteral("display"), fieldGetDisplay, fieldSetDisplay ); + + g_fieldProto->defineFunction( ctx, QStringLiteral("buttonGetIcon"), fieldButtonGetIcon ); + g_fieldProto->defineFunction( ctx, QStringLiteral("buttonSetIcon"), fieldButtonSetIcon ); } KJSObject JSField::wrapField( KJSContext *ctx, FormField *field, Page *page ) @@ -255,7 +337,8 @@ void JSField::clearCachedFields() { if ( g_fieldCache.exists() ) - { g_fieldCache->clear(); - } + + if( g_buttonCache.exists() ) + g_buttonCache->clear(); } diff --git a/core/script/kjs_ocg.cpp b/core/script/kjs_ocg.cpp new file mode 100644 --- /dev/null +++ b/core/script/kjs_ocg.cpp @@ -0,0 +1,86 @@ +/*************************************************************************** + * 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 "kjs_ocg_p.h" + +#include +#include +#include + +#include +#include +#include +#include + +using namespace Okular; + +static KJSPrototype *g_OCGProto; + +typedef QHash< QPair< int, int > *, QAbstractItemModel* > OCGCache; +Q_GLOBAL_STATIC( OCGCache, g_OCGCache ) + +// OCG.state (getter) +static KJSObject OCGGetState( KJSContext *, void *object ) +{ + QPair< int, int > *pair = reinterpret_cast< QPair< int, int >* > ( object ); + QAbstractItemModel *model = g_OCGCache->value( pair ); + + const QModelIndex index = model->index( pair->first, pair->second ); + + const bool state = model->data( index, Qt::CheckStateRole ).toBool(); + + return KJSBoolean( state ); +} + +// OCG.state (setter) +static void OCGSetState( KJSContext* ctx, void* object, + KJSObject value ) +{ + QPair< int, int > *pair = reinterpret_cast< QPair< int, int >* > ( object ); + QAbstractItemModel *model = g_OCGCache->value( pair ); + + const QModelIndex index = model->index( pair->first, pair->second ); + + const bool state = value.toBoolean( ctx ); + + model->setData( index, QVariant( state ? Qt::Checked : Qt::Unchecked ), Qt::CheckStateRole ); +} + + +void JSOCG::initType( KJSContext *ctx ) +{ + static bool initialized = false; + if ( initialized ) + return; + initialized = true; + + g_OCGProto = new KJSPrototype(); + + g_OCGProto->defineProperty( ctx, QStringLiteral("state"), OCGGetState, OCGSetState ); +} + +KJSObject JSOCG::object( KJSContext *ctx ) +{ + return g_OCGProto->constructObject( ctx, nullptr ); +} + +KJSObject JSOCG::wrapOCGObject( KJSContext *ctx, QAbstractItemModel *model, const int &i, const int &j ) +{ + QPair< int, int > *pair = new QPair< int ,int >( i, j ); + g_OCGCache->insert( pair, model ); + return g_OCGProto->constructObject( ctx, pair ); +} + +void JSOCG::clearCachedFields() +{ + if ( g_OCGCache.exists() ) + { + g_OCGCache->clear(); + } +} diff --git a/core/script/kjs_app_p.h b/core/script/kjs_ocg_p.h copy from core/script/kjs_app_p.h copy to core/script/kjs_ocg_p.h --- a/core/script/kjs_app_p.h +++ b/core/script/kjs_ocg_p.h @@ -1,28 +1,28 @@ /*************************************************************************** - * Copyright (C) 2008 by Pino Toscano * - * Copyright (C) 2008 by Harri Porten * + * 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. * ***************************************************************************/ -#ifndef OKULAR_SCRIPT_KJS_APP_P_H -#define OKULAR_SCRIPT_KJS_APP_P_H +#ifndef OKULAR_SCRIPT_KJS_OCG_P_H +#define OKULAR_SCRIPT_KJS_OCG_P_H class KJSContext; class KJSObject; +class QAbstractItemModel; namespace Okular { -class DocumentPrivate; - -class JSApp +class JSOCG { public: static void initType( KJSContext *ctx ); - static KJSObject object( KJSContext *ctx, DocumentPrivate *doc ); + static KJSObject object( KJSContext *ctx ); + static KJSObject wrapOCGObject( KJSContext *ctx, QAbstractItemModel *model, const int &i, const int &j ); + static void clearCachedFields(); }; } diff --git a/core/script/kjs_util.cpp b/core/script/kjs_util.cpp --- a/core/script/kjs_util.cpp +++ b/core/script/kjs_util.cpp @@ -15,6 +15,10 @@ #include #include +#include +#include +#include +#include using namespace Okular; @@ -56,6 +60,59 @@ return obj; } +static KJSObject printd( KJSContext *context, void *, + const KJSArguments &arguments ) +{ + if ( arguments.count() < 2 ) + { + return context->throwException( QStringLiteral("Invalid arguments") ); + } + + KJSObject oFormat = arguments.at( 0 ); + QString format; + QLocale defaultLocale; + + if( oFormat.isNumber() ) + { + int formatType = oFormat.toInt32( context ); + switch( formatType ) + { + case 0: + format = QStringLiteral( "D:yyyyMMddHHmmss" ); + break; + case 1: + format = QStringLiteral( "yyyy.MM.dd HH:mm:ss"); + break; + case 2: + format = defaultLocale.dateTimeFormat( QLocale::ShortFormat ); + if( !format.contains( QStringLiteral( "ss" ) ) ) + format.insert( format.indexOf( QStringLiteral( "mm" ) ) + 2, QStringLiteral( ":ss" ) ); + break; + } + } + else + { + format = arguments.at( 0 ).toString( context ).replace( "tt", "ap" ); + format.replace( "t", "a" ); + for( int i = 0 ; i < format.size() ; ++i ) + { + if( format[i] == 'M' ) + format[i] = 'm'; + else if( format[i] == 'm' ) + format[i] = 'M'; + } + } + + QLocale locale( "en_US" ); + QStringList str = arguments.at( 1 ).toString( context ).split( QRegularExpression( "\\W") ); + QString myStr = QStringLiteral( "%1/%2/%3 %4:%5:%6" ).arg( str[1] ). + arg( str[2] ).arg( str[3] ).arg( str[4] ).arg( str[5] ).arg( str[6] ); + QDateTime date = locale.toDateTime( myStr, QStringLiteral( "MMM/d/yyyy H:m:s" ) ); + + + return KJSString( defaultLocale.toString( date, format ) ); +} + void JSUtil::initType( KJSContext *ctx ) { static bool initialized = false; @@ -65,6 +122,7 @@ g_utilProto = new KJSPrototype(); g_utilProto->defineFunction( ctx, QStringLiteral("crackURL"), crackURL ); + g_utilProto->defineFunction( ctx, QStringLiteral("printd"), printd ); } KJSObject JSUtil::object( KJSContext *ctx ) diff --git a/core/scripter.h b/core/scripter.h --- a/core/scripter.h +++ b/core/scripter.h @@ -32,7 +32,7 @@ Scripter(const Scripter &) = delete; Scripter &operator=(const Scripter &) = delete; - QString execute( ScriptType type, const QString &script ); + void execute( ScriptType type, const QString &script ); void setEvent( Event *event ); Event *event() const; diff --git a/core/scripter.cpp b/core/scripter.cpp --- a/core/scripter.cpp +++ b/core/scripter.cpp @@ -46,7 +46,7 @@ delete d; } -QString Scripter::execute( ScriptType type, const QString &script ) +void Scripter::execute( ScriptType type, const QString &script ) { qCDebug(OkularCoreDebug) << "executing the script:"; #ifdef WITH_KJS @@ -79,10 +79,8 @@ d->m_kjs.reset(new ExecutorKJS( d->m_doc )); } d->m_kjs->execute( builtInScript + script, d->m_event ); - break; - } + } #endif - return QString(); } void Scripter::setEvent( Event *event ) diff --git a/core/signatureutils.h b/core/signatureutils.h --- a/core/signatureutils.h +++ b/core/signatureutils.h @@ -235,7 +235,7 @@ virtual QString reason() const; /** - * The the hash algorithm used for the signature. + * The hash algorithm used for the signature. */ virtual HashAlgorithm hashAlgorithm() const; diff --git a/core/textpage.h b/core/textpage.h --- a/core/textpage.h +++ b/core/textpage.h @@ -29,28 +29,38 @@ class RegularAreaRect; /*! @class TextEntity - * @short Abstract textentity of Okular - * @par The context - * A document can provide different forms of information about textual representation - * of its contents. It can include information about positions of every character on the - * page, this is the best possibility. + * @short Represents a piece of text on a TextPage, containing its textual representation and its bounding box. * - * But also it can provide information only about positions of every word on the page (not the character). - * Furthermore it can provide information only about the position of the whole page's text on the page. + * To enable searching and text selection, a generator can give information about the textual + * content of a Page using a TextPage. + * A TextPage is created using TextEntity objects. + * A TextEntity can represent a single character/glyph, a word, a line, or even the whole page. * - * Also some document types have glyphes - sets of characters rendered as one, so in search they should - * appear as a text but are only one character when drawn on screen. We need to allow this. + * Ideally, every single glyph is represented by its own TextEntity. + * If the textual representation of a graphical glyph contains more than one character, + * the TextEntity must contain the whole string which represents the glyph. + * + * When the Generator has created the TextPage, and it is added to a Page, + * the text entities are reordered to words, lines, and paragraphs, to optimize search and text selection. + * This way, the Generator does not need to care about the logical order of lines or paragraphs. + * + * @par Text Selection/Highlighting + * A TextEntity is the smallest piece of text, which the user can select, or which can be highlighted. + * That is, if the TextEntity represents a word, only the whole word can be selected. + * It would not be possible to select a single glyph of the word, because its bounding box is not known. + * + * @see TextPage, Generator */ class OKULARCORE_EXPORT TextEntity { public: typedef QList List; /** * Creates a new text entity with the given @p text and the - * given @p area. + * given @p boundingBox. */ - TextEntity( const QString &text, NormalizedRect *area ); + TextEntity( const QString &text, NormalizedRect *boundingBox ); /** * Destroys the text entity. @@ -83,9 +93,17 @@ }; /** - * The TextPage class represents the text of a page by - * providing @see TextEntity items for every word/character of - * the page. + * @short Represents the textual information of a Page. Makes search and text selection possible. + * + * A Generator with text support should add a TextPage to every Page. + * For every piece of text, a TextEntity is added, holding the string representation and the bounding box. + * + * Ideally, every TextEntity describes only one glyph. + * A "glyph" is one character in the graphical representation, but the textual representation may consist of multiple characters (like diacritic modifiers). + * + * When the TextPage is added to the Page, the TextEntitys are restructured to optimize text selection. + * + * @see TextEntity */ class OKULARCORE_EXPORT TextPage { @@ -142,26 +160,26 @@ Qt::CaseSensitivity caseSensitivity, const RegularAreaRect *lastRect ); /** - * Text extraction function. + * Text extraction function. Looks for text in the given @p area. * - * Returns: - * - a null string if @p rect is a valid pointer to a null area - * - the whole page text if @p rect is a null pointer - * - the text which is included by rectangular area @p rect otherwise + * @return + * - If @p area points to a valid null area, a null string. + * - If @p area is nullptr, the whole page text as a single string. + * - Otherwise, the text which is included by @p area, as a single string. * Uses AnyPixelTextAreaInclusionBehaviour */ - QString text( const RegularAreaRect *rect = nullptr ) const; + QString text( const RegularAreaRect *area = nullptr ) const; /** - * Text extraction function. + * Text extraction function. Looks for text in the given @p area. * - * Returns: - * - a null string if @p rect is a valid pointer to a null area - * - the whole page text if @p rect is a null pointer - * - the text which is included by rectangular area @p rect otherwise + * @return + * - If @p area points to a valid null area, a null string. + * - If @p area is nullptr, the whole page text as a single string. + * - Otherwise, the text which is included by @p area, as a single string. * @since 0.10 (KDE 4.4) */ - QString text( const RegularAreaRect * rect, TextAreaInclusionBehaviour b ) const; + QString text( const RegularAreaRect * area, TextAreaInclusionBehaviour b ) const; /** * Text entity extraction function. Similar to text() but returns diff --git a/core/textpage.cpp b/core/textpage.cpp --- a/core/textpage.cpp +++ b/core/textpage.cpp @@ -1760,7 +1760,7 @@ { for( int j = 0 ; j < list.length() ; ++j ) { - const WordWithCharacters word = list.at(j); + const WordWithCharacters &word = list.at(j); const QRect wordRect = word.area().geometry(pageWidth,pageHeight); if(topRect.intersects(wordRect)) @@ -1781,7 +1781,7 @@ { for( int j = 0 ; j < list.length() ; ++j ) { - const WordWithCharacters word = list.at(j); + const WordWithCharacters &word = list.at(j); const QRect wordRect = word.area().geometry(pageWidth,pageHeight); if(leftRect.intersects(wordRect)) @@ -1877,10 +1877,10 @@ */ void TextPagePrivate::correctTextOrder() { - //m_page->m_page->width() and m_page->m_page->height() are in pixels at - //100% zoom level, and thus depend on display DPI. We scale pageWidth and - //pageHeight to remove the dependence. Otherwise bugs would be more difficult - //to reproduce and Okular could fail in extreme cases like a large TV with low DPI. + // m_page->width() and m_page->height() are in pixels at + // 100% zoom level, and thus depend on display DPI. + // To avoid Okular failing on lowDPI displays, + // we scale pageWidth and pageHeight so their sum equals 2000. const double scalingFactor = 2000.0 / (m_page->width() + m_page->height()); const int pageWidth = (int) (scalingFactor * m_page->width() ); const int pageHeight = (int) (scalingFactor * m_page->height()); diff --git a/core/textpage_p.h b/core/textpage_p.h --- a/core/textpage_p.h +++ b/core/textpage_p.h @@ -17,6 +17,15 @@ #include class SearchPoint; + +/** + * Memory-optimized storage of a TextEntity. Stores a string and its bounding box. + * + * When a generator adds a TextEntity to a TextPage, it is internally stored as TinyTextEntity. + * TinyTextEntity is also internally used to get the geometry of text selections and highlight areas. + * + * @see TextEntity + */ class TinyTextEntity; class RegionText; diff --git a/core/view.h b/core/view.h --- a/core/view.h +++ b/core/view.h @@ -43,7 +43,10 @@ enum ViewCapability { Zoom, ///< Possibility to get/set the zoom of the view - ZoomModality ///< Possibility to get/set the zoom mode of the view + ZoomModality, ///< Possibility to get/set the zoom mode of the view + Continuous, ///< Possibility to toggle continuous mode @since 1.9 + ViewModeModality, ///< Possibility to get/set the view mode @since 1.9 + TrimMargins ///< Possibility to toggle trim-margins mode @since 1.9 }; /** diff --git a/doc/index.docbook b/doc/index.docbook --- a/doc/index.docbook +++ b/doc/index.docbook @@ -34,8 +34,8 @@ &FDLNotice; - 2019-06-24 - 1.8 (Applications 19.08) + 2019-08-10 + 1.9 (Applications 19.12) &okular; is a &kde; universal document viewer based on &kpdf; code. @@ -390,7 +390,7 @@ Embedded Files - If the current document has some files embedded in it, when you open it a yellow bar + If the current document has some files embedded in it, when you open it a bar will appear above the page view to notify you about the embedded files. @@ -730,8 +730,13 @@ To add a rectangular stamp you can click with the &LMB; and hold to place the top-left point, then drag to place the bottom-right one. - It is possible to define the Opacity and Stamp symbol for the stamp. Just right-click on the stamp icon created and select the Properties menu item. + It is possible to define the Opacity and Stamp symbol for the stamp. Just right-click on the stamp icon created and select the Properties menu item. + + + This feature is experimental. Custom stamps inserted in &PDF; documents are not visible in &PDF; readers other than &okular;. + + @@ -1207,6 +1212,9 @@ Display some basic information about the document, such as title, author, creation date, and details about the fonts used. The available information depends on the type of document. + + Please pay attention on the information about substituting font in the Fonts tab of the Properties dialog. Many problems with font rendering can be solved by installing of the substituted fonts. + diff --git a/generators/chm/generator_chm.cpp b/generators/chm/generator_chm.cpp --- a/generators/chm/generator_chm.cpp +++ b/generators/chm/generator_chm.cpp @@ -371,7 +371,7 @@ new Okular::ObjectRect ( Okular::NormalizedRect(r,xScale,yScale), false, Okular::ObjectRect::Action, - new Okular::GotoAction ( QString::null, viewport))); //krazy:exclude=nullstrassign for old broken gcc + new Okular::GotoAction ( QString(), viewport))); } } } diff --git a/generators/chm/lib/ebook.h b/generators/chm/lib/ebook.h --- a/generators/chm/lib/ebook.h +++ b/generators/chm/lib/ebook.h @@ -188,7 +188,7 @@ /*! * \brief Gets the Title of the page referenced by url. * \param url An URL in ebook file to get title from. Must be absolute. - * \return The title, or QString::null if the URL cannot be found or not a HTML page. + * \return The title, or QString() if the URL cannot be found or not a HTML page. * * \ingroup dataretrieve */ diff --git a/generators/chm/lib/ebook_chm.h b/generators/chm/lib/ebook_chm.h --- a/generators/chm/lib/ebook_chm.h +++ b/generators/chm/lib/ebook_chm.h @@ -151,7 +151,7 @@ /*! * \brief Gets the Title of the page referenced by url. * \param url An URL in ebook file to get title from. Must be absolute. - * \return The title, or QString::null if the URL cannot be found or not a HTML page. + * \return The title, or QString() if the URL cannot be found or not a HTML page. * * \ingroup dataretrieve */ diff --git a/generators/chm/lib/ebook_chm.cpp b/generators/chm/lib/ebook_chm.cpp --- a/generators/chm/lib/ebook_chm.cpp +++ b/generators/chm/lib/ebook_chm.cpp @@ -44,7 +44,7 @@ { m_envOptions = getenv("KCHMVIEWEROPTS"); m_chmFile = NULL; - m_filename = m_font = QString::null; + m_filename = m_font = QString(); m_textCodec = 0; m_textCodecForSpecialFiles = 0; @@ -66,7 +66,7 @@ chm_close( m_chmFile ); m_chmFile = NULL; - m_filename = m_font = QString::null; + m_filename = m_font = QString(); m_home.clear(); m_topicsFile.clear(); @@ -334,7 +334,7 @@ // If we do not need to decode HTML entities, just return. if ( decodeentities ) { - QString htmlentity = QString::null; + QString htmlentity = QString(); bool fill_entity = false; value.reserve (qend - qbegin); // to avoid multiple memory allocations @@ -359,7 +359,7 @@ break; value.append ( decode ); - htmlentity = QString::null; + htmlentity = QString(); fill_entity = false; } else @@ -472,7 +472,7 @@ data.push_back( entry ); } - entry.name = QString::null; + entry.name = QString(); entry.urls.clear(); entry.iconid = defaultimagenum; entry.seealso.clear(); @@ -787,7 +787,7 @@ QMap< QUrl, QString >::const_iterator it = m_url2topics.constFind( url ); if ( it == m_url2topics.constEnd() ) - return QString::null; + return QString(); return it.value(); } diff --git a/generators/chm/lib/ebook_epub.h b/generators/chm/lib/ebook_epub.h --- a/generators/chm/lib/ebook_epub.h +++ b/generators/chm/lib/ebook_epub.h @@ -139,7 +139,7 @@ /*! * \brief Gets the Title of the page referenced by url. * \param url An URL in ebook file to get title from. Must be absolute. - * \return The title, or QString::null if the URL cannot be found or not a HTML page. + * \return The title, or QString() if the URL cannot be found or not a HTML page. * * \ingroup dataretrieve */ diff --git a/generators/chm/lib/ebook_search.cpp b/generators/chm/lib/ebook_search.cpp --- a/generators/chm/lib/ebook_search.cpp +++ b/generators/chm/lib/ebook_search.cpp @@ -203,7 +203,7 @@ // Just add the word; it is most likely a space or terminated by tokenizer. keeper.addTerm( term ); - term = QString::null; + term = QString(); } keeper.addTerm( term ); diff --git a/generators/chm/lib/helper_entitydecoder.cpp b/generators/chm/lib/helper_entitydecoder.cpp --- a/generators/chm/lib/helper_entitydecoder.cpp +++ b/generators/chm/lib/helper_entitydecoder.cpp @@ -212,7 +212,7 @@ if ( !valid ) { qWarning ( "HelperEntityDecoder::decode: could not decode HTML entity '%s'", qPrintable( entity ) ); - return QString::null; + return QString(); } return (QString) (QChar( ascode )); diff --git a/generators/chm/lib/helper_search_index.cpp b/generators/chm/lib/helper_search_index.cpp --- a/generators/chm/lib/helper_search_index.cpp +++ b/generators/chm/lib/helper_search_index.cpp @@ -260,7 +260,7 @@ if ( ch == '&' ) { state = STATE_IN_HTML_ENTITY; - parseentity = QString::null; + parseentity = QString(); continue; } @@ -283,16 +283,16 @@ tokenlist.push_back( parsedbuf.toLower() ); tokenlist.push_back( ch.toLower() ); - parsedbuf = QString::null; + parsedbuf = QString(); continue; } tokenize_buf: // Just add the word; it is most likely a space or terminated by tokenizer. if ( !parsedbuf.isEmpty() ) { tokenlist.push_back( parsedbuf.toLower() ); - parsedbuf = QString::null; + parsedbuf = QString(); } } diff --git a/generators/comicbook/CMakeLists.txt b/generators/comicbook/CMakeLists.txt --- a/generators/comicbook/CMakeLists.txt +++ b/generators/comicbook/CMakeLists.txt @@ -5,7 +5,6 @@ ${CMAKE_CURRENT_SOURCE_DIR}/../.. ) - ########### next target ############### set( okularGenerator_comicbook_PART_SRCS @@ -25,6 +24,10 @@ target_link_libraries(okularGenerator_comicbook KF5::Pty) endif () +if (KArchive_HAVE_LZMA) + target_compile_definitions(okularGenerator_comicbook PRIVATE -DWITH_K7ZIP=1) +endif() + ########### install files ############### install( FILES okularComicbook.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR} ) install( PROGRAMS okularApplication_comicbook.desktop org.kde.mobile.okular_comicbook.desktop DESTINATION ${KDE_INSTALL_APPDIR} ) diff --git a/generators/comicbook/document.cpp b/generators/comicbook/document.cpp --- a/generators/comicbook/document.cpp +++ b/generators/comicbook/document.cpp @@ -18,6 +18,9 @@ #include #include #include +#if WITH_K7ZIP +#include +#endif #include @@ -78,6 +81,17 @@ if ( !processArchive() ) { return false; } +#ifdef WITH_K7ZIP + /** + * We have a 7z archive + */ + } else if ( mime.inherits( QStringLiteral("application/x-cb7") ) || mime.inherits( QStringLiteral("application/x-7z-compressed") ) ) { + mArchive = new K7Zip( fileName ); + + if ( !processArchive() ) { + return false; + } +#endif } else if ( mime.inherits( QStringLiteral("application/x-cbr") ) || mime.inherits( QStringLiteral("application/x-rar") ) || mime.inherits( QStringLiteral("application/vnd.rar") ) ) { if ( !Unrar::isAvailable() ) { mLastErrorString = i18n( "Cannot open document, neither unrar nor unarchiver were found." ); diff --git a/generators/comicbook/libokularGenerator_comicbook.json b/generators/comicbook/libokularGenerator_comicbook.json --- a/generators/comicbook/libokularGenerator_comicbook.json +++ b/generators/comicbook/libokularGenerator_comicbook.json @@ -116,6 +116,7 @@ "application/x-cbz", "application/x-cbr", "application/x-cbt", + "application/x-cb7", "inode/directory" ], "Name": "ComicBook Backend", diff --git a/generators/comicbook/okularApplication_comicbook.desktop b/generators/comicbook/okularApplication_comicbook.desktop --- a/generators/comicbook/okularApplication_comicbook.desktop +++ b/generators/comicbook/okularApplication_comicbook.desktop @@ -1,5 +1,5 @@ [Desktop Entry] -MimeType=application/x-cbz;application/x-cbr;application/x-cbt; +MimeType=application/x-cbz;application/x-cbr;application/x-cbt;application/x-cb7; Terminal=false Name=Okular Name[ar]=اوكلار @@ -165,32 +165,27 @@ InitialPreference=7 Categories=Qt;KDE;Graphics;Viewer; NoDisplay=true -X-KDE-Keywords=cbr, cbz, cbt, Comic Book -X-KDE-Keywords[ca]=cbr, cbz, cbt, Comic Book -X-KDE-Keywords[cs]=cbr, cbz, cbt, Comic Book -X-KDE-Keywords[de]=cbr, cbz, cbt, Comic-Book -X-KDE-Keywords[en_GB]=cbr, cbz, cbt, Comic Book -X-KDE-Keywords[es]=cbr, cbz, cbt, libro de cómic -X-KDE-Keywords[fi]=cbr, cbz, cbt, Comic Book -X-KDE-Keywords[fr]=cbr, cbz, cbt, Comic Book -X-KDE-Keywords[gl]=cbr, cbz, cbt, Comic Book -X-KDE-Keywords[ia]=cbr, cbz, cbt, Comic Book -X-KDE-Keywords[it]=cbr, cbz, cbt, Comic Book -X-KDE-Keywords[ja]=cbr, cbz, cbt, Comic Book -X-KDE-Keywords[ko]=cbr, cbz, cbt, Comic Book,만화책 -X-KDE-Keywords[nb]=cbr, cbz, cbt, tegneserie -X-KDE-Keywords[nl]=cbr, cbz, cbt, Stripverhaal -X-KDE-Keywords[nn]=cbr, cbz, cbt, Comic Book -X-KDE-Keywords[pl]=cbr, cbz, cbt, Komiks -X-KDE-Keywords[pt]=cbr, cbz, cbt, Comic Book -X-KDE-Keywords[pt_BR]=cbr, cbz, cbt, Comic Book -X-KDE-Keywords[sk]=cbr, cbz, cbt, Comic Book, Kniha komiksov -X-KDE-Keywords[sr]=cbr, cbz, cbt, Comic Book,комикбук -X-KDE-Keywords[sr@ijekavian]=cbr, cbz, cbt, Comic Book,комикбук -X-KDE-Keywords[sr@ijekavianlatin]=cbr, cbz, cbt, Comic Book,ComicBook -X-KDE-Keywords[sr@latin]=cbr, cbz, cbt, Comic Book,ComicBook -X-KDE-Keywords[sv]=cbr, cbz, cbt, Comic Book -X-KDE-Keywords[uk]=cbr,cbz,cbt,Comic Book,комікс -X-KDE-Keywords[x-test]=xxcbrxx,xx cbzxx,xx cbtxx,xx Comic Bookxx -X-KDE-Keywords[zh_CN]=cbr, cbz, cbt, Comic Book,漫画书,漫画,连环画 -X-KDE-Keywords[zh_TW]=cbr, cbz, cbt, Comic Book, 漫畫書 +X-KDE-Keywords=cbr, cbz, cbt, cb7, Comic Book +X-KDE-Keywords[ca]=cbr, cbz, cbt, cb7, Comic Book +X-KDE-Keywords[cs]=cbr, cbz, cbt, cb7, Comic Book +X-KDE-Keywords[de]=cbr, cbz, cbt, cb7, Comic-Book +X-KDE-Keywords[el]=cbr, cbz, cbt, cb7, Comic Book +X-KDE-Keywords[en_GB]=cbr, cbz, cbt, cb7, Comic Book +X-KDE-Keywords[es]=cbr, cbz, cbt, cb7, libro de cómic +X-KDE-Keywords[eu]=cbr, cbz, cbt, cb7, komiki-liburua +X-KDE-Keywords[fr]=cbr, cbz, cbt, cb7, Comic Book +X-KDE-Keywords[gl]=cbr, cbz, cbt, cb7, Comic Book +X-KDE-Keywords[ia]=cbr, cbz, cbt, cb7, Comic Book +X-KDE-Keywords[it]=cbr, cbz, cbt, cb7, Comic Book +X-KDE-Keywords[ko]=cbr, cbz, cbt, cb7, Comic Book, 만화책, 만화 +X-KDE-Keywords[nl]=cbr, cbz, cbt, cbz, Stripverhaal +X-KDE-Keywords[nn]=cbr, cbz, cbt, cb7, Comic Book +X-KDE-Keywords[pl]=cbr, cbz, cbt, cb7, Komiks +X-KDE-Keywords[pt]=cbr, cbz, cbt, cb7, Comic Book +X-KDE-Keywords[pt_BR]=cbr, cbz, cbt, cb7, Comic Book +X-KDE-Keywords[sk]=cbr, cbz, cbt, cb7, Kniha komiksov +X-KDE-Keywords[sv]=cbr, cbz, cbt, cb7, Comic Book +X-KDE-Keywords[uk]=cbr,cbz,cbt,cb7,комікс +X-KDE-Keywords[x-test]=xxcbrxx,xx cbzxx,xx cbtxx,xx cb7xx,xx Comic Bookxx +X-KDE-Keywords[zh_CN]=cbr, cbz, cbt, cb7, Comic Book, 漫画书, 漫画, 连环画 +X-KDE-Keywords[zh_TW]=cbr, cbz, cbt, cb7, Comic Book, 漫畫, 漫畫書 diff --git a/generators/comicbook/okularComicbook.desktop b/generators/comicbook/okularComicbook.desktop --- a/generators/comicbook/okularComicbook.desktop +++ b/generators/comicbook/okularComicbook.desktop @@ -62,4 +62,4 @@ X-KDE-ServiceTypes=KParts/ReadOnlyPart X-KDE-Library=okularpart Type=Service -MimeType=application/x-cbz;application/x-cbr;application/x-cbt; +MimeType=application/x-cbz;application/x-cbr;application/x-cbt;application/x-cb7; diff --git a/generators/comicbook/org.kde.mobile.okular_comicbook.desktop b/generators/comicbook/org.kde.mobile.okular_comicbook.desktop --- a/generators/comicbook/org.kde.mobile.okular_comicbook.desktop +++ b/generators/comicbook/org.kde.mobile.okular_comicbook.desktop @@ -1,5 +1,5 @@ [Desktop Entry] -MimeType=application/x-cbz;application/x-cbr;application/x-cbt; +MimeType=application/x-cbz;application/x-cbr;application/x-cbt;application/x-cb7; Name=Reader Name[ar]=التصيير Name[bg]=Четец @@ -165,23 +165,18 @@ X-KDE-Keywords[fi]=cbr, cbz, cbt, Comic Book X-KDE-Keywords[fr]=cbr, cbz, cbt, Comic Book X-KDE-Keywords[gl]=cbr, cbz, cbt, Comic Book -X-KDE-Keywords[ia]=cbr, cbz, cbt, Comic Book X-KDE-Keywords[it]=cbr, cbz, cbt, Comic Book -X-KDE-Keywords[ja]=cbr, cbz, cbt, Comic Book -X-KDE-Keywords[ko]=cbr, cbz, cbt, Comic Book,만화책 X-KDE-Keywords[nb]=cbr, cbz, cbt, tegneserie X-KDE-Keywords[nl]=cbr, cbz, cbt, Stripverhaal X-KDE-Keywords[nn]=cbr, cbz, cbt, Comic Book -X-KDE-Keywords[pl]=cbr, cbz, cbt, Komiks X-KDE-Keywords[pt]=cbr, cbz, cbt, Comic Book X-KDE-Keywords[pt_BR]=cbr, cbz, cbt, Comic Book -X-KDE-Keywords[sk]=cbr, cbz, cbt, Comic Book, Kniha komiksov X-KDE-Keywords[sr]=cbr, cbz, cbt, Comic Book,комикбук X-KDE-Keywords[sr@ijekavian]=cbr, cbz, cbt, Comic Book,комикбук X-KDE-Keywords[sr@ijekavianlatin]=cbr, cbz, cbt, Comic Book,ComicBook X-KDE-Keywords[sr@latin]=cbr, cbz, cbt, Comic Book,ComicBook X-KDE-Keywords[sv]=cbr, cbz, cbt, Comic Book -X-KDE-Keywords[uk]=cbr,cbz,cbt,Comic Book,комікс +X-KDE-Keywords[uk]=cbr,cbz,cbt,комікс X-KDE-Keywords[x-test]=xxcbrxx,xx cbzxx,xx cbtxx,xx Comic Bookxx X-KDE-Keywords[zh_CN]=cbr, cbz, cbt, Comic Book,漫画书,漫画,连环画 -X-KDE-Keywords[zh_TW]=cbr, cbz, cbt, Comic Book, 漫畫書 +X-KDE-Keywords[zh_TW]=cbr, cbz, cbt, Comic Book, 漫畫, 漫畫書 diff --git a/generators/comicbook/org.kde.okular-comicbook.metainfo.xml b/generators/comicbook/org.kde.okular-comicbook.metainfo.xml --- a/generators/comicbook/org.kde.okular-comicbook.metainfo.xml +++ b/generators/comicbook/org.kde.okular-comicbook.metainfo.xml @@ -78,6 +78,7 @@ application/x-cbr application/x-cbz application/x-cbt + application/x-cb7 https://okular.kde.org diff --git a/generators/dvi/dviRenderer.cpp b/generators/dvi/dviRenderer.cpp --- a/generators/dvi/dviRenderer.cpp +++ b/generators/dvi/dviRenderer.cpp @@ -282,7 +282,7 @@ QVBoxLayout *topcontentsVBoxLayout = new QVBoxLayout(topcontents); topcontentsVBoxLayout->setContentsMargins(0, 0, 0, 0); topcontentsVBoxLayout->setSpacing(KDialog::spacingHint()*2); - topcontentsVBoxLayout->setMargin(KDialog::marginHint()*2); + topcontentsVBoxLayout->setContentsMargins(KDialog::marginHint()*2, KDialog::marginHint()*2, KDialog::marginHint()*2, KDialog::marginHint()*2); QWidget *contents = new QWidget(topcontents); topcontentsVBoxLayout->addWidget(contents); diff --git a/generators/dvi/psgs.cpp b/generators/dvi/psgs.cpp --- a/generators/dvi/psgs.cpp +++ b/generators/dvi/psgs.cpp @@ -95,7 +95,7 @@ #endif if (pageList.value(page) == 0) { - pageInfo *info = new pageInfo(QString::null); //krazy:exclude=nullstrassign for old broken gcc + pageInfo *info = new pageInfo(QString()); info->background = background_color; if (permanent) info->permanentBackground = background_color; diff --git a/generators/mobipocket/converter.h b/generators/mobipocket/converter.h --- a/generators/mobipocket/converter.h +++ b/generators/mobipocket/converter.h @@ -25,7 +25,7 @@ QTextDocument *convert( const QString &fileName ) override; private: - void handleMetadata(const QMap metadata); + void handleMetadata(const QMap &metadata); }; } diff --git a/generators/mobipocket/converter.cpp b/generators/mobipocket/converter.cpp --- a/generators/mobipocket/converter.cpp +++ b/generators/mobipocket/converter.cpp @@ -31,7 +31,7 @@ { } -void Converter::handleMetadata(const QMap metadata) +void Converter::handleMetadata(const QMap &metadata) { QMapIterator it(metadata); while (it.hasNext()) { diff --git a/generators/plucker/unpluck/qunpluck.cpp b/generators/plucker/unpluck/qunpluck.cpp --- a/generators/plucker/unpluck/qunpluck.cpp +++ b/generators/plucker/unpluck/qunpluck.cpp @@ -134,7 +134,7 @@ mInfo.insert( QStringLiteral("name"), QString::fromLocal8Bit(plkr_GetName( mDocument ) )); mInfo.insert( QStringLiteral("title"), QString::fromLocal8Bit(plkr_GetTitle( mDocument ) )); mInfo.insert( QStringLiteral("author"), QString::fromLocal8Bit(plkr_GetAuthor( mDocument ) )); - mInfo.insert( QStringLiteral("time"), QDateTime::fromTime_t( plkr_GetPublicationTime( mDocument ) ).toString() ); + mInfo.insert( QStringLiteral("time"), QDateTime::fromSecsSinceEpoch( plkr_GetPublicationTime( mDocument ) ).toString() ); AddRecord( plkr_GetHomeRecordID( mDocument ) ); diff --git a/generators/poppler/CMakeLists.txt b/generators/poppler/CMakeLists.txt --- a/generators/poppler/CMakeLists.txt +++ b/generators/poppler/CMakeLists.txt @@ -144,6 +144,28 @@ } " HAVE_POPPLER_0_73) +check_cxx_source_compiles(" +#include +#include +int main() +{ + Poppler::FormFieldIcon icon(nullptr); + Poppler::FormFieldButton *button; + button->setIcon( icon ); + return 0; +} +" HAVE_POPPLER_0_79) + +check_cxx_source_compiles(" +#include +int main() +{ + Poppler::FontInfo info; + QString substituteName = info.substituteName(); + return 0; +} +" HAVE_POPPLER_0_80) + configure_file( ${CMAKE_CURRENT_SOURCE_DIR}/config-okular-poppler.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-okular-poppler.h diff --git a/generators/poppler/annots.cpp b/generators/poppler/annots.cpp --- a/generators/poppler/annots.cpp +++ b/generators/poppler/annots.cpp @@ -130,7 +130,7 @@ delete ppl_page; // Set pointer to poppler annotation as native Id - okl_ann->setNativeId( qVariantFromValue( ppl_ann ) ); + okl_ann->setNativeId( QVariant::fromValue( ppl_ann ) ); okl_ann->setDisposeDataFunction( disposeAnnotation ); qCDebug(OkularPdfDebug) << okl_ann->uniqueName(); @@ -265,7 +265,7 @@ ppl_page->removeAnnotation( ppl_ann ); // Also destroys ppl_ann delete ppl_page; - okl_ann->setNativeId( qVariantFromValue(0) ); // So that we don't double-free in disposeAnnotation + okl_ann->setNativeId( QVariant::fromValue(0) ); // So that we don't double-free in disposeAnnotation qCDebug(OkularPdfDebug) << okl_ann->uniqueName(); } @@ -423,7 +423,7 @@ // TODO clone revisions if ( tieToOkularAnn ) { - annotation->setNativeId( qVariantFromValue( ann ) ); + annotation->setNativeId( QVariant::fromValue( ann ) ); annotation->setDisposeDataFunction( disposeAnnotation ); } } diff --git a/generators/poppler/config-okular-poppler.h.cmake b/generators/poppler/config-okular-poppler.h.cmake --- a/generators/poppler/config-okular-poppler.h.cmake +++ b/generators/poppler/config-okular-poppler.h.cmake @@ -45,3 +45,9 @@ /* Defined if we have the 0.73 version of the Poppler library */ #cmakedefine HAVE_POPPLER_0_73 1 + +/* Defined if we have the 0.79 version of the Poppler library */ +#cmakedefine HAVE_POPPLER_0_79 1 + +/* Defined if we have the 0.80 version of the Poppler library */ +#cmakedefine HAVE_POPPLER_0_80 1 diff --git a/generators/poppler/formfields.h b/generators/poppler/formfields.h --- a/generators/poppler/formfields.h +++ b/generators/poppler/formfields.h @@ -11,6 +11,7 @@ #define _OKULAR_GENERATOR_PDF_FORMFIELDS_H_ #include +#include #include "core/form.h" class PopplerFormFieldButton : public Okular::FormFieldButton @@ -24,17 +25,29 @@ int id() const override; QString name() const override; QString uiName() const override; + QString fullyQualifiedName() const override; bool isReadOnly() const override; void setReadOnly( bool value ) override; bool isVisible() const override; void setVisible( bool value ) override; + bool isPrintable() const override; + void setPrintable( bool value ) override; // inherited from Okular::FormFieldButton ButtonType buttonType() const override; QString caption() const override; bool state() const override; void setState( bool state ) override; QList< int > siblings() const override; + void setIcon( Okular::FormField *field ) override; +#ifdef HAVE_POPPLER_0_79 + /* + * Supported only in newer versions of Poppler library. + * + * @since 1.9 + */ + Poppler::FormFieldIcon icon() const; +#endif private: Poppler::FormFieldButton * m_field; @@ -54,15 +67,19 @@ int id() const override; QString name() const override; QString uiName() const override; + QString fullyQualifiedName() const override; bool isReadOnly() const override; void setReadOnly( bool value ) override; bool isVisible() const override; void setVisible( bool value ) override; + bool isPrintable() const override; + void setPrintable( bool value ) override; // inherited from Okular::FormFieldText Okular::FormFieldText::TextType textType() const override; QString text() const override; void setText( const QString& text ) override; + void setAppearanceText( const QString& text ) override; bool isPassword() const override; bool isRichText() const override; int maximumLength() const override; @@ -87,10 +104,13 @@ int id() const override; QString name() const override; QString uiName() const override; + QString fullyQualifiedName() const override; bool isReadOnly() const override; void setReadOnly( bool value ) override; bool isVisible() const override; void setVisible( bool value ) override; + bool isPrintable() const override; + void setPrintable( bool value ) override; // inherited from Okular::FormFieldChoice ChoiceType choiceType() const override; @@ -122,6 +142,7 @@ int id() const override; QString name() const override; QString uiName() const override; + QString fullyQualifiedName() const override; bool isReadOnly() const override; bool isVisible() const override; diff --git a/generators/poppler/formfields.cpp b/generators/poppler/formfields.cpp --- a/generators/poppler/formfields.cpp +++ b/generators/poppler/formfields.cpp @@ -77,6 +77,11 @@ return m_field->uiName(); } +QString PopplerFormFieldButton::fullyQualifiedName() const +{ + return m_field->fullyQualifiedName(); +} + bool PopplerFormFieldButton::isReadOnly() const { return m_field->isReadOnly(); @@ -105,6 +110,24 @@ #endif } +bool PopplerFormFieldButton::isPrintable() const +{ +#ifdef HAVE_POPPLER_0_79 + return m_field->isPrintable(); +#else + return true; +#endif +} + +void PopplerFormFieldButton::setPrintable( bool value ) +{ +#ifdef HAVE_POPPLER_0_79 + m_field->setPrintable( value ); +#else + Q_UNUSED( value ); +#endif +} + Okular::FormFieldButton::ButtonType PopplerFormFieldButton::buttonType() const { switch ( m_field->buttonType() ) @@ -139,6 +162,26 @@ return m_field->siblings(); } +#ifdef HAVE_POPPLER_0_79 +Poppler::FormFieldIcon PopplerFormFieldButton::icon() const +{ + return m_field->icon(); +} +#endif + +void PopplerFormFieldButton::setIcon( Okular::FormField *field ) +{ +#ifdef HAVE_POPPLER_0_79 + if( field->type() == Okular::FormField::FormButton ) + { + PopplerFormFieldButton *button = static_cast< PopplerFormFieldButton * >( field ); + m_field->setIcon( button->icon() ); + } +#else + Q_UNUSED( field ); +#endif +} + PopplerFormFieldText::PopplerFormFieldText( Poppler::FormFieldText * field ) : Okular::FormFieldText(), m_field( field ) @@ -173,6 +216,11 @@ return m_field->uiName(); } +QString PopplerFormFieldText::fullyQualifiedName() const +{ + return m_field->fullyQualifiedName(); +} + bool PopplerFormFieldText::isReadOnly() const { return m_field->isReadOnly(); @@ -201,6 +249,25 @@ #endif } +bool PopplerFormFieldText::isPrintable() const +{ +#ifdef HAVE_POPPLER_0_79 + return m_field->isPrintable(); +#else + return true; +#endif +} + +void PopplerFormFieldText::setPrintable( bool value ) +{ +#ifdef HAVE_POPPLER_0_79 + m_field->setPrintable( value ); +#else + Q_UNUSED( value ); +#endif +} + + Okular::FormFieldText::TextType PopplerFormFieldText::textType() const { switch ( m_field->textType() ) @@ -225,6 +292,16 @@ m_field->setText( text ); } +void PopplerFormFieldText::setAppearanceText( const QString& text ) +{ +#ifdef HAVE_POPPLER_0_80 + m_field->setAppearanceText( text ); +#else + Q_UNUSED( text ); +#endif +} + + bool PopplerFormFieldText::isPassword() const { return m_field->isPassword(); @@ -284,6 +361,11 @@ return m_field->uiName(); } +QString PopplerFormFieldChoice::fullyQualifiedName() const +{ + return m_field->fullyQualifiedName(); +} + bool PopplerFormFieldChoice::isReadOnly() const { return m_field->isReadOnly(); @@ -312,6 +394,24 @@ #endif } +bool PopplerFormFieldChoice::isPrintable() const +{ +#ifdef HAVE_POPPLER_0_79 + return m_field->isPrintable(); +#else + return true; +#endif +} + +void PopplerFormFieldChoice::setPrintable( bool value ) +{ +#ifdef HAVE_POPPLER_0_79 + m_field->setPrintable( value ); +#else + Q_UNUSED( value ); +#endif +} + Okular::FormFieldChoice::ChoiceType PopplerFormFieldChoice::choiceType() const { switch ( m_field->choiceType() ) @@ -417,6 +517,11 @@ return m_field->uiName(); } +QString PopplerFormFieldSignature::fullyQualifiedName() const +{ + return m_field->fullyQualifiedName(); +} + bool PopplerFormFieldSignature::isReadOnly() const { return m_field->isReadOnly(); diff --git a/generators/poppler/generator_pdf.cpp b/generators/poppler/generator_pdf.cpp --- a/generators/poppler/generator_pdf.cpp +++ b/generators/poppler/generator_pdf.cpp @@ -942,6 +942,9 @@ { 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() ); diff --git a/generators/poppler/pdfsignatureutils.cpp b/generators/poppler/pdfsignatureutils.cpp --- a/generators/poppler/pdfsignatureutils.cpp +++ b/generators/poppler/pdfsignatureutils.cpp @@ -252,7 +252,7 @@ QDateTime PopplerSignatureInfo::signingTime() const { - return QDateTime::fromTime_t( m_info.signingTime() ); + return QDateTime::fromSecsSinceEpoch( m_info.signingTime() ); } QByteArray PopplerSignatureInfo::signature() const diff --git a/generators/xps/generator_xps.cpp b/generators/xps/generator_xps.cpp --- a/generators/xps/generator_xps.cpp +++ b/generators/xps/generator_xps.cpp @@ -986,7 +986,7 @@ brush = QBrush( image ); brush.setTransform( viewboxMatrix.inverted() * viewportMatrix ); - node.data = qVariantFromValue( brush ); + node.data = QVariant::fromValue( brush ); } void XpsHandler::processPath( XpsRenderNode &node ) @@ -1173,7 +1173,7 @@ } if ( !geom->paths.isEmpty() ) { - node.data = qVariantFromValue( geom ); + node.data = QVariant::fromValue( geom ); } else { delete geom; } @@ -1262,7 +1262,7 @@ } if ( !path.isEmpty() ) { - node.data = qVariantFromValue( new XpsPathFigure( path, isFilled ) ); + node.data = QVariant::fromValue( new XpsPathFigure( path, isFilled ) ); } } @@ -1296,7 +1296,7 @@ processPath( node ); } else if (node.name == QLatin1String("MatrixTransform")) { //TODO Ignoring x:key - node.data = qVariantFromValue( QTransform( attsToMatrix( node.attributes.value( QStringLiteral("Matrix") ) ) ) ); + node.data = QVariant::fromValue( QTransform( attsToMatrix( node.attributes.value( QStringLiteral("Matrix") ) ) ) ); } else if ((node.name == QLatin1String("Canvas.RenderTransform")) || (node.name == QLatin1String("Glyphs.RenderTransform")) || (node.name == QLatin1String("Path.RenderTransform"))) { QVariant data = node.getRequiredChildData( QStringLiteral("MatrixTransform") ); if (data.canConvert()) { @@ -1310,7 +1310,7 @@ processStroke( node ); } else if (node.name == QLatin1String("SolidColorBrush")) { //TODO Ignoring opacity, x:key - node.data = qVariantFromValue( QBrush( QColor( hexToRgba( node.attributes.value( QStringLiteral("Color") ).toLatin1() ) ) ) ); + node.data = QVariant::fromValue( QBrush( QColor( hexToRgba( node.attributes.value( QStringLiteral("Color") ).toLatin1() ) ) ) ); } else if (node.name == QLatin1String("ImageBrush")) { processImageBrush( node ); } else if (node.name == QLatin1String("ImageBrush.Transform")) { @@ -1324,7 +1324,7 @@ qgrad->setStart( start ); qgrad->setFinalStop( end ); applySpreadStyleToQGradient( node.attributes.value( QStringLiteral("SpreadMethod") ), qgrad ); - node.data = qVariantFromValue( QBrush( *qgrad ) ); + node.data = QVariant::fromValue( QBrush( *qgrad ) ); delete qgrad; } } else if (node.name == QLatin1String("RadialGradientBrush")) { @@ -1340,7 +1340,7 @@ // TODO what in case of different radii? qgrad->setRadius( qMin( radiusX, radiusY ) ); applySpreadStyleToQGradient( node.attributes.value( QStringLiteral("SpreadMethod") ), qgrad ); - node.data = qVariantFromValue( QBrush( *qgrad ) ); + node.data = QVariant::fromValue( QBrush( *qgrad ) ); delete qgrad; } } else if (node.name == QLatin1String("LinearGradientBrush.GradientStops")) { @@ -1354,7 +1354,7 @@ if ( !gradients.isEmpty() ) { QLinearGradient * qgrad = new QLinearGradient(); addXpsGradientsToQGradient( gradients, qgrad ); - node.data = qVariantFromValue< QGradient * >( qgrad ); + node.data = QVariant::fromValue< QGradient * >( qgrad ); } } else if (node.name == QLatin1String("RadialGradientBrush.GradientStops")) { QList gradients; @@ -1367,7 +1367,7 @@ if ( !gradients.isEmpty() ) { QRadialGradient * qgrad = new QRadialGradient(); addXpsGradientsToQGradient( gradients, qgrad ); - node.data = qVariantFromValue< QGradient * >( qgrad ); + node.data = QVariant::fromValue< QGradient * >( qgrad ); } } else if (node.name == QLatin1String("PathFigure")) { processPathFigure( node ); diff --git a/generators/xps/org.kde.okular-xps.metainfo.xml b/generators/xps/org.kde.okular-xps.metainfo.xml --- a/generators/xps/org.kde.okular-xps.metainfo.xml +++ b/generators/xps/org.kde.okular-xps.metainfo.xml @@ -20,6 +20,7 @@ XPS XPS XPS + XPS എക്സ് പി എസ് XPS XPS @@ -55,6 +56,7 @@ Adde supporto per leger documentos XPS Aggiunge il supporto per la lettura di documenti XPS XPS 문서 읽기 지원 추가 + Prideda XPS dokumentų skaitymo palaikymą എക്‌സ് പി എസ് പ്രമാണങ്ങൾ വായിക്കാൻ പിന്തുണാ ചേർക്കുന്നു Voegt ondersteuning voor lezen van XPS-documenten toe Legg til støtte for å lesa XPS-dokument diff --git a/mobile/android/src/OpenFileActivity.java b/mobile/android/src/OpenFileActivity.java --- a/mobile/android/src/OpenFileActivity.java +++ b/mobile/android/src/OpenFileActivity.java @@ -8,6 +8,8 @@ import android.net.Uri; import android.app.Activity; +import java.io.FileNotFoundException; + import org.qtproject.qt5.android.bindings.QtActivity; class FileClass @@ -17,6 +19,20 @@ public class OpenFileActivity extends QtActivity { + + public String contentUrlToFd(String url) + { + try { + ContentResolver resolver = getBaseContext().getContentResolver(); + ParcelFileDescriptor fdObject = resolver.openFileDescriptor(Uri.parse(url), "r"); + return "fd:///" + fdObject.detachFd(); + } catch (FileNotFoundException e) { + Log.e("Okular", "Cannot find file", e); + } + return ""; + } + + private void displayUri(Uri uri) { if (uri == null) @@ -51,27 +67,4 @@ displayUri(bundleIntent.getData()); } } - - private static int OpenDocumentRequest = 42; - - public static void openFile(Activity context, String title, String mimes) - { - Intent intent = new Intent(); - intent.setAction(Intent.ACTION_GET_CONTENT); - intent.setType("application/pdf"); - Log.v("Okular", "opening: " + mimes); - intent.putExtra(Intent.EXTRA_MIME_TYPES, mimes.split(";")); - - context.startActivityForResult(intent, OpenDocumentRequest); - } - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent intent) { - Log.v("Okular", "Activity Result: " + String.valueOf(requestCode) + " with code: " + String.valueOf(resultCode)); - if (resultCode == RESULT_OK && requestCode == OpenDocumentRequest) { - Uri uri = intent.getData(); - Log.v("Okular", "Opening document: " + uri.toString()); - displayUri(uri); - } - } } diff --git a/mobile/app/org.kde.okular.kirigami.appdata.xml b/mobile/app/org.kde.okular.kirigami.appdata.xml --- a/mobile/app/org.kde.okular.kirigami.appdata.xml +++ b/mobile/app/org.kde.okular.kirigami.appdata.xml @@ -7,6 +7,7 @@ Okular Mobile Okular Mobile Okular Mobile + Okular για κινητά Okular Mobile Okular Mobile Okular Mugikorra @@ -33,6 +34,7 @@ Visualitzador de documents Prohlížeč dokumentů Dokumentenbetrachter + Πρόγραμμα προβολής εγγράφων Document Viewer Visor de documentos Dokumentu erakuslea @@ -61,6 +63,7 @@

L'Okular és un visualitzador universal de documents desenvolupat pel KDE. L'Okular funciona en diverses plataformes, incloent-hi però sense limitar en el Linux, Windows, Mac OS X, *BSD, etc.

L'Okular és un visualitzador universal de documents desenvolupat pel KDE. L'Okular funciona en diverses plataformes, incloent-hi però sense limitar en el Linux, Windows, Mac OS X, *BSD, etc.

Okular ist ein universeller Dokumentenbetrachter, der von KDE entwickelt wird. Okular ist auf mehreren Plattformen verfügbar, darunter auch Linux, Windows, Mac OS X, *BSD usw.

+

Το Okular είναι ένα καθολικό πρόγραμμα προβολής εγγράφων που αναπτύχθηκε από το KDE. Το Okular λειτουργεί σε πολλές πλατφόρμες, συμπεριλαμβανομένων αλλά όχι μόνο των Linux, Windows, Mac OS X, *BSD, κλπ.

Okular is a universal document viewer developed by KDE. Okular works on multiple platforms, including but not limited to Linux, Windows, Mac OS X, *BSD, etc.

Okular es un visor universal de documentos desarrollado por KDE. Okular funciona en diversas plataformas, incluidas Linux, Windows, Mac OS X, *BSD, etc.

Okular KDEk garatutako dokumentu erakusle unibertsal bat da. Okular hainbat plataformatan dabil, haien artean baino ez soilik haietara mugatua, Linux, windows, Mac OS X, *BSD, etab.

@@ -87,6 +90,7 @@

Característiques:

Vlastnosti:

Funktionen:

+

Χαρακτηριστικά:

Features:

Funciones:

Eginbideak:

@@ -114,6 +118,7 @@
  • Formats acceptats: PDF, PS, Tiff, CHM, DjVu, imatges, DVI, XPS, ODT, Fiction Book, llibres de còmic, Plucker, EPub, Fax
  • Podporované formáty: PDF, PS, Tiff, CHM, DjVu, obrázky, DVI, XPS, ODT, Fiction Book, Comic Book, Plucker, EPub, Fax
  • Unterstützte Formate: PDF, PS, Tiff, CHM, DjVu, Images, DVI, XPS, ODT, Fiction Book, Comic Book, Plucker, EPub, Fax
  • +
  • Υποστηριζόμενοι τύποι αποθήκευσης: PDF, PS, Tiff, CHM, DjVu, Images, DVI, XPS, ODT, Fiction Book, Comic Book, Plucker, EPub, Fax
  • Supported Formats: PDF, PS, Tiff, CHM, DjVu, Images, DVI, XPS, ODT, Fiction Book, Comic Book, Plucker, EPub, Fax
  • Formatos permitidos: PDF, PS, Tiff, CHM, DjVu, imágenes, DVI, XPS, ODT, Fiction Book, libros de cómics, Plucker, EPub, Fax
  • Onartutako formatuak: PDF, PS, Tiff, CHM, DjVu, irudiak, DVI, XPS, ODT, FictionBook, Comic Book, Plucker, EPub, Fax
  • @@ -139,6 +144,7 @@
  • Barra lateral amb el contingut, miniatures, revisions i punts
  • Barra lateral amb el contingut, miniatures, revisions i punts
  • Seitenleiste mit Inhalt, Vorschaubildern, Rezensionen und Lesezeichen
  • +
  • Πλευρική γραμμή με περιεχόμενα, εικόνες επισκόπησης, αναλύσεις και σελιδοδείκτες
  • Sidebar with contents, thumbnails, reviews and bookmarks
  • Barra lateral con contenido, miniaturas, revisiones y marcadores
  • Alboko-barra edukiekin, koadro-txikiekin, iritziekin eta laster-markekin
  • @@ -164,6 +170,7 @@
  • Admet anotacions
  • Admet anotacions
  • Unterstützung für Anmerkungen
  • +
  • Υποστήριξη σημειώσεων
  • Annotations support
  • Permite el uso de notas
  • Idatzoharrak onartzen ditu
  • @@ -193,6 +200,7 @@ Lectura de manuals a l'Okular Lectura de manuals a l'Okular Lesen eines Handbuchs in Okular + Ανάγνωση οδηγού χρήσης στο Okular Reading manual in Okular Lectura del manual en Okular Eskuliburua Okularren irakurtzea diff --git a/mobile/app/package/contents/ui/MainView.qml b/mobile/app/package/contents/ui/MainView.qml --- a/mobile/app/package/contents/ui/MainView.qml +++ b/mobile/app/package/contents/ui/MainView.qml @@ -30,9 +30,10 @@ bottomPadding: 0 actions.main: Kirigami.Action { - iconName: "bookmarks-organize" + icon.name: pageArea.page.bookmarked ? "bookmark-remove" : "bookmarks-organize" checkable: true - onCheckedChanged: pageArea.page.bookmarked = checked; + onCheckedChanged: pageArea.page.bookmarked = checked + text: pageArea.page.bookmarked ? i18n("Remove bookmark") : i18n("Bookmark this page") } Okular.DocumentView { @@ -61,6 +62,6 @@ right: parent.right bottom: parent.bottom } - value: documentItem.pageCount != 0 ? ((documentItem.currentPage+1) / documentItem.pageCount) : 0 + value: documentItem.pageCount !== 0 ? ((documentItem.currentPage+1) / documentItem.pageCount) : 0 } } diff --git a/mobile/app/package/contents/ui/TableOfContents.qml b/mobile/app/package/contents/ui/TableOfContents.qml --- a/mobile/app/package/contents/ui/TableOfContents.qml +++ b/mobile/app/package/contents/ui/TableOfContents.qml @@ -18,25 +18,24 @@ */ import QtQuick 2.1 -import QtQuick.Controls 2.0 as QQC2 +import QtQuick.Controls 2.2 as QQC2 import org.kde.kirigami 2.0 as Kirigami Kirigami.Page { id: root leftPadding: 0 topPadding: 0 rightPadding: 0 bottomPadding: 0 - property alias contentY: flickable.contentY - property alias contentHeight: flickable.contentHeight + property alias tocContentY: flickable.contentY + property alias tocContentHeight: flickable.contentHeight QQC2.ToolBar { id: toolBarContent width: root.width - height: searchField.height - TextField { + contentItem: QQC2.TextField { id: searchField - anchors.centerIn: parent + placeholderText: i18n("Search...") } } QQC2.ScrollView { diff --git a/mobile/app/package/contents/ui/Thumbnails.qml b/mobile/app/package/contents/ui/Thumbnails.qml --- a/mobile/app/package/contents/ui/Thumbnails.qml +++ b/mobile/app/package/contents/ui/Thumbnails.qml @@ -28,10 +28,9 @@ QQC2.ToolBar { id: toolBarContent width: root.width - height: searchField.height - QQC2.TextField { + contentItem: QQC2.TextField { id: searchField - anchors.fill: parent + placeholderText: i18n("Search...") enabled: documentItem ? documentItem.supportsSearching : false onTextChanged: { if (text.length > 2) { diff --git a/mobile/app/package/contents/ui/main.qml b/mobile/app/package/contents/ui/main.qml --- a/mobile/app/package/contents/ui/main.qml +++ b/mobile/app/package/contents/ui/main.qml @@ -23,7 +23,7 @@ import org.kde.kirigami 2.0 as Kirigami import org.kde.okular.app 2.0 -Kirigami.AbstractApplicationWindow { +Kirigami.ApplicationWindow { id: fileBrowserRoot visible: true @@ -45,27 +45,9 @@ Kirigami.Action { text: i18n("Open...") icon.name: "document-open" - visible: Qt.platform.os !== "android" onTriggered: { fileDialog.open() } - }, - Kirigami.Action { - text: i18n("Open...") - icon.name: "document-open" - visible: p0.enabled - readonly property var p0: Connections { - target: AndroidInstance - enabled: AndroidInstance.hasOwnProperty("openFile") - onOpenUri: { - console.log("open uri!", uri) - documentItem.url = uri - } - } - onTriggered: { -// var mimetypes = Okular.Okular.mimeTypes.join(",") - AndroidInstance.openFile(i18n("Document to open..."), "*/*") - } } ] } diff --git a/mobile/components/CMakeLists.txt b/mobile/components/CMakeLists.txt --- a/mobile/components/CMakeLists.txt +++ b/mobile/components/CMakeLists.txt @@ -37,6 +37,10 @@ okularcore ) +if(ANDROID) + target_link_libraries(okularplugin Qt5::AndroidExtras) +endif() + install(TARGETS okularplugin DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/okular) install(FILES qmldir DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/okular) install(FILES DocumentView.qml DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/okular) diff --git a/mobile/components/documentitem.cpp b/mobile/components/documentitem.cpp --- a/mobile/components/documentitem.cpp +++ b/mobile/components/documentitem.cpp @@ -22,6 +22,11 @@ #include // krazy:exclude=includes #include +#ifdef Q_OS_ANDROID +#include +#include +#endif + #include #include #include @@ -59,9 +64,18 @@ //TODO: password QMimeDatabase db; - const QString path = url.isLocalFile() ? url.toLocalFile() : QStringLiteral("-"); + QUrl realUrl = url; + +#ifdef Q_OS_ANDROID + realUrl = QUrl(QtAndroid::androidActivity().callObjectMethod("contentUrlToFd", + "(Ljava/lang/String;)Ljava/lang/String;", + QAndroidJniObject::fromString(url.toString()).object() + ).toString()); +#endif + + const QString path = realUrl.isLocalFile() ? realUrl.toLocalFile() : QStringLiteral("-"); - m_document->openDocument(path, url, db.mimeTypeForUrl(url)); + m_document->openDocument(path, realUrl, db.mimeTypeForUrl(realUrl)); m_tocModel->clear(); m_tocModel->fill(m_document->documentSynopsis()); diff --git a/okular.upd b/okular.upd --- a/okular.upd +++ b/okular.upd @@ -23,3 +23,8 @@ Group=Dlg Presentation,Core Presentation Key=SlidesAdvance Key=SlidesAdvanceTime + +# remove user-defined annotation tools to make space to new builtin annotation tools +Id=annotation-toolbar +File=okularpartrc +Key=AnnotationTools,QuickAnnotationTools diff --git a/part.h b/part.h --- a/part.h +++ b/part.h @@ -53,7 +53,10 @@ class QTemporaryFile; class QAction; class QJsonObject; -namespace KParts { class GUIActivateEvent; } +namespace KParts { + class GUIActivateEvent; + class MainWindow; +} class FindBar; class ThumbnailList; @@ -262,7 +265,7 @@ void setupViewerActions(); void setViewerShortcuts(); - void setupActions(); + void setupActions( KParts::MainWindow * shell ); void setupPrint( QPrinter &printer ); bool doPrint( QPrinter &printer ); diff --git a/part.cpp b/part.cpp --- a/part.cpp +++ b/part.cpp @@ -62,13 +62,15 @@ #include #include #include +#include #include #include #include #include #include #include #include +#include #ifdef WITH_KWALLET #include #endif @@ -447,7 +449,7 @@ // [left toolbox: Reviews] | [] m_reviewsWidget = new Reviews( nullptr, m_document ); - m_sidebar->addItem( m_reviewsWidget, QIcon::fromTheme(QStringLiteral("draw-freehand")), i18n("Reviews") ); + m_sidebar->addItem( m_reviewsWidget, QIcon::fromTheme(QStringLiteral("draw-freehand")), i18n("Annotations") ); m_sidebar->setItemEnabled( m_reviewsWidget, false ); // [left toolbox: Bookmarks] | [] @@ -466,14 +468,14 @@ QWidget * miniBarContainer = new QWidget( 0 ); m_sidebar->setBottomWidget( miniBarContainer ); QVBoxLayout * miniBarLayout = new QVBoxLayout( miniBarContainer ); - miniBarLayout->setMargin( 0 ); + miniBarLayout->setContentsMargins( 0, 0, 0, 0 ); // widgets: [../[spacer/..]] | [] miniBarLayout->addItem( new QSpacerItem( 6, 6, QSizePolicy::Fixed, QSizePolicy::Fixed ) ); // widgets: [../[../MiniBar]] | [] QFrame * bevelContainer = new QFrame( miniBarContainer ); bevelContainer->setFrameStyle( QFrame::StyledPanel | QFrame::Sunken ); QVBoxLayout * bevelContainerLayout = new QVBoxLayout( bevelContainer ); - bevelContainerLayout->setMargin( 4 ); + bevelContainerLayout->setContentsMargins( 4, 4, 4, 4 ); m_progressWidget = new ProgressWidget( bevelContainer, m_document ); bevelContainerLayout->addWidget( m_progressWidget ); miniBarLayout->addWidget( bevelContainer ); @@ -484,7 +486,7 @@ QWidget * rightContainer = new QWidget( nullptr ); m_sidebar->setMainWidget( rightContainer ); QVBoxLayout * rightLayout = new QVBoxLayout( rightContainer ); - rightLayout->setMargin( 0 ); + rightLayout->setContentsMargins( 0, 0, 0, 0 ); rightLayout->setSpacing( 0 ); // KToolBar * rtb = new KToolBar( rightContainer, "mainToolBarSS" ); // rightLayout->addWidget( rtb ); @@ -524,6 +526,12 @@ QMetaObject::invokeMethod( m_pageView, "setFocus", Qt::QueuedConnection ); //usability setting // m_splitter->setFocusProxy(m_pageView); connect( m_pageView.data(), &PageView::rightClick, this, &Part::slotShowMenu ); + connect( m_pageView, &PageView::triggerSearch, this, + [this] (const QString& searchText){ + m_findBar->startSearch(searchText); + slotShowFindBar(); + } + ); connect( m_document, &Document::error, this, &Part::errorMessage ); connect( m_document, &Document::warning, this, &Part::warningMessage ); connect( m_document, &Document::notice, this, &Part::noticeMessage ); @@ -537,7 +545,7 @@ m_bottomBar = new QWidget( rightContainer ); QHBoxLayout * bottomBarLayout = new QHBoxLayout( m_bottomBar ); m_pageSizeLabel = new PageSizeLabel( m_bottomBar, m_document ); - bottomBarLayout->setMargin( 0 ); + bottomBarLayout->setContentsMargins( 0, 0, 0, 0 ); bottomBarLayout->setSpacing( 0 ); bottomBarLayout->addItem( new QSpacerItem( 5, 5, QSizePolicy::Expanding, QSizePolicy::Minimum ) ); m_miniBarLogic = new MiniBarLogic( this, m_document ); @@ -579,7 +587,8 @@ if ( m_embedMode != ViewerWidgetMode ) { - setupActions(); + KParts::MainWindow * shell = qobject_cast( parent ); + setupActions( shell ); } else { @@ -838,7 +847,7 @@ } } -void Part::setupActions() +void Part::setupActions( KParts::MainWindow * shell ) { KActionCollection * ac = actionCollection(); @@ -953,6 +962,14 @@ QAction *playPauseAction = new QAction( i18n( "Play/Pause Presentation" ), ac ); ac->addAction( QStringLiteral("presentation_play_pause"), playPauseAction ); playPauseAction->setEnabled( false ); + + // force the creation of the main toolbar before the annotation toolbar + // to respect the default toolbar layout defined in shell.rc + shell->toolBar( "mainToolBar" ); + KToggleToolBarAction * showAnnotationToolBar = new KToggleToolBarAction( shell->toolBar( "annotationToolBar" ), i18n("&Annotations"), this ); + showAnnotationToolBar->setIcon( QIcon::fromTheme( QStringLiteral("draw-freehand") ) ); + actionCollection()->addAction( QStringLiteral("mouse_toggle_annotate"), showAnnotationToolBar ); + actionCollection()->setDefaultShortcut( showAnnotationToolBar, Qt::Key_F6 ); } Part::~Part() @@ -2278,7 +2295,7 @@ connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); QVBoxLayout *topLayout = new QVBoxLayout(this); - topLayout->setMargin(6); + topLayout->setContentsMargins(6, 6, 6, 6); QHBoxLayout *midLayout = new QHBoxLayout(); spinbox = new QSpinBox(this); spinbox->setRange(1, max); diff --git a/part.rc b/part.rc --- a/part.rc +++ b/part.rc @@ -1,5 +1,5 @@ - + &File @@ -84,6 +84,7 @@ + &Settings @@ -106,5 +107,30 @@ + + +Annotation Toolbar + + + + + + + + + + + + + + + + + + + + + + diff --git a/shell/org.kde.okular.appdata.xml b/shell/org.kde.okular.appdata.xml --- a/shell/org.kde.okular.appdata.xml +++ b/shell/org.kde.okular.appdata.xml @@ -16,7 +16,6 @@ Okular Okular Okular - Okular Okular Okular Okular @@ -253,6 +252,7 @@ Lectura de manuals a l'Okular Lectura de manuals a l'Okular Lesen eines Handbuchs in Okular + Ανάγνωση οδηγού χρήσης στο Okular Reading manual in Okular Lectura del manual en Okular Eskuliburua Okularren irakurtzea @@ -284,4 +284,8 @@ okular + + + https://www.microsoft.com/store/apps/9n41msq1wnm8 + diff --git a/shell/shell.rc b/shell/shell.rc --- a/shell/shell.rc +++ b/shell/shell.rc @@ -1,5 +1,5 @@ - + @@ -23,4 +23,7 @@ + diff --git a/ui/annotationactionhandler.h b/ui/annotationactionhandler.h new file mode 100644 --- /dev/null +++ b/ui/annotationactionhandler.h @@ -0,0 +1,51 @@ +/************************************************************************** +* Copyright (C) 2019 by Simone Gaiarin * +* * +* This program is free software; you can redistribute it and/or modify * +* it under the terms of the GNU General Public License as published by * +* the Free Software Foundation; either version 2 of the License, or * +* (at your option) any later version. * +**************************************************************************/ + +#ifndef _ANNOTATIONACTIONHANDLER_H_ +#define _ANNOTATIONACTIONHANDLER_H_ + +#include + +class QAction; +class QColor; +class QFont; +class KActionCollection; +class PageViewAnnotator; +class AnnotationActionHandlerPrivate; + +/** + * @short Handles all the actions of the annotation toolbar + */ +class AnnotationActionHandler : public QObject +{ + Q_OBJECT + + public: + AnnotationActionHandler( PageViewAnnotator * parent, KActionCollection * ac ); + ~AnnotationActionHandler(); + + /** + * @short Reads the settings for the current annotation and rebuild the quick annotations menu + * + * This method is called each time okularpartrc is modified. This happens in the following + * situations (among others): the quick annotations are modified from the KCM settings + * page, a tool is modified using the "advanced settings" action, a quick annotation is + * selected, an annotation property (line width, colors, opacity, font) is modified. + */ + void reparseTools(); + void setToolsEnabled( bool on ); + void setTextToolsEnabled( bool on ); + void deselectAllAnnotationActions(); + + private: + friend class AnnotationActionHandlerPrivate; + class AnnotationActionHandlerPrivate * d; +}; + +#endif // _ANNOTATIONACTIONHANDLER_H_ diff --git a/ui/annotationactionhandler.cpp b/ui/annotationactionhandler.cpp new file mode 100644 --- /dev/null +++ b/ui/annotationactionhandler.cpp @@ -0,0 +1,777 @@ +/************************************************************************** +* Copyright (C) 2019 by Simone Gaiarin * +* * +* 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 "annotationactionhandler.h" + +// qt includes +#include +#include +#include +#include +#include +#include + +// kde includes +#include +#include +#include +#include + +// local includes +#include "annotationwidgets.h" +#include "guiutils.h" +#include "pageview.h" +#include "pageviewannotator.h" +#include "toggleactionmenu.h" + + +static const QList widthStandardValues = { 1, 1.5, 2, 2.5, 3, 3.5, 4, 4.5, 5 }; +static const QList opacityStandardValues = { 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }; + + +class AnnotationActionHandlerPrivate +{ + public: + enum class AnnotationColor { Color, InnerColor }; + static const QList> defaultColors; + + explicit AnnotationActionHandlerPrivate( AnnotationActionHandler * qq ) : + q( qq ), + annotator( nullptr ), + textTools( nullptr ), + textQuickTools( nullptr ), + agTools( nullptr ), + aQuickTools( nullptr), + aGeomShapes( nullptr ), + aStamp( nullptr), + aAddToQuickTools( nullptr ), + aContinuousMode( nullptr ), + aWidth( nullptr ), + aColor( nullptr ), + aInnerColor( nullptr ), + aOpacity( nullptr ), + aFont( nullptr ), + aAdvancedSettings( nullptr ), + aCustomStamp(nullptr), + aCustomWidth( nullptr ), + aCustomOpacity( nullptr ), + currentColor( QColor() ), + currentInnerColor( QColor() ), + currentFont( QFont() ), + currentWidth( -1 ), + selectedTool( -1 ), + textToolsEnabled( false ) + {} + + QAction * selectActionItem( KSelectAction * aList, QAction * aCustomCurrent, double value, + const QList &defaultValues, const QIcon &icon, const QString &label ); + void selectStampActionItem( const QString &stampIconName ); + void parseTool( int toolID ); + + void updateConfigActions( const QString &annotType = "" ); + void populateQuickAnnotations(); + KSelectAction * colorPickerAction( AnnotationColor colorType ); + + QIcon colorIcon( const QColor &color ); + QIcon widthIcon( double width ); + QIcon colorPickerIcon( const QString &iconName, const QColor &color ); + QIcon opacityIcon( double opacity ); + QIcon stampIcon( const QString &stampIconName ); + + void selectTool( int toolID ); + void slotStampToolSelected( QString stamp ); + void slotQuickToolSelected( int favToolID ); + void slotSetColor( const AnnotationColor &colorType, const QColor &color = QColor() ); + void slotSelectAnnotationFont(); + + AnnotationActionHandler * q; + + PageViewAnnotator * annotator; + + QList * textTools; + QList * textQuickTools; + QActionGroup * agTools; + + KSelectAction * aQuickTools; + ToggleActionMenu * aGeomShapes; + ToggleActionMenu * aStamp; + QAction * aAddToQuickTools; + KToggleAction * aContinuousMode; + KSelectAction * aWidth; + KSelectAction * aColor; + KSelectAction * aInnerColor; + KSelectAction * aOpacity; + QAction * aFont; + QAction * aAdvancedSettings; + QAction * aHideToolbar; + QAction * aShowToolBar; + + QAction * aCustomStamp; + QAction * aCustomWidth; + QAction * aCustomOpacity; + + QColor currentColor; + QColor currentInnerColor; + QFont currentFont; + int currentWidth; + + int selectedTool; + bool textToolsEnabled; +}; + +const QList> AnnotationActionHandlerPrivate::defaultColors = { + { i18n( "Red" ), Qt::red }, + { i18n( "Orange" ), "#ff5500" }, + { i18n( "Yellow" ), Qt::yellow }, + { i18n( "Green" ), Qt::green }, + { i18n( "Cyan" ), Qt::cyan }, + { i18n( "Blue" ), Qt::blue }, + { i18n( "Magenta" ), Qt::magenta }, + { i18n( "White" ), Qt::white }, + { i18n( "Gray" ), Qt::gray }, + { i18n( "Black" ), Qt::black } + +}; + +QAction * AnnotationActionHandlerPrivate::selectActionItem( KSelectAction * aList, QAction * aCustomCurrent, + double value, const QList &defaultValues, + const QIcon &icon, const QString &label ) +{ + QAction * aCustom = nullptr; + if( aCustomCurrent ) { + aList->removeAction( aCustomCurrent ); + } + int idx = defaultValues.indexOf( value ); + if( idx >= 0 ) { + aList->setCurrentItem( idx ); + } else { + aCustom = new KToggleAction( icon, label, q ); + int i = std::lower_bound(defaultValues.begin(), defaultValues.end(), value) - defaultValues.begin(); + QAction * before = i < defaultValues.size() ? aList->actions()[ i ] : nullptr; + aList->insertAction( before, aCustom ); + aList->setCurrentAction( aCustom ); + } + return aCustom; +} + +void AnnotationActionHandlerPrivate::selectStampActionItem( const QString &stampIconName ) +{ + QPair pair; + bool defaultStamp = false; + for( const auto& pair : StampAnnotationWidget::defaultStamps ) { + if( stampIconName == pair.second) { + defaultStamp = true; + break; + } + } + if( aCustomStamp ) { + aStamp->removeAction( aCustomStamp ); + agTools->removeAction( aCustomStamp ); + aCustomStamp = nullptr; + } + if( !defaultStamp ) { + QFileInfo info( stampIconName ); + QString stampActionName = info.fileName(); + aCustomStamp = new KToggleAction( stampIcon( stampIconName ), stampActionName, q); + aStamp->addAction( aCustomStamp ); + aStamp->setDefaultAction( aCustomStamp ); + agTools->addAction( aCustomStamp ); + aCustomStamp->setChecked( true ); + QObject::connect( aCustomStamp, &QAction::triggered, [this, stampIconName] () { slotStampToolSelected( stampIconName ); } ); + } +} + +void AnnotationActionHandlerPrivate::parseTool( int toolID ) +{ + if( toolID == -1 ) { + updateConfigActions(); + return; + } + + QDomElement toolElement = annotator->builtinTool( toolID ); + const QString annotType = toolElement.attribute( QStringLiteral("type") ); + QDomElement engineElement = toolElement.firstChildElement( QStringLiteral("engine") ); + QDomElement annElement = engineElement.firstChildElement( QStringLiteral("annotation") ); + + QColor color, innerColor, textColor; + if( annElement.hasAttribute( QStringLiteral( "color" ) ) ) { + color = QColor( annElement.attribute( QStringLiteral( "color" ) ) ); + } + if( annElement.hasAttribute( QStringLiteral( "innerColor" ) ) ) { + innerColor = QColor( annElement.attribute( QStringLiteral( "innerColor" ) ) ); + } + if( annElement.hasAttribute( QStringLiteral( "textColor" ) ) ) { + textColor = QColor( annElement.attribute( QStringLiteral( "textColor" ) ) ); + } + if ( textColor.isValid() ) { + currentColor = textColor; + currentInnerColor = color; + } else { + currentColor = color; + currentInnerColor = innerColor; + } + + if( annElement.hasAttribute( QStringLiteral( "font" ) ) ) { + currentFont.fromString( annElement.attribute( QStringLiteral( "font" ) ) ); + } + + // if the width value is not a default one, insert a new action in the width list + if( annElement.hasAttribute( QStringLiteral( "width" ) ) ) { + double width = annElement.attribute( QStringLiteral( "width" ) ).toDouble(); + aCustomWidth = selectActionItem( aWidth, aCustomWidth, width, widthStandardValues, + widthIcon( width ), i18nc("@item:inlistbox", "Width %1", width ) ); + } + + // if the opacity value is not a default one, insert a new action in the opacity list + if( annElement.hasAttribute( QStringLiteral( "opacity" ) ) ) { + double opacity = 100 * annElement.attribute( QStringLiteral( "opacity" ) ).toDouble(); + aCustomOpacity = selectActionItem( aOpacity, aCustomOpacity, opacity, opacityStandardValues, + opacityIcon( opacity ), i18nc("@item:inlistbox", "%1\%", opacity) ); + } else { + aOpacity->setCurrentItem( opacityStandardValues.size() - 1 ); // 100 % + } + + // if the tool is a custom stamp, insert a new action in the stamp list + if( annotType == QStringLiteral( "stamp" ) ) { + QString stampIconName = annElement.attribute( QStringLiteral("icon") ); + selectStampActionItem( stampIconName ); + } + + updateConfigActions( annotType ); +} + +void AnnotationActionHandlerPrivate::updateConfigActions( const QString &annotType ) +{ + const bool annotationSelected = !annotType.isEmpty(); + const bool isTypewriter = annotType == QStringLiteral( "typewriter" ); + const bool isInlineNote = annotType == QStringLiteral( "note-inline" ); + const bool isText = isInlineNote || isTypewriter; + const bool isShape = annotType == QStringLiteral( "rectangle" ) + || annotType == QStringLiteral( "ellipse" ) + || annotType == QStringLiteral( "polygon" ); + const bool isLine = annotType == QStringLiteral( "ink" ) + || annotType == QStringLiteral( "straight-line" ); + const bool isStamp = annotType == QStringLiteral( "stamp" ); + + if ( isTypewriter ) { + aColor->setIcon( colorPickerIcon( QStringLiteral( "format-text-color" ), currentColor ) ); + } else { + aColor->setIcon( colorPickerIcon( QStringLiteral( "format-stroke-color" ), currentColor ) ); + } + aInnerColor->setIcon( colorPickerIcon( QStringLiteral( "format-fill-color" ), currentInnerColor ) ); + + aAddToQuickTools->setEnabled( annotationSelected ); + aWidth->setEnabled( isLine || isShape ); + aColor->setEnabled( annotationSelected && !isStamp ); + aInnerColor->setEnabled( isShape ); + aOpacity->setEnabled( annotationSelected && !isStamp ); + aFont->setEnabled( isText ); + aAdvancedSettings->setEnabled( annotationSelected ); + + // set tooltips + if ( !annotationSelected ) { + aWidth->setToolTip( i18nc( "@info:tooltip", "Annotation line width (No annotation selected)") ); + aColor->setToolTip( i18nc( "@info:tooltip", "Annotation color (No annotation selected)") ); + aInnerColor->setToolTip( i18nc( "@info:tooltip", "Annotation fill color (No annotation selected)") ); + aOpacity->setToolTip( i18nc( "@info:tooltip", "Annotation opacity (No annotation selected)") ); + aFont->setToolTip( i18nc( "@info:tooltip", "Annotation font (No annotation selected)") ); + aAddToQuickTools->setToolTip( i18nc( "@info:tooltip", "Add the current annotation to the quick annotations menu (No annotation selected)") ); + aAdvancedSettings->setToolTip( i18nc( "@info:tooltip", "Advanced settings for the current annotation tool (No annotation selected)") ); + return; + } + + if( isLine || isShape ) { + aWidth->setToolTip( i18nc( "@info:tooltip", "Annotation line width") ); + } else { + aWidth->setToolTip( i18nc( "@info:tooltip", "Annotation line width (Current annotation has no line width)") ); + } + + if ( isTypewriter ) { + aColor->setToolTip( i18nc( "@info:tooltip", "Annotation text color") ); + } else if( isShape ) { + aColor->setToolTip( i18nc( "@info:tooltip", "Annotation border color") ); + } else { + aColor->setToolTip( i18nc( "@info:tooltip", "Annotation color") ); + } + + if( isShape ) { + aInnerColor->setToolTip( i18nc( "@info:tooltip", "Annotation fill color") ); + } else { + aInnerColor->setToolTip( i18nc( "@info:tooltip", "Annotation fill color (Current annotation has no fill color)") ); + } + + if( isStamp ) { + aOpacity->setToolTip( i18nc( "@info:tooltip", "Annotation opacity (Cannot change opacity of current annotation)") ); + } else { + aOpacity->setToolTip( i18nc( "@info:tooltip", "Annotation opacity") ); + } + + if( isText ) { + aFont->setToolTip( i18nc( "@info:tooltip", "Annotation font") ); + } else { + aFont->setToolTip( i18nc( "@info:tooltip", "Annotation font (Current annotation has no font)") ); + } + + aAddToQuickTools->setToolTip( i18nc( "@info:tooltip", "Add the current annotation to the quick annotations menu") ); + aAdvancedSettings->setToolTip( i18nc( "@info:tooltip", "Advanced settings for the current annotation tool") ); +} + +void AnnotationActionHandlerPrivate::populateQuickAnnotations() +{ + const QList numberKeys = { Qt::Key_1, Qt::Key_2, Qt::Key_3, Qt::Key_4, Qt::Key_5, + Qt::Key_6, Qt::Key_7,Qt::Key_8, Qt::Key_9, Qt::Key_0 }; + + textQuickTools->clear(); + aQuickTools->removeAllActions(); + + int favToolId = 1; + QList::const_iterator shortcutNumber = numberKeys.begin(); + QDomElement favToolElement = annotator->quickTool( favToolId ); + while( !favToolElement.isNull() ) { + QString itemText = favToolElement.attribute( "name" ); + if ( itemText.isEmpty() ) { + itemText = PageViewAnnotator::defaultToolName( favToolElement ); + } + QIcon toolIcon = QIcon( PageViewAnnotator::makeToolPixmap( favToolElement ) ); + QAction * annFav = new QAction( toolIcon, itemText, q); + aQuickTools->addAction( annFav ); + if( shortcutNumber != numberKeys.end() ) + annFav->setShortcut( QKeySequence(Qt::ALT + *(shortcutNumber++) ) ); + QObject::connect( annFav, &QAction::triggered, [this, favToolId] () { slotQuickToolSelected( favToolId ); } ); + + QDomElement engineElement = favToolElement.firstChildElement( QStringLiteral("engine") ); + if( engineElement.attribute( "type" ) == QStringLiteral("TextSelector") ) + { + textQuickTools->append( annFav ); + annFav->setEnabled( textToolsEnabled ); + } + + favToolElement = annotator->quickTool( ++favToolId ); + } + QAction * separator = new QAction(); + separator->setSeparator( true ); + aQuickTools->addAction( separator ); + // add action to open "Configure Annotation" settings dialog + KActionCollection * ac = qobject_cast( q->parent()->parent() )->actionCollection(); + QAction * aConfigAnnotation = ac->action( QStringLiteral("options_configure_annotations") ); + if ( aConfigAnnotation ) { + aQuickTools->addAction( aConfigAnnotation ); + } +} + +KSelectAction * AnnotationActionHandlerPrivate::colorPickerAction( AnnotationColor colorType ) +{ + auto colorList = defaultColors; + QString aText( i18nc( "@action:intoolbar Current annotation config option", "Color" ) ); + if ( colorType == AnnotationColor::InnerColor ) { + aText = i18nc( "@action:intoolbar Current annotation config option", "Fill Color" ); + colorList.append( QPair( QStringLiteral( "Transparent" ), Qt::transparent ) ); + } + KSelectAction * aColorPicker = new KSelectAction( QIcon(), aText, q); + aColorPicker->setToolBarMode( KSelectAction::MenuMode ); + for( const auto& colorNameValue : colorList ) { + QColor color( colorNameValue.second ); + QAction * aColor = new QAction( colorIcon( color ), i18nc("@item:inlistbox Color name", "%1", colorNameValue.first ), q); + aColorPicker->addAction( aColor ); + QObject::connect( aColor, &QAction::triggered, [this, colorType, color] () { + slotSetColor( colorType, color ); + } ); + } + QAction * aCustomColor = new QAction( QIcon::fromTheme( QStringLiteral("color-picker") ), + i18nc("@item:inlistbox", "Custom Color..."), q ); + aColorPicker->addAction( aCustomColor ); + QObject::connect( aCustomColor, &QAction::triggered, [this, colorType] () { + slotSetColor( colorType ); + } ); + return aColorPicker; +} + +QIcon AnnotationActionHandlerPrivate::widthIcon( double width ) +{ + QPixmap pm( 32, 32 ); + pm.fill( Qt::transparent ); + QPainter p( &pm ); + p.setRenderHint( QPainter::Antialiasing ); + p.setPen( QPen( Qt::black, 2 * width, Qt::SolidLine, Qt::RoundCap ) ); + p.drawLine( 0, pm.height()/2, pm.width(), pm.height()/2 ); + p.end(); + return QIcon( pm ); +} + +QIcon AnnotationActionHandlerPrivate::colorPickerIcon( const QString &iconName, const QColor &color ) +{ + QIcon icon = QIcon::fromTheme( iconName ); + if ( !color.isValid() ) { + return icon; + } + QSize iconSize = QSize( 32, 32 ); + QPixmap pm = icon.pixmap( iconSize ); + QPainter p( &pm ); + p.fillRect( 0, iconSize.height() - 8, iconSize.width(), 8, color ); + p.end(); + return QIcon( pm ); +} + +QIcon AnnotationActionHandlerPrivate::colorIcon( const QColor &color ) +{ + QSize iconSize = QSize( 32, 32 ); + QPixmap pm( iconSize ); + QPainter p( &pm ); + if( color == Qt::transparent ) { + p.fillRect( 0, 0, iconSize.width(), iconSize.height(), Qt::white ); + p.setPen( QPen( Qt::red, 2 ) ); + p.drawLine( iconSize.width() - 1, 0, 0, iconSize.height() - 1 ); + } else { + p.fillRect( 0, 0, iconSize.width(), iconSize.height(), color ); + } + p.setPen( Qt::black ); + p.drawRect( 0, 0, iconSize.width() - 1, iconSize.height() - 1 ); + p.end(); + return QIcon( pm ); +} + +QIcon AnnotationActionHandlerPrivate::opacityIcon( double opacity ) +{ + QPixmap pm( 32, 32 ); + pm.fill( Qt::transparent ); + QPainter p( &pm ); + p.setRenderHint( QPainter::Antialiasing ); + p.setPen( Qt::NoPen ); + QColor color( Qt::black ); + color.setAlpha( opacity*255/100 ); + p.setBrush( QBrush( color ) ); + p.drawRect( 4, 4, 24, 24 ); + p.end(); + return QIcon( pm ); +} + +QIcon AnnotationActionHandlerPrivate::stampIcon( const QString &stampIconName ) +{ + QPixmap stampPix = GuiUtils::loadStamp( stampIconName, 32 ); + if( stampPix.width() == stampPix.height() ) + return QIcon( stampPix ); + else + return QIcon::fromTheme( QStringLiteral("tag") ); +} + +void AnnotationActionHandlerPrivate::selectTool( int toolID ) +{ + selectedTool = toolID; + annotator->selectTool( toolID ); + parseTool( toolID ); +} + +void AnnotationActionHandlerPrivate::slotStampToolSelected( QString stamp ) +{ + KMessageBox::information( nullptr, + i18nc( "@info", "Stamps inserted in PDF documents are not visible in PDF readers other than Okular" ), + i18nc( "@title:window", "Experimental feature" ), + QStringLiteral( "stampAnnotationWarning" ) + ); + selectedTool = PageViewAnnotator::STAMP_TOOL_ID; + annotator->selectStampTool( stamp ); // triggers a reparsing thus calling parseTool +} + +void AnnotationActionHandlerPrivate::slotQuickToolSelected( int favToolID ) +{ + int toolID = annotator->setQuickTool( favToolID ); // always triggers an unuseful reparsing + QAction * favToolAction = agTools->actions()[ toolID - 1 ]; + if ( !favToolAction->isChecked() ) { + // action group workaround: activates the action slot calling selectTool + // when new tool if different from the selected one + favToolAction->setChecked( true ); + } else { + selectTool( toolID ); + } + aShowToolBar->setChecked( true ); +} + +void AnnotationActionHandlerPrivate::slotSetColor( const AnnotationColor &colorType, const QColor &color ) +{ + QColor selectedColor( color ); + if ( !selectedColor.isValid() ) { + selectedColor = QColorDialog::getColor( currentColor, nullptr, + i18nc( "@title:window", "Select color" ) ); + if ( !selectedColor.isValid() ) { + return; + } + } + if ( colorType == AnnotationColor::Color ) { + currentColor = selectedColor; + annotator->setAnnotationColor( selectedColor ); + } else if ( colorType == AnnotationColor::InnerColor ) { + currentInnerColor = selectedColor; + annotator->setAnnotationInnerColor( selectedColor ); + } +} + +void AnnotationActionHandlerPrivate::slotSelectAnnotationFont() +{ + bool ok; + QFont selectedFont = QFontDialog::getFont( &ok, currentFont ); + if ( ok ) { + currentFont = selectedFont; + annotator->setAnnotationFont( currentFont ); + } +} + +AnnotationActionHandler::AnnotationActionHandler( PageViewAnnotator * parent, KActionCollection * ac ) : + QObject( parent ), + d( new AnnotationActionHandlerPrivate( this ) ) +{ + d->annotator = parent; + + // Text markup actions + KToggleAction * aHighlighter = new KToggleAction( QIcon::fromTheme( QStringLiteral("draw-highlight") ), + i18nc("@action:intoolbar Annotation tool", "Highlighter"), this ); + KToggleAction * aUnderline = new KToggleAction( QIcon::fromTheme( QStringLiteral("format-text-underline") ), + i18nc("@action:intoolbar Annotation tool", "Underline"), this) ; + KToggleAction * aSquiggle = new KToggleAction( QIcon::fromTheme( QStringLiteral("format-text-underline-squiggle") ), + i18nc("@action:intoolbar Annotation tool", "Squiggle"), this ); + KToggleAction * aStrikeout = new KToggleAction( QIcon::fromTheme( QStringLiteral("format-text-strikethrough") ), + i18nc("@action:intoolbar Annotation tool", "Strike Out"), this ); + // Notes actions + KToggleAction * aTypewriter = new KToggleAction( QIcon::fromTheme( QStringLiteral("tool-text") ), + i18nc("@action:intoolbar Annotation tool", "Typewriter"), this ); + KToggleAction * aInlineNote = new KToggleAction( QIcon::fromTheme( QStringLiteral("note") ), + i18nc("@action:intoolbar Annotation tool", "Inline Note"), this ); + KToggleAction * aPopupNote = new KToggleAction( QIcon::fromTheme( QStringLiteral("edit-comment") ), + i18nc("@action:intoolbar Annotation tool", "Popup Note"), this ); + KToggleAction * aFreehandLine = new KToggleAction( QIcon::fromTheme( QStringLiteral("draw-freehand") ), + i18nc("@action:intoolbar Annotation tool", "Freehand Line"), this ); + // Geometrical shapes actions + KToggleAction * aStraightLine = new KToggleAction( QIcon::fromTheme( QStringLiteral("draw-line") ), + i18nc("@action:intoolbar Annotation tool", "Straight line"), this ); + KToggleAction * aArrow = new KToggleAction( QIcon::fromTheme( QStringLiteral("draw-arrow") ), + i18nc("@action:intoolbar Annotation tool", "Arrow"), this ); + KToggleAction * aRectangle = new KToggleAction( QIcon::fromTheme( QStringLiteral("draw-rectangle") ), + i18nc("@action:intoolbar Annotation tool", "Rectangle"), this ); + KToggleAction * aEllipse = new KToggleAction( QIcon::fromTheme( QStringLiteral("draw-ellipse") ), + i18nc("@action:intoolbar Annotation tool", "Ellipse"), this ); + KToggleAction * aPolygon = new KToggleAction( QIcon::fromTheme( QStringLiteral("draw-polyline") ), + i18nc("@action:intoolbar Annotation tool", "Polygon"), this ); + d->aGeomShapes = new ToggleActionMenu( QIcon(),QString(), this, + QToolButton::MenuButtonPopup, + ToggleActionMenu::ImplicitDefaultAction ); + d->aGeomShapes->setText( i18nc( "@action", "Geometrical shapes" ) ); + d->aGeomShapes->setEnabled( true ); // Need to explicitly set this once, or refreshActions() in part.cpp will disable this action + d->aGeomShapes->addAction( aArrow ); + d->aGeomShapes->addAction( aStraightLine ); + d->aGeomShapes->addAction( aRectangle ); + d->aGeomShapes->addAction( aEllipse ); + d->aGeomShapes->addAction( aPolygon ); + d->aGeomShapes->setDefaultAction( aArrow ); + + // The order in which the actions are added is relevant to connect + // them to the correct toolId defined in tools.xml + d->agTools = new QActionGroup( this ); + d->agTools->addAction( aHighlighter ); + d->agTools->addAction( aUnderline ); + d->agTools->addAction( aSquiggle ); + d->agTools->addAction( aStrikeout ); + d->agTools->addAction( aTypewriter ); + d->agTools->addAction( aInlineNote ); + d->agTools->addAction( aPopupNote ); + d->agTools->addAction( aFreehandLine ); + d->agTools->addAction( aArrow ); + d->agTools->addAction( aStraightLine ); + d->agTools->addAction( aRectangle ); + d->agTools->addAction( aEllipse ); + d->agTools->addAction( aPolygon ); + + d->textQuickTools = new QList(); + d->textTools = new QList(); + d->textTools->append( aHighlighter ); + d->textTools->append( aUnderline ); + d->textTools->append( aSquiggle ); + d->textTools->append( aStrikeout ); + + int toolId = 1; + for( QAction * ann : d->agTools->actions() ) { + // action group workaround: connecting to toggled instead of triggered + connect( ann, &QAction::toggled, [this, toolId] ( bool checked ) { + if( checked ) + d->selectTool( toolId ); + } ); + toolId++; + } + + // Stamp action + d->aStamp = new ToggleActionMenu( QIcon::fromTheme( QStringLiteral("tag") ), + QString(), + this, + QToolButton::MenuButtonPopup, + ToggleActionMenu::ImplicitDefaultAction ); + d->aStamp->setText( i18nc( "@action", "Stamp" ) ); + + QPair stamp; + for( const auto& stamp : StampAnnotationWidget::defaultStamps ) { + + KToggleAction * ann = new KToggleAction( d->stampIcon( stamp.second ), stamp.first, this); + if( !d->aStamp->defaultAction() ) + d->aStamp->setDefaultAction( ann ); + d->aStamp->addAction( ann ); + d->agTools->addAction( ann ); + // action group workaround: connecting to toggled instead of triggered + // (because deselectAllAnnotationActions has to call triggered) + connect( ann, &QAction::toggled, [this, stamp] ( bool checked ) { + if( checked ) + d->slotStampToolSelected( stamp.second ); + } ); + } + + // Quick annotations action + d->aQuickTools = new KSelectAction( QIcon::fromTheme( QStringLiteral("draw-freehand") ), + i18nc("@action:intoolbar Show list of quick annotation tools", "Quick Annotations"), this ); + d->aQuickTools->setToolBarMode( KSelectAction::MenuMode ); + d->populateQuickAnnotations(); + + // Add to quick annotation action + d->aAddToQuickTools = new QAction(QIcon::fromTheme( QStringLiteral("favorite") ), + i18nc("@action:intoolbar Add current annotation tool to the quick annotations list", "Add to Quick Annotations"), this); + + // Pin action + d->aContinuousMode = new KToggleAction(QIcon::fromTheme( QStringLiteral("pin") ), + i18nc("@action:intoolbar When checked keep the current annotation tool active after use", "Keep Active"), this); + d->aContinuousMode->setToolTip( i18nc( "@info:tooltip", "Keep the annotation tool active after use") ); + d->aContinuousMode->setChecked( d->annotator->continuousMode() ); + + // Annotation settings actions + d->aColor = d->colorPickerAction( AnnotationActionHandlerPrivate::AnnotationColor::Color ); + d->aInnerColor = d->colorPickerAction( AnnotationActionHandlerPrivate::AnnotationColor::InnerColor ); + d->aFont = new QAction( QIcon::fromTheme( QStringLiteral("font-face") ), + i18nc("@action:intoolbar Current annotation config option", "Font"), this ); + d->aAdvancedSettings = new QAction( QIcon::fromTheme( QStringLiteral("settings-configure") ), + i18nc("@action:intoolbar Current annotation advanced settings", "Annotation Settings"), this ); + d->aHideToolbar = new QAction( QIcon::fromTheme( QStringLiteral("dialog-close") ), + i18nc("@action:intoolbar Hide the toolbar", "Hide"), this ); + + // Width list + d->aWidth = new KSelectAction( QIcon::fromTheme( QStringLiteral("edit-line-width") ), + i18nc("@action:intoolbar Current annotation config option", "Line width"), this ); + d->aWidth->setToolBarMode( KSelectAction::MenuMode ); + for( int i = 0; i < widthStandardValues.size(); i++ ) { + KToggleAction * ann = new KToggleAction( d->widthIcon( widthStandardValues[i] ), i18nc("@item:inlistbox", "Width %1", widthStandardValues[i]), this); + d->aWidth->addAction( ann ); + connect( ann, &QAction::triggered, [this, i] () { d->annotator->setAnnotationWidth( widthStandardValues[i] ); } ); + } + + // Opacity list + d->aOpacity = new KSelectAction( QIcon::fromTheme( QStringLiteral("edit-opacity") ), + i18nc("@action:intoolbar Current annotation config option", "Opacity"), this ); + d->aOpacity->setToolBarMode( KSelectAction::MenuMode ); + for( int i = 0; i < opacityStandardValues.size(); i++ ) { + KToggleAction * ann = new KToggleAction( d->opacityIcon( opacityStandardValues[i] ), QString( "%1\%" ).arg( opacityStandardValues[i] ), this); + d->aOpacity->addAction( ann ); + connect( ann, &QAction::triggered, [this, i] () { d->annotator->setAnnotationOpacity( opacityStandardValues[i] / 100 ); } ); + } + + connect( d->aAddToQuickTools, &QAction::triggered, d->annotator, &PageViewAnnotator::addToQuickAnnotations ); + connect( d->aContinuousMode, &QAction::toggled , d->annotator, &PageViewAnnotator::setContinuousMode ); + connect( d->aAdvancedSettings, &QAction::triggered, d->annotator, &PageViewAnnotator::slotAdvancedSettings ); + connect( d->aFont, &QAction::triggered, std::bind( &AnnotationActionHandlerPrivate::slotSelectAnnotationFont, d ) ); + + // action group workaround: allows unchecking the currently selected annotation action. + // Other parts of code dependent to this workaround are marked with "action group workaround". + connect( d->agTools, &QActionGroup::triggered, [this]( QAction* action ) { + static QAction* lastAction = nullptr; + if (action == lastAction) { + lastAction = nullptr; + d->agTools->checkedAction()->setChecked( false ); + d->selectTool( -1 ); + } else { + lastAction = action; + } + } ); + + d->aShowToolBar = ac->action( "mouse_toggle_annotate" ); + connect( d->aHideToolbar, &QAction::triggered, [this] () { d->aShowToolBar->setChecked( false ); } ); + + ac->addAction( QStringLiteral("annotation_highlighter"), aHighlighter ); + ac->addAction( QStringLiteral("annotation_underline"), aUnderline ); + ac->addAction( QStringLiteral("annotation_squiggle"), aSquiggle ); + ac->addAction( QStringLiteral("annotation_strike_out"), aStrikeout ); + ac->addAction( QStringLiteral("annotation_typewriter"), aTypewriter ); + ac->addAction( QStringLiteral("annotation_inline_note"), aInlineNote ); + ac->addAction( QStringLiteral("annotation_popup_note"), aPopupNote ); + ac->addAction( QStringLiteral("annotation_freehand_line"), aFreehandLine ); + ac->addAction( QStringLiteral("annotation_arrow"), aArrow ); + ac->addAction( QStringLiteral("annotation_straight_line"), aStraightLine ); + ac->addAction( QStringLiteral("annotation_rectangle"), aRectangle ); + ac->addAction( QStringLiteral("annotation_ellipse"), aEllipse ); + ac->addAction( QStringLiteral("annotation_polygon"), aPolygon ); + ac->addAction( QStringLiteral("annotation_geometrical_shape"), d->aGeomShapes ); + ac->addAction( QStringLiteral("annotation_stamp"), d->aStamp ); + ac->addAction( QStringLiteral("annotation_favorites"), d->aQuickTools ); + ac->addAction( QStringLiteral("annotation_bookmark"), d->aAddToQuickTools ); + ac->addAction(QStringLiteral("annotation_settings_pin"), d->aContinuousMode ); + ac->addAction( QStringLiteral("annotation_settings_width"), d->aWidth ); + ac->addAction(QStringLiteral("annotation_settings_color"), d->aColor ); + ac->addAction(QStringLiteral("annotation_settings_inner_color"), d->aInnerColor ); + ac->addAction( QStringLiteral("annotation_settings_opacity"), d->aOpacity ); + ac->addAction(QStringLiteral("annotation_settings_font"), d->aFont ); + ac->addAction(QStringLiteral("annotation_settings_advanced"), d->aAdvancedSettings ); + ac->addAction( QStringLiteral("hide_annotation_toolbar"), d->aHideToolbar ); + + ac->setDefaultShortcut( aHighlighter, Qt::Key_1 ); + ac->setDefaultShortcut( aUnderline, Qt::Key_2 ); + ac->setDefaultShortcut( aSquiggle, Qt::Key_3 ); + ac->setDefaultShortcut( aStrikeout, Qt::Key_4 ); + ac->setDefaultShortcut( aTypewriter, Qt::Key_5 ); + ac->setDefaultShortcut( aInlineNote, Qt::Key_6 ); + ac->setDefaultShortcut( aPopupNote, Qt::Key_7 ); + ac->setDefaultShortcut( aFreehandLine, Qt::Key_8 ); + ac->setDefaultShortcut( aArrow, Qt::Key_9 ); + ac->setDefaultShortcut( d->aAddToQuickTools, QKeySequence( Qt::CTRL + Qt::SHIFT + Qt::Key_B ) ); + + d->updateConfigActions(); +} + +AnnotationActionHandler::~AnnotationActionHandler() +{ + // delete the private data storage structure + delete d; +} + +void AnnotationActionHandler::reparseTools() +{ + d->parseTool( d->selectedTool ); + d->populateQuickAnnotations(); +} + +void AnnotationActionHandler::setToolsEnabled( bool on ) +{ + for( QAction * ann : d->agTools->actions() ) { + ann->setEnabled( on ); + } + d->aQuickTools->setEnabled( on ); + d->aGeomShapes->setEnabled( on ); + d->aStamp->setEnabled( on ); + d->aContinuousMode->setEnabled( on ); +} + +void AnnotationActionHandler::setTextToolsEnabled( bool on ) +{ + d->textToolsEnabled = on; + for( QAction * ann : *d->textTools ) { + ann->setEnabled( on ); + } + for( QAction * ann : *d->textQuickTools ) { + ann->setEnabled( on ); + } +} + +void AnnotationActionHandler::deselectAllAnnotationActions() +{ + QAction * checkedAction = d->agTools->checkedAction(); + if( checkedAction ) + checkedAction->trigger(); // action group workaround: using trigger instead of setChecked +} + +#include "moc_annotationactionhandler.cpp" diff --git a/ui/annotationwidgets.h b/ui/annotationwidgets.h --- a/ui/annotationwidgets.h +++ b/ui/annotationwidgets.h @@ -19,6 +19,7 @@ class QDoubleSpinBox; class QFormLayout; class QLabel; +class QPushButton; class QWidget; class KColorButton; class QSpinBox; @@ -31,7 +32,9 @@ Q_OBJECT public: - explicit PixmapPreviewSelector( QWidget * parent = nullptr ); + enum PreviewPosition{ Side, Below }; + + explicit PixmapPreviewSelector( QWidget * parent = nullptr, PreviewPosition position = Side ); virtual ~PixmapPreviewSelector(); void setIcon( const QString& icon ); @@ -49,12 +52,15 @@ private Q_SLOTS: void iconComboChanged( const QString& icon ); + void selectCustomStamp(); private: QString m_icon; + QPushButton * m_stampPushButton; QLabel * m_iconLabel; QComboBox * m_comboItems; int m_previewSize; + PreviewPosition m_previewPosition; }; @@ -83,6 +89,8 @@ virtual void applyChanges(); + void setAnnotTypeEditable( bool ); + Q_SIGNALS: void dataChanged(); @@ -96,6 +104,8 @@ void addOpacitySpinBox( QWidget * widget, QFormLayout * formlayout ); void addVerticalSpacer( QFormLayout * formlayout ); + bool m_typeEditable; + private: Okular::Annotation * m_ann; QWidget * m_appearanceWidget { nullptr }; @@ -147,6 +157,8 @@ Q_OBJECT public: + static const QList> defaultStamps; + explicit StampAnnotationWidget( Okular::Annotation * ann ); void applyChanges() override; diff --git a/ui/annotationwidgets.cpp b/ui/annotationwidgets.cpp --- a/ui/annotationwidgets.cpp +++ b/ui/annotationwidgets.cpp @@ -19,7 +19,10 @@ #include #include #include +#include +#include #include +#include #include #include #include @@ -37,16 +40,31 @@ #define FILEATTACH_ICONSIZE 48 -PixmapPreviewSelector::PixmapPreviewSelector( QWidget * parent ) - : QWidget( parent ) +PixmapPreviewSelector::PixmapPreviewSelector( QWidget * parent, PreviewPosition position ) + : QWidget( parent ), m_previewPosition( position ) { - QHBoxLayout * mainlay = new QHBoxLayout( this ); - mainlay->setMargin( 0 ); + QVBoxLayout * mainlay = new QVBoxLayout( this ); + mainlay->setContentsMargins( 0, 0, 0, 0 ); + QHBoxLayout * toplay = new QHBoxLayout( this ); + toplay->setContentsMargins( 0, 0, 0, 0 ); + mainlay->addLayout( toplay ); m_comboItems = new KComboBox( this ); - mainlay->addWidget( m_comboItems ); - mainlay->setAlignment( m_comboItems, Qt::AlignTop ); + toplay->addWidget( m_comboItems ); + m_stampPushButton = new QPushButton(QIcon::fromTheme( "document-open" ), QString(), this ); + m_stampPushButton->setVisible( false ); + m_stampPushButton->setToolTip( i18nc( "@info:tooltip", "Select a custom stamp symbol from file") ); + toplay->addWidget(m_stampPushButton); m_iconLabel = new QLabel( this ); - mainlay->addWidget( m_iconLabel ); + switch ( m_previewPosition ) + { + case Side: + toplay->addWidget( m_iconLabel ); + break; + case Below: + mainlay->addWidget( m_iconLabel ); + mainlay->setAlignment( m_iconLabel, Qt::AlignHCenter ); + break; + } m_iconLabel->setSizePolicy( QSizePolicy::Fixed, QSizePolicy::Fixed ); m_iconLabel->setAlignment( Qt::AlignCenter ); m_iconLabel->setFrameStyle( QFrame::StyledPanel ); @@ -57,6 +75,7 @@ connect( m_comboItems, SIGNAL(currentIndexChanged(QString)), this, SLOT(iconComboChanged(QString)) ); connect( m_comboItems, &QComboBox::editTextChanged, this, &PixmapPreviewSelector::iconComboChanged ); + connect( m_stampPushButton, &QPushButton::clicked, this, &PixmapPreviewSelector::selectCustomStamp ); } PixmapPreviewSelector::~PixmapPreviewSelector() @@ -93,7 +112,15 @@ void PixmapPreviewSelector::setPreviewSize( int size ) { m_previewSize = size; - m_iconLabel->setFixedSize( m_previewSize + 8, m_previewSize + 8 ); + switch( m_previewPosition ) + { + case Side: + m_iconLabel->setFixedSize( m_previewSize + 8, m_previewSize + 8 ); + break; + case Below: + m_iconLabel->setFixedSize( 3 * m_previewSize + 8, m_previewSize + 8 ); + break; + } iconComboChanged( m_icon ); } @@ -105,6 +132,7 @@ void PixmapPreviewSelector::setEditable( bool editable ) { m_comboItems->setEditable( editable ); + m_stampPushButton->setVisible( editable ); } void PixmapPreviewSelector::iconComboChanged( const QString& icon ) @@ -119,15 +147,32 @@ m_icon = icon; } - QPixmap pixmap = GuiUtils::loadStamp( m_icon, QSize(), m_previewSize ); + QPixmap pixmap = GuiUtils::loadStamp( m_icon, m_previewSize ); const QRect cr = m_iconLabel->contentsRect(); if ( pixmap.width() > cr.width() || pixmap.height() > cr.height() ) pixmap = pixmap.scaled( cr.size(), Qt::KeepAspectRatio, Qt::SmoothTransformation ); m_iconLabel->setPixmap( pixmap ); emit iconChanged( m_icon ); } +void PixmapPreviewSelector::selectCustomStamp() +{ + const QString customStampFile = QFileDialog::getOpenFileName(this, i18nc("@title:window file chooser", "Select custom stamp symbol"), + QString(), i18n("*.ico *.png *.xpm *.svg *.svgz | Icon Files (*.ico *.png *.xpm *.svg *.svgz)") ); + if ( !customStampFile.isEmpty() ) + { + QPixmap pixmap = GuiUtils::loadStamp( customStampFile, m_previewSize ); + if( pixmap.isNull() ) { + KMessageBox::sorry( this, + xi18nc("@info", "Could not load the file %1", customStampFile ), + i18nc("@title:window", "Invalid file") + ); + } else { + m_comboItems->setEditText(customStampFile); + } + } +} AnnotationWidget * AnnotationWidgetFactory::widgetFor( Okular::Annotation * ann ) { @@ -167,7 +212,7 @@ AnnotationWidget::AnnotationWidget( Okular::Annotation * ann ) - : m_ann( ann ) + : m_typeEditable( true ), m_ann( ann ) { } @@ -236,7 +281,7 @@ m_opacity = new QSpinBox( widget ); m_opacity->setRange( 0, 100 ); m_opacity->setValue( (int)( m_ann->style().opacity() * 100 ) ); - m_opacity->setSuffix( i18nc( "Suffix for the opacity level, eg '80 %'", " %" ) ); + m_opacity->setSuffix( i18nc( "Suffix for the opacity level, eg '80%'", "%" ) ); formlayout->addRow( i18n( "&Opacity:" ), m_opacity); connect( m_opacity, SIGNAL(valueChanged(int)), this, SIGNAL(dataChanged()) ); } @@ -301,6 +346,12 @@ } } + +void AnnotationWidget::setAnnotTypeEditable( bool editable ) +{ + m_typeEditable = editable; +} + void TextAnnotationWidget::createPopupNoteStyleUi( QWidget * widget, QFormLayout * formlayout ) { addColorButton( widget, formlayout ); addOpacitySpinBox( widget, formlayout ); @@ -376,6 +427,26 @@ connect( m_spinWidth, SIGNAL(valueChanged(double)), this, SIGNAL(dataChanged()) ); } +const QList> StampAnnotationWidget::defaultStamps = { + { i18n( "Approved" ), QStringLiteral("Approved") }, + { i18n( "As Is" ), QStringLiteral("AsIs") }, + { i18n( "Confidential" ), QStringLiteral("Confidential") }, + { i18n( "Departmental" ), QStringLiteral("Departmental") }, + { i18n( "Draft" ), QStringLiteral("Draft") }, + { i18n( "Experimental" ), QStringLiteral("Experimental") }, + { i18n( "Final" ), QStringLiteral("Expired") }, + { i18n( "For Comment" ), QStringLiteral("ForComment") }, + { i18n( "For Public Release" ), QStringLiteral("ForPublicRelease") }, + { i18n( "Not Approved" ), QStringLiteral("NotApproved") }, + { i18n( "Not For Public Release" ), QStringLiteral("NotForPublicRelease") }, + { i18n( "Sold" ), QStringLiteral("Sold") }, + { i18n( "Top Secret" ), QStringLiteral("TopSecret") }, + { i18n( "Bookmark" ), QStringLiteral("bookmark-new") }, + { i18n( "Information" ), QStringLiteral("help-about") }, + { i18n( "KDE" ), QStringLiteral("kde") }, + { i18n( "Okular" ), QStringLiteral("okular") } +}; + StampAnnotationWidget::StampAnnotationWidget( Okular::Annotation * ann ) : AnnotationWidget( ann ), m_pixmapSelector( nullptr ) { @@ -386,31 +457,27 @@ { QWidget * widget = qobject_cast( formlayout->parent() ); + KMessageWidget * brokenStampSupportWarning = new KMessageWidget( widget ); + brokenStampSupportWarning->setText( xi18nc("@info", "experimental feature." + "Stamps inserted in PDF documents are not visible in PDF readers other than Okular.") ); + brokenStampSupportWarning->setMessageType( KMessageWidget::Warning ); + brokenStampSupportWarning->setWordWrap( true ); + brokenStampSupportWarning->setCloseButtonVisible( false ); + formlayout->insertRow( 0, brokenStampSupportWarning ); + addOpacitySpinBox( widget, formlayout ); addVerticalSpacer( formlayout ); - m_pixmapSelector = new PixmapPreviewSelector( widget ); + m_pixmapSelector = new PixmapPreviewSelector( widget, PixmapPreviewSelector::Below ); formlayout->addRow( i18n( "Stamp symbol:" ), m_pixmapSelector ); m_pixmapSelector->setEditable( true ); - m_pixmapSelector->addItem( i18n( "Okular" ), QStringLiteral("okular") ); - m_pixmapSelector->addItem( i18n( "Bookmark" ), QStringLiteral("bookmarks") ); - m_pixmapSelector->addItem( i18n( "KDE" ), QStringLiteral("kde") ); - m_pixmapSelector->addItem( i18n( "Information" ), QStringLiteral("help-about") ); - m_pixmapSelector->addItem( i18n( "Approved" ), QStringLiteral("Approved") ); - m_pixmapSelector->addItem( i18n( "As Is" ), QStringLiteral("AsIs") ); - m_pixmapSelector->addItem( i18n( "Confidential" ), QStringLiteral("Confidential") ); - m_pixmapSelector->addItem( i18n( "Departmental" ), QStringLiteral("Departmental") ); - m_pixmapSelector->addItem( i18n( "Draft" ), QStringLiteral("Draft") ); - m_pixmapSelector->addItem( i18n( "Experimental" ), QStringLiteral("Experimental") ); - m_pixmapSelector->addItem( i18n( "Expired" ), QStringLiteral("Expired") ); - m_pixmapSelector->addItem( i18n( "Final" ), QStringLiteral("Final") ); - m_pixmapSelector->addItem( i18n( "For Comment" ), QStringLiteral("ForComment") ); - m_pixmapSelector->addItem( i18n( "For Public Release" ), QStringLiteral("ForPublicRelease") ); - m_pixmapSelector->addItem( i18n( "Not Approved" ), QStringLiteral("NotApproved") ); - m_pixmapSelector->addItem( i18n( "Not For Public Release" ), QStringLiteral("NotForPublicRelease") ); - m_pixmapSelector->addItem( i18n( "Sold" ), QStringLiteral("Sold") ); - m_pixmapSelector->addItem( i18n( "Top Secret" ), QStringLiteral("TopSecret") ); + QPair pair; + foreach(pair, defaultStamps) + { + m_pixmapSelector->addItem(pair.first, pair.second); + } + m_pixmapSelector->setIcon( m_stampAnn->stampIconName() ); m_pixmapSelector->setPreviewSize( 64 ); @@ -620,8 +687,13 @@ { QWidget * widget = qobject_cast( formlayout->parent() ); + m_typeCombo = new KComboBox( widget ); - formlayout->addRow( i18n( "Type:" ), m_typeCombo ); + m_typeCombo->setVisible( m_typeEditable ); + if( m_typeEditable ) + { + formlayout->addRow( i18n( "Type:" ), m_typeCombo ); + } m_typeCombo->addItem( i18n( "Highlight" ) ); m_typeCombo->addItem( i18n( "Squiggle" ) ); m_typeCombo->addItem( i18n( "Underline" ) ); @@ -654,7 +726,11 @@ QWidget * widget = qobject_cast( formlayout->parent() ); m_typeCombo = new KComboBox( widget ); - formlayout->addRow( i18n( "Type:" ), m_typeCombo ); + m_typeCombo->setVisible( m_typeEditable ); + if( m_typeEditable ) + { + formlayout->addRow( i18n( "Type:" ), m_typeCombo ); + } addVerticalSpacer( formlayout ); addColorButton( widget, formlayout ); addOpacitySpinBox( widget, formlayout ); diff --git a/ui/annotwindow.cpp b/ui/annotwindow.cpp --- a/ui/annotwindow.cpp +++ b/ui/annotwindow.cpp @@ -70,7 +70,7 @@ : QWidget( parent ) { QVBoxLayout * mainlay = new QVBoxLayout( this ); - mainlay->setMargin( 0 ); + mainlay->setContentsMargins( 0, 0, 0, 0 ); mainlay->setSpacing( 0 ); // close button row QHBoxLayout * buttonlay = new QHBoxLayout(); @@ -220,7 +220,7 @@ textEdit->setReadOnly(true); QVBoxLayout * mainlay = new QVBoxLayout( this ); - mainlay->setMargin( 2 ); + mainlay->setContentsMargins( 2, 2, 2, 2 ); mainlay->setSpacing( 0 ); m_title = new MovableTitle( this ); mainlay->addWidget( m_title ); diff --git a/ui/bookmarklist.cpp b/ui/bookmarklist.cpp --- a/ui/bookmarklist.cpp +++ b/ui/bookmarklist.cpp @@ -98,7 +98,7 @@ setFlags( Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsEditable ); const QString fileString = document->bookmarkManager()->titleForUrl( url ); setText( 0, fileString ); - setData( 0, UrlRole, qVariantFromValue( url ) ); + setData( 0, UrlRole, QVariant::fromValue( url ) ); } QVariant data( int column, int role ) const override @@ -119,7 +119,7 @@ : QWidget( parent ), m_document( document ), m_currentDocumentItem( nullptr ) { QVBoxLayout *mainlay = new QVBoxLayout( this ); - mainlay->setMargin( 0 ); + mainlay->setContentsMargins( 0, 0, 0, 0 ); mainlay->setSpacing( 6 ); m_searchLine = new KTreeWidgetSearchLine( this ); diff --git a/ui/data/CMakeLists.txt b/ui/data/CMakeLists.txt --- a/ui/data/CMakeLists.txt +++ b/ui/data/CMakeLists.txt @@ -4,6 +4,7 @@ # install annotator xml tools description install(FILES tools.xml + toolsQuick.xml drawingtools.xml DESTINATION ${KDE_INSTALL_DATADIR}/okular) diff --git a/ui/data/icons/150-apps-okular.png b/ui/data/icons/150-apps-okular.png new file mode 100644 index 0000000000000000000000000000000000000000..247fa2b90ddecacff08a3fbcbd0c4b6ab417ae93 GIT binary patch literal 16098 zc$_6$1y~ea+g^I=Vg^!Ze`hdFsq)GzaO*)AR=tyGowhC1nX zzdHH!Qo$5z8s zWyWI7>i4RZcaZw9Hxp?tjBjUlVfq;|f|R6}7Ubv*3=A$xzRPjm9K@W-V>Bz8hK4I2 zPzfi`{!5b3AZCsWH>|@mWY0*E>HhchkU~G+JW4!GwNe7A@x8F*RIH+@iKqKy6YQun=O{^HtSAVxR4P7wdABwzTIyH}e*fgJ!}zb$^2paY3i>qKQ+~`Xb=0tOOcSvK{}4r zylni9`yGF>{M#I6avW8Z5_LGts9!&$XAkusUqZQgrCOoxnMfZvWZjvYoZo;wgM>7` zJO!hiD(h|W>XRV`HA;bbhiPk_*xL!zIb)=8y@HC}8pZyH>44gA-&9pt(^qQd9ns!q z7_nz4x)PO~6_9h{M!K)>NF<427N@i|o5nEKAc@~M@RMcPo@&qV*fuZU-0Z^zOgND5 zldx(F3aDJT$-50IXV;uz6!3O^_VEeZ*9>aYU;Cwr5wARq_wcv6S0hYAutbn}E?^#L z5cBDot37lj4e<%mkCLYlntW9=9pw8B4t=~BsUy#hzrVdYgKV4qO2@1XUHNm_o<>^` z#i9mI(+B@`fRKq$o!`u+7TCv8F%(7S$*piS?DEjMbGq*+j%OCIC2fCvoLU#8rm|A* z85H#|MZ(A(dc>B0^YV9R(AS~2aH>50du7IfCiZSq_>%7VTsAZ@v5W`feJeF#6fCTv zj!(!7wnsK$M1n;=d8U^h`VjHtC_g_x$C{@@u1@zzAa^1+(!D~nI?I{|(}AxL;U@!8 z?V7pz8`~iMKC2L)nV`R@;-q<%O{2c>Pufat9B*E9yd-W{4(Ufc46#)IJCEVh!oA(S)p9_Jr+z zu3PVNAR^!AFXFpj^1%nB{qjsx&H2SeA99>%BTyc;#c^sHX+qh`bjk8G>52n8o?+V` zyCFN{w`TbozE*izg|kXF9x~69u;u_4AOHf};@Mo4jfev6dX;|C zaKvEJq>OkG>O8T^DFhQ4W;p-b9`-ZRp0;Jdc?QmiBnCIfBmhIf17%WPTU*;&Qc|K( zuIaFSA_xi_Kx1&L0h;_hF6;yOM^SR+e!^cxRpn{j#MxgSJab84uU=DcXu@8}pJCEg zRaNO1v>($g=k@#V*R}J5PIGq%hAm%y+V-GNRd|<}_=^&cuH7<}rGkANh08Q?Vzc}$ zJ6rRb^Q^g@l&f3%K1JG>i59os7Rl4l8oB;S%-V@ufj&oW@`GggGW)kb+tk&O`nw`9 zeM@S8I`amw3{cs#A=SBmHAWOx=goM(bl}}N5edQD=^{hhb&dWTLXh zm7M-6_uDk%lA*VIL8oV}=%LU(NAHMg&oaLm?Ju?PU)s0o7Spewk5du9rwZ8rQdaW% zvCt#zyoA>BIL+(TF$+6R*Y}c!GB;@@U*HF6Ihu-u{KAji#IC8l#302^ql{ker|&bn z7Jm_GOoAM&Y92MTmGn+_yY{k>tIG2*cQZ7TOODn<1FM*8RSDB{v9{P_mM7J>dl|Vr zjcH96nX|B6voq%OUtC-q$t)IU(*!0v;9}u)#q@xdkYs+NDo?-KOZ06hW5;eWS5hXC z^M>aW*oO`zop6!)0&TZY-zj7!3b33P7%Im*5&FrJRxDK>pO?Uh?$dijL z-66W;;C3k!f2vtR7~Z98c;+ut{`%X35lKo0mkC~xbU#-%I4#YKO+E@0jazn9?Ncis zzgcp;rvHj^zNfj^G1KcCRSz{0sYq-R?~na@(qJqLHSk0dvulVWImSjy|3}-qMmF)s zI73{T`_;LPv7YV4H=T^U(R?NQG*^TY8+w`Xx>GgUzr2As*-|+ya13VpIb|_%QWB+y zb$gRqRIfu&q&1dcYV=3}WBd;fY=c4vzB1(CZ{`UJ2|@aAL(S}~$^>ri z!pLWSeEi}3if>IxA%bC`cgNQLss931je3!%3)@<|)eEH~FSX4A{S$RWv|S!eN-BPFPT=HpC42T}i7`qhrF6U$1nS4c6ujLoD= zad()lX{gW?!$rRKQ5S;4den~?`oxC&=`XEMgE9^KC$aUfQM+dn4CRw9PCl2lg=?2# z^S?S*;9E-cf1-Wv#e7<))Th-bPJ*JeZ>)#}jv7Pz=Pa3J{yJNeGPdMpHWdSg{Z>!UKOpf;M0FnN0dt znMx`6ZsxE9qVvvdw=!;Dh zvW|TAacaWZvP znSdBYyRicGhbHvO4~>lgnzyij0f|GAb8HUV?-qWs5+8*H(bfB1tcCd7q`26jE>5za zed@qg%@Gw^sMlcGxTMizD#qj<$HErq-T9cKA2Y z%W)GalA@)zblJTrr$wCX>~_@4u>`ekX&TyAcq$U?G!2s;pJ95loF-~n91Xcv6B;q-->AN$SjeV+eTsErEP4GnLcE2grKK2{O8kHW z|2m6V!N1qcG|K8VI6AfgLfWYPUrxWPyy&QZQ@<-&pcyQxZ@b^|NuZ0&pnp-6+AlLPJs_I2J!K{h)q= z2duti?3lq#Xe~h&HmgNLp+QZ!Oy;W+3CH1nEoY9Gnf;uMCM+S0ze%Gr66q!Zzi4DH72Rqs7#&MieUyAQyO(2$%o`;%s1CJmW6 zD!kI%EZU;4=O8=k3Xm4Z%E=UiT6w*uW&i4q=mAORmv+Sl1P!6pv@mgt_wse>_#>)! zB4iLW6EuakT0YY$z0=p<(lX$+A}04!@o0);fFB&kU|sge!2|s%Sia`>Z%Ap316>Gus(2xuC9dp?BP0M#DOtii- zEHp1NX)#i(Hu4Qh_UILxvTlg-mb0a2S0B7)iaAl(`1H8aD32?>RotLYsD{0K?Uhci zVW?cn`mcrzWnOueS0axVXlX4X&Y zd^LJAQ=NMHn>BkRr#C&Qz<9M|=FG2n8Ozsi-8mE)62cyZ2o_8G4?Pt7MVQP}l)fT! zG~_Venfmc8;ta{A5k{j=hBR+T*mJf)YYiqtS@_w-zCy@ZG?q59aF ztr1FE=7e4W6Y56r?F?+PTOT=CVju9HZ2mXb$OzP!w9x)cwIvv{|_^Fq`plcN!@e5ehi=*=~+py z8y2ynV)2tIk_2-6xNi<+HTyko*NmGWLhJkQLKXFg1G6StI45Ed`w9(%jUs`H#nw4v z4hsohw*s427jn3kENbB6CX`Bj)uDstMosr&DW6E?rxZg_;wyHGBeO2J~|_Y8yNXz|#cQ}axdR2P`s^7!eriG(M(z!v%h*?v&lKJI>TyDG zs>fnz@WZknvLiR}(LJer7}@6HcN9~9O)SSFUEOZvjgt8I-W0x{b5h8_#%A9nL#q|1 zagVmhLOaogsN2n#)UBX03>3g+?)4u~XTEEv6{tY-*tD-})$1S!XdHAS z3+3OVksQY(XK43h^A`riK-@=jP91hUuAPE z4i4j8KfYLYF+8Jfd0jr)mZVLys8nNuqjnqexGr5mzU(S`zl= z_f+xf<)5`%T|vrkc^@GkPs=eYTJH5QxPFudi=8^uRAc0@G`qcFxL+k(j2qQNw&(A3 zUcRax)AzX!Uykd}z+14K z_j~E};oUm63H>ED$2DRbk-vB%YQO)B7I*O1(<4(Y5Q$aFB?}_vWhMf7ZG#3y(f-Ckj&qolhhB9`25K@9ypp%8aSO!*L{E<4D+kOVdgG3+_{d zFZP|K7XJJBEY3{XG9F7;PjB%Sj3VH_#N6}|R$^9*+sw+mtNWuH9?Up%W&HGcQVvsJ z^CutJSHdMXor`aNmnOL%EWIRf7daeeiFGx8!? zA&EZhZJ}}GxneVENN>+mwwkYga07Xz%Gs;)S+$V)>FE;)0gOe))G?}IEat! zOv}g!w;-Q_C48Hv!84&8)#nZ)iaE$-&`akKpVH840^YzZNl{NeZ>c0pB23vrrIFs7Ix zq?n;FaT@dBz3NKQ57pH#_4M?t+}#Nv+%Q<=>8WR)pa(KQc-Z~@{VWLy$pfLd{I#mu za*5;PrHl*zp!{Ppty)jza*31!fI{BMzIa55k&%X}DP957@5^Cs7~xR11+^q5Da~h?{b{z-7ADf9T??^z z|B0j^J$Kd912e6JTwNWBx-a9_W14XGpNn128n}f!mJJ7@)K0qT` zOz8)H%}h0Xm`)=$_K~QhO)Sz9WaeiH&Ujr&LUQ zShn&Hfxc_p18?EMEpFDmocBDs=KW8?*ixiZ804F}nR_cy{~OC6vHO*~)KP*gQ1{{M z%^mgbb;JP8$jHd1m(Z4XSMltwx~^{YhYzfCb94QcjWj@DKqJPou(n3!%*1{qnbAa` zG4P`{pXF~(MyWS&`V05{el^M9Mv2=(aaCpo$eyg&K0{BZVdJYh z>FF{$+2Odjf*+X-EN@j$<9?|-)9RoJ9ikrUqogJdgE>4+dp6H)PoDJ#2No%__Q~L2 ztr3%pvgfT@3I8Wme2LL_=A);c+F5Vt*ayW$O!X(RLbUkWNl0uDoU6rid+43W`A0p} ztF`6niQEM#KL%bm>5sRp9@@LP#i&*SB{L^SryR8~H-`)j4Us>89yM$!VPnJaxba3t z&wK&q?)QHdD@hekg_^=QoVm%p8ue>812~sTA{d}oUY*B_aI99#zb=8N5aKM%f*cUlpfi7Ibv34AtD*7# zU!4W?_)=6f(N`pKWyNrAanWjfB!TWDv5Z9NG>mQt1uc~7*)!}_XMQO^KflA{~RGV68{yWedu{6S}g38~_CY7DR;vB#nlmwXbgFT3 z`xX?=_lq&)!ew=_`oAbX(bdgyf5`AZj_lt`vRWb=ea{tVVTOYjg0n>*?;9t>K1?g( zOJ%mwg(UawO=)48l|eQke*{Hvn5P}oEwzW(vr*Cs!lmiLCObP{ zzI+)5#0vZX#7bFNITYb{EFy*r&9FK7cQ(20WOhpdbh2o6w<0~+XNP$p{eO4=BBM1a zATrSD2KIP$=CncWhx&hva!8+rt&!LX^~1zB-zk=NW+Utl#=kn8A!*X8pu=Q}8|OmN zbE#G-I0){^BPHTvrI8|wLtiK?~p(B94<&Ki1 zB`hNXpjzWhybbd_#R@#20aEs9z!P&a>(f~t)p>ftmy zMWqaS&);n3vHg!1nx94!qWv=#(V^v^MbTA!;_6G6EDocP5uw5u^^j*weJfiEUTGp} zDDgn0y`aRiTC6wuaqVdm0zT*CBhYQi1?U=p*gqhEFln?|Bz2}_+P=Ydf}*9p-D>`W zP7%YC$(=LR88P!~f1YgONry)L56Q6$<@~vIBU|puG_&N2DCS7M*j?L7h(>87u1Pj+ zGlox0IL@;|xyqqJY=u6W$SQ`6 zR7p}-+Xa*r&a;lKozcSw&|4>EDKwSfu923 zk1mI{PeLmkjzzfEy|(g%^6{SWJBLo#Mi|ybE0zx)A9EM@{dy{xuR1qBFCkP>Xk&1G zaWSytIdF9OB3a%X5OK0R{W#lLYih#-aFq?-hlcEC1%Anl$?`M+6E2#Ad{Bfo)@#ip z`bjm$)c1dvo7k~)Cu}uq=I0kpeAYM|TPZj*_n~Prx9{5M&5@oWxwRVZ3nmUtO}$^* zp=lv+X2R_I4!{_W2Gg3&x9^JN3Xfcy?)*7tGbKbF8&KCJu?%)s1y#z z-*P7hFh@)l0-RZLOl-MZJG8JExI-XRZy%opU@TX+gHc%U=RO*Y_+h%}8BmG!^_+zC zTfF(I@haHt<8~qjnW@o8d2>c_TaYT-toOpb)dBlwKz}wXg(k^x6F!78mQV1&x3!#(S;2Ugp z>`+fifr%Q-RbrU|bMAg;{vm4~5CjLD`QzhhFvz1NxXC|(^!wvPFp?8ifEebaa`>!U zGk<&?xYTGh`U;}_>eZ_aB!VFv+~N-B##<~sei+QztjGTXv9h`<(Aw6vusADFTPlueE8PF5u;qE3!u4qu-vR(GyisU+g&IhOd@&`F$8k*BV~)-&5FqY zvz0-E?iqjg=Z`C;STJQzPtT+8uC5-KRTIO_SB(ky(}4jri!@%Qn^fZWZJ?+$U9v;5 z`tFlZXnLR#US6PC>R4n`6OXp8Zb?ZAG6MqxGaH+ffx%Ny)S&VRu%g7F5()~KIXOA* zKR*+~`1rQUGfn;PE*8xmjCMKD3B(D5qT|sPMiLVf`gQ|p1dP}-w4OY9a`c$* zsNV?Xe=*Oxaru=kR8+pIsX{l2LgUX>+;;%-4HOlboPt7`)pJe+T$0C+Pni74@*)9O$6J}t@#Nq?+mdMH za??{dnDxQbyV_k=|9&ob72s;`o3a2LT6I9%@%x z_#CbGEOq zhskktxScXR5t#C4N1id8NP=3EE>fR$KPJ<_6B!(Q{PeUJURcFEG| z%^$<@$zTb#E7c0`3z^gk0uRA&{(H3)7%~6RZKHSe2w1T$u;D#m{nf-k_=lq+hY^I@ zKy+Ws7AruGK`b@=F|b6H6@)Pukoz5+`y6 zO8fc>yS#QaL2z7zZv>uA6qsrMIa_gx@^Nx)! ztNaHmt=L^YN9f~CA3b-z#N1vU9T9x_+R;&5U++5pYa+L>`>6NUwTO8yAC%7ZWbn;w zq4%q+&4?PLT8yHRx$(q7WJp;_$q!`ONT$fQDm$-tzhI(A{NCS>7}^S*L_B%Mgc1gk zm6Ll68Sa8OITb*h8$7m099qRY#;896-T>W*iL7<9Jg7q>*hv z+Q9d|0RCUH`2Erk&X#cnr|u{SJ#MbZq+P2OCbz;)i`8kIB=7HDI?=-HC`=NhXQ^l zfBH0RaX)HwTY@?5={7YE`f!H7B#+x29zMR^JRKV=>+#ig(sp%a0a0tzIU`oQYNeEt z687IeYw<>IJtVEIt^eIpy`sWj4>)=Syxi4Y@WE<3ezi6hZ~(hr6vQ%N1a$n~AGBFL zOoBxuc}mGp;q+08q4FQ?!hfY!Ov7-aEu`Hz&NZv%9i6{_Bur1wE`B943GCMou(pp2 z!fDzKg)bVt$H=4@_29zA#}9duegA3G`Blatfc^DA1it81C9%xi{ixXeiJqDNZh3)Q z@L6syQtdFYxOiJ-PMdV;G$C+&8AtKZL4S950w4vh?M9TUbm!X&!dg*%uq_K{Kh`cx zWoXAUtinMX`0Oyg>$A3{o&W(nO2N=NqxQHpiH>@WDF+Z-($mwA{d*oIQTV7k!pOA) z4d`K`KF%P)hL;e>0y`Q3>icpNm?sif4|<2slSGgiHqWjB%Xw@KqBYX$z!$eW_v@ux zTn=H+?b=r@+E+bhRRkm|XSc?f3jYD$^5PXmLPcD~ZXLwAqaeXO&r9ezs(P66hUJNQ zompS-4xp_NkBy*9m)(Pdf74b;ul@?sMk2c-eukZe(;ETz7Qhl`$n`2_<7l+M{}H}2 zD)e)4z)^ofA2HT>a~(lQm37V~lCL_n<=$`E7`iP8l%@kp{kWnE(yEen-5Zlw$Qz}P*z-|3*?Hn z0CVMr!RU46wOL8NjE`FYOVb_^c?Ol$AbuxXS~VScG(R&_^7xDtXVlOZHKa&MNy!KY zeMmQ_qJ0V$^a@gXPK6@QPI~)_2L+ial#WkVPHuGJ@87?fe?ToCz6Ka46MMLx3{ilx zAh|F8l-MzJ9En0)D@o!`Y@hTWdcPpXioJ6gFehLi|0XD^;E+7XSeO}K;Q&m1X;Tvs z{;P9>-m77*5Ox7rL~?&)BY^fJ&IZ7VcfH~kK{x=Oa&LcsdTI)iJHdY_ zH1Ejg%>NAVU6X7$6!E>mB0{%Z^C{95#6C#WEOQdEk8zAO#O>I;+$~!=?QBVIo2ppu z$PlTF8ZlsDjegG%kL&~j**>UlZ+9TrBM}(*_c4VJ{}66ZB-uavvaaE%OR%gV&R%kL z9}h~2SLDn8{+J%8vo(TMc)bwyo;HR%SzcVoEOOp)xZb4e)7}FpDvRdjrXr%q#s(as)<$1xjfzZck{1K9`icvZZ1!*B)HFG|n0m2@sY+?U?5)lDf zAS-i%ED9*ytWTL13=PQH6{K4xlI4miQy5(!sg)|0&n(Q(U(3qMmVGfDZmRuubm@a~ z#Q_01h*JY(mDEYIz{6hkhA;J9e>w|YSKB6TwqmH)ABL#!k6k3+Qa!CY&syd^0iG`h zxcHTBQ@8YuBRAb@?Z>D5ye%zDWJS}YF)94s&hB3j1S`s99Vy_Yn}@dIHz!?~aE(G${#lptMhM9{iX2N#7Q*~g@v31FtvCv$9jPaRI z9ppXOHl%ZfrvI$FHMQm63%q`NX*I%&^zr&vF23_}lg;f|D$~O?hS=3WZt9o-GB<5f zrDun6=NV#B4<8rT1;n>~M<=KKnwpx{K_^9+$~N%nmUi}>4WNWH%*>KMK$dH zqr}r_QF8n6kG#wKa4-x7qXDcof#fHQN$z(`8WU&TzcC}N#ulT_KGSMCiV~Ou>sRDJ zF`mI8Wo7jO0zf*5I0J;-R>DMt3H*`g*x45FS1>!)QO|OL_qW%aBVN`?WOYWdJYH`;CkrM z2Lj^Y=BBxiV)GM_gtD@+4ZDJz&b$t@;HDXG;?7nEh7OQUE7z4}L+gw+77#0^7SDRcQGP!+qviclk3`o(?P2a?qeipevOCYm{k^;Lk;y9DD5)+n8BIX^$I0-@^5 z?d|OygtfD)%LN4m<-!!+-UBwZW6c!~$OpxRC%d2f9W(#v$jA&ZZNcrUuf#Rx47%2W zt4m9VhCQ+!%%HUYMiQQd7@3#^iH$e4xWC9(WfHyGL;=?N{PL0wFE5pogM&lReG{^> zvLX&8#AsKbeg}yGhH1if=<{bN$P@WGfXu81V0xv@ijt)j)r zTIHHHe6ETiC2;*IY`y|SqvbHxK3GemHs@uYHIPES=2!4&A&-Bw7>e%e-A)w#v)UUJ zkh|tuiT7McT0bLd*mC)7$tr2B)pe~i03an(M^_h^b0ZlJP=`~itK*4DNw+}woV(iiwIO5X+`k1V5^R7^p$?X=#gC; z9p_mab2tw#$dt)vzMhG2n)SZhukXUci$TG~#ieTiwmdN1k(P==MgydfS;_R7j-L+L zXvrxjB1sh$jMxxX()fAg-&}vm4b0bo){VCq9&%9!Uu_)%yJ|bL3mX|3nVOlA>^T{E z29~n_^73H0v}yJ5hY|*$JbD7?Rk!I8Kz%-cZXNu?hcE5^7kkW*0)jy$^e)20?W!>~ ze;#V~@tIvOe`_zVmV@8ZrJ&4%f`X23?`9q22fbxv(EjvXntI?N({fQH*z$2e3vq@* z5q^}Sf#e|eK2M|;OT4?iRCD6)wtdQBG zPi@Zs{qu|KKH*aB^~zHoE;3GiK;cM@I*7;C-v_v@yq zH}iegkyK`vI$kratga?2mIDKhIo@Q$eH4fgzHe*e*VEEU-8i~*Vtht`W*>?p zFFjMlT#sFVom$*bSy|asQ?q}RAZxY#c*k{Qq#_|YPXxWqbuBL7sE3R(l?A&~pPj$% zC;{Qu_+KbNIIrEee#cfzdwXbIU7cloi@S5N?VE|F)h6b+ob=F1)-j*@ zGE2VwU0rr9mqgX`k{Ih%t!_;#2%ZL9Pa#^JgO05~Chrd>r&d;S=5q@b%Rdp~Ncz#5 z5%fe7JYE5?cXS*)K5kelrxTMiFfh=)90~M$8+8uDe&Z|&b>`27=w1US*8-eB+Qd($ zCD_S*yxG5!pu3xk=fpB52rr>YUy%YT$To2H?LztA@&(p?xAXBuj{CRO#O}N%K&;Yn zY|SbB=Se(yCT)?gjCWTAA;_>{N!X;^!$?~Y69%u~>4Ud=Y3N3&N{|NA&c@2fDklen%Cn#o_0~FuZ)TFuX z-K9m9R7u(|{_>V>zVg4!@p%DJ(O^2#cwCgX5OE(LAGg=9&k)`ROO4xe)mn&xko)UY zz)&{)xf4ax_8%3v(j3IHK1e>HwYDH?5T_}6*o?rxm#EWqBm0Gyxv&|D;R>TT0ULq_ zQZQl7-Jl58n6;;2-*bqDTD3MSpw-Q<*}V1ume*&9#@7f{4?}7eZAqoS)ZrXnJK+@$ zjp<@Uv61Vb%HugR+0e|{ljU(HeIcgu}x-tqbYUu70|=5pnyIv*e37T3cmmxi7m6Mt>= zkcGw+Hm%wf@rbPxjG|1Fg;LmCIg*bweyeTO^+{BR7pjDgPWqCCu$k`XAw5=JBl}-i zx$YpAJpb-`xr70jZr7*pc5Nf*Zr@By2$KX#+bB&pZ7}zS@YL*E#F)LH~xD4MoB#*qni5q zdRugK^xm)BUwaz$-vRl3f-%NGgz3>)1d^*#xu)9VgKlb`l$Dx9Gsybk-U@!^`cZ=Ts6VZem4jnq?-Lp+Rqge6couA+OseQbviwi5g*`EN z&-d;w;32Olv7*XQ=4T*zB9r(iI z?^#y@AvURR*Rw-aHDmS#FTWCd7f?YaM@L6drfyo^Lu4L&Uv^@wy9AIO{1Ev*czc$cy|ABFHL*j?S-38x$_`q6g)<>mV!{=ls`S{ca=!Ezd4S}4Yxe|z+ zf}It3i!~#z4+wr#El+UzcCQBhx99AeaDDZUPuS+r(1?m%3;3dNB69F@4MA@86T>VS zJ)v~m$IsnM!xy<$`$8TfHm*hldT&dq?|vg9A|m=EZ1YrcLUrqOjet7{*4NPRh#1=X zOp!tdWPUQBXI-vz`Zp@XKQOS{45IX#>`5ra^XJ2xn>6Sc7{i#7%yHqSa9{_8&3Uq$ za$baft%{6`!^)bl9X>Npvr0D~jv}xKCo*NNJWh ziV=Eg!#Zzi{)e9&SK&Q>S8&1E z0z?U*=S$A~ovp1GPiy8^7{7X#(DV2egxGm`ou7CK<$wAqA1hAdK41@XMF|B|eZqIL zzHT}N<48p4*35%!*#AMN=F`dUOkAjL4N2t*p>rdR0S--+82bR5OzHGz;QeobbD6sY zgZPV%9A*($5^a$2=2%1wfFqEX`pvyFrw-OL$m~L$k(*Jw>)N6ZU^5|doM>TT;oE#w zM4UB`0}6%0H6A|x19|pwwVdp@gKN*lLf0dD7G2wo&gBOyWlJT1*9Yar z(69z9;e!+{d=f%f-4iR*m}XR9I0&vC?#d zP0q|fbhb~fmZGr9I3LFQ{*8;(8Fw^E-e@(Swg*NvekYA*s7d15z=lU5^F5)*-YG7# zY5;LqFwv^V6h4GFGogI`{BDg6-hKLIGWahqhWgSwE+vQ8LBpb${cB50zFJaJL)g1s zozhj;*LT5JcUos&CnD7EWJ^Q#PkifXP`rqq7IXC|5JRS5Yo+s!Y-KqLSw3_2Freta zDEV0M6EwSWZXQtIqP{O9xW2oeowh&!JT*1d7(*Sbf8B0T@0ok{9T(uk#EL&Rgd{;$ z>AR~>{j)!b(sX7g{yjaP|E8Q$FhSHyOH0XCS66+YrT^igqePA^$F&9)A$@uO6+JI- zG+F9XlQXF_V5uSCMv@-XX*HYv3u|?lt>Cb2zE@zzqrIKkJ*Q@nR^+d#^uD`Tg!5)A%V||>$12Ok!kf-SLO_Q$XF3#X#;+r@! z8n;-uD$PhL)vtwt;4^F;P)Ikogbx^Yc-nNRR^Engf0VT-K$VnVm?D+@><6S&UW6)% z21SP`TlHJXhElw2shke$d`L>Kht|6>iBwnY!bKWGK{jaZKD=)oWH;IZgq{MTB4xtw zx_b5S-cW@1s0clq#XXvPeErm%`-(R6DI+VMEc*hR!nkRKy+>V9<@xnwNblEOFTBVh z3tn1!Cq-(=rr@7EaJ0XrW6l;WgmXB8#DW*och|euoitV&4<8SYst z&bI)n?p_X9wIIsTHK_F<(Wr+V2sr|&WAqZTd1fObe?MR#hw;Kxj#9NxW~q`!x6ZCP z0aad^5Qccs@E-o<>#{wwHDup9dT!e6=YF2=|BtW3 zkqhp9X0KW69W!h18>^=B1_PA@6#{`^$jeDro`3 zu^N6Sc{!bDF{v8(75(*!eZAw%$+h>=*-ao{uA{E9@^_t@c{~4;+BPRdmrqk?Ecn`e zI^YC2go8_$Io}Tq)L}@S|ND_|3RVn^I1`?v09lkyO{7X?_;;%=zQQv$&;NdnqsTf) zlZw1^;WN$|MpZO>q-JEa{?dP7LM~2{zo;Zm;cbU_0u?eAd}?D*xkl{AkIWGR+oKb& zdL4Z3wxiA8i^Xzj(VNv&(G-}4E)jq+9ge@kGjpJ!6{yvzXxm z3_mFM60e%P+D8PlTmvO-%maeoaG`j~J z(#GbFTm(ousAM5o#y93Z)5*`iVzL>MJ^`7;n6GaO;+8|ynX<;#@e!C2!?3^{W$fB0tQUO8{)$VG%MyYL99SfJwLpcMZl3F*F~zM~zpIqfwz-;L0( z6HgLtQd{zg23gYge653=IsKTTY5B{f1(zoxZ0K;lu(wr6){D#q zs)Z&j>0gpQ7-rHma8mxHp6yNZDep60WZ^e)kAGq$NqlcWML#fk+Gfp~D_iK3j=lMT+_%hx!GrLh2FL@H^i{iBDoNRdUimoxd`3}nRZxn2 zLUew*@>YCMpl2mOqe+$9JE|rz)bSb%ndf|? zJD1!Ho@4*U+@?ReKa$Gt5bhr@M$`Lnvag=YmfUm1J#Aq`s|#k*E95r=vE%p6S)I53y33|&l|kA2@cNwnO01AEd;)wp>LzAW z7HVwxcO;aQo8WZ6^uzW1Ify=Oc6zd~42BGnNd5H^Y1CugSHw?*3-({gg|XA{6xJ;5 zeIotS-b%fUz{WEbw@yxfLN&6Sr*b%4S>UIj$%(ZD$}%}lQE7N8yg!j z^>ALxB;J+c%~}y<=Iew2E?5i*x*bA+-bq*-!iwKn(t8#xW<*IVkk#-Y7r6xl+PMkh z*UfoI>KR7{+dI?5-OlJ<?3*f*8piFED@w?kSN)q=&0hJbq>ADoPJ6w4MlhGv zF6I}Rv=Om~o5P>URR&J!rQMfd4eI3l*RM_N41;)Flf%L35MR z=`}?aFAR9tL?a`kJS;TCa4k9l%;>kT2)+^;XK&pq6~5r^X<CSn%Oi>4qk%E&(E(FuHBmr=L8)dR@wjNbPSc#k4{#X z9WwEOuN`mbq_qEwjO|{xR4s#tmOXbXr>0QhZaE@1=l=h$lR?9JvfxakX1s!^ zmHQhitfdC?5zHSi35wNXNS0ni50nfKmdVBA_9ctm)HO0MG9^+@LBuOHwx-E-e%x3p zOnN0uy1u+Z9?P`PR{BVUy!J69FF}HBy$nUegSZ3_1N(te#mUEu5&R)$Tr*FDE#d1V%Gqq+mlt(y=?r@jAPrXuS5cT_EPAR zq&Nm7Xkk~;hylU3l!xnxdtHZ!cl^`Y#D{{jEWDBXgE8x%I-$`?;~{a=#ZH*j?SW|i z)w3Hgx`{-cnwC|oCPyBU#4l8`Dn8|kDj{Pq9q#V$zKG!~$P<#4qrj6ucyi|N2Kn5F z{|g^0<;c(hep;dPa$IQ2TT7JI|EP^1em^NHDyoZ>V8W|wy#RMGiLImDRKr@0Z!8UMC;A19JMrX1 zQg{^<#h(9pGnTe!Amrq6q|v;u0V&ALwDcoi_}0wc{^V>3p0i>vEawAccl_!!gtFg8 z#95uBnjxM9i7)#SwK?D`>n7ENPOBMJSxu4YtX&Hxj@=N1vmy?|PIBDnZQ}NsYcOJ! zJY{)mb+X55u|=K7?iQFXO7v8ScJShCAOCrHn{AL7bS)LY zExP0C%O39r^fNNN+T;p224r;#B4A}@l{9P?yMCk1tWBLODUV9j80a#zhiz~kDM1;p zOVx0a{Z)WqD-^MLFu37JtKGULvME2iAbg2Say;%eJA-o_mN^`?!E3rDIXH>mvEy0@ z2&<*ZqQ?gM`ZU?{*!|X63sXC{&#_pW!> z{%V0Mp|YI5;`JVws?K+%C@rjnHrOQPV+gE*ro(UUf4+bJ@T83%QHF}iHzUrZH8|YH zLbKPn=G#90bX@XcTEIRyt?MM4AnPJ|=I7u1q`9YsOAH=%c^GDD({Tg~vr@sF&?&Z` z5tg6A!fR6y!|+nGmxRhjrBtt?c75D=Le@%n2&yb&X5?CyT9%BQM^}CXz$hbs`b#fV+!JAO<}c5gO4Xnv_2xIDW;uCR9!j zu2PLsl&9kEV>&j9*Q@*^O2?VvlK;VOTZdH|Zy03Lw9HJXE}I&{R){FsA)#EXwzXfp zhoC>`^*U18_7AJKUQ5*Ir#F_A`8UufK922|0 zad{`|$`)=|l<}Ra=#6bztJYCNt-{E%73a|&XPrl5AG3$ImXo#)RoO?TzKj=e#Y9EJ zNO}R2QWf1Ic`*xm#lzMjiK>ff*yX80&Ga1sakbwyLTj`-OWkBG3=d5UYM{ZvGlkpZ z#fn3)lRO#C7$J<^9o>ht$@e`|W#>9Crj>tEsP4-*Sk@(xLtt+NBVPYTS4}HGJ&xD_ zr7anFz7BNwmk_!)`5Jq^-ZUXD3G^%Pt5337CAR)z$Sz%14uRW@O$>r*gXA#TX zJ5M@5vvTu%%YrDWAp_UCyu9p<)*YEThlRTuxu0h3#v3J}dyJ6cn^+l}H1qz}K?wiG zk;k=Fcif}VvP`6aC14MhSQE@Ie9y&PB>+8n?5965-EH^ zZy;^Xt;jvdZZBck2>0PE4F7kD;zDr6Q&?%FcAT(2)@y}wAIwA&-z=v6XkF!Z&qdVm z5Z;IA^G)rcmITOH23YdtzQF6mh}2RK{eI?Ei0H>LwtP_IE8h_*cLD8&4v}c67I)%zS?8BE-#IS*Fy+s2eeEg*VQ4zeowvm9xqKxp9F+^TSd&kgwH&1VB^&BFX*vJgS>9;-ah7lSZ^nW2rs)s5oz!l@s zaC3UXm&V4hy=+udRNOxGn{g+%X>n;yRKi^$2Y#GR9I8VHFnpr$ zLs;3d2)nT_-OfpTeZC~E%#pAk=%r7-AV0`CQV4*J>D>tOhkS9r$?Y5n*Af>Nu zj?mIFg&QPB)(DZ0K_x!x=HEs#5Ez=c$D6(`W89cC3w^(@$i$$4n^W+rXpA_2426p? zTdBgc3%2-YAH$sZsCQ;liW-_Ua$W&S$jgl%j0S=~EcGlTxh@uTSh&MKsq0gRFtl8v zaB1Mf6(jdE42m=He7Hp6g$Qu&S9(g}j$^hsFu(|m%>H!yIlrV`_}4tm{zC?ais8`^ zdUL=V%k1U?jEEArTrEV&;$nK^tW-z{?-qc2_oawwLCyvDbBqhN2RlPCx23j0fzR{ zz+VnBRTWZ0qV!qT7K|70Ys{-YF57MrWEm7bH#FL`u@6A-Y^8p7s$TJTV}3`ncMwoP zSGI2|N;hFNZxd#m_E&xE%?Ad$e3p7VtnoJ<3WJc`ejLwh4R_WdnBa| zBl%<`RzkukTd1c!^~HNPh#CeGd!?ZUtD0iR$jCiB|C~7FA&1R&OtP!Mf!X2%18)E-47U|eRyD@Sv^bM~ zgDWwzn*{q}XITh|L4l?wOj3~|`rHhqRalO3?VxsI^^a!5-{Xv65=Gw)BN=CF85wn- zqeqlLwaT+2D?vPrJREi5v>wBlI=^mq@=|B9ZwLwG)RsrGRH0Qdu8wi@i`kXDks-z^RaLo@x<@Y3Pd_u;JWXZ*Oa;z%3<*h>t5q?{NPWh3JJM|eR>^*SX@d{3U4B`0HV zA`@oHqEtjnK0CMxkS*fx4I*f$hbMj)Pr=d3*0QO3iHG{FZyEV_bLzSSc;s0dsYA8TraQP-maK34S)^I%;PC(6IgTpD@p-oI8Po9n{@ZwG03 zQ%1@R?-4M5`H~g^1eW^UZ~|)fnVvJisqjQwswwUBl&c7{ESJh3Cb;OlG2tXIYQqwB z>q;G^JdN*B#qXou@`bqCc7q^d@KM=PS?FV#FpbI^@jto#3gAExypPC{Me$g4b=IWg zor_>5Op(Lj>7d`gTl3?>ilcCFK$K+P8mAL1X4IOy(~sMWCP9W>Z;|~ucSMQNQ?z?U zk9Q>lO!i*Vk3nfO$>stu*+`_b;wjuh6Xl=glKl^Dy7p(DGfC|rF4}2=XFiDg%CtzMzuKh-9r+7`Q%{5Mv8O7D63>yhUxNU ze?NeG&tHYCjbmy~f~@iNjhrzx<&5F6f`^o46p2I`Al;Sx(|S&W97BM4%!ox}2;7{I z-S_KGy*edU_$>;s4#xGjMvy7H7Tb|H1hndx_nxr$G7QN)Lz z(lOt1Al1!D+3h$WG>Bql#Gi6YHN<|)cLk9nLS|!Qc;sZU)rgbUMzHZG`}Nc)<54jEGv zXUWvuJGZID;Nusw%Xl`a3|urGPRlY9_X+$s3-9JNjEMKJq#cpi+-7O^d5O-im&Y$_ z{a2*8F+cy%PQ$c=Oj*pP{a{jk8CF5xXgn2YF6veTzuIujRD`Hk0IWP%sSf`AmKDk`A$61 z-Y}6~5UDB@GLLVWmZyeW5}^Eq1)C+A+}1chQhDyikEMX>0BLAw;DFHSP~i;-kZzg> z9P9sPq~R;Jl8~)xtSD2Zx7~pla7rjwMTDuLPeoK^eS<6d0xk=Vv;&rUb$*+hd;)22 zFweT5G~E-Hop26v6O;G~F9KHjyzb+a`zT7Y88=9{cT^w9{=2NWX{FJ>GMY?VmraxJ z6q(vO?6Qk0v{}^551dD1#uyobdrEkE?k_nvWM8oyMi@b=>gvuv?8D0xfe*|LrP7Aa zILcbvb=XitPi@D;WXini!{iMSFV#l#i&r4F3m76KyQgqY^S_39B2%e!m+BxvrbpuA zgA1*blmc0P*~m{8&0{5zVW&qiMXoMb#9*zfZS&*?s~ocjA>N!v+B0v_H)=(c91Df! z%{PP#H&1SEa^Pc~1u}B_IO{Uf;Kn7|8CZJ9I?Cc~MU;=7tT~h0T<5hpv1YX2n@%)G zVpp4S;6`Mj^|<8GM~frztTtmN`JEwps*5YflqJnv!ZaIdabiVO(?Ru$nfL5>4<&~W zjAh2HBgR5YT!}S}asQez`Ii428Ts+y#WA7MQT^gU@Jl8o#l#~x+TSbkK zOlaEIU!Z(@5{F8mi+d3AIntAW!jy%#N}jlU?DfVlD@$f*p9rB^5A=Yu?Py9&R(5t3 zI21T)N~#GDrMOY{oE`GtYg59^L`arh?IcH0_ z#OQGFy_bBYpnQ$RRhLolIu}kMSWea}yH5vQZCAKpm|r2Sgo4vI+wu~H+iKB^m#E*4 z0~W$TlGt`oOYN&M7M5s_0BdybqEsGtieri^P{;5UMwu<=ZnNaDr;OSJpCm*>+O~Wr z=2GxYD`#h6VNPEpixVVdV7pzvRTf{49<3D+8}+vc!=8pUclV~QoH0CC-wqnS>U@ok zgC=?W@{8BHJ>=64o@lQ}>^R~Ig9So+vhgANwp1Tjm$DEgtGy(~%sCO4%ec{&;wr5g zjw6&kGPny<9+!KjBKffP3IbSkCiA|aJ9EVK*uDt4z#57Ghi2 z;rWOh1dr@y=Bf`nG?S82Y?-@~mwN-)-?a)qc-Fzw+znNuK;ZEk(@R6uJ_D&|0vD=9 zUBXN4J2pb1&1H1guD^g}yK7hW=XfqzoVu7{kZ$zahk9i7ZjN5}_WB5S_je@js<|@z z{Wi*n%22f#Q(6r8%j4hT7jNthN8}pDxax*scp)*foqdr}B$V>g*WdQZBqtfmC_0R0 zEQ5(A^9z&;2*oGoxI!+uu@p#v762iSGGyO%(C0GST}IdkNdhZQ)sta-sGT`wv-1Uyw{{2J6#H=`zZNu0*sy&>1ON~~)YrFI!b{lppiA$o?)KVpU z96LA4Nu()MUSdLsVGqai)3@}&7mR1JVYO@M5$_R6*TvPUH4e>U5#Ck^?R~SKu7>xL zXc9F&r1nfGdPXK+n=A{ERA$!wkX#1Qr@o#X8Notk^s=eg^s~nKQKet2vwC)ui@GE& zuRUW57eR_VWJ=U9<}}J7&!c^MnS8wwU36mz>NBQRlGw72i$jk9bZ1(OC zMI=Omt={ua?!BjEc^iKSX!tTk zTSL|(-~{;HfFCBz;uEbaTh7Vow0M@Q1omupx$M38$n-Dx>K7l_`+9QOH6uCD?-tDG zYf^>sS=-oc4no_?cHUm%f7N0_tcq3d31P4(InR!@{4&Empp6TG?bbwTNKXq@yEMJ> z5&7jtj6BpzLY=0iAoF*oBCoT^;qBWBe~UY_4>)s|9jFo?-OPSa?};=Dg!TrwL#j*d z{uoH^+faQFS>hr__DKKw*ougDpS`n+fX`o-@lAz_EOFM0U~Em44kXF9M+L~~Q-V~g;VSRQ;~_eYA|gwCM+6Ejj( zEKIbac$%BgXbFEruz=847H2^cWJ?&nR(xa+e1qm;&x9{kaeGE;3o~PD@jV>Yx$Rn~ zYH4mCqf>;2#E1xZW3Q2ormW+*(OLB8)T056(Cw`) zmF@?I^1ZxI{(mpIj4gT5XC8|8dJDL?vBqKlS8G!2X<5Gff*Sna_rO)c^0xjZSnMph zFR02zvCAGGeZ9f37a&RBHjNdJdpE)Vw;!Ic@y31Fi4qcMk8J?lyd_Ml85b58`NWOo z<^hkIt_9%?0zOSx`Q)*ghYYtoGa9E?%>VTQ+{H%bWC`6H7ZmqR2kRxd}Gc%iB!0EK4R{ZDthGhix<&at}^azJtr!JE3V@No} zh=xXL8nkFZ3-X*wK!T7qq{oG($fHKWu6r^N z@qBBG{LV4jji&I7GO|PT_K$n(%iIO-xjjfuN;TE93wrS548l7xr96O-B*^7JvtTSi zOkG`FAzM9kl$w#E!x_4@TBhEm+0pN)#0}L-ZB3nc*_2U{n)Qkq5K{JFbFiN3+YmV# z2cJvn8W`io=qYs`sUhXgR3Gq{M)8q{?5HeoUjJ$#{giiKb{aPG;&#$ca5YWdY+Ki+ zYM(~XP?PhzdtNL)Q?x+8CT2gwuOfjT7JdifSEq*fmG^6*@gw$|Rv!I`1Mu{V<4M4PvN*~|1aWZT?z zG`muGn_(QSHN7E5)*S&Rv=8xfo|Y`4z4ITX0uE#Z@1 zy%Ju?(zK~;ZT1E(7FI3F&#;*$GGZ?jyHYHaNZsPnGBY@ml%<1*Nt2^}SGNYq_!p=s z5JHAb2!t=U)&e8qGh8kf9CaG1BNY=Qpk(4DlRoY&LitH)tmR63qRT#-alknK=aZKs z((AGGVm;4~Z>7yx2`%4+v5u(N3L3%{pY|PG_-rJ~mU2?TV7@G=mSl}X3PEaH{A9Ex zLoqh?`0mX(KhS-}c*=36!7s~O1n z1alY0mK)lq)k7;tJtf>Q6q@L@@}u)hDQ}O_l-B)M&O9>fbDaL#YfRpkg9syqWTgEQ zhn0hK$U=izzjr?4bdoyUEg#lJ%GCF-cuc+L#Tv(fu2;GxcU^o}tYE=M4cJ~8KE=N{ zwYwbqz0+~@=?re+)!ZfL+gq_oqyB%-XB?Y3;>m+qSi7Zlh%7;WJM2vtswJ@C5n46= zI~Ty)moG~=8gVoa1C4mAIPEQC7W6K$;e&*mV>k&%z8;Hw)GN-Y`5bx?$(?rhtNxEw zD9&pd#l*pgk2U>q!Ov?g4aaHjVq-VyA4AnL{xjWg-bp4vIJYb+?6puu{5;{TUx$_Q zQiu-p=H)TjG&78Rh&4R`Nhw=@x85=u0=ED3uZ_T_N^C!f;%@)ZzA<7Ny=))gBOowqxW~5bz_s8En%yiEP zM{o98av!s@+Uvx_8@sVqWPVJ@5_&!xaDCjWm1vzQ+OP`~cko0}-^4N7upE43p$ZEc z+*KDZ4wI~okSq_AERK+@MZ{wkuZG8CW|}7v2S2S2ov|4D-qm&SBH0+JJ1X$s*Z;LF zH7y5lhYWOx{0pdY=x?>6TC1M7!$ytNyTaS0@vZZy)sJ;vn@e!+9d=)n`IBWJnW{%7 zCdL~pCe)s80#i4OKi#fRT{PVG$jvzXTQ~X@jesY1YUD)By>3zB-{M%rqBz9j1jJAq z@j*>D)EQf!?X1il)43|`12_@OFfPP{~Fl0<5EP{Nefg_wUxV-}bd1HfHoYzFgF5 zeXbDm5|b>EmtsxqXhoHULHhreCxaGhbZWF&SXii#CEvSpRiA8)4|smDw!$wAu0!Hn zfFTIGh_?58V<+^G$&q@qADH_)y3O%>7Js49tVZO@-X2{NrC8F{;iS2Z4f5>lEC&&~ zjg5n}y&_s?S68F^p$bHt3M)jTT3UqS4J*L_P(=6hkPMxt-+pj<7x&v2_t*)8SQ2Jc z^aPAp$uhXu1|pa=6wwMWHrl8*+6p%F;Dy>ezuH(lj-Dthl_*xD#asnm^L9Tyguwf8 zg%)#TLqiA{Kxtc>x21=Nhpm&7lZ%zr0b`@y>0*fqUAA_sDO||%^74eIr{{O$dctWN z9&7P^GZHxjKFHm3s`}&QH}qYmR4?5a+?+ai0c{0@f`2o1!@egon$iecnVG?P z-W7X>qYS#Ej*5yB$AZtv$(fp&31c&8HGQ}_ogHd^e*RLMGo_HQu)4N3LfdMh4Nsh! zKp6M#)tyfOj5`c${5y;i5?U=p?hl9ut-lR_(ON6cXz1r#J^oCsnbvn4UkU=*>M5bC zqXRCDgO@ilHWme#lX)p-R>@17v{eX{QIYgo11%c=UQ@K-V&qr zb~E^&|HJLCe`$KGi1qv(*Pfyh8Enq29(B8w3_8E>uJ#NWeM@*ddxA{4dp`F@zFD4$ z$k8jx7j#E_)%(b$QXqm-zZhRv$9}rrMYw3_j**d(0pc6yB9H)1SUMfEpGENRVGvyh zgGe$jKUHG3j-d^*&V@O{8RhE?Y6=J{S=-!Fi@Q~P*>eT9>CCY}=zDXjx zALj45&{9;Ku)aB8@2cS7>LdTgK&3lj0T`@QY`%J_>9lP1em^=HJ@Rp;KoLvru6 z3j)ZePoLC|BFR)I_}*nH8;%6~SfKX`aAS;>wlwd+H#5^eUrj8&YvR7IdF8#RC07t| zP?(=TlpOqMPA2Sy4rvH{@LKcPO|-GMA6Q!>gAIj97GovI(n9laY6=oP5W0v&a6wpq zU0yp>-V&kvUNZG@2Zu`Uu&Ejk^>J}^^RVW`5yd#FgG&MEh zVqjq8LQH2XltC`9-u6=X?xh+y@R5R#*a&(!yCZe}axvW3*T?9SFJ}eP>?dN_$Zqk* znq*>&JbrCz+%SsB%>-IIN4M9JNOlSD=brpToyXNuU8z!y12%mYhN|x8zbALM!T-F` zF)?>KPwED`Z#UvsR`ivWmHSVwX$azrKYm30{{6d+on6?#HYCNTFzD_E5_GX``J=PN z4gsBn7o7zEk3Rk%JN&uv!J=N-@(BI%5LlF|)Hqyw(|9<}FmYCjH%uha6186`*EXW^ zmA%yKQ6P6uL4#(THZA%BWSE0yjZ3eBF3_&blV8gw1rybGop=5UJ`~=(FB#b8z=jVk zd^)w>E`5{yyu7ruWKz5DTDMLD;eo9(R!pv4FR^c<3ekl@?k&NJ9w%`S{}_rDKZGh& zz`zSMx)*PR>c5i_mj_&f@1xkY(=RsHwWBFTm%jh)f9XM}mBiTsil_H!wRZ>v5~fxs z9u5g}YpXvTAok$nqsZ574nBvakct0uwn$Eh{u=Rb>t92_SwDS8j~3pMYi z?H8QvvOMjo(iKoO;A@RyH|fF@XvHbsxQ)L^Wu4V<{JQAmD3$)w6CY2Ta@Uz5)rB?@ znyqWTKc|X6#CH1MoL^Z^x^|p!ub< zQ)pps4!%XdRxh_e^3!_JyxqXl(^LJ;%}o$KD#I#*a4%a&pTN}RUA1Md(g zICF%rV`+^4er8)2EZ?`(2c8Y{Z}`9k^=|%|l6v6?{tP9N3r}rqB!O$s`ff#pjtKJR zE}ggk(9Xx_PE4wJ%0ACI4wC*F>ZnK(qudc-)t1V2I*y|Vke*lb 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 literal 0 Hc$@6A?A6c+-CCkc_0kPb9T(->^W zv9V?2*pcP+>aNw^P9JP3vMgVcp>+Cr&YZJ5M|*zfzu))iJ2LQpX4E&m;0GUk5ac-S zTY*5}QLEK@o5^HyUcP+!Gn>t~==FNXAHPV`M=xfPlCl6HxUZgt-DoMomP42JCX>nbwE>?!dv=xCY<`I2 zxHgl?bc?}YaHy(EYnld%qCg12rDzWC_N74x0SJLf5A5Gyhfb$k5V#ClcjU;C+uGXN zF5NKji4!Mw`ThPES(d%sU@*7<(2Ak}08~{)!T+wq=d5(yT1)!Nnl<14VTOH1D0j)2jG!qnU-a__}~JhPNyrjlOrO&6y#ZTHP{#mkFBxK znX{a>V#SJwmV&JaJUu=AUirOBu!Zq^S29IcAcUZ`-U3EXFAI{Uug`Nh9Loy4c`hLc z!pAgCLsiw89hCu=j=zkn7){Koiy7ei8f_JQx7;NPLKKW;*<}UZJcak}-J1X)6<`aC zR02OWk^vzaKr|4dq1r-YeYJT(MHkxyMNzsVM~>Wf!@vL_%ko4e(9&xH3ZY1*=n5bN zyBckvZydDPW&`iWfs@JPC*=#31N2Wzn9eG5{kOqB=PfJ+2w=|j%9Sf0zA0dy=l@m- zvJ}35N}2;}&;hGG<|S1vhb(PQr*p}`tN6tckH=5ZG(G3xLyUNZ11 z0t0{~NfV2`-h`-JSC|m^tjvPymV%Z-miC3_@p!&-Q^3h&^2|a3M^O~=idtL*gurWI z7WC0lpwdTL;QZ%3>=*-OZlCc!L5?o;nU=DC+&jkKGsoZi? z!0~we)MC-@;|xW0K?riPwjk_ctpF`4DqcHz1p`xA(@g;{7I+jzA?P##AjQ9AUMYTd zDcEvl0pJhkhA}2&(K8;W-hco7+iw^c07Ow7UkLctYU><@KOavn>azvN@}3ZabAwZO zvugl^Xt)wh!QuAqzA0dy=g%$#{NOqd7@C@e2L3q`hgLzorQk=WGkEc@7oaL4ph|F? zXt>8{Pv?y!KxVqj2hP16zspvhFnr`HZ(N+*tq zzM4euwIl%Gwa@@I*4R+vxE?p9;IUW?gM)*hC<-xR#oL#XP)z~QRvqqY@?rn>dgyh4 zBuU0lD0I)kg9i`P)zvY%T<*}zFTZ>g<@A*-9*>JOO&d##?o#l{w1gkO`#JifDG;K; zZDH_>-K%j|W7Uj*1IXoa=;`T!EGy7CYw+oK79)HHvZBJlGI(%902^u?kR%D&Y!(v} z6F7J7+(m|ATAWU2qPx5MsaIcp_2Xp$PfSbB{4q{W$nex<{R zTYMZ=ax~1TF?^-jK$$owss>rr5U{hjtHFgTDh)-Jkw_$9u~?w0D$?mRy1TnSk|b&J zg%@6U9zX&BS<)qdnZV;XuD;wIN=>h$@RNHQ@WXAjIQJ!wvx6~QONbboO299s(4+Ta zdy@|qMvodh1ARJ%{=P5J`R86FlS%B^vj-N71y-vS27>|B)zy$B$@9r4pNs*p17H9M z0OTcs$H&Jzg2CWEQ4}Fb5>lyD(I<^YBPs=K54LVFsW$~pnf;+d| ziC{2@k&zMf^Ro zrq0gJXyJh-lL~=eNo<}ekL@*dcZEY=Lu^2i!I^b|PAP53z zng+u#Sg~RSa=9GB;V`qYbQC<#0{~X7T7|m0I;7L-vBt*6^Bl(=J%0T7pN<_n zmY)aw%rnn4Zr!@|sqNdh|7-2qwKPrBOfs2#on={X0anvAOiWB*XlMxi{rxy~>J&U4 z54yU#psFfpnnqn+9ky=Wist5K7z_q9Ha5cPbQaf9RTYvXL6RioaybZsfbQ;Y#9}c_ zPEH~m4nq`0=yW>xd_JsQyEfg>&`>i6_>DK-*sa&=*X-N3ug7dQdjQC)s>%Zc1MP`K zVtXVK!SL`fhKGkC2m(Y=oH>2;_v7xn?*;%o{q)n=ym@mGB%jZtr>6%1K+`nzdOc{G z1|bBQOs1I1^7%Zfs;c1idf|4vp=lZp960csD_5>0bvoS^LWp&2Z0ymZ4?p+Zb9LEl zc1L@A`zb|Hf{{qXIx#V^OOmAZ;c%Gi>+6Ht?S{wWK_C!7I-N!;m4esnMSFWYR<2x$ zmX;P6jYdSH(IS}DYQ=^P8!$LHh+Hm*R4N5g6d}tp7>0qtV1Uo(gWvCm&1M6~aYdJV z;DHB7C=~knETRE`st154o_OL`yWRe4nx=a$UcC5AmSvk5hB0WGc1=-~&K)~;gdczW z@k3|MoWaSHC&BYPnwpw$;J^X+{eCElf}x=y0D#$ShRI}_`6*yFo3Jd4b?ep@K@~*- zP1B%h8U}*_CR62#vbMH%UCE^k%w!4kwr$&v1Ofq1KA&HsD9U+F(>gRwOZff%jG`#Y zg$ox}A3Ju8^!N8uE|&`j4<1A?7(_0YgD8sl>Z`9%UtbTFWnr~i=hYFwObTQe2EhE% zvV3mAYHDf%PN&l@2tuN8PY)p5+1YvL@bECz-roMR)2B~&?b)-3qiI^pX0uc%6x!X} z+xw&-2-KD>Td;fgZm6n?WHO0-K96WLirU&*uq-<#sFq`eQnb>20Ls;?R|SfqR$%6v zt_X~yM~_|uUNbiVV>J7@Fx{HW1r9GSD3k}(57 zd+DW@9-t`dyF)`mt&vEiNtWfb*=&COjyvwC*|KHJKANUsv)K>`1W;XFjjF0D==J)F zhsweR78>hxI+92vqJ4dRT^$`Ae>r^k@Ohr+uK`dOd@JQQR*deO-)9(+x<~I9=}(Tq%of7pG_nZVE{=0N;aDn4F-b|n17U?cVyQzC6mca z#pCgCBocWqo6R0?YiqlDBXE_hn1@?iTlML5`tO1u+$%}az|_>#v#C_-0)T8`PCA{Q za5x^7UN>3_2LGVMQHUw - - - + + + - 1 - - - + + + - 2 - - - + + + - 3 - - - + + + - 4 - - - + + + - 5 - - - + + + - 6 - - - + + + - 7 - - - - + + + + - 8 - - - + + + - 9 - - - + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui/data/toolsQuick.xml b/ui/data/toolsQuick.xml new file mode 100644 --- /dev/null +++ b/ui/data/toolsQuick.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui/embeddedfilesdialog.cpp b/ui/embeddedfilesdialog.cpp --- a/ui/embeddedfilesdialog.cpp +++ b/ui/embeddedfilesdialog.cpp @@ -92,7 +92,7 @@ twi->setText(2, ef->size() <= 0 ? i18nc("Not available size", "N/A") : KFormat().formatByteSize(ef->size())); twi->setText(3, dateToString( ef->creationDate() ) ); twi->setText(4, dateToString( ef->modificationDate() ) ); - twi->setData( 0, EmbeddedFileRole, qVariantFromValue( ef ) ); + twi->setData( 0, EmbeddedFileRole, QVariant::fromValue( ef ) ); m_tw->addTopLevelItem(twi); } // Having filled the columns, it is nice to resize them to be able to read the contents diff --git a/ui/findbar.cpp b/ui/findbar.cpp --- a/ui/findbar.cpp +++ b/ui/findbar.cpp @@ -29,7 +29,7 @@ , m_active( false ) { QHBoxLayout * lay = new QHBoxLayout( this ); - lay->setMargin( 2 ); + lay->setContentsMargins( 2, 2, 2, 2 ); QToolButton * closeBtn = new QToolButton( this ); closeBtn->setIcon( QIcon::fromTheme( QStringLiteral("dialog-close") ) ); diff --git a/ui/formwidgets.h b/ui/formwidgets.h --- a/ui/formwidgets.h +++ b/ui/formwidgets.h @@ -14,6 +14,7 @@ #define _OKULAR_FORMWIDGETS_H_ #include "core/area.h" +#include "core/form.h" #include #include @@ -60,6 +61,8 @@ void signalAction( Okular::Action *action ); + void processScriptAction( Okular::Action *a, Okular::FormField * field, Okular::Annotation::AdditionalActionType type ); + void registerRadioButton( FormWidgetIface *fwButton, Okular::FormFieldButton *formButton ); void dropRadioButtons(); bool canUndo(); @@ -116,6 +119,14 @@ void action( Okular::Action *action ); + void focusAction( const Okular::Action *action, Okular::FormFieldText *ff ); + + void formatAction( const Okular::Action *action, Okular::FormFieldText *ff ); + + void keystrokeAction( const Okular::Action *action, Okular::FormFieldText *ff, bool &ok ); + + void validateAction( const Okular::Action *action, Okular::FormFieldText *ff, bool &ok ); + void refreshFormWidget( Okular::FormField * form ); private Q_SLOTS: @@ -248,6 +259,7 @@ private: int m_prevCursorPos; int m_prevAnchorPos; + bool m_editing; DECLARE_ADDITIONAL_ACTIONS }; @@ -279,6 +291,7 @@ private: int m_prevCursorPos; int m_prevAnchorPos; + bool m_editing; DECLARE_ADDITIONAL_ACTIONS }; diff --git a/ui/formwidgets.cpp b/ui/formwidgets.cpp --- a/ui/formwidgets.cpp +++ b/ui/formwidgets.cpp @@ -28,6 +28,7 @@ #include // local includes +#include "core/action.h" #include "core/form.h" #include "core/document.h" #include "debug_ui.h" @@ -89,6 +90,30 @@ emit action( a ); } +void FormWidgetsController::processScriptAction( Okular::Action *a, Okular::FormField * field, Okular::Annotation::AdditionalActionType type ) +{ + // If it's not a Action Script or if the field is not a FormText, handle it normally + if( a->actionType() != Okular::Action::Script || field->type() != Okular::FormField::FormText ) + { + emit action( a ); + return; + } + switch( type ) + { + // These cases are to be handled by the FormField text, so we let it happen. + case Okular::Annotation::FocusIn: + case Okular::Annotation::FocusOut: + return; + case Okular::Annotation::PageOpening: + case Okular::Annotation::PageClosing: + case Okular::Annotation::CursorEntering: + case Okular::Annotation::CursorLeaving: + case Okular::Annotation::MousePressed: + case Okular::Annotation::MouseReleased: + emit action( a ); + } +} + void FormWidgetsController::registerRadioButton( FormWidgetIface *fwButton, Okular::FormFieldButton *formButton ) { if ( !fwButton ) @@ -106,12 +131,13 @@ m_buttons.insert( id, button ); for ( ; it != itEnd; ++it ) { - const QList< int >::const_iterator idsIt = qFind( (*it).ids, id ); - if ( idsIt != (*it).ids.constEnd() ) + const RadioData &rd = *it; + const QList< int >::const_iterator idsIt = std::find( rd.ids.begin(), rd.ids.end(), id ); + if ( idsIt != rd.ids.constEnd() ) { - qCDebug(OkularUiDebug) << "Adding id" << id << "To group including" << (*it).ids; - (*it).group->addButton( button ); - (*it).group->setId( button, id ); + qCDebug(OkularUiDebug) << "Adding id" << id << "To group including" << rd.ids; + rd.group->addButton( button ); + rd.group->setId( button, id ); return; } } @@ -375,7 +401,7 @@ { return; } - setVisibility( form->isVisible() /*&& !form->isReadOnly()*/ ); + setVisibility( form->isVisible() && m_controller->shouldFormWidgetBeShown( form ) ); m_widget->setEnabled( !form->isReadOnly() ); } @@ -385,6 +411,12 @@ : QPushButton( parent ), FormWidgetIface( this, button ) { setText( button->caption() ); + + if( button->caption().isEmpty() ) + { + setFlat( true ); + } + setVisible( button->isVisible() ); setCursor( Qt::ArrowCursor ); } @@ -464,6 +496,7 @@ m_prevCursorPos = cursorPosition(); m_prevAnchorPos = cursorPosition(); + m_editing = false; connect( this, &QLineEdit::textEdited, this, &FormLineEdit::slotChanged ); connect( this, &QLineEdit::cursorPositionChanged, this, &FormLineEdit::slotChanged ); @@ -494,6 +527,33 @@ return true; } } + else if ( e->type() == QEvent::FocusIn ) + { + const auto fft = static_cast< Okular::FormFieldText * > ( m_ff ); + setText( fft->text() ); + m_editing = true; + if( const Okular::Action *action = m_ff->additionalAction( Okular::Annotation::FocusIn ) ) + emit m_controller->focusAction( action, fft ); + setFocus(); + } + else if ( e->type() == QEvent::FocusOut ) + { + // Don't worry about focus events from other sources than the user FocusEvent to edit the field + QFocusEvent *focusEvent = static_cast< QFocusEvent* >( e ); + if( focusEvent->reason() == Qt::OtherFocusReason ) + return true; + m_editing = false; + + if( const Okular::Action *action = m_ff->additionalAction( Okular::Annotation::FocusOut ) ) + { + bool ok = false; + emit m_controller->validateAction( action, static_cast< Okular::FormFieldText * > ( m_ff ), ok ); + } + if ( const Okular::Action *action = m_ff->additionalAction( Okular::FormField::FormatField ) ) + { + emit m_controller->formatAction( action, static_cast< Okular::FormFieldText * > ( m_ff ) ); + } + } return QLineEdit::event( e ); } @@ -530,6 +590,21 @@ Okular::FormFieldText *form = static_cast(m_ff); QString contents = text(); int cursorPos = cursorPosition(); + + if( form->additionalAction( Okular::FormField::FieldModified ) && m_editing && !form->isReadOnly() ) + { + bool ok = false; + QString oldInputText = form->text(); + form->setText( text() ); + emit m_controller->keystrokeAction( form->additionalAction( Okular::FormField::FieldModified ), form, ok ); + form->setText( oldInputText ); + if(!ok) + { + setText( oldInputText ); + return; + } + } + if ( contents != form->text() ) { m_controller->formTextChangedByWidget( pageItem()->pageNumber(), @@ -599,6 +674,7 @@ this, &TextAreaEdit::slotUpdateUndoAndRedoInContextMenu ); m_prevCursorPos = textCursor().position(); m_prevAnchorPos = textCursor().anchor(); + m_editing = false; setVisible( text->isVisible() ); } @@ -626,6 +702,20 @@ return true; } } + else if ( e->type() == QEvent::FocusIn ) + { + const auto fft = static_cast< Okular::FormFieldText * > ( m_ff ); + setText( fft->text() ); + m_editing = true; + } + else if ( e->type() == QEvent::FocusOut ) + { + m_editing = false; + if ( const Okular::Action *action = m_ff->additionalAction( Okular::FormField::FormatField ) ) + { + emit m_controller->formatAction( action, static_cast< Okular::FormFieldText * > ( m_ff ) ); + } + } return KTextEdit::event( e ); } @@ -687,6 +777,21 @@ Okular::FormFieldText *form = static_cast(m_ff); QString contents = toPlainText(); int cursorPos = textCursor().position(); + + if( form->additionalAction( Okular::FormField::FieldModified ) && m_editing && !form->isReadOnly() ) + { + bool ok = false; + QString oldInputText = form->text(); + form->setText( toPlainText() ); + emit m_controller->keystrokeAction( form->additionalAction( Okular::FormField::FieldModified ), form, ok ); + form->setText( oldInputText ); + if(!ok) + { + setText( oldInputText ); + return; + } + } + if (contents != form->text()) { m_controller->formTextChangedByWidget( pageItem()->pageNumber(), @@ -1229,16 +1334,16 @@ Okular::Action *act = m_ff->additionalAction( Okular::Annotation::FocusIn ); \ if ( act ) \ { \ - m_controller->signalAction( act ); \ + m_controller->processScriptAction( act, m_ff, Okular::Annotation::FocusIn ); \ } \ BaseClass::focusInEvent( event ); \ } \ void FormClass::focusOutEvent( QFocusEvent *event ) \ { \ Okular::Action *act = m_ff->additionalAction( Okular::Annotation::FocusOut ); \ if ( act ) \ { \ - m_controller->signalAction( act ); \ + m_controller->processScriptAction( act, m_ff, Okular::Annotation::FocusOut ); \ } \ BaseClass::focusOutEvent( event ); \ } \ diff --git a/ui/guiutils.h b/ui/guiutils.h --- a/ui/guiutils.h +++ b/ui/guiutils.h @@ -39,7 +39,14 @@ QString prettyToolTip( const Okular::Annotation * annotation ); - QPixmap loadStamp( const QString& name, const QSize& size, int iconSize = 0 ); + /** + * Returns a pixmap for a stamp symbol + * + * @p name Name of a Okular stamp symbol, icon or path to an image + * @p size Size of the pixmap (ignore aspect ratio). Takes precedence over @p iconSize + * @p iconSize Maximum size of the pixmap (keep aspect ratio) + */ + QPixmap loadStamp( const QString& nameOrPath, int size, bool keepAspectRatio = true ); void addIconLoader( KIconLoader * loader ); void removeIconLoader( KIconLoader * loader ); diff --git a/ui/guiutils.cpp b/ui/guiutils.cpp --- a/ui/guiutils.cpp +++ b/ui/guiutils.cpp @@ -169,29 +169,45 @@ return tooltip; } -QPixmap loadStamp( const QString& _name, const QSize& size, int iconSize ) +QPixmap loadStamp( const QString& nameOrPath, int size, bool keepAspectRatio ) { - const QString name = _name.toLower(); + const QString name = nameOrPath.toLower(); + + // _name is the name of an Okular stamp symbols ( multiple symbols in a single *.svg file) QSvgRenderer * r = nullptr; if ( ( r = s_data->svgStamps() ) && r->elementExists( name ) ) { - const QRectF stampElemRect = r->boundsOnElement( name ); - const QRectF stampRect( size.isValid() ? QRectF( QPointF( 0, 0 ), size ) : stampElemRect ); - QPixmap pixmap( stampRect.size().toSize() ); + const QSize stampSize = r->boundsOnElement( name ).size().toSize(); + const QSize pixmapSize = stampSize.scaled( size, size, + keepAspectRatio ? Qt::KeepAspectRatioByExpanding + : Qt::IgnoreAspectRatio ); + QPixmap pixmap( pixmapSize ); pixmap.fill( Qt::transparent ); QPainter p( &pixmap ); r->render( &p, name ); p.end(); return pixmap; } + + // _name is a path (do this before loading as icon name to avoid some rare weirdness ) QPixmap pixmap; + pixmap.load( nameOrPath ); + if ( !pixmap.isNull() ) { + pixmap = pixmap.scaled( size, size, + keepAspectRatio ? Qt::KeepAspectRatioByExpanding + : Qt::IgnoreAspectRatio, + Qt::SmoothTransformation ); + return pixmap; + } + + // _name is an icon name const KIconLoader * il = iconLoader(); QString path; - const int minSize = iconSize > 0 ? iconSize : qMin( size.width(), size.height() ); - pixmap = il->loadIcon( name, KIconLoader::User, minSize, KIconLoader::DefaultState, QStringList(), &path, true ); + pixmap = il->loadIcon( name, KIconLoader::User, size, KIconLoader::DefaultState, QStringList(), &path, true ); if ( path.isEmpty() ) - pixmap = il->loadIcon( name, KIconLoader::NoGroup, minSize ); - return pixmap; + pixmap = il->loadIcon( name, KIconLoader::NoGroup, size ); + + return pixmap; // can be a null pixmap } void addIconLoader( KIconLoader * loader ) diff --git a/ui/ktreeviewsearchline.cpp b/ui/ktreeviewsearchline.cpp --- a/ui/ktreeviewsearchline.cpp +++ b/ui/ktreeviewsearchline.cpp @@ -383,7 +383,7 @@ QHBoxLayout* layout = new QHBoxLayout( this ); layout->setSpacing( 5 ); - layout->setMargin( 0 ); + layout->setContentsMargins( 0, 0, 0, 0 ); layout->addWidget( label ); layout->addWidget( d->searchLine ); } diff --git a/ui/layers.cpp b/ui/layers.cpp --- a/ui/layers.cpp +++ b/ui/layers.cpp @@ -23,7 +23,7 @@ Layers::Layers(QWidget *parent, Okular::Document *document) : QWidget(parent), m_document(document) { QVBoxLayout * const mainlay = new QVBoxLayout( this ); - mainlay->setMargin( 0 ); + mainlay->setContentsMargins( 0, 0, 0, 0 ); mainlay->setSpacing( 6 ); m_document->addObserver( this ); diff --git a/ui/minibar.cpp b/ui/minibar.cpp --- a/ui/minibar.cpp +++ b/ui/minibar.cpp @@ -161,7 +161,7 @@ QHBoxLayout * horLayout = new QHBoxLayout( this ); - horLayout->setMargin( 0 ); + horLayout->setContentsMargins( 0, 0, 0, 0 ); horLayout->setSpacing( 3 ); QSize buttonSize( KIconLoader::SizeSmallMedium, KIconLoader::SizeSmallMedium ); @@ -377,7 +377,7 @@ void ProgressWidget::wheelEvent( QWheelEvent * e ) { - if ( e->delta() > 0 ) + if ( e->angleDelta().y() > 0 ) emit nextPage(); else emit prevPage(); @@ -575,7 +575,7 @@ void PagesEdit::wheelEvent( QWheelEvent * e ) { - if ( e->delta() > 0 ) + if ( e->angleDelta().y() > 0 ) m_miniBar->slotEmitNextPage(); else m_miniBar->slotEmitPrevPage(); diff --git a/ui/pagepainter.cpp b/ui/pagepainter.cpp --- a/ui/pagepainter.cpp +++ b/ui/pagepainter.cpp @@ -680,7 +680,7 @@ Okular::StampAnnotation * stamp = (Okular::StampAnnotation *)a; // get pixmap and alpha blend it if needed - QPixmap pixmap = GuiUtils::loadStamp( stamp->stampIconName(), annotBoundary.size() ); + 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()); diff --git a/ui/pageview.h b/ui/pageview.h --- a/ui/pageview.h +++ b/ui/pageview.h @@ -30,6 +30,7 @@ #include "core/observer.h" #include "core/view.h" +class QMenu; class KActionCollection; namespace Okular { @@ -135,6 +136,7 @@ void mouseForwardButtonClick(); void escPressed(); void fitWindowToPage( const QSize& pageViewPortSize, const QSize& pageSize ); + void triggerSearch( const QString& text ); protected: bool event( QEvent * event ) override; @@ -179,6 +181,8 @@ void updateZoom( ZoomMode newZm ); // update the text on the label using global zoom value or current page's one void updateZoomText(); + // update view mode (single, facing...) + void updateViewMode ( const int nr ); void textSelectionClear(); // updates cursor void updateCursor( const QPoint &p ); @@ -196,6 +200,7 @@ void resizeContentArea( const QSize & newSize ); void updatePageStep(); + void addSearchWithinDocumentAction(QMenu * menu, const QString & searchText ); void addWebShortcutsMenu( QMenu * menu, const QString & text ); QMenu* createProcessLinkMenu( PageViewItem *item, const QPoint & eventPos ); // used when selecting stuff, makes the view scroll as necessary to keep the mouse inside the view @@ -246,13 +251,12 @@ void slotAutoFitToggled( bool ); void slotViewMode( QAction *action ); void slotContinuousToggled( bool ); - void slotSetMouseNormal(); + void slotMouseNormalToggled( bool ); void slotSetMouseZoom(); void slotSetMouseMagnifier(); void slotSetMouseSelect(); void slotSetMouseTextSelect(); void slotSetMouseTableSelect(); - void slotToggleAnnotator( bool ); void slotAutoScrollUp(); void slotAutoScrollDown(); void slotScrollUp( bool singleStep = false ); @@ -270,6 +274,7 @@ void slotSpeakDocument(); void slotSpeakCurrentPage(); void slotStopSpeaks(); + void slotPauseResumeSpeech(); #endif void slotAction( Okular::Action *action ); void externalKeyPressEvent( QKeyEvent *e ); diff --git a/ui/pageview.cpp b/ui/pageview.cpp --- a/ui/pageview.cpp +++ b/ui/pageview.cpp @@ -41,6 +41,7 @@ #include #include #include +#include #include #include @@ -73,7 +74,7 @@ #include "pageviewannotator.h" #include "pageviewmouseannotation.h" #include "priorities.h" -#include "toolaction.h" +#include "toggleactionmenu.h" #include "okmenutitle.h" #ifdef HAVE_SPEECH #include "tts.h" @@ -102,6 +103,12 @@ 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; @@ -164,7 +171,7 @@ // viewport move bool viewportMoveActive; - QTime viewportMoveTime; + QElapsedTimer viewportMoveTime; QPoint viewportMoveDest; int lastSourceLocationViewportPageNumber; double lastSourceLocationViewportNormalizedX; @@ -219,7 +226,6 @@ QAction * aMouseTableSelect; QAction * aMouseMagnifier; KToggleAction * aTrimToSelection; - KToggleAction * aToggleAnnotator; KSelectAction * aZoom; QAction * aZoomIn; QAction * aZoomOut; @@ -234,8 +240,10 @@ QAction * aSpeakDoc; QAction * aSpeakPage; QAction * aSpeakStop; + QAction * aSpeakPauseResume; KActionCollection * actionCollection; QActionGroup * mouseModeActionGroup; + ToggleActionMenu * aMouseModeMenu; QAction * aFitWindowToPage; int setting_viewCols; @@ -264,6 +272,22 @@ 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; @@ -277,9 +301,15 @@ m_tts = new OkularTTS( q ); if ( aSpeakStop ) { - QObject::connect( m_tts, &OkularTTS::isSpeaking, + QObject::connect( m_tts, &OkularTTS::canPauseOrResume, aSpeakStop, &QAction::setEnabled ); } + + if ( aSpeakPauseResume ) + { + QObject::connect( m_tts, &OkularTTS::canPauseOrResume, + aSpeakPauseResume, &QAction::setEnabled ); + } } return m_tts; @@ -345,7 +375,6 @@ d->aMouseNormal = nullptr; d->aMouseSelect = nullptr; d->aMouseTextSelect = nullptr; - d->aToggleAnnotator = nullptr; d->aZoomFitWidth = nullptr; d->aZoomFitPage = nullptr; d->aZoomAutoFit = nullptr; @@ -356,6 +385,7 @@ d->aSpeakDoc = nullptr; d->aSpeakPage = nullptr; d->aSpeakStop = nullptr; + d->aSpeakPauseResume = nullptr; d->actionCollection = nullptr; d->aPageSizes=nullptr; d->setting_viewCols = Okular::Settings::viewColumns(); @@ -526,14 +556,14 @@ d->aTrimMargins = new KToggleAction(QIcon::fromTheme( QStringLiteral("trim-margins") ), i18n( "&Trim Margins" ), d->aTrimMode->menu() ); d->aTrimMode->addAction( d->aTrimMargins ); ac->addAction( QStringLiteral("view_trim_margins"), d->aTrimMargins ); - d->aTrimMargins->setData( qVariantFromValue( (int)Okular::Settings::EnumTrimMode::Margins ) ); + 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( qVariantFromValue( (int)Okular::Settings::EnumTrimMode::Selection ) ); + 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); @@ -561,7 +591,7 @@ do { \ QAction *vm = new QAction( text, this ); \ vm->setCheckable( true ); \ - vm->setData( qVariantFromValue( id ) ); \ + vm->setData( QVariant::fromValue( id ) ); \ d->aViewMode->addAction( vm ); \ ac->addAction( QStringLiteral(name), vm ); \ vmGroup->addAction( vm ); \ @@ -593,7 +623,7 @@ 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 ); + connect( d->aMouseNormal, &QAction::toggled, this, &PageView::slotMouseNormalToggled ); d->aMouseNormal->setCheckable( true ); ac->setDefaultShortcut(d->aMouseNormal, QKeySequence(Qt::CTRL + Qt::Key_1)); d->aMouseNormal->setActionGroup( d->mouseModeActionGroup ); @@ -654,17 +684,16 @@ d->aMouseMagnifier->setActionGroup( d->mouseModeActionGroup ); d->aMouseMagnifier->setChecked( Okular::Settings::mouseMode() == Okular::Settings::EnumMouseMode::Magnifier ); - d->aToggleAnnotator = new KToggleAction(QIcon::fromTheme( QStringLiteral("draw-freehand") ), i18n("&Review"), this); - ac->addAction(QStringLiteral("mouse_toggle_annotate"), d->aToggleAnnotator ); - d->aToggleAnnotator->setCheckable( true ); - connect( d->aToggleAnnotator, &QAction::toggled, this, &PageView::slotToggleAnnotator ); - ac->setDefaultShortcut(d->aToggleAnnotator, Qt::Key_F6); - - ToolAction *ta = new ToolAction( this ); - ac->addAction( QStringLiteral("mouse_selecttools"), ta ); - ta->addAction( d->aMouseTextSelect ); - ta->addAction( d->aMouseSelect ); - ta->addAction( d->aMouseTableSelect ); + // Mouse-Mode action menu + d->aMouseModeMenu = new ToggleActionMenu( QIcon(),QString(), this, + QToolButton::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 ); // speak actions #ifdef HAVE_SPEECH @@ -682,10 +711,16 @@ 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 = 0; d->aSpeakPage = 0; d->aSpeakStop = 0; + d->aSpeakPauseResume = 0; #endif // Other actions @@ -726,6 +761,13 @@ connect(d->document, &Okular::Document::canRedoChanged, kredo, &QAction::setEnabled); kundo->setEnabled(false); kredo->setEnabled(false); + + if( !d->annotator ) + { + d->annotator = new PageViewAnnotator( this, d->document ); + connect( d->annotator, &PageViewAnnotator::toolSelected, d->aMouseNormal , &QAction::trigger ); + } + d->annotator->setupActions( ac ); } bool PageView::canFitPageWidth() const @@ -841,12 +883,7 @@ 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 @@ -979,13 +1016,8 @@ 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 ) ) { @@ -1198,6 +1230,8 @@ if ( d->mouseModeActionGroup ) d->mouseModeActionGroup->setEnabled( haspages ); + if ( d->aMouseModeMenu ) + d->aMouseModeMenu->setEnabled( haspages ); if ( d->aRotateClockwise ) d->aRotateClockwise->setEnabled( haspages ); @@ -1216,14 +1250,6 @@ 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 ) { @@ -1366,7 +1392,7 @@ QSet< AnnotWindow * >::Iterator it = d->m_annowindows.begin(); for ( ; it != d->m_annowindows.end(); ) { - QLinkedList< Okular::Annotation * >::ConstIterator annIt = qFind( annots, (*it)->annotation() ); + QLinkedList< Okular::Annotation * >::ConstIterator annIt = std::find( annots.begin(), annots.end(), (*it)->annotation() ); if ( annIt != annItEnd ) { (*it)->reloadInfo(); @@ -1467,6 +1493,18 @@ Q_FOREACH ( VideoWidget *videoWidget, item->videoWidgets() ) videoWidget->pageLeft(); } + + // On close, run the widget scripts, needed for running animated PDF + const Okular::Page *page = d->document->page( previous ); + foreach( Okular::Annotation *annotation, page->annotations() ) + { + if ( annotation->subType() == Okular::Annotation::AWidget ) + { + Okular::WidgetAnnotation *widgetAnnotation = static_cast( annotation ); + d->document->processAction( widgetAnnotation->additionalAction( Okular::Annotation::PageClosing ) ); + } + } + } if ( current != -1 ) @@ -1481,6 +1519,17 @@ // 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 ); + foreach( Okular::Annotation *annotation, page->annotations() ) + { + if ( annotation->subType() == Okular::Annotation::AWidget ) + { + Okular::WidgetAnnotation *widgetAnnotation = static_cast( annotation ); + d->document->processAction( widgetAnnotation->additionalAction( Okular::Annotation::PageOpening ) ); + } + } } } @@ -1493,6 +1542,9 @@ { case Zoom: case ZoomModality: + case Continuous: + case ViewModeModality: + case TrimMargins: return true; } return false; @@ -1504,9 +1556,12 @@ { case Zoom: case ZoomModality: + case Continuous: + case ViewModeModality: + case TrimMargins: return CapabilityRead | CapabilityWrite | CapabilitySerializable; } - return nullptr; + return NoFlag; } QVariant PageView::capability( ViewCapability capability ) const @@ -1517,6 +1572,20 @@ return d->zoomFactor; case ZoomModality: return d->zoomMode; + case Continuous: + return d->aViewContinuous->isChecked(); + case ViewModeModality: + { + for (int i=0; i < d->aViewMode->menu()->actions().size(); ++i) + { + const QAction* action = d->aViewMode->menu()->actions().at(i); + if ( action->isChecked() ) + return action->data(); + } + return QVariant(); + } + case TrimMargins: + return d->aTrimMargins->isChecked(); } return QVariant(); } @@ -1547,6 +1616,31 @@ } 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; + } } } @@ -1591,7 +1685,7 @@ d->blockPixmapsRequest = true; updateZoom( ZoomRefreshCurrent ); d->blockPixmapsRequest = false; - viewport()->repaint(); + viewport()->update(); } // Count the number of 90-degree rotations we did since the start of the pinch gesture. @@ -1658,28 +1752,27 @@ d->mouseSelectionColor : Qt::red; // subdivide region into rects - const QVector &allRects = pe->region().rects(); - uint numRects = allRects.count(); - + QRegion rgn = pe->region(); // preprocess rects area to see if it worths or not using subdivision uint summedArea = 0; - for ( uint i = 0; i < numRects; i++ ) + for ( const QRect & r : rgn ) { - const QRect & r = allRects[i]; summedArea += r.width() * r.height(); } // very elementary check: SUMj(Region[j].area) is less than boundingRect.area - bool useSubdivision = summedArea < (0.6 * contentsRect.width() * contentsRect.height()); + const bool useSubdivision = summedArea < (0.6 * contentsRect.width() * contentsRect.height()); if ( !useSubdivision ) - numRects = 1; + { + rgn = contentsRect; + } // iterate over the rects (only one loop if not using subdivision) - for ( uint i = 0; i < numRects; i++ ) + for ( const QRect & r : rgn ) { if ( useSubdivision ) { // set 'contentsRect' to a part of the sub-divided region - contentsRect = allRects[i].translated( areaPos ).intersected( viewportRect ); + contentsRect = r.translated( areaPos ).intersected( viewportRect ); if ( !contentsRect.isValid() ) continue; } @@ -1911,25 +2004,24 @@ switch ( e->key() ) { case Qt::Key_J: - case Qt::Key_K: case Qt::Key_Down: + slotScrollDown( true /* singleStep */ ); + break; + case Qt::Key_PageDown: + slotScrollDown(); + break; + + case Qt::Key_K: case Qt::Key_Up: + slotScrollUp( true /* singleStep */ ); + break; + case Qt::Key_PageUp: case Qt::Key_Backspace: - if ( e->key() == Qt::Key_Down - || e->key() == Qt::Key_PageDown - || e->key() == Qt::Key_J ) - { - bool singleStep = e->key() == Qt::Key_Down || e->key() == Qt::Key_J; - slotScrollDown( singleStep ); - } - else - { - bool singleStep = e->key() == Qt::Key_Up || e->key() == Qt::Key_K; - slotScrollUp( singleStep ); - } - break; + slotScrollUp(); + break; + case Qt::Key_Left: case Qt::Key_H: if ( horizontalScrollBar()->maximum() == 0 ) @@ -2106,7 +2198,7 @@ d->blockPixmapsRequest = true; updateZoom( ZoomRefreshCurrent ); d->blockPixmapsRequest = false; - viewport()->repaint(); + viewport()->update(); } return; } @@ -2794,6 +2886,7 @@ #endif if ( copyAllowed ) { + addSearchWithinDocumentAction( &menu, selectedText ); addWebShortcutsMenu( &menu, selectedText ); } } @@ -3077,6 +3170,7 @@ } else { + addSearchWithinDocumentAction(menu, d->selectedText()); addWebShortcutsMenu( menu, d->selectedText() ); } @@ -3086,7 +3180,7 @@ url = UrlUtils::getUrl( d->selectedText() ); if ( !url.isEmpty() ) { - const QString squeezedText = KStringHandler::rsqueeze( url, 30 ); + const QString squeezedText = KStringHandler::rsqueeze( url, linkTextPreviewLength ); httpLink = menu->addAction( i18n( "Go to '%1'", squeezedText ) ); httpLink->setObjectName(QStringLiteral("GoToAction")); } @@ -3295,7 +3389,7 @@ return; } - int delta = e->delta(), + int delta = e->angleDelta().y(), vScroll = verticalScrollBar()->value(); e->accept(); if ( (e->modifiers() & Qt::ControlModifier) == Qt::ControlModifier ) { @@ -3408,8 +3502,9 @@ // thus leaving artifacts around QRegion rgn( r ); rgn -= rgn & r.translated( dx, dy ); - foreach ( const QRect &rect, rgn.rects() ) - viewport()->repaint( rect ); + + for ( const QRect &rect : rgn ) + viewport()->update( rect ); } //END widget events @@ -3986,22 +4081,22 @@ const float zoomFactorFitWidth = zoomFactorFitMode(ZoomFitWidth); const float zoomFactorFitPage = zoomFactorFitMode(ZoomFitPage); QVector zoomValue(15); - qCopy(kZoomValues, kZoomValues + 13, zoomValue.begin()); + 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 = qLowerBound(zoomValue.begin(), zoomValue.end(), newFactor) - 1; + i = std::lower_bound(zoomValue.begin(), zoomValue.end(), newFactor) - 1; } else { if (newFactor >= zoomValue.last()) return; - i = qUpperBound(zoomValue.begin(), zoomValue.end(), newFactor); + i = std::upper_bound(zoomValue.begin(), zoomValue.end(), newFactor); } const float tmpFactor = *i; if ( tmpFactor == zoomFactorFitWidth ) @@ -4129,6 +4224,16 @@ d->aZoom->selectableActionGroup()->setEnabled( d->items.size() > 0 ); } +void PageView::updateViewMode(const int nr) +{ + for ( QAction* action : d->aViewMode->menu()->actions() ) { + QVariant mode_id = action->data(); + if (mode_id.toInt() == nr) { + action->trigger(); + } + } +} + void PageView::updateCursor() { const QPoint p = contentAreaPosition() + viewport()->mapFromGlobal( QCursor::pos() ); @@ -4391,7 +4496,7 @@ QMenu *webShortcutsMenu = new QMenu( menu ); webShortcutsMenu->setIcon( QIcon::fromTheme( QStringLiteral("preferences-web-browser-shortcuts") ) ); - const QString squeezedText = KStringHandler::rsqueeze( searchText, 21 ); + const QString squeezedText = KStringHandler::rsqueeze( searchText, searchTextPreviewLength ); webShortcutsMenu->setTitle( i18n( "Search for '%1' with", squeezedText ) ); QAction *action = nullptr; @@ -4467,6 +4572,15 @@ 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 @@ -5041,32 +5155,30 @@ } } -void PageView::slotSetMouseNormal() +void PageView::slotMouseNormalToggled( bool checked ) { - 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(); + if ( checked ) + { + d->mouseMode = Okular::Settings::EnumMouseMode::Browse; + Okular::Settings::setMouseMode( d->mouseMode ); + // hide the messageWindow + d->messageWindow->hide(); + // force an update of the cursor + updateCursor(); + Okular::Settings::self()->save(); + } + else + { + d->annotator->detachAnnotation(); + } } 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(); @@ -5089,12 +5201,6 @@ 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(); @@ -5106,12 +5212,6 @@ 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(); @@ -5125,75 +5225,11 @@ 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 ) @@ -5333,12 +5369,6 @@ 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 { @@ -5435,6 +5465,15 @@ d->m_tts->stopAllSpeechs(); } + +void PageView::slotPauseResumeSpeech() +{ + if ( !d->m_tts ) + return; + + d->m_tts->pauseResumeSpeech(); +} + #endif void PageView::slotAction( Okular::Action *action ) diff --git a/ui/pageviewannotator.h b/ui/pageviewannotator.h --- a/ui/pageviewannotator.h +++ b/ui/pageviewannotator.h @@ -14,20 +14,24 @@ #include #include +#include + #include "pageviewutils.h" #include "annotationtools.h" class QKeyEvent; class QMouseEvent; class QPainter; +class AnnotationActionHandler; namespace Okular { class Document; } // engines are defined and implemented in the cpp class AnnotatorEngine; +class AnnotationTools; class PageView; /** @@ -40,31 +44,26 @@ * to this class that performs a rough visual representation of what the * annotation will become when finished. * - * m_toolsDefinition is a DOM object that contains Annotations/Engine association - * for the items placed in the toolbar. The XML is parsed (1) when populating - * the toolbar and (2)after selecting a toolbar item, in which case an Ann is + * m_toolsDefinition is a AnnotationTools object that wraps a DOM object that + * contains Annotations/Engine association for the items placed in the toolbar. + * The XML is parsed after selecting a toolbar item, in which case an Ann is * initialized with the values in the XML and an engine is created to handle * that annotation. m_toolsDefinition is created in reparseConfig according to - * user configuration. + * user configuration. m_toolsDefinition is updated (and saved to disk) (1) each + * time a property of an annotation (color, font, etc) is changed by the user, + * and (2) each time a "quick annotation" is selected, in which case the properties + * of the selected quick annotation are written over those of the corresponding + * builtin tool */ class PageViewAnnotator : public QObject { Q_OBJECT public: + static const int STAMP_TOOL_ID; + PageViewAnnotator( PageView * parent, Okular::Document * storage ); ~PageViewAnnotator(); - // called to show/hide the editing toolbar - void setEnabled( bool enabled ); - - // called to toggle the usage of text annotating tools - void setTextToolsEnabled( bool enabled ); - - void setToolsEnabled( bool enabled ); - - void setHidingForced( bool forced ); - bool hidingWasForced() const; - // methods used when creating the annotation // @return Is a tool currently selected? bool active() const; @@ -88,25 +87,62 @@ static QString defaultToolName( const QDomElement &toolElement ); static QPixmap makeToolPixmap( const QDomElement &toolElement ); - private Q_SLOTS: - void slotToolSelected( int toolID ); - void slotSaveToolbarOrientation( int side ); - void slotToolDoubleClicked( int toolID ); + // methods related to the annotation actions + void setupActions( KActionCollection *ac ); + // @return Is continuous mode active (pin annotation)? + bool continuousMode(); + // enable/disable the annotation actions + void setToolsEnabled( bool enabled ); + // enable/disable the text-selection annotation actions + void setTextToolsEnabled( bool enabled ); - private: + // selects the active tool + void selectTool( int toolID ); + // selects a stamp tool and sets the stamp symbol + void selectStampTool( const QString &stampSymbol ); + // makes a quick annotation the active tool + int setQuickTool ( int toolID ); + // deselects the tool and uncheck all the annotation actions void detachAnnotation(); + // returns the builtin annotation tool with the given Id + QDomElement builtinTool( int toolID ); + // returns the quick annotation tool with the given Id + QDomElement quickTool( int toolID ); + + // methods that write the properties + void setAnnotationWidth( double width ); + void setAnnotationColor( const QColor &color ); + void setAnnotationInnerColor( const QColor &color ); + void setAnnotationOpacity( double opacity ); + void setAnnotationFont( const QFont &font ); + + public Q_SLOTS: + void setContinuousMode( bool enabled ); + void addToQuickAnnotations(); + void slotAdvancedSettings(); + + Q_SIGNALS: + void toolSelected(); + + private: + // save the annotation tools to Okular settings + void saveAnnotationTools(); + // returns the engine QDomElement of the the currently active tool + QDomElement currentEngineElement(); + // returns the annotation QDomElement of the the currently active tool + QDomElement currentAnnotationElement(); + // global class pointers Okular::Document * m_document; PageView * m_pageView; - PageViewToolBar * m_toolBar; + AnnotationActionHandler * m_actionHandler; AnnotatorEngine * m_engine; - QDomElement m_toolsDefinition; - QLinkedList m_items; + AnnotationTools * m_toolsDefinition; + AnnotationTools * m_quickToolsDefinition; bool m_textToolsEnabled; bool m_toolsEnabled; bool m_continuousMode; - bool m_hidingWasForced; // creation related variables int m_lastToolID; diff --git a/ui/pageviewannotator.cpp b/ui/pageviewannotator.cpp --- a/ui/pageviewannotator.cpp +++ b/ui/pageviewannotator.cpp @@ -33,10 +33,12 @@ #include // local includes +#include "conf/editannottooldialog.h" #include "core/area.h" #include "core/document.h" #include "core/page.h" #include "core/annotations.h" +#include "ui/annotationactionhandler.h" #include "settings.h" #include "annotationtools.h" #include "guiutils.h" @@ -65,7 +67,7 @@ // create engine objects if ( !hoverIconName.simplified().isEmpty() ) - pixmap = GuiUtils::loadStamp( hoverIconName, QSize( size, size ) ); + pixmap = GuiUtils::loadStamp( hoverIconName, size ); } QRect event( EventType type, Button button, double nX, double nY, double xScale, double yScale, const Okular::Page * page ) override @@ -244,8 +246,8 @@ const int ml = ( rcf.bottomRight() - rcf.topLeft() ).toPoint().manhattanLength(); if ( ml <= QApplication::startDragDistance() ) { - const double stampxscale = size / xscale; - const double stampyscale = size / yscale; + const double stampxscale = pixmap.width() / xscale; + const double stampyscale = pixmap.height() / yscale; if ( center ) { rect.left = point.x - stampxscale / 2; @@ -656,134 +658,119 @@ QRect rect; }; - -/** PageViewAnnotator **/ - -PageViewAnnotator::PageViewAnnotator( PageView * parent, Okular::Document * storage ) - : QObject( parent ), m_document( storage ), m_pageView( parent ), - m_toolBar( nullptr ), m_engine( nullptr ), m_textToolsEnabled( false ), m_toolsEnabled( false ), - m_continuousMode( false ), m_hidingWasForced( false ), m_lastToolID( -1 ), m_lockedItem( nullptr ) -{ - reparseConfig(); -} - -void PageViewAnnotator::reparseConfig() +/** @short AnnotationTools*/ +class AnnotationTools { - m_items.clear(); - - // Read tool list from configuration. It's a list of XML elements - const QStringList userTools = Okular::Settings::annotationTools(); + public: + AnnotationTools() : m_toolsCount( 0 ) {} - // Populate m_toolsDefinition - QDomDocument doc; - m_toolsDefinition = doc.createElement( QStringLiteral("annotatingTools") ); - foreach ( const QString &toolXml, userTools ) - { - QDomDocument entryParser; - if ( entryParser.setContent( toolXml ) ) - m_toolsDefinition.appendChild( doc.importNode( entryParser.documentElement(), true ) ); - else - qCWarning(OkularUiDebug) << "Skipping malformed tool XML in AnnotationTools setting"; - } + void setTools( QStringList tools ) + { + // Populate m_toolsDefinition + m_toolsCount = 0; + m_toolsDefinition.clear(); + QDomElement root = m_toolsDefinition.createElement( QStringLiteral( "root" ) ); + m_toolsDefinition.appendChild( root ); + foreach ( const QString &toolXml, tools ) + { + QDomDocument entryParser; + if ( entryParser.setContent( toolXml ) ) { + root.appendChild( m_toolsDefinition.importNode( entryParser.documentElement(), true ) ); + m_toolsCount++; + } else { + qCWarning(OkularUiDebug) << "Skipping malformed tool XML in AnnotationTools setting"; + } + } + } - // Create the AnnotationToolItems from the XML dom tree - QDomNode toolDescription = m_toolsDefinition.firstChild(); - while ( toolDescription.isElement() ) - { - QDomElement toolElement = toolDescription.toElement(); - if ( toolElement.tagName() == QLatin1String("tool") ) + QStringList toStringList() { - AnnotationToolItem item; - item.id = toolElement.attribute(QStringLiteral("id")).toInt(); - if ( toolElement.hasAttribute( QStringLiteral("name") ) ) - item.text = toolElement.attribute( QStringLiteral("name") ); - else - item.text = defaultToolName( toolElement ); - item.pixmap = makeToolPixmap( toolElement ); - QDomNode shortcutNode = toolElement.elementsByTagName( QStringLiteral("shortcut") ).item( 0 ); - if ( shortcutNode.isElement() ) - item.shortcut = shortcutNode.toElement().text(); - QDomNodeList engineNodeList = toolElement.elementsByTagName( QStringLiteral("engine") ); - if ( engineNodeList.size() > 0 ) + QStringList tools; + QDomElement toolElement = m_toolsDefinition.documentElement().firstChildElement(); + QString str; + QTextStream stream(&str); + while( !toolElement.isNull() ) { - QDomElement engineEl = engineNodeList.item( 0 ).toElement(); - if ( !engineEl.isNull() && engineEl.hasAttribute( QStringLiteral("type") ) ) - item.isText = engineEl.attribute( QStringLiteral("type") ) == QLatin1String( "TextSelector" ); + str.clear(); + toolElement.save(stream, -1 /* indent disabled */); + tools << str; + toolElement = toolElement.nextSiblingElement(); } - m_items.push_back( item ); + return tools; } - toolDescription = toolDescription.nextSibling(); - } -} -PageViewAnnotator::~PageViewAnnotator() -{ - delete m_engine; -} + QDomElement tool( int toolID ) + { + QDomElement toolElement = m_toolsDefinition.documentElement().firstChildElement(); + while( !toolElement.isNull() && toolElement.attribute("id").toInt() != toolID ) { + toolElement = toolElement.nextSiblingElement(); + } + return toolElement; // can return a null element + } -void PageViewAnnotator::setEnabled( bool on ) -{ - if ( !on ) - { - // remove toolBar - if ( m_toolBar ) - m_toolBar->hideAndDestroy(); - m_toolBar = nullptr; - // deactivate the active tool, if any - slotToolSelected( -1 ); - return; - } + void appendTool( QDomElement toolElement ) + { + toolElement = toolElement.cloneNode().toElement(); + toolElement.setAttribute( QStringLiteral( "id" ), ++m_toolsCount ); + m_toolsDefinition.documentElement().appendChild( toolElement ); + } - // if no tools are defined, don't show the toolbar - if ( !m_toolsDefinition.hasChildNodes() ) - return; + bool updateTool( QDomElement newToolElement, int toolID ) + { + QDomElement toolElement = tool( toolID ); + if ( toolElement.isNull() ) + return false; + newToolElement = newToolElement.cloneNode().toElement(); + newToolElement.setAttribute( QStringLiteral( "id" ), toolID ); + QDomNode oldTool = m_toolsDefinition.documentElement().replaceChild( newToolElement, toolElement ); + return !oldTool.isNull(); + } - // create toolBar - if ( !m_toolBar ) - { - m_toolBar = new PageViewToolBar( m_pageView, m_pageView->viewport() ); - m_toolBar->setSide( (PageViewToolBar::Side)Okular::Settings::editToolBarPlacement() ); - m_toolBar->setItems( m_items ); - m_toolBar->setToolsEnabled( m_toolsEnabled ); - m_toolBar->setTextToolsEnabled( m_textToolsEnabled ); - connect(m_toolBar, &PageViewToolBar::toolSelected, this, &PageViewAnnotator::slotToolSelected); - connect(m_toolBar, &PageViewToolBar::orientationChanged, this, &PageViewAnnotator::slotSaveToolbarOrientation); - - connect(m_toolBar, &PageViewToolBar::buttonDoubleClicked, this, &PageViewAnnotator::slotToolDoubleClicked); - m_toolBar->setCursor(Qt::ArrowCursor); - } + private: + QDomDocument m_toolsDefinition; + int m_toolsCount; +}; - // show the toolBar - m_toolBar->showAndAnimate(); -} +/** PageViewAnnotator **/ +const int PageViewAnnotator::STAMP_TOOL_ID = 14; -void PageViewAnnotator::setTextToolsEnabled( bool enabled ) +PageViewAnnotator::PageViewAnnotator( PageView * parent, Okular::Document * storage ) + : QObject( parent ), m_document( storage ), m_pageView( parent ), + m_actionHandler( nullptr ), m_engine( nullptr ), m_toolsDefinition( nullptr ), + m_quickToolsDefinition( nullptr ), m_continuousMode( true ), m_lastToolID( -1 ), + m_lockedItem( nullptr ) { - m_textToolsEnabled = enabled; - if ( m_toolBar ) - m_toolBar->setTextToolsEnabled( m_textToolsEnabled ); + reparseConfig(); } -void PageViewAnnotator::setToolsEnabled( bool enabled ) +void PageViewAnnotator::reparseConfig() { - m_toolsEnabled = enabled; - if ( m_toolBar ) - m_toolBar->setToolsEnabled( m_toolsEnabled ); -} + // Read tool list from configuration. It's a list of XML elements + if( !m_toolsDefinition ) + m_toolsDefinition = new AnnotationTools(); + m_toolsDefinition->setTools( Okular::Settings::annotationTools() ); -void PageViewAnnotator::setHidingForced( bool forced ) -{ - m_hidingWasForced = forced; + if( !m_quickToolsDefinition ) + m_quickToolsDefinition = new AnnotationTools(); + m_quickToolsDefinition->setTools( Okular::Settings::quickAnnotationTools() ); + + m_continuousMode = Okular::Settings::annotationContinuousMode(); + + if ( Okular::Settings::identityAuthor().isEmpty() ) + detachAnnotation(); + + if( m_actionHandler ) + m_actionHandler->reparseTools(); } -bool PageViewAnnotator::hidingWasForced() const +PageViewAnnotator::~PageViewAnnotator() { - return m_hidingWasForced; + delete m_engine; } bool PageViewAnnotator::active() const { - return m_engine && m_toolBar; + return m_engine != nullptr; } bool PageViewAnnotator::annotating() const @@ -839,10 +826,10 @@ m_lastDrawnRect = paintRect; m_lastDrawnRect.translate( itemRect.left(), itemRect.top() ); // 3.2. decompose paint region in rects and send paint events - const QVector rects = compoundRegion.united( m_lastDrawnRect ).rects(); + const QRegion rgn = compoundRegion.united( m_lastDrawnRect ); const QPoint areaPos = m_pageView->contentAreaPosition(); - for ( int i = 0; i < rects.count(); i++ ) - m_pageView->viewport()->update( rects[i].translated( -areaPos ) ); + for ( const QRect &r : rgn ) + m_pageView->viewport()->update( r.translated( -areaPos ) ); modifiedRect = compoundRegion.boundingRect() | m_lastDrawnRect; } @@ -866,7 +853,7 @@ } if ( m_continuousMode ) - slotToolSelected( m_lastToolID ); + selectTool( m_lastToolID ); else detachAnnotation(); } @@ -895,18 +882,6 @@ return QRect(); } - // We set all tablet events that take place over the annotations toolbar to ignore so that corresponding mouse - // events will be delivered to the toolbar. However, we still allow the annotations code to handle - // TabletMove and TabletRelease events in case the user is drawing an annotation onto the toolbar. - const QPoint toolBarPos = m_toolBar->mapFromGlobal( e->globalPos() ); - const QRect toolBarRect = m_toolBar->rect(); - if ( toolBarRect.contains( toolBarPos ) ) - { - e->ignore(); - if (e->type() == QEvent::TabletPress) - return QRect(); - } - AnnotatorEngine::EventType eventType; AnnotatorEngine::Button button; @@ -930,7 +905,7 @@ bool PageViewAnnotator::routePaints( const QRect & wantedRect ) const { - return m_engine && m_toolBar && wantedRect.intersects( m_lastDrawnRect ) && m_lockedItem; + return m_engine && wantedRect.intersects( m_lastDrawnRect ) && m_lockedItem; } void PageViewAnnotator::routePaint( QPainter * painter, const QRect & paintRect ) @@ -959,8 +934,38 @@ painter->restore(); } -void PageViewAnnotator::slotToolSelected( int toolID ) +void PageViewAnnotator::selectTool( int toolID ) { + + // ask for Author's name if not already set + if ( toolID > 0 && Okular::Settings::identityAuthor().isEmpty() ) + { + // get default username from the kdelibs/kdecore/KUser + KUser currentUser; + QString userName = currentUser.property( KUser::FullName ).toString(); + userName = ""; + // ask the user for confirmation/change + if ( userName.isEmpty() ) + { + bool ok = false; + userName = QInputDialog::getText(nullptr, + i18n( "Bookmark annotation" ), + i18n( "Insert a custom name for the annotation:" ), + QLineEdit::Normal, + QString(), + &ok ); + + if ( !ok ) + { + detachAnnotation(); + return; + } + } + // save the name + Okular::Settings::setIdentityAuthor( userName ); + Okular::Settings::self()->save(); + } + // terminate any previous operation if ( m_engine ) { @@ -974,7 +979,6 @@ m_lastDrawnRect = QRect(); } - if ( toolID != m_lastToolID ) m_continuousMode = false; // store current tool for later usage m_lastToolID = toolID; @@ -987,38 +991,25 @@ } // for the selected tool create the Engine - QDomNode toolNode = m_toolsDefinition.firstChild(); - while ( toolNode.isElement() ) + QDomElement toolElement = m_toolsDefinition->tool( toolID ); + if ( !toolElement.isNull() ) { - QDomElement toolElement = toolNode.toElement(); - toolNode = toolNode.nextSibling(); - - // only find out the element describing selected tool - if ( toolElement.tagName() != QLatin1String("tool") || toolElement.attribute(QStringLiteral("id")).toInt() != toolID ) - continue; - // parse tool properties - QDomNode toolSubNode = toolElement.firstChild(); - while ( toolSubNode.isElement() ) + QDomElement engineElement = toolElement.firstChildElement( QStringLiteral("engine") ); + if ( !engineElement.isNull() ) { - QDomElement toolSubElement = toolSubNode.toElement(); - toolSubNode = toolSubNode.nextSibling(); - // create the AnnotatorEngine - if ( toolSubElement.tagName() == QLatin1String("engine") ) - { - QString type = toolSubElement.attribute( QStringLiteral("type") ); - if ( type == QLatin1String("SmoothLine") ) - m_engine = new SmoothPathEngine( toolSubElement ); - else if ( type == QLatin1String("PickPoint") ) - m_engine = new PickPointEngine( toolSubElement ); - else if ( type == QLatin1String("PolyLine") ) - m_engine = new PolyLineEngine( toolSubElement ); - else if ( type == QLatin1String("TextSelector") ) - m_engine = new TextSelectorEngine( toolSubElement, m_pageView ); - else - qCWarning(OkularUiDebug).nospace() << "tools.xml: engine type:'" << type << "' is not defined!"; - } + QString type = engineElement.attribute( QStringLiteral("type") ); + if ( type == QLatin1String("SmoothLine") ) + m_engine = new SmoothPathEngine( engineElement ); + else if ( type == QLatin1String("PickPoint") ) + m_engine = new PickPointEngine( engineElement ); + else if ( type == QLatin1String("PolyLine") ) + m_engine = new PolyLineEngine( engineElement ); + else if ( type == QLatin1String("TextSelector") ) + m_engine = new TextSelectorEngine( engineElement, m_pageView ); + else + qCWarning(OkularUiDebug).nospace() << "tools.xml: engine type:'" << type << "' is not defined!"; // display the tooltip const QString annotType = toolElement.attribute( QStringLiteral("type") ); @@ -1062,25 +1053,28 @@ } m_pageView->updateCursor(); - // stop after parsing selected tool's node - break; } -} -void PageViewAnnotator::slotSaveToolbarOrientation( int side ) -{ - Okular::Settings::setEditToolBarPlacement( (int)side ); - Okular::Settings::self()->save(); + if( toolID > 0 ) + emit toolSelected(); } -void PageViewAnnotator::slotToolDoubleClicked( int /*toolID*/ ) +void PageViewAnnotator::selectStampTool( const QString &stampSymbol) { - m_continuousMode = true; + QDomElement toolElement = builtinTool( STAMP_TOOL_ID ); + QDomElement engineElement = toolElement.firstChildElement( QStringLiteral("engine") ); + QDomElement annotationElement = engineElement.firstChildElement( QStringLiteral("annotation") ); + engineElement.setAttribute( QStringLiteral( "hoverIcon" ), stampSymbol ); + annotationElement.setAttribute( QStringLiteral( "icon" ), stampSymbol ); + saveAnnotationTools(); + selectTool( STAMP_TOOL_ID ); } void PageViewAnnotator::detachAnnotation() { - m_toolBar->selectButton( -1 ); + selectTool( -1 ); + if( m_actionHandler ) + m_actionHandler->deselectAllAnnotationActions(); } QString PageViewAnnotator::defaultToolName( const QDomElement &toolElement ) @@ -1235,7 +1229,7 @@ } else if ( annotType == QLatin1String("stamp") ) { - QPixmap stamp = GuiUtils::loadStamp( icon, QSize( 16, 16 ) ); + QPixmap stamp = GuiUtils::loadStamp( icon, 16, false /* keepAspectRatio */ ); p.setRenderHint( QPainter::Antialiasing ); p.drawPixmap( 16, 14, stamp ); } @@ -1278,6 +1272,166 @@ return pixmap; } +void PageViewAnnotator::setupActions( KActionCollection * ac ) +{ + if ( !m_actionHandler ) { + m_actionHandler = new AnnotationActionHandler( this, ac ); + } +} + +bool PageViewAnnotator::continuousMode() +{ + return m_continuousMode; +} + +void PageViewAnnotator::setContinuousMode( bool enabled ) +{ + m_continuousMode = enabled; + Okular::Settings::setAnnotationContinuousMode( enabled ); + Okular::Settings::self()->save(); +} + +void PageViewAnnotator::setToolsEnabled( bool enabled ) +{ + if ( m_actionHandler ) + m_actionHandler->setToolsEnabled( enabled ); +} + +void PageViewAnnotator::setTextToolsEnabled( bool enabled ) +{ + if ( m_actionHandler ) + m_actionHandler->setTextToolsEnabled( enabled ); +} + +void PageViewAnnotator::saveAnnotationTools() +{ + Okular::Settings::setAnnotationTools( m_toolsDefinition->toStringList() ); + Okular::Settings::setQuickAnnotationTools( m_quickToolsDefinition->toStringList() ); + Okular::Settings::self()->save(); +} + +int PageViewAnnotator::setQuickTool( int favToolID ) +{ + int toolId = -1; + QDomElement favToolElement = m_quickToolsDefinition->tool( favToolID ); + if ( !favToolElement.isNull() && favToolElement.hasAttribute( QStringLiteral("sourceId") ) ) + { + toolId = favToolElement.attribute( QStringLiteral("sourceId") ).toInt(); + if ( m_toolsDefinition->updateTool( favToolElement, toolId ) ) + saveAnnotationTools(); + } + return toolId; +} + +QDomElement PageViewAnnotator::builtinTool( int toolID ) +{ + return m_toolsDefinition->tool( toolID ); +} + +QDomElement PageViewAnnotator::quickTool( int toolID ) +{ + return m_quickToolsDefinition->tool( toolID ); +} + +QDomElement PageViewAnnotator::currentEngineElement() +{ + return m_toolsDefinition->tool( m_lastToolID ).firstChildElement( QStringLiteral("engine") ); +} + +QDomElement PageViewAnnotator::currentAnnotationElement() +{ + return currentEngineElement().firstChildElement( QStringLiteral("annotation") ); +} + +void PageViewAnnotator::setAnnotationWidth( double width ) +{ + currentAnnotationElement().setAttribute( QStringLiteral( "width" ), QString::number( width ) ); + saveAnnotationTools(); + selectTool( m_lastToolID ); +} + +void PageViewAnnotator::setAnnotationColor( const QColor &color ) +{ + currentEngineElement().setAttribute( QStringLiteral( "color" ), color.name(QColor::HexRgb ) ); + QDomElement annotationElement = currentAnnotationElement(); + QString annotType = annotationElement.attribute( QStringLiteral( "type" ) ); + if ( annotType == "Typewriter" ) { + annotationElement.setAttribute( QStringLiteral( "textColor" ), color.name( QColor::HexRgb ) ); + } else { + annotationElement.setAttribute( QStringLiteral( "color" ), color.name( QColor::HexRgb ) ); + } + saveAnnotationTools(); + selectTool( m_lastToolID ); +} + +void PageViewAnnotator::setAnnotationInnerColor( const QColor &color ) +{ + QDomElement annotationElement = currentAnnotationElement(); + QString annotType = annotationElement.attribute( QStringLiteral( "type" ) ); + if ( color == Qt::transparent ) { + annotationElement.removeAttribute( QStringLiteral( "innerColor" ) ); + } else { + annotationElement.setAttribute( QStringLiteral( "innerColor" ), color.name( QColor::HexRgb ) ); + } + saveAnnotationTools(); + selectTool( m_lastToolID ); +} + +void PageViewAnnotator::setAnnotationOpacity( double opacity ) +{ + currentAnnotationElement().setAttribute( QStringLiteral( "opacity" ), QString::number( opacity ) ); + saveAnnotationTools(); + selectTool( m_lastToolID ); +} + +void PageViewAnnotator::setAnnotationFont( const QFont &font ) +{ + currentAnnotationElement().setAttribute( QStringLiteral( "font" ), font.toString() ); + saveAnnotationTools(); + selectTool( m_lastToolID ); +} + +void PageViewAnnotator::addToQuickAnnotations() +{ + QDomElement sourceToolElement = m_toolsDefinition->tool( m_lastToolID ); + if( sourceToolElement.isNull() ) + return; + + // set custom name for quick annotation + bool ok = false; + QString itemText = QInputDialog::getText(nullptr, + i18n( "Add favorite annotation" ), + i18n( "Custom annotation name:" ), + QLineEdit::Normal, + defaultToolName( sourceToolElement ), + &ok ); + if( !ok ) + return; + + QDomElement toolElement = sourceToolElement.cloneNode().toElement(); + // store name attribute only if the user specified a customized name + if ( !itemText.isEmpty() ) + toolElement.setAttribute( QStringLiteral("name"), itemText ); + toolElement.setAttribute( QStringLiteral("sourceId"), sourceToolElement.attribute( QStringLiteral("id") ) ); + m_quickToolsDefinition->appendTool( toolElement ); + saveAnnotationTools(); +} + +void PageViewAnnotator::slotAdvancedSettings() +{ + QDomElement toolElement = m_toolsDefinition->tool( m_lastToolID ); + + EditAnnotToolDialog t( nullptr, toolElement, true ); + if ( t.exec() != QDialog::Accepted ) + return; + + QDomElement toolElementUpdated = t.toolXml().documentElement(); + int toolID = toolElement.attribute( QStringLiteral("id") ).toInt(); + m_toolsDefinition->updateTool( toolElementUpdated, toolID ); + saveAnnotationTools(); + selectTool( m_lastToolID ); +} + #include "moc_pageviewannotator.cpp" /* kate: replace-tabs on; indent-width 4; */ diff --git a/ui/pageviewutils.h b/ui/pageviewutils.h --- a/ui/pageviewutils.h +++ b/ui/pageviewutils.h @@ -17,8 +17,6 @@ #include #include #include -#include - #include "core/area.h" @@ -128,97 +126,4 @@ int m_lineSpacing; }; -struct AnnotationToolItem -{ - AnnotationToolItem() - : id( -1 ), isText( false ) - { - } - - int id; - QString text; - QPixmap pixmap; - QString shortcut; - bool isText; -}; - -class ToolBarButton : public QToolButton -{ - Q_OBJECT - public: - static const int iconSize = 32; - static const int buttonSize = 40; - - ToolBarButton( QWidget * parent, const AnnotationToolItem &item ); - int buttonID() const { return m_id; } - bool isText() const { return m_isText; } - - Q_SIGNALS: - void buttonDoubleClicked( int buttonID ); - - protected: - void mouseDoubleClickEvent( QMouseEvent * event ) override; - - private: - int m_id; - bool m_isText; -}; - -/** - * @short A widget containing exclusive buttons, that slides in from a side. - * - * This is a shaped widget that slides in from a side of the 'anchor widget' - * it's attached to. It can be dragged and docked on {left,top,right,bottom} - * sides and contains toggable exclusive buttons. - * When a 'tool' of this 'toolBar' is selected, a signal is emitted. - */ -class PageViewToolBar : public QWidget -{ - Q_OBJECT - public: - PageViewToolBar( PageView * parent, QWidget * anchorWidget ); - ~PageViewToolBar(); - - // animated widget controls - enum Side { Left = 0, Top = 1, Right = 2, Bottom = 3 }; - - void setItems( const QLinkedList &items ); - void setSide( Side side ); - - void showAndAnimate(); - void hideAndDestroy(); - - void selectButton( int id ); - - void setToolsEnabled( bool on ); - void setTextToolsEnabled( bool on ); - - // query properties - - Q_SIGNALS: - // the tool 'toolID' has been selected - void toolSelected( int toolID ); - // orientation has been changed - void orientationChanged( int side ); - // a tool button of this toolbar has been double clicked - void buttonDoubleClicked( int buttonID ); - - protected: - // handle widget events { anchor_resize, paint, animation, drag } - bool eventFilter( QObject * o, QEvent * e ) override; - void paintEvent( QPaintEvent * ) override; - void mousePressEvent( QMouseEvent * e ) override; - void mouseMoveEvent( QMouseEvent * e ) override; - void mouseReleaseEvent( QMouseEvent * e ) override; - - private: - // private variables - friend class ToolBarPrivate; - class ToolBarPrivate * d; - - private Q_SLOTS: - void slotAnimate(); - void slotButtonClicked(); -}; - #endif diff --git a/ui/pageviewutils.cpp b/ui/pageviewutils.cpp --- a/ui/pageviewutils.cpp +++ b/ui/pageviewutils.cpp @@ -14,30 +14,14 @@ // qt/kde includes #include -#include -#include -#include -#include -#include +#include #include -#include -#include #include -#include -#include -#include -#include #include -#include - -// system includes -#include // local includes #include "formwidgets.h" -#include "pageview.h" #include "videowidget.h" -#include "core/movie.h" #include "core/page.h" #include "core/form.h" #include "settings.h" @@ -429,520 +413,4 @@ hide(); } - -/*********************/ -/** PageViewToolBar */ -/*********************/ - -ToolBarButton::ToolBarButton( QWidget * parent, const AnnotationToolItem &item ) - : QToolButton( parent ), m_id( item.id ), m_isText( item.isText ) -{ - setCheckable( true ); - setAutoRaise( true ); - resize( buttonSize, buttonSize ); - setIconSize( QSize( iconSize, iconSize ) ); - setIcon( QIcon( item.pixmap ) ); - // set shortcut if defined - if ( !item.shortcut.isEmpty() ) - setShortcut( QKeySequence( item.shortcut ) ); - else - KAcceleratorManager::setNoAccel( this ); - - // if accel is set display it along name - QString accelString = shortcut().toString( QKeySequence::NativeText ); - if ( !accelString.isEmpty() ) - setToolTip( QStringLiteral("%1 [%2]").arg( item.text, accelString ) ); - else - setToolTip( item.text ); -} - -void ToolBarButton::mouseDoubleClickEvent( QMouseEvent * /*event*/ ) -{ - emit buttonDoubleClicked( buttonID() ); -} - -/* PageViewToolBar */ - -static const int toolBarGridSize = 40; - -class ToolBarPrivate -{ -public: - ToolBarPrivate( PageViewToolBar * qq ) - : q( qq ) - { - } - - // rebuild contents and reposition then widget - void buildToolBar(); - void reposition(); - // compute the visible and hidden positions along current side - QPoint getInnerPoint() const; - QPoint getOuterPoint() const; - void selectButton( ToolBarButton * button ); - - PageViewToolBar * q; - - // anchored widget and side - QWidget * anchorWidget; - PageViewToolBar::Side anchorSide; - - // slide in/out stuff - QTimer * animTimer; - QPoint currentPosition; - QPoint endPosition; - bool hiding; - bool visible; - - // background pixmap and buttons - QPixmap backgroundPixmap; - QLinkedList< ToolBarButton * > buttons; -}; - -PageViewToolBar::PageViewToolBar( PageView * parent, QWidget * anchorWidget ) - : QWidget( parent ), d( new ToolBarPrivate( this ) ) -{ - // initialize values of the private data storage structure - d->anchorWidget = anchorWidget; - d->anchorSide = Left; - d->hiding = false; - d->visible = false; - - // create the animation timer - d->animTimer = new QTimer( this ); - connect( d->animTimer, &QTimer::timeout, this, &PageViewToolBar::slotAnimate ); - - // apply a filter to get notified when anchor changes geometry - d->anchorWidget->installEventFilter( this ); - - setContextMenuPolicy( Qt::ActionsContextMenu ); - addAction( parent->actionCollection()->action( QStringLiteral("options_configure_annotations") ) ); -} - -PageViewToolBar::~PageViewToolBar() -{ - // delete the private data storage structure - delete d; -} - -void PageViewToolBar::setItems( const QLinkedList &items ) -{ - // delete buttons if already present - if ( !d->buttons.isEmpty() ) - { - QLinkedList< ToolBarButton * >::iterator it = d->buttons.begin(), end = d->buttons.end(); - for ( ; it != end; ++it ) - delete *it; - d->buttons.clear(); - } - - // create new buttons for given items - QLinkedList::const_iterator it = items.begin(), end = items.end(); - for ( ; it != end; ++it ) - { - ToolBarButton * button = new ToolBarButton( this, *it ); - connect(button, &ToolBarButton::clicked, this, &PageViewToolBar::slotButtonClicked); - connect(button, &ToolBarButton::buttonDoubleClicked, this, &PageViewToolBar::buttonDoubleClicked); - d->buttons.append( button ); - } - - // rebuild toolbar shape and contents - d->reposition(); -} - -void PageViewToolBar::setSide( Side side ) -{ - d->anchorSide = side; - - d->reposition(); -} - -void PageViewToolBar::showAndAnimate() -{ - // set parameters for sliding in - d->hiding = false; - - show(); - -#ifdef OKULAR_ANIMATE_REVIEW_TOOBAR - // start scrolling in - d->animTimer->start( 20 ); -#else - d->currentPosition = d->endPosition; - - move( d->currentPosition ); - - d->visible = true; -#endif -} - -void PageViewToolBar::hideAndDestroy() -{ - // set parameters for sliding out - d->hiding = true; - d->endPosition = d->getOuterPoint(); - -#ifdef OKULAR_ANIMATE_REVIEW_TOOBAR - // start scrolling out - d->animTimer->start( 20 ); -#else - d->currentPosition = d->endPosition; - - move( d->currentPosition ); - - d->visible = false; - deleteLater(); -#endif -} - -void PageViewToolBar::selectButton( int id ) -{ - ToolBarButton * button = nullptr; - if ( id >= 0 && id < d->buttons.count() ) - button = *(d->buttons.begin() + id); - else - { - QLinkedList< ToolBarButton * >::const_iterator it = d->buttons.begin(), end = d->buttons.end(); - for ( ; !button && it != end; ++it ) - if ( (*it)->isChecked() ) - button = *it; - if ( button ) - button->setChecked( false ); - } - d->selectButton( button ); -} - -bool PageViewToolBar::eventFilter( QObject * obj, QEvent * e ) -{ - // if anchorWidget changed geometry reposition toolbar - if ( obj == d->anchorWidget && e->type() == QEvent::Resize ) - { - d->animTimer->stop(); - if ( d->hiding ) - deleteLater(); - else - d->reposition(); - } - - // don't block event - return false; -} - -void PageViewToolBar::paintEvent( QPaintEvent * e ) -{ - // paint the internal pixmap over the widget - QPainter p( this ); - p.drawImage( e->rect().topLeft(), d->backgroundPixmap.toImage(), e->rect() ); -} - -void PageViewToolBar::mousePressEvent( QMouseEvent * e ) -{ - // set 'dragging' cursor - if ( e->button() == Qt::LeftButton ) - setCursor( Qt::SizeAllCursor ); -} - -void PageViewToolBar::mouseMoveEvent( QMouseEvent * e ) -{ - if ( ( QApplication::mouseButtons() & Qt::LeftButton ) != Qt::LeftButton ) - return; - - // compute the nearest side to attach the widget to - QPoint parentPos = mapToParent( e->pos() ); - float nX = (float)parentPos.x() / (float)d->anchorWidget->width(), - nY = (float)parentPos.y() / (float)d->anchorWidget->height(); - if ( nX > 0.3 && nX < 0.7 && nY > 0.3 && nY < 0.7 ) - return; - bool LT = nX < (1.0 - nY); - bool LB = nX < (nY); - Side side = LT ? ( LB ? Left : Top ) : ( LB ? Bottom : Right ); - - // check if side changed - if ( side == d->anchorSide ) - return; - - d->anchorSide = side; - d->reposition(); - emit orientationChanged( (int)side ); -} - -void PageViewToolBar::mouseReleaseEvent( QMouseEvent * e ) -{ - // set normal cursor - if ( e->button() == Qt::LeftButton ) - setCursor( Qt::ArrowCursor ); -} - -void ToolBarPrivate::buildToolBar() -{ - int buttonsNumber = buttons.count(), - parentWidth = anchorWidget->width(), - parentHeight = anchorWidget->height(), - myCols = 1, - myRows = 1; - - // 1. find out columns and rows we're going to use - bool topLeft = anchorSide == PageViewToolBar::Left || anchorSide == PageViewToolBar::Top; - bool vertical = anchorSide == PageViewToolBar::Left || anchorSide == PageViewToolBar::Right; - if ( vertical ) - { - myCols = 1 + (buttonsNumber * toolBarGridSize) / - (parentHeight - toolBarGridSize); - myRows = (int)ceil( (float)buttonsNumber / (float)myCols ); - } - else - { - myRows = 1 + (buttonsNumber * toolBarGridSize) / - (parentWidth - toolBarGridSize); - myCols = (int)ceil( (float)buttonsNumber / (float)myRows ); - } - - // 2. compute widget size (from rows/cols) - int myWidth = myCols * toolBarGridSize, - myHeight = myRows * toolBarGridSize, - xOffset = (toolBarGridSize - ToolBarButton::buttonSize) / 2, - yOffset = (toolBarGridSize - ToolBarButton::buttonSize) / 2; - - if ( vertical ) - { - myHeight += 16; - myWidth += 4; - yOffset += 12; - if ( anchorSide == PageViewToolBar::Right ) - xOffset += 4; - } - else - { - myWidth += 16; - myHeight += 4; - xOffset += 12; - if ( anchorSide == PageViewToolBar::Bottom ) - yOffset += 4; - } - - bool prevUpdates = q->updatesEnabled(); - q->setUpdatesEnabled( false ); - - // 3. resize pixmap, mask and widget - QBitmap mask( myWidth + 1, myHeight + 1 ); - backgroundPixmap = QPixmap( myWidth + 1, myHeight + 1 ); - backgroundPixmap.fill(Qt::transparent); - q->resize( myWidth + 1, myHeight + 1 ); - - // 4. create and set transparency mask // 4. draw background - QPainter maskPainter( &mask); - mask.fill( Qt::white ); - maskPainter.setBrush( Qt::black ); - if ( vertical ) - maskPainter.drawRoundRect( topLeft ? -10 : 0, 0, myWidth + 11, myHeight, 2000 / (myWidth + 10), 2000 / myHeight ); - else - maskPainter.drawRoundRect( 0, topLeft ? -10 : 0, myWidth, myHeight + 11, 2000 / myWidth, 2000 / (myHeight + 10) ); - maskPainter.end(); - q->setMask( mask ); - - // 5. draw background - QPainter bufferPainter( &backgroundPixmap ); - bufferPainter.translate( 0.5, 0.5 ); - QPalette pal = q->palette(); - // 5.1. draw horizontal/vertical gradient - QLinearGradient grad; - switch ( anchorSide ) - { - case PageViewToolBar::Left: - grad = QLinearGradient( 0, 1, myWidth + 1, 1 ); - break; - case PageViewToolBar::Right: - grad = QLinearGradient( myWidth + 1, 1, 0, 1 ); - break; - case PageViewToolBar::Top: - grad = QLinearGradient( 1, 0, 1, myHeight + 1 ); - break; - case PageViewToolBar::Bottom: - grad = QLinearGradient( 1, myHeight + 1, 0, 1 ); - break; - } - grad.setColorAt( 0, pal.color( QPalette::Active, QPalette::Button ) ); - grad.setColorAt( 1, pal.color( QPalette::Active, QPalette::Light ) ); - bufferPainter.setBrush( QBrush( grad ) ); - // 5.2. draw rounded border - bufferPainter.setPen( pal.color( QPalette::Active, QPalette::Dark ).lighter( 140 ) ); - bufferPainter.setRenderHints( QPainter::Antialiasing ); - if ( vertical ) - bufferPainter.drawRoundRect( topLeft ? -10 : 0, 0, myWidth + 10, myHeight, 2000 / (myWidth + 10), 2000 / myHeight ); - else - bufferPainter.drawRoundRect( 0, topLeft ? -10 : 0, myWidth, myHeight + 10, 2000 / myWidth, 2000 / (myHeight + 10) ); - // 5.3. draw handle - bufferPainter.translate( -0.5, -0.5 ); - bufferPainter.setPen( pal.color( QPalette::Active, QPalette::Mid ) ); - if ( vertical ) - { - int dx = anchorSide == PageViewToolBar::Left ? 2 : 4; - bufferPainter.drawLine( dx, 6, dx + myWidth - 8, 6 ); - bufferPainter.drawLine( dx, 9, dx + myWidth - 8, 9 ); - bufferPainter.setPen( pal.color( QPalette::Active, QPalette::Light ) ); - bufferPainter.drawLine( dx + 1, 7, dx + myWidth - 7, 7 ); - bufferPainter.drawLine( dx + 1, 10, dx + myWidth - 7, 10 ); - } - else - { - int dy = anchorSide == PageViewToolBar::Top ? 2 : 4; - bufferPainter.drawLine( 6, dy, 6, dy + myHeight - 8 ); - bufferPainter.drawLine( 9, dy, 9, dy + myHeight - 8 ); - bufferPainter.setPen( pal.color( QPalette::Active, QPalette::Light ) ); - bufferPainter.drawLine( 7, dy + 1, 7, dy + myHeight - 7 ); - bufferPainter.drawLine( 10, dy + 1, 10, dy + myHeight - 7 ); - } - bufferPainter.end(); - - // 6. reposition buttons (in rows/col grid) - int gridX = 0, - gridY = 0; - QLinkedList< ToolBarButton * >::const_iterator it = buttons.begin(), end = buttons.end(); - for ( ; it != end; ++it ) - { - ToolBarButton * button = *it; - button->move( gridX * toolBarGridSize + xOffset, - gridY * toolBarGridSize + yOffset ); - button->show(); - if ( ++gridX == myCols ) - { - gridX = 0; - gridY++; - } - } - - q->setUpdatesEnabled( prevUpdates ); -} - -void ToolBarPrivate::reposition() -{ - // note: hiding widget here will gives better gfx, but ends drag operation - // rebuild widget and move it to its final place - buildToolBar(); - if ( !visible ) - { - currentPosition = getOuterPoint(); - endPosition = getInnerPoint(); - } - else - { - currentPosition = getInnerPoint(); - endPosition = getOuterPoint(); - } - q->move( currentPosition ); - - // repaint all buttons (to update background) - QLinkedList< ToolBarButton * >::const_iterator it = buttons.begin(), end = buttons.end(); - for ( ; it != end; ++it ) - (*it)->update(); -} - -QPoint ToolBarPrivate::getInnerPoint() const -{ - // returns the final position of the widget - QPoint newPos; - switch ( anchorSide ) - { - case PageViewToolBar::Left: - newPos = QPoint( 0, ( anchorWidget->height() - q->height() ) / 2 ); - break; - case PageViewToolBar::Top: - newPos = QPoint( ( anchorWidget->width() - q->width() ) / 2, 0 ); - break; - case PageViewToolBar::Right: - newPos = QPoint( anchorWidget->width() - q->width(), ( anchorWidget->height() - q->height() ) / 2 ); - break; - case PageViewToolBar::Bottom: - newPos = QPoint( ( anchorWidget->width() - q->width()) / 2, anchorWidget->height() - q->height() ); - break; - } - return newPos + anchorWidget->pos(); -} - -QPoint ToolBarPrivate::getOuterPoint() const -{ - // returns the point from which the transition starts - QPoint newPos; - switch ( anchorSide ) - { - case PageViewToolBar::Left: - newPos = QPoint( -q->width(), ( anchorWidget->height() - q->height() ) / 2 ); - break; - case PageViewToolBar::Top: - newPos = QPoint( ( anchorWidget->width() - q->width() ) / 2, -q->height() ); - break; - case PageViewToolBar::Right: - newPos = QPoint( anchorWidget->width(), ( anchorWidget->height() - q->height() ) / 2 ); - break; - case PageViewToolBar::Bottom: - newPos = QPoint( ( anchorWidget->width() - q->width() ) / 2, anchorWidget->height() ); - break; - } - return newPos + anchorWidget->pos(); -} - -void PageViewToolBar::slotAnimate() -{ - // move currentPosition towards endPosition - int dX = d->endPosition.x() - d->currentPosition.x(), - dY = d->endPosition.y() - d->currentPosition.y(); - dX = dX / 6 + qMax( -1, qMin( 1, dX) ); - dY = dY / 6 + qMax( -1, qMin( 1, dY) ); - d->currentPosition.setX( d->currentPosition.x() + dX ); - d->currentPosition.setY( d->currentPosition.y() + dY ); - - // move the widget - move( d->currentPosition ); - - // handle arrival to the end - if ( d->currentPosition == d->endPosition ) - { - d->animTimer->stop(); - if ( d->hiding ) - { - d->visible = false; - deleteLater(); - } - else - { - d->visible = true; - } - } -} - -void PageViewToolBar::slotButtonClicked() -{ - ToolBarButton * button = qobject_cast( sender() ); - d->selectButton( button ); -} - -void ToolBarPrivate::selectButton( ToolBarButton * button ) -{ - if ( button ) - { - // deselect other buttons - QLinkedList< ToolBarButton * >::const_iterator it = buttons.begin(), end = buttons.end(); - for ( ; it != end; ++it ) - if ( *it != button ) - (*it)->setChecked( false ); - // emit signal (-1 if button has been unselected) - emit q->toolSelected( button->isChecked() ? button->buttonID() : -1 ); - } -} - -void PageViewToolBar::setToolsEnabled( bool on ) -{ - QLinkedList< ToolBarButton * >::const_iterator it = d->buttons.begin(), end = d->buttons.end(); - for ( ; it != end; ++it ) - (*it)->setEnabled( on ); -} - -void PageViewToolBar::setTextToolsEnabled( bool on ) -{ - QLinkedList< ToolBarButton * >::const_iterator it = d->buttons.begin(), end = d->buttons.end(); - for ( ; it != end; ++it ) - if ( (*it)->isText() ) - (*it)->setEnabled( on ); -} - #include "moc_pageviewutils.cpp" diff --git a/ui/presentationsearchbar.cpp b/ui/presentationsearchbar.cpp --- a/ui/presentationsearchbar.cpp +++ b/ui/presentationsearchbar.cpp @@ -55,7 +55,7 @@ setAutoFillBackground( true ); QHBoxLayout * lay = new QHBoxLayout( this ); - lay->setMargin( 0 ); + lay->setContentsMargins( 0, 0, 0, 0 ); m_handle = new HandleDrag( this ); lay->addWidget( m_handle ); diff --git a/ui/presentationwidget.h b/ui/presentationwidget.h --- a/ui/presentationwidget.h +++ b/ui/presentationwidget.h @@ -74,6 +74,9 @@ void leaveEvent( QEvent * e ) override; bool gestureEvent (QGestureEvent * e ); + // Catch TabletEnterProximity and TabletLeaveProximity events from the QApplication + bool eventFilter (QObject * o, QEvent * ev ) override; + private: const void * getObjectRect( Okular::ObjectRect::ObjectType type, int x, int y, QRect * geometry = nullptr ) const; const Okular::Action * getLink( int x, int y, QRect * geometry = nullptr ) const; diff --git a/ui/presentationwidget.cpp b/ui/presentationwidget.cpp --- a/ui/presentationwidget.cpp +++ b/ui/presentationwidget.cpp @@ -227,7 +227,7 @@ for ( int i = 0; i < screenCount; ++i ) { QAction *act = m_screenSelect->addAction( i18nc( "%1 is the screen number (0, 1, ...)", "Screen %1", i ) ); - act->setData( qVariantFromValue( i ) ); + act->setData( QVariant::fromValue( i ) ); } } QWidget *spacer = new QWidget( m_topBar ); @@ -283,6 +283,9 @@ // setFocus() so KCursor::setAutoHideCursor() goes into effect if it's enabled setFocus( Qt::OtherFocusReason ); + + // Catch TabletEnterProximity and TabletLeaveProximity events from the QApplication + qApp->installEventFilter( this ); } PresentationWidget::~PresentationWidget() @@ -312,6 +315,8 @@ // delete frames qDeleteAll( m_frames ); + + qApp->removeEventFilter( this ); } @@ -531,6 +536,30 @@ } } +bool PresentationWidget::eventFilter (QObject *o, QEvent *e ) +{ + if ( o == qApp ) + { + if ( e->type() == QTabletEvent::TabletEnterProximity ) + { + setCursor( QCursor( Qt::CrossCursor ) ); + } + else if ( e->type() == QTabletEvent::TabletLeaveProximity ) + { + setCursor( QCursor( Okular::Settings::slidesCursor() == Okular::Settings::EnumSlidesCursor::Hidden ? Qt::BlankCursor : Qt::ArrowCursor ) ); + if ( Okular::Settings::slidesCursor() == Okular::Settings::EnumSlidesCursor::HiddenDelay) { + // Trick KCursor to hide the cursor if needed by sending an "unknown" key press event + // Send also the key release to make the world happy even it's probably not needed + QKeyEvent kp( QEvent::KeyPress, 0, Qt::NoModifier ); + qApp->sendEvent( this, &kp ); + QKeyEvent kr( QEvent::KeyRelease, 0, Qt::NoModifier ); + qApp->sendEvent( this, &kr ); + } + } + } + return false; +} + // bool PresentationWidget::event( QEvent * e ) { @@ -625,7 +654,7 @@ return; // performance note: don't remove the clipping - int div = e->delta() / 120; + int div = e->angleDelta().y() / 120; if ( div > 0 ) { if ( div > 3 ) @@ -853,12 +882,9 @@ } // blit the pixmap to the screen - QVector allRects = pe->region().rects(); - uint numRects = allRects.count(); QPainter painter( this ); - for ( uint i = 0; i < numRects; i++ ) + for ( const QRect &r : pe->region() ) { - const QRect & r = allRects[i]; if ( !r.isValid() ) continue; #ifdef ENABLE_PROGRESS_OVERLAY @@ -1177,11 +1203,10 @@ p.translate( -frame->geometry.left(), -frame->geometry.top() ); // fill unpainted areas with background color - QRegion unpainted( QRect( 0, 0, m_width, m_height ) ); - QVector rects = unpainted.subtracted( frame->geometry ).rects(); - for ( int i = 0; i < rects.count(); i++ ) + const QRegion unpainted( QRect( 0, 0, m_width, m_height ) ); + const QRegion rgn = unpainted.subtracted( frame->geometry ); + for ( const QRect & r : rgn ) { - const QRect & r = rects[i]; p.fillRect( r, Okular::Settings::slidesBackgroundColor() ); } } @@ -1373,11 +1398,11 @@ // add drawing to current page m_frames[ m_frameIndex ]->drawings << m_drawingEngine->endSmoothPath(); - // manually disable and re-enable the pencil mode, so we can do - // cleaning of the actual drawer and create a new one just after + // remove the actual drawer and create a new one just after // that - that gives continuous drawing - slotChangeDrawingToolEngine( QDomElement() ); - slotChangeDrawingToolEngine( m_currentDrawingToolElement ); + delete m_drawingEngine; + m_drawingRect = QRect(); + m_drawingEngine = new SmoothPathEngine( m_currentDrawingToolElement ); // schedule repaint update(); diff --git a/ui/propertiesdialog.cpp b/ui/propertiesdialog.cpp --- a/ui/propertiesdialog.cpp +++ b/ui/propertiesdialog.cpp @@ -95,7 +95,7 @@ value = new QWidget( page ); /// place icon left of mime type's name QHBoxLayout *hboxLayout = new QHBoxLayout( value ); - hboxLayout->setMargin( 0 ); + hboxLayout->setContentsMargins( 0, 0, 0, 0 ); /// retrieve icon and place it in a QLabel QMimeDatabase db; QMimeType mimeType = db.mimeTypeForName( valueString ); @@ -355,7 +355,12 @@ { case 0: { - QString fontname = m_fonts.at( index.row() ).name(); + 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; } diff --git a/ui/searchlineedit.cpp b/ui/searchlineedit.cpp --- a/ui/searchlineedit.cpp +++ b/ui/searchlineedit.cpp @@ -281,7 +281,7 @@ : QWidget( parent ) { QHBoxLayout *layout = new QHBoxLayout( this ); - layout->setMargin( 0 ); + layout->setContentsMargins( 0, 0, 0, 0 ); m_edit = new SearchLineEdit( this, document ); layout->addWidget( m_edit ); diff --git a/ui/searchwidget.cpp b/ui/searchwidget.cpp --- a/ui/searchwidget.cpp +++ b/ui/searchwidget.cpp @@ -31,7 +31,7 @@ setSizePolicy( sp ); QHBoxLayout * mainlay = new QHBoxLayout( this ); - mainlay->setMargin( 0 ); + mainlay->setContentsMargins( 0, 0, 0, 0 ); mainlay->setSpacing( 3 ); // 2. text line diff --git a/ui/side_reviews.cpp b/ui/side_reviews.cpp --- a/ui/side_reviews.cpp +++ b/ui/side_reviews.cpp @@ -61,7 +61,7 @@ QTextDocument document; document.setHtml( i18n( "

    No annotations

    " - "To create new annotations press F6 or select Tools -> Review" + "To create new annotations press F6 or select Tools -> Annotations" " from the menu.
    " ) ); document.setTextWidth( width() - 50 ); @@ -89,7 +89,7 @@ { // create widgets and layout them vertically QVBoxLayout * vLayout = new QVBoxLayout( this ); - vLayout->setMargin( 0 ); + vLayout->setContentsMargins( 0, 0, 0, 0 ); vLayout->setSpacing( 6 ); m_view = new TreeView( m_document, this ); @@ -141,7 +141,7 @@ // - add separator toolBar->addSeparator(); // - add Current Page Only button - QAction * curPageOnlyAction = toolBar->addAction( QIcon::fromTheme( QStringLiteral("arrow-down") ), i18n( "Show reviews for current page only" ) ); + QAction * curPageOnlyAction = toolBar->addAction( QIcon::fromTheme( QStringLiteral("arrow-down") ), i18n( "Show annotations for current page only" ) ); curPageOnlyAction->setCheckable( true ); connect(curPageOnlyAction, &QAction::toggled, this, &Reviews::slotCurrentPageOnly); curPageOnlyAction->setChecked( Okular::Settings::currentPageOnly() ); diff --git a/ui/sidebar.cpp b/ui/sidebar.cpp --- a/ui/sidebar.cpp +++ b/ui/sidebar.cpp @@ -469,7 +469,7 @@ : QWidget( parent ), d( new Private ) { QHBoxLayout *mainlay = new QHBoxLayout( this ); - mainlay->setMargin( 0 ); + mainlay->setContentsMargins( 0, 0, 0, 0 ); mainlay->setSpacing( 0 ); setAutoFillBackground( true ); @@ -500,7 +500,7 @@ d->sideContainer->setMinimumWidth( 90 ); d->sideContainer->setMaximumWidth( 600 ); d->vlay = new QVBoxLayout( d->sideContainer ); - d->vlay->setMargin( 0 ); + d->vlay->setContentsMargins( 0, 0, 0, 0 ); d->sideTitle = new QLabel( d->sideContainer ); d->vlay->addWidget( d->sideTitle ); @@ -776,7 +776,7 @@ const int itssize = static_cast< int >( _itssize ); \ QAction *sizeAct = menu.addAction( text ); \ sizeAct->setCheckable( true ); \ - sizeAct->setData( qVariantFromValue( itssize ) ); \ + sizeAct->setData( QVariant::fromValue( itssize ) ); \ sizeAct->setChecked( itssize == curSize ); \ sizeGroup->addAction( sizeAct ); \ } diff --git a/ui/signaturepanel.cpp b/ui/signaturepanel.cpp --- a/ui/signaturepanel.cpp +++ b/ui/signaturepanel.cpp @@ -54,7 +54,7 @@ connect( d->m_view, &QTreeView::customContextMenuRequested, this, &SignaturePanel::slotShowContextMenu ); auto vLayout = new QVBoxLayout( this ); - vLayout->setMargin( 0 ); + vLayout->setContentsMargins( 0, 0, 0, 0 ); vLayout->setSpacing( 6 ); vLayout->addWidget( d->m_view ); } diff --git a/ui/thumbnaillist.cpp b/ui/thumbnaillist.cpp --- a/ui/thumbnaillist.cpp +++ b/ui/thumbnaillist.cpp @@ -892,7 +892,7 @@ if ( r.contains( e->pos() - QPoint( margin / 2, margin / 2 ) ) && e->orientation() == Qt::Vertical && e->modifiers() == Qt::ControlModifier ) { - m_document->setZoom( e->delta() ); + m_document->setZoom( e->angleDelta().y() ); } else { diff --git a/ui/toc.cpp b/ui/toc.cpp --- a/ui/toc.cpp +++ b/ui/toc.cpp @@ -30,7 +30,7 @@ TOC::TOC(QWidget *parent, Okular::Document *document) : QWidget(parent), m_document(document) { QVBoxLayout *mainlay = new QVBoxLayout( this ); - mainlay->setMargin( 0 ); + mainlay->setContentsMargins( 0, 0, 0, 0 ); mainlay->setSpacing( 6 ); m_searchLine = new KTreeViewSearchLine( this ); diff --git a/ui/toggleactionmenu.h b/ui/toggleactionmenu.h new file mode 100644 --- /dev/null +++ b/ui/toggleactionmenu.h @@ -0,0 +1,135 @@ +/*************************************************************************** + * 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. * + ***************************************************************************/ + +#ifndef TOGGLEACTIONMENU_H +#define TOGGLEACTIONMENU_H + +#include +#include +#include + +/** + * @brief A KActionMenu, with allows to set the default action of its toolbar buttons. + * + * Usually, a KActionMenu creates toolbar buttons which reflect its own action properties + * (icon, text, tooltip, checked state,...), as it is a QAction itself. + * ToggleActionMenu will use its own action properties only when plugged as submenu in another menu. + * But the default action of the toolbar buttons can easily be changed with the slot setDefaultAction(). + * + * Naming: The user can *Toggle* the checked state of an *Action* by directly clicking the toolbar button, + * but can also open a *Menu*. + * + * @par Intention + * Setting the default action of the toolbar button can be useful for: + * * Providing the most propably needed entry of a menu directly on the menu button. + * * Showing the last used menu entry on the menu button, including its checked state. + * The advantage is that the user often does not need to open the menu, + * and that the toolbar button shows additional information + * like checked state or the user's last selection. + * + * This shall replace the former ToolAction in Okular, + * while beeing flexible enough for other (planned) action menus. + */ +class ToggleActionMenu : public KActionMenu +{ + Q_OBJECT + +public: + /** + * Defines how the menu behaves. + */ + enum MenuLogic { + DefaultLogic = 0x0, + /** + * Automatically makes the triggered action the default action, even if in a submenu. + * When a toolbar button is constructed, + * the default action is set to the default action set with setDefaultAction() before, + * otherwise to the first checked action in the menu, + * otherwise to the action suggested with suggestDefaultAction(). + */ + ImplicitDefaultAction = 0x1 + }; + + explicit ToggleActionMenu( QObject *parent ); + ToggleActionMenu( const QString &text, QObject * parent ); + /** + * These are the usual constructors for KActionMenu. + * + * @note + * @p text and @p icon are used only if this menu is a submenu in another menu. + * To set the appearance of the toolbar buttons, use setDefaultAction. + */ + ToggleActionMenu( const QIcon &icon, + const QString &text, + QObject *parent, + QToolButton::ToolButtonPopupMode popupMode = QToolButton::MenuButtonPopup, + MenuLogic logic = DefaultLogic + ); + + QWidget *createWidget( QWidget *parent ) override; + + /** + * Returns the current default action of the toolbar buttons. + * + * In ImplicitDefaultAction mode, + * when the default action was not yet set with setDefaultAction(), + * it will determine it from the first checked action in the menu, + * otherwise from the action set with suggestDefaultAction(). + */ + QAction *defaultAction(); + + /** + * Suggests a default action to be used as fallback. + * + * It will be used if the default action is not determined another way. + */ + void suggestDefaultAction( QAction * action ); + +public slots: + /** + * Sets the default action of the toolbar buttons. + * + * This action will be triggered by clicking directly on the toolbar buttons. + * It will also set the text, icon, checked state, etc. of the toolbar buttons. + * + * @note + * The default action will not set the enabled state or popup mode of the menu buttons. + * These properties are still set by the corresponding properties of this ToggleActionMenu. + * + * @note + * The action will not be added to the menu, + * it usually makes sense to addAction() it before to setDefaultAction() it. + */ + void setDefaultAction( QAction *action ); + +private: + QAction *m_defaultAction; + QAction *m_suggestedDefaultAction; + QList< QPointer< QToolButton > > m_buttons; + MenuLogic m_menuLogic; + + /** + * Returns the first checked action in menu(), + * or nullptr if no action is checked. + */ + QAction *checkedAction() const; + +private slots: + /** + * Updates the toolbar buttons, using both the default action and properties of this menu itself. + * + * This ensures that the toolbar buttons reflect e. g. a disabled state of this menu. + */ + void updateButtons(); +}; + +#endif // TOGGLEACTIONMENU_H diff --git a/ui/toggleactionmenu.cpp b/ui/toggleactionmenu.cpp new file mode 100644 --- /dev/null +++ b/ui/toggleactionmenu.cpp @@ -0,0 +1,147 @@ +/*************************************************************************** + * 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, + QToolButton::ToolButtonPopupMode 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 == QToolButton::DelayedPopup ) + { + setDelayed( true ); + } + else if ( popupMode == QToolButton::InstantPopup ) + { + setDelayed( false ); + setStickyMenu( 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() const +{ + for ( QAction * a : menu()->actions() ) + { + if ( a->isChecked() ) + { + return a; + } + } + return nullptr; +} + +void ToggleActionMenu::updateButtons() +{ + for ( 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(); + } + if ( !m_defaultAction ) + { + m_defaultAction = m_suggestedDefaultAction; + } + return m_defaultAction; +} diff --git a/ui/toolaction.h b/ui/toolaction.h deleted file mode 100644 --- a/ui/toolaction.h +++ /dev/null @@ -1,41 +0,0 @@ -/*************************************************************************** - * 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. * - ***************************************************************************/ - -#ifndef TOOLACTION_H -#define TOOLACTION_H - -#include -#include - -#include - -class QToolButton; - -class ToolAction : public KSelectAction -{ - Q_OBJECT - - public: - explicit ToolAction( QObject *parent = nullptr ); - virtual ~ToolAction(); - - void addAction( QAction *action ); - - protected: - QWidget* createWidget( QWidget *parent ) override; - - private Q_SLOTS: - void slotNewDefaultAction( QAction *action ); - - private: - QList< QPointer< QToolButton > > m_buttons; - QList< QAction * > m_actions; -}; - -#endif diff --git a/ui/toolaction.cpp b/ui/toolaction.cpp deleted file mode 100644 --- a/ui/toolaction.cpp +++ /dev/null @@ -1,87 +0,0 @@ -/*************************************************************************** - * 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 "toolaction.h" - -#include -#include -#include - -#include - -ToolAction::ToolAction( QObject *parent ) - : KSelectAction( parent ) -{ - setText( i18n( "Selection Tools" ) ); -} - -ToolAction::~ToolAction() -{ -} - -void ToolAction::addAction( QAction *action ) -{ - bool setDefault = !m_buttons.isEmpty() ? m_buttons.first()->menu()->actions().isEmpty() : false; - foreach ( QToolButton *button, m_buttons ) - if ( button ) - { - button->menu()->addAction( action ); - if ( setDefault ) - button->setDefaultAction( action ); - } - m_actions.append( action ); -} - -QWidget* ToolAction::createWidget( QWidget *parent ) -{ - QToolBar *toolBar = qobject_cast< QToolBar * >( parent ); - if ( !toolBar ) - return nullptr; - - QToolButton *button = new QToolButton( toolBar ); - button->setAutoRaise( true ); - button->setFocusPolicy( Qt::NoFocus ); - button->setIconSize( toolBar->iconSize() ); - button->setToolButtonStyle( toolBar->toolButtonStyle() ); - button->setPopupMode( QToolButton::MenuButtonPopup ); - button->setMenu( new QMenu( button ) ); - button->setCheckable( true ); - connect(toolBar, &QToolBar::iconSizeChanged, button, &QToolButton::setIconSize); - connect(toolBar, &QToolBar::toolButtonStyleChanged, button, &QToolButton::setToolButtonStyle); - connect(button, &QToolButton::triggered, toolBar, &QToolBar::actionTriggered); - connect( button->menu(), &QMenu::triggered, this, &ToolAction::slotNewDefaultAction ); - - m_buttons.append( button ); - - if ( !m_actions.isEmpty() ) - { - button->setDefaultAction( m_actions.first() ); - foreach ( QAction *action, m_actions ) - { - button->menu()->addAction( action ); - if ( action->isChecked() ) - button->setDefaultAction( action ); - } - button->setToolTip( i18n("Click to use the current selection tool\nClick on the arrow to choose another selection tool") ); - } - - return button; -} - -void ToolAction::slotNewDefaultAction( QAction *action ) -{ - foreach ( QToolButton *button, m_buttons ) - if ( button ) - { - button->setDefaultAction( action ); - button->setToolTip( i18n("Click to use the current selection tool\nClick on the arrow to choose another selection tool") ); - } -} - -#include "moc_toolaction.cpp" diff --git a/ui/tts.h b/ui/tts.h --- a/ui/tts.h +++ b/ui/tts.h @@ -22,12 +22,15 @@ void say( const QString &text ); void stopAllSpeechs(); + void pauseResumeSpeech(); public slots: void slotSpeechStateChanged(QTextToSpeech::State state); + void slotConfigChanged(); signals: void isSpeaking( bool speaking ); + void canPauseOrResume( bool speakingOrPaused ); private: // private storage diff --git a/ui/tts.cpp b/ui/tts.cpp --- a/ui/tts.cpp +++ b/ui/tts.cpp @@ -14,12 +14,14 @@ #include +#include "settings.h" + /* Private storage. */ class OkularTTS::Private { public: Private( OkularTTS *qq ) - : q( qq ), speech( new QTextToSpeech ) + : q( qq ), speech( new QTextToSpeech( Okular::Settings::ttsEngine() ) ) { } @@ -31,12 +33,19 @@ OkularTTS *q; QTextToSpeech *speech; + // Which speech engine was used when above object was created. + // When the setting changes, we need to stop speaking and recreate. + QString speechEngine; }; OkularTTS::OkularTTS( QObject *parent ) : QObject( parent ), d( new Private( this ) ) { + // Initialize speechEngine so we can reinitialize if it changes. + d->speechEngine = Okular::Settings::ttsEngine(); connect( d->speech, &QTextToSpeech::stateChanged, this, &OkularTTS::slotSpeechStateChanged); + connect( Okular::Settings::self(), &KConfigSkeleton::configChanged, + this, &OkularTTS::slotConfigChanged); } OkularTTS::~OkularTTS() @@ -60,12 +69,45 @@ d->speech->stop(); } +void OkularTTS::pauseResumeSpeech() +{ + if ( !d->speech ) + return; + + if ( d->speech->state() == QTextToSpeech::Speaking ) + d->speech->pause(); + else + d->speech->resume(); +} + void OkularTTS::slotSpeechStateChanged(QTextToSpeech::State state) { if (state == QTextToSpeech::Speaking) + { emit isSpeaking(true); + emit canPauseOrResume(true); + } else + { emit isSpeaking(false); + if (state == QTextToSpeech::Paused) + emit canPauseOrResume(true); + else + emit canPauseOrResume(false); + } +} + +void OkularTTS::slotConfigChanged() +{ + const QString engine = Okular::Settings::ttsEngine(); + if (engine != d->speechEngine) + { + d->speech->stop(); + delete d->speech; + d->speech = new QTextToSpeech(engine); + connect( d->speech, &QTextToSpeech::stateChanged, this, &OkularTTS::slotSpeechStateChanged); + d->speechEngine = engine; + } } #include "moc_tts.cpp" diff --git a/ui/videowidget.cpp b/ui/videowidget.cpp --- a/ui/videowidget.cpp +++ b/ui/videowidget.cpp @@ -48,7 +48,7 @@ QWidget *dummy = new QWidget( menu ); widgetAction->setDefaultWidget( dummy ); QVBoxLayout *dummyLayout = new QVBoxLayout( dummy ); - dummyLayout->setMargin( 5 ); + dummyLayout->setContentsMargins( 5, 5, 5, 5 ); dummyLayout->addWidget( widget ); menu->addAction( widgetAction ); return action; @@ -235,7 +235,7 @@ QWidget *playerPage = new QWidget( this ); QVBoxLayout *mainlay = new QVBoxLayout( playerPage ); - mainlay->setMargin( 0 ); + mainlay->setContentsMargins( 0, 0, 0, 0 ); mainlay->setSpacing( 0 ); d->player = new Phonon::VideoPlayer( Phonon::NoCategory, playerPage ); @@ -403,7 +403,7 @@ QWheelEvent * we = static_cast< QWheelEvent * >( event ); // forward wheel events to parent widget - QWheelEvent *copy = new QWheelEvent( we->pos(), we->globalPos(), we->delta(), we->buttons(), we->modifiers(), we->orientation() ); + QWheelEvent *copy = new QWheelEvent( we->pos(), we->globalPos(), we->angleDelta().y(), we->buttons(), we->modifiers(), we->orientation() ); QCoreApplication::postEvent( parentWidget(), copy ); } break;