X-Git-Url: https://cloud.milkyroute.net/gitweb/dolphin.git/blobdiff_plain/c872dcbda9b0fbcd945a7917ae9e4b3cb61f347c..c0559a2a1d7d66b26e1d00b4ff59c7fce8848566:/src/kitemviews/kfileitemmodel.cpp diff --git a/src/kitemviews/kfileitemmodel.cpp b/src/kitemviews/kfileitemmodel.cpp index 0a89068b4..61f512a8e 100644 --- a/src/kitemviews/kfileitemmodel.cpp +++ b/src/kitemviews/kfileitemmodel.cpp @@ -19,62 +19,74 @@ #include "kfileitemmodel.h" -#include #include +#include #include #include #include +#include "private/kfileitemmodelsortalgorithm.h" +#include "private/kfileitemmodeldirlister.h" + +#include #include #include +#include -#define KFILEITEMMODEL_DEBUG +// #define KFILEITEMMODEL_DEBUG -KFileItemModel::KFileItemModel(KDirLister* dirLister, QObject* parent) : - KItemModelBase("name", parent), - m_dirLister(dirLister), - m_naturalSorting(true), - m_sortFoldersFirst(true), +KFileItemModel::KFileItemModel(QObject* parent) : + KItemModelBase("text", parent), + m_dirLister(0), + m_naturalSorting(KGlobalSettings::naturalSorting()), + m_sortDirsFirst(true), m_sortRole(NameRole), + m_sortingProgressPercent(-1), m_roles(), m_caseSensitivity(Qt::CaseInsensitive), - m_sortedItems(), + m_itemData(), m_items(), - m_data(), + m_filter(), + m_filteredItems(), m_requestRole(), - m_minimumUpdateIntervalTimer(0), m_maximumUpdateIntervalTimer(0), + m_resortAllItemsTimer(0), m_pendingItemsToInsert(), - m_pendingEmitLoadingCompleted(false), m_groups(), - m_rootExpansionLevel(-1), - m_expandedUrls(), - m_restoredExpandedUrls() + 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(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(newItems(KFileItemList)), this, SLOT(slotNewItems(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()), 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))); - - // Although the layout engine of KItemListView is fast it is very inefficient to e.g. - // emit 50 itemsInserted()-signals each 100 ms. m_minimumUpdateIntervalTimer assures that updates - // are done in 1 second intervals for equal operations. - m_minimumUpdateIntervalTimer = new QTimer(this); - m_minimumUpdateIntervalTimer->setInterval(1000); - m_minimumUpdateIntervalTimer->setSingleShot(true); - connect(m_minimumUpdateIntervalTimer, SIGNAL(timeout()), this, SLOT(dispatchPendingItemsToInsert())); + 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. @@ -83,22 +95,53 @@ KFileItemModel::KFileItemModel(KDirLister* dirLister, QObject* parent) : m_maximumUpdateIntervalTimer->setSingleShot(true); connect(m_maximumUpdateIntervalTimer, SIGNAL(timeout()), this, SLOT(dispatchPendingItemsToInsert())); - Q_ASSERT(m_minimumUpdateIntervalTimer->interval() <= m_maximumUpdateIntervalTimer->interval()); + // When changing the value of an item which represents the sort-role a resorting must be + // triggered. Especially in combination with KFileItemModelRolesUpdater this might be done + // for a lot of items within a quite small timeslot. To prevent expensive resortings the + // resorting is postponed until the timer has been exceeded. + m_resortAllItemsTimer = new QTimer(this); + m_resortAllItemsTimer->setInterval(500); + m_resortAllItemsTimer->setSingleShot(true); + connect(m_resortAllItemsTimer, SIGNAL(timeout()), this, SLOT(resortAllItems())); + + connect(KGlobalSettings::self(), SIGNAL(naturalSortingChanged()), this, SLOT(slotNaturalSortingChanged())); } KFileItemModel::~KFileItemModel() { + qDeleteAll(m_itemData); + m_itemData.clear(); +} + +void KFileItemModel::loadDirectory(const KUrl& url) +{ + m_dirLister->openUrl(url); +} + +void KFileItemModel::refreshDirectory(const KUrl& url) +{ + m_dirLister->openUrl(url, KDirLister::Reload); +} + +KUrl KFileItemModel::directory() const +{ + return m_dirLister->url(); +} + +void KFileItemModel::cancelDirectoryLoading() +{ + m_dirLister->stop(); } int KFileItemModel::count() const { - return m_data.count(); + return m_itemData.count(); } QHash KFileItemModel::data(int index) const { if (index >= 0 && index < count()) { - return m_data.at(index); + return m_itemData.at(index)->values; } return QHash(); } @@ -109,7 +152,7 @@ bool KFileItemModel::setData(int index, const QHash& value return false; } - QHash currentValue = m_data.at(index); + QHash currentValues = m_itemData.at(index)->values; // Determine which roles have been changed QSet changedRoles; @@ -119,8 +162,8 @@ bool KFileItemModel::setData(int index, const QHash& value const QByteArray role = it.key(); const QVariant value = it.value(); - if (currentValue[role] != value) { - currentValue[role] = value; + if (currentValues[role] != value) { + currentValues[role] = value; changedRoles.insert(role); } } @@ -129,98 +172,57 @@ bool KFileItemModel::setData(int index, const QHash& value return false; } - m_data[index] = currentValue; - - if (!changedRoles.contains(sortRole())) { - emit itemsChanged(KItemRangeList() << KItemRange(index, 1), changedRoles); - return true; - } - - // The sort role has been changed which might result in a changed - // item index. In this case instead of emitting the itemsChanged() - // signal the following is done: - // 1. The item gets removed from the current position and the signal - // itemsRemoved() will be emitted. - // 2. The item gets inserted to the new position and the signal - // itemsInserted() will be emitted. - - const KFileItem& changedItem = m_sortedItems.at(index); - const bool sortOrderDecreased = (index > 0 && lessThan(changedItem, m_sortedItems.at(index - 1))); - const bool sortOrderIncreased = !sortOrderDecreased && - (index < count() - 1 && lessThan(m_sortedItems.at(index + 1), changedItem)); - - if (!sortOrderDecreased && !sortOrderIncreased) { - // Although the value of the sort-role has been changed it did not result - // into a changed position. - emit itemsChanged(KItemRangeList() << KItemRange(index, 1), changedRoles); - return true; + 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); } - m_groups.clear(); - - if (!m_pendingItemsToInsert.isEmpty()) { - insertItems(m_pendingItemsToInsert); - m_pendingItemsToInsert.clear(); - } - - // Do a binary search to find the new position where the item - // should get inserted. The result will be stored in 'mid'. - int min = sortOrderIncreased ? index + 1 : 0; - int max = sortOrderDecreased ? index - 1 : count() - 1; - int mid = 0; - do { - mid = (min + max) / 2; - if (lessThan(m_sortedItems.at(mid), changedItem)) { - min = mid + 1; - } else { - max = mid - 1; - } - } while (min <= max); + emit itemsChanged(KItemRangeList() << KItemRange(index, 1), changedRoles); - if (sortOrderIncreased && mid == max && lessThan(m_sortedItems.at(max), changedItem)) { - ++mid; + if (changedRoles.contains(sortRole())) { + m_resortAllItemsTimer->start(); } - // Remove the item from the old position - const KFileItem removedItem = changedItem; - const QHash removedData = m_data[index]; + return true; +} - m_items.remove(changedItem.url()); - m_sortedItems.removeAt(index); - m_data.removeAt(index); - for (int i = 0; i < m_sortedItems.count(); ++i) { - m_items.insert(m_sortedItems.at(i).url(), i); +void KFileItemModel::setSortDirectoriesFirst(bool dirsFirst) +{ + if (dirsFirst != m_sortDirsFirst) { + m_sortDirsFirst = dirsFirst; + resortAllItems(); } +} - emit itemsRemoved(KItemRangeList() << KItemRange(index, 1)); - - // Insert the item to the new position - if (sortOrderIncreased) { - --mid; - } +bool KFileItemModel::sortDirectoriesFirst() const +{ + return m_sortDirsFirst; +} - m_sortedItems.insert(mid, removedItem); - m_data.insert(mid, removedData); - for (int i = 0; i < m_sortedItems.count(); ++i) { - m_items.insert(m_sortedItems.at(i).url(), i); +void KFileItemModel::setShowHiddenFiles(bool show) +{ + m_dirLister->setShowingDotFiles(show); + m_dirLister->emitChanges(); + if (show) { + slotCompleted(); } +} - emit itemsInserted(KItemRangeList() << KItemRange(mid, 1)); - - return true; +bool KFileItemModel::showHiddenFiles() const +{ + return m_dirLister->showingDotFiles(); } -void KFileItemModel::setSortFoldersFirst(bool foldersFirst) +void KFileItemModel::setShowDirectoriesOnly(bool enabled) { - if (foldersFirst != m_sortFoldersFirst) { - m_sortFoldersFirst = foldersFirst; - resortAllItems(); - } + m_dirLister->setDirOnlyMode(enabled); } -bool KFileItemModel::sortFoldersFirst() const +bool KFileItemModel::showDirectoriesOnly() const { - return m_sortFoldersFirst; + return m_dirLister->dirOnlyMode(); } QMimeData* KFileItemModel::createMimeData(const QSet& indexes) const @@ -265,12 +267,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; } } @@ -280,61 +282,37 @@ int KFileItemModel::indexForKeyboardSearch(const QString& text, int startFromInd bool KFileItemModel::supportsDropping(int index) const { const KFileItem item = fileItem(index); - return item.isNull() ? false : item.isDir(); + return !item.isNull() && (item.isDir() || item.isDesktopFile()); } QString KFileItemModel::roleDescription(const QByteArray& role) const { - QString descr; - - switch (roleIndex(role)) { - case NameRole: descr = i18nc("@item:intable", "Name"); break; - case SizeRole: descr = i18nc("@item:intable", "Size"); break; - case DateRole: descr = i18nc("@item:intable", "Date"); break; - case PermissionsRole: descr = i18nc("@item:intable", "Permissions"); break; - case OwnerRole: descr = i18nc("@item:intable", "Owner"); break; - case GroupRole: descr = i18nc("@item:intable", "Group"); break; - case TypeRole: descr = i18nc("@item:intable", "Type"); break; - case DestinationRole: descr = i18nc("@item:intable", "Destination"); break; - case PathRole: descr = i18nc("@item:intable", "Path"); break; - case CommentRole: descr = i18nc("@item:intable", "Comment"); break; - case TagsRole: descr = i18nc("@item:intable", "Tags"); break; - case RatingRole: descr = i18nc("@item:intable", "Rating"); break; - case NoRole: break; - case IsDirRole: break; - case IsExpandedRole: break; - case ExpansionLevelRole: break; - default: Q_ASSERT(false); break; + static QHash description; + if (description.isEmpty()) { + int count = 0; + const RoleInfoMap* map = rolesInfoMap(count); + for (int i = 0; i < count; ++i) { + description.insert(map[i].role, i18nc(map[i].roleTranslationContext, map[i].roleTranslation)); + } } - return descr; + return description.value(role); } QList > KFileItemModel::groups() const { - if (!m_data.isEmpty() && m_groups.isEmpty()) { + if (!m_itemData.isEmpty() && m_groups.isEmpty()) { #ifdef KFILEITEMMODEL_DEBUG QElapsedTimer timer; timer.start(); #endif - switch (roleIndex(sortRole())) { - case NameRole: m_groups = nameRoleGroups(); break; - 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 ExpansionLevelRole: break; - default: Q_ASSERT(false); break; + switch (typeForRole(sortRole())) { + case NameRole: m_groups = nameRoleGroups(); break; + case SizeRole: m_groups = sizeRoleGroups(); break; + case DateRole: m_groups = dateRoleGroups(); break; + case PermissionsRole: m_groups = permissionRoleGroups(); break; + case RatingRole: m_groups = ratingRoleGroups(); break; + default: m_groups = genericStringRoleGroups(sortRole()); break; } #ifdef KFILEITEMMODEL_DEBUG @@ -348,7 +326,7 @@ QList > KFileItemModel::groups() const KFileItem KFileItemModel::fileItem(int index) const { if (index >= 0 && index < count()) { - return m_sortedItems.at(index); + return m_itemData.at(index)->item; } return KFileItem(); @@ -358,7 +336,7 @@ KFileItem KFileItemModel::fileItem(const KUrl& url) const { const int index = m_items.value(url, -1); if (index >= 0) { - return m_sortedItems.at(index); + return m_itemData.at(index)->item; } return KFileItem(); } @@ -381,11 +359,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() @@ -395,11 +369,14 @@ void KFileItemModel::clear() void KFileItemModel::setRoles(const QSet& roles) { + if (m_roles == roles) { + return; + } m_roles = roles; if (count() > 0) { - const bool supportedExpanding = m_requestRole[IsExpandedRole] && m_requestRole[ExpansionLevelRole]; - const bool willSupportExpanding = roles.contains("isExpanded") && roles.contains("expansionLevel"); + const bool supportedExpanding = m_requestRole[ExpandedParentsCountRole]; + const bool willSupportExpanding = roles.contains("expandedParentsCount"); if (supportedExpanding && !willSupportExpanding) { // No expanding is supported anymore. Take care to delete all items that have an expansion level // that is not 0 (and hence are part of an expanded item). @@ -407,19 +384,20 @@ void KFileItemModel::setRoles(const QSet& roles) } } + m_groups.clear(); resetRoles(); QSetIterator it(roles); while (it.hasNext()) { const QByteArray& role = it.next(); - m_requestRole[roleIndex(role)] = true; + m_requestRole[typeForRole(role)] = true; } if (count() > 0) { // Update m_data with the changed requested roles const int maxIndex = count() - 1; for (int i = 0; i <= maxIndex; ++i) { - m_data[i] = retrieveData(m_sortedItems.at(i)); + m_itemData[i]->values = retrieveData(m_itemData.at(i)->item); } kWarning() << "TODO: Emitting itemsChanged() with no information what has changed!"; @@ -434,7 +412,7 @@ QSet KFileItemModel::roles() const bool KFileItemModel::setExpanded(int index, bool expanded) { - if (isExpanded(index) == expanded || index < 0 || index >= count()) { + if (!isExpandable(index) || isExpanded(index) == expanded) { return false; } @@ -444,36 +422,32 @@ bool KFileItemModel::setExpanded(int index, bool expanded) return false; } - const KUrl url = m_sortedItems.at(index).url(); + const KUrl url = m_itemData.at(index)->item.url(); if (expanded) { - m_expandedUrls.insert(url); - - KDirLister* dirLister = m_dirLister.data(); - 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); + KFileItemList itemsToRemove; - const int expansionLevel = data(index)["expansionLevel"].toInt(); + const int expandedParentsCount = data(index)["expandedParentsCount"].toInt(); ++index; - while (index < count() && data(index)["expansionLevel"].toInt() > expansionLevel) { - itemsToRemove.append(m_sortedItems.at(index)); + while (index < count() && data(index)["expandedParentsCount"].toInt() > expandedParentsCount) { + itemsToRemove.append(m_itemData.at(index)->item); ++index; } removeItems(itemsToRemove); - return true; } - return false; + return true; } bool KFileItemModel::isExpanded(int index) const { if (index >= 0 && index < count()) { - return m_data.at(index).value("isExpanded").toBool(); + return m_itemData.at(index)->values.value("isExpanded").toBool(); } return false; } @@ -481,19 +455,151 @@ bool KFileItemModel::isExpanded(int index) const bool KFileItemModel::isExpandable(int index) const { if (index >= 0 && index < count()) { - return m_sortedItems.at(index).isDir(); + return m_itemData.at(index)->values.value("isExpandable").toBool(); } return false; } -QSet KFileItemModel::expandedUrls() const +int KFileItemModel::expandedParentsCount(int index) const +{ + if (index >= 0 && index < count()) { + const int parentsCount = m_itemData.at(index)->values.value("expandedParentsCount").toInt(); + if (parentsCount > 0) { + return parentsCount; + } + } + return 0; +} + +QSet KFileItemModel::expandedDirectories() const +{ + return m_expandedDirs; +} + +void KFileItemModel::restoreExpandedDirectories(const QSet& urls) +{ + m_urlsToExpand = urls; +} + +void KFileItemModel::expandParentDirectories(const KUrl& url) +{ + 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 = 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)); + m_urlsToExpand.insert(urlToExpand); + } + + // KDirLister::open() must called at least once to trigger an initial + // loading. The pending URLs that must be restored are handled + // in slotCompleted(). + QSetIterator it2(m_urlsToExpand); + while (it2.hasNext()) { + const int idx = index(it2.next()); + if (idx >= 0 && !isExpanded(idx)) { + setExpanded(idx, true); + break; + } + } +} + +void KFileItemModel::setNameFilter(const QString& nameFilter) +{ + if (m_filter.pattern() != nameFilter) { + dispatchPendingItemsToInsert(); + m_filter.setPattern(nameFilter); + applyFilters(); + } +} + +QString KFileItemModel::nameFilter() const +{ + return m_filter.pattern(); +} + +void KFileItemModel::setMimeTypeFilters(const QStringList& filters) { - return m_expandedUrls; + if (m_filter.mimeTypes() != filters) { + dispatchPendingItemsToInsert(); + m_filter.setMimeTypes(filters); + applyFilters(); + } } -void KFileItemModel::restoreExpandedUrls(const QSet& urls) +QStringList KFileItemModel::mimeTypeFilters() const { - m_restoredExpandedUrls = urls; + return m_filter.mimeTypes(); +} + + +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()) { + if (!m_filter.matches(itemData->item)) { + newFilteredItems.append(itemData->item); + m_filteredItems.insert(itemData->item); + } + } + } + + removeItems(newFilteredItems); + + // 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); + it.remove(); + } + } + + insertItems(newVisibleItems); +} + +QList KFileItemModel::rolesInformation() +{ + static QList rolesInfo; + if (rolesInfo.isEmpty()) { + int count = 0; + const RoleInfoMap* map = rolesInfoMap(count); + for (int i = 0; i < count; ++i) { + if (map[i].roleType != NoRole) { + RoleInfo info; + info.role = map[i].role; + info.translation = i18nc(map[i].roleTranslationContext, map[i].roleTranslation); + if (map[i].groupTranslation) { + info.group = i18nc(map[i].groupTranslationContext, map[i].groupTranslation); + } 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); + } + } + } + + return rolesInfo; } void KFileItemModel::onGroupedSortingChanged(bool current) @@ -505,7 +611,7 @@ void KFileItemModel::onGroupedSortingChanged(bool current) void KFileItemModel::onSortRoleChanged(const QByteArray& current, const QByteArray& previous) { Q_UNUSED(previous); - m_sortRole = roleIndex(current); + m_sortRole = typeForRole(current); #ifdef KFILEITEMMODEL_DEBUG if (!m_requestRole[m_sortRole]) { @@ -523,53 +629,146 @@ void KFileItemModel::onSortOrderChanged(Qt::SortOrder current, Qt::SortOrder pre resortAllItems(); } -void KFileItemModel::slotCompleted() +void KFileItemModel::resortAllItems() { - if (m_restoredExpandedUrls.isEmpty() && m_minimumUpdateIntervalTimer->isActive()) { - // dispatchPendingItems() will be called when the timer - // has been expired. - m_pendingEmitLoadingCompleted = true; + m_resortAllItemsTimer->stop(); + + const int itemCount = count(); + if (itemCount <= 0) { return; } - m_pendingEmitLoadingCompleted = false; +#ifdef KFILEITEMMODEL_DEBUG + QElapsedTimer timer; + timer.start(); + kDebug() << "==========================================================="; + kDebug() << "Resorting" << itemCount << "items"; +#endif + + // Remember the order of the current URLs so + // that it can be determined which indexes have + // been moved because of the resorting. + QList oldUrls; + oldUrls.reserve(itemCount); + foreach (const ItemData* itemData, m_itemData) { + oldUrls.append(itemData->item.url()); + } + + m_groups.clear(); + m_items.clear(); + + // Resort the items + KFileItemModelSortAlgorithm::sort(this, m_itemData.begin(), m_itemData.end()); + for (int i = 0; i < itemCount; ++i) { + m_items.insert(m_itemData.at(i)->item.url(), i); + } + + // Determine the indexes that have been moved + QList movedToIndexes; + movedToIndexes.reserve(itemCount); + for (int i = 0; i < itemCount; i++) { + const int newIndex = m_items.value(oldUrls.at(i).url()); + movedToIndexes.append(newIndex); + } + + // Don't check whether items have really been moved and always emit a + // itemsMoved() signal after resorting: In case of grouped items + // the groups might change even if the items themselves don't change their + // position. Let the receiver of the signal decide whether a check for moved + // items makes sense. + emit itemsMoved(KItemRange(0, itemCount), movedToIndexes); + +#ifdef KFILEITEMMODEL_DEBUG + kDebug() << "[TIME] Resorting of" << itemCount << "items:" << timer.elapsed(); +#endif +} + +void KFileItemModel::slotCompleted() +{ dispatchPendingItemsToInsert(); - if (!m_restoredExpandedUrls.isEmpty()) { + if (!m_urlsToExpand.isEmpty()) { // Try to find a URL that can be expanded. // 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_restoredExpandedUrls) { + foreach (const KUrl& url, m_urlsToExpand) { const int index = m_items.value(url, -1); if (index >= 0) { - // We have found an expandable URL. Expand it and return - when - // the dir lister has finished, this slot will be called again. - m_restoredExpandedUrls.remove(url); - setExpanded(index, true); - return; + m_urlsToExpand.remove(url); + if (setExpanded(index, true)) { + // The dir lister has been triggered. This slot will be called + // again after the directory has been expanded. + return; + } } } // None of the URLs in m_restoredExpandedUrls could be found in the model. This can happen // if these URLs have been deleted in the meantime. - m_restoredExpandedUrls.clear(); + m_urlsToExpand.clear(); } - emit loadingCompleted(); - m_minimumUpdateIntervalTimer->start(); + emit directoryLoadingCompleted(); } void KFileItemModel::slotCanceled() { - m_minimumUpdateIntervalTimer->stop(); m_maximumUpdateIntervalTimer->stop(); dispatchPendingItemsToInsert(); } void KFileItemModel::slotNewItems(const KFileItemList& items) { - m_pendingItemsToInsert.append(items); + Q_ASSERT(!items.isEmpty()); + + if (m_requestRole[ExpandedParentsCountRole] && m_expandedParentsCountRoot >= 0) { + // 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(); + + KFileItem item = items.first(); + + // If the expanding of items is enabled, the call + // dirLister->openUrl(url, KDirLister::Keep) in KFileItemModel::setExpanded() + // might result in emitting the same items twice due to the Keep-parameter. + // This case happens if an item gets expanded, collapsed and expanded again + // before the items could be loaded for the first expansion. + const int index = m_items.value(item.url(), -1); + if (index >= 0) { + // The items are already part of the model. + return; + } + + // 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. + return; + } + } + + if (!m_filter.hasSetFilters()) { + m_pendingItemsToInsert.append(items); + } 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); + } else { + m_filteredItems.insert(item); + } + } + + m_pendingItemsToInsert.append(filteredItems); + } if (useMaximumUpdateInterval() && !m_maximumUpdateIntervalTimer->isActive()) { // Assure that items get dispatched if no completed() or canceled() signal is @@ -580,11 +779,23 @@ void KFileItemModel::slotNewItems(const KFileItemList& items) void KFileItemModel::slotItemsDeleted(const KFileItemList& items) { - if (!m_pendingItemsToInsert.isEmpty()) { - insertItems(m_pendingItemsToInsert); - m_pendingItemsToInsert.clear(); + dispatchPendingItemsToInsert(); + + KFileItemList itemsToRemove = items; + if (m_requestRole[ExpandedParentsCountRole] && m_expandedParentsCountRoot >= 0) { + // Assure that removing a parent item also results in removing all children + foreach (const KFileItem& item, items) { + itemsToRemove.append(childItems(item)); + } } - removeItems(items); + + if (!m_filteredItems.isEmpty()) { + foreach (const KFileItem& item, itemsToRemove) { + m_filteredItems.remove(item); + } + } + + removeItems(itemsToRemove); } void KFileItemModel::slotRefreshItems(const QList >& items) @@ -603,8 +814,22 @@ void KFileItemModel::slotRefreshItems(const QList >& QListIterator > it(items); while (it.hasNext()) { const QPair& itemPair = it.next(); - const int index = m_items.value(itemPair.second.url(), -1); + const KFileItem& oldItem = itemPair.first; + const KFileItem& newItem = itemPair.second; + const int index = m_items.value(oldItem.url(), -1); if (index >= 0) { + m_itemData[index]->item = newItem; + + // Keep old values as long as possible if they could not retrieved synchronously yet. + // The update of the values will be done asynchronously by KFileItemModelRolesUpdater. + QHashIterator it(retrieveData(newItem)); + 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); } } @@ -619,9 +844,9 @@ void KFileItemModel::slotRefreshItems(const QList >& qSort(indexes); KItemRangeList itemRangeList; - int rangeIndex = 0; - int rangeCount = 1; int previousIndex = indexes.at(0); + int rangeIndex = previousIndex; + int rangeCount = 1; const int maxIndex = indexes.count() - 1; for (int i = 1; i <= maxIndex; ++i) { @@ -641,7 +866,9 @@ void KFileItemModel::slotRefreshItems(const QList >& itemRangeList.append(KItemRange(rangeIndex, rangeCount)); } - emit itemsChanged(itemRangeList, QSet()); + emit itemsChanged(itemRangeList, m_roles); + + resortAllItems(); } void KFileItemModel::slotClear() @@ -650,23 +877,24 @@ void KFileItemModel::slotClear() kDebug() << "Clearing all items"; #endif + m_filteredItems.clear(); m_groups.clear(); - m_minimumUpdateIntervalTimer->stop(); m_maximumUpdateIntervalTimer->stop(); + m_resortAllItemsTimer->stop(); m_pendingItemsToInsert.clear(); - m_rootExpansionLevel = -1; + m_expandedParentsCountRoot = UninitializedExpandedParentsCountRoot; - const int removedCount = m_data.count(); + const int removedCount = m_itemData.count(); if (removedCount > 0) { - m_sortedItems.clear(); + qDeleteAll(m_itemData); + m_itemData.clear(); m_items.clear(); - m_data.clear(); emit itemsRemoved(KItemRangeList() << KItemRange(0, removedCount)); } - m_expandedUrls.clear(); + m_expandedDirs.clear(); } void KFileItemModel::slotClear(const KUrl& url) @@ -674,16 +902,18 @@ void KFileItemModel::slotClear(const KUrl& url) Q_UNUSED(url); } +void KFileItemModel::slotNaturalSortingChanged() +{ + m_naturalSorting = KGlobalSettings::naturalSorting(); + resortAllItems(); +} + void KFileItemModel::dispatchPendingItemsToInsert() { if (!m_pendingItemsToInsert.isEmpty()) { insertItems(m_pendingItemsToInsert); m_pendingItemsToInsert.clear(); } - - if (m_pendingEmitLoadingCompleted) { - emit loadingCompleted(); - } } void KFileItemModel::insertItems(const KFileItemList& items) @@ -692,6 +922,13 @@ void KFileItemModel::insertItems(const KFileItemList& items) 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(); @@ -701,8 +938,8 @@ void KFileItemModel::insertItems(const KFileItemList& items) m_groups.clear(); - KFileItemList sortedItems = items; - sort(sortedItems.begin(), sortedItems.end()); + QList sortedItems = createItemDataList(items); + KFileItemModelSortAlgorithm::sort(this, sortedItems.begin(), sortedItems.end()); #ifdef KFILEITEMMODEL_DEBUG kDebug() << "[TIME] Sorting:" << timer.elapsed(); @@ -711,15 +948,15 @@ void KFileItemModel::insertItems(const KFileItemList& items) 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 + 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_sortedItems.count()) { - if (!lessThan(m_sortedItems.at(targetIndex), sortedItems.at(sourceIndex))) { + while (targetIndex < m_itemData.count()) { + if (!lessThan(m_itemData.at(targetIndex), sortedItems.at(sourceIndex))) { break; } ++targetIndex; @@ -732,11 +969,10 @@ void KFileItemModel::insertItems(const KFileItemList& items) insertedCount = 0; } - // Insert item at the position targetIndex - const KFileItem item = sortedItems.at(sourceIndex); - m_sortedItems.insert(targetIndex, item); - m_data.insert(targetIndex, retrieveData(item)); + // Insert item at the position targetIndex by transferring + // the ownership of the item-data from sortedItems to m_itemData. // m_items will be inserted after the loop (see comment below) + m_itemData.insert(targetIndex, sortedItems.at(sourceIndex)); ++insertedCount; if (insertedAtIndex < 0) { @@ -749,8 +985,9 @@ void KFileItemModel::insertItems(const KFileItemList& items) // The indexes of all m_items must be adjusted, not only the index // of the new items - for (int i = 0; i < m_sortedItems.count(); ++i) { - m_items.insert(m_sortedItems.at(i).url(), i); + const int itemDataCount = m_itemData.count(); + for (int i = 0; i < itemDataCount; ++i) { + m_items.insert(m_itemData.at(i)->item.url(), i); } itemRanges << KItemRange(insertedAtIndex, insertedCount); @@ -773,8 +1010,15 @@ void KFileItemModel::removeItems(const KFileItemList& items) m_groups.clear(); - KFileItemList sortedItems = items; - sort(sortedItems.begin(), sortedItems.end()); + QList 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()); QList indexesToRemove; indexesToRemove.reserve(items.count()); @@ -784,15 +1028,17 @@ void KFileItemModel::removeItems(const KFileItemList& items) int removedAtIndex = -1; int removedCount = 0; int targetIndex = 0; - foreach (const KFileItem& itemToRemove, sortedItems) { + foreach (const ItemData* itemData, sortedItems) { + const KFileItem& itemToRemove = itemData->item; + const int previousTargetIndex = targetIndex; - while (targetIndex < m_sortedItems.count()) { - if (m_sortedItems.at(targetIndex).url() == itemToRemove.url()) { + while (targetIndex < m_itemData.count()) { + if (m_itemData.at(targetIndex)->item.url() == itemToRemove.url()) { break; } ++targetIndex; } - if (targetIndex >= m_sortedItems.count()) { + if (targetIndex >= m_itemData.count()) { kWarning() << "Item that should be deleted has not been found!"; return; } @@ -814,86 +1060,77 @@ void KFileItemModel::removeItems(const KFileItemList& items) // Delete the items for (int i = indexesToRemove.count() - 1; i >= 0; --i) { const int indexToRemove = indexesToRemove.at(i); - m_items.remove(m_sortedItems.at(indexToRemove).url()); - m_sortedItems.removeAt(indexToRemove); - m_data.removeAt(indexToRemove); + ItemData* data = m_itemData.at(indexToRemove); + + m_items.remove(data->item.url()); + + delete data; + m_itemData.removeAt(indexToRemove); } // The indexes of all m_items must be adjusted, not only the index // of the removed items - for (int i = 0; i < m_sortedItems.count(); ++i) { - m_items.insert(m_sortedItems.at(i).url(), i); + const int itemDataCount = m_itemData.count(); + for (int i = 0; i < itemDataCount; ++i) { + m_items.insert(m_itemData.at(i)->item.url(), i); } if (count() <= 0) { - m_rootExpansionLevel = -1; + m_expandedParentsCountRoot = UninitializedExpandedParentsCountRoot; } itemRanges << KItemRange(removedAtIndex, removedCount); emit itemsRemoved(itemRanges); } -void KFileItemModel::resortAllItems() +QList KFileItemModel::createItemDataList(const KFileItemList& items) const { - const int itemCount = count(); - if (itemCount <= 0) { - return; - } - - const KFileItemList oldSortedItems = m_sortedItems; - const QHash oldItems = m_items; - const QList > oldData = m_data; - - m_groups.clear(); - m_items.clear(); - m_data.clear(); - - sort(m_sortedItems.begin(), m_sortedItems.end()); - int index = 0; - foreach (const KFileItem& item, m_sortedItems) { - m_items.insert(item.url(), index); - - const int oldItemIndex = oldItems.value(item.url()); - m_data.append(oldData.at(oldItemIndex)); - - ++index; - } - - bool emitItemsMoved = false; - QList movedToIndexes; - movedToIndexes.reserve(m_sortedItems.count()); - for (int i = 0; i < itemCount; i++) { - const int newIndex = m_items.value(oldSortedItems.at(i).url()); - movedToIndexes.append(newIndex); - if (!emitItemsMoved && newIndex != i) { - emitItemsMoved = true; + 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(); + } } - } - if (emitItemsMoved) { - emit itemsMoved(KItemRange(0, itemCount), movedToIndexes); + itemDataList.append(itemData); } + + return itemDataList; } void KFileItemModel::removeExpandedItems() { KFileItemList expandedItems; - const int maxIndex = m_data.count() - 1; + const int maxIndex = m_itemData.count() - 1; for (int i = 0; i <= maxIndex; ++i) { - if (m_data.at(i).value("expansionLevel").toInt() > 0) { - const KFileItem fileItem = m_sortedItems.at(i); - expandedItems.append(fileItem); + const ItemData* itemData = m_itemData.at(i); + if (itemData->values.value("expandedParentsCount").toInt() > 0) { + expandedItems.append(itemData->item); } } - // The m_rootExpansionLevel may not get reset before all items with - // a bigger expansionLevel have been removed. - Q_ASSERT(m_rootExpansionLevel >= 0); + // The m_expandedParentsCountRoot may not get reset before all items with + // a bigger count have been removed. removeItems(expandedItems); - m_rootExpansionLevel = -1; - m_expandedUrls.clear(); + m_expandedParentsCountRoot = UninitializedExpandedParentsCountRoot; + m_expandedDirs.clear(); } void KFileItemModel::resetRoles() @@ -903,27 +1140,56 @@ void KFileItemModel::resetRoles() } } -KFileItemModel::Role KFileItemModel::roleIndex(const QByteArray& role) const +KFileItemModel::RoleType KFileItemModel::typeForRole(const QByteArray& role) const { - static QHash rolesHash; - if (rolesHash.isEmpty()) { - rolesHash.insert("name", NameRole); - rolesHash.insert("size", SizeRole); - rolesHash.insert("date", DateRole); - rolesHash.insert("permissions", PermissionsRole); - rolesHash.insert("owner", OwnerRole); - rolesHash.insert("group", GroupRole); - rolesHash.insert("type", TypeRole); - rolesHash.insert("destination", DestinationRole); - rolesHash.insert("path", PathRole); - rolesHash.insert("comment", CommentRole); - rolesHash.insert("tags", TagsRole); - rolesHash.insert("rating", RatingRole); - rolesHash.insert("isDir", IsDirRole); - rolesHash.insert("isExpanded", IsExpandedRole); - rolesHash.insert("expansionLevel", ExpansionLevelRole); + static QHash roles; + if (roles.isEmpty()) { + // Insert user visible roles that can be accessed with + // KFileItemModel::roleInformation() + int count = 0; + const RoleInfoMap* map = rolesInfoMap(count); + for (int i = 0; i < count; ++i) { + roles.insert(map[i].role, map[i].roleType); + } + + // 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); + + Q_ASSERT(roles.count() == RolesCount); } - return rolesHash.value(role, NoRole); + + return roles.value(role, NoRole); +} + +QByteArray KFileItemModel::roleForType(RoleType roleType) const +{ + static QHash roles; + if (roles.isEmpty()) { + // Insert user visible roles that can be accessed with + // KFileItemModel::roleInformation() + int count = 0; + const RoleInfoMap* map = rolesInfoMap(count); + for (int i = 0; i < count; ++i) { + roles.insert(map[i].roleType, map[i].role); + } + + // 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"); + + Q_ASSERT(roles.count() == RolesCount); + }; + + return roles.value(roleType); } QHash KFileItemModel::retrieveData(const KFileItem& item) const @@ -932,15 +1198,20 @@ QHash KFileItemModel::retrieveData(const KFileItem& item) // 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(); if (m_requestRole[IsDirRole]) { 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.name()); + data.insert("text", item.text()); } if (m_requestRole[SizeRole]) { @@ -974,30 +1245,65 @@ 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); } if (m_requestRole[PathRole]) { - data.insert("path", item.localPath()); + QString path; + 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()); + path = path.mid(0, index - 1); + data.insert("path", path); } if (m_requestRole[IsExpandedRole]) { data.insert("isExpanded", false); } - if (m_requestRole[ExpansionLevelRole]) { - if (m_rootExpansionLevel < 0) { - KDirLister* dirLister = m_dirLister.data(); - if (dirLister) { - const QString rootDir = dirLister->url().directory(KUrl::AppendTrailingSlash); - m_rootExpansionLevel = rootDir.count('/'); + if (m_requestRole[IsExpandableRole]) { + data.insert("isExpandable", item.isDir() && item.url() == item.targetUrl()); + } + + if (m_requestRole[ExpandedParentsCountRole]) { + if (m_expandedParentsCountRoot == UninitializedExpandedParentsCountRoot) { + const KUrl rootUrl = m_dirLister->url(); + const QString protocol = rootUrl.protocol(); + const bool forceExpandedParentsCountRoot = (protocol == QLatin1String("trash") || + protocol == QLatin1String("nepomuk") || + protocol == QLatin1String("remote") || + protocol.contains(QLatin1String("search"))); + if (forceExpandedParentsCountRoot) { + m_expandedParentsCountRoot = ForceExpandedParentsCountRoot; + } else { + const QString rootDir = rootUrl.path(KUrl::AddTrailingSlash); + m_expandedParentsCountRoot = rootDir.count('/'); } } - const QString dir = item.url().directory(KUrl::AppendTrailingSlash); - const int level = dir.count('/') - m_rootExpansionLevel - 1; - data.insert("expansionLevel", level); + + 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); + } } if (item.isMimeTypeKnown()) { @@ -1011,21 +1317,21 @@ QHash KFileItemModel::retrieveData(const KFileItem& item) return data; } -bool KFileItemModel::lessThan(const KFileItem& a, const KFileItem& b) const +bool KFileItemModel::lessThan(const ItemData* a, const ItemData* b) const { int result = 0; - if (m_rootExpansionLevel >= 0) { - result = expansionLevelsCompare(a, b); + 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 (m_sortFoldersFirst || m_sortRole == SizeRole) { - const bool isDirA = a.isDir(); - const bool isDirB = b.isDir(); + if (m_sortDirsFirst || m_sortRole == SizeRole) { + const bool isDirA = a->item.isDir(); + const bool isDirB = b->item.isDir(); if (isDirA && !isDirB) { return true; } else if (!isDirA && isDirB) { @@ -1033,20 +1339,58 @@ bool KFileItemModel::lessThan(const KFileItem& a, const KFileItem& b) const } } + result = sortRoleCompare(a, b); + + return (sortOrder() == Qt::AscendingOrder) ? result < 0 : result > 0; +} + +int KFileItemModel::sortRoleCompare(const ItemData* a, const ItemData* b) const +{ + const KFileItem& itemA = a->item; + const KFileItem& itemB = b->item; + + int result = 0; + switch (m_sortRole) { - case NameRole: { - result = stringCompare(a.text(), b.text()); - if (result == 0) { - // KFileItem::text() may not be unique in case UDS_DISPLAY_NAME is used - result = stringCompare(a.name(m_caseSensitivity == Qt::CaseInsensitive), - b.name(m_caseSensitivity == Qt::CaseInsensitive)); + case NameRole: + // The name role is handled as default fallback after the switch + break; + + case SizeRole: { + if (itemA.isDir()) { + // See "if (m_sortFoldersFirst || m_sortRole == SizeRole)" in KFileItemModel::lessThan(): + Q_ASSERT(itemB.isDir()); + + const QVariant valueA = a->values.value("size"); + const QVariant valueB = b->values.value("size"); + if (valueA.isNull() && valueB.isNull()) { + result = 0; + } else if (valueA.isNull()) { + result = -1; + } else if (valueB.isNull()) { + result = +1; + } else { + result = valueA.toInt() - valueB.toInt(); + } + } else { + // See "if (m_sortFoldersFirst || m_sortRole == SizeRole)" in KFileItemModel::lessThan(): + Q_ASSERT(!itemB.isDir()); + const KIO::filesize_t sizeA = itemA.size(); + const KIO::filesize_t sizeB = itemB.size(); + if (sizeA > sizeB) { + result = +1; + } else if (sizeA < sizeB) { + result = -1; + } else { + result = 0; + } } break; } case DateRole: { - const KDateTime dateTimeA = a.time(KFileItem::ModificationTime); - const KDateTime dateTimeB = b.time(KFileItem::ModificationTime); + const KDateTime dateTimeA = itemA.time(KFileItem::ModificationTime); + const KDateTime dateTimeB = itemB.time(KFileItem::ModificationTime); if (dateTimeA < dateTimeB) { result = -1; } else if (dateTimeA > dateTimeB) { @@ -1055,112 +1399,51 @@ bool KFileItemModel::lessThan(const KFileItem& a, const KFileItem& b) const break; } - case SizeRole: { - const KIO::filesize_t sizeA = a.size(); - const KIO::filesize_t sizeB = b.size(); - if (sizeA < sizeB) { - result = -1; - } else if (sizeA > sizeB) { - result = +1; - } + case RatingRole: { + result = a->values.value("rating").toInt() - b->values.value("rating").toInt(); break; } - case TypeRole: { - // Only compare the type if the MIME-type is known for performance reasons. - // If the MIME-type is unknown it will be resolved later by KFileItemModelRolesUpdater - // and a resorting will be triggered. - if (a.isMimeTypeKnown() && b.isMimeTypeKnown()) { - result = QString::compare(a.mimeComment(), b.mimeComment()); - } + 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; } - case RatingRole: { - const int indexA = m_items.value(a.url(), -1); - const int indexB = m_items.value(b.url(), -1); - result = m_data.value(indexA).value("rating").toInt() - m_data.value(indexB).value("rating").toInt(); + 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) { - // It must be assured that the sort order is always unique even if two values have been - // equal. In this case a comparison of the URL is done which is unique in all cases - // within KDirLister. - result = QString::compare(a.url().url(), b.url().url(), Qt::CaseSensitive); + if (result != 0) { + // The current sort role was sufficient to define an order + return result; } - return (sortOrder() == Qt::AscendingOrder) ? result < 0 : result > 0; -} - -void KFileItemModel::sort(const KFileItemList::iterator& startIterator, const KFileItemList::iterator& endIterator) -{ - KFileItemList::iterator start = startIterator; - KFileItemList::iterator end = endIterator; - - // The implementation is based on qSortHelper() from qalgorithms.h - // Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). - // In opposite to qSort() it allows to use a member-function for the comparison of elements. - while (1) { - int span = int(end - start); - if (span < 2) { - return; - } - - --end; - KFileItemList::iterator low = start, high = end - 1; - KFileItemList::iterator pivot = start + span / 2; - - if (lessThan(*end, *start)) { - qSwap(*end, *start); - } - if (span == 2) { - return; - } - - if (lessThan(*pivot, *start)) { - qSwap(*pivot, *start); - } - if (lessThan(*end, *pivot)) { - qSwap(*end, *pivot); - } - if (span == 3) { - return; - } - - qSwap(*pivot, *end); - - while (low < high) { - while (low < high && lessThan(*low, *end)) { - ++low; - } - - while (high > low && lessThan(*end, *high)) { - --high; - } - if (low < high) { - qSwap(*low, *high); - ++low; - --high; - } else { - break; - } - } - - if (lessThan(*low, *end)) { - ++low; - } - - qSwap(*end, *low); - sort(start, low); + // Fallback #1: Compare the text of the items + result = stringCompare(itemA.text(), itemB.text()); + if (result != 0) { + return result; + } - start = low + 1; - ++end; + // Fallback #2: KFileItem::text() may not be unique in case UDS_DISPLAY_NAME is used + result = stringCompare(itemA.name(m_caseSensitivity == Qt::CaseInsensitive), + itemB.name(m_caseSensitivity == Qt::CaseInsensitive)); + if (result != 0) { + return result; } + + // Fallback #3: It must be assured that the sort order is always unique even if two values have been + // equal. In this case a comparison of the URL is done which is unique in all cases + // within KDirLister. + return QString::compare(itemA.url().url(), itemB.url().url(), Qt::CaseSensitive); } int KFileItemModel::stringCompare(const QString& a, const QString& b) const @@ -1185,10 +1468,10 @@ int KFileItemModel::stringCompare(const QString& a, const QString& b) const : QString::compare(a, b, Qt::CaseSensitive); } -int KFileItemModel::expansionLevelsCompare(const KFileItem& a, const KFileItem& b) const +int KFileItemModel::expandedParentsCountCompare(const ItemData* a, const ItemData* b) const { - const KUrl urlA = a.url(); - const KUrl urlB = b.url(); + 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; @@ -1196,9 +1479,9 @@ int KFileItemModel::expansionLevelsCompare(const KFileItem& a, const KFileItem& // Check whether one item is the parent of the other item if (urlA.isParentOf(urlB)) { - return -1; + return (sortOrder() == Qt::AscendingOrder) ? -1 : +1; } else if (urlB.isParentOf(urlA)) { - return +1; + return (sortOrder() == Qt::AscendingOrder) ? +1 : -1; } // Determine the maximum common path of both items and @@ -1221,17 +1504,39 @@ int KFileItemModel::expansionLevelsCompare(const KFileItem& a, const KFileItem& // 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, pathA, index, &isDirA); + const QString subPathA = subPath(a->item, pathA, index, &isDirA); bool isDirB = true; - const QString subPathB = subPath(b, pathB, index, &isDirB); + const QString subPathB = subPath(b->item, pathB, index, &isDirB); - if (isDirA && !isDirB) { - return -1; - } else if (!isDirA && isDirB) { - return +1; + 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; } - return stringCompare(subPathA, subPathB); + 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, @@ -1247,13 +1552,12 @@ QString KFileItemModel::subPath(const KFileItem& item, bool KFileItemModel::useMaximumUpdateInterval() const { - const KDirLister* dirLister = m_dirLister.data(); - return dirLister && !dirLister->url().isLocalFile(); + return !m_dirLister->url().isLocalFile(); } QList > KFileItemModel::nameRoleGroups() const { - Q_ASSERT(!m_data.isEmpty()); + Q_ASSERT(!m_itemData.isEmpty()); const int maxIndex = count() - 1; QList > groups; @@ -1266,12 +1570,12 @@ QList > KFileItemModel::nameRoleGroups() const continue; } - const QString name = m_data.at(i).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(); if (newFirstChar == QLatin1Char('~') && name.length() > 1) { - newFirstChar = name.at(1); + newFirstChar = name.at(1).toUpper(); } if (firstChar != newFirstChar) { @@ -1316,7 +1620,7 @@ QList > KFileItemModel::nameRoleGroups() const QList > KFileItemModel::sizeRoleGroups() const { - Q_ASSERT(!m_data.isEmpty()); + Q_ASSERT(!m_itemData.isEmpty()); const int maxIndex = count() - 1; QList > groups; @@ -1327,7 +1631,7 @@ QList > KFileItemModel::sizeRoleGroups() const continue; } - const KFileItem& item = m_sortedItems.at(i); + const KFileItem& item = m_itemData.at(i)->item; const KIO::filesize_t fileSize = !item.isNull() ? item.size() : ~0U; QString newGroupValue; if (!item.isNull() && item.isDir()) { @@ -1351,7 +1655,7 @@ QList > KFileItemModel::sizeRoleGroups() const QList > KFileItemModel::dateRoleGroups() const { - Q_ASSERT(!m_data.isEmpty()); + Q_ASSERT(!m_itemData.isEmpty()); const int maxIndex = count() - 1; QList > groups; @@ -1371,7 +1675,7 @@ QList > KFileItemModel::dateRoleGroups() const continue; } - const KDateTime modifiedTime = m_sortedItems.at(i).time(KFileItem::ModificationTime); + const KDateTime modifiedTime = m_itemData.at(i)->item.time(KFileItem::ModificationTime); const QDate modifiedDate = modifiedTime.date(); if (modifiedDate == previousModifiedDate) { // The current item is in the same group as the previous item @@ -1450,7 +1754,7 @@ QList > KFileItemModel::dateRoleGroups() const QList > KFileItemModel::permissionRoleGroups() const { - Q_ASSERT(!m_data.isEmpty()); + Q_ASSERT(!m_itemData.isEmpty()); const int maxIndex = count() - 1; QList > groups; @@ -1462,13 +1766,14 @@ QList > KFileItemModel::permissionRoleGroups() const continue; } - const QString newPermissionsString = m_data.at(i).value("permissions").toString(); + const ItemData* itemData = m_itemData.at(i); + const QString newPermissionsString = itemData->values.value("permissions").toString(); if (newPermissionsString == permissionsString) { continue; } permissionsString = newPermissionsString; - const QFileInfo info(m_sortedItems.at(i).url().pathOrUrl()); + const QFileInfo info(itemData->item.url().pathOrUrl()); // Set user string QString user; @@ -1521,17 +1826,17 @@ QList > KFileItemModel::permissionRoleGroups() const QList > KFileItemModel::ratingRoleGroups() const { - Q_ASSERT(!m_data.isEmpty()); + Q_ASSERT(!m_itemData.isEmpty()); const int maxIndex = count() - 1; QList > groups; - int groupValue; + int groupValue = -1; for (int i = 0; i <= maxIndex; ++i) { if (isChildItem(i)) { continue; } - const int newGroupValue = m_data.at(i).value("rating").toInt(); + const int newGroupValue = m_itemData.at(i)->values.value("rating", 0).toInt(); if (newGroupValue != groupValue) { groupValue = newGroupValue; groups.append(QPair(i, newGroupValue)); @@ -1543,24 +1848,117 @@ QList > KFileItemModel::ratingRoleGroups() const QList > KFileItemModel::genericStringRoleGroups(const QByteArray& role) const { - Q_ASSERT(!m_data.isEmpty()); + Q_ASSERT(!m_itemData.isEmpty()); const int maxIndex = count() - 1; QList > groups; + bool isFirstGroupValue = true; QString groupValue; for (int i = 0; i <= maxIndex; ++i) { if (isChildItem(i)) { continue; } - const QString newGroupValue = m_data.at(i).value(role).toString(); - if (newGroupValue != groupValue) { + const QString newGroupValue = m_itemData.at(i)->values.value(role).toString(); + if (newGroupValue != groupValue || isFirstGroupValue) { groupValue = newGroupValue; groups.append(QPair(i, newGroupValue)); + isFirstGroupValue = false; } } return groups; } +KFileItemList KFileItemModel::childItems(const KFileItem& item) const +{ + KFileItemList items; + + int index = m_items.value(item.url(), -1); + if (index >= 0) { + const int parentLevel = m_itemData.at(index)->values.value("expandedParentsCount").toInt(); + ++index; + while (index < m_itemData.count() && m_itemData.at(index)->values.value("expandedParentsCount").toInt() > parentLevel) { + items.append(m_itemData.at(index)->item); + ++index; + } + } + + 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 | 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 (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; + } + } +} + #include "kfileitemmodel.moc"