File indexing completed on 2024-11-10 04:40:35
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"