1/*
2 Copyright (c) 2006 - 2008 Volker Krause <vkrause@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//@cond PRIVATE
21
22#include "collectionmodel_p.h"
23#include "collectionmodel.h"
24#include "collectionutils_p.h"
25
26#include "collectionfetchjob.h"
27#include "collectionstatistics.h"
28#include "collectionstatisticsjob.h"
29#include "monitor.h"
30#include "session.h"
31
32#include <kdebug.h>
33#include <kjob.h>
34#include <kiconloader.h>
35
36#include <QCoreApplication>
37#include <QtCore/QTimer>
38#include "collectionfetchscope.h"
39
40using namespace Akonadi;
41
42void CollectionModelPrivate::collectionRemoved(const Akonadi::Collection &collection)
43{
44 Q_Q(CollectionModel);
45 QModelIndex colIndex = indexForId(collection.id());
46 if (colIndex.isValid()) {
47 QModelIndex parentIndex = q->parent(colIndex);
48 // collection is still somewhere in the hierarchy
49 removeRowFromModel(colIndex.row(), parentIndex);
50 } else {
51 if (collections.contains(collection.id())) {
52 // collection is orphan, ie. the parent has been removed already
53 collections.remove(collection.id());
54 childCollections.remove(collection.id());
55 }
56 }
57}
58
59void CollectionModelPrivate::collectionChanged(const Akonadi::Collection &collection)
60{
61 Q_Q(CollectionModel);
62 // What kind of change is it ?
63 Collection::Id oldParentId = collections.value(collection.id()).parentCollection().id();
64 Collection::Id newParentId = collection.parentCollection().id();
65 if (newParentId != oldParentId && oldParentId >= 0) { // It's a move
66 removeRowFromModel(indexForId(collections[collection.id()].id()).row(), indexForId(oldParentId));
67 Collection newParent;
68 if (newParentId == Collection::root().id()) {
69 newParent = Collection::root();
70 } else {
71 newParent = collections.value(newParentId);
72 }
73 CollectionFetchJob *job = new CollectionFetchJob(newParent, CollectionFetchJob::Recursive, session);
74 job->fetchScope().setIncludeUnsubscribed(unsubscribed);
75 job->fetchScope().setIncludeStatistics(fetchStatistics);
76 q->connect(job, SIGNAL(collectionsReceived(Akonadi::Collection::List)),
77 q, SLOT(collectionsChanged(Akonadi::Collection::List)));
78 q->connect(job, SIGNAL(result(KJob*)),
79 q, SLOT(listDone(KJob*)));
80
81 } else { // It's a simple change
82 CollectionFetchJob *job = new CollectionFetchJob(collection, CollectionFetchJob::Base, session);
83 job->fetchScope().setIncludeUnsubscribed(unsubscribed);
84 job->fetchScope().setIncludeStatistics(fetchStatistics);
85 q->connect(job, SIGNAL(collectionsReceived(Akonadi::Collection::List)),
86 q, SLOT(collectionsChanged(Akonadi::Collection::List)));
87 q->connect(job, SIGNAL(result(KJob*)),
88 q, SLOT(listDone(KJob*)));
89 }
90
91}
92
93void CollectionModelPrivate::updateDone(KJob *job)
94{
95 if (job->error()) {
96 // TODO: handle job errors
97 kWarning() << "Job error:" << job->errorString();
98 } else {
99 CollectionStatisticsJob *csjob = static_cast<CollectionStatisticsJob *>(job);
100 Collection result = csjob->collection();
101 collectionStatisticsChanged(result.id(), csjob->statistics());
102 }
103}
104
105void CollectionModelPrivate::collectionStatisticsChanged(Collection::Id collection,
106 const Akonadi::CollectionStatistics &statistics)
107{
108 Q_Q(CollectionModel);
109
110 if (!collections.contains(collection)) {
111 kWarning() << "Got statistics response for non-existing collection:" << collection;
112 } else {
113 collections[collection].setStatistics(statistics);
114
115 Collection col = collections.value(collection);
116 QModelIndex startIndex = indexForId(col.id());
117 QModelIndex endIndex = indexForId(col.id(), q->columnCount(q->parent(startIndex)) - 1);
118 emit q->dataChanged(startIndex, endIndex);
119 }
120}
121
122void CollectionModelPrivate::listDone(KJob *job)
123{
124 if (job->error()) {
125 kWarning() << "Job error: " << job->errorString() << endl;
126 }
127}
128
129void CollectionModelPrivate::editDone(KJob *job)
130{
131 if (job->error()) {
132 kWarning() << "Edit failed: " << job->errorString();
133 }
134}
135
136void CollectionModelPrivate::dropResult(KJob *job)
137{
138 if (job->error()) {
139 kWarning() << "Paste failed:" << job->errorString();
140 // TODO: error handling
141 }
142}
143
144void CollectionModelPrivate::collectionsChanged(const Collection::List &cols)
145{
146 Q_Q(CollectionModel);
147
148 foreach (Collection col, cols) { //krazy:exclude=foreach non-const is needed here
149 if (collections.contains(col.id())) {
150 // If the collection is already known to the model, we simply update it...
151 col.setStatistics(collections.value(col.id()).statistics());
152 collections[col.id()] = col;
153 QModelIndex startIndex = indexForId(col.id());
154 QModelIndex endIndex = indexForId(col.id(), q->columnCount(q->parent(startIndex)) - 1);
155 emit q->dataChanged(startIndex, endIndex);
156 continue;
157 }
158 // ... otherwise we add it to the set of collections we need to handle.
159 m_newChildCollections[col.parentCollection().id()].append(col.id());
160 m_newCollections.insert(col.id(), col);
161 }
162
163 // Handle the collections in m_newChildCollections. If the collections
164 // parent is already in the model, the collection can be added to the model.
165 // Otherwise it is persisted until it has a valid parent in the model.
166 int currentSize = m_newChildCollections.size();
167 int lastSize = -1;
168
169 while (currentSize > 0) {
170 lastSize = currentSize;
171
172 QMutableHashIterator< Collection::Id, QVector< Collection::Id > > i(m_newChildCollections);
173 while (i.hasNext()) {
174 i.next();
175
176 // the key is the parent of new collections. It may itself also be new,
177 // but that will be handled later.
178 Collection::Id colId = i.key();
179
180 QVector< Collection::Id > newChildCols = i.value();
181 int newChildCount = newChildCols.size();
182// if ( newChildCount == 0 )
183// {
184// // Sanity check.
185// kDebug() << "No new child collections have been added to the collection:" << colId;
186// i.remove();
187// currentSize--;
188// break;
189// }
190
191 if (collections.contains(colId) || colId == Collection::root().id()) {
192 QModelIndex parentIndex = indexForId(colId);
193 int currentChildCount = childCollections.value(colId).size();
194
195 q->beginInsertRows(parentIndex,
196 currentChildCount, // Start index is at the end of existing collections.
197 currentChildCount + newChildCount - 1); // End index is the result of the insertion.
198
199 foreach (Collection::Id id, newChildCols) {
200 Collection c = m_newCollections.take(id);
201 collections.insert(id, c);
202 }
203
204 childCollections[colId] << newChildCols;
205 q->endInsertRows();
206 i.remove();
207 currentSize--;
208 break;
209 }
210 }
211
212 // We iterated through once without adding any more collections to the model.
213 if (currentSize == lastSize) {
214 // The remaining collections in the list do not have a valid parent in the model yet. They
215 // might arrive in the next batch from the monitor, so they're still in m_newCollections
216 // and m_newChildCollections.
217 kDebug() << "Some collections did not have a parent in the model yet!";
218 break;
219 }
220 }
221}
222
223QModelIndex CollectionModelPrivate::indexForId(Collection::Id id, int column) const
224{
225 Q_Q(const CollectionModel);
226 if (!collections.contains(id)) {
227 return QModelIndex();
228 }
229
230 Collection::Id parentId = collections.value(id).parentCollection().id();
231 // check if parent still exist or if this is an orphan collection
232 if (parentId != Collection::root().id() && !collections.contains(parentId)) {
233 return QModelIndex();
234 }
235
236 QVector<Collection::Id> list = childCollections.value(parentId);
237 int row = list.indexOf(id);
238
239 if (row >= 0) {
240 return q->createIndex(row, column, reinterpret_cast<void *>(collections.value(list.at(row)).id()));
241 }
242 return QModelIndex();
243}
244
245bool CollectionModelPrivate::removeRowFromModel(int row, const QModelIndex &parent)
246{
247 Q_Q(CollectionModel);
248 QVector<Collection::Id> list;
249 Collection parentCol;
250 if (parent.isValid()) {
251 parentCol = collections.value(parent.internalId());
252 Q_ASSERT(parentCol.id() == parent.internalId());
253 list = childCollections.value(parentCol.id());
254 } else {
255 parentCol = Collection::root();
256 list = childCollections.value(Collection::root().id());
257 }
258 if (row < 0 || row >= list.size()) {
259 kWarning() << "Index out of bounds:" << row << " parent:" << parentCol.id();
260 return false;
261 }
262
263 q->beginRemoveRows(parent, row, row);
264 const Collection::Id delColId = list[row];
265 list.remove(row);
266 foreach (Collection::Id childColId, childCollections[delColId]) {
267 collections.remove(childColId);
268 }
269 collections.remove(delColId);
270 childCollections.remove(delColId); // remove children of deleted collection
271 childCollections.insert(parentCol.id(), list); // update children of parent
272 q->endRemoveRows();
273
274 return true;
275}
276
277bool CollectionModelPrivate::supportsContentType(const QModelIndex &index, const QStringList &contentTypes)
278{
279 if (!index.isValid()) {
280 return false;
281 }
282 Collection col = collections.value(index.internalId());
283 Q_ASSERT(col.isValid());
284 QStringList ct = col.contentMimeTypes();
285 foreach (const QString &a, ct) {
286 if (contentTypes.contains(a)) {
287 return true;
288 }
289 }
290 return false;
291}
292
293void CollectionModelPrivate::init()
294{
295 Q_Q(CollectionModel);
296
297 session = new Session(QCoreApplication::instance()->applicationName().toUtf8()
298 + QByteArray("-CollectionModel-") + QByteArray::number(qrand()), q);
299 QTimer::singleShot(0, q, SLOT(startFirstListJob()));
300
301 // monitor collection changes
302 monitor = new Monitor();
303 monitor->setCollectionMonitored(Collection::root());
304 monitor->fetchCollection(true);
305
306 // ### Hack to get the kmail resource folder icons
307 KIconLoader::global()->addAppDir(QLatin1String("kmail"));
308 KIconLoader::global()->addAppDir(QLatin1String("kdepim"));
309
310 // monitor collection changes
311 q->connect(monitor, SIGNAL(collectionChanged(Akonadi::Collection)),
312 q, SLOT(collectionChanged(Akonadi::Collection)));
313 q->connect(monitor, SIGNAL(collectionAdded(Akonadi::Collection,Akonadi::Collection)),
314 q, SLOT(collectionChanged(Akonadi::Collection)));
315 q->connect(monitor, SIGNAL(collectionRemoved(Akonadi::Collection)),
316 q, SLOT(collectionRemoved(Akonadi::Collection)));
317 q->connect(monitor, SIGNAL(collectionStatisticsChanged(Akonadi::Collection::Id,Akonadi::CollectionStatistics)),
318 q, SLOT(collectionStatisticsChanged(Akonadi::Collection::Id,Akonadi::CollectionStatistics)));
319}
320
321void CollectionModelPrivate::startFirstListJob()
322{
323 Q_Q(CollectionModel);
324
325 // start a list job
326 CollectionFetchJob *job = new CollectionFetchJob(Collection::root(), CollectionFetchJob::Recursive, session);
327 job->fetchScope().setIncludeUnsubscribed(unsubscribed);
328 job->fetchScope().setIncludeStatistics(fetchStatistics);
329 q->connect(job, SIGNAL(collectionsReceived(Akonadi::Collection::List)),
330 q, SLOT(collectionsChanged(Akonadi::Collection::List)));
331 q->connect(job, SIGNAL(result(KJob*)), q, SLOT(listDone(KJob*)));
332}
333
334//@endcond
335