#include "dolphin_generalsettings.h"
#include "dolphindebug.h"
#include "private/kfileitemmodelsortalgorithm.h"
+#include "views/draganddrophelper.h"
#include <KDirLister>
#include <KIO/Job>
+#include <KIO/ListJob>
#include <KLocalizedString>
#include <KUrlMimeData>
-#include <kio_version.h>
#include <QElapsedTimer>
#include <QIcon>
#include <QRecursiveMutex>
#include <QTimer>
#include <QWidget>
-#include <algorithm>
#include <klazylocalizedstring.h>
Q_GLOBAL_STATIC(QRecursiveMutex, s_collatorMutex)
// #define KFILEITEMMODEL_DEBUG
KFileItemModel::KFileItemModel(QObject *parent)
- : KItemModelBase("text", parent)
+ : KItemModelBase("text", "text", parent)
, m_dirLister(nullptr)
, m_sortDirsFirst(true)
, m_sortHiddenLast(false)
// for a lot of items within a quite small timeslot. To prevent expensive resortings the
// resorting is postponed until the timer has been exceeded.
m_resortAllItemsTimer = new QTimer(this);
- m_resortAllItemsTimer->setInterval(500);
+ m_resortAllItemsTimer->setInterval(100); // 100 is a middle ground between sorting too frequently which makes the view unreadable
+ // and sorting too infrequently which leads to users seeing an outdated sort order.
m_resortAllItemsTimer->setSingleShot(true);
connect(m_resortAllItemsTimer, &QTimer::timeout, this, &KFileItemModel::resortAllItems);
connect(GeneralSettings::self(), &GeneralSettings::sortingChoiceChanged, this, &KFileItemModel::slotSortingChoiceChanged);
- setShowTrashMime(m_dirLister->showHiddenFiles());
+ setShowTrashMime(m_dirLister->showHiddenFiles() || !GeneralSettings::hideXTrashFile());
}
KFileItemModel::~KFileItemModel()
return m_sortHiddenLast;
}
-void KFileItemModel::setShowTrashMime(bool show)
+void KFileItemModel::setShowTrashMime(bool showTrashMime)
{
const auto trashMime = QStringLiteral("application/x-trash");
QStringList excludeFilter = m_filter.excludeMimeTypes();
- bool wasShown = !excludeFilter.contains(trashMime);
- if (show) {
- if (!wasShown) {
- excludeFilter.removeAll(trashMime);
- }
- } else {
- if (wasShown) {
- excludeFilter.append(trashMime);
- }
+ if (showTrashMime) {
+ excludeFilter.removeAll(trashMime);
+ } else if (!excludeFilter.contains(trashMime)) {
+ excludeFilter.append(trashMime);
}
- if (wasShown != show) {
- setExcludeMimeTypeFilter(excludeFilter);
+ setExcludeMimeTypeFilter(excludeFilter);
+}
+
+void KFileItemModel::scheduleResortAllItems()
+{
+ if (!m_resortAllItemsTimer->isActive()) {
+ m_resortAllItemsTimer->start();
}
}
void KFileItemModel::setShowHiddenFiles(bool show)
{
-#if KIO_VERSION < QT_VERSION_CHECK(5, 100, 0)
- m_dirLister->setShowingDotFiles(show);
-#else
m_dirLister->setShowHiddenFiles(show);
-#endif
- setShowTrashMime(show);
+ setShowTrashMime(show || !GeneralSettings::hideXTrashFile());
m_dirLister->emitChanges();
if (show) {
dispatchPendingItemsToInsert();
bool KFileItemModel::showHiddenFiles() const
{
-#if KIO_VERSION < QT_VERSION_CHECK(5, 100, 0)
- return m_dirLister->showingDotFiles();
-#else
return m_dirLister->showHiddenFiles();
-#endif
}
void KFileItemModel::setShowDirectoriesOnly(bool enabled)
} else {
item = fileItem(index);
}
- return !item.isNull() && ((item.isDir() && item.isWritable()) || item.isDesktopFile());
+ return !item.isNull() && DragAndDropHelper::supportsDropping(item);
+}
+
+bool KFileItemModel::canEnterOnHover(int index) const
+{
+ KFileItem item;
+ if (index == -1) {
+ item = rootItem();
+ } else {
+ item = fileItem(index);
+ }
+ return !item.isNull() && (item.isDir() || item.isDesktopFile());
}
QString KFileItemModel::roleDescription(const QByteArray &role) const
QElapsedTimer timer;
timer.start();
#endif
- switch (typeForRole(sortRole())) {
+ switch (typeForRole(groupRole())) {
+ case NoRole:
+ m_groups.clear();
+ break;
case NameRole:
m_groups = nameRoleGroups();
break;
m_groups = ratingRoleGroups();
break;
default:
- m_groups = genericStringRoleGroups(sortRole());
+ m_groups = genericStringRoleGroups(groupRole());
break;
}
m_expandedDirs.remove(targetUrl);
m_dirLister->stop(url);
-#if KIO_VERSION >= QT_VERSION_CHECK(5, 92, 0)
m_dirLister->forgetDirs(url);
-#endif
const int parentLevel = expandedParentsCount(index);
const int itemCount = m_itemData.count();
const QUrl url = itemData->item.url();
m_expandedDirs.remove(targetUrl);
m_dirLister->stop(url); // TODO: try to unit-test this, see https://bugs.kde.org/show_bug.cgi?id=332102#c11
-#if KIO_VERSION >= QT_VERSION_CHECK(5, 92, 0)
m_dirLister->forgetDirs(url);
-#endif
expandedChildren.append(targetUrl);
}
++childIndex;
}
}
+KFileItemModel::RoleInfo KFileItemModel::roleInformation(const QByteArray &role)
+{
+ static QHash<QByteArray, RoleInfo> 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::RoleInfo> KFileItemModel::rolesInformation()
{
static QList<RoleInfo> rolesInfo;
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);
}
}
}
}
-void KFileItemModel::onSortOrderChanged(Qt::SortOrder current, Qt::SortOrder previous)
+void KFileItemModel::onSortOrderChanged(Qt::SortOrder current, Qt::SortOrder previous, bool resortItems)
{
Q_UNUSED(current)
Q_UNUSED(previous)
- resortAllItems();
+
+ if (resortItems) {
+ resortAllItems();
+ }
+}
+
+void KFileItemModel::onGroupRoleChanged(const QByteArray ¤t, const QByteArray &previous, bool resortItems)
+{
+ Q_UNUSED(previous)
+ m_groupRole = typeForRole(current);
+
+ if (!m_requestRole[m_sortRole]) {
+ QSet<QByteArray> newRoles = m_roles;
+ newRoles << current;
+ setRoles(newRoles);
+ }
+
+ if (resortItems) {
+ resortAllItems();
+ }
+}
+
+void KFileItemModel::onGroupOrderChanged(Qt::SortOrder current, Qt::SortOrder previous, bool resortItems)
+{
+ Q_UNUSED(current)
+ Q_UNUSED(previous)
+
+ if (resortItems) {
+ resortAllItems();
+ }
}
void KFileItemModel::loadSortingSettings()
// been moved because of the resorting.
QList<QUrl> oldUrls;
oldUrls.reserve(itemCount);
- for (const ItemData *itemData : qAsConst(m_itemData)) {
+ for (const ItemData *itemData : std::as_const(m_itemData)) {
oldUrls.append(itemData->item.url());
}
indexesToRemoveWithChildren.reserve(m_itemData.count());
const int itemCount = m_itemData.count();
- for (int index : qAsConst(indexesToRemove)) {
+ for (int index : std::as_const(indexesToRemove)) {
indexesToRemoveWithChildren.append(index);
const int parentLevel = expandedParentsCount(index);
case DeletionTimeRole:
// These roles can be determined with retrieveData, and they have to be stored
// in the QHash "values" for the sorting.
- for (ItemData *itemData : qAsConst(itemDataList)) {
+ for (ItemData *itemData : std::as_const(itemDataList)) {
if (itemData->values.isEmpty()) {
itemData->values = retrieveData(itemData->item, itemData->parent);
}
case TypeRole:
// At least store the data including the file type for items with known MIME type.
- for (ItemData *itemData : qAsConst(itemDataList)) {
+ for (ItemData *itemData : std::as_const(itemDataList)) {
if (itemData->values.isEmpty()) {
const KFileItem item = itemData->item;
if (item.isDir() || item.isMimeTypeKnown()) {
// 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))) {
+ 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;
}
if (needsResorting) {
- m_resortAllItemsTimer->start();
+ scheduleResortAllItems();
return;
}
}
}
}
- // 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::directorySizeCount() && 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<KFileItemModel::ItemData *>::iterator &begin, const QList<KFileItemModel::ItemData *>::iterator &end) const
break;
case SizeRole: {
- if (ContentDisplaySettings::directorySizeCount() && itemA.isDir()) {
+ 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");
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;
+
+ int groupA, groupB;
+ switch (m_groupRole) {
+ case NoRole:
+ break;
+ case NameRole:
+ groupA = nameRoleGroup(a, false).comparable;
+ groupB = nameRoleGroup(b, false).comparable;
+ break;
+ case SizeRole:
+ groupA = sizeRoleGroup(a, false).comparable;
+ groupB = sizeRoleGroup(b, false).comparable;
+ break;
+ case ModificationTimeRole:
+ groupA = timeRoleGroup(
+ [](const ItemData *item) {
+ return item->item.time(KFileItem::ModificationTime);
+ },
+ a,
+ false)
+ .comparable;
+ groupB = timeRoleGroup(
+ [](const ItemData *item) {
+ return item->item.time(KFileItem::ModificationTime);
+ },
+ b,
+ false)
+ .comparable;
+ break;
+ case CreationTimeRole:
+ groupA = timeRoleGroup(
+ [](const ItemData *item) {
+ return item->item.time(KFileItem::CreationTime);
+ },
+ a,
+ false)
+ .comparable;
+ groupB = timeRoleGroup(
+ [](const ItemData *item) {
+ return item->item.time(KFileItem::CreationTime);
+ },
+ b,
+ false)
+ .comparable;
+ break;
+ case AccessTimeRole:
+ groupA = timeRoleGroup(
+ [](const ItemData *item) {
+ return item->item.time(KFileItem::AccessTime);
+ },
+ a,
+ false)
+ .comparable;
+ groupB = timeRoleGroup(
+ [](const ItemData *item) {
+ return item->item.time(KFileItem::AccessTime);
+ },
+ b,
+ false)
+ .comparable;
+ break;
+ case DeletionTimeRole:
+ groupA = timeRoleGroup(
+ [](const ItemData *item) {
+ return item->values.value("deletiontime").toDateTime();
+ },
+ a,
+ false)
+ .comparable;
+ groupB = timeRoleGroup(
+ [](const ItemData *item) {
+ return item->values.value("deletiontime").toDateTime();
+ },
+ b,
+ false)
+ .comparable;
+ break;
+ case PermissionsRole:
+ groupA = permissionRoleGroup(a, false).comparable;
+ groupB = permissionRoleGroup(b, false).comparable;
+ break;
+ case RatingRole:
+ groupA = ratingRoleGroup(a, false).comparable;
+ groupB = ratingRoleGroup(b, false).comparable;
+ break;
+ default: {
+ QString strGroupA = genericStringRoleGroup(groupRole(), a);
+ QString strGroupB = genericStringRoleGroup(groupRole(), b);
+ result = stringCompare(strGroupA, strGroupB, collator);
+ break;
+ }
+ }
+ if (result == 0) {
+ if (groupA < groupB) {
+ result = -1;
+ } else if (groupA > groupB) {
+ result = 1;
+ }
+ }
+ return result;
+}
+
int KFileItemModel::stringCompare(const QString &a, const QString &b, const QCollator &collator) const
{
QMutexLocker collatorLock(s_collatorMutex());
return QString::compare(a, b, Qt::CaseSensitive);
}
-QList<QPair<int, QVariant>> KFileItemModel::nameRoleGroups() const
+KFileItemModel::ItemGroupInfo KFileItemModel::nameRoleGroup(const ItemData *itemData, bool withString) const
{
- Q_ASSERT(!m_itemData.isEmpty());
-
- const int maxIndex = count() - 1;
- QList<QPair<int, QVariant>> groups;
-
- QString groupValue;
+ 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.
+ // Use the first character of the name as group indication
+ firstChar = name.at(0).toUpper();
- // 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));
- }
- }
+ if (firstChar == oldFirstChar) {
+ 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.
- auto localeAwareLessThan = [this](QChar c1, QChar c2) -> bool {
- return m_collator.compare(c1, c2) < 0;
- };
+ // 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));
+ }
+ }
- std::vector<QChar>::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<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;
}
- } 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<int, QVariant>(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;
+ oldFirstChar = firstChar;
+ oldGroupInfo = groupInfo;
+ return groupInfo;
}
-QList<QPair<int, QVariant>> KFileItemModel::sizeRoleGroups() const
+KFileItemModel::ItemGroupInfo KFileItemModel::sizeRoleGroup(const ItemData *itemData, bool withString) const
{
- Q_ASSERT(!m_itemData.isEmpty());
+ static ItemGroupInfo oldGroupInfo;
+ static KIO::filesize_t oldFileSize;
+ ItemGroupInfo groupInfo;
+ KIO::filesize_t fileSize;
- const int maxIndex = count() - 1;
- QList<QPair<int, QVariant>> groups;
-
- 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::directorySizeCount() || m_sortDirsFirst) {
- newGroupValue = i18nc("@title:group Size", "Folders");
- } else {
- fileSize = m_itemData.at(i)->values.value("size").toULongLong();
- }
- }
+ const KFileItem item = itemData->item;
+ fileSize = !item.isNull() ? item.size() : ~0U;
- 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::ContentCount || m_sortDirsFirst) {
+ groupInfo.comparable = 0; // Folders
+ } else {
+ fileSize = itemData->values.value("size").toULongLong();
}
-
- if (newGroupValue != groupValue) {
- groupValue = newGroupValue;
- groups.append(QPair<int, QVariant>(i, newGroupValue));
+ }
+ if (fileSize == oldFileSize) {
+ return oldGroupInfo;
+ }
+ 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]);
+ }
+ oldFileSize = fileSize;
+ oldGroupInfo = groupInfo;
+ return groupInfo;
}
-QList<QPair<int, QVariant>> KFileItemModel::timeRoleGroups(const std::function<QDateTime(const ItemData *)> &fileTimeCb) const
+KFileItemModel::ItemGroupInfo
+KFileItemModel::timeRoleGroup(const std::function<QDateTime(const ItemData *)> &fileTimeCb, const ItemData *itemData, bool withString) const
{
- Q_ASSERT(!m_itemData.isEmpty());
-
- const int maxIndex = count() - 1;
- QList<QPair<int, QVariant>> groups;
+ 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 QDateTime fileTime = fileTimeCb(itemData);
+ const QDate fileDate = fileTime.date();
+ const int daysDistance = fileDate.daysTo(currentDate);
+
+ // 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
}
-
- 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;
+ } 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
}
- previousFileDate = fileDate;
-
- const int daysDistance = fileDate.daysTo(currentDate);
-
- QString newGroupValue;
+ }
+ if (withString) {
if (currentDate.year() == fileDate.year() && currentDate.month() == fileDate.month()) {
switch (daysDistance / 7) {
case 0:
switch (daysDistance) {
case 0:
- newGroupValue = i18nc("@title:group Date", "Today");
+ groupInfo.text = i18nc("@title:group Date", "Today");
break;
case 1:
- newGroupValue = i18nc("@title:group Date", "Yesterday");
+ groupInfo.text = i18nc("@title:group Date", "Yesterday");
break;
default:
- newGroupValue = fileTime.toString(i18nc("@title:group Date: The week day name: dddd", "dddd"));
- newGroupValue = i18nc(
+ groupInfo.text = fileTime.toString(i18nc("@title:group Date: The week day name: dddd", "dddd"));
+ groupInfo.text = i18nc(
"Can be used to script translation of \"dddd\""
"with context @title:group Date",
"%1",
- newGroupValue);
+ groupInfo.text);
}
break;
case 1:
- newGroupValue = i18nc("@title:group Date", "One Week Ago");
+ groupInfo.text = i18nc("@title:group Date", "One Week Ago");
break;
case 2:
- newGroupValue = i18nc("@title:group Date", "Two Weeks Ago");
+ groupInfo.text = i18nc("@title:group Date", "Two Weeks Ago");
break;
case 3:
- newGroupValue = i18nc("@title:group Date", "Three Weeks Ago");
+ groupInfo.text = i18nc("@title:group Date", "Three Weeks Ago");
break;
case 4:
case 5:
- newGroupValue = i18nc("@title:group Date", "Earlier this Month");
+ groupInfo.text = i18nc("@title:group Date", "Earlier this Month");
break;
default:
Q_ASSERT(false);
"'Yesterday' (MMMM, yyyy)");
const QString translatedFormat = format.toString();
if (translatedFormat.count(QLatin1Char('\'')) == 2) {
- newGroupValue = fileTime.toString(translatedFormat);
- newGroupValue = i18nc(
+ groupInfo.text = fileTime.toString(translatedFormat);
+ groupInfo.text = 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";
const QString untranslatedFormat = format.toString({QLatin1String("en_US")});
- newGroupValue = fileTime.toString(untranslatedFormat);
+ groupInfo.text = fileTime.toString(untranslatedFormat);
}
- } else if (daysDistance <= 7) {
- newGroupValue =
+ } else if (daysDistance < 7) {
+ groupInfo.text =
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)"));
- newGroupValue = i18nc(
+ groupInfo.text = i18nc(
"Can be used to script translation of "
"\"dddd (MMMM, yyyy)\" with context @title:group Date",
"%1",
- newGroupValue);
- } 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 "
"'One Week Ago' (MMMM, yyyy)");
const QString translatedFormat = format.toString();
if (translatedFormat.count(QLatin1Char('\'')) == 2) {
- newGroupValue = fileTime.toString(translatedFormat);
- newGroupValue = i18nc(
+ groupInfo.text = fileTime.toString(translatedFormat);
+ groupInfo.text = 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 = fileTime.toString(untranslatedFormat);
+ groupInfo.text = fileTime.toString(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 "
"'Two Weeks Ago' (MMMM, yyyy)");
const QString translatedFormat = format.toString();
if (translatedFormat.count(QLatin1Char('\'')) == 2) {
- newGroupValue = fileTime.toString(translatedFormat);
- newGroupValue = i18nc(
+ groupInfo.text = fileTime.toString(translatedFormat);
+ groupInfo.text = 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 = fileTime.toString(untranslatedFormat);
+ groupInfo.text = fileTime.toString(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 "
"'Three Weeks Ago' (MMMM, yyyy)");
const QString translatedFormat = format.toString();
if (translatedFormat.count(QLatin1Char('\'')) == 2) {
- newGroupValue = fileTime.toString(translatedFormat);
- newGroupValue = i18nc(
+ groupInfo.text = fileTime.toString(translatedFormat);
+ groupInfo.text = 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";
const QString untranslatedFormat = format.toString({QLatin1String("en_US")});
- newGroupValue = fileTime.toString(untranslatedFormat);
+ groupInfo.text = fileTime.toString(untranslatedFormat);
}
} else {
const KLocalizedString format = ki18nc(
"'Earlier on' MMMM, yyyy");
const QString translatedFormat = format.toString();
if (translatedFormat.count(QLatin1Char('\'')) == 2) {
- newGroupValue = fileTime.toString(translatedFormat);
- newGroupValue = i18nc(
+ groupInfo.text = fileTime.toString(translatedFormat);
+ groupInfo.text = 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";
const QString untranslatedFormat = format.toString({QLatin1String("en_US")});
- newGroupValue = fileTime.toString(untranslatedFormat);
+ groupInfo.text = fileTime.toString(untranslatedFormat);
}
}
} else {
- newGroupValue =
+ groupInfo.text =
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"));
- newGroupValue = i18nc(
+ groupInfo.text = i18nc(
"Can be used to script translation of "
"\"MMMM, yyyy\" with context @title:group Date",
"%1",
- newGroupValue);
+ groupInfo.text);
}
}
-
- if (newGroupValue != groupValue) {
- groupValue = newGroupValue;
- groups.append(QPair<int, QVariant>(i, newGroupValue));
- }
}
-
- return groups;
+ oldFileDate = fileDate;
+ oldGroupInfo = groupInfo;
+ return groupInfo;
}
-QList<QPair<int, QVariant>> KFileItemModel::permissionRoleGroups() const
+KFileItemModel::ItemGroupInfo KFileItemModel::permissionRoleGroup(const ItemData *itemData, bool withString) const
{
- Q_ASSERT(!m_itemData.isEmpty());
-
- const int maxIndex = count() - 1;
- QList<QPair<int, QVariant>> groups;
-
- QString permissionsString;
- QString groupValue;
- for (int i = 0; i <= maxIndex; ++i) {
- if (isChildItem(i)) {
- continue;
- }
+ static ItemGroupInfo oldGroupInfo;
+ static QFileDevice::Permissions oldPermissions;
+ ItemGroupInfo groupInfo;
- 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());
+ const QFileInfo info(itemData->item.url().toLocalFile());
+ const QFileDevice::Permissions permissions = info.permissions();
+ if (permissions == oldPermissions) {
+ 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.count() - 2);
+ 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.count() - 2);
+ 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.count() - 2);
+ 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);
+ }
+ oldPermissions = permissions;
+ oldGroupInfo = groupInfo;
+ return groupInfo;
+}
- 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));
+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 ") + QString::number(groupInfo.comparable);
+ }
+ return groupInfo;
+}
+
+QString KFileItemModel::genericStringRoleGroup(const QByteArray &role, const ItemData *itemData) const
+{
+ return 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;
}
-QList<QPair<int, QVariant>> KFileItemModel::ratingRoleGroups() const
+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;
- 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 = 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::genericStringRoleGroups(const QByteArray &role) const
+QList<QPair<int, QVariant>> KFileItemModel::ratingRoleGroups() const
{
Q_ASSERT(!m_itemData.isEmpty());
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 = m_itemData.at(i)->values.value(role).toString();
- if (newGroupValue != groupValue || isFirstGroupValue) {
- groupValue = newGroupValue;
- groups.append(QPair<int, QVariant>(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<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;
+ QString groupText;
+ for (int i = 0; i <= maxIndex; ++i) {
+ if (isChildItem(i)) {
+ continue;
+ }
+
+ QString newGroupText = genericStringRoleGroup(role, m_itemData.at(i));
+
+ if (newGroupText != groupText) {
+ groupText = newGroupText;
+ groups.append(QPair<int, QVariant>(i, newGroupText));
+ }
+ }
return groups;
}
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 },