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