Akonadi

favoritecollectionsmodel.cpp
1/*
2 SPDX-FileCopyrightText: 2009 Kevin Ottens <ervin@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5*/
6
7#include "favoritecollectionsmodel.h"
8#include "akonadicore_debug.h"
9
10#include <QItemSelectionModel>
11#include <QMimeData>
12
13#include <KConfig>
14#include <KConfigGroup>
15#include <KJob>
16#include <KLocalizedString>
17#include <QUrl>
18
19#include "collectionmodifyjob.h"
20#include "entitytreemodel.h"
21#include "favoritecollectionattribute.h"
22#include "mimetypechecker.h"
23#include "pastehelper_p.h"
24
25using namespace Akonadi;
26
27/**
28 * @internal
29 */
30class Akonadi::FavoriteCollectionsModelPrivate
31{
32public:
33 FavoriteCollectionsModelPrivate(const KConfigGroup &group, FavoriteCollectionsModel *parent)
34 : q(parent)
35 , configGroup(group)
36 {
37 }
38
39 QString labelForCollection(Collection::Id collectionId) const
40 {
41 if (labelMap.contains(collectionId)) {
42 return labelMap[collectionId];
43 }
44
45 return q->defaultFavoriteLabel(Collection{collectionId});
46 }
47
48 void insertIfAvailable(Collection::Id col)
49 {
50 if (collectionIds.contains(col)) {
51 select(col);
52 if (!referencedCollections.contains(col)) {
53 reference(col);
54 }
56 if (idx.isValid()) {
57 auto c = q->data(idx, EntityTreeModel::CollectionRole).value<Collection>();
58 if (c.isValid() && !c.hasAttribute<FavoriteCollectionAttribute>()) {
59 c.addAttribute(new FavoriteCollectionAttribute());
60 new CollectionModifyJob(c, q);
61 }
62 }
63 }
64 }
65
66 void insertIfAvailable(const QModelIndex &idx)
67 {
69 }
70
71 /**
72 * Stuff changed (e.g. new rows inserted into sorted model), reload everything.
73 */
74 void reload()
75 {
76 // don't clear the selection model here. Otherwise we mess up the users selection as collections get removed and re-inserted.
77 for (const Collection::Id &collectionId : std::as_const(collectionIds)) {
78 insertIfAvailable(collectionId);
79 }
80 // If a favorite folder was removed then surely it's gone from the selection model, so no need to do anything about that.
81 }
82
83 void rowsInserted(const QModelIndex &parent, int begin, int end)
84 {
85 for (int row = begin; row <= end; row++) {
86 const QModelIndex child = q->sourceModel()->index(row, 0, parent);
87 if (!child.isValid()) {
88 continue;
89 }
90 insertIfAvailable(child);
91 const int childRows = q->sourceModel()->rowCount(child);
92 if (childRows > 0) {
93 rowsInserted(child, 0, childRows - 1);
94 }
95 }
96 }
97
98 void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight)
99 {
100 for (int row = topLeft.row(); row <= bottomRight.row(); row++) {
101 const QModelIndex idx = topLeft.sibling(row, 0);
102 insertIfAvailable(idx);
103 }
104 }
105
106 /**
107 * Selects the index in the internal selection model to make the collection visible in the model
108 */
109 void select(Collection::Id collectionId)
110 {
112 if (index.isValid()) {
113 q->selectionModel()->select(index, QItemSelectionModel::Select);
114 }
115 }
116
117 void deselect(Collection::Id collectionId)
118 {
120 if (idx.isValid()) {
121 q->selectionModel()->select(idx, QItemSelectionModel::Deselect);
122 }
123 }
124
125 void reference(Collection::Id collectionId)
126 {
127 if (referencedCollections.contains(collectionId)) {
128 qCWarning(AKONADICORE_LOG) << "already referenced " << collectionId;
129 return;
130 }
132 if (index.isValid()) {
133 if (q->sourceModel()->setData(index, QVariant(), EntityTreeModel::CollectionRefRole)) {
134 referencedCollections << collectionId;
135 } else {
136 qCWarning(AKONADICORE_LOG) << "failed to reference collection";
137 }
138 q->sourceModel()->fetchMore(index);
139 }
140 }
141
142 void dereference(Collection::Id collectionId)
143 {
144 if (!referencedCollections.contains(collectionId)) {
145 qCWarning(AKONADICORE_LOG) << "not referenced " << collectionId;
146 return;
147 }
149 if (index.isValid()) {
151 referencedCollections.remove(collectionId);
152 }
153 }
154
155 /**
156 * Adds a collection to the favorite collections
157 */
158 void add(Collection::Id collectionId)
159 {
160 if (collectionIds.contains(collectionId)) {
161 qCDebug(AKONADICORE_LOG) << "already in model " << collectionId;
162 return;
163 }
164 collectionIds << collectionId;
165 reference(collectionId);
166 select(collectionId);
167 const auto idx = EntityTreeModel::modelIndexForCollection(q, Collection{collectionId});
168 if (idx.isValid()) {
169 auto col = q->data(idx, EntityTreeModel::CollectionRole).value<Collection>();
170 if (col.isValid() && !col.hasAttribute<FavoriteCollectionAttribute>()) {
171 col.addAttribute(new FavoriteCollectionAttribute());
172 new CollectionModifyJob(col, q);
173 }
174 }
175 }
176
177 void remove(Collection::Id collectionId)
178 {
179 collectionIds.removeAll(collectionId);
180 labelMap.remove(collectionId);
181 dereference(collectionId);
182 deselect(collectionId);
183 const auto idx = EntityTreeModel::modelIndexForCollection(q, Collection{collectionId});
184 if (idx.isValid()) {
185 auto col = q->data(idx, EntityTreeModel::CollectionRole).value<Collection>();
186 if (col.isValid() && col.hasAttribute<FavoriteCollectionAttribute>()) {
187 col.removeAttribute<FavoriteCollectionAttribute>();
188 new CollectionModifyJob(col, q);
189 }
190 }
191 }
192
193 void set(const QList<Collection::Id> &collections)
194 {
195 QList<Collection::Id> colIds = collectionIds;
196 for (const Collection::Id &col : collections) {
197 const int removed = colIds.removeAll(col);
198 const bool isNewCollection = removed <= 0;
199 if (isNewCollection) {
200 add(col);
201 }
202 }
203 // Remove what's left
204 for (Akonadi::Collection::Id colId : std::as_const(colIds)) {
205 remove(colId);
206 }
207 }
208
209 void set(const Akonadi::Collection::List &collections)
210 {
212 colIds.reserve(collections.count());
213 for (const Akonadi::Collection &col : collections) {
214 colIds << col.id();
215 }
216 set(colIds);
217 }
218
219 void loadConfig()
220 {
221 const QList<Collection::Id> collections = configGroup.readEntry("FavoriteCollectionIds", QList<qint64>());
222 const QStringList labels = configGroup.readEntry("FavoriteCollectionLabels", QStringList());
223 const int numberOfLabels(labels.size());
224 for (int i = 0; i < collections.size(); ++i) {
225 if (i < numberOfLabels) {
226 labelMap[collections[i]] = labels[i];
227 }
228 add(collections[i]);
229 }
230 }
231
232 void saveConfig()
233 {
234 QStringList labels;
235 labels.reserve(collectionIds.count());
236 for (const Collection::Id &collectionId : std::as_const(collectionIds)) {
237 labels << labelForCollection(collectionId);
238 }
239
240 configGroup.writeEntry("FavoriteCollectionIds", collectionIds);
241 configGroup.writeEntry("FavoriteCollectionLabels", labels);
242 configGroup.config()->sync();
243 }
244
246
247 QList<Collection::Id> collectionIds;
248 QSet<Collection::Id> referencedCollections;
249 QHash<qint64, QString> labelMap;
250 KConfigGroup configGroup;
251};
252
253/* Implementation note:
254 *
255 * We use KSelectionProxyModel in order to make a flat list of selected folders from the folder tree.
256 *
257 * Attempts to use QSortFilterProxyModel make code somewhat simpler,
258 * but don't work since we then get a filtered tree, not a flat list. Stacking a KDescendantsProxyModel
259 * on top would likely remove explicitly selected parents when one of their child is selected too.
260 */
261
263 : KSelectionProxyModel(new QItemSelectionModel(source, parent), parent)
264 , d(new FavoriteCollectionsModelPrivate(group, this))
265{
266 setSourceModel(source);
267 setFilterBehavior(ExactSelection);
268
269 d->loadConfig();
270 // React to various changes in the source model
271 connect(source, &QAbstractItemModel::modelReset, this, [this]() {
272 d->reload();
273 });
274 connect(source, &QAbstractItemModel::layoutChanged, this, [this]() {
275 d->reload();
276 });
277 connect(source, &QAbstractItemModel::rowsInserted, this, [this](const QModelIndex &parent, int begin, int end) {
278 d->rowsInserted(parent, begin, end);
279 });
280 connect(source, &QAbstractItemModel::dataChanged, this, [this](const QModelIndex &tl, const QModelIndex &br) {
281 d->dataChanged(tl, br);
282 });
283}
284
286
288{
289 d->set(collections);
290 d->saveConfig();
291}
292
294{
295 d->add(collection.id());
296 d->saveConfig();
297}
298
300{
301 d->remove(collection.id());
302 d->saveConfig();
303}
304
306{
307 Collection::List cols;
308 cols.reserve(d->collectionIds.count());
309 for (const Collection::Id &colId : std::as_const(d->collectionIds)) {
311 const auto collection = sourceModel()->data(idx, EntityTreeModel::CollectionRole).value<Collection>();
312 cols << collection;
313 }
314 return cols;
315}
316
318{
319 return d->collectionIds;
320}
321
323{
324 Q_ASSERT(d->collectionIds.contains(collection.id()));
325 d->labelMap[collection.id()] = label;
326 d->saveConfig();
327
328 const QModelIndex idx = EntityTreeModel::modelIndexForCollection(sourceModel(), collection);
329
330 if (!idx.isValid()) {
331 return;
332 }
333
334 const QModelIndex index = mapFromSource(idx);
335 Q_EMIT dataChanged(index, index);
336}
337
338QVariant Akonadi::FavoriteCollectionsModel::data(const QModelIndex &index, int role) const
339{
340 if (index.column() == 0 && (role == Qt::DisplayRole || role == Qt::EditRole)) {
341 const QModelIndex sourceIndex = mapToSource(index);
342 const Collection::Id collectionId = sourceModel()->data(sourceIndex, EntityTreeModel::CollectionIdRole).toLongLong();
343
344 return d->labelForCollection(collectionId);
345 } else {
346 return KSelectionProxyModel::data(index, role);
347 }
348}
349
350bool FavoriteCollectionsModel::setData(const QModelIndex &index, const QVariant &value, int role)
351{
352 if (index.isValid() && index.column() == 0 && role == Qt::EditRole) {
353 const QString newLabel = value.toString();
354 if (newLabel.isEmpty()) {
355 return false;
356 }
357 const QModelIndex sourceIndex = mapToSource(index);
358 const auto collection = sourceModel()->data(sourceIndex, EntityTreeModel::CollectionRole).value<Collection>();
359 setFavoriteLabel(collection, newLabel);
360 return true;
361 }
362 return KSelectionProxyModel::setData(index, value, role);
363}
364
366{
367 if (!collection.isValid()) {
368 return QString();
369 }
370 return d->labelForCollection(collection.id());
371}
372
373QString Akonadi::FavoriteCollectionsModel::defaultFavoriteLabel(const Akonadi::Collection &collection)
374{
375 if (!collection.isValid()) {
376 return QString();
377 }
378
379 const auto colIdx = EntityTreeModel::modelIndexForCollection(sourceModel(), Collection(collection.id()));
380 const QString nameOfCollection = colIdx.data().toString();
381
382 QModelIndex idx = colIdx.parent();
383 QString accountName;
384 while (idx != QModelIndex()) {
386 idx = idx.parent();
387 }
388 if (accountName.isEmpty()) {
389 return nameOfCollection;
390 } else {
391 return nameOfCollection + QStringLiteral(" (") + accountName + QLatin1Char(')');
392 }
393}
394
395QVariant FavoriteCollectionsModel::headerData(int section, Qt::Orientation orientation, int role) const
396{
397 if (section == 0 && orientation == Qt::Horizontal && role == Qt::DisplayRole) {
398 return i18n("Favorite Folders");
399 } else {
400 return KSelectionProxyModel::headerData(section, orientation, role);
401 }
402}
403
404bool FavoriteCollectionsModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent)
405{
406 Q_UNUSED(action)
407 Q_UNUSED(row)
408 Q_UNUSED(column)
409 if (data->hasFormat(QStringLiteral("text/uri-list"))) {
410 const QList<QUrl> urls = data->urls();
411
412 const QModelIndex sourceIndex = mapToSource(parent);
413 const auto destCollection = sourceModel()->data(sourceIndex, EntityTreeModel::CollectionRole).value<Collection>();
414
415 MimeTypeChecker mimeChecker;
416 mimeChecker.setWantedMimeTypes(destCollection.contentMimeTypes());
417
418 for (const QUrl &url : urls) {
419 const Collection col = Collection::fromUrl(url);
420 if (col.isValid()) {
421 addCollection(col);
422 } else {
423 const Item item = Item::fromUrl(url);
424 if (item.isValid()) {
425 if (item.parentCollection().id() == destCollection.id() && action != Qt::CopyAction) {
426 qCDebug(AKONADICORE_LOG) << "Error: source and destination of move are the same.";
427 return false;
428 }
429#if 0
430 if (!mimeChecker.isWantedItem(item)) {
431 qCDebug(AKONADICORE_LOG) << "unwanted item" << mimeChecker.wantedMimeTypes() << item.mimeType();
432 return false;
433 }
434#endif
435 KJob *job = PasteHelper::pasteUriList(data, destCollection, action);
436 if (!job) {
437 return false;
438 }
439 connect(job, &KJob::result, this, &FavoriteCollectionsModel::pasteJobDone);
440 // Accept the event so that it doesn't propagate.
441 return true;
442 }
443 }
444 }
445 return true;
446 }
447 return false;
448}
449
450QStringList FavoriteCollectionsModel::mimeTypes() const
451{
453 if (!mts.contains(QLatin1StringView("text/uri-list"))) {
454 mts.append(QStringLiteral("text/uri-list"));
455 }
456 return mts;
457}
458
459Qt::ItemFlags FavoriteCollectionsModel::flags(const QModelIndex &index) const
460{
462 if (!index.isValid()) {
464 }
465 return fs;
466}
467
468void FavoriteCollectionsModel::pasteJobDone(KJob *job)
469{
470 if (job->error()) {
471 qCDebug(AKONADICORE_LOG) << "Paste job error:" << job->errorString();
472 }
473}
474
475#include "moc_favoritecollectionsmodel.cpp"
Job that modifies a collection in the Akonadi storage.
Represents a collection of PIM items.
Definition collection.h:62
qint64 Id
Describes the unique id type.
Definition collection.h:79
static Collection fromUrl(const QUrl &url)
Creates a collection from the given url.
static QModelIndex modelIndexForCollection(const QAbstractItemModel *model, const Collection &collection)
Returns a QModelIndex in model which points to collection.
@ OriginalCollectionNameRole
Returns original name for collection.
@ CollectionRole
The collection.
@ CollectionIdRole
The collection id.
A model that lists a set of favorite collections.
AKONADICORE_DEPRECATED Collection::List collections() const
Returns the list of favorite collections.
QString favoriteLabel(const Akonadi::Collection &col)
Return associate label for collection.
void addCollection(const Akonadi::Collection &collection)
Adds a collection to the list of favorite collections.
~FavoriteCollectionsModel() override
Destroys the favorite collections model.
FavoriteCollectionsModel(QAbstractItemModel *model, const KConfigGroup &group, QObject *parent=nullptr)
Creates a new favorite collections model.
QList< Collection::Id > collectionIds() const
Returns the list of ids of favorite collections set on the FavoriteCollectionsModel.
void setFavoriteLabel(const Akonadi::Collection &collection, const QString &label)
Sets a custom label that will be used when showing the favorite collection.
void setCollections(const Akonadi::Collection::List &collections)
Sets the collections as favorite collections.
void removeCollection(const Akonadi::Collection &collection)
Removes a collection from the list of favorite collections.
static Item fromUrl(const QUrl &url)
Creates an item from the given url.
Definition item.cpp:386
Helper for checking MIME types of Collections and Items.
void setWantedMimeTypes(const QStringList &mimeTypes)
Sets the list of wanted MIME types this instance checks against.
void writeEntry(const char *key, const char *value, WriteConfigFlags pFlags=Normal)
KConfig * config()
QString readEntry(const char *key, const char *aDefault=nullptr) const
bool sync() override
virtual QString errorString() const
int error() const
void result(KJob *job)
void setFilterBehavior(FilterBehavior behavior)
void setSourceModel(QAbstractItemModel *sourceModel) override
QString i18n(const char *text, const TYPE &arg...)
Helper integration between Akonadi and Qt.
const QList< QKeySequence > & end()
virtual QVariant data(const QModelIndex &index, int role) const const=0
void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList< int > &roles)
virtual Qt::ItemFlags flags(const QModelIndex &index) const const
virtual QVariant headerData(int section, Qt::Orientation orientation, int role) const const
virtual QModelIndex index(int row, int column, const QModelIndex &parent) const const=0
void layoutChanged(const QList< QPersistentModelIndex > &parents, QAbstractItemModel::LayoutChangeHint hint)
virtual QStringList mimeTypes() const const
void rowsInserted(const QModelIndex &parent, int first, int last)
virtual bool setData(const QModelIndex &index, const QVariant &value, int role)
virtual QModelIndex mapToSource(const QModelIndex &proxyIndex) const const=0
bool contains(const Key &key) const const
bool remove(const Key &key)
void append(QList< T > &&value)
bool contains(const AT &value) const const
qsizetype count() const const
qsizetype removeAll(const AT &t)
void reserve(qsizetype size)
qsizetype size() const const
virtual bool hasFormat(const QString &mimeType) const const
QList< QUrl > urls() const const
int column() const const
QVariant data(int role) const const
bool isValid() const const
QModelIndex parent() const const
int row() const const
QModelIndex sibling(int row, int column) const const
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
QObject * parent() const const
bool contains(const QSet< T > &other) const const
bool remove(const T &value)
QChar * data()
bool isEmpty() const const
bool contains(QLatin1StringView str, Qt::CaseSensitivity cs) const const
DropAction
DisplayRole
typedef ItemFlags
Orientation
QString toString() const const
T value() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Sat Dec 21 2024 17:01:42 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.