diff --git a/src/kitemviews/kfileitemmodel.cpp b/src/kitemviews/kfileitemmodel.cpp index 2a9e2ea41..9db3a2e5a 100644 --- a/src/kitemviews/kfileitemmodel.cpp +++ b/src/kitemviews/kfileitemmodel.cpp @@ -1,2411 +1,2411 @@ /***************************************************************************** * Copyright (C) 2011 by Peter Penz * * Copyright (C) 2013 by Frank Reininghaus * * Copyright (C) 2013 by Emmanuel Pescosta * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * *****************************************************************************/ #include "kfileitemmodel.h" #include "dolphin_generalsettings.h" #include "dolphindebug.h" #include "private/kfileitemmodeldirlister.h" #include "private/kfileitemmodelsortalgorithm.h" #include #include #include #include #include #include // #define KFILEITEMMODEL_DEBUG KFileItemModel::KFileItemModel(QObject* parent) : KItemModelBase("text", parent), m_dirLister(nullptr), m_sortDirsFirst(true), m_sortRole(NameRole), m_sortingProgressPercent(-1), m_roles(), m_itemData(), m_items(), m_filter(), m_filteredItems(), m_requestRole(), m_maximumUpdateIntervalTimer(nullptr), m_resortAllItemsTimer(nullptr), m_pendingItemsToInsert(), m_groups(), m_expandedDirs(), m_urlsToExpand() { m_collator.setNumericMode(true); loadSortingSettings(); m_dirLister = new KFileItemModelDirLister(this); m_dirLister->setDelayedMimeTypes(true); const QWidget* parentWidget = qobject_cast(parent); if (parentWidget) { m_dirLister->setMainWindow(parentWidget->window()); } connect(m_dirLister, &KFileItemModelDirLister::started, this, &KFileItemModel::directoryLoadingStarted); connect(m_dirLister, static_cast(&KFileItemModelDirLister::canceled), this, &KFileItemModel::slotCanceled); connect(m_dirLister, static_cast(&KFileItemModelDirLister::completed), this, &KFileItemModel::slotCompleted); connect(m_dirLister, &KFileItemModelDirLister::itemsAdded, this, &KFileItemModel::slotItemsAdded); connect(m_dirLister, &KFileItemModelDirLister::itemsDeleted, this, &KFileItemModel::slotItemsDeleted); connect(m_dirLister, &KFileItemModelDirLister::refreshItems, this, &KFileItemModel::slotRefreshItems); connect(m_dirLister, static_cast(&KFileItemModelDirLister::clear), this, &KFileItemModel::slotClear); connect(m_dirLister, &KFileItemModelDirLister::infoMessage, this, &KFileItemModel::infoMessage); connect(m_dirLister, &KFileItemModelDirLister::errorMessage, this, &KFileItemModel::errorMessage); connect(m_dirLister, &KFileItemModelDirLister::percent, this, &KFileItemModel::directoryLoadingProgress); connect(m_dirLister, static_cast(&KFileItemModelDirLister::redirection), this, &KFileItemModel::directoryRedirection); connect(m_dirLister, &KFileItemModelDirLister::urlIsFileError, this, &KFileItemModel::urlIsFileError); // Apply default roles that should be determined resetRoles(); m_requestRole[NameRole] = true; m_requestRole[IsDirRole] = true; m_requestRole[IsLinkRole] = true; m_roles.insert("text"); m_roles.insert("isDir"); m_roles.insert("isLink"); m_roles.insert("isHidden"); // 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, &QTimer::timeout, this, &KFileItemModel::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, &QTimer::timeout, this, &KFileItemModel::resortAllItems); connect(GeneralSettings::self(), &GeneralSettings::sortingChoiceChanged, this, &KFileItemModel::slotSortingChoiceChanged); } KFileItemModel::~KFileItemModel() { qDeleteAll(m_itemData); qDeleteAll(m_filteredItems); qDeleteAll(m_pendingItemsToInsert); } void KFileItemModel::loadDirectory(const QUrl &url) { m_dirLister->openUrl(url); } void KFileItemModel::refreshDirectory(const QUrl &url) { // Refresh all expanded directories first (Bug 295300) QHashIterator expandedDirs(m_expandedDirs); while (expandedDirs.hasNext()) { expandedDirs.next(); m_dirLister->openUrl(expandedDirs.value(), KDirLister::Reload); } m_dirLister->openUrl(url, KDirLister::Reload); } QUrl KFileItemModel::directory() const { return m_dirLister->url(); } void KFileItemModel::cancelDirectoryLoading() { m_dirLister->stop(); } int KFileItemModel::count() const { return m_itemData.count(); } QHash KFileItemModel::data(int index) const { if (index >= 0 && index < count()) { ItemData* data = m_itemData.at(index); if (data->values.isEmpty()) { data->values = retrieveData(data->item, data->parent); } return data->values; } return QHash(); } bool KFileItemModel::setData(int index, const QHash& values) { if (index < 0 || index >= count()) { return false; } QHash currentValues = data(index); // Determine which roles have been changed QSet changedRoles; QHashIterator it(values); while (it.hasNext()) { it.next(); const QByteArray role = sharedValue(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; if (changedRoles.contains("text")) { QUrl url = m_itemData[index]->item.url(); url = url.adjusted(QUrl::RemoveFilename); url.setPath(url.path() + currentValues["text"].toString()); m_itemData[index]->item.setUrl(url); } emitItemsChangedAndTriggerResorting(KItemRangeList() << KItemRange(index, 1), changedRoles); return true; } void KFileItemModel::setSortDirectoriesFirst(bool dirsFirst) { if (dirsFirst != m_sortDirsFirst) { m_sortDirsFirst = dirsFirst; resortAllItems(); } } bool KFileItemModel::sortDirectoriesFirst() const { return m_sortDirsFirst; } void KFileItemModel::setShowHiddenFiles(bool show) { m_dirLister->setShowingDotFiles(show); m_dirLister->emitChanges(); if (show) { dispatchPendingItemsToInsert(); } } bool KFileItemModel::showHiddenFiles() const { return m_dirLister->showingDotFiles(); } void KFileItemModel::setShowDirectoriesOnly(bool enabled) { m_dirLister->setDirOnlyMode(enabled); } bool KFileItemModel::showDirectoriesOnly() const { return m_dirLister->dirOnlyMode(); } QMimeData* KFileItemModel::createMimeData(const KItemSet& 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 QList urls; QList mostLocalUrls; const ItemData* lastAddedItem = nullptr; for (int index : indexes) { const ItemData* itemData = m_itemData.at(index); const ItemData* parent = itemData->parent; while (parent && parent != lastAddedItem) { parent = parent->parent; } if (parent && parent == lastAddedItem) { // A parent of 'itemData' has been added already. continue; } lastAddedItem = itemData; const KFileItem& item = itemData->item; if (!item.isNull()) { urls << item.url(); bool isLocal; mostLocalUrls << item.mostLocalUrl(isLocal); } } KUrlMimeData::setUrls(urls, mostLocalUrls, data); return data; } int KFileItemModel::indexForKeyboardSearch(const QString& text, int startFromIndex) const { startFromIndex = qMax(0, startFromIndex); for (int i = startFromIndex; i < count(); ++i) { if (fileItem(i).text().startsWith(text, Qt::CaseInsensitive)) { return i; } } for (int i = 0; i < startFromIndex; ++i) { if (fileItem(i).text().startsWith(text, Qt::CaseInsensitive)) { return i; } } return -1; } bool KFileItemModel::supportsDropping(int index) const { const KFileItem item = fileItem(index); return !item.isNull() && (item.isDir() || item.isDesktopFile()); } QString KFileItemModel::roleDescription(const QByteArray& role) const { 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 description.value(role); } QList > KFileItemModel::groups() const { if (!m_itemData.isEmpty() && m_groups.isEmpty()) { #ifdef KFILEITEMMODEL_DEBUG QElapsedTimer timer; timer.start(); #endif switch (typeForRole(sortRole())) { case NameRole: m_groups = nameRoleGroups(); break; case SizeRole: m_groups = sizeRoleGroups(); break; case ModificationTimeRole: m_groups = timeRoleGroups([](const ItemData *item) { return item->item.time(KFileItem::ModificationTime); }); break; case CreationTimeRole: m_groups = timeRoleGroups([](const ItemData *item) { return item->item.time(KFileItem::CreationTime); }); break; case AccessTimeRole: m_groups = timeRoleGroups([](const ItemData *item) { return item->item.time(KFileItem::AccessTime); }); break; case DeletionTimeRole: m_groups = timeRoleGroups([](const ItemData *item) { return item->values.value("deletiontime").toDateTime(); }); break; case PermissionsRole: m_groups = permissionRoleGroups(); break; case RatingRole: m_groups = ratingRoleGroups(); break; default: m_groups = genericStringRoleGroups(sortRole()); break; } #ifdef KFILEITEMMODEL_DEBUG qCDebug(DolphinDebug) << "[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_itemData.at(index)->item; } return KFileItem(); } KFileItem KFileItemModel::fileItem(const QUrl &url) const { const int indexForUrl = index(url); if (indexForUrl >= 0) { return m_itemData.at(indexForUrl)->item; } return KFileItem(); } int KFileItemModel::index(const KFileItem& item) const { return index(item.url()); } int KFileItemModel::index(const QUrl& url) const { const QUrl urlToFind = url.adjusted(QUrl::StripTrailingSlash); const int itemCount = m_itemData.count(); int itemsInHash = m_items.count(); int index = m_items.value(urlToFind, -1); while (index < 0 && itemsInHash < itemCount) { // Not all URLs are stored yet in m_items. We grow m_items until either // urlToFind is found, or all URLs have been stored in m_items. // Note that we do not add the URLs to m_items one by one, but in // larger blocks. After each block, we check if urlToFind is in // m_items. We could in principle compare urlToFind with each URL while // we are going through m_itemData, but comparing two QUrls will, // unlike calling qHash for the URLs, trigger a parsing of the URLs // which costs both CPU cycles and memory. const int blockSize = 1000; const int currentBlockEnd = qMin(itemsInHash + blockSize, itemCount); for (int i = itemsInHash; i < currentBlockEnd; ++i) { const QUrl nextUrl = m_itemData.at(i)->item.url(); m_items.insert(nextUrl, i); } itemsInHash = currentBlockEnd; index = m_items.value(urlToFind, -1); } if (index < 0) { // The item could not be found, even though all items from m_itemData // should be in m_items now. We print some diagnostic information which // might help to find the cause of the problem, but only once. This // prevents that obtaining and printing the debugging information // wastes CPU cycles and floods the shell or .xsession-errors. static bool printDebugInfo = true; if (m_items.count() != m_itemData.count() && printDebugInfo) { printDebugInfo = false; qCWarning(DolphinDebug) << "The model is in an inconsistent state."; qCWarning(DolphinDebug) << "m_items.count() ==" << m_items.count(); qCWarning(DolphinDebug) << "m_itemData.count() ==" << m_itemData.count(); // Check if there are multiple items with the same URL. QMultiHash indexesForUrl; for (int i = 0; i < m_itemData.count(); ++i) { indexesForUrl.insert(m_itemData.at(i)->item.url(), i); } foreach (const QUrl& url, indexesForUrl.uniqueKeys()) { if (indexesForUrl.count(url) > 1) { qCWarning(DolphinDebug) << "Multiple items found with the URL" << url; auto it = indexesForUrl.find(url); while (it != indexesForUrl.end() && it.key() == url) { const ItemData* data = m_itemData.at(it.value()); qCWarning(DolphinDebug) << "index" << it.value() << ":" << data->item; if (data->parent) { qCWarning(DolphinDebug) << "parent" << data->parent->item; } ++it; } } } } } return index; } KFileItem KFileItemModel::rootItem() const { return m_dirLister->rootItem(); } void KFileItemModel::clear() { slotClear(); } void KFileItemModel::setRoles(const QSet& roles) { if (m_roles == roles) { return; } const QSet changedRoles = (roles - m_roles) + (m_roles - roles); m_roles = roles; if (count() > 0) { 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). removeExpandedItems(); } } m_groups.clear(); resetRoles(); QSetIterator it(roles); while (it.hasNext()) { const QByteArray& role = it.next(); 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_itemData[i]->values = retrieveData(m_itemData.at(i)->item, m_itemData.at(i)->parent); } emit itemsChanged(KItemRangeList() << KItemRange(0, count()), changedRoles); } // Clear the 'values' of all filtered items. They will be re-populated with the // correct roles the next time 'values' will be accessed via data(int). QHash::iterator filteredIt = m_filteredItems.begin(); const QHash::iterator filteredEnd = m_filteredItems.end(); while (filteredIt != filteredEnd) { (*filteredIt)->values.clear(); ++filteredIt; } } QSet KFileItemModel::roles() const { return m_roles; } bool KFileItemModel::setExpanded(int index, bool expanded) { if (!isExpandable(index) || isExpanded(index) == expanded) { return false; } QHash values; values.insert(sharedValue("isExpanded"), expanded); if (!setData(index, values)) { return false; } const KFileItem item = m_itemData.at(index)->item; const QUrl url = item.url(); const QUrl targetUrl = item.targetUrl(); if (expanded) { m_expandedDirs.insert(targetUrl, url); m_dirLister->openUrl(url, KDirLister::Keep); const QVariantList previouslyExpandedChildren = m_itemData.at(index)->values.value("previouslyExpandedChildren").value(); foreach (const QVariant& var, previouslyExpandedChildren) { m_urlsToExpand.insert(var.toUrl()); } } else { // Note that there might be (indirect) children of the folder which is to be collapsed in // m_pendingItemsToInsert. To prevent that they will be inserted into the model later, // possibly without a parent, which might result in a crash, we insert all pending items // right now. All new items which would be without a parent will then be removed. dispatchPendingItemsToInsert(); // Check if the index of the collapsed folder has changed. If that is the case, then items // were inserted before the collapsed folder, and its index needs to be updated. if (m_itemData.at(index)->item != item) { index = this->index(item); } m_expandedDirs.remove(targetUrl); m_dirLister->stop(url); const int parentLevel = expandedParentsCount(index); const int itemCount = m_itemData.count(); const int firstChildIndex = index + 1; QVariantList expandedChildren; int childIndex = firstChildIndex; while (childIndex < itemCount && expandedParentsCount(childIndex) > parentLevel) { ItemData* itemData = m_itemData.at(childIndex); if (itemData->values.value("isExpanded").toBool()) { const QUrl targetUrl = itemData->item.targetUrl(); const QUrl url = itemData->item.url(); m_expandedDirs.remove(targetUrl); m_dirLister->stop(url); // TODO: try to unit-test this, see https://bugs.kde.org/show_bug.cgi?id=332102#c11 expandedChildren.append(targetUrl); } ++childIndex; } const int childrenCount = childIndex - firstChildIndex; removeFilteredChildren(KItemRangeList() << KItemRange(index, 1 + childrenCount)); removeItems(KItemRangeList() << KItemRange(firstChildIndex, childrenCount), DeleteItemData); m_itemData.at(index)->values.insert("previouslyExpandedChildren", expandedChildren); } return true; } bool KFileItemModel::isExpanded(int index) const { if (index >= 0 && index < count()) { return m_itemData.at(index)->values.value("isExpanded").toBool(); } return false; } bool KFileItemModel::isExpandable(int index) const { if (index >= 0 && index < count()) { // Call data (instead of accessing m_itemData directly) // to ensure that the value is initialized. return data(index).value("isExpandable").toBool(); } return false; } int KFileItemModel::expandedParentsCount(int index) const { if (index >= 0 && index < count()) { return expandedParentsCount(m_itemData.at(index)); } return 0; } QSet KFileItemModel::expandedDirectories() const { QSet result; const auto dirs = m_expandedDirs; for (const auto &dir : dirs) { result.insert(dir); } return result; } void KFileItemModel::restoreExpandedDirectories(const QSet &urls) { m_urlsToExpand = urls; } void KFileItemModel::expandParentDirectories(const QUrl &url) { // 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. QUrl urlToExpand = m_dirLister->url(); const int pos = urlToExpand.path().length(); // first subdir can be empty, if m_dirLister->url().path() does not end with '/' // this happens if baseUrl is not root but a home directory, see FoldersPanel, // so using QString::SkipEmptyParts const QStringList subDirs = url.path().mid(pos).split(QDir::separator(), QString::SkipEmptyParts); for (int i = 0; i < subDirs.count() - 1; ++i) { QString path = urlToExpand.path(); if (!path.endsWith(QLatin1Char('/'))) { path.append(QLatin1Char('/')); } urlToExpand.setPath(path + 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) { if (m_filter.mimeTypes() != filters) { dispatchPendingItemsToInsert(); m_filter.setMimeTypes(filters); applyFilters(); } } QStringList KFileItemModel::mimeTypeFilters() const { return m_filter.mimeTypes(); } void KFileItemModel::applyFilters() { // Check which shown items from m_itemData must get // hidden and hence moved to m_filteredItems. QVector newFilteredIndexes; const int itemCount = m_itemData.count(); for (int index = 0; index < itemCount; ++index) { ItemData* itemData = m_itemData.at(index); // Only filter non-expanded items as child items may never // exist without a parent item if (!itemData->values.value("isExpanded").toBool()) { const KFileItem item = itemData->item; if (!m_filter.matches(item)) { newFilteredIndexes.append(index); m_filteredItems.insert(item, itemData); } } } const KItemRangeList removedRanges = KItemRangeList::fromSortedContainer(newFilteredIndexes); removeItems(removedRanges, KeepItemData); // Check which hidden items from m_filteredItems should // get visible again and hence removed from m_filteredItems. QList newVisibleItems; QHash::iterator it = m_filteredItems.begin(); while (it != m_filteredItems.end()) { if (m_filter.matches(it.key())) { newVisibleItems.append(it.value()); it = m_filteredItems.erase(it); } else { ++it; } } insertItems(newVisibleItems); } void KFileItemModel::removeFilteredChildren(const KItemRangeList& itemRanges) { if (m_filteredItems.isEmpty() || !m_requestRole[ExpandedParentsCountRole]) { // There are either no filtered items, or it is not possible to expand // folders -> there cannot be any filtered children. return; } QSet parents; foreach (const KItemRange& range, itemRanges) { for (int index = range.index; index < range.index + range.count; ++index) { parents.insert(m_itemData.at(index)); } } QHash::iterator it = m_filteredItems.begin(); while (it != m_filteredItems.end()) { if (parents.contains(it.value()->parent)) { delete it.value(); it = m_filteredItems.erase(it); } else { ++it; } } } 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.requiresBaloo = map[i].requiresBaloo; info.requiresIndexer = map[i].requiresIndexer; rolesInfo.append(info); } } } return rolesInfo; } 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 = typeForRole(current); if (!m_requestRole[m_sortRole]) { QSet newRoles = m_roles; newRoles << current; setRoles(newRoles); } resortAllItems(); } void KFileItemModel::onSortOrderChanged(Qt::SortOrder current, Qt::SortOrder previous) { Q_UNUSED(current); Q_UNUSED(previous); resortAllItems(); } void KFileItemModel::loadSortingSettings() { using Choice = GeneralSettings::EnumSortingChoice; switch (GeneralSettings::sortingChoice()) { case Choice::NaturalSorting: m_naturalSorting = true; m_collator.setCaseSensitivity(Qt::CaseInsensitive); break; case Choice::CaseSensitiveSorting: m_naturalSorting = false; m_collator.setCaseSensitivity(Qt::CaseSensitive); break; case Choice::CaseInsensitiveSorting: m_naturalSorting = false; m_collator.setCaseSensitivity(Qt::CaseInsensitive); break; default: Q_UNREACHABLE(); } } void KFileItemModel::resortAllItems() { m_resortAllItemsTimer->stop(); const int itemCount = count(); if (itemCount <= 0) { return; } #ifdef KFILEITEMMODEL_DEBUG QElapsedTimer timer; timer.start(); qCDebug(DolphinDebug) << "==========================================================="; qCDebug(DolphinDebug) << "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_items.clear(); m_items.reserve(itemCount); // 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 first index that has been moved. int firstMovedIndex = 0; while (firstMovedIndex < itemCount && firstMovedIndex == m_items.value(oldUrls.at(firstMovedIndex))) { ++firstMovedIndex; } const bool itemsHaveMoved = firstMovedIndex < itemCount; if (itemsHaveMoved) { m_groups.clear(); int lastMovedIndex = itemCount - 1; while (lastMovedIndex > firstMovedIndex && lastMovedIndex == m_items.value(oldUrls.at(lastMovedIndex))) { --lastMovedIndex; } Q_ASSERT(firstMovedIndex <= lastMovedIndex); // Create a list movedToIndexes, which has the property that // movedToIndexes[i] is the new index of the item with the old index // firstMovedIndex + i. const int movedItemsCount = lastMovedIndex - firstMovedIndex + 1; QList movedToIndexes; movedToIndexes.reserve(movedItemsCount); for (int i = firstMovedIndex; i <= lastMovedIndex; ++i) { const int newIndex = m_items.value(oldUrls.at(i)); movedToIndexes.append(newIndex); } emit itemsMoved(KItemRange(firstMovedIndex, movedItemsCount), movedToIndexes); } else if (groupedSorting()) { // The groups might have changed even if the order of the items has not. const QList > oldGroups = m_groups; m_groups.clear(); if (groups() != oldGroups) { emit groupsChanged(); } } #ifdef KFILEITEMMODEL_DEBUG qCDebug(DolphinDebug) << "[TIME] Resorting of" << itemCount << "items:" << timer.elapsed(); #endif } void KFileItemModel::slotCompleted() { 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 QUrl& url, m_urlsToExpand) { const int indexForUrl = index(url); if (indexForUrl >= 0) { m_urlsToExpand.remove(url); if (setExpanded(indexForUrl, 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 directoryLoadingCompleted(); } void KFileItemModel::slotCanceled() { m_maximumUpdateIntervalTimer->stop(); dispatchPendingItemsToInsert(); emit directoryLoadingCanceled(); } void KFileItemModel::slotItemsAdded(const QUrl &directoryUrl, const KFileItemList& items) { Q_ASSERT(!items.isEmpty()); QUrl parentUrl; if (m_expandedDirs.contains(directoryUrl)) { parentUrl = m_expandedDirs.value(directoryUrl); } else { parentUrl = directoryUrl.adjusted(QUrl::StripTrailingSlash); } if (m_requestRole[ExpandedParentsCountRole]) { // 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. if (index(items.first().url()) >= 0) { // The items are already part of the model. return; } if (directoryUrl != directory()) { // To be able to compare whether the new items may be inserted as children // of a parent item the pending items must be added to the model first. dispatchPendingItemsToInsert(); } // KDirLister keeps the children of items that got expanded once even if // they got collapsed again with KFileItemModel::setExpanded(false). So it must be // checked whether the parent for new items is still expanded. const int parentIndex = index(parentUrl); if (parentIndex >= 0 && !m_itemData[parentIndex]->values.value("isExpanded").toBool()) { // The parent is not expanded. return; } } QList itemDataList = createItemDataList(parentUrl, items); if (!m_filter.hasSetFilters()) { m_pendingItemsToInsert.append(itemDataList); } else { // The name or type filter is active. Hide filtered items // before inserting them into the model and remember // the filtered items in m_filteredItems. foreach (ItemData* itemData, itemDataList) { if (m_filter.matches(itemData->item)) { m_pendingItemsToInsert.append(itemData); } else { m_filteredItems.insert(itemData->item, itemData); } } } if (useMaximumUpdateInterval() && !m_maximumUpdateIntervalTimer->isActive()) { // Assure that items get dispatched if no completed() or canceled() signal is // emitted during the maximum update interval. m_maximumUpdateIntervalTimer->start(); } } void KFileItemModel::slotItemsDeleted(const KFileItemList& items) { dispatchPendingItemsToInsert(); QVector indexesToRemove; indexesToRemove.reserve(items.count()); foreach (const KFileItem& item, items) { const int indexForItem = index(item); if (indexForItem >= 0) { indexesToRemove.append(indexForItem); } else { // Probably the item has been filtered. QHash::iterator it = m_filteredItems.find(item); if (it != m_filteredItems.end()) { delete it.value(); m_filteredItems.erase(it); } } } std::sort(indexesToRemove.begin(), indexesToRemove.end()); if (m_requestRole[ExpandedParentsCountRole] && !m_expandedDirs.isEmpty()) { // Assure that removing a parent item also results in removing all children QVector indexesToRemoveWithChildren; indexesToRemoveWithChildren.reserve(m_itemData.count()); const int itemCount = m_itemData.count(); foreach (int index, indexesToRemove) { indexesToRemoveWithChildren.append(index); const int parentLevel = expandedParentsCount(index); int childIndex = index + 1; while (childIndex < itemCount && expandedParentsCount(childIndex) > parentLevel) { indexesToRemoveWithChildren.append(childIndex); ++childIndex; } } indexesToRemove = indexesToRemoveWithChildren; } const KItemRangeList itemRanges = KItemRangeList::fromSortedContainer(indexesToRemove); removeFilteredChildren(itemRanges); removeItems(itemRanges, DeleteItemData); } void KFileItemModel::slotRefreshItems(const QList >& items) { Q_ASSERT(!items.isEmpty()); #ifdef KFILEITEMMODEL_DEBUG qCDebug(DolphinDebug) << "Refreshing" << items.count() << "items"; #endif // Get the indexes of all items that have been refreshed QList indexes; indexes.reserve(items.count()); QSet changedRoles; QListIterator > it(items); while (it.hasNext()) { const QPair& itemPair = it.next(); const KFileItem& oldItem = itemPair.first; const KFileItem& newItem = itemPair.second; const int indexForItem = index(oldItem); if (indexForItem >= 0) { m_itemData[indexForItem]->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, m_itemData.at(indexForItem)->parent)); QHash& values = m_itemData[indexForItem]->values; while (it.hasNext()) { it.next(); const QByteArray& role = it.key(); if (values.value(role) != it.value()) { values.insert(role, it.value()); changedRoles.insert(role); } } m_items.remove(oldItem.url()); m_items.insert(newItem.url(), indexForItem); indexes.append(indexForItem); } else { // Check if 'oldItem' is one of the filtered items. QHash::iterator it = m_filteredItems.find(oldItem); if (it != m_filteredItems.end()) { ItemData* itemData = it.value(); itemData->item = newItem; // The data stored in 'values' might have changed. Therefore, we clear // 'values' and re-populate it the next time it is requested via data(int). itemData->values.clear(); m_filteredItems.erase(it); m_filteredItems.insert(newItem, itemData); } } } // 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); const KItemRangeList itemRangeList = KItemRangeList::fromSortedContainer(indexes); emitItemsChangedAndTriggerResorting(itemRangeList, changedRoles); } void KFileItemModel::slotClear() { #ifdef KFILEITEMMODEL_DEBUG qCDebug(DolphinDebug) << "Clearing all items"; #endif qDeleteAll(m_filteredItems); m_filteredItems.clear(); m_groups.clear(); m_maximumUpdateIntervalTimer->stop(); m_resortAllItemsTimer->stop(); qDeleteAll(m_pendingItemsToInsert); m_pendingItemsToInsert.clear(); const int removedCount = m_itemData.count(); if (removedCount > 0) { qDeleteAll(m_itemData); m_itemData.clear(); m_items.clear(); emit itemsRemoved(KItemRangeList() << KItemRange(0, removedCount)); } m_expandedDirs.clear(); } void KFileItemModel::slotSortingChoiceChanged() { loadSortingSettings(); resortAllItems(); } void KFileItemModel::dispatchPendingItemsToInsert() { if (!m_pendingItemsToInsert.isEmpty()) { insertItems(m_pendingItemsToInsert); m_pendingItemsToInsert.clear(); } } void KFileItemModel::insertItems(QList& newItems) { if (newItems.isEmpty()) { return; } #ifdef KFILEITEMMODEL_DEBUG QElapsedTimer timer; timer.start(); qCDebug(DolphinDebug) << "==========================================================="; qCDebug(DolphinDebug) << "Inserting" << newItems.count() << "items"; #endif m_groups.clear(); prepareItemsForSorting(newItems); if (m_sortRole == NameRole && m_naturalSorting) { // Natural sorting of items can be very slow. However, it becomes much // faster if the input sequence is already mostly sorted. Therefore, we // first sort 'newItems' according to the QStrings returned by // KFileItem::text() using QString::operator<(), which is quite fast. parallelMergeSort(newItems.begin(), newItems.end(), nameLessThan, QThread::idealThreadCount()); } sort(newItems.begin(), newItems.end()); #ifdef KFILEITEMMODEL_DEBUG qCDebug(DolphinDebug) << "[TIME] Sorting:" << timer.elapsed(); #endif KItemRangeList itemRanges; const int existingItemCount = m_itemData.count(); const int newItemCount = newItems.count(); const int totalItemCount = existingItemCount + newItemCount; if (existingItemCount == 0) { // Optimization for the common special case that there are no // items in the model yet. Happens, e.g., when entering a folder. m_itemData = newItems; itemRanges << KItemRange(0, newItemCount); } else { m_itemData.reserve(totalItemCount); for (int i = existingItemCount; i < totalItemCount; ++i) { - m_itemData.append(0); + m_itemData.append(nullptr); } // We build the new list m_itemData in reverse order to minimize // the number of moves and guarantee O(N) complexity. int targetIndex = totalItemCount - 1; int sourceIndexExistingItems = existingItemCount - 1; int sourceIndexNewItems = newItemCount - 1; int rangeCount = 0; while (sourceIndexNewItems >= 0) { ItemData* newItem = newItems.at(sourceIndexNewItems); if (sourceIndexExistingItems >= 0 && lessThan(newItem, m_itemData.at(sourceIndexExistingItems), m_collator)) { // Move an existing item to its new position. If any new items // are behind it, push the item range to itemRanges. if (rangeCount > 0) { itemRanges << KItemRange(sourceIndexExistingItems + 1, rangeCount); rangeCount = 0; } m_itemData[targetIndex] = m_itemData.at(sourceIndexExistingItems); --sourceIndexExistingItems; } else { // Insert a new item into the list. ++rangeCount; m_itemData[targetIndex] = newItem; --sourceIndexNewItems; } --targetIndex; } // Push the final item range to itemRanges. if (rangeCount > 0) { itemRanges << KItemRange(sourceIndexExistingItems + 1, rangeCount); } // Note that itemRanges is still sorted in reverse order. std::reverse(itemRanges.begin(), itemRanges.end()); } // The indexes in m_items are not correct anymore. Therefore, we clear m_items. // It will be re-populated with the updated indices if index(const QUrl&) is called. m_items.clear(); emit itemsInserted(itemRanges); #ifdef KFILEITEMMODEL_DEBUG qCDebug(DolphinDebug) << "[TIME] Inserting of" << newItems.count() << "items:" << timer.elapsed(); #endif } void KFileItemModel::removeItems(const KItemRangeList& itemRanges, RemoveItemsBehavior behavior) { if (itemRanges.isEmpty()) { return; } m_groups.clear(); // Step 1: Remove the items from m_itemData, and free the ItemData. int removedItemsCount = 0; foreach (const KItemRange& range, itemRanges) { removedItemsCount += range.count; for (int index = range.index; index < range.index + range.count; ++index) { if (behavior == DeleteItemData) { delete m_itemData.at(index); } - m_itemData[index] = 0; + m_itemData[index] = nullptr; } } // Step 2: Remove the ItemData pointers from the list m_itemData. int target = itemRanges.at(0).index; int source = itemRanges.at(0).index + itemRanges.at(0).count; int nextRange = 1; const int oldItemDataCount = m_itemData.count(); while (source < oldItemDataCount) { m_itemData[target] = m_itemData[source]; ++target; ++source; if (nextRange < itemRanges.count() && source == itemRanges.at(nextRange).index) { // Skip the items in the next removed range. source += itemRanges.at(nextRange).count; ++nextRange; } } m_itemData.erase(m_itemData.end() - removedItemsCount, m_itemData.end()); // The indexes in m_items are not correct anymore. Therefore, we clear m_items. // It will be re-populated with the updated indices if index(const QUrl&) is called. m_items.clear(); emit itemsRemoved(itemRanges); } QList KFileItemModel::createItemDataList(const QUrl& parentUrl, const KFileItemList& items) const { if (m_sortRole == TypeRole) { // Try to resolve the MIME-types synchronously to prevent a reordering of // the items when sorting by type (per default MIME-types are resolved // asynchronously by KFileItemModelRolesUpdater). determineMimeTypes(items, 200); } const int parentIndex = index(parentUrl); - ItemData* parentItem = parentIndex < 0 ? 0 : m_itemData.at(parentIndex); + ItemData* parentItem = parentIndex < 0 ? nullptr : m_itemData.at(parentIndex); QList itemDataList; itemDataList.reserve(items.count()); foreach (const KFileItem& item, items) { ItemData* itemData = new ItemData(); itemData->item = item; itemData->parent = parentItem; itemDataList.append(itemData); } return itemDataList; } void KFileItemModel::prepareItemsForSorting(QList& itemDataList) { switch (m_sortRole) { case PermissionsRole: case OwnerRole: case GroupRole: case DestinationRole: case PathRole: case DeletionTimeRole: // These roles can be determined with retrieveData, and they have to be stored // in the QHash "values" for the sorting. foreach (ItemData* itemData, itemDataList) { if (itemData->values.isEmpty()) { itemData->values = retrieveData(itemData->item, itemData->parent); } } break; case TypeRole: // At least store the data including the file type for items with known MIME type. foreach (ItemData* itemData, itemDataList) { if (itemData->values.isEmpty()) { const KFileItem item = itemData->item; if (item.isDir() || item.isMimeTypeKnown()) { itemData->values = retrieveData(itemData->item, itemData->parent); } } } break; default: // The other roles are either resolved by KFileItemModelRolesUpdater // (this includes the SizeRole for directories), or they do not need // to be stored in the QHash "values" for sorting because the data can // be retrieved directly from the KFileItem (NameRole, SizeRole for files, // DateRole). break; } } int KFileItemModel::expandedParentsCount(const ItemData* data) { // The hash 'values' is only guaranteed to contain the key "expandedParentsCount" // if the corresponding item is expanded, and it is not a top-level item. const ItemData* parent = data->parent; if (parent) { if (parent->parent) { Q_ASSERT(parent->values.contains("expandedParentsCount")); return parent->values.value("expandedParentsCount").toInt() + 1; } else { return 1; } } else { return 0; } } void KFileItemModel::removeExpandedItems() { QVector indexesToRemove; const int maxIndex = m_itemData.count() - 1; for (int i = 0; i <= maxIndex; ++i) { const ItemData* itemData = m_itemData.at(i); if (itemData->parent) { indexesToRemove.append(i); } } removeItems(KItemRangeList::fromSortedContainer(indexesToRemove), DeleteItemData); m_expandedDirs.clear(); // Also remove all filtered items which have a parent. QHash::iterator it = m_filteredItems.begin(); const QHash::iterator end = m_filteredItems.end(); while (it != end) { if (it.value()->parent) { delete it.value(); it = m_filteredItems.erase(it); } else { ++it; } } } void KFileItemModel::emitItemsChangedAndTriggerResorting(const KItemRangeList& itemRanges, const QSet& changedRoles) { emit itemsChanged(itemRanges, changedRoles); // Trigger a resorting if necessary. Note that this can happen even if the sort // role has not changed at all because the file name can be used as a fallback. if (changedRoles.contains(sortRole()) || changedRoles.contains(roleForType(NameRole))) { foreach (const KItemRange& range, itemRanges) { bool needsResorting = false; const int first = range.index; const int last = range.index + range.count - 1; // Resorting the model is necessary if // (a) The first item in the range is "lessThan" its predecessor, // (b) the successor of the last item is "lessThan" the last item, or // (c) the internal order of the items in the range is incorrect. if (first > 0 && lessThan(m_itemData.at(first), m_itemData.at(first - 1), m_collator)) { needsResorting = true; } else if (last < count() - 1 && lessThan(m_itemData.at(last + 1), m_itemData.at(last), m_collator)) { needsResorting = true; } else { for (int index = first; index < last; ++index) { if (lessThan(m_itemData.at(index + 1), m_itemData.at(index), m_collator)) { needsResorting = true; break; } } } if (needsResorting) { m_resortAllItemsTimer->start(); return; } } } if (groupedSorting() && changedRoles.contains(sortRole())) { // The position is still correct, but the groups might have changed // if the changed item is either the first or the last item in a // group. // In principle, we could try to find out if the item really is the // first or last one in its group and then update the groups // (possibly with a delayed timer to make sure that we don't // re-calculate the groups very often if items are updated one by // one), but starting m_resortAllItemsTimer is easier. m_resortAllItemsTimer->start(); } } void KFileItemModel::resetRoles() { for (int i = 0; i < RolesCount; ++i) { m_requestRole[i] = false; } } KFileItemModel::RoleType KFileItemModel::typeForRole(const QByteArray& role) 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].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("isHidden", IsHiddenRole); roles.insert("isExpanded", IsExpandedRole); roles.insert("isExpandable", IsExpandableRole); roles.insert("expandedParentsCount", ExpandedParentsCountRole); Q_ASSERT(roles.count() == RolesCount); } 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(IsHiddenRole, "isHidden"); 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 ItemData* parent) const { // It is important to insert only roles that are fast to retrieve. E.g. // KFileItem::iconName() can be very expensive if the MIME-type is unknown // and hence will be retrieved asynchronously by KFileItemModelRolesUpdater. QHash data; data.insert(sharedValue("url"), item.url()); const bool isDir = item.isDir(); if (m_requestRole[IsDirRole] && isDir) { data.insert(sharedValue("isDir"), true); } if (m_requestRole[IsLinkRole] && item.isLink()) { data.insert(sharedValue("isLink"), true); } if (m_requestRole[IsHiddenRole] && item.isHidden()) { data.insert(sharedValue("isHidden"), true); } if (m_requestRole[NameRole]) { data.insert(sharedValue("text"), item.text()); } if (m_requestRole[SizeRole] && !isDir) { data.insert(sharedValue("size"), item.size()); } if (m_requestRole[ModificationTimeRole]) { // Don't use KFileItem::timeString() as this is too expensive when // having several thousands of items. Instead the formatting of the // date-time will be done on-demand by the view when the date will be shown. const QDateTime dateTime = item.time(KFileItem::ModificationTime); data.insert(sharedValue("modificationtime"), dateTime); } if (m_requestRole[CreationTimeRole]) { // Don't use KFileItem::timeString() as this is too expensive when // having several thousands of items. Instead the formatting of the // date-time will be done on-demand by the view when the date will be shown. const QDateTime dateTime = item.time(KFileItem::CreationTime); data.insert(sharedValue("creationtime"), dateTime); } if (m_requestRole[AccessTimeRole]) { // Don't use KFileItem::timeString() as this is too expensive when // having several thousands of items. Instead the formatting of the // date-time will be done on-demand by the view when the date will be shown. const QDateTime dateTime = item.time(KFileItem::AccessTime); data.insert(sharedValue("accesstime"), dateTime); } if (m_requestRole[PermissionsRole]) { data.insert(sharedValue("permissions"), item.permissionsString()); } if (m_requestRole[OwnerRole]) { data.insert(sharedValue("owner"), item.user()); } if (m_requestRole[GroupRole]) { data.insert(sharedValue("group"), item.group()); } if (m_requestRole[DestinationRole]) { QString destination = item.linkDest(); if (destination.isEmpty()) { destination = QStringLiteral("-"); } data.insert(sharedValue("destination"), destination); } if (m_requestRole[PathRole]) { QString path; if (item.url().scheme() == 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(sharedValue("path"), path); } if (m_requestRole[DeletionTimeRole]) { QDateTime deletionTime; if (item.url().scheme() == QLatin1String("trash")) { deletionTime = QDateTime::fromString(item.entry().stringValue(KIO::UDSEntry::UDS_EXTRA + 1), Qt::ISODate); } data.insert(sharedValue("deletiontime"), deletionTime); } if (m_requestRole[IsExpandableRole] && isDir) { data.insert(sharedValue("isExpandable"), true); } if (m_requestRole[ExpandedParentsCountRole]) { if (parent) { const int level = expandedParentsCount(parent) + 1; data.insert(sharedValue("expandedParentsCount"), level); } } if (item.isMimeTypeKnown()) { data.insert(sharedValue("iconName"), item.iconName()); if (m_requestRole[TypeRole]) { data.insert(sharedValue("type"), item.mimeComment()); } } else if (m_requestRole[TypeRole] && isDir) { static const QString folderMimeType = item.mimeComment(); data.insert(sharedValue("type"), folderMimeType); } return data; } bool KFileItemModel::lessThan(const ItemData* a, const ItemData* b, const QCollator& collator) const { int result = 0; if (a->parent != b->parent) { const int expansionLevelA = expandedParentsCount(a); const int expansionLevelB = expandedParentsCount(b); // If b has a higher expansion level than a, check if a is a parent // of b, and make sure that both expansion levels are equal otherwise. for (int i = expansionLevelB; i > expansionLevelA; --i) { if (b->parent == a) { return true; } b = b->parent; } // If a has a higher expansion level than a, check if b is a parent // of a, and make sure that both expansion levels are equal otherwise. for (int i = expansionLevelA; i > expansionLevelB; --i) { if (a->parent == b) { return false; } a = a->parent; } Q_ASSERT(expandedParentsCount(a) == expandedParentsCount(b)); // Compare the last parents of a and b which are different. while (a->parent != b->parent) { a = a->parent; b = b->parent; } } 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) { return false; } } result = sortRoleCompare(a, b, collator); return (sortOrder() == Qt::AscendingOrder) ? result < 0 : result > 0; } /** * Helper class for KFileItemModel::sort(). */ class KFileItemModelLessThan { public: KFileItemModelLessThan(const KFileItemModel* model, const QCollator& collator) : m_model(model), m_collator(collator) { } KFileItemModelLessThan(const KFileItemModelLessThan& other) : m_model(other.m_model), m_collator() { m_collator.setCaseSensitivity(other.m_collator.caseSensitivity()); m_collator.setIgnorePunctuation(other.m_collator.ignorePunctuation()); m_collator.setLocale(other.m_collator.locale()); m_collator.setNumericMode(other.m_collator.numericMode()); } ~KFileItemModelLessThan() = default; //We do not delete m_model as the pointer was passed from outside ant it will be deleted elsewhere. KFileItemModelLessThan& operator=(const KFileItemModelLessThan& other) { m_model = other.m_model; m_collator = other.m_collator; return *this; } bool operator()(const KFileItemModel::ItemData* a, const KFileItemModel::ItemData* b) const { return m_model->lessThan(a, b, m_collator); } private: const KFileItemModel* m_model; QCollator m_collator; }; void KFileItemModel::sort(QList::iterator begin, QList::iterator end) const { KFileItemModelLessThan lessThan(this, m_collator); if (m_sortRole == NameRole) { // Sorting by name can be expensive, in particular if natural sorting is // enabled. Use all CPU cores to speed up the sorting process. static const int numberOfThreads = QThread::idealThreadCount(); parallelMergeSort(begin, end, lessThan, numberOfThreads); } else { // Sorting by other roles is quite fast. Use only one thread to prevent // problems caused by non-reentrant comparison functions, see // https://bugs.kde.org/show_bug.cgi?id=312679 mergeSort(begin, end, lessThan); } } int KFileItemModel::sortRoleCompare(const ItemData* a, const ItemData* b, const QCollator& collator) const { const KFileItem& itemA = a->item; const KFileItem& itemB = b->item; int result = 0; switch (m_sortRole) { 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 ModificationTimeRole: { const QDateTime dateTimeA = itemA.time(KFileItem::ModificationTime); const QDateTime dateTimeB = itemB.time(KFileItem::ModificationTime); if (dateTimeA < dateTimeB) { result = -1; } else if (dateTimeA > dateTimeB) { result = +1; } break; } case CreationTimeRole: { const QDateTime dateTimeA = itemA.time(KFileItem::CreationTime); const QDateTime dateTimeB = itemB.time(KFileItem::CreationTime); if (dateTimeA < dateTimeB) { result = -1; } else if (dateTimeA > dateTimeB) { result = +1; } break; } case DeletionTimeRole: { const QDateTime dateTimeA = a->values.value("deletiontime").toDateTime(); const QDateTime dateTimeB = b->values.value("deletiontime").toDateTime(); if (dateTimeA < dateTimeB) { result = -1; } else if (dateTimeA > dateTimeB) { result = +1; } break; } case RatingRole: case WidthRole: case HeightRole: case WordCountRole: case LineCountRole: case TrackRole: case ReleaseYearRole: { result = a->values.value(roleForType(m_sortRole)).toInt() - b->values.value(roleForType(m_sortRole)).toInt(); break; } default: { const QByteArray role = roleForType(m_sortRole); result = QString::compare(a->values.value(role).toString(), b->values.value(role).toString()); break; } } if (result != 0) { // The current sort role was sufficient to define an order return result; } // Fallback #1: Compare the text of the items result = stringCompare(itemA.text(), itemB.text(), collator); if (result != 0) { return result; } // Fallback #2: KFileItem::text() may not be unique in case UDS_DISPLAY_NAME is used result = stringCompare(itemA.name(), itemB.name(), collator); 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 QCollator& collator) const { if (m_naturalSorting) { return collator.compare(a, b); } const int result = QString::compare(a, b, collator.caseSensitivity()); if (result != 0 || collator.caseSensitivity() == Qt::CaseSensitive) { // Only return the result, if the strings are not equal. If they are equal by a case insensitive // comparison, still a deterministic sort order is required. A case sensitive // comparison is done as fallback. return result; } return QString::compare(a, b, Qt::CaseSensitive); } bool KFileItemModel::useMaximumUpdateInterval() const { return !m_dirLister->url().isLocalFile(); } QList > KFileItemModel::nameRoleGroups() const { Q_ASSERT(!m_itemData.isEmpty()); const int maxIndex = count() - 1; QList > groups; QString groupValue; QChar firstChar; for (int i = 0; i <= maxIndex; ++i) { if (isChildItem(i)) { continue; } const QString name = m_itemData.at(i)->item.text(); // 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).toUpper(); } if (firstChar != newFirstChar) { QString newGroupValue; if (newFirstChar.isLetter()) { // Try to find a matching group in the range 'A' to 'Z'. static std::vector lettersAtoZ; lettersAtoZ.reserve('Z' - 'A' + 1); if (lettersAtoZ.empty()) { for (char c = 'A'; c <= 'Z'; ++c) { lettersAtoZ.push_back(QLatin1Char(c)); } } auto localeAwareLessThan = [this](QChar c1, QChar c2) -> bool { return m_collator.compare(c1, c2) < 0; }; std::vector::iterator it = std::lower_bound(lettersAtoZ.begin(), lettersAtoZ.end(), newFirstChar, localeAwareLessThan); if (it != lettersAtoZ.end()) { if (localeAwareLessThan(newFirstChar, *it) && it != lettersAtoZ.begin()) { // newFirstChar belongs to the group preceding *it. // Example: for an umlaut 'A' in the German locale, *it would be 'B' now. --it; } newGroupValue = *it; } else { newGroupValue = newFirstChar; } } else if (newFirstChar >= QLatin1Char('0') && newFirstChar <= QLatin1Char('9')) { // Apply group '0 - 9' for any name that starts with a digit newGroupValue = i18nc("@title:group Groups that start with a digit", "0 - 9"); } else { newGroupValue = i18nc("@title:group", "Others"); } 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::timeRoleGroups(std::function fileTimeCb) const { Q_ASSERT(!m_itemData.isEmpty()); const int maxIndex = count() - 1; QList > groups; const QDate currentDate = QDate::currentDate(); QDate previousFileDate; QString groupValue; for (int i = 0; i <= maxIndex; ++i) { if (isChildItem(i)) { continue; } const QDateTime fileTime = fileTimeCb(m_itemData.at(i)); const QDate fileDate = fileTime.date(); if (fileDate == previousFileDate) { // The current item is in the same group as the previous item continue; } previousFileDate = fileDate; const int daysDistance = fileDate.daysTo(currentDate); QString newGroupValue; if (currentDate.year() == fileDate.year() && currentDate.month() == fileDate.month()) { switch (daysDistance / 7) { case 0: switch (daysDistance) { case 0: newGroupValue = i18nc("@title:group Date", "Today"); break; case 1: newGroupValue = i18nc("@title:group Date", "Yesterday"); break; default: newGroupValue = fileTime.toString( i18nc("@title:group Date: The week day name: dddd", "dddd")); newGroupValue = i18nc("Can be used to script translation of \"dddd\"" "with context @title:group Date", "%1", newGroupValue); } break; case 1: newGroupValue = i18nc("@title:group Date", "One Week Ago"); 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() == fileDate.year() && lastMonthDate.month() == fileDate.month()) { if (daysDistance == 1) { newGroupValue = fileTime.toString(i18nc("@title:group Date: " "MMMM is full month name in current locale, and yyyy is " "full year number", "'Yesterday' (MMMM, yyyy)")); newGroupValue = i18nc("Can be used to script translation of " "\"'Yesterday' (MMMM, yyyy)\" with context @title:group Date", "%1", newGroupValue); } else if (daysDistance <= 7) { newGroupValue = fileTime.toString(i18nc("@title:group Date: " "The week day name: dddd, MMMM is full month name " "in current locale, and yyyy is full year number", "dddd (MMMM, yyyy)")); newGroupValue = i18nc("Can be used to script translation of " "\"dddd (MMMM, yyyy)\" with context @title:group Date", "%1", newGroupValue); } else if (daysDistance <= 7 * 2) { newGroupValue = fileTime.toString(i18nc("@title:group Date: " "MMMM is full month name in current locale, and yyyy is " "full year number", "'One Week Ago' (MMMM, yyyy)")); newGroupValue = i18nc("Can be used to script translation of " "\"'One Week Ago' (MMMM, yyyy)\" with context @title:group Date", "%1", newGroupValue); } else if (daysDistance <= 7 * 3) { newGroupValue = fileTime.toString(i18nc("@title:group Date: " "MMMM is full month name in current locale, and yyyy is " "full year number", "'Two Weeks Ago' (MMMM, yyyy)")); newGroupValue = i18nc("Can be used to script translation of " "\"'Two Weeks Ago' (MMMM, yyyy)\" with context @title:group Date", "%1", newGroupValue); } else if (daysDistance <= 7 * 4) { newGroupValue = fileTime.toString(i18nc("@title:group Date: " "MMMM is full month name in current locale, and yyyy is " "full year number", "'Three Weeks Ago' (MMMM, yyyy)")); newGroupValue = i18nc("Can be used to script translation of " "\"'Three Weeks Ago' (MMMM, yyyy)\" with context @title:group Date", "%1", newGroupValue); } else { newGroupValue = fileTime.toString(i18nc("@title:group Date: " "MMMM is full month name in current locale, and yyyy is " "full year number", "'Earlier on' MMMM, yyyy")); newGroupValue = i18nc("Can be used to script translation of " "\"'Earlier on' MMMM, yyyy\" with context @title:group Date", "%1", newGroupValue); } } else { newGroupValue = fileTime.toString(i18nc("@title:group " "The month and year: MMMM is full month name in current locale, " "and yyyy is full year number", "MMMM, yyyy")); newGroupValue = i18nc("Can be used to script translation of " "\"MMMM, yyyy\" with context @title:group Date", "%1", newGroupValue); } } 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().toLocalFile()); // 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 = -1; for (int i = 0; i <= maxIndex; ++i) { if (isChildItem(i)) { continue; } const int newGroupValue = m_itemData.at(i)->values.value("rating", 0).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; bool isFirstGroupValue = true; 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 || isFirstGroupValue) { groupValue = newGroupValue; groups.append(QPair(i, newGroupValue)); isFirstGroupValue = false; } } return groups; } 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 Baloo | requires indexer { nullptr, NoRole, nullptr, nullptr, nullptr, nullptr, false, false }, { "text", NameRole, I18N_NOOP2_NOSTRIP("@label", "Name"), nullptr, nullptr, false, false }, { "size", SizeRole, I18N_NOOP2_NOSTRIP("@label", "Size"), nullptr, nullptr, false, false }, { "modificationtime", ModificationTimeRole, I18N_NOOP2_NOSTRIP("@label", "Modified"), nullptr, nullptr, false, false }, { "creationtime", CreationTimeRole, I18N_NOOP2_NOSTRIP("@label", "Created"), nullptr, nullptr, false, false }, { "accesstime", AccessTimeRole, I18N_NOOP2_NOSTRIP("@label", "Accessed"), nullptr, nullptr, false, false }, { "type", TypeRole, I18N_NOOP2_NOSTRIP("@label", "Type"), nullptr, nullptr, false, false }, { "rating", RatingRole, I18N_NOOP2_NOSTRIP("@label", "Rating"), nullptr, nullptr, true, false }, { "tags", TagsRole, I18N_NOOP2_NOSTRIP("@label", "Tags"), nullptr, nullptr, true, false }, { "comment", CommentRole, I18N_NOOP2_NOSTRIP("@label", "Comment"), nullptr, nullptr, true, false }, { "title", TitleRole, I18N_NOOP2_NOSTRIP("@label", "Title"), I18N_NOOP2_NOSTRIP("@label", "Document"), true, true }, { "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 }, { "imageDateTime", ImageDateTimeRole, I18N_NOOP2_NOSTRIP("@label", "Date Photographed"), I18N_NOOP2_NOSTRIP("@label", "Image"), true, true }, { "width", WidthRole, I18N_NOOP2_NOSTRIP("@label", "Width"), I18N_NOOP2_NOSTRIP("@label", "Image"), true, true }, { "height", HeightRole, I18N_NOOP2_NOSTRIP("@label", "Height"), 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 }, { "genre", GenreRole, I18N_NOOP2_NOSTRIP("@label", "Genre"), 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 }, { "bitrate", BitrateRole, I18N_NOOP2_NOSTRIP("@label", "Bitrate"), I18N_NOOP2_NOSTRIP("@label", "Audio"), true, true }, { "track", TrackRole, I18N_NOOP2_NOSTRIP("@label", "Track"), I18N_NOOP2_NOSTRIP("@label", "Audio"), true, true }, { "releaseYear", ReleaseYearRole, I18N_NOOP2_NOSTRIP("@label", "Release Year"), I18N_NOOP2_NOSTRIP("@label", "Audio"), true, true }, { "path", PathRole, I18N_NOOP2_NOSTRIP("@label", "Path"), I18N_NOOP2_NOSTRIP("@label", "Other"), false, false }, { "deletiontime",DeletionTimeRole,I18N_NOOP2_NOSTRIP("@label", "Deletion Time"), I18N_NOOP2_NOSTRIP("@label", "Other"), false, false }, { "destination", DestinationRole, I18N_NOOP2_NOSTRIP("@label", "Link Destination"), I18N_NOOP2_NOSTRIP("@label", "Other"), false, false }, { "originUrl", OriginUrlRole, I18N_NOOP2_NOSTRIP("@label", "Downloaded From"), I18N_NOOP2_NOSTRIP("@label", "Other"), true, false }, { "permissions", PermissionsRole, I18N_NOOP2_NOSTRIP("@label", "Permissions"), I18N_NOOP2_NOSTRIP("@label", "Other"), false, false }, { "owner", OwnerRole, I18N_NOOP2_NOSTRIP("@label", "Owner"), I18N_NOOP2_NOSTRIP("@label", "Other"), false, false }, { "group", GroupRole, I18N_NOOP2_NOSTRIP("@label", "User Group"), I18N_NOOP2_NOSTRIP("@label", "Other"), false, false }, }; count = sizeof(rolesInfoMap) / sizeof(RoleInfoMap); return rolesInfoMap; } void KFileItemModel::determineMimeTypes(const KFileItemList& items, int timeout) { QElapsedTimer timer; timer.start(); foreach (const KFileItem& item, items) { // krazy:exclude=foreach // Only determine mime types for files here. For directories, // KFileItem::determineMimeType() reads the .directory file inside to // load the icon, but this is not necessary at all if we just need the // type. Some special code for setting the correct mime type for // directories is in retrieveData(). if (!item.isDir()) { item.determineMimeType(); } if (timer.elapsed() > timeout) { // Don't block the user interface, let the remaining items // be resolved asynchronously. return; } } } QByteArray KFileItemModel::sharedValue(const QByteArray& value) { static QSet pool; const QSet::const_iterator it = pool.constFind(value); if (it != pool.constEnd()) { return *it; } else { pool.insert(value); return value; } } bool KFileItemModel::isConsistent() const { // m_items may contain less items than m_itemData because m_items // is populated lazily, see KFileItemModel::index(const QUrl& url). if (m_items.count() > m_itemData.count()) { return false; } for (int i = 0, iMax = count(); i < iMax; ++i) { // Check if m_items and m_itemData are consistent. const KFileItem item = fileItem(i); if (item.isNull()) { qCWarning(DolphinDebug) << "Item" << i << "is null"; return false; } const int itemIndex = index(item); if (itemIndex != i) { qCWarning(DolphinDebug) << "Item" << i << "has a wrong index:" << itemIndex; return false; } // Check if the items are sorted correctly. if (i > 0 && !lessThan(m_itemData.at(i - 1), m_itemData.at(i), m_collator)) { qCWarning(DolphinDebug) << "The order of items" << i - 1 << "and" << i << "is wrong:" << fileItem(i - 1) << fileItem(i); return false; } // Check if all parent-child relationships are consistent. const ItemData* data = m_itemData.at(i); const ItemData* parent = data->parent; if (parent) { if (expandedParentsCount(data) != expandedParentsCount(parent) + 1) { qCWarning(DolphinDebug) << "expandedParentsCount is inconsistent for parent" << parent->item << "and child" << data->item; return false; } const int parentIndex = index(parent->item); if (parentIndex >= i) { qCWarning(DolphinDebug) << "Index" << parentIndex << "of parent" << parent->item << "is not smaller than index" << i << "of child" << data->item; return false; } } } return true; } diff --git a/src/kitemviews/kitemlistview.cpp b/src/kitemviews/kitemlistview.cpp index f0647fb3e..316daa88d 100644 --- a/src/kitemviews/kitemlistview.cpp +++ b/src/kitemviews/kitemlistview.cpp @@ -1,2744 +1,2744 @@ /*************************************************************************** * Copyright (C) 2011 by Peter Penz * * * * Based on the Itemviews NG project from Trolltech Labs: * * http://qt.gitorious.org/qt-labs/itemviews-ng * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "kitemlistview.h" #include "dolphindebug.h" #include "kitemlistcontainer.h" #include "kitemlistcontroller.h" #include "kitemlistheader.h" #include "kitemlistselectionmanager.h" #include "kitemlistviewaccessible.h" #include "kstandarditemlistwidget.h" #include "private/kitemlistheaderwidget.h" #include "private/kitemlistrubberband.h" #include "private/kitemlistsizehintresolver.h" #include "private/kitemlistviewlayouter.h" #include #include #include #include #include namespace { // Time in ms until reaching the autoscroll margin triggers // an initial autoscrolling const int InitialAutoScrollDelay = 700; // Delay in ms for triggering the next autoscroll const int RepeatingAutoScrollDelay = 1000 / 60; } #ifndef QT_NO_ACCESSIBILITY QAccessibleInterface* accessibleInterfaceFactory(const QString& key, QObject* object) { Q_UNUSED(key) if (KItemListContainer* container = qobject_cast(object)) { return new KItemListContainerAccessible(container); } else if (KItemListView* view = qobject_cast(object)) { return new KItemListViewAccessible(view); } return nullptr; } #endif KItemListView::KItemListView(QGraphicsWidget* parent) : QGraphicsWidget(parent), m_enabledSelectionToggles(false), m_grouped(false), m_supportsItemExpanding(false), m_editingRole(false), m_activeTransactions(0), m_endTransactionAnimationHint(Animation), m_itemSize(), m_controller(nullptr), m_model(nullptr), m_visibleRoles(), m_widgetCreator(nullptr), m_groupHeaderCreator(nullptr), m_styleOption(), m_visibleItems(), m_visibleGroups(), m_visibleCells(), m_sizeHintResolver(nullptr), m_layouter(nullptr), m_animation(nullptr), m_layoutTimer(nullptr), m_oldScrollOffset(0), m_oldMaximumScrollOffset(0), m_oldItemOffset(0), m_oldMaximumItemOffset(0), m_skipAutoScrollForRubberBand(false), m_rubberBand(nullptr), m_mousePos(), m_autoScrollIncrement(0), m_autoScrollTimer(nullptr), m_header(nullptr), m_headerWidget(nullptr), m_dropIndicator() { setAcceptHoverEvents(true); m_sizeHintResolver = new KItemListSizeHintResolver(this); m_layouter = new KItemListViewLayouter(m_sizeHintResolver, this); m_animation = new KItemListViewAnimation(this); connect(m_animation, &KItemListViewAnimation::finished, this, &KItemListView::slotAnimationFinished); m_layoutTimer = new QTimer(this); m_layoutTimer->setInterval(300); m_layoutTimer->setSingleShot(true); connect(m_layoutTimer, &QTimer::timeout, this, &KItemListView::slotLayoutTimerFinished); m_rubberBand = new KItemListRubberBand(this); connect(m_rubberBand, &KItemListRubberBand::activationChanged, this, &KItemListView::slotRubberBandActivationChanged); m_headerWidget = new KItemListHeaderWidget(this); m_headerWidget->setVisible(false); m_header = new KItemListHeader(this); #ifndef QT_NO_ACCESSIBILITY QAccessible::installFactory(accessibleInterfaceFactory); #endif } KItemListView::~KItemListView() { // The group headers are children of the widgets created by // widgetCreator(). So it is mandatory to delete the group headers // first. delete m_groupHeaderCreator; m_groupHeaderCreator = nullptr; delete m_widgetCreator; m_widgetCreator = nullptr; delete m_sizeHintResolver; m_sizeHintResolver = nullptr; } void KItemListView::setScrollOffset(qreal offset) { if (offset < 0) { offset = 0; } const qreal previousOffset = m_layouter->scrollOffset(); if (offset == previousOffset) { return; } m_layouter->setScrollOffset(offset); m_animation->setScrollOffset(offset); // Don't check whether the m_layoutTimer is active: Changing the // scroll offset must always trigger a synchronous layout, otherwise // the smooth-scrolling might get jerky. doLayout(NoAnimation); onScrollOffsetChanged(offset, previousOffset); } qreal KItemListView::scrollOffset() const { return m_layouter->scrollOffset(); } qreal KItemListView::maximumScrollOffset() const { return m_layouter->maximumScrollOffset(); } void KItemListView::setItemOffset(qreal offset) { if (m_layouter->itemOffset() == offset) { return; } m_layouter->setItemOffset(offset); if (m_headerWidget->isVisible()) { m_headerWidget->setOffset(offset); } // Don't check whether the m_layoutTimer is active: Changing the // item offset must always trigger a synchronous layout, otherwise // the smooth-scrolling might get jerky. doLayout(NoAnimation); } qreal KItemListView::itemOffset() const { return m_layouter->itemOffset(); } qreal KItemListView::maximumItemOffset() const { return m_layouter->maximumItemOffset(); } int KItemListView::maximumVisibleItems() const { return m_layouter->maximumVisibleItems(); } void KItemListView::setVisibleRoles(const QList& roles) { const QList previousRoles = m_visibleRoles; m_visibleRoles = roles; onVisibleRolesChanged(roles, previousRoles); m_sizeHintResolver->clearCache(); m_layouter->markAsDirty(); if (m_itemSize.isEmpty()) { m_headerWidget->setColumns(roles); updatePreferredColumnWidths(); if (!m_headerWidget->automaticColumnResizing()) { // The column-width of new roles are still 0. Apply the preferred // column-width as default with. foreach (const QByteArray& role, m_visibleRoles) { if (m_headerWidget->columnWidth(role) == 0) { const qreal width = m_headerWidget->preferredColumnWidth(role); m_headerWidget->setColumnWidth(role, width); } } applyColumnWidthsFromHeader(); } } const bool alternateBackgroundsChanged = m_itemSize.isEmpty() && ((roles.count() > 1 && previousRoles.count() <= 1) || (roles.count() <= 1 && previousRoles.count() > 1)); QHashIterator it(m_visibleItems); while (it.hasNext()) { it.next(); KItemListWidget* widget = it.value(); widget->setVisibleRoles(roles); if (alternateBackgroundsChanged) { updateAlternateBackgroundForWidget(widget); } } doLayout(NoAnimation); } QList KItemListView::visibleRoles() const { return m_visibleRoles; } void KItemListView::setAutoScroll(bool enabled) { if (enabled && !m_autoScrollTimer) { m_autoScrollTimer = new QTimer(this); m_autoScrollTimer->setSingleShot(true); connect(m_autoScrollTimer, &QTimer::timeout, this, &KItemListView::triggerAutoScrolling); m_autoScrollTimer->start(InitialAutoScrollDelay); } else if (!enabled && m_autoScrollTimer) { delete m_autoScrollTimer; m_autoScrollTimer = nullptr; } } bool KItemListView::autoScroll() const { return m_autoScrollTimer != nullptr; } void KItemListView::setEnabledSelectionToggles(bool enabled) { if (m_enabledSelectionToggles != enabled) { m_enabledSelectionToggles = enabled; QHashIterator it(m_visibleItems); while (it.hasNext()) { it.next(); it.value()->setEnabledSelectionToggle(enabled); } } } bool KItemListView::enabledSelectionToggles() const { return m_enabledSelectionToggles; } KItemListController* KItemListView::controller() const { return m_controller; } KItemModelBase* KItemListView::model() const { return m_model; } void KItemListView::setWidgetCreator(KItemListWidgetCreatorBase* widgetCreator) { delete m_widgetCreator; m_widgetCreator = widgetCreator; } KItemListWidgetCreatorBase* KItemListView::widgetCreator() const { if (!m_widgetCreator) { m_widgetCreator = defaultWidgetCreator(); } return m_widgetCreator; } void KItemListView::setGroupHeaderCreator(KItemListGroupHeaderCreatorBase* groupHeaderCreator) { delete m_groupHeaderCreator; m_groupHeaderCreator = groupHeaderCreator; } KItemListGroupHeaderCreatorBase* KItemListView::groupHeaderCreator() const { if (!m_groupHeaderCreator) { m_groupHeaderCreator = defaultGroupHeaderCreator(); } return m_groupHeaderCreator; } QSizeF KItemListView::itemSize() const { return m_itemSize; } QSizeF KItemListView::itemSizeHint() const { return m_sizeHintResolver->minSizeHint(); } const KItemListStyleOption& KItemListView::styleOption() const { return m_styleOption; } void KItemListView::setGeometry(const QRectF& rect) { QGraphicsWidget::setGeometry(rect); if (!m_model) { return; } const QSizeF newSize = rect.size(); if (m_itemSize.isEmpty()) { m_headerWidget->resize(rect.width(), m_headerWidget->size().height()); if (m_headerWidget->automaticColumnResizing()) { applyAutomaticColumnWidths(); } else { const qreal requiredWidth = columnWidthsSum(); const QSizeF dynamicItemSize(qMax(newSize.width(), requiredWidth), m_itemSize.height()); m_layouter->setItemSize(dynamicItemSize); } // Triggering a synchronous layout is fine from a performance point of view, // as with dynamic item sizes no moving animation must be done. m_layouter->setSize(newSize); doLayout(NoAnimation); } else { const bool animate = !changesItemGridLayout(newSize, m_layouter->itemSize(), m_layouter->itemMargin()); m_layouter->setSize(newSize); if (animate) { // Trigger an asynchronous relayout with m_layoutTimer to prevent // performance bottlenecks. If the timer is exceeded, an animated layout // will be triggered. if (!m_layoutTimer->isActive()) { m_layoutTimer->start(); } } else { m_layoutTimer->stop(); doLayout(NoAnimation); } } } qreal KItemListView::verticalPageStep() const { qreal headerHeight = 0; if (m_headerWidget->isVisible()) { headerHeight = m_headerWidget->size().height(); } return size().height() - headerHeight; } int KItemListView::itemAt(const QPointF& pos) const { QHashIterator it(m_visibleItems); while (it.hasNext()) { it.next(); const KItemListWidget* widget = it.value(); const QPointF mappedPos = widget->mapFromItem(this, pos); if (widget->contains(mappedPos)) { return it.key(); } } return -1; } bool KItemListView::isAboveSelectionToggle(int index, const QPointF& pos) const { if (!m_enabledSelectionToggles) { return false; } const KItemListWidget* widget = m_visibleItems.value(index); if (widget) { const QRectF selectionToggleRect = widget->selectionToggleRect(); if (!selectionToggleRect.isEmpty()) { const QPointF mappedPos = widget->mapFromItem(this, pos); return selectionToggleRect.contains(mappedPos); } } return false; } bool KItemListView::isAboveExpansionToggle(int index, const QPointF& pos) const { const KItemListWidget* widget = m_visibleItems.value(index); if (widget) { const QRectF expansionToggleRect = widget->expansionToggleRect(); if (!expansionToggleRect.isEmpty()) { const QPointF mappedPos = widget->mapFromItem(this, pos); return expansionToggleRect.contains(mappedPos); } } return false; } bool KItemListView::isAboveText(int index, const QPointF &pos) const { const KItemListWidget* widget = m_visibleItems.value(index); if (widget) { const QRectF &textRect = widget->textRect(); if (!textRect.isEmpty()) { const QPointF mappedPos = widget->mapFromItem(this, pos); return textRect.contains(mappedPos); } } return false; } int KItemListView::firstVisibleIndex() const { return m_layouter->firstVisibleIndex(); } int KItemListView::lastVisibleIndex() const { return m_layouter->lastVisibleIndex(); } void KItemListView::calculateItemSizeHints(QVector& logicalHeightHints, qreal& logicalWidthHint) const { widgetCreator()->calculateItemSizeHints(logicalHeightHints, logicalWidthHint, this); } void KItemListView::setSupportsItemExpanding(bool supportsExpanding) { if (m_supportsItemExpanding != supportsExpanding) { m_supportsItemExpanding = supportsExpanding; updateSiblingsInformation(); onSupportsItemExpandingChanged(supportsExpanding); } } bool KItemListView::supportsItemExpanding() const { return m_supportsItemExpanding; } QRectF KItemListView::itemRect(int index) const { return m_layouter->itemRect(index); } QRectF KItemListView::itemContextRect(int index) const { QRectF contextRect; const KItemListWidget* widget = m_visibleItems.value(index); if (widget) { contextRect = widget->iconRect() | widget->textRect(); contextRect.translate(itemRect(index).topLeft()); } return contextRect; } void KItemListView::scrollToItem(int index) { QRectF viewGeometry = geometry(); if (m_headerWidget->isVisible()) { const qreal headerHeight = m_headerWidget->size().height(); viewGeometry.adjust(0, headerHeight, 0, 0); } QRectF currentRect = itemRect(index); // Fix for Bug 311099 - View the underscore when using Ctrl + PagDown currentRect.adjust(-m_styleOption.horizontalMargin, -m_styleOption.verticalMargin, m_styleOption.horizontalMargin, m_styleOption.verticalMargin); if (!viewGeometry.contains(currentRect)) { qreal newOffset = scrollOffset(); if (scrollOrientation() == Qt::Vertical) { if (currentRect.top() < viewGeometry.top()) { newOffset += currentRect.top() - viewGeometry.top(); } else if (currentRect.bottom() > viewGeometry.bottom()) { newOffset += currentRect.bottom() - viewGeometry.bottom(); } } else { if (currentRect.left() < viewGeometry.left()) { newOffset += currentRect.left() - viewGeometry.left(); } else if (currentRect.right() > viewGeometry.right()) { newOffset += currentRect.right() - viewGeometry.right(); } } if (newOffset != scrollOffset()) { emit scrollTo(newOffset); } } } void KItemListView::beginTransaction() { ++m_activeTransactions; if (m_activeTransactions == 1) { onTransactionBegin(); } } void KItemListView::endTransaction() { --m_activeTransactions; if (m_activeTransactions < 0) { m_activeTransactions = 0; qCWarning(DolphinDebug) << "Mismatch between beginTransaction()/endTransaction()"; } if (m_activeTransactions == 0) { onTransactionEnd(); doLayout(m_endTransactionAnimationHint); m_endTransactionAnimationHint = Animation; } } bool KItemListView::isTransactionActive() const { return m_activeTransactions > 0; } void KItemListView::setHeaderVisible(bool visible) { if (visible && !m_headerWidget->isVisible()) { QStyleOptionHeader option; const QSize headerSize = style()->sizeFromContents(QStyle::CT_HeaderSection, &option, QSize()); m_headerWidget->setPos(0, 0); m_headerWidget->resize(size().width(), headerSize.height()); m_headerWidget->setModel(m_model); m_headerWidget->setColumns(m_visibleRoles); m_headerWidget->setZValue(1); connect(m_headerWidget, &KItemListHeaderWidget::columnWidthChanged, this, &KItemListView::slotHeaderColumnWidthChanged); connect(m_headerWidget, &KItemListHeaderWidget::columnMoved, this, &KItemListView::slotHeaderColumnMoved); connect(m_headerWidget, &KItemListHeaderWidget::sortOrderChanged, this, &KItemListView::sortOrderChanged); connect(m_headerWidget, &KItemListHeaderWidget::sortRoleChanged, this, &KItemListView::sortRoleChanged); m_layouter->setHeaderHeight(headerSize.height()); m_headerWidget->setVisible(true); } else if (!visible && m_headerWidget->isVisible()) { disconnect(m_headerWidget, &KItemListHeaderWidget::columnWidthChanged, this, &KItemListView::slotHeaderColumnWidthChanged); disconnect(m_headerWidget, &KItemListHeaderWidget::columnMoved, this, &KItemListView::slotHeaderColumnMoved); disconnect(m_headerWidget, &KItemListHeaderWidget::sortOrderChanged, this, &KItemListView::sortOrderChanged); disconnect(m_headerWidget, &KItemListHeaderWidget::sortRoleChanged, this, &KItemListView::sortRoleChanged); m_layouter->setHeaderHeight(0); m_headerWidget->setVisible(false); } } bool KItemListView::isHeaderVisible() const { return m_headerWidget->isVisible(); } KItemListHeader* KItemListView::header() const { return m_header; } QPixmap KItemListView::createDragPixmap(const KItemSet& indexes) const { QPixmap pixmap; if (indexes.count() == 1) { KItemListWidget* item = m_visibleItems.value(indexes.first()); QGraphicsView* graphicsView = scene()->views()[0]; if (item && graphicsView) { pixmap = item->createDragPixmap(nullptr, graphicsView); } } else { // TODO: Not implemented yet. Probably extend the interface // from KItemListWidget::createDragPixmap() to return a pixmap // that can be used for multiple indexes. } return pixmap; } void KItemListView::editRole(int index, const QByteArray& role) { KStandardItemListWidget* widget = qobject_cast(m_visibleItems.value(index)); if (!widget || m_editingRole) { return; } m_editingRole = true; widget->setEditedRole(role); connect(widget, &KItemListWidget::roleEditingCanceled, this, &KItemListView::slotRoleEditingCanceled); connect(widget, &KItemListWidget::roleEditingFinished, this, &KItemListView::slotRoleEditingFinished); connect(this, &KItemListView::scrollOffsetChanged, widget, &KStandardItemListWidget::finishRoleEditing); } void KItemListView::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) { QGraphicsWidget::paint(painter, option, widget); if (m_rubberBand->isActive()) { QRectF rubberBandRect = QRectF(m_rubberBand->startPosition(), m_rubberBand->endPosition()).normalized(); const QPointF topLeft = rubberBandRect.topLeft(); if (scrollOrientation() == Qt::Vertical) { rubberBandRect.moveTo(topLeft.x(), topLeft.y() - scrollOffset()); } else { rubberBandRect.moveTo(topLeft.x() - scrollOffset(), topLeft.y()); } QStyleOptionRubberBand opt; opt.initFrom(widget); opt.shape = QRubberBand::Rectangle; opt.opaque = false; opt.rect = rubberBandRect.toRect(); style()->drawControl(QStyle::CE_RubberBand, &opt, painter); } if (!m_dropIndicator.isEmpty()) { const QRectF r = m_dropIndicator.toRect(); QColor color = palette().brush(QPalette::Normal, QPalette::Highlight).color(); painter->setPen(color); // TODO: The following implementation works only for a vertical scroll-orientation // and assumes a height of the m_draggingInsertIndicator of 1. Q_ASSERT(r.height() == 1); painter->drawLine(r.left() + 1, r.top(), r.right() - 1, r.top()); color.setAlpha(128); painter->setPen(color); painter->drawRect(r.left(), r.top() - 1, r.width() - 1, 2); } } QVariant KItemListView::itemChange(GraphicsItemChange change, const QVariant &value) { if (change == QGraphicsItem::ItemSceneHasChanged && scene()) { if (!scene()->views().isEmpty()) { m_styleOption.palette = scene()->views().at(0)->palette(); } } return QGraphicsItem::itemChange(change, value); } void KItemListView::setItemSize(const QSizeF& size) { const QSizeF previousSize = m_itemSize; if (size == previousSize) { return; } // Skip animations when the number of rows or columns // are changed in the grid layout. Although the animation // engine can handle this usecase, it looks obtrusive. const bool animate = !changesItemGridLayout(m_layouter->size(), size, m_layouter->itemMargin()); const bool alternateBackgroundsChanged = (m_visibleRoles.count() > 1) && (( m_itemSize.isEmpty() && !size.isEmpty()) || (!m_itemSize.isEmpty() && size.isEmpty())); m_itemSize = size; if (alternateBackgroundsChanged) { // For an empty item size alternate backgrounds are drawn if more than // one role is shown. Assure that the backgrounds for visible items are // updated when changing the size in this context. updateAlternateBackgrounds(); } if (size.isEmpty()) { if (m_headerWidget->automaticColumnResizing()) { updatePreferredColumnWidths(); } else { // Only apply the changed height and respect the header widths // set by the user const qreal currentWidth = m_layouter->itemSize().width(); const QSizeF newSize(currentWidth, size.height()); m_layouter->setItemSize(newSize); } } else { m_layouter->setItemSize(size); } m_sizeHintResolver->clearCache(); doLayout(animate ? Animation : NoAnimation); onItemSizeChanged(size, previousSize); } void KItemListView::setStyleOption(const KItemListStyleOption& option) { if (m_styleOption == option) { return; } const KItemListStyleOption previousOption = m_styleOption; m_styleOption = option; bool animate = true; const QSizeF margin(option.horizontalMargin, option.verticalMargin); if (margin != m_layouter->itemMargin()) { // Skip animations when the number of rows or columns // are changed in the grid layout. Although the animation // engine can handle this usecase, it looks obtrusive. animate = !changesItemGridLayout(m_layouter->size(), m_layouter->itemSize(), margin); m_layouter->setItemMargin(margin); } if (m_grouped) { updateGroupHeaderHeight(); } if (animate && (previousOption.maxTextLines != option.maxTextLines || previousOption.maxTextWidth != option.maxTextWidth)) { // Animating a change of the maximum text size just results in expensive // temporary eliding and clipping operations and does not look good visually. animate = false; } QHashIterator it(m_visibleItems); while (it.hasNext()) { it.next(); it.value()->setStyleOption(option); } m_sizeHintResolver->clearCache(); m_layouter->markAsDirty(); doLayout(animate ? Animation : NoAnimation); if (m_itemSize.isEmpty()) { updatePreferredColumnWidths(); } onStyleOptionChanged(option, previousOption); } void KItemListView::setScrollOrientation(Qt::Orientation orientation) { const Qt::Orientation previousOrientation = m_layouter->scrollOrientation(); if (orientation == previousOrientation) { return; } m_layouter->setScrollOrientation(orientation); m_animation->setScrollOrientation(orientation); m_sizeHintResolver->clearCache(); if (m_grouped) { QMutableHashIterator it (m_visibleGroups); while (it.hasNext()) { it.next(); it.value()->setScrollOrientation(orientation); } updateGroupHeaderHeight(); } doLayout(NoAnimation); onScrollOrientationChanged(orientation, previousOrientation); emit scrollOrientationChanged(orientation, previousOrientation); } Qt::Orientation KItemListView::scrollOrientation() const { return m_layouter->scrollOrientation(); } KItemListWidgetCreatorBase* KItemListView::defaultWidgetCreator() const { return nullptr; } KItemListGroupHeaderCreatorBase* KItemListView::defaultGroupHeaderCreator() const { return nullptr; } void KItemListView::initializeItemListWidget(KItemListWidget* item) { Q_UNUSED(item); } bool KItemListView::itemSizeHintUpdateRequired(const QSet& changedRoles) const { Q_UNUSED(changedRoles); return true; } void KItemListView::onControllerChanged(KItemListController* current, KItemListController* previous) { Q_UNUSED(current); Q_UNUSED(previous); } void KItemListView::onModelChanged(KItemModelBase* current, KItemModelBase* previous) { Q_UNUSED(current); Q_UNUSED(previous); } void KItemListView::onScrollOrientationChanged(Qt::Orientation current, Qt::Orientation previous) { Q_UNUSED(current); Q_UNUSED(previous); } void KItemListView::onItemSizeChanged(const QSizeF& current, const QSizeF& previous) { Q_UNUSED(current); Q_UNUSED(previous); } void KItemListView::onScrollOffsetChanged(qreal current, qreal previous) { Q_UNUSED(current); Q_UNUSED(previous); } void KItemListView::onVisibleRolesChanged(const QList& current, const QList& previous) { Q_UNUSED(current); Q_UNUSED(previous); } void KItemListView::onStyleOptionChanged(const KItemListStyleOption& current, const KItemListStyleOption& previous) { Q_UNUSED(current); Q_UNUSED(previous); } void KItemListView::onSupportsItemExpandingChanged(bool supportsExpanding) { Q_UNUSED(supportsExpanding); } void KItemListView::onTransactionBegin() { } void KItemListView::onTransactionEnd() { } bool KItemListView::event(QEvent* event) { switch (event->type()) { case QEvent::PaletteChange: updatePalette(); break; case QEvent::FontChange: updateFont(); break; default: // Forward all other events to the controller and handle them there if (!m_editingRole && m_controller && m_controller->processEvent(event, transform())) { event->accept(); return true; } } return QGraphicsWidget::event(event); } void KItemListView::mousePressEvent(QGraphicsSceneMouseEvent* event) { m_mousePos = transform().map(event->pos()); event->accept(); } void KItemListView::mouseMoveEvent(QGraphicsSceneMouseEvent* event) { QGraphicsWidget::mouseMoveEvent(event); m_mousePos = transform().map(event->pos()); if (m_autoScrollTimer && !m_autoScrollTimer->isActive()) { m_autoScrollTimer->start(InitialAutoScrollDelay); } } void KItemListView::dragEnterEvent(QGraphicsSceneDragDropEvent* event) { event->setAccepted(true); setAutoScroll(true); } void KItemListView::dragMoveEvent(QGraphicsSceneDragDropEvent* event) { QGraphicsWidget::dragMoveEvent(event); m_mousePos = transform().map(event->pos()); if (m_autoScrollTimer && !m_autoScrollTimer->isActive()) { m_autoScrollTimer->start(InitialAutoScrollDelay); } } void KItemListView::dragLeaveEvent(QGraphicsSceneDragDropEvent* event) { QGraphicsWidget::dragLeaveEvent(event); setAutoScroll(false); } void KItemListView::dropEvent(QGraphicsSceneDragDropEvent* event) { QGraphicsWidget::dropEvent(event); setAutoScroll(false); } QList KItemListView::visibleItemListWidgets() const { return m_visibleItems.values(); } void KItemListView::updateFont() { if (scene() && !scene()->views().isEmpty()) { KItemListStyleOption option = styleOption(); option.font = scene()->views().first()->font(); option.fontMetrics = QFontMetrics(option.font); setStyleOption(option); } } void KItemListView::updatePalette() { if (scene() && !scene()->views().isEmpty()) { KItemListStyleOption option = styleOption(); option.palette = scene()->views().first()->palette(); setStyleOption(option); } } void KItemListView::slotItemsInserted(const KItemRangeList& itemRanges) { if (m_itemSize.isEmpty()) { updatePreferredColumnWidths(itemRanges); } const bool hasMultipleRanges = (itemRanges.count() > 1); if (hasMultipleRanges) { beginTransaction(); } m_layouter->markAsDirty(); m_sizeHintResolver->itemsInserted(itemRanges); int previouslyInsertedCount = 0; foreach (const KItemRange& range, itemRanges) { // range.index is related to the model before anything has been inserted. // As in each loop the current item-range gets inserted the index must // be increased by the already previously inserted items. const int index = range.index + previouslyInsertedCount; const int count = range.count; if (index < 0 || count <= 0) { qCWarning(DolphinDebug) << "Invalid item range (index:" << index << ", count:" << count << ")"; continue; } previouslyInsertedCount += count; // Determine which visible items must be moved QList itemsToMove; QHashIterator it(m_visibleItems); while (it.hasNext()) { it.next(); const int visibleItemIndex = it.key(); if (visibleItemIndex >= index) { itemsToMove.append(visibleItemIndex); } } // Update the indexes of all KItemListWidget instances that are located // after the inserted items. It is important to adjust the indexes in the order // from the highest index to the lowest index to prevent overlaps when setting the new index. qSort(itemsToMove); for (int i = itemsToMove.count() - 1; i >= 0; --i) { KItemListWidget* widget = m_visibleItems.value(itemsToMove[i]); Q_ASSERT(widget); const int newIndex = widget->index() + count; if (hasMultipleRanges) { setWidgetIndex(widget, newIndex); } else { // Try to animate the moving of the item moveWidgetToIndex(widget, newIndex); } } if (m_model->count() == count && m_activeTransactions == 0) { // Check whether a scrollbar is required to show the inserted items. In this case // the size of the layouter will be decreased before calling doLayout(): This prevents // an unnecessary temporary animation due to the geometry change of the inserted scrollbar. const bool verticalScrollOrientation = (scrollOrientation() == Qt::Vertical); const bool decreaseLayouterSize = ( verticalScrollOrientation && maximumScrollOffset() > size().height()) || (!verticalScrollOrientation && maximumScrollOffset() > size().width()); if (decreaseLayouterSize) { const int scrollBarExtent = style()->pixelMetric(QStyle::PM_ScrollBarExtent); int scrollbarSpacing = 0; if (style()->styleHint(QStyle::SH_ScrollView_FrameOnlyAroundContents)) { scrollbarSpacing = style()->pixelMetric(QStyle::PM_ScrollView_ScrollBarSpacing); } QSizeF layouterSize = m_layouter->size(); if (verticalScrollOrientation) { layouterSize.rwidth() -= scrollBarExtent + scrollbarSpacing; } else { layouterSize.rheight() -= scrollBarExtent + scrollbarSpacing; } m_layouter->setSize(layouterSize); } } if (!hasMultipleRanges) { doLayout(animateChangedItemCount(count) ? Animation : NoAnimation, index, count); updateSiblingsInformation(); } } if (m_controller) { m_controller->selectionManager()->itemsInserted(itemRanges); } if (hasMultipleRanges) { m_endTransactionAnimationHint = NoAnimation; endTransaction(); updateSiblingsInformation(); } if (m_grouped && (hasMultipleRanges || itemRanges.first().count < m_model->count())) { // In case if items of the same group have been inserted before an item that // currently represents the first item of the group, the group header of // this item must be removed. updateVisibleGroupHeaders(); } if (useAlternateBackgrounds()) { updateAlternateBackgrounds(); } } void KItemListView::slotItemsRemoved(const KItemRangeList& itemRanges) { if (m_itemSize.isEmpty()) { // Don't pass the item-range: The preferred column-widths of // all items must be adjusted when removing items. updatePreferredColumnWidths(); } const bool hasMultipleRanges = (itemRanges.count() > 1); if (hasMultipleRanges) { beginTransaction(); } m_layouter->markAsDirty(); m_sizeHintResolver->itemsRemoved(itemRanges); for (int i = itemRanges.count() - 1; i >= 0; --i) { const KItemRange& range = itemRanges[i]; const int index = range.index; const int count = range.count; if (index < 0 || count <= 0) { qCWarning(DolphinDebug) << "Invalid item range (index:" << index << ", count:" << count << ")"; continue; } const int firstRemovedIndex = index; const int lastRemovedIndex = index + count - 1; // Remember which items have to be moved because they are behind the removed range. QVector itemsToMove; // Remove all KItemListWidget instances that got deleted foreach (KItemListWidget* widget, m_visibleItems) { const int i = widget->index(); if (i < firstRemovedIndex) { continue; } else if (i > lastRemovedIndex) { itemsToMove.append(i); continue; } m_animation->stop(widget); // Stopping the animation might lead to recycling the widget if // it is invisible (see slotAnimationFinished()). // Check again whether it is still visible: if (!m_visibleItems.contains(i)) { continue; } if (m_model->count() == 0 || hasMultipleRanges || !animateChangedItemCount(count)) { // Remove the widget without animation recycleWidget(widget); } else { // Animate the removing of the items. Special case: When removing an item there // is no valid model index available anymore. For the // remove-animation the item gets removed from m_visibleItems but the widget // will stay alive until the animation has been finished and will // be recycled (deleted) in KItemListView::slotAnimationFinished(). m_visibleItems.remove(i); widget->setIndex(-1); m_animation->start(widget, KItemListViewAnimation::DeleteAnimation); } } // Update the indexes of all KItemListWidget instances that are located // after the deleted items. It is important to update them in ascending // order to prevent overlaps when setting the new index. std::sort(itemsToMove.begin(), itemsToMove.end()); foreach (int i, itemsToMove) { KItemListWidget* widget = m_visibleItems.value(i); Q_ASSERT(widget); const int newIndex = i - count; if (hasMultipleRanges) { setWidgetIndex(widget, newIndex); } else { // Try to animate the moving of the item moveWidgetToIndex(widget, newIndex); } } if (!hasMultipleRanges) { // The decrease-layout-size optimization in KItemListView::slotItemsInserted() // assumes an updated geometry. If items are removed during an active transaction, // the transaction will be temporary deactivated so that doLayout() triggers a // geometry update if necessary. const int activeTransactions = m_activeTransactions; m_activeTransactions = 0; doLayout(animateChangedItemCount(count) ? Animation : NoAnimation, index, -count); m_activeTransactions = activeTransactions; updateSiblingsInformation(); } } if (m_controller) { m_controller->selectionManager()->itemsRemoved(itemRanges); } if (hasMultipleRanges) { m_endTransactionAnimationHint = NoAnimation; endTransaction(); updateSiblingsInformation(); } if (m_grouped && (hasMultipleRanges || m_model->count() > 0)) { // In case if the first item of a group has been removed, the group header // must be applied to the next visible item. updateVisibleGroupHeaders(); } if (useAlternateBackgrounds()) { updateAlternateBackgrounds(); } } void KItemListView::slotItemsMoved(const KItemRange& itemRange, const QList& movedToIndexes) { m_sizeHintResolver->itemsMoved(itemRange, movedToIndexes); m_layouter->markAsDirty(); if (m_controller) { m_controller->selectionManager()->itemsMoved(itemRange, movedToIndexes); } const int firstVisibleMovedIndex = qMax(firstVisibleIndex(), itemRange.index); const int lastVisibleMovedIndex = qMin(lastVisibleIndex(), itemRange.index + itemRange.count - 1); for (int index = firstVisibleMovedIndex; index <= lastVisibleMovedIndex; ++index) { KItemListWidget* widget = m_visibleItems.value(index); if (widget) { updateWidgetProperties(widget, index); initializeItemListWidget(widget); } } doLayout(NoAnimation); updateSiblingsInformation(); } void KItemListView::slotItemsChanged(const KItemRangeList& itemRanges, const QSet& roles) { const bool updateSizeHints = itemSizeHintUpdateRequired(roles); if (updateSizeHints && m_itemSize.isEmpty()) { updatePreferredColumnWidths(itemRanges); } foreach (const KItemRange& itemRange, itemRanges) { const int index = itemRange.index; const int count = itemRange.count; if (updateSizeHints) { m_sizeHintResolver->itemsChanged(index, count, roles); m_layouter->markAsDirty(); if (!m_layoutTimer->isActive()) { m_layoutTimer->start(); } } // Apply the changed roles to the visible item-widgets const int lastIndex = index + count - 1; for (int i = index; i <= lastIndex; ++i) { KItemListWidget* widget = m_visibleItems.value(i); if (widget) { widget->setData(m_model->data(i), roles); } } if (m_grouped && roles.contains(m_model->sortRole())) { // The sort-role has been changed which might result // in modified group headers updateVisibleGroupHeaders(); doLayout(NoAnimation); } QAccessibleTableModelChangeEvent ev(this, QAccessibleTableModelChangeEvent::DataChanged); ev.setFirstRow(itemRange.index); ev.setLastRow(itemRange.index + itemRange.count); QAccessible::updateAccessibility(&ev); } } void KItemListView::slotGroupsChanged() { updateVisibleGroupHeaders(); doLayout(NoAnimation); updateSiblingsInformation(); } void KItemListView::slotGroupedSortingChanged(bool current) { m_grouped = current; m_layouter->markAsDirty(); if (m_grouped) { updateGroupHeaderHeight(); } else { // Clear all visible headers. Note that the QHashIterator takes a copy of // m_visibleGroups. Therefore, it remains valid even if items are removed // from m_visibleGroups in recycleGroupHeaderForWidget(). QHashIterator it(m_visibleGroups); while (it.hasNext()) { it.next(); recycleGroupHeaderForWidget(it.key()); } Q_ASSERT(m_visibleGroups.isEmpty()); } if (useAlternateBackgrounds()) { // Changing the group mode requires to update the alternate backgrounds // as with the enabled group mode the altering is done on base of the first // group item. updateAlternateBackgrounds(); } updateSiblingsInformation(); doLayout(NoAnimation); } void KItemListView::slotSortOrderChanged(Qt::SortOrder current, Qt::SortOrder previous) { Q_UNUSED(current); Q_UNUSED(previous); if (m_grouped) { updateVisibleGroupHeaders(); doLayout(NoAnimation); } } void KItemListView::slotSortRoleChanged(const QByteArray& current, const QByteArray& previous) { Q_UNUSED(current); Q_UNUSED(previous); if (m_grouped) { updateVisibleGroupHeaders(); doLayout(NoAnimation); } } void KItemListView::slotCurrentChanged(int current, int previous) { Q_UNUSED(previous); // In SingleSelection mode (e.g., in the Places Panel), the current item is // always the selected item. It is not necessary to highlight the current item then. if (m_controller->selectionBehavior() != KItemListController::SingleSelection) { - KItemListWidget* previousWidget = m_visibleItems.value(previous, 0); + KItemListWidget* previousWidget = m_visibleItems.value(previous, nullptr); if (previousWidget) { previousWidget->setCurrent(false); } - KItemListWidget* currentWidget = m_visibleItems.value(current, 0); + KItemListWidget* currentWidget = m_visibleItems.value(current, nullptr); if (currentWidget) { currentWidget->setCurrent(true); } } QAccessibleEvent ev(this, QAccessible::Focus); ev.setChild(current); QAccessible::updateAccessibility(&ev); } void KItemListView::slotSelectionChanged(const KItemSet& current, const KItemSet& previous) { Q_UNUSED(previous); QHashIterator it(m_visibleItems); while (it.hasNext()) { it.next(); const int index = it.key(); KItemListWidget* widget = it.value(); widget->setSelected(current.contains(index)); } } void KItemListView::slotAnimationFinished(QGraphicsWidget* widget, KItemListViewAnimation::AnimationType type) { KItemListWidget* itemListWidget = qobject_cast(widget); Q_ASSERT(itemListWidget); switch (type) { case KItemListViewAnimation::DeleteAnimation: { // As we recycle the widget in this case it is important to assure that no // other animation has been started. This is a convention in KItemListView and // not a requirement defined by KItemListViewAnimation. Q_ASSERT(!m_animation->isStarted(itemListWidget)); // All KItemListWidgets that are animated by the DeleteAnimation are not maintained // by m_visibleWidgets and must be deleted manually after the animation has // been finished. recycleGroupHeaderForWidget(itemListWidget); widgetCreator()->recycle(itemListWidget); break; } case KItemListViewAnimation::CreateAnimation: case KItemListViewAnimation::MovingAnimation: case KItemListViewAnimation::ResizeAnimation: { const int index = itemListWidget->index(); const bool invisible = (index < m_layouter->firstVisibleIndex()) || (index > m_layouter->lastVisibleIndex()); if (invisible && !m_animation->isStarted(itemListWidget)) { recycleWidget(itemListWidget); } break; } default: break; } } void KItemListView::slotLayoutTimerFinished() { m_layouter->setSize(geometry().size()); doLayout(Animation); } void KItemListView::slotRubberBandPosChanged() { update(); } void KItemListView::slotRubberBandActivationChanged(bool active) { if (active) { connect(m_rubberBand, &KItemListRubberBand::startPositionChanged, this, &KItemListView::slotRubberBandPosChanged); connect(m_rubberBand, &KItemListRubberBand::endPositionChanged, this, &KItemListView::slotRubberBandPosChanged); m_skipAutoScrollForRubberBand = true; } else { disconnect(m_rubberBand, &KItemListRubberBand::startPositionChanged, this, &KItemListView::slotRubberBandPosChanged); disconnect(m_rubberBand, &KItemListRubberBand::endPositionChanged, this, &KItemListView::slotRubberBandPosChanged); m_skipAutoScrollForRubberBand = false; } update(); } void KItemListView::slotHeaderColumnWidthChanged(const QByteArray& role, qreal currentWidth, qreal previousWidth) { Q_UNUSED(role); Q_UNUSED(currentWidth); Q_UNUSED(previousWidth); m_headerWidget->setAutomaticColumnResizing(false); applyColumnWidthsFromHeader(); doLayout(NoAnimation); } void KItemListView::slotHeaderColumnMoved(const QByteArray& role, int currentIndex, int previousIndex) { Q_ASSERT(m_visibleRoles[previousIndex] == role); const QList previous = m_visibleRoles; QList current = m_visibleRoles; current.removeAt(previousIndex); current.insert(currentIndex, role); setVisibleRoles(current); emit visibleRolesChanged(current, previous); } void KItemListView::triggerAutoScrolling() { if (!m_autoScrollTimer) { return; } int pos = 0; int visibleSize = 0; if (scrollOrientation() == Qt::Vertical) { pos = m_mousePos.y(); visibleSize = size().height(); } else { pos = m_mousePos.x(); visibleSize = size().width(); } if (m_autoScrollTimer->interval() == InitialAutoScrollDelay) { m_autoScrollIncrement = 0; } m_autoScrollIncrement = calculateAutoScrollingIncrement(pos, visibleSize, m_autoScrollIncrement); if (m_autoScrollIncrement == 0) { // The mouse position is not above an autoscroll margin (the autoscroll timer // will be restarted in mouseMoveEvent()) m_autoScrollTimer->stop(); return; } if (m_rubberBand->isActive() && m_skipAutoScrollForRubberBand) { // If a rubberband selection is ongoing the autoscrolling may only get triggered // if the direction of the rubberband is similar to the autoscroll direction. This // prevents that starting to create a rubberband within the autoscroll margins starts // an autoscrolling. const qreal minDiff = 4; // Ignore any autoscrolling if the rubberband is very small const qreal diff = (scrollOrientation() == Qt::Vertical) ? m_rubberBand->endPosition().y() - m_rubberBand->startPosition().y() : m_rubberBand->endPosition().x() - m_rubberBand->startPosition().x(); if (qAbs(diff) < minDiff || (m_autoScrollIncrement < 0 && diff > 0) || (m_autoScrollIncrement > 0 && diff < 0)) { // The rubberband direction is different from the scroll direction (e.g. the rubberband has // been moved up although the autoscroll direction might be down) m_autoScrollTimer->stop(); return; } } // As soon as the autoscrolling has been triggered at least once despite having an active rubberband, // the autoscrolling may not get skipped anymore until a new rubberband is created m_skipAutoScrollForRubberBand = false; const qreal maxVisibleOffset = qMax(qreal(0), maximumScrollOffset() - visibleSize); const qreal newScrollOffset = qMin(scrollOffset() + m_autoScrollIncrement, maxVisibleOffset); setScrollOffset(newScrollOffset); // Trigger the autoscroll timer which will periodically call // triggerAutoScrolling() m_autoScrollTimer->start(RepeatingAutoScrollDelay); } void KItemListView::slotGeometryOfGroupHeaderParentChanged() { KItemListWidget* widget = qobject_cast(sender()); Q_ASSERT(widget); KItemListGroupHeader* groupHeader = m_visibleGroups.value(widget); Q_ASSERT(groupHeader); updateGroupHeaderLayout(widget); } void KItemListView::slotRoleEditingCanceled(int index, const QByteArray& role, const QVariant& value) { disconnectRoleEditingSignals(index); emit roleEditingCanceled(index, role, value); m_editingRole = false; } void KItemListView::slotRoleEditingFinished(int index, const QByteArray& role, const QVariant& value) { disconnectRoleEditingSignals(index); emit roleEditingFinished(index, role, value); m_editingRole = false; } void KItemListView::setController(KItemListController* controller) { if (m_controller != controller) { KItemListController* previous = m_controller; if (previous) { KItemListSelectionManager* selectionManager = previous->selectionManager(); disconnect(selectionManager, &KItemListSelectionManager::currentChanged, this, &KItemListView::slotCurrentChanged); disconnect(selectionManager, &KItemListSelectionManager::selectionChanged, this, &KItemListView::slotSelectionChanged); } m_controller = controller; if (controller) { KItemListSelectionManager* selectionManager = controller->selectionManager(); connect(selectionManager, &KItemListSelectionManager::currentChanged, this, &KItemListView::slotCurrentChanged); connect(selectionManager, &KItemListSelectionManager::selectionChanged, this, &KItemListView::slotSelectionChanged); } onControllerChanged(controller, previous); } } void KItemListView::setModel(KItemModelBase* model) { if (m_model == model) { return; } KItemModelBase* previous = m_model; if (m_model) { disconnect(m_model, &KItemModelBase::itemsChanged, this, &KItemListView::slotItemsChanged); disconnect(m_model, &KItemModelBase::itemsInserted, this, &KItemListView::slotItemsInserted); disconnect(m_model, &KItemModelBase::itemsRemoved, this, &KItemListView::slotItemsRemoved); disconnect(m_model, &KItemModelBase::itemsMoved, this, &KItemListView::slotItemsMoved); disconnect(m_model, &KItemModelBase::groupsChanged, this, &KItemListView::slotGroupsChanged); disconnect(m_model, &KItemModelBase::groupedSortingChanged, this, &KItemListView::slotGroupedSortingChanged); disconnect(m_model, &KItemModelBase::sortOrderChanged, this, &KItemListView::slotSortOrderChanged); disconnect(m_model, &KItemModelBase::sortRoleChanged, this, &KItemListView::slotSortRoleChanged); m_sizeHintResolver->itemsRemoved(KItemRangeList() << KItemRange(0, m_model->count())); } m_model = model; m_layouter->setModel(model); m_grouped = model->groupedSorting(); if (m_model) { connect(m_model, &KItemModelBase::itemsChanged, this, &KItemListView::slotItemsChanged); connect(m_model, &KItemModelBase::itemsInserted, this, &KItemListView::slotItemsInserted); connect(m_model, &KItemModelBase::itemsRemoved, this, &KItemListView::slotItemsRemoved); connect(m_model, &KItemModelBase::itemsMoved, this, &KItemListView::slotItemsMoved); connect(m_model, &KItemModelBase::groupsChanged, this, &KItemListView::slotGroupsChanged); connect(m_model, &KItemModelBase::groupedSortingChanged, this, &KItemListView::slotGroupedSortingChanged); connect(m_model, &KItemModelBase::sortOrderChanged, this, &KItemListView::slotSortOrderChanged); connect(m_model, &KItemModelBase::sortRoleChanged, this, &KItemListView::slotSortRoleChanged); const int itemCount = m_model->count(); if (itemCount > 0) { slotItemsInserted(KItemRangeList() << KItemRange(0, itemCount)); } } onModelChanged(model, previous); } KItemListRubberBand* KItemListView::rubberBand() const { return m_rubberBand; } void KItemListView::doLayout(LayoutAnimationHint hint, int changedIndex, int changedCount) { if (m_layoutTimer->isActive()) { m_layoutTimer->stop(); } if (m_activeTransactions > 0) { if (hint == NoAnimation) { // As soon as at least one property change should be done without animation, // the whole transaction will be marked as not animated. m_endTransactionAnimationHint = NoAnimation; } return; } if (!m_model || m_model->count() < 0) { return; } int firstVisibleIndex = m_layouter->firstVisibleIndex(); if (firstVisibleIndex < 0) { emitOffsetChanges(); return; } // Do a sanity check of the scroll-offset property: When properties of the itemlist-view have been changed // it might be possible that the maximum offset got changed too. Assure that the full visible range // is still shown if the maximum offset got decreased. const qreal visibleOffsetRange = (scrollOrientation() == Qt::Horizontal) ? size().width() : size().height(); const qreal maxOffsetToShowFullRange = maximumScrollOffset() - visibleOffsetRange; if (scrollOffset() > maxOffsetToShowFullRange) { m_layouter->setScrollOffset(qMax(qreal(0), maxOffsetToShowFullRange)); firstVisibleIndex = m_layouter->firstVisibleIndex(); } const int lastVisibleIndex = m_layouter->lastVisibleIndex(); int firstSibblingIndex = -1; int lastSibblingIndex = -1; const bool supportsExpanding = supportsItemExpanding(); QList reusableItems = recycleInvisibleItems(firstVisibleIndex, lastVisibleIndex, hint); // Assure that for each visible item a KItemListWidget is available. KItemListWidget // instances from invisible items are reused. If no reusable items are // found then new KItemListWidget instances get created. const bool animate = (hint == Animation); for (int i = firstVisibleIndex; i <= lastVisibleIndex; ++i) { bool applyNewPos = true; bool wasHidden = false; const QRectF itemBounds = m_layouter->itemRect(i); const QPointF newPos = itemBounds.topLeft(); KItemListWidget* widget = m_visibleItems.value(i); if (!widget) { wasHidden = true; if (!reusableItems.isEmpty()) { // Reuse a KItemListWidget instance from an invisible item const int oldIndex = reusableItems.takeLast(); widget = m_visibleItems.value(oldIndex); setWidgetIndex(widget, i); updateWidgetProperties(widget, i); initializeItemListWidget(widget); } else { // No reusable KItemListWidget instance is available, create a new one widget = createWidget(i); } widget->resize(itemBounds.size()); if (animate && changedCount < 0) { // Items have been deleted. if (i >= changedIndex) { // The item is located behind the removed range. Move the // created item to the imaginary old position outside the // view. It will get animated to the new position later. const int previousIndex = i - changedCount; const QRectF itemRect = m_layouter->itemRect(previousIndex); if (itemRect.isEmpty()) { const QPointF invisibleOldPos = (scrollOrientation() == Qt::Vertical) ? QPointF(0, size().height()) : QPointF(size().width(), 0); widget->setPos(invisibleOldPos); } else { widget->setPos(itemRect.topLeft()); } applyNewPos = false; } } if (supportsExpanding && changedCount == 0) { if (firstSibblingIndex < 0) { firstSibblingIndex = i; } lastSibblingIndex = i; } } if (animate) { if (m_animation->isStarted(widget, KItemListViewAnimation::MovingAnimation)) { m_animation->start(widget, KItemListViewAnimation::MovingAnimation, newPos); applyNewPos = false; } const bool itemsRemoved = (changedCount < 0); const bool itemsInserted = (changedCount > 0); if (itemsRemoved && (i >= changedIndex)) { // The item is located after the removed items. Animate the moving of the position. applyNewPos = !moveWidget(widget, newPos); } else if (itemsInserted && i >= changedIndex) { // The item is located after the first inserted item if (i <= changedIndex + changedCount - 1) { // The item is an inserted item. Animate the appearing of the item. // For performance reasons no animation is done when changedCount is equal // to all available items. if (changedCount < m_model->count()) { m_animation->start(widget, KItemListViewAnimation::CreateAnimation); } } else if (!m_animation->isStarted(widget, KItemListViewAnimation::CreateAnimation)) { // The item was already there before, so animate the moving of the position. // No moving animation is done if the item is animated by a create animation: This // prevents a "move animation mess" when inserting several ranges in parallel. applyNewPos = !moveWidget(widget, newPos); } } else if (!itemsRemoved && !itemsInserted && !wasHidden) { // The size of the view might have been changed. Animate the moving of the position. applyNewPos = !moveWidget(widget, newPos); } } else { m_animation->stop(widget); } if (applyNewPos) { widget->setPos(newPos); } Q_ASSERT(widget->index() == i); widget->setVisible(true); if (widget->size() != itemBounds.size()) { // Resize the widget for the item to the changed size. if (animate) { // If a dynamic item size is used then no animation is done in the direction // of the dynamic size. if (m_itemSize.width() <= 0) { // The width is dynamic, apply the new width without animation. widget->resize(itemBounds.width(), widget->size().height()); } else if (m_itemSize.height() <= 0) { // The height is dynamic, apply the new height without animation. widget->resize(widget->size().width(), itemBounds.height()); } m_animation->start(widget, KItemListViewAnimation::ResizeAnimation, itemBounds.size()); } else { widget->resize(itemBounds.size()); } } // Updating the cell-information must be done as last step: The decision whether the // moving-animation should be started at all is based on the previous cell-information. const Cell cell(m_layouter->itemColumn(i), m_layouter->itemRow(i)); m_visibleCells.insert(i, cell); } // Delete invisible KItemListWidget instances that have not been reused foreach (int index, reusableItems) { recycleWidget(m_visibleItems.value(index)); } if (supportsExpanding && firstSibblingIndex >= 0) { Q_ASSERT(lastSibblingIndex >= 0); updateSiblingsInformation(firstSibblingIndex, lastSibblingIndex); } if (m_grouped) { // Update the layout of all visible group headers QHashIterator it(m_visibleGroups); while (it.hasNext()) { it.next(); updateGroupHeaderLayout(it.key()); } } emitOffsetChanges(); } QList KItemListView::recycleInvisibleItems(int firstVisibleIndex, int lastVisibleIndex, LayoutAnimationHint hint) { // Determine all items that are completely invisible and might be // reused for items that just got (at least partly) visible. If the // animation hint is set to 'Animation' items that do e.g. an animated // moving of their position are not marked as invisible: This assures // that a scrolling inside the view can be done without breaking an animation. QList items; QHashIterator it(m_visibleItems); while (it.hasNext()) { it.next(); KItemListWidget* widget = it.value(); const int index = widget->index(); const bool invisible = (index < firstVisibleIndex) || (index > lastVisibleIndex); if (invisible) { if (m_animation->isStarted(widget)) { if (hint == NoAnimation) { // Stopping the animation will call KItemListView::slotAnimationFinished() // and the widget will be recycled if necessary there. m_animation->stop(widget); } } else { widget->setVisible(false); items.append(index); if (m_grouped) { recycleGroupHeaderForWidget(widget); } } } } return items; } bool KItemListView::moveWidget(KItemListWidget* widget,const QPointF& newPos) { if (widget->pos() == newPos) { return false; } bool startMovingAnim = false; if (m_itemSize.isEmpty()) { // The items are not aligned in a grid but either as columns or rows. startMovingAnim = true; } else { // When having a grid the moving-animation should only be started, if it is done within // one row in the vertical scroll-orientation or one column in the horizontal scroll-orientation. // Otherwise instead of a moving-animation a create-animation on the new position will be used // instead. This is done to prevent overlapping (and confusing) moving-animations. const int index = widget->index(); const Cell cell = m_visibleCells.value(index); if (cell.column >= 0 && cell.row >= 0) { if (scrollOrientation() == Qt::Vertical) { startMovingAnim = (cell.row == m_layouter->itemRow(index)); } else { startMovingAnim = (cell.column == m_layouter->itemColumn(index)); } } } if (startMovingAnim) { m_animation->start(widget, KItemListViewAnimation::MovingAnimation, newPos); return true; } m_animation->stop(widget); m_animation->start(widget, KItemListViewAnimation::CreateAnimation); return false; } void KItemListView::emitOffsetChanges() { const qreal newScrollOffset = m_layouter->scrollOffset(); if (m_oldScrollOffset != newScrollOffset) { emit scrollOffsetChanged(newScrollOffset, m_oldScrollOffset); m_oldScrollOffset = newScrollOffset; } const qreal newMaximumScrollOffset = m_layouter->maximumScrollOffset(); if (m_oldMaximumScrollOffset != newMaximumScrollOffset) { emit maximumScrollOffsetChanged(newMaximumScrollOffset, m_oldMaximumScrollOffset); m_oldMaximumScrollOffset = newMaximumScrollOffset; } const qreal newItemOffset = m_layouter->itemOffset(); if (m_oldItemOffset != newItemOffset) { emit itemOffsetChanged(newItemOffset, m_oldItemOffset); m_oldItemOffset = newItemOffset; } const qreal newMaximumItemOffset = m_layouter->maximumItemOffset(); if (m_oldMaximumItemOffset != newMaximumItemOffset) { emit maximumItemOffsetChanged(newMaximumItemOffset, m_oldMaximumItemOffset); m_oldMaximumItemOffset = newMaximumItemOffset; } } KItemListWidget* KItemListView::createWidget(int index) { KItemListWidget* widget = widgetCreator()->create(this); widget->setFlag(QGraphicsItem::ItemStacksBehindParent); m_visibleItems.insert(index, widget); m_visibleCells.insert(index, Cell()); updateWidgetProperties(widget, index); initializeItemListWidget(widget); return widget; } void KItemListView::recycleWidget(KItemListWidget* widget) { if (m_grouped) { recycleGroupHeaderForWidget(widget); } const int index = widget->index(); m_visibleItems.remove(index); m_visibleCells.remove(index); widgetCreator()->recycle(widget); } void KItemListView::setWidgetIndex(KItemListWidget* widget, int index) { const int oldIndex = widget->index(); m_visibleItems.remove(oldIndex); m_visibleCells.remove(oldIndex); m_visibleItems.insert(index, widget); m_visibleCells.insert(index, Cell()); widget->setIndex(index); } void KItemListView::moveWidgetToIndex(KItemListWidget* widget, int index) { const int oldIndex = widget->index(); const Cell oldCell = m_visibleCells.value(oldIndex); setWidgetIndex(widget, index); const Cell newCell(m_layouter->itemColumn(index), m_layouter->itemRow(index)); const bool vertical = (scrollOrientation() == Qt::Vertical); const bool updateCell = (vertical && oldCell.row == newCell.row) || (!vertical && oldCell.column == newCell.column); if (updateCell) { m_visibleCells.insert(index, newCell); } } void KItemListView::setLayouterSize(const QSizeF& size, SizeType sizeType) { switch (sizeType) { case LayouterSize: m_layouter->setSize(size); break; case ItemSize: m_layouter->setItemSize(size); break; default: break; } } void KItemListView::updateWidgetProperties(KItemListWidget* widget, int index) { widget->setVisibleRoles(m_visibleRoles); updateWidgetColumnWidths(widget); widget->setStyleOption(m_styleOption); const KItemListSelectionManager* selectionManager = m_controller->selectionManager(); // In SingleSelection mode (e.g., in the Places Panel), the current item is // always the selected item. It is not necessary to highlight the current item then. if (m_controller->selectionBehavior() != KItemListController::SingleSelection) { widget->setCurrent(index == selectionManager->currentItem()); } widget->setSelected(selectionManager->isSelected(index)); widget->setHovered(false); widget->setEnabledSelectionToggle(enabledSelectionToggles()); widget->setIndex(index); widget->setData(m_model->data(index)); widget->setSiblingsInformation(QBitArray()); updateAlternateBackgroundForWidget(widget); if (m_grouped) { updateGroupHeaderForWidget(widget); } } void KItemListView::updateGroupHeaderForWidget(KItemListWidget* widget) { Q_ASSERT(m_grouped); const int index = widget->index(); if (!m_layouter->isFirstGroupItem(index)) { // The widget does not represent the first item of a group // and hence requires no header recycleGroupHeaderForWidget(widget); return; } const QList > groups = model()->groups(); if (groups.isEmpty() || !groupHeaderCreator()) { return; } KItemListGroupHeader* groupHeader = m_visibleGroups.value(widget); if (!groupHeader) { groupHeader = groupHeaderCreator()->create(this); groupHeader->setParentItem(widget); m_visibleGroups.insert(widget, groupHeader); connect(widget, &KItemListWidget::geometryChanged, this, &KItemListView::slotGeometryOfGroupHeaderParentChanged); } Q_ASSERT(groupHeader->parentItem() == widget); const int groupIndex = groupIndexForItem(index); Q_ASSERT(groupIndex >= 0); groupHeader->setData(groups.at(groupIndex).second); groupHeader->setRole(model()->sortRole()); groupHeader->setStyleOption(m_styleOption); groupHeader->setScrollOrientation(scrollOrientation()); groupHeader->setItemIndex(index); groupHeader->show(); } void KItemListView::updateGroupHeaderLayout(KItemListWidget* widget) { KItemListGroupHeader* groupHeader = m_visibleGroups.value(widget); Q_ASSERT(groupHeader); const int index = widget->index(); const QRectF groupHeaderRect = m_layouter->groupHeaderRect(index); const QRectF itemRect = m_layouter->itemRect(index); // The group-header is a child of the itemlist widget. Translate the // group header position to the relative position. if (scrollOrientation() == Qt::Vertical) { // In the vertical scroll orientation the group header should always span // the whole width no matter which temporary position the parent widget // has. In this case the x-position and width will be adjusted manually. const qreal x = -widget->x() - itemOffset(); const qreal width = maximumItemOffset(); groupHeader->setPos(x, -groupHeaderRect.height()); groupHeader->resize(width, groupHeaderRect.size().height()); } else { groupHeader->setPos(groupHeaderRect.x() - itemRect.x(), -widget->y()); groupHeader->resize(groupHeaderRect.size()); } } void KItemListView::recycleGroupHeaderForWidget(KItemListWidget* widget) { KItemListGroupHeader* header = m_visibleGroups.value(widget); if (header) { header->setParentItem(nullptr); groupHeaderCreator()->recycle(header); m_visibleGroups.remove(widget); disconnect(widget, &KItemListWidget::geometryChanged, this, &KItemListView::slotGeometryOfGroupHeaderParentChanged); } } void KItemListView::updateVisibleGroupHeaders() { Q_ASSERT(m_grouped); m_layouter->markAsDirty(); QHashIterator it(m_visibleItems); while (it.hasNext()) { it.next(); updateGroupHeaderForWidget(it.value()); } } int KItemListView::groupIndexForItem(int index) const { Q_ASSERT(m_grouped); const QList > groups = model()->groups(); if (groups.isEmpty()) { return -1; } int min = 0; int max = groups.count() - 1; int mid = 0; do { mid = (min + max) / 2; if (index > groups[mid].first) { min = mid + 1; } else { max = mid - 1; } } while (groups[mid].first != index && min <= max); if (min > max) { while (groups[mid].first > index && mid > 0) { --mid; } } return mid; } void KItemListView::updateAlternateBackgrounds() { QHashIterator it(m_visibleItems); while (it.hasNext()) { it.next(); updateAlternateBackgroundForWidget(it.value()); } } void KItemListView::updateAlternateBackgroundForWidget(KItemListWidget* widget) { bool enabled = useAlternateBackgrounds(); if (enabled) { const int index = widget->index(); enabled = (index & 0x1) > 0; if (m_grouped) { const int groupIndex = groupIndexForItem(index); if (groupIndex >= 0) { const QList > groups = model()->groups(); const int indexOfFirstGroupItem = groups[groupIndex].first; const int relativeIndex = index - indexOfFirstGroupItem; enabled = (relativeIndex & 0x1) > 0; } } } widget->setAlternateBackground(enabled); } bool KItemListView::useAlternateBackgrounds() const { return m_itemSize.isEmpty() && m_visibleRoles.count() > 1; } QHash KItemListView::preferredColumnWidths(const KItemRangeList& itemRanges) const { QElapsedTimer timer; timer.start(); QHash widths; // Calculate the minimum width for each column that is required // to show the headline unclipped. const QFontMetricsF fontMetrics(m_headerWidget->font()); const int gripMargin = m_headerWidget->style()->pixelMetric(QStyle::PM_HeaderGripMargin); const int headerMargin = m_headerWidget->style()->pixelMetric(QStyle::PM_HeaderMargin); foreach (const QByteArray& visibleRole, visibleRoles()) { const QString headerText = m_model->roleDescription(visibleRole); const qreal headerWidth = fontMetrics.width(headerText) + gripMargin + headerMargin * 2; widths.insert(visibleRole, headerWidth); } // Calculate the preferred column withs for each item and ignore values // smaller than the width for showing the headline unclipped. const KItemListWidgetCreatorBase* creator = widgetCreator(); int calculatedItemCount = 0; bool maxTimeExceeded = false; foreach (const KItemRange& itemRange, itemRanges) { const int startIndex = itemRange.index; const int endIndex = startIndex + itemRange.count - 1; for (int i = startIndex; i <= endIndex; ++i) { foreach (const QByteArray& visibleRole, visibleRoles()) { qreal maxWidth = widths.value(visibleRole, 0); const qreal width = creator->preferredRoleColumnWidth(visibleRole, i, this); maxWidth = qMax(width, maxWidth); widths.insert(visibleRole, maxWidth); } if (calculatedItemCount > 100 && timer.elapsed() > 200) { // When having several thousands of items calculating the sizes can get // very expensive. We accept a possibly too small role-size in favour // of having no blocking user interface. maxTimeExceeded = true; break; } ++calculatedItemCount; } if (maxTimeExceeded) { break; } } return widths; } void KItemListView::applyColumnWidthsFromHeader() { // Apply the new size to the layouter const qreal requiredWidth = columnWidthsSum(); const QSizeF dynamicItemSize(qMax(size().width(), requiredWidth), m_itemSize.height()); m_layouter->setItemSize(dynamicItemSize); // Update the role sizes for all visible widgets QHashIterator it(m_visibleItems); while (it.hasNext()) { it.next(); updateWidgetColumnWidths(it.value()); } } void KItemListView::updateWidgetColumnWidths(KItemListWidget* widget) { foreach (const QByteArray& role, m_visibleRoles) { widget->setColumnWidth(role, m_headerWidget->columnWidth(role)); } } void KItemListView::updatePreferredColumnWidths(const KItemRangeList& itemRanges) { Q_ASSERT(m_itemSize.isEmpty()); const int itemCount = m_model->count(); int rangesItemCount = 0; foreach (const KItemRange& range, itemRanges) { rangesItemCount += range.count; } if (itemCount == rangesItemCount) { const QHash preferredWidths = preferredColumnWidths(itemRanges); foreach (const QByteArray& role, m_visibleRoles) { m_headerWidget->setPreferredColumnWidth(role, preferredWidths.value(role)); } } else { // Only a sub range of the roles need to be determined. // The chances are good that the widths of the sub ranges // already fit into the available widths and hence no // expensive update might be required. bool changed = false; const QHash updatedWidths = preferredColumnWidths(itemRanges); QHashIterator it(updatedWidths); while (it.hasNext()) { it.next(); const QByteArray& role = it.key(); const qreal updatedWidth = it.value(); const qreal currentWidth = m_headerWidget->preferredColumnWidth(role); if (updatedWidth > currentWidth) { m_headerWidget->setPreferredColumnWidth(role, updatedWidth); changed = true; } } if (!changed) { // All the updated sizes are smaller than the current sizes and no change // of the stretched roles-widths is required return; } } if (m_headerWidget->automaticColumnResizing()) { applyAutomaticColumnWidths(); } } void KItemListView::updatePreferredColumnWidths() { if (m_model) { updatePreferredColumnWidths(KItemRangeList() << KItemRange(0, m_model->count())); } } void KItemListView::applyAutomaticColumnWidths() { Q_ASSERT(m_itemSize.isEmpty()); Q_ASSERT(m_headerWidget->automaticColumnResizing()); if (m_visibleRoles.isEmpty()) { return; } // Calculate the maximum size of an item by considering the // visible role sizes and apply them to the layouter. If the // size does not use the available view-size the size of the // first role will get stretched. foreach (const QByteArray& role, m_visibleRoles) { const qreal preferredWidth = m_headerWidget->preferredColumnWidth(role); m_headerWidget->setColumnWidth(role, preferredWidth); } const QByteArray firstRole = m_visibleRoles.first(); qreal firstColumnWidth = m_headerWidget->columnWidth(firstRole); QSizeF dynamicItemSize = m_itemSize; qreal requiredWidth = columnWidthsSum(); const qreal availableWidth = size().width(); if (requiredWidth < availableWidth) { // Stretch the first column to use the whole remaining width firstColumnWidth += availableWidth - requiredWidth; m_headerWidget->setColumnWidth(firstRole, firstColumnWidth); } else if (requiredWidth > availableWidth && m_visibleRoles.count() > 1) { // Shrink the first column to be able to show as much other // columns as possible qreal shrinkedFirstColumnWidth = firstColumnWidth - requiredWidth + availableWidth; // TODO: A proper calculation of the minimum width depends on the implementation // of KItemListWidget. Probably a kind of minimum size-hint should be introduced // later. const qreal minWidth = qMin(firstColumnWidth, qreal(m_styleOption.iconSize * 2 + 200)); if (shrinkedFirstColumnWidth < minWidth) { shrinkedFirstColumnWidth = minWidth; } m_headerWidget->setColumnWidth(firstRole, shrinkedFirstColumnWidth); requiredWidth -= firstColumnWidth - shrinkedFirstColumnWidth; } dynamicItemSize.rwidth() = qMax(requiredWidth, availableWidth); m_layouter->setItemSize(dynamicItemSize); // Update the role sizes for all visible widgets QHashIterator it(m_visibleItems); while (it.hasNext()) { it.next(); updateWidgetColumnWidths(it.value()); } } qreal KItemListView::columnWidthsSum() const { qreal widthsSum = 0; foreach (const QByteArray& role, m_visibleRoles) { widthsSum += m_headerWidget->columnWidth(role); } return widthsSum; } QRectF KItemListView::headerBoundaries() const { return m_headerWidget->isVisible() ? m_headerWidget->geometry() : QRectF(); } bool KItemListView::changesItemGridLayout(const QSizeF& newGridSize, const QSizeF& newItemSize, const QSizeF& newItemMargin) const { if (newItemSize.isEmpty() || newGridSize.isEmpty()) { return false; } if (m_layouter->scrollOrientation() == Qt::Vertical) { const qreal itemWidth = m_layouter->itemSize().width(); if (itemWidth > 0) { const int newColumnCount = itemsPerSize(newGridSize.width(), newItemSize.width(), newItemMargin.width()); if (m_model->count() > newColumnCount) { const int oldColumnCount = itemsPerSize(m_layouter->size().width(), itemWidth, m_layouter->itemMargin().width()); return oldColumnCount != newColumnCount; } } } else { const qreal itemHeight = m_layouter->itemSize().height(); if (itemHeight > 0) { const int newRowCount = itemsPerSize(newGridSize.height(), newItemSize.height(), newItemMargin.height()); if (m_model->count() > newRowCount) { const int oldRowCount = itemsPerSize(m_layouter->size().height(), itemHeight, m_layouter->itemMargin().height()); return oldRowCount != newRowCount; } } } return false; } bool KItemListView::animateChangedItemCount(int changedItemCount) const { if (m_itemSize.isEmpty()) { // We have only columns or only rows, but no grid: An animation is usually // welcome when inserting or removing items. return !supportsItemExpanding(); } if (m_layouter->size().isEmpty() || m_layouter->itemSize().isEmpty()) { return false; } const int maximum = (scrollOrientation() == Qt::Vertical) ? m_layouter->size().width() / m_layouter->itemSize().width() : m_layouter->size().height() / m_layouter->itemSize().height(); // Only animate if up to 2/3 of a row or column are inserted or removed return changedItemCount <= maximum * 2 / 3; } bool KItemListView::scrollBarRequired(const QSizeF& size) const { const QSizeF oldSize = m_layouter->size(); m_layouter->setSize(size); const qreal maxOffset = m_layouter->maximumScrollOffset(); m_layouter->setSize(oldSize); return m_layouter->scrollOrientation() == Qt::Vertical ? maxOffset > size.height() : maxOffset > size.width(); } int KItemListView::showDropIndicator(const QPointF& pos) { QHashIterator it(m_visibleItems); while (it.hasNext()) { it.next(); const KItemListWidget* widget = it.value(); const QPointF mappedPos = widget->mapFromItem(this, pos); const QRectF rect = itemRect(widget->index()); if (mappedPos.y() >= 0 && mappedPos.y() <= rect.height()) { if (m_model->supportsDropping(widget->index())) { // Keep 30% of the rectangle as the gap instead of always having a fixed gap const int gap = qMax(qreal(4.0), qreal(0.3) * rect.height()); if (mappedPos.y() >= gap && mappedPos.y() <= rect.height() - gap) { return -1; } } const bool isAboveItem = (mappedPos.y () < rect.height() / 2); const qreal y = isAboveItem ? rect.top() : rect.bottom(); const QRectF draggingInsertIndicator(rect.left(), y, rect.width(), 1); if (m_dropIndicator != draggingInsertIndicator) { m_dropIndicator = draggingInsertIndicator; update(); } int index = widget->index(); if (!isAboveItem) { ++index; } return index; } } const QRectF firstItemRect = itemRect(firstVisibleIndex()); return (pos.y() <= firstItemRect.top()) ? 0 : -1; } void KItemListView::hideDropIndicator() { if (!m_dropIndicator.isNull()) { m_dropIndicator = QRectF(); update(); } } void KItemListView::updateGroupHeaderHeight() { qreal groupHeaderHeight = m_styleOption.fontMetrics.height(); qreal groupHeaderMargin = 0; if (scrollOrientation() == Qt::Horizontal) { // The vertical margin above and below the header should be // equal to the horizontal margin, not the vertical margin // from m_styleOption. groupHeaderHeight += 2 * m_styleOption.horizontalMargin; groupHeaderMargin = m_styleOption.horizontalMargin; } else if (m_itemSize.isEmpty()){ groupHeaderHeight += 4 * m_styleOption.padding; groupHeaderMargin = m_styleOption.iconSize / 2; } else { groupHeaderHeight += 2 * m_styleOption.padding + m_styleOption.verticalMargin; groupHeaderMargin = m_styleOption.iconSize / 4; } m_layouter->setGroupHeaderHeight(groupHeaderHeight); m_layouter->setGroupHeaderMargin(groupHeaderMargin); updateVisibleGroupHeaders(); } void KItemListView::updateSiblingsInformation(int firstIndex, int lastIndex) { if (!supportsItemExpanding() || !m_model) { return; } if (firstIndex < 0 || lastIndex < 0) { firstIndex = m_layouter->firstVisibleIndex(); lastIndex = m_layouter->lastVisibleIndex(); } else { const bool isRangeVisible = (firstIndex <= m_layouter->lastVisibleIndex() && lastIndex >= m_layouter->firstVisibleIndex()); if (!isRangeVisible) { return; } } int previousParents = 0; QBitArray previousSiblings; // The rootIndex describes the first index where the siblings get // calculated from. For the calculation the upper most parent item // is required. For performance reasons it is checked first whether // the visible items before or after the current range already // contain a siblings information which can be used as base. int rootIndex = firstIndex; KItemListWidget* widget = m_visibleItems.value(firstIndex - 1); if (!widget) { // There is no visible widget before the range, check whether there // is one after the range: widget = m_visibleItems.value(lastIndex + 1); if (widget) { // The sibling information of the widget may only be used if // all items of the range have the same number of parents. const int parents = m_model->expandedParentsCount(lastIndex + 1); for (int i = lastIndex; i >= firstIndex; --i) { if (m_model->expandedParentsCount(i) != parents) { widget = nullptr; break; } } } } if (widget) { // Performance optimization: Use the sibling information of the visible // widget beside the given range. previousSiblings = widget->siblingsInformation(); if (previousSiblings.isEmpty()) { return; } previousParents = previousSiblings.count() - 1; previousSiblings.truncate(previousParents); } else { // Potentially slow path: Go back to the upper most parent of firstIndex // to be able to calculate the initial value for the siblings. while (rootIndex > 0 && m_model->expandedParentsCount(rootIndex) > 0) { --rootIndex; } } Q_ASSERT(previousParents >= 0); for (int i = rootIndex; i <= lastIndex; ++i) { // Update the parent-siblings in case if the current item represents // a child or an upper parent. const int currentParents = m_model->expandedParentsCount(i); Q_ASSERT(currentParents >= 0); if (previousParents < currentParents) { previousParents = currentParents; previousSiblings.resize(currentParents); previousSiblings.setBit(currentParents - 1, hasSiblingSuccessor(i - 1)); } else if (previousParents > currentParents) { previousParents = currentParents; previousSiblings.truncate(currentParents); } if (i >= firstIndex) { // The index represents a visible item. Apply the parent-siblings // and update the sibling of the current item. KItemListWidget* widget = m_visibleItems.value(i); if (!widget) { continue; } QBitArray siblings = previousSiblings; siblings.resize(siblings.count() + 1); siblings.setBit(siblings.count() - 1, hasSiblingSuccessor(i)); widget->setSiblingsInformation(siblings); } } } bool KItemListView::hasSiblingSuccessor(int index) const { bool hasSuccessor = false; const int parentsCount = m_model->expandedParentsCount(index); int successorIndex = index + 1; // Search the next sibling const int itemCount = m_model->count(); while (successorIndex < itemCount) { const int currentParentsCount = m_model->expandedParentsCount(successorIndex); if (currentParentsCount == parentsCount) { hasSuccessor = true; break; } else if (currentParentsCount < parentsCount) { break; } ++successorIndex; } if (m_grouped && hasSuccessor) { // If the sibling is part of another group, don't mark it as // successor as the group header is between the sibling connections. for (int i = index + 1; i <= successorIndex; ++i) { if (m_layouter->isFirstGroupItem(i)) { hasSuccessor = false; break; } } } return hasSuccessor; } void KItemListView::disconnectRoleEditingSignals(int index) { KStandardItemListWidget* widget = qobject_cast(m_visibleItems.value(index)); if (!widget) { return; } disconnect(widget, &KItemListWidget::roleEditingCanceled, this, nullptr); disconnect(widget, &KItemListWidget::roleEditingFinished, this, nullptr); disconnect(this, &KItemListView::scrollOffsetChanged, widget, nullptr); } int KItemListView::calculateAutoScrollingIncrement(int pos, int range, int oldInc) { int inc = 0; const int minSpeed = 4; const int maxSpeed = 128; const int speedLimiter = 96; const int autoScrollBorder = 64; // Limit the increment that is allowed to be added in comparison to 'oldInc'. // This assures that the autoscrolling speed grows gradually. const int incLimiter = 1; if (pos < autoScrollBorder) { inc = -minSpeed + qAbs(pos - autoScrollBorder) * (pos - autoScrollBorder) / speedLimiter; inc = qMax(inc, -maxSpeed); inc = qMax(inc, oldInc - incLimiter); } else if (pos > range - autoScrollBorder) { inc = minSpeed + qAbs(pos - range + autoScrollBorder) * (pos - range + autoScrollBorder) / speedLimiter; inc = qMin(inc, maxSpeed); inc = qMin(inc, oldInc + incLimiter); } return inc; } int KItemListView::itemsPerSize(qreal size, qreal itemSize, qreal itemMargin) { const qreal availableSize = size - itemMargin; const int count = availableSize / (itemSize + itemMargin); return count; } KItemListCreatorBase::~KItemListCreatorBase() { qDeleteAll(m_recycleableWidgets); qDeleteAll(m_createdWidgets); } void KItemListCreatorBase::addCreatedWidget(QGraphicsWidget* widget) { m_createdWidgets.insert(widget); } void KItemListCreatorBase::pushRecycleableWidget(QGraphicsWidget* widget) { Q_ASSERT(m_createdWidgets.contains(widget)); m_createdWidgets.remove(widget); if (m_recycleableWidgets.count() < 100) { m_recycleableWidgets.append(widget); widget->setVisible(false); } else { delete widget; } } QGraphicsWidget* KItemListCreatorBase::popRecycleableWidget() { if (m_recycleableWidgets.isEmpty()) { return nullptr; } QGraphicsWidget* widget = m_recycleableWidgets.takeLast(); m_createdWidgets.insert(widget); return widget; } KItemListWidgetCreatorBase::~KItemListWidgetCreatorBase() { } void KItemListWidgetCreatorBase::recycle(KItemListWidget* widget) { widget->setParentItem(nullptr); widget->setOpacity(1.0); pushRecycleableWidget(widget); } KItemListGroupHeaderCreatorBase::~KItemListGroupHeaderCreatorBase() { } void KItemListGroupHeaderCreatorBase::recycle(KItemListGroupHeader* header) { header->setOpacity(1.0); pushRecycleableWidget(header); }