1/*
2 Copyright (c) 2006 - 2007 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#include "itemmodel.h"
21
22#include "itemfetchjob.h"
23#include "collectionfetchjob.h"
24#include "itemfetchscope.h"
25#include "monitor.h"
26#include "pastehelper_p.h"
27#include "session.h"
28
29#include <kdebug.h>
30#include <klocalizedstring.h>
31#include <kurl.h>
32
33#include <QCoreApplication>
34#include <QtCore/QDebug>
35#include <QtCore/QMimeData>
36
37using namespace Akonadi;
38
39/**
40 * @internal
41 *
42 * This struct is used for optimization reasons.
43 * because it embeds the row.
44 *
45 * Semantically, we could have used an item instead.
46 */
47struct ItemContainer
48{
49 ItemContainer(const Item &i, int r)
50 : item(i)
51 , row(r)
52 {
53 }
54 Item item;
55 int row;
56};
57
58/**
59 * @internal
60 */
61class ItemModel::Private
62{
63public:
64 Private(ItemModel *parent)
65 : mParent(parent)
66 , monitor(new Monitor())
67 {
68 session = new Session(QCoreApplication::instance()->applicationName().toUtf8()
69 + QByteArray("-ItemModel-") + QByteArray::number(qrand()), mParent);
70
71 monitor->ignoreSession(session);
72
73 mParent->connect(monitor, SIGNAL(itemChanged(Akonadi::Item,QSet<QByteArray>)),
74 mParent, SLOT(itemChanged(Akonadi::Item,QSet<QByteArray>)));
75 mParent->connect(monitor, SIGNAL(itemMoved(Akonadi::Item,Akonadi::Collection,Akonadi::Collection)),
76 mParent, SLOT(itemMoved(Akonadi::Item,Akonadi::Collection,Akonadi::Collection)));
77 mParent->connect(monitor, SIGNAL(itemAdded(Akonadi::Item,Akonadi::Collection)),
78 mParent, SLOT(itemAdded(Akonadi::Item)));
79 mParent->connect(monitor, SIGNAL(itemRemoved(Akonadi::Item)),
80 mParent, SLOT(itemRemoved(Akonadi::Item)));
81 mParent->connect(monitor, SIGNAL(itemLinked(Akonadi::Item,Akonadi::Collection)),
82 mParent, SLOT(itemAdded(Akonadi::Item)));
83 mParent->connect(monitor, SIGNAL(itemUnlinked(Akonadi::Item,Akonadi::Collection)),
84 mParent, SLOT(itemRemoved(Akonadi::Item)));
85 }
86
87 ~Private()
88 {
89 delete monitor;
90 }
91
92 void listingDone(KJob *job);
93 void collectionFetchResult(KJob *job);
94 void itemChanged(const Akonadi::Item &item, const QSet<QByteArray> &);
95 void itemsAdded(const Akonadi::Item::List &list);
96 void itemAdded(const Akonadi::Item &item);
97 void itemMoved(const Akonadi::Item &item, const Akonadi::Collection &src, const Akonadi::Collection &dst);
98 void itemRemoved(const Akonadi::Item &item);
99 int rowForItem(const Akonadi::Item &item);
100 bool collectionIsCompatible() const;
101
102 ItemModel *mParent;
103
104 QList<ItemContainer *> items;
105 QHash<Item, ItemContainer *> itemHash;
106
107 Collection collection;
108 Monitor *monitor;
109 Session *session;
110};
111
112bool ItemModel::Private::collectionIsCompatible() const
113{
114 // in the generic case, we show any collection
115 if (mParent->mimeTypes() == QStringList(QLatin1String("text/uri-list"))) {
116 return true;
117 }
118 // if the model's mime types are more specific, limit to those
119 // collections that have matching types
120 Q_FOREACH (const QString &type, mParent->mimeTypes()) {
121 if (collection.contentMimeTypes().contains(type)) {
122 return true;
123 }
124 }
125 return false;
126}
127
128void ItemModel::Private::listingDone(KJob *job)
129{
130 ItemFetchJob *fetch = static_cast<ItemFetchJob *>(job);
131 Q_UNUSED(fetch);
132 if (job->error()) {
133 // TODO
134 kWarning() << "Item query failed:" << job->errorString();
135 }
136}
137
138void ItemModel::Private::collectionFetchResult(KJob *job)
139{
140 CollectionFetchJob *fetch = static_cast<CollectionFetchJob *>(job);
141
142 if (fetch->collections().isEmpty()) {
143 return;
144 }
145
146 Q_ASSERT(fetch->collections().count() == 1); // we only listed base
147 Collection c = fetch->collections().first();
148 // avoid recursion, if this fails for some reason
149 if (!c.contentMimeTypes().isEmpty()) {
150 mParent->setCollection(c);
151 } else {
152 kWarning() << "Failed to retrieve the contents mime type of the collection: " << c;
153 mParent->setCollection(Collection());
154 }
155}
156
157int ItemModel::Private::rowForItem(const Akonadi::Item &item)
158{
159 ItemContainer *container = itemHash.value(item);
160 if (!container) {
161 return -1;
162 }
163
164 /* Try to find the item directly;
165
166 If items have been removed, this first try won't succeed because
167 the ItemContainer rows have not been updated (costs too much).
168 */
169 if (container->row < items.count()
170 && items.at(container->row) == container) {
171 return container->row;
172 } else {
173 // Slow solution if the fist one has not succeeded
174 int row = -1;
175 const int numberOfItems(items.size());
176 for (int i = 0; i < numberOfItems; ++i) {
177 if (items.at(i)->item == item) {
178 row = i;
179 break;
180 }
181 }
182 return row;
183 }
184
185}
186
187void ItemModel::Private::itemChanged(const Akonadi::Item &item, const QSet<QByteArray> &)
188{
189 int row = rowForItem(item);
190 if (row < 0) {
191 return;
192 }
193
194 items[row]->item = item;
195 itemHash.remove(item);
196 itemHash[item] = items[row];
197
198 QModelIndex start = mParent->index(row, 0, QModelIndex());
199 QModelIndex end = mParent->index(row, mParent->columnCount(QModelIndex()) - 1 , QModelIndex());
200
201 mParent->dataChanged(start, end);
202}
203
204void ItemModel::Private::itemMoved(const Akonadi::Item &item, const Akonadi::Collection &colSrc, const Akonadi::Collection &colDst)
205{
206 if (colSrc == collection && colDst != collection) {
207 // item leaving this model
208 itemRemoved(item);
209 return;
210 }
211
212 if (colDst == collection && colSrc != collection) {
213 itemAdded(item);
214 return;
215 }
216}
217
218void ItemModel::Private::itemsAdded(const Akonadi::Item::List &list)
219{
220 if (list.isEmpty()) {
221 return;
222 }
223 mParent->beginInsertRows(QModelIndex(), items.count(), items.count() + list.count() - 1);
224 foreach (const Item &item, list) {
225 ItemContainer *c = new ItemContainer(item, items.count());
226 items.append(c);
227 itemHash[item] = c;
228 }
229 mParent->endInsertRows();
230}
231
232void ItemModel::Private::itemAdded(const Akonadi::Item &item)
233{
234 Item::List l;
235 l << item;
236 itemsAdded(l);
237}
238
239void ItemModel::Private::itemRemoved(const Akonadi::Item &_item)
240{
241 int row = rowForItem(_item);
242 if (row < 0) {
243 return;
244 }
245
246 mParent->beginRemoveRows(QModelIndex(), row, row);
247 const Item item = items.at(row)->item;
248 Q_ASSERT(item.isValid());
249 itemHash.remove(item);
250 delete items.takeAt(row);
251 mParent->endRemoveRows();
252}
253
254ItemModel::ItemModel(QObject *parent)
255 : QAbstractTableModel(parent)
256 , d(new Private(this))
257{
258}
259
260ItemModel::~ItemModel()
261{
262 delete d;
263}
264
265QVariant ItemModel::data(const QModelIndex &index, int role) const
266{
267 if (!index.isValid()) {
268 return QVariant();
269 }
270 if (index.row() >= d->items.count()) {
271 return QVariant();
272 }
273 const Item item = d->items.at(index.row())->item;
274 if (!item.isValid()) {
275 return QVariant();
276 }
277
278 if (role == Qt::DisplayRole) {
279 switch (index.column()) {
280 case Id:
281 return QString::number(item.id());
282 case RemoteId:
283 return item.remoteId();
284 case MimeType:
285 return item.mimeType();
286 default:
287 return QVariant();
288 }
289 }
290
291 if (role == IdRole) {
292 return item.id();
293 }
294
295 if (role == ItemRole) {
296 QVariant var;
297 var.setValue(item);
298 return var;
299 }
300
301 if (role == MimeTypeRole) {
302 return item.mimeType();
303 }
304
305 return QVariant();
306}
307
308int ItemModel::rowCount(const QModelIndex &parent) const
309{
310 if (!parent.isValid()) {
311 return d->items.count();
312 }
313 return 0;
314}
315
316int ItemModel::columnCount(const QModelIndex &parent) const
317{
318 if (!parent.isValid()) {
319 return 3; // keep in sync with Column enum
320 }
321 return 0;
322}
323
324QVariant ItemModel::headerData(int section, Qt::Orientation orientation, int role) const
325{
326 if (orientation == Qt::Horizontal && role == Qt::DisplayRole) {
327 switch (section) {
328 case Id:
329 return i18n("Id");
330 case RemoteId:
331 return i18n("Remote Id");
332 case MimeType:
333 return i18n("MimeType");
334 default:
335 return QString();
336 }
337 }
338 return QAbstractTableModel::headerData(section, orientation, role);
339}
340
341void ItemModel::setCollection(const Collection &collection)
342{
343 kDebug();
344 if (d->collection == collection) {
345 return;
346 }
347
348 // if we don't know anything about this collection yet, fetch it
349 if (collection.isValid() && collection.contentMimeTypes().isEmpty()) {
350 CollectionFetchJob *job = new CollectionFetchJob(collection, CollectionFetchJob::Base, this);
351 connect(job, SIGNAL(result(KJob*)), this, SLOT(collectionFetchResult(KJob*)));
352 return;
353 }
354
355 d->monitor->setCollectionMonitored(d->collection, false);
356
357 d->collection = collection;
358
359 d->monitor->setCollectionMonitored(d->collection, true);
360
361 // the query changed, thus everything we have already is invalid
362 qDeleteAll(d->items);
363 d->items.clear();
364 reset();
365
366 // stop all running jobs
367 d->session->clear();
368
369 // start listing job
370 if (d->collectionIsCompatible()) {
371 ItemFetchJob *job = new ItemFetchJob(collection, session());
372 job->setFetchScope(d->monitor->itemFetchScope());
373 connect(job, SIGNAL(itemsReceived(Akonadi::Item::List)),
374 SLOT(itemsAdded(Akonadi::Item::List)));
375 connect(job, SIGNAL(result(KJob*)), SLOT(listingDone(KJob*)));
376 }
377
378 emit collectionChanged(collection);
379}
380
381void ItemModel::setFetchScope(const ItemFetchScope &fetchScope)
382{
383 d->monitor->setItemFetchScope(fetchScope);
384}
385
386ItemFetchScope &ItemModel::fetchScope()
387{
388 return d->monitor->itemFetchScope();
389}
390
391Item ItemModel::itemForIndex(const QModelIndex &index) const
392{
393 if (!index.isValid()) {
394 return Akonadi::Item();
395 }
396
397 if (index.row() >= d->items.count()) {
398 return Akonadi::Item();
399 }
400
401 Item item = d->items.at(index.row())->item;
402 if (item.isValid()) {
403 return item;
404 } else {
405 return Akonadi::Item();
406 }
407}
408
409Qt::ItemFlags ItemModel::flags(const QModelIndex &index) const
410{
411 Qt::ItemFlags defaultFlags = QAbstractTableModel::flags(index);
412
413 if (index.isValid()) {
414 return Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | defaultFlags;
415 } else {
416 return Qt::ItemIsDropEnabled | defaultFlags;
417 }
418}
419
420QStringList ItemModel::mimeTypes() const
421{
422 return QStringList() << QLatin1String("text/uri-list");
423}
424
425Session *ItemModel::session() const
426{
427 return d->session;
428}
429
430QMimeData *ItemModel::mimeData(const QModelIndexList &indexes) const
431{
432 QMimeData *data = new QMimeData();
433 // Add item uri to the mimedata for dropping in external applications
434 KUrl::List urls;
435 foreach (const QModelIndex &index, indexes) {
436 if (index.column() != 0) {
437 continue;
438 }
439
440 urls << itemForIndex(index).url(Item::UrlWithMimeType);
441 }
442 urls.populateMimeData(data);
443
444 return data;
445}
446
447QModelIndex ItemModel::indexForItem(const Akonadi::Item &item, const int column) const
448{
449 return index(d->rowForItem(item), column);
450}
451
452bool ItemModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent)
453{
454 Q_UNUSED(row);
455 Q_UNUSED(column);
456 Q_UNUSED(parent);
457 KJob *job = PasteHelper::paste(data, d->collection, action != Qt::MoveAction);
458 // TODO: error handling
459 return job;
460}
461
462Collection ItemModel::collection() const
463{
464 return d->collection;
465}
466
467Qt::DropActions ItemModel::supportedDropActions() const
468{
469 return Qt::CopyAction | Qt::MoveAction | Qt::LinkAction;
470}
471
472#include "moc_itemmodel.cpp"
473