X-Git-Url: https://cloud.milkyroute.net/gitweb/dolphin.git/blobdiff_plain/d3a2f1ba82de87dbc0f762263e4509d2d73f7fd0..04e825d022c77baad3345269da7a210e95274f07:/src/kitemviews/kfileitemmodel.cpp diff --git a/src/kitemviews/kfileitemmodel.cpp b/src/kitemviews/kfileitemmodel.cpp index 49c40eda1..d30d9e5be 100644 --- a/src/kitemviews/kfileitemmodel.cpp +++ b/src/kitemviews/kfileitemmodel.cpp @@ -1,42 +1,52 @@ -/*************************************************************************** - * Copyright (C) 2011 by Peter Penz * - * * - * 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 * + * Copyright (C) 2013 by Frank Reininghaus * + * Copyright (C) 2013 by Emmanuel Pescosta * + * * + * 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" -#include #include #include #include #include #include +#include "private/kfileitemmodelsortalgorithm.h" +#include "private/kfileitemmodeldirlister.h" + +#include #include #include +#include + +#include +#include // #define KFILEITEMMODEL_DEBUG -KFileItemModel::KFileItemModel(KDirLister* dirLister, QObject* parent) : - KItemModelBase("name", parent), - m_dirLister(dirLister), +KFileItemModel::KFileItemModel(QObject* parent) : + KItemModelBase("text", parent), + m_dirLister(0), m_naturalSorting(KGlobalSettings::naturalSorting()), - m_sortFoldersFirst(true), + m_sortDirsFirst(true), m_sortRole(NameRole), + m_sortingProgressPercent(-1), m_roles(), m_caseSensitivity(Qt::CaseInsensitive), m_itemData(), @@ -48,26 +58,38 @@ KFileItemModel::KFileItemModel(KDirLister* dirLister, QObject* parent) : m_resortAllItemsTimer(0), m_pendingItemsToInsert(), m_groups(), - m_expandedParentsCountRoot(UninitializedExpandedParentsCountRoot), - m_expandedUrls(), + m_expandedDirs(), m_urlsToExpand() { + m_dirLister = new KFileItemModelDirLister(this); + m_dirLister->setDelayedMimeTypes(true); + + const QWidget* parentWidget = qobject_cast(parent); + if (parentWidget) { + m_dirLister->setMainWindow(parentWidget->window()); + } + + 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(itemsAdded(KUrl,KFileItemList)), this, SLOT(slotItemsAdded(KUrl,KFileItemList))); + connect(m_dirLister, SIGNAL(itemsDeleted(KFileItemList)), this, SLOT(slotItemsDeleted(KFileItemList))); + connect(m_dirLister, SIGNAL(refreshItems(QList >)), this, SLOT(slotRefreshItems(QList >))); + connect(m_dirLister, SIGNAL(clear()), this, SLOT(slotClear())); + connect(m_dirLister, SIGNAL(clear(KUrl)), this, SLOT(slotClear(KUrl))); + connect(m_dirLister, SIGNAL(infoMessage(QString)), this, SIGNAL(infoMessage(QString))); + connect(m_dirLister, SIGNAL(errorMessage(QString)), this, SIGNAL(errorMessage(QString))); + connect(m_dirLister, SIGNAL(redirection(KUrl,KUrl)), this, SIGNAL(directoryRedirection(KUrl,KUrl))); + connect(m_dirLister, SIGNAL(urlIsFileError(KUrl)), this, SIGNAL(urlIsFileError(KUrl))); + // Apply default roles that should be determined resetRoles(); m_requestRole[NameRole] = true; m_requestRole[IsDirRole] = true; - m_roles.insert("name"); + m_requestRole[IsLinkRole] = true; + m_roles.insert("text"); m_roles.insert("isDir"); - - Q_ASSERT(dirLister); - - connect(dirLister, SIGNAL(canceled()), this, SLOT(slotCanceled())); - connect(dirLister, SIGNAL(completed(KUrl)), this, SLOT(slotCompleted())); - connect(dirLister, SIGNAL(newItems(KFileItemList)), this, SLOT(slotNewItems(KFileItemList))); - connect(dirLister, SIGNAL(itemsDeleted(KFileItemList)), this, SLOT(slotItemsDeleted(KFileItemList))); - connect(dirLister, SIGNAL(refreshItems(QList >)), this, SLOT(slotRefreshItems(QList >))); - connect(dirLister, SIGNAL(clear()), this, SLOT(slotClear())); - connect(dirLister, SIGNAL(clear(KUrl)), this, SLOT(slotClear(KUrl))); + m_roles.insert("isLink"); // For slow KIO-slaves like used for searching it makes sense to show results periodically even // before the completed() or canceled() signal has been emitted. @@ -91,7 +113,32 @@ KFileItemModel::KFileItemModel(KDirLister* dirLister, QObject* parent) : KFileItemModel::~KFileItemModel() { qDeleteAll(m_itemData); - m_itemData.clear(); + qDeleteAll(m_filteredItems.values()); +} + +void KFileItemModel::loadDirectory(const KUrl& url) +{ + m_dirLister->openUrl(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); +} + +KUrl KFileItemModel::directory() const +{ + return m_dirLister->url(); +} + +void KFileItemModel::cancelDirectoryLoading() +{ + m_dirLister->stop(); } int KFileItemModel::count() const @@ -134,6 +181,12 @@ bool KFileItemModel::setData(int index, const QHash& value } m_itemData[index]->values = currentValues; + if (changedRoles.contains("text")) { + KUrl url = m_itemData[index]->item.url(); + url.setFileName(currentValues["text"].toString()); + m_itemData[index]->item.setUrl(url); + } + emit itemsChanged(KItemRangeList() << KItemRange(index, 1), changedRoles); if (changedRoles.contains(sortRole())) { @@ -143,49 +196,41 @@ bool KFileItemModel::setData(int index, const QHash& value return true; } -void KFileItemModel::setSortFoldersFirst(bool foldersFirst) +void KFileItemModel::setSortDirectoriesFirst(bool dirsFirst) { - if (foldersFirst != m_sortFoldersFirst) { - m_sortFoldersFirst = foldersFirst; + if (dirsFirst != m_sortDirsFirst) { + m_sortDirsFirst = dirsFirst; resortAllItems(); } } -bool KFileItemModel::sortFoldersFirst() const +bool KFileItemModel::sortDirectoriesFirst() const { - return m_sortFoldersFirst; + return m_sortDirsFirst; } void KFileItemModel::setShowHiddenFiles(bool show) { - KDirLister* dirLister = m_dirLister.data(); - if (dirLister) { - dirLister->setShowingDotFiles(show); - dirLister->emitChanges(); - if (show) { - slotCompleted(); - } + m_dirLister->setShowingDotFiles(show); + m_dirLister->emitChanges(); + if (show) { + slotCompleted(); } } bool KFileItemModel::showHiddenFiles() const { - const KDirLister* dirLister = m_dirLister.data(); - return dirLister ? dirLister->showingDotFiles() : false; + return m_dirLister->showingDotFiles(); } -void KFileItemModel::setShowFoldersOnly(bool enabled) +void KFileItemModel::setShowDirectoriesOnly(bool enabled) { - KDirLister* dirLister = m_dirLister.data(); - if (dirLister) { - dirLister->setDirOnlyMode(enabled); - } + m_dirLister->setDirOnlyMode(enabled); } -bool KFileItemModel::showFoldersOnly() const +bool KFileItemModel::showDirectoriesOnly() const { - KDirLister* dirLister = m_dirLister.data(); - return dirLister ? dirLister->dirOnlyMode() : false; + return m_dirLister->dirOnlyMode(); } QMimeData* KFileItemModel::createMimeData(const QSet& indexes) const @@ -230,12 +275,12 @@ int KFileItemModel::indexForKeyboardSearch(const QString& text, int startFromInd { startFromIndex = qMax(0, startFromIndex); for (int i = startFromIndex; i < count(); ++i) { - if (data(i)["name"].toString().startsWith(text, Qt::CaseInsensitive)) { + if (data(i)["text"].toString().startsWith(text, Qt::CaseInsensitive)) { return i; } } for (int i = 0; i < startFromIndex; ++i) { - if (data(i)["name"].toString().startsWith(text, Qt::CaseInsensitive)) { + if (data(i)["text"].toString().startsWith(text, Qt::CaseInsensitive)) { return i; } } @@ -255,7 +300,7 @@ QString KFileItemModel::roleDescription(const QByteArray& role) const int count = 0; const RoleInfoMap* map = rolesInfoMap(count); for (int i = 0; i < count; ++i) { - description.insert(map[i].role, map[i].roleTranslation); + description.insert(map[i].role, i18nc(map[i].roleTranslationContext, map[i].roleTranslation)); } } @@ -274,19 +319,8 @@ QList > KFileItemModel::groups() const case SizeRole: m_groups = sizeRoleGroups(); break; case DateRole: m_groups = dateRoleGroups(); break; case PermissionsRole: m_groups = permissionRoleGroups(); break; - case OwnerRole: m_groups = genericStringRoleGroups("owner"); break; - case GroupRole: m_groups = genericStringRoleGroups("group"); break; - case TypeRole: m_groups = genericStringRoleGroups("type"); break; - case DestinationRole: m_groups = genericStringRoleGroups("destination"); break; - case PathRole: m_groups = genericStringRoleGroups("path"); break; - case CommentRole: m_groups = genericStringRoleGroups("comment"); break; - case TagsRole: m_groups = genericStringRoleGroups("tags"); break; case RatingRole: m_groups = ratingRoleGroups(); break; - case NoRole: break; - case IsDirRole: break; - case IsExpandedRole: break; - case ExpandedParentsCountRole: break; - default: Q_ASSERT(false); break; + default: m_groups = genericStringRoleGroups(sortRole()); break; } #ifdef KFILEITEMMODEL_DEBUG @@ -333,11 +367,7 @@ int KFileItemModel::index(const KUrl& url) const KFileItem KFileItemModel::rootItem() const { - const KDirLister* dirLister = m_dirLister.data(); - if (dirLister) { - return dirLister->rootItem(); - } - return KFileItem(); + return m_dirLister->rootItem(); } void KFileItemModel::clear() @@ -375,7 +405,7 @@ void KFileItemModel::setRoles(const QSet& roles) // 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!"; @@ -400,34 +430,23 @@ bool KFileItemModel::setExpanded(int index, bool expanded) return false; } - KDirLister* dirLister = m_dirLister.data(); - 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_expandedUrls.insert(url); - - if (dirLister) { - dirLister->openUrl(url, KDirLister::Keep); - return true; - } + m_expandedDirs.insert(url); + m_dirLister->openUrl(url, KDirLister::Keep); } else { - m_expandedUrls.remove(url); + m_expandedDirs.remove(url); + m_dirLister->stop(url); - if (dirLister) { - 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; - } - removeItems(itemsToRemove); - return true; + const KFileItemList itemsToRemove = childItems(item); + removeFilteredChildren(itemsToRemove); + removeItems(itemsToRemove, DeleteItemData); } - return false; + return true; } bool KFileItemModel::isExpanded(int index) const @@ -457,30 +476,25 @@ int KFileItemModel::expandedParentsCount(int index) const return 0; } -QSet KFileItemModel::expandedUrls() const +QSet KFileItemModel::expandedDirectories() const { - return m_expandedUrls; + return m_expandedDirs; } -void KFileItemModel::restoreExpandedUrls(const QSet& urls) +void KFileItemModel::restoreExpandedDirectories(const QSet& urls) { m_urlsToExpand = urls; } -void KFileItemModel::expandParentItems(const KUrl& url) +void KFileItemModel::expandParentDirectories(const KUrl& url) { - const KDirLister* dirLister = m_dirLister.data(); - if (!dirLister) { - return; - } - - const int pos = dirLister->url().path().length(); + const int pos = m_dirLister->url().path().length(); // Assure that each sub-path of the URL that should be // expanded is added to m_urlsToExpand. KDirLister // does not care whether the parent-URL has already been // expanded. - KUrl urlToExpand = dirLister->url(); + KUrl urlToExpand = m_dirLister->url(); const QStringList subDirs = url.path().mid(pos).split(QDir::separator()); for (int i = 0; i < subDirs.count() - 1; ++i) { urlToExpand.addPath(subDirs.at(i)); @@ -504,46 +518,90 @@ void KFileItemModel::setNameFilter(const QString& nameFilter) { if (m_filter.pattern() != nameFilter) { dispatchPendingItemsToInsert(); - m_filter.setPattern(nameFilter); + applyFilters(); + } +} - // Check which shown items from m_itemData must get - // hidden and hence moved to m_filteredItems. - KFileItemList newFilteredItems; - - foreach (ItemData* itemData, m_itemData) { - if (!m_filter.matches(itemData->item)) { - // Only filter non-expanded items as child items may never - // exist without a parent item - if (!itemData->values.value("isExpanded").toBool()) { - newFilteredItems.append(itemData->item); - m_filteredItems.insert(itemData->item); - } - } - } +QString KFileItemModel::nameFilter() const +{ + return m_filter.pattern(); +} - removeItems(newFilteredItems); +void KFileItemModel::setMimeTypeFilters(const QStringList& filters) +{ + if (m_filter.mimeTypes() != filters) { + dispatchPendingItemsToInsert(); + m_filter.setMimeTypes(filters); + applyFilters(); + } +} + +QStringList KFileItemModel::mimeTypeFilters() const +{ + return m_filter.mimeTypes(); +} - // Check which hidden items from m_filteredItems should - // get visible again and hence removed from m_filteredItems. - KFileItemList newVisibleItems; - QMutableSetIterator it(m_filteredItems); - while (it.hasNext()) { - const KFileItem item = it.next(); - if (m_filter.matches(item)) { - newVisibleItems.append(item); - m_filteredItems.remove(item); +void KFileItemModel::applyFilters() +{ + // Check which shown items from m_itemData must get + // hidden and hence moved to m_filteredItems. + KFileItemList newFilteredItems; + + foreach (ItemData* itemData, m_itemData) { + // Only filter non-expanded items as child items may never + // exist without a parent item + if (!itemData->values.value("isExpanded").toBool()) { + const KFileItem item = itemData->item; + if (!m_filter.matches(item)) { + newFilteredItems.append(item); + m_filteredItems.insert(item, itemData); } } + } + + removeItems(newFilteredItems, KeepItemData); + + // Check which hidden items from m_filteredItems should + // get visible again and hence removed from m_filteredItems. + QList newVisibleItems; - insertItems(newVisibleItems); + QHash::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); } -QString KFileItemModel::nameFilter() const +void KFileItemModel::removeFilteredChildren(const KFileItemList& parentsList) { - return m_filter.pattern(); + 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 parents = parentsList.toSet(); + + QHash::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::rolesInformation() @@ -556,8 +614,17 @@ QList KFileItemModel::rolesInformation() if (map[i].roleType != NoRole) { RoleInfo info; info.role = map[i].role; - info.translation = map[i].roleTranslation; - info.group = map[i].groupTranslation; + info.translation = i18nc(map[i].roleTranslationContext, map[i].roleTranslation); + if (map[i].groupTranslation) { + info.group = i18nc(map[i].groupTranslationContext, map[i].groupTranslation); + } else { + // For top level roles, groupTranslation is 0. We must make sure that + // info.group is an empty string then because the code that generates + // menus tries to put the actions into sub menus otherwise. + info.group = QString(); + } + info.requiresNepomuk = map[i].requiresNepomuk; + info.requiresIndexer = map[i].requiresIndexer; rolesInfo.append(info); } } @@ -656,7 +723,7 @@ void KFileItemModel::slotCompleted() // Note that the parent folder must be expanded before any of its subfolders become visible. // Therefore, some URLs in m_restoredExpandedUrls might not be visible yet // -> we expand the first visible URL we find in m_restoredExpandedUrls. - foreach(const KUrl& url, m_urlsToExpand) { + foreach (const KUrl& url, m_urlsToExpand) { const int index = m_items.value(url, -1); if (index >= 0) { m_urlsToExpand.remove(url); @@ -673,20 +740,25 @@ void KFileItemModel::slotCompleted() m_urlsToExpand.clear(); } - emit loadingCompleted(); + emit directoryLoadingCompleted(); } void KFileItemModel::slotCanceled() { m_maximumUpdateIntervalTimer->stop(); dispatchPendingItemsToInsert(); + + 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(); @@ -707,8 +779,6 @@ void KFileItemModel::slotNewItems(const KFileItemList& items) // 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. @@ -716,22 +786,21 @@ void KFileItemModel::slotNewItems(const KFileItemList& items) } } - if (m_filter.pattern().isEmpty()) { - m_pendingItemsToInsert.append(items); + QList itemDataList = createItemDataList(parentUrl, items); + + if (!m_filter.hasSetFilters()) { + m_pendingItemsToInsert.append(itemDataList); } else { - // The name-filter is active. Hide filtered items + // 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()) { @@ -746,7 +815,7 @@ void KFileItemModel::slotItemsDeleted(const KFileItemList& items) 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)); @@ -755,11 +824,19 @@ void KFileItemModel::slotItemsDeleted(const KFileItemList& items) if (!m_filteredItems.isEmpty()) { foreach (const KFileItem& item, itemsToRemove) { - m_filteredItems.remove(item); + QHash::iterator it = m_filteredItems.find(item); + if (it != m_filteredItems.end()) { + delete it.value(); + m_filteredItems.erase(it); + } + } + + if (m_requestRole[ExpandedParentsCountRole]) { + removeFilteredChildren(itemsToRemove); } } - removeItems(itemsToRemove); + removeItems(itemsToRemove, DeleteItemData); } void KFileItemModel::slotRefreshItems(const QList >& items) @@ -783,7 +860,15 @@ void KFileItemModel::slotRefreshItems(const QList >& const int index = m_items.value(oldItem.url(), -1); if (index >= 0) { m_itemData[index]->item = newItem; - m_itemData[index]->values = retrieveData(newItem); + + // Keep old values as long as possible if they could not retrieved synchronously yet. + // The update of the values will be done asynchronously by KFileItemModelRolesUpdater. + QHashIterator it(retrieveData(newItem, m_itemData.at(index)->parent)); + while (it.hasNext()) { + it.next(); + m_itemData[index]->values.insert(it.key(), it.value()); + } + m_items.remove(oldItem.url()); m_items.insert(newItem.url(), index); indexes.append(index); @@ -833,6 +918,7 @@ void KFileItemModel::slotClear() kDebug() << "Clearing all items"; #endif + qDeleteAll(m_filteredItems.values()); m_filteredItems.clear(); m_groups.clear(); @@ -840,8 +926,6 @@ void KFileItemModel::slotClear() m_resortAllItemsTimer->stop(); m_pendingItemsToInsert.clear(); - m_expandedParentsCountRoot = UninitializedExpandedParentsCountRoot; - const int removedCount = m_itemData.count(); if (removedCount > 0) { qDeleteAll(m_itemData); @@ -850,7 +934,7 @@ void KFileItemModel::slotClear() emit itemsRemoved(KItemRangeList() << KItemRange(0, removedCount)); } - m_expandedUrls.clear(); + m_expandedDirs.clear(); } void KFileItemModel::slotClear(const KUrl& url) @@ -872,9 +956,9 @@ void KFileItemModel::dispatchPendingItemsToInsert() } } -void KFileItemModel::insertItems(const KFileItemList& items) +void KFileItemModel::insertItems(QList& newItems) { - if (items.isEmpty()) { + if (newItems.isEmpty()) { return; } @@ -882,180 +966,202 @@ void KFileItemModel::insertItems(const KFileItemList& items) QElapsedTimer timer; timer.start(); kDebug() << "==========================================================="; - kDebug() << "Inserting" << items.count() << "items"; + kDebug() << "Inserting" << newItems.count() << "items"; #endif m_groups.clear(); - QList sortedItems = createItemDataList(items); - sort(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 transfering - // 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& sortedNumbers) { - if (items.isEmpty()) { - return; + if (sortedNumbers.empty()) { + return KItemRangeList(); + } + + KItemRangeList result; + + QList::const_iterator it = sortedNumbers.begin(); + int index = *it; + int count = 1; + + ++it; + + QList::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 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 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)); - } - } - sort(sortedItems.begin(), sortedItems.end()); + indexesToRemove.append(index); - QList indexesToRemove; - indexesToRemove.reserve(items.count()); + // Prevent repeated expensive rehashing by using QHash::erase(), + // rather than QHash::remove(). + QHash::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::createItemDataList(const KFileItemList& items) const +QList 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 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); } @@ -1076,11 +1182,9 @@ void KFileItemModel::removeExpandedItems() // The m_expandedParentsCountRoot may not get reset before all items with // a bigger count have been removed. - Q_ASSERT(m_expandedParentsCountRoot >= 0); - removeItems(expandedItems); + removeItems(expandedItems, DeleteItemData); - m_expandedParentsCountRoot = UninitializedExpandedParentsCountRoot; - m_expandedUrls.clear(); + m_expandedDirs.clear(); } void KFileItemModel::resetRoles() @@ -1105,6 +1209,7 @@ KFileItemModel::RoleType KFileItemModel::typeForRole(const QByteArray& role) con // Insert internal roles (take care to synchronize the implementation // with KFileItemModel::roleForType() in case if a change is done). roles.insert("isDir", IsDirRole); + roles.insert("isLink", IsLinkRole); roles.insert("isExpanded", IsExpandedRole); roles.insert("isExpandable", IsExpandableRole); roles.insert("expandedParentsCount", ExpandedParentsCountRole); @@ -1130,6 +1235,7 @@ QByteArray KFileItemModel::roleForType(RoleType roleType) const // Insert internal roles (take care to synchronize the implementation // with KFileItemModel::typeForRole() in case if a change is done). roles.insert(IsDirRole, "isDir"); + roles.insert(IsLinkRole, "isLink"); roles.insert(IsExpandedRole, "isExpanded"); roles.insert(IsExpandableRole, "isExpandable"); roles.insert(ExpandedParentsCountRole, "expandedParentsCount"); @@ -1140,13 +1246,12 @@ QByteArray KFileItemModel::roleForType(RoleType roleType) const return roles.value(roleType); } -QHash KFileItemModel::retrieveData(const KFileItem& item) const +QHash 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 // and hence will be retrieved asynchronously by KFileItemModelRolesUpdater. QHash data; - data.insert("iconPixmap", QPixmap()); data.insert("url", item.url()); const bool isDir = item.isDir(); @@ -1154,8 +1259,13 @@ QHash KFileItemModel::retrieveData(const KFileItem& item) data.insert("isDir", isDir); } + if (m_requestRole[IsLinkRole]) { + const bool isLink = item.isLink(); + data.insert("isLink", isLink); + } + if (m_requestRole[NameRole]) { - data.insert("name", item.text()); + data.insert("text", item.text()); } if (m_requestRole[SizeRole]) { @@ -1189,7 +1299,7 @@ QHash KFileItemModel::retrieveData(const KFileItem& item) if (m_requestRole[DestinationRole]) { QString destination = item.linkDest(); if (destination.isEmpty()) { - destination = i18nc("@item:intable", "No destination"); + destination = QLatin1String("-"); } data.insert("destination", destination); } @@ -1199,7 +1309,17 @@ QHash KFileItemModel::retrieveData(const KFileItem& item) if (item.url().protocol() == QLatin1String("trash")) { path = item.entry().stringValue(KIO::UDSEntry::UDS_EXTRA); } else { + // For performance reasons cache the home-path in a static QString + // (see QDir::homePath() for more details) + static QString homePath; + if (homePath.isEmpty()) { + homePath = QDir::homePath(); + } + path = item.localPath(); + if (path.startsWith(homePath)) { + path.replace(0, homePath.length(), QLatin1Char('~')); + } } const int index = path.lastIndexOf(item.text()); @@ -1207,37 +1327,17 @@ QHash 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()); } if (m_requestRole[ExpandedParentsCountRole]) { - if (m_expandedParentsCountRoot == UninitializedExpandedParentsCountRoot && m_dirLister.data()) { - const KUrl rootUrl = m_dirLister.data()->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()) { @@ -1255,15 +1355,38 @@ bool KFileItemModel::lessThan(const ItemData* a, const ItemData* b) const { 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; } } - if (m_sortFoldersFirst || m_sortRole == SizeRole) { + if (m_sortDirsFirst || m_sortRole == SizeRole) { const bool isDirA = a->item.isDir(); const bool isDirB = b->item.isDir(); if (isDirA && !isDirB) { @@ -1278,6 +1401,44 @@ bool KFileItemModel::lessThan(const ItemData* a, const ItemData* b) const 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::iterator begin, + QList::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; @@ -1338,22 +1499,22 @@ int KFileItemModel::sortRoleCompare(const ItemData* a, const ItemData* b) const break; } - case PermissionsRole: - case OwnerRole: - case GroupRole: - case TypeRole: - case DestinationRole: - case PathRole: - case CommentRole: - case TagsRole: { + case ImageSizeRole: { + // Alway use a natural comparing to interpret the numbers of a string like + // "1600 x 1200" for having a correct sorting. + result = KStringHandler::naturalCompare(a->values.value("imageSize").toString(), + b->values.value("imageSize").toString(), + Qt::CaseSensitive); + break; + } + + default: { const QByteArray role = roleForType(m_sortRole); result = QString::compare(a->values.value(role).toString(), b->values.value(role).toString()); break; } - default: - break; } if (result != 0) { @@ -1380,128 +1541,6 @@ int KFileItemModel::sortRoleCompare(const ItemData* a, const ItemData* b) const return QString::compare(itemA.url().url(), itemB.url().url(), Qt::CaseSensitive); } -void KFileItemModel::sort(QList::iterator begin, - QList::iterator end) -{ - // The implementation is based on qStableSortHelper() from qalgorithms.h - // Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). - // In opposite to qStableSort() it allows to use a member-function for the comparison of elements. - - const int span = end - begin; - if (span < 2) { - return; - } - - const QList::iterator middle = begin + span / 2; - sort(begin, middle); - sort(middle, end); - merge(begin, middle, end); -} - -void KFileItemModel::merge(QList::iterator begin, - QList::iterator pivot, - QList::iterator end) -{ - // The implementation is based on qMerge() from qalgorithms.h - // Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). - - const int len1 = pivot - begin; - const int len2 = end - pivot; - - if (len1 == 0 || len2 == 0) { - return; - } - - if (len1 + len2 == 2) { - if (lessThan(*(begin + 1), *(begin))) { - qSwap(*begin, *(begin + 1)); - } - return; - } - - QList::iterator firstCut; - QList::iterator secondCut; - int len2Half; - if (len1 > len2) { - const int len1Half = len1 / 2; - firstCut = begin + len1Half; - secondCut = lowerBound(pivot, end, *firstCut); - len2Half = secondCut - pivot; - } else { - len2Half = len2 / 2; - secondCut = pivot + len2Half; - firstCut = upperBound(begin, pivot, *secondCut); - } - - reverse(firstCut, pivot); - reverse(pivot, secondCut); - reverse(firstCut, secondCut); - - const QList::iterator newPivot = firstCut + len2Half; - merge(begin, firstCut, newPivot); - merge(newPivot, secondCut, end); -} - -QList::iterator KFileItemModel::lowerBound(QList::iterator begin, - QList::iterator end, - const ItemData* value) -{ - // The implementation is based on qLowerBound() from qalgorithms.h - // Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). - - QList::iterator middle; - int n = int(end - begin); - int half; - - while (n > 0) { - half = n >> 1; - middle = begin + half; - if (lessThan(*middle, value)) { - begin = middle + 1; - n -= half + 1; - } else { - n = half; - } - } - return begin; -} - -QList::iterator KFileItemModel::upperBound(QList::iterator begin, - QList::iterator end, - const ItemData* value) -{ - // The implementation is based on qUpperBound() from qalgorithms.h - // Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). - - QList::iterator middle; - int n = end - begin; - int half; - - while (n > 0) { - half = n >> 1; - middle = begin + half; - if (lessThan(value, *middle)) { - n = half; - } else { - begin = middle + 1; - n -= half + 1; - } - } - return begin; -} - -void KFileItemModel::reverse(QList::iterator begin, - QList::iterator end) -{ - // The implementation is based on qReverse() from qalgorithms.h - // Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). - - --end; - while (begin < end) { - qSwap(*begin++, *end--); - } -} - int KFileItemModel::stringCompare(const QString& a, const QString& b) const { // Taken from KDirSortFilterProxyModel (kdelibs/kfile/kdirsortfilterproxymodel.*) @@ -1524,92 +1563,14 @@ int KFileItemModel::stringCompare(const QString& a, const QString& b) const : 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 ((pathA.at(index) != QLatin1Char('/') || pathB.at(index) != QLatin1Char('/')) && index > 0) { - --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_sortFoldersFirst || 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 +bool KFileItemModel::useMaximumUpdateInterval() const { - Q_ASSERT(isDir); - const int pathIndex = itemPath.indexOf('/', start + 1); - *isDir = (pathIndex > 0) || item.isDir(); - return itemPath.mid(start, pathIndex - start); + return !m_dirLister->url().isLocalFile(); } -bool KFileItemModel::useMaximumUpdateInterval() const +static bool localeAwareLessThan(const QChar& c1, const QChar& c2) { - const KDirLister* dirLister = m_dirLister.data(); - return dirLister && !dirLister->url().isLocalFile(); + return QString::localeAwareCompare(c1, c2) < 0; } QList > KFileItemModel::nameRoleGroups() const @@ -1621,13 +1582,12 @@ QList > KFileItemModel::nameRoleGroups() const QString groupValue; QChar firstChar; - bool isLetter = false; for (int i = 0; i <= maxIndex; ++i) { if (isChildItem(i)) { continue; } - const QString name = m_itemData.at(i)->values.value("name").toString(); + const QString name = m_itemData.at(i)->values.value("text").toString(); // Use the first character of the name as group indication QChar newFirstChar = name.at(0).toUpper(); @@ -1637,31 +1597,31 @@ QList > 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 lettersAtoZ; + if (lettersAtoZ.empty()) { + for (char c = 'A'; c <= 'Z'; ++c) { + lettersAtoZ.push_back(QLatin1Char(c)); + } + } + + std::vector::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) { @@ -1719,12 +1679,6 @@ QList > KFileItemModel::dateRoleGroups() const 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) { @@ -1742,20 +1696,9 @@ QList > KFileItemModel::dateRoleGroups() const 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; @@ -1764,7 +1707,7 @@ QList > KFileItemModel::dateRoleGroups() const } 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"); @@ -1787,7 +1730,7 @@ QList > KFileItemModel::dateRoleGroups() const } 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) { @@ -1944,36 +1887,125 @@ KFileItemList KFileItemModel::childItems(const KFileItem& item) const return items; } +void KFileItemModel::emitSortProgress(int resolvedCount) +{ + // Be tolerant against a resolvedCount with a wrong range. + // Although there should not be a case where KFileItemModelRolesUpdater + // (= caller) provides a wrong range, it is important to emit + // a useful progress information even if there is an unexpected + // implementation issue. + + const int itemCount = count(); + if (resolvedCount >= itemCount) { + m_sortingProgressPercent = -1; + if (m_resortAllItemsTimer->isActive()) { + m_resortAllItemsTimer->stop(); + resortAllItems(); + } + + emit directorySortingProgress(100); + } else if (itemCount > 0) { + resolvedCount = qBound(0, resolvedCount, itemCount); + + const int progress = resolvedCount * 100 / itemCount; + if (m_sortingProgressPercent != progress) { + m_sortingProgressPercent = progress; + emit directorySortingProgress(progress); + } + } +} + const KFileItemModel::RoleInfoMap* KFileItemModel::rolesInfoMap(int& count) { static const RoleInfoMap rolesInfoMap[] = { - // role roleType role translation group translation - { 0, NoRole, 0, 0, 0, 0 }, - { "name", NameRole, I18N_NOOP2_NOSTRIP("@label", "Name"), 0, 0 }, - { "size", SizeRole, I18N_NOOP2_NOSTRIP("@label", "Size"), 0, 0 }, - { "date", DateRole, I18N_NOOP2_NOSTRIP("@label", "Date"), 0, 0 }, - { "type", TypeRole, I18N_NOOP2_NOSTRIP("@label", "Type"), 0, 0 }, - { "rating", RatingRole, I18N_NOOP2_NOSTRIP("@label", "Rating"), 0, 0 }, - { "tags", TagsRole, I18N_NOOP2_NOSTRIP("@label", "Tags"), 0, 0 }, - { "comment", CommentRole, I18N_NOOP2_NOSTRIP("@label", "Comment"), 0, 0 }, - { "wordCount", WordCountRole, I18N_NOOP2_NOSTRIP("@label", "Word Count"), I18N_NOOP2_NOSTRIP("@label", "Document") }, - { "lineCount", LineCountRole, I18N_NOOP2_NOSTRIP("@label", "Line Count"), I18N_NOOP2_NOSTRIP("@label", "Document") }, - { "imageSize", ImageSizeRole, I18N_NOOP2_NOSTRIP("@label", "Image Size"), I18N_NOOP2_NOSTRIP("@label", "Image") }, - { "orientation", OrientationRole, I18N_NOOP2_NOSTRIP("@label", "Orientation"), I18N_NOOP2_NOSTRIP("@label", "Image") }, - { "artist", ArtistRole, I18N_NOOP2_NOSTRIP("@label", "Artist"), I18N_NOOP2_NOSTRIP("@label", "Music") }, - { "album", AlbumRole, I18N_NOOP2_NOSTRIP("@label", "Album"), I18N_NOOP2_NOSTRIP("@label", "Music") }, - { "duration", DurationRole, I18N_NOOP2_NOSTRIP("@label", "Duration"), I18N_NOOP2_NOSTRIP("@label", "Music") }, - { "track", TrackRole, I18N_NOOP2_NOSTRIP("@label", "Track"), I18N_NOOP2_NOSTRIP("@label", "Music") }, - { "path", PathRole, I18N_NOOP2_NOSTRIP("@label", "Path"), I18N_NOOP2_NOSTRIP("@label", "Other") }, - { "destination", DestinationRole, I18N_NOOP2_NOSTRIP("@label", "Link Destination"), I18N_NOOP2_NOSTRIP("@label", "Other") }, - { "copiedFrom", CopiedFromRole, I18N_NOOP2_NOSTRIP("@label", "Copied From"), I18N_NOOP2_NOSTRIP("@label", "Other") }, - { "permissions", PermissionsRole, I18N_NOOP2_NOSTRIP("@label", "Permissions"), I18N_NOOP2_NOSTRIP("@label", "Other") }, - { "owner", OwnerRole, I18N_NOOP2_NOSTRIP("@label", "Owner"), I18N_NOOP2_NOSTRIP("@label", "Other") }, - { "group", GroupRole, I18N_NOOP2_NOSTRIP("@label", "Group"), I18N_NOOP2_NOSTRIP("@label", "Other") }, + // | role | roleType | role translation | group translation | requires Nepomuk | requires indexer + { 0, NoRole, 0, 0, 0, 0, false, false }, + { "text", NameRole, I18N_NOOP2_NOSTRIP("@label", "Name"), 0, 0, false, false }, + { "size", SizeRole, I18N_NOOP2_NOSTRIP("@label", "Size"), 0, 0, false, false }, + { "date", DateRole, I18N_NOOP2_NOSTRIP("@label", "Date"), 0, 0, false, false }, + { "type", TypeRole, I18N_NOOP2_NOSTRIP("@label", "Type"), 0, 0, false, false }, + { "rating", RatingRole, I18N_NOOP2_NOSTRIP("@label", "Rating"), 0, 0, true, false }, + { "tags", TagsRole, I18N_NOOP2_NOSTRIP("@label", "Tags"), 0, 0, true, false }, + { "comment", CommentRole, I18N_NOOP2_NOSTRIP("@label", "Comment"), 0, 0, true, false }, + { "wordCount", WordCountRole, I18N_NOOP2_NOSTRIP("@label", "Word Count"), I18N_NOOP2_NOSTRIP("@label", "Document"), true, true }, + { "lineCount", LineCountRole, I18N_NOOP2_NOSTRIP("@label", "Line Count"), I18N_NOOP2_NOSTRIP("@label", "Document"), true, true }, + { "imageSize", ImageSizeRole, I18N_NOOP2_NOSTRIP("@label", "Image Size"), I18N_NOOP2_NOSTRIP("@label", "Image"), true, true }, + { "orientation", OrientationRole, I18N_NOOP2_NOSTRIP("@label", "Orientation"), I18N_NOOP2_NOSTRIP("@label", "Image"), true, true }, + { "artist", ArtistRole, I18N_NOOP2_NOSTRIP("@label", "Artist"), I18N_NOOP2_NOSTRIP("@label", "Audio"), true, true }, + { "album", AlbumRole, I18N_NOOP2_NOSTRIP("@label", "Album"), I18N_NOOP2_NOSTRIP("@label", "Audio"), true, true }, + { "duration", DurationRole, I18N_NOOP2_NOSTRIP("@label", "Duration"), I18N_NOOP2_NOSTRIP("@label", "Audio"), true, true }, + { "track", TrackRole, I18N_NOOP2_NOSTRIP("@label", "Track"), I18N_NOOP2_NOSTRIP("@label", "Audio"), true, true }, + { "path", PathRole, I18N_NOOP2_NOSTRIP("@label", "Path"), I18N_NOOP2_NOSTRIP("@label", "Other"), false, false }, + { "destination", DestinationRole, I18N_NOOP2_NOSTRIP("@label", "Link Destination"), I18N_NOOP2_NOSTRIP("@label", "Other"), false, false }, + { "copiedFrom", CopiedFromRole, I18N_NOOP2_NOSTRIP("@label", "Copied From"), I18N_NOOP2_NOSTRIP("@label", "Other"), true, false }, + { "permissions", PermissionsRole, I18N_NOOP2_NOSTRIP("@label", "Permissions"), I18N_NOOP2_NOSTRIP("@label", "Other"), false, false }, + { "owner", OwnerRole, I18N_NOOP2_NOSTRIP("@label", "Owner"), I18N_NOOP2_NOSTRIP("@label", "Other"), false, false }, + { "group", GroupRole, I18N_NOOP2_NOSTRIP("@label", "User Group"), I18N_NOOP2_NOSTRIP("@label", "Other"), false, false }, }; count = sizeof(rolesInfoMap) / sizeof(RoleInfoMap); return rolesInfoMap; } +void KFileItemModel::determineMimeTypes(const KFileItemList& items, int timeout) +{ + QElapsedTimer timer; + timer.start(); + 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. + return; + } + } +} + +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"