+ // 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;
+ }
+ }
+
+ result = groupRoleCompare(a, b, collator);
+ if (result == 0) {
+ // 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);
+ result = (sortOrder() == Qt::AscendingOrder) ? result < 0 : result > 0;
+ } else {
+ result = (groupOrder() == Qt::AscendingOrder) ? result < 0 : result > 0;
+ }
+ return result;
+}
+
+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::groupRoleCompare(const ItemData *a, const ItemData *b, const QCollator &collator) const
+{
+ // Unlike sortRoleCompare, this function can and often will return 0.
+ int result = 0;
+
+ ItemGroupInfo groupA, groupB;
+ switch (m_groupRole) {
+ case NoRole:
+ // Non-trivial grouping behavior might be handled there in the future.
+ return 0;
+ case NameRole:
+ groupA = nameRoleGroup(a, false);
+ groupB = nameRoleGroup(b, false);
+ break;
+ case SizeRole:
+ groupA = sizeRoleGroup(a, false);
+ groupB = sizeRoleGroup(b, false);
+ break;
+ case ModificationTimeRole:
+ groupA = timeRoleGroup(
+ [](const ItemData *item) {
+ return item->item.time(KFileItem::ModificationTime);
+ },
+ a,
+ false);
+ groupB = timeRoleGroup(
+ [](const ItemData *item) {
+ return item->item.time(KFileItem::ModificationTime);
+ },
+ b,
+ false);
+ break;
+ case CreationTimeRole:
+ groupA = timeRoleGroup(
+ [](const ItemData *item) {
+ return item->item.time(KFileItem::CreationTime);
+ },
+ a,
+ false);
+ groupB = timeRoleGroup(
+ [](const ItemData *item) {
+ return item->item.time(KFileItem::CreationTime);
+ },
+ b,
+ false);
+ break;
+ case AccessTimeRole:
+ groupA = timeRoleGroup(
+ [](const ItemData *item) {
+ return item->item.time(KFileItem::AccessTime);
+ },
+ a,
+ false);
+ groupB = timeRoleGroup(
+ [](const ItemData *item) {
+ return item->item.time(KFileItem::AccessTime);
+ },
+ b,
+ false);
+ break;
+ case DeletionTimeRole:
+ groupA = timeRoleGroup(
+ [](const ItemData *item) {
+ return item->values.value("deletiontime").toDateTime();
+ },
+ a,
+ false);
+ groupB = timeRoleGroup(
+ [](const ItemData *item) {
+ return item->values.value("deletiontime").toDateTime();
+ },
+ b,
+ false);
+ break;
+ case PermissionsRole:
+ groupA = permissionRoleGroup(a, false);
+ groupB = permissionRoleGroup(b, false);
+ break;
+ case RatingRole:
+ groupA = ratingRoleGroup(a, false);
+ groupB = ratingRoleGroup(b, false);
+ break;
+ case TypeRole:
+ groupA = typeRoleGroup(a);
+ groupB = typeRoleGroup(b);
+ break;
+ default: {
+ groupA = genericStringRoleGroup(groupRole(), a);
+ groupB = genericStringRoleGroup(groupRole(), b);
+ break;
+ }
+ }
+ if (groupA.comparable < groupB.comparable) {
+ result = -1;
+ } else if (groupA.comparable > groupB.comparable) {
+ result = 1;
+ } else {
+ result = stringCompare(groupA.text, groupB.text, collator);
+ }
+ return result;
+}
+
+int KFileItemModel::stringCompare(const QString &a, const QString &b, const QCollator &collator) const
+{
+ QMutexLocker collatorLock(s_collatorMutex());
+
+ if (m_naturalSorting) {
+ // Split extension, taking into account it can be empty
+ constexpr QString::SectionFlags flags = QString::SectionSkipEmpty | QString::SectionIncludeLeadingSep;
+
+ // Sort by baseName first
+ const QString aBaseName = a.section('.', 0, 0, flags);
+ const QString bBaseName = b.section('.', 0, 0, flags);
+
+ const int res = collator.compare(aBaseName, bBaseName);
+ if (res != 0 || (aBaseName.length() == a.length() && bBaseName.length() == b.length())) {
+ return res;
+ }
+
+ // sliced() has undefined behavior when pos < 0 or pos > size().
+ Q_ASSERT(aBaseName.length() <= a.length() && aBaseName.length() >= 0);
+ Q_ASSERT(bBaseName.length() <= b.length() && bBaseName.length() >= 0);
+
+ // baseNames were equal, sort by extension
+ return collator.compare(a.sliced(aBaseName.length()), b.sliced(bBaseName.length()));
+ }
+
+ 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);
+}
+
+KFileItemModel::ItemGroupInfo KFileItemModel::nameRoleGroup(const ItemData *itemData, bool withString) const
+{
+ static bool oldWithString;
+ static ItemGroupInfo oldGroupInfo;
+ static QChar oldFirstChar;
+ ItemGroupInfo groupInfo;
+ QChar firstChar;
+
+ const QString name = itemData->item.text();
+
+ QMutexLocker collatorLock(s_collatorMutex());
+
+ // Use the first character of the name as group indication
+ firstChar = name.at(0).toUpper();
+
+ if (firstChar == oldFirstChar && withString == oldWithString) {
+ return oldGroupInfo;
+ }
+ if (firstChar == QLatin1Char('~') && name.length() > 1) {
+ firstChar = name.at(1).toUpper();
+ }
+ if (firstChar.isLetter()) {
+ if (m_collator.compare(firstChar, QChar(QLatin1Char('A'))) >= 0 && m_collator.compare(firstChar, QChar(QLatin1Char('Z'))) <= 0) {
+ // WARNING! Symbols based on latin 'Z' like 'Z' with acute are treated wrong as non Latin and put in a new group.
+
+ // Try to find a matching group in the range 'A' to 'Z'.
+ static std::vector<QChar> lettersAtoZ;
+ lettersAtoZ.reserve('Z' - 'A' + 1);
+ if (lettersAtoZ.empty()) {
+ for (char c = 'A'; c <= 'Z'; ++c) {
+ lettersAtoZ.push_back(QLatin1Char(c));
+ }
+ }
+
+ auto localeAwareLessThan = [this](QChar c1, QChar c2) -> bool {
+ return m_collator.compare(c1, c2) < 0;
+ };
+
+ std::vector<QChar>::iterator it = std::lower_bound(lettersAtoZ.begin(), lettersAtoZ.end(), firstChar, localeAwareLessThan);
+ if (it != lettersAtoZ.end()) {
+ if (localeAwareLessThan(firstChar, *it)) {
+ // newFirstChar belongs to the group preceding *it.
+ // Example: for an umlaut 'A' in the German locale, *it would be 'B' now.
+ --it;
+ }
+ if (withString) {
+ groupInfo.text = *it;
+ }
+ groupInfo.comparable = (*it).unicode();
+ }
+
+ } else {
+ // Symbols from non Latin-based scripts
+ if (withString) {
+ groupInfo.text = firstChar;
+ }
+ groupInfo.comparable = firstChar.unicode();
+ }
+ } else if (firstChar >= QLatin1Char('0') && firstChar <= QLatin1Char('9')) {
+ // Apply group '0 - 9' for any name that starts with a digit
+ if (withString) {
+ groupInfo.text = i18nc("@title:group Groups that start with a digit", "0 - 9");
+ }
+ groupInfo.comparable = (int)'0';
+ } else {
+ if (withString) {
+ groupInfo.text = i18nc("@title:group", "Others");
+ }
+ groupInfo.comparable = (int)'.';
+ }
+ oldWithString = withString;
+ oldFirstChar = firstChar;
+ oldGroupInfo = groupInfo;
+ return groupInfo;
+}
+
+KFileItemModel::ItemGroupInfo KFileItemModel::sizeRoleGroup(const ItemData *itemData, bool withString) const
+{
+ ItemGroupInfo groupInfo;
+ KIO::filesize_t fileSize;
+
+ const KFileItem item = itemData->item;
+ fileSize = !item.isNull() ? item.size() : ~0U;
+
+ groupInfo.comparable = -1; // None
+ if (!item.isNull() && item.isDir()) {
+ if (ContentDisplaySettings::directorySizeMode() != ContentDisplaySettings::EnumDirectorySizeMode::ContentSize) {
+ groupInfo.comparable = 0; // Folders
+ } else {
+ fileSize = itemData->values.value("size").toULongLong();
+ }
+ }
+ if (groupInfo.comparable < 0) {
+ if (fileSize < 5 * 1024 * 1024) { // < 5 MB
+ groupInfo.comparable = 1; // Small
+ } else if (fileSize < 10 * 1024 * 1024) { // < 10 MB
+ groupInfo.comparable = 2; // Medium
+ } else {
+ groupInfo.comparable = 3; // Big
+ }
+ }
+
+ if (withString) {
+ char const *groupNames[] = {"Folders", "Small", "Medium", "Big"};
+ groupInfo.text = i18nc("@title:group Size", groupNames[groupInfo.comparable]);
+ }
+ return groupInfo;
+}
+
+KFileItemModel::ItemGroupInfo
+KFileItemModel::timeRoleGroup(const std::function<QDateTime(const ItemData *)> &fileTimeCb, const ItemData *itemData, bool withString) const
+{
+ static bool oldWithString;
+ static ItemGroupInfo oldGroupInfo;
+ static QDate oldFileDate;
+ ItemGroupInfo groupInfo;
+
+ const QDate currentDate = QDate::currentDate();
+
+ QDate previousFileDate;
+ QString groupValue;
+ for (int i = 0; i <= maxIndex; ++i) {
+ if (isChildItem(i)) {
+ continue;
+ }
+
+ const QLocale locale;
+ const QDateTime fileTime = fileTimeCb(m_itemData.at(i));
+ const QDate fileDate = fileTime.date();
+ if (fileDate == previousFileDate) {
+ // The current item is in the same group as the previous item
+ continue;
+ }
+ previousFileDate = fileDate;
+
+ const int daysDistance = fileDate.daysTo(currentDate);
+
+ if (fileDate == oldFileDate && withString == oldWithString) {
+ return oldGroupInfo;
+ }
+ // Simplified grouping algorithm, preserving dates
+ // but not taking "pretty printing" into account
+ if (currentDate.year() == fileDate.year() && currentDate.month() == fileDate.month()) {
+ if (daysDistance < 7) {
+ groupInfo.comparable = daysDistance; // Today, Yesterday and week days
+ } else if (daysDistance < 14) {
+ groupInfo.comparable = 10; // One Week Ago
+ } else if (daysDistance < 21) {
+ groupInfo.comparable = 20; // Two Weeks Ago
+ } else if (daysDistance < 28) {
+ groupInfo.comparable = 30; // Three Weeks Ago
+ } else {
+ groupInfo.comparable = 40; // Earlier This Month
+ }
+ } else {
+ const QDate lastMonthDate = currentDate.addMonths(-1);
+ if (lastMonthDate.year() == fileDate.year() && lastMonthDate.month() == fileDate.month()) {
+ if (daysDistance < 7) {
+ groupInfo.comparable = daysDistance; // Today, Yesterday and week days (Month, Year)
+ } else if (daysDistance < 14) {
+ groupInfo.comparable = 11; // One Week Ago (Month, Year)
+ } else if (daysDistance < 21) {
+ groupInfo.comparable = 21; // Two Weeks Ago (Month, Year)
+ } else if (daysDistance < 28) {
+ groupInfo.comparable = 31; // Three Weeks Ago (Month, Year)
+ } else {
+ groupInfo.comparable = 41; // Earlier on Month, Year
+ }
+ } else {
+ // The trick will fail for dates past April, 178956967 or before 1 AD.
+ groupInfo.comparable = 2147483647 - (fileDate.year() * 12 + fileDate.month() - 1); // Month, Year; newer < older
+ }
+ }
+ if (withString) {
+ if (currentDate.year() == fileDate.year() && currentDate.month() == fileDate.month()) {
+ switch (daysDistance / 7) {
+ case 0:
+ switch (daysDistance) {
+ case 0:
+ groupInfo.text = i18nc("@title:group Date", "Today");
+ break;
+ case 1:
+ groupInfo.text = i18nc("@title:group Date", "Yesterday");
+ break;
+ default:
+ newGroupValue = locale.toString(fileTime, i18nc("@title:group Date: The week day name: dddd", "dddd"));
+ newGroupValue = i18nc(
+ "Can be used to script translation of \"dddd\""
+ "with context @title:group Date",
+ "%1",
+ groupInfo.text);
+ }
+ break;
+ case 1:
+ groupInfo.text = i18nc("@title:group Date", "One Week Ago");
+ break;
+ case 2:
+ groupInfo.text = i18nc("@title:group Date", "Two Weeks Ago");
+ break;
+ case 3:
+ groupInfo.text = i18nc("@title:group Date", "Three Weeks Ago");
+ break;
+ case 4:
+ case 5:
+ groupInfo.text = i18nc("@title:group Date", "Earlier this Month");
+ break;
+ default:
+ Q_ASSERT(false);
+ }
+ } else {
+ const QDate lastMonthDate = currentDate.addMonths(-1);
+ if (lastMonthDate.year() == fileDate.year() && lastMonthDate.month() == fileDate.month()) {
+ if (daysDistance == 1) {
+ const KLocalizedString format = ki18nc(
+ "@title:group Date: "
+ "MMMM is full month name in current locale, and yyyy is "
+ "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 "
+ "part of the text that should not be formatted as a date",
+ "'Yesterday' (MMMM, yyyy)");
+ const QString translatedFormat = format.toString();
+ if (const int count = translatedFormat.count(QLatin1Char('\'')); count >= 2 && count % 2 == 0) {
+ newGroupValue = locale.toString(fileTime, translatedFormat);
+ newGroupValue = i18nc(
+ "Can be used to script translation of "
+ "\"'Yesterday' (MMMM, yyyy)\" with context @title:group Date",
+ "%1",
+ groupInfo.text);
+ } else {
+ qCWarning(DolphinDebug).nospace()
+ << "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org";
+ const QString untranslatedFormat = format.toString({QLatin1String("en_US")});
+ newGroupValue = locale.toString(fileTime, untranslatedFormat);
+ }
+ } else if (daysDistance <= 7) {
+ newGroupValue = locale.toString(fileTime,
+ i18nc("@title:group Date: "
+ "The week day name: dddd, MMMM is full month name "
+ "in current locale, and yyyy is full year number.",
+ "dddd (MMMM, yyyy)"));
+ newGroupValue = i18nc(
+ "Can be used to script translation of "
+ "\"dddd (MMMM, yyyy)\" with context @title:group Date",
+ "%1",
+ groupInfo.text);
+ } else if (daysDistance < 7 * 2) {
+ const KLocalizedString format = ki18nc(
+ "@title:group Date: "
+ "MMMM is full month name in current locale, and yyyy is "
+ "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 "
+ "part of the text that should not be formatted as a date",
+ "'One Week Ago' (MMMM, yyyy)");
+ const QString translatedFormat = format.toString();
+ if (const int count = translatedFormat.count(QLatin1Char('\'')); count >= 2 && count % 2 == 0) {
+ newGroupValue = locale.toString(fileTime, translatedFormat);
+ newGroupValue = i18nc(
+ "Can be used to script translation of "
+ "\"'One Week Ago' (MMMM, yyyy)\" with context @title:group Date",
+ "%1",
+ groupInfo.text);
+ } else {
+ qCWarning(DolphinDebug).nospace()
+ << "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org";
+ const QString untranslatedFormat = format.toString({QLatin1String("en_US")});
+ newGroupValue = locale.toString(fileTime, untranslatedFormat);
+ }
+ } else if (daysDistance < 7 * 3) {
+ const KLocalizedString format = ki18nc(
+ "@title:group Date: "
+ "MMMM is full month name in current locale, and yyyy is "
+ "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 "
+ "part of the text that should not be formatted as a date",
+ "'Two Weeks Ago' (MMMM, yyyy)");
+ const QString translatedFormat = format.toString();
+ if (const int count = translatedFormat.count(QLatin1Char('\'')); count >= 2 && count % 2 == 0) {
+ newGroupValue = locale.toString(fileTime, translatedFormat);
+ newGroupValue = i18nc(
+ "Can be used to script translation of "
+ "\"'Two Weeks Ago' (MMMM, yyyy)\" with context @title:group Date",
+ "%1",
+ groupInfo.text);
+ } else {
+ qCWarning(DolphinDebug).nospace()
+ << "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org";
+ const QString untranslatedFormat = format.toString({QLatin1String("en_US")});
+ newGroupValue = locale.toString(fileTime, untranslatedFormat);
+ }
+ } else if (daysDistance < 7 * 4) {
+ const KLocalizedString format = ki18nc(
+ "@title:group Date: "
+ "MMMM is full month name in current locale, and yyyy is "
+ "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 "
+ "part of the text that should not be formatted as a date",
+ "'Three Weeks Ago' (MMMM, yyyy)");
+ const QString translatedFormat = format.toString();
+ if (const int count = translatedFormat.count(QLatin1Char('\'')); count >= 2 && count % 2 == 0) {
+ newGroupValue = locale.toString(fileTime, translatedFormat);
+ newGroupValue = i18nc(
+ "Can be used to script translation of "
+ "\"'Three Weeks Ago' (MMMM, yyyy)\" with context @title:group Date",
+ "%1",
+ groupInfo.text);
+ } else {
+ qCWarning(DolphinDebug).nospace()
+ << "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org";
+ const QString untranslatedFormat = format.toString({QLatin1String("en_US")});
+ newGroupValue = locale.toString(fileTime, untranslatedFormat);
+ }
+ } else {
+ const KLocalizedString format = ki18nc(
+ "@title:group Date: "
+ "MMMM is full month name in current locale, and yyyy is "
+ "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 "
+ "part of the text that should not be formatted as a date",
+ "'Earlier on' MMMM, yyyy");
+ const QString translatedFormat = format.toString();
+ if (const int count = translatedFormat.count(QLatin1Char('\'')); count >= 2 && count % 2 == 0) {
+ newGroupValue = locale.toString(fileTime, translatedFormat);
+ newGroupValue = i18nc(
+ "Can be used to script translation of "
+ "\"'Earlier on' MMMM, yyyy\" with context @title:group Date",
+ "%1",
+ groupInfo.text);
+ } else {
+ qCWarning(DolphinDebug).nospace()
+ << "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org";
+ const QString untranslatedFormat = format.toString({QLatin1String("en_US")});
+ newGroupValue = locale.toString(fileTime, untranslatedFormat);
+ }
+ }
+ } else {
+ newGroupValue = locale.toString(fileTime,
+ i18nc("@title:group "
+ "The month and year: MMMM is full month name in current locale, "
+ "and yyyy is full year number",
+ "MMMM, yyyy"));
+ newGroupValue = i18nc(
+ "Can be used to script translation of "
+ "\"MMMM, yyyy\" with context @title:group Date",
+ "%1",
+ groupInfo.text);
+ }
+ }
+ }
+ oldWithString = withString;
+ oldFileDate = fileDate;
+ oldGroupInfo = groupInfo;
+ return groupInfo;
+}
+
+KFileItemModel::ItemGroupInfo KFileItemModel::permissionRoleGroup(const ItemData *itemData, bool withString) const
+{
+ static bool oldWithString;
+ static ItemGroupInfo oldGroupInfo;
+ static QFileDevice::Permissions oldPermissions;
+ ItemGroupInfo groupInfo;
+
+ const QFileInfo info(itemData->item.url().toLocalFile());
+ const QFileDevice::Permissions permissions = info.permissions();
+ if (permissions == oldPermissions && withString == oldWithString) {
+ return oldGroupInfo;
+ }
+ groupInfo.comparable = (int)permissions;
+
+ if (withString) {
+ // Set user string
+ QString user;
+ if (permissions & QFile::ReadUser) {
+ user = i18nc("@item:intext Access permission, concatenated", "Read, ");
+ }
+ if (permissions & QFile::WriteUser) {
+ user += i18nc("@item:intext Access permission, concatenated", "Write, ");
+ }
+ if (permissions & QFile::ExeUser) {
+ user += i18nc("@item:intext Access permission, concatenated", "Execute, ");
+ }
+ user = user.isEmpty() ? i18nc("@item:intext Access permission, concatenated", "Forbidden") : user.mid(0, user.length() - 2);
+
+ // Set group string
+ QString group;
+ if (permissions & QFile::ReadGroup) {
+ group = i18nc("@item:intext Access permission, concatenated", "Read, ");
+ }
+ if (permissions & QFile::WriteGroup) {
+ group += i18nc("@item:intext Access permission, concatenated", "Write, ");
+ }
+ if (permissions & QFile::ExeGroup) {
+ group += i18nc("@item:intext Access permission, concatenated", "Execute, ");
+ }
+ group = group.isEmpty() ? i18nc("@item:intext Access permission, concatenated", "Forbidden") : group.mid(0, group.length() - 2);
+
+ // Set others string
+ QString others;
+ if (permissions & QFile::ReadOther) {
+ others = i18nc("@item:intext Access permission, concatenated", "Read, ");
+ }
+ if (permissions & QFile::WriteOther) {
+ others += i18nc("@item:intext Access permission, concatenated", "Write, ");
+ }
+ if (permissions & QFile::ExeOther) {
+ others += i18nc("@item:intext Access permission, concatenated", "Execute, ");
+ }
+ others = others.isEmpty() ? i18nc("@item:intext Access permission, concatenated", "Forbidden") : others.mid(0, others.length() - 2);
+ groupInfo.text = i18nc("@title:group Files and folders by permissions", "User: %1 | Group: %2 | Others: %3", user, group, others);
+ }
+ oldWithString = withString;
+ oldPermissions = permissions;
+ oldGroupInfo = groupInfo;
+ return groupInfo;
+}
+
+KFileItemModel::ItemGroupInfo KFileItemModel::ratingRoleGroup(const ItemData *itemData, bool withString) const
+{
+ ItemGroupInfo groupInfo;
+ groupInfo.comparable = itemData->values.value("rating", 0).toInt();
+ if (withString) {
+ // Dolphin does not currently use string representation of star rating
+ // as stars are rendered as graphics in group headers.
+ groupInfo.text = i18nc("@item:intext Rated N (stars)", "Rated %i", QString::number(groupInfo.comparable));
+ }
+ return groupInfo;
+}
+
+KFileItemModel::ItemGroupInfo KFileItemModel::genericStringRoleGroup(const QByteArray &role, const ItemData *itemData) const
+{
+ return {0, itemData->values.value(role).toString()};
+}
+
+QList<QPair<int, QVariant>> KFileItemModel::nameRoleGroups() const
+{
+ Q_ASSERT(!m_itemData.isEmpty());
+
+ const int maxIndex = count() - 1;
+ QList<QPair<int, QVariant>> groups;
+
+ ItemGroupInfo groupInfo;
+ for (int i = 0; i <= maxIndex; ++i) {
+ if (isChildItem(i)) {
+ continue;
+ }
+
+ ItemGroupInfo newGroupInfo = nameRoleGroup(m_itemData.at(i));
+
+ if (newGroupInfo != groupInfo) {
+ groupInfo = newGroupInfo;
+ groups.append(QPair<int, QVariant>(i, newGroupInfo.text));
+ }
+ }
+ return groups;
+}
+
+QList<QPair<int, QVariant>> KFileItemModel::sizeRoleGroups() const
+{
+ Q_ASSERT(!m_itemData.isEmpty());
+
+ const int maxIndex = count() - 1;
+ QList<QPair<int, QVariant>> groups;
+
+ ItemGroupInfo groupInfo;
+ for (int i = 0; i <= maxIndex; ++i) {
+ if (isChildItem(i)) {
+ continue;
+ }
+
+ ItemGroupInfo newGroupInfo = sizeRoleGroup(m_itemData.at(i));
+
+ if (newGroupInfo != groupInfo) {
+ groupInfo = newGroupInfo;
+ groups.append(QPair<int, QVariant>(i, newGroupInfo.text));
+ }
+ }
+ return groups;
+}
+
+KFileItemModel::ItemGroupInfo KFileItemModel::typeRoleGroup(const ItemData *itemData) const
+{
+ int priority = 0;
+ if (itemData->item.isDir() && m_sortDirsFirst) {
+ // Ensure folders stay first regardless of grouping order
+ if (groupOrder() == Qt::AscendingOrder) {
+ priority = -1;
+ } else {
+ priority = 1;
+ }
+ }
+ return {priority, itemData->values.value("type").toString()};
+}
+
+QList<QPair<int, QVariant>> KFileItemModel::timeRoleGroups(const std::function<QDateTime(const ItemData *)> &fileTimeCb) const
+{
+ Q_ASSERT(!m_itemData.isEmpty());
+
+ const int maxIndex = count() - 1;
+ QList<QPair<int, QVariant>> groups;
+
+ ItemGroupInfo groupInfo;
+ for (int i = 0; i <= maxIndex; ++i) {
+ if (isChildItem(i)) {
+ continue;
+ }
+
+ ItemGroupInfo newGroupInfo = timeRoleGroup(fileTimeCb, m_itemData.at(i));
+
+ if (newGroupInfo != groupInfo) {
+ groupInfo = newGroupInfo;
+ groups.append(QPair<int, QVariant>(i, newGroupInfo.text));
+ }
+ }
+ return groups;
+}
+
+QList<QPair<int, QVariant>> KFileItemModel::permissionRoleGroups() const
+{
+ Q_ASSERT(!m_itemData.isEmpty());
+
+ const int maxIndex = count() - 1;
+ QList<QPair<int, QVariant>> groups;
+
+ ItemGroupInfo groupInfo;
+ for (int i = 0; i <= maxIndex; ++i) {
+ if (isChildItem(i)) {
+ continue;
+ }
+
+ ItemGroupInfo newGroupInfo = permissionRoleGroup(m_itemData.at(i));
+
+ if (newGroupInfo != groupInfo) {
+ groupInfo = newGroupInfo;
+ groups.append(QPair<int, QVariant>(i, newGroupInfo.text));
+ }
+ }
+ return groups;
+}
+
+QList<QPair<int, QVariant>> KFileItemModel::ratingRoleGroups() const
+{
+ Q_ASSERT(!m_itemData.isEmpty());
+
+ const int maxIndex = count() - 1;
+ QList<QPair<int, QVariant>> groups;
+
+ ItemGroupInfo groupInfo;
+ for (int i = 0; i <= maxIndex; ++i) {
+ if (isChildItem(i)) {
+ continue;
+ }
+
+ ItemGroupInfo newGroupInfo = ratingRoleGroup(m_itemData.at(i));
+
+ if (newGroupInfo != groupInfo) {
+ groupInfo = newGroupInfo;
+ // Using the numeric representation because Dolphin has a special
+ // case for drawing stars.
+ groups.append(QPair<int, QVariant>(i, newGroupInfo.comparable));
+ }
+ }
+ return groups;
+}
+
+QList<QPair<int, QVariant>> KFileItemModel::genericStringRoleGroups(const QByteArray &role) const
+{
+ Q_ASSERT(!m_itemData.isEmpty());
+
+ const int maxIndex = count() - 1;
+ QList<QPair<int, QVariant>> groups;
+
+ ItemGroupInfo groupInfo;
+ for (int i = 0; i <= maxIndex; ++i) {
+ if (isChildItem(i)) {
+ continue;
+ }
+
+ ItemGroupInfo newGroupInfo = genericStringRoleGroup(role, m_itemData.at(i));
+
+ if (newGroupInfo != groupInfo) {
+ groupInfo = newGroupInfo;
+ groups.append(QPair<int, QVariant>(i, newGroupInfo.text));
+ }
+ }
+ return groups;
+}
+
+void KFileItemModel::emitSortProgress(int resolvedCount)
+{
+ // Be tolerant against a resolvedCount with a wrong range.
+ // Although there should not be a case where KFileItemModelRolesUpdater
+ // (= caller) provides a wrong range, it is important to emit
+ // a useful progress information even if there is an unexpected
+ // implementation issue.
+
+ const int itemCount = count();
+ if (resolvedCount >= itemCount) {
+ m_sortingProgressPercent = -1;
+ if (m_resortAllItemsTimer->isActive()) {
+ m_resortAllItemsTimer->stop();
+ resortAllItems();
+ }
+
+ Q_EMIT directorySortingProgress(100);
+ } else if (itemCount > 0) {
+ resolvedCount = qBound(0, resolvedCount, itemCount);
+
+ const int progress = resolvedCount * 100 / itemCount;
+ if (m_sortingProgressPercent != progress) {
+ m_sortingProgressPercent = progress;
+ Q_EMIT directorySortingProgress(progress);
+ }
+ }
+}
+
+const KFileItemModel::RoleInfoMap *KFileItemModel::rolesInfoMap(int &count)
+{
+ static const RoleInfoMap rolesInfoMap[] = {
+ // clang-format off
+ // | role | roleType | role translation | group translation | requires Baloo | requires indexer
+ { nullptr, NoRole, kli18nc("@label", "None"), KLazyLocalizedString(), KLazyLocalizedString(), false, false },
+ { "text", NameRole, kli18nc("@label", "Name"), KLazyLocalizedString(), KLazyLocalizedString(), false, false },
+ { "size", SizeRole, kli18nc("@label", "Size"), KLazyLocalizedString(), KLazyLocalizedString(), false, false },
+ { "modificationtime", ModificationTimeRole, kli18nc("@label", "Modified"), KLazyLocalizedString(), kli18nc("@tooltip", "The date format can be selected in settings."), false, false },
+ { "creationtime", CreationTimeRole, kli18nc("@label", "Created"), KLazyLocalizedString(), kli18nc("@tooltip", "The date format can be selected in settings."), false, false },
+ { "accesstime", AccessTimeRole, kli18nc("@label", "Accessed"), KLazyLocalizedString(), kli18nc("@tooltip", "The date format can be selected in settings."), false, false },
+ { "type", TypeRole, kli18nc("@label", "Type"), KLazyLocalizedString(), KLazyLocalizedString(), false, false },
+ { "rating", RatingRole, kli18nc("@label", "Rating"), KLazyLocalizedString(), KLazyLocalizedString(), true, false },
+ { "tags", TagsRole, kli18nc("@label", "Tags"), KLazyLocalizedString(), KLazyLocalizedString(), true, false },
+ { "comment", CommentRole, kli18nc("@label", "Comment"), KLazyLocalizedString(), KLazyLocalizedString(), true, false },
+ { "title", TitleRole, kli18nc("@label", "Title"), kli18nc("@label", "Document"), KLazyLocalizedString(), true, true },
+ { "author", AuthorRole, kli18nc("@label", "Author"), kli18nc("@label", "Document"), KLazyLocalizedString(), true, true },
+ { "publisher", PublisherRole, kli18nc("@label", "Publisher"), kli18nc("@label", "Document"), KLazyLocalizedString(), true, true },
+ { "pageCount", PageCountRole, kli18nc("@label", "Page Count"), kli18nc("@label", "Document"), KLazyLocalizedString(), true, true },
+ { "wordCount", WordCountRole, kli18nc("@label", "Word Count"), kli18nc("@label", "Document"), KLazyLocalizedString(), true, true },
+ { "lineCount", LineCountRole, kli18nc("@label", "Line Count"), kli18nc("@label", "Document"), KLazyLocalizedString(), true, true },
+ { "imageDateTime", ImageDateTimeRole, kli18nc("@label", "Date Photographed"), kli18nc("@label", "Image"), KLazyLocalizedString(), true, true },
+ { "dimensions", DimensionsRole, kli18nc("@label width x height", "Dimensions"), kli18nc("@label", "Image"), KLazyLocalizedString(), true, true },
+ { "width", WidthRole, kli18nc("@label", "Width"), kli18nc("@label", "Image"), KLazyLocalizedString(), true, true },
+ { "height", HeightRole, kli18nc("@label", "Height"), kli18nc("@label", "Image"), KLazyLocalizedString(), true, true },
+ { "orientation", OrientationRole, kli18nc("@label", "Orientation"), kli18nc("@label", "Image"), KLazyLocalizedString(), true, true },
+ { "artist", ArtistRole, kli18nc("@label", "Artist"), kli18nc("@label", "Audio"), KLazyLocalizedString(), true, true },
+ { "genre", GenreRole, kli18nc("@label", "Genre"), kli18nc("@label", "Audio"), KLazyLocalizedString(), true, true },
+ { "album", AlbumRole, kli18nc("@label", "Album"), kli18nc("@label", "Audio"), KLazyLocalizedString(), true, true },
+ { "duration", DurationRole, kli18nc("@label", "Duration"), kli18nc("@label", "Audio"), KLazyLocalizedString(), true, true },
+ { "bitrate", BitrateRole, kli18nc("@label", "Bitrate"), kli18nc("@label", "Audio"), KLazyLocalizedString(), true, true },
+ { "track", TrackRole, kli18nc("@label", "Track"), kli18nc("@label", "Audio"), KLazyLocalizedString(), true, true },
+ { "releaseYear", ReleaseYearRole, kli18nc("@label", "Release Year"), kli18nc("@label", "Audio"), KLazyLocalizedString(), true, true },
+ { "aspectRatio", AspectRatioRole, kli18nc("@label", "Aspect Ratio"), kli18nc("@label", "Video"), KLazyLocalizedString(), true, true },
+ { "frameRate", FrameRateRole, kli18nc("@label", "Frame Rate"), kli18nc("@label", "Video"), KLazyLocalizedString(), true, true },
+ { "duration", DurationRole, kli18nc("@label", "Duration"), kli18nc("@label", "Video"), KLazyLocalizedString(), true, true },
+ { "path", PathRole, kli18nc("@label", "Path"), kli18nc("@label", "Other"), KLazyLocalizedString(), false, false },
+ { "extension", ExtensionRole, kli18nc("@label", "File Extension"), kli18nc("@label", "Other"), KLazyLocalizedString(), false, false },
+ { "deletiontime", DeletionTimeRole, kli18nc("@label", "Deletion Time"), kli18nc("@label", "Other"), KLazyLocalizedString(), false, false },
+ { "destination", DestinationRole, kli18nc("@label", "Link Destination"), kli18nc("@label", "Other"), KLazyLocalizedString(), false, false },
+ { "originUrl", OriginUrlRole, kli18nc("@label", "Downloaded From"), kli18nc("@label", "Other"), KLazyLocalizedString(), true, false },
+ { "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 },
+ { "owner", OwnerRole, kli18nc("@label", "Owner"), kli18nc("@label", "Other"), KLazyLocalizedString(), false, false },
+ { "group", GroupRole, kli18nc("@label", "User Group"), kli18nc("@label", "Other"), KLazyLocalizedString(), false, false },
+ };
+ // clang-format on
+
+ count = sizeof(rolesInfoMap) / sizeof(RoleInfoMap);
+ return rolesInfoMap;
+}
+
+void KFileItemModel::determineMimeTypes(const KFileItemList &items, int timeout)
+{
+ QElapsedTimer timer;
+ timer.start();
+ for (const KFileItem &item : items) {
+ // Only determine mime types for files here. For directories,
+ // KFileItem::determineMimeType() reads the .directory file inside to
+ // load the icon, but this is not necessary at all if we just need the
+ // type. Some special code for setting the correct mime type for
+ // directories is in retrieveData().
+ if (!item.isDir()) {
+ item.determineMimeType();
+ }
+
+ if (timer.elapsed() > timeout) {
+ // Don't block the user interface, let the remaining items
+ // be resolved asynchronously.
+ return;
+ }
+ }
+}
+
+QByteArray KFileItemModel::sharedValue(const QByteArray &value)
+{
+ static QSet<QByteArray> pool;
+ const QSet<QByteArray>::const_iterator it = pool.constFind(value);
+
+ if (it != pool.constEnd()) {
+ return *it;
+ } else {
+ pool.insert(value);
+ return value;
+ }
+}
+
+bool KFileItemModel::isConsistent() const
+{
+ // m_items may contain less items than m_itemData because m_items
+ // is populated lazily, see KFileItemModel::index(const QUrl& url).
+ if (m_items.count() > m_itemData.count()) {
+ return false;
+ }
+
+ for (int i = 0, iMax = count(); i < iMax; ++i) {
+ // Check if m_items and m_itemData are consistent.
+ const KFileItem item = fileItem(i);
+ if (item.isNull()) {
+ qCWarning(DolphinDebug) << "Item" << i << "is null";
+ return false;
+ }
+
+ const int itemIndex = index(item);
+ if (itemIndex != i) {
+ qCWarning(DolphinDebug) << "Item" << i << "has a wrong index:" << itemIndex;
+ return false;
+ }
+
+ // Check if the items are sorted correctly.
+ if (i > 0 && !lessThan(m_itemData.at(i - 1), m_itemData.at(i), m_collator)) {
+ qCWarning(DolphinDebug) << "The order of items" << i - 1 << "and" << i << "is wrong:" << fileItem(i - 1) << fileItem(i);
+ return false;
+ }
+
+ // Check if all parent-child relationships are consistent.
+ const ItemData *data = m_itemData.at(i);
+ const ItemData *parent = data->parent;
+ if (parent) {
+ if (expandedParentsCount(data) != expandedParentsCount(parent) + 1) {
+ qCWarning(DolphinDebug) << "expandedParentsCount is inconsistent for parent" << parent->item << "and child" << data->item;
+ return false;
+ }
+
+ const int parentIndex = index(parent->item);
+ if (parentIndex >= i) {
+ qCWarning(DolphinDebug) << "Index" << parentIndex << "of parent" << parent->item << "is not smaller than index" << i << "of child"
+ << data->item;
+ return false;
+ }
+ }
+ }
+
+ return true;
+}
+
+void KFileItemModel::slotListerError(KIO::Job *job)
+{
+ const int jobError = job->error();
+ if (jobError == KIO::ERR_IS_FILE) {
+ if (auto *listJob = qobject_cast<KIO::ListJob *>(job)) {
+ Q_EMIT urlIsFileError(listJob->url());
+ }
+ } else {
+ const QString errorString = job->errorString();
+ Q_EMIT errorMessage(!errorString.isEmpty() ? errorString : i18nc("@info:status", "Unknown error."), jobError);
+ }