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 | |
37 | using 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 | */ |
47 | struct 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 | */ |
61 | class ItemModel::Private |
62 | { |
63 | public: |
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 | |
112 | bool 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 | |
128 | void 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 | |
138 | void 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 | |
157 | int 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 | |
187 | void 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 | |
204 | void 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 | |
218 | void 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 | |
232 | void ItemModel::Private::itemAdded(const Akonadi::Item &item) |
233 | { |
234 | Item::List l; |
235 | l << item; |
236 | itemsAdded(l); |
237 | } |
238 | |
239 | void 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 | |
254 | ItemModel::ItemModel(QObject *parent) |
255 | : QAbstractTableModel(parent) |
256 | , d(new Private(this)) |
257 | { |
258 | } |
259 | |
260 | ItemModel::~ItemModel() |
261 | { |
262 | delete d; |
263 | } |
264 | |
265 | QVariant 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 | |
308 | int ItemModel::rowCount(const QModelIndex &parent) const |
309 | { |
310 | if (!parent.isValid()) { |
311 | return d->items.count(); |
312 | } |
313 | return 0; |
314 | } |
315 | |
316 | int 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 | |
324 | QVariant ItemModel::(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 | |
341 | void 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 | |
381 | void ItemModel::setFetchScope(const ItemFetchScope &fetchScope) |
382 | { |
383 | d->monitor->setItemFetchScope(fetchScope); |
384 | } |
385 | |
386 | ItemFetchScope &ItemModel::fetchScope() |
387 | { |
388 | return d->monitor->itemFetchScope(); |
389 | } |
390 | |
391 | Item 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 | |
409 | Qt::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 | |
420 | QStringList ItemModel::mimeTypes() const |
421 | { |
422 | return QStringList() << QLatin1String("text/uri-list" ); |
423 | } |
424 | |
425 | Session *ItemModel::session() const |
426 | { |
427 | return d->session; |
428 | } |
429 | |
430 | QMimeData *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 | |
447 | QModelIndex ItemModel::indexForItem(const Akonadi::Item &item, const int column) const |
448 | { |
449 | return index(d->rowForItem(item), column); |
450 | } |
451 | |
452 | bool 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 | |
462 | Collection ItemModel::collection() const |
463 | { |
464 | return d->collection; |
465 | } |
466 | |
467 | Qt::DropActions ItemModel::supportedDropActions() const |
468 | { |
469 | return Qt::CopyAction | Qt::MoveAction | Qt::LinkAction; |
470 | } |
471 | |
472 | #include "moc_itemmodel.cpp" |
473 | |