+ return itemDataList;
+}
+
+void KFileItemModel::prepareItemsForSorting(QList<ItemData *> &itemDataList)
+{
+ switch (m_sortRole) {
+ case ExtensionRole:
+ case PermissionsRole:
+ case OwnerRole:
+ case GroupRole:
+ case DestinationRole:
+ case PathRole:
+ case DeletionTimeRole:
+ // These roles can be determined with retrieveData, and they have to be stored
+ // in the QHash "values" for the sorting.
+ for (ItemData *itemData : std::as_const(itemDataList)) {
+ if (itemData->values.isEmpty()) {
+ itemData->values = retrieveData(itemData->item, itemData->parent);
+ }
+ }
+ break;
+
+ case TypeRole:
+ // At least store the data including the file type for items with known MIME type.
+ for (ItemData *itemData : std::as_const(itemDataList)) {
+ if (itemData->values.isEmpty()) {
+ const KFileItem item = itemData->item;
+ if (item.isDir() || item.isMimeTypeKnown()) {
+ itemData->values = retrieveData(itemData->item, itemData->parent);
+ }
+ }
+ }
+ break;
+
+ default:
+ // The other roles are either resolved by KFileItemModelRolesUpdater
+ // (this includes the SizeRole for directories), or they do not need
+ // to be stored in the QHash "values" for sorting because the data can
+ // be retrieved directly from the KFileItem (NameRole, SizeRole for files,
+ // DateRole).
+ break;
+ }
+}
+
+int KFileItemModel::expandedParentsCount(const ItemData *data)
+{
+ // The hash 'values' is only guaranteed to contain the key "expandedParentsCount"
+ // if the corresponding item is expanded, and it is not a top-level item.
+ const ItemData *parent = data->parent;
+ if (parent) {
+ if (parent->parent) {
+ Q_ASSERT(parent->values.contains("expandedParentsCount"));
+ return parent->values.value("expandedParentsCount").toInt() + 1;
+ } else {
+ return 1;
+ }
+ } else {
+ return 0;
+ }
+}
+
+void KFileItemModel::removeExpandedItems()
+{
+ QVector<int> indexesToRemove;
+
+ const int maxIndex = m_itemData.count() - 1;
+ for (int i = 0; i <= maxIndex; ++i) {
+ const ItemData *itemData = m_itemData.at(i);
+ if (itemData->parent) {
+ indexesToRemove.append(i);
+ }
+ }
+
+ removeItems(KItemRangeList::fromSortedContainer(indexesToRemove), DeleteItemData);
+ m_expandedDirs.clear();
+
+ // Also remove all filtered items which have a parent.
+ QHash<KFileItem, ItemData *>::iterator it = m_filteredItems.begin();
+ const QHash<KFileItem, ItemData *>::iterator end = m_filteredItems.end();
+
+ while (it != end) {
+ if (it.value()->parent) {
+ delete it.value();
+ it = m_filteredItems.erase(it);
+ } else {
+ ++it;
+ }
+ }
+}
+
+void KFileItemModel::emitItemsChangedAndTriggerResorting(const KItemRangeList &itemRanges, const QSet<QByteArray> &changedRoles)
+{
+ Q_EMIT itemsChanged(itemRanges, changedRoles);
+
+ // Trigger a resorting if necessary. Note that this can happen even if the sort
+ // role has not changed at all because the file name can be used as a fallback.
+ if (changedRoles.contains(sortRole()) || changedRoles.contains(roleForType(NameRole))
+ || (changedRoles.contains("count") && sortRole() == "size")) { // "count" is used in the "size" sort role, so this might require a resorting.
+ for (const KItemRange &range : itemRanges) {
+ bool needsResorting = false;
+
+ const int first = range.index;
+ const int last = range.index + range.count - 1;
+
+ // Resorting the model is necessary if
+ // (a) The first item in the range is "lessThan" its predecessor,
+ // (b) the successor of the last item is "lessThan" the last item, or
+ // (c) the internal order of the items in the range is incorrect.
+ if (first > 0 && lessThan(m_itemData.at(first), m_itemData.at(first - 1), m_collator)) {
+ needsResorting = true;
+ } else if (last < count() - 1 && lessThan(m_itemData.at(last + 1), m_itemData.at(last), m_collator)) {
+ needsResorting = true;
+ } else {
+ for (int index = first; index < last; ++index) {
+ if (lessThan(m_itemData.at(index + 1), m_itemData.at(index), m_collator)) {
+ needsResorting = true;
+ break;
+ }
+ }
+ }
+
+ if (needsResorting) {
+ scheduleResortAllItems();
+ return;
+ }
+ }
+ }
+
+ if (groupedSorting() && changedRoles.contains(sortRole())) {
+ // The position is still correct, but the groups might have changed
+ // if the changed item is either the first or the last item in a
+ // group.
+ // In principle, we could try to find out if the item really is the
+ // first or last one in its group and then update the groups
+ // (possibly with a delayed timer to make sure that we don't
+ // re-calculate the groups very often if items are updated one by
+ // one), but starting m_resortAllItemsTimer is easier.
+ m_resortAllItemsTimer->start();
+ }
+}
+
+void KFileItemModel::resetRoles()
+{
+ for (int i = 0; i < RolesCount; ++i) {
+ m_requestRole[i] = false;
+ }
+}
+
+KFileItemModel::RoleType KFileItemModel::typeForRole(const QByteArray &role) const
+{
+ static QHash<QByteArray, RoleType> roles;
+ if (roles.isEmpty()) {
+ // Insert user visible roles that can be accessed with
+ // KFileItemModel::roleInformation()
+ int count = 0;
+ const RoleInfoMap *map = rolesInfoMap(count);
+ for (int i = 0; i < count; ++i) {
+ roles.insert(map[i].role, map[i].roleType);
+ }
+
+ // Insert internal roles (take care to synchronize the implementation
+ // with KFileItemModel::roleForType() in case if a change is done).
+ roles.insert("isDir", IsDirRole);
+ roles.insert("isLink", IsLinkRole);
+ roles.insert("isHidden", IsHiddenRole);
+ roles.insert("isExpanded", IsExpandedRole);
+ roles.insert("isExpandable", IsExpandableRole);
+ roles.insert("expandedParentsCount", ExpandedParentsCountRole);
+
+ Q_ASSERT(roles.count() == RolesCount);
+ }
+
+ return roles.value(role, NoRole);
+}
+
+QByteArray KFileItemModel::roleForType(RoleType roleType) const
+{
+ static QHash<RoleType, QByteArray> roles;
+ if (roles.isEmpty()) {
+ // Insert user visible roles that can be accessed with
+ // KFileItemModel::roleInformation()
+ int count = 0;
+ const RoleInfoMap *map = rolesInfoMap(count);
+ for (int i = 0; i < count; ++i) {
+ roles.insert(map[i].roleType, map[i].role);
+ }
+
+ // Insert internal roles (take care to synchronize the implementation
+ // with KFileItemModel::typeForRole() in case if a change is done).
+ roles.insert(IsDirRole, "isDir");
+ roles.insert(IsLinkRole, "isLink");
+ roles.insert(IsHiddenRole, "isHidden");
+ roles.insert(IsExpandedRole, "isExpanded");
+ roles.insert(IsExpandableRole, "isExpandable");
+ roles.insert(ExpandedParentsCountRole, "expandedParentsCount");
+
+ Q_ASSERT(roles.count() == RolesCount);
+ };
+
+ return roles.value(roleType);
+}
+
+QHash<QByteArray, QVariant> KFileItemModel::retrieveData(const KFileItem &item, const ItemData *parent) const
+{
+ // It is important to insert only roles that are fast to retrieve. E.g.
+ // KFileItem::iconName() can be very expensive if the MIME-type is unknown
+ // and hence will be retrieved asynchronously by KFileItemModelRolesUpdater.
+ QHash<QByteArray, QVariant> data;
+ data.insert(sharedValue("url"), item.url());
+
+ const bool isDir = item.isDir();
+ if (m_requestRole[IsDirRole] && isDir) {
+ data.insert(sharedValue("isDir"), true);
+ }
+
+ if (m_requestRole[IsLinkRole] && item.isLink()) {
+ data.insert(sharedValue("isLink"), true);
+ }
+
+ if (m_requestRole[IsHiddenRole]) {
+ data.insert(sharedValue("isHidden"), item.isHidden() || item.mimetype() == QStringLiteral("application/x-trash"));
+ }
+
+ if (m_requestRole[NameRole]) {
+ data.insert(sharedValue("text"), item.text());
+ }
+
+ if (m_requestRole[ExtensionRole] && !isDir) {
+ // TODO KF6 use KFileItem::suffix 464722
+ data.insert(sharedValue("extension"), QFileInfo(item.name()).suffix());
+ }
+
+ if (m_requestRole[SizeRole] && !isDir) {
+ data.insert(sharedValue("size"), item.size());
+ }
+
+ if (m_requestRole[ModificationTimeRole]) {
+ // Don't use KFileItem::timeString() or KFileItem::time() as this is too expensive when
+ // having several thousands of items. Instead read the raw number from UDSEntry directly
+ // and the formatting of the date-time will be done on-demand by the view when the date will be shown.
+ const long long dateTime = item.entry().numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME, -1);
+ data.insert(sharedValue("modificationtime"), dateTime);
+ }
+
+ if (m_requestRole[CreationTimeRole]) {
+ // Don't use KFileItem::timeString() or KFileItem::time() as this is too expensive when
+ // having several thousands of items. Instead read the raw number from UDSEntry directly
+ // and the formatting of the date-time will be done on-demand by the view when the date will be shown.
+ const long long dateTime = item.entry().numberValue(KIO::UDSEntry::UDS_CREATION_TIME, -1);
+ data.insert(sharedValue("creationtime"), dateTime);
+ }
+
+ if (m_requestRole[AccessTimeRole]) {
+ // Don't use KFileItem::timeString() or KFileItem::time() as this is too expensive when
+ // having several thousands of items. Instead read the raw number from UDSEntry directly
+ // and the formatting of the date-time will be done on-demand by the view when the date will be shown.
+ const long long dateTime = item.entry().numberValue(KIO::UDSEntry::UDS_ACCESS_TIME, -1);
+ data.insert(sharedValue("accesstime"), dateTime);
+ }
+
+ if (m_requestRole[PermissionsRole]) {
+ data.insert(sharedValue("permissions"), QVariantList() << item.permissionsString() << item.permissions());
+ }
+
+ if (m_requestRole[OwnerRole]) {
+ data.insert(sharedValue("owner"), item.user());
+ }
+
+ if (m_requestRole[GroupRole]) {
+ data.insert(sharedValue("group"), item.group());
+ }
+
+ if (m_requestRole[DestinationRole]) {
+ QString destination = item.linkDest();
+ if (destination.isEmpty()) {
+ destination = QLatin1Char('-');
+ }
+ data.insert(sharedValue("destination"), destination);
+ }
+
+ if (m_requestRole[PathRole]) {
+ QString path;
+ if (item.url().scheme() == QLatin1String("trash")) {
+ path = item.entry().stringValue(KIO::UDSEntry::UDS_EXTRA);
+ } else {
+ // For performance reasons cache the home-path in a static QString
+ // (see QDir::homePath() for more details)
+ static QString homePath;
+ if (homePath.isEmpty()) {
+ homePath = QDir::homePath();
+ }
+
+ path = item.localPath();
+ if (path.startsWith(homePath)) {
+ path.replace(0, homePath.length(), QLatin1Char('~'));
+ }
+ }
+
+ const int index = path.lastIndexOf(item.text());
+ path = path.mid(0, index - 1);
+ data.insert(sharedValue("path"), path);
+ }
+
+ if (m_requestRole[DeletionTimeRole]) {
+ QDateTime deletionTime;
+ if (item.url().scheme() == QLatin1String("trash")) {
+ deletionTime = QDateTime::fromString(item.entry().stringValue(KIO::UDSEntry::UDS_EXTRA + 1), Qt::ISODate);
+ }
+ data.insert(sharedValue("deletiontime"), deletionTime);
+ }
+
+ if (m_requestRole[IsExpandableRole] && isDir) {
+ data.insert(sharedValue("isExpandable"), true);
+ }
+
+ if (m_requestRole[ExpandedParentsCountRole]) {
+ if (parent) {
+ const int level = expandedParentsCount(parent) + 1;
+ data.insert(sharedValue("expandedParentsCount"), level);
+ }
+ }
+
+ if (item.isMimeTypeKnown()) {
+ QString iconName = item.iconName();
+ if (!QIcon::hasThemeIcon(iconName)) {
+ QMimeType mimeType = QMimeDatabase().mimeTypeForName(item.mimetype());
+ iconName = mimeType.genericIconName();
+ }
+
+ data.insert(sharedValue("iconName"), iconName);
+
+ if (m_requestRole[TypeRole]) {
+ data.insert(sharedValue("type"), item.mimeComment());
+ }
+ } else if (m_requestRole[TypeRole] && isDir) {
+ static const QString folderMimeType = item.mimeComment();
+ data.insert(sharedValue("type"), folderMimeType);
+ }
+
+ return data;
+}
+
+bool KFileItemModel::lessThan(const ItemData *a, const ItemData *b, const QCollator &collator) const
+{
+ int result = 0;
+
+ if (a->parent != b->parent) {
+ const int expansionLevelA = expandedParentsCount(a);
+ const int expansionLevelB = expandedParentsCount(b);
+
+ // If b has a higher expansion level than a, check if a is a parent
+ // of b, and make sure that both expansion levels are equal otherwise.
+ for (int i = expansionLevelB; i > expansionLevelA; --i) {
+ if (b->parent == a) {
+ return true;
+ }
+ b = b->parent;
+ }
+
+ // If a has a higher expansion level than a, check if b is a parent
+ // of a, and make sure that both expansion levels are equal otherwise.
+ for (int i = expansionLevelA; i > expansionLevelB; --i) {
+ if (a->parent == b) {
+ return false;
+ }
+ a = a->parent;
+ }
+
+ Q_ASSERT(expandedParentsCount(a) == expandedParentsCount(b));
+
+ // Compare the last parents of a and b which are different.
+ while (a->parent != b->parent) {
+ a = a->parent;
+ b = b->parent;
+ }
+ }
+
+ // Show hidden files and folders last
+ if (m_sortHiddenLast) {
+ const bool isHiddenA = a->item.isHidden();
+ const bool isHiddenB = b->item.isHidden();
+ if (isHiddenA && !isHiddenB) {
+ return false;
+ } else if (!isHiddenA && isHiddenB) {
+ return true;
+ }
+ }
+
+ if (m_sortDirsFirst
+ || (ContentDisplaySettings::directorySizeMode() == ContentDisplaySettings::EnumDirectorySizeMode::ContentCount && m_sortRole == SizeRole)) {
+ const bool isDirA = a->item.isDir();
+ const bool isDirB = b->item.isDir();
+ if (isDirA && !isDirB) {
+ return true;
+ } else if (!isDirA && isDirB) {
+ return false;
+ }
+ }
+
+ result = sortRoleCompare(a, b, collator);
+
+ return (sortOrder() == Qt::AscendingOrder) ? result < 0 : result > 0;
+}
+
+void KFileItemModel::sort(const QList<KFileItemModel::ItemData *>::iterator &begin, const QList<KFileItemModel::ItemData *>::iterator &end) const
+{
+ auto lambdaLessThan = [&](const KFileItemModel::ItemData *a, const KFileItemModel::ItemData *b) {
+ return lessThan(a, b, m_collator);
+ };
+
+ if (m_sortRole == NameRole || isRoleValueNatural(m_sortRole)) {
+ // Sorting by string can be expensive, in particular if natural sorting is
+ // enabled. Use all CPU cores to speed up the sorting process.
+ static const int numberOfThreads = QThread::idealThreadCount();
+ parallelMergeSort(begin, end, lambdaLessThan, numberOfThreads);
+ } else {
+ // Sorting by other roles is quite fast. Use only one thread to prevent
+ // problems caused by non-reentrant comparison functions, see
+ // https://bugs.kde.org/show_bug.cgi?id=312679
+ mergeSort(begin, end, lambdaLessThan);
+ }
+}
+
+int KFileItemModel::sortRoleCompare(const ItemData *a, const ItemData *b, const QCollator &collator) const
+{
+ // This function must never return 0, because that would break stable
+ // sorting, which leads to all kinds of bugs.
+ // See: https://bugs.kde.org/show_bug.cgi?id=433247
+ // If two items have equal sort values, let the fallbacks at the bottom of
+ // the function handle it.
+ const KFileItem &itemA = a->item;
+ const KFileItem &itemB = b->item;
+
+ int result = 0;
+
+ switch (m_sortRole) {
+ case NameRole:
+ // The name role is handled as default fallback after the switch
+ break;
+
+ case SizeRole: {
+ if (ContentDisplaySettings::directorySizeMode() == ContentDisplaySettings::EnumDirectorySizeMode::ContentCount && itemA.isDir()) {
+ // folders first then
+ // items A and B are folders thanks to lessThan checks
+ auto valueA = a->values.value("count");
+ auto valueB = b->values.value("count");
+ if (valueA.isNull()) {
+ if (!valueB.isNull()) {
+ return -1;
+ }
+ } else if (valueB.isNull()) {
+ return +1;
+ } else {
+ if (valueA.toLongLong() < valueB.toLongLong()) {
+ return -1;
+ } else if (valueA.toLongLong() > valueB.toLongLong()) {
+ return +1;