]> cloud.milkyroute.net Git - dolphin.git/blob - src/kitemviews/kfileitemmodel.cpp
Remove workaround to draw the focus indicator manually
[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 <KLocale>
25 #include <KStringHandler>
26 #include <KDebug>
27
28 #include <QMimeData>
29 #include <QTimer>
30
31 #define KFILEITEMMODEL_DEBUG
32
33 KFileItemModel::KFileItemModel(KDirLister* dirLister, QObject* parent) :
34 KItemModelBase(QByteArray(), "name", parent),
35 m_dirLister(dirLister),
36 m_naturalSorting(true),
37 m_sortFoldersFirst(true),
38 m_groupRole(NoRole),
39 m_sortRole(NameRole),
40 m_caseSensitivity(Qt::CaseInsensitive),
41 m_sortedItems(),
42 m_items(),
43 m_data(),
44 m_requestRole(),
45 m_minimumUpdateIntervalTimer(0),
46 m_maximumUpdateIntervalTimer(0),
47 m_pendingItemsToInsert(),
48 m_pendingEmitLoadingCompleted(false),
49 m_rootExpansionLevel(-1),
50 m_expandedUrls(),
51 m_restoredExpandedUrls()
52 {
53 resetRoles();
54 m_requestRole[NameRole] = true;
55 m_requestRole[IsDirRole] = true;
56
57 Q_ASSERT(dirLister);
58
59 connect(dirLister, SIGNAL(canceled()), this, SLOT(slotCanceled()));
60 connect(dirLister, SIGNAL(completed()), this, SLOT(slotCompleted()));
61 connect(dirLister, SIGNAL(newItems(KFileItemList)), this, SLOT(slotNewItems(KFileItemList)));
62 connect(dirLister, SIGNAL(itemsDeleted(KFileItemList)), this, SLOT(slotItemsDeleted(KFileItemList)));
63 connect(dirLister, SIGNAL(refreshItems(QList<QPair<KFileItem,KFileItem> >)), this, SLOT(slotRefreshItems(QList<QPair<KFileItem,KFileItem> >)));
64 connect(dirLister, SIGNAL(clear()), this, SLOT(slotClear()));
65 connect(dirLister, SIGNAL(clear(KUrl)), this, SLOT(slotClear(KUrl)));
66
67 // Although the layout engine of KItemListView is fast it is very inefficient to e.g.
68 // emit 50 itemsInserted()-signals each 100 ms. m_minimumUpdateIntervalTimer assures that updates
69 // are done in 1 second intervals for equal operations.
70 m_minimumUpdateIntervalTimer = new QTimer(this);
71 m_minimumUpdateIntervalTimer->setInterval(1000);
72 m_minimumUpdateIntervalTimer->setSingleShot(true);
73 connect(m_minimumUpdateIntervalTimer, SIGNAL(timeout()), this, SLOT(dispatchPendingItemsToInsert()));
74
75 // For slow KIO-slaves like used for searching it makes sense to show results periodically even
76 // before the completed() or canceled() signal has been emitted.
77 m_maximumUpdateIntervalTimer = new QTimer(this);
78 m_maximumUpdateIntervalTimer->setInterval(2000);
79 m_maximumUpdateIntervalTimer->setSingleShot(true);
80 connect(m_maximumUpdateIntervalTimer, SIGNAL(timeout()), this, SLOT(dispatchPendingItemsToInsert()));
81
82 Q_ASSERT(m_minimumUpdateIntervalTimer->interval() <= m_maximumUpdateIntervalTimer->interval());
83 }
84
85 KFileItemModel::~KFileItemModel()
86 {
87 }
88
89 int KFileItemModel::count() const
90 {
91 return m_data.count();
92 }
93
94 QHash<QByteArray, QVariant> KFileItemModel::data(int index) const
95 {
96 if (index >= 0 && index < count()) {
97 return m_data.at(index);
98 }
99 return QHash<QByteArray, QVariant>();
100 }
101
102 bool KFileItemModel::setData(int index, const QHash<QByteArray, QVariant>& values)
103 {
104 if (index >= 0 && index < count()) {
105 QHash<QByteArray, QVariant> currentValue = m_data.at(index);
106
107 QSet<QByteArray> changedRoles;
108 QHashIterator<QByteArray, QVariant> it(values);
109 while (it.hasNext()) {
110 it.next();
111 const QByteArray role = it.key();
112 const QVariant value = it.value();
113
114 if (currentValue[role] != value) {
115 currentValue[role] = value;
116 changedRoles.insert(role);
117 }
118 }
119
120 if (!changedRoles.isEmpty()) {
121 m_data[index] = currentValue;
122 emit itemsChanged(KItemRangeList() << KItemRange(index, 1), changedRoles);
123 }
124
125 return true;
126 }
127 return false;
128 }
129
130 bool KFileItemModel::supportsGrouping() const
131 {
132 return true;
133 }
134
135 bool KFileItemModel::supportsSorting() const
136 {
137 return true;
138 }
139
140 void KFileItemModel::setSortFoldersFirst(bool foldersFirst)
141 {
142 if (foldersFirst != m_sortFoldersFirst) {
143 m_sortFoldersFirst = foldersFirst;
144 resortAllItems();
145 }
146 }
147
148 bool KFileItemModel::sortFoldersFirst() const
149 {
150 return m_sortFoldersFirst;
151 }
152
153 QMimeData* KFileItemModel::createMimeData(const QSet<int>& indexes) const
154 {
155 QMimeData* data = new QMimeData();
156
157 // The following code has been taken from KDirModel::mimeData()
158 // (kdelibs/kio/kio/kdirmodel.cpp)
159 // Copyright (C) 2006 David Faure <faure@kde.org>
160 KUrl::List urls;
161 KUrl::List mostLocalUrls;
162 bool canUseMostLocalUrls = true;
163
164 QSetIterator<int> it(indexes);
165 while (it.hasNext()) {
166 const int index = it.next();
167 const KFileItem item = fileItem(index);
168 if (!item.isNull()) {
169 urls << item.url();
170
171 bool isLocal;
172 mostLocalUrls << item.mostLocalUrl(isLocal);
173 if (!isLocal) {
174 canUseMostLocalUrls = false;
175 }
176 }
177 }
178
179 const bool different = canUseMostLocalUrls && mostLocalUrls != urls;
180 urls = KDirModel::simplifiedUrlList(urls); // TODO: Check if we still need KDirModel for this in KDE 5.0
181 if (different) {
182 mostLocalUrls = KDirModel::simplifiedUrlList(mostLocalUrls);
183 urls.populateMimeData(mostLocalUrls, data);
184 } else {
185 urls.populateMimeData(data);
186 }
187
188 return data;
189 }
190
191 int KFileItemModel::indexForKeyboardSearch(const QString& text, int startFromIndex) const
192 {
193 startFromIndex = qMax(0, startFromIndex);
194 for (int i = startFromIndex; i < count(); ++i) {
195 if (data(i)["name"].toString().startsWith(text, Qt::CaseInsensitive)) {
196 return i;
197 }
198 }
199 for (int i = 0; i < startFromIndex; ++i) {
200 if (data(i)["name"].toString().startsWith(text, Qt::CaseInsensitive)) {
201 return i;
202 }
203 }
204 return -1;
205 }
206
207 bool KFileItemModel::supportsDropping(int index) const
208 {
209 const KFileItem item = fileItem(index);
210 return item.isNull() ? false : item.isDir();
211 }
212
213 QString KFileItemModel::roleDescription(const QByteArray& role) const
214 {
215 QString descr;
216
217 switch (roleIndex(role)) {
218 case NameRole: descr = i18nc("@item:intable", "Name"); break;
219 case SizeRole: descr = i18nc("@item:intable", "Size"); break;
220 case DateRole: descr = i18nc("@item:intable", "Date"); break;
221 case PermissionsRole: descr = i18nc("@item:intable", "Permissions"); break;
222 case OwnerRole: descr = i18nc("@item:intable", "Owner"); break;
223 case GroupRole: descr = i18nc("@item:intable", "Group"); break;
224 case TypeRole: descr = i18nc("@item:intable", "Type"); break;
225 case DestinationRole: descr = i18nc("@item:intable", "Destination"); break;
226 case PathRole: descr = i18nc("@item:intable", "Path"); break;
227 case NoRole: break;
228 case IsDirRole: break;
229 case IsExpandedRole: break;
230 case ExpansionLevelRole: break;
231 default: Q_ASSERT(false); break;
232 }
233
234 return descr;
235 }
236
237 KFileItem KFileItemModel::fileItem(int index) const
238 {
239 if (index >= 0 && index < count()) {
240 return m_sortedItems.at(index);
241 }
242
243 return KFileItem();
244 }
245
246 KFileItem KFileItemModel::fileItem(const KUrl& url) const
247 {
248 const int index = m_items.value(url, -1);
249 if (index >= 0) {
250 return m_sortedItems.at(index);
251 }
252 return KFileItem();
253 }
254
255 int KFileItemModel::index(const KFileItem& item) const
256 {
257 if (item.isNull()) {
258 return -1;
259 }
260
261 return m_items.value(item.url(), -1);
262 }
263
264 int KFileItemModel::index(const KUrl& url) const
265 {
266 KUrl urlToFind = url;
267 urlToFind.adjustPath(KUrl::RemoveTrailingSlash);
268 return m_items.value(urlToFind, -1);
269 }
270
271 KFileItem KFileItemModel::rootItem() const
272 {
273 const KDirLister* dirLister = m_dirLister.data();
274 if (dirLister) {
275 return dirLister->rootItem();
276 }
277 return KFileItem();
278 }
279
280 void KFileItemModel::clear()
281 {
282 slotClear();
283 }
284
285 void KFileItemModel::setRoles(const QSet<QByteArray>& roles)
286 {
287 if (count() > 0) {
288 const bool supportedExpanding = m_requestRole[IsExpandedRole] && m_requestRole[ExpansionLevelRole];
289 const bool willSupportExpanding = roles.contains("isExpanded") && roles.contains("expansionLevel");
290 if (supportedExpanding && !willSupportExpanding) {
291 // No expanding is supported anymore. Take care to delete all items that have an expansion level
292 // that is not 0 (and hence are part of an expanded item).
293 removeExpandedItems();
294 }
295 }
296
297 resetRoles();
298 QSetIterator<QByteArray> it(roles);
299 while (it.hasNext()) {
300 const QByteArray& role = it.next();
301 m_requestRole[roleIndex(role)] = true;
302 }
303
304 if (count() > 0) {
305 // Update m_data with the changed requested roles
306 const int maxIndex = count() - 1;
307 for (int i = 0; i <= maxIndex; ++i) {
308 m_data[i] = retrieveData(m_sortedItems.at(i));
309 }
310
311 kWarning() << "TODO: Emitting itemsChanged() with no information what has changed!";
312 emit itemsChanged(KItemRangeList() << KItemRange(0, count()), QSet<QByteArray>());
313 }
314 }
315
316 QSet<QByteArray> KFileItemModel::roles() const
317 {
318 QSet<QByteArray> roles;
319 for (int i = 0; i < RolesCount; ++i) {
320 if (m_requestRole[i]) {
321 switch (i) {
322 case NoRole: break;
323 case NameRole: roles.insert("name"); break;
324 case SizeRole: roles.insert("size"); break;
325 case DateRole: roles.insert("date"); break;
326 case PermissionsRole: roles.insert("permissions"); break;
327 case OwnerRole: roles.insert("owner"); break;
328 case GroupRole: roles.insert("group"); break;
329 case TypeRole: roles.insert("type"); break;
330 case DestinationRole: roles.insert("destination"); break;
331 case PathRole: roles.insert("path"); break;
332 case IsDirRole: roles.insert("isDir"); break;
333 case IsExpandedRole: roles.insert("isExpanded"); break;
334 case ExpansionLevelRole: roles.insert("expansionLevel"); break;
335 default: Q_ASSERT(false); break;
336 }
337 }
338 }
339 return roles;
340 }
341
342 bool KFileItemModel::setExpanded(int index, bool expanded)
343 {
344 if (isExpanded(index) == expanded || index < 0 || index >= count()) {
345 return false;
346 }
347
348 QHash<QByteArray, QVariant> values;
349 values.insert("isExpanded", expanded);
350 if (!setData(index, values)) {
351 return false;
352 }
353
354 const KUrl url = m_sortedItems.at(index).url();
355 if (expanded) {
356 m_expandedUrls.insert(url);
357
358 KDirLister* dirLister = m_dirLister.data();
359 if (dirLister) {
360 dirLister->openUrl(url, KDirLister::Keep);
361 return true;
362 }
363 } else {
364 m_expandedUrls.remove(url);
365
366 KFileItemList itemsToRemove;
367 const int expansionLevel = data(index)["expansionLevel"].toInt();
368 ++index;
369 while (index < count() && data(index)["expansionLevel"].toInt() > expansionLevel) {
370 itemsToRemove.append(m_sortedItems.at(index));
371 ++index;
372 }
373 removeItems(itemsToRemove);
374 return true;
375 }
376
377 return false;
378 }
379
380 bool KFileItemModel::isExpanded(int index) const
381 {
382 if (index >= 0 && index < count()) {
383 return m_data.at(index).value("isExpanded").toBool();
384 }
385 return false;
386 }
387
388 bool KFileItemModel::isExpandable(int index) const
389 {
390 if (index >= 0 && index < count()) {
391 return m_sortedItems.at(index).isDir();
392 }
393 return false;
394 }
395
396 QSet<KUrl> KFileItemModel::expandedUrls() const
397 {
398 return m_expandedUrls;
399 }
400
401 void KFileItemModel::restoreExpandedUrls(const QSet<KUrl>& urls)
402 {
403 m_restoredExpandedUrls = urls;
404 }
405
406 void KFileItemModel::onGroupRoleChanged(const QByteArray& current, const QByteArray& previous)
407 {
408 Q_UNUSED(previous);
409 m_groupRole = roleIndex(current);
410 }
411
412 void KFileItemModel::onSortRoleChanged(const QByteArray& current, const QByteArray& previous)
413 {
414 Q_UNUSED(previous);
415 m_sortRole = roleIndex(current);
416 resortAllItems();
417 }
418
419 void KFileItemModel::onSortOrderChanged(Qt::SortOrder current, Qt::SortOrder previous)
420 {
421 Q_UNUSED(current);
422 Q_UNUSED(previous);
423 resortAllItems();
424 }
425
426 void KFileItemModel::slotCompleted()
427 {
428 if (m_restoredExpandedUrls.isEmpty() && m_minimumUpdateIntervalTimer->isActive()) {
429 // dispatchPendingItems() will be called when the timer
430 // has been expired.
431 m_pendingEmitLoadingCompleted = true;
432 return;
433 }
434
435 m_pendingEmitLoadingCompleted = false;
436 dispatchPendingItemsToInsert();
437
438 if (!m_restoredExpandedUrls.isEmpty()) {
439 // Try to find a URL that can be expanded.
440 // Note that the parent folder must be expanded before any of its subfolders become visible.
441 // Therefore, some URLs in m_restoredExpandedUrls might not be visible yet
442 // -> we expand the first visible URL we find in m_restoredExpandedUrls.
443 foreach(const KUrl& url, m_restoredExpandedUrls) {
444 const int index = m_items.value(url, -1);
445 if (index >= 0) {
446 // We have found an expandable URL. Expand it and return - when
447 // the dir lister has finished, this slot will be called again.
448 m_restoredExpandedUrls.remove(url);
449 setExpanded(index, true);
450 return;
451 }
452 }
453
454 // None of the URLs in m_restoredExpandedUrls could be found in the model. This can happen
455 // if these URLs have been deleted in the meantime.
456 m_restoredExpandedUrls.clear();
457 }
458
459 emit loadingCompleted();
460 m_minimumUpdateIntervalTimer->start();
461 }
462
463 void KFileItemModel::slotCanceled()
464 {
465 m_minimumUpdateIntervalTimer->stop();
466 m_maximumUpdateIntervalTimer->stop();
467 dispatchPendingItemsToInsert();
468 }
469
470 void KFileItemModel::slotNewItems(const KFileItemList& items)
471 {
472 m_pendingItemsToInsert.append(items);
473
474 if (useMaximumUpdateInterval() && !m_maximumUpdateIntervalTimer->isActive()) {
475 // Assure that items get dispatched if no completed() or canceled() signal is
476 // emitted during the maximum update interval.
477 m_maximumUpdateIntervalTimer->start();
478 }
479 }
480
481 void KFileItemModel::slotItemsDeleted(const KFileItemList& items)
482 {
483 if (!m_pendingItemsToInsert.isEmpty()) {
484 insertItems(m_pendingItemsToInsert);
485 m_pendingItemsToInsert.clear();
486 }
487 removeItems(items);
488 }
489
490 void KFileItemModel::slotRefreshItems(const QList<QPair<KFileItem, KFileItem> >& items)
491 {
492 Q_ASSERT(!items.isEmpty());
493 #ifdef KFILEITEMMODEL_DEBUG
494 kDebug() << "Refreshing" << items.count() << "items";
495 #endif
496
497 // Get the indexes of all items that have been refreshed
498 QList<int> indexes;
499 indexes.reserve(items.count());
500
501 QListIterator<QPair<KFileItem, KFileItem> > it(items);
502 while (it.hasNext()) {
503 const QPair<KFileItem, KFileItem>& itemPair = it.next();
504 const int index = m_items.value(itemPair.second.url(), -1);
505 if (index >= 0) {
506 indexes.append(index);
507 }
508 }
509
510 // If the changed items have been created recently, they might not be in m_items yet.
511 // In that case, the list 'indexes' might be empty.
512 if (indexes.isEmpty()) {
513 return;
514 }
515
516 // Extract the item-ranges out of the changed indexes
517 qSort(indexes);
518
519 KItemRangeList itemRangeList;
520 int rangeIndex = 0;
521 int rangeCount = 1;
522 int previousIndex = indexes.at(0);
523
524 const int maxIndex = indexes.count() - 1;
525 for (int i = 1; i <= maxIndex; ++i) {
526 const int currentIndex = indexes.at(i);
527 if (currentIndex == previousIndex + 1) {
528 ++rangeCount;
529 } else {
530 itemRangeList.append(KItemRange(rangeIndex, rangeCount));
531
532 rangeIndex = currentIndex;
533 rangeCount = 1;
534 }
535 previousIndex = currentIndex;
536 }
537
538 if (rangeCount > 0) {
539 itemRangeList.append(KItemRange(rangeIndex, rangeCount));
540 }
541
542 emit itemsChanged(itemRangeList, QSet<QByteArray>());
543 }
544
545 void KFileItemModel::slotClear()
546 {
547 #ifdef KFILEITEMMODEL_DEBUG
548 kDebug() << "Clearing all items";
549 #endif
550
551 m_minimumUpdateIntervalTimer->stop();
552 m_maximumUpdateIntervalTimer->stop();
553 m_pendingItemsToInsert.clear();
554
555 m_rootExpansionLevel = -1;
556
557 const int removedCount = m_data.count();
558 if (removedCount > 0) {
559 m_sortedItems.clear();
560 m_items.clear();
561 m_data.clear();
562 emit itemsRemoved(KItemRangeList() << KItemRange(0, removedCount));
563 }
564
565 m_expandedUrls.clear();
566 }
567
568 void KFileItemModel::slotClear(const KUrl& url)
569 {
570 Q_UNUSED(url);
571 }
572
573 void KFileItemModel::dispatchPendingItemsToInsert()
574 {
575 if (!m_pendingItemsToInsert.isEmpty()) {
576 insertItems(m_pendingItemsToInsert);
577 m_pendingItemsToInsert.clear();
578 }
579
580 if (m_pendingEmitLoadingCompleted) {
581 emit loadingCompleted();
582 }
583 }
584
585 void KFileItemModel::insertItems(const KFileItemList& items)
586 {
587 if (items.isEmpty()) {
588 return;
589 }
590
591 #ifdef KFILEITEMMODEL_DEBUG
592 QElapsedTimer timer;
593 timer.start();
594 kDebug() << "===========================================================";
595 kDebug() << "Inserting" << items.count() << "items";
596 #endif
597
598 KFileItemList sortedItems = items;
599 sort(sortedItems.begin(), sortedItems.end());
600
601 #ifdef KFILEITEMMODEL_DEBUG
602 kDebug() << "[TIME] Sorting:" << timer.elapsed();
603 #endif
604
605 KItemRangeList itemRanges;
606 int targetIndex = 0;
607 int sourceIndex = 0;
608 int insertedAtIndex = -1; // Index for the current item-range
609 int insertedCount = 0; // Count for the current item-range
610 int previouslyInsertedCount = 0; // Sum of previously inserted items for all ranges
611 while (sourceIndex < sortedItems.count()) {
612 // Find target index from m_items to insert the current item
613 // in a sorted order
614 const int previousTargetIndex = targetIndex;
615 while (targetIndex < m_sortedItems.count()) {
616 if (!lessThan(m_sortedItems.at(targetIndex), sortedItems.at(sourceIndex))) {
617 break;
618 }
619 ++targetIndex;
620 }
621
622 if (targetIndex - previousTargetIndex > 0 && insertedAtIndex >= 0) {
623 itemRanges << KItemRange(insertedAtIndex, insertedCount);
624 previouslyInsertedCount += insertedCount;
625 insertedAtIndex = targetIndex - previouslyInsertedCount;
626 insertedCount = 0;
627 }
628
629 // Insert item at the position targetIndex
630 const KFileItem item = sortedItems.at(sourceIndex);
631 m_sortedItems.insert(targetIndex, item);
632 m_data.insert(targetIndex, retrieveData(item));
633 // m_items will be inserted after the loop (see comment below)
634 ++insertedCount;
635
636 if (insertedAtIndex < 0) {
637 insertedAtIndex = targetIndex;
638 Q_ASSERT(previouslyInsertedCount == 0);
639 }
640 ++targetIndex;
641 ++sourceIndex;
642 }
643
644 // The indexes of all m_items must be adjusted, not only the index
645 // of the new items
646 for (int i = 0; i < m_sortedItems.count(); ++i) {
647 m_items.insert(m_sortedItems.at(i).url(), i);
648 }
649
650 itemRanges << KItemRange(insertedAtIndex, insertedCount);
651 emit itemsInserted(itemRanges);
652
653 #ifdef KFILEITEMMODEL_DEBUG
654 kDebug() << "[TIME] Inserting of" << items.count() << "items:" << timer.elapsed();
655 #endif
656 }
657
658 void KFileItemModel::removeItems(const KFileItemList& items)
659 {
660 if (items.isEmpty()) {
661 return;
662 }
663
664 #ifdef KFILEITEMMODEL_DEBUG
665 kDebug() << "Removing " << items.count() << "items";
666 #endif
667
668 KFileItemList sortedItems = items;
669 sort(sortedItems.begin(), sortedItems.end());
670
671 QList<int> indexesToRemove;
672 indexesToRemove.reserve(items.count());
673
674 // Calculate the item ranges that will get deleted
675 KItemRangeList itemRanges;
676 int removedAtIndex = -1;
677 int removedCount = 0;
678 int targetIndex = 0;
679 foreach (const KFileItem& itemToRemove, sortedItems) {
680 const int previousTargetIndex = targetIndex;
681 while (targetIndex < m_sortedItems.count()) {
682 if (m_sortedItems.at(targetIndex).url() == itemToRemove.url()) {
683 break;
684 }
685 ++targetIndex;
686 }
687 if (targetIndex >= m_sortedItems.count()) {
688 kWarning() << "Item that should be deleted has not been found!";
689 return;
690 }
691
692 if (targetIndex - previousTargetIndex > 0 && removedAtIndex >= 0) {
693 itemRanges << KItemRange(removedAtIndex, removedCount);
694 removedAtIndex = targetIndex;
695 removedCount = 0;
696 }
697
698 indexesToRemove.append(targetIndex);
699 if (removedAtIndex < 0) {
700 removedAtIndex = targetIndex;
701 }
702 ++removedCount;
703 ++targetIndex;
704 }
705
706 // Delete the items
707 for (int i = indexesToRemove.count() - 1; i >= 0; --i) {
708 const int indexToRemove = indexesToRemove.at(i);
709 m_items.remove(m_sortedItems.at(indexToRemove).url());
710 m_sortedItems.removeAt(indexToRemove);
711 m_data.removeAt(indexToRemove);
712 }
713
714 // The indexes of all m_items must be adjusted, not only the index
715 // of the removed items
716 for (int i = 0; i < m_sortedItems.count(); ++i) {
717 m_items.insert(m_sortedItems.at(i).url(), i);
718 }
719
720 if (count() <= 0) {
721 m_rootExpansionLevel = -1;
722 }
723
724 itemRanges << KItemRange(removedAtIndex, removedCount);
725 emit itemsRemoved(itemRanges);
726 }
727
728 void KFileItemModel::resortAllItems()
729 {
730 const int itemCount = count();
731 if (itemCount <= 0) {
732 return;
733 }
734
735 const KFileItemList oldSortedItems = m_sortedItems;
736 const QHash<KUrl, int> oldItems = m_items;
737 const QList<QHash<QByteArray, QVariant> > oldData = m_data;
738
739 m_items.clear();
740 m_data.clear();
741
742 sort(m_sortedItems.begin(), m_sortedItems.end());
743 int index = 0;
744 foreach (const KFileItem& item, m_sortedItems) {
745 m_items.insert(item.url(), index);
746
747 const int oldItemIndex = oldItems.value(item.url());
748 m_data.append(oldData.at(oldItemIndex));
749
750 ++index;
751 }
752
753 bool emitItemsMoved = false;
754 QList<int> movedToIndexes;
755 movedToIndexes.reserve(m_sortedItems.count());
756 for (int i = 0; i < itemCount; i++) {
757 const int newIndex = m_items.value(oldSortedItems.at(i).url());
758 movedToIndexes.append(newIndex);
759 if (!emitItemsMoved && newIndex != i) {
760 emitItemsMoved = true;
761 }
762 }
763
764 if (emitItemsMoved) {
765 emit itemsMoved(KItemRange(0, itemCount), movedToIndexes);
766 }
767 }
768
769 void KFileItemModel::removeExpandedItems()
770 {
771 KFileItemList expandedItems;
772
773 const int maxIndex = m_data.count() - 1;
774 for (int i = 0; i <= maxIndex; ++i) {
775 if (m_data.at(i).value("expansionLevel").toInt() > 0) {
776 const KFileItem fileItem = m_sortedItems.at(i);
777 expandedItems.append(fileItem);
778 }
779 }
780
781 // The m_rootExpansionLevel may not get reset before all items with
782 // a bigger expansionLevel have been removed.
783 Q_ASSERT(m_rootExpansionLevel >= 0);
784 removeItems(expandedItems);
785
786 m_rootExpansionLevel = -1;
787 m_expandedUrls.clear();
788 }
789
790 void KFileItemModel::resetRoles()
791 {
792 for (int i = 0; i < RolesCount; ++i) {
793 m_requestRole[i] = false;
794 }
795 }
796
797 KFileItemModel::Role KFileItemModel::roleIndex(const QByteArray& role) const
798 {
799 static QHash<QByteArray, Role> rolesHash;
800 if (rolesHash.isEmpty()) {
801 rolesHash.insert("name", NameRole);
802 rolesHash.insert("size", SizeRole);
803 rolesHash.insert("date", DateRole);
804 rolesHash.insert("permissions", PermissionsRole);
805 rolesHash.insert("owner", OwnerRole);
806 rolesHash.insert("group", GroupRole);
807 rolesHash.insert("type", TypeRole);
808 rolesHash.insert("destination", DestinationRole);
809 rolesHash.insert("path", PathRole);
810 rolesHash.insert("isDir", IsDirRole);
811 rolesHash.insert("isExpanded", IsExpandedRole);
812 rolesHash.insert("expansionLevel", ExpansionLevelRole);
813 }
814 return rolesHash.value(role, NoRole);
815 }
816
817 QHash<QByteArray, QVariant> KFileItemModel::retrieveData(const KFileItem& item) const
818 {
819 // It is important to insert only roles that are fast to retrieve. E.g.
820 // KFileItem::iconName() can be very expensive if the MIME-type is unknown
821 // and hence will be retrieved asynchronously by KFileItemModelRolesUpdater.
822 QHash<QByteArray, QVariant> data;
823 data.insert("iconPixmap", QPixmap());
824
825 const bool isDir = item.isDir();
826 if (m_requestRole[IsDirRole]) {
827 data.insert("isDir", isDir);
828 }
829
830 if (m_requestRole[NameRole]) {
831 data.insert("name", item.name());
832 }
833
834 if (m_requestRole[SizeRole]) {
835 if (isDir) {
836 data.insert("size", QVariant());
837 } else {
838 data.insert("size", item.size());
839 }
840 }
841
842 if (m_requestRole[DateRole]) {
843 // Don't use KFileItem::timeString() as this is too expensive when
844 // having several thousands of items. Instead the formatting of the
845 // date-time will be done on-demand by the view when the date will be shown.
846 const KDateTime dateTime = item.time(KFileItem::ModificationTime);
847 data.insert("date", dateTime.dateTime());
848 }
849
850 if (m_requestRole[PermissionsRole]) {
851 data.insert("permissions", item.permissionsString());
852 }
853
854 if (m_requestRole[OwnerRole]) {
855 data.insert("owner", item.user());
856 }
857
858 if (m_requestRole[GroupRole]) {
859 data.insert("group", item.group());
860 }
861
862 if (m_requestRole[DestinationRole]) {
863 QString destination = item.linkDest();
864 if (destination.isEmpty()) {
865 destination = i18nc("@item:intable", "No destination");
866 }
867 data.insert("destination", destination);
868 }
869
870 if (m_requestRole[PathRole]) {
871 data.insert("path", item.localPath());
872 }
873
874 if (m_requestRole[IsExpandedRole]) {
875 data.insert("isExpanded", false);
876 }
877
878 if (m_requestRole[ExpansionLevelRole]) {
879 if (m_rootExpansionLevel < 0) {
880 KDirLister* dirLister = m_dirLister.data();
881 if (dirLister) {
882 const QString rootDir = dirLister->url().directory(KUrl::AppendTrailingSlash);
883 m_rootExpansionLevel = rootDir.count('/');
884 }
885 }
886 const QString dir = item.url().directory(KUrl::AppendTrailingSlash);
887 const int level = dir.count('/') - m_rootExpansionLevel - 1;
888 data.insert("expansionLevel", level);
889 }
890
891 if (item.isMimeTypeKnown()) {
892 data.insert("iconName", item.iconName());
893
894 if (m_requestRole[TypeRole]) {
895 data.insert("type", item.mimeComment());
896 }
897 }
898
899 return data;
900 }
901
902 bool KFileItemModel::lessThan(const KFileItem& a, const KFileItem& b) const
903 {
904 int result = 0;
905
906 if (m_rootExpansionLevel >= 0) {
907 result = expansionLevelsCompare(a, b);
908 if (result != 0) {
909 // The items have parents with different expansion levels
910 return (sortOrder() == Qt::AscendingOrder) ? result < 0 : result > 0;
911 }
912 }
913
914 if (m_sortFoldersFirst || m_sortRole == SizeRole) {
915 const bool isDirA = a.isDir();
916 const bool isDirB = b.isDir();
917 if (isDirA && !isDirB) {
918 return true;
919 } else if (!isDirA && isDirB) {
920 return false;
921 }
922 }
923
924 switch (m_sortRole) {
925 case NameRole: {
926 result = stringCompare(a.text(), b.text());
927 if (result == 0) {
928 // KFileItem::text() may not be unique in case UDS_DISPLAY_NAME is used
929 result = stringCompare(a.name(m_caseSensitivity == Qt::CaseInsensitive),
930 b.name(m_caseSensitivity == Qt::CaseInsensitive));
931 }
932 break;
933 }
934
935 case DateRole: {
936 const KDateTime dateTimeA = a.time(KFileItem::ModificationTime);
937 const KDateTime dateTimeB = b.time(KFileItem::ModificationTime);
938 if (dateTimeA < dateTimeB) {
939 result = -1;
940 } else if (dateTimeA > dateTimeB) {
941 result = +1;
942 }
943 break;
944 }
945
946 case SizeRole: {
947 // TODO: Implement sorting folders by the number of items inside.
948 // This is more tricky to get right because this number is retrieved
949 // asynchronously by KFileItemModelRolesUpdater.
950 const KIO::filesize_t sizeA = a.size();
951 const KIO::filesize_t sizeB = b.size();
952 if (sizeA < sizeB) {
953 result = -1;
954 } else if (sizeA > sizeB) {
955 result = +1;
956 }
957 break;
958 }
959
960 default:
961 break;
962 }
963
964 if (result == 0) {
965 // It must be assured that the sort order is always unique even if two values have been
966 // equal. In this case a comparison of the URL is done which is unique in all cases
967 // within KDirLister.
968 result = QString::compare(a.url().url(), b.url().url(), Qt::CaseSensitive);
969 }
970
971 return (sortOrder() == Qt::AscendingOrder) ? result < 0 : result > 0;
972 }
973
974 void KFileItemModel::sort(const KFileItemList::iterator& startIterator, const KFileItemList::iterator& endIterator)
975 {
976 KFileItemList::iterator start = startIterator;
977 KFileItemList::iterator end = endIterator;
978
979 // The implementation is based on qSortHelper() from qalgorithms.h
980 // Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies).
981 // In opposite to qSort() it allows to use a member-function for the comparison of elements.
982 while (1) {
983 int span = int(end - start);
984 if (span < 2) {
985 return;
986 }
987
988 --end;
989 KFileItemList::iterator low = start, high = end - 1;
990 KFileItemList::iterator pivot = start + span / 2;
991
992 if (lessThan(*end, *start)) {
993 qSwap(*end, *start);
994 }
995 if (span == 2) {
996 return;
997 }
998
999 if (lessThan(*pivot, *start)) {
1000 qSwap(*pivot, *start);
1001 }
1002 if (lessThan(*end, *pivot)) {
1003 qSwap(*end, *pivot);
1004 }
1005 if (span == 3) {
1006 return;
1007 }
1008
1009 qSwap(*pivot, *end);
1010
1011 while (low < high) {
1012 while (low < high && lessThan(*low, *end)) {
1013 ++low;
1014 }
1015
1016 while (high > low && lessThan(*end, *high)) {
1017 --high;
1018 }
1019 if (low < high) {
1020 qSwap(*low, *high);
1021 ++low;
1022 --high;
1023 } else {
1024 break;
1025 }
1026 }
1027
1028 if (lessThan(*low, *end)) {
1029 ++low;
1030 }
1031
1032 qSwap(*end, *low);
1033 sort(start, low);
1034
1035 start = low + 1;
1036 ++end;
1037 }
1038 }
1039
1040 int KFileItemModel::stringCompare(const QString& a, const QString& b) const
1041 {
1042 // Taken from KDirSortFilterProxyModel (kdelibs/kfile/kdirsortfilterproxymodel.*)
1043 // Copyright (C) 2006 by Peter Penz <peter.penz@gmx.at>
1044 // Copyright (C) 2006 by Dominic Battre <dominic@battre.de>
1045 // Copyright (C) 2006 by Martin Pool <mbp@canonical.com>
1046
1047 if (m_caseSensitivity == Qt::CaseInsensitive) {
1048 const int result = m_naturalSorting ? KStringHandler::naturalCompare(a, b, Qt::CaseInsensitive)
1049 : QString::compare(a, b, Qt::CaseInsensitive);
1050 if (result != 0) {
1051 // Only return the result, if the strings are not equal. If they are equal by a case insensitive
1052 // comparison, still a deterministic sort order is required. A case sensitive
1053 // comparison is done as fallback.
1054 return result;
1055 }
1056 }
1057
1058 return m_naturalSorting ? KStringHandler::naturalCompare(a, b, Qt::CaseSensitive)
1059 : QString::compare(a, b, Qt::CaseSensitive);
1060 }
1061
1062 int KFileItemModel::expansionLevelsCompare(const KFileItem& a, const KFileItem& b) const
1063 {
1064 const KUrl urlA = a.url();
1065 const KUrl urlB = b.url();
1066 if (urlA.directory() == urlB.directory()) {
1067 // Both items have the same directory as parent
1068 return 0;
1069 }
1070
1071 // Check whether one item is the parent of the other item
1072 if (urlA.isParentOf(urlB)) {
1073 return -1;
1074 } else if (urlB.isParentOf(urlA)) {
1075 return +1;
1076 }
1077
1078 // Determine the maximum common path of both items and
1079 // remember the index in 'index'
1080 const QString pathA = urlA.path();
1081 const QString pathB = urlB.path();
1082
1083 const int maxIndex = qMin(pathA.length(), pathB.length()) - 1;
1084 int index = 0;
1085 while (index <= maxIndex && pathA.at(index) == pathB.at(index)) {
1086 ++index;
1087 }
1088 if (index > maxIndex) {
1089 index = maxIndex;
1090 }
1091 while ((pathA.at(index) != QLatin1Char('/') || pathB.at(index) != QLatin1Char('/')) && index > 0) {
1092 --index;
1093 }
1094
1095 // Determine the first sub-path after the common path and
1096 // check whether it represents a directory or already a file
1097 bool isDirA = true;
1098 const QString subPathA = subPath(a, pathA, index, &isDirA);
1099 bool isDirB = true;
1100 const QString subPathB = subPath(b, pathB, index, &isDirB);
1101
1102 if (isDirA && !isDirB) {
1103 return -1;
1104 } else if (!isDirA && isDirB) {
1105 return +1;
1106 }
1107
1108 return stringCompare(subPathA, subPathB);
1109 }
1110
1111 QString KFileItemModel::subPath(const KFileItem& item,
1112 const QString& itemPath,
1113 int start,
1114 bool* isDir) const
1115 {
1116 Q_ASSERT(isDir);
1117 const int pathIndex = itemPath.indexOf('/', start + 1);
1118 *isDir = (pathIndex > 0) || item.isDir();
1119 return itemPath.mid(start, pathIndex - start);
1120 }
1121
1122 bool KFileItemModel::useMaximumUpdateInterval() const
1123 {
1124 const KDirLister* dirLister = m_dirLister.data();
1125 return dirLister && !dirLister->url().isLocalFile();
1126 }
1127
1128 #include "kfileitemmodel.moc"