Akonadi

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

KDE's Doxygen guidelines are available online.