]> cloud.milkyroute.net Git - dolphin.git/blob - src/kitemviews/kfileitemmodel.cpp
Merge remote-tracking branch 'fork/work/zakharafoniam/useful-groups'
[dolphin.git] / src / kitemviews / kfileitemmodel.cpp
1 /*
2 * SPDX-FileCopyrightText: 2011 Peter Penz <peter.penz19@gmail.com>
3 * SPDX-FileCopyrightText: 2013 Frank Reininghaus <frank78ac@googlemail.com>
4 * SPDX-FileCopyrightText: 2013 Emmanuel Pescosta <emmanuelpescosta099@gmail.com>
5 *
6 * SPDX-License-Identifier: GPL-2.0-or-later
7 */
8
9 #include "kfileitemmodel.h"
10
11 #include "dolphin_contentdisplaysettings.h"
12 #include "dolphin_generalsettings.h"
13 #include "dolphindebug.h"
14 #include "private/kfileitemmodelsortalgorithm.h"
15 #include "views/draganddrophelper.h"
16
17 #include <KDirLister>
18 #include <KIO/Job>
19 #include <KIO/ListJob>
20 #include <KLocalizedString>
21 #include <KUrlMimeData>
22
23 #ifndef QT_NO_ACCESSIBILITY
24 #include <QAccessible>
25 #endif
26 #include <QElapsedTimer>
27 #include <QIcon>
28 #include <QMimeData>
29 #include <QMimeDatabase>
30 #include <QRecursiveMutex>
31 #include <QTimer>
32 #include <QWidget>
33 #include <klazylocalizedstring.h>
34
35 Q_GLOBAL_STATIC(QRecursiveMutex, s_collatorMutex)
36
37 // #define KFILEITEMMODEL_DEBUG
38
39 KFileItemModel::KFileItemModel(QObject *parent)
40 : KItemModelBase("text", "none", parent)
41 , m_dirLister(nullptr)
42 , m_sortDirsFirst(true)
43 , m_sortHiddenLast(false)
44 , m_sortRole(NameRole)
45 , m_groupRole(NoRole)
46 , m_sortingProgressPercent(-1)
47 , m_roles()
48 , m_itemData()
49 , m_items()
50 , m_filter()
51 , m_filteredItems()
52 , m_requestRole()
53 , m_maximumUpdateIntervalTimer(nullptr)
54 , m_resortAllItemsTimer(nullptr)
55 , m_pendingItemsToInsert()
56 , m_groups()
57 , m_expandedDirs()
58 , m_urlsToExpand()
59 {
60 m_collator.setNumericMode(true);
61
62 loadSortingSettings();
63
64 m_dirLister = new KDirLister(this);
65 m_dirLister->setAutoErrorHandlingEnabled(false);
66 m_dirLister->setDelayedMimeTypes(true);
67
68 const QWidget *parentWidget = qobject_cast<QWidget *>(parent);
69 if (parentWidget) {
70 m_dirLister->setMainWindow(parentWidget->window());
71 }
72
73 connect(m_dirLister, &KCoreDirLister::started, this, &KFileItemModel::directoryLoadingStarted);
74 connect(m_dirLister, &KCoreDirLister::canceled, this, &KFileItemModel::slotCanceled);
75 connect(m_dirLister, &KCoreDirLister::itemsAdded, this, &KFileItemModel::slotItemsAdded);
76 connect(m_dirLister, &KCoreDirLister::itemsDeleted, this, &KFileItemModel::slotItemsDeleted);
77 connect(m_dirLister, &KCoreDirLister::refreshItems, this, &KFileItemModel::slotRefreshItems);
78 connect(m_dirLister, &KCoreDirLister::clear, this, &KFileItemModel::slotClear);
79 connect(m_dirLister, &KCoreDirLister::infoMessage, this, &KFileItemModel::infoMessage);
80 connect(m_dirLister, &KCoreDirLister::jobError, this, &KFileItemModel::slotListerError);
81 connect(m_dirLister, &KCoreDirLister::percent, this, &KFileItemModel::directoryLoadingProgress);
82 connect(m_dirLister, &KCoreDirLister::redirection, this, &KFileItemModel::directoryRedirection);
83 connect(m_dirLister, &KCoreDirLister::listingDirCompleted, this, &KFileItemModel::slotCompleted);
84
85 // Apply default roles that should be determined
86 resetRoles();
87 m_requestRole[NameRole] = true;
88 m_requestRole[IsDirRole] = true;
89 m_requestRole[IsLinkRole] = true;
90 m_roles.insert("text");
91 m_roles.insert("isDir");
92 m_roles.insert("isLink");
93 m_roles.insert("isHidden");
94
95 // For slow KIO-slaves like used for searching it makes sense to show results periodically even
96 // before the completed() or canceled() signal has been emitted.
97 m_maximumUpdateIntervalTimer = new QTimer(this);
98 m_maximumUpdateIntervalTimer->setInterval(2000);
99 m_maximumUpdateIntervalTimer->setSingleShot(true);
100 connect(m_maximumUpdateIntervalTimer, &QTimer::timeout, this, &KFileItemModel::dispatchPendingItemsToInsert);
101
102 // When changing the value of an item which represents the sort-role a resorting must be
103 // triggered. Especially in combination with KFileItemModelRolesUpdater this might be done
104 // for a lot of items within a quite small timeslot. To prevent expensive resortings the
105 // resorting is postponed until the timer has been exceeded.
106 m_resortAllItemsTimer = new QTimer(this);
107 m_resortAllItemsTimer->setInterval(100); // 100 is a middle ground between sorting too frequently which makes the view unreadable
108 // and sorting too infrequently which leads to users seeing an outdated sort order.
109 m_resortAllItemsTimer->setSingleShot(true);
110 connect(m_resortAllItemsTimer, &QTimer::timeout, this, &KFileItemModel::resortAllItems);
111
112 connect(GeneralSettings::self(), &GeneralSettings::sortingChoiceChanged, this, &KFileItemModel::slotSortingChoiceChanged);
113
114 setShowTrashMime(m_dirLister->showHiddenFiles() || !GeneralSettings::hideXTrashFile());
115 }
116
117 KFileItemModel::~KFileItemModel()
118 {
119 qDeleteAll(m_itemData);
120 qDeleteAll(m_filteredItems);
121 qDeleteAll(m_pendingItemsToInsert);
122 }
123
124 void KFileItemModel::loadDirectory(const QUrl &url)
125 {
126 m_dirLister->openUrl(url);
127 }
128
129 void KFileItemModel::refreshDirectory(const QUrl &url)
130 {
131 // Refresh all expanded directories first (Bug 295300)
132 QHashIterator<QUrl, QUrl> expandedDirs(m_expandedDirs);
133 while (expandedDirs.hasNext()) {
134 expandedDirs.next();
135 m_dirLister->openUrl(expandedDirs.value(), KDirLister::Reload);
136 }
137
138 m_dirLister->openUrl(url, KDirLister::Reload);
139
140 Q_EMIT directoryRefreshing();
141 }
142
143 QUrl KFileItemModel::directory() const
144 {
145 return m_dirLister->url();
146 }
147
148 void KFileItemModel::cancelDirectoryLoading()
149 {
150 m_dirLister->stop();
151 }
152
153 int KFileItemModel::count() const
154 {
155 return m_itemData.count();
156 }
157
158 QHash<QByteArray, QVariant> KFileItemModel::data(int index) const
159 {
160 if (index >= 0 && index < count()) {
161 ItemData *data = m_itemData.at(index);
162 if (data->values.isEmpty()) {
163 data->values = retrieveData(data->item, data->parent);
164 } else if (data->values.count() <= 2 && data->values.value("isExpanded").toBool()) {
165 // Special case dealt by slotRefreshItems(), avoid losing the "isExpanded" and "expandedParentsCount" state when refreshing
166 // slotRefreshItems() makes sure folders keep the "isExpanded" and "expandedParentsCount" while clearing the remaining values
167 // so this special request of different behavior can be identified here.
168 bool hasExpandedParentsCount = false;
169 const int expandedParentsCount = data->values.value("expandedParentsCount").toInt(&hasExpandedParentsCount);
170
171 data->values = retrieveData(data->item, data->parent);
172 data->values.insert("isExpanded", true);
173 if (hasExpandedParentsCount) {
174 data->values.insert("expandedParentsCount", expandedParentsCount);
175 }
176 }
177
178 return data->values;
179 }
180 return QHash<QByteArray, QVariant>();
181 }
182
183 bool KFileItemModel::setData(int index, const QHash<QByteArray, QVariant> &values)
184 {
185 if (index < 0 || index >= count()) {
186 return false;
187 }
188
189 QHash<QByteArray, QVariant> currentValues = data(index);
190
191 // Determine which roles have been changed
192 QSet<QByteArray> changedRoles;
193 QHashIterator<QByteArray, QVariant> it(values);
194 while (it.hasNext()) {
195 it.next();
196 const QByteArray role = sharedValue(it.key());
197 const QVariant value = it.value();
198
199 if (currentValues[role] != value) {
200 currentValues[role] = value;
201 changedRoles.insert(role);
202 }
203 }
204
205 if (changedRoles.isEmpty()) {
206 return false;
207 }
208
209 if (changedRoles.contains("text")) {
210 QUrl url = m_itemData[index]->item.url();
211 m_items.remove(url);
212 url = url.adjusted(QUrl::RemoveFilename);
213 url.setPath(url.path() + currentValues["text"].toString());
214 m_itemData[index]->item.setUrl(url);
215 m_items.insert(url, index);
216
217 if (!changedRoles.contains("url")) {
218 changedRoles.insert("url");
219 currentValues["url"] = url;
220 }
221 }
222 m_itemData[index]->values = currentValues;
223
224 emitItemsChangedAndTriggerResorting(KItemRangeList() << KItemRange(index, 1), changedRoles);
225
226 return true;
227 }
228
229 void KFileItemModel::setSortDirectoriesFirst(bool dirsFirst)
230 {
231 if (dirsFirst != m_sortDirsFirst) {
232 m_sortDirsFirst = dirsFirst;
233 resortAllItems();
234 }
235 }
236
237 bool KFileItemModel::sortDirectoriesFirst() const
238 {
239 return m_sortDirsFirst;
240 }
241
242 void KFileItemModel::setSortHiddenLast(bool hiddenLast)
243 {
244 if (hiddenLast != m_sortHiddenLast) {
245 m_sortHiddenLast = hiddenLast;
246 resortAllItems();
247 }
248 }
249
250 bool KFileItemModel::sortHiddenLast() const
251 {
252 return m_sortHiddenLast;
253 }
254
255 void KFileItemModel::setShowTrashMime(bool showTrashMime)
256 {
257 const auto trashMime = QStringLiteral("application/x-trash");
258 QStringList excludeFilter = m_filter.excludeMimeTypes();
259
260 if (showTrashMime) {
261 excludeFilter.removeAll(trashMime);
262 } else if (!excludeFilter.contains(trashMime)) {
263 excludeFilter.append(trashMime);
264 }
265
266 setExcludeMimeTypeFilter(excludeFilter);
267 }
268
269 void KFileItemModel::scheduleResortAllItems()
270 {
271 if (!m_resortAllItemsTimer->isActive()) {
272 m_resortAllItemsTimer->start();
273 }
274 }
275
276 void KFileItemModel::setShowHiddenFiles(bool show)
277 {
278 m_dirLister->setShowHiddenFiles(show);
279 setShowTrashMime(show || !GeneralSettings::hideXTrashFile());
280 m_dirLister->emitChanges();
281 if (show) {
282 dispatchPendingItemsToInsert();
283 }
284 }
285
286 bool KFileItemModel::showHiddenFiles() const
287 {
288 return m_dirLister->showHiddenFiles();
289 }
290
291 void KFileItemModel::setShowDirectoriesOnly(bool enabled)
292 {
293 m_dirLister->setDirOnlyMode(enabled);
294 }
295
296 bool KFileItemModel::showDirectoriesOnly() const
297 {
298 return m_dirLister->dirOnlyMode();
299 }
300
301 QMimeData *KFileItemModel::createMimeData(const KItemSet &indexes) const
302 {
303 QMimeData *data = new QMimeData();
304
305 // The following code has been taken from KDirModel::mimeData()
306 // (kdelibs/kio/kio/kdirmodel.cpp)
307 // SPDX-FileCopyrightText: 2006 David Faure <faure@kde.org>
308 QList<QUrl> urls;
309 QList<QUrl> mostLocalUrls;
310 const ItemData *lastAddedItem = nullptr;
311
312 for (int index : indexes) {
313 const ItemData *itemData = m_itemData.at(index);
314 const ItemData *parent = itemData->parent;
315
316 while (parent && parent != lastAddedItem) {
317 parent = parent->parent;
318 }
319
320 if (parent && parent == lastAddedItem) {
321 // A parent of 'itemData' has been added already.
322 continue;
323 }
324
325 lastAddedItem = itemData;
326 const KFileItem &item = itemData->item;
327 if (!item.isNull()) {
328 urls << item.url();
329
330 bool isLocal;
331 mostLocalUrls << item.mostLocalUrl(&isLocal);
332 }
333 }
334
335 KUrlMimeData::setUrls(urls, mostLocalUrls, data);
336 return data;
337 }
338
339 namespace
340 {
341 QString removeMarks(const QString &original)
342 {
343 const auto normalized = original.normalized(QString::NormalizationForm_D);
344 QString res;
345 for (auto ch : normalized) {
346 if (!ch.isMark()) {
347 res.append(ch);
348 }
349 }
350 return res;
351 }
352 }
353
354 int KFileItemModel::indexForKeyboardSearch(const QString &text, int startFromIndex) const
355 {
356 const auto noMarkText = removeMarks(text);
357 startFromIndex = qMax(0, startFromIndex);
358 for (int i = startFromIndex; i < count(); ++i) {
359 if (removeMarks(fileItem(i).text()).startsWith(noMarkText, Qt::CaseInsensitive)) {
360 return i;
361 }
362 }
363 for (int i = 0; i < startFromIndex; ++i) {
364 if (removeMarks(fileItem(i).text()).startsWith(noMarkText, Qt::CaseInsensitive)) {
365 return i;
366 }
367 }
368 return -1;
369 }
370
371 bool KFileItemModel::supportsDropping(int index) const
372 {
373 KFileItem item;
374 if (index == -1) {
375 item = rootItem();
376 } else {
377 item = fileItem(index);
378 }
379 return !item.isNull() && DragAndDropHelper::supportsDropping(item);
380 }
381
382 bool KFileItemModel::canEnterOnHover(int index) const
383 {
384 KFileItem item;
385 if (index == -1) {
386 item = rootItem();
387 } else {
388 item = fileItem(index);
389 }
390 return !item.isNull() && (item.isDir() || item.isDesktopFile());
391 }
392
393 QString KFileItemModel::roleDescription(const QByteArray &role) const
394 {
395 static QHash<QByteArray, QString> description;
396 if (description.isEmpty()) {
397 int count = 0;
398 const RoleInfoMap *map = rolesInfoMap(count);
399 for (int i = 0; i < count; ++i) {
400 if (map[i].roleTranslation.isEmpty()) {
401 continue;
402 }
403 description.insert(map[i].role, map[i].roleTranslation.toString());
404 }
405 }
406
407 return description.value(role);
408 }
409
410 QList<QPair<int, QVariant>> KFileItemModel::groups() const
411 {
412 if (!m_itemData.isEmpty() && m_groups.isEmpty()) {
413 #ifdef KFILEITEMMODEL_DEBUG
414 QElapsedTimer timer;
415 timer.start();
416 #endif
417 QByteArray role = groupRole();
418 if (typeForRole(role) == NoRole) {
419 // Handle extra grouping information
420 if (m_groupExtraInfo == "followSort") {
421 role = sortRole();
422 }
423 }
424 switch (typeForRole(role)) {
425 case NoRole:
426 m_groups.clear();
427 break;
428 case NameRole:
429 m_groups = nameRoleGroups();
430 break;
431 case SizeRole:
432 m_groups = sizeRoleGroups();
433 break;
434 case ModificationTimeRole:
435 m_groups = timeRoleGroups([](const ItemData *item) {
436 return item->item.time(KFileItem::ModificationTime);
437 });
438 break;
439 case CreationTimeRole:
440 m_groups = timeRoleGroups([](const ItemData *item) {
441 return item->item.time(KFileItem::CreationTime);
442 });
443 break;
444 case AccessTimeRole:
445 m_groups = timeRoleGroups([](const ItemData *item) {
446 return item->item.time(KFileItem::AccessTime);
447 });
448 break;
449 case DeletionTimeRole:
450 m_groups = timeRoleGroups([](const ItemData *item) {
451 return item->values.value("deletiontime").toDateTime();
452 });
453 break;
454 case PermissionsRole:
455 m_groups = permissionRoleGroups();
456 break;
457 case RatingRole:
458 m_groups = ratingRoleGroups();
459 break;
460 default:
461 m_groups = genericStringRoleGroups(role);
462 break;
463 }
464
465 #ifdef KFILEITEMMODEL_DEBUG
466 qCDebug(DolphinDebug) << "[TIME] Calculating groups for" << count() << "items:" << timer.elapsed();
467 #endif
468 }
469
470 return m_groups;
471 }
472
473 KFileItem KFileItemModel::fileItem(int index) const
474 {
475 if (index >= 0 && index < count()) {
476 return m_itemData.at(index)->item;
477 }
478
479 return KFileItem();
480 }
481
482 KFileItem KFileItemModel::fileItem(const QUrl &url) const
483 {
484 const int indexForUrl = index(url);
485 if (indexForUrl >= 0) {
486 return m_itemData.at(indexForUrl)->item;
487 }
488 return KFileItem();
489 }
490
491 int KFileItemModel::index(const KFileItem &item) const
492 {
493 return index(item.url());
494 }
495
496 int KFileItemModel::index(const QUrl &url) const
497 {
498 const QUrl urlToFind = url.adjusted(QUrl::StripTrailingSlash);
499
500 const int itemCount = m_itemData.count();
501 int itemsInHash = m_items.count();
502
503 int index = m_items.value(urlToFind, -1);
504 while (index < 0 && itemsInHash < itemCount) {
505 // Not all URLs are stored yet in m_items. We grow m_items until either
506 // urlToFind is found, or all URLs have been stored in m_items.
507 // Note that we do not add the URLs to m_items one by one, but in
508 // larger blocks. After each block, we check if urlToFind is in
509 // m_items. We could in principle compare urlToFind with each URL while
510 // we are going through m_itemData, but comparing two QUrls will,
511 // unlike calling qHash for the URLs, trigger a parsing of the URLs
512 // which costs both CPU cycles and memory.
513 const int blockSize = 1000;
514 const int currentBlockEnd = qMin(itemsInHash + blockSize, itemCount);
515 for (int i = itemsInHash; i < currentBlockEnd; ++i) {
516 const QUrl nextUrl = m_itemData.at(i)->item.url();
517 m_items.insert(nextUrl, i);
518 }
519
520 itemsInHash = currentBlockEnd;
521 index = m_items.value(urlToFind, -1);
522 }
523
524 if (index < 0) {
525 // The item could not be found, even though all items from m_itemData
526 // should be in m_items now. We print some diagnostic information which
527 // might help to find the cause of the problem, but only once. This
528 // prevents that obtaining and printing the debugging information
529 // wastes CPU cycles and floods the shell or .xsession-errors.
530 static bool printDebugInfo = true;
531
532 if (m_items.count() != m_itemData.count() && printDebugInfo) {
533 printDebugInfo = false;
534
535 qCWarning(DolphinDebug) << "The model is in an inconsistent state.";
536 qCWarning(DolphinDebug) << "m_items.count() ==" << m_items.count();
537 qCWarning(DolphinDebug) << "m_itemData.count() ==" << m_itemData.count();
538
539 // Check if there are multiple items with the same URL.
540 QMultiHash<QUrl, int> indexesForUrl;
541 for (int i = 0; i < m_itemData.count(); ++i) {
542 indexesForUrl.insert(m_itemData.at(i)->item.url(), i);
543 }
544
545 const auto uniqueKeys = indexesForUrl.uniqueKeys();
546 for (const QUrl &url : uniqueKeys) {
547 if (indexesForUrl.count(url) > 1) {
548 qCWarning(DolphinDebug) << "Multiple items found with the URL" << url;
549
550 auto it = indexesForUrl.find(url);
551 while (it != indexesForUrl.end() && it.key() == url) {
552 const ItemData *data = m_itemData.at(it.value());
553 qCWarning(DolphinDebug) << "index" << it.value() << ":" << data->item;
554 if (data->parent) {
555 qCWarning(DolphinDebug) << "parent" << data->parent->item;
556 }
557 ++it;
558 }
559 }
560 }
561 }
562 }
563
564 return index;
565 }
566
567 KFileItem KFileItemModel::rootItem() const
568 {
569 return m_dirLister->rootItem();
570 }
571
572 void KFileItemModel::clear()
573 {
574 slotClear();
575 }
576
577 void KFileItemModel::setRoles(const QSet<QByteArray> &roles)
578 {
579 if (m_roles == roles) {
580 return;
581 }
582
583 const QSet<QByteArray> changedRoles = (roles - m_roles) + (m_roles - roles);
584 m_roles = roles;
585
586 if (count() > 0) {
587 const bool supportedExpanding = m_requestRole[ExpandedParentsCountRole];
588 const bool willSupportExpanding = roles.contains("expandedParentsCount");
589 if (supportedExpanding && !willSupportExpanding) {
590 // No expanding is supported anymore. Take care to delete all items that have an expansion level
591 // that is not 0 (and hence are part of an expanded item).
592 removeExpandedItems();
593 }
594 }
595
596 m_groups.clear();
597 resetRoles();
598
599 QSetIterator<QByteArray> it(roles);
600 while (it.hasNext()) {
601 const QByteArray &role = it.next();
602 m_requestRole[typeForRole(role)] = true;
603 }
604
605 if (count() > 0) {
606 // Update m_data with the changed requested roles
607 const int maxIndex = count() - 1;
608 for (int i = 0; i <= maxIndex; ++i) {
609 m_itemData[i]->values = retrieveData(m_itemData.at(i)->item, m_itemData.at(i)->parent);
610 }
611
612 Q_EMIT itemsChanged(KItemRangeList() << KItemRange(0, count()), changedRoles);
613 }
614
615 // Clear the 'values' of all filtered items. They will be re-populated with the
616 // correct roles the next time 'values' will be accessed via data(int).
617 QHash<KFileItem, ItemData *>::iterator filteredIt = m_filteredItems.begin();
618 const QHash<KFileItem, ItemData *>::iterator filteredEnd = m_filteredItems.end();
619 while (filteredIt != filteredEnd) {
620 (*filteredIt)->values.clear();
621 ++filteredIt;
622 }
623 }
624
625 QSet<QByteArray> KFileItemModel::roles() const
626 {
627 return m_roles;
628 }
629
630 bool KFileItemModel::setExpanded(int index, bool expanded)
631 {
632 if (!isExpandable(index) || isExpanded(index) == expanded) {
633 return false;
634 }
635
636 QHash<QByteArray, QVariant> values;
637 values.insert(sharedValue("isExpanded"), expanded);
638 if (!setData(index, values)) {
639 return false;
640 }
641
642 const KFileItem item = m_itemData.at(index)->item;
643 const QUrl url = item.url();
644 const QUrl targetUrl = item.targetUrl();
645 if (expanded) {
646 m_expandedDirs.insert(targetUrl, url);
647 m_dirLister->openUrl(url, KDirLister::Keep);
648
649 const QVariantList previouslyExpandedChildren = m_itemData.at(index)->values.value("previouslyExpandedChildren").value<QVariantList>();
650 for (const QVariant &var : previouslyExpandedChildren) {
651 m_urlsToExpand.insert(var.toUrl());
652 }
653 } else {
654 // Note that there might be (indirect) children of the folder which is to be collapsed in
655 // m_pendingItemsToInsert. To prevent that they will be inserted into the model later,
656 // possibly without a parent, which might result in a crash, we insert all pending items
657 // right now. All new items which would be without a parent will then be removed.
658 dispatchPendingItemsToInsert();
659
660 // Check if the index of the collapsed folder has changed. If that is the case, then items
661 // were inserted before the collapsed folder, and its index needs to be updated.
662 if (m_itemData.at(index)->item != item) {
663 index = this->index(item);
664 }
665
666 m_expandedDirs.remove(targetUrl);
667 m_dirLister->stop(url);
668 m_dirLister->forgetDirs(url);
669
670 const int parentLevel = expandedParentsCount(index);
671 const int itemCount = m_itemData.count();
672 const int firstChildIndex = index + 1;
673
674 QVariantList expandedChildren;
675
676 int childIndex = firstChildIndex;
677 while (childIndex < itemCount && expandedParentsCount(childIndex) > parentLevel) {
678 ItemData *itemData = m_itemData.at(childIndex);
679 if (itemData->values.value("isExpanded").toBool()) {
680 const QUrl targetUrl = itemData->item.targetUrl();
681 const QUrl url = itemData->item.url();
682 m_expandedDirs.remove(targetUrl);
683 m_dirLister->stop(url); // TODO: try to unit-test this, see https://bugs.kde.org/show_bug.cgi?id=332102#c11
684 m_dirLister->forgetDirs(url);
685 expandedChildren.append(targetUrl);
686 }
687 ++childIndex;
688 }
689 const int childrenCount = childIndex - firstChildIndex;
690
691 removeFilteredChildren(KItemRangeList() << KItemRange(index, 1 + childrenCount));
692 removeItems(KItemRangeList() << KItemRange(firstChildIndex, childrenCount), DeleteItemData);
693
694 m_itemData.at(index)->values.insert("previouslyExpandedChildren", expandedChildren);
695 }
696
697 return true;
698 }
699
700 bool KFileItemModel::isExpanded(int index) const
701 {
702 if (index >= 0 && index < count()) {
703 return m_itemData.at(index)->values.value("isExpanded").toBool();
704 }
705 return false;
706 }
707
708 bool KFileItemModel::isExpandable(int index) const
709 {
710 if (index >= 0 && index < count()) {
711 // Call data (instead of accessing m_itemData directly)
712 // to ensure that the value is initialized.
713 return data(index).value("isExpandable").toBool();
714 }
715 return false;
716 }
717
718 int KFileItemModel::expandedParentsCount(int index) const
719 {
720 if (index >= 0 && index < count()) {
721 return expandedParentsCount(m_itemData.at(index));
722 }
723 return 0;
724 }
725
726 QSet<QUrl> KFileItemModel::expandedDirectories() const
727 {
728 QSet<QUrl> result;
729 const auto dirs = m_expandedDirs;
730 for (const auto &dir : dirs) {
731 result.insert(dir);
732 }
733 return result;
734 }
735
736 void KFileItemModel::restoreExpandedDirectories(const QSet<QUrl> &urls)
737 {
738 m_urlsToExpand = urls;
739 }
740
741 void KFileItemModel::expandParentDirectories(const QUrl &url)
742 {
743 // Assure that each sub-path of the URL that should be
744 // expanded is added to m_urlsToExpand. KDirLister
745 // does not care whether the parent-URL has already been
746 // expanded.
747 QUrl urlToExpand = m_dirLister->url();
748 const int pos = urlToExpand.path().length();
749
750 // first subdir can be empty, if m_dirLister->url().path() does not end with '/'
751 // this happens if baseUrl is not root but a home directory, see FoldersPanel,
752 // so using QString::SkipEmptyParts
753 const QStringList subDirs = url.path().mid(pos).split(QDir::separator(), Qt::SkipEmptyParts);
754 for (int i = 0; i < subDirs.count() - 1; ++i) {
755 QString path = urlToExpand.path();
756 if (!path.endsWith(QLatin1Char('/'))) {
757 path.append(QLatin1Char('/'));
758 }
759 urlToExpand.setPath(path + subDirs.at(i));
760 m_urlsToExpand.insert(urlToExpand);
761 }
762
763 // KDirLister::open() must called at least once to trigger an initial
764 // loading. The pending URLs that must be restored are handled
765 // in slotCompleted().
766 QSetIterator<QUrl> it2(m_urlsToExpand);
767 while (it2.hasNext()) {
768 const int idx = index(it2.next());
769 if (idx >= 0 && !isExpanded(idx)) {
770 setExpanded(idx, true);
771 break;
772 }
773 }
774 }
775
776 void KFileItemModel::setNameFilter(const QString &nameFilter)
777 {
778 if (m_filter.pattern() != nameFilter) {
779 dispatchPendingItemsToInsert();
780 m_filter.setPattern(nameFilter);
781 applyFilters();
782 }
783 }
784
785 QString KFileItemModel::nameFilter() const
786 {
787 return m_filter.pattern();
788 }
789
790 void KFileItemModel::setMimeTypeFilters(const QStringList &filters)
791 {
792 if (m_filter.mimeTypes() != filters) {
793 dispatchPendingItemsToInsert();
794 m_filter.setMimeTypes(filters);
795 applyFilters();
796 }
797 }
798
799 QStringList KFileItemModel::mimeTypeFilters() const
800 {
801 return m_filter.mimeTypes();
802 }
803
804 void KFileItemModel::setExcludeMimeTypeFilter(const QStringList &filters)
805 {
806 if (m_filter.excludeMimeTypes() != filters) {
807 dispatchPendingItemsToInsert();
808 m_filter.setExcludeMimeTypes(filters);
809 applyFilters();
810 }
811 }
812
813 QStringList KFileItemModel::excludeMimeTypeFilter() const
814 {
815 return m_filter.excludeMimeTypes();
816 }
817
818 void KFileItemModel::applyFilters()
819 {
820 // ===STEP 1===
821 // Check which previously shown items from m_itemData must now get
822 // hidden and hence moved from m_itemData into m_filteredItems.
823
824 QList<int> newFilteredIndexes; // This structure is good for prepending. We will want an ascending sorted Container at the end, this will do fine.
825
826 // This pointer will refer to the next confirmed shown item from the point of
827 // view of the current "itemData" in the upcoming "for" loop.
828 ItemData *itemShownBelow = nullptr;
829
830 // We will iterate backwards because it's convenient to know beforehand if the item just below is its child or not.
831 for (int index = m_itemData.count() - 1; index >= 0; --index) {
832 ItemData *itemData = m_itemData.at(index);
833
834 if (m_filter.matches(itemData->item) || (itemShownBelow && itemShownBelow->parent == itemData)) {
835 // We could've entered here for two reasons:
836 // 1. This item passes the filter itself
837 // 2. This is an expanded folder that doesn't pass the filter but sees a filter-passing child just below
838
839 // So this item must remain shown.
840 // Lets register this item as the next shown item from the point of view of the next iteration of this for loop
841 itemShownBelow = itemData;
842 } else {
843 // We hide this item for now, however, for expanded folders this is not final:
844 // if after the next "for" loop we discover that its children must now be shown with the newly applied fliter, we shall re-insert it
845 newFilteredIndexes.prepend(index);
846 m_filteredItems.insert(itemData->item, itemData);
847 // indexShownBelow doesn't get updated since this item will be hidden
848 }
849 }
850
851 // This will remove the newly filtered items from m_itemData
852 removeItems(KItemRangeList::fromSortedContainer(newFilteredIndexes), KeepItemData);
853
854 // ===STEP 2===
855 // Check which hidden items from m_filteredItems should
856 // become visible again and hence moved from m_filteredItems back into m_itemData.
857
858 QList<ItemData *> newVisibleItems;
859
860 QHash<KFileItem, ItemData *> ancestorsOfNewVisibleItems; // We will make sure these also become visible in step 3.
861
862 QHash<KFileItem, ItemData *>::iterator it = m_filteredItems.begin();
863 while (it != m_filteredItems.end()) {
864 if (m_filter.matches(it.key())) {
865 newVisibleItems.append(it.value());
866
867 // If this is a child of an expanded folder, we must make sure that its whole parental chain will also be shown.
868 // We will go up through its parental chain until we either:
869 // 1 - reach the "root item" of the current view, i.e the currently opened folder on Dolphin. Their children have their ItemData::parent set to
870 // nullptr. or 2 - we reach an unfiltered parent or a previously discovered ancestor.
871 for (ItemData *parent = it.value()->parent; parent && !ancestorsOfNewVisibleItems.contains(parent->item) && m_filteredItems.contains(parent->item);
872 parent = parent->parent) {
873 // We wish we could remove this parent from m_filteredItems right now, but we are iterating over it
874 // and it would mess up the iteration. We will mark it to be removed in step 3.
875 ancestorsOfNewVisibleItems.insert(parent->item, parent);
876 }
877
878 it = m_filteredItems.erase(it);
879 } else {
880 // Item remains filtered for now
881 // However, for expanded folders this is not final, we may discover later that it has unfiltered descendants.
882 ++it;
883 }
884 }
885
886 // ===STEP 3===
887 // Handles the ancestorsOfNewVisibleItems.
888 // Now that we are done iterating through m_filteredItems we can safely move the ancestorsOfNewVisibleItems from m_filteredItems to newVisibleItems.
889 for (it = ancestorsOfNewVisibleItems.begin(); it != ancestorsOfNewVisibleItems.end(); it++) {
890 if (m_filteredItems.remove(it.key())) {
891 // m_filteredItems still contained this ancestor until now so we can be sure that we aren't adding a duplicate ancestor to newVisibleItems.
892 newVisibleItems.append(it.value());
893 }
894 }
895
896 // This will insert the newly discovered unfiltered items into m_itemData
897 insertItems(newVisibleItems);
898 }
899
900 void KFileItemModel::removeFilteredChildren(const KItemRangeList &itemRanges)
901 {
902 if (m_filteredItems.isEmpty() || !m_requestRole[ExpandedParentsCountRole]) {
903 // There are either no filtered items, or it is not possible to expand
904 // folders -> there cannot be any filtered children.
905 return;
906 }
907
908 QSet<ItemData *> parents;
909 for (const KItemRange &range : itemRanges) {
910 for (int index = range.index; index < range.index + range.count; ++index) {
911 parents.insert(m_itemData.at(index));
912 }
913 }
914
915 QHash<KFileItem, ItemData *>::iterator it = m_filteredItems.begin();
916 while (it != m_filteredItems.end()) {
917 if (parents.contains(it.value()->parent)) {
918 delete it.value();
919 it = m_filteredItems.erase(it);
920 } else {
921 ++it;
922 }
923 }
924 }
925
926 KFileItemModel::RoleInfo KFileItemModel::roleInformation(const QByteArray &role)
927 {
928 static QHash<QByteArray, RoleInfo> information;
929 if (information.isEmpty()) {
930 int count = 0;
931 const RoleInfoMap *map = rolesInfoMap(count);
932 for (int i = 0; i < count; ++i) {
933 RoleInfo info;
934 info.role = map[i].role;
935 info.translation = map[i].roleTranslation.toString();
936 if (!map[i].groupTranslation.isEmpty()) {
937 info.group = map[i].groupTranslation.toString();
938 } else {
939 // For top level roles, groupTranslation is 0. We must make sure that
940 // info.group is an empty string then because the code that generates
941 // menus tries to put the actions into sub menus otherwise.
942 info.group = QString();
943 }
944 info.requiresBaloo = map[i].requiresBaloo;
945 info.requiresIndexer = map[i].requiresIndexer;
946 if (!map[i].tooltipTranslation.isEmpty()) {
947 info.tooltip = map[i].tooltipTranslation.toString();
948 } else {
949 info.tooltip = QString();
950 }
951
952 information.insert(map[i].role, info);
953 }
954 }
955
956 return information.value(role);
957 }
958
959 QList<KFileItemModel::RoleInfo> KFileItemModel::rolesInformation()
960 {
961 static QList<RoleInfo> rolesInfo;
962 if (rolesInfo.isEmpty()) {
963 int count = 0;
964 const RoleInfoMap *map = rolesInfoMap(count);
965 for (int i = 0; i < count; ++i) {
966 if (map[i].roleType != NoRole) {
967 RoleInfo info = roleInformation(map[i].role);
968 rolesInfo.append(info);
969 }
970 }
971 }
972
973 return rolesInfo;
974 }
975
976 QList<KFileItemModel::RoleInfo> KFileItemModel::extraGroupingInformation()
977 {
978 static QList<RoleInfo> rolesInfo{
979 {QByteArray("none"), kli18nc("@label", "No grouping").toString(), nullptr, nullptr, false, false},
980 {QByteArray("followSort"), kli18nc("@label", "Follow sorting").toString(), nullptr, nullptr, false, false}
981 };
982 return rolesInfo;
983 }
984
985 void KFileItemModel::onGroupedSortingChanged(bool current)
986 {
987 Q_UNUSED(current)
988 m_groups.clear();
989 }
990
991 void KFileItemModel::onSortRoleChanged(const QByteArray &current, const QByteArray &previous, bool resortItems)
992 {
993 Q_UNUSED(previous)
994 m_sortRole = typeForRole(current);
995 if (m_sortRole == NoRole) {
996 // Requested role not in list of roles. This could
997 // be used for indicating non-trivial sorting behavior
998 m_sortExtraInfo = current;
999 } else {
1000 m_sortExtraInfo.clear();
1001 }
1002
1003 if (!m_requestRole[m_sortRole]) {
1004 QSet<QByteArray> newRoles = m_roles;
1005 newRoles << current;
1006 setRoles(newRoles);
1007 }
1008
1009 if (resortItems) {
1010 resortAllItems();
1011 }
1012 }
1013
1014 void KFileItemModel::onSortOrderChanged(Qt::SortOrder current, Qt::SortOrder previous)
1015 {
1016 Q_UNUSED(current)
1017 Q_UNUSED(previous)
1018 resortAllItems();
1019 }
1020
1021 void KFileItemModel::onGroupRoleChanged(const QByteArray &current, const QByteArray &previous, bool resortItems)
1022 {
1023 Q_UNUSED(previous)
1024 m_groupRole = typeForRole(current);
1025 if (m_groupRole == NoRole) {
1026 // Requested role not in list of roles. This could
1027 // be used for indicating non-trivial grouping behavior
1028 m_groupExtraInfo = current;
1029 } else {
1030 m_groupExtraInfo.clear();
1031 }
1032
1033 if (!m_requestRole[m_groupRole]) {
1034 QSet<QByteArray> newRoles = m_roles;
1035 newRoles << current;
1036 setRoles(newRoles);
1037 }
1038
1039 if (resortItems) {
1040 resortAllItems();
1041 }
1042 }
1043
1044 void KFileItemModel::onGroupOrderChanged(Qt::SortOrder current, Qt::SortOrder previous)
1045 {
1046 Q_UNUSED(current)
1047 Q_UNUSED(previous)
1048 resortAllItems();
1049 }
1050
1051 void KFileItemModel::loadSortingSettings()
1052 {
1053 using Choice = GeneralSettings::EnumSortingChoice;
1054 switch (GeneralSettings::sortingChoice()) {
1055 case Choice::NaturalSorting:
1056 m_naturalSorting = true;
1057 m_collator.setCaseSensitivity(Qt::CaseInsensitive);
1058 break;
1059 case Choice::CaseSensitiveSorting:
1060 m_naturalSorting = false;
1061 m_collator.setCaseSensitivity(Qt::CaseSensitive);
1062 break;
1063 case Choice::CaseInsensitiveSorting:
1064 m_naturalSorting = false;
1065 m_collator.setCaseSensitivity(Qt::CaseInsensitive);
1066 break;
1067 default:
1068 Q_UNREACHABLE();
1069 }
1070 // Workaround for bug https://bugreports.qt.io/browse/QTBUG-69361
1071 // Force the clean state of QCollator in single thread to avoid thread safety problems in sort
1072 m_collator.compare(QString(), QString());
1073 ContentDisplaySettings::self();
1074 }
1075
1076 void KFileItemModel::resortAllItems()
1077 {
1078 m_resortAllItemsTimer->stop();
1079
1080 const int itemCount = count();
1081 if (itemCount <= 0) {
1082 return;
1083 }
1084
1085 #ifdef KFILEITEMMODEL_DEBUG
1086 QElapsedTimer timer;
1087 timer.start();
1088 qCDebug(DolphinDebug) << "===========================================================";
1089 qCDebug(DolphinDebug) << "Resorting" << itemCount << "items";
1090 #endif
1091
1092 // Remember the order of the current URLs so
1093 // that it can be determined which indexes have
1094 // been moved because of the resorting.
1095 QList<QUrl> oldUrls;
1096 oldUrls.reserve(itemCount);
1097 for (const ItemData *itemData : std::as_const(m_itemData)) {
1098 oldUrls.append(itemData->item.url());
1099 }
1100
1101 m_items.clear();
1102 m_items.reserve(itemCount);
1103
1104 // Resort the items
1105 sort(m_itemData.begin(), m_itemData.end());
1106 for (int i = 0; i < itemCount; ++i) {
1107 m_items.insert(m_itemData.at(i)->item.url(), i);
1108 }
1109
1110 // Determine the first index that has been moved.
1111 int firstMovedIndex = 0;
1112 while (firstMovedIndex < itemCount && firstMovedIndex == m_items.value(oldUrls.at(firstMovedIndex))) {
1113 ++firstMovedIndex;
1114 }
1115
1116 const bool itemsHaveMoved = firstMovedIndex < itemCount;
1117 if (itemsHaveMoved) {
1118 m_groups.clear();
1119
1120 int lastMovedIndex = itemCount - 1;
1121 while (lastMovedIndex > firstMovedIndex && lastMovedIndex == m_items.value(oldUrls.at(lastMovedIndex))) {
1122 --lastMovedIndex;
1123 }
1124
1125 Q_ASSERT(firstMovedIndex <= lastMovedIndex);
1126
1127 // Create a list movedToIndexes, which has the property that
1128 // movedToIndexes[i] is the new index of the item with the old index
1129 // firstMovedIndex + i.
1130 const int movedItemsCount = lastMovedIndex - firstMovedIndex + 1;
1131 QList<int> movedToIndexes;
1132 movedToIndexes.reserve(movedItemsCount);
1133 for (int i = firstMovedIndex; i <= lastMovedIndex; ++i) {
1134 const int newIndex = m_items.value(oldUrls.at(i));
1135 movedToIndexes.append(newIndex);
1136 }
1137
1138 Q_EMIT itemsMoved(KItemRange(firstMovedIndex, movedItemsCount), movedToIndexes);
1139 }
1140 if (groupedSorting()) {
1141 // The groups might have changed even if the order of the items has not.
1142 const QList<QPair<int, QVariant>> oldGroups = m_groups;
1143 m_groups.clear();
1144 if (groups() != oldGroups) {
1145 Q_EMIT groupsChanged();
1146 }
1147 }
1148
1149 #ifdef KFILEITEMMODEL_DEBUG
1150 qCDebug(DolphinDebug) << "[TIME] Resorting of" << itemCount << "items:" << timer.elapsed();
1151 #endif
1152 }
1153
1154 void KFileItemModel::slotCompleted()
1155 {
1156 m_maximumUpdateIntervalTimer->stop();
1157 dispatchPendingItemsToInsert();
1158
1159 if (!m_urlsToExpand.isEmpty()) {
1160 // Try to find a URL that can be expanded.
1161 // Note that the parent folder must be expanded before any of its subfolders become visible.
1162 // Therefore, some URLs in m_restoredExpandedUrls might not be visible yet
1163 // -> we expand the first visible URL we find in m_restoredExpandedUrls.
1164 // Iterate over a const copy because items are deleted and inserted within the loop
1165 const auto urlsToExpand = m_urlsToExpand;
1166 for (const QUrl &url : urlsToExpand) {
1167 const int indexForUrl = index(url);
1168 if (indexForUrl >= 0) {
1169 m_urlsToExpand.remove(url);
1170 if (setExpanded(indexForUrl, true)) {
1171 // The dir lister has been triggered. This slot will be called
1172 // again after the directory has been expanded.
1173 return;
1174 }
1175 }
1176 }
1177
1178 // None of the URLs in m_restoredExpandedUrls could be found in the model. This can happen
1179 // if these URLs have been deleted in the meantime.
1180 m_urlsToExpand.clear();
1181 }
1182
1183 Q_EMIT directoryLoadingCompleted();
1184 }
1185
1186 void KFileItemModel::slotCanceled()
1187 {
1188 m_maximumUpdateIntervalTimer->stop();
1189 dispatchPendingItemsToInsert();
1190
1191 Q_EMIT directoryLoadingCanceled();
1192 }
1193
1194 void KFileItemModel::slotItemsAdded(const QUrl &directoryUrl, const KFileItemList &items)
1195 {
1196 Q_ASSERT(!items.isEmpty());
1197
1198 const QUrl parentUrl = m_expandedDirs.value(directoryUrl, directoryUrl.adjusted(QUrl::StripTrailingSlash));
1199
1200 if (m_requestRole[ExpandedParentsCountRole]) {
1201 // If the expanding of items is enabled, the call
1202 // dirLister->openUrl(url, KDirLister::Keep) in KFileItemModel::setExpanded()
1203 // might result in emitting the same items twice due to the Keep-parameter.
1204 // This case happens if an item gets expanded, collapsed and expanded again
1205 // before the items could be loaded for the first expansion.
1206 if (index(items.first().url()) >= 0) {
1207 // The items are already part of the model.
1208 return;
1209 }
1210
1211 if (directoryUrl != directory()) {
1212 // To be able to compare whether the new items may be inserted as children
1213 // of a parent item the pending items must be added to the model first.
1214 dispatchPendingItemsToInsert();
1215 }
1216
1217 // KDirLister keeps the children of items that got expanded once even if
1218 // they got collapsed again with KFileItemModel::setExpanded(false). So it must be
1219 // checked whether the parent for new items is still expanded.
1220 const int parentIndex = index(parentUrl);
1221 if (parentIndex >= 0 && !m_itemData[parentIndex]->values.value("isExpanded").toBool()) {
1222 // The parent is not expanded.
1223 return;
1224 }
1225 }
1226
1227 const QList<ItemData *> itemDataList = createItemDataList(parentUrl, items);
1228
1229 if (!m_filter.hasSetFilters()) {
1230 m_pendingItemsToInsert.append(itemDataList);
1231 } else {
1232 QSet<ItemData *> parentsToEnsureVisible;
1233
1234 // The name or type filter is active. Hide filtered items
1235 // before inserting them into the model and remember
1236 // the filtered items in m_filteredItems.
1237 for (ItemData *itemData : itemDataList) {
1238 if (m_filter.matches(itemData->item)) {
1239 m_pendingItemsToInsert.append(itemData);
1240 if (itemData->parent) {
1241 parentsToEnsureVisible.insert(itemData->parent);
1242 }
1243 } else {
1244 m_filteredItems.insert(itemData->item, itemData);
1245 }
1246 }
1247
1248 // Entire parental chains must be shown
1249 for (ItemData *parent : parentsToEnsureVisible) {
1250 for (; parent && m_filteredItems.remove(parent->item); parent = parent->parent) {
1251 m_pendingItemsToInsert.append(parent);
1252 }
1253 }
1254 }
1255
1256 if (!m_maximumUpdateIntervalTimer->isActive()) {
1257 // Assure that items get dispatched if no completed() or canceled() signal is
1258 // emitted during the maximum update interval.
1259 m_maximumUpdateIntervalTimer->start();
1260 }
1261
1262 Q_EMIT fileItemsChanged({KFileItem(directoryUrl)});
1263 }
1264
1265 int KFileItemModel::filterChildlessParents(KItemRangeList &removedItemRanges, const QSet<ItemData *> &parentsToEnsureVisible)
1266 {
1267 int filteredParentsCount = 0;
1268 // The childless parents not yet removed will always be right above the start of a removed range.
1269 // We iterate backwards to ensure the deepest folders are processed before their parents
1270 for (int i = removedItemRanges.size() - 1; i >= 0; i--) {
1271 KItemRange itemRange = removedItemRanges.at(i);
1272 const ItemData *const firstInRange = m_itemData.at(itemRange.index);
1273 ItemData *itemAbove = itemRange.index - 1 >= 0 ? m_itemData.at(itemRange.index - 1) : nullptr;
1274 const ItemData *const itemBelow = itemRange.index + itemRange.count < m_itemData.count() ? m_itemData.at(itemRange.index + itemRange.count) : nullptr;
1275
1276 if (itemAbove && firstInRange->parent == itemAbove && !m_filter.matches(itemAbove->item) && (!itemBelow || itemBelow->parent != itemAbove)
1277 && !parentsToEnsureVisible.contains(itemAbove)) {
1278 // The item above exists, is the parent, doesn't pass the filter, does not belong to parentsToEnsureVisible
1279 // and this deleted range covers all of its descendents, so none will be left.
1280 m_filteredItems.insert(itemAbove->item, itemAbove);
1281 // This range's starting index will be extended to include the parent above:
1282 --itemRange.index;
1283 ++itemRange.count;
1284 ++filteredParentsCount;
1285 KItemRange previousRange = i > 0 ? removedItemRanges.at(i - 1) : KItemRange();
1286 // We must check if this caused the range to touch the previous range, if that's the case they shall be merged
1287 if (i > 0 && previousRange.index + previousRange.count == itemRange.index) {
1288 previousRange.count += itemRange.count;
1289 removedItemRanges.replace(i - 1, previousRange);
1290 removedItemRanges.removeAt(i);
1291 } else {
1292 removedItemRanges.replace(i, itemRange);
1293 // We must revisit this range in the next iteration since its starting index changed
1294 ++i;
1295 }
1296 }
1297 }
1298 return filteredParentsCount;
1299 }
1300
1301 void KFileItemModel::slotItemsDeleted(const KFileItemList &items)
1302 {
1303 dispatchPendingItemsToInsert();
1304
1305 QVector<int> indexesToRemove;
1306 indexesToRemove.reserve(items.count());
1307 KFileItemList dirsChanged;
1308
1309 const auto currentDir = directory();
1310
1311 for (const KFileItem &item : items) {
1312 if (item.url() == currentDir) {
1313 Q_EMIT currentDirectoryRemoved();
1314 return;
1315 }
1316
1317 const int indexForItem = index(item);
1318 if (indexForItem >= 0) {
1319 indexesToRemove.append(indexForItem);
1320 } else {
1321 // Probably the item has been filtered.
1322 QHash<KFileItem, ItemData *>::iterator it = m_filteredItems.find(item);
1323 if (it != m_filteredItems.end()) {
1324 delete it.value();
1325 m_filteredItems.erase(it);
1326 }
1327 }
1328
1329 QUrl parentUrl = item.url().adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash);
1330 if (dirsChanged.findByUrl(parentUrl).isNull()) {
1331 dirsChanged << KFileItem(parentUrl);
1332 }
1333 }
1334
1335 std::sort(indexesToRemove.begin(), indexesToRemove.end());
1336
1337 if (m_requestRole[ExpandedParentsCountRole] && !m_expandedDirs.isEmpty()) {
1338 // Assure that removing a parent item also results in removing all children
1339 QVector<int> indexesToRemoveWithChildren;
1340 indexesToRemoveWithChildren.reserve(m_itemData.count());
1341
1342 const int itemCount = m_itemData.count();
1343 for (int index : std::as_const(indexesToRemove)) {
1344 indexesToRemoveWithChildren.append(index);
1345
1346 const int parentLevel = expandedParentsCount(index);
1347 int childIndex = index + 1;
1348 while (childIndex < itemCount && expandedParentsCount(childIndex) > parentLevel) {
1349 indexesToRemoveWithChildren.append(childIndex);
1350 ++childIndex;
1351 }
1352 }
1353
1354 indexesToRemove = indexesToRemoveWithChildren;
1355 }
1356
1357 KItemRangeList itemRanges = KItemRangeList::fromSortedContainer(indexesToRemove);
1358 removeFilteredChildren(itemRanges);
1359
1360 // This call will update itemRanges to include the childless parents that have been filtered.
1361 const int filteredParentsCount = filterChildlessParents(itemRanges);
1362
1363 // If any childless parents were filtered, then itemRanges got updated and now contains items that were really deleted
1364 // mixed with expanded folders that are just being filtered out.
1365 // If that's the case, we pass 'DeleteItemDataIfUnfiltered' as a hint
1366 // so removeItems() will check m_filteredItems to differentiate which is which.
1367 removeItems(itemRanges, filteredParentsCount > 0 ? DeleteItemDataIfUnfiltered : DeleteItemData);
1368
1369 Q_EMIT fileItemsChanged(dirsChanged);
1370 }
1371
1372 void KFileItemModel::slotRefreshItems(const QList<QPair<KFileItem, KFileItem>> &items)
1373 {
1374 Q_ASSERT(!items.isEmpty());
1375 #ifdef KFILEITEMMODEL_DEBUG
1376 qCDebug(DolphinDebug) << "Refreshing" << items.count() << "items";
1377 #endif
1378
1379 // Get the indexes of all items that have been refreshed
1380 QList<int> indexes;
1381 indexes.reserve(items.count());
1382
1383 QSet<QByteArray> changedRoles;
1384 KFileItemList changedFiles;
1385
1386 // Contains the indexes of the currently visible items
1387 // that should get hidden and hence moved to m_filteredItems.
1388 QVector<int> newFilteredIndexes;
1389
1390 // Contains currently hidden items that should
1391 // get visible and hence removed from m_filteredItems
1392 QList<ItemData *> newVisibleItems;
1393
1394 QListIterator<QPair<KFileItem, KFileItem>> it(items);
1395
1396 while (it.hasNext()) {
1397 const QPair<KFileItem, KFileItem> &itemPair = it.next();
1398 const KFileItem &oldItem = itemPair.first;
1399 const KFileItem &newItem = itemPair.second;
1400 const int indexForItem = index(oldItem);
1401 const bool newItemMatchesFilter = m_filter.matches(newItem);
1402 if (indexForItem >= 0) {
1403 m_itemData[indexForItem]->item = newItem;
1404
1405 // Keep old values as long as possible if they could not retrieved synchronously yet.
1406 // The update of the values will be done asynchronously by KFileItemModelRolesUpdater.
1407 ItemData *const itemData = m_itemData.at(indexForItem);
1408 QHashIterator<QByteArray, QVariant> it(retrieveData(newItem, itemData->parent));
1409 while (it.hasNext()) {
1410 it.next();
1411 const QByteArray &role = it.key();
1412 if (itemData->values.value(role) != it.value()) {
1413 itemData->values.insert(role, it.value());
1414 changedRoles.insert(role);
1415 }
1416 }
1417
1418 m_items.remove(oldItem.url());
1419 // We must maintain m_items consistent with m_itemData for now, this very loop is using it.
1420 // We leave it to be cleared by removeItems() later, when m_itemData actually gets updated.
1421 m_items.insert(newItem.url(), indexForItem);
1422 if (newItemMatchesFilter
1423 || (itemData->values.value("isExpanded").toBool()
1424 && (indexForItem + 1 < m_itemData.count() && m_itemData.at(indexForItem + 1)->parent == itemData))) {
1425 // We are lenient with expanded folders that originally had visible children.
1426 // If they become childless now they will be caught by filterChildlessParents()
1427 changedFiles.append(newItem);
1428 indexes.append(indexForItem);
1429 } else {
1430 newFilteredIndexes.append(indexForItem);
1431 m_filteredItems.insert(newItem, itemData);
1432 }
1433 } else {
1434 // Check if 'oldItem' is one of the filtered items.
1435 QHash<KFileItem, ItemData *>::iterator it = m_filteredItems.find(oldItem);
1436 if (it != m_filteredItems.end()) {
1437 ItemData *const itemData = it.value();
1438 itemData->item = newItem;
1439
1440 // The data stored in 'values' might have changed. Therefore, we clear
1441 // 'values' and re-populate it the next time it is requested via data(int).
1442 // Before clearing, we must remember if it was expanded and the expanded parents count,
1443 // otherwise these states would be lost. The data() method will deal with this special case.
1444 const bool isExpanded = itemData->values.value("isExpanded").toBool();
1445 bool hasExpandedParentsCount = false;
1446 const int expandedParentsCount = itemData->values.value("expandedParentsCount").toInt(&hasExpandedParentsCount);
1447 itemData->values.clear();
1448 if (isExpanded) {
1449 itemData->values.insert("isExpanded", true);
1450 if (hasExpandedParentsCount) {
1451 itemData->values.insert("expandedParentsCount", expandedParentsCount);
1452 }
1453 }
1454
1455 m_filteredItems.erase(it);
1456 if (newItemMatchesFilter) {
1457 newVisibleItems.append(itemData);
1458 } else {
1459 m_filteredItems.insert(newItem, itemData);
1460 }
1461 }
1462 }
1463 }
1464
1465 std::sort(newFilteredIndexes.begin(), newFilteredIndexes.end());
1466
1467 // We must keep track of parents of new visible items since they must be shown no matter what
1468 // They will be considered "immune" to filterChildlessParents()
1469 QSet<ItemData *> parentsToEnsureVisible;
1470
1471 for (ItemData *item : newVisibleItems) {
1472 for (ItemData *parent = item->parent; parent && !parentsToEnsureVisible.contains(parent); parent = parent->parent) {
1473 parentsToEnsureVisible.insert(parent);
1474 }
1475 }
1476 for (ItemData *parent : parentsToEnsureVisible) {
1477 // We make sure they are all unfiltered.
1478 if (m_filteredItems.remove(parent->item)) {
1479 // If it is being unfiltered now, we mark it to be inserted by appending it to newVisibleItems
1480 newVisibleItems.append(parent);
1481 // It could be in newFilteredIndexes, we must remove it if it's there:
1482 const int parentIndex = index(parent->item);
1483 if (parentIndex >= 0) {
1484 QVector<int>::iterator it = std::lower_bound(newFilteredIndexes.begin(), newFilteredIndexes.end(), parentIndex);
1485 if (it != newFilteredIndexes.end() && *it == parentIndex) {
1486 newFilteredIndexes.erase(it);
1487 }
1488 }
1489 }
1490 }
1491
1492 KItemRangeList removedRanges = KItemRangeList::fromSortedContainer(newFilteredIndexes);
1493
1494 // This call will update itemRanges to include the childless parents that have been filtered.
1495 filterChildlessParents(removedRanges, parentsToEnsureVisible);
1496
1497 removeItems(removedRanges, KeepItemData);
1498
1499 // Show previously hidden items that should get visible
1500 insertItems(newVisibleItems);
1501
1502 // Final step: we will emit 'itemsChanged' and 'fileItemsChanged' signals and trigger the asynchronous re-sorting logic.
1503
1504 // If the changed items have been created recently, they might not be in m_items yet.
1505 // In that case, the list 'indexes' might be empty.
1506 if (indexes.isEmpty()) {
1507 return;
1508 }
1509
1510 if (newVisibleItems.count() > 0 || removedRanges.count() > 0) {
1511 // The original indexes have changed and are now worthless since items were removed and/or inserted.
1512 indexes.clear();
1513 // m_items is not yet rebuilt at this point, so we use our own means to resolve the new indexes.
1514 const QSet<const KFileItem> changedFilesSet(changedFiles.cbegin(), changedFiles.cend());
1515 for (int i = 0; i < m_itemData.count(); i++) {
1516 if (changedFilesSet.contains(m_itemData.at(i)->item)) {
1517 indexes.append(i);
1518 }
1519 }
1520 } else {
1521 std::sort(indexes.begin(), indexes.end());
1522 }
1523
1524 // Extract the item-ranges out of the changed indexes
1525 const KItemRangeList itemRangeList = KItemRangeList::fromSortedContainer(indexes);
1526 emitItemsChangedAndTriggerResorting(itemRangeList, changedRoles);
1527
1528 Q_EMIT fileItemsChanged(changedFiles);
1529 }
1530
1531 void KFileItemModel::slotClear()
1532 {
1533 #ifdef KFILEITEMMODEL_DEBUG
1534 qCDebug(DolphinDebug) << "Clearing all items";
1535 #endif
1536
1537 qDeleteAll(m_filteredItems);
1538 m_filteredItems.clear();
1539 m_groups.clear();
1540
1541 m_maximumUpdateIntervalTimer->stop();
1542 m_resortAllItemsTimer->stop();
1543
1544 qDeleteAll(m_pendingItemsToInsert);
1545 m_pendingItemsToInsert.clear();
1546
1547 const int removedCount = m_itemData.count();
1548 if (removedCount > 0) {
1549 qDeleteAll(m_itemData);
1550 m_itemData.clear();
1551 m_items.clear();
1552 Q_EMIT itemsRemoved(KItemRangeList() << KItemRange(0, removedCount));
1553 }
1554
1555 m_expandedDirs.clear();
1556 }
1557
1558 void KFileItemModel::slotSortingChoiceChanged()
1559 {
1560 loadSortingSettings();
1561 resortAllItems();
1562 }
1563
1564 void KFileItemModel::dispatchPendingItemsToInsert()
1565 {
1566 if (!m_pendingItemsToInsert.isEmpty()) {
1567 insertItems(m_pendingItemsToInsert);
1568 m_pendingItemsToInsert.clear();
1569 }
1570 }
1571
1572 void KFileItemModel::insertItems(QList<ItemData *> &newItems)
1573 {
1574 if (newItems.isEmpty()) {
1575 return;
1576 }
1577
1578 #ifdef KFILEITEMMODEL_DEBUG
1579 QElapsedTimer timer;
1580 timer.start();
1581 qCDebug(DolphinDebug) << "===========================================================";
1582 qCDebug(DolphinDebug) << "Inserting" << newItems.count() << "items";
1583 #endif
1584
1585 m_groups.clear();
1586 prepareItemsForSorting(newItems);
1587
1588 // Natural sorting of items can be very slow. However, it becomes much faster
1589 // if the input sequence is already mostly sorted. Therefore, we first sort
1590 // 'newItems' according to the QStrings using QString::operator<(), which is quite fast.
1591 if (m_naturalSorting) {
1592 if (m_sortRole == NameRole) {
1593 parallelMergeSort(newItems.begin(), newItems.end(), nameLessThan, QThread::idealThreadCount());
1594 } else if (isRoleValueNatural(m_sortRole)) {
1595 auto lambdaLessThan = [&](const KFileItemModel::ItemData *a, const KFileItemModel::ItemData *b) {
1596 const QByteArray role = roleForType(m_sortRole);
1597 return a->values.value(role).toString() < b->values.value(role).toString();
1598 };
1599 parallelMergeSort(newItems.begin(), newItems.end(), lambdaLessThan, QThread::idealThreadCount());
1600 }
1601 }
1602
1603 sort(newItems.begin(), newItems.end());
1604
1605 #ifdef KFILEITEMMODEL_DEBUG
1606 qCDebug(DolphinDebug) << "[TIME] Sorting:" << timer.elapsed();
1607 #endif
1608
1609 KItemRangeList itemRanges;
1610 const int existingItemCount = m_itemData.count();
1611 const int newItemCount = newItems.count();
1612 const int totalItemCount = existingItemCount + newItemCount;
1613
1614 if (existingItemCount == 0) {
1615 // Optimization for the common special case that there are no
1616 // items in the model yet. Happens, e.g., when entering a folder.
1617 m_itemData = newItems;
1618 itemRanges << KItemRange(0, newItemCount);
1619 } else {
1620 m_itemData.reserve(totalItemCount);
1621 for (int i = existingItemCount; i < totalItemCount; ++i) {
1622 m_itemData.append(nullptr);
1623 }
1624
1625 // We build the new list m_itemData in reverse order to minimize
1626 // the number of moves and guarantee O(N) complexity.
1627 int targetIndex = totalItemCount - 1;
1628 int sourceIndexExistingItems = existingItemCount - 1;
1629 int sourceIndexNewItems = newItemCount - 1;
1630
1631 int rangeCount = 0;
1632
1633 while (sourceIndexNewItems >= 0) {
1634 ItemData *newItem = newItems.at(sourceIndexNewItems);
1635 if (sourceIndexExistingItems >= 0 && lessThan(newItem, m_itemData.at(sourceIndexExistingItems), m_collator)) {
1636 // Move an existing item to its new position. If any new items
1637 // are behind it, push the item range to itemRanges.
1638 if (rangeCount > 0) {
1639 itemRanges << KItemRange(sourceIndexExistingItems + 1, rangeCount);
1640 rangeCount = 0;
1641 }
1642
1643 m_itemData[targetIndex] = m_itemData.at(sourceIndexExistingItems);
1644 --sourceIndexExistingItems;
1645 } else {
1646 // Insert a new item into the list.
1647 ++rangeCount;
1648 m_itemData[targetIndex] = newItem;
1649 --sourceIndexNewItems;
1650 }
1651 --targetIndex;
1652 }
1653
1654 // Push the final item range to itemRanges.
1655 if (rangeCount > 0) {
1656 itemRanges << KItemRange(sourceIndexExistingItems + 1, rangeCount);
1657 }
1658
1659 // Note that itemRanges is still sorted in reverse order.
1660 std::reverse(itemRanges.begin(), itemRanges.end());
1661 }
1662
1663 // The indexes in m_items are not correct anymore. Therefore, we clear m_items.
1664 // It will be re-populated with the updated indices if index(const QUrl&) is called.
1665 m_items.clear();
1666
1667 Q_EMIT itemsInserted(itemRanges);
1668
1669 #ifdef KFILEITEMMODEL_DEBUG
1670 qCDebug(DolphinDebug) << "[TIME] Inserting of" << newItems.count() << "items:" << timer.elapsed();
1671 #endif
1672 }
1673
1674 void KFileItemModel::removeItems(const KItemRangeList &itemRanges, RemoveItemsBehavior behavior)
1675 {
1676 if (itemRanges.isEmpty()) {
1677 return;
1678 }
1679
1680 m_groups.clear();
1681
1682 // Step 1: Remove the items from m_itemData, and free the ItemData.
1683 int removedItemsCount = 0;
1684 for (const KItemRange &range : itemRanges) {
1685 removedItemsCount += range.count;
1686
1687 for (int index = range.index; index < range.index + range.count; ++index) {
1688 if (behavior == DeleteItemData || (behavior == DeleteItemDataIfUnfiltered && !m_filteredItems.contains(m_itemData.at(index)->item))) {
1689 delete m_itemData.at(index);
1690 }
1691
1692 m_itemData[index] = nullptr;
1693 }
1694 }
1695
1696 // Step 2: Remove the ItemData pointers from the list m_itemData.
1697 int target = itemRanges.at(0).index;
1698 int source = itemRanges.at(0).index + itemRanges.at(0).count;
1699 int nextRange = 1;
1700
1701 const int oldItemDataCount = m_itemData.count();
1702 while (source < oldItemDataCount) {
1703 m_itemData[target] = m_itemData[source];
1704 ++target;
1705 ++source;
1706
1707 if (nextRange < itemRanges.count() && source == itemRanges.at(nextRange).index) {
1708 // Skip the items in the next removed range.
1709 source += itemRanges.at(nextRange).count;
1710 ++nextRange;
1711 }
1712 }
1713
1714 m_itemData.erase(m_itemData.end() - removedItemsCount, m_itemData.end());
1715
1716 // The indexes in m_items are not correct anymore. Therefore, we clear m_items.
1717 // It will be re-populated with the updated indices if index(const QUrl&) is called.
1718 m_items.clear();
1719
1720 Q_EMIT itemsRemoved(itemRanges);
1721 }
1722
1723 QList<KFileItemModel::ItemData *> KFileItemModel::createItemDataList(const QUrl &parentUrl, const KFileItemList &items) const
1724 {
1725 if (m_sortRole == TypeRole || m_groupRole == TypeRole) {
1726 // Try to resolve the MIME-types synchronously to prevent a reordering of
1727 // the items when sorting by type (per default MIME-types are resolved
1728 // asynchronously by KFileItemModelRolesUpdater).
1729 determineMimeTypes(items, 200);
1730 }
1731
1732 // We search for the parent in m_itemData and then in m_filteredItems if necessary
1733 const int parentIndex = index(parentUrl);
1734 ItemData *parentItem = parentIndex < 0 ? m_filteredItems.value(KFileItem(parentUrl), nullptr) : m_itemData.at(parentIndex);
1735
1736 QList<ItemData *> itemDataList;
1737 itemDataList.reserve(items.count());
1738
1739 for (const KFileItem &item : items) {
1740 ItemData *itemData = new ItemData();
1741 itemData->item = item;
1742 itemData->parent = parentItem;
1743 itemDataList.append(itemData);
1744 }
1745
1746 return itemDataList;
1747 }
1748
1749 void KFileItemModel::prepareItemsWithRole(QList<ItemData *> &itemDataList, RoleType roleType)
1750 {
1751 switch (roleType) {
1752 case ExtensionRole:
1753 case PermissionsRole:
1754 case OwnerRole:
1755 case GroupRole:
1756 case DestinationRole:
1757 case PathRole:
1758 case DeletionTimeRole:
1759 // These roles can be determined with retrieveData, and they have to be stored
1760 // in the QHash "values" for the sorting.
1761 for (ItemData *itemData : std::as_const(itemDataList)) {
1762 if (itemData->values.isEmpty()) {
1763 itemData->values = retrieveData(itemData->item, itemData->parent);
1764 }
1765 }
1766 break;
1767
1768 case TypeRole:
1769 // At least store the data including the file type for items with known MIME type.
1770 for (ItemData *itemData : std::as_const(itemDataList)) {
1771 if (itemData->values.isEmpty()) {
1772 const KFileItem item = itemData->item;
1773 if (item.isDir() || item.isMimeTypeKnown()) {
1774 itemData->values = retrieveData(itemData->item, itemData->parent);
1775 }
1776 }
1777 }
1778 break;
1779
1780 default:
1781 // The other roles are either resolved by KFileItemModelRolesUpdater
1782 // (this includes the SizeRole for directories), or they do not need
1783 // to be stored in the QHash "values" for sorting because the data can
1784 // be retrieved directly from the KFileItem (NameRole, SizeRole for files,
1785 // DateRole).
1786 break;
1787 }
1788 }
1789
1790 void KFileItemModel::prepareItemsForSorting(QList<ItemData *> &itemDataList)
1791 {
1792 prepareItemsWithRole(itemDataList, m_sortRole);
1793 prepareItemsWithRole(itemDataList, m_groupRole);
1794 }
1795
1796 int KFileItemModel::expandedParentsCount(const ItemData *data)
1797 {
1798 // The hash 'values' is only guaranteed to contain the key "expandedParentsCount"
1799 // if the corresponding item is expanded, and it is not a top-level item.
1800 const ItemData *parent = data->parent;
1801 if (parent) {
1802 if (parent->parent) {
1803 Q_ASSERT(parent->values.contains("expandedParentsCount"));
1804 return parent->values.value("expandedParentsCount").toInt() + 1;
1805 } else {
1806 return 1;
1807 }
1808 } else {
1809 return 0;
1810 }
1811 }
1812
1813 void KFileItemModel::removeExpandedItems()
1814 {
1815 QVector<int> indexesToRemove;
1816
1817 const int maxIndex = m_itemData.count() - 1;
1818 for (int i = 0; i <= maxIndex; ++i) {
1819 const ItemData *itemData = m_itemData.at(i);
1820 if (itemData->parent) {
1821 indexesToRemove.append(i);
1822 }
1823 }
1824
1825 removeItems(KItemRangeList::fromSortedContainer(indexesToRemove), DeleteItemData);
1826 m_expandedDirs.clear();
1827
1828 // Also remove all filtered items which have a parent.
1829 QHash<KFileItem, ItemData *>::iterator it = m_filteredItems.begin();
1830 const QHash<KFileItem, ItemData *>::iterator end = m_filteredItems.end();
1831
1832 while (it != end) {
1833 if (it.value()->parent) {
1834 delete it.value();
1835 it = m_filteredItems.erase(it);
1836 } else {
1837 ++it;
1838 }
1839 }
1840 }
1841
1842 void KFileItemModel::emitItemsChangedAndTriggerResorting(const KItemRangeList &itemRanges, const QSet<QByteArray> &changedRoles)
1843 {
1844 Q_EMIT itemsChanged(itemRanges, changedRoles);
1845
1846 // Trigger a resorting if necessary. Note that this can happen even if the sort
1847 // role has not changed at all because the file name can be used as a fallback.
1848 if (changedRoles.contains(sortRole()) || changedRoles.contains(roleForType(NameRole))
1849 || (changedRoles.contains("count") && sortRole() == "size")) { // "count" is used in the "size" sort role, so this might require a resorting.
1850 for (const KItemRange &range : itemRanges) {
1851 bool needsResorting = false;
1852
1853 const int first = range.index;
1854 const int last = range.index + range.count - 1;
1855
1856 // Resorting the model is necessary if
1857 // (a) The first item in the range is "lessThan" its predecessor,
1858 // (b) the successor of the last item is "lessThan" the last item, or
1859 // (c) the internal order of the items in the range is incorrect.
1860 if (first > 0 && lessThan(m_itemData.at(first), m_itemData.at(first - 1), m_collator)) {
1861 needsResorting = true;
1862 } else if (last < count() - 1 && lessThan(m_itemData.at(last + 1), m_itemData.at(last), m_collator)) {
1863 needsResorting = true;
1864 } else {
1865 for (int index = first; index < last; ++index) {
1866 if (lessThan(m_itemData.at(index + 1), m_itemData.at(index), m_collator)) {
1867 needsResorting = true;
1868 break;
1869 }
1870 }
1871 }
1872
1873 if (needsResorting) {
1874 scheduleResortAllItems();
1875 return;
1876 }
1877 }
1878 }
1879
1880 if (groupedSorting() && changedRoles.contains(sortRole())) {
1881 // The position is still correct, but the groups might have changed
1882 // if the changed item is either the first or the last item in a
1883 // group.
1884 // In principle, we could try to find out if the item really is the
1885 // first or last one in its group and then update the groups
1886 // (possibly with a delayed timer to make sure that we don't
1887 // re-calculate the groups very often if items are updated one by
1888 // one), but starting m_resortAllItemsTimer is easier.
1889 m_resortAllItemsTimer->start();
1890 }
1891 }
1892
1893 void KFileItemModel::resetRoles()
1894 {
1895 for (int i = 0; i < RolesCount; ++i) {
1896 m_requestRole[i] = false;
1897 }
1898 }
1899
1900 KFileItemModel::RoleType KFileItemModel::typeForRole(const QByteArray &role) const
1901 {
1902 static QHash<QByteArray, RoleType> roles;
1903 if (roles.isEmpty()) {
1904 // Insert user visible roles that can be accessed with
1905 // KFileItemModel::roleInformation()
1906 int count = 0;
1907 const RoleInfoMap *map = rolesInfoMap(count);
1908 for (int i = 0; i < count; ++i) {
1909 roles.insert(map[i].role, map[i].roleType);
1910 }
1911
1912 // Insert internal roles (take care to synchronize the implementation
1913 // with KFileItemModel::roleForType() in case if a change is done).
1914 roles.insert("isDir", IsDirRole);
1915 roles.insert("isLink", IsLinkRole);
1916 roles.insert("isHidden", IsHiddenRole);
1917 roles.insert("isExpanded", IsExpandedRole);
1918 roles.insert("isExpandable", IsExpandableRole);
1919 roles.insert("expandedParentsCount", ExpandedParentsCountRole);
1920
1921 Q_ASSERT(roles.count() == RolesCount);
1922 }
1923
1924 return roles.value(role, NoRole);
1925 }
1926
1927 QByteArray KFileItemModel::roleForType(RoleType roleType) const
1928 {
1929 static QHash<RoleType, QByteArray> roles;
1930 if (roles.isEmpty()) {
1931 // Insert user visible roles that can be accessed with
1932 // KFileItemModel::roleInformation()
1933 int count = 0;
1934 const RoleInfoMap *map = rolesInfoMap(count);
1935 for (int i = 0; i < count; ++i) {
1936 roles.insert(map[i].roleType, map[i].role);
1937 }
1938
1939 // Insert internal roles (take care to synchronize the implementation
1940 // with KFileItemModel::typeForRole() in case if a change is done).
1941 roles.insert(IsDirRole, "isDir");
1942 roles.insert(IsLinkRole, "isLink");
1943 roles.insert(IsHiddenRole, "isHidden");
1944 roles.insert(IsExpandedRole, "isExpanded");
1945 roles.insert(IsExpandableRole, "isExpandable");
1946 roles.insert(ExpandedParentsCountRole, "expandedParentsCount");
1947
1948 Q_ASSERT(roles.count() == RolesCount);
1949 };
1950
1951 return roles.value(roleType);
1952 }
1953
1954 QHash<QByteArray, QVariant> KFileItemModel::retrieveData(const KFileItem &item, const ItemData *parent) const
1955 {
1956 // It is important to insert only roles that are fast to retrieve. E.g.
1957 // KFileItem::iconName() can be very expensive if the MIME-type is unknown
1958 // and hence will be retrieved asynchronously by KFileItemModelRolesUpdater.
1959 QHash<QByteArray, QVariant> data;
1960 data.insert(sharedValue("url"), item.url());
1961
1962 const bool isDir = item.isDir();
1963 if (m_requestRole[IsDirRole] && isDir) {
1964 data.insert(sharedValue("isDir"), true);
1965 }
1966
1967 if (m_requestRole[IsLinkRole] && item.isLink()) {
1968 data.insert(sharedValue("isLink"), true);
1969 }
1970
1971 if (m_requestRole[IsHiddenRole]) {
1972 data.insert(sharedValue("isHidden"), item.isHidden() || item.mimetype() == QStringLiteral("application/x-trash"));
1973 }
1974
1975 if (m_requestRole[NameRole]) {
1976 data.insert(sharedValue("text"), item.text());
1977 }
1978
1979 if (m_requestRole[ExtensionRole] && !isDir) {
1980 // TODO KF6 use KFileItem::suffix 464722
1981 data.insert(sharedValue("extension"), QFileInfo(item.name()).suffix());
1982 }
1983
1984 if (m_requestRole[SizeRole] && !isDir) {
1985 data.insert(sharedValue("size"), item.size());
1986 }
1987
1988 if (m_requestRole[ModificationTimeRole]) {
1989 // Don't use KFileItem::timeString() or KFileItem::time() as this is too expensive when
1990 // having several thousands of items. Instead read the raw number from UDSEntry directly
1991 // and the formatting of the date-time will be done on-demand by the view when the date will be shown.
1992 const long long dateTime = item.entry().numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME, -1);
1993 data.insert(sharedValue("modificationtime"), dateTime);
1994 }
1995
1996 if (m_requestRole[CreationTimeRole]) {
1997 // Don't use KFileItem::timeString() or KFileItem::time() as this is too expensive when
1998 // having several thousands of items. Instead read the raw number from UDSEntry directly
1999 // and the formatting of the date-time will be done on-demand by the view when the date will be shown.
2000 const long long dateTime = item.entry().numberValue(KIO::UDSEntry::UDS_CREATION_TIME, -1);
2001 data.insert(sharedValue("creationtime"), dateTime);
2002 }
2003
2004 if (m_requestRole[AccessTimeRole]) {
2005 // Don't use KFileItem::timeString() or KFileItem::time() as this is too expensive when
2006 // having several thousands of items. Instead read the raw number from UDSEntry directly
2007 // and the formatting of the date-time will be done on-demand by the view when the date will be shown.
2008 const long long dateTime = item.entry().numberValue(KIO::UDSEntry::UDS_ACCESS_TIME, -1);
2009 data.insert(sharedValue("accesstime"), dateTime);
2010 }
2011
2012 if (m_requestRole[PermissionsRole]) {
2013 data.insert(sharedValue("permissions"), QVariantList() << item.permissionsString() << item.permissions());
2014 }
2015
2016 if (m_requestRole[OwnerRole]) {
2017 data.insert(sharedValue("owner"), item.user());
2018 }
2019
2020 if (m_requestRole[GroupRole]) {
2021 data.insert(sharedValue("group"), item.group());
2022 }
2023
2024 if (m_requestRole[DestinationRole]) {
2025 QString destination = item.linkDest();
2026 if (destination.isEmpty()) {
2027 destination = QLatin1Char('-');
2028 }
2029 data.insert(sharedValue("destination"), destination);
2030 }
2031
2032 if (m_requestRole[PathRole]) {
2033 QString path;
2034 if (item.url().scheme() == QLatin1String("trash")) {
2035 path = item.entry().stringValue(KIO::UDSEntry::UDS_EXTRA);
2036 } else {
2037 // For performance reasons cache the home-path in a static QString
2038 // (see QDir::homePath() for more details)
2039 static QString homePath;
2040 if (homePath.isEmpty()) {
2041 homePath = QDir::homePath();
2042 }
2043
2044 path = item.localPath();
2045 if (path.startsWith(homePath)) {
2046 path.replace(0, homePath.length(), QLatin1Char('~'));
2047 }
2048 }
2049
2050 const int index = path.lastIndexOf(item.text());
2051 path = path.mid(0, index - 1);
2052 data.insert(sharedValue("path"), path);
2053 }
2054
2055 if (m_requestRole[DeletionTimeRole]) {
2056 QDateTime deletionTime;
2057 if (item.url().scheme() == QLatin1String("trash")) {
2058 deletionTime = QDateTime::fromString(item.entry().stringValue(KIO::UDSEntry::UDS_EXTRA + 1), Qt::ISODate);
2059 }
2060 data.insert(sharedValue("deletiontime"), deletionTime);
2061 }
2062
2063 if (m_requestRole[IsExpandableRole] && isDir) {
2064 data.insert(sharedValue("isExpandable"), true);
2065 }
2066
2067 if (m_requestRole[ExpandedParentsCountRole]) {
2068 if (parent) {
2069 const int level = expandedParentsCount(parent) + 1;
2070 data.insert(sharedValue("expandedParentsCount"), level);
2071 }
2072 }
2073
2074 if (item.isMimeTypeKnown()) {
2075 QString iconName = item.iconName();
2076 if (!QIcon::hasThemeIcon(iconName)) {
2077 QMimeType mimeType = QMimeDatabase().mimeTypeForName(item.mimetype());
2078 iconName = mimeType.genericIconName();
2079 }
2080
2081 data.insert(sharedValue("iconName"), iconName);
2082
2083 if (m_requestRole[TypeRole]) {
2084 data.insert(sharedValue("type"), item.mimeComment());
2085 }
2086 } else if (m_requestRole[TypeRole] && isDir) {
2087 static const QString folderMimeType = item.mimeComment();
2088 data.insert(sharedValue("type"), folderMimeType);
2089 }
2090
2091 return data;
2092 }
2093
2094 bool KFileItemModel::lessThan(const ItemData *a, const ItemData *b, const QCollator &collator) const
2095 {
2096 int result = 0;
2097
2098 if (a->parent != b->parent) {
2099 const int expansionLevelA = expandedParentsCount(a);
2100 const int expansionLevelB = expandedParentsCount(b);
2101
2102 // If b has a higher expansion level than a, check if a is a parent
2103 // of b, and make sure that both expansion levels are equal otherwise.
2104 for (int i = expansionLevelB; i > expansionLevelA; --i) {
2105 if (b->parent == a) {
2106 return true;
2107 }
2108 b = b->parent;
2109 }
2110
2111 // If a has a higher expansion level than a, check if b is a parent
2112 // of a, and make sure that both expansion levels are equal otherwise.
2113 for (int i = expansionLevelA; i > expansionLevelB; --i) {
2114 if (a->parent == b) {
2115 return false;
2116 }
2117 a = a->parent;
2118 }
2119
2120 Q_ASSERT(expandedParentsCount(a) == expandedParentsCount(b));
2121
2122 // Compare the last parents of a and b which are different.
2123 while (a->parent != b->parent) {
2124 a = a->parent;
2125 b = b->parent;
2126 }
2127 }
2128
2129 result = groupRoleCompare(a, b, collator);
2130 if (result == 0) {
2131 // Show hidden files and folders last
2132 if (m_sortHiddenLast) {
2133 const bool isHiddenA = a->item.isHidden();
2134 const bool isHiddenB = b->item.isHidden();
2135 if (isHiddenA && !isHiddenB) {
2136 return false;
2137 } else if (!isHiddenA && isHiddenB) {
2138 return true;
2139 }
2140 }
2141 if (m_sortDirsFirst || (ContentDisplaySettings::directorySizeMode() == ContentDisplaySettings::EnumDirectorySizeMode::ContentCount && m_sortRole == SizeRole)) {
2142 const bool isDirA = a->item.isDir();
2143 const bool isDirB = b->item.isDir();
2144 if (isDirA && !isDirB) {
2145 return true;
2146 } else if (!isDirA && isDirB) {
2147 return false;
2148 }
2149 }
2150 result = sortRoleCompare(a, b, collator);
2151 result = (sortOrder() == Qt::AscendingOrder) ? result < 0 : result > 0;
2152 } else {
2153 result = (groupOrder() == Qt::AscendingOrder) ? result < 0 : result > 0;
2154 }
2155 return result;
2156 }
2157
2158 void KFileItemModel::sort(const QList<KFileItemModel::ItemData *>::iterator &begin, const QList<KFileItemModel::ItemData *>::iterator &end) const
2159 {
2160 auto lambdaLessThan = [&](const KFileItemModel::ItemData *a, const KFileItemModel::ItemData *b) {
2161 return lessThan(a, b, m_collator);
2162 };
2163
2164 if (m_sortRole == NameRole || isRoleValueNatural(m_sortRole)) {
2165 // Sorting by string can be expensive, in particular if natural sorting is
2166 // enabled. Use all CPU cores to speed up the sorting process.
2167 static const int numberOfThreads = QThread::idealThreadCount();
2168 parallelMergeSort(begin, end, lambdaLessThan, numberOfThreads);
2169 } else {
2170 // Sorting by other roles is quite fast. Use only one thread to prevent
2171 // problems caused by non-reentrant comparison functions, see
2172 // https://bugs.kde.org/show_bug.cgi?id=312679
2173 mergeSort(begin, end, lambdaLessThan);
2174 }
2175 }
2176
2177 int KFileItemModel::sortRoleCompare(const ItemData *a, const ItemData *b, const QCollator &collator) const
2178 {
2179 // This function must never return 0, because that would break stable
2180 // sorting, which leads to all kinds of bugs.
2181 // See: https://bugs.kde.org/show_bug.cgi?id=433247
2182 // If two items have equal sort values, let the fallbacks at the bottom of
2183 // the function handle it.
2184 const KFileItem &itemA = a->item;
2185 const KFileItem &itemB = b->item;
2186
2187 int result = 0;
2188
2189 switch (m_sortRole) {
2190 case NameRole:
2191 // The name role is handled as default fallback after the switch
2192 break;
2193
2194 case SizeRole: {
2195 if (ContentDisplaySettings::directorySizeMode() == ContentDisplaySettings::EnumDirectorySizeMode::ContentCount && itemA.isDir()) {
2196 // folders first then
2197 // items A and B are folders thanks to lessThan checks
2198 auto valueA = a->values.value("count");
2199 auto valueB = b->values.value("count");
2200 if (valueA.isNull()) {
2201 if (!valueB.isNull()) {
2202 return -1;
2203 }
2204 } else if (valueB.isNull()) {
2205 return +1;
2206 } else {
2207 if (valueA.toLongLong() < valueB.toLongLong()) {
2208 return -1;
2209 } else if (valueA.toLongLong() > valueB.toLongLong()) {
2210 return +1;
2211 }
2212 }
2213 break;
2214 }
2215
2216 KIO::filesize_t sizeA = 0;
2217 if (itemA.isDir()) {
2218 sizeA = a->values.value("size").toULongLong();
2219 } else {
2220 sizeA = itemA.size();
2221 }
2222 KIO::filesize_t sizeB = 0;
2223 if (itemB.isDir()) {
2224 sizeB = b->values.value("size").toULongLong();
2225 } else {
2226 sizeB = itemB.size();
2227 }
2228 if (sizeA < sizeB) {
2229 return -1;
2230 } else if (sizeA > sizeB) {
2231 return +1;
2232 }
2233 break;
2234 }
2235
2236 case ModificationTimeRole: {
2237 const long long dateTimeA = itemA.entry().numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME, -1);
2238 const long long dateTimeB = itemB.entry().numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME, -1);
2239 if (dateTimeA < dateTimeB) {
2240 return -1;
2241 } else if (dateTimeA > dateTimeB) {
2242 return +1;
2243 }
2244 break;
2245 }
2246
2247 case AccessTimeRole: {
2248 const long long dateTimeA = itemA.entry().numberValue(KIO::UDSEntry::UDS_ACCESS_TIME, -1);
2249 const long long dateTimeB = itemB.entry().numberValue(KIO::UDSEntry::UDS_ACCESS_TIME, -1);
2250 if (dateTimeA < dateTimeB) {
2251 return -1;
2252 } else if (dateTimeA > dateTimeB) {
2253 return +1;
2254 }
2255 break;
2256 }
2257
2258 case CreationTimeRole: {
2259 const long long dateTimeA = itemA.entry().numberValue(KIO::UDSEntry::UDS_CREATION_TIME, -1);
2260 const long long dateTimeB = itemB.entry().numberValue(KIO::UDSEntry::UDS_CREATION_TIME, -1);
2261 if (dateTimeA < dateTimeB) {
2262 return -1;
2263 } else if (dateTimeA > dateTimeB) {
2264 return +1;
2265 }
2266 break;
2267 }
2268
2269 case DeletionTimeRole: {
2270 const QDateTime dateTimeA = a->values.value("deletiontime").toDateTime();
2271 const QDateTime dateTimeB = b->values.value("deletiontime").toDateTime();
2272 if (dateTimeA < dateTimeB) {
2273 return -1;
2274 } else if (dateTimeA > dateTimeB) {
2275 return +1;
2276 }
2277 break;
2278 }
2279
2280 case RatingRole:
2281 case WidthRole:
2282 case HeightRole:
2283 case PublisherRole:
2284 case PageCountRole:
2285 case WordCountRole:
2286 case LineCountRole:
2287 case TrackRole:
2288 case ReleaseYearRole: {
2289 result = a->values.value(roleForType(m_sortRole)).toInt() - b->values.value(roleForType(m_sortRole)).toInt();
2290 break;
2291 }
2292
2293 case DimensionsRole: {
2294 const QByteArray role = roleForType(m_sortRole);
2295 const QSize dimensionsA = a->values.value(role).toSize();
2296 const QSize dimensionsB = b->values.value(role).toSize();
2297
2298 if (dimensionsA.width() == dimensionsB.width()) {
2299 result = dimensionsA.height() - dimensionsB.height();
2300 } else {
2301 result = dimensionsA.width() - dimensionsB.width();
2302 }
2303 break;
2304 }
2305
2306 default: {
2307 const QByteArray role = roleForType(m_sortRole);
2308 const QString roleValueA = a->values.value(role).toString();
2309 const QString roleValueB = b->values.value(role).toString();
2310 if (!roleValueA.isEmpty() && roleValueB.isEmpty()) {
2311 return -1;
2312 } else if (roleValueA.isEmpty() && !roleValueB.isEmpty()) {
2313 return +1;
2314 } else if (isRoleValueNatural(m_sortRole)) {
2315 result = stringCompare(roleValueA, roleValueB, collator);
2316 } else {
2317 result = QString::compare(roleValueA, roleValueB);
2318 }
2319 break;
2320 }
2321 }
2322
2323 if (result != 0) {
2324 // The current sort role was sufficient to define an order
2325 return result;
2326 }
2327
2328 // Fallback #1: Compare the text of the items
2329 result = stringCompare(itemA.text(), itemB.text(), collator);
2330 if (result != 0) {
2331 return result;
2332 }
2333
2334 // Fallback #2: KFileItem::text() may not be unique in case UDS_DISPLAY_NAME is used
2335 result = stringCompare(itemA.name(), itemB.name(), collator);
2336 if (result != 0) {
2337 return result;
2338 }
2339
2340 // Fallback #3: It must be assured that the sort order is always unique even if two values have been
2341 // equal. In this case a comparison of the URL is done which is unique in all cases
2342 // within KDirLister.
2343 return QString::compare(itemA.url().url(), itemB.url().url(), Qt::CaseSensitive);
2344 }
2345
2346 int KFileItemModel::groupRoleCompare(const ItemData *a, const ItemData *b, const QCollator &collator) const
2347 {
2348 // Unlike sortRoleCompare, this function can and often will return 0.
2349 int result = 0;
2350
2351 ItemGroupInfo groupA, groupB;
2352 switch (m_groupRole) {
2353 case NoRole:
2354 // Non-trivial grouping behavior might be handled there in the future.
2355 return 0;
2356 case NameRole:
2357 groupA = nameRoleGroup(a, false);
2358 groupB = nameRoleGroup(b, false);
2359 break;
2360 case SizeRole:
2361 groupA = sizeRoleGroup(a, false);
2362 groupB = sizeRoleGroup(b, false);
2363 break;
2364 case ModificationTimeRole:
2365 groupA = timeRoleGroup(
2366 [](const ItemData *item) {
2367 return item->item.time(KFileItem::ModificationTime);
2368 },
2369 a,
2370 false);
2371 groupB = timeRoleGroup(
2372 [](const ItemData *item) {
2373 return item->item.time(KFileItem::ModificationTime);
2374 },
2375 b,
2376 false);
2377 break;
2378 case CreationTimeRole:
2379 groupA = timeRoleGroup(
2380 [](const ItemData *item) {
2381 return item->item.time(KFileItem::CreationTime);
2382 },
2383 a,
2384 false);
2385 groupB = timeRoleGroup(
2386 [](const ItemData *item) {
2387 return item->item.time(KFileItem::CreationTime);
2388 },
2389 b,
2390 false);
2391 break;
2392 case AccessTimeRole:
2393 groupA = timeRoleGroup(
2394 [](const ItemData *item) {
2395 return item->item.time(KFileItem::AccessTime);
2396 },
2397 a,
2398 false);
2399 groupB = timeRoleGroup(
2400 [](const ItemData *item) {
2401 return item->item.time(KFileItem::AccessTime);
2402 },
2403 b,
2404 false);
2405 break;
2406 case DeletionTimeRole:
2407 groupA = timeRoleGroup(
2408 [](const ItemData *item) {
2409 return item->values.value("deletiontime").toDateTime();
2410 },
2411 a,
2412 false);
2413 groupB = timeRoleGroup(
2414 [](const ItemData *item) {
2415 return item->values.value("deletiontime").toDateTime();
2416 },
2417 b,
2418 false);
2419 break;
2420 case PermissionsRole:
2421 groupA = permissionRoleGroup(a, false);
2422 groupB = permissionRoleGroup(b, false);
2423 break;
2424 case RatingRole:
2425 groupA = ratingRoleGroup(a, false);
2426 groupB = ratingRoleGroup(b, false);
2427 break;
2428 case TypeRole:
2429 groupA = typeRoleGroup(a);
2430 groupB = typeRoleGroup(b);
2431 break;
2432 default: {
2433 groupA = genericStringRoleGroup(groupRole(), a);
2434 groupB = genericStringRoleGroup(groupRole(), b);
2435 break;
2436 }
2437 }
2438 if (groupA.comparable < groupB.comparable) {
2439 result = -1;
2440 } else if (groupA.comparable > groupB.comparable) {
2441 result = 1;
2442 } else {
2443 result = stringCompare(groupA.text, groupB.text, collator);
2444 }
2445 return result;
2446 }
2447
2448 int KFileItemModel::stringCompare(const QString &a, const QString &b, const QCollator &collator) const
2449 {
2450 QMutexLocker collatorLock(s_collatorMutex());
2451
2452 if (m_naturalSorting) {
2453 // Split extension, taking into account it can be empty
2454 constexpr QString::SectionFlags flags = QString::SectionSkipEmpty | QString::SectionIncludeLeadingSep;
2455
2456 // Sort by baseName first
2457 const QString aBaseName = a.section('.', 0, 0, flags);
2458 const QString bBaseName = b.section('.', 0, 0, flags);
2459
2460 const int res = collator.compare(aBaseName, bBaseName);
2461 if (res != 0 || (aBaseName.length() == a.length() && bBaseName.length() == b.length())) {
2462 return res;
2463 }
2464
2465 // sliced() has undefined behavior when pos < 0 or pos > size().
2466 Q_ASSERT(aBaseName.length() <= a.length() && aBaseName.length() >= 0);
2467 Q_ASSERT(bBaseName.length() <= b.length() && bBaseName.length() >= 0);
2468
2469 // baseNames were equal, sort by extension
2470 return collator.compare(a.sliced(aBaseName.length()), b.sliced(bBaseName.length()));
2471 }
2472
2473 const int result = QString::compare(a, b, collator.caseSensitivity());
2474 if (result != 0 || collator.caseSensitivity() == Qt::CaseSensitive) {
2475 // Only return the result, if the strings are not equal. If they are equal by a case insensitive
2476 // comparison, still a deterministic sort order is required. A case sensitive
2477 // comparison is done as fallback.
2478 return result;
2479 }
2480
2481 return QString::compare(a, b, Qt::CaseSensitive);
2482 }
2483
2484 KFileItemModel::ItemGroupInfo KFileItemModel::nameRoleGroup(const ItemData *itemData, bool withString) const
2485 {
2486 static bool oldWithString;
2487 static ItemGroupInfo oldGroupInfo;
2488 static QChar oldFirstChar;
2489 ItemGroupInfo groupInfo;
2490 QChar firstChar;
2491
2492 const QString name = itemData->item.text();
2493
2494 QMutexLocker collatorLock(s_collatorMutex());
2495
2496 // Use the first character of the name as group indication
2497 firstChar = name.at(0).toUpper();
2498
2499 if (firstChar == oldFirstChar && withString == oldWithString) {
2500 return oldGroupInfo;
2501 }
2502 if (firstChar == QLatin1Char('~') && name.length() > 1) {
2503 firstChar = name.at(1).toUpper();
2504 }
2505 if (firstChar.isLetter()) {
2506 if (m_collator.compare(firstChar, QChar(QLatin1Char('A'))) >= 0 && m_collator.compare(firstChar, QChar(QLatin1Char('Z'))) <= 0) {
2507 // WARNING! Symbols based on latin 'Z' like 'Z' with acute are treated wrong as non Latin and put in a new group.
2508
2509 // Try to find a matching group in the range 'A' to 'Z'.
2510 static std::vector<QChar> lettersAtoZ;
2511 lettersAtoZ.reserve('Z' - 'A' + 1);
2512 if (lettersAtoZ.empty()) {
2513 for (char c = 'A'; c <= 'Z'; ++c) {
2514 lettersAtoZ.push_back(QLatin1Char(c));
2515 }
2516 }
2517
2518 auto localeAwareLessThan = [this](QChar c1, QChar c2) -> bool {
2519 return m_collator.compare(c1, c2) < 0;
2520 };
2521
2522 std::vector<QChar>::iterator it = std::lower_bound(lettersAtoZ.begin(), lettersAtoZ.end(), firstChar, localeAwareLessThan);
2523 if (it != lettersAtoZ.end()) {
2524 if (localeAwareLessThan(firstChar, *it)) {
2525 // newFirstChar belongs to the group preceding *it.
2526 // Example: for an umlaut 'A' in the German locale, *it would be 'B' now.
2527 --it;
2528 }
2529 if (withString) {
2530 groupInfo.text = *it;
2531 }
2532 groupInfo.comparable = (*it).unicode();
2533 }
2534
2535 } else {
2536 // Symbols from non Latin-based scripts
2537 if (withString) {
2538 groupInfo.text = firstChar;
2539 }
2540 groupInfo.comparable = firstChar.unicode();
2541 }
2542 } else if (firstChar >= QLatin1Char('0') && firstChar <= QLatin1Char('9')) {
2543 // Apply group '0 - 9' for any name that starts with a digit
2544 if (withString) {
2545 groupInfo.text = i18nc("@title:group Groups that start with a digit", "0 - 9");
2546 }
2547 groupInfo.comparable = (int)'0';
2548 } else {
2549 if (withString) {
2550 groupInfo.text = i18nc("@title:group", "Others");
2551 }
2552 groupInfo.comparable = (int)'.';
2553 }
2554 oldWithString = withString;
2555 oldFirstChar = firstChar;
2556 oldGroupInfo = groupInfo;
2557 return groupInfo;
2558 }
2559
2560 KFileItemModel::ItemGroupInfo KFileItemModel::sizeRoleGroup(const ItemData *itemData, bool withString) const
2561 {
2562 ItemGroupInfo groupInfo;
2563 KIO::filesize_t fileSize;
2564
2565 const KFileItem item = itemData->item;
2566 fileSize = !item.isNull() ? item.size() : ~0U;
2567
2568 groupInfo.comparable = -1; // None
2569 if (!item.isNull() && item.isDir()) {
2570 if (ContentDisplaySettings::directorySizeMode() != ContentDisplaySettings::EnumDirectorySizeMode::ContentSize) {
2571 groupInfo.comparable = 0; // Folders
2572 } else {
2573 fileSize = itemData->values.value("size").toULongLong();
2574 }
2575 }
2576 if (groupInfo.comparable < 0) {
2577 if (fileSize < 5 * 1024 * 1024) { // < 5 MB
2578 groupInfo.comparable = 1; // Small
2579 } else if (fileSize < 10 * 1024 * 1024) { // < 10 MB
2580 groupInfo.comparable = 2; // Medium
2581 } else {
2582 groupInfo.comparable = 3; // Big
2583 }
2584 }
2585
2586 if (withString) {
2587 char const *groupNames[] = {"Folders", "Small", "Medium", "Big"};
2588 groupInfo.text = i18nc("@title:group Size", groupNames[groupInfo.comparable]);
2589 }
2590 return groupInfo;
2591 }
2592
2593 KFileItemModel::ItemGroupInfo
2594 KFileItemModel::timeRoleGroup(const std::function<QDateTime(const ItemData *)> &fileTimeCb, const ItemData *itemData, bool withString) const
2595 {
2596 static bool oldWithString;
2597 static ItemGroupInfo oldGroupInfo;
2598 static QDate oldFileDate;
2599 ItemGroupInfo groupInfo;
2600
2601 const QDate currentDate = QDate::currentDate();
2602
2603 QDate previousFileDate;
2604 QString groupValue;
2605 for (int i = 0; i <= maxIndex; ++i) {
2606 if (isChildItem(i)) {
2607 continue;
2608 }
2609
2610 const QLocale locale;
2611 const QDateTime fileTime = fileTimeCb(m_itemData.at(i));
2612 const QDate fileDate = fileTime.date();
2613 if (fileDate == previousFileDate) {
2614 // The current item is in the same group as the previous item
2615 continue;
2616 }
2617 previousFileDate = fileDate;
2618
2619 const int daysDistance = fileDate.daysTo(currentDate);
2620
2621 if (fileDate == oldFileDate && withString == oldWithString) {
2622 return oldGroupInfo;
2623 }
2624 // Simplified grouping algorithm, preserving dates
2625 // but not taking "pretty printing" into account
2626 if (currentDate.year() == fileDate.year() && currentDate.month() == fileDate.month()) {
2627 if (daysDistance < 7) {
2628 groupInfo.comparable = daysDistance; // Today, Yesterday and week days
2629 } else if (daysDistance < 14) {
2630 groupInfo.comparable = 10; // One Week Ago
2631 } else if (daysDistance < 21) {
2632 groupInfo.comparable = 20; // Two Weeks Ago
2633 } else if (daysDistance < 28) {
2634 groupInfo.comparable = 30; // Three Weeks Ago
2635 } else {
2636 groupInfo.comparable = 40; // Earlier This Month
2637 }
2638 } else {
2639 const QDate lastMonthDate = currentDate.addMonths(-1);
2640 if (lastMonthDate.year() == fileDate.year() && lastMonthDate.month() == fileDate.month()) {
2641 if (daysDistance < 7) {
2642 groupInfo.comparable = daysDistance; // Today, Yesterday and week days (Month, Year)
2643 } else if (daysDistance < 14) {
2644 groupInfo.comparable = 11; // One Week Ago (Month, Year)
2645 } else if (daysDistance < 21) {
2646 groupInfo.comparable = 21; // Two Weeks Ago (Month, Year)
2647 } else if (daysDistance < 28) {
2648 groupInfo.comparable = 31; // Three Weeks Ago (Month, Year)
2649 } else {
2650 groupInfo.comparable = 41; // Earlier on Month, Year
2651 }
2652 } else {
2653 // The trick will fail for dates past April, 178956967 or before 1 AD.
2654 groupInfo.comparable = 2147483647 - (fileDate.year() * 12 + fileDate.month() - 1); // Month, Year; newer < older
2655 }
2656 }
2657 if (withString) {
2658 if (currentDate.year() == fileDate.year() && currentDate.month() == fileDate.month()) {
2659 switch (daysDistance / 7) {
2660 case 0:
2661 switch (daysDistance) {
2662 case 0:
2663 groupInfo.text = i18nc("@title:group Date", "Today");
2664 break;
2665 case 1:
2666 groupInfo.text = i18nc("@title:group Date", "Yesterday");
2667 break;
2668 default:
2669 newGroupValue = locale.toString(fileTime, i18nc("@title:group Date: The week day name: dddd", "dddd"));
2670 newGroupValue = i18nc(
2671 "Can be used to script translation of \"dddd\""
2672 "with context @title:group Date",
2673 "%1",
2674 groupInfo.text);
2675 }
2676 break;
2677 case 1:
2678 groupInfo.text = i18nc("@title:group Date", "One Week Ago");
2679 break;
2680 case 2:
2681 groupInfo.text = i18nc("@title:group Date", "Two Weeks Ago");
2682 break;
2683 case 3:
2684 groupInfo.text = i18nc("@title:group Date", "Three Weeks Ago");
2685 break;
2686 case 4:
2687 case 5:
2688 groupInfo.text = i18nc("@title:group Date", "Earlier this Month");
2689 break;
2690 default:
2691 Q_ASSERT(false);
2692 }
2693 } else {
2694 const QDate lastMonthDate = currentDate.addMonths(-1);
2695 if (lastMonthDate.year() == fileDate.year() && lastMonthDate.month() == fileDate.month()) {
2696 if (daysDistance == 1) {
2697 const KLocalizedString format = ki18nc(
2698 "@title:group Date: "
2699 "MMMM is full month name in current locale, and yyyy is "
2700 "full year number. You must keep the ' don't use any fancy \" or « or similar. The ' is not shown to the user, it's there to mark a "
2701 "part of the text that should not be formatted as a date",
2702 "'Yesterday' (MMMM, yyyy)");
2703 const QString translatedFormat = format.toString();
2704 if (const int count = translatedFormat.count(QLatin1Char('\'')); count >= 2 && count % 2 == 0) {
2705 newGroupValue = locale.toString(fileTime, translatedFormat);
2706 newGroupValue = i18nc(
2707 "Can be used to script translation of "
2708 "\"'Yesterday' (MMMM, yyyy)\" with context @title:group Date",
2709 "%1",
2710 groupInfo.text);
2711 } else {
2712 qCWarning(DolphinDebug).nospace()
2713 << "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org";
2714 const QString untranslatedFormat = format.toString({QLatin1String("en_US")});
2715 newGroupValue = locale.toString(fileTime, untranslatedFormat);
2716 }
2717 } else if (daysDistance <= 7) {
2718 newGroupValue = locale.toString(fileTime,
2719 i18nc("@title:group Date: "
2720 "The week day name: dddd, MMMM is full month name "
2721 "in current locale, and yyyy is full year number.",
2722 "dddd (MMMM, yyyy)"));
2723 newGroupValue = i18nc(
2724 "Can be used to script translation of "
2725 "\"dddd (MMMM, yyyy)\" with context @title:group Date",
2726 "%1",
2727 groupInfo.text);
2728 } else if (daysDistance < 7 * 2) {
2729 const KLocalizedString format = ki18nc(
2730 "@title:group Date: "
2731 "MMMM is full month name in current locale, and yyyy is "
2732 "full year number. You must keep the ' don't use any fancy \" or « or similar. The ' is not shown to the user, it's there to mark a "
2733 "part of the text that should not be formatted as a date",
2734 "'One Week Ago' (MMMM, yyyy)");
2735 const QString translatedFormat = format.toString();
2736 if (const int count = translatedFormat.count(QLatin1Char('\'')); count >= 2 && count % 2 == 0) {
2737 newGroupValue = locale.toString(fileTime, translatedFormat);
2738 newGroupValue = i18nc(
2739 "Can be used to script translation of "
2740 "\"'One Week Ago' (MMMM, yyyy)\" with context @title:group Date",
2741 "%1",
2742 groupInfo.text);
2743 } else {
2744 qCWarning(DolphinDebug).nospace()
2745 << "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org";
2746 const QString untranslatedFormat = format.toString({QLatin1String("en_US")});
2747 newGroupValue = locale.toString(fileTime, untranslatedFormat);
2748 }
2749 } else if (daysDistance < 7 * 3) {
2750 const KLocalizedString format = ki18nc(
2751 "@title:group Date: "
2752 "MMMM is full month name in current locale, and yyyy is "
2753 "full year number. You must keep the ' don't use any fancy \" or « or similar. The ' is not shown to the user, it's there to mark a "
2754 "part of the text that should not be formatted as a date",
2755 "'Two Weeks Ago' (MMMM, yyyy)");
2756 const QString translatedFormat = format.toString();
2757 if (const int count = translatedFormat.count(QLatin1Char('\'')); count >= 2 && count % 2 == 0) {
2758 newGroupValue = locale.toString(fileTime, translatedFormat);
2759 newGroupValue = i18nc(
2760 "Can be used to script translation of "
2761 "\"'Two Weeks Ago' (MMMM, yyyy)\" with context @title:group Date",
2762 "%1",
2763 groupInfo.text);
2764 } else {
2765 qCWarning(DolphinDebug).nospace()
2766 << "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org";
2767 const QString untranslatedFormat = format.toString({QLatin1String("en_US")});
2768 newGroupValue = locale.toString(fileTime, untranslatedFormat);
2769 }
2770 } else if (daysDistance < 7 * 4) {
2771 const KLocalizedString format = ki18nc(
2772 "@title:group Date: "
2773 "MMMM is full month name in current locale, and yyyy is "
2774 "full year number. You must keep the ' don't use any fancy \" or « or similar. The ' is not shown to the user, it's there to mark a "
2775 "part of the text that should not be formatted as a date",
2776 "'Three Weeks Ago' (MMMM, yyyy)");
2777 const QString translatedFormat = format.toString();
2778 if (const int count = translatedFormat.count(QLatin1Char('\'')); count >= 2 && count % 2 == 0) {
2779 newGroupValue = locale.toString(fileTime, translatedFormat);
2780 newGroupValue = i18nc(
2781 "Can be used to script translation of "
2782 "\"'Three Weeks Ago' (MMMM, yyyy)\" with context @title:group Date",
2783 "%1",
2784 groupInfo.text);
2785 } else {
2786 qCWarning(DolphinDebug).nospace()
2787 << "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org";
2788 const QString untranslatedFormat = format.toString({QLatin1String("en_US")});
2789 newGroupValue = locale.toString(fileTime, untranslatedFormat);
2790 }
2791 } else {
2792 const KLocalizedString format = ki18nc(
2793 "@title:group Date: "
2794 "MMMM is full month name in current locale, and yyyy is "
2795 "full year number. You must keep the ' don't use any fancy \" or « or similar. The ' is not shown to the user, it's there to mark a "
2796 "part of the text that should not be formatted as a date",
2797 "'Earlier on' MMMM, yyyy");
2798 const QString translatedFormat = format.toString();
2799 if (const int count = translatedFormat.count(QLatin1Char('\'')); count >= 2 && count % 2 == 0) {
2800 newGroupValue = locale.toString(fileTime, translatedFormat);
2801 newGroupValue = i18nc(
2802 "Can be used to script translation of "
2803 "\"'Earlier on' MMMM, yyyy\" with context @title:group Date",
2804 "%1",
2805 groupInfo.text);
2806 } else {
2807 qCWarning(DolphinDebug).nospace()
2808 << "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org";
2809 const QString untranslatedFormat = format.toString({QLatin1String("en_US")});
2810 newGroupValue = locale.toString(fileTime, untranslatedFormat);
2811 }
2812 }
2813 } else {
2814 newGroupValue = locale.toString(fileTime,
2815 i18nc("@title:group "
2816 "The month and year: MMMM is full month name in current locale, "
2817 "and yyyy is full year number",
2818 "MMMM, yyyy"));
2819 newGroupValue = i18nc(
2820 "Can be used to script translation of "
2821 "\"MMMM, yyyy\" with context @title:group Date",
2822 "%1",
2823 groupInfo.text);
2824 }
2825 }
2826 }
2827 oldWithString = withString;
2828 oldFileDate = fileDate;
2829 oldGroupInfo = groupInfo;
2830 return groupInfo;
2831 }
2832
2833 KFileItemModel::ItemGroupInfo KFileItemModel::permissionRoleGroup(const ItemData *itemData, bool withString) const
2834 {
2835 static bool oldWithString;
2836 static ItemGroupInfo oldGroupInfo;
2837 static QFileDevice::Permissions oldPermissions;
2838 ItemGroupInfo groupInfo;
2839
2840 const QFileInfo info(itemData->item.url().toLocalFile());
2841 const QFileDevice::Permissions permissions = info.permissions();
2842 if (permissions == oldPermissions && withString == oldWithString) {
2843 return oldGroupInfo;
2844 }
2845 groupInfo.comparable = (int)permissions;
2846
2847 if (withString) {
2848 // Set user string
2849 QString user;
2850 if (permissions & QFile::ReadUser) {
2851 user = i18nc("@item:intext Access permission, concatenated", "Read, ");
2852 }
2853 if (permissions & QFile::WriteUser) {
2854 user += i18nc("@item:intext Access permission, concatenated", "Write, ");
2855 }
2856 if (permissions & QFile::ExeUser) {
2857 user += i18nc("@item:intext Access permission, concatenated", "Execute, ");
2858 }
2859 user = user.isEmpty() ? i18nc("@item:intext Access permission, concatenated", "Forbidden") : user.mid(0, user.length() - 2);
2860
2861 // Set group string
2862 QString group;
2863 if (permissions & QFile::ReadGroup) {
2864 group = i18nc("@item:intext Access permission, concatenated", "Read, ");
2865 }
2866 if (permissions & QFile::WriteGroup) {
2867 group += i18nc("@item:intext Access permission, concatenated", "Write, ");
2868 }
2869 if (permissions & QFile::ExeGroup) {
2870 group += i18nc("@item:intext Access permission, concatenated", "Execute, ");
2871 }
2872 group = group.isEmpty() ? i18nc("@item:intext Access permission, concatenated", "Forbidden") : group.mid(0, group.length() - 2);
2873
2874 // Set others string
2875 QString others;
2876 if (permissions & QFile::ReadOther) {
2877 others = i18nc("@item:intext Access permission, concatenated", "Read, ");
2878 }
2879 if (permissions & QFile::WriteOther) {
2880 others += i18nc("@item:intext Access permission, concatenated", "Write, ");
2881 }
2882 if (permissions & QFile::ExeOther) {
2883 others += i18nc("@item:intext Access permission, concatenated", "Execute, ");
2884 }
2885 others = others.isEmpty() ? i18nc("@item:intext Access permission, concatenated", "Forbidden") : others.mid(0, others.length() - 2);
2886 groupInfo.text = i18nc("@title:group Files and folders by permissions", "User: %1 | Group: %2 | Others: %3", user, group, others);
2887 }
2888 oldWithString = withString;
2889 oldPermissions = permissions;
2890 oldGroupInfo = groupInfo;
2891 return groupInfo;
2892 }
2893
2894 KFileItemModel::ItemGroupInfo KFileItemModel::ratingRoleGroup(const ItemData *itemData, bool withString) const
2895 {
2896 ItemGroupInfo groupInfo;
2897 groupInfo.comparable = itemData->values.value("rating", 0).toInt();
2898 if (withString) {
2899 // Dolphin does not currently use string representation of star rating
2900 // as stars are rendered as graphics in group headers.
2901 groupInfo.text = i18nc("@item:intext Rated N (stars)", "Rated %i", QString::number(groupInfo.comparable));
2902 }
2903 return groupInfo;
2904 }
2905
2906 KFileItemModel::ItemGroupInfo KFileItemModel::genericStringRoleGroup(const QByteArray &role, const ItemData *itemData) const
2907 {
2908 return {0, itemData->values.value(role).toString()};
2909 }
2910
2911 QList<QPair<int, QVariant>> KFileItemModel::nameRoleGroups() const
2912 {
2913 Q_ASSERT(!m_itemData.isEmpty());
2914
2915 const int maxIndex = count() - 1;
2916 QList<QPair<int, QVariant>> groups;
2917
2918 ItemGroupInfo groupInfo;
2919 for (int i = 0; i <= maxIndex; ++i) {
2920 if (isChildItem(i)) {
2921 continue;
2922 }
2923
2924 ItemGroupInfo newGroupInfo = nameRoleGroup(m_itemData.at(i));
2925
2926 if (newGroupInfo != groupInfo) {
2927 groupInfo = newGroupInfo;
2928 groups.append(QPair<int, QVariant>(i, newGroupInfo.text));
2929 }
2930 }
2931 return groups;
2932 }
2933
2934 QList<QPair<int, QVariant>> KFileItemModel::sizeRoleGroups() const
2935 {
2936 Q_ASSERT(!m_itemData.isEmpty());
2937
2938 const int maxIndex = count() - 1;
2939 QList<QPair<int, QVariant>> groups;
2940
2941 ItemGroupInfo groupInfo;
2942 for (int i = 0; i <= maxIndex; ++i) {
2943 if (isChildItem(i)) {
2944 continue;
2945 }
2946
2947 ItemGroupInfo newGroupInfo = sizeRoleGroup(m_itemData.at(i));
2948
2949 if (newGroupInfo != groupInfo) {
2950 groupInfo = newGroupInfo;
2951 groups.append(QPair<int, QVariant>(i, newGroupInfo.text));
2952 }
2953 }
2954 return groups;
2955 }
2956
2957 KFileItemModel::ItemGroupInfo KFileItemModel::typeRoleGroup(const ItemData *itemData) const
2958 {
2959 int priority = 0;
2960 if (itemData->item.isDir() && m_sortDirsFirst) {
2961 // Ensure folders stay first regardless of grouping order
2962 if (groupOrder() == Qt::AscendingOrder) {
2963 priority = -1;
2964 } else {
2965 priority = 1;
2966 }
2967 }
2968 return {priority, itemData->values.value("type").toString()};
2969 }
2970
2971 QList<QPair<int, QVariant>> KFileItemModel::timeRoleGroups(const std::function<QDateTime(const ItemData *)> &fileTimeCb) const
2972 {
2973 Q_ASSERT(!m_itemData.isEmpty());
2974
2975 const int maxIndex = count() - 1;
2976 QList<QPair<int, QVariant>> groups;
2977
2978 ItemGroupInfo groupInfo;
2979 for (int i = 0; i <= maxIndex; ++i) {
2980 if (isChildItem(i)) {
2981 continue;
2982 }
2983
2984 ItemGroupInfo newGroupInfo = timeRoleGroup(fileTimeCb, m_itemData.at(i));
2985
2986 if (newGroupInfo != groupInfo) {
2987 groupInfo = newGroupInfo;
2988 groups.append(QPair<int, QVariant>(i, newGroupInfo.text));
2989 }
2990 }
2991 return groups;
2992 }
2993
2994 QList<QPair<int, QVariant>> KFileItemModel::permissionRoleGroups() const
2995 {
2996 Q_ASSERT(!m_itemData.isEmpty());
2997
2998 const int maxIndex = count() - 1;
2999 QList<QPair<int, QVariant>> groups;
3000
3001 ItemGroupInfo groupInfo;
3002 for (int i = 0; i <= maxIndex; ++i) {
3003 if (isChildItem(i)) {
3004 continue;
3005 }
3006
3007 ItemGroupInfo newGroupInfo = permissionRoleGroup(m_itemData.at(i));
3008
3009 if (newGroupInfo != groupInfo) {
3010 groupInfo = newGroupInfo;
3011 groups.append(QPair<int, QVariant>(i, newGroupInfo.text));
3012 }
3013 }
3014 return groups;
3015 }
3016
3017 QList<QPair<int, QVariant>> KFileItemModel::ratingRoleGroups() const
3018 {
3019 Q_ASSERT(!m_itemData.isEmpty());
3020
3021 const int maxIndex = count() - 1;
3022 QList<QPair<int, QVariant>> groups;
3023
3024 ItemGroupInfo groupInfo;
3025 for (int i = 0; i <= maxIndex; ++i) {
3026 if (isChildItem(i)) {
3027 continue;
3028 }
3029
3030 ItemGroupInfo newGroupInfo = ratingRoleGroup(m_itemData.at(i));
3031
3032 if (newGroupInfo != groupInfo) {
3033 groupInfo = newGroupInfo;
3034 // Using the numeric representation because Dolphin has a special
3035 // case for drawing stars.
3036 groups.append(QPair<int, QVariant>(i, newGroupInfo.comparable));
3037 }
3038 }
3039 return groups;
3040 }
3041
3042 QList<QPair<int, QVariant>> KFileItemModel::genericStringRoleGroups(const QByteArray &role) const
3043 {
3044 Q_ASSERT(!m_itemData.isEmpty());
3045
3046 const int maxIndex = count() - 1;
3047 QList<QPair<int, QVariant>> groups;
3048
3049 ItemGroupInfo groupInfo;
3050 for (int i = 0; i <= maxIndex; ++i) {
3051 if (isChildItem(i)) {
3052 continue;
3053 }
3054
3055 ItemGroupInfo newGroupInfo = genericStringRoleGroup(role, m_itemData.at(i));
3056
3057 if (newGroupInfo != groupInfo) {
3058 groupInfo = newGroupInfo;
3059 groups.append(QPair<int, QVariant>(i, newGroupInfo.text));
3060 }
3061 }
3062 return groups;
3063 }
3064
3065 void KFileItemModel::emitSortProgress(int resolvedCount)
3066 {
3067 // Be tolerant against a resolvedCount with a wrong range.
3068 // Although there should not be a case where KFileItemModelRolesUpdater
3069 // (= caller) provides a wrong range, it is important to emit
3070 // a useful progress information even if there is an unexpected
3071 // implementation issue.
3072
3073 const int itemCount = count();
3074 if (resolvedCount >= itemCount) {
3075 m_sortingProgressPercent = -1;
3076 if (m_resortAllItemsTimer->isActive()) {
3077 m_resortAllItemsTimer->stop();
3078 resortAllItems();
3079 }
3080
3081 Q_EMIT directorySortingProgress(100);
3082 } else if (itemCount > 0) {
3083 resolvedCount = qBound(0, resolvedCount, itemCount);
3084
3085 const int progress = resolvedCount * 100 / itemCount;
3086 if (m_sortingProgressPercent != progress) {
3087 m_sortingProgressPercent = progress;
3088 Q_EMIT directorySortingProgress(progress);
3089 }
3090 }
3091 }
3092
3093 const KFileItemModel::RoleInfoMap *KFileItemModel::rolesInfoMap(int &count)
3094 {
3095 static const RoleInfoMap rolesInfoMap[] = {
3096 // clang-format off
3097 // | role | roleType | role translation | group translation | requires Baloo | requires indexer
3098 { nullptr, NoRole, kli18nc("@label", "None"), KLazyLocalizedString(), KLazyLocalizedString(), false, false },
3099 { "text", NameRole, kli18nc("@label", "Name"), KLazyLocalizedString(), KLazyLocalizedString(), false, false },
3100 { "size", SizeRole, kli18nc("@label", "Size"), KLazyLocalizedString(), KLazyLocalizedString(), false, false },
3101 { "modificationtime", ModificationTimeRole, kli18nc("@label", "Modified"), KLazyLocalizedString(), kli18nc("@tooltip", "The date format can be selected in settings."), false, false },
3102 { "creationtime", CreationTimeRole, kli18nc("@label", "Created"), KLazyLocalizedString(), kli18nc("@tooltip", "The date format can be selected in settings."), false, false },
3103 { "accesstime", AccessTimeRole, kli18nc("@label", "Accessed"), KLazyLocalizedString(), kli18nc("@tooltip", "The date format can be selected in settings."), false, false },
3104 { "type", TypeRole, kli18nc("@label", "Type"), KLazyLocalizedString(), KLazyLocalizedString(), false, false },
3105 { "rating", RatingRole, kli18nc("@label", "Rating"), KLazyLocalizedString(), KLazyLocalizedString(), true, false },
3106 { "tags", TagsRole, kli18nc("@label", "Tags"), KLazyLocalizedString(), KLazyLocalizedString(), true, false },
3107 { "comment", CommentRole, kli18nc("@label", "Comment"), KLazyLocalizedString(), KLazyLocalizedString(), true, false },
3108 { "title", TitleRole, kli18nc("@label", "Title"), kli18nc("@label", "Document"), KLazyLocalizedString(), true, true },
3109 { "author", AuthorRole, kli18nc("@label", "Author"), kli18nc("@label", "Document"), KLazyLocalizedString(), true, true },
3110 { "publisher", PublisherRole, kli18nc("@label", "Publisher"), kli18nc("@label", "Document"), KLazyLocalizedString(), true, true },
3111 { "pageCount", PageCountRole, kli18nc("@label", "Page Count"), kli18nc("@label", "Document"), KLazyLocalizedString(), true, true },
3112 { "wordCount", WordCountRole, kli18nc("@label", "Word Count"), kli18nc("@label", "Document"), KLazyLocalizedString(), true, true },
3113 { "lineCount", LineCountRole, kli18nc("@label", "Line Count"), kli18nc("@label", "Document"), KLazyLocalizedString(), true, true },
3114 { "imageDateTime", ImageDateTimeRole, kli18nc("@label", "Date Photographed"), kli18nc("@label", "Image"), KLazyLocalizedString(), true, true },
3115 { "dimensions", DimensionsRole, kli18nc("@label width x height", "Dimensions"), kli18nc("@label", "Image"), KLazyLocalizedString(), true, true },
3116 { "width", WidthRole, kli18nc("@label", "Width"), kli18nc("@label", "Image"), KLazyLocalizedString(), true, true },
3117 { "height", HeightRole, kli18nc("@label", "Height"), kli18nc("@label", "Image"), KLazyLocalizedString(), true, true },
3118 { "orientation", OrientationRole, kli18nc("@label", "Orientation"), kli18nc("@label", "Image"), KLazyLocalizedString(), true, true },
3119 { "artist", ArtistRole, kli18nc("@label", "Artist"), kli18nc("@label", "Audio"), KLazyLocalizedString(), true, true },
3120 { "genre", GenreRole, kli18nc("@label", "Genre"), kli18nc("@label", "Audio"), KLazyLocalizedString(), true, true },
3121 { "album", AlbumRole, kli18nc("@label", "Album"), kli18nc("@label", "Audio"), KLazyLocalizedString(), true, true },
3122 { "duration", DurationRole, kli18nc("@label", "Duration"), kli18nc("@label", "Audio"), KLazyLocalizedString(), true, true },
3123 { "bitrate", BitrateRole, kli18nc("@label", "Bitrate"), kli18nc("@label", "Audio"), KLazyLocalizedString(), true, true },
3124 { "track", TrackRole, kli18nc("@label", "Track"), kli18nc("@label", "Audio"), KLazyLocalizedString(), true, true },
3125 { "releaseYear", ReleaseYearRole, kli18nc("@label", "Release Year"), kli18nc("@label", "Audio"), KLazyLocalizedString(), true, true },
3126 { "aspectRatio", AspectRatioRole, kli18nc("@label", "Aspect Ratio"), kli18nc("@label", "Video"), KLazyLocalizedString(), true, true },
3127 { "frameRate", FrameRateRole, kli18nc("@label", "Frame Rate"), kli18nc("@label", "Video"), KLazyLocalizedString(), true, true },
3128 { "duration", DurationRole, kli18nc("@label", "Duration"), kli18nc("@label", "Video"), KLazyLocalizedString(), true, true },
3129 { "path", PathRole, kli18nc("@label", "Path"), kli18nc("@label", "Other"), KLazyLocalizedString(), false, false },
3130 { "extension", ExtensionRole, kli18nc("@label", "File Extension"), kli18nc("@label", "Other"), KLazyLocalizedString(), false, false },
3131 { "deletiontime", DeletionTimeRole, kli18nc("@label", "Deletion Time"), kli18nc("@label", "Other"), KLazyLocalizedString(), false, false },
3132 { "destination", DestinationRole, kli18nc("@label", "Link Destination"), kli18nc("@label", "Other"), KLazyLocalizedString(), false, false },
3133 { "originUrl", OriginUrlRole, kli18nc("@label", "Downloaded From"), kli18nc("@label", "Other"), KLazyLocalizedString(), true, false },
3134 { "permissions", PermissionsRole, kli18nc("@label", "Permissions"), kli18nc("@label", "Other"), kli18nc("@tooltip", "The permission format can be changed in settings. Options are Symbolic, Numeric (Octal) or Combined formats"), false, false },
3135 { "owner", OwnerRole, kli18nc("@label", "Owner"), kli18nc("@label", "Other"), KLazyLocalizedString(), false, false },
3136 { "group", GroupRole, kli18nc("@label", "User Group"), kli18nc("@label", "Other"), KLazyLocalizedString(), false, false },
3137 };
3138 // clang-format on
3139
3140 count = sizeof(rolesInfoMap) / sizeof(RoleInfoMap);
3141 return rolesInfoMap;
3142 }
3143
3144 void KFileItemModel::determineMimeTypes(const KFileItemList &items, int timeout)
3145 {
3146 QElapsedTimer timer;
3147 timer.start();
3148 for (const KFileItem &item : items) {
3149 // Only determine mime types for files here. For directories,
3150 // KFileItem::determineMimeType() reads the .directory file inside to
3151 // load the icon, but this is not necessary at all if we just need the
3152 // type. Some special code for setting the correct mime type for
3153 // directories is in retrieveData().
3154 if (!item.isDir()) {
3155 item.determineMimeType();
3156 }
3157
3158 if (timer.elapsed() > timeout) {
3159 // Don't block the user interface, let the remaining items
3160 // be resolved asynchronously.
3161 return;
3162 }
3163 }
3164 }
3165
3166 QByteArray KFileItemModel::sharedValue(const QByteArray &value)
3167 {
3168 static QSet<QByteArray> pool;
3169 const QSet<QByteArray>::const_iterator it = pool.constFind(value);
3170
3171 if (it != pool.constEnd()) {
3172 return *it;
3173 } else {
3174 pool.insert(value);
3175 return value;
3176 }
3177 }
3178
3179 bool KFileItemModel::isConsistent() const
3180 {
3181 // m_items may contain less items than m_itemData because m_items
3182 // is populated lazily, see KFileItemModel::index(const QUrl& url).
3183 if (m_items.count() > m_itemData.count()) {
3184 return false;
3185 }
3186
3187 for (int i = 0, iMax = count(); i < iMax; ++i) {
3188 // Check if m_items and m_itemData are consistent.
3189 const KFileItem item = fileItem(i);
3190 if (item.isNull()) {
3191 qCWarning(DolphinDebug) << "Item" << i << "is null";
3192 return false;
3193 }
3194
3195 const int itemIndex = index(item);
3196 if (itemIndex != i) {
3197 qCWarning(DolphinDebug) << "Item" << i << "has a wrong index:" << itemIndex;
3198 return false;
3199 }
3200
3201 // Check if the items are sorted correctly.
3202 if (i > 0 && !lessThan(m_itemData.at(i - 1), m_itemData.at(i), m_collator)) {
3203 qCWarning(DolphinDebug) << "The order of items" << i - 1 << "and" << i << "is wrong:" << fileItem(i - 1) << fileItem(i);
3204 return false;
3205 }
3206
3207 // Check if all parent-child relationships are consistent.
3208 const ItemData *data = m_itemData.at(i);
3209 const ItemData *parent = data->parent;
3210 if (parent) {
3211 if (expandedParentsCount(data) != expandedParentsCount(parent) + 1) {
3212 qCWarning(DolphinDebug) << "expandedParentsCount is inconsistent for parent" << parent->item << "and child" << data->item;
3213 return false;
3214 }
3215
3216 const int parentIndex = index(parent->item);
3217 if (parentIndex >= i) {
3218 qCWarning(DolphinDebug) << "Index" << parentIndex << "of parent" << parent->item << "is not smaller than index" << i << "of child"
3219 << data->item;
3220 return false;
3221 }
3222 }
3223 }
3224
3225 return true;
3226 }
3227
3228 void KFileItemModel::slotListerError(KIO::Job *job)
3229 {
3230 const int jobError = job->error();
3231 if (jobError == KIO::ERR_IS_FILE) {
3232 if (auto *listJob = qobject_cast<KIO::ListJob *>(job)) {
3233 Q_EMIT urlIsFileError(listJob->url());
3234 }
3235 } else {
3236 const QString errorString = job->errorString();
3237 Q_EMIT errorMessage(!errorString.isEmpty() ? errorString : i18nc("@info:status", "Unknown error."), jobError);
3238 }
3239 }
3240
3241 #include "moc_kfileitemmodel.cpp"