diff --git a/autotests/data/file1-docdata.xml b/autotests/data/file1-docdata.xml
new file mode 100644
--- /dev/null
+++ b/autotests/data/file1-docdata.xml
@@ -0,0 +1,427 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/autotests/data/potato.jpg b/autotests/data/potato.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
GIT binary patch
literal 0
Hc$@
+#include "../core/annotations.h"
#include "../core/document.h"
+#include "../core/document_p.h"
#include "../core/generator.h"
#include "../core/observer.h"
+#include "../core/page.h"
#include "../core/rotationjob_p.h"
#include "../settings_core.h"
@@ -24,6 +27,7 @@
private slots:
void testCloseDuringRotationJob();
+ void testDocdataMigration();
};
// Test that we don't crash if the document is closed while a RotationJob
@@ -57,6 +61,59 @@
ThreadWeaver::Queue::instance()->resume();
ThreadWeaver::Queue::instance()->finish();
qApp->processEvents();
+
+ delete dummyDocumentObserver;
+}
+
+// Test that, if there's a XML file in docdata referring to a document, we
+// detect that it must be migrated, that it doesn't get wiped out if you close
+// the document without migrating and that it does get wiped out after migrating
+void DocumentTest::testDocdataMigration()
+{
+ Okular::SettingsCore::instance( "documenttest" );
+
+ const QUrl testFileUrl = QUrl::fromLocalFile(KDESRCDIR "data/file1.pdf");
+ const QString testFilePath = testFileUrl.toLocalFile();
+ const qint64 testFileSize = QFileInfo(testFilePath).size();
+
+ // Copy XML file to the docdata/ directory
+ const QString docDataPath = Okular::DocumentPrivate::docDataFileName(testFileUrl, testFileSize);
+ QFile::remove(docDataPath);
+ QVERIFY( QFile::copy(KDESRCDIR "data/file1-docdata.xml", docDataPath) );
+
+ // Open our document
+ Okular::Document *m_document = new Okular::Document( 0 );
+ QMimeDatabase db;
+ const QMimeType mime = db.mimeTypeForFile( testFilePath );
+ QCOMPARE( m_document->openDocument( testFilePath, testFileUrl, mime ), Okular::Document::OpenSuccess );
+
+ // Check that the annotation from file1-docdata.xml was loaded
+ QCOMPARE( m_document->page( 0 )->annotations().size(), 1 );
+ QCOMPARE( m_document->page( 0 )->annotations().first()->uniqueName(), QString("testannot") );
+
+ // Check that we detect that it must be migrated
+ QCOMPARE( m_document->isDocdataMigrationNeeded(), true );
+ m_document->closeDocument();
+
+ // Reopen the document and check that the annotation is still present
+ // (because we have not migrated)
+ QCOMPARE( m_document->openDocument( testFilePath, testFileUrl, mime ), Okular::Document::OpenSuccess );
+ QCOMPARE( m_document->page( 0 )->annotations().size(), 1 );
+ QCOMPARE( m_document->page( 0 )->annotations().first()->uniqueName(), QString("testannot") );
+ QCOMPARE( m_document->isDocdataMigrationNeeded(), true );
+
+ // Pretend the user has done the migration
+ m_document->docdataMigrationDone();
+ QCOMPARE( m_document->isDocdataMigrationNeeded(), false );
+ m_document->closeDocument();
+
+ // Now the docdata file should have no annotations, let's check
+ QCOMPARE( m_document->openDocument( testFilePath, testFileUrl, mime ), Okular::Document::OpenSuccess );
+ QCOMPARE( m_document->page( 0 )->annotations().size(), 0 );
+ QCOMPARE( m_document->isDocdataMigrationNeeded(), false );
+ m_document->closeDocument();
+
+ delete m_document;
}
QTEST_MAIN( DocumentTest )
diff --git a/autotests/parttest.cpp b/autotests/parttest.cpp
--- a/autotests/parttest.cpp
+++ b/autotests/parttest.cpp
@@ -9,6 +9,8 @@
#include
+#include "../core/annotations.h"
+#include "../core/form.h"
#include "../core/page.h"
#include "../part.h"
#include "../ui/toc.h"
@@ -17,6 +19,8 @@
#include
#include
+#include
+#include
#include
#include
#include
@@ -24,6 +28,40 @@
#include
#include
+class CloseDialogHelper : public QObject
+{
+ Q_OBJECT
+
+public:
+ CloseDialogHelper(Okular::Part *p, QDialogButtonBox::StandardButton b) : m_part(p), m_button(b), m_clicked(false)
+ {
+ QTimer::singleShot(0, this, &CloseDialogHelper::closeDialog);
+ }
+
+ ~CloseDialogHelper()
+ {
+ QVERIFY(m_clicked);
+ }
+
+private slots:
+ void closeDialog()
+ {
+ QDialog *dialog = m_part->widget()->findChild();
+ if (!dialog) {
+ QTimer::singleShot(0, this, &CloseDialogHelper::closeDialog);
+ return;
+ }
+ QDialogButtonBox *buttonBox = dialog->findChild();
+ buttonBox->button(m_button)->click();
+ m_clicked = true;
+ }
+
+private:
+ Okular::Part *m_part;
+ QDialogButtonBox::StandardButton m_button;
+ bool m_clicked;
+};
+
namespace Okular
{
class PartTest
@@ -45,6 +83,12 @@
void testGeneratorPreferences();
void testSelectText();
void testClickInternalLink();
+ void testSaveAs();
+ void testSaveAs_data();
+ void testSaveAsUndoStackAnnotations();
+ void testSaveAsUndoStackAnnotations_data();
+ void testSaveAsUndoStackForms();
+ void testSaveAsUndoStackForms_data();
void testMouseMoveOverLinkWhileInSelectionMode();
void testClickUrlLinkWhileInSelectionMode();
void testeTextSelectionOverAndAcrossLinks_data();
@@ -725,6 +769,412 @@
events.simulate(target);
}
+void PartTest::testSaveAs()
+{
+ QFETCH(QString, file);
+ QFETCH(QString, extension);
+ QFETCH(bool, nativelySupportsAnnotations);
+ QFETCH(bool, canSwapBackingFile);
+
+ QScopedPointer closeDialogHelper;
+
+ QString annotName;
+ QTemporaryFile archiveSave( QString( "%1/okrXXXXXX.okular" ).arg( QDir::tempPath() ) );
+ QTemporaryFile nativeDirectSave( QString( "%1/okrXXXXXX.%2" ).arg( QDir::tempPath() ).arg ( extension ) );
+ QTemporaryFile nativeFromArchiveFile( QString( "%1/okrXXXXXX.%2" ).arg( QDir::tempPath() ).arg ( extension ) );
+ QVERIFY( archiveSave.open() );
+ archiveSave.close();
+ QVERIFY( nativeDirectSave.open() );
+ nativeDirectSave.close();
+ QVERIFY( nativeFromArchiveFile.open() );
+ nativeFromArchiveFile.close();
+
+ qDebug() << "Open file, add annotation and save both natively and to .okular";
+ {
+ Okular::Part part(nullptr, nullptr, QVariantList());
+ part.openDocument( file );
+
+ QCOMPARE(part.m_document->canSwapBackingFile(), canSwapBackingFile);
+
+ Okular::Annotation *annot = new Okular::TextAnnotation();
+ annot->setBoundingRectangle( Okular::NormalizedRect( 0.1, 0.1, 0.15, 0.15 ) );
+ annot->setContents( "annot contents" );
+ part.m_document->addPageAnnotation( 0, annot );
+ annotName = annot->uniqueName();
+
+ if ( canSwapBackingFile )
+ {
+ if ( !nativelySupportsAnnotations )
+ {
+ closeDialogHelper.reset(new CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog
+ }
+ QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), Part::NoSaveAsFlags ) );
+ // For backends that don't support annotations natively we mark the part as still modified
+ // after a save because we keep the annotation around but it will get lost if the user closes the app
+ // so we want to give her a last chance to save on close with the "you have changes dialog"
+ QCOMPARE( part.isModified(), !nativelySupportsAnnotations );
+ QVERIFY( part.saveAs( QUrl::fromLocalFile( archiveSave.fileName() ), Part::SaveAsOkularArchive ) );
+ }
+ else
+ {
+ // We need to save to archive first otherwise we lose the annotation
+
+ closeDialogHelper.reset(new CloseDialogHelper( &part, QDialogButtonBox::Yes )); // this is the "you're going to lose the undo/redo stack" dialog
+ QVERIFY( part.saveAs( QUrl::fromLocalFile( archiveSave.fileName() ), Part::SaveAsOkularArchive ) );
+
+ if ( !nativelySupportsAnnotations )
+ {
+ closeDialogHelper.reset(new CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog
+ }
+ QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), Part::NoSaveAsFlags ) );
+ }
+
+ part.closeUrl();
+ }
+
+ qDebug() << "Open the .okular, check that the annotation is present and save to native";
+ {
+ Okular::Part part(nullptr, nullptr, QVariantList());
+ part.openDocument( archiveSave.fileName() );
+
+ QCOMPARE( part.m_document->page( 0 )->annotations().size(), 1 );
+ QCOMPARE( part.m_document->page( 0 )->annotations().first()->uniqueName(), annotName );
+
+ if ( !nativelySupportsAnnotations )
+ {
+ closeDialogHelper.reset(new CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog
+ }
+ QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeFromArchiveFile.fileName() ), Part::NoSaveAsFlags ) );
+
+ if ( canSwapBackingFile && !nativelySupportsAnnotations )
+ {
+ // For backends that don't support annotations natively we mark the part as still modified
+ // after a save because we keep the annotation around but it will get lost if the user closes the app
+ // so we want to give her a last chance to save on close with the "you have changes dialog"
+ closeDialogHelper.reset(new CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "do you want to save or discard" dialog
+ }
+
+ part.closeUrl();
+ }
+
+ qDebug() << "Open the native file saved directly, and check that the annot"
+ << "is there iff we expect it";
+ {
+ Okular::Part part(nullptr, nullptr, QVariantList());
+ part.openDocument( nativeDirectSave.fileName() );
+
+ QCOMPARE( part.m_document->page( 0 )->annotations().size(), nativelySupportsAnnotations ? 1 : 0 );
+ if ( nativelySupportsAnnotations )
+ QCOMPARE( part.m_document->page( 0 )->annotations().first()->uniqueName(), annotName );
+
+ part.closeUrl();
+ }
+
+ qDebug() << "Open the native file saved from the .okular, and check that the annot"
+ << "is there iff we expect it";
+ {
+ Okular::Part part(nullptr, nullptr, QVariantList());
+ part.openDocument( nativeFromArchiveFile.fileName() );
+
+ QCOMPARE( part.m_document->page( 0 )->annotations().size(), nativelySupportsAnnotations ? 1 : 0 );
+ if ( nativelySupportsAnnotations )
+ QCOMPARE( part.m_document->page( 0 )->annotations().first()->uniqueName(), annotName );
+
+ part.closeUrl();
+ }
+}
+
+void PartTest::testSaveAs_data()
+{
+ QTest::addColumn("file");
+ QTest::addColumn("extension");
+ QTest::addColumn("nativelySupportsAnnotations");
+ QTest::addColumn("canSwapBackingFile");
+
+ QTest::newRow("pdf") << KDESRCDIR "data/file1.pdf" << "pdf" << true << true;
+ QTest::newRow("epub") << KDESRCDIR "data/contents.epub" << "epub" << false << false;
+ QTest::newRow("jpg") << KDESRCDIR "data/potato.jpg" << "jpg" << false << true;
+}
+
+void PartTest::testSaveAsUndoStackAnnotations()
+{
+ QFETCH(QString, file);
+ QFETCH(QString, extension);
+ QFETCH(bool, nativelySupportsAnnotations);
+ QFETCH(bool, canSwapBackingFile);
+ QFETCH(bool, saveToArchive);
+
+ const Part::SaveAsFlag saveFlags = saveToArchive ? Part::SaveAsOkularArchive : Part::NoSaveAsFlags;
+
+ QScopedPointer closeDialogHelper;
+
+ QTemporaryFile saveFile( QString( "%1/okrXXXXXX.%2" ).arg( QDir::tempPath() ).arg ( extension ) );
+ QVERIFY( saveFile.open() );
+ saveFile.close();
+
+ Okular::Part part(nullptr, nullptr, QVariantList());
+ part.openDocument( file );
+
+ QCOMPARE(part.m_document->canSwapBackingFile(), canSwapBackingFile);
+
+ Okular::Annotation *annot = new Okular::TextAnnotation();
+ annot->setBoundingRectangle( Okular::NormalizedRect( 0.1, 0.1, 0.15, 0.15 ) );
+ annot->setContents( "annot contents" );
+ part.m_document->addPageAnnotation( 0, annot );
+ QString annotName = annot->uniqueName();
+
+ if ( !nativelySupportsAnnotations && !saveToArchive ) {
+ closeDialogHelper.reset(new CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog
+ }
+
+ QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) );
+
+ if (!canSwapBackingFile) {
+ // The undo/redo stack gets lost if you can not swap the backing file
+ QVERIFY( !part.m_document->canUndo() );
+ QVERIFY( !part.m_document->canRedo() );
+ return;
+ }
+
+ // Check we can still undo the annot add after save
+ QVERIFY( part.m_document->canUndo() );
+ part.m_document->undo();
+ QVERIFY( !part.m_document->canUndo() );
+
+ QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) );
+ QVERIFY( part.m_document->page( 0 )->annotations().isEmpty() );
+
+ // Check we can redo the annot add after save
+ QVERIFY( part.m_document->canRedo() );
+ part.m_document->redo();
+ QVERIFY( !part.m_document->canRedo() );
+
+ if ( nativelySupportsAnnotations ) {
+ // If the annots are provived by the backend we need to refetch the pointer after save
+ annot = part.m_document->page( 0 )->annotation( annotName );
+ QVERIFY( annot );
+ }
+
+
+
+ // Remove the annotation, creates another undo command
+ QVERIFY( part.m_document->canRemovePageAnnotation( annot ) );
+ part.m_document->removePageAnnotation( 0, annot );
+ QVERIFY( part.m_document->page( 0 )->annotations().isEmpty() );
+
+ // Check we can still undo the annot remove after save
+ QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) );
+ QVERIFY( part.m_document->canUndo() );
+ part.m_document->undo();
+ QVERIFY( part.m_document->canUndo() );
+ QCOMPARE( part.m_document->page( 0 )->annotations().count(), 1 );
+
+ // Check we can still undo the annot add after save
+ if ( !nativelySupportsAnnotations && !saveToArchive ) {
+ closeDialogHelper.reset(new CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog
+ }
+ QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) );
+ QVERIFY( part.m_document->canUndo() );
+ part.m_document->undo();
+ QVERIFY( !part.m_document->canUndo() );
+ QVERIFY( part.m_document->page( 0 )->annotations().isEmpty() );
+
+
+ // Redo the add annotation
+ QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) );
+ QVERIFY( part.m_document->canRedo() );
+ part.m_document->redo();
+ QVERIFY( part.m_document->canUndo() );
+ QVERIFY( part.m_document->canRedo() );
+
+ if ( nativelySupportsAnnotations ) {
+ // If the annots are provived by the backend we need to refetch the pointer after save
+ annot = part.m_document->page( 0 )->annotation( annotName );
+ QVERIFY( annot );
+ }
+
+
+ // Add translate, adjust and modify commands
+ part.m_document->translatePageAnnotation( 0, annot, Okular::NormalizedPoint( 0.1, 0.1 ) );
+ part.m_document->adjustPageAnnotation( 0, annot, Okular::NormalizedPoint( 0.1, 0.1 ), Okular::NormalizedPoint( 0.1, 0.1 ) );
+ part.m_document->prepareToModifyAnnotationProperties( annot );
+ part.m_document->modifyPageAnnotationProperties( 0, annot );
+
+ // Now check we can still undo/redo/save at all the intermediate states and things still work
+ if ( !nativelySupportsAnnotations && !saveToArchive ) {
+ closeDialogHelper.reset(new CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog
+ }
+ QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) );
+ QVERIFY( part.m_document->canUndo() );
+ part.m_document->undo();
+ QVERIFY( part.m_document->canUndo() );
+
+ if ( !nativelySupportsAnnotations && !saveToArchive ) {
+ closeDialogHelper.reset(new CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog
+ }
+ QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) );
+ QVERIFY( part.m_document->canUndo() );
+ part.m_document->undo();
+ QVERIFY( part.m_document->canUndo() );
+
+ if ( !nativelySupportsAnnotations && !saveToArchive ) {
+ closeDialogHelper.reset(new CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog
+ }
+ QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) );
+ QVERIFY( part.m_document->canUndo() );
+ part.m_document->undo();
+ QVERIFY( part.m_document->canUndo() );
+
+ if ( !nativelySupportsAnnotations && !saveToArchive ) {
+ closeDialogHelper.reset(new CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog
+ }
+ QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) );
+ QVERIFY( part.m_document->canUndo() );
+ part.m_document->undo();
+ QVERIFY( !part.m_document->canUndo() );
+ QVERIFY( part.m_document->canRedo() );
+ QVERIFY( part.m_document->page( 0 )->annotations().isEmpty() );
+
+ QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) );
+ QVERIFY( part.m_document->canRedo() );
+ part.m_document->redo();
+ QVERIFY( part.m_document->canRedo() );
+
+ if ( !nativelySupportsAnnotations && !saveToArchive ) {
+ closeDialogHelper.reset(new CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog
+ }
+ QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) );
+ QVERIFY( part.m_document->canRedo() );
+ part.m_document->redo();
+ QVERIFY( part.m_document->canRedo() );
+
+ if ( !nativelySupportsAnnotations && !saveToArchive ) {
+ closeDialogHelper.reset(new CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog
+ }
+ QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) );
+ QVERIFY( part.m_document->canRedo() );
+ part.m_document->redo();
+ QVERIFY( part.m_document->canRedo() );
+
+ if ( !nativelySupportsAnnotations && !saveToArchive ) {
+ closeDialogHelper.reset(new CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog
+ }
+ QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) );
+ QVERIFY( part.m_document->canRedo() );
+ part.m_document->redo();
+ QVERIFY( !part.m_document->canRedo() );
+
+ closeDialogHelper.reset(new CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "do you want to save or discard" dialog
+ part.closeUrl();
+}
+
+void PartTest::testSaveAsUndoStackAnnotations_data()
+{
+ QTest::addColumn("file");
+ QTest::addColumn("extension");
+ QTest::addColumn("nativelySupportsAnnotations");
+ QTest::addColumn("canSwapBackingFile");
+ QTest::addColumn("saveToArchive");
+
+ QTest::newRow("pdf") << KDESRCDIR "data/file1.pdf" << "pdf" << true << true << false;
+ QTest::newRow("epub") << KDESRCDIR "data/contents.epub" << "epub" << false << false << false;
+ QTest::newRow("jpg") << KDESRCDIR "data/potato.jpg" << "jpg" << false << true << false;
+ QTest::newRow("pdfarchive") << KDESRCDIR "data/file1.pdf" << "okular" << true << true << true;
+ QTest::newRow("jpgarchive") << KDESRCDIR "data/potato.jpg" << "okular" << false << true << true;
+}
+
+void PartTest::testSaveAsUndoStackForms()
+{
+ QFETCH(QString, file);
+ QFETCH(QString, extension);
+ QFETCH(bool, saveToArchive);
+
+ const Part::SaveAsFlag saveFlags = saveToArchive ? Part::SaveAsOkularArchive : Part::NoSaveAsFlags;
+
+ QTemporaryFile saveFile( QString( "%1/okrXXXXXX.%2" ).arg( QDir::tempPath(), extension ) );
+ QVERIFY( saveFile.open() );
+ saveFile.close();
+
+ Okular::Part part(nullptr, nullptr, QVariantList());
+ part.openDocument( file );
+
+ for ( FormField *ff : part.m_document->page( 0 )->formFields() )
+ {
+ if ( ff->id() == 65537 )
+ {
+ QCOMPARE( ff->type(), FormField::FormText );
+ FormFieldText *fft = static_cast( ff );
+ part.m_document->editFormText( 0, fft, "BlaBla", 6, 0, 0 );
+ }
+ else if ( ff->id() == 65538 )
+ {
+ QCOMPARE( ff->type(), FormField::FormButton );
+ FormFieldButton *ffb = static_cast( ff );
+ QCOMPARE( ffb->buttonType(), FormFieldButton::Radio );
+ part.m_document->editFormButtons( 0, QList< FormFieldButton* >() << ffb, QList< bool >() << true );
+ }
+ else if ( ff->id() == 65542 )
+ {
+ QCOMPARE( ff->type(), FormField::FormChoice );
+ FormFieldChoice *ffc = static_cast( ff );
+ QCOMPARE( ffc->choiceType(), FormFieldChoice::ListBox );
+ part.m_document->editFormList( 0, ffc, QList< int >() << 1 );
+ }
+ else if ( ff->id() == 65543 )
+ {
+ QCOMPARE( ff->type(), FormField::FormChoice );
+ FormFieldChoice *ffc = static_cast( ff );
+ QCOMPARE( ffc->choiceType(), FormFieldChoice::ComboBox );
+ part.m_document->editFormCombo( 0, ffc, "combo2", 3, 0, 0);
+ }
+ }
+
+ QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) );
+
+ QVERIFY( part.m_document->canUndo() );
+ part.m_document->undo();
+ QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) );
+
+ QVERIFY( part.m_document->canUndo() );
+ part.m_document->undo();
+ QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) );
+
+ QVERIFY( part.m_document->canUndo() );
+ part.m_document->undo();
+ QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) );
+
+ QVERIFY( part.m_document->canUndo() );
+ part.m_document->undo();
+ QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) );
+ QVERIFY( !part.m_document->canUndo() );
+
+ QVERIFY( part.m_document->canRedo() );
+ part.m_document->redo();
+ QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) );
+
+ QVERIFY( part.m_document->canRedo() );
+ part.m_document->redo();
+ QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) );
+
+ QVERIFY( part.m_document->canRedo() );
+ part.m_document->redo();
+ QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) );
+
+ QVERIFY( part.m_document->canRedo() );
+ part.m_document->redo();
+ QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) );
+}
+
+void PartTest::testSaveAsUndoStackForms_data()
+{
+ QTest::addColumn("file");
+ QTest::addColumn("extension");
+ QTest::addColumn("saveToArchive");
+
+ QTest::newRow("pdf") << KDESRCDIR "data/formSamples.pdf" << "pdf" << false;
+ QTest::newRow("pdfarchive") << KDESRCDIR "data/formSamples.pdf" << "okular" << true;
+}
+
}
int main(int argc, char *argv[])
diff --git a/core/document.h b/core/document.h
--- a/core/document.h
+++ b/core/document.h
@@ -737,6 +737,48 @@
*/
KPluginMetaData generatorInfo() const;
+ /**
+ * Returns whether the generator supports hot-swapping the current file
+ * with another identical file
+ *
+ * @since 1.3
+ */
+ bool canSwapBackingFile() const;
+
+ /**
+ * Reload the document from a new location, without any visible effect
+ * to the user.
+ *
+ * The new file must be identical to the current one or, if the document
+ * has been modified (eg the user edited forms and annotations), the new
+ * document must have these changes too. For example, you can call
+ * saveChanges first to write changes to a file and then swapBackingFile
+ * to switch to the new location.
+ *
+ * @since 1.3
+ */
+ bool swapBackingFile( const QString &newFileName, const QUrl &url );
+
+ /**
+ * Same as swapBackingFile, but newFileName must be a .okular file.
+ *
+ * The new file must be identical to the current one or, if the document
+ * has been modified (eg the user edited forms and annotations), the new
+ * document must have these changes too. For example, you can call
+ * saveDocumentArchive first to write changes to a file and then
+ * swapBackingFileArchive to switch to the new location.
+ *
+ * @since 1.3
+ */
+ bool swapBackingFileArchive( const QString &newFileName, const QUrl &url );
+
+ /**
+ * Sets the history to be clean
+ *
+ * @since 1.3
+ */
+ void setHistoryClean( bool clean );
+
/**
* Saving capabilities. Their availability varies according to the
* underlying generator and/or the document type.
@@ -822,6 +864,15 @@
*/
bool saveDocumentArchive( const QString &fileName );
+ /**
+ * Extract the document file from the current archive.
+ *
+ * @warning This function only works if the current file is a document archive
+ *
+ * @since 1.3
+ */
+ bool extractArchivedFile( const QString &destFileName );
+
/**
* Asks the generator to dynamically generate a SourceReference for a given
* page number and absolute X and Y position on this page.
@@ -857,6 +908,25 @@
*/
void walletDataForFile( const QString &fileName, QString *walletName, QString *walletFolder, QString *walletKey ) const;
+ /**
+ * Since version 0.21, okular does not allow editing annotations and
+ * form data if they are stored in the docdata directory (like older
+ * okular versions did by default).
+ * If this flag is set, then annotations and forms cannot be edited.
+ *
+ * @since 1.3
+ */
+ bool isDocdataMigrationNeeded() const;
+
+ /**
+ * Delete annotations and form data from the docdata folder. Call it if
+ * isDocdataMigrationNeeded() was true and you've just saved them to an
+ * external file.
+ *
+ * @since 1.3
+ */
+ void docdataMigrationDone();
+
/**
* Returns the model for rendering layers (NULL if the document has no layers)
*
@@ -1066,6 +1136,12 @@
*/
void canRedoChanged( bool redoAvailable );
+ /**
+ * This signal is emmitted whenever the undo history is clean (i.e. the same status the last time it was saved)
+ * @since 1.3
+ */
+ void undoHistoryCleanChanged( bool clean );
+
/**
* This signal is emitted whenever an rendition action is triggered and the UI should process it.
*
diff --git a/core/document.cpp b/core/document.cpp
--- a/core/document.cpp
+++ b/core/document.cpp
@@ -114,6 +114,7 @@
{
}
+ QString originalFileName;
QTemporaryFile document;
QTemporaryFile metadataFile;
};
@@ -526,22 +527,22 @@
#endif
}
-void DocumentPrivate::loadDocumentInfo()
+bool DocumentPrivate::loadDocumentInfo( LoadDocumentInfoFlags loadWhat )
// note: load data and stores it internally (document or pages). observers
// are still uninitialized at this point so don't access them
{
//qCDebug(OkularCoreDebug).nospace() << "Using '" << d->m_xmlFileName << "' as document info file.";
if ( m_xmlFileName.isEmpty() )
- return;
+ return false;
QFile infoFile( m_xmlFileName );
- loadDocumentInfo( infoFile );
+ return loadDocumentInfo( infoFile, loadWhat );
}
-void DocumentPrivate::loadDocumentInfo( QFile &infoFile )
+bool DocumentPrivate::loadDocumentInfo( QFile &infoFile, LoadDocumentInfoFlags loadWhat )
{
if ( !infoFile.exists() || !infoFile.open( QIODevice::ReadOnly ) )
- return;
+ return false;
// Load DOM from XML file
QDomDocument doc( QStringLiteral("documentInfo") );
@@ -549,13 +550,17 @@
{
qCDebug(OkularCoreDebug) << "Can't load XML pair! Check for broken xml.";
infoFile.close();
- return;
+ return false;
}
infoFile.close();
QDomElement root = doc.documentElement();
+
if ( root.tagName() != QLatin1String("documentInfo") )
- return;
+ return false;
+
+ QUrl documentUrl( root.attribute( "url" ) );
+ bool loadedAnything = false; // set if something gets actually loaded
// Parse the DOM tree
QDomNode topLevelNode = root.firstChild();
@@ -564,7 +569,7 @@
QString catName = topLevelNode.toElement().tagName();
// Restore page attributes (bookmark, annotations, ...) from the DOM
- if ( catName == QLatin1String("pageList") )
+ if ( catName == QLatin1String("pageList") && ( loadWhat & LoadPageInfo ) )
{
QDomNode pageNode = topLevelNode.firstChild();
while ( pageNode.isElement() )
@@ -578,14 +583,17 @@
// pass the domElement to the right page, to read config data from
if ( ok && pageNumber >= 0 && pageNumber < (int)m_pagesVector.count() )
- m_pagesVector[ pageNumber ]->d->restoreLocalContents( pageElement );
+ {
+ if ( m_pagesVector[ pageNumber ]->d->restoreLocalContents( pageElement ) )
+ loadedAnything = true;
+ }
}
pageNode = pageNode.nextSibling();
}
}
// Restore 'general info' from the DOM
- else if ( catName == QLatin1String("generalInfo") )
+ else if ( catName == QLatin1String("generalInfo") && ( loadWhat & LoadGeneralInfo ) )
{
QDomNode infoNode = topLevelNode.firstChild();
while ( infoNode.isElement() )
@@ -607,6 +615,7 @@
QString vpString = historyElement.attribute( QStringLiteral("viewport") );
m_viewportIterator = m_viewportHistory.insert( m_viewportHistory.end(),
DocumentViewport( vpString ) );
+ loadedAnything = true;
}
historyNode = historyNode.nextSibling();
}
@@ -622,6 +631,7 @@
if ( ok && newrotation != 0 )
{
setRotationInternal( newrotation, false );
+ loadedAnything = true;
}
}
else if ( infoElement.tagName() == QLatin1String("views") )
@@ -638,6 +648,7 @@
if ( view->name() == viewName )
{
loadViewsInfo( view, viewElement );
+ loadedAnything = true;
break;
}
}
@@ -651,6 +662,8 @@
topLevelNode = topLevelNode.nextSibling();
} //
+
+ return loadedAnything;
}
void DocumentPrivate::loadViewsInfo( View *view, const QDomElement &e )
@@ -952,25 +965,6 @@
return ret;
}
-void DocumentPrivate::warnLimitedAnnotSupport()
-{
- if ( !m_showWarningLimitedAnnotSupport )
- return;
- m_showWarningLimitedAnnotSupport = false; // Show the warning once
-
- if ( m_annotationsNeedSaveAs )
- {
- // Shown if the user is editing annotations in a file whose metadata is
- // not stored locally (.okular archives belong to this category)
- KMessageBox::information( m_widget, i18n("Your annotation changes will not be saved automatically. Use File -> Save As...\nor your changes will be lost once the document is closed"), QString(), QStringLiteral("annotNeedSaveAs") );
- }
- else if ( !canAddAnnotationsNatively() )
- {
- // If the generator doesn't support native annotations
- KMessageBox::information( m_widget, i18n("Your annotations are saved internally by Okular.\nYou can export the annotated document using File -> Export As -> Document Archive"), QString(), QStringLiteral("annotExportAsArchive") );
- }
-}
-
void DocumentPrivate::performAddPageAnnotation( int page, Annotation * annotation )
{
Okular::SaveInterface * iface = qobject_cast< Okular::SaveInterface * >( m_generator );
@@ -1000,8 +994,6 @@
// Redraw everything, including ExternallyDrawn annotations
refreshPixmaps( page );
}
-
- warnLimitedAnnotSupport();
}
void DocumentPrivate::performRemovePageAnnotation( int page, Annotation * annotation )
@@ -1038,8 +1030,6 @@
refreshPixmaps( page );
}
}
-
- warnLimitedAnnotSupport();
}
void DocumentPrivate::performModifyPageAnnotation( int page, Annotation * annotation, bool appearanceChanged )
@@ -1080,10 +1070,6 @@
qCDebug(OkularCoreDebug) << "Refreshing Pixmaps";
refreshPixmaps( page );
}
-
- // If the user is moving or resizing the annotation, don't steal the focus
- if ( (annotation->flags() & (Annotation::BeingMoved | Annotation::BeingResized) ) == 0 )
- warnLimitedAnnotSupport();
}
void DocumentPrivate::performSetAnnotationContents( const QString & newContents, Annotation *annot, int pageNumber )
@@ -1162,7 +1148,6 @@
{
qCWarning(OkularCoreDebug) << "Failed to open docdata file" << m_xmlFileName;
return;
-
}
// 1. Create DOM
QDomDocument doc( QStringLiteral("documentInfo") );
@@ -1174,21 +1159,23 @@
doc.appendChild( root );
// 2.1. Save page attributes (bookmark state, annotations, ... ) to DOM
- QDomElement pageList = doc.createElement( QStringLiteral("pageList") );
- root.appendChild( pageList );
- PageItems saveWhat = AllPageItems;
- if ( m_annotationsNeedSaveAs )
- {
- /* In this case, if the user makes a modification, he's requested to
- * save to a new document. Therefore, if there are existing local
- * annotations, we save them back unmodified in the original
- * document's metadata, so that it appears that it was not changed */
- saveWhat |= OriginalAnnotationPageItems;
- }
- // .... save pages that hold data
- QVector< Page * >::const_iterator pIt = m_pagesVector.constBegin(), pEnd = m_pagesVector.constEnd();
- for ( ; pIt != pEnd; ++pIt )
- (*pIt)->d->saveLocalContents( pageList, doc, saveWhat );
+ // -> do this there are not-yet-migrated annots or forms in docdata/
+ if ( m_docdataMigrationNeeded )
+ {
+ QDomElement pageList = doc.createElement( "pageList" );
+ root.appendChild( pageList );
+ // OriginalAnnotationPageItems and OriginalFormFieldPageItems tell to
+ // store the same unmodified annotation list and form contents that we
+ // read when we opened the file and ignore any change made by the user.
+ // Since we don't store annotations and forms docdata/ any more, this is
+ // necessary to preserve annotations/forms that previous Okular version
+ // had stored there.
+ const PageItems saveWhat = AllPageItems | OriginalAnnotationPageItems | OriginalFormFieldPageItems;
+ // .... save pages that hold data
+ QVector< Page * >::const_iterator pIt = m_pagesVector.constBegin(), pEnd = m_pagesVector.constEnd();
+ for ( ; pIt != pEnd; ++pIt )
+ (*pIt)->d->saveLocalContents( pageList, doc, saveWhat );
+ }
// 2.2. Save document info (current viewport, history, ... ) to DOM
QDomElement generalInfo = doc.createElement( QStringLiteral("generalInfo") );
@@ -2142,6 +2129,7 @@
connect( SettingsCore::self(), SIGNAL(configChanged()), this, SLOT(_o_configChanged()) );
connect(d->m_undoStack, &QUndoStack::canUndoChanged, this, &Document::canUndoChanged);
connect(d->m_undoStack, &QUndoStack::canRedoChanged, this, &Document::canRedoChanged);
+ connect(d->m_undoStack, &QUndoStack::cleanChanged, this, &Document::undoHistoryCleanChanged);
qRegisterMetaType();
}
@@ -2290,7 +2278,6 @@
QMimeDatabase db;
QMimeType mime = _mime;
QByteArray filedata;
- qint64 document_size = -1;
bool isstdin = url.fileName() == QLatin1String( "-" );
bool triedMimeFromFileContent = false;
if ( !isstdin )
@@ -2298,21 +2285,11 @@
if ( !mime.isValid() )
return OpenError;
- // docFile is always local so we can use QFileInfo on it
- QFileInfo fileReadTest( docFile );
- if ( fileReadTest.isFile() && !fileReadTest.isReadable() )
- {
- d->m_docFileName.clear();
- return OpenError;
- }
- // determine the related "xml document-info" filename
d->m_url = url;
d->m_docFileName = docFile;
- if ( url.isLocalFile() && !d->m_archiveData )
- {
- document_size = fileReadTest.size();
- d->m_xmlFileName = DocumentPrivate::docDataFileName(url, document_size);
- }
+
+ if ( !d->updateMetadataXmlNameAndDocSize() )
+ return OpenError;
}
else
{
@@ -2322,7 +2299,7 @@
mime = db.mimeTypeForData( filedata );
if ( !mime.isValid() || mime.isDefault() )
return OpenError;
- document_size = filedata.size();
+ d->m_docSize = filedata.size();
triedMimeFromFileContent = true;
}
@@ -2428,35 +2405,30 @@
connect( d->m_pageController, SIGNAL(rotationFinished(int,Okular::Page*)),
this, SLOT(rotationFinished(int,Okular::Page*)) );
- bool containsExternalAnnotations = false;
foreach ( Page * p, d->m_pagesVector )
- {
p->d->m_doc = d;
- if ( !p->annotations().empty() )
- containsExternalAnnotations = true;
- }
- // Be quiet while restoring local annotations
- d->m_showWarningLimitedAnnotSupport = false;
- d->m_annotationsNeedSaveAs = false;
+ d->m_metadataLoadingCompleted = false;
+ d->m_docdataMigrationNeeded = false;
// 2. load Additional Data (bookmarks, local annotations and metadata) about the document
if ( d->m_archiveData )
{
- d->loadDocumentInfo( d->m_archiveData->metadataFile );
- d->m_annotationsNeedSaveAs = true;
+ d->loadDocumentInfo( d->m_archiveData->metadataFile, LoadPageInfo );
+ d->loadDocumentInfo( LoadGeneralInfo );
}
else
{
- d->loadDocumentInfo();
- d->m_annotationsNeedSaveAs = ( d->canAddAnnotationsNatively() && containsExternalAnnotations );
+ if ( d->loadDocumentInfo( LoadPageInfo ) )
+ d->m_docdataMigrationNeeded = true;
+ d->loadDocumentInfo( LoadGeneralInfo );
}
- d->m_showWarningLimitedAnnotSupport = true;
+ d->m_metadataLoadingCompleted = true;
d->m_bookmarkManager->setUrl( d->m_url );
// 3. setup observers inernal lists and data
- foreachObserver( notifySetup( d->m_pagesVector, DocumentObserver::DocumentChanged ) );
+ foreachObserver( notifySetup( d->m_pagesVector, DocumentObserver::DocumentChanged | DocumentObserver::UrlChanged ) );
// 4. set initial page (restoring the page saved in xml if loaded)
DocumentViewport loadedViewport = (*d->m_viewportIterator);
@@ -2495,7 +2467,6 @@
}
AudioPlayer::instance()->d->m_currentDocument = isstdin ? QUrl() : d->m_url;
- d->m_docSize = document_size;
const QStringList docScripts = d->m_generator->metaData( QStringLiteral("DocumentScripts"), QStringLiteral ( "JavaScript" ) ).toStringList();
if ( !docScripts.isEmpty() )
@@ -2510,6 +2481,31 @@
return OpenSuccess;
}
+bool DocumentPrivate::updateMetadataXmlNameAndDocSize()
+{
+ // m_docFileName is always local so we can use QFileInfo on it
+ QFileInfo fileReadTest( m_docFileName );
+ if ( !fileReadTest.isFile() && !fileReadTest.isReadable() )
+ return false;
+
+ m_docSize = fileReadTest.size();
+
+ // determine the related "xml document-info" filename
+ if ( m_url.isLocalFile() )
+ {
+ const QString filePath = docDataFileName( m_url, m_docSize );
+ qCDebug(OkularCoreDebug) << "Metadata file is now:" << filePath;
+ m_xmlFileName = filePath;
+ }
+ else
+ {
+ qCDebug(OkularCoreDebug) << "Metadata file: disabled";
+ m_xmlFileName = QString();
+ }
+
+ return true;
+}
+
KXMLGUIClient* Document::guiClient()
{
@@ -2618,7 +2614,7 @@
d->m_rotation = Rotation0;
// send an empty list to observers (to free their data)
- foreachObserver( notifySetup( QVector< Page * >(), DocumentObserver::DocumentChanged ) );
+ foreachObserver( notifySetup( QVector< Page * >(), DocumentObserver::DocumentChanged | DocumentObserver::UrlChanged ) );
// delete pages and clear 'd->m_pagesVector' container
QVector< Page * >::const_iterator pIt = d->m_pagesVector.constBegin();
@@ -2662,6 +2658,7 @@
AudioPlayer::instance()->d->m_currentDocument = QUrl();
d->m_undoStack->clear();
+ d->m_docdataMigrationNeeded = false;
}
void Document::addObserver( DocumentObserver * pObserver )
@@ -2672,7 +2669,7 @@
// if the observer is added while a document is already opened, tell it
if ( !d->m_pagesVector.isEmpty() )
{
- pObserver->notifySetup( d->m_pagesVector, DocumentObserver::DocumentChanged );
+ pObserver->notifySetup( d->m_pagesVector, DocumentObserver::DocumentChanged | DocumentObserver::UrlChanged );
pObserver->notifyViewportChanged( false /*disables smoothMove*/ );
}
}
@@ -2909,7 +2906,9 @@
bool Document::isAllowed( Permission action ) const
{
- if ( action == Okular::AllowNotes && !d->m_annotationEditingEnabled )
+ if ( action == Okular::AllowNotes && ( d->m_docdataMigrationNeeded || !d->m_annotationEditingEnabled ) )
+ return false;
+ if ( action == Okular::AllowFillForms && d->m_docdataMigrationNeeded )
return false;
#if !OKULAR_FORCE_DRM
@@ -3219,12 +3218,11 @@
void DocumentPrivate::notifyAnnotationChanges( int page )
{
- int flags = DocumentObserver::Annotations;
-
- if ( m_annotationsNeedSaveAs )
- flags |= DocumentObserver::NeedSaveAs;
+ foreachObserverD( notifyPageChanged( page, DocumentObserver::Annotations ) );
+}
- foreachObserverD( notifyPageChanged( page, flags ) );
+void DocumentPrivate::notifyFormChanges( int /*page*/ )
+{
}
void Document::addPageAnnotation( int page, Annotation * annotation )
@@ -4317,6 +4315,9 @@
result.append(mimeType.name());
}
+ // Add the Okular archive mimetype
+ result << QStringLiteral("application/vnd.kde.okular-archive");
+
// Sorting by mimetype name doesn't make a ton of sense,
// but ensures that the list is ordered the same way every time
qSort(result);
@@ -4326,6 +4327,154 @@
return result;
}
+bool Document::canSwapBackingFile() const
+{
+ if ( !d->m_generator )
+ return false;
+ Q_ASSERT( !d->m_generatorName.isEmpty() );
+
+ QHash< QString, GeneratorInfo >::iterator genIt = d->m_loadedGenerators.find( d->m_generatorName );
+ Q_ASSERT( genIt != d->m_loadedGenerators.end() );
+
+ return genIt->generator->hasFeature( Generator::SwapBackingFile );
+}
+
+bool Document::swapBackingFile( const QString &newFileName, const QUrl &url )
+{
+ if ( !d->m_generator )
+ return false;
+ Q_ASSERT( !d->m_generatorName.isEmpty() );
+
+ QHash< QString, GeneratorInfo >::iterator genIt = d->m_loadedGenerators.find( d->m_generatorName );
+ Q_ASSERT( genIt != d->m_loadedGenerators.end() );
+
+ if ( !genIt->generator->hasFeature( Generator::SwapBackingFile ) )
+ return false;
+
+ // Save metadata about the file we're about to close
+ d->saveDocumentInfo();
+
+ qCDebug(OkularCoreDebug) << "Swapping backing file to" << newFileName;
+ QVector< Page * > newPagesVector;
+ Generator::SwapBackingFileResult result = genIt->generator->swapBackingFile( newFileName, newPagesVector );
+ if (result != Generator::SwapBackingFileError)
+ {
+ QLinkedList< ObjectRect* > rectsToDelete;
+ QLinkedList< Annotation* > annotationsToDelete;
+ QSet< PagePrivate* > pagePrivatesToDelete;
+
+ if (result == Generator::SwapBackingFileReloadInternalData)
+ {
+ // Here we need to replace everything that the old generator
+ // had created with what the new one has without making it look like
+ // we have actually closed and opened the file again
+
+ // Simple sanity check
+ if (newPagesVector.count() != d->m_pagesVector.count())
+ return false;
+
+ // Update the undo stack contents
+ for (int i = 0; i < d->m_undoStack->count(); ++i)
+ {
+ // Trust me on the const_cast ^_^
+ QUndoCommand *uc = const_cast( d->m_undoStack->command( i ) );
+ if (OkularUndoCommand *ouc = dynamic_cast( uc ))
+ {
+ const bool success = ouc->refreshInternalPageReferences( newPagesVector );
+ if ( !success )
+ {
+ qWarning() << "Document::swapBackingFile: refreshInternalPageReferences failed" << ouc;
+ return false;
+ }
+ }
+ else
+ {
+ qWarning() << "Document::swapBackingFile: Unhandled undo command" << uc;
+ return false;
+ }
+ }
+
+ for (int i = 0; i < d->m_pagesVector.count(); ++i)
+ {
+ // switch the PagePrivate* from newPage to oldPage
+ // this way everyone still holding Page* doesn't get
+ // disturbed by it
+ Page *oldPage = d->m_pagesVector[i];
+ Page *newPage = newPagesVector[i];
+ newPage->d->adoptGeneratedContents(oldPage->d);
+
+ pagePrivatesToDelete << oldPage->d;
+ oldPage->d = newPage->d;
+ oldPage->d->m_page = oldPage;
+ oldPage->d->m_doc = d;
+ newPage->d = nullptr;
+
+ annotationsToDelete << oldPage->m_annotations;
+ rectsToDelete << oldPage->m_rects;
+ oldPage->m_annotations = newPage->m_annotations;
+ oldPage->m_rects = newPage->m_rects;
+ }
+ qDeleteAll( newPagesVector );
+ }
+
+ d->m_url = url;
+ d->m_docFileName = newFileName;
+ d->updateMetadataXmlNameAndDocSize();
+ d->m_bookmarkManager->setUrl( d->m_url );
+
+ if ( d->m_synctex_scanner )
+ {
+ synctex_scanner_free( d->m_synctex_scanner );
+ d->m_synctex_scanner = synctex_scanner_new_with_output_file( QFile::encodeName( newFileName ).constData(), nullptr, 1);
+ if ( !d->m_synctex_scanner && QFile::exists(newFileName + QLatin1String( "sync" ) ) )
+ {
+ d->loadSyncFile(newFileName);
+ }
+ }
+
+ foreachObserver( notifySetup( d->m_pagesVector, DocumentObserver::UrlChanged ) );
+
+ qDeleteAll( annotationsToDelete );
+ qDeleteAll( rectsToDelete );
+ qDeleteAll( pagePrivatesToDelete );
+
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+}
+
+bool Document::swapBackingFileArchive( const QString &newFileName, const QUrl &url )
+{
+ qCDebug(OkularCoreDebug) << "Swapping backing archive to" << newFileName;
+
+ ArchiveData *newArchive = DocumentPrivate::unpackDocumentArchive( newFileName );
+ if ( !newArchive )
+ return false;
+
+ const QString tempFileName = newArchive->document.fileName();
+
+ const bool success = swapBackingFile( tempFileName, url );
+
+ if ( success )
+ {
+ delete d->m_archiveData;
+ d->m_archiveData = newArchive;
+ }
+
+ return success;
+}
+
+void Document::setHistoryClean( bool clean )
+{
+ if ( clean )
+ d->m_undoStack->setClean();
+ else
+ d->m_undoStack->resetClean();
+}
+
bool Document::canSaveChanges() const
{
if ( !d->m_generator )
@@ -4423,31 +4572,31 @@
return result;
}
-Document::OpenResult Document::openDocumentArchive( const QString & docFile, const QUrl & url, const QString & password )
+ArchiveData *DocumentPrivate::unpackDocumentArchive( const QString &archivePath )
{
QMimeDatabase db;
- const QMimeType mime = db.mimeTypeForFile( docFile, QMimeDatabase::MatchExtension );
+ const QMimeType mime = db.mimeTypeForFile( archivePath, QMimeDatabase::MatchExtension );
if ( !mime.inherits( QStringLiteral("application/vnd.kde.okular-archive") ) )
- return OpenError;
+ return nullptr;
- KZip okularArchive( docFile );
+ KZip okularArchive( archivePath );
if ( !okularArchive.open( QIODevice::ReadOnly ) )
- return OpenError;
+ return nullptr;
const KArchiveDirectory * mainDir = okularArchive.directory();
const KArchiveEntry * mainEntry = mainDir->entry( QStringLiteral("content.xml") );
if ( !mainEntry || !mainEntry->isFile() )
- return OpenError;
+ return nullptr;
std::unique_ptr< QIODevice > mainEntryDevice( static_cast< const KZipFileEntry * >( mainEntry )->createDevice() );
QDomDocument doc;
if ( !doc.setContent( mainEntryDevice.get() ) )
- return OpenError;
+ return nullptr;
mainEntryDevice.reset();
QDomElement root = doc.documentElement();
- if ( root.tagName() != QLatin1String("OkularArchive") )
- return OpenError;
+ if ( root.tagName() != QLatin1String("OkularArchive") )
+ return nullptr;
QString documentFileName;
QString metadataFileName;
@@ -4467,20 +4616,21 @@
}
}
if ( documentFileName.isEmpty() )
- return OpenError;
+ return nullptr;
const KArchiveEntry * docEntry = mainDir->entry( documentFileName );
if ( !docEntry || !docEntry->isFile() )
- return OpenError;
+ return nullptr;
std::unique_ptr< ArchiveData > archiveData( new ArchiveData() );
const int dotPos = documentFileName.indexOf( QLatin1Char('.') );
if ( dotPos != -1 )
archiveData->document.setFileTemplate(QDir::tempPath() + QLatin1String("/okular_XXXXXX") + documentFileName.mid(dotPos));
if ( !archiveData->document.open() )
- return OpenError;
+ return nullptr;
+
+ archiveData->originalFileName = documentFileName;
- QString tempFileName = archiveData->document.fileName();
{
std::unique_ptr< QIODevice > docEntryDevice( static_cast< const KZipFileEntry * >( docEntry )->createDevice() );
copyQIODevice( docEntryDevice.get(), &archiveData->document );
@@ -4499,17 +4649,23 @@
}
}
+ return archiveData.release();
+}
+
+Document::OpenResult Document::openDocumentArchive( const QString & docFile, const QUrl & url, const QString & password )
+{
+ d->m_archiveData = DocumentPrivate::unpackDocumentArchive( docFile );
+ if ( !d->m_archiveData )
+ return OpenError;
+
+ const QString tempFileName = d->m_archiveData->document.fileName();
+ QMimeDatabase db;
const QMimeType docMime = db.mimeTypeForFile( tempFileName, QMimeDatabase::MatchContent );
- d->m_archiveData = archiveData.get();
- d->m_archivedFileName = documentFileName;
const OpenResult ret = openDocument( tempFileName, url, docMime, password );
- if ( ret == OpenSuccess )
- {
- archiveData.release();
- }
- else
+ if ( ret != OpenSuccess )
{
+ delete d->m_archiveData;
d->m_archiveData = nullptr;
}
@@ -4523,7 +4679,7 @@
/* If we opened an archive, use the name of original file (eg foo.pdf)
* instead of the archive's one (eg foo.okular) */
- QString docFileName = d->m_archiveData ? d->m_archivedFileName : d->m_url.fileName();
+ QString docFileName = d->m_archiveData ? d->m_archiveData->originalFileName : d->m_url.fileName();
if ( docFileName == QLatin1String( "-" ) )
return false;
@@ -4564,7 +4720,8 @@
// If the generator can save annotations natively, do it
QTemporaryFile modifiedFile;
bool annotationsSavedNatively = false;
- if ( d->canAddAnnotationsNatively() )
+ bool formsSavedNatively = false;
+ if ( d->canAddAnnotationsNatively() || canSaveChanges( SaveFormsCapability ) )
{
if ( !modifiedFile.open() )
return false;
@@ -4575,7 +4732,8 @@
if ( saveChanges( modifiedFile.fileName(), &errorText ) )
{
docPath = modifiedFile.fileName(); // Save this instead of the original file
- annotationsSavedNatively = true;
+ annotationsSavedNatively = d->canAddAnnotationsNatively();
+ formsSavedNatively = canSaveChanges( SaveFormsCapability );
}
else
{
@@ -4584,8 +4742,13 @@
}
}
+ PageItems saveWhat = None;
+ if ( !annotationsSavedNatively )
+ saveWhat |= AnnotationPageItems;
+ if ( !formsSavedNatively )
+ saveWhat |= FormFieldPageItems;
+
QTemporaryFile metadataFile;
- PageItems saveWhat = annotationsSavedNatively ? None : AnnotationPageItems;
if ( !d->savePageDocumentInfo( &metadataFile, saveWhat ) )
return false;
@@ -4604,6 +4767,17 @@
return true;
}
+bool Document::extractArchivedFile( const QString &destFileName )
+{
+ if ( !d->m_archiveData )
+ return false;
+
+ // Remove existing file, if present (QFile::copy doesn't overwrite by itself)
+ QFile::remove( destFileName );
+
+ return d->m_archiveData->document.copy( destFileName );
+}
+
QPrinter::Orientation Document::orientation() const
{
double width, height;
@@ -4641,6 +4815,20 @@
}
}
+bool Document::isDocdataMigrationNeeded() const
+{
+ return d->m_docdataMigrationNeeded;
+}
+
+void Document::docdataMigrationDone()
+{
+ if (d->m_docdataMigrationNeeded)
+ {
+ d->m_docdataMigrationNeeded = false;
+ foreachObserver( notifySetup( d->m_pagesVector, 0 ) );
+ }
+}
+
QAbstractItemModel * Document::layersModel() const
{
return d->m_generator ? d->m_generator->layersModel() : nullptr;
diff --git a/core/document_p.h b/core/document_p.h
--- a/core/document_p.h
+++ b/core/document_p.h
@@ -74,6 +74,15 @@
int searchID;
};
+enum LoadDocumentInfoFlag
+{
+ LoadNone = 0,
+ LoadPageInfo = 1, // Load annotations and forms
+ LoadGeneralInfo = 2, // History, rotation, ...
+ LoadAllInfo = 0xff
+};
+Q_DECLARE_FLAGS(LoadDocumentInfoFlags, LoadDocumentInfoFlag)
+
class DocumentPrivate
{
public:
@@ -99,12 +108,14 @@
m_fontsCached( false ),
m_annotationEditingEnabled ( true ),
m_annotationBeingModified( false ),
+ m_docdataMigrationNeeded( false ),
m_synctex_scanner( nullptr )
{
calculateMaxTextPages();
}
// private methods
+ bool updateMetadataXmlNameAndDocSize();
QString pagesSizeString() const;
QString namePaperSize(double inchesWidth, double inchesHeight) const;
QString localizedSize(const QSizeF &size) const;
@@ -115,8 +126,8 @@
void calculateMaxTextPages();
qulonglong getTotalMemory();
qulonglong getFreeMemory( qulonglong *freeSwap = nullptr );
- void loadDocumentInfo();
- void loadDocumentInfo( QFile &infoFile );
+ bool loadDocumentInfo( LoadDocumentInfoFlags loadWhat );
+ bool loadDocumentInfo( QFile &infoFile, LoadDocumentInfoFlags loadWhat );
void loadViewsInfo( View *view, const QDomElement &e );
void saveViewsInfo( View *view, QDomElement &e ) const;
QUrl giveAbsoluteUrl( const QString & fileName ) const;
@@ -130,13 +141,14 @@
ConfigInterface* generatorConfig( GeneratorInfo& info );
SaveInterface* generatorSave( GeneratorInfo& info );
Document::OpenResult openDocumentInternal( const KPluginMetaData& offer, bool isstdin, const QString& docFile, const QByteArray& filedata, const QString& password );
+ static ArchiveData *unpackDocumentArchive( const QString &archivePath );
bool savePageDocumentInfo( QTemporaryFile *infoFile, int what ) const;
DocumentViewport nextDocumentViewport() const;
void notifyAnnotationChanges( int page );
+ void notifyFormChanges( int page );
bool canAddAnnotationsNatively() const;
bool canModifyExternalAnnotations() const;
bool canRemoveExternalAnnotations() const;
- void warnLimitedAnnotSupport();
OKULARCORE_EXPORT static QString docDataFileName(const QUrl &url, qint64 document_size);
// Methods that implement functionality needed by undo commands
@@ -273,13 +285,19 @@
QSet< View * > m_views;
bool m_annotationEditingEnabled;
- bool m_annotationsNeedSaveAs;
bool m_annotationBeingModified; // is an annotation currently being moved or resized?
- bool m_showWarningLimitedAnnotSupport;
+ bool m_metadataLoadingCompleted;
QUndoStack *m_undoStack;
QDomNode m_prevPropsOfAnnotBeingModified;
+ // Since 0.21, we no longer support saving annotations and form data in
+ // the docdata/ directory and we ask the user to migrate them to an
+ // external file as soon as possible, otherwise the document will be
+ // shown in read-only mode. This flag is set if the docdata/ XML file
+ // for the current document contains any annotation or form.
+ bool m_docdataMigrationNeeded;
+
synctex_scanner_p m_synctex_scanner;
// generator selection
diff --git a/core/documentcommands.cpp b/core/documentcommands.cpp
--- a/core/documentcommands.cpp
+++ b/core/documentcommands.cpp
@@ -15,6 +15,7 @@
#include "form.h"
#include "utils_p.h"
#include "page.h"
+#include "page_p.h"
#include
@@ -87,6 +88,21 @@
m_done = true;
}
+bool AddAnnotationCommand::refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector )
+{
+ if ( m_done )
+ {
+ // We don't always update m_annotation because even if the annotation has been added to the document
+ // it can have been removed later so the annotation pointer is stored inside a following RemoveAnnotationCommand
+ // and thus doesn't need updating because it didn't change
+ // because of the document reload
+ auto a = newPagesVector[m_pageNumber]->annotation( m_annotation->uniqueName() );
+ if (a) m_annotation = a;
+ }
+
+ return true;
+}
+
RemoveAnnotationCommand::RemoveAnnotationCommand(Okular::DocumentPrivate * doc, Okular::Annotation* annotation, int pageNumber)
: m_docPriv( doc ),
@@ -112,12 +128,27 @@
m_done = false;
}
-void RemoveAnnotationCommand::redo(){
+void RemoveAnnotationCommand::redo()
+{
moveViewportIfBoundingRectNotFullyVisible( m_annotation->boundingRectangle(), m_docPriv, m_pageNumber );
m_docPriv->performRemovePageAnnotation( m_pageNumber, m_annotation );
m_done = true;
}
+bool RemoveAnnotationCommand::refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector )
+{
+ if ( !m_done )
+ {
+ // We don't always update m_annotation because it can happen that the annotation remove has been undo
+ // and that annotation addition has also been undone so the the annotation pointer is stored inside
+ // a previous AddAnnotationCommand and thus doesn't need updating because it didn't change
+ // because of the document reload
+ auto a = newPagesVector[m_pageNumber]->annotation( m_annotation->uniqueName() );
+ if (a) m_annotation = a;
+ }
+
+ return true;
+}
ModifyAnnotationPropertiesCommand::ModifyAnnotationPropertiesCommand( DocumentPrivate* docPriv,
Annotation* annotation,
@@ -147,6 +178,16 @@
m_docPriv->performModifyPageAnnotation( m_pageNumber, m_annotation, true );
}
+bool ModifyAnnotationPropertiesCommand::refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector )
+{
+ // Same reason for not unconditionally updating m_annotation, the annotation pointer can be stored in an add/Remove command
+ auto a = newPagesVector[m_pageNumber]->annotation( m_annotation->uniqueName() );
+ if (a) m_annotation = a;
+
+ return true;
+}
+
+
TranslateAnnotationCommand::TranslateAnnotationCommand( DocumentPrivate* docPriv,
Annotation* annotation,
int pageNumber,
@@ -212,6 +253,16 @@
return boundingRect;
}
+bool TranslateAnnotationCommand::refreshInternalPageReferences( const QVector< Page * > &newPagesVector )
+{
+ // Same reason for not unconditionally updating m_annotation, the annotation pointer can be stored in an add/Remove command
+ auto a = newPagesVector[m_pageNumber]->annotation( m_annotation->uniqueName() );
+ if (a) m_annotation = a;
+
+ return true;
+}
+
+
AdjustAnnotationCommand::AdjustAnnotationCommand(Okular::DocumentPrivate * docPriv,
Okular::Annotation * annotation,
int pageNumber,
@@ -277,6 +328,16 @@
return Okular::NormalizedRect( left, top, right, bottom );
}
+bool AdjustAnnotationCommand::refreshInternalPageReferences( const QVector< Page * > &newPagesVector )
+{
+ // Same reason for not unconditionally updating m_annotation, the annotation pointer can be stored in an add/Remove command
+ auto a = newPagesVector[m_pageNumber]->annotation( m_annotation->uniqueName() );
+ if (a) m_annotation = a;
+
+ return true;
+}
+
+
EditTextCommand::EditTextCommand( const QString & newContents,
int newCursorPos,
const QString & prevContents,
@@ -363,6 +424,7 @@
return m_newContents.right(m_newContents.length() - m_newCursorPos);
}
+
EditAnnotationContentsCommand::EditAnnotationContentsCommand( DocumentPrivate* docPriv,
Annotation* annotation,
int pageNumber,
@@ -412,6 +474,15 @@
}
}
+bool EditAnnotationContentsCommand::refreshInternalPageReferences( const QVector< Page * > &newPagesVector )
+{
+ auto a = newPagesVector[m_pageNumber]->annotation( m_annotation->uniqueName() );
+ if (a) m_annotation = a;
+
+ return true;
+}
+
+
EditFormTextCommand::EditFormTextCommand( Okular::DocumentPrivate* docPriv,
Okular::FormFieldText* form,
int pageNumber,
@@ -433,6 +504,7 @@
moveViewportIfBoundingRectNotFullyVisible( m_form->rect(), m_docPriv, m_pageNumber );
m_form->setText( m_prevContents );
emit m_docPriv->m_parent->formTextChangedByUndoRedo( m_pageNumber, m_form, m_prevContents, m_prevCursorPos, m_prevAnchorPos );
+ m_docPriv->notifyFormChanges( m_pageNumber );
}
void EditFormTextCommand::redo()
@@ -440,6 +512,7 @@
moveViewportIfBoundingRectNotFullyVisible( m_form->rect(), m_docPriv, m_pageNumber );
m_form->setText( m_newContents );
emit m_docPriv->m_parent->formTextChangedByUndoRedo( m_pageNumber, m_form, m_newContents, m_newCursorPos, m_newCursorPos );
+ m_docPriv->notifyFormChanges( m_pageNumber );
}
int EditFormTextCommand::id() const
@@ -461,6 +534,14 @@
}
}
+bool EditFormTextCommand::refreshInternalPageReferences( const QVector< Page * > &newPagesVector )
+{
+ m_form = dynamic_cast(Okular::PagePrivate::findEquivalentForm( newPagesVector[m_pageNumber], m_form ));
+
+ return m_form;
+}
+
+
EditFormListCommand::EditFormListCommand( Okular::DocumentPrivate* docPriv,
FormFieldChoice* form,
int pageNumber,
@@ -480,6 +561,7 @@
moveViewportIfBoundingRectNotFullyVisible( m_form->rect(), m_docPriv, m_pageNumber );
m_form->setCurrentChoices( m_prevChoices );
emit m_docPriv->m_parent->formListChangedByUndoRedo( m_pageNumber, m_form, m_prevChoices );
+ m_docPriv->notifyFormChanges( m_pageNumber );
}
void EditFormListCommand::redo()
@@ -487,8 +569,17 @@
moveViewportIfBoundingRectNotFullyVisible( m_form->rect(), m_docPriv, m_pageNumber );
m_form->setCurrentChoices( m_newChoices );
emit m_docPriv->m_parent->formListChangedByUndoRedo( m_pageNumber, m_form, m_newChoices );
+ m_docPriv->notifyFormChanges( m_pageNumber );
}
+bool EditFormListCommand::refreshInternalPageReferences( const QVector< Page * > &newPagesVector )
+{
+ m_form = dynamic_cast(Okular::PagePrivate::findEquivalentForm( newPagesVector[m_pageNumber], m_form ));
+
+ return m_form;
+}
+
+
EditFormComboCommand::EditFormComboCommand( Okular::DocumentPrivate* docPriv,
FormFieldChoice* form,
int pageNumber,
@@ -533,6 +624,7 @@
}
moveViewportIfBoundingRectNotFullyVisible( m_form->rect(), m_docPriv, m_pageNumber );
emit m_docPriv->m_parent->formComboChangedByUndoRedo( m_pageNumber, m_form, m_prevContents, m_prevCursorPos, m_prevAnchorPos );
+ m_docPriv->notifyFormChanges( m_pageNumber );
}
void EditFormComboCommand::redo()
@@ -547,6 +639,7 @@
}
moveViewportIfBoundingRectNotFullyVisible( m_form->rect(), m_docPriv, m_pageNumber );
emit m_docPriv->m_parent->formComboChangedByUndoRedo( m_pageNumber, m_form, m_newContents, m_newCursorPos, m_newCursorPos );
+ m_docPriv->notifyFormChanges( m_pageNumber );
}
int EditFormComboCommand::id() const
@@ -573,6 +666,14 @@
}
}
+bool EditFormComboCommand::refreshInternalPageReferences( const QVector< Page * > &newPagesVector )
+{
+ m_form = dynamic_cast(Okular::PagePrivate::findEquivalentForm( newPagesVector[m_pageNumber], m_form ));
+
+ return m_form;
+}
+
+
EditFormButtonsCommand::EditFormButtonsCommand( Okular::DocumentPrivate* docPriv,
int pageNumber,
const QList< FormFieldButton* > & formButtons,
@@ -603,6 +704,7 @@
Okular::NormalizedRect boundingRect = buildBoundingRectangleForButtons( m_formButtons );
moveViewportIfBoundingRectNotFullyVisible( boundingRect, m_docPriv, m_pageNumber );
emit m_docPriv->m_parent->formButtonsChangedByUndoRedo( m_pageNumber, m_formButtons );
+ m_docPriv->notifyFormChanges( m_pageNumber );
}
void EditFormButtonsCommand::redo()
@@ -618,6 +720,22 @@
Okular::NormalizedRect boundingRect = buildBoundingRectangleForButtons( m_formButtons );
moveViewportIfBoundingRectNotFullyVisible( boundingRect, m_docPriv, m_pageNumber );
emit m_docPriv->m_parent->formButtonsChangedByUndoRedo( m_pageNumber, m_formButtons );
+ m_docPriv->notifyFormChanges( m_pageNumber );
+}
+
+bool EditFormButtonsCommand::refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector )
+{
+ const QList< FormFieldButton* > oldFormButtons = m_formButtons;
+ m_formButtons.clear();
+ foreach( FormFieldButton* oldFormButton, oldFormButtons )
+ {
+ FormFieldButton *button = dynamic_cast(Okular::PagePrivate::findEquivalentForm( newPagesVector[m_pageNumber], oldFormButton ));
+ if ( !button )
+ return false;
+ m_formButtons << button;
+ }
+
+ return true;
}
void EditFormButtonsCommand::clearFormButtonStates()
diff --git a/core/documentcommands_p.h b/core/documentcommands_p.h
--- a/core/documentcommands_p.h
+++ b/core/documentcommands_p.h
@@ -23,8 +23,15 @@
class FormFieldText;
class FormFieldButton;
class FormFieldChoice;
+class Page;
-class AddAnnotationCommand : public QUndoCommand
+class OkularUndoCommand : public QUndoCommand
+{
+ public:
+ virtual bool refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) = 0;
+};
+
+class AddAnnotationCommand : public OkularUndoCommand
{
public:
AddAnnotationCommand(Okular::DocumentPrivate * docPriv, Okular::Annotation* annotation, int pageNumber);
@@ -35,6 +42,8 @@
void redo() override;
+ bool refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) override;
+
private:
Okular::DocumentPrivate * m_docPriv;
Okular::Annotation* m_annotation;
@@ -42,7 +51,7 @@
bool m_done;
};
-class RemoveAnnotationCommand : public QUndoCommand
+class RemoveAnnotationCommand : public OkularUndoCommand
{
public:
RemoveAnnotationCommand(Okular::DocumentPrivate * doc, Okular::Annotation* annotation, int pageNumber);
@@ -50,6 +59,8 @@
void undo() override;
void redo() override;
+ bool refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) override;
+
private:
Okular::DocumentPrivate * m_docPriv;
Okular::Annotation* m_annotation;
@@ -57,7 +68,7 @@
bool m_done;
};
-class ModifyAnnotationPropertiesCommand : public QUndoCommand
+class ModifyAnnotationPropertiesCommand : public OkularUndoCommand
{
public:
ModifyAnnotationPropertiesCommand( Okular::DocumentPrivate* docPriv, Okular::Annotation* annotation,
@@ -68,6 +79,8 @@
void undo() override;
void redo() override;
+ bool refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) override;
+
private:
Okular::DocumentPrivate * m_docPriv;
Okular::Annotation* m_annotation;
@@ -76,7 +89,7 @@
QDomNode m_newProperties;
};
-class TranslateAnnotationCommand : public QUndoCommand
+class TranslateAnnotationCommand : public OkularUndoCommand
{
public:
TranslateAnnotationCommand(Okular::DocumentPrivate* docPriv,
@@ -92,6 +105,8 @@
Okular::NormalizedPoint minusDelta();
Okular::NormalizedRect translateBoundingRectangle( const Okular::NormalizedPoint & delta );
+ bool refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) override;
+
private:
Okular::DocumentPrivate * m_docPriv;
Okular::Annotation* m_annotation;
@@ -100,7 +115,7 @@
bool m_completeDrag;
};
-class AdjustAnnotationCommand : public QUndoCommand
+class AdjustAnnotationCommand : public OkularUndoCommand
{
public:
AdjustAnnotationCommand(Okular::DocumentPrivate * docPriv,
@@ -117,6 +132,8 @@
Okular::NormalizedRect adjustBoundingRectangle(
const Okular::NormalizedPoint & delta1, const Okular::NormalizedPoint & delta2 );
+ bool refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) override;
+
private:
Okular::DocumentPrivate * m_docPriv;
Okular::Annotation* m_annotation;
@@ -126,7 +143,7 @@
bool m_completeDrag;
};
-class EditTextCommand : public QUndoCommand
+class EditTextCommand : public OkularUndoCommand
{
public:
EditTextCommand( const QString & newContents,
@@ -182,6 +199,8 @@
int id() const override;
bool mergeWith(const QUndoCommand *uc) override;
+ bool refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) override;
+
private:
Okular::DocumentPrivate * m_docPriv;
Okular::Annotation* m_annotation;
@@ -203,13 +222,16 @@
void redo() override;
int id() const override;
bool mergeWith( const QUndoCommand *uc ) override;
+
+ bool refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) override;
+
private:
Okular::DocumentPrivate* m_docPriv;
Okular::FormFieldText* m_form;
int m_pageNumber;
};
-class EditFormListCommand : public QUndoCommand
+class EditFormListCommand : public OkularUndoCommand
{
public:
EditFormListCommand( Okular::DocumentPrivate* docPriv,
@@ -222,6 +244,8 @@
void undo() override;
void redo() override;
+ bool refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) override;
+
private:
Okular::DocumentPrivate* m_docPriv;
FormFieldChoice* m_form;
@@ -248,6 +272,8 @@
int id() const override;
bool mergeWith( const QUndoCommand *uc ) override;
+ bool refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) override;
+
private:
Okular::DocumentPrivate* m_docPriv;
FormFieldChoice* m_form;
@@ -256,7 +282,7 @@
int m_prevIndex;
};
-class EditFormButtonsCommand : public QUndoCommand
+class EditFormButtonsCommand : public OkularUndoCommand
{
public:
EditFormButtonsCommand( Okular::DocumentPrivate* docPriv,
@@ -268,6 +294,8 @@
void undo() override;
void redo() override;
+ bool refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) override;
+
private:
void clearFormButtonStates();
diff --git a/core/generator.h b/core/generator.h
--- a/core/generator.h
+++ b/core/generator.h
@@ -211,7 +211,8 @@
PrintNative, ///< Whether the Generator supports native cross-platform printing (QPainter-based).
PrintPostscript, ///< Whether the Generator supports postscript-based file printing.
PrintToFile, ///< Whether the Generator supports export to PDF & PS through the Print Dialog
- TiledRendering ///< Whether the Generator can render tiles @since 0.16 (KDE 4.10)
+ TiledRendering, ///< Whether the Generator can render tiles @since 0.16 (KDE 4.10)
+ SwapBackingFile ///< Whether the Generator can hot-swap the file it's reading from @since 1.3
};
/**
@@ -272,6 +273,27 @@
*/
virtual Document::OpenResult loadDocumentFromDataWithPassword( const QByteArray & fileData, QVector< Page * > & pagesVector, const QString &password );
+ /**
+ * Describes the result of an swap file operation.
+ *
+ * @since 1.3
+ */
+ enum SwapBackingFileResult
+ {
+ SwapBackingFileError, //< The document could not be swapped
+ SwapBackingFileNoOp, //< The document was swapped and nothing needs to be done
+ SwapBackingFileReloadInternalData //< The document was swapped and internal data (forms, annotations, etc) needs to be reloaded
+ };
+
+ /**
+ * Changes the path of the file we are reading from. The new path must
+ * point to a copy of the same document.
+ *
+ * @note the Generator has to have the feature @ref SwapBackingFile enabled
+ *
+ * @since 1.3
+ */
+ virtual SwapBackingFileResult swapBackingFile( const QString &newFileName, QVector & newPagesVector );
/**
* This method is called when the document is closed and not used
diff --git a/core/generator.cpp b/core/generator.cpp
--- a/core/generator.cpp
+++ b/core/generator.cpp
@@ -203,6 +203,11 @@
return loadDocumentFromData( fileData, pagesVector ) ? Document::OpenSuccess : Document::OpenError;
}
+Generator::SwapBackingFileResult Generator::swapBackingFile( QString const &/*newFileName */, QVector & /*newPagesVector*/ )
+{
+ return SwapBackingFileError;
+}
+
bool Generator::closeDocument()
{
Q_D( Generator );
diff --git a/core/observer.h b/core/observer.h
--- a/core/observer.h
+++ b/core/observer.h
@@ -45,7 +45,7 @@
TextSelection = 8, ///< Text selection has been changed
Annotations = 16, ///< Annotations have been changed
BoundingBox = 32, ///< Bounding boxes have been changed
- NeedSaveAs = 64 ///< Set along with Annotations when Save As is needed or annotation changes will be lost @since 0.15 (KDE 4.9)
+ NeedSaveAs = 64 ///< Set when "Save" is needed or annotation/form changes will be lost @since 0.15 (KDE 4.9) @deprecated
};
/**
@@ -53,7 +53,8 @@
*/
enum SetupFlags {
DocumentChanged = 1, ///< The document is a new document.
- NewLayoutForPages = 2 ///< All the pages have
+ NewLayoutForPages = 2, ///< All the pages have
+ UrlChanged = 4 ///< The URL has changed @since 1.3
};
/**
diff --git a/core/page.h b/core/page.h
--- a/core/page.h
+++ b/core/page.h
@@ -249,6 +249,12 @@
*/
QLinkedList< Annotation* > annotations() const;
+ /**
+ * Returns the annotation with the given unique name.
+ * @since 1.3
+ */
+ Annotation * annotation( const QString & uniqueName ) const;
+
/**
* Returns the @ref Action object which is associated with the given page @p action
* or 0 if no page action is set.
@@ -386,7 +392,7 @@
QList tilesAt( const DocumentObserver *observer, const NormalizedRect &rect ) const;
private:
- PagePrivate* const d;
+ PagePrivate* d;
/// @cond PRIVATE
friend class PagePrivate;
friend class Document;
diff --git a/core/page.cpp b/core/page.cpp
--- a/core/page.cpp
+++ b/core/page.cpp
@@ -139,14 +139,17 @@
Page::~Page()
{
- deletePixmaps();
- deleteRects();
- d->deleteHighlights();
- deleteAnnotations();
- d->deleteTextSelections();
- deleteSourceReferences();
-
- delete d;
+ if (d)
+ {
+ deletePixmaps();
+ deleteRects();
+ d->deleteHighlights();
+ deleteAnnotations();
+ d->deleteTextSelections();
+ deleteSourceReferences();
+
+ delete d;
+ }
}
int Page::number() const
@@ -496,6 +499,16 @@
return m_annotations;
}
+Annotation * Page::annotation( const QString & uniqueName ) const
+{
+ foreach(Annotation *a, m_annotations)
+ {
+ if ( a->uniqueName() == uniqueName )
+ return a;
+ }
+ return nullptr;
+}
+
const Action * Page::pageAction( PageAction action ) const
{
switch ( action )
@@ -801,8 +814,10 @@
m_annotations.clear();
}
-void PagePrivate::restoreLocalContents( const QDomNode & pageNode )
+bool PagePrivate::restoreLocalContents( const QDomNode & pageNode )
{
+ bool loadedAnything = false; // set if something actually gets loaded
+
// iterate over all chilren (annotationList, ...)
QDomNode childNode = pageNode.firstChild();
while ( childNode.isElement() )
@@ -837,6 +852,7 @@
{
m_doc->performAddPageAnnotation(m_number, annotation);
qCDebug(OkularCoreDebug) << "restored annot:" << annotation->uniqueName();
+ loadedAnything = true;
}
else
qCWarning(OkularCoreDebug).nospace() << "page (" << m_number << "): can't restore an annotation from XML.";
@@ -848,6 +864,10 @@
// parse formList child element
else if ( childElement.tagName() == QLatin1String("forms") )
{
+ // Clone forms as root node in restoredFormFieldList
+ const QDomNode clonedNode = restoredFormFieldList.importNode( childElement, true );
+ restoredFormFieldList.appendChild( clonedNode );
+
if ( formfields.isEmpty() )
continue;
@@ -880,9 +900,12 @@
QString value = formElement.attribute( QStringLiteral("value") );
(*wantedIt)->d_ptr->setValue( value );
+ loadedAnything = true;
}
}
}
+
+ return loadedAnything;
}
void PagePrivate::saveLocalContents( QDomNode & parentNode, QDomDocument & document, PageItems what ) const
@@ -943,7 +966,17 @@
}
// add forms info if has got any
- if ( ( what & FormFieldPageItems ) && !formfields.isEmpty() )
+ if ( ( what & FormFieldPageItems ) && ( what & OriginalFormFieldPageItems ) )
+ {
+ const QDomElement savedDocRoot = restoredFormFieldList.documentElement();
+ if ( !savedDocRoot.isNull() )
+ {
+ // Import and append node in target document
+ const QDomNode importedNode = document.importNode( savedDocRoot, true );
+ pageElement.appendChild( importedNode );
+ }
+ }
+ else if ( ( what & FormFieldPageItems ) && !formfields.isEmpty() )
{
// create the formList
QDomElement formListElement = document.createElement( QStringLiteral("forms") );
@@ -1032,3 +1065,59 @@
m_tilesManagers.insert(observer, tm);
}
+
+void PagePrivate::adoptGeneratedContents( PagePrivate *oldPage )
+{
+ rotateAt( oldPage->m_rotation );
+
+ m_pixmaps = oldPage->m_pixmaps;
+ oldPage->m_pixmaps.clear();
+
+ m_tilesManagers = oldPage->m_tilesManagers;
+ oldPage->m_tilesManagers.clear();
+
+ m_boundingBox = oldPage->m_boundingBox;
+ m_isBoundingBoxKnown = oldPage->m_isBoundingBoxKnown;
+ m_text = oldPage->m_text;
+ oldPage->m_text = nullptr;
+
+ m_textSelections = oldPage->m_textSelections;
+ oldPage->m_textSelections = nullptr;
+
+ restoredLocalAnnotationList = oldPage->restoredLocalAnnotationList;
+ restoredFormFieldList = oldPage->restoredFormFieldList;
+}
+
+FormField *PagePrivate::findEquivalentForm( const Page *p, FormField *oldField )
+{
+ // given how id is not very good of id (at least for pdf) we do a few passes
+ // same rect, type and id
+ foreach(FormField *f, p->d->formfields)
+ {
+ if (f->rect() == oldField->rect() && f->type() == oldField->type() && f->id() == oldField->id())
+ return f;
+ }
+ // same rect and type
+ foreach(FormField *f, p->d->formfields)
+ {
+ if (f->rect() == oldField->rect() && f->type() == oldField->type())
+ return f;
+ }
+ // fuzzy rect, same type and id
+ foreach(FormField *f, p->d->formfields)
+ {
+ if (f->type() == oldField->type() && f->id() == oldField->id() && qFuzzyCompare(f->rect().left, oldField->rect().left) && qFuzzyCompare(f->rect().top, oldField->rect().top) && qFuzzyCompare(f->rect().right, oldField->rect().right) && qFuzzyCompare(f->rect().bottom, oldField->rect().bottom))
+ {
+ return f;
+ }
+ }
+ // fuzzy rect and same type
+ foreach(FormField *f, p->d->formfields)
+ {
+ if (f->type() == oldField->type() && qFuzzyCompare(f->rect().left, oldField->rect().left) && qFuzzyCompare(f->rect().top, oldField->rect().top) && qFuzzyCompare(f->rect().right, oldField->rect().right) && qFuzzyCompare(f->rect().bottom, oldField->rect().bottom))
+ {
+ return f;
+ }
+ }
+ return nullptr;
+}
diff --git a/core/page_p.h b/core/page_p.h
--- a/core/page_p.h
+++ b/core/page_p.h
@@ -48,7 +48,11 @@
/* If set along with AnnotationPageItems, tells saveLocalContents to save
* the original annotations (if any) instead of the modified ones */
- OriginalAnnotationPageItems = 0x100
+ OriginalAnnotationPageItems = 0x100,
+
+ /* If set along with FormFieldPageItems, tells saveLocalContents to save
+ * the original form contents (if any) instead of the modified one */
+ OriginalFormFieldPageItems = 0x200
};
Q_DECLARE_FLAGS(PageItems, PageItem)
@@ -66,7 +70,7 @@
/**
* Loads the local contents (e.g. annotations) of the page.
*/
- void restoreLocalContents( const QDomNode & pageNode );
+ bool restoreLocalContents( const QDomNode & pageNode );
/**
* Saves the local contents (e.g. annotations) of the page.
@@ -116,6 +120,17 @@
*/
void setTilesManager( const DocumentObserver *observer, TilesManager *tm );
+ /**
+ * Moves contents that are generated from oldPage to this. And clears them from page
+ * so it can be deleted fine.
+ */
+ void adoptGeneratedContents( PagePrivate *oldPage );
+
+ /*
+ * Tries to find an equivalent form field to oldField by looking into the rect, type and name
+ */
+ OKULARCORE_EXPORT static FormField *findEquivalentForm( const Page *p, FormField *oldField );
+
class PixmapObject
{
public:
@@ -144,6 +159,7 @@
bool m_isBoundingBoxKnown : 1;
QDomDocument restoredLocalAnnotationList; // ...
+ QDomDocument restoredFormFieldList; // ...
};
}
diff --git a/doc/index.docbook b/doc/index.docbook
--- a/doc/index.docbook
+++ b/doc/index.docbook
@@ -438,9 +438,6 @@
Annotations
&okular; allows you to review and annotate your documents.
- Annotations created in &okular; are automatically saved in the internal local data folder
- for each user.
- &okular; does not implicitly change any document it opens.
&okular;'s Annotations
@@ -460,22 +457,11 @@
Using the context menu either in the Reviews view of the navigation panel or in the main window you can open a Pop up Note for any kind of annotation and add or edit comments.
Annotations are not only limited to &PDF; files, they can be used for any format &okular; supports.
- Since &kde; 4.2, &okular; has the "document archiving" feature. This is an &okular;-specific format for carrying the document plus various metadata related to it (currently only annotations). You can save a "document archive" from the open document by choosing FileExport AsDocument Archive. To open an &okular; document archive, just open it with &okular; as it would be ⪚ a &PDF; document.
+ &okular; has the "document archiving" feature. This is an &okular;-specific format for carrying the document plus various metadata related to it (currently only annotations). You can save a "document archive" from the open document by choosing FileSave As and selecting Okular Archive in the Filter selector. To open an &okular; document archive, just open it with &okular; as it would be ⪚ a &PDF; document.
- Since &okular; 0.15 you can also save annotations directly into &PDF; files. This feature is only available if &okular; has been built with version 0.20 or later of Poppler rendering library. You can use File Save As... to save the copy of &PDF; file with annotations.
+ You can also save annotations directly into &PDF; files. You can use File Save to save it over the current file or File Save As... to save it to a new file.
-
-
- It is not possible to save annotations into &PDF; file if original file was encrypted and &okular; uses Poppler libraries of version which is lower than 0.22.
-
-
-
-
- If you open a &PDF; with existing annotations, your annotation changes are not automatically saved in the internal local data folder, and you need to save the modified document (using FileSave As...) before closing it. Should you forget to do this &okular; will show confirmation window that allows you to save the document.
-
-
-
Due to DRM limitations (typically with &PDF; documents), adding, editing some properties
@@ -488,7 +474,7 @@
- Since &okular; 0.17 you can configure the default properties and appearance of each annotating tool. Please refer to the corresponding section in this documentation.
+ You can configure the default properties and appearance of each annotating tool. Please refer to the corresponding section in this documentation.
Adding annotations
@@ -1010,31 +996,31 @@
&Ctrl;S
File
- Save As...
+ Save
- Saves the document under a new name including all the changes (annotations, form contents, &etc;), provided the document backend supports saving changes. With the &PDF; backend it is possible to save the document with the changed values of the form fields. It can be possible (provided that the data were not secured using DRM) to save annotations with &PDF; files.
-
-
- Note that, due to the way this is implemented, even if there are no changes to the file, the new file need not to be an exact bit-for-bit copy of the original file (⪚ can have a different SHA-1 hash, &etc;).
-
-
+ Saves the document including all the changes (annotations, form contents, &etc;), provided the document backend supports saving those changes, if the backend does not support saving the changes the user will be give the option to lose them or to save as &okular; archive.
-
+
&Ctrl;&Shift;S
File
- Save Copy As...
+ Save As...
- Saves a copy of the original document under a new name (completely bypassing the document backend). The saved document will be a bit-for-bit copy of the original.
+ Saves the document under a new name including all the changes (annotations, form contents, &etc;), provided the document backend supports saving changes, if the backend does not support saving the changes the user will be give the option to lose them or to save as &okular; archive.
+
+
+ Note that, due to the way this is implemented, even if there are no changes to the file, the new file need not to be an exact bit-for-bit copy of the original file (⪚ can have a different SHA-1 hash, &etc;).
+
+
diff --git a/generators/kimgio/generator_kimgio.h b/generators/kimgio/generator_kimgio.h
--- a/generators/kimgio/generator_kimgio.h
+++ b/generators/kimgio/generator_kimgio.h
@@ -27,6 +27,7 @@
// [INHERITED] load a document and fill up the pagesVector
bool loadDocument( const QString & fileName, QVector & pagesVector ) override;
bool loadDocumentFromData( const QByteArray & fileData, QVector & pagesVector ) override;
+ SwapBackingFileResult swapBackingFile( QString const &newFileName, QVector & newPagesVector ) override;
// [INHERITED] print document using already configured kprinter
bool print( QPrinter& printer ) override;
diff --git a/generators/kimgio/generator_kimgio.cpp b/generators/kimgio/generator_kimgio.cpp
--- a/generators/kimgio/generator_kimgio.cpp
+++ b/generators/kimgio/generator_kimgio.cpp
@@ -39,6 +39,7 @@
setFeature( TiledRendering );
setFeature( PrintNative );
setFeature( PrintToFile );
+ setFeature( SwapBackingFile );
}
KIMGIOGenerator::~KIMGIOGenerator()
@@ -90,6 +91,13 @@
return true;
}
+KIMGIOGenerator::SwapBackingFileResult KIMGIOGenerator::swapBackingFile( QString const &/*newFileName*/, QVector & /*newPagesVector*/ )
+{
+ // NOP: We don't actually need to do anything because all data has already
+ // been loaded in RAM
+ return SwapBackingFileNoOp;
+}
+
bool KIMGIOGenerator::doCloseDocument()
{
m_img = QImage();
diff --git a/generators/poppler/generator_pdf.h b/generators/poppler/generator_pdf.h
--- a/generators/poppler/generator_pdf.h
+++ b/generators/poppler/generator_pdf.h
@@ -99,6 +99,7 @@
Okular::AnnotationProxy* annotationProxy() const override;
protected:
+ SwapBackingFileResult swapBackingFile( QString const &newFileName, QVector & newPagesVector ) override;
bool doCloseDocument() override;
Okular::TextPage* textPage( Okular::Page *page ) override;
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
@@ -516,6 +516,7 @@
setFeature( PrintToFile );
setFeature( ReadRawData );
setFeature( TiledRendering );
+ setFeature( SwapBackingFile );
// You only need to do it once not for each of the documents but it is cheap enough
// so doing it all the time won't hurt either
@@ -596,6 +597,16 @@
return Okular::Document::OpenSuccess;
}
+PDFGenerator::SwapBackingFileResult PDFGenerator::swapBackingFile( QString const &newFileName, QVector & newPagesVector )
+{
+ doCloseDocument();
+ auto openResult = loadDocumentWithPassword(newFileName, newPagesVector, QString());
+ if (openResult != Okular::Document::OpenSuccess)
+ return SwapBackingFileError;
+
+ return SwapBackingFileReloadInternalData;
+}
+
bool PDFGenerator::doCloseDocument()
{
// remove internal objects
@@ -1770,6 +1781,18 @@
pdfConv->setPDFOptions( pdfConv->pdfOptions() | Poppler::PDFConverter::WithChanges );
QMutexLocker locker( userMutex() );
+
+ QHashIterator it( annotationsHash );
+ while ( it.hasNext() )
+ {
+ it.next();
+
+ if ( it.value()->uniqueName().isEmpty() )
+ {
+ it.value()->setUniqueName( it.key()->uniqueName() );
+ }
+ }
+
bool success = pdfConv->convert();
if (!success)
{
diff --git a/part.h b/part.h
--- a/part.h
+++ b/part.h
@@ -179,7 +179,6 @@
void guiActivateEvent(KParts::GUIActivateEvent *event) override;
void displayInfoMessage( const QString &message, KMessageWidget::MessageType messageType = KMessageWidget::Information, int duration = -1 );
public:
- bool saveFile() override;
bool queryClose() override;
bool closeUrl() override;
bool closeUrl(bool promptToSave) override;
@@ -202,8 +201,7 @@
void slotNextBookmark();
void slotFindNext();
void slotFindPrev();
- void slotSaveFileAs();
- void slotSaveCopyAs();
+ bool slotSaveFileAs(bool showOkularArchiveAsDefaultFormat = false);
void slotGetNewStuff();
void slotNewConfig();
void slotShowMenu(const Okular::Page *page, const QPoint &point);
@@ -234,10 +232,11 @@
void enableLayers( bool enable );
public Q_SLOTS:
+ bool saveFile() override;
// connected to Shell action (and browserExtension), not local one
void slotPrint();
void slotFileDirty( const QString& );
- void slotDoFileDirty();
+ bool slotAttemptReload( bool oneShot = false, const QUrl &newUrl = QUrl() );
void psTransformEnded(int, QProcess::ExitStatus);
KConfigDialog * slotGeneratorPreferences();
@@ -252,6 +251,7 @@
void showMenu(const Okular::Page *page, const QPoint &point, const QString &bookmarkTitle = QString(), const Okular::DocumentViewport &vp = DocumentViewport());
bool eventFilter(QObject * watched, QEvent * event) override;
Document::OpenResult doOpenFile(const QMimeType &mime, const QString &fileNameToOpen, bool *isCompressedFile);
+ bool openUrl( const QUrl &url, bool swapInsteadOfOpening );
void setupViewerActions();
void setViewerShortcuts();
@@ -266,6 +266,19 @@
void slotRenameBookmark( const DocumentViewport &viewport );
void slotRemoveBookmark( const DocumentViewport &viewport );
void resetStartArguments();
+ void checkNativeSaveDataLoss(bool *out_wontSaveForms, bool *out_wontSaveAnnotations) const;
+
+ enum SaveAsFlag
+ {
+ NoSaveAsFlags = 0, ///< No options
+ SaveAsOkularArchive = 1 ///< Save as Okular Archive (.okular) instead of document's native format
+ };
+ Q_DECLARE_FLAGS( SaveAsFlags, SaveAsFlag )
+
+ bool saveAs( const QUrl & saveUrl, SaveAsFlags flags );
+
+ void setFileToWatch( const QString &filePath );
+ void unsetFileToWatch();
#if PURPOSE_FOUND
void slotShareActionFinished(const QJsonObject &output, int error, const QString &message);
@@ -279,11 +292,14 @@
Okular::Document * m_document;
QString m_temporaryLocalFile;
bool isDocumentArchive;
+ bool m_documentOpenWithPassword;
+ bool m_swapInsteadOfOpening; // if set, the next open operation will replace the backing file (used when reloading just saved files)
// main widgets
Sidebar *m_sidebar;
SearchWidget *m_searchWidget;
FindBar * m_findBar;
+ KMessageWidget * m_migrationMessage;
KMessageWidget * m_topMessage;
KMessageWidget * m_formsMessage;
KMessageWidget * m_infoMessage;
@@ -303,6 +319,7 @@
// document watcher (and reloader) variables
KDirWatch *m_watcher;
+ QString m_watchedFilePath, m_watchedFileSymlinkTarget;
QTimer *m_dirtyHandler;
QUrl m_oldUrl;
Okular::DocumentViewport m_viewportDirty;
@@ -334,6 +351,7 @@
QAction *m_find;
QAction *m_findNext;
QAction *m_findPrev;
+ QAction *m_save;
QAction *m_saveAs;
QAction *m_saveCopyAs;
QAction *m_printPreview;
diff --git a/part.cpp b/part.cpp
--- a/part.cpp
+++ b/part.cpp
@@ -300,7 +300,7 @@
QObject *parent,
const QVariantList &args)
: KParts::ReadWritePart(parent),
-m_tempfile( nullptr ), m_isReloading( false ), m_fileWasRemoved( false ), m_showMenuBarAction( nullptr ), m_showFullScreenAction( nullptr ), m_actionsSearched( false ),
+m_tempfile( nullptr ), m_documentOpenWithPassword( false ), m_swapInsteadOfOpening( false ), m_isReloading( false ), m_fileWasRemoved( false ), m_showMenuBarAction( nullptr ), m_showFullScreenAction( nullptr ), m_actionsSearched( false ),
m_cliPresentation(false), m_cliPrint(false), m_embedMode(detectEmbedMode(parentWidget, parent, args)), m_generatorGuiClient(nullptr), m_keeper( nullptr )
{
// make sure that the component name is okular otherwise the XMLGUI .rc files are not found
@@ -389,6 +389,13 @@
connect( m_document, &Document::openUrl, this, &Part::openUrlFromDocument );
connect( m_document->bookmarkManager(), &BookmarkManager::openUrl, this, &Part::openUrlFromBookmarks );
connect( m_document, &Document::close, this, &Part::close );
+ connect( m_document, &Document::undoHistoryCleanChanged, this,
+ [this](bool clean)
+ {
+ setModified( !clean );
+ setWindowTitleFromDocument();
+ }
+ );
if ( parent && parent->metaObject()->indexOfSlot( QMetaObject::normalizedSignature( "slotQuit()" ).constData() ) != -1 )
connect( m_document, SIGNAL(quit()), parent, SLOT(slotQuit()) );
@@ -465,6 +472,12 @@
rightLayout->setSpacing( 0 );
// KToolBar * rtb = new KToolBar( rightContainer, "mainToolBarSS" );
// rightLayout->addWidget( rtb );
+ m_migrationMessage = new KMessageWidget( rightContainer );
+ m_migrationMessage->setVisible( false );
+ m_migrationMessage->setWordWrap( true );
+ m_migrationMessage->setMessageType( KMessageWidget::Warning );
+ m_migrationMessage->setText( i18n( "This document contains annotations or form data that were saved internally by a previous Okular version. Internal storage is no longer supported.
Please save to a file in order to move them if you want to continue to edit the document." ) );
+ rightLayout->addWidget( m_migrationMessage );
m_topMessage = new KMessageWidget( rightContainer );
m_topMessage->setVisible( false );
m_topMessage->setWordWrap( true );
@@ -554,9 +567,10 @@
m_watcher = new KDirWatch( this );
connect( m_watcher, &KDirWatch::dirty, this, &Part::slotFileDirty );
connect( m_watcher, &KDirWatch::created, this, &Part::slotFileDirty );
+ connect( m_watcher, &KDirWatch::deleted, this, &Part::slotFileDirty );
m_dirtyHandler = new QTimer( this );
m_dirtyHandler->setSingleShot( true );
- connect( m_dirtyHandler, &QTimer::timeout,this, &Part::slotDoFileDirty );
+ connect( m_dirtyHandler, &QTimer::timeout, this, [this] { slotAttemptReload(); } );
slotNewConfig();
@@ -698,7 +712,7 @@
m_findPrev = KStandardAction::findPrev( this, SLOT(slotFindPrev()), ac );
m_findPrev->setEnabled( false );
- m_saveCopyAs = nullptr;
+ m_save = nullptr;
m_saveAs = nullptr;
QAction * prefs = KStandardAction::preferences( this, SLOT(slotPreferences()), ac);
@@ -807,15 +821,12 @@
m_selectAll = KStandardAction::selectAll( m_pageView, SLOT(selectAll()), ac );
- m_saveCopyAs = KStandardAction::saveAs( this, SLOT(slotSaveCopyAs()), ac );
- m_saveCopyAs->setText( i18n( "Save &Copy As..." ) );
- ac->addAction( QStringLiteral("file_save_copy"), m_saveCopyAs );
- ac->setDefaultShortcuts(m_saveCopyAs, KStandardShortcut::shortcut(KStandardShortcut::SaveAs));
- m_saveCopyAs->setEnabled( false );
+ m_save = KStandardAction::save( this, [this] { saveFile(); }, ac );
+ m_save->setEnabled( false );
m_saveAs = KStandardAction::saveAs( this, SLOT(slotSaveFileAs()), ac );
- ac->setDefaultShortcuts(m_saveAs, KStandardShortcut::shortcut(KStandardShortcut::Save));
m_saveAs->setEnabled( false );
+ m_migrationMessage->addAction( m_saveAs );
m_showLeftPanel = ac->add(QStringLiteral("show_leftpanel"));
m_showLeftPanel->setText(i18n( "Show &Navigation Panel"));
@@ -847,11 +858,6 @@
m_exportAsMenu->addAction( m_exportAsText );
m_exportAs->setEnabled( false );
m_exportAsText->setEnabled( false );
- m_exportAsDocArchive = actionForExportFormat( Okular::ExportFormat(
- i18nc( "A document format, Okular-specific", "Document Archive" ),
- db.mimeTypeForName( QStringLiteral("application/vnd.kde.okular-archive") ) ), m_exportAsMenu );
- m_exportAsMenu->addAction( m_exportAsDocArchive );
- m_exportAsDocArchive->setEnabled( false );
#if PURPOSE_FOUND
m_share = ac->addAction( QStringLiteral("file_share") );
@@ -1120,7 +1126,7 @@
emit setWindowCaption( QString() );
resetStartArguments();
- // when m_viewportDirty.pageNumber != -1 we come from slotDoFileDirty
+ // when m_viewportDirty.pageNumber != -1 we come from slotAttemptReload
// so we don't want to show an ugly messagebox just because the document is
// taking more than usual to be recreated
if (m_viewportDirty.pageNumber == -1)
@@ -1178,6 +1184,11 @@
void Part::notifySetup( const QVector< Okular::Page * > & /*pages*/, int setupFlags )
{
+ // Hide the migration message if the user has just migrated. Otherwise,
+ // if m_migrationMessage is already hidden, this does nothing.
+ if ( !m_document->isDocdataMigrationNeeded() )
+ m_migrationMessage->animatedHide();
+
if ( !( setupFlags & Okular::DocumentObserver::DocumentChanged ) )
return;
@@ -1194,9 +1205,6 @@
void Part::notifyPageChanged( int page, int flags )
{
- if ( flags & Okular::DocumentObserver::NeedSaveAs )
- setModified();
-
if ( !(flags & Okular::DocumentObserver::Bookmark ) )
return;
@@ -1281,12 +1289,39 @@
return false;
}
-static void addFileToWatcher( KDirWatch *watcher, const QString &filePath)
+void Part::setFileToWatch( const QString &filePath )
{
- if ( !watcher->contains( filePath ) ) watcher->addFile(filePath);
+ if ( !m_watchedFilePath.isEmpty() )
+ unsetFileToWatch();
+
const QFileInfo fi(filePath);
- if ( !watcher->contains( fi.absolutePath() ) ) watcher->addDir(fi.absolutePath());
- if ( fi.isSymLink() ) watcher->addFile( fi.readLink() );
+
+ m_watchedFilePath = filePath;
+ m_watcher->addFile( m_watchedFilePath );
+
+ if ( fi.isSymLink() )
+ {
+ m_watchedFileSymlinkTarget = fi.readLink();
+ m_watcher->addFile( m_watchedFileSymlinkTarget );
+ }
+ else
+ {
+ m_watchedFileSymlinkTarget.clear();
+ }
+}
+
+void Part::unsetFileToWatch()
+{
+ if ( m_watchedFilePath.isEmpty() )
+ return;
+
+ m_watcher->removeFile( m_watchedFilePath );
+
+ if ( !m_watchedFileSymlinkTarget.isEmpty() )
+ m_watcher->removeFile( m_watchedFileSymlinkTarget );
+
+ m_watchedFilePath.clear();
+ m_watchedFileSymlinkTarget.clear();
}
Document::OpenResult Part::doOpenFile( const QMimeType &mimeA, const QString &fileNameToOpenA, bool *isCompressedFile )
@@ -1308,6 +1343,29 @@
*isCompressedFile = false;
}
+ if ( m_swapInsteadOfOpening )
+ {
+ m_swapInsteadOfOpening = false;
+
+ if ( !uncompressOk )
+ return Document::OpenError;
+
+ if ( mime.inherits( QStringLiteral("application/vnd.kde.okular-archive") ) )
+ {
+ isDocumentArchive = true;
+ if (!m_document->swapBackingFileArchive( fileNameToOpen, url() ))
+ return Document::OpenError;
+ }
+ else
+ {
+ isDocumentArchive = false;
+ if (!m_document->swapBackingFile( fileNameToOpen, url() ))
+ return Document::OpenError;
+ }
+
+ return Document::OpenSuccess;
+ }
+
isDocumentArchive = false;
if ( uncompressOk )
{
@@ -1320,6 +1378,7 @@
{
openResult = m_document->openDocument( fileNameToOpen, url(), mime );
}
+ m_documentOpenWithPassword = false;
// if the file didn't open correctly it might be encrypted, so ask for a pass
QString walletName, walletFolder, walletKey;
@@ -1384,10 +1443,15 @@
openResult = m_document->openDocument( fileNameToOpen, url(), mime, password );
}
- // 3. if the password is correct and the user chose to remember it, store it to the wallet
- if ( openResult == Document::OpenSuccess && wallet && /*safety check*/ wallet->isOpen() && keep )
+ if ( openResult == Document::OpenSuccess )
{
- wallet->writePassword( walletKey, password );
+ m_documentOpenWithPassword = true;
+
+ // 3. if the password is correct and the user chose to remember it, store it to the wallet
+ if (wallet && /*safety check*/ wallet->isOpen() && keep )
+ {
+ wallet->writePassword( walletKey, password );
+ }
}
}
}
@@ -1452,14 +1516,15 @@
m_find->setEnabled( ok && canSearch );
m_findNext->setEnabled( ok && canSearch );
m_findPrev->setEnabled( ok && canSearch );
- if( m_saveAs ) m_saveAs->setEnabled( ok && (m_document->canSaveChanges() || isDocumentArchive) );
- if( m_saveCopyAs ) m_saveCopyAs->setEnabled( ok );
+ if( m_save ) m_save->setEnabled( ok && !( isstdin || mime.inherits( "inode/directory" ) ) );
+ if( m_saveAs ) m_saveAs->setEnabled( ok && !( isstdin || mime.inherits( "inode/directory" ) ) );
emit enablePrintAction( ok && m_document->printingSupport() != Okular::Document::NoPrinting );
m_printPreview->setEnabled( ok && m_document->printingSupport() != Okular::Document::NoPrinting );
m_showProperties->setEnabled( ok );
bool hasEmbeddedFiles = ok && m_document->embeddedFiles() && m_document->embeddedFiles()->count() > 0;
if ( m_showEmbeddedFiles ) m_showEmbeddedFiles->setEnabled( hasEmbeddedFiles );
m_topMessage->setVisible( hasEmbeddedFiles && Okular::Settings::showOSD() );
+ m_migrationMessage->setVisible( m_document->isDocdataMigrationNeeded() );
// Warn the user that XFA forms are not supported yet (NOTE: poppler generator only)
if ( ok && m_document->metaData( QStringLiteral("HasUnsupportedXfaForm") ).toBool() == true )
@@ -1516,7 +1581,6 @@
#endif
}
if ( m_exportAsText ) m_exportAsText->setEnabled( ok && m_document->canExportToText() );
- if ( m_exportAsDocArchive ) m_exportAsDocArchive->setEnabled( ok );
if ( m_exportAs ) m_exportAs->setEnabled( ok );
#if PURPOSE_FOUND
if ( m_share ) m_share->setEnabled( ok );
@@ -1538,9 +1602,7 @@
// set the file to the fileWatcher
if ( url().isLocalFile() )
- {
- addFileToWatcher( m_watcher, localFilePath() );
- }
+ setFileToWatch( localFilePath() );
// if the 'OpenTOC' flag is set, open the TOC
if ( m_document->metaData( QStringLiteral("OpenTOC") ).toBool() && m_sidebar->isItemEnabled( m_toc ) && !m_sidebar->isCollapsed() && m_sidebar->currentItem() != m_toc )
@@ -1578,8 +1640,17 @@
return true;
}
-bool Part::openUrl(const QUrl &_url)
+bool Part::openUrl( const QUrl &url )
+{
+ return openUrl( url, false /* swapInsteadOfOpening */ );
+}
+
+bool Part::openUrl( const QUrl &_url, bool swapInsteadOfOpening )
{
+ /* Store swapInsteadOfOpening, so that closeUrl and openFile will be able
+ * to read it */
+ m_swapInsteadOfOpening = swapInsteadOfOpening;
+
// Close current document if any
if ( !closeUrl() )
return false;
@@ -1630,15 +1701,15 @@
return true;
const int res = KMessageBox::warningYesNoCancel( widget(),
- i18n( "Do you want to save your annotation changes or discard them?" ),
+ i18n( "Do you want to save your changes to \"%1\" or discard them?", url().toDisplayString() ),
i18n( "Close Document" ),
- KStandardGuiItem::saveAs(),
+ KStandardGuiItem::save(),
KStandardGuiItem::discard() );
switch ( res )
{
- case KMessageBox::Yes: // Save as
- slotSaveFileAs();
+ case KMessageBox::Yes: // Save
+ saveFile();
return !isModified(); // Only allow closing if file was really saved
case KMessageBox::No: // Discard
return true;
@@ -1652,7 +1723,12 @@
if ( promptToSave && !queryClose() )
return false;
- setModified( false );
+ if ( m_swapInsteadOfOpening )
+ {
+ // If we're swapping the backing file, we don't want to close the
+ // current one when openUrl() calls us internally
+ return true; // pretend it worked
+ }
if (!m_temporaryLocalFile.isNull() && m_temporaryLocalFile != localFilePath())
{
@@ -1665,21 +1741,20 @@
m_find->setEnabled( false );
m_findNext->setEnabled( false );
m_findPrev->setEnabled( false );
+ if( m_save ) m_save->setEnabled( false );
if( m_saveAs ) m_saveAs->setEnabled( false );
- if( m_saveCopyAs ) m_saveCopyAs->setEnabled( false );
m_printPreview->setEnabled( false );
m_showProperties->setEnabled( false );
if ( m_showEmbeddedFiles ) m_showEmbeddedFiles->setEnabled( false );
if ( m_exportAs ) m_exportAs->setEnabled( false );
if ( m_exportAsText ) m_exportAsText->setEnabled( false );
- if ( m_exportAsDocArchive ) m_exportAsDocArchive->setEnabled( false );
m_exportFormats.clear();
if ( m_exportAs )
{
QMenu *menu = m_exportAs->menu();
QList acts = menu->actions();
int num = acts.count();
- for ( int i = 2; i < num; ++i )
+ for ( int i = 1; i < num; ++i )
{
menu->removeAction( acts.at(i) );
delete acts.at(i);
@@ -1697,12 +1772,7 @@
emit enablePrintAction(false);
m_realUrl = QUrl();
if ( url().isLocalFile() )
- {
- m_watcher->removeFile( localFilePath() );
- QFileInfo fi(localFilePath());
- m_watcher->removeDir( fi.absolutePath() );
- if ( fi.isSymLink() ) m_watcher->removeFile( fi.readLink() );
- }
+ unsetFileToWatch();
m_fileWasRemoved = false;
if ( m_generatorGuiClient )
factory()->removeClient( m_generatorGuiClient );
@@ -1714,6 +1784,7 @@
if ( widget() )
{
m_searchWidget->clearText();
+ m_migrationMessage->setVisible( false );
m_topMessage->setVisible( false );
m_formsMessage->setVisible( false );
}
@@ -1802,8 +1873,8 @@
else if (m_fileWasRemoved && QFile::exists(localFilePath()))
{
// we need to watch the new file
- m_watcher->removeFile(localFilePath());
- m_watcher->addFile(localFilePath());
+ unsetFileToWatch();
+ setFileToWatch( localFilePath() );
m_dirtyHandler->start( 750 );
}
}
@@ -1817,12 +1888,12 @@
}
}
-
-void Part::slotDoFileDirty()
+// Attempt to reload the document, one or more times, optionally from a different URL
+bool Part::slotAttemptReload( bool oneShot, const QUrl &newUrl )
{
// Skip reload when another reload is already in progress
if ( m_isReloading ) {
- return;
+ return false;
}
QScopedValueRollback rollback(m_isReloading, true);
@@ -1832,7 +1903,7 @@
if ( m_viewportDirty.pageNumber == -1 )
{
// store the url of the current document
- m_oldUrl = url();
+ m_oldUrl = newUrl.isEmpty() ? url() : newUrl;
// store the current viewport
m_viewportDirty = m_document->viewport();
@@ -1866,7 +1937,7 @@
{
m_toc->rollbackReload();
}
- return;
+ return false;
}
if ( tocReloadPrepared )
@@ -1875,6 +1946,8 @@
// inform the user about the operation in progress
m_pageView->displayMessage( i18n("Reloading the document...") );
+ bool reloadSucceeded = false;
+
if ( KParts::ReadWritePart::openUrl( m_oldUrl ) )
{
// on successful opening, restore the previous viewport
@@ -1899,13 +1972,17 @@
}
if (m_wasPresentationOpen) slotShowPresentation();
emit enablePrintAction(true && m_document->printingSupport() != Okular::Document::NoPrinting);
+
+ reloadSucceeded = true;
}
- else
+ else if ( !oneShot )
{
- // start watching the file again (since we dropped it on close)
- addFileToWatcher( m_watcher, localFilePath() );
+ // start watching the file again (since we dropped it on close)
+ setFileToWatch( localFilePath() );
m_dirtyHandler->start( 750 );
}
+
+ return reloadSucceeded;
}
@@ -2312,56 +2389,80 @@
bool Part::saveFile()
{
- qCDebug(OkularUiDebug) << "Okular part doesn't support saving the file in the location from which it was opened";
- return false;
+ if ( !isModified() )
+ return true;
+ else
+ return saveAs( url() );
}
-void Part::slotSaveFileAs()
+bool Part::slotSaveFileAs( bool showOkularArchiveAsDefaultFormat )
{
if ( m_embedMode == PrintPreviewMode )
- return;
+ return false;
- /* Show a warning before saving if the generator can't save annotations,
- * unless we are going to save a .okular archive. */
- if ( !isDocumentArchive && !m_document->canSaveChanges( Document::SaveAnnotationsCapability ) )
- {
- /* Search local annotations */
- bool containsLocalAnnotations = false;
- const int pagecount = m_document->pages();
+ // Determine the document's mimetype
+ QMimeDatabase db;
+ QMimeType originalMimeType;
+ const QString typeName = m_document->documentInfo().get( DocumentInfo::MimeType );
+ if ( !typeName.isEmpty() )
+ originalMimeType = db.mimeTypeForName( typeName );
- for ( int pageno = 0; pageno < pagecount; ++pageno )
- {
- const Okular::Page *page = m_document->page( pageno );
- foreach ( const Okular::Annotation *ann, page->annotations() )
- {
- if ( !(ann->flags() & Okular::Annotation::External) )
- {
- containsLocalAnnotations = true;
- break;
- }
- }
- if ( containsLocalAnnotations )
- break;
- }
+ // What data would we lose if we saved natively?
+ bool wontSaveForms, wontSaveAnnotations;
+ checkNativeSaveDataLoss(&wontSaveForms, &wontSaveAnnotations);
- /* Don't show it if there are no local annotations */
- if ( containsLocalAnnotations )
- {
- int res = KMessageBox::warningContinueCancel( widget(), i18n("Your annotations will not be exported.\nYou can export the annotated document using File -> Export As -> Document Archive") );
- if ( res != KMessageBox::Continue )
- return; // Canceled
- }
- }
+ const QMimeType okularArchiveMimeType = db.mimeTypeForName( QStringLiteral("application/vnd.kde.okular-archive") );
+
+ // Prepare "Save As" dialog
+ const QString originalMimeTypeFilter = i18nc("File type name and pattern", "%1 (%2)", originalMimeType.comment(), originalMimeType.globPatterns().join(QLatin1Char(' ')));
+ const QString okularArchiveMimeTypeFilter = i18nc("File type name and pattern", "%1 (%2)", okularArchiveMimeType.comment(), okularArchiveMimeType.globPatterns().join(QLatin1Char(' ')));
+
+ // What format choice should we show as default?
+ QString selectedFilter = (isDocumentArchive || showOkularArchiveAsDefaultFormat ||
+ wontSaveForms || wontSaveAnnotations) ?
+ okularArchiveMimeTypeFilter : originalMimeTypeFilter;
+
+ QString filter = originalMimeTypeFilter + QStringLiteral(";;") + okularArchiveMimeTypeFilter;
+
+ const QUrl saveUrl = QFileDialog::getSaveFileUrl(widget(), i18n("Save As"), url(), filter, &selectedFilter);
- QUrl saveUrl = QFileDialog::getSaveFileUrl( widget(), QString(), url() );
if ( !saveUrl.isValid() || saveUrl.isEmpty() )
- return;
+ return false;
- saveAs( saveUrl );
+ // Has the user chosen to save in .okular archive format?
+ const bool saveAsOkularArchive = ( selectedFilter == okularArchiveMimeTypeFilter );
+
+ return saveAs( saveUrl, saveAsOkularArchive ? SaveAsOkularArchive : NoSaveAsFlags );
}
-bool Part::saveAs( const QUrl & saveUrl )
+bool Part::saveAs(const QUrl & saveUrl)
{
+ // Save in the same format (.okular vs native) as the current file
+ return saveAs( saveUrl, isDocumentArchive ? SaveAsOkularArchive : NoSaveAsFlags );
+}
+
+bool Part::saveAs( const QUrl & saveUrl, SaveAsFlags flags )
+{
+ bool hasUserAcceptedReload = false;
+ if ( m_documentOpenWithPassword )
+ {
+ const int res = KMessageBox::warningYesNo( widget(),
+ i18n( "The current document is protected with a password.
In order to save, the file needs to be reloaded. You will be asked for the password again and your undo/redo history will be lost.
Do you want to continue?" ),
+ i18n( "Save - Warning" ) );
+
+ switch ( res )
+ {
+ case KMessageBox::Yes:
+ hasUserAcceptedReload = true;
+ // do nothing
+ break;
+ case KMessageBox::No: // User said no to continue, so return true even if save didn't happen otherwise we will get an error
+ return true;
+ }
+ }
+
+ bool setModifiedAfterSave = false;
+
QTemporaryFile tf;
QString fileName;
if ( !tf.open() )
@@ -2372,85 +2473,261 @@
fileName = tf.fileName();
tf.close();
- QString errorText;
- bool saved;
+ QScopedPointer tempFile;
+ KIO::Job *copyJob = nullptr; // this will be filled with the job that writes to saveUrl
- if ( isDocumentArchive )
- saved = m_document->saveDocumentArchive( fileName );
- else
- saved = m_document->saveChanges( fileName, &errorText );
-
- if ( !saved )
+ // Does the user want a .okular archive?
+ if ( flags & SaveAsOkularArchive )
{
- if (errorText.isEmpty())
+ if ( !hasUserAcceptedReload && !m_document->canSwapBackingFile() )
+ {
+ const int res = KMessageBox::warningYesNo( widget(),
+ i18n( "The current document format backend doesn't support internal reload on save so we will close and open the file again.
This means that the undo/redo stack will be lost.
Do you want to continue?" ),
+ i18n( "Save - Warning" ) );
+
+ switch ( res )
+ {
+ case KMessageBox::Yes:
+ // do nothing
+ break;
+ case KMessageBox::No: // User said no to continue, so return true even if save didn't happen otherwise we will get an error
+ return true;
+ }
+ }
+
+ if ( !m_document->saveDocumentArchive( fileName ) )
{
KMessageBox::information( widget(), i18n("File could not be saved in '%1'. Try to save it to another location.", fileName ) );
+ return false;
+ }
+
+ copyJob = KIO::file_copy( QUrl::fromLocalFile( fileName ), saveUrl, -1, KIO::Overwrite );
+ }
+ else
+ {
+ bool wontSaveForms, wontSaveAnnotations;
+ checkNativeSaveDataLoss(&wontSaveForms, &wontSaveAnnotations);
+
+ // If something can't be saved in this format, ask for confirmation
+ QStringList listOfwontSaves;
+ if ( wontSaveForms ) listOfwontSaves << i18n( "Filled form contents" );
+ if ( wontSaveAnnotations ) listOfwontSaves << i18n( "User annotations" );
+ if ( !listOfwontSaves.isEmpty() )
+ {
+ const QString warningMessage = m_document->canSwapBackingFile() ?
+ i18n( "You are about to save changes, but the current file format does not support saving the following elements. Please use the Okular document archive format to preserve them. Click Continue to save ignoring these elements." ) :
+ i18n( "You are about to save changes, but the current file format does not support saving the following elements. Please use the Okular document archive format to preserve them. Click Continue to save but you will lose these elements (as well as the undo/redo history)." );
+ const QString continueMessage = m_document->canSwapBackingFile() ?
+ i18n( "Continue" ) :
+ i18n( "Continue losing changes" );
+ const int result = KMessageBox::warningYesNoCancelList( widget(),
+ warningMessage,
+ listOfwontSaves, i18n( "Warning" ),
+ KGuiItem( i18n( "Save as Okular document archive..." ), "document-save-as" ), // <- KMessageBox::Yes
+ KGuiItem( continueMessage, "arrow-right" ) ); // <- KMessageBox::NO
+
+ switch (result)
+ {
+ case KMessageBox::Yes: // -> Save as Okular document archive
+ return slotSaveFileAs( true /* showOkularArchiveAsDefaultFormat */ );
+ case KMessageBox::No: // -> Continue
+ setModifiedAfterSave = m_document->canSwapBackingFile();
+ break;
+ case KMessageBox::Cancel:
+ return false;
+ }
+ }
+
+ if ( m_document->canSaveChanges() )
+ {
+ // If the generator supports saving changes, save them
+
+ QString errorText;
+ if ( !m_document->saveChanges( fileName, &errorText ) )
+ {
+ if (errorText.isEmpty())
+ KMessageBox::information( widget(), i18n("File could not be saved in '%1'. Try to save it to another location.", fileName ) );
+ else
+ KMessageBox::information( widget(), i18n("File could not be saved in '%1'. %2", fileName, errorText ) );
+
+ return false;
+ }
+
+ copyJob = KIO::file_copy( QUrl::fromLocalFile( fileName ), saveUrl, -1, KIO::Overwrite );
}
else
{
- KMessageBox::information( widget(), i18n("File could not be saved in '%1'. %2", fileName, errorText ) );
+ // If the generators doesn't support saving changes, we will
+ // just copy the original file.
+
+ if ( isDocumentArchive )
+ {
+ // Special case: if the user is extracting the contents of a
+ // .okular archive back to the native format, we can't just copy
+ // the open file (which is a .okular). So let's ask to core to
+ // extract and give us the real file
+
+ if ( !m_document->extractArchivedFile( fileName ) )
+ {
+ KMessageBox::information( widget(), i18n("File could not be saved in '%1'. Try to save it to another location.", fileName ) );
+ return false;
+ }
+
+ copyJob = KIO::file_copy( QUrl::fromLocalFile( fileName ), saveUrl, -1, KIO::Overwrite );
+ }
+ else
+ {
+ // Otherwise just copy the open file.
+ // make use of the already downloaded (in case of remote URLs) file,
+ // no point in downloading that again
+ QUrl srcUrl = QUrl::fromLocalFile( localFilePath() );
+ // duh, our local file disappeared...
+ if ( !QFile::exists( localFilePath() ) )
+ {
+ if ( url().isLocalFile() )
+ {
+#ifdef OKULAR_KEEP_FILE_OPEN
+ // local file: try to get it back from the open handle on it
+ tempFile.reset( m_keeper->copyToTemporary() );
+ if ( tempFile )
+ srcUrl = KUrl::fromPath( tempFile->fileName() );
+#else
+ const QString msg = i18n( "Okular cannot copy %1 to the specified location.\n\nThe document does not exist anymore.", localFilePath() );
+ KMessageBox::sorry( widget(), msg );
+ return false;
+#endif
+ }
+ else
+ {
+ // we still have the original remote URL of the document,
+ // so copy the document from there
+ srcUrl = url();
+ }
+ }
+
+ if ( srcUrl != saveUrl )
+ {
+ copyJob = KIO::file_copy( srcUrl, saveUrl, -1, KIO::Overwrite );
+ }
+ else
+ {
+ // Don't do a real copy in this case, just update the timestamps
+ copyJob = KIO::setModificationTime( saveUrl, QDateTime::currentDateTime() );
+ }
+ }
}
- return false;
}
- KIO::Job *copyJob = KIO::file_copy( QUrl::fromLocalFile(fileName), saveUrl, -1, KIO::Overwrite );
+ // Stop watching for changes while we write the new file (useful when
+ // overwriting)
+ if ( url().isLocalFile() )
+ unsetFileToWatch();
+
KJobWidgets::setWindow(copyJob, widget());
if ( !copyJob->exec() )
{
- KMessageBox::information( widget(), i18n("File could not be saved in '%1'. Try to save it to another location.", saveUrl.toDisplayString() ) );
+ KMessageBox::information( widget(), i18n("File could not be saved in '%1'. Error: '%2'. Try to save it to another location.", saveUrl.toDisplayString(), copyJob->errorString() ) );
+
+ // Restore watcher
+ if ( url().isLocalFile() )
+ setFileToWatch( localFilePath() );
+
return false;
}
- setModified( false );
- return true;
-}
-
+ m_document->setHistoryClean( true );
-void Part::slotSaveCopyAs()
-{
- if ( m_embedMode == PrintPreviewMode )
- return;
+ if ( m_document->isDocdataMigrationNeeded() )
+ m_document->docdataMigrationDone();
- QUrl saveUrl = QFileDialog::getSaveFileUrl( widget(), QString(), url());
+ bool reloadedCorrectly = true;
- if ( saveUrl.isValid() && !saveUrl.isEmpty() )
+ // Make the generator use the new new file instead of the old one
+ if ( m_document->canSwapBackingFile() && !m_documentOpenWithPassword )
{
- // make use of the already downloaded (in case of remote URLs) file,
- // no point in downloading that again
- QUrl srcUrl = QUrl::fromLocalFile( localFilePath() );
- QTemporaryFile * tempFile = nullptr;
- // duh, our local file disappeared...
- if ( !QFile::exists( localFilePath() ) )
+ // this calls openFile internally, which in turn actually calls
+ // m_document->swapBackingFile() instead of the regular loadDocument
+ if ( openUrl( saveUrl, true /* swapInsteadOfOpening */ ) )
{
- if ( url().isLocalFile() )
+ if ( setModifiedAfterSave )
{
-#ifdef OKULAR_KEEP_FILE_OPEN
- // local file: try to get it back from the open handle on it
- if ( ( tempFile = m_keeper->copyToTemporary() ) )
- srcUrl = QUrl::fromLocalFile( tempFile->fileName() );
-#else
- const QString msg = i18n( "Okular cannot copy %1 to the specified location.\n\nThe document does not exist anymore.", localFilePath() );
- KMessageBox::sorry( widget(), msg );
- return;
-#endif
+ m_document->setHistoryClean( false );
}
- else
+ }
+ else
+ {
+ reloadedCorrectly = false;
+ }
+ }
+ else
+ {
+ // If the generator doesn't support swapping file, then just reload
+ // the document from the new location
+ if ( !slotAttemptReload( true, saveUrl ) )
+ reloadedCorrectly = false;
+ }
+
+ // In case of file swapping errors, close the document to avoid inconsistencies
+ if ( !reloadedCorrectly )
+ {
+ qWarning() << "The document hasn't been reloaded/swapped correctly";
+ closeUrl();
+ }
+
+ // Restore watcher
+ if ( url().isLocalFile() )
+ setFileToWatch( localFilePath() );
+
+ return true;
+}
+
+// If the user wants to save in the original file's format, some features might
+// not be available. Find out what cannot be saved in this format
+void Part::checkNativeSaveDataLoss(bool *out_wontSaveForms, bool *out_wontSaveAnnotations) const
+{
+ bool wontSaveForms = false;
+ bool wontSaveAnnotations = false;
+
+ if ( !m_document->canSaveChanges( Document::SaveFormsCapability ) )
+ {
+ /* Set wontSaveForms only if there are forms */
+ const int pagecount = m_document->pages();
+
+ for ( int pageno = 0; pageno < pagecount; ++pageno )
+ {
+ const Okular::Page *page = m_document->page( pageno );
+ if ( !page->formFields().empty() )
{
- // we still have the original remote URL of the document,
- // so copy the document from there
- srcUrl = url();
+ wontSaveForms = true;
+ break;
}
}
+ }
- KIO::Job *copyJob = KIO::file_copy( srcUrl, saveUrl, -1, KIO::Overwrite );
- KJobWidgets::setWindow(copyJob, widget());
- if ( !copyJob->exec() )
- KMessageBox::information( widget(), i18n("File could not be saved in '%1'. Try to save it to another location.", saveUrl.toDisplayString() ) );
+ if ( !m_document->canSaveChanges( Document::SaveAnnotationsCapability ) )
+ {
+ /* Set wontSaveAnnotations only if there are local annotations */
+ const int pagecount = m_document->pages();
- delete tempFile;
+ for ( int pageno = 0; pageno < pagecount; ++pageno )
+ {
+ const Okular::Page *page = m_document->page( pageno );
+ foreach ( const Okular::Annotation *ann, page->annotations() )
+ {
+ if ( !(ann->flags() & Okular::Annotation::External) )
+ {
+ wontSaveAnnotations = true;
+ break;
+ }
+ }
+ if ( wontSaveAnnotations )
+ break;
+ }
}
-}
+ *out_wontSaveForms = wontSaveForms;
+ *out_wontSaveAnnotations = wontSaveAnnotations;
+}
void Part::slotGetNewStuff()
{
@@ -2763,9 +3040,6 @@
case 0:
mimeType = mimeDatabase.mimeTypeForName(QStringLiteral("text/plain"));
break;
- case 1:
- mimeType = mimeDatabase.mimeTypeForName(QStringLiteral("application/vnd.kde.okular-archive"));
- break;
default:
mimeType = m_exportFormats.at( id - 2 ).mimeType();
break;
@@ -2782,11 +3056,8 @@
case 0:
saved = m_document->exportToText( fileName );
break;
- case 1:
- saved = m_document->saveDocumentArchive( fileName );
- break;
default:
- saved = m_document->exportTo( fileName, m_exportFormats.at( id - 2 ) );
+ saved = m_document->exportTo( fileName, m_exportFormats.at( id - 1 ) );
break;
}
if ( !saved )
@@ -2801,7 +3072,7 @@
// auto-refresh system
m_dirtyHandler->stop();
- slotDoFileDirty();
+ slotAttemptReload();
}
diff --git a/part.rc b/part.rc
--- a/part.rc
+++ b/part.rc
@@ -1,11 +1,11 @@
-
+