]> cloud.milkyroute.net Git - dolphin.git/blob - src/kitemviews/kfileitemmodel.cpp
Fix sorting issue when sorting by "size"
[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 <KGlobalSettings>
25 #include <KLocale>
26 #include <KStringHandler>
27 #include <KDebug>
28
29 #include <QMimeData>
30 #include <QTimer>
31
32 // #define KFILEITEMMODEL_DEBUG
33
34 KFileItemModel::KFileItemModel(KDirLister* dirLister, QObject* parent) :
35 KItemModelBase("name", parent),
36 m_dirLister(dirLister),
37 m_naturalSorting(KGlobalSettings::naturalSorting()),
38 m_sortFoldersFirst(true),
39 m_sortRole(NameRole),
40 m_roles(),
41 m_caseSensitivity(Qt::CaseInsensitive),
42 m_itemData(),
43 m_items(),
44 m_filter(),
45 m_filteredItems(),
46 m_requestRole(),
47 m_minimumUpdateIntervalTimer(0),
48 m_maximumUpdateIntervalTimer(0),
49 m_resortAllItemsTimer(0),
50 m_pendingItemsToInsert(),
51 m_pendingEmitLoadingCompleted(false),
52 m_groups(),
53 m_rootExpansionLevel(UninitializedRootExpansionLevel),
54 m_expandedUrls(),
55 m_urlsToExpand()
56 {
57 // Apply default roles that should be determined
58 resetRoles();
59 m_requestRole[NameRole] = true;
60 m_requestRole[IsDirRole] = true;
61 m_roles.insert("name");
62 m_roles.insert("isDir");
63
64 Q_ASSERT(dirLister);
65
66 connect(dirLister, SIGNAL(canceled()), this, SLOT(slotCanceled()));
67 connect(dirLister, SIGNAL(completed(KUrl)), this, SLOT(slotCompleted()));
68 connect(dirLister, SIGNAL(newItems(KFileItemList)), this, SLOT(slotNewItems(KFileItemList)));
69 connect(dirLister, SIGNAL(itemsDeleted(KFileItemList)), this, SLOT(slotItemsDeleted(KFileItemList)));
70 connect(dirLister, SIGNAL(refreshItems(QList<QPair<KFileItem,KFileItem> >)), this, SLOT(slotRefreshItems(QList<QPair<KFileItem,KFileItem> >)));
71 connect(dirLister, SIGNAL(clear()), this, SLOT(slotClear()));
72 connect(dirLister, SIGNAL(clear(KUrl)), this, SLOT(slotClear(KUrl)));
73
74 // Although the layout engine of KItemListView is fast it is very inefficient to e.g.
75 // emit 50 itemsInserted()-signals each 100 ms. m_minimumUpdateIntervalTimer assures that updates
76 // are done in 1 second intervals for equal operations.
77 m_minimumUpdateIntervalTimer = new QTimer(this);
78 m_minimumUpdateIntervalTimer->setInterval(1000);
79 m_minimumUpdateIntervalTimer->setSingleShot(true);
80 connect(m_minimumUpdateIntervalTimer, SIGNAL(timeout()), this, SLOT(dispatchPendingItemsToInsert()));
81
82 // For slow KIO-slaves like used for searching it makes sense to show results periodically even
83 // before the completed() or canceled() signal has been emitted.
84 m_maximumUpdateIntervalTimer = new QTimer(this);
85 m_maximumUpdateIntervalTimer->setInterval(2000);
86 m_maximumUpdateIntervalTimer->setSingleShot(true);
87 connect(m_maximumUpdateIntervalTimer, SIGNAL(timeout()), this, SLOT(dispatchPendingItemsToInsert()));
88
89 // When changing the value of an item which represents the sort-role a resorting must be
90 // triggered. Especially in combination with KFileItemModelRolesUpdater this might be done
91 // for a lot of items within a quite small timeslot. To prevent expensive resortings the
92 // resorting is postponed until the timer has been exceeded.
93 m_resortAllItemsTimer = new QTimer(this);
94 m_resortAllItemsTimer->setInterval(500);
95 m_resortAllItemsTimer->setSingleShot(true);
96 connect(m_resortAllItemsTimer, SIGNAL(timeout()), this, SLOT(resortAllItems()));
97
98 Q_ASSERT(m_minimumUpdateIntervalTimer->interval() <= m_maximumUpdateIntervalTimer->interval());
99
100 connect(KGlobalSettings::self(), SIGNAL(naturalSortingChanged()), this, SLOT(slotNaturalSortingChanged()));
101 }
102
103 KFileItemModel::~KFileItemModel()
104 {
105 qDeleteAll(m_itemData);
106 m_itemData.clear();
107 }
108
109 int KFileItemModel::count() const
110 {
111 return m_itemData.count();
112 }
113
114 QHash<QByteArray, QVariant> KFileItemModel::data(int index) const
115 {
116 if (index >= 0 && index < count()) {
117 return m_itemData.at(index)->values;
118 }
119 return QHash<QByteArray, QVariant>();
120 }
121
122 bool KFileItemModel::setData(int index, const QHash<QByteArray, QVariant>& values)
123 {
124 if (index < 0 || index >= count()) {
125 return false;
126 }
127
128 QHash<QByteArray, QVariant> currentValues = m_itemData.at(index)->values;
129
130 // Determine which roles have been changed
131 QSet<QByteArray> changedRoles;
132 QHashIterator<QByteArray, QVariant> it(values);
133 while (it.hasNext()) {
134 it.next();
135 const QByteArray role = it.key();
136 const QVariant value = it.value();
137
138 if (currentValues[role] != value) {
139 currentValues[role] = value;
140 changedRoles.insert(role);
141 }
142 }
143
144 if (changedRoles.isEmpty()) {
145 return false;
146 }
147
148 m_itemData[index]->values = currentValues;
149 emit itemsChanged(KItemRangeList() << KItemRange(index, 1), changedRoles);
150
151 if (changedRoles.contains(sortRole())) {
152 m_resortAllItemsTimer->start();
153 }
154
155 return true;
156 }
157
158 void KFileItemModel::setSortFoldersFirst(bool foldersFirst)
159 {
160 if (foldersFirst != m_sortFoldersFirst) {
161 m_sortFoldersFirst = foldersFirst;
162 resortAllItems();
163 }
164 }
165
166 bool KFileItemModel::sortFoldersFirst() const
167 {
168 return m_sortFoldersFirst;
169 }
170
171 void KFileItemModel::setShowHiddenFiles(bool show)
172 {
173 KDirLister* dirLister = m_dirLister.data();
174 if (dirLister) {
175 dirLister->setShowingDotFiles(show);
176 dirLister->emitChanges();
177 if (show) {
178 slotCompleted();
179 }
180 }
181 }
182
183 bool KFileItemModel::showHiddenFiles() const
184 {
185 const KDirLister* dirLister = m_dirLister.data();
186 return dirLister ? dirLister->showingDotFiles() : false;
187 }
188
189 void KFileItemModel::setShowFoldersOnly(bool enabled)
190 {
191 KDirLister* dirLister = m_dirLister.data();
192 if (dirLister) {
193 dirLister->setDirOnlyMode(enabled);
194 }
195 }
196
197 bool KFileItemModel::showFoldersOnly() const
198 {
199 KDirLister* dirLister = m_dirLister.data();
200 return dirLister ? dirLister->dirOnlyMode() : false;
201 }
202
203 QMimeData* KFileItemModel::createMimeData(const QSet<int>& indexes) const
204 {
205 QMimeData* data = new QMimeData();
206
207 // The following code has been taken from KDirModel::mimeData()
208 // (kdelibs/kio/kio/kdirmodel.cpp)
209 // Copyright (C) 2006 David Faure <faure@kde.org>
210 KUrl::List urls;
211 KUrl::List mostLocalUrls;
212 bool canUseMostLocalUrls = true;
213
214 QSetIterator<int> it(indexes);
215 while (it.hasNext()) {
216 const int index = it.next();
217 const KFileItem item = fileItem(index);
218 if (!item.isNull()) {
219 urls << item.url();
220
221 bool isLocal;
222 mostLocalUrls << item.mostLocalUrl(isLocal);
223 if (!isLocal) {
224 canUseMostLocalUrls = false;
225 }
226 }
227 }
228
229 const bool different = canUseMostLocalUrls && mostLocalUrls != urls;
230 urls = KDirModel::simplifiedUrlList(urls); // TODO: Check if we still need KDirModel for this in KDE 5.0
231 if (different) {
232 mostLocalUrls = KDirModel::simplifiedUrlList(mostLocalUrls);
233 urls.populateMimeData(mostLocalUrls, data);
234 } else {
235 urls.populateMimeData(data);
236 }
237
238 return data;
239 }
240
241 int KFileItemModel::indexForKeyboardSearch(const QString& text, int startFromIndex) const
242 {
243 startFromIndex = qMax(0, startFromIndex);
244 for (int i = startFromIndex; i < count(); ++i) {
245 if (data(i)["name"].toString().startsWith(text, Qt::CaseInsensitive)) {
246 return i;
247 }
248 }
249 for (int i = 0; i < startFromIndex; ++i) {
250 if (data(i)["name"].toString().startsWith(text, Qt::CaseInsensitive)) {
251 return i;
252 }
253 }
254 return -1;
255 }
256
257 bool KFileItemModel::supportsDropping(int index) const
258 {
259 const KFileItem item = fileItem(index);
260 return item.isNull() ? false : item.isDir();
261 }
262
263 QString KFileItemModel::roleDescription(const QByteArray& role) const
264 {
265 QString descr;
266
267 switch (roleIndex(role)) {
268 case NameRole: descr = i18nc("@item:intable", "Name"); break;
269 case SizeRole: descr = i18nc("@item:intable", "Size"); break;
270 case DateRole: descr = i18nc("@item:intable", "Date"); break;
271 case PermissionsRole: descr = i18nc("@item:intable", "Permissions"); break;
272 case OwnerRole: descr = i18nc("@item:intable", "Owner"); break;
273 case GroupRole: descr = i18nc("@item:intable", "Group"); break;
274 case TypeRole: descr = i18nc("@item:intable", "Type"); break;
275 case DestinationRole: descr = i18nc("@item:intable", "Destination"); break;
276 case PathRole: descr = i18nc("@item:intable", "Path"); break;
277 case CommentRole: descr = i18nc("@item:intable", "Comment"); break;
278 case TagsRole: descr = i18nc("@item:intable", "Tags"); break;
279 case RatingRole: descr = i18nc("@item:intable", "Rating"); break;
280 case NoRole: break;
281 case IsDirRole: break;
282 case IsExpandedRole: break;
283 case ExpansionLevelRole: break;
284 default: Q_ASSERT(false); break;
285 }
286
287 return descr;
288 }
289
290 QList<QPair<int, QVariant> > KFileItemModel::groups() const
291 {
292 if (!m_itemData.isEmpty() && m_groups.isEmpty()) {
293 #ifdef KFILEITEMMODEL_DEBUG
294 QElapsedTimer timer;
295 timer.start();
296 #endif
297 switch (roleIndex(sortRole())) {
298 case NameRole: m_groups = nameRoleGroups(); break;
299 case SizeRole: m_groups = sizeRoleGroups(); break;
300 case DateRole: m_groups = dateRoleGroups(); break;
301 case PermissionsRole: m_groups = permissionRoleGroups(); break;
302 case OwnerRole: m_groups = genericStringRoleGroups("owner"); break;
303 case GroupRole: m_groups = genericStringRoleGroups("group"); break;
304 case TypeRole: m_groups = genericStringRoleGroups("type"); break;
305 case DestinationRole: m_groups = genericStringRoleGroups("destination"); break;
306 case PathRole: m_groups = genericStringRoleGroups("path"); break;
307 case CommentRole: m_groups = genericStringRoleGroups("comment"); break;
308 case TagsRole: m_groups = genericStringRoleGroups("tags"); break;
309 case RatingRole: m_groups = ratingRoleGroups(); break;
310 case NoRole: break;
311 case IsDirRole: break;
312 case IsExpandedRole: break;
313 case ExpansionLevelRole: break;
314 default: Q_ASSERT(false); break;
315 }
316
317 #ifdef KFILEITEMMODEL_DEBUG
318 kDebug() << "[TIME] Calculating groups for" << count() << "items:" << timer.elapsed();
319 #endif
320 }
321
322 return m_groups;
323 }
324
325 KFileItem KFileItemModel::fileItem(int index) const
326 {
327 if (index >= 0 && index < count()) {
328 return m_itemData.at(index)->item;
329 }
330
331 return KFileItem();
332 }
333
334 KFileItem KFileItemModel::fileItem(const KUrl& url) const
335 {
336 const int index = m_items.value(url, -1);
337 if (index >= 0) {
338 return m_itemData.at(index)->item;
339 }
340 return KFileItem();
341 }
342
343 int KFileItemModel::index(const KFileItem& item) const
344 {
345 if (item.isNull()) {
346 return -1;
347 }
348
349 return m_items.value(item.url(), -1);
350 }
351
352 int KFileItemModel::index(const KUrl& url) const
353 {
354 KUrl urlToFind = url;
355 urlToFind.adjustPath(KUrl::RemoveTrailingSlash);
356 return m_items.value(urlToFind, -1);
357 }
358
359 KFileItem KFileItemModel::rootItem() const
360 {
361 const KDirLister* dirLister = m_dirLister.data();
362 if (dirLister) {
363 return dirLister->rootItem();
364 }
365 return KFileItem();
366 }
367
368 void KFileItemModel::clear()
369 {
370 slotClear();
371 }
372
373 void KFileItemModel::setRoles(const QSet<QByteArray>& roles)
374 {
375 m_roles = roles;
376
377 if (count() > 0) {
378 const bool supportedExpanding = m_requestRole[ExpansionLevelRole];
379 const bool willSupportExpanding = roles.contains("expansionLevel");
380 if (supportedExpanding && !willSupportExpanding) {
381 // No expanding is supported anymore. Take care to delete all items that have an expansion level
382 // that is not 0 (and hence are part of an expanded item).
383 removeExpandedItems();
384 }
385 }
386
387 m_groups.clear();
388 resetRoles();
389
390 QSetIterator<QByteArray> it(roles);
391 while (it.hasNext()) {
392 const QByteArray& role = it.next();
393 m_requestRole[roleIndex(role)] = true;
394 }
395
396 if (count() > 0) {
397 // Update m_data with the changed requested roles
398 const int maxIndex = count() - 1;
399 for (int i = 0; i <= maxIndex; ++i) {
400 m_itemData[i]->values = retrieveData(m_itemData.at(i)->item);
401 }
402
403 kWarning() << "TODO: Emitting itemsChanged() with no information what has changed!";
404 emit itemsChanged(KItemRangeList() << KItemRange(0, count()), QSet<QByteArray>());
405 }
406 }
407
408 QSet<QByteArray> KFileItemModel::roles() const
409 {
410 return m_roles;
411 }
412
413 bool KFileItemModel::setExpanded(int index, bool expanded)
414 {
415 if (!isExpandable(index) || isExpanded(index) == expanded) {
416 return false;
417 }
418
419 QHash<QByteArray, QVariant> values;
420 values.insert("isExpanded", expanded);
421 if (!setData(index, values)) {
422 return false;
423 }
424
425 KDirLister* dirLister = m_dirLister.data();
426 const KUrl url = m_itemData.at(index)->item.url();
427 if (expanded) {
428 m_expandedUrls.insert(url);
429
430 if (dirLister) {
431 dirLister->openUrl(url, KDirLister::Keep);
432 return true;
433 }
434 } else {
435 m_expandedUrls.remove(url);
436
437 if (dirLister) {
438 dirLister->stop(url);
439 }
440
441 KFileItemList itemsToRemove;
442 const int expansionLevel = data(index)["expansionLevel"].toInt();
443 ++index;
444 while (index < count() && data(index)["expansionLevel"].toInt() > expansionLevel) {
445 itemsToRemove.append(m_itemData.at(index)->item);
446 ++index;
447 }
448 removeItems(itemsToRemove);
449 return true;
450 }
451
452 return false;
453 }
454
455 bool KFileItemModel::isExpanded(int index) const
456 {
457 if (index >= 0 && index < count()) {
458 return m_itemData.at(index)->values.value("isExpanded").toBool();
459 }
460 return false;
461 }
462
463 bool KFileItemModel::isExpandable(int index) const
464 {
465 if (index >= 0 && index < count()) {
466 return m_itemData.at(index)->values.value("isExpandable").toBool();
467 }
468 return false;
469 }
470
471 QSet<KUrl> KFileItemModel::expandedUrls() const
472 {
473 return m_expandedUrls;
474 }
475
476 void KFileItemModel::restoreExpandedUrls(const QSet<KUrl>& urls)
477 {
478 m_urlsToExpand = urls;
479 }
480
481 void KFileItemModel::setExpanded(const QSet<KUrl>& urls)
482 {
483 const KDirLister* dirLister = m_dirLister.data();
484 if (!dirLister) {
485 return;
486 }
487
488 const int pos = dirLister->url().path().length();
489
490 // Assure that each sub-path of the URLs that should be
491 // expanded is added to m_urlsToExpand too. KDirLister
492 // does not care whether the parent-URL has already been
493 // expanded.
494 QSetIterator<KUrl> it1(urls);
495 while (it1.hasNext()) {
496 const KUrl& url = it1.next();
497
498 KUrl urlToExpand = dirLister->url();
499 const QStringList subDirs = url.path().mid(pos).split(QDir::separator());
500 for (int i = 0; i < subDirs.count(); ++i) {
501 urlToExpand.addPath(subDirs.at(i));
502 m_urlsToExpand.insert(urlToExpand);
503 }
504 }
505
506 // KDirLister::open() must called at least once to trigger an initial
507 // loading. The pending URLs that must be restored are handled
508 // in slotCompleted().
509 QSetIterator<KUrl> it2(m_urlsToExpand);
510 while (it2.hasNext()) {
511 const int idx = index(it2.next());
512 if (idx >= 0 && !isExpanded(idx)) {
513 setExpanded(idx, true);
514 break;
515 }
516 }
517 }
518
519 void KFileItemModel::setNameFilter(const QString& nameFilter)
520 {
521 if (m_filter.pattern() != nameFilter) {
522 dispatchPendingItemsToInsert();
523
524 m_filter.setPattern(nameFilter);
525
526 // Check which shown items from m_itemData must get
527 // hidden and hence moved to m_filteredItems.
528 KFileItemList newFilteredItems;
529
530 foreach (ItemData* itemData, m_itemData) {
531 if (!m_filter.matches(itemData->item)) {
532 // Only filter non-expanded items as child items may never
533 // exist without a parent item
534 if (!itemData->values.value("isExpanded").toBool()) {
535 newFilteredItems.append(itemData->item);
536 m_filteredItems.insert(itemData->item);
537 }
538 }
539 }
540
541 removeItems(newFilteredItems);
542
543 // Check which hidden items from m_filteredItems should
544 // get visible again and hence removed from m_filteredItems.
545 KFileItemList newVisibleItems;
546
547 QMutableSetIterator<KFileItem> it(m_filteredItems);
548 while (it.hasNext()) {
549 const KFileItem item = it.next();
550 if (m_filter.matches(item)) {
551 newVisibleItems.append(item);
552 m_filteredItems.remove(item);
553 }
554 }
555
556 insertItems(newVisibleItems);
557 }
558 }
559
560 QString KFileItemModel::nameFilter() const
561 {
562 return m_filter.pattern();
563 }
564
565 void KFileItemModel::onGroupedSortingChanged(bool current)
566 {
567 Q_UNUSED(current);
568 m_groups.clear();
569 }
570
571 void KFileItemModel::onSortRoleChanged(const QByteArray& current, const QByteArray& previous)
572 {
573 Q_UNUSED(previous);
574 m_sortRole = roleIndex(current);
575
576 #ifdef KFILEITEMMODEL_DEBUG
577 if (!m_requestRole[m_sortRole]) {
578 kWarning() << "The sort-role has been changed to a role that has not been received yet";
579 }
580 #endif
581
582 resortAllItems();
583 }
584
585 void KFileItemModel::onSortOrderChanged(Qt::SortOrder current, Qt::SortOrder previous)
586 {
587 Q_UNUSED(current);
588 Q_UNUSED(previous);
589 resortAllItems();
590 }
591
592 void KFileItemModel::resortAllItems()
593 {
594 m_resortAllItemsTimer->stop();
595
596 const int itemCount = count();
597 if (itemCount <= 0) {
598 return;
599 }
600
601 #ifdef KFILEITEMMODEL_DEBUG
602 QElapsedTimer timer;
603 timer.start();
604 kDebug() << "===========================================================";
605 kDebug() << "Resorting" << itemCount << "items";
606 #endif
607
608 // Remember the order of the current URLs so
609 // that it can be determined which indexes have
610 // been moved because of the resorting.
611 QList<KUrl> oldUrls;
612 oldUrls.reserve(itemCount);
613 foreach (const ItemData* itemData, m_itemData) {
614 oldUrls.append(itemData->item.url());
615 }
616
617 m_groups.clear();
618 m_items.clear();
619
620 // Resort the items
621 sort(m_itemData.begin(), m_itemData.end());
622 for (int i = 0; i < itemCount; ++i) {
623 m_items.insert(m_itemData.at(i)->item.url(), i);
624 }
625
626 // Determine the indexes that have been moved
627 QList<int> movedToIndexes;
628 movedToIndexes.reserve(itemCount);
629 for (int i = 0; i < itemCount; i++) {
630 const int newIndex = m_items.value(oldUrls.at(i).url());
631 movedToIndexes.append(newIndex);
632 }
633
634 // Don't check whether items have really been moved and always emit a
635 // itemsMoved() signal after resorting: In case of grouped items
636 // the groups might change even if the items themselves don't change their
637 // position. Let the receiver of the signal decide whether a check for moved
638 // items makes sense.
639 emit itemsMoved(KItemRange(0, itemCount), movedToIndexes);
640
641 #ifdef KFILEITEMMODEL_DEBUG
642 kDebug() << "[TIME] Resorting of" << itemCount << "items:" << timer.elapsed();
643 #endif
644 }
645
646 void KFileItemModel::slotCompleted()
647 {
648 if (m_urlsToExpand.isEmpty() && m_minimumUpdateIntervalTimer->isActive()) {
649 // dispatchPendingItems() will be called when the timer
650 // has been expired.
651 m_pendingEmitLoadingCompleted = true;
652 return;
653 }
654
655 m_pendingEmitLoadingCompleted = false;
656 dispatchPendingItemsToInsert();
657
658 if (!m_urlsToExpand.isEmpty()) {
659 // Try to find a URL that can be expanded.
660 // Note that the parent folder must be expanded before any of its subfolders become visible.
661 // Therefore, some URLs in m_restoredExpandedUrls might not be visible yet
662 // -> we expand the first visible URL we find in m_restoredExpandedUrls.
663 foreach(const KUrl& url, m_urlsToExpand) {
664 const int index = m_items.value(url, -1);
665 if (index >= 0) {
666 m_urlsToExpand.remove(url);
667 if (setExpanded(index, true)) {
668 // The dir lister has been triggered. This slot will be called
669 // again after the directory has been expanded.
670 return;
671 }
672 }
673 }
674
675 // None of the URLs in m_restoredExpandedUrls could be found in the model. This can happen
676 // if these URLs have been deleted in the meantime.
677 m_urlsToExpand.clear();
678 }
679
680 emit loadingCompleted();
681 m_minimumUpdateIntervalTimer->start();
682 }
683
684 void KFileItemModel::slotCanceled()
685 {
686 m_minimumUpdateIntervalTimer->stop();
687 m_maximumUpdateIntervalTimer->stop();
688 dispatchPendingItemsToInsert();
689 }
690
691 void KFileItemModel::slotNewItems(const KFileItemList& items)
692 {
693 Q_ASSERT(!items.isEmpty());
694
695 if (m_requestRole[ExpansionLevelRole] && m_rootExpansionLevel >= 0) {
696 // To be able to compare whether the new items may be inserted as children
697 // of a parent item the pending items must be added to the model first.
698 dispatchPendingItemsToInsert();
699
700 KFileItem item = items.first();
701
702 // If the expanding of items is enabled, the call
703 // dirLister->openUrl(url, KDirLister::Keep) in KFileItemModel::setExpanded()
704 // might result in emitting the same items twice due to the Keep-parameter.
705 // This case happens if an item gets expanded, collapsed and expanded again
706 // before the items could be loaded for the first expansion.
707 const int index = m_items.value(item.url(), -1);
708 if (index >= 0) {
709 // The items are already part of the model.
710 return;
711 }
712
713 // KDirLister keeps the children of items that got expanded once even if
714 // they got collapsed again with KFileItemModel::setExpanded(false). So it must be
715 // checked whether the parent for new items is still expanded.
716 KUrl parentUrl = item.url().upUrl();
717 parentUrl.adjustPath(KUrl::RemoveTrailingSlash);
718 const int parentIndex = m_items.value(parentUrl, -1);
719 if (parentIndex >= 0 && !m_itemData[parentIndex]->values.value("isExpanded").toBool()) {
720 // The parent is not expanded.
721 return;
722 }
723 }
724
725 if (m_filter.pattern().isEmpty()) {
726 m_pendingItemsToInsert.append(items);
727 } else {
728 // The name-filter is active. Hide filtered items
729 // before inserting them into the model and remember
730 // the filtered items in m_filteredItems.
731 KFileItemList filteredItems;
732 foreach (const KFileItem& item, items) {
733 if (m_filter.matches(item)) {
734 filteredItems.append(item);
735 } else {
736 m_filteredItems.insert(item);
737 }
738 }
739
740 m_pendingItemsToInsert.append(filteredItems);
741 }
742
743 if (useMaximumUpdateInterval() && !m_maximumUpdateIntervalTimer->isActive()) {
744 // Assure that items get dispatched if no completed() or canceled() signal is
745 // emitted during the maximum update interval.
746 m_maximumUpdateIntervalTimer->start();
747 }
748 }
749
750 void KFileItemModel::slotItemsDeleted(const KFileItemList& items)
751 {
752 dispatchPendingItemsToInsert();
753
754 KFileItemList itemsToRemove = items;
755 if (m_requestRole[ExpansionLevelRole] && m_rootExpansionLevel >= 0) {
756 // Assure that removing a parent item also results in removing all children
757 foreach (const KFileItem& item, items) {
758 itemsToRemove.append(childItems(item));
759 }
760 }
761
762 if (!m_filteredItems.isEmpty()) {
763 foreach (const KFileItem& item, itemsToRemove) {
764 m_filteredItems.remove(item);
765 }
766 }
767
768 removeItems(itemsToRemove);
769 }
770
771 void KFileItemModel::slotRefreshItems(const QList<QPair<KFileItem, KFileItem> >& items)
772 {
773 Q_ASSERT(!items.isEmpty());
774 #ifdef KFILEITEMMODEL_DEBUG
775 kDebug() << "Refreshing" << items.count() << "items";
776 #endif
777
778 m_groups.clear();
779
780 // Get the indexes of all items that have been refreshed
781 QList<int> indexes;
782 indexes.reserve(items.count());
783
784 QListIterator<QPair<KFileItem, KFileItem> > it(items);
785 while (it.hasNext()) {
786 const QPair<KFileItem, KFileItem>& itemPair = it.next();
787 const KFileItem& oldItem = itemPair.first;
788 const KFileItem& newItem = itemPair.second;
789 const int index = m_items.value(oldItem.url(), -1);
790 if (index >= 0) {
791 m_itemData[index]->item = newItem;
792 m_itemData[index]->values = retrieveData(newItem);
793 m_items.remove(oldItem.url());
794 m_items.insert(newItem.url(), index);
795 indexes.append(index);
796 }
797 }
798
799 // If the changed items have been created recently, they might not be in m_items yet.
800 // In that case, the list 'indexes' might be empty.
801 if (indexes.isEmpty()) {
802 return;
803 }
804
805 // Extract the item-ranges out of the changed indexes
806 qSort(indexes);
807
808 KItemRangeList itemRangeList;
809 int previousIndex = indexes.at(0);
810 int rangeIndex = previousIndex;
811 int rangeCount = 1;
812
813 const int maxIndex = indexes.count() - 1;
814 for (int i = 1; i <= maxIndex; ++i) {
815 const int currentIndex = indexes.at(i);
816 if (currentIndex == previousIndex + 1) {
817 ++rangeCount;
818 } else {
819 itemRangeList.append(KItemRange(rangeIndex, rangeCount));
820
821 rangeIndex = currentIndex;
822 rangeCount = 1;
823 }
824 previousIndex = currentIndex;
825 }
826
827 if (rangeCount > 0) {
828 itemRangeList.append(KItemRange(rangeIndex, rangeCount));
829 }
830
831 emit itemsChanged(itemRangeList, m_roles);
832
833 resortAllItems();
834 }
835
836 void KFileItemModel::slotClear()
837 {
838 #ifdef KFILEITEMMODEL_DEBUG
839 kDebug() << "Clearing all items";
840 #endif
841
842 m_filteredItems.clear();
843 m_groups.clear();
844
845 m_minimumUpdateIntervalTimer->stop();
846 m_maximumUpdateIntervalTimer->stop();
847 m_resortAllItemsTimer->stop();
848 m_pendingItemsToInsert.clear();
849
850 m_rootExpansionLevel = UninitializedRootExpansionLevel;
851
852 const int removedCount = m_itemData.count();
853 if (removedCount > 0) {
854 qDeleteAll(m_itemData);
855 m_itemData.clear();
856 m_items.clear();
857 emit itemsRemoved(KItemRangeList() << KItemRange(0, removedCount));
858 }
859
860 m_expandedUrls.clear();
861 }
862
863 void KFileItemModel::slotClear(const KUrl& url)
864 {
865 Q_UNUSED(url);
866 }
867
868 void KFileItemModel::slotNaturalSortingChanged()
869 {
870 m_naturalSorting = KGlobalSettings::naturalSorting();
871 resortAllItems();
872 }
873
874 void KFileItemModel::dispatchPendingItemsToInsert()
875 {
876 if (!m_pendingItemsToInsert.isEmpty()) {
877 insertItems(m_pendingItemsToInsert);
878 m_pendingItemsToInsert.clear();
879 }
880
881 if (m_pendingEmitLoadingCompleted) {
882 emit loadingCompleted();
883 }
884 }
885
886 void KFileItemModel::insertItems(const KFileItemList& items)
887 {
888 if (items.isEmpty()) {
889 return;
890 }
891
892 #ifdef KFILEITEMMODEL_DEBUG
893 QElapsedTimer timer;
894 timer.start();
895 kDebug() << "===========================================================";
896 kDebug() << "Inserting" << items.count() << "items";
897 #endif
898
899 m_groups.clear();
900
901 QList<ItemData*> sortedItems = createItemDataList(items);
902 sort(sortedItems.begin(), sortedItems.end());
903
904 #ifdef KFILEITEMMODEL_DEBUG
905 kDebug() << "[TIME] Sorting:" << timer.elapsed();
906 #endif
907
908 KItemRangeList itemRanges;
909 int targetIndex = 0;
910 int sourceIndex = 0;
911 int insertedAtIndex = -1; // Index for the current item-range
912 int insertedCount = 0; // Count for the current item-range
913 int previouslyInsertedCount = 0; // Sum of previously inserted items for all ranges
914 while (sourceIndex < sortedItems.count()) {
915 // Find target index from m_items to insert the current item
916 // in a sorted order
917 const int previousTargetIndex = targetIndex;
918 while (targetIndex < m_itemData.count()) {
919 if (!lessThan(m_itemData.at(targetIndex), sortedItems.at(sourceIndex))) {
920 break;
921 }
922 ++targetIndex;
923 }
924
925 if (targetIndex - previousTargetIndex > 0 && insertedAtIndex >= 0) {
926 itemRanges << KItemRange(insertedAtIndex, insertedCount);
927 previouslyInsertedCount += insertedCount;
928 insertedAtIndex = targetIndex - previouslyInsertedCount;
929 insertedCount = 0;
930 }
931
932 // Insert item at the position targetIndex by transfering
933 // the ownership of the item-data from sortedItems to m_itemData.
934 // m_items will be inserted after the loop (see comment below)
935 m_itemData.insert(targetIndex, sortedItems.at(sourceIndex));
936 ++insertedCount;
937
938 if (insertedAtIndex < 0) {
939 insertedAtIndex = targetIndex;
940 Q_ASSERT(previouslyInsertedCount == 0);
941 }
942 ++targetIndex;
943 ++sourceIndex;
944 }
945
946 // The indexes of all m_items must be adjusted, not only the index
947 // of the new items
948 const int itemDataCount = m_itemData.count();
949 for (int i = 0; i < itemDataCount; ++i) {
950 m_items.insert(m_itemData.at(i)->item.url(), i);
951 }
952
953 itemRanges << KItemRange(insertedAtIndex, insertedCount);
954 emit itemsInserted(itemRanges);
955
956 #ifdef KFILEITEMMODEL_DEBUG
957 kDebug() << "[TIME] Inserting of" << items.count() << "items:" << timer.elapsed();
958 #endif
959 }
960
961 void KFileItemModel::removeItems(const KFileItemList& items)
962 {
963 if (items.isEmpty()) {
964 return;
965 }
966
967 #ifdef KFILEITEMMODEL_DEBUG
968 kDebug() << "Removing " << items.count() << "items";
969 #endif
970
971 m_groups.clear();
972
973 QList<ItemData*> sortedItems;
974 sortedItems.reserve(items.count());
975 foreach (const KFileItem& item, items) {
976 const int index = m_items.value(item.url(), -1);
977 if (index >= 0) {
978 sortedItems.append(m_itemData.at(index));
979 }
980 }
981 sort(sortedItems.begin(), sortedItems.end());
982
983 QList<int> indexesToRemove;
984 indexesToRemove.reserve(items.count());
985
986 // Calculate the item ranges that will get deleted
987 KItemRangeList itemRanges;
988 int removedAtIndex = -1;
989 int removedCount = 0;
990 int targetIndex = 0;
991 foreach (const ItemData* itemData, sortedItems) {
992 const KFileItem& itemToRemove = itemData->item;
993
994 const int previousTargetIndex = targetIndex;
995 while (targetIndex < m_itemData.count()) {
996 if (m_itemData.at(targetIndex)->item.url() == itemToRemove.url()) {
997 break;
998 }
999 ++targetIndex;
1000 }
1001 if (targetIndex >= m_itemData.count()) {
1002 kWarning() << "Item that should be deleted has not been found!";
1003 return;
1004 }
1005
1006 if (targetIndex - previousTargetIndex > 0 && removedAtIndex >= 0) {
1007 itemRanges << KItemRange(removedAtIndex, removedCount);
1008 removedAtIndex = targetIndex;
1009 removedCount = 0;
1010 }
1011
1012 indexesToRemove.append(targetIndex);
1013 if (removedAtIndex < 0) {
1014 removedAtIndex = targetIndex;
1015 }
1016 ++removedCount;
1017 ++targetIndex;
1018 }
1019
1020 // Delete the items
1021 for (int i = indexesToRemove.count() - 1; i >= 0; --i) {
1022 const int indexToRemove = indexesToRemove.at(i);
1023 ItemData* data = m_itemData.at(indexToRemove);
1024
1025 m_items.remove(data->item.url());
1026
1027 delete data;
1028 m_itemData.removeAt(indexToRemove);
1029 }
1030
1031 // The indexes of all m_items must be adjusted, not only the index
1032 // of the removed items
1033 const int itemDataCount = m_itemData.count();
1034 for (int i = 0; i < itemDataCount; ++i) {
1035 m_items.insert(m_itemData.at(i)->item.url(), i);
1036 }
1037
1038 if (count() <= 0) {
1039 m_rootExpansionLevel = UninitializedRootExpansionLevel;
1040 }
1041
1042 itemRanges << KItemRange(removedAtIndex, removedCount);
1043 emit itemsRemoved(itemRanges);
1044 }
1045
1046 QList<KFileItemModel::ItemData*> KFileItemModel::createItemDataList(const KFileItemList& items) const
1047 {
1048 QList<ItemData*> itemDataList;
1049 itemDataList.reserve(items.count());
1050
1051 foreach (const KFileItem& item, items) {
1052 ItemData* itemData = new ItemData();
1053 itemData->item = item;
1054 itemData->values = retrieveData(item);
1055 itemData->parent = 0;
1056
1057 const bool determineParent = m_requestRole[ExpansionLevelRole]
1058 && itemData->values["expansionLevel"].toInt() > 0;
1059 if (determineParent) {
1060 KUrl parentUrl = item.url().upUrl();
1061 parentUrl.adjustPath(KUrl::RemoveTrailingSlash);
1062 const int parentIndex = m_items.value(parentUrl, -1);
1063 if (parentIndex >= 0) {
1064 itemData->parent = m_itemData.at(parentIndex);
1065 } else {
1066 kWarning() << "Parent item not found for" << item.url();
1067 }
1068 }
1069
1070 itemDataList.append(itemData);
1071 }
1072
1073 return itemDataList;
1074 }
1075
1076 void KFileItemModel::removeExpandedItems()
1077 {
1078 KFileItemList expandedItems;
1079
1080 const int maxIndex = m_itemData.count() - 1;
1081 for (int i = 0; i <= maxIndex; ++i) {
1082 const ItemData* itemData = m_itemData.at(i);
1083 if (itemData->values.value("expansionLevel").toInt() > 0) {
1084 expandedItems.append(itemData->item);
1085 }
1086 }
1087
1088 // The m_rootExpansionLevel may not get reset before all items with
1089 // a bigger expansionLevel have been removed.
1090 Q_ASSERT(m_rootExpansionLevel >= 0);
1091 removeItems(expandedItems);
1092
1093 m_rootExpansionLevel = UninitializedRootExpansionLevel;
1094 m_expandedUrls.clear();
1095 }
1096
1097 void KFileItemModel::resetRoles()
1098 {
1099 for (int i = 0; i < RolesCount; ++i) {
1100 m_requestRole[i] = false;
1101 }
1102 }
1103
1104 KFileItemModel::Role KFileItemModel::roleIndex(const QByteArray& role) const
1105 {
1106 static QHash<QByteArray, Role> rolesHash;
1107 if (rolesHash.isEmpty()) {
1108 rolesHash.insert("name", NameRole);
1109 rolesHash.insert("size", SizeRole);
1110 rolesHash.insert("date", DateRole);
1111 rolesHash.insert("permissions", PermissionsRole);
1112 rolesHash.insert("owner", OwnerRole);
1113 rolesHash.insert("group", GroupRole);
1114 rolesHash.insert("type", TypeRole);
1115 rolesHash.insert("destination", DestinationRole);
1116 rolesHash.insert("path", PathRole);
1117 rolesHash.insert("comment", CommentRole);
1118 rolesHash.insert("tags", TagsRole);
1119 rolesHash.insert("rating", RatingRole);
1120 rolesHash.insert("isDir", IsDirRole);
1121 rolesHash.insert("isExpanded", IsExpandedRole);
1122 rolesHash.insert("isExpandable", IsExpandableRole);
1123 rolesHash.insert("expansionLevel", ExpansionLevelRole);
1124 }
1125 return rolesHash.value(role, NoRole);
1126 }
1127
1128 QByteArray KFileItemModel::roleByteArray(Role role) const
1129 {
1130 static const char* const roles[RolesCount] = {
1131 0, // NoRole
1132 "name",
1133 "size",
1134 "date",
1135 "permissions",
1136 "owner",
1137 "group",
1138 "type",
1139 "destination",
1140 "path",
1141 "comment",
1142 "tags",
1143 "rating",
1144 "isDir",
1145 "isExpanded",
1146 "isExpandable",
1147 "expansionLevel"
1148 };
1149 return roles[role];
1150 }
1151
1152 QHash<QByteArray, QVariant> KFileItemModel::retrieveData(const KFileItem& item) const
1153 {
1154 // It is important to insert only roles that are fast to retrieve. E.g.
1155 // KFileItem::iconName() can be very expensive if the MIME-type is unknown
1156 // and hence will be retrieved asynchronously by KFileItemModelRolesUpdater.
1157 QHash<QByteArray, QVariant> data;
1158 data.insert("iconPixmap", QPixmap());
1159 data.insert("url", item.url());
1160
1161 const bool isDir = item.isDir();
1162 if (m_requestRole[IsDirRole]) {
1163 data.insert("isDir", isDir);
1164 }
1165
1166 if (m_requestRole[NameRole]) {
1167 data.insert("name", item.text());
1168 }
1169
1170 if (m_requestRole[SizeRole]) {
1171 if (isDir) {
1172 data.insert("size", QVariant());
1173 } else {
1174 data.insert("size", item.size());
1175 }
1176 }
1177
1178 if (m_requestRole[DateRole]) {
1179 // Don't use KFileItem::timeString() as this is too expensive when
1180 // having several thousands of items. Instead the formatting of the
1181 // date-time will be done on-demand by the view when the date will be shown.
1182 const KDateTime dateTime = item.time(KFileItem::ModificationTime);
1183 data.insert("date", dateTime.dateTime());
1184 }
1185
1186 if (m_requestRole[PermissionsRole]) {
1187 data.insert("permissions", item.permissionsString());
1188 }
1189
1190 if (m_requestRole[OwnerRole]) {
1191 data.insert("owner", item.user());
1192 }
1193
1194 if (m_requestRole[GroupRole]) {
1195 data.insert("group", item.group());
1196 }
1197
1198 if (m_requestRole[DestinationRole]) {
1199 QString destination = item.linkDest();
1200 if (destination.isEmpty()) {
1201 destination = i18nc("@item:intable", "No destination");
1202 }
1203 data.insert("destination", destination);
1204 }
1205
1206 if (m_requestRole[PathRole]) {
1207 QString path;
1208 if (item.url().protocol() == QLatin1String("trash")) {
1209 path = item.entry().stringValue(KIO::UDSEntry::UDS_EXTRA);
1210 } else {
1211 path = item.localPath();
1212 }
1213
1214 const int index = path.lastIndexOf(item.text());
1215 path = path.mid(0, index - 1);
1216 data.insert("path", path);
1217 }
1218
1219 if (m_requestRole[IsExpandedRole]) {
1220 data.insert("isExpanded", false);
1221 }
1222
1223 if (m_requestRole[IsExpandableRole]) {
1224 data.insert("isExpandable", item.isDir() && item.url() == item.targetUrl());
1225 }
1226
1227 if (m_requestRole[ExpansionLevelRole]) {
1228 if (m_rootExpansionLevel == UninitializedRootExpansionLevel && m_dirLister.data()) {
1229 const KUrl rootUrl = m_dirLister.data()->url();
1230 const QString protocol = rootUrl.protocol();
1231 const bool forceRootExpansionLevel = (protocol == QLatin1String("trash") ||
1232 protocol == QLatin1String("nepomuk") ||
1233 protocol == QLatin1String("remote") ||
1234 protocol.contains(QLatin1String("search")));
1235 if (forceRootExpansionLevel) {
1236 m_rootExpansionLevel = ForceRootExpansionLevel;
1237 } else {
1238 const QString rootDir = rootUrl.path(KUrl::AddTrailingSlash);
1239 m_rootExpansionLevel = rootDir.count('/');
1240 }
1241 }
1242
1243 if (m_rootExpansionLevel == ForceRootExpansionLevel) {
1244 data.insert("expansionLevel", -1);
1245 } else {
1246 const QString dir = item.url().directory(KUrl::AppendTrailingSlash);
1247 const int level = dir.count('/') - m_rootExpansionLevel;
1248 data.insert("expansionLevel", level);
1249 }
1250 }
1251
1252 if (item.isMimeTypeKnown()) {
1253 data.insert("iconName", item.iconName());
1254
1255 if (m_requestRole[TypeRole]) {
1256 data.insert("type", item.mimeComment());
1257 }
1258 }
1259
1260 return data;
1261 }
1262
1263 bool KFileItemModel::lessThan(const ItemData* a, const ItemData* b) const
1264 {
1265 int result = 0;
1266
1267 if (m_rootExpansionLevel >= 0) {
1268 result = expansionLevelsCompare(a, b);
1269 if (result != 0) {
1270 // The items have parents with different expansion levels
1271 return (sortOrder() == Qt::AscendingOrder) ? result < 0 : result > 0;
1272 }
1273 }
1274
1275 if (m_sortFoldersFirst || m_sortRole == SizeRole) {
1276 const bool isDirA = a->item.isDir();
1277 const bool isDirB = b->item.isDir();
1278 if (isDirA && !isDirB) {
1279 return true;
1280 } else if (!isDirA && isDirB) {
1281 return false;
1282 }
1283 }
1284
1285 result = sortRoleCompare(a, b);
1286
1287 return (sortOrder() == Qt::AscendingOrder) ? result < 0 : result > 0;
1288 }
1289
1290 int KFileItemModel::sortRoleCompare(const ItemData* a, const ItemData* b) const
1291 {
1292 const KFileItem& itemA = a->item;
1293 const KFileItem& itemB = b->item;
1294
1295 int result = 0;
1296
1297 switch (m_sortRole) {
1298 case NameRole:
1299 // The name role is handled as default fallback after the switch
1300 break;
1301
1302 case SizeRole: {
1303 if (itemA.isDir()) {
1304 // See "if (m_sortFoldersFirst || m_sortRole == SizeRole)" in KFileItemModel::lessThan():
1305 Q_ASSERT(itemB.isDir());
1306
1307 const QVariant valueA = a->values.value("size");
1308 const QVariant valueB = b->values.value("size");
1309 if (valueA.isNull() && valueB.isNull()) {
1310 result = 0;
1311 } else if (valueA.isNull()) {
1312 result = -1;
1313 } else if (valueB.isNull()) {
1314 result = +1;
1315 } else {
1316 result = fileSizeCompare(valueA.value<KIO::filesize_t>(), valueB.value<KIO::filesize_t>());
1317 }
1318 } else {
1319 // See "if (m_sortFoldersFirst || m_sortRole == SizeRole)" in KFileItemModel::lessThan():
1320 Q_ASSERT(!itemB.isDir());
1321 result = fileSizeCompare(itemA.size(), itemB.size());
1322 }
1323 break;
1324 }
1325
1326 case DateRole: {
1327 const KDateTime dateTimeA = itemA.time(KFileItem::ModificationTime);
1328 const KDateTime dateTimeB = itemB.time(KFileItem::ModificationTime);
1329 if (dateTimeA < dateTimeB) {
1330 result = -1;
1331 } else if (dateTimeA > dateTimeB) {
1332 result = +1;
1333 }
1334 break;
1335 }
1336
1337 case RatingRole: {
1338 result = a->values.value("rating").toInt() - b->values.value("rating").toInt();
1339 break;
1340 }
1341
1342 case PermissionsRole:
1343 case OwnerRole:
1344 case GroupRole:
1345 case TypeRole:
1346 case DestinationRole:
1347 case PathRole:
1348 case CommentRole:
1349 case TagsRole: {
1350 const QByteArray role = roleByteArray(m_sortRole);
1351 result = QString::compare(a->values.value(role).toString(),
1352 b->values.value(role).toString());
1353 break;
1354 }
1355
1356 default:
1357 break;
1358 }
1359
1360 if (result != 0) {
1361 // The current sort role was sufficient to define an order
1362 return result;
1363 }
1364
1365 // Fallback #1: Compare the text of the items
1366 result = stringCompare(itemA.text(), itemB.text());
1367 if (result != 0) {
1368 return result;
1369 }
1370
1371 // Fallback #2: KFileItem::text() may not be unique in case UDS_DISPLAY_NAME is used
1372 result = stringCompare(itemA.name(m_caseSensitivity == Qt::CaseInsensitive),
1373 itemB.name(m_caseSensitivity == Qt::CaseInsensitive));
1374 if (result != 0) {
1375 return result;
1376 }
1377
1378 // Fallback #3: It must be assured that the sort order is always unique even if two values have been
1379 // equal. In this case a comparison of the URL is done which is unique in all cases
1380 // within KDirLister.
1381 return QString::compare(itemA.url().url(), itemB.url().url(), Qt::CaseSensitive);
1382 }
1383
1384 void KFileItemModel::sort(QList<ItemData*>::iterator begin,
1385 QList<ItemData*>::iterator end)
1386 {
1387 // The implementation is based on qStableSortHelper() from qalgorithms.h
1388 // Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies).
1389 // In opposite to qStableSort() it allows to use a member-function for the comparison of elements.
1390
1391 const int span = end - begin;
1392 if (span < 2) {
1393 return;
1394 }
1395
1396 const QList<ItemData*>::iterator middle = begin + span / 2;
1397 sort(begin, middle);
1398 sort(middle, end);
1399 merge(begin, middle, end);
1400 }
1401
1402 void KFileItemModel::merge(QList<ItemData*>::iterator begin,
1403 QList<ItemData*>::iterator pivot,
1404 QList<ItemData*>::iterator end)
1405 {
1406 // The implementation is based on qMerge() from qalgorithms.h
1407 // Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies).
1408
1409 const int len1 = pivot - begin;
1410 const int len2 = end - pivot;
1411
1412 if (len1 == 0 || len2 == 0) {
1413 return;
1414 }
1415
1416 if (len1 + len2 == 2) {
1417 if (lessThan(*(begin + 1), *(begin))) {
1418 qSwap(*begin, *(begin + 1));
1419 }
1420 return;
1421 }
1422
1423 QList<ItemData*>::iterator firstCut;
1424 QList<ItemData*>::iterator secondCut;
1425 int len2Half;
1426 if (len1 > len2) {
1427 const int len1Half = len1 / 2;
1428 firstCut = begin + len1Half;
1429 secondCut = lowerBound(pivot, end, *firstCut);
1430 len2Half = secondCut - pivot;
1431 } else {
1432 len2Half = len2 / 2;
1433 secondCut = pivot + len2Half;
1434 firstCut = upperBound(begin, pivot, *secondCut);
1435 }
1436
1437 reverse(firstCut, pivot);
1438 reverse(pivot, secondCut);
1439 reverse(firstCut, secondCut);
1440
1441 const QList<ItemData*>::iterator newPivot = firstCut + len2Half;
1442 merge(begin, firstCut, newPivot);
1443 merge(newPivot, secondCut, end);
1444 }
1445
1446 QList<KFileItemModel::ItemData*>::iterator KFileItemModel::lowerBound(QList<ItemData*>::iterator begin,
1447 QList<ItemData*>::iterator end,
1448 const ItemData* value)
1449 {
1450 // The implementation is based on qLowerBound() from qalgorithms.h
1451 // Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies).
1452
1453 QList<ItemData*>::iterator middle;
1454 int n = int(end - begin);
1455 int half;
1456
1457 while (n > 0) {
1458 half = n >> 1;
1459 middle = begin + half;
1460 if (lessThan(*middle, value)) {
1461 begin = middle + 1;
1462 n -= half + 1;
1463 } else {
1464 n = half;
1465 }
1466 }
1467 return begin;
1468 }
1469
1470 QList<KFileItemModel::ItemData*>::iterator KFileItemModel::upperBound(QList<ItemData*>::iterator begin,
1471 QList<ItemData*>::iterator end,
1472 const ItemData* value)
1473 {
1474 // The implementation is based on qUpperBound() from qalgorithms.h
1475 // Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies).
1476
1477 QList<ItemData*>::iterator middle;
1478 int n = end - begin;
1479 int half;
1480
1481 while (n > 0) {
1482 half = n >> 1;
1483 middle = begin + half;
1484 if (lessThan(value, *middle)) {
1485 n = half;
1486 } else {
1487 begin = middle + 1;
1488 n -= half + 1;
1489 }
1490 }
1491 return begin;
1492 }
1493
1494 void KFileItemModel::reverse(QList<ItemData*>::iterator begin,
1495 QList<ItemData*>::iterator end)
1496 {
1497 // The implementation is based on qReverse() from qalgorithms.h
1498 // Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies).
1499
1500 --end;
1501 while (begin < end) {
1502 qSwap(*begin++, *end--);
1503 }
1504 }
1505
1506 int KFileItemModel::stringCompare(const QString& a, const QString& b) const
1507 {
1508 // Taken from KDirSortFilterProxyModel (kdelibs/kfile/kdirsortfilterproxymodel.*)
1509 // Copyright (C) 2006 by Peter Penz <peter.penz@gmx.at>
1510 // Copyright (C) 2006 by Dominic Battre <dominic@battre.de>
1511 // Copyright (C) 2006 by Martin Pool <mbp@canonical.com>
1512
1513 if (m_caseSensitivity == Qt::CaseInsensitive) {
1514 const int result = m_naturalSorting ? KStringHandler::naturalCompare(a, b, Qt::CaseInsensitive)
1515 : QString::compare(a, b, Qt::CaseInsensitive);
1516 if (result != 0) {
1517 // Only return the result, if the strings are not equal. If they are equal by a case insensitive
1518 // comparison, still a deterministic sort order is required. A case sensitive
1519 // comparison is done as fallback.
1520 return result;
1521 }
1522 }
1523
1524 return m_naturalSorting ? KStringHandler::naturalCompare(a, b, Qt::CaseSensitive)
1525 : QString::compare(a, b, Qt::CaseSensitive);
1526 }
1527
1528 int KFileItemModel::expansionLevelsCompare(const ItemData* a, const ItemData* b) const
1529 {
1530 const KUrl urlA = a->item.url();
1531 const KUrl urlB = b->item.url();
1532 if (urlA.directory() == urlB.directory()) {
1533 // Both items have the same directory as parent
1534 return 0;
1535 }
1536
1537 // Check whether one item is the parent of the other item
1538 if (urlA.isParentOf(urlB)) {
1539 return (sortOrder() == Qt::AscendingOrder) ? -1 : +1;
1540 } else if (urlB.isParentOf(urlA)) {
1541 return (sortOrder() == Qt::AscendingOrder) ? +1 : -1;
1542 }
1543
1544 // Determine the maximum common path of both items and
1545 // remember the index in 'index'
1546 const QString pathA = urlA.path();
1547 const QString pathB = urlB.path();
1548
1549 const int maxIndex = qMin(pathA.length(), pathB.length()) - 1;
1550 int index = 0;
1551 while (index <= maxIndex && pathA.at(index) == pathB.at(index)) {
1552 ++index;
1553 }
1554 if (index > maxIndex) {
1555 index = maxIndex;
1556 }
1557 while ((pathA.at(index) != QLatin1Char('/') || pathB.at(index) != QLatin1Char('/')) && index > 0) {
1558 --index;
1559 }
1560
1561 // Determine the first sub-path after the common path and
1562 // check whether it represents a directory or already a file
1563 bool isDirA = true;
1564 const QString subPathA = subPath(a->item, pathA, index, &isDirA);
1565 bool isDirB = true;
1566 const QString subPathB = subPath(b->item, pathB, index, &isDirB);
1567
1568 if (isDirA && !isDirB) {
1569 return (sortOrder() == Qt::AscendingOrder) ? -1 : +1;
1570 } else if (!isDirA && isDirB) {
1571 return (sortOrder() == Qt::AscendingOrder) ? +1 : -1;
1572 }
1573
1574 // Compare the items of the parents that represent the first
1575 // different path after the common path.
1576 const QString parentPathA = pathA.left(index) + subPathA;
1577 const QString parentPathB = pathB.left(index) + subPathB;
1578
1579 const ItemData* parentA = a;
1580 while (parentA && parentA->item.url().path() != parentPathA) {
1581 parentA = parentA->parent;
1582 }
1583
1584 const ItemData* parentB = b;
1585 while (parentB && parentB->item.url().path() != parentPathB) {
1586 parentB = parentB->parent;
1587 }
1588
1589 if (parentA && parentB) {
1590 return sortRoleCompare(parentA, parentB);
1591 }
1592
1593 kWarning() << "Child items without parent detected:" << a->item.url() << b->item.url();
1594 return QString::compare(urlA.url(), urlB.url(), Qt::CaseSensitive);
1595 }
1596
1597 QString KFileItemModel::subPath(const KFileItem& item,
1598 const QString& itemPath,
1599 int start,
1600 bool* isDir) const
1601 {
1602 Q_ASSERT(isDir);
1603 const int pathIndex = itemPath.indexOf('/', start + 1);
1604 *isDir = (pathIndex > 0) || item.isDir();
1605 return itemPath.mid(start, pathIndex - start);
1606 }
1607
1608 bool KFileItemModel::useMaximumUpdateInterval() const
1609 {
1610 const KDirLister* dirLister = m_dirLister.data();
1611 return dirLister && !dirLister->url().isLocalFile();
1612 }
1613
1614 QList<QPair<int, QVariant> > KFileItemModel::nameRoleGroups() const
1615 {
1616 Q_ASSERT(!m_itemData.isEmpty());
1617
1618 const int maxIndex = count() - 1;
1619 QList<QPair<int, QVariant> > groups;
1620
1621 QString groupValue;
1622 QChar firstChar;
1623 bool isLetter = false;
1624 for (int i = 0; i <= maxIndex; ++i) {
1625 if (isChildItem(i)) {
1626 continue;
1627 }
1628
1629 const QString name = m_itemData.at(i)->values.value("name").toString();
1630
1631 // Use the first character of the name as group indication
1632 QChar newFirstChar = name.at(0).toUpper();
1633 if (newFirstChar == QLatin1Char('~') && name.length() > 1) {
1634 newFirstChar = name.at(1).toUpper();
1635 }
1636
1637 if (firstChar != newFirstChar) {
1638 QString newGroupValue;
1639 if (newFirstChar >= QLatin1Char('A') && newFirstChar <= QLatin1Char('Z')) {
1640 // Apply group 'A' - 'Z'
1641 newGroupValue = newFirstChar;
1642 isLetter = true;
1643 } else if (newFirstChar >= QLatin1Char('0') && newFirstChar <= QLatin1Char('9')) {
1644 // Apply group '0 - 9' for any name that starts with a digit
1645 newGroupValue = i18nc("@title:group Groups that start with a digit", "0 - 9");
1646 isLetter = false;
1647 } else {
1648 if (isLetter) {
1649 // If the current group is 'A' - 'Z' check whether a locale character
1650 // fits into the existing group.
1651 // TODO: This does not work in the case if e.g. the group 'O' starts with
1652 // an umlaut 'O' -> provide unit-test to document this known issue
1653 const QChar prevChar(firstChar.unicode() - ushort(1));
1654 const QChar nextChar(firstChar.unicode() + ushort(1));
1655 const QString currChar(newFirstChar);
1656 const bool partOfCurrentGroup = currChar.localeAwareCompare(prevChar) > 0 &&
1657 currChar.localeAwareCompare(nextChar) < 0;
1658 if (partOfCurrentGroup) {
1659 continue;
1660 }
1661 }
1662 newGroupValue = i18nc("@title:group", "Others");
1663 isLetter = false;
1664 }
1665
1666 if (newGroupValue != groupValue) {
1667 groupValue = newGroupValue;
1668 groups.append(QPair<int, QVariant>(i, newGroupValue));
1669 }
1670
1671 firstChar = newFirstChar;
1672 }
1673 }
1674 return groups;
1675 }
1676
1677 QList<QPair<int, QVariant> > KFileItemModel::sizeRoleGroups() const
1678 {
1679 Q_ASSERT(!m_itemData.isEmpty());
1680
1681 const int maxIndex = count() - 1;
1682 QList<QPair<int, QVariant> > groups;
1683
1684 QString groupValue;
1685 for (int i = 0; i <= maxIndex; ++i) {
1686 if (isChildItem(i)) {
1687 continue;
1688 }
1689
1690 const KFileItem& item = m_itemData.at(i)->item;
1691 const KIO::filesize_t fileSize = !item.isNull() ? item.size() : ~0U;
1692 QString newGroupValue;
1693 if (!item.isNull() && item.isDir()) {
1694 newGroupValue = i18nc("@title:group Size", "Folders");
1695 } else if (fileSize < 5 * 1024 * 1024) {
1696 newGroupValue = i18nc("@title:group Size", "Small");
1697 } else if (fileSize < 10 * 1024 * 1024) {
1698 newGroupValue = i18nc("@title:group Size", "Medium");
1699 } else {
1700 newGroupValue = i18nc("@title:group Size", "Big");
1701 }
1702
1703 if (newGroupValue != groupValue) {
1704 groupValue = newGroupValue;
1705 groups.append(QPair<int, QVariant>(i, newGroupValue));
1706 }
1707 }
1708
1709 return groups;
1710 }
1711
1712 QList<QPair<int, QVariant> > KFileItemModel::dateRoleGroups() const
1713 {
1714 Q_ASSERT(!m_itemData.isEmpty());
1715
1716 const int maxIndex = count() - 1;
1717 QList<QPair<int, QVariant> > groups;
1718
1719 const QDate currentDate = KDateTime::currentLocalDateTime().date();
1720
1721 int yearForCurrentWeek = 0;
1722 int currentWeek = currentDate.weekNumber(&yearForCurrentWeek);
1723 if (yearForCurrentWeek == currentDate.year() + 1) {
1724 currentWeek = 53;
1725 }
1726
1727 QDate previousModifiedDate;
1728 QString groupValue;
1729 for (int i = 0; i <= maxIndex; ++i) {
1730 if (isChildItem(i)) {
1731 continue;
1732 }
1733
1734 const KDateTime modifiedTime = m_itemData.at(i)->item.time(KFileItem::ModificationTime);
1735 const QDate modifiedDate = modifiedTime.date();
1736 if (modifiedDate == previousModifiedDate) {
1737 // The current item is in the same group as the previous item
1738 continue;
1739 }
1740 previousModifiedDate = modifiedDate;
1741
1742 const int daysDistance = modifiedDate.daysTo(currentDate);
1743
1744 int yearForModifiedWeek = 0;
1745 int modifiedWeek = modifiedDate.weekNumber(&yearForModifiedWeek);
1746 if (yearForModifiedWeek == modifiedDate.year() + 1) {
1747 modifiedWeek = 53;
1748 }
1749
1750 QString newGroupValue;
1751 if (currentDate.year() == modifiedDate.year() && currentDate.month() == modifiedDate.month()) {
1752 if (modifiedWeek > currentWeek) {
1753 // Usecase: modified date = 2010-01-01, current date = 2010-01-22
1754 // modified week = 53, current week = 3
1755 modifiedWeek = 0;
1756 }
1757 switch (currentWeek - modifiedWeek) {
1758 case 0:
1759 switch (daysDistance) {
1760 case 0: newGroupValue = i18nc("@title:group Date", "Today"); break;
1761 case 1: newGroupValue = i18nc("@title:group Date", "Yesterday"); break;
1762 default: newGroupValue = modifiedTime.toString(i18nc("@title:group The week day name: %A", "%A"));
1763 }
1764 break;
1765 case 1:
1766 newGroupValue = i18nc("@title:group Date", "Last Week");
1767 break;
1768 case 2:
1769 newGroupValue = i18nc("@title:group Date", "Two Weeks Ago");
1770 break;
1771 case 3:
1772 newGroupValue = i18nc("@title:group Date", "Three Weeks Ago");
1773 break;
1774 case 4:
1775 case 5:
1776 newGroupValue = i18nc("@title:group Date", "Earlier this Month");
1777 break;
1778 default:
1779 Q_ASSERT(false);
1780 }
1781 } else {
1782 const QDate lastMonthDate = currentDate.addMonths(-1);
1783 if (lastMonthDate.year() == modifiedDate.year() && lastMonthDate.month() == modifiedDate.month()) {
1784 if (daysDistance == 1) {
1785 newGroupValue = modifiedTime.toString(i18nc("@title:group Date: %B is full month name in current locale, and %Y is full year number", "Yesterday (%B, %Y)"));
1786 } else if (daysDistance <= 7) {
1787 newGroupValue = modifiedTime.toString(i18nc("@title:group The week day name: %A, %B is full month name in current locale, and %Y is full year number", "%A (%B, %Y)"));
1788 } else if (daysDistance <= 7 * 2) {
1789 newGroupValue = modifiedTime.toString(i18nc("@title:group Date: %B is full month name in current locale, and %Y is full year number", "Last Week (%B, %Y)"));
1790 } else if (daysDistance <= 7 * 3) {
1791 newGroupValue = modifiedTime.toString(i18nc("@title:group Date: %B is full month name in current locale, and %Y is full year number", "Two Weeks Ago (%B, %Y)"));
1792 } else if (daysDistance <= 7 * 4) {
1793 newGroupValue = modifiedTime.toString(i18nc("@title:group Date: %B is full month name in current locale, and %Y is full year number", "Three Weeks Ago (%B, %Y)"));
1794 } else {
1795 newGroupValue = modifiedTime.toString(i18nc("@title:group Date: %B is full month name in current locale, and %Y is full year number", "Earlier on %B, %Y"));
1796 }
1797 } else {
1798 newGroupValue = modifiedTime.toString(i18nc("@title:group The month and year: %B is full month name in current locale, and %Y is full year number", "%B, %Y"));
1799 }
1800 }
1801
1802 if (newGroupValue != groupValue) {
1803 groupValue = newGroupValue;
1804 groups.append(QPair<int, QVariant>(i, newGroupValue));
1805 }
1806 }
1807
1808 return groups;
1809 }
1810
1811 QList<QPair<int, QVariant> > KFileItemModel::permissionRoleGroups() const
1812 {
1813 Q_ASSERT(!m_itemData.isEmpty());
1814
1815 const int maxIndex = count() - 1;
1816 QList<QPair<int, QVariant> > groups;
1817
1818 QString permissionsString;
1819 QString groupValue;
1820 for (int i = 0; i <= maxIndex; ++i) {
1821 if (isChildItem(i)) {
1822 continue;
1823 }
1824
1825 const ItemData* itemData = m_itemData.at(i);
1826 const QString newPermissionsString = itemData->values.value("permissions").toString();
1827 if (newPermissionsString == permissionsString) {
1828 continue;
1829 }
1830 permissionsString = newPermissionsString;
1831
1832 const QFileInfo info(itemData->item.url().pathOrUrl());
1833
1834 // Set user string
1835 QString user;
1836 if (info.permission(QFile::ReadUser)) {
1837 user = i18nc("@item:intext Access permission, concatenated", "Read, ");
1838 }
1839 if (info.permission(QFile::WriteUser)) {
1840 user += i18nc("@item:intext Access permission, concatenated", "Write, ");
1841 }
1842 if (info.permission(QFile::ExeUser)) {
1843 user += i18nc("@item:intext Access permission, concatenated", "Execute, ");
1844 }
1845 user = user.isEmpty() ? i18nc("@item:intext Access permission, concatenated", "Forbidden") : user.mid(0, user.count() - 2);
1846
1847 // Set group string
1848 QString group;
1849 if (info.permission(QFile::ReadGroup)) {
1850 group = i18nc("@item:intext Access permission, concatenated", "Read, ");
1851 }
1852 if (info.permission(QFile::WriteGroup)) {
1853 group += i18nc("@item:intext Access permission, concatenated", "Write, ");
1854 }
1855 if (info.permission(QFile::ExeGroup)) {
1856 group += i18nc("@item:intext Access permission, concatenated", "Execute, ");
1857 }
1858 group = group.isEmpty() ? i18nc("@item:intext Access permission, concatenated", "Forbidden") : group.mid(0, group.count() - 2);
1859
1860 // Set others string
1861 QString others;
1862 if (info.permission(QFile::ReadOther)) {
1863 others = i18nc("@item:intext Access permission, concatenated", "Read, ");
1864 }
1865 if (info.permission(QFile::WriteOther)) {
1866 others += i18nc("@item:intext Access permission, concatenated", "Write, ");
1867 }
1868 if (info.permission(QFile::ExeOther)) {
1869 others += i18nc("@item:intext Access permission, concatenated", "Execute, ");
1870 }
1871 others = others.isEmpty() ? i18nc("@item:intext Access permission, concatenated", "Forbidden") : others.mid(0, others.count() - 2);
1872
1873 const QString newGroupValue = i18nc("@title:group Files and folders by permissions", "User: %1 | Group: %2 | Others: %3", user, group, others);
1874 if (newGroupValue != groupValue) {
1875 groupValue = newGroupValue;
1876 groups.append(QPair<int, QVariant>(i, newGroupValue));
1877 }
1878 }
1879
1880 return groups;
1881 }
1882
1883 QList<QPair<int, QVariant> > KFileItemModel::ratingRoleGroups() const
1884 {
1885 Q_ASSERT(!m_itemData.isEmpty());
1886
1887 const int maxIndex = count() - 1;
1888 QList<QPair<int, QVariant> > groups;
1889
1890 int groupValue = -1;
1891 for (int i = 0; i <= maxIndex; ++i) {
1892 if (isChildItem(i)) {
1893 continue;
1894 }
1895 const int newGroupValue = m_itemData.at(i)->values.value("rating", 0).toInt();
1896 if (newGroupValue != groupValue) {
1897 groupValue = newGroupValue;
1898 groups.append(QPair<int, QVariant>(i, newGroupValue));
1899 }
1900 }
1901
1902 return groups;
1903 }
1904
1905 QList<QPair<int, QVariant> > KFileItemModel::genericStringRoleGroups(const QByteArray& role) const
1906 {
1907 Q_ASSERT(!m_itemData.isEmpty());
1908
1909 const int maxIndex = count() - 1;
1910 QList<QPair<int, QVariant> > groups;
1911
1912 bool isFirstGroupValue = true;
1913 QString groupValue;
1914 for (int i = 0; i <= maxIndex; ++i) {
1915 if (isChildItem(i)) {
1916 continue;
1917 }
1918 const QString newGroupValue = m_itemData.at(i)->values.value(role).toString();
1919 if (newGroupValue != groupValue || isFirstGroupValue) {
1920 groupValue = newGroupValue;
1921 groups.append(QPair<int, QVariant>(i, newGroupValue));
1922 isFirstGroupValue = false;
1923 }
1924 }
1925
1926 return groups;
1927 }
1928
1929 KFileItemList KFileItemModel::childItems(const KFileItem& item) const
1930 {
1931 KFileItemList items;
1932
1933 int index = m_items.value(item.url(), -1);
1934 if (index >= 0) {
1935 const int parentLevel = m_itemData.at(index)->values.value("expansionLevel").toInt();
1936 ++index;
1937 while (index < m_itemData.count() && m_itemData.at(index)->values.value("expansionLevel").toInt() > parentLevel) {
1938 items.append(m_itemData.at(index)->item);
1939 ++index;
1940 }
1941 }
1942
1943 return items;
1944 }
1945
1946 int KFileItemModel::fileSizeCompare(KIO::filesize_t a, KIO::filesize_t b)
1947 {
1948 if (a > b) {
1949 return +1;
1950 } else if (a < b) {
1951 return -1;
1952 } else {
1953 return 0;
1954 }
1955 }
1956
1957 #include "kfileitemmodel.moc"