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