File indexing completed on 2024-05-19 05:05:46

0001 /***************************************************************************
0002  *   SPDX-License-Identifier: GPL-2.0-or-later
0003  *                                                                         *
0004  *   SPDX-FileCopyrightText: 2004-2023 Thomas Fischer <fischer@unix-ag.uni-kl.de>
0005  *                                                                         *
0006  *   This program is free software; you can redistribute it and/or modify  *
0007  *   it under the terms of the GNU General Public License as published by  *
0008  *   the Free Software Foundation; either version 2 of the License, or     *
0009  *   (at your option) any later version.                                   *
0010  *                                                                         *
0011  *   This program is distributed in the hope that it will be useful,       *
0012  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
0013  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
0014  *   GNU General Public License for more details.                          *
0015  *                                                                         *
0016  *   You should have received a copy of the GNU General Public License     *
0017  *   along with this program; if not, see <https://www.gnu.org/licenses/>. *
0018  ***************************************************************************/
0019 
0020 #include "part.h"
0021 
0022 #include <QLabel>
0023 #include <QAction>
0024 #include <QFile>
0025 #include <QFileInfo>
0026 #include <QMenu>
0027 #include <QShortcut>
0028 #include <QApplication>
0029 #include <QLayout>
0030 #include <QListWidget>
0031 #include <QKeyEvent>
0032 #include <QMimeType>
0033 #include <QPointer>
0034 #include <QFileSystemWatcher>
0035 #include <QFileDialog>
0036 #include <QDialog>
0037 #include <QDialogButtonBox>
0038 #include <QPushButton>
0039 #include <QTemporaryFile>
0040 #include <QTimer>
0041 #include <QStandardPaths>
0042 #include <QFlags>
0043 
0044 #include <kio_version.h>
0045 #include <KMessageBox> // FIXME deprecated
0046 #include <KLocalizedString>
0047 #include <KActionCollection>
0048 #include <KStandardAction>
0049 #include <KActionMenu>
0050 #include <KSelectAction>
0051 #include <KToggleAction>
0052 #if KIO_VERSION >= QT_VERSION_CHECK(5, 71, 0)
0053 #include <KIO/OpenUrlJob>
0054 #if KIO_VERSION >= QT_VERSION_CHECK(5, 98, 0)
0055 #include <KIO/JobUiDelegateFactory>
0056 #else // < 5.98.0
0057 #include <KIO/JobUiDelegate>
0058 #endif // QT_VERSION_CHECK(5, 98, 0)
0059 #else // < 5.71.0
0060 #include <KRun>
0061 #endif // KIO_VERSION >= QT_VERSION_CHECK(5, 71, 0)
0062 #include <KPluginFactory>
0063 #include <KIO/StatJob>
0064 #include <KIO/CopyJob>
0065 #include <KIO/Job>
0066 #include <KJobWidgets>
0067 
0068 #include <Preferences>
0069 #include <File>
0070 #include <Macro>
0071 #include <Preamble>
0072 #include <Comment>
0073 #include <FileInfo>
0074 #include <FileImporter>
0075 #include <FileExporter>
0076 #include <FileExporterBibTeX>
0077 #include <FileExporterToolchain>
0078 #include <BibUtils>
0079 #include <models/FileModel>
0080 #include <IdSuggestions>
0081 #include <LyX>
0082 #include <UrlChecker>
0083 #include <widgets/FileSettingsWidget>
0084 #include <widgets/FilterBar>
0085 #include <element/FindPDFUI>
0086 #include <file/FileView>
0087 #include <file/FindDuplicatesUI>
0088 #include <file/Clipboard>
0089 #include <preferences/SettingsColorLabelWidget>
0090 #include <preferences/SettingsFileExporterPDFPSWidget>
0091 #include <ValueListModel>
0092 #include "logging_part.h"
0093 
0094 class KBibTeXPart::KBibTeXPartPrivate
0095 {
0096 private:
0097     KBibTeXPart *p;
0098 
0099     /**
0100      * Modifies a given URL to become a "backup" filename/URL.
0101      * A backup level or 0 or less does not modify the URL.
0102      * A backup level of 1 appends a '~' (tilde) to the URL's filename.
0103      * A backup level of 2 or more appends '~N', where N is the level.
0104      * The provided URL will be modified in the process. It is assumed
0105      * that the URL is not yet a "backup URL".
0106      */
0107     void constructBackupUrl(const int level, QUrl &url) const {
0108         if (level <= 0)
0109             /// No modification
0110             return;
0111         else if (level == 1)
0112             /// Simply append '~' to the URL's filename
0113             url.setPath(url.path() + QStringLiteral("~"));
0114         else
0115             /// Append '~' followed by a number to the filename
0116             url.setPath(url.path() + QString(QStringLiteral("~%1")).arg(level));
0117     }
0118 
0119 public:
0120     enum FileScope { scopeAllElements, scopeSelectedElements };
0121 
0122     File *bibTeXFile;
0123     PartWidget *partWidget;
0124     FileModel *model;
0125     SortFilterFileModel *sortFilterProxyModel;
0126     QAction *editCutAction, *editDeleteAction, *editCopyAction, *editPasteAction, *editCopyReferencesAction, *elementEditAction, *elementViewDocumentAction, *fileSaveAction, *elementFindPDFAction, *entryApplyDefaultFormatString;
0127     QShortcut *updateViewDocumentShortcut;
0128     QMenu *viewDocumentMenu;
0129     bool isSaveAsOperation;
0130     LyX *lyx;
0131     FindDuplicatesUI *findDuplicatesUI;
0132     ColorLabelContextMenu *colorLabelContextMenu;
0133     QAction *colorLabelContextMenuAction;
0134     QFileSystemWatcher fileSystemWatcher;
0135 
0136     KBibTeXPartPrivate(QWidget *parentWidget, KBibTeXPart *parent)
0137             : p(parent), bibTeXFile(nullptr), model(nullptr), sortFilterProxyModel(nullptr), viewDocumentMenu(new QMenu(i18n("View Document"), parent->widget())), isSaveAsOperation(false), fileSystemWatcher(p) {
0138         connect(&fileSystemWatcher, &QFileSystemWatcher::fileChanged, p, &KBibTeXPart::fileExternallyChange);
0139 
0140         partWidget = new PartWidget(parentWidget);
0141         partWidget->fileView()->setReadOnly(!p->isReadWrite());
0142         connect(partWidget->fileView(), &FileView::modified, p, &KBibTeXPart::setModified);
0143 
0144         setupActions();
0145     }
0146 
0147     ~KBibTeXPartPrivate() {
0148         delete bibTeXFile;
0149         delete model;
0150         delete viewDocumentMenu;
0151         delete findDuplicatesUI;
0152     }
0153 
0154 
0155     void setupActions()
0156     {
0157         /// "Save" action
0158         fileSaveAction = p->actionCollection()->addAction(KStandardAction::Save);
0159         connect(fileSaveAction, &QAction::triggered, p, &KBibTeXPart::documentSave);
0160         fileSaveAction->setEnabled(false);
0161         QAction *action = p->actionCollection()->addAction(KStandardAction::SaveAs);
0162         connect(action, &QAction::triggered, p, &KBibTeXPart::documentSaveAs);
0163         /// "Save copy as" action
0164         QAction *saveCopyAsAction = new QAction(QIcon::fromTheme(QStringLiteral("document-save")), i18n("Save Copy As..."), p);
0165         p->actionCollection()->addAction(QStringLiteral("file_save_copy_as"), saveCopyAsAction);
0166         connect(saveCopyAsAction, &QAction::triggered, p, &KBibTeXPart::documentSaveCopyAs);
0167         /// "Save selection" action
0168         QAction *saveSelectionAction = new QAction(QIcon::fromTheme(QStringLiteral("document-save")), i18n("Save Selection..."), p);
0169         p->actionCollection()->addAction(QStringLiteral("file_save_selection"), saveSelectionAction);
0170         connect(saveSelectionAction, &QAction::triggered, p, &KBibTeXPart::documentSaveSelection);
0171         /// Enable "save selection" action only if there is something selected
0172         connect(partWidget->fileView(), &BasicFileView::hasSelectionChanged, saveSelectionAction, &QAction::setEnabled);
0173         saveSelectionAction->setEnabled(false);
0174 
0175         /// Filter bar widget
0176         QAction *filterWidgetAction = new QAction(i18n("Filter"), p);
0177         p->actionCollection()->addAction(QStringLiteral("toolbar_filter_widget"), filterWidgetAction);
0178         filterWidgetAction->setIcon(QIcon::fromTheme(QStringLiteral("view-filter")));
0179         p->actionCollection()->setDefaultShortcut(filterWidgetAction, Qt::CTRL | Qt::Key_F);
0180         connect(filterWidgetAction, &QAction::triggered, partWidget->filterBar(), static_cast<void(QWidget::*)()>(&QWidget::setFocus));
0181         partWidget->filterBar()->setPlaceholderText(i18n("Filter bibliographic entries (%1)", filterWidgetAction->shortcut().toString()));
0182 
0183         /// Actions for creating new elements (entries, macros, ...)
0184         KActionMenu *newElementAction = new KActionMenu(QIcon::fromTheme(QStringLiteral("address-book-new")), i18n("New element"), p);
0185         p->actionCollection()->addAction(QStringLiteral("element_new"), newElementAction);
0186         QMenu *newElementMenu = new QMenu(newElementAction->text(), p->widget());
0187         newElementAction->setMenu(newElementMenu);
0188         connect(newElementAction, &QAction::triggered, p, &KBibTeXPart::newEntryTriggered);
0189 
0190         QAction *newEntry = new QAction(QIcon::fromTheme(QStringLiteral("address-book-new")), i18n("New entry"), newElementAction);
0191         newElementMenu->addAction(newEntry);
0192         p->actionCollection()->setDefaultShortcut(newEntry, Qt::CTRL | Qt::SHIFT | Qt::Key_N);
0193         connect(newEntry, &QAction::triggered, p, &KBibTeXPart::newEntryTriggered);
0194 
0195         QAction *newComment = new QAction(QIcon::fromTheme(QStringLiteral("address-book-new")), i18n("New comment"), newElementAction);
0196         newElementMenu->addAction(newComment);
0197         connect(newComment, &QAction::triggered, p, &KBibTeXPart::newCommentTriggered);
0198 
0199         QAction *newMacro = new QAction(QIcon::fromTheme(QStringLiteral("address-book-new")), i18n("New macro"), newElementAction);
0200         newElementMenu->addAction(newMacro);
0201         connect(newMacro, &QAction::triggered, p, &KBibTeXPart::newMacroTriggered);
0202 
0203         QAction *newPreamble = new QAction(QIcon::fromTheme(QStringLiteral("address-book-new")), i18n("New preamble"), newElementAction);
0204         newElementMenu->addAction(newPreamble);
0205         connect(newPreamble, &QAction::triggered, p, &KBibTeXPart::newPreambleTriggered);
0206 
0207         /// Action to edit an element
0208         elementEditAction = new QAction(QIcon::fromTheme(QStringLiteral("document-edit")), i18n("Edit Element"), p);
0209         p->actionCollection()->addAction(QStringLiteral("element_edit"), elementEditAction);
0210         p->actionCollection()->setDefaultShortcut(elementEditAction, Qt::CTRL | Qt::Key_E);
0211         connect(elementEditAction, &QAction::triggered, partWidget->fileView(), &FileView::editCurrentElement);
0212 
0213         /// Action to view the document associated to the current element
0214         elementViewDocumentAction = new QAction(QIcon::fromTheme(QStringLiteral("application-pdf")), i18n("View Document"), p);
0215         p->actionCollection()->addAction(QStringLiteral("element_viewdocument"), elementViewDocumentAction);
0216         p->actionCollection()->setDefaultShortcut(elementViewDocumentAction, Qt::CTRL | Qt::Key_D);
0217         connect(elementViewDocumentAction, &QAction::triggered, p, &KBibTeXPart::elementViewDocument);
0218 
0219         // Shortcut complementing elementViewDocumentAction; to be enabled if elementViewDocumentAction is disabled and vice versa
0220         // Triggers a check if elementViewDocumentAction should be enabled and then opens a document if so
0221         updateViewDocumentShortcut = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_D), partWidget);
0222         updateViewDocumentShortcut->setEnabled(false);
0223         connect(updateViewDocumentShortcut, &QShortcut::activated, p, [this]() {
0224             // Update menu items for opening documents associated with an entry
0225             updateViewDocumentMenu();
0226             // Show 'best' document. If no document is available, this function will nothing
0227             p->elementViewDocument();
0228         });
0229 
0230         /// Action to find a PDF matching the current element
0231         elementFindPDFAction = new QAction(QIcon::fromTheme(QStringLiteral("application-pdf")), i18n("Find PDF..."), p);
0232         p->actionCollection()->addAction(QStringLiteral("element_findpdf"), elementFindPDFAction);
0233         connect(elementFindPDFAction, &QAction::triggered, p, &KBibTeXPart::elementFindPDF);
0234 
0235         /// Action to reformat the selected elements' ids
0236         entryApplyDefaultFormatString = new QAction(QIcon::fromTheme(QStringLiteral("favorites")), i18n("Format entry ids"), p);
0237         p->actionCollection()->addAction(QStringLiteral("entry_applydefaultformatstring"), entryApplyDefaultFormatString);
0238         connect(entryApplyDefaultFormatString, &QAction::triggered, p, &KBibTeXPart::applyDefaultFormatString);
0239 
0240         /// Clipboard object, required for various copy&paste operations
0241         Clipboard *clipboard = new Clipboard(partWidget->fileView());
0242 
0243         /// Actions to cut and copy selected elements as BibTeX code
0244         editCutAction = p->actionCollection()->addAction(KStandardAction::Cut);
0245         connect(editCutAction, &QAction::triggered, clipboard, &Clipboard::cut);
0246         editCopyAction = p->actionCollection()->addAction(KStandardAction::Copy);
0247         connect(editCopyAction, &QAction::triggered, clipboard, &Clipboard::copy);
0248 
0249         /// Action to copy references, e.g. '\\cite{fordfulkerson1959}'
0250         editCopyReferencesAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Copy References"), p);
0251         p->actionCollection()->setDefaultShortcut(editCopyReferencesAction, Qt::CTRL | Qt::SHIFT | Qt::Key_C);
0252         p->actionCollection()->addAction(QStringLiteral("edit_copy_references"), editCopyReferencesAction);
0253         connect(editCopyReferencesAction, &QAction::triggered, clipboard, &Clipboard::copyReferences);
0254 
0255         /// Action to paste BibTeX code
0256         action = editPasteAction = p->actionCollection()->addAction(KStandardAction::Paste);
0257         connect(action, &QAction::triggered, clipboard, &Clipboard::paste);
0258 
0259         /// Action to delete selected rows/elements
0260         editDeleteAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-table-delete-row")), i18n("Delete"), p);
0261         p->actionCollection()->setDefaultShortcut(editDeleteAction, Qt::Key_Delete);
0262         p->actionCollection()->addAction(QStringLiteral("edit_delete"), editDeleteAction);
0263         connect(editDeleteAction, &QAction::triggered, partWidget->fileView(), &FileView::selectionDelete);
0264 
0265         /// Build context menu for central BibTeX file view
0266         partWidget->fileView()->setContextMenuPolicy(Qt::DefaultContextMenu);
0267         connect(partWidget->fileView(), &FileView::contextMenuTriggered, p, [this](QContextMenuEvent * event) {
0268             // Update menu items for opening documents associated with an entry
0269             updateViewDocumentMenu();
0270             // Assemble context menu from actions that were associated with this QWidget
0271             // using the same mechanism as if policy Qt::ActionsContextMenu was used
0272             QMenu menu(partWidget);
0273             for (QAction *action : partWidget->fileView()->actions())
0274                 menu.addAction(action);
0275             menu.exec(event->globalPos());
0276         });
0277         partWidget->fileView()->addAction(elementEditAction);
0278         partWidget->fileView()->addAction(elementViewDocumentAction);
0279         QAction *separator = new QAction(p);
0280         separator->setSeparator(true);
0281         partWidget->fileView()->addAction(separator);
0282         partWidget->fileView()->addAction(editCutAction);
0283         partWidget->fileView()->addAction(editCopyAction);
0284         partWidget->fileView()->addAction(editCopyReferencesAction);
0285         partWidget->fileView()->addAction(editPasteAction);
0286         partWidget->fileView()->addAction(editDeleteAction);
0287         separator = new QAction(p);
0288         separator->setSeparator(true);
0289         partWidget->fileView()->addAction(separator);
0290         partWidget->fileView()->addAction(elementFindPDFAction);
0291         partWidget->fileView()->addAction(entryApplyDefaultFormatString);
0292         colorLabelContextMenu = new ColorLabelContextMenu(partWidget->fileView());
0293         colorLabelContextMenuAction = p->actionCollection()->addAction(QStringLiteral("entry_colorlabel"), colorLabelContextMenu->menuAction());
0294 
0295         findDuplicatesUI = new FindDuplicatesUI(p, partWidget->fileView());
0296         lyx = new LyX(p, partWidget->fileView());
0297 
0298         connect(partWidget->fileView(), &FileView::selectedElementsChanged, p, &KBibTeXPart::updateActions);
0299         connect(partWidget->fileView(), &FileView::currentElementChanged, p, &KBibTeXPart::updateActions);
0300     }
0301 
0302     QString findUnusedId() {
0303         int i = 1;
0304         while (true) {
0305             QString result = i18n("New%1", i);
0306             if (!bibTeXFile->containsKey(result))
0307                 return result;
0308             ++i;
0309         }
0310     }
0311 
0312     void initializeNew() {
0313         bibTeXFile = new File();
0314         model = new FileModel();
0315         model->setBibliographyFile(bibTeXFile);
0316 
0317         if (sortFilterProxyModel != nullptr) delete sortFilterProxyModel;
0318         sortFilterProxyModel = new SortFilterFileModel(p);
0319         sortFilterProxyModel->setSourceModel(model);
0320         partWidget->fileView()->setModel(sortFilterProxyModel);
0321         connect(partWidget->filterBar(), &FilterBar::filterChanged, sortFilterProxyModel, &SortFilterFileModel::updateFilter);
0322         p->setModified(false);
0323     }
0324 
0325     bool openFile(const QUrl &url, const QString &localFilePath) {
0326         p->setObjectName(QString(QStringLiteral("KBibTeXPart::KBibTeXPart for '%1' aka '%2'")).arg(url.toDisplayString(), localFilePath));
0327 
0328         qApp->setOverrideCursor(Qt::WaitCursor);
0329 
0330         if (bibTeXFile != nullptr) {
0331             const QUrl oldUrl = bibTeXFile->property(File::Url, QUrl()).toUrl();
0332             if (oldUrl.isValid() && oldUrl.isLocalFile()) {
0333                 const QString path = oldUrl.toLocalFile();
0334                 if (!path.isEmpty())
0335                     fileSystemWatcher.removePath(path);
0336                 else
0337                     qCWarning(LOG_KBIBTEX_PART) << "No filename to stop watching";
0338             }
0339             delete bibTeXFile;
0340             bibTeXFile = nullptr;
0341         }
0342 
0343         QFile inputfile(localFilePath);
0344         if (!inputfile.open(QIODevice::ReadOnly)) {
0345             qCWarning(LOG_KBIBTEX_PART) << "Opening file failed, creating new one instead:" << url.toDisplayString() << "aka" << localFilePath;
0346             qApp->restoreOverrideCursor();
0347             /// Opening file failed, creating new one instead
0348             initializeNew();
0349             return false;
0350         }
0351 
0352         FileImporter *importer = FileImporter::factory(url, p);
0353         importer->showImportDialog(p->widget());
0354         bibTeXFile = importer->load(&inputfile);
0355         inputfile.close();
0356         delete importer;
0357 
0358         if (bibTeXFile == nullptr) {
0359             qCWarning(LOG_KBIBTEX_PART) << "Opening file failed, creating new one instead:" << url.toDisplayString() << "aka" << localFilePath;
0360             qApp->restoreOverrideCursor();
0361             /// Opening file failed, creating new one instead
0362             initializeNew();
0363             return false;
0364         }
0365 
0366         bibTeXFile->setProperty(File::Url, QUrl(url));
0367 
0368         model->setBibliographyFile(bibTeXFile);
0369         if (sortFilterProxyModel != nullptr) delete sortFilterProxyModel;
0370         sortFilterProxyModel = new SortFilterFileModel(p);
0371         sortFilterProxyModel->setSourceModel(model);
0372         partWidget->fileView()->setModel(sortFilterProxyModel);
0373         connect(partWidget->filterBar(), &FilterBar::filterChanged, sortFilterProxyModel, &SortFilterFileModel::updateFilter);
0374 
0375         if (url.isLocalFile())
0376             fileSystemWatcher.addPath(url.toLocalFile());
0377 
0378         p->setModified(false);
0379         qApp->restoreOverrideCursor();
0380 
0381         return true;
0382     }
0383 
0384     void makeBackup(const QUrl &url) const {
0385         /// Fetch settings from configuration
0386         const int numberOfBackups = Preferences::instance().numberOfBackups();
0387 
0388         /// Stop right here if no backup is requested
0389         if (Preferences::instance().backupScope() == Preferences::BackupScope::None)
0390             return;
0391 
0392         /// For non-local files, proceed only if backups to remote storage is allowed
0393         if (Preferences::instance().backupScope() != Preferences::BackupScope::BothLocalAndRemote && !url.isLocalFile())
0394             return;
0395 
0396         /// Do not make backup copies if destination file does not exist yet
0397 #if KIO_VERSION >= QT_VERSION_CHECK(5, 240, 0) // >= 5.240.0
0398         KIO::StatJob *statJob = KIO::stat(url, KIO::StatJob::DestinationSide, KIO::StatNoDetails /** not details necessary, just need to know if file exists */, KIO::HideProgressInfo);
0399 #elif KIO_VERSION >= QT_VERSION_CHECK(5, 69, 0) // >= 5.69.0
0400         KIO::StatJob *statJob = KIO::statDetails(url, KIO::StatJob::DestinationSide, KIO::StatNoDetails /** not details necessary, just need to know if file exists */, KIO::HideProgressInfo);
0401 #else // KIO_VERSION < 0x054500 // < 5.69.0
0402         KIO::StatJob *statJob = KIO::stat(url, KIO::StatJob::DestinationSide, 0 /** not details necessary, just need to know if file exists */, KIO::HideProgressInfo);
0403 #endif // KIO_VERSION
0404         KJobWidgets::setWindow(statJob, p->widget());
0405         statJob->exec();
0406         if (statJob->error() == KIO::ERR_DOES_NOT_EXIST)
0407             return;
0408         else if (statJob->error() != KIO::Job::NoError) {
0409             /// Something else went wrong, quit with error
0410             qCWarning(LOG_KBIBTEX_PART) << "Probing" << url.toDisplayString() << "failed:" << statJob->errorString();
0411             return;
0412         }
0413 
0414         bool copySucceeded = true;
0415         /// Copy e.g. test.bib~ to test.bib~2, test.bib to test.bib~ etc.
0416         for (int level = numberOfBackups; copySucceeded && level >= 1; --level) {
0417             QUrl newerBackupUrl = url;
0418             constructBackupUrl(level - 1, newerBackupUrl);
0419             QUrl olderBackupUrl = url;
0420             constructBackupUrl(level, olderBackupUrl);
0421 
0422 #if KIO_VERSION >= QT_VERSION_CHECK(5, 240, 0) // >= 5.240.0
0423             statJob = KIO::stat(newerBackupUrl, KIO::StatJob::DestinationSide, KIO::StatNoDetails /** not details necessary, just need to know if file exists */, KIO::HideProgressInfo);
0424 #elif KIO_VERSION >= QT_VERSION_CHECK(5, 69, 0) // >= 5.69.0
0425             statJob = KIO::statDetails(newerBackupUrl, KIO::StatJob::DestinationSide, KIO::StatNoDetails /** not details necessary, just need to know if file exists */, KIO::HideProgressInfo);
0426 #else // KIO_VERSION < 0x054500 // < 5.69.0
0427             statJob = KIO::stat(newerBackupUrl, KIO::StatJob::DestinationSide, 0 /** not details necessary, just need to know if file exists */, KIO::HideProgressInfo);
0428 #endif // KIO_VERSION
0429             KJobWidgets::setWindow(statJob, p->widget());
0430             if (statJob->exec() && statJob->error() == KIO::Job::NoError) {
0431                 KIO::CopyJob *moveJob = nullptr; ///< guaranteed to be initialized in either branch of the following code
0432                 /**
0433                  * The following 'if' block is necessary to handle the
0434                  * following situation: User opens, modifies, and saves
0435                  * file /tmp/b/bbb.bib which is actually a symlink to
0436                  * file /tmp/a/aaa.bib. Now a 'move' operation like the
0437                  * implicit 'else' section below does, would move /tmp/b/bbb.bib
0438                  * to become /tmp/b/bbb.bib~ still pointing to /tmp/a/aaa.bib.
0439                  * Then, the save operation would create a new file /tmp/b/bbb.bib
0440                  * without any symbolic linking to /tmp/a/aaa.bib.
0441                  * The following code therefore checks if /tmp/b/bbb.bib is
0442                  * to be copied/moved to /tmp/b/bbb.bib~ and /tmp/b/bbb.bib
0443                  * is a local file and /tmp/b/bbb.bib is a symbolic link to
0444                  * another file. Then /tmp/b/bbb.bib is resolved to the real
0445                  * file /tmp/a/aaa.bib which is then copied into plain file
0446                  * /tmp/b/bbb.bib~. The save function (outside of this function's
0447                  * scope) will then see that /tmp/b/bbb.bib is a symbolic link,
0448                  * resolve this symlink to /tmp/a/aaa.bib, and then write
0449                  * all changes to /tmp/a/aaa.bib keeping /tmp/b/bbb.bib a
0450                  * link to.
0451                  */
0452                 if (level == 1 && newerBackupUrl.isLocalFile() /** for level==1, this is actually the current file*/) {
0453                     QFileInfo newerBackupFileInfo(newerBackupUrl.toLocalFile());
0454                     if (newerBackupFileInfo.isSymLink()) {
0455                         while (newerBackupFileInfo.isSymLink()) {
0456                             newerBackupUrl = QUrl::fromLocalFile(newerBackupFileInfo.symLinkTarget());
0457                             newerBackupFileInfo = QFileInfo(newerBackupUrl.toLocalFile());
0458                         }
0459                         moveJob = KIO::copy(newerBackupUrl, olderBackupUrl, KIO::HideProgressInfo | KIO::Overwrite);
0460                     }
0461                 }
0462                 if (moveJob == nullptr) ///< implicit 'else' section, see longer comment above
0463                     moveJob = KIO::move(newerBackupUrl, olderBackupUrl, KIO::HideProgressInfo | KIO::Overwrite);
0464                 KJobWidgets::setWindow(moveJob, p->widget());
0465                 copySucceeded = moveJob->exec();
0466             }
0467         }
0468 
0469         if (!copySucceeded)
0470             KMessageBox::error(p->widget(), i18n("Could not create backup copies of document '%1'.", url.url(QUrl::PreferLocalFile)), i18n("Backup copies"));
0471     }
0472 
0473     /**
0474      * Options to tell @see getSaveFileame how to prepare the file-save dialog.
0475      */
0476     enum GetSaveFilenameOption {
0477         gsfoImportableFiletype = 0x01, ///< List of available mime types to save as may onlz include mime types which can be loaded/imported, too
0478         gsfoCurrentFilenameReused = 0x02 ///< Propose the current filename as the default suggestion (file extension may get adjusted, though)
0479     };
0480 
0481     /**
0482      * Prepare and show a file-save dialog to the user. The dialog is prepared
0483      * as requested by the options, which are an OR combination of flags from
0484      * @see GetSaveFilenameOption
0485      * @param options OR combination of flags from @see GetSaveFilenameOption enum
0486      * @return Valid URL if querying filename in file-save dialog succeeded, invalid URL otherwise
0487      */
0488     QUrl getSaveFilename(const int options) const {
0489         const QString startDir = p->url().isValid() ? ((options & GetSaveFilenameOption::gsfoCurrentFilenameReused) > 0 ? p->url().path() : QFileInfo(p->url().path()).absolutePath()) : QString();
0490         QString supportedMimeTypes = QStringLiteral("text/x-bibtex text/x-research-info-systems");
0491         if (BibUtils::available())
0492             supportedMimeTypes += QStringLiteral(" application/x-isi-export-format application/x-endnote-refer");
0493         if ((options & GetSaveFilenameOption::gsfoImportableFiletype) == 0) {
0494             if (!QStandardPaths::findExecutable(QStringLiteral("pdflatex")).isEmpty())
0495                 supportedMimeTypes += QStringLiteral(" application/pdf");
0496             if (!QStandardPaths::findExecutable(QStringLiteral("dvips")).isEmpty())
0497                 supportedMimeTypes += QStringLiteral(" application/postscript");
0498             supportedMimeTypes += QStringLiteral(" text/html application/xml");
0499             if (!QStandardPaths::findExecutable(QStringLiteral("latex2rtf")).isEmpty())
0500                 supportedMimeTypes += QStringLiteral(" application/rtf");
0501         }
0502 
0503         QPointer<QFileDialog> saveDlg = new QFileDialog(p->widget(), i18n("Save file") /* TODO better text */, startDir, supportedMimeTypes);
0504         /// Setting list of mime types for the second time,
0505         /// essentially calling this function only to set the "default mime type" parameter
0506 #if QT_VERSION >= 0x050e00
0507         saveDlg->setMimeTypeFilters(supportedMimeTypes.split(QLatin1Char(' '), Qt::SkipEmptyParts));
0508 #else // QT_VERSION < 0x050e00
0509         saveDlg->setMimeTypeFilters(supportedMimeTypes.split(QLatin1Char(' '), QString::SkipEmptyParts));
0510 #endif // QT_VERSION >= 0x050e00
0511         /// Setting the dialog into "Saving" mode make the "add extension" checkbox available
0512         saveDlg->setAcceptMode(QFileDialog::AcceptSave);
0513         /// Mime type 'text/x-bibtex' is guaranteed to be pre-selected, so set default filename suffix accordingly
0514         saveDlg->setDefaultSuffix(QStringLiteral("bib"));
0515         saveDlg->setFileMode(QFileDialog::AnyFile);
0516         if (saveDlg->exec() != QDialog::Accepted)
0517             /// User cancelled saving operation, return invalid filename/URL
0518             return QUrl();
0519         const QList<QUrl> selectedUrls = saveDlg->selectedUrls();
0520         delete saveDlg;
0521         return selectedUrls.isEmpty() ? QUrl() : selectedUrls.first();
0522     }
0523 
0524     QString chooseExporterClass(const QUrl &url, const QVector<QString> &availableExporters) {
0525         if (availableExporters.isEmpty())
0526             return QString();
0527         else if (availableExporters.size() == 1)
0528             return *availableExporters.begin();
0529         else {
0530             // Fancy short descriptions for exporter classes
0531             static const QHash<QString, QString> exporterDescription{{QStringLiteral("FileExporterBibUtils"), i18n("BibUtils")},
0532                 {QStringLiteral("FileExporterBibTeX2HTML"), i18n("BibTeX2HTML")},
0533                 {QStringLiteral("FileExporterWordBibXML"), i18n("Word XML bibliography")},
0534                 {QStringLiteral("FileExporterXML"), i18n("KBibTeX's own XML structure")},
0535                 {QStringLiteral("FileExporterHTML"), i18n("KBibTeX's built-in HTML exporter")},
0536                 {QStringLiteral("FileExporterRIS"), i18n("KBibTeX's built-in RIS exporter")}
0537             };
0538             QStringList choices;
0539             choices.reserve(availableExporters.length());
0540             for (const QString &exporterName : availableExporters)
0541                 if (exporterDescription.contains(exporterName))
0542                     choices.append(exporterDescription[exporterName]);
0543 
0544             QPointer<QDialog> dlg = new QDialog(p->widget());
0545             dlg->setWindowTitle(i18nc("@title:window", "Choose Exporter"));
0546             QBoxLayout *layout = new QVBoxLayout(dlg);
0547             QLabel *label = new QLabel(i18n("Several exporter modules are available to write file '%1'.\n\nPlease choose one.", url.fileName()), dlg);
0548             layout->addWidget(label);
0549             QListWidget *list = new QListWidget(dlg);
0550             layout->addWidget(list);
0551             list->addItems(choices);
0552             list->setSelectionMode(QAbstractItemView::SingleSelection);
0553             list->setCurrentRow(0, QItemSelectionModel::Current);
0554             list->item(0)->setSelected(true);
0555             QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok, Qt::Horizontal, dlg);
0556             buttonBox->button(QDialogButtonBox::Ok)->setDefault(true);
0557             layout->addWidget(buttonBox);
0558             connect(buttonBox->button(QDialogButtonBox::Ok), &QPushButton::clicked, dlg.data(), &QDialog::accept);
0559 
0560             const QString result{dlg->exec() == QDialog::Accepted && !dlg.isNull()
0561                                  ? exporterDescription.key(list->currentItem()->text()) //<Map back from exporter description to exporter class name
0562                                  :*availableExporters.begin()
0563                                 };
0564             delete dlg;
0565             return result;
0566         }
0567     }
0568 
0569     FileExporter *saveFileExporter(const QUrl &url) {
0570         FileExporter *exporter = FileExporter::factory(url, chooseExporterClass(url, FileExporter::exporterClasses(url)), p);
0571 
0572         if (isSaveAsOperation) {
0573             /// only show export dialog at SaveAs or SaveCopyAs operations
0574             FileExporterToolchain *fet = nullptr;
0575 
0576             if (FileExporterBibTeX::isFileExporterBibTeX(*exporter)) {
0577                 QPointer<QDialog> dlg = new QDialog(p->widget());
0578                 dlg->setWindowTitle(i18nc("@title:window", "BibTeX File Settings"));
0579                 QBoxLayout *layout = new QVBoxLayout(dlg);
0580                 FileSettingsWidget *settingsWidget = new FileSettingsWidget(dlg);
0581                 layout->addWidget(settingsWidget);
0582                 QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::RestoreDefaults | QDialogButtonBox::Reset | QDialogButtonBox::Ok, Qt::Horizontal, dlg);
0583                 buttonBox->button(QDialogButtonBox::Ok)->setDefault(true);
0584                 layout->addWidget(buttonBox);
0585                 connect(buttonBox->button(QDialogButtonBox::RestoreDefaults), &QPushButton::clicked, settingsWidget, &FileSettingsWidget::resetToDefaults);
0586                 connect(buttonBox->button(QDialogButtonBox::Reset), &QPushButton::clicked, settingsWidget, &FileSettingsWidget::resetToLoadedProperties);
0587                 connect(buttonBox->button(QDialogButtonBox::Ok), &QPushButton::clicked, dlg.data(), &QDialog::accept);
0588 
0589                 settingsWidget->loadProperties(bibTeXFile);
0590 
0591                 if (dlg->exec() == QDialog::Accepted)
0592                     settingsWidget->saveProperties(bibTeXFile);
0593                 delete dlg;
0594             } else if ((fet = qobject_cast<FileExporterToolchain *>(exporter)) != nullptr) {
0595                 QPointer<QDialog> dlg = new QDialog(p->widget());
0596                 dlg->setWindowTitle(i18nc("@title:window", "PDF/PostScript File Settings"));
0597                 QBoxLayout *layout = new QVBoxLayout(dlg);
0598                 SettingsFileExporterPDFPSWidget *settingsWidget = new SettingsFileExporterPDFPSWidget(dlg);
0599                 layout->addWidget(settingsWidget);
0600                 QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::RestoreDefaults | QDialogButtonBox::Reset | QDialogButtonBox::Ok, Qt::Horizontal, dlg);
0601                 layout->addWidget(buttonBox);
0602                 connect(buttonBox->button(QDialogButtonBox::RestoreDefaults), &QPushButton::clicked, settingsWidget, &SettingsFileExporterPDFPSWidget::resetToDefaults);
0603                 connect(buttonBox->button(QDialogButtonBox::Reset), &QPushButton::clicked, settingsWidget, &SettingsFileExporterPDFPSWidget::loadState);
0604                 connect(buttonBox->button(QDialogButtonBox::Ok), &QPushButton::clicked, dlg.data(), &QDialog::accept);
0605 
0606                 if (dlg->exec() == QDialog::Accepted)
0607                     settingsWidget->saveState();
0608                 delete dlg;
0609             }
0610         }
0611 
0612         return exporter;
0613     }
0614 
0615     bool saveFile(QFile &file, const FileScope fileScope, FileExporter *exporter) {
0616         SortFilterFileModel *model = qobject_cast<SortFilterFileModel *>(partWidget->fileView()->model());
0617         Q_ASSERT_X(model != nullptr, "FileExporter *KBibTeXPart::KBibTeXPartPrivate:saveFile(...)", "SortFilterFileModel *model from editor->model() is invalid");
0618         Q_ASSERT_X(model->fileSourceModel()->bibliographyFile() == bibTeXFile, "FileExporter *KBibTeXPart::KBibTeXPartPrivate:saveFile(...)", "SortFilterFileModel's BibTeX File does not match Part's BibTeX File");
0619 
0620         switch (fileScope) {
0621         case scopeAllElements: {
0622             /// Save complete file
0623             return exporter->save(&file, bibTeXFile);
0624         } ///< no break required as there is an unconditional 'return' further above
0625         case scopeSelectedElements: {
0626             /// Save only selected elements
0627             const auto &list = partWidget->fileView()->selectionModel()->selectedRows();
0628             if (list.isEmpty()) return false; /// Empty selection? Abort here
0629 
0630             File fileWithSelectedElements;
0631             for (const QModelIndex &indexInSelection : list) {
0632                 const QModelIndex &indexInFileModel = model->mapToSource(indexInSelection);
0633                 const int row = indexInFileModel.row();
0634                 const QSharedPointer<Element> &element = (*bibTeXFile)[row];
0635                 fileWithSelectedElements << element;
0636             }
0637             return exporter->save(&file, &fileWithSelectedElements);
0638         } ///< no break required as there is an unconditional 'return' further above
0639         }
0640 
0641         /// Above switch should cover all cases and each case should
0642         /// invoke 'return'. Thus, this code here should never be reached.
0643         return false;
0644     }
0645 
0646     bool saveFile(const QUrl &url, const FileScope &fileScope = scopeAllElements) {
0647         bool result = false;
0648         Q_ASSERT_X(url.isValid(), "bool KBibTeXPart::KBibTeXPartPrivate:saveFile(const QUrl &url, const FileScope&)", "url must be valid");
0649 
0650         /// Extract filename extension (e.g. 'bib') to determine which FileExporter to use
0651         FileExporter *exporter = saveFileExporter(url);
0652         QStringList errorLog;
0653         QObject::connect(exporter, &FileExporter::message, p, [&errorLog](const FileExporter::MessageSeverity severity, const QString & messageText) {
0654             if (severity >= FileExporter::MessageSeverity::Warning)
0655                 errorLog.append(messageText);
0656         });
0657 
0658         qApp->setOverrideCursor(Qt::WaitCursor);
0659 
0660         if (url.isLocalFile()) {
0661             /// Take precautions for local files
0662             QFileInfo fileInfo(url.toLocalFile());
0663             /// Do not overwrite symbolic link, but linked file instead
0664             QString filename = fileInfo.absoluteFilePath();
0665             while (fileInfo.isSymLink()) {
0666                 filename = fileInfo.symLinkTarget();
0667                 fileInfo = QFileInfo(filename);
0668             }
0669             if (!fileInfo.exists() || fileInfo.isWritable()) {
0670                 /// Make backup before overwriting target destination, intentionally
0671                 /// using the provided filename, not the resolved symlink
0672                 makeBackup(url);
0673 
0674                 QFile file(filename);
0675                 if (file.open(QIODevice::WriteOnly)) {
0676                     result = saveFile(file, fileScope, exporter);
0677                     file.close();
0678                     if (!result)
0679                         qCWarning(LOG_KBIBTEX_PART) << "Could not write bibliographic data to file.";
0680                 } else
0681                     qCWarning(LOG_KBIBTEX_PART) << QString(QStringLiteral("Could not open local file '%1' for writing.")).arg(filename);
0682             }
0683         } else {
0684             /// URL points to a remote location
0685 
0686             /// Configure and open temporary file
0687             const QString ending = QFileInfo(url.fileName()).completeSuffix();
0688             QTemporaryFile temporaryFile(QStandardPaths::writableLocation(QStandardPaths::TempLocation) + QDir::separator() + QStringLiteral("kbibtex_savefile_XXXXXX") + ending);
0689             temporaryFile.setAutoRemove(true);
0690             if (temporaryFile.open()) {
0691                 result = saveFile(temporaryFile, fileScope, exporter);
0692 
0693                 /// Close/flush temporary file
0694                 temporaryFile.close();
0695 
0696                 if (result) {
0697                     /// Make backup before overwriting target destination
0698                     makeBackup(url);
0699 
0700                     KIO::CopyJob *copyJob = KIO::copy(QUrl::fromLocalFile(temporaryFile.fileName()), url, KIO::HideProgressInfo | KIO::Overwrite);
0701                     KJobWidgets::setWindow(copyJob, p->widget());
0702                     result &= copyJob->exec() && copyJob->error() == KIO::Job::NoError;
0703                     if (!result)
0704                         qCWarning(LOG_KBIBTEX_PART) << QString(QStringLiteral("Failed to upload temporary file to final destination '%1'")).arg(url.toDisplayString());
0705                 } else
0706                     qCWarning(LOG_KBIBTEX_PART) << QStringLiteral("Could not write bibliographic data to temporary file.");
0707             } else
0708                 qCWarning(LOG_KBIBTEX_PART) << QStringLiteral("Could not open temporary file for writing.");
0709         }
0710 
0711         qApp->restoreOverrideCursor();
0712 
0713         delete exporter;
0714 
0715         if (!result) {
0716             QString msg = i18n("Saving the bibliography to file '%1' failed.", url.toDisplayString());
0717             if (errorLog.isEmpty())
0718                 KMessageBox::error(p->widget(), msg, i18n("Saving bibliography failed"));
0719             else if (errorLog.size() == 1)
0720                 KMessageBox::error(p->widget(), msg, i18n("Saving bibliography failed:") + QStringLiteral("\n\n") + errorLog.first());
0721             else {
0722                 msg += QLatin1String("\n\n");
0723                 msg += i18n("The following output was generated by the export filter:");
0724                 KMessageBox::errorList(p->widget(), msg, errorLog, i18n("Saving bibliography failed"));
0725             }
0726         } else
0727             bibTeXFile->setProperty(File::Url, url);
0728         return result;
0729     }
0730 
0731     /**
0732      * Builds or resets the menu with local and remote
0733      * references (URLs, files) of an entry.
0734      *
0735      * @return Number of known references
0736      */
0737     int updateViewDocumentMenu() {
0738         viewDocumentMenu->clear();
0739         int numDocumentsToView = 0; ///< Initially, no references are known
0740 
0741         /// Retrieve Entry object of currently selected line
0742         /// in main list view
0743         QSharedPointer<const Entry> entry = partWidget->fileView()->currentElement().dynamicCast<const Entry>();
0744         /// Test and continue if there was an Entry to retrieve
0745         if (!entry.isNull()) {
0746             /// Get list of URLs associated with this entry
0747             const auto urlList = FileInfo::entryUrls(entry, bibTeXFile->property(File::Url).toUrl(), FileInfo::TestExistence::Yes);
0748             if (!urlList.isEmpty()) {
0749                 /// Memorize first action, necessary to set menu title
0750                 QAction *firstAction = nullptr;
0751                 /// First iteration: local references only
0752                 for (const QUrl &url : urlList) {
0753                     /// First iteration: local references only
0754                     if (!url.isLocalFile()) continue; ///< skip remote URLs
0755 
0756                     /// Build a nice menu item (label, icon, ...)
0757                     const QFileInfo fi(url.toLocalFile());
0758                     const QString label = QString(QStringLiteral("%1 [%2]")).arg(fi.fileName(), fi.absolutePath());
0759                     QAction *action = new QAction(QIcon::fromTheme(FileInfo::mimeTypeForUrl(url).iconName()), label, p);
0760                     action->setData(QUrl::fromLocalFile(fi.absoluteFilePath()));
0761                     action->setToolTip(fi.absoluteFilePath());
0762                     /// Open URL when action is triggered
0763                     connect(action, &QAction::triggered, p, [this, fi]() {
0764                         elementViewDocumentMenu(QUrl::fromLocalFile(fi.absoluteFilePath()));
0765                     });
0766                     viewDocumentMenu->addAction(action);
0767                     /// Memorize first action
0768                     if (firstAction == nullptr) firstAction = action;
0769                 }
0770                 if (firstAction != nullptr) {
0771                     /// If there is 'first action', then there must be
0772                     /// local URLs (i.e. local files) and firstAction
0773                     /// is the first one where a title can be set above
0774                     viewDocumentMenu->insertSection(firstAction, i18n("Local Files"));
0775                 }
0776 
0777                 firstAction = nullptr; /// Now the first remote action is to be memorized
0778                 /// Second iteration: remote references only
0779                 for (const QUrl &url : urlList) {
0780                     if (url.isLocalFile()) continue; ///< skip local files
0781 
0782                     /// Build a nice menu item (label, icon, ...)
0783                     const QString prettyUrl = url.toDisplayString();
0784                     QAction *action = new QAction(QIcon::fromTheme(FileInfo::mimeTypeForUrl(url).iconName()), prettyUrl, p);
0785                     action->setData(url);
0786                     action->setToolTip(prettyUrl);
0787                     /// Open URL when action is triggered
0788                     connect(action, &QAction::triggered, p, [this, url]() {
0789                         elementViewDocumentMenu(url);
0790                     });
0791                     viewDocumentMenu->addAction(action);
0792                     /// Memorize first action
0793                     if (firstAction == nullptr) firstAction = action;
0794                 }
0795                 if (firstAction != nullptr) {
0796                     /// If there is 'first action', then there must be
0797                     /// some remote URLs and firstAction is the first
0798                     /// one where a title can be set above
0799                     viewDocumentMenu->insertSection(firstAction, i18n("Remote Files"));
0800                 }
0801 
0802                 numDocumentsToView = urlList.count();
0803             }
0804         }
0805 
0806         const bool emptySelection = partWidget->fileView()->selectedElements().isEmpty();
0807         // Enable menu item only if there is at least one document to view
0808         elementViewDocumentAction->setEnabled(!emptySelection && numDocumentsToView > 0);
0809         // Activate sub-menu only if there are at least two documents to view
0810         elementViewDocumentAction->setMenu(numDocumentsToView > 1 ? viewDocumentMenu : nullptr);
0811         elementViewDocumentAction->setToolTip(numDocumentsToView == 1 ? (*viewDocumentMenu->actions().constBegin())->text() : QString());
0812         updateViewDocumentShortcut->setEnabled(!elementViewDocumentAction->isEnabled());
0813 
0814         return numDocumentsToView;
0815     }
0816 
0817     void readConfiguration() {
0818         disconnect(partWidget->fileView(), &FileView::elementExecuted, partWidget->fileView(), &FileView::editElement);
0819         disconnect(partWidget->fileView(), &FileView::elementExecuted, p, &KBibTeXPart::elementViewDocument);
0820         switch (Preferences::instance().fileViewDoubleClickAction()) {
0821         case Preferences::FileViewDoubleClickAction::OpenEditor:
0822             connect(partWidget->fileView(), &FileView::elementExecuted, partWidget->fileView(), &FileView::editElement);
0823             break;
0824         case Preferences::FileViewDoubleClickAction::ViewDocument:
0825             connect(partWidget->fileView(), &FileView::elementExecuted, p, &KBibTeXPart::elementViewDocument);
0826             break;
0827         }
0828     }
0829 
0830     void elementViewDocumentMenu(const QUrl &url)
0831     {
0832         const QMimeType mimeType = FileInfo::mimeTypeForUrl(url);
0833         const QString mimeTypeName = mimeType.name();
0834         /// Ask KDE subsystem to open url in viewer matching mime type
0835 #if KIO_VERSION < QT_VERSION_CHECK(5, 71, 0)
0836         KRun::runUrl(url, mimeTypeName, p->widget(), KRun::RunFlags());
0837 #else // KIO_VERSION < QT_VERSION_CHECK(5, 71, 0) // >= 5.71.0
0838         KIO::OpenUrlJob *job = new KIO::OpenUrlJob(url, mimeTypeName);
0839 #if KIO_VERSION < QT_VERSION_CHECK(5, 98, 0) // < 5.98.0
0840         job->setUiDelegate(new KIO::JobUiDelegate());
0841 #else // KIO_VERSION < QT_VERSION_CHECK(5, 98, 0) // >= 5.98.0
0842         job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, p->widget()));
0843 #endif // KIO_VERSION < QT_VERSION_CHECK(5, 98, 0)
0844         job->start();
0845 #endif // KIO_VERSION < QT_VERSION_CHECK(5, 71, 0)
0846     }
0847 
0848 };
0849 
0850 KBibTeXPart::KBibTeXPart(QWidget *parentWidget, QObject *parent,
0851 #if KPARTS_VERSION >= 0x054D00 // >= 5.77.0
0852                          const KPluginMetaData &metaData
0853 #else // KPARTS_VERSION < 0x054D00 // < 5.77.0
0854                          const KAboutData &componentData
0855 #endif // KPARTS_VERSION >= 0x054D00 // >= 5.77.0
0856                          , const QVariantList &)
0857         : KParts::ReadWritePart(parent
0858 #if KPARTS_VERSION >= 0x05C800 // >= 5.200.0
0859                             , metaData
0860 #endif // KPARTS_VERSION >= 0x05C800 // >= 5.200.0
0861                            ), d(new KBibTeXPartPrivate(parentWidget, this))
0862 {
0863 #if KPARTS_VERSION <= 0x05C800 // <= 5.200.0
0864 #if KPARTS_VERSION >= 0x054D00 // >= 5.77.0
0865     setMetaData(metaData);
0866 #else // KPARTS_VERSION < 0x054D00 // < 5.77.0
0867     setComponentData(componentData);
0868 #endif // KPARTS_VERSION >= 0x054D00 // >= 5.77.0
0869 #endif // KPARTS_VERSION <= 0x05C800 // <= 5.200.0
0870 
0871     setWidget(d->partWidget);
0872     updateActions();
0873 
0874     d->initializeNew();
0875 
0876     setXMLFile(QStringLiteral("kbibtexpartui.rc"));
0877 
0878     NotificationHub::registerNotificationListener(this, NotificationHub::EventConfigurationChanged);
0879     d->readConfiguration();
0880 
0881     setModified(false);
0882 }
0883 
0884 KBibTeXPart::~KBibTeXPart()
0885 {
0886     delete d;
0887 }
0888 
0889 void KBibTeXPart::setModified(bool modified)
0890 {
0891     KParts::ReadWritePart::setModified(modified);
0892 
0893     d->fileSaveAction->setEnabled(modified);
0894 }
0895 
0896 void KBibTeXPart::notificationEvent(int eventId)
0897 {
0898     if (eventId == NotificationHub::EventConfigurationChanged)
0899         d->readConfiguration();
0900 }
0901 
0902 bool KBibTeXPart::saveFile()
0903 {
0904     Q_ASSERT_X(isReadWrite(), "bool KBibTeXPart::saveFile()", "Trying to save although document is in read-only mode");
0905 
0906     if (!url().isValid())
0907         return documentSaveAs();
0908 
0909     /// If the current file is "watchable" (i.e. a local file),
0910     /// memorize local filename for future reference
0911     const QString watchableFilename = url().isValid() && url().isLocalFile() ? url().toLocalFile() : QString();
0912     /// Stop watching local file that will be written to
0913     if (!watchableFilename.isEmpty())
0914         d->fileSystemWatcher.removePath(watchableFilename);
0915     else
0916         qCWarning(LOG_KBIBTEX_PART) << "watchableFilename is Empty";
0917 
0918     const bool saveOperationSuccess = d->saveFile(url());
0919 
0920     if (!watchableFilename.isEmpty()) {
0921         /// Continue watching a local file after write operation, but do
0922         /// so only after a short delay. The delay is necessary in some
0923         /// situations as observed in KDE bug report 396343 where the
0924         /// DropBox client seemingly touched the file right after saving
0925         /// from within KBibTeX, triggering KBibTeX to show a 'reload'
0926         /// message box.
0927         QTimer::singleShot(500, this, [this, watchableFilename]() {
0928             d->fileSystemWatcher.addPath(watchableFilename);
0929         });
0930     } else
0931         qCWarning(LOG_KBIBTEX_PART) << "watchableFilename is Empty";
0932 
0933     if (!saveOperationSuccess) {
0934         KMessageBox::error(widget(), i18n("The document could not be saved, as it was not possible to write to '%1'.\n\nCheck that you have write access to this file or that enough disk space is available.", url().toDisplayString()));
0935         return false;
0936     }
0937 
0938     return true;
0939 }
0940 
0941 bool KBibTeXPart::documentSave()
0942 {
0943     d->isSaveAsOperation = false;
0944     if (!isReadWrite())
0945         return documentSaveCopyAs();
0946     else if (!url().isValid())
0947         return documentSaveAs();
0948     else
0949         return KParts::ReadWritePart::save();
0950 }
0951 
0952 bool KBibTeXPart::documentSaveAs()
0953 {
0954     d->isSaveAsOperation = true;
0955     QUrl newUrl = d->getSaveFilename(KBibTeXPartPrivate::gsfoCurrentFilenameReused | KBibTeXPartPrivate::gsfoImportableFiletype);
0956     if (!newUrl.isValid())
0957         return false;
0958 
0959     /// Remove old URL from file system watcher
0960     if (url().isValid() && url().isLocalFile()) {
0961         const QString path = url().toLocalFile();
0962         if (!path.isEmpty())
0963             d->fileSystemWatcher.removePath(path);
0964         else
0965             qCWarning(LOG_KBIBTEX_PART) << "No filename to stop watching";
0966     } else
0967         qCWarning(LOG_KBIBTEX_PART) << "Not removing" << url().url(QUrl::PreferLocalFile) << "from fileSystemWatcher";
0968 
0969     // TODO how does SaveAs dialog know which mime types to support?
0970     if (KParts::ReadWritePart::saveAs(newUrl))
0971         return true;
0972     else
0973         return false;
0974 }
0975 
0976 bool KBibTeXPart::documentSaveCopyAs()
0977 {
0978     d->isSaveAsOperation = true;
0979     QUrl newUrl = d->getSaveFilename(KBibTeXPartPrivate::gsfoCurrentFilenameReused);
0980     if (!newUrl.isValid() || newUrl == url())
0981         return false;
0982 
0983     /// difference from KParts::ReadWritePart::saveAs:
0984     /// current document's URL won't be changed
0985     return d->saveFile(newUrl);
0986 }
0987 
0988 bool KBibTeXPart::documentSaveSelection()
0989 {
0990     d->isSaveAsOperation = true;
0991     const QUrl newUrl = d->getSaveFilename(~KBibTeXPartPrivate::gsfoCurrentFilenameReused);
0992     if (!newUrl.isValid() || newUrl == url())
0993         return false;
0994 
0995     /// difference from KParts::ReadWritePart::saveAs:
0996     /// current document's URL won't be changed
0997     return d->saveFile(newUrl, KBibTeXPartPrivate::scopeSelectedElements);
0998 }
0999 
1000 void KBibTeXPart::elementViewDocument()
1001 {
1002     QUrl url;
1003 
1004     const QList<QAction *> actionList = d->viewDocumentMenu->actions();
1005     /// Go through all actions (i.e. document URLs) for this element
1006     for (const QAction *action : actionList) {
1007         /// Make URL from action's data ...
1008         const QUrl tmpUrl = action->data().toUrl();
1009         if (!tmpUrl.isValid()) continue;
1010         /// ... but skip this action if the URL is invalid
1011         if (!tmpUrl.isValid()) continue;
1012         if (tmpUrl.isLocalFile()) {
1013             /// If action's URL points to local file,
1014             /// keep it and stop search for document
1015             url = tmpUrl;
1016             break;
1017         } else if (!url.isValid())
1018             /// First valid URL found, keep it
1019             /// URL is not local, so it may get overwritten by another URL
1020             url = tmpUrl;
1021     }
1022 
1023     /// Open selected URL
1024     if (url.isValid()) {
1025         /// Guess mime type for url to open
1026         QMimeType mimeType = FileInfo::mimeTypeForUrl(url);
1027         const QString mimeTypeName = mimeType.name();
1028         /// Ask KDE subsystem to open url in viewer matching mime type
1029 #if KIO_VERSION < QT_VERSION_CHECK(5, 71, 0)
1030         KRun::runUrl(url, mimeTypeName, widget(), KRun::RunFlags());
1031 #else // KIO_VERSION < QT_VERSION_CHECK(5, 71, 0) // >= 5.71.0
1032         KIO::OpenUrlJob *job = new KIO::OpenUrlJob(url, mimeTypeName);
1033 #if KIO_VERSION < QT_VERSION_CHECK(5, 98, 0) // < 5.98.0
1034         job->setUiDelegate(new KIO::JobUiDelegate());
1035 #else // KIO_VERSION < QT_VERSION_CHECK(5, 98, 0) // >= 5.98.0
1036         job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, widget()));
1037 #endif // KIO_VERSION < QT_VERSION_CHECK(5, 98, 0)
1038         job->start();
1039 #endif // KIO_VERSION < QT_VERSION_CHECK(5, 71, 0)
1040     }
1041 }
1042 
1043 void KBibTeXPart::elementFindPDF()
1044 {
1045     QModelIndexList mil = d->partWidget->fileView()->selectionModel()->selectedRows();
1046     if (mil.count() == 1) {
1047         const int row = d->partWidget->fileView()->sortFilterProxyModel()->mapToSource(*mil.constBegin()).row();
1048         QSharedPointer<Entry> entry = d->partWidget->fileView()->fileModel()->element(row).dynamicCast<Entry>();
1049         if (!entry.isNull()) {
1050             const bool gotModified = FindPDFUI::interactiveFindPDF(*entry, *d->bibTeXFile, widget());
1051             if (gotModified) {
1052                 d->model->elementChanged(row);
1053                 setModified(true);
1054             }
1055         }
1056     }
1057 }
1058 
1059 void KBibTeXPart::applyDefaultFormatString()
1060 {
1061     FileModel *model = d->partWidget != nullptr && d->partWidget->fileView() != nullptr ? d->partWidget->fileView()->fileModel() : nullptr;
1062     if (model == nullptr) return;
1063 
1064     bool documentModified = false;
1065     const QModelIndexList mil = d->partWidget->fileView()->selectionModel()->selectedRows();
1066     for (const QModelIndex &index : mil) {
1067         QSharedPointer<Entry> entry = model->element(d->partWidget->fileView()->sortFilterProxyModel()->mapToSource(index).row()).dynamicCast<Entry>();
1068         if (!entry.isNull()) {
1069             static IdSuggestions idSuggestions;
1070             bool success = idSuggestions.applyDefaultFormatId(*entry.data());
1071             documentModified |= success;
1072             if (!success) {
1073                 KMessageBox::information(widget(), i18n("Cannot apply default formatting for entry ids: No default format specified."), i18n("Cannot Apply Default Formatting"));
1074                 break;
1075             }
1076         }
1077     }
1078 
1079     if (documentModified)
1080         d->partWidget->fileView()->externalModification();
1081 }
1082 
1083 bool KBibTeXPart::openFile()
1084 {
1085     const bool success = d->openFile(url(), localFilePath());
1086     Q_EMIT completed();
1087     return success;
1088 }
1089 
1090 void KBibTeXPart::newEntryTriggered()
1091 {
1092     QSharedPointer<Entry> newEntry = QSharedPointer<Entry>(new Entry(QStringLiteral("Article"), d->findUnusedId()));
1093     d->model->insertRow(newEntry, d->model->rowCount());
1094     d->partWidget->fileView()->setSelectedElement(newEntry);
1095     if (d->partWidget->fileView()->editElement(newEntry))
1096         d->partWidget->fileView()->scrollToBottom(); // FIXME always correct behaviour?
1097     else {
1098         /// Editing this new element was cancelled,
1099         /// therefore remove it again
1100         d->model->removeRow(d->model->rowCount() - 1);
1101     }
1102 }
1103 
1104 void KBibTeXPart::newMacroTriggered()
1105 {
1106     QSharedPointer<Macro> newMacro = QSharedPointer<Macro>(new Macro(d->findUnusedId()));
1107     d->model->insertRow(newMacro, d->model->rowCount());
1108     d->partWidget->fileView()->setSelectedElement(newMacro);
1109     if (d->partWidget->fileView()->editElement(newMacro))
1110         d->partWidget->fileView()->scrollToBottom(); // FIXME always correct behaviour?
1111     else {
1112         /// Editing this new element was cancelled,
1113         /// therefore remove it again
1114         d->model->removeRow(d->model->rowCount() - 1);
1115     }
1116 }
1117 
1118 void KBibTeXPart::newPreambleTriggered()
1119 {
1120     QSharedPointer<Preamble> newPreamble = QSharedPointer<Preamble>(new Preamble());
1121     d->model->insertRow(newPreamble, d->model->rowCount());
1122     d->partWidget->fileView()->setSelectedElement(newPreamble);
1123     if (d->partWidget->fileView()->editElement(newPreamble))
1124         d->partWidget->fileView()->scrollToBottom(); // FIXME always correct behaviour?
1125     else {
1126         /// Editing this new element was cancelled,
1127         /// therefore remove it again
1128         d->model->removeRow(d->model->rowCount() - 1);
1129     }
1130 }
1131 
1132 void KBibTeXPart::newCommentTriggered()
1133 {
1134     // Fetch comment context and prefix preferrably from File object,
1135     // otherwise from Preferences
1136     const Preferences::CommentContext commentContext {d->bibTeXFile != nullptr && d->bibTeXFile->hasProperty(File::CommentContext) ? static_cast<Preferences::CommentContext>(d->bibTeXFile->property(File::CommentContext).toInt()) : Preferences::instance().bibTeXCommentContext()};
1137     const QString commentPrefix {commentContext == Preferences::CommentContext::Prefix ? (d->bibTeXFile != nullptr && d->bibTeXFile->hasProperty(File::CommentPrefix) ? d->bibTeXFile->property(File::CommentPrefix).toString() : Preferences::instance().bibTeXCommentPrefix()) : QString()};
1138     QSharedPointer<Comment> newComment = QSharedPointer<Comment>(new Comment(QString(), commentContext, commentPrefix));
1139     d->model->insertRow(newComment, d->model->rowCount());
1140     d->partWidget->fileView()->setSelectedElement(newComment);
1141     if (d->partWidget->fileView()->editElement(newComment))
1142         d->partWidget->fileView()->scrollToBottom(); // FIXME always correct behaviour?
1143     else {
1144         /// Editing this new element was cancelled,
1145         /// therefore remove it again
1146         d->model->removeRow(d->model->rowCount() - 1);
1147     }
1148 }
1149 
1150 void KBibTeXPart::updateActions()
1151 {
1152     FileModel *model = d->partWidget != nullptr && d->partWidget->fileView() != nullptr ? d->partWidget->fileView()->fileModel() : nullptr;
1153     if (model == nullptr) return;
1154 
1155     bool emptySelection = d->partWidget->fileView()->selectedElements().isEmpty();
1156     d->elementEditAction->setEnabled(!emptySelection);
1157     d->editCopyAction->setEnabled(!emptySelection);
1158     d->editCopyReferencesAction->setEnabled(!emptySelection);
1159     d->editCutAction->setEnabled(!emptySelection && isReadWrite());
1160     d->editPasteAction->setEnabled(isReadWrite());
1161     d->editDeleteAction->setEnabled(!emptySelection && isReadWrite());
1162     d->elementFindPDFAction->setEnabled(!emptySelection && isReadWrite());
1163     d->entryApplyDefaultFormatString->setEnabled(!emptySelection && isReadWrite());
1164     d->colorLabelContextMenu->menuAction()->setEnabled(!emptySelection && isReadWrite());
1165     d->colorLabelContextMenuAction->setEnabled(!emptySelection && isReadWrite());
1166 
1167     // Update menu items for opening documents associated with an entry
1168     d->updateViewDocumentMenu();
1169 
1170     /// update list of references which can be sent to LyX
1171     QStringList references;
1172     if (d->partWidget->fileView()->selectionModel() != nullptr) {
1173         const QModelIndexList mil = d->partWidget->fileView()->selectionModel()->selectedRows();
1174         references.reserve(mil.size());
1175         for (const QModelIndex &index : mil) {
1176             const QSharedPointer<Entry> entry = model->element(d->partWidget->fileView()->sortFilterProxyModel()->mapToSource(index).row()).dynamicCast<Entry>();
1177             if (!entry.isNull())
1178                 references << entry->id();
1179         }
1180     }
1181     d->lyx->setReferences(references);
1182 }
1183 
1184 void KBibTeXPart::fileExternallyChange(const QString &path)
1185 {
1186     /// Should never happen: triggering this slot for non-local or invalid URLs
1187     if (!url().isValid() || !url().isLocalFile())
1188         return;
1189     /// Should never happen: triggering this slot for filenames not being the opened file
1190     if (path != url().toLocalFile()) {
1191         qCWarning(LOG_KBIBTEX_PART) << "Got file modification warning for wrong file: " << path << "!=" << url().toLocalFile();
1192         return;
1193     }
1194 
1195     /// Stop watching file while asking for user interaction
1196     if (!path.isEmpty())
1197         d->fileSystemWatcher.removePath(path);
1198     else
1199         qCWarning(LOG_KBIBTEX_PART) << "No filename to stop watching";
1200 
1201     const QString message {isModified() ? i18n("The file '%1' has changed on disk but got also modified in KBibTeX.\n\nReload file and loose changes made in KBibTeX or ignore changes on disk and keep changes made in KBibTeX?", path) : i18n("The file '%1' has changed on disk.\n\nReload file or ignore changes on disk?", path)};
1202     if (KMessageBox::warningContinueCancel(widget(), message, i18n("File changed externally"), KGuiItem(i18n("Reload file"), QIcon::fromTheme(QStringLiteral("edit-redo"))), KGuiItem(i18n("Ignore on-disk changes"), QIcon::fromTheme(QStringLiteral("edit-undo")))) == KMessageBox::Continue) {
1203         d->openFile(QUrl::fromLocalFile(path), path);
1204         /// No explicit call to QFileSystemWatcher.addPath(...) necessary,
1205         /// openFile(...) has done that already
1206     } else {
1207         /// Even if the user did not request reloaded the file,
1208         /// still resume watching file for future external changes
1209         if (!path.isEmpty())
1210             d->fileSystemWatcher.addPath(path);
1211         else
1212             qCWarning(LOG_KBIBTEX_PART) << "path is Empty";
1213     }
1214 }
1215 
1216 K_PLUGIN_CLASS_WITH_JSON(KBibTeXPart, "kbibtexpart.json")
1217 
1218 #include "part.moc"