diff --git a/mobile/sailfishos/bibsearch.pro b/mobile/sailfishos/bibsearch.pro index c6da525e..ac591f33 100644 --- a/mobile/sailfishos/bibsearch.pro +++ b/mobile/sailfishos/bibsearch.pro @@ -1,109 +1,109 @@ TARGET = harbour-bibsearch CONFIG += sailfishapp SOURCES += src/main.cpp src/searchenginelist.cpp \ src/bibliographymodel.cpp ../../src/data/value.cpp \ ../../src/data/entry.cpp ../../src/data/macro.cpp \ ../../src/data/comment.cpp ../../src/data/file.cpp \ ../../src/data/preamble.cpp ../../src/data/element.cpp \ ../../src/networking/internalnetworkaccessmanager.cpp \ ../../src/networking/onlinesearch/onlinesearchabstract.cpp \ ../../src/networking/onlinesearch/onlinesearchbibsonomy.cpp \ ../../src/networking/onlinesearch/onlinesearchacmportal.cpp \ ../../src/networking/onlinesearch/onlinesearchsciencedirect.cpp \ ../../src/networking/onlinesearch/onlinesearchgooglescholar.cpp \ ../../src/networking/onlinesearch/onlinesearchjstor.cpp \ ../../src/networking/onlinesearch/onlinesearchspringerlink.cpp \ ../../src/networking/onlinesearch/onlinesearchieeexplore.cpp \ ../../src/networking/onlinesearch/onlinesearcharxiv.cpp \ ../../src/networking/onlinesearch/onlinesearchingentaconnect.cpp \ ../../src/networking/onlinesearch/onlinesearchpubmed.cpp \ ../../src/global/kbibtex.cpp \ ../../src/io/encoderxml.cpp ../../src/io/encoder.cpp \ ../../src/io/encoderlatex.cpp \ ../../src/io/fileimporter.cpp \ ../../src/io/fileimporterbibtex.cpp \ ../../src/io/textencoder.cpp ../../src/io/xsltransform.cpp \ ../../src/config/preferences.cpp \ ../../src/config/bibtexfields.cpp \ ../../src/config/bibtexentries.cpp \ ../../src/config/logging_config.cpp \ ../../src/networking/logging_networking.cpp \ ../../src/data/logging_data.cpp ../../src/io/logging_io.cpp HEADERS += src/bibliographymodel.h src/searchenginelist.h \ src/kbibtexnamespace.h ../../src/data/entry.h \ ../../src/data/macro.h ../../src/data/comment.h \ ../../src/data/file.h ../../src/data/preamble.h \ ../../src/data/value.h ../../src/data/element.h \ ../../src/networking/internalnetworkaccessmanager.h \ ../../src/networking/onlinesearch/onlinesearchabstract.h \ ../../src/networking/onlinesearch/onlinesearchbibsonomy.h \ ../../src/networking/onlinesearch/onlinesearchacmportal.h \ ../../src/networking/onlinesearch/onlinesearchsciencedirect.h \ ../../src/networking/onlinesearch/onlinesearchgooglescholar.h \ ../../src/networking/onlinesearch/onlinesearchjstor.h \ ../../src/networking/onlinesearch/onlinesearcharxiv.h \ ../../src/networking/onlinesearch/onlinesearchingentaconnect.h \ ../../src/networking/onlinesearch/onlinesearchspringerlink.h \ ../../src/networking/onlinesearch/onlinesearchieeexplore.h \ ../../src/networking/onlinesearch/onlinesearchpubmed.h \ ../../src/global/kbibtex.h \ ../../src/io/encoderxml.h ../../src/io/encoder.h \ ../../src/io/encoderlatex.h ../../src/io/fileimporter.h \ ../../src/io/fileimporterbibtex.h \ ../../src/io/textencoder.h ../../src/io/xsltransform.h \ ../../src/config/preferences.h \ ../../src/config/bibtexfields.h ../../src/config/bibtexentries.h OTHER_FILES += qml/pages/SearchForm.qml qml/pages/EntryView.qml \ qml/pages/AboutPage.qml qml/pages/BibliographyListView.qml \ qml/cover/CoverPage.qml qml/BibSearch.qml \ qml/pages/AboutPage.qml qml/pages/SearchEngineListView.qml \ rpm/$${TARGET}.spec \ rpm/$${TARGET}.yaml \ translations/*.ts \ $${TARGET}.desktop RESOURCES += sailfishos_res.qrc QT += xmlpatterns DEFINES += KBIBTEXGLOBAL_EXPORT= KBIBTEXCONFIG_EXPORT= KBIBTEXDATA_EXPORT= KBIBTEXIO_EXPORT= KBIBTEXNETWORKING_EXPORT= -INCLUDEPATH += ../../src/global $${OUT_PWD}/src/global ../../src/config $${OUT_PWD}/src/config ../../src/data ../../src/networking ../../src/networking/onlinesearch ../../src/io +INCLUDEPATH += ../../src/global $${OUT_PWD}/src/global ../../src/config $${OUT_PWD}/src/config ../../src/data $${OUT_PWD}/src/data ../../src/networking ../../src/networking/onlinesearch ../../src/io CONFIG += sailfishapp_i18n sailfishapp_i18n_idbased TRANSLATIONS += \ translations/$${TARGET}-de.ts \ translations/$${TARGET}-en.ts DISTFILES += \ qml/pages/BibliographyListView.qml \ qml/pages/EntryView.qml \ qml/pages/SearchForm.qml \ qml/pages/SettingsPage.qml \ qml/pages/AboutPage.qml xslt.files = ../../xslt/pam2bibtex.xsl ../../xslt/ieeexploreapiv1-to-bibtex.xsl \ ../../xslt/arxiv2bibtex.xsl ../../xslt/pubmed2bibtex.xsl xslt.path = /usr/share/$${TARGET} INSTALLS += xslt icon86.files = icons/86/$${TARGET}.png icon86.path = /usr/share/icons/hicolor/86x86/apps/ INSTALLS += icon86 icon108.files = icons/108/$${TARGET}.png icon108.path = /usr/share/icons/hicolor/108x108/apps/ INSTALLS += icon108 icon128.files = icons/128/$${TARGET}.png icon128.path = /usr/share/icons/hicolor/128x128/apps/ INSTALLS += icon128 icon172.files = icons/172/$${TARGET}.png icon172.path = /usr/share/icons/hicolor/172x172/apps/ INSTALLS += icon172 icon256.files = icons/256/$${TARGET}.png icon256.path = /usr/share/icons/hicolor/256x256/apps/ INSTALLS += icon256 diff --git a/mobile/sailfishos/src/bibliographymodel.cpp b/mobile/sailfishos/src/bibliographymodel.cpp index 3ea36fe6..3153ac6c 100644 --- a/mobile/sailfishos/src/bibliographymodel.cpp +++ b/mobile/sailfishos/src/bibliographymodel.cpp @@ -1,440 +1,440 @@ /*************************************************************************** * Copyright (C) 2016-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 "bibliographymodel.h" #include -#include "entry.h" +#include #include "onlinesearchabstract.h" const int SortedBibliographyModel::SortAuthorNewestTitle = 0; const int SortedBibliographyModel::SortAuthorOldestTitle = 1; const int SortedBibliographyModel::SortNewestAuthorTitle = 2; const int SortedBibliographyModel::SortOldestAuthorTitle = 3; SortedBibliographyModel::SortedBibliographyModel() : QSortFilterProxyModel(), m_sortOrder(0), model(new BibliographyModel()) { const QSettings settings(QStringLiteral("harbour-bibsearch"), QStringLiteral("BibSearch")); m_sortOrder = settings.value(QStringLiteral("sortOrder"), 0).toInt(); setSourceModel(model); setDynamicSortFilter(true); sort(0); connect(model, &BibliographyModel::busyChanged, this, &SortedBibliographyModel::busyChanged); connect(model, &BibliographyModel::progressChanged, this, &SortedBibliographyModel::progressChanged); } SortedBibliographyModel::~SortedBibliographyModel() { QSettings settings(QStringLiteral("harbour-bibsearch"), QStringLiteral("BibSearch")); settings.setValue(QStringLiteral("sortOrder"), m_sortOrder); delete model; } QHash SortedBibliographyModel::roleNames() const { QHash roles; roles[BibTeXIdRole] = "bibtexid"; roles[FoundViaRole] = "foundVia"; roles[TitleRole] = "title"; roles[AuthorRole] = "author"; roles[AuthorShortRole] = "authorShort"; roles[YearRole] = "year"; roles[WherePublishedRole] = "wherePublished"; roles[UrlRole] = "url"; roles[DoiRole] = "doi"; return roles; } bool SortedBibliographyModel::isBusy() const { if (model != nullptr) return model->isBusy(); else return false; } int SortedBibliographyModel::progress() const { if (model != nullptr) return model->progress(); else return -1; } int SortedBibliographyModel::sortOrder() const { return m_sortOrder; } void SortedBibliographyModel::setSortOrder(int _sortOrder) { if (_sortOrder != m_sortOrder) { m_sortOrder = _sortOrder; invalidate(); emit sortOrderChanged(m_sortOrder); } } QStringList SortedBibliographyModel::humanReadableSortOrder() const { static const QStringList result { //% "Last name, newest first" qtTrId("sortorder-humanreadable-lastname-newestfirst"), ///< SortAuthorNewestTitle //% "Last name, oldest first" qtTrId("sortorder-humanreadable-lastname-oldestfirst"), ///< SortAuthorOldestTitle //% "Newest first, last name" qtTrId("sortorder-humanreadable-newestfirst-lastname"), ///< SortNewestAuthorTitle //% "Oldest first, last name" qtTrId("sortorder-humanreadable-oldestfirst-lastname") ///< SortOldestAuthorTitle }; return result; } void SortedBibliographyModel::startSearch(const QString &freeText, const QString &title, const QString &author) { if (model != nullptr) model->startSearch(freeText, title, author); } void SortedBibliographyModel::clear() { if (model != nullptr) model->clear(); } bool SortedBibliographyModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const { if (model == nullptr) return source_left.row() < source_right.row(); const QSharedPointer entryLeft = model->entry(source_left.row()); const QSharedPointer entryRight = model->entry(source_right.row()); if (entryLeft.isNull() || entryRight.isNull()) return source_left.row() < source_right.row(); SortingTriState sortingTriState = Undecided; switch (m_sortOrder) { case SortAuthorNewestTitle: sortingTriState = compareTitles(entryLeft, entryRight, compareYears(entryLeft, entryRight, MostRecentFirst, compareAuthors(entryLeft, entryRight))); break; case SortAuthorOldestTitle: sortingTriState = compareTitles(entryLeft, entryRight, compareYears(entryLeft, entryRight, LeastRecentFirst, compareAuthors(entryLeft, entryRight))); break; case SortNewestAuthorTitle: sortingTriState = compareTitles(entryLeft, entryRight, compareAuthors(entryLeft, entryRight, compareYears(entryLeft, entryRight, MostRecentFirst))); break; case SortOldestAuthorTitle: sortingTriState = compareTitles(entryLeft, entryRight, compareAuthors(entryLeft, entryRight, compareYears(entryLeft, entryRight, LeastRecentFirst))); break; default: sortingTriState = Undecided; } switch (sortingTriState) { case True: return true; case False: return false; default: return (source_left.row() < source_right.row()); } } SortedBibliographyModel::SortingTriState SortedBibliographyModel::compareAuthors(const QSharedPointer entryLeft, const QSharedPointer entryRight, SortedBibliographyModel::SortingTriState previousDecision) const { if (previousDecision != Undecided) return previousDecision; const Value authorsLeft = entryLeft->operator[](Entry::ftAuthor); const Value authorsRight = entryRight->operator[](Entry::ftAuthor); int p = 0; while (p < authorsLeft.count() && p < authorsRight.count()) { const QSharedPointer personLeft = authorsLeft.at(p).dynamicCast(); const QSharedPointer personRight = authorsRight.at(p).dynamicCast(); if (personLeft.isNull() || personRight.isNull()) return Undecided; const int cmpLast = removeNobiliaryParticle(personLeft->lastName()).localeAwareCompare(removeNobiliaryParticle(personRight->lastName())); if (cmpLast < 0) return True; else if (cmpLast > 0) return False; const int cmpFirst = personLeft->firstName().left(1).localeAwareCompare(personRight->firstName().left(1)); if (cmpFirst < 0) return True; else if (cmpFirst > 0) return False; ++p; } if (authorsLeft.count() < authorsRight.count()) return True; else if (authorsLeft.count() > authorsRight.count()) return False; return Undecided; } SortedBibliographyModel::SortingTriState SortedBibliographyModel::compareYears(const QSharedPointer entryLeft, const QSharedPointer entryRight, AgeSorting ageSorting, SortedBibliographyModel::SortingTriState previousDecision) const { if (previousDecision != Undecided) return previousDecision; bool yearLeftOk = false, yearRightOk = false; const int yearLeft = BibliographyModel::valueToText(entryLeft->operator[](Entry::ftYear)).toInt(&yearLeftOk); const int yearRight = BibliographyModel::valueToText(entryRight->operator[](Entry::ftYear)).toInt(&yearRightOk); if (yearLeftOk && yearRightOk && yearLeft > 1000 && yearRight > 1000 && yearLeft < 3000 && yearRight < 3000) { if (yearLeft < yearRight) return ageSorting == LeastRecentFirst ? True : False; else if (yearLeft > yearRight) return ageSorting == LeastRecentFirst ? False : True; } return Undecided; } SortedBibliographyModel::SortingTriState SortedBibliographyModel::compareTitles(const QSharedPointer entryLeft, const QSharedPointer entryRight, SortedBibliographyModel::SortingTriState previousDecision) const { if (previousDecision != Undecided) return previousDecision; const QString titleLeft = BibliographyModel::valueToText(entryLeft->operator[](Entry::ftTitle)); const QString titleRight = BibliographyModel::valueToText(entryRight->operator[](Entry::ftTitle)); const int titleCmp = titleLeft.localeAwareCompare(titleRight); if (titleCmp < 0) return True; else if (titleCmp > 0) return False; return Undecided; } QString SortedBibliographyModel::removeNobiliaryParticle(const QString &lastname) const { static const QStringList nobiliaryParticles {QStringLiteral("af "), QStringLiteral("d'"), QStringLiteral("de "), QStringLiteral("di "), QStringLiteral("du "), QStringLiteral("of "), QStringLiteral("van "), QStringLiteral("von "), QStringLiteral("zu ")}; for (QStringList::ConstIterator it = nobiliaryParticles.constBegin(); it != nobiliaryParticles.constEnd(); ++it) if (lastname.startsWith(*it)) return lastname.mid(it->length()); return lastname; } BibliographyModel::BibliographyModel() { m_file = new File(); m_runningSearches = 0; m_searchEngineList = new SearchEngineList(); connect(m_searchEngineList, &SearchEngineList::foundEntry, this, &BibliographyModel::newEntry); connect(m_searchEngineList, &SearchEngineList::busyChanged, this, &BibliographyModel::busyChanged); connect(m_searchEngineList, &SearchEngineList::progressChanged, this, &BibliographyModel::progressChanged); } BibliographyModel::~BibliographyModel() { delete m_file; delete m_searchEngineList; } int BibliographyModel::rowCount(const QModelIndex &parent) const { if (parent == QModelIndex()) return m_file->count(); else return 0; } QVariant BibliographyModel::data(const QModelIndex &index, int role) const { if (index.row() < 0 || index.row() >= m_file->count() || index.column() != 0) return QVariant(); const QSharedPointer curEntry = entry(index.row()); if (!curEntry.isNull()) { QString fieldName; switch (role) { case Qt::DisplayRole: /// fall-through on purpose case TitleRole: fieldName = Entry::ftTitle; break; case AuthorRole: fieldName = Entry::ftAuthor; break; case YearRole: fieldName = Entry::ftYear; break; } if (!fieldName.isEmpty()) return valueToText(curEntry->operator[](fieldName)); if (role == BibTeXIdRole) { return curEntry->id(); } else if (role == FoundViaRole) { const QString foundVia = valueToText(curEntry->operator[](QStringLiteral("x-fetchedfrom"))); if (!foundVia.isEmpty()) return foundVia; } else if (role == AuthorRole) { const QString authors = valueToText(curEntry->operator[](Entry::ftAuthor)); if (!authors.isEmpty()) return authors; else return valueToText(curEntry->operator[](Entry::ftEditor)); } else if (role == WherePublishedRole) { const QString journal = valueToText(curEntry->operator[](Entry::ftJournal)); if (!journal.isEmpty()) { const QString volume = valueToText(curEntry->operator[](Entry::ftVolume)); const QString issue = valueToText(curEntry->operator[](Entry::ftNumber)); if (volume.isEmpty()) return journal; else if (issue.isEmpty()) /// but 'volume' is not empty return journal + QStringLiteral(" ") + volume; else /// both 'volume' and 'issue' are not empty return journal + QStringLiteral(" ") + volume + QStringLiteral(" (") + issue + QStringLiteral(")"); } const QString bookTitle = valueToText(curEntry->operator[](Entry::ftBookTitle)); if (!bookTitle.isEmpty()) return bookTitle; const bool isPhdThesis = curEntry->type() == Entry::etPhDThesis; if (isPhdThesis) { const QString school = valueToText(curEntry->operator[](Entry::ftSchool)); if (school.isEmpty()) { //% "Doctoral dissertation" return qtTrId("wherepublished-doctoral-dissertation"); } else { //% "Doctoral dissertation (%1)" return qtTrId("wherepublished-doctoral-dissertation-arg").arg(school); } } const QString school = valueToText(curEntry->operator[](Entry::ftSchool)); if (!school.isEmpty()) return school; const QString publisher = valueToText(curEntry->operator[](Entry::ftPublisher)); if (!publisher.isEmpty()) return publisher; return QStringLiteral(""); } else if (role == AuthorShortRole) { const QStringList authors = valueToList(curEntry->operator[](Entry::ftAuthor)); switch (authors.size()) { case 0: return QString(); ///< empty list of authors case 1: return authors.first(); ///< single author case 2: //% "%1 and %2" return qtTrId("shortauthors-two-author-args").arg(authors.first()).arg(authors[1]); ///< two authors default: //% "%1 and %2 more" return qtTrId("shortauthors-one-author-arg-and-n-others-arg").arg(authors.first()).arg(QString::number(authors.size() - 1)); ///< three or more authors } } else if (role == UrlRole) { const QStringList doiList = valueToList(curEntry->operator[](Entry::ftDOI)); if (!doiList.isEmpty()) return QStringLiteral("http://dx.doi.org/") + doiList.first(); const QStringList urlList = valueToList(curEntry->operator[](Entry::ftUrl)); if (!urlList.isEmpty()) return urlList.first(); const QStringList bibUrlList = valueToList(curEntry->operator[](QStringLiteral("biburl"))); if (!bibUrlList.isEmpty()) return bibUrlList.first(); return QString(); } else if (role == DoiRole) { const QStringList doiList = valueToList(curEntry->operator[](Entry::ftDOI)); if (!doiList.isEmpty()) return doiList.first(); return QString(); } } return QVariant(); } const QSharedPointer BibliographyModel::entry(int row) const { const QSharedPointer element = m_file->at(row); const QSharedPointer result = element.dynamicCast(); return result; } void BibliographyModel::startSearch(const QString &freeText, const QString &title, const QString &author) { QMap query; query[OnlineSearchAbstract::queryKeyFreeText] = freeText; query[OnlineSearchAbstract::queryKeyTitle] = title; query[OnlineSearchAbstract::queryKeyAuthor] = author; m_searchEngineList->resetProgress(); m_runningSearches = 0; const QSettings settings(QStringLiteral("harbour-bibsearch"), QStringLiteral("BibSearch")); for (int i = 0; i < m_searchEngineList->size(); ++i) { OnlineSearchAbstract *osa = m_searchEngineList->at(i); const bool doSearchThisEngine = isSearchEngineEnabled(settings, osa); if (!doSearchThisEngine) continue; osa->startSearch(query, 10 /** TODO make number configurable */); ++m_runningSearches; } } void BibliographyModel::clear() { beginResetModel(); m_file->clear(); endResetModel(); } bool BibliographyModel::isBusy() const { for (QVector::ConstIterator it = m_searchEngineList->constBegin(); it != m_searchEngineList->constEnd(); ++it) { if ((*it)->busy()) return true; } return false; } int BibliographyModel::progress() const { return m_searchEngineList->progress(); } void BibliographyModel::searchFinished() { --m_runningSearches; } void BibliographyModel::newEntry(QSharedPointer e) { const int n = m_file->count(); beginInsertRows(QModelIndex(), n, n); m_file->insert(n, e); endInsertRows(); } QString BibliographyModel::valueToText(const Value &value) { return valueToList(value).join(QStringLiteral(", ")); } QStringList BibliographyModel::valueToList(const Value &value) { if (value.isEmpty()) return QStringList(); QStringList resultItems; const QSharedPointer firstPerson = value.first().dynamicCast(); if (!firstPerson.isNull()) { /// First item in value is a Person, assume all other items are Persons as well for (Value::ConstIterator it = value.constBegin(); it != value.constEnd(); ++it) { QSharedPointer person = (*it).dynamicCast(); if (person.isNull()) continue; const QString name = personToText(person); if (name.isEmpty()) continue; resultItems.append(beautifyLaTeX(name)); } } else { for (Value::ConstIterator it = value.constBegin(); it != value.constEnd(); ++it) { const QString valueItem = valueItemToText(*it); if (valueItem.isEmpty()) continue; resultItems.append(beautifyLaTeX(valueItem)); } } return resultItems; } QString BibliographyModel::personToText(const QSharedPointer &person) { if (person.isNull()) return QString(); QString name = person->lastName(); if (name.isEmpty()) return QString(); const QString firstName = person->firstName().left(1); if (!firstName.isEmpty()) name = name.prepend(QStringLiteral(". ")).prepend(firstName); return name; } QString BibliographyModel::valueItemToText(const QSharedPointer &valueItem) { const QSharedPointer plainText = valueItem.dynamicCast<PlainText>(); if (!plainText.isNull()) return plainText->text(); else { const QSharedPointer<VerbatimText> verbatimText = valueItem.dynamicCast<VerbatimText>(); if (!verbatimText.isNull()) return verbatimText->text(); else { const QSharedPointer<MacroKey> macroKey = valueItem.dynamicCast<MacroKey>(); if (!macroKey.isNull()) return macroKey->text(); else { // TODO return QString(); } } } } QString BibliographyModel::beautifyLaTeX(const QString &input) { QString output = input; static const QStringList toBeRemoved {QStringLiteral("\\textsuperscript{"), QStringLiteral("\\}"), QStringLiteral("\\{"), QStringLiteral("}"), QStringLiteral("{")}; for (QStringList::ConstIterator it = toBeRemoved.constBegin(); it != toBeRemoved.constEnd(); ++it) output = output.remove(*it); return output; } diff --git a/mobile/sailfishos/src/bibliographymodel.h b/mobile/sailfishos/src/bibliographymodel.h index 5c2d1c55..26e666e1 100644 --- a/mobile/sailfishos/src/bibliographymodel.h +++ b/mobile/sailfishos/src/bibliographymodel.h @@ -1,119 +1,118 @@ /*************************************************************************** * Copyright (C) 2016-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 BIBLIOGRAPHY_MODEL_H #define BIBLIOGRAPHY_MODEL_H #include <QAbstractListModel> #include <QSortFilterProxyModel> #include <QSet> -#include "file.h" -#include "value.h" -#include "entry.h" +#include <File> +#include <Value> #include "searchenginelist.h" class OnlineSearchAbstract; class BibliographyModel; class BibliographyRoles { public: enum Roles {BibTeXIdRole = Qt::UserRole + 100, FoundViaRole = Qt::UserRole + 102, EntryRole = Qt::UserRole + 999, TitleRole = Qt::UserRole + 1000, AuthorRole = Qt::UserRole + 1010, AuthorShortRole = Qt::UserRole + 1011, YearRole = Qt::UserRole + 1020, WherePublishedRole = Qt::UserRole + 1100, UrlRole = Qt::UserRole + 1110, DoiRole = Qt::UserRole + 1111}; }; class SortedBibliographyModel : public QSortFilterProxyModel, public BibliographyRoles { Q_OBJECT public: static const int SortAuthorNewestTitle, SortAuthorOldestTitle, SortNewestAuthorTitle, SortOldestAuthorTitle; SortedBibliographyModel(); ~SortedBibliographyModel(); virtual QHash<int, QByteArray> roleNames() const; bool isBusy() const; int progress() const; int sortOrder() const; void setSortOrder(int sortOrder); Q_PROPERTY(int sortOrder READ sortOrder WRITE setSortOrder NOTIFY sortOrderChanged) Q_INVOKABLE QStringList humanReadableSortOrder() const; Q_INVOKABLE void startSearch(const QString &freeText, const QString &title, const QString &author); Q_INVOKABLE void clear(); Q_PROPERTY(bool busy READ isBusy NOTIFY busyChanged) Q_PROPERTY(int progress READ progress NOTIFY progressChanged) signals: void busyChanged(); void progressChanged(); void sortOrderChanged(int sortOrder); protected: virtual bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const; private: enum SortingTriState {True = -1, Undecided = 0, False = 1}; enum AgeSorting {MostRecentFirst = 0, LeastRecentFirst = 1}; SortingTriState compareAuthors(const QSharedPointer<const Entry> entryLeft, const QSharedPointer<const Entry> entryRight, SortingTriState previousDecision = Undecided) const; SortingTriState compareYears(const QSharedPointer<const Entry> entryLeft, const QSharedPointer<const Entry> entryRight, AgeSorting ageSorting, SortingTriState previousDecision = Undecided) const; SortingTriState compareTitles(const QSharedPointer<const Entry> entryLeft, const QSharedPointer<const Entry> entryRight, SortingTriState previousDecision = Undecided) const; int m_sortOrder; BibliographyModel *model; QString removeNobiliaryParticle(const QString &lastname) const; }; class BibliographyModel : public QAbstractListModel, public BibliographyRoles { Q_OBJECT public: explicit BibliographyModel(); ~BibliographyModel(); int rowCount(const QModelIndex &parent = QModelIndex()) const; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; const QSharedPointer<const Entry> entry(int row) const; static QString valueToText(const Value &value); bool isBusy() const; int progress() const; void startSearch(const QString &freeText, const QString &title, const QString &author); void clear(); signals: void busyChanged(); void progressChanged(); private slots: void newEntry(QSharedPointer<Entry>); void searchFinished(); private: File *m_file; int m_runningSearches; SearchEngineList *m_searchEngineList; static QStringList valueToList(const Value &value); static QString personToText(const QSharedPointer<const Person> &person); static QString valueItemToText(const QSharedPointer<ValueItem> &valueItem); static QString beautifyLaTeX(const QString &input); }; #endif // BIBLIOGRAPHY_MODEL_H diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index afa4b801..e0931379 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,87 +1,85 @@ # "Unity build" found at # https://cheind.wordpress.com/2009/12/10/reducing-compilation-time-unity-builds/ function(enable_unity_build UB_SUFFIX SOURCE_VARIABLE_NAME) set(files ${${SOURCE_VARIABLE_NAME}}) # Generate a unique filename for the unity build translation unit set(unit_build_file ${CMAKE_CURRENT_BINARY_DIR}/ub_${UB_SUFFIX}.cpp) # Exclude all translation units from compilation set_source_files_properties(${files} PROPERTIES HEADER_FILE_ONLY true) # Open the ub file file(WRITE ${unit_build_file} "// Unity Build generated by CMake\n") # Add include statement for each translation unit foreach(source_file ${files}) file(APPEND ${unit_build_file} "#include <${source_file}>\n") endforeach(source_file) # Complement list of translation units with the name of ub set(${SOURCE_VARIABLE_NAME} ${${SOURCE_VARIABLE_NAME}} ${unit_build_file} PARENT_SCOPE) endfunction(enable_unity_build) if(Qt5WebEngineWidgets_FOUND) add_definitions( -DHAVE_WEBENGINEWIDGETS ) endif(Qt5WebEngineWidgets_FOUND) if(Qt5WebKitWidgets_FOUND) add_definitions( -DHAVE_WEBKITWIDGETS ) endif(Qt5WebKitWidgets_FOUND) if(Qt5WebEngineWidgets_FOUND) if(Qt5WebKitWidgets_FOUND) message(STATUS "Found both QtWebEngine and QtWebKit, preferring to use QtWebEngine") else(Qt5WebKitWidgets_FOUND) message(STATUS "Found QtWebEngine, but not QtWebKit, therefore going to use QtWebEngine") endif(Qt5WebKitWidgets_FOUND) else(Qt5WebEngineWidgets_FOUND) if(Qt5WebKitWidgets_FOUND) message(STATUS "Found QtWebKit, but not QtWebEngine, therefore going to use QtWebKit") else(Qt5WebKitWidgets_FOUND) message(STATUS "Found neither QtWebEngine nor QtWebKit, therefore trying to locate a KPart for HTML data") endif(Qt5WebKitWidgets_FOUND) endif(Qt5WebEngineWidgets_FOUND) add_subdirectory(global) add_subdirectory(config) -add_subdirectory( - data -) +add_subdirectory(data) add_subdirectory( io ) add_subdirectory( processing ) add_subdirectory( networking ) add_subdirectory( gui ) add_subdirectory( program ) add_subdirectory( parts ) if( BUILD_TESTING ) add_subdirectory( test ) endif( BUILD_TESTING ) # install( # FILES # kbibtexnamespace.h # DESTINATION # ${INCLUDE_INSTALL_DIR}/kbibtex # COMPONENT # development # ) diff --git a/src/data/CMakeLists.txt b/src/data/CMakeLists.txt index 8e92f0f8..89f316ef 100644 --- a/src/data/CMakeLists.txt +++ b/src/data/CMakeLists.txt @@ -1,60 +1,98 @@ # KBibTeXData library set( kbibtexdata_LIB_SRCS comment.cpp element.cpp entry.cpp file.cpp macro.cpp preamble.cpp value.cpp models/filemodel.cpp logging_data.cpp ) -set( - kbibtexdata_HDRS - comment.h - element.h - entry.h - file.h - macro.h - preamble.h - value.h - models/filemodel.h -) - if(UNITY_BUILD) enable_unity_build(kbibtexdata kbibtexdata_LIB_SRCS) endif(UNITY_BUILD) -add_library( - kbibtexdata +add_library(kbibtexdata SHARED ${kbibtexdata_LIB_SRCS} ) - -target_link_libraries( kbibtexdata - Qt5::Core - Qt5::Widgets - KF5::I18n - KF5::XmlGui - kbibtexconfig -) +generate_export_header(kbibtexdata) +add_library(KBibTeX::Data ALIAS kbibtexdata) set_target_properties( kbibtexdata PROPERTIES EXPORT_NAME "kbibtexdata" VERSION ${KBIBTEX_RELEASE_VERSION} SOVERSION ${KBIBTEX_SOVERSION} ) +target_include_directories(kbibtexdata + INTERFACE + $<INSTALL_INTERFACE:${KDE_INSTALL_INCLUDEDIR}/KBibTeX/data> +) + +target_link_libraries(kbibtexdata + PUBLIC + Qt5::Core + KBibTeX::Config + PRIVATE + Qt5::Gui + KF5::ConfigCore + KF5::ConfigGui + KF5::I18n + KBibTeX::Global +) + install( - TARGETS - kbibtexdata - ${INSTALL_TARGETS_DEFAULT_ARGS} + TARGETS kbibtexdata + EXPORT kbibtexdata-targets + ${KDE_INSTALL_TARGETS_DEFAULT_ARGS} ) -generate_export_header( kbibtexdata ) +set_target_properties(kbibtexdata PROPERTIES + EXPORT_NAME "Data" +) + +ecm_generate_headers(kbibtexdata_HEADERS + HEADER_NAMES + Comment + Element + Entry + File + Macro + Preamble + Value + models/FileModel + REQUIRED_HEADERS kbibtexdata_HEADERS +) + +install(FILES + ${CMAKE_CURRENT_BINARY_DIR}/kbibtexdata_export.h + ${kbibtexdata_HEADERS} + DESTINATION ${KDE_INSTALL_INCLUDEDIR}/KBibTeX/data + COMPONENT Devel +) + +include(CMakePackageConfigHelpers) +write_basic_package_version_file( + ${CMAKE_CURRENT_BINARY_DIR}/KBibTeXData-configVersion.cmake + VERSION ${PROJECT_VERSION} + COMPATIBILITY ExactVersion +) + +configure_package_config_file(${CMAKE_CURRENT_LIST_DIR}/cmake/KBibTeXData-config.cmake.in + ${CMAKE_CURRENT_BINARY_DIR}/KBibTeXData-config.cmake + INSTALL_DESTINATION ${KDE_INSTALL_LIBDIR}/cmake/KBibTeX +) + +install(FILES + ${CMAKE_CURRENT_BINARY_DIR}/KBibTeXData-config.cmake + ${CMAKE_CURRENT_BINARY_DIR}/KBibTeXData-configVersion.cmake + DESTINATION ${KDE_INSTALL_LIBDIR}/cmake/KBibTeX +) diff --git a/src/data/cmake/KBibTeXData-config.cmake.in b/src/data/cmake/KBibTeXData-config.cmake.in new file mode 100644 index 00000000..f4e713cb --- /dev/null +++ b/src/data/cmake/KBibTeXData-config.cmake.in @@ -0,0 +1,32 @@ +@PACKAGE_INIT@ + +find_package( + Qt5 @QT_MIN_VERSION@ + CONFIG + REQUIRED + Core + Gui +) + +find_package( + KF5 @KF5_MIN_VERSION@ + CONFIG + REQUIRED + ConfigCore + ConfigGui + I18n +) + +find_package( + KBibTeX @PROJECT_VERSION@ EXACT + CONFIG + REQUIRED + Config + Global +) + +if(NOT TARGET KBibTeX::Data) + include("${KBibTeXData_CMAKE_DIR}/KBibTeXData-targets.cmake") +endif() + +set(KBibTeXData_LIBRARIES KBibTeX::Data) diff --git a/src/data/comment.h b/src/data/comment.h index 5771b7cc..df750d78 100644 --- a/src/data/comment.h +++ b/src/data/comment.h @@ -1,92 +1,96 @@ /*************************************************************************** - * 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 KBIBTEX_DATA_COMMENT_H #define KBIBTEX_DATA_COMMENT_H -#include "element.h" +#include <Element> + +#ifdef HAVE_KF5 +#include "kbibtexdata_export.h" +#endif // HAVE_KF5 /** * This class represents a comment in a BibTeX file. In BibTeX files, * everything that cannot be interpreted as a BibTeX comment is see * as a comment. Alternatively, the comment command can be used in BibTeX * files. * @author Thomas Fischer <fischer@unix-ag.uni-kl.de> */ class KBIBTEXDATA_EXPORT Comment : public Element { public: /** * Create a new comment with a given text. * @param text comment's textual content * @param useCommand mark this comment to use BibTeX's comment command */ explicit Comment(const QString &text = QString(), bool useCommand = false); /** * Copy constructor cloning another comment object. * @param other comment object to clone */ Comment(const Comment &other); ~Comment() override; /** * Assignment operator, working similar to a copy constructor, * but overwrites the current object's values. */ Comment &operator= (const Comment &other); /** * Retrieve the text of this comment. * @return text of this comment */ QString text() const; /** * Set the text of this comment. * @param text text of this comment */ void setText(const QString &text); /** * Retrieve the flag whether to use BibTeX's comment command or not. * @return mark if this comment has to use BibTeX's comment command */ bool useCommand() const; /** * Set the flag whether to use BibTeX's comment command or not. * @param useCommand set if this comment has to use BibTeX's comment command */ void setUseCommand(bool useCommand); /** * Cheap and fast test if another Element is a Comment object. * @param other another Element object to test * @return true if Element is actually a Comment */ static bool isComment(const Element &other); private: class CommentPrivate; CommentPrivate *const d; }; QDebug operator<<(QDebug dbg, const Comment &comment); #endif // KBIBTEX_DATA_COMMENT_H diff --git a/src/data/element.h b/src/data/element.h index d4420fec..d535838e 100644 --- a/src/data/element.h +++ b/src/data/element.h @@ -1,43 +1,43 @@ /*************************************************************************** - * Copyright (C) 2004-2017 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 KBIBTEX_DATA_ELEMENT_H #define KBIBTEX_DATA_ELEMENT_H -#include "file.h" +#include <File> #ifdef HAVE_KF5 #include "kbibtexdata_export.h" #endif // HAVE_KF5 /** * Base class for bibliographic elements in a BibTeX file. * @author Thomas Fischer <fischer@unix-ag.uni-kl.de> */ class KBIBTEXDATA_EXPORT Element { public: Element(); virtual ~Element() { /* nothing */ } private: int uniqueId; }; #endif // KBIBTEX_DATA_ELEMENT_H diff --git a/src/data/entry.h b/src/data/entry.h index 22717c08..3ad7a1e7 100644 --- a/src/data/entry.h +++ b/src/data/entry.h @@ -1,248 +1,248 @@ /*************************************************************************** - * 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 KBIBTEX_DATA_ENTRY_H #define KBIBTEX_DATA_ENTRY_H #include <QMap> -#include "element.h" -#include "value.h" +#include <Element> +#include <Value> class File; /** * This class represents an entry in a BibTeX file such as an article * or a book. This class is essentially a map from keys such as title, * year or other bibliography data to corresponding values. * @see Value * @author Thomas Fischer <fischer@unix-ag.uni-kl.de> */ class KBIBTEXDATA_EXPORT Entry : public Element, public QMap<QString, Value> { public: /** Representation of the BibTeX field key "abstract" */ static const QString ftAbstract; /** Representation of the BibTeX field key "address" */ static const QString ftAddress; /** Representation of the BibTeX field key "author" */ static const QString ftAuthor; /** Representation of the BibTeX field key "booktitle" */ static const QString ftBookTitle; /** Representation of the BibTeX field key "chapter" */ static const QString ftChapter; /** Representation of the BibTeX field key "x-color" */ static const QString ftColor; /** Representation of the BibTeX field key "comment" */ static const QString ftComment; /** Representation of the BibTeX field key "crossref" */ static const QString ftCrossRef; /** Representation of the BibTeX field key "doi" */ static const QString ftDOI; /** Representation of the BibTeX field key "editor" */ static const QString ftEditor; /** Representation of the BibTeX field key "file" */ static const QString ftFile; /** Representation of the BibTeX field key "issn" */ static const QString ftISSN; /** Representation of the BibTeX field key "isbn" */ static const QString ftISBN; /** Representation of the BibTeX field key "journal" */ static const QString ftJournal; /** Representation of the BibTeX field key "keywords" */ static const QString ftKeywords; /** Representation of the BibTeX field key "localfile" */ static const QString ftLocalFile; /** Representation of the BibTeX field key "location" */ static const QString ftLocation; /** Representation of the BibTeX field key "month" */ static const QString ftMonth; /** Representation of the BibTeX field key "note" */ static const QString ftNote; /** Representation of the BibTeX field key "number" */ static const QString ftNumber; /** Representation of the BibTeX field key "pages" */ static const QString ftPages; /** Representation of the BibTeX field key "publisher" */ static const QString ftPublisher; /** Representation of the BibTeX field key "school" */ static const QString ftSchool; /** Representation of the BibTeX field key "series" */ static const QString ftSeries; /** Representation of the BibTeX field key "x-stars" */ static const QString ftStarRating; /** Representation of the BibTeX field key "title" */ static const QString ftTitle; /** Representation of the BibTeX field key "url" */ static const QString ftUrl; /** Representation of the BibLaTeX field key "urldate" */ static const QString ftUrlDate; /** Representation of the BibTeX field key "volume" */ static const QString ftVolume; /** Representation of the BibTeX field key "year" */ static const QString ftYear; /** Representation of the BibLaTeX field key "year" */ static const QString ftXData; /** Representation of the BibTeX entry type "Article" */ static const QString etArticle; /** Representation of the BibTeX entry type "Book" */ static const QString etBook; /** Representation of the BibTeX entry type "InBook" */ static const QString etInBook; /** Representation of the BibTeX entry type "InProceedings" */ static const QString etInProceedings; /** Representation of the BibTeX entry type "Proceedings" */ static const QString etProceedings; /** Representation of the BibTeX entry type "Misc" */ static const QString etMisc; /** Representation of the BibTeX entry type "TechReport" */ static const QString etTechReport; /** Representation of the BibTeX entry type "PhDThesis" */ static const QString etPhDThesis; /** Representation of the BibTeX entry type "MastersThesis" */ static const QString etMastersThesis; /** Representation of the BibTeX entry type "Unpublished" */ static const QString etUnpublished; /** * Create a new entry type. Both type and id are optionally, * allowing to call the constructor as Entry() only. * Both type and id can be set and retrieved later. * @param type type of this entry */ explicit Entry(const QString &type = QString(), const QString &id = QString()); /** * Copy constructor cloning another entry object. * @param other entry object to clone */ Entry(const Entry &other); ~Entry() override; bool operator==(const Entry &other) const; bool operator!=(const Entry &other) const; /** * Assignment operator, working similar to a copy constructor, * but overwrites the current object's values. */ Entry &operator= (const Entry &other); /** * Set the type of this entry. Common values are "article" or "book". * @param type type of this entry */ void setType(const QString &type); /** * Retrieve the type of this entry. Common values are "article" or "book". * @return type of this entry */ QString type() const; /** * Set the id of this entry. In LaTeX, this id is used to refer to a BibTeX * entry using the "ref" command. * @param id id of this entry */ void setId(const QString &id); /** * Retrieve the id of this entry. In LaTeX, this id is used to refer to a BibTeX * entry using the "ref" command. * @return id of this entry */ QString id() const; /** * Re-implementation of QMap's value function, but performing a case-insensitive * match on the key. E.g. querying for key "title" will find a key-value pair with * key "TITLE". * @see #contains(const QString&) * @param key field name to search for * @return found value or Value() if nothing found */ const Value value(const QString &key) const; int remove(const QString &key); /** * Re-implementation of QMap's contains function, but performing a case-insensitive * match on the key. E.g. querying for key "title" will find a key-value pair with * key "TITLE". * @see #value(const QString&) * @param key field name to search for * @return true if value with key found, else false */ bool contains(const QString &key) const; /** * Resolve cross references in an entry. This function evaluates known cross * reference fields such as 'crossref' from BibTeX and 'xdata' from BibLaTeX. * Fields that exist in cross-referenced entries will be copied in the entry * that this function will eventually return. * This function always returns a new Entry that must be deleted by the caller, * even if no cross reference fields existed in the original entry or no fields * were copied from cross-referenced entries. * This new Entry will not be part of any File object, not even the one passed * to this function. * Tip: As the returned Entry is most often only used temporary, it may be a * good idea to wrap it into a QScopedPointer<const Entry> at the caller's side. * @param bibTeXfile bibliography to search for entries being cross-referenced * @return New entry object with known cross references resolved */ Entry *resolveCrossref(const File *bibTeXfile) const; static QStringList authorsLastName(const Entry &entry); QStringList authorsLastName() const; quint64 uniqueId() const; /** * Cheap and fast test if another Element is a Entry object. * @param other another Element object to test * @return true if Element is actually a Entry */ static bool isEntry(const Element &other); private: /// Unique numeric identifier const quint64 internalUniqueId; /// Keeping track of next available unique numeric identifier static quint64 internalUniqueIdCounter; class EntryPrivate; EntryPrivate *const d; }; Q_DECLARE_METATYPE(Entry) Q_DECLARE_METATYPE(Entry *) QDebug operator<<(QDebug dbg, const Entry &Entry); /** * Comparison operator, necessary for QMap operations. */ static inline bool operator< (const QSharedPointer<Entry> &a, const QSharedPointer<Entry> &b) { return a->uniqueId() < b->uniqueId(); } #endif // KBIBTEX_DATA_ENTRY_H diff --git a/src/data/file.cpp b/src/data/file.cpp index 3d4764ae..892d7036 100644 --- a/src/data/file.cpp +++ b/src/data/file.cpp @@ -1,338 +1,338 @@ /*************************************************************************** * 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 "file.h" #include <QFile> #include <QTextStream> #include <QIODevice> #include <QStringList> -#include "preferences.h" +#include <Preferences> #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; public: const quint64 internalId; QHash<QString, QVariant> properties; explicit FilePrivate(File *parent) : validInvalidField(valid), internalId(++internalIdCounter) { Q_UNUSED(parent) const bool isValid = checkValidity(); if (!isValid) qCDebug(LOG_KBIBTEX_DATA) << "Creating File instance" << internalId << " Valid?" << isValid; loadConfiguration(); } ~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; } void loadConfiguration() { /// Load and set configuration as stored in settings properties.insert(File::Encoding, Preferences::instance().bibTeXEncoding()); properties.insert(File::StringDelimiter, Preferences::instance().bibTeXStringDelimiter()); properties.insert(File::QuoteComment, Preferences::instance().bibTeXQuoteComment()); properties.insert(File::KeywordCasing, Preferences::instance().bibTeXKeywordCasing()); properties.insert(File::NameFormatting, Preferences::instance().personNameFormat()); properties.insert(File::ProtectCasing, static_cast<int>(Preferences::instance().bibTeXProtectCasing() ? Qt::Checked : Qt::Unchecked)); properties.insert(File::ListSeparator, Preferences::instance().bibTeXListSeparator()); } 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<QSharedPointer<Element> >(), d(new FilePrivate(this)) { /// nothing } File::File(const File &other) : QList<QSharedPointer<Element> >(other), d(new FilePrivate(this)) { d->operator =(*other.d); } File::File(File &&other) : QList<QSharedPointer<Element> >(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<const Entry> myEntry = myIt->dynamicCast<const Entry>(); QSharedPointer<const Entry> otherEntry = otherIt->dynamicCast<const Entry>(); 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<const Macro> myMacro = myIt->dynamicCast<const Macro>(); QSharedPointer<const Macro> otherMacro = otherIt->dynamicCast<const Macro>(); 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<const Preamble> myPreamble = myIt->dynamicCast<const Preamble>(); QSharedPointer<const Preamble> otherPreamble = otherIt->dynamicCast<const Preamble>(); 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<const Comment> myComment = myIt->dynamicCast<const Comment>(); QSharedPointer<const Comment> otherComment = otherIt->dynamicCast<const Comment>(); 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<Element> File::containsKey(const QString &key, ElementTypes elementTypes) const { if (!d->checkValidity()) qCCritical(LOG_KBIBTEX_DATA) << "const QSharedPointer<Element> File::containsKey(const QString &key, ElementTypes elementTypes) const" << "This File object is not valid"; for (const auto &element : const_cast<const File &>(*this)) { const QSharedPointer<Entry> entry = elementTypes.testFlag(etEntry) ? element.dynamicCast<Entry>() : QSharedPointer<Entry>(); if (!entry.isNull()) { if (entry->id() == key) return entry; } else { const QSharedPointer<Macro> macro = elementTypes.testFlag(etMacro) ? element.dynamicCast<Macro>() : QSharedPointer<Macro>(); if (!macro.isNull()) { if (macro->key() == key) return macro; } } } return QSharedPointer<Element>(); } 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<const File &>(*this)) { const QSharedPointer<Entry> entry = elementTypes.testFlag(etEntry) ? element.dynamicCast<Entry>() : QSharedPointer<Entry>(); if (!entry.isNull()) result.append(entry->id()); else { const QSharedPointer<Macro> macro = elementTypes.testFlag(etMacro) ? element.dynamicCast<Macro>() : QSharedPointer<Macro>(); if (!macro.isNull()) result.append(macro->key()); } } return result; } QSet<QString> File::uniqueEntryValuesSet(const QString &fieldName) const { if (!d->checkValidity()) qCCritical(LOG_KBIBTEX_DATA) << "QSet<QString> File::uniqueEntryValuesSet(const QString &fieldName) const" << "This File object is not valid"; QSet<QString> valueSet; const QString lcFieldName = fieldName.toLower(); for (const auto &element : const_cast<const File &>(*this)) { const QSharedPointer<Entry> entry = element.dynamicCast<Entry>(); 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> &valueItem : itValue) { /// Check if ValueItem to process points to a person const QSharedPointer<Person> person = valueItem.dynamicCast<Person>(); if (!person.isNull()) { QSet<QString> personNameFormattingSet {Preferences::personNameFormatLastFirst, Preferences::personNameFormatFirstLast}; personNameFormattingSet.insert(Preferences::instance().personNameFormat()); /// Add person's name formatted using each of the templates assembled above for (const QString &personNameFormatting : const_cast<const QSet<QString> &>(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<QString> 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); } void File::setPropertiesToDefault() { if (!d->checkValidity()) qCCritical(LOG_KBIBTEX_DATA) << "void File::setPropertiesToDefault()" << "This File object is not valid"; d->loadConfiguration(); } 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/file.h b/src/data/file.h index 1050f320..f172f575 100644 --- a/src/data/file.h +++ b/src/data/file.h @@ -1,128 +1,126 @@ /*************************************************************************** - * 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 KBIBTEX_DATA_FILE_H #define KBIBTEX_DATA_FILE_H #include <QList> #include <QStringList> #include <QSharedPointer> -#include "element.h" - #ifdef HAVE_KF5 #include "kbibtexdata_export.h" #endif // HAVE_KF5 class Element; /** * This class represents a bibliographic file such as a BibTeX file * (or any other format after proper conversion). The file's content * can be accessed using the inherited QList interface (for example * list iterators). * @see Element * @author Thomas Fischer <fischer@unix-ag.uni-kl.de> */ class KBIBTEXDATA_EXPORT File : public QList<QSharedPointer<Element> > { public: /// enum and flags to differ between entries, macros etc /// used for @see #allKeys() and @see #containsKey() enum ElementType { etEntry = 0x1, etMacro = 0x2, etAll = 0x3 }; Q_DECLARE_FLAGS(ElementTypes, ElementType) /// used for property map const static QString Url; const static QString Encoding; const static QString StringDelimiter; const static QString QuoteComment; const static QString KeywordCasing; const static QString ProtectCasing; const static QString NameFormatting; const static QString ListSeparator; explicit File(); explicit File(const File &other); explicit File(File &&other); ~File(); /// Copy-assignment operator. File &operator= (const File &other); /// Move-assignment operator. File &operator= (File &&other); bool operator== (const File &other) const; bool operator!= (const File &other) const; /** * Check if a given key (e.g. a key for a macro or an id for an entry) * is contained in the file object. * @see #allKeys() const * @return @c the object addressed by the key @c, NULL if no such file has been found */ const QSharedPointer<Element> containsKey(const QString &key, ElementTypes elementTypes = etAll) const; /** * Retrieves a list of all keys for example from macros or entries. * @see #const containsKey(const QString &) const * @return list of keys */ QStringList allKeys(ElementTypes elementTypes = etAll) const; /** * Retrieves a set of all unique values (as text) for a specified * field from all entries * @param fieldName field name to scan, e.g. "volume" * @return list of unique values */ QSet<QString> uniqueEntryValuesSet(const QString &fieldName) const; /** * Retrieves a list of all unique values (as text) for a specified * field from all entries * @param fieldName field name to scan, e.g. "volume" * @return list of unique values */ QStringList uniqueEntryValuesList(const QString &fieldName) const; void setProperty(const QString &key, const QVariant &value); QVariant property(const QString &key) const; QVariant property(const QString &key, const QVariant &defaultValue) const; bool hasProperty(const QString &key) const; void setPropertiesToDefault(); /** * Check if this File object is valid by its own assessment. * No high-level checks, such as on the File instance's content, * are performed. * @return True if validity checks succeed, false otherwise */ bool checkValidity() const; private: class FilePrivate; FilePrivate *d; }; Q_DECLARE_METATYPE(File *) QDebug operator<<(QDebug dbg, const File &file); #endif // KBIBTEX_DATA_FILE_H diff --git a/src/data/macro.h b/src/data/macro.h index b018d178..b15b9615 100644 --- a/src/data/macro.h +++ b/src/data/macro.h @@ -1,103 +1,107 @@ /*************************************************************************** - * 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 KBIBTEX_DATA_MACRO_H #define KBIBTEX_DATA_MACRO_H -#include "element.h" -#include "value.h" +#include <Element> +#include <Value> class QString; +#ifdef HAVE_KF5 +#include "kbibtexdata_export.h" +#endif // HAVE_KF5 + /** * This class represents a macro in a BibTeX file. Macros in BibTeX * are similar to variables, allowing to use the same value such as * journal titles in several entries. * @author Thomas Fischer <fischer@unix-ag.uni-kl.de> */ class KBIBTEXDATA_EXPORT Macro : public Element { public: /** * Create a new macro with a given key-value pair. * @param key macro's key * @param value macro's value */ explicit Macro(const QString &key = QString(), const Value &value = Value()); /** * Copy constructor cloning another macro object. * @param other macro object to clone */ Macro(const Macro &other); ~Macro() override; bool operator==(const Macro &other) const; bool operator!=(const Macro &other) const; /** * Assignment operator, working similar to a copy constructor, * but overwrites the current object's values. */ Macro &operator= (const Macro &other); /** * Set the key of this macro. * @param key new key of this macro */ void setKey(const QString &key); /** * Retrieve the key of this macro. * @return key of this macro */ QString key() const; /** * Retrieve the key of this macro. Returns a reference which may not be modified. * @return key of this macro */ const Value &value() const; /** * Retrieve the key of this macro. Returns a reference which may be modified. * @return key of this macro */ Value &value(); /** * Set the value of this macro. * @param value new value of this macro */ void setValue(const Value &value); /** * Cheap and fast test if another Element is a Macro object. * @param other another Element object to test * @return true if Element is actually a Macro */ static bool isMacro(const Element &other); private: class MacroPrivate; MacroPrivate *const d; }; QDebug operator<<(QDebug dbg, const Macro &macro); #endif diff --git a/src/data/models/filemodel.cpp b/src/data/models/filemodel.cpp index 31dd4716..0fbf0114 100644 --- a/src/data/models/filemodel.cpp +++ b/src/data/models/filemodel.cpp @@ -1,399 +1,399 @@ /*************************************************************************** * 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 "filemodel.h" #include <algorithm> #include <QColor> #include <QFile> #include <QString> #include <KLocalizedString> -#include "element.h" +#include <BibTeXEntries> +#include <BibTeXFields> +#include <Preferences> +#include "file.h" #include "entry.h" #include "macro.h" #include "comment.h" #include "preamble.h" -#include "bibtexentries.h" -#include "bibtexfields.h" -#include "preferences.h" const int FileModel::NumberRole = Qt::UserRole + 9581; const int FileModel::SortRole = Qt::UserRole + 236; /// see also MDIWidget's SortRole FileModel::FileModel(QObject *parent) : QAbstractTableModel(parent), m_file(nullptr) { NotificationHub::registerNotificationListener(this); readConfiguration(); } void FileModel::notificationEvent(int eventId) { if (eventId == NotificationHub::EventConfigurationChanged) { readConfiguration(); int column = 0; for (const auto &fd : const_cast<const BibTeXFields &>(BibTeXFields::instance())) { /// Colors may have changed bool columnChanged = fd.upperCamelCase.toLower() == Entry::ftColor; /// Person name formatting may has changed columnChanged |= fd.upperCamelCase.toLower() == Entry::ftAuthor || fd.upperCamelCase.toLower() == Entry::ftEditor; columnChanged |= fd.upperCamelCaseAlt.toLower() == Entry::ftAuthor || fd.upperCamelCaseAlt.toLower() == Entry::ftEditor; /// Changes necessary for this column? Publish update if (columnChanged) emit dataChanged(index(0, column), index(rowCount() - 1, column)); ++column; } } else if (eventId == NotificationHub::EventBibliographySystemChanged) emit headerDataChanged(Qt::Horizontal, 0, 0xffff); } void FileModel::readConfiguration() { colorToLabel.clear(); for (QVector<QPair<QColor, QString>>::ConstIterator it = Preferences::instance().colorCodes().constBegin(); it != Preferences::instance().colorCodes().constEnd(); ++it) colorToLabel.insert(it->first.name(), it->second); } QString FileModel::entryText(const Entry *entry, const QString &raw, const QString &rawAlt, const QStringList &rawAliases, int role, bool followCrossRef) const { if (role != Qt::DisplayRole && role != Qt::ToolTipRole && role != SortRole) return QString(); if (raw == QStringLiteral("^id")) { return entry->id(); } else if (raw == QStringLiteral("^type")) { // FIXME: Use constant here? /// Try to beautify type, e.g. translate "proceedings" into /// "Conference or Workshop Proceedings" const QString label = BibTeXEntries::instance().label(entry->type()); if (label.isEmpty()) { /// Fall-back to entry type as it is return entry->type(); } else return label; } else if (raw.toLower() == Entry::ftStarRating) { return QString(); /// Stars have no string representation } else if (raw.toLower() == Entry::ftColor) { const QString text = PlainTextValue::text(entry->value(raw)); if (text.isEmpty()) return QString(); const QString colorText = colorToLabel[text]; if (colorText.isEmpty()) return text; return colorText; } else { QString text; if (entry->contains(raw)) text = PlainTextValue::text(entry->value(raw)).simplified(); else if (!rawAlt.isEmpty() && entry->contains(rawAlt)) text = PlainTextValue::text(entry->value(rawAlt)).simplified(); if (text.isEmpty()) for (const QString &alias : rawAliases) { if (entry->contains(alias)) { text = PlainTextValue::text(entry->value(alias)).simplified(); if (!text.isEmpty()) break; } } if (followCrossRef && text.isEmpty() && entry->contains(Entry::ftCrossRef)) { QScopedPointer<const Entry> completedEntry(entry->resolveCrossref(m_file)); return entryText(completedEntry.data(), raw, rawAlt, rawAliases, role, false); } if (text.isEmpty()) return QString(); else if (role == FileModel::SortRole) return text.toLower(); else if (role == Qt::ToolTipRole) { // TODO: find a better solution, such as line-wrapping tooltips return KBibTeX::leftSqueezeText(text, 128); } else return text; } } File *FileModel::bibliographyFile() const { return m_file; } void FileModel::setBibliographyFile(File *file) { bool resetNecessary = m_file != file; if (resetNecessary) { beginResetModel(); m_file = file; endResetModel(); } } QModelIndex FileModel::parent(const QModelIndex &index) const { Q_UNUSED(index) return QModelIndex(); } bool FileModel::hasChildren(const QModelIndex &parent) const { return parent == QModelIndex(); } int FileModel::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent) return m_file != nullptr ? m_file->count() : 0; } int FileModel::columnCount(const QModelIndex &parent) const { Q_UNUSED(parent) return BibTeXFields::instance().count(); } QVariant FileModel::data(const QModelIndex &index, int role) const { /// do not accept invalid indices if (!index.isValid()) return QVariant(); /// check backend storage (File object) if (m_file == nullptr) return QVariant(); /// for now, only display data (no editing or icons etc) if (role != NumberRole && role != SortRole && role != Qt::DisplayRole && role != Qt::ToolTipRole && role != Qt::BackgroundRole && role != Qt::ForegroundRole) return QVariant(); if (index.row() < m_file->count() && index.column() < BibTeXFields::instance().count()) { const FieldDescription &fd = BibTeXFields::instance().at(index.column()); const QString &raw = fd.upperCamelCase; const QString &rawAlt = fd.upperCamelCaseAlt; const QStringList &rawAliases = fd.upperCamelCaseAliases; QSharedPointer<Element> element = (*m_file)[index.row()]; QSharedPointer<Entry> entry = element.dynamicCast<Entry>(); /// if BibTeX entry has a "x-color" field, use that color to highlight row if (role == Qt::BackgroundRole) { /// Retrieve "color" QString colorName; if (entry.isNull() || (colorName = PlainTextValue::text(entry->value(Entry::ftColor))) == QStringLiteral("#000000") || colorName.isEmpty()) return QVariant(); else { /// There is a valid color, set it as background QColor color(colorName); /// Use slightly different colors for even and odd rows color.setAlphaF(index.row() % 2 == 0 ? 0.75 : 1.0); return QVariant(color); } } else if (role == Qt::ForegroundRole) { /// Retrieve "color" QString colorName; if (entry.isNull() || (colorName = PlainTextValue::text(entry->value(Entry::ftColor))) == QStringLiteral("#000000") || colorName.isEmpty()) return QVariant(); else { /// There is a valid color ... const QColor color(colorName); /// Retrieve red, green, blue, and alpha components int r = 0, g = 0, b = 0, a = 0; color.getRgb(&r, &g, &b, &a); /// If gray value is rather dark, return white as foreground color if (qGray(r, g, b) < 128) return QColor(Qt::white); /// For light gray values, return black as foreground color else return QColor(Qt::black); } } else if (role == NumberRole) { if (!entry.isNull() && raw.toLower() == Entry::ftStarRating) { const QString text = PlainTextValue::text(entry->value(raw)).simplified(); bool ok = false; const double numValue = text.toDouble(&ok); if (ok) return QVariant::fromValue<double>(numValue); else return QVariant(); } else return QVariant(); } /// The only roles left at this point shall be SortRole, Qt::DisplayRole, and Qt::ToolTipRole if (!entry.isNull()) { return QVariant(entryText(entry.data(), raw, rawAlt, rawAliases, role, true)); } else { QSharedPointer<Macro> macro = element.dynamicCast<Macro>(); if (!macro.isNull()) { if (raw == QStringLiteral("^id")) return QVariant(macro->key()); else if (raw == QStringLiteral("^type")) return QVariant(i18n("Macro")); else if (raw == QStringLiteral("Title")) { const QString text = PlainTextValue::text(macro->value()).simplified(); return QVariant(text); } else return QVariant(); } else { QSharedPointer<Comment> comment = element.dynamicCast<Comment>(); if (!comment.isNull()) { if (raw == QStringLiteral("^type")) return QVariant(i18n("Comment")); else if (raw == Entry::ftTitle) { const QString text = comment->text().simplified(); return QVariant(text); } else return QVariant(); } else { QSharedPointer<Preamble> preamble = element.dynamicCast<Preamble>(); if (!preamble.isNull()) { if (raw == QStringLiteral("^type")) return QVariant(i18n("Preamble")); else if (raw == Entry::ftTitle) { const QString text = PlainTextValue::text(preamble->value()).simplified(); return QVariant(text); } else return QVariant(); } else return QVariant("?"); } } } } else return QVariant("?"); } QVariant FileModel::headerData(int section, Qt::Orientation orientation, int role) const { if (role != Qt::DisplayRole || orientation != Qt::Horizontal || section < 0 || section >= BibTeXFields::instance().count()) return QVariant(); return BibTeXFields::instance().at(section).label; } Qt::ItemFlags FileModel::flags(const QModelIndex &index) const { Q_UNUSED(index) return Qt::ItemIsEnabled | Qt::ItemIsSelectable; // FIXME: What about drag'n'drop? } void FileModel::clear() { beginResetModel(); m_file->clear(); endResetModel(); } bool FileModel::removeRow(int row, const QModelIndex &parent) { if (row < 0 || m_file == nullptr || row >= rowCount() || row >= m_file->count()) return false; if (parent != QModelIndex()) return false; beginRemoveRows(QModelIndex(), row, row); m_file->removeAt(row); endRemoveRows(); return true; } bool FileModel::removeRowList(const QList<int> &rows) { if (m_file == nullptr) return false; QList<int> internalRows = rows; std::sort(internalRows.begin(), internalRows.end(), std::greater<int>()); beginRemoveRows(QModelIndex(), internalRows.last(), internalRows.first()); for (int row : const_cast<const QList<int> &>(internalRows)) { if (row < 0 || row >= rowCount() || row >= m_file->count()) return false; m_file->removeAt(row); } endRemoveRows(); return true; } bool FileModel::insertRow(QSharedPointer<Element> element, int row, const QModelIndex &parent) { if (m_file == nullptr || row < 0 || row > rowCount() || parent != QModelIndex()) return false; /// Check for duplicate ids or keys when inserting a new element /// First, check entries QSharedPointer<Entry> entry = element.dynamicCast<Entry>(); if (!entry.isNull()) { /// Fetch current entry's id const QString id = entry->id(); if (!m_file->containsKey(id).isNull()) { /// Same entry id used for an existing entry or macro int overflow = 2; static const QString pattern = QStringLiteral("%1_%2"); /// Test alternative ids with increasing "overflow" counter: /// id_2, id_3, id_4 ,... QString newId = pattern.arg(id).arg(overflow); while (!m_file->containsKey(newId).isNull()) { ++overflow; newId = pattern.arg(id).arg(overflow); } /// Guaranteed to find an alternative, apply it to entry entry->setId(newId); } } else { /// Next, check macros QSharedPointer<Macro> macro = element.dynamicCast<Macro>(); if (!macro.isNull()) { /// Fetch current macro's key const QString key = macro->key(); if (!m_file->containsKey(key).isNull()) { /// Same entry key used for an existing entry or macro int overflow = 2; static const QString pattern = QStringLiteral("%1_%2"); /// Test alternative keys with increasing "overflow" counter: /// key_2, key_3, key_4 ,... QString newKey = pattern.arg(key).arg(overflow); while (!m_file->containsKey(newKey).isNull()) { ++overflow; newKey = pattern.arg(key).arg(overflow); } /// Guaranteed to find an alternative, apply it to macro macro->setKey(newKey); } } } beginInsertRows(QModelIndex(), row, row); m_file->insert(row, element); endInsertRows(); return true; } QSharedPointer<Element> FileModel::element(int row) const { if (m_file == nullptr || row < 0 || row >= m_file->count()) return QSharedPointer<Element>(); return (*m_file)[row]; } int FileModel::row(QSharedPointer<Element> element) const { if (m_file == nullptr) return -1; return m_file->indexOf(element); } void FileModel::elementChanged(int row) { emit dataChanged(createIndex(row, 0), createIndex(row, columnCount() - 1)); } diff --git a/src/data/models/filemodel.h b/src/data/models/filemodel.h index e9eeb66e..8d348227 100644 --- a/src/data/models/filemodel.h +++ b/src/data/models/filemodel.h @@ -1,79 +1,81 @@ /*************************************************************************** * 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_DATA_FILEMODEL_H #define KBIBTEX_DATA_FILEMODEL_H #include <QAbstractItemModel> #include <QLatin1String> #include <QList> #include <QStringList> -#include "kbibtexdata_export.h" +#include <NotificationHub> +#include <Element> -#include "notificationhub.h" -#include "file.h" -#include "entry.h" +#ifdef HAVE_KF5 +#include "kbibtexdata_export.h" +#endif // HAVE_KF5 -class FileModel; +class File; +class Entry; /** @author Thomas Fischer */ class KBIBTEXDATA_EXPORT FileModel : public QAbstractTableModel, private NotificationListener { Q_OBJECT public: static const int NumberRole; static const int SortRole; explicit FileModel(QObject *parent = nullptr); File *bibliographyFile() const; virtual void setBibliographyFile(File *file); QModelIndex parent(const QModelIndex &index) const override; bool hasChildren(const QModelIndex &parent = QModelIndex()) const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override; int columnCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; Qt::ItemFlags flags(const QModelIndex &index) const override; void clear(); virtual bool removeRow(int row, const QModelIndex &parent = QModelIndex()); bool removeRowList(const QList<int> &rows); bool insertRow(QSharedPointer<Element> element, int row, const QModelIndex &parent = QModelIndex()); QSharedPointer<Element> element(int row) const; int row(QSharedPointer<Element> element) const; /// Notifies the model that a given element has been modifed void elementChanged(int row); void notificationEvent(int eventId) override; private: File *m_file; QMap<QString, QString> colorToLabel; void readConfiguration(); QString entryText(const Entry *entry, const QString &raw, const QString &rawAlt, const QStringList &rawAliases, int role, bool followCrossRef) const; }; #endif // KBIBTEX_DATA_FILEMODEL_H diff --git a/src/data/preamble.h b/src/data/preamble.h index 69d7c915..0937c191 100644 --- a/src/data/preamble.h +++ b/src/data/preamble.h @@ -1,65 +1,65 @@ /*************************************************************************** - * 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 KBIBTEX_DATA_PREAMBLE_H #define KBIBTEX_DATA_PREAMBLE_H -#include "element.h" -#include "value.h" +#include <Element> +#include <Value> /** * This class represents a preamble in a BibTeX file. Preables contain * LaTeX commands required for the bibliography, such as hyphenation commands. * @author Thomas Fischer <fischer@unix-ag.uni-kl.de> */ class KBIBTEXDATA_EXPORT Preamble : public Element { public: explicit Preamble(const Value &value = Value()); Preamble(const Preamble &other); ~Preamble() override; bool operator==(const Preamble &other) const; bool operator!=(const Preamble &other) const; /** * Assignment operator, working similar to a copy constructor, * but overwrites the current object's values. */ Preamble &operator= (const Preamble &other); Value &value(); const Value &value() const; void setValue(const Value &value); // bool containsPattern(const QString& pattern, Field::FieldType fieldType = Field::ftUnknown, FilterType filterType = Element::ftExact, Qt::CaseSensitivity caseSensitive = Qt::CaseInsensitive) const; // FIXME: Rewrite filtering code /** * Cheap and fast test if another Element is a Preamble object. * @param other another Element object to test * @return true if Element is actually a Preamble */ static bool isPreamble(const Element &other); private: class PreamblePrivate; PreamblePrivate *const d; }; QDebug operator<<(QDebug dbg, const Preamble &preamble); #endif // KBIBTEX_DATA_PREAMBLE_H diff --git a/src/data/value.cpp b/src/data/value.cpp index 36da4e2c..5f6217d6 100644 --- a/src/data/value.cpp +++ b/src/data/value.cpp @@ -1,707 +1,707 @@ /*************************************************************************** * 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 "value.h" #include <typeinfo> #include <QSet> #include <QString> #include <QStringList> #include <QDebug> #include <QRegularExpression> #ifdef HAVE_KF5 #include <KSharedConfig> #include <KConfigGroup> #include <KLocalizedString> #else // HAVE_KF5 #define i18n(text) QObject::tr(text) #endif // HAVE_KF5 +#include <Preferences> #include "file.h" -#include "preferences.h" quint64 ValueItem::internalIdCounter = 0; uint qHash(const QSharedPointer<ValueItem> &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<const Keyword *>(&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<const Person *>(&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::defaultPersonNameFormat); 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<const MacroKey *>(&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 &macrokey) { 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<const PlainText *>(&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; } 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); 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 (QVector<QPair<QColor, QString>>::ConstIterator it = Preferences::instance().colorCodes().constBegin(); !contained && it != Preferences::instance().colorCodes().constEnd(); ++it) contained = text.compare(it->first.name(), Qt::CaseInsensitive) == 0 && it->second.contains(pattern, Qt::CaseInsensitive); } #endif // HAVE_KF5 return contained; } bool VerbatimText::operator==(const ValueItem &other) const { const VerbatimText *otherVerbatimText = dynamic_cast<const VerbatimText *>(&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<QSharedPointer<ValueItem> >() { /// nothing } Value::Value(const Value &other) : QVector<QSharedPointer<ValueItem> >(other) { /// nothing } Value::Value(Value &&other) : QVector<QSharedPointer<ValueItem> >(other) { /// nothing } Value::~Value() { clear(); } void Value::replace(const QString &before, const QString &after, ValueItem::ReplaceMode replaceMode) { QSet<QSharedPointer<ValueItem> > 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<const QSet<QSharedPointer<ValueItem> > &>(unique)) { containedInUnique = *valueItem.data() == *(*it).data(); if (containedInUnique) break; } if (containedInUnique) it = erase(it); else { unique.insert(*it); ++it; } } QSet<QString> 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<ValueItem> &after) { const QString valueText = PlainTextValue::text(*this); if (valueText == before) { clear(); append(after); } else { QSet<QString> uniqueValueItemTexts; for (int i = count() - 1; i >= 0; --i) { QString valueItemText = PlainTextValue::text(*at(i).data()); if (valueItemText == before) { /// Perform replacement operation QVector<QSharedPointer<ValueItem> >::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<const Value &>(*this)) { if (valueItem->containsPattern(pattern, caseSensitive)) return true; } return false; } bool Value::contains(const ValueItem &item) const { for (const auto &valueItem : const_cast<const Value &>(*this)) if (valueItem->operator==(item)) return true; return false; } Value &Value::operator=(const Value &rhs) { return static_cast<Value &>(QVector<QSharedPointer<ValueItem> >::operator =((rhs))); } Value &Value::operator=(Value &&rhs) { return static_cast<Value &>(QVector<QSharedPointer<ValueItem> >::operator =((rhs))); } Value &Value::operator<<(const QSharedPointer<ValueItem> &value) { return static_cast<Value &>(QVector<QSharedPointer<ValueItem> >::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<PlainText> 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; 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, Preferences::instance().personNameFormat()); 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; } diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 0e6cce7d..54ce3429 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -1,134 +1,132 @@ # KBibTeX GUI library set( kbibtexgui_LIB_SRCS field/fieldinput.cpp field/fieldlineedit.cpp field/fieldlistedit.cpp field/colorlabelwidget.cpp file/fileview.cpp file/filedelegate.cpp file/partwidget.cpp file/findduplicatesui.cpp file/clipboard.cpp file/basicfileview.cpp file/sortfilterfilemodel.cpp element/elementeditor.cpp element/elementwidgets.cpp element/findpdfui.cpp element/associatedfilesui.cpp widgets/menulineedit.cpp widgets/filesettingswidget.cpp widgets/filterbar.cpp widgets/radiobuttontreeview.cpp widgets/hidingtabwidget.cpp widgets/starrating.cpp widgets/rangewidget.cpp config/entrylayout.cpp preferences/kbibtexpreferencesdialog.cpp preferences/settingsgeneralwidget.cpp preferences/settingsglobalkeywordswidget.cpp preferences/settingscolorlabelwidget.cpp preferences/settingsuserinterfacewidget.cpp preferences/settingsfileexporterpdfpswidget.cpp preferences/settingsfileexporterwidget.cpp preferences/settingsabstractwidget.cpp preferences/settingsidsuggestionswidget.cpp preferences/settingsidsuggestionseditor.cpp guihelper.cpp italictextitemmodel.cpp valuelistmodel.cpp delayedexecutiontimer.cpp logging_gui.cpp ) set( kbibtexgui_HDRS field/fieldinput.h field/colorlabelwidget.h field/fieldlineedit.h widgets/filterbar.h preferences/settingsuserinterfacewidget.h preferences/kbibtexpreferencesdialog.h preferences/settingsglobalkeywordswidget.h preferences/settingsfileexporterwidget.h preferences/settingsgeneralwidget.h preferences/settingsabstractwidget.h preferences/settingscolorlabelwidget.h preferences/settingsfileexporterpdfpswidget.h preferences/settingsidsuggestionswidget.h preferences/settingsidsuggestionseditor.h guihelper.h valuelistmodel.h italictextitemmodel.h delayedexecutiontimer.h file/findduplicatesui.h file/basicfileview.h file/clipboard.h file/fileview.h file/filedelegate.h file/sortfilterfilemodel.h file/partwidget.h element/findpdfui.h element/elementeditor.h element/associatedfilesui.h ) if(UNITY_BUILD) enable_unity_build(kbibtexgui kbibtexgui_LIB_SRCS) endif(UNITY_BUILD) include_directories( - ${CMAKE_SOURCE_DIR}/src/data - ${CMAKE_BINARY_DIR}/src/data ${CMAKE_SOURCE_DIR}/src/io ${CMAKE_BINARY_DIR}/src/io ${CMAKE_SOURCE_DIR}/src/io/config ${CMAKE_SOURCE_DIR}/src/networking ${CMAKE_BINARY_DIR}/src/networking ${CMAKE_SOURCE_DIR}/src/processing ${CMAKE_BINARY_DIR}/src/processing ${CMAKE_SOURCE_DIR}/src/gui ${CMAKE_BINARY_DIR}/src/gui ${CMAKE_SOURCE_DIR}/src/gui/file ${CMAKE_SOURCE_DIR}/src/gui/dialogs ${CMAKE_SOURCE_DIR}/src/gui/element ${CMAKE_SOURCE_DIR}/src/gui/field ${CMAKE_SOURCE_DIR}/src/gui/widgets ${CMAKE_SOURCE_DIR}/src/gui/config ) add_library( kbibtexgui SHARED ${kbibtexgui_LIB_SRCS} ) target_link_libraries( kbibtexgui Qt5::Core KF5::IconThemes KF5::ItemViews KF5::Completion KF5::TextEditor kbibtexconfig kbibtexdata kbibtexio kbibtexnetworking kbibtexproc ) set_target_properties( kbibtexgui PROPERTIES EXPORT_NAME "kbibtexgui" VERSION ${KBIBTEX_RELEASE_VERSION} SOVERSION ${KBIBTEX_SOVERSION} ) install( TARGETS kbibtexgui ${INSTALL_TARGETS_DEFAULT_ARGS} ) generate_export_header( kbibtexgui ) diff --git a/src/gui/file/filedelegate.cpp b/src/gui/file/filedelegate.cpp index 3a4d1d57..9ecc0f37 100644 --- a/src/gui/file/filedelegate.cpp +++ b/src/gui/file/filedelegate.cpp @@ -1,38 +1,39 @@ /*************************************************************************** * Copyright (C) 2004-2018 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 "filedelegate.h" #include <KIconLoader> #include "models/filemodel.h" +#include "entry.h" #include "bibtexfields.h" #include "starrating.h" void FileDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { QStyledItemDelegate::paint(painter, option, index); static const int numTotalStars = 8; bool ok = false; double percent = index.data(FileModel::NumberRole).toDouble(&ok); if (ok) { const FieldDescription &fd = BibTeXFields::instance().at(index.column()); if (fd.upperCamelCase.toLower() == Entry::ftStarRating) StarRating::paintStars(painter, KIconLoader::DefaultState, numTotalStars, percent, option.rect); } } diff --git a/src/gui/file/findduplicatesui.cpp b/src/gui/file/findduplicatesui.cpp index 7520ca92..07f56c57 100644 --- a/src/gui/file/findduplicatesui.cpp +++ b/src/gui/file/findduplicatesui.cpp @@ -1,740 +1,741 @@ /*************************************************************************** * Copyright (C) 2004-2018 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 "findduplicatesui.h" #include <QApplication> #include <QWidget> #include <QBoxLayout> #include <QLabel> #include <QLineEdit> #include <QSortFilterProxyModel> #include <QStyle> #include <QRadioButton> #include <QMouseEvent> #include <QKeyEvent> #include <QSplitter> #include <QPointer> #include <QStandardPaths> #include <QPushButton> #include <QAction> #include <QDialog> #include <QDialogButtonBox> #include <KActionCollection> #include <KLocalizedString> #include <kparts/part.h> #include <KMessageBox> +#include "entry.h" #include "fileimporterbibtex.h" #include "bibtexentries.h" #include "radiobuttontreeview.h" #include "fileview.h" #include "filedelegate.h" #include "models/filemodel.h" #include "findduplicates.h" #include "logging_gui.h" /** * Model to hold alternative values as visualized in the RadioTreeView */ class AlternativesItemModel : public QAbstractItemModel { Q_OBJECT private: /// marker to memorize in an index's internal id that it is a top-level index static const quintptr noParentInternalId; /// parent widget, needed to get font from (for text in italics) QTreeView *p; EntryClique *currentClique; public: enum SelectionType {SelectionTypeNone, SelectionTypeRadio, SelectionTypeCheck}; enum AlternativesItemModelRole { /// Raw, all-lowercase field name FieldNameRole = Qt::UserRole + 101, /// Text used for inline editing of values UserInputRole = Qt::UserRole + 103 }; AlternativesItemModel(QTreeView *parent) : QAbstractItemModel(parent), p(parent), currentClique(nullptr) { /// nothing } static SelectionType selectionType(const QString &fieldName) { if (fieldName.isEmpty()) return SelectionTypeNone; if (fieldName == Entry::ftKeywords || fieldName == Entry::ftUrl) return SelectionTypeCheck; return SelectionTypeRadio; } void setCurrentClique(EntryClique *currentClique) { this->currentClique = currentClique; } QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override { if (parent == QModelIndex()) return createIndex(row, column, noParentInternalId); else if (parent.parent() == QModelIndex()) return createIndex(row, column, static_cast<quintptr>(parent.row())); return QModelIndex(); } QModelIndex parent(const QModelIndex &index) const override { if (index.internalId() >= noParentInternalId) return QModelIndex(); else return createIndex(static_cast<int>(index.internalId()), 0, noParentInternalId); } int rowCount(const QModelIndex &parent = QModelIndex()) const override { if (currentClique == nullptr) return 0; if (parent == QModelIndex()) { /// top-level index, check how many lists of lists of alternatives exist return currentClique->fieldCount(); } else if (parent.parent() == QModelIndex()) { /// first, find the map of alternatives for this chosen field name (see parent) QString fieldName = parent.data(FieldNameRole).toString(); const auto alt = currentClique->values(fieldName); /// second, return number of alternatives for list of alternatives /// plus one for an "else" option return alt.count() + (fieldName.startsWith('^') || fieldName == Entry::ftKeywords || fieldName == Entry::ftUrl ? 0 : 1); } return 0; } int columnCount(const QModelIndex &) const override { /// only one column in use return 1; } QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override { Q_UNUSED(section) Q_UNUSED(orientation) if (role == Qt::DisplayRole) return i18n("Alternatives"); return QVariant(); } QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override { if (index.parent() == QModelIndex()) { /// top-level elements showing field names like "Title", "Authors", etc const QString fieldName = currentClique->fieldList().at(index.row()).toLower(); switch (role) { case AlternativesItemModelRole::FieldNameRole: /// plain-and-simple field name (all lower case) return fieldName; case Qt::ToolTipRole: case Qt::DisplayRole: /// nicely formatted field names for visual representation if (fieldName == QStringLiteral("^id")) return i18n("Identifier"); else if (fieldName == QStringLiteral("^type")) return i18n("Type"); else return BibTeXEntries::instance().format(fieldName, KBibTeX::cUpperCamelCase); case RadioButtonTreeView::IsRadioRole: /// this is not to be a radio widget return QVariant::fromValue(false); case Qt::FontRole: if (fieldName.startsWith('^')) { QFont f = p->font(); f.setItalic(true); return f; } else return p->font(); } } else if (index.parent().parent() == QModelIndex()) { /// second-level entries for alternatives /// start with determining which list of alternatives actually to use QString fieldName = index.parent().data(FieldNameRole).toString(); const auto &values = currentClique->values(fieldName); switch (role) { case Qt::EditRole: case Qt::ToolTipRole: case Qt::DisplayRole: if (index.row() < values.count()) { QString text = PlainTextValue::text(values.at(index.row())); if (fieldName == QStringLiteral("^type")) text = BibTeXEntries::instance().format(text, KBibTeX::cUpperCamelCase); /// textual representation of the alternative's value return text; } else /// add an "else" option return i18n("None of the above"); case Qt::FontRole: /// for the "else" option, make font italic if (index.row() >= values.count()) { QFont f = p->font(); f.setItalic(true); return f; } else return p->font(); case Qt::CheckStateRole: { if (selectionType(fieldName) != SelectionTypeCheck) return QVariant(); const auto chosenValues = currentClique->chosenValues(fieldName); QString text = PlainTextValue::text(values.at(index.row())); for (const Value &value : chosenValues) { if (PlainTextValue::text(value) == text) return Qt::Checked; } return Qt::Unchecked; } case RadioButtonTreeView::RadioSelectedRole: { if (selectionType(fieldName) != SelectionTypeRadio) return QVariant::fromValue(false); /// return selection status (true or false) for this alternative Value chosen = currentClique->chosenValue(fieldName); if (chosen.isEmpty()) return QVariant::fromValue(index.row() >= values.count()); else if (index.row() < values.count()) { QString chosenPlainText = PlainTextValue::text(chosen); QString rowPlainText = PlainTextValue::text(values[index.row()]); return QVariant::fromValue(chosenPlainText == rowPlainText); } return QVariant::fromValue(false); } case RadioButtonTreeView::IsRadioRole: /// this is to be a radio widget return QVariant::fromValue(selectionType(fieldName) == SelectionTypeRadio); } } return QVariant(); } bool setData(const QModelIndex &index, const QVariant &value, int role = RadioButtonTreeView::RadioSelectedRole) override { if (index.parent() != QModelIndex()) { bool isInt; int checkState = value.toInt(&isInt); const QString fieldName = index.parent().data(FieldNameRole).toString(); auto values = currentClique->values(fieldName); if (role == RadioButtonTreeView::RadioSelectedRole && value.canConvert<bool>() && value.toBool() == true && selectionType(fieldName) == SelectionTypeRadio) { /// start with determining which list of alternatives actually to use /// store which alternative was selected if (index.row() < values.count()) currentClique->setChosenValue(fieldName, values[index.row()]); else { Value v; currentClique->setChosenValue(fieldName, v); } /// update view on neighbouring alternatives emit dataChanged(index.sibling(0, 0), index.sibling(rowCount(index.parent()), 0)); return true; } else if (role == Qt::CheckStateRole && isInt && selectionType(fieldName) == SelectionTypeCheck) { if (checkState == Qt::Checked) currentClique->setChosenValue(fieldName, values[index.row()], EntryClique::AddValue); else if (checkState == Qt::Unchecked) currentClique->setChosenValue(fieldName, values[index.row()], EntryClique::RemoveValue); else return false; ///< tertium non datur emit dataChanged(index, index); return true; } else if (role == UserInputRole) { const QString text = value.toString(); if (text.isEmpty()) return false; const Value old = values.at(index.row()); if (old.isEmpty()) return false; Value v; QSharedPointer<PlainText> pt = old.first().dynamicCast<PlainText>(); if (!pt.isNull()) v.append(QSharedPointer<PlainText>(new PlainText(text))); else { QSharedPointer<VerbatimText> vt = old.first().dynamicCast<VerbatimText>(); if (!vt.isNull()) v.append(QSharedPointer<VerbatimText>(new VerbatimText(text))); else { QSharedPointer<MacroKey> mk = old.first().dynamicCast<MacroKey>(); if (!mk.isNull()) v.append(QSharedPointer<MacroKey>(new MacroKey(text))); else { QSharedPointer<Person> ps = old.first().dynamicCast<Person>(); if (!ps.isNull()) FileImporterBibTeX::parsePersonList(text, v); else { QSharedPointer<Keyword> kw = old.first().dynamicCast<Keyword>(); if (!kw.isNull()) { const QList<QSharedPointer<Keyword> > keywordList = FileImporterBibTeX::splitKeywords(text); v.reserve(keywordList.size()); for (const auto &keyword : keywordList) v.append(keyword); } else { qCDebug(LOG_KBIBTEX_GUI) << "Not know how to set this text:" << text; } } } } } if (!v.isEmpty()) { values.removeAt(index.row()); values.insert(index.row(), v); emit dataChanged(index, index); return true; } else return false; } } return false; } bool hasChildren(const QModelIndex &parent = QModelIndex()) const override { /// depth-two tree return parent == QModelIndex() || parent.parent() == QModelIndex(); } Qt::ItemFlags flags(const QModelIndex &index) const override { Qt::ItemFlags f = QAbstractItemModel::flags(index); if (index.parent() != QModelIndex()) { QString fieldName = index.parent().data(FieldNameRole).toString(); if (selectionType(fieldName) == SelectionTypeCheck) f |= Qt::ItemIsUserCheckable; const auto values = currentClique->values(fieldName); if (index.row() < values.count()) f |= Qt::ItemIsEditable; } return f; } }; const quintptr AlternativesItemModel::noParentInternalId = std::numeric_limits<quintptr>::max(); /** * Specialization of RadioButtonItemDelegate which allows to edit * values in a AlternativesItemModel. */ class AlternativesItemDelegate: public RadioButtonItemDelegate { Q_OBJECT public: AlternativesItemDelegate(QObject *p) : RadioButtonItemDelegate(p) { /// nothing } QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &, const QModelIndex &index) const override { if (index.parent() != QModelIndex()) { /// Only second-level indices in the model can be edited /// (those are the actual values). /// Use a plain, border-less QLineEdit. QLineEdit *lineEdit = new QLineEdit(parent); lineEdit->setStyleSheet(QStringLiteral("border: none;")); return lineEdit; } return nullptr; } void setEditorData(QWidget *editor, const QModelIndex &index) const override { if (QLineEdit *lineEdit = qobject_cast<QLineEdit *>(editor)) { /// Set line edit's default value to string fetched from model lineEdit->setText(index.data(Qt::EditRole).toString()); } } void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override { if (QLineEdit *lineEdit = qobject_cast<QLineEdit *>(editor)) { /// Set user-entered text to model (and underlying value) model->setData(index, lineEdit->text(), AlternativesItemModel::UserInputRole); /// Ensure that the edited value is checked if it is /// a checkbox-checkable value, or gets a "dot" in its /// radio button if it is radio-checkable. const QString fieldName = index.parent().data(AlternativesItemModel::FieldNameRole).toString(); if (AlternativesItemModel::selectionType(fieldName) == AlternativesItemModel::SelectionTypeCheck) model->setData(index, Qt::Checked, Qt::CheckStateRole); else if (AlternativesItemModel::selectionType(fieldName) == AlternativesItemModel::SelectionTypeRadio) model->setData(index, true, RadioButtonTreeView::RadioSelectedRole); } } void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &) const override { QRect rect = option.rect; // TODO better placement of editing widget? //int radioButtonWidth = QApplication::style()->pixelMetric(QStyle::PM_ExclusiveIndicatorWidth, &option); //int spacing = QApplication::style()->pixelMetric(QStyle::PM_RadioButtonLabelSpacing, &option); //rect.setLeft(rect.left() +spacing*3/2 + radioButtonWidth); editor->setGeometry(rect); } }; class CheckableFileModel : public FileModel { Q_OBJECT private: QVector<EntryClique *> cl; int currentClique; QTreeView *tv; public: CheckableFileModel(QVector<EntryClique *> &cliqueList, QTreeView *treeView, QObject *parent = nullptr) : FileModel(parent), cl(cliqueList), currentClique(0), tv(treeView) { /// nothing } void setCurrentClique(EntryClique *currentClique) { this->currentClique = cl.indexOf(currentClique); } QVariant data(const QModelIndex &index, int role) const override { if (role == Qt::CheckStateRole && index.column() == 1) { QSharedPointer<Entry> entry = element(index.row()).dynamicCast<Entry>(); Q_ASSERT_X(!entry.isNull(), "CheckableFileModel::data", "entry is NULL"); if (!entry.isNull()) { QList<QSharedPointer<Entry> > entryList = cl[currentClique]->entryList(); if (entryList.contains(QSharedPointer<Entry>(entry))) // TODO does this work? return cl[currentClique]->isEntryChecked(QSharedPointer<Entry>(entry)) ? Qt::Checked : Qt::Unchecked; } } return FileModel::data(index, role); } bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override { bool ok; int checkState = value.toInt(&ok); Q_ASSERT_X(ok, "CheckableFileModel::setData", QString("Could not convert value " + value.toString()).toLatin1()); if (ok && role == Qt::CheckStateRole && index.column() == 1) { QSharedPointer<Entry> entry = element(index.row()).dynamicCast<Entry>(); if (!entry.isNull()) { QList<QSharedPointer<Entry> > entryList = cl[currentClique]->entryList(); if (entryList.contains(QSharedPointer<Entry>(entry))) { // TODO does this work? EntryClique *ec = cl[currentClique]; ec->setEntryChecked(QSharedPointer<Entry>(entry), checkState == Qt::Checked); cl[currentClique] = ec; emit dataChanged(index, index); tv->reset(); tv->expandAll(); return true; } } } return false; } Qt::ItemFlags flags(const QModelIndex &index) const override { Qt::ItemFlags f = FileModel::flags(index); if (index.column() == 1) f |= Qt::ItemIsUserCheckable; return f; } }; class FilterIdFileModel : public QSortFilterProxyModel { Q_OBJECT private: CheckableFileModel *internalModel; EntryClique *currentClique; public: FilterIdFileModel(QObject *parent = nullptr) : QSortFilterProxyModel(parent), internalModel(nullptr), currentClique(nullptr) { /// nothing } void setCurrentClique(EntryClique *currentClique) { Q_ASSERT_X(internalModel != nullptr, "FilterIdFileModel::setCurrentClique(EntryClique *currentClique)", "internalModel is NULL"); internalModel->setCurrentClique(currentClique); this->currentClique = currentClique; invalidate(); } void setSourceModel(QAbstractItemModel *model) override { QSortFilterProxyModel::setSourceModel(model); internalModel = dynamic_cast<CheckableFileModel *>(model); Q_ASSERT_X(internalModel != nullptr, "FilterIdFileModel::setSourceModel(QAbstractItemModel *model)", "internalModel is NULL"); } bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override { Q_UNUSED(source_parent) if (internalModel != nullptr && currentClique != nullptr) { QSharedPointer<Entry> entry = internalModel->element(source_row).dynamicCast<Entry>(); if (!entry.isNull()) { QList<QSharedPointer<Entry> > entryList = currentClique->entryList(); if (entryList.contains(QSharedPointer<Entry>(entry))) return true; // TODO does this work? } } return false; } }; class MergeWidget::MergeWidgetPrivate { private: MergeWidget *p; public: File *file; FileView *editor; QPushButton *buttonNext, *buttonPrev; QLabel *labelWhichClique; static const char *whichCliqueText; CheckableFileModel *model; FilterIdFileModel *filterModel; RadioButtonTreeView *alternativesView; AlternativesItemModel *alternativesItemModel; AlternativesItemDelegate *alternativesItemDelegate; int currentClique; QVector<EntryClique *> &cl; MergeWidgetPrivate(MergeWidget *parent, File *bibTeXFile, QVector<EntryClique *> &cliqueList) : p(parent), file(bibTeXFile), currentClique(0), cl(cliqueList) { setupGUI(); } void setupGUI() { p->setMinimumSize(p->fontMetrics().xHeight() * 96, p->fontMetrics().xHeight() * 64); p->setBaseSize(p->fontMetrics().xHeight() * 128, p->fontMetrics().xHeight() * 96); QBoxLayout *layout = new QVBoxLayout(p); QLabel *label = new QLabel(i18n("Select your duplicates"), p); layout->addWidget(label); QSplitter *splitter = new QSplitter(Qt::Vertical, p); layout->addWidget(splitter); editor = new FileView(QStringLiteral("MergeWidget"), splitter); editor->setItemDelegate(new FileDelegate(editor)); editor->setReadOnly(true); alternativesView = new RadioButtonTreeView(splitter); model = new CheckableFileModel(cl, alternativesView, p); model->setBibliographyFile(file); QBoxLayout *containerLayout = new QHBoxLayout(); layout->addLayout(containerLayout); containerLayout->addStretch(10); labelWhichClique = new QLabel(p); containerLayout->addWidget(labelWhichClique); buttonPrev = new QPushButton(QIcon::fromTheme(QStringLiteral("go-previous")), i18n("Previous Clique"), p); containerLayout->addWidget(buttonPrev, 1); buttonNext = new QPushButton(QIcon::fromTheme(QStringLiteral("go-next")), i18n("Next Clique"), p); containerLayout->addWidget(buttonNext, 1); filterModel = new FilterIdFileModel(p); filterModel->setSourceModel(model); alternativesItemModel = new AlternativesItemModel(alternativesView); alternativesItemDelegate = new AlternativesItemDelegate(alternativesView); showCurrentClique(); connect(buttonPrev, &QPushButton::clicked, p, &MergeWidget::previousClique); connect(buttonNext, &QPushButton::clicked, p, &MergeWidget::nextClique); connect(editor, &FileView::doubleClicked, editor, &FileView::viewCurrentElement); } void showCurrentClique() { EntryClique *ec = cl[currentClique]; filterModel->setCurrentClique(ec); alternativesItemModel->setCurrentClique(ec); editor->setModel(filterModel); alternativesView->setModel(alternativesItemModel); alternativesView->setItemDelegate(alternativesItemDelegate); editor->reset(); alternativesView->reset(); alternativesView->expandAll(); buttonNext->setEnabled(currentClique >= 0 && currentClique < cl.count() - 1); buttonPrev->setEnabled(currentClique > 0); labelWhichClique->setText(i18n(whichCliqueText, currentClique + 1, cl.count())); } }; const char *MergeWidget::MergeWidgetPrivate::whichCliqueText = "Showing clique %1 of %2."; MergeWidget::MergeWidget(File *file, QVector<EntryClique *> &cliqueList, QWidget *parent) : QWidget(parent), d(new MergeWidgetPrivate(this, file, cliqueList)) { /// nothing } MergeWidget::~MergeWidget() { delete d; } void MergeWidget::previousClique() { if (d->currentClique > 0) { --d->currentClique; d->showCurrentClique(); } } void MergeWidget::nextClique() { if (d->currentClique >= 0 && d->currentClique < d->cl.count() - 1) { ++d->currentClique; d->showCurrentClique(); } } class FindDuplicatesUI::FindDuplicatesUIPrivate { public: KParts::Part *part; FileView *view; FindDuplicatesUIPrivate(FindDuplicatesUI *parent, KParts::Part *kpart, FileView *fileView) : part(kpart), view(fileView) { Q_UNUSED(parent) } }; FindDuplicatesUI::FindDuplicatesUI(KParts::Part *part, FileView *fileView) : QObject(), d(new FindDuplicatesUIPrivate(this, part, fileView)) { QAction *newAction = new QAction(QIcon::fromTheme(QStringLiteral("tab-duplicate")), i18n("Find Duplicates"), this); part->actionCollection()->addAction(QStringLiteral("findduplicates"), newAction); connect(newAction, &QAction::triggered, this, &FindDuplicatesUI::startDuplicatesSearch); } FindDuplicatesUI::~FindDuplicatesUI() { delete d; } void FindDuplicatesUI::startDuplicatesSearch() { // FIXME move to settings //bool ok = false; //int sensitivity = KInputDialog::getInteger(i18n("Sensitivity"), i18n("Enter a value for sensitivity.\n\nLow values (close to 0) require very similar entries for duplicate detection, larger values (close to 10000) are more likely to count entries as duplicates.\n\nPlease provide feedback to the developers if you have a suggestion for a better default value than 4000."), 4000, 0, 10000, 10, &ok, d->part->widget()); //if (!ok) sensitivity = 4000; int sensitivity = 4000; FileModel *model = d->view->fileModel(); if (model == nullptr) return; /// Full file, used to remove merged elements from /// Stays the same even when merging is restricted to selected elements File *originalFile = model->bibliographyFile(); /// File to be used to find duplicate in, /// may be only a subset of the original one if selection is used File *workingSetFile = originalFile; /// If more than one element but not all are selected in the main list view, /// ask the user if duplicates are to be searched only within the selection const int selectedRowsCount = d->view->selectedElements().count(); if (selectedRowsCount > 1 && selectedRowsCount < d->view->sortFilterProxyModel()->sourceModel()->rowCount() && KMessageBox::questionYesNo(d->part->widget(), i18n("Multiple elements are selected. Do you want to search for duplicates only within the selection or in the whole document?"), i18n("Search only in selection?"), KGuiItem(i18n("Only in selection")), KGuiItem(i18n("Whole document"))) == KMessageBox::Yes) { /// Yes, do only search for duplicates within selection, so copy all /// selected elements to a temporary new File object workingSetFile = new File(); const QModelIndexList mil = d->view->selectionModel()->selectedRows(); for (const QModelIndex &index : mil) workingSetFile->append(model->element(d->view->sortFilterProxyModel()->mapToSource(index).row())); } /// Actual duplicate finder, can be given a widget that will be the /// parent of a progress bar window and sensitivity value when to /// recognize two entries as being duplicates of each other FindDuplicates fd(d->part->widget(), sensitivity); QVector<EntryClique *> cliques; bool gotCanceled = fd.findDuplicateEntries(workingSetFile, cliques); if (gotCanceled) { /// Duplicate search was cancelled, e.g. by pressing the Cancel /// button on the progress bar window if (workingSetFile != originalFile) delete workingSetFile; return; } if (cliques.isEmpty()) KMessageBox::information(d->part->widget(), i18n("No duplicates were found."), i18n("No duplicates found")); else { /// Duplicates have been found, so let user choose how to handle duplicate fields /// Why is a QPointer used here you may wonder? Check here in case the link still works: /// https://blogs.kde.org/2009/03/26/how-crash-almost-every-qtkde-application-and-how-fix-it-0 QPointer<QDialog> dlg = new QDialog(d->part->widget()); dlg->setWindowTitle(i18n("Merge Duplicates")); MergeWidget *mw = new MergeWidget(workingSetFile, cliques, dlg); mw->layout()->setMargin(0); QBoxLayout *layout = new QVBoxLayout(dlg); layout->addWidget(mw); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, Qt::Horizontal, dlg); layout->addWidget(buttonBox); connect(buttonBox->button(QDialogButtonBox::Ok), &QPushButton::clicked, dlg.data(), &QDialog::accept); connect(buttonBox->button(QDialogButtonBox::Cancel), &QPushButton::clicked, dlg.data(), &QDialog::reject); if (dlg->exec() == QDialog::Accepted && MergeDuplicates::mergeDuplicateEntries(cliques, d->view->fileModel())) { /// If the user made selections on what to merge how /// AND the requested changes could be applied on the /// 'original' file (not the working set file), then /// notify the world that things have changed d->view->externalModification(); } /// Clean memory while (!cliques.isEmpty()) { EntryClique *ec = cliques.first(); cliques.removeFirst(); delete ec; } delete dlg; } if (workingSetFile != originalFile) delete workingSetFile; } #include "findduplicatesui.moc" diff --git a/src/gui/file/sortfilterfilemodel.cpp b/src/gui/file/sortfilterfilemodel.cpp index cf9c1c56..022d0e65 100644 --- a/src/gui/file/sortfilterfilemodel.cpp +++ b/src/gui/file/sortfilterfilemodel.cpp @@ -1,262 +1,263 @@ /*************************************************************************** * 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 "sortfilterfilemodel.h" #include <QRegularExpression> #include "bibtexfields.h" #include "bibtexentries.h" +#include "entry.h" #include "macro.h" #include "preamble.h" #include "comment.h" #include "fileinfo.h" SortFilterFileModel::SortFilterFileModel(QObject *parent) : QSortFilterProxyModel(parent), m_internalModel(nullptr) { m_filterQuery.combination = AnyTerm; setSortRole(FileModel::SortRole); } void SortFilterFileModel::setSourceModel(QAbstractItemModel *model) { QSortFilterProxyModel::setSourceModel(model); m_internalModel = dynamic_cast<FileModel *>(model); } FileModel *SortFilterFileModel::fileSourceModel() const { return m_internalModel; } void SortFilterFileModel::updateFilter(const SortFilterFileModel::FilterQuery &filterQuery) { m_filterQuery = filterQuery; m_filterQuery.field = filterQuery.field.toLower(); /// required for comparison in filter code invalidate(); } bool SortFilterFileModel::simpleLessThan(const QModelIndex &left, const QModelIndex &right) const { const QString leftString = left.data(Qt::DisplayRole).toString().toLower(); const QString rightString = right.data(Qt::DisplayRole).toString().toLower(); const int cmp = QString::localeAwareCompare(leftString, rightString); if (cmp == 0) return left.row() < right.row(); else return cmp < 0; } bool SortFilterFileModel::lessThan(const QModelIndex &left, const QModelIndex &right) const { int column = left.column(); Q_ASSERT_X(left.column() == right.column(), "bool SortFilterFileModel::lessThan(const QModelIndex &left, const QModelIndex &right) const", "Not comparing items in same column"); ///< assume that we only sort by column const FieldDescription &fd = BibTeXFields::instance().at(column); if (column == right.column() && (fd.upperCamelCase == QStringLiteral("Author") || fd.upperCamelCase == QStringLiteral("Editor"))) { /// special sorting for authors or editors: check all names, /// compare last and then first names /// first, check if two entries (and not e.g. comments) are to be compared QSharedPointer<Entry> entryA = m_internalModel->element(left.row()).dynamicCast<Entry>(); QSharedPointer<Entry> entryB = m_internalModel->element(right.row()).dynamicCast<Entry>(); if (entryA.isNull() || entryB.isNull()) return simpleLessThan(left, right); /// retrieve values of both cells Value valueA = entryA->value(fd.upperCamelCase); Value valueB = entryB->value(fd.upperCamelCase); if (valueA.isEmpty()) valueA = entryA->value(fd.upperCamelCaseAlt); if (valueB.isEmpty()) valueB = entryB->value(fd.upperCamelCaseAlt); /// if either value is empty, use default implementation if (valueA.isEmpty() || valueB.isEmpty()) return simpleLessThan(left, right); /// compare each person in both values for (Value::Iterator itA = valueA.begin(), itB = valueB.begin(); itA != valueA.end() && itB != valueB.end(); ++itA, ++itB) { static const QRegularExpression curlyRegExp(QStringLiteral("[{}]+")); QSharedPointer<Person> personA = (*itA).dynamicCast<Person>(); QSharedPointer<Person> personB = (*itB).dynamicCast<Person>(); /// not a Person object in value? fall back to default implementation if (personA.isNull() || personB.isNull()) return QSortFilterProxyModel::lessThan(left, right); /// get both values' next persons' last names for comparison QString nameA = personA->lastName().remove(curlyRegExp).toLower(); QString nameB = personB->lastName().remove(curlyRegExp).toLower(); int cmp = QString::localeAwareCompare(nameA, nameB); if (cmp < 0) return true; if (cmp > 0) return false; /// if last names were inconclusive ... /// get both values' next persons' first names for comparison nameA = personA->firstName().remove(curlyRegExp).toLower(); nameB = personB->firstName().remove(curlyRegExp).toLower(); cmp = QString::localeAwareCompare(nameA, nameB); if (cmp < 0) return true; if (cmp > 0) return false; // TODO Check for suffix and prefix? } /// comparison by names did not work (was not conclusive) /// fall back to default implementation return simpleLessThan(left, right); } else { /// if comparing two numbers, do not perform lexicographical sorting (i.e. 13 < 2), /// but numerical sorting instead (i.e. 13 > 2) const QString textLeft = left.data(Qt::DisplayRole).toString(); const QString textRight = right.data(Qt::DisplayRole).toString(); bool okLeft = false, okRight = false; int numberLeft = textLeft.toInt(&okLeft); int numberRight = textRight.toInt(&okRight); if (okLeft && okRight) return numberLeft < numberRight; /// everything else can be sorted by default implementation /// (i.e. alphabetically or lexicographically) return simpleLessThan(left, right); } } bool SortFilterFileModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const { Q_UNUSED(source_parent) const QSharedPointer<Element> rowElement = m_internalModel->element(source_row); Q_ASSERT_X(!rowElement.isNull(), "bool SortFilterFileModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const", "rowElement is NULL"); if (m_filterQuery.terms.isEmpty()) return true; /// empty filter query QScopedArrayPointer<bool> eachTerm(new bool[m_filterQuery.terms.count()]); for (int i = m_filterQuery.terms.count() - 1; i >= 0; --i) eachTerm[i] = false; const QSharedPointer<Entry> entry = rowElement.dynamicCast<Entry>(); if (!entry.isNull()) { /// if current row contains an Entry ... if (m_filterQuery.field.isEmpty() || m_filterQuery.field == QStringLiteral("^id")) { /// Check entry's id const QString id = entry->id(); int i = 0; for (QStringList::ConstIterator itsl = m_filterQuery.terms.constBegin(); itsl != m_filterQuery.terms.constEnd(); ++itsl, ++i) eachTerm[i] |= (*itsl).isEmpty() ? true : id.contains(*itsl, Qt::CaseInsensitive); } if (m_filterQuery.field.isEmpty() || m_filterQuery.field == QStringLiteral("^type")) { /// Check entry's type const QString type = entry->type(); /// Check type's description ("Journal Article") const QString label = BibTeXEntries::instance().label(type); // TODO test for internationalized variants like "Artikel" or "bok" as well? int i = 0; for (QStringList::ConstIterator itsl = m_filterQuery.terms.constBegin(); itsl != m_filterQuery.terms.constEnd(); ++itsl, ++i) eachTerm[i] |= (*itsl).isEmpty() ? true : type.contains(*itsl, Qt::CaseInsensitive) || label.contains(*itsl, Qt::CaseInsensitive); } for (Entry::ConstIterator it = entry->constBegin(); it != entry->constEnd(); ++it) if (m_filterQuery.field.isEmpty() || m_filterQuery.field == it.key().toLower()) { int i = 0; for (QStringList::ConstIterator itsl = m_filterQuery.terms.constBegin(); itsl != m_filterQuery.terms.constEnd(); ++itsl, ++i) eachTerm[i] |= (*itsl).isEmpty() ? true : it.value().containsPattern(*itsl); } /// Test associated PDF files if (m_filterQuery.searchPDFfiles && m_filterQuery.field.isEmpty()) {///< not filtering for any specific field const auto entryUrlList = FileInfo::entryUrls(entry, fileSourceModel()->bibliographyFile()->property(File::Url, QUrl()).toUrl(), FileInfo::TestExistenceYes); for (const QUrl &url : entryUrlList) { if (url.isLocalFile() && url.fileName().endsWith(QStringLiteral(".pdf"))) { const QString text = FileInfo::pdfToText(url.toLocalFile()); int i = 0; for (QStringList::ConstIterator itsl = m_filterQuery.terms.constBegin(); itsl != m_filterQuery.terms.constEnd(); ++itsl, ++i) eachTerm[i] |= (*itsl).isEmpty() ? true : text.contains(*itsl, Qt::CaseInsensitive); } } } int i = 0; if (m_filterQuery.field.isEmpty()) for (QStringList::ConstIterator itsl = m_filterQuery.terms.constBegin(); itsl != m_filterQuery.terms.constEnd(); ++itsl, ++i) eachTerm[i] |= (*itsl).isEmpty() ? true : entry->id().contains(*itsl, Qt::CaseInsensitive); } else { QSharedPointer<Macro> macro = rowElement.dynamicCast<Macro>(); if (!macro.isNull()) { if (m_filterQuery.field.isEmpty()) { int i = 0; for (QStringList::ConstIterator itsl = m_filterQuery.terms.constBegin(); itsl != m_filterQuery.terms.constEnd(); ++itsl, ++i) eachTerm[i] |= macro->value().containsPattern(*itsl, Qt::CaseInsensitive) || macro->key().contains(*itsl, Qt::CaseInsensitive); } if (m_filterQuery.field.isEmpty() || m_filterQuery.field == QStringLiteral("^type")) { static const QString label = QStringLiteral("macro"); int i = 0; for (QStringList::ConstIterator itsl = m_filterQuery.terms.constBegin(); itsl != m_filterQuery.terms.constEnd(); ++itsl, ++i) eachTerm[i] = eachTerm[i] || label.contains(*itsl, Qt::CaseInsensitive); } } else { QSharedPointer<Comment> comment = rowElement.dynamicCast<Comment>(); if (!comment.isNull()) { if (m_filterQuery.field.isEmpty()) { int i = 0; for (QStringList::ConstIterator itsl = m_filterQuery.terms.constBegin(); itsl != m_filterQuery.terms.constEnd(); ++itsl, ++i) eachTerm[i] |= (*itsl).isEmpty() ? true : comment->text().contains(*itsl, Qt::CaseInsensitive); } if (m_filterQuery.field.isEmpty() || m_filterQuery.field == QStringLiteral("^type")) { static const QString label = QStringLiteral("comment"); int i = 0; for (QStringList::ConstIterator itsl = m_filterQuery.terms.constBegin(); itsl != m_filterQuery.terms.constEnd(); ++itsl, ++i) eachTerm[i] = eachTerm[i] || label.contains(*itsl, Qt::CaseInsensitive); } } else { QSharedPointer<Preamble> preamble = rowElement.dynamicCast<Preamble>(); if (!preamble.isNull()) { if (m_filterQuery.field.isEmpty()) { int i = 0; for (QStringList::ConstIterator itsl = m_filterQuery.terms.constBegin(); itsl != m_filterQuery.terms.constEnd(); ++itsl, ++i) eachTerm[i] |= preamble->value().containsPattern(*itsl, Qt::CaseInsensitive); } if (m_filterQuery.field.isEmpty() || m_filterQuery.field == QStringLiteral("^type")) { static const QString label = QStringLiteral("preamble"); int i = 0; for (QStringList::ConstIterator itsl = m_filterQuery.terms.constBegin(); itsl != m_filterQuery.terms.constEnd(); ++itsl, ++i) eachTerm[i] = eachTerm[i] || label.contains(*itsl, Qt::CaseInsensitive); } } } } } bool every = true, any = false; for (int i = m_filterQuery.terms.count() - 1; i >= 0; --i) { every &= eachTerm[i]; any |= eachTerm[i]; } if (m_filterQuery.combination == SortFilterFileModel::AnyTerm) return any; else return every; } diff --git a/src/gui/preferences/settingscolorlabelwidget.cpp b/src/gui/preferences/settingscolorlabelwidget.cpp index 3814cc49..ce8a6939 100644 --- a/src/gui/preferences/settingscolorlabelwidget.cpp +++ b/src/gui/preferences/settingscolorlabelwidget.cpp @@ -1,519 +1,520 @@ /*************************************************************************** * 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 "settingscolorlabelwidget.h" #include "settingscolorlabelwidget_p.h" #include <ctime> #include <QLayout> #include <QStyledItemDelegate> #include <QSignalMapper> #include <QPushButton> #include <QLineEdit> #include <QMenu> #include <KColorButton> #include <KActionMenu> #include <KLocalizedString> +#include "entry.h" #include "file.h" #include "fileview.h" #include "colorlabelwidget.h" #include "models/filemodel.h" #include "preferences.h" class ColorLabelSettingsDelegate : public QStyledItemDelegate { Q_OBJECT public: ColorLabelSettingsDelegate(QWidget *parent = nullptr) : QStyledItemDelegate(parent) { /// nothing } QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &, const QModelIndex &index) const override { if (index.column() == 0) /// Colors are to be edited in a color button return new KColorButton(parent); else /// Text strings are to be edited in a line edit return new QLineEdit(parent); } void setEditorData(QWidget *editor, const QModelIndex &index) const override { if (index.column() == 0) { KColorButton *colorButton = qobject_cast<KColorButton *>(editor); /// Initialized color button with row's current color colorButton->setColor(index.model()->data(index, Qt::EditRole).value<QColor>()); } else { QLineEdit *lineEdit = qobject_cast<QLineEdit *>(editor); /// Initialized line edit with row's current color's label lineEdit->setText(index.model()->data(index, Qt::EditRole).toString()); } } void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override { if (index.column() == 0) { KColorButton *colorButton = qobject_cast<KColorButton *>(editor); if (colorButton->color() != Qt::black) /// Assign color button's color back to model model->setData(index, colorButton->color(), Qt::EditRole); } else if (index.column() == 1) { QLineEdit *lineEdit = qobject_cast<QLineEdit *>(editor); if (!lineEdit->text().isEmpty()) /// Assign line edit's text back to model model->setData(index, lineEdit->text(), Qt::EditRole); } } QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override { QSize hint = QStyledItemDelegate::sizeHint(option, index); QFontMetrics fm = QFontMetrics(QFont()); /// Enforce minimum height of 4 times x-height hint.setHeight(qMax(hint.height(), fm.xHeight() * 4)); return hint; } }; ColorLabelSettingsModel::ColorLabelSettingsModel(QObject *parent) : QAbstractItemModel(parent) { /// Load stored color-label pairs loadState(); } int ColorLabelSettingsModel::rowCount(const QModelIndex &parent) const { /// Single-level list of color-label pairs has as many rows as pairs return parent == QModelIndex() ? colorLabelPairs.size() : 0; } int ColorLabelSettingsModel::columnCount(const QModelIndex &parent) const { /// Single-level list of color-label pairs has as 2 columns /// (one of color, one for label) return parent == QModelIndex() ? 2 : 0; } QModelIndex ColorLabelSettingsModel::index(int row, int column, const QModelIndex &parent) const { if (row >= 0 && row <= colorLabelPairs.size() - 1 && column >= 0 && column <= 1 && parent == QModelIndex()) /// Create index for valid combinations of row, column, and parent return createIndex(row, column, row); else return QModelIndex(); } QModelIndex ColorLabelSettingsModel::parent(const QModelIndex &) const { /// Single-level list's indices have no other index as parent return QModelIndex(); } QVariant ColorLabelSettingsModel::data(const QModelIndex &index, int role) const { /// Skip invalid model indices if (index == QModelIndex() || index.row() < 0 || index.row() >= colorLabelPairs.size()) return QVariant(); if ((role == Qt::DecorationRole || role == Qt::EditRole) && index.column() == 0) /// First column has colors only (no text) return colorLabelPairs[index.row()].first; else if ((role == Qt::DisplayRole || role == Qt::EditRole) && index.column() == 1) /// Second column has colors' labels return colorLabelPairs[index.row()].second; return QVariant(); } bool ColorLabelSettingsModel::setData(const QModelIndex &index, const QVariant &value, int role) { if (role == Qt::EditRole) { const QModelIndex left = index.sibling(index.row(), 0); const QModelIndex right = index.sibling(index.row(), 1); if (index.column() == 0 && value.canConvert<QColor>()) { /// For first column if a valid color is to be set ... const QColor color = value.value<QColor>(); if (color != Qt::black && (color.red() > 0 || color.green() > 0 || color.blue() > 0)) { /// ... store this color in the data structure colorLabelPairs[index.row()].first = color; /// Notify everyone about the changes emit dataChanged(left, right); emit modified(); return true; } } else if (index.column() == 1 && value.canConvert<QString>()) { /// For second column if a label text is to be set ... const QString text = value.toString(); if (!text.isEmpty()) { /// ... store this text in the data structure colorLabelPairs[index.row()].second = text; /// Notify everyone about the changes emit dataChanged(left, right); emit modified(); return true; } } } return false; } Qt::ItemFlags ColorLabelSettingsModel::flags(const QModelIndex &index) const { Qt::ItemFlags result = QAbstractItemModel::flags(index); result |= Qt::ItemIsEditable; ///< all cells can be edited (color or text label) return result; } QVariant ColorLabelSettingsModel::headerData(int section, Qt::Orientation orientation, int role) const { /// Only vertical lists supported if (orientation != Qt::Horizontal || role != Qt::DisplayRole) return QVariant(); switch (section) { case 0: return i18n("Color"); case 1: return i18n("Label"); default: return QVariant(); } } /** * Load list of color-label pairs from user's configuration file. * Fall back on pre-defined colors and labels if no user settings exist. */ void ColorLabelSettingsModel::loadState() { colorLabelPairs = Preferences::instance().colorCodes(); } /** * Save list of color-label pairs in user's configuration file. */ void ColorLabelSettingsModel::saveState() { Preferences::instance().setColorCodes(colorLabelPairs); } /** * Revert in-memory data structure (list of color-label pairs) to defaults. * Does not touch user's configuration file (would require an Apply operation). */ void ColorLabelSettingsModel::resetToDefaults() { colorLabelPairs = Preferences::instance().defaultColorCodes; emit modified(); } /** * Add a new color-label pair to this model. * The pair will be appended to the list's end. * No check is performed if a similar color or label is already in use. * * @param color Color of the color-label pair * @param label Label of the color-label pair */ void ColorLabelSettingsModel::addColorLabel(const QColor &color, const QString &label) { const int newRow = colorLabelPairs.size(); beginInsertRows(QModelIndex(), newRow, newRow); colorLabelPairs.append(qMakePair(color, label)); endInsertRows(); emit modified(); } /** * Remove a color-label pair from this model. * The pair is identified by the row number. * * @param row Row number of the pair to be removed */ void ColorLabelSettingsModel::removeColorLabel(int row) { if (row >= 0 && row < colorLabelPairs.size()) { beginRemoveRows(QModelIndex(), row, row); colorLabelPairs.removeAt(row); endRemoveRows(); emit modified(); } } class SettingsColorLabelWidget::Private { private: SettingsColorLabelWidget *p; ColorLabelSettingsDelegate *delegate; public: ColorLabelSettingsModel *model; QPushButton *buttonRemove; QTreeView *view; Private(SettingsColorLabelWidget *parent) : p(parent), delegate(nullptr), model(nullptr), buttonRemove(nullptr), view(nullptr) { /// nothing } void loadState() { /// Delegate state maintenance to model if (model != nullptr) model->loadState(); } void saveState() { /// Delegate state maintenance to model if (model != nullptr) model->saveState(); } void resetToDefaults() { /// Delegate state maintenance to model if (model != nullptr) model->resetToDefaults(); } void setupGUI() { QGridLayout *layout = new QGridLayout(p); layout->setMargin(0); /// Central element in the main widget /// is a tree view for color-label pairs view = new QTreeView(p); layout->addWidget(view, 0, 0, 3, 1); view->setRootIsDecorated(false); /// Tree view's model maintains color-label pairs model = new ColorLabelSettingsModel(view); view->setModel(model); /// Changes in the model (e.g. through setData(..)) /// get propagated as this widget's changed() signal connect(model, &ColorLabelSettingsModel::modified, p, &SettingsColorLabelWidget::changed); /// Delegate to handle changes of color (through KColorButton) /// and label (throuh QLineEdit) delegate = new ColorLabelSettingsDelegate(view); view->setItemDelegate(delegate); /// Button to add a new randomized color QPushButton *buttonAdd = new QPushButton(QIcon::fromTheme(QStringLiteral("list-add")), i18n("Add..."), p); layout->addWidget(buttonAdd, 0, 1, 1, 1); connect(buttonAdd, &QPushButton::clicked, p, &SettingsColorLabelWidget::addColor); /// Remove selected color-label pair; button is disabled /// if no row is selected in tree view buttonRemove = new QPushButton(QIcon::fromTheme(QStringLiteral("list-remove")), i18n("Remove"), p); layout->addWidget(buttonRemove, 1, 1, 1, 1); buttonRemove->setEnabled(false); connect(buttonRemove, &QPushButton::clicked, p, &SettingsColorLabelWidget::removeColor); } }; SettingsColorLabelWidget::SettingsColorLabelWidget(QWidget *parent) : SettingsAbstractWidget(parent), d(new Private(this)) { /// Seed random number generator qsrand(time(nullptr)); /// Setup GUI elements d->setupGUI(); /// Connect signals connect(d->view->selectionModel(), &QItemSelectionModel::selectionChanged, this, &SettingsColorLabelWidget::updateRemoveButtonStatus); } SettingsColorLabelWidget::~SettingsColorLabelWidget() { delete d; } QString SettingsColorLabelWidget::label() const { return i18n("Color & Labels"); } QIcon SettingsColorLabelWidget::icon() const { return QIcon::fromTheme(QStringLiteral("preferences-desktop-color")); } void SettingsColorLabelWidget::loadState() { d->loadState(); } void SettingsColorLabelWidget::saveState() { d->saveState(); } void SettingsColorLabelWidget::resetToDefaults() { d->resetToDefaults(); } void SettingsColorLabelWidget::addColor() { /// Create a randomized color, but guarantee /// some minimum value for each color component const QColor newColor((qrand() & 0xff) | 0x30, (qrand() & 0xff) | 0x30, (qrand() & 0xff) | 0x30); /// Set the new label to be the color's hex string const QString newColorName(newColor.name().remove(QLatin1Char('#'))); /// Add new color-label pair to model's data d->model->addColorLabel(newColor, i18nc("Label for a new color; placeholder is for a 6-digit hex string", "NewColor%1", newColorName)); } void SettingsColorLabelWidget::removeColor() { if (!d->view->selectionModel()->selectedIndexes().isEmpty()) { /// Determine which row is selected const int row = d->view->selectionModel()->selectedIndexes().first().row(); /// Remove row from model d->model->removeColorLabel(row); updateRemoveButtonStatus(); } } void SettingsColorLabelWidget::updateRemoveButtonStatus() { /// Enable remove button iff tree view's selection is not empty d->buttonRemove->setEnabled(!d->view->selectionModel()->selectedIndexes().isEmpty()); } class ColorLabelContextMenu::Private { private: // UNUSED ColorLabelContextMenu *p; public: /// Tree view to show this context menu in FileView *fileView; /// Actual menu to show KActionMenu *menu; /// Signal handle to react to calls to items in menu QSignalMapper *sm; Private(FileView *fv, ColorLabelContextMenu *parent) : /* UNUSED p(parent),*/ fileView(fv) { sm = new QSignalMapper(parent); menu = new KActionMenu(QIcon::fromTheme(QStringLiteral("preferences-desktop-color")), i18n("Color"), fileView); /// Let menu be a sub menu to the tree view's context menu fileView->addAction(menu); } void rebuildMenu() { menu->menu()->clear(); /// Add color-label pairs to menu as stored /// in the user's configuration file for (QVector<QPair<QColor, QString>>::ConstIterator it = Preferences::instance().colorCodes().constBegin(); it != Preferences::instance().colorCodes().constEnd(); ++it) { QAction *action = new QAction(QIcon(ColorLabelWidget::createSolidIcon(it->first)), it->second, menu); menu->addAction(action); sm->setMapping(action, it->first.name()); connect(action, &QAction::triggered, sm, static_cast<void(QSignalMapper::*)()>(&QSignalMapper::map)); } QAction *action = new QAction(menu); action->setSeparator(true); menu->addAction(action); /// Special action that removes any color /// from a BibTeX entry by setting the color to black action = new QAction(i18n("No color"), menu); menu->addAction(action); sm->setMapping(action, QStringLiteral("#000000")); connect(action, &QAction::triggered, sm, static_cast<void(QSignalMapper::*)()>(&QSignalMapper::map)); } }; ColorLabelContextMenu::ColorLabelContextMenu(FileView *widget) : QObject(widget), d(new Private(widget, this)) { connect(d->sm, static_cast<void(QSignalMapper::*)(const QString &)>(&QSignalMapper::mapped), this, &ColorLabelContextMenu::colorActivated); /// Listen to changes in the configuration files NotificationHub::registerNotificationListener(this, NotificationHub::EventConfigurationChanged); d->rebuildMenu(); } ColorLabelContextMenu::~ColorLabelContextMenu() { delete d; } KActionMenu *ColorLabelContextMenu::menuAction() { return d->menu; } void ColorLabelContextMenu::setEnabled(bool enabled) { d->menu->setEnabled(enabled); } void ColorLabelContextMenu::notificationEvent(int eventId) { if (eventId == NotificationHub::EventConfigurationChanged) d->rebuildMenu(); } void ColorLabelContextMenu::colorActivated(const QString &colorString) { /// User activated some color from the menu, /// so apply this color code to the currently /// selected item in the tree view SortFilterFileModel *sfbfm = qobject_cast<SortFilterFileModel *>(d->fileView->model()); Q_ASSERT_X(sfbfm != nullptr, "ColorLabelContextMenu::colorActivated(const QString &colorString)", "SortFilterFileModel *sfbfm is NULL"); FileModel *model = sfbfm->fileSourceModel(); Q_ASSERT_X(model != nullptr, "ColorLabelContextMenu::colorActivated(const QString &colorString)", "FileModel *model is NULL"); /// Apply color change to all selected rows const QModelIndexList list = d->fileView->selectionModel()->selectedIndexes(); for (const QModelIndex &index : list) { const QModelIndex mappedIndex = sfbfm->mapToSource(index); /// Selection may span over multiple columns; /// to avoid duplicate assignments, consider only column 1 if (mappedIndex.column() == 1) { const int row = mappedIndex.row(); QSharedPointer<Entry> entry = model->element(row).dynamicCast<Entry>(); if (!entry.isNull()) { /// Clear old color entry bool modifying = entry->remove(Entry::ftColor) > 0; if (colorString != QStringLiteral("#000000")) { ///< black is a special color that means "no color" /// Only if valid color was selected, set this color Value v; v.append(QSharedPointer<VerbatimText>(new VerbatimText(colorString))); entry->insert(Entry::ftColor, v); modifying = true; } if (modifying) model->elementChanged(row); } } } } #include "settingscolorlabelwidget.moc" diff --git a/src/gui/valuelistmodel.cpp b/src/gui/valuelistmodel.cpp index d4861d84..7f93feab 100644 --- a/src/gui/valuelistmodel.cpp +++ b/src/gui/valuelistmodel.cpp @@ -1,572 +1,573 @@ /*************************************************************************** * 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 "valuelistmodel.h" #include <typeinfo> #include <QApplication> #include <QTextDocument> #include <QAbstractTextDocumentLayout> #include <QListView> #include <QLineEdit> #include <QGridLayout> #include <QStringListModel> #include <QPainter> #include <QFrame> #include <QLayout> #include <QHeaderView> #include <QComboBox> #include <KLocalizedString> #include <KColorScheme> #include "fieldlineedit.h" #include "bibtexfields.h" #include "entry.h" +#include "value.h" #include "preferences.h" #include "models/filemodel.h" #include "logging_gui.h" QWidget *ValueListDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &sovi, const QModelIndex &index) const { if (index.column() == 0) { const FieldDescription &fd = BibTeXFields::instance().find(m_fieldName); FieldLineEdit *fieldLineEdit = new FieldLineEdit(fd.preferredTypeFlag, fd.typeFlags, false, parent); fieldLineEdit->setAutoFillBackground(true); return fieldLineEdit; } else return QStyledItemDelegate::createEditor(parent, sovi, index); } void ValueListDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const { if (index.column() == 0) { FieldLineEdit *fieldLineEdit = qobject_cast<FieldLineEdit *>(editor); if (fieldLineEdit != nullptr) fieldLineEdit->reset(index.model()->data(index, Qt::EditRole).value<Value>()); } } void ValueListDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const { FieldLineEdit *fieldLineEdit = qobject_cast<FieldLineEdit *>(editor); if (fieldLineEdit != nullptr) { Value v; fieldLineEdit->apply(v); if (v.count() == 1) /// field should contain exactly one value item (no zero, not two or more) model->setData(index, QVariant::fromValue(v)); } } QSize ValueListDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const { QSize size = QStyledItemDelegate::sizeHint(option, index); size.setHeight(qMax(size.height(), option.fontMetrics.height() * 3 / 2)); // TODO calculate height better return size; } void ValueListDelegate::commitAndCloseEditor() { QLineEdit *editor = qobject_cast<QLineEdit *>(sender()); emit commitData(editor); emit closeEditor(editor); } void ValueListDelegate::initStyleOption(QStyleOptionViewItem *option, const QModelIndex &index) const { QStyledItemDelegate::initStyleOption(option, index); if (option->decorationPosition != QStyleOptionViewItem::Top) { /// remove text from style (do not draw text) option->text.clear(); } } void ValueListDelegate::paint(QPainter *painter, const QStyleOptionViewItem &_option, const QModelIndex &index) const { QStyleOptionViewItem option = _option; /// code heavily inspired by kdepimlibs-4.6.3/akonadi/collectionstatisticsdelegate.cpp /// save painter's state, restored before leaving this function painter->save(); /// first, paint the basic, but without the text. We remove the text /// in initStyleOption(), which gets called by QStyledItemDelegate::paint(). QStyledItemDelegate::paint(painter, option, index); /// now, we retrieve the correct style option by calling intiStyleOption from /// the superclass. QStyledItemDelegate::initStyleOption(&option, index); QString field = option.text; /// now calculate the rectangle for the text QStyle *s = m_parent->style(); const QWidget *widget = option.widget; const QRect textRect = s->subElementRect(QStyle::SE_ItemViewItemText, &option, widget); if (option.state & QStyle::State_Selected) { /// selected lines are drawn with different color painter->setPen(option.palette.highlightedText().color()); } /// count will be empty unless only one column is shown const QString count = index.column() == 0 && index.model()->columnCount() == 1 ? QString(QStringLiteral(" (%1)")).arg(index.data(ValueListModel::CountRole).toInt()) : QString(); /// squeeze the folder text if it is to big and calculate the rectangles /// where the folder text and the unread count will be drawn to QFontMetrics fm(painter->fontMetrics()); int countWidth = fm.width(count); int fieldWidth = fm.width(field); if (countWidth + fieldWidth > textRect.width()) { /// text plus count is too wide for column, cut text and insert "..." field = fm.elidedText(field, Qt::ElideRight, textRect.width() - countWidth - 8); fieldWidth = textRect.width() - countWidth - 12; } /// determine rects to draw field int top = textRect.top() + (textRect.height() - fm.height()) / 2; QRect fieldRect = textRect; QRect countRect = textRect; fieldRect.setTop(top); fieldRect.setHeight(fm.height()); if (m_parent->header()->visualIndex(index.column()) == 0) { /// left-align text fieldRect.setLeft(fieldRect.left() + 4); ///< hm, indent necessary? fieldRect.setRight(fieldRect.left() + fieldWidth); } else { /// right-align text fieldRect.setRight(fieldRect.right() - 4); ///< hm, indent necessary? fieldRect.setLeft(fieldRect.right() - fieldWidth); ///< hm, indent necessary? } /// draw field name painter->drawText(fieldRect, Qt::AlignLeft, field); if (!count.isEmpty()) { /// determine rects to draw count countRect.setTop(top); countRect.setHeight(fm.height()); countRect.setLeft(fieldRect.right()); /// use bold font QFont font = painter->font(); font.setBold(true); painter->setFont(font); /// determine color for count number const QColor countColor = (option.state & QStyle::State_Selected) ? KColorScheme(QPalette::Active, KColorScheme::Selection).foreground(KColorScheme::LinkText).color() : KColorScheme(QPalette::Active, KColorScheme::View).foreground(KColorScheme::LinkText).color(); painter->setPen(countColor); /// draw count painter->drawText(countRect, Qt::AlignLeft, count); } /// restore painter's state painter->restore(); } ValueListModel::ValueListModel(const File *bibtexFile, const QString &fieldName, QObject *parent) : QAbstractTableModel(parent), file(bibtexFile), fName(fieldName.toLower()), showCountColumn(true), sortBy(SortByText) { readConfiguration(); updateValues(); NotificationHub::registerNotificationListener(this, NotificationHub::EventConfigurationChanged); } int ValueListModel::rowCount(const QModelIndex &parent) const { return parent == QModelIndex() ? values.count() : 0; } int ValueListModel::columnCount(const QModelIndex &parent) const { return parent == QModelIndex() ? (showCountColumn ? 2 : 1) : 0; } QVariant ValueListModel::data(const QModelIndex &index, int role) const { if (index.row() >= values.count() || index.column() >= 2) return QVariant(); if (role == Qt::DisplayRole || role == Qt::ToolTipRole) { if (index.column() == 0) { if (fName == Entry::ftColor) { QString text = values[index.row()].text; if (text.isEmpty()) return QVariant(); QString colorText = colorToLabel[text]; if (colorText.isEmpty()) return QVariant(text); return QVariant(colorText); } else return QVariant(values[index.row()].text); } else return QVariant(values[index.row()].count); } else if (role == SortRole) { static const QRegularExpression ignoredInSorting(QStringLiteral("[{}\\\\]+")); QString buffer = values[index.row()].sortBy.isEmpty() ? values[index.row()].text : values[index.row()].sortBy; buffer = buffer.remove(ignoredInSorting).toLower(); if ((showCountColumn && index.column() == 1) || (!showCountColumn && sortBy == SortByCount)) { /// Sort by string consisting of a zero-padded count and the lower-case text, /// for example "0000000051keyword" /// Used if (a) two columns are shown (showCountColumn is true) and column 1 /// (the count column) is to be sorted or (b) if only one column is shown /// (showCountColumn is false) and this single column is to be sorted by count. return QString(QStringLiteral("%1%2")).arg(values[index.row()].count, 10, 10, QLatin1Char('0')).arg(buffer); } else { /// Otherwise use lower-case text for sorting return QVariant(buffer); } } else if (role == SearchTextRole) { return QVariant(values[index.row()].text); } else if (role == Qt::EditRole) return QVariant::fromValue(values[index.row()].value); else if (role == CountRole) return QVariant(values[index.row()].count); else return QVariant(); } bool ValueListModel::setData(const QModelIndex &index, const QVariant &value, int role) { Q_ASSERT_X(file != nullptr, "ValueListModel::setData", "You cannot set data if there is no BibTeX file associated with this value list."); /// Continue only if in edit role and first column is to be changed if (role == Qt::EditRole && index.column() == 0) { /// Fetch the string as it was shown before the editing started QString origText = data(index, Qt::DisplayRole).toString(); /// Special treatment for colors if (fName == Entry::ftColor) { /// for colors, convert color (RGB) to the associated label QString color = colorToLabel.key(origText); if (!color.isEmpty()) origText = color; } /// Retrieve the Value object containing the user-entered data Value newValue = value.value<Value>(); /// nice variable names ... ;-) if (newValue.isEmpty()) { qCWarning(LOG_KBIBTEX_GUI) << "Cannot replace with empty value"; return false; } /// Fetch the string representing the new, user-entered value const QString newText = PlainTextValue::text(newValue); if (newText == origText) { qCWarning(LOG_KBIBTEX_GUI) << "Skipping to replace value with itself"; return false; } bool success = searchAndReplaceValueInEntries(index, newValue) && searchAndReplaceValueInModel(index, newValue); return success; } return false; } Qt::ItemFlags ValueListModel::flags(const QModelIndex &index) const { Qt::ItemFlags result = QAbstractTableModel::flags(index); /// make first column editable if (index.column() == 0) result |= Qt::ItemIsEditable; return result; } QVariant ValueListModel::headerData(int section, Qt::Orientation orientation, int role) const { if (section >= 2 || orientation != Qt::Horizontal || role != Qt::DisplayRole) return QVariant(); else if ((section == 0 && columnCount() == 2) || (columnCount() == 1 && sortBy == SortByText)) return QVariant(i18n("Value")); else return QVariant(i18n("Count")); } void ValueListModel::removeValue(const QModelIndex &index) { removeValueFromEntries(index); removeValueFromModel(index); } void ValueListModel::setShowCountColumn(bool showCountColumn) { beginResetModel(); this->showCountColumn = showCountColumn; endResetModel(); } void ValueListModel::setSortBy(SortBy sortBy) { beginResetModel(); this->sortBy = sortBy; endResetModel(); } void ValueListModel::notificationEvent(int eventId) { if (eventId == NotificationHub::EventConfigurationChanged) { beginResetModel(); readConfiguration(); endResetModel(); } } void ValueListModel::readConfiguration() { /// load mapping from color value to label colorToLabel.clear(); for (QVector<QPair<QColor, QString>>::ConstIterator it = Preferences::instance().colorCodes().constBegin(); it != Preferences::instance().colorCodes().constEnd(); ++it) colorToLabel.insert(it->first.name(), it->second); } void ValueListModel::updateValues() { values.clear(); if (file == nullptr) return; for (const auto &element : const_cast<const File &>(*file)) { QSharedPointer<const Entry> entry = element.dynamicCast<const Entry>(); if (!entry.isNull()) { for (Entry::ConstIterator eit = entry->constBegin(); eit != entry->constEnd(); ++eit) { QString key = eit.key().toLower(); if (key == fName) { insertValue(eit.value()); break; } if (eit.value().isEmpty()) qCWarning(LOG_KBIBTEX_GUI) << "value for key" << key << "in entry" << entry->id() << "is empty"; } } } } void ValueListModel::insertValue(const Value &value) { for (const QSharedPointer<ValueItem> &item : value) { const QString text = PlainTextValue::text(*item); if (text.isEmpty()) continue; ///< skip empty values int index = indexOf(text); if (index < 0) { /// previously unknown text ValueLine newValueLine; newValueLine.text = text; newValueLine.count = 1; newValueLine.value.append(item); /// memorize sorting criterium: /// * for persons, use last name first /// * in any case, use lower case const QSharedPointer<Person> person = item.dynamicCast<Person>(); newValueLine.sortBy = person.isNull() ? text.toLower() : person->lastName().toLower() + QStringLiteral(" ") + person->firstName().toLower(); values << newValueLine; } else { ++values[index].count; } } } int ValueListModel::indexOf(const QString &text) { QString color; QString cmpText = text; if (fName == Entry::ftColor && !(color = colorToLabel.key(text, QString())).isEmpty()) cmpText = color; if (cmpText.isEmpty()) qCWarning(LOG_KBIBTEX_GUI) << "Should never happen"; int i = 0; /// this is really slow for large data sets: O(n^2) /// maybe use a hash table instead? for (const ValueLine &valueLine : const_cast<const ValueLineList &>(values)) { if (valueLine.text == cmpText) return i; ++i; } return -1; } bool ValueListModel::searchAndReplaceValueInEntries(const QModelIndex &index, const Value &newValue) { /// Fetch the string representing the new, user-entered value const QString newText = PlainTextValue::text(newValue); if (newText.isEmpty()) return false; /// Fetch the string as it was shown before the editing started QString origText = data(index, Qt::DisplayRole).toString(); /// Special treatment for colors if (fName == Entry::ftColor) { /// for colors, convert color (RGB) to the associated label QString color = colorToLabel.key(origText); if (!color.isEmpty()) origText = color; } /// Go through all elements in the current file for (const QSharedPointer<Element> &element : const_cast<const File &>(*file)) { QSharedPointer<Entry> entry = element.dynamicCast<Entry>(); /// Process only Entry objects if (!entry.isNull()) { /// Go through every key-value pair in entry (author, title, ...) for (Entry::Iterator eit = entry->begin(); eit != entry->end(); ++eit) { /// Fetch key-value pair's key const QString key = eit.key().toLower(); /// Process only key-value pairs that are filtered for (e.g. only keywords) if (key == fName) { eit.value().replace(origText, newValue.first()); break; } } } } return true; } bool ValueListModel::searchAndReplaceValueInModel(const QModelIndex &index, const Value &newValue) { /// Fetch the string representing the new, user-entered value const QString newText = PlainTextValue::text(newValue); if (newText.isEmpty()) return false; const int row = index.row(); /// Test if user-entered text exists already in model's data /// newTextAlreadyInListIndex will be row of duplicate or /// -1 if new text is unique int newTextAlreadyInListIndex = -1; for (int r = values.count() - 1; newTextAlreadyInListIndex < 0 && r >= 0; --r) { if (row != r && values[r].text == newText) newTextAlreadyInListIndex = r; } if (newTextAlreadyInListIndex < 0) { /// User-entered text is unique, so simply replace /// old text with new text values[row].text = newText; values[row].value = newValue; const QSharedPointer<Person> person = newValue.first().dynamicCast<Person>(); values[row].sortBy = person.isNull() ? QString() : person->lastName() + QStringLiteral(" ") + person->firstName(); } else { /// The user-entered text existed before const int lastRow = values.count() - 1; if (row != lastRow) { /// Unless duplicate is last one in list, /// overwrite edited row with last row's value values[row].text = values[lastRow].text; values[row].value = values[lastRow].value; values[row].sortBy = values[lastRow].sortBy; } /// Remove last row, which is no longer used beginRemoveRows(QModelIndex(), lastRow, lastRow); values.remove(lastRow); endRemoveRows(); } /// Notify Qt about data changed emit dataChanged(index, index); return true; } void ValueListModel::removeValueFromEntries(const QModelIndex &index) { /// Retrieve the Value object containing the user-entered data const Value toBeDeletedValue = values[index.row()].value; if (toBeDeletedValue.isEmpty()) { return; } const QString toBeDeletedText = PlainTextValue::text(toBeDeletedValue); if (toBeDeletedText.isEmpty()) { return; } /// Go through all elements in the current file for (const QSharedPointer<Element> &element : const_cast<const File &>(*file)) { QSharedPointer<Entry> entry = element.dynamicCast<Entry>(); /// Process only Entry objects if (!entry.isNull()) { /// Go through every key-value pair in entry (author, title, ...) for (Entry::Iterator eit = entry->begin(); eit != entry->end(); ++eit) { /// Fetch key-value pair's key const QString key = eit.key().toLower(); /// Process only key-value pairs that are filtered for (e.g. only keywords) if (key == fName) { /// Fetch the key-value pair's value's textual representation const QString valueFullText = PlainTextValue::text(eit.value()); if (valueFullText == toBeDeletedText) { /// If the key-value pair's value's textual representation is the same /// as the value to be delted, remove this key-value pair /// This test is usually true for keys like title, year, or edition. entry->remove(key); /// This would break the Iterator, but code "breakes" from loop anyways } else { /// The test above failed, but the delete operation may have /// to be applied to a ValueItem inside the value. /// Possible keys for such a case include author, editor, or keywords. /// Process each ValueItem inside this Value for (Value::Iterator vit = eit.value().begin(); vit != eit.value().end();) { /// Similar procedure as for full values above: /// If a ValueItem's textual representation is the same /// as the shown string which has be deleted, remove the /// ValueItem from this Value. If the Value becomes empty, /// remove Value as well. const QString valueItemText = PlainTextValue::text(* (*vit)); if (valueItemText == toBeDeletedText) { /// Erase old ValueItem from this Value vit = eit.value().erase(vit); } else ++vit; } if (eit.value().isEmpty()) { /// This value does no longer contain any ValueItems. entry->remove(key); /// This would break the Iterator, but code "breakes" from loop anyways } } break; } } } } } void ValueListModel::removeValueFromModel(const QModelIndex &index) { const int row = index.row(); const int lastRow = values.count() - 1; if (row != lastRow) { /// Unless duplicate is last one in list, /// overwrite edited row with last row's value values[row].text = values[lastRow].text; values[row].value = values[lastRow].value; values[row].sortBy = values[lastRow].sortBy; emit dataChanged(index, index); } /// Remove last row, which is no longer used beginRemoveRows(QModelIndex(), lastRow, lastRow); values.remove(lastRow); endRemoveRows(); } diff --git a/src/gui/valuelistmodel.h b/src/gui/valuelistmodel.h index a24fd257..22ab3c21 100644 --- a/src/gui/valuelistmodel.h +++ b/src/gui/valuelistmodel.h @@ -1,120 +1,121 @@ /*************************************************************************** * Copyright (C) 2004-2017 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_GUI_VALUELISTMODEL_H #define KBIBTEX_GUI_VALUELISTMODEL_H #include "kbibtexgui_export.h" #include <QAbstractTableModel> #include <QTreeView> #include <QStyledItemDelegate> +#include "value.h" #include "notificationhub.h" #include "models/filemodel.h" class KBIBTEXGUI_EXPORT ValueListDelegate : public QStyledItemDelegate { Q_OBJECT private: QString m_fieldName; QTreeView *m_parent; public: explicit ValueListDelegate(QTreeView *parent = nullptr) : QStyledItemDelegate(parent), m_fieldName(QString()), m_parent(parent) {} QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &, const QModelIndex &index) const override; void setEditorData(QWidget *editor, const QModelIndex &index) const override; void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override; QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; void initStyleOption(QStyleOptionViewItem *option, const QModelIndex &index) const override; void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; void setFieldName(const QString &fieldName) { m_fieldName = fieldName; } private slots: void commitAndCloseEditor(); }; class KBIBTEXGUI_EXPORT ValueListModel : public QAbstractTableModel, private NotificationListener { Q_OBJECT public: enum ValueListModelRole { /// How many occurrences a value has CountRole = Qt::UserRole + 112, /// Role to sort values by SortRole = Qt::UserRole + 113, /// Role to get text to filter for SearchTextRole = Qt::UserRole + 114 }; enum SortBy { SortByText, SortByCount }; private: struct ValueLine { QString text; QString sortBy; Value value; int count; }; typedef QVector<ValueLine> ValueLineList; const File *file; const QString fName; ValueLineList values; QMap<QString, QString> colorToLabel; bool showCountColumn; SortBy sortBy; public: ValueListModel(const File *bibtexFile, const QString &fieldName, QObject *parent); int rowCount(const QModelIndex &parent = QModelIndex()) const override; int columnCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; Qt::ItemFlags flags(const QModelIndex &index) const override; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; void removeValue(const QModelIndex &index); void setShowCountColumn(bool showCountColumn); void setSortBy(SortBy sortBy); void notificationEvent(int eventId) override; private: void readConfiguration(); void updateValues(); void insertValue(const Value &value); int indexOf(const QString &text); QString htmlize(const QString &text) const; bool searchAndReplaceValueInEntries(const QModelIndex &index, const Value &newValue); bool searchAndReplaceValueInModel(const QModelIndex &index, const Value &newValue); void removeValueFromEntries(const QModelIndex &index); void removeValueFromModel(const QModelIndex &index); }; #endif // KBIBTEX_GUI_VALUELISTMODEL_H diff --git a/src/io/CMakeLists.txt b/src/io/CMakeLists.txt index 998c4e13..123443dc 100644 --- a/src/io/CMakeLists.txt +++ b/src/io/CMakeLists.txt @@ -1,102 +1,97 @@ # KBibTeXIO library set( kbibtexio_LIB_SRCS encoder.cpp encoderlatex.cpp encoderxml.cpp fileexporterbibtex2html.cpp fileexporterbibtex.cpp fileexporterbibutils.cpp fileexporterbibtexoutput.cpp fileexporter.cpp fileexporterpdf.cpp fileexporterps.cpp fileexporterris.cpp fileexporterrtf.cpp fileexportertoolchain.cpp fileexporterxml.cpp fileexporterxslt.cpp fileimporterbibtex.cpp fileimporterbibutils.cpp fileimporter.cpp fileimporterpdf.cpp fileimporterris.cpp fileinfo.cpp textencoder.cpp bibutils.cpp xsltransform.cpp logging_io.cpp ) set( kbibtexio_HDRS encoder.h encoderlatex.h encoderxml.h fileexporterbibtex2html.h fileexporterbibtex.h fileexporterbibutils.h fileexporterbibtexoutput.h fileexporter.h fileexporterpdf.h fileexporterps.h fileexporterris.h fileexporterrtf.h fileexportertoolchain.h fileexporterxml.h fileexporterxslt.h fileimporterbibtex.h fileimporterbibutils.h fileimporter.h fileimporterpdf.h fileimporterris.h fileinfo.h textencoder.h bibutils.h xsltransform.h ) if(UNITY_BUILD) enable_unity_build(kbibtexio kbibtexio_LIB_SRCS) endif(UNITY_BUILD) -include_directories( - ${CMAKE_SOURCE_DIR}/src/data - ${CMAKE_BINARY_DIR}/src/data -) - add_library( kbibtexio SHARED ${kbibtexio_LIB_SRCS} ) target_link_libraries( kbibtexio Qt5::Core Qt5::Widgets Qt5::XmlPatterns Qt5::Concurrent KF5::I18n KF5::XmlGui Poppler::Qt5 ${ICU_LIBRARIES} kbibtexconfig kbibtexdata ) set_target_properties( kbibtexio PROPERTIES EXPORT_NAME "kbibtexio" VERSION ${KBIBTEX_RELEASE_VERSION} SOVERSION ${KBIBTEX_SOVERSION} ) install( TARGETS kbibtexio ${INSTALL_TARGETS_DEFAULT_ARGS} ) generate_export_header( kbibtexio ) diff --git a/src/networking/CMakeLists.txt b/src/networking/CMakeLists.txt index 5ba120b6..bc082185 100644 --- a/src/networking/CMakeLists.txt +++ b/src/networking/CMakeLists.txt @@ -1,133 +1,131 @@ # Network library set( kbibtexnetworking_LIB_SRCS onlinesearch/onlinesearchabstract.cpp onlinesearch/onlinesearchbibsonomy.cpp onlinesearch/onlinesearcharxiv.cpp onlinesearch/onlinesearchsciencedirect.cpp onlinesearch/onlinesearchgooglescholar.cpp onlinesearch/onlinesearchieeexplore.cpp onlinesearch/onlinesearchpubmed.cpp onlinesearch/onlinesearchacmportal.cpp onlinesearch/onlinesearchspringerlink.cpp onlinesearch/onlinesearchjstor.cpp onlinesearch/onlinesearchmathscinet.cpp onlinesearch/onlinesearchmrlookup.cpp onlinesearch/onlinesearchinspirehep.cpp onlinesearch/onlinesearchcernds.cpp onlinesearch/onlinesearchingentaconnect.cpp onlinesearch/onlinesearchsimplebibtexdownload.cpp onlinesearch/onlinesearchgeneral.cpp onlinesearch/onlinesearchsoanasaads.cpp # onlinesearch/onlinesearchisbndb.cpp # disabled as provider switched to a paid model on 2017-12-26 onlinesearch/onlinesearchideasrepec.cpp onlinesearch/onlinesearchdoi.cpp onlinesearch/onlinesearchbiorxiv.cpp onlinesearch/onlinesearchsemanticscholar.cpp zotero/api.cpp zotero/collectionmodel.cpp zotero/collection.cpp zotero/items.cpp zotero/groups.cpp zotero/oauthwizard.cpp zotero/tags.cpp zotero/tagmodel.cpp associatedfiles.cpp findpdf.cpp internalnetworkaccessmanager.cpp logging_networking.cpp ) set( kbibtexnetworking_HDRS onlinesearch/onlinesearchgeneral.h onlinesearch/onlinesearchsciencedirect.h onlinesearch/onlinesearchabstract.h onlinesearch/onlinesearchacmportal.h onlinesearch/onlinesearchbibsonomy.h onlinesearch/onlinesearchgooglescholar.h onlinesearch/onlinesearchspringerlink.h onlinesearch/onlinesearchjstor.h onlinesearch/onlinesearchieeexplore.h onlinesearch/onlinesearcharxiv.h onlinesearch/onlinesearchpubmed.h onlinesearch/onlinesearchingentaconnect.h onlinesearch/onlinesearchsimplebibtexdownload.h onlinesearch/onlinesearchsoanasaads.h onlinesearch/onlinesearchmathscinet.h onlinesearch/onlinesearchmrlookup.h onlinesearch/onlinesearchinspirehep.h onlinesearch/onlinesearchcernds.h onlinesearch/onlinesearchisbndb.h onlinesearch/onlinesearchideasrepec.h onlinesearch/onlinesearchdoi.h onlinesearch/onlinesearchbiorxiv.h onlinesearch/onlinesearchsemanticscholar.h zotero/api.h zotero/collectionmodel.h zotero/collection.h zotero/items.h zotero/groups.h zotero/oauthwizard.h zotero/tags.h zotero/tagmodel.h associatedfiles.h findpdf.h internalnetworkaccessmanager.h ) if(UNITY_BUILD) enable_unity_build(kbibtexnetworking kbibtexnetworking_LIB_SRCS) endif(UNITY_BUILD) include_directories( - ${CMAKE_BINARY_DIR}/src/data - ${CMAKE_SOURCE_DIR}/src/data ${CMAKE_BINARY_DIR}/src/io ${CMAKE_SOURCE_DIR}/src/io ${CMAKE_SOURCE_DIR}/src/networking/onlinesearch ${CMAKE_SOURCE_DIR}/src/networking ) add_library( kbibtexnetworking SHARED ${kbibtexnetworking_LIB_SRCS} ) target_link_libraries( kbibtexnetworking Qt5::Core Qt5::Widgets Qt5::Network Qt5::NetworkAuth KF5::I18n KF5::XmlGui KF5::Completion KF5::KIOCore KF5::KIOFileWidgets KF5::KIOWidgets KF5::KIONTLM KF5::Wallet kbibtexconfig kbibtexdata kbibtexio Poppler::Qt5 ) set_target_properties( kbibtexnetworking PROPERTIES EXPORT_NAME "kbibtexnetworking" VERSION ${KBIBTEX_RELEASE_VERSION} SOVERSION ${KBIBTEX_SOVERSION} ) install( TARGETS kbibtexnetworking ${INSTALL_TARGETS_DEFAULT_ARGS} ) generate_export_header( kbibtexnetworking ) diff --git a/src/parts/CMakeLists.txt b/src/parts/CMakeLists.txt index 061c7646..af465e17 100644 --- a/src/parts/CMakeLists.txt +++ b/src/parts/CMakeLists.txt @@ -1,87 +1,86 @@ # KBibTeX KPart set( kbibtexpart_SRCS part.cpp partfactory.cpp browserextension.cpp logging_parts.cpp ) if(UNITY_BUILD) enable_unity_build(kbibtexpart kbibtexpart_SRCS) endif(UNITY_BUILD) include_directories( ${CMAKE_BINARY_DIR} ${CMAKE_CURRENT_BINARY_DIR} - ${CMAKE_SOURCE_DIR}/src/data ${CMAKE_BINARY_DIR}/src/io ${CMAKE_SOURCE_DIR}/src/io ${CMAKE_SOURCE_DIR}/src/gui ${CMAKE_SOURCE_DIR}/src/gui/config ${CMAKE_SOURCE_DIR}/src/gui/element ${CMAKE_SOURCE_DIR}/src/gui/preferences ${CMAKE_SOURCE_DIR}/src/gui/widgets ${CMAKE_SOURCE_DIR}/src/gui/file ${CMAKE_SOURCE_DIR}/src/processing ${CMAKE_SOURCE_DIR}/src/networking ) # Creates kbibtex-git-info.h containing information about the source code's Git revision # (if source directory is a Git clone) add_custom_command( OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/kbibtex-git-info.h COMMAND ${CMAKE_COMMAND} -DSOURCE_DIR=${CMAKE_SOURCE_DIR} -DBINARY_DIR=${CMAKE_CURRENT_BINARY_DIR} -P ${CMAKE_SOURCE_DIR}/src/getgit.cmake ) set_source_files_properties( ${CMAKE_CURRENT_BINARY_DIR}/kbibtex-git-info.h PROPERTIES GENERATED 1 HEADER_FILE_ONLY 1 SKIP_AUTOMOC ON SKIP_AUTOUIC ON SKIP_AUTOGEN ON ) add_library( kbibtexpart MODULE ${kbibtexpart_SRCS} ${CMAKE_CURRENT_BINARY_DIR}/kbibtex-git-info.h ) target_link_libraries( kbibtexpart KF5::Parts KF5::CoreAddons KF5::ItemViews kbibtexconfig kbibtexdata kbibtexio kbibtexgui kbibtexproc ) install( TARGETS kbibtexpart DESTINATION ${KDE_INSTALL_PLUGINDIR} ) kcoreaddons_desktop_to_json(kbibtexpart kbibtexpart.desktop SERVICE_TYPES kpart.desktop) install( FILES kbibtexpart.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR} ) install( FILES kbibtexpartui.rc DESTINATION ${KDE_INSTALL_KXMLGUI5DIR}/kbibtexpart ) diff --git a/src/processing/CMakeLists.txt b/src/processing/CMakeLists.txt index 7a8e7ab3..1f07e3c9 100644 --- a/src/processing/CMakeLists.txt +++ b/src/processing/CMakeLists.txt @@ -1,65 +1,63 @@ # KBibTeX Processing library set( kbibtexproc_LIB_SRCS findduplicates.cpp idsuggestions.cpp lyx.cpp checkbibtex.cpp bibliographyservice.cpp journalabbreviations.cpp logging_processing.cpp ) set( kbibtexproc_HDRS findduplicates.h idsuggestions.h lyx.h checkbibtex.h bibliographyservice.h journalabbreviations.h ) if(UNITY_BUILD) enable_unity_build(kbibtexproc kbibtexproc_LIB_SRCS) endif(UNITY_BUILD) include_directories( - ${CMAKE_BINARY_DIR}/src/data - ${CMAKE_SOURCE_DIR}/src/data ${CMAKE_BINARY_DIR}/src/io ${CMAKE_SOURCE_DIR}/src/io ${CMAKE_BINARY_DIR}/src/gui ${CMAKE_SOURCE_DIR}/src/gui ) add_library( kbibtexproc SHARED ${kbibtexproc_LIB_SRCS} ) target_link_libraries( kbibtexproc Qt5::Core KF5::Parts kbibtexconfig kbibtexdata kbibtexio ) set_target_properties( kbibtexproc PROPERTIES EXPORT_NAME "kbibtexproc" VERSION ${KBIBTEX_RELEASE_VERSION} SOVERSION ${KBIBTEX_SOVERSION} ) install( TARGETS kbibtexproc ${INSTALL_TARGETS_DEFAULT_ARGS} ) generate_export_header( kbibtexproc ) diff --git a/src/program/CMakeLists.txt b/src/program/CMakeLists.txt index 98d8db9c..23b4dc76 100644 --- a/src/program/CMakeLists.txt +++ b/src/program/CMakeLists.txt @@ -1,153 +1,151 @@ # KBibTeX main program project( kbibtexprogram ) set( kbibtex_SRCS program.cpp mainwindow.cpp documentlist.cpp mdiwidget.cpp docklets/statistics.cpp docklets/referencepreview.cpp docklets/documentpreview.cpp docklets/valuelist.cpp docklets/searchform.cpp docklets/searchresults.cpp docklets/elementform.cpp docklets/filesettings.cpp docklets/zoterobrowser.cpp openfileinfo.cpp logging_program.cpp ) if(UNITY_BUILD AND NOT WIN32) # FIXME: Unity build of programs breaks on Windows enable_unity_build(kbibtex kbibtex_SRCS) endif(UNITY_BUILD AND NOT WIN32) include_directories( ${CMAKE_BINARY_DIR} ${CMAKE_CURRENT_BINARY_DIR} - ${CMAKE_SOURCE_DIR}/src/data - ${CMAKE_BINARY_DIR}/src/data ${CMAKE_SOURCE_DIR}/src/processing ${CMAKE_BINARY_DIR}/src/processing ${CMAKE_SOURCE_DIR}/src/io ${CMAKE_BINARY_DIR}/src/io ${CMAKE_SOURCE_DIR}/src/io/config ${CMAKE_SOURCE_DIR}/src/networking ${CMAKE_BINARY_DIR}/src/networking ${CMAKE_SOURCE_DIR}/src/networking/onlinesearch ${CMAKE_SOURCE_DIR}/src/gui ${CMAKE_BINARY_DIR}/src/gui ${CMAKE_SOURCE_DIR}/src/gui/widgets ${CMAKE_SOURCE_DIR}/src/gui/file ${CMAKE_SOURCE_DIR}/src/gui/element ${CMAKE_SOURCE_DIR}/src/program/docklets ) ecm_add_app_icon( kbibtex_SRCS ICONS ${CMAKE_SOURCE_DIR}/icons/128-apps-kbibtex.png ${CMAKE_SOURCE_DIR}/icons/16-apps-kbibtex.png ${CMAKE_SOURCE_DIR}/icons/22-apps-kbibtex.png ${CMAKE_SOURCE_DIR}/icons/32-apps-kbibtex.png ${CMAKE_SOURCE_DIR}/icons/48-apps-kbibtex.png ${CMAKE_SOURCE_DIR}/icons/64-apps-kbibtex.png ) # Creates kbibtex-git-info.h containing information about the source code's Git revision # (if source directory is a Git clone) add_custom_command( OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/kbibtex-git-info.h COMMAND ${CMAKE_COMMAND} -DSOURCE_DIR=${CMAKE_SOURCE_DIR} -DBINARY_DIR=${CMAKE_CURRENT_BINARY_DIR} -P ${CMAKE_SOURCE_DIR}/src/getgit.cmake ) set_source_files_properties( ${CMAKE_CURRENT_BINARY_DIR}/kbibtex-git-info.h PROPERTIES GENERATED 1 HEADER_FILE_ONLY 1 SKIP_AUTOMOC ON SKIP_AUTOUIC ON SKIP_AUTOGEN ON ) add_executable( kbibtex ${kbibtex_SRCS} ${CMAKE_CURRENT_BINARY_DIR}/kbibtex-git-info.h ) target_link_libraries( kbibtex Qt5::Core Qt5::Widgets KF5::CoreAddons KF5::I18n KF5::ConfigCore KF5::Service KF5::Parts KF5::IconThemes KF5::KIOCore KF5::KIOFileWidgets KF5::KIOWidgets KF5::KIONTLM KF5::Crash kbibtexio kbibtexgui kbibtexnetworking ) if(Qt5WebEngineWidgets_FOUND) target_link_libraries( kbibtex Qt5::WebEngineWidgets ) else(Qt5WebEngineWidgets_FOUND) if(Qt5WebKitWidgets_FOUND) target_link_libraries( kbibtex Qt5::WebKitWidgets ) endif(Qt5WebKitWidgets_FOUND) endif(Qt5WebEngineWidgets_FOUND) install( TARGETS kbibtex ${INSTALL_TARGETS_DEFAULT_ARGS} ) install( PROGRAMS org.kde.kbibtex.desktop DESTINATION ${KDE_INSTALL_APPDIR} ) install( FILES kbibtexui.rc DESTINATION ${KDE_INSTALL_KXMLGUI5DIR}/kbibtex ) install( FILES org.kde.kbibtex.appdata.xml DESTINATION ${KDE_INSTALL_METAINFODIR} ) ecm_install_icons( ICONS ${CMAKE_SOURCE_DIR}/icons/128-apps-kbibtex.png ${CMAKE_SOURCE_DIR}/icons/16-apps-kbibtex.png ${CMAKE_SOURCE_DIR}/icons/22-apps-kbibtex.png ${CMAKE_SOURCE_DIR}/icons/32-apps-kbibtex.png ${CMAKE_SOURCE_DIR}/icons/48-apps-kbibtex.png ${CMAKE_SOURCE_DIR}/icons/64-apps-kbibtex.png DESTINATION ${KDE_INSTALL_ICONDIR} ) diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt index 9d8459eb..09a21832 100644 --- a/src/test/CMakeLists.txt +++ b/src/test/CMakeLists.txt @@ -1,183 +1,182 @@ # KBibTeX test program project( test ) include( AddFileDependencies ) include( ECMMarkAsTest ) configure_file(test-config.h.in test-config.h @ONLY) include_directories( ${CMAKE_BINARY_DIR} ${CMAKE_CURRENT_BINARY_DIR} - ${CMAKE_SOURCE_DIR}/src/data ${CMAKE_SOURCE_DIR}/src/io ${CMAKE_SOURCE_DIR}/src/gui ${CMAKE_SOURCE_DIR}/src/gui/config ${CMAKE_SOURCE_DIR}/src/gui/bibtex ${CMAKE_SOURCE_DIR}/src/gui/element ${CMAKE_SOURCE_DIR}/src/gui/widgets ${CMAKE_SOURCE_DIR}/src/networking ${CMAKE_SOURCE_DIR}/src/networking/onlinesearch ${CMAKE_SOURCE_DIR}/src/processing ) set( kbibtextest_SRCS main.cpp kbibtextest.cpp logging_test.cpp ) set( kbibtexfilestest_SRCS kbibtexfilestest.cpp kbibtexfilestest-rawdata.h ) set( kbibtexnetworkingtest_SRCS kbibtexnetworkingtest.cpp ) set( kbibtexiotest_SRCS kbibtexiotest.cpp ) set( kbibtexdatatest_SRCS kbibtexdatatest.cpp ) if(UNITY_BUILD AND NOT WIN32) # FIXME: Unity build of programs breaks on Windows enable_unity_build(kbibtextest kbibtextest_SRCS) enable_unity_build(kbibtexfilestest kbibtexfilestest_SRCS) enable_unity_build(kbibtexnetworkingtest kbibtexnetworkingtest_SRCS) enable_unity_build(kbibtexiotest kbibtexiotest_SRCS) enable_unity_build(kbibtexdatatest kbibtexdatatest_SRCS) endif(UNITY_BUILD AND NOT WIN32) # Creates kbibtex-git-info.h containing information about the source code's Git revision # (if source directory is a Git clone) add_custom_command( OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/kbibtex-git-info.h COMMAND ${CMAKE_COMMAND} -DSOURCE_DIR=${CMAKE_SOURCE_DIR} -DBINARY_DIR=${CMAKE_CURRENT_BINARY_DIR} -P ${CMAKE_SOURCE_DIR}/src/getgit.cmake ) set_source_files_properties( ${CMAKE_CURRENT_BINARY_DIR}/kbibtex-git-info.h PROPERTIES GENERATED 1 HEADER_FILE_ONLY 1 SKIP_AUTOMOC ON SKIP_AUTOUIC ON SKIP_AUTOGEN ON ) add_executable( kbibtextest ${kbibtextest_SRCS} ${CMAKE_CURRENT_BINARY_DIR}/kbibtex-git-info.h ) add_executable( kbibtexfilestest ${kbibtexfilestest_SRCS} ${CMAKE_CURRENT_BINARY_DIR}/kbibtex-git-info.h ) add_executable( kbibtexnetworkingtest ${kbibtexnetworkingtest_SRCS} ${CMAKE_CURRENT_BINARY_DIR}/kbibtex-git-info.h ) add_executable( kbibtexiotest ${kbibtexiotest_SRCS} ${CMAKE_CURRENT_BINARY_DIR}/kbibtex-git-info.h ) add_executable( kbibtexdatatest ${kbibtexdatatest_SRCS} ${CMAKE_CURRENT_BINARY_DIR}/kbibtex-git-info.h ) target_link_libraries( kbibtextest Qt5::Core KF5::KIOCore kbibtexconfig kbibtexdata kbibtexio kbibtexproc kbibtexgui kbibtexnetworking ) target_link_libraries( kbibtexfilestest Qt5::Test kbibtexdata kbibtexio ) target_link_libraries( kbibtexnetworkingtest Qt5::Test kbibtexnetworking ) target_link_libraries( kbibtexiotest Qt5::Test kbibtexio ) target_link_libraries( kbibtexdatatest Qt5::Test kbibtexdata ) ecm_mark_as_test( kbibtexfilestest kbibtexnetworkingtest kbibtexiotest kbibtexdatatest ) add_test( NAME kbibtexfilestest COMMAND kbibtexfilestest ) add_test( NAME kbibtexnetworkingtest COMMAND kbibtexnetworkingtest ) add_test( NAME kbibtexiotest COMMAND kbibtexiotest ) add_test( NAME kbibtexdatatest COMMAND kbibtexdatatest )