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 }