File indexing completed on 2024-11-24 04:34:13

0001 /***************************************************************************
0002  *   SPDX-License-Identifier: GPL-2.0-or-later
0003  *                                                                         *
0004  *   SPDX-FileCopyrightText: 2004-2023 Thomas Fischer <fischer@unix-ag.uni-kl.de>
0005  *                                                                         *
0006  *   This program is free software; you can redistribute it and/or modify  *
0007  *   it under the terms of the GNU General Public License as published by  *
0008  *   the Free Software Foundation; either version 2 of the License, or     *
0009  *   (at your option) any later version.                                   *
0010  *                                                                         *
0011  *   This program is distributed in the hope that it will be useful,       *
0012  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
0013  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
0014  *   GNU General Public License for more details.                          *
0015  *                                                                         *
0016  *   You should have received a copy of the GNU General Public License     *
0017  *   along with this program; if not, see <https://www.gnu.org/licenses/>. *
0018  ***************************************************************************/
0019 
0020 #include "filemodel.h"
0021 
0022 #include <algorithm>
0023 
0024 #include <QColor>
0025 #include <QFile>
0026 #include <QString>
0027 
0028 #ifdef HAVE_KFI18N
0029 #include <KLocalizedString>
0030 #else // HAVE_KFI18N
0031 #include <QObject>
0032 #define i18n(text) QObject::tr(text)
0033 #endif // HAVE_KFI18N
0034 
0035 #include <BibTeXEntries>
0036 #include <BibTeXFields>
0037 #include <Preferences>
0038 #include "file.h"
0039 #include "entry.h"
0040 #include "macro.h"
0041 #include "comment.h"
0042 #include "preamble.h"
0043 
0044 const int FileModel::NumberRole = Qt::UserRole + 9581;
0045 const int FileModel::SortRole = Qt::UserRole + 236; /// see also MDIWidget's SortRole
0046 
0047 
0048 FileModel::FileModel(QObject *parent)
0049         : QAbstractTableModel(parent), m_file(nullptr)
0050 {
0051     NotificationHub::registerNotificationListener(this);
0052     readConfiguration();
0053 }
0054 
0055 void FileModel::notificationEvent(int eventId)
0056 {
0057     if (eventId == NotificationHub::EventConfigurationChanged) {
0058         readConfiguration();
0059         int column = 0;
0060         for (const auto &fd : const_cast<const BibTeXFields &>(BibTeXFields::instance())) {
0061             /// Colors may have changed
0062             bool columnChanged = fd.upperCamelCase.toLower() == Entry::ftColor;
0063             /// Person name formatting may has changed
0064             columnChanged |= fd.upperCamelCase.toLower() == Entry::ftAuthor || fd.upperCamelCase.toLower() == Entry::ftEditor;
0065             columnChanged |= fd.upperCamelCaseAlt.toLower() == Entry::ftAuthor || fd.upperCamelCaseAlt.toLower() == Entry::ftEditor;
0066             /// Changes necessary for this column? Publish update
0067             if (columnChanged)
0068                 Q_EMIT dataChanged(index(0, column), index(rowCount() - 1, column));
0069             ++column;
0070         }
0071     } else if (eventId == NotificationHub::EventBibliographySystemChanged) {
0072         beginResetModel();
0073         endResetModel();
0074         Q_EMIT bibliographySystemChanged();
0075     }
0076 }
0077 
0078 void FileModel::readConfiguration()
0079 {
0080     colorToLabel.clear();
0081     for (QVector<QPair<QString, QString>>::ConstIterator it = Preferences::instance().colorCodes().constBegin(); it != Preferences::instance().colorCodes().constEnd(); ++it)
0082         colorToLabel.insert(it->first, it->second);
0083 }
0084 
0085 QString FileModel::leftSqueezeText(const QString &text, int n)
0086 {
0087     return text.length() <= n ? text : text.left(n) + QStringLiteral("...");
0088 }
0089 
0090 QString FileModel::entryText(const Entry *entry, const QString &raw, const QString &rawAlt, const QStringList &rawAliases, int role, bool followCrossRef) const
0091 {
0092     if (role != Qt::DisplayRole && role != Qt::ToolTipRole && role != SortRole)
0093         return QString();
0094 
0095     if (raw == QStringLiteral("^id")) {
0096         return entry->id();
0097     } else if (raw == QStringLiteral("^type")) { // FIXME: Use constant here?
0098         /// Try to beautify type, e.g. translate "proceedings" into
0099         /// "Conference or Workshop Proceedings"
0100         const QString label = BibTeXEntries::instance().label(entry->type());
0101         if (label.isEmpty()) {
0102             /// Fall-back to entry type as it is
0103             return entry->type();
0104         } else
0105             return label;
0106     } else if (raw.toLower() == Entry::ftStarRating) {
0107         return QString(); /// Stars have no string representation
0108     } else if (raw.toLower() == Entry::ftColor) {
0109         const QString text = PlainTextValue::text(entry->value(raw));
0110         if (text.isEmpty()) return QString();
0111         const QString colorText = colorToLabel[text];
0112         if (colorText.isEmpty()) return text;
0113         return colorText;
0114     } else {
0115         QString text;
0116         if (entry->contains(raw))
0117             text = PlainTextValue::text(entry->value(raw), raw.toLower() == Entry::ftMonth ? PlainTextValue::BeautifyMonth : PlainTextValue::NoOptions).simplified();
0118         else if (!rawAlt.isEmpty() && entry->contains(rawAlt))
0119             text = PlainTextValue::text(entry->value(rawAlt), rawAlt.toLower() == Entry::ftMonth ? PlainTextValue::BeautifyMonth : PlainTextValue::NoOptions).simplified();
0120         if (text.isEmpty())
0121             for (const QString &alias : rawAliases) {
0122                 if (entry->contains(alias)) {
0123                     text = PlainTextValue::text(entry->value(alias), alias.toLower() == Entry::ftMonth ? PlainTextValue::BeautifyMonth : PlainTextValue::NoOptions).simplified();
0124                     if (!text.isEmpty()) break;
0125                 }
0126             }
0127 
0128         if (followCrossRef && text.isEmpty() && entry->contains(Entry::ftCrossRef)) {
0129             const QSharedPointer<const Entry> completedEntry(entry->resolveCrossref(m_file));
0130             return entryText(completedEntry.data(), raw, rawAlt, rawAliases, role, false);
0131         }
0132 
0133         if (text.isEmpty())
0134             return QString();
0135         else if (role == FileModel::SortRole)
0136             return text.toLower();
0137         else if (role == Qt::ToolTipRole) {
0138             // TODO: find a better solution, such as line-wrapping tooltips
0139             return leftSqueezeText(text, 128);
0140         } else
0141             return text;
0142     }
0143 
0144 }
0145 
0146 File *FileModel::bibliographyFile() const
0147 {
0148     return m_file;
0149 }
0150 
0151 void FileModel::setBibliographyFile(File *file)
0152 {
0153     bool resetNecessary = m_file != file;
0154     if (resetNecessary) {
0155         beginResetModel();
0156         m_file = file;
0157         endResetModel();
0158     }
0159 }
0160 
0161 QModelIndex FileModel::parent(const QModelIndex &index) const
0162 {
0163     Q_UNUSED(index)
0164     return QModelIndex();
0165 }
0166 
0167 bool FileModel::hasChildren(const QModelIndex &parent) const
0168 {
0169     return parent == QModelIndex();
0170 }
0171 
0172 int FileModel::rowCount(const QModelIndex &parent) const
0173 {
0174     return parent == QModelIndex() && m_file != nullptr ? m_file->count() : 0;
0175 }
0176 
0177 int FileModel::columnCount(const QModelIndex &parent) const
0178 {
0179     return parent == QModelIndex() ? BibTeXFields::instance().count() : 0;
0180 }
0181 
0182 QVariant FileModel::data(const QModelIndex &index, int role) const
0183 {
0184     /// do not accept invalid indices
0185     if (!index.isValid())
0186         return QVariant();
0187 
0188     /// check backend storage (File object)
0189     if (m_file == nullptr)
0190         return QVariant();
0191 
0192     /// for now, only display data (no editing or icons etc)
0193     if (role != NumberRole && role != SortRole && role != Qt::DisplayRole && role != Qt::ToolTipRole && role != Qt::BackgroundRole && role != Qt::ForegroundRole)
0194         return QVariant();
0195 
0196     if (index.row() < m_file->count() && index.column() < BibTeXFields::instance().count()) {
0197         const FieldDescription &fd = BibTeXFields::instance().at(index.column());
0198         const QString &raw = fd.upperCamelCase;
0199         const QString &rawAlt = fd.upperCamelCaseAlt;
0200         const QStringList &rawAliases = fd.upperCamelCaseAliases;
0201         QSharedPointer<Element> element = (*m_file)[index.row()];
0202         QSharedPointer<Entry> entry = element.dynamicCast<Entry>();
0203 
0204         /// if BibTeX entry has a "x-color" field, use that color to highlight row
0205         if (role == Qt::BackgroundRole) {
0206             /// Retrieve "color"
0207             QString colorName;
0208             if (entry.isNull() || (colorName = PlainTextValue::text(entry->value(Entry::ftColor))) == QStringLiteral("#000000") || colorName.isEmpty())
0209                 return QVariant();
0210             else {
0211                 /// There is a valid color, set it as background
0212                 QColor color(colorName);
0213                 /// Use slightly different colors for even and odd rows
0214                 color.setAlphaF(index.row() % 2 == 0 ? 0.75 : 1.0);
0215                 return QVariant(color);
0216             }
0217         } else if (role == Qt::ForegroundRole) {
0218             /// Retrieve "color"
0219             QString colorName;
0220             if (entry.isNull() || (colorName = PlainTextValue::text(entry->value(Entry::ftColor))) == QStringLiteral("#000000") || colorName.isEmpty())
0221                 return QVariant();
0222             else {
0223                 /// There is a valid color ...
0224                 const QColor color(colorName);
0225                 /// Retrieve red, green, blue, and alpha components
0226                 int r = 0, g = 0, b = 0, a = 0;
0227                 color.getRgb(&r, &g, &b, &a);
0228                 /// If gray value is rather dark, return white as foreground color
0229                 if (qGray(r, g, b) < 128) return QColor(Qt::white);
0230                 /// For light gray values, return black as foreground color
0231                 else return QColor(Qt::black);
0232             }
0233         } else if (role == NumberRole) {
0234             if (!entry.isNull() && raw.toLower() == Entry::ftStarRating) {
0235                 const QString text = PlainTextValue::text(entry->value(raw)).simplified();
0236                 bool ok = false;
0237                 const double numValue = text.toDouble(&ok);
0238                 if (ok)
0239                     return QVariant::fromValue<double>(numValue);
0240                 else
0241                     return QVariant();
0242             } else
0243                 return QVariant();
0244         }
0245 
0246         /// The only roles left at this point shall be SortRole, Qt::DisplayRole, and Qt::ToolTipRole
0247 
0248         if (!entry.isNull()) {
0249             return QVariant(entryText(entry.data(), raw, rawAlt, rawAliases, role, true));
0250         } else {
0251             QSharedPointer<Macro> macro = element.dynamicCast<Macro>();
0252             if (!macro.isNull()) {
0253                 if (raw == QStringLiteral("^id"))
0254                     return QVariant(macro->key());
0255                 else if (raw == QStringLiteral("^type"))
0256                     return QVariant(i18n("Macro"));
0257                 else if (raw == QStringLiteral("Title")) {
0258                     const QString text = PlainTextValue::text(macro->value()).simplified();
0259                     return QVariant(text);
0260                 } else
0261                     return QVariant();
0262             } else {
0263                 QSharedPointer<Comment> comment = element.dynamicCast<Comment>();
0264                 if (!comment.isNull()) {
0265                     if (raw == QStringLiteral("^type"))
0266                         return QVariant(i18n("Comment"));
0267                     else if (raw == Entry::ftTitle) {
0268                         const QString text = comment->text().simplified();
0269                         return QVariant(text);
0270                     } else
0271                         return QVariant();
0272                 } else {
0273                     QSharedPointer<Preamble> preamble = element.dynamicCast<Preamble>();
0274                     if (!preamble.isNull()) {
0275                         if (raw == QStringLiteral("^type"))
0276                             return QVariant(i18n("Preamble"));
0277                         else if (raw == Entry::ftTitle) {
0278                             const QString text = PlainTextValue::text(preamble->value()).simplified();
0279                             return QVariant(text);
0280                         } else
0281                             return QVariant();
0282                     } else
0283                         return QVariant(QStringLiteral("?"));
0284                 }
0285             }
0286         }
0287     } else
0288         return QVariant(QStringLiteral("?"));
0289 }
0290 
0291 QVariant FileModel::headerData(int section, Qt::Orientation orientation, int role) const
0292 {
0293     if (role != Qt::DisplayRole || orientation != Qt::Horizontal || section < 0 || section >= BibTeXFields::instance().count())
0294         return QVariant();
0295     return BibTeXFields::instance().at(section).label;
0296 }
0297 
0298 Qt::ItemFlags FileModel::flags(const QModelIndex &index) const
0299 {
0300     Q_UNUSED(index)
0301     return Qt::ItemIsEnabled | Qt::ItemIsSelectable; // FIXME: What about drag'n'drop?
0302 }
0303 
0304 void FileModel::clear() {
0305     beginResetModel();
0306     m_file->clear();
0307     endResetModel();
0308 }
0309 
0310 bool FileModel::removeRow(int row, const QModelIndex &parent)
0311 {
0312     if (row < 0 || m_file == nullptr || row >= rowCount() || row >= m_file->count())
0313         return false;
0314     if (parent != QModelIndex())
0315         return false;
0316 
0317     beginRemoveRows(QModelIndex(), row, row);
0318     m_file->removeAt(row);
0319     endRemoveRows();
0320 
0321     return true;
0322 }
0323 
0324 bool FileModel::removeRowList(const QList<int> &rows)
0325 {
0326     if (m_file == nullptr) return false;
0327 
0328     QList<int> internalRows = rows;
0329     std::sort(internalRows.begin(), internalRows.end(), std::greater<int>());
0330 
0331     beginRemoveRows(QModelIndex(), internalRows.last(), internalRows.first());
0332     for (int row : const_cast<const QList<int> &>(internalRows)) {
0333         if (row < 0 || row >= rowCount() || row >= m_file->count())
0334             return false;
0335         m_file->removeAt(row);
0336     }
0337     endRemoveRows();
0338 
0339     return true;
0340 }
0341 
0342 bool FileModel::insertRow(QSharedPointer<Element> element, int row, const QModelIndex &parent)
0343 {
0344     if (m_file == nullptr || row < 0 || row > rowCount() || parent != QModelIndex())
0345         return false;
0346 
0347     /// Check for duplicate ids or keys when inserting a new element
0348     /// First, check entries
0349     QSharedPointer<Entry> entry = element.dynamicCast<Entry>();
0350     if (!entry.isNull()) {
0351         /// Fetch current entry's id
0352         const QString id = entry->id();
0353         if (!m_file->containsKey(id).isNull()) {
0354             /// Same entry id used for an existing entry or macro
0355             int overflow = 2;
0356             static const QString pattern = QStringLiteral("%1_%2");
0357             /// Test alternative ids with increasing "overflow" counter:
0358             /// id_2, id_3, id_4 ,...
0359             QString newId = pattern.arg(id).arg(overflow);
0360             while (!m_file->containsKey(newId).isNull()) {
0361                 ++overflow;
0362                 newId = pattern.arg(id).arg(overflow);
0363             }
0364             /// Guaranteed to find an alternative, apply it to entry
0365             entry->setId(newId);
0366         }
0367     } else {
0368         /// Next, check macros
0369         QSharedPointer<Macro> macro = element.dynamicCast<Macro>();
0370         if (!macro.isNull()) {
0371             /// Fetch current macro's key
0372             const QString key = macro->key();
0373             if (!m_file->containsKey(key).isNull()) {
0374                 /// Same entry key used for an existing entry or macro
0375                 int overflow = 2;
0376                 static const QString pattern = QStringLiteral("%1_%2");
0377                 /// Test alternative keys with increasing "overflow" counter:
0378                 /// key_2, key_3, key_4 ,...
0379                 QString newKey = pattern.arg(key).arg(overflow);
0380                 while (!m_file->containsKey(newKey).isNull()) {
0381                     ++overflow;
0382                     newKey = pattern.arg(key).arg(overflow);
0383                 }
0384                 /// Guaranteed to find an alternative, apply it to macro
0385                 macro->setKey(newKey);
0386             }
0387         }
0388     }
0389 
0390     beginInsertRows(QModelIndex(), row, row);
0391     m_file->insert(row, element);
0392     endInsertRows();
0393 
0394     return true;
0395 }
0396 
0397 QSharedPointer<Element> FileModel::element(int row) const
0398 {
0399     if (m_file == nullptr || row < 0 || row >= m_file->count()) return QSharedPointer<Element>();
0400 
0401     return (*m_file)[row];
0402 }
0403 
0404 int FileModel::row(QSharedPointer<Element> element) const
0405 {
0406     if (m_file == nullptr) return -1;
0407     return m_file->indexOf(element);
0408 }
0409 
0410 void FileModel::elementChanged(int row) {
0411     Q_EMIT dataChanged(createIndex(row, 0), createIndex(row, columnCount() - 1));
0412 }