File indexing completed on 2024-11-24 04:34:18

0001 /***************************************************************************
0002  *   SPDX-License-Identifier: GPL-2.0-or-later
0003  *                                                                         *
0004  *   SPDX-FileCopyrightText: 2004-2023 Thomas Fischer <fischer@unix-ag.uni-kl.de>
0005  *                                                                         *
0006  *   This program is free software; you can redistribute it and/or modify  *
0007  *   it under the terms of the GNU General Public License as published by  *
0008  *   the Free Software Foundation; either version 2 of the License, or     *
0009  *   (at your option) any later version.                                   *
0010  *                                                                         *
0011  *   This program is distributed in the hope that it will be useful,       *
0012  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
0013  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
0014  *   GNU General Public License for more details.                          *
0015  *                                                                         *
0016  *   You should have received a copy of the GNU General Public License     *
0017  *   along with this program; if not, see <https://www.gnu.org/licenses/>. *
0018  ***************************************************************************/
0019 
0020 #include "fieldlistedit.h"
0021 
0022 #include <typeinfo>
0023 
0024 #include <QApplication>
0025 #include <QClipboard>
0026 #include <QScrollArea>
0027 #include <QLayout>
0028 #include <QCheckBox>
0029 #include <QDragEnterEvent>
0030 #include <QDropEvent>
0031 #include <QMimeData>
0032 #include <QUrl>
0033 #include <QTimer>
0034 #include <QAction>
0035 #include <QPushButton>
0036 #include <QFontDatabase>
0037 #include <QFileDialog>
0038 #include <QMenu>
0039 
0040 #include <kwidgetsaddons_version.h>
0041 #include <KMessageBox>
0042 #include <KLocalizedString>
0043 #include <KIO/CopyJob>
0044 #include <KSharedConfig>
0045 #include <KConfigGroup>
0046 
0047 #include <File>
0048 #include <Entry>
0049 #include <FileImporterBibTeX>
0050 #include <FileExporterBibTeX>
0051 #include <FileInfo>
0052 #include <AssociatedFiles>
0053 #include "fieldlineedit.h"
0054 #include "element/associatedfilesui.h"
0055 #include "logging_gui.h"
0056 
0057 class FieldListEdit::FieldListEditProtected
0058 {
0059 private:
0060     FieldListEdit *p;
0061     const int innerSpacing;
0062     KBibTeX::TypeFlag preferredTypeFlag;
0063     KBibTeX::TypeFlags typeFlags;
0064 
0065 public:
0066     QVBoxLayout *layout;
0067     QList<FieldLineEdit *> lineEditList;
0068     QWidget *pushButtonContainer;
0069     QBoxLayout *pushButtonContainerLayout;
0070     QPushButton *addLineButton;
0071     const File *file;
0072     QString fieldKey;
0073     QWidget *container;
0074     QScrollArea *scrollArea;
0075     bool m_isReadOnly;
0076     QStringList completionItems;
0077 
0078     FieldListEditProtected(KBibTeX::TypeFlag ptf, KBibTeX::TypeFlags tf, FieldListEdit *parent)
0079             : p(parent), innerSpacing(4), preferredTypeFlag(ptf), typeFlags(tf), file(nullptr), m_isReadOnly(false) {
0080         setupGUI();
0081     }
0082 
0083     FieldListEditProtected(const FieldListEditProtected &other) = delete;
0084     FieldListEditProtected &operator= (const FieldListEditProtected &other) = delete;
0085 
0086     void setupGUI() {
0087         QBoxLayout *outerLayout = new QVBoxLayout(p);
0088         outerLayout->setContentsMargins(0, 0, 0, 0);
0089         scrollArea = new QScrollArea(p);
0090         outerLayout->addWidget(scrollArea);
0091 
0092         container = new QWidget(scrollArea->viewport());
0093         container->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Minimum);
0094         scrollArea->setWidget(container);
0095         layout = new QVBoxLayout(container);
0096         layout->setContentsMargins(0, 0, 0, 0);
0097         layout->setSpacing(innerSpacing);
0098 
0099         pushButtonContainer = new QWidget(container);
0100         pushButtonContainerLayout = new QHBoxLayout(pushButtonContainer);
0101         pushButtonContainerLayout->setContentsMargins(0, 0, 0, 0);
0102         layout->addWidget(pushButtonContainer);
0103 
0104         addLineButton = new QPushButton(QIcon::fromTheme(QStringLiteral("list-add")), i18n("Add"), pushButtonContainer);
0105         addLineButton->setObjectName(QStringLiteral("addButton"));
0106         connect(addLineButton, &QPushButton::clicked, p, static_cast<void(FieldListEdit::*)()>(&FieldListEdit::lineAdd));
0107         connect(addLineButton, &QPushButton::clicked, p, &FieldListEdit::modified);
0108         pushButtonContainerLayout->addWidget(addLineButton);
0109 
0110         layout->addStretch(100);
0111 
0112         scrollArea->setBackgroundRole(QPalette::Base);
0113         scrollArea->ensureWidgetVisible(container);
0114         scrollArea->setWidgetResizable(true);
0115     }
0116 
0117     void addButton(QPushButton *button) {
0118         button->setParent(pushButtonContainer);
0119         pushButtonContainerLayout->addWidget(button);
0120     }
0121 
0122     int recommendedHeight() {
0123         int heightHint = 0;
0124 
0125         for (const auto *fieldLineEdit : const_cast<const QList<FieldLineEdit *> &>(lineEditList))
0126             heightHint += fieldLineEdit->sizeHint().height();
0127 
0128         heightHint += lineEditList.count() * innerSpacing;
0129         heightHint += addLineButton->sizeHint().height();
0130 
0131         return heightHint;
0132     }
0133 
0134     FieldLineEdit *addFieldLineEdit() {
0135         FieldLineEdit *le = new FieldLineEdit(preferredTypeFlag, typeFlags, false, container);
0136         le->setFile(file);
0137         le->setAcceptDrops(false);
0138         le->setReadOnly(m_isReadOnly);
0139         le->setInnerWidgetsTransparency(true);
0140         layout->insertWidget(layout->count() - 2, le);
0141         lineEditList.append(le);
0142 
0143         QPushButton *remove = new QPushButton(QIcon::fromTheme(QStringLiteral("list-remove")), QString(), le);
0144         remove->setToolTip(i18n("Remove value"));
0145         le->appendWidget(remove);
0146         connect(remove, &QPushButton::clicked, p, [this, le]() {
0147             removeFieldLineEdit(le);
0148             const QSize size(container->width(), recommendedHeight());
0149             container->resize(size);
0150             /// Instead of an 'emit' ...
0151 #if QT_VERSION < QT_VERSION_CHECK(6, 5, 0)
0152             QMetaObject::invokeMethod(p, "modified", Qt::DirectConnection, QGenericReturnArgument());
0153 #else // QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
0154             QMetaObject::invokeMethod(p, "modified", Qt::DirectConnection, QMetaMethodReturnArgument());
0155 #endif
0156         });
0157 
0158         QPushButton *goDown = new QPushButton(QIcon::fromTheme(QStringLiteral("go-down")), QString(), le);
0159         goDown->setToolTip(i18n("Move value down"));
0160         le->appendWidget(goDown);
0161         connect(goDown, &QPushButton::clicked, p, [this, le]() {
0162             const bool gotModified = goDownFieldLineEdit(le);
0163             if (gotModified) {
0164                 /// Instead of an 'emit' ...
0165 #if QT_VERSION < QT_VERSION_CHECK(6, 5, 0)
0166                 QMetaObject::invokeMethod(p, "modified", Qt::DirectConnection, QGenericReturnArgument());
0167 #else // QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
0168                 QMetaObject::invokeMethod(p, "modified", Qt::DirectConnection, QMetaMethodReturnArgument());
0169 #endif
0170             }
0171         });
0172 
0173         QPushButton *goUp = new QPushButton(QIcon::fromTheme(QStringLiteral("go-up")), QString(), le);
0174         goUp->setToolTip(i18n("Move value up"));
0175         le->appendWidget(goUp);
0176         connect(goUp, &QPushButton::clicked, p, [this, le]() {
0177             const bool gotModified = goUpFieldLineEdit(le);
0178             if (gotModified) {
0179                 /// Instead of an 'emit' ...
0180 #if QT_VERSION < QT_VERSION_CHECK(6, 5, 0)
0181                 QMetaObject::invokeMethod(p, "modified", Qt::DirectConnection, QGenericReturnArgument());
0182 #else // QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
0183                 QMetaObject::invokeMethod(p, "modified", Qt::DirectConnection, QMetaMethodReturnArgument());
0184 #endif
0185             }
0186         });
0187 
0188         connect(le, &FieldLineEdit::modified, p, &FieldListEdit::modified);
0189 
0190         return le;
0191     }
0192 
0193     void removeAllFieldLineEdits() {
0194         while (!lineEditList.isEmpty()) {
0195             FieldLineEdit *fieldLineEdit = *lineEditList.begin();
0196             layout->removeWidget(fieldLineEdit);
0197             lineEditList.removeFirst();
0198             delete fieldLineEdit;
0199         }
0200 
0201         /// This fixes a layout problem where the container element
0202         /// does not shrink correctly once the line edits have been
0203         /// removed
0204         QSize pSize = container->size();
0205         pSize.setHeight(addLineButton->height());
0206         container->resize(pSize);
0207     }
0208 
0209     void removeFieldLineEdit(FieldLineEdit *fieldLineEdit) {
0210         lineEditList.removeOne(fieldLineEdit);
0211         layout->removeWidget(fieldLineEdit);
0212         delete fieldLineEdit;
0213     }
0214 
0215     bool goDownFieldLineEdit(FieldLineEdit *fieldLineEdit) {
0216         int idx = lineEditList.indexOf(fieldLineEdit);
0217         if (idx < lineEditList.count() - 1) {
0218             layout->removeWidget(fieldLineEdit);
0219             lineEditList.removeOne(fieldLineEdit);
0220             lineEditList.insert(idx + 1, fieldLineEdit);
0221             layout->insertWidget(idx + 1, fieldLineEdit);
0222             return true; ///< return 'true' upon actual modification
0223         }
0224         return false; ///< return 'false' if nothing changed, i.e. FieldLineEdit is already at bottom
0225     }
0226 
0227     bool goUpFieldLineEdit(FieldLineEdit *fieldLineEdit) {
0228         int idx = lineEditList.indexOf(fieldLineEdit);
0229         if (idx > 0) {
0230             layout->removeWidget(fieldLineEdit);
0231             lineEditList.removeOne(fieldLineEdit);
0232             lineEditList.insert(idx - 1, fieldLineEdit);
0233             layout->insertWidget(idx - 1, fieldLineEdit);
0234             return true; ///< return 'true' upon actual modification
0235         }
0236         return false; ///< return 'false' if nothing changed, i.e. FieldLineEdit is already at top
0237     }
0238 };
0239 
0240 FieldListEdit::FieldListEdit(KBibTeX::TypeFlag preferredTypeFlag, KBibTeX::TypeFlags typeFlags, QWidget *parent)
0241         : QWidget(parent), d(new FieldListEditProtected(preferredTypeFlag, typeFlags, this))
0242 {
0243     setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding);
0244     setMinimumSize(fontMetrics().averageCharWidth() * 40, fontMetrics().averageCharWidth() * 10);
0245     setAcceptDrops(true);
0246 }
0247 
0248 FieldListEdit::~FieldListEdit()
0249 {
0250     delete d;
0251 }
0252 
0253 bool FieldListEdit::reset(const Value &value)
0254 {
0255     int pos = 0;
0256     for (const auto &valueItem : value) {
0257         Value v;
0258         v.append(valueItem);
0259         // Re-use existing FieldInput widgets and only create new ones if necessary
0260         FieldLineEdit *fieldLineEdit = pos < d->lineEditList.count() ? d->lineEditList.at(pos) : addFieldLineEdit();
0261         fieldLineEdit->setFile(d->file);
0262         fieldLineEdit->reset(v);
0263         ++pos;
0264     }
0265     // Remove unused FieldInput widgets
0266     for (int i = d->lineEditList.count() - 1; i >= pos; --i) {
0267         FieldLineEdit *fieldLineEdit = d->lineEditList.last();
0268         d->layout->removeWidget(fieldLineEdit);
0269         d->lineEditList.removeLast();
0270         delete fieldLineEdit;
0271     }
0272 
0273     QSize size(d->container->width(), d->recommendedHeight());
0274     d->container->resize(size);
0275 
0276     return true;
0277 }
0278 
0279 bool FieldListEdit::apply(Value &value) const
0280 {
0281     value.clear();
0282 
0283     for (const auto *fieldLineEdit : const_cast<const QList<FieldLineEdit *> &>(d->lineEditList)) {
0284         Value v;
0285         fieldLineEdit->apply(v);
0286         for (const auto &valueItem : const_cast<const Value &>(v))
0287             value.append(valueItem);
0288     }
0289 
0290     return true;
0291 }
0292 
0293 bool FieldListEdit::validate(QWidget **widgetWithIssue, QString &message) const
0294 {
0295     for (const auto *fieldLineEdit : const_cast<const QList<FieldLineEdit *> &>(d->lineEditList)) {
0296         const bool v = fieldLineEdit->validate(widgetWithIssue, message);
0297         if (!v) return false;
0298     }
0299     return true;
0300 }
0301 
0302 void FieldListEdit::clear()
0303 {
0304     d->removeAllFieldLineEdits();
0305 }
0306 
0307 void FieldListEdit::setReadOnly(bool isReadOnly)
0308 {
0309     d->m_isReadOnly = isReadOnly;
0310     for (FieldLineEdit *fieldLineEdit : const_cast<const QList<FieldLineEdit *> &>(d->lineEditList))
0311         fieldLineEdit->setReadOnly(isReadOnly);
0312     d->addLineButton->setEnabled(!isReadOnly);
0313 }
0314 
0315 void FieldListEdit::setFile(const File *file)
0316 {
0317     d->file = file;
0318     for (FieldLineEdit *fieldLineEdit : const_cast<const QList<FieldLineEdit *> &>(d->lineEditList))
0319         fieldLineEdit->setFile(file);
0320 }
0321 
0322 void FieldListEdit::setElement(const Element *element)
0323 {
0324     m_element = element;
0325     for (FieldLineEdit *fieldLineEdit : const_cast<const QList<FieldLineEdit *> &>(d->lineEditList))
0326         fieldLineEdit->setElement(element);
0327 }
0328 
0329 void FieldListEdit::setFieldKey(const QString &fieldKey)
0330 {
0331     d->fieldKey = fieldKey;
0332     for (FieldLineEdit *fieldLineEdit : const_cast<const QList<FieldLineEdit *> &>(d->lineEditList))
0333         fieldLineEdit->setFieldKey(fieldKey);
0334 }
0335 
0336 void FieldListEdit::setCompletionItems(const QStringList &items)
0337 {
0338     d->completionItems = items;
0339     for (FieldLineEdit *fieldLineEdit : const_cast<const QList<FieldLineEdit *> &>(d->lineEditList))
0340         fieldLineEdit->setCompletionItems(items);
0341 }
0342 
0343 FieldLineEdit *FieldListEdit::addFieldLineEdit()
0344 {
0345     return d->addFieldLineEdit();
0346 }
0347 
0348 void FieldListEdit::addButton(QPushButton *button)
0349 {
0350     d->addButton(button);
0351 }
0352 
0353 void FieldListEdit::dragEnterEvent(QDragEnterEvent *event)
0354 {
0355     if (event->mimeData()->hasFormat(QStringLiteral("text/plain")) || event->mimeData()->hasFormat(QStringLiteral("text/x-bibtex")))
0356         event->acceptProposedAction();
0357 }
0358 
0359 void FieldListEdit::dropEvent(QDropEvent *event)
0360 {
0361     const QString clipboardText = QString::fromUtf8(event->mimeData()->data(QStringLiteral("text/plain")));
0362     event->acceptProposedAction();
0363     if (clipboardText.isEmpty()) return;
0364 
0365     bool success = false;
0366     if (!d->fieldKey.isEmpty() && clipboardText.startsWith(QStringLiteral("@"))) {
0367         FileImporterBibTeX importer(this);
0368         QScopedPointer<const File> file(importer.fromString(clipboardText));
0369         const QSharedPointer<Entry> entry = (!file.isNull() && file->count() == 1) ? file->first().dynamicCast<Entry>() : QSharedPointer<Entry>();
0370 
0371         if (!entry.isNull() && d->fieldKey == QStringLiteral("^external")) {
0372             /// handle "external" list differently
0373             const auto urlList = FileInfo::entryUrls(entry, QUrl(file->property(File::Url).toUrl()), FileInfo::TestExistence::No);
0374             Value v;
0375             v.reserve(urlList.size());
0376             for (const QUrl &url : urlList) {
0377                 v.append(QSharedPointer<VerbatimText>(new VerbatimText(url.url(QUrl::PreferLocalFile))));
0378             }
0379             reset(v);
0380             Q_EMIT modified();
0381             success = true;
0382         } else if (!entry.isNull() && entry->contains(d->fieldKey)) {
0383             /// case for "normal" lists like for authors, editors, ...
0384             reset(entry->value(d->fieldKey));
0385             Q_EMIT modified();
0386             success = true;
0387         }
0388     }
0389 
0390     if (!success) {
0391         // In case above cases were not met and thus 'success' is still false,
0392         // keep a single FieldLineEdit and use the clipboad text as its content
0393         FieldLineEdit *fle = d->lineEditList.count() > 0 ? d->lineEditList.first() : addFieldLineEdit();
0394         fle->setText(clipboardText);
0395 
0396         // Remove unused FieldInput widgets
0397         for (int i = d->lineEditList.count() - 1; i > 0; --i) {
0398             FieldLineEdit *fieldLineEdit = d->lineEditList.last();
0399             d->layout->removeWidget(fieldLineEdit);
0400             d->lineEditList.removeLast();
0401             delete fieldLineEdit;
0402         }
0403 
0404         Q_EMIT modified();
0405     }
0406 }
0407 
0408 void FieldListEdit::lineAdd(Value *value)
0409 {
0410     FieldLineEdit *le = addFieldLineEdit();
0411     le->setCompletionItems(d->completionItems);
0412     if (value != nullptr)
0413         le->reset(*value);
0414 }
0415 
0416 void FieldListEdit::lineAdd()
0417 {
0418     FieldLineEdit *newEdit = addFieldLineEdit();
0419     newEdit->setCompletionItems(d->completionItems);
0420     QSize size(d->container->width(), d->recommendedHeight());
0421     d->container->resize(size);
0422     newEdit->setFocus(Qt::ShortcutFocusReason);
0423 }
0424 
0425 PersonListEdit::PersonListEdit(KBibTeX::TypeFlag preferredTypeFlag, KBibTeX::TypeFlags typeFlags, QWidget *parent)
0426         : FieldListEdit(preferredTypeFlag, typeFlags, parent)
0427 {
0428     m_checkBoxOthers = new QCheckBox(i18n("... and others (et al.)"), this);
0429     connect(m_checkBoxOthers, &QCheckBox::toggled, this, &PersonListEdit::modified);
0430     QBoxLayout *boxLayout = static_cast<QBoxLayout *>(layout());
0431     boxLayout->addWidget(m_checkBoxOthers);
0432 
0433     m_buttonAddNamesFromClipboard = new QPushButton(QIcon::fromTheme(QStringLiteral("edit-paste")), i18n("Add from Clipboard"), this);
0434     m_buttonAddNamesFromClipboard->setToolTip(i18n("Add a list of names from clipboard"));
0435     addButton(m_buttonAddNamesFromClipboard);
0436 
0437     connect(m_buttonAddNamesFromClipboard, &QPushButton::clicked, this, &PersonListEdit::slotAddNamesFromClipboard);
0438 }
0439 
0440 bool PersonListEdit::reset(const Value &value)
0441 {
0442     Value internal = value;
0443 
0444     m_checkBoxOthers->setCheckState(Qt::Unchecked);
0445     QSharedPointer<PlainText> pt;
0446     if (!internal.isEmpty() && !(pt = internal.last().dynamicCast<PlainText>()).isNull()) {
0447         if (pt->text() == QStringLiteral("others")) {
0448             internal.erase(internal.end() - 1);
0449             m_checkBoxOthers->setCheckState(Qt::Checked);
0450         }
0451     }
0452 
0453     return FieldListEdit::reset(internal);
0454 }
0455 
0456 bool PersonListEdit::apply(Value &value) const
0457 {
0458     bool result = FieldListEdit::apply(value);
0459 
0460     if (result && m_checkBoxOthers->checkState() == Qt::Checked)
0461         value.append(QSharedPointer<PlainText>(new PlainText(QStringLiteral("others"))));
0462 
0463     return result;
0464 }
0465 
0466 void PersonListEdit::setReadOnly(bool isReadOnly)
0467 {
0468     FieldListEdit::setReadOnly(isReadOnly);
0469     m_checkBoxOthers->setEnabled(!isReadOnly);
0470     m_buttonAddNamesFromClipboard->setEnabled(!isReadOnly);
0471 }
0472 
0473 void PersonListEdit::slotAddNamesFromClipboard()
0474 {
0475     QClipboard *clipboard = QApplication::clipboard();
0476     QString text = clipboard->text(QClipboard::Clipboard);
0477     if (text.isEmpty())
0478         text = clipboard->text(QClipboard::Selection);
0479     if (!text.isEmpty()) {
0480         const QList<QSharedPointer<Person> > personList = FileImporterBibTeX::splitNames(text);
0481         for (const QSharedPointer<Person> &person : personList) {
0482             Value *value = new Value();
0483             value->append(person);
0484             lineAdd(value);
0485             delete value;
0486         }
0487         if (!personList.isEmpty())
0488             Q_EMIT modified();
0489     }
0490 }
0491 
0492 
0493 UrlListEdit::UrlListEdit(QWidget *parent)
0494         : FieldListEdit(KBibTeX::TypeFlag::Verbatim, KBibTeX::TypeFlag::Verbatim, parent)
0495 {
0496     m_buttonAddFile = new QPushButton(QIcon::fromTheme(QStringLiteral("list-add")), i18n("Add file..."), this);
0497     addButton(m_buttonAddFile);
0498     QMenu *menuAddFile = new QMenu(m_buttonAddFile);
0499     m_buttonAddFile->setMenu(menuAddFile);
0500     connect(m_buttonAddFile, &QPushButton::clicked, m_buttonAddFile, &QPushButton::showMenu);
0501 
0502     menuAddFile->addAction(QIcon::fromTheme(QStringLiteral("emblem-symbolic-link")), i18n("Add reference ..."), this, [this]() {
0503         slotAddReference();
0504     });
0505     menuAddFile->addAction(QIcon::fromTheme(QStringLiteral("emblem-symbolic-link")), i18n("Add reference from clipboard"), this, [this]() {
0506         slotAddReferenceFromClipboard();
0507     });
0508 }
0509 
0510 void UrlListEdit::slotAddReference()
0511 {
0512     QUrl bibtexUrl(d->file != nullptr ? d->file->property(File::Url, QVariant()).toUrl() : QUrl());
0513     if (bibtexUrl.isValid()) {
0514         const QFileInfo fi(bibtexUrl.path());
0515         bibtexUrl.setPath(fi.absolutePath());
0516     }
0517     const QUrl documentUrl = QFileDialog::getOpenFileUrl(this, i18n("File to Associate"), bibtexUrl);
0518     if (documentUrl.isValid())
0519         addReference(documentUrl);
0520 }
0521 
0522 void UrlListEdit::slotAddReferenceFromClipboard()
0523 {
0524     const QUrl url = QUrl::fromUserInput(QApplication::clipboard()->text());
0525     if (url.isValid())
0526         addReference(url);
0527 }
0528 
0529 void UrlListEdit::addReference(const QUrl &url) {
0530     const Entry *entry = dynamic_cast<const Entry *>(m_element);
0531     if (entry != nullptr) {
0532         QSharedPointer<Entry> fakeTempEntry(new Entry(entry->type(), entry->id()));
0533         const QString visibleFilename = AssociatedFilesUI::associateUrl(url, fakeTempEntry, d->file, false, this);
0534         if (!visibleFilename.isEmpty()) {
0535             Value *value = new Value();
0536             value->append(QSharedPointer<VerbatimText>(new VerbatimText(visibleFilename)));
0537             lineAdd(value);
0538             delete value;
0539             Q_EMIT modified();
0540         }
0541     }
0542 }
0543 
0544 void UrlListEdit::downloadAndSaveLocally(const QUrl &url)
0545 {
0546     /// Only proceed if Url is valid and points to a remote location
0547     if (url.isValid() && !url.isLocalFile()) {
0548         /// Get filename from url (without any path/directory part)
0549         QString filename = url.fileName();
0550         /// Build QFileInfo from current BibTeX file if available
0551         QFileInfo bibFileinfo = d->file != nullptr ? QFileInfo(d->file->property(File::Url).toUrl().path()) : QFileInfo();
0552         /// Build proposal to a local filename for remote file
0553         filename = bibFileinfo.isFile() ? bibFileinfo.absolutePath() + QDir::separator() + filename : filename;
0554         /// Ask user for actual local filename to save remote file to
0555         filename = QFileDialog::getSaveFileName(this, i18n("Save file locally"), filename, QStringLiteral("application/pdf application/postscript image/vnd.djvu"));
0556         /// Check if user entered a valid filename ...
0557         if (!filename.isEmpty()) {
0558             /// Ask user if reference to local file should be
0559             /// relative or absolute in relation to the BibTeX file
0560             const QString absoluteFilename = filename;
0561             QString visibleFilename = filename;
0562             if (bibFileinfo.isFile())
0563                 visibleFilename = askRelativeOrStaticFilename(this, absoluteFilename, d->file->property(File::Url).toUrl());
0564 
0565             /// Download remote file and save it locally
0566             setEnabled(false);
0567             setCursor(Qt::WaitCursor);
0568             KIO::CopyJob *job = KIO::copy(url, QUrl::fromLocalFile(absoluteFilename), KIO::Overwrite);
0569             job->setProperty("visibleFilename", QVariant::fromValue<QString>(visibleFilename));
0570             connect(job, &KJob::result, this, &UrlListEdit::downloadFinished);
0571         }
0572     }
0573 }
0574 
0575 void UrlListEdit::downloadFinished(KJob *j) {
0576     KIO::CopyJob *job = static_cast<KIO::CopyJob *>(j);
0577     if (job->error() == 0) {
0578         /// Download succeeded, add reference to local file to this BibTeX entry
0579         Value *value = new Value();
0580         value->append(QSharedPointer<VerbatimText>(new VerbatimText(job->property("visibleFilename").toString())));
0581         lineAdd(value);
0582         delete value;
0583     } else {
0584         qCWarning(LOG_KBIBTEX_GUI) << "Downloading" << (*job->srcUrls().constBegin()).toDisplayString() << "failed with error" << job->error() << job->errorString();
0585     }
0586     setEnabled(true);
0587     unsetCursor();
0588 }
0589 
0590 void UrlListEdit::textChanged(QPushButton *buttonSaveLocally, FieldLineEdit *fieldLineEdit)
0591 {
0592     if (buttonSaveLocally == nullptr || fieldLineEdit == nullptr) return; ///< should never happen!
0593 
0594     /// Create URL from new text to make some tests on it
0595     /// Only remote URLs are of interest, therefore no tests
0596     /// on local file or relative paths
0597     const QString newText = fieldLineEdit->text();
0598     const QString lowerText = newText.toLower();
0599 
0600     /// Enable button only if Url is valid and points to a remote
0601     /// DjVu, PDF, or PostScript file
0602     // TODO more file types?
0603     const bool canBeSaved = lowerText.contains(QStringLiteral("://")) && (lowerText.endsWith(QStringLiteral(".djvu")) || lowerText.endsWith(QStringLiteral(".pdf")) || lowerText.endsWith(QStringLiteral(".ps")));
0604     buttonSaveLocally->setEnabled(canBeSaved);
0605     buttonSaveLocally->setToolTip(canBeSaved ? i18n("Save file '%1' locally", newText) : QString());
0606 }
0607 
0608 QString UrlListEdit::askRelativeOrStaticFilename(QWidget *parent, const QString &absoluteFilename, const QUrl &baseUrl)
0609 {
0610     QFileInfo baseUrlInfo = baseUrl.isValid() ? QFileInfo(baseUrl.path()) : QFileInfo();
0611     QFileInfo filenameInfo(absoluteFilename);
0612     if (baseUrl.isValid() && (filenameInfo.absolutePath() == baseUrlInfo.absolutePath() || filenameInfo.absolutePath().startsWith(baseUrlInfo.absolutePath() + QDir::separator()))) {
0613         // TODO cover level-up cases like "../../test.pdf"
0614         const QString relativePath = filenameInfo.absolutePath().mid(baseUrlInfo.absolutePath().length() + 1);
0615         const QString relativeFilename = relativePath + (relativePath.isEmpty() ? QString() : QString(QDir::separator())) + filenameInfo.fileName();
0616         if (
0617 #if KWIDGETSADDONS_VERSION < QT_VERSION_CHECK(5, 100, 0)
0618             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
0619 #else // >= 5.100.0
0620             KMessageBox::questionTwoActions(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::PrimaryAction
0621 #endif // KWIDGETSADDONS_VERSION < QT_VERSION_CHECK(5, 100, 0)
0622         )
0623             return relativeFilename;
0624     }
0625     return absoluteFilename;
0626 }
0627 
0628 FieldLineEdit *UrlListEdit::addFieldLineEdit()
0629 {
0630     /// Call original implementation to get an instance of a FieldLineEdit
0631     FieldLineEdit *fieldLineEdit = FieldListEdit::addFieldLineEdit();
0632 
0633     /// Create a new "save locally" button
0634     QPushButton *buttonSaveLocally = new QPushButton(QIcon::fromTheme(QStringLiteral("document-save")), QString(), fieldLineEdit);
0635     buttonSaveLocally->setToolTip(i18n("Save file locally"));
0636     buttonSaveLocally->setEnabled(false);
0637     /// Append button to new FieldLineEdit
0638     fieldLineEdit->appendWidget(buttonSaveLocally);
0639     /// Connect signals to react on button events
0640     /// or changes in the FieldLineEdit's text
0641     connect(buttonSaveLocally, &QPushButton::clicked, this, [this, fieldLineEdit]() {
0642         downloadAndSaveLocally(QUrl::fromUserInput(fieldLineEdit->text()));
0643     });
0644     connect(fieldLineEdit, &FieldLineEdit::textChanged, this, [this, buttonSaveLocally, fieldLineEdit]() {
0645         textChanged(buttonSaveLocally, fieldLineEdit);
0646     });
0647 
0648     return fieldLineEdit;
0649 }
0650 
0651 void UrlListEdit::setReadOnly(bool isReadOnly)
0652 {
0653     FieldListEdit::setReadOnly(isReadOnly);
0654     m_buttonAddFile->setEnabled(!isReadOnly);
0655 }
0656 
0657 
0658 const QString KeywordListEdit::keyGlobalKeywordList = QStringLiteral("globalKeywordList");
0659 
0660 KeywordListEdit::KeywordListEdit(QWidget *parent)
0661         : FieldListEdit(KBibTeX::TypeFlag::Keyword, KBibTeX::TypeFlag::Keyword | KBibTeX::TypeFlag::Source, parent), m_config(KSharedConfig::openConfig(QStringLiteral("kbibtexrc"))), m_configGroupName(QStringLiteral("Global Keywords"))
0662 {
0663     m_buttonAddKeywordsFromList = new QPushButton(QIcon::fromTheme(QStringLiteral("list-add")), i18n("Add Keywords from List"), this);
0664     m_buttonAddKeywordsFromList->setToolTip(i18n("Add keywords as selected from a pre-defined list of keywords"));
0665     addButton(m_buttonAddKeywordsFromList);
0666     connect(m_buttonAddKeywordsFromList, &QPushButton::clicked, this, &KeywordListEdit::slotAddKeywordsFromList);
0667     m_buttonAddKeywordsFromClipboard = new QPushButton(QIcon::fromTheme(QStringLiteral("edit-paste")), i18n("Add Keywords from Clipboard"), this);
0668     m_buttonAddKeywordsFromClipboard->setToolTip(i18n("Add a punctuation-separated list of keywords from clipboard"));
0669     addButton(m_buttonAddKeywordsFromClipboard);
0670     connect(m_buttonAddKeywordsFromClipboard, &QPushButton::clicked, this, &KeywordListEdit::slotAddKeywordsFromClipboard);
0671 }
0672 
0673 void KeywordListEdit::slotAddKeywordsFromList()
0674 {
0675     /// fetch stored, global keywords
0676     KConfigGroup configGroup(m_config, m_configGroupName);
0677     QStringList keywords = configGroup.readEntry(KeywordListEdit::keyGlobalKeywordList, QStringList());
0678 
0679     /// use a map for case-insensitive sorting of strings
0680     /// (recommended by Qt's documentation)
0681     QMap<QString, QString> forCaseInsensitiveSorting;
0682     /// insert all stored, global keywords
0683     for (const QString &keyword : const_cast<const QStringList &>(keywords))
0684         forCaseInsensitiveSorting.insert(keyword.toLower(), keyword);
0685     /// insert all unique keywords used in this file
0686     for (const QString &keyword : const_cast<const QSet<QString> &>(m_keywordsFromFile))
0687         forCaseInsensitiveSorting.insert(keyword.toLower(), keyword);
0688     /// re-create string list from map's values
0689     keywords = forCaseInsensitiveSorting.values();
0690 
0691     // FIXME QInputDialog does not have a 'getItemList'
0692     /*
0693     bool ok = false;
0694     const QStringList newKeywordList = KInputDialog::getItemList(i18n("Add Keywords"), i18n("Select keywords to add:"), keywords, QStringList(), true, &ok, this);
0695     if (ok) {
0696         for(const QString &newKeywordText : newKeywordList) {
0697             Value *value = new Value();
0698             value->append(QSharedPointer<Keyword>(new Keyword(newKeywordText)));
0699             lineAdd(value);
0700             delete value;
0701         }
0702         if (!newKeywordList.isEmpty())
0703             Q_EMIT modified();
0704     }
0705     */
0706 }
0707 
0708 void KeywordListEdit::slotAddKeywordsFromClipboard()
0709 {
0710     QClipboard *clipboard = QApplication::clipboard();
0711     QString text = clipboard->text(QClipboard::Clipboard);
0712     if (text.isEmpty()) ///< use "mouse" clipboard as fallback
0713         text = clipboard->text(QClipboard::Selection);
0714     if (!text.isEmpty()) {
0715         const QList<QSharedPointer<Keyword> > keywords = FileImporterBibTeX::splitKeywords(text);
0716         for (const auto &keyword : keywords) {
0717             Value *value = new Value();
0718             value->append(keyword);
0719             lineAdd(value);
0720             delete value;
0721         }
0722         if (!keywords.isEmpty())
0723             Q_EMIT modified();
0724     }
0725 }
0726 
0727 void KeywordListEdit::setReadOnly(bool isReadOnly)
0728 {
0729     FieldListEdit::setReadOnly(isReadOnly);
0730     m_buttonAddKeywordsFromList->setEnabled(!isReadOnly);
0731     m_buttonAddKeywordsFromClipboard->setEnabled(!isReadOnly);
0732 }
0733 
0734 void KeywordListEdit::setFile(const File *file)
0735 {
0736     if (file == nullptr)
0737         m_keywordsFromFile.clear();
0738     else
0739         m_keywordsFromFile = file->uniqueEntryValuesSet(Entry::ftKeywords);
0740 
0741     FieldListEdit::setFile(file);
0742 }
0743 
0744 void KeywordListEdit::setCompletionItems(const QStringList &items)
0745 {
0746     /// fetch stored, global keywords
0747     KConfigGroup configGroup(m_config, m_configGroupName);
0748     QStringList keywords = configGroup.readEntry(KeywordListEdit::keyGlobalKeywordList, QStringList());
0749 
0750     /// use a map for case-insensitive sorting of strings
0751     /// (recommended by Qt's documentation)
0752     QMap<QString, QString> forCaseInsensitiveSorting;
0753     /// insert all stored, global keywords
0754     for (const QString &keyword : const_cast<const QStringList &>(keywords))
0755         forCaseInsensitiveSorting.insert(keyword.toLower(), keyword);
0756     /// insert all unique keywords used in this file
0757     for (const QString &keyword : const_cast<const QStringList &>(items))
0758         forCaseInsensitiveSorting.insert(keyword.toLower(), keyword);
0759     /// re-create string list from map's values
0760     keywords = forCaseInsensitiveSorting.values();
0761 
0762     FieldListEdit::setCompletionItems(keywords);
0763 }