From a20afdff7da16e85d3ec8eced66d9a8fac29dbe7 Mon Sep 17 00:00:00 2001 From: Fabio D'Urso Date: Fri, 9 May 2014 17:04:48 +0200 Subject: [PATCH 01/78] "Save Copy As" removed, make it possible to create .okular files with "Save As" --- part.cpp | 282 ++++++++++++++++++++++++++++++++++--------------------- part.h | 7 +- part.rc | 1 - 3 files changed, 179 insertions(+), 111 deletions(-) diff --git a/part.cpp b/part.cpp index d9f168202..fd0a92845 100644 --- a/part.cpp +++ b/part.cpp @@ -654,7 +654,6 @@ void Part::setupViewerActions() m_findPrev = KStandardAction::findPrev( this, SLOT(slotFindPrev()), ac ); m_findPrev->setEnabled( false ); - m_saveCopyAs = 0; m_saveAs = 0; QAction * prefs = KStandardAction::preferences( this, SLOT(slotPreferences()), ac); @@ -752,12 +751,6 @@ void Part::setupActions() m_selectAll = KStandardAction::selectAll( m_pageView, SLOT(selectAll()), ac ); - m_saveCopyAs = KStandardAction::saveAs( this, SLOT(slotSaveCopyAs()), ac ); - m_saveCopyAs->setText( i18n( "Save &Copy As..." ) ); - m_saveCopyAs->setShortcut( KShortcut() ); - ac->addAction( "file_save_copy", m_saveCopyAs ); - m_saveCopyAs->setEnabled( false ); - m_saveAs = KStandardAction::saveAs( this, SLOT(slotSaveFileAs()), ac ); m_saveAs->setEnabled( false ); @@ -1340,8 +1333,7 @@ bool Part::openFile() 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_saveAs ) m_saveAs->setEnabled( ok ); emit enablePrintAction( ok && m_document->printingSupport() != Okular::Document::NoPrinting ); m_printPreview->setEnabled( ok && m_document->printingSupport() != Okular::Document::NoPrinting ); m_showProperties->setEnabled( ok ); @@ -1540,7 +1532,6 @@ bool Part::closeUrl(bool promptToSave) m_findNext->setEnabled( false ); m_findPrev->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 ); @@ -2138,53 +2129,59 @@ bool Part::saveFile() return false; } -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 ) ) + // Determine the document's mimetype + KMimeType::Ptr originalMimeType; + if ( const Okular::DocumentInfo *documentInfo = m_document->documentInfo() ) { - /* Search local annotations */ - bool containsLocalAnnotations = false; - const int pagecount = m_document->pages(); - - for ( int pageno = 0; pageno < pagecount; ++pageno ) - { - const Okular::Page *page = m_document->page( pageno ); - foreach ( const Okular::Annotation *ann, page->annotations() ) - { - if ( !(ann->flags() & Okular::Annotation::External) ) - { - containsLocalAnnotations = true; - break; - } - } - if ( containsLocalAnnotations ) - break; - } - - /* 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 - } + QString typeName = documentInfo->get("mimeType"); + if ( !typeName.isEmpty() ) + originalMimeType = KMimeType::mimeType( typeName ); } - KUrl saveUrl = KFileDialog::getSaveUrl( url(), - QString(), widget(), QString(), - KFileDialog::ConfirmOverwrite ); - if ( !saveUrl.isValid() || saveUrl.isEmpty() ) - return; + // What format choice should we show as default? + const QString defaultMimeType = (isDocumentArchive || showOkularArchiveAsDefaultFormat) ? + "application/vnd.kde.okular-archive" : originalMimeType->name(); - saveAs( saveUrl ); + // Prepare "Save As" dialog + KFileDialog fd( url(), QString(), widget() ); + fd.setWindowTitle( i18n("Save As") ); + fd.setOperationMode( KFileDialog::Saving ); + fd.setMode( KFile::File ); + fd.setConfirmOverwrite( true ); + fd.setMimeFilter( + QStringList() << originalMimeType->name() << "application/vnd.kde.okular-archive", + defaultMimeType ); + + // Show it + if ( fd.exec() == QDialog::Accepted ) + { + const KUrl saveUrl = fd.selectedUrl(); + if ( !saveUrl.isValid() || saveUrl.isEmpty() ) + return false; + + // Has the user chosen to save in .okular archive format? + const bool saveAsOkularArchive = ( fd.currentMimeFilter() == "application/vnd.kde.okular-archive" ); + + return saveAs( saveUrl, saveAsOkularArchive ); + } + else + { + return false; + } } -bool Part::saveAs( const KUrl & saveUrl ) +bool Part::saveAs(const KUrl & saveUrl) +{ + // Save in the same format (.okular vs native) as the current file + return saveAs( saveUrl, isDocumentArchive /* saveAsOkularArchive */ ); +} + +bool Part::saveAs( const KUrl & saveUrl, bool saveAsOkularArchive ) { KTemporaryFile tf; QString fileName; @@ -2196,85 +2193,156 @@ bool Part::saveAs( const KUrl & saveUrl ) fileName = tf.fileName(); tf.close(); - QString errorText; - bool saved; + QScopedPointer tempFile; + KIO::Job *copyJob; // 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 ( saveAsOkularArchive ) { - if (errorText.isEmpty()) + 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( fileName, saveUrl, -1, KIO::Overwrite ); + } + else + { + // If the user wants to save in the original file's format, some features + // might not be available. Find out what can't be saved in this format + bool wontSaveAnnotations = false; + bool wontSaveForms = false; + + if ( !m_document->canSaveChanges( Document::SaveFormsCapability ) ) + { + /* Set wontSaveForms only if there are forms */ + const int pagecount = m_document->pages(); + + for ( int pageno = 0; pageno < pagecount; ++pageno ) + { + const Okular::Page *page = m_document->page( pageno ); + if ( !page->formFields().empty() ) + { + wontSaveForms = true; + break; + } + } + } + if ( !m_document->canSaveChanges( Document::SaveAnnotationsCapability ) ) + { + /* Set wontSaveAnnotations only if there are local annotations */ + const int pagecount = m_document->pages(); + + for ( int pageno = 0; pageno < pagecount; ++pageno ) + { + const Okular::Page *page = m_document->page( pageno ); + foreach ( const Okular::Annotation *ann, page->annotations() ) + { + if ( !(ann->flags() & Okular::Annotation::External) ) + { + wontSaveAnnotations = true; + break; + } + } + if ( wontSaveAnnotations ) + break; + } + } + + // 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() ) + { + int result = KMessageBox::warningYesNoCancelList( widget(), + i18n( "The following elements cannot be saved in this format and will be lost.
If you want to preserve them, please use the Okular document archive format." ), + listOfwontSaves, i18n( "Warning" ), + KGuiItem( i18n( "Save as Okular document archive..." ), "document-save-as" ), // <- KMessageBox::Yes + KStandardGuiItem::cont() ); // <- KMessageBox::NO + + switch (result) + { + case KMessageBox::Yes: // -> Save as Okular document archive + return slotSaveFileAs( true /* showOkularArchiveAsDefaultFormat */ ); + case KMessageBox::No: // -> Continue + 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( fileName, saveUrl, -1, KIO::Overwrite ); } else { - KMessageBox::information( widget(), i18n("File could not be saved in '%1'. %2", fileName, errorText ) ); + // make use of the already downloaded (in case of remote URLs) file, + // no point in downloading that again + KUrl srcUrl = KUrl::fromPath( 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( fileName, saveUrl, -1, KIO::Overwrite ); if ( !KIO::NetAccess::synchronousRun( copyJob, widget() ) ) { KMessageBox::information( widget(), i18n("File could not be saved in '%1'. Try to save it to another location.", saveUrl.prettyUrl() ) ); return false; } + // TODO: Load new file instead of the old one + setModified( false ); return true; } - -void Part::slotSaveCopyAs() -{ - if ( m_embedMode == PrintPreviewMode ) - return; - - KUrl saveUrl = KFileDialog::getSaveUrl( KUrl("kfiledialog:///okular/" + url().fileName()), - QString(), widget(), QString(), - KFileDialog::ConfirmOverwrite ); - if ( saveUrl.isValid() && !saveUrl.isEmpty() ) - { - // make use of the already downloaded (in case of remote URLs) file, - // no point in downloading that again - KUrl srcUrl = KUrl::fromPath( localFilePath() ); - KTemporaryFile * tempFile = 0; - // 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 - if ( ( tempFile = m_keeper->copyToTemporary() ) ) - 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; -#endif - } - else - { - // we still have the original remote URL of the document, - // so copy the document from there - srcUrl = url(); - } - } - - KIO::Job *copyJob = KIO::file_copy( srcUrl, saveUrl, -1, KIO::Overwrite ); - if ( !KIO::NetAccess::synchronousRun( copyJob, widget() ) ) - KMessageBox::information( widget(), i18n("File could not be saved in '%1'. Try to save it to another location.", saveUrl.prettyUrl() ) ); - - delete tempFile; - } -} - - void Part::slotGetNewStuff() { #if 0 diff --git a/part.h b/part.h index 19c0d3eda..48ab17a67 100644 --- a/part.h +++ b/part.h @@ -166,6 +166,7 @@ class OKULAR_PART_EXPORT Part : public KParts::ReadWritePart, public Okular::Doc bool openUrl(const KUrl &url); void guiActivateEvent(KParts::GUIActivateEvent *event); void displayInfoMessage( const QString &message, KMessageWidget::MessageType messageType = KMessageWidget::Information, int duration = -1 );; + public: bool saveFile(); bool queryClose(); @@ -190,8 +191,7 @@ class OKULAR_PART_EXPORT Part : public KParts::ReadWritePart, public Okular::Doc 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); @@ -246,7 +246,8 @@ class OKULAR_PART_EXPORT Part : public KParts::ReadWritePart, public Okular::Doc void unsetDummyMode(); void slotRenameBookmark( const DocumentViewport &viewport ); void resetStartArguments(); - + bool saveAs( const KUrl & saveUrl, bool saveAsOkularArchive ); + static int numberOfParts; KTemporaryFile *m_tempfile; diff --git a/part.rc b/part.rc index 60f86e5ba..883df3d80 100644 --- a/part.rc +++ b/part.rc @@ -5,7 +5,6 @@ - From eb6cd088d7a046b39e7283d979960b4111ee662d Mon Sep 17 00:00:00 2001 From: Fabio D'Urso Date: Sat, 10 May 2014 16:34:53 +0200 Subject: [PATCH 02/78] Fix non-PDF native file extraction from document archives --- core/document.cpp | 11 +++++++ core/document.h | 9 ++++++ part.cpp | 82 ++++++++++++++++++++++++++++++----------------- 3 files changed, 72 insertions(+), 30 deletions(-) diff --git a/core/document.cpp b/core/document.cpp index 504d07c55..803b7cb50 100644 --- a/core/document.cpp +++ b/core/document.cpp @@ -4272,6 +4272,17 @@ bool Document::saveDocumentArchive( const QString &fileName ) 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; diff --git a/core/document.h b/core/document.h index 06bac8a0f..585004960 100644 --- a/core/document.h +++ b/core/document.h @@ -698,6 +698,15 @@ class OKULAR_EXPORT Document : public QObject */ 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 0.14 (KDE 4.20) + */ + 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. diff --git a/part.cpp b/part.cpp index fd0a92845..847406eec 100644 --- a/part.cpp +++ b/part.cpp @@ -2292,41 +2292,63 @@ bool Part::saveAs( const KUrl & saveUrl, bool saveAsOkularArchive ) } else { - // make use of the already downloaded (in case of remote URLs) file, - // no point in downloading that again - KUrl srcUrl = KUrl::fromPath( 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 the generators doesn't support saving changes, we will + // just copy the original file. - if ( srcUrl != saveUrl ) + if ( isDocumentArchive ) { - copyJob = KIO::file_copy( srcUrl, saveUrl, -1, KIO::Overwrite ); + // 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( fileName, saveUrl, -1, KIO::Overwrite ); } else { - // Don't do a real copy in this case, just update the timestamps - copyJob = KIO::setModificationTime( saveUrl, QDateTime::currentDateTime() ); + // Otherwise just copy the open file. + // make use of the already downloaded (in case of remote URLs) file, + // no point in downloading that again + KUrl srcUrl = KUrl::fromPath( 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() ); + } } } } From 2f90f3b6c9b1b3972bc84223fceb41935d63101f Mon Sep 17 00:00:00 2001 From: Fabio D'Urso Date: Fri, 9 May 2014 17:40:14 +0200 Subject: [PATCH 03/78] Remove Okular archive from the Export formats --- part.cpp | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/part.cpp b/part.cpp index 847406eec..81fc11e9f 100644 --- a/part.cpp +++ b/part.cpp @@ -699,7 +699,6 @@ void Part::setupViewerActions() m_exportAs = 0; m_exportAsMenu = 0; m_exportAsText = 0; - m_exportAsDocArchive = 0; m_aboutBackend = ac->addAction("help_about_backend"); m_aboutBackend->setText(i18n("About Backend")); @@ -784,11 +783,6 @@ void Part::setupActions() 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" ), - KMimeType::mimeType( "application/vnd.kde.okular-archive" ) ), m_exportAsMenu ); - m_exportAsMenu->addAction( m_exportAsDocArchive ); - m_exportAsDocArchive->setEnabled( false ); m_showPresentation = ac->addAction("presentation"); m_showPresentation->setText(i18n("P&resentation")); @@ -1385,7 +1379,6 @@ bool Part::openFile() #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 ); // update viewing actions @@ -1537,14 +1530,13 @@ bool Part::closeUrl(bool promptToSave) 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); @@ -2656,11 +2648,8 @@ void Part::slotExportAs(QAction * act) case 0: filter = "text/plain"; break; - case 1: - filter = "application/vnd.kde.okular-archive"; - break; default: - filter = m_exportFormats.at( id - 2 ).mimeType()->name(); + filter = m_exportFormats.at( id - 1 ).mimeType()->name(); break; } QString fileName = KFileDialog::getSaveFileName( url().isLocalFile() ? url().directory() : QString(), @@ -2674,11 +2663,8 @@ void Part::slotExportAs(QAction * act) 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 ) From d29b8a261d801d7031cd3e8ac77187fc92975d51 Mon Sep 17 00:00:00 2001 From: Fabio D'Urso Date: Fri, 9 May 2014 19:06:45 +0200 Subject: [PATCH 04/78] Don't enable "Save As" on directories and on stdin (it wouldn't work anyway) --- part.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/part.cpp b/part.cpp index 81fc11e9f..ae742acc4 100644 --- a/part.cpp +++ b/part.cpp @@ -1327,7 +1327,7 @@ bool Part::openFile() m_find->setEnabled( ok && canSearch ); m_findNext->setEnabled( ok && canSearch ); m_findPrev->setEnabled( ok && canSearch ); - if( m_saveAs ) m_saveAs->setEnabled( ok ); + if( m_saveAs ) m_saveAs->setEnabled( ok && !( isstdin || mime->is( "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 ); From 0ea96257653b80e42116576055181bf011bb9985 Mon Sep 17 00:00:00 2001 From: Fabio D'Urso Date: Fri, 9 May 2014 22:36:24 +0200 Subject: [PATCH 05/78] Save action implemented --- part.cpp | 15 +++++++++++---- part.h | 3 ++- part.rc | 3 ++- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/part.cpp b/part.cpp index ae742acc4..8fe4d437d 100644 --- a/part.cpp +++ b/part.cpp @@ -654,6 +654,7 @@ void Part::setupViewerActions() m_findPrev = KStandardAction::findPrev( this, SLOT(slotFindPrev()), ac ); m_findPrev->setEnabled( false ); + m_save = 0; m_saveAs = 0; QAction * prefs = KStandardAction::preferences( this, SLOT(slotPreferences()), ac); @@ -750,6 +751,8 @@ void Part::setupActions() m_selectAll = KStandardAction::selectAll( m_pageView, SLOT(selectAll()), ac ); + m_save = KStandardAction::save( this, SLOT(saveFile()), ac ); + m_save->setEnabled( false ); m_saveAs = KStandardAction::saveAs( this, SLOT(slotSaveFileAs()), ac ); m_saveAs->setEnabled( false ); @@ -1327,6 +1330,7 @@ bool Part::openFile() m_find->setEnabled( ok && canSearch ); m_findNext->setEnabled( ok && canSearch ); m_findPrev->setEnabled( ok && canSearch ); + if( m_save ) m_save->setEnabled( ok && !( isstdin || mime->is( "inode/directory" ) ) ); if( m_saveAs ) m_saveAs->setEnabled( ok && !( isstdin || mime->is( "inode/directory" ) ) ); emit enablePrintAction( ok && m_document->printingSupport() != Okular::Document::NoPrinting ); m_printPreview->setEnabled( ok && m_document->printingSupport() != Okular::Document::NoPrinting ); @@ -1491,13 +1495,13 @@ bool Part::queryClose() const int res = KMessageBox::warningYesNoCancel( widget(), i18n( "Do you want to save your annotation changes or discard them?" ), i18n( "Close Document" ), - KStandardGuiItem::saveAs(), + KStandardGuiItem::save(), KStandardGuiItem::discard() ); switch ( res ) { case KMessageBox::Yes: // Save as - slotSaveFileAs(); + saveFile(); return !isModified(); // Only allow closing if file was really saved case KMessageBox::No: // Discard return true; @@ -1524,6 +1528,7 @@ bool Part::closeUrl(bool promptToSave) m_find->setEnabled( false ); m_findNext->setEnabled( false ); m_findPrev->setEnabled( false ); + if( m_save ) m_save->setEnabled( false ); if( m_saveAs ) m_saveAs->setEnabled( false ); m_printPreview->setEnabled( false ); m_showProperties->setEnabled( false ); @@ -2117,8 +2122,10 @@ void Part::slotFindPrev() bool Part::saveFile() { - kDebug() << "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() ); } bool Part::slotSaveFileAs( bool showOkularArchiveAsDefaultFormat ) diff --git a/part.h b/part.h index 48ab17a67..23e53d4c3 100644 --- a/part.h +++ b/part.h @@ -168,7 +168,6 @@ class OKULAR_PART_EXPORT Part : public KParts::ReadWritePart, public Okular::Doc void displayInfoMessage( const QString &message, KMessageWidget::MessageType messageType = KMessageWidget::Information, int duration = -1 );; public: - bool saveFile(); bool queryClose(); bool closeUrl(); bool closeUrl(bool promptToSave); @@ -220,6 +219,7 @@ class OKULAR_PART_EXPORT Part : public KParts::ReadWritePart, public Okular::Doc void slotRebuildBookmarkMenu(); public slots: + bool saveFile(); // connected to Shell action (and browserExtension), not local one void slotPrint(); void restoreDocument(const KConfigGroup &group); @@ -309,6 +309,7 @@ class OKULAR_PART_EXPORT Part : public KParts::ReadWritePart, public Okular::Doc KAction *m_find; KAction *m_findNext; KAction *m_findPrev; + KAction *m_save; KAction *m_saveAs; KAction *m_saveCopyAs; KAction *m_printPreview; diff --git a/part.rc b/part.rc index 883df3d80..ffe933d59 100644 --- a/part.rc +++ b/part.rc @@ -1,9 +1,10 @@ - + &File + From 3bae3c289fa5c5d90c1270d96b4bf6cc6abcc6ac Mon Sep 17 00:00:00 2001 From: Fabio D'Urso Date: Sun, 11 May 2014 00:53:20 +0200 Subject: [PATCH 06/78] Removed parent directory check on KDirWatch, handling created() and deleted() is enough --- part.cpp | 56 ++++++++++++++++++++++++++++++++++++++++---------------- part.h | 4 ++++ 2 files changed, 44 insertions(+), 16 deletions(-) diff --git a/part.cpp b/part.cpp index 8fe4d437d..5b95766ed 100644 --- a/part.cpp +++ b/part.cpp @@ -510,6 +510,8 @@ m_cliPresentation(false), m_cliPrint(false), m_embedMode(detectEmbedMode(parentW // document watcher and reloader m_watcher = new KDirWatch( this ); + connect( m_watcher, SIGNAL(created(QString)), this, SLOT(slotFileDirty(QString)) ); + connect( m_watcher, SIGNAL(deleted(QString)), this, SLOT(slotFileDirty(QString)) ); connect( m_watcher, SIGNAL(dirty(QString)), this, SLOT(slotFileDirty(QString)) ); m_dirtyHandler = new QTimer( this ); m_dirtyHandler->setSingleShot( true ); @@ -1201,12 +1203,39 @@ bool Part::slotImportPSFile() 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(); } bool Part::openFile() @@ -1401,9 +1430,7 @@ bool Part::openFile() // 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( "OpenTOC" ).toBool() && m_sidebar->isItemEnabled( 0 ) && !m_sidebar->isCollapsed() ) @@ -1552,12 +1579,7 @@ bool Part::closeUrl(bool promptToSave) emit enablePrintAction(false); m_realUrl = KUrl(); 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 ); @@ -1629,6 +1651,8 @@ void Part::slotShowBottomBar() void Part::slotFileDirty( const QString& path ) { + kDebug() << path; + // The beauty of this is that each start cancels the previous one. // This means that timeout() is only fired when there have // no changes to the file for the last 750 milisecs. @@ -1655,8 +1679,8 @@ void Part::slotFileDirty( const QString& path ) 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 ); } } @@ -1750,7 +1774,7 @@ void Part::slotDoFileDirty() else { // start watching the file again (since we dropped it on close) - addFileToWatcher( m_watcher, localFilePath() ); + setFileToWatch( localFilePath() ); m_dirtyHandler->start( 750 ); } } diff --git a/part.h b/part.h index 23e53d4c3..75ed8160f 100644 --- a/part.h +++ b/part.h @@ -248,6 +248,9 @@ class OKULAR_PART_EXPORT Part : public KParts::ReadWritePart, public Okular::Doc void resetStartArguments(); bool saveAs( const KUrl & saveUrl, bool saveAsOkularArchive ); + void setFileToWatch( const QString &filePath ); + void unsetFileToWatch(); + static int numberOfParts; KTemporaryFile *m_tempfile; @@ -279,6 +282,7 @@ class OKULAR_PART_EXPORT Part : public KParts::ReadWritePart, public Okular::Doc // document watcher (and reloader) variables KDirWatch *m_watcher; + QString m_watchedFilePath, m_watchedFileSymlinkTarget; QTimer *m_dirtyHandler; KUrl m_oldUrl; Okular::DocumentViewport m_viewportDirty; From 005cf7f0822602d6c12e93f93bc3f445e4d14164 Mon Sep 17 00:00:00 2001 From: Fabio D'Urso Date: Sun, 11 May 2014 10:53:56 +0200 Subject: [PATCH 07/78] Load new file instead of the old one after Save As --- part.cpp | 35 ++++++++++++++++++++++++++--------- part.h | 2 +- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/part.cpp b/part.cpp index 5b95766ed..620530cdc 100644 --- a/part.cpp +++ b/part.cpp @@ -515,7 +515,7 @@ m_cliPresentation(false), m_cliPrint(false), m_embedMode(detectEmbedMode(parentW connect( m_watcher, SIGNAL(dirty(QString)), this, SLOT(slotFileDirty(QString)) ); m_dirtyHandler = new QTimer( this ); m_dirtyHandler->setSingleShot( true ); - connect( m_dirtyHandler, SIGNAL(timeout()),this, SLOT(slotDoFileDirty()) ); + connect( m_dirtyHandler, SIGNAL(timeout()),this, SLOT(slotAttemptReload()) ); slotNewConfig(); @@ -1034,7 +1034,7 @@ void Part::loadCancelled(const QString &reason) 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) @@ -1694,8 +1694,8 @@ void Part::slotFileDirty( const QString& path ) } } - -void Part::slotDoFileDirty() +// Attempt to reload the document, one or more times, optionally from a different URL +void Part::slotAttemptReload( bool oneShot, const KUrl &newUrl ) { bool tocReloadPrepared = false; @@ -1703,7 +1703,7 @@ void Part::slotDoFileDirty() 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(); @@ -1771,7 +1771,7 @@ void Part::slotDoFileDirty() if (m_wasPresentationOpen) slotShowPresentation(); emit enablePrintAction(true && m_document->printingSupport() != Okular::Document::NoPrinting); } - else + else if ( !oneShot ) { // start watching the file again (since we dropped it on close) setFileToWatch( localFilePath() ); @@ -2376,15 +2376,32 @@ bool Part::saveAs( const KUrl & saveUrl, bool saveAsOkularArchive ) } } + // Stop watching for changes while we write the new file (useful when + // overwriting) + if ( url().isLocalFile() ) + unsetFileToWatch(); + if ( !KIO::NetAccess::synchronousRun( copyJob, widget() ) ) { KMessageBox::information( widget(), i18n("File could not be saved in '%1'. Try to save it to another location.", saveUrl.prettyUrl() ) ); + + // Restore watcher + if ( url().isLocalFile() ) + setFileToWatch( localFilePath() ); + return false; } - // TODO: Load new file instead of the old one - setModified( false ); + + // Load new file instead of the old one + if ( url() != saveUrl ) + slotAttemptReload( true, saveUrl ); + + // Restore watcher + if ( url().isLocalFile() ) + setFileToWatch( localFilePath() ); + return true; } @@ -2710,7 +2727,7 @@ void Part::slotReload() // auto-refresh system m_dirtyHandler->stop(); - slotDoFileDirty(); + slotAttemptReload(); } diff --git a/part.h b/part.h index 75ed8160f..19b432031 100644 --- a/part.h +++ b/part.h @@ -225,7 +225,7 @@ class OKULAR_PART_EXPORT Part : public KParts::ReadWritePart, public Okular::Doc void restoreDocument(const KConfigGroup &group); void saveDocumentRestoreInfo(KConfigGroup &group); void slotFileDirty( const QString& ); - void slotDoFileDirty(); + void slotAttemptReload( bool oneShot = false, const KUrl &newUrl = KUrl() ); void psTransformEnded(int, QProcess::ExitStatus); KConfigDialog * slotGeneratorPreferences(); From d0af66f0822730236726e5bfd32212403188f214 Mon Sep 17 00:00:00 2001 From: Fabio D'Urso Date: Sat, 10 May 2014 12:32:12 +0200 Subject: [PATCH 08/78] openDocumentArchive: moved unpacking code to a separate method --- core/document.cpp | 47 +++++++++++++++++++++++++++-------------------- core/document_p.h | 1 + 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/core/document.cpp b/core/document.cpp index 803b7cb50..d458a4c2c 100644 --- a/core/document.cpp +++ b/core/document.cpp @@ -108,6 +108,7 @@ struct ArchiveData { } + QString originalFileName; KTemporaryFile document; KTemporaryFile metadataFile; }; @@ -4094,30 +4095,30 @@ QByteArray Document::fontData(const FontInfo &font) const return result; } -Document::OpenResult Document::openDocumentArchive( const QString & docFile, const KUrl & url, const QString & password ) +ArchiveData *DocumentPrivate::unpackDocumentArchive( const QString &archivePath ) { - const KMimeType::Ptr mime = KMimeType::findByPath( docFile, 0, false /* content too */ ); + const KMimeType::Ptr mime = KMimeType::findByPath( archivePath, 0, false /* content too */ ); if ( !mime->is( "application/vnd.kde.okular-archive" ) ) - return OpenError; + return 0; - KZip okularArchive( docFile ); + KZip okularArchive( archivePath ); if ( !okularArchive.open( QIODevice::ReadOnly ) ) - return OpenError; + return 0; const KArchiveDirectory * mainDir = okularArchive.directory(); const KArchiveEntry * mainEntry = mainDir->entry( "content.xml" ); if ( !mainEntry || !mainEntry->isFile() ) - return OpenError; + return 0; std::auto_ptr< QIODevice > mainEntryDevice( static_cast< const KZipFileEntry * >( mainEntry )->createDevice() ); QDomDocument doc; if ( !doc.setContent( mainEntryDevice.get() ) ) - return OpenError; + return 0; mainEntryDevice.reset(); QDomElement root = doc.documentElement(); if ( root.tagName() != "OkularArchive" ) - return OpenError; + return 0; QString documentFileName; QString metadataFileName; @@ -4137,20 +4138,21 @@ Document::OpenResult Document::openDocumentArchive( const QString & docFile, con } } if ( documentFileName.isEmpty() ) - return OpenError; + return 0; const KArchiveEntry * docEntry = mainDir->entry( documentFileName ); if ( !docEntry || !docEntry->isFile() ) - return OpenError; + return 0; std::auto_ptr< ArchiveData > archiveData( new ArchiveData() ); const int dotPos = documentFileName.indexOf( '.' ); if ( dotPos != -1 ) archiveData->document.setSuffix( documentFileName.mid( dotPos ) ); if ( !archiveData->document.open() ) - return OpenError; + return 0; + + archiveData->originalFileName = documentFileName; - QString tempFileName = archiveData->document.fileName(); { std::auto_ptr< QIODevice > docEntryDevice( static_cast< const KZipFileEntry * >( docEntry )->createDevice() ); copyQIODevice( docEntryDevice.get(), &archiveData->document ); @@ -4169,17 +4171,22 @@ Document::OpenResult Document::openDocumentArchive( const QString & docFile, con } } + return archiveData.release(); +} + +Document::OpenResult Document::openDocumentArchive( const QString & docFile, const KUrl & url, const QString & password ) +{ + d->m_archiveData = DocumentPrivate::unpackDocumentArchive( docFile ); + if ( !d->m_archiveData ) + return OpenError; + + const QString tempFileName = d->m_archiveData->document.fileName(); const KMimeType::Ptr docMime = KMimeType::findByPath( tempFileName, 0, true /* local file */ ); - 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 = 0; } @@ -4193,7 +4200,7 @@ bool Document::saveDocumentArchive( const QString &fileName ) /* 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; diff --git a/core/document_p.h b/core/document_p.h index aabd192f5..62a60c468 100644 --- a/core/document_p.h +++ b/core/document_p.h @@ -129,6 +129,7 @@ class DocumentPrivate ConfigInterface* generatorConfig( GeneratorInfo& info ); SaveInterface* generatorSave( GeneratorInfo& info ); Document::OpenResult openDocumentInternal( const KService::Ptr& offer, bool isstdin, const QString& docFile, const QByteArray& filedata, const QString& password ); + static ArchiveData *unpackDocumentArchive( const QString &archivePath ); bool savePageDocumentInfo( KTemporaryFile *infoFile, int what ) const; DocumentViewport nextDocumentViewport() const; void notifyAnnotationChanges( int page ); From 43a3756e1c77b7e4ffebb26bdd024613fa1f4e18 Mon Sep 17 00:00:00 2001 From: Fabio D'Urso Date: Sat, 10 May 2014 00:53:39 +0200 Subject: [PATCH 09/78] Hot-swap backing file instead of reloading (if the generator supports it) --- core/document.cpp | 70 ++++++++++++++++++++++++++ core/document.h | 35 +++++++++++++ core/generator.cpp | 5 ++ core/generator.h | 14 +++++- generators/kimgio/generator_kimgio.cpp | 8 +++ generators/kimgio/generator_kimgio.h | 1 + part.cpp | 58 ++++++++++++++++++++- part.h | 1 + 8 files changed, 189 insertions(+), 3 deletions(-) diff --git a/core/document.cpp b/core/document.cpp index d458a4c2c..c41d047e1 100644 --- a/core/document.cpp +++ b/core/document.cpp @@ -3998,6 +3998,76 @@ const KComponentData* Document::componentData() const return kcd; } +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 KUrl & 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; + + kDebug(OkularDebug) << "Swapping backing file to" << newFileName; + if (genIt->generator->swapBackingFile( newFileName )) + { + d->m_url = url; + return true; + } + else + { + return false; + } +} + +bool Document::swapBackingFileArchive( const QString &newFileName, const KUrl & 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; + + kDebug(OkularDebug) << "Swapping backing archive to" << newFileName; + + ArchiveData *newArchive = DocumentPrivate::unpackDocumentArchive( newFileName ); + if ( !newArchive ) + return false; + + const QString tempFileName = newArchive->document.fileName(); + + kDebug(OkularDebug) << "Swapping backing file to" << tempFileName; + if (genIt->generator->swapBackingFile( tempFileName )) + { + delete d->m_archiveData; + d->m_archiveData = newArchive; + d->m_url = url; + return true; + } + else + { + return false; + } +} + bool Document::canSaveChanges() const { if ( !d->m_generator ) diff --git a/core/document.h b/core/document.h index 585004960..58c2c4b45 100644 --- a/core/document.h +++ b/core/document.h @@ -613,6 +613,41 @@ class OKULAR_EXPORT Document : public QObject */ const KComponentData* componentData() const; + /** + * Returns whether the generator supports hot-swapping the current file + * with another identical file + * + * @since 0.20 (KDE 4.14) + */ + 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 0.20 (KDE 4.14) + */ + bool swapBackingFile( const QString &newFileName, const KUrl & 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 0.20 (KDE 4.14) + */ + bool swapBackingFileArchive( const QString &newFileName, const KUrl & url ); + /** * Saving capabilities. Their availability varies according to the * underlying generator and/or the document type. diff --git a/core/generator.cpp b/core/generator.cpp index ec1d1f76d..704bf4d66 100644 --- a/core/generator.cpp +++ b/core/generator.cpp @@ -196,6 +196,11 @@ Document::OpenResult Generator::loadDocumentFromDataWithPassword( const QByteArr return loadDocumentFromData( fileData, pagesVector ) ? Document::OpenSuccess : Document::OpenError; } +bool Generator::swapBackingFile( QString const &newFileName ) +{ + return false; +} + bool Generator::closeDocument() { Q_D( Generator ); diff --git a/core/generator.h b/core/generator.h index 506f8a826..9784dadb0 100644 --- a/core/generator.h +++ b/core/generator.h @@ -207,7 +207,8 @@ class OKULAR_EXPORT Generator : public QObject 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 0.20 (KDE 4.14) }; /** @@ -268,6 +269,17 @@ class OKULAR_EXPORT Generator : public QObject */ virtual Document::OpenResult loadDocumentFromDataWithPassword( const QByteArray & fileData, QVector< Page * > & pagesVector, const QString &password ); + /** + * 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 0.20 (KDE 4.14) + * + * @returns true on success, false otherwise. + */ + virtual bool swapBackingFile( const QString &newFileName ); /** * This method is called when the document is closed and not used diff --git a/generators/kimgio/generator_kimgio.cpp b/generators/kimgio/generator_kimgio.cpp index f856434fe..c6822415a 100644 --- a/generators/kimgio/generator_kimgio.cpp +++ b/generators/kimgio/generator_kimgio.cpp @@ -56,6 +56,7 @@ KIMGIOGenerator::KIMGIOGenerator( QObject *parent, const QVariantList &args ) setFeature( TiledRendering ); setFeature( PrintNative ); setFeature( PrintToFile ); + setFeature( SwapBackingFile ); /* setComponentData( *ownComponentData() ); @@ -130,6 +131,13 @@ bool KIMGIOGenerator::loadDocumentFromData( const QByteArray & fileData, QVector return true; } +bool KIMGIOGenerator::swapBackingFile( QString const &newFileName ) +{ + // NOP: We don't actually need to do anything because all data has already + // been loaded in RAM + return true; +} + bool KIMGIOGenerator::doCloseDocument() { m_img = QImage(); diff --git a/generators/kimgio/generator_kimgio.h b/generators/kimgio/generator_kimgio.h index faebd8572..a60c36e72 100644 --- a/generators/kimgio/generator_kimgio.h +++ b/generators/kimgio/generator_kimgio.h @@ -25,6 +25,7 @@ class KIMGIOGenerator : public Okular::Generator // [INHERITED] load a document and fill up the pagesVector bool loadDocument( const QString & fileName, QVector & pagesVector ); bool loadDocumentFromData( const QByteArray & fileData, QVector & pagesVector ); + bool swapBackingFile( QString const &newFileName ); // [INHERITED] print document using already configured kprinter bool print( QPrinter& printer ); diff --git a/part.cpp b/part.cpp index 620530cdc..3ab912317 100644 --- a/part.cpp +++ b/part.cpp @@ -303,7 +303,7 @@ QObject *parent, const QVariantList &args, KComponentData componentData ) : KParts::ReadWritePart(parent), -m_tempfile( 0 ), m_fileWasRemoved( false ), m_showMenuBarAction( 0 ), m_showFullScreenAction( 0 ), m_actionsSearched( false ), +m_tempfile( 0 ), m_swapInsteadOfOpening( false ), m_fileWasRemoved( false ), m_showMenuBarAction( 0 ), m_showFullScreenAction( 0 ), m_actionsSearched( false ), m_cliPresentation(false), m_cliPrint(false), m_embedMode(detectEmbedMode(parentWidget, parent, args)), m_generatorGuiClient(0), m_keeper( 0 ) { // first, we check if a config file name has been specified @@ -1265,6 +1265,26 @@ bool Part::openFile() uncompressOk = handleCompressed( fileNameToOpen, localFilePath(), compressedMime ); mime = KMimeType::findByPath( fileNameToOpen ); } + + if ( m_swapInsteadOfOpening ) + { + m_swapInsteadOfOpening = false; + + if ( !uncompressOk ) + return false; + + if ( mime->is( "application/vnd.kde.okular-archive" ) ) + { + isDocumentArchive = true; + return m_document->swapBackingFileArchive( fileNameToOpen, url() ); + } + else + { + isDocumentArchive = false; + return m_document->swapBackingFile( fileNameToOpen, url() ); + } + } + Document::OpenResult openResult = Document::OpenError; isDocumentArchive = false; if ( uncompressOk ) @@ -1542,6 +1562,13 @@ bool Part::closeUrl(bool promptToSave) if ( promptToSave && !queryClose() ) return false; + if ( m_swapInsteadOfOpening ) + { + // If we're swapping the backing file, we don't want to close the + // current one when openUrl() calls us internally + return true; // pretend it worked + } + setModified( false ); if (!m_temporaryLocalFile.isNull() && m_temporaryLocalFile != localFilePath()) @@ -2396,7 +2423,34 @@ bool Part::saveAs( const KUrl & saveUrl, bool saveAsOkularArchive ) // Load new file instead of the old one if ( url() != saveUrl ) - slotAttemptReload( true, saveUrl ); + { + if ( m_document->canSwapBackingFile() ) + { + // If the generator supports hot-swapping of the backing file + // tell openFile to swap the backing file instead of opening a new one + m_swapInsteadOfOpening = true; + + // this calls openFile internally, which in turn actually calls + // m_document->swapBackingFile() instead of the regular loadDocument + const bool success = openUrl( saveUrl ); + + // restore it back to false -- this has already been done by + // openFile, but let's do it again for extra safety + m_swapInsteadOfOpening = false; + + // In case of file swapping errors, close everything to avoid inconsistencies + if ( !success ) + { + closeUrl(); + } + } + else + { + // If the generator doesn't support swapping file, then just reload + // the document from the new location + slotAttemptReload( true, saveUrl ); + } + } // Restore watcher if ( url().isLocalFile() ) diff --git a/part.h b/part.h index 19b432031..4facd1f29 100644 --- a/part.h +++ b/part.h @@ -259,6 +259,7 @@ class OKULAR_PART_EXPORT Part : public KParts::ReadWritePart, public Okular::Doc Okular::Document * m_document; QString m_temporaryLocalFile; bool isDocumentArchive; + 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; From 68d5f0af8957f3c7deb56e9dde9e8cf133134b34 Mon Sep 17 00:00:00 2001 From: Fabio D'Urso Date: Sat, 10 May 2014 18:00:30 +0200 Subject: [PATCH 10/78] Better error handling (ie avoid inconsistencies) in case of reload/swapping errors --- part.cpp | 31 +++++++++++++++++++++---------- part.h | 2 +- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/part.cpp b/part.cpp index 3ab912317..8e5c82725 100644 --- a/part.cpp +++ b/part.cpp @@ -1722,7 +1722,7 @@ void Part::slotFileDirty( const QString& path ) } // Attempt to reload the document, one or more times, optionally from a different URL -void Part::slotAttemptReload( bool oneShot, const KUrl &newUrl ) +bool Part::slotAttemptReload( bool oneShot, const KUrl &newUrl ) { bool tocReloadPrepared = false; @@ -1764,7 +1764,7 @@ void Part::slotAttemptReload( bool oneShot, const KUrl &newUrl ) { m_toc->rollbackReload(); } - return; + return false; } if ( tocReloadPrepared ) @@ -1773,6 +1773,8 @@ void Part::slotAttemptReload( bool oneShot, const KUrl &newUrl ) // 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 @@ -1797,6 +1799,8 @@ void Part::slotAttemptReload( bool oneShot, const KUrl &newUrl ) } if (m_wasPresentationOpen) slotShowPresentation(); emit enablePrintAction(true && m_document->printingSupport() != Okular::Document::NoPrinting); + + reloadSucceeded = true; } else if ( !oneShot ) { @@ -1804,6 +1808,8 @@ void Part::slotAttemptReload( bool oneShot, const KUrl &newUrl ) setFileToWatch( localFilePath() ); m_dirtyHandler->start( 750 ); } + + return reloadSucceeded; } @@ -2421,6 +2427,8 @@ bool Part::saveAs( const KUrl & saveUrl, bool saveAsOkularArchive ) setModified( false ); + bool reloadedCorrectly = true; + // Load new file instead of the old one if ( url() != saveUrl ) { @@ -2432,26 +2440,29 @@ bool Part::saveAs( const KUrl & saveUrl, bool saveAsOkularArchive ) // this calls openFile internally, which in turn actually calls // m_document->swapBackingFile() instead of the regular loadDocument - const bool success = openUrl( saveUrl ); + if ( !openUrl( saveUrl ) ) + reloadedCorrectly = false; // restore it back to false -- this has already been done by // openFile, but let's do it again for extra safety m_swapInsteadOfOpening = false; - - // In case of file swapping errors, close everything to avoid inconsistencies - if ( !success ) - { - closeUrl(); - } } else { // If the generator doesn't support swapping file, then just reload // the document from the new location - slotAttemptReload( true, saveUrl ); + if ( !slotAttemptReload( true, saveUrl ) ) + reloadedCorrectly = false; } } + // In case of file swapping errors, close the document to avoid inconsistencies + if ( !reloadedCorrectly ) + { + kDebug() << "The document hasn't been reloaded/swapped correctly"; + closeUrl(); + } + // Restore watcher if ( url().isLocalFile() ) setFileToWatch( localFilePath() ); diff --git a/part.h b/part.h index 4facd1f29..821867619 100644 --- a/part.h +++ b/part.h @@ -225,7 +225,7 @@ class OKULAR_PART_EXPORT Part : public KParts::ReadWritePart, public Okular::Doc void restoreDocument(const KConfigGroup &group); void saveDocumentRestoreInfo(KConfigGroup &group); void slotFileDirty( const QString& ); - void slotAttemptReload( bool oneShot = false, const KUrl &newUrl = KUrl() ); + bool slotAttemptReload( bool oneShot = false, const KUrl &newUrl = KUrl() ); void psTransformEnded(int, QProcess::ExitStatus); KConfigDialog * slotGeneratorPreferences(); From 6691401ea602e140ffee120dde68bf4e41abc31e Mon Sep 17 00:00:00 2001 From: Fabio D'Urso Date: Sun, 11 May 2014 12:23:37 +0200 Subject: [PATCH 11/78] Fix file swapping after saving to a remote URL --- part.cpp | 21 +++++++++++---------- part.h | 1 + 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/part.cpp b/part.cpp index 8e5c82725..0d9994149 100644 --- a/part.cpp +++ b/part.cpp @@ -1488,8 +1488,17 @@ bool Part::openFile() return true; } -bool Part::openUrl(const KUrl &_url) +bool Part::openUrl( const KUrl &url ) { + return openUrl( url, false /* swapInsteadOfOpening */ ); +} + +bool Part::openUrl( const KUrl &_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; @@ -2434,18 +2443,10 @@ bool Part::saveAs( const KUrl & saveUrl, bool saveAsOkularArchive ) { if ( m_document->canSwapBackingFile() ) { - // If the generator supports hot-swapping of the backing file - // tell openFile to swap the backing file instead of opening a new one - m_swapInsteadOfOpening = true; - // this calls openFile internally, which in turn actually calls // m_document->swapBackingFile() instead of the regular loadDocument - if ( !openUrl( saveUrl ) ) + if ( !openUrl( saveUrl, true /* swapInsteadOfOpening */ ) ) reloadedCorrectly = false; - - // restore it back to false -- this has already been done by - // openFile, but let's do it again for extra safety - m_swapInsteadOfOpening = false; } else { diff --git a/part.h b/part.h index 821867619..41bfc6c4f 100644 --- a/part.h +++ b/part.h @@ -234,6 +234,7 @@ class OKULAR_PART_EXPORT Part : public KParts::ReadWritePart, public Okular::Doc void noticeMessage( const QString &message, int duration = -1 ); private: + bool openUrl( const KUrl &url, bool swapInsteadOfOpening ); void setupViewerActions(); void setViewerShortcuts(); void setupActions(); From 3b227582d2df0fdca530c4eb91fd362e71abd9c5 Mon Sep 17 00:00:00 2001 From: Fabio D'Urso Date: Sun, 11 May 2014 13:37:22 +0200 Subject: [PATCH 12/78] Don't save annotation changes to docdata/ But existing annotations (from previous versions) are preserved --- core/document.cpp | 59 +++++++++-------------------------------------- core/document_p.h | 4 +--- 2 files changed, 12 insertions(+), 51 deletions(-) diff --git a/core/document.cpp b/core/document.cpp index c41d047e1..e78dcf17e 100644 --- a/core/document.cpp +++ b/core/document.cpp @@ -1035,25 +1035,6 @@ DocumentViewport DocumentPrivate::nextDocumentViewport() const 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(), "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(), "annotExportAsArchive" ); - } -} - void DocumentPrivate::performAddPageAnnotation( int page, Annotation * annotation ) { Okular::SaveInterface * iface = qobject_cast< Okular::SaveInterface * >( m_generator ); @@ -1083,8 +1064,6 @@ void DocumentPrivate::performAddPageAnnotation( int page, Annotation * annotatio // Redraw everything, including ExternallyDrawn annotations refreshPixmaps( page ); } - - warnLimitedAnnotSupport(); } void DocumentPrivate::performRemovePageAnnotation( int page, Annotation * annotation ) @@ -1121,8 +1100,6 @@ void DocumentPrivate::performRemovePageAnnotation( int page, Annotation * annota refreshPixmaps( page ); } } - - warnLimitedAnnotSupport(); } void DocumentPrivate::performModifyPageAnnotation( int page, Annotation * annotation, bool appearanceChanged ) @@ -1163,10 +1140,6 @@ void DocumentPrivate::performModifyPageAnnotation( int page, Annotation * annota kDebug(OkularDebug) << "Refreshing Pixmaps"; refreshPixmaps( page ); } - - // If the user is moving the annotation, don't steal the focus - if ( (annotation->flags() & Annotation::BeingMoved) == 0 ) - warnLimitedAnnotSupport(); } void DocumentPrivate::performSetAnnotationContents( const QString & newContents, Annotation *annot, int pageNumber ) @@ -1225,15 +1198,12 @@ void DocumentPrivate::saveDocumentInfo() const // 2.1. Save page attributes (bookmark state, annotations, ... ) to DOM QDomElement pageList = doc.createElement( "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; - } + // OriginalAnnotationPageItems tells to store the same unmodified + // annotation list that we read when we opened the file and ignore any + // change made by the user. Since we don't store annotations in + // docdata/ any more, this is necessary to preserve annotations that + // previous Okular version had stored there. + const PageItems saveWhat = AllPageItems | OriginalAnnotationPageItems; // .... save pages that hold data QVector< Page * >::const_iterator pIt = m_pagesVector.constBegin(), pEnd = m_pagesVector.constEnd(); for ( ; pIt != pEnd; ++pIt ) @@ -2244,31 +2214,22 @@ Document::OpenResult Document::openDocument( const QString & docFile, const KUrl 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; // 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; } else { d->loadDocumentInfo(); - d->m_annotationsNeedSaveAs = ( d->canAddAnnotationsNatively() && containsExternalAnnotations ); } - d->m_showWarningLimitedAnnotSupport = true; + d->m_metadataLoadingCompleted = true; d->m_bookmarkManager->setUrl( d->m_url ); // 3. setup observers inernal lists and data @@ -2952,7 +2913,9 @@ void DocumentPrivate::notifyAnnotationChanges( int page ) { int flags = DocumentObserver::Annotations; - if ( m_annotationsNeedSaveAs ) + // Unless we're still loading initial annotations from metadata, the user + // now needs to save or annotation changes will be lost + if ( m_metadataLoadingCompleted ) flags |= DocumentObserver::NeedSaveAs; foreachObserverD( notifyPageChanged( page, flags ) ); diff --git a/core/document_p.h b/core/document_p.h index 62a60c468..3c539cf42 100644 --- a/core/document_p.h +++ b/core/document_p.h @@ -136,7 +136,6 @@ class DocumentPrivate bool canAddAnnotationsNatively() const; bool canModifyExternalAnnotations() const; bool canRemoveExternalAnnotations() const; - void warnLimitedAnnotSupport(); // Methods that implement functionality needed by undo commands void performAddPageAnnotation( int page, Annotation *annotation ); @@ -264,9 +263,8 @@ class DocumentPrivate QSet< View * > m_views; bool m_annotationEditingEnabled; - bool m_annotationsNeedSaveAs; bool m_annotationBeingMoved; // is an annotation currently being moved? - bool m_showWarningLimitedAnnotSupport; + bool m_metadataLoadingCompleted; QUndoStack *m_undoStack; QDomNode m_prevPropsOfAnnotBeingModified; From 57c9cae257c7db47c5862d7d9a36b8e983139d19 Mon Sep 17 00:00:00 2001 From: Fabio D'Urso Date: Sun, 11 May 2014 15:06:27 +0200 Subject: [PATCH 13/78] Don't save form contents to docdata/ But existing saved form from previous Okular versions are preserved --- core/document.cpp | 23 +++++++++++++++++------ core/document_p.h | 1 + core/documentcommands.cpp | 8 ++++++++ core/observer.h | 2 +- core/page.cpp | 16 +++++++++++++++- core/page_p.h | 7 ++++++- 6 files changed, 48 insertions(+), 9 deletions(-) diff --git a/core/document.cpp b/core/document.cpp index e78dcf17e..dd67e4afb 100644 --- a/core/document.cpp +++ b/core/document.cpp @@ -1198,12 +1198,13 @@ void DocumentPrivate::saveDocumentInfo() const // 2.1. Save page attributes (bookmark state, annotations, ... ) to DOM QDomElement pageList = doc.createElement( "pageList" ); root.appendChild( pageList ); - // OriginalAnnotationPageItems tells to store the same unmodified - // annotation list that we read when we opened the file and ignore any - // change made by the user. Since we don't store annotations in - // docdata/ any more, this is necessary to preserve annotations that - // previous Okular version had stored there. - const PageItems saveWhat = AllPageItems | OriginalAnnotationPageItems; + // 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 ) @@ -2921,6 +2922,16 @@ void DocumentPrivate::notifyAnnotationChanges( int page ) foreachObserverD( notifyPageChanged( page, flags ) ); } +void DocumentPrivate::notifyFormChanges( int page ) +{ + // Unless we're still loading initial form contents from metadata, the user + // now needs to save or form changes will be lost + if ( !m_metadataLoadingCompleted ) + return; + + foreachObserverD( notifyPageChanged( page, DocumentObserver::NeedSaveAs ) ); +} + void Document::addPageAnnotation( int page, Annotation * annotation ) { // Transform annotation's base boundary rectangle into unrotated coordinates diff --git a/core/document_p.h b/core/document_p.h index 3c539cf42..deff05a61 100644 --- a/core/document_p.h +++ b/core/document_p.h @@ -133,6 +133,7 @@ class DocumentPrivate bool savePageDocumentInfo( KTemporaryFile *infoFile, int what ) const; DocumentViewport nextDocumentViewport() const; void notifyAnnotationChanges( int page ); + void notifyFormChanges( int page ); bool canAddAnnotationsNatively() const; bool canModifyExternalAnnotations() const; bool canRemoveExternalAnnotations() const; diff --git a/core/documentcommands.cpp b/core/documentcommands.cpp index 95aded51d..b106f0457 100644 --- a/core/documentcommands.cpp +++ b/core/documentcommands.cpp @@ -368,6 +368,7 @@ void EditFormTextCommand::undo() moveViewportIfBoundingRectNotFullyVisible( m_form->rect(), m_docPriv, m_pageNumber ); m_form->setText( m_prevContents ); emit m_docPriv->m_parent->formTextChangedByUndoRedo( m_pageNumber, m_form, m_prevContents, m_prevCursorPos, m_prevAnchorPos ); + m_docPriv->notifyFormChanges( m_pageNumber ); } void EditFormTextCommand::redo() @@ -375,6 +376,7 @@ void EditFormTextCommand::redo() moveViewportIfBoundingRectNotFullyVisible( m_form->rect(), m_docPriv, m_pageNumber ); m_form->setText( m_newContents ); emit m_docPriv->m_parent->formTextChangedByUndoRedo( m_pageNumber, m_form, m_newContents, m_newCursorPos, m_newCursorPos ); + m_docPriv->notifyFormChanges( m_pageNumber ); } int EditFormTextCommand::id() const @@ -415,6 +417,7 @@ void EditFormListCommand::undo() moveViewportIfBoundingRectNotFullyVisible( m_form->rect(), m_docPriv, m_pageNumber ); m_form->setCurrentChoices( m_prevChoices ); emit m_docPriv->m_parent->formListChangedByUndoRedo( m_pageNumber, m_form, m_prevChoices ); + m_docPriv->notifyFormChanges( m_pageNumber ); } void EditFormListCommand::redo() @@ -422,6 +425,7 @@ void EditFormListCommand::redo() moveViewportIfBoundingRectNotFullyVisible( m_form->rect(), m_docPriv, m_pageNumber ); m_form->setCurrentChoices( m_newChoices ); emit m_docPriv->m_parent->formListChangedByUndoRedo( m_pageNumber, m_form, m_newChoices ); + m_docPriv->notifyFormChanges( m_pageNumber ); } EditFormComboCommand::EditFormComboCommand( Okular::DocumentPrivate* docPriv, @@ -468,6 +472,7 @@ void EditFormComboCommand::undo() } 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() @@ -482,6 +487,7 @@ void EditFormComboCommand::redo() } 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 @@ -538,6 +544,7 @@ void EditFormButtonsCommand::undo() 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() @@ -553,6 +560,7 @@ void EditFormButtonsCommand::redo() 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::clearFormButtonStates() diff --git a/core/observer.h b/core/observer.h index 4d3d9e0e4..bda930672 100644 --- a/core/observer.h +++ b/core/observer.h @@ -45,7 +45,7 @@ class OKULAR_EXPORT DocumentObserver 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) }; /** diff --git a/core/page.cpp b/core/page.cpp index b48f21067..5b988a2cc 100644 --- a/core/page.cpp +++ b/core/page.cpp @@ -832,6 +832,10 @@ void PagePrivate::restoreLocalContents( const QDomNode & pageNode ) // parse formList child element else if ( childElement.tagName() == "forms" ) { + // Clone forms as root node in restoredFormFieldList + const QDomNode clonedNode = restoredFormFieldList.importNode( childElement, true ); + restoredFormFieldList.appendChild( clonedNode ); + if ( formfields.isEmpty() ) continue; @@ -927,7 +931,17 @@ void PagePrivate::saveLocalContents( QDomNode & parentNode, QDomDocument & docum } // 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( "forms" ); diff --git a/core/page_p.h b/core/page_p.h index 69a2c66c2..8be2da08e 100644 --- a/core/page_p.h +++ b/core/page_p.h @@ -48,7 +48,11 @@ enum PageItem /* 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) @@ -142,6 +146,7 @@ class PagePrivate bool m_isBoundingBoxKnown : 1; QDomDocument restoredLocalAnnotationList; // ... + QDomDocument restoredFormFieldList; // ... }; } From 48a23fc443c13168a14b5d4c406072a26c19a008 Mon Sep 17 00:00:00 2001 From: Fabio D'Urso Date: Sun, 11 May 2014 15:22:41 +0200 Subject: [PATCH 14/78] Fallback behavior for generators that support forms but can't save them natively (there is no such generator at the moment) --- core/document.cpp | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/core/document.cpp b/core/document.cpp index dd67e4afb..7d2ae148d 100644 --- a/core/document.cpp +++ b/core/document.cpp @@ -4285,7 +4285,8 @@ bool Document::saveDocumentArchive( const QString &fileName ) // If the generator can save annotations natively, do it KTemporaryFile modifiedFile; bool annotationsSavedNatively = false; - if ( d->canAddAnnotationsNatively() ) + bool formsSavedNatively = false; + if ( d->canAddAnnotationsNatively() || canSaveChanges( SaveFormsCapability ) ) { if ( !modifiedFile.open() ) return false; @@ -4296,7 +4297,8 @@ bool Document::saveDocumentArchive( const QString &fileName ) if ( saveChanges( modifiedFile.fileName(), &errorText ) ) { docPath = modifiedFile.fileName(); // Save this instead of the original file - annotationsSavedNatively = true; + annotationsSavedNatively = d->canAddAnnotationsNatively(); + formsSavedNatively = canSaveChanges( SaveFormsCapability ); } else { @@ -4305,8 +4307,13 @@ bool Document::saveDocumentArchive( const QString &fileName ) } } + PageItems saveWhat = None; + if ( !annotationsSavedNatively ) + saveWhat |= AnnotationPageItems; + if ( !formsSavedNatively ) + saveWhat |= FormFieldPageItems; + KTemporaryFile metadataFile; - PageItems saveWhat = annotationsSavedNatively ? None : AnnotationPageItems; if ( !d->savePageDocumentInfo( &metadataFile, saveWhat ) ) return false; From 31efd827f924cfd9b80e64b8081c84dc2fc3227b Mon Sep 17 00:00:00 2001 From: Fabio D'Urso Date: Sun, 11 May 2014 15:55:46 +0200 Subject: [PATCH 15/78] Load and store generalInfo metadata for .okular archives too generalInfo metadata is everything but annots and forms, which are stored within the .okular archive itself --- core/document.cpp | 47 ++++++++++++++++++++++++++--------------------- core/document_p.h | 13 +++++++++++-- 2 files changed, 37 insertions(+), 23 deletions(-) diff --git a/core/document.cpp b/core/document.cpp index 7d2ae148d..864a899c6 100644 --- a/core/document.cpp +++ b/core/document.cpp @@ -606,7 +606,7 @@ qulonglong DocumentPrivate::getFreeMemory( qulonglong *freeSwap ) #endif } -void DocumentPrivate::loadDocumentInfo() +void 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 { @@ -615,10 +615,10 @@ void DocumentPrivate::loadDocumentInfo() return; QFile infoFile( m_xmlFileName ); - loadDocumentInfo( infoFile ); + loadDocumentInfo( infoFile, loadWhat ); } -void DocumentPrivate::loadDocumentInfo( QFile &infoFile ) +void DocumentPrivate::loadDocumentInfo( QFile &infoFile, LoadDocumentInfoFlags loadWhat ) { if ( !infoFile.exists() || !infoFile.open( QIODevice::ReadOnly ) ) return; @@ -646,7 +646,7 @@ void DocumentPrivate::loadDocumentInfo( QFile &infoFile ) QString catName = topLevelNode.toElement().tagName(); // Restore page attributes (bookmark, annotations, ...) from the DOM - if ( catName == "pageList" ) + if ( catName == "pageList" && ( loadWhat & LoadPageInfo ) ) { QDomNode pageNode = topLevelNode.firstChild(); while ( pageNode.isElement() ) @@ -667,7 +667,7 @@ void DocumentPrivate::loadDocumentInfo( QFile &infoFile ) } // Restore 'general info' from the DOM - else if ( catName == "generalInfo" ) + else if ( catName == "generalInfo" && ( loadWhat & LoadGeneralInfo ) ) { QDomNode infoNode = topLevelNode.firstChild(); while ( infoNode.isElement() ) @@ -1196,19 +1196,23 @@ void DocumentPrivate::saveDocumentInfo() const doc.appendChild( root ); // 2.1. Save page attributes (bookmark state, annotations, ... ) to DOM - 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 ); + // -> skipped for archives, because they store such info in their internal metadata.xml + if ( !m_archiveData ) + { + 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( "generalInfo" ); @@ -2097,7 +2101,7 @@ Document::OpenResult Document::openDocument( const QString & docFile, const KUrl // determine the related "xml document-info" filename d->m_url = url; d->m_docFileName = docFile; - if ( url.isLocalFile() && !d->m_archiveData ) + if ( url.isLocalFile() ) { QString fn = url.fileName(); document_size = fileReadTest.size(); @@ -2223,11 +2227,12 @@ Document::OpenResult Document::openDocument( const QString & docFile, const KUrl // 2. load Additional Data (bookmarks, local annotations and metadata) about the document if ( d->m_archiveData ) { - d->loadDocumentInfo( d->m_archiveData->metadataFile ); + d->loadDocumentInfo( d->m_archiveData->metadataFile, LoadPageInfo ); + d->loadDocumentInfo( LoadGeneralInfo ); } else { - d->loadDocumentInfo(); + d->loadDocumentInfo( LoadAllInfo ); } d->m_metadataLoadingCompleted = true; diff --git a/core/document_p.h b/core/document_p.h index deff05a61..829a44c66 100644 --- a/core/document_p.h +++ b/core/document_p.h @@ -74,6 +74,15 @@ struct DoContinueDirectionMatchSearchStruct 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: @@ -114,8 +123,8 @@ class DocumentPrivate void calculateMaxTextPages(); qulonglong getTotalMemory(); qulonglong getFreeMemory( qulonglong *freeSwap = 0 ); - void loadDocumentInfo(); - void loadDocumentInfo( QFile &infoFile ); + void loadDocumentInfo( LoadDocumentInfoFlags loadWhat ); + void loadDocumentInfo( QFile &infoFile, LoadDocumentInfoFlags loadWhat ); void loadViewsInfo( View *view, const QDomElement &e ); void saveViewsInfo( View *view, QDomElement &e ) const; QString giveAbsolutePath( const QString & fileName ) const; From 859168eaa8b412c46e6295afe33d4b1e3b439c7b Mon Sep 17 00:00:00 2001 From: Fabio D'Urso Date: Sun, 11 May 2014 16:36:28 +0200 Subject: [PATCH 16/78] Update some variables properly when swapping backing file --- core/document.cpp | 86 ++++++++++++++++++++++++++++++----------------- core/document_p.h | 1 + 2 files changed, 56 insertions(+), 31 deletions(-) diff --git a/core/document.cpp b/core/document.cpp index 864a899c6..1309abd7c 100644 --- a/core/document.cpp +++ b/core/document.cpp @@ -2083,7 +2083,6 @@ Document::OpenResult Document::openDocument( const QString & docFile, const KUrl { KMimeType::Ptr mime = _mime; QByteArray filedata; - qint64 document_size = -1; bool isstdin = url.fileName( KUrl::ObeyTrailingSlash ) == QLatin1String( "-" ); bool triedMimeFromFileContent = false; if ( !isstdin ) @@ -2091,36 +2090,11 @@ Document::OpenResult Document::openDocument( const QString & docFile, const KUrl if ( mime.count() <= 0 ) 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() ) - { - QString fn = url.fileName(); - document_size = fileReadTest.size(); - fn = QString::number( document_size ) + '.' + fn + ".xml"; - QString newokular = "okular/docdata/" + fn; - QString newokularfile = KStandardDirs::locateLocal( "data", newokular ); - if ( !QFile::exists( newokularfile ) ) - { - QString oldkpdf = "kpdf/" + fn; - QString oldkpdffile = KStandardDirs::locateLocal( "data", oldkpdf ); - if ( QFile::exists( oldkpdffile ) ) - { - // ### copy or move? - if ( !QFile::copy( oldkpdffile, newokularfile ) ) - return OpenError; - } - } - d->m_xmlFileName = newokularfile; - } + + if ( !d->updateMetadataXmlNameAndDocSize() ) + return OpenError; } else { @@ -2130,7 +2104,7 @@ Document::OpenResult Document::openDocument( const QString & docFile, const KUrl mime = KMimeType::findByContent( filedata ); if ( !mime || mime->name() == QLatin1String( "application/octet-stream" ) ) return OpenError; - document_size = filedata.size(); + d->m_docSize = filedata.size(); triedMimeFromFileContent = true; } @@ -2278,7 +2252,6 @@ Document::OpenResult Document::openDocument( const QString & docFile, const KUrl } AudioPlayer::instance()->d->m_currentDocument = isstdin ? KUrl() : d->m_url; - d->m_docSize = document_size; const QStringList docScripts = d->m_generator->metaData( "DocumentScripts", "JavaScript" ).toStringList(); if ( !docScripts.isEmpty() ) @@ -2293,6 +2266,45 @@ Document::OpenResult Document::openDocument( const QString & docFile, const KUrl 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 fn = QString::number( m_docSize ) + '.' + m_url.fileName() + ".xml"; + const QString newokular = "okular/docdata/" + fn; + const QString newokularfile = KStandardDirs::locateLocal( "data", newokular ); + if ( !QFile::exists( newokularfile ) ) + { + const QString oldkpdf = "kpdf/" + fn; + const QString oldkpdffile = KStandardDirs::locateLocal( "data", oldkpdf ); + if ( QFile::exists( oldkpdffile ) ) + { + // ### copy or move? + if ( !QFile::copy( oldkpdffile, newokularfile ) ) + return false; + } + } + + kDebug(OkularDebug) << "Metadata file is now:" << newokularfile; + m_xmlFileName = newokularfile; + } + else + { + kDebug(OkularDebug) << "Metadata file: disabled"; + m_xmlFileName = QString(); + } + + return true; +} + KXMLGUIClient* Document::guiClient() { @@ -4001,10 +4013,16 @@ bool Document::swapBackingFile( const QString &newFileName, const KUrl & url ) if ( !genIt->generator->hasFeature( Generator::SwapBackingFile ) ) return false; + // Save metadata about the file we're about to close + d->saveDocumentInfo(); + kDebug(OkularDebug) << "Swapping backing file to" << newFileName; if (genIt->generator->swapBackingFile( newFileName )) { d->m_url = url; + d->m_docFileName = newFileName; + d->updateMetadataXmlNameAndDocSize(); + d->m_bookmarkManager->setUrl( d->m_url ); return true; } else @@ -4025,6 +4043,9 @@ bool Document::swapBackingFileArchive( const QString &newFileName, const KUrl & if ( !genIt->generator->hasFeature( Generator::SwapBackingFile ) ) return false; + // Save metadata about the file we're about to close + d->saveDocumentInfo(); + kDebug(OkularDebug) << "Swapping backing archive to" << newFileName; ArchiveData *newArchive = DocumentPrivate::unpackDocumentArchive( newFileName ); @@ -4039,6 +4060,9 @@ bool Document::swapBackingFileArchive( const QString &newFileName, const KUrl & delete d->m_archiveData; d->m_archiveData = newArchive; d->m_url = url; + d->m_docFileName = tempFileName; + d->updateMetadataXmlNameAndDocSize(); + d->m_bookmarkManager->setUrl( d->m_url ); return true; } else diff --git a/core/document_p.h b/core/document_p.h index 829a44c66..a265f9b26 100644 --- a/core/document_p.h +++ b/core/document_p.h @@ -113,6 +113,7 @@ class DocumentPrivate } // private methods + bool updateMetadataXmlNameAndDocSize(); QString pagesSizeString() const; QString namePaperSize(double inchesWidth, double inchesHeight) const; QString localizedSize(const QSizeF &size) const; From 48c324f77ce071b164d20594c3577cff6a85a8b8 Mon Sep 17 00:00:00 2001 From: Fabio D'Urso Date: Sun, 11 May 2014 17:38:30 +0200 Subject: [PATCH 17/78] Added UrlChanged notification in notifySetup to fix BookmarkList not updating after save as --- core/document.cpp | 8 +++++--- core/observer.h | 3 ++- ui/bookmarklist.cpp | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/core/document.cpp b/core/document.cpp index 1309abd7c..0d5a61ecf 100644 --- a/core/document.cpp +++ b/core/document.cpp @@ -2213,7 +2213,7 @@ Document::OpenResult Document::openDocument( const QString & docFile, const KUrl 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); @@ -2408,7 +2408,7 @@ void Document::closeDocument() 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(); @@ -2462,7 +2462,7 @@ void Document::addObserver( DocumentObserver * pObserver ) // if the observer is added while a document is already opened, tell it if ( !d->m_pagesVector.isEmpty() ) { - pObserver->notifySetup( d->m_pagesVector, DocumentObserver::DocumentChanged ); + pObserver->notifySetup( d->m_pagesVector, DocumentObserver::DocumentChanged | DocumentObserver::UrlChanged ); pObserver->notifyViewportChanged( false /*disables smoothMove*/ ); } } @@ -4023,6 +4023,7 @@ bool Document::swapBackingFile( const QString &newFileName, const KUrl & url ) d->m_docFileName = newFileName; d->updateMetadataXmlNameAndDocSize(); d->m_bookmarkManager->setUrl( d->m_url ); + foreachObserver( notifySetup( d->m_pagesVector, DocumentObserver::UrlChanged ) ); return true; } else @@ -4063,6 +4064,7 @@ bool Document::swapBackingFileArchive( const QString &newFileName, const KUrl & d->m_docFileName = tempFileName; d->updateMetadataXmlNameAndDocSize(); d->m_bookmarkManager->setUrl( d->m_url ); + foreachObserver( notifySetup( d->m_pagesVector, DocumentObserver::UrlChanged ) ); return true; } else diff --git a/core/observer.h b/core/observer.h index bda930672..671767f04 100644 --- a/core/observer.h +++ b/core/observer.h @@ -53,7 +53,8 @@ class OKULAR_EXPORT DocumentObserver */ 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 0.20 (KDE 4.14) }; /** diff --git a/ui/bookmarklist.cpp b/ui/bookmarklist.cpp index 1db03dfdf..20917f29f 100644 --- a/ui/bookmarklist.cpp +++ b/ui/bookmarklist.cpp @@ -169,7 +169,7 @@ BookmarkList::~BookmarkList() void BookmarkList::notifySetup( const QVector< Okular::Page * > & pages, int setupFlags ) { Q_UNUSED( pages ); - if ( !( setupFlags & Okular::DocumentObserver::DocumentChanged ) ) + if ( !( setupFlags & Okular::DocumentObserver::UrlChanged ) ) return; // clear contents From b6d89ebc0f60b412e3ca37b33bf9af458701fe53 Mon Sep 17 00:00:00 2001 From: Fabio D'Urso Date: Sun, 11 May 2014 17:41:52 +0200 Subject: [PATCH 18/78] Update text "Do you want to save -annotation- changes?" in queryClose --- part.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/part.cpp b/part.cpp index 0d9994149..45fb9d53e 100644 --- a/part.cpp +++ b/part.cpp @@ -1549,7 +1549,7 @@ bool Part::queryClose() 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 or discard them?" ), i18n( "Close Document" ), KStandardGuiItem::save(), KStandardGuiItem::discard() ); From 8427d40c48dc68a03b5efce735e3cf6a9485e455 Mon Sep 17 00:00:00 2001 From: Fabio D'Urso Date: Sun, 11 May 2014 18:21:18 +0200 Subject: [PATCH 19/78] Show [modified] in the window title if there are unsaved changes --- part.cpp | 4 ++++ shell/shell.cpp | 15 +++++++++++++++ shell/shell.h | 5 ++++- 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/part.cpp b/part.cpp index 45fb9d53e..7495eb81b 100644 --- a/part.cpp +++ b/part.cpp @@ -1108,7 +1108,10 @@ void Part::notifyViewportChanged( bool /*smoothMove*/ ) void Part::notifyPageChanged( int page, int flags ) { if ( flags & Okular::DocumentObserver::NeedSaveAs ) + { setModified(); + setWindowTitleFromDocument(); + } if ( !(flags & Okular::DocumentObserver::Bookmark ) ) return; @@ -2435,6 +2438,7 @@ bool Part::saveAs( const KUrl & saveUrl, bool saveAsOkularArchive ) } setModified( false ); + setWindowTitleFromDocument(); bool reloadedCorrectly = true; diff --git a/shell/shell.cpp b/shell/shell.cpp index 9ee422a60..e14adc588 100644 --- a/shell/shell.cpp +++ b/shell/shell.cpp @@ -429,6 +429,21 @@ void Shell::setFullScreen( bool useFullScreen ) setWindowState( windowState() & ~Qt::WindowFullScreen ); // reset } +void Shell::setCaption( const QString &caption ) +{ + bool modified = false; + + const int activeTab = m_tabWidget->currentIndex(); + if ( activeTab >= 0 && activeTab < m_tabs.size() ) + { + KParts::ReadWritePart* const activePart = m_tabs[activeTab].part; + if ( activePart->isModified() ) + modified = true; + } + + setCaption( caption, modified ); +} + void Shell::showEvent(QShowEvent *e) { if (m_showMenuBarAction) diff --git a/shell/shell.h b/shell/shell.h index f25b3d8c0..e55e81c5a 100644 --- a/shell/shell.h +++ b/shell/shell.h @@ -82,8 +82,11 @@ protected: void readSettings(); void writeSettings(); void setFullScreen( bool ); - bool queryClose(); + using KParts::MainWindow::setCaption; + void setCaption( const QString &caption ); + + bool queryClose(); void showEvent(QShowEvent *event); private slots: From 4cfea5e4c4d7086d81812d18f279f8bc39266ad1 Mon Sep 17 00:00:00 2001 From: Fabio D'Urso Date: Mon, 8 Sep 2014 16:59:14 +0200 Subject: [PATCH 20/78] Added test to check save to native and to .okular features --- part.cpp | 10 ++--- part.h | 11 +++++- tests/parttest.cpp | 93 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 108 insertions(+), 6 deletions(-) diff --git a/part.cpp b/part.cpp index e45f11b7c..409018f7a 100644 --- a/part.cpp +++ b/part.cpp @@ -2264,7 +2264,7 @@ bool Part::slotSaveFileAs( bool showOkularArchiveAsDefaultFormat ) // Has the user chosen to save in .okular archive format? const bool saveAsOkularArchive = ( fd.currentMimeFilter() == "application/vnd.kde.okular-archive" ); - return saveAs( saveUrl, saveAsOkularArchive ); + return saveAs( saveUrl, saveAsOkularArchive ? SaveAsOkularArchive : NoSaveAsFlags ); } else { @@ -2275,10 +2275,10 @@ bool Part::slotSaveFileAs( bool showOkularArchiveAsDefaultFormat ) bool Part::saveAs(const KUrl & saveUrl) { // Save in the same format (.okular vs native) as the current file - return saveAs( saveUrl, isDocumentArchive /* saveAsOkularArchive */ ); + return saveAs( saveUrl, isDocumentArchive ? SaveAsOkularArchive : NoSaveAsFlags ); } -bool Part::saveAs( const KUrl & saveUrl, bool saveAsOkularArchive ) +bool Part::saveAs( const KUrl & saveUrl, SaveAsFlags flags ) { KTemporaryFile tf; QString fileName; @@ -2294,7 +2294,7 @@ bool Part::saveAs( const KUrl & saveUrl, bool saveAsOkularArchive ) KIO::Job *copyJob; // this will be filled with the job that writes to saveUrl // Does the user want a .okular archive? - if ( saveAsOkularArchive ) + if ( flags & SaveAsOkularArchive ) { if ( !m_document->saveDocumentArchive( fileName ) ) { @@ -2351,7 +2351,7 @@ bool Part::saveAs( const KUrl & saveUrl, bool saveAsOkularArchive ) QStringList listOfwontSaves; if ( wontSaveForms ) listOfwontSaves << i18n( "Filled form contents" ); if ( wontSaveAnnotations ) listOfwontSaves << i18n( "User annotations" ); - if ( !listOfwontSaves.isEmpty() ) + if ( !listOfwontSaves.isEmpty() && !( flags && SaveAsDontShowWarning ) ) { int result = KMessageBox::warningYesNoCancelList( widget(), i18n( "The following elements cannot be saved in this format and will be lost.
If you want to preserve them, please use the Okular document archive format." ), diff --git a/part.h b/part.h index 25070ac5f..e2d5a4162 100644 --- a/part.h +++ b/part.h @@ -249,7 +249,16 @@ class OKULAR_PART_EXPORT Part : public KParts::ReadWritePart, public Okular::Doc void unsetDummyMode(); void slotRenameBookmark( const DocumentViewport &viewport ); void resetStartArguments(); - bool saveAs( const KUrl & saveUrl, bool saveAsOkularArchive ); + + enum SaveAsFlag + { + NoSaveAsFlags = 0, ///< No options + SaveAsOkularArchive = 1, ///< Save as Okular Archive (.okular) instead of document's native format + SaveAsDontShowWarning = 2, ///< Don't show warning for unsupported save features + }; + Q_DECLARE_FLAGS( SaveAsFlags, SaveAsFlag ) + + bool saveAs( const KUrl & saveUrl, SaveAsFlags flags ); void setFileToWatch( const QString &filePath ); void unsetFileToWatch(); diff --git a/tests/parttest.cpp b/tests/parttest.cpp index db43eee7b..9e29d3548 100644 --- a/tests/parttest.cpp +++ b/tests/parttest.cpp @@ -10,6 +10,8 @@ #include #include "../part.h" +#include "../core/annotations.h" +#include "../core/page.h" #include "../ui/toc.h" #include @@ -31,6 +33,8 @@ class PartTest void testTOCReload(); void testFowardPDF(); void testFowardPDF_data(); + void testSaveAs(); + void testSaveAs_data(); void testGeneratorPreferences(); }; @@ -140,6 +144,95 @@ void PartTest::testFowardPDF_data() QTest::newRow("utf8") << QString(KGlobal::dirs()->resourceDirs("tmp")[0] + QString::fromUtf8("ßðđđŋßðđŋ")); } +void PartTest::testSaveAs() +{ + QFETCH(QString, file); + QFETCH(QString, extension); + QFETCH(bool, nativelySupportsAnnotations); + + // If we expect not to be able to preserve annotations when we write a + // native file, disable the warning otherwise the test will wait for the + // user to confirm. On the other end, if we expect annotations to be + // preserved (and thus no warning), we keep the warning on so that if it + // shows the test timeouts and we can notice that something is wrong. + const Part::SaveAsFlags saveAsNativeFlags = nativelySupportsAnnotations ? + Part::NoSaveAsFlags : Part::SaveAsDontShowWarning; + + 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(NULL, NULL, QVariantList(), KGlobal::mainComponent()); + part.openDocument( file ); + + 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(); + + QVERIFY( part.saveAs( KUrl( archiveSave.fileName() ), Part::SaveAsOkularArchive ) ); + QVERIFY( part.saveAs( KUrl( nativeDirectSave.fileName() ), saveAsNativeFlags ) ); + + part.closeUrl(); + } + + qDebug() << "Open the .okular, check that the annotation is present and save to native"; + { + Okular::Part part(NULL, NULL, QVariantList(), KGlobal::mainComponent()); + part.openDocument( archiveSave.fileName() ); + + QVERIFY( part.m_document->page( 0 )->annotations().size() == 1 ); + QVERIFY( part.m_document->page( 0 )->annotations().first()->uniqueName() == annotName ); + + QVERIFY( part.saveAs( KUrl( nativeFromArchiveFile.fileName() ), saveAsNativeFlags ) ); + + part.closeUrl(); + } + + qDebug() << "Open the native file saved directly, and check that the annot" + << "is there iff we expect it"; + { + Okular::Part part(NULL, NULL, QVariantList(), KGlobal::mainComponent()); + part.openDocument( nativeDirectSave.fileName() ); + + QCOMPARE( part.m_document->page( 0 )->annotations().size(), nativelySupportsAnnotations ? 1 : 0 ); + if ( nativelySupportsAnnotations ) + QVERIFY( 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(NULL, NULL, QVariantList(), KGlobal::mainComponent()); + part.openDocument( nativeFromArchiveFile.fileName() ); + + QCOMPARE( part.m_document->page( 0 )->annotations().size(), nativelySupportsAnnotations ? 1 : 0 ); + if ( nativelySupportsAnnotations ) + QVERIFY( 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::newRow("pdf") << KDESRCDIR "data/file1.pdf" << "pdf" << true; + QTest::newRow("epub") << KDESRCDIR "data/contents.epub" << "epub" << false; +} + void PartTest::testGeneratorPreferences() { KConfigDialog * dialog; From 354e343949c3699756cbb6cc937526b1cec65d2c Mon Sep 17 00:00:00 2001 From: Fabio D'Urso Date: Wed, 10 Sep 2014 11:16:22 +0200 Subject: [PATCH 21/78] Fix previous commit: QVERIFY( ... == ... ) -> QCOMPARE --- tests/parttest.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/parttest.cpp b/tests/parttest.cpp index 9e29d3548..837058064 100644 --- a/tests/parttest.cpp +++ b/tests/parttest.cpp @@ -188,8 +188,8 @@ void PartTest::testSaveAs() Okular::Part part(NULL, NULL, QVariantList(), KGlobal::mainComponent()); part.openDocument( archiveSave.fileName() ); - QVERIFY( part.m_document->page( 0 )->annotations().size() == 1 ); - QVERIFY( part.m_document->page( 0 )->annotations().first()->uniqueName() == annotName ); + QCOMPARE( part.m_document->page( 0 )->annotations().size(), 1 ); + QCOMPARE( part.m_document->page( 0 )->annotations().first()->uniqueName(), annotName ); QVERIFY( part.saveAs( KUrl( nativeFromArchiveFile.fileName() ), saveAsNativeFlags ) ); @@ -204,7 +204,7 @@ void PartTest::testSaveAs() QCOMPARE( part.m_document->page( 0 )->annotations().size(), nativelySupportsAnnotations ? 1 : 0 ); if ( nativelySupportsAnnotations ) - QVERIFY( part.m_document->page( 0 )->annotations().first()->uniqueName() == annotName ); + QCOMPARE( part.m_document->page( 0 )->annotations().first()->uniqueName(), annotName ); part.closeUrl(); } @@ -217,7 +217,7 @@ void PartTest::testSaveAs() QCOMPARE( part.m_document->page( 0 )->annotations().size(), nativelySupportsAnnotations ? 1 : 0 ); if ( nativelySupportsAnnotations ) - QVERIFY( part.m_document->page( 0 )->annotations().first()->uniqueName() == annotName ); + QCOMPARE( part.m_document->page( 0 )->annotations().first()->uniqueName(), annotName ); part.closeUrl(); } From 3e1b1ff04c227012602a4ca4d96a29b6667b1261 Mon Sep 17 00:00:00 2001 From: Fabio D'Urso Date: Wed, 10 Sep 2014 12:15:09 +0200 Subject: [PATCH 22/78] Fix missing return I forgot in the merge commit --- part.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/part.cpp b/part.cpp index 409018f7a..896e12ff2 100644 --- a/part.cpp +++ b/part.cpp @@ -1269,6 +1269,8 @@ Document::OpenResult Part::doOpenFile( const KMimeType::Ptr &mimeA, const QStrin if (!m_document->swapBackingFile( fileNameToOpen, url() )) return Document::OpenError; } + + return Document::OpenSuccess; } isDocumentArchive = false; From e059d2652caf965f2ca20f5da666d64db4672a80 Mon Sep 17 00:00:00 2001 From: Fabio D'Urso Date: Tue, 9 Sep 2014 16:08:32 +0200 Subject: [PATCH 23/78] core: support for migration of annots and forms out of docdata/ --- core/document.cpp | 55 ++++++++++++++++++++++++++++++++---------- core/document.h | 19 +++++++++++++++ core/document_p.h | 14 ++++++++--- core/page.cpp | 8 +++++- core/page_p.h | 2 +- tests/documenttest.cpp | 52 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 132 insertions(+), 18 deletions(-) diff --git a/core/document.cpp b/core/document.cpp index b2867ec0e..526c1e186 100644 --- a/core/document.cpp +++ b/core/document.cpp @@ -606,22 +606,22 @@ qulonglong DocumentPrivate::getFreeMemory( qulonglong *freeSwap ) #endif } -void DocumentPrivate::loadDocumentInfo( LoadDocumentInfoFlags loadWhat ) +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 { //kDebug(OkularDebug).nospace() << "Using '" << d->m_xmlFileName << "' as document info file."; if ( m_xmlFileName.isEmpty() ) - return; + return false; QFile infoFile( m_xmlFileName ); - loadDocumentInfo( infoFile, loadWhat ); + return loadDocumentInfo( infoFile, loadWhat ); } -void DocumentPrivate::loadDocumentInfo( QFile &infoFile, LoadDocumentInfoFlags loadWhat ) +bool DocumentPrivate::loadDocumentInfo( QFile &infoFile, LoadDocumentInfoFlags loadWhat ) { if ( !infoFile.exists() || !infoFile.open( QIODevice::ReadOnly ) ) - return; + return false; // Load DOM from XML file QDomDocument doc( "documentInfo" ); @@ -629,15 +629,16 @@ void DocumentPrivate::loadDocumentInfo( QFile &infoFile, LoadDocumentInfoFlags l { kDebug(OkularDebug) << "Can't load XML pair! Check for broken xml."; infoFile.close(); - return; + return false; } infoFile.close(); QDomElement root = doc.documentElement(); if ( root.tagName() != "documentInfo" ) - return; + return false; KUrl documentUrl( root.attribute( "url" ) ); + bool loadedAnything = false; // set if something gets actually loaded // Parse the DOM tree QDomNode topLevelNode = root.firstChild(); @@ -645,7 +646,7 @@ void DocumentPrivate::loadDocumentInfo( QFile &infoFile, LoadDocumentInfoFlags l { QString catName = topLevelNode.toElement().tagName(); - // Restore page attributes (bookmark, annotations, ...) from the DOM + // Restore page attributes (form data, annotations, ...) from the DOM if ( catName == "pageList" && ( loadWhat & LoadPageInfo ) ) { QDomNode pageNode = topLevelNode.firstChild(); @@ -660,7 +661,10 @@ void DocumentPrivate::loadDocumentInfo( QFile &infoFile, LoadDocumentInfoFlags l // 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(); } @@ -689,6 +693,7 @@ void DocumentPrivate::loadDocumentInfo( QFile &infoFile, LoadDocumentInfoFlags l QString vpString = historyElement.attribute( "viewport" ); m_viewportIterator = m_viewportHistory.insert( m_viewportHistory.end(), DocumentViewport( vpString ) ); + loadedAnything = true; } historyNode = historyNode.nextSibling(); } @@ -704,6 +709,7 @@ void DocumentPrivate::loadDocumentInfo( QFile &infoFile, LoadDocumentInfoFlags l if ( ok && newrotation != 0 ) { setRotationInternal( newrotation, false ); + loadedAnything = true; } } else if ( infoElement.tagName() == "views" ) @@ -720,6 +726,7 @@ void DocumentPrivate::loadDocumentInfo( QFile &infoFile, LoadDocumentInfoFlags l if ( view->name() == viewName ) { loadViewsInfo( view, viewElement ); + loadedAnything = true; break; } } @@ -733,6 +740,8 @@ void DocumentPrivate::loadDocumentInfo( QFile &infoFile, LoadDocumentInfoFlags l topLevelNode = topLevelNode.nextSibling(); } // + + return loadedAnything; } void DocumentPrivate::loadViewsInfo( View *view, const QDomElement &e ) @@ -1198,8 +1207,8 @@ void DocumentPrivate::saveDocumentInfo() const doc.appendChild( root ); // 2.1. Save page attributes (bookmark state, annotations, ... ) to DOM - // -> skipped for archives, because they store such info in their internal metadata.xml - if ( !m_archiveData ) + // -> do this there are not-yet-migrated annots or forms in docdata/ + if ( m_docdataMigrationNeeded ) { QDomElement pageList = doc.createElement( "pageList" ); root.appendChild( pageList ); @@ -2219,6 +2228,7 @@ Document::OpenResult Document::openDocument( const QString & docFile, const KUrl p->d->m_doc = d; d->m_metadataLoadingCompleted = false; + d->m_docdataMigrationNeeded = false; // 2. load Additional Data (bookmarks, local annotations and metadata) about the document if ( d->m_archiveData ) @@ -2228,7 +2238,9 @@ Document::OpenResult Document::openDocument( const QString & docFile, const KUrl } else { - d->loadDocumentInfo( LoadAllInfo ); + if ( d->loadDocumentInfo( LoadPageInfo ) ) + d->m_docdataMigrationNeeded = true; + d->loadDocumentInfo( LoadGeneralInfo ); } d->m_metadataLoadingCompleted = true; @@ -2460,6 +2472,7 @@ void Document::closeDocument() AudioPlayer::instance()->d->m_currentDocument = KUrl(); d->m_undoStack->clear(); + d->m_docdataMigrationNeeded = false; } void Document::addObserver( DocumentObserver * pObserver ) @@ -2707,7 +2720,9 @@ KUrl Document::currentDocument() const 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 @@ -4432,6 +4447,20 @@ void Document::walletDataForFile( const QString &fileName, QString *walletName, } } +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 ) ); + } +} + void DocumentPrivate::requestDone( PixmapRequest * req ) { if ( !req ) diff --git a/core/document.h b/core/document.h index 8e86e6fce..555b027ad 100644 --- a/core/document.h +++ b/core/document.h @@ -886,6 +886,25 @@ class OKULAR_EXPORT Document : public QObject */ 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 0.21 + */ + 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 0.21 + */ + void docdataMigrationDone(); + public Q_SLOTS: /** * This slot is called whenever the user changes the @p rotation of diff --git a/core/document_p.h b/core/document_p.h index 0bbcf2440..126214634 100644 --- a/core/document_p.h +++ b/core/document_p.h @@ -106,7 +106,8 @@ class DocumentPrivate m_archiveData( 0 ), m_fontsCached( false ), m_annotationEditingEnabled ( true ), - m_annotationBeingMoved( false ) + m_annotationBeingMoved( false ), + m_docdataMigrationNeeded( false ) { calculateMaxTextPages(); } @@ -123,8 +124,8 @@ class DocumentPrivate void calculateMaxTextPages(); qulonglong getTotalMemory(); qulonglong getFreeMemory( qulonglong *freeSwap = 0 ); - void loadDocumentInfo( LoadDocumentInfoFlags loadWhat ); - void loadDocumentInfo( QFile &infoFile, LoadDocumentInfoFlags loadWhat ); + bool loadDocumentInfo( LoadDocumentInfoFlags loadWhat ); + bool loadDocumentInfo( QFile &infoFile, LoadDocumentInfoFlags loadWhat ); void loadViewsInfo( View *view, const QDomElement &e ); void saveViewsInfo( View *view, QDomElement &e ) const; QString giveAbsolutePath( const QString & fileName ) const; @@ -280,6 +281,13 @@ class DocumentPrivate 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; }; class DocumentInfoPrivate diff --git a/core/page.cpp b/core/page.cpp index 5b988a2cc..84126abee 100644 --- a/core/page.cpp +++ b/core/page.cpp @@ -785,8 +785,10 @@ void Page::deleteAnnotations() 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() ) @@ -821,6 +823,7 @@ void PagePrivate::restoreLocalContents( const QDomNode & pageNode ) { m_doc->performAddPageAnnotation(m_number, annotation); kDebug(OkularDebug) << "restored annot:" << annotation->uniqueName(); + loadedAnything = true; } else kWarning(OkularDebug).nospace() << "page (" << m_number << "): can't restore an annotation from XML."; @@ -868,9 +871,12 @@ void PagePrivate::restoreLocalContents( const QDomNode & pageNode ) QString value = formElement.attribute( "value" ); (*wantedIt)->d_ptr->setValue( value ); + loadedAnything = true; } } } + + return loadedAnything; } void PagePrivate::saveLocalContents( QDomNode & parentNode, QDomDocument & document, PageItems what ) const diff --git a/core/page_p.h b/core/page_p.h index 8be2da08e..1588d2c5c 100644 --- a/core/page_p.h +++ b/core/page_p.h @@ -68,7 +68,7 @@ class PagePrivate /** * 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. diff --git a/tests/documenttest.cpp b/tests/documenttest.cpp index 8a56136a3..f70518bc6 100644 --- a/tests/documenttest.cpp +++ b/tests/documenttest.cpp @@ -11,9 +11,12 @@ #include +#include "../core/annotations.h" #include "../core/document.h" +#include "../core/document_p.h" #include "../core/generator.h" #include "../core/observer.h" +#include "../core/page.h" #include "../core/rotationjob_p.h" #include "../settings_core.h" @@ -24,6 +27,7 @@ class DocumentTest private slots: void testCloseDuringRotationJob(); + void testDocdataMigration(); }; // Test that we don't crash if the document is closed while a RotationJob @@ -58,5 +62,53 @@ void DocumentTest::testCloseDuringRotationJob() qApp->processEvents(); } +// 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 KUrl testFileUrl("file://" 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 ); + const KMimeType::Ptr mime = KMimeType::findByPath( 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(); +} + QTEST_KDEMAIN( DocumentTest, GUI ) #include "documenttest.moc" From dc655cf348a6b3be54f355e7556c6640b062627f Mon Sep 17 00:00:00 2001 From: Fabio D'Urso Date: Tue, 9 Sep 2014 16:09:40 +0200 Subject: [PATCH 24/78] Show the window before calling Part::openUrl, so that KMessageBoxes have a visible parent --- shell/shell.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/shell.cpp b/shell/shell.cpp index e208ce4d1..7cfa83007 100644 --- a/shell/shell.cpp +++ b/shell/shell.cpp @@ -215,8 +215,8 @@ void Shell::openUrl( const KUrl & url, const QString &serializedOptions ) else { Shell* newShell = new Shell( serializedOptions ); - newShell->openUrl( url, serializedOptions ); newShell->show(); + newShell->openUrl( url, serializedOptions ); } } } From 8e70c16f12c83778ec5ad08cdc6ac563be3b5f35 Mon Sep 17 00:00:00 2001 From: Fabio D'Urso Date: Tue, 9 Sep 2014 16:57:41 +0200 Subject: [PATCH 25/78] Make it possile to re-enable widgets that had been disabled --- ui/formwidgets.cpp | 39 +++++++++++++++++++-------------------- ui/formwidgets.h | 3 ++- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/ui/formwidgets.cpp b/ui/formwidgets.cpp index 8eb040915..83e2aaecf 100644 --- a/ui/formwidgets.cpp +++ b/ui/formwidgets.cpp @@ -241,9 +241,11 @@ FormWidgetIface * FormWidgetFactory::createWidget( Okular::FormField * ff, QWidg } -FormWidgetIface::FormWidgetIface( QWidget * w, Okular::FormField * ff ) - : m_controller( 0 ), m_widget( w ), m_ff( ff ), m_pageItem( 0 ) +FormWidgetIface::FormWidgetIface( QWidget * w, Okular::FormField * ff, bool canBeEnabled ) + : m_controller( 0 ), m_widget( w ), m_ff( ff ), m_pageItem( 0 ), + m_canBeEnabled( canBeEnabled ) { + m_widget->setEnabled( m_canBeEnabled ); } FormWidgetIface::~FormWidgetIface() @@ -279,10 +281,7 @@ bool FormWidgetIface::setVisibility( bool visible ) void FormWidgetIface::setCanBeFilled( bool fill ) { - if ( m_widget->isEnabled() ) - { - m_widget->setEnabled( fill ); - } + m_widget->setEnabled( fill && m_canBeEnabled ); } void FormWidgetIface::setPageItem( PageViewItem *pageItem ) @@ -312,10 +311,10 @@ QAbstractButton* FormWidgetIface::button() PushButtonEdit::PushButtonEdit( Okular::FormFieldButton * button, QWidget * parent ) - : QPushButton( parent ), FormWidgetIface( this, button ), m_form( button ) + : QPushButton( parent ), FormWidgetIface( this, button, !button->isReadOnly() ), + m_form( button ) { setText( m_form->caption() ); - setEnabled( !m_form->isReadOnly() ); setVisible( m_form->isVisible() ); setCursor( Qt::ArrowCursor ); @@ -333,10 +332,10 @@ void PushButtonEdit::slotClicked() CheckBoxEdit::CheckBoxEdit( Okular::FormFieldButton * button, QWidget * parent ) - : QCheckBox( parent ), FormWidgetIface( this, button ), m_form( button ) + : QCheckBox( parent ), FormWidgetIface( this, button, !button->isReadOnly() ), + m_form( button ) { setText( m_form->caption() ); - setEnabled( !m_form->isReadOnly() ); setVisible( m_form->isVisible() ); setCursor( Qt::ArrowCursor ); @@ -363,10 +362,10 @@ void CheckBoxEdit::slotStateChanged( int state ) RadioButtonEdit::RadioButtonEdit( Okular::FormFieldButton * button, QWidget * parent ) - : QRadioButton( parent ), FormWidgetIface( this, button ), m_form( button ) + : QRadioButton( parent ), FormWidgetIface( this, button, !button->isReadOnly() ), + m_form( button ) { setText( m_form->caption() ); - setEnabled( !m_form->isReadOnly() ); setVisible( m_form->isVisible() ); setCursor( Qt::ArrowCursor ); @@ -385,7 +384,7 @@ QAbstractButton* RadioButtonEdit::button() } FormLineEdit::FormLineEdit( Okular::FormFieldText * text, QWidget * parent ) - : QLineEdit( parent ), FormWidgetIface( this, text ), m_form( text ) + : QLineEdit( parent ), FormWidgetIface( this, text, true ), m_form( text ) { int maxlen = m_form->maximumLength(); if ( maxlen >= 0 ) @@ -507,7 +506,7 @@ void FormLineEdit::slotHandleTextChangedByUndoRedo( int pageNumber, } TextAreaEdit::TextAreaEdit( Okular::FormFieldText * text, QWidget * parent ) -: KTextEdit( parent ), FormWidgetIface( this, text ), m_form( text ) +: KTextEdit( parent ), FormWidgetIface( this, text, true ), m_form( text ) { setAcceptRichText( m_form->isRichText() ); setCheckSpellingEnabled( m_form->canBeSpellChecked() ); @@ -618,13 +617,13 @@ void TextAreaEdit::slotChanged() FileEdit::FileEdit( Okular::FormFieldText * text, QWidget * parent ) - : KUrlRequester( parent ), FormWidgetIface( this, text ), m_form( text ) + : KUrlRequester( parent ), FormWidgetIface( this, text, !text->isReadOnly() ), + m_form( text ) { setMode( KFile::File | KFile::ExistingOnly | KFile::LocalOnly ); setFilter( i18n( "*|All Files" ) ); setUrl( KUrl( m_form->text() ) ); lineEdit()->setAlignment( m_form->textAlignment() ); - setEnabled( !m_form->isReadOnly() ); m_prevCursorPos = lineEdit()->cursorPosition(); m_prevAnchorPos = lineEdit()->cursorPosition(); @@ -745,7 +744,8 @@ void FileEdit::slotHandleFileChangedByUndoRedo( int pageNumber, } ListEdit::ListEdit( Okular::FormFieldChoice * choice, QWidget * parent ) - : QListWidget( parent ), FormWidgetIface( this, choice ), m_form( choice ) + : QListWidget( parent ), FormWidgetIface( this, choice, !choice->isReadOnly() ), + m_form( choice ) { addItems( m_form->choices() ); setSelectionMode( m_form->multiSelect() ? QAbstractItemView::ExtendedSelection : QAbstractItemView::SingleSelection ); @@ -765,7 +765,6 @@ ListEdit::ListEdit( Okular::FormFieldChoice * choice, QWidget * parent ) scrollToItem( item( selectedItems.at(0) ) ); } } - setEnabled( !m_form->isReadOnly() ); if ( !m_form->isReadOnly() ) { @@ -815,7 +814,8 @@ void ListEdit::slotHandleFormListChangedByUndoRedo( int pageNumber, } ComboEdit::ComboEdit( Okular::FormFieldChoice * choice, QWidget * parent ) - : QComboBox( parent ), FormWidgetIface( this, choice ), m_form( choice ) + : QComboBox( parent ), FormWidgetIface( this, choice, !choice->isReadOnly() ), + m_form( choice ) { addItems( m_form->choices() ); setEditable( true ); @@ -824,7 +824,6 @@ ComboEdit::ComboEdit( Okular::FormFieldChoice * choice, QWidget * parent ) QList< int > selectedItems = m_form->currentChoices(); if ( selectedItems.count() == 1 && selectedItems.at(0) >= 0 && selectedItems.at(0) < count() ) setCurrentIndex( selectedItems.at(0) ); - setEnabled( !m_form->isReadOnly() ); if ( m_form->isEditable() && !m_form->editChoice().isEmpty() ) lineEdit()->setText( m_form->editChoice() ); diff --git a/ui/formwidgets.h b/ui/formwidgets.h index 9b5d455e6..5c959c7ea 100644 --- a/ui/formwidgets.h +++ b/ui/formwidgets.h @@ -139,7 +139,7 @@ class FormWidgetFactory class FormWidgetIface { public: - FormWidgetIface( QWidget * w, Okular::FormField * ff ); + FormWidgetIface( QWidget * w, Okular::FormField * ff, bool canBeEnabled ); virtual ~FormWidgetIface(); Okular::NormalizedRect rect() const; @@ -162,6 +162,7 @@ class FormWidgetIface QWidget * m_widget; Okular::FormField * m_ff; PageViewItem * m_pageItem; + bool m_canBeEnabled; }; From f36d1de126a68bc679c88121a4e363ca6aac44c2 Mon Sep 17 00:00:00 2001 From: Fabio D'Urso Date: Tue, 9 Sep 2014 17:22:34 +0200 Subject: [PATCH 26/78] pageview: React to Document::isAllowed(AllowFillForms) changes in notifySetup --- ui/pageview.cpp | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/ui/pageview.cpp b/ui/pageview.cpp index 39a9390f8..ba5d2d3a8 100644 --- a/ui/pageview.cpp +++ b/ui/pageview.cpp @@ -879,13 +879,26 @@ void PageView::selectAll() void PageView::notifySetup( const QVector< Okular::Page * > & pageSet, int setupFlags ) { bool documentChanged = setupFlags & Okular::DocumentObserver::DocumentChanged; + const bool allowfillforms = d->document->isAllowed( Okular::AllowFillForms ); + // reuse current pages if nothing new if ( ( pageSet.count() == d->items.count() ) && !documentChanged && !( setupFlags & Okular::DocumentObserver::NewLayoutForPages ) ) { int count = pageSet.count(); for ( int i = 0; (i < count) && !documentChanged; i++ ) + { if ( (int)pageSet[i]->number() != d->items[i]->pageNumber() ) + { documentChanged = true; + } + else + { + // even if the document has not changed, allowfillforms may have + // changed, so update all fields' "canBeFilled" flag + foreach ( FormWidgetIface * w, d->items[i]->formWidgets() ) + w->setCanBeFilled( allowfillforms ); + } + } if ( !documentChanged ) return; } @@ -923,7 +936,7 @@ void PageView::notifySetup( const QVector< Okular::Page * > & pageSet, int setup w->setPageItem( item ); w->setFormWidgetsController( d->formWidgetsController() ); w->setVisibility( false ); - w->setCanBeFilled( d->document->isAllowed( Okular::AllowFillForms ) ); + w->setCanBeFilled( allowfillforms ); item->formWidgets().insert( ff->id(), w ); hasformwidgets = true; } From b6d92a8e22377843e16ec6393c60f2aeb9d3866e Mon Sep 17 00:00:00 2001 From: Fabio D'Urso Date: Wed, 10 Sep 2014 12:16:05 +0200 Subject: [PATCH 27/78] pageview: React to Document::isAllowed(AllowNotes) changes in notifySetup --- ui/pageview.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ui/pageview.cpp b/ui/pageview.cpp index ba5d2d3a8..f0b62a614 100644 --- a/ui/pageview.cpp +++ b/ui/pageview.cpp @@ -879,8 +879,13 @@ void PageView::selectAll() void PageView::notifySetup( const QVector< Okular::Page * > & pageSet, int setupFlags ) { bool documentChanged = setupFlags & Okular::DocumentObserver::DocumentChanged; + const bool allownotes = d->document->isAllowed( Okular::AllowNotes ); const bool allowfillforms = d->document->isAllowed( Okular::AllowFillForms ); + // allownotes may have changed + if ( d->aToggleAnnotator ) + d->aToggleAnnotator->setEnabled( allownotes ); + // reuse current pages if nothing new if ( ( pageSet.count() == d->items.count() ) && !documentChanged && !( setupFlags & Okular::DocumentObserver::NewLayoutForPages ) ) { From ba9491bccdf9d9bba118185af8a0d976313d0bcf Mon Sep 17 00:00:00 2001 From: Fabio D'Urso Date: Thu, 11 Sep 2014 14:31:46 +0200 Subject: [PATCH 28/78] Save dialog: use .okular as default format if there's data that can't be saved natively --- part.cpp | 97 ++++++++++++++++++++++++++++++++------------------------ part.h | 1 + 2 files changed, 57 insertions(+), 41 deletions(-) diff --git a/part.cpp b/part.cpp index 896e12ff2..7fb84b31f 100644 --- a/part.cpp +++ b/part.cpp @@ -2242,8 +2242,13 @@ bool Part::slotSaveFileAs( bool showOkularArchiveAsDefaultFormat ) if ( !typeName.isEmpty() ) originalMimeType = KMimeType::mimeType( typeName ); + // What data would we lose if we saved natively? + bool wontSaveForms, wontSaveAnnotations; + checkNativeSaveDataLoss(&wontSaveForms, &wontSaveAnnotations); + // What format choice should we show as default? - const QString defaultMimeType = (isDocumentArchive || showOkularArchiveAsDefaultFormat) ? + const QString defaultMimeType = (isDocumentArchive || showOkularArchiveAsDefaultFormat || + wontSaveForms || wontSaveAnnotations) ? "application/vnd.kde.okular-archive" : originalMimeType->name(); // Prepare "Save As" dialog @@ -2308,46 +2313,8 @@ bool Part::saveAs( const KUrl & saveUrl, SaveAsFlags flags ) } else { - // If the user wants to save in the original file's format, some features - // might not be available. Find out what can't be saved in this format - bool wontSaveAnnotations = false; - bool wontSaveForms = false; - - if ( !m_document->canSaveChanges( Document::SaveFormsCapability ) ) - { - /* Set wontSaveForms only if there are forms */ - const int pagecount = m_document->pages(); - - for ( int pageno = 0; pageno < pagecount; ++pageno ) - { - const Okular::Page *page = m_document->page( pageno ); - if ( !page->formFields().empty() ) - { - wontSaveForms = true; - break; - } - } - } - if ( !m_document->canSaveChanges( Document::SaveAnnotationsCapability ) ) - { - /* Set wontSaveAnnotations only if there are local annotations */ - const int pagecount = m_document->pages(); - - for ( int pageno = 0; pageno < pagecount; ++pageno ) - { - const Okular::Page *page = m_document->page( pageno ); - foreach ( const Okular::Annotation *ann, page->annotations() ) - { - if ( !(ann->flags() & Okular::Annotation::External) ) - { - wontSaveAnnotations = true; - break; - } - } - if ( wontSaveAnnotations ) - break; - } - } + bool wontSaveForms, wontSaveAnnotations; + checkNativeSaveDataLoss(&wontSaveForms, &wontSaveAnnotations); // If something can't be saved in this format, ask for confirmation QStringList listOfwontSaves; @@ -2506,6 +2473,54 @@ bool Part::saveAs( const KUrl & saveUrl, SaveAsFlags flags ) return true; } +// If the user wants to save in the original file's format, some features might +// not be available. Find out what cannot be saved in this format +void Part::checkNativeSaveDataLoss(bool *out_wontSaveForms, bool *out_wontSaveAnnotations) const +{ + bool wontSaveForms = false; + bool wontSaveAnnotations = false; + + if ( !m_document->canSaveChanges( Document::SaveFormsCapability ) ) + { + /* Set wontSaveForms only if there are forms */ + const int pagecount = m_document->pages(); + + for ( int pageno = 0; pageno < pagecount; ++pageno ) + { + const Okular::Page *page = m_document->page( pageno ); + if ( !page->formFields().empty() ) + { + wontSaveForms = true; + break; + } + } + } + + if ( !m_document->canSaveChanges( Document::SaveAnnotationsCapability ) ) + { + /* Set wontSaveAnnotations only if there are local annotations */ + const int pagecount = m_document->pages(); + + for ( int pageno = 0; pageno < pagecount; ++pageno ) + { + const Okular::Page *page = m_document->page( pageno ); + foreach ( const Okular::Annotation *ann, page->annotations() ) + { + if ( !(ann->flags() & Okular::Annotation::External) ) + { + wontSaveAnnotations = true; + break; + } + } + if ( wontSaveAnnotations ) + break; + } + } + + *out_wontSaveForms = wontSaveForms; + *out_wontSaveAnnotations = wontSaveAnnotations; +} + void Part::slotGetNewStuff() { #if 0 diff --git a/part.h b/part.h index e2d5a4162..bc9ae32ea 100644 --- a/part.h +++ b/part.h @@ -249,6 +249,7 @@ class OKULAR_PART_EXPORT Part : public KParts::ReadWritePart, public Okular::Doc void unsetDummyMode(); void slotRenameBookmark( const DocumentViewport &viewport ); void resetStartArguments(); + void checkNativeSaveDataLoss(bool *out_wontSaveForms, bool *out_wontSaveAnnotations) const; enum SaveAsFlag { From 8a1acdcd6a0ce93c062f549c5f02dc38694cc9f2 Mon Sep 17 00:00:00 2001 From: Fabio D'Urso Date: Wed, 10 Sep 2014 12:01:58 +0200 Subject: [PATCH 29/78] Non-blocking docdata/ migration message --- part.cpp | 16 ++++++++++++++++ part.h | 1 + 2 files changed, 17 insertions(+) diff --git a/part.cpp b/part.cpp index 7fb84b31f..06cdcd4a9 100644 --- a/part.cpp +++ b/part.cpp @@ -426,6 +426,12 @@ m_cliPresentation(false), m_cliPrint(false), m_embedMode(detectEmbedMode(parentW 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 and continue editing the document." ) ); + rightLayout->addWidget( m_migrationMessage ); m_topMessage = new KMessageWidget( rightContainer ); m_topMessage->setVisible( false ); m_topMessage->setWordWrap( true ); @@ -757,6 +763,7 @@ void Part::setupActions() m_save->setEnabled( false ); m_saveAs = KStandardAction::saveAs( this, SLOT(slotSaveFileAs()), ac ); m_saveAs->setEnabled( false ); + m_migrationMessage->addAction( m_saveAs ); m_showLeftPanel = ac->add("show_leftpanel"); m_showLeftPanel->setText(i18n( "Show &Navigation Panel")); @@ -1091,6 +1098,11 @@ KConfigDialog * Part::slotGeneratorPreferences( ) 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; @@ -1424,6 +1436,7 @@ bool Part::openFile() 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( "HasUnsupportedXfaForm" ).toBool() == true ) @@ -1664,6 +1677,7 @@ bool Part::closeUrl(bool promptToSave) if ( widget() ) { m_searchWidget->clearText(); + m_migrationMessage->setVisible( false ); m_topMessage->setVisible( false ); m_formsMessage->setVisible( false ); } @@ -2437,6 +2451,8 @@ bool Part::saveAs( const KUrl & saveUrl, SaveAsFlags flags ) setModified( false ); setWindowTitleFromDocument(); + if ( m_document->isDocdataMigrationNeeded() ) + m_document->docdataMigrationDone(); bool reloadedCorrectly = true; diff --git a/part.h b/part.h index bc9ae32ea..c8d197f73 100644 --- a/part.h +++ b/part.h @@ -278,6 +278,7 @@ class OKULAR_PART_EXPORT Part : public KParts::ReadWritePart, public Okular::Doc Sidebar *m_sidebar; SearchWidget *m_searchWidget; FindBar * m_findBar; + KMessageWidget * m_migrationMessage; KMessageWidget * m_topMessage; KMessageWidget * m_formsMessage; KMessageWidget * m_infoMessage; From 729a622a497aedcff6bda8aa11e34f76d47f48e0 Mon Sep 17 00:00:00 2001 From: Albert Astals Cid Date: Mon, 11 Sep 2017 19:52:04 +0200 Subject: [PATCH 30/78] This test file was lost at some point Or was never added --- autotests/data/file1-docdata.xml | 427 +++++++++++++++++++++++++++++++ 1 file changed, 427 insertions(+) create mode 100644 autotests/data/file1-docdata.xml diff --git a/autotests/data/file1-docdata.xml b/autotests/data/file1-docdata.xml new file mode 100644 index 000000000..0904d748b --- /dev/null +++ b/autotests/data/file1-docdata.xml @@ -0,0 +1,427 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 5413d38d22e413d4b84eac51ada93e00d903e885 Mon Sep 17 00:00:00 2001 From: Albert Astals Cid Date: Mon, 23 Oct 2017 16:33:20 +0200 Subject: [PATCH 31/78] Fix Cltr+S being used both for save and saveas --- part.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/part.cpp b/part.cpp index 9f70f87ba..9fa642b9e 100644 --- a/part.cpp +++ b/part.cpp @@ -818,7 +818,6 @@ void Part::setupActions() 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 ); From 3435eb6df89db93932a9fa28a4c109f78b6275aa Mon Sep 17 00:00:00 2001 From: Albert Astals Cid Date: Wed, 25 Oct 2017 14:51:58 +0200 Subject: [PATCH 32/78] Convert m_formWidgets from hash to set We don't use the key for anything --- ui/pageview.cpp | 2 +- ui/pageviewutils.cpp | 10 ++++------ ui/pageviewutils.h | 4 ++-- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/ui/pageview.cpp b/ui/pageview.cpp index fb25169f9..0593380cc 100644 --- a/ui/pageview.cpp +++ b/ui/pageview.cpp @@ -1016,7 +1016,7 @@ void PageView::notifySetup( const QVector< Okular::Page * > & pageSet, int setup w->setFormWidgetsController( d->formWidgetsController() ); w->setVisibility( false ); w->setCanBeFilled( allowfillforms ); - item->formWidgets().insert( ff->id(), w ); + item->formWidgets().insert( w ); hasformwidgets = true; } } diff --git a/ui/pageviewutils.cpp b/ui/pageviewutils.cpp index a46384afb..88123d3a8 100644 --- a/ui/pageviewutils.cpp +++ b/ui/pageviewutils.cpp @@ -51,9 +51,7 @@ PageViewItem::PageViewItem( const Okular::Page * page ) PageViewItem::~PageViewItem() { - QHash::iterator it = m_formWidgets.begin(), itEnd = m_formWidgets.end(); - for ( ; it != itEnd; ++it ) - delete *it; + qDeleteAll( m_formWidgets ); qDeleteAll( m_videoWidgets ); } @@ -122,7 +120,7 @@ bool PageViewItem::isVisible() const return m_visible; } -QHash& PageViewItem::formWidgets() +QSet& PageViewItem::formWidgets() { return m_formWidgets; } @@ -163,7 +161,7 @@ void PageViewItem::moveTo( int x, int y ) m_croppedGeometry.moveTop( y ); m_uncroppedGeometry.moveLeft( qRound( x - m_crop.left * m_uncroppedGeometry.width() ) ); m_uncroppedGeometry.moveTop( qRound( y - m_crop.top * m_uncroppedGeometry.height() ) ); - QHash::iterator it = m_formWidgets.begin(), itEnd = m_formWidgets.end(); + QSet::iterator it = m_formWidgets.begin(), itEnd = m_formWidgets.end(); for ( ; it != itEnd; ++it ) { Okular::NormalizedRect r = (*it)->rect(); @@ -200,7 +198,7 @@ bool PageViewItem::setFormWidgetsVisible( bool visible ) return false; bool somehadfocus = false; - QHash::iterator it = m_formWidgets.begin(), itEnd = m_formWidgets.end(); + QSet::iterator it = m_formWidgets.begin(), itEnd = m_formWidgets.end(); for ( ; it != itEnd; ++it ) { bool hadfocus = (*it)->setVisibility( visible && (*it)->formField()->isVisible() ); diff --git a/ui/pageviewutils.h b/ui/pageviewutils.h index ca8bd78a4..a5cd89a2d 100644 --- a/ui/pageviewutils.h +++ b/ui/pageviewutils.h @@ -47,7 +47,7 @@ class PageViewItem int pageNumber() const; double zoomFactor() const; bool isVisible() const; - QHash& formWidgets(); + QSet& formWidgets(); QHash< Okular::Movie *, VideoWidget * >& videoWidgets(); /* The page is cropped as follows: */ @@ -87,7 +87,7 @@ class PageViewItem QRect m_croppedGeometry; QRect m_uncroppedGeometry; Okular::NormalizedRect m_crop; - QHash m_formWidgets; + QSet m_formWidgets; QHash< Okular::Movie *, VideoWidget * > m_videoWidgets; }; From ba026566469453cc1758a4ec668dc569f10ab6f0 Mon Sep 17 00:00:00 2001 From: Albert Astals Cid Date: Wed, 25 Oct 2017 14:59:17 +0200 Subject: [PATCH 33/78] Make m_annowindows a set instead of a hash Will make it for easier maintainance when we need to reuse the windows and annotation pointers change --- ui/annotwindow.cpp | 5 +++++ ui/annotwindow.h | 2 ++ ui/pageview.cpp | 35 +++++++++++++---------------------- 3 files changed, 20 insertions(+), 22 deletions(-) diff --git a/ui/annotwindow.cpp b/ui/annotwindow.cpp index 4cca403b8..011f70cf6 100644 --- a/ui/annotwindow.cpp +++ b/ui/annotwindow.cpp @@ -241,6 +241,11 @@ AnnotWindow::~AnnotWindow() delete m_latexRenderer; } +Okular::Annotation * AnnotWindow::annotation() const +{ + return m_annot; +} + void AnnotWindow::reloadInfo() { const QColor newcolor = m_annot->style().color().isValid() ? m_annot->style().color() : Qt::yellow; diff --git a/ui/annotwindow.h b/ui/annotwindow.h index d34c3ea20..5c403f1fc 100644 --- a/ui/annotwindow.h +++ b/ui/annotwindow.h @@ -36,6 +36,8 @@ class AnnotWindow : public QFrame void reloadInfo(); + Okular::Annotation * annotation() const; + private: MovableTitle * m_title; KTextEdit *textEdit; diff --git a/ui/pageview.cpp b/ui/pageview.cpp index 0593380cc..8b98c36b2 100644 --- a/ui/pageview.cpp +++ b/ui/pageview.cpp @@ -172,7 +172,7 @@ public: // annotations PageViewAnnotator * annotator; //text annotation dialogs list - QHash< Okular::Annotation *, AnnotWindow * > m_annowindows; + QSet< AnnotWindow * > m_annowindows; // other stuff QTimer * delayResizeEventTimer; bool dirtyLayout; @@ -451,7 +451,7 @@ PageView::~PageView() // We need to assign it to a different list otherwise slotAnnotationWindowDestroyed // will bite us and clear d->m_annowindows - QHash< Okular::Annotation *, AnnotWindow * > annowindows = d->m_annowindows; + QSet< AnnotWindow * > annowindows = d->m_annowindows; d->m_annowindows.clear(); qDeleteAll( annowindows ); @@ -756,10 +756,13 @@ void PageView::openAnnotationWindow( Okular::Annotation * annotation, int pageNu // find the annot window AnnotWindow* existWindow = nullptr; - QHash< Okular::Annotation *, AnnotWindow * >::ConstIterator it = d->m_annowindows.constFind( annotation ); - if ( it != d->m_annowindows.constEnd() ) + foreach(AnnotWindow *aw, d->m_annowindows) { - existWindow = *it; + if ( aw->annotation() == annotation ) + { + existWindow = aw; + break; + } } if ( existWindow == nullptr ) @@ -767,7 +770,7 @@ void PageView::openAnnotationWindow( Okular::Annotation * annotation, int pageNu existWindow = new AnnotWindow( this, annotation, d->document, pageNumber ); connect(existWindow, &QObject::destroyed, this, &PageView::slotAnnotationWindowDestroyed); - d->m_annowindows.insert( annotation, existWindow ); + d->m_annowindows << existWindow; } existWindow->show(); @@ -775,19 +778,7 @@ void PageView::openAnnotationWindow( Okular::Annotation * annotation, int pageNu void PageView::slotAnnotationWindowDestroyed( QObject * window ) { - QHash< Okular::Annotation*, AnnotWindow * >::Iterator it = d->m_annowindows.begin(); - QHash< Okular::Annotation*, AnnotWindow * >::Iterator itEnd = d->m_annowindows.end(); - while ( it != itEnd ) - { - if ( it.value() == window ) - { - it = d->m_annowindows.erase( it ); - } - else - { - ++it; - } - } + d->m_annowindows.remove( static_cast( window ) ); } void PageView::displayMessage( const QString & message, const QString & details, PageViewMessage::Icon icon, int duration ) @@ -1087,7 +1078,7 @@ void PageView::notifySetup( const QVector< Okular::Page * > & pageSet, int setup // We need to assign it to a different list otherwise slotAnnotationWindowDestroyed // will bite us and clear d->m_annowindows - QHash< Okular::Annotation *, AnnotWindow * > annowindows = d->m_annowindows; + QSet< AnnotWindow * > annowindows = d->m_annowindows; d->m_annowindows.clear(); qDeleteAll( annowindows ); @@ -1330,10 +1321,10 @@ void PageView::notifyPageChanged( int pageNumber, int changedFlags ) { const QLinkedList< Okular::Annotation * > annots = d->document->page( pageNumber )->annotations(); const QLinkedList< Okular::Annotation * >::ConstIterator annItEnd = annots.end(); - QHash< Okular::Annotation*, AnnotWindow * >::Iterator it = d->m_annowindows.begin(); + QSet< AnnotWindow * >::Iterator it = d->m_annowindows.begin(); for ( ; it != d->m_annowindows.end(); ) { - QLinkedList< Okular::Annotation * >::ConstIterator annIt = qFind( annots, it.key() ); + QLinkedList< Okular::Annotation * >::ConstIterator annIt = qFind( annots, (*it)->annotation() ); if ( annIt != annItEnd ) { (*it)->reloadInfo(); From ead094862f00a884f5f2a536e55a5324d2ff505e Mon Sep 17 00:00:00 2001 From: Albert Astals Cid Date: Wed, 25 Oct 2017 15:03:19 +0200 Subject: [PATCH 34/78] Fix using && for flags --- part.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/part.cpp b/part.cpp index 9fa642b9e..4ec5b0cdf 100644 --- a/part.cpp +++ b/part.cpp @@ -2469,7 +2469,7 @@ bool Part::saveAs( const QUrl & saveUrl, SaveAsFlags flags ) QStringList listOfwontSaves; if ( wontSaveForms ) listOfwontSaves << i18n( "Filled form contents" ); if ( wontSaveAnnotations ) listOfwontSaves << i18n( "User annotations" ); - if ( !listOfwontSaves.isEmpty() && !( flags && SaveAsDontShowWarning ) ) + if ( !listOfwontSaves.isEmpty() && !( flags & SaveAsDontShowWarning ) ) { int result = KMessageBox::warningYesNoCancelList( widget(), i18n( "The following elements cannot be saved in this format and will be lost.
If you want to preserve them, please use the Okular document archive format." ), From 1095ffd85db8b059538b7babdc72265320060711 Mon Sep 17 00:00:00 2001 From: Albert Astals Cid Date: Wed, 25 Oct 2017 15:05:31 +0200 Subject: [PATCH 35/78] We always need to swap/reload even if the url is the same Since our generators are file oriented, saving means a new file was saved so we need to reopen the new one for things to work properly --- part.cpp | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/part.cpp b/part.cpp index 4ec5b0cdf..12fffe760 100644 --- a/part.cpp +++ b/part.cpp @@ -2592,23 +2592,20 @@ bool Part::saveAs( const QUrl & saveUrl, SaveAsFlags flags ) bool reloadedCorrectly = true; - // Load new file instead of the old one - if ( url() != saveUrl ) + // Make the generator use the new new file instead of the old one + if ( m_document->canSwapBackingFile() ) { - if ( m_document->canSwapBackingFile() ) - { - // this calls openFile internally, which in turn actually calls - // m_document->swapBackingFile() instead of the regular loadDocument - if ( !openUrl( saveUrl, true /* swapInsteadOfOpening */ ) ) - 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; - } + // this calls openFile internally, which in turn actually calls + // m_document->swapBackingFile() instead of the regular loadDocument + if ( !openUrl( saveUrl, true /* swapInsteadOfOpening */ ) ) + 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 From 5c9edea430b73c11855d31b1b6863b83ae155580 Mon Sep 17 00:00:00 2001 From: Albert Astals Cid Date: Wed, 25 Oct 2017 15:12:35 +0200 Subject: [PATCH 36/78] Don't store the form twice in the formwidget classes Just use m_ff and cast it when needed. This will make it easy in the future when the given form for a widget may change so we only need to update one value --- ui/formwidgets.cpp | 144 +++++++++++++++++++++++---------------------- ui/formwidgets.h | 18 +----- 2 files changed, 75 insertions(+), 87 deletions(-) diff --git a/ui/formwidgets.cpp b/ui/formwidgets.cpp index 104671887..3612c1123 100644 --- a/ui/formwidgets.cpp +++ b/ui/formwidgets.cpp @@ -260,7 +260,7 @@ FormWidgetIface * FormWidgetFactory::createWidget( Okular::FormField * ff, QWidg FormWidgetIface::FormWidgetIface( QWidget * w, Okular::FormField * ff, bool canBeEnabled ) - : m_controller( nullptr ), m_widget( w ), m_ff( ff ), m_pageItem( nullptr ), + : m_controller( nullptr ), m_ff( ff ), m_widget( w ), m_pageItem( nullptr ), m_canBeEnabled( canBeEnabled ) { m_widget->setEnabled( m_canBeEnabled ); @@ -326,11 +326,10 @@ QAbstractButton* FormWidgetIface::button() PushButtonEdit::PushButtonEdit( Okular::FormFieldButton * button, QWidget * parent ) - : QPushButton( parent ), FormWidgetIface( this, button, !button->isReadOnly() ), - m_form( button ) + : QPushButton( parent ), FormWidgetIface( this, button, !button->isReadOnly() ) { - setText( m_form->caption() ); - setVisible( m_form->isVisible() ); + setText( button->caption() ); + setVisible( button->isVisible() ); setCursor( Qt::ArrowCursor ); connect( this, &QAbstractButton::clicked, this, &PushButtonEdit::slotClicked ); @@ -338,26 +337,26 @@ PushButtonEdit::PushButtonEdit( Okular::FormFieldButton * button, QWidget * pare void PushButtonEdit::slotClicked() { - if ( m_form->activationAction() ) - m_controller->signalAction( m_form->activationAction() ); + if ( m_ff->activationAction() ) + m_controller->signalAction( m_ff->activationAction() ); } CheckBoxEdit::CheckBoxEdit( Okular::FormFieldButton * button, QWidget * parent ) - : QCheckBox( parent ), FormWidgetIface( this, button, !button->isReadOnly() ), - m_form( button ) + : QCheckBox( parent ), FormWidgetIface( this, button, !button->isReadOnly() ) { - setText( m_form->caption() ); + setText( button->caption() ); - setVisible( m_form->isVisible() ); + setVisible( button->isVisible() ); setCursor( Qt::ArrowCursor ); } void CheckBoxEdit::setFormWidgetsController( FormWidgetsController *controller ) { + Okular::FormFieldButton *form = static_cast(m_ff); FormWidgetIface::setFormWidgetsController( controller ); - m_controller->registerRadioButton( button(), m_form ); - setChecked( m_form->state() ); + m_controller->registerRadioButton( button(), form ); + setChecked( form->state() ); connect( this, &QCheckBox::stateChanged, this, &CheckBoxEdit::slotStateChanged ); } @@ -368,26 +367,27 @@ QAbstractButton* CheckBoxEdit::button() void CheckBoxEdit::slotStateChanged( int state ) { - if ( state == Qt::Checked && m_form->activationAction() ) - m_controller->signalAction( m_form->activationAction() ); + Okular::FormFieldButton *form = static_cast(m_ff); + if ( state == Qt::Checked && form->activationAction() ) + m_controller->signalAction( form->activationAction() ); } RadioButtonEdit::RadioButtonEdit( Okular::FormFieldButton * button, QWidget * parent ) - : QRadioButton( parent ), FormWidgetIface( this, button, !button->isReadOnly() ), - m_form( button ) + : QRadioButton( parent ), FormWidgetIface( this, button, !button->isReadOnly() ) { - setText( m_form->caption() ); + setText( button->caption() ); - setVisible( m_form->isVisible() ); + setVisible( button->isVisible() ); setCursor( Qt::ArrowCursor ); } void RadioButtonEdit::setFormWidgetsController( FormWidgetsController *controller ) { + Okular::FormFieldButton *form = static_cast(m_ff); FormWidgetIface::setFormWidgetsController( controller ); - m_controller->registerRadioButton( button(), m_form ); - setChecked( m_form->state() ); + m_controller->registerRadioButton( button(), form ); + setChecked( form->state() ); } QAbstractButton* RadioButtonEdit::button() @@ -396,14 +396,14 @@ QAbstractButton* RadioButtonEdit::button() } FormLineEdit::FormLineEdit( Okular::FormFieldText * text, QWidget * parent ) - : QLineEdit( parent ), FormWidgetIface( this, text, true ), m_form( text ) + : QLineEdit( parent ), FormWidgetIface( this, text, true ) { - int maxlen = m_form->maximumLength(); + int maxlen = text->maximumLength(); if ( maxlen >= 0 ) setMaxLength( maxlen ); - setAlignment( m_form->textAlignment() ); - setText( m_form->text() ); - if ( m_form->isPassword() ) + setAlignment( text->textAlignment() ); + setText( text->text() ); + if ( text->isPassword() ) setEchoMode( QLineEdit::Password ); m_prevCursorPos = cursorPosition(); @@ -412,7 +412,7 @@ FormLineEdit::FormLineEdit( Okular::FormFieldText * text, QWidget * parent ) connect( this, &QLineEdit::textEdited, this, &FormLineEdit::slotChanged ); connect( this, &QLineEdit::cursorPositionChanged, this, &FormLineEdit::slotChanged ); - setVisible( m_form->isVisible() ); + setVisible( text->isVisible() ); } void FormLineEdit::setFormWidgetsController(FormWidgetsController* controller) @@ -471,12 +471,13 @@ void FormLineEdit::contextMenuEvent( QContextMenuEvent* event ) void FormLineEdit::slotChanged() { + Okular::FormFieldText *form = static_cast(m_ff); QString contents = text(); int cursorPos = cursorPosition(); - if ( contents != m_form->text() ) + if ( contents != form->text() ) { m_controller->formTextChangedByWidget( pageItem()->pageNumber(), - m_form, + form, contents, cursorPos, m_prevCursorPos, @@ -501,7 +502,7 @@ void FormLineEdit::slotHandleTextChangedByUndoRedo( int pageNumber, int anchorPos ) { Q_UNUSED(pageNumber); - if ( textForm != m_form || contents == text() ) + if ( textForm != m_ff || contents == text() ) { return; } @@ -516,12 +517,12 @@ void FormLineEdit::slotHandleTextChangedByUndoRedo( int pageNumber, } TextAreaEdit::TextAreaEdit( Okular::FormFieldText * text, QWidget * parent ) -: KTextEdit( parent ), FormWidgetIface( this, text, true ), m_form( text ) +: KTextEdit( parent ), FormWidgetIface( this, text, true ) { - setAcceptRichText( m_form->isRichText() ); - setCheckSpellingEnabled( m_form->canBeSpellChecked() ); - setAlignment( m_form->textAlignment() ); - setPlainText( m_form->text() ); + setAcceptRichText( text->isRichText() ); + setCheckSpellingEnabled( text->canBeSpellChecked() ); + setAlignment( text->textAlignment() ); + setPlainText( text->text() ); setUndoRedoEnabled( false ); connect( this, &QTextEdit::textChanged, this, &TextAreaEdit::slotChanged ); @@ -530,7 +531,7 @@ TextAreaEdit::TextAreaEdit( Okular::FormFieldText * text, QWidget * parent ) this, &TextAreaEdit::slotUpdateUndoAndRedoInContextMenu ); m_prevCursorPos = textCursor().position(); m_prevAnchorPos = textCursor().anchor(); - setVisible( m_form->isVisible() ); + setVisible( text->isVisible() ); } bool TextAreaEdit::event( QEvent* e ) @@ -591,7 +592,7 @@ void TextAreaEdit::slotHandleTextChangedByUndoRedo( int pageNumber, int anchorPos ) { Q_UNUSED(pageNumber); - if ( textForm != m_form ) + if ( textForm != m_ff ) { return; } @@ -607,12 +608,13 @@ void TextAreaEdit::slotHandleTextChangedByUndoRedo( int pageNumber, void TextAreaEdit::slotChanged() { + Okular::FormFieldText *form = static_cast(m_ff); QString contents = toPlainText(); int cursorPos = textCursor().position(); - if (contents != m_form->text()) + if (contents != form->text()) { m_controller->formTextChangedByWidget( pageItem()->pageNumber(), - m_form, + form, contents, cursorPos, m_prevCursorPos, @@ -624,20 +626,19 @@ void TextAreaEdit::slotChanged() FileEdit::FileEdit( Okular::FormFieldText * text, QWidget * parent ) - : KUrlRequester( parent ), FormWidgetIface( this, text, !text->isReadOnly() ), - m_form( text ) + : KUrlRequester( parent ), FormWidgetIface( this, text, !text->isReadOnly() ) { setMode( KFile::File | KFile::ExistingOnly | KFile::LocalOnly ); setFilter( i18n( "*|All Files" ) ); - setUrl( QUrl::fromUserInput( m_form->text() ) ); - lineEdit()->setAlignment( m_form->textAlignment() ); + setUrl( QUrl::fromUserInput( text->text() ) ); + lineEdit()->setAlignment( text->textAlignment() ); m_prevCursorPos = lineEdit()->cursorPosition(); m_prevAnchorPos = lineEdit()->cursorPosition(); connect( this, &KUrlRequester::textChanged, this, &FileEdit::slotChanged ); connect( lineEdit(), &QLineEdit::cursorPositionChanged, this, &FileEdit::slotChanged ); - setVisible( m_form->isVisible() ); + setVisible( text->isVisible() ); } void FileEdit::setFormWidgetsController( FormWidgetsController* controller ) @@ -704,12 +705,14 @@ void FileEdit::slotChanged() if ( text() != url().toLocalFile() ) this->setText( url().toLocalFile() ); + Okular::FormFieldText *form = static_cast(m_ff); + QString contents = text(); int cursorPos = lineEdit()->cursorPosition(); - if (contents != m_form->text()) + if (contents != form->text()) { m_controller->formTextChangedByWidget( pageItem()->pageNumber(), - m_form, + form, contents, cursorPos, m_prevCursorPos, @@ -734,7 +737,7 @@ void FileEdit::slotHandleFileChangedByUndoRedo( int pageNumber, int anchorPos ) { Q_UNUSED(pageNumber); - if ( form != m_form || contents == text() ) + if ( form != m_ff || contents == text() ) { return; } @@ -749,14 +752,13 @@ void FileEdit::slotHandleFileChangedByUndoRedo( int pageNumber, } ListEdit::ListEdit( Okular::FormFieldChoice * choice, QWidget * parent ) - : QListWidget( parent ), FormWidgetIface( this, choice, !choice->isReadOnly() ), - m_form( choice ) + : QListWidget( parent ), FormWidgetIface( this, choice, !choice->isReadOnly() ) { - addItems( m_form->choices() ); - setSelectionMode( m_form->multiSelect() ? QAbstractItemView::ExtendedSelection : QAbstractItemView::SingleSelection ); + addItems( choice->choices() ); + setSelectionMode( choice->multiSelect() ? QAbstractItemView::ExtendedSelection : QAbstractItemView::SingleSelection ); setVerticalScrollMode( QAbstractItemView::ScrollPerPixel ); - QList< int > selectedItems = m_form->currentChoices(); - if ( m_form->multiSelect() ) + QList< int > selectedItems = choice->currentChoices(); + if ( choice->multiSelect() ) { foreach ( int index, selectedItems ) if ( index >= 0 && index < count() ) @@ -773,7 +775,7 @@ ListEdit::ListEdit( Okular::FormFieldChoice * choice, QWidget * parent ) connect( this, &QListWidget::itemSelectionChanged, this, &ListEdit::slotSelectionChanged ); - setVisible( m_form->isVisible() ); + setVisible( choice->isVisible() ); setCursor( Qt::ArrowCursor ); } @@ -791,9 +793,10 @@ void ListEdit::slotSelectionChanged() foreach( const QListWidgetItem * item, selection ) rows.append( row( item ) ); - if ( rows != m_form->currentChoices() ) { + Okular::FormFieldChoice *form = static_cast(m_ff); + if ( rows != form->currentChoices() ) { m_controller->formListChangedByWidget( pageItem()->pageNumber(), - m_form, + form, rows ); } } @@ -804,7 +807,7 @@ void ListEdit::slotHandleFormListChangedByUndoRedo( int pageNumber, { Q_UNUSED(pageNumber); - if ( m_form != listForm ) { + if ( m_ff != listForm ) { return; } @@ -819,25 +822,24 @@ void ListEdit::slotHandleFormListChangedByUndoRedo( int pageNumber, } ComboEdit::ComboEdit( Okular::FormFieldChoice * choice, QWidget * parent ) - : QComboBox( parent ), FormWidgetIface( this, choice, !choice->isReadOnly() ), - m_form( choice ) + : QComboBox( parent ), FormWidgetIface( this, choice, !choice->isReadOnly() ) { - addItems( m_form->choices() ); + addItems( choice->choices() ); setEditable( true ); setInsertPolicy( NoInsert ); - lineEdit()->setReadOnly( !m_form->isEditable() ); - QList< int > selectedItems = m_form->currentChoices(); + lineEdit()->setReadOnly( !choice->isEditable() ); + QList< int > selectedItems = choice->currentChoices(); if ( selectedItems.count() == 1 && selectedItems.at(0) >= 0 && selectedItems.at(0) < count() ) setCurrentIndex( selectedItems.at(0) ); - if ( m_form->isEditable() && !m_form->editChoice().isEmpty() ) - lineEdit()->setText( m_form->editChoice() ); + if ( choice->isEditable() && !choice->editChoice().isEmpty() ) + lineEdit()->setText( choice->editChoice() ); connect( this, SIGNAL(currentIndexChanged(int)), this, SLOT(slotValueChanged()) ); connect( this, &QComboBox::editTextChanged, this, &ComboEdit::slotValueChanged ); connect( lineEdit(), &QLineEdit::cursorPositionChanged, this, &ComboEdit::slotValueChanged ); - setVisible( m_form->isVisible() ); + setVisible( choice->isVisible() ); setCursor( Qt::ArrowCursor ); m_prevCursorPos = lineEdit()->cursorPosition(); m_prevAnchorPos = lineEdit()->cursorPosition(); @@ -855,21 +857,23 @@ void ComboEdit::slotValueChanged() { const QString text = lineEdit()->text(); + Okular::FormFieldChoice *form = static_cast(m_ff); + QString prevText; - if ( m_form->currentChoices().isEmpty() ) + if ( form->currentChoices().isEmpty() ) { - prevText = m_form->editChoice(); + prevText = form->editChoice(); } else { - prevText = m_form->choices()[m_form->currentChoices()[0]]; + prevText = form->choices()[form->currentChoices()[0]]; } int cursorPos = lineEdit()->cursorPosition(); if ( text != prevText ) { m_controller->formComboChangedByWidget( pageItem()->pageNumber(), - m_form, + form, currentText(), cursorPos, m_prevCursorPos, @@ -896,7 +900,7 @@ void ComboEdit::slotHandleFormComboChangedByUndoRedo( int pageNumber, { Q_UNUSED(pageNumber); - if ( m_form != form ) { + if ( m_ff != form ) { return; } diff --git a/ui/formwidgets.h b/ui/formwidgets.h index 1e9f8a3f8..ddfbc6bff 100644 --- a/ui/formwidgets.h +++ b/ui/formwidgets.h @@ -157,10 +157,10 @@ class FormWidgetIface protected: FormWidgetsController * m_controller; + Okular::FormField * m_ff; private: QWidget * m_widget; - Okular::FormField * m_ff; PageViewItem * m_pageItem; bool m_canBeEnabled; }; @@ -175,9 +175,6 @@ class PushButtonEdit : public QPushButton, public FormWidgetIface private Q_SLOTS: void slotClicked(); - - private: - Okular::FormFieldButton * m_form; }; class CheckBoxEdit : public QCheckBox, public FormWidgetIface @@ -193,9 +190,6 @@ class CheckBoxEdit : public QCheckBox, public FormWidgetIface private Q_SLOTS: void slotStateChanged( int state ); - - private: - Okular::FormFieldButton * m_form; }; class RadioButtonEdit : public QRadioButton, public FormWidgetIface @@ -208,9 +202,6 @@ class RadioButtonEdit : public QRadioButton, public FormWidgetIface // reimplemented from FormWidgetIface void setFormWidgetsController( FormWidgetsController *controller ) override; QAbstractButton* button() override; - - private: - Okular::FormFieldButton * m_form; }; class FormLineEdit : public QLineEdit, public FormWidgetIface @@ -234,7 +225,6 @@ class FormLineEdit : public QLineEdit, public FormWidgetIface void slotChanged(); private: - Okular::FormFieldText * m_form; int m_prevCursorPos; int m_prevAnchorPos; }; @@ -261,7 +251,6 @@ class TextAreaEdit : public KTextEdit, public FormWidgetIface void slotChanged(); private: - Okular::FormFieldText * m_form; int m_prevCursorPos; int m_prevAnchorPos; }; @@ -287,7 +276,6 @@ class FileEdit : public KUrlRequester, public FormWidgetIface int cursorPos, int anchorPos ); private: - Okular::FormFieldText * m_form; int m_prevCursorPos; int m_prevAnchorPos; }; @@ -306,9 +294,6 @@ class ListEdit : public QListWidget, public FormWidgetIface void slotHandleFormListChangedByUndoRedo( int pageNumber, Okular::FormFieldChoice * listForm, const QList< int > & choices ); - - private: - Okular::FormFieldChoice * m_form; }; @@ -332,7 +317,6 @@ class ComboEdit : public QComboBox, public FormWidgetIface ); private: - Okular::FormFieldChoice * m_form; int m_prevCursorPos; int m_prevAnchorPos; }; From 8690497be722b321f6752642775b1cd93f426c98 Mon Sep 17 00:00:00 2001 From: Albert Astals Cid Date: Wed, 25 Oct 2017 15:21:46 +0200 Subject: [PATCH 37/78] Add Page::annotation Will use it later --- core/page.cpp | 10 ++++++++++ core/page.h | 6 ++++++ 2 files changed, 16 insertions(+) diff --git a/core/page.cpp b/core/page.cpp index 8bdd5e6dc..7fe78edfb 100644 --- a/core/page.cpp +++ b/core/page.cpp @@ -488,6 +488,16 @@ QLinkedList< Annotation* > Page::annotations() const 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 ) diff --git a/core/page.h b/core/page.h index 70f89ea03..b3b71944e 100644 --- a/core/page.h +++ b/core/page.h @@ -249,6 +249,12 @@ class OKULARCORE_EXPORT Page */ QLinkedList< Annotation* > annotations() const; + /** + * Returns the annotation with the given unique name. + * @since TODO + */ + 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. From 423dd010e0c94c78d2ddbc697490945e553113be Mon Sep 17 00:00:00 2001 From: Albert Astals Cid Date: Wed, 25 Oct 2017 16:08:13 +0200 Subject: [PATCH 38/78] Remove m_formButtons We don't need it, so it's one less thing to make sure we need to maintain in sync --- ui/formwidgets.cpp | 43 +++++++++++++++++-------------------------- ui/formwidgets.h | 6 +----- 2 files changed, 18 insertions(+), 31 deletions(-) diff --git a/ui/formwidgets.cpp b/ui/formwidgets.cpp index 3612c1123..90f42d173 100644 --- a/ui/formwidgets.cpp +++ b/ui/formwidgets.cpp @@ -78,14 +78,20 @@ void FormWidgetsController::signalAction( Okular::Action *a ) emit action( a ); } -QButtonGroup* FormWidgetsController::registerRadioButton( QAbstractButton *button, Okular::FormFieldButton *formButton ) +void FormWidgetsController::registerRadioButton( FormWidgetIface *fwButton, Okular::FormFieldButton *formButton ) { + if ( !fwButton ) + return; + + QAbstractButton *button = dynamic_cast(fwButton); if ( !button ) - return nullptr; + { + qWarning() << "fwButton is not a QAbstractButton" << fwButton; + return; + } QList< RadioData >::iterator it = m_radios.begin(), itEnd = m_radios.end(); const int id = formButton->id(); - m_formButtons.insert( id, formButton ); m_buttons.insert( id, button ); for ( ; it != itEnd; ++it ) { @@ -95,7 +101,7 @@ QButtonGroup* FormWidgetsController::registerRadioButton( QAbstractButton *butto qCDebug(OkularUiDebug) << "Adding id" << id << "To group including" << (*it).ids; (*it).group->addButton( button ); (*it).group->setId( button, id ); - return (*it).group; + return; } } @@ -115,7 +121,6 @@ QButtonGroup* FormWidgetsController::registerRadioButton( QAbstractButton *butto connect( newdata.group, SIGNAL( buttonClicked(QAbstractButton* ) ), this, SLOT( slotButtonClicked( QAbstractButton* ) ) ); m_radios.append( newdata ); - return newdata.group; } void FormWidgetsController::dropRadioButtons() @@ -127,7 +132,6 @@ void FormWidgetsController::dropRadioButtons() } m_radios.clear(); m_buttons.clear(); - m_formButtons.clear(); } bool FormWidgetsController::canUndo() @@ -147,7 +151,8 @@ void FormWidgetsController::slotButtonClicked( QAbstractButton *button ) { // Checkboxes need to be uncheckable so if clicking a checked one // disable the exclusive status temporarily and uncheck it - if (m_formButtons[check->formField()->id()]->state()) { + Okular::FormFieldButton *formButton = static_cast( check->formField() ); + if ( formButton->state() ) { const bool wasExclusive = button->group()->exclusive(); button->group()->setExclusive(false); check->setChecked(false); @@ -168,9 +173,9 @@ void FormWidgetsController::slotButtonClicked( QAbstractButton *button ) foreach ( QAbstractButton* button, buttons ) { checked.append( button->isChecked() ); - int id = button->group()->id( button ); - formButtons.append( m_formButtons[id] ); - prevChecked.append( m_formButtons[id]->state() ); + Okular::FormFieldButton *formButton = static_cast( dynamic_cast(button)->formField() ); + formButtons.append( formButton ); + prevChecked.append( formButton->state() ); } if (checked != prevChecked) emit formButtonsChangedByWidget( pageNumber, formButtons, checked ); @@ -319,11 +324,6 @@ void FormWidgetIface::setFormWidgetsController( FormWidgetsController *controlle m_controller = controller; } -QAbstractButton* FormWidgetIface::button() -{ - return nullptr; -} - PushButtonEdit::PushButtonEdit( Okular::FormFieldButton * button, QWidget * parent ) : QPushButton( parent ), FormWidgetIface( this, button, !button->isReadOnly() ) @@ -355,16 +355,11 @@ void CheckBoxEdit::setFormWidgetsController( FormWidgetsController *controller ) { Okular::FormFieldButton *form = static_cast(m_ff); FormWidgetIface::setFormWidgetsController( controller ); - m_controller->registerRadioButton( button(), form ); + m_controller->registerRadioButton( this, form ); setChecked( form->state() ); connect( this, &QCheckBox::stateChanged, this, &CheckBoxEdit::slotStateChanged ); } -QAbstractButton* CheckBoxEdit::button() -{ - return this; -} - void CheckBoxEdit::slotStateChanged( int state ) { Okular::FormFieldButton *form = static_cast(m_ff); @@ -386,14 +381,10 @@ void RadioButtonEdit::setFormWidgetsController( FormWidgetsController *controlle { Okular::FormFieldButton *form = static_cast(m_ff); FormWidgetIface::setFormWidgetsController( controller ); - m_controller->registerRadioButton( button(), form ); + m_controller->registerRadioButton( this, form ); setChecked( form->state() ); } -QAbstractButton* RadioButtonEdit::button() -{ - return this; -} FormLineEdit::FormLineEdit( Okular::FormFieldText * text, QWidget * parent ) : QLineEdit( parent ), FormWidgetIface( this, text, true ) diff --git a/ui/formwidgets.h b/ui/formwidgets.h index ddfbc6bff..1f32a76a2 100644 --- a/ui/formwidgets.h +++ b/ui/formwidgets.h @@ -56,7 +56,7 @@ class FormWidgetsController : public QObject void signalAction( Okular::Action *action ); - QButtonGroup* registerRadioButton( QAbstractButton *button, Okular::FormFieldButton *formButton ); + void registerRadioButton( FormWidgetIface *fwButton, Okular::FormFieldButton *formButton ); void dropRadioButtons(); bool canUndo(); bool canRedo(); @@ -123,7 +123,6 @@ class FormWidgetsController : public QObject friend class ComboEdit; QList< RadioData > m_radios; - QHash< int, Okular::FormFieldButton* > m_formButtons; QHash< int, QAbstractButton* > m_buttons; Okular::Document* m_doc; }; @@ -153,7 +152,6 @@ class FormWidgetIface PageViewItem* pageItem() const; virtual void setFormWidgetsController( FormWidgetsController *controller ); - virtual QAbstractButton* button(); protected: FormWidgetsController * m_controller; @@ -186,7 +184,6 @@ class CheckBoxEdit : public QCheckBox, public FormWidgetIface // reimplemented from FormWidgetIface void setFormWidgetsController( FormWidgetsController *controller ) override; - QAbstractButton* button() override; private Q_SLOTS: void slotStateChanged( int state ); @@ -201,7 +198,6 @@ class RadioButtonEdit : public QRadioButton, public FormWidgetIface // reimplemented from FormWidgetIface void setFormWidgetsController( FormWidgetsController *controller ) override; - QAbstractButton* button() override; }; class FormLineEdit : public QLineEdit, public FormWidgetIface From ca5422d0e9ad61d757dea76b67bfd84dda0101f3 Mon Sep 17 00:00:00 2001 From: Albert Astals Cid Date: Thu, 26 Oct 2017 09:47:18 +0200 Subject: [PATCH 39/78] Implement swapBackingFile for the PDF backend How does it work: * What it does is really closing and opening the file again through poppler * This means that things that are generated in "open" time like Page, Rects, Annotations, Forms need to be updated * For Page what we do is swap the PagePrivate so that other classes that hold Page* don't break * Since some parts of the PagePrivate can be reused, we move them in PagePrivate::adoptGeneratedContents * For all the commands in the undo stack we need to update the annotations/forms it refers to, added a new function to do that * The annotationmodel needs updating it's pointers * The widgets for the forms are reused and their form* updated * the widgets for the videos are recreased since videos don't really hold much content (you lose the playing status on save but i think that's acceptable) TODO: Make this work for .okular files TODO: For files with password we will need to reload the file, asking for the password again and thus losing the undo stack, warn the user TODO: autotests --- core/document.cpp | 74 +++++++++++++- core/documentcommands.cpp | 90 ++++++++++++++++- core/documentcommands_p.h | 44 +++++++-- core/generator.cpp | 4 +- core/generator.h | 18 +++- core/page.cpp | 73 ++++++++++++-- core/page.h | 2 +- core/page_p.h | 11 +++ generators/kimgio/generator_kimgio.cpp | 4 +- generators/kimgio/generator_kimgio.h | 2 +- generators/poppler/generator_pdf.cpp | 14 +++ generators/poppler/generator_pdf.h | 1 + ui/annotationmodel.cpp | 22 +++++ ui/annotwindow.cpp | 10 ++ ui/annotwindow.h | 3 + ui/formwidgets.cpp | 5 + ui/formwidgets.h | 3 +- ui/pageview.cpp | 129 +++++++++++++++++++------ ui/pageview.h | 2 + ui/pageviewmouseannotation.cpp | 13 +++ ui/pageviewmouseannotation.h | 3 + 21 files changed, 467 insertions(+), 60 deletions(-) diff --git a/core/document.cpp b/core/document.cpp index bf568a465..960376210 100644 --- a/core/document.cpp +++ b/core/document.cpp @@ -4359,13 +4359,80 @@ bool Document::swapBackingFile( const QString &newFileName, const QUrl & url ) d->saveDocumentInfo(); qCDebug(OkularCoreDebug) << "Swapping backing file to" << newFileName; - if (genIt->generator->swapBackingFile( 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 )) ouc->refreshInternalPageReferences( newPagesVector ); + else + { + qWarning() << "Unhandled undo command" << uc; + } + } + + 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 @@ -4398,8 +4465,11 @@ bool Document::swapBackingFileArchive( const QString &newFileName, const QUrl & const QString tempFileName = newArchive->document.fileName(); qCDebug(OkularCoreDebug) << "Swapping backing file to" << tempFileName; - if (genIt->generator->swapBackingFile( tempFileName )) + QVector< Page * > newPagesVector; + if (genIt->generator->swapBackingFile( newFileName, newPagesVector )) { + // TODO Do the same we do in the other swapBackingFile call + delete d->m_archiveData; d->m_archiveData = newArchive; d->m_url = url; diff --git a/core/documentcommands.cpp b/core/documentcommands.cpp index fd6941e21..4f9c5e8a1 100644 --- 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,19 @@ void AddAnnotationCommand::redo() m_done = true; } +void 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; + } +} + RemoveAnnotationCommand::RemoveAnnotationCommand(Okular::DocumentPrivate * doc, Okular::Annotation* annotation, int pageNumber) : m_docPriv( doc ), @@ -112,12 +126,25 @@ void RemoveAnnotationCommand::undo() 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; } +void 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; + } +} ModifyAnnotationPropertiesCommand::ModifyAnnotationPropertiesCommand( DocumentPrivate* docPriv, Annotation* annotation, @@ -147,6 +174,14 @@ void ModifyAnnotationPropertiesCommand::redo() m_docPriv->performModifyPageAnnotation( m_pageNumber, m_annotation, true ); } +void 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; +} + + TranslateAnnotationCommand::TranslateAnnotationCommand( DocumentPrivate* docPriv, Annotation* annotation, int pageNumber, @@ -212,6 +247,14 @@ Okular::NormalizedRect TranslateAnnotationCommand::translateBoundingRectangle( c return boundingRect; } +void 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; +} + + AdjustAnnotationCommand::AdjustAnnotationCommand(Okular::DocumentPrivate * docPriv, Okular::Annotation * annotation, int pageNumber, @@ -277,6 +320,14 @@ Okular::NormalizedRect AdjustAnnotationCommand::adjustBoundingRectangle( return Okular::NormalizedRect( left, top, right, bottom ); } +void 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; +} + + EditTextCommand::EditTextCommand( const QString & newContents, int newCursorPos, const QString & prevContents, @@ -363,6 +414,7 @@ QString EditTextCommand::newContentsRightOfCursor() return m_newContents.right(m_newContents.length() - m_newCursorPos); } + EditAnnotationContentsCommand::EditAnnotationContentsCommand( DocumentPrivate* docPriv, Annotation* annotation, int pageNumber, @@ -412,6 +464,13 @@ bool EditAnnotationContentsCommand::mergeWith(const QUndoCommand* uc) } } +void EditAnnotationContentsCommand::refreshInternalPageReferences( const QVector< Page * > &newPagesVector ) +{ + auto a = newPagesVector[m_pageNumber]->annotation( m_annotation->uniqueName() ); + if (a) m_annotation = a; +} + + EditFormTextCommand::EditFormTextCommand( Okular::DocumentPrivate* docPriv, Okular::FormFieldText* form, int pageNumber, @@ -463,6 +522,12 @@ bool EditFormTextCommand::mergeWith(const QUndoCommand* uc) } } +void EditFormTextCommand::refreshInternalPageReferences( const QVector< Page * > &newPagesVector ) +{ + m_form = dynamic_cast(Okular::PagePrivate::findEquivalentForm( newPagesVector[m_pageNumber], m_form )); +} + + EditFormListCommand::EditFormListCommand( Okular::DocumentPrivate* docPriv, FormFieldChoice* form, int pageNumber, @@ -493,6 +558,12 @@ void EditFormListCommand::redo() m_docPriv->notifyFormChanges( m_pageNumber ); } +void EditFormListCommand::refreshInternalPageReferences( const QVector< Page * > &newPagesVector ) +{ + m_form = dynamic_cast(Okular::PagePrivate::findEquivalentForm( newPagesVector[m_pageNumber], m_form )); +} + + EditFormComboCommand::EditFormComboCommand( Okular::DocumentPrivate* docPriv, FormFieldChoice* form, int pageNumber, @@ -579,6 +650,12 @@ bool EditFormComboCommand::mergeWith( const QUndoCommand *uc ) } } +void EditFormComboCommand::refreshInternalPageReferences( const QVector< Page * > &newPagesVector ) +{ + m_form = dynamic_cast(Okular::PagePrivate::findEquivalentForm( newPagesVector[m_pageNumber], m_form )); +} + + EditFormButtonsCommand::EditFormButtonsCommand( Okular::DocumentPrivate* docPriv, int pageNumber, const QList< FormFieldButton* > & formButtons, @@ -628,6 +705,17 @@ void EditFormButtonsCommand::redo() m_docPriv->notifyFormChanges( m_pageNumber ); } +void 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 )); + m_formButtons << button; + } +} + void EditFormButtonsCommand::clearFormButtonStates() { foreach( FormFieldButton* formButton, m_formButtons ) diff --git a/core/documentcommands_p.h b/core/documentcommands_p.h index 757397337..44b7c19f6 100644 --- a/core/documentcommands_p.h +++ b/core/documentcommands_p.h @@ -23,8 +23,15 @@ class DocumentPrivate; class FormFieldText; class FormFieldButton; class FormFieldChoice; +class Page; -class AddAnnotationCommand : public QUndoCommand +class OkularUndoCommand : public QUndoCommand +{ + public: + virtual void 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 @@ class AddAnnotationCommand : public QUndoCommand void redo() override; + void refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) override; + private: Okular::DocumentPrivate * m_docPriv; Okular::Annotation* m_annotation; @@ -42,7 +51,7 @@ class AddAnnotationCommand : public QUndoCommand 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 @@ class RemoveAnnotationCommand : public QUndoCommand void undo() override; void redo() override; + void refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) override; + private: Okular::DocumentPrivate * m_docPriv; Okular::Annotation* m_annotation; @@ -57,7 +68,7 @@ class RemoveAnnotationCommand : public QUndoCommand bool m_done; }; -class ModifyAnnotationPropertiesCommand : public QUndoCommand +class ModifyAnnotationPropertiesCommand : public OkularUndoCommand { public: ModifyAnnotationPropertiesCommand( Okular::DocumentPrivate* docPriv, Okular::Annotation* annotation, @@ -68,6 +79,8 @@ class ModifyAnnotationPropertiesCommand : public QUndoCommand void undo() override; void redo() override; + void refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) override; + private: Okular::DocumentPrivate * m_docPriv; Okular::Annotation* m_annotation; @@ -76,7 +89,7 @@ class ModifyAnnotationPropertiesCommand : public QUndoCommand QDomNode m_newProperties; }; -class TranslateAnnotationCommand : public QUndoCommand +class TranslateAnnotationCommand : public OkularUndoCommand { public: TranslateAnnotationCommand(Okular::DocumentPrivate* docPriv, @@ -92,6 +105,8 @@ class TranslateAnnotationCommand : public QUndoCommand Okular::NormalizedPoint minusDelta(); Okular::NormalizedRect translateBoundingRectangle( const Okular::NormalizedPoint & delta ); + void refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) override; + private: Okular::DocumentPrivate * m_docPriv; Okular::Annotation* m_annotation; @@ -100,7 +115,7 @@ class TranslateAnnotationCommand : public QUndoCommand bool m_completeDrag; }; -class AdjustAnnotationCommand : public QUndoCommand +class AdjustAnnotationCommand : public OkularUndoCommand { public: AdjustAnnotationCommand(Okular::DocumentPrivate * docPriv, @@ -117,6 +132,8 @@ class AdjustAnnotationCommand : public QUndoCommand Okular::NormalizedRect adjustBoundingRectangle( const Okular::NormalizedPoint & delta1, const Okular::NormalizedPoint & delta2 ); + void refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) override; + private: Okular::DocumentPrivate * m_docPriv; Okular::Annotation* m_annotation; @@ -126,7 +143,7 @@ class AdjustAnnotationCommand : public QUndoCommand bool m_completeDrag; }; -class EditTextCommand : public QUndoCommand +class EditTextCommand : public OkularUndoCommand { public: EditTextCommand( const QString & newContents, @@ -182,6 +199,8 @@ class EditAnnotationContentsCommand : public EditTextCommand int id() const override; bool mergeWith(const QUndoCommand *uc) override; + void refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) override; + private: Okular::DocumentPrivate * m_docPriv; Okular::Annotation* m_annotation; @@ -203,13 +222,16 @@ class EditFormTextCommand : public EditTextCommand void redo() override; int id() const override; bool mergeWith( const QUndoCommand *uc ) override; + + void 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 @@ class EditFormListCommand : public QUndoCommand void undo() override; void redo() override; + void refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) override; + private: Okular::DocumentPrivate* m_docPriv; FormFieldChoice* m_form; @@ -248,6 +272,8 @@ class EditFormComboCommand : public EditTextCommand int id() const override; bool mergeWith( const QUndoCommand *uc ) override; + void refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) override; + private: Okular::DocumentPrivate* m_docPriv; FormFieldChoice* m_form; @@ -256,7 +282,7 @@ class EditFormComboCommand : public EditTextCommand int m_prevIndex; }; -class EditFormButtonsCommand : public QUndoCommand +class EditFormButtonsCommand : public OkularUndoCommand { public: EditFormButtonsCommand( Okular::DocumentPrivate* docPriv, @@ -268,6 +294,8 @@ class EditFormButtonsCommand : public QUndoCommand void undo() override; void redo() override; + void refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) override; + private: void clearFormButtonStates(); diff --git a/core/generator.cpp b/core/generator.cpp index 5b904ed1b..e9605d62f 100644 --- a/core/generator.cpp +++ b/core/generator.cpp @@ -199,9 +199,9 @@ Document::OpenResult Generator::loadDocumentFromDataWithPassword( const QByteArr return loadDocumentFromData( fileData, pagesVector ) ? Document::OpenSuccess : Document::OpenError; } -bool Generator::swapBackingFile( QString const &/*newFileName */) +Generator::SwapBackingFileResult Generator::swapBackingFile( QString const &/*newFileName */, QVector & /*newPagesVector*/ ) { - return false; + return SwapBackingFileError; } bool Generator::closeDocument() diff --git a/core/generator.h b/core/generator.h index 064984e1f..2d3edfcbf 100644 --- a/core/generator.h +++ b/core/generator.h @@ -270,17 +270,27 @@ class OKULARCORE_EXPORT Generator : public QObject */ 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 0.20 (KDE 4.14) - * - * @returns true on success, false otherwise. + * @since 1.3 */ - virtual bool swapBackingFile( const QString &newFileName ); + virtual SwapBackingFileResult swapBackingFile( const QString &newFileName, QVector & newPagesVector ); /** * This method is called when the document is closed and not used diff --git a/core/page.cpp b/core/page.cpp index 7fe78edfb..6ee095379 100644 --- a/core/page.cpp +++ b/core/page.cpp @@ -135,14 +135,17 @@ Page::Page( uint page, double w, double h, Rotation o ) Page::~Page() { - deletePixmaps(); - deleteRects(); - d->deleteHighlights(); - deleteAnnotations(); - d->deleteTextSelections(); - deleteSourceReferences(); + if (d) + { + deletePixmaps(); + deleteRects(); + d->deleteHighlights(); + deleteAnnotations(); + d->deleteTextSelections(); + deleteSourceReferences(); - delete d; + delete d; + } } int Page::number() const @@ -1050,3 +1053,59 @@ void PagePrivate::setTilesManager( const DocumentObserver *observer, TilesManage 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.h b/core/page.h index b3b71944e..62c005fdd 100644 --- a/core/page.h +++ b/core/page.h @@ -392,7 +392,7 @@ class OKULARCORE_EXPORT Page 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_p.h b/core/page_p.h index 234e84f1a..86248eff1 100644 --- a/core/page_p.h +++ b/core/page_p.h @@ -120,6 +120,17 @@ class PagePrivate */ 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: diff --git a/generators/kimgio/generator_kimgio.cpp b/generators/kimgio/generator_kimgio.cpp index b53129e89..124672877 100644 --- a/generators/kimgio/generator_kimgio.cpp +++ b/generators/kimgio/generator_kimgio.cpp @@ -91,11 +91,11 @@ bool KIMGIOGenerator::loadDocumentInternal(const QByteArray & fileData, const QS return true; } -bool KIMGIOGenerator::swapBackingFile( QString const &/*newFileName*/ ) +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 true; + return SwapBackingFileNoOp; } bool KIMGIOGenerator::doCloseDocument() diff --git a/generators/kimgio/generator_kimgio.h b/generators/kimgio/generator_kimgio.h index eee4232a1..b80534acb 100644 --- a/generators/kimgio/generator_kimgio.h +++ b/generators/kimgio/generator_kimgio.h @@ -27,7 +27,7 @@ class KIMGIOGenerator : public Okular::Generator // [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; - bool swapBackingFile( QString const &newFileName ) 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/poppler/generator_pdf.cpp b/generators/poppler/generator_pdf.cpp index c29f96bf7..4adc126ad 100644 --- a/generators/poppler/generator_pdf.cpp +++ b/generators/poppler/generator_pdf.cpp @@ -512,6 +512,7 @@ PDFGenerator::PDFGenerator( QObject *parent, const QVariantList &args ) 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 @@ -592,6 +593,19 @@ Okular::Document::OpenResult PDFGenerator::init(QVector & pagesVe return Okular::Document::OpenSuccess; } +PDFGenerator::SwapBackingFileResult PDFGenerator::swapBackingFile( QString const &newFileName, QVector & newPagesVector ) +{ + doCloseDocument(); + // TODO For files with password we need to figure out a way to return false but that doesn't + // end in error but that ends up in a reload. + // Probably hijacking at the canSwapBackingFile level + auto openResult = loadDocumentWithPassword(newFileName, newPagesVector, QString()); + if (openResult != Okular::Document::OpenSuccess) + return SwapBackingFileError; + + return SwapBackingFileReloadInternalData; +} + bool PDFGenerator::doCloseDocument() { // remove internal objects diff --git a/generators/poppler/generator_pdf.h b/generators/poppler/generator_pdf.h index a078f50bd..957a28592 100644 --- a/generators/poppler/generator_pdf.h +++ b/generators/poppler/generator_pdf.h @@ -99,6 +99,7 @@ class PDFGenerator : public Okular::Generator, public Okular::ConfigInterface, p 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/ui/annotationmodel.cpp b/ui/annotationmodel.cpp index df6b89c9a..ea5347dea 100644 --- a/ui/annotationmodel.cpp +++ b/ui/annotationmodel.cpp @@ -105,10 +105,32 @@ AnnotationModelPrivate::~AnnotationModelPrivate() delete root; } +static void updateAnnotationPointer( AnnItem *item, const QVector< Okular::Page * > &pages ) +{ + if ( item->annotation ) { + item->annotation = pages[ item->page ]->annotation( item->annotation->uniqueName() ); + if ( !item->annotation ) + qWarning() << "Lost annotation on document save, something went wrong"; + } + + foreach ( AnnItem *child, item->children ) + updateAnnotationPointer( child, pages ); +} + void AnnotationModelPrivate::notifySetup( const QVector< Okular::Page * > &pages, int setupFlags ) { if ( !( setupFlags & Okular::DocumentObserver::DocumentChanged ) ) + { + if ( setupFlags & Okular::DocumentObserver::UrlChanged ) + { + // Here with UrlChanged and no document changed it means we + // need to update all the Annotation* otherwise + // they still point to the old document ones, luckily the old ones are still + // around so we can look for the new ones using unique ids, etc + updateAnnotationPointer( root, pages ); + } return; + } q->beginResetModel(); qDeleteAll( root->children ); diff --git a/ui/annotwindow.cpp b/ui/annotwindow.cpp index 011f70cf6..ec8e81470 100644 --- a/ui/annotwindow.cpp +++ b/ui/annotwindow.cpp @@ -246,6 +246,11 @@ Okular::Annotation * AnnotWindow::annotation() const return m_annot; } +void AnnotWindow::updateAnnotation( Okular::Annotation * a ) +{ + m_annot = a; +} + void AnnotWindow::reloadInfo() { const QColor newcolor = m_annot->style().color().isValid() ? m_annot->style().color() : Qt::yellow; @@ -261,6 +266,11 @@ void AnnotWindow::reloadInfo() m_title->setDate( m_annot->modificationDate() ); } +int AnnotWindow::pageNumber() const +{ + return m_page; +} + void AnnotWindow::showEvent( QShowEvent * event ) { QFrame::showEvent( event ); diff --git a/ui/annotwindow.h b/ui/annotwindow.h index 5c403f1fc..a81154c4d 100644 --- a/ui/annotwindow.h +++ b/ui/annotwindow.h @@ -37,6 +37,9 @@ class AnnotWindow : public QFrame void reloadInfo(); Okular::Annotation * annotation() const; + int pageNumber() const; + + void updateAnnotation( Okular::Annotation * a ); private: MovableTitle * m_title; diff --git a/ui/formwidgets.cpp b/ui/formwidgets.cpp index 90f42d173..64cc152a2 100644 --- a/ui/formwidgets.cpp +++ b/ui/formwidgets.cpp @@ -309,6 +309,11 @@ void FormWidgetIface::setPageItem( PageViewItem *pageItem ) m_pageItem = pageItem; } +void FormWidgetIface::setFormField( Okular::FormField *field ) +{ + m_ff = field; +} + Okular::FormField* FormWidgetIface::formField() const { return m_ff; diff --git a/ui/formwidgets.h b/ui/formwidgets.h index 1f32a76a2..bb5773d3f 100644 --- a/ui/formwidgets.h +++ b/ui/formwidgets.h @@ -148,8 +148,9 @@ class FormWidgetIface void setCanBeFilled( bool fill ); void setPageItem( PageViewItem *pageItem ); - Okular::FormField* formField() const; PageViewItem* pageItem() const; + void setFormField( Okular::FormField *field ); + Okular::FormField* formField() const; virtual void setFormWidgetsController( FormWidgetsController *controller ); diff --git a/ui/pageview.cpp b/ui/pageview.cpp index 8b98c36b2..0ddbb2bf7 100644 --- a/ui/pageview.cpp +++ b/ui/pageview.cpp @@ -81,6 +81,7 @@ #include "core/document_p.h" #include "core/form.h" #include "core/page.h" +#include "core/page_p.h" #include "core/misc.h" #include "core/generator.h" #include "core/movie.h" @@ -937,6 +938,43 @@ void PageView::selectAll() } } +void PageView::createAnnotationsVideoWidgets(PageViewItem *item, const QLinkedList< Okular::Annotation * > &annotations) +{ + qDeleteAll( item->videoWidgets() ); + item->videoWidgets().clear(); + + QLinkedList< Okular::Annotation * >::const_iterator aIt = annotations.constBegin(), aEnd = annotations.constEnd(); + for ( ; aIt != aEnd; ++aIt ) + { + Okular::Annotation * a = *aIt; + if ( a->subType() == Okular::Annotation::AMovie ) + { + Okular::MovieAnnotation * movieAnn = static_cast< Okular::MovieAnnotation * >( a ); + VideoWidget * vw = new VideoWidget( movieAnn, movieAnn->movie(), d->document, viewport() ); + item->videoWidgets().insert( movieAnn->movie(), vw ); + vw->pageInitialized(); + } + else if ( a->subType() == Okular::Annotation::ARichMedia ) + { + Okular::RichMediaAnnotation * richMediaAnn = static_cast< Okular::RichMediaAnnotation * >( a ); + VideoWidget * vw = new VideoWidget( richMediaAnn, richMediaAnn->movie(), d->document, viewport() ); + item->videoWidgets().insert( richMediaAnn->movie(), vw ); + vw->pageInitialized(); + } + else if ( a->subType() == Okular::Annotation::AScreen ) + { + const Okular::ScreenAnnotation * screenAnn = static_cast< Okular::ScreenAnnotation * >( a ); + Okular::Movie *movie = GuiUtils::renditionMovieFromScreenAnnotation( screenAnn ); + if ( movie ) + { + VideoWidget * vw = new VideoWidget( screenAnn, movie, d->document, viewport() ); + item->videoWidgets().insert( movie, vw ); + vw->pageInitialized(); + } + } + } +} + //BEGIN DocumentObserver inherited methods void PageView::notifySetup( const QVector< Okular::Page * > & pageSet, int setupFlags ) { @@ -966,8 +1004,66 @@ void PageView::notifySetup( const QVector< Okular::Page * > & pageSet, int setup w->setCanBeFilled( allowfillforms ); } } + if ( !documentChanged ) + { + if ( setupFlags & Okular::DocumentObserver::UrlChanged ) + { + // Here with UrlChanged and no document changed it means we + // need to update all the Annotation* and Form* otherwise + // they still point to the old document ones, luckily the old ones are still + // around so we can look for the new ones using unique ids, etc + d->mouseAnnotation->updateAnnotationPointers(); + + foreach(AnnotWindow *aw, d->m_annowindows) + { + Okular::Annotation *newA = d->document->page( aw->pageNumber() )->annotation( aw->annotation()->uniqueName() ); + aw->updateAnnotation( newA ); + } + + const QRect viewportRect( horizontalScrollBar()->value(), verticalScrollBar()->value(), + viewport()->width(), viewport()->height() ); + for ( int i = 0; i < count; i++ ) + { + PageViewItem *item = d->items[i]; + const QSet fws = item->formWidgets(); + foreach ( FormWidgetIface * w, fws ) + { + Okular::FormField *f = Okular::PagePrivate::findEquivalentForm( d->document->page( i ), w->formField() ); + if (f) + { + w->setFormField( f ); + } + else + { + qWarning() << "Lost form field on document save, something is wrong"; + item->formWidgets().remove(w); + delete w; + } + } + + // For the video widgets we don't really care about reusing them since they don't contain much info so just + // create them again + createAnnotationsVideoWidgets( item, pageSet[i]->annotations() ); + Q_FOREACH ( VideoWidget *vw, item->videoWidgets() ) + { + const Okular::NormalizedRect r = vw->normGeometry(); + vw->setGeometry( + qRound( item->uncroppedGeometry().left() + item->uncroppedWidth() * r.left ) + 1 - viewportRect.left(), + qRound( item->uncroppedGeometry().top() + item->uncroppedHeight() * r.top ) + 1 - viewportRect.top(), + qRound( fabs( r.right - r.left ) * item->uncroppedGeometry().width() ), + qRound( fabs( r.bottom - r.top ) * item->uncroppedGeometry().height() ) ); + + // Workaround, otherwise the size somehow gets lost + vw->show(); + vw->hide(); + } + + } + } + return; + } } // mouseAnnotation must not access our PageViewItem widgets any longer @@ -1011,37 +1107,8 @@ void PageView::notifySetup( const QVector< Okular::Page * > & pageSet, int setup hasformwidgets = true; } } - const QLinkedList< Okular::Annotation * > annotations = (*setIt)->annotations(); - QLinkedList< Okular::Annotation * >::const_iterator aIt = annotations.constBegin(), aEnd = annotations.constEnd(); - for ( ; aIt != aEnd; ++aIt ) - { - Okular::Annotation * a = *aIt; - if ( a->subType() == Okular::Annotation::AMovie ) - { - Okular::MovieAnnotation * movieAnn = static_cast< Okular::MovieAnnotation * >( a ); - VideoWidget * vw = new VideoWidget( movieAnn, movieAnn->movie(), d->document, viewport() ); - item->videoWidgets().insert( movieAnn->movie(), vw ); - vw->pageInitialized(); - } - else if ( a->subType() == Okular::Annotation::ARichMedia ) - { - Okular::RichMediaAnnotation * richMediaAnn = static_cast< Okular::RichMediaAnnotation * >( a ); - VideoWidget * vw = new VideoWidget( richMediaAnn, richMediaAnn->movie(), d->document, viewport() ); - item->videoWidgets().insert( richMediaAnn->movie(), vw ); - vw->pageInitialized(); - } - else if ( a->subType() == Okular::Annotation::AScreen ) - { - const Okular::ScreenAnnotation * screenAnn = static_cast< Okular::ScreenAnnotation * >( a ); - Okular::Movie *movie = GuiUtils::renditionMovieFromScreenAnnotation( screenAnn ); - if ( movie ) - { - VideoWidget * vw = new VideoWidget( screenAnn, movie, d->document, viewport() ); - item->videoWidgets().insert( movie, vw ); - vw->pageInitialized(); - } - } - } + + createAnnotationsVideoWidgets( item, (*setIt)->annotations() ); } // invalidate layout so relayout/repaint will happen on next viewport change diff --git a/ui/pageview.h b/ui/pageview.h index 2b0de2b22..e09ecaf24 100644 --- a/ui/pageview.h +++ b/ui/pageview.h @@ -199,6 +199,8 @@ Q_OBJECT // handle link clicked bool mouseReleaseOverLink( const Okular::ObjectRect * rect ) const; + void createAnnotationsVideoWidgets(PageViewItem *item, const QLinkedList< Okular::Annotation * > &annotations); + // don't want to expose classes in here class PageViewPrivate * d; diff --git a/ui/pageviewmouseannotation.cpp b/ui/pageviewmouseannotation.cpp index 01ae335c0..c15bc4164 100644 --- a/ui/pageviewmouseannotation.cpp +++ b/ui/pageviewmouseannotation.cpp @@ -400,6 +400,19 @@ Qt::CursorShape MouseAnnotation::cursor() const return Qt::ArrowCursor; } +void MouseAnnotation::updateAnnotationPointers() +{ + if (m_focusedAnnotation.annotation) + { + m_focusedAnnotation.annotation = m_document->page( m_focusedAnnotation.pageNumber )->annotation( m_focusedAnnotation.annotation->uniqueName() ); + } + + if (m_mouseOverAnnotation.annotation) + { + m_mouseOverAnnotation.annotation = m_document->page( m_mouseOverAnnotation.pageNumber )->annotation( m_mouseOverAnnotation.annotation->uniqueName() ); + } +} + void MouseAnnotation::cancel() { if ( isActive() ) diff --git a/ui/pageviewmouseannotation.h b/ui/pageviewmouseannotation.h index 0c9545137..fe06e3195 100644 --- a/ui/pageviewmouseannotation.h +++ b/ui/pageviewmouseannotation.h @@ -113,6 +113,9 @@ public: Qt::CursorShape cursor() const; + // needs to be called after document save + void updateAnnotationPointers(); + enum MouseAnnotationState { StateInactive, StateFocused, From 8b06b0feec8a4cbf2348cab2f2d1250b322954a0 Mon Sep 17 00:00:00 2001 From: Albert Astals Cid Date: Thu, 26 Oct 2017 15:47:21 +0200 Subject: [PATCH 40/78] Make it clear continue will make you lose changes --- part.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/part.cpp b/part.cpp index 12fffe760..99c594403 100644 --- a/part.cpp +++ b/part.cpp @@ -2475,7 +2475,7 @@ bool Part::saveAs( const QUrl & saveUrl, SaveAsFlags flags ) i18n( "The following elements cannot be saved in this format and will be lost.
If you want to preserve them, please use the Okular document archive format." ), listOfwontSaves, i18n( "Warning" ), KGuiItem( i18n( "Save as Okular document archive..." ), "document-save-as" ), // <- KMessageBox::Yes - KStandardGuiItem::cont() ); // <- KMessageBox::NO + KGuiItem( i18n( "Continue losing changes..." ), "arrow-right" ) ); // <- KMessageBox::NO switch (result) { From b2673a58b10c1399722978f5a2e914cc9faa2162 Mon Sep 17 00:00:00 2001 From: Albert Astals Cid Date: Thu, 26 Oct 2017 18:58:34 +0200 Subject: [PATCH 41/78] When saving a file that had password we will reload it Also tweak error messages a bit --- generators/poppler/generator_pdf.cpp | 3 -- part.cpp | 62 ++++++++++++++++++++++++---- part.h | 1 + 3 files changed, 55 insertions(+), 11 deletions(-) diff --git a/generators/poppler/generator_pdf.cpp b/generators/poppler/generator_pdf.cpp index 4adc126ad..2cfbd895a 100644 --- a/generators/poppler/generator_pdf.cpp +++ b/generators/poppler/generator_pdf.cpp @@ -596,9 +596,6 @@ Okular::Document::OpenResult PDFGenerator::init(QVector & pagesVe PDFGenerator::SwapBackingFileResult PDFGenerator::swapBackingFile( QString const &newFileName, QVector & newPagesVector ) { doCloseDocument(); - // TODO For files with password we need to figure out a way to return false but that doesn't - // end in error but that ends up in a reload. - // Probably hijacking at the canSwapBackingFile level auto openResult = loadDocumentWithPassword(newFileName, newPagesVector, QString()); if (openResult != Okular::Document::OpenSuccess) return SwapBackingFileError; diff --git a/part.cpp b/part.cpp index 99c594403..559f55657 100644 --- a/part.cpp +++ b/part.cpp @@ -300,7 +300,7 @@ Part::Part(QWidget *parentWidget, QObject *parent, const QVariantList &args) : KParts::ReadWritePart(parent), -m_tempfile( nullptr ), m_swapInsteadOfOpening( false ), 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 @@ -1377,6 +1377,7 @@ Document::OpenResult Part::doOpenFile( const QMimeType &mimeA, const QString &fi { 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; @@ -1441,10 +1442,15 @@ Document::OpenResult Part::doOpenFile( const QMimeType &mimeA, const QString &fi 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 ); + } } } } @@ -2436,6 +2442,24 @@ bool Part::saveAs(const QUrl & saveUrl) bool Part::saveAs( const QUrl & saveUrl, SaveAsFlags flags ) { + bool hasUserAcceptedReload = false; + if ( m_documentOpenWithPassword ) + { + const int res = KMessageBox::warningYesNo( widget(), + i18n( "The current document has password.
When saving we need to reload the file so you will get the password asked again and the undo/redo stack 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; + } + } + QTemporaryFile tf; QString fileName; if ( !tf.open() ) @@ -2452,6 +2476,22 @@ bool Part::saveAs( const QUrl & saveUrl, SaveAsFlags flags ) // Does the user want a .okular archive? if ( flags & SaveAsOkularArchive ) { + 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 ) ); @@ -2471,11 +2511,17 @@ bool Part::saveAs( const QUrl & saveUrl, SaveAsFlags flags ) if ( wontSaveAnnotations ) listOfwontSaves << i18n( "User annotations" ); if ( !listOfwontSaves.isEmpty() && !( flags & SaveAsDontShowWarning ) ) { - int result = KMessageBox::warningYesNoCancelList( widget(), - i18n( "The following elements cannot be saved in this format and will be lost.
If you want to preserve them, please use the Okular document archive format." ), + const QString warningMessage = m_document->canSwapBackingFile() ? + i18n( "The following elements cannot be saved in this format.
If you want to preserve them, please use the Okular document archive format." ) : + i18n( "The following elements cannot be saved in this format and will be lost (as well as the undo/redo stack).
If you want to preserve them, please use the Okular document archive format." ); + 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( i18n( "Continue losing changes..." ), "arrow-right" ) ); // <- KMessageBox::NO + KGuiItem( continueMessage, "arrow-right" ) ); // <- KMessageBox::NO switch (result) { @@ -2593,7 +2639,7 @@ bool Part::saveAs( const QUrl & saveUrl, SaveAsFlags flags ) bool reloadedCorrectly = true; // Make the generator use the new new file instead of the old one - if ( m_document->canSwapBackingFile() ) + if ( m_document->canSwapBackingFile() && !m_documentOpenWithPassword ) { // this calls openFile internally, which in turn actually calls // m_document->swapBackingFile() instead of the regular loadDocument diff --git a/part.h b/part.h index 5ab09e907..4fb8ab40a 100644 --- a/part.h +++ b/part.h @@ -293,6 +293,7 @@ class OKULARPART_EXPORT Part : public KParts::ReadWritePart, public Okular::Docu 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 From 147735fc5a85a5d33d44368af7c6b0be227d3b04 Mon Sep 17 00:00:00 2001 From: Albert Astals Cid Date: Thu, 26 Oct 2017 19:11:44 +0200 Subject: [PATCH 42/78] set the modified flag when saving a file that doesn't support saving all of the things (e.g. annotations) but supports file swapping Example, png files. If you add an annotation and save and choose to lose changes, the annotation will still be around "in okular" but not on the file, so we need to mark the file as modified so that when the user tries to close he will get again the warning of "you're trying to save and will lose data" because otherwise the user may think the annotation actually was saved since he can see it --- part.cpp | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/part.cpp b/part.cpp index 559f55657..f7bf554fc 100644 --- a/part.cpp +++ b/part.cpp @@ -2460,6 +2460,8 @@ bool Part::saveAs( const QUrl & saveUrl, SaveAsFlags flags ) } } + bool setModifiedAfterSave = false; + QTemporaryFile tf; QString fileName; if ( !tf.open() ) @@ -2528,6 +2530,7 @@ bool Part::saveAs( const QUrl & saveUrl, SaveAsFlags flags ) 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; @@ -2643,8 +2646,18 @@ bool Part::saveAs( const QUrl & saveUrl, SaveAsFlags flags ) { // this calls openFile internally, which in turn actually calls // m_document->swapBackingFile() instead of the regular loadDocument - if ( !openUrl( saveUrl, true /* swapInsteadOfOpening */ ) ) + if ( openUrl( saveUrl, true /* swapInsteadOfOpening */ ) ) + { + if ( setModifiedAfterSave ) + { + setModified(); + setWindowTitleFromDocument(); + } + } + else + { reloadedCorrectly = false; + } } else { From 0cb4ff7dad74f5815efea62bc32a11f16efea38d Mon Sep 17 00:00:00 2001 From: Albert Astals Cid Date: Thu, 26 Oct 2017 21:09:21 +0200 Subject: [PATCH 43/78] Fix copy&paste mistake --- core/document.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/document.cpp b/core/document.cpp index 960376210..19293140a 100644 --- a/core/document.cpp +++ b/core/document.cpp @@ -4466,7 +4466,7 @@ bool Document::swapBackingFileArchive( const QString &newFileName, const QUrl & qCDebug(OkularCoreDebug) << "Swapping backing file to" << tempFileName; QVector< Page * > newPagesVector; - if (genIt->generator->swapBackingFile( newFileName, newPagesVector )) + if (genIt->generator->swapBackingFile( tempFileName, newPagesVector )) { // TODO Do the same we do in the other swapBackingFile call From 8d7ea894f3d1ed6aba46f182aeaa20125f146bbb Mon Sep 17 00:00:00 2001 From: Albert Astals Cid Date: Thu, 26 Oct 2017 21:24:09 +0200 Subject: [PATCH 44/78] make parttest pass again --- autotests/parttest.cpp | 8 ++++---- part.cpp | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/autotests/parttest.cpp b/autotests/parttest.cpp index 71cfac04f..95b907018 100644 --- a/autotests/parttest.cpp +++ b/autotests/parttest.cpp @@ -739,7 +739,7 @@ void PartTest::testSaveAs() // user to confirm. On the other end, if we expect annotations to be // preserved (and thus no warning), we keep the warning on so that if it // shows the test timeouts and we can notice that something is wrong. - const Part::SaveAsFlags saveAsNativeFlags = nativelySupportsAnnotations ? + const Part::SaveAsFlags extraSaveAsFlags = nativelySupportsAnnotations ? Part::NoSaveAsFlags : Part::SaveAsDontShowWarning; QString annotName; @@ -761,8 +761,8 @@ void PartTest::testSaveAs() part.m_document->addPageAnnotation( 0, annot ); annotName = annot->uniqueName(); - QVERIFY( part.saveAs( QUrl::fromLocalFile( archiveSave.fileName() ), Part::SaveAsOkularArchive ) ); - QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), saveAsNativeFlags ) ); + QVERIFY( part.saveAs( QUrl::fromLocalFile( archiveSave.fileName() ), extraSaveAsFlags | Part::SaveAsOkularArchive ) ); + QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), extraSaveAsFlags ) ); part.closeUrl(); } @@ -775,7 +775,7 @@ void PartTest::testSaveAs() QCOMPARE( part.m_document->page( 0 )->annotations().size(), 1 ); QCOMPARE( part.m_document->page( 0 )->annotations().first()->uniqueName(), annotName ); - QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeFromArchiveFile.fileName() ), saveAsNativeFlags ) ); + QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeFromArchiveFile.fileName() ), extraSaveAsFlags ) ); part.closeUrl(); } diff --git a/part.cpp b/part.cpp index f7bf554fc..9d97fa646 100644 --- a/part.cpp +++ b/part.cpp @@ -2478,7 +2478,7 @@ bool Part::saveAs( const QUrl & saveUrl, SaveAsFlags flags ) // Does the user want a .okular archive? if ( flags & SaveAsOkularArchive ) { - if ( !hasUserAcceptedReload && !m_document->canSwapBackingFile() ) + if ( !hasUserAcceptedReload && !m_document->canSwapBackingFile() && !( flags & SaveAsDontShowWarning ) ) { 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?" ), From c86c9a04a28ca7edb5e779a6b37fd78e661bd39a Mon Sep 17 00:00:00 2001 From: Albert Astals Cid Date: Fri, 27 Oct 2017 09:35:11 +0200 Subject: [PATCH 45/78] Add a jpg to the saveAs tests This way we're also testing a backend that supports swapBackingFile but doesn't natively support annotations --- autotests/parttest.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/autotests/parttest.cpp b/autotests/parttest.cpp index 95b907018..1082e26bd 100644 --- a/autotests/parttest.cpp +++ b/autotests/parttest.cpp @@ -733,6 +733,7 @@ void PartTest::testSaveAs() QFETCH(QString, file); QFETCH(QString, extension); QFETCH(bool, nativelySupportsAnnotations); + QFETCH(bool, canSwapBackingFile); // If we expect not to be able to preserve annotations when we write a // native file, disable the warning otherwise the test will wait for the @@ -755,6 +756,8 @@ void PartTest::testSaveAs() 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" ); @@ -812,9 +815,11 @@ 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; - QTest::newRow("epub") << KDESRCDIR "data/contents.epub" << "epub" << false; + 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; } } From 835c9c495d4b340005af21e0e5e757b224ab413f Mon Sep 17 00:00:00 2001 From: Albert Astals Cid Date: Fri, 27 Oct 2017 10:12:29 +0200 Subject: [PATCH 46/78] Add test for saveAs + undo/redo stack Proves that if the backend supports swapping the file we keep the undo/stack around correctly --- autotests/parttest.cpp | 161 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) diff --git a/autotests/parttest.cpp b/autotests/parttest.cpp index 1082e26bd..10a5db9b4 100644 --- a/autotests/parttest.cpp +++ b/autotests/parttest.cpp @@ -48,6 +48,8 @@ class PartTest void testClickInternalLink(); void testSaveAs(); void testSaveAs_data(); + void testSaveAsUndoStackAnnotations(); + void testSaveAsUndoStackAnnotations_data(); void testMouseMoveOverLinkWhileInSelectionMode(); void testClickUrlLinkWhileInSelectionMode(); void testeTextSelectionOverAndAcrossLinks_data(); @@ -822,6 +824,165 @@ void PartTest::testSaveAs_data() 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); + + // If we expect not to be able to preserve annotations when we write a + // native file, disable the warning otherwise the test will wait for the + // user to confirm. On the other end, if we expect annotations to be + // preserved (and thus no warning), we keep the warning on so that if it + // shows the test timeouts and we can notice that something is wrong. + const Part::SaveAsFlags extraSaveAsFlags = nativelySupportsAnnotations ? + Part::NoSaveAsFlags : Part::SaveAsDontShowWarning; + + QTemporaryFile nativeDirectSave( QString( "%1/okrXXXXXX.%2" ).arg( QDir::tempPath() ).arg ( extension ) ); + QVERIFY( nativeDirectSave.open() ); nativeDirectSave.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(); + + QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), extraSaveAsFlags ) ); + + 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( nativeDirectSave.fileName() ), extraSaveAsFlags ) ); + 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( nativeDirectSave.fileName() ), extraSaveAsFlags ) ); + 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 + QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), extraSaveAsFlags ) ); + 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( nativeDirectSave.fileName() ), extraSaveAsFlags ) ); + 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 + QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), extraSaveAsFlags ) ); + QVERIFY( part.m_document->canUndo() ); + part.m_document->undo(); + QVERIFY( part.m_document->canUndo() ); + + QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), extraSaveAsFlags ) ); + QVERIFY( part.m_document->canUndo() ); + part.m_document->undo(); + QVERIFY( part.m_document->canUndo() ); + + QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), extraSaveAsFlags ) ); + QVERIFY( part.m_document->canUndo() ); + part.m_document->undo(); + QVERIFY( part.m_document->canUndo() ); + + QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), extraSaveAsFlags ) ); + 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( nativeDirectSave.fileName() ), extraSaveAsFlags ) ); + QVERIFY( part.m_document->canRedo() ); + part.m_document->redo(); + QVERIFY( part.m_document->canRedo() ); + + QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), extraSaveAsFlags ) ); + QVERIFY( part.m_document->canRedo() ); + part.m_document->redo(); + QVERIFY( part.m_document->canRedo() ); + + QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), extraSaveAsFlags ) ); + QVERIFY( part.m_document->canRedo() ); + part.m_document->redo(); + QVERIFY( part.m_document->canRedo() ); + + QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), extraSaveAsFlags ) ); + QVERIFY( part.m_document->canRedo() ); + part.m_document->redo(); + QVERIFY( !part.m_document->canRedo() ); + + // Do a last save as so that close url doesn't popup the "You have changes. Do you want to save?" dialog + QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), extraSaveAsFlags ) ); + part.closeUrl(); +} + +void PartTest::testSaveAsUndoStackAnnotations_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; +} + } int main(int argc, char *argv[]) From c14fbe5eb629815c992a82e26428423c5532b96e Mon Sep 17 00:00:00 2001 From: Albert Astals Cid Date: Mon, 30 Oct 2017 11:11:08 +0100 Subject: [PATCH 47/78] Remove SaveAsDontShowWarning It was only used in tests as a way to "hide" warning dialogs, now we have a proper way to close them, this way we make sure that dialogs are should when they should not not when they should not --- autotests/parttest.cpp | 151 ++++++++++++++++++++++++++++++++--------- part.cpp | 4 +- part.h | 3 +- 3 files changed, 122 insertions(+), 36 deletions(-) diff --git a/autotests/parttest.cpp b/autotests/parttest.cpp index 10a5db9b4..ec2c7bedb 100644 --- a/autotests/parttest.cpp +++ b/autotests/parttest.cpp @@ -18,6 +18,8 @@ #include #include +#include +#include #include #include #include @@ -25,6 +27,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 @@ -737,13 +773,7 @@ void PartTest::testSaveAs() QFETCH(bool, nativelySupportsAnnotations); QFETCH(bool, canSwapBackingFile); - // If we expect not to be able to preserve annotations when we write a - // native file, disable the warning otherwise the test will wait for the - // user to confirm. On the other end, if we expect annotations to be - // preserved (and thus no warning), we keep the warning on so that if it - // shows the test timeouts and we can notice that something is wrong. - const Part::SaveAsFlags extraSaveAsFlags = nativelySupportsAnnotations ? - Part::NoSaveAsFlags : Part::SaveAsDontShowWarning; + QScopedPointer closeDialogHelper; QString annotName; QTemporaryFile archiveSave( QString( "%1/okrXXXXXX.okular" ).arg( QDir::tempPath() ) ); @@ -766,8 +796,32 @@ void PartTest::testSaveAs() part.m_document->addPageAnnotation( 0, annot ); annotName = annot->uniqueName(); - QVERIFY( part.saveAs( QUrl::fromLocalFile( archiveSave.fileName() ), extraSaveAsFlags | Part::SaveAsOkularArchive ) ); - QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), extraSaveAsFlags ) ); + 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(); } @@ -780,7 +834,19 @@ void PartTest::testSaveAs() QCOMPARE( part.m_document->page( 0 )->annotations().size(), 1 ); QCOMPARE( part.m_document->page( 0 )->annotations().first()->uniqueName(), annotName ); - QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeFromArchiveFile.fileName() ), extraSaveAsFlags ) ); + 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(); } @@ -831,13 +897,7 @@ void PartTest::testSaveAsUndoStackAnnotations() QFETCH(bool, nativelySupportsAnnotations); QFETCH(bool, canSwapBackingFile); - // If we expect not to be able to preserve annotations when we write a - // native file, disable the warning otherwise the test will wait for the - // user to confirm. On the other end, if we expect annotations to be - // preserved (and thus no warning), we keep the warning on so that if it - // shows the test timeouts and we can notice that something is wrong. - const Part::SaveAsFlags extraSaveAsFlags = nativelySupportsAnnotations ? - Part::NoSaveAsFlags : Part::SaveAsDontShowWarning; + QScopedPointer closeDialogHelper; QTemporaryFile nativeDirectSave( QString( "%1/okrXXXXXX.%2" ).arg( QDir::tempPath() ).arg ( extension ) ); QVERIFY( nativeDirectSave.open() ); nativeDirectSave.close(); @@ -853,7 +913,11 @@ void PartTest::testSaveAsUndoStackAnnotations() part.m_document->addPageAnnotation( 0, annot ); QString annotName = annot->uniqueName(); - QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), extraSaveAsFlags ) ); + 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 ) ); if (!canSwapBackingFile) { // The undo/redo stack gets lost if you can not swap the backing file @@ -867,7 +931,7 @@ void PartTest::testSaveAsUndoStackAnnotations() part.m_document->undo(); QVERIFY( !part.m_document->canUndo() ); - QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), extraSaveAsFlags ) ); + QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), Part::NoSaveAsFlags ) ); QVERIFY( part.m_document->page( 0 )->annotations().isEmpty() ); // Check we can redo the annot add after save @@ -889,14 +953,17 @@ void PartTest::testSaveAsUndoStackAnnotations() QVERIFY( part.m_document->page( 0 )->annotations().isEmpty() ); // Check we can still undo the annot remove after save - QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), extraSaveAsFlags ) ); + QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), Part::NoSaveAsFlags ) ); 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 - QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), extraSaveAsFlags ) ); + 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 ) ); QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); QVERIFY( !part.m_document->canUndo() ); @@ -904,7 +971,7 @@ void PartTest::testSaveAsUndoStackAnnotations() // Redo the add annotation - QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), extraSaveAsFlags ) ); + QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), Part::NoSaveAsFlags ) ); QVERIFY( part.m_document->canRedo() ); part.m_document->redo(); QVERIFY( part.m_document->canUndo() ); @@ -924,50 +991,70 @@ void PartTest::testSaveAsUndoStackAnnotations() part.m_document->modifyPageAnnotationProperties( 0, annot ); // Now check we can still undo/redo/save at all the intermediate states and things still work - QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), extraSaveAsFlags ) ); + 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 ) ); QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); QVERIFY( part.m_document->canUndo() ); - QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), extraSaveAsFlags ) ); + 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 ) ); QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); QVERIFY( part.m_document->canUndo() ); - QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), extraSaveAsFlags ) ); + 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 ) ); QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); QVERIFY( part.m_document->canUndo() ); - QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), extraSaveAsFlags ) ); + 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 ) ); 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( nativeDirectSave.fileName() ), extraSaveAsFlags ) ); + QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), Part::NoSaveAsFlags ) ); QVERIFY( part.m_document->canRedo() ); part.m_document->redo(); QVERIFY( part.m_document->canRedo() ); - QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), extraSaveAsFlags ) ); + 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 ) ); QVERIFY( part.m_document->canRedo() ); part.m_document->redo(); QVERIFY( part.m_document->canRedo() ); - QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), extraSaveAsFlags ) ); + 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 ) ); QVERIFY( part.m_document->canRedo() ); part.m_document->redo(); QVERIFY( part.m_document->canRedo() ); - QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), extraSaveAsFlags ) ); + 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 ) ); QVERIFY( part.m_document->canRedo() ); part.m_document->redo(); QVERIFY( !part.m_document->canRedo() ); - // Do a last save as so that close url doesn't popup the "You have changes. Do you want to save?" dialog - QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), extraSaveAsFlags ) ); + closeDialogHelper.reset(new CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "do you want to save or discard" dialog part.closeUrl(); } diff --git a/part.cpp b/part.cpp index 9d97fa646..12716bf9e 100644 --- a/part.cpp +++ b/part.cpp @@ -2478,7 +2478,7 @@ bool Part::saveAs( const QUrl & saveUrl, SaveAsFlags flags ) // Does the user want a .okular archive? if ( flags & SaveAsOkularArchive ) { - if ( !hasUserAcceptedReload && !m_document->canSwapBackingFile() && !( flags & SaveAsDontShowWarning ) ) + 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?" ), @@ -2511,7 +2511,7 @@ bool Part::saveAs( const QUrl & saveUrl, SaveAsFlags flags ) QStringList listOfwontSaves; if ( wontSaveForms ) listOfwontSaves << i18n( "Filled form contents" ); if ( wontSaveAnnotations ) listOfwontSaves << i18n( "User annotations" ); - if ( !listOfwontSaves.isEmpty() && !( flags & SaveAsDontShowWarning ) ) + if ( !listOfwontSaves.isEmpty() ) { const QString warningMessage = m_document->canSwapBackingFile() ? i18n( "The following elements cannot be saved in this format.
If you want to preserve them, please use the Okular document archive format." ) : diff --git a/part.h b/part.h index 4fb8ab40a..a8d84bf14 100644 --- a/part.h +++ b/part.h @@ -271,8 +271,7 @@ class OKULARPART_EXPORT Part : public KParts::ReadWritePart, public Okular::Docu enum SaveAsFlag { NoSaveAsFlags = 0, ///< No options - SaveAsOkularArchive = 1, ///< Save as Okular Archive (.okular) instead of document's native format - SaveAsDontShowWarning = 2, ///< Don't show warning for unsupported save features + SaveAsOkularArchive = 1 ///< Save as Okular Archive (.okular) instead of document's native format }; Q_DECLARE_FLAGS( SaveAsFlags, SaveAsFlag ) From 946d54fb3ef0a9ef83116b18d6656a6c25214b10 Mon Sep 17 00:00:00 2001 From: Albert Astals Cid Date: Mon, 30 Oct 2017 16:03:15 +0100 Subject: [PATCH 48/78] Add saveas+undo/redo for forms --- autotests/parttest.cpp | 77 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/autotests/parttest.cpp b/autotests/parttest.cpp index ec2c7bedb..eab1a7a52 100644 --- a/autotests/parttest.cpp +++ b/autotests/parttest.cpp @@ -10,6 +10,7 @@ #include #include "../core/annotations.h" +#include "../core/form.h" #include "../core/page.h" #include "../part.h" #include "../ui/toc.h" @@ -86,6 +87,7 @@ class PartTest void testSaveAs_data(); void testSaveAsUndoStackAnnotations(); void testSaveAsUndoStackAnnotations_data(); + void testSaveAsUndoStackForms(); void testMouseMoveOverLinkWhileInSelectionMode(); void testClickUrlLinkWhileInSelectionMode(); void testeTextSelectionOverAndAcrossLinks_data(); @@ -1070,6 +1072,81 @@ void PartTest::testSaveAsUndoStackAnnotations_data() QTest::newRow("jpg") << KDESRCDIR "data/potato.jpg" << "jpg" << false << true; } +void PartTest::testSaveAsUndoStackForms() +{ + QTemporaryFile nativeDirectSave( QString( "%1/okrXXXXXX.pdf" ).arg( QDir::tempPath() ) ); + QVERIFY( nativeDirectSave.open() ); nativeDirectSave.close(); + + Okular::Part part(nullptr, nullptr, QVariantList()); + part.openDocument( KDESRCDIR "data/formSamples.pdf" ); + + 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( nativeDirectSave.fileName() ), Part::NoSaveAsFlags ) ); + + QVERIFY( part.m_document->canUndo() ); + part.m_document->undo(); + QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), Part::NoSaveAsFlags ) ); + + QVERIFY( part.m_document->canUndo() ); + part.m_document->undo(); + QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), Part::NoSaveAsFlags ) ); + + QVERIFY( part.m_document->canUndo() ); + part.m_document->undo(); + QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), Part::NoSaveAsFlags ) ); + + QVERIFY( part.m_document->canUndo() ); + part.m_document->undo(); + QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), Part::NoSaveAsFlags ) ); + QVERIFY( !part.m_document->canUndo() ); + + QVERIFY( part.m_document->canRedo() ); + part.m_document->redo(); + QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), Part::NoSaveAsFlags ) ); + + QVERIFY( part.m_document->canRedo() ); + part.m_document->redo(); + QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), Part::NoSaveAsFlags ) ); + + QVERIFY( part.m_document->canRedo() ); + part.m_document->redo(); + QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), Part::NoSaveAsFlags ) ); + + QVERIFY( part.m_document->canRedo() ); + part.m_document->redo(); + QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), Part::NoSaveAsFlags ) ); +} + } int main(int argc, char *argv[]) From cd24ad31c82583be4facbc6386f6277e2acbb90a Mon Sep 17 00:00:00 2001 From: Albert Astals Cid Date: Mon, 30 Oct 2017 17:01:46 +0100 Subject: [PATCH 49/78] Implement missing todo in Document::swapBackingFileArchive And extend tests to include archive files --- autotests/parttest.cpp | 101 +++++++++++++++++++++++++---------------- core/document.cpp | 68 ++++++++++++++++++++++++++- 2 files changed, 128 insertions(+), 41 deletions(-) diff --git a/autotests/parttest.cpp b/autotests/parttest.cpp index eab1a7a52..16a0de382 100644 --- a/autotests/parttest.cpp +++ b/autotests/parttest.cpp @@ -88,6 +88,7 @@ class PartTest void testSaveAsUndoStackAnnotations(); void testSaveAsUndoStackAnnotations_data(); void testSaveAsUndoStackForms(); + void testSaveAsUndoStackForms_data(); void testMouseMoveOverLinkWhileInSelectionMode(); void testClickUrlLinkWhileInSelectionMode(); void testeTextSelectionOverAndAcrossLinks_data(); @@ -898,11 +899,14 @@ void PartTest::testSaveAsUndoStackAnnotations() QFETCH(QString, extension); QFETCH(bool, nativelySupportsAnnotations); QFETCH(bool, canSwapBackingFile); + QFETCH(bool, saveToArchive); + + const Part::SaveAsFlag saveFlags = saveToArchive ? Part::SaveAsOkularArchive : Part::NoSaveAsFlags; QScopedPointer closeDialogHelper; - QTemporaryFile nativeDirectSave( QString( "%1/okrXXXXXX.%2" ).arg( QDir::tempPath() ).arg ( extension ) ); - QVERIFY( nativeDirectSave.open() ); nativeDirectSave.close(); + 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 ); @@ -915,11 +919,11 @@ void PartTest::testSaveAsUndoStackAnnotations() part.m_document->addPageAnnotation( 0, annot ); QString annotName = annot->uniqueName(); - if (!nativelySupportsAnnotations) { + 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( nativeDirectSave.fileName() ), Part::NoSaveAsFlags ) ); + QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) ); if (!canSwapBackingFile) { // The undo/redo stack gets lost if you can not swap the backing file @@ -933,7 +937,7 @@ void PartTest::testSaveAsUndoStackAnnotations() part.m_document->undo(); QVERIFY( !part.m_document->canUndo() ); - QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), Part::NoSaveAsFlags ) ); + 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 @@ -955,17 +959,17 @@ void PartTest::testSaveAsUndoStackAnnotations() QVERIFY( part.m_document->page( 0 )->annotations().isEmpty() ); // Check we can still undo the annot remove after save - QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), Part::NoSaveAsFlags ) ); + 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) { + 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( nativeDirectSave.fileName() ), Part::NoSaveAsFlags ) ); + QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); QVERIFY( !part.m_document->canUndo() ); @@ -973,7 +977,7 @@ void PartTest::testSaveAsUndoStackAnnotations() // Redo the add annotation - QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), Part::NoSaveAsFlags ) ); + QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canRedo() ); part.m_document->redo(); QVERIFY( part.m_document->canUndo() ); @@ -993,65 +997,65 @@ void PartTest::testSaveAsUndoStackAnnotations() 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) { + 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( nativeDirectSave.fileName() ), Part::NoSaveAsFlags ) ); + QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); QVERIFY( part.m_document->canUndo() ); - if (!nativelySupportsAnnotations) { + 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( nativeDirectSave.fileName() ), Part::NoSaveAsFlags ) ); + QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); QVERIFY( part.m_document->canUndo() ); - if (!nativelySupportsAnnotations) { + 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( nativeDirectSave.fileName() ), Part::NoSaveAsFlags ) ); + QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); QVERIFY( part.m_document->canUndo() ); - if (!nativelySupportsAnnotations) { + 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( nativeDirectSave.fileName() ), Part::NoSaveAsFlags ) ); + 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( nativeDirectSave.fileName() ), Part::NoSaveAsFlags ) ); + QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canRedo() ); part.m_document->redo(); QVERIFY( part.m_document->canRedo() ); - if (!nativelySupportsAnnotations) { + 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( nativeDirectSave.fileName() ), Part::NoSaveAsFlags ) ); + QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canRedo() ); part.m_document->redo(); QVERIFY( part.m_document->canRedo() ); - if (!nativelySupportsAnnotations) { + 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( nativeDirectSave.fileName() ), Part::NoSaveAsFlags ) ); + QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canRedo() ); part.m_document->redo(); QVERIFY( part.m_document->canRedo() ); - if (!nativelySupportsAnnotations) { + 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( nativeDirectSave.fileName() ), Part::NoSaveAsFlags ) ); + QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canRedo() ); part.m_document->redo(); QVERIFY( !part.m_document->canRedo() ); @@ -1066,19 +1070,28 @@ void PartTest::testSaveAsUndoStackAnnotations_data() QTest::addColumn("extension"); QTest::addColumn("nativelySupportsAnnotations"); QTest::addColumn("canSwapBackingFile"); + QTest::addColumn("saveToArchive"); - 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; + 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() { - QTemporaryFile nativeDirectSave( QString( "%1/okrXXXXXX.pdf" ).arg( QDir::tempPath() ) ); - QVERIFY( nativeDirectSave.open() ); nativeDirectSave.close(); + 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( KDESRCDIR "data/formSamples.pdf" ); + part.openDocument( file ); for ( FormField *ff : part.m_document->page( 0 )->formFields() ) { @@ -1111,40 +1124,50 @@ void PartTest::testSaveAsUndoStackForms() } } - QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), Part::NoSaveAsFlags ) ); + QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); - QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), Part::NoSaveAsFlags ) ); + QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); - QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), Part::NoSaveAsFlags ) ); + QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); - QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), Part::NoSaveAsFlags ) ); + QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); - QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), Part::NoSaveAsFlags ) ); + 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( nativeDirectSave.fileName() ), Part::NoSaveAsFlags ) ); + QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canRedo() ); part.m_document->redo(); - QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), Part::NoSaveAsFlags ) ); + QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canRedo() ); part.m_document->redo(); - QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), Part::NoSaveAsFlags ) ); + QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canRedo() ); part.m_document->redo(); - QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), Part::NoSaveAsFlags ) ); + 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; } } diff --git a/core/document.cpp b/core/document.cpp index 19293140a..cbc067f37 100644 --- a/core/document.cpp +++ b/core/document.cpp @@ -4466,9 +4466,57 @@ bool Document::swapBackingFileArchive( const QString &newFileName, const QUrl & qCDebug(OkularCoreDebug) << "Swapping backing file to" << tempFileName; QVector< Page * > newPagesVector; - if (genIt->generator->swapBackingFile( tempFileName, newPagesVector )) + Generator::SwapBackingFileResult result = genIt->generator->swapBackingFile( tempFileName, newPagesVector ); + if (result != Generator::SwapBackingFileError) { - // TODO Do the same we do in the other swapBackingFile call + 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 )) ouc->refreshInternalPageReferences( newPagesVector ); + else + { + qWarning() << "Unhandled undo command" << uc; + } + } + + 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 ); + } delete d->m_archiveData; d->m_archiveData = newArchive; @@ -4476,7 +4524,23 @@ bool Document::swapBackingFileArchive( const QString &newFileName, const QUrl & d->m_docFileName = tempFileName; 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 From 30297bc477960b7a94e7777dda4049d6b2703fc3 Mon Sep 17 00:00:00 2001 From: Albert Astals Cid Date: Fri, 3 Nov 2017 16:39:53 +0100 Subject: [PATCH 50/78] Adapt the manual a bit about the new save options And also remove some "you can do this since this ancient version" which doesn't really make much sense. --- doc/index.docbook | 40 +++++++++++++--------------------------- 1 file changed, 13 insertions(+), 27 deletions(-) diff --git a/doc/index.docbook b/doc/index.docbook index 0d01a9f4f..74140b402 100644 --- a/doc/index.docbook +++ b/doc/index.docbook @@ -438,9 +438,6 @@ Context menu actions like Rename Bookmarks etc.) 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 @@ Context menu actions like Rename Bookmarks etc.) 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 @@ Context menu actions like Rename Bookmarks etc.) - 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 @@ -999,31 +985,31 @@ Context menu actions like Rename Bookmarks etc.) &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;). + + From fcbe97bdc0309f577404bba675faff22fd9c6d75 Mon Sep 17 00:00:00 2001 From: Albert Astals Cid Date: Mon, 6 Nov 2017 05:57:20 +0100 Subject: [PATCH 51/78] Fix leaks in documenttest --- autotests/documenttest.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/autotests/documenttest.cpp b/autotests/documenttest.cpp index 5e23dd6ef..4c79b2b38 100644 --- a/autotests/documenttest.cpp +++ b/autotests/documenttest.cpp @@ -61,6 +61,8 @@ void DocumentTest::testCloseDuringRotationJob() 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 @@ -110,6 +112,8 @@ void DocumentTest::testDocdataMigration() QCOMPARE( m_document->page( 0 )->annotations().size(), 0 ); QCOMPARE( m_document->isDocdataMigrationNeeded(), false ); m_document->closeDocument(); + + delete m_document; } QTEST_MAIN( DocumentTest ) From ae4f0671bff02d717201f7bf9510e50749529020 Mon Sep 17 00:00:00 2001 From: Albert Astals Cid Date: Mon, 6 Nov 2017 06:05:22 +0100 Subject: [PATCH 52/78] Unsplit some lines in tests --- autotests/parttest.cpp | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/autotests/parttest.cpp b/autotests/parttest.cpp index 16a0de382..ac45978ab 100644 --- a/autotests/parttest.cpp +++ b/autotests/parttest.cpp @@ -782,9 +782,12 @@ void PartTest::testSaveAs() 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(); + 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"; { @@ -906,7 +909,8 @@ void PartTest::testSaveAsUndoStackAnnotations() QScopedPointer closeDialogHelper; QTemporaryFile saveFile( QString( "%1/okrXXXXXX.%2" ).arg( QDir::tempPath() ).arg ( extension ) ); - QVERIFY( saveFile.open() ); saveFile.close(); + QVERIFY( saveFile.open() ); + saveFile.close(); Okular::Part part(nullptr, nullptr, QVariantList()); part.openDocument( file ); @@ -1088,7 +1092,8 @@ void PartTest::testSaveAsUndoStackForms() const Part::SaveAsFlag saveFlags = saveToArchive ? Part::SaveAsOkularArchive : Part::NoSaveAsFlags; QTemporaryFile saveFile( QString( "%1/okrXXXXXX.%2" ).arg( QDir::tempPath(), extension ) ); - QVERIFY( saveFile.open() ); saveFile.close(); + QVERIFY( saveFile.open() ); + saveFile.close(); Okular::Part part(nullptr, nullptr, QVariantList()); part.openDocument( file ); From 099d3cfcc8f9ca8c3e5feb52172a2941d56bd64d Mon Sep 17 00:00:00 2001 From: Albert Astals Cid Date: Mon, 6 Nov 2017 06:10:13 +0100 Subject: [PATCH 53/78] Update @since to 1.3 Let's see if we can squeeze it in --- core/document.h | 12 ++++++------ core/generator.h | 2 +- core/observer.h | 2 +- core/page.h | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/core/document.h b/core/document.h index b81a2b86c..9d44dd1ad 100644 --- a/core/document.h +++ b/core/document.h @@ -741,7 +741,7 @@ class OKULARCORE_EXPORT Document : public QObject * Returns whether the generator supports hot-swapping the current file * with another identical file * - * @since 0.20 (KDE 4.14) + * @since 1.3 */ bool canSwapBackingFile() const; @@ -755,7 +755,7 @@ class OKULARCORE_EXPORT Document : public QObject * saveChanges first to write changes to a file and then swapBackingFile * to switch to the new location. * - * @since 0.20 (KDE 4.14) + * @since 1.3 */ bool swapBackingFile( const QString &newFileName, const QUrl & url ); @@ -768,7 +768,7 @@ class OKULARCORE_EXPORT Document : public QObject * saveDocumentArchive first to write changes to a file and then * swapBackingFileArchive to switch to the new location. * - * @since 0.20 (KDE 4.14) + * @since 1.3 */ bool swapBackingFileArchive( const QString &newFileName, const QUrl & url ); @@ -862,7 +862,7 @@ class OKULARCORE_EXPORT Document : public QObject * * @warning This function only works if the current file is a document archive * - * @since 0.14 (KDE 4.20) + * @since 1.3 */ bool extractArchivedFile( const QString &destFileName ); @@ -907,7 +907,7 @@ class OKULARCORE_EXPORT Document : public QObject * okular versions did by default). * If this flag is set, then annotations and forms cannot be edited. * - * @since 0.21 + * @since 1.3 */ bool isDocdataMigrationNeeded() const; @@ -916,7 +916,7 @@ class OKULARCORE_EXPORT Document : public QObject * isDocdataMigrationNeeded() was true and you've just saved them to an * external file. * - * @since 0.21 + * @since 1.3 */ void docdataMigrationDone(); diff --git a/core/generator.h b/core/generator.h index 2d3edfcbf..b15f47808 100644 --- a/core/generator.h +++ b/core/generator.h @@ -209,7 +209,7 @@ class OKULARCORE_EXPORT Generator : public QObject 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) - SwapBackingFile ///< Whether the Generator can hot-swap the file it's reading from @since 0.20 (KDE 4.14) + SwapBackingFile ///< Whether the Generator can hot-swap the file it's reading from @since 1.3 }; /** diff --git a/core/observer.h b/core/observer.h index 2bec2444b..ed65e3b1f 100644 --- a/core/observer.h +++ b/core/observer.h @@ -54,7 +54,7 @@ class OKULARCORE_EXPORT DocumentObserver enum SetupFlags { DocumentChanged = 1, ///< The document is a new document. NewLayoutForPages = 2, ///< All the pages have - UrlChanged = 4 ///< The URL has changed @since 0.20 (KDE 4.14) + UrlChanged = 4 ///< The URL has changed @since 1.3 }; /** diff --git a/core/page.h b/core/page.h index 62c005fdd..21f0687ce 100644 --- a/core/page.h +++ b/core/page.h @@ -251,7 +251,7 @@ class OKULARCORE_EXPORT Page /** * Returns the annotation with the given unique name. - * @since TODO + * @since 1.3 */ Annotation * annotation( const QString & uniqueName ) const; From d2d7a880b18a7d29827bb4306f79257147c9df9e Mon Sep 17 00:00:00 2001 From: Albert Astals Cid Date: Mon, 6 Nov 2017 06:12:03 +0100 Subject: [PATCH 54/78] Minor style adjustment --- core/document.cpp | 4 ++-- core/document.h | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/document.cpp b/core/document.cpp index cbc067f37..11945bbd1 100644 --- a/core/document.cpp +++ b/core/document.cpp @@ -4343,7 +4343,7 @@ bool Document::canSwapBackingFile() const return genIt->generator->hasFeature( Generator::SwapBackingFile ); } -bool Document::swapBackingFile( const QString &newFileName, const QUrl & url ) +bool Document::swapBackingFile( const QString &newFileName, const QUrl &url ) { if ( !d->m_generator ) return false; @@ -4441,7 +4441,7 @@ bool Document::swapBackingFile( const QString &newFileName, const QUrl & url ) } } -bool Document::swapBackingFileArchive( const QString &newFileName, const QUrl & url ) +bool Document::swapBackingFileArchive( const QString &newFileName, const QUrl &url ) { if ( !d->m_generator ) return false; diff --git a/core/document.h b/core/document.h index 9d44dd1ad..4d5fff179 100644 --- a/core/document.h +++ b/core/document.h @@ -757,7 +757,7 @@ class OKULARCORE_EXPORT Document : public QObject * * @since 1.3 */ - bool swapBackingFile( const QString &newFileName, const QUrl & url ); + bool swapBackingFile( const QString &newFileName, const QUrl &url ); /** * Same as swapBackingFile, but newFileName must be a .okular file. @@ -770,7 +770,7 @@ class OKULARCORE_EXPORT Document : public QObject * * @since 1.3 */ - bool swapBackingFileArchive( const QString &newFileName, const QUrl & url ); + bool swapBackingFileArchive( const QString &newFileName, const QUrl &url ); /** * Saving capabilities. Their availability varies according to the From 9d450161b26853b453cb83544bd7741a7255bd9b Mon Sep 17 00:00:00 2001 From: Albert Astals Cid Date: Mon, 6 Nov 2017 09:57:35 +0100 Subject: [PATCH 55/78] Use new style connects --- part.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/part.cpp b/part.cpp index 12716bf9e..e6b7ce170 100644 --- a/part.cpp +++ b/part.cpp @@ -563,7 +563,7 @@ m_cliPresentation(false), m_cliPrint(false), m_embedMode(detectEmbedMode(parentW connect( m_watcher, &KDirWatch::deleted, this, &Part::slotFileDirty ); m_dirtyHandler = new QTimer( this ); m_dirtyHandler->setSingleShot( true ); - connect( m_dirtyHandler, SIGNAL(timeout()),this, SLOT(slotAttemptReload()) ); + connect( m_dirtyHandler, &QTimer::timeout, this, [this] { slotAttemptReload(); } ); slotNewConfig(); @@ -814,7 +814,7 @@ void Part::setupActions() m_selectAll = KStandardAction::selectAll( m_pageView, SLOT(selectAll()), ac ); - m_save = KStandardAction::save( this, SLOT(saveFile()), ac ); + m_save = KStandardAction::save( this, [this] { saveFile(); }, ac ); m_save->setEnabled( false ); m_saveAs = KStandardAction::saveAs( this, SLOT(slotSaveFileAs()), ac ); From 433d1eac23599c1bc2c9fc5cc9c6b0189c1bbd22 Mon Sep 17 00:00:00 2001 From: Albert Astals Cid Date: Mon, 6 Nov 2017 09:59:01 +0100 Subject: [PATCH 56/78] Initialize copyJob just in case --- part.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/part.cpp b/part.cpp index e6b7ce170..93ffadb32 100644 --- a/part.cpp +++ b/part.cpp @@ -2473,7 +2473,7 @@ bool Part::saveAs( const QUrl & saveUrl, SaveAsFlags flags ) tf.close(); QScopedPointer tempFile; - KIO::Job *copyJob; // this will be filled with the job that writes to saveUrl + KIO::Job *copyJob = nullptr; // this will be filled with the job that writes to saveUrl // Does the user want a .okular archive? if ( flags & SaveAsOkularArchive ) From 3c99a280f3af0dfa4cfc71a44755a90db81f4bf8 Mon Sep 17 00:00:00 2001 From: Albert Astals Cid Date: Mon, 13 Nov 2017 10:10:46 +0100 Subject: [PATCH 57/78] Add the file url to the save text Makes more clear we're going to be saving over it --- part.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/part.cpp b/part.cpp index 93ffadb32..25c6d5e68 100644 --- a/part.cpp +++ b/part.cpp @@ -1700,14 +1700,14 @@ bool Part::queryClose() return true; const int res = KMessageBox::warningYesNoCancel( widget(), - i18n( "Do you want to save your changes or discard them?" ), + i18n( "Do you want to save your changes to \"%1\" or discard them?", url().toDisplayString() ), i18n( "Close Document" ), KStandardGuiItem::save(), KStandardGuiItem::discard() ); switch ( res ) { - case KMessageBox::Yes: // Save as + case KMessageBox::Yes: // Save saveFile(); return !isModified(); // Only allow closing if file was really saved case KMessageBox::No: // Discard From 3e423eeb7cb06711e9c14c4a15ed993fa1d9d00f Mon Sep 17 00:00:00 2001 From: Albert Astals Cid Date: Mon, 13 Nov 2017 10:16:25 +0100 Subject: [PATCH 58/78] Add missing test file --- autotests/data/potato.jpg | Bin 0 -> 34999 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 autotests/data/potato.jpg diff --git a/autotests/data/potato.jpg b/autotests/data/potato.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b7dd4d1887811af29ae05dfec555de042ba55813 GIT binary patch literal 34999 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 Row3mKQ_#vg^V0v{1ON?gNAdsw literal 0 HcmV?d00001 From 210a6ced5a02856d1f25a58e6ac680a0e862a676 Mon Sep 17 00:00:00 2001 From: Albert Astals Cid Date: Mon, 13 Nov 2017 10:37:35 +0100 Subject: [PATCH 59/78] Tweak saveAs + file with password text --- part.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/part.cpp b/part.cpp index 25c6d5e68..6b48dc01a 100644 --- a/part.cpp +++ b/part.cpp @@ -2446,7 +2446,7 @@ bool Part::saveAs( const QUrl & saveUrl, SaveAsFlags flags ) if ( m_documentOpenWithPassword ) { const int res = KMessageBox::warningYesNo( widget(), - i18n( "The current document has password.
When saving we need to reload the file so you will get the password asked again and the undo/redo stack will be lost.
Do you want to continue?" ), + 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 ) From 055f2db76d58b559b8a215cd5713cdbd7265fddd Mon Sep 17 00:00:00 2001 From: Albert Astals Cid Date: Mon, 13 Nov 2017 14:39:50 +0100 Subject: [PATCH 60/78] Set back the autogenerated annotation unique name on saving The the original unique name was empty. Otherwise we can't find the annotation after save/reload because the annotation name is still empty and a new random id is assigned --- generators/poppler/generator_pdf.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/generators/poppler/generator_pdf.cpp b/generators/poppler/generator_pdf.cpp index 2cfbd895a..ff13fbf3f 100644 --- a/generators/poppler/generator_pdf.cpp +++ b/generators/poppler/generator_pdf.cpp @@ -1717,6 +1717,18 @@ bool PDFGenerator::save( const QString &fileName, SaveOptions options, QString * 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) { From de948c4d7187f551f4cd6c77ae0dc391ad9e8900 Mon Sep 17 00:00:00 2001 From: Albert Astals Cid Date: Mon, 13 Nov 2017 14:49:08 +0100 Subject: [PATCH 61/78] We need to increase the part.rc version number Since we added the "Save" menu option --- part.rc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/part.rc b/part.rc index 4a736c6d4..4eba35d00 100644 --- a/part.rc +++ b/part.rc @@ -1,5 +1,5 @@ - + &File From 34f40b2c6f301a8eab1ea244aab5d281a0384a9b Mon Sep 17 00:00:00 2001 From: Albert Astals Cid Date: Mon, 13 Nov 2017 15:35:06 +0100 Subject: [PATCH 62/78] Tweak migrationMessage text a bit --- part.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/part.cpp b/part.cpp index 6b48dc01a..b4d782c9a 100644 --- a/part.cpp +++ b/part.cpp @@ -469,7 +469,7 @@ m_cliPresentation(false), m_cliPrint(false), m_embedMode(detectEmbedMode(parentW 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 and continue editing the document." ) ); + 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 ); From eea5127e731ddd48c706f940dd4b88929df2895f Mon Sep 17 00:00:00 2001 From: Albert Astals Cid Date: Mon, 13 Nov 2017 17:30:10 +0100 Subject: [PATCH 63/78] We also support opening Okular archive files --- core/document.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/document.cpp b/core/document.cpp index 11945bbd1..81e82868d 100644 --- a/core/document.cpp +++ b/core/document.cpp @@ -4322,6 +4322,9 @@ QStringList Document::supportedMimeTypes() const 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); From ea1815be136ca32d0bde3a9bd98d630edec1b511 Mon Sep 17 00:00:00 2001 From: Albert Astals Cid Date: Mon, 13 Nov 2017 17:59:13 +0100 Subject: [PATCH 64/78] Also note the modified status (*) on the tab name --- shell/shell.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/shell/shell.cpp b/shell/shell.cpp index 5787ebf18..52d5fc367 100644 --- a/shell/shell.cpp +++ b/shell/shell.cpp @@ -503,8 +503,15 @@ void Shell::setCaption( const QString &caption ) if ( activeTab >= 0 && activeTab < m_tabs.size() ) { KParts::ReadWritePart* const activePart = m_tabs[activeTab].part; - if ( activePart->isModified() ) + QString tabCaption = activePart->url().fileName(); + if ( activePart->isModified() ) { modified = true; + if ( !tabCaption.isEmpty() ) { + tabCaption.append( QStringLiteral( " *" ) ); + } + } + + m_tabWidget->setTabText( activeTab, tabCaption ); } setCaption( caption, modified ); From bd724e49449dba1b90f246f551aece0a3245ee5b Mon Sep 17 00:00:00 2001 From: Albert Astals Cid Date: Mon, 13 Nov 2017 18:12:34 +0100 Subject: [PATCH 65/78] Get the okular archive mime translation name from kcoreaddons Instead of asking the translators to translate it again --- part.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/part.cpp b/part.cpp index b4d782c9a..14ba0212f 100644 --- a/part.cpp +++ b/part.cpp @@ -2412,9 +2412,11 @@ bool Part::slotSaveFileAs( bool showOkularArchiveAsDefaultFormat ) bool wontSaveForms, wontSaveAnnotations; checkNativeSaveDataLoss(&wontSaveForms, &wontSaveAnnotations); + const QMimeType okularArchiveMimeType = db.mimeTypeForName( QStringLiteral("application/vnd.kde.okular-archive") ); + // Prepare "Save As" dialog const QString originalMimeTypeFilter = i18nc("File type name and pattern", "%1 (%2)", originalMimeType.comment(), originalMimeType.globPatterns().join(QLatin1Char(' '))); - const QString okularArchiveMimeTypeFilter = i18n("Okular Archive (*.okular)"); + 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 || From 1ed2522b530cdd492fc6c1e2db710e9e4a360a6f Mon Sep 17 00:00:00 2001 From: Albert Astals Cid Date: Mon, 13 Nov 2017 23:23:32 +0100 Subject: [PATCH 66/78] tweak can't save in this format warning message with suggestion from Burkhard --- part.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/part.cpp b/part.cpp index 14ba0212f..2f34d4a46 100644 --- a/part.cpp +++ b/part.cpp @@ -2516,8 +2516,8 @@ bool Part::saveAs( const QUrl & saveUrl, SaveAsFlags flags ) if ( !listOfwontSaves.isEmpty() ) { const QString warningMessage = m_document->canSwapBackingFile() ? - i18n( "The following elements cannot be saved in this format.
If you want to preserve them, please use the Okular document archive format." ) : - i18n( "The following elements cannot be saved in this format and will be lost (as well as the undo/redo stack).
If you want to preserve them, please use the Okular document archive format." ); + 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" ); From 302c38672f3e50130adf583288c13ad5401e3472 Mon Sep 17 00:00:00 2001 From: Albert Astals Cid Date: Tue, 14 Nov 2017 09:49:32 +0100 Subject: [PATCH 67/78] Make Document::swapBackingFileArchive use swapBackingFile --- core/document.cpp | 97 +++-------------------------------------------- 1 file changed, 5 insertions(+), 92 deletions(-) diff --git a/core/document.cpp b/core/document.cpp index 81e82868d..3517f018e 100644 --- a/core/document.cpp +++ b/core/document.cpp @@ -4446,19 +4446,6 @@ bool Document::swapBackingFile( const QString &newFileName, const QUrl &url ) bool Document::swapBackingFileArchive( 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 archive to" << newFileName; ArchiveData *newArchive = DocumentPrivate::unpackDocumentArchive( newFileName ); @@ -4467,89 +4454,15 @@ bool Document::swapBackingFileArchive( const QString &newFileName, const QUrl &u const QString tempFileName = newArchive->document.fileName(); - qCDebug(OkularCoreDebug) << "Swapping backing file to" << tempFileName; - QVector< Page * > newPagesVector; - Generator::SwapBackingFileResult result = genIt->generator->swapBackingFile( tempFileName, newPagesVector ); - if (result != Generator::SwapBackingFileError) + const bool success = swapBackingFile( tempFileName, url ); + + if ( success ) { - 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 )) ouc->refreshInternalPageReferences( newPagesVector ); - else - { - qWarning() << "Unhandled undo command" << uc; - } - } - - 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 ); - } - delete d->m_archiveData; d->m_archiveData = newArchive; - d->m_url = url; - d->m_docFileName = tempFileName; - 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; } + + return success; } bool Document::canSaveChanges() const From 8e80a4f57048ef095229a87b669ce84b56f9f995 Mon Sep 17 00:00:00 2001 From: Albert Astals Cid Date: Tue, 14 Nov 2017 09:57:38 +0100 Subject: [PATCH 68/78] Account for Okular::PagePrivate::findEquivalentForm failing It should never fail but it's better if we have the backup for not crashing at least if it does --- core/document.cpp | 13 ++++++++++-- core/documentcommands.cpp | 42 +++++++++++++++++++++++++++++---------- core/documentcommands_p.h | 22 ++++++++++---------- 3 files changed, 54 insertions(+), 23 deletions(-) diff --git a/core/document.cpp b/core/document.cpp index 3517f018e..c3ccf7f3b 100644 --- a/core/document.cpp +++ b/core/document.cpp @@ -4385,10 +4385,19 @@ bool Document::swapBackingFile( const QString &newFileName, const QUrl &url ) { // Trust me on the const_cast ^_^ QUndoCommand *uc = const_cast( d->m_undoStack->command( i ) ); - if (OkularUndoCommand *ouc = dynamic_cast( uc )) ouc->refreshInternalPageReferences( newPagesVector ); + if (OkularUndoCommand *ouc = dynamic_cast( uc )) + { + const bool success = ouc->refreshInternalPageReferences( newPagesVector ); + if ( !success ) + { + qWarning() << "Document::swapBackingFile: refreshInternalPageReferences failed" << ouc; + return false; + } + } else { - qWarning() << "Unhandled undo command" << uc; + qWarning() << "Document::swapBackingFile: Unhandled undo command" << uc; + return false; } } diff --git a/core/documentcommands.cpp b/core/documentcommands.cpp index 4f9c5e8a1..58804a7c7 100644 --- a/core/documentcommands.cpp +++ b/core/documentcommands.cpp @@ -88,7 +88,7 @@ void AddAnnotationCommand::redo() m_done = true; } -void AddAnnotationCommand::refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) +bool AddAnnotationCommand::refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) { if ( m_done ) { @@ -99,6 +99,8 @@ void AddAnnotationCommand::refreshInternalPageReferences( const QVector< Okular: auto a = newPagesVector[m_pageNumber]->annotation( m_annotation->uniqueName() ); if (a) m_annotation = a; } + + return true; } @@ -133,7 +135,7 @@ void RemoveAnnotationCommand::redo() m_done = true; } -void RemoveAnnotationCommand::refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) +bool RemoveAnnotationCommand::refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) { if ( !m_done ) { @@ -144,6 +146,8 @@ void RemoveAnnotationCommand::refreshInternalPageReferences( const QVector< Okul auto a = newPagesVector[m_pageNumber]->annotation( m_annotation->uniqueName() ); if (a) m_annotation = a; } + + return true; } ModifyAnnotationPropertiesCommand::ModifyAnnotationPropertiesCommand( DocumentPrivate* docPriv, @@ -174,11 +178,13 @@ void ModifyAnnotationPropertiesCommand::redo() m_docPriv->performModifyPageAnnotation( m_pageNumber, m_annotation, true ); } -void ModifyAnnotationPropertiesCommand::refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) +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; } @@ -247,11 +253,13 @@ Okular::NormalizedRect TranslateAnnotationCommand::translateBoundingRectangle( c return boundingRect; } -void TranslateAnnotationCommand::refreshInternalPageReferences( const QVector< Page * > &newPagesVector ) +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; } @@ -320,11 +328,13 @@ Okular::NormalizedRect AdjustAnnotationCommand::adjustBoundingRectangle( return Okular::NormalizedRect( left, top, right, bottom ); } -void AdjustAnnotationCommand::refreshInternalPageReferences( const QVector< Page * > &newPagesVector ) +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; } @@ -464,10 +474,12 @@ bool EditAnnotationContentsCommand::mergeWith(const QUndoCommand* uc) } } -void EditAnnotationContentsCommand::refreshInternalPageReferences( const QVector< Page * > &newPagesVector ) +bool EditAnnotationContentsCommand::refreshInternalPageReferences( const QVector< Page * > &newPagesVector ) { auto a = newPagesVector[m_pageNumber]->annotation( m_annotation->uniqueName() ); if (a) m_annotation = a; + + return true; } @@ -522,9 +534,11 @@ bool EditFormTextCommand::mergeWith(const QUndoCommand* uc) } } -void EditFormTextCommand::refreshInternalPageReferences( const QVector< Page * > &newPagesVector ) +bool EditFormTextCommand::refreshInternalPageReferences( const QVector< Page * > &newPagesVector ) { m_form = dynamic_cast(Okular::PagePrivate::findEquivalentForm( newPagesVector[m_pageNumber], m_form )); + + return m_form; } @@ -558,9 +572,11 @@ void EditFormListCommand::redo() m_docPriv->notifyFormChanges( m_pageNumber ); } -void EditFormListCommand::refreshInternalPageReferences( const QVector< Page * > &newPagesVector ) +bool EditFormListCommand::refreshInternalPageReferences( const QVector< Page * > &newPagesVector ) { m_form = dynamic_cast(Okular::PagePrivate::findEquivalentForm( newPagesVector[m_pageNumber], m_form )); + + return m_form; } @@ -650,9 +666,11 @@ bool EditFormComboCommand::mergeWith( const QUndoCommand *uc ) } } -void EditFormComboCommand::refreshInternalPageReferences( const QVector< Page * > &newPagesVector ) +bool EditFormComboCommand::refreshInternalPageReferences( const QVector< Page * > &newPagesVector ) { m_form = dynamic_cast(Okular::PagePrivate::findEquivalentForm( newPagesVector[m_pageNumber], m_form )); + + return m_form; } @@ -705,15 +723,19 @@ void EditFormButtonsCommand::redo() m_docPriv->notifyFormChanges( m_pageNumber ); } -void EditFormButtonsCommand::refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) +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 index 44b7c19f6..055d35c7e 100644 --- a/core/documentcommands_p.h +++ b/core/documentcommands_p.h @@ -28,7 +28,7 @@ class Page; class OkularUndoCommand : public QUndoCommand { public: - virtual void refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) = 0; + virtual bool refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) = 0; }; class AddAnnotationCommand : public OkularUndoCommand @@ -42,7 +42,7 @@ class AddAnnotationCommand : public OkularUndoCommand void redo() override; - void refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) override; + bool refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) override; private: Okular::DocumentPrivate * m_docPriv; @@ -59,7 +59,7 @@ class RemoveAnnotationCommand : public OkularUndoCommand void undo() override; void redo() override; - void refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) override; + bool refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) override; private: Okular::DocumentPrivate * m_docPriv; @@ -79,7 +79,7 @@ class ModifyAnnotationPropertiesCommand : public OkularUndoCommand void undo() override; void redo() override; - void refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) override; + bool refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) override; private: Okular::DocumentPrivate * m_docPriv; @@ -105,7 +105,7 @@ class TranslateAnnotationCommand : public OkularUndoCommand Okular::NormalizedPoint minusDelta(); Okular::NormalizedRect translateBoundingRectangle( const Okular::NormalizedPoint & delta ); - void refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) override; + bool refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) override; private: Okular::DocumentPrivate * m_docPriv; @@ -132,7 +132,7 @@ class AdjustAnnotationCommand : public OkularUndoCommand Okular::NormalizedRect adjustBoundingRectangle( const Okular::NormalizedPoint & delta1, const Okular::NormalizedPoint & delta2 ); - void refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) override; + bool refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) override; private: Okular::DocumentPrivate * m_docPriv; @@ -199,7 +199,7 @@ class EditAnnotationContentsCommand : public EditTextCommand int id() const override; bool mergeWith(const QUndoCommand *uc) override; - void refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) override; + bool refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) override; private: Okular::DocumentPrivate * m_docPriv; @@ -223,7 +223,7 @@ class EditFormTextCommand : public EditTextCommand int id() const override; bool mergeWith( const QUndoCommand *uc ) override; - void refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) override; + bool refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) override; private: Okular::DocumentPrivate* m_docPriv; @@ -244,7 +244,7 @@ class EditFormListCommand : public OkularUndoCommand void undo() override; void redo() override; - void refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) override; + bool refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) override; private: Okular::DocumentPrivate* m_docPriv; @@ -272,7 +272,7 @@ class EditFormComboCommand : public EditTextCommand int id() const override; bool mergeWith( const QUndoCommand *uc ) override; - void refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) override; + bool refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) override; private: Okular::DocumentPrivate* m_docPriv; @@ -294,7 +294,7 @@ class EditFormButtonsCommand : public OkularUndoCommand void undo() override; void redo() override; - void refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) override; + bool refreshInternalPageReferences( const QVector< Okular::Page * > &newPagesVector ) override; private: void clearFormButtonStates(); From 21028432739cd20825417a30b287a0dbc2399282 Mon Sep 17 00:00:00 2001 From: Albert Astals Cid Date: Tue, 14 Nov 2017 14:52:02 +0100 Subject: [PATCH 69/78] Use the undo stack to set the modified bit This way it's much easier to track whether we are modified or not --- core/document.cpp | 26 +++++++++++--------------- core/document.h | 13 +++++++++++++ core/observer.h | 2 +- part.cpp | 22 ++++++++++------------ 4 files changed, 35 insertions(+), 28 deletions(-) diff --git a/core/document.cpp b/core/document.cpp index c3ccf7f3b..6a080ae77 100644 --- a/core/document.cpp +++ b/core/document.cpp @@ -2124,6 +2124,7 @@ Document::Document( QWidget *widget ) 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(); } @@ -3212,24 +3213,11 @@ void Document::requestTextPage( uint page ) void DocumentPrivate::notifyAnnotationChanges( int page ) { - int flags = DocumentObserver::Annotations; - - // Unless we're still loading initial annotations from metadata, the user - // now needs to save or annotation changes will be lost - if ( m_metadataLoadingCompleted ) - flags |= DocumentObserver::NeedSaveAs; - - foreachObserverD( notifyPageChanged( page, flags ) ); + foreachObserverD( notifyPageChanged( page, DocumentObserver::Annotations ) ); } -void DocumentPrivate::notifyFormChanges( int page ) +void DocumentPrivate::notifyFormChanges( int /*page*/ ) { - // Unless we're still loading initial form contents from metadata, the user - // now needs to save or form changes will be lost - if ( !m_metadataLoadingCompleted ) - return; - - foreachObserverD( notifyPageChanged( page, DocumentObserver::NeedSaveAs ) ); } void Document::addPageAnnotation( int page, Annotation * annotation ) @@ -4474,6 +4462,14 @@ bool Document::swapBackingFileArchive( const QString &newFileName, const QUrl &u 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 ) diff --git a/core/document.h b/core/document.h index 4d5fff179..ceb95867c 100644 --- a/core/document.h +++ b/core/document.h @@ -772,6 +772,13 @@ class OKULARCORE_EXPORT Document : public QObject */ 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. @@ -1129,6 +1136,12 @@ class OKULARCORE_EXPORT Document : public QObject */ 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/observer.h b/core/observer.h index ed65e3b1f..058404908 100644 --- a/core/observer.h +++ b/core/observer.h @@ -45,7 +45,7 @@ class OKULARCORE_EXPORT DocumentObserver TextSelection = 8, ///< Text selection has been changed Annotations = 16, ///< Annotations have been changed BoundingBox = 32, ///< Bounding boxes have been changed - NeedSaveAs = 64 ///< Set when "Save" is needed or annotation/form 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 }; /** diff --git a/part.cpp b/part.cpp index 2f34d4a46..d0bf891ee 100644 --- a/part.cpp +++ b/part.cpp @@ -389,6 +389,13 @@ m_cliPresentation(false), m_cliPrint(false), m_embedMode(detectEmbedMode(parentW 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()) ); @@ -1198,12 +1205,6 @@ void Part::notifyViewportChanged( bool /*smoothMove*/ ) void Part::notifyPageChanged( int page, int flags ) { - if ( flags & Okular::DocumentObserver::NeedSaveAs ) - { - setModified(); - setWindowTitleFromDocument(); - } - if ( !(flags & Okular::DocumentObserver::Bookmark ) ) return; @@ -1729,8 +1730,6 @@ bool Part::closeUrl(bool promptToSave) return true; // pretend it worked } - setModified( false ); - if (!m_temporaryLocalFile.isNull() && m_temporaryLocalFile != localFilePath()) { QFile::remove( m_temporaryLocalFile ); @@ -2636,8 +2635,8 @@ bool Part::saveAs( const QUrl & saveUrl, SaveAsFlags flags ) return false; } - setModified( false ); - setWindowTitleFromDocument(); + m_document->setHistoryClean( true ); + if ( m_document->isDocdataMigrationNeeded() ) m_document->docdataMigrationDone(); @@ -2652,8 +2651,7 @@ bool Part::saveAs( const QUrl & saveUrl, SaveAsFlags flags ) { if ( setModifiedAfterSave ) { - setModified(); - setWindowTitleFromDocument(); + m_document->setHistoryClean( false ); } } else From d7457e35695dd79ea2b4a2571c6e3f70c04e50e3 Mon Sep 17 00:00:00 2001 From: Albert Astals Cid Date: Wed, 15 Nov 2017 09:46:36 +0100 Subject: [PATCH 70/78] Fix double dialog in closing sometimes sometimes = when you opened a .okular containing a png and an annotation and then saved as to just png Found by the autotest that stopped passing \o/ --- part.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/part.cpp b/part.cpp index d0bf891ee..50ca274bf 100644 --- a/part.cpp +++ b/part.cpp @@ -1730,6 +1730,8 @@ bool Part::closeUrl(bool promptToSave) return true; // pretend it worked } + m_document->setHistoryClean( true ); + if (!m_temporaryLocalFile.isNull() && m_temporaryLocalFile != localFilePath()) { QFile::remove( m_temporaryLocalFile ); From d63c0ed50ecea12a5cb5760733f48a719ab1d517 Mon Sep 17 00:00:00 2001 From: Albert Astals Cid Date: Wed, 15 Nov 2017 10:00:40 +0100 Subject: [PATCH 71/78] okular archive -> okular document archive --- doc/index.docbook | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/index.docbook b/doc/index.docbook index 74140b402..4b35dc785 100644 --- a/doc/index.docbook +++ b/doc/index.docbook @@ -457,7 +457,7 @@ Context menu actions like Rename Bookmarks etc.) 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. - &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. + &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 document archive in the Filter selector. To open an &okular; document archive, just open it with &okular; as it would be ⪚ a &PDF; document. 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. @@ -929,7 +929,7 @@ Context menu actions like Rename Bookmarks etc.) - Open a supported file or &okular; archive. If there is already an opened file it will be closed. For more information, see the section about Opening Files. + Open a supported file or &okular; document archive. If there is already an opened file it will be closed. For more information, see the section about Opening Files. @@ -989,7 +989,7 @@ Context menu actions like Rename Bookmarks 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. + 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; document archive. @@ -1004,7 +1004,7 @@ Context menu actions like Rename Bookmarks etc.) - 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. + 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; document 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;). From 37097a0c3875b21b67126c11673f54092440018d Mon Sep 17 00:00:00 2001 From: Albert Astals Cid Date: Wed, 15 Nov 2017 10:08:20 +0100 Subject: [PATCH 72/78] Some text tweaking suggested in the phabricator review --- doc/index.docbook | 4 ++-- part.cpp | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/index.docbook b/doc/index.docbook index 4b35dc785..5218b752d 100644 --- a/doc/index.docbook +++ b/doc/index.docbook @@ -989,7 +989,7 @@ Context menu actions like Rename Bookmarks 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; document archive. + 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 given the option to either discard or to save them together with the document in an &okular; document archive. @@ -1004,7 +1004,7 @@ Context menu actions like Rename Bookmarks etc.) - 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; document archive. + 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 given the option to either discard or to save them together with the document in an &okular; document 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/part.cpp b/part.cpp index 50ca274bf..c025bf6ce 100644 --- a/part.cpp +++ b/part.cpp @@ -2484,7 +2484,7 @@ bool Part::saveAs( const QUrl & saveUrl, SaveAsFlags flags ) 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( "After saving, the current document format requires the file to be reloaded. Your undo/redo history will be lost.
Do you want to continue?" ), i18n( "Save - Warning" ) ); switch ( res ) @@ -2517,8 +2517,8 @@ bool Part::saveAs( const QUrl & saveUrl, SaveAsFlags flags ) 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)." ); + i18n( "You are about to save changes, but the current file format does not support saving the following elements. Please use the Okular document archive format to preserve them. Click Continue to save the document and discard these elements." ) : + i18n( "You are about to save changes, but the current file format does not support saving the following elements. Please use the Okular document archive format to preserve them. Click Continue to save, but you will lose these elements as well as the undo/redo history." ); const QString continueMessage = m_document->canSwapBackingFile() ? i18n( "Continue" ) : i18n( "Continue losing changes" ); From e29892fda499b1484c00035c718eda3fc2351120 Mon Sep 17 00:00:00 2001 From: Albert Astals Cid Date: Wed, 15 Nov 2017 11:20:25 +0100 Subject: [PATCH 73/78] some review tweaks --- core/document.cpp | 4 ++-- core/generator.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/document.cpp b/core/document.cpp index 6a080ae77..405dc1597 100644 --- a/core/document.cpp +++ b/core/document.cpp @@ -1156,7 +1156,7 @@ void DocumentPrivate::saveDocumentInfo() const doc.appendChild( root ); // 2.1. Save page attributes (bookmark state, annotations, ... ) to DOM - // -> do this there are not-yet-migrated annots or forms in docdata/ + // -> do this if there are not-yet-migrated annots or forms in docdata/ if ( m_docdataMigrationNeeded ) { QDomElement pageList = doc.createElement( "pageList" ); @@ -1164,7 +1164,7 @@ void DocumentPrivate::saveDocumentInfo() const // 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 + // Since we don't store annotations and forms in docdata/ any more, this is // necessary to preserve annotations/forms that previous Okular version // had stored there. const PageItems saveWhat = AllPageItems | OriginalAnnotationPageItems | OriginalFormFieldPageItems; diff --git a/core/generator.h b/core/generator.h index b15f47808..8521d3777 100644 --- a/core/generator.h +++ b/core/generator.h @@ -290,7 +290,7 @@ class OKULARCORE_EXPORT Generator : public QObject * * @since 1.3 */ - virtual SwapBackingFileResult swapBackingFile( const QString &newFileName, QVector & newPagesVector ); + virtual SwapBackingFileResult swapBackingFile( const QString & newFileName, QVector & newPagesVector ); /** * This method is called when the document is closed and not used From 2da30e747ddd828ec3fe3884ba9e25dc802554b7 Mon Sep 17 00:00:00 2001 From: Albert Astals Cid Date: Wed, 15 Nov 2017 11:53:14 +0100 Subject: [PATCH 74/78] Fix crash on saving time if some of the existing annotations where removed Also rename a variable to make it more clear what it does --- generators/poppler/annots.cpp | 5 +++-- generators/poppler/annots.h | 3 ++- generators/poppler/generator_pdf.cpp | 12 ++++++------ generators/poppler/generator_pdf.h | 4 +++- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/generators/poppler/annots.cpp b/generators/poppler/annots.cpp index c7fd79834..ee44e23bf 100644 --- a/generators/poppler/annots.cpp +++ b/generators/poppler/annots.cpp @@ -62,8 +62,8 @@ static int maskExportedFlags(int flags) } //BEGIN PopplerAnnotationProxy implementation -PopplerAnnotationProxy::PopplerAnnotationProxy( Poppler::Document *doc, QMutex *userMutex ) - : ppl_doc ( doc ), mutex ( userMutex ) +PopplerAnnotationProxy::PopplerAnnotationProxy( Poppler::Document *doc, QMutex *userMutex, QHash *annotsOnOpenHash ) + : ppl_doc ( doc ), mutex ( userMutex ), annotationsOnOpenHash( annotsOnOpenHash ) { } @@ -254,6 +254,7 @@ void PopplerAnnotationProxy::notifyRemoval( Okular::Annotation *okl_ann, int pag QMutexLocker ml(mutex); Poppler::Page *ppl_page = ppl_doc->page( page ); + annotationsOnOpenHash->remove( okl_ann ); ppl_page->removeAnnotation( ppl_ann ); // Also destroys ppl_ann delete ppl_page; diff --git a/generators/poppler/annots.h b/generators/poppler/annots.h index 9dc6aa6d7..d6d1dd651 100644 --- a/generators/poppler/annots.h +++ b/generators/poppler/annots.h @@ -23,7 +23,7 @@ extern Okular::Annotation* createAnnotationFromPopplerAnnotation( Poppler::Annot class PopplerAnnotationProxy : public Okular::AnnotationProxy { public: - PopplerAnnotationProxy( Poppler::Document *doc, QMutex *userMutex ); + PopplerAnnotationProxy( Poppler::Document *doc, QMutex *userMutex, QHash *annotsOnOpenHash ); ~PopplerAnnotationProxy(); bool supports( Capability capability ) const override; @@ -33,6 +33,7 @@ class PopplerAnnotationProxy : public Okular::AnnotationProxy private: Poppler::Document *ppl_doc; QMutex *mutex; + QHash *annotationsOnOpenHash; }; #endif diff --git a/generators/poppler/generator_pdf.cpp b/generators/poppler/generator_pdf.cpp index ff13fbf3f..8c2025a2f 100644 --- a/generators/poppler/generator_pdf.cpp +++ b/generators/poppler/generator_pdf.cpp @@ -579,7 +579,7 @@ Okular::Document::OpenResult PDFGenerator::init(QVector & pagesVe pagesVector.resize(pageCount); rectsGenerated.fill(false, pageCount); - annotationsHash.clear(); + annotationsOnOpenHash.clear(); loadPages(pagesVector, 0, false); @@ -587,7 +587,7 @@ Okular::Document::OpenResult PDFGenerator::init(QVector & pagesVe reparseConfig(); // create annotation proxy - annotProxy = new PopplerAnnotationProxy( pdfdoc, userMutex() ); + annotProxy = new PopplerAnnotationProxy( pdfdoc, userMutex(), &annotationsOnOpenHash ); // the file has been loaded correctly return Okular::Document::OpenSuccess; @@ -1006,8 +1006,8 @@ void PDFGenerator::resolveMediaLinkReference( Okular::Action *action ) if ( (action->actionType() != Okular::Action::Movie) && (action->actionType() != Okular::Action::Rendition) ) return; - resolveMediaLinks( action, Okular::Annotation::AMovie, annotationsHash ); - resolveMediaLinks( action, Okular::Annotation::AScreen, annotationsHash ); + resolveMediaLinks( action, Okular::Annotation::AMovie, annotationsOnOpenHash ); + resolveMediaLinks( action, Okular::Annotation::AScreen, annotationsOnOpenHash ); } void PDFGenerator::resolveMediaLinkReferences( Okular::Page *page ) @@ -1564,7 +1564,7 @@ void PDFGenerator::addAnnotations( Poppler::Page * popplerPage, Okular::Page * p } if ( !doDelete ) - annotationsHash.insert( newann, a ); + annotationsOnOpenHash.insert( newann, a ); } if ( doDelete ) delete a; @@ -1718,7 +1718,7 @@ bool PDFGenerator::save( const QString &fileName, SaveOptions options, QString * QMutexLocker locker( userMutex() ); - QHashIterator it( annotationsHash ); + QHashIterator it( annotationsOnOpenHash ); while ( it.hasNext() ) { it.next(); diff --git a/generators/poppler/generator_pdf.h b/generators/poppler/generator_pdf.h index 957a28592..ef8c98a4f 100644 --- a/generators/poppler/generator_pdf.h +++ b/generators/poppler/generator_pdf.h @@ -137,7 +137,9 @@ class PDFGenerator : public Okular::Generator, public Okular::ConfigInterface, p mutable QList docEmbeddedFiles; int nextFontPage; PopplerAnnotationProxy *annotProxy; - QHash annotationsHash; + // the hash below only contains annotations that were present on the file at open time + // this is enough for what we use it for + QHash annotationsOnOpenHash; QBitArray rectsGenerated; From f63aa211dc7a75b2a2805aa017d6e855bb07ea2e Mon Sep 17 00:00:00 2001 From: Albert Astals Cid Date: Wed, 15 Nov 2017 12:09:17 +0100 Subject: [PATCH 75/78] test: Actually do the migration instead of faking it --- autotests/documenttest.cpp | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/autotests/documenttest.cpp b/autotests/documenttest.cpp index 4c79b2b38..4f7e2fd06 100644 --- a/autotests/documenttest.cpp +++ b/autotests/documenttest.cpp @@ -102,7 +102,11 @@ void DocumentTest::testDocdataMigration() QCOMPARE( m_document->page( 0 )->annotations().first()->uniqueName(), QString("testannot") ); QCOMPARE( m_document->isDocdataMigrationNeeded(), true ); - // Pretend the user has done the migration + // Do the migration + QTemporaryFile migratedSaveFile( QString( "%1/okrXXXXXX.pdf" ).arg( QDir::tempPath() ) ); + QVERIFY( migratedSaveFile.open() ); + migratedSaveFile.close(); + QVERIFY( m_document->saveChanges( migratedSaveFile.fileName() ) ); m_document->docdataMigrationDone(); QCOMPARE( m_document->isDocdataMigrationNeeded(), false ); m_document->closeDocument(); @@ -113,6 +117,12 @@ void DocumentTest::testDocdataMigration() QCOMPARE( m_document->isDocdataMigrationNeeded(), false ); m_document->closeDocument(); + // And the new file should have 1 annotation, let's check + QCOMPARE( m_document->openDocument( migratedSaveFile.fileName(), migratedSaveFile.fileName(), mime ), Okular::Document::OpenSuccess ); + QCOMPARE( m_document->page( 0 )->annotations().size(), 1 ); + QCOMPARE( m_document->isDocdataMigrationNeeded(), false ); + m_document->closeDocument(); + delete m_document; } From 481676dcedf17a48d996b1f368f72d38869a3e4d Mon Sep 17 00:00:00 2001 From: Albert Astals Cid Date: Wed, 15 Nov 2017 14:10:36 +0100 Subject: [PATCH 76/78] Do not show "Continue" while on Save format warning It makes no sense, only on Save As makes sense --- part.cpp | 62 ++++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 42 insertions(+), 20 deletions(-) diff --git a/part.cpp b/part.cpp index c025bf6ce..33102a9be 100644 --- a/part.cpp +++ b/part.cpp @@ -2516,27 +2516,49 @@ bool Part::saveAs( const QUrl & saveUrl, SaveAsFlags flags ) 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 the document and discard these elements." ) : - i18n( "You are about to save changes, but the current file format does not support saving the following elements. Please use the Okular document archive format to preserve them. Click Continue to save, but you will lose these elements as well as the undo/redo history." ); - const QString continueMessage = m_document->canSwapBackingFile() ? - i18n( "Continue" ) : - i18n( "Continue losing changes" ); - const int result = KMessageBox::warningYesNoCancelList( widget(), - warningMessage, - listOfwontSaves, i18n( "Warning" ), - KGuiItem( i18n( "Save as Okular document archive..." ), "document-save-as" ), // <- KMessageBox::Yes - KGuiItem( continueMessage, "arrow-right" ) ); // <- KMessageBox::NO - - switch (result) + if ( saveUrl == url() ) { - 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; + // Save + const QString warningMessage = i18n( "You are about to save changes, but the current file format does not support saving the following elements. Please use the Okular document archive format to preserve them." ); + const int result = KMessageBox::warningYesNoList( widget(), + warningMessage, + listOfwontSaves, i18n( "Warning" ), + KGuiItem( i18n( "Save as Okular document archive..." ), "document-save-as" ), // <- KMessageBox::Yes + KStandardGuiItem::cancel() ); + + switch (result) + { + case KMessageBox::Yes: // -> Save as Okular document archive + return slotSaveFileAs( true /* showOkularArchiveAsDefaultFormat */ ); + default: + return false; + } + } + else + { + // Save as + const QString warningMessage = m_document->canSwapBackingFile() ? + i18n( "You are about to save changes, but the current file format does not support saving the following elements. Please use the Okular document archive format to preserve them. Click Continue to save the document and discard these elements." ) : + i18n( "You are about to save changes, but the current file format does not support saving the following elements. Please use the Okular document archive format to preserve them. Click Continue to save, but you will lose these elements as well as the undo/redo history." ); + const QString continueMessage = m_document->canSwapBackingFile() ? + i18n( "Continue" ) : + i18n( "Continue losing changes" ); + const int result = KMessageBox::warningYesNoCancelList( widget(), + warningMessage, + listOfwontSaves, i18n( "Warning" ), + KGuiItem( i18n( "Save as Okular document archive..." ), "document-save-as" ), // <- KMessageBox::Yes + KGuiItem( continueMessage, "arrow-right" ) ); // <- KMessageBox::NO + + 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; + } } } From 19b7e3c1120981fcfc25e210eb02da3d36604ff4 Mon Sep 17 00:00:00 2001 From: Albert Astals Cid Date: Thu, 16 Nov 2017 09:57:58 +0100 Subject: [PATCH 77/78] The work in this branch was sponsored by LiMux give them some credit in the headers --- autotests/documenttest.cpp | 3 +++ autotests/parttest.cpp | 3 +++ core/document.cpp | 3 +++ core/document.h | 3 +++ core/documentcommands.cpp | 3 +++ core/documentcommands_p.h | 3 +++ core/generator.cpp | 3 +++ core/generator.h | 3 +++ core/generator_p.h | 3 +++ core/observer.h | 3 +++ core/page.cpp | 4 ++++ core/page.h | 3 +++ core/page_p.h | 3 +++ generators/kimgio/generator_kimgio.cpp | 3 +++ generators/kimgio/generator_kimgio.h | 3 +++ generators/poppler/annots.cpp | 3 +++ generators/poppler/annots.h | 3 +++ generators/poppler/generator_pdf.cpp | 3 +++ generators/poppler/generator_pdf.h | 3 +++ part.cpp | 3 +++ part.h | 3 +++ shell/shell.cpp | 3 +++ ui/annotationmodel.cpp | 3 +++ ui/annotwindow.cpp | 3 +++ ui/annotwindow.h | 3 +++ ui/formwidgets.cpp | 3 +++ ui/formwidgets.h | 3 +++ ui/pageview.cpp | 3 +++ ui/pageview.h | 3 +++ ui/pageviewmouseannotation.cpp | 3 +++ ui/pageviewmouseannotation.h | 3 +++ ui/pageviewutils.cpp | 3 +++ ui/pageviewutils.h | 3 +++ 33 files changed, 100 insertions(+) diff --git a/autotests/documenttest.cpp b/autotests/documenttest.cpp index 4f7e2fd06..2430be3b4 100644 --- a/autotests/documenttest.cpp +++ b/autotests/documenttest.cpp @@ -1,5 +1,8 @@ /*************************************************************************** * Copyright (C) 2013 by Fabio D'Urso * + * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * + * company, info@kdab.com. Work sponsored by the * + * LiMux project of the city of Munich * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * diff --git a/autotests/parttest.cpp b/autotests/parttest.cpp index ac45978ab..0b25f4423 100644 --- a/autotests/parttest.cpp +++ b/autotests/parttest.cpp @@ -1,5 +1,8 @@ /*************************************************************************** * Copyright (C) 2013 by Albert Astals Cid * + * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * + * company, info@kdab.com. Work sponsored by the * + * LiMux project of the city of Munich * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * diff --git a/core/document.cpp b/core/document.cpp index 405dc1597..6b640c450 100644 --- a/core/document.cpp +++ b/core/document.cpp @@ -1,6 +1,9 @@ /*************************************************************************** * Copyright (C) 2004-2005 by Enrico Ros * * Copyright (C) 2004-2008 by Albert Astals Cid * + * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * + * company, info@kdab.com. Work sponsored by the * + * LiMux project of the city of Munich * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * diff --git a/core/document.h b/core/document.h index ceb95867c..f996f41f6 100644 --- a/core/document.h +++ b/core/document.h @@ -1,6 +1,9 @@ /*************************************************************************** * Copyright (C) 2004-2005 by Enrico Ros * * Copyright (C) 2004-2008 by Albert Astals Cid * + * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * + * company, info@kdab.com. Work sponsored by the * + * LiMux project of the city of Munich * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * diff --git a/core/documentcommands.cpp b/core/documentcommands.cpp index 58804a7c7..3b9348bd4 100644 --- a/core/documentcommands.cpp +++ b/core/documentcommands.cpp @@ -1,5 +1,8 @@ /*************************************************************************** * Copyright (C) 2013 Jon Mease * + * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * + * company, info@kdab.com. Work sponsored by the * + * LiMux project of the city of Munich * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * diff --git a/core/documentcommands_p.h b/core/documentcommands_p.h index 055d35c7e..2f4cc2d7a 100644 --- a/core/documentcommands_p.h +++ b/core/documentcommands_p.h @@ -1,5 +1,8 @@ /*************************************************************************** * Copyright (C) 2013 Jon Mease * + * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * + * company, info@kdab.com. Work sponsored by the * + * LiMux project of the city of Munich * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * diff --git a/core/generator.cpp b/core/generator.cpp index 7d1dfa122..41a6318c5 100644 --- a/core/generator.cpp +++ b/core/generator.cpp @@ -1,6 +1,9 @@ /*************************************************************************** * Copyright (C) 2005 by Piotr Szymanski * * Copyright (C) 2008 by Albert Astals Cid * + * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * + * company, info@kdab.com. Work sponsored by the * + * LiMux project of the city of Munich * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * diff --git a/core/generator.h b/core/generator.h index 8521d3777..bd761d3c3 100644 --- a/core/generator.h +++ b/core/generator.h @@ -2,6 +2,9 @@ * Copyright (C) 2004-5 by Enrico Ros * * Copyright (C) 2005 by Piotr Szymanski * * Copyright (C) 2008 by Albert Astals Cid * + * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * + * company, info@kdab.com. Work sponsored by the * + * LiMux project of the city of Munich * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * diff --git a/core/generator_p.h b/core/generator_p.h index 6457e7be1..50927eab8 100644 --- a/core/generator_p.h +++ b/core/generator_p.h @@ -1,5 +1,8 @@ /*************************************************************************** * Copyright (C) 2007 Tobias Koenig * + * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * + * company, info@kdab.com. Work sponsored by the * + * LiMux project of the city of Munich * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * diff --git a/core/observer.h b/core/observer.h index 058404908..9e01c8707 100644 --- a/core/observer.h +++ b/core/observer.h @@ -1,6 +1,9 @@ /*************************************************************************** * Copyright (C) 2005 by Enrico Ros * * Copyright (C) 2005 by Albert Astals Cid * + * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * + * company, info@kdab.com. Work sponsored by the * + * LiMux project of the city of Munich * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * diff --git a/core/page.cpp b/core/page.cpp index 6ee095379..c457f928b 100644 --- a/core/page.cpp +++ b/core/page.cpp @@ -1,5 +1,9 @@ /*************************************************************************** * Copyright (C) 2004 by Enrico Ros * + * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * + * company, info@kdab.com. Work sponsored by the * + * LiMux project of the city of Munich * + * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * diff --git a/core/page.h b/core/page.h index 21f0687ce..7324b2be4 100644 --- a/core/page.h +++ b/core/page.h @@ -1,5 +1,8 @@ /*************************************************************************** * Copyright (C) 2004 by Enrico Ros * + * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * + * company, info@kdab.com. Work sponsored by the * + * LiMux project of the city of Munich * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * diff --git a/core/page_p.h b/core/page_p.h index 86248eff1..4b7831c4c 100644 --- a/core/page_p.h +++ b/core/page_p.h @@ -1,6 +1,9 @@ /*************************************************************************** * Copyright (C) 2004 by Enrico Ros * * Copyright (C) 2007 by Pino Toscano * + * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * + * company, info@kdab.com. Work sponsored by the * + * LiMux project of the city of Munich * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * diff --git a/generators/kimgio/generator_kimgio.cpp b/generators/kimgio/generator_kimgio.cpp index 124672877..f38e694c1 100644 --- a/generators/kimgio/generator_kimgio.cpp +++ b/generators/kimgio/generator_kimgio.cpp @@ -2,6 +2,9 @@ * Copyright (C) 2005 by Albert Astals Cid * * Copyright (C) 2006-2007 by Pino Toscano * * Copyright (C) 2006-2007 by Tobias Koenig * + * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * + * company, info@kdab.com. Work sponsored by the * + * LiMux project of the city of Munich * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * diff --git a/generators/kimgio/generator_kimgio.h b/generators/kimgio/generator_kimgio.h index b80534acb..bd09bbed0 100644 --- a/generators/kimgio/generator_kimgio.h +++ b/generators/kimgio/generator_kimgio.h @@ -1,5 +1,8 @@ /*************************************************************************** * Copyright (C) 2005 by Albert Astals Cid * + * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * + * company, info@kdab.com. Work sponsored by the * + * LiMux project of the city of Munich * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * diff --git a/generators/poppler/annots.cpp b/generators/poppler/annots.cpp index ee44e23bf..a24539e75 100644 --- a/generators/poppler/annots.cpp +++ b/generators/poppler/annots.cpp @@ -1,6 +1,9 @@ /*************************************************************************** * Copyright (C) 2008 by Pino Toscano * * Copyright (C) 2012 by Guillermo A. Amaral B. * + * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * + * company, info@kdab.com. Work sponsored by the * + * LiMux project of the city of Munich * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * diff --git a/generators/poppler/annots.h b/generators/poppler/annots.h index d6d1dd651..cf7496a98 100644 --- a/generators/poppler/annots.h +++ b/generators/poppler/annots.h @@ -1,5 +1,8 @@ /*************************************************************************** * Copyright (C) 2012 by Fabio D'Urso * + * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * + * company, info@kdab.com. Work sponsored by the * + * LiMux project of the city of Munich * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * diff --git a/generators/poppler/generator_pdf.cpp b/generators/poppler/generator_pdf.cpp index 8c2025a2f..08ebf5339 100644 --- a/generators/poppler/generator_pdf.cpp +++ b/generators/poppler/generator_pdf.cpp @@ -2,6 +2,9 @@ * Copyright (C) 2004-2008 by Albert Astals Cid * * Copyright (C) 2004 by Enrico Ros * * Copyright (C) 2012 by Guillermo A. Amaral B. * + * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * + * company, info@kdab.com. Work sponsored by the * + * LiMux project of the city of Munich * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * diff --git a/generators/poppler/generator_pdf.h b/generators/poppler/generator_pdf.h index ef8c98a4f..d6ea065ba 100644 --- a/generators/poppler/generator_pdf.h +++ b/generators/poppler/generator_pdf.h @@ -1,6 +1,9 @@ /*************************************************************************** * Copyright (C) 2004-2008 by Albert Astals Cid * * Copyright (C) 2004 by Enrico Ros * + * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * + * company, info@kdab.com. Work sponsored by the * + * LiMux project of the city of Munich * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * diff --git a/part.cpp b/part.cpp index 33102a9be..31fd74c3d 100644 --- a/part.cpp +++ b/part.cpp @@ -14,6 +14,9 @@ * Copyright (C) 2004 by Waldo Bastian * * Copyright (C) 2004-2008 by Albert Astals Cid * * Copyright (C) 2004 by Antti Markus * + * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * + * company, info@kdab.com. Work sponsored by the * + * LiMux project of the city of Munich * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * diff --git a/part.h b/part.h index a8d84bf14..8d85ccb06 100644 --- a/part.h +++ b/part.h @@ -6,6 +6,9 @@ * Copyright (C) 2003 by Laurent Montel * * Copyright (C) 2004 by Dominique Devriese * * Copyright (C) 2004-2007 by Albert Astals Cid * + * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * + * company, info@kdab.com. Work sponsored by the * + * LiMux project of the city of Munich * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * diff --git a/shell/shell.cpp b/shell/shell.cpp index 52d5fc367..16290dbcc 100644 --- a/shell/shell.cpp +++ b/shell/shell.cpp @@ -10,6 +10,9 @@ * Copyright (C) 2003 by Malcolm Hunter * * Copyright (C) 2004 by Dominique Devriese * * Copyright (C) 2004 by Dirk Mueller * + * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * + * company, info@kdab.com. Work sponsored by the * + * LiMux project of the city of Munich * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * diff --git a/ui/annotationmodel.cpp b/ui/annotationmodel.cpp index ea5347dea..8f4861c8f 100644 --- a/ui/annotationmodel.cpp +++ b/ui/annotationmodel.cpp @@ -1,5 +1,8 @@ /*************************************************************************** * Copyright (C) 2006 by Pino Toscano * + * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * + * company, info@kdab.com. Work sponsored by the * + * LiMux project of the city of Munich * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * diff --git a/ui/annotwindow.cpp b/ui/annotwindow.cpp index ec8e81470..ab4e0cc5b 100644 --- a/ui/annotwindow.cpp +++ b/ui/annotwindow.cpp @@ -1,6 +1,9 @@ /*************************************************************************** * Copyright (C) 2006 by Chu Xiaodong * * Copyright (C) 2006 by Pino Toscano * + * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * + * company, info@kdab.com. Work sponsored by the * + * LiMux project of the city of Munich * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * diff --git a/ui/annotwindow.h b/ui/annotwindow.h index a81154c4d..1af2c32b3 100644 --- a/ui/annotwindow.h +++ b/ui/annotwindow.h @@ -1,6 +1,9 @@ /*************************************************************************** * Copyright (C) 2006 by Chu Xiaodong * * Copyright (C) 2006 by Pino Toscano * + * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * + * company, info@kdab.com. Work sponsored by the * + * LiMux project of the city of Munich * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * diff --git a/ui/formwidgets.cpp b/ui/formwidgets.cpp index 64cc152a2..7f3d188c8 100644 --- a/ui/formwidgets.cpp +++ b/ui/formwidgets.cpp @@ -1,5 +1,8 @@ /*************************************************************************** * Copyright (C) 2007 by Pino Toscano * + * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * + * company, info@kdab.com. Work sponsored by the * + * LiMux project of the city of Munich * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * diff --git a/ui/formwidgets.h b/ui/formwidgets.h index bb5773d3f..d8ba9619d 100644 --- a/ui/formwidgets.h +++ b/ui/formwidgets.h @@ -1,5 +1,8 @@ /*************************************************************************** * Copyright (C) 2007 by Pino Toscano * + * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * + * company, info@kdab.com. Work sponsored by the * + * LiMux project of the city of Munich * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * diff --git a/ui/pageview.cpp b/ui/pageview.cpp index 6bd6c12be..16f57c23d 100644 --- a/ui/pageview.cpp +++ b/ui/pageview.cpp @@ -1,6 +1,9 @@ /*************************************************************************** * Copyright (C) 2004-2005 by Enrico Ros * * Copyright (C) 2004-2006 by Albert Astals Cid * + * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * + * company, info@kdab.com. Work sponsored by the * + * LiMux project of the city of Munich * * * * With portions of code from kpdf/kpdf_pagewidget.cc by: * * Copyright (C) 2002 by Wilco Greven * diff --git a/ui/pageview.h b/ui/pageview.h index e09ecaf24..d0d015ba7 100644 --- a/ui/pageview.h +++ b/ui/pageview.h @@ -1,6 +1,9 @@ /*************************************************************************** * Copyright (C) 2004 by Enrico Ros * * Copyright (C) 2004 by Albert Astals Cid * + * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * + * company, info@kdab.com. Work sponsored by the * + * LiMux project of the city of Munich * * * * With portions of code from kpdf/kpdf_pagewidget.h by: * * Copyright (C) 2002 by Wilco Greven * diff --git a/ui/pageviewmouseannotation.cpp b/ui/pageviewmouseannotation.cpp index c15bc4164..de9d46f5f 100644 --- a/ui/pageviewmouseannotation.cpp +++ b/ui/pageviewmouseannotation.cpp @@ -2,6 +2,9 @@ * Copyright (C) 2017 by Tobias Deiminger * * Copyright (C) 2004-2005 by Enrico Ros * * Copyright (C) 2004-2006 by Albert Astals Cid * + * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * + * company, info@kdab.com. Work sponsored by the * + * LiMux project of the city of Munich * * * * With portions of code from kpdf/kpdf_pagewidget.cc by: * * Copyright (C) 2002 by Wilco Greven * diff --git a/ui/pageviewmouseannotation.h b/ui/pageviewmouseannotation.h index fe06e3195..87dbb8db2 100644 --- a/ui/pageviewmouseannotation.h +++ b/ui/pageviewmouseannotation.h @@ -2,6 +2,9 @@ * Copyright (C) 2017 by Tobias Deiminger * * Copyright (C) 2004-2005 by Enrico Ros * * Copyright (C) 2004-2006 by Albert Astals Cid * + * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * + * company, info@kdab.com. Work sponsored by the * + * LiMux project of the city of Munich * * * * With portions of code from kpdf/kpdf_pagewidget.cc by: * * Copyright (C) 2002 by Wilco Greven * diff --git a/ui/pageviewutils.cpp b/ui/pageviewutils.cpp index 88123d3a8..d8a8698bc 100644 --- a/ui/pageviewutils.cpp +++ b/ui/pageviewutils.cpp @@ -1,5 +1,8 @@ /*************************************************************************** * Copyright (C) 2004-2005 by Enrico Ros * + * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * + * company, info@kdab.com. Work sponsored by the * + * LiMux project of the city of Munich * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * diff --git a/ui/pageviewutils.h b/ui/pageviewutils.h index a5cd89a2d..6c3740927 100644 --- a/ui/pageviewutils.h +++ b/ui/pageviewutils.h @@ -1,5 +1,8 @@ /*************************************************************************** * Copyright (C) 2004-2005 by Enrico Ros * + * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * + * company, info@kdab.com. Work sponsored by the * + * LiMux project of the city of Munich * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * From 1420c9411eb64e7265e88d66d0c442ed0cfc8f0c Mon Sep 17 00:00:00 2001 From: Albert Astals Cid Date: Thu, 16 Nov 2017 12:03:23 +0100 Subject: [PATCH 78/78] Save or Discard dialog: show only the file name and not the full url --- part.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/part.cpp b/part.cpp index 31fd74c3d..afbb4a527 100644 --- a/part.cpp +++ b/part.cpp @@ -1704,7 +1704,7 @@ bool Part::queryClose() return true; const int res = KMessageBox::warningYesNoCancel( widget(), - i18n( "Do you want to save your changes to \"%1\" or discard them?", url().toDisplayString() ), + i18n( "Do you want to save your changes to \"%1\" or discard them?", url().fileName() ), i18n( "Close Document" ), KStandardGuiItem::save(), KStandardGuiItem::discard() );