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 | |
35 | using namespace Akonadi; |
36 | |
37 | /** |
38 | * @internal |
39 | */ |
40 | class FavoriteCollectionsModel::Private |
41 | { |
42 | public: |
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 | |
261 | FavoriteCollectionsModel::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 | |
281 | FavoriteCollectionsModel::~FavoriteCollectionsModel() |
282 | { |
283 | delete d; |
284 | } |
285 | |
286 | void FavoriteCollectionsModel::setCollections(const Collection::List &collections) |
287 | { |
288 | d->set(collections); |
289 | d->saveConfig(); |
290 | } |
291 | |
292 | void FavoriteCollectionsModel::addCollection(const Collection &collection) |
293 | { |
294 | d->add(collection.id()); |
295 | d->saveConfig(); |
296 | } |
297 | |
298 | void FavoriteCollectionsModel::removeCollection(const Collection &collection) |
299 | { |
300 | d->remove(collection.id()); |
301 | d->saveConfig(); |
302 | } |
303 | |
304 | Akonadi::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 | |
315 | QList<Collection::Id> FavoriteCollectionsModel::collectionIds() const |
316 | { |
317 | return d->collectionIds; |
318 | } |
319 | |
320 | void 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 | |
336 | QVariant 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 | |
350 | bool 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 | |
366 | QString Akonadi::FavoriteCollectionsModel::favoriteLabel(const Akonadi::Collection &collection) |
367 | { |
368 | if (!collection.isValid()) { |
369 | return QString(); |
370 | } |
371 | return d->labelForCollection(collection.id()); |
372 | } |
373 | |
374 | QVariant FavoriteCollectionsModel::(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 | |
385 | bool 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 | |
434 | QStringList 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 | |
443 | Qt::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 | |
452 | void FavoriteCollectionsModel::pasteJobDone(KJob *job) |
453 | { |
454 | if (job->error()) { |
455 | kDebug() << job->errorString(); |
456 | } |
457 | } |
458 | |
459 | #include "moc_favoritecollectionsmodel.cpp" |
460 | |