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