X-Git-Url: https://cloud.milkyroute.net/gitweb/dolphin.git/blobdiff_plain/dd71994b6cea7730c126f0b68351a96a0c624634..756c648f62d03749fe464e6bb0b3d3595a4ced99:/src/kitemviews/kfileitemmodel.cpp diff --git a/src/kitemviews/kfileitemmodel.cpp b/src/kitemviews/kfileitemmodel.cpp index c54a982f6..04e3c8ca7 100644 --- a/src/kitemviews/kfileitemmodel.cpp +++ b/src/kitemviews/kfileitemmodel.cpp @@ -20,35 +20,45 @@ #include "kfileitemmodel.h" #include +#include #include #include #include +#include #include #define KFILEITEMMODEL_DEBUG KFileItemModel::KFileItemModel(KDirLister* dirLister, QObject* parent) : - KItemModelBase(QByteArray(), "name", parent), + KItemModelBase("name", parent), m_dirLister(dirLister), m_naturalSorting(true), m_sortFoldersFirst(true), - m_groupRole(NoRole), m_sortRole(NameRole), + 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_pendingItemsToDelete(), - m_rootExpansionLevel(-1) + m_pendingEmitLoadingCompleted(false), + m_groups(), + m_rootExpansionLevel(UninitializedRootExpansionLevel), + m_expandedUrls(), + m_urlsToExpand() { + // Apply default roles that should be determined resetRoles(); m_requestRole[NameRole] = true; m_requestRole[IsDirRole] = true; + m_roles.insert("name"); + m_roles.insert("isDir"); Q_ASSERT(dirLister); @@ -56,6 +66,7 @@ KFileItemModel::KFileItemModel(KDirLister* dirLister, QObject* parent) : 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))); @@ -65,89 +76,276 @@ KFileItemModel::KFileItemModel(KDirLister* dirLister, QObject* parent) : m_minimumUpdateIntervalTimer = new QTimer(this); m_minimumUpdateIntervalTimer->setInterval(1000); m_minimumUpdateIntervalTimer->setSingleShot(true); - connect(m_minimumUpdateIntervalTimer, SIGNAL(timeout()), this, SLOT(dispatchPendingItems())); + connect(m_minimumUpdateIntervalTimer, SIGNAL(timeout()), this, SLOT(dispatchPendingItemsToInsert())); // 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. m_maximumUpdateIntervalTimer = new QTimer(this); m_maximumUpdateIntervalTimer->setInterval(2000); m_maximumUpdateIntervalTimer->setSingleShot(true); - connect(m_maximumUpdateIntervalTimer, SIGNAL(timeout()), this, SLOT(dispatchPendingItems())); + connect(m_maximumUpdateIntervalTimer, SIGNAL(timeout()), this, SLOT(dispatchPendingItemsToInsert())); + + // 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())); Q_ASSERT(m_minimumUpdateIntervalTimer->interval() <= m_maximumUpdateIntervalTimer->interval()); } KFileItemModel::~KFileItemModel() { + qDeleteAll(m_itemData); + m_itemData.clear(); } 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(); } bool KFileItemModel::setData(int index, const QHash& values) { - if (index >= 0 && index < count()) { - QHash currentValue = m_data.at(index); + if (index < 0 || index >= count()) { + return false; + } - QSet changedRoles; - QHashIterator it(values); - while (it.hasNext()) { - it.next(); - const QByteArray role = it.key(); - const QVariant value = it.value(); + QHash currentValues = m_itemData.at(index)->values; - if (currentValue[role] != value) { - currentValue[role] = value; - changedRoles.insert(role); - } + // Determine which roles have been changed + QSet changedRoles; + QHashIterator it(values); + while (it.hasNext()) { + it.next(); + const QByteArray role = it.key(); + const QVariant value = it.value(); + + if (currentValues[role] != value) { + currentValues[role] = value; + changedRoles.insert(role); + } + } + + if (changedRoles.isEmpty()) { + return false; + } + + m_itemData[index]->values = currentValues; + emit itemsChanged(KItemRangeList() << KItemRange(index, 1), changedRoles); + + if (changedRoles.contains(sortRole())) { + m_resortAllItemsTimer->start(); + } + + return true; +} + +void KFileItemModel::setSortFoldersFirst(bool foldersFirst) +{ + if (foldersFirst != m_sortFoldersFirst) { + m_sortFoldersFirst = foldersFirst; + resortAllItems(); + } +} + +bool KFileItemModel::sortFoldersFirst() const +{ + return m_sortFoldersFirst; +} + +void KFileItemModel::setShowHiddenFiles(bool show) +{ + KDirLister* dirLister = m_dirLister.data(); + if (dirLister) { + dirLister->setShowingDotFiles(show); + dirLister->emitChanges(); + if (show) { + slotCompleted(); } + } +} - if (!changedRoles.isEmpty()) { - m_data[index] = currentValue; - emit itemsChanged(KItemRangeList() << KItemRange(index, 1), changedRoles); +bool KFileItemModel::showHiddenFiles() const +{ + const KDirLister* dirLister = m_dirLister.data(); + return dirLister ? dirLister->showingDotFiles() : false; +} + +QMimeData* KFileItemModel::createMimeData(const QSet& indexes) const +{ + QMimeData* data = new QMimeData(); + + // The following code has been taken from KDirModel::mimeData() + // (kdelibs/kio/kio/kdirmodel.cpp) + // Copyright (C) 2006 David Faure + KUrl::List urls; + KUrl::List mostLocalUrls; + bool canUseMostLocalUrls = true; + + QSetIterator it(indexes); + while (it.hasNext()) { + const int index = it.next(); + const KFileItem item = fileItem(index); + if (!item.isNull()) { + urls << item.url(); + + bool isLocal; + mostLocalUrls << item.mostLocalUrl(isLocal); + if (!isLocal) { + canUseMostLocalUrls = false; + } } + } - return true; + const bool different = canUseMostLocalUrls && mostLocalUrls != urls; + urls = KDirModel::simplifiedUrlList(urls); // TODO: Check if we still need KDirModel for this in KDE 5.0 + if (different) { + mostLocalUrls = KDirModel::simplifiedUrlList(mostLocalUrls); + urls.populateMimeData(mostLocalUrls, data); + } else { + urls.populateMimeData(data); } - return false; + + return data; } -bool KFileItemModel::supportsGrouping() const +int KFileItemModel::indexForKeyboardSearch(const QString& text, int startFromIndex) const { - return true; + startFromIndex = qMax(0, startFromIndex); + for (int i = startFromIndex; i < count(); ++i) { + if (data(i)["name"].toString().startsWith(text, Qt::CaseInsensitive)) { + return i; + } + } + for (int i = 0; i < startFromIndex; ++i) { + if (data(i)["name"].toString().startsWith(text, Qt::CaseInsensitive)) { + return i; + } + } + return -1; } -bool KFileItemModel::supportsSorting() const +bool KFileItemModel::supportsDropping(int index) const { - return true; + const KFileItem item = fileItem(index); + return item.isNull() ? false : item.isDir(); +} + +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; + } + + return descr; +} + +QList > KFileItemModel::groups() const +{ + 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; + } + +#ifdef KFILEITEMMODEL_DEBUG + kDebug() << "[TIME] Calculating groups for" << count() << "items:" << timer.elapsed(); +#endif + } + + return m_groups; } KFileItem KFileItemModel::fileItem(int index) const { if (index >= 0 && index < count()) { - return m_sortedItems.at(index); + return m_itemData.at(index)->item; } return KFileItem(); } +KFileItem KFileItemModel::fileItem(const KUrl& url) const +{ + const int index = m_items.value(url, -1); + if (index >= 0) { + return m_itemData.at(index)->item; + } + return KFileItem(); +} + int KFileItemModel::index(const KFileItem& item) const { if (item.isNull()) { return -1; } - return m_items.value(item, -1); + return m_items.value(item.url(), -1); +} + +int KFileItemModel::index(const KUrl& url) const +{ + KUrl urlToFind = url; + urlToFind.adjustPath(KUrl::RemoveTrailingSlash); + return m_items.value(urlToFind, -1); +} + +KFileItem KFileItemModel::rootItem() const +{ + const KDirLister* dirLister = m_dirLister.data(); + if (dirLister) { + return dirLister->rootItem(); + } + return KFileItem(); } void KFileItemModel::clear() @@ -157,6 +355,8 @@ void KFileItemModel::clear() void KFileItemModel::setRoles(const QSet& roles) { + m_roles = roles; + if (count() > 0) { const bool supportedExpanding = m_requestRole[IsExpandedRole] && m_requestRole[ExpansionLevelRole]; const bool willSupportExpanding = roles.contains("isExpanded") && roles.contains("expansionLevel"); @@ -168,6 +368,7 @@ void KFileItemModel::setRoles(const QSet& roles) } resetRoles(); + QSetIterator it(roles); while (it.hasNext()) { const QByteArray& role = it.next(); @@ -178,7 +379,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_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!"; @@ -188,28 +389,7 @@ void KFileItemModel::setRoles(const QSet& roles) QSet KFileItemModel::roles() const { - QSet roles; - for (int i = 0; i < RolesCount; ++i) { - if (m_requestRole[i]) { - switch (i) { - case NoRole: break; - case NameRole: roles.insert("name"); break; - case SizeRole: roles.insert("size"); break; - case DateRole: roles.insert("date"); break; - case PermissionsRole: roles.insert("permissions"); break; - case OwnerRole: roles.insert("owner"); break; - case GroupRole: roles.insert("group"); break; - case TypeRole: roles.insert("type"); break; - case DestinationRole: roles.insert("destination"); break; - case PathRole: roles.insert("path"); break; - case IsDirRole: roles.insert("isDir"); break; - case IsExpandedRole: roles.insert("isExpanded"); break; - case ExpansionLevelRole: roles.insert("expansionLevel"); break; - default: Q_ASSERT(false); break; - } - } - } - return roles; + return m_roles; } bool KFileItemModel::setExpanded(int index, bool expanded) @@ -224,19 +404,27 @@ bool KFileItemModel::setExpanded(int index, bool expanded) return false; } + KDirLister* dirLister = m_dirLister.data(); + const KUrl url = m_itemData.at(index)->item.url(); if (expanded) { - const KUrl url = m_sortedItems.at(index).url(); - KDirLister* dirLister = m_dirLister.data(); + m_expandedUrls.insert(url); + if (dirLister) { dirLister->openUrl(url, KDirLister::Keep); return true; } } else { + m_expandedUrls.remove(url); + + if (dirLister) { + dirLister->stop(url); + } + KFileItemList itemsToRemove; const int expansionLevel = data(index)["expansionLevel"].toInt(); ++index; while (index < count() && data(index)["expansionLevel"].toInt() > expansionLevel) { - itemsToRemove.append(m_sortedItems.at(index)); + itemsToRemove.append(m_itemData.at(index)->item); ++index; } removeItems(itemsToRemove); @@ -249,7 +437,7 @@ bool KFileItemModel::setExpanded(int index, bool expanded) 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; } @@ -257,55 +445,223 @@ 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)->item.isDir(); } return false; } -void KFileItemModel::onGroupRoleChanged(const QByteArray& current, const QByteArray& previous) +QSet KFileItemModel::expandedUrls() const { - Q_UNUSED(previous); - m_groupRole = roleIndex(current); + return m_expandedUrls; +} + +void KFileItemModel::restoreExpandedUrls(const QSet& urls) +{ + m_urlsToExpand = urls; +} + +void KFileItemModel::setExpanded(const QSet& urls) +{ + + const KDirLister* dirLister = m_dirLister.data(); + if (!dirLister) { + return; + } + + const int pos = dirLister->url().url().length(); + + // Assure that each sub-path of the URLs that should be + // expanded is added to m_urlsToExpand too. KDirLister + // does not care whether the parent-URL has already been + // expanded. + QSetIterator it1(urls); + while (it1.hasNext()) { + const KUrl& url = it1.next(); + + KUrl urlToExpand = dirLister->url(); + const QStringList subDirs = url.url().mid(pos).split(QDir::separator()); + for (int i = 0; i < subDirs.count(); ++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); + + // 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); + } + } + } + + 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); + m_filteredItems.remove(item); + } + } + + insertItems(newVisibleItems); + } +} + +QString KFileItemModel::nameFilter() const +{ + return m_filter.pattern(); +} + +void KFileItemModel::onGroupedSortingChanged(bool current) +{ + Q_UNUSED(current); + m_groups.clear(); } void KFileItemModel::onSortRoleChanged(const QByteArray& current, const QByteArray& previous) { Q_UNUSED(previous); + m_sortRole = roleIndex(current); + +#ifdef KFILEITEMMODEL_DEBUG + if (!m_requestRole[m_sortRole]) { + kWarning() << "The sort-role has been changed to a role that has not been received yet"; + } +#endif + + resortAllItems(); +} + +void KFileItemModel::onSortOrderChanged(Qt::SortOrder current, Qt::SortOrder previous) +{ + Q_UNUSED(current); + Q_UNUSED(previous); + resortAllItems(); +} + +void KFileItemModel::resortAllItems() +{ + m_resortAllItemsTimer->stop(); + const int itemCount = count(); if (itemCount <= 0) { return; } - m_sortRole = roleIndex(current); +#ifdef KFILEITEMMODEL_DEBUG + QElapsedTimer timer; + timer.start(); + kDebug() << "==========================================================="; + kDebug() << "Resorting" << itemCount << "items"; +#endif - KFileItemList sortedItems = m_sortedItems; - m_sortedItems.clear(); + // 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(); - m_data.clear(); - emit itemsRemoved(KItemRangeList() << KItemRange(0, itemCount)); - - sort(sortedItems.begin(), sortedItems.end()); - int index = 0; - foreach (const KFileItem& item, sortedItems) { - m_sortedItems.append(item); - m_items.insert(item, index); - m_data.append(retrieveData(item)); - - ++index; + + // Resort the items + sort(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 + bool emitItemsMoved = false; + 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); + if (!emitItemsMoved && newIndex != i) { + emitItemsMoved = true; + } + } - emit itemsInserted(KItemRangeList() << KItemRange(0, itemCount)); + if (emitItemsMoved) { + emit itemsMoved(KItemRange(0, itemCount), movedToIndexes); + } + +#ifdef KFILEITEMMODEL_DEBUG + kDebug() << "[TIME] Resorting of" << itemCount << "items:" << timer.elapsed(); +#endif } void KFileItemModel::slotCompleted() { - if (m_minimumUpdateIntervalTimer->isActive()) { + if (m_urlsToExpand.isEmpty() && m_minimumUpdateIntervalTimer->isActive()) { // dispatchPendingItems() will be called when the timer // has been expired. + m_pendingEmitLoadingCompleted = true; return; } - dispatchPendingItems(); + m_pendingEmitLoadingCompleted = false; + dispatchPendingItemsToInsert(); + + 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_urlsToExpand) { + const int index = m_items.value(url, -1); + if (index >= 0) { + 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_urlsToExpand.clear(); + } + + emit loadingCompleted(); m_minimumUpdateIntervalTimer->start(); } @@ -313,16 +669,44 @@ void KFileItemModel::slotCanceled() { m_minimumUpdateIntervalTimer->stop(); m_maximumUpdateIntervalTimer->stop(); - dispatchPendingItems(); + dispatchPendingItemsToInsert(); } void KFileItemModel::slotNewItems(const KFileItemList& items) { - if (!m_pendingItemsToDelete.isEmpty()) { - removeItems(m_pendingItemsToDelete); - m_pendingItemsToDelete.clear(); + if (m_requestRole[ExpansionLevelRole] && m_rootExpansionLevel >= 0) { + // If the expanding of items is enabled in the model, it might be + // possible that the call dirLister->openUrl(url, KDirLister::Keep) in + // KFileItemModel::setExpanded() results in emitting of 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. + foreach (const KFileItem& item, items) { + const int index = m_items.value(item.url(), -1); + if (index >= 0) { + // The items are already part of the model. + return; + } + } + } + + if (m_filter.pattern().isEmpty()) { + m_pendingItemsToInsert.append(items); + } else { + // The name-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); } - m_pendingItemsToInsert.append(items); if (useMaximumUpdateInterval() && !m_maximumUpdateIntervalTimer->isActive()) { // Assure that items get dispatched if no completed() or canceled() signal is @@ -333,11 +717,80 @@ void KFileItemModel::slotNewItems(const KFileItemList& items) void KFileItemModel::slotItemsDeleted(const KFileItemList& items) { - if (!m_pendingItemsToInsert.isEmpty()) { - insertItems(m_pendingItemsToInsert); - m_pendingItemsToInsert.clear(); + dispatchPendingItemsToInsert(); + + if (!m_filteredItems.isEmpty()) { + foreach (const KFileItem& item, items) { + m_filteredItems.remove(item); + } + } + + removeItems(items); +} + +void KFileItemModel::slotRefreshItems(const QList >& items) +{ + Q_ASSERT(!items.isEmpty()); +#ifdef KFILEITEMMODEL_DEBUG + kDebug() << "Refreshing" << items.count() << "items"; +#endif + + m_groups.clear(); + + // Get the indexes of all items that have been refreshed + QList indexes; + indexes.reserve(items.count()); + + QListIterator > it(items); + while (it.hasNext()) { + const QPair& itemPair = it.next(); + 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; + m_itemData[index]->values = retrieveData(newItem); + m_items.remove(oldItem.url()); + m_items.insert(newItem.url(), index); + indexes.append(index); + } + } + + // If the changed items have been created recently, they might not be in m_items yet. + // In that case, the list 'indexes' might be empty. + if (indexes.isEmpty()) { + return; + } + + // Extract the item-ranges out of the changed indexes + qSort(indexes); + + KItemRangeList itemRangeList; + int previousIndex = indexes.at(0); + int rangeIndex = previousIndex; + int rangeCount = 1; + + const int maxIndex = indexes.count() - 1; + for (int i = 1; i <= maxIndex; ++i) { + const int currentIndex = indexes.at(i); + if (currentIndex == previousIndex + 1) { + ++rangeCount; + } else { + itemRangeList.append(KItemRange(rangeIndex, rangeCount)); + + rangeIndex = currentIndex; + rangeCount = 1; + } + previousIndex = currentIndex; } - m_pendingItemsToDelete.append(items); + + if (rangeCount > 0) { + itemRangeList.append(KItemRange(rangeIndex, rangeCount)); + } + + emit itemsChanged(itemRangeList, m_roles); + + resortAllItems(); } void KFileItemModel::slotClear() @@ -346,20 +799,25 @@ 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_pendingItemsToDelete.clear(); - m_rootExpansionLevel = -1; + m_rootExpansionLevel = UninitializedRootExpansionLevel; - 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(); } void KFileItemModel::slotClear(const KUrl& url) @@ -367,16 +825,15 @@ void KFileItemModel::slotClear(const KUrl& url) Q_UNUSED(url); } -void KFileItemModel::dispatchPendingItems() +void KFileItemModel::dispatchPendingItemsToInsert() { if (!m_pendingItemsToInsert.isEmpty()) { - Q_ASSERT(m_pendingItemsToDelete.isEmpty()); insertItems(m_pendingItemsToInsert); m_pendingItemsToInsert.clear(); - } else if (!m_pendingItemsToDelete.isEmpty()) { - Q_ASSERT(m_pendingItemsToInsert.isEmpty()); - removeItems(m_pendingItemsToDelete); - m_pendingItemsToDelete.clear(); + } + + if (m_pendingEmitLoadingCompleted) { + emit loadingCompleted(); } } @@ -393,7 +850,9 @@ void KFileItemModel::insertItems(const KFileItemList& items) kDebug() << "Inserting" << items.count() << "items"; #endif - KFileItemList sortedItems = items; + m_groups.clear(); + + QList sortedItems = createItemDataList(items); sort(sortedItems.begin(), sortedItems.end()); #ifdef KFILEITEMMODEL_DEBUG @@ -403,15 +862,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; @@ -424,11 +883,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 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) { @@ -441,8 +899,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), 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); @@ -463,7 +922,16 @@ void KFileItemModel::removeItems(const KFileItemList& items) kDebug() << "Removing " << items.count() << "items"; #endif - KFileItemList sortedItems = items; + m_groups.clear(); + + 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)); + } + } sort(sortedItems.begin(), sortedItems.end()); QList indexesToRemove; @@ -474,15 +942,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) == itemToRemove) { + 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; } @@ -504,35 +974,68 @@ 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)); - 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), 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_rootExpansionLevel = UninitializedRootExpansionLevel; } itemRanges << KItemRange(removedAtIndex, removedCount); emit itemsRemoved(itemRanges); } -void KFileItemModel::removeExpandedItems() +QList KFileItemModel::createItemDataList(const KFileItemList& items) const { + 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[ExpansionLevelRole] + && itemData->values["expansionLevel"].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(); + } + } + 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("expansionLevel").toInt() > 0) { + expandedItems.append(itemData->item); } } @@ -541,7 +1044,8 @@ void KFileItemModel::removeExpandedItems() Q_ASSERT(m_rootExpansionLevel >= 0); removeItems(expandedItems); - m_rootExpansionLevel = -1; + m_rootExpansionLevel = UninitializedRootExpansionLevel; + m_expandedUrls.clear(); } void KFileItemModel::resetRoles() @@ -564,6 +1068,9 @@ KFileItemModel::Role KFileItemModel::roleIndex(const QByteArray& role) const 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); @@ -572,12 +1079,13 @@ KFileItemModel::Role KFileItemModel::roleIndex(const QByteArray& role) const } QHash KFileItemModel::retrieveData(const KFileItem& item) 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(); if (m_requestRole[IsDirRole]) { @@ -585,7 +1093,7 @@ QHash KFileItemModel::retrieveData(const KFileItem& item) } if (m_requestRole[NameRole]) { - data.insert("name", item.name()); + data.insert("name", item.text()); } if (m_requestRole[SizeRole]) { @@ -633,16 +1141,29 @@ QHash KFileItemModel::retrieveData(const KFileItem& item) } if (m_requestRole[ExpansionLevelRole]) { - if (m_rootExpansionLevel < 0) { - KDirLister* dirLister = m_dirLister.data(); - if (dirLister) { - const QString rootDir = dirLister->url().directory(KUrl::AppendTrailingSlash); + if (m_rootExpansionLevel == UninitializedRootExpansionLevel && m_dirLister.data()) { + const KUrl rootUrl = m_dirLister.data()->url(); + const QString protocol = rootUrl.protocol(); + const bool isSearchUrl = (protocol.contains("search") || protocol == QLatin1String("nepomuk")); + if (isSearchUrl) { + m_rootExpansionLevel = ForceRootExpansionLevel; + } else { + const QString rootDir = rootUrl.directory(KUrl::AppendTrailingSlash); m_rootExpansionLevel = rootDir.count('/'); + if (m_rootExpansionLevel == 1) { + // Special case: The root is already reached and no parent is available + --m_rootExpansionLevel; + } } } - const QString dir = item.url().directory(KUrl::AppendTrailingSlash); - const int level = dir.count('/') - m_rootExpansionLevel - 1; - data.insert("expansionLevel", level); + + if (m_rootExpansionLevel == ForceRootExpansionLevel) { + data.insert("expansionLevel", -1); + } else { + const QString dir = item.url().directory(KUrl::AppendTrailingSlash); + const int level = dir.count('/') - m_rootExpansionLevel - 1; + data.insert("expansionLevel", level); + } } if (item.isMimeTypeKnown()) { @@ -656,7 +1177,7 @@ 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; @@ -664,13 +1185,13 @@ bool KFileItemModel::lessThan(const KFileItem& a, const KFileItem& b) const result = expansionLevelsCompare(a, b); if (result != 0) { // The items have parents with different expansion levels - return result < 0; + return (sortOrder() == Qt::AscendingOrder) ? result < 0 : result > 0; } } - if (m_sortFoldersFirst) { - const bool isDirA = a.isDir(); - const bool isDirB = b.isDir(); + if (m_sortFoldersFirst || m_sortRole == SizeRole) { + const bool isDirA = a->item.isDir(); + const bool isDirB = b->item.isDir(); if (isDirA && !isDirB) { return true; } else if (!isDirA && isDirB) { @@ -678,20 +1199,26 @@ 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 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) { @@ -700,84 +1227,198 @@ bool KFileItemModel::lessThan(const KFileItem& a, const KFileItem& b) const break; } - default: + case SizeRole: { + if (itemA.isDir()) { + Q_ASSERT(itemB.isDir()); // see "if (m_sortFoldersFirst || m_sortRole == SizeRole)" above + + const QVariant valueA = a->values.value("size"); + const QVariant valueB = b->values.value("size"); + + if (valueA.isNull()) { + result = -1; + } else if (valueB.isNull()) { + result = +1; + } else { + result = valueA.value() - valueB.value(); + } + } else { + Q_ASSERT(!itemB.isDir()); // see "if (m_sortFoldersFirst || m_sortRole == SizeRole)" above + result = itemA.size() - itemB.size(); + } 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); + case TypeRole: { + result = QString::compare(a->values.value("type").toString(), + b->values.value("type").toString()); + break; } - return result < 0; -} + case CommentRole: { + result = QString::compare(a->values.value("comment").toString(), + b->values.value("comment").toString()); + break; + } -void KFileItemModel::sort(const KFileItemList::iterator& startIterator, const KFileItemList::iterator& endIterator) -{ - KFileItemList::iterator start = startIterator; - KFileItemList::iterator end = endIterator; + case TagsRole: { + result = QString::compare(a->values.value("tags").toString(), + b->values.value("tags").toString()); + break; + } - // 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; - } + case RatingRole: { + result = a->values.value("rating").toInt() - b->values.value("rating").toInt(); + break; + } - --end; - KFileItemList::iterator low = start, high = end - 1; - KFileItemList::iterator pivot = start + span / 2; + default: + break; + } - if (lessThan(*end, *start)) { - qSwap(*end, *start); - } - if (span == 2) { - return; - } + if (result != 0) { + // The current sort role was sufficient to define an order + return result; + } - if (lessThan(*pivot, *start)) { - qSwap(*pivot, *start); - } - if (lessThan(*end, *pivot)) { - qSwap(*end, *pivot); - } - if (span == 3) { - return; - } + // Fallback #1: Compare the text of the items + result = stringCompare(itemA.text(), itemB.text()); + if (result != 0) { + return result; + } - qSwap(*pivot, *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; + } - while (low < high) { - while (low < high && lessThan(*low, *end)) { - ++low; - } + // 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); +} - while (high > low && lessThan(*end, *high)) { - --high; - } - if (low < high) { - qSwap(*low, *high); - ++low; - --high; - } else { - break; - } - } +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); +} - if (lessThan(*low, *end)) { - ++low; +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); +} - qSwap(*end, *low); - sort(start, low); +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; +} - start = low + 1; - ++end; +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 @@ -802,10 +1443,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::expansionLevelsCompare(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; @@ -813,9 +1454,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 @@ -838,9 +1479,9 @@ 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; @@ -848,7 +1489,27 @@ int KFileItemModel::expansionLevelsCompare(const KFileItem& a, const KFileItem& return +1; } - return stringCompare(subPathA, subPathB); + // Compare the items of the parents that represent the first + // different path after the common path. + const KUrl parentUrlA(pathA.left(index) + subPathA); + const KUrl parentUrlB(pathB.left(index) + subPathB); + + const ItemData* parentA = a; + while (parentA && parentA->item.url() != parentUrlA) { + parentA = parentA->parent; + } + + const ItemData* parentB = b; + while (parentB && parentB->item.url() != parentUrlB) { + 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, @@ -868,4 +1529,317 @@ bool KFileItemModel::useMaximumUpdateInterval() const return dirLister && !dirLister->url().isLocalFile(); } +QList > KFileItemModel::nameRoleGroups() const +{ + Q_ASSERT(!m_itemData.isEmpty()); + + const int maxIndex = count() - 1; + QList > groups; + + 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(); + + // 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); + } + + if (firstChar != newFirstChar) { + QString newGroupValue; + if (newFirstChar >= QLatin1Char('A') && newFirstChar <= QLatin1Char('Z')) { + // Apply group 'A' - 'Z' + newGroupValue = newFirstChar; + isLetter = true; + } 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) { + groupValue = newGroupValue; + groups.append(QPair(i, newGroupValue)); + } + + firstChar = newFirstChar; + } + } + return groups; +} + +QList > KFileItemModel::sizeRoleGroups() const +{ + Q_ASSERT(!m_itemData.isEmpty()); + + const int maxIndex = count() - 1; + QList > groups; + + QString groupValue; + for (int i = 0; i <= maxIndex; ++i) { + if (isChildItem(i)) { + continue; + } + + 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()) { + newGroupValue = i18nc("@title:group Size", "Folders"); + } else if (fileSize < 5 * 1024 * 1024) { + newGroupValue = i18nc("@title:group Size", "Small"); + } else if (fileSize < 10 * 1024 * 1024) { + newGroupValue = i18nc("@title:group Size", "Medium"); + } else { + newGroupValue = i18nc("@title:group Size", "Big"); + } + + if (newGroupValue != groupValue) { + groupValue = newGroupValue; + groups.append(QPair(i, newGroupValue)); + } + } + + return groups; +} + +QList > KFileItemModel::dateRoleGroups() const +{ + Q_ASSERT(!m_itemData.isEmpty()); + + const int maxIndex = count() - 1; + QList > groups; + + 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) { + if (isChildItem(i)) { + continue; + } + + 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 + continue; + } + previousModifiedDate = modifiedDate; + + 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) { + case 0: + switch (daysDistance) { + case 0: newGroupValue = i18nc("@title:group Date", "Today"); break; + case 1: newGroupValue = i18nc("@title:group Date", "Yesterday"); break; + default: newGroupValue = modifiedTime.toString(i18nc("@title:group The week day name: %A", "%A")); + } + break; + case 1: + newGroupValue = i18nc("@title:group Date", "Last Week"); + break; + case 2: + newGroupValue = i18nc("@title:group Date", "Two Weeks Ago"); + break; + case 3: + newGroupValue = i18nc("@title:group Date", "Three Weeks Ago"); + break; + case 4: + case 5: + newGroupValue = i18nc("@title:group Date", "Earlier this Month"); + break; + default: + Q_ASSERT(false); + } + } else { + const QDate lastMonthDate = currentDate.addMonths(-1); + if (lastMonthDate.year() == modifiedDate.year() && lastMonthDate.month() == modifiedDate.month()) { + if (daysDistance == 1) { + newGroupValue = modifiedTime.toString(i18nc("@title:group Date: %B is full month name in current locale, and %Y is full year number", "Yesterday (%B, %Y)")); + } 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)")); + } 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) { + newGroupValue = modifiedTime.toString(i18nc("@title:group Date: %B is full month name in current locale, and %Y is full year number", "Three Weeks Ago (%B, %Y)")); + } else { + newGroupValue = modifiedTime.toString(i18nc("@title:group Date: %B is full month name in current locale, and %Y is full year number", "Earlier on %B, %Y")); + } + } else { + newGroupValue = modifiedTime.toString(i18nc("@title:group The month and year: %B is full month name in current locale, and %Y is full year number", "%B, %Y")); + } + } + + if (newGroupValue != groupValue) { + groupValue = newGroupValue; + groups.append(QPair(i, newGroupValue)); + } + } + + return groups; +} + +QList > KFileItemModel::permissionRoleGroups() const +{ + Q_ASSERT(!m_itemData.isEmpty()); + + const int maxIndex = count() - 1; + QList > groups; + + QString permissionsString; + QString groupValue; + for (int i = 0; i <= maxIndex; ++i) { + if (isChildItem(i)) { + continue; + } + + const ItemData* itemData = m_itemData.at(i); + const QString newPermissionsString = itemData->values.value("permissions").toString(); + if (newPermissionsString == permissionsString) { + continue; + } + permissionsString = newPermissionsString; + + const QFileInfo info(itemData->item.url().pathOrUrl()); + + // Set user string + QString user; + if (info.permission(QFile::ReadUser)) { + user = i18nc("@item:intext Access permission, concatenated", "Read, "); + } + if (info.permission(QFile::WriteUser)) { + user += i18nc("@item:intext Access permission, concatenated", "Write, "); + } + if (info.permission(QFile::ExeUser)) { + user += i18nc("@item:intext Access permission, concatenated", "Execute, "); + } + user = user.isEmpty() ? i18nc("@item:intext Access permission, concatenated", "Forbidden") : user.mid(0, user.count() - 2); + + // Set group string + QString group; + if (info.permission(QFile::ReadGroup)) { + group = i18nc("@item:intext Access permission, concatenated", "Read, "); + } + if (info.permission(QFile::WriteGroup)) { + group += i18nc("@item:intext Access permission, concatenated", "Write, "); + } + if (info.permission(QFile::ExeGroup)) { + group += i18nc("@item:intext Access permission, concatenated", "Execute, "); + } + group = group.isEmpty() ? i18nc("@item:intext Access permission, concatenated", "Forbidden") : group.mid(0, group.count() - 2); + + // Set others string + QString others; + if (info.permission(QFile::ReadOther)) { + others = i18nc("@item:intext Access permission, concatenated", "Read, "); + } + if (info.permission(QFile::WriteOther)) { + others += i18nc("@item:intext Access permission, concatenated", "Write, "); + } + if (info.permission(QFile::ExeOther)) { + others += i18nc("@item:intext Access permission, concatenated", "Execute, "); + } + others = others.isEmpty() ? i18nc("@item:intext Access permission, concatenated", "Forbidden") : others.mid(0, others.count() - 2); + + const QString newGroupValue = i18nc("@title:group Files and folders by permissions", "User: %1 | Group: %2 | Others: %3", user, group, others); + if (newGroupValue != groupValue) { + groupValue = newGroupValue; + groups.append(QPair(i, newGroupValue)); + } + } + + return groups; +} + +QList > KFileItemModel::ratingRoleGroups() const +{ + Q_ASSERT(!m_itemData.isEmpty()); + + const int maxIndex = count() - 1; + QList > groups; + + int groupValue; + for (int i = 0; i <= maxIndex; ++i) { + if (isChildItem(i)) { + continue; + } + const int newGroupValue = m_itemData.at(i)->values.value("rating").toInt(); + if (newGroupValue != groupValue) { + groupValue = newGroupValue; + groups.append(QPair(i, newGroupValue)); + } + } + + return groups; +} + +QList > KFileItemModel::genericStringRoleGroups(const QByteArray& role) const +{ + Q_ASSERT(!m_itemData.isEmpty()); + + const int maxIndex = count() - 1; + QList > groups; + + QString groupValue; + for (int i = 0; i <= maxIndex; ++i) { + if (isChildItem(i)) { + continue; + } + const QString newGroupValue = m_itemData.at(i)->values.value(role).toString(); + if (newGroupValue != groupValue) { + groupValue = newGroupValue; + groups.append(QPair(i, newGroupValue)); + } + } + + return groups; +} + #include "kfileitemmodel.moc"