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 }