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