#include <KLocalizedString>
#include <KUrlMimeData>
+#ifndef QT_NO_ACCESSIBILITY
+#include <QAccessible>
+#endif
#include <QElapsedTimer>
#include <QIcon>
#include <QMimeData>
// #define KFILEITEMMODEL_DEBUG
KFileItemModel::KFileItemModel(QObject *parent)
- : KItemModelBase("text", "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()
return false;
}
- m_itemData[index]->values = currentValues;
if (changedRoles.contains("text")) {
QUrl url = m_itemData[index]->item.url();
+ m_items.remove(url);
url = url.adjusted(QUrl::RemoveFilename);
url.setPath(url.path() + currentValues["text"].toString());
m_itemData[index]->item.setUrl(url);
+ m_items.insert(url, index);
+
+ if (!changedRoles.contains("url")) {
+ changedRoles.insert("url");
+ currentValues["url"] = url;
+ }
}
+ m_itemData[index]->values = currentValues;
emitItemsChangedAndTriggerResorting(KItemRangeList() << KItemRange(index, 1), changedRoles);
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;
}
}
QElapsedTimer timer;
timer.start();
#endif
- switch (typeForRole(groupRole())) {
+ 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;
m_groups = ratingRoleGroups();
break;
default:
- m_groups = genericStringRoleGroups(groupRole());
+ m_groups = genericStringRoleGroups(role);
break;
}
return rolesInfo;
}
+QList<KFileItemModel::RoleInfo> KFileItemModel::extraGroupingInformation()
+{
+ static QList<RoleInfo> 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)
{
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<QByteArray> newRoles = m_roles;
}
}
-void KFileItemModel::onSortOrderChanged(Qt::SortOrder current, Qt::SortOrder previous, bool resortItems)
+void KFileItemModel::onSortOrderChanged(Qt::SortOrder current, Qt::SortOrder previous)
{
Q_UNUSED(current)
Q_UNUSED(previous)
-
- if (resortItems) {
- resortAllItems();
- }
+ 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_sortRole]) {
+ if (!m_requestRole[m_groupRole]) {
QSet<QByteArray> newRoles = m_roles;
newRoles << current;
setRoles(newRoles);
}
}
-void KFileItemModel::onGroupOrderChanged(Qt::SortOrder current, Qt::SortOrder previous, bool resortItems)
+void KFileItemModel::onGroupOrderChanged(Qt::SortOrder current, Qt::SortOrder previous)
{
Q_UNUSED(current)
Q_UNUSED(previous)
-
- if (resortItems) {
- resortAllItems();
- }
+ resortAllItems();
}
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()
}
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<QPair<int, QVariant>> oldGroups = m_groups;
m_groups.clear();
std::reverse(itemRanges.begin(), itemRanges.end());
}
- // N
- // resortAllItems();
// The indexes in m_items are not correct anymore. Therefore, we clear m_items.
// It will be re-populated with the updated indices if index(const QUrl&) is called.
m_items.clear();
QList<KFileItemModel::ItemData *> 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).
return itemDataList;
}
-void KFileItemModel::prepareItemsForSorting(QList<ItemData *> &itemDataList)
+void KFileItemModel::prepareItemsWithRole(QList<ItemData *> &itemDataList, RoleType roleType)
{
- switch (m_sortRole) {
+ switch (roleType) {
case ExtensionRole:
case PermissionsRole:
case OwnerRole:
}
}
+void KFileItemModel::prepareItemsForSorting(QList<ItemData *> &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"
return true;
}
}
- if (m_sortDirsFirst
- || (ContentDisplaySettings::directorySizeMode() == ContentDisplaySettings::EnumDirectorySizeMode::ContentCount && m_sortRole == SizeRole)) {
+ 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) {
int KFileItemModel::groupRoleCompare(const ItemData *a, const ItemData *b, const QCollator &collator) const
{
// Unlike sortRoleCompare, this function can and often will return 0.
- const KFileItem &itemA = a->item;
- const KFileItem &itemB = b->item;
-
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 NameRole: {
- QChar groupA = getNameRoleGroup(a, false).toChar();
- QChar groupB = getNameRoleGroup(b, false).toChar();
- if (groupA < groupB) {
- result = -1;
- } else if (groupA > groupB) {
- result = 1;
- }
+ case SizeRole:
+ groupA = sizeRoleGroup(a, false);
+ groupB = sizeRoleGroup(b, false);
break;
- }
- case SizeRole: {
- int groupA = getSizeRoleGroup(a, false).toInt();
- int groupB = getSizeRoleGroup(b, false).toInt();
- if (groupA < groupB) {
- result = -1;
- } else if (groupA > groupB) {
- result = 1;
- }
+ 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 ModificationTimeRole: {
- int groupA = getTimeRoleGroup(
- [](const ItemData *item) {
- return item->item.time(KFileItem::ModificationTime);
- },
- a,
- false)
- .toInt();
- int groupB = getTimeRoleGroup(
- [](const ItemData *item) {
- return item->item.time(KFileItem::ModificationTime);
- },
- b,
- false)
- .toInt();
- if (groupA < groupB) {
- result = -1;
- } else if (groupA > groupB) {
- result = 1;
- }
+ 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 CreationTimeRole: {
- int groupA = getTimeRoleGroup(
- [](const ItemData *item) {
- return item->item.time(KFileItem::CreationTime);
- },
- a,
- false)
- .toInt();
- int groupB = getTimeRoleGroup(
- [](const ItemData *item) {
- return item->item.time(KFileItem::CreationTime);
- },
- b,
- false)
- .toInt();
- if (groupA < groupB) {
- result = -1;
- } else if (groupA > groupB) {
- result = 1;
- }
+ 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 AccessTimeRole: {
- int groupA = getTimeRoleGroup(
- [](const ItemData *item) {
- return item->item.time(KFileItem::AccessTime);
- },
- a,
- false)
- .toInt();
- int groupB = getTimeRoleGroup(
- [](const ItemData *item) {
- return item->item.time(KFileItem::AccessTime);
- },
- b,
- false)
- .toInt();
- if (groupA < groupB) {
- result = -1;
- } else if (groupA > groupB) {
- result = 1;
- }
+ 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 DeletionTimeRole: {
- int groupA = getTimeRoleGroup(
- [](const ItemData *item) {
- return item->values.value("deletiontime").toDateTime();
- },
- a,
- false)
- .toInt();
- int groupB = getTimeRoleGroup(
- [](const ItemData *item) {
- return item->values.value("deletiontime").toDateTime();
- },
- b,
- false)
- .toInt();
- if (groupA < groupB) {
- result = -1;
- } else if (groupA > groupB) {
- result = 1;
- }
+ 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;
- }
- // case PermissionsRole:
- // case RatingRole:
default: {
- QString groupA = getGenericStringRoleGroup(groupRole(), a);
- QString groupB = getGenericStringRoleGroup(groupRole(), b);
- if (groupA < groupB) {
- result = -1;
- } else if (groupA > groupB) {
- result = 1;
- }
+ 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;
}
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());
return QString::compare(a, b, Qt::CaseSensitive);
}
-QVariant KFileItemModel::getNameRoleGroup(const ItemData *itemData, bool asString) const
+KFileItemModel::ItemGroupInfo KFileItemModel::nameRoleGroup(const ItemData *itemData, bool withString) const
{
- const KFileItem item = itemData->item;
- const QString name = item.text();
- QVariant newGroupValue;
+ 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
- QChar newFirstChar = name.at(0).toUpper();
- if (newFirstChar == QLatin1Char('~') && name.length() > 1) {
- newFirstChar = name.at(1).toUpper();
+ firstChar = name.at(0).toUpper();
+
+ if (firstChar == oldFirstChar && withString == oldWithString) {
+ return oldGroupInfo;
}
-
- if (newFirstChar.isLetter()) {
- if (m_collator.compare(newFirstChar, QChar(QLatin1Char('A'))) >= 0 && m_collator.compare(newFirstChar, QChar(QLatin1Char('Z'))) <= 0) {
+ 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'.
return m_collator.compare(c1, c2) < 0;
};
- std::vector<QChar>::iterator it = std::lower_bound(lettersAtoZ.begin(), lettersAtoZ.end(), newFirstChar, localeAwareLessThan);
+ std::vector<QChar>::iterator it = std::lower_bound(lettersAtoZ.begin(), lettersAtoZ.end(), firstChar, localeAwareLessThan);
if (it != lettersAtoZ.end()) {
- if (localeAwareLessThan(newFirstChar, *it)) {
+ 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;
}
- newGroupValue = *it;
+ if (withString) {
+ groupInfo.text = *it;
+ }
+ groupInfo.comparable = (*it).unicode();
}
} else {
// Symbols from non Latin-based scripts
- newGroupValue = newFirstChar;
+ if (withString) {
+ groupInfo.text = firstChar;
+ }
+ groupInfo.comparable = firstChar.unicode();
}
- } else if (newFirstChar >= QLatin1Char('0') && newFirstChar <= QLatin1Char('9')) {
+ } else if (firstChar >= QLatin1Char('0') && firstChar <= QLatin1Char('9')) {
// Apply group '0 - 9' for any name that starts with a digit
- if (asString) {
- newGroupValue = i18nc("@title:group Groups that start with a digit", "0 - 9");
- } else {
- newGroupValue = QChar('0');
+ if (withString) {
+ groupInfo.text = i18nc("@title:group Groups that start with a digit", "0 - 9");
}
+ groupInfo.comparable = (int)'0';
} else {
- if (asString) {
- newGroupValue = i18nc("@title:group", "Others");
- } else {
- newGroupValue = QChar('.');
+ if (withString) {
+ groupInfo.text = i18nc("@title:group", "Others");
}
+ groupInfo.comparable = (int)'.';
}
- return newGroupValue;
+ oldWithString = withString;
+ oldFirstChar = firstChar;
+ oldGroupInfo = groupInfo;
+ return groupInfo;
}
-QVariant KFileItemModel::getSizeRoleGroup(const ItemData *itemData, bool asString) const
+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;
- KIO::filesize_t fileSize = !item.isNull() ? item.size() : ~0U;
- int newGroupValue = -1; // None
+ groupInfo.comparable = -1; // None
if (!item.isNull() && item.isDir()) {
- if (ContentDisplaySettings::directorySizeMode() == ContentDisplaySettings::EnumDirectorySizeMode::ContentCount || m_sortDirsFirst) {
- newGroupValue = 0; // Folders
+ if (ContentDisplaySettings::directorySizeMode() != ContentDisplaySettings::EnumDirectorySizeMode::ContentSize) {
+ groupInfo.comparable = 0; // Folders
} else {
fileSize = itemData->values.value("size").toULongLong();
}
}
-
- if (newGroupValue < 0) {
+ if (groupInfo.comparable < 0) {
if (fileSize < 5 * 1024 * 1024) { // < 5 MB
- newGroupValue = 1; // Small
+ groupInfo.comparable = 1; // Small
} else if (fileSize < 10 * 1024 * 1024) { // < 10 MB
- newGroupValue = 2; // Medium
+ groupInfo.comparable = 2; // Medium
} else {
- newGroupValue = 3; // Big
+ groupInfo.comparable = 3; // Big
}
}
- if (asString) {
+ if (withString) {
char const *groupNames[] = {"Folders", "Small", "Medium", "Big"};
- return i18nc("@title:group Size", groupNames[newGroupValue]);
- } else {
- return newGroupValue;
+ groupInfo.text = i18nc("@title:group Size", groupNames[groupInfo.comparable]);
}
+ return groupInfo;
}
-QVariant KFileItemModel::getTimeRoleGroup(const std::function<QDateTime(const ItemData *)> &fileTimeCb, const ItemData *itemData, bool asString) const
+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();
- const QDateTime fileTime = fileTimeCb(itemData);
- const QDate fileDate = fileTime.date();
- const int daysDistance = fileDate.daysTo(currentDate);
- int intGroupValue;
- QString strGroupValue;
+ QDate previousFileDate;
+ QString groupValue;
+ for (int i = 0; i <= maxIndex; ++i) {
+ if (isChildItem(i)) {
+ continue;
+ }
- if (!asString) {
- // Simplified grouping algorithm, preserving dates
- // but not taking "pretty printing" into account
- if (currentDate.year() == fileDate.year() && currentDate.month() == fileDate.month()) {
+ 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) {
- intGroupValue = daysDistance; // Today, Yesterday and week days
+ groupInfo.comparable = daysDistance; // Today, Yesterday and week days (Month, Year)
} else if (daysDistance < 14) {
- intGroupValue = 10; // One Week Ago
+ groupInfo.comparable = 11; // One Week Ago (Month, Year)
} else if (daysDistance < 21) {
- intGroupValue = 20; // Two Weeks Ago
+ groupInfo.comparable = 21; // Two Weeks Ago (Month, Year)
} else if (daysDistance < 28) {
- intGroupValue = 30; // Three Weeks Ago
+ groupInfo.comparable = 31; // Three Weeks Ago (Month, Year)
} else {
- intGroupValue = 40; // Earlier This Month
+ groupInfo.comparable = 41; // Earlier on Month, Year
}
} else {
- const QDate lastMonthDate = currentDate.addMonths(-1);
- if (lastMonthDate.year() == fileDate.year() && lastMonthDate.month() == fileDate.month()) {
- if (daysDistance < 7) {
- intGroupValue = daysDistance; // Today, Yesterday and week days (Month, Year)
- } else if (daysDistance < 14) {
- intGroupValue = 9; // One Week Ago (Month, Year)
- } else if (daysDistance < 21) {
- intGroupValue = 19; // Two Weeks Ago (Month, Year)
- } else if (daysDistance < 28) {
- intGroupValue = 29; // Three Weeks Ago (Month, Year)
- } else {
- intGroupValue = 39; // Earlier on Month, Year
- }
- } else {
- // The trick will fail for dates past April, 178956967 or before 1 AD.
- intGroupValue = 2147483647 - (fileDate.year() * 12 + fileDate.month() - 1); // Month, Year; newer < older
- }
+ // 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
}
- return QVariant(intGroupValue);
- } else {
+ }
+ if (withString) {
if (currentDate.year() == fileDate.year() && currentDate.month() == fileDate.month()) {
switch (daysDistance / 7) {
case 0:
switch (daysDistance) {
case 0:
- strGroupValue = i18nc("@title:group Date", "Today");
+ groupInfo.text = i18nc("@title:group Date", "Today");
break;
case 1:
- strGroupValue = i18nc("@title:group Date", "Yesterday");
+ groupInfo.text = i18nc("@title:group Date", "Yesterday");
break;
default:
- strGroupValue = fileTime.toString(i18nc("@title:group Date: The week day name: dddd", "dddd"));
- strGroupValue = i18nc(
+ 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",
- strGroupValue);
+ groupInfo.text);
}
break;
case 1:
- strGroupValue = i18nc("@title:group Date", "One Week Ago");
+ groupInfo.text = i18nc("@title:group Date", "One Week Ago");
break;
case 2:
- strGroupValue = i18nc("@title:group Date", "Two Weeks Ago");
+ groupInfo.text = i18nc("@title:group Date", "Two Weeks Ago");
break;
case 3:
- strGroupValue = i18nc("@title:group Date", "Three Weeks Ago");
+ groupInfo.text = i18nc("@title:group Date", "Three Weeks Ago");
break;
case 4:
case 5:
- strGroupValue = i18nc("@title:group Date", "Earlier this Month");
+ groupInfo.text = i18nc("@title:group Date", "Earlier this Month");
break;
default:
Q_ASSERT(false);
"part of the text that should not be formatted as a date",
"'Yesterday' (MMMM, yyyy)");
const QString translatedFormat = format.toString();
- if (translatedFormat.count(QLatin1Char('\'')) == 2) {
- strGroupValue = fileTime.toString(translatedFormat);
- strGroupValue = i18nc(
+ 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",
- strGroupValue);
+ 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")});
- strGroupValue = fileTime.toString(untranslatedFormat);
+ newGroupValue = locale.toString(fileTime, untranslatedFormat);
}
} else if (daysDistance <= 7) {
- strGroupValue =
- fileTime.toString(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)"));
- strGroupValue = i18nc(
+ 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",
- strGroupValue);
- } else if (daysDistance <= 7 * 2) {
+ 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 "
"part of the text that should not be formatted as a date",
"'One Week Ago' (MMMM, yyyy)");
const QString translatedFormat = format.toString();
- if (translatedFormat.count(QLatin1Char('\'')) == 2) {
- strGroupValue = fileTime.toString(translatedFormat);
- strGroupValue = i18nc(
+ 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",
- strGroupValue);
+ 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")});
- strGroupValue = fileTime.toString(untranslatedFormat);
+ 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 "
"part of the text that should not be formatted as a date",
"'Two Weeks Ago' (MMMM, yyyy)");
const QString translatedFormat = format.toString();
- if (translatedFormat.count(QLatin1Char('\'')) == 2) {
- strGroupValue = fileTime.toString(translatedFormat);
- strGroupValue = i18nc(
+ 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",
- strGroupValue);
+ 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")});
- strGroupValue = fileTime.toString(untranslatedFormat);
+ 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 "
"part of the text that should not be formatted as a date",
"'Three Weeks Ago' (MMMM, yyyy)");
const QString translatedFormat = format.toString();
- if (translatedFormat.count(QLatin1Char('\'')) == 2) {
- strGroupValue = fileTime.toString(translatedFormat);
- strGroupValue = i18nc(
+ 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",
- strGroupValue);
+ 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")});
- strGroupValue = fileTime.toString(untranslatedFormat);
+ newGroupValue = locale.toString(fileTime, untranslatedFormat);
}
} else {
const KLocalizedString format = ki18nc(
"part of the text that should not be formatted as a date",
"'Earlier on' MMMM, yyyy");
const QString translatedFormat = format.toString();
- if (translatedFormat.count(QLatin1Char('\'')) == 2) {
- strGroupValue = fileTime.toString(translatedFormat);
- strGroupValue = i18nc(
+ 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",
- strGroupValue);
+ 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")});
- strGroupValue = fileTime.toString(untranslatedFormat);
+ newGroupValue = locale.toString(fileTime, untranslatedFormat);
}
}
} else {
- strGroupValue =
- fileTime.toString(i18nc("@title:group "
- "The month and year: MMMM is full month name in current locale, "
- "and yyyy is full year number",
- "MMMM, yyyy"));
- strGroupValue = i18nc(
+ 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",
- strGroupValue);
+ groupInfo.text);
}
}
- return QVariant(strGroupValue);
}
+ 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;
}
-QString KFileItemModel::getGenericStringRoleGroup(const QByteArray &role, const ItemData *itemData) const
+KFileItemModel::ItemGroupInfo KFileItemModel::genericStringRoleGroup(const QByteArray &role, const ItemData *itemData) const
{
- return itemData->values.value(role).toString();
+ return {0, itemData->values.value(role).toString()};
}
QList<QPair<int, QVariant>> KFileItemModel::nameRoleGroups() const
const int maxIndex = count() - 1;
QList<QPair<int, QVariant>> groups;
- QString groupValue;
- QChar firstChar;
+ ItemGroupInfo groupInfo;
for (int i = 0; i <= maxIndex; ++i) {
if (isChildItem(i)) {
continue;
}
- QString newGroupValue = getNameRoleGroup(m_itemData.at(i)).toString();
+ ItemGroupInfo newGroupInfo = nameRoleGroup(m_itemData.at(i));
- if (newGroupValue != groupValue) {
- groupValue = newGroupValue;
- groups.append(QPair<int, QVariant>(i, newGroupValue));
+ if (newGroupInfo != groupInfo) {
+ groupInfo = newGroupInfo;
+ groups.append(QPair<int, QVariant>(i, newGroupInfo.text));
}
-
- // firstChar = newFirstChar;
}
return groups;
}
const int maxIndex = count() - 1;
QList<QPair<int, QVariant>> groups;
- QString groupValue;
+ ItemGroupInfo groupInfo;
for (int i = 0; i <= maxIndex; ++i) {
if (isChildItem(i)) {
continue;
}
- QString newGroupValue = getSizeRoleGroup(m_itemData.at(i)).toString();
+ ItemGroupInfo newGroupInfo = sizeRoleGroup(m_itemData.at(i));
- if (newGroupValue != groupValue) {
- groupValue = newGroupValue;
- groups.append(QPair<int, QVariant>(i, newGroupValue));
+ 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;
- const QDate currentDate = QDate::currentDate();
-
- QDate previousFileDate;
- QString groupValue;
+ ItemGroupInfo groupInfo;
for (int i = 0; i <= maxIndex; ++i) {
if (isChildItem(i)) {
continue;
}
- 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;
+ ItemGroupInfo newGroupInfo = timeRoleGroup(fileTimeCb, m_itemData.at(i));
- QString newGroupValue = getTimeRoleGroup(fileTimeCb, m_itemData.at(i)).toString();
-
- if (newGroupValue != groupValue) {
- groupValue = newGroupValue;
- groups.append(QPair<int, QVariant>(i, newGroupValue));
+ if (newGroupInfo != groupInfo) {
+ groupInfo = newGroupInfo;
+ groups.append(QPair<int, QVariant>(i, newGroupInfo.text));
}
}
-
return groups;
}
const int maxIndex = count() - 1;
QList<QPair<int, QVariant>> groups;
- QString permissionsString;
- QString groupValue;
+ ItemGroupInfo groupInfo;
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;
-
- const QFileInfo info(itemData->item.url().toLocalFile());
-
- // Set user string
- QString user;
- if (info.permission(QFile::ReadUser)) {
- user = i18nc("@item:intext Access permission, concatenated", "Read, ");
- }
- if (info.permission(QFile::WriteUser)) {
- user += i18nc("@item:intext Access permission, concatenated", "Write, ");
- }
- if (info.permission(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)) {
- group = i18nc("@item:intext Access permission, concatenated", "Read, ");
- }
- if (info.permission(QFile::WriteGroup)) {
- group += i18nc("@item:intext Access permission, concatenated", "Write, ");
- }
- if (info.permission(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);
+ ItemGroupInfo newGroupInfo = permissionRoleGroup(m_itemData.at(i));
- // Set others string
- QString others;
- if (info.permission(QFile::ReadOther)) {
- others = i18nc("@item:intext Access permission, concatenated", "Read, ");
- }
- if (info.permission(QFile::WriteOther)) {
- others += i18nc("@item:intext Access permission, concatenated", "Write, ");
- }
- if (info.permission(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);
-
- 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<int, QVariant>(i, newGroupValue));
+ if (newGroupInfo != groupInfo) {
+ groupInfo = newGroupInfo;
+ groups.append(QPair<int, QVariant>(i, newGroupInfo.text));
}
}
-
return groups;
}
const int maxIndex = count() - 1;
QList<QPair<int, QVariant>> 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<int, QVariant>(i, newGroupValue));
+ 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;
}
const int maxIndex = count() - 1;
QList<QPair<int, QVariant>> groups;
- bool isFirstGroupValue = true;
- QString groupValue;
+ ItemGroupInfo groupInfo;
for (int i = 0; i <= maxIndex; ++i) {
if (isChildItem(i)) {
continue;
}
- const QString newGroupValue = getGenericStringRoleGroup(role, m_itemData.at(i));
- if (newGroupValue != groupValue || isFirstGroupValue) {
- groupValue = newGroupValue;
- groups.append(QPair<int, QVariant>(i, newGroupValue));
- isFirstGroupValue = false;
+ ItemGroupInfo newGroupInfo = genericStringRoleGroup(role, m_itemData.at(i));
+
+ if (newGroupInfo != groupInfo) {
+ groupInfo = newGroupInfo;
+ groups.append(QPair<int, QVariant>(i, newGroupInfo.text));
}
}
-
return groups;
}
{ "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 },
void KFileItemModel::slotListerError(KIO::Job *job)
{
- if (job->error() == KIO::ERR_IS_FILE) {
+ 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."));
+ Q_EMIT errorMessage(!errorString.isEmpty() ? errorString : i18nc("@info:status", "Unknown error."), jobError);
}
}