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 }