]> cloud.milkyroute.net Git - dolphin.git/blob - src/kitemviews/kfileitemmodel.cpp
da64895fbe12df009d7e7b80d24b2e0d0bff2c4f
[dolphin.git] / src / kitemviews / kfileitemmodel.cpp
1 /*****************************************************************************
2 * Copyright (C) 2011 by Peter Penz <peter.penz19@gmail.com> *
3 * Copyright (C) 2013 by Frank Reininghaus <frank78ac@googlemail.com> *
4 * Copyright (C) 2013 by Emmanuel Pescosta <emmanuelpescosta099@gmail.com> *
5 * *
6 * This program is free software; you can redistribute it and/or modify *
7 * it under the terms of the GNU General Public License as published by *
8 * the Free Software Foundation; either version 2 of the License, or *
9 * (at your option) any later version. *
10 * *
11 * This program is distributed in the hope that it will be useful, *
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
14 * GNU General Public License for more details. *
15 * *
16 * You should have received a copy of the GNU General Public License *
17 * along with this program; if not, write to the *
18 * Free Software Foundation, Inc., *
19 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *
20 *****************************************************************************/
21
22 #include "kfileitemmodel.h"
23
24 #include "dolphin_generalsettings.h"
25
26 #include <KLocalizedString>
27 #include <KUrlMimeData>
28
29 #include "dolphindebug.h"
30
31 #include "private/kfileitemmodelsortalgorithm.h"
32 #include "private/kfileitemmodeldirlister.h"
33
34 #include <QMimeData>
35 #include <QTimer>
36 #include <QWidget>
37
38 #include <algorithm>
39 #include <vector>
40
41 // #define KFILEITEMMODEL_DEBUG
42
43 KFileItemModel::KFileItemModel(QObject* parent) :
44 KItemModelBase("text", parent),
45 m_dirLister(0),
46 m_sortDirsFirst(true),
47 m_sortRole(NameRole),
48 m_sortingProgressPercent(-1),
49 m_roles(),
50 m_itemData(),
51 m_items(),
52 m_filter(),
53 m_filteredItems(),
54 m_requestRole(),
55 m_maximumUpdateIntervalTimer(0),
56 m_resortAllItemsTimer(0),
57 m_pendingItemsToInsert(),
58 m_groups(),
59 m_expandedDirs(),
60 m_urlsToExpand()
61 {
62 m_collator.setNumericMode(true);
63
64 loadSortingSettings();
65
66 m_dirLister = new KFileItemModelDirLister(this);
67 m_dirLister->setDelayedMimeTypes(true);
68
69 const QWidget* parentWidget = qobject_cast<QWidget*>(parent);
70 if (parentWidget) {
71 m_dirLister->setMainWindow(parentWidget->window());
72 }
73
74 connect(m_dirLister, &KFileItemModelDirLister::started, this, &KFileItemModel::directoryLoadingStarted);
75 connect(m_dirLister, static_cast<void(KFileItemModelDirLister::*)()>(&KFileItemModelDirLister::canceled), this, &KFileItemModel::slotCanceled);
76 connect(m_dirLister, static_cast<void(KFileItemModelDirLister::*)(const QUrl&)>(&KFileItemModelDirLister::completed), this, &KFileItemModel::slotCompleted);
77 connect(m_dirLister, &KFileItemModelDirLister::itemsAdded, this, &KFileItemModel::slotItemsAdded);
78 connect(m_dirLister, &KFileItemModelDirLister::itemsDeleted, this, &KFileItemModel::slotItemsDeleted);
79 connect(m_dirLister, &KFileItemModelDirLister::refreshItems, this, &KFileItemModel::slotRefreshItems);
80 connect(m_dirLister, static_cast<void(KFileItemModelDirLister::*)()>(&KFileItemModelDirLister::clear), this, &KFileItemModel::slotClear);
81 connect(m_dirLister, &KFileItemModelDirLister::infoMessage, this, &KFileItemModel::infoMessage);
82 connect(m_dirLister, &KFileItemModelDirLister::errorMessage, this, &KFileItemModel::errorMessage);
83 connect(m_dirLister, &KFileItemModelDirLister::percent, this, &KFileItemModel::directoryLoadingProgress);
84 connect(m_dirLister, static_cast<void(KFileItemModelDirLister::*)(const QUrl&, const QUrl&)>(&KFileItemModelDirLister::redirection), this, &KFileItemModel::directoryRedirection);
85 connect(m_dirLister, &KFileItemModelDirLister::urlIsFileError, this, &KFileItemModel::urlIsFileError);
86
87 // Apply default roles that should be determined
88 resetRoles();
89 m_requestRole[NameRole] = true;
90 m_requestRole[IsDirRole] = true;
91 m_requestRole[IsLinkRole] = true;
92 m_roles.insert("text");
93 m_roles.insert("isDir");
94 m_roles.insert("isLink");
95 m_roles.insert("isHidden");
96
97 // For slow KIO-slaves like used for searching it makes sense to show results periodically even
98 // before the completed() or canceled() signal has been emitted.
99 m_maximumUpdateIntervalTimer = new QTimer(this);
100 m_maximumUpdateIntervalTimer->setInterval(2000);
101 m_maximumUpdateIntervalTimer->setSingleShot(true);
102 connect(m_maximumUpdateIntervalTimer, &QTimer::timeout, this, &KFileItemModel::dispatchPendingItemsToInsert);
103
104 // When changing the value of an item which represents the sort-role a resorting must be
105 // triggered. Especially in combination with KFileItemModelRolesUpdater this might be done
106 // for a lot of items within a quite small timeslot. To prevent expensive resortings the
107 // resorting is postponed until the timer has been exceeded.
108 m_resortAllItemsTimer = new QTimer(this);
109 m_resortAllItemsTimer->setInterval(500);
110 m_resortAllItemsTimer->setSingleShot(true);
111 connect(m_resortAllItemsTimer, &QTimer::timeout, this, &KFileItemModel::resortAllItems);
112
113 connect(GeneralSettings::self(), &GeneralSettings::sortingChoiceChanged, this, &KFileItemModel::slotSortingChoiceChanged);
114 }
115
116 KFileItemModel::~KFileItemModel()
117 {
118 qDeleteAll(m_itemData);
119 qDeleteAll(m_filteredItems);
120 qDeleteAll(m_pendingItemsToInsert);
121 }
122
123 void KFileItemModel::loadDirectory(const QUrl &url)
124 {
125 m_dirLister->openUrl(url);
126 }
127
128 void KFileItemModel::refreshDirectory(const QUrl &url)
129 {
130 // Refresh all expanded directories first (Bug 295300)
131 QHashIterator<QUrl, QUrl> expandedDirs(m_expandedDirs);
132 while (expandedDirs.hasNext()) {
133 expandedDirs.next();
134 m_dirLister->openUrl(expandedDirs.value(), KDirLister::Reload);
135 }
136
137 m_dirLister->openUrl(url, KDirLister::Reload);
138 }
139
140 QUrl KFileItemModel::directory() const
141 {
142 return m_dirLister->url();
143 }
144
145 void KFileItemModel::cancelDirectoryLoading()
146 {
147 m_dirLister->stop();
148 }
149
150 int KFileItemModel::count() const
151 {
152 return m_itemData.count();
153 }
154
155 QHash<QByteArray, QVariant> KFileItemModel::data(int index) const
156 {
157 if (index >= 0 && index < count()) {
158 ItemData* data = m_itemData.at(index);
159 if (data->values.isEmpty()) {
160 data->values = retrieveData(data->item, data->parent);
161 }
162
163 return data->values;
164 }
165 return QHash<QByteArray, QVariant>();
166 }
167
168 bool KFileItemModel::setData(int index, const QHash<QByteArray, QVariant>& values)
169 {
170 if (index < 0 || index >= count()) {
171 return false;
172 }
173
174 QHash<QByteArray, QVariant> currentValues = data(index);
175
176 // Determine which roles have been changed
177 QSet<QByteArray> changedRoles;
178 QHashIterator<QByteArray, QVariant> it(values);
179 while (it.hasNext()) {
180 it.next();
181 const QByteArray role = sharedValue(it.key());
182 const QVariant value = it.value();
183
184 if (currentValues[role] != value) {
185 currentValues[role] = value;
186 changedRoles.insert(role);
187 }
188 }
189
190 if (changedRoles.isEmpty()) {
191 return false;
192 }
193
194 m_itemData[index]->values = currentValues;
195 if (changedRoles.contains("text")) {
196 QUrl url = m_itemData[index]->item.url();
197 url = url.adjusted(QUrl::RemoveFilename);
198 url.setPath(url.path() + currentValues["text"].toString());
199 m_itemData[index]->item.setUrl(url);
200 }
201
202 emitItemsChangedAndTriggerResorting(KItemRangeList() << KItemRange(index, 1), changedRoles);
203
204 return true;
205 }
206
207 void KFileItemModel::setSortDirectoriesFirst(bool dirsFirst)
208 {
209 if (dirsFirst != m_sortDirsFirst) {
210 m_sortDirsFirst = dirsFirst;
211 resortAllItems();
212 }
213 }
214
215 bool KFileItemModel::sortDirectoriesFirst() const
216 {
217 return m_sortDirsFirst;
218 }
219
220 void KFileItemModel::setShowHiddenFiles(bool show)
221 {
222 m_dirLister->setShowingDotFiles(show);
223 m_dirLister->emitChanges();
224 if (show) {
225 dispatchPendingItemsToInsert();
226 }
227 }
228
229 bool KFileItemModel::showHiddenFiles() const
230 {
231 return m_dirLister->showingDotFiles();
232 }
233
234 void KFileItemModel::setShowDirectoriesOnly(bool enabled)
235 {
236 m_dirLister->setDirOnlyMode(enabled);
237 }
238
239 bool KFileItemModel::showDirectoriesOnly() const
240 {
241 return m_dirLister->dirOnlyMode();
242 }
243
244 QMimeData* KFileItemModel::createMimeData(const KItemSet& indexes) const
245 {
246 QMimeData* data = new QMimeData();
247
248 // The following code has been taken from KDirModel::mimeData()
249 // (kdelibs/kio/kio/kdirmodel.cpp)
250 // Copyright (C) 2006 David Faure <faure@kde.org>
251 QList<QUrl> urls;
252 QList<QUrl> mostLocalUrls;
253 bool canUseMostLocalUrls = true;
254 const ItemData* lastAddedItem = 0;
255
256 for (int index : indexes) {
257 const ItemData* itemData = m_itemData.at(index);
258 const ItemData* parent = itemData->parent;
259
260 while (parent && parent != lastAddedItem) {
261 parent = parent->parent;
262 }
263
264 if (parent && parent == lastAddedItem) {
265 // A parent of 'itemData' has been added already.
266 continue;
267 }
268
269 lastAddedItem = itemData;
270 const KFileItem& item = itemData->item;
271 if (!item.isNull()) {
272 urls << item.url();
273
274 bool isLocal;
275 mostLocalUrls << item.mostLocalUrl(isLocal);
276 if (!isLocal) {
277 canUseMostLocalUrls = false;
278 }
279 }
280 }
281
282 KUrlMimeData::setUrls(urls, mostLocalUrls, data);
283 return data;
284 }
285
286 int KFileItemModel::indexForKeyboardSearch(const QString& text, int startFromIndex) const
287 {
288 startFromIndex = qMax(0, startFromIndex);
289 for (int i = startFromIndex; i < count(); ++i) {
290 if (fileItem(i).text().startsWith(text, Qt::CaseInsensitive)) {
291 return i;
292 }
293 }
294 for (int i = 0; i < startFromIndex; ++i) {
295 if (fileItem(i).text().startsWith(text, Qt::CaseInsensitive)) {
296 return i;
297 }
298 }
299 return -1;
300 }
301
302 bool KFileItemModel::supportsDropping(int index) const
303 {
304 const KFileItem item = fileItem(index);
305 return !item.isNull() && (item.isDir() || item.isDesktopFile());
306 }
307
308 QString KFileItemModel::roleDescription(const QByteArray& role) const
309 {
310 static QHash<QByteArray, QString> description;
311 if (description.isEmpty()) {
312 int count = 0;
313 const RoleInfoMap* map = rolesInfoMap(count);
314 for (int i = 0; i < count; ++i) {
315 description.insert(map[i].role, i18nc(map[i].roleTranslationContext, map[i].roleTranslation));
316 }
317 }
318
319 return description.value(role);
320 }
321
322 QList<QPair<int, QVariant> > KFileItemModel::groups() const
323 {
324 if (!m_itemData.isEmpty() && m_groups.isEmpty()) {
325 #ifdef KFILEITEMMODEL_DEBUG
326 QElapsedTimer timer;
327 timer.start();
328 #endif
329 switch (typeForRole(sortRole())) {
330 case NameRole: m_groups = nameRoleGroups(); break;
331 case SizeRole: m_groups = sizeRoleGroups(); break;
332 case ModificationTimeRole:
333 m_groups = timeRoleGroups([](const ItemData *item) {
334 return item->item.time(KFileItem::ModificationTime);
335 });
336 break;
337 case AccessTimeRole:
338 m_groups = timeRoleGroups([](const ItemData *item) {
339 return item->item.time(KFileItem::AccessTime);
340 });
341 break;
342 case DeletionTimeRole:
343 m_groups = timeRoleGroups([](const ItemData *item) {
344 return item->values.value("deletiontime").toDateTime();
345 });
346 break;
347 case PermissionsRole: m_groups = permissionRoleGroups(); break;
348 case RatingRole: m_groups = ratingRoleGroups(); break;
349 default: m_groups = genericStringRoleGroups(sortRole()); break;
350 }
351
352 #ifdef KFILEITEMMODEL_DEBUG
353 qCDebug(DolphinDebug) << "[TIME] Calculating groups for" << count() << "items:" << timer.elapsed();
354 #endif
355 }
356
357 return m_groups;
358 }
359
360 KFileItem KFileItemModel::fileItem(int index) const
361 {
362 if (index >= 0 && index < count()) {
363 return m_itemData.at(index)->item;
364 }
365
366 return KFileItem();
367 }
368
369 KFileItem KFileItemModel::fileItem(const QUrl &url) const
370 {
371 const int indexForUrl = index(url);
372 if (indexForUrl >= 0) {
373 return m_itemData.at(indexForUrl)->item;
374 }
375 return KFileItem();
376 }
377
378 int KFileItemModel::index(const KFileItem& item) const
379 {
380 return index(item.url());
381 }
382
383 int KFileItemModel::index(const QUrl& url) const
384 {
385 const QUrl urlToFind = url.adjusted(QUrl::StripTrailingSlash);
386
387 const int itemCount = m_itemData.count();
388 int itemsInHash = m_items.count();
389
390 int index = m_items.value(urlToFind, -1);
391 while (index < 0 && itemsInHash < itemCount) {
392 // Not all URLs are stored yet in m_items. We grow m_items until either
393 // urlToFind is found, or all URLs have been stored in m_items.
394 // Note that we do not add the URLs to m_items one by one, but in
395 // larger blocks. After each block, we check if urlToFind is in
396 // m_items. We could in principle compare urlToFind with each URL while
397 // we are going through m_itemData, but comparing two QUrls will,
398 // unlike calling qHash for the URLs, trigger a parsing of the URLs
399 // which costs both CPU cycles and memory.
400 const int blockSize = 1000;
401 const int currentBlockEnd = qMin(itemsInHash + blockSize, itemCount);
402 for (int i = itemsInHash; i < currentBlockEnd; ++i) {
403 const QUrl nextUrl = m_itemData.at(i)->item.url();
404 m_items.insert(nextUrl, i);
405 }
406
407 itemsInHash = currentBlockEnd;
408 index = m_items.value(urlToFind, -1);
409 }
410
411 if (index < 0) {
412 // The item could not be found, even though all items from m_itemData
413 // should be in m_items now. We print some diagnostic information which
414 // might help to find the cause of the problem, but only once. This
415 // prevents that obtaining and printing the debugging information
416 // wastes CPU cycles and floods the shell or .xsession-errors.
417 static bool printDebugInfo = true;
418
419 if (m_items.count() != m_itemData.count() && printDebugInfo) {
420 printDebugInfo = false;
421
422 qCWarning(DolphinDebug) << "The model is in an inconsistent state.";
423 qCWarning(DolphinDebug) << "m_items.count() ==" << m_items.count();
424 qCWarning(DolphinDebug) << "m_itemData.count() ==" << m_itemData.count();
425
426 // Check if there are multiple items with the same URL.
427 QMultiHash<QUrl, int> indexesForUrl;
428 for (int i = 0; i < m_itemData.count(); ++i) {
429 indexesForUrl.insert(m_itemData.at(i)->item.url(), i);
430 }
431
432 foreach (const QUrl& url, indexesForUrl.uniqueKeys()) {
433 if (indexesForUrl.count(url) > 1) {
434 qCWarning(DolphinDebug) << "Multiple items found with the URL" << url;
435
436 auto it = indexesForUrl.find(url);
437 while (it != indexesForUrl.end() && it.key() == url) {
438 const ItemData* data = m_itemData.at(it.value());
439 qCWarning(DolphinDebug) << "index" << it.value() << ":" << data->item;
440 if (data->parent) {
441 qCWarning(DolphinDebug) << "parent" << data->parent->item;
442 }
443 ++it;
444 }
445 }
446 }
447 }
448 }
449
450 return index;
451 }
452
453 KFileItem KFileItemModel::rootItem() const
454 {
455 return m_dirLister->rootItem();
456 }
457
458 void KFileItemModel::clear()
459 {
460 slotClear();
461 }
462
463 void KFileItemModel::setRoles(const QSet<QByteArray>& roles)
464 {
465 if (m_roles == roles) {
466 return;
467 }
468
469 const QSet<QByteArray> changedRoles = (roles - m_roles) + (m_roles - roles);
470 m_roles = roles;
471
472 if (count() > 0) {
473 const bool supportedExpanding = m_requestRole[ExpandedParentsCountRole];
474 const bool willSupportExpanding = roles.contains("expandedParentsCount");
475 if (supportedExpanding && !willSupportExpanding) {
476 // No expanding is supported anymore. Take care to delete all items that have an expansion level
477 // that is not 0 (and hence are part of an expanded item).
478 removeExpandedItems();
479 }
480 }
481
482 m_groups.clear();
483 resetRoles();
484
485 QSetIterator<QByteArray> it(roles);
486 while (it.hasNext()) {
487 const QByteArray& role = it.next();
488 m_requestRole[typeForRole(role)] = true;
489 }
490
491 if (count() > 0) {
492 // Update m_data with the changed requested roles
493 const int maxIndex = count() - 1;
494 for (int i = 0; i <= maxIndex; ++i) {
495 m_itemData[i]->values = retrieveData(m_itemData.at(i)->item, m_itemData.at(i)->parent);
496 }
497
498 emit itemsChanged(KItemRangeList() << KItemRange(0, count()), changedRoles);
499 }
500
501 // Clear the 'values' of all filtered items. They will be re-populated with the
502 // correct roles the next time 'values' will be accessed via data(int).
503 QHash<KFileItem, ItemData*>::iterator filteredIt = m_filteredItems.begin();
504 const QHash<KFileItem, ItemData*>::iterator filteredEnd = m_filteredItems.end();
505 while (filteredIt != filteredEnd) {
506 (*filteredIt)->values.clear();
507 ++filteredIt;
508 }
509 }
510
511 QSet<QByteArray> KFileItemModel::roles() const
512 {
513 return m_roles;
514 }
515
516 bool KFileItemModel::setExpanded(int index, bool expanded)
517 {
518 if (!isExpandable(index) || isExpanded(index) == expanded) {
519 return false;
520 }
521
522 QHash<QByteArray, QVariant> values;
523 values.insert(sharedValue("isExpanded"), expanded);
524 if (!setData(index, values)) {
525 return false;
526 }
527
528 const KFileItem item = m_itemData.at(index)->item;
529 const QUrl url = item.url();
530 const QUrl targetUrl = item.targetUrl();
531 if (expanded) {
532 m_expandedDirs.insert(targetUrl, url);
533 m_dirLister->openUrl(url, KDirLister::Keep);
534
535 const QVariantList previouslyExpandedChildren = m_itemData.at(index)->values.value("previouslyExpandedChildren").value<QVariantList>();
536 foreach (const QVariant& var, previouslyExpandedChildren) {
537 m_urlsToExpand.insert(var.toUrl());
538 }
539 } else {
540 // Note that there might be (indirect) children of the folder which is to be collapsed in
541 // m_pendingItemsToInsert. To prevent that they will be inserted into the model later,
542 // possibly without a parent, which might result in a crash, we insert all pending items
543 // right now. All new items which would be without a parent will then be removed.
544 dispatchPendingItemsToInsert();
545
546 // Check if the index of the collapsed folder has changed. If that is the case, then items
547 // were inserted before the collapsed folder, and its index needs to be updated.
548 if (m_itemData.at(index)->item != item) {
549 index = this->index(item);
550 }
551
552 m_expandedDirs.remove(targetUrl);
553 m_dirLister->stop(url);
554
555 const int parentLevel = expandedParentsCount(index);
556 const int itemCount = m_itemData.count();
557 const int firstChildIndex = index + 1;
558
559 QVariantList expandedChildren;
560
561 int childIndex = firstChildIndex;
562 while (childIndex < itemCount && expandedParentsCount(childIndex) > parentLevel) {
563 ItemData* itemData = m_itemData.at(childIndex);
564 if (itemData->values.value("isExpanded").toBool()) {
565 const QUrl targetUrl = itemData->item.targetUrl();
566 const QUrl url = itemData->item.url();
567 m_expandedDirs.remove(targetUrl);
568 m_dirLister->stop(url); // TODO: try to unit-test this, see https://bugs.kde.org/show_bug.cgi?id=332102#c11
569 expandedChildren.append(targetUrl);
570 }
571 ++childIndex;
572 }
573 const int childrenCount = childIndex - firstChildIndex;
574
575 removeFilteredChildren(KItemRangeList() << KItemRange(index, 1 + childrenCount));
576 removeItems(KItemRangeList() << KItemRange(firstChildIndex, childrenCount), DeleteItemData);
577
578 m_itemData.at(index)->values.insert("previouslyExpandedChildren", expandedChildren);
579 }
580
581 return true;
582 }
583
584 bool KFileItemModel::isExpanded(int index) const
585 {
586 if (index >= 0 && index < count()) {
587 return m_itemData.at(index)->values.value("isExpanded").toBool();
588 }
589 return false;
590 }
591
592 bool KFileItemModel::isExpandable(int index) const
593 {
594 if (index >= 0 && index < count()) {
595 // Call data (instead of accessing m_itemData directly)
596 // to ensure that the value is initialized.
597 return data(index).value("isExpandable").toBool();
598 }
599 return false;
600 }
601
602 int KFileItemModel::expandedParentsCount(int index) const
603 {
604 if (index >= 0 && index < count()) {
605 return expandedParentsCount(m_itemData.at(index));
606 }
607 return 0;
608 }
609
610 QSet<QUrl> KFileItemModel::expandedDirectories() const
611 {
612 QSet<QUrl> result;
613 const auto dirs = m_expandedDirs;
614 for (const auto &dir : dirs) {
615 result.insert(dir);
616 }
617 return result;
618 }
619
620 void KFileItemModel::restoreExpandedDirectories(const QSet<QUrl> &urls)
621 {
622 m_urlsToExpand = urls;
623 }
624
625 void KFileItemModel::expandParentDirectories(const QUrl &url)
626 {
627 const int pos = m_dirLister->url().path().length();
628
629 // Assure that each sub-path of the URL that should be
630 // expanded is added to m_urlsToExpand. KDirLister
631 // does not care whether the parent-URL has already been
632 // expanded.
633 QUrl urlToExpand = m_dirLister->url();
634 const QStringList subDirs = url.path().mid(pos).split(QDir::separator());
635 for (int i = 0; i < subDirs.count() - 1; ++i) {
636 urlToExpand.setPath(urlToExpand.path() + '/' + subDirs.at(i));
637 m_urlsToExpand.insert(urlToExpand);
638 }
639
640 // KDirLister::open() must called at least once to trigger an initial
641 // loading. The pending URLs that must be restored are handled
642 // in slotCompleted().
643 QSetIterator<QUrl> it2(m_urlsToExpand);
644 while (it2.hasNext()) {
645 const int idx = index(it2.next());
646 if (idx >= 0 && !isExpanded(idx)) {
647 setExpanded(idx, true);
648 break;
649 }
650 }
651 }
652
653 void KFileItemModel::setNameFilter(const QString& nameFilter)
654 {
655 if (m_filter.pattern() != nameFilter) {
656 dispatchPendingItemsToInsert();
657 m_filter.setPattern(nameFilter);
658 applyFilters();
659 }
660 }
661
662 QString KFileItemModel::nameFilter() const
663 {
664 return m_filter.pattern();
665 }
666
667 void KFileItemModel::setMimeTypeFilters(const QStringList& filters)
668 {
669 if (m_filter.mimeTypes() != filters) {
670 dispatchPendingItemsToInsert();
671 m_filter.setMimeTypes(filters);
672 applyFilters();
673 }
674 }
675
676 QStringList KFileItemModel::mimeTypeFilters() const
677 {
678 return m_filter.mimeTypes();
679 }
680
681
682 void KFileItemModel::applyFilters()
683 {
684 // Check which shown items from m_itemData must get
685 // hidden and hence moved to m_filteredItems.
686 QVector<int> newFilteredIndexes;
687
688 const int itemCount = m_itemData.count();
689 for (int index = 0; index < itemCount; ++index) {
690 ItemData* itemData = m_itemData.at(index);
691
692 // Only filter non-expanded items as child items may never
693 // exist without a parent item
694 if (!itemData->values.value("isExpanded").toBool()) {
695 const KFileItem item = itemData->item;
696 if (!m_filter.matches(item)) {
697 newFilteredIndexes.append(index);
698 m_filteredItems.insert(item, itemData);
699 }
700 }
701 }
702
703 const KItemRangeList removedRanges = KItemRangeList::fromSortedContainer(newFilteredIndexes);
704 removeItems(removedRanges, KeepItemData);
705
706 // Check which hidden items from m_filteredItems should
707 // get visible again and hence removed from m_filteredItems.
708 QList<ItemData*> newVisibleItems;
709
710 QHash<KFileItem, ItemData*>::iterator it = m_filteredItems.begin();
711 while (it != m_filteredItems.end()) {
712 if (m_filter.matches(it.key())) {
713 newVisibleItems.append(it.value());
714 it = m_filteredItems.erase(it);
715 } else {
716 ++it;
717 }
718 }
719
720 insertItems(newVisibleItems);
721 }
722
723 void KFileItemModel::removeFilteredChildren(const KItemRangeList& itemRanges)
724 {
725 if (m_filteredItems.isEmpty() || !m_requestRole[ExpandedParentsCountRole]) {
726 // There are either no filtered items, or it is not possible to expand
727 // folders -> there cannot be any filtered children.
728 return;
729 }
730
731 QSet<ItemData*> parents;
732 foreach (const KItemRange& range, itemRanges) {
733 for (int index = range.index; index < range.index + range.count; ++index) {
734 parents.insert(m_itemData.at(index));
735 }
736 }
737
738 QHash<KFileItem, ItemData*>::iterator it = m_filteredItems.begin();
739 while (it != m_filteredItems.end()) {
740 if (parents.contains(it.value()->parent)) {
741 delete it.value();
742 it = m_filteredItems.erase(it);
743 } else {
744 ++it;
745 }
746 }
747 }
748
749 QList<KFileItemModel::RoleInfo> KFileItemModel::rolesInformation()
750 {
751 static QList<RoleInfo> rolesInfo;
752 if (rolesInfo.isEmpty()) {
753 int count = 0;
754 const RoleInfoMap* map = rolesInfoMap(count);
755 for (int i = 0; i < count; ++i) {
756 if (map[i].roleType != NoRole) {
757 RoleInfo info;
758 info.role = map[i].role;
759 info.translation = i18nc(map[i].roleTranslationContext, map[i].roleTranslation);
760 if (map[i].groupTranslation) {
761 info.group = i18nc(map[i].groupTranslationContext, map[i].groupTranslation);
762 } else {
763 // For top level roles, groupTranslation is 0. We must make sure that
764 // info.group is an empty string then because the code that generates
765 // menus tries to put the actions into sub menus otherwise.
766 info.group = QString();
767 }
768 info.requiresBaloo = map[i].requiresBaloo;
769 info.requiresIndexer = map[i].requiresIndexer;
770 rolesInfo.append(info);
771 }
772 }
773 }
774
775 return rolesInfo;
776 }
777
778 void KFileItemModel::onGroupedSortingChanged(bool current)
779 {
780 Q_UNUSED(current);
781 m_groups.clear();
782 }
783
784 void KFileItemModel::onSortRoleChanged(const QByteArray& current, const QByteArray& previous)
785 {
786 Q_UNUSED(previous);
787 m_sortRole = typeForRole(current);
788
789 if (!m_requestRole[m_sortRole]) {
790 QSet<QByteArray> newRoles = m_roles;
791 newRoles << current;
792 setRoles(newRoles);
793 }
794
795 resortAllItems();
796 }
797
798 void KFileItemModel::onSortOrderChanged(Qt::SortOrder current, Qt::SortOrder previous)
799 {
800 Q_UNUSED(current);
801 Q_UNUSED(previous);
802 resortAllItems();
803 }
804
805 void KFileItemModel::loadSortingSettings()
806 {
807 using Choice = GeneralSettings::EnumSortingChoice;
808 switch (GeneralSettings::sortingChoice()) {
809 case Choice::NaturalSorting:
810 m_naturalSorting = true;
811 m_collator.setCaseSensitivity(Qt::CaseInsensitive);
812 break;
813 case Choice::CaseSensitiveSorting:
814 m_naturalSorting = false;
815 m_collator.setCaseSensitivity(Qt::CaseSensitive);
816 break;
817 case Choice::CaseInsensitiveSorting:
818 m_naturalSorting = false;
819 m_collator.setCaseSensitivity(Qt::CaseInsensitive);
820 break;
821 default:
822 Q_UNREACHABLE();
823 }
824 }
825
826 void KFileItemModel::resortAllItems()
827 {
828 m_resortAllItemsTimer->stop();
829
830 const int itemCount = count();
831 if (itemCount <= 0) {
832 return;
833 }
834
835 #ifdef KFILEITEMMODEL_DEBUG
836 QElapsedTimer timer;
837 timer.start();
838 qCDebug(DolphinDebug) << "===========================================================";
839 qCDebug(DolphinDebug) << "Resorting" << itemCount << "items";
840 #endif
841
842 // Remember the order of the current URLs so
843 // that it can be determined which indexes have
844 // been moved because of the resorting.
845 QList<QUrl> oldUrls;
846 oldUrls.reserve(itemCount);
847 foreach (const ItemData* itemData, m_itemData) {
848 oldUrls.append(itemData->item.url());
849 }
850
851 m_items.clear();
852 m_items.reserve(itemCount);
853
854 // Resort the items
855 sort(m_itemData.begin(), m_itemData.end());
856 for (int i = 0; i < itemCount; ++i) {
857 m_items.insert(m_itemData.at(i)->item.url(), i);
858 }
859
860 // Determine the first index that has been moved.
861 int firstMovedIndex = 0;
862 while (firstMovedIndex < itemCount
863 && firstMovedIndex == m_items.value(oldUrls.at(firstMovedIndex))) {
864 ++firstMovedIndex;
865 }
866
867 const bool itemsHaveMoved = firstMovedIndex < itemCount;
868 if (itemsHaveMoved) {
869 m_groups.clear();
870
871 int lastMovedIndex = itemCount - 1;
872 while (lastMovedIndex > firstMovedIndex
873 && lastMovedIndex == m_items.value(oldUrls.at(lastMovedIndex))) {
874 --lastMovedIndex;
875 }
876
877 Q_ASSERT(firstMovedIndex <= lastMovedIndex);
878
879 // Create a list movedToIndexes, which has the property that
880 // movedToIndexes[i] is the new index of the item with the old index
881 // firstMovedIndex + i.
882 const int movedItemsCount = lastMovedIndex - firstMovedIndex + 1;
883 QList<int> movedToIndexes;
884 movedToIndexes.reserve(movedItemsCount);
885 for (int i = firstMovedIndex; i <= lastMovedIndex; ++i) {
886 const int newIndex = m_items.value(oldUrls.at(i));
887 movedToIndexes.append(newIndex);
888 }
889
890 emit itemsMoved(KItemRange(firstMovedIndex, movedItemsCount), movedToIndexes);
891 } else if (groupedSorting()) {
892 // The groups might have changed even if the order of the items has not.
893 const QList<QPair<int, QVariant> > oldGroups = m_groups;
894 m_groups.clear();
895 if (groups() != oldGroups) {
896 emit groupsChanged();
897 }
898 }
899
900 #ifdef KFILEITEMMODEL_DEBUG
901 qCDebug(DolphinDebug) << "[TIME] Resorting of" << itemCount << "items:" << timer.elapsed();
902 #endif
903 }
904
905 void KFileItemModel::slotCompleted()
906 {
907 dispatchPendingItemsToInsert();
908
909 if (!m_urlsToExpand.isEmpty()) {
910 // Try to find a URL that can be expanded.
911 // Note that the parent folder must be expanded before any of its subfolders become visible.
912 // Therefore, some URLs in m_restoredExpandedUrls might not be visible yet
913 // -> we expand the first visible URL we find in m_restoredExpandedUrls.
914 foreach (const QUrl& url, m_urlsToExpand) {
915 const int indexForUrl = index(url);
916 if (indexForUrl >= 0) {
917 m_urlsToExpand.remove(url);
918 if (setExpanded(indexForUrl, true)) {
919 // The dir lister has been triggered. This slot will be called
920 // again after the directory has been expanded.
921 return;
922 }
923 }
924 }
925
926 // None of the URLs in m_restoredExpandedUrls could be found in the model. This can happen
927 // if these URLs have been deleted in the meantime.
928 m_urlsToExpand.clear();
929 }
930
931 emit directoryLoadingCompleted();
932 }
933
934 void KFileItemModel::slotCanceled()
935 {
936 m_maximumUpdateIntervalTimer->stop();
937 dispatchPendingItemsToInsert();
938
939 emit directoryLoadingCanceled();
940 }
941
942 void KFileItemModel::slotItemsAdded(const QUrl &directoryUrl, const KFileItemList& items)
943 {
944 Q_ASSERT(!items.isEmpty());
945
946 QUrl parentUrl;
947 if (m_expandedDirs.contains(directoryUrl)) {
948 parentUrl = m_expandedDirs.value(directoryUrl);
949 } else {
950 parentUrl = directoryUrl.adjusted(QUrl::StripTrailingSlash);
951 }
952
953 if (m_requestRole[ExpandedParentsCountRole]) {
954 // If the expanding of items is enabled, the call
955 // dirLister->openUrl(url, KDirLister::Keep) in KFileItemModel::setExpanded()
956 // might result in emitting the same items twice due to the Keep-parameter.
957 // This case happens if an item gets expanded, collapsed and expanded again
958 // before the items could be loaded for the first expansion.
959 if (index(items.first().url()) >= 0) {
960 // The items are already part of the model.
961 return;
962 }
963
964 if (directoryUrl != directory()) {
965 // To be able to compare whether the new items may be inserted as children
966 // of a parent item the pending items must be added to the model first.
967 dispatchPendingItemsToInsert();
968 }
969
970 // KDirLister keeps the children of items that got expanded once even if
971 // they got collapsed again with KFileItemModel::setExpanded(false). So it must be
972 // checked whether the parent for new items is still expanded.
973 const int parentIndex = index(parentUrl);
974 if (parentIndex >= 0 && !m_itemData[parentIndex]->values.value("isExpanded").toBool()) {
975 // The parent is not expanded.
976 return;
977 }
978 }
979
980 QList<ItemData*> itemDataList = createItemDataList(parentUrl, items);
981
982 if (!m_filter.hasSetFilters()) {
983 m_pendingItemsToInsert.append(itemDataList);
984 } else {
985 // The name or type filter is active. Hide filtered items
986 // before inserting them into the model and remember
987 // the filtered items in m_filteredItems.
988 foreach (ItemData* itemData, itemDataList) {
989 if (m_filter.matches(itemData->item)) {
990 m_pendingItemsToInsert.append(itemData);
991 } else {
992 m_filteredItems.insert(itemData->item, itemData);
993 }
994 }
995 }
996
997 if (useMaximumUpdateInterval() && !m_maximumUpdateIntervalTimer->isActive()) {
998 // Assure that items get dispatched if no completed() or canceled() signal is
999 // emitted during the maximum update interval.
1000 m_maximumUpdateIntervalTimer->start();
1001 }
1002 }
1003
1004 void KFileItemModel::slotItemsDeleted(const KFileItemList& items)
1005 {
1006 dispatchPendingItemsToInsert();
1007
1008 QVector<int> indexesToRemove;
1009 indexesToRemove.reserve(items.count());
1010
1011 foreach (const KFileItem& item, items) {
1012 const int indexForItem = index(item);
1013 if (indexForItem >= 0) {
1014 indexesToRemove.append(indexForItem);
1015 } else {
1016 // Probably the item has been filtered.
1017 QHash<KFileItem, ItemData*>::iterator it = m_filteredItems.find(item);
1018 if (it != m_filteredItems.end()) {
1019 delete it.value();
1020 m_filteredItems.erase(it);
1021 }
1022 }
1023 }
1024
1025 std::sort(indexesToRemove.begin(), indexesToRemove.end());
1026
1027 if (m_requestRole[ExpandedParentsCountRole] && !m_expandedDirs.isEmpty()) {
1028 // Assure that removing a parent item also results in removing all children
1029 QVector<int> indexesToRemoveWithChildren;
1030 indexesToRemoveWithChildren.reserve(m_itemData.count());
1031
1032 const int itemCount = m_itemData.count();
1033 foreach (int index, indexesToRemove) {
1034 indexesToRemoveWithChildren.append(index);
1035
1036 const int parentLevel = expandedParentsCount(index);
1037 int childIndex = index + 1;
1038 while (childIndex < itemCount && expandedParentsCount(childIndex) > parentLevel) {
1039 indexesToRemoveWithChildren.append(childIndex);
1040 ++childIndex;
1041 }
1042 }
1043
1044 indexesToRemove = indexesToRemoveWithChildren;
1045 }
1046
1047 const KItemRangeList itemRanges = KItemRangeList::fromSortedContainer(indexesToRemove);
1048 removeFilteredChildren(itemRanges);
1049 removeItems(itemRanges, DeleteItemData);
1050 }
1051
1052 void KFileItemModel::slotRefreshItems(const QList<QPair<KFileItem, KFileItem> >& items)
1053 {
1054 Q_ASSERT(!items.isEmpty());
1055 #ifdef KFILEITEMMODEL_DEBUG
1056 qCDebug(DolphinDebug) << "Refreshing" << items.count() << "items";
1057 #endif
1058
1059 // Get the indexes of all items that have been refreshed
1060 QList<int> indexes;
1061 indexes.reserve(items.count());
1062
1063 QSet<QByteArray> changedRoles;
1064
1065 QListIterator<QPair<KFileItem, KFileItem> > it(items);
1066 while (it.hasNext()) {
1067 const QPair<KFileItem, KFileItem>& itemPair = it.next();
1068 const KFileItem& oldItem = itemPair.first;
1069 const KFileItem& newItem = itemPair.second;
1070 const int indexForItem = index(oldItem);
1071 if (indexForItem >= 0) {
1072 m_itemData[indexForItem]->item = newItem;
1073
1074 // Keep old values as long as possible if they could not retrieved synchronously yet.
1075 // The update of the values will be done asynchronously by KFileItemModelRolesUpdater.
1076 QHashIterator<QByteArray, QVariant> it(retrieveData(newItem, m_itemData.at(indexForItem)->parent));
1077 QHash<QByteArray, QVariant>& values = m_itemData[indexForItem]->values;
1078 while (it.hasNext()) {
1079 it.next();
1080 const QByteArray& role = it.key();
1081 if (values.value(role) != it.value()) {
1082 values.insert(role, it.value());
1083 changedRoles.insert(role);
1084 }
1085 }
1086
1087 m_items.remove(oldItem.url());
1088 m_items.insert(newItem.url(), indexForItem);
1089 indexes.append(indexForItem);
1090 } else {
1091 // Check if 'oldItem' is one of the filtered items.
1092 QHash<KFileItem, ItemData*>::iterator it = m_filteredItems.find(oldItem);
1093 if (it != m_filteredItems.end()) {
1094 ItemData* itemData = it.value();
1095 itemData->item = newItem;
1096
1097 // The data stored in 'values' might have changed. Therefore, we clear
1098 // 'values' and re-populate it the next time it is requested via data(int).
1099 itemData->values.clear();
1100
1101 m_filteredItems.erase(it);
1102 m_filteredItems.insert(newItem, itemData);
1103 }
1104 }
1105 }
1106
1107 // If the changed items have been created recently, they might not be in m_items yet.
1108 // In that case, the list 'indexes' might be empty.
1109 if (indexes.isEmpty()) {
1110 return;
1111 }
1112
1113 // Extract the item-ranges out of the changed indexes
1114 qSort(indexes);
1115 const KItemRangeList itemRangeList = KItemRangeList::fromSortedContainer(indexes);
1116 emitItemsChangedAndTriggerResorting(itemRangeList, changedRoles);
1117 }
1118
1119 void KFileItemModel::slotClear()
1120 {
1121 #ifdef KFILEITEMMODEL_DEBUG
1122 qCDebug(DolphinDebug) << "Clearing all items";
1123 #endif
1124
1125 qDeleteAll(m_filteredItems);
1126 m_filteredItems.clear();
1127 m_groups.clear();
1128
1129 m_maximumUpdateIntervalTimer->stop();
1130 m_resortAllItemsTimer->stop();
1131
1132 qDeleteAll(m_pendingItemsToInsert);
1133 m_pendingItemsToInsert.clear();
1134
1135 const int removedCount = m_itemData.count();
1136 if (removedCount > 0) {
1137 qDeleteAll(m_itemData);
1138 m_itemData.clear();
1139 m_items.clear();
1140 emit itemsRemoved(KItemRangeList() << KItemRange(0, removedCount));
1141 }
1142
1143 m_expandedDirs.clear();
1144 }
1145
1146 void KFileItemModel::slotSortingChoiceChanged()
1147 {
1148 loadSortingSettings();
1149 resortAllItems();
1150 }
1151
1152 void KFileItemModel::dispatchPendingItemsToInsert()
1153 {
1154 if (!m_pendingItemsToInsert.isEmpty()) {
1155 insertItems(m_pendingItemsToInsert);
1156 m_pendingItemsToInsert.clear();
1157 }
1158 }
1159
1160 void KFileItemModel::insertItems(QList<ItemData*>& newItems)
1161 {
1162 if (newItems.isEmpty()) {
1163 return;
1164 }
1165
1166 #ifdef KFILEITEMMODEL_DEBUG
1167 QElapsedTimer timer;
1168 timer.start();
1169 qCDebug(DolphinDebug) << "===========================================================";
1170 qCDebug(DolphinDebug) << "Inserting" << newItems.count() << "items";
1171 #endif
1172
1173 m_groups.clear();
1174 prepareItemsForSorting(newItems);
1175
1176 if (m_sortRole == NameRole && m_naturalSorting) {
1177 // Natural sorting of items can be very slow. However, it becomes much
1178 // faster if the input sequence is already mostly sorted. Therefore, we
1179 // first sort 'newItems' according to the QStrings returned by
1180 // KFileItem::text() using QString::operator<(), which is quite fast.
1181 parallelMergeSort(newItems.begin(), newItems.end(), nameLessThan, QThread::idealThreadCount());
1182 }
1183
1184 sort(newItems.begin(), newItems.end());
1185
1186 #ifdef KFILEITEMMODEL_DEBUG
1187 qCDebug(DolphinDebug) << "[TIME] Sorting:" << timer.elapsed();
1188 #endif
1189
1190 KItemRangeList itemRanges;
1191 const int existingItemCount = m_itemData.count();
1192 const int newItemCount = newItems.count();
1193 const int totalItemCount = existingItemCount + newItemCount;
1194
1195 if (existingItemCount == 0) {
1196 // Optimization for the common special case that there are no
1197 // items in the model yet. Happens, e.g., when entering a folder.
1198 m_itemData = newItems;
1199 itemRanges << KItemRange(0, newItemCount);
1200 } else {
1201 m_itemData.reserve(totalItemCount);
1202 for (int i = existingItemCount; i < totalItemCount; ++i) {
1203 m_itemData.append(0);
1204 }
1205
1206 // We build the new list m_itemData in reverse order to minimize
1207 // the number of moves and guarantee O(N) complexity.
1208 int targetIndex = totalItemCount - 1;
1209 int sourceIndexExistingItems = existingItemCount - 1;
1210 int sourceIndexNewItems = newItemCount - 1;
1211
1212 int rangeCount = 0;
1213
1214 while (sourceIndexNewItems >= 0) {
1215 ItemData* newItem = newItems.at(sourceIndexNewItems);
1216 if (sourceIndexExistingItems >= 0 && lessThan(newItem, m_itemData.at(sourceIndexExistingItems), m_collator)) {
1217 // Move an existing item to its new position. If any new items
1218 // are behind it, push the item range to itemRanges.
1219 if (rangeCount > 0) {
1220 itemRanges << KItemRange(sourceIndexExistingItems + 1, rangeCount);
1221 rangeCount = 0;
1222 }
1223
1224 m_itemData[targetIndex] = m_itemData.at(sourceIndexExistingItems);
1225 --sourceIndexExistingItems;
1226 } else {
1227 // Insert a new item into the list.
1228 ++rangeCount;
1229 m_itemData[targetIndex] = newItem;
1230 --sourceIndexNewItems;
1231 }
1232 --targetIndex;
1233 }
1234
1235 // Push the final item range to itemRanges.
1236 if (rangeCount > 0) {
1237 itemRanges << KItemRange(sourceIndexExistingItems + 1, rangeCount);
1238 }
1239
1240 // Note that itemRanges is still sorted in reverse order.
1241 std::reverse(itemRanges.begin(), itemRanges.end());
1242 }
1243
1244 // The indexes in m_items are not correct anymore. Therefore, we clear m_items.
1245 // It will be re-populated with the updated indices if index(const QUrl&) is called.
1246 m_items.clear();
1247
1248 emit itemsInserted(itemRanges);
1249
1250 #ifdef KFILEITEMMODEL_DEBUG
1251 qCDebug(DolphinDebug) << "[TIME] Inserting of" << newItems.count() << "items:" << timer.elapsed();
1252 #endif
1253 }
1254
1255 void KFileItemModel::removeItems(const KItemRangeList& itemRanges, RemoveItemsBehavior behavior)
1256 {
1257 if (itemRanges.isEmpty()) {
1258 return;
1259 }
1260
1261 m_groups.clear();
1262
1263 // Step 1: Remove the items from m_itemData, and free the ItemData.
1264 int removedItemsCount = 0;
1265 foreach (const KItemRange& range, itemRanges) {
1266 removedItemsCount += range.count;
1267
1268 for (int index = range.index; index < range.index + range.count; ++index) {
1269 if (behavior == DeleteItemData) {
1270 delete m_itemData.at(index);
1271 }
1272
1273 m_itemData[index] = 0;
1274 }
1275 }
1276
1277 // Step 2: Remove the ItemData pointers from the list m_itemData.
1278 int target = itemRanges.at(0).index;
1279 int source = itemRanges.at(0).index + itemRanges.at(0).count;
1280 int nextRange = 1;
1281
1282 const int oldItemDataCount = m_itemData.count();
1283 while (source < oldItemDataCount) {
1284 m_itemData[target] = m_itemData[source];
1285 ++target;
1286 ++source;
1287
1288 if (nextRange < itemRanges.count() && source == itemRanges.at(nextRange).index) {
1289 // Skip the items in the next removed range.
1290 source += itemRanges.at(nextRange).count;
1291 ++nextRange;
1292 }
1293 }
1294
1295 m_itemData.erase(m_itemData.end() - removedItemsCount, m_itemData.end());
1296
1297 // The indexes in m_items are not correct anymore. Therefore, we clear m_items.
1298 // It will be re-populated with the updated indices if index(const QUrl&) is called.
1299 m_items.clear();
1300
1301 emit itemsRemoved(itemRanges);
1302 }
1303
1304 QList<KFileItemModel::ItemData*> KFileItemModel::createItemDataList(const QUrl& parentUrl, const KFileItemList& items) const
1305 {
1306 if (m_sortRole == TypeRole) {
1307 // Try to resolve the MIME-types synchronously to prevent a reordering of
1308 // the items when sorting by type (per default MIME-types are resolved
1309 // asynchronously by KFileItemModelRolesUpdater).
1310 determineMimeTypes(items, 200);
1311 }
1312
1313 const int parentIndex = index(parentUrl);
1314 ItemData* parentItem = parentIndex < 0 ? 0 : m_itemData.at(parentIndex);
1315
1316 QList<ItemData*> itemDataList;
1317 itemDataList.reserve(items.count());
1318
1319 foreach (const KFileItem& item, items) {
1320 ItemData* itemData = new ItemData();
1321 itemData->item = item;
1322 itemData->parent = parentItem;
1323 itemDataList.append(itemData);
1324 }
1325
1326 return itemDataList;
1327 }
1328
1329 void KFileItemModel::prepareItemsForSorting(QList<ItemData*>& itemDataList)
1330 {
1331 switch (m_sortRole) {
1332 case PermissionsRole:
1333 case OwnerRole:
1334 case GroupRole:
1335 case DestinationRole:
1336 case PathRole:
1337 case DeletionTimeRole:
1338 // These roles can be determined with retrieveData, and they have to be stored
1339 // in the QHash "values" for the sorting.
1340 foreach (ItemData* itemData, itemDataList) {
1341 if (itemData->values.isEmpty()) {
1342 itemData->values = retrieveData(itemData->item, itemData->parent);
1343 }
1344 }
1345 break;
1346
1347 case TypeRole:
1348 // At least store the data including the file type for items with known MIME type.
1349 foreach (ItemData* itemData, itemDataList) {
1350 if (itemData->values.isEmpty()) {
1351 const KFileItem item = itemData->item;
1352 if (item.isDir() || item.isMimeTypeKnown()) {
1353 itemData->values = retrieveData(itemData->item, itemData->parent);
1354 }
1355 }
1356 }
1357 break;
1358
1359 default:
1360 // The other roles are either resolved by KFileItemModelRolesUpdater
1361 // (this includes the SizeRole for directories), or they do not need
1362 // to be stored in the QHash "values" for sorting because the data can
1363 // be retrieved directly from the KFileItem (NameRole, SizeRole for files,
1364 // DateRole).
1365 break;
1366 }
1367 }
1368
1369 int KFileItemModel::expandedParentsCount(const ItemData* data)
1370 {
1371 // The hash 'values' is only guaranteed to contain the key "expandedParentsCount"
1372 // if the corresponding item is expanded, and it is not a top-level item.
1373 const ItemData* parent = data->parent;
1374 if (parent) {
1375 if (parent->parent) {
1376 Q_ASSERT(parent->values.contains("expandedParentsCount"));
1377 return parent->values.value("expandedParentsCount").toInt() + 1;
1378 } else {
1379 return 1;
1380 }
1381 } else {
1382 return 0;
1383 }
1384 }
1385
1386 void KFileItemModel::removeExpandedItems()
1387 {
1388 QVector<int> indexesToRemove;
1389
1390 const int maxIndex = m_itemData.count() - 1;
1391 for (int i = 0; i <= maxIndex; ++i) {
1392 const ItemData* itemData = m_itemData.at(i);
1393 if (itemData->parent) {
1394 indexesToRemove.append(i);
1395 }
1396 }
1397
1398 removeItems(KItemRangeList::fromSortedContainer(indexesToRemove), DeleteItemData);
1399 m_expandedDirs.clear();
1400
1401 // Also remove all filtered items which have a parent.
1402 QHash<KFileItem, ItemData*>::iterator it = m_filteredItems.begin();
1403 const QHash<KFileItem, ItemData*>::iterator end = m_filteredItems.end();
1404
1405 while (it != end) {
1406 if (it.value()->parent) {
1407 delete it.value();
1408 it = m_filteredItems.erase(it);
1409 } else {
1410 ++it;
1411 }
1412 }
1413 }
1414
1415 void KFileItemModel::emitItemsChangedAndTriggerResorting(const KItemRangeList& itemRanges, const QSet<QByteArray>& changedRoles)
1416 {
1417 emit itemsChanged(itemRanges, changedRoles);
1418
1419 // Trigger a resorting if necessary. Note that this can happen even if the sort
1420 // role has not changed at all because the file name can be used as a fallback.
1421 if (changedRoles.contains(sortRole()) || changedRoles.contains(roleForType(NameRole))) {
1422 foreach (const KItemRange& range, itemRanges) {
1423 bool needsResorting = false;
1424
1425 const int first = range.index;
1426 const int last = range.index + range.count - 1;
1427
1428 // Resorting the model is necessary if
1429 // (a) The first item in the range is "lessThan" its predecessor,
1430 // (b) the successor of the last item is "lessThan" the last item, or
1431 // (c) the internal order of the items in the range is incorrect.
1432 if (first > 0
1433 && lessThan(m_itemData.at(first), m_itemData.at(first - 1), m_collator)) {
1434 needsResorting = true;
1435 } else if (last < count() - 1
1436 && lessThan(m_itemData.at(last + 1), m_itemData.at(last), m_collator)) {
1437 needsResorting = true;
1438 } else {
1439 for (int index = first; index < last; ++index) {
1440 if (lessThan(m_itemData.at(index + 1), m_itemData.at(index), m_collator)) {
1441 needsResorting = true;
1442 break;
1443 }
1444 }
1445 }
1446
1447 if (needsResorting) {
1448 m_resortAllItemsTimer->start();
1449 return;
1450 }
1451 }
1452 }
1453
1454 if (groupedSorting() && changedRoles.contains(sortRole())) {
1455 // The position is still correct, but the groups might have changed
1456 // if the changed item is either the first or the last item in a
1457 // group.
1458 // In principle, we could try to find out if the item really is the
1459 // first or last one in its group and then update the groups
1460 // (possibly with a delayed timer to make sure that we don't
1461 // re-calculate the groups very often if items are updated one by
1462 // one), but starting m_resortAllItemsTimer is easier.
1463 m_resortAllItemsTimer->start();
1464 }
1465 }
1466
1467 void KFileItemModel::resetRoles()
1468 {
1469 for (int i = 0; i < RolesCount; ++i) {
1470 m_requestRole[i] = false;
1471 }
1472 }
1473
1474 KFileItemModel::RoleType KFileItemModel::typeForRole(const QByteArray& role) const
1475 {
1476 static QHash<QByteArray, RoleType> roles;
1477 if (roles.isEmpty()) {
1478 // Insert user visible roles that can be accessed with
1479 // KFileItemModel::roleInformation()
1480 int count = 0;
1481 const RoleInfoMap* map = rolesInfoMap(count);
1482 for (int i = 0; i < count; ++i) {
1483 roles.insert(map[i].role, map[i].roleType);
1484 }
1485
1486 // Insert internal roles (take care to synchronize the implementation
1487 // with KFileItemModel::roleForType() in case if a change is done).
1488 roles.insert("isDir", IsDirRole);
1489 roles.insert("isLink", IsLinkRole);
1490 roles.insert("isHidden", IsHiddenRole);
1491 roles.insert("isExpanded", IsExpandedRole);
1492 roles.insert("isExpandable", IsExpandableRole);
1493 roles.insert("expandedParentsCount", ExpandedParentsCountRole);
1494
1495 Q_ASSERT(roles.count() == RolesCount);
1496 }
1497
1498 return roles.value(role, NoRole);
1499 }
1500
1501 QByteArray KFileItemModel::roleForType(RoleType roleType) const
1502 {
1503 static QHash<RoleType, QByteArray> roles;
1504 if (roles.isEmpty()) {
1505 // Insert user visible roles that can be accessed with
1506 // KFileItemModel::roleInformation()
1507 int count = 0;
1508 const RoleInfoMap* map = rolesInfoMap(count);
1509 for (int i = 0; i < count; ++i) {
1510 roles.insert(map[i].roleType, map[i].role);
1511 }
1512
1513 // Insert internal roles (take care to synchronize the implementation
1514 // with KFileItemModel::typeForRole() in case if a change is done).
1515 roles.insert(IsDirRole, "isDir");
1516 roles.insert(IsLinkRole, "isLink");
1517 roles.insert(IsHiddenRole, "isHidden");
1518 roles.insert(IsExpandedRole, "isExpanded");
1519 roles.insert(IsExpandableRole, "isExpandable");
1520 roles.insert(ExpandedParentsCountRole, "expandedParentsCount");
1521
1522 Q_ASSERT(roles.count() == RolesCount);
1523 };
1524
1525 return roles.value(roleType);
1526 }
1527
1528 QHash<QByteArray, QVariant> KFileItemModel::retrieveData(const KFileItem& item, const ItemData* parent) const
1529 {
1530 // It is important to insert only roles that are fast to retrieve. E.g.
1531 // KFileItem::iconName() can be very expensive if the MIME-type is unknown
1532 // and hence will be retrieved asynchronously by KFileItemModelRolesUpdater.
1533 QHash<QByteArray, QVariant> data;
1534 data.insert(sharedValue("url"), item.url());
1535
1536 const bool isDir = item.isDir();
1537 if (m_requestRole[IsDirRole] && isDir) {
1538 data.insert(sharedValue("isDir"), true);
1539 }
1540
1541 if (m_requestRole[IsLinkRole] && item.isLink()) {
1542 data.insert(sharedValue("isLink"), true);
1543 }
1544
1545 if (m_requestRole[IsHiddenRole] && item.isHidden()) {
1546 data.insert(sharedValue("isHidden"), true);
1547 }
1548
1549 if (m_requestRole[NameRole]) {
1550 data.insert(sharedValue("text"), item.text());
1551 }
1552
1553 if (m_requestRole[SizeRole] && !isDir) {
1554 data.insert(sharedValue("size"), item.size());
1555 }
1556
1557 if (m_requestRole[ModificationTimeRole]) {
1558 // Don't use KFileItem::timeString() as this is too expensive when
1559 // having several thousands of items. Instead the formatting of the
1560 // date-time will be done on-demand by the view when the date will be shown.
1561 const QDateTime dateTime = item.time(KFileItem::ModificationTime);
1562 data.insert(sharedValue("modificationtime"), dateTime);
1563 }
1564
1565 if (m_requestRole[AccessTimeRole]) {
1566 // Don't use KFileItem::timeString() as this is too expensive when
1567 // having several thousands of items. Instead the formatting of the
1568 // date-time will be done on-demand by the view when the date will be shown.
1569 const QDateTime dateTime = item.time(KFileItem::AccessTime);
1570 data.insert(sharedValue("accesstime"), dateTime);
1571 }
1572
1573 if (m_requestRole[PermissionsRole]) {
1574 data.insert(sharedValue("permissions"), item.permissionsString());
1575 }
1576
1577 if (m_requestRole[OwnerRole]) {
1578 data.insert(sharedValue("owner"), item.user());
1579 }
1580
1581 if (m_requestRole[GroupRole]) {
1582 data.insert(sharedValue("group"), item.group());
1583 }
1584
1585 if (m_requestRole[DestinationRole]) {
1586 QString destination = item.linkDest();
1587 if (destination.isEmpty()) {
1588 destination = QStringLiteral("-");
1589 }
1590 data.insert(sharedValue("destination"), destination);
1591 }
1592
1593 if (m_requestRole[PathRole]) {
1594 QString path;
1595 if (item.url().scheme() == QLatin1String("trash")) {
1596 path = item.entry().stringValue(KIO::UDSEntry::UDS_EXTRA);
1597 } else {
1598 // For performance reasons cache the home-path in a static QString
1599 // (see QDir::homePath() for more details)
1600 static QString homePath;
1601 if (homePath.isEmpty()) {
1602 homePath = QDir::homePath();
1603 }
1604
1605 path = item.localPath();
1606 if (path.startsWith(homePath)) {
1607 path.replace(0, homePath.length(), QLatin1Char('~'));
1608 }
1609 }
1610
1611 const int index = path.lastIndexOf(item.text());
1612 path = path.mid(0, index - 1);
1613 data.insert(sharedValue("path"), path);
1614 }
1615
1616 if (m_requestRole[DeletionTimeRole]) {
1617 QDateTime deletionTime;
1618 if (item.url().scheme() == QLatin1String("trash")) {
1619 deletionTime = QDateTime::fromString(item.entry().stringValue(KIO::UDSEntry::UDS_EXTRA + 1), Qt::ISODate);
1620 }
1621 data.insert(sharedValue("deletiontime"), deletionTime);
1622 }
1623
1624 if (m_requestRole[IsExpandableRole] && isDir) {
1625 data.insert(sharedValue("isExpandable"), true);
1626 }
1627
1628 if (m_requestRole[ExpandedParentsCountRole]) {
1629 if (parent) {
1630 const int level = expandedParentsCount(parent) + 1;
1631 data.insert(sharedValue("expandedParentsCount"), level);
1632 }
1633 }
1634
1635 if (item.isMimeTypeKnown()) {
1636 data.insert(sharedValue("iconName"), item.iconName());
1637
1638 if (m_requestRole[TypeRole]) {
1639 data.insert(sharedValue("type"), item.mimeComment());
1640 }
1641 } else if (m_requestRole[TypeRole] && isDir) {
1642 static const QString folderMimeType = item.mimeComment();
1643 data.insert(sharedValue("type"), folderMimeType);
1644 }
1645
1646 return data;
1647 }
1648
1649 bool KFileItemModel::lessThan(const ItemData* a, const ItemData* b, const QCollator& collator) const
1650 {
1651 int result = 0;
1652
1653 if (a->parent != b->parent) {
1654 const int expansionLevelA = expandedParentsCount(a);
1655 const int expansionLevelB = expandedParentsCount(b);
1656
1657 // If b has a higher expansion level than a, check if a is a parent
1658 // of b, and make sure that both expansion levels are equal otherwise.
1659 for (int i = expansionLevelB; i > expansionLevelA; --i) {
1660 if (b->parent == a) {
1661 return true;
1662 }
1663 b = b->parent;
1664 }
1665
1666 // If a has a higher expansion level than a, check if b is a parent
1667 // of a, and make sure that both expansion levels are equal otherwise.
1668 for (int i = expansionLevelA; i > expansionLevelB; --i) {
1669 if (a->parent == b) {
1670 return false;
1671 }
1672 a = a->parent;
1673 }
1674
1675 Q_ASSERT(expandedParentsCount(a) == expandedParentsCount(b));
1676
1677 // Compare the last parents of a and b which are different.
1678 while (a->parent != b->parent) {
1679 a = a->parent;
1680 b = b->parent;
1681 }
1682 }
1683
1684 if (m_sortDirsFirst || m_sortRole == SizeRole) {
1685 const bool isDirA = a->item.isDir();
1686 const bool isDirB = b->item.isDir();
1687 if (isDirA && !isDirB) {
1688 return true;
1689 } else if (!isDirA && isDirB) {
1690 return false;
1691 }
1692 }
1693
1694 result = sortRoleCompare(a, b, collator);
1695
1696 return (sortOrder() == Qt::AscendingOrder) ? result < 0 : result > 0;
1697 }
1698
1699 /**
1700 * Helper class for KFileItemModel::sort().
1701 */
1702 class KFileItemModelLessThan
1703 {
1704 public:
1705 KFileItemModelLessThan(const KFileItemModel* model, const QCollator& collator) :
1706 m_model(model),
1707 m_collator(collator)
1708 {
1709 }
1710
1711 KFileItemModelLessThan(const KFileItemModelLessThan& other) :
1712 m_model(other.m_model),
1713 m_collator()
1714 {
1715 m_collator.setCaseSensitivity(other.m_collator.caseSensitivity());
1716 m_collator.setIgnorePunctuation(other.m_collator.ignorePunctuation());
1717 m_collator.setLocale(other.m_collator.locale());
1718 m_collator.setNumericMode(other.m_collator.numericMode());
1719 }
1720
1721 ~KFileItemModelLessThan() = default;
1722 //We do not delete m_model as the pointer was passed from outside ant it will be deleted elsewhere.
1723
1724 KFileItemModelLessThan& operator=(const KFileItemModelLessThan& other)
1725 {
1726 m_model = other.m_model;
1727 m_collator = other.m_collator;
1728 return *this;
1729 }
1730
1731 bool operator()(const KFileItemModel::ItemData* a, const KFileItemModel::ItemData* b) const
1732 {
1733 return m_model->lessThan(a, b, m_collator);
1734 }
1735
1736 private:
1737 const KFileItemModel* m_model;
1738 QCollator m_collator;
1739 };
1740
1741 void KFileItemModel::sort(QList<KFileItemModel::ItemData*>::iterator begin,
1742 QList<KFileItemModel::ItemData*>::iterator end) const
1743 {
1744 KFileItemModelLessThan lessThan(this, m_collator);
1745
1746 if (m_sortRole == NameRole) {
1747 // Sorting by name can be expensive, in particular if natural sorting is
1748 // enabled. Use all CPU cores to speed up the sorting process.
1749 static const int numberOfThreads = QThread::idealThreadCount();
1750 parallelMergeSort(begin, end, lessThan, numberOfThreads);
1751 } else {
1752 // Sorting by other roles is quite fast. Use only one thread to prevent
1753 // problems caused by non-reentrant comparison functions, see
1754 // https://bugs.kde.org/show_bug.cgi?id=312679
1755 mergeSort(begin, end, lessThan);
1756 }
1757 }
1758
1759 int KFileItemModel::sortRoleCompare(const ItemData* a, const ItemData* b, const QCollator& collator) const
1760 {
1761 const KFileItem& itemA = a->item;
1762 const KFileItem& itemB = b->item;
1763
1764 int result = 0;
1765
1766 switch (m_sortRole) {
1767 case NameRole:
1768 // The name role is handled as default fallback after the switch
1769 break;
1770
1771 case SizeRole: {
1772 if (itemA.isDir()) {
1773 // See "if (m_sortFoldersFirst || m_sortRole == SizeRole)" in KFileItemModel::lessThan():
1774 Q_ASSERT(itemB.isDir());
1775
1776 const QVariant valueA = a->values.value("size");
1777 const QVariant valueB = b->values.value("size");
1778 if (valueA.isNull() && valueB.isNull()) {
1779 result = 0;
1780 } else if (valueA.isNull()) {
1781 result = -1;
1782 } else if (valueB.isNull()) {
1783 result = +1;
1784 } else {
1785 result = valueA.toInt() - valueB.toInt();
1786 }
1787 } else {
1788 // See "if (m_sortFoldersFirst || m_sortRole == SizeRole)" in KFileItemModel::lessThan():
1789 Q_ASSERT(!itemB.isDir());
1790 const KIO::filesize_t sizeA = itemA.size();
1791 const KIO::filesize_t sizeB = itemB.size();
1792 if (sizeA > sizeB) {
1793 result = +1;
1794 } else if (sizeA < sizeB) {
1795 result = -1;
1796 } else {
1797 result = 0;
1798 }
1799 }
1800 break;
1801 }
1802
1803 case ModificationTimeRole: {
1804 const QDateTime dateTimeA = itemA.time(KFileItem::ModificationTime);
1805 const QDateTime dateTimeB = itemB.time(KFileItem::ModificationTime);
1806 if (dateTimeA < dateTimeB) {
1807 result = -1;
1808 } else if (dateTimeA > dateTimeB) {
1809 result = +1;
1810 }
1811 break;
1812 }
1813
1814 case DeletionTimeRole: {
1815 const QDateTime dateTimeA = a->values.value("deletiontime").toDateTime();
1816 const QDateTime dateTimeB = b->values.value("deletiontime").toDateTime();
1817 if (dateTimeA < dateTimeB) {
1818 result = -1;
1819 } else if (dateTimeA > dateTimeB) {
1820 result = +1;
1821 }
1822 break;
1823 }
1824
1825 case RatingRole: {
1826 result = a->values.value("rating").toInt() - b->values.value("rating").toInt();
1827 break;
1828 }
1829
1830 case ImageSizeRole: {
1831 // Alway use a natural comparing to interpret the numbers of a string like
1832 // "1600 x 1200" for having a correct sorting.
1833 result = collator.compare(a->values.value("imageSize").toString(),
1834 b->values.value("imageSize").toString());
1835 break;
1836 }
1837
1838 default: {
1839 const QByteArray role = roleForType(m_sortRole);
1840 result = QString::compare(a->values.value(role).toString(),
1841 b->values.value(role).toString());
1842 break;
1843 }
1844
1845 }
1846
1847 if (result != 0) {
1848 // The current sort role was sufficient to define an order
1849 return result;
1850 }
1851
1852 // Fallback #1: Compare the text of the items
1853 result = stringCompare(itemA.text(), itemB.text(), collator);
1854 if (result != 0) {
1855 return result;
1856 }
1857
1858 // Fallback #2: KFileItem::text() may not be unique in case UDS_DISPLAY_NAME is used
1859 result = stringCompare(itemA.name(), itemB.name(), collator);
1860 if (result != 0) {
1861 return result;
1862 }
1863
1864 // Fallback #3: It must be assured that the sort order is always unique even if two values have been
1865 // equal. In this case a comparison of the URL is done which is unique in all cases
1866 // within KDirLister.
1867 return QString::compare(itemA.url().url(), itemB.url().url(), Qt::CaseSensitive);
1868 }
1869
1870 int KFileItemModel::stringCompare(const QString& a, const QString& b, const QCollator& collator) const
1871 {
1872 if (m_naturalSorting) {
1873 return collator.compare(a, b);
1874 }
1875
1876 const int result = QString::compare(a, b, collator.caseSensitivity());
1877 if (result != 0 || collator.caseSensitivity() == Qt::CaseSensitive) {
1878 // Only return the result, if the strings are not equal. If they are equal by a case insensitive
1879 // comparison, still a deterministic sort order is required. A case sensitive
1880 // comparison is done as fallback.
1881 return result;
1882 }
1883
1884 return QString::compare(a, b, Qt::CaseSensitive);
1885 }
1886
1887 bool KFileItemModel::useMaximumUpdateInterval() const
1888 {
1889 return !m_dirLister->url().isLocalFile();
1890 }
1891
1892 QList<QPair<int, QVariant> > KFileItemModel::nameRoleGroups() const
1893 {
1894 Q_ASSERT(!m_itemData.isEmpty());
1895
1896 const int maxIndex = count() - 1;
1897 QList<QPair<int, QVariant> > groups;
1898
1899 QString groupValue;
1900 QChar firstChar;
1901 for (int i = 0; i <= maxIndex; ++i) {
1902 if (isChildItem(i)) {
1903 continue;
1904 }
1905
1906 const QString name = m_itemData.at(i)->item.text();
1907
1908 // Use the first character of the name as group indication
1909 QChar newFirstChar = name.at(0).toUpper();
1910 if (newFirstChar == QLatin1Char('~') && name.length() > 1) {
1911 newFirstChar = name.at(1).toUpper();
1912 }
1913
1914 if (firstChar != newFirstChar) {
1915 QString newGroupValue;
1916 if (newFirstChar.isLetter()) {
1917 // Try to find a matching group in the range 'A' to 'Z'.
1918 static std::vector<QChar> lettersAtoZ;
1919 lettersAtoZ.reserve('Z' - 'A' + 1);
1920 if (lettersAtoZ.empty()) {
1921 for (char c = 'A'; c <= 'Z'; ++c) {
1922 lettersAtoZ.push_back(QLatin1Char(c));
1923 }
1924 }
1925
1926 auto localeAwareLessThan = [this](QChar c1, QChar c2) -> bool {
1927 return m_collator.compare(c1, c2) < 0;
1928 };
1929
1930 std::vector<QChar>::iterator it = std::lower_bound(lettersAtoZ.begin(), lettersAtoZ.end(), newFirstChar, localeAwareLessThan);
1931 if (it != lettersAtoZ.end()) {
1932 if (localeAwareLessThan(newFirstChar, *it) && it != lettersAtoZ.begin()) {
1933 // newFirstChar belongs to the group preceding *it.
1934 // Example: for an umlaut 'A' in the German locale, *it would be 'B' now.
1935 --it;
1936 }
1937 newGroupValue = *it;
1938 } else {
1939 newGroupValue = newFirstChar;
1940 }
1941 } else if (newFirstChar >= QLatin1Char('0') && newFirstChar <= QLatin1Char('9')) {
1942 // Apply group '0 - 9' for any name that starts with a digit
1943 newGroupValue = i18nc("@title:group Groups that start with a digit", "0 - 9");
1944 } else {
1945 newGroupValue = i18nc("@title:group", "Others");
1946 }
1947
1948 if (newGroupValue != groupValue) {
1949 groupValue = newGroupValue;
1950 groups.append(QPair<int, QVariant>(i, newGroupValue));
1951 }
1952
1953 firstChar = newFirstChar;
1954 }
1955 }
1956 return groups;
1957 }
1958
1959 QList<QPair<int, QVariant> > KFileItemModel::sizeRoleGroups() const
1960 {
1961 Q_ASSERT(!m_itemData.isEmpty());
1962
1963 const int maxIndex = count() - 1;
1964 QList<QPair<int, QVariant> > groups;
1965
1966 QString groupValue;
1967 for (int i = 0; i <= maxIndex; ++i) {
1968 if (isChildItem(i)) {
1969 continue;
1970 }
1971
1972 const KFileItem& item = m_itemData.at(i)->item;
1973 const KIO::filesize_t fileSize = !item.isNull() ? item.size() : ~0U;
1974 QString newGroupValue;
1975 if (!item.isNull() && item.isDir()) {
1976 newGroupValue = i18nc("@title:group Size", "Folders");
1977 } else if (fileSize < 5 * 1024 * 1024) {
1978 newGroupValue = i18nc("@title:group Size", "Small");
1979 } else if (fileSize < 10 * 1024 * 1024) {
1980 newGroupValue = i18nc("@title:group Size", "Medium");
1981 } else {
1982 newGroupValue = i18nc("@title:group Size", "Big");
1983 }
1984
1985 if (newGroupValue != groupValue) {
1986 groupValue = newGroupValue;
1987 groups.append(QPair<int, QVariant>(i, newGroupValue));
1988 }
1989 }
1990
1991 return groups;
1992 }
1993
1994 QList<QPair<int, QVariant> > KFileItemModel::timeRoleGroups(std::function<QDateTime(const ItemData *)> fileTimeCb) const
1995 {
1996 Q_ASSERT(!m_itemData.isEmpty());
1997
1998 const int maxIndex = count() - 1;
1999 QList<QPair<int, QVariant> > groups;
2000
2001 const QDate currentDate = QDate::currentDate();
2002
2003 QDate previousFileDate;
2004 QString groupValue;
2005 for (int i = 0; i <= maxIndex; ++i) {
2006 if (isChildItem(i)) {
2007 continue;
2008 }
2009
2010 const QDateTime fileTime = fileTimeCb(m_itemData.at(i));
2011 const QDate fileDate = fileTime.date();
2012 if (fileDate == previousFileDate) {
2013 // The current item is in the same group as the previous item
2014 continue;
2015 }
2016 previousFileDate = fileDate;
2017
2018 const int daysDistance = fileDate.daysTo(currentDate);
2019
2020 QString newGroupValue;
2021 if (currentDate.year() == fileDate.year() &&
2022 currentDate.month() == fileDate.month()) {
2023
2024 switch (daysDistance / 7) {
2025 case 0:
2026 switch (daysDistance) {
2027 case 0: newGroupValue = i18nc("@title:group Date", "Today"); break;
2028 case 1: newGroupValue = i18nc("@title:group Date", "Yesterday"); break;
2029 default:
2030 newGroupValue = fileTime.toString(
2031 i18nc("@title:group Date: The week day name: dddd", "dddd"));
2032 newGroupValue = i18nc("Can be used to script translation of \"dddd\""
2033 "with context @title:group Date", "%1", newGroupValue);
2034 }
2035 break;
2036 case 1:
2037 newGroupValue = i18nc("@title:group Date", "One Week Ago");
2038 break;
2039 case 2:
2040 newGroupValue = i18nc("@title:group Date", "Two Weeks Ago");
2041 break;
2042 case 3:
2043 newGroupValue = i18nc("@title:group Date", "Three Weeks Ago");
2044 break;
2045 case 4:
2046 case 5:
2047 newGroupValue = i18nc("@title:group Date", "Earlier this Month");
2048 break;
2049 default:
2050 Q_ASSERT(false);
2051 }
2052 } else {
2053 const QDate lastMonthDate = currentDate.addMonths(-1);
2054 if (lastMonthDate.year() == fileDate.year() &&
2055 lastMonthDate.month() == fileDate.month()) {
2056
2057 if (daysDistance == 1) {
2058 newGroupValue = fileTime.toString(i18nc("@title:group Date: "
2059 "MMMM is full month name in current locale, and yyyy is "
2060 "full year number", "'Yesterday' (MMMM, yyyy)"));
2061 newGroupValue = i18nc("Can be used to script translation of "
2062 "\"'Yesterday' (MMMM, yyyy)\" with context @title:group Date",
2063 "%1", newGroupValue);
2064 } else if (daysDistance <= 7) {
2065 newGroupValue = fileTime.toString(i18nc("@title:group Date: "
2066 "The week day name: dddd, MMMM is full month name "
2067 "in current locale, and yyyy is full year number",
2068 "dddd (MMMM, yyyy)"));
2069 newGroupValue = i18nc("Can be used to script translation of "
2070 "\"dddd (MMMM, yyyy)\" with context @title:group Date",
2071 "%1", newGroupValue);
2072 } else if (daysDistance <= 7 * 2) {
2073 newGroupValue = fileTime.toString(i18nc("@title:group Date: "
2074 "MMMM is full month name in current locale, and yyyy is "
2075 "full year number", "'One Week Ago' (MMMM, yyyy)"));
2076 newGroupValue = i18nc("Can be used to script translation of "
2077 "\"'One Week Ago' (MMMM, yyyy)\" with context @title:group Date",
2078 "%1", newGroupValue);
2079 } else if (daysDistance <= 7 * 3) {
2080 newGroupValue = fileTime.toString(i18nc("@title:group Date: "
2081 "MMMM is full month name in current locale, and yyyy is "
2082 "full year number", "'Two Weeks Ago' (MMMM, yyyy)"));
2083 newGroupValue = i18nc("Can be used to script translation of "
2084 "\"'Two Weeks Ago' (MMMM, yyyy)\" with context @title:group Date",
2085 "%1", newGroupValue);
2086 } else if (daysDistance <= 7 * 4) {
2087 newGroupValue = fileTime.toString(i18nc("@title:group Date: "
2088 "MMMM is full month name in current locale, and yyyy is "
2089 "full year number", "'Three Weeks Ago' (MMMM, yyyy)"));
2090 newGroupValue = i18nc("Can be used to script translation of "
2091 "\"'Three Weeks Ago' (MMMM, yyyy)\" with context @title:group Date",
2092 "%1", newGroupValue);
2093 } else {
2094 newGroupValue = fileTime.toString(i18nc("@title:group Date: "
2095 "MMMM is full month name in current locale, and yyyy is "
2096 "full year number", "'Earlier on' MMMM, yyyy"));
2097 newGroupValue = i18nc("Can be used to script translation of "
2098 "\"'Earlier on' MMMM, yyyy\" with context @title:group Date",
2099 "%1", newGroupValue);
2100 }
2101 } else {
2102 newGroupValue = fileTime.toString(i18nc("@title:group "
2103 "The month and year: MMMM is full month name in current locale, "
2104 "and yyyy is full year number", "MMMM, yyyy"));
2105 newGroupValue = i18nc("Can be used to script translation of "
2106 "\"MMMM, yyyy\" with context @title:group Date",
2107 "%1", newGroupValue);
2108 }
2109 }
2110
2111 if (newGroupValue != groupValue) {
2112 groupValue = newGroupValue;
2113 groups.append(QPair<int, QVariant>(i, newGroupValue));
2114 }
2115 }
2116
2117 return groups;
2118 }
2119
2120 QList<QPair<int, QVariant> > KFileItemModel::permissionRoleGroups() const
2121 {
2122 Q_ASSERT(!m_itemData.isEmpty());
2123
2124 const int maxIndex = count() - 1;
2125 QList<QPair<int, QVariant> > groups;
2126
2127 QString permissionsString;
2128 QString groupValue;
2129 for (int i = 0; i <= maxIndex; ++i) {
2130 if (isChildItem(i)) {
2131 continue;
2132 }
2133
2134 const ItemData* itemData = m_itemData.at(i);
2135 const QString newPermissionsString = itemData->values.value("permissions").toString();
2136 if (newPermissionsString == permissionsString) {
2137 continue;
2138 }
2139 permissionsString = newPermissionsString;
2140
2141 const QFileInfo info(itemData->item.url().toLocalFile());
2142
2143 // Set user string
2144 QString user;
2145 if (info.permission(QFile::ReadUser)) {
2146 user = i18nc("@item:intext Access permission, concatenated", "Read, ");
2147 }
2148 if (info.permission(QFile::WriteUser)) {
2149 user += i18nc("@item:intext Access permission, concatenated", "Write, ");
2150 }
2151 if (info.permission(QFile::ExeUser)) {
2152 user += i18nc("@item:intext Access permission, concatenated", "Execute, ");
2153 }
2154 user = user.isEmpty() ? i18nc("@item:intext Access permission, concatenated", "Forbidden") : user.mid(0, user.count() - 2);
2155
2156 // Set group string
2157 QString group;
2158 if (info.permission(QFile::ReadGroup)) {
2159 group = i18nc("@item:intext Access permission, concatenated", "Read, ");
2160 }
2161 if (info.permission(QFile::WriteGroup)) {
2162 group += i18nc("@item:intext Access permission, concatenated", "Write, ");
2163 }
2164 if (info.permission(QFile::ExeGroup)) {
2165 group += i18nc("@item:intext Access permission, concatenated", "Execute, ");
2166 }
2167 group = group.isEmpty() ? i18nc("@item:intext Access permission, concatenated", "Forbidden") : group.mid(0, group.count() - 2);
2168
2169 // Set others string
2170 QString others;
2171 if (info.permission(QFile::ReadOther)) {
2172 others = i18nc("@item:intext Access permission, concatenated", "Read, ");
2173 }
2174 if (info.permission(QFile::WriteOther)) {
2175 others += i18nc("@item:intext Access permission, concatenated", "Write, ");
2176 }
2177 if (info.permission(QFile::ExeOther)) {
2178 others += i18nc("@item:intext Access permission, concatenated", "Execute, ");
2179 }
2180 others = others.isEmpty() ? i18nc("@item:intext Access permission, concatenated", "Forbidden") : others.mid(0, others.count() - 2);
2181
2182 const QString newGroupValue = i18nc("@title:group Files and folders by permissions", "User: %1 | Group: %2 | Others: %3", user, group, others);
2183 if (newGroupValue != groupValue) {
2184 groupValue = newGroupValue;
2185 groups.append(QPair<int, QVariant>(i, newGroupValue));
2186 }
2187 }
2188
2189 return groups;
2190 }
2191
2192 QList<QPair<int, QVariant> > KFileItemModel::ratingRoleGroups() const
2193 {
2194 Q_ASSERT(!m_itemData.isEmpty());
2195
2196 const int maxIndex = count() - 1;
2197 QList<QPair<int, QVariant> > groups;
2198
2199 int groupValue = -1;
2200 for (int i = 0; i <= maxIndex; ++i) {
2201 if (isChildItem(i)) {
2202 continue;
2203 }
2204 const int newGroupValue = m_itemData.at(i)->values.value("rating", 0).toInt();
2205 if (newGroupValue != groupValue) {
2206 groupValue = newGroupValue;
2207 groups.append(QPair<int, QVariant>(i, newGroupValue));
2208 }
2209 }
2210
2211 return groups;
2212 }
2213
2214 QList<QPair<int, QVariant> > KFileItemModel::genericStringRoleGroups(const QByteArray& role) const
2215 {
2216 Q_ASSERT(!m_itemData.isEmpty());
2217
2218 const int maxIndex = count() - 1;
2219 QList<QPair<int, QVariant> > groups;
2220
2221 bool isFirstGroupValue = true;
2222 QString groupValue;
2223 for (int i = 0; i <= maxIndex; ++i) {
2224 if (isChildItem(i)) {
2225 continue;
2226 }
2227 const QString newGroupValue = m_itemData.at(i)->values.value(role).toString();
2228 if (newGroupValue != groupValue || isFirstGroupValue) {
2229 groupValue = newGroupValue;
2230 groups.append(QPair<int, QVariant>(i, newGroupValue));
2231 isFirstGroupValue = false;
2232 }
2233 }
2234
2235 return groups;
2236 }
2237
2238 void KFileItemModel::emitSortProgress(int resolvedCount)
2239 {
2240 // Be tolerant against a resolvedCount with a wrong range.
2241 // Although there should not be a case where KFileItemModelRolesUpdater
2242 // (= caller) provides a wrong range, it is important to emit
2243 // a useful progress information even if there is an unexpected
2244 // implementation issue.
2245
2246 const int itemCount = count();
2247 if (resolvedCount >= itemCount) {
2248 m_sortingProgressPercent = -1;
2249 if (m_resortAllItemsTimer->isActive()) {
2250 m_resortAllItemsTimer->stop();
2251 resortAllItems();
2252 }
2253
2254 emit directorySortingProgress(100);
2255 } else if (itemCount > 0) {
2256 resolvedCount = qBound(0, resolvedCount, itemCount);
2257
2258 const int progress = resolvedCount * 100 / itemCount;
2259 if (m_sortingProgressPercent != progress) {
2260 m_sortingProgressPercent = progress;
2261 emit directorySortingProgress(progress);
2262 }
2263 }
2264 }
2265
2266 const KFileItemModel::RoleInfoMap* KFileItemModel::rolesInfoMap(int& count)
2267 {
2268 static const RoleInfoMap rolesInfoMap[] = {
2269 // | role | roleType | role translation | group translation | requires Baloo | requires indexer
2270 { 0, NoRole, 0, 0, 0, 0, false, false },
2271 { "text", NameRole, I18N_NOOP2_NOSTRIP("@label", "Name"), 0, 0, false, false },
2272 { "size", SizeRole, I18N_NOOP2_NOSTRIP("@label", "Size"), 0, 0, false, false },
2273 { "modificationtime", ModificationTimeRole, I18N_NOOP2_NOSTRIP("@label", "Modified"), 0, 0, false, false },
2274 { "accesstime", AccessTimeRole, I18N_NOOP2_NOSTRIP("@label", "Accessed"), 0, 0, false, false },
2275 { "type", TypeRole, I18N_NOOP2_NOSTRIP("@label", "Type"), 0, 0, false, false },
2276 { "rating", RatingRole, I18N_NOOP2_NOSTRIP("@label", "Rating"), 0, 0, true, false },
2277 { "tags", TagsRole, I18N_NOOP2_NOSTRIP("@label", "Tags"), 0, 0, true, false },
2278 { "comment", CommentRole, I18N_NOOP2_NOSTRIP("@label", "Comment"), 0, 0, true, false },
2279 { "title", TitleRole, I18N_NOOP2_NOSTRIP("@label", "Title"), I18N_NOOP2_NOSTRIP("@label", "Document"), true, true },
2280 { "wordCount", WordCountRole, I18N_NOOP2_NOSTRIP("@label", "Word Count"), I18N_NOOP2_NOSTRIP("@label", "Document"), true, true },
2281 { "lineCount", LineCountRole, I18N_NOOP2_NOSTRIP("@label", "Line Count"), I18N_NOOP2_NOSTRIP("@label", "Document"), true, true },
2282 { "imageSize", ImageSizeRole, I18N_NOOP2_NOSTRIP("@label", "Image Size"), I18N_NOOP2_NOSTRIP("@label", "Image"), true, true },
2283 { "orientation", OrientationRole, I18N_NOOP2_NOSTRIP("@label", "Orientation"), I18N_NOOP2_NOSTRIP("@label", "Image"), true, true },
2284 { "artist", ArtistRole, I18N_NOOP2_NOSTRIP("@label", "Artist"), I18N_NOOP2_NOSTRIP("@label", "Audio"), true, true },
2285 { "album", AlbumRole, I18N_NOOP2_NOSTRIP("@label", "Album"), I18N_NOOP2_NOSTRIP("@label", "Audio"), true, true },
2286 { "duration", DurationRole, I18N_NOOP2_NOSTRIP("@label", "Duration"), I18N_NOOP2_NOSTRIP("@label", "Audio"), true, true },
2287 { "track", TrackRole, I18N_NOOP2_NOSTRIP("@label", "Track"), I18N_NOOP2_NOSTRIP("@label", "Audio"), true, true },
2288 { "path", PathRole, I18N_NOOP2_NOSTRIP("@label", "Path"), I18N_NOOP2_NOSTRIP("@label", "Other"), false, false },
2289 { "deletiontime",DeletionTimeRole,I18N_NOOP2_NOSTRIP("@label", "Deletion Time"), I18N_NOOP2_NOSTRIP("@label", "Other"), false, false },
2290 { "destination", DestinationRole, I18N_NOOP2_NOSTRIP("@label", "Link Destination"), I18N_NOOP2_NOSTRIP("@label", "Other"), false, false },
2291 { "originUrl", OriginUrlRole, I18N_NOOP2_NOSTRIP("@label", "Downloaded From"), I18N_NOOP2_NOSTRIP("@label", "Other"), true, false },
2292 { "permissions", PermissionsRole, I18N_NOOP2_NOSTRIP("@label", "Permissions"), I18N_NOOP2_NOSTRIP("@label", "Other"), false, false },
2293 { "owner", OwnerRole, I18N_NOOP2_NOSTRIP("@label", "Owner"), I18N_NOOP2_NOSTRIP("@label", "Other"), false, false },
2294 { "group", GroupRole, I18N_NOOP2_NOSTRIP("@label", "User Group"), I18N_NOOP2_NOSTRIP("@label", "Other"), false, false },
2295 };
2296
2297 count = sizeof(rolesInfoMap) / sizeof(RoleInfoMap);
2298 return rolesInfoMap;
2299 }
2300
2301 void KFileItemModel::determineMimeTypes(const KFileItemList& items, int timeout)
2302 {
2303 QElapsedTimer timer;
2304 timer.start();
2305 foreach (const KFileItem& item, items) { // krazy:exclude=foreach
2306 // Only determine mime types for files here. For directories,
2307 // KFileItem::determineMimeType() reads the .directory file inside to
2308 // load the icon, but this is not necessary at all if we just need the
2309 // type. Some special code for setting the correct mime type for
2310 // directories is in retrieveData().
2311 if (!item.isDir()) {
2312 item.determineMimeType();
2313 }
2314
2315 if (timer.elapsed() > timeout) {
2316 // Don't block the user interface, let the remaining items
2317 // be resolved asynchronously.
2318 return;
2319 }
2320 }
2321 }
2322
2323 QByteArray KFileItemModel::sharedValue(const QByteArray& value)
2324 {
2325 static QSet<QByteArray> pool;
2326 const QSet<QByteArray>::const_iterator it = pool.constFind(value);
2327
2328 if (it != pool.constEnd()) {
2329 return *it;
2330 } else {
2331 pool.insert(value);
2332 return value;
2333 }
2334 }
2335
2336 bool KFileItemModel::isConsistent() const
2337 {
2338 // m_items may contain less items than m_itemData because m_items
2339 // is populated lazily, see KFileItemModel::index(const QUrl& url).
2340 if (m_items.count() > m_itemData.count()) {
2341 return false;
2342 }
2343
2344 for (int i = 0; i < count(); ++i) {
2345 // Check if m_items and m_itemData are consistent.
2346 const KFileItem item = fileItem(i);
2347 if (item.isNull()) {
2348 qCWarning(DolphinDebug) << "Item" << i << "is null";
2349 return false;
2350 }
2351
2352 const int itemIndex = index(item);
2353 if (itemIndex != i) {
2354 qCWarning(DolphinDebug) << "Item" << i << "has a wrong index:" << itemIndex;
2355 return false;
2356 }
2357
2358 // Check if the items are sorted correctly.
2359 if (i > 0 && !lessThan(m_itemData.at(i - 1), m_itemData.at(i), m_collator)) {
2360 qCWarning(DolphinDebug) << "The order of items" << i - 1 << "and" << i << "is wrong:"
2361 << fileItem(i - 1) << fileItem(i);
2362 return false;
2363 }
2364
2365 // Check if all parent-child relationships are consistent.
2366 const ItemData* data = m_itemData.at(i);
2367 const ItemData* parent = data->parent;
2368 if (parent) {
2369 if (expandedParentsCount(data) != expandedParentsCount(parent) + 1) {
2370 qCWarning(DolphinDebug) << "expandedParentsCount is inconsistent for parent" << parent->item << "and child" << data->item;
2371 return false;
2372 }
2373
2374 const int parentIndex = index(parent->item);
2375 if (parentIndex >= i) {
2376 qCWarning(DolphinDebug) << "Index" << parentIndex << "of parent" << parent->item << "is not smaller than index" << i << "of child" << data->item;
2377 return false;
2378 }
2379 }
2380 }
2381
2382 return true;
2383 }