]> cloud.milkyroute.net Git - dolphin.git/blob - src/kitemviews/kfileitemmodel.cpp
SVN_SILENT made messages (.desktop file)
[dolphin.git] / src / kitemviews / kfileitemmodel.cpp
1 /***************************************************************************
2 * Copyright (C) 2011 by Peter Penz <peter.penz19@gmail.com> *
3 * *
4 * This program is free software; you can redistribute it and/or modify *
5 * it under the terms of the GNU General Public License as published by *
6 * the Free Software Foundation; either version 2 of the License, or *
7 * (at your option) any later version. *
8 * *
9 * This program is distributed in the hope that it will be useful, *
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
12 * GNU General Public License for more details. *
13 * *
14 * You should have received a copy of the GNU General Public License *
15 * along with this program; if not, write to the *
16 * Free Software Foundation, Inc., *
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *
18 ***************************************************************************/
19
20 #include "kfileitemmodel.h"
21
22 #include <KDirLister>
23 #include <KDirModel>
24 #include <KLocale>
25 #include <KStringHandler>
26 #include <KDebug>
27
28 #include <QMimeData>
29 #include <QTimer>
30
31 #define KFILEITEMMODEL_DEBUG
32
33 KFileItemModel::KFileItemModel(KDirLister* dirLister, QObject* parent) :
34 KItemModelBase(QByteArray(), "name", parent),
35 m_dirLister(dirLister),
36 m_naturalSorting(true),
37 m_sortFoldersFirst(true),
38 m_groupRole(NoRole),
39 m_sortRole(NameRole),
40 m_caseSensitivity(Qt::CaseInsensitive),
41 m_sortedItems(),
42 m_items(),
43 m_data(),
44 m_requestRole(),
45 m_minimumUpdateIntervalTimer(0),
46 m_maximumUpdateIntervalTimer(0),
47 m_pendingItemsToInsert(),
48 m_pendingEmitLoadingCompleted(false),
49 m_rootExpansionLevel(-1),
50 m_expandedUrls(),
51 m_restoredExpandedUrls()
52 {
53 resetRoles();
54 m_requestRole[NameRole] = true;
55 m_requestRole[IsDirRole] = true;
56
57 Q_ASSERT(dirLister);
58
59 connect(dirLister, SIGNAL(canceled()), this, SLOT(slotCanceled()));
60 connect(dirLister, SIGNAL(completed()), this, SLOT(slotCompleted()));
61 connect(dirLister, SIGNAL(newItems(KFileItemList)), this, SLOT(slotNewItems(KFileItemList)));
62 connect(dirLister, SIGNAL(itemsDeleted(KFileItemList)), this, SLOT(slotItemsDeleted(KFileItemList)));
63 connect(dirLister, SIGNAL(refreshItems(QList<QPair<KFileItem,KFileItem> >)), this, SLOT(slotRefreshItems(QList<QPair<KFileItem,KFileItem> >)));
64 connect(dirLister, SIGNAL(clear()), this, SLOT(slotClear()));
65 connect(dirLister, SIGNAL(clear(KUrl)), this, SLOT(slotClear(KUrl)));
66
67 // Although the layout engine of KItemListView is fast it is very inefficient to e.g.
68 // emit 50 itemsInserted()-signals each 100 ms. m_minimumUpdateIntervalTimer assures that updates
69 // are done in 1 second intervals for equal operations.
70 m_minimumUpdateIntervalTimer = new QTimer(this);
71 m_minimumUpdateIntervalTimer->setInterval(1000);
72 m_minimumUpdateIntervalTimer->setSingleShot(true);
73 connect(m_minimumUpdateIntervalTimer, SIGNAL(timeout()), this, SLOT(dispatchPendingItemsToInsert()));
74
75 // For slow KIO-slaves like used for searching it makes sense to show results periodically even
76 // before the completed() or canceled() signal has been emitted.
77 m_maximumUpdateIntervalTimer = new QTimer(this);
78 m_maximumUpdateIntervalTimer->setInterval(2000);
79 m_maximumUpdateIntervalTimer->setSingleShot(true);
80 connect(m_maximumUpdateIntervalTimer, SIGNAL(timeout()), this, SLOT(dispatchPendingItemsToInsert()));
81
82 Q_ASSERT(m_minimumUpdateIntervalTimer->interval() <= m_maximumUpdateIntervalTimer->interval());
83 }
84
85 KFileItemModel::~KFileItemModel()
86 {
87 }
88
89 int KFileItemModel::count() const
90 {
91 return m_data.count();
92 }
93
94 QHash<QByteArray, QVariant> KFileItemModel::data(int index) const
95 {
96 if (index >= 0 && index < count()) {
97 return m_data.at(index);
98 }
99 return QHash<QByteArray, QVariant>();
100 }
101
102 bool KFileItemModel::setData(int index, const QHash<QByteArray, QVariant>& values)
103 {
104 if (index >= 0 && index < count()) {
105 QHash<QByteArray, QVariant> currentValue = m_data.at(index);
106
107 QSet<QByteArray> changedRoles;
108 QHashIterator<QByteArray, QVariant> it(values);
109 while (it.hasNext()) {
110 it.next();
111 const QByteArray role = it.key();
112 const QVariant value = it.value();
113
114 if (currentValue[role] != value) {
115 currentValue[role] = value;
116 changedRoles.insert(role);
117 }
118 }
119
120 if (!changedRoles.isEmpty()) {
121 m_data[index] = currentValue;
122 emit itemsChanged(KItemRangeList() << KItemRange(index, 1), changedRoles);
123 }
124
125 return true;
126 }
127 return false;
128 }
129
130 bool KFileItemModel::supportsGrouping() const
131 {
132 return true;
133 }
134
135 bool KFileItemModel::supportsSorting() const
136 {
137 return true;
138 }
139
140 void KFileItemModel::setSortFoldersFirst(bool foldersFirst)
141 {
142 if (foldersFirst != m_sortFoldersFirst) {
143 m_sortFoldersFirst = foldersFirst;
144 resortAllItems();
145 }
146 }
147
148 bool KFileItemModel::sortFoldersFirst() const
149 {
150 return m_sortFoldersFirst;
151 }
152
153 QMimeData* KFileItemModel::createMimeData(const QSet<int>& indexes) const
154 {
155 QMimeData* data = new QMimeData();
156
157 // The following code has been taken from KDirModel::mimeData()
158 // (kdelibs/kio/kio/kdirmodel.cpp)
159 // Copyright (C) 2006 David Faure <faure@kde.org>
160 KUrl::List urls;
161 KUrl::List mostLocalUrls;
162 bool canUseMostLocalUrls = true;
163
164 QSetIterator<int> it(indexes);
165 while (it.hasNext()) {
166 const int index = it.next();
167 const KFileItem item = fileItem(index);
168 if (!item.isNull()) {
169 urls << item.url();
170
171 bool isLocal;
172 mostLocalUrls << item.mostLocalUrl(isLocal);
173 if (!isLocal) {
174 canUseMostLocalUrls = false;
175 }
176 }
177 }
178
179 const bool different = canUseMostLocalUrls && mostLocalUrls != urls;
180 urls = KDirModel::simplifiedUrlList(urls); // TODO: Check if we still need KDirModel for this in KDE 5.0
181 if (different) {
182 mostLocalUrls = KDirModel::simplifiedUrlList(mostLocalUrls);
183 urls.populateMimeData(mostLocalUrls, data);
184 } else {
185 urls.populateMimeData(data);
186 }
187
188 return data;
189 }
190
191 int KFileItemModel::indexForKeyboardSearch(const QString& text, int startFromIndex) const
192 {
193 startFromIndex = qMax(0, startFromIndex);
194 for (int i = startFromIndex; i < count(); ++i) {
195 if (data(i)["name"].toString().startsWith(text, Qt::CaseInsensitive)) {
196 return i;
197 }
198 }
199 for (int i = 0; i < startFromIndex; ++i) {
200 if (data(i)["name"].toString().startsWith(text, Qt::CaseInsensitive)) {
201 return i;
202 }
203 }
204 return -1;
205 }
206
207 bool KFileItemModel::supportsDropping(int index) const
208 {
209 const KFileItem item = fileItem(index);
210 return item.isNull() ? false : item.isDir();
211 }
212
213 QString KFileItemModel::roleDescription(const QByteArray& role) const
214 {
215 QString descr;
216
217 switch (roleIndex(role)) {
218 case NameRole: descr = i18nc("@item:intable", "Name"); break;
219 case SizeRole: descr = i18nc("@item:intable", "Size"); break;
220 case DateRole: descr = i18nc("@item:intable", "Date"); break;
221 case PermissionsRole: descr = i18nc("@item:intable", "Permissions"); break;
222 case OwnerRole: descr = i18nc("@item:intable", "Owner"); break;
223 case GroupRole: descr = i18nc("@item:intable", "Group"); break;
224 case TypeRole: descr = i18nc("@item:intable", "Type"); break;
225 case DestinationRole: descr = i18nc("@item:intable", "Destination"); break;
226 case PathRole: descr = i18nc("@item:intable", "Path"); break;
227 case NoRole: break;
228 case IsDirRole: break;
229 case IsExpandedRole: break;
230 case ExpansionLevelRole: break;
231 default: Q_ASSERT(false); break;
232 }
233
234 return descr;
235 }
236
237 KFileItem KFileItemModel::fileItem(int index) const
238 {
239 if (index >= 0 && index < count()) {
240 return m_sortedItems.at(index);
241 }
242
243 return KFileItem();
244 }
245
246 KFileItem KFileItemModel::fileItem(const KUrl& url) const
247 {
248 const int index = m_items.value(url, -1);
249 if (index >= 0) {
250 return m_sortedItems.at(index);
251 }
252 return KFileItem();
253 }
254
255 int KFileItemModel::index(const KFileItem& item) const
256 {
257 if (item.isNull()) {
258 return -1;
259 }
260
261 return m_items.value(item.url(), -1);
262 }
263
264 int KFileItemModel::index(const KUrl& url) const
265 {
266 KUrl urlToFind = url;
267 urlToFind.adjustPath(KUrl::RemoveTrailingSlash);
268 return m_items.value(urlToFind, -1);
269 }
270
271 KFileItem KFileItemModel::rootItem() const
272 {
273 const KDirLister* dirLister = m_dirLister.data();
274 if (dirLister) {
275 return dirLister->rootItem();
276 }
277 return KFileItem();
278 }
279
280 void KFileItemModel::clear()
281 {
282 slotClear();
283 }
284
285 void KFileItemModel::setRoles(const QSet<QByteArray>& roles)
286 {
287 if (count() > 0) {
288 const bool supportedExpanding = m_requestRole[IsExpandedRole] && m_requestRole[ExpansionLevelRole];
289 const bool willSupportExpanding = roles.contains("isExpanded") && roles.contains("expansionLevel");
290 if (supportedExpanding && !willSupportExpanding) {
291 // No expanding is supported anymore. Take care to delete all items that have an expansion level
292 // that is not 0 (and hence are part of an expanded item).
293 removeExpandedItems();
294 }
295 }
296
297 resetRoles();
298 QSetIterator<QByteArray> it(roles);
299 while (it.hasNext()) {
300 const QByteArray& role = it.next();
301 m_requestRole[roleIndex(role)] = true;
302 }
303
304 if (count() > 0) {
305 // Update m_data with the changed requested roles
306 const int maxIndex = count() - 1;
307 for (int i = 0; i <= maxIndex; ++i) {
308 m_data[i] = retrieveData(m_sortedItems.at(i));
309 }
310
311 kWarning() << "TODO: Emitting itemsChanged() with no information what has changed!";
312 emit itemsChanged(KItemRangeList() << KItemRange(0, count()), QSet<QByteArray>());
313 }
314 }
315
316 QSet<QByteArray> KFileItemModel::roles() const
317 {
318 QSet<QByteArray> roles;
319 for (int i = 0; i < RolesCount; ++i) {
320 if (m_requestRole[i]) {
321 switch (i) {
322 case NoRole: break;
323 case NameRole: roles.insert("name"); break;
324 case SizeRole: roles.insert("size"); break;
325 case DateRole: roles.insert("date"); break;
326 case PermissionsRole: roles.insert("permissions"); break;
327 case OwnerRole: roles.insert("owner"); break;
328 case GroupRole: roles.insert("group"); break;
329 case TypeRole: roles.insert("type"); break;
330 case DestinationRole: roles.insert("destination"); break;
331 case PathRole: roles.insert("path"); break;
332 case IsDirRole: roles.insert("isDir"); break;
333 case IsExpandedRole: roles.insert("isExpanded"); break;
334 case ExpansionLevelRole: roles.insert("expansionLevel"); break;
335 default: Q_ASSERT(false); break;
336 }
337 }
338 }
339 return roles;
340 }
341
342 bool KFileItemModel::setExpanded(int index, bool expanded)
343 {
344 if (isExpanded(index) == expanded || index < 0 || index >= count()) {
345 return false;
346 }
347
348 QHash<QByteArray, QVariant> values;
349 values.insert("isExpanded", expanded);
350 if (!setData(index, values)) {
351 return false;
352 }
353
354 const KUrl url = m_sortedItems.at(index).url();
355 if (expanded) {
356 m_expandedUrls.insert(url);
357
358 KDirLister* dirLister = m_dirLister.data();
359 if (dirLister) {
360 dirLister->openUrl(url, KDirLister::Keep);
361 return true;
362 }
363 } else {
364 m_expandedUrls.remove(url);
365
366 KFileItemList itemsToRemove;
367 const int expansionLevel = data(index)["expansionLevel"].toInt();
368 ++index;
369 while (index < count() && data(index)["expansionLevel"].toInt() > expansionLevel) {
370 itemsToRemove.append(m_sortedItems.at(index));
371 ++index;
372 }
373 removeItems(itemsToRemove);
374 return true;
375 }
376
377 return false;
378 }
379
380 bool KFileItemModel::isExpanded(int index) const
381 {
382 if (index >= 0 && index < count()) {
383 return m_data.at(index).value("isExpanded").toBool();
384 }
385 return false;
386 }
387
388 bool KFileItemModel::isExpandable(int index) const
389 {
390 if (index >= 0 && index < count()) {
391 return m_sortedItems.at(index).isDir();
392 }
393 return false;
394 }
395
396 QSet<KUrl> KFileItemModel::expandedUrls() const
397 {
398 return m_expandedUrls;
399 }
400
401 void KFileItemModel::restoreExpandedUrls(const QSet<KUrl>& urls)
402 {
403 m_restoredExpandedUrls = urls;
404 }
405
406 void KFileItemModel::onGroupRoleChanged(const QByteArray& current, const QByteArray& previous)
407 {
408 Q_UNUSED(previous);
409 m_groupRole = roleIndex(current);
410 }
411
412 void KFileItemModel::onSortRoleChanged(const QByteArray& current, const QByteArray& previous)
413 {
414 Q_UNUSED(previous);
415 m_sortRole = roleIndex(current);
416 resortAllItems();
417 }
418
419 void KFileItemModel::onSortOrderChanged(Qt::SortOrder current, Qt::SortOrder previous)
420 {
421 Q_UNUSED(current);
422 Q_UNUSED(previous);
423 resortAllItems();
424 }
425
426 void KFileItemModel::slotCompleted()
427 {
428 if (m_restoredExpandedUrls.isEmpty() && m_minimumUpdateIntervalTimer->isActive()) {
429 // dispatchPendingItems() will be called when the timer
430 // has been expired.
431 m_pendingEmitLoadingCompleted = true;
432 return;
433 }
434
435 m_pendingEmitLoadingCompleted = false;
436 dispatchPendingItemsToInsert();
437
438 if (!m_restoredExpandedUrls.isEmpty()) {
439 // Try to find a URL that can be expanded.
440 // Note that the parent folder must be expanded before any of its subfolders become visible.
441 // Therefore, some URLs in m_restoredExpandedUrls might not be visible yet
442 // -> we expand the first visible URL we find in m_restoredExpandedUrls.
443 foreach(const KUrl& url, m_restoredExpandedUrls) {
444 const int index = m_items.value(url, -1);
445 if (index >= 0) {
446 // We have found an expandable URL. Expand it and return - when
447 // the dir lister has finished, this slot will be called again.
448 m_restoredExpandedUrls.remove(url);
449 setExpanded(index, true);
450 return;
451 }
452 }
453
454 // None of the URLs in m_restoredExpandedUrls could be found in the model. This can happen
455 // if these URLs have been deleted in the meantime.
456 m_restoredExpandedUrls.clear();
457 }
458
459 emit loadingCompleted();
460 m_minimumUpdateIntervalTimer->start();
461 }
462
463 void KFileItemModel::slotCanceled()
464 {
465 m_minimumUpdateIntervalTimer->stop();
466 m_maximumUpdateIntervalTimer->stop();
467 dispatchPendingItemsToInsert();
468 }
469
470 void KFileItemModel::slotNewItems(const KFileItemList& items)
471 {
472 m_pendingItemsToInsert.append(items);
473
474 if (useMaximumUpdateInterval() && !m_maximumUpdateIntervalTimer->isActive()) {
475 // Assure that items get dispatched if no completed() or canceled() signal is
476 // emitted during the maximum update interval.
477 m_maximumUpdateIntervalTimer->start();
478 }
479 }
480
481 void KFileItemModel::slotItemsDeleted(const KFileItemList& items)
482 {
483 if (!m_pendingItemsToInsert.isEmpty()) {
484 insertItems(m_pendingItemsToInsert);
485 m_pendingItemsToInsert.clear();
486 }
487 removeItems(items);
488 }
489
490 void KFileItemModel::slotRefreshItems(const QList<QPair<KFileItem, KFileItem> >& items)
491 {
492 Q_ASSERT(!items.isEmpty());
493 #ifdef KFILEITEMMODEL_DEBUG
494 kDebug() << "Refreshing" << items.count() << "items";
495 #endif
496
497 // Get the indexes of all items that have been refreshed
498 QList<int> indexes;
499 indexes.reserve(items.count());
500
501 QListIterator<QPair<KFileItem, KFileItem> > it(items);
502 while (it.hasNext()) {
503 const QPair<KFileItem, KFileItem>& itemPair = it.next();
504 const int index = m_items.value(itemPair.second.url(), -1);
505 if (index >= 0) {
506 indexes.append(index);
507 }
508 }
509
510 // If the changed items have been created recently, they might not be in m_items yet.
511 // In that case, the list 'indexes' might be empty.
512 if (indexes.isEmpty()) {
513 return;
514 }
515
516 // Extract the item-ranges out of the changed indexes
517 qSort(indexes);
518
519 KItemRangeList itemRangeList;
520 int rangeIndex = 0;
521 int rangeCount = 1;
522 int previousIndex = indexes.at(0);
523
524 const int maxIndex = indexes.count() - 1;
525 for (int i = 1; i <= maxIndex; ++i) {
526 const int currentIndex = indexes.at(i);
527 if (currentIndex == previousIndex + 1) {
528 ++rangeCount;
529 } else {
530 itemRangeList.append(KItemRange(rangeIndex, rangeCount));
531
532 rangeIndex = currentIndex;
533 rangeCount = 1;
534 }
535 previousIndex = currentIndex;
536 }
537
538 if (rangeCount > 0) {
539 itemRangeList.append(KItemRange(rangeIndex, rangeCount));
540 }
541
542 emit itemsChanged(itemRangeList, QSet<QByteArray>());
543 }
544
545 void KFileItemModel::slotClear()
546 {
547 #ifdef KFILEITEMMODEL_DEBUG
548 kDebug() << "Clearing all items";
549 #endif
550
551 m_minimumUpdateIntervalTimer->stop();
552 m_maximumUpdateIntervalTimer->stop();
553 m_pendingItemsToInsert.clear();
554
555 m_rootExpansionLevel = -1;
556
557 const int removedCount = m_data.count();
558 if (removedCount > 0) {
559 m_sortedItems.clear();
560 m_items.clear();
561 m_data.clear();
562 emit itemsRemoved(KItemRangeList() << KItemRange(0, removedCount));
563 }
564
565 m_expandedUrls.clear();
566 }
567
568 void KFileItemModel::slotClear(const KUrl& url)
569 {
570 Q_UNUSED(url);
571 }
572
573 void KFileItemModel::dispatchPendingItemsToInsert()
574 {
575 if (!m_pendingItemsToInsert.isEmpty()) {
576 insertItems(m_pendingItemsToInsert);
577 m_pendingItemsToInsert.clear();
578 }
579
580 if (m_pendingEmitLoadingCompleted) {
581 emit loadingCompleted();
582 }
583 }
584
585 void KFileItemModel::insertItems(const KFileItemList& items)
586 {
587 if (items.isEmpty()) {
588 return;
589 }
590
591 #ifdef KFILEITEMMODEL_DEBUG
592 QElapsedTimer timer;
593 timer.start();
594 kDebug() << "===========================================================";
595 kDebug() << "Inserting" << items.count() << "items";
596 #endif
597
598 KFileItemList sortedItems = items;
599 sort(sortedItems.begin(), sortedItems.end());
600
601 #ifdef KFILEITEMMODEL_DEBUG
602 kDebug() << "[TIME] Sorting:" << timer.elapsed();
603 #endif
604
605 KItemRangeList itemRanges;
606 int targetIndex = 0;
607 int sourceIndex = 0;
608 int insertedAtIndex = -1; // Index for the current item-range
609 int insertedCount = 0; // Count for the current item-range
610 int previouslyInsertedCount = 0; // Sum of previously inserted items for all ranges
611 while (sourceIndex < sortedItems.count()) {
612 // Find target index from m_items to insert the current item
613 // in a sorted order
614 const int previousTargetIndex = targetIndex;
615 while (targetIndex < m_sortedItems.count()) {
616 if (!lessThan(m_sortedItems.at(targetIndex), sortedItems.at(sourceIndex))) {
617 break;
618 }
619 ++targetIndex;
620 }
621
622 if (targetIndex - previousTargetIndex > 0 && insertedAtIndex >= 0) {
623 itemRanges << KItemRange(insertedAtIndex, insertedCount);
624 previouslyInsertedCount += insertedCount;
625 insertedAtIndex = targetIndex - previouslyInsertedCount;
626 insertedCount = 0;
627 }
628
629 // Insert item at the position targetIndex
630 const KFileItem item = sortedItems.at(sourceIndex);
631 m_sortedItems.insert(targetIndex, item);
632 m_data.insert(targetIndex, retrieveData(item));
633 // m_items will be inserted after the loop (see comment below)
634 ++insertedCount;
635
636 if (insertedAtIndex < 0) {
637 insertedAtIndex = targetIndex;
638 Q_ASSERT(previouslyInsertedCount == 0);
639 }
640 ++targetIndex;
641 ++sourceIndex;
642 }
643
644 // The indexes of all m_items must be adjusted, not only the index
645 // of the new items
646 for (int i = 0; i < m_sortedItems.count(); ++i) {
647 m_items.insert(m_sortedItems.at(i).url(), i);
648 }
649
650 itemRanges << KItemRange(insertedAtIndex, insertedCount);
651 emit itemsInserted(itemRanges);
652
653 #ifdef KFILEITEMMODEL_DEBUG
654 kDebug() << "[TIME] Inserting of" << items.count() << "items:" << timer.elapsed();
655 #endif
656 }
657
658 void KFileItemModel::removeItems(const KFileItemList& items)
659 {
660 if (items.isEmpty()) {
661 return;
662 }
663
664 #ifdef KFILEITEMMODEL_DEBUG
665 kDebug() << "Removing " << items.count() << "items";
666 #endif
667
668 KFileItemList sortedItems = items;
669 sort(sortedItems.begin(), sortedItems.end());
670
671 QList<int> indexesToRemove;
672 indexesToRemove.reserve(items.count());
673
674 // Calculate the item ranges that will get deleted
675 KItemRangeList itemRanges;
676 int removedAtIndex = -1;
677 int removedCount = 0;
678 int targetIndex = 0;
679 foreach (const KFileItem& itemToRemove, sortedItems) {
680 const int previousTargetIndex = targetIndex;
681 while (targetIndex < m_sortedItems.count()) {
682 if (m_sortedItems.at(targetIndex).url() == itemToRemove.url()) {
683 break;
684 }
685 ++targetIndex;
686 }
687 if (targetIndex >= m_sortedItems.count()) {
688 kWarning() << "Item that should be deleted has not been found!";
689 return;
690 }
691
692 if (targetIndex - previousTargetIndex > 0 && removedAtIndex >= 0) {
693 itemRanges << KItemRange(removedAtIndex, removedCount);
694 removedAtIndex = targetIndex;
695 removedCount = 0;
696 }
697
698 indexesToRemove.append(targetIndex);
699 if (removedAtIndex < 0) {
700 removedAtIndex = targetIndex;
701 }
702 ++removedCount;
703 ++targetIndex;
704 }
705
706 // Delete the items
707 for (int i = indexesToRemove.count() - 1; i >= 0; --i) {
708 const int indexToRemove = indexesToRemove.at(i);
709 m_items.remove(m_sortedItems.at(indexToRemove).url());
710 m_sortedItems.removeAt(indexToRemove);
711 m_data.removeAt(indexToRemove);
712 }
713
714 // The indexes of all m_items must be adjusted, not only the index
715 // of the removed items
716 for (int i = 0; i < m_sortedItems.count(); ++i) {
717 m_items.insert(m_sortedItems.at(i).url(), i);
718 }
719
720 if (count() <= 0) {
721 m_rootExpansionLevel = -1;
722 }
723
724 itemRanges << KItemRange(removedAtIndex, removedCount);
725 emit itemsRemoved(itemRanges);
726 }
727
728 void KFileItemModel::resortAllItems()
729 {
730 const int itemCount = count();
731 if (itemCount <= 0) {
732 return;
733 }
734
735 KFileItemList sortedItems = m_sortedItems;
736 m_sortedItems.clear();
737 m_items.clear();
738 m_data.clear();
739 emit itemsRemoved(KItemRangeList() << KItemRange(0, itemCount));
740
741 sort(sortedItems.begin(), sortedItems.end());
742 int index = 0;
743 foreach (const KFileItem& item, sortedItems) {
744 m_sortedItems.append(item);
745 m_items.insert(item.url(), index);
746 m_data.append(retrieveData(item));
747
748 ++index;
749 }
750
751 emit itemsInserted(KItemRangeList() << KItemRange(0, itemCount));
752 }
753
754 void KFileItemModel::removeExpandedItems()
755 {
756
757 KFileItemList expandedItems;
758
759 const int maxIndex = m_data.count() - 1;
760 for (int i = 0; i <= maxIndex; ++i) {
761 if (m_data.at(i).value("expansionLevel").toInt() > 0) {
762 const KFileItem fileItem = m_sortedItems.at(i);
763 expandedItems.append(fileItem);
764 }
765 }
766
767 // The m_rootExpansionLevel may not get reset before all items with
768 // a bigger expansionLevel have been removed.
769 Q_ASSERT(m_rootExpansionLevel >= 0);
770 removeItems(expandedItems);
771
772 m_rootExpansionLevel = -1;
773 m_expandedUrls.clear();
774 }
775
776 void KFileItemModel::resetRoles()
777 {
778 for (int i = 0; i < RolesCount; ++i) {
779 m_requestRole[i] = false;
780 }
781 }
782
783 KFileItemModel::Role KFileItemModel::roleIndex(const QByteArray& role) const
784 {
785 static QHash<QByteArray, Role> rolesHash;
786 if (rolesHash.isEmpty()) {
787 rolesHash.insert("name", NameRole);
788 rolesHash.insert("size", SizeRole);
789 rolesHash.insert("date", DateRole);
790 rolesHash.insert("permissions", PermissionsRole);
791 rolesHash.insert("owner", OwnerRole);
792 rolesHash.insert("group", GroupRole);
793 rolesHash.insert("type", TypeRole);
794 rolesHash.insert("destination", DestinationRole);
795 rolesHash.insert("path", PathRole);
796 rolesHash.insert("isDir", IsDirRole);
797 rolesHash.insert("isExpanded", IsExpandedRole);
798 rolesHash.insert("expansionLevel", ExpansionLevelRole);
799 }
800 return rolesHash.value(role, NoRole);
801 }
802
803 QHash<QByteArray, QVariant> KFileItemModel::retrieveData(const KFileItem& item) const
804 {
805 // It is important to insert only roles that are fast to retrieve. E.g.
806 // KFileItem::iconName() can be very expensive if the MIME-type is unknown
807 // and hence will be retrieved asynchronously by KFileItemModelRolesUpdater.
808 QHash<QByteArray, QVariant> data;
809 data.insert("iconPixmap", QPixmap());
810
811 const bool isDir = item.isDir();
812 if (m_requestRole[IsDirRole]) {
813 data.insert("isDir", isDir);
814 }
815
816 if (m_requestRole[NameRole]) {
817 data.insert("name", item.name());
818 }
819
820 if (m_requestRole[SizeRole]) {
821 if (isDir) {
822 data.insert("size", QVariant());
823 } else {
824 data.insert("size", item.size());
825 }
826 }
827
828 if (m_requestRole[DateRole]) {
829 // Don't use KFileItem::timeString() as this is too expensive when
830 // having several thousands of items. Instead the formatting of the
831 // date-time will be done on-demand by the view when the date will be shown.
832 const KDateTime dateTime = item.time(KFileItem::ModificationTime);
833 data.insert("date", dateTime.dateTime());
834 }
835
836 if (m_requestRole[PermissionsRole]) {
837 data.insert("permissions", item.permissionsString());
838 }
839
840 if (m_requestRole[OwnerRole]) {
841 data.insert("owner", item.user());
842 }
843
844 if (m_requestRole[GroupRole]) {
845 data.insert("group", item.group());
846 }
847
848 if (m_requestRole[DestinationRole]) {
849 QString destination = item.linkDest();
850 if (destination.isEmpty()) {
851 destination = i18nc("@item:intable", "No destination");
852 }
853 data.insert("destination", destination);
854 }
855
856 if (m_requestRole[PathRole]) {
857 data.insert("path", item.localPath());
858 }
859
860 if (m_requestRole[IsExpandedRole]) {
861 data.insert("isExpanded", false);
862 }
863
864 if (m_requestRole[ExpansionLevelRole]) {
865 if (m_rootExpansionLevel < 0) {
866 KDirLister* dirLister = m_dirLister.data();
867 if (dirLister) {
868 const QString rootDir = dirLister->url().directory(KUrl::AppendTrailingSlash);
869 m_rootExpansionLevel = rootDir.count('/');
870 }
871 }
872 const QString dir = item.url().directory(KUrl::AppendTrailingSlash);
873 const int level = dir.count('/') - m_rootExpansionLevel - 1;
874 data.insert("expansionLevel", level);
875 }
876
877 if (item.isMimeTypeKnown()) {
878 data.insert("iconName", item.iconName());
879
880 if (m_requestRole[TypeRole]) {
881 data.insert("type", item.mimeComment());
882 }
883 }
884
885 return data;
886 }
887
888 bool KFileItemModel::lessThan(const KFileItem& a, const KFileItem& b) const
889 {
890 int result = 0;
891
892 if (m_rootExpansionLevel >= 0) {
893 result = expansionLevelsCompare(a, b);
894 if (result != 0) {
895 // The items have parents with different expansion levels
896 return (sortOrder() == Qt::AscendingOrder) ? result < 0 : result > 0;
897 }
898 }
899
900 if (m_sortFoldersFirst || m_sortRole == SizeRole) {
901 const bool isDirA = a.isDir();
902 const bool isDirB = b.isDir();
903 if (isDirA && !isDirB) {
904 return true;
905 } else if (!isDirA && isDirB) {
906 return false;
907 }
908 }
909
910 switch (m_sortRole) {
911 case NameRole: {
912 result = stringCompare(a.text(), b.text());
913 if (result == 0) {
914 // KFileItem::text() may not be unique in case UDS_DISPLAY_NAME is used
915 result = stringCompare(a.name(m_caseSensitivity == Qt::CaseInsensitive),
916 b.name(m_caseSensitivity == Qt::CaseInsensitive));
917 }
918 break;
919 }
920
921 case DateRole: {
922 const KDateTime dateTimeA = a.time(KFileItem::ModificationTime);
923 const KDateTime dateTimeB = b.time(KFileItem::ModificationTime);
924 if (dateTimeA < dateTimeB) {
925 result = -1;
926 } else if (dateTimeA > dateTimeB) {
927 result = +1;
928 }
929 break;
930 }
931
932 case SizeRole: {
933 // TODO: Implement sorting folders by the number of items inside.
934 // This is more tricky to get right because this number is retrieved
935 // asynchronously by KFileItemModelRolesUpdater.
936 const KIO::filesize_t sizeA = a.size();
937 const KIO::filesize_t sizeB = b.size();
938 if (sizeA < sizeB) {
939 result = -1;
940 } else if (sizeA > sizeB) {
941 result = +1;
942 }
943 break;
944 }
945
946 default:
947 break;
948 }
949
950 if (result == 0) {
951 // It must be assured that the sort order is always unique even if two values have been
952 // equal. In this case a comparison of the URL is done which is unique in all cases
953 // within KDirLister.
954 result = QString::compare(a.url().url(), b.url().url(), Qt::CaseSensitive);
955 }
956
957 return (sortOrder() == Qt::AscendingOrder) ? result < 0 : result > 0;
958 }
959
960 void KFileItemModel::sort(const KFileItemList::iterator& startIterator, const KFileItemList::iterator& endIterator)
961 {
962 KFileItemList::iterator start = startIterator;
963 KFileItemList::iterator end = endIterator;
964
965 // The implementation is based on qSortHelper() from qalgorithms.h
966 // Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies).
967 // In opposite to qSort() it allows to use a member-function for the comparison of elements.
968 while (1) {
969 int span = int(end - start);
970 if (span < 2) {
971 return;
972 }
973
974 --end;
975 KFileItemList::iterator low = start, high = end - 1;
976 KFileItemList::iterator pivot = start + span / 2;
977
978 if (lessThan(*end, *start)) {
979 qSwap(*end, *start);
980 }
981 if (span == 2) {
982 return;
983 }
984
985 if (lessThan(*pivot, *start)) {
986 qSwap(*pivot, *start);
987 }
988 if (lessThan(*end, *pivot)) {
989 qSwap(*end, *pivot);
990 }
991 if (span == 3) {
992 return;
993 }
994
995 qSwap(*pivot, *end);
996
997 while (low < high) {
998 while (low < high && lessThan(*low, *end)) {
999 ++low;
1000 }
1001
1002 while (high > low && lessThan(*end, *high)) {
1003 --high;
1004 }
1005 if (low < high) {
1006 qSwap(*low, *high);
1007 ++low;
1008 --high;
1009 } else {
1010 break;
1011 }
1012 }
1013
1014 if (lessThan(*low, *end)) {
1015 ++low;
1016 }
1017
1018 qSwap(*end, *low);
1019 sort(start, low);
1020
1021 start = low + 1;
1022 ++end;
1023 }
1024 }
1025
1026 int KFileItemModel::stringCompare(const QString& a, const QString& b) const
1027 {
1028 // Taken from KDirSortFilterProxyModel (kdelibs/kfile/kdirsortfilterproxymodel.*)
1029 // Copyright (C) 2006 by Peter Penz <peter.penz@gmx.at>
1030 // Copyright (C) 2006 by Dominic Battre <dominic@battre.de>
1031 // Copyright (C) 2006 by Martin Pool <mbp@canonical.com>
1032
1033 if (m_caseSensitivity == Qt::CaseInsensitive) {
1034 const int result = m_naturalSorting ? KStringHandler::naturalCompare(a, b, Qt::CaseInsensitive)
1035 : QString::compare(a, b, Qt::CaseInsensitive);
1036 if (result != 0) {
1037 // Only return the result, if the strings are not equal. If they are equal by a case insensitive
1038 // comparison, still a deterministic sort order is required. A case sensitive
1039 // comparison is done as fallback.
1040 return result;
1041 }
1042 }
1043
1044 return m_naturalSorting ? KStringHandler::naturalCompare(a, b, Qt::CaseSensitive)
1045 : QString::compare(a, b, Qt::CaseSensitive);
1046 }
1047
1048 int KFileItemModel::expansionLevelsCompare(const KFileItem& a, const KFileItem& b) const
1049 {
1050 const KUrl urlA = a.url();
1051 const KUrl urlB = b.url();
1052 if (urlA.directory() == urlB.directory()) {
1053 // Both items have the same directory as parent
1054 return 0;
1055 }
1056
1057 // Check whether one item is the parent of the other item
1058 if (urlA.isParentOf(urlB)) {
1059 return -1;
1060 } else if (urlB.isParentOf(urlA)) {
1061 return +1;
1062 }
1063
1064 // Determine the maximum common path of both items and
1065 // remember the index in 'index'
1066 const QString pathA = urlA.path();
1067 const QString pathB = urlB.path();
1068
1069 const int maxIndex = qMin(pathA.length(), pathB.length()) - 1;
1070 int index = 0;
1071 while (index <= maxIndex && pathA.at(index) == pathB.at(index)) {
1072 ++index;
1073 }
1074 if (index > maxIndex) {
1075 index = maxIndex;
1076 }
1077 while ((pathA.at(index) != QLatin1Char('/') || pathB.at(index) != QLatin1Char('/')) && index > 0) {
1078 --index;
1079 }
1080
1081 // Determine the first sub-path after the common path and
1082 // check whether it represents a directory or already a file
1083 bool isDirA = true;
1084 const QString subPathA = subPath(a, pathA, index, &isDirA);
1085 bool isDirB = true;
1086 const QString subPathB = subPath(b, pathB, index, &isDirB);
1087
1088 if (isDirA && !isDirB) {
1089 return -1;
1090 } else if (!isDirA && isDirB) {
1091 return +1;
1092 }
1093
1094 return stringCompare(subPathA, subPathB);
1095 }
1096
1097 QString KFileItemModel::subPath(const KFileItem& item,
1098 const QString& itemPath,
1099 int start,
1100 bool* isDir) const
1101 {
1102 Q_ASSERT(isDir);
1103 const int pathIndex = itemPath.indexOf('/', start + 1);
1104 *isDir = (pathIndex > 0) || item.isDir();
1105 return itemPath.mid(start, pathIndex - start);
1106 }
1107
1108 bool KFileItemModel::useMaximumUpdateInterval() const
1109 {
1110 const KDirLister* dirLister = m_dirLister.data();
1111 return dirLister && !dirLister->url().isLocalFile();
1112 }
1113
1114 #include "kfileitemmodel.moc"