1 /***************************************************************************
2  *   Copyright (C) 2004-2018 by Thomas Fischer <fischer@unix-ag.uni-kl.de> *
3  *                                                                         *
4  *   This program is free software; you can redistribute it and/or modify  *
5  *   it under the terms of the GNU General Public License as published by  *
6  *   the Free Software Foundation; either version 2 of the License, or     *
7  *   (at your option) any later version.                                   *
8  *                                                                         *
9  *   This program is distributed in the hope that it will be useful,       *
10  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
11  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
12  *   GNU General Public License for more details.                          *
13  *                                                                         *
14  *   You should have received a copy of the GNU General Public License     *
15  *   along with this program; if not, see <https://www.gnu.org/licenses/>. *
16  ***************************************************************************/
17 
18 #include "part.h"
19 
20 #include <QLabel>
21 #include <QAction>
22 #include <QFile>
23 #include <QFileInfo>
24 #include <QMenu>
25 #include <QApplication>
26 #include <QLayout>
27 #include <QKeyEvent>
28 #include <QSignalMapper>
29 #include <QMimeDatabase>
30 #include <QMimeType>
31 #include <QPointer>
32 #include <QFileSystemWatcher>
33 #include <QFileDialog>
34 #include <QDialog>
35 #include <QDialogButtonBox>
36 #include <QPushButton>
37 #include <QTemporaryFile>
38 #include <QTimer>
39 
40 #include <KMessageBox> // FIXME deprecated
41 #include <KLocalizedString>
42 #include <KActionCollection>
43 #include <KStandardAction>
44 #include <KActionMenu>
45 #include <KSelectAction>
46 #include <KToggleAction>
47 #include <KSharedConfig>
48 #include <KConfigGroup>
49 #include <KRun>
50 #include <KPluginFactory>
51 #include <KIO/StatJob>
52 #include <KIO/CopyJob>
53 #include <KIO/Job>
54 #include <KJobWidgets>
55 #include <kio_version.h>
56 
57 #include "file.h"
58 #include "macro.h"
59 #include "preamble.h"
60 #include "comment.h"
61 #include "fileinfo.h"
62 #include "fileexporterbibtexoutput.h"
63 #include "fileimporterbibtex.h"
64 #include "fileexporterbibtex.h"
65 #include "fileimporterris.h"
66 #include "fileimporterbibutils.h"
67 #include "fileexporterris.h"
68 #include "fileexporterbibutils.h"
69 #include "fileimporterpdf.h"
70 #include "fileexporterps.h"
71 #include "fileexporterpdf.h"
72 #include "fileexporterrtf.h"
73 #include "fileexporterbibtex2html.h"
74 #include "fileexporterxml.h"
75 #include "fileexporterxslt.h"
76 #include "models/filemodel.h"
77 #include "filesettingswidget.h"
78 #include "filterbar.h"
79 #include "findduplicatesui.h"
80 #include "lyx.h"
81 #include "preferences.h"
82 #include "settingscolorlabelwidget.h"
83 #include "settingsfileexporterpdfpswidget.h"
84 #include "findpdfui.h"
85 #include "valuelistmodel.h"
86 #include "clipboard.h"
87 #include "idsuggestions.h"
88 #include "fileview.h"
89 #include "browserextension.h"
90 #include "logging_parts.h"
91 
92 static const char RCFileName[] = "kbibtexpartui.rc";
93 static const int smEntry = 1;
94 static const int smComment = 2;
95 static const int smPreamble = 3;
96 static const int smMacro = 4;
97 
98 class KBibTeXPart::KBibTeXPartPrivate
99 {
100 private:
101     KBibTeXPart *p;
102     KSharedConfigPtr config;
103 
104     /**
105      * Modifies a given URL to become a "backup" filename/URL.
106      * A backup level or 0 or less does not modify the URL.
107      * A backup level of 1 appends a '~' (tilde) to the URL's filename.
108      * A backup level of 2 or more appends '~N', where N is the level.
109      * The provided URL will be modified in the process. It is assumed
110      * that the URL is not yet a "backup URL".
111      */
112     void constructBackupUrl(const int level, QUrl &url) const {
113         if (level <= 0)
114             /// No modification
115             return;
116         else if (level == 1)
117             /// Simply append '~' to the URL's filename
118             url.setPath(url.path() + QStringLiteral("~"));
119         else
120             /// Append '~' followed by a number to the filename
121             url.setPath(url.path() + QString(QStringLiteral("~%1")).arg(level));
122     }
123 
124 public:
125     File *bibTeXFile;
126     PartWidget *partWidget;
127     FileModel *model;
128     SortFilterFileModel *sortFilterProxyModel;
129     QSignalMapper *signalMapperNewElement;
130     QAction *editCutAction, *editDeleteAction, *editCopyAction, *editPasteAction, *editCopyReferencesAction, *elementEditAction, *elementViewDocumentAction, *fileSaveAction, *elementFindPDFAction, *entryApplyDefaultFormatString;
131     QMenu *viewDocumentMenu;
132     QSignalMapper *signalMapperViewDocument;
133     QSet<QObject *> signalMapperViewDocumentSenders;
134     bool isSaveAsOperation;
135     LyX *lyx;
136     FindDuplicatesUI *findDuplicatesUI;
137     ColorLabelContextMenu *colorLabelContextMenu;
138     QAction *colorLabelContextMenuAction;
139     QFileSystemWatcher fileSystemWatcher;
140 
141     KBibTeXPartPrivate(QWidget *parentWidget, KBibTeXPart *parent)
142             : p(parent), config(KSharedConfig::openConfig(QStringLiteral("kbibtexrc"))), bibTeXFile(nullptr), model(nullptr), sortFilterProxyModel(nullptr), signalMapperNewElement(new QSignalMapper(parent)), viewDocumentMenu(new QMenu(i18n("View Document"), parent->widget())), signalMapperViewDocument(new QSignalMapper(parent)), isSaveAsOperation(false), fileSystemWatcher(p) {
143         connect(signalMapperViewDocument, static_cast<void(QSignalMapper::*)(QObject *)>(&QSignalMapper::mapped), p, &KBibTeXPart::elementViewDocumentMenu);
144         connect(&fileSystemWatcher, &QFileSystemWatcher::fileChanged, p, &KBibTeXPart::fileExternallyChange);
145 
146         partWidget = new PartWidget(parentWidget);
147         partWidget->fileView()->setReadOnly(!p->isReadWrite());
148         connect(partWidget->fileView(), &FileView::modified, p, &KBibTeXPart::setModified);
149 
150         setupActions();
151     }
152 
153     ~KBibTeXPartPrivate() {
154         delete bibTeXFile;
155         delete model;
156         delete signalMapperNewElement;
157         delete viewDocumentMenu;
158         delete signalMapperViewDocument;
159         delete findDuplicatesUI;
160     }
161 
162 
163     void setupActions()
164     {
165         /// "Save" action
166         fileSaveAction = p->actionCollection()->addAction(KStandardAction::Save);
167         connect(fileSaveAction, &QAction::triggered, p, &KBibTeXPart::documentSave);
168         fileSaveAction->setEnabled(false);
169         QAction *action = p->actionCollection()->addAction(KStandardAction::SaveAs);
170         connect(action, &QAction::triggered, p, &KBibTeXPart::documentSaveAs);
171         /// "Save copy as" action
172         QAction *saveCopyAsAction = new QAction(QIcon::fromTheme(QStringLiteral("document-save")), i18n("Save Copy As..."), p);
173         p->actionCollection()->addAction(QStringLiteral("file_save_copy_as"), saveCopyAsAction);
174         connect(saveCopyAsAction, &QAction::triggered, p, &KBibTeXPart::documentSaveCopyAs);
175 
176         /// Filter bar widget
177         QAction *filterWidgetAction = new QAction(i18n("Filter"), p);
178         p->actionCollection()->addAction(QStringLiteral("toolbar_filter_widget"), filterWidgetAction);
179         filterWidgetAction->setIcon(QIcon::fromTheme(QStringLiteral("view-filter")));
180         p->actionCollection()->setDefaultShortcut(filterWidgetAction, Qt::CTRL + Qt::Key_F);
181         connect(filterWidgetAction, &QAction::triggered, partWidget->filterBar(), static_cast<void(QWidget::*)()>(&QWidget::setFocus));
182         partWidget->filterBar()->setPlaceholderText(i18n("Filter bibliographic entries (%1)", filterWidgetAction->shortcut().toString()));
183 
184         /// Actions for creating new elements (entries, macros, ...)
185         KActionMenu *newElementAction = new KActionMenu(QIcon::fromTheme(QStringLiteral("address-book-new")), i18n("New element"), p);
186         p->actionCollection()->addAction(QStringLiteral("element_new"), newElementAction);
187         QMenu *newElementMenu = new QMenu(newElementAction->text(), p->widget());
188         newElementAction->setMenu(newElementMenu);
189         connect(newElementAction, &QAction::triggered, p, &KBibTeXPart::newEntryTriggered);
190         QAction *newEntry = newElementMenu->addAction(QIcon::fromTheme(QStringLiteral("address-book-new")), i18n("New entry"));
191         p->actionCollection()->setDefaultShortcut(newEntry, Qt::CTRL + Qt::SHIFT + Qt::Key_N);
192         connect(newEntry, &QAction::triggered, signalMapperNewElement, static_cast<void(QSignalMapper::*)()>(&QSignalMapper::map));
193         signalMapperNewElement->setMapping(newEntry, smEntry);
194         QAction *newComment = newElementMenu->addAction(QIcon::fromTheme(QStringLiteral("address-book-new")), i18n("New comment"));
195         connect(newComment, &QAction::triggered, signalMapperNewElement, static_cast<void(QSignalMapper::*)()>(&QSignalMapper::map));
196         signalMapperNewElement->setMapping(newComment, smComment);
197         QAction *newMacro = newElementMenu->addAction(QIcon::fromTheme(QStringLiteral("address-book-new")), i18n("New macro"));
198         connect(newMacro, &QAction::triggered, signalMapperNewElement, static_cast<void(QSignalMapper::*)()>(&QSignalMapper::map));
199         signalMapperNewElement->setMapping(newMacro, smMacro);
200         QAction *newPreamble = newElementMenu->addAction(QIcon::fromTheme(QStringLiteral("address-book-new")), i18n("New preamble"));
201         connect(newPreamble, &QAction::triggered, signalMapperNewElement, static_cast<void(QSignalMapper::*)()>(&QSignalMapper::map));
202         signalMapperNewElement->setMapping(newPreamble, smPreamble);
203         connect(signalMapperNewElement, static_cast<void(QSignalMapper::*)(int)>(&QSignalMapper::mapped), p, &KBibTeXPart::newElementTriggered);
204 
205         /// Action to edit an element
206         elementEditAction = new QAction(QIcon::fromTheme(QStringLiteral("document-edit")), i18n("Edit Element"), p);
207         p->actionCollection()->addAction(QStringLiteral("element_edit"), elementEditAction);
208         p->actionCollection()->setDefaultShortcut(elementEditAction, Qt::CTRL + Qt::Key_E);
209         connect(elementEditAction, &QAction::triggered, partWidget->fileView(), &FileView::editCurrentElement);
210 
211         /// Action to view the document associated to the current element
212         elementViewDocumentAction = new QAction(QIcon::fromTheme(QStringLiteral("application-pdf")), i18n("View Document"), p);
213         p->actionCollection()->addAction(QStringLiteral("element_viewdocument"), elementViewDocumentAction);
214         p->actionCollection()->setDefaultShortcut(elementViewDocumentAction, Qt::CTRL + Qt::Key_D);
215         connect(elementViewDocumentAction, &QAction::triggered, p, &KBibTeXPart::elementViewDocument);
216 
217         /// Action to find a PDF matching the current element
218         elementFindPDFAction = new QAction(QIcon::fromTheme(QStringLiteral("application-pdf")), i18n("Find PDF..."), p);
219         p->actionCollection()->addAction(QStringLiteral("element_findpdf"), elementFindPDFAction);
220         connect(elementFindPDFAction, &QAction::triggered, p, &KBibTeXPart::elementFindPDF);
221 
222         /// Action to reformat the selected elements' ids
223         entryApplyDefaultFormatString = new QAction(QIcon::fromTheme(QStringLiteral("favorites")), i18n("Format entry ids"), p);
224         p->actionCollection()->addAction(QStringLiteral("entry_applydefaultformatstring"), entryApplyDefaultFormatString);
225         connect(entryApplyDefaultFormatString, &QAction::triggered, p, &KBibTeXPart::applyDefaultFormatString);
226 
227         /// Clipboard object, required for various copy&paste operations
228         Clipboard *clipboard = new Clipboard(partWidget->fileView());
229 
230         /// Actions to cut and copy selected elements as BibTeX code
231         editCutAction = p->actionCollection()->addAction(KStandardAction::Cut, clipboard, SLOT(cut()));
232         editCopyAction = p->actionCollection()->addAction(KStandardAction::Copy, clipboard, SLOT(copy()));
233 
234         /// Action to copy references, e.g. '\cite{fordfulkerson1959}'
235         editCopyReferencesAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Copy References"), p);
236         p->actionCollection()->setDefaultShortcut(editCopyReferencesAction, Qt::CTRL + Qt::SHIFT + Qt::Key_C);
237         p->actionCollection()->addAction(QStringLiteral("edit_copy_references"), editCopyReferencesAction);
238         connect(editCopyReferencesAction, &QAction::triggered, clipboard, &Clipboard::copyReferences);
239 
240         /// Action to paste BibTeX code
241         editPasteAction = p->actionCollection()->addAction(KStandardAction::Paste, clipboard, SLOT(paste()));
242 
243         /// Action to delete selected rows/elements
244         editDeleteAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-table-delete-row")), i18n("Delete"), p);
245         p->actionCollection()->setDefaultShortcut(editDeleteAction, Qt::Key_Delete);
246         p->actionCollection()->addAction(QStringLiteral("edit_delete"), editDeleteAction);
247         connect(editDeleteAction, &QAction::triggered, partWidget->fileView(), &FileView::selectionDelete);
248 
249         /// Build context menu for central BibTeX file view
250         partWidget->fileView()->setContextMenuPolicy(Qt::ActionsContextMenu); ///< context menu is based on actions
251         partWidget->fileView()->addAction(elementEditAction);
252         partWidget->fileView()->addAction(elementViewDocumentAction);
253         QAction *separator = new QAction(p);
254         separator->setSeparator(true);
255         partWidget->fileView()->addAction(separator);
256         partWidget->fileView()->addAction(editCutAction);
257         partWidget->fileView()->addAction(editCopyAction);
258         partWidget->fileView()->addAction(editCopyReferencesAction);
259         partWidget->fileView()->addAction(editPasteAction);
260         partWidget->fileView()->addAction(editDeleteAction);
261         separator = new QAction(p);
262         separator->setSeparator(true);
263         partWidget->fileView()->addAction(separator);
264         partWidget->fileView()->addAction(elementFindPDFAction);
265         partWidget->fileView()->addAction(entryApplyDefaultFormatString);
266         colorLabelContextMenu = new ColorLabelContextMenu(partWidget->fileView());
267         colorLabelContextMenuAction = p->actionCollection()->addAction(QStringLiteral("entry_colorlabel"), colorLabelContextMenu->menuAction());
268 
269         findDuplicatesUI = new FindDuplicatesUI(p, partWidget->fileView());
270         lyx = new LyX(p, partWidget->fileView());
271 
272         connect(partWidget->fileView(), &FileView::selectedElementsChanged, p, &KBibTeXPart::updateActions);
273         connect(partWidget->fileView(), &FileView::currentElementChanged, p, &KBibTeXPart::updateActions);
274     }
275 
276     FileImporter *fileImporterFactory(const QUrl &url) {
277         QString ending = url.path().toLower();
278         const auto pos = ending.lastIndexOf(QStringLiteral("."));
279         ending = ending.mid(pos + 1);
280 
281         if (ending == QStringLiteral("pdf")) {
282             return new FileImporterPDF(p);
283         } else if (ending == QStringLiteral("ris")) {
284             return new FileImporterRIS(p);
285         } else if (BibUtils::available() && ending == QStringLiteral("isi")) {
286             FileImporterBibUtils *fileImporterBibUtils = new FileImporterBibUtils(p);
287             fileImporterBibUtils->setFormat(BibUtils::ISI);
288             return fileImporterBibUtils;
289         } else {
290             FileImporterBibTeX *fileImporterBibTeX = new FileImporterBibTeX(p);
291             fileImporterBibTeX->setCommentHandling(FileImporterBibTeX::KeepComments);
292             return fileImporterBibTeX;
293         }
294     }
295 
296     FileExporter *fileExporterFactory(const QString &ending) {
297         if (ending == QStringLiteral("html")) {
298             return new FileExporterHTML(p);
299         } else if (ending == QStringLiteral("xml")) {
300             return new FileExporterXML(p);
301         } else if (ending == QStringLiteral("ris")) {
302             return new FileExporterRIS(p);
303         } else if (ending == QStringLiteral("pdf")) {
304             return new FileExporterPDF(p);
305         } else if (ending == QStringLiteral("ps")) {
306             return new FileExporterPS(p);
307         } else if (BibUtils::available() && ending == QStringLiteral("isi")) {
308             FileExporterBibUtils *fileExporterBibUtils = new FileExporterBibUtils(p);
309             fileExporterBibUtils->setFormat(BibUtils::ISI);
310             return fileExporterBibUtils;
311         } else if (ending == QStringLiteral("rtf")) {
312             return new FileExporterRTF(p);
313         } else if (ending == QStringLiteral("html") || ending == QStringLiteral("htm")) {
314             return new FileExporterBibTeX2HTML(p);
315         } else if (ending == QStringLiteral("bbl")) {
316             return new FileExporterBibTeXOutput(FileExporterBibTeXOutput::BibTeXBlockList, p);
317         } else {
318             return new FileExporterBibTeX(p);
319         }
320     }
321 
322     QString findUnusedId() {
323         int i = 1;
324         while (true) {
325             QString result = i18n("New%1", i);
326             if (!bibTeXFile->containsKey(result))
327                 return result;
328             ++i;
329         }
330         return QString();
331     }
332 
333     void initializeNew() {
334         bibTeXFile = new File();
335         model = new FileModel();
336         model->setBibliographyFile(bibTeXFile);
337 
338         if (sortFilterProxyModel != nullptr) delete sortFilterProxyModel;
339         sortFilterProxyModel = new SortFilterFileModel(p);
340         sortFilterProxyModel->setSourceModel(model);
341         partWidget->fileView()->setModel(sortFilterProxyModel);
342         connect(partWidget->filterBar(), &FilterBar::filterChanged, sortFilterProxyModel, &SortFilterFileModel::updateFilter);
343     }
344 
345     bool openFile(const QUrl &url, const QString &localFilePath) {
346         p->setObjectName("KBibTeXPart::KBibTeXPart for " + url.toDisplayString() + " aka " + localFilePath);
347 
348         qApp->setOverrideCursor(Qt::WaitCursor);
349 
350         if (bibTeXFile != nullptr) {
351             const QUrl oldUrl = bibTeXFile->property(File::Url, QUrl()).toUrl();
352             if (oldUrl.isValid() && oldUrl.isLocalFile()) {
353                 const QString path = oldUrl.toLocalFile();
354                 if (!path.isEmpty())
355                     fileSystemWatcher.removePath(path);
356                 else
357                     qCWarning(LOG_KBIBTEX_PARTS) << "No filename to stop watching";
358             }
359             delete bibTeXFile;
360             bibTeXFile = nullptr;
361         }
362 
363         QFile inputfile(localFilePath);
364         if (!inputfile.open(QIODevice::ReadOnly)) {
365             qCWarning(LOG_KBIBTEX_PARTS) << "Opening file failed, creating new one instead:" << url.toDisplayString() << "aka" << localFilePath;
366             qApp->restoreOverrideCursor();
367             /// Opening file failed, creating new one instead
368             initializeNew();
369             return false;
370         }
371 
372         FileImporter *importer = fileImporterFactory(url);
373         importer->showImportDialog(p->widget());
374         bibTeXFile = importer->load(&inputfile);
375         inputfile.close();
376         delete importer;
377 
378         if (bibTeXFile == nullptr) {
379             qCWarning(LOG_KBIBTEX_PARTS) << "Opening file failed, creating new one instead:" << url.toDisplayString() << "aka" << localFilePath;
380             qApp->restoreOverrideCursor();
381             /// Opening file failed, creating new one instead
382             initializeNew();
383             return false;
384         }
385 
386         bibTeXFile->setProperty(File::Url, QUrl(url));
387 
388         model->setBibliographyFile(bibTeXFile);
389         if (sortFilterProxyModel != nullptr) delete sortFilterProxyModel;
390         sortFilterProxyModel = new SortFilterFileModel(p);
391         sortFilterProxyModel->setSourceModel(model);
392         partWidget->fileView()->setModel(sortFilterProxyModel);
393         connect(partWidget->filterBar(), &FilterBar::filterChanged, sortFilterProxyModel, &SortFilterFileModel::updateFilter);
394 
395         if (url.isLocalFile())
396             fileSystemWatcher.addPath(url.toLocalFile());
397 
398         qApp->restoreOverrideCursor();
399 
400         return true;
401     }
402 
403     void makeBackup(const QUrl &url) const {
404         /// Fetch settings from configuration
405         KConfigGroup configGroup(config, Preferences::groupGeneral);
406         const Preferences::BackupScope backupScope = static_cast<Preferences::BackupScope>(configGroup.readEntry(Preferences::keyBackupScope, static_cast<int>(Preferences::defaultBackupScope)));
407         const int numberOfBackups = configGroup.readEntry(Preferences::keyNumberOfBackups, Preferences::defaultNumberOfBackups);
408 
409         /// Stop right here if no backup is requested
410         if (backupScope == Preferences::NoBackup)
411             return;
412 
413         /// For non-local files, proceed only if backups to remote storage is allowed
414         if (backupScope != Preferences::BothLocalAndRemote && !url.isLocalFile())
415             return;
416 
417         /// Do not make backup copies if destination file does not exist yet
418         KIO::StatJob *statJob = KIO::stat(url, KIO::StatJob::DestinationSide, 0 /** not details necessary, just need to know if file exists */, KIO::HideProgressInfo);
419         KJobWidgets::setWindow(statJob, p->widget());
420         statJob->exec();
421         if (statJob->error() == KIO::ERR_DOES_NOT_EXIST)
422             return;
423         else if (statJob->error() != KIO::Job::NoError) {
424             /// Something else went wrong, quit with error
425             qCWarning(LOG_KBIBTEX_PARTS) << "Probing" << url.toDisplayString() << "failed:" << statJob->errorString();
426             return;
427         }
428 
429         bool copySucceeded = true;
430         /// Copy e.g. test.bib~ to test.bib~2, test.bib to test.bib~ etc.
431         for (int level = numberOfBackups; copySucceeded && level >= 1; --level) {
432             QUrl newerBackupUrl = url;
433             constructBackupUrl(level - 1, newerBackupUrl);
434             QUrl olderBackupUrl = url;
435             constructBackupUrl(level, olderBackupUrl);
436 
437             statJob = KIO::stat(newerBackupUrl, KIO::StatJob::DestinationSide, 0 /** not details necessary, just need to know if file exists */, KIO::HideProgressInfo);
438             KJobWidgets::setWindow(statJob, p->widget());
439             if (statJob->exec() && statJob->error() == KIO::Job::NoError) {
440                 KIO::CopyJob *moveJob = nullptr; ///< guaranteed to be initialized in either branch of the following code
441                 /**
442                  * The following 'if' block is necessary to handle the
443                  * following situation: User opens, modifies, and saves
444                  * file /tmp/b/bbb.bib which is actually a symlink to
445                  * file /tmp/a/aaa.bib. Now a 'move' operation like the
446                  * implicit 'else' section below does, would move /tmp/b/bbb.bib
447                  * to become /tmp/b/bbb.bib~ still pointing to /tmp/a/aaa.bib.
448                  * Then, the save operation would create a new file /tmp/b/bbb.bib
449                  * without any symbolic linking to /tmp/a/aaa.bib.
450                  * The following code therefore checks if /tmp/b/bbb.bib is
451                  * to be copied/moved to /tmp/b/bbb.bib~ and /tmp/b/bbb.bib
452                  * is a local file and /tmp/b/bbb.bib is a symbolic link to
453                  * another file. Then /tmp/b/bbb.bib is resolved to the real
454                  * file /tmp/a/aaa.bib which is then copied into plain file
455                  * /tmp/b/bbb.bib~. The save function (outside of this function's
456                  * scope) will then see that /tmp/b/bbb.bib is a symbolic link,
457                  * resolve this symlink to /tmp/a/aaa.bib, and then write
458                  * all changes to /tmp/a/aaa.bib keeping /tmp/b/bbb.bib a
459                  * link to.
460                  */
461                 if (level == 1 && newerBackupUrl.isLocalFile() /** for level==1, this is actually the current file*/) {
462                     QFileInfo newerBackupFileInfo(newerBackupUrl.toLocalFile());
463                     if (newerBackupFileInfo.isSymLink()) {
464                         while (newerBackupFileInfo.isSymLink()) {
465                             newerBackupUrl = QUrl::fromLocalFile(newerBackupFileInfo.symLinkTarget());
466                             newerBackupFileInfo = QFileInfo(newerBackupUrl.toLocalFile());
467                         }
468                         moveJob = KIO::copy(newerBackupUrl, olderBackupUrl, KIO::HideProgressInfo | KIO::Overwrite);
469                     }
470                 }
471                 if (moveJob == nullptr) ///< implicit 'else' section, see longer comment above
472                     moveJob = KIO::move(newerBackupUrl, olderBackupUrl, KIO::HideProgressInfo | KIO::Overwrite);
473                 KJobWidgets::setWindow(moveJob, p->widget());
474                 copySucceeded = moveJob->exec();
475             }
476         }
477 
478         if (!copySucceeded)
479             KMessageBox::error(p->widget(), i18n("Could not create backup copies of document '%1'.", url.url(QUrl::PreferLocalFile)), i18n("Backup copies"));
480     }
481 
482     QUrl getSaveFilename(bool mustBeImportable = true) {
483         QString startDir = p->url().isValid() ? p->url().path() : QString();
484         QString supportedMimeTypes = QStringLiteral("text/x-bibtex text/x-research-info-systems");
485         if (BibUtils::available())
486             supportedMimeTypes += QStringLiteral(" application/x-isi-export-format application/x-endnote-refer");
487         if (!mustBeImportable && !QStandardPaths::findExecutable(QStringLiteral("pdflatex")).isEmpty())
488             supportedMimeTypes += QStringLiteral(" application/pdf");
489         if (!mustBeImportable && !QStandardPaths::findExecutable(QStringLiteral("dvips")).isEmpty())
490             supportedMimeTypes += QStringLiteral(" application/postscript");
491         if (!mustBeImportable)
492             supportedMimeTypes += QStringLiteral(" text/html");
493         if (!mustBeImportable && !QStandardPaths::findExecutable(QStringLiteral("latex2rtf")).isEmpty())
494             supportedMimeTypes += QStringLiteral(" application/rtf");
495 
496         QPointer<QFileDialog> saveDlg = new QFileDialog(p->widget(), i18n("Save file") /* TODO better text */, startDir, supportedMimeTypes);
497         /// Setting list of mime types for the second time,
498         /// essentially calling this function only to set the "default mime type" parameter
499         saveDlg->setMimeTypeFilters(supportedMimeTypes.split(QLatin1Char(' '), QString::SkipEmptyParts));
500         /// Setting the dialog into "Saving" mode make the "add extension" checkbox available
501         saveDlg->setAcceptMode(QFileDialog::AcceptSave);
502         saveDlg->setDefaultSuffix(QStringLiteral("bib"));
503         saveDlg->setFileMode(QFileDialog::AnyFile);
504         if (saveDlg->exec() != QDialog::Accepted)
505             /// User cancelled saving operation, return invalid filename/URL
506             return QUrl();
507         const QList<QUrl> selectedUrls = saveDlg->selectedUrls();
508         delete saveDlg;
509         return selectedUrls.isEmpty() ? QUrl() : selectedUrls.first();
510     }
511 
512     FileExporter *saveFileExporter(const QString &ending) {
513         FileExporter *exporter = fileExporterFactory(ending);
514 
515         if (isSaveAsOperation) {
516             /// only show export dialog at SaveAs or SaveCopyAs operations
517             FileExporterToolchain *fet = nullptr;
518 
519             if (FileExporterBibTeX::isFileExporterBibTeX(*exporter)) {
520                 QPointer<QDialog> dlg = new QDialog(p->widget());
521                 dlg->setWindowTitle(i18n("BibTeX File Settings"));
522                 QBoxLayout *layout = new QVBoxLayout(dlg);
523                 FileSettingsWidget *settingsWidget = new FileSettingsWidget(dlg);
524                 layout->addWidget(settingsWidget);
525                 QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::RestoreDefaults | QDialogButtonBox::Reset | QDialogButtonBox::Ok, Qt::Horizontal, dlg);
526                 layout->addWidget(buttonBox);
527                 connect(buttonBox->button(QDialogButtonBox::RestoreDefaults), &QPushButton::clicked, settingsWidget, &FileSettingsWidget::resetToDefaults);
528                 connect(buttonBox->button(QDialogButtonBox::Reset), &QPushButton::clicked, settingsWidget, &FileSettingsWidget::resetToLoadedProperties);
529                 connect(buttonBox->button(QDialogButtonBox::Ok), &QPushButton::clicked, dlg.data(), &QDialog::accept);
530 
531                 settingsWidget->loadProperties(bibTeXFile);
532 
533                 if (dlg->exec() == QDialog::Accepted)
534                     settingsWidget->saveProperties(bibTeXFile);
535                 delete dlg;
536             } else if ((fet = qobject_cast<FileExporterToolchain *>(exporter)) != nullptr) {
537                 QPointer<QDialog> dlg = new QDialog(p->widget());
538                 dlg->setWindowTitle(i18n("PDF/PostScript File Settings"));
539                 QBoxLayout *layout = new QVBoxLayout(dlg);
540                 SettingsFileExporterPDFPSWidget *settingsWidget = new SettingsFileExporterPDFPSWidget(dlg);
541                 layout->addWidget(settingsWidget);
542                 QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::RestoreDefaults | QDialogButtonBox::Reset | QDialogButtonBox::Ok, Qt::Horizontal, dlg);
543                 layout->addWidget(buttonBox);
544                 connect(buttonBox->button(QDialogButtonBox::RestoreDefaults), &QPushButton::clicked, settingsWidget, &SettingsFileExporterPDFPSWidget::resetToDefaults);
545                 connect(buttonBox->button(QDialogButtonBox::Reset), &QPushButton::clicked, settingsWidget, &SettingsFileExporterPDFPSWidget::loadState);
546                 connect(buttonBox->button(QDialogButtonBox::Ok), &QPushButton::clicked, dlg.data(), &QDialog::accept);
547 
548                 if (dlg->exec() == QDialog::Accepted)
549                     settingsWidget->saveState();
550                 fet->reloadConfig();
551                 delete dlg;
552             }
553         }
554 
555         return exporter;
556     }
557 
558     bool saveFile(QFile &file, FileExporter *exporter, QStringList *errorLog = nullptr) {
559         SortFilterFileModel *model = qobject_cast<SortFilterFileModel *>(partWidget->fileView()->model());
560         Q_ASSERT_X(model != nullptr, "FileExporter *KBibTeXPart::KBibTeXPartPrivate:saveFile(...)", "SortFilterFileModel *model from editor->model() is invalid");
561 
562         return exporter->save(&file, model->fileSourceModel()->bibliographyFile(), errorLog);
563     }
564 
565     bool saveFile(const QUrl &url) {
566         bool result = false;
567         Q_ASSERT_X(!url.isEmpty(), "bool KBibTeXPart::KBibTeXPartPrivate:saveFile(const QUrl &url)", "url is not allowed to be empty");
568 
569         /// Extract filename extension (e.g. 'bib') to determine which FileExporter to use
570         static const QRegularExpression suffixRegExp(QStringLiteral("\\.([^.]{1,4})$"));
571         const QRegularExpressionMatch suffixRegExpMatch = suffixRegExp.match(url.fileName());
572         const QString ending = suffixRegExpMatch.hasMatch() ? suffixRegExpMatch.captured(1) : QStringLiteral("bib");
573         FileExporter *exporter = saveFileExporter(ending);
574 
575         /// String list to collect error message from FileExporer
576         QStringList errorLog;
577         qApp->setOverrideCursor(Qt::WaitCursor);
578 
579         if (url.isLocalFile()) {
580             /// Take precautions for local files
581             QFileInfo fileInfo(url.toLocalFile());
582             /// Do not overwrite symbolic link, but linked file instead
583             QString filename = fileInfo.absoluteFilePath();
584             while (fileInfo.isSymLink()) {
585                 filename = fileInfo.symLinkTarget();
586                 fileInfo = QFileInfo(filename);
587             }
588             if (!fileInfo.exists() || fileInfo.isWritable()) {
589                 /// Make backup before overwriting target destination, intentionally
590                 /// using the provided filename, not the resolved symlink
591                 makeBackup(url);
592 
593                 QFile file(filename);
594                 if (file.open(QIODevice::WriteOnly)) {
595                     result = saveFile(file, exporter, &errorLog);
596                     file.close();
597                 }
598             }
599         } else {
600             /// URL points to a remote location
601 
602             /// Configure and open temporary file
603             QTemporaryFile temporaryFile(QStandardPaths::writableLocation(QStandardPaths::TempLocation) + QDir::separator() + QStringLiteral("kbibtex_savefile_XXXXXX") + ending);
604             temporaryFile.setAutoRemove(true);
605             if (temporaryFile.open()) {
606                 result = saveFile(temporaryFile, exporter, &errorLog);
607 
608                 /// Close/flush temporary file
609                 temporaryFile.close();
610 
611                 if (result) {
612                     /// Make backup before overwriting target destination
613                     makeBackup(url);
614 
615                     KIO::CopyJob *copyJob = KIO::copy(QUrl::fromLocalFile(temporaryFile.fileName()), url, KIO::HideProgressInfo | KIO::Overwrite);
616                     KJobWidgets::setWindow(copyJob, p->widget());
617                     result &= copyJob->exec() && copyJob->error() == KIO::Job::NoError;
618                 }
619             }
620         }
621 
622         qApp->restoreOverrideCursor();
623 
624         delete exporter;
625 
626         if (!result) {
627             QString msg = i18n("Saving the bibliography to file '%1' failed.", url.toDisplayString());
628             if (errorLog.isEmpty())
629                 KMessageBox::error(p->widget(), msg, i18n("Saving bibliography failed"));
630             else {
631                 msg += QLatin1String("\n\n");
632                 msg += i18n("The following output was generated by the export filter:");
633                 KMessageBox::errorList(p->widget(), msg, errorLog, i18n("Saving bibliography failed"));
634             }
635         } else
636             bibTeXFile->setProperty(File::Url, url);
637         return result;
638     }
639 
640     /**
641      * Builds or resets the menu with local and remote
642      * references (URLs, files) of an entry.
643      *
644      * @return Number of known references
645      */
646     int updateViewDocumentMenu() {
647         viewDocumentMenu->clear();
648         int result = 0; ///< Initially, no references are known
649 
650         File *bibliographyFile = partWidget != nullptr && partWidget->fileView() != nullptr && partWidget->fileView()->fileModel() != nullptr ? partWidget->fileView()->fileModel()->bibliographyFile() : nullptr;
651         if (bibliographyFile == nullptr) return result;
652 
653         /// Clean signal mapper of old mappings
654         /// as stored in QSet signalMapperViewDocumentSenders
655         /// and identified by their QAction*'s
656         QSet<QObject *>::Iterator it = signalMapperViewDocumentSenders.begin();
657         while (it != signalMapperViewDocumentSenders.end()) {
658             signalMapperViewDocument->removeMappings(*it);
659             it = signalMapperViewDocumentSenders.erase(it);
660         }
661 
662         /// Retrieve Entry object of currently selected line
663         /// in main list view
664         QSharedPointer<const Entry> entry = partWidget->fileView()->currentElement().dynamicCast<const Entry>();
665         /// Test and continue if there was an Entry to retrieve
666         if (!entry.isNull()) {
667             /// Get list of URLs associated with this entry
668             const auto urlList = FileInfo::entryUrls(entry, bibliographyFile->property(File::Url).toUrl(), FileInfo::TestExistenceYes);
669             if (!urlList.isEmpty()) {
670                 /// Memorize first action, necessary to set menu title
671                 QAction *firstAction = nullptr;
672                 /// First iteration: local references only
673                 for (const QUrl &url : urlList) {
674                     /// First iteration: local references only
675                     if (!url.isLocalFile()) continue; ///< skip remote URLs
676 
677                     /// Build a nice menu item (label, icon, ...)
678                     const QFileInfo fi(url.toLocalFile());
679                     const QString label = QString(QStringLiteral("%1 [%2]")).arg(fi.fileName(), fi.absolutePath());
680                     QMimeDatabase db;
681                     QAction *action = new QAction(QIcon::fromTheme(db.mimeTypeForUrl(url).iconName()), label, p);
682                     action->setData(fi.absoluteFilePath());
683                     action->setToolTip(fi.absoluteFilePath());
684                     /// Register action at signal handler to open URL when triggered
685                     connect(action, &QAction::triggered, signalMapperViewDocument, static_cast<void(QSignalMapper::*)()>(&QSignalMapper::map));
686                     signalMapperViewDocument->setMapping(action, action);
687                     signalMapperViewDocumentSenders.insert(action);
688                     viewDocumentMenu->addAction(action);
689                     /// Memorize first action
690                     if (firstAction == nullptr) firstAction = action;
691                 }
692                 if (firstAction != nullptr) {
693                     /// If there is 'first action', then there must be
694                     /// local URLs (i.e. local files) and firstAction
695                     /// is the first one where a title can be set above
696                     viewDocumentMenu->insertSection(firstAction, i18n("Local Files"));
697                 }
698 
699                 firstAction = nullptr; /// Now the first remote action is to be memorized
700                 /// Second iteration: remote references only
701                 for (const QUrl &url : urlList) {
702                     if (url.isLocalFile()) continue; ///< skip local files
703 
704                     /// Build a nice menu item (label, icon, ...)
705                     const QString prettyUrl = url.toDisplayString();
706                     QMimeDatabase db;
707                     QAction *action = new QAction(QIcon::fromTheme(db.mimeTypeForUrl(url).iconName()), prettyUrl, p);
708                     action->setData(prettyUrl);
709                     action->setToolTip(prettyUrl);
710                     /// Register action at signal handler to open URL when triggered
711                     connect(action, &QAction::triggered, signalMapperViewDocument, static_cast<void(QSignalMapper::*)()>(&QSignalMapper::map));
712                     signalMapperViewDocument->setMapping(action, action);
713                     signalMapperViewDocumentSenders.insert(action);
714                     viewDocumentMenu->addAction(action);
715                     /// Memorize first action
716                     if (firstAction == nullptr) firstAction = action;
717                 }
718                 if (firstAction != nullptr) {
719                     /// If there is 'first action', then there must be
720                     /// some remote URLs and firstAction is the first
721                     /// one where a title can be set above
722                     viewDocumentMenu->insertSection(firstAction, i18n("Remote Files"));
723                 }
724 
725                 result = urlList.count();
726             }
727         }
728 
729         return result;
730     }
731 
732     void readConfiguration() {
733         /// Fetch settings from configuration
734         KConfigGroup configGroup(config, Preferences::groupUserInterface);
735         const Preferences::ElementDoubleClickAction doubleClickAction = static_cast<Preferences::ElementDoubleClickAction>(configGroup.readEntry(Preferences::keyElementDoubleClickAction, static_cast<int>(Preferences::defaultElementDoubleClickAction)));
736 
737         disconnect(partWidget->fileView(), &FileView::elementExecuted, partWidget->fileView(), &FileView::editElement);
738         disconnect(partWidget->fileView(), &FileView::elementExecuted, p, &KBibTeXPart::elementViewDocument);
739         switch (doubleClickAction) {
740         case Preferences::ActionOpenEditor:
741             connect(partWidget->fileView(), &FileView::elementExecuted, partWidget->fileView(), &FileView::editElement);
742             break;
743         case Preferences::ActionViewDocument:
744             connect(partWidget->fileView(), &FileView::elementExecuted, p, &KBibTeXPart::elementViewDocument);
745             break;
746         }
747     }
748 };
749 
750 KBibTeXPart::KBibTeXPart(QWidget *parentWidget, QObject *parent, const KAboutData &componentData)
751         : KParts::ReadWritePart(parent), d(new KBibTeXPartPrivate(parentWidget, this))
752 {
753     setComponentData(componentData);
754 
755     setWidget(d->partWidget);
756     updateActions();
757 
758     d->initializeNew();
759 
760     setXMLFile(RCFileName);
761 
762     new BrowserExtension(this);
763 
764     NotificationHub::registerNotificationListener(this, NotificationHub::EventConfigurationChanged);
765     d->readConfiguration();
766 
767     setModified(false);
768 }
769 
770 KBibTeXPart::~KBibTeXPart()
771 {
772     delete d;
773 }
774 
775 void KBibTeXPart::setModified(bool modified)
776 {
777     KParts::ReadWritePart::setModified(modified);
778 
779     d->fileSaveAction->setEnabled(modified);
780 }
781 
782 void KBibTeXPart::notificationEvent(int eventId)
783 {
784     if (eventId == NotificationHub::EventConfigurationChanged)
785         d->readConfiguration();
786 }
787 
788 bool KBibTeXPart::saveFile()
789 {
790     Q_ASSERT_X(isReadWrite(), "bool KBibTeXPart::saveFile()", "Trying to save although document is in read-only mode");
791 
792     if (url().isEmpty())
793         return documentSaveAs();
794 
795     /// If the current file is "watchable" (i.e. a local file),
796     /// memorize local filename for future reference
797     const QString watchableFilename = url().isValid() && url().isLocalFile() ? url().toLocalFile() : QString();
798     /// Stop watching local file that will be written to
799     if (!watchableFilename.isEmpty())
800         d->fileSystemWatcher.removePath(watchableFilename);
801     else
802         qCWarning(LOG_KBIBTEX_PARTS) << "watchableFilename is Empty";
803 
804     const bool saveOperationSuccess = d->saveFile(url());
805 
806     if (!watchableFilename.isEmpty()) {
807         /// Continue watching a local file after write operation, but do
808         /// so only after a short delay. The delay is necessary in some
809         /// situations as observed in KDE bug report 396343 where the
810         /// DropBox client seemingly touched the file right after saving
811         /// from within KBibTeX, triggering KBibTeX to show a 'reload'
812         /// message box.
813         QTimer::singleShot(500, this, [this, watchableFilename]() {
814             d->fileSystemWatcher.addPath(watchableFilename);
815         });
816     } else
817         qCWarning(LOG_KBIBTEX_PARTS) << "watchableFilename is Empty";
818 
819     if (!saveOperationSuccess) {
820         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()));
821         return false;
822     }
823 
824     return true;
825 }
826 
827 bool KBibTeXPart::documentSave()
828 {
829     d->isSaveAsOperation = false;
830     if (!isReadWrite())
831         return documentSaveCopyAs();
832     else if (!url().isValid())
833         return documentSaveAs();
834     else
835         return KParts::ReadWritePart::save();
836 }
837 
838 bool KBibTeXPart::documentSaveAs()
839 {
840     d->isSaveAsOperation = true;
841     QUrl newUrl = d->getSaveFilename();
842     if (!newUrl.isValid())
843         return false;
844 
845     /// Remove old URL from file system watcher
846     if (url().isValid() && url().isLocalFile()) {
847         const QString path = url().toLocalFile();
848         if (!path.isEmpty())
849             d->fileSystemWatcher.removePath(path);
850         else
851             qCWarning(LOG_KBIBTEX_PARTS) << "No filename to stop watching";
852     } else
853         qCWarning(LOG_KBIBTEX_PARTS) << "Not removing" << url().url(QUrl::PreferLocalFile) << "from fileSystemWatcher";
854 
855     // TODO how does SaveAs dialog know which mime types to support?
856     if (KParts::ReadWritePart::saveAs(newUrl))
857         return true;
858     else
859         return false;
860 }
861 
862 bool KBibTeXPart::documentSaveCopyAs()
863 {
864     d->isSaveAsOperation = true;
865     QUrl newUrl = d->getSaveFilename(false);
866     if (!newUrl.isValid() || newUrl == url())
867         return false;
868 
869     /// difference from KParts::ReadWritePart::saveAs:
870     /// current document's URL won't be changed
871     return d->saveFile(newUrl);
872 }
873 
874 void KBibTeXPart::elementViewDocument()
875 {
876     QUrl url;
877 
878     const QList<QAction *> actionList = d->viewDocumentMenu->actions();
879     /// Go through all actions (i.e. document URLs) for this element
880     for (const QAction *action : actionList) {
881         /// Make URL from action's data ...
882         const QString actionData = action->data().toString();
883         if (actionData.isEmpty()) continue; ///< No URL from empty string
884         const QUrl tmpUrl = QUrl::fromUserInput(actionData);
885         /// ... but skip this action if the URL is invalid
886         if (!tmpUrl.isValid()) continue;
887         if (tmpUrl.isLocalFile()) {
888             /// If action's URL points to local file,
889             /// keep it and stop search for document
890             url = tmpUrl;
891             break;
892         } else if (!url.isValid())
893             /// First valid URL found, keep it
894             /// URL is not local, so it may get overwritten by another URL
895             url = tmpUrl;
896     }
897 
898     /// Open selected URL
899     if (url.isValid()) {
900         /// Guess mime type for url to open
901         QMimeType mimeType = FileInfo::mimeTypeForUrl(url);
902         const QString mimeTypeName = mimeType.name();
903         /// Ask KDE subsystem to open url in viewer matching mime type
904 #if KIO_VERSION < 0x051f00 // < 5.31.0
905         KRun::runUrl(url, mimeTypeName, widget(), false, false);
906 #else // KIO_VERSION < 0x051f00 // >= 5.31.0
907         KRun::runUrl(url, mimeTypeName, widget(), KRun::RunFlags());
908 #endif // KIO_VERSION < 0x051f00
909     }
910 }
911 
912 void KBibTeXPart::elementViewDocumentMenu(QObject *obj)
913 {
914     QString text = static_cast<QAction *>(obj)->data().toString(); ///< only a QAction will be passed along
915 
916     /// Guess mime type for url to open
917     QUrl url(text);
918     QMimeType mimeType = FileInfo::mimeTypeForUrl(url);
919     const QString mimeTypeName = mimeType.name();
920     /// Ask KDE subsystem to open url in viewer matching mime type
921 #if KIO_VERSION < 0x051f00 // < 5.31.0
922     KRun::runUrl(url, mimeTypeName, widget(), false, false);
923 #else // KIO_VERSION < 0x051f00 // >= 5.31.0
924     KRun::runUrl(url, mimeTypeName, widget(), KRun::RunFlags());
925 #endif // KIO_VERSION < 0x051f00
926 }
927 
928 void KBibTeXPart::elementFindPDF()
929 {
930     QModelIndexList mil = d->partWidget->fileView()->selectionModel()->selectedRows();
931     if (mil.count() == 1) {
932         QSharedPointer<Entry> entry = d->partWidget->fileView()->fileModel()->element(d->partWidget->fileView()->sortFilterProxyModel()->mapToSource(*mil.constBegin()).row()).dynamicCast<Entry>();
933         if (!entry.isNull())
934             FindPDFUI::interactiveFindPDF(*entry, *d->bibTeXFile, widget());
935     }
936 }
937 
938 void KBibTeXPart::applyDefaultFormatString()
939 {
940     FileModel *model = d->partWidget != nullptr && d->partWidget->fileView() != nullptr ? d->partWidget->fileView()->fileModel() : nullptr;
941     if (model == nullptr) return;
942 
943     bool documentModified = false;
944     const QModelIndexList mil = d->partWidget->fileView()->selectionModel()->selectedRows();
945     for (const QModelIndex &index : mil) {
946         QSharedPointer<Entry> entry = model->element(d->partWidget->fileView()->sortFilterProxyModel()->mapToSource(index).row()).dynamicCast<Entry>();
947         if (!entry.isNull()) {
948             static IdSuggestions idSuggestions;
949             bool success = idSuggestions.applyDefaultFormatId(*entry.data());
950             documentModified |= success;
951             if (!success) {
952                 KMessageBox::information(widget(), i18n("Cannot apply default formatting for entry ids: No default format specified."), i18n("Cannot Apply Default Formatting"));
953                 break;
954             }
955         }
956     }
957 
958     if (documentModified)
959         d->partWidget->fileView()->externalModification();
960 }
961 
962 bool KBibTeXPart::openFile()
963 {
964     const bool success = d->openFile(url(), localFilePath());
965     emit completed();
966     return success;
967 }
968 
969 void KBibTeXPart::newElementTriggered(int event)
970 {
971     switch (event) {
972     case smComment:
973         newCommentTriggered();
974         break;
975     case smMacro:
976         newMacroTriggered();
977         break;
978     case smPreamble:
979         newPreambleTriggered();
980         break;
981     default:
982         newEntryTriggered();
983     }
984 }
985 
986 void KBibTeXPart::newEntryTriggered()
987 {
988     QSharedPointer<Entry> newEntry = QSharedPointer<Entry>(new Entry(QStringLiteral("Article"), d->findUnusedId()));
989     d->model->insertRow(newEntry, d->model->rowCount());
990     d->partWidget->fileView()->setSelectedElement(newEntry);
991     if (d->partWidget->fileView()->editElement(newEntry))
992         d->partWidget->fileView()->scrollToBottom(); // FIXME always correct behaviour?
993     else {
994         /// Editing this new element was cancelled,
995         /// therefore remove it again
996         d->model->removeRow(d->model->rowCount() - 1);
997     }
998 }
999 
1000 void KBibTeXPart::newMacroTriggered()
1001 {
1002     QSharedPointer<Macro> newMacro = QSharedPointer<Macro>(new Macro(d->findUnusedId()));
1003     d->model->insertRow(newMacro, d->model->rowCount());
1004     d->partWidget->fileView()->setSelectedElement(newMacro);
1005     if (d->partWidget->fileView()->editElement(newMacro))
1006         d->partWidget->fileView()->scrollToBottom(); // FIXME always correct behaviour?
1007     else {
1008         /// Editing this new element was cancelled,
1009         /// therefore remove it again
1010         d->model->removeRow(d->model->rowCount() - 1);
1011     }
1012 }
1013 
1014 void KBibTeXPart::newPreambleTriggered()
1015 {
1016     QSharedPointer<Preamble> newPreamble = QSharedPointer<Preamble>(new Preamble());
1017     d->model->insertRow(newPreamble, d->model->rowCount());
1018     d->partWidget->fileView()->setSelectedElement(newPreamble);
1019     if (d->partWidget->fileView()->editElement(newPreamble))
1020         d->partWidget->fileView()->scrollToBottom(); // FIXME always correct behaviour?
1021     else {
1022         /// Editing this new element was cancelled,
1023         /// therefore remove it again
1024         d->model->removeRow(d->model->rowCount() - 1);
1025     }
1026 }
1027 
1028 void KBibTeXPart::newCommentTriggered()
1029 {
1030     QSharedPointer<Comment> newComment = QSharedPointer<Comment>(new Comment());
1031     d->model->insertRow(newComment, d->model->rowCount());
1032     d->partWidget->fileView()->setSelectedElement(newComment);
1033     if (d->partWidget->fileView()->editElement(newComment))
1034         d->partWidget->fileView()->scrollToBottom(); // FIXME always correct behaviour?
1035     else {
1036         /// Editing this new element was cancelled,
1037         /// therefore remove it again
1038         d->model->removeRow(d->model->rowCount() - 1);
1039     }
1040 }
1041 
1042 void KBibTeXPart::updateActions()
1043 {
1044     FileModel *model = d->partWidget != nullptr && d->partWidget->fileView() != nullptr ? d->partWidget->fileView()->fileModel() : nullptr;
1045     if (model == nullptr) return;
1046 
1047     bool emptySelection = d->partWidget->fileView()->selectedElements().isEmpty();
1048     d->elementEditAction->setEnabled(!emptySelection);
1049     d->editCopyAction->setEnabled(!emptySelection);
1050     d->editCopyReferencesAction->setEnabled(!emptySelection);
1051     d->editCutAction->setEnabled(!emptySelection && isReadWrite());
1052     d->editPasteAction->setEnabled(isReadWrite());
1053     d->editDeleteAction->setEnabled(!emptySelection && isReadWrite());
1054     d->elementFindPDFAction->setEnabled(!emptySelection && isReadWrite());
1055     d->entryApplyDefaultFormatString->setEnabled(!emptySelection && isReadWrite());
1056     d->colorLabelContextMenu->menuAction()->setEnabled(!emptySelection && isReadWrite());
1057     d->colorLabelContextMenuAction->setEnabled(!emptySelection && isReadWrite());
1058 
1059     int numDocumentsToView = d->updateViewDocumentMenu();
1060     /// enable menu item only if there is at least one document to view
1061     d->elementViewDocumentAction->setEnabled(!emptySelection && numDocumentsToView > 0);
1062     /// activate sub-menu only if there are at least two documents to view
1063     d->elementViewDocumentAction->setMenu(numDocumentsToView > 1 ? d->viewDocumentMenu : nullptr);
1064     d->elementViewDocumentAction->setToolTip(numDocumentsToView == 1 ? (*d->viewDocumentMenu->actions().constBegin())->text() : QString());
1065 
1066     /// update list of references which can be sent to LyX
1067     QStringList references;
1068     if (d->partWidget->fileView()->selectionModel() != nullptr) {
1069         const QModelIndexList mil = d->partWidget->fileView()->selectionModel()->selectedRows();
1070         references.reserve(mil.size());
1071         for (const QModelIndex &index : mil) {
1072             const QSharedPointer<Entry> entry = model->element(d->partWidget->fileView()->sortFilterProxyModel()->mapToSource(index).row()).dynamicCast<Entry>();
1073             if (!entry.isNull())
1074                 references << entry->id();
1075         }
1076     }
1077     d->lyx->setReferences(references);
1078 }
1079 
1080 void KBibTeXPart::fileExternallyChange(const QString &path)
1081 {
1082     /// Should never happen: triggering this slot for non-local or invalid URLs
1083     if (!url().isValid() || !url().isLocalFile())
1084         return;
1085     /// Should never happen: triggering this slot for filenames not being the opened file
1086     if (path != url().toLocalFile()) {
1087         qCWarning(LOG_KBIBTEX_PARTS) << "Got file modification warning for wrong file: " << path << "!=" << url().toLocalFile();
1088         return;
1089     }
1090 
1091     /// Stop watching file while asking for user interaction
1092     if (!path.isEmpty())
1093         d->fileSystemWatcher.removePath(path);
1094     else
1095         qCWarning(LOG_KBIBTEX_PARTS) << "No filename to stop watching";
1096 
1097     if (KMessageBox::warningContinueCancel(widget(), i18n("The file '%1' has changed on disk.\n\nReload file or ignore changes on disk?", path), 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) {
1098         d->openFile(QUrl::fromLocalFile(path), path);
1099         /// No explicit call to QFileSystemWatcher.addPath(...) necessary,
1100         /// openFile(...) has done that already
1101     } else {
1102         /// Even if the user did not request reloaded the file,
1103         /// still resume watching file for future external changes
1104         if (!path.isEmpty())
1105             d->fileSystemWatcher.addPath(path);
1106         else
1107             qCWarning(LOG_KBIBTEX_PARTS) << "path is Empty";
1108     }
1109 }
1110 
1111 #include "part.moc"
1112