File indexing completed on 2024-05-05 05:04:33
0001 /*************************************************************************** 0002 * SPDX-License-Identifier: GPL-2.0-or-later 0003 * * 0004 * SPDX-FileCopyrightText: 2016-2020 Thomas Fischer <fischer@unix-ag.uni-kl.de> 0005 * * 0006 * This program is free software; you can redistribute it and/or modify * 0007 * it under the terms of the GNU General Public License as published by * 0008 * the Free Software Foundation; either version 2 of the License, or * 0009 * (at your option) any later version. * 0010 * * 0011 * This program is distributed in the hope that it will be useful, * 0012 * but WITHOUT ANY WARRANTY; without even the implied warranty of * 0013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * 0014 * GNU General Public License for more details. * 0015 * * 0016 * You should have received a copy of the GNU General Public License * 0017 * along with this program; if not, see <https://www.gnu.org/licenses/>. * 0018 ***************************************************************************/ 0019 0020 #include "bibliographymodel.h" 0021 0022 #include <QSettings> 0023 0024 #include <Entry> 0025 #include <onlinesearch/OnlineSearchAbstract> 0026 0027 const int SortedBibliographyModel::SortAuthorNewestTitle = 0; 0028 const int SortedBibliographyModel::SortAuthorOldestTitle = 1; 0029 const int SortedBibliographyModel::SortNewestAuthorTitle = 2; 0030 const int SortedBibliographyModel::SortOldestAuthorTitle = 3; 0031 0032 SortedBibliographyModel::SortedBibliographyModel() 0033 : QSortFilterProxyModel(), m_sortOrder(0), model(new BibliographyModel()) { 0034 const QSettings settings(QStringLiteral("harbour-bibsearch"), QStringLiteral("BibSearch")); 0035 m_sortOrder = settings.value(QStringLiteral("sortOrder"), 0).toInt(); 0036 0037 setSourceModel(model); 0038 setDynamicSortFilter(true); 0039 sort(0); 0040 connect(model, &BibliographyModel::busyChanged, this, &SortedBibliographyModel::busyChanged); 0041 connect(model, &BibliographyModel::progressChanged, this, &SortedBibliographyModel::progressChanged); 0042 } 0043 0044 SortedBibliographyModel::~SortedBibliographyModel() { 0045 QSettings settings(QStringLiteral("harbour-bibsearch"), QStringLiteral("BibSearch")); 0046 settings.setValue(QStringLiteral("sortOrder"), m_sortOrder); 0047 0048 delete model; 0049 } 0050 0051 QHash<int, QByteArray> SortedBibliographyModel::roleNames() const { 0052 QHash<int, QByteArray> roles; 0053 roles[BibTeXIdRole] = "bibtexid"; 0054 roles[FoundViaRole] = "foundVia"; 0055 roles[TitleRole] = "title"; 0056 roles[AuthorRole] = "author"; 0057 roles[AuthorShortRole] = "authorShort"; 0058 roles[YearRole] = "year"; 0059 roles[WherePublishedRole] = "wherePublished"; 0060 roles[UrlRole] = "url"; 0061 roles[DoiRole] = "doi"; 0062 return roles; 0063 } 0064 0065 bool SortedBibliographyModel::isBusy() const { 0066 if (model != nullptr) 0067 return model->isBusy(); 0068 else 0069 return false; 0070 } 0071 0072 int SortedBibliographyModel::progress() const { 0073 if (model != nullptr) 0074 return model->progress(); 0075 else 0076 return -1; 0077 } 0078 0079 int SortedBibliographyModel::sortOrder() const { 0080 return m_sortOrder; 0081 } 0082 0083 void SortedBibliographyModel::setSortOrder(int _sortOrder) { 0084 if (_sortOrder != m_sortOrder) { 0085 m_sortOrder = _sortOrder; 0086 invalidate(); 0087 emit sortOrderChanged(m_sortOrder); 0088 } 0089 } 0090 0091 QStringList SortedBibliographyModel::humanReadableSortOrder() const { 0092 static const QStringList result { 0093 tr("Last name, newest first"), ///< SortAuthorNewestTitle 0094 tr("Last name, oldest first"), ///< SortAuthorOldestTitle 0095 tr("Newest first, last name"), ///< SortNewestAuthorTitle 0096 tr("Newest first, last name") ///< SortOldestAuthorTitle 0097 }; 0098 return result; 0099 } 0100 0101 void SortedBibliographyModel::startSearch(const QString &freeText, const QString &title, const QString &author) { 0102 if (model != nullptr) 0103 model->startSearch(freeText, title, author); 0104 } 0105 0106 void SortedBibliographyModel::clear() { 0107 if (model != nullptr) 0108 model->clear(); 0109 } 0110 0111 bool SortedBibliographyModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const { 0112 if (model == nullptr) 0113 return source_left.row() < source_right.row(); 0114 0115 const QSharedPointer<const Entry> entryLeft = model->entry(source_left.row()); 0116 const QSharedPointer<const Entry> entryRight = model->entry(source_right.row()); 0117 if (entryLeft.isNull() || entryRight.isNull()) 0118 return source_left.row() < source_right.row(); 0119 0120 SortingTriState sortingTriState = Undecided; 0121 switch (m_sortOrder) { 0122 case SortAuthorNewestTitle: 0123 sortingTriState = compareTitles(entryLeft, entryRight, compareYears(entryLeft, entryRight, MostRecentFirst, compareAuthors(entryLeft, entryRight))); 0124 break; 0125 case SortAuthorOldestTitle: 0126 sortingTriState = compareTitles(entryLeft, entryRight, compareYears(entryLeft, entryRight, LeastRecentFirst, compareAuthors(entryLeft, entryRight))); 0127 break; 0128 case SortNewestAuthorTitle: 0129 sortingTriState = compareTitles(entryLeft, entryRight, compareAuthors(entryLeft, entryRight, compareYears(entryLeft, entryRight, MostRecentFirst))); 0130 break; 0131 case SortOldestAuthorTitle: 0132 sortingTriState = compareTitles(entryLeft, entryRight, compareAuthors(entryLeft, entryRight, compareYears(entryLeft, entryRight, LeastRecentFirst))); 0133 break; 0134 default: 0135 sortingTriState = Undecided; 0136 } 0137 0138 switch (sortingTriState) { 0139 case True: 0140 return true; 0141 case False: 0142 return false; 0143 default: 0144 return (source_left.row() < source_right.row()); 0145 } 0146 } 0147 0148 SortedBibliographyModel::SortingTriState SortedBibliographyModel::compareAuthors(const QSharedPointer<const Entry> entryLeft, const QSharedPointer<const Entry> entryRight, SortedBibliographyModel::SortingTriState previousDecision) const { 0149 if (previousDecision != Undecided) return previousDecision; 0150 0151 const Value authorsLeft = entryLeft->operator[](Entry::ftAuthor); 0152 const Value authorsRight = entryRight->operator[](Entry::ftAuthor); 0153 int p = 0; 0154 while (p < authorsLeft.count() && p < authorsRight.count()) { 0155 const QSharedPointer<Person> personLeft = authorsLeft.at(p).dynamicCast<Person>(); 0156 const QSharedPointer<Person> personRight = authorsRight.at(p).dynamicCast<Person>(); 0157 if (personLeft.isNull() || personRight.isNull()) 0158 return Undecided; 0159 0160 const int cmpLast = removeNobiliaryParticle(personLeft->lastName()).localeAwareCompare(removeNobiliaryParticle(personRight->lastName())); 0161 if (cmpLast < 0) return True; 0162 else if (cmpLast > 0) return False; 0163 const int cmpFirst = personLeft->firstName().left(1).localeAwareCompare(personRight->firstName().left(1)); 0164 if (cmpFirst < 0) return True; 0165 else if (cmpFirst > 0) return False; 0166 0167 ++p; 0168 } 0169 if (authorsLeft.count() < authorsRight.count()) return True; 0170 else if (authorsLeft.count() > authorsRight.count()) return False; 0171 0172 return Undecided; 0173 } 0174 0175 SortedBibliographyModel::SortingTriState SortedBibliographyModel::compareYears(const QSharedPointer<const Entry> entryLeft, const QSharedPointer<const Entry> entryRight, AgeSorting ageSorting, SortedBibliographyModel::SortingTriState previousDecision) const { 0176 if (previousDecision != Undecided) return previousDecision; 0177 0178 bool yearLeftOk = false, yearRightOk = false; 0179 const int yearLeft = BibliographyModel::valueToText(entryLeft->operator[](Entry::ftYear)).toInt(&yearLeftOk); 0180 const int yearRight = BibliographyModel::valueToText(entryRight->operator[](Entry::ftYear)).toInt(&yearRightOk); 0181 if (yearLeftOk && yearRightOk && yearLeft > 1000 && yearRight > 1000 && yearLeft < 3000 && yearRight < 3000) { 0182 if (yearLeft < yearRight) return ageSorting == LeastRecentFirst ? True : False; 0183 else if (yearLeft > yearRight) return ageSorting == LeastRecentFirst ? False : True; 0184 } 0185 0186 return Undecided; 0187 } 0188 0189 SortedBibliographyModel::SortingTriState SortedBibliographyModel::compareTitles(const QSharedPointer<const Entry> entryLeft, const QSharedPointer<const Entry> entryRight, SortedBibliographyModel::SortingTriState previousDecision) const { 0190 if (previousDecision != Undecided) return previousDecision; 0191 0192 const QString titleLeft = BibliographyModel::valueToText(entryLeft->operator[](Entry::ftTitle)); 0193 const QString titleRight = BibliographyModel::valueToText(entryRight->operator[](Entry::ftTitle)); 0194 const int titleCmp = titleLeft.localeAwareCompare(titleRight); 0195 if (titleCmp < 0) return True; 0196 else if (titleCmp > 0) return False; 0197 0198 return Undecided; 0199 } 0200 0201 QString SortedBibliographyModel::removeNobiliaryParticle(const QString &lastname) const { 0202 static const QStringList nobiliaryParticles {QStringLiteral("af "), QStringLiteral("d'"), QStringLiteral("de "), QStringLiteral("di "), QStringLiteral("du "), QStringLiteral("of "), QStringLiteral("van "), QStringLiteral("von "), QStringLiteral("zu ")}; 0203 for (QStringList::ConstIterator it = nobiliaryParticles.constBegin(); it != nobiliaryParticles.constEnd(); ++it) 0204 if (lastname.startsWith(*it)) 0205 return lastname.mid(it->length()); 0206 return lastname; 0207 } 0208 0209 BibliographyModel::BibliographyModel() { 0210 m_file = new File(); 0211 m_runningSearches = 0; 0212 0213 m_searchEngineList = new SearchEngineList(); 0214 connect(m_searchEngineList, &SearchEngineList::foundEntry, this, &BibliographyModel::newEntry); 0215 connect(m_searchEngineList, &SearchEngineList::busyChanged, this, &BibliographyModel::busyChanged); 0216 connect(m_searchEngineList, &SearchEngineList::progressChanged, this, &BibliographyModel::progressChanged); 0217 } 0218 0219 BibliographyModel::~BibliographyModel() { 0220 delete m_file; 0221 delete m_searchEngineList; 0222 } 0223 0224 int BibliographyModel::rowCount(const QModelIndex &parent) const { 0225 if (parent == QModelIndex()) 0226 return m_file->count(); 0227 else 0228 return 0; 0229 } 0230 0231 QVariant BibliographyModel::data(const QModelIndex &index, int role) const { 0232 if (index.row() < 0 || index.row() >= m_file->count() || index.column() != 0) 0233 return QVariant(); 0234 0235 const QSharedPointer<const Entry> curEntry = entry(index.row()); 0236 0237 if (!curEntry.isNull()) { 0238 QString fieldName; 0239 switch (role) { 0240 case Qt::DisplayRole: /// fall-through on purpose 0241 case TitleRole: fieldName = Entry::ftTitle; break; 0242 case AuthorRole: fieldName = Entry::ftAuthor; break; 0243 case YearRole: fieldName = Entry::ftYear; break; 0244 } 0245 if (!fieldName.isEmpty()) 0246 return valueToText(curEntry->operator[](fieldName)); 0247 0248 if (role == BibTeXIdRole) { 0249 return curEntry->id(); 0250 } else if (role == FoundViaRole) { 0251 const QString foundVia = valueToText(curEntry->operator[](QStringLiteral("x-fetchedfrom"))); 0252 if (!foundVia.isEmpty()) return foundVia; 0253 } else if (role == AuthorRole) { 0254 const QString authors = valueToText(curEntry->operator[](Entry::ftAuthor)); 0255 if (!authors.isEmpty()) return authors; 0256 else return valueToText(curEntry->operator[](Entry::ftEditor)); 0257 } else if (role == WherePublishedRole) { 0258 const QString journal = valueToText(curEntry->operator[](Entry::ftJournal)); 0259 if (!journal.isEmpty()) { 0260 const QString volume = valueToText(curEntry->operator[](Entry::ftVolume)); 0261 const QString issue = valueToText(curEntry->operator[](Entry::ftNumber)); 0262 if (volume.isEmpty()) 0263 return journal; 0264 else if (issue.isEmpty()) /// but 'volume' is not empty 0265 return journal + QStringLiteral(" ") + volume; 0266 else /// both 'volume' and 'issue' are not empty 0267 return journal + QStringLiteral(" ") + volume + QStringLiteral(" (") + issue + QStringLiteral(")"); 0268 } 0269 const QString bookTitle = valueToText(curEntry->operator[](Entry::ftBookTitle)); 0270 if (!bookTitle.isEmpty()) return bookTitle; 0271 const bool isPhdThesis = curEntry->type() == Entry::etPhDThesis; 0272 if (isPhdThesis) { 0273 const QString school = valueToText(curEntry->operator[](Entry::ftSchool)); 0274 if (school.isEmpty()) { 0275 return tr("Doctoral dissertation"); 0276 } else { 0277 return tr("Doctoral dissertation (%1)").arg(school); 0278 } 0279 } 0280 const QString school = valueToText(curEntry->operator[](Entry::ftSchool)); 0281 if (!school.isEmpty()) return school; 0282 const QString publisher = valueToText(curEntry->operator[](Entry::ftPublisher)); 0283 if (!publisher.isEmpty()) return publisher; 0284 return QStringLiteral(""); 0285 } else if (role == AuthorShortRole) { 0286 const QStringList authors = valueToList(curEntry->operator[](Entry::ftAuthor)); 0287 switch (authors.size()) { 0288 case 0: return QString(); ///< empty list of authors 0289 case 1: return authors.first(); ///< single author 0290 case 2: 0291 return tr("%1 and %2").arg(authors.first()).arg(authors[1]); ///< two authors 0292 default: 0293 return tr("%1 and %2 more").arg(authors.first()).arg(QString::number(authors.size() - 1)); ///< three or more authors 0294 } 0295 } else if (role == UrlRole) { 0296 const QStringList doiList = valueToList(curEntry->operator[](Entry::ftDOI)); 0297 if (!doiList.isEmpty()) return QStringLiteral("https://dx.doi.org/") + doiList.first(); 0298 const QStringList urlList = valueToList(curEntry->operator[](Entry::ftUrl)); 0299 if (!urlList.isEmpty()) return urlList.first(); 0300 const QStringList bibUrlList = valueToList(curEntry->operator[](QStringLiteral("biburl"))); 0301 if (!bibUrlList.isEmpty()) return bibUrlList.first(); 0302 return QString(); 0303 } else if (role == DoiRole) { 0304 const QStringList doiList = valueToList(curEntry->operator[](Entry::ftDOI)); 0305 if (!doiList.isEmpty()) return doiList.first(); 0306 return QString(); 0307 } 0308 } 0309 0310 return QVariant(); 0311 } 0312 0313 const QSharedPointer<const Entry> BibliographyModel::entry(int row) const { 0314 const QSharedPointer<const Element> element = m_file->at(row); 0315 const QSharedPointer<const Entry> result = element.dynamicCast<const Entry>(); 0316 return result; 0317 } 0318 0319 void BibliographyModel::startSearch(const QString &freeText, const QString &title, const QString &author) { 0320 QMap<OnlineSearchAbstract::QueryKey, QString> query; 0321 query[OnlineSearchAbstract::QueryKey::FreeText] = freeText; 0322 query[OnlineSearchAbstract::QueryKey::Title] = title; 0323 query[OnlineSearchAbstract::QueryKey::Author] = author; 0324 0325 m_searchEngineList->resetProgress(); 0326 0327 m_runningSearches = 0; 0328 const QSettings settings(QStringLiteral("harbour-bibsearch"), QStringLiteral("BibSearch")); 0329 for (int i = 0; i < m_searchEngineList->size(); ++i) { 0330 OnlineSearchAbstract *osa = m_searchEngineList->at(i); 0331 const bool doSearchThisEngine = isSearchEngineEnabled(settings, osa); 0332 if (!doSearchThisEngine) continue; 0333 osa->startSearch(query, 10 /** TODO make number configurable */); 0334 ++m_runningSearches; 0335 } 0336 } 0337 0338 void BibliographyModel::clear() { 0339 beginResetModel(); 0340 m_file->clear(); 0341 endResetModel(); 0342 } 0343 0344 bool BibliographyModel::isBusy() const { 0345 for (QVector<OnlineSearchAbstract *>::ConstIterator it = m_searchEngineList->constBegin(); it != m_searchEngineList->constEnd(); ++it) { 0346 if ((*it)->busy()) return true; 0347 } 0348 return false; 0349 } 0350 0351 int BibliographyModel::progress() const { 0352 return m_searchEngineList->progress(); 0353 } 0354 0355 void BibliographyModel::searchFinished() { 0356 --m_runningSearches; 0357 } 0358 0359 void BibliographyModel::newEntry(QSharedPointer<Entry> e) { 0360 const int n = m_file->count(); 0361 beginInsertRows(QModelIndex(), n, n); 0362 m_file->insert(n, e); 0363 endInsertRows(); 0364 } 0365 0366 QString BibliographyModel::valueToText(const Value &value) { 0367 return valueToList(value).join(QStringLiteral(", ")); 0368 } 0369 0370 0371 QStringList BibliographyModel::valueToList(const Value &value) { 0372 if (value.isEmpty()) return QStringList(); 0373 0374 QStringList resultItems; 0375 0376 const QSharedPointer<const Person> firstPerson = value.first().dynamicCast<const Person>(); 0377 if (!firstPerson.isNull()) { 0378 /// First item in value is a Person, assume all other items are Persons as well 0379 for (Value::ConstIterator it = value.constBegin(); it != value.constEnd(); ++it) { 0380 QSharedPointer<const Person> person = (*it).dynamicCast<const Person>(); 0381 if (person.isNull()) continue; 0382 const QString name = personToText(person); 0383 if (name.isEmpty()) continue; 0384 resultItems.append(beautifyLaTeX(name)); 0385 } 0386 } else { 0387 for (Value::ConstIterator it = value.constBegin(); it != value.constEnd(); ++it) { 0388 const QString valueItem = valueItemToText(*it); 0389 if (valueItem.isEmpty()) continue; 0390 resultItems.append(beautifyLaTeX(valueItem)); 0391 } 0392 } 0393 0394 return resultItems; 0395 } 0396 0397 QString BibliographyModel::personToText(const QSharedPointer<const Person> &person) { 0398 if (person.isNull()) return QString(); 0399 QString name = person->lastName(); 0400 if (name.isEmpty()) return QString(); 0401 const QString firstName = person->firstName().left(1); 0402 if (!firstName.isEmpty()) 0403 name = name.prepend(QStringLiteral(". ")).prepend(firstName); 0404 return name; 0405 } 0406 0407 QString BibliographyModel::valueItemToText(const QSharedPointer<ValueItem> &valueItem) { 0408 const QSharedPointer<PlainText> plainText = valueItem.dynamicCast<PlainText>(); 0409 if (!plainText.isNull()) 0410 return plainText->text(); 0411 else { 0412 const QSharedPointer<VerbatimText> verbatimText = valueItem.dynamicCast<VerbatimText>(); 0413 if (!verbatimText.isNull()) 0414 return verbatimText->text(); 0415 else { 0416 const QSharedPointer<MacroKey> macroKey = valueItem.dynamicCast<MacroKey>(); 0417 if (!macroKey.isNull()) 0418 return macroKey->text(); 0419 else { 0420 // TODO 0421 return QString(); 0422 } 0423 } 0424 } 0425 } 0426 0427 QString BibliographyModel::beautifyLaTeX(const QString &input) { 0428 QString output = input; 0429 static const QStringList toBeRemoved {QStringLiteral("\\textsuperscript{"), QStringLiteral("\\}"), QStringLiteral("\\{"), QStringLiteral("}"), QStringLiteral("{")}; 0430 for (QStringList::ConstIterator it = toBeRemoved.constBegin(); it != toBeRemoved.constEnd(); ++it) 0431 output = output.remove(*it); 0432 0433 return output; 0434 }