1/*
2 Copyright (c) 2009 Kevin Ottens <ervin@kde.org>
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
22#include <QItemSelectionModel>
23#include <QtCore/QMimeData>
24
25#include <kconfiggroup.h>
26#include <klocale.h>
27#include <klocalizedstring.h>
28#include <KJob>
29#include <KUrl>
30
31#include "entitytreemodel.h"
32#include "mimetypechecker.h"
33#include "pastehelper_p.h"
34
35using namespace Akonadi;
36
37/**
38 * @internal
39 */
40class FavoriteCollectionsModel::Private
41{
42public:
43 Private(const KConfigGroup &group, FavoriteCollectionsModel *parent)
44 : q(parent)
45 , configGroup(group)
46 {
47 }
48
49 QString labelForCollection(Collection::Id collectionId) const
50 {
51 if (labelMap.contains(collectionId)) {
52 return labelMap[collectionId];
53 }
54
55 const QModelIndex collectionIdx = EntityTreeModel::modelIndexForCollection(q->sourceModel(), Collection(collectionId));
56
57 QString accountName;
58
59 const QString nameOfCollection = collectionIdx.data().toString();
60
61 QModelIndex idx = collectionIdx.parent();
62 while (idx != QModelIndex()) {
63 accountName = idx.data().toString();
64 idx = idx.parent();
65 }
66
67 if (accountName.isEmpty()) {
68 return nameOfCollection;
69 } else {
70 return nameOfCollection + QLatin1String(" (") + accountName + QLatin1Char(')');
71 }
72 }
73
74 void insertIfAvailable(Collection::Id col)
75 {
76 if (collectionIds.contains(col)) {
77 select(col);
78 if (!referencedCollections.contains(col)) {
79 reference(col);
80 }
81 }
82 }
83
84 void insertIfAvailable(const QModelIndex &idx)
85 {
86 insertIfAvailable(idx.data(EntityTreeModel::CollectionIdRole).value<Collection::Id>());
87 }
88
89 /**
90 * Stuff changed, reload everything.
91 */
92 void reload()
93 {
94 //don't clear the selection model here. Otherwise we mess up the users selection as collections get removed and re-inserted.
95 foreach (const Collection::Id &collectionId, collectionIds) {
96 insertIfAvailable(collectionId);
97 }
98 //TODO remove what's no longer here
99 }
100
101 void rowsInserted(const QModelIndex &parent, int begin, int end)
102 {
103 for (int row = begin; row <= end; row++) {
104 const QModelIndex child = parent.child(row, 0);
105 if (!child.isValid()) {
106 continue;
107 }
108 insertIfAvailable(child);
109 const int childRows = q->sourceModel()->rowCount(child);
110 if (childRows > 0) {
111 rowsInserted(child, 0, childRows - 1);
112 }
113 }
114 }
115
116 void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight)
117 {
118 for (int row = topLeft.row(); row <= bottomRight.row(); row++) {
119 const QModelIndex idx = topLeft.parent().child(row, 0);
120 insertIfAvailable(idx);
121 }
122 }
123
124 /**
125 * Selects the index in the internal selection model to make the collection visible in the model
126 */
127 void select(const Collection::Id &collectionId)
128 {
129 const QModelIndex index = EntityTreeModel::modelIndexForCollection(q->sourceModel(), Collection(collectionId));
130 if (index.isValid()) {
131 q->selectionModel()->select(index, QItemSelectionModel::Select);
132 }
133 }
134
135 void deselect(const Collection::Id &collectionId)
136 {
137 const QModelIndex idx = EntityTreeModel::modelIndexForCollection(q->sourceModel(), Collection(collectionId));
138 if (idx.isValid()) {
139 q->selectionModel()->select(idx, QItemSelectionModel::Deselect);
140 }
141 }
142
143 void reference(const Collection::Id &collectionId)
144 {
145 if (referencedCollections.contains(collectionId)) {
146 kWarning() << "already referenced " << collectionId;
147 return;
148 }
149 const QModelIndex index = EntityTreeModel::modelIndexForCollection(q->sourceModel(), Collection(collectionId));
150 if (index.isValid()) {
151 if (q->sourceModel()->setData(index, QVariant(), EntityTreeModel::CollectionRefRole)) {
152 referencedCollections << collectionId;
153 } else {
154 kWarning() << "failed to reference collection";
155 }
156 q->sourceModel()->fetchMore(index);
157 }
158 }
159
160 void dereference(const Collection::Id &collectionId)
161 {
162 if (!referencedCollections.contains(collectionId)) {
163 kWarning() << "not referenced " << collectionId;
164 return;
165 }
166 const QModelIndex index = EntityTreeModel::modelIndexForCollection(q->sourceModel(), Collection(collectionId));
167 if (index.isValid()) {
168 q->sourceModel()->setData(index, QVariant(), EntityTreeModel::CollectionDerefRole);
169 referencedCollections.remove(collectionId);
170 }
171 }
172
173 void clearReferences()
174 {
175 foreach (const Collection::Id &collectionId, referencedCollections) {
176 dereference(collectionId);
177 }
178 }
179
180 /**
181 * Adds a collection to the favorite collections
182 */
183 void add(const Collection::Id &collectionId)
184 {
185 if (collectionIds.contains(collectionId)) {
186 kDebug() << "already in model " << collectionId;
187 return;
188 }
189 collectionIds << collectionId;
190 reference(collectionId);
191 select(collectionId);
192 }
193
194 void remove(const Collection::Id &collectionId)
195 {
196 collectionIds.removeAll(collectionId);
197 labelMap.remove(collectionId);
198 dereference(collectionId);
199 deselect(collectionId);
200 }
201
202 void set(const QList<Collection::Id> &collections)
203 {
204 QList<Collection::Id> colIds = collectionIds;
205 foreach (const Collection::Id &col, collections) {
206 const int removed = colIds.removeAll(col);
207 const bool isNewCollection = removed <= 0;
208 if (isNewCollection) {
209 add(col);
210 }
211 }
212 //Remove what's left
213 foreach (const Akonadi::Collection::Id &colId, colIds) {
214 remove(colId);
215 }
216 }
217
218 void set(const Akonadi::Collection::List &collections)
219 {
220 QList<Akonadi::Collection::Id> colIds;
221 foreach (const Akonadi::Collection &col, collections) {
222 colIds << col.id();
223 }
224 set(colIds);
225 }
226
227 void loadConfig()
228 {
229 const QList<Collection::Id> collections = configGroup.readEntry("FavoriteCollectionIds", QList<qint64>());
230 const QStringList labels = configGroup.readEntry("FavoriteCollectionLabels", QStringList());
231 const int numberOfLabels(labels.size());
232 for (int i = 0; i < collections.size(); ++i) {
233 if (i < numberOfLabels) {
234 labelMap[collections[i]] = labels[i];
235 }
236 add(collections[i]);
237 }
238 }
239
240 void saveConfig()
241 {
242 QStringList labels;
243
244 foreach (const Collection::Id &collectionId, collectionIds) {
245 labels << labelForCollection(collectionId);
246 }
247
248 configGroup.writeEntry("FavoriteCollectionIds", collectionIds);
249 configGroup.writeEntry("FavoriteCollectionLabels", labels);
250 configGroup.config()->sync();
251 }
252
253 FavoriteCollectionsModel *const q;
254
255 QList<Collection::Id> collectionIds;
256 QSet<Collection::Id> referencedCollections;
257 QHash<qint64, QString> labelMap;
258 KConfigGroup configGroup;
259};
260
261FavoriteCollectionsModel::FavoriteCollectionsModel(QAbstractItemModel *source, const KConfigGroup &group, QObject *parent)
262 : Akonadi::SelectionProxyModel(new QItemSelectionModel(source, parent), parent)
263 , d(new Private(group, this))
264{
265 //This should only be a KRecursiveFilterProxyModel, but remains a SelectionProxyModel for backwards compatiblity.
266 // We therefore disable what we anyways don't want (the referencing is handled separately).
267 disconnect(this, SIGNAL(rootIndexAdded(QModelIndex)), this, SLOT(rootIndexAdded(QModelIndex)));
268 disconnect(this, SIGNAL(rootIndexAboutToBeRemoved(QModelIndex)), this, SLOT(rootIndexAboutToBeRemoved(QModelIndex)));
269
270 setSourceModel(source);
271 setFilterBehavior(ExactSelection);
272
273 d->loadConfig();
274 //React to various changes in the source model
275 connect(source, SIGNAL(modelReset()), this, SLOT(reload()));
276 connect(source, SIGNAL(layoutChanged()), this, SLOT(reload()));
277 connect(source, SIGNAL(rowsInserted(QModelIndex,int,int)), this, SLOT(rowsInserted(QModelIndex,int,int)));
278 connect(source, SIGNAL(dataChanged(QModelIndex,QModelIndex)), this, SLOT(dataChanged(QModelIndex,QModelIndex)));
279}
280
281FavoriteCollectionsModel::~FavoriteCollectionsModel()
282{
283 delete d;
284}
285
286void FavoriteCollectionsModel::setCollections(const Collection::List &collections)
287{
288 d->set(collections);
289 d->saveConfig();
290}
291
292void FavoriteCollectionsModel::addCollection(const Collection &collection)
293{
294 d->add(collection.id());
295 d->saveConfig();
296}
297
298void FavoriteCollectionsModel::removeCollection(const Collection &collection)
299{
300 d->remove(collection.id());
301 d->saveConfig();
302}
303
304Akonadi::Collection::List FavoriteCollectionsModel::collections() const
305{
306 Collection::List cols;
307 foreach (const Collection::Id &colId, d->collectionIds) {
308 const QModelIndex idx = EntityTreeModel::modelIndexForCollection(sourceModel(), Collection(colId));
309 const Collection collection = sourceModel()->data(idx, EntityTreeModel::CollectionRole).value<Collection>();
310 cols << collection;
311 }
312 return cols;
313}
314
315QList<Collection::Id> FavoriteCollectionsModel::collectionIds() const
316{
317 return d->collectionIds;
318}
319
320void Akonadi::FavoriteCollectionsModel::setFavoriteLabel(const Collection &collection, const QString &label)
321{
322 Q_ASSERT(d->collectionIds.contains(collection.id()));
323 d->labelMap[collection.id()] = label;
324 d->saveConfig();
325
326 const QModelIndex idx = EntityTreeModel::modelIndexForCollection(sourceModel(), collection);
327
328 if (!idx.isValid()) {
329 return;
330 }
331
332 const QModelIndex index = mapFromSource(idx);
333 emit dataChanged(index, index);
334}
335
336QVariant Akonadi::FavoriteCollectionsModel::data(const QModelIndex &index, int role) const
337{
338 if (index.column() == 0 &&
339 (role == Qt::DisplayRole ||
340 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 &&
353 role == Qt::EditRole) {
354 const QString newLabel = value.toString();
355 if (newLabel.isEmpty()) {
356 return false;
357 }
358 const QModelIndex sourceIndex = mapToSource(index);
359 const Collection collection = sourceModel()->data(sourceIndex, EntityTreeModel::CollectionRole).value<Collection>();
360 setFavoriteLabel(collection, newLabel);
361 return true;
362 }
363 return Akonadi::SelectionProxyModel::setData(index, value, role);
364}
365
366QString Akonadi::FavoriteCollectionsModel::favoriteLabel(const Akonadi::Collection &collection)
367{
368 if (!collection.isValid()) {
369 return QString();
370 }
371 return d->labelForCollection(collection.id());
372}
373
374QVariant FavoriteCollectionsModel::headerData(int section, Qt::Orientation orientation, int role) const
375{
376 if (section == 0 &&
377 orientation == Qt::Horizontal &&
378 role == Qt::DisplayRole) {
379 return i18n("Favorite Folders");
380 } else {
381 return KSelectionProxyModel::headerData(section, orientation, role);
382 }
383}
384
385bool FavoriteCollectionsModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent)
386{
387 Q_UNUSED(action);
388 Q_UNUSED(row);
389 Q_UNUSED(column);
390 if (data->hasFormat(QLatin1String("text/uri-list"))) {
391 const KUrl::List urls = KUrl::List::fromMimeData(data);
392
393 const QModelIndex sourceIndex = mapToSource(parent);
394 const Collection destCollection = sourceModel()->data(sourceIndex, EntityTreeModel::CollectionRole).value<Collection>();
395
396 MimeTypeChecker mimeChecker;
397 mimeChecker.setWantedMimeTypes(destCollection.contentMimeTypes());
398
399 foreach (const KUrl &url, urls) {
400 const Collection col = Collection::fromUrl(url);
401 if (col.isValid()) {
402 addCollection(col);
403 } else {
404 const Item item = Item::fromUrl(url);
405 if (item.isValid()) {
406 if (item.parentCollection().id() == destCollection.id() &&
407 action != Qt::CopyAction) {
408 kDebug() << "Error: source and destination of move are the same.";
409 return false;
410 }
411#if 0
412 if (!mimeChecker.isWantedItem(item)) {
413 kDebug() << "unwanted item" << mimeChecker.wantedMimeTypes() << item.mimeType();
414 return false;
415 }
416#endif
417 KJob *job = PasteHelper::pasteUriList(data, destCollection, action);
418 if (!job) {
419 return false;
420 }
421 connect(job, SIGNAL(result(KJob*)), SLOT(pasteJobDone(KJob*)));
422 // Accept the event so that it doesn't propagate.
423 return true;
424
425 }
426 }
427
428 }
429 return true;
430 }
431 return false;
432}
433
434QStringList FavoriteCollectionsModel::mimeTypes() const
435{
436 QStringList mts = Akonadi::SelectionProxyModel::mimeTypes();
437 if (!mts.contains(QLatin1String("text/uri-list"))) {
438 mts.append(QLatin1String("text/uri-list"));
439 }
440 return mts;
441}
442
443Qt::ItemFlags FavoriteCollectionsModel::flags(const QModelIndex &index) const
444{
445 Qt::ItemFlags fs = Akonadi::SelectionProxyModel::flags(index);
446 if (!index.isValid()) {
447 fs |= Qt::ItemIsDropEnabled;
448 }
449 return fs;
450}
451
452void FavoriteCollectionsModel::pasteJobDone(KJob *job)
453{
454 if (job->error()) {
455 kDebug() << job->errorString();
456 }
457}
458
459#include "moc_favoritecollectionsmodel.cpp"
460