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"