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