diff --git a/src/data/file.cpp b/src/data/file.cpp index 825af2a4..e5efa9ef 100644 --- a/src/data/file.cpp +++ b/src/data/file.cpp @@ -1,374 +1,358 @@ /*************************************************************************** - * Copyright (C) 2004-2018 by Thomas Fischer * + * 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 "file.h" #include #include #include #include #ifdef HAVE_KF5 #include #include #endif // HAVE_KF5 #include "preferences.h" #include "entry.h" #include "element.h" #include "macro.h" #include "comment.h" #include "preamble.h" #include "logging_data.h" const QString File::Url = QStringLiteral("Url"); const QString File::Encoding = QStringLiteral("Encoding"); const QString File::StringDelimiter = QStringLiteral("StringDelimiter"); const QString File::QuoteComment = QStringLiteral("QuoteComment"); const QString File::KeywordCasing = QStringLiteral("KeywordCasing"); const QString File::ProtectCasing = QStringLiteral("ProtectCasing"); const QString File::NameFormatting = QStringLiteral("NameFormatting"); const QString File::ListSeparator = QStringLiteral("ListSeparator"); const quint64 valid = 0x08090a0b0c0d0e0f; const quint64 invalid = 0x0102030405060708; class File::FilePrivate { private: quint64 validInvalidField; static const quint64 initialInternalIdCounter; static quint64 internalIdCounter; #ifdef HAVE_KF5 KSharedConfigPtr config; const QString configGroupName; #endif // HAVE_KF5 public: const quint64 internalId; QHash properties; explicit FilePrivate(File *parent) : validInvalidField(valid), #ifdef HAVE_KF5 config(KSharedConfig::openConfig(QStringLiteral("kbibtexrc"))), configGroupName(QStringLiteral("FileExporterBibTeX")), #endif // HAVE_KF5 internalId(++internalIdCounter) { Q_UNUSED(parent) const bool isValid = checkValidity(); if (!isValid) qCDebug(LOG_KBIBTEX_DATA) << "Creating File instance" << internalId << " Valid?" << isValid; #ifdef HAVE_KF5 loadConfiguration(); #endif // HAVE_KF5 } ~FilePrivate() { const bool isValid = checkValidity(); if (!isValid) qCDebug(LOG_KBIBTEX_DATA) << "Deleting File instance" << internalId << " Valid?" << isValid; validInvalidField = invalid; } /// Copy-assignment operator FilePrivate &operator= (const FilePrivate &other) { if (this != &other) { validInvalidField = other.validInvalidField; properties = other.properties; const bool isValid = checkValidity(); if (!isValid) qCDebug(LOG_KBIBTEX_DATA) << "Assigning File instance" << other.internalId << "to" << internalId << " Is other valid?" << other.checkValidity() << " Self valid?" << isValid; } return *this; } /// Move-assignment operator FilePrivate &operator= (FilePrivate &&other) { if (this != &other) { validInvalidField = std::move(other.validInvalidField); properties = std::move(other.properties); const bool isValid = checkValidity(); if (!isValid) qCDebug(LOG_KBIBTEX_DATA) << "Assigning File instance" << other.internalId << "to" << internalId << " Is other valid?" << other.checkValidity() << " Self valid?" << isValid; } return *this; } #ifdef HAVE_KF5 void loadConfiguration() { /// Load and set configuration as stored in settings KConfigGroup configGroup(config, configGroupName); properties.insert(File::Encoding, configGroup.readEntry(Preferences::keyEncoding, Preferences::defaultEncoding)); properties.insert(File::StringDelimiter, configGroup.readEntry(Preferences::keyStringDelimiter, Preferences::defaultStringDelimiter)); properties.insert(File::QuoteComment, static_cast(configGroup.readEntry(Preferences::keyQuoteComment, static_cast(Preferences::defaultQuoteComment)))); properties.insert(File::KeywordCasing, static_cast(configGroup.readEntry(Preferences::keyKeywordCasing, static_cast(Preferences::defaultKeywordCasing)))); - properties.insert(File::NameFormatting, configGroup.readEntry(Preferences::keyPersonNameFormatting, QString())); + properties.insert(File::NameFormatting, Preferences::instance().personNameFormatting()); properties.insert(File::ProtectCasing, configGroup.readEntry(Preferences::keyProtectCasing, static_cast(Preferences::defaultProtectCasing))); properties.insert(File::ListSeparator, configGroup.readEntry(Preferences::keyListSeparator, Preferences::defaultListSeparator)); } #endif // HAVE_KF5 bool checkValidity() const { if (validInvalidField != valid) { /// 'validInvalidField' must equal to the know 'valid' value qCWarning(LOG_KBIBTEX_DATA) << "Failed validity check: " << validInvalidField << "!=" << valid; return false; } else if (internalId <= initialInternalIdCounter) { /// Internal id counter starts at initialInternalIdCounter+1 qCWarning(LOG_KBIBTEX_DATA) << "Failed validity check: " << internalId << "< " << (initialInternalIdCounter + 1); return false; } else if (internalId > 600000) { /// Reasonable assumption: not more that 500000 ids used qCWarning(LOG_KBIBTEX_DATA) << "Failed validity check: " << internalId << "> 600000"; return false; } return true; } }; const quint64 File::FilePrivate::initialInternalIdCounter = 99999; quint64 File::FilePrivate::internalIdCounter = File::FilePrivate::initialInternalIdCounter; File::File() : QList >(), d(new FilePrivate(this)) { /// nothing } File::File(const File &other) : QList >(other), d(new FilePrivate(this)) { d->operator =(*other.d); } File::File(File &&other) : QList >(std::move(other)), d(new FilePrivate(this)) { d->operator =(std::move(*other.d)); } File::~File() { Q_ASSERT_X(d->checkValidity(), "File::~File()", "This File object is not valid"); delete d; } File &File::operator= (const File &other) { if (this != &other) d->operator =(*other.d); return *this; } File &File::operator= (File &&other) { if (this != &other) d->operator =(std::move(*other.d)); return *this; } bool File::operator==(const File &other) const { if (size() != other.size()) return false; for (File::ConstIterator myIt = constBegin(), otherIt = other.constBegin(); myIt != constEnd() && otherIt != constEnd(); ++myIt, ++otherIt) { QSharedPointer myEntry = myIt->dynamicCast(); QSharedPointer otherEntry = otherIt->dynamicCast(); if ((myEntry.isNull() && !otherEntry.isNull()) || (!myEntry.isNull() && otherEntry.isNull())) return false; if (!myEntry.isNull() && !otherEntry.isNull()) { if (myEntry->operator !=(*otherEntry.data())) return false; } else { QSharedPointer myMacro = myIt->dynamicCast(); QSharedPointer otherMacro = otherIt->dynamicCast(); if ((myMacro.isNull() && !otherMacro.isNull()) || (!myMacro.isNull() && otherMacro.isNull())) return false; if (!myMacro.isNull() && !otherMacro.isNull()) { if (myMacro->operator !=(*otherMacro.data())) return false; } else { QSharedPointer myPreamble = myIt->dynamicCast(); QSharedPointer otherPreamble = otherIt->dynamicCast(); if ((myPreamble.isNull() && !otherPreamble.isNull()) || (!myPreamble.isNull() && otherPreamble.isNull())) return false; if (!myPreamble.isNull() && !otherPreamble.isNull()) { if (myPreamble->operator !=(*otherPreamble.data())) return false; } else { QSharedPointer myComment = myIt->dynamicCast(); QSharedPointer otherComment = otherIt->dynamicCast(); if ((myComment.isNull() && !otherComment.isNull()) || (!myComment.isNull() && otherComment.isNull())) return false; if (!myComment.isNull() && !otherComment.isNull()) { // TODO right now, don't care if comments are equal qCDebug(LOG_KBIBTEX_DATA) << "File objects being compared contain comments, ignoring those"; } else { /// This case should never be reached qCWarning(LOG_KBIBTEX_DATA) << "Met unhandled case while comparing two File objects"; return false; } } } } } return true; } bool File::operator!=(const File &other) const { return !operator ==(other); } const QSharedPointer File::containsKey(const QString &key, ElementTypes elementTypes) const { if (!d->checkValidity()) qCCritical(LOG_KBIBTEX_DATA) << "const QSharedPointer File::containsKey(const QString &key, ElementTypes elementTypes) const" << "This File object is not valid"; for (const auto &element : const_cast(*this)) { const QSharedPointer entry = elementTypes.testFlag(etEntry) ? element.dynamicCast() : QSharedPointer(); if (!entry.isNull()) { if (entry->id() == key) return entry; } else { const QSharedPointer macro = elementTypes.testFlag(etMacro) ? element.dynamicCast() : QSharedPointer(); if (!macro.isNull()) { if (macro->key() == key) return macro; } } } return QSharedPointer(); } QStringList File::allKeys(ElementTypes elementTypes) const { if (!d->checkValidity()) qCCritical(LOG_KBIBTEX_DATA) << "QStringList File::allKeys(ElementTypes elementTypes) const" << "This File object is not valid"; QStringList result; result.reserve(size()); for (const auto &element : const_cast(*this)) { const QSharedPointer entry = elementTypes.testFlag(etEntry) ? element.dynamicCast() : QSharedPointer(); if (!entry.isNull()) result.append(entry->id()); else { const QSharedPointer macro = elementTypes.testFlag(etMacro) ? element.dynamicCast() : QSharedPointer(); if (!macro.isNull()) result.append(macro->key()); } } return result; } QSet File::uniqueEntryValuesSet(const QString &fieldName) const { if (!d->checkValidity()) qCCritical(LOG_KBIBTEX_DATA) << "QSet File::uniqueEntryValuesSet(const QString &fieldName) const" << "This File object is not valid"; QSet valueSet; const QString lcFieldName = fieldName.toLower(); for (const auto &element : const_cast(*this)) { const QSharedPointer entry = element.dynamicCast(); if (!entry.isNull()) for (Entry::ConstIterator it = entry->constBegin(); it != entry->constEnd(); ++it) if (it.key().toLower() == lcFieldName) { const auto itValue = it.value(); for (const QSharedPointer &valueItem : itValue) { /// Check if ValueItem to process points to a person const QSharedPointer person = valueItem.dynamicCast(); if (!person.isNull()) { - /// Assemble a list of formatting templates for a person's name - static QStringList personNameFormattingList; ///< use static to do pattern assembly only once - if (personNameFormattingList.isEmpty()) { - /// Use the two default patterns last-name-first and first-name-first -#ifdef HAVE_KF5 - personNameFormattingList << Preferences::personNameFormatLastFirst << Preferences::personNameFormatFirstLast; - /// Check configuration if user-specified formatting template is different - KSharedConfigPtr config(KSharedConfig::openConfig(QStringLiteral("kbibtexrc"))); - KConfigGroup configGroup(config, "General"); - QString personNameFormatting = configGroup.readEntry(Preferences::keyPersonNameFormatting, Preferences::defaultPersonNameFormatting); - /// Add user's template if it differs from the two specified above - if (!personNameFormattingList.contains(personNameFormatting)) - personNameFormattingList << personNameFormatting; -#else // HAVE_KF5 - personNameFormattingList << QStringLiteral("<%l><, %s><, %f>") << QStringLiteral("<%f ><%l>< %s>"); -#endif // HAVE_KF5 - } + QSet personNameFormattingSet {Preferences::personNameFormatLastFirst, Preferences::personNameFormatFirstLast}; + personNameFormattingSet.insert(Preferences::instance().personNameFormatting()); /// Add person's name formatted using each of the templates assembled above - for (const QString &personNameFormatting : const_cast(personNameFormattingList)) { + for (const QString &personNameFormatting : const_cast &>(personNameFormattingSet)) valueSet.insert(Person::transcribePersonName(person.data(), personNameFormatting)); - } } else { /// Default case: use PlainTextValue::text to translate ValueItem /// to a human-readable text valueSet.insert(PlainTextValue::text(*valueItem)); } } } } return valueSet; } QStringList File::uniqueEntryValuesList(const QString &fieldName) const { if (!d->checkValidity()) qCCritical(LOG_KBIBTEX_DATA) << "QStringList File::uniqueEntryValuesList(const QString &fieldName) const" << "This File object is not valid"; QSet valueSet = uniqueEntryValuesSet(fieldName); QStringList list = valueSet.toList(); list.sort(); return list; } void File::setProperty(const QString &key, const QVariant &value) { if (!d->checkValidity()) qCCritical(LOG_KBIBTEX_DATA) << "void File::setProperty(const QString &key, const QVariant &value)" << "This File object is not valid"; d->properties.insert(key, value); } QVariant File::property(const QString &key) const { if (!d->checkValidity()) qCCritical(LOG_KBIBTEX_DATA) << "QVariant File::property(const QString &key) const" << "This File object is not valid"; return d->properties.contains(key) ? d->properties.value(key) : QVariant(); } QVariant File::property(const QString &key, const QVariant &defaultValue) const { if (!d->checkValidity()) qCCritical(LOG_KBIBTEX_DATA) << "QVariant File::property(const QString &key, const QVariant &defaultValue) const" << "This File object is not valid"; return d->properties.value(key, defaultValue); } bool File::hasProperty(const QString &key) const { if (!d->checkValidity()) qCCritical(LOG_KBIBTEX_DATA) << "bool File::hasProperty(const QString &key) const" << "This File object is not valid"; return d->properties.contains(key); } #ifdef HAVE_KF5 void File::setPropertiesToDefault() { if (!d->checkValidity()) qCCritical(LOG_KBIBTEX_DATA) << "void File::setPropertiesToDefault()" << "This File object is not valid"; d->loadConfiguration(); } #endif // HAVE_KF5 bool File::checkValidity() const { return d->checkValidity(); } QDebug operator<<(QDebug dbg, const File &file) { dbg.nospace() << "File is " << (file.checkValidity() ? "" : "NOT ") << "valid and has " << file.count() << " members"; return dbg; } diff --git a/src/data/value.cpp b/src/data/value.cpp index 6b11a24b..f6a1979d 100644 --- a/src/data/value.cpp +++ b/src/data/value.cpp @@ -1,769 +1,736 @@ /*************************************************************************** - * Copyright (C) 2004-2018 by Thomas Fischer * + * 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 "value.h" #include #include #include #include #include #include #ifdef HAVE_KF5 #include #include #include #else // HAVE_KF5 #define i18n(text) QObject::tr(text) #endif // HAVE_KF5 #include "file.h" #include "preferences.h" quint64 ValueItem::internalIdCounter = 0; uint qHash(const QSharedPointer &valueItem) { return qHash(valueItem->id()); } const QRegularExpression ValueItem::ignoredInSorting(QStringLiteral("[{}\\\\]+")); ValueItem::ValueItem() : internalId(++internalIdCounter) { /// nothing } ValueItem::~ValueItem() { /// nothing } quint64 ValueItem::id() const { return internalId; } bool ValueItem::operator!=(const ValueItem &other) const { return !operator ==(other); } Keyword::Keyword(const Keyword &other) : m_text(other.m_text) { /// nothing } Keyword::Keyword(const QString &text) : m_text(text) { /// nothing } void Keyword::setText(const QString &text) { m_text = text; } QString Keyword::text() const { return m_text; } void Keyword::replace(const QString &before, const QString &after, ValueItem::ReplaceMode replaceMode) { if (replaceMode == ValueItem::AnySubstring) m_text = m_text.replace(before, after); else if (replaceMode == ValueItem::CompleteMatch && m_text == before) m_text = after; } bool Keyword::containsPattern(const QString &pattern, Qt::CaseSensitivity caseSensitive) const { const QString text = QString(m_text).remove(ignoredInSorting); return text.contains(pattern, caseSensitive); } bool Keyword::operator==(const ValueItem &other) const { const Keyword *otherKeyword = dynamic_cast(&other); if (otherKeyword != nullptr) { return otherKeyword->text() == text(); } else return false; } bool Keyword::isKeyword(const ValueItem &other) { return typeid(other) == typeid(Keyword); } Person::Person(const QString &firstName, const QString &lastName, const QString &suffix) : m_firstName(firstName), m_lastName(lastName), m_suffix(suffix) { /// nothing } Person::Person(const Person &other) : m_firstName(other.firstName()), m_lastName(other.lastName()), m_suffix(other.suffix()) { /// nothing } QString Person::firstName() const { return m_firstName; } QString Person::lastName() const { return m_lastName; } QString Person::suffix() const { return m_suffix; } void Person::replace(const QString &before, const QString &after, ValueItem::ReplaceMode replaceMode) { if (replaceMode == ValueItem::AnySubstring) { m_firstName = m_firstName.replace(before, after); m_lastName = m_lastName.replace(before, after); m_suffix = m_suffix.replace(before, after); } else if (replaceMode == ValueItem::CompleteMatch) { if (m_firstName == before) m_firstName = after; if (m_lastName == before) m_lastName = after; if (m_suffix == before) m_suffix = after; } } bool Person::containsPattern(const QString &pattern, Qt::CaseSensitivity caseSensitive) const { const QString firstName = QString(m_firstName).remove(ignoredInSorting); const QString lastName = QString(m_lastName).remove(ignoredInSorting); const QString suffix = QString(m_suffix).remove(ignoredInSorting); return firstName.contains(pattern, caseSensitive) || lastName.contains(pattern, caseSensitive) || suffix.contains(pattern, caseSensitive) || QString(QStringLiteral("%1 %2|%2, %1")).arg(firstName, lastName).contains(pattern, caseSensitive); } bool Person::operator==(const ValueItem &other) const { const Person *otherPerson = dynamic_cast(&other); if (otherPerson != nullptr) { return otherPerson->firstName() == firstName() && otherPerson->lastName() == lastName() && otherPerson->suffix() == suffix(); } else return false; } QString Person::transcribePersonName(const Person *person, const QString &formatting) { return transcribePersonName(formatting, person->firstName(), person->lastName(), person->suffix()); } QString Person::transcribePersonName(const QString &formatting, const QString &firstName, const QString &lastName, const QString &suffix) { QString result = formatting; int p1 = -1, p2 = -1, p3 = -1; while ((p1 = result.indexOf('<')) >= 0 && (p2 = result.indexOf('>', p1 + 1)) >= 0 && (p3 = result.indexOf('%', p1)) >= 0 && p3 < p2) { QString insert; switch (result[p3 + 1].toLatin1()) { case 'f': insert = firstName; break; case 'l': insert = lastName; break; case 's': insert = suffix; break; } if (!insert.isEmpty()) insert = result.mid(p1 + 1, p3 - p1 - 1) + insert + result.mid(p3 + 2, p2 - p3 - 2); result = result.left(p1) + insert + result.mid(p2 + 1); } return result; } bool Person::isPerson(const ValueItem &other) { return typeid(other) == typeid(Person); } QDebug operator<<(QDebug dbg, const Person &person) { dbg.nospace() << "Person " << Person::transcribePersonName(&person, Preferences::defaultPersonNameFormatting); return dbg; } MacroKey::MacroKey(const MacroKey &other) : m_text(other.m_text) { /// nothing } MacroKey::MacroKey(const QString &text) : m_text(text) { /// nothing } void MacroKey::setText(const QString &text) { m_text = text; } QString MacroKey::text() const { return m_text; } bool MacroKey::isValid() { const QString t = text(); static const QRegularExpression validMacroKey(QStringLiteral("^[a-z][-.:/+_a-z0-9]*$|^[0-9]+$"), QRegularExpression::CaseInsensitiveOption); const QRegularExpressionMatch match = validMacroKey.match(t); return match.hasMatch() && match.captured(0) == t; } void MacroKey::replace(const QString &before, const QString &after, ValueItem::ReplaceMode replaceMode) { if (replaceMode == ValueItem::AnySubstring) m_text = m_text.replace(before, after); else if (replaceMode == ValueItem::CompleteMatch && m_text == before) m_text = after; } bool MacroKey::containsPattern(const QString &pattern, Qt::CaseSensitivity caseSensitive) const { const QString text = QString(m_text).remove(ignoredInSorting); return text.contains(pattern, caseSensitive); } bool MacroKey::operator==(const ValueItem &other) const { const MacroKey *otherMacroKey = dynamic_cast(&other); if (otherMacroKey != nullptr) { return otherMacroKey->text() == text(); } else return false; } bool MacroKey::isMacroKey(const ValueItem &other) { return typeid(other) == typeid(MacroKey); } QDebug operator<<(QDebug dbg, const MacroKey ¯okey) { dbg.nospace() << "MacroKey " << macrokey.text(); return dbg; } PlainText::PlainText(const PlainText &other) : m_text(other.text()) { /// nothing } PlainText::PlainText(const QString &text) : m_text(text) { /// nothing } void PlainText::setText(const QString &text) { m_text = text; } QString PlainText::text() const { return m_text; } void PlainText::replace(const QString &before, const QString &after, ValueItem::ReplaceMode replaceMode) { if (replaceMode == ValueItem::AnySubstring) m_text = m_text.replace(before, after); else if (replaceMode == ValueItem::CompleteMatch && m_text == before) m_text = after; } bool PlainText::containsPattern(const QString &pattern, Qt::CaseSensitivity caseSensitive) const { const QString text = QString(m_text).remove(ignoredInSorting); return text.contains(pattern, caseSensitive); } bool PlainText::operator==(const ValueItem &other) const { const PlainText *otherPlainText = dynamic_cast(&other); if (otherPlainText != nullptr) { return otherPlainText->text() == text(); } else return false; } bool PlainText::isPlainText(const ValueItem &other) { return typeid(other) == typeid(PlainText); } QDebug operator<<(QDebug dbg, const PlainText &plainText) { dbg.nospace() << "PlainText " << plainText.text(); return dbg; } #ifdef HAVE_KF5 bool VerbatimText::colorLabelPairsInitialized = false; QList VerbatimText::colorLabelPairs = QList(); #endif // HAVE_KF5 VerbatimText::VerbatimText(const VerbatimText &other) : m_text(other.text()) { /// nothing } VerbatimText::VerbatimText(const QString &text) : m_text(text) { /// nothing } void VerbatimText::setText(const QString &text) { m_text = text; } QString VerbatimText::text() const { return m_text; } void VerbatimText::replace(const QString &before, const QString &after, ValueItem::ReplaceMode replaceMode) { if (replaceMode == ValueItem::AnySubstring) m_text = m_text.replace(before, after); else if (replaceMode == ValueItem::CompleteMatch && m_text == before) m_text = after; } bool VerbatimText::containsPattern(const QString &pattern, Qt::CaseSensitivity caseSensitive) const { const QString text = QString(m_text).remove(ignoredInSorting); #ifdef HAVE_KF5 /// Initialize map of labels to color (hex string) only once // FIXME if user changes colors/labels later, it will not be updated here if (!colorLabelPairsInitialized) { colorLabelPairsInitialized = true; /// Read data from config file KSharedConfigPtr config(KSharedConfig::openConfig(QStringLiteral("kbibtexrc"))); KConfigGroup configGroup(config, Preferences::groupColor); const QStringList colorCodes = configGroup.readEntry(Preferences::keyColorCodes, Preferences::defaultColorCodes); const QStringList colorLabels = configGroup.readEntry(Preferences::keyColorLabels, Preferences::defaultColorLabels); /// Translate data from config file into internal mapping for (QStringList::ConstIterator itc = colorCodes.constBegin(), itl = colorLabels.constBegin(); itc != colorCodes.constEnd() && itl != colorLabels.constEnd(); ++itc, ++itl) { ColorLabelPair clp; clp.hexColor = *itc; clp.label = i18n((*itl).toUtf8().constData()); colorLabelPairs << clp; } } #endif // HAVE_KF5 bool contained = text.contains(pattern, caseSensitive); #ifdef HAVE_KF5 if (!contained) { /// Only if simple text match failed, check color labels /// For a match, the user's pattern has to be the start of the color label /// and this verbatim text has to contain the color as hex string for (const auto &clp : const_cast &>(colorLabelPairs)) { contained = text.compare(clp.hexColor, Qt::CaseInsensitive) == 0 && clp.label.contains(pattern, Qt::CaseInsensitive); if (contained) break; } } #endif // HAVE_KF5 return contained; } bool VerbatimText::operator==(const ValueItem &other) const { const VerbatimText *otherVerbatimText = dynamic_cast(&other); if (otherVerbatimText != nullptr) { return otherVerbatimText->text() == text(); } else return false; } bool VerbatimText::isVerbatimText(const ValueItem &other) { return typeid(other) == typeid(VerbatimText); } QDebug operator<<(QDebug dbg, const VerbatimText &verbatimText) { dbg.nospace() << "VerbatimText " << verbatimText.text(); return dbg; } Value::Value() : QVector >() { /// nothing } Value::Value(const Value &other) : QVector >(other) { /// nothing } Value::Value(Value &&other) : QVector >(other) { /// nothing } Value::~Value() { clear(); } void Value::replace(const QString &before, const QString &after, ValueItem::ReplaceMode replaceMode) { QSet > unique; /// Delegate the replace operation to each individual ValueItem /// contained in this Value object for (Value::Iterator it = begin(); it != end();) { (*it)->replace(before, after, replaceMode); bool containedInUnique = false; for (const auto &valueItem : const_cast > &>(unique)) { containedInUnique = *valueItem.data() == *(*it).data(); if (containedInUnique) break; } if (containedInUnique) it = erase(it); else { unique.insert(*it); ++it; } } QSet uniqueValueItemTexts; for (int i = count() - 1; i >= 0; --i) { at(i)->replace(before, after, replaceMode); const QString valueItemText = PlainTextValue::text(*at(i).data()); if (uniqueValueItemTexts.contains(valueItemText)) { /// Due to a replace/delete operation above, an old ValueItem's text /// matches the replaced text. /// Therefore, remove the replaced text to avoid duplicates remove(i); ++i; /// compensate for for-loop's --i } else uniqueValueItemTexts.insert(valueItemText); } } void Value::replace(const QString &before, const QSharedPointer &after) { const QString valueText = PlainTextValue::text(*this); if (valueText == before) { clear(); append(after); } else { QSet uniqueValueItemTexts; for (int i = count() - 1; i >= 0; --i) { QString valueItemText = PlainTextValue::text(*at(i).data()); if (valueItemText == before) { /// Perform replacement operation QVector >::replace(i, after); valueItemText = PlainTextValue::text(*after.data()); // uniqueValueItemTexts.insert(PlainTextValue::text(*after.data())); } if (uniqueValueItemTexts.contains(valueItemText)) { /// Due to a previous replace operation, an existingValueItem's /// text matches a text which was inserted as an "after" ValueItem. /// Therefore, remove the old ValueItem to avoid duplicates. remove(i); } else { /// Neither a replacement, nor a duplicate. Keep this /// ValueItem (memorize as unique) and continue. uniqueValueItemTexts.insert(valueItemText); } } } } bool Value::containsPattern(const QString &pattern, Qt::CaseSensitivity caseSensitive) const { for (const auto &valueItem : const_cast(*this)) { if (valueItem->containsPattern(pattern, caseSensitive)) return true; } return false; } bool Value::contains(const ValueItem &item) const { for (const auto &valueItem : const_cast(*this)) if (valueItem->operator==(item)) return true; return false; } Value &Value::operator=(const Value &rhs) { return static_cast(QVector >::operator =((rhs))); } Value &Value::operator=(Value &&rhs) { return static_cast(QVector >::operator =((rhs))); } Value &Value::operator<<(const QSharedPointer &value) { return static_cast(QVector >::operator<<((value))); } bool Value::operator==(const Value &rhs) const { const Value &lhs = *this; ///< just for readability to have a 'lhs' matching 'rhs' /// Obviously, both Values must be of same size if (lhs.count() != rhs.count()) return false; /// Synchronously iterate over both Values' ValueItems for (Value::ConstIterator lhsIt = lhs.constBegin(), rhsIt = rhs.constBegin(); lhsIt != lhs.constEnd() && rhsIt != rhs.constEnd(); ++lhsIt, ++rhsIt) { /// Are both ValueItems PlainTexts and are both PlainTexts equal? const QSharedPointer lhsPlainText = lhsIt->dynamicCast<PlainText>(); const QSharedPointer<PlainText> rhsPlainText = rhsIt->dynamicCast<PlainText>(); if ((lhsPlainText.isNull() && !rhsPlainText.isNull()) || (!lhsPlainText.isNull() && rhsPlainText.isNull())) return false; if (!lhsPlainText.isNull() && !rhsPlainText.isNull()) { if (*lhsPlainText.data() != *rhsPlainText.data()) return false; } else { /// Remainder of comparisons is like for PlainText above, just for other descendants of ValueItem const QSharedPointer<MacroKey> lhsMacroKey = lhsIt->dynamicCast<MacroKey>(); const QSharedPointer<MacroKey> rhsMacroKey = rhsIt->dynamicCast<MacroKey>(); if ((lhsMacroKey.isNull() && !rhsMacroKey.isNull()) || (!lhsMacroKey.isNull() && rhsMacroKey.isNull())) return false; if (!lhsMacroKey.isNull() && !rhsMacroKey.isNull()) { if (*lhsMacroKey.data() != *rhsMacroKey.data()) return false; } else { const QSharedPointer<Person> lhsPerson = lhsIt->dynamicCast<Person>(); const QSharedPointer<Person> rhsPerson = rhsIt->dynamicCast<Person>(); if ((lhsPerson.isNull() && !rhsPerson.isNull()) || (!lhsPerson.isNull() && rhsPerson.isNull())) return false; if (!lhsPerson.isNull() && !rhsPerson.isNull()) { if (*lhsPerson.data() != *rhsPerson.data()) return false; } else { const QSharedPointer<VerbatimText> lhsVerbatimText = lhsIt->dynamicCast<VerbatimText>(); const QSharedPointer<VerbatimText> rhsVerbatimText = rhsIt->dynamicCast<VerbatimText>(); if ((lhsVerbatimText.isNull() && !rhsVerbatimText.isNull()) || (!lhsVerbatimText.isNull() && rhsVerbatimText.isNull())) return false; if (!lhsVerbatimText.isNull() && !rhsVerbatimText.isNull()) { if (*lhsVerbatimText.data() != *rhsVerbatimText.data()) return false; } else { const QSharedPointer<Keyword> lhsKeyword = lhsIt->dynamicCast<Keyword>(); const QSharedPointer<Keyword> rhsKeyword = rhsIt->dynamicCast<Keyword>(); if ((lhsKeyword.isNull() && !rhsKeyword.isNull()) || (!lhsKeyword.isNull() && rhsKeyword.isNull())) return false; if (!lhsKeyword.isNull() && !rhsKeyword.isNull()) { if (*lhsKeyword.data() != *rhsKeyword.data()) return false; } else { /// If there are other descendants of ValueItem, add tests here ... return false; } } } } } } /// No check failed, so equalness is proven return true; } bool Value::operator!=(const Value &rhs) const { return !operator ==(rhs); } QDebug operator<<(QDebug dbg, const Value &value) { dbg.nospace() << "Value"; if (value.isEmpty()) dbg << " is empty"; else dbg.nospace() << ": " << PlainTextValue::text(value); return dbg; } QString PlainTextValue::text(const Value &value) { ValueItemType vit = VITOther; ValueItemType lastVit = VITOther; QString result; for (const auto &valueItem : value) { QString nextText = text(*valueItem, vit); if (!nextText.isEmpty()) { if (lastVit == VITPerson && vit == VITPerson) result.append(i18n(" and ")); // TODO proper list of authors/editors, not just joined by "and" else if (lastVit == VITPerson && vit == VITOther && nextText == QStringLiteral("others")) { /// "and others" case: replace text to be appended by translated variant nextText = i18n(" and others"); } else if (lastVit == VITKeyword && vit == VITKeyword) result.append("; "); else if (!result.isEmpty()) result.append(" "); result.append(nextText); lastVit = vit; } } return result; } QString PlainTextValue::text(const QSharedPointer<const ValueItem> &valueItem) { const ValueItem *p = valueItem.data(); return text(*p); } QString PlainTextValue::text(const ValueItem &valueItem) { ValueItemType vit; return text(valueItem, vit); } QString PlainTextValue::text(const ValueItem &valueItem, ValueItemType &vit) { QString result; vit = VITOther; -#ifdef HAVE_KF5 - /// Required to have static instance of PlainTextValue here - /// to initialize @see personNameFormatting from settings - /// as well as update @see personNameFormatting upon notification - /// from NotificationHub - static PlainTextValue ptv; -#endif // HAVE_KF5 - bool isVerbatim = false; const PlainText *plainText = dynamic_cast<const PlainText *>(&valueItem); if (plainText != nullptr) { result = plainText->text(); } else { const MacroKey *macroKey = dynamic_cast<const MacroKey *>(&valueItem); if (macroKey != nullptr) { result = macroKey->text(); // TODO Use File to resolve key to full text } else { const Person *person = dynamic_cast<const Person *>(&valueItem); if (person != nullptr) { - result = Person::transcribePersonName(person, personNameFormatting); + result = Person::transcribePersonName(person, Preferences::instance().personNameFormatting()); vit = VITPerson; } else { const Keyword *keyword = dynamic_cast<const Keyword *>(&valueItem); if (keyword != nullptr) { result = keyword->text(); vit = VITKeyword; } else { const VerbatimText *verbatimText = dynamic_cast<const VerbatimText *>(&valueItem); if (verbatimText != nullptr) { result = verbatimText->text(); isVerbatim = true; } else qWarning() << "Cannot interpret ValueItem to one of its descendants"; } } } } /// clean up result string const int len = result.length(); int j = 0; static const QChar cbo = QLatin1Char('{'), cbc = QLatin1Char('}'), bs = QLatin1Char('\\'), mns = QLatin1Char('-'), comma = QLatin1Char(','), thinspace = QChar(0x2009), tilde = QLatin1Char('~'), nobreakspace = QChar(0x00a0); for (int i = 0; i < len; ++i) { if ((result[i] == cbo || result[i] == cbc) && (i < 1 || result[i - 1] != bs)) { /// hop over curly brackets } else if (i < len - 1 && result[i] == bs && result[i + 1] == mns) { /// hop over hyphenation commands ++i; } else if (i < len - 1 && result[i] == bs && result[i + 1] == comma) { /// place '\,' with a thin space result[j] = thinspace; ++i; ++j; } else if (!isVerbatim && result[i] == tilde && (i < 1 || result[i - 1] != bs)) { /// replace '~' with a non-breaking space, /// except if text was verbatim (e.g. a 'localfile' value /// like '~/document.pdf' or 'document.pdf~') result[j] = nobreakspace; ++j; } else { if (i > j) { /// move individual characters forward in result string result[j] = result[i]; } ++j; } } result.resize(j); return result; } - -#ifdef HAVE_KF5 -PlainTextValue::PlainTextValue() -{ - NotificationHub::registerNotificationListener(this, NotificationHub::EventConfigurationChanged); - readConfiguration(); -} - -void PlainTextValue::notificationEvent(int eventId) -{ - if (eventId == NotificationHub::EventConfigurationChanged) - readConfiguration(); -} - -void PlainTextValue::readConfiguration() -{ - KSharedConfigPtr config(KSharedConfig::openConfig(QStringLiteral("kbibtexrc"))); - KConfigGroup configGroup(config, "General"); - personNameFormatting = configGroup.readEntry(Preferences::keyPersonNameFormatting, Preferences::defaultPersonNameFormatting); -} - -QString PlainTextValue::personNameFormatting; -#else // HAVE_KF5 -const QString PlainTextValue::personNameFormatting = QStringLiteral("<%l><, %s><, %f>"); -#endif // HAVE_KF5 diff --git a/src/data/value.h b/src/data/value.h index 9b87a3c0..d8fdef1e 100644 --- a/src/data/value.h +++ b/src/data/value.h @@ -1,314 +1,294 @@ /*************************************************************************** - * Copyright (C) 2004-2018 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * + * 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 BIBTEXVALUE_H #define BIBTEXVALUE_H #include <QVector> #include <QVariant> #include <QSharedPointer> #ifdef HAVE_KF5 #include "kbibtexdata_export.h" #endif // HAVE_KF5 -#ifdef HAVE_KF5 -#include "notificationhub.h" -#endif // HAVE_KF5 - class File; /** * Generic class of an information element in a @see Value object. * In BibTeX, ValueItems are concatenated by "#". */ class KBIBTEXDATA_EXPORT ValueItem { public: enum ReplaceMode {CompleteMatch, AnySubstring}; ValueItem(); virtual ~ValueItem(); virtual void replace(const QString &before, const QString &after, ValueItem::ReplaceMode replaceMode) = 0; /** * Check if this object contains text pattern @p pattern. * @param pattern Pattern to check for * @param caseSensitive Case sensitivity setting for check * @return TRUE if pattern is contained within this value, otherwise FALSE */ virtual bool containsPattern(const QString &pattern, Qt::CaseSensitivity caseSensitive = Qt::CaseInsensitive) const = 0; /** * Compare to instance if they contain the same content. * Subclasses implement under which conditions two instances are equal. * Subclasses of different type are never equal. * @param other other instance to compare with * @return TRUE if both instances are equal */ virtual bool operator==(const ValueItem &other) const = 0; bool operator!=(const ValueItem &other) const; /** * Unique numeric identifier for every ValueItem instance. * @return Unique numeric identifier */ quint64 id() const; protected: /// contains text fragments to be removed before performing a "contains pattern" operation /// includes among other "{" and "}" static const QRegularExpression ignoredInSorting; private: /// Unique numeric identifier const quint64 internalId; /// Keeping track of next available unique numeric identifier static quint64 internalIdCounter; }; class KBIBTEXDATA_EXPORT Keyword: public ValueItem { public: Keyword(const Keyword &other); explicit Keyword(const QString &text); void setText(const QString &text); QString text() const; void replace(const QString &before, const QString &after, ValueItem::ReplaceMode replaceMode) override; bool containsPattern(const QString &pattern, Qt::CaseSensitivity caseSensitive = Qt::CaseInsensitive) const override; bool operator==(const ValueItem &other) const override; /** * Cheap and fast test if another ValueItem is a Keyword object. * @param other another ValueItem object to test * @return true if ValueItem is actually a Keyword */ static bool isKeyword(const ValueItem &other); protected: QString m_text; }; class KBIBTEXDATA_EXPORT Person: public ValueItem { public: /** * Create a representation for a person's name. In bibliographies, * a person is either an author or an editor. The four parameters * cover all common parts of a name. Only first and last name are * mandatory (each person should have those). @param firstName First name of a person. Example: "Peter" @param lastName Last name of a person. Example: "Smith" @param suffix Suffix after a name. Example: "jr." */ Person(const QString &firstName, const QString &lastName, const QString &suffix = QString()); Person(const Person &other); QString firstName() const; QString lastName() const; QString suffix() const; void replace(const QString &before, const QString &after, ValueItem::ReplaceMode replaceMode) override; bool containsPattern(const QString &pattern, Qt::CaseSensitivity caseSensitive = Qt::CaseInsensitive) const override; bool operator==(const ValueItem &other) const override; static QString transcribePersonName(const QString &formatting, const QString &firstName, const QString &lastName, const QString &suffix = QString()); static QString transcribePersonName(const Person *person, const QString &formatting); /** * Cheap and fast test if another ValueItem is a Person object. * @param other another ValueItem object to test * @return true if ValueItem is actually a Person */ static bool isPerson(const ValueItem &other); private: QString m_firstName; QString m_lastName; QString m_suffix; }; QDebug operator<<(QDebug dbg, const Person &person); Q_DECLARE_METATYPE(Person *) class KBIBTEXDATA_EXPORT MacroKey: public ValueItem { public: MacroKey(const MacroKey &other); explicit MacroKey(const QString &text); void setText(const QString &text); QString text() const; bool isValid(); void replace(const QString &before, const QString &after, ValueItem::ReplaceMode replaceMode) override; bool containsPattern(const QString &pattern, Qt::CaseSensitivity caseSensitive = Qt::CaseInsensitive) const override; bool operator==(const ValueItem &other) const override; /** * Cheap and fast test if another ValueItem is a MacroKey object. * @param other another ValueItem object to test * @return true if ValueItem is actually a MacroKey */ static bool isMacroKey(const ValueItem &other); protected: QString m_text; }; QDebug operator<<(QDebug dbg, const MacroKey &macrokey); class KBIBTEXDATA_EXPORT PlainText: public ValueItem { public: PlainText(const PlainText &other); explicit PlainText(const QString &text); void setText(const QString &text); QString text() const; void replace(const QString &before, const QString &after, ValueItem::ReplaceMode replaceMode) override; bool containsPattern(const QString &pattern, Qt::CaseSensitivity caseSensitive = Qt::CaseInsensitive) const override; bool operator==(const ValueItem &other) const override; /** * Cheap and fast test if another ValueItem is a PlainText object. * @param other another ValueItem object to test * @return true if ValueItem is actually a PlainText */ static bool isPlainText(const ValueItem &other); protected: QString m_text; }; QDebug operator<<(QDebug dbg, const PlainText &plainText); class KBIBTEXDATA_EXPORT VerbatimText: public ValueItem { public: VerbatimText(const VerbatimText &other); explicit VerbatimText(const QString &text); void setText(const QString &text); QString text() const; void replace(const QString &before, const QString &after, ValueItem::ReplaceMode replaceMode) override; bool containsPattern(const QString &pattern, Qt::CaseSensitivity caseSensitive = Qt::CaseInsensitive) const override; bool operator==(const ValueItem &other) const override; /** * Cheap and fast test if another ValueItem is a VerbatimText object. * @param other another ValueItem object to test * @return true if ValueItem is actually a VerbatimText */ static bool isVerbatimText(const ValueItem &other); protected: QString m_text; private: #ifdef HAVE_KF5 struct ColorLabelPair { QString hexColor; QString label; }; static QList<ColorLabelPair> colorLabelPairs; static bool colorLabelPairsInitialized; #endif // HAVE_KF5 }; QDebug operator<<(QDebug dbg, const VerbatimText &verbatimText); /** * Container class to hold values of BibTeX entry fields and similar value types in BibTeX file. * A Value object is built from a list of @see ValueItem objects. * @author Thomas Fischer <fischer@unix-ag.uni-kl.de> */ class KBIBTEXDATA_EXPORT Value: public QVector<QSharedPointer<ValueItem> > { public: Value(); Value(const Value &other); Value(Value &&other); virtual ~Value(); void replace(const QString &before, const QString &after, ValueItem::ReplaceMode replaceMode); void replace(const QString &before, const QSharedPointer<ValueItem> &after); /** * Check if this value contains text pattern @p pattern. * @param pattern Pattern to check for * @param caseSensitive Case sensitivity setting for check * @return TRUE if pattern is contained within this value, otherwise FALSE */ bool containsPattern(const QString &pattern, Qt::CaseSensitivity caseSensitive = Qt::CaseInsensitive) const; bool contains(const ValueItem &item) const; Value &operator=(const Value &rhs); Value &operator=(Value &&rhs); Value &operator<<(const QSharedPointer<ValueItem> &value); bool operator==(const Value &rhs) const; bool operator!=(const Value &rhs) const; }; QDebug operator<<(QDebug dbg, const Value &value); class KBIBTEXDATA_EXPORT PlainTextValue -#ifdef HAVE_KF5 - : private NotificationListener -#endif // HAVE_KF5 { public: static QString text(const Value &value); static QString text(const ValueItem &valueItem); static QString text(const QSharedPointer<const ValueItem> &valueItem); -#ifdef HAVE_KF5 - void notificationEvent(int eventId) override; -#endif // HAVE_KF5 - private: enum ValueItemType { VITOther = 0, VITPerson, VITKeyword}; -#ifdef HAVE_KF5 - PlainTextValue(); - void readConfiguration(); - static QString personNameFormatting; -#else // HAVE_KF5 - static const QString personNameFormatting; -#endif // HAVE_KF5 - static QString text(const ValueItem &valueItem, ValueItemType &vit); - }; Q_DECLARE_METATYPE(Value) #endif diff --git a/src/global/preferences.cpp b/src/global/preferences.cpp index 22b579a2..25ecbc84 100644 --- a/src/global/preferences.cpp +++ b/src/global/preferences.cpp @@ -1,184 +1,226 @@ /*************************************************************************** * 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 "preferences.h" #include <QDebug> #ifdef HAVE_KF5 #include <KLocalizedString> #include <KSharedConfig> #include <KConfigWatcher> #include <KConfigGroup> #else // HAVE_KF5 #define I18N_NOOP(text) QObject::tr(text) #define i18n(text) QObject::tr(text) #endif // HAVE_KF5 #ifdef HAVE_KF5 #include "notificationhub.h" #endif // HAVE_KF5 const QString Preferences::groupColor = QStringLiteral("Color Labels"); const QString Preferences::keyColorCodes = QStringLiteral("colorCodes"); const QStringList Preferences::defaultColorCodes {QStringLiteral("#cc3300"), QStringLiteral("#0033ff"), QStringLiteral("#009966"), QStringLiteral("#f0d000")}; const QString Preferences::keyColorLabels = QStringLiteral("colorLabels"); // FIXME // clazy warns: QString(const char*) being called [-Wclazy-qstring-uneeded-heap-allocations] // ... but using QStringLiteral may break the translation process? const QStringList Preferences::defaultColorLabels {I18N_NOOP("Important"), I18N_NOOP("Unread"), I18N_NOOP("Read"), I18N_NOOP("Watch")}; const QString Preferences::groupGeneral = QStringLiteral("General"); const QString Preferences::keyBackupScope = QStringLiteral("backupScope"); const Preferences::BackupScope Preferences::defaultBackupScope = LocalOnly; const QString Preferences::keyNumberOfBackups = QStringLiteral("numberOfBackups"); const int Preferences::defaultNumberOfBackups = 5; const QString Preferences::groupUserInterface = QStringLiteral("User Interface"); const QString Preferences::keyElementDoubleClickAction = QStringLiteral("elementDoubleClickAction"); const Preferences::ElementDoubleClickAction Preferences::defaultElementDoubleClickAction = ActionOpenEditor; const QString Preferences::keyEncoding = QStringLiteral("encoding"); const QString Preferences::defaultEncoding = QStringLiteral("LaTeX"); const QString Preferences::keyStringDelimiter = QStringLiteral("stringDelimiter"); const QString Preferences::defaultStringDelimiter = QStringLiteral("{}"); const QString Preferences::keyQuoteComment = QStringLiteral("quoteComment"); const Preferences::QuoteComment Preferences::defaultQuoteComment = qcNone; const QString Preferences::keyKeywordCasing = QStringLiteral("keywordCasing"); const KBibTeX::Casing Preferences::defaultKeywordCasing = KBibTeX::cLowerCase; const QString Preferences::keyProtectCasing = QStringLiteral("protectCasing"); const Qt::CheckState Preferences::defaultProtectCasing = Qt::PartiallyChecked; const QString Preferences::keyListSeparator = QStringLiteral("ListSeparator"); const QString Preferences::defaultListSeparator = QStringLiteral("; "); const Preferences::BibliographySystem Preferences::defaultBibliographySystem = Preferences::BibTeX; -/** - * Preferences for Data objects - */ -const QString Preferences::keyPersonNameFormatting = QStringLiteral("personNameFormatting"); const QString Preferences::personNameFormatLastFirst = QStringLiteral("<%l><, %s><, %f>"); const QString Preferences::personNameFormatFirstLast = QStringLiteral("<%f ><%l>< %s>"); -const QString Preferences::defaultPersonNameFormatting = personNameFormatLastFirst; +const QString Preferences::defaultPersonNameFormatting = Preferences::personNameFormatLastFirst; class Preferences::Private { private: Preferences *parent; public: #ifdef HAVE_KF5 KSharedConfigPtr config; KConfigWatcher::Ptr watcher; #endif // HAVE_KF5 static const QString keyBibliographySystem; #ifdef HAVE_KF5 bool bibliographySystemDirtyFlag; Preferences::BibliographySystem bibliographySystemCached; #endif // HAVE_KF5 + static const QString keyPersonNameFormatting; +#ifdef HAVE_KF5 + bool personNameFormattingDirtyFlag; + QString personNameFormattingCached; +#endif // HAVE_KF5 + Private(Preferences *_parent) : parent(_parent) { #ifdef HAVE_KF5 config = KSharedConfig::openConfig(QStringLiteral("kbibtexrc")); watcher = KConfigWatcher::create(config); bibliographySystemDirtyFlag = true; bibliographySystemCached = defaultBibliographySystem; + personNameFormattingDirtyFlag = true; + personNameFormattingCached = defaultPersonNameFormatting; #endif // HAVE_KF5 } }; const QString Preferences::Private::keyBibliographySystem = QStringLiteral("BibliographySystem"); +const QString Preferences::Private::keyPersonNameFormatting = QStringLiteral("personNameFormatting"); Preferences &Preferences::instance() { static Preferences singleton; return singleton; } Preferences::Preferences() : d(new Preferences::Private(this)) { #ifdef HAVE_KF5 QObject::connect(d->watcher.data(), &KConfigWatcher::configChanged, [this](const KConfigGroup & group, const QByteArrayList & names) { QSet<int> eventsToPublish; if (group.name() == QStringLiteral("General")) { if (names.contains(Preferences::Private::keyBibliographySystem.toLatin1())) { qDebug() << "Bibliography system got changed by another Preferences instance"; d->bibliographySystemDirtyFlag = true; eventsToPublish.insert(NotificationHub::EventBibliographySystemChanged); } + if (names.contains(Preferences::Private::keyPersonNameFormatting.toLatin1())) { + qDebug() << "Person name formatting got changed by another Preferences instance"; + d->personNameFormattingDirtyFlag = true; + eventsToPublish.insert(NotificationHub::EventConfigurationChanged); + } } for (const int eventId : eventsToPublish) NotificationHub::publishEvent(eventId); }); #endif // HAVE_KF5 } Preferences::~Preferences() { delete d; } Preferences::BibliographySystem Preferences::bibliographySystem() { #ifdef HAVE_KF5 if (d->bibliographySystemDirtyFlag) { d->config->reparseConfiguration(); static const KConfigGroup configGroup(d->config, QStringLiteral("General")); const int index = configGroup.readEntry(Preferences::Private::keyBibliographySystem, static_cast<int>(defaultBibliographySystem)); if (index != static_cast<int>(Preferences::BibTeX) && index != static_cast<int>(Preferences::BibLaTeX)) { qWarning() << "Configuration file setting for Bibliography System has an invalid value, using default as fallback"; setBibliographySystem(defaultBibliographySystem); d->bibliographySystemCached = defaultBibliographySystem; } else d->bibliographySystemCached = static_cast<Preferences::BibliographySystem>(index); d->bibliographySystemDirtyFlag = false; } return d->bibliographySystemCached; #else // HAVE_KF5 return defaultBibliographySystem; #endif // HAVE_KF5 } bool Preferences::setBibliographySystem(const Preferences::BibliographySystem bibliographySystem) { #ifdef HAVE_KF5 d->bibliographySystemDirtyFlag = false; d->bibliographySystemCached = bibliographySystem; static KConfigGroup configGroup(d->config, QStringLiteral("General")); const int prevIndex = configGroup.readEntry(Preferences::Private::keyBibliographySystem, static_cast<int>(defaultBibliographySystem)); const int newIndex = static_cast<int>(bibliographySystem); if (prevIndex == newIndex) return false; /// If old and new bibliography system are the same, return 'false' directly configGroup.writeEntry(Preferences::Private::keyBibliographySystem, newIndex, KConfig::Notify /** to catch changes via KConfigWatcher */); d->config->sync(); NotificationHub::publishEvent(NotificationHub::EventBibliographySystemChanged); #else // HAVE_KF5 Q_UNUSED(bibliographySystem); #endif // HAVE_KF5 return true; } const QMap<Preferences::BibliographySystem, QString> Preferences::availableBibliographySystems() { static const QMap<Preferences::BibliographySystem, QString> result {{Preferences::BibTeX, i18n("BibTeX")}, {Preferences::BibLaTeX, i18n("BibLaTeX")}}; return result; } + +QString Preferences::personNameFormatting() +{ +#ifdef HAVE_KF5 + if (d->personNameFormattingDirtyFlag) { + d->config->reparseConfiguration(); + static const KConfigGroup configGroup(d->config, QStringLiteral("General")); + d->personNameFormattingCached = configGroup.readEntry(Preferences::Private::keyPersonNameFormatting, defaultPersonNameFormatting); + d->personNameFormattingDirtyFlag = false; + } + return d->personNameFormattingCached; +#else // HAVE_KF5 + return defaultPersonNameFormatting; +#endif // HAVE_KF5 +} + +bool Preferences::setPersonNameFormatting(const QString &personNameFormatting) +{ +#ifdef HAVE_KF5 + d->personNameFormattingDirtyFlag = false; + d->personNameFormattingCached = personNameFormatting; + static KConfigGroup configGroup(d->config, QStringLiteral("General")); + const QString prevFormatting = configGroup.readEntry(Preferences::Private::keyPersonNameFormatting, defaultPersonNameFormatting); + if (prevFormatting == personNameFormatting) return false; + configGroup.writeEntry(Preferences::Private::keyPersonNameFormatting, personNameFormatting, KConfig::Notify /** to catch changes via KConfigWatcher */); + d->config->sync(); + NotificationHub::publishEvent(NotificationHub::EventConfigurationChanged); +#else // HAVE_KF5 + Q_UNUSED(personNameFormatting); +#endif // HAVE_KF5 + return true; +} diff --git a/src/global/preferences.h b/src/global/preferences.h index b18b5c02..300962b8 100644 --- a/src/global/preferences.h +++ b/src/global/preferences.h @@ -1,100 +1,109 @@ /*************************************************************************** * 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_GLOBAL_PREFERENCES_H #define KBIBTEX_GLOBAL_PREFERENCES_H #include "kbibtex.h" /** @author Thomas Fischer <fischer@unix-ag.uni-kl.de> */ class Preferences { public: static Preferences &instance(); ~Preferences(); protected: Preferences(); public: /// *** Bibliography system, as of now either BibTeX or BibLaTeX /// Bibliography system: either BibTeX or BibLaTeX enum BibliographySystem { BibTeX = 0, BibLaTeX = 1 }; /// Default bibliography system if nothing else is set or defined static const BibliographySystem defaultBibliographySystem; /// Retrieve current bibliography system BibliographySystem bibliographySystem(); /// Set bibliography system /// @return true if the set bibliography system is differed from the previous value, false if both were the same bool setBibliographySystem(const BibliographySystem bibliographySystem); /// Map of supported bibliography systems, should be the same as in enum BibliographySystem static const QMap<BibliographySystem, QString> availableBibliographySystems(); + /// *** Name formatting like "Firstname Lastname", "Lastname, Firstname", or any other combination + + /// Predefined value for a person formatting, where last name comes before first name + static const QString personNameFormatLastFirst; + /// Predefined value for a person formatting, where first name comes before last name + static const QString personNameFormatFirstLast; + /// Default name formatting for a person if nothing else is set or defined + static const QString defaultPersonNameFormatting; + /// Retrieve current name formatting + QString personNameFormatting(); + /// Set name formatting + /// @return true if the set formatting is differed from the previous value, false if both were the same + bool setPersonNameFormatting(const QString &personNameFormatting); + + enum BackupScope { NoBackup, LocalOnly, BothLocalAndRemote }; enum ElementDoubleClickAction { ActionOpenEditor = 0, ActionViewDocument = 1 }; /** * Preferences for File objects */ enum QuoteComment { qcNone = 0, qcCommand = 1, qcPercentSign = 2 }; static const QString groupColor; static const QString keyColorCodes; static const QStringList defaultColorCodes; static const QString keyColorLabels; static const QStringList defaultColorLabels; static const QString groupGeneral; static const QString keyBackupScope; static const BackupScope defaultBackupScope; static const QString keyNumberOfBackups; static const int defaultNumberOfBackups; static const QString groupUserInterface; static const QString keyElementDoubleClickAction; static const ElementDoubleClickAction defaultElementDoubleClickAction; static const QString keyEncoding; static const QString defaultEncoding; static const QString keyStringDelimiter; static const QString defaultStringDelimiter; static const QString keyQuoteComment; static const QuoteComment defaultQuoteComment; static const QString keyKeywordCasing; static const KBibTeX::Casing defaultKeywordCasing; static const QString keyProtectCasing; static const Qt::CheckState defaultProtectCasing; static const QString keyListSeparator; static const QString defaultListSeparator; -static const QString keyPersonNameFormatting; -static const QString personNameFormatLastFirst; -static const QString personNameFormatFirstLast; -static const QString defaultPersonNameFormatting; - - private: Q_DISABLE_COPY(Preferences) class Private; Private *const d; }; #endif // KBIBTEX_GLOBAL_PREFERENCES_H diff --git a/src/gui/element/elementeditor.cpp b/src/gui/element/elementeditor.cpp index c991329c..462ff428 100644 --- a/src/gui/element/elementeditor.cpp +++ b/src/gui/element/elementeditor.cpp @@ -1,676 +1,677 @@ /*************************************************************************** - * Copyright (C) 2004-2018 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * + * 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 "elementeditor.h" #include <typeinfo> #include <QCheckBox> #include <QLabel> #include <QLayout> #include <QBuffer> #include <QTextStream> #include <QApplication> #include <QFileInfo> #include <QMenu> #include <QScrollArea> #include <QPushButton> #include <KMessageBox> #include <KLocalizedString> #include <KSharedConfig> #include <KConfigGroup> #include "menulineedit.h" #include "entry.h" #include "comment.h" #include "macro.h" #include "preamble.h" #include "element.h" #include "file.h" #include "elementwidgets.h" #include "checkbibtex.h" #include "hidingtabwidget.h" +#include "notificationhub.h" class ElementEditor::ElementEditorPrivate : public ElementEditor::ApplyElementInterface { private: typedef QVector<ElementWidget *> WidgetList; WidgetList widgets; const File *file; QSharedPointer<Entry> internalEntry; QSharedPointer<Macro> internalMacro; QSharedPointer<Preamble> internalPreamble; QSharedPointer<Comment> internalComment; ElementEditor *p; ElementWidget *previousWidget; ReferenceWidget *referenceWidget; SourceWidget *sourceWidget; QPushButton *buttonCheckWithBibTeX; /// Settings management through a push button with menu KSharedConfigPtr config; QPushButton *buttonOptions; QAction *actionForceShowAllWidgets, *actionLimitKeyboardTabStops; public: QSharedPointer<Element> element; HidingTabWidget *tab; bool elementChanged, elementUnapplied; ElementEditorPrivate(bool scrollable, ElementEditor *parent) : file(nullptr), p(parent), previousWidget(nullptr), config(KSharedConfig::openConfig(QStringLiteral("kbibtexrc"))), elementChanged(false), elementUnapplied(false) { internalEntry = QSharedPointer<Entry>(); internalMacro = QSharedPointer<Macro>(); internalComment = QSharedPointer<Comment>(); internalPreamble = QSharedPointer<Preamble>(); createGUI(scrollable); } ~ElementEditorPrivate() override { clearWidgets(); } void clearWidgets() { for (int i = widgets.count() - 1; i >= 0; --i) { QWidget *w = widgets[i]; w->deleteLater(); } widgets.clear(); } void setElement(QSharedPointer<Element> element, const File *file) { this->element = element; this->file = file; referenceWidget->setOriginalElement(element); updateTabVisibility(); } void addTabWidgets() { for (const auto &etl : EntryLayout::instance()) { ElementWidget *widget = new EntryConfiguredWidget(etl, tab); connect(widget, &ElementWidget::modified, p, &ElementEditor::childModified); widgets << widget; if (previousWidget == nullptr) previousWidget = widget; ///< memorize the first tab int index = tab->addTab(widget, widget->icon(), widget->label()); tab->hideTab(index); } ElementWidget *widget = new PreambleWidget(tab); connect(widget, &ElementWidget::modified, p, &ElementEditor::childModified); widgets << widget; int index = tab->addTab(widget, widget->icon(), widget->label()); tab->hideTab(index); widget = new MacroWidget(tab); connect(widget, &ElementWidget::modified, p, &ElementEditor::childModified); widgets << widget; index = tab->addTab(widget, widget->icon(), widget->label()); tab->hideTab(index); FilesWidget *filesWidget = new FilesWidget(tab); connect(filesWidget, &FilesWidget::modified, p, &ElementEditor::childModified); widgets << filesWidget; index = tab->addTab(filesWidget, filesWidget->icon(), filesWidget->label()); tab->hideTab(index); QStringList blacklistedFields; /// blacklist fields covered by EntryConfiguredWidget for (const auto &etl : EntryLayout::instance()) for (const auto &sfl : const_cast<const QList<SingleFieldLayout> &>(etl->singleFieldLayouts)) blacklistedFields << sfl.bibtexLabel; /// blacklist fields covered by FilesWidget blacklistedFields << QString(Entry::ftUrl) << QString(Entry::ftLocalFile) << QString(Entry::ftFile) << QString(Entry::ftDOI) << QStringLiteral("ee") << QStringLiteral("biburl") << QStringLiteral("postscript"); for (int i = 2; i < 256; ++i) // FIXME replace number by constant blacklistedFields << QString(Entry::ftUrl) + QString::number(i) << QString(Entry::ftLocalFile) + QString::number(i) << QString(Entry::ftFile) + QString::number(i) << QString(Entry::ftDOI) + QString::number(i) << QStringLiteral("ee") + QString::number(i) << QStringLiteral("postscript") + QString::number(i); widget = new OtherFieldsWidget(blacklistedFields, tab); connect(widget, &ElementWidget::modified, p, &ElementEditor::childModified); widgets << widget; index = tab->addTab(widget, widget->icon(), widget->label()); tab->hideTab(index); sourceWidget = new SourceWidget(tab); connect(sourceWidget, &ElementWidget::modified, p, &ElementEditor::childModified); widgets << sourceWidget; index = tab->addTab(sourceWidget, sourceWidget->icon(), sourceWidget->label()); tab->hideTab(index); } void createGUI(bool scrollable) { /// load configuration for options push button static const QString configGroupName = QStringLiteral("User Interface"); static const QString keyEnableAllWidgets = QStringLiteral("EnableAllWidgets"); KConfigGroup configGroup(config, configGroupName); const bool showAll = configGroup.readEntry(keyEnableAllWidgets, true); const bool limitKeyboardTabStops = configGroup.readEntry(MenuLineEdit::keyLimitKeyboardTabStops, false); QBoxLayout *vLayout = new QVBoxLayout(p); referenceWidget = new ReferenceWidget(p); referenceWidget->setApplyElementInterface(this); connect(referenceWidget, &ElementWidget::modified, p, &ElementEditor::childModified); connect(referenceWidget, &ReferenceWidget::entryTypeChanged, p, &ElementEditor::updateReqOptWidgets); vLayout->addWidget(referenceWidget, 0); widgets << referenceWidget; if (scrollable) { QScrollArea *sa = new QScrollArea(p); tab = new HidingTabWidget(sa); sa->setFrameStyle(0); sa->setWidget(tab); sa->setWidgetResizable(true); sa->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); sa->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); vLayout->addWidget(sa, 10); } else { tab = new HidingTabWidget(p); vLayout->addWidget(tab, 10); } QBoxLayout *hLayout = new QHBoxLayout(); vLayout->addLayout(hLayout, 0); /// Push button with menu to toggle various options buttonOptions = new QPushButton(QIcon::fromTheme(QStringLiteral("configure")), i18n("Options"), p); hLayout->addWidget(buttonOptions, 0); QMenu *menuOptions = new QMenu(buttonOptions); buttonOptions->setMenu(menuOptions); /// Option to show all fields or only those require for current entry type actionForceShowAllWidgets = menuOptions->addAction(i18n("Show all fields"), p, SLOT(updateReqOptWidgets())); actionForceShowAllWidgets->setCheckable(true); actionForceShowAllWidgets->setChecked(showAll); /// Option to disable tab key focus to reach/visit various non-editable widgets actionLimitKeyboardTabStops = menuOptions->addAction(i18n("Tab key visits only editable fields"), p, SLOT(limitKeyboardTabStops())); actionLimitKeyboardTabStops->setCheckable(true); actionLimitKeyboardTabStops->setChecked(limitKeyboardTabStops); hLayout->addStretch(10); buttonCheckWithBibTeX = new QPushButton(QIcon::fromTheme(QStringLiteral("tools-check-spelling")), i18n("Check with BibTeX"), p); hLayout->addWidget(buttonCheckWithBibTeX, 0); connect(buttonCheckWithBibTeX, &QPushButton::clicked, p, &ElementEditor::checkBibTeX); addTabWidgets(); } void updateTabVisibility() { disconnect(tab, &HidingTabWidget::currentChanged, p, &ElementEditor::tabChanged); if (element.isNull()) { p->setEnabled(false); } else { p->setEnabled(true); int firstEnabledTab = 1024; for (ElementWidget *widget : const_cast<const WidgetList &>(widgets)) { const int index = tab->indexOf(widget); const bool canEdit = widget->canEdit(element.data()); if (widget == referenceWidget) { /// Reference widget widget->setVisible(canEdit); widget->setEnabled(canEdit); } else { if (canEdit) tab->showTab(widget); else if (index >= 0) tab->hideTab(index); if (canEdit && index >= 0 && index < firstEnabledTab) firstEnabledTab = index; } } if (firstEnabledTab < 1024) tab->setCurrentIndex(firstEnabledTab); } connect(tab, &HidingTabWidget::currentChanged, p, &ElementEditor::tabChanged); } /** * If this element editor makes use of a reference widget * (e.g. where entry type and entry id/macro key can be edited), * then return the current value of the entry id/macro key * editing widget. * Otherwise, return an empty string. * * @return Current value of entry id/macro key if any, otherwise empty string */ QString currentId() const { if (referenceWidget != nullptr) return referenceWidget->currentId(); return QString(); } void setCurrentId(const QString &newId) { if (referenceWidget != nullptr) return referenceWidget->setCurrentId(newId); } /** * Return the current File object set for this element editor. * May be NULL if nothing has been set or if it has been cleared. * * @return Current File object, may be nullptr */ const File *currentFile() const { return file; } void apply() { elementChanged = true; elementUnapplied = false; apply(element); } void apply(QSharedPointer<Element> element) override { QSharedPointer<Entry> e = element.dynamicCast<Entry>(); QSharedPointer<Macro> m = e.isNull() ? element.dynamicCast<Macro>() : QSharedPointer<Macro>(); QSharedPointer<Comment> c = e.isNull() && m.isNull() ? element.dynamicCast<Comment>() : QSharedPointer<Comment>(); QSharedPointer<Preamble> p = e.isNull() && m.isNull() && c.isNull() ? element.dynamicCast<Preamble>() : QSharedPointer<Preamble>(); if (tab->currentWidget() == sourceWidget) { /// Very simple if source view is active: BibTeX code contains /// all necessary data if (!e.isNull()) sourceWidget->setElementClass(SourceWidget::elementEntry); else if (!m.isNull()) sourceWidget->setElementClass(SourceWidget::elementMacro); else if (!p.isNull()) sourceWidget->setElementClass(SourceWidget::elementPreamble); else sourceWidget->setElementClass(SourceWidget::elementInvalid); sourceWidget->apply(element); } else { /// Start by assigning the current internal element's /// data to the output element if (!e.isNull()) *e = *internalEntry; else { if (!m.isNull()) *m = *internalMacro; else { if (!c.isNull()) *c = *internalComment; else { if (!p.isNull()) *p = *internalPreamble; else Q_ASSERT_X(element.isNull(), "ElementEditor::ElementEditorPrivate::apply(QSharedPointer<Element> element)", "element is not NULL but could not be cast on a valid Element sub-class"); } } } /// The internal element may be outdated (only updated on tab switch), /// so apply the reference widget's data on the output element if (referenceWidget != nullptr) referenceWidget->apply(element); /// The internal element may be outdated (only updated on tab switch), /// so apply the current widget's data on the output element ElementWidget *currentElementWidget = qobject_cast<ElementWidget *>(tab->currentWidget()); if (currentElementWidget != nullptr) currentElementWidget->apply(element); } } bool validate(QWidget **widgetWithIssue, QString &message) const override { if (tab->currentWidget() == sourceWidget) { /// Source widget must check its textual content for being valid BibTeX code return sourceWidget->validate(widgetWithIssue, message); } else { /// All widgets except for the source widget must validate their values for (WidgetList::ConstIterator it = widgets.begin(); it != widgets.end(); ++it) { if ((*it) == sourceWidget) continue; const bool v = (*it)->validate(widgetWithIssue, message); /// A single widget failing to validate lets the whole validation fail if (!v) return false; } return true; } } void reset() { elementChanged = false; elementUnapplied = false; reset(element); /// show checkbox to enable all fields only if editing an entry actionForceShowAllWidgets->setVisible(!internalEntry.isNull()); /// Disable widgets if necessary if (!actionForceShowAllWidgets->isChecked()) updateReqOptWidgets(); } void reset(QSharedPointer<const Element> element) { for (WidgetList::Iterator it = widgets.begin(); it != widgets.end(); ++it) { (*it)->setFile(file); (*it)->reset(element); (*it)->setModified(false); } QSharedPointer<const Entry> e = element.dynamicCast<const Entry>(); if (!e.isNull()) { internalEntry = QSharedPointer<Entry>(new Entry(*e.data())); sourceWidget->setElementClass(SourceWidget::elementEntry); } else { QSharedPointer<const Macro> m = element.dynamicCast<const Macro>(); if (!m.isNull()) { internalMacro = QSharedPointer<Macro>(new Macro(*m.data())); sourceWidget->setElementClass(SourceWidget::elementMacro); } else { QSharedPointer<const Comment> c = element.dynamicCast<const Comment>(); if (!c.isNull()) { internalComment = QSharedPointer<Comment>(new Comment(*c.data())); sourceWidget->setElementClass(SourceWidget::elementComment); } else { QSharedPointer<const Preamble> p = element.dynamicCast<const Preamble>(); if (!p.isNull()) { internalPreamble = QSharedPointer<Preamble>(new Preamble(*p.data())); sourceWidget->setElementClass(SourceWidget::elementPreamble); } else Q_ASSERT_X(element.isNull(), "ElementEditor::ElementEditorPrivate::reset(QSharedPointer<const Element> element)", "element is not NULL but could not be cast on a valid Element sub-class"); } } } buttonCheckWithBibTeX->setEnabled(!internalEntry.isNull()); } void setReadOnly(bool isReadOnly) { for (WidgetList::Iterator it = widgets.begin(); it != widgets.end(); ++it) (*it)->setReadOnly(isReadOnly); } void updateReqOptWidgets() { /// this function is only relevant if editing an entry (and not e.g. a comment) if (internalEntry.isNull()) return; /// quick-and-dirty test if editing an entry /// make a temporary snapshot of the current state QSharedPointer<Entry> tempEntry = QSharedPointer<Entry>(new Entry()); apply(tempEntry); /// update the enabled/disabled state of required and optional widgets/fields bool forceVisible = actionForceShowAllWidgets->isChecked(); for (ElementWidget *elementWidget : const_cast<const WidgetList &>(widgets)) { elementWidget->showReqOptWidgets(forceVisible, tempEntry->type()); } /// save configuration static const QString configGroupName = QStringLiteral("User Interface"); static const QString keyEnableAllWidgets = QStringLiteral("EnableAllWidgets"); KConfigGroup configGroup(config, configGroupName); configGroup.writeEntry(keyEnableAllWidgets, actionForceShowAllWidgets->isChecked()); config->sync(); } void limitKeyboardTabStops() { /// save configuration static const QString configGroupName = QStringLiteral("User Interface"); KConfigGroup configGroup(config, configGroupName); configGroup.writeEntry(MenuLineEdit::keyLimitKeyboardTabStops, actionLimitKeyboardTabStops->isChecked()); config->sync(); /// notify all listening MenuLineEdit widgets to change their behavior NotificationHub::publishEvent(MenuLineEdit::MenuLineConfigurationChangedEvent); } void switchTo(QWidget *futureTab) { /// Switched from source widget to another widget? const bool isToSourceWidget = futureTab == sourceWidget; /// Switch from some widget to the source widget? const bool isFromSourceWidget = previousWidget == sourceWidget; /// Interprete future widget as an ElementWidget ElementWidget *futureWidget = qobject_cast<ElementWidget *>(futureTab); /// Past and future ElementWidget values are valid? if (previousWidget != nullptr && futureWidget != nullptr) { /// Assign to temp wihch internal variable holds current state QSharedPointer<Element> temp; if (!internalEntry.isNull()) temp = internalEntry; else if (!internalMacro.isNull()) temp = internalMacro; else if (!internalComment.isNull()) temp = internalComment; else if (!internalPreamble.isNull()) temp = internalPreamble; Q_ASSERT_X(!temp.isNull(), "void ElementEditor::ElementEditorPrivate::switchTo(QWidget *newTab)", "temp is NULL"); /// Past widget writes its state to the internal state previousWidget->apply(temp); /// Before switching to source widget, store internally reference widget's state if (isToSourceWidget && referenceWidget != nullptr) referenceWidget->apply(temp); /// Tell future widget to initialize itself based on internal state futureWidget->reset(temp); /// When switchin from source widget to another widget, initialize reference widget if (isFromSourceWidget && referenceWidget != nullptr) referenceWidget->reset(temp); } previousWidget = futureWidget; /// Enable/disable tabs for (WidgetList::Iterator it = widgets.begin(); it != widgets.end(); ++it) (*it)->setEnabled(!isToSourceWidget || *it == futureTab); } /** * Test current entry if it compiles with BibTeX. * Show warnings and errors in message box. */ void checkBibTeX() { /// disable GUI under process p->setEnabled(false); QSharedPointer<Entry> entry = QSharedPointer<Entry>(new Entry()); apply(entry); CheckBibTeX::checkBibTeX(entry, file, p); p->setEnabled(true); } void setModified(bool newIsModified) { for (WidgetList::Iterator it = widgets.begin(); it != widgets.end(); ++it) (*it)->setModified(newIsModified); } void referenceWidgetSetEntryIdByDefault() { referenceWidget->setEntryIdByDefault(); } }; ElementEditor::ElementEditor(bool scrollable, QWidget *parent) : QWidget(parent), d(new ElementEditorPrivate(scrollable, this)) { connect(d->tab, &HidingTabWidget::currentChanged, this, &ElementEditor::tabChanged); } ElementEditor::~ElementEditor() { disconnect(d->tab, &HidingTabWidget::currentChanged, this, &ElementEditor::tabChanged); delete d; } void ElementEditor::apply() { /// The prime problem to tackle in this function is to cope with /// invalid/problematic entry ids or macro keys, respectively: /// - empty ids/keys /// - ids/keys that are duplicates of already used ids/keys QSharedPointer<Entry> entry = d->element.dynamicCast<Entry>(); QSharedPointer<Macro> macro = d->element.dynamicCast<Macro>(); /// Determine id/key as it was set before the current editing started const QString originalId = !entry.isNull() ? entry->id() : (!macro.isNull() ? macro->key() : QString()); /// Get the id/key as it is in the editing widget right now const QString newId = d->currentId(); /// Keep track whether the 'original' id/key or the 'new' id/key will eventually be used enum IdToUse {UseOriginalId, UseNewId}; IdToUse idToUse = UseNewId; if (newId.isEmpty() && !originalId.isEmpty()) { /// New id/key is empty (invalid by definition), so just notify use and revert back to original id/key /// (assuming that original id/key is valid) KMessageBox::sorry(this, i18n("No id was entered, so the previous id '%1' will be restored.", originalId), i18n("No id given")); idToUse = UseOriginalId; } else if (!newId.isEmpty()) { /// If new id/key is not empty, then check if it is identical to another entry/macro in the current file const QSharedPointer<Element> knownElementWithSameId = d->currentFile() != nullptr ? d->currentFile()->containsKey(newId) : QSharedPointer<Element>(); if (!knownElementWithSameId.isNull() && d->element != knownElementWithSameId) { /// Some other, different element (entry or macro) uses same id/key, so ask user how to proceed const int msgBoxResult = KMessageBox::warningContinueCancel(this, i18n("The entered id '%1' is already in use for another element.\n\nKeep original id '%2' instead?", newId, originalId), i18n("Id already in use"), KGuiItem(i18n("Keep duplicate ids")), KGuiItem(i18n("Restore original id"))); idToUse = msgBoxResult == KMessageBox::Continue ? UseNewId : UseOriginalId; } } if (idToUse == UseOriginalId) { /// As 'apply()' above set the 'new' id/key but the 'original' id/key is to be used, /// now UI must be updated accordingly. Changes will propagate to the entry id or /// macro key, respectively, when invoking apply() further down d->setCurrentId(originalId); } /// Apply will always set the 'new' id/key to the entry or macro, respectively d->apply(); d->setModified(false); emit modified(false); } void ElementEditor::reset() { d->reset(); emit modified(false); } bool ElementEditor::validate() { QWidget *widgetWithIssue = nullptr; QString message; if (!validate(&widgetWithIssue, message)) { const QString msgBoxMessage = message.isEmpty() ? i18n("Validation for the current element failed.") : i18n("Validation for the current element failed:\n%1", message); KMessageBox::error(this, msgBoxMessage, i18n("Element validation failed")); if (widgetWithIssue != nullptr) { /// Probe if widget with issue is inside a QTabWiget; if yes, make parenting tab the current tab QWidget *cur = widgetWithIssue; do { QTabWidget *tabWidget = cur->parent() != nullptr && cur->parent()->parent() != nullptr ? qobject_cast<QTabWidget *>(cur->parent()->parent()) : nullptr; if (tabWidget != nullptr) { tabWidget->setCurrentWidget(cur); break; } cur = qobject_cast<QWidget *>(cur->parent()); } while (cur != nullptr); /// Set focus to widget with issue widgetWithIssue->setFocus(); } return false; } return true; } void ElementEditor::setElement(QSharedPointer<Element> element, const File *file) { d->setElement(element, file); d->reset(); emit modified(false); } void ElementEditor::setElement(QSharedPointer<const Element> element, const File *file) { QSharedPointer<Element> clone; QSharedPointer<const Entry> entry = element.dynamicCast<const Entry>(); if (!entry.isNull()) clone = QSharedPointer<Entry>(new Entry(*entry.data())); else { QSharedPointer<const Macro> macro = element.dynamicCast<const Macro>(); if (!macro.isNull()) clone = QSharedPointer<Macro>(new Macro(*macro.data())); else { QSharedPointer<const Preamble> preamble = element.dynamicCast<const Preamble>(); if (!preamble.isNull()) clone = QSharedPointer<Preamble>(new Preamble(*preamble.data())); else { QSharedPointer<const Comment> comment = element.dynamicCast<const Comment>(); if (!comment.isNull()) clone = QSharedPointer<Comment>(new Comment(*comment.data())); else Q_ASSERT_X(element == nullptr, "ElementEditor::ElementEditor(const Element *element, QWidget *parent)", "element is not NULL but could not be cast on a valid Element sub-class"); } } } d->setElement(clone, file); d->reset(); } void ElementEditor::setReadOnly(bool isReadOnly) { d->setReadOnly(isReadOnly); } bool ElementEditor::elementChanged() { return d->elementChanged; } bool ElementEditor::elementUnapplied() { return d->elementUnapplied; } bool ElementEditor::validate(QWidget **widgetWithIssue, QString &message) { return d->validate(widgetWithIssue, message); } QWidget *ElementEditor::currentPage() const { return d->tab->currentWidget(); } void ElementEditor::setCurrentPage(QWidget *page) { if (d->tab->indexOf(page) >= 0) d->tab->setCurrentWidget(page); } void ElementEditor::tabChanged() { d->switchTo(d->tab->currentWidget()); } void ElementEditor::checkBibTeX() { d->checkBibTeX(); } void ElementEditor::childModified(bool m) { if (m) { d->elementUnapplied = true; d->referenceWidgetSetEntryIdByDefault(); } emit modified(m); } void ElementEditor::updateReqOptWidgets() { d->updateReqOptWidgets(); } void ElementEditor::limitKeyboardTabStops() { d->limitKeyboardTabStops(); } diff --git a/src/gui/field/fieldlineedit.cpp b/src/gui/field/fieldlineedit.cpp index 78a96b5b..dd047548 100644 --- a/src/gui/field/fieldlineedit.cpp +++ b/src/gui/field/fieldlineedit.cpp @@ -1,563 +1,556 @@ /*************************************************************************** - * Copyright (C) 2004-2018 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * + * 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 "fieldlineedit.h" #include <typeinfo> #include <QMenu> #include <QSignalMapper> #include <QBuffer> #include <QFileInfo> #include <QDir> #include <QDragEnterEvent> #include <QDropEvent> #include <QPushButton> #include <QFontDatabase> #include <QUrl> #include <QMimeDatabase> #include <QMimeType> #include <QMimeData> #include <QRegularExpression> #include <KRun> #include <KMessageBox> #include <KLocalizedString> #include <KSharedConfig> #include <KConfigGroup> #include <kio_version.h> #include "fileinfo.h" #include "file.h" #include "entry.h" #include "value.h" #include "fileimporterbibtex.h" #include "fileexporterbibtex.h" #include "bibtexfields.h" #include "encoderlatex.h" #include "preferences.h" #include "logging_gui.h" class FieldLineEdit::FieldLineEditPrivate { private: FieldLineEdit *parent; Value currentValue; KBibTeX::TypeFlag preferredTypeFlag; KBibTeX::TypeFlags typeFlags; QSignalMapper *menuTypesSignalMapper; QPushButton *buttonOpenUrl; - KSharedConfigPtr config; - const QString configGroupNameGeneral; - QString personNameFormatting; - 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), config(KSharedConfig::openConfig(QStringLiteral("kbibtexrc"))), configGroupNameGeneral(QStringLiteral("General")), file(nullptr) { + : parent(p), preferredTypeFlag(ptf), typeFlags(tf), file(nullptr) { menuTypes = new QMenu(parent); menuTypesSignalMapper = new QSignalMapper(parent); setupMenu(); connect(menuTypesSignalMapper, static_cast<void(QSignalMapper::*)(int)>(&QSignalMapper::mapped), parent, &FieldLineEdit::slotTypeChanged); 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); - - KConfigGroup configGroup(config, configGroupNameGeneral); - personNameFormatting = configGroup.readEntry(Preferences::keyPersonNameFormatting, Preferences::defaultPersonNameFormatting); } 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<ValueItem> first = value.first(); const QSharedPointer<PlainText> 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(), personNameFormatting); + text = Person::transcribePersonName(person.data(), Preferences::instance().personNameFormatting()); 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"), menuTypesSignalMapper, SLOT(map())); menuTypesSignalMapper->setMapping(action, KBibTeX::tfPlainText); } if (typeFlags.testFlag(KBibTeX::tfReference)) { QAction *action = menuTypes->addAction(iconForTypeFlag(KBibTeX::tfReference), i18n("Reference"), menuTypesSignalMapper, SLOT(map())); menuTypesSignalMapper->setMapping(action, KBibTeX::tfReference); } if (typeFlags.testFlag(KBibTeX::tfPerson)) { QAction *action = menuTypes->addAction(iconForTypeFlag(KBibTeX::tfPerson), i18n("Person"), menuTypesSignalMapper, SLOT(map())); menuTypesSignalMapper->setMapping(action, KBibTeX::tfPerson); } if (typeFlags.testFlag(KBibTeX::tfKeyword)) { QAction *action = menuTypes->addAction(iconForTypeFlag(KBibTeX::tfKeyword), i18n("Keyword"), menuTypesSignalMapper, SLOT(map())); menuTypesSignalMapper->setMapping(action, KBibTeX::tfKeyword); } if (typeFlags.testFlag(KBibTeX::tfSource)) { QAction *action = menuTypes->addAction(iconForTypeFlag(KBibTeX::tfSource), i18n("Source Code"), menuTypesSignalMapper, SLOT(map())); menuTypesSignalMapper->setMapping(action, KBibTeX::tfSource); } if (typeFlags.testFlag(KBibTeX::tfVerbatim)) { QAction *action = menuTypes->addAction(iconForTypeFlag(KBibTeX::tfVerbatim), i18n("Verbatim Text"), menuTypesSignalMapper, SLOT(map())); menuTypesSignalMapper->setMapping(action, 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 #if KIO_VERSION < 0x051f00 // < 5.31.0 KRun::runUrl(urlToOpen, mimeTypeName, parent, false, false); #else // KIO_VERSION < 0x051f00 // >= 5.31.0 KRun::runUrl(urlToOpen, mimeTypeName, parent, KRun::RunFlags()); #endif // KIO_VERSION < 0x051f00 } } 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); } }; 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::slotTypeChanged(int newTypeFlagInt) { const KBibTeX::TypeFlag newTypeFlag = static_cast<KBibTeX::TypeFlag>(newTypeFlagInt); Value value; d->apply(value); if (d->convertValueType(value, newTypeFlag)) { d->typeFlag = newTypeFlag; d->reset(value); } else KMessageBox::error(this, i18n("The current text cannot be used as value of type '%1'.\n\nSwitching back to type '%2'.", BibTeXFields::typeFlagToString(newTypeFlag), BibTeXFields::typeFlagToString(d->typeFlag))); } 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/gui/preferences/settingsgeneralwidget.cpp b/src/gui/preferences/settingsgeneralwidget.cpp index 26209d8c..69a3e040 100644 --- a/src/gui/preferences/settingsgeneralwidget.cpp +++ b/src/gui/preferences/settingsgeneralwidget.cpp @@ -1,132 +1,121 @@ /*************************************************************************** * 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 "settingsgeneralwidget.h" #include <QFormLayout> #include <KLocalizedString> #include <KSharedConfig> #include <KConfigGroup> #include <KComboBox> #include "guihelper.h" #include "value.h" #include "preferences.h" class SettingsGeneralWidget::SettingsGeneralWidgetPrivate { private: SettingsGeneralWidget *p; KComboBox *comboBoxBibliographySystem; KComboBox *comboBoxPersonNameFormatting; const Person dummyPerson; - QString restartRequiredMsg; - - KSharedConfigPtr config; - const QString configGroupName; public: SettingsGeneralWidgetPrivate(SettingsGeneralWidget *parent) - : p(parent), dummyPerson(Person(i18n("John"), i18n("Doe"), i18n("Jr."))), restartRequiredMsg(i18n("Changing this option requires a restart to take effect.")), config(KSharedConfig::openConfig(QStringLiteral("kbibtexrc"))), configGroupName(QStringLiteral("General")) { + : p(parent), dummyPerson(Person(i18n("John"), i18n("Doe"), i18n("Jr."))) { setupGUI(); } void loadState() { comboBoxBibliographySystem->setCurrentIndex(comboBoxBibliographySystem->findData(QVariant::fromValue<int>(static_cast<int>(Preferences::instance().bibliographySystem())))); - KConfigGroup configGroup(config, configGroupName); - QString personNameFormatting = configGroup.readEntry(Preferences::keyPersonNameFormatting, Preferences::defaultPersonNameFormatting); - int row = GUIHelper::selectValue(comboBoxPersonNameFormatting->model(), Person::transcribePersonName(&dummyPerson, personNameFormatting)); + int row = GUIHelper::selectValue(comboBoxPersonNameFormatting->model(), Person::transcribePersonName(&dummyPerson, Preferences::instance().personNameFormatting())); comboBoxPersonNameFormatting->setCurrentIndex(row); } void saveState() { Preferences::instance().setBibliographySystem(static_cast<Preferences::BibliographySystem>(comboBoxBibliographySystem->currentData().toInt())); - - KConfigGroup configGroup(config, configGroupName); - configGroup.writeEntry(Preferences::keyPersonNameFormatting, comboBoxPersonNameFormatting->itemData(comboBoxPersonNameFormatting->currentIndex())); - config->sync(); + Preferences::instance().setPersonNameFormatting(comboBoxPersonNameFormatting->itemData(comboBoxPersonNameFormatting->currentIndex()).toString()); } void resetToDefaults() { comboBoxBibliographySystem->setCurrentIndex(static_cast<int>(Preferences::defaultBibliographySystem)); int row = GUIHelper::selectValue(comboBoxPersonNameFormatting->model(), Person::transcribePersonName(&dummyPerson, Preferences::defaultPersonNameFormatting)); comboBoxPersonNameFormatting->setCurrentIndex(row); } void setupGUI() { QFormLayout *layout = new QFormLayout(p); comboBoxBibliographySystem = new KComboBox(false, p); comboBoxBibliographySystem->setObjectName(QStringLiteral("comboBoxBibliographySystem")); const QMap<Preferences::BibliographySystem, QString> &availableBibliographySystems = Preferences::availableBibliographySystems(); for (QMap<Preferences::BibliographySystem, QString>::ConstIterator it = availableBibliographySystems.constBegin(), itEnd = availableBibliographySystems.constEnd(); it != itEnd; ++it) comboBoxBibliographySystem->addItem(it.value(), QVariant::fromValue<int>(static_cast<int>(it.key()))); layout->addRow(i18n("Bibliography System:"), comboBoxBibliographySystem); connect(comboBoxBibliographySystem, static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged), p, &SettingsGeneralWidget::changed); comboBoxPersonNameFormatting = new KComboBox(false, p); layout->addRow(i18n("Person Names Formatting:"), comboBoxPersonNameFormatting); const QStringList formattingOptions {Preferences::personNameFormatFirstLast, Preferences::personNameFormatLastFirst}; - for (const QString &formattingOption : formattingOptions) { + for (const QString &formattingOption : formattingOptions) comboBoxPersonNameFormatting->addItem(Person::transcribePersonName(&dummyPerson, formattingOption), formattingOption); - } - comboBoxPersonNameFormatting->setToolTip(restartRequiredMsg); connect(comboBoxPersonNameFormatting, static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged), p, &SettingsGeneralWidget::changed); } }; SettingsGeneralWidget::SettingsGeneralWidget(QWidget *parent) : SettingsAbstractWidget(parent), d(new SettingsGeneralWidgetPrivate(this)) { d->loadState(); } SettingsGeneralWidget::~SettingsGeneralWidget() { delete d; } QString SettingsGeneralWidget::label() const { return i18n("General"); } QIcon SettingsGeneralWidget::icon() const { return QIcon::fromTheme(QStringLiteral("kbibtex")); } void SettingsGeneralWidget::loadState() { d->loadState(); } void SettingsGeneralWidget::saveState() { d->saveState(); } void SettingsGeneralWidget::resetToDefaults() { d->resetToDefaults(); } diff --git a/src/io/fileexporterbibtex.cpp b/src/io/fileexporterbibtex.cpp index d5ea3aa2..dfffbcfb 100644 --- a/src/io/fileexporterbibtex.cpp +++ b/src/io/fileexporterbibtex.cpp @@ -1,663 +1,656 @@ /*************************************************************************** - * Copyright (C) 2004-2018 by Thomas Fischer <fischer@unix-ag.uni-kl.de> * + * 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 "fileexporterbibtex.h" #include <typeinfo> #include <QTextCodec> #include <QTextStream> #include <QStringList> #ifdef HAVE_KF5 #include <KSharedConfig> #include <KConfigGroup> #endif // HAVE_KF5 #include "preferences.h" #include "file.h" #include "element.h" #include "entry.h" #include "macro.h" #include "preamble.h" #include "value.h" #include "comment.h" #include "encoderlatex.h" #include "bibtexentries.h" #include "bibtexfields.h" #include "textencoder.h" #include "logging_io.h" FileExporterBibTeX *FileExporterBibTeX::staticFileExporterBibTeX = nullptr; class FileExporterBibTeX::FileExporterBibTeXPrivate { private: FileExporterBibTeX *p; public: QChar stringOpenDelimiter; QChar stringCloseDelimiter; KBibTeX::Casing keywordCasing; Preferences::QuoteComment quoteComment; QString encoding, forcedEncoding; Qt::CheckState protectCasing; QString personNameFormatting; QString listSeparator; bool cancelFlag; QTextCodec *destinationCodec; #ifdef HAVE_KF5 KSharedConfigPtr config; const QString configGroupName, configGroupNameGeneral; #endif // HAVE_KF5 FileExporterBibTeXPrivate(FileExporterBibTeX *parent) : p(parent), keywordCasing(KBibTeX::cLowerCase), quoteComment(Preferences::qcNone), protectCasing(Qt::PartiallyChecked), cancelFlag(false), destinationCodec(nullptr), config(KSharedConfig::openConfig(QStringLiteral("kbibtexrc"))), configGroupName(QStringLiteral("FileExporterBibTeX")), configGroupNameGeneral(QStringLiteral("General")) { /// nothing } void loadState() { #ifdef HAVE_KF5 KConfigGroup configGroup(config, configGroupName); encoding = configGroup.readEntry(Preferences::keyEncoding, Preferences::defaultEncoding); QString stringDelimiter = configGroup.readEntry(Preferences::keyStringDelimiter, Preferences::defaultStringDelimiter); if (stringDelimiter.length() != 2) stringDelimiter = Preferences::defaultStringDelimiter; #else // HAVE_KF5 encoding = QStringLiteral("LaTeX"); const QString stringDelimiter = QStringLiteral("{}"); #endif // HAVE_KF5 stringOpenDelimiter = stringDelimiter[0]; stringCloseDelimiter = stringDelimiter[1]; #ifdef HAVE_KF5 keywordCasing = static_cast<KBibTeX::Casing>(configGroup.readEntry(Preferences::keyKeywordCasing, static_cast<int>(Preferences::defaultKeywordCasing))); quoteComment = static_cast<Preferences::QuoteComment>(configGroup.readEntry(Preferences::keyQuoteComment, static_cast<int>(Preferences::defaultQuoteComment))); protectCasing = static_cast<Qt::CheckState>(configGroup.readEntry(Preferences::keyProtectCasing, static_cast<int>(Preferences::defaultProtectCasing))); - personNameFormatting = configGroup.readEntry(Preferences::keyPersonNameFormatting, QString()); listSeparator = configGroup.readEntry(Preferences::keyListSeparator, Preferences::defaultListSeparator); - - if (personNameFormatting.isEmpty()) { - /// no person name formatting is specified for BibTeX, fall back to general setting - KConfigGroup configGroupGeneral(config, configGroupNameGeneral); - personNameFormatting = configGroupGeneral.readEntry(Preferences::keyPersonNameFormatting, Preferences::defaultPersonNameFormatting); - } #else // HAVE_KF5 keywordCasing = KBibTeX::cLowerCase; quoteComment = qcNone; protectCasing = Qt::PartiallyChecked; - personNameFormatting = QStringLiteral("<%l><, %s><, %f>"); listSeparator = QStringLiteral("; "); #endif // HAVE_KF5 + personNameFormatting = Preferences::instance().personNameFormatting(); } void loadStateFromFile(const File *bibtexfile) { if (bibtexfile == nullptr) return; if (bibtexfile->hasProperty(File::Encoding)) encoding = bibtexfile->property(File::Encoding).toString(); if (!forcedEncoding.isEmpty()) encoding = forcedEncoding; applyEncoding(encoding); if (bibtexfile->hasProperty(File::StringDelimiter)) { QString stringDelimiter = bibtexfile->property(File::StringDelimiter).toString(); if (stringDelimiter.length() != 2) #ifdef HAVE_KF5 stringDelimiter = Preferences::defaultStringDelimiter; #else // HAVE_KF5 stringDelimiter = QStringLiteral("{}"); #endif // HAVE_KF5 stringOpenDelimiter = stringDelimiter[0]; stringCloseDelimiter = stringDelimiter[1]; } if (bibtexfile->hasProperty(File::QuoteComment)) quoteComment = static_cast<Preferences::QuoteComment>(bibtexfile->property(File::QuoteComment).toInt()); if (bibtexfile->hasProperty(File::KeywordCasing)) keywordCasing = static_cast<KBibTeX::Casing>(bibtexfile->property(File::KeywordCasing).toInt()); if (bibtexfile->hasProperty(File::ProtectCasing)) protectCasing = static_cast<Qt::CheckState>(bibtexfile->property(File::ProtectCasing).toInt()); if (bibtexfile->hasProperty(File::NameFormatting)) { /// if the user set "use global default", this property is an empty string /// in this case, keep default value const QString buffer = bibtexfile->property(File::NameFormatting).toString(); personNameFormatting = buffer.isEmpty() ? personNameFormatting : buffer; } if (bibtexfile->hasProperty(File::ListSeparator)) listSeparator = bibtexfile->property(File::ListSeparator).toString(); } bool writeEntry(QIODevice *iodevice, const Entry &entry) { const EncoderLaTeX &laTeXEncoder = EncoderLaTeX::instance(); /// write start of a entry (entry type and id) in plain ASCII iodevice->putChar('@'); iodevice->write(BibTeXEntries::instance().format(entry.type(), keywordCasing).toLatin1().data()); iodevice->putChar('{'); iodevice->write(laTeXEncoder.convertToPlainAscii(entry.id()).toLatin1()); for (Entry::ConstIterator it = entry.constBegin(); it != entry.constEnd(); ++it) { const QString key = it.key(); Value value = it.value(); if (value.isEmpty()) continue; ///< ignore empty key-value pairs QString text = p->internalValueToBibTeX(value, key, leUTF8); if (text.isEmpty()) { /// ignore empty key-value pairs qCWarning(LOG_KBIBTEX_IO) << "Value for field " << key << " is empty" << endl; continue; } // FIXME hack! const QSharedPointer<ValueItem> first = *value.constBegin(); if (PlainText::isPlainText(*first) && (key == Entry::ftTitle || key == Entry::ftBookTitle || key == Entry::ftSeries)) { if (protectCasing == Qt::Checked) addProtectiveCasing(text); else if (protectCasing == Qt::Unchecked) removeProtectiveCasing(text); } iodevice->putChar(','); iodevice->putChar('\n'); iodevice->putChar('\t'); iodevice->write(laTeXEncoder.convertToPlainAscii(BibTeXFields::instance().format(key, keywordCasing)).toLatin1()); iodevice->putChar(' '); iodevice->putChar('='); iodevice->putChar(' '); iodevice->write(TextEncoder::encode(text, destinationCodec)); } iodevice->putChar('\n'); iodevice->putChar('}'); iodevice->putChar('\n'); iodevice->putChar('\n'); return true; } bool writeMacro(QIODevice *iodevice, const Macro &macro) { QString text = p->internalValueToBibTeX(macro.value(), QString(), leUTF8); if (protectCasing == Qt::Checked) addProtectiveCasing(text); else if (protectCasing == Qt::Unchecked) removeProtectiveCasing(text); iodevice->putChar('@'); iodevice->write(BibTeXEntries::instance().format(QStringLiteral("String"), keywordCasing).toLatin1().data()); iodevice->putChar('{'); iodevice->write(TextEncoder::encode(macro.key(), destinationCodec)); iodevice->putChar(' '); iodevice->putChar('='); iodevice->putChar(' '); iodevice->write(TextEncoder::encode(text, destinationCodec)); iodevice->putChar('}'); iodevice->putChar('\n'); iodevice->putChar('\n'); return true; } bool writeComment(QIODevice *iodevice, const Comment &comment) { QString text = comment.text() ; if (comment.useCommand() || quoteComment == Preferences::qcCommand) { iodevice->putChar('@'); iodevice->write(BibTeXEntries::instance().format(QStringLiteral("Comment"), keywordCasing).toLatin1().data()); iodevice->putChar('{'); iodevice->write(TextEncoder::encode(text, destinationCodec)); iodevice->putChar('}'); iodevice->putChar('\n'); iodevice->putChar('\n'); } else if (quoteComment == Preferences::qcPercentSign) { QStringList commentLines = text.split('\n', QString::SkipEmptyParts); for (QStringList::Iterator it = commentLines.begin(); it != commentLines.end(); ++it) { const QByteArray line = TextEncoder::encode(*it, destinationCodec); if (line.length() == 0 || line[0] != QLatin1Char('%')) { /// Guarantee that every line starts with /// a percent sign iodevice->putChar('%'); } iodevice->write(line); iodevice->putChar('\n'); } iodevice->putChar('\n'); } else { iodevice->write(TextEncoder::encode(text, destinationCodec)); iodevice->putChar('\n'); iodevice->putChar('\n'); } return true; } bool writePreamble(QIODevice *iodevice, const Preamble &preamble) { iodevice->putChar('@'); iodevice->write(BibTeXEntries::instance().format(QStringLiteral("Preamble"), keywordCasing).toLatin1().data()); iodevice->putChar('{'); /// Remember: strings from preamble do not get encoded, /// may contain raw LaTeX commands and code iodevice->write(TextEncoder::encode(p->internalValueToBibTeX(preamble.value(), QString(), leRaw), destinationCodec)); iodevice->putChar('}'); iodevice->putChar('\n'); iodevice->putChar('\n'); return true; } QString addProtectiveCasing(QString &text) { /// Check if either /// - text is too short (less than two characters) or /// - text neither starts/stops with double quotation marks /// nor starts with { and stops with } if (text.length() < 2 || ((text[0] != QLatin1Char('"') || text[text.length() - 1] != QLatin1Char('"')) && (text[0] != QLatin1Char('{') || text[text.length() - 1] != QLatin1Char('}')))) { /// Nothing to protect, as this is no text string return text; } bool addBrackets = true; if (text[1] == QLatin1Char('{') && text[text.length() - 2] == QLatin1Char('}')) { /// If the given text looks like this: {{...}} or "{...}" /// still check that it is not like this: {{..}..{..}} addBrackets = false; for (int i = text.length() - 2, count = 0; !addBrackets && i > 1; --i) { if (text[i] == QLatin1Char('{')) ++count; else if (text[i] == QLatin1Char('}')) --count; if (count == 0) addBrackets = true; } } if (addBrackets) text.insert(1, QStringLiteral("{")).insert(text.length() - 1, QStringLiteral("}")); return text; } QString removeProtectiveCasing(QString &text) { /// Check if either /// - text is too short (less than two characters) or /// - text neither starts/stops with double quotation marks /// nor starts with { and stops with } if (text.length() < 2 || ((text[0] != QLatin1Char('"') || text[text.length() - 1] != QLatin1Char('"')) && (text[0] != QLatin1Char('{') || text[text.length() - 1] != QLatin1Char('}')))) { /// Nothing to protect, as this is no text string return text; } if (text[1] != QLatin1Char('{') || text[text.length() - 2] != QLatin1Char('}')) /// Nothing to remove return text; /// If the given text looks like this: {{...}} or "{...}" /// still check that it is not like this: {{..}..{..}} bool removeBrackets = true; for (int i = text.length() - 2, count = 0; removeBrackets && i > 1; --i) { if (text[i] == QLatin1Char('{')) ++count; else if (text[i] == QLatin1Char('}')) --count; if (count == 0) removeBrackets = false; } if (removeBrackets) text.remove(text.length() - 2, 1).remove(1, 1); return text; } QString &protectQuotationMarks(QString &text) { int p = -1; while ((p = text.indexOf(QLatin1Char('"'), p + 1)) > 0) if (p == 0 || text[p - 1] != QLatin1Char('\\')) { text.insert(p + 1, QStringLiteral("}")).insert(p, QStringLiteral("{")); ++p; } return text; } void applyEncoding(QString &encoding) { encoding = encoding.isEmpty() ? QStringLiteral("latex") : encoding.toLower(); destinationCodec = QTextCodec::codecForName(encoding == QStringLiteral("latex") ? "us-ascii" : encoding.toLatin1()); } bool requiresPersonQuoting(const QString &text, bool isLastName) { if (isLastName && !text.contains(QChar(' '))) /** Last name contains NO spaces, no quoting necessary */ return false; else if (!isLastName && !text.contains(QStringLiteral(" and "))) /** First name contains no " and " no quoting necessary */ return false; else if (isLastName && !text.isEmpty() && text[0].isLower()) /** Last name starts with lower-case character (von, van, de, ...) */ // FIXME does not work yet return false; else if (text[0] != '{' || text[text.length() - 1] != '}') /** as either last name contains spaces or first name contains " and " and there is no protective quoting yet, there must be a protective quoting added */ return true; int bracketCounter = 0; for (int i = text.length() - 1; i >= 0; --i) { if (text[i] == '{') ++bracketCounter; else if (text[i] == '}') --bracketCounter; if (bracketCounter == 0 && i > 0) return true; } return false; } }; FileExporterBibTeX::FileExporterBibTeX(QObject *parent) : FileExporter(parent), d(new FileExporterBibTeXPrivate(this)) { /// nothing } FileExporterBibTeX::~FileExporterBibTeX() { delete d; } void FileExporterBibTeX::setEncoding(const QString &encoding) { d->forcedEncoding = encoding; } bool FileExporterBibTeX::save(QIODevice *iodevice, const File *bibtexfile, QStringList *errorLog) { Q_UNUSED(errorLog) if (!iodevice->isWritable() && !iodevice->open(QIODevice::WriteOnly)) { qCWarning(LOG_KBIBTEX_IO) << "Output device not writable"; return false; } bool result = true; const int totalElements = bibtexfile->count(); int currentPos = 0; d->loadState(); d->loadStateFromFile(bibtexfile); if (d->encoding != QStringLiteral("latex")) { Comment encodingComment(QStringLiteral("x-kbibtex-encoding=") + d->encoding, true); result &= d->writeComment(iodevice, encodingComment); } /// Memorize which entries are used in a crossref field QStringList crossRefIdList; for (File::ConstIterator it = bibtexfile->constBegin(); it != bibtexfile->constEnd() && result && !d->cancelFlag; ++it) { QSharedPointer<const Entry> entry = (*it).dynamicCast<const Entry>(); if (!entry.isNull()) { const QString crossRef = PlainTextValue::text(entry->value(Entry::ftCrossRef)); if (!crossRef.isEmpty()) crossRefIdList << crossRef; } } bool allPreamblesAndMacrosProcessed = false; for (File::ConstIterator it = bibtexfile->constBegin(); it != bibtexfile->constEnd() && result && !d->cancelFlag; ++it) { QSharedPointer<const Element> element = (*it); QSharedPointer<const Entry> entry = element.dynamicCast<const Entry>(); if (!entry.isNull()) { /// Postpone entries that are crossref'ed if (crossRefIdList.contains(entry->id())) continue; if (!allPreamblesAndMacrosProcessed) { /// Guarantee that all macros and the preamble are written /// before the first entry (@article, ...) is written for (File::ConstIterator msit = it + 1; msit != bibtexfile->constEnd() && result && !d->cancelFlag; ++msit) { QSharedPointer<const Preamble> preamble = (*msit).dynamicCast<const Preamble>(); if (!preamble.isNull()) { result &= d->writePreamble(iodevice, *preamble); emit progress(++currentPos, totalElements); } else { QSharedPointer<const Macro> macro = (*msit).dynamicCast<const Macro>(); if (!macro.isNull()) { result &= d->writeMacro(iodevice, *macro); emit progress(++currentPos, totalElements); } } } allPreamblesAndMacrosProcessed = true; } result &= d->writeEntry(iodevice, *entry); emit progress(++currentPos, totalElements); } else { QSharedPointer<const Comment> comment = element.dynamicCast<const Comment>(); if (!comment.isNull() && !comment->text().startsWith(QStringLiteral("x-kbibtex-"))) { result &= d->writeComment(iodevice, *comment); emit progress(++currentPos, totalElements); } else if (!allPreamblesAndMacrosProcessed) { QSharedPointer<const Preamble> preamble = element.dynamicCast<const Preamble>(); if (!preamble.isNull()) { result &= d->writePreamble(iodevice, *preamble); emit progress(++currentPos, totalElements); } else { QSharedPointer<const Macro> macro = element.dynamicCast<const Macro>(); if (!macro.isNull()) { result &= d->writeMacro(iodevice, *macro); emit progress(++currentPos, totalElements); } } } } } /// Crossref'ed entries are written last for (File::ConstIterator it = bibtexfile->constBegin(); it != bibtexfile->constEnd() && result && !d->cancelFlag; ++it) { QSharedPointer<const Entry> entry = (*it).dynamicCast<const Entry>(); if (entry.isNull()) continue; if (!crossRefIdList.contains(entry->id())) continue; result &= d->writeEntry(iodevice, *entry); emit progress(++currentPos, totalElements); } iodevice->close(); return result && !d->cancelFlag; } bool FileExporterBibTeX::save(QIODevice *iodevice, const QSharedPointer<const Element> element, const File *bibtexfile, QStringList *errorLog) { Q_UNUSED(errorLog) if (!iodevice->isWritable() && !iodevice->open(QIODevice::WriteOnly)) { qCWarning(LOG_KBIBTEX_IO) << "Output device not writable"; return false; } bool result = false; d->loadState(); d->loadStateFromFile(bibtexfile); if (!d->forcedEncoding.isEmpty()) d->encoding = d->forcedEncoding; d->applyEncoding(d->encoding); const QSharedPointer<const Entry> entry = element.dynamicCast<const Entry>(); if (!entry.isNull()) result |= d->writeEntry(iodevice, *entry); else { const QSharedPointer<const Macro> macro = element.dynamicCast<const Macro>(); if (!macro.isNull()) result |= d->writeMacro(iodevice, *macro); else { const QSharedPointer<const Comment> comment = element.dynamicCast<const Comment>(); if (!comment.isNull()) result |= d->writeComment(iodevice, *comment); else { const QSharedPointer<const Preamble> preamble = element.dynamicCast<const Preamble>(); if (!preamble.isNull()) result |= d->writePreamble(iodevice, *preamble); } } } iodevice->close(); return result && !d->cancelFlag; } void FileExporterBibTeX::cancel() { d->cancelFlag = true; } QString FileExporterBibTeX::valueToBibTeX(const Value &value, const QString &key, UseLaTeXEncoding useLaTeXEncoding) { if (staticFileExporterBibTeX == nullptr) { staticFileExporterBibTeX = new FileExporterBibTeX(nullptr); staticFileExporterBibTeX->d->loadState(); } return staticFileExporterBibTeX->internalValueToBibTeX(value, key, useLaTeXEncoding); } QString FileExporterBibTeX::applyEncoder(const QString &input, UseLaTeXEncoding useLaTeXEncoding) const { switch (useLaTeXEncoding) { case leLaTeX: return EncoderLaTeX::instance().encode(input, Encoder::TargetEncodingASCII); case leUTF8: return EncoderLaTeX::instance().encode(input, Encoder::TargetEncodingUTF8); default: return input; } } QString FileExporterBibTeX::internalValueToBibTeX(const Value &value, const QString &key, UseLaTeXEncoding useLaTeXEncoding) { if (value.isEmpty()) return QString(); QString result; bool isOpen = false; QSharedPointer<const ValueItem> prev; for (const auto &valueItem : value) { QSharedPointer<const MacroKey> macroKey = valueItem.dynamicCast<const MacroKey>(); if (!macroKey.isNull()) { if (isOpen) result.append(d->stringCloseDelimiter); isOpen = false; if (!result.isEmpty()) result.append(" # "); result.append(macroKey->text()); prev = macroKey; } else { QSharedPointer<const PlainText> plainText = valueItem.dynamicCast<const PlainText>(); if (!plainText.isNull()) { QString textBody = applyEncoder(plainText->text(), useLaTeXEncoding); if (!isOpen) { if (!result.isEmpty()) result.append(" # "); result.append(d->stringOpenDelimiter); } else if (!prev.dynamicCast<const PlainText>().isNull()) result.append(' '); else if (!prev.dynamicCast<const Person>().isNull()) { /// handle "et al." i.e. "and others" result.append(" and "); } else { result.append(d->stringCloseDelimiter).append(" # ").append(d->stringOpenDelimiter); } isOpen = true; if (d->stringOpenDelimiter == QLatin1Char('"')) d->protectQuotationMarks(textBody); result.append(textBody); prev = plainText; } else { QSharedPointer<const VerbatimText> verbatimText = valueItem.dynamicCast<const VerbatimText>(); if (!verbatimText.isNull()) { QString textBody = verbatimText->text(); if (!isOpen) { if (!result.isEmpty()) result.append(" # "); result.append(d->stringOpenDelimiter); } else if (!prev.dynamicCast<const VerbatimText>().isNull()) { const QString keyToLower(key.toLower()); if (keyToLower.startsWith(Entry::ftUrl) || keyToLower.startsWith(Entry::ftLocalFile) || keyToLower.startsWith(Entry::ftFile) || keyToLower.startsWith(Entry::ftDOI)) /// Filenames and alike have be separated by a semicolon, /// as a plain comma may be part of the filename or URL result.append(QStringLiteral("; ")); else result.append(' '); } else { result.append(d->stringCloseDelimiter).append(" # ").append(d->stringOpenDelimiter); } isOpen = true; if (d->stringOpenDelimiter == QLatin1Char('"')) d->protectQuotationMarks(textBody); result.append(textBody); prev = verbatimText; } else { QSharedPointer<const Person> person = valueItem.dynamicCast<const Person>(); if (!person.isNull()) { QString firstName = person->firstName(); if (!firstName.isEmpty() && d->requiresPersonQuoting(firstName, false)) firstName = firstName.prepend("{").append("}"); QString lastName = person->lastName(); if (!lastName.isEmpty() && d->requiresPersonQuoting(lastName, true)) lastName = lastName.prepend("{").append("}"); QString suffix = person->suffix(); /// Fall back and enforce comma-based name formatting /// if name contains a suffix like "Jr." /// Otherwise name could not be parsed again reliable const QString pnf = suffix.isEmpty() ? d->personNameFormatting : Preferences::personNameFormatLastFirst; QString thisName = applyEncoder(Person::transcribePersonName(pnf, firstName, lastName, suffix), useLaTeXEncoding); if (!isOpen) { if (!result.isEmpty()) result.append(" # "); result.append(d->stringOpenDelimiter); } else if (!prev.dynamicCast<const Person>().isNull()) result.append(" and "); else { result.append(d->stringCloseDelimiter).append(" # ").append(d->stringOpenDelimiter); } isOpen = true; if (d->stringOpenDelimiter == QLatin1Char('"')) d->protectQuotationMarks(thisName); result.append(thisName); prev = person; } else { QSharedPointer<const Keyword> keyword = valueItem.dynamicCast<const Keyword>(); if (!keyword.isNull()) { QString textBody = applyEncoder(keyword->text(), useLaTeXEncoding); if (!isOpen) { if (!result.isEmpty()) result.append(" # "); result.append(d->stringOpenDelimiter); } else if (!prev.dynamicCast<const Keyword>().isNull()) result.append(d->listSeparator); else { result.append(d->stringCloseDelimiter).append(" # ").append(d->stringOpenDelimiter); } isOpen = true; if (d->stringOpenDelimiter == QLatin1Char('"')) d->protectQuotationMarks(textBody); result.append(textBody); prev = keyword; } } } } } prev = valueItem; } if (isOpen) result.append(d->stringCloseDelimiter); return result; } bool FileExporterBibTeX::isFileExporterBibTeX(const FileExporter &other) { return typeid(other) == typeid(FileExporterBibTeX); }