]> cloud.milkyroute.net Git - dolphin.git/blobdiff - src/kitemviews/kfileitemmodel.cpp
Merge remote-tracking branch 'origin/KDE/4.10'
[dolphin.git] / src / kitemviews / kfileitemmodel.cpp
index 688826ee416b2e7c41361002c3e1efdb23a9e094..d30d9e5be054605e2abef0c19aa0f795e4d4ec1d 100644 (file)
@@ -1,22 +1,23 @@
-/***************************************************************************
- *   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  *
- *   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"
 
@@ -34,6 +35,9 @@
 #include <QTimer>
 #include <QWidget>
 
+#include <algorithm>
+#include <vector>
+
 // #define KFILEITEMMODEL_DEBUG
 
 KFileItemModel::KFileItemModel(QObject* parent) :
@@ -119,6 +123,11 @@ void KFileItemModel::loadDirectory(const KUrl& url)
 
 void KFileItemModel::refreshDirectory(const KUrl& url)
 {
+    // Refresh all expanded directories first (Bug 295300)
+    foreach (const KUrl& expandedUrl, m_expandedDirs) {
+        m_dirLister->openUrl(expandedUrl, KDirLister::Reload);
+    }
+
     m_dirLister->openUrl(url, KDirLister::Reload);
 }
 
@@ -421,7 +430,8 @@ bool KFileItemModel::setExpanded(int index, bool expanded)
         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);
@@ -429,14 +439,10 @@ bool KFileItemModel::setExpanded(int index, bool expanded)
         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;
-        }
+        const KFileItemList itemsToRemove = childItems(item);
+        removeFilteredChildren(itemsToRemove);
         removeItems(itemsToRemove, DeleteItemData);
     }
 
@@ -574,6 +580,30 @@ void KFileItemModel::applyFilters()
     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;
@@ -802,35 +832,7 @@ void KFileItemModel::slotItemsDeleted(const KFileItemList& items)
         }
 
         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) {
-                KUrl url = item.url();
-                url.adjustPath(KUrl::RemoveTrailingSlash);
-                urlsToRemove.insert(url);
-            }
-
-            QHash<KFileItem, ItemData*>::iterator it = m_filteredItems.begin();
-            while (it != m_filteredItems.end()) {
-                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;
-                }
-            }
+            removeFilteredChildren(itemsToRemove);
         }
     }
 
@@ -954,85 +956,91 @@ void KFileItemModel::dispatchPendingItemsToInsert()
     }
 }
 
-void KFileItemModel::insertItems(QList<ItemData*>& 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();
 
-    sort(items.begin(), items.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 < 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), items.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 'items' to m_itemData.
-        // m_items will be inserted after the loop (see comment below)
-        m_itemData.insert(targetIndex, items.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();
-    m_items.reserve(itemDataCount);
-    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
 }
 
@@ -1124,10 +1132,10 @@ void KFileItemModel::removeItems(const KFileItemList& items, RemoveItemsBehavior
 
     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.
+    // 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 = 0; i < newItemDataCount; ++i) {
+    for (int i = itemRanges.front().index; i < newItemDataCount; ++i) {
         m_items.insert(m_itemData.at(i)->item.url(), i);
     }
 
@@ -1136,6 +1144,13 @@ void KFileItemModel::removeItems(const KFileItemList& items, RemoveItemsBehavior
 
 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);
 
@@ -1312,10 +1327,6 @@ QHash<QByteArray, QVariant> KFileItemModel::retrieveData(const KFileItem& item,
         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());
     }
@@ -1557,6 +1568,11 @@ bool KFileItemModel::useMaximumUpdateInterval() const
     return !m_dirLister->url().isLocalFile();
 }
 
+static bool localeAwareLessThan(const QChar& c1, const QChar& c2)
+{
+    return QString::localeAwareCompare(c1, c2) < 0;
+}
+
 QList<QPair<int, QVariant> > KFileItemModel::nameRoleGroups() const
 {
     Q_ASSERT(!m_itemData.isEmpty());
@@ -1566,7 +1582,6 @@ QList<QPair<int, QVariant> > KFileItemModel::nameRoleGroups() const
 
     QString groupValue;
     QChar firstChar;
-    bool isLetter = false;
     for (int i = 0; i <= maxIndex; ++i) {
         if (isChildItem(i)) {
             continue;
@@ -1582,31 +1597,31 @@ QList<QPair<int, QVariant> > KFileItemModel::nameRoleGroups() const
 
         if (firstChar != newFirstChar) {
             QString newGroupValue;
-            if (newFirstChar >= QLatin1Char('A') && newFirstChar <= QLatin1Char('Z')) {
-                // Apply group 'A' - 'Z'
-                newGroupValue = newFirstChar;
-                isLetter = true;
+            if (newFirstChar.isLetter()) {
+                // Try to find a matching group in the range 'A' to 'Z'.
+                static std::vector<QChar> lettersAtoZ;
+                if (lettersAtoZ.empty()) {
+                    for (char c = 'A'; c <= 'Z'; ++c) {
+                        lettersAtoZ.push_back(QLatin1Char(c));
+                    }
+                }
+
+                std::vector<QChar>::iterator it = std::lower_bound(lettersAtoZ.begin(), lettersAtoZ.end(), newFirstChar, localeAwareLessThan);
+                if (it != lettersAtoZ.end()) {
+                    if (localeAwareLessThan(newFirstChar, *it) && it != lettersAtoZ.begin()) {
+                        // newFirstChar belongs to the group preceding *it.
+                        // Example: for an umlaut 'A' in the German locale, *it would be 'B' now.
+                        --it;
+                    }
+                    newGroupValue = *it;
+                } else {
+                    newGroupValue = newFirstChar;
+                }
             } else if (newFirstChar >= QLatin1Char('0') && newFirstChar <= QLatin1Char('9')) {
                 // Apply group '0 - 9' for any name that starts with a digit
                 newGroupValue = i18nc("@title:group Groups that start with a digit", "0 - 9");
-                isLetter = false;
             } else {
-                if (isLetter) {
-                    // If the current group is 'A' - 'Z' check whether a locale character
-                    // fits into the existing group.
-                    // TODO: This does not work in the case if e.g. the group 'O' starts with
-                    // an umlaut 'O' -> provide unit-test to document this known issue
-                    const QChar prevChar(firstChar.unicode() - ushort(1));
-                    const QChar nextChar(firstChar.unicode() + ushort(1));
-                    const QString currChar(newFirstChar);
-                    const bool partOfCurrentGroup = currChar.localeAwareCompare(prevChar) > 0 &&
-                                                    currChar.localeAwareCompare(nextChar) < 0;
-                    if (partOfCurrentGroup) {
-                        continue;
-                    }
-                }
                 newGroupValue = i18nc("@title:group", "Others");
-                isLetter = false;
             }
 
             if (newGroupValue != groupValue) {
@@ -1932,12 +1947,12 @@ const KFileItemModel::RoleInfoMap* KFileItemModel::rolesInfoMap(int& count)
     return rolesInfoMap;
 }
 
-void KFileItemModel::determineMimeTypes(const QList<ItemData*>& items, int timeout)
+void KFileItemModel::determineMimeTypes(const KFileItemList& items, int timeout)
 {
     QElapsedTimer timer;
     timer.start();
-    foreach (const ItemData* itemData, items) { // krazy:exclude=foreach
-        itemData->item.determineMimeType();
+    foreach (const KFileItem& item, items) { // krazy:exclude=foreach
+        item.determineMimeType();
         if (timer.elapsed() > timeout) {
             // Don't block the user interface, let the remaining items
             // be resolved asynchronously.