diff --git a/src/gui/element/associatedfilesui.cpp b/src/gui/element/associatedfilesui.cpp index 6a9a25e5..8eaacd05 100644 --- a/src/gui/element/associatedfilesui.cpp +++ b/src/gui/element/associatedfilesui.cpp @@ -1,274 +1,274 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, see . * ***************************************************************************/ #include "associatedfilesui.h" #include #include #include #include #include #include #include #include #include #include #include #include class AssociatedFilesUI::Private { private: AssociatedFilesUI *p; public: QLabel *labelGreeting; QLineEdit *lineEditSourceUrl; QRadioButton *radioNoCopyMove, *radioCopyFile, *radioMoveFile; QLabel *labelMoveCopyLocation; QLineEdit *lineMoveCopyLocation; QGroupBox *groupBoxRename; QRadioButton *radioKeepFilename, *radioRenameToEntryId, *radioUserDefinedName; QLineEdit *lineEditUserDefinedName; QGroupBox *groupBoxPathType; QRadioButton *radioRelativePath, *radioAbsolutePath; QLineEdit *linePreview; QUrl sourceUrl; QSharedPointer entry; QString entryId; const File *bibTeXfile; Private(AssociatedFilesUI *parent) : p(parent), entry(QSharedPointer()), bibTeXfile(nullptr) { setupGUI(); } void setupGUI() { QBoxLayout *layout = new QVBoxLayout(p); labelGreeting = new QLabel(p); layout->addWidget(labelGreeting); labelGreeting->setWordWrap(true); lineEditSourceUrl = new QLineEdit(p); layout->addWidget(lineEditSourceUrl); lineEditSourceUrl->setReadOnly(true); layout->addSpacing(8); QLabel *label = new QLabel(i18n("The following operations can be performed when associating the document with the entry:"), p); layout->addWidget(label); label->setWordWrap(true); QGroupBox *groupBox = new QGroupBox(i18n("File operation"), p); layout->addWidget(groupBox); QBoxLayout *groupBoxLayout = new QVBoxLayout(groupBox); QButtonGroup *buttonGroup = new QButtonGroup(groupBox); radioNoCopyMove = new QRadioButton(i18n("Do not copy or move document, only insert reference to it"), groupBox); groupBoxLayout->addWidget(radioNoCopyMove); buttonGroup->addButton(radioNoCopyMove); radioCopyFile = new QRadioButton(i18n("Copy document next to bibliography file"), groupBox); groupBoxLayout->addWidget(radioCopyFile); buttonGroup->addButton(radioCopyFile); radioMoveFile = new QRadioButton(i18n("Move document next to bibliography file"), groupBox); groupBoxLayout->addWidget(radioMoveFile); buttonGroup->addButton(radioMoveFile); connect(buttonGroup, static_cast(&QButtonGroup::buttonClicked), p, &AssociatedFilesUI::updateUIandPreview); radioNoCopyMove->setChecked(true); /// by default groupBoxLayout->addSpacing(4); labelMoveCopyLocation = new QLabel(i18n("Path and filename of bibliography file:"), groupBox); groupBoxLayout->addWidget(labelMoveCopyLocation, 1); lineMoveCopyLocation = new QLineEdit(groupBox); lineMoveCopyLocation->setReadOnly(true); groupBoxLayout->addWidget(lineMoveCopyLocation, 1); groupBoxRename = new QGroupBox(i18n("Rename Document?"), p); layout->addWidget(groupBoxRename); QGridLayout *gridLayout = new QGridLayout(groupBoxRename); gridLayout->setColumnMinimumWidth(0, 16); gridLayout->setColumnStretch(0, 0); gridLayout->setColumnStretch(1, 1); buttonGroup = new QButtonGroup(groupBoxRename); radioKeepFilename = new QRadioButton(i18n("Keep document's original filename"), groupBoxRename); gridLayout->addWidget(radioKeepFilename, 0, 0, 1, 2); buttonGroup->addButton(radioKeepFilename); radioRenameToEntryId = new QRadioButton(groupBoxRename); gridLayout->addWidget(radioRenameToEntryId, 1, 0, 1, 2); buttonGroup->addButton(radioRenameToEntryId); radioUserDefinedName = new QRadioButton(i18n("User-defined name:"), groupBoxRename); gridLayout->addWidget(radioUserDefinedName, 2, 0, 1, 2); buttonGroup->addButton(radioUserDefinedName); lineEditUserDefinedName = new QLineEdit(groupBoxRename); gridLayout->addWidget(lineEditUserDefinedName, 3, 1, 1, 1); connect(lineEditUserDefinedName, &QLineEdit::textEdited, p, &AssociatedFilesUI::updateUIandPreview); connect(buttonGroup, static_cast(&QButtonGroup::buttonClicked), p, &AssociatedFilesUI::updateUIandPreview); radioRenameToEntryId->setChecked(true); /// by default groupBoxPathType = new QGroupBox(i18n("Path as Inserted into Entry"), p); buttonGroup = new QButtonGroup(groupBoxPathType); layout->addWidget(groupBoxPathType); groupBoxLayout = new QVBoxLayout(groupBoxPathType); radioRelativePath = new QRadioButton(i18n("Relative Path"), groupBoxPathType); groupBoxLayout->addWidget(radioRelativePath); buttonGroup->addButton(radioRelativePath); radioAbsolutePath = new QRadioButton(i18n("Absolute Path"), groupBoxPathType); groupBoxLayout->addWidget(radioAbsolutePath); buttonGroup->addButton(radioAbsolutePath); connect(buttonGroup, static_cast(&QButtonGroup::buttonClicked), p, &AssociatedFilesUI::updateUIandPreview); radioRelativePath->setChecked(true); /// by default layout->addSpacing(8); label = new QLabel(i18n("Preview of reference to be inserted:"), p); layout->addWidget(label); linePreview = new QLineEdit(p); layout->addWidget(linePreview); linePreview->setReadOnly(true); layout->addStretch(10); } }; QString AssociatedFilesUI::associateUrl(const QUrl &url, QSharedPointer &entry, const File *bibTeXfile, const bool doInsertUrl, QWidget *parent) { QPointer dlg = new QDialog(parent); QBoxLayout *layout = new QVBoxLayout(dlg); QPointer ui = new AssociatedFilesUI(entry->id(), bibTeXfile, dlg); layout->addWidget(ui); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, Qt::Horizontal, dlg); layout->addWidget(buttonBox); dlg->setLayout(layout); connect(buttonBox->button(QDialogButtonBox::Ok), &QPushButton::clicked, dlg.data(), &QDialog::accept); connect(buttonBox->button(QDialogButtonBox::Cancel), &QPushButton::clicked, dlg.data(), &QDialog::reject); if (url.isLocalFile()) ui->setupForLocalFile(url, entry->id()); else ui->setupForRemoteUrl(url, entry->id()); const bool accepted = dlg->exec() == QDialog::Accepted; bool success = false; QString referenceString; if (accepted) { const QUrl newUrl = AssociatedFiles::copyDocument(url, entry->id(), bibTeXfile, ui->renameOperation(), ui->moveCopyOperation(), dlg, ui->userDefinedFilename()); success = newUrl.isValid(); if (success) { referenceString = doInsertUrl ? AssociatedFiles::insertUrl(newUrl, entry, bibTeXfile, ui->pathType()) : AssociatedFiles::computeAssociateUrl(newUrl, bibTeXfile, ui->pathType()); success &= !referenceString.isEmpty(); } } delete dlg; return success ? referenceString : QString(); } AssociatedFilesUI::AssociatedFilesUI(const QString &entryId, const File *bibTeXfile, QWidget *parent) : QWidget(parent), d(new AssociatedFilesUI::Private(this)) { d->entryId = entryId; d->bibTeXfile = bibTeXfile; } AssociatedFilesUI::~AssociatedFilesUI() { delete d; } AssociatedFiles::RenameOperation AssociatedFilesUI::renameOperation() const { if (d->radioRenameToEntryId->isChecked()) return AssociatedFiles::roEntryId; else if (d->radioKeepFilename->isChecked() || d->lineEditUserDefinedName->text().isEmpty()) return AssociatedFiles::roKeepName; else return AssociatedFiles::roUserDefined; } AssociatedFiles::MoveCopyOperation AssociatedFilesUI::moveCopyOperation() const { if (d->radioNoCopyMove->isChecked()) return AssociatedFiles::mcoNoCopyMove; else if (d->radioMoveFile->isChecked()) return AssociatedFiles::mcoMove; else return AssociatedFiles::mcoCopy; } AssociatedFiles::PathType AssociatedFilesUI::pathType() const { return d->radioAbsolutePath->isChecked() ? AssociatedFiles::ptAbsolute : AssociatedFiles::ptRelative; } QString AssociatedFilesUI::userDefinedFilename() const { QString text = d->lineEditUserDefinedName->text(); const int p = qMax(text.lastIndexOf(QLatin1Char('/')), text.lastIndexOf(QDir::separator())); if (p > 0) text = text.mid(p + 1); return text; } void AssociatedFilesUI::updateUIandPreview() { QString preview = i18n("No preview available"); const QString entryId = d->entryId.isEmpty() && !d->entry.isNull() ? d->entry->id() : d->entryId; if (entryId.isEmpty()) { d->radioRenameToEntryId->setEnabled(false); d->radioKeepFilename->setChecked(true); } else d->radioRenameToEntryId->setEnabled(true); if (d->bibTeXfile == nullptr || !d->bibTeXfile->hasProperty(File::Url)) { d->radioRelativePath->setEnabled(false); d->radioAbsolutePath->setChecked(true); d->labelMoveCopyLocation->hide(); d->lineMoveCopyLocation->hide(); } else { d->radioRelativePath->setEnabled(true); d->labelMoveCopyLocation->show(); d->lineMoveCopyLocation->show(); d->lineMoveCopyLocation->setText(d->bibTeXfile->property(File::Url).toUrl().path()); } - if (d->bibTeXfile != nullptr && !d->sourceUrl.isEmpty() && !entryId.isEmpty()) { + if (d->bibTeXfile != nullptr && d->sourceUrl.isValid() && !entryId.isEmpty()) { const QPair newURLs = AssociatedFiles::computeSourceDestinationUrls(d->sourceUrl, entryId, d->bibTeXfile, renameOperation(), d->lineEditUserDefinedName->text()); if (newURLs.second.isValid()) preview = AssociatedFiles::computeAssociateUrl(newURLs.second, d->bibTeXfile, pathType()); } d->linePreview->setText(preview); d->groupBoxRename->setEnabled(!d->radioNoCopyMove->isChecked()); } void AssociatedFilesUI::setupForRemoteUrl(const QUrl &url, const QString &entryId) { d->sourceUrl = url; d->lineEditSourceUrl->setText(url.toDisplayString()); if (entryId.isEmpty()) { d->labelGreeting->setText(i18n("The following remote document is about to be associated with the current entry:")); d->radioRenameToEntryId->setText(i18n("Rename after entry's id")); } else { d->labelGreeting->setText(i18n("The following remote document is about to be associated with entry '%1':", entryId)); d->radioRenameToEntryId->setText(i18n("Rename after entry's id: '%1'", entryId)); } updateUIandPreview(); } void AssociatedFilesUI::setupForLocalFile(const QUrl &url, const QString &entryId) { d->sourceUrl = url; d->lineEditSourceUrl->setText(url.path()); if (entryId.isEmpty()) { d->labelGreeting->setText(i18n("The following local document is about to be associated with the current entry:")); d->radioRenameToEntryId->setText(i18n("Rename after entry's id")); } else { d->labelGreeting->setText(i18n("The following local document is about to be associated with entry '%1':", entryId)); d->radioRenameToEntryId->setText(i18n("Rename after entry's id: '%1'", entryId)); } updateUIandPreview(); } diff --git a/src/gui/field/fieldlistedit.cpp b/src/gui/field/fieldlistedit.cpp index c9ff25f3..908384a3 100644 --- a/src/gui/field/fieldlistedit.cpp +++ b/src/gui/field/fieldlistedit.cpp @@ -1,719 +1,719 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, see . * ***************************************************************************/ #include "fieldlistedit.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "fieldlineedit.h" #include "element/associatedfilesui.h" #include "logging_gui.h" class FieldListEdit::FieldListEditProtected { private: FieldListEdit *p; const int innerSpacing; QVBoxLayout *layout; KBibTeX::TypeFlag preferredTypeFlag; KBibTeX::TypeFlags typeFlags; public: QList lineEditList; QWidget *pushButtonContainer; QBoxLayout *pushButtonContainerLayout; QPushButton *addLineButton; const File *file; QString fieldKey; QWidget *container; QScrollArea *scrollArea; bool m_isReadOnly; QStringList completionItems; FieldListEditProtected(KBibTeX::TypeFlag ptf, KBibTeX::TypeFlags tf, FieldListEdit *parent) : p(parent), innerSpacing(4), preferredTypeFlag(ptf), typeFlags(tf), file(nullptr), m_isReadOnly(false) { setupGUI(); } FieldListEditProtected(const FieldListEditProtected &other) = delete; FieldListEditProtected &operator= (const FieldListEditProtected &other) = delete; void setupGUI() { QBoxLayout *outerLayout = new QVBoxLayout(p); outerLayout->setMargin(0); outerLayout->setSpacing(0); scrollArea = new QScrollArea(p); outerLayout->addWidget(scrollArea); container = new QWidget(scrollArea->viewport()); container->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Minimum); scrollArea->setWidget(container); layout = new QVBoxLayout(container); layout->setMargin(0); layout->setSpacing(innerSpacing); pushButtonContainer = new QWidget(container); pushButtonContainerLayout = new QHBoxLayout(pushButtonContainer); pushButtonContainerLayout->setMargin(0); layout->addWidget(pushButtonContainer); addLineButton = new QPushButton(QIcon::fromTheme(QStringLiteral("list-add")), i18n("Add"), pushButtonContainer); addLineButton->setObjectName(QStringLiteral("addButton")); connect(addLineButton, &QPushButton::clicked, p, static_cast(&FieldListEdit::lineAdd)); connect(addLineButton, &QPushButton::clicked, p, &FieldListEdit::modified); pushButtonContainerLayout->addWidget(addLineButton); layout->addStretch(100); scrollArea->setBackgroundRole(QPalette::Base); scrollArea->ensureWidgetVisible(container); scrollArea->setWidgetResizable(true); } void addButton(QPushButton *button) { button->setParent(pushButtonContainer); pushButtonContainerLayout->addWidget(button); } int recommendedHeight() { int heightHint = 0; for (const auto *fieldLineEdit : const_cast &>(lineEditList)) heightHint += fieldLineEdit->sizeHint().height(); heightHint += lineEditList.count() * innerSpacing; heightHint += addLineButton->sizeHint().height(); return heightHint; } FieldLineEdit *addFieldLineEdit() { FieldLineEdit *le = new FieldLineEdit(preferredTypeFlag, typeFlags, false, container); le->setFile(file); le->setAcceptDrops(false); le->setReadOnly(m_isReadOnly); le->setInnerWidgetsTransparency(true); layout->insertWidget(layout->count() - 2, le); lineEditList.append(le); QPushButton *remove = new QPushButton(QIcon::fromTheme(QStringLiteral("list-remove")), QString(), le); remove->setToolTip(i18n("Remove value")); le->appendWidget(remove); connect(remove, &QPushButton::clicked, p, [this, le]() { removeFieldLineEdit(le); const QSize size(container->width(), recommendedHeight()); container->resize(size); /// Instead of an 'emit' ... QMetaObject::invokeMethod(p, "modified", Qt::DirectConnection, QGenericReturnArgument()); }); QPushButton *goDown = new QPushButton(QIcon::fromTheme(QStringLiteral("go-down")), QString(), le); goDown->setToolTip(i18n("Move value down")); le->appendWidget(goDown); connect(goDown, &QPushButton::clicked, p, [this, le]() { const bool gotModified = goDownFieldLineEdit(le); if (gotModified) { /// Instead of an 'emit' ... QMetaObject::invokeMethod(p, "modified", Qt::DirectConnection, QGenericReturnArgument()); } }); QPushButton *goUp = new QPushButton(QIcon::fromTheme(QStringLiteral("go-up")), QString(), le); goUp->setToolTip(i18n("Move value up")); le->appendWidget(goUp); connect(goUp, &QPushButton::clicked, p, [this, le]() { const bool gotModified = goUpFieldLineEdit(le); if (gotModified) { /// Instead of an 'emit' ... QMetaObject::invokeMethod(p, "modified", Qt::DirectConnection, QGenericReturnArgument()); } }); connect(le, &FieldLineEdit::textChanged, p, &FieldListEdit::modified); return le; } void removeAllFieldLineEdits() { while (!lineEditList.isEmpty()) { FieldLineEdit *fieldLineEdit = *lineEditList.begin(); layout->removeWidget(fieldLineEdit); lineEditList.removeFirst(); delete fieldLineEdit; } /// This fixes a layout problem where the container element /// does not shrink correctly once the line edits have been /// removed QSize pSize = container->size(); pSize.setHeight(addLineButton->height()); container->resize(pSize); } void removeFieldLineEdit(FieldLineEdit *fieldLineEdit) { lineEditList.removeOne(fieldLineEdit); layout->removeWidget(fieldLineEdit); delete fieldLineEdit; } bool goDownFieldLineEdit(FieldLineEdit *fieldLineEdit) { int idx = lineEditList.indexOf(fieldLineEdit); if (idx < lineEditList.count() - 1) { layout->removeWidget(fieldLineEdit); lineEditList.removeOne(fieldLineEdit); lineEditList.insert(idx + 1, fieldLineEdit); layout->insertWidget(idx + 1, fieldLineEdit); return true; ///< return 'true' upon actual modification } return false; ///< return 'false' if nothing changed, i.e. FieldLineEdit is already at bottom } bool goUpFieldLineEdit(FieldLineEdit *fieldLineEdit) { int idx = lineEditList.indexOf(fieldLineEdit); if (idx > 0) { layout->removeWidget(fieldLineEdit); lineEditList.removeOne(fieldLineEdit); lineEditList.insert(idx - 1, fieldLineEdit); layout->insertWidget(idx - 1, fieldLineEdit); return true; ///< return 'true' upon actual modification } return false; ///< return 'false' if nothing changed, i.e. FieldLineEdit is already at top } }; FieldListEdit::FieldListEdit(KBibTeX::TypeFlag preferredTypeFlag, KBibTeX::TypeFlags typeFlags, QWidget *parent) : QWidget(parent), d(new FieldListEditProtected(preferredTypeFlag, typeFlags, this)) { setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); setMinimumSize(fontMetrics().averageCharWidth() * 30, fontMetrics().averageCharWidth() * 10); setAcceptDrops(true); } FieldListEdit::~FieldListEdit() { delete d; } bool FieldListEdit::reset(const Value &value) { d->removeAllFieldLineEdits(); for (const auto &valueItem : value) { Value v; v.append(valueItem); FieldLineEdit *fieldLineEdit = addFieldLineEdit(); fieldLineEdit->setFile(d->file); fieldLineEdit->reset(v); } QSize size(d->container->width(), d->recommendedHeight()); d->container->resize(size); return true; } bool FieldListEdit::apply(Value &value) const { value.clear(); for (const auto *fieldLineEdit : const_cast &>(d->lineEditList)) { Value v; fieldLineEdit->apply(v); for (const auto &valueItem : const_cast(v)) value.append(valueItem); } return true; } bool FieldListEdit::validate(QWidget **widgetWithIssue, QString &message) const { for (const auto *fieldLineEdit : const_cast &>(d->lineEditList)) { const bool v = fieldLineEdit->validate(widgetWithIssue, message); if (!v) return false; } return true; } void FieldListEdit::clear() { d->removeAllFieldLineEdits(); } void FieldListEdit::setReadOnly(bool isReadOnly) { d->m_isReadOnly = isReadOnly; for (FieldLineEdit *fieldLineEdit : const_cast &>(d->lineEditList)) fieldLineEdit->setReadOnly(isReadOnly); d->addLineButton->setEnabled(!isReadOnly); } void FieldListEdit::setFile(const File *file) { d->file = file; for (FieldLineEdit *fieldLineEdit : const_cast &>(d->lineEditList)) fieldLineEdit->setFile(file); } void FieldListEdit::setElement(const Element *element) { m_element = element; for (FieldLineEdit *fieldLineEdit : const_cast &>(d->lineEditList)) fieldLineEdit->setElement(element); } void FieldListEdit::setFieldKey(const QString &fieldKey) { d->fieldKey = fieldKey; for (FieldLineEdit *fieldLineEdit : const_cast &>(d->lineEditList)) fieldLineEdit->setFieldKey(fieldKey); } void FieldListEdit::setCompletionItems(const QStringList &items) { d->completionItems = items; for (FieldLineEdit *fieldLineEdit : const_cast &>(d->lineEditList)) fieldLineEdit->setCompletionItems(items); } FieldLineEdit *FieldListEdit::addFieldLineEdit() { return d->addFieldLineEdit(); } void FieldListEdit::addButton(QPushButton *button) { d->addButton(button); } void FieldListEdit::dragEnterEvent(QDragEnterEvent *event) { if (event->mimeData()->hasFormat(QStringLiteral("text/plain")) || event->mimeData()->hasFormat(QStringLiteral("text/x-bibtex"))) event->acceptProposedAction(); } void FieldListEdit::dropEvent(QDropEvent *event) { const QString clipboardText = event->mimeData()->text(); if (clipboardText.isEmpty()) return; const File *file = nullptr; if (!d->fieldKey.isEmpty() && clipboardText.startsWith(QStringLiteral("@"))) { FileImporterBibTeX importer(this); file = importer.fromString(clipboardText); const QSharedPointer entry = (file != nullptr && file->count() == 1) ? file->first().dynamicCast() : QSharedPointer(); if (file != nullptr && !entry.isNull() && d->fieldKey == QStringLiteral("^external")) { /// handle "external" list differently const auto urlList = FileInfo::entryUrls(entry, QUrl(file->property(File::Url).toUrl()), FileInfo::TestExistenceNo); Value v; v.reserve(urlList.size()); for (const QUrl &url : urlList) { v.append(QSharedPointer(new VerbatimText(url.url(QUrl::PreferLocalFile)))); } reset(v); emit modified(); return; } else if (!entry.isNull() && entry->contains(d->fieldKey)) { /// case for "normal" lists like for authors, editors, ... reset(entry->value(d->fieldKey)); emit modified(); return; } } if (file == nullptr || file->count() == 0) { /// fall-back case: single field line edit with text d->removeAllFieldLineEdits(); FieldLineEdit *fle = addFieldLineEdit(); fle->setText(clipboardText); emit modified(); } } void FieldListEdit::lineAdd(Value *value) { FieldLineEdit *le = addFieldLineEdit(); le->setCompletionItems(d->completionItems); if (value != nullptr) le->reset(*value); } void FieldListEdit::lineAdd() { FieldLineEdit *newEdit = addFieldLineEdit(); newEdit->setCompletionItems(d->completionItems); QSize size(d->container->width(), d->recommendedHeight()); d->container->resize(size); newEdit->setFocus(Qt::ShortcutFocusReason); } PersonListEdit::PersonListEdit(KBibTeX::TypeFlag preferredTypeFlag, KBibTeX::TypeFlags typeFlags, QWidget *parent) : FieldListEdit(preferredTypeFlag, typeFlags, parent) { m_checkBoxOthers = new QCheckBox(i18n("... and others (et al.)"), this); connect(m_checkBoxOthers, &QCheckBox::toggled, this, &PersonListEdit::modified); QBoxLayout *boxLayout = static_cast(layout()); boxLayout->addWidget(m_checkBoxOthers); m_buttonAddNamesFromClipboard = new QPushButton(QIcon::fromTheme(QStringLiteral("edit-paste")), i18n("Add from Clipboard"), this); m_buttonAddNamesFromClipboard->setToolTip(i18n("Add a list of names from clipboard")); addButton(m_buttonAddNamesFromClipboard); connect(m_buttonAddNamesFromClipboard, &QPushButton::clicked, this, &PersonListEdit::slotAddNamesFromClipboard); } bool PersonListEdit::reset(const Value &value) { Value internal = value; m_checkBoxOthers->setCheckState(Qt::Unchecked); QSharedPointer pt; if (!internal.isEmpty() && !(pt = internal.last().dynamicCast<PlainText>()).isNull()) { if (pt->text() == QStringLiteral("others")) { internal.erase(internal.end() - 1); m_checkBoxOthers->setCheckState(Qt::Checked); } } return FieldListEdit::reset(internal); } bool PersonListEdit::apply(Value &value) const { bool result = FieldListEdit::apply(value); if (result && m_checkBoxOthers->checkState() == Qt::Checked) value.append(QSharedPointer<PlainText>(new PlainText(QStringLiteral("others")))); return result; } void PersonListEdit::setReadOnly(bool isReadOnly) { FieldListEdit::setReadOnly(isReadOnly); m_checkBoxOthers->setEnabled(!isReadOnly); m_buttonAddNamesFromClipboard->setEnabled(!isReadOnly); } void PersonListEdit::slotAddNamesFromClipboard() { QClipboard *clipboard = QApplication::clipboard(); QString text = clipboard->text(QClipboard::Clipboard); if (text.isEmpty()) text = clipboard->text(QClipboard::Selection); if (!text.isEmpty()) { const QList<QSharedPointer<Person> > personList = FileImporterBibTeX::splitNames(text); for (const QSharedPointer<Person> &person : personList) { Value *value = new Value(); value->append(person); lineAdd(value); delete value; } if (!personList.isEmpty()) emit modified(); } } UrlListEdit::UrlListEdit(QWidget *parent) : FieldListEdit(KBibTeX::tfVerbatim, KBibTeX::tfVerbatim, parent) { m_buttonAddFile = new QPushButton(QIcon::fromTheme(QStringLiteral("list-add")), i18n("Add file..."), this); addButton(m_buttonAddFile); QMenu *menuAddFile = new QMenu(m_buttonAddFile); m_buttonAddFile->setMenu(menuAddFile); connect(m_buttonAddFile, &QPushButton::clicked, m_buttonAddFile, &QPushButton::showMenu); menuAddFile->addAction(QIcon::fromTheme(QStringLiteral("emblem-symbolic-link")), i18n("Add reference ..."), this, SLOT(slotAddReference())); menuAddFile->addAction(QIcon::fromTheme(QStringLiteral("emblem-symbolic-link")), i18n("Add reference from clipboard"), this, SLOT(slotAddReferenceFromClipboard())); } void UrlListEdit::slotAddReference() { QUrl bibtexUrl(d->file != nullptr ? d->file->property(File::Url, QVariant()).toUrl() : QUrl()); - if (!bibtexUrl.isEmpty()) { + if (bibtexUrl.isValid()) { const QFileInfo fi(bibtexUrl.path()); bibtexUrl.setPath(fi.absolutePath()); } const QUrl documentUrl = QFileDialog::getOpenFileUrl(this, i18n("File to Associate"), bibtexUrl); - if (!documentUrl.isEmpty()) + if (documentUrl.isValid()) addReference(documentUrl); } void UrlListEdit::slotAddReferenceFromClipboard() { const QUrl url = QUrl::fromUserInput(QApplication::clipboard()->text()); - if (!url.isEmpty()) + if (url.isValid()) addReference(url); } void UrlListEdit::addReference(const QUrl &url) { const Entry *entry = dynamic_cast<const Entry *>(m_element); if (entry != nullptr) { QSharedPointer<Entry> fakeTempEntry(new Entry(entry->type(), entry->id())); const QString visibleFilename = AssociatedFilesUI::associateUrl(url, fakeTempEntry, d->file, false, this); if (!visibleFilename.isEmpty()) { Value *value = new Value(); value->append(QSharedPointer<VerbatimText>(new VerbatimText(visibleFilename))); lineAdd(value); delete value; emit modified(); } } } void UrlListEdit::downloadAndSaveLocally(const QUrl &url) { /// Only proceed if Url is valid and points to a remote location if (url.isValid() && !url.isLocalFile()) { /// Get filename from url (without any path/directory part) QString filename = url.fileName(); /// Build QFileInfo from current BibTeX file if available QFileInfo bibFileinfo = d->file != nullptr ? QFileInfo(d->file->property(File::Url).toUrl().path()) : QFileInfo(); /// Build proposal to a local filename for remote file filename = bibFileinfo.isFile() ? bibFileinfo.absolutePath() + QDir::separator() + filename : filename; /// Ask user for actual local filename to save remote file to filename = QFileDialog::getSaveFileName(this, i18n("Save file locally"), filename, QStringLiteral("application/pdf application/postscript image/vnd.djvu")); /// Check if user entered a valid filename ... if (!filename.isEmpty()) { /// Ask user if reference to local file should be /// relative or absolute in relation to the BibTeX file const QString absoluteFilename = filename; QString visibleFilename = filename; if (bibFileinfo.isFile()) visibleFilename = askRelativeOrStaticFilename(this, absoluteFilename, d->file->property(File::Url).toUrl()); /// Download remote file and save it locally setEnabled(false); setCursor(Qt::WaitCursor); KIO::CopyJob *job = KIO::copy(url, QUrl::fromLocalFile(absoluteFilename), KIO::Overwrite); job->setProperty("visibleFilename", QVariant::fromValue<QString>(visibleFilename)); connect(job, &KJob::result, this, &UrlListEdit::downloadFinished); } } } void UrlListEdit::downloadFinished(KJob *j) { KIO::CopyJob *job = static_cast<KIO::CopyJob *>(j); if (job->error() == 0) { /// Download succeeded, add reference to local file to this BibTeX entry Value *value = new Value(); value->append(QSharedPointer<VerbatimText>(new VerbatimText(job->property("visibleFilename").toString()))); lineAdd(value); delete value; } else { qCWarning(LOG_KBIBTEX_GUI) << "Downloading" << (*job->srcUrls().constBegin()).toDisplayString() << "failed with error" << job->error() << job->errorString(); } setEnabled(true); unsetCursor(); } void UrlListEdit::textChanged(QPushButton *buttonSaveLocally, FieldLineEdit *fieldLineEdit) { if (buttonSaveLocally == nullptr || fieldLineEdit == nullptr) return; ///< should never happen! /// Create URL from new text to make some tests on it /// Only remote URLs are of interest, therefore no tests /// on local file or relative paths const QString newText = fieldLineEdit->text(); const QString lowerText = newText.toLower(); /// Enable button only if Url is valid and points to a remote /// DjVu, PDF, or PostScript file // TODO more file types? const bool canBeSaved = lowerText.contains(QStringLiteral("://")) && (lowerText.endsWith(QStringLiteral(".djvu")) || lowerText.endsWith(QStringLiteral(".pdf")) || lowerText.endsWith(QStringLiteral(".ps"))); buttonSaveLocally->setEnabled(canBeSaved); buttonSaveLocally->setToolTip(canBeSaved ? i18n("Save file '%1' locally", newText) : QString()); } QString UrlListEdit::askRelativeOrStaticFilename(QWidget *parent, const QString &absoluteFilename, const QUrl &baseUrl) { - QFileInfo baseUrlInfo = baseUrl.isEmpty() ? QFileInfo() : QFileInfo(baseUrl.path()); + QFileInfo baseUrlInfo = baseUrl.isValid() ? QFileInfo(baseUrl.path()) : QFileInfo(); QFileInfo filenameInfo(absoluteFilename); - if (!baseUrl.isEmpty() && (filenameInfo.absolutePath() == baseUrlInfo.absolutePath() || filenameInfo.absolutePath().startsWith(baseUrlInfo.absolutePath() + QDir::separator()))) { + if (baseUrl.isValid() && (filenameInfo.absolutePath() == baseUrlInfo.absolutePath() || filenameInfo.absolutePath().startsWith(baseUrlInfo.absolutePath() + QDir::separator()))) { // TODO cover level-up cases like "../../test.pdf" const QString relativePath = filenameInfo.absolutePath().mid(baseUrlInfo.absolutePath().length() + 1); const QString relativeFilename = relativePath + (relativePath.isEmpty() ? QString() : QString(QDir::separator())) + filenameInfo.fileName(); if (KMessageBox::questionYesNo(parent, i18n("<qt><p>Use a filename relative to the bibliography file?</p><p>The relative path would be<br/><tt style=\"font-family: %3;\">%1</tt></p><p>The absolute path would be<br/><tt style=\"font-family: %3;\">%2</tt></p></qt>", relativeFilename, absoluteFilename, QFontDatabase::systemFont(QFontDatabase::FixedFont).family()), i18n("Relative Path"), KGuiItem(i18n("Relative Path")), KGuiItem(i18n("Absolute Path"))) == KMessageBox::Yes) return relativeFilename; } return absoluteFilename; } FieldLineEdit *UrlListEdit::addFieldLineEdit() { /// Call original implementation to get an instance of a FieldLineEdit FieldLineEdit *fieldLineEdit = FieldListEdit::addFieldLineEdit(); /// Create a new "save locally" button QPushButton *buttonSaveLocally = new QPushButton(QIcon::fromTheme(QStringLiteral("document-save")), QString(), fieldLineEdit); buttonSaveLocally->setToolTip(i18n("Save file locally")); buttonSaveLocally->setEnabled(false); /// Append button to new FieldLineEdit fieldLineEdit->appendWidget(buttonSaveLocally); /// Connect signals to react on button events /// or changes in the FieldLineEdit's text connect(buttonSaveLocally, &QPushButton::clicked, this, [this, fieldLineEdit]() { downloadAndSaveLocally(QUrl::fromUserInput(fieldLineEdit->text())); }); connect(fieldLineEdit, &FieldLineEdit::textChanged, this, [this, buttonSaveLocally, fieldLineEdit]() { textChanged(buttonSaveLocally, fieldLineEdit); }); return fieldLineEdit; } void UrlListEdit::setReadOnly(bool isReadOnly) { FieldListEdit::setReadOnly(isReadOnly); m_buttonAddFile->setEnabled(!isReadOnly); } const QString KeywordListEdit::keyGlobalKeywordList = QStringLiteral("globalKeywordList"); KeywordListEdit::KeywordListEdit(QWidget *parent) : FieldListEdit(KBibTeX::tfKeyword, KBibTeX::tfKeyword | KBibTeX::tfSource, parent), m_config(KSharedConfig::openConfig(QStringLiteral("kbibtexrc"))), m_configGroupName(QStringLiteral("Global Keywords")) { m_buttonAddKeywordsFromList = new QPushButton(QIcon::fromTheme(QStringLiteral("list-add")), i18n("Add Keywords from List"), this); m_buttonAddKeywordsFromList->setToolTip(i18n("Add keywords as selected from a pre-defined list of keywords")); addButton(m_buttonAddKeywordsFromList); connect(m_buttonAddKeywordsFromList, &QPushButton::clicked, this, &KeywordListEdit::slotAddKeywordsFromList); m_buttonAddKeywordsFromClipboard = new QPushButton(QIcon::fromTheme(QStringLiteral("edit-paste")), i18n("Add Keywords from Clipboard"), this); m_buttonAddKeywordsFromClipboard->setToolTip(i18n("Add a punctuation-separated list of keywords from clipboard")); addButton(m_buttonAddKeywordsFromClipboard); connect(m_buttonAddKeywordsFromClipboard, &QPushButton::clicked, this, &KeywordListEdit::slotAddKeywordsFromClipboard); } void KeywordListEdit::slotAddKeywordsFromList() { /// fetch stored, global keywords KConfigGroup configGroup(m_config, m_configGroupName); QStringList keywords = configGroup.readEntry(KeywordListEdit::keyGlobalKeywordList, QStringList()); /// use a map for case-insensitive sorting of strings /// (recommended by Qt's documentation) QMap<QString, QString> forCaseInsensitiveSorting; /// insert all stored, global keywords for (const QString &keyword : const_cast<const QStringList &>(keywords)) forCaseInsensitiveSorting.insert(keyword.toLower(), keyword); /// insert all unique keywords used in this file for (const QString &keyword : const_cast<const QSet<QString> &>(m_keywordsFromFile)) forCaseInsensitiveSorting.insert(keyword.toLower(), keyword); /// re-create string list from map's values keywords = forCaseInsensitiveSorting.values(); // FIXME QInputDialog does not have a 'getItemList' /* bool ok = false; const QStringList newKeywordList = KInputDialog::getItemList(i18n("Add Keywords"), i18n("Select keywords to add:"), keywords, QStringList(), true, &ok, this); if (ok) { for(const QString &newKeywordText : newKeywordList) { Value *value = new Value(); value->append(QSharedPointer<Keyword>(new Keyword(newKeywordText))); lineAdd(value); delete value; } if (!newKeywordList.isEmpty()) emit modified(); } */ } void KeywordListEdit::slotAddKeywordsFromClipboard() { QClipboard *clipboard = QApplication::clipboard(); QString text = clipboard->text(QClipboard::Clipboard); if (text.isEmpty()) ///< use "mouse" clipboard as fallback text = clipboard->text(QClipboard::Selection); if (!text.isEmpty()) { const QList<QSharedPointer<Keyword> > keywords = FileImporterBibTeX::splitKeywords(text); for (const auto &keyword : keywords) { Value *value = new Value(); value->append(keyword); lineAdd(value); delete value; } if (!keywords.isEmpty()) emit modified(); } } void KeywordListEdit::setReadOnly(bool isReadOnly) { FieldListEdit::setReadOnly(isReadOnly); m_buttonAddKeywordsFromList->setEnabled(!isReadOnly); m_buttonAddKeywordsFromClipboard->setEnabled(!isReadOnly); } void KeywordListEdit::setFile(const File *file) { if (file == nullptr) m_keywordsFromFile.clear(); else m_keywordsFromFile = file->uniqueEntryValuesSet(Entry::ftKeywords); FieldListEdit::setFile(file); } void KeywordListEdit::setCompletionItems(const QStringList &items) { /// fetch stored, global keywords KConfigGroup configGroup(m_config, m_configGroupName); QStringList keywords = configGroup.readEntry(KeywordListEdit::keyGlobalKeywordList, QStringList()); /// use a map for case-insensitive sorting of strings /// (recommended by Qt's documentation) QMap<QString, QString> forCaseInsensitiveSorting; /// insert all stored, global keywords for (const QString &keyword : const_cast<const QStringList &>(keywords)) forCaseInsensitiveSorting.insert(keyword.toLower(), keyword); /// insert all unique keywords used in this file for (const QString &keyword : const_cast<const QStringList &>(items)) forCaseInsensitiveSorting.insert(keyword.toLower(), keyword); /// re-create string list from map's values keywords = forCaseInsensitiveSorting.values(); FieldListEdit::setCompletionItems(keywords); } diff --git a/src/io/fileinfo.cpp b/src/io/fileinfo.cpp index 538b7f29..78fc9595 100644 --- a/src/io/fileinfo.cpp +++ b/src/io/fileinfo.cpp @@ -1,371 +1,371 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, see <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "fileinfo.h" #include <poppler-qt5.h> #include <QFileInfo> #include <QMimeDatabase> #include <QDir> #include <QTextStream> #include <QStandardPaths> #include <QRegularExpression> #include <QtConcurrentRun> #include <KBibTeX> #include <Entry> #include "logging_io.h" FileInfo::FileInfo() { /// nothing } const QString FileInfo::mimetypeOctetStream = QStringLiteral("application/octet-stream"); const QString FileInfo::mimetypeHTML = QStringLiteral("text/html"); const QString FileInfo::mimetypeBibTeX = QStringLiteral("text/x-bibtex"); const QString FileInfo::mimetypeRIS = QStringLiteral("application/x-research-info-systems"); const QString FileInfo::mimetypePDF = QStringLiteral("application/pdf"); QMimeType FileInfo::mimeTypeForUrl(const QUrl &url) { - if (!url.isValid() || url.isEmpty()) { + if (!url.isValid()) { qCWarning(LOG_KBIBTEX_IO) << "Cannot determine mime type for empty or invalid QUrl"; return QMimeType(); ///< invalid input gives invalid mime type } static const QMimeDatabase db; static const QMimeType mtHTML(db.mimeTypeForName(mimetypeHTML)); static const QMimeType mtOctetStream(db.mimeTypeForName(mimetypeOctetStream)); static const QMimeType mtBibTeX(db.mimeTypeForName(mimetypeBibTeX)); static const QMimeType mtPDF(db.mimeTypeForName(mimetypePDF)); static const QMimeType mtRIS(db.mimeTypeForName(mimetypeRIS)); /// Test if mime type for BibTeX is registered before determining file extension static const QString mimetypeBibTeXExt = mtBibTeX.preferredSuffix(); /// Test if mime type for RIS is registered before determining file extension static const QString mimetypeRISExt = mtRIS.preferredSuffix(); /// Test if mime type for PDF is registered before determining file extension static const QString mimetypePDFExt = mtPDF.preferredSuffix(); const QString extension = db.suffixForFileName(url.fileName()).toLower(); /// First, check preferred suffixes if (extension == mimetypeBibTeXExt) return mtBibTeX; else if (extension == mimetypeRISExt) return mtRIS; else if (extension == mimetypePDFExt) return mtPDF; /// Second, check any other suffixes else if (mtBibTeX.suffixes().contains(extension)) return mtBibTeX; else if (mtRIS.suffixes().contains(extension)) return mtRIS; else if (mtPDF.suffixes().contains(extension)) return mtPDF; /// Let the KDE subsystem guess the mime type QMimeType result = db.mimeTypeForUrl(url); /// Fall back to application/octet-stream if something goes wrong if (!result.isValid()) result = mtOctetStream; /// In case that KDE could not determine mime type, /// do some educated guesses on our own if (result.name() == mimetypeOctetStream) { if (url.scheme().startsWith(QStringLiteral("http"))) result = mtHTML; // TODO more tests? } return result; } void FileInfo::urlsInText(const QString &text, const TestExistence testExistence, const QString &baseDirectory, QSet<QUrl> &result) { if (text.isEmpty()) return; /// DOI identifiers have to extracted first as KBibTeX::fileListSeparatorRegExp /// contains characters that can be part of a DOI (e.g. ';') and thus could split /// a DOI in between. QString internalText = text; int pos = 0; QRegularExpressionMatch doiRegExpMatch; while ((doiRegExpMatch = KBibTeX::doiRegExp.match(internalText, pos)).hasMatch()) { pos = doiRegExpMatch.capturedStart(0); QString doiMatch = doiRegExpMatch.captured(0); const int semicolonHttpPos = doiMatch.indexOf(QStringLiteral(";http")); if (semicolonHttpPos > 0) doiMatch = doiMatch.left(semicolonHttpPos); const QUrl url(KBibTeX::doiUrlPrefix + QString(doiMatch).remove(QStringLiteral("\\"))); if (url.isValid() && !result.contains(url)) result << url; /// remove match from internal text to avoid duplicates /// Cut away any URL that may be right before found DOI number: /// For example, if DOI '10.1000/38-abc' was found in /// 'Lore ipsum http://doi.example.org/10.1000/38-abc Lore ipsum' /// also remove 'http://doi.example.org/' from the text, keeping only /// 'Lore ipsum Lore ipsum' static const QRegularExpression genericDoiUrlPrefix(QStringLiteral("http[s]?://[a-z0-9./-]+/$")); ///< looks like an URL const QRegularExpressionMatch genericDoiUrlPrefixMatch = genericDoiUrlPrefix.match(internalText.left(pos)); if (genericDoiUrlPrefixMatch.hasMatch()) /// genericDoiUrlPrefixMatch.captured(0) may contain (parts of) DOI internalText = internalText.left(genericDoiUrlPrefixMatch.capturedStart(0)) + internalText.mid(pos + doiMatch.length()); else internalText = internalText.left(pos) + internalText.mid(pos + doiMatch.length()); } const QStringList fileList = internalText.split(KBibTeX::fileListSeparatorRegExp, QString::SkipEmptyParts); for (const QString &text : fileList) { internalText = text; /// If testing for the actual existence of a filename found in the text ... if (testExistence == TestExistenceYes) { /// If a base directory (e.g. the location of the parent .bib file) is given /// and the potential filename fragment is NOT an absolute path, ... if (internalText.startsWith(QStringLiteral("~") + QDir::separator())) { const QString fullFilename = QDir::homePath() + internalText.mid(1); const QFileInfo fileInfo(fullFilename); const QUrl url = QUrl::fromLocalFile(fileInfo.canonicalFilePath()); if (fileInfo.exists() && fileInfo.isFile() && url.isValid() && !result.contains(url)) { result << url; /// Stop searching for URLs or filenames in current internal text continue; } } else if (!baseDirectory.isEmpty() && // TODO the following test assumes that absolute paths start // with a dir separator, which may only be true on Unix/Linux, // but not Windows. May be a test for 'first character is a letter, // second is ":", third is "\"' may be necessary. !internalText.startsWith(QDir::separator())) { /// To get the absolute path, prepend filename fragment with base directory const QString fullFilename = baseDirectory + QDir::separator() + internalText; const QFileInfo fileInfo(fullFilename); const QUrl url = QUrl::fromLocalFile(fileInfo.canonicalFilePath()); if (fileInfo.exists() && fileInfo.isFile() && url.isValid() && !result.contains(url)) { result << url; /// Stop searching for URLs or filenames in current internal text continue; } } else { /// Either the filename fragment is an absolute path OR no base directory /// was given (current working directory is assumed), ... const QFileInfo fileInfo(internalText); const QUrl url = QUrl::fromLocalFile(fileInfo.canonicalFilePath()); if (fileInfo.exists() && fileInfo.isFile() && url.isValid() && !result.contains(url)) { result << url; /// stop searching for URLs or filenames in current internal text continue; } } } /// extract URL from current field pos = 0; QRegularExpressionMatch urlRegExpMatch; while ((urlRegExpMatch = KBibTeX::urlRegExp.match(internalText, pos)).hasMatch()) { pos = urlRegExpMatch.capturedStart(0); const QString match = urlRegExpMatch.captured(0); QUrl url(match); if (url.isValid() && (testExistence == TestExistenceNo || !url.isLocalFile() || QFileInfo::exists(url.toLocalFile())) && !result.contains(url)) result << url; /// remove match from internal text to avoid duplicates internalText = internalText.left(pos) + internalText.mid(pos + match.length()); } /// explicitly check URL entry, may be an URL even if http:// or alike is missing pos = 0; QRegularExpressionMatch domainNameRegExpMatch; while ((domainNameRegExpMatch = KBibTeX::domainNameRegExp.match(internalText, pos)).hasMatch()) { pos = domainNameRegExpMatch.capturedStart(0); int pos2 = internalText.indexOf(QStringLiteral(" "), pos + 1); if (pos2 < 0) pos2 = internalText.length(); QString match = internalText.mid(pos, pos2 - pos); const QUrl url(QStringLiteral("http://") + match); // FIXME what about HTTPS? if (url.isValid() && !result.contains(url)) result << url; /// remove match from internal text to avoid duplicates internalText = internalText.left(pos) + internalText.mid(pos + match.length()); } /// extract general file-like patterns pos = 0; QRegularExpressionMatch fileRegExpMatch; while ((fileRegExpMatch = KBibTeX::fileRegExp.match(internalText, pos)).hasMatch()) { pos = fileRegExpMatch.capturedStart(0); const QString match = fileRegExpMatch.captured(0); QUrl url(match); if (url.isValid() && (testExistence == TestExistenceNo || !url.isLocalFile() || QFileInfo::exists(url.toLocalFile())) && !result.contains(url)) result << url; /// remove match from internal text to avoid duplicates internalText = internalText.left(pos) + internalText.mid(pos + match.length()); } } } QSet<QUrl> FileInfo::entryUrls(const QSharedPointer<const Entry> &entry, const QUrl &bibTeXUrl, TestExistence testExistence) { QSet<QUrl> result; if (entry.isNull() || entry->isEmpty()) return result; if (entry->contains(Entry::ftDOI)) { const QString doi = PlainTextValue::text(entry->value(Entry::ftDOI)); QRegularExpressionMatch doiRegExpMatch; if (!doi.isEmpty() && (doiRegExpMatch = KBibTeX::doiRegExp.match(doi)).hasMatch()) { QString match = doiRegExpMatch.captured(0); QUrl url(KBibTeX::doiUrlPrefix + match.remove(QStringLiteral("\\"))); result.insert(url); } } static const QString etPMID = QStringLiteral("pmid"); if (entry->contains(etPMID)) { const QString pmid = PlainTextValue::text(entry->value(etPMID)); bool ok = false; ok &= pmid.toInt(&ok) > 0; if (ok) { QUrl url(QStringLiteral("https://www.ncbi.nlm.nih.gov/pubmed/") + pmid); result.insert(url); } } static const QString etEPrint = QStringLiteral("eprint"); if (entry->contains(etEPrint)) { const QString eprint = PlainTextValue::text(entry->value(etEPrint)); if (!eprint.isEmpty()) { QUrl url(QStringLiteral("http://arxiv.org/search?query=") + eprint); result.insert(url); } } const QString baseDirectory = bibTeXUrl.isValid() ? bibTeXUrl.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path() : QString(); for (Entry::ConstIterator it = entry->constBegin(); it != entry->constEnd(); ++it) { /// skip abstracts, they contain sometimes strange text fragments /// that are mistaken for URLs if (it.key().toLower() == Entry::ftAbstract) continue; const Value v = it.value(); for (const auto &valueItem : v) { QString plainText = PlainTextValue::text(*valueItem); static const QRegularExpression regExpEscapedChars = QRegularExpression(QStringLiteral("\\\\+([&_~])")); plainText.replace(regExpEscapedChars, QStringLiteral("\\1")); urlsInText(plainText, testExistence, baseDirectory, result); } } if (!baseDirectory.isEmpty()) { /// File types supported by "document preview" static const QStringList documentFileExtensions {QStringLiteral(".pdf"), QStringLiteral(".pdf.gz"), QStringLiteral(".pdf.bz2"), QStringLiteral(".ps"), QStringLiteral(".ps.gz"), QStringLiteral(".ps.bz2"), QStringLiteral(".eps"), QStringLiteral(".eps.gz"), QStringLiteral(".eps.bz2"), QStringLiteral(".html"), QStringLiteral(".xhtml"), QStringLiteral(".htm"), QStringLiteral(".dvi"), QStringLiteral(".djvu"), QStringLiteral(".wwf"), QStringLiteral(".jpeg"), QStringLiteral(".jpg"), QStringLiteral(".png"), QStringLiteral(".gif"), QStringLiteral(".tif"), QStringLiteral(".tiff")}; result.reserve(result.size() + documentFileExtensions.size() * 2); /// check if in the same directory as the BibTeX file /// a PDF file exists which filename is based on the entry's id for (const QString &extension : documentFileExtensions) { const QFileInfo fi(baseDirectory + QDir::separator() + entry->id() + extension); if (fi.exists()) { const QUrl url = QUrl::fromLocalFile(fi.canonicalFilePath()); if (!result.contains(url)) result << url; } } /// check if in the same directory as the BibTeX file there is a subdirectory /// similar to the BibTeX file's name and which contains a PDF file exists /// which filename is based on the entry's id static const QRegularExpression filenameExtension(QStringLiteral("\\.[^.]{2,5}$")); const QString basename = bibTeXUrl.fileName().remove(filenameExtension); QString directory = baseDirectory + QDir::separator() + basename; for (const QString &extension : documentFileExtensions) { const QFileInfo fi(directory + QDir::separator() + entry->id() + extension); if (fi.exists()) { const QUrl url = QUrl::fromLocalFile(fi.canonicalFilePath()); if (!result.contains(url)) result << url; } } } return result; } QString FileInfo::pdfToText(const QString &pdfFilename) { /// Build filename for text file where PDF file's plain text is cached const QString cacheDirectory = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QStringLiteral("/pdftotext"); if (!QDir(cacheDirectory).exists() && !QDir::home().mkdir(cacheDirectory)) /// Could not create cache directory return QString(); static const QRegularExpression invalidChars(QStringLiteral("[^-a-z0-9_]"), QRegularExpression::CaseInsensitiveOption); const QString textFilename = QString(pdfFilename).remove(invalidChars).append(QStringLiteral(".txt")).prepend(QStringLiteral("/")).prepend(cacheDirectory); /// First, check if there is a cache text file if (QFileInfo::exists(textFilename)) { /// Load text from cache file QFile f(textFilename); if (f.open(QFile::ReadOnly)) { const QString text = QString::fromUtf8(f.readAll()); f.close(); return text; } } else /// No cache file exists, so run text extraction in another thread QtConcurrent::run(extractPDFTextToCache, pdfFilename, textFilename); return QString(); } void FileInfo::extractPDFTextToCache(const QString &pdfFilename, const QString &cacheFilename) { /// In case of multiple calls, skip text extraction if cache file already exists if (QFile(cacheFilename).exists()) return; QString text; QStringList msgList; /// Load PDF file through Poppler Poppler::Document *doc = Poppler::Document::load(pdfFilename); if (doc != nullptr) { static const int maxPages = 64; /// Build text by appending each page's text for (int i = 0; i < qMin(maxPages, doc->numPages()); ++i) text.append(doc->page(i)->text(QRect())).append(QStringLiteral("\n\n")); if (doc->numPages() > maxPages) msgList << QString(QStringLiteral("### Skipped %1 pages as PDF file contained too many pages (limit is %2 pages) ###")).arg(doc->numPages() - maxPages).arg(maxPages); delete doc; } else msgList << QStringLiteral("### Skipped as file could not be opened as PDF file ###"); /// Save text in cache file QFile f(cacheFilename); if (f.open(QFile::WriteOnly)) { static const int maxCharacters = 1 << 18; f.write(text.left(maxCharacters).toUtf8()); ///< keep only the first 2^18 many characters if (text.length() > maxCharacters) msgList << QString(QStringLiteral("### Text too long, skipping %1 characters ###")).arg(text.length() - maxCharacters); /// Write all messages (warnings) to end of text file for (const QString &msg : const_cast<const QStringList &>(msgList)) { static const char linebreak = '\n'; f.write(&linebreak, 1); f.write(msg.toUtf8()); } f.close(); } } diff --git a/src/networking/associatedfiles.cpp b/src/networking/associatedfiles.cpp index 4c738be6..604ccee8 100644 --- a/src/networking/associatedfiles.cpp +++ b/src/networking/associatedfiles.cpp @@ -1,170 +1,170 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, see <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "associatedfiles.h" #include <QFileInfo> #include <QDir> #include <KIO/CopyJob> #include <KJobWidgets> #include <Preferences> #include "logging_networking.h" QString AssociatedFiles::relativeFilename(const QUrl &documentUrl, const QUrl &baseUrl) { - if (documentUrl.isEmpty()) { - qCWarning(LOG_KBIBTEX_NETWORKING) << "document URL has to point to a file location or URL"; + if (!documentUrl.isValid()) { + qCWarning(LOG_KBIBTEX_NETWORKING) << "document URL has to point to a file location or URL but is invalid"; return QString(); } - if (baseUrl.isEmpty() || baseUrl.isRelative()) { - qCWarning(LOG_KBIBTEX_NETWORKING) << "base URL has to point to an absolute file location or URL"; + if (!baseUrl.isValid() || baseUrl.isRelative()) { + qCWarning(LOG_KBIBTEX_NETWORKING) << "base URL has to point to an absolute file location or URL and must be valid"; return documentUrl.url(QUrl::PreferLocalFile); } if (documentUrl.scheme() != baseUrl.scheme() || (documentUrl.scheme() != QStringLiteral("file") && documentUrl.host() != baseUrl.host())) { qCWarning(LOG_KBIBTEX_NETWORKING) << "document URL and base URL do not match (protocol, host, ...)"; return documentUrl.url(QUrl::PreferLocalFile); } /// First, resolve the provided document URL to an absolute URL /// using the given base URL QUrl internaldocumentUrl = documentUrl; if (internaldocumentUrl.isRelative()) internaldocumentUrl = baseUrl.resolved(internaldocumentUrl); /// Get the absolute path of the base URL const QString baseUrlDirectory = QFileInfo(baseUrl.path()).absolutePath(); /// Let QDir calculate the relative directory return QDir(baseUrlDirectory).relativeFilePath(internaldocumentUrl.path()); } QString AssociatedFiles::absoluteFilename(const QUrl &documentUrl, const QUrl &baseUrl) { - if (documentUrl.isEmpty()) { - qCWarning(LOG_KBIBTEX_NETWORKING) << "document URL has to point to a file location or URL"; + if (!documentUrl.isValid()) { + qCWarning(LOG_KBIBTEX_NETWORKING) << "document URL has to point to a file location or URL but is invalid"; return QString(); } - if (documentUrl.isRelative() && (baseUrl.isEmpty() || baseUrl.isRelative())) { - qCWarning(LOG_KBIBTEX_NETWORKING) << "base URL has to point to an absolute file location or URL if the document URL is relative"; + if (documentUrl.isRelative() && (!baseUrl.isValid() || baseUrl.isRelative())) { + qCWarning(LOG_KBIBTEX_NETWORKING) << "base URL has to point to an absolute, valid file location or URL if the document URL is relative"; return documentUrl.url(QUrl::PreferLocalFile); } if (documentUrl.isRelative() && (documentUrl.scheme() != baseUrl.scheme() || (documentUrl.scheme() != QStringLiteral("file") && documentUrl.host() != baseUrl.host()))) { qCWarning(LOG_KBIBTEX_NETWORKING) << "document URL and base URL do not match (protocol, host, ...), but necessary if the document URL is relative"; return documentUrl.url(QUrl::PreferLocalFile); } /// Resolve the provided document URL to an absolute URL /// using the given base URL QUrl internaldocumentUrl = documentUrl; if (internaldocumentUrl.isRelative()) internaldocumentUrl = baseUrl.resolved(internaldocumentUrl); return internaldocumentUrl.url(QUrl::PreferLocalFile); } QString AssociatedFiles::insertUrl(const QUrl &documentUrl, QSharedPointer<Entry> &entry, const File *bibTeXFile, PathType pathType) { const QString finalUrl = computeAssociateUrl(documentUrl, bibTeXFile, pathType); bool alreadyContained = false; for (QMap<QString, Value>::ConstIterator it = entry->constBegin(); !alreadyContained && it != entry->constEnd(); ++it) { const Value v = it.value(); for (Value::ConstIterator vit = v.constBegin(); !alreadyContained && vit != v.constEnd(); ++vit) { if (PlainTextValue::text(*vit) == finalUrl) alreadyContained = true; } } if (!alreadyContained) { const QString field = documentUrl.isLocalFile() ? (Preferences::instance().bibliographySystem() == Preferences::instance().BibTeX ? Entry::ftLocalFile : Entry::ftFile) : Entry::ftUrl; Value value = entry->value(field); value.append(QSharedPointer<VerbatimText>(new VerbatimText(finalUrl))); entry->insert(field, value); } return finalUrl; } QString AssociatedFiles::computeAssociateUrl(const QUrl &documentUrl, const File *bibTeXFile, PathType pathType) { Q_ASSERT(bibTeXFile != nullptr); // FIXME more graceful? const QUrl baseUrl = bibTeXFile->property(File::Url).toUrl(); - if (baseUrl.isEmpty() && pathType == ptRelative) { + if (!baseUrl.isValid() && pathType == ptRelative) { /// If no base URL was given but still a relative path was requested, /// revert choice and enforce the generation of an absolute one pathType = ptAbsolute; } const QString finalUrl = pathType == ptAbsolute ? absoluteFilename(documentUrl, baseUrl) : relativeFilename(documentUrl, baseUrl); return finalUrl; } QPair<QUrl, QUrl> AssociatedFiles::computeSourceDestinationUrls(const QUrl &sourceUrl, const QString &entryId, const File *bibTeXFile, RenameOperation renameOperation, const QString &userDefinedFilename) { Q_ASSERT(bibTeXFile != nullptr); // FIXME more graceful? if (entryId.isEmpty() && renameOperation == roEntryId) { /// If no entry id was given but still a rename after entry id was requested, /// revert choice and enforce keeping the original name renameOperation = roKeepName; } const QUrl baseUrl = bibTeXFile->property(File::Url).toUrl(); const QUrl internalSourceUrl = baseUrl.resolved(sourceUrl); const QFileInfo internalSourceInfo(internalSourceUrl.path()); QString filename = internalSourceInfo.fileName(); QString suffix = internalSourceInfo.suffix(); if (suffix.isEmpty()) { suffix = QStringLiteral("html"); filename.append(QLatin1Char('.')).append(suffix); } if (filename.isEmpty()) filename = internalSourceUrl.url(QUrl::PreferLocalFile).remove(QDir::separator()).remove(QLatin1Char('/')).remove(QLatin1Char(':')).remove(QLatin1Char('.')) + QStringLiteral(".") + suffix; if (!bibTeXFile->hasProperty(File::Url)) return QPair<QUrl, QUrl>(); /// no valid URL set of BibTeX file object QUrl targetUrl = bibTeXFile->property(File::Url).toUrl(); - if (targetUrl.isEmpty()) return QPair<QUrl, QUrl>(); /// no valid URL set of BibTeX file object + if (!targetUrl.isValid()) return QPair<QUrl, QUrl>(); /// no valid URL set of BibTeX file object const QString targetPath = QFileInfo(targetUrl.path()).absolutePath(); targetUrl.setPath(targetPath + QDir::separator() + (renameOperation == roEntryId ? entryId + QStringLiteral(".") + suffix : (renameOperation == roUserDefined ? userDefinedFilename : filename))); return QPair<QUrl, QUrl>(internalSourceUrl, targetUrl); } QUrl AssociatedFiles::copyDocument(const QUrl &sourceUrl, const QString &entryId, const File *bibTeXFile, RenameOperation renameOperation, MoveCopyOperation moveCopyOperation, QWidget *widget, const QString &userDefinedFilename) { const QPair<QUrl, QUrl> r = computeSourceDestinationUrls(sourceUrl, entryId, bibTeXFile, renameOperation, userDefinedFilename); const QUrl internalSourceUrl = r.first, targetUrl = r.second; bool success = true; if (internalSourceUrl.isLocalFile() && targetUrl.isLocalFile()) { QFile(targetUrl.path()).remove(); success &= QFile::copy(internalSourceUrl.path(), targetUrl.path()); if (success && moveCopyOperation == mcoMove) { success &= QFile(internalSourceUrl.path()).remove(); } } else if (internalSourceUrl.isValid() && targetUrl.isValid()) { // FIXME non-blocking KIO::CopyJob *moveCopyJob = moveCopyOperation == mcoMove ? KIO::move(sourceUrl, targetUrl, KIO::HideProgressInfo | KIO::Overwrite) : KIO::copy(sourceUrl, targetUrl, KIO::HideProgressInfo | KIO::Overwrite); KJobWidgets::setWindow(moveCopyJob, widget); success &= moveCopyJob->exec(); } else { qWarning() << "Either sourceUrl or targetUrl is invalid"; return QUrl(); } if (!success) return QUrl(); ///< either copy/move or delete operation failed return targetUrl; } diff --git a/src/networking/findpdf.cpp b/src/networking/findpdf.cpp index 7bbeeb42..c1f7f52a 100644 --- a/src/networking/findpdf.cpp +++ b/src/networking/findpdf.cpp @@ -1,455 +1,455 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, see <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "findpdf.h" #include <QNetworkReply> #include <QNetworkRequest> #include <QRegularExpression> #include <QApplication> #include <QTemporaryFile> #include <QUrlQuery> #include <QStandardPaths> #include <QDir> #include <poppler-qt5.h> #include <KBibTeX> #include <Value> #include <FileInfo> #include "internalnetworkaccessmanager.h" #include "logging_networking.h" int maxDepth = 5; static const char *depthProperty = "depth"; static const char *termProperty = "term"; static const char *originProperty = "origin"; class FindPDF::Private { private: FindPDF *p; public: int aliveCounter; QList<ResultItem> result; Entry currentEntry; QSet<QUrl> knownUrls; QSet<QNetworkReply *> runningDownloads; Private(FindPDF *parent) : p(parent), aliveCounter(0) { /// nothing } bool queueUrl(const QUrl &url, const QString &term, const QString &origin, int depth) { if (!knownUrls.contains(url) && depth > 0) { knownUrls.insert(url); QNetworkRequest request = QNetworkRequest(url); QNetworkReply *reply = InternalNetworkAccessManager::instance().get(request); InternalNetworkAccessManager::instance().setNetworkReplyTimeout(reply, 15); ///< set a timeout on network connections reply->setProperty(depthProperty, QVariant::fromValue<int>(depth)); reply->setProperty(termProperty, term); reply->setProperty(originProperty, origin); runningDownloads.insert(reply); connect(reply, &QNetworkReply::finished, p, &FindPDF::downloadFinished); ++aliveCounter; return true; } else return false; } void processGeneralHTML(QNetworkReply *reply, const QString &text) { /// fetch some properties from Reply object const QString term = reply->property(termProperty).toString(); const QString origin = reply->property(originProperty).toString(); bool ok = false; int depth = reply->property(depthProperty).toInt(&ok); if (!ok) depth = 0; /// regular expressions to guess links to follow const QVector<QRegularExpression> specificAnchorRegExp = { QRegularExpression(QString(QStringLiteral("<a[^>]*href=\"([^\"]*%1[^\"]*[.]pdf)\"")).arg(QRegularExpression::escape(term)), QRegularExpression::CaseInsensitiveOption), QRegularExpression(QString(QStringLiteral("<a[^>]*href=\"([^\"]+)\"[^>]*>[^<]*%1[^<]*[.]pdf")).arg(QRegularExpression::escape(term)), QRegularExpression::CaseInsensitiveOption), QRegularExpression(QString(QStringLiteral("<a[^>]*href=\"([^\"]*%1[^\"]*)\"")).arg(QRegularExpression::escape(term)), QRegularExpression::CaseInsensitiveOption), QRegularExpression(QString(QStringLiteral("<a[^>]*href=\"([^\"]+)\"[^>]*>[^<]*%1[^<]*\\b")).arg(QRegularExpression::escape(term)), QRegularExpression::CaseInsensitiveOption) }; static const QRegularExpression genericAnchorRegExp = QRegularExpression(QStringLiteral("<a[^>]*href=\"([^\"]+)\""), QRegularExpression::CaseInsensitiveOption); bool gotLink = false; for (const QRegularExpression &anchorRegExp : specificAnchorRegExp) { const QRegularExpressionMatch match = anchorRegExp.match(text); if (match.hasMatch()) { const QUrl url = QUrl::fromEncoded(match.captured(1).toLatin1()); queueUrl(reply->url().resolved(url), term, origin, depth - 1); gotLink = true; break; } } if (!gotLink) { /// this is only the last resort: /// to follow the first link found in the HTML document const QRegularExpressionMatch match = genericAnchorRegExp.match(text); if (match.hasMatch()) { const QUrl url = QUrl::fromEncoded(match.captured(1).toLatin1()); queueUrl(reply->url().resolved(url), term, origin, depth - 1); } } } void processGoogleResult(QNetworkReply *reply, const QString &text) { static const QString h3Tag(QStringLiteral("<h3")); static const QString aTag(QStringLiteral("<a")); static const QString hrefAttrib(QStringLiteral("href=\"")); const QString term = reply->property(termProperty).toString(); bool ok = false; int depth = reply->property(depthProperty).toInt(&ok); if (!ok) depth = 0; /// extract the first numHitsToFollow-many hits found by Google Scholar const int numHitsToFollow = 10; int p = -1; for (int i = 0; i < numHitsToFollow; ++i) { if ((p = text.indexOf(h3Tag, p + 1)) >= 0 && (p = text.indexOf(aTag, p + 1)) >= 0 && (p = text.indexOf(hrefAttrib, p + 1)) >= 0) { int p1 = p + 6; int p2 = text.indexOf(QLatin1Char('"'), p1 + 1); QUrl url(text.mid(p1, p2 - p1)); const QString googleService = reply->url().host().contains(QStringLiteral("scholar.google")) ? QStringLiteral("scholar.google") : QStringLiteral("www.google"); queueUrl(reply->url().resolved(url), term, googleService, depth - 1); } } } void processSpringerLink(QNetworkReply *reply, const QString &text) { static const QRegularExpression fulltextPDFlink(QStringLiteral("href=\"([^\"]+/fulltext.pdf)\"")); const QRegularExpressionMatch match = fulltextPDFlink.match(text); if (match.hasMatch()) { bool ok = false; int depth = reply->property(depthProperty).toInt(&ok); if (!ok) depth = 0; const QUrl url(match.captured(1)); queueUrl(reply->url().resolved(url), QString(), QStringLiteral("springerlink"), depth - 1); } } void processCiteSeerX(QNetworkReply *reply, const QString &text) { static const QRegularExpression downloadPDFlink(QStringLiteral("href=\"(/viewdoc/download[^\"]+type=pdf)\"")); const QRegularExpressionMatch match = downloadPDFlink.match(text); if (match.hasMatch()) { bool ok = false; int depth = reply->property(depthProperty).toInt(&ok); if (!ok) depth = 0; const QUrl url = QUrl::fromEncoded(match.captured(1).toLatin1()); queueUrl(reply->url().resolved(url), QString(), QStringLiteral("citeseerx"), depth - 1); } } void processACMDigitalLibrary(QNetworkReply *reply, const QString &text) { static const QRegularExpression downloadPDFlink(QStringLiteral("href=\"(ft_gateway.cfm\\?id=\\d+&ftid=\\d+&dwn=1&CFID=\\d+&CFTOKEN=\\d+)\"")); const QRegularExpressionMatch match = downloadPDFlink.match(text); if (match.hasMatch()) { bool ok = false; int depth = reply->property(depthProperty).toInt(&ok); if (!ok) depth = 0; const QUrl url = QUrl::fromEncoded(match.captured(1).toLatin1()); queueUrl(reply->url().resolved(url), QString(), QStringLiteral("acmdl"), depth - 1); } } bool processPDF(QNetworkReply *reply, const QByteArray &data) { bool progress = false; const QString origin = reply->property(originProperty).toString(); const QUrl url = reply->url(); /// Search for duplicate URLs bool containsUrl = false; for (const ResultItem &ri : const_cast<const QList<ResultItem> &>(result)) { containsUrl |= ri.url == url; /// Skip already visited URLs if (containsUrl) break; } if (!containsUrl) { Poppler::Document *doc = Poppler::Document::loadFromData(data); ResultItem resultItem; resultItem.tempFilename = new QTemporaryFile(QStandardPaths::writableLocation(QStandardPaths::TempLocation) + QDir::separator() + QStringLiteral("kbibtex_findpdf_XXXXXX.pdf")); resultItem.tempFilename->setAutoRemove(true); if (resultItem.tempFilename->open()) { const int lenDataWritten = resultItem.tempFilename->write(data); resultItem.tempFilename->close(); if (lenDataWritten != data.length()) { /// Failed to write to temporary file qCWarning(LOG_KBIBTEX_NETWORKING) << "Failed to write to temporary file for filename" << resultItem.tempFilename->fileName(); delete resultItem.tempFilename; resultItem.tempFilename = nullptr; } } else { /// Failed to create temporary file qCWarning(LOG_KBIBTEX_NETWORKING) << "Failed to create temporary file for templaet" << resultItem.tempFilename->fileTemplate(); delete resultItem.tempFilename; resultItem.tempFilename = nullptr; } resultItem.url = url; resultItem.textPreview = doc->info(QStringLiteral("Title")).simplified(); static const int maxTextLen = 1024; for (int i = 0; i < doc->numPages() && resultItem.textPreview.length() < maxTextLen; ++i) { Poppler::Page *page = doc->page(i); if (!resultItem.textPreview.isEmpty()) resultItem.textPreview += QLatin1Char(' '); resultItem.textPreview += page->text(QRect()).simplified().leftRef(maxTextLen); delete page; } resultItem.textPreview.remove(QStringLiteral("Microsoft Word - ")); ///< Some word processors need to put their name everywhere ... resultItem.downloadMode = NoDownload; resultItem.relevance = origin == Entry::ftDOI ? 1.0 : (origin == QStringLiteral("eprint") ? 0.75 : 0.5); result << resultItem; progress = true; delete doc; } return progress; } QUrl ieeeDocumentUrlToDownloadUrl(const QUrl &url) { /// Basic checking if provided URL is from IEEE Xplore if (!url.host().contains(QStringLiteral("ieeexplore.ieee.org"))) return url; /// Assuming URL looks like this: /// http://ieeexplore.ieee.org/document/8092651/ static const QRegularExpression documentIdRegExp(QStringLiteral("/(\\d{6,})/$")); const QRegularExpressionMatch documentIdRegExpMatch = documentIdRegExp.match(url.path()); if (!documentIdRegExpMatch.hasMatch()) return url; /// Use document id extracted above to build URL to PDF file return QUrl(QStringLiteral("http://ieeexplore.ieee.org/stamp/stamp.jsp?arnumber=") + documentIdRegExpMatch.captured(1)); } }; FindPDF::FindPDF(QObject *parent) : QObject(parent), d(new Private(this)) { /// nothing } FindPDF::~FindPDF() { abort(); delete d; } bool FindPDF::search(const Entry &entry) { if (d->aliveCounter > 0) return false; d->knownUrls.clear(); d->result.clear(); d->currentEntry = entry; emit progress(0, d->aliveCounter, 0); /// Generate a string which contains the title's beginning QString searchWords; if (entry.contains(Entry::ftTitle)) { const QStringList titleChunks = PlainTextValue::text(entry.value(Entry::ftTitle)).split(QStringLiteral(" "), QString::SkipEmptyParts); if (!titleChunks.isEmpty()) { searchWords = titleChunks[0]; for (int i = 1; i < titleChunks.count() && searchWords.length() < 64; ++i) searchWords += QLatin1Char(' ') + titleChunks[i]; } } const QStringList authors = entry.authorsLastName(); for (int i = 0; i < authors.count() && searchWords.length() < 96; ++i) searchWords += QLatin1Char(' ') + authors[i]; searchWords.remove(QLatin1Char('{')).remove(QLatin1Char('}')); QStringList urlFields {Entry::ftDOI, Entry::ftUrl, QStringLiteral("ee")}; for (int i = 2; i < 256; ++i) urlFields << QString(QStringLiteral("%1%2")).arg(Entry::ftDOI).arg(i) << QString(QStringLiteral("%1%2")).arg(Entry::ftUrl).arg(i); for (const QString &field : const_cast<const QStringList &>(urlFields)) { if (entry.contains(field)) { const QString fieldText = PlainTextValue::text(entry.value(field)); QRegularExpressionMatchIterator doiRegExpMatchIt = KBibTeX::doiRegExp.globalMatch(fieldText); while (doiRegExpMatchIt.hasNext()) { const QRegularExpressionMatch doiRegExpMatch = doiRegExpMatchIt.next(); d->queueUrl(QUrl(KBibTeX::doiUrlPrefix + doiRegExpMatch.captured(0)), fieldText, Entry::ftDOI, maxDepth); } QRegularExpressionMatchIterator urlRegExpMatchIt = KBibTeX::urlRegExp.globalMatch(fieldText); while (urlRegExpMatchIt.hasNext()) { QRegularExpressionMatch urlRegExpMatch = urlRegExpMatchIt.next(); d->queueUrl(QUrl(urlRegExpMatch.captured(0)), searchWords, Entry::ftUrl, maxDepth); } } } if (entry.contains(QStringLiteral("eprint"))) { /// check eprint fields as used for arXiv const QString eprintId = PlainTextValue::text(entry.value(QStringLiteral("eprint"))); if (!eprintId.isEmpty()) { const QUrl arxivUrl = QUrl::fromUserInput(QStringLiteral("http://arxiv.org/find/all/1/all:+") + eprintId + QStringLiteral("/0/1/0/all/0/1")); d->queueUrl(arxivUrl, eprintId, QStringLiteral("eprint"), maxDepth); } } if (!searchWords.isEmpty()) { /// Search in Google const QUrl googleUrl = QUrl::fromUserInput(QStringLiteral("https://www.google.com/search?hl=en&sa=G&q=filetype:pdf ") + searchWords); d->queueUrl(googleUrl, searchWords, QStringLiteral("www.google"), maxDepth); /// Search in Google Scholar const QUrl googleScholarUrl = QUrl::fromUserInput(QStringLiteral("https://scholar.google.com/scholar?hl=en&btnG=Search&as_sdt=1&q=filetype:pdf ") + searchWords); d->queueUrl(googleScholarUrl, searchWords, QStringLiteral("scholar.google"), maxDepth); /// Search in Bing const QUrl bingUrl = QUrl::fromUserInput(QStringLiteral("https://www.bing.com/search?setlang=en-US&q=filetype:pdf ") + searchWords); d->queueUrl(bingUrl, searchWords, QStringLiteral("bing"), maxDepth); /// Search in CiteSeerX const QUrl citeseerXurl = QUrl::fromUserInput(QStringLiteral("http://citeseerx.ist.psu.edu/search?submit=Search&sort=rlv&t=doc&q=") + searchWords); d->queueUrl(citeseerXurl, searchWords, QStringLiteral("citeseerx"), maxDepth); /// Search in StartPage const QUrl startPageUrl = QUrl::fromUserInput(QStringLiteral("https://www.startpage.com/do/asearch?cat=web&cmd=process_search&language=english&engine0=v1all&abp=-1&t=white&nj=1&prf=23ad6aab054a88d3da5c443280cee596&suggestOn=0&query=filetype:pdf ") + searchWords); d->queueUrl(startPageUrl, searchWords, QStringLiteral("startpage"), maxDepth); } if (d->aliveCounter == 0) { qCWarning(LOG_KBIBTEX_NETWORKING) << "Directly at start, no URLs are queue for a search -> this should never happen"; emit finished(); } return true; } QList<FindPDF::ResultItem> FindPDF::results() { if (d->aliveCounter == 0) return d->result; else { /// Return empty list while search is running return QList<FindPDF::ResultItem>(); } } void FindPDF::abort() { QSet<QNetworkReply *>::Iterator it = d->runningDownloads.begin(); while (it != d->runningDownloads.end()) { QNetworkReply *reply = *it; it = d->runningDownloads.erase(it); reply->abort(); } } void FindPDF::downloadFinished() { static const char *htmlHead1 = "<html", *htmlHead2 = "<HTML", *htmlHead3 = "<!doctype html>" /** ACM Digital Library */; static const char *pdfHead = "%PDF-"; --d->aliveCounter; emit progress(d->knownUrls.count(), d->aliveCounter, d->result.count()); QNetworkReply *reply = static_cast<QNetworkReply *>(sender()); d->runningDownloads.remove(reply); const QString term = reply->property(termProperty).toString(); const QString origin = reply->property(originProperty).toString(); bool depthOk = false; int depth = reply->property(depthProperty).toInt(&depthOk); if (!depthOk) depth = 0; if (reply->error() == QNetworkReply::NoError) { const QByteArray data = reply->readAll(); QUrl redirUrl = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl(); - redirUrl = redirUrl.isEmpty() ? QUrl() : reply->url().resolved(redirUrl); + redirUrl = redirUrl.isValid() ? reply->url().resolved(redirUrl) : QUrl(); qCDebug(LOG_KBIBTEX_NETWORKING) << "finished Downloading " << reply->url().toDisplayString() << " depth=" << depth << " d->aliveCounter=" << d->aliveCounter << " data.size=" << data.size() << " redirUrl=" << redirUrl.toDisplayString() << " origin=" << origin; - if (!redirUrl.isEmpty()) { + if (redirUrl.isValid()) { redirUrl = d->ieeeDocumentUrlToDownloadUrl(redirUrl); d->queueUrl(redirUrl, term, origin, depth - 1); } else if (data.contains(htmlHead1) || data.contains(htmlHead2) || data.contains(htmlHead3)) { /// returned data is a HTML file, i.e. contains "<html" /// check for limited depth before continuing if (depthOk && depth > 0) { /// Get webpage as plain text /// Assume UTF-8 data const QString text = QString::fromUtf8(data.constData()); /// regular expression to check if this is a Google Scholar result page static const QRegularExpression googleScholarTitleRegExp(QStringLiteral("<title>[^>]* - Google Scholar</title>")); /// regular expression to check if this is a SpringerLink page static const QRegularExpression springerLinkTitleRegExp(QStringLiteral("<title>[^>]* - Springer - [^>]*</title>")); /// regular expression to check if this is a CiteSeerX page static const QRegularExpression citeseerxTitleRegExp(QStringLiteral("<title>CiteSeerX &mdash; [^>]*</title>")); /// regular expression to check if this is a ACM Digital Library page static const QString acmDigitalLibraryString(QStringLiteral("The ACM Digital Library is published by the Association for Computing Machinery")); if (googleScholarTitleRegExp.match(text).hasMatch()) d->processGoogleResult(reply, text); else if (springerLinkTitleRegExp.match(text).hasMatch()) d->processSpringerLink(reply, text); else if (citeseerxTitleRegExp.match(text).hasMatch()) d->processCiteSeerX(reply, text); else if (text.contains(acmDigitalLibraryString)) d->processACMDigitalLibrary(reply, text); else { /// regular expression to extract title static const QRegularExpression titleRegExp(QStringLiteral("<title>(.*?)</title>")); const QRegularExpressionMatch match = titleRegExp.match(text); if (match.hasMatch()) qCDebug(LOG_KBIBTEX_NETWORKING) << "Using general HTML processor for page" << match.captured(1) << " URL=" << reply->url().toDisplayString(); else qCDebug(LOG_KBIBTEX_NETWORKING) << "Using general HTML processor for URL=" << reply->url().toDisplayString(); d->processGeneralHTML(reply, text); } } } else if (data.contains(pdfHead)) { /// looks like a PDF file -> grab it const bool gotPDFfile = d->processPDF(reply, data); if (gotPDFfile) emit progress(d->knownUrls.count(), d->aliveCounter, d->result.count()); } else { /// Assume UTF-8 data const QString text = QString::fromUtf8(data.constData()); qCWarning(LOG_KBIBTEX_NETWORKING) << "don't know how to handle " << text.left(256); } } else qCWarning(LOG_KBIBTEX_NETWORKING) << "error from reply: " << reply->errorString() << "(" << reply->url().toDisplayString() << ")" << " term=" << term << " origin=" << origin << " depth=" << depth; if (d->aliveCounter == 0) { /// no more running downloads left emit finished(); } } diff --git a/src/parts/part.cpp b/src/parts/part.cpp index 5258fea9..13b69e44 100644 --- a/src/parts/part.cpp +++ b/src/parts/part.cpp @@ -1,1045 +1,1045 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, see <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "part.h" #include <QLabel> #include <QAction> #include <QFile> #include <QFileInfo> #include <QMenu> #include <QApplication> #include <QLayout> #include <QKeyEvent> #include <QMimeType> #include <QPointer> #include <QFileSystemWatcher> #include <QFileDialog> #include <QDialog> #include <QDialogButtonBox> #include <QPushButton> #include <QTemporaryFile> #include <QTimer> #include <QStandardPaths> #include <KMessageBox> // FIXME deprecated #include <KLocalizedString> #include <KActionCollection> #include <KStandardAction> #include <KActionMenu> #include <KSelectAction> #include <KToggleAction> #include <KRun> #include <KPluginFactory> #include <KIO/StatJob> #include <KIO/CopyJob> #include <KIO/Job> #include <KJobWidgets> #include <kio_version.h> #include <Preferences> #include <File> #include <Macro> #include <Preamble> #include <Comment> #include <FileInfo> #include <FileExporterBibTeXOutput> #include <FileImporterBibTeX> #include <FileExporterBibTeX> #include <FileImporterRIS> #include <FileImporterBibUtils> #include <FileExporterRIS> #include <FileExporterBibUtils> #include <FileImporterPDF> #include <FileExporterPS> #include <FileExporterPDF> #include <FileExporterRTF> #include <FileExporterBibTeX2HTML> #include <FileExporterXML> #include <FileExporterXSLT> #include <models/FileModel> #include <IdSuggestions> #include <LyX> #include <widgets/FileSettingsWidget> #include <widgets/FilterBar> #include <element/FindPDFUI> #include <file/FileView> #include <file/FindDuplicatesUI> #include <file/Clipboard> #include <preferences/SettingsColorLabelWidget> #include <preferences/SettingsFileExporterPDFPSWidget> #include <ValueListModel> #include "logging_parts.h" static const char RCFileName[] = "kbibtexpartui.rc"; class KBibTeXPart::KBibTeXPartPrivate { private: KBibTeXPart *p; /** * Modifies a given URL to become a "backup" filename/URL. * A backup level or 0 or less does not modify the URL. * A backup level of 1 appends a '~' (tilde) to the URL's filename. * A backup level of 2 or more appends '~N', where N is the level. * The provided URL will be modified in the process. It is assumed * that the URL is not yet a "backup URL". */ void constructBackupUrl(const int level, QUrl &url) const { if (level <= 0) /// No modification return; else if (level == 1) /// Simply append '~' to the URL's filename url.setPath(url.path() + QStringLiteral("~")); else /// Append '~' followed by a number to the filename url.setPath(url.path() + QString(QStringLiteral("~%1")).arg(level)); } public: File *bibTeXFile; PartWidget *partWidget; FileModel *model; SortFilterFileModel *sortFilterProxyModel; QAction *editCutAction, *editDeleteAction, *editCopyAction, *editPasteAction, *editCopyReferencesAction, *elementEditAction, *elementViewDocumentAction, *fileSaveAction, *elementFindPDFAction, *entryApplyDefaultFormatString; QMenu *viewDocumentMenu; bool isSaveAsOperation; LyX *lyx; FindDuplicatesUI *findDuplicatesUI; ColorLabelContextMenu *colorLabelContextMenu; QAction *colorLabelContextMenuAction; QFileSystemWatcher fileSystemWatcher; KBibTeXPartPrivate(QWidget *parentWidget, KBibTeXPart *parent) : p(parent), bibTeXFile(nullptr), model(nullptr), sortFilterProxyModel(nullptr), viewDocumentMenu(new QMenu(i18n("View Document"), parent->widget())), isSaveAsOperation(false), fileSystemWatcher(p) { connect(&fileSystemWatcher, &QFileSystemWatcher::fileChanged, p, &KBibTeXPart::fileExternallyChange); partWidget = new PartWidget(parentWidget); partWidget->fileView()->setReadOnly(!p->isReadWrite()); connect(partWidget->fileView(), &FileView::modified, p, &KBibTeXPart::setModified); setupActions(); } ~KBibTeXPartPrivate() { delete bibTeXFile; delete model; delete viewDocumentMenu; delete findDuplicatesUI; } void setupActions() { /// "Save" action fileSaveAction = p->actionCollection()->addAction(KStandardAction::Save); connect(fileSaveAction, &QAction::triggered, p, &KBibTeXPart::documentSave); fileSaveAction->setEnabled(false); QAction *action = p->actionCollection()->addAction(KStandardAction::SaveAs); connect(action, &QAction::triggered, p, &KBibTeXPart::documentSaveAs); /// "Save copy as" action QAction *saveCopyAsAction = new QAction(QIcon::fromTheme(QStringLiteral("document-save")), i18n("Save Copy As..."), p); p->actionCollection()->addAction(QStringLiteral("file_save_copy_as"), saveCopyAsAction); connect(saveCopyAsAction, &QAction::triggered, p, &KBibTeXPart::documentSaveCopyAs); /// Filter bar widget QAction *filterWidgetAction = new QAction(i18n("Filter"), p); p->actionCollection()->addAction(QStringLiteral("toolbar_filter_widget"), filterWidgetAction); filterWidgetAction->setIcon(QIcon::fromTheme(QStringLiteral("view-filter"))); p->actionCollection()->setDefaultShortcut(filterWidgetAction, Qt::CTRL + Qt::Key_F); connect(filterWidgetAction, &QAction::triggered, partWidget->filterBar(), static_cast<void(QWidget::*)()>(&QWidget::setFocus)); partWidget->filterBar()->setPlaceholderText(i18n("Filter bibliographic entries (%1)", filterWidgetAction->shortcut().toString())); /// Actions for creating new elements (entries, macros, ...) KActionMenu *newElementAction = new KActionMenu(QIcon::fromTheme(QStringLiteral("address-book-new")), i18n("New element"), p); p->actionCollection()->addAction(QStringLiteral("element_new"), newElementAction); QMenu *newElementMenu = new QMenu(newElementAction->text(), p->widget()); newElementAction->setMenu(newElementMenu); connect(newElementAction, &QAction::triggered, p, &KBibTeXPart::newEntryTriggered); QAction *newEntry = new QAction(QIcon::fromTheme(QStringLiteral("address-book-new")), i18n("New entry"), newElementAction); newElementMenu->addAction(newEntry); p->actionCollection()->setDefaultShortcut(newEntry, Qt::CTRL + Qt::SHIFT + Qt::Key_N); connect(newEntry, &QAction::triggered, p, &KBibTeXPart::newEntryTriggered); QAction *newComment = new QAction(QIcon::fromTheme(QStringLiteral("address-book-new")), i18n("New comment"), newElementAction); newElementMenu->addAction(newComment); connect(newComment, &QAction::triggered, p, &KBibTeXPart::newCommentTriggered); QAction *newMacro = new QAction(QIcon::fromTheme(QStringLiteral("address-book-new")), i18n("New macro"), newElementAction); newElementMenu->addAction(newMacro); connect(newMacro, &QAction::triggered, p, &KBibTeXPart::newMacroTriggered); QAction *newPreamble = new QAction(QIcon::fromTheme(QStringLiteral("address-book-new")), i18n("New preamble"), newElementAction); newElementMenu->addAction(newPreamble); connect(newPreamble, &QAction::triggered, p, &KBibTeXPart::newPreambleTriggered); /// Action to edit an element elementEditAction = new QAction(QIcon::fromTheme(QStringLiteral("document-edit")), i18n("Edit Element"), p); p->actionCollection()->addAction(QStringLiteral("element_edit"), elementEditAction); p->actionCollection()->setDefaultShortcut(elementEditAction, Qt::CTRL + Qt::Key_E); connect(elementEditAction, &QAction::triggered, partWidget->fileView(), &FileView::editCurrentElement); /// Action to view the document associated to the current element elementViewDocumentAction = new QAction(QIcon::fromTheme(QStringLiteral("application-pdf")), i18n("View Document"), p); p->actionCollection()->addAction(QStringLiteral("element_viewdocument"), elementViewDocumentAction); p->actionCollection()->setDefaultShortcut(elementViewDocumentAction, Qt::CTRL + Qt::Key_D); connect(elementViewDocumentAction, &QAction::triggered, p, &KBibTeXPart::elementViewDocument); /// Action to find a PDF matching the current element elementFindPDFAction = new QAction(QIcon::fromTheme(QStringLiteral("application-pdf")), i18n("Find PDF..."), p); p->actionCollection()->addAction(QStringLiteral("element_findpdf"), elementFindPDFAction); connect(elementFindPDFAction, &QAction::triggered, p, &KBibTeXPart::elementFindPDF); /// Action to reformat the selected elements' ids entryApplyDefaultFormatString = new QAction(QIcon::fromTheme(QStringLiteral("favorites")), i18n("Format entry ids"), p); p->actionCollection()->addAction(QStringLiteral("entry_applydefaultformatstring"), entryApplyDefaultFormatString); connect(entryApplyDefaultFormatString, &QAction::triggered, p, &KBibTeXPart::applyDefaultFormatString); /// Clipboard object, required for various copy&paste operations Clipboard *clipboard = new Clipboard(partWidget->fileView()); /// Actions to cut and copy selected elements as BibTeX code editCutAction = p->actionCollection()->addAction(KStandardAction::Cut, clipboard, SLOT(cut())); editCopyAction = p->actionCollection()->addAction(KStandardAction::Copy, clipboard, SLOT(copy())); /// Action to copy references, e.g. '\cite{fordfulkerson1959}' editCopyReferencesAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Copy References"), p); p->actionCollection()->setDefaultShortcut(editCopyReferencesAction, Qt::CTRL + Qt::SHIFT + Qt::Key_C); p->actionCollection()->addAction(QStringLiteral("edit_copy_references"), editCopyReferencesAction); connect(editCopyReferencesAction, &QAction::triggered, clipboard, &Clipboard::copyReferences); /// Action to paste BibTeX code editPasteAction = p->actionCollection()->addAction(KStandardAction::Paste, clipboard, SLOT(paste())); /// Action to delete selected rows/elements editDeleteAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-table-delete-row")), i18n("Delete"), p); p->actionCollection()->setDefaultShortcut(editDeleteAction, Qt::Key_Delete); p->actionCollection()->addAction(QStringLiteral("edit_delete"), editDeleteAction); connect(editDeleteAction, &QAction::triggered, partWidget->fileView(), &FileView::selectionDelete); /// Build context menu for central BibTeX file view partWidget->fileView()->setContextMenuPolicy(Qt::ActionsContextMenu); ///< context menu is based on actions partWidget->fileView()->addAction(elementEditAction); partWidget->fileView()->addAction(elementViewDocumentAction); QAction *separator = new QAction(p); separator->setSeparator(true); partWidget->fileView()->addAction(separator); partWidget->fileView()->addAction(editCutAction); partWidget->fileView()->addAction(editCopyAction); partWidget->fileView()->addAction(editCopyReferencesAction); partWidget->fileView()->addAction(editPasteAction); partWidget->fileView()->addAction(editDeleteAction); separator = new QAction(p); separator->setSeparator(true); partWidget->fileView()->addAction(separator); partWidget->fileView()->addAction(elementFindPDFAction); partWidget->fileView()->addAction(entryApplyDefaultFormatString); colorLabelContextMenu = new ColorLabelContextMenu(partWidget->fileView()); colorLabelContextMenuAction = p->actionCollection()->addAction(QStringLiteral("entry_colorlabel"), colorLabelContextMenu->menuAction()); findDuplicatesUI = new FindDuplicatesUI(p, partWidget->fileView()); lyx = new LyX(p, partWidget->fileView()); connect(partWidget->fileView(), &FileView::selectedElementsChanged, p, &KBibTeXPart::updateActions); connect(partWidget->fileView(), &FileView::currentElementChanged, p, &KBibTeXPart::updateActions); } FileImporter *fileImporterFactory(const QUrl &url) { QString ending = url.path().toLower(); const auto pos = ending.lastIndexOf(QStringLiteral(".")); ending = ending.mid(pos + 1); if (ending == QStringLiteral("pdf")) { return new FileImporterPDF(p); } else if (ending == QStringLiteral("ris")) { return new FileImporterRIS(p); } else if (BibUtils::available() && ending == QStringLiteral("isi")) { FileImporterBibUtils *fileImporterBibUtils = new FileImporterBibUtils(p); fileImporterBibUtils->setFormat(BibUtils::ISI); return fileImporterBibUtils; } else { FileImporterBibTeX *fileImporterBibTeX = new FileImporterBibTeX(p); fileImporterBibTeX->setCommentHandling(FileImporterBibTeX::KeepComments); return fileImporterBibTeX; } } FileExporter *fileExporterFactory(const QString &ending) { if (ending == QStringLiteral("html")) { return new FileExporterHTML(p); } else if (ending == QStringLiteral("xml")) { return new FileExporterXML(p); } else if (ending == QStringLiteral("ris")) { return new FileExporterRIS(p); } else if (ending == QStringLiteral("pdf")) { return new FileExporterPDF(p); } else if (ending == QStringLiteral("ps")) { return new FileExporterPS(p); } else if (BibUtils::available() && ending == QStringLiteral("isi")) { FileExporterBibUtils *fileExporterBibUtils = new FileExporterBibUtils(p); fileExporterBibUtils->setFormat(BibUtils::ISI); return fileExporterBibUtils; } else if (ending == QStringLiteral("rtf")) { return new FileExporterRTF(p); } else if (ending == QStringLiteral("html") || ending == QStringLiteral("htm")) { return new FileExporterBibTeX2HTML(p); } else if (ending == QStringLiteral("bbl")) { return new FileExporterBibTeXOutput(FileExporterBibTeXOutput::BibTeXBlockList, p); } else { return new FileExporterBibTeX(p); } } QString findUnusedId() { int i = 1; while (true) { QString result = i18n("New%1", i); if (!bibTeXFile->containsKey(result)) return result; ++i; } } void initializeNew() { bibTeXFile = new File(); model = new FileModel(); model->setBibliographyFile(bibTeXFile); if (sortFilterProxyModel != nullptr) delete sortFilterProxyModel; sortFilterProxyModel = new SortFilterFileModel(p); sortFilterProxyModel->setSourceModel(model); partWidget->fileView()->setModel(sortFilterProxyModel); connect(partWidget->filterBar(), &FilterBar::filterChanged, sortFilterProxyModel, &SortFilterFileModel::updateFilter); } bool openFile(const QUrl &url, const QString &localFilePath) { p->setObjectName("KBibTeXPart::KBibTeXPart for " + url.toDisplayString() + " aka " + localFilePath); qApp->setOverrideCursor(Qt::WaitCursor); if (bibTeXFile != nullptr) { const QUrl oldUrl = bibTeXFile->property(File::Url, QUrl()).toUrl(); if (oldUrl.isValid() && oldUrl.isLocalFile()) { const QString path = oldUrl.toLocalFile(); if (!path.isEmpty()) fileSystemWatcher.removePath(path); else qCWarning(LOG_KBIBTEX_PARTS) << "No filename to stop watching"; } delete bibTeXFile; bibTeXFile = nullptr; } QFile inputfile(localFilePath); if (!inputfile.open(QIODevice::ReadOnly)) { qCWarning(LOG_KBIBTEX_PARTS) << "Opening file failed, creating new one instead:" << url.toDisplayString() << "aka" << localFilePath; qApp->restoreOverrideCursor(); /// Opening file failed, creating new one instead initializeNew(); return false; } FileImporter *importer = fileImporterFactory(url); importer->showImportDialog(p->widget()); bibTeXFile = importer->load(&inputfile); inputfile.close(); delete importer; if (bibTeXFile == nullptr) { qCWarning(LOG_KBIBTEX_PARTS) << "Opening file failed, creating new one instead:" << url.toDisplayString() << "aka" << localFilePath; qApp->restoreOverrideCursor(); /// Opening file failed, creating new one instead initializeNew(); return false; } bibTeXFile->setProperty(File::Url, QUrl(url)); model->setBibliographyFile(bibTeXFile); if (sortFilterProxyModel != nullptr) delete sortFilterProxyModel; sortFilterProxyModel = new SortFilterFileModel(p); sortFilterProxyModel->setSourceModel(model); partWidget->fileView()->setModel(sortFilterProxyModel); connect(partWidget->filterBar(), &FilterBar::filterChanged, sortFilterProxyModel, &SortFilterFileModel::updateFilter); if (url.isLocalFile()) fileSystemWatcher.addPath(url.toLocalFile()); qApp->restoreOverrideCursor(); return true; } void makeBackup(const QUrl &url) const { /// Fetch settings from configuration const int numberOfBackups = Preferences::instance().numberOfBackups(); /// Stop right here if no backup is requested if (Preferences::instance().backupScope() == Preferences::NoBackup) return; /// For non-local files, proceed only if backups to remote storage is allowed if (Preferences::instance().backupScope() != Preferences::BothLocalAndRemote && !url.isLocalFile()) return; /// Do not make backup copies if destination file does not exist yet KIO::StatJob *statJob = KIO::stat(url, KIO::StatJob::DestinationSide, 0 /** not details necessary, just need to know if file exists */, KIO::HideProgressInfo); KJobWidgets::setWindow(statJob, p->widget()); statJob->exec(); if (statJob->error() == KIO::ERR_DOES_NOT_EXIST) return; else if (statJob->error() != KIO::Job::NoError) { /// Something else went wrong, quit with error qCWarning(LOG_KBIBTEX_PARTS) << "Probing" << url.toDisplayString() << "failed:" << statJob->errorString(); return; } bool copySucceeded = true; /// Copy e.g. test.bib~ to test.bib~2, test.bib to test.bib~ etc. for (int level = numberOfBackups; copySucceeded && level >= 1; --level) { QUrl newerBackupUrl = url; constructBackupUrl(level - 1, newerBackupUrl); QUrl olderBackupUrl = url; constructBackupUrl(level, olderBackupUrl); statJob = KIO::stat(newerBackupUrl, KIO::StatJob::DestinationSide, 0 /** not details necessary, just need to know if file exists */, KIO::HideProgressInfo); KJobWidgets::setWindow(statJob, p->widget()); if (statJob->exec() && statJob->error() == KIO::Job::NoError) { KIO::CopyJob *moveJob = nullptr; ///< guaranteed to be initialized in either branch of the following code /** * The following 'if' block is necessary to handle the * following situation: User opens, modifies, and saves * file /tmp/b/bbb.bib which is actually a symlink to * file /tmp/a/aaa.bib. Now a 'move' operation like the * implicit 'else' section below does, would move /tmp/b/bbb.bib * to become /tmp/b/bbb.bib~ still pointing to /tmp/a/aaa.bib. * Then, the save operation would create a new file /tmp/b/bbb.bib * without any symbolic linking to /tmp/a/aaa.bib. * The following code therefore checks if /tmp/b/bbb.bib is * to be copied/moved to /tmp/b/bbb.bib~ and /tmp/b/bbb.bib * is a local file and /tmp/b/bbb.bib is a symbolic link to * another file. Then /tmp/b/bbb.bib is resolved to the real * file /tmp/a/aaa.bib which is then copied into plain file * /tmp/b/bbb.bib~. The save function (outside of this function's * scope) will then see that /tmp/b/bbb.bib is a symbolic link, * resolve this symlink to /tmp/a/aaa.bib, and then write * all changes to /tmp/a/aaa.bib keeping /tmp/b/bbb.bib a * link to. */ if (level == 1 && newerBackupUrl.isLocalFile() /** for level==1, this is actually the current file*/) { QFileInfo newerBackupFileInfo(newerBackupUrl.toLocalFile()); if (newerBackupFileInfo.isSymLink()) { while (newerBackupFileInfo.isSymLink()) { newerBackupUrl = QUrl::fromLocalFile(newerBackupFileInfo.symLinkTarget()); newerBackupFileInfo = QFileInfo(newerBackupUrl.toLocalFile()); } moveJob = KIO::copy(newerBackupUrl, olderBackupUrl, KIO::HideProgressInfo | KIO::Overwrite); } } if (moveJob == nullptr) ///< implicit 'else' section, see longer comment above moveJob = KIO::move(newerBackupUrl, olderBackupUrl, KIO::HideProgressInfo | KIO::Overwrite); KJobWidgets::setWindow(moveJob, p->widget()); copySucceeded = moveJob->exec(); } } if (!copySucceeded) KMessageBox::error(p->widget(), i18n("Could not create backup copies of document '%1'.", url.url(QUrl::PreferLocalFile)), i18n("Backup copies")); } QUrl getSaveFilename(bool mustBeImportable = true) { QString startDir = p->url().isValid() ? p->url().path() : QString(); QString supportedMimeTypes = QStringLiteral("text/x-bibtex text/x-research-info-systems"); if (BibUtils::available()) supportedMimeTypes += QStringLiteral(" application/x-isi-export-format application/x-endnote-refer"); if (!mustBeImportable && !QStandardPaths::findExecutable(QStringLiteral("pdflatex")).isEmpty()) supportedMimeTypes += QStringLiteral(" application/pdf"); if (!mustBeImportable && !QStandardPaths::findExecutable(QStringLiteral("dvips")).isEmpty()) supportedMimeTypes += QStringLiteral(" application/postscript"); if (!mustBeImportable) supportedMimeTypes += QStringLiteral(" text/html"); if (!mustBeImportable && !QStandardPaths::findExecutable(QStringLiteral("latex2rtf")).isEmpty()) supportedMimeTypes += QStringLiteral(" application/rtf"); QPointer<QFileDialog> saveDlg = new QFileDialog(p->widget(), i18n("Save file") /* TODO better text */, startDir, supportedMimeTypes); /// Setting list of mime types for the second time, /// essentially calling this function only to set the "default mime type" parameter saveDlg->setMimeTypeFilters(supportedMimeTypes.split(QLatin1Char(' '), QString::SkipEmptyParts)); /// Setting the dialog into "Saving" mode make the "add extension" checkbox available saveDlg->setAcceptMode(QFileDialog::AcceptSave); saveDlg->setDefaultSuffix(QStringLiteral("bib")); saveDlg->setFileMode(QFileDialog::AnyFile); if (saveDlg->exec() != QDialog::Accepted) /// User cancelled saving operation, return invalid filename/URL return QUrl(); const QList<QUrl> selectedUrls = saveDlg->selectedUrls(); delete saveDlg; return selectedUrls.isEmpty() ? QUrl() : selectedUrls.first(); } FileExporter *saveFileExporter(const QString &ending) { FileExporter *exporter = fileExporterFactory(ending); if (isSaveAsOperation) { /// only show export dialog at SaveAs or SaveCopyAs operations FileExporterToolchain *fet = nullptr; if (FileExporterBibTeX::isFileExporterBibTeX(*exporter)) { QPointer<QDialog> dlg = new QDialog(p->widget()); dlg->setWindowTitle(i18n("BibTeX File Settings")); QBoxLayout *layout = new QVBoxLayout(dlg); FileSettingsWidget *settingsWidget = new FileSettingsWidget(dlg); layout->addWidget(settingsWidget); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::RestoreDefaults | QDialogButtonBox::Reset | QDialogButtonBox::Ok, Qt::Horizontal, dlg); layout->addWidget(buttonBox); connect(buttonBox->button(QDialogButtonBox::RestoreDefaults), &QPushButton::clicked, settingsWidget, &FileSettingsWidget::resetToDefaults); connect(buttonBox->button(QDialogButtonBox::Reset), &QPushButton::clicked, settingsWidget, &FileSettingsWidget::resetToLoadedProperties); connect(buttonBox->button(QDialogButtonBox::Ok), &QPushButton::clicked, dlg.data(), &QDialog::accept); settingsWidget->loadProperties(bibTeXFile); if (dlg->exec() == QDialog::Accepted) settingsWidget->saveProperties(bibTeXFile); delete dlg; } else if ((fet = qobject_cast<FileExporterToolchain *>(exporter)) != nullptr) { QPointer<QDialog> dlg = new QDialog(p->widget()); dlg->setWindowTitle(i18n("PDF/PostScript File Settings")); QBoxLayout *layout = new QVBoxLayout(dlg); SettingsFileExporterPDFPSWidget *settingsWidget = new SettingsFileExporterPDFPSWidget(dlg); layout->addWidget(settingsWidget); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::RestoreDefaults | QDialogButtonBox::Reset | QDialogButtonBox::Ok, Qt::Horizontal, dlg); layout->addWidget(buttonBox); connect(buttonBox->button(QDialogButtonBox::RestoreDefaults), &QPushButton::clicked, settingsWidget, &SettingsFileExporterPDFPSWidget::resetToDefaults); connect(buttonBox->button(QDialogButtonBox::Reset), &QPushButton::clicked, settingsWidget, &SettingsFileExporterPDFPSWidget::loadState); connect(buttonBox->button(QDialogButtonBox::Ok), &QPushButton::clicked, dlg.data(), &QDialog::accept); if (dlg->exec() == QDialog::Accepted) settingsWidget->saveState(); delete dlg; } } return exporter; } bool saveFile(QFile &file, FileExporter *exporter, QStringList *errorLog = nullptr) { SortFilterFileModel *model = qobject_cast<SortFilterFileModel *>(partWidget->fileView()->model()); Q_ASSERT_X(model != nullptr, "FileExporter *KBibTeXPart::KBibTeXPartPrivate:saveFile(...)", "SortFilterFileModel *model from editor->model() is invalid"); return exporter->save(&file, model->fileSourceModel()->bibliographyFile(), errorLog); } bool saveFile(const QUrl &url) { bool result = false; - Q_ASSERT_X(!url.isEmpty(), "bool KBibTeXPart::KBibTeXPartPrivate:saveFile(const QUrl &url)", "url is not allowed to be empty"); + Q_ASSERT_X(url.isValid(), "bool KBibTeXPart::KBibTeXPartPrivate:saveFile(const QUrl &url)", "url must be valid"); /// Extract filename extension (e.g. 'bib') to determine which FileExporter to use static const QRegularExpression suffixRegExp(QStringLiteral("\\.([^.]{1,4})$")); const QRegularExpressionMatch suffixRegExpMatch = suffixRegExp.match(url.fileName()); const QString ending = suffixRegExpMatch.hasMatch() ? suffixRegExpMatch.captured(1) : QStringLiteral("bib"); FileExporter *exporter = saveFileExporter(ending); /// String list to collect error message from FileExporer QStringList errorLog; qApp->setOverrideCursor(Qt::WaitCursor); if (url.isLocalFile()) { /// Take precautions for local files QFileInfo fileInfo(url.toLocalFile()); /// Do not overwrite symbolic link, but linked file instead QString filename = fileInfo.absoluteFilePath(); while (fileInfo.isSymLink()) { filename = fileInfo.symLinkTarget(); fileInfo = QFileInfo(filename); } if (!fileInfo.exists() || fileInfo.isWritable()) { /// Make backup before overwriting target destination, intentionally /// using the provided filename, not the resolved symlink makeBackup(url); QFile file(filename); if (file.open(QIODevice::WriteOnly)) { result = saveFile(file, exporter, &errorLog); file.close(); } } } else { /// URL points to a remote location /// Configure and open temporary file QTemporaryFile temporaryFile(QStandardPaths::writableLocation(QStandardPaths::TempLocation) + QDir::separator() + QStringLiteral("kbibtex_savefile_XXXXXX") + ending); temporaryFile.setAutoRemove(true); if (temporaryFile.open()) { result = saveFile(temporaryFile, exporter, &errorLog); /// Close/flush temporary file temporaryFile.close(); if (result) { /// Make backup before overwriting target destination makeBackup(url); KIO::CopyJob *copyJob = KIO::copy(QUrl::fromLocalFile(temporaryFile.fileName()), url, KIO::HideProgressInfo | KIO::Overwrite); KJobWidgets::setWindow(copyJob, p->widget()); result &= copyJob->exec() && copyJob->error() == KIO::Job::NoError; } } } qApp->restoreOverrideCursor(); delete exporter; if (!result) { QString msg = i18n("Saving the bibliography to file '%1' failed.", url.toDisplayString()); if (errorLog.isEmpty()) KMessageBox::error(p->widget(), msg, i18n("Saving bibliography failed")); else { msg += QLatin1String("\n\n"); msg += i18n("The following output was generated by the export filter:"); KMessageBox::errorList(p->widget(), msg, errorLog, i18n("Saving bibliography failed")); } } return result; } /** * Builds or resets the menu with local and remote * references (URLs, files) of an entry. * * @return Number of known references */ int updateViewDocumentMenu() { viewDocumentMenu->clear(); int result = 0; ///< Initially, no references are known File *bibliographyFile = partWidget != nullptr && partWidget->fileView() != nullptr && partWidget->fileView()->fileModel() != nullptr ? partWidget->fileView()->fileModel()->bibliographyFile() : nullptr; if (bibliographyFile == nullptr) return result; /// Retrieve Entry object of currently selected line /// in main list view QSharedPointer<const Entry> entry = partWidget->fileView()->currentElement().dynamicCast<const Entry>(); /// Test and continue if there was an Entry to retrieve if (!entry.isNull()) { /// Get list of URLs associated with this entry const auto urlList = FileInfo::entryUrls(entry, bibliographyFile->property(File::Url).toUrl(), FileInfo::TestExistenceYes); if (!urlList.isEmpty()) { /// Memorize first action, necessary to set menu title QAction *firstAction = nullptr; /// First iteration: local references only for (const QUrl &url : urlList) { /// First iteration: local references only if (!url.isLocalFile()) continue; ///< skip remote URLs /// Build a nice menu item (label, icon, ...) const QFileInfo fi(url.toLocalFile()); const QString label = QString(QStringLiteral("%1 [%2]")).arg(fi.fileName(), fi.absolutePath()); QAction *action = new QAction(QIcon::fromTheme(FileInfo::mimeTypeForUrl(url).iconName()), label, p); action->setToolTip(fi.absoluteFilePath()); /// Open URL when action is triggered connect(action, &QAction::triggered, p, [this, fi]() { elementViewDocumentMenu(QUrl::fromLocalFile(fi.absoluteFilePath())); }); viewDocumentMenu->addAction(action); /// Memorize first action if (firstAction == nullptr) firstAction = action; } if (firstAction != nullptr) { /// If there is 'first action', then there must be /// local URLs (i.e. local files) and firstAction /// is the first one where a title can be set above viewDocumentMenu->insertSection(firstAction, i18n("Local Files")); } firstAction = nullptr; /// Now the first remote action is to be memorized /// Second iteration: remote references only for (const QUrl &url : urlList) { if (url.isLocalFile()) continue; ///< skip local files /// Build a nice menu item (label, icon, ...) const QString prettyUrl = url.toDisplayString(); QAction *action = new QAction(QIcon::fromTheme(FileInfo::mimeTypeForUrl(url).iconName()), prettyUrl, p); action->setToolTip(prettyUrl); /// Open URL when action is triggered connect(action, &QAction::triggered, p, [this, url]() { elementViewDocumentMenu(url); }); viewDocumentMenu->addAction(action); /// Memorize first action if (firstAction == nullptr) firstAction = action; } if (firstAction != nullptr) { /// If there is 'first action', then there must be /// some remote URLs and firstAction is the first /// one where a title can be set above viewDocumentMenu->insertSection(firstAction, i18n("Remote Files")); } result = urlList.count(); } } return result; } void readConfiguration() { disconnect(partWidget->fileView(), &FileView::elementExecuted, partWidget->fileView(), &FileView::editElement); disconnect(partWidget->fileView(), &FileView::elementExecuted, p, &KBibTeXPart::elementViewDocument); switch (Preferences::instance().fileViewDoubleClickAction()) { case Preferences::ActionOpenEditor: connect(partWidget->fileView(), &FileView::elementExecuted, partWidget->fileView(), &FileView::editElement); break; case Preferences::ActionViewDocument: connect(partWidget->fileView(), &FileView::elementExecuted, p, &KBibTeXPart::elementViewDocument); break; } } void elementViewDocumentMenu(const QUrl &url) { const QMimeType mimeType = FileInfo::mimeTypeForUrl(url); const QString mimeTypeName = mimeType.name(); /// Ask KDE subsystem to open url in viewer matching mime type KRun::runUrl(url, mimeTypeName, p->widget(), KRun::RunFlags()); } }; KBibTeXPart::KBibTeXPart(QWidget *parentWidget, QObject *parent, const KAboutData &componentData) : KParts::ReadWritePart(parent), d(new KBibTeXPartPrivate(parentWidget, this)) { setComponentData(componentData); setWidget(d->partWidget); updateActions(); d->initializeNew(); setXMLFile(RCFileName); NotificationHub::registerNotificationListener(this, NotificationHub::EventConfigurationChanged); d->readConfiguration(); setModified(false); } KBibTeXPart::~KBibTeXPart() { delete d; } void KBibTeXPart::setModified(bool modified) { KParts::ReadWritePart::setModified(modified); d->fileSaveAction->setEnabled(modified); } void KBibTeXPart::notificationEvent(int eventId) { if (eventId == NotificationHub::EventConfigurationChanged) d->readConfiguration(); } bool KBibTeXPart::saveFile() { Q_ASSERT_X(isReadWrite(), "bool KBibTeXPart::saveFile()", "Trying to save although document is in read-only mode"); - if (url().isEmpty()) + if (!url().isValid()) return documentSaveAs(); /// If the current file is "watchable" (i.e. a local file), /// memorize local filename for future reference const QString watchableFilename = url().isValid() && url().isLocalFile() ? url().toLocalFile() : QString(); /// Stop watching local file that will be written to if (!watchableFilename.isEmpty()) d->fileSystemWatcher.removePath(watchableFilename); else qCWarning(LOG_KBIBTEX_PARTS) << "watchableFilename is Empty"; const bool saveOperationSuccess = d->saveFile(url()); if (!watchableFilename.isEmpty()) { /// Continue watching a local file after write operation, but do /// so only after a short delay. The delay is necessary in some /// situations as observed in KDE bug report 396343 where the /// DropBox client seemingly touched the file right after saving /// from within KBibTeX, triggering KBibTeX to show a 'reload' /// message box. QTimer::singleShot(500, this, [this, watchableFilename]() { d->fileSystemWatcher.addPath(watchableFilename); }); } else qCWarning(LOG_KBIBTEX_PARTS) << "watchableFilename is Empty"; if (!saveOperationSuccess) { 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())); return false; } return true; } bool KBibTeXPart::documentSave() { d->isSaveAsOperation = false; if (!isReadWrite()) return documentSaveCopyAs(); else if (!url().isValid()) return documentSaveAs(); else return KParts::ReadWritePart::save(); } bool KBibTeXPart::documentSaveAs() { d->isSaveAsOperation = true; QUrl newUrl = d->getSaveFilename(); if (!newUrl.isValid()) return false; /// Remove old URL from file system watcher if (url().isValid() && url().isLocalFile()) { const QString path = url().toLocalFile(); if (!path.isEmpty()) d->fileSystemWatcher.removePath(path); else qCWarning(LOG_KBIBTEX_PARTS) << "No filename to stop watching"; } else qCWarning(LOG_KBIBTEX_PARTS) << "Not removing" << url().url(QUrl::PreferLocalFile) << "from fileSystemWatcher"; // TODO how does SaveAs dialog know which mime types to support? if (KParts::ReadWritePart::saveAs(newUrl)) { // FIXME d->model->bibliographyFile()->setProperty(File::Url, newUrl); return true; } else return false; } bool KBibTeXPart::documentSaveCopyAs() { d->isSaveAsOperation = true; QUrl newUrl = d->getSaveFilename(false); if (!newUrl.isValid() || newUrl == url()) return false; /// difference from KParts::ReadWritePart::saveAs: /// current document's URL won't be changed return d->saveFile(newUrl); } void KBibTeXPart::elementViewDocument() { QUrl url; const QList<QAction *> actionList = d->viewDocumentMenu->actions(); /// Go through all actions (i.e. document URLs) for this element for (const QAction *action : actionList) { /// Make URL from action's data ... QUrl tmpUrl = QUrl(action->data().toString()); /// ... but skip this action if the URL is invalid if (!tmpUrl.isValid()) continue; if (tmpUrl.isLocalFile()) { /// If action's URL points to local file, /// keep it and stop search for document url = tmpUrl; break; } else if (!url.isValid()) /// First valid URL found, keep it /// URL is not local, so it may get overwritten by another URL url = tmpUrl; } /// Open selected URL if (url.isValid()) { /// Guess mime type for url to open QMimeType mimeType = FileInfo::mimeTypeForUrl(url); const QString mimeTypeName = mimeType.name(); /// Ask KDE subsystem to open url in viewer matching mime type KRun::runUrl(url, mimeTypeName, widget(), KRun::RunFlags()); } } void KBibTeXPart::elementFindPDF() { QModelIndexList mil = d->partWidget->fileView()->selectionModel()->selectedRows(); if (mil.count() == 1) { QSharedPointer<Entry> entry = d->partWidget->fileView()->fileModel()->element(d->partWidget->fileView()->sortFilterProxyModel()->mapToSource(*mil.constBegin()).row()).dynamicCast<Entry>(); if (!entry.isNull()) FindPDFUI::interactiveFindPDF(*entry, *d->bibTeXFile, widget()); } } void KBibTeXPart::applyDefaultFormatString() { FileModel *model = d->partWidget != nullptr && d->partWidget->fileView() != nullptr ? d->partWidget->fileView()->fileModel() : nullptr; if (model == nullptr) return; bool documentModified = false; const QModelIndexList mil = d->partWidget->fileView()->selectionModel()->selectedRows(); for (const QModelIndex &index : mil) { QSharedPointer<Entry> entry = model->element(d->partWidget->fileView()->sortFilterProxyModel()->mapToSource(index).row()).dynamicCast<Entry>(); if (!entry.isNull()) { static IdSuggestions idSuggestions; bool success = idSuggestions.applyDefaultFormatId(*entry.data()); documentModified |= success; if (!success) { KMessageBox::information(widget(), i18n("Cannot apply default formatting for entry ids: No default format specified."), i18n("Cannot Apply Default Formatting")); break; } } } if (documentModified) d->partWidget->fileView()->externalModification(); } bool KBibTeXPart::openFile() { const bool success = d->openFile(url(), localFilePath()); emit completed(); return success; } void KBibTeXPart::newEntryTriggered() { QSharedPointer<Entry> newEntry = QSharedPointer<Entry>(new Entry(QStringLiteral("Article"), d->findUnusedId())); d->model->insertRow(newEntry, d->model->rowCount()); d->partWidget->fileView()->setSelectedElement(newEntry); if (d->partWidget->fileView()->editElement(newEntry)) d->partWidget->fileView()->scrollToBottom(); // FIXME always correct behaviour? else { /// Editing this new element was cancelled, /// therefore remove it again d->model->removeRow(d->model->rowCount() - 1); } } void KBibTeXPart::newMacroTriggered() { QSharedPointer<Macro> newMacro = QSharedPointer<Macro>(new Macro(d->findUnusedId())); d->model->insertRow(newMacro, d->model->rowCount()); d->partWidget->fileView()->setSelectedElement(newMacro); if (d->partWidget->fileView()->editElement(newMacro)) d->partWidget->fileView()->scrollToBottom(); // FIXME always correct behaviour? else { /// Editing this new element was cancelled, /// therefore remove it again d->model->removeRow(d->model->rowCount() - 1); } } void KBibTeXPart::newPreambleTriggered() { QSharedPointer<Preamble> newPreamble = QSharedPointer<Preamble>(new Preamble()); d->model->insertRow(newPreamble, d->model->rowCount()); d->partWidget->fileView()->setSelectedElement(newPreamble); if (d->partWidget->fileView()->editElement(newPreamble)) d->partWidget->fileView()->scrollToBottom(); // FIXME always correct behaviour? else { /// Editing this new element was cancelled, /// therefore remove it again d->model->removeRow(d->model->rowCount() - 1); } } void KBibTeXPart::newCommentTriggered() { QSharedPointer<Comment> newComment = QSharedPointer<Comment>(new Comment()); d->model->insertRow(newComment, d->model->rowCount()); d->partWidget->fileView()->setSelectedElement(newComment); if (d->partWidget->fileView()->editElement(newComment)) d->partWidget->fileView()->scrollToBottom(); // FIXME always correct behaviour? else { /// Editing this new element was cancelled, /// therefore remove it again d->model->removeRow(d->model->rowCount() - 1); } } void KBibTeXPart::updateActions() { FileModel *model = d->partWidget != nullptr && d->partWidget->fileView() != nullptr ? d->partWidget->fileView()->fileModel() : nullptr; if (model == nullptr) return; bool emptySelection = d->partWidget->fileView()->selectedElements().isEmpty(); d->elementEditAction->setEnabled(!emptySelection); d->editCopyAction->setEnabled(!emptySelection); d->editCopyReferencesAction->setEnabled(!emptySelection); d->editCutAction->setEnabled(!emptySelection && isReadWrite()); d->editPasteAction->setEnabled(isReadWrite()); d->editDeleteAction->setEnabled(!emptySelection && isReadWrite()); d->elementFindPDFAction->setEnabled(!emptySelection && isReadWrite()); d->entryApplyDefaultFormatString->setEnabled(!emptySelection && isReadWrite()); d->colorLabelContextMenu->menuAction()->setEnabled(!emptySelection && isReadWrite()); d->colorLabelContextMenuAction->setEnabled(!emptySelection && isReadWrite()); int numDocumentsToView = d->updateViewDocumentMenu(); /// enable menu item only if there is at least one document to view d->elementViewDocumentAction->setEnabled(!emptySelection && numDocumentsToView > 0); /// activate sub-menu only if there are at least two documents to view d->elementViewDocumentAction->setMenu(numDocumentsToView > 1 ? d->viewDocumentMenu : nullptr); d->elementViewDocumentAction->setToolTip(numDocumentsToView == 1 ? (*d->viewDocumentMenu->actions().constBegin())->text() : QString()); /// update list of references which can be sent to LyX QStringList references; if (d->partWidget->fileView()->selectionModel() != nullptr) { const QModelIndexList mil = d->partWidget->fileView()->selectionModel()->selectedRows(); references.reserve(mil.size()); for (const QModelIndex &index : mil) { const QSharedPointer<Entry> entry = model->element(d->partWidget->fileView()->sortFilterProxyModel()->mapToSource(index).row()).dynamicCast<Entry>(); if (!entry.isNull()) references << entry->id(); } } d->lyx->setReferences(references); } void KBibTeXPart::fileExternallyChange(const QString &path) { /// Should never happen: triggering this slot for non-local or invalid URLs if (!url().isValid() || !url().isLocalFile()) return; /// Should never happen: triggering this slot for filenames not being the opened file if (path != url().toLocalFile()) { qCWarning(LOG_KBIBTEX_PARTS) << "Got file modification warning for wrong file: " << path << "!=" << url().toLocalFile(); return; } /// Stop watching file while asking for user interaction if (!path.isEmpty()) d->fileSystemWatcher.removePath(path); else qCWarning(LOG_KBIBTEX_PARTS) << "No filename to stop watching"; 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) { d->openFile(QUrl::fromLocalFile(path), path); /// No explicit call to QFileSystemWatcher.addPath(...) necessary, /// openFile(...) has done that already } else { /// Even if the user did not request reloaded the file, /// still resume watching file for future external changes if (!path.isEmpty()) d->fileSystemWatcher.addPath(path); else qCWarning(LOG_KBIBTEX_PARTS) << "path is Empty"; } } #include "part.moc" diff --git a/src/program/mainwindow.cpp b/src/program/mainwindow.cpp index f30b99c9..4ee7ce3b 100644 --- a/src/program/mainwindow.cpp +++ b/src/program/mainwindow.cpp @@ -1,481 +1,481 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, see <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "mainwindow.h" #include <QDockWidget> #include <QDragEnterEvent> #include <QDropEvent> #include <QLabel> #include <QMimeData> #include <QPointer> #include <QMenu> #include <QTimer> #include <QApplication> #include <QFileDialog> #include <QAction> #include <KActionMenu> #include <KActionCollection> #include <KPluginFactory> #include <KPluginLoader> #include <KLocalizedString> #include <KMessageBox> #include <KBibTeX> #include <preferences/KBibTeXPreferencesDialog> #include <file/FileView> #include <XSLTransform> #include <BibliographyService> #include <BibUtils> #include "docklets/referencepreview.h" #include "docklets/documentpreview.h" #include "docklets/searchform.h" #include "docklets/searchresults.h" #include "docklets/elementform.h" #include "docklets/documentpreview.h" #include "docklets/statistics.h" #include "docklets/filesettings.h" #include "docklets/valuelist.h" #include "docklets/zoterobrowser.h" #include "documentlist.h" #include "mdiwidget.h" class KBibTeXMainWindow::KBibTeXMainWindowPrivate { private: KBibTeXMainWindow *p; public: QAction *actionClose; QDockWidget *dockDocumentList; QDockWidget *dockReferencePreview; QDockWidget *dockDocumentPreview; QDockWidget *dockValueList; QDockWidget *dockZotero; QDockWidget *dockStatistics; QDockWidget *dockSearchForm; QDockWidget *dockSearchResults; QDockWidget *dockElementForm; QDockWidget *dockFileSettings; DocumentList *listDocumentList; MDIWidget *mdiWidget; ReferencePreview *referencePreview; DocumentPreview *documentPreview; FileSettings *fileSettings; ValueList *valueList; ZoteroBrowser *zotero; Statistics *statistics; SearchForm *searchForm; SearchResults *searchResults; ElementForm *elementForm; QMenu *actionMenuRecentFilesMenu; KBibTeXMainWindowPrivate(KBibTeXMainWindow *parent) : p(parent) { mdiWidget = new MDIWidget(p); KActionMenu *showPanelsAction = new KActionMenu(i18n("Show Panels"), p); p->actionCollection()->addAction(QStringLiteral("settings_shown_panels"), showPanelsAction); QMenu *showPanelsMenu = new QMenu(showPanelsAction->text(), p->widget()); showPanelsAction->setMenu(showPanelsMenu); KActionMenu *actionMenuRecentFiles = new KActionMenu(QIcon::fromTheme(QStringLiteral("document-open-recent")), i18n("Recently used files"), p); p->actionCollection()->addAction(QStringLiteral("file_open_recent"), actionMenuRecentFiles); actionMenuRecentFilesMenu = new QMenu(actionMenuRecentFiles->text(), p->widget()); actionMenuRecentFiles->setMenu(actionMenuRecentFilesMenu); /** * Docklets (a.k.a. panels) will be added by default to the following * positions unless otherwise configured by the user. * - "List of Values" on the left * - "Statistics" on the left * - "List of Documents" on the left in the same tab * - "Online Search" on the left in a new tab * - "Reference Preview" on the left in the same tab * - "Search Results" on the bottom * - "Document Preview" is hidden * - "Element Editor" is hidden */ dockDocumentList = new QDockWidget(i18n("List of Documents"), p); dockDocumentList->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); p->addDockWidget(Qt::LeftDockWidgetArea, dockDocumentList); listDocumentList = new DocumentList(dockDocumentList); dockDocumentList->setWidget(listDocumentList); dockDocumentList->setObjectName(QStringLiteral("dockDocumentList")); dockDocumentList->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable); connect(listDocumentList, &DocumentList::openFile, p, &KBibTeXMainWindow::openDocument); showPanelsMenu->addAction(dockDocumentList->toggleViewAction()); dockValueList = new QDockWidget(i18n("List of Values"), p); dockValueList->setAllowedAreas(Qt::BottomDockWidgetArea | Qt::TopDockWidgetArea | Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); p->addDockWidget(Qt::LeftDockWidgetArea, dockValueList); valueList = new ValueList(dockValueList); dockValueList->setWidget(valueList); dockValueList->setObjectName(QStringLiteral("dockValueList")); dockValueList->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable); showPanelsMenu->addAction(dockValueList->toggleViewAction()); dockStatistics = new QDockWidget(i18n("Statistics"), p); dockStatistics->setAllowedAreas(Qt::BottomDockWidgetArea | Qt::TopDockWidgetArea | Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); p->addDockWidget(Qt::LeftDockWidgetArea, dockStatistics); statistics = new Statistics(dockStatistics); dockStatistics->setWidget(statistics); dockStatistics->setObjectName(QStringLiteral("dockStatistics")); dockStatistics->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable); showPanelsMenu->addAction(dockStatistics->toggleViewAction()); dockSearchResults = new QDockWidget(i18n("Search Results"), p); dockSearchResults->setAllowedAreas(Qt::BottomDockWidgetArea | Qt::TopDockWidgetArea | Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); p->addDockWidget(Qt::BottomDockWidgetArea, dockSearchResults); dockSearchResults->hide(); searchResults = new SearchResults(mdiWidget, dockSearchResults); dockSearchResults->setWidget(searchResults); dockSearchResults->setObjectName(QStringLiteral("dockResultsFrom")); dockSearchResults->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable); showPanelsMenu->addAction(dockSearchResults->toggleViewAction()); connect(mdiWidget, &MDIWidget::documentSwitched, searchResults, &SearchResults::documentSwitched); dockSearchForm = new QDockWidget(i18n("Online Search"), p); dockSearchForm->setAllowedAreas(Qt::BottomDockWidgetArea | Qt::TopDockWidgetArea | Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); p->addDockWidget(Qt::LeftDockWidgetArea, dockSearchForm); searchForm = new SearchForm(searchResults, dockSearchForm); connect(searchForm, &SearchForm::doneSearching, p, &KBibTeXMainWindow::showSearchResults); dockSearchForm->setWidget(searchForm); dockSearchForm->setObjectName(QStringLiteral("dockSearchFrom")); dockSearchForm->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable); showPanelsMenu->addAction(dockSearchForm->toggleViewAction()); dockZotero = new QDockWidget(i18n("Zotero"), p); dockZotero->setAllowedAreas(Qt::BottomDockWidgetArea | Qt::TopDockWidgetArea | Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); p->addDockWidget(Qt::LeftDockWidgetArea, dockZotero); zotero = new ZoteroBrowser(searchResults, dockZotero); connect(dockZotero, &QDockWidget::visibilityChanged, zotero, &ZoteroBrowser::visibiltyChanged); connect(zotero, &ZoteroBrowser::itemToShow, p, &KBibTeXMainWindow::showSearchResults); dockZotero->setWidget(zotero); dockZotero->setObjectName(QStringLiteral("dockZotero")); dockZotero->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable); showPanelsMenu->addAction(dockZotero->toggleViewAction()); dockReferencePreview = new QDockWidget(i18n("Reference Preview"), p); dockReferencePreview->setAllowedAreas(Qt::BottomDockWidgetArea | Qt::TopDockWidgetArea | Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); p->addDockWidget(Qt::LeftDockWidgetArea, dockReferencePreview); referencePreview = new ReferencePreview(dockReferencePreview); dockReferencePreview->setWidget(referencePreview); dockReferencePreview->setObjectName(QStringLiteral("dockReferencePreview")); dockReferencePreview->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable); showPanelsMenu->addAction(dockReferencePreview->toggleViewAction()); dockDocumentPreview = new QDockWidget(i18n("Document Preview"), p); dockDocumentPreview->setAllowedAreas(Qt::BottomDockWidgetArea | Qt::TopDockWidgetArea | Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); p->addDockWidget(Qt::RightDockWidgetArea, dockDocumentPreview); dockDocumentPreview->hide(); documentPreview = new DocumentPreview(dockDocumentPreview); dockDocumentPreview->setWidget(documentPreview); dockDocumentPreview->setObjectName(QStringLiteral("dockDocumentPreview")); dockDocumentPreview->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable); showPanelsMenu->addAction(dockDocumentPreview->toggleViewAction()); p->actionCollection()->setDefaultShortcut(dockDocumentPreview->toggleViewAction(), Qt::CTRL + Qt::SHIFT + Qt::Key_D); dockElementForm = new QDockWidget(i18n("Element Editor"), p); dockElementForm->setAllowedAreas(Qt::BottomDockWidgetArea | Qt::TopDockWidgetArea | Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); p->addDockWidget(Qt::BottomDockWidgetArea, dockElementForm); dockElementForm->hide(); elementForm = new ElementForm(mdiWidget, dockElementForm); dockElementForm->setWidget(elementForm); dockElementForm->setObjectName(QStringLiteral("dockElementFrom")); dockElementForm->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable); showPanelsMenu->addAction(dockElementForm->toggleViewAction()); dockFileSettings = new QDockWidget(i18n("File Settings"), p); dockFileSettings->setAllowedAreas(Qt::BottomDockWidgetArea | Qt::TopDockWidgetArea | Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); p->addDockWidget(Qt::LeftDockWidgetArea, dockFileSettings); fileSettings = new FileSettings(dockFileSettings); dockFileSettings->setWidget(fileSettings); dockFileSettings->setObjectName(QStringLiteral("dockFileSettings")); dockFileSettings->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable); showPanelsMenu->addAction(dockFileSettings->toggleViewAction()); p->tabifyDockWidget(dockFileSettings, dockSearchForm); p->tabifyDockWidget(dockZotero, dockSearchForm); p->tabifyDockWidget(dockValueList, dockStatistics); p->tabifyDockWidget(dockStatistics, dockFileSettings); p->tabifyDockWidget(dockSearchForm, dockReferencePreview); p->tabifyDockWidget(dockFileSettings, dockDocumentList); QAction *action = p->actionCollection()->addAction(KStandardAction::New); connect(action, &QAction::triggered, p, &KBibTeXMainWindow::newDocument); action = p->actionCollection()->addAction(KStandardAction::Open); connect(action, &QAction::triggered, p, &KBibTeXMainWindow::openDocumentDialog); actionClose = p->actionCollection()->addAction(KStandardAction::Close); connect(actionClose, &QAction::triggered, p, &KBibTeXMainWindow::closeDocument); actionClose->setEnabled(false); action = p->actionCollection()->addAction(KStandardAction::Quit); connect(action, &QAction::triggered, p, &KBibTeXMainWindow::queryCloseAll); action = p->actionCollection()->addAction(KStandardAction::Preferences); connect(action, &QAction::triggered, p, &KBibTeXMainWindow::showPreferences); } ~KBibTeXMainWindowPrivate() { elementForm->deleteLater(); delete mdiWidget; // TODO other deletes } }; KBibTeXMainWindow::KBibTeXMainWindow(QWidget *parent) : KParts::MainWindow(parent, (Qt::WindowFlags)KDE_DEFAULT_WINDOWFLAGS), d(new KBibTeXMainWindowPrivate(this)) { setObjectName(QStringLiteral("KBibTeXShell")); setXMLFile(QStringLiteral("kbibtexui.rc")); setCentralWidget(d->mdiWidget); connect(d->mdiWidget, &MDIWidget::documentSwitched, this, &KBibTeXMainWindow::documentSwitched); connect(d->mdiWidget, &MDIWidget::activePartChanged, this, &KBibTeXMainWindow::createGUI); ///< actually: KParts::MainWindow::createGUI connect(d->mdiWidget, &MDIWidget::documentNew, this, &KBibTeXMainWindow::newDocument); connect(d->mdiWidget, &MDIWidget::documentOpen, this, &KBibTeXMainWindow::openDocumentDialog); connect(d->mdiWidget, &MDIWidget::documentOpenURL, this, &KBibTeXMainWindow::openDocument); connect(&OpenFileInfoManager::instance(), &OpenFileInfoManager::currentChanged, d->mdiWidget, &MDIWidget::setFile); connect(&OpenFileInfoManager::instance(), &OpenFileInfoManager::flagsChanged, this, &KBibTeXMainWindow::documentListsChanged); connect(d->mdiWidget, &MDIWidget::setCaption, this, static_cast<void(KMainWindow::*)(const QString &)>(&KMainWindow::setCaption)); ///< actually: KMainWindow::setCaption documentListsChanged(OpenFileInfo::RecentlyUsed); /// force initialization of menu of recently used files setupControllers(); setupGUI(KXmlGuiWindow::Create | KXmlGuiWindow::Save | KXmlGuiWindow::Keys | KXmlGuiWindow::ToolBar); setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); setAcceptDrops(true); QTimer::singleShot(500, this, &KBibTeXMainWindow::delayed); } KBibTeXMainWindow::~KBibTeXMainWindow() { delete d; } void KBibTeXMainWindow::setupControllers() { // TODO } void KBibTeXMainWindow::dragEnterEvent(QDragEnterEvent *event) { if (event->mimeData()->hasUrls()) event->acceptProposedAction(); } void KBibTeXMainWindow::dropEvent(QDropEvent *event) { QList<QUrl> urlList = event->mimeData()->urls(); if (urlList.isEmpty()) { const QUrl url(event->mimeData()->text()); if (url.isValid()) urlList << url; } if (!urlList.isEmpty()) for (const QUrl &url : const_cast<const QList<QUrl> &>(urlList)) openDocument(url); } void KBibTeXMainWindow::newDocument() { const QString mimeType = FileInfo::mimetypeBibTeX; OpenFileInfo *openFileInfo = OpenFileInfoManager::instance().createNew(mimeType); if (openFileInfo) OpenFileInfoManager::instance().setCurrentFile(openFileInfo); else KMessageBox::error(this, i18n("Creating a new document of mime type '%1' failed as no editor component could be instantiated.", mimeType), i18n("Creating document failed")); } void KBibTeXMainWindow::openDocumentDialog() { OpenFileInfo *currFile = OpenFileInfoManager::instance().currentFile(); QUrl currFileUrl = currFile == nullptr ? QUrl() : currFile->url(); QString startDir = currFileUrl.isValid() ? QUrl(currFileUrl.url()).path() : QString(); OpenFileInfo *ofi = OpenFileInfoManager::instance().currentFile(); if (ofi != nullptr) { QUrl url = ofi->url(); if (url.isValid()) startDir = url.path(); } /// Assemble list of supported mimetypes QStringList supportedMimeTypes {QStringLiteral("text/x-bibtex"), QStringLiteral("application/x-research-info-systems"), QStringLiteral("application/xml")}; if (BibUtils::available()) { supportedMimeTypes.append(QStringLiteral("application/x-isi-export-format")); supportedMimeTypes.append(QStringLiteral("application/x-endnote-refer")); } supportedMimeTypes.append(QStringLiteral("application/pdf")); supportedMimeTypes.append(QStringLiteral("all/all")); QPointer<QFileDialog> dlg = new QFileDialog(this, i18n("Open file") /* TODO better text */, startDir); dlg->setMimeTypeFilters(supportedMimeTypes); dlg->setFileMode(QFileDialog::ExistingFile); const bool dialogAccepted = dlg->exec() != 0; const QUrl url = (dialogAccepted && !dlg->selectedUrls().isEmpty()) ? dlg->selectedUrls().first() : QUrl(); delete dlg; - if (!url.isEmpty()) + if (url.isValid()) openDocument(url); } void KBibTeXMainWindow::openDocument(const QUrl &url) { OpenFileInfo *openFileInfo = OpenFileInfoManager::instance().open(url); OpenFileInfoManager::instance().setCurrentFile(openFileInfo); } void KBibTeXMainWindow::closeDocument() { OpenFileInfoManager::instance().close(OpenFileInfoManager::instance().currentFile()); } void KBibTeXMainWindow::closeEvent(QCloseEvent *event) { KMainWindow::closeEvent(event); if (OpenFileInfoManager::instance().queryCloseAll()) event->accept(); else event->ignore(); } void KBibTeXMainWindow::showPreferences() { QPointer<KBibTeXPreferencesDialog> dlg = new KBibTeXPreferencesDialog(this); dlg->exec(); delete dlg; } void KBibTeXMainWindow::documentSwitched(FileView *oldFileView, FileView *newFileView) { OpenFileInfo *openFileInfo = d->mdiWidget->currentFile(); bool validFile = openFileInfo != nullptr; d->actionClose->setEnabled(validFile); setCaption(validFile ? i18n("%1 - KBibTeX", openFileInfo->shortCaption()) : i18n("KBibTeX")); d->fileSettings->setEnabled(newFileView != nullptr); d->referencePreview->setEnabled(newFileView != nullptr); d->elementForm->setEnabled(newFileView != nullptr); d->documentPreview->setEnabled(newFileView != nullptr); if (oldFileView != nullptr) { disconnect(newFileView, &FileView::currentElementChanged, d->referencePreview, &ReferencePreview::setElement); disconnect(newFileView, &FileView::currentElementChanged, d->elementForm, &ElementForm::setElement); disconnect(newFileView, &FileView::currentElementChanged, d->documentPreview, &DocumentPreview::setElement); disconnect(newFileView, &FileView::currentElementChanged, d->searchForm, &SearchForm::setElement); disconnect(newFileView, &FileView::modified, d->valueList, &ValueList::update); disconnect(newFileView, &FileView::modified, d->statistics, &Statistics::update); // FIXME disconnect(oldEditor, SIGNAL(modified()), d->elementForm, SLOT(refreshElement())); disconnect(d->elementForm, &ElementForm::elementModified, newFileView, &FileView::externalModification); } if (newFileView != nullptr) { connect(newFileView, &FileView::currentElementChanged, d->referencePreview, &ReferencePreview::setElement); connect(newFileView, &FileView::currentElementChanged, d->elementForm, &ElementForm::setElement); connect(newFileView, &FileView::currentElementChanged, d->documentPreview, &DocumentPreview::setElement); connect(newFileView, &FileView::currentElementChanged, d->searchForm, &SearchForm::setElement); connect(newFileView, &FileView::modified, d->valueList, &ValueList::update); connect(newFileView, &FileView::modified, d->statistics, &Statistics::update); // FIXME connect(newEditor, SIGNAL(modified()), d->elementForm, SLOT(refreshElement())); connect(d->elementForm, &ElementForm::elementModified, newFileView, &FileView::externalModification); connect(d->elementForm, &ElementForm::elementModified, newFileView, &FileView::externalModification); } d->documentPreview->setBibTeXUrl(validFile ? openFileInfo->url() : QUrl()); d->referencePreview->setElement(QSharedPointer<Element>(), nullptr); d->elementForm->setElement(QSharedPointer<Element>(), nullptr); d->documentPreview->setElement(QSharedPointer<Element>(), nullptr); d->valueList->setFileView(newFileView); d->fileSettings->setFileView(newFileView); d->statistics->setFileView(newFileView); d->referencePreview->setFileView(newFileView); } void KBibTeXMainWindow::showSearchResults() { d->dockSearchResults->show(); } void KBibTeXMainWindow::documentListsChanged(OpenFileInfo::StatusFlags statusFlags) { if (statusFlags.testFlag(OpenFileInfo::RecentlyUsed)) { const OpenFileInfoManager::OpenFileInfoList list = OpenFileInfoManager::instance().filteredItems(OpenFileInfo::RecentlyUsed); d->actionMenuRecentFilesMenu->clear(); for (OpenFileInfo *cur : list) { /// Fixing bug 19511: too long filenames make menu too large, /// therefore squeeze text if it is longer than squeezeLen. const int squeezeLen = 64; const QString squeezedShortCap = squeeze_text(cur->shortCaption(), squeezeLen); const QString squeezedFullCap = squeeze_text(cur->fullCaption(), squeezeLen); QAction *action = new QAction(QString(QStringLiteral("%1 [%2]")).arg(squeezedShortCap, squeezedFullCap), this); action->setData(cur->url()); action->setIcon(QIcon::fromTheme(cur->mimeType().replace(QLatin1Char('/'), QLatin1Char('-')))); d->actionMenuRecentFilesMenu->addAction(action); connect(action, &QAction::triggered, this, &KBibTeXMainWindow::openRecentFile); } } } void KBibTeXMainWindow::openRecentFile() { QAction *action = static_cast<QAction *>(sender()); QUrl url = action->data().toUrl(); openDocument(url); } void KBibTeXMainWindow::queryCloseAll() { if (OpenFileInfoManager::instance().queryCloseAll()) qApp->quit(); } void KBibTeXMainWindow::delayed() { /// Static variable, memorizes the dynamically created /// BibliographyService instance and allows to tell if /// this slot was called for the first or second time. static BibliographyService *bs = nullptr; if (bs == nullptr) { /// First call to this slot bs = new BibliographyService(this); if (!bs->isKBibTeXdefault() && KMessageBox::questionYesNo(this, i18n("KBibTeX is not the default editor for its bibliography formats like BibTeX or RIS."), i18n("Default Bibliography Editor"), KGuiItem(i18n("Set as Default Editor")), KGuiItem(i18n("Keep settings unchanged"))) == KMessageBox::Yes) { bs->setKBibTeXasDefault(); /// QTimer calls this slot again, but as 'bs' will not be NULL, /// the 'if' construct's 'else' path will be followed. QTimer::singleShot(5000, this, &KBibTeXMainWindow::delayed); } else { /// KBibTeX is default application or user doesn't care, /// therefore clean up memory delete bs; bs = nullptr; } } else { /// Second call to this slot. This time, clean up memory. bs->deleteLater(); bs = nullptr; } }