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 }