]> cloud.milkyroute.net Git - dolphin.git/blob - src/kitemviews/kfileitemmodel.cpp
Details view: Siblings fixes
[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_expandedParentsCountRoot(UninitializedExpandedParentsCountRoot),
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() || item.isDesktopFile();
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 ExpandedParentsCountRole: 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 ExpandedParentsCountRole: 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[ExpandedParentsCountRole];
379 const bool willSupportExpanding = roles.contains("expandedParentsCount");
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 expandedParentsCount = data(index)["expandedParentsCount"].toInt();
443 ++index;
444 while (index < count() && data(index)["expandedParentsCount"].toInt() > expandedParentsCount) {
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 int KFileItemModel::expandedParentsCount(int index) const
472 {
473 if (index >= 0 && index < count()) {
474 return m_itemData.at(index)->values.value("expandedParentsCount").toInt();
475 }
476 return 0;
477 }
478
479 QSet<KUrl> KFileItemModel::expandedUrls() const
480 {
481 return m_expandedUrls;
482 }
483
484 void KFileItemModel::restoreExpandedUrls(const QSet<KUrl>& urls)
485 {
486 m_urlsToExpand = urls;
487 }
488
489 void KFileItemModel::expandParentItems(const KUrl& url)
490 {
491 const KDirLister* dirLister = m_dirLister.data();
492 if (!dirLister) {
493 return;
494 }
495
496 const int pos = dirLister->url().path().length();
497
498 // Assure that each sub-path of the URL that should be
499 // expanded is added to m_urlsToExpand. KDirLister
500 // does not care whether the parent-URL has already been
501 // expanded.
502 KUrl urlToExpand = dirLister->url();
503 const QStringList subDirs = url.path().mid(pos).split(QDir::separator());
504 for (int i = 0; i < subDirs.count() - 1; ++i) {
505 urlToExpand.addPath(subDirs.at(i));
506 m_urlsToExpand.insert(urlToExpand);
507 }
508
509 // KDirLister::open() must called at least once to trigger an initial
510 // loading. The pending URLs that must be restored are handled
511 // in slotCompleted().
512 QSetIterator<KUrl> it2(m_urlsToExpand);
513 while (it2.hasNext()) {
514 const int idx = index(it2.next());
515 if (idx >= 0 && !isExpanded(idx)) {
516 setExpanded(idx, true);
517 break;
518 }
519 }
520 }
521
522 void KFileItemModel::setNameFilter(const QString& nameFilter)
523 {
524 if (m_filter.pattern() != nameFilter) {
525 dispatchPendingItemsToInsert();
526
527 m_filter.setPattern(nameFilter);
528
529 // Check which shown items from m_itemData must get
530 // hidden and hence moved to m_filteredItems.
531 KFileItemList newFilteredItems;
532
533 foreach (ItemData* itemData, m_itemData) {
534 if (!m_filter.matches(itemData->item)) {
535 // Only filter non-expanded items as child items may never
536 // exist without a parent item
537 if (!itemData->values.value("isExpanded").toBool()) {
538 newFilteredItems.append(itemData->item);
539 m_filteredItems.insert(itemData->item);
540 }
541 }
542 }
543
544 removeItems(newFilteredItems);
545
546 // Check which hidden items from m_filteredItems should
547 // get visible again and hence removed from m_filteredItems.
548 KFileItemList newVisibleItems;
549
550 QMutableSetIterator<KFileItem> it(m_filteredItems);
551 while (it.hasNext()) {
552 const KFileItem item = it.next();
553 if (m_filter.matches(item)) {
554 newVisibleItems.append(item);
555 m_filteredItems.remove(item);
556 }
557 }
558
559 insertItems(newVisibleItems);
560 }
561 }
562
563 QString KFileItemModel::nameFilter() const
564 {
565 return m_filter.pattern();
566 }
567
568 void KFileItemModel::onGroupedSortingChanged(bool current)
569 {
570 Q_UNUSED(current);
571 m_groups.clear();
572 }
573
574 void KFileItemModel::onSortRoleChanged(const QByteArray& current, const QByteArray& previous)
575 {
576 Q_UNUSED(previous);
577 m_sortRole = roleIndex(current);
578
579 #ifdef KFILEITEMMODEL_DEBUG
580 if (!m_requestRole[m_sortRole]) {
581 kWarning() << "The sort-role has been changed to a role that has not been received yet";
582 }
583 #endif
584
585 resortAllItems();
586 }
587
588 void KFileItemModel::onSortOrderChanged(Qt::SortOrder current, Qt::SortOrder previous)
589 {
590 Q_UNUSED(current);
591 Q_UNUSED(previous);
592 resortAllItems();
593 }
594
595 void KFileItemModel::resortAllItems()
596 {
597 m_resortAllItemsTimer->stop();
598
599 const int itemCount = count();
600 if (itemCount <= 0) {
601 return;
602 }
603
604 #ifdef KFILEITEMMODEL_DEBUG
605 QElapsedTimer timer;
606 timer.start();
607 kDebug() << "===========================================================";
608 kDebug() << "Resorting" << itemCount << "items";
609 #endif
610
611 // Remember the order of the current URLs so
612 // that it can be determined which indexes have
613 // been moved because of the resorting.
614 QList<KUrl> oldUrls;
615 oldUrls.reserve(itemCount);
616 foreach (const ItemData* itemData, m_itemData) {
617 oldUrls.append(itemData->item.url());
618 }
619
620 m_groups.clear();
621 m_items.clear();
622
623 // Resort the items
624 sort(m_itemData.begin(), m_itemData.end());
625 for (int i = 0; i < itemCount; ++i) {
626 m_items.insert(m_itemData.at(i)->item.url(), i);
627 }
628
629 // Determine the indexes that have been moved
630 QList<int> movedToIndexes;
631 movedToIndexes.reserve(itemCount);
632 for (int i = 0; i < itemCount; i++) {
633 const int newIndex = m_items.value(oldUrls.at(i).url());
634 movedToIndexes.append(newIndex);
635 }
636
637 // Don't check whether items have really been moved and always emit a
638 // itemsMoved() signal after resorting: In case of grouped items
639 // the groups might change even if the items themselves don't change their
640 // position. Let the receiver of the signal decide whether a check for moved
641 // items makes sense.
642 emit itemsMoved(KItemRange(0, itemCount), movedToIndexes);
643
644 #ifdef KFILEITEMMODEL_DEBUG
645 kDebug() << "[TIME] Resorting of" << itemCount << "items:" << timer.elapsed();
646 #endif
647 }
648
649 void KFileItemModel::slotCompleted()
650 {
651 if (m_urlsToExpand.isEmpty() && m_minimumUpdateIntervalTimer->isActive()) {
652 // dispatchPendingItems() will be called when the timer
653 // has been expired.
654 m_pendingEmitLoadingCompleted = true;
655 return;
656 }
657
658 m_pendingEmitLoadingCompleted = false;
659 dispatchPendingItemsToInsert();
660
661 if (!m_urlsToExpand.isEmpty()) {
662 // Try to find a URL that can be expanded.
663 // Note that the parent folder must be expanded before any of its subfolders become visible.
664 // Therefore, some URLs in m_restoredExpandedUrls might not be visible yet
665 // -> we expand the first visible URL we find in m_restoredExpandedUrls.
666 foreach(const KUrl& url, m_urlsToExpand) {
667 const int index = m_items.value(url, -1);
668 if (index >= 0) {
669 m_urlsToExpand.remove(url);
670 if (setExpanded(index, true)) {
671 // The dir lister has been triggered. This slot will be called
672 // again after the directory has been expanded.
673 return;
674 }
675 }
676 }
677
678 // None of the URLs in m_restoredExpandedUrls could be found in the model. This can happen
679 // if these URLs have been deleted in the meantime.
680 m_urlsToExpand.clear();
681 }
682
683 emit loadingCompleted();
684 m_minimumUpdateIntervalTimer->start();
685 }
686
687 void KFileItemModel::slotCanceled()
688 {
689 m_minimumUpdateIntervalTimer->stop();
690 m_maximumUpdateIntervalTimer->stop();
691 dispatchPendingItemsToInsert();
692 }
693
694 void KFileItemModel::slotNewItems(const KFileItemList& items)
695 {
696 Q_ASSERT(!items.isEmpty());
697
698 if (m_requestRole[ExpandedParentsCountRole] && m_expandedParentsCountRoot >= 0) {
699 // To be able to compare whether the new items may be inserted as children
700 // of a parent item the pending items must be added to the model first.
701 dispatchPendingItemsToInsert();
702
703 KFileItem item = items.first();
704
705 // If the expanding of items is enabled, the call
706 // dirLister->openUrl(url, KDirLister::Keep) in KFileItemModel::setExpanded()
707 // might result in emitting the same items twice due to the Keep-parameter.
708 // This case happens if an item gets expanded, collapsed and expanded again
709 // before the items could be loaded for the first expansion.
710 const int index = m_items.value(item.url(), -1);
711 if (index >= 0) {
712 // The items are already part of the model.
713 return;
714 }
715
716 // KDirLister keeps the children of items that got expanded once even if
717 // they got collapsed again with KFileItemModel::setExpanded(false). So it must be
718 // checked whether the parent for new items is still expanded.
719 KUrl parentUrl = item.url().upUrl();
720 parentUrl.adjustPath(KUrl::RemoveTrailingSlash);
721 const int parentIndex = m_items.value(parentUrl, -1);
722 if (parentIndex >= 0 && !m_itemData[parentIndex]->values.value("isExpanded").toBool()) {
723 // The parent is not expanded.
724 return;
725 }
726 }
727
728 if (m_filter.pattern().isEmpty()) {
729 m_pendingItemsToInsert.append(items);
730 } else {
731 // The name-filter is active. Hide filtered items
732 // before inserting them into the model and remember
733 // the filtered items in m_filteredItems.
734 KFileItemList filteredItems;
735 foreach (const KFileItem& item, items) {
736 if (m_filter.matches(item)) {
737 filteredItems.append(item);
738 } else {
739 m_filteredItems.insert(item);
740 }
741 }
742
743 m_pendingItemsToInsert.append(filteredItems);
744 }
745
746 if (useMaximumUpdateInterval() && !m_maximumUpdateIntervalTimer->isActive()) {
747 // Assure that items get dispatched if no completed() or canceled() signal is
748 // emitted during the maximum update interval.
749 m_maximumUpdateIntervalTimer->start();
750 }
751 }
752
753 void KFileItemModel::slotItemsDeleted(const KFileItemList& items)
754 {
755 dispatchPendingItemsToInsert();
756
757 KFileItemList itemsToRemove = items;
758 if (m_requestRole[ExpandedParentsCountRole] && m_expandedParentsCountRoot >= 0) {
759 // Assure that removing a parent item also results in removing all children
760 foreach (const KFileItem& item, items) {
761 itemsToRemove.append(childItems(item));
762 }
763 }
764
765 if (!m_filteredItems.isEmpty()) {
766 foreach (const KFileItem& item, itemsToRemove) {
767 m_filteredItems.remove(item);
768 }
769 }
770
771 removeItems(itemsToRemove);
772 }
773
774 void KFileItemModel::slotRefreshItems(const QList<QPair<KFileItem, KFileItem> >& items)
775 {
776 Q_ASSERT(!items.isEmpty());
777 #ifdef KFILEITEMMODEL_DEBUG
778 kDebug() << "Refreshing" << items.count() << "items";
779 #endif
780
781 m_groups.clear();
782
783 // Get the indexes of all items that have been refreshed
784 QList<int> indexes;
785 indexes.reserve(items.count());
786
787 QListIterator<QPair<KFileItem, KFileItem> > it(items);
788 while (it.hasNext()) {
789 const QPair<KFileItem, KFileItem>& itemPair = it.next();
790 const KFileItem& oldItem = itemPair.first;
791 const KFileItem& newItem = itemPair.second;
792 const int index = m_items.value(oldItem.url(), -1);
793 if (index >= 0) {
794 m_itemData[index]->item = newItem;
795 m_itemData[index]->values = retrieveData(newItem);
796 m_items.remove(oldItem.url());
797 m_items.insert(newItem.url(), index);
798 indexes.append(index);
799 }
800 }
801
802 // If the changed items have been created recently, they might not be in m_items yet.
803 // In that case, the list 'indexes' might be empty.
804 if (indexes.isEmpty()) {
805 return;
806 }
807
808 // Extract the item-ranges out of the changed indexes
809 qSort(indexes);
810
811 KItemRangeList itemRangeList;
812 int previousIndex = indexes.at(0);
813 int rangeIndex = previousIndex;
814 int rangeCount = 1;
815
816 const int maxIndex = indexes.count() - 1;
817 for (int i = 1; i <= maxIndex; ++i) {
818 const int currentIndex = indexes.at(i);
819 if (currentIndex == previousIndex + 1) {
820 ++rangeCount;
821 } else {
822 itemRangeList.append(KItemRange(rangeIndex, rangeCount));
823
824 rangeIndex = currentIndex;
825 rangeCount = 1;
826 }
827 previousIndex = currentIndex;
828 }
829
830 if (rangeCount > 0) {
831 itemRangeList.append(KItemRange(rangeIndex, rangeCount));
832 }
833
834 emit itemsChanged(itemRangeList, m_roles);
835
836 resortAllItems();
837 }
838
839 void KFileItemModel::slotClear()
840 {
841 #ifdef KFILEITEMMODEL_DEBUG
842 kDebug() << "Clearing all items";
843 #endif
844
845 m_filteredItems.clear();
846 m_groups.clear();
847
848 m_minimumUpdateIntervalTimer->stop();
849 m_maximumUpdateIntervalTimer->stop();
850 m_resortAllItemsTimer->stop();
851 m_pendingItemsToInsert.clear();
852
853 m_expandedParentsCountRoot = UninitializedExpandedParentsCountRoot;
854
855 const int removedCount = m_itemData.count();
856 if (removedCount > 0) {
857 qDeleteAll(m_itemData);
858 m_itemData.clear();
859 m_items.clear();
860 emit itemsRemoved(KItemRangeList() << KItemRange(0, removedCount));
861 }
862
863 m_expandedUrls.clear();
864 }
865
866 void KFileItemModel::slotClear(const KUrl& url)
867 {
868 Q_UNUSED(url);
869 }
870
871 void KFileItemModel::slotNaturalSortingChanged()
872 {
873 m_naturalSorting = KGlobalSettings::naturalSorting();
874 resortAllItems();
875 }
876
877 void KFileItemModel::dispatchPendingItemsToInsert()
878 {
879 if (!m_pendingItemsToInsert.isEmpty()) {
880 insertItems(m_pendingItemsToInsert);
881 m_pendingItemsToInsert.clear();
882 }
883
884 if (m_pendingEmitLoadingCompleted) {
885 emit loadingCompleted();
886 }
887 }
888
889 void KFileItemModel::insertItems(const KFileItemList& items)
890 {
891 if (items.isEmpty()) {
892 return;
893 }
894
895 #ifdef KFILEITEMMODEL_DEBUG
896 QElapsedTimer timer;
897 timer.start();
898 kDebug() << "===========================================================";
899 kDebug() << "Inserting" << items.count() << "items";
900 #endif
901
902 m_groups.clear();
903
904 QList<ItemData*> sortedItems = createItemDataList(items);
905 sort(sortedItems.begin(), sortedItems.end());
906
907 #ifdef KFILEITEMMODEL_DEBUG
908 kDebug() << "[TIME] Sorting:" << timer.elapsed();
909 #endif
910
911 KItemRangeList itemRanges;
912 int targetIndex = 0;
913 int sourceIndex = 0;
914 int insertedAtIndex = -1; // Index for the current item-range
915 int insertedCount = 0; // Count for the current item-range
916 int previouslyInsertedCount = 0; // Sum of previously inserted items for all ranges
917 while (sourceIndex < sortedItems.count()) {
918 // Find target index from m_items to insert the current item
919 // in a sorted order
920 const int previousTargetIndex = targetIndex;
921 while (targetIndex < m_itemData.count()) {
922 if (!lessThan(m_itemData.at(targetIndex), sortedItems.at(sourceIndex))) {
923 break;
924 }
925 ++targetIndex;
926 }
927
928 if (targetIndex - previousTargetIndex > 0 && insertedAtIndex >= 0) {
929 itemRanges << KItemRange(insertedAtIndex, insertedCount);
930 previouslyInsertedCount += insertedCount;
931 insertedAtIndex = targetIndex - previouslyInsertedCount;
932 insertedCount = 0;
933 }
934
935 // Insert item at the position targetIndex by transfering
936 // the ownership of the item-data from sortedItems to m_itemData.
937 // m_items will be inserted after the loop (see comment below)
938 m_itemData.insert(targetIndex, sortedItems.at(sourceIndex));
939 ++insertedCount;
940
941 if (insertedAtIndex < 0) {
942 insertedAtIndex = targetIndex;
943 Q_ASSERT(previouslyInsertedCount == 0);
944 }
945 ++targetIndex;
946 ++sourceIndex;
947 }
948
949 // The indexes of all m_items must be adjusted, not only the index
950 // of the new items
951 const int itemDataCount = m_itemData.count();
952 for (int i = 0; i < itemDataCount; ++i) {
953 m_items.insert(m_itemData.at(i)->item.url(), i);
954 }
955
956 itemRanges << KItemRange(insertedAtIndex, insertedCount);
957 emit itemsInserted(itemRanges);
958
959 #ifdef KFILEITEMMODEL_DEBUG
960 kDebug() << "[TIME] Inserting of" << items.count() << "items:" << timer.elapsed();
961 #endif
962 }
963
964 void KFileItemModel::removeItems(const KFileItemList& items)
965 {
966 if (items.isEmpty()) {
967 return;
968 }
969
970 #ifdef KFILEITEMMODEL_DEBUG
971 kDebug() << "Removing " << items.count() << "items";
972 #endif
973
974 m_groups.clear();
975
976 QList<ItemData*> sortedItems;
977 sortedItems.reserve(items.count());
978 foreach (const KFileItem& item, items) {
979 const int index = m_items.value(item.url(), -1);
980 if (index >= 0) {
981 sortedItems.append(m_itemData.at(index));
982 }
983 }
984 sort(sortedItems.begin(), sortedItems.end());
985
986 QList<int> indexesToRemove;
987 indexesToRemove.reserve(items.count());
988
989 // Calculate the item ranges that will get deleted
990 KItemRangeList itemRanges;
991 int removedAtIndex = -1;
992 int removedCount = 0;
993 int targetIndex = 0;
994 foreach (const ItemData* itemData, sortedItems) {
995 const KFileItem& itemToRemove = itemData->item;
996
997 const int previousTargetIndex = targetIndex;
998 while (targetIndex < m_itemData.count()) {
999 if (m_itemData.at(targetIndex)->item.url() == itemToRemove.url()) {
1000 break;
1001 }
1002 ++targetIndex;
1003 }
1004 if (targetIndex >= m_itemData.count()) {
1005 kWarning() << "Item that should be deleted has not been found!";
1006 return;
1007 }
1008
1009 if (targetIndex - previousTargetIndex > 0 && removedAtIndex >= 0) {
1010 itemRanges << KItemRange(removedAtIndex, removedCount);
1011 removedAtIndex = targetIndex;
1012 removedCount = 0;
1013 }
1014
1015 indexesToRemove.append(targetIndex);
1016 if (removedAtIndex < 0) {
1017 removedAtIndex = targetIndex;
1018 }
1019 ++removedCount;
1020 ++targetIndex;
1021 }
1022
1023 // Delete the items
1024 for (int i = indexesToRemove.count() - 1; i >= 0; --i) {
1025 const int indexToRemove = indexesToRemove.at(i);
1026 ItemData* data = m_itemData.at(indexToRemove);
1027
1028 m_items.remove(data->item.url());
1029
1030 delete data;
1031 m_itemData.removeAt(indexToRemove);
1032 }
1033
1034 // The indexes of all m_items must be adjusted, not only the index
1035 // of the removed items
1036 const int itemDataCount = m_itemData.count();
1037 for (int i = 0; i < itemDataCount; ++i) {
1038 m_items.insert(m_itemData.at(i)->item.url(), i);
1039 }
1040
1041 if (count() <= 0) {
1042 m_expandedParentsCountRoot = UninitializedExpandedParentsCountRoot;
1043 }
1044
1045 itemRanges << KItemRange(removedAtIndex, removedCount);
1046 emit itemsRemoved(itemRanges);
1047 }
1048
1049 QList<KFileItemModel::ItemData*> KFileItemModel::createItemDataList(const KFileItemList& items) const
1050 {
1051 QList<ItemData*> itemDataList;
1052 itemDataList.reserve(items.count());
1053
1054 foreach (const KFileItem& item, items) {
1055 ItemData* itemData = new ItemData();
1056 itemData->item = item;
1057 itemData->values = retrieveData(item);
1058 itemData->parent = 0;
1059
1060 const bool determineParent = m_requestRole[ExpandedParentsCountRole]
1061 && itemData->values["expandedParentsCount"].toInt() > 0;
1062 if (determineParent) {
1063 KUrl parentUrl = item.url().upUrl();
1064 parentUrl.adjustPath(KUrl::RemoveTrailingSlash);
1065 const int parentIndex = m_items.value(parentUrl, -1);
1066 if (parentIndex >= 0) {
1067 itemData->parent = m_itemData.at(parentIndex);
1068 } else {
1069 kWarning() << "Parent item not found for" << item.url();
1070 }
1071 }
1072
1073 itemDataList.append(itemData);
1074 }
1075
1076 return itemDataList;
1077 }
1078
1079 void KFileItemModel::removeExpandedItems()
1080 {
1081 KFileItemList expandedItems;
1082
1083 const int maxIndex = m_itemData.count() - 1;
1084 for (int i = 0; i <= maxIndex; ++i) {
1085 const ItemData* itemData = m_itemData.at(i);
1086 if (itemData->values.value("expandedParentsCount").toInt() > 0) {
1087 expandedItems.append(itemData->item);
1088 }
1089 }
1090
1091 // The m_expandedParentsCountRoot may not get reset before all items with
1092 // a bigger count have been removed.
1093 Q_ASSERT(m_expandedParentsCountRoot >= 0);
1094 removeItems(expandedItems);
1095
1096 m_expandedParentsCountRoot = UninitializedExpandedParentsCountRoot;
1097 m_expandedUrls.clear();
1098 }
1099
1100 void KFileItemModel::resetRoles()
1101 {
1102 for (int i = 0; i < RolesCount; ++i) {
1103 m_requestRole[i] = false;
1104 }
1105 }
1106
1107 KFileItemModel::Role KFileItemModel::roleIndex(const QByteArray& role) const
1108 {
1109 static QHash<QByteArray, Role> rolesHash;
1110 if (rolesHash.isEmpty()) {
1111 rolesHash.insert("name", NameRole);
1112 rolesHash.insert("size", SizeRole);
1113 rolesHash.insert("date", DateRole);
1114 rolesHash.insert("permissions", PermissionsRole);
1115 rolesHash.insert("owner", OwnerRole);
1116 rolesHash.insert("group", GroupRole);
1117 rolesHash.insert("type", TypeRole);
1118 rolesHash.insert("destination", DestinationRole);
1119 rolesHash.insert("path", PathRole);
1120 rolesHash.insert("comment", CommentRole);
1121 rolesHash.insert("tags", TagsRole);
1122 rolesHash.insert("rating", RatingRole);
1123 rolesHash.insert("isDir", IsDirRole);
1124 rolesHash.insert("isExpanded", IsExpandedRole);
1125 rolesHash.insert("isExpandable", IsExpandableRole);
1126 rolesHash.insert("expandedParentsCount", ExpandedParentsCountRole);
1127 }
1128 return rolesHash.value(role, NoRole);
1129 }
1130
1131 QByteArray KFileItemModel::roleByteArray(Role role) const
1132 {
1133 static const char* const roles[RolesCount] = {
1134 0, // NoRole
1135 "name",
1136 "size",
1137 "date",
1138 "permissions",
1139 "owner",
1140 "group",
1141 "type",
1142 "destination",
1143 "path",
1144 "comment",
1145 "tags",
1146 "rating",
1147 "isDir",
1148 "isExpanded",
1149 "isExpandable",
1150 "expandedParentsCount"
1151 };
1152 return roles[role];
1153 }
1154
1155 QHash<QByteArray, QVariant> KFileItemModel::retrieveData(const KFileItem& item) const
1156 {
1157 // It is important to insert only roles that are fast to retrieve. E.g.
1158 // KFileItem::iconName() can be very expensive if the MIME-type is unknown
1159 // and hence will be retrieved asynchronously by KFileItemModelRolesUpdater.
1160 QHash<QByteArray, QVariant> data;
1161 data.insert("iconPixmap", QPixmap());
1162 data.insert("url", item.url());
1163
1164 const bool isDir = item.isDir();
1165 if (m_requestRole[IsDirRole]) {
1166 data.insert("isDir", isDir);
1167 }
1168
1169 if (m_requestRole[NameRole]) {
1170 data.insert("name", item.text());
1171 }
1172
1173 if (m_requestRole[SizeRole]) {
1174 if (isDir) {
1175 data.insert("size", QVariant());
1176 } else {
1177 data.insert("size", item.size());
1178 }
1179 }
1180
1181 if (m_requestRole[DateRole]) {
1182 // Don't use KFileItem::timeString() as this is too expensive when
1183 // having several thousands of items. Instead the formatting of the
1184 // date-time will be done on-demand by the view when the date will be shown.
1185 const KDateTime dateTime = item.time(KFileItem::ModificationTime);
1186 data.insert("date", dateTime.dateTime());
1187 }
1188
1189 if (m_requestRole[PermissionsRole]) {
1190 data.insert("permissions", item.permissionsString());
1191 }
1192
1193 if (m_requestRole[OwnerRole]) {
1194 data.insert("owner", item.user());
1195 }
1196
1197 if (m_requestRole[GroupRole]) {
1198 data.insert("group", item.group());
1199 }
1200
1201 if (m_requestRole[DestinationRole]) {
1202 QString destination = item.linkDest();
1203 if (destination.isEmpty()) {
1204 destination = i18nc("@item:intable", "No destination");
1205 }
1206 data.insert("destination", destination);
1207 }
1208
1209 if (m_requestRole[PathRole]) {
1210 QString path;
1211 if (item.url().protocol() == QLatin1String("trash")) {
1212 path = item.entry().stringValue(KIO::UDSEntry::UDS_EXTRA);
1213 } else {
1214 path = item.localPath();
1215 }
1216
1217 const int index = path.lastIndexOf(item.text());
1218 path = path.mid(0, index - 1);
1219 data.insert("path", path);
1220 }
1221
1222 if (m_requestRole[IsExpandedRole]) {
1223 data.insert("isExpanded", false);
1224 }
1225
1226 if (m_requestRole[IsExpandableRole]) {
1227 data.insert("isExpandable", item.isDir() && item.url() == item.targetUrl());
1228 }
1229
1230 if (m_requestRole[ExpandedParentsCountRole]) {
1231 if (m_expandedParentsCountRoot == UninitializedExpandedParentsCountRoot && m_dirLister.data()) {
1232 const KUrl rootUrl = m_dirLister.data()->url();
1233 const QString protocol = rootUrl.protocol();
1234 const bool forceExpandedParentsCountRoot = (protocol == QLatin1String("trash") ||
1235 protocol == QLatin1String("nepomuk") ||
1236 protocol == QLatin1String("remote") ||
1237 protocol.contains(QLatin1String("search")));
1238 if (forceExpandedParentsCountRoot) {
1239 m_expandedParentsCountRoot = ForceExpandedParentsCountRoot;
1240 } else {
1241 const QString rootDir = rootUrl.path(KUrl::AddTrailingSlash);
1242 m_expandedParentsCountRoot = rootDir.count('/');
1243 }
1244 }
1245
1246 if (m_expandedParentsCountRoot == ForceExpandedParentsCountRoot) {
1247 data.insert("expandedParentsCount", -1);
1248 } else {
1249 const QString dir = item.url().directory(KUrl::AppendTrailingSlash);
1250 const int level = dir.count('/') - m_expandedParentsCountRoot;
1251 data.insert("expandedParentsCount", level);
1252 }
1253 }
1254
1255 if (item.isMimeTypeKnown()) {
1256 data.insert("iconName", item.iconName());
1257
1258 if (m_requestRole[TypeRole]) {
1259 data.insert("type", item.mimeComment());
1260 }
1261 }
1262
1263 return data;
1264 }
1265
1266 bool KFileItemModel::lessThan(const ItemData* a, const ItemData* b) const
1267 {
1268 int result = 0;
1269
1270 if (m_expandedParentsCountRoot >= 0) {
1271 result = expandedParentsCountCompare(a, b);
1272 if (result != 0) {
1273 // The items have parents with different expansion levels
1274 return (sortOrder() == Qt::AscendingOrder) ? result < 0 : result > 0;
1275 }
1276 }
1277
1278 if (m_sortFoldersFirst || m_sortRole == SizeRole) {
1279 const bool isDirA = a->item.isDir();
1280 const bool isDirB = b->item.isDir();
1281 if (isDirA && !isDirB) {
1282 return true;
1283 } else if (!isDirA && isDirB) {
1284 return false;
1285 }
1286 }
1287
1288 result = sortRoleCompare(a, b);
1289
1290 return (sortOrder() == Qt::AscendingOrder) ? result < 0 : result > 0;
1291 }
1292
1293 int KFileItemModel::sortRoleCompare(const ItemData* a, const ItemData* b) const
1294 {
1295 const KFileItem& itemA = a->item;
1296 const KFileItem& itemB = b->item;
1297
1298 int result = 0;
1299
1300 switch (m_sortRole) {
1301 case NameRole:
1302 // The name role is handled as default fallback after the switch
1303 break;
1304
1305 case SizeRole: {
1306 if (itemA.isDir()) {
1307 // See "if (m_sortFoldersFirst || m_sortRole == SizeRole)" in KFileItemModel::lessThan():
1308 Q_ASSERT(itemB.isDir());
1309
1310 const QVariant valueA = a->values.value("size");
1311 const QVariant valueB = b->values.value("size");
1312 if (valueA.isNull() && valueB.isNull()) {
1313 result = 0;
1314 } else if (valueA.isNull()) {
1315 result = -1;
1316 } else if (valueB.isNull()) {
1317 result = +1;
1318 } else {
1319 result = valueA.toInt() - valueB.toInt();
1320 }
1321 } else {
1322 // See "if (m_sortFoldersFirst || m_sortRole == SizeRole)" in KFileItemModel::lessThan():
1323 Q_ASSERT(!itemB.isDir());
1324 const KIO::filesize_t sizeA = itemA.size();
1325 const KIO::filesize_t sizeB = itemB.size();
1326 if (sizeA > sizeB) {
1327 result = +1;
1328 } else if (sizeA < sizeB) {
1329 result = -1;
1330 } else {
1331 result = 0;
1332 }
1333 }
1334 break;
1335 }
1336
1337 case DateRole: {
1338 const KDateTime dateTimeA = itemA.time(KFileItem::ModificationTime);
1339 const KDateTime dateTimeB = itemB.time(KFileItem::ModificationTime);
1340 if (dateTimeA < dateTimeB) {
1341 result = -1;
1342 } else if (dateTimeA > dateTimeB) {
1343 result = +1;
1344 }
1345 break;
1346 }
1347
1348 case RatingRole: {
1349 result = a->values.value("rating").toInt() - b->values.value("rating").toInt();
1350 break;
1351 }
1352
1353 case PermissionsRole:
1354 case OwnerRole:
1355 case GroupRole:
1356 case TypeRole:
1357 case DestinationRole:
1358 case PathRole:
1359 case CommentRole:
1360 case TagsRole: {
1361 const QByteArray role = roleByteArray(m_sortRole);
1362 result = QString::compare(a->values.value(role).toString(),
1363 b->values.value(role).toString());
1364 break;
1365 }
1366
1367 default:
1368 break;
1369 }
1370
1371 if (result != 0) {
1372 // The current sort role was sufficient to define an order
1373 return result;
1374 }
1375
1376 // Fallback #1: Compare the text of the items
1377 result = stringCompare(itemA.text(), itemB.text());
1378 if (result != 0) {
1379 return result;
1380 }
1381
1382 // Fallback #2: KFileItem::text() may not be unique in case UDS_DISPLAY_NAME is used
1383 result = stringCompare(itemA.name(m_caseSensitivity == Qt::CaseInsensitive),
1384 itemB.name(m_caseSensitivity == Qt::CaseInsensitive));
1385 if (result != 0) {
1386 return result;
1387 }
1388
1389 // Fallback #3: It must be assured that the sort order is always unique even if two values have been
1390 // equal. In this case a comparison of the URL is done which is unique in all cases
1391 // within KDirLister.
1392 return QString::compare(itemA.url().url(), itemB.url().url(), Qt::CaseSensitive);
1393 }
1394
1395 void KFileItemModel::sort(QList<ItemData*>::iterator begin,
1396 QList<ItemData*>::iterator end)
1397 {
1398 // The implementation is based on qStableSortHelper() from qalgorithms.h
1399 // Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies).
1400 // In opposite to qStableSort() it allows to use a member-function for the comparison of elements.
1401
1402 const int span = end - begin;
1403 if (span < 2) {
1404 return;
1405 }
1406
1407 const QList<ItemData*>::iterator middle = begin + span / 2;
1408 sort(begin, middle);
1409 sort(middle, end);
1410 merge(begin, middle, end);
1411 }
1412
1413 void KFileItemModel::merge(QList<ItemData*>::iterator begin,
1414 QList<ItemData*>::iterator pivot,
1415 QList<ItemData*>::iterator end)
1416 {
1417 // The implementation is based on qMerge() from qalgorithms.h
1418 // Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies).
1419
1420 const int len1 = pivot - begin;
1421 const int len2 = end - pivot;
1422
1423 if (len1 == 0 || len2 == 0) {
1424 return;
1425 }
1426
1427 if (len1 + len2 == 2) {
1428 if (lessThan(*(begin + 1), *(begin))) {
1429 qSwap(*begin, *(begin + 1));
1430 }
1431 return;
1432 }
1433
1434 QList<ItemData*>::iterator firstCut;
1435 QList<ItemData*>::iterator secondCut;
1436 int len2Half;
1437 if (len1 > len2) {
1438 const int len1Half = len1 / 2;
1439 firstCut = begin + len1Half;
1440 secondCut = lowerBound(pivot, end, *firstCut);
1441 len2Half = secondCut - pivot;
1442 } else {
1443 len2Half = len2 / 2;
1444 secondCut = pivot + len2Half;
1445 firstCut = upperBound(begin, pivot, *secondCut);
1446 }
1447
1448 reverse(firstCut, pivot);
1449 reverse(pivot, secondCut);
1450 reverse(firstCut, secondCut);
1451
1452 const QList<ItemData*>::iterator newPivot = firstCut + len2Half;
1453 merge(begin, firstCut, newPivot);
1454 merge(newPivot, secondCut, end);
1455 }
1456
1457 QList<KFileItemModel::ItemData*>::iterator KFileItemModel::lowerBound(QList<ItemData*>::iterator begin,
1458 QList<ItemData*>::iterator end,
1459 const ItemData* value)
1460 {
1461 // The implementation is based on qLowerBound() from qalgorithms.h
1462 // Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies).
1463
1464 QList<ItemData*>::iterator middle;
1465 int n = int(end - begin);
1466 int half;
1467
1468 while (n > 0) {
1469 half = n >> 1;
1470 middle = begin + half;
1471 if (lessThan(*middle, value)) {
1472 begin = middle + 1;
1473 n -= half + 1;
1474 } else {
1475 n = half;
1476 }
1477 }
1478 return begin;
1479 }
1480
1481 QList<KFileItemModel::ItemData*>::iterator KFileItemModel::upperBound(QList<ItemData*>::iterator begin,
1482 QList<ItemData*>::iterator end,
1483 const ItemData* value)
1484 {
1485 // The implementation is based on qUpperBound() from qalgorithms.h
1486 // Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies).
1487
1488 QList<ItemData*>::iterator middle;
1489 int n = end - begin;
1490 int half;
1491
1492 while (n > 0) {
1493 half = n >> 1;
1494 middle = begin + half;
1495 if (lessThan(value, *middle)) {
1496 n = half;
1497 } else {
1498 begin = middle + 1;
1499 n -= half + 1;
1500 }
1501 }
1502 return begin;
1503 }
1504
1505 void KFileItemModel::reverse(QList<ItemData*>::iterator begin,
1506 QList<ItemData*>::iterator end)
1507 {
1508 // The implementation is based on qReverse() from qalgorithms.h
1509 // Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies).
1510
1511 --end;
1512 while (begin < end) {
1513 qSwap(*begin++, *end--);
1514 }
1515 }
1516
1517 int KFileItemModel::stringCompare(const QString& a, const QString& b) const
1518 {
1519 // Taken from KDirSortFilterProxyModel (kdelibs/kfile/kdirsortfilterproxymodel.*)
1520 // Copyright (C) 2006 by Peter Penz <peter.penz@gmx.at>
1521 // Copyright (C) 2006 by Dominic Battre <dominic@battre.de>
1522 // Copyright (C) 2006 by Martin Pool <mbp@canonical.com>
1523
1524 if (m_caseSensitivity == Qt::CaseInsensitive) {
1525 const int result = m_naturalSorting ? KStringHandler::naturalCompare(a, b, Qt::CaseInsensitive)
1526 : QString::compare(a, b, Qt::CaseInsensitive);
1527 if (result != 0) {
1528 // Only return the result, if the strings are not equal. If they are equal by a case insensitive
1529 // comparison, still a deterministic sort order is required. A case sensitive
1530 // comparison is done as fallback.
1531 return result;
1532 }
1533 }
1534
1535 return m_naturalSorting ? KStringHandler::naturalCompare(a, b, Qt::CaseSensitive)
1536 : QString::compare(a, b, Qt::CaseSensitive);
1537 }
1538
1539 int KFileItemModel::expandedParentsCountCompare(const ItemData* a, const ItemData* b) const
1540 {
1541 const KUrl urlA = a->item.url();
1542 const KUrl urlB = b->item.url();
1543 if (urlA.directory() == urlB.directory()) {
1544 // Both items have the same directory as parent
1545 return 0;
1546 }
1547
1548 // Check whether one item is the parent of the other item
1549 if (urlA.isParentOf(urlB)) {
1550 return (sortOrder() == Qt::AscendingOrder) ? -1 : +1;
1551 } else if (urlB.isParentOf(urlA)) {
1552 return (sortOrder() == Qt::AscendingOrder) ? +1 : -1;
1553 }
1554
1555 // Determine the maximum common path of both items and
1556 // remember the index in 'index'
1557 const QString pathA = urlA.path();
1558 const QString pathB = urlB.path();
1559
1560 const int maxIndex = qMin(pathA.length(), pathB.length()) - 1;
1561 int index = 0;
1562 while (index <= maxIndex && pathA.at(index) == pathB.at(index)) {
1563 ++index;
1564 }
1565 if (index > maxIndex) {
1566 index = maxIndex;
1567 }
1568 while ((pathA.at(index) != QLatin1Char('/') || pathB.at(index) != QLatin1Char('/')) && index > 0) {
1569 --index;
1570 }
1571
1572 // Determine the first sub-path after the common path and
1573 // check whether it represents a directory or already a file
1574 bool isDirA = true;
1575 const QString subPathA = subPath(a->item, pathA, index, &isDirA);
1576 bool isDirB = true;
1577 const QString subPathB = subPath(b->item, pathB, index, &isDirB);
1578
1579 if (isDirA && !isDirB) {
1580 return (sortOrder() == Qt::AscendingOrder) ? -1 : +1;
1581 } else if (!isDirA && isDirB) {
1582 return (sortOrder() == Qt::AscendingOrder) ? +1 : -1;
1583 }
1584
1585 // Compare the items of the parents that represent the first
1586 // different path after the common path.
1587 const QString parentPathA = pathA.left(index) + subPathA;
1588 const QString parentPathB = pathB.left(index) + subPathB;
1589
1590 const ItemData* parentA = a;
1591 while (parentA && parentA->item.url().path() != parentPathA) {
1592 parentA = parentA->parent;
1593 }
1594
1595 const ItemData* parentB = b;
1596 while (parentB && parentB->item.url().path() != parentPathB) {
1597 parentB = parentB->parent;
1598 }
1599
1600 if (parentA && parentB) {
1601 return sortRoleCompare(parentA, parentB);
1602 }
1603
1604 kWarning() << "Child items without parent detected:" << a->item.url() << b->item.url();
1605 return QString::compare(urlA.url(), urlB.url(), Qt::CaseSensitive);
1606 }
1607
1608 QString KFileItemModel::subPath(const KFileItem& item,
1609 const QString& itemPath,
1610 int start,
1611 bool* isDir) const
1612 {
1613 Q_ASSERT(isDir);
1614 const int pathIndex = itemPath.indexOf('/', start + 1);
1615 *isDir = (pathIndex > 0) || item.isDir();
1616 return itemPath.mid(start, pathIndex - start);
1617 }
1618
1619 bool KFileItemModel::useMaximumUpdateInterval() const
1620 {
1621 const KDirLister* dirLister = m_dirLister.data();
1622 return dirLister && !dirLister->url().isLocalFile();
1623 }
1624
1625 QList<QPair<int, QVariant> > KFileItemModel::nameRoleGroups() const
1626 {
1627 Q_ASSERT(!m_itemData.isEmpty());
1628
1629 const int maxIndex = count() - 1;
1630 QList<QPair<int, QVariant> > groups;
1631
1632 QString groupValue;
1633 QChar firstChar;
1634 bool isLetter = false;
1635 for (int i = 0; i <= maxIndex; ++i) {
1636 if (isChildItem(i)) {
1637 continue;
1638 }
1639
1640 const QString name = m_itemData.at(i)->values.value("name").toString();
1641
1642 // Use the first character of the name as group indication
1643 QChar newFirstChar = name.at(0).toUpper();
1644 if (newFirstChar == QLatin1Char('~') && name.length() > 1) {
1645 newFirstChar = name.at(1).toUpper();
1646 }
1647
1648 if (firstChar != newFirstChar) {
1649 QString newGroupValue;
1650 if (newFirstChar >= QLatin1Char('A') && newFirstChar <= QLatin1Char('Z')) {
1651 // Apply group 'A' - 'Z'
1652 newGroupValue = newFirstChar;
1653 isLetter = true;
1654 } else if (newFirstChar >= QLatin1Char('0') && newFirstChar <= QLatin1Char('9')) {
1655 // Apply group '0 - 9' for any name that starts with a digit
1656 newGroupValue = i18nc("@title:group Groups that start with a digit", "0 - 9");
1657 isLetter = false;
1658 } else {
1659 if (isLetter) {
1660 // If the current group is 'A' - 'Z' check whether a locale character
1661 // fits into the existing group.
1662 // TODO: This does not work in the case if e.g. the group 'O' starts with
1663 // an umlaut 'O' -> provide unit-test to document this known issue
1664 const QChar prevChar(firstChar.unicode() - ushort(1));
1665 const QChar nextChar(firstChar.unicode() + ushort(1));
1666 const QString currChar(newFirstChar);
1667 const bool partOfCurrentGroup = currChar.localeAwareCompare(prevChar) > 0 &&
1668 currChar.localeAwareCompare(nextChar) < 0;
1669 if (partOfCurrentGroup) {
1670 continue;
1671 }
1672 }
1673 newGroupValue = i18nc("@title:group", "Others");
1674 isLetter = false;
1675 }
1676
1677 if (newGroupValue != groupValue) {
1678 groupValue = newGroupValue;
1679 groups.append(QPair<int, QVariant>(i, newGroupValue));
1680 }
1681
1682 firstChar = newFirstChar;
1683 }
1684 }
1685 return groups;
1686 }
1687
1688 QList<QPair<int, QVariant> > KFileItemModel::sizeRoleGroups() const
1689 {
1690 Q_ASSERT(!m_itemData.isEmpty());
1691
1692 const int maxIndex = count() - 1;
1693 QList<QPair<int, QVariant> > groups;
1694
1695 QString groupValue;
1696 for (int i = 0; i <= maxIndex; ++i) {
1697 if (isChildItem(i)) {
1698 continue;
1699 }
1700
1701 const KFileItem& item = m_itemData.at(i)->item;
1702 const KIO::filesize_t fileSize = !item.isNull() ? item.size() : ~0U;
1703 QString newGroupValue;
1704 if (!item.isNull() && item.isDir()) {
1705 newGroupValue = i18nc("@title:group Size", "Folders");
1706 } else if (fileSize < 5 * 1024 * 1024) {
1707 newGroupValue = i18nc("@title:group Size", "Small");
1708 } else if (fileSize < 10 * 1024 * 1024) {
1709 newGroupValue = i18nc("@title:group Size", "Medium");
1710 } else {
1711 newGroupValue = i18nc("@title:group Size", "Big");
1712 }
1713
1714 if (newGroupValue != groupValue) {
1715 groupValue = newGroupValue;
1716 groups.append(QPair<int, QVariant>(i, newGroupValue));
1717 }
1718 }
1719
1720 return groups;
1721 }
1722
1723 QList<QPair<int, QVariant> > KFileItemModel::dateRoleGroups() const
1724 {
1725 Q_ASSERT(!m_itemData.isEmpty());
1726
1727 const int maxIndex = count() - 1;
1728 QList<QPair<int, QVariant> > groups;
1729
1730 const QDate currentDate = KDateTime::currentLocalDateTime().date();
1731
1732 int yearForCurrentWeek = 0;
1733 int currentWeek = currentDate.weekNumber(&yearForCurrentWeek);
1734 if (yearForCurrentWeek == currentDate.year() + 1) {
1735 currentWeek = 53;
1736 }
1737
1738 QDate previousModifiedDate;
1739 QString groupValue;
1740 for (int i = 0; i <= maxIndex; ++i) {
1741 if (isChildItem(i)) {
1742 continue;
1743 }
1744
1745 const KDateTime modifiedTime = m_itemData.at(i)->item.time(KFileItem::ModificationTime);
1746 const QDate modifiedDate = modifiedTime.date();
1747 if (modifiedDate == previousModifiedDate) {
1748 // The current item is in the same group as the previous item
1749 continue;
1750 }
1751 previousModifiedDate = modifiedDate;
1752
1753 const int daysDistance = modifiedDate.daysTo(currentDate);
1754
1755 int yearForModifiedWeek = 0;
1756 int modifiedWeek = modifiedDate.weekNumber(&yearForModifiedWeek);
1757 if (yearForModifiedWeek == modifiedDate.year() + 1) {
1758 modifiedWeek = 53;
1759 }
1760
1761 QString newGroupValue;
1762 if (currentDate.year() == modifiedDate.year() && currentDate.month() == modifiedDate.month()) {
1763 if (modifiedWeek > currentWeek) {
1764 // Usecase: modified date = 2010-01-01, current date = 2010-01-22
1765 // modified week = 53, current week = 3
1766 modifiedWeek = 0;
1767 }
1768 switch (currentWeek - modifiedWeek) {
1769 case 0:
1770 switch (daysDistance) {
1771 case 0: newGroupValue = i18nc("@title:group Date", "Today"); break;
1772 case 1: newGroupValue = i18nc("@title:group Date", "Yesterday"); break;
1773 default: newGroupValue = modifiedTime.toString(i18nc("@title:group The week day name: %A", "%A"));
1774 }
1775 break;
1776 case 1:
1777 newGroupValue = i18nc("@title:group Date", "Last Week");
1778 break;
1779 case 2:
1780 newGroupValue = i18nc("@title:group Date", "Two Weeks Ago");
1781 break;
1782 case 3:
1783 newGroupValue = i18nc("@title:group Date", "Three Weeks Ago");
1784 break;
1785 case 4:
1786 case 5:
1787 newGroupValue = i18nc("@title:group Date", "Earlier this Month");
1788 break;
1789 default:
1790 Q_ASSERT(false);
1791 }
1792 } else {
1793 const QDate lastMonthDate = currentDate.addMonths(-1);
1794 if (lastMonthDate.year() == modifiedDate.year() && lastMonthDate.month() == modifiedDate.month()) {
1795 if (daysDistance == 1) {
1796 newGroupValue = modifiedTime.toString(i18nc("@title:group Date: %B is full month name in current locale, and %Y is full year number", "Yesterday (%B, %Y)"));
1797 } else if (daysDistance <= 7) {
1798 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)"));
1799 } else if (daysDistance <= 7 * 2) {
1800 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)"));
1801 } else if (daysDistance <= 7 * 3) {
1802 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)"));
1803 } else if (daysDistance <= 7 * 4) {
1804 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)"));
1805 } else {
1806 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"));
1807 }
1808 } else {
1809 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"));
1810 }
1811 }
1812
1813 if (newGroupValue != groupValue) {
1814 groupValue = newGroupValue;
1815 groups.append(QPair<int, QVariant>(i, newGroupValue));
1816 }
1817 }
1818
1819 return groups;
1820 }
1821
1822 QList<QPair<int, QVariant> > KFileItemModel::permissionRoleGroups() const
1823 {
1824 Q_ASSERT(!m_itemData.isEmpty());
1825
1826 const int maxIndex = count() - 1;
1827 QList<QPair<int, QVariant> > groups;
1828
1829 QString permissionsString;
1830 QString groupValue;
1831 for (int i = 0; i <= maxIndex; ++i) {
1832 if (isChildItem(i)) {
1833 continue;
1834 }
1835
1836 const ItemData* itemData = m_itemData.at(i);
1837 const QString newPermissionsString = itemData->values.value("permissions").toString();
1838 if (newPermissionsString == permissionsString) {
1839 continue;
1840 }
1841 permissionsString = newPermissionsString;
1842
1843 const QFileInfo info(itemData->item.url().pathOrUrl());
1844
1845 // Set user string
1846 QString user;
1847 if (info.permission(QFile::ReadUser)) {
1848 user = i18nc("@item:intext Access permission, concatenated", "Read, ");
1849 }
1850 if (info.permission(QFile::WriteUser)) {
1851 user += i18nc("@item:intext Access permission, concatenated", "Write, ");
1852 }
1853 if (info.permission(QFile::ExeUser)) {
1854 user += i18nc("@item:intext Access permission, concatenated", "Execute, ");
1855 }
1856 user = user.isEmpty() ? i18nc("@item:intext Access permission, concatenated", "Forbidden") : user.mid(0, user.count() - 2);
1857
1858 // Set group string
1859 QString group;
1860 if (info.permission(QFile::ReadGroup)) {
1861 group = i18nc("@item:intext Access permission, concatenated", "Read, ");
1862 }
1863 if (info.permission(QFile::WriteGroup)) {
1864 group += i18nc("@item:intext Access permission, concatenated", "Write, ");
1865 }
1866 if (info.permission(QFile::ExeGroup)) {
1867 group += i18nc("@item:intext Access permission, concatenated", "Execute, ");
1868 }
1869 group = group.isEmpty() ? i18nc("@item:intext Access permission, concatenated", "Forbidden") : group.mid(0, group.count() - 2);
1870
1871 // Set others string
1872 QString others;
1873 if (info.permission(QFile::ReadOther)) {
1874 others = i18nc("@item:intext Access permission, concatenated", "Read, ");
1875 }
1876 if (info.permission(QFile::WriteOther)) {
1877 others += i18nc("@item:intext Access permission, concatenated", "Write, ");
1878 }
1879 if (info.permission(QFile::ExeOther)) {
1880 others += i18nc("@item:intext Access permission, concatenated", "Execute, ");
1881 }
1882 others = others.isEmpty() ? i18nc("@item:intext Access permission, concatenated", "Forbidden") : others.mid(0, others.count() - 2);
1883
1884 const QString newGroupValue = i18nc("@title:group Files and folders by permissions", "User: %1 | Group: %2 | Others: %3", user, group, others);
1885 if (newGroupValue != groupValue) {
1886 groupValue = newGroupValue;
1887 groups.append(QPair<int, QVariant>(i, newGroupValue));
1888 }
1889 }
1890
1891 return groups;
1892 }
1893
1894 QList<QPair<int, QVariant> > KFileItemModel::ratingRoleGroups() const
1895 {
1896 Q_ASSERT(!m_itemData.isEmpty());
1897
1898 const int maxIndex = count() - 1;
1899 QList<QPair<int, QVariant> > groups;
1900
1901 int groupValue = -1;
1902 for (int i = 0; i <= maxIndex; ++i) {
1903 if (isChildItem(i)) {
1904 continue;
1905 }
1906 const int newGroupValue = m_itemData.at(i)->values.value("rating", 0).toInt();
1907 if (newGroupValue != groupValue) {
1908 groupValue = newGroupValue;
1909 groups.append(QPair<int, QVariant>(i, newGroupValue));
1910 }
1911 }
1912
1913 return groups;
1914 }
1915
1916 QList<QPair<int, QVariant> > KFileItemModel::genericStringRoleGroups(const QByteArray& role) const
1917 {
1918 Q_ASSERT(!m_itemData.isEmpty());
1919
1920 const int maxIndex = count() - 1;
1921 QList<QPair<int, QVariant> > groups;
1922
1923 bool isFirstGroupValue = true;
1924 QString groupValue;
1925 for (int i = 0; i <= maxIndex; ++i) {
1926 if (isChildItem(i)) {
1927 continue;
1928 }
1929 const QString newGroupValue = m_itemData.at(i)->values.value(role).toString();
1930 if (newGroupValue != groupValue || isFirstGroupValue) {
1931 groupValue = newGroupValue;
1932 groups.append(QPair<int, QVariant>(i, newGroupValue));
1933 isFirstGroupValue = false;
1934 }
1935 }
1936
1937 return groups;
1938 }
1939
1940 KFileItemList KFileItemModel::childItems(const KFileItem& item) const
1941 {
1942 KFileItemList items;
1943
1944 int index = m_items.value(item.url(), -1);
1945 if (index >= 0) {
1946 const int parentLevel = m_itemData.at(index)->values.value("expandedParentsCount").toInt();
1947 ++index;
1948 while (index < m_itemData.count() && m_itemData.at(index)->values.value("expandedParentsCount").toInt() > parentLevel) {
1949 items.append(m_itemData.at(index)->item);
1950 ++index;
1951 }
1952 }
1953
1954 return items;
1955 }
1956
1957 #include "kfileitemmodel.moc"