+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;
+ }
+ }
+ break;
+ }
+
+ KIO::filesize_t sizeA = 0;
+ if (itemA.isDir()) {
+ sizeA = a->values.value("size").toULongLong();
+ } else {
+ sizeA = itemA.size();
+ }
+ KIO::filesize_t sizeB = 0;
+ if (itemB.isDir()) {
+ sizeB = b->values.value("size").toULongLong();
+ } else {
+ sizeB = itemB.size();
+ }
+ if (sizeA < sizeB) {
+ return -1;
+ } else if (sizeA > sizeB) {
+ return +1;
+ }
+ break;
+ }
+
+ case ModificationTimeRole: {
+ const long long dateTimeA = itemA.entry().numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME, -1);
+ const long long dateTimeB = itemB.entry().numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME, -1);
+ if (dateTimeA < dateTimeB) {
+ return -1;
+ } else if (dateTimeA > dateTimeB) {
+ return +1;
+ }
+ break;
+ }
+
+ case AccessTimeRole: {
+ const long long dateTimeA = itemA.entry().numberValue(KIO::UDSEntry::UDS_ACCESS_TIME, -1);
+ const long long dateTimeB = itemB.entry().numberValue(KIO::UDSEntry::UDS_ACCESS_TIME, -1);
+ if (dateTimeA < dateTimeB) {
+ return -1;
+ } else if (dateTimeA > dateTimeB) {
+ return +1;
+ }
+ break;
+ }
+
+ case CreationTimeRole: {
+ const long long dateTimeA = itemA.entry().numberValue(KIO::UDSEntry::UDS_CREATION_TIME, -1);
+ const long long dateTimeB = itemB.entry().numberValue(KIO::UDSEntry::UDS_CREATION_TIME, -1);
+ if (dateTimeA < dateTimeB) {
+ return -1;
+ } else if (dateTimeA > dateTimeB) {
+ return +1;
+ }
+ break;
+ }
+
+ case DeletionTimeRole: {
+ const QDateTime dateTimeA = a->values.value("deletiontime").toDateTime();
+ const QDateTime dateTimeB = b->values.value("deletiontime").toDateTime();
+ if (dateTimeA < dateTimeB) {
+ return -1;
+ } else if (dateTimeA > dateTimeB) {
+ return +1;
+ }
+ break;
+ }
+
+ case RatingRole:
+ case WidthRole:
+ case HeightRole:
+ case PublisherRole:
+ case PageCountRole:
+ case WordCountRole:
+ case LineCountRole:
+ case TrackRole:
+ case ReleaseYearRole: {
+ result = a->values.value(roleForType(m_sortRole)).toInt() - b->values.value(roleForType(m_sortRole)).toInt();
+ break;
+ }
+
+ case DimensionsRole: {
+ const QByteArray role = roleForType(m_sortRole);
+ const QSize dimensionsA = a->values.value(role).toSize();
+ const QSize dimensionsB = b->values.value(role).toSize();
+
+ if (dimensionsA.width() == dimensionsB.width()) {
+ result = dimensionsA.height() - dimensionsB.height();
+ } else {
+ result = dimensionsA.width() - dimensionsB.width();
+ }
+ break;
+ }
+
+ default: {
+ const QByteArray role = roleForType(m_sortRole);
+ const QString roleValueA = a->values.value(role).toString();
+ const QString roleValueB = b->values.value(role).toString();
+ if (!roleValueA.isEmpty() && roleValueB.isEmpty()) {
+ return -1;
+ } else if (roleValueA.isEmpty() && !roleValueB.isEmpty()) {
+ return +1;
+ } else if (isRoleValueNatural(m_sortRole)) {
+ result = stringCompare(roleValueA, roleValueB, collator);
+ } else {
+ result = QString::compare(roleValueA, roleValueB);
+ }
+ break;
+ }
+ }
+
+ if (result != 0) {
+ // The current sort role was sufficient to define an order
+ return result;
+ }
+
+ // Fallback #1: Compare the text of the items
+ result = stringCompare(itemA.text(), itemB.text(), collator);
+ if (result != 0) {
+ return result;
+ }
+
+ // Fallback #2: KFileItem::text() may not be unique in case UDS_DISPLAY_NAME is used
+ result = stringCompare(itemA.name(), itemB.name(), collator);
+ if (result != 0) {
+ return result;
+ }
+
+ // Fallback #3: It must be assured that the sort order is always unique even if two values have been
+ // equal. In this case a comparison of the URL is done which is unique in all cases
+ // within KDirLister.
+ return QString::compare(itemA.url().url(), itemB.url().url(), Qt::CaseSensitive);
+}
+
+int KFileItemModel::stringCompare(const QString &a, const QString &b, const QCollator &collator) const
+{
+ QMutexLocker collatorLock(s_collatorMutex());
+
+ if (m_naturalSorting) {
+ return collator.compare(a, b);
+ }
+
+ const int result = QString::compare(a, b, collator.caseSensitivity());
+ if (result != 0 || collator.caseSensitivity() == Qt::CaseSensitive) {
+ // Only return the result, if the strings are not equal. If they are equal by a case insensitive
+ // comparison, still a deterministic sort order is required. A case sensitive
+ // comparison is done as fallback.
+ return result;
+ }
+
+ return QString::compare(a, b, Qt::CaseSensitive);
+}
+
+QList<QPair<int, QVariant>> KFileItemModel::nameRoleGroups() const
+{
+ Q_ASSERT(!m_itemData.isEmpty());
+
+ const int maxIndex = count() - 1;
+ QList<QPair<int, QVariant>> groups;
+
+ QString groupValue;
+ QChar firstChar;
+ for (int i = 0; i <= maxIndex; ++i) {
+ if (isChildItem(i)) {
+ continue;
+ }
+
+ const QString name = m_itemData.at(i)->item.text();
+
+ // Use the first character of the name as group indication
+ QChar newFirstChar = name.at(0).toUpper();
+ if (newFirstChar == QLatin1Char('~') && name.length() > 1) {
+ newFirstChar = name.at(1).toUpper();
+ }
+
+ if (firstChar != newFirstChar) {