]> cloud.milkyroute.net Git - dolphin.git/commitdiff
Merge remote-tracking branch 'origin/KDE/4.10'
authorFrank Reininghaus <frank78ac@googlemail.com>
Wed, 22 May 2013 16:34:25 +0000 (18:34 +0200)
committerFrank Reininghaus <frank78ac@googlemail.com>
Wed, 22 May 2013 16:34:25 +0000 (18:34 +0200)
1  2 
src/kitemviews/kfileitemmodel.cpp
src/tests/kfileitemmodeltest.cpp

index 0289666ff09ea00c245ce202883361a8dab9ee1c,c78fdc358cb02632f63c040e92bb172da9be3199..d30d9e5be054605e2abef0c19aa0f795e4d4ec1d
@@@ -1,23 -1,21 +1,23 @@@
 -/***************************************************************************
 - *   Copyright (C) 2011 by Peter Penz <peter.penz19@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            *
 - ***************************************************************************/
 +/*****************************************************************************
 + *   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              *
 + *****************************************************************************/
  
  #include "kfileitemmodel.h"
  
@@@ -58,10 -56,12 +58,10 @@@ KFileItemModel::KFileItemModel(QObject
      m_resortAllItemsTimer(0),
      m_pendingItemsToInsert(),
      m_groups(),
 -    m_expandedParentsCountRoot(UninitializedExpandedParentsCountRoot),
      m_expandedDirs(),
      m_urlsToExpand()
  {
      m_dirLister = new KFileItemModelDirLister(this);
 -    m_dirLister->setAutoUpdate(true);
      m_dirLister->setDelayedMimeTypes(true);
  
      const QWidget* parentWidget = qobject_cast<QWidget*>(parent);
@@@ -72,7 -72,7 +72,7 @@@
      connect(m_dirLister, SIGNAL(started(KUrl)), this, SIGNAL(directoryLoadingStarted()));
      connect(m_dirLister, SIGNAL(canceled()), this, SLOT(slotCanceled()));
      connect(m_dirLister, SIGNAL(completed(KUrl)), this, SLOT(slotCompleted()));
 -    connect(m_dirLister, SIGNAL(newItems(KFileItemList)), this, SLOT(slotNewItems(KFileItemList)));
 +    connect(m_dirLister, SIGNAL(itemsAdded(KUrl,KFileItemList)), this, SLOT(slotItemsAdded(KUrl,KFileItemList)));
      connect(m_dirLister, SIGNAL(itemsDeleted(KFileItemList)), this, SLOT(slotItemsDeleted(KFileItemList)));
      connect(m_dirLister, SIGNAL(refreshItems(QList<QPair<KFileItem,KFileItem> >)), this, SLOT(slotRefreshItems(QList<QPair<KFileItem,KFileItem> >)));
      connect(m_dirLister, SIGNAL(clear()), this, SLOT(slotClear()));
  KFileItemModel::~KFileItemModel()
  {
      qDeleteAll(m_itemData);
 -    m_itemData.clear();
 +    qDeleteAll(m_filteredItems.values());
  }
  
  void KFileItemModel::loadDirectory(const KUrl& url)
@@@ -405,7 -405,7 +405,7 @@@ void KFileItemModel::setRoles(const QSe
          // Update m_data with the changed requested roles
          const int maxIndex = count() - 1;
          for (int i = 0; i <= maxIndex; ++i) {
 -            m_itemData[i]->values = retrieveData(m_itemData.at(i)->item);
 +            m_itemData[i]->values = retrieveData(m_itemData.at(i)->item, m_itemData.at(i)->parent);
          }
  
          kWarning() << "TODO: Emitting itemsChanged() with no information what has changed!";
@@@ -430,8 -430,7 +430,8 @@@ bool KFileItemModel::setExpanded(int in
          return false;
      }
  
 -    const KUrl url = m_itemData.at(index)->item.url();
 +    const KFileItem item = m_itemData.at(index)->item;
 +    const KUrl url = item.url();
      if (expanded) {
          m_expandedDirs.insert(url);
          m_dirLister->openUrl(url, KDirLister::Keep);
          m_expandedDirs.remove(url);
          m_dirLister->stop(url);
  
 +        removeFilteredChildren(KFileItemList() << item);
  
 -        KFileItemList itemsToRemove;
 -        const int expandedParentsCount = data(index)["expandedParentsCount"].toInt();
 -        ++index;
 -        while (index < count() && data(index)["expandedParentsCount"].toInt() > expandedParentsCount) {
 -            itemsToRemove.append(m_itemData.at(index)->item);
 -            ++index;
 -        }
 -
 -        QSet<KUrl> urlsToRemove;
 -        urlsToRemove.reserve(itemsToRemove.count() + 1);
 -        urlsToRemove.insert(url);
 -        foreach (const KFileItem& item, itemsToRemove) {
 -            KUrl url = item.url();
 -            url.adjustPath(KUrl::RemoveTrailingSlash);
 -            urlsToRemove.insert(url);
 -        }
 -
 -        QSet<KFileItem>::iterator it = m_filteredItems.begin();
 -        while (it != m_filteredItems.end()) {
 -            const KUrl url = it->url();
 -            KUrl parentUrl = url.upUrl();
 -            parentUrl.adjustPath(KUrl::RemoveTrailingSlash);
 -
 -            if (urlsToRemove.contains(parentUrl)) {
 -                it = m_filteredItems.erase(it);
 -            } else {
 -                ++it;
 -            }
 -        }
 -
 -        removeItems(itemsToRemove);
 +        const KFileItemList itemsToRemove = childItems(item);
 +        removeFilteredChildren(itemsToRemove);
 +        removeItems(itemsToRemove, DeleteItemData);
      }
  
      return true;
@@@ -553,57 -579,31 +553,57 @@@ void KFileItemModel::applyFilters(
          // Only filter non-expanded items as child items may never
          // exist without a parent item
          if (!itemData->values.value("isExpanded").toBool()) {
 -            if (!m_filter.matches(itemData->item)) {
 -                newFilteredItems.append(itemData->item);
 -                m_filteredItems.insert(itemData->item);
 +            const KFileItem item = itemData->item;
 +            if (!m_filter.matches(item)) {
 +                newFilteredItems.append(item);
 +                m_filteredItems.insert(item, itemData);
              }
          }
      }
  
 -    removeItems(newFilteredItems);
 +    removeItems(newFilteredItems, KeepItemData);
  
      // Check which hidden items from m_filteredItems should
      // get visible again and hence removed from m_filteredItems.
 -    KFileItemList newVisibleItems;
 +    QList<ItemData*> newVisibleItems;
  
 -    QMutableSetIterator<KFileItem> it(m_filteredItems);
 -    while (it.hasNext()) {
 -        const KFileItem item = it.next();
 -        if (m_filter.matches(item)) {
 -            newVisibleItems.append(item);
 -            it.remove();
 +    QHash<KFileItem, ItemData*>::iterator it = m_filteredItems.begin();
 +    while (it != m_filteredItems.end()) {
 +        if (m_filter.matches(it.key())) {
 +            newVisibleItems.append(it.value());
 +            it = m_filteredItems.erase(it);
 +        } else {
 +            ++it;
          }
      }
  
      insertItems(newVisibleItems);
  }
  
 +void KFileItemModel::removeFilteredChildren(const KFileItemList& parentsList)
 +{
 +    if (m_filteredItems.isEmpty()) {
 +        return;
 +    }
 +
 +    // First, we put the parent items into a set to provide fast lookup
 +    // while iterating over m_filteredItems and prevent quadratic
 +    // complexity if there are N parents and N filtered items.
 +    const QSet<KFileItem> parents = parentsList.toSet();
 +
 +    QHash<KFileItem, ItemData*>::iterator it = m_filteredItems.begin();
 +    while (it != m_filteredItems.end()) {
 +        const ItemData* parent = it.value()->parent;
 +
 +        if (parent && parents.contains(parent->item)) {
 +            delete it.value();
 +            it = m_filteredItems.erase(it);
 +        } else {
 +            ++it;
 +        }
 +    }
 +}
 +
  QList<KFileItemModel::RoleInfo> KFileItemModel::rolesInformation()
  {
      static QList<RoleInfo> rolesInfo;
@@@ -689,7 -689,7 +689,7 @@@ void KFileItemModel::resortAllItems(
      m_items.clear();
  
      // Resort the items
 -    KFileItemModelSortAlgorithm::sort(this, m_itemData.begin(), m_itemData.end());
 +    sort(m_itemData.begin(), m_itemData.end());
      for (int i = 0; i < itemCount; ++i) {
          m_items.insert(m_itemData.at(i)->item.url(), i);
      }
@@@ -751,14 -751,11 +751,14 @@@ void KFileItemModel::slotCanceled(
      emit directoryLoadingCanceled();
  }
  
 -void KFileItemModel::slotNewItems(const KFileItemList& items)
 +void KFileItemModel::slotItemsAdded(const KUrl& directoryUrl, const KFileItemList& items)
  {
      Q_ASSERT(!items.isEmpty());
  
 -    if (m_requestRole[ExpandedParentsCountRole] && m_expandedParentsCountRoot >= 0) {
 +    KUrl parentUrl = directoryUrl;
 +    parentUrl.adjustPath(KUrl::RemoveTrailingSlash);
 +
 +    if (m_requestRole[ExpandedParentsCountRole]) {
          // To be able to compare whether the new items may be inserted as children
          // of a parent item the pending items must be added to the model first.
          dispatchPendingItemsToInsert();
          // KDirLister keeps the children of items that got expanded once even if
          // they got collapsed again with KFileItemModel::setExpanded(false). So it must be
          // checked whether the parent for new items is still expanded.
 -        KUrl parentUrl = item.url().upUrl();
 -        parentUrl.adjustPath(KUrl::RemoveTrailingSlash);
          const int parentIndex = m_items.value(parentUrl, -1);
          if (parentIndex >= 0 && !m_itemData[parentIndex]->values.value("isExpanded").toBool()) {
              // The parent is not expanded.
          }
      }
  
 +    QList<ItemData*> itemDataList = createItemDataList(parentUrl, items);
 +
      if (!m_filter.hasSetFilters()) {
 -        m_pendingItemsToInsert.append(items);
 +        m_pendingItemsToInsert.append(itemDataList);
      } else {
          // The name or type filter is active. Hide filtered items
          // before inserting them into the model and remember
          // the filtered items in m_filteredItems.
 -        KFileItemList filteredItems;
 -        foreach (const KFileItem& item, items) {
 -            if (m_filter.matches(item)) {
 -                filteredItems.append(item);
 +        foreach (ItemData* itemData, itemDataList) {
 +            if (m_filter.matches(itemData->item)) {
 +                m_pendingItemsToInsert.append(itemData);
              } else {
 -                m_filteredItems.insert(item);
 +                m_filteredItems.insert(itemData->item, itemData);
              }
          }
 -
 -        m_pendingItemsToInsert.append(filteredItems);
      }
  
      if (useMaximumUpdateInterval() && !m_maximumUpdateIntervalTimer->isActive()) {
@@@ -815,7 -815,7 +815,7 @@@ void KFileItemModel::slotItemsDeleted(c
      dispatchPendingItemsToInsert();
  
      KFileItemList itemsToRemove = items;
 -    if (m_requestRole[ExpandedParentsCountRole] && m_expandedParentsCountRoot >= 0) {
 +    if (m_requestRole[ExpandedParentsCountRole]) {
          // Assure that removing a parent item also results in removing all children
          foreach (const KFileItem& item, items) {
              itemsToRemove.append(childItems(item));
  
      if (!m_filteredItems.isEmpty()) {
          foreach (const KFileItem& item, itemsToRemove) {
 -            m_filteredItems.remove(item);
 -        }
 -
 -        if (m_requestRole[ExpandedParentsCountRole] && m_expandedParentsCountRoot >= 0) {
 -            // Remove all filtered children of deleted items. First, we put the
 -            // deleted URLs into a set to provide fast lookup while iterating
 -            // over m_filteredItems and prevent quadratic complexity if there
 -            // are N removed items and N filtered items.
 -            QSet<KUrl> urlsToRemove;
 -            urlsToRemove.reserve(itemsToRemove.count());
 -            foreach (const KFileItem& item, itemsToRemove) {
 -                KUrl url = item.url();
 -                url.adjustPath(KUrl::RemoveTrailingSlash);
 -                urlsToRemove.insert(url);
 +            QHash<KFileItem, ItemData*>::iterator it = m_filteredItems.find(item);
 +            if (it != m_filteredItems.end()) {
 +                delete it.value();
 +                m_filteredItems.erase(it);
              }
 +        }
  
 -            QSet<KFileItem>::iterator it = m_filteredItems.begin();
 -            while (it != m_filteredItems.end()) {
 -                const KUrl url = it->url();
 -                KUrl parentUrl = url.upUrl();
 -                parentUrl.adjustPath(KUrl::RemoveTrailingSlash);
 -
 -                if (urlsToRemove.contains(parentUrl)) {
 -                    it = m_filteredItems.erase(it);
 -                } else {
 -                    ++it;
 -                }
 -            }
 +        if (m_requestRole[ExpandedParentsCountRole]) {
 +            removeFilteredChildren(itemsToRemove);
          }
      }
  
 -    removeItems(itemsToRemove);
 +    removeItems(itemsToRemove, DeleteItemData);
  }
  
  void KFileItemModel::slotRefreshItems(const QList<QPair<KFileItem, KFileItem> >& items)
  
              // 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));
 +            QHashIterator<QByteArray, QVariant> it(retrieveData(newItem, m_itemData.at(index)->parent));
              while (it.hasNext()) {
                  it.next();
                  m_itemData[index]->values.insert(it.key(), it.value());
@@@ -918,7 -937,6 +918,7 @@@ void KFileItemModel::slotClear(
      kDebug() << "Clearing all items";
  #endif
  
 +    qDeleteAll(m_filteredItems.values());
      m_filteredItems.clear();
      m_groups.clear();
  
      m_resortAllItemsTimer->stop();
      m_pendingItemsToInsert.clear();
  
 -    m_expandedParentsCountRoot = UninitializedExpandedParentsCountRoot;
 -
      const int removedCount = m_itemData.count();
      if (removedCount > 0) {
          qDeleteAll(m_itemData);
@@@ -956,212 -976,197 +956,212 @@@ void KFileItemModel::dispatchPendingIte
      }
  }
  
 -void KFileItemModel::insertItems(const KFileItemList& items)
 +void KFileItemModel::insertItems(QList<ItemData*>& newItems)
  {
 -    if (items.isEmpty()) {
 +    if (newItems.isEmpty()) {
          return;
      }
  
 -    if (m_sortRole == 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).
 -        determineMimeTypes(items, 200);
 -    }
 -
  #ifdef KFILEITEMMODEL_DEBUG
      QElapsedTimer timer;
      timer.start();
      kDebug() << "===========================================================";
 -    kDebug() << "Inserting" << items.count() << "items";
 +    kDebug() << "Inserting" << newItems.count() << "items";
  #endif
  
      m_groups.clear();
  
 -    QList<ItemData*> sortedItems = createItemDataList(items);
 -    KFileItemModelSortAlgorithm::sort(this, sortedItems.begin(), sortedItems.end());
 +    sort(newItems.begin(), newItems.end());
  
  #ifdef KFILEITEMMODEL_DEBUG
      kDebug() << "[TIME] Sorting:" << timer.elapsed();
  #endif
  
      KItemRangeList itemRanges;
 -    int targetIndex = 0;
 -    int sourceIndex = 0;
 -    int insertedAtIndex = -1;        // Index for the current item-range
 -    int insertedCount = 0;           // Count for the current item-range
 -    int previouslyInsertedCount = 0; // Sum of previously inserted items for all ranges
 -    while (sourceIndex < sortedItems.count()) {
 -        // Find target index from m_items to insert the current item
 -        // in a sorted order
 -        const int previousTargetIndex = targetIndex;
 -        while (targetIndex < m_itemData.count()) {
 -            if (!lessThan(m_itemData.at(targetIndex), sortedItems.at(sourceIndex))) {
 -                break;
 +    const int existingItemCount = m_itemData.count();
 +    const int newItemCount = newItems.count();
 +    const int totalItemCount = existingItemCount + newItemCount;
 +
 +    if (existingItemCount == 0) {
 +        // Optimization for the common special case that there are no
 +        // items in the model yet. Happens, e.g., when entering a folder.
 +        m_itemData = newItems;
 +        itemRanges << KItemRange(0, newItemCount);
 +    } else {
 +        m_itemData.reserve(totalItemCount);
 +        for (int i = existingItemCount; i < totalItemCount; ++i) {
 +            m_itemData.append(0);
 +        }
 +
 +        // We build the new list m_items in reverse order to minimize
 +        // the number of moves and guarantee O(N) complexity.
 +        int targetIndex = totalItemCount - 1;
 +        int sourceIndexExistingItems = existingItemCount - 1;
 +        int sourceIndexNewItems = newItemCount - 1;
 +
 +        int rangeCount = 0;
 +
 +        while (sourceIndexNewItems >= 0) {
 +            ItemData* newItem = newItems.at(sourceIndexNewItems);
 +            if (sourceIndexExistingItems >= 0 && lessThan(newItem, m_itemData.at(sourceIndexExistingItems))) {
 +                // Move an existing item to its new position. If any new items
 +                // are behind it, push the item range to itemRanges.
 +                if (rangeCount > 0) {
 +                    itemRanges << KItemRange(sourceIndexExistingItems + 1, rangeCount);
 +                    rangeCount = 0;
 +                }
 +
 +                m_itemData[targetIndex] = m_itemData.at(sourceIndexExistingItems);
 +                --sourceIndexExistingItems;
 +            } else {
 +                // Insert a new item into the list.
 +                ++rangeCount;
 +                m_itemData[targetIndex] = newItem;
 +                --sourceIndexNewItems;
              }
 -            ++targetIndex;
 +            --targetIndex;
          }
  
 -        if (targetIndex - previousTargetIndex > 0 && insertedAtIndex >= 0) {
 -            itemRanges << KItemRange(insertedAtIndex, insertedCount);
 -            previouslyInsertedCount += insertedCount;
 -            insertedAtIndex = targetIndex - previouslyInsertedCount;
 -            insertedCount = 0;
 +        // Push the final item range to itemRanges.
 +        if (rangeCount > 0) {
 +            itemRanges << KItemRange(sourceIndexExistingItems + 1, rangeCount);
          }
  
 -        // Insert item at the position targetIndex by transferring
 -        // the ownership of the item-data from sortedItems to m_itemData.
 -        // m_items will be inserted after the loop (see comment below)
 -        m_itemData.insert(targetIndex, sortedItems.at(sourceIndex));
 -        ++insertedCount;
 -
 -        if (insertedAtIndex < 0) {
 -            insertedAtIndex = targetIndex;
 -            Q_ASSERT(previouslyInsertedCount == 0);
 -        }
 -        ++targetIndex;
 -        ++sourceIndex;
 +        // Note that itemRanges is still sorted in reverse order.
 +        std::reverse(itemRanges.begin(), itemRanges.end());
      }
  
 -    // The indexes of all m_items must be adjusted, not only the index
 -    // of the new items
 -    const int itemDataCount = m_itemData.count();
 -    for (int i = 0; i < itemDataCount; ++i) {
 +    // The indexes starting from the first inserted item must be adjusted.
 +    m_items.reserve(totalItemCount);
 +    for (int i = itemRanges.front().index; i < totalItemCount; ++i) {
          m_items.insert(m_itemData.at(i)->item.url(), i);
      }
  
 -    itemRanges << KItemRange(insertedAtIndex, insertedCount);
      emit itemsInserted(itemRanges);
  
  #ifdef KFILEITEMMODEL_DEBUG
 -    kDebug() << "[TIME] Inserting of" << items.count() << "items:" << timer.elapsed();
 +    kDebug() << "[TIME] Inserting of" << newItems.count() << "items:" << timer.elapsed();
  #endif
  }
  
 -void KFileItemModel::removeItems(const KFileItemList& items)
 +static KItemRangeList sortedIndexesToKItemRangeList(const QList<int>& sortedNumbers)
  {
 -    if (items.isEmpty()) {
 -        return;
 +    if (sortedNumbers.empty()) {
 +        return KItemRangeList();
 +    }
 +
 +    KItemRangeList result;
 +
 +    QList<int>::const_iterator it = sortedNumbers.begin();
 +    int index = *it;
 +    int count = 1;
 +
 +    ++it;
 +
 +    QList<int>::const_iterator end = sortedNumbers.end();
 +    while (it != end) {
 +        if (*it == index + count) {
 +            ++count;
 +        } else {
 +            result << KItemRange(index, count);
 +            index = *it;
 +            count = 1;
 +        }
 +        ++it;
      }
  
 +    result << KItemRange(index, count);
 +    return result;
 +}
 +
 +void KFileItemModel::removeItems(const KFileItemList& items, RemoveItemsBehavior behavior)
 +{
  #ifdef KFILEITEMMODEL_DEBUG
      kDebug() << "Removing " << items.count() << "items";
  #endif
  
      m_groups.clear();
  
 -    QList<ItemData*> sortedItems;
 -    sortedItems.reserve(items.count());
 +    // Step 1: Determine the indexes of the removed items, remove them from
 +    //         the hash m_items, and free the ItemData.
 +    QList<int> indexesToRemove;
 +    indexesToRemove.reserve(items.count());
      foreach (const KFileItem& item, items) {
 -        const int index = m_items.value(item.url(), -1);
 +        const KUrl url = item.url();
 +        const int index = m_items.value(url, -1);
          if (index >= 0) {
 -            sortedItems.append(m_itemData.at(index));
 -        }
 -    }
 -    KFileItemModelSortAlgorithm::sort(this, sortedItems.begin(), sortedItems.end());
 +            indexesToRemove.append(index);
  
 -    QList<int> indexesToRemove;
 -    indexesToRemove.reserve(items.count());
 +            // Prevent repeated expensive rehashing by using QHash::erase(),
 +            // rather than QHash::remove().
 +            QHash<KUrl, int>::iterator it = m_items.find(url);
 +            m_items.erase(it);
  
 -    // Calculate the item ranges that will get deleted
 -    KItemRangeList itemRanges;
 -    int removedAtIndex = -1;
 -    int removedCount = 0;
 -    int targetIndex = 0;
 -    foreach (const ItemData* itemData, sortedItems) {
 -        const KFileItem& itemToRemove = itemData->item;
 -
 -        const int previousTargetIndex = targetIndex;
 -        while (targetIndex < m_itemData.count()) {
 -            if (m_itemData.at(targetIndex)->item.url() == itemToRemove.url()) {
 -                break;
 +            if (behavior == DeleteItemData) {
 +                delete m_itemData.at(index);
              }
 -            ++targetIndex;
 -        }
 -        if (targetIndex >= m_itemData.count()) {
 -            kWarning() << "Item that should be deleted has not been found!";
 -            return;
 -        }
  
 -        if (targetIndex - previousTargetIndex > 0 && removedAtIndex >= 0) {
 -            itemRanges << KItemRange(removedAtIndex, removedCount);
 -            removedAtIndex = targetIndex;
 -            removedCount = 0;
 +            m_itemData[index] = 0;
          }
 +    }
  
 -        indexesToRemove.append(targetIndex);
 -        if (removedAtIndex < 0) {
 -            removedAtIndex = targetIndex;
 -        }
 -        ++removedCount;
 -        ++targetIndex;
 +    if (indexesToRemove.isEmpty()) {
 +        return;
      }
  
 -    // Delete the items
 -    for (int i = indexesToRemove.count() - 1; i >= 0; --i) {
 -        const int indexToRemove = indexesToRemove.at(i);
 -        ItemData* data = m_itemData.at(indexToRemove);
 +    std::sort(indexesToRemove.begin(), indexesToRemove.end());
  
 -        m_items.remove(data->item.url());
 +    // Step 2: Remove the ItemData pointers from the list m_itemData.
 +    const KItemRangeList itemRanges = sortedIndexesToKItemRangeList(indexesToRemove);
 +    int target = itemRanges.at(0).index;
 +    int source = itemRanges.at(0).index + itemRanges.at(0).count;
 +    int nextRange = 1;
  
 -        delete data;
 -        m_itemData.removeAt(indexToRemove);
 -    }
 +    const int oldItemDataCount = m_itemData.count();
 +    while (source < oldItemDataCount) {
 +        m_itemData[target] = m_itemData[source];
 +        ++target;
 +        ++source;
  
 -    // The indexes of all m_items must be adjusted, not only the index
 -    // of the removed items
 -    const int itemDataCount = m_itemData.count();
 -    for (int i = 0; i < itemDataCount; ++i) {
 -        m_items.insert(m_itemData.at(i)->item.url(), i);
 +        if (nextRange < itemRanges.count() && source == itemRanges.at(nextRange).index) {
 +            // Skip the items in the next removed range.
 +            source += itemRanges.at(nextRange).count;
 +            ++nextRange;
 +        }
      }
  
 -    if (count() <= 0) {
 -        m_expandedParentsCountRoot = UninitializedExpandedParentsCountRoot;
 +    m_itemData.erase(m_itemData.end() - indexesToRemove.count(), m_itemData.end());
 +
 +    // Step 3: Adjust indexes in the hash m_items, starting from the
 +    //         index of the first removed item.
 +    const int newItemDataCount = m_itemData.count();
 +    for (int i = itemRanges.front().index; i < newItemDataCount; ++i) {
 +        m_items.insert(m_itemData.at(i)->item.url(), i);
      }
  
 -    itemRanges << KItemRange(removedAtIndex, removedCount);
      emit itemsRemoved(itemRanges);
  }
  
 -QList<KFileItemModel::ItemData*> KFileItemModel::createItemDataList(const KFileItemList& items) const
 +QList<KFileItemModel::ItemData*> KFileItemModel::createItemDataList(const KUrl& parentUrl, const KFileItemList& items) const
  {
 +    if (m_sortRole == 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).
 +        determineMimeTypes(items, 200);
 +    }
 +
 +    const int parentIndex = m_items.value(parentUrl, -1);
 +    ItemData* parentItem = parentIndex < 0 ? 0 : m_itemData.at(parentIndex);
 +
      QList<ItemData*> itemDataList;
      itemDataList.reserve(items.count());
  
      foreach (const KFileItem& item, items) {
          ItemData* itemData = new ItemData();
          itemData->item = item;
 -        itemData->values = retrieveData(item);
 -        itemData->parent = 0;
 -
 -        const bool determineParent = m_requestRole[ExpandedParentsCountRole]
 -                                     && itemData->values["expandedParentsCount"].toInt() > 0;
 -        if (determineParent) {
 -            KUrl parentUrl = item.url().upUrl();
 -            parentUrl.adjustPath(KUrl::RemoveTrailingSlash);
 -            const int parentIndex = m_items.value(parentUrl, -1);
 -            if (parentIndex >= 0) {
 -                itemData->parent = m_itemData.at(parentIndex);
 -            } else {
 -                kWarning() << "Parent item not found for" << item.url();
 -            }
 -        }
 -
 +        itemData->values = retrieveData(item, parentItem);
 +        itemData->parent = parentItem;
          itemDataList.append(itemData);
      }
  
@@@ -1182,8 -1187,9 +1182,8 @@@ void KFileItemModel::removeExpandedItem
  
      // The m_expandedParentsCountRoot may not get reset before all items with
      // a bigger count have been removed.
 -    removeItems(expandedItems);
 +    removeItems(expandedItems, DeleteItemData);
  
 -    m_expandedParentsCountRoot = UninitializedExpandedParentsCountRoot;
      m_expandedDirs.clear();
  }
  
@@@ -1246,7 -1252,7 +1246,7 @@@ QByteArray KFileItemModel::roleForType(
      return roles.value(roleType);
  }
  
 -QHash<QByteArray, QVariant> KFileItemModel::retrieveData(const KFileItem& item) 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
          data.insert("path", path);
      }
  
-     if (m_requestRole[IsExpandedRole]) {
-         data.insert("isExpanded", false);
-     }
      if (m_requestRole[IsExpandableRole]) {
          data.insert("isExpandable", item.isDir() && item.url() == item.targetUrl());
      }
  
      if (m_requestRole[ExpandedParentsCountRole]) {
 -        if (m_expandedParentsCountRoot == UninitializedExpandedParentsCountRoot) {
 -            const KUrl rootUrl = m_dirLister->url();
 -            const QString protocol = rootUrl.protocol();
 -            const bool forceExpandedParentsCountRoot = (protocol == QLatin1String("trash") ||
 -                                                        protocol == QLatin1String("nepomuk") ||
 -                                                        protocol == QLatin1String("remote") ||
 -                                                        protocol.contains(QLatin1String("search")));
 -            if (forceExpandedParentsCountRoot) {
 -                m_expandedParentsCountRoot = ForceExpandedParentsCountRoot;
 -            } else {
 -                const QString rootDir = rootUrl.path(KUrl::AddTrailingSlash);
 -                m_expandedParentsCountRoot = rootDir.count('/');
 -            }
 +        int level = 0;
 +        if (parent) {
 +            level = parent->values["expandedParentsCount"].toInt() + 1;
          }
  
 -        if (m_expandedParentsCountRoot == ForceExpandedParentsCountRoot) {
 -            data.insert("expandedParentsCount", -1);
 -        } else {
 -            const QString dir = item.url().directory(KUrl::AppendTrailingSlash);
 -            const int level = dir.count('/') - m_expandedParentsCountRoot;
 -            data.insert("expandedParentsCount", level);
 -        }
 +        data.insert("expandedParentsCount", level);
      }
  
      if (item.isMimeTypeKnown()) {
@@@ -1359,34 -1377,11 +1355,34 @@@ bool KFileItemModel::lessThan(const Ite
  {
      int result = 0;
  
 -    if (m_expandedParentsCountRoot >= 0) {
 -        result = expandedParentsCountCompare(a, b);
 -        if (result != 0) {
 -            // The items have parents with different expansion levels
 -            return (sortOrder() == Qt::AscendingOrder) ? result < 0 : result > 0;
 +    if (a->parent != b->parent) {
 +        const int expansionLevelA = a->values.value("expandedParentsCount").toInt();
 +        const int expansionLevelB = b->values.value("expandedParentsCount").toInt();
 +
 +        // If b has a higher expansion level than a, check if a is a parent
 +        // of b, and make sure that both expansion levels are equal otherwise.
 +        for (int i = expansionLevelB; i > expansionLevelA; --i) {
 +            if (b->parent == a) {
 +                return true;
 +            }
 +            b = b->parent;
 +        }
 +
 +        // If a has a higher expansion level than a, check if b is a parent
 +        // of a, and make sure that both expansion levels are equal otherwise.
 +        for (int i = expansionLevelA; i > expansionLevelB; --i) {
 +            if (a->parent == b) {
 +                return false;
 +            }
 +            a = a->parent;
 +        }
 +
 +        Q_ASSERT(a->values.value("expandedParentsCount").toInt() == b->values.value("expandedParentsCount").toInt());
 +
 +        // Compare the last parents of a and b which are different.
 +        while (a->parent != b->parent) {
 +            a = a->parent;
 +            b = b->parent;
          }
      }
  
      return (sortOrder() == Qt::AscendingOrder) ? result < 0 : result > 0;
  }
  
 +/**
 + * Helper class for KFileItemModel::sort().
 + */
 +class KFileItemModelLessThan
 +{
 +public:
 +    KFileItemModelLessThan(const KFileItemModel* model) :
 +        m_model(model)
 +    {
 +    }
 +
 +    bool operator()(const KFileItemModel::ItemData* a, const KFileItemModel::ItemData* b) const
 +    {
 +        return m_model->lessThan(a, b);
 +    }
 +
 +private:
 +    const KFileItemModel* m_model;
 +};
 +
 +void KFileItemModel::sort(QList<KFileItemModel::ItemData*>::iterator begin,
 +                          QList<KFileItemModel::ItemData*>::iterator end) const
 +{
 +    KFileItemModelLessThan lessThan(this);
 +
 +    if (m_sortRole == NameRole) {
 +        // Sorting by name 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, lessThan, numberOfThreads);
 +    } else {
 +        // Sorting by other roles is quite fast. Use only one thread to prevent
 +        // problems caused by non-reentrant comparison functions, see
 +        // https://bugs.kde.org/show_bug.cgi?id=312679
 +        mergeSort(begin, end, lessThan);
 +    }
 +}
 +
  int KFileItemModel::sortRoleCompare(const ItemData* a, const ItemData* b) const
  {
      const KFileItem& itemA = a->item;
@@@ -1567,6 -1524,88 +1563,6 @@@ int KFileItemModel::stringCompare(cons
                              : QString::compare(a, b, Qt::CaseSensitive);
  }
  
 -int KFileItemModel::expandedParentsCountCompare(const ItemData* a, const ItemData* b) const
 -{
 -    const KUrl urlA = a->item.url();
 -    const KUrl urlB = b->item.url();
 -    if (urlA.directory() == urlB.directory()) {
 -        // Both items have the same directory as parent
 -        return 0;
 -    }
 -
 -    // Check whether one item is the parent of the other item
 -    if (urlA.isParentOf(urlB)) {
 -        return (sortOrder() == Qt::AscendingOrder) ? -1 : +1;
 -    } else if (urlB.isParentOf(urlA)) {
 -        return (sortOrder() == Qt::AscendingOrder) ? +1 : -1;
 -    }
 -
 -    // Determine the maximum common path of both items and
 -    // remember the index in 'index'
 -    const QString pathA = urlA.path();
 -    const QString pathB = urlB.path();
 -
 -    const int maxIndex = qMin(pathA.length(), pathB.length()) - 1;
 -    int index = 0;
 -    while (index <= maxIndex && pathA.at(index) == pathB.at(index)) {
 -        ++index;
 -    }
 -    if (index > maxIndex) {
 -        index = maxIndex;
 -    }
 -    while (index > 0 && (pathA.at(index) != QLatin1Char('/') || pathB.at(index) != QLatin1Char('/'))) {
 -        --index;
 -    }
 -
 -    // Determine the first sub-path after the common path and
 -    // check whether it represents a directory or already a file
 -    bool isDirA = true;
 -    const QString subPathA = subPath(a->item, pathA, index, &isDirA);
 -    bool isDirB = true;
 -    const QString subPathB = subPath(b->item, pathB, index, &isDirB);
 -
 -    if (m_sortDirsFirst || m_sortRole == SizeRole) {
 -        if (isDirA && !isDirB) {
 -            return (sortOrder() == Qt::AscendingOrder) ? -1 : +1;
 -        } else if (!isDirA && isDirB) {
 -            return (sortOrder() == Qt::AscendingOrder) ? +1 : -1;
 -        }
 -    }
 -
 -    // Compare the items of the parents that represent the first
 -    // different path after the common path.
 -    const QString parentPathA = pathA.left(index) + subPathA;
 -    const QString parentPathB = pathB.left(index) + subPathB;
 -
 -    const ItemData* parentA = a;
 -    while (parentA && parentA->item.url().path() != parentPathA) {
 -        parentA = parentA->parent;
 -    }
 -
 -    const ItemData* parentB = b;
 -    while (parentB && parentB->item.url().path() != parentPathB) {
 -        parentB = parentB->parent;
 -    }
 -
 -    if (parentA && parentB) {
 -        return sortRoleCompare(parentA, parentB);
 -    }
 -
 -    kWarning() << "Child items without parent detected:" << a->item.url() << b->item.url();
 -    return QString::compare(urlA.url(), urlB.url(), Qt::CaseSensitive);
 -}
 -
 -QString KFileItemModel::subPath(const KFileItem& item,
 -                                const QString& itemPath,
 -                                int start,
 -                                bool* isDir) const
 -{
 -    Q_ASSERT(isDir);
 -    const int pathIndex = itemPath.indexOf('/', start + 1);
 -    *isDir = (pathIndex > 0) || item.isDir();
 -    return itemPath.mid(start, pathIndex - start);
 -}
 -
  bool KFileItemModel::useMaximumUpdateInterval() const
  {
      return !m_dirLister->url().isLocalFile();
@@@ -1683,6 -1722,12 +1679,6 @@@ QList<QPair<int, QVariant> > KFileItemM
  
      const QDate currentDate = KDateTime::currentLocalDateTime().date();
  
 -    int yearForCurrentWeek = 0;
 -    int currentWeek = currentDate.weekNumber(&yearForCurrentWeek);
 -    if (yearForCurrentWeek == currentDate.year() + 1) {
 -        currentWeek = 53;
 -    }
 -
      QDate previousModifiedDate;
      QString groupValue;
      for (int i = 0; i <= maxIndex; ++i) {
  
          const int daysDistance = modifiedDate.daysTo(currentDate);
  
 -        int yearForModifiedWeek = 0;
 -        int modifiedWeek = modifiedDate.weekNumber(&yearForModifiedWeek);
 -        if (yearForModifiedWeek == modifiedDate.year() + 1) {
 -            modifiedWeek = 53;
 -        }
 -
          QString newGroupValue;
          if (currentDate.year() == modifiedDate.year() && currentDate.month() == modifiedDate.month()) {
 -            if (modifiedWeek > currentWeek) {
 -                // Usecase: modified date = 2010-01-01, current date = 2010-01-22
 -                //          modified week = 53,         current week = 3
 -                modifiedWeek = 0;
 -            }
 -            switch (currentWeek - modifiedWeek) {
 +            switch (daysDistance / 7) {
              case 0:
                  switch (daysDistance) {
                  case 0:  newGroupValue = i18nc("@title:group Date", "Today"); break;
                  }
                  break;
              case 1:
 -                newGroupValue = i18nc("@title:group Date", "Last Week");
 +                newGroupValue = i18nc("@title:group Date", "One Week Ago");
                  break;
              case 2:
                  newGroupValue = i18nc("@title:group Date", "Two Weeks Ago");
                  } else if (daysDistance <= 7) {
                      newGroupValue = modifiedTime.toString(i18nc("@title:group The week day name: %A, %B is full month name in current locale, and %Y is full year number", "%A (%B, %Y)"));
                  } else if (daysDistance <= 7 * 2) {
 -                    newGroupValue = modifiedTime.toString(i18nc("@title:group Date: %B is full month name in current locale, and %Y is full year number", "Last Week (%B, %Y)"));
 +                    newGroupValue = modifiedTime.toString(i18nc("@title:group Date: %B is full month name in current locale, and %Y is full year number", "One Week Ago (%B, %Y)"));
                  } else if (daysDistance <= 7 * 3) {
                      newGroupValue = modifiedTime.toString(i18nc("@title:group Date: %B is full month name in current locale, and %Y is full year number", "Two Weeks Ago (%B, %Y)"));
                  } else if (daysDistance <= 7 * 4) {
@@@ -1955,7 -2011,7 +1951,7 @@@ void KFileItemModel::determineMimeTypes
  {
      QElapsedTimer timer;
      timer.start();
 -    foreach (KFileItem item, items) { // krazy:exclude=foreach
 +    foreach (const KFileItem& item, items) { // krazy:exclude=foreach
          item.determineMimeType();
          if (timer.elapsed() > timeout) {
              // Don't block the user interface, let the remaining items
      }
  }
  
 +bool KFileItemModel::isConsistent() const
 +{
 +    if (m_items.count() != m_itemData.count()) {
 +        return false;
 +    }
 +
 +    for (int i = 0; i < count(); ++i) {
 +        // Check if m_items and m_itemData are consistent.
 +        const KFileItem item = fileItem(i);
 +        if (item.isNull()) {
 +            qWarning() << "Item" << i << "is null";
 +            return false;
 +        }
 +
 +        const int itemIndex = index(item);
 +        if (itemIndex != i) {
 +            qWarning() << "Item" << i << "has a wrong index:" << itemIndex;
 +            return false;
 +        }
 +
 +        // Check if the items are sorted correctly.
 +        if (i > 0 && !lessThan(m_itemData.at(i - 1), m_itemData.at(i))) {
 +            qWarning() << "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;
 +        if (parent) {
 +            if (data->values.value("expandedParentsCount").toInt() != parent->values.value("expandedParentsCount").toInt() + 1) {
 +                qWarning() << "expandedParentsCount is inconsistent for parent" << parent->item << "and child" << data->item;
 +                return false;
 +            }
 +
 +            const int parentIndex = index(parent->item);
 +            if (parentIndex >= i) {
 +                qWarning() << "Index" << parentIndex << "of parent" << parent->item << "is not smaller than index" << i << "of child" << data->item;
 +                return false;
 +            }
 +        }
 +    }
 +
 +    return true;
 +}
 +
  #include "kfileitemmodel.moc"
index 484ddee110ce816bf3146e19ca300c5bdab51d21,e636bcd9170c5559a9bb2b9d7e27449f4ab77e1d..383575a97a8bb7bb91756770f0391ea6a61335ff
@@@ -21,8 -21,6 +21,8 @@@
  #include <qtest_kde.h>
  
  #include <KDirLister>
 +#include <kio/job.h>
 +
  #include "kitemviews/kfileitemmodel.h"
  #include "kitemviews/private/kfileitemmodeldirlister.h"
  #include "testdir.h"
@@@ -73,17 -71,17 +73,18 @@@ private slots
      void testItemRangeConsistencyWhenInsertingItems();
      void testExpandItems();
      void testExpandParentItems();
 +    void testMakeExpandedItemHidden();
      void testSorting();
      void testIndexForKeyboardSearch();
      void testNameFilter();
      void testEmptyPath();
+     void testRefreshExpandedItem();
      void testRemoveHiddenItems();
      void collapseParentOfHiddenItems();
      void removeParentOfHiddenItems();
 +    void testGeneralParentChildRelationships();
  
  private:
 -    bool isModelConsistent() const;
      QStringList itemsInModel() const;
  
  private:
@@@ -158,7 -156,7 +159,7 @@@ void KFileItemModelTest::testNewItems(
  
      QCOMPARE(m_model->count(), 3);
  
 -    QVERIFY(isModelConsistent());
 +    QVERIFY(m_model->isConsistent());
  }
  
  void KFileItemModelTest::testRemoveItems()
      m_model->loadDirectory(m_testDir->url());
      QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout));
      QCOMPARE(m_model->count(), 2);
 -    QVERIFY(isModelConsistent());
 +    QVERIFY(m_model->isConsistent());
  
      m_testDir->removeFile("a.txt");
      m_model->m_dirLister->updateDirectory(m_testDir->url());
      QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsRemoved(KItemRangeList)), DefaultTimeout));
      QCOMPARE(m_model->count(), 1);
 -    QVERIFY(isModelConsistent());
 +    QVERIFY(m_model->isConsistent());
  }
  
  void KFileItemModelTest::testDirLoadingCompleted()
      QCOMPARE(itemsRemovedSpy.count(), 2);
      QCOMPARE(m_model->count(), 4);
  
 -    QVERIFY(isModelConsistent());
 +    QVERIFY(m_model->isConsistent());
  }
  
  void KFileItemModelTest::testSetData()
      values = m_model->data(0);
      QCOMPARE(values.value("customRole1").toString(), QString("Test1"));
      QCOMPARE(values.value("customRole2").toString(), QString("Test2"));
 -    QVERIFY(isModelConsistent());
 +    QVERIFY(m_model->isConsistent());
  }
  
  void KFileItemModelTest::testSetDataWithModifiedSortRole_data()
@@@ -319,7 -317,7 +320,7 @@@ void KFileItemModelTest::testSetDataWit
      QCOMPARE(m_model->data(0).value("rating").toInt(), ratingIndex0);
      QCOMPARE(m_model->data(1).value("rating").toInt(), ratingIndex1);
      QCOMPARE(m_model->data(2).value("rating").toInt(), ratingIndex2);
 -    QVERIFY(isModelConsistent());
 +    QVERIFY(m_model->isConsistent());
  }
  
  void KFileItemModelTest::testModelConsistencyWhenInsertingItems()
              QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout));
          }
  
 -        QVERIFY(isModelConsistent());
 +        QVERIFY(m_model->isConsistent());
      }
  
      QCOMPARE(m_model->count(), 201);
@@@ -492,7 -490,6 +493,7 @@@ void KFileItemModelTest::testExpandItem
      QCOMPARE(spyRemoved.count(), 1);
      itemRangeList = spyRemoved.takeFirst().at(0).value<KItemRangeList>();
      QCOMPARE(itemRangeList, KItemRangeList() << KItemRange(1, 4)); // 4 items removed
 +    QVERIFY(m_model->isConsistent());
  
      // Clear the model, reload the folder and try to restore the expanded folders.
      m_model->clear();
      QVERIFY(m_model->isExpanded(3));
      QVERIFY(!m_model->isExpanded(4));
      QCOMPARE(m_model->expandedDirectories(), allFolders);
 +    QVERIFY(m_model->isConsistent());
  
      // Move to a sub folder, then call restoreExpandedFolders() *before* going back.
      // This is how DolphinView restores the expanded folders when navigating in history.
@@@ -572,56 -568,6 +573,56 @@@ void KFileItemModelTest::testExpandPare
      QVERIFY(m_model->isExpanded(2));
      QVERIFY(m_model->isExpanded(3));
      QVERIFY(!m_model->isExpanded(4));
 +    QVERIFY(m_model->isConsistent());
 +}
 +
 +/**
 + * Renaming an expanded folder by prepending its name with a dot makes it
 + * hidden. Verify that this does not cause an inconsistent model state and
 + * a crash later on, see https://bugs.kde.org/show_bug.cgi?id=311947
 + */
 +void KFileItemModelTest::testMakeExpandedItemHidden()
 +{
 +    QSet<QByteArray> modelRoles = m_model->roles();
 +    modelRoles << "isExpanded" << "isExpandable" << "expandedParentsCount";
 +    m_model->setRoles(modelRoles);
 +
 +    QStringList files;
 +    m_testDir->createFile("1a/2a/3a");
 +    m_testDir->createFile("1a/2a/3b");
 +    m_testDir->createFile("1a/2b");
 +    m_testDir->createFile("1b");
 +
 +    m_model->loadDirectory(m_testDir->url());
 +    QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout));
 +
 +    // So far, the model contains only "1a/" and "1b".
 +    QCOMPARE(m_model->count(), 2);
 +    m_model->setExpanded(0, true);
 +    QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout));
 +
 +    // Now "1a/2a" and "1a/2b" have appeared.
 +    QCOMPARE(m_model->count(), 4);
 +    m_model->setExpanded(1, true);
 +    QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout));
 +    QCOMPARE(m_model->count(), 6);
 +
 +    // Rename "1a/2" and make it hidden.
 +    const QString oldPath = m_model->fileItem(0).url().path() + "/2a";
 +    const QString newPath = m_model->fileItem(0).url().path() + "/.2a";
 +
 +    KIO::SimpleJob* job = KIO::rename(oldPath, newPath, KIO::HideProgressInfo);
 +    bool ok = job->exec();
 +    QVERIFY(ok);
 +    QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsRemoved(KItemRangeList)), DefaultTimeout));
 +
 +    // "1a/2" and its subfolders have disappeared now.
 +    QVERIFY(m_model->isConsistent());
 +    QCOMPARE(m_model->count(), 3);
 +
 +    m_model->setExpanded(0, false);
 +    QCOMPARE(m_model->count(), 2);
 +
  }
  
  void KFileItemModelTest::testSorting()
@@@ -852,10 -798,43 +853,43 @@@ void KFileItemModelTest::testEmptyPath(
      
      KFileItemList items;
      items << KFileItem(emptyUrl, QString(), KFileItem::Unknown) << KFileItem(url, QString(), KFileItem::Unknown);
 -    m_model->slotNewItems(items);
 +    m_model->slotItemsAdded(emptyUrl, items);
      m_model->slotCompleted();
  }
  
+ /**
+  * Verifies that the 'isExpanded' state of folders does not change when the
+  * 'refreshItems' signal is received, see https://bugs.kde.org/show_bug.cgi?id=299675.
+  */
+ void KFileItemModelTest::testRefreshExpandedItem()
+ {
+     QSet<QByteArray> modelRoles = m_model->roles();
+     modelRoles << "isExpanded" << "isExpandable" << "expandedParentsCount";
+     m_model->setRoles(modelRoles);
+     QStringList files;
+     files << "a/1" << "a/2" << "3" << "4";
+     m_testDir->createFiles(files);
+     m_model->loadDirectory(m_testDir->url());
+     QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout));
+     QCOMPARE(m_model->count(), 3); // "a/", "3", "4"
+     m_model->setExpanded(0, true);
+     QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout));
+     QCOMPARE(m_model->count(), 5); // "a/", "a/1", "a/2", "3", "4"
+     QVERIFY(m_model->isExpanded(0));
+     QSignalSpy spyItemsChanged(m_model, SIGNAL(itemsChanged(KItemRangeList,QSet<QByteArray>)));
+     const KFileItem item = m_model->fileItem(0);
+     m_model->slotRefreshItems(QList<QPair<KFileItem, KFileItem> >() << qMakePair(item, item));
+     QVERIFY(!spyItemsChanged.isEmpty());
+     QCOMPARE(m_model->count(), 5); // "a/", "a/1", "a/2", "3", "4"
+     QVERIFY(m_model->isExpanded(0));
+ }
  /**
   * Verify that removing hidden files and folders from the model does not
   * result in a crash, see https://bugs.kde.org/show_bug.cgi?id=314046
@@@ -1005,96 -984,27 +1039,96 @@@ void KFileItemModelTest::removeParentOf
      QCOMPARE(itemsInModel(), QStringList() << "a" << "1");
  }
  
 -bool KFileItemModelTest::isModelConsistent() const
 +/**
 + * Create a tree structure where parent-child relationships can not be
 + * determined by parsing the URLs, and verify that KFileItemModel
 + * handles them correctly.
 + */
 +void KFileItemModelTest::testGeneralParentChildRelationships()
  {
 -    if (m_model->m_items.count() != m_model->m_itemData.count()) {
 -        return false;
 -    }
 +    QSet<QByteArray> modelRoles = m_model->roles();
 +    modelRoles << "isExpanded" << "isExpandable" << "expandedParentsCount";
 +    m_model->setRoles(modelRoles);
  
 -    for (int i = 0; i < m_model->count(); ++i) {
 -        const KFileItem item = m_model->fileItem(i);
 -        if (item.isNull()) {
 -            qWarning() << "Item" << i << "is null";
 -            return false;
 -        }
 +    QStringList files;
 +    files << "parent1/realChild1/realGrandChild1" << "parent2/realChild2/realGrandChild2";
 +    m_testDir->createFiles(files);
  
 -        const int itemIndex = m_model->index(item);
 -        if (itemIndex != i) {
 -            qWarning() << "Item" << i << "has a wrong index:" << itemIndex;
 -            return false;
 -        }
 -    }
 +    m_model->loadDirectory(m_testDir->url());
 +    QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout));
 +    QCOMPARE(itemsInModel(), QStringList() << "parent1" << "parent2");
 +
 +    // Expand all folders.
 +    m_model->setExpanded(0, true);
 +    QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout));
 +    QCOMPARE(itemsInModel(), QStringList() << "parent1" << "realChild1" << "parent2");
 +
 +    m_model->setExpanded(1, true);
 +    QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout));
 +    QCOMPARE(itemsInModel(), QStringList() << "parent1" << "realChild1" << "realGrandChild1" << "parent2");
 +
 +    m_model->setExpanded(3, true);
 +    QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout));
 +    QCOMPARE(itemsInModel(), QStringList() << "parent1" << "realChild1" << "realGrandChild1" << "parent2" << "realChild2");
 +
 +    m_model->setExpanded(4, true);
 +    QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout));
 +    QCOMPARE(itemsInModel(), QStringList() << "parent1" << "realChild1" << "realGrandChild1" << "parent2" << "realChild2" << "realGrandChild2");
 +
 +    // Add some more children and grand-children.
 +    const KUrl parent1 = m_model->fileItem(0).url();
 +    const KUrl parent2 = m_model->fileItem(3).url();
 +    const KUrl realChild1 = m_model->fileItem(1).url();
 +    const KUrl realChild2 = m_model->fileItem(4).url();
 +
 +    m_model->slotItemsAdded(parent1, KFileItemList() << KFileItem(KUrl("child1"), QString(), KFileItem::Unknown));
 +    m_model->slotCompleted();
 +    QCOMPARE(itemsInModel(), QStringList() << "parent1" << "realChild1" << "realGrandChild1" << "child1" << "parent2" << "realChild2" << "realGrandChild2");
 +
 +    m_model->slotItemsAdded(parent2, KFileItemList() << KFileItem(KUrl("child2"), QString(), KFileItem::Unknown));
 +    m_model->slotCompleted();
 +    QCOMPARE(itemsInModel(), QStringList() << "parent1" << "realChild1" << "realGrandChild1" << "child1" << "parent2" << "realChild2" << "realGrandChild2" << "child2");
  
 -    return true;
 +    m_model->slotItemsAdded(realChild1, KFileItemList() << KFileItem(KUrl("grandChild1"), QString(), KFileItem::Unknown));
 +    m_model->slotCompleted();
 +    QCOMPARE(itemsInModel(), QStringList() << "parent1" << "realChild1" << "grandChild1" << "realGrandChild1" << "child1" << "parent2" << "realChild2" << "realGrandChild2" << "child2");
 +
 +    m_model->slotItemsAdded(realChild1, KFileItemList() << KFileItem(KUrl("grandChild1"), QString(), KFileItem::Unknown));
 +    m_model->slotCompleted();
 +    QCOMPARE(itemsInModel(), QStringList() << "parent1" << "realChild1" << "grandChild1" << "realGrandChild1" << "child1" << "parent2" << "realChild2" << "realGrandChild2" << "child2");
 +
 +    m_model->slotItemsAdded(realChild2, KFileItemList() << KFileItem(KUrl("grandChild2"), QString(), KFileItem::Unknown));
 +    m_model->slotCompleted();
 +    QCOMPARE(itemsInModel(), QStringList() << "parent1" << "realChild1" << "grandChild1" << "realGrandChild1" << "child1" << "parent2" << "realChild2" << "grandChild2" << "realGrandChild2" << "child2");
 +
 +    // Set a name filter that matches nothing -> only expanded folders remain.
 +    QSignalSpy itemsRemovedSpy(m_model, SIGNAL(itemsRemoved(KItemRangeList)));
 +    m_model->setNameFilter("xyz");
 +    QCOMPARE(itemsInModel(), QStringList() << "parent1" << "realChild1" << "parent2" << "realChild2");
 +    QCOMPARE(itemsRemovedSpy.count(), 1);
 +    QList<QVariant> arguments = itemsRemovedSpy.takeFirst();
 +    KItemRangeList itemRangeList = arguments.at(0).value<KItemRangeList>();
 +    QCOMPARE(itemRangeList, KItemRangeList() << KItemRange(2, 3) << KItemRange(7, 3));
 +
 +    // Collapse "parent1".
 +    m_model->setExpanded(0, false);
 +    QCOMPARE(itemsInModel(), QStringList() << "parent1" << "parent2" << "realChild2");
 +    QCOMPARE(itemsRemovedSpy.count(), 1);
 +    arguments = itemsRemovedSpy.takeFirst();
 +    itemRangeList = arguments.at(0).value<KItemRangeList>();
 +    QCOMPARE(itemRangeList, KItemRangeList() << KItemRange(1, 1));
 +
 +    // Remove "parent2".
 +    m_model->slotItemsDeleted(KFileItemList() << m_model->fileItem(1));
 +    QCOMPARE(itemsInModel(), QStringList() << "parent1");
 +    QCOMPARE(itemsRemovedSpy.count(), 1);
 +    arguments = itemsRemovedSpy.takeFirst();
 +    itemRangeList = arguments.at(0).value<KItemRangeList>();
 +    QCOMPARE(itemRangeList, KItemRangeList() << KItemRange(1, 2));
 +
 +    // Clear filter, verify that no items reappear.
 +    m_model->setNameFilter(QString());
 +    QCOMPARE(itemsInModel(), QStringList() << "parent1");
  }
  
  QStringList KFileItemModelTest::itemsInModel() const