]> cloud.milkyroute.net Git - dolphin.git/commitdiff
Merge remote-tracking branch 'origin/KDE/4.10'
authorFrank Reininghaus <frank78ac@googlemail.com>
Sun, 10 Mar 2013 17:31:23 +0000 (18:31 +0100)
committerFrank Reininghaus <frank78ac@googlemail.com>
Sun, 10 Mar 2013 17:31:23 +0000 (18:31 +0100)
Conflicts:
dolphin/src/kitemviews/kfileitemmodel.cpp

1  2 
src/kitemviews/kfileitemmodel.cpp
src/tests/kfileitemmodeltest.cpp

index 688826ee416b2e7c41361002c3e1efdb23a9e094,792724502159cd8ea13341cabe41d310b8874340..61703e760f5f1f01618ceb27a1023b5fb1d1125c
@@@ -1,6 -1,5 +1,6 @@@
  /***************************************************************************
   *   Copyright (C) 2011 by Peter Penz <peter.penz19@gmail.com>             *
 + *   Copyright (C) 2013 by Frank Reininghaus <frank78ac@googlemail.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  *
@@@ -54,10 -53,12 +54,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);
@@@ -68,7 -69,7 +68,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)
@@@ -396,7 -397,7 +396,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!";
@@@ -437,7 -438,30 +437,31 @@@ bool KFileItemModel::setExpanded(int in
              itemsToRemove.append(m_itemData.at(index)->item);
              ++index;
          }
 -        QSet<KFileItem>::iterator it = m_filteredItems.begin();
+         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);
+         }
 -            const KUrl url = it->url();
++        QHash<KFileItem, ItemData*>::iterator it = m_filteredItems.begin();
+         while (it != m_filteredItems.end()) {
 -        removeItems(itemsToRemove);
++            const KUrl url = it.key().url();
+             KUrl parentUrl = url.upUrl();
+             parentUrl.adjustPath(KUrl::RemoveTrailingSlash);
+             if (urlsToRemove.contains(parentUrl)) {
++                delete it.value();
+                 it = m_filteredItems.erase(it);
+             } else {
+                 ++it;
+             }
+         }
 +        removeItems(itemsToRemove, DeleteItemData);
      }
  
      return true;
@@@ -547,27 -571,25 +571,27 @@@ 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;
          }
      }
  
@@@ -659,7 -681,7 +683,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);
      }
@@@ -721,14 -743,11 +745,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()) {
@@@ -785,7 -807,7 +809,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);
 +            QHash<KFileItem, ItemData*>::iterator it = m_filteredItems.find(item);
 +            if (it != m_filteredItems.end()) {
 +                delete it.value();
 +                m_filteredItems.erase(it);
 +            }
          }
  
 -        if (m_requestRole[ExpandedParentsCountRole] && m_expandedParentsCountRoot >= 0) {
 +        if (m_requestRole[ExpandedParentsCountRole]) {
              // 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.
 +            //
 +            // TODO: This does currently *not* work if the parent-child
 +            // relationships can not be determined just by using KUrl::upUrl().
 +            // This is the case, e.g., when browsing smb:/.
              QSet<KUrl> urlsToRemove;
              urlsToRemove.reserve(itemsToRemove.count());
              foreach (const KFileItem& item, itemsToRemove) {
                  urlsToRemove.insert(url);
              }
  
 -            QSet<KFileItem>::iterator it = m_filteredItems.begin();
 +            QHash<KFileItem, ItemData*>::iterator it = m_filteredItems.begin();
              while (it != m_filteredItems.end()) {
 -                const KUrl url = it->url();
 +                const KUrl url = it.key().url();
                  KUrl parentUrl = url.upUrl();
                  parentUrl.adjustPath(KUrl::RemoveTrailingSlash);
  
                  if (urlsToRemove.contains(parentUrl)) {
 +                    delete it.value();
                      it = m_filteredItems.erase(it);
                  } else {
                      ++it;
          }
      }
  
 -    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());
@@@ -916,7 -929,6 +940,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);
@@@ -954,7 -968,7 +978,7 @@@ void KFileItemModel::dispatchPendingIte
      }
  }
  
 -void KFileItemModel::insertItems(const KFileItemList& items)
 +void KFileItemModel::insertItems(QList<ItemData*>& items)
  {
      if (items.isEmpty()) {
          return;
  
      m_groups.clear();
  
 -    QList<ItemData*> sortedItems = createItemDataList(items);
 -    KFileItemModelSortAlgorithm::sort(this, sortedItems.begin(), sortedItems.end());
 +    sort(items.begin(), items.end());
  
  #ifdef KFILEITEMMODEL_DEBUG
      kDebug() << "[TIME] Sorting:" << timer.elapsed();
      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()) {
 +    while (sourceIndex < items.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))) {
 +            if (!lessThan(m_itemData.at(targetIndex), items.at(sourceIndex))) {
                  break;
              }
              ++targetIndex;
          }
  
          // Insert item at the position targetIndex by transferring
 -        // the ownership of the item-data from sortedItems to m_itemData.
 +        // the ownership of the item-data from 'items' to m_itemData.
          // m_items will be inserted after the loop (see comment below)
 -        m_itemData.insert(targetIndex, sortedItems.at(sourceIndex));
 +        m_itemData.insert(targetIndex, items.at(sourceIndex));
          ++insertedCount;
  
          if (insertedAtIndex < 0) {
      // The indexes of all m_items must be adjusted, not only the index
      // of the new items
      const int itemDataCount = m_itemData.count();
 +    m_items.reserve(itemDataCount);
      for (int i = 0; i < itemDataCount; ++i) {
          m_items.insert(m_itemData.at(i)->item.url(), i);
      }
  #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());
 -    foreach (const KFileItem& item, items) {
 -        const int index = m_items.value(item.url(), -1);
 -        if (index >= 0) {
 -            sortedItems.append(m_itemData.at(index));
 -        }
 -    }
 -    KFileItemModelSortAlgorithm::sort(this, sortedItems.begin(), sortedItems.end());
 -
 +    // 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 KUrl url = item.url();
 +        const int index = m_items.value(url, -1);
 +        if (index >= 0) {
 +            indexesToRemove.append(index);
  
 -    // 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;
 +            // 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);
  
 -        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. Note that all indexes
 +    //         might have been changed by the removal of the items.
 +    const int newItemDataCount = m_itemData.count();
 +    for (int i = 0; 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
  {
 +    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);
      }
  
@@@ -1167,8 -1179,9 +1191,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();
  }
  
@@@ -1231,7 -1244,7 +1255,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
      }
  
      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()) {
@@@ -1344,34 -1373,11 +1368,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;
@@@ -1552,6 -1520,88 +1576,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();
@@@ -1664,6 -1714,12 +1688,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) {
@@@ -1932,12 -1999,12 +1956,12 @@@ const KFileItemModel::RoleInfoMap* KFil
      return rolesInfoMap;
  }
  
 -void KFileItemModel::determineMimeTypes(const KFileItemList& items, int timeout)
 +void KFileItemModel::determineMimeTypes(const QList<ItemData*>& items, int timeout)
  {
      QElapsedTimer timer;
      timer.start();
 -    foreach (KFileItem item, items) { // krazy:exclude=foreach
 -        item.determineMimeType();
 +    foreach (const ItemData* itemData, items) { // krazy:exclude=foreach
 +        itemData->item.determineMimeType();
          if (timer.elapsed() > timeout) {
              // Don't block the user interface, let the remaining items
              // be resolved asynchronously.
      }
  }
  
 +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 1145e1535dd6fc4fbeb3d189913f602bebb24ada,58e83ac68cf469f7e9e169ec24ee861f1681c0ed..cd2e8d7773bf0fb468f2941a6ffff1b8f87f3ac9
@@@ -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,15 -71,16 +73,16 @@@ private slots
      void testItemRangeConsistencyWhenInsertingItems();
      void testExpandItems();
      void testExpandParentItems();
 +    void testMakeExpandedItemHidden();
      void testSorting();
      void testIndexForKeyboardSearch();
      void testNameFilter();
      void testEmptyPath();
      void testRemoveHiddenItems();
+     void collapseParentOfHiddenItems();
      void removeParentOfHiddenItems();
  
  private:
 -    bool isModelConsistent() const;
      QStringList itemsInModel() const;
  
  private:
@@@ -156,7 -155,7 +157,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()
@@@ -317,7 -316,7 +318,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);
@@@ -490,7 -489,6 +491,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.
@@@ -570,56 -567,6 +571,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()
@@@ -850,7 -797,7 +851,7 @@@ 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();
  }
  
@@@ -904,6 -851,55 +905,55 @@@ void KFileItemModelTest::testRemoveHidd
      m_model->setShowHiddenFiles(false);
  }
  
+ /**
+  * Verify that filtered items are removed when their parent is collapsed.
+  */
+ void KFileItemModelTest::collapseParentOfHiddenItems()
+ {
+     QSet<QByteArray> modelRoles = m_model->roles();
+     modelRoles << "isExpanded" << "isExpandable" << "expandedParentsCount";
+     m_model->setRoles(modelRoles);
+     QStringList files;
+     files << "a/1" << "a/b/1" << "a/b/c/1" << "a/b/c/d/1";
+     m_testDir->createFiles(files);
+     m_model->loadDirectory(m_testDir->url());
+     QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout));
+     QCOMPARE(m_model->count(), 1); // Only "a/"
+     // Expand "a/".
+     m_model->setExpanded(0, true);
+     QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout));
+     QCOMPARE(m_model->count(), 3); // 3 items: "a/", "a/b/", "a/1"
+     // Expand "a/b/".
+     m_model->setExpanded(1, true);
+     QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout));
+     QCOMPARE(m_model->count(), 5); // 5 items: "a/", "a/b/", "a/b/c", "a/b/1", "a/1"
+     // Expand "a/b/c/".
+     m_model->setExpanded(2, true);
+     QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout));
+     QCOMPARE(m_model->count(), 7); // 7 items: "a/", "a/b/", "a/b/c", "a/b/c/d/", "a/b/c/1", "a/b/1", "a/1"
+     // Set a name filter that matches nothing -> only the expanded folders remain.
+     m_model->setNameFilter("xyz");
+     QCOMPARE(m_model->count(), 3);
+     QCOMPARE(itemsInModel(), QStringList() << "a" << "b" << "c");
+     // Collapse the folder "a/".
+     QSignalSpy spyItemsRemoved(m_model, SIGNAL(itemsRemoved(KItemRangeList)));
+     m_model->setExpanded(0, false);
+     QCOMPARE(spyItemsRemoved.count(), 1);
+     QCOMPARE(m_model->count(), 1);
+     QCOMPARE(itemsInModel(), QStringList() << "a");
+     // Remove the filter -> no files should appear (and we should not get a crash).
+     m_model->setNameFilter(QString());
+     QCOMPARE(m_model->count(), 1);
+ }
  /**
   * Verify that filtered items are removed when their parent is deleted.
   */
@@@ -954,6 -950,29 +1004,6 @@@ void KFileItemModelTest::removeParentOf
      QCOMPARE(itemsInModel(), QStringList() << "a" << "1");
  }
  
 -bool KFileItemModelTest::isModelConsistent() const
 -{
 -    if (m_model->m_items.count() != m_model->m_itemData.count()) {
 -        return false;
 -    }
 -
 -    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;
 -        }
 -
 -        const int itemIndex = m_model->index(item);
 -        if (itemIndex != i) {
 -            qWarning() << "Item" << i << "has a wrong index:" << itemIndex;
 -            return false;
 -        }
 -    }
 -
 -    return true;
 -}
 -
  QStringList KFileItemModelTest::itemsInModel() const
  {
      QStringList items;