X-Git-Url: https://cloud.milkyroute.net/gitweb/dolphin.git/blobdiff_plain/829e0eb912a5681d5c6fddfd461165ae9f5cb093..b4e80645e8e39ef7fcc1545136bad06ab3dd5f3e:/src/kitemviews/kfileitemmodel.cpp diff --git a/src/kitemviews/kfileitemmodel.cpp b/src/kitemviews/kfileitemmodel.cpp index a7796bc93..603c16e0d 100644 --- a/src/kitemviews/kfileitemmodel.cpp +++ b/src/kitemviews/kfileitemmodel.cpp @@ -20,6 +20,9 @@ #include #include +#ifndef QT_NO_ACCESSIBILITY +#include +#endif #include #include #include @@ -34,11 +37,12 @@ Q_GLOBAL_STATIC(QRecursiveMutex, s_collatorMutex) // #define KFILEITEMMODEL_DEBUG KFileItemModel::KFileItemModel(QObject *parent) - : KItemModelBase("text", parent) + : KItemModelBase("text", "none", parent) , m_dirLister(nullptr) , m_sortDirsFirst(true) , m_sortHiddenLast(false) , m_sortRole(NameRole) + , m_groupRole(NoRole) , m_sortingProgressPercent(-1) , m_roles() , m_itemData() @@ -332,16 +336,32 @@ QMimeData *KFileItemModel::createMimeData(const KItemSet &indexes) const return data; } +namespace +{ +QString removeMarks(const QString &original) +{ + const auto normalized = original.normalized(QString::NormalizationForm_D); + QString res; + for (auto ch : normalized) { + if (!ch.isMark()) { + res.append(ch); + } + } + return res; +} +} + int KFileItemModel::indexForKeyboardSearch(const QString &text, int startFromIndex) const { + const auto noMarkText = removeMarks(text); startFromIndex = qMax(0, startFromIndex); for (int i = startFromIndex; i < count(); ++i) { - if (fileItem(i).text().startsWith(text, Qt::CaseInsensitive)) { + if (removeMarks(fileItem(i).text()).startsWith(noMarkText, Qt::CaseInsensitive)) { return i; } } for (int i = 0; i < startFromIndex; ++i) { - if (fileItem(i).text().startsWith(text, Qt::CaseInsensitive)) { + if (removeMarks(fileItem(i).text()).startsWith(noMarkText, Qt::CaseInsensitive)) { return i; } } @@ -394,7 +414,17 @@ QList> KFileItemModel::groups() const QElapsedTimer timer; timer.start(); #endif - switch (typeForRole(sortRole())) { + QByteArray role = groupRole(); + if (typeForRole(role) == NoRole) { + // Handle extra grouping information + if (m_groupExtraInfo == "followSort") { + role = sortRole(); + } + } + switch (typeForRole(role)) { + case NoRole: + m_groups.clear(); + break; case NameRole: m_groups = nameRoleGroups(); break; @@ -428,7 +458,7 @@ QList> KFileItemModel::groups() const m_groups = ratingRoleGroups(); break; default: - m_groups = genericStringRoleGroups(sortRole()); + m_groups = genericStringRoleGroups(role); break; } @@ -893,6 +923,39 @@ void KFileItemModel::removeFilteredChildren(const KItemRangeList &itemRanges) } } +KFileItemModel::RoleInfo KFileItemModel::roleInformation(const QByteArray &role) +{ + static QHash information; + if (information.isEmpty()) { + int count = 0; + const RoleInfoMap *map = rolesInfoMap(count); + for (int i = 0; i < count; ++i) { + RoleInfo info; + info.role = map[i].role; + info.translation = map[i].roleTranslation.toString(); + if (!map[i].groupTranslation.isEmpty()) { + info.group = map[i].groupTranslation.toString(); + } else { + // For top level roles, groupTranslation is 0. We must make sure that + // info.group is an empty string then because the code that generates + // menus tries to put the actions into sub menus otherwise. + info.group = QString(); + } + info.requiresBaloo = map[i].requiresBaloo; + info.requiresIndexer = map[i].requiresIndexer; + if (!map[i].tooltipTranslation.isEmpty()) { + info.tooltip = map[i].tooltipTranslation.toString(); + } else { + info.tooltip = QString(); + } + + information.insert(map[i].role, info); + } + } + + return information.value(role); +} + QList KFileItemModel::rolesInformation() { static QList rolesInfo; @@ -901,24 +964,7 @@ QList KFileItemModel::rolesInformation() const RoleInfoMap *map = rolesInfoMap(count); for (int i = 0; i < count; ++i) { if (map[i].roleType != NoRole) { - RoleInfo info; - info.role = map[i].role; - info.translation = map[i].roleTranslation.toString(); - if (!map[i].groupTranslation.isEmpty()) { - info.group = map[i].groupTranslation.toString(); - } else { - // For top level roles, groupTranslation is 0. We must make sure that - // info.group is an empty string then because the code that generates - // menus tries to put the actions into sub menus otherwise. - info.group = QString(); - } - info.requiresBaloo = map[i].requiresBaloo; - info.requiresIndexer = map[i].requiresIndexer; - if (!map[i].tooltipTranslation.isEmpty()) { - info.tooltip = map[i].tooltipTranslation.toString(); - } else { - info.tooltip = QString(); - } + RoleInfo info = roleInformation(map[i].role); rolesInfo.append(info); } } @@ -927,6 +973,15 @@ QList KFileItemModel::rolesInformation() return rolesInfo; } +QList KFileItemModel::extraGroupingInformation() +{ + static QList rolesInfo{ + {QByteArray("none"), kli18nc("@label", "No grouping").toString(), nullptr, nullptr, false, false}, + {QByteArray("followSort"), kli18nc("@label", "Follow sorting").toString(), nullptr, nullptr, false, false} + }; + return rolesInfo; +} + void KFileItemModel::onGroupedSortingChanged(bool current) { Q_UNUSED(current) @@ -937,6 +992,13 @@ void KFileItemModel::onSortRoleChanged(const QByteArray ¤t, const QByteArr { Q_UNUSED(previous) m_sortRole = typeForRole(current); + if (m_sortRole == NoRole) { + // Requested role not in list of roles. This could + // be used for indicating non-trivial sorting behavior + m_sortExtraInfo = current; + } else { + m_sortExtraInfo.clear(); + } if (!m_requestRole[m_sortRole]) { QSet newRoles = m_roles; @@ -956,6 +1018,36 @@ void KFileItemModel::onSortOrderChanged(Qt::SortOrder current, Qt::SortOrder pre resortAllItems(); } +void KFileItemModel::onGroupRoleChanged(const QByteArray ¤t, const QByteArray &previous, bool resortItems) +{ + Q_UNUSED(previous) + m_groupRole = typeForRole(current); + if (m_groupRole == NoRole) { + // Requested role not in list of roles. This could + // be used for indicating non-trivial grouping behavior + m_groupExtraInfo = current; + } else { + m_groupExtraInfo.clear(); + } + + if (!m_requestRole[m_groupRole]) { + QSet newRoles = m_roles; + newRoles << current; + setRoles(newRoles); + } + + if (resortItems) { + resortAllItems(); + } +} + +void KFileItemModel::onGroupOrderChanged(Qt::SortOrder current, Qt::SortOrder previous) +{ + Q_UNUSED(current) + Q_UNUSED(previous) + resortAllItems(); +} + void KFileItemModel::loadSortingSettings() { using Choice = GeneralSettings::EnumSortingChoice; @@ -978,6 +1070,7 @@ void KFileItemModel::loadSortingSettings() // Workaround for bug https://bugreports.qt.io/browse/QTBUG-69361 // Force the clean state of QCollator in single thread to avoid thread safety problems in sort m_collator.compare(QString(), QString()); + ContentDisplaySettings::self(); } void KFileItemModel::resortAllItems() @@ -1043,7 +1136,8 @@ void KFileItemModel::resortAllItems() } Q_EMIT itemsMoved(KItemRange(firstMovedIndex, movedItemsCount), movedToIndexes); - } else if (groupedSorting()) { + } + if (groupedSorting()) { // The groups might have changed even if the order of the items has not. const QList> oldGroups = m_groups; m_groups.clear(); @@ -1628,7 +1722,7 @@ void KFileItemModel::removeItems(const KItemRangeList &itemRanges, RemoveItemsBe QList KFileItemModel::createItemDataList(const QUrl &parentUrl, const KFileItemList &items) const { - if (m_sortRole == TypeRole) { + if (m_sortRole == TypeRole || m_groupRole == TypeRole) { // Try to resolve the MIME-types synchronously to prevent a reordering of // the items when sorting by type (per default MIME-types are resolved // asynchronously by KFileItemModelRolesUpdater). @@ -1652,9 +1746,9 @@ QList KFileItemModel::createItemDataList(const QUrl return itemDataList; } -void KFileItemModel::prepareItemsForSorting(QList &itemDataList) +void KFileItemModel::prepareItemsWithRole(QList &itemDataList, RoleType roleType) { - switch (m_sortRole) { + switch (roleType) { case ExtensionRole: case PermissionsRole: case OwnerRole: @@ -1693,6 +1787,12 @@ void KFileItemModel::prepareItemsForSorting(QList &itemDataList) } } +void KFileItemModel::prepareItemsForSorting(QList &itemDataList) +{ + prepareItemsWithRole(itemDataList, m_sortRole); + prepareItemsWithRole(itemDataList, m_groupRole); +} + int KFileItemModel::expandedParentsCount(const ItemData *data) { // The hash 'values' is only guaranteed to contain the key "expandedParentsCount" @@ -2026,31 +2126,33 @@ bool KFileItemModel::lessThan(const ItemData *a, const ItemData *b, const QColla } } - // 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; + 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; + 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; } - - result = sortRoleCompare(a, b, collator); - - return (sortOrder() == Qt::AscendingOrder) ? result < 0 : result > 0; + return result; } void KFileItemModel::sort(const QList::iterator &begin, const QList::iterator &end) const @@ -2241,12 +2343,131 @@ int KFileItemModel::sortRoleCompare(const ItemData *a, const ItemData *b, const 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) { - return collator.compare(a, b); + // 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()); @@ -2260,128 +2481,122 @@ int KFileItemModel::stringCompare(const QString &a, const QString &b, const QCol return QString::compare(a, b, Qt::CaseSensitive); } -QList> KFileItemModel::nameRoleGroups() const +KFileItemModel::ItemGroupInfo KFileItemModel::nameRoleGroup(const ItemData *itemData, bool withString) const { - Q_ASSERT(!m_itemData.isEmpty()); - - const int maxIndex = count() - 1; - QList> groups; - - QString groupValue; + static bool oldWithString; + static ItemGroupInfo oldGroupInfo; + static QChar oldFirstChar; + ItemGroupInfo groupInfo; 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(); - } + const QString name = itemData->item.text(); - if (firstChar != newFirstChar) { - QString newGroupValue; - if (newFirstChar.isLetter()) { - if (m_collator.compare(newFirstChar, QChar(QLatin1Char('A'))) >= 0 && m_collator.compare(newFirstChar, 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 lettersAtoZ; - lettersAtoZ.reserve('Z' - 'A' + 1); - if (lettersAtoZ.empty()) { - for (char c = 'A'; c <= 'Z'; ++c) { - lettersAtoZ.push_back(QLatin1Char(c)); - } - } + QMutexLocker collatorLock(s_collatorMutex()); - auto localeAwareLessThan = [this](QChar c1, QChar c2) -> bool { - return m_collator.compare(c1, c2) < 0; - }; + // 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 lettersAtoZ; + lettersAtoZ.reserve('Z' - 'A' + 1); + if (lettersAtoZ.empty()) { + for (char c = 'A'; c <= 'Z'; ++c) { + lettersAtoZ.push_back(QLatin1Char(c)); + } + } - std::vector::iterator it = std::lower_bound(lettersAtoZ.begin(), lettersAtoZ.end(), newFirstChar, localeAwareLessThan); - if (it != lettersAtoZ.end()) { - if (localeAwareLessThan(newFirstChar, *it)) { - // newFirstChar belongs to the group preceding *it. - // Example: for an umlaut 'A' in the German locale, *it would be 'B' now. - --it; - } - newGroupValue = *it; - } + auto localeAwareLessThan = [this](QChar c1, QChar c2) -> bool { + return m_collator.compare(c1, c2) < 0; + }; - } else { - // Symbols from non Latin-based scripts - newGroupValue = newFirstChar; + std::vector::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; } - } else if (newFirstChar >= QLatin1Char('0') && newFirstChar <= QLatin1Char('9')) { - // Apply group '0 - 9' for any name that starts with a digit - newGroupValue = i18nc("@title:group Groups that start with a digit", "0 - 9"); - } else { - newGroupValue = i18nc("@title:group", "Others"); + if (withString) { + groupInfo.text = *it; + } + groupInfo.comparable = (*it).unicode(); } - if (newGroupValue != groupValue) { - groupValue = newGroupValue; - groups.append(QPair(i, newGroupValue)); + } else { + // Symbols from non Latin-based scripts + if (withString) { + groupInfo.text = firstChar; } - - firstChar = newFirstChar; + 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)'.'; } - return groups; + oldWithString = withString; + oldFirstChar = firstChar; + oldGroupInfo = groupInfo; + return groupInfo; } -QList> KFileItemModel::sizeRoleGroups() const +KFileItemModel::ItemGroupInfo KFileItemModel::sizeRoleGroup(const ItemData *itemData, bool withString) const { - Q_ASSERT(!m_itemData.isEmpty()); + ItemGroupInfo groupInfo; + KIO::filesize_t fileSize; - const int maxIndex = count() - 1; - QList> groups; + const KFileItem item = itemData->item; + fileSize = !item.isNull() ? item.size() : ~0U; - QString groupValue; - for (int i = 0; i <= maxIndex; ++i) { - if (isChildItem(i)) { - continue; - } - - const KFileItem &item = m_itemData.at(i)->item; - KIO::filesize_t fileSize = !item.isNull() ? item.size() : ~0U; - QString newGroupValue; - if (!item.isNull() && item.isDir()) { - if (ContentDisplaySettings::directorySizeMode() == ContentDisplaySettings::EnumDirectorySizeMode::ContentCount || m_sortDirsFirst) { - newGroupValue = i18nc("@title:group Size", "Folders"); - } else { - fileSize = m_itemData.at(i)->values.value("size").toULongLong(); - } - } - - if (newGroupValue.isEmpty()) { - if (fileSize < 5 * 1024 * 1024) { // < 5 MB - newGroupValue = i18nc("@title:group Size", "Small"); - } else if (fileSize < 10 * 1024 * 1024) { // < 10 MB - newGroupValue = i18nc("@title:group Size", "Medium"); - } else { - newGroupValue = i18nc("@title:group Size", "Big"); - } + 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 (newGroupValue != groupValue) { - groupValue = newGroupValue; - groups.append(QPair(i, newGroupValue)); + } + 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 } } - return groups; + if (withString) { + char const *groupNames[] = {"Folders", "Small", "Medium", "Big"}; + groupInfo.text = i18nc("@title:group Size", groupNames[groupInfo.comparable]); + } + return groupInfo; } -QList> KFileItemModel::timeRoleGroups(const std::function &fileTimeCb) const +KFileItemModel::ItemGroupInfo +KFileItemModel::timeRoleGroup(const std::function &fileTimeCb, const ItemData *itemData, bool withString) const { - Q_ASSERT(!m_itemData.isEmpty()); - - const int maxIndex = count() - 1; - QList> groups; + static bool oldWithString; + static ItemGroupInfo oldGroupInfo; + static QDate oldFileDate; + ItemGroupInfo groupInfo; const QDate currentDate = QDate::currentDate(); @@ -2403,16 +2618,52 @@ QList> KFileItemModel::timeRoleGroups(const std::function> KFileItemModel::timeRoleGroups(const std::function> KFileItemModel::timeRoleGroups(const std::function= 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", - newGroupValue); + groupInfo.text); } else { qCWarning(DolphinDebug).nospace() << "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org"; @@ -2473,8 +2724,8 @@ QList> KFileItemModel::timeRoleGroups(const std::function> KFileItemModel::timeRoleGroups(const std::function= 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", - newGroupValue); + 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) { + } else if (daysDistance < 7 * 3) { const KLocalizedString format = ki18nc( "@title:group Date: " "MMMM is full month name in current locale, and yyyy is " @@ -2503,20 +2754,20 @@ QList> KFileItemModel::timeRoleGroups(const std::function= 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", - newGroupValue); + 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) { + } else if (daysDistance < 7 * 4) { const KLocalizedString format = ki18nc( "@title:group Date: " "MMMM is full month name in current locale, and yyyy is " @@ -2524,13 +2775,13 @@ QList> KFileItemModel::timeRoleGroups(const std::function= 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", - newGroupValue); + groupInfo.text); } else { qCWarning(DolphinDebug).nospace() << "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org"; @@ -2545,13 +2796,13 @@ QList> KFileItemModel::timeRoleGroups(const std::function= 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", - newGroupValue); + groupInfo.text); } else { qCWarning(DolphinDebug).nospace() << "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org"; @@ -2569,134 +2820,245 @@ QList> KFileItemModel::timeRoleGroups(const std::function(i, newGroupValue)); - } } - - return groups; + oldWithString = withString; + oldFileDate = fileDate; + oldGroupInfo = groupInfo; + return groupInfo; } -QList> KFileItemModel::permissionRoleGroups() const +KFileItemModel::ItemGroupInfo KFileItemModel::permissionRoleGroup(const ItemData *itemData, bool withString) const { - Q_ASSERT(!m_itemData.isEmpty()); - - const int maxIndex = count() - 1; - QList> groups; - - QString permissionsString; - QString groupValue; - for (int i = 0; i <= maxIndex; ++i) { - if (isChildItem(i)) { - continue; - } - - const ItemData *itemData = m_itemData.at(i); - const QString newPermissionsString = itemData->values.value("permissions").toString(); - if (newPermissionsString == permissionsString) { - continue; - } - permissionsString = newPermissionsString; + static bool oldWithString; + static ItemGroupInfo oldGroupInfo; + static QFileDevice::Permissions oldPermissions; + ItemGroupInfo groupInfo; - const QFileInfo info(itemData->item.url().toLocalFile()); + 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 (info.permission(QFile::ReadUser)) { + if (permissions & QFile::ReadUser) { user = i18nc("@item:intext Access permission, concatenated", "Read, "); } - if (info.permission(QFile::WriteUser)) { + if (permissions & QFile::WriteUser) { user += i18nc("@item:intext Access permission, concatenated", "Write, "); } - if (info.permission(QFile::ExeUser)) { + 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 (info.permission(QFile::ReadGroup)) { + if (permissions & QFile::ReadGroup) { group = i18nc("@item:intext Access permission, concatenated", "Read, "); } - if (info.permission(QFile::WriteGroup)) { + if (permissions & QFile::WriteGroup) { group += i18nc("@item:intext Access permission, concatenated", "Write, "); } - if (info.permission(QFile::ExeGroup)) { + 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 (info.permission(QFile::ReadOther)) { + if (permissions & QFile::ReadOther) { others = i18nc("@item:intext Access permission, concatenated", "Read, "); } - if (info.permission(QFile::WriteOther)) { + if (permissions & QFile::WriteOther) { others += i18nc("@item:intext Access permission, concatenated", "Write, "); } - if (info.permission(QFile::ExeOther)) { + 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> KFileItemModel::nameRoleGroups() const +{ + Q_ASSERT(!m_itemData.isEmpty()); + + const int maxIndex = count() - 1; + QList> groups; + + ItemGroupInfo groupInfo; + for (int i = 0; i <= maxIndex; ++i) { + if (isChildItem(i)) { + continue; + } + + ItemGroupInfo newGroupInfo = nameRoleGroup(m_itemData.at(i)); - const QString newGroupValue = i18nc("@title:group Files and folders by permissions", "User: %1 | Group: %2 | Others: %3", user, group, others); - if (newGroupValue != groupValue) { - groupValue = newGroupValue; - groups.append(QPair(i, newGroupValue)); + if (newGroupInfo != groupInfo) { + groupInfo = newGroupInfo; + groups.append(QPair(i, newGroupInfo.text)); } } + return groups; +} +QList> KFileItemModel::sizeRoleGroups() const +{ + Q_ASSERT(!m_itemData.isEmpty()); + + const int maxIndex = count() - 1; + QList> 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(i, newGroupInfo.text)); + } + } return groups; } -QList> KFileItemModel::ratingRoleGroups() const +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> KFileItemModel::timeRoleGroups(const std::function &fileTimeCb) const { Q_ASSERT(!m_itemData.isEmpty()); const int maxIndex = count() - 1; QList> groups; - int groupValue = -1; + ItemGroupInfo groupInfo; for (int i = 0; i <= maxIndex; ++i) { if (isChildItem(i)) { continue; } - const int newGroupValue = m_itemData.at(i)->values.value("rating", 0).toInt(); - if (newGroupValue != groupValue) { - groupValue = newGroupValue; - groups.append(QPair(i, newGroupValue)); + + ItemGroupInfo newGroupInfo = timeRoleGroup(fileTimeCb, m_itemData.at(i)); + + if (newGroupInfo != groupInfo) { + groupInfo = newGroupInfo; + groups.append(QPair(i, newGroupInfo.text)); } } + return groups; +} +QList> KFileItemModel::permissionRoleGroups() const +{ + Q_ASSERT(!m_itemData.isEmpty()); + + const int maxIndex = count() - 1; + QList> 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(i, newGroupInfo.text)); + } + } return groups; } -QList> KFileItemModel::genericStringRoleGroups(const QByteArray &role) const +QList> KFileItemModel::ratingRoleGroups() const { Q_ASSERT(!m_itemData.isEmpty()); const int maxIndex = count() - 1; QList> groups; - bool isFirstGroupValue = true; - QString groupValue; + ItemGroupInfo groupInfo; for (int i = 0; i <= maxIndex; ++i) { if (isChildItem(i)) { continue; } - const QString newGroupValue = m_itemData.at(i)->values.value(role).toString(); - if (newGroupValue != groupValue || isFirstGroupValue) { - groupValue = newGroupValue; - groups.append(QPair(i, newGroupValue)); - isFirstGroupValue = false; + + 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(i, newGroupInfo.comparable)); } } + return groups; +} + +QList> KFileItemModel::genericStringRoleGroups(const QByteArray &role) const +{ + Q_ASSERT(!m_itemData.isEmpty()); + + const int maxIndex = count() - 1; + QList> 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(i, newGroupInfo.text)); + } + } return groups; } @@ -2733,7 +3095,7 @@ 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, KLazyLocalizedString(), KLazyLocalizedString(), KLazyLocalizedString(), false, false }, + { 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 }, @@ -2763,6 +3125,7 @@ const KFileItemModel::RoleInfoMap *KFileItemModel::rolesInfoMap(int &count) { "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 },