File indexing completed on 2024-09-22 04:41:03

0001 /*
0002     SPDX-FileCopyrightText: 2009 Kevin Ottens <ervin@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "favoritecollectionsmodel.h"
0008 #include "akonadicore_debug.h"
0009 
0010 #include <QItemSelectionModel>
0011 #include <QMimeData>
0012 
0013 #include <KConfig>
0014 #include <KConfigGroup>
0015 #include <KJob>
0016 #include <KLocalizedString>
0017 #include <QUrl>
0018 
0019 #include "collectionmodifyjob.h"
0020 #include "entitytreemodel.h"
0021 #include "favoritecollectionattribute.h"
0022 #include "mimetypechecker.h"
0023 #include "pastehelper_p.h"
0024 
0025 using namespace Akonadi;
0026 
0027 /**
0028  * @internal
0029  */
0030 class Akonadi::FavoriteCollectionsModelPrivate
0031 {
0032 public:
0033     FavoriteCollectionsModelPrivate(const KConfigGroup &group, FavoriteCollectionsModel *parent)
0034         : q(parent)
0035         , configGroup(group)
0036     {
0037     }
0038 
0039     QString labelForCollection(Collection::Id collectionId) const
0040     {
0041         if (labelMap.contains(collectionId)) {
0042             return labelMap[collectionId];
0043         }
0044 
0045         return q->defaultFavoriteLabel(Collection{collectionId});
0046     }
0047 
0048     void insertIfAvailable(Collection::Id col)
0049     {
0050         if (collectionIds.contains(col)) {
0051             select(col);
0052             if (!referencedCollections.contains(col)) {
0053                 reference(col);
0054             }
0055             auto idx = EntityTreeModel::modelIndexForCollection(q, Collection{col});
0056             if (idx.isValid()) {
0057                 auto c = q->data(idx, EntityTreeModel::CollectionRole).value<Collection>();
0058                 if (c.isValid() && !c.hasAttribute<FavoriteCollectionAttribute>()) {
0059                     c.addAttribute(new FavoriteCollectionAttribute());
0060                     new CollectionModifyJob(c, q);
0061                 }
0062             }
0063         }
0064     }
0065 
0066     void insertIfAvailable(const QModelIndex &idx)
0067     {
0068         insertIfAvailable(idx.data(EntityTreeModel::CollectionIdRole).value<Collection::Id>());
0069     }
0070 
0071     /**
0072      * Stuff changed (e.g. new rows inserted into sorted model), reload everything.
0073      */
0074     void reload()
0075     {
0076         // don't clear the selection model here. Otherwise we mess up the users selection as collections get removed and re-inserted.
0077         for (const Collection::Id &collectionId : std::as_const(collectionIds)) {
0078             insertIfAvailable(collectionId);
0079         }
0080         // If a favorite folder was removed then surely it's gone from the selection model, so no need to do anything about that.
0081     }
0082 
0083     void rowsInserted(const QModelIndex &parent, int begin, int end)
0084     {
0085         for (int row = begin; row <= end; row++) {
0086             const QModelIndex child = q->sourceModel()->index(row, 0, parent);
0087             if (!child.isValid()) {
0088                 continue;
0089             }
0090             insertIfAvailable(child);
0091             const int childRows = q->sourceModel()->rowCount(child);
0092             if (childRows > 0) {
0093                 rowsInserted(child, 0, childRows - 1);
0094             }
0095         }
0096     }
0097 
0098     void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight)
0099     {
0100         for (int row = topLeft.row(); row <= bottomRight.row(); row++) {
0101             const QModelIndex idx = topLeft.sibling(row, 0);
0102             insertIfAvailable(idx);
0103         }
0104     }
0105 
0106     /**
0107      *  Selects the index in the internal selection model to make the collection visible in the model
0108      */
0109     void select(Collection::Id collectionId)
0110     {
0111         const QModelIndex index = EntityTreeModel::modelIndexForCollection(q->sourceModel(), Collection(collectionId));
0112         if (index.isValid()) {
0113             q->selectionModel()->select(index, QItemSelectionModel::Select);
0114         }
0115     }
0116 
0117     void deselect(Collection::Id collectionId)
0118     {
0119         const QModelIndex idx = EntityTreeModel::modelIndexForCollection(q->sourceModel(), Collection(collectionId));
0120         if (idx.isValid()) {
0121             q->selectionModel()->select(idx, QItemSelectionModel::Deselect);
0122         }
0123     }
0124 
0125     void reference(Collection::Id collectionId)
0126     {
0127         if (referencedCollections.contains(collectionId)) {
0128             qCWarning(AKONADICORE_LOG) << "already referenced " << collectionId;
0129             return;
0130         }
0131         const QModelIndex index = EntityTreeModel::modelIndexForCollection(q->sourceModel(), Collection(collectionId));
0132         if (index.isValid()) {
0133             if (q->sourceModel()->setData(index, QVariant(), EntityTreeModel::CollectionRefRole)) {
0134                 referencedCollections << collectionId;
0135             } else {
0136                 qCWarning(AKONADICORE_LOG) << "failed to reference collection";
0137             }
0138             q->sourceModel()->fetchMore(index);
0139         }
0140     }
0141 
0142     void dereference(Collection::Id collectionId)
0143     {
0144         if (!referencedCollections.contains(collectionId)) {
0145             qCWarning(AKONADICORE_LOG) << "not referenced " << collectionId;
0146             return;
0147         }
0148         const QModelIndex index = EntityTreeModel::modelIndexForCollection(q->sourceModel(), Collection(collectionId));
0149         if (index.isValid()) {
0150             q->sourceModel()->setData(index, QVariant(), EntityTreeModel::CollectionDerefRole);
0151             referencedCollections.remove(collectionId);
0152         }
0153     }
0154 
0155     /**
0156      * Adds a collection to the favorite collections
0157      */
0158     void add(Collection::Id collectionId)
0159     {
0160         if (collectionIds.contains(collectionId)) {
0161             qCDebug(AKONADICORE_LOG) << "already in model " << collectionId;
0162             return;
0163         }
0164         collectionIds << collectionId;
0165         reference(collectionId);
0166         select(collectionId);
0167         const auto idx = EntityTreeModel::modelIndexForCollection(q, Collection{collectionId});
0168         if (idx.isValid()) {
0169             auto col = q->data(idx, EntityTreeModel::CollectionRole).value<Collection>();
0170             if (col.isValid() && !col.hasAttribute<FavoriteCollectionAttribute>()) {
0171                 col.addAttribute(new FavoriteCollectionAttribute());
0172                 new CollectionModifyJob(col, q);
0173             }
0174         }
0175     }
0176 
0177     void remove(Collection::Id collectionId)
0178     {
0179         collectionIds.removeAll(collectionId);
0180         labelMap.remove(collectionId);
0181         dereference(collectionId);
0182         deselect(collectionId);
0183         const auto idx = EntityTreeModel::modelIndexForCollection(q, Collection{collectionId});
0184         if (idx.isValid()) {
0185             auto col = q->data(idx, EntityTreeModel::CollectionRole).value<Collection>();
0186             if (col.isValid() && col.hasAttribute<FavoriteCollectionAttribute>()) {
0187                 col.removeAttribute<FavoriteCollectionAttribute>();
0188                 new CollectionModifyJob(col, q);
0189             }
0190         }
0191     }
0192 
0193     void set(const QList<Collection::Id> &collections)
0194     {
0195         QList<Collection::Id> colIds = collectionIds;
0196         for (const Collection::Id &col : collections) {
0197             const int removed = colIds.removeAll(col);
0198             const bool isNewCollection = removed <= 0;
0199             if (isNewCollection) {
0200                 add(col);
0201             }
0202         }
0203         // Remove what's left
0204         for (Akonadi::Collection::Id colId : std::as_const(colIds)) {
0205             remove(colId);
0206         }
0207     }
0208 
0209     void set(const Akonadi::Collection::List &collections)
0210     {
0211         QList<Akonadi::Collection::Id> colIds;
0212         colIds.reserve(collections.count());
0213         for (const Akonadi::Collection &col : collections) {
0214             colIds << col.id();
0215         }
0216         set(colIds);
0217     }
0218 
0219     void loadConfig()
0220     {
0221         const QList<Collection::Id> collections = configGroup.readEntry("FavoriteCollectionIds", QList<qint64>());
0222         const QStringList labels = configGroup.readEntry("FavoriteCollectionLabels", QStringList());
0223         const int numberOfLabels(labels.size());
0224         for (int i = 0; i < collections.size(); ++i) {
0225             if (i < numberOfLabels) {
0226                 labelMap[collections[i]] = labels[i];
0227             }
0228             add(collections[i]);
0229         }
0230     }
0231 
0232     void saveConfig()
0233     {
0234         QStringList labels;
0235         labels.reserve(collectionIds.count());
0236         for (const Collection::Id &collectionId : std::as_const(collectionIds)) {
0237             labels << labelForCollection(collectionId);
0238         }
0239 
0240         configGroup.writeEntry("FavoriteCollectionIds", collectionIds);
0241         configGroup.writeEntry("FavoriteCollectionLabels", labels);
0242         configGroup.config()->sync();
0243     }
0244 
0245     FavoriteCollectionsModel *const q;
0246 
0247     QList<Collection::Id> collectionIds;
0248     QSet<Collection::Id> referencedCollections;
0249     QHash<qint64, QString> labelMap;
0250     KConfigGroup configGroup;
0251 };
0252 
0253 /* Implementation note:
0254  *
0255  * We use KSelectionProxyModel in order to make a flat list of selected folders from the folder tree.
0256  *
0257  * Attempts to use QSortFilterProxyModel make code somewhat simpler,
0258  * but don't work since we then get a filtered tree, not a flat list. Stacking a KDescendantsProxyModel
0259  * on top would likely remove explicitly selected parents when one of their child is selected too.
0260  */
0261 
0262 FavoriteCollectionsModel::FavoriteCollectionsModel(QAbstractItemModel *source, const KConfigGroup &group, QObject *parent)
0263     : KSelectionProxyModel(new QItemSelectionModel(source, parent), parent)
0264     , d(new FavoriteCollectionsModelPrivate(group, this))
0265 {
0266     setSourceModel(source);
0267     setFilterBehavior(ExactSelection);
0268 
0269     d->loadConfig();
0270     // React to various changes in the source model
0271     connect(source, &QAbstractItemModel::modelReset, this, [this]() {
0272         d->reload();
0273     });
0274     connect(source, &QAbstractItemModel::layoutChanged, this, [this]() {
0275         d->reload();
0276     });
0277     connect(source, &QAbstractItemModel::rowsInserted, this, [this](const QModelIndex &parent, int begin, int end) {
0278         d->rowsInserted(parent, begin, end);
0279     });
0280     connect(source, &QAbstractItemModel::dataChanged, this, [this](const QModelIndex &tl, const QModelIndex &br) {
0281         d->dataChanged(tl, br);
0282     });
0283 }
0284 
0285 FavoriteCollectionsModel::~FavoriteCollectionsModel() = default;
0286 
0287 void FavoriteCollectionsModel::setCollections(const Collection::List &collections)
0288 {
0289     d->set(collections);
0290     d->saveConfig();
0291 }
0292 
0293 void FavoriteCollectionsModel::addCollection(const Collection &collection)
0294 {
0295     d->add(collection.id());
0296     d->saveConfig();
0297 }
0298 
0299 void FavoriteCollectionsModel::removeCollection(const Collection &collection)
0300 {
0301     d->remove(collection.id());
0302     d->saveConfig();
0303 }
0304 
0305 Akonadi::Collection::List FavoriteCollectionsModel::collections() const
0306 {
0307     Collection::List cols;
0308     cols.reserve(d->collectionIds.count());
0309     for (const Collection::Id &colId : std::as_const(d->collectionIds)) {
0310         const QModelIndex idx = EntityTreeModel::modelIndexForCollection(sourceModel(), Collection(colId));
0311         const auto collection = sourceModel()->data(idx, EntityTreeModel::CollectionRole).value<Collection>();
0312         cols << collection;
0313     }
0314     return cols;
0315 }
0316 
0317 QList<Collection::Id> FavoriteCollectionsModel::collectionIds() const
0318 {
0319     return d->collectionIds;
0320 }
0321 
0322 void Akonadi::FavoriteCollectionsModel::setFavoriteLabel(const Collection &collection, const QString &label)
0323 {
0324     Q_ASSERT(d->collectionIds.contains(collection.id()));
0325     d->labelMap[collection.id()] = label;
0326     d->saveConfig();
0327 
0328     const QModelIndex idx = EntityTreeModel::modelIndexForCollection(sourceModel(), collection);
0329 
0330     if (!idx.isValid()) {
0331         return;
0332     }
0333 
0334     const QModelIndex index = mapFromSource(idx);
0335     Q_EMIT dataChanged(index, index);
0336 }
0337 
0338 QVariant Akonadi::FavoriteCollectionsModel::data(const QModelIndex &index, int role) const
0339 {
0340     if (index.column() == 0 && (role == Qt::DisplayRole || role == Qt::EditRole)) {
0341         const QModelIndex sourceIndex = mapToSource(index);
0342         const Collection::Id collectionId = sourceModel()->data(sourceIndex, EntityTreeModel::CollectionIdRole).toLongLong();
0343 
0344         return d->labelForCollection(collectionId);
0345     } else {
0346         return KSelectionProxyModel::data(index, role);
0347     }
0348 }
0349 
0350 bool FavoriteCollectionsModel::setData(const QModelIndex &index, const QVariant &value, int role)
0351 {
0352     if (index.isValid() && index.column() == 0 && role == Qt::EditRole) {
0353         const QString newLabel = value.toString();
0354         if (newLabel.isEmpty()) {
0355             return false;
0356         }
0357         const QModelIndex sourceIndex = mapToSource(index);
0358         const auto collection = sourceModel()->data(sourceIndex, EntityTreeModel::CollectionRole).value<Collection>();
0359         setFavoriteLabel(collection, newLabel);
0360         return true;
0361     }
0362     return KSelectionProxyModel::setData(index, value, role);
0363 }
0364 
0365 QString Akonadi::FavoriteCollectionsModel::favoriteLabel(const Akonadi::Collection &collection)
0366 {
0367     if (!collection.isValid()) {
0368         return QString();
0369     }
0370     return d->labelForCollection(collection.id());
0371 }
0372 
0373 QString Akonadi::FavoriteCollectionsModel::defaultFavoriteLabel(const Akonadi::Collection &collection)
0374 {
0375     if (!collection.isValid()) {
0376         return QString();
0377     }
0378 
0379     const auto colIdx = EntityTreeModel::modelIndexForCollection(sourceModel(), Collection(collection.id()));
0380     const QString nameOfCollection = colIdx.data().toString();
0381 
0382     QModelIndex idx = colIdx.parent();
0383     QString accountName;
0384     while (idx != QModelIndex()) {
0385         accountName = idx.data(EntityTreeModel::OriginalCollectionNameRole).toString();
0386         idx = idx.parent();
0387     }
0388     if (accountName.isEmpty()) {
0389         return nameOfCollection;
0390     } else {
0391         return nameOfCollection + QStringLiteral(" (") + accountName + QLatin1Char(')');
0392     }
0393 }
0394 
0395 QVariant FavoriteCollectionsModel::headerData(int section, Qt::Orientation orientation, int role) const
0396 {
0397     if (section == 0 && orientation == Qt::Horizontal && role == Qt::DisplayRole) {
0398         return i18n("Favorite Folders");
0399     } else {
0400         return KSelectionProxyModel::headerData(section, orientation, role);
0401     }
0402 }
0403 
0404 bool FavoriteCollectionsModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent)
0405 {
0406     Q_UNUSED(action)
0407     Q_UNUSED(row)
0408     Q_UNUSED(column)
0409     if (data->hasFormat(QStringLiteral("text/uri-list"))) {
0410         const QList<QUrl> urls = data->urls();
0411 
0412         const QModelIndex sourceIndex = mapToSource(parent);
0413         const auto destCollection = sourceModel()->data(sourceIndex, EntityTreeModel::CollectionRole).value<Collection>();
0414 
0415         MimeTypeChecker mimeChecker;
0416         mimeChecker.setWantedMimeTypes(destCollection.contentMimeTypes());
0417 
0418         for (const QUrl &url : urls) {
0419             const Collection col = Collection::fromUrl(url);
0420             if (col.isValid()) {
0421                 addCollection(col);
0422             } else {
0423                 const Item item = Item::fromUrl(url);
0424                 if (item.isValid()) {
0425                     if (item.parentCollection().id() == destCollection.id() && action != Qt::CopyAction) {
0426                         qCDebug(AKONADICORE_LOG) << "Error: source and destination of move are the same.";
0427                         return false;
0428                     }
0429 #if 0
0430                     if (!mimeChecker.isWantedItem(item)) {
0431                         qCDebug(AKONADICORE_LOG) << "unwanted item" << mimeChecker.wantedMimeTypes() << item.mimeType();
0432                         return false;
0433                     }
0434 #endif
0435                     KJob *job = PasteHelper::pasteUriList(data, destCollection, action);
0436                     if (!job) {
0437                         return false;
0438                     }
0439                     connect(job, &KJob::result, this, &FavoriteCollectionsModel::pasteJobDone);
0440                     // Accept the event so that it doesn't propagate.
0441                     return true;
0442                 }
0443             }
0444         }
0445         return true;
0446     }
0447     return false;
0448 }
0449 
0450 QStringList FavoriteCollectionsModel::mimeTypes() const
0451 {
0452     QStringList mts = KSelectionProxyModel::mimeTypes();
0453     if (!mts.contains(QLatin1StringView("text/uri-list"))) {
0454         mts.append(QStringLiteral("text/uri-list"));
0455     }
0456     return mts;
0457 }
0458 
0459 Qt::ItemFlags FavoriteCollectionsModel::flags(const QModelIndex &index) const
0460 {
0461     Qt::ItemFlags fs = KSelectionProxyModel::flags(index);
0462     if (!index.isValid()) {
0463         fs |= Qt::ItemIsDropEnabled;
0464     }
0465     return fs;
0466 }
0467 
0468 void FavoriteCollectionsModel::pasteJobDone(KJob *job)
0469 {
0470     if (job->error()) {
0471         qCDebug(AKONADICORE_LOG) << "Paste job error:" << job->errorString();
0472     }
0473 }
0474 
0475 #include "moc_favoritecollectionsmodel.cpp"