#include "kfileitemmodel.h"
+#include "dolphin_contentdisplaysettings.h"
#include "dolphin_generalsettings.h"
-#include "dolphin_detailsmodesettings.h"
#include "dolphindebug.h"
#include "private/kfileitemmodelsortalgorithm.h"
+#include "views/draganddrophelper.h"
#include <KDirLister>
#include <KIO/Job>
-#include <kio_version.h>
+#include <KIO/ListJob>
#include <KLocalizedString>
#include <KUrlMimeData>
+#ifndef QT_NO_ACCESSIBILITY
+#include <QAccessible>
+#endif
#include <QElapsedTimer>
+#include <QIcon>
#include <QMimeData>
#include <QMimeDatabase>
+#include <QRecursiveMutex>
#include <QTimer>
#include <QWidget>
-#include <QRecursiveMutex>
-#include <QIcon>
-#include <algorithm>
#include <klazylocalizedstring.h>
Q_GLOBAL_STATIC(QRecursiveMutex, s_collatorMutex)
// #define KFILEITEMMODEL_DEBUG
-KFileItemModel::KFileItemModel(QObject* parent) :
- KItemModelBase("text", parent),
- m_dirLister(nullptr),
- m_sortDirsFirst(true),
- m_sortHiddenLast(false),
- m_sortRole(NameRole),
- m_sortingProgressPercent(-1),
- m_roles(),
- m_itemData(),
- m_items(),
- m_filter(),
- m_filteredItems(),
- m_requestRole(),
- m_maximumUpdateIntervalTimer(nullptr),
- m_resortAllItemsTimer(nullptr),
- m_pendingItemsToInsert(),
- m_groups(),
- m_expandedDirs(),
- m_urlsToExpand()
+KFileItemModel::KFileItemModel(QObject *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()
+ , m_items()
+ , m_filter()
+ , m_filteredItems()
+ , m_requestRole()
+ , m_maximumUpdateIntervalTimer(nullptr)
+ , m_resortAllItemsTimer(nullptr)
+ , m_pendingItemsToInsert()
+ , m_groups()
+ , m_expandedDirs()
+ , m_urlsToExpand()
{
m_collator.setNumericMode(true);
m_dirLister->setAutoErrorHandlingEnabled(false);
m_dirLister->setDelayedMimeTypes(true);
- const QWidget* parentWidget = qobject_cast<QWidget*>(parent);
+ const QWidget *parentWidget = qobject_cast<QWidget *>(parent);
if (parentWidget) {
m_dirLister->setMainWindow(parentWidget->window());
}
// 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() || !GeneralSettings::hideXTrashFile());
}
KFileItemModel::~KFileItemModel()
}
m_dirLister->openUrl(url, KDirLister::Reload);
+
+ Q_EMIT directoryRefreshing();
}
QUrl KFileItemModel::directory() const
QHash<QByteArray, QVariant> KFileItemModel::data(int index) const
{
if (index >= 0 && index < count()) {
- ItemData* data = m_itemData.at(index);
+ ItemData *data = m_itemData.at(index);
if (data->values.isEmpty()) {
data->values = retrieveData(data->item, data->parent);
} else if (data->values.count() <= 2 && data->values.value("isExpanded").toBool()) {
return QHash<QByteArray, QVariant>();
}
-bool KFileItemModel::setData(int index, const QHash<QByteArray, QVariant>& values)
+bool KFileItemModel::setData(int index, const QHash<QByteArray, QVariant> &values)
{
if (index < 0 || index >= count()) {
return false;
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 m_sortHiddenLast;
}
+void KFileItemModel::setShowTrashMime(bool showTrashMime)
+{
+ const auto trashMime = QStringLiteral("application/x-trash");
+ QStringList excludeFilter = m_filter.excludeMimeTypes();
+
+ if (showTrashMime) {
+ excludeFilter.removeAll(trashMime);
+ } else if (!excludeFilter.contains(trashMime)) {
+ excludeFilter.append(trashMime);
+ }
+
+ 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 || !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)
return m_dirLister->dirOnlyMode();
}
-QMimeData* KFileItemModel::createMimeData(const KItemSet& indexes) const
+QMimeData *KFileItemModel::createMimeData(const KItemSet &indexes) const
{
- QMimeData* data = new QMimeData();
+ QMimeData *data = new QMimeData();
// The following code has been taken from KDirModel::mimeData()
// (kdelibs/kio/kio/kdirmodel.cpp)
// SPDX-FileCopyrightText: 2006 David Faure <faure@kde.org>
QList<QUrl> urls;
QList<QUrl> mostLocalUrls;
- const ItemData* lastAddedItem = nullptr;
+ const ItemData *lastAddedItem = nullptr;
for (int index : indexes) {
- const ItemData* itemData = m_itemData.at(index);
- const ItemData* parent = itemData->parent;
+ const ItemData *itemData = m_itemData.at(index);
+ const ItemData *parent = itemData->parent;
while (parent && parent != lastAddedItem) {
parent = parent->parent;
}
lastAddedItem = itemData;
- const KFileItem& item = itemData->item;
+ const KFileItem &item = itemData->item;
if (!item.isNull()) {
urls << item.url();
return data;
}
-int KFileItemModel::indexForKeyboardSearch(const QString& text, int startFromIndex) const
+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;
}
}
bool KFileItemModel::supportsDropping(int index) const
{
- const KFileItem item = fileItem(index);
+ KFileItem item;
+ if (index == -1) {
+ item = rootItem();
+ } else {
+ item = fileItem(index);
+ }
+ 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
+QString KFileItemModel::roleDescription(const QByteArray &role) const
{
static QHash<QByteArray, QString> description;
if (description.isEmpty()) {
int count = 0;
- const RoleInfoMap* map = rolesInfoMap(count);
+ const RoleInfoMap *map = rolesInfoMap(count);
for (int i = 0; i < count; ++i) {
if (map[i].roleTranslation.isEmpty()) {
continue;
return description.value(role);
}
-QList<QPair<int, QVariant> > KFileItemModel::groups() const
+QList<QPair<int, QVariant>> KFileItemModel::groups() const
{
if (!m_itemData.isEmpty() && m_groups.isEmpty()) {
#ifdef KFILEITEMMODEL_DEBUG
QElapsedTimer timer;
timer.start();
#endif
- switch (typeForRole(sortRole())) {
- case NameRole: m_groups = nameRoleGroups(); break;
- case SizeRole: m_groups = sizeRoleGroups(); break;
+ 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;
+ case SizeRole:
+ m_groups = sizeRoleGroups();
+ break;
case ModificationTimeRole:
m_groups = timeRoleGroups([](const ItemData *item) {
return item->item.time(KFileItem::ModificationTime);
return item->values.value("deletiontime").toDateTime();
});
break;
- case PermissionsRole: m_groups = permissionRoleGroups(); break;
- case RatingRole: m_groups = ratingRoleGroups(); break;
- default: m_groups = genericStringRoleGroups(sortRole()); break;
+ case PermissionsRole:
+ m_groups = permissionRoleGroups();
+ break;
+ case RatingRole:
+ m_groups = ratingRoleGroups();
+ break;
+ default:
+ m_groups = genericStringRoleGroups(role);
+ break;
}
#ifdef KFILEITEMMODEL_DEBUG
return KFileItem();
}
-int KFileItemModel::index(const KFileItem& item) const
+int KFileItemModel::index(const KFileItem &item) const
{
return index(item.url());
}
-int KFileItemModel::index(const QUrl& url) const
+int KFileItemModel::index(const QUrl &url) const
{
const QUrl urlToFind = url.adjusted(QUrl::StripTrailingSlash);
}
const auto uniqueKeys = indexesForUrl.uniqueKeys();
- for (const QUrl& url : uniqueKeys) {
+ for (const QUrl &url : uniqueKeys) {
if (indexesForUrl.count(url) > 1) {
qCWarning(DolphinDebug) << "Multiple items found with the URL" << url;
auto it = indexesForUrl.find(url);
while (it != indexesForUrl.end() && it.key() == url) {
- const ItemData* data = m_itemData.at(it.value());
+ const ItemData *data = m_itemData.at(it.value());
qCWarning(DolphinDebug) << "index" << it.value() << ":" << data->item;
if (data->parent) {
qCWarning(DolphinDebug) << "parent" << data->parent->item;
slotClear();
}
-void KFileItemModel::setRoles(const QSet<QByteArray>& roles)
+void KFileItemModel::setRoles(const QSet<QByteArray> &roles)
{
if (m_roles == roles) {
return;
QSetIterator<QByteArray> it(roles);
while (it.hasNext()) {
- const QByteArray& role = it.next();
+ const QByteArray &role = it.next();
m_requestRole[typeForRole(role)] = true;
}
// Clear the 'values' of all filtered items. They will be re-populated with the
// correct roles the next time 'values' will be accessed via data(int).
- QHash<KFileItem, ItemData*>::iterator filteredIt = m_filteredItems.begin();
- const QHash<KFileItem, ItemData*>::iterator filteredEnd = m_filteredItems.end();
+ QHash<KFileItem, ItemData *>::iterator filteredIt = m_filteredItems.begin();
+ const QHash<KFileItem, ItemData *>::iterator filteredEnd = m_filteredItems.end();
while (filteredIt != filteredEnd) {
(*filteredIt)->values.clear();
++filteredIt;
m_dirLister->openUrl(url, KDirLister::Keep);
const QVariantList previouslyExpandedChildren = m_itemData.at(index)->values.value("previouslyExpandedChildren").value<QVariantList>();
- for (const QVariant& var : previouslyExpandedChildren) {
+ for (const QVariant &var : previouslyExpandedChildren) {
m_urlsToExpand.insert(var.toUrl());
}
} else {
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();
int childIndex = firstChildIndex;
while (childIndex < itemCount && expandedParentsCount(childIndex) > parentLevel) {
- ItemData* itemData = m_itemData.at(childIndex);
+ ItemData *itemData = m_itemData.at(childIndex);
if (itemData->values.value("isExpanded").toBool()) {
const QUrl targetUrl = itemData->item.targetUrl();
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->stop(url); // TODO: try to unit-test this, see https://bugs.kde.org/show_bug.cgi?id=332102#c11
m_dirLister->forgetDirs(url);
-#endif
expandedChildren.append(targetUrl);
}
++childIndex;
void KFileItemModel::expandParentDirectories(const QUrl &url)
{
-
// Assure that each sub-path of the URL that should be
// expanded is added to m_urlsToExpand. KDirLister
// does not care whether the parent-URL has already been
}
}
-void KFileItemModel::setNameFilter(const QString& nameFilter)
+void KFileItemModel::setNameFilter(const QString &nameFilter)
{
if (m_filter.pattern() != nameFilter) {
dispatchPendingItemsToInsert();
return m_filter.pattern();
}
-void KFileItemModel::setMimeTypeFilters(const QStringList& filters)
+void KFileItemModel::setMimeTypeFilters(const QStringList &filters)
{
if (m_filter.mimeTypes() != filters) {
dispatchPendingItemsToInsert();
return m_filter.mimeTypes();
}
+void KFileItemModel::setExcludeMimeTypeFilter(const QStringList &filters)
+{
+ if (m_filter.excludeMimeTypes() != filters) {
+ dispatchPendingItemsToInsert();
+ m_filter.setExcludeMimeTypes(filters);
+ applyFilters();
+ }
+}
+
+QStringList KFileItemModel::excludeMimeTypeFilter() const
+{
+ return m_filter.excludeMimeTypes();
+}
+
void KFileItemModel::applyFilters()
{
// ===STEP 1===
for (int index = m_itemData.count() - 1; index >= 0; --index) {
ItemData *itemData = m_itemData.at(index);
- if (m_filter.matches(itemData->item)
- || (itemShownBelow && itemShownBelow->parent == itemData)) {
+ if (m_filter.matches(itemData->item) || (itemShownBelow && itemShownBelow->parent == itemData)) {
// We could've entered here for two reasons:
// 1. This item passes the filter itself
// 2. This is an expanded folder that doesn't pass the filter but sees a filter-passing child just below
// If this is a child of an expanded folder, we must make sure that its whole parental chain will also be shown.
// We will go up through its parental chain until we either:
- // 1 - reach the "root item" of the current view, i.e the currently opened folder on Dolphin. Their children have their ItemData::parent set to nullptr.
- // or
- // 2 - we reach an unfiltered parent or a previously discovered ancestor.
+ // 1 - reach the "root item" of the current view, i.e the currently opened folder on Dolphin. Their children have their ItemData::parent set to
+ // nullptr. or 2 - we reach an unfiltered parent or a previously discovered ancestor.
for (ItemData *parent = it.value()->parent; parent && !ancestorsOfNewVisibleItems.contains(parent->item) && m_filteredItems.contains(parent->item);
parent = parent->parent) {
// We wish we could remove this parent from m_filteredItems right now, but we are iterating over it
insertItems(newVisibleItems);
}
-void KFileItemModel::removeFilteredChildren(const KItemRangeList& itemRanges)
+void KFileItemModel::removeFilteredChildren(const KItemRangeList &itemRanges)
{
if (m_filteredItems.isEmpty() || !m_requestRole[ExpandedParentsCountRole]) {
// There are either no filtered items, or it is not possible to expand
return;
}
- QSet<ItemData*> parents;
- for (const KItemRange& range : itemRanges) {
+ QSet<ItemData *> parents;
+ for (const KItemRange &range : itemRanges) {
for (int index = range.index; index < range.index + range.count; ++index) {
parents.insert(m_itemData.at(index));
}
}
- QHash<KFileItem, ItemData*>::iterator it = m_filteredItems.begin();
+ QHash<KFileItem, ItemData *>::iterator it = m_filteredItems.begin();
while (it != m_filteredItems.end()) {
if (parents.contains(it.value()->parent)) {
delete it.value();
}
}
+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;
if (rolesInfo.isEmpty()) {
int count = 0;
- const RoleInfoMap* map = rolesInfoMap(count);
+ 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;
+ RoleInfo info = roleInformation(map[i].role);
rolesInfo.append(info);
}
}
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)
m_groups.clear();
}
-void KFileItemModel::onSortRoleChanged(const QByteArray& current, const QByteArray& previous, bool resortItems)
+void KFileItemModel::onSortRoleChanged(const QByteArray ¤t, const QByteArray &previous, bool resortItems)
{
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;
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<QByteArray> 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;
// 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()
// 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());
}
// Determine the first index that has been moved.
int firstMovedIndex = 0;
- while (firstMovedIndex < itemCount
- && firstMovedIndex == m_items.value(oldUrls.at(firstMovedIndex))) {
+ while (firstMovedIndex < itemCount && firstMovedIndex == m_items.value(oldUrls.at(firstMovedIndex))) {
++firstMovedIndex;
}
m_groups.clear();
int lastMovedIndex = itemCount - 1;
- while (lastMovedIndex > firstMovedIndex
- && lastMovedIndex == m_items.value(oldUrls.at(lastMovedIndex))) {
+ while (lastMovedIndex > firstMovedIndex && lastMovedIndex == m_items.value(oldUrls.at(lastMovedIndex))) {
--lastMovedIndex;
}
}
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;
+ const QList<QPair<int, QVariant>> oldGroups = m_groups;
m_groups.clear();
if (groups() != oldGroups) {
Q_EMIT groupsChanged();
// -> we expand the first visible URL we find in m_restoredExpandedUrls.
// Iterate over a const copy because items are deleted and inserted within the loop
const auto urlsToExpand = m_urlsToExpand;
- for(const QUrl &url : urlsToExpand) {
+ for (const QUrl &url : urlsToExpand) {
const int indexForUrl = index(url);
if (indexForUrl >= 0) {
m_urlsToExpand.remove(url);
Q_EMIT directoryLoadingCanceled();
}
-void KFileItemModel::slotItemsAdded(const QUrl &directoryUrl, const KFileItemList& items)
+void KFileItemModel::slotItemsAdded(const QUrl &directoryUrl, const KFileItemList &items)
{
Q_ASSERT(!items.isEmpty());
}
}
- const QList<ItemData*> itemDataList = createItemDataList(parentUrl, items);
+ const QList<ItemData *> itemDataList = createItemDataList(parentUrl, items);
if (!m_filter.hasSetFilters()) {
m_pendingItemsToInsert.append(itemDataList);
// The name or type filter is active. Hide filtered items
// before inserting them into the model and remember
// the filtered items in m_filteredItems.
- for (ItemData* itemData : itemDataList) {
+ for (ItemData *itemData : itemDataList) {
if (m_filter.matches(itemData->item)) {
m_pendingItemsToInsert.append(itemData);
if (itemData->parent) {
return filteredParentsCount;
}
-void KFileItemModel::slotItemsDeleted(const KFileItemList& items)
+void KFileItemModel::slotItemsDeleted(const KFileItemList &items)
{
dispatchPendingItemsToInsert();
const auto currentDir = directory();
- for (const KFileItem& item : items) {
+ for (const KFileItem &item : items) {
if (item.url() == currentDir) {
Q_EMIT currentDirectoryRemoved();
return;
indexesToRemove.append(indexForItem);
} else {
// Probably the item has been filtered.
- QHash<KFileItem, ItemData*>::iterator it = m_filteredItems.find(item);
+ QHash<KFileItem, ItemData *>::iterator it = m_filteredItems.find(item);
if (it != m_filteredItems.end()) {
delete it.value();
m_filteredItems.erase(it);
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);
Q_EMIT fileItemsChanged(dirsChanged);
}
-void KFileItemModel::slotRefreshItems(const QList<QPair<KFileItem, KFileItem> >& items)
+void KFileItemModel::slotRefreshItems(const QList<QPair<KFileItem, KFileItem>> &items)
{
Q_ASSERT(!items.isEmpty());
#ifdef KFILEITEMMODEL_DEBUG
// Contains currently hidden items that should
// get visible and hence removed from m_filteredItems
- QList<ItemData*> newVisibleItems;
+ QList<ItemData *> newVisibleItems;
- QListIterator<QPair<KFileItem, KFileItem> > it(items);
+ QListIterator<QPair<KFileItem, KFileItem>> it(items);
while (it.hasNext()) {
- const QPair<KFileItem, KFileItem>& itemPair = it.next();
- const KFileItem& oldItem = itemPair.first;
- const KFileItem& newItem = itemPair.second;
+ const QPair<KFileItem, KFileItem> &itemPair = it.next();
+ const KFileItem &oldItem = itemPair.first;
+ const KFileItem &newItem = itemPair.second;
const int indexForItem = index(oldItem);
const bool newItemMatchesFilter = m_filter.matches(newItem);
if (indexForItem >= 0) {
// Keep old values as long as possible if they could not retrieved synchronously yet.
// The update of the values will be done asynchronously by KFileItemModelRolesUpdater.
- ItemData * const itemData = m_itemData.at(indexForItem);
+ ItemData *const itemData = m_itemData.at(indexForItem);
QHashIterator<QByteArray, QVariant> it(retrieveData(newItem, itemData->parent));
while (it.hasNext()) {
it.next();
- const QByteArray& role = it.key();
+ const QByteArray &role = it.key();
if (itemData->values.value(role) != it.value()) {
itemData->values.insert(role, it.value());
changedRoles.insert(role);
}
} else {
// Check if 'oldItem' is one of the filtered items.
- QHash<KFileItem, ItemData*>::iterator it = m_filteredItems.find(oldItem);
+ QHash<KFileItem, ItemData *>::iterator it = m_filteredItems.find(oldItem);
if (it != m_filteredItems.end()) {
ItemData *const itemData = it.value();
itemData->item = newItem;
}
}
-void KFileItemModel::insertItems(QList<ItemData*>& newItems)
+void KFileItemModel::insertItems(QList<ItemData *> &newItems)
{
if (newItems.isEmpty()) {
return;
if (m_sortRole == NameRole) {
parallelMergeSort(newItems.begin(), newItems.end(), nameLessThan, QThread::idealThreadCount());
} else if (isRoleValueNatural(m_sortRole)) {
- auto lambdaLessThan = [&] (const KFileItemModel::ItemData* a, const KFileItemModel::ItemData* b)
- {
+ auto lambdaLessThan = [&](const KFileItemModel::ItemData *a, const KFileItemModel::ItemData *b) {
const QByteArray role = roleForType(m_sortRole);
return a->values.value(role).toString() < b->values.value(role).toString();
};
int rangeCount = 0;
while (sourceIndexNewItems >= 0) {
- ItemData* newItem = newItems.at(sourceIndexNewItems);
+ ItemData *newItem = newItems.at(sourceIndexNewItems);
if (sourceIndexExistingItems >= 0 && lessThan(newItem, m_itemData.at(sourceIndexExistingItems), m_collator)) {
// Move an existing item to its new position. If any new items
// are behind it, push the item range to itemRanges.
#endif
}
-void KFileItemModel::removeItems(const KItemRangeList& itemRanges, RemoveItemsBehavior behavior)
+void KFileItemModel::removeItems(const KItemRangeList &itemRanges, RemoveItemsBehavior behavior)
{
if (itemRanges.isEmpty()) {
return;
// Step 1: Remove the items from m_itemData, and free the ItemData.
int removedItemsCount = 0;
- for (const KItemRange& range : itemRanges) {
+ for (const KItemRange &range : itemRanges) {
removedItemsCount += range.count;
for (int index = range.index; index < range.index + range.count; ++index) {
Q_EMIT itemsRemoved(itemRanges);
}
-QList<KFileItemModel::ItemData*> KFileItemModel::createItemDataList(const QUrl& parentUrl, const KFileItemList& items) const
+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).
const int parentIndex = index(parentUrl);
ItemData *parentItem = parentIndex < 0 ? m_filteredItems.value(KFileItem(parentUrl), nullptr) : m_itemData.at(parentIndex);
- QList<ItemData*> itemDataList;
+ QList<ItemData *> itemDataList;
itemDataList.reserve(items.count());
- for (const KFileItem& item : items) {
- ItemData* itemData = new ItemData();
+ for (const KFileItem &item : items) {
+ ItemData *itemData = new ItemData();
itemData->item = item;
itemData->parent = parentItem;
itemDataList.append(itemData);
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:
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()) {
}
}
-int KFileItemModel::expandedParentsCount(const ItemData* data)
+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"
// if the corresponding item is expanded, and it is not a top-level item.
- const ItemData* parent = data->parent;
+ const ItemData *parent = data->parent;
if (parent) {
if (parent->parent) {
Q_ASSERT(parent->values.contains("expandedParentsCount"));
const int maxIndex = m_itemData.count() - 1;
for (int i = 0; i <= maxIndex; ++i) {
- const ItemData* itemData = m_itemData.at(i);
+ const ItemData *itemData = m_itemData.at(i);
if (itemData->parent) {
indexesToRemove.append(i);
}
m_expandedDirs.clear();
// Also remove all filtered items which have a parent.
- QHash<KFileItem, ItemData*>::iterator it = m_filteredItems.begin();
- const QHash<KFileItem, ItemData*>::iterator end = m_filteredItems.end();
+ QHash<KFileItem, ItemData *>::iterator it = m_filteredItems.begin();
+ const QHash<KFileItem, ItemData *>::iterator end = m_filteredItems.end();
while (it != end) {
if (it.value()->parent) {
}
}
-void KFileItemModel::emitItemsChangedAndTriggerResorting(const KItemRangeList& itemRanges, const QSet<QByteArray>& changedRoles)
+void KFileItemModel::emitItemsChangedAndTriggerResorting(const KItemRangeList &itemRanges, const QSet<QByteArray> &changedRoles)
{
Q_EMIT itemsChanged(itemRanges, changedRoles);
// Trigger a resorting if necessary. Note that this can happen even if the sort
// role has not changed at all because the file name can be used as a fallback.
- if (changedRoles.contains(sortRole()) || changedRoles.contains(roleForType(NameRole))) {
- for (const KItemRange& range : itemRanges) {
+ if (changedRoles.contains(sortRole()) || changedRoles.contains(roleForType(NameRole))
+ || (changedRoles.contains("count") && sortRole() == "size")) { // "count" is used in the "size" sort role, so this might require a resorting.
+ for (const KItemRange &range : itemRanges) {
bool needsResorting = false;
const int first = range.index;
// (a) The first item in the range is "lessThan" its predecessor,
// (b) the successor of the last item is "lessThan" the last item, or
// (c) the internal order of the items in the range is incorrect.
- if (first > 0
- && lessThan(m_itemData.at(first), m_itemData.at(first - 1), m_collator)) {
+ if (first > 0 && lessThan(m_itemData.at(first), m_itemData.at(first - 1), m_collator)) {
needsResorting = true;
- } else if (last < count() - 1
- && lessThan(m_itemData.at(last + 1), m_itemData.at(last), m_collator)) {
+ } else if (last < count() - 1 && lessThan(m_itemData.at(last + 1), m_itemData.at(last), m_collator)) {
needsResorting = true;
} else {
for (int index = first; index < last; ++index) {
}
if (needsResorting) {
- m_resortAllItemsTimer->start();
+ scheduleResortAllItems();
return;
}
}
}
}
-KFileItemModel::RoleType KFileItemModel::typeForRole(const QByteArray& role) const
+KFileItemModel::RoleType KFileItemModel::typeForRole(const QByteArray &role) const
{
static QHash<QByteArray, RoleType> roles;
if (roles.isEmpty()) {
// Insert user visible roles that can be accessed with
// KFileItemModel::roleInformation()
int count = 0;
- const RoleInfoMap* map = rolesInfoMap(count);
+ const RoleInfoMap *map = rolesInfoMap(count);
for (int i = 0; i < count; ++i) {
roles.insert(map[i].role, map[i].roleType);
}
// Insert user visible roles that can be accessed with
// KFileItemModel::roleInformation()
int count = 0;
- const RoleInfoMap* map = rolesInfoMap(count);
+ const RoleInfoMap *map = rolesInfoMap(count);
for (int i = 0; i < count; ++i) {
roles.insert(map[i].roleType, map[i].role);
}
return roles.value(roleType);
}
-QHash<QByteArray, QVariant> KFileItemModel::retrieveData(const KFileItem& item, const ItemData* parent) const
+QHash<QByteArray, QVariant> KFileItemModel::retrieveData(const KFileItem &item, const ItemData *parent) const
{
// It is important to insert only roles that are fast to retrieve. E.g.
// KFileItem::iconName() can be very expensive if the MIME-type is unknown
}
if (m_requestRole[IsHiddenRole]) {
- data.insert(sharedValue("isHidden"), item.isHidden());
+ data.insert(sharedValue("isHidden"), item.isHidden() || item.mimetype() == QStringLiteral("application/x-trash"));
}
if (m_requestRole[NameRole]) {
}
if (m_requestRole[ExtensionRole] && !isDir) {
+ // TODO KF6 use KFileItem::suffix 464722
data.insert(sharedValue("extension"), QFileInfo(item.name()).suffix());
}
}
if (m_requestRole[PermissionsRole]) {
- data.insert(sharedValue("permissions"), item.permissionsString());
+ data.insert(sharedValue("permissions"), QVariantList() << item.permissionsString() << item.permissions());
}
if (m_requestRole[OwnerRole]) {
return data;
}
-bool KFileItemModel::lessThan(const ItemData* a, const ItemData* b, const QCollator& collator) const
+bool KFileItemModel::lessThan(const ItemData *a, const ItemData *b, const QCollator &collator) const
{
int 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;
+ 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 || (DetailsModeSettings::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
+void KFileItemModel::sort(const QList<KFileItemModel::ItemData *>::iterator &begin, const QList<KFileItemModel::ItemData *>::iterator &end) const
{
- auto lambdaLessThan = [&] (const KFileItemModel::ItemData* a, const KFileItemModel::ItemData* b)
- {
+ auto lambdaLessThan = [&](const KFileItemModel::ItemData *a, const KFileItemModel::ItemData *b) {
return lessThan(a, b, m_collator);
};
}
}
-int KFileItemModel::sortRoleCompare(const ItemData* a, const ItemData* b, const QCollator& collator) const
+int KFileItemModel::sortRoleCompare(const ItemData *a, const ItemData *b, const QCollator &collator) const
{
// This function must never return 0, because that would break stable
// sorting, which leads to all kinds of bugs.
// See: https://bugs.kde.org/show_bug.cgi?id=433247
// If two items have equal sort values, let the fallbacks at the bottom of
// the function handle it.
- const KFileItem& itemA = a->item;
- const KFileItem& itemB = b->item;
+ const KFileItem &itemA = a->item;
+ const KFileItem &itemB = b->item;
int result = 0;
break;
case SizeRole: {
- if (DetailsModeSettings::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");
case RatingRole:
case WidthRole:
case HeightRole:
+ case PublisherRole:
+ case PageCountRole:
case WordCountRole:
case LineCountRole:
case TrackRole:
break;
}
- case DimensionsRole: {
+ case DimensionsRole: {
const QByteArray role = roleForType(m_sortRole);
const QSize dimensionsA = a->values.value(role).toSize();
const QSize dimensionsB = b->values.value(role).toSize();
}
break;
}
-
}
if (result != 0) {
return QString::compare(itemA.url().url(), itemB.url().url(), Qt::CaseSensitive);
}
-int KFileItemModel::stringCompare(const QString& a, const QString& b, const QCollator& collator) const
+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());
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 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();
- }
- if (firstChar != newFirstChar) {
- QString newGroupValue;
- if (newFirstChar.isLetter()) {
+ const QString name = itemData->item.text();
- 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<QChar> 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<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;
+ oldWithString = withString;
+ 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());
-
- const int maxIndex = count() - 1;
- QList<QPair<int, QVariant> > groups;
-
- QString groupValue;
- for (int i = 0; i <= maxIndex; ++i) {
- if (isChildItem(i)) {
- continue;
- }
+ ItemGroupInfo groupInfo;
+ KIO::filesize_t fileSize;
- 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 (DetailsModeSettings::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::ContentSize) {
+ groupInfo.comparable = 0; // Folders
+ } else {
+ fileSize = itemData->values.value("size").toULongLong();
}
-
- if (newGroupValue != groupValue) {
- groupValue = newGroupValue;
- groups.append(QPair<int, QVariant>(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<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 bool oldWithString;
+ static ItemGroupInfo oldGroupInfo;
+ static QDate oldFileDate;
+ ItemGroupInfo groupInfo;
const QDate currentDate = QDate::currentDate();
continue;
}
+ const QLocale locale;
const QDateTime fileTime = fileTimeCb(m_itemData.at(i));
const QDate fileDate = fileTime.date();
if (fileDate == previousFileDate) {
const int daysDistance = fileDate.daysTo(currentDate);
- QString newGroupValue;
- if (currentDate.year() == fileDate.year() &&
- currentDate.month() == fileDate.month()) {
-
+ if (fileDate == oldFileDate && withString == oldWithString) {
+ return oldGroupInfo;
+ }
+ // Simplified grouping algorithm, preserving dates
+ // but not taking "pretty printing" into account
+ if (currentDate.year() == fileDate.year() && currentDate.month() == fileDate.month()) {
+ if (daysDistance < 7) {
+ groupInfo.comparable = daysDistance; // Today, Yesterday and week days
+ } else if (daysDistance < 14) {
+ groupInfo.comparable = 10; // One Week Ago
+ } else if (daysDistance < 21) {
+ groupInfo.comparable = 20; // Two Weeks Ago
+ } else if (daysDistance < 28) {
+ groupInfo.comparable = 30; // Three Weeks Ago
+ } else {
+ groupInfo.comparable = 40; // Earlier This Month
+ }
+ } else {
+ const QDate lastMonthDate = currentDate.addMonths(-1);
+ if (lastMonthDate.year() == fileDate.year() && lastMonthDate.month() == fileDate.month()) {
+ if (daysDistance < 7) {
+ groupInfo.comparable = daysDistance; // Today, Yesterday and week days (Month, Year)
+ } else if (daysDistance < 14) {
+ groupInfo.comparable = 11; // One Week Ago (Month, Year)
+ } else if (daysDistance < 21) {
+ groupInfo.comparable = 21; // Two Weeks Ago (Month, Year)
+ } else if (daysDistance < 28) {
+ groupInfo.comparable = 31; // Three Weeks Ago (Month, Year)
+ } else {
+ groupInfo.comparable = 41; // Earlier on Month, Year
+ }
+ } else {
+ // The trick will fail for dates past April, 178956967 or before 1 AD.
+ groupInfo.comparable = 2147483647 - (fileDate.year() * 12 + fileDate.month() - 1); // Month, Year; newer < older
+ }
+ }
+ if (withString) {
+ if (currentDate.year() == fileDate.year() && currentDate.month() == fileDate.month()) {
switch (daysDistance / 7) {
case 0:
switch (daysDistance) {
- case 0: newGroupValue = i18nc("@title:group Date", "Today"); break;
- case 1: newGroupValue = i18nc("@title:group Date", "Yesterday"); break;
+ case 0:
+ groupInfo.text = i18nc("@title:group Date", "Today");
+ break;
+ case 1:
+ groupInfo.text = i18nc("@title:group Date", "Yesterday");
+ break;
default:
- newGroupValue = fileTime.toString(
- 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", newGroupValue);
+ newGroupValue = locale.toString(fileTime, i18nc("@title:group Date: The week day name: dddd", "dddd"));
+ newGroupValue = i18nc(
+ "Can be used to script translation of \"dddd\""
+ "with context @title:group Date",
+ "%1",
+ groupInfo.text);
}
break;
case 1:
- 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);
}
} else {
const QDate lastMonthDate = currentDate.addMonths(-1);
- if (lastMonthDate.year() == fileDate.year() &&
- lastMonthDate.month() == fileDate.month()) {
-
+ if (lastMonthDate.year() == fileDate.year() && lastMonthDate.month() == fileDate.month()) {
if (daysDistance == 1) {
- const KLocalizedString format = ki18nc("@title:group Date: "
- "MMMM is full month name in current locale, and yyyy is "
- "full year number. You must keep the ' don't use any fancy \" or « or similar. The ' is not shown to the user, it's there to mark a part of the text that should not be formatted as a date", "'Yesterday' (MMMM, yyyy)");
+ const KLocalizedString format = ki18nc(
+ "@title:group Date: "
+ "MMMM is full month name in current locale, and yyyy is "
+ "full year number. You must keep the ' don't use any fancy \" or « or similar. The ' is not shown to the user, it's there to mark a "
+ "part of the text that should not be formatted as a date",
+ "'Yesterday' (MMMM, yyyy)");
const QString translatedFormat = format.toString();
- if (translatedFormat.count(QLatin1Char('\'')) == 2) {
- newGroupValue = fileTime.toString(translatedFormat);
- newGroupValue = i18nc("Can be used to script translation of "
+ 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", newGroupValue);
+ "%1",
+ groupInfo.text);
} else {
- qCWarning(DolphinDebug).nospace() << "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org";
- const QString untranslatedFormat = format.toString({ QLatin1String("en_US") });
- newGroupValue = fileTime.toString(untranslatedFormat);
+ qCWarning(DolphinDebug).nospace()
+ << "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org";
+ const QString untranslatedFormat = format.toString({QLatin1String("en_US")});
+ newGroupValue = locale.toString(fileTime, untranslatedFormat);
}
} else if (daysDistance <= 7) {
- newGroupValue = 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("Can be used to script translation of "
+ 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", newGroupValue);
- } else if (daysDistance <= 7 * 2) {
- const KLocalizedString format = ki18nc("@title:group Date: "
- "MMMM is full month name in current locale, and yyyy is "
- "full year number. You must keep the ' don't use any fancy \" or « or similar. The ' is not shown to the user, it's there to mark a part of the text that should not be formatted as a date", "'One Week Ago' (MMMM, yyyy)");
+ "%1",
+ groupInfo.text);
+ } else if (daysDistance < 7 * 2) {
+ const KLocalizedString format = ki18nc(
+ "@title:group Date: "
+ "MMMM is full month name in current locale, and yyyy is "
+ "full year number. You must keep the ' don't use any fancy \" or « or similar. The ' is not shown to the user, it's there to mark a "
+ "part of the text that should not be formatted as a date",
+ "'One Week Ago' (MMMM, yyyy)");
const QString translatedFormat = format.toString();
- if (translatedFormat.count(QLatin1Char('\'')) == 2) {
- newGroupValue = fileTime.toString(translatedFormat);
- newGroupValue = i18nc("Can be used to script translation of "
+ 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", newGroupValue);
+ "%1",
+ groupInfo.text);
} else {
- qCWarning(DolphinDebug).nospace() << "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org";
- const QString untranslatedFormat = format.toString({ QLatin1String("en_US") });
- newGroupValue = fileTime.toString(untranslatedFormat);
+ qCWarning(DolphinDebug).nospace()
+ << "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org";
+ const QString untranslatedFormat = format.toString({QLatin1String("en_US")});
+ newGroupValue = locale.toString(fileTime, untranslatedFormat);
}
- } else if (daysDistance <= 7 * 3) {
- const KLocalizedString format = ki18nc("@title:group Date: "
- "MMMM is full month name in current locale, and yyyy is "
- "full year number. You must keep the ' don't use any fancy \" or « or similar. The ' is not shown to the user, it's there to mark a part of the text that should not be formatted as a date", "'Two Weeks Ago' (MMMM, yyyy)");
+ } else if (daysDistance < 7 * 3) {
+ const KLocalizedString format = ki18nc(
+ "@title:group Date: "
+ "MMMM is full month name in current locale, and yyyy is "
+ "full year number. You must keep the ' don't use any fancy \" or « or similar. The ' is not shown to the user, it's there to mark a "
+ "part of the text that should not be formatted as a date",
+ "'Two Weeks Ago' (MMMM, yyyy)");
const QString translatedFormat = format.toString();
- if (translatedFormat.count(QLatin1Char('\'')) == 2) {
- newGroupValue = fileTime.toString(translatedFormat);
- newGroupValue = i18nc("Can be used to script translation of "
+ 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", newGroupValue);
+ "%1",
+ groupInfo.text);
} else {
- qCWarning(DolphinDebug).nospace() << "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org";
- const QString untranslatedFormat = format.toString({ QLatin1String("en_US") });
- newGroupValue = fileTime.toString(untranslatedFormat);
+ qCWarning(DolphinDebug).nospace()
+ << "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org";
+ const QString untranslatedFormat = format.toString({QLatin1String("en_US")});
+ newGroupValue = locale.toString(fileTime, untranslatedFormat);
}
- } else if (daysDistance <= 7 * 4) {
- const KLocalizedString format = ki18nc("@title:group Date: "
- "MMMM is full month name in current locale, and yyyy is "
- "full year number. You must keep the ' don't use any fancy \" or « or similar. The ' is not shown to the user, it's there to mark a part of the text that should not be formatted as a date", "'Three Weeks Ago' (MMMM, yyyy)");
+ } else if (daysDistance < 7 * 4) {
+ const KLocalizedString format = ki18nc(
+ "@title:group Date: "
+ "MMMM is full month name in current locale, and yyyy is "
+ "full year number. You must keep the ' don't use any fancy \" or « or similar. The ' is not shown to the user, it's there to mark a "
+ "part of the text that should not be formatted as a date",
+ "'Three Weeks Ago' (MMMM, yyyy)");
const QString translatedFormat = format.toString();
- if (translatedFormat.count(QLatin1Char('\'')) == 2) {
- newGroupValue = fileTime.toString(translatedFormat);
- newGroupValue = i18nc("Can be used to script translation of "
+ 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", newGroupValue);
+ "%1",
+ groupInfo.text);
} else {
- qCWarning(DolphinDebug).nospace() << "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org";
- const QString untranslatedFormat = format.toString({ QLatin1String("en_US") });
- newGroupValue = fileTime.toString(untranslatedFormat);
+ qCWarning(DolphinDebug).nospace()
+ << "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org";
+ const QString untranslatedFormat = format.toString({QLatin1String("en_US")});
+ newGroupValue = locale.toString(fileTime, untranslatedFormat);
}
} else {
- const KLocalizedString format = ki18nc("@title:group Date: "
- "MMMM is full month name in current locale, and yyyy is "
- "full year number. You must keep the ' don't use any fancy \" or « or similar. The ' is not shown to the user, it's there to mark a part of the text that should not be formatted as a date", "'Earlier on' MMMM, yyyy");
+ const KLocalizedString format = ki18nc(
+ "@title:group Date: "
+ "MMMM is full month name in current locale, and yyyy is "
+ "full year number. You must keep the ' don't use any fancy \" or « or similar. The ' is not shown to the user, it's there to mark a "
+ "part of the text that should not be formatted as a date",
+ "'Earlier on' MMMM, yyyy");
const QString translatedFormat = format.toString();
- if (translatedFormat.count(QLatin1Char('\'')) == 2) {
- newGroupValue = fileTime.toString(translatedFormat);
- newGroupValue = i18nc("Can be used to script translation of "
+ 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", newGroupValue);
+ "%1",
+ groupInfo.text);
} else {
- qCWarning(DolphinDebug).nospace() << "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org";
- const QString untranslatedFormat = format.toString({ QLatin1String("en_US") });
- newGroupValue = fileTime.toString(untranslatedFormat);
+ qCWarning(DolphinDebug).nospace()
+ << "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org";
+ const QString untranslatedFormat = format.toString({QLatin1String("en_US")});
+ newGroupValue = locale.toString(fileTime, untranslatedFormat);
}
}
} else {
- newGroupValue = 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("Can be used to script translation of "
+ 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", newGroupValue);
+ "%1",
+ groupInfo.text);
}
}
-
- if (newGroupValue != groupValue) {
- groupValue = newGroupValue;
- groups.append(QPair<int, QVariant>(i, newGroupValue));
- }
}
-
- return groups;
+ oldWithString = withString;
+ 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;
- }
-
- 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.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);
+ }
+ oldWithString = withString;
+ oldPermissions = permissions;
+ oldGroupInfo = groupInfo;
+ return groupInfo;
+}
+
+KFileItemModel::ItemGroupInfo KFileItemModel::ratingRoleGroup(const ItemData *itemData, bool withString) const
+{
+ ItemGroupInfo groupInfo;
+ groupInfo.comparable = itemData->values.value("rating", 0).toInt();
+ if (withString) {
+ // Dolphin does not currently use string representation of star rating
+ // as stars are rendered as graphics in group headers.
+ groupInfo.text = i18nc("@item:intext Rated N (stars)", "Rated %i", QString::number(groupInfo.comparable));
+ }
+ return groupInfo;
+}
+
+KFileItemModel::ItemGroupInfo KFileItemModel::genericStringRoleGroup(const QByteArray &role, const ItemData *itemData) const
+{
+ return {0, itemData->values.value(role).toString()};
+}
+
+QList<QPair<int, QVariant>> KFileItemModel::nameRoleGroups() const
+{
+ Q_ASSERT(!m_itemData.isEmpty());
+
+ const int maxIndex = count() - 1;
+ QList<QPair<int, QVariant>> groups;
+
+ ItemGroupInfo groupInfo;
+ for (int i = 0; i <= maxIndex; ++i) {
+ if (isChildItem(i)) {
+ continue;
+ }
+
+ ItemGroupInfo newGroupInfo = nameRoleGroup(m_itemData.at(i));
- 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;
+}
+
+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
+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;
+ 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;
+ 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;
+
+ ItemGroupInfo groupInfo;
+ for (int i = 0; i <= maxIndex; ++i) {
+ if (isChildItem(i)) {
+ continue;
+ }
+
+ ItemGroupInfo newGroupInfo = genericStringRoleGroup(role, m_itemData.at(i));
+ if (newGroupInfo != groupInfo) {
+ groupInfo = newGroupInfo;
+ groups.append(QPair<int, QVariant>(i, newGroupInfo.text));
+ }
+ }
return groups;
}
}
}
-const KFileItemModel::RoleInfoMap* KFileItemModel::rolesInfoMap(int& count)
+const KFileItemModel::RoleInfoMap *KFileItemModel::rolesInfoMap(int &count)
{
static const RoleInfoMap rolesInfoMap[] = {
- // | role | roleType | role translation | group translation | requires Baloo | requires indexer
- { nullptr, NoRole, KLazyLocalizedString(), KLazyLocalizedString(), false, false },
- { "text", NameRole, kli18nc("@label", "Name"), KLazyLocalizedString(), false, false },
- { "size", SizeRole, kli18nc("@label", "Size"), KLazyLocalizedString(), false, false },
- { "modificationtime", ModificationTimeRole, kli18nc("@label", "Modified"), KLazyLocalizedString(), false, false },
- { "creationtime", CreationTimeRole, kli18nc("@label", "Created"), KLazyLocalizedString(), false, false },
- { "accesstime", AccessTimeRole, kli18nc("@label", "Accessed"), KLazyLocalizedString(), false, false },
- { "type", TypeRole, kli18nc("@label", "Type"), KLazyLocalizedString(), false, false },
- { "rating", RatingRole, kli18nc("@label", "Rating"), KLazyLocalizedString(), true, false },
- { "tags", TagsRole, kli18nc("@label", "Tags"), KLazyLocalizedString(), true, false },
- { "comment", CommentRole, kli18nc("@label", "Comment"), KLazyLocalizedString(), true, false },
- { "title", TitleRole, kli18nc("@label", "Title"), kli18nc("@label", "Document"), true, true },
- { "author", AuthorRole, kli18nc("@label", "Author"), kli18nc("@label", "Document"), true, true },
- { "wordCount", WordCountRole, kli18nc("@label", "Word Count"), kli18nc("@label", "Document"), true, true },
- { "lineCount", LineCountRole, kli18nc("@label", "Line Count"), kli18nc("@label", "Document"), true, true },
- { "imageDateTime", ImageDateTimeRole, kli18nc("@label", "Date Photographed"), kli18nc("@label", "Image"), true, true },
- { "dimensions", DimensionsRole, kli18nc("@label width x height", "Dimensions"), kli18nc("@label", "Image"), true, true },
- { "width", WidthRole, kli18nc("@label", "Width"), kli18nc("@label", "Image"), true, true },
- { "height", HeightRole, kli18nc("@label", "Height"), kli18nc("@label", "Image"), true, true },
- { "orientation", OrientationRole, kli18nc("@label", "Orientation"), kli18nc("@label", "Image"), true, true },
- { "artist", ArtistRole, kli18nc("@label", "Artist"), kli18nc("@label", "Audio"), true, true },
- { "genre", GenreRole, kli18nc("@label", "Genre"), kli18nc("@label", "Audio"), true, true },
- { "album", AlbumRole, kli18nc("@label", "Album"), kli18nc("@label", "Audio"), true, true },
- { "duration", DurationRole, kli18nc("@label", "Duration"), kli18nc("@label", "Audio"), true, true },
- { "bitrate", BitrateRole, kli18nc("@label", "Bitrate"), kli18nc("@label", "Audio"), true, true },
- { "track", TrackRole, kli18nc("@label", "Track"), kli18nc("@label", "Audio"), true, true },
- { "releaseYear", ReleaseYearRole, kli18nc("@label", "Release Year"), kli18nc("@label", "Audio"), true, true },
- { "aspectRatio", AspectRatioRole, kli18nc("@label", "Aspect Ratio"), kli18nc("@label", "Video"), true, true },
- { "frameRate", FrameRateRole, kli18nc("@label", "Frame Rate"), kli18nc("@label", "Video"), true, true },
- { "path", PathRole, kli18nc("@label", "Path"), kli18nc("@label", "Other"), false, false },
- { "extension", ExtensionRole, kli18nc("@label", "File Extension"), kli18nc("@label", "Other"), false, false },
- { "deletiontime", DeletionTimeRole, kli18nc("@label", "Deletion Time"), kli18nc("@label", "Other"), false, false },
- { "destination", DestinationRole, kli18nc("@label", "Link Destination"), kli18nc("@label", "Other"), false, false },
- { "originUrl", OriginUrlRole, kli18nc("@label", "Downloaded From"), kli18nc("@label", "Other"), true, false },
- { "permissions", PermissionsRole, kli18nc("@label", "Permissions"), kli18nc("@label", "Other"), false, false },
- { "owner", OwnerRole, kli18nc("@label", "Owner"), kli18nc("@label", "Other"), false, false },
- { "group", GroupRole, kli18nc("@label", "User Group"), kli18nc("@label", "Other"), false, false },
+ // clang-format off
+ // | role | roleType | role translation | group translation | requires Baloo | requires indexer
+ { nullptr, NoRole, kli18nc("@label", "None"), KLazyLocalizedString(), KLazyLocalizedString(), false, false },
+ { "text", NameRole, kli18nc("@label", "Name"), KLazyLocalizedString(), KLazyLocalizedString(), false, false },
+ { "size", SizeRole, kli18nc("@label", "Size"), KLazyLocalizedString(), KLazyLocalizedString(), false, false },
+ { "modificationtime", ModificationTimeRole, kli18nc("@label", "Modified"), KLazyLocalizedString(), kli18nc("@tooltip", "The date format can be selected in settings."), false, false },
+ { "creationtime", CreationTimeRole, kli18nc("@label", "Created"), KLazyLocalizedString(), kli18nc("@tooltip", "The date format can be selected in settings."), false, false },
+ { "accesstime", AccessTimeRole, kli18nc("@label", "Accessed"), KLazyLocalizedString(), kli18nc("@tooltip", "The date format can be selected in settings."), false, false },
+ { "type", TypeRole, kli18nc("@label", "Type"), KLazyLocalizedString(), KLazyLocalizedString(), false, false },
+ { "rating", RatingRole, kli18nc("@label", "Rating"), KLazyLocalizedString(), KLazyLocalizedString(), true, false },
+ { "tags", TagsRole, kli18nc("@label", "Tags"), KLazyLocalizedString(), KLazyLocalizedString(), true, false },
+ { "comment", CommentRole, kli18nc("@label", "Comment"), KLazyLocalizedString(), KLazyLocalizedString(), true, false },
+ { "title", TitleRole, kli18nc("@label", "Title"), kli18nc("@label", "Document"), KLazyLocalizedString(), true, true },
+ { "author", AuthorRole, kli18nc("@label", "Author"), kli18nc("@label", "Document"), KLazyLocalizedString(), true, true },
+ { "publisher", PublisherRole, kli18nc("@label", "Publisher"), kli18nc("@label", "Document"), KLazyLocalizedString(), true, true },
+ { "pageCount", PageCountRole, kli18nc("@label", "Page Count"), kli18nc("@label", "Document"), KLazyLocalizedString(), true, true },
+ { "wordCount", WordCountRole, kli18nc("@label", "Word Count"), kli18nc("@label", "Document"), KLazyLocalizedString(), true, true },
+ { "lineCount", LineCountRole, kli18nc("@label", "Line Count"), kli18nc("@label", "Document"), KLazyLocalizedString(), true, true },
+ { "imageDateTime", ImageDateTimeRole, kli18nc("@label", "Date Photographed"), kli18nc("@label", "Image"), KLazyLocalizedString(), true, true },
+ { "dimensions", DimensionsRole, kli18nc("@label width x height", "Dimensions"), kli18nc("@label", "Image"), KLazyLocalizedString(), true, true },
+ { "width", WidthRole, kli18nc("@label", "Width"), kli18nc("@label", "Image"), KLazyLocalizedString(), true, true },
+ { "height", HeightRole, kli18nc("@label", "Height"), kli18nc("@label", "Image"), KLazyLocalizedString(), true, true },
+ { "orientation", OrientationRole, kli18nc("@label", "Orientation"), kli18nc("@label", "Image"), KLazyLocalizedString(), true, true },
+ { "artist", ArtistRole, kli18nc("@label", "Artist"), kli18nc("@label", "Audio"), KLazyLocalizedString(), true, true },
+ { "genre", GenreRole, kli18nc("@label", "Genre"), kli18nc("@label", "Audio"), KLazyLocalizedString(), true, true },
+ { "album", AlbumRole, kli18nc("@label", "Album"), kli18nc("@label", "Audio"), KLazyLocalizedString(), true, true },
+ { "duration", DurationRole, kli18nc("@label", "Duration"), kli18nc("@label", "Audio"), KLazyLocalizedString(), true, true },
+ { "bitrate", BitrateRole, kli18nc("@label", "Bitrate"), kli18nc("@label", "Audio"), KLazyLocalizedString(), true, true },
+ { "track", TrackRole, kli18nc("@label", "Track"), kli18nc("@label", "Audio"), KLazyLocalizedString(), true, true },
+ { "releaseYear", ReleaseYearRole, kli18nc("@label", "Release Year"), kli18nc("@label", "Audio"), KLazyLocalizedString(), true, true },
+ { "aspectRatio", AspectRatioRole, kli18nc("@label", "Aspect Ratio"), kli18nc("@label", "Video"), KLazyLocalizedString(), true, true },
+ { "frameRate", FrameRateRole, kli18nc("@label", "Frame Rate"), kli18nc("@label", "Video"), KLazyLocalizedString(), true, true },
+ { "duration", DurationRole, kli18nc("@label", "Duration"), kli18nc("@label", "Video"), KLazyLocalizedString(), true, true },
+ { "path", PathRole, kli18nc("@label", "Path"), kli18nc("@label", "Other"), KLazyLocalizedString(), false, false },
+ { "extension", ExtensionRole, kli18nc("@label", "File Extension"), kli18nc("@label", "Other"), KLazyLocalizedString(), false, false },
+ { "deletiontime", DeletionTimeRole, kli18nc("@label", "Deletion Time"), kli18nc("@label", "Other"), KLazyLocalizedString(), false, false },
+ { "destination", DestinationRole, kli18nc("@label", "Link Destination"), kli18nc("@label", "Other"), KLazyLocalizedString(), false, false },
+ { "originUrl", OriginUrlRole, kli18nc("@label", "Downloaded From"), kli18nc("@label", "Other"), KLazyLocalizedString(), true, false },
+ { "permissions", PermissionsRole, kli18nc("@label", "Permissions"), kli18nc("@label", "Other"), kli18nc("@tooltip", "The permission format can be changed in settings. Options are Symbolic, Numeric (Octal) or Combined formats"), false, false },
+ { "owner", OwnerRole, kli18nc("@label", "Owner"), kli18nc("@label", "Other"), KLazyLocalizedString(), false, false },
+ { "group", GroupRole, kli18nc("@label", "User Group"), kli18nc("@label", "Other"), KLazyLocalizedString(), false, false },
};
+ // clang-format on
count = sizeof(rolesInfoMap) / sizeof(RoleInfoMap);
return rolesInfoMap;
}
-void KFileItemModel::determineMimeTypes(const KFileItemList& items, int timeout)
+void KFileItemModel::determineMimeTypes(const KFileItemList &items, int timeout)
{
QElapsedTimer timer;
timer.start();
- for (const KFileItem& item : items) {
+ for (const KFileItem &item : items) {
// Only determine mime types for files here. For directories,
// KFileItem::determineMimeType() reads the .directory file inside to
// load the icon, but this is not necessary at all if we just need the
}
}
-QByteArray KFileItemModel::sharedValue(const QByteArray& value)
+QByteArray KFileItemModel::sharedValue(const QByteArray &value)
{
static QSet<QByteArray> pool;
const QSet<QByteArray>::const_iterator it = pool.constFind(value);
// Check if the items are sorted correctly.
if (i > 0 && !lessThan(m_itemData.at(i - 1), m_itemData.at(i), m_collator)) {
- qCWarning(DolphinDebug) << "The order of items" << i - 1 << "and" << i << "is wrong:"
- << fileItem(i - 1) << fileItem(i);
+ qCWarning(DolphinDebug) << "The order of items" << i - 1 << "and" << i << "is wrong:" << fileItem(i - 1) << fileItem(i);
return false;
}
// Check if all parent-child relationships are consistent.
- const ItemData* data = m_itemData.at(i);
- const ItemData* parent = data->parent;
+ const ItemData *data = m_itemData.at(i);
+ const ItemData *parent = data->parent;
if (parent) {
if (expandedParentsCount(data) != expandedParentsCount(parent) + 1) {
qCWarning(DolphinDebug) << "expandedParentsCount is inconsistent for parent" << parent->item << "and child" << data->item;
const int parentIndex = index(parent->item);
if (parentIndex >= i) {
- qCWarning(DolphinDebug) << "Index" << parentIndex << "of parent" << parent->item << "is not smaller than index" << i << "of child" << data->item;
+ qCWarning(DolphinDebug) << "Index" << parentIndex << "of parent" << parent->item << "is not smaller than index" << i << "of child"
+ << data->item;
return 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);
}
}
+
+#include "moc_kfileitemmodel.cpp"