File indexing completed on 2024-05-12 16:46:10
0001 /*************************************************************************** 0002 Copyright (C) 2008-2009 Robby Stephenson <robby@periapsis.org> 0003 ***************************************************************************/ 0004 0005 /*************************************************************************** 0006 * * 0007 * This program is free software; you can redistribute it and/or * 0008 * modify it under the terms of the GNU General Public License as * 0009 * published by the Free Software Foundation; either version 2 of * 0010 * the License or (at your option) version 3 or any later version * 0011 * accepted by the membership of KDE e.V. (or its successor approved * 0012 * by the membership of KDE e.V.), which shall act as a proxy * 0013 * defined in Section 14 of version 3 of the license. * 0014 * * 0015 * This program is distributed in the hope that it will be useful, * 0016 * but WITHOUT ANY WARRANTY; without even the implied warranty of * 0017 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * 0018 * GNU General Public License for more details. * 0019 * * 0020 * You should have received a copy of the GNU General Public License * 0021 * along with this program. If not, see <http://www.gnu.org/licenses/>. * 0022 * * 0023 ***************************************************************************/ 0024 0025 #include "entrymodel.h" 0026 #include "models.h" 0027 #include "../collection.h" 0028 #include "../collectionfactory.h" 0029 #include "../entry.h" 0030 #include "../field.h" 0031 #include "../document.h" 0032 #include "../images/image.h" 0033 #include "../images/imagefactory.h" 0034 #include "../tellico_debug.h" 0035 0036 namespace { 0037 static const int ENTRYMODEL_IMAGE_HEIGHT = 64; 0038 // number of entries in a list considered to be "small" in that 0039 // faster to do individual operations than model reset 0040 static const int SMALL_OPERATION_ENTRY_SIZE = 10; 0041 } 0042 0043 using namespace Tellico; 0044 using Tellico::EntryModel; 0045 0046 EntryModel::EntryModel(QObject* parent) : QAbstractItemModel(parent), 0047 m_imagesAreAvailable(false) { 0048 m_checkPix = QIcon::fromTheme(QStringLiteral("checkmark"), QIcon(QLatin1String(":/icons/checkmark"))); 0049 connect(ImageFactory::self(), &ImageFactory::imageAvailable, this, &EntryModel::refreshImage); 0050 } 0051 0052 EntryModel::~EntryModel() { 0053 } 0054 0055 int EntryModel::rowCount(const QModelIndex& index_) const { 0056 // valid indexes have no children/rows 0057 if(index_.isValid()) { 0058 return 0; 0059 } 0060 // even if entries are included, if there are no fields, then no rows either 0061 return m_fields.isEmpty() ? 0 : m_entries.count(); 0062 } 0063 0064 int EntryModel::columnCount(const QModelIndex& index_) const { 0065 // valid indexes have no columns 0066 if(index_.isValid()) { 0067 return 0; 0068 } 0069 return m_fields.count(); 0070 } 0071 0072 QModelIndex EntryModel::index(int row_, int column_, const QModelIndex& parent_) const { 0073 return hasIndex(row_, column_, parent_) ? createIndex(row_, column_) : QModelIndex(); 0074 } 0075 0076 QModelIndex EntryModel::parent(const QModelIndex&) const { 0077 return QModelIndex(); 0078 } 0079 0080 QVariant EntryModel::headerData(int section_, Qt::Orientation orientation_, int role_) const { 0081 if(section_ < 0 || section_ >= m_fields.count() || orientation_ != Qt::Horizontal) { 0082 return QVariant(); 0083 } 0084 switch(role_) { 0085 case Qt::DisplayRole: 0086 return m_fields.at(section_)->title(); 0087 0088 case FieldPtrRole: 0089 return QVariant::fromValue(m_fields.at(section_)); 0090 } 0091 return QVariant(); 0092 } 0093 0094 QVariant EntryModel::data(const QModelIndex& index_, int role_) const { 0095 if(!index_.isValid()) { 0096 return QVariant(); 0097 } 0098 0099 if(index_.row() >= rowCount()) { 0100 return QVariant(); 0101 } 0102 0103 Data::EntryPtr entry; 0104 Data::FieldPtr field; 0105 0106 QString value; 0107 0108 switch(role_) { 0109 case Qt::DisplayRole: 0110 case Qt::ToolTipRole: 0111 field = this->field(index_); 0112 if(!field || 0113 field->type() == Data::Field::Image || 0114 field->type() == Data::Field::Bool) { 0115 return QVariant(); 0116 } 0117 entry = this->entry(index_); 0118 if(!entry) { 0119 return QVariant(); 0120 } 0121 value = entry->formattedField(field); 0122 return value.isEmpty() ? QVariant() : value; 0123 0124 case Qt::DecorationRole: 0125 field = this->field(index_); 0126 if(!field) { 0127 return QVariant(); 0128 } 0129 entry = this->entry(index_); 0130 if(!entry) { 0131 return QVariant(); 0132 } 0133 0134 // just return the image for the entry 0135 // we don't need a formatted value for any pixmaps 0136 value = entry->field(field); 0137 if(value.isEmpty()) { 0138 return QVariant(); 0139 } 0140 0141 if(field->type() == Data::Field::Bool) { 0142 // assume any non-empty value equals true 0143 return m_checkPix; 0144 } 0145 0146 if(field->type() == Data::Field::Image) { 0147 // convert pixmap to icon 0148 QVariant v = requestImage(entry, value); 0149 if(!v.isNull() && v.canConvert<QPixmap>()) { 0150 return QIcon(v.value<QPixmap>()); 0151 } 0152 } 0153 return QVariant(); 0154 0155 case PrimaryImageRole: 0156 // return the primary image for the entry, no matter the index column 0157 entry = this->entry(index_); 0158 if(!entry) { 0159 return QVariant(); 0160 } 0161 field = entry->collection()->primaryImageField(); 0162 if(!field) { 0163 return QVariant(); 0164 } 0165 value = entry->field(field); 0166 if(value.isEmpty()) { 0167 return QVariant(); 0168 } 0169 return requestImage(entry, value); 0170 0171 case EntryPtrRole: 0172 entry = this->entry(index_); 0173 if(!entry) { 0174 return QVariant(); 0175 } 0176 return QVariant::fromValue(entry); 0177 0178 case FieldPtrRole: 0179 field = this->field(index_); 0180 if(!field) { 0181 return QVariant(); 0182 } 0183 return QVariant::fromValue(field); 0184 0185 case SaveStateRole: 0186 if(!m_saveStates.contains(index_.row())) { 0187 return NormalState; 0188 } 0189 return m_saveStates.value(index_.row()); 0190 0191 case Qt::TextAlignmentRole: 0192 field = this->field(index_); 0193 if(!field) { 0194 return QVariant(); 0195 } 0196 // special-case a few types to align center, default otherwise 0197 if(field->type() == Data::Field::Bool || 0198 field->type() == Data::Field::Number || 0199 field->type() == Data::Field::Image || 0200 field->type() == Data::Field::Rating) { 0201 return Qt::AlignCenter; 0202 } 0203 return QVariant(); 0204 0205 case Qt::SizeHintRole: 0206 field = this->field(index_); 0207 if(field && field->type() == Data::Field::Image) { 0208 return QSize(0, ENTRYMODEL_IMAGE_HEIGHT+4); 0209 } 0210 return QVariant(); 0211 } 0212 return QVariant(); 0213 } 0214 0215 QModelIndex EntryModel::indexFromEntry(Data::EntryPtr entry_) const { 0216 const int idx = m_entries.indexOf(entry_); 0217 if(idx == -1) { 0218 return QModelIndex(); 0219 } 0220 return createIndex(idx, 0); 0221 } 0222 0223 Tellico::Data::EntryPtr EntryModel::entry(const QModelIndex& index_) const { 0224 Q_ASSERT(index_.isValid()); 0225 Data::EntryPtr entry; 0226 if(index_.isValid() && index_.row() < m_entries.count()) { 0227 entry = m_entries.at(index_.row()); 0228 } 0229 return entry; 0230 } 0231 0232 Tellico::Data::FieldPtr EntryModel::field(const QModelIndex& index_) const { 0233 Q_ASSERT(index_.isValid()); 0234 Q_ASSERT(index_.column() < m_fields.count()); 0235 0236 Data::FieldPtr field; 0237 if(index_.isValid() && index_.column() < m_fields.count()) { 0238 field = m_fields.at(index_.column()); 0239 } 0240 return field; 0241 } 0242 0243 bool EntryModel::setData(const QModelIndex& index_, const QVariant& value_, int role_) { 0244 if(!index_.isValid() || role_ != SaveStateRole) { 0245 return false; 0246 } 0247 const int state = value_.toInt(); 0248 if(state == NormalState) { 0249 m_saveStates.remove(index_.row()); 0250 } else { 0251 Q_ASSERT(state == NewState || state == ModifiedState); 0252 m_saveStates.insert(index_.row(), value_.toInt()); 0253 } 0254 emit dataChanged(index_, index_); 0255 return true; 0256 } 0257 0258 void EntryModel::clear() { 0259 beginResetModel(); 0260 m_entries.clear(); 0261 m_fields.clear(); 0262 m_saveStates.clear(); 0263 endResetModel(); 0264 } 0265 0266 void EntryModel::clearSaveState() { 0267 // if there are many save states to be toggled, do a full model reset 0268 if(m_saveStates.size() > SMALL_OPERATION_ENTRY_SIZE) { 0269 beginResetModel(); 0270 m_saveStates.clear(); 0271 endResetModel(); 0272 } else { 0273 QHashIterator<int, int> i(m_saveStates); 0274 while(i.hasNext()) { 0275 i.next(); 0276 // If the hash is modified while a QHashIterator is active, the QHashIterator 0277 // will continue iterating over the original hash, ignoring the modified copy. 0278 m_saveStates.remove(i.key()); 0279 QModelIndex idx = createIndex(i.key(), 0); 0280 emit dataChanged(idx, idx, QVector<int>() << SaveStateRole); 0281 } 0282 } 0283 } 0284 0285 void EntryModel::setEntries(const Tellico::Data::EntryList& entries_) { 0286 // should never have entries without having fields first 0287 Q_ASSERT(!m_fields.isEmpty() || entries_.isEmpty()); 0288 beginResetModel(); 0289 m_entries = entries_; 0290 endResetModel(); 0291 } 0292 0293 void EntryModel::addEntries(const Tellico::Data::EntryList& entries_) { 0294 beginInsertRows(QModelIndex(), m_entries.count(), m_entries.count() + entries_.count() - 1); 0295 m_entries += entries_; 0296 endInsertRows(); 0297 } 0298 0299 void EntryModel::modifyEntries(const Tellico::Data::EntryList& entries_) { 0300 foreach(Data::EntryPtr entry, entries_) { 0301 QModelIndex index = indexFromEntry(entry); 0302 if(index.isValid()) { 0303 emit dataChanged(index, index); 0304 } 0305 } 0306 } 0307 0308 void EntryModel::removeEntries(const Tellico::Data::EntryList& entries_) { 0309 // for performance reasons, if more than 10 entries are being removed, rather than 0310 // iterating over all of them, which really hurts, just signal a full replacement 0311 const bool bigRemoval = (entries_.size() > SMALL_OPERATION_ENTRY_SIZE); 0312 if(bigRemoval) { 0313 beginResetModel(); 0314 } 0315 foreach(Data::EntryPtr entry, entries_) { 0316 int idx = m_entries.indexOf(entry); 0317 if(idx > -1) { 0318 if(!bigRemoval) { 0319 beginRemoveRows(QModelIndex(), idx, idx); 0320 } 0321 m_entries.removeAt(idx); 0322 if(!bigRemoval) { 0323 endRemoveRows(); 0324 } 0325 } 0326 } 0327 if(bigRemoval) { 0328 endResetModel(); 0329 } 0330 } 0331 0332 void EntryModel::setFields(const Tellico::Data::FieldList& fields_) { 0333 // if fields are being replaced, it's a full model reset 0334 // if not, it's just adding columns 0335 if(!m_fields.isEmpty()) { 0336 beginResetModel(); 0337 m_fields = fields_; 0338 endResetModel(); 0339 } else if(!fields_.isEmpty()) { 0340 beginInsertColumns(QModelIndex(), 0, fields_.size()-1); 0341 m_fields = fields_; 0342 endInsertColumns(); 0343 } 0344 } 0345 0346 void EntryModel::reorderFields(const Tellico::Data::FieldList& fields_) { 0347 emit layoutAboutToBeChanged(); 0348 // update the persistent model indexes by building list of old index 0349 // and new if the columns are moved 0350 QModelIndexList oldPersistentList = persistentIndexList(); 0351 QModelIndexList fromList, toList; 0352 for(int i = 0; i < m_fields.count(); ++i) { 0353 const int j = fields_.indexOf(m_fields.at(i)); 0354 Q_ASSERT(j >= 0); 0355 // old add the model index list if the columns are different 0356 if(i != j) { 0357 foreach(QModelIndex oldIndex, oldPersistentList) { 0358 if(oldIndex.column() == i) { 0359 fromList += oldIndex; 0360 toList += createIndex(oldIndex.row(), j); 0361 } 0362 } 0363 } 0364 } 0365 0366 m_fields = fields_; 0367 0368 changePersistentIndexList(fromList, toList); 0369 emit layoutChanged(); 0370 } 0371 0372 void EntryModel::addFields(const Tellico::Data::FieldList& fields_) { 0373 if(!fields_.isEmpty()) { 0374 beginInsertColumns(QModelIndex(), m_fields.size(), m_fields.size() + fields_.size()-1); 0375 m_fields += fields_; 0376 endInsertColumns(); 0377 } 0378 } 0379 0380 void EntryModel::modifyField(Data::FieldPtr oldField_, Data::FieldPtr newField_) { 0381 for(int i = 0; i < m_fields.count(); ++i) { 0382 if(m_fields.at(i)->name() == oldField_->name()) { 0383 m_fields.replace(i, newField_); 0384 emit headerDataChanged(Qt::Horizontal, i, i); 0385 break; 0386 } 0387 } 0388 } 0389 0390 void EntryModel::removeFields(const Tellico::Data::FieldList& fields_) { 0391 foreach(Data::FieldPtr field, fields_) { 0392 int idx = m_fields.indexOf(field); 0393 if(idx > -1) { 0394 beginRemoveColumns(QModelIndex(), idx, idx); 0395 m_fields.removeAt(idx); 0396 endRemoveColumns(); 0397 } 0398 } 0399 } 0400 0401 void EntryModel::setImagesAreAvailable(bool available_) { 0402 if(m_imagesAreAvailable != available_) { 0403 beginResetModel(); 0404 m_imagesAreAvailable = available_; 0405 endResetModel(); 0406 } 0407 } 0408 0409 QVariant EntryModel::requestImage(Data::EntryPtr entry_, const QString& id_) const { 0410 if(!m_imagesAreAvailable) { 0411 return QVariant(); 0412 } 0413 // if it's not a local image, request that it be downloaded 0414 if(ImageFactory::hasLocalImage(id_)) { 0415 const Data::Image& img = ImageFactory::imageById(id_); 0416 if(!img.isNull()) { 0417 return img.convertToPixmap(); 0418 } 0419 } else if(!m_requestedImages.contains(id_, entry_)) { 0420 m_requestedImages.insert(id_, entry_); 0421 ImageFactory::requestImageById(id_); 0422 } 0423 return QVariant(); 0424 } 0425 0426 void EntryModel::refreshImage(const QString& id_) { 0427 QMultiHash<QString, Data::EntryPtr>::iterator i = m_requestedImages.find(id_); 0428 while(i != m_requestedImages.end() && i.key() == id_) { 0429 QModelIndex index = indexFromEntry(i.value()); 0430 emit dataChanged(index, index); 0431 ++i; 0432 } 0433 m_requestedImages.remove(id_); 0434 }