]> cloud.milkyroute.net Git - dolphin.git/blobdiff - src/kitemviews/kfileitemmodel.cpp
Utilize ecm_set_deprecation_versions to exclude deprecated API
[dolphin.git] / src / kitemviews / kfileitemmodel.cpp
index 3cc140701634770d1d82d4d5d21de17a0d60babe..d4d0be0e8b2332a124526941e1dc3df2d0111638 100644 (file)
@@ -1,38 +1,35 @@
-/*****************************************************************************
- *   Copyright (C) 2011 by Peter Penz <peter.penz19@gmail.com>               *
- *   Copyright (C) 2013 by Frank Reininghaus <frank78ac@googlemail.com>      *
- *   Copyright (C) 2013 by Emmanuel Pescosta <emmanuelpescosta099@gmail.com> *
- *                                                                           *
- *   This program is free software; you can redistribute it and/or modify    *
- *   it under the terms of the GNU General Public License as published by    *
- *   the Free Software Foundation; either version 2 of the License, or       *
- *   (at your option) any later version.                                     *
- *                                                                           *
- *   This program is distributed in the hope that it will be useful,         *
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of          *
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the           *
- *   GNU General Public License for more details.                            *
- *                                                                           *
- *   You should have received a copy of the GNU General Public License       *
- *   along with this program; if not, write to the                           *
- *   Free Software Foundation, Inc.,                                         *
- *   51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA              *
- *****************************************************************************/
+/*
+ * SPDX-FileCopyrightText: 2011 Peter Penz <peter.penz19@gmail.com>
+ * SPDX-FileCopyrightText: 2013 Frank Reininghaus <frank78ac@googlemail.com>
+ * SPDX-FileCopyrightText: 2013 Emmanuel Pescosta <emmanuelpescosta099@gmail.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
 
 #include "kfileitemmodel.h"
 
 #include "dolphin_generalsettings.h"
+#include "dolphin_detailsmodesettings.h"
 #include "dolphindebug.h"
-#include "private/kfileitemmodeldirlister.h"
 #include "private/kfileitemmodelsortalgorithm.h"
 
+#include <KDirLister>
+#include <KIO/Job>
 #include <KLocalizedString>
+#include <KLazyLocalizedString>
 #include <KUrlMimeData>
 
 #include <QElapsedTimer>
 #include <QMimeData>
+#include <QMimeDatabase>
 #include <QTimer>
 #include <QWidget>
+#include <QRecursiveMutex>
+#include <QIcon>
+#include <algorithm>
+#include <klazylocalizedstring.h>
+
+Q_GLOBAL_STATIC(QRecursiveMutex, s_collatorMutex)
 
 // #define KFILEITEMMODEL_DEBUG
 
@@ -40,6 +37,7 @@ KFileItemModel::KFileItemModel(QObject* parent) :
     KItemModelBase("text", parent),
     m_dirLister(nullptr),
     m_sortDirsFirst(true),
+    m_sortHiddenLast(false),
     m_sortRole(NameRole),
     m_sortingProgressPercent(-1),
     m_roles(),
@@ -59,7 +57,8 @@ KFileItemModel::KFileItemModel(QObject* parent) :
 
     loadSortingSettings();
 
-    m_dirLister = new KFileItemModelDirLister(this);
+    m_dirLister = new KDirLister(this);
+    m_dirLister->setAutoErrorHandlingEnabled(false);
     m_dirLister->setDelayedMimeTypes(true);
 
     const QWidget* parentWidget = qobject_cast<QWidget*>(parent);
@@ -67,18 +66,17 @@ KFileItemModel::KFileItemModel(QObject* parent) :
         m_dirLister->setMainWindow(parentWidget->window());
     }
 
-    connect(m_dirLister, &KFileItemModelDirLister::started, this, &KFileItemModel::directoryLoadingStarted);
-    connect(m_dirLister, QOverload<>::of(&KCoreDirLister::canceled), this, &KFileItemModel::slotCanceled);
-    connect(m_dirLister, QOverload<const QUrl&>::of(&KCoreDirLister::completed), this, &KFileItemModel::slotCompleted);
-    connect(m_dirLister, &KFileItemModelDirLister::itemsAdded, this, &KFileItemModel::slotItemsAdded);
-    connect(m_dirLister, &KFileItemModelDirLister::itemsDeleted, this, &KFileItemModel::slotItemsDeleted);
-    connect(m_dirLister, &KFileItemModelDirLister::refreshItems, this, &KFileItemModel::slotRefreshItems);
-    connect(m_dirLister, QOverload<>::of(&KCoreDirLister::clear), this, &KFileItemModel::slotClear);
-    connect(m_dirLister, &KFileItemModelDirLister::infoMessage, this, &KFileItemModel::infoMessage);
-    connect(m_dirLister, &KFileItemModelDirLister::errorMessage, this, &KFileItemModel::errorMessage);
-    connect(m_dirLister, &KFileItemModelDirLister::percent, this, &KFileItemModel::directoryLoadingProgress);
-    connect(m_dirLister, QOverload<const QUrl&, const QUrl&>::of(&KCoreDirLister::redirection), this, &KFileItemModel::directoryRedirection);
-    connect(m_dirLister, &KFileItemModelDirLister::urlIsFileError, this, &KFileItemModel::urlIsFileError);
+    connect(m_dirLister, &KCoreDirLister::started, this, &KFileItemModel::directoryLoadingStarted);
+    connect(m_dirLister, &KCoreDirLister::canceled, this, &KFileItemModel::slotCanceled);
+    connect(m_dirLister, &KCoreDirLister::itemsAdded, this, &KFileItemModel::slotItemsAdded);
+    connect(m_dirLister, &KCoreDirLister::itemsDeleted, this, &KFileItemModel::slotItemsDeleted);
+    connect(m_dirLister, &KCoreDirLister::refreshItems, this, &KFileItemModel::slotRefreshItems);
+    connect(m_dirLister, &KCoreDirLister::clear, this, &KFileItemModel::slotClear);
+    connect(m_dirLister, &KCoreDirLister::infoMessage, this, &KFileItemModel::infoMessage);
+    connect(m_dirLister, &KCoreDirLister::jobError, this, &KFileItemModel::slotListerError);
+    connect(m_dirLister, &KCoreDirLister::percent, this, &KFileItemModel::directoryLoadingProgress);
+    connect(m_dirLister, &KCoreDirLister::redirection, this, &KFileItemModel::directoryRedirection);
+    connect(m_dirLister, &KCoreDirLister::listingDirCompleted, this, &KFileItemModel::slotCompleted);
 
     // Apply default roles that should be determined
     resetRoles();
@@ -154,6 +152,18 @@ QHash<QByteArray, QVariant> KFileItemModel::data(int index) const
         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()) {
+            // Special case dealt by slotRefreshItems(), avoid losing the "isExpanded" and "expandedParentsCount" state when refreshing
+            // slotRefreshItems() makes sure folders keep the "isExpanded" and "expandedParentsCount" while clearing the remaining values
+            // so this special request of different behavior can be identified here.
+            bool hasExpandedParentsCount = false;
+            const int expandedParentsCount = data->values.value("expandedParentsCount").toInt(&hasExpandedParentsCount);
+
+            data->values = retrieveData(data->item, data->parent);
+            data->values.insert("isExpanded", true);
+            if (hasExpandedParentsCount) {
+                data->values.insert("expandedParentsCount", expandedParentsCount);
+            }
         }
 
         return data->values;
@@ -213,6 +223,19 @@ bool KFileItemModel::sortDirectoriesFirst() const
     return m_sortDirsFirst;
 }
 
+void KFileItemModel::setSortHiddenLast(bool hiddenLast)
+{
+    if (hiddenLast != m_sortHiddenLast) {
+        m_sortHiddenLast = hiddenLast;
+        resortAllItems();
+    }
+}
+
+bool KFileItemModel::sortHiddenLast() const
+{
+    return m_sortHiddenLast;
+}
+
 void KFileItemModel::setShowHiddenFiles(bool show)
 {
     m_dirLister->setShowingDotFiles(show);
@@ -243,7 +266,7 @@ QMimeData* KFileItemModel::createMimeData(const KItemSet& indexes) const
 
     // The following code has been taken from KDirModel::mimeData()
     // (kdelibs/kio/kio/kdirmodel.cpp)
-    // Copyright (C) 2006 David Faure <faure@kde.org>
+    // SPDX-FileCopyrightText: 2006 David Faure <faure@kde.org>
     QList<QUrl> urls;
     QList<QUrl> mostLocalUrls;
     const ItemData* lastAddedItem = nullptr;
@@ -267,7 +290,7 @@ QMimeData* KFileItemModel::createMimeData(const KItemSet& indexes) const
             urls << item.url();
 
             bool isLocal;
-            mostLocalUrls << item.mostLocalUrl(isLocal);
+            mostLocalUrls << item.mostLocalUrl(&isLocal);
         }
     }
 
@@ -304,10 +327,10 @@ QString KFileItemModel::roleDescription(const QByteArray& role) const
         int count = 0;
         const RoleInfoMap* map = rolesInfoMap(count);
         for (int i = 0; i < count; ++i) {
-            if (!map[i].roleTranslation) {
+            if (map[i].roleTranslation.isEmpty()) {
                 continue;
             }
-            description.insert(map[i].role, i18nc(map[i].roleTranslationContext, map[i].roleTranslation));
+            description.insert(map[i].role, map[i].roleTranslation.toString());
         }
     }
 
@@ -429,7 +452,8 @@ int KFileItemModel::index(const QUrl& url) const
                 indexesForUrl.insert(m_itemData.at(i)->item.url(), i);
             }
 
-            foreach (const QUrl& url, indexesForUrl.uniqueKeys()) {
+            const auto uniqueKeys = indexesForUrl.uniqueKeys();
+            for (const QUrl& url : uniqueKeys) {
                 if (indexesForUrl.count(url) > 1) {
                     qCWarning(DolphinDebug) << "Multiple items found with the URL" << url;
 
@@ -495,7 +519,7 @@ void KFileItemModel::setRoles(const QSet<QByteArray>& roles)
             m_itemData[i]->values = retrieveData(m_itemData.at(i)->item, m_itemData.at(i)->parent);
         }
 
-        emit itemsChanged(KItemRangeList() << KItemRange(0, count()), changedRoles);
+        Q_EMIT itemsChanged(KItemRangeList() << KItemRange(0, count()), changedRoles);
     }
 
     // Clear the 'values' of all filtered items. They will be re-populated with the
@@ -533,7 +557,7 @@ bool KFileItemModel::setExpanded(int index, bool expanded)
         m_dirLister->openUrl(url, KDirLister::Keep);
 
         const QVariantList previouslyExpandedChildren = m_itemData.at(index)->values.value("previouslyExpandedChildren").value<QVariantList>();
-        foreach (const QVariant& var, previouslyExpandedChildren) {
+        for (const QVariant& var : previouslyExpandedChildren) {
             m_urlsToExpand.insert(var.toUrl());
         }
     } else {
@@ -635,7 +659,7 @@ void KFileItemModel::expandParentDirectories(const QUrl &url)
     // first subdir can be empty, if m_dirLister->url().path() does not end with '/'
     // this happens if baseUrl is not root but a home directory, see FoldersPanel,
     // so using QString::SkipEmptyParts
-    const QStringList subDirs = url.path().mid(pos).split(QDir::separator(), QString::SkipEmptyParts);
+    const QStringList subDirs = url.path().mid(pos).split(QDir::separator(), Qt::SkipEmptyParts);
     for (int i = 0; i < subDirs.count() - 1; ++i) {
         QString path = urlToExpand.path();
         if (!path.endsWith(QLatin1Char('/'))) {
@@ -686,45 +710,87 @@ QStringList KFileItemModel::mimeTypeFilters() const
     return m_filter.mimeTypes();
 }
 
-
 void KFileItemModel::applyFilters()
 {
-    // Check which shown items from m_itemData must get
-    // hidden and hence moved to m_filteredItems.
-    QVector<int> newFilteredIndexes;
+    // ===STEP 1===
+    // Check which previously shown items from m_itemData must now get
+    // hidden and hence moved from m_itemData into m_filteredItems.
 
-    const int itemCount = m_itemData.count();
-    for (int index = 0; index < itemCount; ++index) {
-        ItemData* itemData = m_itemData.at(index);
-
-        // Only filter non-expanded items as child items may never
-        // exist without a parent item
-        if (!itemData->values.value("isExpanded").toBool()) {
-            const KFileItem item = itemData->item;
-            if (!m_filter.matches(item)) {
-                newFilteredIndexes.append(index);
-                m_filteredItems.insert(item, itemData);
-            }
+    QList<int> newFilteredIndexes; // This structure is good for prepending. We will want an ascending sorted Container at the end, this will do fine.
+
+    // This pointer will refer to the next confirmed shown item from the point of
+    // view of the current "itemData" in the upcoming "for" loop.
+    ItemData *itemShownBelow = nullptr;
+
+    // We will iterate backwards because it's convenient to know beforehand if the item just below is its child or not.
+    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)) {
+            // 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
+
+            // So this item must remain shown.
+            // Lets register this item as the next shown item from the point of view of the next iteration of this for loop
+            itemShownBelow = itemData;
+        } else {
+            // We hide this item for now, however, for expanded folders this is not final:
+            // if after the next "for" loop we discover that its children must now be shown with the newly applied fliter, we shall re-insert it
+            newFilteredIndexes.prepend(index);
+            m_filteredItems.insert(itemData->item, itemData);
+            // indexShownBelow doesn't get updated since this item will be hidden
         }
     }
 
-    const KItemRangeList removedRanges = KItemRangeList::fromSortedContainer(newFilteredIndexes);
-    removeItems(removedRanges, KeepItemData);
+    // This will remove the newly filtered items from m_itemData
+    removeItems(KItemRangeList::fromSortedContainer(newFilteredIndexes), KeepItemData);
 
+    // ===STEP 2===
     // Check which hidden items from m_filteredItems should
-    // get visible again and hence removed from m_filteredItems.
-    QList<ItemData*> newVisibleItems;
+    // become visible again and hence moved from m_filteredItems back into m_itemData.
 
-    QHash<KFileItem, ItemData*>::iterator it = m_filteredItems.begin();
+    QList<ItemData *> newVisibleItems;
+
+    QHash<KFileItem, ItemData *> ancestorsOfNewVisibleItems; // We will make sure these also become visible in step 3.
+
+    QHash<KFileItem, ItemData *>::iterator it = m_filteredItems.begin();
     while (it != m_filteredItems.end()) {
         if (m_filter.matches(it.key())) {
             newVisibleItems.append(it.value());
+
+            // 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.
+            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
+                // and it would mess up the iteration. We will mark it to be removed in step 3.
+                ancestorsOfNewVisibleItems.insert(parent->item, parent);
+            }
+
             it = m_filteredItems.erase(it);
         } else {
+            // Item remains filtered for now
+            // However, for expanded folders this is not final, we may discover later that it has unfiltered descendants.
             ++it;
         }
     }
 
+    // ===STEP 3===
+    // Handles the ancestorsOfNewVisibleItems.
+    // Now that we are done iterating through m_filteredItems we can safely move the ancestorsOfNewVisibleItems from m_filteredItems to newVisibleItems.
+    for (it = ancestorsOfNewVisibleItems.begin(); it != ancestorsOfNewVisibleItems.end(); it++) {
+        if (m_filteredItems.remove(it.key())) {
+            // m_filteredItems still contained this ancestor until now so we can be sure that we aren't adding a duplicate ancestor to newVisibleItems.
+            newVisibleItems.append(it.value());
+        }
+    }
+
+    // This will insert the newly discovered unfiltered items into m_itemData
     insertItems(newVisibleItems);
 }
 
@@ -737,7 +803,7 @@ void KFileItemModel::removeFilteredChildren(const KItemRangeList& itemRanges)
     }
 
     QSet<ItemData*> parents;
-    foreach (const KItemRange& range, itemRanges) {
+    for (const KItemRange& range : itemRanges) {
         for (int index = range.index; index < range.index + range.count; ++index) {
             parents.insert(m_itemData.at(index));
         }
@@ -764,9 +830,9 @@ QList<KFileItemModel::RoleInfo> KFileItemModel::rolesInformation()
             if (map[i].roleType != NoRole) {
                 RoleInfo info;
                 info.role = map[i].role;
-                info.translation = i18nc(map[i].roleTranslationContext, map[i].roleTranslation);
-                if (map[i].groupTranslation) {
-                    info.group = i18nc(map[i].groupTranslationContext, map[i].groupTranslation);
+                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
@@ -785,13 +851,13 @@ QList<KFileItemModel::RoleInfo> KFileItemModel::rolesInformation()
 
 void KFileItemModel::onGroupedSortingChanged(bool current)
 {
-    Q_UNUSED(current);
+    Q_UNUSED(current)
     m_groups.clear();
 }
 
 void KFileItemModel::onSortRoleChanged(const QByteArray& current, const QByteArray& previous, bool resortItems)
 {
-    Q_UNUSED(previous);
+    Q_UNUSED(previous)
     m_sortRole = typeForRole(current);
 
     if (!m_requestRole[m_sortRole]) {
@@ -807,8 +873,8 @@ void KFileItemModel::onSortRoleChanged(const QByteArray& current, const QByteArr
 
 void KFileItemModel::onSortOrderChanged(Qt::SortOrder current, Qt::SortOrder previous)
 {
-    Q_UNUSED(current);
-    Q_UNUSED(previous);
+    Q_UNUSED(current)
+    Q_UNUSED(previous)
     resortAllItems();
 }
 
@@ -857,7 +923,7 @@ void KFileItemModel::resortAllItems()
     // been moved because of the resorting.
     QList<QUrl> oldUrls;
     oldUrls.reserve(itemCount);
-    foreach (const ItemData* itemData, m_itemData) {
+    for (const ItemData* itemData : qAsConst(m_itemData)) {
         oldUrls.append(itemData->item.url());
     }
 
@@ -900,13 +966,13 @@ void KFileItemModel::resortAllItems()
             movedToIndexes.append(newIndex);
         }
 
-        emit itemsMoved(KItemRange(firstMovedIndex, movedItemsCount), movedToIndexes);
+        Q_EMIT itemsMoved(KItemRange(firstMovedIndex, movedItemsCount), movedToIndexes);
     } else if (groupedSorting()) {
         // The groups might have changed even if the order of the items has not.
         const QList<QPair<int, QVariant> > oldGroups = m_groups;
         m_groups.clear();
         if (groups() != oldGroups) {
-            emit groupsChanged();
+            Q_EMIT groupsChanged();
         }
     }
 
@@ -917,6 +983,7 @@ void KFileItemModel::resortAllItems()
 
 void KFileItemModel::slotCompleted()
 {
+    m_maximumUpdateIntervalTimer->stop();
     dispatchPendingItemsToInsert();
 
     if (!m_urlsToExpand.isEmpty()) {
@@ -924,7 +991,9 @@ void KFileItemModel::slotCompleted()
         // Note that the parent folder must be expanded before any of its subfolders become visible.
         // Therefore, some URLs in m_restoredExpandedUrls might not be visible yet
         // -> we expand the first visible URL we find in m_restoredExpandedUrls.
-        foreach (const QUrl& url, m_urlsToExpand) {
+        // Iterate over a const copy because items are deleted and inserted within the loop
+        const auto urlsToExpand = m_urlsToExpand;
+        for(const QUrl &url : urlsToExpand) {
             const int indexForUrl = index(url);
             if (indexForUrl >= 0) {
                 m_urlsToExpand.remove(url);
@@ -941,7 +1010,7 @@ void KFileItemModel::slotCompleted()
         m_urlsToExpand.clear();
     }
 
-    emit directoryLoadingCompleted();
+    Q_EMIT directoryLoadingCompleted();
 }
 
 void KFileItemModel::slotCanceled()
@@ -949,19 +1018,14 @@ void KFileItemModel::slotCanceled()
     m_maximumUpdateIntervalTimer->stop();
     dispatchPendingItemsToInsert();
 
-    emit directoryLoadingCanceled();
+    Q_EMIT directoryLoadingCanceled();
 }
 
 void KFileItemModel::slotItemsAdded(const QUrl &directoryUrl, const KFileItemList& items)
 {
     Q_ASSERT(!items.isEmpty());
 
-    QUrl parentUrl;
-    if (m_expandedDirs.contains(directoryUrl)) {
-        parentUrl = m_expandedDirs.value(directoryUrl);
-    } else {
-        parentUrl = directoryUrl.adjusted(QUrl::StripTrailingSlash);
-    }
+    const QUrl parentUrl = m_expandedDirs.value(directoryUrl, directoryUrl.adjusted(QUrl::StripTrailingSlash));
 
     if (m_requestRole[ExpandedParentsCountRole]) {
         // If the expanding of items is enabled, the call
@@ -990,28 +1054,78 @@ void KFileItemModel::slotItemsAdded(const QUrl &directoryUrl, const KFileItemLis
         }
     }
 
-    QList<ItemData*> itemDataList = createItemDataList(parentUrl, items);
+    const QList<ItemData*> itemDataList = createItemDataList(parentUrl, items);
 
     if (!m_filter.hasSetFilters()) {
         m_pendingItemsToInsert.append(itemDataList);
     } else {
+        QSet<ItemData *> parentsToEnsureVisible;
+
         // The name or type filter is active. Hide filtered items
         // before inserting them into the model and remember
         // the filtered items in m_filteredItems.
-        foreach (ItemData* itemData, itemDataList) {
+        for (ItemData* itemData : itemDataList) {
             if (m_filter.matches(itemData->item)) {
                 m_pendingItemsToInsert.append(itemData);
+                if (itemData->parent) {
+                    parentsToEnsureVisible.insert(itemData->parent);
+                }
             } else {
                 m_filteredItems.insert(itemData->item, itemData);
             }
         }
+
+        // Entire parental chains must be shown
+        for (ItemData *parent : parentsToEnsureVisible) {
+            for (; parent && m_filteredItems.remove(parent->item); parent = parent->parent) {
+                m_pendingItemsToInsert.append(parent);
+            }
+        }
     }
 
-    if (useMaximumUpdateInterval() && !m_maximumUpdateIntervalTimer->isActive()) {
+    if (!m_maximumUpdateIntervalTimer->isActive()) {
         // Assure that items get dispatched if no completed() or canceled() signal is
         // emitted during the maximum update interval.
         m_maximumUpdateIntervalTimer->start();
     }
+
+    Q_EMIT fileItemsChanged({KFileItem(directoryUrl)});
+}
+
+int KFileItemModel::filterChildlessParents(KItemRangeList &removedItemRanges, const QSet<ItemData *> &parentsToEnsureVisible)
+{
+    int filteredParentsCount = 0;
+    // The childless parents not yet removed will always be right above the start of a removed range.
+    // We iterate backwards to ensure the deepest folders are processed before their parents
+    for (int i = removedItemRanges.size() - 1; i >= 0; i--) {
+        KItemRange itemRange = removedItemRanges.at(i);
+        const ItemData *const firstInRange = m_itemData.at(itemRange.index);
+        ItemData *itemAbove = itemRange.index - 1 >= 0 ? m_itemData.at(itemRange.index - 1) : nullptr;
+        const ItemData *const itemBelow = itemRange.index + itemRange.count < m_itemData.count() ? m_itemData.at(itemRange.index + itemRange.count) : nullptr;
+
+        if (itemAbove && firstInRange->parent == itemAbove && !m_filter.matches(itemAbove->item) && (!itemBelow || itemBelow->parent != itemAbove)
+            && !parentsToEnsureVisible.contains(itemAbove)) {
+            // The item above exists, is the parent, doesn't pass the filter, does not belong to parentsToEnsureVisible
+            // and this deleted range covers all of its descendents, so none will be left.
+            m_filteredItems.insert(itemAbove->item, itemAbove);
+            // This range's starting index will be extended to include the parent above:
+            --itemRange.index;
+            ++itemRange.count;
+            ++filteredParentsCount;
+            KItemRange previousRange = i > 0 ? removedItemRanges.at(i - 1) : KItemRange();
+            // We must check if this caused the range to touch the previous range, if that's the case they shall be merged
+            if (i > 0 && previousRange.index + previousRange.count == itemRange.index) {
+                previousRange.count += itemRange.count;
+                removedItemRanges.replace(i - 1, previousRange);
+                removedItemRanges.removeAt(i);
+            } else {
+                removedItemRanges.replace(i, itemRange);
+                // We must revisit this range in the next iteration since its starting index changed
+                ++i;
+            }
+        }
+    }
+    return filteredParentsCount;
 }
 
 void KFileItemModel::slotItemsDeleted(const KFileItemList& items)
@@ -1020,8 +1134,9 @@ void KFileItemModel::slotItemsDeleted(const KFileItemList& items)
 
     QVector<int> indexesToRemove;
     indexesToRemove.reserve(items.count());
+    KFileItemList dirsChanged;
 
-    foreach (const KFileItem& item, items) {
+    for (const KFileItem& item : items) {
         const int indexForItem = index(item);
         if (indexForItem >= 0) {
             indexesToRemove.append(indexForItem);
@@ -1033,6 +1148,11 @@ void KFileItemModel::slotItemsDeleted(const KFileItemList& items)
                 m_filteredItems.erase(it);
             }
         }
+
+        QUrl parentUrl = item.url().adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash);
+        if (dirsChanged.findByUrl(parentUrl).isNull()) {
+            dirsChanged << KFileItem(parentUrl);
+        }
     }
 
     std::sort(indexesToRemove.begin(), indexesToRemove.end());
@@ -1043,7 +1163,7 @@ void KFileItemModel::slotItemsDeleted(const KFileItemList& items)
         indexesToRemoveWithChildren.reserve(m_itemData.count());
 
         const int itemCount = m_itemData.count();
-        foreach (int index, indexesToRemove) {
+        for (int index : qAsConst(indexesToRemove)) {
             indexesToRemoveWithChildren.append(index);
 
             const int parentLevel = expandedParentsCount(index);
@@ -1057,9 +1177,19 @@ void KFileItemModel::slotItemsDeleted(const KFileItemList& items)
         indexesToRemove = indexesToRemoveWithChildren;
     }
 
-    const KItemRangeList itemRanges = KItemRangeList::fromSortedContainer(indexesToRemove);
+    KItemRangeList itemRanges = KItemRangeList::fromSortedContainer(indexesToRemove);
     removeFilteredChildren(itemRanges);
-    removeItems(itemRanges, DeleteItemData);
+
+    // This call will update itemRanges to include the childless parents that have been filtered.
+    const int filteredParentsCount = filterChildlessParents(itemRanges);
+
+    // If any childless parents were filtered, then itemRanges got updated and now contains items that were really deleted
+    // mixed with expanded folders that are just being filtered out.
+    // If that's the case, we pass 'DeleteItemDataIfUnfiltered' as a hint
+    // so removeItems() will check m_filteredItems to differentiate which is which.
+    removeItems(itemRanges, filteredParentsCount > 0 ? DeleteItemDataIfUnfiltered : DeleteItemData);
+
+    Q_EMIT fileItemsChanged(dirsChanged);
 }
 
 void KFileItemModel::slotRefreshItems(const QList<QPair<KFileItem, KFileItem> >& items)
@@ -1074,59 +1204,151 @@ void KFileItemModel::slotRefreshItems(const QList<QPair<KFileItem, KFileItem> >&
     indexes.reserve(items.count());
 
     QSet<QByteArray> changedRoles;
+    KFileItemList changedFiles;
+
+    // Contains the indexes of the currently visible items
+    // that should get hidden and hence moved to m_filteredItems.
+    QVector<int> newFilteredIndexes;
+
+    // Contains currently hidden items that should
+    // get visible and hence removed from m_filteredItems
+    QList<ItemData*> newVisibleItems;
 
     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 int indexForItem = index(oldItem);
+        const bool newItemMatchesFilter = m_filter.matches(newItem);
         if (indexForItem >= 0) {
             m_itemData[indexForItem]->item = newItem;
 
             // 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.
-            QHashIterator<QByteArray, QVariant> it(retrieveData(newItem, m_itemData.at(indexForItem)->parent));
-            QHash<QByteArray, QVariant>& values = m_itemData[indexForItem]->values;
+            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();
-                if (values.value(role) != it.value()) {
-                    values.insert(role, it.value());
+                if (itemData->values.value(role) != it.value()) {
+                    itemData->values.insert(role, it.value());
                     changedRoles.insert(role);
                 }
             }
 
             m_items.remove(oldItem.url());
+            // We must maintain m_items consistent with m_itemData for now, this very loop is using it.
+            // We leave it to be cleared by removeItems() later, when m_itemData actually gets updated.
             m_items.insert(newItem.url(), indexForItem);
-            indexes.append(indexForItem);
+            if (newItemMatchesFilter
+                || (itemData->values.value("isExpanded").toBool()
+                    && (indexForItem + 1 < m_itemData.count() && m_itemData.at(indexForItem + 1)->parent == itemData))) {
+                // We are lenient with expanded folders that originally had visible children.
+                // If they become childless now they will be caught by filterChildlessParents()
+                changedFiles.append(newItem);
+                indexes.append(indexForItem);
+            } else {
+                newFilteredIndexes.append(indexForItem);
+                m_filteredItems.insert(newItem, itemData);
+            }
         } else {
             // Check if 'oldItem' is one of the filtered items.
             QHash<KFileItem, ItemData*>::iterator it = m_filteredItems.find(oldItem);
             if (it != m_filteredItems.end()) {
-                ItemData* itemData = it.value();
+                ItemData *const itemData = it.value();
                 itemData->item = newItem;
 
                 // The data stored in 'values' might have changed. Therefore, we clear
                 // 'values' and re-populate it the next time it is requested via data(int).
+                // Before clearing, we must remember if it was expanded and the expanded parents count,
+                // otherwise these states would be lost. The data() method will deal with this special case.
+                const bool isExpanded = itemData->values.value("isExpanded").toBool();
+                bool hasExpandedParentsCount = false;
+                const int expandedParentsCount = itemData->values.value("expandedParentsCount").toInt(&hasExpandedParentsCount);
                 itemData->values.clear();
+                if (isExpanded) {
+                    itemData->values.insert("isExpanded", true);
+                    if (hasExpandedParentsCount) {
+                        itemData->values.insert("expandedParentsCount", expandedParentsCount);
+                    }
+                }
 
                 m_filteredItems.erase(it);
-                m_filteredItems.insert(newItem, itemData);
+                if (newItemMatchesFilter) {
+                    newVisibleItems.append(itemData);
+                } else {
+                    m_filteredItems.insert(newItem, itemData);
+                }
+            }
+        }
+    }
+
+    std::sort(newFilteredIndexes.begin(), newFilteredIndexes.end());
+
+    // We must keep track of parents of new visible items since they must be shown no matter what
+    // They will be considered "immune" to filterChildlessParents()
+    QSet<ItemData *> parentsToEnsureVisible;
+
+    for (ItemData *item : newVisibleItems) {
+        for (ItemData *parent = item->parent; parent && !parentsToEnsureVisible.contains(parent); parent = parent->parent) {
+            parentsToEnsureVisible.insert(parent);
+        }
+    }
+    for (ItemData *parent : parentsToEnsureVisible) {
+        // We make sure they are all unfiltered.
+        if (m_filteredItems.remove(parent->item)) {
+            // If it is being unfiltered now, we mark it to be inserted by appending it to newVisibleItems
+            newVisibleItems.append(parent);
+            // It could be in newFilteredIndexes, we must remove it if it's there:
+            const int parentIndex = index(parent->item);
+            if (parentIndex >= 0) {
+                QVector<int>::iterator it = std::lower_bound(newFilteredIndexes.begin(), newFilteredIndexes.end(), parentIndex);
+                if (it != newFilteredIndexes.end() && *it == parentIndex) {
+                    newFilteredIndexes.erase(it);
+                }
             }
         }
     }
 
+    KItemRangeList removedRanges = KItemRangeList::fromSortedContainer(newFilteredIndexes);
+
+    // This call will update itemRanges to include the childless parents that have been filtered.
+    filterChildlessParents(removedRanges, parentsToEnsureVisible);
+
+    removeItems(removedRanges, KeepItemData);
+
+    // Show previously hidden items that should get visible
+    insertItems(newVisibleItems);
+
+    // Final step: we will emit 'itemsChanged' and 'fileItemsChanged' signals and trigger the asynchronous re-sorting logic.
+
     // If the changed items have been created recently, they might not be in m_items yet.
     // In that case, the list 'indexes' might be empty.
     if (indexes.isEmpty()) {
         return;
     }
 
+    if (newVisibleItems.count() > 0 || removedRanges.count() > 0) {
+        // The original indexes have changed and are now worthless since items were removed and/or inserted.
+        indexes.clear();
+        // m_items is not yet rebuilt at this point, so we use our own means to resolve the new indexes.
+        const QSet<const KFileItem> changedFilesSet(changedFiles.cbegin(), changedFiles.cend());
+        for (int i = 0; i < m_itemData.count(); i++) {
+            if (changedFilesSet.contains(m_itemData.at(i)->item)) {
+                indexes.append(i);
+            }
+        }
+    } else {
+        std::sort(indexes.begin(), indexes.end());
+    }
+
     // Extract the item-ranges out of the changed indexes
-    std::sort(indexes.begin(), indexes.end());
     const KItemRangeList itemRangeList = KItemRangeList::fromSortedContainer(indexes);
     emitItemsChangedAndTriggerResorting(itemRangeList, changedRoles);
+
+    Q_EMIT fileItemsChanged(changedFiles);
 }
 
 void KFileItemModel::slotClear()
@@ -1150,7 +1372,7 @@ void KFileItemModel::slotClear()
         qDeleteAll(m_itemData);
         m_itemData.clear();
         m_items.clear();
-        emit itemsRemoved(KItemRangeList() << KItemRange(0, removedCount));
+        Q_EMIT itemsRemoved(KItemRangeList() << KItemRange(0, removedCount));
     }
 
     m_expandedDirs.clear();
@@ -1186,12 +1408,20 @@ void KFileItemModel::insertItems(QList<ItemData*>& newItems)
     m_groups.clear();
     prepareItemsForSorting(newItems);
 
-    if (m_sortRole == NameRole && m_naturalSorting) {
-        // Natural sorting of items can be very slow. However, it becomes much
-        // faster if the input sequence is already mostly sorted. Therefore, we
-        // first sort 'newItems' according to the QStrings returned by
-        // KFileItem::text() using QString::operator<(), which is quite fast.
-        parallelMergeSort(newItems.begin(), newItems.end(), nameLessThan, QThread::idealThreadCount());
+    // Natural sorting of items can be very slow. However, it becomes much faster
+    // if the input sequence is already mostly sorted. Therefore, we first sort
+    // 'newItems' according to the QStrings using QString::operator<(), which is quite fast.
+    if (m_naturalSorting) {
+        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)
+            {
+                const QByteArray role = roleForType(m_sortRole);
+                return a->values.value(role).toString() < b->values.value(role).toString();
+            };
+            parallelMergeSort(newItems.begin(), newItems.end(), lambdaLessThan, QThread::idealThreadCount());
+        }
     }
 
     sort(newItems.begin(), newItems.end());
@@ -1258,7 +1488,7 @@ void KFileItemModel::insertItems(QList<ItemData*>& newItems)
     // It will be re-populated with the updated indices if index(const QUrl&) is called.
     m_items.clear();
 
-    emit itemsInserted(itemRanges);
+    Q_EMIT itemsInserted(itemRanges);
 
 #ifdef KFILEITEMMODEL_DEBUG
     qCDebug(DolphinDebug) << "[TIME] Inserting of" << newItems.count() << "items:" << timer.elapsed();
@@ -1275,11 +1505,11 @@ void KFileItemModel::removeItems(const KItemRangeList& itemRanges, RemoveItemsBe
 
     // Step 1: Remove the items from m_itemData, and free the ItemData.
     int removedItemsCount = 0;
-    foreach (const KItemRange& range, itemRanges) {
+    for (const KItemRange& range : itemRanges) {
         removedItemsCount += range.count;
 
         for (int index = range.index; index < range.index + range.count; ++index) {
-            if (behavior == DeleteItemData) {
+            if (behavior == DeleteItemData || (behavior == DeleteItemDataIfUnfiltered && !m_filteredItems.contains(m_itemData.at(index)->item))) {
                 delete m_itemData.at(index);
             }
 
@@ -1311,7 +1541,7 @@ void KFileItemModel::removeItems(const KItemRangeList& itemRanges, RemoveItemsBe
     // It will be re-populated with the updated indices if index(const QUrl&) is called.
     m_items.clear();
 
-    emit itemsRemoved(itemRanges);
+    Q_EMIT itemsRemoved(itemRanges);
 }
 
 QList<KFileItemModel::ItemData*> KFileItemModel::createItemDataList(const QUrl& parentUrl, const KFileItemList& items) const
@@ -1323,13 +1553,14 @@ QList<KFileItemModel::ItemData*> KFileItemModel::createItemDataList(const QUrl&
         determineMimeTypes(items, 200);
     }
 
+    // We search for the parent in m_itemData and then in m_filteredItems if necessary
     const int parentIndex = index(parentUrl);
-    ItemData* parentItem = parentIndex < 0 ? nullptr : m_itemData.at(parentIndex);
+    ItemData *parentItem = parentIndex < 0 ? m_filteredItems.value(KFileItem(parentUrl), nullptr) : m_itemData.at(parentIndex);
 
     QList<ItemData*> itemDataList;
     itemDataList.reserve(items.count());
 
-    foreach (const KFileItem& item, items) {
+    for (const KFileItem& item : items) {
         ItemData* itemData = new ItemData();
         itemData->item = item;
         itemData->parent = parentItem;
@@ -1350,7 +1581,7 @@ void KFileItemModel::prepareItemsForSorting(QList<ItemData*>& itemDataList)
     case DeletionTimeRole:
         // These roles can be determined with retrieveData, and they have to be stored
         // in the QHash "values" for the sorting.
-        foreach (ItemData* itemData, itemDataList) {
+        for (ItemData* itemData : qAsConst(itemDataList)) {
             if (itemData->values.isEmpty()) {
                 itemData->values = retrieveData(itemData->item, itemData->parent);
             }
@@ -1359,7 +1590,7 @@ void KFileItemModel::prepareItemsForSorting(QList<ItemData*>& itemDataList)
 
     case TypeRole:
         // At least store the data including the file type for items with known MIME type.
-        foreach (ItemData* itemData, itemDataList) {
+        for (ItemData* itemData : qAsConst(itemDataList)) {
             if (itemData->values.isEmpty()) {
                 const KFileItem item = itemData->item;
                 if (item.isDir() || item.isMimeTypeKnown()) {
@@ -1427,12 +1658,12 @@ void KFileItemModel::removeExpandedItems()
 
 void KFileItemModel::emitItemsChangedAndTriggerResorting(const KItemRangeList& itemRanges, const QSet<QByteArray>& changedRoles)
 {
-    emit itemsChanged(itemRanges, 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))) {
-        foreach (const KItemRange& range, itemRanges) {
+        for (const KItemRange& range : itemRanges) {
             bool needsResorting = false;
 
             const int first = range.index;
@@ -1606,7 +1837,7 @@ QHash<QByteArray, QVariant> KFileItemModel::retrieveData(const KFileItem& item,
     if (m_requestRole[DestinationRole]) {
         QString destination = item.linkDest();
         if (destination.isEmpty()) {
-            destination = QStringLiteral("-");
+            destination = QLatin1Char('-');
         }
         data.insert(sharedValue("destination"), destination);
     }
@@ -1654,7 +1885,13 @@ QHash<QByteArray, QVariant> KFileItemModel::retrieveData(const KFileItem& item,
     }
 
     if (item.isMimeTypeKnown()) {
-        data.insert(sharedValue("iconName"), item.iconName());
+        QString iconName = item.iconName();
+        if (!QIcon::hasThemeIcon(iconName)) {
+            QMimeType mimeType = QMimeDatabase().mimeTypeForName(item.mimetype());
+            iconName = mimeType.genericIconName();
+        }
+
+        data.insert(sharedValue("iconName"), iconName);
 
         if (m_requestRole[TypeRole]) {
             data.insert(sharedValue("type"), item.mimeComment());
@@ -1702,7 +1939,18 @@ bool KFileItemModel::lessThan(const ItemData* a, const ItemData* b, const QColla
         }
     }
 
-    if (m_sortDirsFirst || m_sortRole == SizeRole) {
+    // 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) {
@@ -1725,8 +1973,8 @@ void KFileItemModel::sort(const QList<KFileItemModel::ItemData*>::iterator &begi
         return lessThan(a, b, m_collator);
     };
 
-    if (m_sortRole == NameRole) {
-        // Sorting by name can be expensive, in particular if natural sorting is
+    if (m_sortRole == NameRole || isRoleValueNatural(m_sortRole)) {
+        // Sorting by string can be expensive, in particular if natural sorting is
         // enabled. Use all CPU cores to speed up the sorting process.
         static const int numberOfThreads = QThread::idealThreadCount();
         parallelMergeSort(begin, end, lambdaLessThan, numberOfThreads);
@@ -1740,6 +1988,11 @@ void KFileItemModel::sort(const QList<KFileItemModel::ItemData*>::iterator &begi
 
 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;
 
@@ -1751,33 +2004,43 @@ int KFileItemModel::sortRoleCompare(const ItemData* a, const ItemData* b, const
         break;
 
     case SizeRole: {
-        if (itemA.isDir()) {
-            // See "if (m_sortFoldersFirst || m_sortRole == SizeRole)" in KFileItemModel::lessThan():
-            Q_ASSERT(itemB.isDir());
-
-            const QVariant valueA = a->values.value("size");
-            const QVariant valueB = b->values.value("size");
-            if (valueA.isNull() && valueB.isNull()) {
-                result = 0;
-            } else if (valueA.isNull()) {
-                result = -1;
+        if (DetailsModeSettings::directorySizeCount() && itemA.isDir()) {
+            // folders first then
+            // items A and B are folders thanks to lessThan checks
+            auto valueA = a->values.value("count");
+            auto valueB = b->values.value("count");
+            if (valueA.isNull()) {
+                if (!valueB.isNull()) {
+                    return -1;
+                }
             } else if (valueB.isNull()) {
-                result = +1;
+                return +1;
             } else {
-                result = valueA.toInt() - valueB.toInt();
+                if (valueA.toLongLong() < valueB.toLongLong()) {
+                    return -1;
+                } else if (valueA.toLongLong() > valueB.toLongLong()) {
+                    return +1;
+                }
             }
+            break;
+        }
+
+        KIO::filesize_t sizeA = 0;
+        if (itemA.isDir()) {
+            sizeA = a->values.value("size").toULongLong();
         } else {
-            // See "if (m_sortFoldersFirst || m_sortRole == SizeRole)" in KFileItemModel::lessThan():
-            Q_ASSERT(!itemB.isDir());
-            const KIO::filesize_t sizeA = itemA.size();
-            const KIO::filesize_t sizeB = itemB.size();
-            if (sizeA > sizeB) {
-                result = +1;
-            } else if (sizeA < sizeB) {
-                result = -1;
-            } else {
-                result = 0;
-            }
+            sizeA = itemA.size();
+        }
+        KIO::filesize_t sizeB = 0;
+        if (itemB.isDir()) {
+            sizeB = b->values.value("size").toULongLong();
+        } else {
+            sizeB = itemB.size();
+        }
+        if (sizeA < sizeB) {
+            return -1;
+        } else if (sizeA > sizeB) {
+            return +1;
         }
         break;
     }
@@ -1786,9 +2049,9 @@ int KFileItemModel::sortRoleCompare(const ItemData* a, const ItemData* b, const
         const long long dateTimeA = itemA.entry().numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME, -1);
         const long long dateTimeB = itemB.entry().numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME, -1);
         if (dateTimeA < dateTimeB) {
-            result = -1;
+            return -1;
         } else if (dateTimeA > dateTimeB) {
-            result = +1;
+            return +1;
         }
         break;
     }
@@ -1797,9 +2060,9 @@ int KFileItemModel::sortRoleCompare(const ItemData* a, const ItemData* b, const
         const long long dateTimeA = itemA.entry().numberValue(KIO::UDSEntry::UDS_CREATION_TIME, -1);
         const long long dateTimeB = itemB.entry().numberValue(KIO::UDSEntry::UDS_CREATION_TIME, -1);
         if (dateTimeA < dateTimeB) {
-            result = -1;
+            return -1;
         } else if (dateTimeA > dateTimeB) {
-            result = +1;
+            return +1;
         }
         break;
     }
@@ -1808,9 +2071,9 @@ int KFileItemModel::sortRoleCompare(const ItemData* a, const ItemData* b, const
         const QDateTime dateTimeA = a->values.value("deletiontime").toDateTime();
         const QDateTime dateTimeB = b->values.value("deletiontime").toDateTime();
         if (dateTimeA < dateTimeB) {
-            result = -1;
+            return -1;
         } else if (dateTimeA > dateTimeB) {
-            result = +1;
+            return +1;
         }
         break;
     }
@@ -1828,8 +2091,17 @@ int KFileItemModel::sortRoleCompare(const ItemData* a, const ItemData* b, const
 
     default: {
         const QByteArray role = roleForType(m_sortRole);
-        result = QString::compare(a->values.value(role).toString(),
-                                  b->values.value(role).toString());
+        const QString roleValueA = a->values.value(role).toString();
+        const QString roleValueB = b->values.value(role).toString();
+        if (!roleValueA.isEmpty() && roleValueB.isEmpty()) {
+            return -1;
+        } else if (roleValueA.isEmpty() && !roleValueB.isEmpty()) {
+            return +1;
+        } else if (isRoleValueNatural(m_sortRole)) {
+            result = stringCompare(roleValueA, roleValueB, collator);
+        } else {
+            result = QString::compare(roleValueA, roleValueB);
+        }
         break;
     }
 
@@ -1860,6 +2132,8 @@ int KFileItemModel::sortRoleCompare(const ItemData* a, const ItemData* b, const
 
 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);
     }
@@ -1875,11 +2149,6 @@ int KFileItemModel::stringCompare(const QString& a, const QString& b, const QCol
     return QString::compare(a, b, Qt::CaseSensitive);
 }
 
-bool KFileItemModel::useMaximumUpdateInterval() const
-{
-    return !m_dirLister->url().isLocalFile();
-}
-
 QList<QPair<int, QVariant> > KFileItemModel::nameRoleGroups() const
 {
     Q_ASSERT(!m_itemData.isEmpty());
@@ -1905,8 +2174,37 @@ QList<QPair<int, QVariant> > KFileItemModel::nameRoleGroups() const
         if (firstChar != newFirstChar) {
             QString newGroupValue;
             if (newFirstChar.isLetter()) {
-                // Put together compatibility equivalent letters like latin 'A' and umlaut 'A' from the German locale
-                newGroupValue = QString(newFirstChar).normalized(QString::NormalizationForm_KD).at(0);
+
+                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));
+                        }
+                    }
+
+                    auto localeAwareLessThan = [this](QChar c1, QChar c2) -> bool {
+                        return m_collator.compare(c1, c2) < 0;
+                    };
+
+                    std::vector<QChar>::iterator it = std::lower_bound(lettersAtoZ.begin(), lettersAtoZ.end(), 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;
+                    }
+
+                } else {
+                    // Symbols from non Latin-based scripts
+                    newGroupValue = newFirstChar;
+                }
             } 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");
@@ -1939,16 +2237,24 @@ QList<QPair<int, QVariant> > KFileItemModel::sizeRoleGroups() const
         }
 
         const KFileItem& item = m_itemData.at(i)->item;
-        const KIO::filesize_t fileSize = !item.isNull() ? item.size() : ~0U;
+        KIO::filesize_t fileSize = !item.isNull() ? item.size() : ~0U;
         QString newGroupValue;
         if (!item.isNull() && item.isDir()) {
-            newGroupValue = i18nc("@title:group Size", "Folders");
-        } else if (fileSize < 5 * 1024 * 1024) {
-            newGroupValue = i18nc("@title:group Size", "Small");
-        } else if (fileSize < 10 * 1024 * 1024) {
-            newGroupValue = i18nc("@title:group Size", "Medium");
-        } else {
-            newGroupValue = i18nc("@title:group Size", "Big");
+            if (DetailsModeSettings::directorySizeCount() || m_sortDirsFirst) {
+                newGroupValue = i18nc("@title:group Size", "Folders");
+            } else {
+                fileSize = m_itemData.at(i)->values.value("size").toULongLong();
+            }
+        }
+
+        if (newGroupValue.isEmpty()) {
+            if (fileSize < 5 * 1024 * 1024) { // < 5 MB
+                newGroupValue = i18nc("@title:group Size", "Small");
+            } else if (fileSize < 10 * 1024 * 1024) { // < 10 MB
+                newGroupValue = i18nc("@title:group Size", "Medium");
+            } else {
+                newGroupValue = i18nc("@title:group Size", "Big");
+            }
         }
 
         if (newGroupValue != groupValue) {
@@ -2026,7 +2332,7 @@ QList<QPair<int, QVariant> > KFileItemModel::timeRoleGroups(const std::function<
                 if (daysDistance == 1) {
                     const KLocalizedString format = ki18nc("@title:group Date: "
                                                     "MMMM is full month name in current locale, and yyyy is "
-                                                    "full year number", "'Yesterday' (MMMM, yyyy)");
+                                                    "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);
@@ -2041,7 +2347,7 @@ QList<QPair<int, QVariant> > KFileItemModel::timeRoleGroups(const std::function<
                 } 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",
+                        "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",
@@ -2049,7 +2355,7 @@ QList<QPair<int, QVariant> > KFileItemModel::timeRoleGroups(const std::function<
                 } 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", "'One Week Ago' (MMMM, yyyy)");
+                                                           "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);
@@ -2064,7 +2370,7 @@ QList<QPair<int, QVariant> > KFileItemModel::timeRoleGroups(const std::function<
                 } 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", "'Two Weeks Ago' (MMMM, yyyy)");
+                                                           "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);
@@ -2079,7 +2385,7 @@ QList<QPair<int, QVariant> > KFileItemModel::timeRoleGroups(const std::function<
                 } 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", "'Three Weeks Ago' (MMMM, yyyy)");
+                                                           "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);
@@ -2094,7 +2400,7 @@ QList<QPair<int, QVariant> > KFileItemModel::timeRoleGroups(const std::function<
                 } else {
                     const KLocalizedString format = ki18nc("@title:group Date: "
                                                            "MMMM is full month name in current locale, and yyyy is "
-                                                           "full year number", "'Earlier on' MMMM, yyyy");
+                                                           "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);
@@ -2260,14 +2566,14 @@ void KFileItemModel::emitSortProgress(int resolvedCount)
             resortAllItems();
         }
 
-        emit directorySortingProgress(100);
+        Q_EMIT directorySortingProgress(100);
     } else if (itemCount > 0) {
         resolvedCount = qBound(0, resolvedCount, itemCount);
 
         const int progress = resolvedCount * 100 / itemCount;
         if (m_sortingProgressPercent != progress) {
             m_sortingProgressPercent = progress;
-            emit directorySortingProgress(progress);
+            Q_EMIT directorySortingProgress(progress);
         }
     }
 }
@@ -2275,40 +2581,40 @@ void KFileItemModel::emitSortProgress(int resolvedCount)
 const KFileItemModel::RoleInfoMap* KFileItemModel::rolesInfoMap(int& count)
 {
     static const RoleInfoMap rolesInfoMap[] = {
-    //  |         role           |        roleType        |                role translation                     |            group translation               | requires Baloo | requires indexer
-        { nullptr,               NoRole,                  nullptr, nullptr,                                     nullptr, nullptr,                            false,           false },
-        { "text",                NameRole,                I18N_NOOP2_NOSTRIP("@label", "Name"),                 nullptr, nullptr,                            false,           false },
-        { "size",                SizeRole,                I18N_NOOP2_NOSTRIP("@label", "Size"),                 nullptr, nullptr,                            false,           false },
-        { "modificationtime",    ModificationTimeRole,    I18N_NOOP2_NOSTRIP("@label", "Modified"),             nullptr, nullptr,                            false,           false },
-        { "creationtime",        CreationTimeRole,        I18N_NOOP2_NOSTRIP("@label", "Created"),              nullptr, nullptr,                            false,           false },
-        { "accesstime",          AccessTimeRole,          I18N_NOOP2_NOSTRIP("@label", "Accessed"),             nullptr, nullptr,                            false,           false },
-        { "type",                TypeRole,                I18N_NOOP2_NOSTRIP("@label", "Type"),                 nullptr, nullptr,                            false,           false },
-        { "rating",              RatingRole,              I18N_NOOP2_NOSTRIP("@label", "Rating"),               nullptr, nullptr,                            true,            false },
-        { "tags",                TagsRole,                I18N_NOOP2_NOSTRIP("@label", "Tags"),                 nullptr, nullptr,                            true,            false },
-        { "comment",             CommentRole,             I18N_NOOP2_NOSTRIP("@label", "Comment"),              nullptr, nullptr,                            true,            false },
-        { "title",               TitleRole,               I18N_NOOP2_NOSTRIP("@label", "Title"),                I18N_NOOP2_NOSTRIP("@label", "Document"),    true,            true  },
-        { "wordCount",           WordCountRole,           I18N_NOOP2_NOSTRIP("@label", "Word Count"),           I18N_NOOP2_NOSTRIP("@label", "Document"),    true,            true  },
-        { "lineCount",           LineCountRole,           I18N_NOOP2_NOSTRIP("@label", "Line Count"),           I18N_NOOP2_NOSTRIP("@label", "Document"),    true,            true  },
-        { "imageDateTime",       ImageDateTimeRole,       I18N_NOOP2_NOSTRIP("@label", "Date Photographed"),    I18N_NOOP2_NOSTRIP("@label", "Image"),       true,            true  },
-        { "width",               WidthRole,               I18N_NOOP2_NOSTRIP("@label", "Width"),                I18N_NOOP2_NOSTRIP("@label", "Image"),       true,            true  },
-        { "height",              HeightRole,              I18N_NOOP2_NOSTRIP("@label", "Height"),               I18N_NOOP2_NOSTRIP("@label", "Image"),       true,            true  },
-        { "orientation",         OrientationRole,         I18N_NOOP2_NOSTRIP("@label", "Orientation"),          I18N_NOOP2_NOSTRIP("@label", "Image"),       true,            true  },
-        { "artist",              ArtistRole,              I18N_NOOP2_NOSTRIP("@label", "Artist"),               I18N_NOOP2_NOSTRIP("@label", "Audio"),       true,            true  },
-        { "genre",               GenreRole,               I18N_NOOP2_NOSTRIP("@label", "Genre"),                I18N_NOOP2_NOSTRIP("@label", "Audio"),       true,            true  },
-        { "album",               AlbumRole,               I18N_NOOP2_NOSTRIP("@label", "Album"),                I18N_NOOP2_NOSTRIP("@label", "Audio"),       true,            true  },
-        { "duration",            DurationRole,            I18N_NOOP2_NOSTRIP("@label", "Duration"),             I18N_NOOP2_NOSTRIP("@label", "Audio"),       true,            true  },
-        { "bitrate",             BitrateRole,             I18N_NOOP2_NOSTRIP("@label", "Bitrate"),              I18N_NOOP2_NOSTRIP("@label", "Audio"),       true,            true  },
-        { "track",               TrackRole,               I18N_NOOP2_NOSTRIP("@label", "Track"),                I18N_NOOP2_NOSTRIP("@label", "Audio"),       true,            true  },
-        { "releaseYear",         ReleaseYearRole,         I18N_NOOP2_NOSTRIP("@label", "Release Year"),         I18N_NOOP2_NOSTRIP("@label", "Audio"),       true,            true  },
-        { "aspectRatio",         AspectRatioRole,         I18N_NOOP2_NOSTRIP("@label", "Aspect Ratio"),         I18N_NOOP2_NOSTRIP("@label", "Video"),       true,            true  },
-        { "frameRate",           FrameRateRole,           I18N_NOOP2_NOSTRIP("@label", "Frame Rate"),           I18N_NOOP2_NOSTRIP("@label", "Video"),       true,            true  },
-        { "path",                PathRole,                I18N_NOOP2_NOSTRIP("@label", "Path"),                 I18N_NOOP2_NOSTRIP("@label", "Other"),       false,           false },
-        { "deletiontime",        DeletionTimeRole,        I18N_NOOP2_NOSTRIP("@label", "Deletion Time"),        I18N_NOOP2_NOSTRIP("@label", "Other"),       false,           false },
-        { "destination",         DestinationRole,         I18N_NOOP2_NOSTRIP("@label", "Link Destination"),     I18N_NOOP2_NOSTRIP("@label", "Other"),       false,           false },
-        { "originUrl",           OriginUrlRole,           I18N_NOOP2_NOSTRIP("@label", "Downloaded From"),      I18N_NOOP2_NOSTRIP("@label", "Other"),       true,            false },
-        { "permissions",         PermissionsRole,         I18N_NOOP2_NOSTRIP("@label", "Permissions"),          I18N_NOOP2_NOSTRIP("@label", "Other"),       false,           false },
-        { "owner",               OwnerRole,               I18N_NOOP2_NOSTRIP("@label", "Owner"),                I18N_NOOP2_NOSTRIP("@label", "Other"),       false,           false },
-        { "group",               GroupRole,               I18N_NOOP2_NOSTRIP("@label", "User Group"),           I18N_NOOP2_NOSTRIP("@label", "Other"),       false,           false },
+    //  |         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  },
+        { "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  },
+        { "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 },
+        { "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 },
     };
 
     count = sizeof(rolesInfoMap) / sizeof(RoleInfoMap);
@@ -2319,7 +2625,7 @@ void KFileItemModel::determineMimeTypes(const KFileItemList& items, int timeout)
 {
     QElapsedTimer timer;
     timer.start();
-    foreach (const KFileItem& item, items) { // krazy:exclude=foreach
+    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
@@ -2398,3 +2704,15 @@ bool KFileItemModel::isConsistent() const
 
     return true;
 }
+
+void KFileItemModel::slotListerError(KIO::Job *job)
+{
+    if (job->error() == 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."));
+    }
+}