1 /***************************************************************************
2  *   Copyright (C) 2004-2018 by Thomas Fischer <fischer@unix-ag.uni-kl.de> *
3  *                                                                         *
4  *   This program is free software; you can redistribute it and/or modify  *
5  *   it under the terms of the GNU General Public License as published by  *
6  *   the Free Software Foundation; either version 2 of the License, or     *
7  *   (at your option) any later version.                                   *
8  *                                                                         *
9  *   This program is distributed in the hope that it will be useful,       *
10  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
11  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
12  *   GNU General Public License for more details.                          *
13  *                                                                         *
14  *   You should have received a copy of the GNU General Public License     *
15  *   along with this program; if not, see <https://www.gnu.org/licenses/>. *
16  ***************************************************************************/
17 
18 #include "fieldlistedit.h"
19 
20 #include <typeinfo>
21 
22 #include <QApplication>
23 #include <QClipboard>
24 #include <QScrollArea>
25 #include <QLayout>
26 #include <QSignalMapper>
27 #include <QCheckBox>
28 #include <QDragEnterEvent>
29 #include <QDropEvent>
30 #include <QMimeData>
31 #include <QUrl>
32 #include <QTimer>
33 #include <QAction>
34 #include <QPushButton>
35 #include <QFontDatabase>
36 #include <QFileDialog>
37 #include <QMenu>
38 
39 #include <KMessageBox>
40 #include <KLocalizedString>
41 #include <KIO/CopyJob>
42 #include <KSharedConfig>
43 #include <KConfigGroup>
44 
45 #include "fileinfo.h"
46 #include "file.h"
47 #include "entry.h"
48 #include "fileimporterbibtex.h"
49 #include "fileexporterbibtex.h"
50 #include "fieldlineedit.h"
51 #include "associatedfiles.h"
52 #include "associatedfilesui.h"
53 #include "logging_gui.h"
54 
55 class FieldListEdit::FieldListEditProtected
56 {
57 private:
58     FieldListEdit *p;
59     const int innerSpacing;
60     QSignalMapper *smRemove, *smGoUp, *smGoDown;
61     QVBoxLayout *layout;
62     KBibTeX::TypeFlag preferredTypeFlag;
63     KBibTeX::TypeFlags typeFlags;
64 
65 public:
66     QList<FieldLineEdit *> lineEditList;
67     QWidget *pushButtonContainer;
68     QBoxLayout *pushButtonContainerLayout;
69     QPushButton *addLineButton;
70     const File *file;
71     QString fieldKey;
72     QWidget *container;
73     QScrollArea *scrollArea;
74     bool m_isReadOnly;
75     QStringList completionItems;
76 
FieldListEditProtected(KBibTeX::TypeFlag ptf,KBibTeX::TypeFlags tf,FieldListEdit * parent)77     FieldListEditProtected(KBibTeX::TypeFlag ptf, KBibTeX::TypeFlags tf, FieldListEdit *parent)
78             : p(parent), innerSpacing(4), preferredTypeFlag(ptf), typeFlags(tf), file(nullptr), m_isReadOnly(false) {
79         smRemove = new QSignalMapper(parent);
80         smGoUp = new QSignalMapper(parent);
81         smGoDown = new QSignalMapper(parent);
82         setupGUI();
83     }
84 
85     FieldListEditProtected(const FieldListEditProtected &other) = delete;
86     FieldListEditProtected &operator= (const FieldListEditProtected &other) = delete;
87 
setupGUI()88     void setupGUI() {
89         QBoxLayout *outerLayout = new QVBoxLayout(p);
90         outerLayout->setMargin(0);
91         outerLayout->setSpacing(0);
92         scrollArea = new QScrollArea(p);
93         outerLayout->addWidget(scrollArea);
94 
95         container = new QWidget(scrollArea->viewport());
96         container->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Minimum);
97         scrollArea->setWidget(container);
98         layout = new QVBoxLayout(container);
99         layout->setMargin(0);
100         layout->setSpacing(innerSpacing);
101 
102         pushButtonContainer = new QWidget(container);
103         pushButtonContainerLayout = new QHBoxLayout(pushButtonContainer);
104         pushButtonContainerLayout->setMargin(0);
105         layout->addWidget(pushButtonContainer);
106 
107         addLineButton = new QPushButton(QIcon::fromTheme(QStringLiteral("list-add")), i18n("Add"), pushButtonContainer);
108         addLineButton->setObjectName(QStringLiteral("addButton"));
109         connect(addLineButton, &QPushButton::clicked, p, static_cast<void(FieldListEdit::*)()>(&FieldListEdit::lineAdd));
110         connect(addLineButton, &QPushButton::clicked, p, &FieldListEdit::modified);
111         pushButtonContainerLayout->addWidget(addLineButton);
112 
113         layout->addStretch(100);
114 
115         connect(smRemove, static_cast<void(QSignalMapper::*)(QWidget *)>(&QSignalMapper::mapped), p, &FieldListEdit::lineRemove);
116         connect(smGoDown, static_cast<void(QSignalMapper::*)(QWidget *)>(&QSignalMapper::mapped), p, &FieldListEdit::lineGoDown);
117         connect(smGoUp, static_cast<void(QSignalMapper::*)(QWidget *)>(&QSignalMapper::mapped), p, &FieldListEdit::lineGoUp);
118 
119         scrollArea->setBackgroundRole(QPalette::Base);
120         scrollArea->ensureWidgetVisible(container);
121         scrollArea->setWidgetResizable(true);
122     }
123 
addButton(QPushButton * button)124     void addButton(QPushButton *button) {
125         button->setParent(pushButtonContainer);
126         pushButtonContainerLayout->addWidget(button);
127     }
128 
recommendedHeight()129     int recommendedHeight() {
130         int heightHint = 0;
131 
132         for (const auto *fieldLineEdit : const_cast<const QList<FieldLineEdit *> &>(lineEditList))
133             heightHint += fieldLineEdit->sizeHint().height();
134 
135         heightHint += lineEditList.count() * innerSpacing;
136         heightHint += addLineButton->sizeHint().height();
137 
138         return heightHint;
139     }
140 
addFieldLineEdit()141     FieldLineEdit *addFieldLineEdit() {
142         FieldLineEdit *le = new FieldLineEdit(preferredTypeFlag, typeFlags, false, container);
143         le->setFile(file);
144         le->setAcceptDrops(false);
145         le->setReadOnly(m_isReadOnly);
146         le->setInnerWidgetsTransparency(true);
147         layout->insertWidget(layout->count() - 2, le);
148         lineEditList.append(le);
149 
150         QPushButton *remove = new QPushButton(QIcon::fromTheme(QStringLiteral("list-remove")), QString(), le);
151         remove->setToolTip(i18n("Remove value"));
152         le->appendWidget(remove);
153         connect(remove, &QPushButton::clicked, smRemove, static_cast<void(QSignalMapper::*)()>(&QSignalMapper::map));
154         smRemove->setMapping(remove, le);
155 
156         QPushButton *goDown = new QPushButton(QIcon::fromTheme(QStringLiteral("go-down")), QString(), le);
157         goDown->setToolTip(i18n("Move value down"));
158         le->appendWidget(goDown);
159         connect(goDown, &QPushButton::clicked, smGoDown, static_cast<void(QSignalMapper::*)()>(&QSignalMapper::map));
160         smGoDown->setMapping(goDown, le);
161 
162         QPushButton *goUp = new QPushButton(QIcon::fromTheme(QStringLiteral("go-up")), QString(), le);
163         goUp->setToolTip(i18n("Move value up"));
164         le->appendWidget(goUp);
165         connect(goUp, &QPushButton::clicked, smGoUp, static_cast<void(QSignalMapper::*)()>(&QSignalMapper::map));
166         smGoUp->setMapping(goUp, le);
167 
168         connect(le, &FieldLineEdit::modified, p, &FieldListEdit::modified);
169 
170         return le;
171     }
172 
removeAllFieldLineEdits()173     void removeAllFieldLineEdits() {
174         while (!lineEditList.isEmpty()) {
175             FieldLineEdit *fieldLineEdit = *lineEditList.begin();
176             layout->removeWidget(fieldLineEdit);
177             lineEditList.removeFirst();
178             delete fieldLineEdit;
179         }
180 
181         /// This fixes a layout problem where the container element
182         /// does not shrink correctly once the line edits have been
183         /// removed
184         QSize pSize = container->size();
185         pSize.setHeight(addLineButton->height());
186         container->resize(pSize);
187     }
188 
removeFieldLineEdit(FieldLineEdit * fieldLineEdit)189     void removeFieldLineEdit(FieldLineEdit *fieldLineEdit) {
190         lineEditList.removeOne(fieldLineEdit);
191         layout->removeWidget(fieldLineEdit);
192         delete fieldLineEdit;
193     }
194 
goDownFieldLineEdit(FieldLineEdit * fieldLineEdit)195     void goDownFieldLineEdit(FieldLineEdit *fieldLineEdit) {
196         int idx = lineEditList.indexOf(fieldLineEdit);
197         if (idx < lineEditList.count() - 1) {
198             layout->removeWidget(fieldLineEdit);
199             lineEditList.removeOne(fieldLineEdit);
200             lineEditList.insert(idx + 1, fieldLineEdit);
201             layout->insertWidget(idx + 1, fieldLineEdit);
202         }
203     }
204 
goUpFieldLineEdit(FieldLineEdit * fieldLineEdit)205     void goUpFieldLineEdit(FieldLineEdit *fieldLineEdit) {
206         int idx = lineEditList.indexOf(fieldLineEdit);
207         if (idx > 0) {
208             layout->removeWidget(fieldLineEdit);
209             lineEditList.removeOne(fieldLineEdit);
210             lineEditList.insert(idx - 1, fieldLineEdit);
211             layout->insertWidget(idx - 1, fieldLineEdit);
212         }
213     }
214 };
215 
FieldListEdit(KBibTeX::TypeFlag preferredTypeFlag,KBibTeX::TypeFlags typeFlags,QWidget * parent)216 FieldListEdit::FieldListEdit(KBibTeX::TypeFlag preferredTypeFlag, KBibTeX::TypeFlags typeFlags, QWidget *parent)
217         : QWidget(parent), d(new FieldListEditProtected(preferredTypeFlag, typeFlags, this))
218 {
219     setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding);
220     setMinimumSize(fontMetrics().averageCharWidth() * 30, fontMetrics().averageCharWidth() * 10);
221     setAcceptDrops(true);
222 }
223 
~FieldListEdit()224 FieldListEdit::~FieldListEdit()
225 {
226     delete d;
227 }
228 
reset(const Value & value)229 bool FieldListEdit::reset(const Value &value)
230 {
231     d->removeAllFieldLineEdits();
232     for (const auto &valueItem : value) {
233         Value v;
234         v.append(valueItem);
235         FieldLineEdit *fieldLineEdit = addFieldLineEdit();
236         fieldLineEdit->setFile(d->file);
237         fieldLineEdit->reset(v);
238     }
239     QSize size(d->container->width(), d->recommendedHeight());
240     d->container->resize(size);
241 
242     return true;
243 }
244 
apply(Value & value) const245 bool FieldListEdit::apply(Value &value) const
246 {
247     value.clear();
248 
249     for (const auto *fieldLineEdit : const_cast<const QList<FieldLineEdit *> &>(d->lineEditList)) {
250         Value v;
251         fieldLineEdit->apply(v);
252         for (const auto &valueItem : const_cast<const Value &>(v))
253             value.append(valueItem);
254     }
255 
256     return true;
257 }
258 
validate(QWidget ** widgetWithIssue,QString & message) const259 bool FieldListEdit::validate(QWidget **widgetWithIssue, QString &message) const
260 {
261     for (const auto *fieldLineEdit : const_cast<const QList<FieldLineEdit *> &>(d->lineEditList)) {
262         const bool v = fieldLineEdit->validate(widgetWithIssue, message);
263         if (!v) return false;
264     }
265     return true;
266 }
267 
clear()268 void FieldListEdit::clear()
269 {
270     d->removeAllFieldLineEdits();
271 }
272 
setReadOnly(bool isReadOnly)273 void FieldListEdit::setReadOnly(bool isReadOnly)
274 {
275     d->m_isReadOnly = isReadOnly;
276     for (FieldLineEdit *fieldLineEdit : const_cast<const QList<FieldLineEdit *> &>(d->lineEditList))
277         fieldLineEdit->setReadOnly(isReadOnly);
278     d->addLineButton->setEnabled(!isReadOnly);
279 }
280 
setFile(const File * file)281 void FieldListEdit::setFile(const File *file)
282 {
283     d->file = file;
284     for (FieldLineEdit *fieldLineEdit : const_cast<const QList<FieldLineEdit *> &>(d->lineEditList))
285         fieldLineEdit->setFile(file);
286 }
287 
setElement(const Element * element)288 void FieldListEdit::setElement(const Element *element)
289 {
290     m_element = element;
291     for (FieldLineEdit *fieldLineEdit : const_cast<const QList<FieldLineEdit *> &>(d->lineEditList))
292         fieldLineEdit->setElement(element);
293 }
294 
setFieldKey(const QString & fieldKey)295 void FieldListEdit::setFieldKey(const QString &fieldKey)
296 {
297     d->fieldKey = fieldKey;
298     for (FieldLineEdit *fieldLineEdit : const_cast<const QList<FieldLineEdit *> &>(d->lineEditList))
299         fieldLineEdit->setFieldKey(fieldKey);
300 }
301 
setCompletionItems(const QStringList & items)302 void FieldListEdit::setCompletionItems(const QStringList &items)
303 {
304     d->completionItems = items;
305     for (FieldLineEdit *fieldLineEdit : const_cast<const QList<FieldLineEdit *> &>(d->lineEditList))
306         fieldLineEdit->setCompletionItems(items);
307 }
308 
addFieldLineEdit()309 FieldLineEdit *FieldListEdit::addFieldLineEdit()
310 {
311     return d->addFieldLineEdit();
312 }
313 
addButton(QPushButton * button)314 void FieldListEdit::addButton(QPushButton *button)
315 {
316     d->addButton(button);
317 }
318 
dragEnterEvent(QDragEnterEvent * event)319 void FieldListEdit::dragEnterEvent(QDragEnterEvent *event)
320 {
321     if (event->mimeData()->hasFormat(QStringLiteral("text/plain")) || event->mimeData()->hasFormat(QStringLiteral("text/x-bibtex")))
322         event->acceptProposedAction();
323 }
324 
dropEvent(QDropEvent * event)325 void FieldListEdit::dropEvent(QDropEvent *event)
326 {
327     const QString clipboardText = event->mimeData()->text();
328     if (clipboardText.isEmpty()) return;
329 
330     const File *file = nullptr;
331     if (!d->fieldKey.isEmpty() && clipboardText.startsWith(QStringLiteral("@"))) {
332         FileImporterBibTeX importer(this);
333         file = importer.fromString(clipboardText);
334         const QSharedPointer<Entry> entry = (file != nullptr && file->count() == 1) ? file->first().dynamicCast<Entry>() : QSharedPointer<Entry>();
335 
336         if (file != nullptr && !entry.isNull() && d->fieldKey == QStringLiteral("^external")) {
337             /// handle "external" list differently
338             const auto urlList = FileInfo::entryUrls(entry, QUrl(file->property(File::Url).toUrl()), FileInfo::TestExistenceNo);
339             Value v;
340             v.reserve(urlList.size());
341             for (const QUrl &url : urlList) {
342                 v.append(QSharedPointer<VerbatimText>(new VerbatimText(url.url(QUrl::PreferLocalFile))));
343             }
344             reset(v);
345             emit modified();
346             return;
347         } else if (!entry.isNull() && entry->contains(d->fieldKey)) {
348             /// case for "normal" lists like for authors, editors, ...
349             reset(entry->value(d->fieldKey));
350             emit modified();
351             return;
352         }
353     }
354 
355     if (file == nullptr || file->count() == 0) {
356         /// fall-back case: single field line edit with text
357         d->removeAllFieldLineEdits();
358         FieldLineEdit *fle = addFieldLineEdit();
359         fle->setText(clipboardText);
360         emit modified();
361     }
362 }
363 
lineAdd(Value * value)364 void FieldListEdit::lineAdd(Value *value)
365 {
366     FieldLineEdit *le = addFieldLineEdit();
367     le->setCompletionItems(d->completionItems);
368     if (value != nullptr)
369         le->reset(*value);
370 }
371 
lineAdd()372 void FieldListEdit::lineAdd()
373 {
374     FieldLineEdit *newEdit = addFieldLineEdit();
375     newEdit->setCompletionItems(d->completionItems);
376     QSize size(d->container->width(), d->recommendedHeight());
377     d->container->resize(size);
378     newEdit->setFocus(Qt::ShortcutFocusReason);
379 }
380 
lineRemove(QWidget * widget)381 void FieldListEdit::lineRemove(QWidget *widget)
382 {
383     FieldLineEdit *fieldLineEdit = static_cast<FieldLineEdit *>(widget);
384     d->removeFieldLineEdit(fieldLineEdit);
385     QSize size(d->container->width(), d->recommendedHeight());
386     d->container->resize(size);
387     emit modified();
388 }
389 
lineGoDown(QWidget * widget)390 void FieldListEdit::lineGoDown(QWidget *widget)
391 {
392     FieldLineEdit *fieldLineEdit = static_cast<FieldLineEdit *>(widget);
393     d->goDownFieldLineEdit(fieldLineEdit);
394     emit modified();
395 }
396 
lineGoUp(QWidget * widget)397 void FieldListEdit::lineGoUp(QWidget *widget)
398 {
399     FieldLineEdit *fieldLineEdit = static_cast<FieldLineEdit *>(widget);
400     d->goUpFieldLineEdit(fieldLineEdit);
401     emit modified();
402 }
403 
PersonListEdit(KBibTeX::TypeFlag preferredTypeFlag,KBibTeX::TypeFlags typeFlags,QWidget * parent)404 PersonListEdit::PersonListEdit(KBibTeX::TypeFlag preferredTypeFlag, KBibTeX::TypeFlags typeFlags, QWidget *parent)
405         : FieldListEdit(preferredTypeFlag, typeFlags, parent)
406 {
407     m_checkBoxOthers = new QCheckBox(i18n("... and others (et al.)"), this);
408     connect(m_checkBoxOthers, &QCheckBox::toggled, this, &PersonListEdit::modified);
409     QBoxLayout *boxLayout = static_cast<QBoxLayout *>(layout());
410     boxLayout->addWidget(m_checkBoxOthers);
411 
412     m_buttonAddNamesFromClipboard = new QPushButton(QIcon::fromTheme(QStringLiteral("edit-paste")), i18n("Add from Clipboard"), this);
413     m_buttonAddNamesFromClipboard->setToolTip(i18n("Add a list of names from clipboard"));
414     addButton(m_buttonAddNamesFromClipboard);
415 
416     connect(m_buttonAddNamesFromClipboard, &QPushButton::clicked, this, &PersonListEdit::slotAddNamesFromClipboard);
417 }
418 
reset(const Value & value)419 bool PersonListEdit::reset(const Value &value)
420 {
421     Value internal = value;
422 
423     m_checkBoxOthers->setCheckState(Qt::Unchecked);
424     QSharedPointer<PlainText> pt;
425     if (!internal.isEmpty() && !(pt = internal.last().dynamicCast<PlainText>()).isNull()) {
426         if (pt->text() == QStringLiteral("others")) {
427             internal.erase(internal.end() - 1);
428             m_checkBoxOthers->setCheckState(Qt::Checked);
429         }
430     }
431 
432     return FieldListEdit::reset(internal);
433 }
434 
apply(Value & value) const435 bool PersonListEdit::apply(Value &value) const
436 {
437     bool result = FieldListEdit::apply(value);
438 
439     if (result && m_checkBoxOthers->checkState() == Qt::Checked)
440         value.append(QSharedPointer<PlainText>(new PlainText(QStringLiteral("others"))));
441 
442     return result;
443 }
444 
setReadOnly(bool isReadOnly)445 void PersonListEdit::setReadOnly(bool isReadOnly)
446 {
447     FieldListEdit::setReadOnly(isReadOnly);
448     m_checkBoxOthers->setEnabled(!isReadOnly);
449     m_buttonAddNamesFromClipboard->setEnabled(!isReadOnly);
450 }
451 
slotAddNamesFromClipboard()452 void PersonListEdit::slotAddNamesFromClipboard()
453 {
454     QClipboard *clipboard = QApplication::clipboard();
455     QString text = clipboard->text(QClipboard::Clipboard);
456     if (text.isEmpty())
457         text = clipboard->text(QClipboard::Selection);
458     if (!text.isEmpty()) {
459         const QList<QSharedPointer<Person> > personList = FileImporterBibTeX::splitNames(text);
460         for (const QSharedPointer<Person> &person : personList) {
461             Value *value = new Value();
462             value->append(person);
463             lineAdd(value);
464             delete value;
465         }
466         if (!personList.isEmpty())
467             emit modified();
468     }
469 }
470 
471 
UrlListEdit(QWidget * parent)472 UrlListEdit::UrlListEdit(QWidget *parent)
473         : FieldListEdit(KBibTeX::tfVerbatim, KBibTeX::tfVerbatim, parent)
474 {
475     m_signalMapperSaveLocallyButtonClicked = new QSignalMapper(this);
476     connect(m_signalMapperSaveLocallyButtonClicked, static_cast<void(QSignalMapper::*)(QWidget *)>(&QSignalMapper::mapped), this, &UrlListEdit::slotSaveLocally);
477     m_signalMapperFieldLineEditTextChanged = new QSignalMapper(this);
478     connect(m_signalMapperFieldLineEditTextChanged, static_cast<void(QSignalMapper::*)(QWidget *)>(&QSignalMapper::mapped), this, &UrlListEdit::textChanged);
479 
480     m_buttonAddFile = new QPushButton(QIcon::fromTheme(QStringLiteral("list-add")), i18n("Add file..."), this);
481     addButton(m_buttonAddFile);
482     QMenu *menuAddFile = new QMenu(m_buttonAddFile);
483     m_buttonAddFile->setMenu(menuAddFile);
484     connect(m_buttonAddFile, &QPushButton::clicked, m_buttonAddFile, &QPushButton::showMenu);
485 
486     menuAddFile->addAction(QIcon::fromTheme(QStringLiteral("emblem-symbolic-link")), i18n("Add reference ..."), this, SLOT(slotAddReference()));
487     menuAddFile->addAction(QIcon::fromTheme(QStringLiteral("emblem-symbolic-link")), i18n("Add reference from clipboard"), this, SLOT(slotAddReferenceFromClipboard()));
488 }
489 
slotAddReference()490 void UrlListEdit::slotAddReference()
491 {
492     QUrl bibtexUrl(d->file != nullptr ? d->file->property(File::Url, QVariant()).toUrl() : QUrl());
493     if (!bibtexUrl.isEmpty()) {
494         const QFileInfo fi(bibtexUrl.path());
495         bibtexUrl.setPath(fi.absolutePath());
496     }
497     const QUrl documentUrl = QFileDialog::getOpenFileUrl(this, i18n("File to Associate"), bibtexUrl);
498     if (!documentUrl.isEmpty())
499         addReference(documentUrl);
500 }
501 
slotAddReferenceFromClipboard()502 void UrlListEdit::slotAddReferenceFromClipboard()
503 {
504     const QUrl url = QUrl::fromUserInput(QApplication::clipboard()->text());
505     if (!url.isEmpty())
506         addReference(url);
507 }
508 
addReference(const QUrl & url)509 void UrlListEdit::addReference(const QUrl &url) {
510     const Entry *entry = dynamic_cast<const Entry *>(m_element);
511     const QString entryId = entry != nullptr ? entry->id() : QString();
512     const QString visibleFilename = AssociatedFilesUI::associateUrl(url, entryId, d->file, this);
513     if (!visibleFilename.isEmpty()) {
514         Value *value = new Value();
515         value->append(QSharedPointer<VerbatimText>(new VerbatimText(visibleFilename)));
516         lineAdd(value);
517         delete value;
518         emit modified();
519     }
520 }
521 
slotSaveLocally(QWidget * widget)522 void UrlListEdit::slotSaveLocally(QWidget *widget)
523 {
524     /// Determine FieldLineEdit widget
525     FieldLineEdit *fieldLineEdit = qobject_cast<FieldLineEdit *>(widget);
526     /// Build Url from line edit's content
527     const QUrl url = QUrl::fromUserInput(fieldLineEdit->text());
528 
529     /// Only proceed if Url is valid and points to a remote location
530     if (url.isValid() && !urlIsLocal(url)) {
531         /// Get filename from url (without any path/directory part)
532         QString filename = url.fileName();
533         /// Build QFileInfo from current BibTeX file if available
534         QFileInfo bibFileinfo = d->file != nullptr ? QFileInfo(d->file->property(File::Url).toUrl().path()) : QFileInfo();
535         /// Build proposal to a local filename for remote file
536         filename = bibFileinfo.isFile() ? bibFileinfo.absolutePath() + QDir::separator() + filename : filename;
537         /// Ask user for actual local filename to save remote file to
538         filename = QFileDialog::getSaveFileName(this, i18n("Save file locally"), filename, QStringLiteral("application/pdf application/postscript image/vnd.djvu"));
539         /// Check if user entered a valid filename ...
540         if (!filename.isEmpty()) {
541             /// Ask user if reference to local file should be
542             /// relative or absolute in relation to the BibTeX file
543             const QString absoluteFilename = filename;
544             QString visibleFilename = filename;
545             if (bibFileinfo.isFile())
546                 visibleFilename = askRelativeOrStaticFilename(this, absoluteFilename, d->file->property(File::Url).toUrl());
547 
548             /// Download remote file and save it locally
549             setEnabled(false);
550             setCursor(Qt::WaitCursor);
551             KIO::CopyJob *job = KIO::copy(url, QUrl::fromLocalFile(absoluteFilename), KIO::Overwrite);
552             job->setProperty("visibleFilename", QVariant::fromValue<QString>(visibleFilename));
553             connect(job, &KJob::result, this, &UrlListEdit::downloadFinished);
554         }
555     }
556 }
557 
downloadFinished(KJob * j)558 void UrlListEdit::downloadFinished(KJob *j) {
559     KIO::CopyJob *job = static_cast<KIO::CopyJob *>(j);
560     if (job->error() == 0) {
561         /// Download succeeded, add reference to local file to this BibTeX entry
562         Value *value = new Value();
563         value->append(QSharedPointer<VerbatimText>(new VerbatimText(job->property("visibleFilename").toString())));
564         lineAdd(value);
565         delete value;
566     } else {
567         qCWarning(LOG_KBIBTEX_GUI) << "Downloading" << (*job->srcUrls().constBegin()).toDisplayString() << "failed with error" << job->error() << job->errorString();
568     }
569     setEnabled(true);
570     unsetCursor();
571 }
572 
textChanged(QWidget * widget)573 void UrlListEdit::textChanged(QWidget *widget)
574 {
575     /// Determine associated QPushButton "Save locally"
576     QPushButton *buttonSaveLocally = qobject_cast<QPushButton *>(widget);
577     if (buttonSaveLocally == nullptr) return; ///< should never happen!
578 
579     /// Assume a FieldLineEdit was the sender of this signal
580     FieldLineEdit *fieldLineEdit = qobject_cast<FieldLineEdit *>(m_signalMapperFieldLineEditTextChanged->mapping(widget));
581     if (fieldLineEdit == nullptr) return; ///< should never happen!
582 
583     /// Create URL from new text to make some tests on it
584     /// Only remote URLs are of interest, therefore no tests
585     /// on local file or relative paths
586     const QString newText = fieldLineEdit->text();
587     const QString lowerText = newText.toLower();
588 
589     /// Enable button only if Url is valid and points to a remote
590     /// DjVu, PDF, or PostScript file
591     // TODO more file types?
592     const bool canBeSaved = lowerText.contains(QStringLiteral("://")) && (lowerText.endsWith(QStringLiteral(".djvu")) || lowerText.endsWith(QStringLiteral(".pdf")) || lowerText.endsWith(QStringLiteral(".ps")));
593     buttonSaveLocally->setEnabled(canBeSaved);
594     buttonSaveLocally->setToolTip(canBeSaved ? i18n("Save file '%1' locally", newText) : QString());
595 }
596 
askRelativeOrStaticFilename(QWidget * parent,const QString & absoluteFilename,const QUrl & baseUrl)597 QString UrlListEdit::askRelativeOrStaticFilename(QWidget *parent, const QString &absoluteFilename, const QUrl &baseUrl)
598 {
599     QFileInfo baseUrlInfo = baseUrl.isEmpty() ? QFileInfo() : QFileInfo(baseUrl.path());
600     QFileInfo filenameInfo(absoluteFilename);
601     if (!baseUrl.isEmpty() && (filenameInfo.absolutePath() == baseUrlInfo.absolutePath() || filenameInfo.absolutePath().startsWith(baseUrlInfo.absolutePath() + QDir::separator()))) {
602         // TODO cover level-up cases like "../../test.pdf"
603         const QString relativePath = filenameInfo.absolutePath().mid(baseUrlInfo.absolutePath().length() + 1);
604         const QString relativeFilename = relativePath + (relativePath.isEmpty() ? QString() : QString(QDir::separator())) + filenameInfo.fileName();
605         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)
606             return relativeFilename;
607     }
608     return absoluteFilename;
609 }
610 
urlIsLocal(const QUrl & url)611 bool UrlListEdit::urlIsLocal(const QUrl &url)
612 {
613     // FIXME same function as in AssociateFiles; move to common code base?
614     const QString scheme = url.scheme();
615     /// Test various schemes such as "http", "https", "ftp", ...
616     return !scheme.startsWith(QStringLiteral("http")) && !scheme.startsWith(QStringLiteral("ftp")) && !scheme.startsWith(QStringLiteral("sftp")) && !scheme.startsWith(QStringLiteral("fish")) && !scheme.startsWith(QStringLiteral("webdav")) && scheme != QStringLiteral("smb");
617 }
618 
addFieldLineEdit()619 FieldLineEdit *UrlListEdit::addFieldLineEdit()
620 {
621     /// Call original implementation to get an instance of a FieldLineEdit
622     FieldLineEdit *fieldLineEdit = FieldListEdit::addFieldLineEdit();
623 
624     /// Create a new "save locally" button
625     QPushButton *buttonSaveLocally = new QPushButton(QIcon::fromTheme(QStringLiteral("document-save")), QString(), fieldLineEdit);
626     buttonSaveLocally->setToolTip(i18n("Save file locally"));
627     buttonSaveLocally->setEnabled(false);
628     /// Append button to new FieldLineEdit
629     fieldLineEdit->appendWidget(buttonSaveLocally);
630     /// Connect signals to react on button events
631     /// or changes in the FieldLineEdit's text
632     m_signalMapperSaveLocallyButtonClicked->setMapping(buttonSaveLocally, fieldLineEdit);
633     m_signalMapperFieldLineEditTextChanged->setMapping(fieldLineEdit, buttonSaveLocally);
634     connect(buttonSaveLocally, &QPushButton::clicked, m_signalMapperSaveLocallyButtonClicked, static_cast<void(QSignalMapper::*)()>(&QSignalMapper::map));
635     connect(fieldLineEdit, &FieldLineEdit::textChanged, m_signalMapperFieldLineEditTextChanged, static_cast<void(QSignalMapper::*)()>(&QSignalMapper::map));
636 
637     return fieldLineEdit;
638 }
639 
setReadOnly(bool isReadOnly)640 void UrlListEdit::setReadOnly(bool isReadOnly)
641 {
642     FieldListEdit::setReadOnly(isReadOnly);
643     m_buttonAddFile->setEnabled(!isReadOnly);
644 }
645 
646 
647 const QString KeywordListEdit::keyGlobalKeywordList = QStringLiteral("globalKeywordList");
648 
KeywordListEdit(QWidget * parent)649 KeywordListEdit::KeywordListEdit(QWidget *parent)
650         : FieldListEdit(KBibTeX::tfKeyword, KBibTeX::tfKeyword | KBibTeX::tfSource, parent), m_config(KSharedConfig::openConfig(QStringLiteral("kbibtexrc"))), m_configGroupName(QStringLiteral("Global Keywords"))
651 {
652     m_buttonAddKeywordsFromList = new QPushButton(QIcon::fromTheme(QStringLiteral("list-add")), i18n("Add Keywords from List"), this);
653     m_buttonAddKeywordsFromList->setToolTip(i18n("Add keywords as selected from a pre-defined list of keywords"));
654     addButton(m_buttonAddKeywordsFromList);
655     connect(m_buttonAddKeywordsFromList, &QPushButton::clicked, this, &KeywordListEdit::slotAddKeywordsFromList);
656     m_buttonAddKeywordsFromClipboard = new QPushButton(QIcon::fromTheme(QStringLiteral("edit-paste")), i18n("Add Keywords from Clipboard"), this);
657     m_buttonAddKeywordsFromClipboard->setToolTip(i18n("Add a punctuation-separated list of keywords from clipboard"));
658     addButton(m_buttonAddKeywordsFromClipboard);
659     connect(m_buttonAddKeywordsFromClipboard, &QPushButton::clicked, this, &KeywordListEdit::slotAddKeywordsFromClipboard);
660 }
661 
slotAddKeywordsFromList()662 void KeywordListEdit::slotAddKeywordsFromList()
663 {
664     /// fetch stored, global keywords
665     KConfigGroup configGroup(m_config, m_configGroupName);
666     QStringList keywords = configGroup.readEntry(KeywordListEdit::keyGlobalKeywordList, QStringList());
667 
668     /// use a map for case-insensitive sorting of strings
669     /// (recommended by Qt's documentation)
670     QMap<QString, QString> forCaseInsensitiveSorting;
671     /// insert all stored, global keywords
672     for (const QString &keyword : const_cast<const QStringList &>(keywords))
673         forCaseInsensitiveSorting.insert(keyword.toLower(), keyword);
674     /// insert all unique keywords used in this file
675     for (const QString &keyword : const_cast<const QSet<QString> &>(m_keywordsFromFile))
676         forCaseInsensitiveSorting.insert(keyword.toLower(), keyword);
677     /// re-create string list from map's values
678     keywords = forCaseInsensitiveSorting.values();
679 
680     // FIXME QInputDialog does not have a 'getItemList'
681     /*
682     bool ok = false;
683     const QStringList newKeywordList = KInputDialog::getItemList(i18n("Add Keywords"), i18n("Select keywords to add:"), keywords, QStringList(), true, &ok, this);
684     if (ok) {
685         for(const QString &newKeywordText : newKeywordList) {
686             Value *value = new Value();
687             value->append(QSharedPointer<Keyword>(new Keyword(newKeywordText)));
688             lineAdd(value);
689             delete value;
690         }
691         if (!newKeywordList.isEmpty())
692             emit modified();
693     }
694     */
695 }
696 
slotAddKeywordsFromClipboard()697 void KeywordListEdit::slotAddKeywordsFromClipboard()
698 {
699     QClipboard *clipboard = QApplication::clipboard();
700     QString text = clipboard->text(QClipboard::Clipboard);
701     if (text.isEmpty()) ///< use "mouse" clipboard as fallback
702         text = clipboard->text(QClipboard::Selection);
703     if (!text.isEmpty()) {
704         const QList<QSharedPointer<Keyword> > keywords = FileImporterBibTeX::splitKeywords(text);
705         for (const auto &keyword : keywords) {
706             Value *value = new Value();
707             value->append(keyword);
708             lineAdd(value);
709             delete value;
710         }
711         if (!keywords.isEmpty())
712             emit modified();
713     }
714 }
715 
setReadOnly(bool isReadOnly)716 void KeywordListEdit::setReadOnly(bool isReadOnly)
717 {
718     FieldListEdit::setReadOnly(isReadOnly);
719     m_buttonAddKeywordsFromList->setEnabled(!isReadOnly);
720     m_buttonAddKeywordsFromClipboard->setEnabled(!isReadOnly);
721 }
722 
setFile(const File * file)723 void KeywordListEdit::setFile(const File *file)
724 {
725     if (file == nullptr)
726         m_keywordsFromFile.clear();
727     else
728         m_keywordsFromFile = file->uniqueEntryValuesSet(Entry::ftKeywords);
729 
730     FieldListEdit::setFile(file);
731 }
732 
setCompletionItems(const QStringList & items)733 void KeywordListEdit::setCompletionItems(const QStringList &items)
734 {
735     /// fetch stored, global keywords
736     KConfigGroup configGroup(m_config, m_configGroupName);
737     QStringList keywords = configGroup.readEntry(KeywordListEdit::keyGlobalKeywordList, QStringList());
738 
739     /// use a map for case-insensitive sorting of strings
740     /// (recommended by Qt's documentation)
741     QMap<QString, QString> forCaseInsensitiveSorting;
742     /// insert all stored, global keywords
743     for (const QString &keyword : const_cast<const QStringList &>(keywords))
744         forCaseInsensitiveSorting.insert(keyword.toLower(), keyword);
745     /// insert all unique keywords used in this file
746     for (const QString &keyword : const_cast<const QStringList &>(items))
747         forCaseInsensitiveSorting.insert(keyword.toLower(), keyword);
748     /// re-create string list from map's values
749     keywords = forCaseInsensitiveSorting.values();
750 
751     FieldListEdit::setCompletionItems(keywords);
752 }
753