Akonadi

favoritecollectionsmodel.cpp
1 /*
2  SPDX-FileCopyrightText: 2009 Kevin Ottens <[email protected]>
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 
25 using namespace Akonadi;
26 
27 /**
28  * @internal
29  */
30 class Akonadi::FavoriteCollectionsModelPrivate
31 {
32 public:
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()) {
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  {
111  const QModelIndex index = EntityTreeModel::modelIndexForCollection(q->sourceModel(), Collection(collectionId));
112  if (index.isValid()) {
113  q->selectionModel()->select(index, QItemSelectionModel::Select);
114  }
115  }
116 
117  void deselect(Collection::Id collectionId)
118  {
119  const QModelIndex idx = EntityTreeModel::modelIndexForCollection(q->sourceModel(), Collection(collectionId));
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  }
131  const QModelIndex index = EntityTreeModel::modelIndexForCollection(q->sourceModel(), Collection(collectionId));
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  }
148  const QModelIndex index = EntityTreeModel::modelIndexForCollection(q->sourceModel(), Collection(collectionId));
149  if (index.isValid()) {
150  q->sourceModel()->setData(index, QVariant(), EntityTreeModel::CollectionDerefRole);
151  referencedCollections.remove(collectionId);
152  }
153  }
154 
155  void clearReferences()
156  {
157  for (const Collection::Id &collectionId : std::as_const(referencedCollections)) {
158  dereference(collectionId);
159  }
160  }
161 
162  /**
163  * Adds a collection to the favorite collections
164  */
165  void add(Collection::Id collectionId)
166  {
167  if (collectionIds.contains(collectionId)) {
168  qCDebug(AKONADICORE_LOG) << "already in model " << collectionId;
169  return;
170  }
171  collectionIds << collectionId;
172  reference(collectionId);
173  select(collectionId);
174  const auto idx = EntityTreeModel::modelIndexForCollection(q, Collection{collectionId});
175  if (idx.isValid()) {
176  auto col = q->data(idx, EntityTreeModel::CollectionRole).value<Collection>();
177  if (col.isValid() && !col.hasAttribute<FavoriteCollectionAttribute>()) {
178  col.addAttribute(new FavoriteCollectionAttribute());
179  new CollectionModifyJob(col, q);
180  }
181  }
182  }
183 
184  void remove(Collection::Id collectionId)
185  {
186  collectionIds.removeAll(collectionId);
187  labelMap.remove(collectionId);
188  dereference(collectionId);
189  deselect(collectionId);
190  const auto idx = EntityTreeModel::modelIndexForCollection(q, Collection{collectionId});
191  if (idx.isValid()) {
192  auto col = q->data(idx, EntityTreeModel::CollectionRole).value<Collection>();
193  if (col.isValid() && col.hasAttribute<FavoriteCollectionAttribute>()) {
194  col.removeAttribute<FavoriteCollectionAttribute>();
195  new CollectionModifyJob(col, q);
196  }
197  }
198  }
199 
200  void set(const QList<Collection::Id> &collections)
201  {
202  QList<Collection::Id> colIds = collectionIds;
203  for (const Collection::Id &col : collections) {
204  const int removed = colIds.removeAll(col);
205  const bool isNewCollection = removed <= 0;
206  if (isNewCollection) {
207  add(col);
208  }
209  }
210  // Remove what's left
211  for (Akonadi::Collection::Id colId : std::as_const(colIds)) {
212  remove(colId);
213  }
214  }
215 
216  void set(const Akonadi::Collection::List &collections)
217  {
219  colIds.reserve(collections.count());
220  for (const Akonadi::Collection &col : collections) {
221  colIds << col.id();
222  }
223  set(colIds);
224  }
225 
226  void loadConfig()
227  {
228  const QList<Collection::Id> collections = configGroup.readEntry("FavoriteCollectionIds", QList<qint64>());
229  const QStringList labels = configGroup.readEntry("FavoriteCollectionLabels", QStringList());
230  const int numberOfLabels(labels.size());
231  for (int i = 0; i < collections.size(); ++i) {
232  if (i < numberOfLabels) {
233  labelMap[collections[i]] = labels[i];
234  }
235  add(collections[i]);
236  }
237  }
238 
239  void saveConfig()
240  {
241  QStringList labels;
242  labels.reserve(collectionIds.count());
243  for (const Collection::Id &collectionId : std::as_const(collectionIds)) {
244  labels << labelForCollection(collectionId);
245  }
246 
247  configGroup.writeEntry("FavoriteCollectionIds", collectionIds);
248  configGroup.writeEntry("FavoriteCollectionLabels", labels);
249  configGroup.config()->sync();
250  }
251 
252  FavoriteCollectionsModel *const q;
253 
254  QList<Collection::Id> collectionIds;
255  QSet<Collection::Id> referencedCollections;
256  QHash<qint64, QString> labelMap;
257  KConfigGroup configGroup;
258 };
259 
260 /* Implementation note:
261  *
262  * We use KSelectionProxyModel in order to make a flat list of selected folders from the folder tree.
263  *
264  * Attempts to use QSortFilterProxyModel make code somewhat simpler,
265  * but don't work since we then get a filtered tree, not a flat list. Stacking a KDescendantsProxyModel
266  * on top would likely remove explicitly selected parents when one of their child is selected too.
267  */
268 
270  : KSelectionProxyModel(new QItemSelectionModel(source, parent), parent)
271  , d(new FavoriteCollectionsModelPrivate(group, this))
272 {
273  setSourceModel(source);
274  setFilterBehavior(ExactSelection);
275 
276  d->loadConfig();
277  // React to various changes in the source model
278  connect(source, &QAbstractItemModel::modelReset, this, [this]() {
279  d->reload();
280  });
281  connect(source, &QAbstractItemModel::layoutChanged, this, [this]() {
282  d->reload();
283  });
284  connect(source, &QAbstractItemModel::rowsInserted, this, [this](const QModelIndex &parent, int begin, int end) {
285  d->rowsInserted(parent, begin, end);
286  });
287  connect(source, &QAbstractItemModel::dataChanged, this, [this](const QModelIndex &tl, const QModelIndex &br) {
288  d->dataChanged(tl, br);
289  });
290 }
291 
293 
295 {
296  d->set(collections);
297  d->saveConfig();
298 }
299 
301 {
302  d->add(collection.id());
303  d->saveConfig();
304 }
305 
307 {
308  d->remove(collection.id());
309  d->saveConfig();
310 }
311 
313 {
314  Collection::List cols;
315  cols.reserve(d->collectionIds.count());
316  for (const Collection::Id &colId : std::as_const(d->collectionIds)) {
318  const auto collection = sourceModel()->data(idx, EntityTreeModel::CollectionRole).value<Collection>();
319  cols << collection;
320  }
321  return cols;
322 }
323 
325 {
326  return d->collectionIds;
327 }
328 
330 {
331  Q_ASSERT(d->collectionIds.contains(collection.id()));
332  d->labelMap[collection.id()] = label;
333  d->saveConfig();
334 
335  const QModelIndex idx = EntityTreeModel::modelIndexForCollection(sourceModel(), collection);
336 
337  if (!idx.isValid()) {
338  return;
339  }
340 
341  const QModelIndex index = mapFromSource(idx);
342  Q_EMIT dataChanged(index, index);
343 }
344 
345 QVariant Akonadi::FavoriteCollectionsModel::data(const QModelIndex &index, int role) const
346 {
347  if (index.column() == 0 && (role == Qt::DisplayRole || role == Qt::EditRole)) {
348  const QModelIndex sourceIndex = mapToSource(index);
349  const Collection::Id collectionId = sourceModel()->data(sourceIndex, EntityTreeModel::CollectionIdRole).toLongLong();
350 
351  return d->labelForCollection(collectionId);
352  } else {
353  return KSelectionProxyModel::data(index, role);
354  }
355 }
356 
357 bool FavoriteCollectionsModel::setData(const QModelIndex &index, const QVariant &value, int role)
358 {
359  if (index.isValid() && index.column() == 0 && role == Qt::EditRole) {
360  const QString newLabel = value.toString();
361  if (newLabel.isEmpty()) {
362  return false;
363  }
364  const QModelIndex sourceIndex = mapToSource(index);
365  const auto collection = sourceModel()->data(sourceIndex, EntityTreeModel::CollectionRole).value<Collection>();
366  setFavoriteLabel(collection, newLabel);
367  return true;
368  }
369  return KSelectionProxyModel::setData(index, value, role);
370 }
371 
373 {
374  if (!collection.isValid()) {
375  return QString();
376  }
377  return d->labelForCollection(collection.id());
378 }
379 
380 QString Akonadi::FavoriteCollectionsModel::defaultFavoriteLabel(const Akonadi::Collection &collection)
381 {
382  if (!collection.isValid()) {
383  return QString();
384  }
385 
386  const auto colIdx = EntityTreeModel::modelIndexForCollection(sourceModel(), Collection(collection.id()));
387  const QString nameOfCollection = colIdx.data().toString();
388 
389  QModelIndex idx = colIdx.parent();
390  QString accountName;
391  while (idx != QModelIndex()) {
393  idx = idx.parent();
394  }
395  if (accountName.isEmpty()) {
396  return nameOfCollection;
397  } else {
398  return nameOfCollection + QStringLiteral(" (") + accountName + QLatin1Char(')');
399  }
400 }
401 
402 QVariant FavoriteCollectionsModel::headerData(int section, Qt::Orientation orientation, int role) const
403 {
404  if (section == 0 && orientation == Qt::Horizontal && role == Qt::DisplayRole) {
405  return i18n("Favorite Folders");
406  } else {
407  return KSelectionProxyModel::headerData(section, orientation, role);
408  }
409 }
410 
411 bool FavoriteCollectionsModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent)
412 {
413  Q_UNUSED(action)
414  Q_UNUSED(row)
415  Q_UNUSED(column)
416  if (data->hasFormat(QStringLiteral("text/uri-list"))) {
417  const QList<QUrl> urls = data->urls();
418 
419  const QModelIndex sourceIndex = mapToSource(parent);
420  const auto destCollection = sourceModel()->data(sourceIndex, EntityTreeModel::CollectionRole).value<Collection>();
421 
422  MimeTypeChecker mimeChecker;
423  mimeChecker.setWantedMimeTypes(destCollection.contentMimeTypes());
424 
425  for (const QUrl &url : urls) {
426  const Collection col = Collection::fromUrl(url);
427  if (col.isValid()) {
428  addCollection(col);
429  } else {
430  const Item item = Item::fromUrl(url);
431  if (item.isValid()) {
432  if (item.parentCollection().id() == destCollection.id() && action != Qt::CopyAction) {
433  qCDebug(AKONADICORE_LOG) << "Error: source and destination of move are the same.";
434  return false;
435  }
436 #if 0
437  if (!mimeChecker.isWantedItem(item)) {
438  qCDebug(AKONADICORE_LOG) << "unwanted item" << mimeChecker.wantedMimeTypes() << item.mimeType();
439  return false;
440  }
441 #endif
442  KJob *job = PasteHelper::pasteUriList(data, destCollection, action);
443  if (!job) {
444  return false;
445  }
446  connect(job, &KJob::result, this, &FavoriteCollectionsModel::pasteJobDone);
447  // Accept the event so that it doesn't propagate.
448  return true;
449  }
450  }
451  }
452  return true;
453  }
454  return false;
455 }
456 
457 QStringList FavoriteCollectionsModel::mimeTypes() const
458 {
460  if (!mts.contains(QLatin1String("text/uri-list"))) {
461  mts.append(QStringLiteral("text/uri-list"));
462  }
463  return mts;
464 }
465 
466 Qt::ItemFlags FavoriteCollectionsModel::flags(const QModelIndex &index) const
467 {
469  if (!index.isValid()) {
470  fs |= Qt::ItemIsDropEnabled;
471  }
472  return fs;
473 }
474 
475 void FavoriteCollectionsModel::pasteJobDone(KJob *job)
476 {
477  if (job->error()) {
478  qCDebug(AKONADICORE_LOG) << "Paste job error:" << job->errorString();
479  }
480 }
481 
482 #include "moc_favoritecollectionsmodel.cpp"
bool isValid() const
Returns whether the item is valid.
Definition: item.cpp:88
void append(const T &value)
static Collection fromUrl(const QUrl &url)
Creates a collection from the given url.
Definition: collection.cpp:267
FavoriteCollectionsModel(QAbstractItemModel *model, const KConfigGroup &group, QObject *parent=nullptr)
Creates a new favorite collections model.
DisplayRole
void removeCollection(const Akonadi::Collection &collection)
Removes a collection from the list of favorite collections.
Job that modifies a collection in the Akonadi storage.
QAction * deselect(const QObject *recvr, const char *slot, QObject *parent)
virtual QVariant data(const QModelIndex &proxyIndex, int role) const const override
QModelIndex sibling(int row, int column) const const
virtual QModelIndex mapToSource(const QModelIndex &proxyIndex) const const=0
virtual Qt::ItemFlags flags(const QModelIndex &index) const const override
void setWantedMimeTypes(const QStringList &mimeTypes)
Sets the list of wanted MIME types this instance checks against.
void result(KJob *job)
int removeAll(const T &value)
void setSourceModel(QAbstractItemModel *sourceModel) override
int column() const const
const QList< QKeySequence > & reload()
T value() const const
void removeAttribute(const QByteArray &name)
Removes and deletes the attribute of the given type name.
Definition: collection.cpp:156
void layoutChanged(const QList< QPersistentModelIndex > &parents, QAbstractItemModel::LayoutChangeHint hint)
bool contains(const QString &str, Qt::CaseSensitivity cs) const const
@ CollectionIdRole
The collection id.
Represents a collection of PIM items.
Definition: collection.h:61
KGuiItem remove()
virtual QVariant headerData(int section, Qt::Orientation orientation, int role) const const override
void setCollections(const Akonadi::Collection::List &collections)
Sets the collections as favorite collections.
@ OriginalCollectionNameRole
Returns original name for collection.
virtual bool setData(const QModelIndex &index, const QVariant &value, int role) override
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
void reserve(int alloc)
QString mimeType() const
Returns the mime type of the item.
Definition: item.cpp:331
QList< Collection::Id > collectionIds() const
Returns the list of ids of favorite collections set on the FavoriteCollectionsModel.
QVariant data(int role) const const
int size() const const
void addAttribute(Attribute *attribute)
Adds an attribute to the collection.
Definition: collection.cpp:151
typedef ItemFlags
QString i18n(const char *text, const TYPE &arg...)
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.
@ CollectionRole
The collection.
void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector< int > &roles)
static Item fromUrl(const QUrl &url)
Creates an item from the given url.
Definition: item.cpp:391
Orientation
bool isEmpty() const const
Collection parentCollection() const
Returns the parent collection of this object.
Definition: item.cpp:153
KGuiItem add()
AKONADICORE_DEPRECATED Collection::List collections() const
Returns the list of favorite collections.
void setFavoriteLabel(const Akonadi::Collection &collection, const QString &label)
Sets a custom label that will be used when showing the favorite collection.
virtual bool hasFormat(const QString &mimeType) const const
bool isValid() const const
void reserve(int size)
int row() const const
virtual QStringList mimeTypes() const const override
QList< QUrl > urls() const const
DropAction
void rowsInserted(const QModelIndex &parent, int first, int last)
void setFilterBehavior(FilterBehavior behavior)
A model that lists a set of favorite collections.
Helper for checking MIME types of Collections and Items.
~FavoriteCollectionsModel() override
Destroys the favorite collections model.
virtual QModelIndex index(int row, int column, const QModelIndex &parent) const const=0
int count(const T &value) const const
static QModelIndex modelIndexForCollection(const QAbstractItemModel *model, const Collection &collection)
Returns a QModelIndex in model which points to collection.
QModelIndex parent() const const
QChar * data()
virtual QString errorString() const
int error() const
qint64 Id
Describes the unique id type.
Definition: collection.h:79
QObject * parent() const const
const QList< QKeySequence > & end()
Represents a PIM item stored in Akonadi storage.
Definition: item.h:104
QString toString() const const
Helper integration between Akonadi and Qt.
This file is part of the KDE documentation.
Documentation copyright © 1996-2022 The KDE developers.
Generated on Sat Jul 2 2022 06:41:48 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.