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