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