diff --git a/src/gui/element/elementwidgets.cpp b/src/gui/element/elementwidgets.cpp index 39f16669..043a54d1 100644 --- a/src/gui/element/elementwidgets.cpp +++ b/src/gui/element/elementwidgets.cpp @@ -1,1377 +1,1376 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, see . * ***************************************************************************/ #include "elementwidgets.h" #include #include #include #include #include #include #include #include #include -#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "field/fieldinput.h" #include "field/fieldlineedit.h" #include "delayedexecutiontimer.h" #include "logging_gui.h" static const unsigned int interColumnSpace = 16; static const char *PropertyIdSuggestion = "PropertyIdSuggestion"; ElementWidget::ElementWidget(QWidget *parent) : QWidget(parent), isReadOnly(false), m_file(nullptr), m_isModified(false) { /// nothing } bool ElementWidget::isModified() const { return m_isModified; } void ElementWidget::setModified(bool newIsModified) { m_isModified = newIsModified; emit modified(newIsModified); } void ElementWidget::gotModified() { setModified(true); } EntryConfiguredWidget::EntryConfiguredWidget(const QSharedPointer &entryTabLayout, QWidget *parent) : ElementWidget(parent), fieldInputCount(entryTabLayout->singleFieldLayouts.size()), numCols(entryTabLayout->columns), etl(entryTabLayout) { gridLayout = new QGridLayout(this); /// Initialize list of field input widgets plus labels listOfLabeledFieldInput = new LabeledFieldInput*[fieldInputCount]; createGUI(); } EntryConfiguredWidget::~EntryConfiguredWidget() { delete[] listOfLabeledFieldInput; } bool EntryConfiguredWidget::apply(QSharedPointer element) const { if (isReadOnly) return false; /// never save data if in read-only mode QSharedPointer entry = element.dynamicCast(); if (entry.isNull()) return false; for (QMap::ConstIterator it = bibtexKeyToWidget.constBegin(); it != bibtexKeyToWidget.constEnd(); ++it) { Value value; it.value()->apply(value); entry->remove(it.key()); if (!value.isEmpty()) entry->insert(it.key(), value); } return true; } bool EntryConfiguredWidget::reset(QSharedPointer element) { QSharedPointer entry = element.dynamicCast(); if (entry.isNull()) return false; /// clear all widgets for (QMap::Iterator it = bibtexKeyToWidget.begin(); it != bibtexKeyToWidget.end(); ++it) { it.value()->setFile(m_file); it.value()->clear(); } for (Entry::ConstIterator it = entry->constBegin(); it != entry->constEnd(); ++it) { const QString key = it.key().toLower(); if (bibtexKeyToWidget.contains(key)) { FieldInput *fieldInput = bibtexKeyToWidget[key]; fieldInput->setElement(element.data()); fieldInput->reset(it.value()); } } return true; } bool EntryConfiguredWidget::validate(QWidget **widgetWithIssue, QString &message) const { for (int i = fieldInputCount - 1; i >= 0; --i) { const bool v = listOfLabeledFieldInput[i]->fieldInput->validate(widgetWithIssue, message); if (!v) return false; } return true; } void EntryConfiguredWidget::showReqOptWidgets(bool forceVisible, const QString &entryType) { layoutGUI(forceVisible, entryType); } void EntryConfiguredWidget::setReadOnly(bool isReadOnly) { ElementWidget::setReadOnly(isReadOnly); for (QMap::Iterator it = bibtexKeyToWidget.begin(); it != bibtexKeyToWidget.end(); ++it) it.value()->setReadOnly(isReadOnly); } QString EntryConfiguredWidget::label() { return etl->uiCaption; } QIcon EntryConfiguredWidget::icon() { return QIcon::fromTheme(etl->iconName); } void EntryConfiguredWidget::setFile(const File *file) { for (QMap::Iterator it = bibtexKeyToWidget.begin(); it != bibtexKeyToWidget.end(); ++it) { it.value()->setFile(file); if (file != nullptr) { /// list of unique values for same field QStringList list = file->uniqueEntryValuesList(it.key()); /// for crossref fields, add all entries' ids if (it.key().toLower() == Entry::ftCrossRef) list.append(file->allKeys(File::etEntry)); /// add macro keys list.append(file->allKeys(File::etMacro)); it.value()->setCompletionItems(list); } } ElementWidget::setFile(file); } bool EntryConfiguredWidget::canEdit(const Element *element) { return Entry::isEntry(*element); } void EntryConfiguredWidget::createGUI() { int i = 0; for (const SingleFieldLayout &sfl : const_cast &>(etl->singleFieldLayouts)) { LabeledFieldInput *labeledFieldInput = new LabeledFieldInput; /// create an editing widget for this field const FieldDescription &fd = BibTeXFields::instance().find(sfl.bibtexLabel); labeledFieldInput->fieldInput = new FieldInput(sfl.fieldInputLayout, fd.preferredTypeFlag, fd.typeFlags, this); labeledFieldInput->fieldInput->setFieldKey(sfl.bibtexLabel); bibtexKeyToWidget.insert(sfl.bibtexLabel, labeledFieldInput->fieldInput); connect(labeledFieldInput->fieldInput, &FieldInput::modified, this, &EntryConfiguredWidget::gotModified); /// memorize if field input should grow vertically (e.g. is a list) labeledFieldInput->isVerticallyMinimumExpaning = sfl.fieldInputLayout == KBibTeX::MultiLine || sfl.fieldInputLayout == KBibTeX::List || sfl.fieldInputLayout == KBibTeX::PersonList || sfl.fieldInputLayout == KBibTeX::KeywordList; /// create a label next to the editing widget labeledFieldInput->label = new QLabel(QString(QStringLiteral("%1:")).arg(sfl.uiLabel), this); labeledFieldInput->label->setBuddy(labeledFieldInput->fieldInput->buddy()); /// align label's text vertically to match field input const Qt::Alignment horizontalAlignment = static_cast(labeledFieldInput->label->style()->styleHint(QStyle::SH_FormLayoutLabelAlignment)) & Qt::AlignHorizontal_Mask; labeledFieldInput->label->setAlignment(horizontalAlignment | (labeledFieldInput->isVerticallyMinimumExpaning ? Qt::AlignTop : Qt::AlignVCenter)); listOfLabeledFieldInput[i] = labeledFieldInput; ++i; } layoutGUI(true); } void EntryConfiguredWidget::layoutGUI(bool forceVisible, const QString &entryType) { QStringList visibleItems; if (!forceVisible && !entryType.isEmpty()) { const QString entryTypeLc = entryType.toLower(); for (const auto &ed : BibTeXEntries::instance()) { if (entryTypeLc == ed.upperCamelCase.toLower() || entryTypeLc == ed.upperCamelCaseAlt.toLower()) { /// this ugly conversion is necessary because we have a "^" (xor) and "|" (and/or) /// syntax to differentiate required items (not used yet, but will be used /// later if missing required items are marked). QString visible = ed.requiredItems.join(QStringLiteral(",")); visible += QLatin1Char(',') + ed.optionalItems.join(QStringLiteral(",")); visible = visible.replace(QLatin1Char('|'), QLatin1Char(',')).replace(QLatin1Char('^'), QLatin1Char(',')); visibleItems = visible.split(QStringLiteral(",")); break; } } } else if (!forceVisible) { // TODO is this an error condition? } /// variables to keep track which and how many field inputs will be visible int countVisible = 0; QScopedArrayPointer visible(new bool[fieldInputCount]); /// ... and if any field input is vertically expaning /// (e.g. a list, important for layout) bool anyoneVerticallyExpanding = false; for (int i = fieldInputCount - 1; i >= 0; --i) { listOfLabeledFieldInput[i]->label->setVisible(false); listOfLabeledFieldInput[i]->fieldInput->setVisible(false); gridLayout->removeWidget(listOfLabeledFieldInput[i]->label); gridLayout->removeWidget(listOfLabeledFieldInput[i]->fieldInput); const QString key = bibtexKeyToWidget.key(listOfLabeledFieldInput[i]->fieldInput).toLower(); const FieldDescription &fd = BibTeXFields::instance().find(key); Value value; listOfLabeledFieldInput[i]->fieldInput->apply(value); /// Hide non-required and non-optional type-dependent fields, /// except if the field has content visible[i] = forceVisible || fd.typeIndependent || !value.isEmpty() || visibleItems.contains(key); if (visible[i]) { ++countVisible; anyoneVerticallyExpanding |= listOfLabeledFieldInput[i]->isVerticallyMinimumExpaning; } } int numRows = countVisible / numCols; if (countVisible % numCols > 0) ++numRows; gridLayout->setRowStretch(numRows, anyoneVerticallyExpanding ? 0 : 1000); int col = 0, row = 0; for (int i = 0; i < fieldInputCount; ++i) if (visible[i]) { /// add label and field input to new position in grid layout gridLayout->addWidget(listOfLabeledFieldInput[i]->label, row, col * 3); gridLayout->addWidget(listOfLabeledFieldInput[i]->fieldInput, row, col * 3 + 1); /// set row stretch gridLayout->setRowStretch(row, listOfLabeledFieldInput[i]->isVerticallyMinimumExpaning ? 1000 : 0); /// set column stretch and spacing gridLayout->setColumnStretch(col * 3, 1); gridLayout->setColumnStretch(col * 3 + 1, 1000); if (col > 0) gridLayout->setColumnMinimumWidth(col * 3 - 1, interColumnSpace); /// count rows and columns correctly ++row; if (row >= numRows) { row = 0; ++col; } /// finally, set label and field input visible again listOfLabeledFieldInput[i]->label->setVisible(true); listOfLabeledFieldInput[i]->fieldInput->setVisible(true); // FIXME expensive! } if (countVisible > 0) { /// fix row stretch for (int i = numRows + 1; i < 100; ++i) gridLayout->setRowStretch(i, 0); /// hide unused columns for (int i = (col + (row == 0 ? 0 : 1)) * 3 - 1; i < 100; ++i) { gridLayout->setColumnMinimumWidth(i, 0); gridLayout->setColumnStretch(i, 0); } } } ReferenceWidget::ReferenceWidget(QWidget *parent) : ElementWidget(parent), m_applyElement(nullptr), m_entryIdManuallySet(false), m_element(QSharedPointer()) { createGUI(); } bool ReferenceWidget::apply(QSharedPointer element) const { if (isReadOnly) return false; /// never save data if in read-only mode bool result = false; QSharedPointer entry = element.dynamicCast(); if (!entry.isNull()) { entry->setType(computeType()); entry->setId(entryId->text()); result = true; } else { QSharedPointer macro = element.dynamicCast(); if (!macro.isNull()) { macro->setKey(entryId->text()); result = true; } } return result; } bool ReferenceWidget::reset(QSharedPointer element) { /// if signals are not deactivated, the "modified" signal would be emitted when /// resetting the widgets' values disconnect(entryType->lineEdit(), &QLineEdit::textChanged, this, &ReferenceWidget::gotModified); disconnect(entryId, &QLineEdit::textEdited, this, &ReferenceWidget::entryIdManuallyChanged); bool result = false; QSharedPointer entry = element.dynamicCast(); if (!entry.isNull()) { entryType->setEnabled(!isReadOnly); buttonSuggestId->setEnabled(!isReadOnly); QString type = BibTeXEntries::instance().format(entry->type(), KBibTeX::cUpperCamelCase); int index = entryType->findData(type); if (index == -1) { const QString typeLower(type.toLower()); for (const auto &ed : BibTeXEntries::instance()) if (typeLower == ed.upperCamelCaseAlt.toLower()) { index = entryType->findData(ed.upperCamelCase); break; } } entryType->setCurrentIndex(index); if (index == -1) { /// A customized value not known to KBibTeX entryType->lineEdit()->setText(type); } entryId->setText(entry->id()); /// New entries have no values. Use this fact /// to recognize new entries, for which it is /// allowed to automatic set their ids /// if a default id suggestion had been specified. m_entryIdManuallySet = entry->count() > 0; result = true; } else { entryType->setEnabled(false); buttonSuggestId->setEnabled(false); QSharedPointer macro = element.dynamicCast(); if (!macro.isNull()) { entryType->lineEdit()->setText(i18n("Macro")); entryId->setText(macro->key()); result = true; } } connect(entryId, &QLineEdit::textEdited, this, &ReferenceWidget::entryIdManuallyChanged); connect(entryType->lineEdit(), &QLineEdit::textChanged, this, &ReferenceWidget::gotModified); return result; } bool ReferenceWidget::validate(QWidget **widgetWithIssue, QString &message) const { message.clear(); static const QRegularExpression validTypeRegExp(QStringLiteral("^[a-z]+$"), QRegularExpression::CaseInsensitiveOption); const QString type = computeType(); const QRegularExpressionMatch validTypeMatch = validTypeRegExp.match(type); if (!validTypeMatch.hasMatch() || validTypeMatch.capturedLength() != type.length()) { if (widgetWithIssue != nullptr) *widgetWithIssue = entryType; message = i18n("Element type '%1' is invalid.", type); return false; } static const QRegularExpression validIdRegExp(QStringLiteral("^[a-z0-9][a-z0-9_:.+/$\\\"&-]*$"), QRegularExpression::CaseInsensitiveOption); const QString id = entryId->text(); const QRegularExpressionMatch validIdMatch = validIdRegExp.match(id); if (!validIdMatch.hasMatch() || validIdMatch.capturedLength() != id.length()) { if (widgetWithIssue != nullptr) *widgetWithIssue = entryId; message = i18n("Id '%1' is invalid", id); return false; } return true; } void ReferenceWidget::setReadOnly(bool isReadOnly) { ElementWidget::setReadOnly(isReadOnly); entryId->setReadOnly(isReadOnly); entryType->setEnabled(!isReadOnly); } QString ReferenceWidget::label() { return QString(); } QIcon ReferenceWidget::icon() { return QIcon(); } bool ReferenceWidget::canEdit(const Element *element) { return Entry::isEntry(*element) || Macro::isMacro(*element); } void ReferenceWidget::setOriginalElement(const QSharedPointer &orig) { m_element = orig; } QString ReferenceWidget::currentId() const { return entryId->text(); } void ReferenceWidget::setCurrentId(const QString &newId) { entryId->setText(newId); } void ReferenceWidget::createGUI() { QHBoxLayout *layout = new QHBoxLayout(this); entryType = new QComboBox(this); entryType->setEditable(true); entryType->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); QLabel *label = new QLabel(i18n("Type:"), this); label->setBuddy(entryType); layout->addWidget(label); layout->addWidget(entryType); layout->addSpacing(interColumnSpace); entryId = new QLineEdit(this); entryId->setClearButtonEnabled(true); label = new QLabel(i18n("Id:"), this); label->setBuddy(entryId); layout->addWidget(label); layout->addWidget(entryId); for (const auto &ed : BibTeXEntries::instance()) entryType->addItem(ed.label, ed.upperCamelCase); /// Sort the combo box locale-aware. Thus we need a SortFilterProxyModel QSortFilterProxyModel *proxy = new QSortFilterProxyModel(entryType); proxy->setSortLocaleAware(true); proxy->setSourceModel(entryType->model()); entryType->model()->setParent(proxy); entryType->setModel(proxy); entryType->model()->sort(0); /// Button with a menu listing a set of preconfigured id suggestions buttonSuggestId = new QPushButton(QIcon::fromTheme(QStringLiteral("view-filter")), QString(), this); buttonSuggestId->setToolTip(i18n("Select a suggested id for this entry")); layout->addWidget(buttonSuggestId); QMenu *suggestionsMenu = new QMenu(buttonSuggestId); buttonSuggestId->setMenu(suggestionsMenu); connect(entryType->lineEdit(), &QLineEdit::textChanged, this, &ReferenceWidget::gotModified); connect(entryId, &QLineEdit::textEdited, this, &ReferenceWidget::entryIdManuallyChanged); connect(entryType->lineEdit(), &QLineEdit::textChanged, this, &ReferenceWidget::entryTypeChanged); connect(suggestionsMenu, &QMenu::aboutToShow, this, &ReferenceWidget::prepareSuggestionsMenu); } void ReferenceWidget::prepareSuggestionsMenu() { /// Collect information on the current entry as it is edited QSharedPointer guiDataEntry(new Entry()); m_applyElement->apply(guiDataEntry); QSharedPointer crossrefResolvedEntry(guiDataEntry->resolveCrossref(m_file)); static const IdSuggestions *idSuggestions = new IdSuggestions(); QMenu *suggestionsMenu = buttonSuggestId->menu(); suggestionsMenu->clear(); /// Keep track of shown suggestions to avoid duplicates QSet knownIdSuggestion; const QString defaultSuggestion = idSuggestions->defaultFormatId(*crossrefResolvedEntry.data()); const auto formatIdList = idSuggestions->formatIdList(*crossrefResolvedEntry.data()); for (const QString &suggestionBase : formatIdList) { bool isDefault = suggestionBase == defaultSuggestion; QString suggestion = suggestionBase; /// Test for duplicate ids, use fallback ids with numeric suffix if (m_file != nullptr && m_file->containsKey(suggestion)) { int suffix = 2; while (m_file->containsKey(suggestion = suggestionBase + QChar('_') + QString::number(suffix))) ++suffix; } /// Keep track of shown suggestions to avoid duplicates if (knownIdSuggestion.contains(suggestion)) continue; else knownIdSuggestion.insert(suggestion); /// Create action for suggestion, use icon depending if default or not QAction *suggestionAction = new QAction(suggestion, suggestionsMenu); suggestionAction->setIcon(QIcon::fromTheme(isDefault ? QStringLiteral("favorites") : QStringLiteral("view-filter"))); /// Mesh action into GUI suggestionsMenu->addAction(suggestionAction); connect(suggestionAction, &QAction::triggered, this, &ReferenceWidget::insertSuggestionFromAction); /// Remember suggestion string for time when action gets triggered suggestionAction->setProperty(PropertyIdSuggestion, suggestion); } } void ReferenceWidget::insertSuggestionFromAction() { QAction *action = qobject_cast(sender()); if (action != nullptr) { const QString suggestion = action->property(PropertyIdSuggestion).toString(); entryId->setText(suggestion); } } void ReferenceWidget::entryIdManuallyChanged() { m_entryIdManuallySet = true; gotModified(); } void ReferenceWidget::setEntryIdByDefault() { if (isReadOnly) { /// Never set the suggestion automatically if in read-only mode return; } if (m_entryIdManuallySet) { /// If user changed entry id manually, /// do not overwrite it by a default value return; } static const IdSuggestions *idSuggestions = new IdSuggestions(); /// If there is a default suggestion format set ... if (idSuggestions->hasDefaultFormat()) { /// Collect information on the current entry as it is edited QSharedPointer guiDataEntry(new Entry()); m_applyElement->apply(guiDataEntry); QSharedPointer crossrefResolvedEntry(guiDataEntry->resolveCrossref(m_file)); /// Determine default suggestion based on current data const QString defaultSuggestion = idSuggestions->defaultFormatId(*crossrefResolvedEntry.data()); if (!defaultSuggestion.isEmpty()) { disconnect(entryId, &QLineEdit::textEdited, this, &ReferenceWidget::entryIdManuallyChanged); /// Apply default suggestion to widget entryId->setText(defaultSuggestion); connect(entryId, &QLineEdit::textEdited, this, &ReferenceWidget::entryIdManuallyChanged); } } } QString ReferenceWidget::computeType() const { if (entryType->currentIndex() < 0 || entryType->lineEdit()->isModified()) return BibTeXEntries::instance().format(entryType->lineEdit()->text(), KBibTeX::cUpperCamelCase); else return entryType->itemData(entryType->currentIndex()).toString(); } FilesWidget::FilesWidget(QWidget *parent) : ElementWidget(parent) { QVBoxLayout *layout = new QVBoxLayout(this); fileList = new FieldInput(KBibTeX::UrlList, KBibTeX::tfVerbatim /* eventually ignored, see constructor of UrlListEdit */, KBibTeX::tfVerbatim /* eventually ignored, see constructor of UrlListEdit */, this); fileList->setFieldKey(QStringLiteral("^external")); layout->addWidget(fileList); connect(fileList, &FieldInput::modified, this, &FilesWidget::gotModified); } bool FilesWidget::apply(QSharedPointer element) const { if (isReadOnly) return false; /// never save data if in read-only mode QSharedPointer entry = element.dynamicCast(); if (entry.isNull()) return false; for (const QString &keyStem : keyStart) for (int i = 1; i < 32; ++i) { /// FIXME replace number by constant const QString key = i > 1 ? keyStem + QString::number(i) : keyStem; entry->remove(key); } Value combinedValue; fileList->apply(combinedValue); Value urlValue, doiValue, localFileValue; urlValue.reserve(combinedValue.size()); doiValue.reserve(combinedValue.size()); localFileValue.reserve(combinedValue.size()); for (const auto &valueItem : const_cast(combinedValue)) { const QSharedPointer verbatimText = valueItem.dynamicCast(); if (!verbatimText.isNull()) { const QString text = verbatimText->text(); QRegularExpressionMatch match; if ((match = KBibTeX::urlRegExp.match(text)).hasMatch()) { /// add full URL VerbatimText *newVT = new VerbatimText(match.captured(0)); /// test for duplicates if (urlValue.contains(*newVT)) delete newVT; else urlValue.append(QSharedPointer(newVT)); } else if ((match = KBibTeX::doiRegExp.match(text)).hasMatch()) { /// add DOI VerbatimText *newVT = new VerbatimText(match.captured(0)); /// test for duplicates if (doiValue.contains(*newVT)) delete newVT; else doiValue.append(QSharedPointer(newVT)); } else { /// add anything else (e.g. local file) VerbatimText *newVT = new VerbatimText(*verbatimText); /// test for duplicates if (localFileValue.contains(*newVT)) delete newVT; else localFileValue.append(QSharedPointer(newVT)); } } } if (urlValue.isEmpty()) entry->remove(Entry::ftUrl); else entry->insert(Entry::ftUrl, urlValue); if (localFileValue.isEmpty()) { entry->remove(Entry::ftFile); entry->remove(Entry::ftLocalFile); } else if (Preferences::instance().bibliographySystem() == Preferences::BibLaTeX) { entry->remove(Entry::ftLocalFile); entry->insert(Entry::ftFile, localFileValue); } else if (Preferences::instance().bibliographySystem() == Preferences::BibTeX) { entry->remove(Entry::ftFile); entry->insert(Entry::ftLocalFile, localFileValue); } if (doiValue.isEmpty()) entry->remove(Entry::ftDOI); else entry->insert(Entry::ftDOI, doiValue); return true; } bool FilesWidget::reset(QSharedPointer element) { QSharedPointer entry = element.dynamicCast(); if (entry.isNull()) return false; Value combinedValue; for (const QString &keyStem : keyStart) for (int i = 1; i < 32; ++i) { /// FIXME replace number by constant const QString key = i > 1 ? keyStem + QString::number(i) : keyStem; const Value &value = entry->operator [](key); for (const auto &valueItem : const_cast(value)) combinedValue.append(valueItem); } fileList->setElement(element.data()); fileList->reset(combinedValue); return true; } bool FilesWidget::validate(QWidget **widgetWithIssue, QString &message) const { return fileList->validate(widgetWithIssue, message); } void FilesWidget::setReadOnly(bool isReadOnly) { ElementWidget::setReadOnly(isReadOnly); fileList->setReadOnly(isReadOnly); } QString FilesWidget::label() { return i18n("External"); } QIcon FilesWidget::icon() { return QIcon::fromTheme(QStringLiteral("emblem-symbolic-link")); } void FilesWidget::setFile(const File *file) { ElementWidget::setFile(file); fileList->setFile(file); } bool FilesWidget::canEdit(const Element *element) { return Entry::isEntry(*element); } const QStringList FilesWidget::keyStart {Entry::ftUrl, QStringLiteral("postscript"), Entry::ftLocalFile, Entry::ftDOI, Entry::ftFile, QStringLiteral("ee"), QStringLiteral("biburl")}; OtherFieldsWidget::OtherFieldsWidget(const QStringList &blacklistedFields, QWidget *parent) : ElementWidget(parent), blackListed(blacklistedFields) { internalEntry = QSharedPointer(new Entry()); createGUI(); } OtherFieldsWidget::~OtherFieldsWidget() { delete fieldContent; } bool OtherFieldsWidget::apply(QSharedPointer element) const { if (isReadOnly) return false; /// never save data if in read-only mode QSharedPointer entry = element.dynamicCast(); if (entry.isNull()) return false; for (const QString &key : const_cast(deletedKeys)) entry->remove(key); for (const QString &key : const_cast(modifiedKeys)) { entry->remove(key); entry->insert(key, internalEntry->value(key)); } return true; } bool OtherFieldsWidget::reset(QSharedPointer element) { QSharedPointer entry = element.dynamicCast(); if (entry.isNull()) return false; internalEntry = QSharedPointer(new Entry(*entry.data())); deletedKeys.clear(); // FIXME clearing list may be premature here... modifiedKeys.clear(); // FIXME clearing list may be premature here... updateList(); updateGUI(); return true; } bool OtherFieldsWidget::validate(QWidget **, QString &) const { /// No checks to make here; all actual check will be conducted in actionAddApply(..) return true; } void OtherFieldsWidget::setReadOnly(bool isReadOnly) { ElementWidget::setReadOnly(isReadOnly); fieldName->setReadOnly(isReadOnly); fieldContent->setReadOnly(isReadOnly); /// will take care of enabled/disabling buttons updateGUI(); updateList(); } QString OtherFieldsWidget::label() { return i18n("Other Fields"); } QIcon OtherFieldsWidget::icon() { return QIcon::fromTheme(QStringLiteral("other")); } bool OtherFieldsWidget::canEdit(const Element *element) { return Entry::isEntry(*element); } void OtherFieldsWidget::listElementExecuted(QTreeWidgetItem *item, int column) { Q_UNUSED(column) /// we do not care which column got clicked QString key = item->text(0); fieldName->setText(key); fieldContent->reset(internalEntry->value(key)); } void OtherFieldsWidget::listCurrentChanged(QTreeWidgetItem *item, QTreeWidgetItem *previous) { Q_UNUSED(previous) bool validUrl = false; bool somethingSelected = item != nullptr; buttonDelete->setEnabled(somethingSelected && !isReadOnly); if (somethingSelected) { currentUrl = QUrl(item->text(1)); validUrl = currentUrl.isValid() && currentUrl.isLocalFile() & QFileInfo::exists(currentUrl.toLocalFile()); if (!validUrl) { const QRegularExpressionMatch urlRegExpMatch = KBibTeX::urlRegExp.match(item->text(1)); if (urlRegExpMatch.hasMatch()) { currentUrl = QUrl(urlRegExpMatch.captured(0)); validUrl = currentUrl.isValid(); buttonOpen->setEnabled(validUrl); } } } if (!validUrl) currentUrl = QUrl(); buttonOpen->setEnabled(validUrl); } void OtherFieldsWidget::actionAddApply() { if (isReadOnly) return; /// never modify anything if in read-only mode QString key = fieldName->text(), message; Value value; if (!fieldContent->validate(nullptr, message)) return; ///< invalid values should not get applied if (!fieldContent->apply(value)) return; if (internalEntry->contains(key)) internalEntry->remove(key); internalEntry->insert(key, value); if (!modifiedKeys.contains(key)) modifiedKeys << key; updateList(); updateGUI(); gotModified(); } void OtherFieldsWidget::actionDelete() { if (isReadOnly) return; /// never modify anything if in read-only mode Q_ASSERT_X(otherFieldsList->currentItem() != nullptr, "OtherFieldsWidget::actionDelete", "otherFieldsList->currentItem() is NULL"); QString key = otherFieldsList->currentItem()->text(0); if (!deletedKeys.contains(key)) deletedKeys << key; internalEntry->remove(key); updateList(); updateGUI(); listCurrentChanged(otherFieldsList->currentItem(), nullptr); gotModified(); } void OtherFieldsWidget::actionOpen() { if (currentUrl.isValid()) { /// Guess mime type for url to open QMimeType mimeType = FileInfo::mimeTypeForUrl(currentUrl); const QString mimeTypeName = mimeType.name(); /// Ask KDE subsystem to open url in viewer matching mime type KRun::runUrl(currentUrl, mimeTypeName, this, KRun::RunFlags()); } } void OtherFieldsWidget::createGUI() { QGridLayout *layout = new QGridLayout(this); /// set row and column stretches based on chosen layout layout->setColumnStretch(0, 0); layout->setColumnStretch(1, 1); layout->setColumnStretch(2, 0); layout->setRowStretch(0, 0); layout->setRowStretch(1, 1); layout->setRowStretch(2, 0); layout->setRowStretch(3, 0); layout->setRowStretch(4, 1); QLabel *label = new QLabel(i18n("Name:"), this); layout->addWidget(label, 0, 0, 1, 1); label->setAlignment(static_cast(label->style()->styleHint(QStyle::SH_FormLayoutLabelAlignment))); fieldName = new QLineEdit(this); layout->addWidget(fieldName, 0, 1, 1, 1); label->setBuddy(fieldName); buttonAddApply = new QPushButton(QIcon::fromTheme(QStringLiteral("list-add")), i18n("Add"), this); buttonAddApply->setEnabled(false); layout->addWidget(buttonAddApply, 0, 2, 1, 1); label = new QLabel(i18n("Content:"), this); layout->addWidget(label, 1, 0, 1, 1); label->setAlignment(static_cast(label->style()->styleHint(QStyle::SH_FormLayoutLabelAlignment))); fieldContent = new FieldInput(KBibTeX::MultiLine, KBibTeX::tfSource, KBibTeX::tfSource, this); layout->addWidget(fieldContent, 1, 1, 1, 2); label->setBuddy(fieldContent->buddy()); label = new QLabel(i18n("List:"), this); layout->addWidget(label, 2, 0, 1, 1); label->setAlignment(static_cast(label->style()->styleHint(QStyle::SH_FormLayoutLabelAlignment))); otherFieldsList = new QTreeWidget(this); otherFieldsList->setHeaderLabels(QStringList {i18n("Key"), i18n("Value")}); otherFieldsList->setRootIsDecorated(false); layout->addWidget(otherFieldsList, 2, 1, 3, 1); label->setBuddy(otherFieldsList); buttonDelete = new QPushButton(QIcon::fromTheme(QStringLiteral("list-remove")), i18n("Delete"), this); buttonDelete->setEnabled(false); layout->addWidget(buttonDelete, 2, 2, 1, 1); buttonOpen = new QPushButton(QIcon::fromTheme(QStringLiteral("document-open")), i18n("Open"), this); buttonOpen->setEnabled(false); layout->addWidget(buttonOpen, 3, 2, 1, 1); connect(otherFieldsList, &QTreeWidget::itemActivated, this, &OtherFieldsWidget::listElementExecuted); connect(otherFieldsList, &QTreeWidget::currentItemChanged, this, &OtherFieldsWidget::listCurrentChanged); connect(otherFieldsList, &QTreeWidget::itemSelectionChanged, this, &OtherFieldsWidget::updateGUI); connect(fieldName, &QLineEdit::textEdited, this, &OtherFieldsWidget::updateGUI); connect(buttonAddApply, &QPushButton::clicked, this, &OtherFieldsWidget::actionAddApply); connect(buttonDelete, &QPushButton::clicked, this, &OtherFieldsWidget::actionDelete); connect(buttonOpen, &QPushButton::clicked, this, &OtherFieldsWidget::actionOpen); } void OtherFieldsWidget::updateList() { const QString selText = otherFieldsList->selectedItems().isEmpty() ? QString() : otherFieldsList->selectedItems().first()->text(0); const QString curText = otherFieldsList->currentItem() == nullptr ? QString() : otherFieldsList->currentItem()->text(0); otherFieldsList->clear(); for (Entry::ConstIterator it = internalEntry->constBegin(); it != internalEntry->constEnd(); ++it) if (!blackListed.contains(it.key().toLower())) { QTreeWidgetItem *item = new QTreeWidgetItem(); item->setText(0, it.key()); item->setText(1, PlainTextValue::text(it.value())); item->setIcon(0, QIcon::fromTheme(QStringLiteral("entry"))); // FIXME otherFieldsList->addTopLevelItem(item); item->setSelected(selText == it.key()); if (it.key() == curText) otherFieldsList->setCurrentItem(item); } } void OtherFieldsWidget::updateGUI() { QString key = fieldName->text(); if (key.isEmpty() || blackListed.contains(key, Qt::CaseInsensitive)) // TODO check for more (e.g. spaces) buttonAddApply->setEnabled(false); else { buttonAddApply->setEnabled(!isReadOnly); buttonAddApply->setText(internalEntry->contains(key) ? i18n("Apply") : i18n("Add")); buttonAddApply->setIcon(internalEntry->contains(key) ? QIcon::fromTheme(QStringLiteral("document-edit")) : QIcon::fromTheme(QStringLiteral("list-add"))); } } MacroWidget::MacroWidget(QWidget *parent) : ElementWidget(parent) { createGUI(); } MacroWidget::~MacroWidget() { delete fieldInputValue; } bool MacroWidget::apply(QSharedPointer element) const { if (isReadOnly) return false; /// never save data if in read-only mode QSharedPointer macro = element.dynamicCast(); if (macro.isNull()) return false; Value value; bool result = fieldInputValue->apply(value); macro->setValue(value); return result; } bool MacroWidget::reset(QSharedPointer element) { QSharedPointer macro = element.dynamicCast(); if (macro.isNull()) return false; return fieldInputValue->reset(macro->value()); } bool MacroWidget::validate(QWidget **widgetWithIssue, QString &message) const { return fieldInputValue->validate(widgetWithIssue, message); } void MacroWidget::setReadOnly(bool isReadOnly) { ElementWidget::setReadOnly(isReadOnly); fieldInputValue->setReadOnly(isReadOnly); } QString MacroWidget::label() { return i18n("Macro"); } QIcon MacroWidget::icon() { return QIcon::fromTheme(QStringLiteral("macro")); } bool MacroWidget::canEdit(const Element *element) { return Macro::isMacro(*element); } void MacroWidget::createGUI() { QBoxLayout *layout = new QHBoxLayout(this); QLabel *label = new QLabel(i18n("Value:"), this); layout->addWidget(label, 0); label->setAlignment(static_cast(label->style()->styleHint(QStyle::SH_FormLayoutLabelAlignment))); fieldInputValue = new FieldInput(KBibTeX::MultiLine, KBibTeX::tfPlainText, KBibTeX::tfPlainText | KBibTeX::tfSource, this); layout->addWidget(fieldInputValue, 1); label->setBuddy(fieldInputValue->buddy()); connect(fieldInputValue, &FieldInput::modified, this, &MacroWidget::gotModified); } PreambleWidget::PreambleWidget(QWidget *parent) : ElementWidget(parent) { createGUI(); } bool PreambleWidget::apply(QSharedPointer element) const { if (isReadOnly) return false; /// never save data if in read-only mode QSharedPointer preamble = element.dynamicCast(); if (preamble.isNull()) return false; Value value; bool result = fieldInputValue->apply(value); preamble->setValue(value); return result; } bool PreambleWidget::reset(QSharedPointer element) { QSharedPointer preamble = element.dynamicCast(); if (preamble.isNull()) return false; return fieldInputValue->reset(preamble->value()); } bool PreambleWidget::validate(QWidget **widgetWithIssue, QString &message) const { return fieldInputValue->validate(widgetWithIssue, message); } void PreambleWidget::setReadOnly(bool isReadOnly) { ElementWidget::setReadOnly(isReadOnly); fieldInputValue->setReadOnly(isReadOnly); } QString PreambleWidget::label() { return i18n("Preamble"); } QIcon PreambleWidget::icon() { return QIcon::fromTheme(QStringLiteral("preamble")); } bool PreambleWidget::canEdit(const Element *element) { return Preamble::isPreamble(*element); } void PreambleWidget::createGUI() { QBoxLayout *layout = new QHBoxLayout(this); QLabel *label = new QLabel(i18n("Value:"), this); layout->addWidget(label, 0); label->setAlignment(static_cast(label->style()->styleHint(QStyle::SH_FormLayoutLabelAlignment))); fieldInputValue = new FieldInput(KBibTeX::MultiLine, KBibTeX::tfSource, KBibTeX::tfSource, this); // FIXME: other editing modes beyond Source applicable? layout->addWidget(fieldInputValue, 1); label->setBuddy(fieldInputValue->buddy()); connect(fieldInputValue, &FieldInput::modified, this, &PreambleWidget::gotModified); } class SourceWidget::Private { public: QComboBox *messages; QPushButton *buttonRestore; FileImporterBibTeX *importerBibTeX; DelayedExecutionTimer *delayedExecutionTimer; Private(SourceWidget *parent) : messages(nullptr), buttonRestore(nullptr), importerBibTeX(new FileImporterBibTeX(parent)), delayedExecutionTimer(new DelayedExecutionTimer(1500, 500, parent)) { /// nothing } void addMessage(const FileImporter::MessageSeverity severity, const QString &messageText) { const QIcon icon = severity == FileImporter::SeverityInfo ? QIcon::fromTheme(QStringLiteral("dialog-information")) : (severity == FileImporter::SeverityWarning ? QIcon::fromTheme(QStringLiteral("dialog-warning")) : (severity == FileImporter::SeverityError ? QIcon::fromTheme(QStringLiteral("dialog-error")) : QIcon::fromTheme(QStringLiteral("dialog-question")))); messages->addItem(icon, messageText); } }; SourceWidget::SourceWidget(QWidget *parent) : ElementWidget(parent), elementClass(elementInvalid), d(new SourceWidget::Private(this)) { createGUI(); connect(document, &KTextEditor::Document::textChanged, d->delayedExecutionTimer, &DelayedExecutionTimer::trigger); connect(document, &KTextEditor::Document::textChanged, d->messages, &QComboBox::clear); connect(d->delayedExecutionTimer, &DelayedExecutionTimer::triggered, this, &SourceWidget::updateMessage); } SourceWidget::~SourceWidget() { delete document; delete d; } void SourceWidget::setElementClass(ElementClass elementClass) { this->elementClass = elementClass; updateMessage(); } bool SourceWidget::apply(QSharedPointer element) const { if (isReadOnly) return false; ///< never save data if in read-only mode const QString text = document->text(); const QScopedPointer file(d->importerBibTeX->fromString(text)); if (file.isNull() || file->count() != 1) return false; QSharedPointer entry = element.dynamicCast(); QSharedPointer readEntry = file->first().dynamicCast(); if (!readEntry.isNull() && !entry.isNull()) { if (elementClass != elementEntry) return false; ///< Source widget should only edit Entry objects entry->operator =(*readEntry.data()); //entry = readEntry; return true; } else { QSharedPointer macro = element.dynamicCast(); QSharedPointer readMacro = file->first().dynamicCast(); if (!readMacro.isNull() && !macro.isNull()) { if (elementClass != elementMacro) return false; ///< Source widget should only edit Macro objects macro->operator =(*readMacro.data()); return true; } else { QSharedPointer preamble = element.dynamicCast(); QSharedPointer readPreamble = file->first().dynamicCast(); if (!readPreamble.isNull() && !preamble.isNull()) { if (elementClass != elementPreamble) return false; ///< Source widget should only edit Preamble objects preamble->operator =(*readPreamble.data()); return true; } else { qCWarning(LOG_KBIBTEX_GUI) << "Do not know how to apply source code"; return false; } } } } bool SourceWidget::reset(QSharedPointer element) { /// if signals are not deactivated, the "modified" signal would be emitted when /// resetting the widget's value disconnect(document, &KTextEditor::Document::textChanged, this, &SourceWidget::gotModified); FileExporterBibTeX exporter(this); exporter.setEncoding(QStringLiteral("utf-8")); const QString exportedText = exporter.toString(element, m_file); if (!exportedText.isEmpty()) { originalText = exportedText; /// Limitation of KTextEditor: If editor is read-only, no text can be set /// Therefore, temporarily lift read-only status const bool originalReadWriteStatus = document->isReadWrite(); document->setReadWrite(true); const bool settingTextSuccessful = document->setText(originalText); if (!settingTextSuccessful) qCWarning(LOG_KBIBTEX_GUI) << "Could not set BibTeX source code to source editor"; document->setReadWrite(originalReadWriteStatus); } else qCWarning(LOG_KBIBTEX_GUI) << "Converting entry to BibTeX source resulting in empty text"; connect(document, &KTextEditor::Document::textChanged, this, &SourceWidget::gotModified); return !exportedText.isEmpty(); } bool SourceWidget::validate(QWidget **widgetWithIssue, QString &message) const { message.clear(); d->messages->clear(); const QString text = document->text(); connect(d->importerBibTeX, &FileImporterBibTeX::message, this, &SourceWidget::addMessage); const QScopedPointer file(d->importerBibTeX->fromString(text)); disconnect(d->importerBibTeX, &FileImporterBibTeX::message, this, &SourceWidget::addMessage); if (file.isNull() || file->count() != 1) { if (widgetWithIssue != nullptr) *widgetWithIssue = document->views().at(0); ///< We create one view initially, so this should never fail message = i18n("Given source code does not parse as one single BibTeX element."); return false; } bool result = false; switch (elementClass) { case elementEntry: { QSharedPointer entry = file->first().dynamicCast(); result = !entry.isNull(); if (!result) message = i18n("Given source code does not parse as one single BibTeX entry."); } break; case elementMacro: { QSharedPointer macro = file->first().dynamicCast(); result = !macro.isNull(); if (!result) message = i18n("Given source code does not parse as one single BibTeX macro."); } break; case elementPreamble: { QSharedPointer preamble = file->first().dynamicCast(); result = !preamble.isNull(); if (!result) message = i18n("Given source code does not parse as one single BibTeX preamble."); } break; // case elementComment // TODO? default: message = QString(QStringLiteral("elementClass is unknown: %1")).arg(elementClass); result = false; } if (!result && widgetWithIssue != nullptr) *widgetWithIssue = document->views().at(0); ///< We create one view initially, so this should never fail if (message.isEmpty() && d->messages->count() == 0) d->addMessage(FileImporter::SeverityInfo, i18n("No issues detected")); return result; } void SourceWidget::setReadOnly(bool isReadOnly) { ElementWidget::setReadOnly(isReadOnly); d->buttonRestore->setEnabled(!isReadOnly); document->setReadWrite(!isReadOnly); } QString SourceWidget::label() { return i18n("Source"); } QIcon SourceWidget::icon() { return QIcon::fromTheme(QStringLiteral("code-context")); } bool SourceWidget::canEdit(const Element *element) { Q_UNUSED(element) return true; /// source widget should be able to edit any element } void SourceWidget::createGUI() { QGridLayout *layout = new QGridLayout(this); layout->setColumnStretch(0, 1); layout->setColumnStretch(1, 0); layout->setRowStretch(0, 1); layout->setRowStretch(1, 0); KTextEditor::Editor *editor = KTextEditor::Editor::instance(); document = editor->createDocument(this); document->setHighlightingMode(QStringLiteral("BibTeX")); KTextEditor::View *view = document->createView(this); layout->addWidget(view, 0, 0, 1, 2); d->messages = new QComboBox(this); layout->addWidget(d->messages, 1, 0, 1, 1); d->buttonRestore = new QPushButton(QIcon::fromTheme(QStringLiteral("edit-undo")), i18n("Restore"), this); layout->addWidget(d->buttonRestore, 1, 1, 1, 1); connect(d->buttonRestore, &QPushButton::clicked, this, static_cast(&SourceWidget::reset)); connect(document, &KTextEditor::Document::textChanged, this, &SourceWidget::gotModified); } void SourceWidget::reset() { /// if signals are not deactivated, the "modified" signal would be emitted when /// resetting the widget's value disconnect(document, &KTextEditor::Document::textChanged, this, &SourceWidget::gotModified); document->setText(originalText); setModified(false); connect(document, &KTextEditor::Document::textChanged, this, &SourceWidget::gotModified); } void SourceWidget::addMessage(const FileImporter::MessageSeverity severity, const QString &messageText) { d->addMessage(severity, messageText); } void SourceWidget::updateMessage() { QString message; const bool validationResult = validate(nullptr, message); if (!message.isEmpty()) { if (validationResult) addMessage(FileImporter::SeverityInfo, message); else addMessage(FileImporter::SeverityError, message); } } #include "elementwidgets.moc" diff --git a/src/gui/element/findpdfui.cpp b/src/gui/element/findpdfui.cpp index 86dbb8fb..2fe07255 100644 --- a/src/gui/element/findpdfui.cpp +++ b/src/gui/element/findpdfui.cpp @@ -1,535 +1,534 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, see . * ***************************************************************************/ #include "findpdfui.h" #include "findpdfui_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include -#include #include #include #include #include #include #include #include #include #include #include "field/fieldlistedit.h" #include "logging_gui.h" class PDFListModel; const int posLabelUrl = 0; const int posLabelPreview = 1; const int posViewButton = 2; const int posRadioNoDownload = 3; const int posRadioDownload = 4; const int posRadioURLonly = 5; /// inspired by KNewStuff3's ItemsViewDelegate PDFItemDelegate::PDFItemDelegate(QListView *itemView, QObject *parent) : KWidgetItemDelegate(itemView, parent), m_parent(itemView) { /// nothing } void PDFItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { QStyle *style = QApplication::style(); style->drawPrimitive(QStyle::PE_PanelItemViewItem, &option, painter, nullptr); painter->save(); if (option.state & QStyle::State_Selected) { painter->setPen(QPen(option.palette.highlightedText().color())); } else { painter->setPen(QPen(option.palette.text().color())); } /// draw icon based on mime-type QPixmap icon = index.data(Qt::DecorationRole).value(); if (!icon.isNull()) { int margin = option.fontMetrics.height() / 3; painter->drawPixmap(margin, margin + option.rect.top(), KIconLoader::SizeMedium, KIconLoader::SizeMedium, icon); } painter->restore(); } QList PDFItemDelegate::createItemWidgets(const QModelIndex &index) const { Q_UNUSED(index) // FIXME really of no use? QList list; /// first, the label with shows the found PDF file's origin (URL) KSqueezedTextLabel *label = new KSqueezedTextLabel(); label->setBackgroundRole(QPalette::NoRole); label->setAlignment(Qt::AlignTop | Qt::AlignLeft); list << label; Q_ASSERT_X(list.count() == posLabelUrl + 1, "QList PDFItemDelegate::createItemWidgets() const", "list.count() != posLabelUrl + 1"); /// a label with shows either the PDF's title or a text snipplet QLabel *previewLabel = new QLabel(); previewLabel->setBackgroundRole(QPalette::NoRole); previewLabel->setAlignment(Qt::AlignTop | Qt::AlignLeft); list << previewLabel; Q_ASSERT_X(list.count() == posLabelPreview + 1, "QList PDFItemDelegate::createItemWidgets() const", "list.count() != posLabelPreview + 1"); /// add a push button to view the PDF file QPushButton *pushButton = new QPushButton(QIcon::fromTheme(QStringLiteral("application-pdf")), i18n("View")); list << pushButton; connect(pushButton, &QPushButton::clicked, this, &PDFItemDelegate::slotViewPDF); Q_ASSERT_X(list.count() == posViewButton + 1, "QList PDFItemDelegate::createItemWidgets() const", "list.count() != posViewButton + 1"); /// a button group to choose what to do with this particular PDF file QButtonGroup *bg = new QButtonGroup(); /// button group's first choice: ignore file (discard it) QRadioButton *radioButton = new QRadioButton(i18n("Ignore")); bg->addButton(radioButton); list << radioButton; connect(radioButton, &QRadioButton::toggled, this, &PDFItemDelegate::slotRadioNoDownloadToggled); Q_ASSERT_X(list.count() == posRadioNoDownload + 1, "QList PDFItemDelegate::createItemWidgets() const", "list.count() != posRadioNoDownload + 1"); /// download this file and store it locally, user will be asked for "Save As" radioButton = new QRadioButton(i18n("Download")); bg->addButton(radioButton); list << radioButton; connect(radioButton, &QRadioButton::toggled, this, &PDFItemDelegate::slotRadioDownloadToggled); Q_ASSERT_X(list.count() == posRadioDownload + 1, "QList PDFItemDelegate::createItemWidgets() const", "list.count() != posRadioDownload + 1"); /// paste URL into BibTeX entry, no local copy is stored radioButton = new QRadioButton(i18n("Use URL only")); bg->addButton(radioButton); list << radioButton; connect(radioButton, &QRadioButton::toggled, this, &PDFItemDelegate::slotRadioURLonlyToggled); Q_ASSERT_X(list.count() == posRadioURLonly + 1, "QList PDFItemDelegate::createItemWidgets() const", "list.count() != posRadioURLonly + 1"); return list; } /// Update the widgets /// Clazy warns: "Missing reference on non-trivial type" for argument 'widgets', /// but KWidgetItemDelegate defines this function this way and cannot be changed. void PDFItemDelegate::updateItemWidgets(const QList widgets, const QStyleOptionViewItem &option, const QPersistentModelIndex &index) const { if (!index.isValid()) return; const PDFListModel *model = qobject_cast(index.model()); if (model == nullptr) { qCDebug(LOG_KBIBTEX_GUI) << "WARNING - INVALID MODEL!"; return; } /// determine some variables used for layout const int margin = option.fontMetrics.height() / 3; const int buttonHeight = option.fontMetrics.height() * 6 / 3; #if QT_VERSION >= 0x050b00 const int maxTextWidth = qMax(qMax(option.fontMetrics.horizontalAdvance(i18n("Use URL only")), option.fontMetrics.horizontalAdvance(i18n("Ignore"))), qMax(option.fontMetrics.horizontalAdvance(i18n("Download")), option.fontMetrics.horizontalAdvance(i18n("View")))); #else // QT_VERSION >= 0x050b00 const int maxTextWidth = qMax(qMax(option.fontMetrics.width(i18n("Use URL only")), option.fontMetrics.width(i18n("Ignore"))), qMax(option.fontMetrics.width(i18n("Download")), option.fontMetrics.width(i18n("View")))); #endif // QT_VERSION >= 0x050b00 const int buttonWidth = maxTextWidth * 3 / 2; const int labelWidth = option.rect.width() - 3 * margin - KIconLoader::SizeMedium; const int labelHeight = option.fontMetrics.height();//(option.rect.height() - 4 * margin - buttonHeight) / 2; /// Total height = margin + labelHeight + margin + labelHeight + marin + buttonHeight + margin /// = option.fontMetrics.height() * (1/3 + 1 + 1/3 + 1 + 1/3 + 6/3 + 1/3) /// = option.fontMetrics.height() * 16 / 3 /// setup label which will show the PDF file's URL KSqueezedTextLabel *label = qobject_cast(widgets[posLabelUrl]); if (label != nullptr) { const QString text = index.data(PDFListModel::URLRole).toUrl().toDisplayString(); label->setText(text); label->setToolTip(text); label->move(margin * 2 + KIconLoader::SizeMedium, margin); label->resize(labelWidth, labelHeight); } /// setup label which will show the PDF's title or textual beginning QLabel *previewLabel = qobject_cast(widgets[posLabelPreview]); if (previewLabel != nullptr) { previewLabel->setText(index.data(PDFListModel::TextualPreviewRole).toString()); previewLabel->move(margin * 2 + KIconLoader::SizeMedium, margin * 2 + labelHeight); previewLabel->resize(labelWidth, labelHeight); } /// setup the view button QPushButton *viewButton = qobject_cast(widgets[posViewButton]); if (viewButton != nullptr) { const QSize hint = viewButton->sizeHint(); const int h = hint.isValid() ? qMin(buttonHeight, hint.height()) : buttonHeight; viewButton->move(margin * 2 + KIconLoader::SizeMedium, option.rect.height() - margin - h); viewButton->resize(buttonWidth, h); } /// setup each of the three radio buttons for (int i = 0; i < 3; ++i) { QRadioButton *radioButton = qobject_cast(widgets[posRadioNoDownload + i]); if (radioButton != nullptr) { const QSize hint = radioButton->sizeHint(); const int h = hint.isValid() ? qMin(buttonHeight, hint.height()) : buttonHeight; radioButton->move(option.rect.width() - margin - (3 - i) * (buttonWidth + margin), option.rect.height() - margin - h); radioButton->resize(buttonWidth, h); bool ok = false; radioButton->setChecked(i + FindPDF::NoDownload == index.data(PDFListModel::DownloadModeRole).toInt(&ok) && ok); } } } QSize PDFItemDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &) const { /// set a size that is suiteable QSize size; #if QT_VERSION >= 0x050b00 size.setWidth(option.fontMetrics.horizontalAdvance(i18n("Download")) * 6); #else // QT_VERSION >= 0x050b00 size.setWidth(option.fontMetrics.width(i18n("Download")) * 6); #endif // QT_VERSION >= 0x050b00 size.setHeight(qMax(option.fontMetrics.height() * 16 / 3, static_cast(KIconLoader::SizeMedium))); ///< KIconLoader::SizeMedium should be 32 return size; } /** * Method is called when the "View PDF" button of a list item is clicked. * Opens the associated URL or its local copy using the system's default viewer. */ void PDFItemDelegate::slotViewPDF() { QModelIndex index = focusedIndex(); if (index.isValid()) { const QString tempfileName = index.data(PDFListModel::TempFileNameRole).toString(); const QUrl url = index.data(PDFListModel::URLRole).toUrl(); if (!tempfileName.isEmpty()) { /// Guess mime type for url to open QUrl tempUrl(tempfileName); QMimeType mimeType = FileInfo::mimeTypeForUrl(tempUrl); const QString mimeTypeName = mimeType.name(); /// Ask KDE subsystem to open url in viewer matching mime type KRun::runUrl(tempUrl, mimeTypeName, itemView(), KRun::RunFlags(), url.toDisplayString()); } else if (url.isValid()) { /// Guess mime type for url to open QMimeType mimeType = FileInfo::mimeTypeForUrl(url); const QString mimeTypeName = mimeType.name(); /// Ask KDE subsystem to open url in viewer matching mime type KRun::runUrl(url, mimeTypeName, itemView(), KRun::RunFlags()); } } } /** * Updated the model when the user selects the radio button for ignoring a PDF file. */ void PDFItemDelegate::slotRadioNoDownloadToggled(bool checked) { QModelIndex index = focusedIndex(); if (index.isValid() && checked) { m_parent->model()->setData(index, FindPDF::NoDownload, PDFListModel::DownloadModeRole); } } /** * Updated the model when the user selects the radio button for downloading a PDF file. */ void PDFItemDelegate::slotRadioDownloadToggled(bool checked) { QModelIndex index = focusedIndex(); if (index.isValid() && checked) { m_parent->model()->setData(index, FindPDF::Download, PDFListModel::DownloadModeRole); } } /** * Updated the model when the user selects the radio button for keeping a PDF file's URL. */ void PDFItemDelegate::slotRadioURLonlyToggled(bool checked) { QModelIndex index = focusedIndex(); if (index.isValid() && checked) { m_parent->model()->setData(index, FindPDF::URLonly, PDFListModel::DownloadModeRole); } } PDFListModel::PDFListModel(QList &resultList, QObject *parent) : QAbstractListModel(parent), m_resultList(resultList) { /// nothing } int PDFListModel::rowCount(const QModelIndex &parent) const { /// row cout depends on number of found PDF references int count = parent == QModelIndex() ? m_resultList.count() : 0; return count; } QVariant PDFListModel::data(const QModelIndex &index, int role) const { if (index != QModelIndex() && index.parent() == QModelIndex() && index.row() < m_resultList.count()) { if (role == Qt::DisplayRole) return m_resultList[index.row()].url.toDisplayString(); else if (role == URLRole) return m_resultList[index.row()].url; else if (role == TextualPreviewRole) return m_resultList[index.row()].textPreview; else if (role == Qt::ToolTipRole) return QStringLiteral("") + m_resultList[index.row()].textPreview + QStringLiteral(""); ///< 'qt' tags required for word wrap else if (role == TempFileNameRole) { if (m_resultList[index.row()].tempFilename != nullptr) return m_resultList[index.row()].tempFilename->fileName(); else return QVariant(); } else if (role == DownloadModeRole) return m_resultList[index.row()].downloadMode; else if (role == Qt::DecorationRole) { /// make an educated guess on the icon, based on URL or path QString iconName = FileInfo::mimeTypeForUrl(m_resultList[index.row()].url).iconName(); iconName = iconName == QStringLiteral("application-octet-stream") ? QStringLiteral("application-pdf") : iconName; return QIcon::fromTheme(iconName).pixmap(KIconLoader::SizeMedium, KIconLoader::SizeMedium); } else return QVariant(); } return QVariant(); } bool PDFListModel::setData(const QModelIndex &index, const QVariant &value, int role) { if (index != QModelIndex() && index.row() < m_resultList.count() && role == DownloadModeRole) { bool ok = false; const FindPDF::DownloadMode downloadMode = static_cast(value.toInt(&ok)); if (ok) { m_resultList[index.row()].downloadMode = downloadMode; return true; } } return false; } QVariant PDFListModel::headerData(int section, Qt::Orientation orientation, int role) const { Q_UNUSED(orientation); if (section == 0) { if (role == Qt::DisplayRole || role == Qt::ToolTipRole) { return i18n("Result"); } else return QVariant(); } return QVariant(); } class FindPDFUI::Private { private: FindPDFUI *p; public: QListView *listViewResult; QLabel *labelMessage; QList resultList; FindPDF *findpdf; Private(FindPDFUI *parent) : p(parent), findpdf(new FindPDF(parent)) { setupGUI(); } void setupGUI() { QGridLayout *layout = new QGridLayout(p); const int minWidth = p->fontMetrics().height() * 40; const int minHeight = p->fontMetrics().height() * 20; p->setMinimumSize(minWidth, minHeight); listViewResult = new QListView(p); layout->addWidget(listViewResult, 0, 0); listViewResult->setEnabled(false); listViewResult->hide(); labelMessage = new QLabel(p); layout->addWidget(labelMessage, 1, 0); labelMessage->setMinimumSize(minWidth, minHeight); labelMessage->setAlignment(Qt::AlignVCenter | Qt::AlignCenter); static_cast(p->parent())->setCursor(Qt::WaitCursor); } }; FindPDFUI::FindPDFUI(Entry &entry, QWidget *parent) : QWidget(parent), d(new Private(this)) { d->labelMessage->show(); d->labelMessage->setText(i18n("Starting to search...")); connect(d->findpdf, &FindPDF::finished, this, &FindPDFUI::searchFinished); connect(d->findpdf, &FindPDF::progress, this, &FindPDFUI::searchProgress); d->findpdf->search(entry); } FindPDFUI::~FindPDFUI() { for (QList::Iterator it = d->resultList.begin(); it != d->resultList.end();) { delete it->tempFilename; it = d->resultList.erase(it); } } void FindPDFUI::interactiveFindPDF(Entry &entry, const File &bibtexFile, QWidget *parent) { QPointer dlg = new QDialog(parent); QPointer widget = new FindPDFUI(entry, dlg); dlg->setWindowTitle(i18n("Find PDF")); QBoxLayout *layout = new QVBoxLayout(dlg); layout->addWidget(widget); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Abort | QDialogButtonBox::Ok | QDialogButtonBox::Cancel, Qt::Horizontal, dlg); layout->addWidget(buttonBox); dlg->setLayout(layout); buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); connect(widget.data(), &FindPDFUI::resultAvailable, buttonBox->button(QDialogButtonBox::Ok), &QWidget::setEnabled); connect(widget.data(), &FindPDFUI::resultAvailable, buttonBox->button(QDialogButtonBox::Abort), &QWidget::setDisabled); connect(buttonBox->button(QDialogButtonBox::Ok), &QPushButton::clicked, dlg.data(), &QDialog::accept); connect(buttonBox->button(QDialogButtonBox::Cancel), &QPushButton::clicked, dlg.data(), &QDialog::reject); connect(buttonBox->button(QDialogButtonBox::Abort), &QPushButton::clicked, widget.data(), &FindPDFUI::stopSearch); if (dlg->exec() == QDialog::Accepted) widget->apply(entry, bibtexFile); delete dlg; } void FindPDFUI::apply(Entry &entry, const File &bibtexFile) { QAbstractItemModel *model = d->listViewResult->model(); for (int i = 0; i < model->rowCount(); ++i) { bool ok = false; FindPDF::DownloadMode downloadMode = static_cast(model->data(model->index(i, 0), PDFListModel::DownloadModeRole).toInt(&ok)); if (!ok) { qCDebug(LOG_KBIBTEX_GUI) << "Could not interprete download mode"; downloadMode = FindPDF::NoDownload; } QUrl url = model->data(model->index(i, 0), PDFListModel::URLRole).toUrl(); QString tempfileName = model->data(model->index(i, 0), PDFListModel::TempFileNameRole).toString(); if (downloadMode == FindPDF::URLonly && url.isValid()) { bool alreadyContained = false; for (QMap::ConstIterator it = entry.constBegin(); !alreadyContained && it != entry.constEnd(); ++it) // FIXME this will terribly break if URLs in an entry's URL field are separated with semicolons alreadyContained |= it.key().toLower().startsWith(Entry::ftUrl) && PlainTextValue::text(it.value()) == url.toDisplayString(); if (!alreadyContained) { Value value; value.append(QSharedPointer(new VerbatimText(url.toDisplayString()))); if (!entry.contains(Entry::ftUrl)) entry.insert(Entry::ftUrl, value); else for (int i = 2; i < 256; ++i) { const QString keyName = QString(QStringLiteral("%1%2")).arg(Entry::ftUrl).arg(i); if (!entry.contains(keyName)) { entry.insert(keyName, value); break; } } } } else if (downloadMode == FindPDF::Download && !tempfileName.isEmpty()) { QUrl startUrl = bibtexFile.property(File::Url, QUrl()).toUrl(); const QString absoluteFilename = QFileDialog::getSaveFileName(this, i18n("Save URL '%1'", url.url(QUrl::PreferLocalFile)), startUrl.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path(), QStringLiteral("application/pdf")); if (!absoluteFilename.isEmpty()) { const QString visibleFilename = UrlListEdit::askRelativeOrStaticFilename(this, absoluteFilename, startUrl); qCDebug(LOG_KBIBTEX_GUI) << "Saving PDF from " << url << " to file " << absoluteFilename << " known as " << visibleFilename; // FIXME test for overwrite QFile::copy(tempfileName, absoluteFilename); bool alreadyContained = false; for (QMap::ConstIterator it = entry.constBegin(); !alreadyContained && it != entry.constEnd(); ++it) alreadyContained |= (it.key().toLower().startsWith(Entry::ftFile) || it.key().toLower().startsWith(Entry::ftLocalFile) || it.key().toLower().startsWith(Entry::ftUrl)) && PlainTextValue::text(it.value()) == url.toDisplayString(); if (!alreadyContained) { Value value; value.append(QSharedPointer(new VerbatimText(visibleFilename))); const QString fieldNameStem = Preferences::instance().bibliographySystem() == Preferences::BibTeX ? Entry::ftLocalFile : Entry::ftFile; if (!entry.contains(fieldNameStem)) entry.insert(fieldNameStem, value); else for (int i = 2; i < 256; ++i) { const QString keyName = QString(QStringLiteral("%1%2")).arg(fieldNameStem).arg(i); if (!entry.contains(keyName)) { entry.insert(keyName, value); break; } } } } } } } void FindPDFUI::searchFinished() { d->labelMessage->hide(); d->listViewResult->show(); d->resultList = d->findpdf->results(); d->listViewResult->setModel(new PDFListModel(d->resultList, d->listViewResult)); d->listViewResult->setItemDelegate(new PDFItemDelegate(d->listViewResult, d->listViewResult)); d->listViewResult->setEnabled(true); d->listViewResult->reset(); static_cast(parent())->unsetCursor(); emit resultAvailable(true); } void FindPDFUI::searchProgress(int visitedPages, int runningJobs, int foundDocuments) { d->listViewResult->hide(); d->labelMessage->show(); d->labelMessage->setText(i18n("Number of visited pages: %1
Number of running downloads: %2
Number of found documents: %3
", visitedPages, runningJobs, foundDocuments)); } void FindPDFUI::stopSearch() { d->findpdf->abort(); searchFinished(); } void FindPDFUI::abort() { d->findpdf->abort(); } diff --git a/src/gui/field/fieldlineedit.cpp b/src/gui/field/fieldlineedit.cpp index f2f8a0bb..b8cf8241 100644 --- a/src/gui/field/fieldlineedit.cpp +++ b/src/gui/field/fieldlineedit.cpp @@ -1,556 +1,555 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, see . * ***************************************************************************/ #include "fieldlineedit.h" #include #include #include #include #include #include #include #include #include #include -#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "logging_gui.h" class FieldLineEdit::FieldLineEditPrivate { private: FieldLineEdit *parent; Value currentValue; KBibTeX::TypeFlag preferredTypeFlag; KBibTeX::TypeFlags typeFlags; QPushButton *buttonOpenUrl; public: QMenu *menuTypes; KBibTeX::TypeFlag typeFlag; QUrl urlToOpen; const File *file; QString fieldKey; FieldLineEditPrivate(KBibTeX::TypeFlag ptf, KBibTeX::TypeFlags tf, FieldLineEdit *p) : parent(p), preferredTypeFlag(ptf), typeFlags(tf), file(nullptr) { menuTypes = new QMenu(parent); setupMenu(); buttonOpenUrl = new QPushButton(QIcon::fromTheme(QStringLiteral("document-open-remote")), QString(), parent); buttonOpenUrl->setVisible(false); buttonOpenUrl->setProperty("isConst", true); parent->appendWidget(buttonOpenUrl); connect(buttonOpenUrl, &QPushButton::clicked, parent, &FieldLineEdit::slotOpenUrl); connect(p, &FieldLineEdit::textChanged, p, &FieldLineEdit::slotTextChanged); Value value; typeFlag = determineTypeFlag(value, preferredTypeFlag, typeFlags); updateGUI(typeFlag); } bool reset(const Value &value) { bool result = false; QString text; typeFlag = determineTypeFlag(value, typeFlag, typeFlags); updateGUI(typeFlag); if (!value.isEmpty()) { if (typeFlag == KBibTeX::tfSource) { /// simple case: field's value is to be shown as BibTeX code, including surrounding curly braces FileExporterBibTeX exporter(parent); text = exporter.valueToBibTeX(value); result = true; } else { /// except for the source view type flag, type flag views do not support composed values, /// therefore only the first value will be shown const QSharedPointer first = value.first(); const QSharedPointer plainText = first.dynamicCast<PlainText>(); if (typeFlag == KBibTeX::tfPlainText && !plainText.isNull()) { text = plainText->text(); result = true; } else { const QSharedPointer<Person> person = first.dynamicCast<Person>(); if (typeFlag == KBibTeX::tfPerson && !person.isNull()) { text = Person::transcribePersonName(person.data(), Preferences::instance().personNameFormat()); result = true; } else { const QSharedPointer<MacroKey> macroKey = first.dynamicCast<MacroKey>(); if (typeFlag == KBibTeX::tfReference && !macroKey.isNull()) { text = macroKey->text(); result = true; } else { const QSharedPointer<Keyword> keyword = first.dynamicCast<Keyword>(); if (typeFlag == KBibTeX::tfKeyword && !keyword.isNull()) { text = keyword->text(); result = true; } else { const QSharedPointer<VerbatimText> verbatimText = first.dynamicCast<VerbatimText>(); if (typeFlag == KBibTeX::tfVerbatim && !verbatimText.isNull()) { text = verbatimText->text(); result = true; } else qCWarning(LOG_KBIBTEX_GUI) << "Could not reset: " << typeFlag << "(" << (typeFlag == KBibTeX::tfSource ? "Source" : (typeFlag == KBibTeX::tfReference ? "Reference" : (typeFlag == KBibTeX::tfPerson ? "Person" : (typeFlag == KBibTeX::tfPlainText ? "PlainText" : (typeFlag == KBibTeX::tfKeyword ? "Keyword" : (typeFlag == KBibTeX::tfVerbatim ? "Verbatim" : "???")))))) << ") " << (typeFlags.testFlag(KBibTeX::tfPerson) ? "Person" : "") << (typeFlags.testFlag(KBibTeX::tfPlainText) ? "PlainText" : "") << (typeFlags.testFlag(KBibTeX::tfReference) ? "Reference" : "") << (typeFlags.testFlag(KBibTeX::tfVerbatim) ? "Verbatim" : "") << " " << typeid((void)*first).name() << " : " << PlainTextValue::text(value); } } } } } } updateURL(text); parent->setText(text); return result; } bool apply(Value &value) const { value.clear(); /// Remove unnecessary white space from input /// Exception: source and verbatim content is kept unmodified const QString text = typeFlag == KBibTeX::tfSource || typeFlag == KBibTeX::tfVerbatim ? parent->text() : parent->text().simplified(); if (text.isEmpty()) return true; const EncoderLaTeX &encoder = EncoderLaTeX::instance(); const QString encodedText = encoder.decode(text); static const QRegularExpression invalidCharsForReferenceRegExp(QStringLiteral("[^-_:/a-zA-Z0-9]")); if (encodedText.isEmpty()) return true; else if (typeFlag == KBibTeX::tfPlainText) { value.append(QSharedPointer<PlainText>(new PlainText(encodedText))); return true; } else if (typeFlag == KBibTeX::tfReference && !encodedText.contains(invalidCharsForReferenceRegExp)) { value.append(QSharedPointer<MacroKey>(new MacroKey(encodedText))); return true; } else if (typeFlag == KBibTeX::tfPerson) { QSharedPointer<Person> person = FileImporterBibTeX::personFromString(encodedText); if (!person.isNull()) value.append(person); return true; } else if (typeFlag == KBibTeX::tfKeyword) { const QList<QSharedPointer<Keyword> > keywords = FileImporterBibTeX::splitKeywords(encodedText); for (const auto &keyword : keywords) value.append(keyword); return true; } else if (typeFlag == KBibTeX::tfSource) { const QString key = typeFlags.testFlag(KBibTeX::tfPerson) ? QStringLiteral("author") : QStringLiteral("title"); FileImporterBibTeX importer(parent); const QString fakeBibTeXFile = QString(QStringLiteral("@article{dummy, %1=%2}")).arg(key, encodedText); const QScopedPointer<const File> file(importer.fromString(fakeBibTeXFile)); if (!file.isNull() && file->count() == 1) { QSharedPointer<Entry> entry = file->first().dynamicCast<Entry>(); if (!entry.isNull()) { value = entry->value(key); return !value.isEmpty(); } else qCWarning(LOG_KBIBTEX_GUI) << "Parsing " << fakeBibTeXFile << " did not result in valid entry"; } } else if (typeFlag == KBibTeX::tfVerbatim) { value.append(QSharedPointer<VerbatimText>(new VerbatimText(text))); return true; } return false; } bool validate(QWidget **widgetWithIssue, QString &message) const { message.clear(); /// Remove unnecessary white space from input /// Exception: source and verbatim content is kept unmodified const QString text = typeFlag == KBibTeX::tfSource || typeFlag == KBibTeX::tfVerbatim ? parent->text() : parent->text().simplified(); if (text.isEmpty()) return true; const EncoderLaTeX &encoder = EncoderLaTeX::instance(); const QString encodedText = encoder.decode(text); if (encodedText.isEmpty()) return true; bool result = false; if (typeFlag == KBibTeX::tfPlainText || typeFlag == KBibTeX::tfPerson || typeFlag == KBibTeX::tfKeyword) { result = KBibTeX::validateCurlyBracketContext(text) == 0; if (!result) message = i18n("Opening and closing curly brackets do not match."); } else if (typeFlag == KBibTeX::tfReference) { static const QRegularExpression validReferenceRegExp(QStringLiteral("^[-_:/a-zA-Z0-9]+$")); const QRegularExpressionMatch validReferenceMatch = validReferenceRegExp.match(text); result = validReferenceMatch.hasMatch() && validReferenceMatch.captured() == text; if (!result) message = i18n("Reference contains characters outside of the allowed set."); } else if (typeFlag == KBibTeX::tfSource) { const QString key = typeFlags.testFlag(KBibTeX::tfPerson) ? QStringLiteral("author") : QStringLiteral("title"); FileImporterBibTeX importer(parent); const QString fakeBibTeXFile = QString(QStringLiteral("@article{dummy, %1=%2}")).arg(key, encodedText); const QScopedPointer<const File> file(importer.fromString(fakeBibTeXFile)); if (file.isNull() || file->count() != 1) return false; QSharedPointer<Entry> entry = file->first().dynamicCast<Entry>(); result = !entry.isNull() && entry->count() == 1; if (!result) message = i18n("Source code could not be parsed correctly."); } else if (typeFlag == KBibTeX::tfVerbatim) { result = KBibTeX::validateCurlyBracketContext(text) == 0; if (!result) message = i18n("Opening and closing curly brackets do not match."); } if (!result && widgetWithIssue != nullptr) *widgetWithIssue = parent; return result; } KBibTeX::TypeFlag determineTypeFlag(const Value &value, KBibTeX::TypeFlag preferredTypeFlag, KBibTeX::TypeFlags availableTypeFlags) { KBibTeX::TypeFlag result = KBibTeX::tfSource; if (availableTypeFlags.testFlag(preferredTypeFlag) && typeFlagSupported(value, preferredTypeFlag)) result = preferredTypeFlag; else if (value.count() == 1) { int p = 1; for (int i = 1; i < 8; ++i, p <<= 1) { const KBibTeX::TypeFlag flag = static_cast<KBibTeX::TypeFlag>(p); if (availableTypeFlags.testFlag(flag) && typeFlagSupported(value, flag)) { result = flag; break; } } } return result; } bool typeFlagSupported(const Value &value, KBibTeX::TypeFlag typeFlag) { if (value.isEmpty() || typeFlag == KBibTeX::tfSource) return true; const QSharedPointer<ValueItem> first = value.first(); if (value.count() > 1) return typeFlag == KBibTeX::tfSource; else if (typeFlag == KBibTeX::tfKeyword && Keyword::isKeyword(*first)) return true; else if (typeFlag == KBibTeX::tfPerson && Person::isPerson(*first)) return true; else if (typeFlag == KBibTeX::tfPlainText && PlainText::isPlainText(*first)) return true; else if (typeFlag == KBibTeX::tfReference && MacroKey::isMacroKey(*first)) return true; else if (typeFlag == KBibTeX::tfVerbatim && VerbatimText::isVerbatimText(*first)) return true; else return false; } void setupMenu() { menuTypes->clear(); if (typeFlags.testFlag(KBibTeX::tfPlainText)) { QAction *action = menuTypes->addAction(iconForTypeFlag(KBibTeX::tfPlainText), i18n("Plain Text")); connect(action, &QAction::triggered, parent, [this]() { typeChanged(KBibTeX::tfPlainText); }); } if (typeFlags.testFlag(KBibTeX::tfReference)) { QAction *action = menuTypes->addAction(iconForTypeFlag(KBibTeX::tfReference), i18n("Reference")); connect(action, &QAction::triggered, parent, [this]() { typeChanged(KBibTeX::tfReference); }); } if (typeFlags.testFlag(KBibTeX::tfPerson)) { QAction *action = menuTypes->addAction(iconForTypeFlag(KBibTeX::tfPerson), i18n("Person")); connect(action, &QAction::triggered, parent, [this]() { typeChanged(KBibTeX::tfPerson); }); } if (typeFlags.testFlag(KBibTeX::tfKeyword)) { QAction *action = menuTypes->addAction(iconForTypeFlag(KBibTeX::tfKeyword), i18n("Keyword")); connect(action, &QAction::triggered, parent, [this]() { typeChanged(KBibTeX::tfKeyword); }); } if (typeFlags.testFlag(KBibTeX::tfSource)) { QAction *action = menuTypes->addAction(iconForTypeFlag(KBibTeX::tfSource), i18n("Source Code")); connect(action, &QAction::triggered, parent, [this]() { typeChanged(KBibTeX::tfSource); }); } if (typeFlags.testFlag(KBibTeX::tfVerbatim)) { QAction *action = menuTypes->addAction(iconForTypeFlag(KBibTeX::tfVerbatim), i18n("Verbatim Text")); connect(action, &QAction::triggered, parent, [this]() { typeChanged(KBibTeX::tfVerbatim); }); } } QIcon iconForTypeFlag(KBibTeX::TypeFlag typeFlag) { switch (typeFlag) { case KBibTeX::tfPlainText: return QIcon::fromTheme(QStringLiteral("draw-text")); case KBibTeX::tfReference: return QIcon::fromTheme(QStringLiteral("emblem-symbolic-link")); case KBibTeX::tfPerson: return QIcon::fromTheme(QStringLiteral("user-identity")); case KBibTeX::tfKeyword: return QIcon::fromTheme(QStringLiteral("edit-find")); case KBibTeX::tfSource: return QIcon::fromTheme(QStringLiteral("code-context")); case KBibTeX::tfVerbatim: return QIcon::fromTheme(QStringLiteral("preferences-desktop-keyboard")); default: return QIcon(); }; } void updateGUI(KBibTeX::TypeFlag typeFlag) { parent->setFont(QFontDatabase::systemFont(QFontDatabase::GeneralFont)); parent->setIcon(iconForTypeFlag(typeFlag)); switch (typeFlag) { case KBibTeX::tfPlainText: parent->setButtonToolTip(i18n("Plain Text")); break; case KBibTeX::tfReference: parent->setButtonToolTip(i18n("Reference")); break; case KBibTeX::tfPerson: parent->setButtonToolTip(i18n("Person")); break; case KBibTeX::tfKeyword: parent->setButtonToolTip(i18n("Keyword")); break; case KBibTeX::tfSource: parent->setButtonToolTip(i18n("Source Code")); parent->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); break; case KBibTeX::tfVerbatim: parent->setButtonToolTip(i18n("Verbatim Text")); break; default: parent->setButtonToolTip(QString()); break; }; } void openUrl() { if (urlToOpen.isValid()) { /// Guess mime type for url to open QMimeType mimeType = FileInfo::mimeTypeForUrl(urlToOpen); const QString mimeTypeName = mimeType.name(); /// Ask KDE subsystem to open url in viewer matching mime type KRun::runUrl(urlToOpen, mimeTypeName, parent, KRun::RunFlags()); } } bool convertValueType(Value &value, KBibTeX::TypeFlag destType) { if (value.isEmpty()) return true; /// simple case if (destType == KBibTeX::tfSource) return true; /// simple case bool result = true; const EncoderLaTeX &encoder = EncoderLaTeX::instance(); QString rawText; const QSharedPointer<ValueItem> first = value.first(); const QSharedPointer<PlainText> plainText = first.dynamicCast<PlainText>(); if (!plainText.isNull()) rawText = encoder.encode(plainText->text(), Encoder::TargetEncodingASCII); else { const QSharedPointer<VerbatimText> verbatimText = first.dynamicCast<VerbatimText>(); if (!verbatimText.isNull()) rawText = verbatimText->text(); else { const QSharedPointer<MacroKey> macroKey = first.dynamicCast<MacroKey>(); if (!macroKey.isNull()) rawText = macroKey->text(); else { const QSharedPointer<Person> person = first.dynamicCast<Person>(); if (!person.isNull()) rawText = encoder.encode(QString(QStringLiteral("%1 %2")).arg(person->firstName(), person->lastName()), Encoder::TargetEncodingASCII); // FIXME proper name conversion else { const QSharedPointer<Keyword> keyword = first.dynamicCast<Keyword>(); if (!keyword.isNull()) rawText = encoder.encode(keyword->text(), Encoder::TargetEncodingASCII); else { // TODO case missed? result = false; } } } } } switch (destType) { case KBibTeX::tfPlainText: value.clear(); value.append(QSharedPointer<PlainText>(new PlainText(encoder.decode(rawText)))); break; case KBibTeX::tfVerbatim: value.clear(); value.append(QSharedPointer<VerbatimText>(new VerbatimText(rawText))); break; case KBibTeX::tfPerson: value.clear(); value.append(QSharedPointer<Person>(FileImporterBibTeX::splitName(encoder.decode(rawText)))); break; case KBibTeX::tfReference: { MacroKey *macroKey = new MacroKey(rawText); if (macroKey->isValid()) { value.clear(); value.append(QSharedPointer<MacroKey>(macroKey)); } else { delete macroKey; result = false; } } break; case KBibTeX::tfKeyword: value.clear(); value.append(QSharedPointer<Keyword>(new Keyword(encoder.decode(rawText)))); break; default: { // TODO result = false; } } return result; } void updateURL(const QString &text) { QSet<QUrl> urls; FileInfo::urlsInText(text, FileInfo::TestExistenceYes, file != nullptr && file->property(File::Url).toUrl().isValid() ? QUrl(file->property(File::Url).toUrl()).path() : QString(), urls); QSet<QUrl>::ConstIterator urlsIt = urls.constBegin(); if (urlsIt != urls.constEnd() && (*urlsIt).isValid()) urlToOpen = (*urlsIt); else urlToOpen = QUrl(); /// set special "open URL" button visible if URL (or file or DOI) found buttonOpenUrl->setVisible(urlToOpen.isValid()); buttonOpenUrl->setToolTip(i18n("Open '%1'", urlToOpen.url(QUrl::PreferLocalFile))); } void textChanged(const QString &text) { updateURL(text); } void typeChanged(const KBibTeX::TypeFlag newTypeFlag) { Value value; apply(value); if (convertValueType(value, newTypeFlag)) { typeFlag = newTypeFlag; reset(value); } else KMessageBox::error(parent, i18n("The current text cannot be used as value of type '%1'.\n\nSwitching back to type '%2'.", BibTeXFields::typeFlagToString(newTypeFlag), BibTeXFields::typeFlagToString(typeFlag))); } }; FieldLineEdit::FieldLineEdit(KBibTeX::TypeFlag preferredTypeFlag, KBibTeX::TypeFlags typeFlags, bool isMultiLine, QWidget *parent) : MenuLineEdit(isMultiLine, parent), d(new FieldLineEdit::FieldLineEditPrivate(preferredTypeFlag, typeFlags, this)) { setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); setObjectName(QStringLiteral("FieldLineEdit")); setMenu(d->menuTypes); setChildAcceptDrops(false); setAcceptDrops(true); } FieldLineEdit::~FieldLineEdit() { delete d; } bool FieldLineEdit::apply(Value &value) const { return d->apply(value); } bool FieldLineEdit::reset(const Value &value) { return d->reset(value); } bool FieldLineEdit::validate(QWidget **widgetWithIssue, QString &message) const { return d->validate(widgetWithIssue, message); } void FieldLineEdit::setReadOnly(bool isReadOnly) { MenuLineEdit::setReadOnly(isReadOnly); } void FieldLineEdit::setFile(const File *file) { d->file = file; } void FieldLineEdit::setElement(const Element *element) { Q_UNUSED(element) } void FieldLineEdit::setFieldKey(const QString &fieldKey) { d->fieldKey = fieldKey; } void FieldLineEdit::slotOpenUrl() { d->openUrl(); } void FieldLineEdit::slotTextChanged(const QString &text) { d->textChanged(text); } void FieldLineEdit::dragEnterEvent(QDragEnterEvent *event) { if (event->mimeData()->hasFormat(QStringLiteral("text/plain")) || event->mimeData()->hasFormat(QStringLiteral("text/x-bibtex"))) event->acceptProposedAction(); } void FieldLineEdit::dropEvent(QDropEvent *event) { const QString clipboardText = event->mimeData()->text(); if (clipboardText.isEmpty()) return; bool success = false; if (!d->fieldKey.isEmpty() && clipboardText.startsWith(QStringLiteral("@"))) { FileImporterBibTeX importer(this); QScopedPointer<File> file(importer.fromString(clipboardText)); const QSharedPointer<Entry> entry = (!file.isNull() && file->count() == 1) ? file->first().dynamicCast<Entry>() : QSharedPointer<Entry>(); if (!entry.isNull() && d->fieldKey == Entry::ftCrossRef) { /// handle drop on crossref line differently (use dropped entry's id) Value v; v.append(QSharedPointer<VerbatimText>(new VerbatimText(entry->id()))); reset(v); emit textChanged(entry->id()); success = true; } else if (!entry.isNull() && entry->contains(d->fieldKey)) { /// case for "normal" fields like for journal, pages, ... reset(entry->value(d->fieldKey)); emit textChanged(text()); success = true; } } if (!success) { /// fall-back case: just copy whole text into edit widget setText(clipboardText); emit textChanged(clipboardText); } } diff --git a/src/io/fileinfo.cpp b/src/io/fileinfo.cpp index 0231eb14..538b7f29 100644 --- a/src/io/fileinfo.cpp +++ b/src/io/fileinfo.cpp @@ -1,370 +1,371 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, see <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "fileinfo.h" #include <poppler-qt5.h> #include <QFileInfo> +#include <QMimeDatabase> #include <QDir> #include <QTextStream> #include <QStandardPaths> #include <QRegularExpression> #include <QtConcurrentRun> #include <KBibTeX> #include <Entry> #include "logging_io.h" FileInfo::FileInfo() { /// nothing } const QString FileInfo::mimetypeOctetStream = QStringLiteral("application/octet-stream"); const QString FileInfo::mimetypeHTML = QStringLiteral("text/html"); const QString FileInfo::mimetypeBibTeX = QStringLiteral("text/x-bibtex"); const QString FileInfo::mimetypeRIS = QStringLiteral("application/x-research-info-systems"); const QString FileInfo::mimetypePDF = QStringLiteral("application/pdf"); QMimeType FileInfo::mimeTypeForUrl(const QUrl &url) { if (!url.isValid() || url.isEmpty()) { qCWarning(LOG_KBIBTEX_IO) << "Cannot determine mime type for empty or invalid QUrl"; return QMimeType(); ///< invalid input gives invalid mime type } static const QMimeDatabase db; static const QMimeType mtHTML(db.mimeTypeForName(mimetypeHTML)); static const QMimeType mtOctetStream(db.mimeTypeForName(mimetypeOctetStream)); static const QMimeType mtBibTeX(db.mimeTypeForName(mimetypeBibTeX)); static const QMimeType mtPDF(db.mimeTypeForName(mimetypePDF)); static const QMimeType mtRIS(db.mimeTypeForName(mimetypeRIS)); /// Test if mime type for BibTeX is registered before determining file extension static const QString mimetypeBibTeXExt = mtBibTeX.preferredSuffix(); /// Test if mime type for RIS is registered before determining file extension static const QString mimetypeRISExt = mtRIS.preferredSuffix(); /// Test if mime type for PDF is registered before determining file extension static const QString mimetypePDFExt = mtPDF.preferredSuffix(); const QString extension = db.suffixForFileName(url.fileName()).toLower(); /// First, check preferred suffixes if (extension == mimetypeBibTeXExt) return mtBibTeX; else if (extension == mimetypeRISExt) return mtRIS; else if (extension == mimetypePDFExt) return mtPDF; /// Second, check any other suffixes else if (mtBibTeX.suffixes().contains(extension)) return mtBibTeX; else if (mtRIS.suffixes().contains(extension)) return mtRIS; else if (mtPDF.suffixes().contains(extension)) return mtPDF; /// Let the KDE subsystem guess the mime type QMimeType result = db.mimeTypeForUrl(url); /// Fall back to application/octet-stream if something goes wrong if (!result.isValid()) result = mtOctetStream; /// In case that KDE could not determine mime type, /// do some educated guesses on our own if (result.name() == mimetypeOctetStream) { if (url.scheme().startsWith(QStringLiteral("http"))) result = mtHTML; // TODO more tests? } return result; } void FileInfo::urlsInText(const QString &text, const TestExistence testExistence, const QString &baseDirectory, QSet<QUrl> &result) { if (text.isEmpty()) return; /// DOI identifiers have to extracted first as KBibTeX::fileListSeparatorRegExp /// contains characters that can be part of a DOI (e.g. ';') and thus could split /// a DOI in between. QString internalText = text; int pos = 0; QRegularExpressionMatch doiRegExpMatch; while ((doiRegExpMatch = KBibTeX::doiRegExp.match(internalText, pos)).hasMatch()) { pos = doiRegExpMatch.capturedStart(0); QString doiMatch = doiRegExpMatch.captured(0); const int semicolonHttpPos = doiMatch.indexOf(QStringLiteral(";http")); if (semicolonHttpPos > 0) doiMatch = doiMatch.left(semicolonHttpPos); const QUrl url(KBibTeX::doiUrlPrefix + QString(doiMatch).remove(QStringLiteral("\\"))); if (url.isValid() && !result.contains(url)) result << url; /// remove match from internal text to avoid duplicates /// Cut away any URL that may be right before found DOI number: /// For example, if DOI '10.1000/38-abc' was found in /// 'Lore ipsum http://doi.example.org/10.1000/38-abc Lore ipsum' /// also remove 'http://doi.example.org/' from the text, keeping only /// 'Lore ipsum Lore ipsum' static const QRegularExpression genericDoiUrlPrefix(QStringLiteral("http[s]?://[a-z0-9./-]+/$")); ///< looks like an URL const QRegularExpressionMatch genericDoiUrlPrefixMatch = genericDoiUrlPrefix.match(internalText.left(pos)); if (genericDoiUrlPrefixMatch.hasMatch()) /// genericDoiUrlPrefixMatch.captured(0) may contain (parts of) DOI internalText = internalText.left(genericDoiUrlPrefixMatch.capturedStart(0)) + internalText.mid(pos + doiMatch.length()); else internalText = internalText.left(pos) + internalText.mid(pos + doiMatch.length()); } const QStringList fileList = internalText.split(KBibTeX::fileListSeparatorRegExp, QString::SkipEmptyParts); for (const QString &text : fileList) { internalText = text; /// If testing for the actual existence of a filename found in the text ... if (testExistence == TestExistenceYes) { /// If a base directory (e.g. the location of the parent .bib file) is given /// and the potential filename fragment is NOT an absolute path, ... if (internalText.startsWith(QStringLiteral("~") + QDir::separator())) { const QString fullFilename = QDir::homePath() + internalText.mid(1); const QFileInfo fileInfo(fullFilename); const QUrl url = QUrl::fromLocalFile(fileInfo.canonicalFilePath()); if (fileInfo.exists() && fileInfo.isFile() && url.isValid() && !result.contains(url)) { result << url; /// Stop searching for URLs or filenames in current internal text continue; } } else if (!baseDirectory.isEmpty() && // TODO the following test assumes that absolute paths start // with a dir separator, which may only be true on Unix/Linux, // but not Windows. May be a test for 'first character is a letter, // second is ":", third is "\"' may be necessary. !internalText.startsWith(QDir::separator())) { /// To get the absolute path, prepend filename fragment with base directory const QString fullFilename = baseDirectory + QDir::separator() + internalText; const QFileInfo fileInfo(fullFilename); const QUrl url = QUrl::fromLocalFile(fileInfo.canonicalFilePath()); if (fileInfo.exists() && fileInfo.isFile() && url.isValid() && !result.contains(url)) { result << url; /// Stop searching for URLs or filenames in current internal text continue; } } else { /// Either the filename fragment is an absolute path OR no base directory /// was given (current working directory is assumed), ... const QFileInfo fileInfo(internalText); const QUrl url = QUrl::fromLocalFile(fileInfo.canonicalFilePath()); if (fileInfo.exists() && fileInfo.isFile() && url.isValid() && !result.contains(url)) { result << url; /// stop searching for URLs or filenames in current internal text continue; } } } /// extract URL from current field pos = 0; QRegularExpressionMatch urlRegExpMatch; while ((urlRegExpMatch = KBibTeX::urlRegExp.match(internalText, pos)).hasMatch()) { pos = urlRegExpMatch.capturedStart(0); const QString match = urlRegExpMatch.captured(0); QUrl url(match); if (url.isValid() && (testExistence == TestExistenceNo || !url.isLocalFile() || QFileInfo::exists(url.toLocalFile())) && !result.contains(url)) result << url; /// remove match from internal text to avoid duplicates internalText = internalText.left(pos) + internalText.mid(pos + match.length()); } /// explicitly check URL entry, may be an URL even if http:// or alike is missing pos = 0; QRegularExpressionMatch domainNameRegExpMatch; while ((domainNameRegExpMatch = KBibTeX::domainNameRegExp.match(internalText, pos)).hasMatch()) { pos = domainNameRegExpMatch.capturedStart(0); int pos2 = internalText.indexOf(QStringLiteral(" "), pos + 1); if (pos2 < 0) pos2 = internalText.length(); QString match = internalText.mid(pos, pos2 - pos); const QUrl url(QStringLiteral("http://") + match); // FIXME what about HTTPS? if (url.isValid() && !result.contains(url)) result << url; /// remove match from internal text to avoid duplicates internalText = internalText.left(pos) + internalText.mid(pos + match.length()); } /// extract general file-like patterns pos = 0; QRegularExpressionMatch fileRegExpMatch; while ((fileRegExpMatch = KBibTeX::fileRegExp.match(internalText, pos)).hasMatch()) { pos = fileRegExpMatch.capturedStart(0); const QString match = fileRegExpMatch.captured(0); QUrl url(match); if (url.isValid() && (testExistence == TestExistenceNo || !url.isLocalFile() || QFileInfo::exists(url.toLocalFile())) && !result.contains(url)) result << url; /// remove match from internal text to avoid duplicates internalText = internalText.left(pos) + internalText.mid(pos + match.length()); } } } QSet<QUrl> FileInfo::entryUrls(const QSharedPointer<const Entry> &entry, const QUrl &bibTeXUrl, TestExistence testExistence) { QSet<QUrl> result; if (entry.isNull() || entry->isEmpty()) return result; if (entry->contains(Entry::ftDOI)) { const QString doi = PlainTextValue::text(entry->value(Entry::ftDOI)); QRegularExpressionMatch doiRegExpMatch; if (!doi.isEmpty() && (doiRegExpMatch = KBibTeX::doiRegExp.match(doi)).hasMatch()) { QString match = doiRegExpMatch.captured(0); QUrl url(KBibTeX::doiUrlPrefix + match.remove(QStringLiteral("\\"))); result.insert(url); } } static const QString etPMID = QStringLiteral("pmid"); if (entry->contains(etPMID)) { const QString pmid = PlainTextValue::text(entry->value(etPMID)); bool ok = false; ok &= pmid.toInt(&ok) > 0; if (ok) { QUrl url(QStringLiteral("https://www.ncbi.nlm.nih.gov/pubmed/") + pmid); result.insert(url); } } static const QString etEPrint = QStringLiteral("eprint"); if (entry->contains(etEPrint)) { const QString eprint = PlainTextValue::text(entry->value(etEPrint)); if (!eprint.isEmpty()) { QUrl url(QStringLiteral("http://arxiv.org/search?query=") + eprint); result.insert(url); } } const QString baseDirectory = bibTeXUrl.isValid() ? bibTeXUrl.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path() : QString(); for (Entry::ConstIterator it = entry->constBegin(); it != entry->constEnd(); ++it) { /// skip abstracts, they contain sometimes strange text fragments /// that are mistaken for URLs if (it.key().toLower() == Entry::ftAbstract) continue; const Value v = it.value(); for (const auto &valueItem : v) { QString plainText = PlainTextValue::text(*valueItem); static const QRegularExpression regExpEscapedChars = QRegularExpression(QStringLiteral("\\\\+([&_~])")); plainText.replace(regExpEscapedChars, QStringLiteral("\\1")); urlsInText(plainText, testExistence, baseDirectory, result); } } if (!baseDirectory.isEmpty()) { /// File types supported by "document preview" static const QStringList documentFileExtensions {QStringLiteral(".pdf"), QStringLiteral(".pdf.gz"), QStringLiteral(".pdf.bz2"), QStringLiteral(".ps"), QStringLiteral(".ps.gz"), QStringLiteral(".ps.bz2"), QStringLiteral(".eps"), QStringLiteral(".eps.gz"), QStringLiteral(".eps.bz2"), QStringLiteral(".html"), QStringLiteral(".xhtml"), QStringLiteral(".htm"), QStringLiteral(".dvi"), QStringLiteral(".djvu"), QStringLiteral(".wwf"), QStringLiteral(".jpeg"), QStringLiteral(".jpg"), QStringLiteral(".png"), QStringLiteral(".gif"), QStringLiteral(".tif"), QStringLiteral(".tiff")}; result.reserve(result.size() + documentFileExtensions.size() * 2); /// check if in the same directory as the BibTeX file /// a PDF file exists which filename is based on the entry's id for (const QString &extension : documentFileExtensions) { const QFileInfo fi(baseDirectory + QDir::separator() + entry->id() + extension); if (fi.exists()) { const QUrl url = QUrl::fromLocalFile(fi.canonicalFilePath()); if (!result.contains(url)) result << url; } } /// check if in the same directory as the BibTeX file there is a subdirectory /// similar to the BibTeX file's name and which contains a PDF file exists /// which filename is based on the entry's id static const QRegularExpression filenameExtension(QStringLiteral("\\.[^.]{2,5}$")); const QString basename = bibTeXUrl.fileName().remove(filenameExtension); QString directory = baseDirectory + QDir::separator() + basename; for (const QString &extension : documentFileExtensions) { const QFileInfo fi(directory + QDir::separator() + entry->id() + extension); if (fi.exists()) { const QUrl url = QUrl::fromLocalFile(fi.canonicalFilePath()); if (!result.contains(url)) result << url; } } } return result; } QString FileInfo::pdfToText(const QString &pdfFilename) { /// Build filename for text file where PDF file's plain text is cached const QString cacheDirectory = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QStringLiteral("/pdftotext"); if (!QDir(cacheDirectory).exists() && !QDir::home().mkdir(cacheDirectory)) /// Could not create cache directory return QString(); static const QRegularExpression invalidChars(QStringLiteral("[^-a-z0-9_]"), QRegularExpression::CaseInsensitiveOption); const QString textFilename = QString(pdfFilename).remove(invalidChars).append(QStringLiteral(".txt")).prepend(QStringLiteral("/")).prepend(cacheDirectory); /// First, check if there is a cache text file if (QFileInfo::exists(textFilename)) { /// Load text from cache file QFile f(textFilename); if (f.open(QFile::ReadOnly)) { const QString text = QString::fromUtf8(f.readAll()); f.close(); return text; } } else /// No cache file exists, so run text extraction in another thread QtConcurrent::run(extractPDFTextToCache, pdfFilename, textFilename); return QString(); } void FileInfo::extractPDFTextToCache(const QString &pdfFilename, const QString &cacheFilename) { /// In case of multiple calls, skip text extraction if cache file already exists if (QFile(cacheFilename).exists()) return; QString text; QStringList msgList; /// Load PDF file through Poppler Poppler::Document *doc = Poppler::Document::load(pdfFilename); if (doc != nullptr) { static const int maxPages = 64; /// Build text by appending each page's text for (int i = 0; i < qMin(maxPages, doc->numPages()); ++i) text.append(doc->page(i)->text(QRect())).append(QStringLiteral("\n\n")); if (doc->numPages() > maxPages) msgList << QString(QStringLiteral("### Skipped %1 pages as PDF file contained too many pages (limit is %2 pages) ###")).arg(doc->numPages() - maxPages).arg(maxPages); delete doc; } else msgList << QStringLiteral("### Skipped as file could not be opened as PDF file ###"); /// Save text in cache file QFile f(cacheFilename); if (f.open(QFile::WriteOnly)) { static const int maxCharacters = 1 << 18; f.write(text.left(maxCharacters).toUtf8()); ///< keep only the first 2^18 many characters if (text.length() > maxCharacters) msgList << QString(QStringLiteral("### Text too long, skipping %1 characters ###")).arg(text.length() - maxCharacters); /// Write all messages (warnings) to end of text file for (const QString &msg : const_cast<const QStringList &>(msgList)) { static const char linebreak = '\n'; f.write(&linebreak, 1); f.write(msg.toUtf8()); } f.close(); } } diff --git a/src/io/fileinfo.h b/src/io/fileinfo.h index 83a1c2f7..4384f337 100644 --- a/src/io/fileinfo.h +++ b/src/io/fileinfo.h @@ -1,106 +1,105 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, see <https://www.gnu.org/licenses/>. * ***************************************************************************/ #ifndef KBIBTEX_IO_FILEINFO_H #define KBIBTEX_IO_FILEINFO_H #include <QSet> #include <QUrl> -#include <QMimeDatabase> #include <QMimeType> #include <QSharedPointer> #ifdef HAVE_KF5 #include "kbibtexio_export.h" #endif // HAVE_KF5 class Entry; class KBIBTEXIO_EXPORT FileInfo { public: static const QString mimetypeOctetStream; static const QString mimetypeHTML; static const QString mimetypeBibTeX; static const QString mimetypeRIS; static const QString mimetypePDF; enum TestExistence { TestExistenceYes, ///< Test if file exists TestExistenceNo ///< Skip test if file exists }; /** * Finds a QMimeType with the given url. * Tries to guess a file's mime type by its extension first, * but falls back to QMimeType's mimeTypeForName if that does * not work. Background: If a HTTP or WebDAV server claims * that a .bib file is of mime type application/octet-stream, * QMimeType::mimeTypeForName will keep that assessment * instead of inspecting the file extension. * * @see QMimeType::mimeTypeForName * @param url Url to analyze * @return Guessed mime type */ static QMimeType mimeTypeForUrl(const QUrl &url); /** * Find all file or URL references in the given text. Found filenames or * URLs are appended to the addTo list (duplicates are avoided). * Different test may get performed depending of the test for existence * of a potential file should be checked or not checked or if this matter * is undecided/irrelevant (recommended default case). For the test of * existence, baseDirectory is used to resolve relative paths. * @param text text to scan for filenames or URLs * @param testExistence shall be tested for file existence? * @param baseDirectory base directory for tests on relative path names * @param addTo add found URLs/filenames to this list */ static void urlsInText(const QString &text, const TestExistence testExistence, const QString &baseDirectory, QSet<QUrl> &addTo); /** * Find all file or URL references in the given entry. Found filenames or * URLs are appended to the addTo list (duplicates are avoided). * Different test may get performed depending of the test for existence * of a potential file should be checked or not checked or if this matter * is undecided/irrelevant (recommended default case). For the test of * existence, bibTeXUrl is used to resolve relative paths. * @param entry entry to scan for filenames or URLs * @param bibTeXUrl base directory/URL for tests on relative path names * @param testExistence shall be tested for file existence? * @return list of found URLs/filenames (duplicates are avoided) */ static QSet<QUrl> entryUrls(const QSharedPointer<const Entry> &entry, const QUrl &bibTeXUrl, TestExistence testExistence); /** * Load the given PDF file and return the contained plain text. * Makes use of Poppler to load and parse the file. All text * will be cached and loaded from cache if possible. * @param pdfFilename PDF file to load and extract text from * @return extracted plain text, either directly from PDF file or from cache OR QString() if there was an error */ static QString pdfToText(const QString &pdfFilename); protected: FileInfo(); private: static void extractPDFTextToCache(const QString &pdfFilename, const QString &cacheFilename); }; #endif // KBIBTEX_IO_FILEINFO_H diff --git a/src/parts/part.cpp b/src/parts/part.cpp index 9b2214a9..5258fea9 100644 --- a/src/parts/part.cpp +++ b/src/parts/part.cpp @@ -1,1048 +1,1045 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, see <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "part.h" #include <QLabel> #include <QAction> #include <QFile> #include <QFileInfo> #include <QMenu> #include <QApplication> #include <QLayout> #include <QKeyEvent> -#include <QMimeDatabase> #include <QMimeType> #include <QPointer> #include <QFileSystemWatcher> #include <QFileDialog> #include <QDialog> #include <QDialogButtonBox> #include <QPushButton> #include <QTemporaryFile> #include <QTimer> #include <QStandardPaths> #include <KMessageBox> // FIXME deprecated #include <KLocalizedString> #include <KActionCollection> #include <KStandardAction> #include <KActionMenu> #include <KSelectAction> #include <KToggleAction> #include <KRun> #include <KPluginFactory> #include <KIO/StatJob> #include <KIO/CopyJob> #include <KIO/Job> #include <KJobWidgets> #include <kio_version.h> #include <Preferences> #include <File> #include <Macro> #include <Preamble> #include <Comment> #include <FileInfo> #include <FileExporterBibTeXOutput> #include <FileImporterBibTeX> #include <FileExporterBibTeX> #include <FileImporterRIS> #include <FileImporterBibUtils> #include <FileExporterRIS> #include <FileExporterBibUtils> #include <FileImporterPDF> #include <FileExporterPS> #include <FileExporterPDF> #include <FileExporterRTF> #include <FileExporterBibTeX2HTML> #include <FileExporterXML> #include <FileExporterXSLT> #include <models/FileModel> #include <IdSuggestions> #include <LyX> #include <widgets/FileSettingsWidget> #include <widgets/FilterBar> #include <element/FindPDFUI> #include <file/FileView> #include <file/FindDuplicatesUI> #include <file/Clipboard> #include <preferences/SettingsColorLabelWidget> #include <preferences/SettingsFileExporterPDFPSWidget> #include <ValueListModel> #include "logging_parts.h" static const char RCFileName[] = "kbibtexpartui.rc"; class KBibTeXPart::KBibTeXPartPrivate { private: KBibTeXPart *p; /** * Modifies a given URL to become a "backup" filename/URL. * A backup level or 0 or less does not modify the URL. * A backup level of 1 appends a '~' (tilde) to the URL's filename. * A backup level of 2 or more appends '~N', where N is the level. * The provided URL will be modified in the process. It is assumed * that the URL is not yet a "backup URL". */ void constructBackupUrl(const int level, QUrl &url) const { if (level <= 0) /// No modification return; else if (level == 1) /// Simply append '~' to the URL's filename url.setPath(url.path() + QStringLiteral("~")); else /// Append '~' followed by a number to the filename url.setPath(url.path() + QString(QStringLiteral("~%1")).arg(level)); } public: File *bibTeXFile; PartWidget *partWidget; FileModel *model; SortFilterFileModel *sortFilterProxyModel; QAction *editCutAction, *editDeleteAction, *editCopyAction, *editPasteAction, *editCopyReferencesAction, *elementEditAction, *elementViewDocumentAction, *fileSaveAction, *elementFindPDFAction, *entryApplyDefaultFormatString; QMenu *viewDocumentMenu; bool isSaveAsOperation; LyX *lyx; FindDuplicatesUI *findDuplicatesUI; ColorLabelContextMenu *colorLabelContextMenu; QAction *colorLabelContextMenuAction; QFileSystemWatcher fileSystemWatcher; KBibTeXPartPrivate(QWidget *parentWidget, KBibTeXPart *parent) : p(parent), bibTeXFile(nullptr), model(nullptr), sortFilterProxyModel(nullptr), viewDocumentMenu(new QMenu(i18n("View Document"), parent->widget())), isSaveAsOperation(false), fileSystemWatcher(p) { connect(&fileSystemWatcher, &QFileSystemWatcher::fileChanged, p, &KBibTeXPart::fileExternallyChange); partWidget = new PartWidget(parentWidget); partWidget->fileView()->setReadOnly(!p->isReadWrite()); connect(partWidget->fileView(), &FileView::modified, p, &KBibTeXPart::setModified); setupActions(); } ~KBibTeXPartPrivate() { delete bibTeXFile; delete model; delete viewDocumentMenu; delete findDuplicatesUI; } void setupActions() { /// "Save" action fileSaveAction = p->actionCollection()->addAction(KStandardAction::Save); connect(fileSaveAction, &QAction::triggered, p, &KBibTeXPart::documentSave); fileSaveAction->setEnabled(false); QAction *action = p->actionCollection()->addAction(KStandardAction::SaveAs); connect(action, &QAction::triggered, p, &KBibTeXPart::documentSaveAs); /// "Save copy as" action QAction *saveCopyAsAction = new QAction(QIcon::fromTheme(QStringLiteral("document-save")), i18n("Save Copy As..."), p); p->actionCollection()->addAction(QStringLiteral("file_save_copy_as"), saveCopyAsAction); connect(saveCopyAsAction, &QAction::triggered, p, &KBibTeXPart::documentSaveCopyAs); /// Filter bar widget QAction *filterWidgetAction = new QAction(i18n("Filter"), p); p->actionCollection()->addAction(QStringLiteral("toolbar_filter_widget"), filterWidgetAction); filterWidgetAction->setIcon(QIcon::fromTheme(QStringLiteral("view-filter"))); p->actionCollection()->setDefaultShortcut(filterWidgetAction, Qt::CTRL + Qt::Key_F); connect(filterWidgetAction, &QAction::triggered, partWidget->filterBar(), static_cast<void(QWidget::*)()>(&QWidget::setFocus)); partWidget->filterBar()->setPlaceholderText(i18n("Filter bibliographic entries (%1)", filterWidgetAction->shortcut().toString())); /// Actions for creating new elements (entries, macros, ...) KActionMenu *newElementAction = new KActionMenu(QIcon::fromTheme(QStringLiteral("address-book-new")), i18n("New element"), p); p->actionCollection()->addAction(QStringLiteral("element_new"), newElementAction); QMenu *newElementMenu = new QMenu(newElementAction->text(), p->widget()); newElementAction->setMenu(newElementMenu); connect(newElementAction, &QAction::triggered, p, &KBibTeXPart::newEntryTriggered); QAction *newEntry = new QAction(QIcon::fromTheme(QStringLiteral("address-book-new")), i18n("New entry"), newElementAction); newElementMenu->addAction(newEntry); p->actionCollection()->setDefaultShortcut(newEntry, Qt::CTRL + Qt::SHIFT + Qt::Key_N); connect(newEntry, &QAction::triggered, p, &KBibTeXPart::newEntryTriggered); QAction *newComment = new QAction(QIcon::fromTheme(QStringLiteral("address-book-new")), i18n("New comment"), newElementAction); newElementMenu->addAction(newComment); connect(newComment, &QAction::triggered, p, &KBibTeXPart::newCommentTriggered); QAction *newMacro = new QAction(QIcon::fromTheme(QStringLiteral("address-book-new")), i18n("New macro"), newElementAction); newElementMenu->addAction(newMacro); connect(newMacro, &QAction::triggered, p, &KBibTeXPart::newMacroTriggered); QAction *newPreamble = new QAction(QIcon::fromTheme(QStringLiteral("address-book-new")), i18n("New preamble"), newElementAction); newElementMenu->addAction(newPreamble); connect(newPreamble, &QAction::triggered, p, &KBibTeXPart::newPreambleTriggered); /// Action to edit an element elementEditAction = new QAction(QIcon::fromTheme(QStringLiteral("document-edit")), i18n("Edit Element"), p); p->actionCollection()->addAction(QStringLiteral("element_edit"), elementEditAction); p->actionCollection()->setDefaultShortcut(elementEditAction, Qt::CTRL + Qt::Key_E); connect(elementEditAction, &QAction::triggered, partWidget->fileView(), &FileView::editCurrentElement); /// Action to view the document associated to the current element elementViewDocumentAction = new QAction(QIcon::fromTheme(QStringLiteral("application-pdf")), i18n("View Document"), p); p->actionCollection()->addAction(QStringLiteral("element_viewdocument"), elementViewDocumentAction); p->actionCollection()->setDefaultShortcut(elementViewDocumentAction, Qt::CTRL + Qt::Key_D); connect(elementViewDocumentAction, &QAction::triggered, p, &KBibTeXPart::elementViewDocument); /// Action to find a PDF matching the current element elementFindPDFAction = new QAction(QIcon::fromTheme(QStringLiteral("application-pdf")), i18n("Find PDF..."), p); p->actionCollection()->addAction(QStringLiteral("element_findpdf"), elementFindPDFAction); connect(elementFindPDFAction, &QAction::triggered, p, &KBibTeXPart::elementFindPDF); /// Action to reformat the selected elements' ids entryApplyDefaultFormatString = new QAction(QIcon::fromTheme(QStringLiteral("favorites")), i18n("Format entry ids"), p); p->actionCollection()->addAction(QStringLiteral("entry_applydefaultformatstring"), entryApplyDefaultFormatString); connect(entryApplyDefaultFormatString, &QAction::triggered, p, &KBibTeXPart::applyDefaultFormatString); /// Clipboard object, required for various copy&paste operations Clipboard *clipboard = new Clipboard(partWidget->fileView()); /// Actions to cut and copy selected elements as BibTeX code editCutAction = p->actionCollection()->addAction(KStandardAction::Cut, clipboard, SLOT(cut())); editCopyAction = p->actionCollection()->addAction(KStandardAction::Copy, clipboard, SLOT(copy())); /// Action to copy references, e.g. '\cite{fordfulkerson1959}' editCopyReferencesAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Copy References"), p); p->actionCollection()->setDefaultShortcut(editCopyReferencesAction, Qt::CTRL + Qt::SHIFT + Qt::Key_C); p->actionCollection()->addAction(QStringLiteral("edit_copy_references"), editCopyReferencesAction); connect(editCopyReferencesAction, &QAction::triggered, clipboard, &Clipboard::copyReferences); /// Action to paste BibTeX code editPasteAction = p->actionCollection()->addAction(KStandardAction::Paste, clipboard, SLOT(paste())); /// Action to delete selected rows/elements editDeleteAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-table-delete-row")), i18n("Delete"), p); p->actionCollection()->setDefaultShortcut(editDeleteAction, Qt::Key_Delete); p->actionCollection()->addAction(QStringLiteral("edit_delete"), editDeleteAction); connect(editDeleteAction, &QAction::triggered, partWidget->fileView(), &FileView::selectionDelete); /// Build context menu for central BibTeX file view partWidget->fileView()->setContextMenuPolicy(Qt::ActionsContextMenu); ///< context menu is based on actions partWidget->fileView()->addAction(elementEditAction); partWidget->fileView()->addAction(elementViewDocumentAction); QAction *separator = new QAction(p); separator->setSeparator(true); partWidget->fileView()->addAction(separator); partWidget->fileView()->addAction(editCutAction); partWidget->fileView()->addAction(editCopyAction); partWidget->fileView()->addAction(editCopyReferencesAction); partWidget->fileView()->addAction(editPasteAction); partWidget->fileView()->addAction(editDeleteAction); separator = new QAction(p); separator->setSeparator(true); partWidget->fileView()->addAction(separator); partWidget->fileView()->addAction(elementFindPDFAction); partWidget->fileView()->addAction(entryApplyDefaultFormatString); colorLabelContextMenu = new ColorLabelContextMenu(partWidget->fileView()); colorLabelContextMenuAction = p->actionCollection()->addAction(QStringLiteral("entry_colorlabel"), colorLabelContextMenu->menuAction()); findDuplicatesUI = new FindDuplicatesUI(p, partWidget->fileView()); lyx = new LyX(p, partWidget->fileView()); connect(partWidget->fileView(), &FileView::selectedElementsChanged, p, &KBibTeXPart::updateActions); connect(partWidget->fileView(), &FileView::currentElementChanged, p, &KBibTeXPart::updateActions); } FileImporter *fileImporterFactory(const QUrl &url) { QString ending = url.path().toLower(); const auto pos = ending.lastIndexOf(QStringLiteral(".")); ending = ending.mid(pos + 1); if (ending == QStringLiteral("pdf")) { return new FileImporterPDF(p); } else if (ending == QStringLiteral("ris")) { return new FileImporterRIS(p); } else if (BibUtils::available() && ending == QStringLiteral("isi")) { FileImporterBibUtils *fileImporterBibUtils = new FileImporterBibUtils(p); fileImporterBibUtils->setFormat(BibUtils::ISI); return fileImporterBibUtils; } else { FileImporterBibTeX *fileImporterBibTeX = new FileImporterBibTeX(p); fileImporterBibTeX->setCommentHandling(FileImporterBibTeX::KeepComments); return fileImporterBibTeX; } } FileExporter *fileExporterFactory(const QString &ending) { if (ending == QStringLiteral("html")) { return new FileExporterHTML(p); } else if (ending == QStringLiteral("xml")) { return new FileExporterXML(p); } else if (ending == QStringLiteral("ris")) { return new FileExporterRIS(p); } else if (ending == QStringLiteral("pdf")) { return new FileExporterPDF(p); } else if (ending == QStringLiteral("ps")) { return new FileExporterPS(p); } else if (BibUtils::available() && ending == QStringLiteral("isi")) { FileExporterBibUtils *fileExporterBibUtils = new FileExporterBibUtils(p); fileExporterBibUtils->setFormat(BibUtils::ISI); return fileExporterBibUtils; } else if (ending == QStringLiteral("rtf")) { return new FileExporterRTF(p); } else if (ending == QStringLiteral("html") || ending == QStringLiteral("htm")) { return new FileExporterBibTeX2HTML(p); } else if (ending == QStringLiteral("bbl")) { return new FileExporterBibTeXOutput(FileExporterBibTeXOutput::BibTeXBlockList, p); } else { return new FileExporterBibTeX(p); } } QString findUnusedId() { int i = 1; while (true) { QString result = i18n("New%1", i); if (!bibTeXFile->containsKey(result)) return result; ++i; } } void initializeNew() { bibTeXFile = new File(); model = new FileModel(); model->setBibliographyFile(bibTeXFile); if (sortFilterProxyModel != nullptr) delete sortFilterProxyModel; sortFilterProxyModel = new SortFilterFileModel(p); sortFilterProxyModel->setSourceModel(model); partWidget->fileView()->setModel(sortFilterProxyModel); connect(partWidget->filterBar(), &FilterBar::filterChanged, sortFilterProxyModel, &SortFilterFileModel::updateFilter); } bool openFile(const QUrl &url, const QString &localFilePath) { p->setObjectName("KBibTeXPart::KBibTeXPart for " + url.toDisplayString() + " aka " + localFilePath); qApp->setOverrideCursor(Qt::WaitCursor); if (bibTeXFile != nullptr) { const QUrl oldUrl = bibTeXFile->property(File::Url, QUrl()).toUrl(); if (oldUrl.isValid() && oldUrl.isLocalFile()) { const QString path = oldUrl.toLocalFile(); if (!path.isEmpty()) fileSystemWatcher.removePath(path); else qCWarning(LOG_KBIBTEX_PARTS) << "No filename to stop watching"; } delete bibTeXFile; bibTeXFile = nullptr; } QFile inputfile(localFilePath); if (!inputfile.open(QIODevice::ReadOnly)) { qCWarning(LOG_KBIBTEX_PARTS) << "Opening file failed, creating new one instead:" << url.toDisplayString() << "aka" << localFilePath; qApp->restoreOverrideCursor(); /// Opening file failed, creating new one instead initializeNew(); return false; } FileImporter *importer = fileImporterFactory(url); importer->showImportDialog(p->widget()); bibTeXFile = importer->load(&inputfile); inputfile.close(); delete importer; if (bibTeXFile == nullptr) { qCWarning(LOG_KBIBTEX_PARTS) << "Opening file failed, creating new one instead:" << url.toDisplayString() << "aka" << localFilePath; qApp->restoreOverrideCursor(); /// Opening file failed, creating new one instead initializeNew(); return false; } bibTeXFile->setProperty(File::Url, QUrl(url)); model->setBibliographyFile(bibTeXFile); if (sortFilterProxyModel != nullptr) delete sortFilterProxyModel; sortFilterProxyModel = new SortFilterFileModel(p); sortFilterProxyModel->setSourceModel(model); partWidget->fileView()->setModel(sortFilterProxyModel); connect(partWidget->filterBar(), &FilterBar::filterChanged, sortFilterProxyModel, &SortFilterFileModel::updateFilter); if (url.isLocalFile()) fileSystemWatcher.addPath(url.toLocalFile()); qApp->restoreOverrideCursor(); return true; } void makeBackup(const QUrl &url) const { /// Fetch settings from configuration const int numberOfBackups = Preferences::instance().numberOfBackups(); /// Stop right here if no backup is requested if (Preferences::instance().backupScope() == Preferences::NoBackup) return; /// For non-local files, proceed only if backups to remote storage is allowed if (Preferences::instance().backupScope() != Preferences::BothLocalAndRemote && !url.isLocalFile()) return; /// Do not make backup copies if destination file does not exist yet KIO::StatJob *statJob = KIO::stat(url, KIO::StatJob::DestinationSide, 0 /** not details necessary, just need to know if file exists */, KIO::HideProgressInfo); KJobWidgets::setWindow(statJob, p->widget()); statJob->exec(); if (statJob->error() == KIO::ERR_DOES_NOT_EXIST) return; else if (statJob->error() != KIO::Job::NoError) { /// Something else went wrong, quit with error qCWarning(LOG_KBIBTEX_PARTS) << "Probing" << url.toDisplayString() << "failed:" << statJob->errorString(); return; } bool copySucceeded = true; /// Copy e.g. test.bib~ to test.bib~2, test.bib to test.bib~ etc. for (int level = numberOfBackups; copySucceeded && level >= 1; --level) { QUrl newerBackupUrl = url; constructBackupUrl(level - 1, newerBackupUrl); QUrl olderBackupUrl = url; constructBackupUrl(level, olderBackupUrl); statJob = KIO::stat(newerBackupUrl, KIO::StatJob::DestinationSide, 0 /** not details necessary, just need to know if file exists */, KIO::HideProgressInfo); KJobWidgets::setWindow(statJob, p->widget()); if (statJob->exec() && statJob->error() == KIO::Job::NoError) { KIO::CopyJob *moveJob = nullptr; ///< guaranteed to be initialized in either branch of the following code /** * The following 'if' block is necessary to handle the * following situation: User opens, modifies, and saves * file /tmp/b/bbb.bib which is actually a symlink to * file /tmp/a/aaa.bib. Now a 'move' operation like the * implicit 'else' section below does, would move /tmp/b/bbb.bib * to become /tmp/b/bbb.bib~ still pointing to /tmp/a/aaa.bib. * Then, the save operation would create a new file /tmp/b/bbb.bib * without any symbolic linking to /tmp/a/aaa.bib. * The following code therefore checks if /tmp/b/bbb.bib is * to be copied/moved to /tmp/b/bbb.bib~ and /tmp/b/bbb.bib * is a local file and /tmp/b/bbb.bib is a symbolic link to * another file. Then /tmp/b/bbb.bib is resolved to the real * file /tmp/a/aaa.bib which is then copied into plain file * /tmp/b/bbb.bib~. The save function (outside of this function's * scope) will then see that /tmp/b/bbb.bib is a symbolic link, * resolve this symlink to /tmp/a/aaa.bib, and then write * all changes to /tmp/a/aaa.bib keeping /tmp/b/bbb.bib a * link to. */ if (level == 1 && newerBackupUrl.isLocalFile() /** for level==1, this is actually the current file*/) { QFileInfo newerBackupFileInfo(newerBackupUrl.toLocalFile()); if (newerBackupFileInfo.isSymLink()) { while (newerBackupFileInfo.isSymLink()) { newerBackupUrl = QUrl::fromLocalFile(newerBackupFileInfo.symLinkTarget()); newerBackupFileInfo = QFileInfo(newerBackupUrl.toLocalFile()); } moveJob = KIO::copy(newerBackupUrl, olderBackupUrl, KIO::HideProgressInfo | KIO::Overwrite); } } if (moveJob == nullptr) ///< implicit 'else' section, see longer comment above moveJob = KIO::move(newerBackupUrl, olderBackupUrl, KIO::HideProgressInfo | KIO::Overwrite); KJobWidgets::setWindow(moveJob, p->widget()); copySucceeded = moveJob->exec(); } } if (!copySucceeded) KMessageBox::error(p->widget(), i18n("Could not create backup copies of document '%1'.", url.url(QUrl::PreferLocalFile)), i18n("Backup copies")); } QUrl getSaveFilename(bool mustBeImportable = true) { QString startDir = p->url().isValid() ? p->url().path() : QString(); QString supportedMimeTypes = QStringLiteral("text/x-bibtex text/x-research-info-systems"); if (BibUtils::available()) supportedMimeTypes += QStringLiteral(" application/x-isi-export-format application/x-endnote-refer"); if (!mustBeImportable && !QStandardPaths::findExecutable(QStringLiteral("pdflatex")).isEmpty()) supportedMimeTypes += QStringLiteral(" application/pdf"); if (!mustBeImportable && !QStandardPaths::findExecutable(QStringLiteral("dvips")).isEmpty()) supportedMimeTypes += QStringLiteral(" application/postscript"); if (!mustBeImportable) supportedMimeTypes += QStringLiteral(" text/html"); if (!mustBeImportable && !QStandardPaths::findExecutable(QStringLiteral("latex2rtf")).isEmpty()) supportedMimeTypes += QStringLiteral(" application/rtf"); QPointer<QFileDialog> saveDlg = new QFileDialog(p->widget(), i18n("Save file") /* TODO better text */, startDir, supportedMimeTypes); /// Setting list of mime types for the second time, /// essentially calling this function only to set the "default mime type" parameter saveDlg->setMimeTypeFilters(supportedMimeTypes.split(QLatin1Char(' '), QString::SkipEmptyParts)); /// Setting the dialog into "Saving" mode make the "add extension" checkbox available saveDlg->setAcceptMode(QFileDialog::AcceptSave); saveDlg->setDefaultSuffix(QStringLiteral("bib")); saveDlg->setFileMode(QFileDialog::AnyFile); if (saveDlg->exec() != QDialog::Accepted) /// User cancelled saving operation, return invalid filename/URL return QUrl(); const QList<QUrl> selectedUrls = saveDlg->selectedUrls(); delete saveDlg; return selectedUrls.isEmpty() ? QUrl() : selectedUrls.first(); } FileExporter *saveFileExporter(const QString &ending) { FileExporter *exporter = fileExporterFactory(ending); if (isSaveAsOperation) { /// only show export dialog at SaveAs or SaveCopyAs operations FileExporterToolchain *fet = nullptr; if (FileExporterBibTeX::isFileExporterBibTeX(*exporter)) { QPointer<QDialog> dlg = new QDialog(p->widget()); dlg->setWindowTitle(i18n("BibTeX File Settings")); QBoxLayout *layout = new QVBoxLayout(dlg); FileSettingsWidget *settingsWidget = new FileSettingsWidget(dlg); layout->addWidget(settingsWidget); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::RestoreDefaults | QDialogButtonBox::Reset | QDialogButtonBox::Ok, Qt::Horizontal, dlg); layout->addWidget(buttonBox); connect(buttonBox->button(QDialogButtonBox::RestoreDefaults), &QPushButton::clicked, settingsWidget, &FileSettingsWidget::resetToDefaults); connect(buttonBox->button(QDialogButtonBox::Reset), &QPushButton::clicked, settingsWidget, &FileSettingsWidget::resetToLoadedProperties); connect(buttonBox->button(QDialogButtonBox::Ok), &QPushButton::clicked, dlg.data(), &QDialog::accept); settingsWidget->loadProperties(bibTeXFile); if (dlg->exec() == QDialog::Accepted) settingsWidget->saveProperties(bibTeXFile); delete dlg; } else if ((fet = qobject_cast<FileExporterToolchain *>(exporter)) != nullptr) { QPointer<QDialog> dlg = new QDialog(p->widget()); dlg->setWindowTitle(i18n("PDF/PostScript File Settings")); QBoxLayout *layout = new QVBoxLayout(dlg); SettingsFileExporterPDFPSWidget *settingsWidget = new SettingsFileExporterPDFPSWidget(dlg); layout->addWidget(settingsWidget); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::RestoreDefaults | QDialogButtonBox::Reset | QDialogButtonBox::Ok, Qt::Horizontal, dlg); layout->addWidget(buttonBox); connect(buttonBox->button(QDialogButtonBox::RestoreDefaults), &QPushButton::clicked, settingsWidget, &SettingsFileExporterPDFPSWidget::resetToDefaults); connect(buttonBox->button(QDialogButtonBox::Reset), &QPushButton::clicked, settingsWidget, &SettingsFileExporterPDFPSWidget::loadState); connect(buttonBox->button(QDialogButtonBox::Ok), &QPushButton::clicked, dlg.data(), &QDialog::accept); if (dlg->exec() == QDialog::Accepted) settingsWidget->saveState(); delete dlg; } } return exporter; } bool saveFile(QFile &file, FileExporter *exporter, QStringList *errorLog = nullptr) { SortFilterFileModel *model = qobject_cast<SortFilterFileModel *>(partWidget->fileView()->model()); Q_ASSERT_X(model != nullptr, "FileExporter *KBibTeXPart::KBibTeXPartPrivate:saveFile(...)", "SortFilterFileModel *model from editor->model() is invalid"); return exporter->save(&file, model->fileSourceModel()->bibliographyFile(), errorLog); } bool saveFile(const QUrl &url) { bool result = false; Q_ASSERT_X(!url.isEmpty(), "bool KBibTeXPart::KBibTeXPartPrivate:saveFile(const QUrl &url)", "url is not allowed to be empty"); /// Extract filename extension (e.g. 'bib') to determine which FileExporter to use static const QRegularExpression suffixRegExp(QStringLiteral("\\.([^.]{1,4})$")); const QRegularExpressionMatch suffixRegExpMatch = suffixRegExp.match(url.fileName()); const QString ending = suffixRegExpMatch.hasMatch() ? suffixRegExpMatch.captured(1) : QStringLiteral("bib"); FileExporter *exporter = saveFileExporter(ending); /// String list to collect error message from FileExporer QStringList errorLog; qApp->setOverrideCursor(Qt::WaitCursor); if (url.isLocalFile()) { /// Take precautions for local files QFileInfo fileInfo(url.toLocalFile()); /// Do not overwrite symbolic link, but linked file instead QString filename = fileInfo.absoluteFilePath(); while (fileInfo.isSymLink()) { filename = fileInfo.symLinkTarget(); fileInfo = QFileInfo(filename); } if (!fileInfo.exists() || fileInfo.isWritable()) { /// Make backup before overwriting target destination, intentionally /// using the provided filename, not the resolved symlink makeBackup(url); QFile file(filename); if (file.open(QIODevice::WriteOnly)) { result = saveFile(file, exporter, &errorLog); file.close(); } } } else { /// URL points to a remote location /// Configure and open temporary file QTemporaryFile temporaryFile(QStandardPaths::writableLocation(QStandardPaths::TempLocation) + QDir::separator() + QStringLiteral("kbibtex_savefile_XXXXXX") + ending); temporaryFile.setAutoRemove(true); if (temporaryFile.open()) { result = saveFile(temporaryFile, exporter, &errorLog); /// Close/flush temporary file temporaryFile.close(); if (result) { /// Make backup before overwriting target destination makeBackup(url); KIO::CopyJob *copyJob = KIO::copy(QUrl::fromLocalFile(temporaryFile.fileName()), url, KIO::HideProgressInfo | KIO::Overwrite); KJobWidgets::setWindow(copyJob, p->widget()); result &= copyJob->exec() && copyJob->error() == KIO::Job::NoError; } } } qApp->restoreOverrideCursor(); delete exporter; if (!result) { QString msg = i18n("Saving the bibliography to file '%1' failed.", url.toDisplayString()); if (errorLog.isEmpty()) KMessageBox::error(p->widget(), msg, i18n("Saving bibliography failed")); else { msg += QLatin1String("\n\n"); msg += i18n("The following output was generated by the export filter:"); KMessageBox::errorList(p->widget(), msg, errorLog, i18n("Saving bibliography failed")); } } return result; } /** * Builds or resets the menu with local and remote * references (URLs, files) of an entry. * * @return Number of known references */ int updateViewDocumentMenu() { viewDocumentMenu->clear(); int result = 0; ///< Initially, no references are known File *bibliographyFile = partWidget != nullptr && partWidget->fileView() != nullptr && partWidget->fileView()->fileModel() != nullptr ? partWidget->fileView()->fileModel()->bibliographyFile() : nullptr; if (bibliographyFile == nullptr) return result; /// Retrieve Entry object of currently selected line /// in main list view QSharedPointer<const Entry> entry = partWidget->fileView()->currentElement().dynamicCast<const Entry>(); /// Test and continue if there was an Entry to retrieve if (!entry.isNull()) { /// Get list of URLs associated with this entry const auto urlList = FileInfo::entryUrls(entry, bibliographyFile->property(File::Url).toUrl(), FileInfo::TestExistenceYes); if (!urlList.isEmpty()) { /// Memorize first action, necessary to set menu title QAction *firstAction = nullptr; /// First iteration: local references only for (const QUrl &url : urlList) { /// First iteration: local references only if (!url.isLocalFile()) continue; ///< skip remote URLs /// Build a nice menu item (label, icon, ...) const QFileInfo fi(url.toLocalFile()); const QString label = QString(QStringLiteral("%1 [%2]")).arg(fi.fileName(), fi.absolutePath()); - QMimeDatabase db; - QAction *action = new QAction(QIcon::fromTheme(db.mimeTypeForUrl(url).iconName()), label, p); + QAction *action = new QAction(QIcon::fromTheme(FileInfo::mimeTypeForUrl(url).iconName()), label, p); action->setToolTip(fi.absoluteFilePath()); /// Open URL when action is triggered connect(action, &QAction::triggered, p, [this, fi]() { elementViewDocumentMenu(QUrl::fromLocalFile(fi.absoluteFilePath())); }); viewDocumentMenu->addAction(action); /// Memorize first action if (firstAction == nullptr) firstAction = action; } if (firstAction != nullptr) { /// If there is 'first action', then there must be /// local URLs (i.e. local files) and firstAction /// is the first one where a title can be set above viewDocumentMenu->insertSection(firstAction, i18n("Local Files")); } firstAction = nullptr; /// Now the first remote action is to be memorized /// Second iteration: remote references only for (const QUrl &url : urlList) { if (url.isLocalFile()) continue; ///< skip local files /// Build a nice menu item (label, icon, ...) const QString prettyUrl = url.toDisplayString(); - QMimeDatabase db; - QAction *action = new QAction(QIcon::fromTheme(db.mimeTypeForUrl(url).iconName()), prettyUrl, p); + QAction *action = new QAction(QIcon::fromTheme(FileInfo::mimeTypeForUrl(url).iconName()), prettyUrl, p); action->setToolTip(prettyUrl); /// Open URL when action is triggered connect(action, &QAction::triggered, p, [this, url]() { elementViewDocumentMenu(url); }); viewDocumentMenu->addAction(action); /// Memorize first action if (firstAction == nullptr) firstAction = action; } if (firstAction != nullptr) { /// If there is 'first action', then there must be /// some remote URLs and firstAction is the first /// one where a title can be set above viewDocumentMenu->insertSection(firstAction, i18n("Remote Files")); } result = urlList.count(); } } return result; } void readConfiguration() { disconnect(partWidget->fileView(), &FileView::elementExecuted, partWidget->fileView(), &FileView::editElement); disconnect(partWidget->fileView(), &FileView::elementExecuted, p, &KBibTeXPart::elementViewDocument); switch (Preferences::instance().fileViewDoubleClickAction()) { case Preferences::ActionOpenEditor: connect(partWidget->fileView(), &FileView::elementExecuted, partWidget->fileView(), &FileView::editElement); break; case Preferences::ActionViewDocument: connect(partWidget->fileView(), &FileView::elementExecuted, p, &KBibTeXPart::elementViewDocument); break; } } void elementViewDocumentMenu(const QUrl &url) { const QMimeType mimeType = FileInfo::mimeTypeForUrl(url); const QString mimeTypeName = mimeType.name(); /// Ask KDE subsystem to open url in viewer matching mime type KRun::runUrl(url, mimeTypeName, p->widget(), KRun::RunFlags()); } }; KBibTeXPart::KBibTeXPart(QWidget *parentWidget, QObject *parent, const KAboutData &componentData) : KParts::ReadWritePart(parent), d(new KBibTeXPartPrivate(parentWidget, this)) { setComponentData(componentData); setWidget(d->partWidget); updateActions(); d->initializeNew(); setXMLFile(RCFileName); NotificationHub::registerNotificationListener(this, NotificationHub::EventConfigurationChanged); d->readConfiguration(); setModified(false); } KBibTeXPart::~KBibTeXPart() { delete d; } void KBibTeXPart::setModified(bool modified) { KParts::ReadWritePart::setModified(modified); d->fileSaveAction->setEnabled(modified); } void KBibTeXPart::notificationEvent(int eventId) { if (eventId == NotificationHub::EventConfigurationChanged) d->readConfiguration(); } bool KBibTeXPart::saveFile() { Q_ASSERT_X(isReadWrite(), "bool KBibTeXPart::saveFile()", "Trying to save although document is in read-only mode"); if (url().isEmpty()) return documentSaveAs(); /// If the current file is "watchable" (i.e. a local file), /// memorize local filename for future reference const QString watchableFilename = url().isValid() && url().isLocalFile() ? url().toLocalFile() : QString(); /// Stop watching local file that will be written to if (!watchableFilename.isEmpty()) d->fileSystemWatcher.removePath(watchableFilename); else qCWarning(LOG_KBIBTEX_PARTS) << "watchableFilename is Empty"; const bool saveOperationSuccess = d->saveFile(url()); if (!watchableFilename.isEmpty()) { /// Continue watching a local file after write operation, but do /// so only after a short delay. The delay is necessary in some /// situations as observed in KDE bug report 396343 where the /// DropBox client seemingly touched the file right after saving /// from within KBibTeX, triggering KBibTeX to show a 'reload' /// message box. QTimer::singleShot(500, this, [this, watchableFilename]() { d->fileSystemWatcher.addPath(watchableFilename); }); } else qCWarning(LOG_KBIBTEX_PARTS) << "watchableFilename is Empty"; if (!saveOperationSuccess) { KMessageBox::error(widget(), i18n("The document could not be saved, as it was not possible to write to '%1'.\n\nCheck that you have write access to this file or that enough disk space is available.", url().toDisplayString())); return false; } return true; } bool KBibTeXPart::documentSave() { d->isSaveAsOperation = false; if (!isReadWrite()) return documentSaveCopyAs(); else if (!url().isValid()) return documentSaveAs(); else return KParts::ReadWritePart::save(); } bool KBibTeXPart::documentSaveAs() { d->isSaveAsOperation = true; QUrl newUrl = d->getSaveFilename(); if (!newUrl.isValid()) return false; /// Remove old URL from file system watcher if (url().isValid() && url().isLocalFile()) { const QString path = url().toLocalFile(); if (!path.isEmpty()) d->fileSystemWatcher.removePath(path); else qCWarning(LOG_KBIBTEX_PARTS) << "No filename to stop watching"; } else qCWarning(LOG_KBIBTEX_PARTS) << "Not removing" << url().url(QUrl::PreferLocalFile) << "from fileSystemWatcher"; // TODO how does SaveAs dialog know which mime types to support? if (KParts::ReadWritePart::saveAs(newUrl)) { // FIXME d->model->bibliographyFile()->setProperty(File::Url, newUrl); return true; } else return false; } bool KBibTeXPart::documentSaveCopyAs() { d->isSaveAsOperation = true; QUrl newUrl = d->getSaveFilename(false); if (!newUrl.isValid() || newUrl == url()) return false; /// difference from KParts::ReadWritePart::saveAs: /// current document's URL won't be changed return d->saveFile(newUrl); } void KBibTeXPart::elementViewDocument() { QUrl url; const QList<QAction *> actionList = d->viewDocumentMenu->actions(); /// Go through all actions (i.e. document URLs) for this element for (const QAction *action : actionList) { /// Make URL from action's data ... QUrl tmpUrl = QUrl(action->data().toString()); /// ... but skip this action if the URL is invalid if (!tmpUrl.isValid()) continue; if (tmpUrl.isLocalFile()) { /// If action's URL points to local file, /// keep it and stop search for document url = tmpUrl; break; } else if (!url.isValid()) /// First valid URL found, keep it /// URL is not local, so it may get overwritten by another URL url = tmpUrl; } /// Open selected URL if (url.isValid()) { /// Guess mime type for url to open QMimeType mimeType = FileInfo::mimeTypeForUrl(url); const QString mimeTypeName = mimeType.name(); /// Ask KDE subsystem to open url in viewer matching mime type KRun::runUrl(url, mimeTypeName, widget(), KRun::RunFlags()); } } void KBibTeXPart::elementFindPDF() { QModelIndexList mil = d->partWidget->fileView()->selectionModel()->selectedRows(); if (mil.count() == 1) { QSharedPointer<Entry> entry = d->partWidget->fileView()->fileModel()->element(d->partWidget->fileView()->sortFilterProxyModel()->mapToSource(*mil.constBegin()).row()).dynamicCast<Entry>(); if (!entry.isNull()) FindPDFUI::interactiveFindPDF(*entry, *d->bibTeXFile, widget()); } } void KBibTeXPart::applyDefaultFormatString() { FileModel *model = d->partWidget != nullptr && d->partWidget->fileView() != nullptr ? d->partWidget->fileView()->fileModel() : nullptr; if (model == nullptr) return; bool documentModified = false; const QModelIndexList mil = d->partWidget->fileView()->selectionModel()->selectedRows(); for (const QModelIndex &index : mil) { QSharedPointer<Entry> entry = model->element(d->partWidget->fileView()->sortFilterProxyModel()->mapToSource(index).row()).dynamicCast<Entry>(); if (!entry.isNull()) { static IdSuggestions idSuggestions; bool success = idSuggestions.applyDefaultFormatId(*entry.data()); documentModified |= success; if (!success) { KMessageBox::information(widget(), i18n("Cannot apply default formatting for entry ids: No default format specified."), i18n("Cannot Apply Default Formatting")); break; } } } if (documentModified) d->partWidget->fileView()->externalModification(); } bool KBibTeXPart::openFile() { const bool success = d->openFile(url(), localFilePath()); emit completed(); return success; } void KBibTeXPart::newEntryTriggered() { QSharedPointer<Entry> newEntry = QSharedPointer<Entry>(new Entry(QStringLiteral("Article"), d->findUnusedId())); d->model->insertRow(newEntry, d->model->rowCount()); d->partWidget->fileView()->setSelectedElement(newEntry); if (d->partWidget->fileView()->editElement(newEntry)) d->partWidget->fileView()->scrollToBottom(); // FIXME always correct behaviour? else { /// Editing this new element was cancelled, /// therefore remove it again d->model->removeRow(d->model->rowCount() - 1); } } void KBibTeXPart::newMacroTriggered() { QSharedPointer<Macro> newMacro = QSharedPointer<Macro>(new Macro(d->findUnusedId())); d->model->insertRow(newMacro, d->model->rowCount()); d->partWidget->fileView()->setSelectedElement(newMacro); if (d->partWidget->fileView()->editElement(newMacro)) d->partWidget->fileView()->scrollToBottom(); // FIXME always correct behaviour? else { /// Editing this new element was cancelled, /// therefore remove it again d->model->removeRow(d->model->rowCount() - 1); } } void KBibTeXPart::newPreambleTriggered() { QSharedPointer<Preamble> newPreamble = QSharedPointer<Preamble>(new Preamble()); d->model->insertRow(newPreamble, d->model->rowCount()); d->partWidget->fileView()->setSelectedElement(newPreamble); if (d->partWidget->fileView()->editElement(newPreamble)) d->partWidget->fileView()->scrollToBottom(); // FIXME always correct behaviour? else { /// Editing this new element was cancelled, /// therefore remove it again d->model->removeRow(d->model->rowCount() - 1); } } void KBibTeXPart::newCommentTriggered() { QSharedPointer<Comment> newComment = QSharedPointer<Comment>(new Comment()); d->model->insertRow(newComment, d->model->rowCount()); d->partWidget->fileView()->setSelectedElement(newComment); if (d->partWidget->fileView()->editElement(newComment)) d->partWidget->fileView()->scrollToBottom(); // FIXME always correct behaviour? else { /// Editing this new element was cancelled, /// therefore remove it again d->model->removeRow(d->model->rowCount() - 1); } } void KBibTeXPart::updateActions() { FileModel *model = d->partWidget != nullptr && d->partWidget->fileView() != nullptr ? d->partWidget->fileView()->fileModel() : nullptr; if (model == nullptr) return; bool emptySelection = d->partWidget->fileView()->selectedElements().isEmpty(); d->elementEditAction->setEnabled(!emptySelection); d->editCopyAction->setEnabled(!emptySelection); d->editCopyReferencesAction->setEnabled(!emptySelection); d->editCutAction->setEnabled(!emptySelection && isReadWrite()); d->editPasteAction->setEnabled(isReadWrite()); d->editDeleteAction->setEnabled(!emptySelection && isReadWrite()); d->elementFindPDFAction->setEnabled(!emptySelection && isReadWrite()); d->entryApplyDefaultFormatString->setEnabled(!emptySelection && isReadWrite()); d->colorLabelContextMenu->menuAction()->setEnabled(!emptySelection && isReadWrite()); d->colorLabelContextMenuAction->setEnabled(!emptySelection && isReadWrite()); int numDocumentsToView = d->updateViewDocumentMenu(); /// enable menu item only if there is at least one document to view d->elementViewDocumentAction->setEnabled(!emptySelection && numDocumentsToView > 0); /// activate sub-menu only if there are at least two documents to view d->elementViewDocumentAction->setMenu(numDocumentsToView > 1 ? d->viewDocumentMenu : nullptr); d->elementViewDocumentAction->setToolTip(numDocumentsToView == 1 ? (*d->viewDocumentMenu->actions().constBegin())->text() : QString()); /// update list of references which can be sent to LyX QStringList references; if (d->partWidget->fileView()->selectionModel() != nullptr) { const QModelIndexList mil = d->partWidget->fileView()->selectionModel()->selectedRows(); references.reserve(mil.size()); for (const QModelIndex &index : mil) { const QSharedPointer<Entry> entry = model->element(d->partWidget->fileView()->sortFilterProxyModel()->mapToSource(index).row()).dynamicCast<Entry>(); if (!entry.isNull()) references << entry->id(); } } d->lyx->setReferences(references); } void KBibTeXPart::fileExternallyChange(const QString &path) { /// Should never happen: triggering this slot for non-local or invalid URLs if (!url().isValid() || !url().isLocalFile()) return; /// Should never happen: triggering this slot for filenames not being the opened file if (path != url().toLocalFile()) { qCWarning(LOG_KBIBTEX_PARTS) << "Got file modification warning for wrong file: " << path << "!=" << url().toLocalFile(); return; } /// Stop watching file while asking for user interaction if (!path.isEmpty()) d->fileSystemWatcher.removePath(path); else qCWarning(LOG_KBIBTEX_PARTS) << "No filename to stop watching"; if (KMessageBox::warningContinueCancel(widget(), i18n("The file '%1' has changed on disk.\n\nReload file or ignore changes on disk?", path), i18n("File changed externally"), KGuiItem(i18n("Reload file"), QIcon::fromTheme(QStringLiteral("edit-redo"))), KGuiItem(i18n("Ignore on-disk changes"), QIcon::fromTheme(QStringLiteral("edit-undo")))) == KMessageBox::Continue) { d->openFile(QUrl::fromLocalFile(path), path); /// No explicit call to QFileSystemWatcher.addPath(...) necessary, /// openFile(...) has done that already } else { /// Even if the user did not request reloaded the file, /// still resume watching file for future external changes if (!path.isEmpty()) d->fileSystemWatcher.addPath(path); else qCWarning(LOG_KBIBTEX_PARTS) << "path is Empty"; } } #include "part.moc" diff --git a/src/program/docklets/documentpreview.cpp b/src/program/docklets/documentpreview.cpp index c21a9108..85d28119 100644 --- a/src/program/docklets/documentpreview.cpp +++ b/src/program/docklets/documentpreview.cpp @@ -1,709 +1,701 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, see <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "documentpreview.h" #include <typeinfo> #include <QDomDocument> #include <QDomElement> #include <QList> #include <QLayout> #include <QMap> #include <QFileInfo> #include <QResizeEvent> #include <QCheckBox> #include <QMenuBar> #include <QStackedWidget> #include <QDockWidget> #include <QDebug> #include <QPushButton> #include <QComboBox> #include <QMutex> -#include <QMimeDatabase> #include <QMimeType> #include <QIcon> #ifdef HAVE_WEBENGINEWIDGETS #include <QWebEngineView> #else // HAVE_WEBENGINEWIDGETS #ifdef HAVE_WEBKITWIDGETS #include <QWebView> #endif // HAVE_WEBKITWIDGETS #endif // HAVE_WEBENGINEWIDGETS #include <KLocalizedString> #include <KJobWidgets> #include <KRun> #include <KMimeTypeTrader> #include <KService> #include <KParts/Part> #include <KParts/ReadOnlyPart> #include <kio/jobclasses.h> #include <kio/job.h> #include <kio/jobuidelegate.h> #include <KToolBar> #include <KActionCollection> #include <KSharedConfig> #include <KConfigGroup> #include <kio_version.h> #include <KBibTeX> #include <Element> #include <Entry> #include <File> #include <FileInfo> #include "logging_program.h" ImageLabel::ImageLabel(const QString &text, QWidget *parent, Qt::WindowFlags f) : QLabel(text, parent, f) { /// nothing } void ImageLabel::setPixmap(const QPixmap &pixmap) { m_pixmap = pixmap; if (!m_pixmap.isNull()) { setCursor(Qt::WaitCursor); QPixmap scaledPixmap = m_pixmap.width() <= width() && m_pixmap.height() <= height() ? m_pixmap : pixmap.scaled(width(), height(), Qt::KeepAspectRatio, Qt::SmoothTransformation); QLabel::setPixmap(scaledPixmap); setMinimumSize(100, 100); unsetCursor(); } else QLabel::setPixmap(m_pixmap); } void ImageLabel::resizeEvent(QResizeEvent *event) { QLabel::resizeEvent(event); if (!m_pixmap.isNull()) { setCursor(Qt::WaitCursor); QPixmap scaledPixmap = m_pixmap.width() <= event->size().width() && m_pixmap.height() <= event->size().height() ? m_pixmap : m_pixmap.scaled(event->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation); QLabel::setPixmap(scaledPixmap); setMinimumSize(100, 100); unsetCursor(); } } class DocumentPreview::DocumentPreviewPrivate { public: struct UrlInfo { QUrl url; QString mimeType; QIcon icon; }; private: DocumentPreview *p; KSharedConfigPtr config; static const QString configGroupName; static const QString onlyLocalFilesCheckConfig; QPushButton *externalViewerButton; QStackedWidget *stackedWidget; ImageLabel *message; QMap<int, struct UrlInfo> cbxEntryToUrlInfo; QMutex addingUrlMutex; static const QString arXivPDFUrlStart; bool anyLocal; QMenuBar *menuBar; KToolBar *toolBar; KParts::ReadOnlyPart *okularPart; #ifdef HAVE_WEBENGINEWIDGETS QWebEngineView *htmlWidget; #else // HAVE_WEBENGINEWIDGETS #ifdef HAVE_WEBKITWIDGETS QWebView *htmlWidget; #else // HAVE_WEBKITWIDGETS KParts::ReadOnlyPart *htmlPart; #endif // HAVE_WEBKITWIDGETS #endif // HAVE_WEBENGINEWIDGETS int swpMessage, swpOkular, swpHTML; public: QComboBox *urlComboBox; QPushButton *onlyLocalFilesButton; QList<KIO::StatJob *> runningJobs; QSharedPointer<const Entry> entry; QUrl baseUrl; bool anyRemote; KParts::ReadOnlyPart *locatePart(const QString &mimeType, QWidget *parentWidget) { KService::Ptr service = KMimeTypeTrader::self()->preferredService(mimeType, QStringLiteral("KParts/ReadOnlyPart")); if (service) { KParts::ReadOnlyPart *part = service->createInstance<KParts::ReadOnlyPart>(parentWidget, p); connect(part, static_cast<void(KParts::ReadOnlyPart::*)()>(&KParts::ReadOnlyPart::completed), p, &DocumentPreview::loadingFinished); return part; } else return nullptr; } DocumentPreviewPrivate(DocumentPreview *parent) : p(parent), config(KSharedConfig::openConfig(QStringLiteral("kbibtexrc"))), anyLocal(false), entry(nullptr), anyRemote(false) { setupGUI(); } /** * Create user interface for this widget. * It consists of some controlling widget on the top, * but the most space is consumed by KPart widgets * inside a QStackedWidget to show the external content * (PDF file, web page, ...). */ void setupGUI() { QVBoxLayout *layout = new QVBoxLayout(p); layout->setMargin(0); /// some widgets on the top to control the view QHBoxLayout *innerLayout = new QHBoxLayout(); layout->addLayout(innerLayout, 0); onlyLocalFilesButton = new QPushButton(QIcon::fromTheme(QStringLiteral("applications-internet")), QString(), p); onlyLocalFilesButton->setToolTip(i18n("Toggle between local files only and all documents including remote ones")); innerLayout->addWidget(onlyLocalFilesButton, 0); onlyLocalFilesButton->setCheckable(true); QSizePolicy sp = onlyLocalFilesButton->sizePolicy(); sp.setVerticalPolicy(QSizePolicy::MinimumExpanding); onlyLocalFilesButton->setSizePolicy(sp); urlComboBox = new QComboBox(p); innerLayout->addWidget(urlComboBox, 1); externalViewerButton = new QPushButton(QIcon::fromTheme(QStringLiteral("document-open")), QString(), p); externalViewerButton->setToolTip(i18n("Open in external program")); innerLayout->addWidget(externalViewerButton, 0); sp = externalViewerButton->sizePolicy(); sp.setVerticalPolicy(QSizePolicy::MinimumExpanding); externalViewerButton->setSizePolicy(sp); menuBar = new QMenuBar(p); menuBar->setBackgroundRole(QPalette::Window); menuBar->setVisible(false); layout->addWidget(menuBar, 0); toolBar = new KToolBar(p); toolBar->setToolButtonStyle(Qt::ToolButtonIconOnly); toolBar->setBackgroundRole(QPalette::Window); toolBar->setVisible(false); layout->addWidget(toolBar, 0); /// main part of the widget stackedWidget = new QStackedWidget(p); layout->addWidget(stackedWidget, 1); /// default widget if no preview is available message = new ImageLabel(i18n("No preview available"), stackedWidget); message->setAlignment(Qt::AlignCenter); message->setWordWrap(true); swpMessage = stackedWidget->addWidget(message); connect(message, &QLabel::linkActivated, p, &DocumentPreview::linkActivated); /// add parts to stackedWidget okularPart = locatePart(QStringLiteral("application/pdf"), stackedWidget); swpOkular = (okularPart == nullptr) ? -1 : stackedWidget->addWidget(okularPart->widget()); if (okularPart == nullptr || swpOkular < 0) { qCWarning(LOG_KBIBTEX_PROGRAM) << "No 'KDE Framworks 5'-based Okular part for PDF or PostScript document preview available."; } #ifdef HAVE_WEBENGINEWIDGETS qCDebug(LOG_KBIBTEX_PROGRAM) << "WebEngine is available, using it instead of WebKit or HTML KPart (both neither considered nor tested for) for HTML/Web preview."; /// To make DrKonqi handle crashes in Chromium-based QtWebEngine, /// set a certain environment variable. For details, see here: /// https://www.dvratil.cz/2018/10/drkonqi-and-qtwebengine/ /// https://phabricator.kde.org/D16004 const auto chromiumFlags = qgetenv("QTWEBENGINE_CHROMIUM_FLAGS"); if (!chromiumFlags.contains("disable-in-process-stack-traces")) { qputenv("QTWEBENGINE_CHROMIUM_FLAGS", chromiumFlags + " --disable-in-process-stack-traces"); } htmlWidget = new QWebEngineView(stackedWidget); swpHTML = stackedWidget->addWidget(htmlWidget); connect(htmlWidget, &QWebEngineView::loadFinished, p, &DocumentPreview::loadingFinished); #else // HAVE_WEBENGINEWIDGETS #ifdef HAVE_WEBKITWIDGETS qCDebug(LOG_KBIBTEX_PROGRAM) << "WebKit is available, using it instead of WebEngine (missing) or HTML KPart (not considered) for HTML/Web preview."; htmlWidget = new QWebView(stackedWidget); swpHTML = stackedWidget->addWidget(htmlWidget); connect(htmlWidget, &QWebView::loadFinished, p, &DocumentPreview::loadingFinished); #else // HAVE_WEBKITWIDGETS htmlPart = locatePart(QStringLiteral("text/html"), stackedWidget); if (htmlPart != nullptr) { qCDebug(LOG_KBIBTEX_PROGRAM) << "HTML KPart is available, using it instead of WebEngine or WebKit (neither available) for HTML/Web preview."; swpHTML = stackedWidget->addWidget(htmlPart->widget()); } else { qCDebug(LOG_KBIBTEX_PROGRAM) << "No HTML viewing component is available, disabling HTML/Web preview."; swpHTML = -1; } #endif // HAVE_WEBKITWIDGETS #endif // HAVE_WEBENGINEWIDGETS loadState(); connect(externalViewerButton, &QPushButton::clicked, p, &DocumentPreview::openExternally); connect(urlComboBox, static_cast<void(QComboBox::*)(int)>(&QComboBox::activated), p, &DocumentPreview::comboBoxChanged); connect(onlyLocalFilesButton, &QPushButton::toggled, p, &DocumentPreview::onlyLocalFilesChanged); } bool addUrl(const struct UrlInfo &urlInfo) { bool isLocal = KBibTeX::isLocalOrRelative(urlInfo.url); anyLocal |= isLocal; if (!onlyLocalFilesButton->isChecked() && !isLocal) return true; ///< ignore URL if only local files are allowed if (isLocal) { /// create a drop-down list entry if file is a local file /// (based on patch by Luis Silva) QString fn = urlInfo.url.fileName(); QString full = urlInfo.url.url(QUrl::PreferLocalFile); QString dir = urlInfo.url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path(); QString text = fn.isEmpty() ? full : (dir.isEmpty() ? fn : QString(QStringLiteral("%1 [%2]")).arg(fn, dir)); urlComboBox->addItem(urlInfo.icon, text); } else { /// create a drop-down list entry if file is a remote file urlComboBox->addItem(urlInfo.icon, urlInfo.url.toDisplayString()); } urlComboBox->setEnabled(true); cbxEntryToUrlInfo.insert(urlComboBox->count() - 1, urlInfo); externalViewerButton->setEnabled(true); if (urlComboBox->count() == 1 || ///< first entry in combobox isLocal || ///< local files always preferred over URLs /// prefer arXiv summary URLs over other URLs (!anyLocal && urlInfo.url.host().contains(QStringLiteral("arxiv.org/abs")))) { showUrl(urlInfo); } return true; } void update() { p->setCursor(Qt::WaitCursor); /// reset and clear all controls if (swpOkular >= 0 && okularPart != nullptr) okularPart->closeUrl(); #ifdef HAVE_WEBENGINEWIDGETS htmlWidget->stop(); #else // HAVE_WEBENGINEWIDGETS #ifdef HAVE_WEBKITWIDGETS htmlWidget->stop(); #else // HAVE_WEBKITWIDGETS if (swpHTML >= 0 && htmlPart != nullptr) htmlPart->closeUrl(); #endif // HAVE_WEBKITWIDGETS #endif // HAVE_WEBENGINEWIDGETS urlComboBox->setEnabled(false); urlComboBox->clear(); cbxEntryToUrlInfo.clear(); externalViewerButton->setEnabled(false); showMessage(i18n("Refreshing...")); // krazy:exclude=qmethods /// cancel/kill all running jobs auto it = runningJobs.begin(); while (it != runningJobs.end()) { (*it)->kill(); it = runningJobs.erase(it); } /// clear flag that memorizes if any local file was referenced anyLocal = false; anyRemote = false; /// do not load external reference if widget is hidden if (isVisible()) { const auto urlList = FileInfo::entryUrls(entry, baseUrl, FileInfo::TestExistenceYes); for (const QUrl &url : urlList) { bool isLocal = KBibTeX::isLocalOrRelative(url); anyRemote |= !isLocal; if (!onlyLocalFilesButton->isChecked() && !isLocal) continue; KIO::StatJob *job = KIO::stat(url, KIO::StatJob::SourceSide, 3, KIO::HideProgressInfo); runningJobs << job; KJobWidgets::setWindow(job, p); connect(job, &KIO::StatJob::result, p, &DocumentPreview::statFinished); } if (urlList.isEmpty()) { /// Case no URLs associated with this entry. /// For-loop above was never executed. showMessage(i18n("No documents to show.")); // krazy:exclude=qmethods p->setCursor(Qt::ArrowCursor); } else if (runningJobs.isEmpty()) { /// Case no stat jobs are running. As there were URLs (tested in /// previous condition), this implies that there were remote /// references that were ignored by executing "continue" above. /// Give user hint that by enabling remote files, more can be shown. showMessage(i18n("<qt>No documents to show.<br/><a href=\"disableonlylocalfiles\">Disable the restriction</a> to local files to see remote documents.</qt>")); // krazy:exclude=qmethods p->setCursor(Qt::ArrowCursor); } } else p->setCursor(Qt::ArrowCursor); } void showMessage(const QString &msgText) { stackedWidget->setCurrentIndex(swpMessage); message->setPixmap(QPixmap()); message->setText(msgText); if (swpOkular >= 0) stackedWidget->widget(swpOkular)->setEnabled(false); if (swpHTML >= 0) stackedWidget->widget(swpHTML)->setEnabled(false); menuBar->setVisible(false); toolBar->setVisible(true); menuBar->clear(); toolBar->clear(); } void setupToolMenuBarForPart(const KParts::ReadOnlyPart *part) { /* KAction *printAction = KStandardAction::print(part, SLOT(slotPrint()), part->actionCollection()); printAction->setEnabled(false); connect(part, SIGNAL(enablePrintAction(bool)), printAction, SLOT(setEnabled(bool))); */ QDomDocument doc = part->domDocument(); QDomElement docElem = doc.documentElement(); QDomNodeList toolbarNodes = docElem.elementsByTagName(QStringLiteral("ToolBar")); for (int i = 0; i < toolbarNodes.count(); ++i) { QDomNodeList toolbarItems = toolbarNodes.at(i).childNodes(); for (int j = 0; j < toolbarItems.count(); ++j) { QDomNode toolbarItem = toolbarItems.at(j); if (toolbarItem.nodeName() == QStringLiteral("Action")) { QString actionName = toolbarItem.attributes().namedItem(QStringLiteral("name")).nodeValue(); toolBar->addAction(part->actionCollection()->action(actionName)); } else if (toolbarItem.nodeName() == QStringLiteral("Separator")) { toolBar->addSeparator(); } } } QDomNodeList menubarNodes = docElem.elementsByTagName(QStringLiteral("MenuBar")); for (int i = 0; i < menubarNodes.count(); ++i) { QDomNodeList menubarNode = menubarNodes.at(i).childNodes(); for (int j = 0; j < menubarNode.count(); ++j) { QDomNode menubarItem = menubarNode.at(j); if (menubarItem.nodeName() == QStringLiteral("Menu")) { QDomNodeList menuNode = menubarItem.childNodes(); QString text; for (int k = 0; k < menuNode.count(); ++k) { QDomNode menuItem = menuNode.at(k); if (menuItem.nodeName() == QStringLiteral("text")) { text = menuItem.firstChild().toText().data(); break; } } QMenu *menu = menuBar->addMenu(text); for (int k = 0; k < menuNode.count(); ++k) { QDomNode menuItem = menuNode.at(k); if (menuItem.nodeName() == QStringLiteral("Action")) { QString actionName = menuItem.attributes().namedItem(QStringLiteral("name")).nodeValue(); menu->addAction(part->actionCollection()->action(actionName)); } else if (menuItem.nodeName() == QStringLiteral("Separator")) { menu->addSeparator(); } } } } } QDomNodeList actionPropertiesList = docElem.elementsByTagName(QStringLiteral("ActionProperties")); for (int i = 0; i < actionPropertiesList.count(); ++i) { QDomNodeList actionProperties = actionPropertiesList.at(i).childNodes(); for (int j = 0; j < actionProperties.count(); ++j) { QDomNode actionNode = actionProperties.at(j); if (actionNode.nodeName() == QStringLiteral("Action")) { const QString actionName = actionNode.attributes().namedItem(QStringLiteral("name")).toAttr().nodeValue(); const QString actionShortcut = actionNode.attributes().namedItem(QStringLiteral("shortcut")).toAttr().value(); QAction *action = part->actionCollection()->action(actionName); if (action != nullptr) { action->setShortcut(QKeySequence(actionShortcut)); } } } } menuBar->setVisible(true); toolBar->setVisible(true); } void showPart(const KParts::ReadOnlyPart *part, QWidget *widget) { menuBar->setVisible(false); toolBar->setVisible(false); menuBar->clear(); toolBar->clear(); if (okularPart != nullptr && part == okularPart && swpOkular >= 0) { stackedWidget->setCurrentIndex(swpOkular); stackedWidget->widget(swpOkular)->setEnabled(true); setupToolMenuBarForPart(okularPart); #ifdef HAVE_WEBENGINEWIDGETS } else if (widget == htmlWidget) { stackedWidget->setCurrentIndex(swpHTML); stackedWidget->widget(swpHTML)->setEnabled(true); #else // HAVE_WEBENGINEWIDGETS #ifdef HAVE_WEBKITWIDGETS } else if (widget == htmlWidget) { stackedWidget->setCurrentIndex(swpHTML); stackedWidget->widget(swpHTML)->setEnabled(true); #else // HAVE_WEBKITWIDGETS } else if (htmlPart != nullptr && part == htmlPart && swpHTML >= 0) { stackedWidget->setCurrentIndex(swpHTML); stackedWidget->widget(swpHTML)->setEnabled(true); setupToolMenuBarForPart(htmlPart); #endif // HAVE_WEBKITWIDGETS #endif // HAVE_WEBENGINEWIDGETS } else if (widget == message) { stackedWidget->setCurrentIndex(swpMessage); } else showMessage(i18n("Cannot show requested part")); // krazy:exclude=qmethods } bool showUrl(const struct UrlInfo &urlInfo) { static const QStringList okularMimetypes {QStringLiteral("application/x-pdf"), QStringLiteral("application/pdf"), QStringLiteral("application/x-gzpdf"), QStringLiteral("application/x-bzpdf"), QStringLiteral("application/x-wwf"), QStringLiteral("image/vnd.djvu"), QStringLiteral("image/vnd.djvu+multipage"), QStringLiteral("application/postscript"), QStringLiteral("image/x-eps"), QStringLiteral("application/x-gzpostscript"), QStringLiteral("application/x-bzpostscript"), QStringLiteral("image/x-gzeps"), QStringLiteral("image/x-bzeps")}; static const QStringList htmlMimetypes {QStringLiteral("text/html"), QStringLiteral("application/xml"), QStringLiteral("application/xhtml+xml")}; static const QStringList imageMimetypes {QStringLiteral("image/jpeg"), QStringLiteral("image/png"), QStringLiteral("image/gif"), QStringLiteral("image/tiff")}; if (swpHTML >= 0) stackedWidget->widget(swpHTML)->setEnabled(false); if (swpOkular >= 0 && okularPart != nullptr) { stackedWidget->widget(swpOkular)->setEnabled(false); okularPart->closeUrl(); } #ifdef HAVE_WEBENGINEWIDGETS htmlWidget->stop(); #else // HAVE_WEBENGINEWIDGETS #ifdef HAVE_WEBKITWIDGETS htmlWidget->stop(); #else // HAVE_WEBKITWIDGETS if (swpHTML >= 0 && htmlPart != nullptr) htmlPart->closeUrl(); #endif // HAVE_WEBKITWIDGETS #endif // HAVE_WEBENGINEWIDGETS if (swpOkular >= 0 && okularPart != nullptr && okularMimetypes.contains(urlInfo.mimeType)) { p->setCursor(Qt::BusyCursor); showMessage(i18n("Loading...")); // krazy:exclude=qmethods return okularPart->openUrl(urlInfo.url); } else if (htmlMimetypes.contains(urlInfo.mimeType)) { p->setCursor(Qt::BusyCursor); showMessage(i18n("Loading...")); // krazy:exclude=qmethods #ifdef HAVE_WEBENGINEWIDGETS htmlWidget->load(urlInfo.url); return true; #else // HAVE_WEBENGINEWIDGETS #ifdef HAVE_WEBKITWIDGETS htmlWidget->load(urlInfo.url); return true; #else // HAVE_WEBKITWIDGETS return (swpHTML >= 0 && htmlPart != nullptr) ? htmlPart->openUrl(urlInfo.url) : false; #endif // HAVE_WEBKITWIDGETS #endif // HAVE_WEBENGINEWIDGETS } else if (imageMimetypes.contains(urlInfo.mimeType)) { p->setCursor(Qt::BusyCursor); message->setPixmap(QPixmap(urlInfo.url.url(QUrl::PreferLocalFile))); showPart(nullptr, message); p->unsetCursor(); return true; } else { QString additionalInformation; if (urlInfo.mimeType == QStringLiteral("application/pdf")) additionalInformation = i18nc("Additional information in case there is not KPart available for mime type 'application/pdf'", "<br/><br/>Please install <a href=\"https://userbase.kde.org/Okular\">Okular</a> for KDE Frameworks&nbsp;5 to make use of its PDF viewing component.<br/>Okular for KDE&nbsp;4 will not work."); showMessage(i18nc("First parameter is mime type, second parameter is optional information (may be empty)", "<qt>Don't know how to show mimetype '%1'.%2</qt>", urlInfo.mimeType, additionalInformation)); // krazy:exclude=qmethods } return false; } void openExternally() { QUrl url(cbxEntryToUrlInfo[urlComboBox->currentIndex()].url); /// Guess mime type for url to open QMimeType mimeType = FileInfo::mimeTypeForUrl(url); const QString mimeTypeName = mimeType.name(); /// Ask KDE subsystem to open url in viewer matching mime type KRun::runUrl(url, mimeTypeName, p, KRun::RunFlags()); } UrlInfo urlMetaInfo(const QUrl &url) { UrlInfo result; result.url = url; if (!KBibTeX::isLocalOrRelative(url) && url.fileName().isEmpty()) { /// URLs not pointing to a specific file should be opened with a web browser component result.icon = QIcon::fromTheme(QStringLiteral("text-html")); result.mimeType = QStringLiteral("text/html"); return result; } - QMimeType mimeType = FileInfo::mimeTypeForUrl(url); - // FIXME accuracy, necessary: - /* - if (accuracy < 50) { - QMimeDatabase db; - mimeType = db.mimeTypeForFile(url.fileName()); - } - */ + const QMimeType mimeType = FileInfo::mimeTypeForUrl(url); result.mimeType = mimeType.name(); result.icon = QIcon::fromTheme(mimeType.iconName()); if (result.mimeType == QStringLiteral("application/octet-stream")) { /// application/octet-stream is a fall-back if KDE did not know better result.icon = QIcon::fromTheme(QStringLiteral("text-html")); result.mimeType = QStringLiteral("text/html"); } else if ((result.mimeType.isEmpty() || result.mimeType == QStringLiteral("inode/directory")) && (result.url.scheme() == QStringLiteral("http") || result.url.scheme() == QStringLiteral("https"))) { /// directory via http means normal webpage (not browsable directory) result.icon = QIcon::fromTheme(QStringLiteral("text-html")); result.mimeType = QStringLiteral("text/html"); } if (url.url(QUrl::PreferLocalFile).startsWith(arXivPDFUrlStart)) { result.icon = QIcon::fromTheme(QStringLiteral("application-pdf")); result.mimeType = QStringLiteral("application/pdf"); } return result; } void comboBoxChanged(int index) { showUrl(cbxEntryToUrlInfo[index]); } bool isVisible() { /// get dock where this widget is inside /// static cast is save as constructor requires parent to be QDockWidget QDockWidget *pp = static_cast<QDockWidget *>(p->parent()); return pp != nullptr && !pp->isHidden(); } void loadState() { KConfigGroup configGroup(config, configGroupName); onlyLocalFilesButton->setChecked(!configGroup.readEntry(onlyLocalFilesCheckConfig, true)); } void saveState() { KConfigGroup configGroup(config, configGroupName); configGroup.writeEntry(onlyLocalFilesCheckConfig, !onlyLocalFilesButton->isChecked()); config->sync(); } }; const QString DocumentPreview::DocumentPreviewPrivate::arXivPDFUrlStart = QStringLiteral("http://arxiv.org/pdf/"); const QString DocumentPreview::DocumentPreviewPrivate::configGroupName = QStringLiteral("URL Preview"); const QString DocumentPreview::DocumentPreviewPrivate::onlyLocalFilesCheckConfig = QStringLiteral("OnlyLocalFiles"); DocumentPreview::DocumentPreview(QDockWidget *parent) : QWidget(parent), d(new DocumentPreviewPrivate(this)) { connect(parent, &QDockWidget::visibilityChanged, this, &DocumentPreview::visibilityChanged); } DocumentPreview::~DocumentPreview() { delete d; } void DocumentPreview::setElement(QSharedPointer<Element> element, const File *) { d->entry = element.dynamicCast<const Entry>(); d->update(); } void DocumentPreview::openExternally() { d->openExternally(); } void DocumentPreview::setBibTeXUrl(const QUrl &url) { d->baseUrl = url; } void DocumentPreview::onlyLocalFilesChanged() { d->saveState(); d->update(); } void DocumentPreview::visibilityChanged(bool) { d->update(); } void DocumentPreview::comboBoxChanged(int index) { d->comboBoxChanged(index); } void DocumentPreview::statFinished(KJob *kjob) { KIO::StatJob *job = static_cast<KIO::StatJob *>(kjob); d->runningJobs.removeOne(job); if (!job->error()) { const QUrl url = job->mostLocalUrl(); DocumentPreviewPrivate::UrlInfo urlInfo = d->urlMetaInfo(url); setCursor(d->runningJobs.isEmpty() ? Qt::ArrowCursor : Qt::BusyCursor); d->addUrl(urlInfo); } else { qCWarning(LOG_KBIBTEX_PROGRAM) << job->error() << job->errorString(); } if (d->runningJobs.isEmpty()) { /// If this was the last background stat job ... setCursor(Qt::ArrowCursor); if (d->urlComboBox->count() < 1) { /// In case that no valid references were found by the stat jobs ... if (d->anyRemote && !d->onlyLocalFilesButton->isChecked()) { /// There are some remote URLs to probe, /// but user was only looking for local files d->showMessage(i18n("<qt>No documents to show.<br/><a href=\"disableonlylocalfiles\">Disable the restriction</a> to local files to see remote documents.</qt>")); // krazy:exclude=qmethods } else { /// No stat job at all succeeded. Show message to user. d->showMessage(i18n("No documents to show.\nSome URLs or files could not be retrieved.")); // krazy:exclude=qmethods } } } } void DocumentPreview::loadingFinished() { setCursor(Qt::ArrowCursor); d->showPart(qobject_cast<KParts::ReadOnlyPart *>(sender()), qobject_cast<QWidget *>(sender())); } void DocumentPreview::linkActivated(const QString &link) { if (link == QStringLiteral("disableonlylocalfiles")) d->onlyLocalFilesButton->setChecked(true); else if (link.startsWith(QStringLiteral("http://")) || link.startsWith(QStringLiteral("https://"))) { const QUrl urlToOpen = QUrl::fromUserInput(link); if (urlToOpen.isValid()) { /// Guess mime type for url to open QMimeType mimeType = FileInfo::mimeTypeForUrl(urlToOpen); const QString mimeTypeName = mimeType.name(); /// Ask KDE subsystem to open url in viewer matching mime type KRun::runUrl(urlToOpen, mimeTypeName, this, KRun::RunFlags()); } } } diff --git a/src/program/docklets/searchform.cpp b/src/program/docklets/searchform.cpp index e872be93..8e5c0401 100644 --- a/src/program/docklets/searchform.cpp +++ b/src/program/docklets/searchform.cpp @@ -1,510 +1,509 @@ /*************************************************************************** * Copyright (C) 2004-2019 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, see <https://www.gnu.org/licenses/>. * ***************************************************************************/ #include "searchform.h" #include <QLayout> #include <QMap> #include <QLabel> #include <QListWidget> #include <QLineEdit> #include <QSpinBox> #include <QStackedWidget> #include <QTabWidget> #include <QProgressBar> -#include <QMimeDatabase> #include <QMimeType> #include <QTimer> #include <QSet> #include <QAction> #include <QScrollArea> #include <QIcon> #include <QPushButton> #include <QDebug> #include <KLocalizedString> #include <KRun> #include <KMessageBox> #include <KParts/Part> #include <KParts/ReadOnlyPart> #include <KConfigGroup> #include <KSharedConfig> #include <kio_version.h> #include <Element> #include <File> #include <Comment> #include <FileExporterBibTeX> #include <onlinesearch/OnlineSearchAbstract> #include <onlinesearch/OnlineSearchGeneral> #include <onlinesearch/OnlineSearchBibsonomy> #include <onlinesearch/onlinesearchgooglescholar.h> #include <onlinesearch/OnlineSearchPubMed> #include <onlinesearch/OnlineSearchIEEEXplore> #include <onlinesearch/OnlineSearchAcmPortal> #include <onlinesearch/OnlineSearchScienceDirect> #include <onlinesearch/OnlineSearchSpringerLink> #include <onlinesearch/OnlineSearchArXiv> #include <onlinesearch/OnlineSearchJStor> #include <onlinesearch/OnlineSearchMathSciNet> #include <onlinesearch/OnlineSearchMRLookup> #include <onlinesearch/OnlineSearchInspireHep> #include <onlinesearch/OnlineSearchCERNDS> #include <onlinesearch/OnlineSearchIngentaConnect> #include <onlinesearch/OnlineSearchSOANASAADS> #include <onlinesearch/OnlineSearchIDEASRePEc> #include <onlinesearch/OnlineSearchDOI> #include <onlinesearch/OnlineSearchBioRxiv> #include <onlinesearch/OnlineSearchSemanticScholar> #include <file/FileView> #include <models/FileModel> #include "openfileinfo.h" #include "searchresults.h" #include "logging_program.h" class SearchForm::SearchFormPrivate { private: SearchForm *p; QStackedWidget *queryTermsStack; QWidget *listContainer; QListWidget *enginesList; QLabel *whichEnginesLabel; QAction *actionOpenHomepage; public: KSharedConfigPtr config; const QString configGroupName; SearchResults *sr; QMap<QListWidgetItem *, OnlineSearchAbstract *> itemToOnlineSearch; QSet<OnlineSearchAbstract *> runningSearches; QPushButton *searchButton; QPushButton *useEntryButton; OnlineSearchQueryFormGeneral *generalQueryTermsForm; QTabWidget *tabWidget; QSharedPointer<const Entry> currentEntry; QProgressBar *progressBar; QMap<OnlineSearchAbstract *, int> progressMap; QMap<OnlineSearchQueryFormAbstract *, QScrollArea *> formToScrollArea; enum SearchFormPrivateRole { /// Homepage of a search engine HomepageRole = Qt::UserRole + 5, /// Special widget for a search engine WidgetRole = Qt::UserRole + 6, /// Name of a search engine NameRole = Qt::UserRole + 7 }; SearchFormPrivate(SearchResults *searchResults, SearchForm *parent) : p(parent), whichEnginesLabel(nullptr), config(KSharedConfig::openConfig(QStringLiteral("kbibtexrc"))), configGroupName(QStringLiteral("Search Engines Docklet")), sr(searchResults), searchButton(nullptr), useEntryButton(nullptr), currentEntry(nullptr) { createGUI(); } OnlineSearchQueryFormAbstract *currentQueryForm() { QScrollArea *area = qobject_cast<QScrollArea *>(queryTermsStack->currentWidget()); return formToScrollArea.key(area, nullptr); } QScrollArea *wrapInScrollArea(OnlineSearchQueryFormAbstract *form, QWidget *parent) { QScrollArea *scrollArea = new QScrollArea(parent); form->setParent(scrollArea); scrollArea->setWidget(form); scrollArea->setWidgetResizable(true); scrollArea->setFrameShape(QFrame::NoFrame); scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); formToScrollArea.insert(form, scrollArea); return scrollArea; } QWidget *createQueryTermsStack(QWidget *parent) { QWidget *container = new QWidget(parent); QVBoxLayout *vLayout = new QVBoxLayout(container); whichEnginesLabel = new QLabel(container); whichEnginesLabel->setWordWrap(true); vLayout->addWidget(whichEnginesLabel); vLayout->setStretchFactor(whichEnginesLabel, 0); connect(whichEnginesLabel, &QLabel::linkActivated, p, &SearchForm::switchToEngines); vLayout->addSpacing(8); queryTermsStack = new QStackedWidget(container); vLayout->addWidget(queryTermsStack); vLayout->setStretchFactor(queryTermsStack, 5); QScrollArea *scrollArea = wrapInScrollArea(createGeneralQueryTermsForm(queryTermsStack), queryTermsStack); queryTermsStack->addWidget(scrollArea); return container; } OnlineSearchQueryFormAbstract *createGeneralQueryTermsForm(QWidget *parent = nullptr) { generalQueryTermsForm = new OnlineSearchQueryFormGeneral(parent); return generalQueryTermsForm; } QWidget *createEnginesGUI(QWidget *parent) { listContainer = new QWidget(parent); QGridLayout *layout = new QGridLayout(listContainer); layout->setRowStretch(0, 1); layout->setRowStretch(1, 0); enginesList = new QListWidget(listContainer); layout->addWidget(enginesList, 0, 0, 1, 1); connect(enginesList, &QListWidget::itemChanged, p, &SearchForm::itemCheckChanged); connect(enginesList, &QListWidget::currentItemChanged, p, &SearchForm::enginesListCurrentChanged); enginesList->setSelectionMode(QAbstractItemView::NoSelection); actionOpenHomepage = new QAction(QIcon::fromTheme(QStringLiteral("internet-web-browser")), i18n("Go to Homepage"), p); connect(actionOpenHomepage, &QAction::triggered, p, &SearchForm::openHomepage); enginesList->addAction(actionOpenHomepage); enginesList->setContextMenuPolicy(Qt::ActionsContextMenu); return listContainer; } void createGUI() { QGridLayout *layout = new QGridLayout(p); layout->setMargin(0); layout->setRowStretch(0, 1); layout->setRowStretch(1, 0); layout->setColumnStretch(0, 0); layout->setColumnStretch(1, 1); layout->setColumnStretch(2, 0); tabWidget = new QTabWidget(p); tabWidget->setDocumentMode(true); layout->addWidget(tabWidget, 0, 0, 1, 3); QWidget *widget = createQueryTermsStack(tabWidget); tabWidget->addTab(widget, QIcon::fromTheme(QStringLiteral("edit-rename")), i18n("Query Terms")); QWidget *listContainer = createEnginesGUI(tabWidget); tabWidget->addTab(listContainer, QIcon::fromTheme(QStringLiteral("applications-engineering")), i18n("Engines")); connect(tabWidget, &QTabWidget::currentChanged, p, &SearchForm::tabSwitched); useEntryButton = new QPushButton(QIcon::fromTheme(QStringLiteral("go-up")), i18n("Use Entry"), p); layout->addWidget(useEntryButton, 1, 0, 1, 1); useEntryButton->setEnabled(false); connect(useEntryButton, &QPushButton::clicked, p, &SearchForm::copyFromEntry); progressBar = new QProgressBar(p); layout->addWidget(progressBar, 1, 1, 1, 1); progressBar->setMaximum(1000); progressBar->hide(); searchButton = new QPushButton(QIcon::fromTheme(QStringLiteral("edit-find")), i18n("Search"), p); layout->addWidget(searchButton, 1, 2, 1, 1); connect(generalQueryTermsForm, &OnlineSearchQueryFormGeneral::returnPressed, searchButton, &QPushButton::click); updateGUI(); } void loadEngines() { enginesList->clear(); addEngine(new OnlineSearchAcmPortal(p)); addEngine(new OnlineSearchArXiv(p)); addEngine(new OnlineSearchBioRxiv(p)); addEngine(new OnlineSearchBibsonomy(p)); addEngine(new OnlineSearchGoogleScholar(p)); addEngine(new OnlineSearchIEEEXplore(p)); addEngine(new OnlineSearchIngentaConnect(p)); addEngine(new OnlineSearchJStor(p)); addEngine(new OnlineSearchMathSciNet(p)); addEngine(new OnlineSearchMRLookup(p)); addEngine(new OnlineSearchInspireHep(p)); addEngine(new OnlineSearchCERNDS(p)); addEngine(new OnlineSearchPubMed(p)); addEngine(new OnlineSearchScienceDirect(p)); addEngine(new OnlineSearchSpringerLink(p)); addEngine(new OnlineSearchSOANASAADS(p)); /// addEngine(new OnlineSearchIsbnDB(p)); /// disabled as provider switched to a paid model on 2017-12-26 addEngine(new OnlineSearchIDEASRePEc(p)); addEngine(new OnlineSearchDOI(p)); addEngine(new OnlineSearchSemanticScholar(p)); p->itemCheckChanged(nullptr); updateGUI(); } void addEngine(OnlineSearchAbstract *engine) { KConfigGroup configGroup(config, configGroupName); /// Disable signals while updating the widget and its items enginesList->blockSignals(true); QListWidgetItem *item = new QListWidgetItem(engine->label(), enginesList); static const QSet<QString> enginesEnabledByDefault {QStringLiteral("GoogleScholar"), QStringLiteral("Bibsonomy")}; item->setCheckState(configGroup.readEntry(engine->name(), enginesEnabledByDefault.contains(engine->name())) ? Qt::Checked : Qt::Unchecked); item->setIcon(engine->icon(item)); item->setToolTip(engine->label()); item->setData(HomepageRole, engine->homepage()); item->setData(NameRole, engine->name()); OnlineSearchQueryFormAbstract *widget = engine->customWidget(queryTermsStack); item->setData(WidgetRole, QVariant::fromValue<OnlineSearchQueryFormAbstract *>(widget)); if (widget != nullptr) { connect(widget, &OnlineSearchQueryFormAbstract::returnPressed, searchButton, &QPushButton::click); QScrollArea *scrollArea = wrapInScrollArea(widget, queryTermsStack); queryTermsStack->addWidget(scrollArea); } itemToOnlineSearch.insert(item, engine); connect(engine, &OnlineSearchAbstract::foundEntry, p, &SearchForm::foundEntry); connect(engine, &OnlineSearchAbstract::stoppedSearch, p, &SearchForm::stoppedSearch); connect(engine, &OnlineSearchAbstract::progress, p, &SearchForm::updateProgress); /// Re-enable signals after updating the widget and its items enginesList->blockSignals(false); } void switchToSearch() { for (QMap<QListWidgetItem *, OnlineSearchAbstract *>::ConstIterator it = itemToOnlineSearch.constBegin(); it != itemToOnlineSearch.constEnd(); ++it) disconnect(searchButton, &QPushButton::clicked, it.value(), &OnlineSearchAbstract::cancel); connect(searchButton, &QPushButton::clicked, p, &SearchForm::startSearch); searchButton->setText(i18n("Search")); searchButton->setIcon(QIcon::fromTheme(QStringLiteral("media-playback-start"))); for (int i = tabWidget->count() - 1; i >= 0; --i) tabWidget->widget(i)->setEnabled(true); tabWidget->unsetCursor(); } void switchToCancel() { disconnect(searchButton, &QPushButton::clicked, p, &SearchForm::startSearch); for (QMap<QListWidgetItem *, OnlineSearchAbstract *>::ConstIterator it = itemToOnlineSearch.constBegin(); it != itemToOnlineSearch.constEnd(); ++it) connect(searchButton, &QPushButton::clicked, it.value(), &OnlineSearchAbstract::cancel); searchButton->setText(i18n("Stop")); searchButton->setIcon(QIcon::fromTheme(QStringLiteral("media-playback-stop"))); for (int i = tabWidget->count() - 1; i >= 0; --i) tabWidget->widget(i)->setEnabled(false); tabWidget->setCursor(Qt::WaitCursor); } void switchToEngines() { tabWidget->setCurrentWidget(listContainer); } void updateGUI() { if (whichEnginesLabel == nullptr) return; QStringList checkedEngines; QListWidgetItem *cursor = nullptr; for (QMap<QListWidgetItem *, OnlineSearchAbstract *>::ConstIterator it = itemToOnlineSearch.constBegin(); it != itemToOnlineSearch.constEnd(); ++it) if (it.key()->checkState() == Qt::Checked) { checkedEngines << it.key()->text(); cursor = it.key(); } switch (checkedEngines.size()) { case 0: whichEnginesLabel->setText(i18n("No search engine selected (<a href=\"changeEngine\">change</a>).")); break; case 1: whichEnginesLabel->setText(i18n("Search engine <b>%1</b> is selected (<a href=\"changeEngine\">change</a>).", checkedEngines.first())); break; case 2: whichEnginesLabel->setText(i18n("Search engines <b>%1</b> and <b>%2</b> are selected (<a href=\"changeEngine\">change</a>).", checkedEngines.first(), checkedEngines.at(1))); break; case 3: whichEnginesLabel->setText(i18n("Search engines <b>%1</b>, <b>%2</b>, and <b>%3</b> are selected (<a href=\"changeEngine\">change</a>).", checkedEngines.first(), checkedEngines.at(1), checkedEngines.at(2))); break; default: whichEnginesLabel->setText(i18n("Search engines <b>%1</b>, <b>%2</b>, and more are selected (<a href=\"changeEngine\">change</a>).", checkedEngines.first(), checkedEngines.at(1))); break; } OnlineSearchQueryFormAbstract *currentQueryWidget = nullptr; if (cursor != nullptr && checkedEngines.size() == 1) currentQueryWidget = cursor->data(WidgetRole).value<OnlineSearchQueryFormAbstract *>(); if (currentQueryWidget == nullptr) currentQueryWidget = generalQueryTermsForm; QScrollArea *area = formToScrollArea.value(currentQueryWidget, nullptr); if (area != nullptr) queryTermsStack->setCurrentWidget(area); if (useEntryButton != nullptr) useEntryButton->setEnabled(!currentEntry.isNull() && tabWidget->currentIndex() == 0); } void openHomepage() { QListWidgetItem *item = enginesList->currentItem(); if (item != nullptr) { QUrl url = item->data(HomepageRole).toUrl(); /// Guess mime type for url to open QMimeType mimeType = FileInfo::mimeTypeForUrl(url); const QString mimeTypeName = mimeType.name(); /// Ask KDE subsystem to open url in viewer matching mime type KRun::runUrl(url, mimeTypeName, p, KRun::RunFlags()); } } void enginesListCurrentChanged(QListWidgetItem *current) { actionOpenHomepage->setEnabled(current != nullptr); } }; SearchForm::SearchForm(SearchResults *searchResults, QWidget *parent) : QWidget(parent), d(new SearchFormPrivate(searchResults, this)) { d->loadEngines(); d->switchToSearch(); } SearchForm::~SearchForm() { delete d; } void SearchForm::updatedConfiguration() { d->loadEngines(); } void SearchForm::setElement(QSharedPointer<Element> element, const File *) { d->currentEntry = element.dynamicCast<const Entry>(); d->useEntryButton->setEnabled(!d->currentEntry.isNull() && d->tabWidget->currentIndex() == 0); } void SearchForm::switchToEngines() { d->switchToEngines(); } void SearchForm::startSearch() { OnlineSearchQueryFormAbstract *currentForm = d->currentQueryForm(); if (!currentForm->readyToStart()) { KMessageBox::sorry(this, i18n("Could not start searching the Internet:\nThe search terms are not complete or invalid."), i18n("Searching the Internet")); return; } d->runningSearches.clear(); d->sr->clear(); d->progressBar->setValue(0); d->progressMap.clear(); d->useEntryButton->hide(); d->progressBar->show(); if (currentForm == d->generalQueryTermsForm) { /// start search using the general-purpose form's values QMap<QString, QString> queryTerms = d->generalQueryTermsForm->getQueryTerms(); int numResults = d->generalQueryTermsForm->getNumResults(); for (QMap<QListWidgetItem *, OnlineSearchAbstract *>::ConstIterator it = d->itemToOnlineSearch.constBegin(); it != d->itemToOnlineSearch.constEnd(); ++it) if (it.key()->checkState() == Qt::Checked) { it.value()->startSearch(queryTerms, numResults); d->runningSearches.insert(it.value()); } if (d->runningSearches.isEmpty()) { /// if no search engine has been checked (selected), something went wrong return; } } else { /// use the single selected search engine's specific form for (QMap<QListWidgetItem *, OnlineSearchAbstract *>::ConstIterator it = d->itemToOnlineSearch.constBegin(); it != d->itemToOnlineSearch.constEnd(); ++it) if (it.key()->checkState() == Qt::Checked) { it.value()->startSearchFromForm(); d->runningSearches.insert(it.value()); } if (d->runningSearches.isEmpty()) { /// if no search engine has been checked (selected), something went wrong return; } } d->switchToCancel(); } void SearchForm::foundEntry(QSharedPointer<Entry> entry) { d->sr->insertElement(entry); } void SearchForm::stoppedSearch(int) { OnlineSearchAbstract *engine = static_cast<OnlineSearchAbstract *>(sender()); if (d->runningSearches.remove(engine)) { if (d->runningSearches.isEmpty()) { /// last search engine stopped d->switchToSearch(); emit doneSearching(); QTimer::singleShot(1000, d->progressBar, &QProgressBar::hide); QTimer::singleShot(1100, d->useEntryButton, &QPushButton::show); } else { QStringList remainingEngines; remainingEngines.reserve(d->runningSearches.size()); for (OnlineSearchAbstract *running : const_cast<const QSet<OnlineSearchAbstract *> &>(d->runningSearches)) { remainingEngines.append(running->label()); } if (!remainingEngines.isEmpty()) qCDebug(LOG_KBIBTEX_PROGRAM) << "Remaining running engines:" << remainingEngines.join(QStringLiteral(", ")); } } } void SearchForm::tabSwitched(int newTab) { Q_UNUSED(newTab); d->updateGUI(); } void SearchForm::itemCheckChanged(QListWidgetItem *item) { int numCheckedEngines = 0; for (QMap<QListWidgetItem *, OnlineSearchAbstract *>::ConstIterator it = d->itemToOnlineSearch.constBegin(); it != d->itemToOnlineSearch.constEnd(); ++it) if (it.key()->checkState() == Qt::Checked) ++numCheckedEngines; d->searchButton->setEnabled(numCheckedEngines > 0); if (item != nullptr) { KConfigGroup configGroup(d->config, d->configGroupName); QString name = item->data(SearchForm::SearchFormPrivate::NameRole).toString(); configGroup.writeEntry(name, item->checkState() == Qt::Checked); d->config->sync(); } } void SearchForm::openHomepage() { d->openHomepage(); } void SearchForm::enginesListCurrentChanged(QListWidgetItem *current, QListWidgetItem *) { d->enginesListCurrentChanged(current); } void SearchForm::copyFromEntry() { Q_ASSERT_X(!d->currentEntry.isNull(), "SearchForm::copyFromEntry", "d->currentEntry is NULL"); d->currentQueryForm()->copyFromEntry(*(d->currentEntry)); } void SearchForm::updateProgress(int cur, int total) { OnlineSearchAbstract *ws = static_cast<OnlineSearchAbstract *>(sender()); d->progressMap[ws] = total > 0 ? cur * 1000 / total : 0; int progress = 0, count = 0; for (QMap<OnlineSearchAbstract *, int>::ConstIterator it = d->progressMap.constBegin(); it != d->progressMap.constEnd(); ++it, ++count) progress += it.value(); d->progressBar->setValue(count >= 1 ? progress / count : 0); }