File indexing completed on 2024-04-28 05:45:04
0001 /* 0002 * SPDX-FileCopyrightText: 2011 Peter Penz <peter.penz19@gmail.com> 0003 * SPDX-FileCopyrightText: 2013 Frank Reininghaus <frank78ac@googlemail.com> 0004 * SPDX-FileCopyrightText: 2013 Emmanuel Pescosta <emmanuelpescosta099@gmail.com> 0005 * 0006 * SPDX-License-Identifier: GPL-2.0-or-later 0007 */ 0008 0009 #include "kfileitemmodel.h" 0010 0011 #include "dolphin_contentdisplaysettings.h" 0012 #include "dolphin_generalsettings.h" 0013 #include "dolphindebug.h" 0014 #include "private/kfileitemmodelsortalgorithm.h" 0015 0016 #include <KDirLister> 0017 #include <KIO/Job> 0018 #include <KIO/ListJob> 0019 #include <KLocalizedString> 0020 #include <KUrlMimeData> 0021 0022 #include <QElapsedTimer> 0023 #include <QIcon> 0024 #include <QMimeData> 0025 #include <QMimeDatabase> 0026 #include <QRecursiveMutex> 0027 #include <QTimer> 0028 #include <QWidget> 0029 #include <klazylocalizedstring.h> 0030 0031 Q_GLOBAL_STATIC(QRecursiveMutex, s_collatorMutex) 0032 0033 // #define KFILEITEMMODEL_DEBUG 0034 0035 KFileItemModel::KFileItemModel(QObject *parent) 0036 : KItemModelBase("text", parent) 0037 , m_dirLister(nullptr) 0038 , m_sortDirsFirst(true) 0039 , m_sortHiddenLast(false) 0040 , m_sortRole(NameRole) 0041 , m_sortingProgressPercent(-1) 0042 , m_roles() 0043 , m_itemData() 0044 , m_items() 0045 , m_filter() 0046 , m_filteredItems() 0047 , m_requestRole() 0048 , m_maximumUpdateIntervalTimer(nullptr) 0049 , m_resortAllItemsTimer(nullptr) 0050 , m_pendingItemsToInsert() 0051 , m_groups() 0052 , m_expandedDirs() 0053 , m_urlsToExpand() 0054 { 0055 m_collator.setNumericMode(true); 0056 0057 loadSortingSettings(); 0058 0059 m_dirLister = new KDirLister(this); 0060 m_dirLister->setAutoErrorHandlingEnabled(false); 0061 m_dirLister->setDelayedMimeTypes(true); 0062 0063 const QWidget *parentWidget = qobject_cast<QWidget *>(parent); 0064 if (parentWidget) { 0065 m_dirLister->setMainWindow(parentWidget->window()); 0066 } 0067 0068 connect(m_dirLister, &KCoreDirLister::started, this, &KFileItemModel::directoryLoadingStarted); 0069 connect(m_dirLister, &KCoreDirLister::canceled, this, &KFileItemModel::slotCanceled); 0070 connect(m_dirLister, &KCoreDirLister::itemsAdded, this, &KFileItemModel::slotItemsAdded); 0071 connect(m_dirLister, &KCoreDirLister::itemsDeleted, this, &KFileItemModel::slotItemsDeleted); 0072 connect(m_dirLister, &KCoreDirLister::refreshItems, this, &KFileItemModel::slotRefreshItems); 0073 connect(m_dirLister, &KCoreDirLister::clear, this, &KFileItemModel::slotClear); 0074 connect(m_dirLister, &KCoreDirLister::infoMessage, this, &KFileItemModel::infoMessage); 0075 connect(m_dirLister, &KCoreDirLister::jobError, this, &KFileItemModel::slotListerError); 0076 connect(m_dirLister, &KCoreDirLister::percent, this, &KFileItemModel::directoryLoadingProgress); 0077 connect(m_dirLister, &KCoreDirLister::redirection, this, &KFileItemModel::directoryRedirection); 0078 connect(m_dirLister, &KCoreDirLister::listingDirCompleted, this, &KFileItemModel::slotCompleted); 0079 0080 // Apply default roles that should be determined 0081 resetRoles(); 0082 m_requestRole[NameRole] = true; 0083 m_requestRole[IsDirRole] = true; 0084 m_requestRole[IsLinkRole] = true; 0085 m_roles.insert("text"); 0086 m_roles.insert("isDir"); 0087 m_roles.insert("isLink"); 0088 m_roles.insert("isHidden"); 0089 0090 // For slow KIO-slaves like used for searching it makes sense to show results periodically even 0091 // before the completed() or canceled() signal has been emitted. 0092 m_maximumUpdateIntervalTimer = new QTimer(this); 0093 m_maximumUpdateIntervalTimer->setInterval(2000); 0094 m_maximumUpdateIntervalTimer->setSingleShot(true); 0095 connect(m_maximumUpdateIntervalTimer, &QTimer::timeout, this, &KFileItemModel::dispatchPendingItemsToInsert); 0096 0097 // When changing the value of an item which represents the sort-role a resorting must be 0098 // triggered. Especially in combination with KFileItemModelRolesUpdater this might be done 0099 // for a lot of items within a quite small timeslot. To prevent expensive resortings the 0100 // resorting is postponed until the timer has been exceeded. 0101 m_resortAllItemsTimer = new QTimer(this); 0102 m_resortAllItemsTimer->setInterval(100); // 100 is a middle ground between sorting too frequently which makes the view unreadable 0103 // and sorting too infrequently which leads to users seeing an outdated sort order. 0104 m_resortAllItemsTimer->setSingleShot(true); 0105 connect(m_resortAllItemsTimer, &QTimer::timeout, this, &KFileItemModel::resortAllItems); 0106 0107 connect(GeneralSettings::self(), &GeneralSettings::sortingChoiceChanged, this, &KFileItemModel::slotSortingChoiceChanged); 0108 0109 setShowTrashMime(m_dirLister->showHiddenFiles() || !GeneralSettings::hideXTrashFile()); 0110 } 0111 0112 KFileItemModel::~KFileItemModel() 0113 { 0114 qDeleteAll(m_itemData); 0115 qDeleteAll(m_filteredItems); 0116 qDeleteAll(m_pendingItemsToInsert); 0117 } 0118 0119 void KFileItemModel::loadDirectory(const QUrl &url) 0120 { 0121 m_dirLister->openUrl(url); 0122 } 0123 0124 void KFileItemModel::refreshDirectory(const QUrl &url) 0125 { 0126 // Refresh all expanded directories first (Bug 295300) 0127 QHashIterator<QUrl, QUrl> expandedDirs(m_expandedDirs); 0128 while (expandedDirs.hasNext()) { 0129 expandedDirs.next(); 0130 m_dirLister->openUrl(expandedDirs.value(), KDirLister::Reload); 0131 } 0132 0133 m_dirLister->openUrl(url, KDirLister::Reload); 0134 0135 Q_EMIT directoryRefreshing(); 0136 } 0137 0138 QUrl KFileItemModel::directory() const 0139 { 0140 return m_dirLister->url(); 0141 } 0142 0143 void KFileItemModel::cancelDirectoryLoading() 0144 { 0145 m_dirLister->stop(); 0146 } 0147 0148 int KFileItemModel::count() const 0149 { 0150 return m_itemData.count(); 0151 } 0152 0153 QHash<QByteArray, QVariant> KFileItemModel::data(int index) const 0154 { 0155 if (index >= 0 && index < count()) { 0156 ItemData *data = m_itemData.at(index); 0157 if (data->values.isEmpty()) { 0158 data->values = retrieveData(data->item, data->parent); 0159 } else if (data->values.count() <= 2 && data->values.value("isExpanded").toBool()) { 0160 // Special case dealt by slotRefreshItems(), avoid losing the "isExpanded" and "expandedParentsCount" state when refreshing 0161 // slotRefreshItems() makes sure folders keep the "isExpanded" and "expandedParentsCount" while clearing the remaining values 0162 // so this special request of different behavior can be identified here. 0163 bool hasExpandedParentsCount = false; 0164 const int expandedParentsCount = data->values.value("expandedParentsCount").toInt(&hasExpandedParentsCount); 0165 0166 data->values = retrieveData(data->item, data->parent); 0167 data->values.insert("isExpanded", true); 0168 if (hasExpandedParentsCount) { 0169 data->values.insert("expandedParentsCount", expandedParentsCount); 0170 } 0171 } 0172 0173 return data->values; 0174 } 0175 return QHash<QByteArray, QVariant>(); 0176 } 0177 0178 bool KFileItemModel::setData(int index, const QHash<QByteArray, QVariant> &values) 0179 { 0180 if (index < 0 || index >= count()) { 0181 return false; 0182 } 0183 0184 QHash<QByteArray, QVariant> currentValues = data(index); 0185 0186 // Determine which roles have been changed 0187 QSet<QByteArray> changedRoles; 0188 QHashIterator<QByteArray, QVariant> it(values); 0189 while (it.hasNext()) { 0190 it.next(); 0191 const QByteArray role = sharedValue(it.key()); 0192 const QVariant value = it.value(); 0193 0194 if (currentValues[role] != value) { 0195 currentValues[role] = value; 0196 changedRoles.insert(role); 0197 } 0198 } 0199 0200 if (changedRoles.isEmpty()) { 0201 return false; 0202 } 0203 0204 m_itemData[index]->values = currentValues; 0205 if (changedRoles.contains("text")) { 0206 QUrl url = m_itemData[index]->item.url(); 0207 url = url.adjusted(QUrl::RemoveFilename); 0208 url.setPath(url.path() + currentValues["text"].toString()); 0209 m_itemData[index]->item.setUrl(url); 0210 } 0211 0212 emitItemsChangedAndTriggerResorting(KItemRangeList() << KItemRange(index, 1), changedRoles); 0213 0214 return true; 0215 } 0216 0217 void KFileItemModel::setSortDirectoriesFirst(bool dirsFirst) 0218 { 0219 if (dirsFirst != m_sortDirsFirst) { 0220 m_sortDirsFirst = dirsFirst; 0221 resortAllItems(); 0222 } 0223 } 0224 0225 bool KFileItemModel::sortDirectoriesFirst() const 0226 { 0227 return m_sortDirsFirst; 0228 } 0229 0230 void KFileItemModel::setSortHiddenLast(bool hiddenLast) 0231 { 0232 if (hiddenLast != m_sortHiddenLast) { 0233 m_sortHiddenLast = hiddenLast; 0234 resortAllItems(); 0235 } 0236 } 0237 0238 bool KFileItemModel::sortHiddenLast() const 0239 { 0240 return m_sortHiddenLast; 0241 } 0242 0243 void KFileItemModel::setShowTrashMime(bool showTrashMime) 0244 { 0245 const auto trashMime = QStringLiteral("application/x-trash"); 0246 QStringList excludeFilter = m_filter.excludeMimeTypes(); 0247 0248 if (showTrashMime) { 0249 excludeFilter.removeAll(trashMime); 0250 } else if (!excludeFilter.contains(trashMime)) { 0251 excludeFilter.append(trashMime); 0252 } 0253 0254 setExcludeMimeTypeFilter(excludeFilter); 0255 } 0256 0257 void KFileItemModel::scheduleResortAllItems() 0258 { 0259 if (!m_resortAllItemsTimer->isActive()) { 0260 m_resortAllItemsTimer->start(); 0261 } 0262 } 0263 0264 void KFileItemModel::setShowHiddenFiles(bool show) 0265 { 0266 m_dirLister->setShowHiddenFiles(show); 0267 setShowTrashMime(show || !GeneralSettings::hideXTrashFile()); 0268 m_dirLister->emitChanges(); 0269 if (show) { 0270 dispatchPendingItemsToInsert(); 0271 } 0272 } 0273 0274 bool KFileItemModel::showHiddenFiles() const 0275 { 0276 return m_dirLister->showHiddenFiles(); 0277 } 0278 0279 void KFileItemModel::setShowDirectoriesOnly(bool enabled) 0280 { 0281 m_dirLister->setDirOnlyMode(enabled); 0282 } 0283 0284 bool KFileItemModel::showDirectoriesOnly() const 0285 { 0286 return m_dirLister->dirOnlyMode(); 0287 } 0288 0289 QMimeData *KFileItemModel::createMimeData(const KItemSet &indexes) const 0290 { 0291 QMimeData *data = new QMimeData(); 0292 0293 // The following code has been taken from KDirModel::mimeData() 0294 // (kdelibs/kio/kio/kdirmodel.cpp) 0295 // SPDX-FileCopyrightText: 2006 David Faure <faure@kde.org> 0296 QList<QUrl> urls; 0297 QList<QUrl> mostLocalUrls; 0298 const ItemData *lastAddedItem = nullptr; 0299 0300 for (int index : indexes) { 0301 const ItemData *itemData = m_itemData.at(index); 0302 const ItemData *parent = itemData->parent; 0303 0304 while (parent && parent != lastAddedItem) { 0305 parent = parent->parent; 0306 } 0307 0308 if (parent && parent == lastAddedItem) { 0309 // A parent of 'itemData' has been added already. 0310 continue; 0311 } 0312 0313 lastAddedItem = itemData; 0314 const KFileItem &item = itemData->item; 0315 if (!item.isNull()) { 0316 urls << item.url(); 0317 0318 bool isLocal; 0319 mostLocalUrls << item.mostLocalUrl(&isLocal); 0320 } 0321 } 0322 0323 KUrlMimeData::setUrls(urls, mostLocalUrls, data); 0324 return data; 0325 } 0326 0327 int KFileItemModel::indexForKeyboardSearch(const QString &text, int startFromIndex) const 0328 { 0329 startFromIndex = qMax(0, startFromIndex); 0330 for (int i = startFromIndex; i < count(); ++i) { 0331 if (fileItem(i).text().startsWith(text, Qt::CaseInsensitive)) { 0332 return i; 0333 } 0334 } 0335 for (int i = 0; i < startFromIndex; ++i) { 0336 if (fileItem(i).text().startsWith(text, Qt::CaseInsensitive)) { 0337 return i; 0338 } 0339 } 0340 return -1; 0341 } 0342 0343 bool KFileItemModel::supportsDropping(int index) const 0344 { 0345 KFileItem item; 0346 if (index == -1) { 0347 item = rootItem(); 0348 } else { 0349 item = fileItem(index); 0350 } 0351 return !item.isNull() && ((item.isDir() && item.isWritable()) || item.isDesktopFile()); 0352 } 0353 0354 QString KFileItemModel::roleDescription(const QByteArray &role) const 0355 { 0356 static QHash<QByteArray, QString> description; 0357 if (description.isEmpty()) { 0358 int count = 0; 0359 const RoleInfoMap *map = rolesInfoMap(count); 0360 for (int i = 0; i < count; ++i) { 0361 if (map[i].roleTranslation.isEmpty()) { 0362 continue; 0363 } 0364 description.insert(map[i].role, map[i].roleTranslation.toString()); 0365 } 0366 } 0367 0368 return description.value(role); 0369 } 0370 0371 QList<QPair<int, QVariant>> KFileItemModel::groups() const 0372 { 0373 if (!m_itemData.isEmpty() && m_groups.isEmpty()) { 0374 #ifdef KFILEITEMMODEL_DEBUG 0375 QElapsedTimer timer; 0376 timer.start(); 0377 #endif 0378 switch (typeForRole(sortRole())) { 0379 case NameRole: 0380 m_groups = nameRoleGroups(); 0381 break; 0382 case SizeRole: 0383 m_groups = sizeRoleGroups(); 0384 break; 0385 case ModificationTimeRole: 0386 m_groups = timeRoleGroups([](const ItemData *item) { 0387 return item->item.time(KFileItem::ModificationTime); 0388 }); 0389 break; 0390 case CreationTimeRole: 0391 m_groups = timeRoleGroups([](const ItemData *item) { 0392 return item->item.time(KFileItem::CreationTime); 0393 }); 0394 break; 0395 case AccessTimeRole: 0396 m_groups = timeRoleGroups([](const ItemData *item) { 0397 return item->item.time(KFileItem::AccessTime); 0398 }); 0399 break; 0400 case DeletionTimeRole: 0401 m_groups = timeRoleGroups([](const ItemData *item) { 0402 return item->values.value("deletiontime").toDateTime(); 0403 }); 0404 break; 0405 case PermissionsRole: 0406 m_groups = permissionRoleGroups(); 0407 break; 0408 case RatingRole: 0409 m_groups = ratingRoleGroups(); 0410 break; 0411 default: 0412 m_groups = genericStringRoleGroups(sortRole()); 0413 break; 0414 } 0415 0416 #ifdef KFILEITEMMODEL_DEBUG 0417 qCDebug(DolphinDebug) << "[TIME] Calculating groups for" << count() << "items:" << timer.elapsed(); 0418 #endif 0419 } 0420 0421 return m_groups; 0422 } 0423 0424 KFileItem KFileItemModel::fileItem(int index) const 0425 { 0426 if (index >= 0 && index < count()) { 0427 return m_itemData.at(index)->item; 0428 } 0429 0430 return KFileItem(); 0431 } 0432 0433 KFileItem KFileItemModel::fileItem(const QUrl &url) const 0434 { 0435 const int indexForUrl = index(url); 0436 if (indexForUrl >= 0) { 0437 return m_itemData.at(indexForUrl)->item; 0438 } 0439 return KFileItem(); 0440 } 0441 0442 int KFileItemModel::index(const KFileItem &item) const 0443 { 0444 return index(item.url()); 0445 } 0446 0447 int KFileItemModel::index(const QUrl &url) const 0448 { 0449 const QUrl urlToFind = url.adjusted(QUrl::StripTrailingSlash); 0450 0451 const int itemCount = m_itemData.count(); 0452 int itemsInHash = m_items.count(); 0453 0454 int index = m_items.value(urlToFind, -1); 0455 while (index < 0 && itemsInHash < itemCount) { 0456 // Not all URLs are stored yet in m_items. We grow m_items until either 0457 // urlToFind is found, or all URLs have been stored in m_items. 0458 // Note that we do not add the URLs to m_items one by one, but in 0459 // larger blocks. After each block, we check if urlToFind is in 0460 // m_items. We could in principle compare urlToFind with each URL while 0461 // we are going through m_itemData, but comparing two QUrls will, 0462 // unlike calling qHash for the URLs, trigger a parsing of the URLs 0463 // which costs both CPU cycles and memory. 0464 const int blockSize = 1000; 0465 const int currentBlockEnd = qMin(itemsInHash + blockSize, itemCount); 0466 for (int i = itemsInHash; i < currentBlockEnd; ++i) { 0467 const QUrl nextUrl = m_itemData.at(i)->item.url(); 0468 m_items.insert(nextUrl, i); 0469 } 0470 0471 itemsInHash = currentBlockEnd; 0472 index = m_items.value(urlToFind, -1); 0473 } 0474 0475 if (index < 0) { 0476 // The item could not be found, even though all items from m_itemData 0477 // should be in m_items now. We print some diagnostic information which 0478 // might help to find the cause of the problem, but only once. This 0479 // prevents that obtaining and printing the debugging information 0480 // wastes CPU cycles and floods the shell or .xsession-errors. 0481 static bool printDebugInfo = true; 0482 0483 if (m_items.count() != m_itemData.count() && printDebugInfo) { 0484 printDebugInfo = false; 0485 0486 qCWarning(DolphinDebug) << "The model is in an inconsistent state."; 0487 qCWarning(DolphinDebug) << "m_items.count() ==" << m_items.count(); 0488 qCWarning(DolphinDebug) << "m_itemData.count() ==" << m_itemData.count(); 0489 0490 // Check if there are multiple items with the same URL. 0491 QMultiHash<QUrl, int> indexesForUrl; 0492 for (int i = 0; i < m_itemData.count(); ++i) { 0493 indexesForUrl.insert(m_itemData.at(i)->item.url(), i); 0494 } 0495 0496 const auto uniqueKeys = indexesForUrl.uniqueKeys(); 0497 for (const QUrl &url : uniqueKeys) { 0498 if (indexesForUrl.count(url) > 1) { 0499 qCWarning(DolphinDebug) << "Multiple items found with the URL" << url; 0500 0501 auto it = indexesForUrl.find(url); 0502 while (it != indexesForUrl.end() && it.key() == url) { 0503 const ItemData *data = m_itemData.at(it.value()); 0504 qCWarning(DolphinDebug) << "index" << it.value() << ":" << data->item; 0505 if (data->parent) { 0506 qCWarning(DolphinDebug) << "parent" << data->parent->item; 0507 } 0508 ++it; 0509 } 0510 } 0511 } 0512 } 0513 } 0514 0515 return index; 0516 } 0517 0518 KFileItem KFileItemModel::rootItem() const 0519 { 0520 return m_dirLister->rootItem(); 0521 } 0522 0523 void KFileItemModel::clear() 0524 { 0525 slotClear(); 0526 } 0527 0528 void KFileItemModel::setRoles(const QSet<QByteArray> &roles) 0529 { 0530 if (m_roles == roles) { 0531 return; 0532 } 0533 0534 const QSet<QByteArray> changedRoles = (roles - m_roles) + (m_roles - roles); 0535 m_roles = roles; 0536 0537 if (count() > 0) { 0538 const bool supportedExpanding = m_requestRole[ExpandedParentsCountRole]; 0539 const bool willSupportExpanding = roles.contains("expandedParentsCount"); 0540 if (supportedExpanding && !willSupportExpanding) { 0541 // No expanding is supported anymore. Take care to delete all items that have an expansion level 0542 // that is not 0 (and hence are part of an expanded item). 0543 removeExpandedItems(); 0544 } 0545 } 0546 0547 m_groups.clear(); 0548 resetRoles(); 0549 0550 QSetIterator<QByteArray> it(roles); 0551 while (it.hasNext()) { 0552 const QByteArray &role = it.next(); 0553 m_requestRole[typeForRole(role)] = true; 0554 } 0555 0556 if (count() > 0) { 0557 // Update m_data with the changed requested roles 0558 const int maxIndex = count() - 1; 0559 for (int i = 0; i <= maxIndex; ++i) { 0560 m_itemData[i]->values = retrieveData(m_itemData.at(i)->item, m_itemData.at(i)->parent); 0561 } 0562 0563 Q_EMIT itemsChanged(KItemRangeList() << KItemRange(0, count()), changedRoles); 0564 } 0565 0566 // Clear the 'values' of all filtered items. They will be re-populated with the 0567 // correct roles the next time 'values' will be accessed via data(int). 0568 QHash<KFileItem, ItemData *>::iterator filteredIt = m_filteredItems.begin(); 0569 const QHash<KFileItem, ItemData *>::iterator filteredEnd = m_filteredItems.end(); 0570 while (filteredIt != filteredEnd) { 0571 (*filteredIt)->values.clear(); 0572 ++filteredIt; 0573 } 0574 } 0575 0576 QSet<QByteArray> KFileItemModel::roles() const 0577 { 0578 return m_roles; 0579 } 0580 0581 bool KFileItemModel::setExpanded(int index, bool expanded) 0582 { 0583 if (!isExpandable(index) || isExpanded(index) == expanded) { 0584 return false; 0585 } 0586 0587 QHash<QByteArray, QVariant> values; 0588 values.insert(sharedValue("isExpanded"), expanded); 0589 if (!setData(index, values)) { 0590 return false; 0591 } 0592 0593 const KFileItem item = m_itemData.at(index)->item; 0594 const QUrl url = item.url(); 0595 const QUrl targetUrl = item.targetUrl(); 0596 if (expanded) { 0597 m_expandedDirs.insert(targetUrl, url); 0598 m_dirLister->openUrl(url, KDirLister::Keep); 0599 0600 const QVariantList previouslyExpandedChildren = m_itemData.at(index)->values.value("previouslyExpandedChildren").value<QVariantList>(); 0601 for (const QVariant &var : previouslyExpandedChildren) { 0602 m_urlsToExpand.insert(var.toUrl()); 0603 } 0604 } else { 0605 // Note that there might be (indirect) children of the folder which is to be collapsed in 0606 // m_pendingItemsToInsert. To prevent that they will be inserted into the model later, 0607 // possibly without a parent, which might result in a crash, we insert all pending items 0608 // right now. All new items which would be without a parent will then be removed. 0609 dispatchPendingItemsToInsert(); 0610 0611 // Check if the index of the collapsed folder has changed. If that is the case, then items 0612 // were inserted before the collapsed folder, and its index needs to be updated. 0613 if (m_itemData.at(index)->item != item) { 0614 index = this->index(item); 0615 } 0616 0617 m_expandedDirs.remove(targetUrl); 0618 m_dirLister->stop(url); 0619 m_dirLister->forgetDirs(url); 0620 0621 const int parentLevel = expandedParentsCount(index); 0622 const int itemCount = m_itemData.count(); 0623 const int firstChildIndex = index + 1; 0624 0625 QVariantList expandedChildren; 0626 0627 int childIndex = firstChildIndex; 0628 while (childIndex < itemCount && expandedParentsCount(childIndex) > parentLevel) { 0629 ItemData *itemData = m_itemData.at(childIndex); 0630 if (itemData->values.value("isExpanded").toBool()) { 0631 const QUrl targetUrl = itemData->item.targetUrl(); 0632 const QUrl url = itemData->item.url(); 0633 m_expandedDirs.remove(targetUrl); 0634 m_dirLister->stop(url); // TODO: try to unit-test this, see https://bugs.kde.org/show_bug.cgi?id=332102#c11 0635 m_dirLister->forgetDirs(url); 0636 expandedChildren.append(targetUrl); 0637 } 0638 ++childIndex; 0639 } 0640 const int childrenCount = childIndex - firstChildIndex; 0641 0642 removeFilteredChildren(KItemRangeList() << KItemRange(index, 1 + childrenCount)); 0643 removeItems(KItemRangeList() << KItemRange(firstChildIndex, childrenCount), DeleteItemData); 0644 0645 m_itemData.at(index)->values.insert("previouslyExpandedChildren", expandedChildren); 0646 } 0647 0648 return true; 0649 } 0650 0651 bool KFileItemModel::isExpanded(int index) const 0652 { 0653 if (index >= 0 && index < count()) { 0654 return m_itemData.at(index)->values.value("isExpanded").toBool(); 0655 } 0656 return false; 0657 } 0658 0659 bool KFileItemModel::isExpandable(int index) const 0660 { 0661 if (index >= 0 && index < count()) { 0662 // Call data (instead of accessing m_itemData directly) 0663 // to ensure that the value is initialized. 0664 return data(index).value("isExpandable").toBool(); 0665 } 0666 return false; 0667 } 0668 0669 int KFileItemModel::expandedParentsCount(int index) const 0670 { 0671 if (index >= 0 && index < count()) { 0672 return expandedParentsCount(m_itemData.at(index)); 0673 } 0674 return 0; 0675 } 0676 0677 QSet<QUrl> KFileItemModel::expandedDirectories() const 0678 { 0679 QSet<QUrl> result; 0680 const auto dirs = m_expandedDirs; 0681 for (const auto &dir : dirs) { 0682 result.insert(dir); 0683 } 0684 return result; 0685 } 0686 0687 void KFileItemModel::restoreExpandedDirectories(const QSet<QUrl> &urls) 0688 { 0689 m_urlsToExpand = urls; 0690 } 0691 0692 void KFileItemModel::expandParentDirectories(const QUrl &url) 0693 { 0694 // Assure that each sub-path of the URL that should be 0695 // expanded is added to m_urlsToExpand. KDirLister 0696 // does not care whether the parent-URL has already been 0697 // expanded. 0698 QUrl urlToExpand = m_dirLister->url(); 0699 const int pos = urlToExpand.path().length(); 0700 0701 // first subdir can be empty, if m_dirLister->url().path() does not end with '/' 0702 // this happens if baseUrl is not root but a home directory, see FoldersPanel, 0703 // so using QString::SkipEmptyParts 0704 const QStringList subDirs = url.path().mid(pos).split(QDir::separator(), Qt::SkipEmptyParts); 0705 for (int i = 0; i < subDirs.count() - 1; ++i) { 0706 QString path = urlToExpand.path(); 0707 if (!path.endsWith(QLatin1Char('/'))) { 0708 path.append(QLatin1Char('/')); 0709 } 0710 urlToExpand.setPath(path + subDirs.at(i)); 0711 m_urlsToExpand.insert(urlToExpand); 0712 } 0713 0714 // KDirLister::open() must called at least once to trigger an initial 0715 // loading. The pending URLs that must be restored are handled 0716 // in slotCompleted(). 0717 QSetIterator<QUrl> it2(m_urlsToExpand); 0718 while (it2.hasNext()) { 0719 const int idx = index(it2.next()); 0720 if (idx >= 0 && !isExpanded(idx)) { 0721 setExpanded(idx, true); 0722 break; 0723 } 0724 } 0725 } 0726 0727 void KFileItemModel::setNameFilter(const QString &nameFilter) 0728 { 0729 if (m_filter.pattern() != nameFilter) { 0730 dispatchPendingItemsToInsert(); 0731 m_filter.setPattern(nameFilter); 0732 applyFilters(); 0733 } 0734 } 0735 0736 QString KFileItemModel::nameFilter() const 0737 { 0738 return m_filter.pattern(); 0739 } 0740 0741 void KFileItemModel::setMimeTypeFilters(const QStringList &filters) 0742 { 0743 if (m_filter.mimeTypes() != filters) { 0744 dispatchPendingItemsToInsert(); 0745 m_filter.setMimeTypes(filters); 0746 applyFilters(); 0747 } 0748 } 0749 0750 QStringList KFileItemModel::mimeTypeFilters() const 0751 { 0752 return m_filter.mimeTypes(); 0753 } 0754 0755 void KFileItemModel::setExcludeMimeTypeFilter(const QStringList &filters) 0756 { 0757 if (m_filter.excludeMimeTypes() != filters) { 0758 dispatchPendingItemsToInsert(); 0759 m_filter.setExcludeMimeTypes(filters); 0760 applyFilters(); 0761 } 0762 } 0763 0764 QStringList KFileItemModel::excludeMimeTypeFilter() const 0765 { 0766 return m_filter.excludeMimeTypes(); 0767 } 0768 0769 void KFileItemModel::applyFilters() 0770 { 0771 // ===STEP 1=== 0772 // Check which previously shown items from m_itemData must now get 0773 // hidden and hence moved from m_itemData into m_filteredItems. 0774 0775 QList<int> newFilteredIndexes; // This structure is good for prepending. We will want an ascending sorted Container at the end, this will do fine. 0776 0777 // This pointer will refer to the next confirmed shown item from the point of 0778 // view of the current "itemData" in the upcoming "for" loop. 0779 ItemData *itemShownBelow = nullptr; 0780 0781 // We will iterate backwards because it's convenient to know beforehand if the item just below is its child or not. 0782 for (int index = m_itemData.count() - 1; index >= 0; --index) { 0783 ItemData *itemData = m_itemData.at(index); 0784 0785 if (m_filter.matches(itemData->item) || (itemShownBelow && itemShownBelow->parent == itemData)) { 0786 // We could've entered here for two reasons: 0787 // 1. This item passes the filter itself 0788 // 2. This is an expanded folder that doesn't pass the filter but sees a filter-passing child just below 0789 0790 // So this item must remain shown. 0791 // Lets register this item as the next shown item from the point of view of the next iteration of this for loop 0792 itemShownBelow = itemData; 0793 } else { 0794 // We hide this item for now, however, for expanded folders this is not final: 0795 // if after the next "for" loop we discover that its children must now be shown with the newly applied fliter, we shall re-insert it 0796 newFilteredIndexes.prepend(index); 0797 m_filteredItems.insert(itemData->item, itemData); 0798 // indexShownBelow doesn't get updated since this item will be hidden 0799 } 0800 } 0801 0802 // This will remove the newly filtered items from m_itemData 0803 removeItems(KItemRangeList::fromSortedContainer(newFilteredIndexes), KeepItemData); 0804 0805 // ===STEP 2=== 0806 // Check which hidden items from m_filteredItems should 0807 // become visible again and hence moved from m_filteredItems back into m_itemData. 0808 0809 QList<ItemData *> newVisibleItems; 0810 0811 QHash<KFileItem, ItemData *> ancestorsOfNewVisibleItems; // We will make sure these also become visible in step 3. 0812 0813 QHash<KFileItem, ItemData *>::iterator it = m_filteredItems.begin(); 0814 while (it != m_filteredItems.end()) { 0815 if (m_filter.matches(it.key())) { 0816 newVisibleItems.append(it.value()); 0817 0818 // If this is a child of an expanded folder, we must make sure that its whole parental chain will also be shown. 0819 // We will go up through its parental chain until we either: 0820 // 1 - reach the "root item" of the current view, i.e the currently opened folder on Dolphin. Their children have their ItemData::parent set to 0821 // nullptr. or 2 - we reach an unfiltered parent or a previously discovered ancestor. 0822 for (ItemData *parent = it.value()->parent; parent && !ancestorsOfNewVisibleItems.contains(parent->item) && m_filteredItems.contains(parent->item); 0823 parent = parent->parent) { 0824 // We wish we could remove this parent from m_filteredItems right now, but we are iterating over it 0825 // and it would mess up the iteration. We will mark it to be removed in step 3. 0826 ancestorsOfNewVisibleItems.insert(parent->item, parent); 0827 } 0828 0829 it = m_filteredItems.erase(it); 0830 } else { 0831 // Item remains filtered for now 0832 // However, for expanded folders this is not final, we may discover later that it has unfiltered descendants. 0833 ++it; 0834 } 0835 } 0836 0837 // ===STEP 3=== 0838 // Handles the ancestorsOfNewVisibleItems. 0839 // Now that we are done iterating through m_filteredItems we can safely move the ancestorsOfNewVisibleItems from m_filteredItems to newVisibleItems. 0840 for (it = ancestorsOfNewVisibleItems.begin(); it != ancestorsOfNewVisibleItems.end(); it++) { 0841 if (m_filteredItems.remove(it.key())) { 0842 // m_filteredItems still contained this ancestor until now so we can be sure that we aren't adding a duplicate ancestor to newVisibleItems. 0843 newVisibleItems.append(it.value()); 0844 } 0845 } 0846 0847 // This will insert the newly discovered unfiltered items into m_itemData 0848 insertItems(newVisibleItems); 0849 } 0850 0851 void KFileItemModel::removeFilteredChildren(const KItemRangeList &itemRanges) 0852 { 0853 if (m_filteredItems.isEmpty() || !m_requestRole[ExpandedParentsCountRole]) { 0854 // There are either no filtered items, or it is not possible to expand 0855 // folders -> there cannot be any filtered children. 0856 return; 0857 } 0858 0859 QSet<ItemData *> parents; 0860 for (const KItemRange &range : itemRanges) { 0861 for (int index = range.index; index < range.index + range.count; ++index) { 0862 parents.insert(m_itemData.at(index)); 0863 } 0864 } 0865 0866 QHash<KFileItem, ItemData *>::iterator it = m_filteredItems.begin(); 0867 while (it != m_filteredItems.end()) { 0868 if (parents.contains(it.value()->parent)) { 0869 delete it.value(); 0870 it = m_filteredItems.erase(it); 0871 } else { 0872 ++it; 0873 } 0874 } 0875 } 0876 0877 QList<KFileItemModel::RoleInfo> KFileItemModel::rolesInformation() 0878 { 0879 static QList<RoleInfo> rolesInfo; 0880 if (rolesInfo.isEmpty()) { 0881 int count = 0; 0882 const RoleInfoMap *map = rolesInfoMap(count); 0883 for (int i = 0; i < count; ++i) { 0884 if (map[i].roleType != NoRole) { 0885 RoleInfo info; 0886 info.role = map[i].role; 0887 info.translation = map[i].roleTranslation.toString(); 0888 if (!map[i].groupTranslation.isEmpty()) { 0889 info.group = map[i].groupTranslation.toString(); 0890 } else { 0891 // For top level roles, groupTranslation is 0. We must make sure that 0892 // info.group is an empty string then because the code that generates 0893 // menus tries to put the actions into sub menus otherwise. 0894 info.group = QString(); 0895 } 0896 info.requiresBaloo = map[i].requiresBaloo; 0897 info.requiresIndexer = map[i].requiresIndexer; 0898 if (!map[i].tooltipTranslation.isEmpty()) { 0899 info.tooltip = map[i].tooltipTranslation.toString(); 0900 } else { 0901 info.tooltip = QString(); 0902 } 0903 rolesInfo.append(info); 0904 } 0905 } 0906 } 0907 0908 return rolesInfo; 0909 } 0910 0911 void KFileItemModel::onGroupedSortingChanged(bool current) 0912 { 0913 Q_UNUSED(current) 0914 m_groups.clear(); 0915 } 0916 0917 void KFileItemModel::onSortRoleChanged(const QByteArray ¤t, const QByteArray &previous, bool resortItems) 0918 { 0919 Q_UNUSED(previous) 0920 m_sortRole = typeForRole(current); 0921 0922 if (!m_requestRole[m_sortRole]) { 0923 QSet<QByteArray> newRoles = m_roles; 0924 newRoles << current; 0925 setRoles(newRoles); 0926 } 0927 0928 if (resortItems) { 0929 resortAllItems(); 0930 } 0931 } 0932 0933 void KFileItemModel::onSortOrderChanged(Qt::SortOrder current, Qt::SortOrder previous) 0934 { 0935 Q_UNUSED(current) 0936 Q_UNUSED(previous) 0937 resortAllItems(); 0938 } 0939 0940 void KFileItemModel::loadSortingSettings() 0941 { 0942 using Choice = GeneralSettings::EnumSortingChoice; 0943 switch (GeneralSettings::sortingChoice()) { 0944 case Choice::NaturalSorting: 0945 m_naturalSorting = true; 0946 m_collator.setCaseSensitivity(Qt::CaseInsensitive); 0947 break; 0948 case Choice::CaseSensitiveSorting: 0949 m_naturalSorting = false; 0950 m_collator.setCaseSensitivity(Qt::CaseSensitive); 0951 break; 0952 case Choice::CaseInsensitiveSorting: 0953 m_naturalSorting = false; 0954 m_collator.setCaseSensitivity(Qt::CaseInsensitive); 0955 break; 0956 default: 0957 Q_UNREACHABLE(); 0958 } 0959 // Workaround for bug https://bugreports.qt.io/browse/QTBUG-69361 0960 // Force the clean state of QCollator in single thread to avoid thread safety problems in sort 0961 m_collator.compare(QString(), QString()); 0962 } 0963 0964 void KFileItemModel::resortAllItems() 0965 { 0966 m_resortAllItemsTimer->stop(); 0967 0968 const int itemCount = count(); 0969 if (itemCount <= 0) { 0970 return; 0971 } 0972 0973 #ifdef KFILEITEMMODEL_DEBUG 0974 QElapsedTimer timer; 0975 timer.start(); 0976 qCDebug(DolphinDebug) << "==========================================================="; 0977 qCDebug(DolphinDebug) << "Resorting" << itemCount << "items"; 0978 #endif 0979 0980 // Remember the order of the current URLs so 0981 // that it can be determined which indexes have 0982 // been moved because of the resorting. 0983 QList<QUrl> oldUrls; 0984 oldUrls.reserve(itemCount); 0985 for (const ItemData *itemData : std::as_const(m_itemData)) { 0986 oldUrls.append(itemData->item.url()); 0987 } 0988 0989 m_items.clear(); 0990 m_items.reserve(itemCount); 0991 0992 // Resort the items 0993 sort(m_itemData.begin(), m_itemData.end()); 0994 for (int i = 0; i < itemCount; ++i) { 0995 m_items.insert(m_itemData.at(i)->item.url(), i); 0996 } 0997 0998 // Determine the first index that has been moved. 0999 int firstMovedIndex = 0; 1000 while (firstMovedIndex < itemCount && firstMovedIndex == m_items.value(oldUrls.at(firstMovedIndex))) { 1001 ++firstMovedIndex; 1002 } 1003 1004 const bool itemsHaveMoved = firstMovedIndex < itemCount; 1005 if (itemsHaveMoved) { 1006 m_groups.clear(); 1007 1008 int lastMovedIndex = itemCount - 1; 1009 while (lastMovedIndex > firstMovedIndex && lastMovedIndex == m_items.value(oldUrls.at(lastMovedIndex))) { 1010 --lastMovedIndex; 1011 } 1012 1013 Q_ASSERT(firstMovedIndex <= lastMovedIndex); 1014 1015 // Create a list movedToIndexes, which has the property that 1016 // movedToIndexes[i] is the new index of the item with the old index 1017 // firstMovedIndex + i. 1018 const int movedItemsCount = lastMovedIndex - firstMovedIndex + 1; 1019 QList<int> movedToIndexes; 1020 movedToIndexes.reserve(movedItemsCount); 1021 for (int i = firstMovedIndex; i <= lastMovedIndex; ++i) { 1022 const int newIndex = m_items.value(oldUrls.at(i)); 1023 movedToIndexes.append(newIndex); 1024 } 1025 1026 Q_EMIT itemsMoved(KItemRange(firstMovedIndex, movedItemsCount), movedToIndexes); 1027 } else if (groupedSorting()) { 1028 // The groups might have changed even if the order of the items has not. 1029 const QList<QPair<int, QVariant>> oldGroups = m_groups; 1030 m_groups.clear(); 1031 if (groups() != oldGroups) { 1032 Q_EMIT groupsChanged(); 1033 } 1034 } 1035 1036 #ifdef KFILEITEMMODEL_DEBUG 1037 qCDebug(DolphinDebug) << "[TIME] Resorting of" << itemCount << "items:" << timer.elapsed(); 1038 #endif 1039 } 1040 1041 void KFileItemModel::slotCompleted() 1042 { 1043 m_maximumUpdateIntervalTimer->stop(); 1044 dispatchPendingItemsToInsert(); 1045 1046 if (!m_urlsToExpand.isEmpty()) { 1047 // Try to find a URL that can be expanded. 1048 // Note that the parent folder must be expanded before any of its subfolders become visible. 1049 // Therefore, some URLs in m_restoredExpandedUrls might not be visible yet 1050 // -> we expand the first visible URL we find in m_restoredExpandedUrls. 1051 // Iterate over a const copy because items are deleted and inserted within the loop 1052 const auto urlsToExpand = m_urlsToExpand; 1053 for (const QUrl &url : urlsToExpand) { 1054 const int indexForUrl = index(url); 1055 if (indexForUrl >= 0) { 1056 m_urlsToExpand.remove(url); 1057 if (setExpanded(indexForUrl, true)) { 1058 // The dir lister has been triggered. This slot will be called 1059 // again after the directory has been expanded. 1060 return; 1061 } 1062 } 1063 } 1064 1065 // None of the URLs in m_restoredExpandedUrls could be found in the model. This can happen 1066 // if these URLs have been deleted in the meantime. 1067 m_urlsToExpand.clear(); 1068 } 1069 1070 Q_EMIT directoryLoadingCompleted(); 1071 } 1072 1073 void KFileItemModel::slotCanceled() 1074 { 1075 m_maximumUpdateIntervalTimer->stop(); 1076 dispatchPendingItemsToInsert(); 1077 1078 Q_EMIT directoryLoadingCanceled(); 1079 } 1080 1081 void KFileItemModel::slotItemsAdded(const QUrl &directoryUrl, const KFileItemList &items) 1082 { 1083 Q_ASSERT(!items.isEmpty()); 1084 1085 const QUrl parentUrl = m_expandedDirs.value(directoryUrl, directoryUrl.adjusted(QUrl::StripTrailingSlash)); 1086 1087 if (m_requestRole[ExpandedParentsCountRole]) { 1088 // If the expanding of items is enabled, the call 1089 // dirLister->openUrl(url, KDirLister::Keep) in KFileItemModel::setExpanded() 1090 // might result in emitting the same items twice due to the Keep-parameter. 1091 // This case happens if an item gets expanded, collapsed and expanded again 1092 // before the items could be loaded for the first expansion. 1093 if (index(items.first().url()) >= 0) { 1094 // The items are already part of the model. 1095 return; 1096 } 1097 1098 if (directoryUrl != directory()) { 1099 // To be able to compare whether the new items may be inserted as children 1100 // of a parent item the pending items must be added to the model first. 1101 dispatchPendingItemsToInsert(); 1102 } 1103 1104 // KDirLister keeps the children of items that got expanded once even if 1105 // they got collapsed again with KFileItemModel::setExpanded(false). So it must be 1106 // checked whether the parent for new items is still expanded. 1107 const int parentIndex = index(parentUrl); 1108 if (parentIndex >= 0 && !m_itemData[parentIndex]->values.value("isExpanded").toBool()) { 1109 // The parent is not expanded. 1110 return; 1111 } 1112 } 1113 1114 const QList<ItemData *> itemDataList = createItemDataList(parentUrl, items); 1115 1116 if (!m_filter.hasSetFilters()) { 1117 m_pendingItemsToInsert.append(itemDataList); 1118 } else { 1119 QSet<ItemData *> parentsToEnsureVisible; 1120 1121 // The name or type filter is active. Hide filtered items 1122 // before inserting them into the model and remember 1123 // the filtered items in m_filteredItems. 1124 for (ItemData *itemData : itemDataList) { 1125 if (m_filter.matches(itemData->item)) { 1126 m_pendingItemsToInsert.append(itemData); 1127 if (itemData->parent) { 1128 parentsToEnsureVisible.insert(itemData->parent); 1129 } 1130 } else { 1131 m_filteredItems.insert(itemData->item, itemData); 1132 } 1133 } 1134 1135 // Entire parental chains must be shown 1136 for (ItemData *parent : parentsToEnsureVisible) { 1137 for (; parent && m_filteredItems.remove(parent->item); parent = parent->parent) { 1138 m_pendingItemsToInsert.append(parent); 1139 } 1140 } 1141 } 1142 1143 if (!m_maximumUpdateIntervalTimer->isActive()) { 1144 // Assure that items get dispatched if no completed() or canceled() signal is 1145 // emitted during the maximum update interval. 1146 m_maximumUpdateIntervalTimer->start(); 1147 } 1148 1149 Q_EMIT fileItemsChanged({KFileItem(directoryUrl)}); 1150 } 1151 1152 int KFileItemModel::filterChildlessParents(KItemRangeList &removedItemRanges, const QSet<ItemData *> &parentsToEnsureVisible) 1153 { 1154 int filteredParentsCount = 0; 1155 // The childless parents not yet removed will always be right above the start of a removed range. 1156 // We iterate backwards to ensure the deepest folders are processed before their parents 1157 for (int i = removedItemRanges.size() - 1; i >= 0; i--) { 1158 KItemRange itemRange = removedItemRanges.at(i); 1159 const ItemData *const firstInRange = m_itemData.at(itemRange.index); 1160 ItemData *itemAbove = itemRange.index - 1 >= 0 ? m_itemData.at(itemRange.index - 1) : nullptr; 1161 const ItemData *const itemBelow = itemRange.index + itemRange.count < m_itemData.count() ? m_itemData.at(itemRange.index + itemRange.count) : nullptr; 1162 1163 if (itemAbove && firstInRange->parent == itemAbove && !m_filter.matches(itemAbove->item) && (!itemBelow || itemBelow->parent != itemAbove) 1164 && !parentsToEnsureVisible.contains(itemAbove)) { 1165 // The item above exists, is the parent, doesn't pass the filter, does not belong to parentsToEnsureVisible 1166 // and this deleted range covers all of its descendents, so none will be left. 1167 m_filteredItems.insert(itemAbove->item, itemAbove); 1168 // This range's starting index will be extended to include the parent above: 1169 --itemRange.index; 1170 ++itemRange.count; 1171 ++filteredParentsCount; 1172 KItemRange previousRange = i > 0 ? removedItemRanges.at(i - 1) : KItemRange(); 1173 // We must check if this caused the range to touch the previous range, if that's the case they shall be merged 1174 if (i > 0 && previousRange.index + previousRange.count == itemRange.index) { 1175 previousRange.count += itemRange.count; 1176 removedItemRanges.replace(i - 1, previousRange); 1177 removedItemRanges.removeAt(i); 1178 } else { 1179 removedItemRanges.replace(i, itemRange); 1180 // We must revisit this range in the next iteration since its starting index changed 1181 ++i; 1182 } 1183 } 1184 } 1185 return filteredParentsCount; 1186 } 1187 1188 void KFileItemModel::slotItemsDeleted(const KFileItemList &items) 1189 { 1190 dispatchPendingItemsToInsert(); 1191 1192 QVector<int> indexesToRemove; 1193 indexesToRemove.reserve(items.count()); 1194 KFileItemList dirsChanged; 1195 1196 const auto currentDir = directory(); 1197 1198 for (const KFileItem &item : items) { 1199 if (item.url() == currentDir) { 1200 Q_EMIT currentDirectoryRemoved(); 1201 return; 1202 } 1203 1204 const int indexForItem = index(item); 1205 if (indexForItem >= 0) { 1206 indexesToRemove.append(indexForItem); 1207 } else { 1208 // Probably the item has been filtered. 1209 QHash<KFileItem, ItemData *>::iterator it = m_filteredItems.find(item); 1210 if (it != m_filteredItems.end()) { 1211 delete it.value(); 1212 m_filteredItems.erase(it); 1213 } 1214 } 1215 1216 QUrl parentUrl = item.url().adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); 1217 if (dirsChanged.findByUrl(parentUrl).isNull()) { 1218 dirsChanged << KFileItem(parentUrl); 1219 } 1220 } 1221 1222 std::sort(indexesToRemove.begin(), indexesToRemove.end()); 1223 1224 if (m_requestRole[ExpandedParentsCountRole] && !m_expandedDirs.isEmpty()) { 1225 // Assure that removing a parent item also results in removing all children 1226 QVector<int> indexesToRemoveWithChildren; 1227 indexesToRemoveWithChildren.reserve(m_itemData.count()); 1228 1229 const int itemCount = m_itemData.count(); 1230 for (int index : std::as_const(indexesToRemove)) { 1231 indexesToRemoveWithChildren.append(index); 1232 1233 const int parentLevel = expandedParentsCount(index); 1234 int childIndex = index + 1; 1235 while (childIndex < itemCount && expandedParentsCount(childIndex) > parentLevel) { 1236 indexesToRemoveWithChildren.append(childIndex); 1237 ++childIndex; 1238 } 1239 } 1240 1241 indexesToRemove = indexesToRemoveWithChildren; 1242 } 1243 1244 KItemRangeList itemRanges = KItemRangeList::fromSortedContainer(indexesToRemove); 1245 removeFilteredChildren(itemRanges); 1246 1247 // This call will update itemRanges to include the childless parents that have been filtered. 1248 const int filteredParentsCount = filterChildlessParents(itemRanges); 1249 1250 // If any childless parents were filtered, then itemRanges got updated and now contains items that were really deleted 1251 // mixed with expanded folders that are just being filtered out. 1252 // If that's the case, we pass 'DeleteItemDataIfUnfiltered' as a hint 1253 // so removeItems() will check m_filteredItems to differentiate which is which. 1254 removeItems(itemRanges, filteredParentsCount > 0 ? DeleteItemDataIfUnfiltered : DeleteItemData); 1255 1256 Q_EMIT fileItemsChanged(dirsChanged); 1257 } 1258 1259 void KFileItemModel::slotRefreshItems(const QList<QPair<KFileItem, KFileItem>> &items) 1260 { 1261 Q_ASSERT(!items.isEmpty()); 1262 #ifdef KFILEITEMMODEL_DEBUG 1263 qCDebug(DolphinDebug) << "Refreshing" << items.count() << "items"; 1264 #endif 1265 1266 // Get the indexes of all items that have been refreshed 1267 QList<int> indexes; 1268 indexes.reserve(items.count()); 1269 1270 QSet<QByteArray> changedRoles; 1271 KFileItemList changedFiles; 1272 1273 // Contains the indexes of the currently visible items 1274 // that should get hidden and hence moved to m_filteredItems. 1275 QVector<int> newFilteredIndexes; 1276 1277 // Contains currently hidden items that should 1278 // get visible and hence removed from m_filteredItems 1279 QList<ItemData *> newVisibleItems; 1280 1281 QListIterator<QPair<KFileItem, KFileItem>> it(items); 1282 1283 while (it.hasNext()) { 1284 const QPair<KFileItem, KFileItem> &itemPair = it.next(); 1285 const KFileItem &oldItem = itemPair.first; 1286 const KFileItem &newItem = itemPair.second; 1287 const int indexForItem = index(oldItem); 1288 const bool newItemMatchesFilter = m_filter.matches(newItem); 1289 if (indexForItem >= 0) { 1290 m_itemData[indexForItem]->item = newItem; 1291 1292 // Keep old values as long as possible if they could not retrieved synchronously yet. 1293 // The update of the values will be done asynchronously by KFileItemModelRolesUpdater. 1294 ItemData *const itemData = m_itemData.at(indexForItem); 1295 QHashIterator<QByteArray, QVariant> it(retrieveData(newItem, itemData->parent)); 1296 while (it.hasNext()) { 1297 it.next(); 1298 const QByteArray &role = it.key(); 1299 if (itemData->values.value(role) != it.value()) { 1300 itemData->values.insert(role, it.value()); 1301 changedRoles.insert(role); 1302 } 1303 } 1304 1305 m_items.remove(oldItem.url()); 1306 // We must maintain m_items consistent with m_itemData for now, this very loop is using it. 1307 // We leave it to be cleared by removeItems() later, when m_itemData actually gets updated. 1308 m_items.insert(newItem.url(), indexForItem); 1309 if (newItemMatchesFilter 1310 || (itemData->values.value("isExpanded").toBool() 1311 && (indexForItem + 1 < m_itemData.count() && m_itemData.at(indexForItem + 1)->parent == itemData))) { 1312 // We are lenient with expanded folders that originally had visible children. 1313 // If they become childless now they will be caught by filterChildlessParents() 1314 changedFiles.append(newItem); 1315 indexes.append(indexForItem); 1316 } else { 1317 newFilteredIndexes.append(indexForItem); 1318 m_filteredItems.insert(newItem, itemData); 1319 } 1320 } else { 1321 // Check if 'oldItem' is one of the filtered items. 1322 QHash<KFileItem, ItemData *>::iterator it = m_filteredItems.find(oldItem); 1323 if (it != m_filteredItems.end()) { 1324 ItemData *const itemData = it.value(); 1325 itemData->item = newItem; 1326 1327 // The data stored in 'values' might have changed. Therefore, we clear 1328 // 'values' and re-populate it the next time it is requested via data(int). 1329 // Before clearing, we must remember if it was expanded and the expanded parents count, 1330 // otherwise these states would be lost. The data() method will deal with this special case. 1331 const bool isExpanded = itemData->values.value("isExpanded").toBool(); 1332 bool hasExpandedParentsCount = false; 1333 const int expandedParentsCount = itemData->values.value("expandedParentsCount").toInt(&hasExpandedParentsCount); 1334 itemData->values.clear(); 1335 if (isExpanded) { 1336 itemData->values.insert("isExpanded", true); 1337 if (hasExpandedParentsCount) { 1338 itemData->values.insert("expandedParentsCount", expandedParentsCount); 1339 } 1340 } 1341 1342 m_filteredItems.erase(it); 1343 if (newItemMatchesFilter) { 1344 newVisibleItems.append(itemData); 1345 } else { 1346 m_filteredItems.insert(newItem, itemData); 1347 } 1348 } 1349 } 1350 } 1351 1352 std::sort(newFilteredIndexes.begin(), newFilteredIndexes.end()); 1353 1354 // We must keep track of parents of new visible items since they must be shown no matter what 1355 // They will be considered "immune" to filterChildlessParents() 1356 QSet<ItemData *> parentsToEnsureVisible; 1357 1358 for (ItemData *item : newVisibleItems) { 1359 for (ItemData *parent = item->parent; parent && !parentsToEnsureVisible.contains(parent); parent = parent->parent) { 1360 parentsToEnsureVisible.insert(parent); 1361 } 1362 } 1363 for (ItemData *parent : parentsToEnsureVisible) { 1364 // We make sure they are all unfiltered. 1365 if (m_filteredItems.remove(parent->item)) { 1366 // If it is being unfiltered now, we mark it to be inserted by appending it to newVisibleItems 1367 newVisibleItems.append(parent); 1368 // It could be in newFilteredIndexes, we must remove it if it's there: 1369 const int parentIndex = index(parent->item); 1370 if (parentIndex >= 0) { 1371 QVector<int>::iterator it = std::lower_bound(newFilteredIndexes.begin(), newFilteredIndexes.end(), parentIndex); 1372 if (it != newFilteredIndexes.end() && *it == parentIndex) { 1373 newFilteredIndexes.erase(it); 1374 } 1375 } 1376 } 1377 } 1378 1379 KItemRangeList removedRanges = KItemRangeList::fromSortedContainer(newFilteredIndexes); 1380 1381 // This call will update itemRanges to include the childless parents that have been filtered. 1382 filterChildlessParents(removedRanges, parentsToEnsureVisible); 1383 1384 removeItems(removedRanges, KeepItemData); 1385 1386 // Show previously hidden items that should get visible 1387 insertItems(newVisibleItems); 1388 1389 // Final step: we will emit 'itemsChanged' and 'fileItemsChanged' signals and trigger the asynchronous re-sorting logic. 1390 1391 // If the changed items have been created recently, they might not be in m_items yet. 1392 // In that case, the list 'indexes' might be empty. 1393 if (indexes.isEmpty()) { 1394 return; 1395 } 1396 1397 if (newVisibleItems.count() > 0 || removedRanges.count() > 0) { 1398 // The original indexes have changed and are now worthless since items were removed and/or inserted. 1399 indexes.clear(); 1400 // m_items is not yet rebuilt at this point, so we use our own means to resolve the new indexes. 1401 const QSet<const KFileItem> changedFilesSet(changedFiles.cbegin(), changedFiles.cend()); 1402 for (int i = 0; i < m_itemData.count(); i++) { 1403 if (changedFilesSet.contains(m_itemData.at(i)->item)) { 1404 indexes.append(i); 1405 } 1406 } 1407 } else { 1408 std::sort(indexes.begin(), indexes.end()); 1409 } 1410 1411 // Extract the item-ranges out of the changed indexes 1412 const KItemRangeList itemRangeList = KItemRangeList::fromSortedContainer(indexes); 1413 emitItemsChangedAndTriggerResorting(itemRangeList, changedRoles); 1414 1415 Q_EMIT fileItemsChanged(changedFiles); 1416 } 1417 1418 void KFileItemModel::slotClear() 1419 { 1420 #ifdef KFILEITEMMODEL_DEBUG 1421 qCDebug(DolphinDebug) << "Clearing all items"; 1422 #endif 1423 1424 qDeleteAll(m_filteredItems); 1425 m_filteredItems.clear(); 1426 m_groups.clear(); 1427 1428 m_maximumUpdateIntervalTimer->stop(); 1429 m_resortAllItemsTimer->stop(); 1430 1431 qDeleteAll(m_pendingItemsToInsert); 1432 m_pendingItemsToInsert.clear(); 1433 1434 const int removedCount = m_itemData.count(); 1435 if (removedCount > 0) { 1436 qDeleteAll(m_itemData); 1437 m_itemData.clear(); 1438 m_items.clear(); 1439 Q_EMIT itemsRemoved(KItemRangeList() << KItemRange(0, removedCount)); 1440 } 1441 1442 m_expandedDirs.clear(); 1443 } 1444 1445 void KFileItemModel::slotSortingChoiceChanged() 1446 { 1447 loadSortingSettings(); 1448 resortAllItems(); 1449 } 1450 1451 void KFileItemModel::dispatchPendingItemsToInsert() 1452 { 1453 if (!m_pendingItemsToInsert.isEmpty()) { 1454 insertItems(m_pendingItemsToInsert); 1455 m_pendingItemsToInsert.clear(); 1456 } 1457 } 1458 1459 void KFileItemModel::insertItems(QList<ItemData *> &newItems) 1460 { 1461 if (newItems.isEmpty()) { 1462 return; 1463 } 1464 1465 #ifdef KFILEITEMMODEL_DEBUG 1466 QElapsedTimer timer; 1467 timer.start(); 1468 qCDebug(DolphinDebug) << "==========================================================="; 1469 qCDebug(DolphinDebug) << "Inserting" << newItems.count() << "items"; 1470 #endif 1471 1472 m_groups.clear(); 1473 prepareItemsForSorting(newItems); 1474 1475 // Natural sorting of items can be very slow. However, it becomes much faster 1476 // if the input sequence is already mostly sorted. Therefore, we first sort 1477 // 'newItems' according to the QStrings using QString::operator<(), which is quite fast. 1478 if (m_naturalSorting) { 1479 if (m_sortRole == NameRole) { 1480 parallelMergeSort(newItems.begin(), newItems.end(), nameLessThan, QThread::idealThreadCount()); 1481 } else if (isRoleValueNatural(m_sortRole)) { 1482 auto lambdaLessThan = [&](const KFileItemModel::ItemData *a, const KFileItemModel::ItemData *b) { 1483 const QByteArray role = roleForType(m_sortRole); 1484 return a->values.value(role).toString() < b->values.value(role).toString(); 1485 }; 1486 parallelMergeSort(newItems.begin(), newItems.end(), lambdaLessThan, QThread::idealThreadCount()); 1487 } 1488 } 1489 1490 sort(newItems.begin(), newItems.end()); 1491 1492 #ifdef KFILEITEMMODEL_DEBUG 1493 qCDebug(DolphinDebug) << "[TIME] Sorting:" << timer.elapsed(); 1494 #endif 1495 1496 KItemRangeList itemRanges; 1497 const int existingItemCount = m_itemData.count(); 1498 const int newItemCount = newItems.count(); 1499 const int totalItemCount = existingItemCount + newItemCount; 1500 1501 if (existingItemCount == 0) { 1502 // Optimization for the common special case that there are no 1503 // items in the model yet. Happens, e.g., when entering a folder. 1504 m_itemData = newItems; 1505 itemRanges << KItemRange(0, newItemCount); 1506 } else { 1507 m_itemData.reserve(totalItemCount); 1508 for (int i = existingItemCount; i < totalItemCount; ++i) { 1509 m_itemData.append(nullptr); 1510 } 1511 1512 // We build the new list m_itemData in reverse order to minimize 1513 // the number of moves and guarantee O(N) complexity. 1514 int targetIndex = totalItemCount - 1; 1515 int sourceIndexExistingItems = existingItemCount - 1; 1516 int sourceIndexNewItems = newItemCount - 1; 1517 1518 int rangeCount = 0; 1519 1520 while (sourceIndexNewItems >= 0) { 1521 ItemData *newItem = newItems.at(sourceIndexNewItems); 1522 if (sourceIndexExistingItems >= 0 && lessThan(newItem, m_itemData.at(sourceIndexExistingItems), m_collator)) { 1523 // Move an existing item to its new position. If any new items 1524 // are behind it, push the item range to itemRanges. 1525 if (rangeCount > 0) { 1526 itemRanges << KItemRange(sourceIndexExistingItems + 1, rangeCount); 1527 rangeCount = 0; 1528 } 1529 1530 m_itemData[targetIndex] = m_itemData.at(sourceIndexExistingItems); 1531 --sourceIndexExistingItems; 1532 } else { 1533 // Insert a new item into the list. 1534 ++rangeCount; 1535 m_itemData[targetIndex] = newItem; 1536 --sourceIndexNewItems; 1537 } 1538 --targetIndex; 1539 } 1540 1541 // Push the final item range to itemRanges. 1542 if (rangeCount > 0) { 1543 itemRanges << KItemRange(sourceIndexExistingItems + 1, rangeCount); 1544 } 1545 1546 // Note that itemRanges is still sorted in reverse order. 1547 std::reverse(itemRanges.begin(), itemRanges.end()); 1548 } 1549 1550 // The indexes in m_items are not correct anymore. Therefore, we clear m_items. 1551 // It will be re-populated with the updated indices if index(const QUrl&) is called. 1552 m_items.clear(); 1553 1554 Q_EMIT itemsInserted(itemRanges); 1555 1556 #ifdef KFILEITEMMODEL_DEBUG 1557 qCDebug(DolphinDebug) << "[TIME] Inserting of" << newItems.count() << "items:" << timer.elapsed(); 1558 #endif 1559 } 1560 1561 void KFileItemModel::removeItems(const KItemRangeList &itemRanges, RemoveItemsBehavior behavior) 1562 { 1563 if (itemRanges.isEmpty()) { 1564 return; 1565 } 1566 1567 m_groups.clear(); 1568 1569 // Step 1: Remove the items from m_itemData, and free the ItemData. 1570 int removedItemsCount = 0; 1571 for (const KItemRange &range : itemRanges) { 1572 removedItemsCount += range.count; 1573 1574 for (int index = range.index; index < range.index + range.count; ++index) { 1575 if (behavior == DeleteItemData || (behavior == DeleteItemDataIfUnfiltered && !m_filteredItems.contains(m_itemData.at(index)->item))) { 1576 delete m_itemData.at(index); 1577 } 1578 1579 m_itemData[index] = nullptr; 1580 } 1581 } 1582 1583 // Step 2: Remove the ItemData pointers from the list m_itemData. 1584 int target = itemRanges.at(0).index; 1585 int source = itemRanges.at(0).index + itemRanges.at(0).count; 1586 int nextRange = 1; 1587 1588 const int oldItemDataCount = m_itemData.count(); 1589 while (source < oldItemDataCount) { 1590 m_itemData[target] = m_itemData[source]; 1591 ++target; 1592 ++source; 1593 1594 if (nextRange < itemRanges.count() && source == itemRanges.at(nextRange).index) { 1595 // Skip the items in the next removed range. 1596 source += itemRanges.at(nextRange).count; 1597 ++nextRange; 1598 } 1599 } 1600 1601 m_itemData.erase(m_itemData.end() - removedItemsCount, m_itemData.end()); 1602 1603 // The indexes in m_items are not correct anymore. Therefore, we clear m_items. 1604 // It will be re-populated with the updated indices if index(const QUrl&) is called. 1605 m_items.clear(); 1606 1607 Q_EMIT itemsRemoved(itemRanges); 1608 } 1609 1610 QList<KFileItemModel::ItemData *> KFileItemModel::createItemDataList(const QUrl &parentUrl, const KFileItemList &items) const 1611 { 1612 if (m_sortRole == TypeRole) { 1613 // Try to resolve the MIME-types synchronously to prevent a reordering of 1614 // the items when sorting by type (per default MIME-types are resolved 1615 // asynchronously by KFileItemModelRolesUpdater). 1616 determineMimeTypes(items, 200); 1617 } 1618 1619 // We search for the parent in m_itemData and then in m_filteredItems if necessary 1620 const int parentIndex = index(parentUrl); 1621 ItemData *parentItem = parentIndex < 0 ? m_filteredItems.value(KFileItem(parentUrl), nullptr) : m_itemData.at(parentIndex); 1622 1623 QList<ItemData *> itemDataList; 1624 itemDataList.reserve(items.count()); 1625 1626 for (const KFileItem &item : items) { 1627 ItemData *itemData = new ItemData(); 1628 itemData->item = item; 1629 itemData->parent = parentItem; 1630 itemDataList.append(itemData); 1631 } 1632 1633 return itemDataList; 1634 } 1635 1636 void KFileItemModel::prepareItemsForSorting(QList<ItemData *> &itemDataList) 1637 { 1638 switch (m_sortRole) { 1639 case ExtensionRole: 1640 case PermissionsRole: 1641 case OwnerRole: 1642 case GroupRole: 1643 case DestinationRole: 1644 case PathRole: 1645 case DeletionTimeRole: 1646 // These roles can be determined with retrieveData, and they have to be stored 1647 // in the QHash "values" for the sorting. 1648 for (ItemData *itemData : std::as_const(itemDataList)) { 1649 if (itemData->values.isEmpty()) { 1650 itemData->values = retrieveData(itemData->item, itemData->parent); 1651 } 1652 } 1653 break; 1654 1655 case TypeRole: 1656 // At least store the data including the file type for items with known MIME type. 1657 for (ItemData *itemData : std::as_const(itemDataList)) { 1658 if (itemData->values.isEmpty()) { 1659 const KFileItem item = itemData->item; 1660 if (item.isDir() || item.isMimeTypeKnown()) { 1661 itemData->values = retrieveData(itemData->item, itemData->parent); 1662 } 1663 } 1664 } 1665 break; 1666 1667 default: 1668 // The other roles are either resolved by KFileItemModelRolesUpdater 1669 // (this includes the SizeRole for directories), or they do not need 1670 // to be stored in the QHash "values" for sorting because the data can 1671 // be retrieved directly from the KFileItem (NameRole, SizeRole for files, 1672 // DateRole). 1673 break; 1674 } 1675 } 1676 1677 int KFileItemModel::expandedParentsCount(const ItemData *data) 1678 { 1679 // The hash 'values' is only guaranteed to contain the key "expandedParentsCount" 1680 // if the corresponding item is expanded, and it is not a top-level item. 1681 const ItemData *parent = data->parent; 1682 if (parent) { 1683 if (parent->parent) { 1684 Q_ASSERT(parent->values.contains("expandedParentsCount")); 1685 return parent->values.value("expandedParentsCount").toInt() + 1; 1686 } else { 1687 return 1; 1688 } 1689 } else { 1690 return 0; 1691 } 1692 } 1693 1694 void KFileItemModel::removeExpandedItems() 1695 { 1696 QVector<int> indexesToRemove; 1697 1698 const int maxIndex = m_itemData.count() - 1; 1699 for (int i = 0; i <= maxIndex; ++i) { 1700 const ItemData *itemData = m_itemData.at(i); 1701 if (itemData->parent) { 1702 indexesToRemove.append(i); 1703 } 1704 } 1705 1706 removeItems(KItemRangeList::fromSortedContainer(indexesToRemove), DeleteItemData); 1707 m_expandedDirs.clear(); 1708 1709 // Also remove all filtered items which have a parent. 1710 QHash<KFileItem, ItemData *>::iterator it = m_filteredItems.begin(); 1711 const QHash<KFileItem, ItemData *>::iterator end = m_filteredItems.end(); 1712 1713 while (it != end) { 1714 if (it.value()->parent) { 1715 delete it.value(); 1716 it = m_filteredItems.erase(it); 1717 } else { 1718 ++it; 1719 } 1720 } 1721 } 1722 1723 void KFileItemModel::emitItemsChangedAndTriggerResorting(const KItemRangeList &itemRanges, const QSet<QByteArray> &changedRoles) 1724 { 1725 Q_EMIT itemsChanged(itemRanges, changedRoles); 1726 1727 // Trigger a resorting if necessary. Note that this can happen even if the sort 1728 // role has not changed at all because the file name can be used as a fallback. 1729 if (changedRoles.contains(sortRole()) || changedRoles.contains(roleForType(NameRole)) 1730 || (changedRoles.contains("count") && sortRole() == "size")) { // "count" is used in the "size" sort role, so this might require a resorting. 1731 for (const KItemRange &range : itemRanges) { 1732 bool needsResorting = false; 1733 1734 const int first = range.index; 1735 const int last = range.index + range.count - 1; 1736 1737 // Resorting the model is necessary if 1738 // (a) The first item in the range is "lessThan" its predecessor, 1739 // (b) the successor of the last item is "lessThan" the last item, or 1740 // (c) the internal order of the items in the range is incorrect. 1741 if (first > 0 && lessThan(m_itemData.at(first), m_itemData.at(first - 1), m_collator)) { 1742 needsResorting = true; 1743 } else if (last < count() - 1 && lessThan(m_itemData.at(last + 1), m_itemData.at(last), m_collator)) { 1744 needsResorting = true; 1745 } else { 1746 for (int index = first; index < last; ++index) { 1747 if (lessThan(m_itemData.at(index + 1), m_itemData.at(index), m_collator)) { 1748 needsResorting = true; 1749 break; 1750 } 1751 } 1752 } 1753 1754 if (needsResorting) { 1755 scheduleResortAllItems(); 1756 return; 1757 } 1758 } 1759 } 1760 1761 if (groupedSorting() && changedRoles.contains(sortRole())) { 1762 // The position is still correct, but the groups might have changed 1763 // if the changed item is either the first or the last item in a 1764 // group. 1765 // In principle, we could try to find out if the item really is the 1766 // first or last one in its group and then update the groups 1767 // (possibly with a delayed timer to make sure that we don't 1768 // re-calculate the groups very often if items are updated one by 1769 // one), but starting m_resortAllItemsTimer is easier. 1770 m_resortAllItemsTimer->start(); 1771 } 1772 } 1773 1774 void KFileItemModel::resetRoles() 1775 { 1776 for (int i = 0; i < RolesCount; ++i) { 1777 m_requestRole[i] = false; 1778 } 1779 } 1780 1781 KFileItemModel::RoleType KFileItemModel::typeForRole(const QByteArray &role) const 1782 { 1783 static QHash<QByteArray, RoleType> roles; 1784 if (roles.isEmpty()) { 1785 // Insert user visible roles that can be accessed with 1786 // KFileItemModel::roleInformation() 1787 int count = 0; 1788 const RoleInfoMap *map = rolesInfoMap(count); 1789 for (int i = 0; i < count; ++i) { 1790 roles.insert(map[i].role, map[i].roleType); 1791 } 1792 1793 // Insert internal roles (take care to synchronize the implementation 1794 // with KFileItemModel::roleForType() in case if a change is done). 1795 roles.insert("isDir", IsDirRole); 1796 roles.insert("isLink", IsLinkRole); 1797 roles.insert("isHidden", IsHiddenRole); 1798 roles.insert("isExpanded", IsExpandedRole); 1799 roles.insert("isExpandable", IsExpandableRole); 1800 roles.insert("expandedParentsCount", ExpandedParentsCountRole); 1801 1802 Q_ASSERT(roles.count() == RolesCount); 1803 } 1804 1805 return roles.value(role, NoRole); 1806 } 1807 1808 QByteArray KFileItemModel::roleForType(RoleType roleType) const 1809 { 1810 static QHash<RoleType, QByteArray> roles; 1811 if (roles.isEmpty()) { 1812 // Insert user visible roles that can be accessed with 1813 // KFileItemModel::roleInformation() 1814 int count = 0; 1815 const RoleInfoMap *map = rolesInfoMap(count); 1816 for (int i = 0; i < count; ++i) { 1817 roles.insert(map[i].roleType, map[i].role); 1818 } 1819 1820 // Insert internal roles (take care to synchronize the implementation 1821 // with KFileItemModel::typeForRole() in case if a change is done). 1822 roles.insert(IsDirRole, "isDir"); 1823 roles.insert(IsLinkRole, "isLink"); 1824 roles.insert(IsHiddenRole, "isHidden"); 1825 roles.insert(IsExpandedRole, "isExpanded"); 1826 roles.insert(IsExpandableRole, "isExpandable"); 1827 roles.insert(ExpandedParentsCountRole, "expandedParentsCount"); 1828 1829 Q_ASSERT(roles.count() == RolesCount); 1830 }; 1831 1832 return roles.value(roleType); 1833 } 1834 1835 QHash<QByteArray, QVariant> KFileItemModel::retrieveData(const KFileItem &item, const ItemData *parent) const 1836 { 1837 // It is important to insert only roles that are fast to retrieve. E.g. 1838 // KFileItem::iconName() can be very expensive if the MIME-type is unknown 1839 // and hence will be retrieved asynchronously by KFileItemModelRolesUpdater. 1840 QHash<QByteArray, QVariant> data; 1841 data.insert(sharedValue("url"), item.url()); 1842 1843 const bool isDir = item.isDir(); 1844 if (m_requestRole[IsDirRole] && isDir) { 1845 data.insert(sharedValue("isDir"), true); 1846 } 1847 1848 if (m_requestRole[IsLinkRole] && item.isLink()) { 1849 data.insert(sharedValue("isLink"), true); 1850 } 1851 1852 if (m_requestRole[IsHiddenRole]) { 1853 data.insert(sharedValue("isHidden"), item.isHidden() || item.mimetype() == QStringLiteral("application/x-trash")); 1854 } 1855 1856 if (m_requestRole[NameRole]) { 1857 data.insert(sharedValue("text"), item.text()); 1858 } 1859 1860 if (m_requestRole[ExtensionRole] && !isDir) { 1861 // TODO KF6 use KFileItem::suffix 464722 1862 data.insert(sharedValue("extension"), QFileInfo(item.name()).suffix()); 1863 } 1864 1865 if (m_requestRole[SizeRole] && !isDir) { 1866 data.insert(sharedValue("size"), item.size()); 1867 } 1868 1869 if (m_requestRole[ModificationTimeRole]) { 1870 // Don't use KFileItem::timeString() or KFileItem::time() as this is too expensive when 1871 // having several thousands of items. Instead read the raw number from UDSEntry directly 1872 // and the formatting of the date-time will be done on-demand by the view when the date will be shown. 1873 const long long dateTime = item.entry().numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME, -1); 1874 data.insert(sharedValue("modificationtime"), dateTime); 1875 } 1876 1877 if (m_requestRole[CreationTimeRole]) { 1878 // Don't use KFileItem::timeString() or KFileItem::time() as this is too expensive when 1879 // having several thousands of items. Instead read the raw number from UDSEntry directly 1880 // and the formatting of the date-time will be done on-demand by the view when the date will be shown. 1881 const long long dateTime = item.entry().numberValue(KIO::UDSEntry::UDS_CREATION_TIME, -1); 1882 data.insert(sharedValue("creationtime"), dateTime); 1883 } 1884 1885 if (m_requestRole[AccessTimeRole]) { 1886 // Don't use KFileItem::timeString() or KFileItem::time() as this is too expensive when 1887 // having several thousands of items. Instead read the raw number from UDSEntry directly 1888 // and the formatting of the date-time will be done on-demand by the view when the date will be shown. 1889 const long long dateTime = item.entry().numberValue(KIO::UDSEntry::UDS_ACCESS_TIME, -1); 1890 data.insert(sharedValue("accesstime"), dateTime); 1891 } 1892 1893 if (m_requestRole[PermissionsRole]) { 1894 data.insert(sharedValue("permissions"), QVariantList() << item.permissionsString() << item.permissions()); 1895 } 1896 1897 if (m_requestRole[OwnerRole]) { 1898 data.insert(sharedValue("owner"), item.user()); 1899 } 1900 1901 if (m_requestRole[GroupRole]) { 1902 data.insert(sharedValue("group"), item.group()); 1903 } 1904 1905 if (m_requestRole[DestinationRole]) { 1906 QString destination = item.linkDest(); 1907 if (destination.isEmpty()) { 1908 destination = QLatin1Char('-'); 1909 } 1910 data.insert(sharedValue("destination"), destination); 1911 } 1912 1913 if (m_requestRole[PathRole]) { 1914 QString path; 1915 if (item.url().scheme() == QLatin1String("trash")) { 1916 path = item.entry().stringValue(KIO::UDSEntry::UDS_EXTRA); 1917 } else { 1918 // For performance reasons cache the home-path in a static QString 1919 // (see QDir::homePath() for more details) 1920 static QString homePath; 1921 if (homePath.isEmpty()) { 1922 homePath = QDir::homePath(); 1923 } 1924 1925 path = item.localPath(); 1926 if (path.startsWith(homePath)) { 1927 path.replace(0, homePath.length(), QLatin1Char('~')); 1928 } 1929 } 1930 1931 const int index = path.lastIndexOf(item.text()); 1932 path = path.mid(0, index - 1); 1933 data.insert(sharedValue("path"), path); 1934 } 1935 1936 if (m_requestRole[DeletionTimeRole]) { 1937 QDateTime deletionTime; 1938 if (item.url().scheme() == QLatin1String("trash")) { 1939 deletionTime = QDateTime::fromString(item.entry().stringValue(KIO::UDSEntry::UDS_EXTRA + 1), Qt::ISODate); 1940 } 1941 data.insert(sharedValue("deletiontime"), deletionTime); 1942 } 1943 1944 if (m_requestRole[IsExpandableRole] && isDir) { 1945 data.insert(sharedValue("isExpandable"), true); 1946 } 1947 1948 if (m_requestRole[ExpandedParentsCountRole]) { 1949 if (parent) { 1950 const int level = expandedParentsCount(parent) + 1; 1951 data.insert(sharedValue("expandedParentsCount"), level); 1952 } 1953 } 1954 1955 if (item.isMimeTypeKnown()) { 1956 QString iconName = item.iconName(); 1957 if (!QIcon::hasThemeIcon(iconName)) { 1958 QMimeType mimeType = QMimeDatabase().mimeTypeForName(item.mimetype()); 1959 iconName = mimeType.genericIconName(); 1960 } 1961 1962 data.insert(sharedValue("iconName"), iconName); 1963 1964 if (m_requestRole[TypeRole]) { 1965 data.insert(sharedValue("type"), item.mimeComment()); 1966 } 1967 } else if (m_requestRole[TypeRole] && isDir) { 1968 static const QString folderMimeType = item.mimeComment(); 1969 data.insert(sharedValue("type"), folderMimeType); 1970 } 1971 1972 return data; 1973 } 1974 1975 bool KFileItemModel::lessThan(const ItemData *a, const ItemData *b, const QCollator &collator) const 1976 { 1977 int result = 0; 1978 1979 if (a->parent != b->parent) { 1980 const int expansionLevelA = expandedParentsCount(a); 1981 const int expansionLevelB = expandedParentsCount(b); 1982 1983 // If b has a higher expansion level than a, check if a is a parent 1984 // of b, and make sure that both expansion levels are equal otherwise. 1985 for (int i = expansionLevelB; i > expansionLevelA; --i) { 1986 if (b->parent == a) { 1987 return true; 1988 } 1989 b = b->parent; 1990 } 1991 1992 // If a has a higher expansion level than a, check if b is a parent 1993 // of a, and make sure that both expansion levels are equal otherwise. 1994 for (int i = expansionLevelA; i > expansionLevelB; --i) { 1995 if (a->parent == b) { 1996 return false; 1997 } 1998 a = a->parent; 1999 } 2000 2001 Q_ASSERT(expandedParentsCount(a) == expandedParentsCount(b)); 2002 2003 // Compare the last parents of a and b which are different. 2004 while (a->parent != b->parent) { 2005 a = a->parent; 2006 b = b->parent; 2007 } 2008 } 2009 2010 // Show hidden files and folders last 2011 if (m_sortHiddenLast) { 2012 const bool isHiddenA = a->item.isHidden(); 2013 const bool isHiddenB = b->item.isHidden(); 2014 if (isHiddenA && !isHiddenB) { 2015 return false; 2016 } else if (!isHiddenA && isHiddenB) { 2017 return true; 2018 } 2019 } 2020 2021 if (m_sortDirsFirst || (ContentDisplaySettings::directorySizeCount() && m_sortRole == SizeRole)) { 2022 const bool isDirA = a->item.isDir(); 2023 const bool isDirB = b->item.isDir(); 2024 if (isDirA && !isDirB) { 2025 return true; 2026 } else if (!isDirA && isDirB) { 2027 return false; 2028 } 2029 } 2030 2031 result = sortRoleCompare(a, b, collator); 2032 2033 return (sortOrder() == Qt::AscendingOrder) ? result < 0 : result > 0; 2034 } 2035 2036 void KFileItemModel::sort(const QList<KFileItemModel::ItemData *>::iterator &begin, const QList<KFileItemModel::ItemData *>::iterator &end) const 2037 { 2038 auto lambdaLessThan = [&](const KFileItemModel::ItemData *a, const KFileItemModel::ItemData *b) { 2039 return lessThan(a, b, m_collator); 2040 }; 2041 2042 if (m_sortRole == NameRole || isRoleValueNatural(m_sortRole)) { 2043 // Sorting by string can be expensive, in particular if natural sorting is 2044 // enabled. Use all CPU cores to speed up the sorting process. 2045 static const int numberOfThreads = QThread::idealThreadCount(); 2046 parallelMergeSort(begin, end, lambdaLessThan, numberOfThreads); 2047 } else { 2048 // Sorting by other roles is quite fast. Use only one thread to prevent 2049 // problems caused by non-reentrant comparison functions, see 2050 // https://bugs.kde.org/show_bug.cgi?id=312679 2051 mergeSort(begin, end, lambdaLessThan); 2052 } 2053 } 2054 2055 int KFileItemModel::sortRoleCompare(const ItemData *a, const ItemData *b, const QCollator &collator) const 2056 { 2057 // This function must never return 0, because that would break stable 2058 // sorting, which leads to all kinds of bugs. 2059 // See: https://bugs.kde.org/show_bug.cgi?id=433247 2060 // If two items have equal sort values, let the fallbacks at the bottom of 2061 // the function handle it. 2062 const KFileItem &itemA = a->item; 2063 const KFileItem &itemB = b->item; 2064 2065 int result = 0; 2066 2067 switch (m_sortRole) { 2068 case NameRole: 2069 // The name role is handled as default fallback after the switch 2070 break; 2071 2072 case SizeRole: { 2073 if (ContentDisplaySettings::directorySizeCount() && itemA.isDir()) { 2074 // folders first then 2075 // items A and B are folders thanks to lessThan checks 2076 auto valueA = a->values.value("count"); 2077 auto valueB = b->values.value("count"); 2078 if (valueA.isNull()) { 2079 if (!valueB.isNull()) { 2080 return -1; 2081 } 2082 } else if (valueB.isNull()) { 2083 return +1; 2084 } else { 2085 if (valueA.toLongLong() < valueB.toLongLong()) { 2086 return -1; 2087 } else if (valueA.toLongLong() > valueB.toLongLong()) { 2088 return +1; 2089 } 2090 } 2091 break; 2092 } 2093 2094 KIO::filesize_t sizeA = 0; 2095 if (itemA.isDir()) { 2096 sizeA = a->values.value("size").toULongLong(); 2097 } else { 2098 sizeA = itemA.size(); 2099 } 2100 KIO::filesize_t sizeB = 0; 2101 if (itemB.isDir()) { 2102 sizeB = b->values.value("size").toULongLong(); 2103 } else { 2104 sizeB = itemB.size(); 2105 } 2106 if (sizeA < sizeB) { 2107 return -1; 2108 } else if (sizeA > sizeB) { 2109 return +1; 2110 } 2111 break; 2112 } 2113 2114 case ModificationTimeRole: { 2115 const long long dateTimeA = itemA.entry().numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME, -1); 2116 const long long dateTimeB = itemB.entry().numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME, -1); 2117 if (dateTimeA < dateTimeB) { 2118 return -1; 2119 } else if (dateTimeA > dateTimeB) { 2120 return +1; 2121 } 2122 break; 2123 } 2124 2125 case AccessTimeRole: { 2126 const long long dateTimeA = itemA.entry().numberValue(KIO::UDSEntry::UDS_ACCESS_TIME, -1); 2127 const long long dateTimeB = itemB.entry().numberValue(KIO::UDSEntry::UDS_ACCESS_TIME, -1); 2128 if (dateTimeA < dateTimeB) { 2129 return -1; 2130 } else if (dateTimeA > dateTimeB) { 2131 return +1; 2132 } 2133 break; 2134 } 2135 2136 case CreationTimeRole: { 2137 const long long dateTimeA = itemA.entry().numberValue(KIO::UDSEntry::UDS_CREATION_TIME, -1); 2138 const long long dateTimeB = itemB.entry().numberValue(KIO::UDSEntry::UDS_CREATION_TIME, -1); 2139 if (dateTimeA < dateTimeB) { 2140 return -1; 2141 } else if (dateTimeA > dateTimeB) { 2142 return +1; 2143 } 2144 break; 2145 } 2146 2147 case DeletionTimeRole: { 2148 const QDateTime dateTimeA = a->values.value("deletiontime").toDateTime(); 2149 const QDateTime dateTimeB = b->values.value("deletiontime").toDateTime(); 2150 if (dateTimeA < dateTimeB) { 2151 return -1; 2152 } else if (dateTimeA > dateTimeB) { 2153 return +1; 2154 } 2155 break; 2156 } 2157 2158 case RatingRole: 2159 case WidthRole: 2160 case HeightRole: 2161 case PublisherRole: 2162 case PageCountRole: 2163 case WordCountRole: 2164 case LineCountRole: 2165 case TrackRole: 2166 case ReleaseYearRole: { 2167 result = a->values.value(roleForType(m_sortRole)).toInt() - b->values.value(roleForType(m_sortRole)).toInt(); 2168 break; 2169 } 2170 2171 case DimensionsRole: { 2172 const QByteArray role = roleForType(m_sortRole); 2173 const QSize dimensionsA = a->values.value(role).toSize(); 2174 const QSize dimensionsB = b->values.value(role).toSize(); 2175 2176 if (dimensionsA.width() == dimensionsB.width()) { 2177 result = dimensionsA.height() - dimensionsB.height(); 2178 } else { 2179 result = dimensionsA.width() - dimensionsB.width(); 2180 } 2181 break; 2182 } 2183 2184 default: { 2185 const QByteArray role = roleForType(m_sortRole); 2186 const QString roleValueA = a->values.value(role).toString(); 2187 const QString roleValueB = b->values.value(role).toString(); 2188 if (!roleValueA.isEmpty() && roleValueB.isEmpty()) { 2189 return -1; 2190 } else if (roleValueA.isEmpty() && !roleValueB.isEmpty()) { 2191 return +1; 2192 } else if (isRoleValueNatural(m_sortRole)) { 2193 result = stringCompare(roleValueA, roleValueB, collator); 2194 } else { 2195 result = QString::compare(roleValueA, roleValueB); 2196 } 2197 break; 2198 } 2199 } 2200 2201 if (result != 0) { 2202 // The current sort role was sufficient to define an order 2203 return result; 2204 } 2205 2206 // Fallback #1: Compare the text of the items 2207 result = stringCompare(itemA.text(), itemB.text(), collator); 2208 if (result != 0) { 2209 return result; 2210 } 2211 2212 // Fallback #2: KFileItem::text() may not be unique in case UDS_DISPLAY_NAME is used 2213 result = stringCompare(itemA.name(), itemB.name(), collator); 2214 if (result != 0) { 2215 return result; 2216 } 2217 2218 // Fallback #3: It must be assured that the sort order is always unique even if two values have been 2219 // equal. In this case a comparison of the URL is done which is unique in all cases 2220 // within KDirLister. 2221 return QString::compare(itemA.url().url(), itemB.url().url(), Qt::CaseSensitive); 2222 } 2223 2224 int KFileItemModel::stringCompare(const QString &a, const QString &b, const QCollator &collator) const 2225 { 2226 QMutexLocker collatorLock(s_collatorMutex()); 2227 2228 if (m_naturalSorting) { 2229 return collator.compare(a, b); 2230 } 2231 2232 const int result = QString::compare(a, b, collator.caseSensitivity()); 2233 if (result != 0 || collator.caseSensitivity() == Qt::CaseSensitive) { 2234 // Only return the result, if the strings are not equal. If they are equal by a case insensitive 2235 // comparison, still a deterministic sort order is required. A case sensitive 2236 // comparison is done as fallback. 2237 return result; 2238 } 2239 2240 return QString::compare(a, b, Qt::CaseSensitive); 2241 } 2242 2243 QList<QPair<int, QVariant>> KFileItemModel::nameRoleGroups() const 2244 { 2245 Q_ASSERT(!m_itemData.isEmpty()); 2246 2247 const int maxIndex = count() - 1; 2248 QList<QPair<int, QVariant>> groups; 2249 2250 QString groupValue; 2251 QChar firstChar; 2252 for (int i = 0; i <= maxIndex; ++i) { 2253 if (isChildItem(i)) { 2254 continue; 2255 } 2256 2257 const QString name = m_itemData.at(i)->item.text(); 2258 2259 // Use the first character of the name as group indication 2260 QChar newFirstChar = name.at(0).toUpper(); 2261 if (newFirstChar == QLatin1Char('~') && name.length() > 1) { 2262 newFirstChar = name.at(1).toUpper(); 2263 } 2264 2265 if (firstChar != newFirstChar) { 2266 QString newGroupValue; 2267 if (newFirstChar.isLetter()) { 2268 if (m_collator.compare(newFirstChar, QChar(QLatin1Char('A'))) >= 0 && m_collator.compare(newFirstChar, QChar(QLatin1Char('Z'))) <= 0) { 2269 // WARNING! Symbols based on latin 'Z' like 'Z' with acute are treated wrong as non Latin and put in a new group. 2270 2271 // Try to find a matching group in the range 'A' to 'Z'. 2272 static std::vector<QChar> lettersAtoZ; 2273 lettersAtoZ.reserve('Z' - 'A' + 1); 2274 if (lettersAtoZ.empty()) { 2275 for (char c = 'A'; c <= 'Z'; ++c) { 2276 lettersAtoZ.push_back(QLatin1Char(c)); 2277 } 2278 } 2279 2280 auto localeAwareLessThan = [this](QChar c1, QChar c2) -> bool { 2281 return m_collator.compare(c1, c2) < 0; 2282 }; 2283 2284 std::vector<QChar>::iterator it = std::lower_bound(lettersAtoZ.begin(), lettersAtoZ.end(), newFirstChar, localeAwareLessThan); 2285 if (it != lettersAtoZ.end()) { 2286 if (localeAwareLessThan(newFirstChar, *it)) { 2287 // newFirstChar belongs to the group preceding *it. 2288 // Example: for an umlaut 'A' in the German locale, *it would be 'B' now. 2289 --it; 2290 } 2291 newGroupValue = *it; 2292 } 2293 2294 } else { 2295 // Symbols from non Latin-based scripts 2296 newGroupValue = newFirstChar; 2297 } 2298 } else if (newFirstChar >= QLatin1Char('0') && newFirstChar <= QLatin1Char('9')) { 2299 // Apply group '0 - 9' for any name that starts with a digit 2300 newGroupValue = i18nc("@title:group Groups that start with a digit", "0 - 9"); 2301 } else { 2302 newGroupValue = i18nc("@title:group", "Others"); 2303 } 2304 2305 if (newGroupValue != groupValue) { 2306 groupValue = newGroupValue; 2307 groups.append(QPair<int, QVariant>(i, newGroupValue)); 2308 } 2309 2310 firstChar = newFirstChar; 2311 } 2312 } 2313 return groups; 2314 } 2315 2316 QList<QPair<int, QVariant>> KFileItemModel::sizeRoleGroups() const 2317 { 2318 Q_ASSERT(!m_itemData.isEmpty()); 2319 2320 const int maxIndex = count() - 1; 2321 QList<QPair<int, QVariant>> groups; 2322 2323 QString groupValue; 2324 for (int i = 0; i <= maxIndex; ++i) { 2325 if (isChildItem(i)) { 2326 continue; 2327 } 2328 2329 const KFileItem &item = m_itemData.at(i)->item; 2330 KIO::filesize_t fileSize = !item.isNull() ? item.size() : ~0U; 2331 QString newGroupValue; 2332 if (!item.isNull() && item.isDir()) { 2333 if (ContentDisplaySettings::directorySizeCount() || m_sortDirsFirst) { 2334 newGroupValue = i18nc("@title:group Size", "Folders"); 2335 } else { 2336 fileSize = m_itemData.at(i)->values.value("size").toULongLong(); 2337 } 2338 } 2339 2340 if (newGroupValue.isEmpty()) { 2341 if (fileSize < 5 * 1024 * 1024) { // < 5 MB 2342 newGroupValue = i18nc("@title:group Size", "Small"); 2343 } else if (fileSize < 10 * 1024 * 1024) { // < 10 MB 2344 newGroupValue = i18nc("@title:group Size", "Medium"); 2345 } else { 2346 newGroupValue = i18nc("@title:group Size", "Big"); 2347 } 2348 } 2349 2350 if (newGroupValue != groupValue) { 2351 groupValue = newGroupValue; 2352 groups.append(QPair<int, QVariant>(i, newGroupValue)); 2353 } 2354 } 2355 2356 return groups; 2357 } 2358 2359 QList<QPair<int, QVariant>> KFileItemModel::timeRoleGroups(const std::function<QDateTime(const ItemData *)> &fileTimeCb) const 2360 { 2361 Q_ASSERT(!m_itemData.isEmpty()); 2362 2363 const int maxIndex = count() - 1; 2364 QList<QPair<int, QVariant>> groups; 2365 2366 const QDate currentDate = QDate::currentDate(); 2367 2368 QDate previousFileDate; 2369 QString groupValue; 2370 for (int i = 0; i <= maxIndex; ++i) { 2371 if (isChildItem(i)) { 2372 continue; 2373 } 2374 2375 const QDateTime fileTime = fileTimeCb(m_itemData.at(i)); 2376 const QDate fileDate = fileTime.date(); 2377 if (fileDate == previousFileDate) { 2378 // The current item is in the same group as the previous item 2379 continue; 2380 } 2381 previousFileDate = fileDate; 2382 2383 const int daysDistance = fileDate.daysTo(currentDate); 2384 2385 QString newGroupValue; 2386 if (currentDate.year() == fileDate.year() && currentDate.month() == fileDate.month()) { 2387 switch (daysDistance / 7) { 2388 case 0: 2389 switch (daysDistance) { 2390 case 0: 2391 newGroupValue = i18nc("@title:group Date", "Today"); 2392 break; 2393 case 1: 2394 newGroupValue = i18nc("@title:group Date", "Yesterday"); 2395 break; 2396 default: 2397 newGroupValue = fileTime.toString(i18nc("@title:group Date: The week day name: dddd", "dddd")); 2398 newGroupValue = i18nc( 2399 "Can be used to script translation of \"dddd\"" 2400 "with context @title:group Date", 2401 "%1", 2402 newGroupValue); 2403 } 2404 break; 2405 case 1: 2406 newGroupValue = i18nc("@title:group Date", "One Week Ago"); 2407 break; 2408 case 2: 2409 newGroupValue = i18nc("@title:group Date", "Two Weeks Ago"); 2410 break; 2411 case 3: 2412 newGroupValue = i18nc("@title:group Date", "Three Weeks Ago"); 2413 break; 2414 case 4: 2415 case 5: 2416 newGroupValue = i18nc("@title:group Date", "Earlier this Month"); 2417 break; 2418 default: 2419 Q_ASSERT(false); 2420 } 2421 } else { 2422 const QDate lastMonthDate = currentDate.addMonths(-1); 2423 if (lastMonthDate.year() == fileDate.year() && lastMonthDate.month() == fileDate.month()) { 2424 if (daysDistance == 1) { 2425 const KLocalizedString format = ki18nc( 2426 "@title:group Date: " 2427 "MMMM is full month name in current locale, and yyyy is " 2428 "full year number. You must keep the ' don't use any fancy \" or « or similar. The ' is not shown to the user, it's there to mark a " 2429 "part of the text that should not be formatted as a date", 2430 "'Yesterday' (MMMM, yyyy)"); 2431 const QString translatedFormat = format.toString(); 2432 if (translatedFormat.count(QLatin1Char('\'')) == 2) { 2433 newGroupValue = fileTime.toString(translatedFormat); 2434 newGroupValue = i18nc( 2435 "Can be used to script translation of " 2436 "\"'Yesterday' (MMMM, yyyy)\" with context @title:group Date", 2437 "%1", 2438 newGroupValue); 2439 } else { 2440 qCWarning(DolphinDebug).nospace() 2441 << "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org"; 2442 const QString untranslatedFormat = format.toString({QLatin1String("en_US")}); 2443 newGroupValue = fileTime.toString(untranslatedFormat); 2444 } 2445 } else if (daysDistance <= 7) { 2446 newGroupValue = 2447 fileTime.toString(i18nc("@title:group Date: " 2448 "The week day name: dddd, MMMM is full month name " 2449 "in current locale, and yyyy is full year number.", 2450 "dddd (MMMM, yyyy)")); 2451 newGroupValue = i18nc( 2452 "Can be used to script translation of " 2453 "\"dddd (MMMM, yyyy)\" with context @title:group Date", 2454 "%1", 2455 newGroupValue); 2456 } else if (daysDistance <= 7 * 2) { 2457 const KLocalizedString format = ki18nc( 2458 "@title:group Date: " 2459 "MMMM is full month name in current locale, and yyyy is " 2460 "full year number. You must keep the ' don't use any fancy \" or « or similar. The ' is not shown to the user, it's there to mark a " 2461 "part of the text that should not be formatted as a date", 2462 "'One Week Ago' (MMMM, yyyy)"); 2463 const QString translatedFormat = format.toString(); 2464 if (translatedFormat.count(QLatin1Char('\'')) == 2) { 2465 newGroupValue = fileTime.toString(translatedFormat); 2466 newGroupValue = i18nc( 2467 "Can be used to script translation of " 2468 "\"'One Week Ago' (MMMM, yyyy)\" with context @title:group Date", 2469 "%1", 2470 newGroupValue); 2471 } else { 2472 qCWarning(DolphinDebug).nospace() 2473 << "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org"; 2474 const QString untranslatedFormat = format.toString({QLatin1String("en_US")}); 2475 newGroupValue = fileTime.toString(untranslatedFormat); 2476 } 2477 } else if (daysDistance <= 7 * 3) { 2478 const KLocalizedString format = ki18nc( 2479 "@title:group Date: " 2480 "MMMM is full month name in current locale, and yyyy is " 2481 "full year number. You must keep the ' don't use any fancy \" or « or similar. The ' is not shown to the user, it's there to mark a " 2482 "part of the text that should not be formatted as a date", 2483 "'Two Weeks Ago' (MMMM, yyyy)"); 2484 const QString translatedFormat = format.toString(); 2485 if (translatedFormat.count(QLatin1Char('\'')) == 2) { 2486 newGroupValue = fileTime.toString(translatedFormat); 2487 newGroupValue = i18nc( 2488 "Can be used to script translation of " 2489 "\"'Two Weeks Ago' (MMMM, yyyy)\" with context @title:group Date", 2490 "%1", 2491 newGroupValue); 2492 } else { 2493 qCWarning(DolphinDebug).nospace() 2494 << "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org"; 2495 const QString untranslatedFormat = format.toString({QLatin1String("en_US")}); 2496 newGroupValue = fileTime.toString(untranslatedFormat); 2497 } 2498 } else if (daysDistance <= 7 * 4) { 2499 const KLocalizedString format = ki18nc( 2500 "@title:group Date: " 2501 "MMMM is full month name in current locale, and yyyy is " 2502 "full year number. You must keep the ' don't use any fancy \" or « or similar. The ' is not shown to the user, it's there to mark a " 2503 "part of the text that should not be formatted as a date", 2504 "'Three Weeks Ago' (MMMM, yyyy)"); 2505 const QString translatedFormat = format.toString(); 2506 if (translatedFormat.count(QLatin1Char('\'')) == 2) { 2507 newGroupValue = fileTime.toString(translatedFormat); 2508 newGroupValue = i18nc( 2509 "Can be used to script translation of " 2510 "\"'Three Weeks Ago' (MMMM, yyyy)\" with context @title:group Date", 2511 "%1", 2512 newGroupValue); 2513 } else { 2514 qCWarning(DolphinDebug).nospace() 2515 << "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org"; 2516 const QString untranslatedFormat = format.toString({QLatin1String("en_US")}); 2517 newGroupValue = fileTime.toString(untranslatedFormat); 2518 } 2519 } else { 2520 const KLocalizedString format = ki18nc( 2521 "@title:group Date: " 2522 "MMMM is full month name in current locale, and yyyy is " 2523 "full year number. You must keep the ' don't use any fancy \" or « or similar. The ' is not shown to the user, it's there to mark a " 2524 "part of the text that should not be formatted as a date", 2525 "'Earlier on' MMMM, yyyy"); 2526 const QString translatedFormat = format.toString(); 2527 if (translatedFormat.count(QLatin1Char('\'')) == 2) { 2528 newGroupValue = fileTime.toString(translatedFormat); 2529 newGroupValue = i18nc( 2530 "Can be used to script translation of " 2531 "\"'Earlier on' MMMM, yyyy\" with context @title:group Date", 2532 "%1", 2533 newGroupValue); 2534 } else { 2535 qCWarning(DolphinDebug).nospace() 2536 << "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org"; 2537 const QString untranslatedFormat = format.toString({QLatin1String("en_US")}); 2538 newGroupValue = fileTime.toString(untranslatedFormat); 2539 } 2540 } 2541 } else { 2542 newGroupValue = 2543 fileTime.toString(i18nc("@title:group " 2544 "The month and year: MMMM is full month name in current locale, " 2545 "and yyyy is full year number", 2546 "MMMM, yyyy")); 2547 newGroupValue = i18nc( 2548 "Can be used to script translation of " 2549 "\"MMMM, yyyy\" with context @title:group Date", 2550 "%1", 2551 newGroupValue); 2552 } 2553 } 2554 2555 if (newGroupValue != groupValue) { 2556 groupValue = newGroupValue; 2557 groups.append(QPair<int, QVariant>(i, newGroupValue)); 2558 } 2559 } 2560 2561 return groups; 2562 } 2563 2564 QList<QPair<int, QVariant>> KFileItemModel::permissionRoleGroups() const 2565 { 2566 Q_ASSERT(!m_itemData.isEmpty()); 2567 2568 const int maxIndex = count() - 1; 2569 QList<QPair<int, QVariant>> groups; 2570 2571 QString permissionsString; 2572 QString groupValue; 2573 for (int i = 0; i <= maxIndex; ++i) { 2574 if (isChildItem(i)) { 2575 continue; 2576 } 2577 2578 const ItemData *itemData = m_itemData.at(i); 2579 const QString newPermissionsString = itemData->values.value("permissions").toString(); 2580 if (newPermissionsString == permissionsString) { 2581 continue; 2582 } 2583 permissionsString = newPermissionsString; 2584 2585 const QFileInfo info(itemData->item.url().toLocalFile()); 2586 2587 // Set user string 2588 QString user; 2589 if (info.permission(QFile::ReadUser)) { 2590 user = i18nc("@item:intext Access permission, concatenated", "Read, "); 2591 } 2592 if (info.permission(QFile::WriteUser)) { 2593 user += i18nc("@item:intext Access permission, concatenated", "Write, "); 2594 } 2595 if (info.permission(QFile::ExeUser)) { 2596 user += i18nc("@item:intext Access permission, concatenated", "Execute, "); 2597 } 2598 user = user.isEmpty() ? i18nc("@item:intext Access permission, concatenated", "Forbidden") : user.mid(0, user.length() - 2); 2599 2600 // Set group string 2601 QString group; 2602 if (info.permission(QFile::ReadGroup)) { 2603 group = i18nc("@item:intext Access permission, concatenated", "Read, "); 2604 } 2605 if (info.permission(QFile::WriteGroup)) { 2606 group += i18nc("@item:intext Access permission, concatenated", "Write, "); 2607 } 2608 if (info.permission(QFile::ExeGroup)) { 2609 group += i18nc("@item:intext Access permission, concatenated", "Execute, "); 2610 } 2611 group = group.isEmpty() ? i18nc("@item:intext Access permission, concatenated", "Forbidden") : group.mid(0, group.length() - 2); 2612 2613 // Set others string 2614 QString others; 2615 if (info.permission(QFile::ReadOther)) { 2616 others = i18nc("@item:intext Access permission, concatenated", "Read, "); 2617 } 2618 if (info.permission(QFile::WriteOther)) { 2619 others += i18nc("@item:intext Access permission, concatenated", "Write, "); 2620 } 2621 if (info.permission(QFile::ExeOther)) { 2622 others += i18nc("@item:intext Access permission, concatenated", "Execute, "); 2623 } 2624 others = others.isEmpty() ? i18nc("@item:intext Access permission, concatenated", "Forbidden") : others.mid(0, others.length() - 2); 2625 2626 const QString newGroupValue = i18nc("@title:group Files and folders by permissions", "User: %1 | Group: %2 | Others: %3", user, group, others); 2627 if (newGroupValue != groupValue) { 2628 groupValue = newGroupValue; 2629 groups.append(QPair<int, QVariant>(i, newGroupValue)); 2630 } 2631 } 2632 2633 return groups; 2634 } 2635 2636 QList<QPair<int, QVariant>> KFileItemModel::ratingRoleGroups() const 2637 { 2638 Q_ASSERT(!m_itemData.isEmpty()); 2639 2640 const int maxIndex = count() - 1; 2641 QList<QPair<int, QVariant>> groups; 2642 2643 int groupValue = -1; 2644 for (int i = 0; i <= maxIndex; ++i) { 2645 if (isChildItem(i)) { 2646 continue; 2647 } 2648 const int newGroupValue = m_itemData.at(i)->values.value("rating", 0).toInt(); 2649 if (newGroupValue != groupValue) { 2650 groupValue = newGroupValue; 2651 groups.append(QPair<int, QVariant>(i, newGroupValue)); 2652 } 2653 } 2654 2655 return groups; 2656 } 2657 2658 QList<QPair<int, QVariant>> KFileItemModel::genericStringRoleGroups(const QByteArray &role) const 2659 { 2660 Q_ASSERT(!m_itemData.isEmpty()); 2661 2662 const int maxIndex = count() - 1; 2663 QList<QPair<int, QVariant>> groups; 2664 2665 bool isFirstGroupValue = true; 2666 QString groupValue; 2667 for (int i = 0; i <= maxIndex; ++i) { 2668 if (isChildItem(i)) { 2669 continue; 2670 } 2671 const QString newGroupValue = m_itemData.at(i)->values.value(role).toString(); 2672 if (newGroupValue != groupValue || isFirstGroupValue) { 2673 groupValue = newGroupValue; 2674 groups.append(QPair<int, QVariant>(i, newGroupValue)); 2675 isFirstGroupValue = false; 2676 } 2677 } 2678 2679 return groups; 2680 } 2681 2682 void KFileItemModel::emitSortProgress(int resolvedCount) 2683 { 2684 // Be tolerant against a resolvedCount with a wrong range. 2685 // Although there should not be a case where KFileItemModelRolesUpdater 2686 // (= caller) provides a wrong range, it is important to emit 2687 // a useful progress information even if there is an unexpected 2688 // implementation issue. 2689 2690 const int itemCount = count(); 2691 if (resolvedCount >= itemCount) { 2692 m_sortingProgressPercent = -1; 2693 if (m_resortAllItemsTimer->isActive()) { 2694 m_resortAllItemsTimer->stop(); 2695 resortAllItems(); 2696 } 2697 2698 Q_EMIT directorySortingProgress(100); 2699 } else if (itemCount > 0) { 2700 resolvedCount = qBound(0, resolvedCount, itemCount); 2701 2702 const int progress = resolvedCount * 100 / itemCount; 2703 if (m_sortingProgressPercent != progress) { 2704 m_sortingProgressPercent = progress; 2705 Q_EMIT directorySortingProgress(progress); 2706 } 2707 } 2708 } 2709 2710 const KFileItemModel::RoleInfoMap *KFileItemModel::rolesInfoMap(int &count) 2711 { 2712 static const RoleInfoMap rolesInfoMap[] = { 2713 // clang-format off 2714 // | role | roleType | role translation | group translation | requires Baloo | requires indexer 2715 { nullptr, NoRole, KLazyLocalizedString(), KLazyLocalizedString(), KLazyLocalizedString(), false, false }, 2716 { "text", NameRole, kli18nc("@label", "Name"), KLazyLocalizedString(), KLazyLocalizedString(), false, false }, 2717 { "size", SizeRole, kli18nc("@label", "Size"), KLazyLocalizedString(), KLazyLocalizedString(), false, false }, 2718 { "modificationtime", ModificationTimeRole, kli18nc("@label", "Modified"), KLazyLocalizedString(), kli18nc("@tooltip", "The date format can be selected in settings."), false, false }, 2719 { "creationtime", CreationTimeRole, kli18nc("@label", "Created"), KLazyLocalizedString(), kli18nc("@tooltip", "The date format can be selected in settings."), false, false }, 2720 { "accesstime", AccessTimeRole, kli18nc("@label", "Accessed"), KLazyLocalizedString(), kli18nc("@tooltip", "The date format can be selected in settings."), false, false }, 2721 { "type", TypeRole, kli18nc("@label", "Type"), KLazyLocalizedString(), KLazyLocalizedString(), false, false }, 2722 { "rating", RatingRole, kli18nc("@label", "Rating"), KLazyLocalizedString(), KLazyLocalizedString(), true, false }, 2723 { "tags", TagsRole, kli18nc("@label", "Tags"), KLazyLocalizedString(), KLazyLocalizedString(), true, false }, 2724 { "comment", CommentRole, kli18nc("@label", "Comment"), KLazyLocalizedString(), KLazyLocalizedString(), true, false }, 2725 { "title", TitleRole, kli18nc("@label", "Title"), kli18nc("@label", "Document"), KLazyLocalizedString(), true, true }, 2726 { "author", AuthorRole, kli18nc("@label", "Author"), kli18nc("@label", "Document"), KLazyLocalizedString(), true, true }, 2727 { "publisher", PublisherRole, kli18nc("@label", "Publisher"), kli18nc("@label", "Document"), KLazyLocalizedString(), true, true }, 2728 { "pageCount", PageCountRole, kli18nc("@label", "Page Count"), kli18nc("@label", "Document"), KLazyLocalizedString(), true, true }, 2729 { "wordCount", WordCountRole, kli18nc("@label", "Word Count"), kli18nc("@label", "Document"), KLazyLocalizedString(), true, true }, 2730 { "lineCount", LineCountRole, kli18nc("@label", "Line Count"), kli18nc("@label", "Document"), KLazyLocalizedString(), true, true }, 2731 { "imageDateTime", ImageDateTimeRole, kli18nc("@label", "Date Photographed"), kli18nc("@label", "Image"), KLazyLocalizedString(), true, true }, 2732 { "dimensions", DimensionsRole, kli18nc("@label width x height", "Dimensions"), kli18nc("@label", "Image"), KLazyLocalizedString(), true, true }, 2733 { "width", WidthRole, kli18nc("@label", "Width"), kli18nc("@label", "Image"), KLazyLocalizedString(), true, true }, 2734 { "height", HeightRole, kli18nc("@label", "Height"), kli18nc("@label", "Image"), KLazyLocalizedString(), true, true }, 2735 { "orientation", OrientationRole, kli18nc("@label", "Orientation"), kli18nc("@label", "Image"), KLazyLocalizedString(), true, true }, 2736 { "artist", ArtistRole, kli18nc("@label", "Artist"), kli18nc("@label", "Audio"), KLazyLocalizedString(), true, true }, 2737 { "genre", GenreRole, kli18nc("@label", "Genre"), kli18nc("@label", "Audio"), KLazyLocalizedString(), true, true }, 2738 { "album", AlbumRole, kli18nc("@label", "Album"), kli18nc("@label", "Audio"), KLazyLocalizedString(), true, true }, 2739 { "duration", DurationRole, kli18nc("@label", "Duration"), kli18nc("@label", "Audio"), KLazyLocalizedString(), true, true }, 2740 { "bitrate", BitrateRole, kli18nc("@label", "Bitrate"), kli18nc("@label", "Audio"), KLazyLocalizedString(), true, true }, 2741 { "track", TrackRole, kli18nc("@label", "Track"), kli18nc("@label", "Audio"), KLazyLocalizedString(), true, true }, 2742 { "releaseYear", ReleaseYearRole, kli18nc("@label", "Release Year"), kli18nc("@label", "Audio"), KLazyLocalizedString(), true, true }, 2743 { "aspectRatio", AspectRatioRole, kli18nc("@label", "Aspect Ratio"), kli18nc("@label", "Video"), KLazyLocalizedString(), true, true }, 2744 { "frameRate", FrameRateRole, kli18nc("@label", "Frame Rate"), kli18nc("@label", "Video"), KLazyLocalizedString(), true, true }, 2745 { "path", PathRole, kli18nc("@label", "Path"), kli18nc("@label", "Other"), KLazyLocalizedString(), false, false }, 2746 { "extension", ExtensionRole, kli18nc("@label", "File Extension"), kli18nc("@label", "Other"), KLazyLocalizedString(), false, false }, 2747 { "deletiontime", DeletionTimeRole, kli18nc("@label", "Deletion Time"), kli18nc("@label", "Other"), KLazyLocalizedString(), false, false }, 2748 { "destination", DestinationRole, kli18nc("@label", "Link Destination"), kli18nc("@label", "Other"), KLazyLocalizedString(), false, false }, 2749 { "originUrl", OriginUrlRole, kli18nc("@label", "Downloaded From"), kli18nc("@label", "Other"), KLazyLocalizedString(), true, false }, 2750 { "permissions", PermissionsRole, kli18nc("@label", "Permissions"), kli18nc("@label", "Other"), kli18nc("@tooltip", "The permission format can be changed in settings. Options are Symbolic, Numeric (Octal) or Combined formats"), false, false }, 2751 { "owner", OwnerRole, kli18nc("@label", "Owner"), kli18nc("@label", "Other"), KLazyLocalizedString(), false, false }, 2752 { "group", GroupRole, kli18nc("@label", "User Group"), kli18nc("@label", "Other"), KLazyLocalizedString(), false, false }, 2753 }; 2754 // clang-format on 2755 2756 count = sizeof(rolesInfoMap) / sizeof(RoleInfoMap); 2757 return rolesInfoMap; 2758 } 2759 2760 void KFileItemModel::determineMimeTypes(const KFileItemList &items, int timeout) 2761 { 2762 QElapsedTimer timer; 2763 timer.start(); 2764 for (const KFileItem &item : items) { 2765 // Only determine mime types for files here. For directories, 2766 // KFileItem::determineMimeType() reads the .directory file inside to 2767 // load the icon, but this is not necessary at all if we just need the 2768 // type. Some special code for setting the correct mime type for 2769 // directories is in retrieveData(). 2770 if (!item.isDir()) { 2771 item.determineMimeType(); 2772 } 2773 2774 if (timer.elapsed() > timeout) { 2775 // Don't block the user interface, let the remaining items 2776 // be resolved asynchronously. 2777 return; 2778 } 2779 } 2780 } 2781 2782 QByteArray KFileItemModel::sharedValue(const QByteArray &value) 2783 { 2784 static QSet<QByteArray> pool; 2785 const QSet<QByteArray>::const_iterator it = pool.constFind(value); 2786 2787 if (it != pool.constEnd()) { 2788 return *it; 2789 } else { 2790 pool.insert(value); 2791 return value; 2792 } 2793 } 2794 2795 bool KFileItemModel::isConsistent() const 2796 { 2797 // m_items may contain less items than m_itemData because m_items 2798 // is populated lazily, see KFileItemModel::index(const QUrl& url). 2799 if (m_items.count() > m_itemData.count()) { 2800 return false; 2801 } 2802 2803 for (int i = 0, iMax = count(); i < iMax; ++i) { 2804 // Check if m_items and m_itemData are consistent. 2805 const KFileItem item = fileItem(i); 2806 if (item.isNull()) { 2807 qCWarning(DolphinDebug) << "Item" << i << "is null"; 2808 return false; 2809 } 2810 2811 const int itemIndex = index(item); 2812 if (itemIndex != i) { 2813 qCWarning(DolphinDebug) << "Item" << i << "has a wrong index:" << itemIndex; 2814 return false; 2815 } 2816 2817 // Check if the items are sorted correctly. 2818 if (i > 0 && !lessThan(m_itemData.at(i - 1), m_itemData.at(i), m_collator)) { 2819 qCWarning(DolphinDebug) << "The order of items" << i - 1 << "and" << i << "is wrong:" << fileItem(i - 1) << fileItem(i); 2820 return false; 2821 } 2822 2823 // Check if all parent-child relationships are consistent. 2824 const ItemData *data = m_itemData.at(i); 2825 const ItemData *parent = data->parent; 2826 if (parent) { 2827 if (expandedParentsCount(data) != expandedParentsCount(parent) + 1) { 2828 qCWarning(DolphinDebug) << "expandedParentsCount is inconsistent for parent" << parent->item << "and child" << data->item; 2829 return false; 2830 } 2831 2832 const int parentIndex = index(parent->item); 2833 if (parentIndex >= i) { 2834 qCWarning(DolphinDebug) << "Index" << parentIndex << "of parent" << parent->item << "is not smaller than index" << i << "of child" 2835 << data->item; 2836 return false; 2837 } 2838 } 2839 } 2840 2841 return true; 2842 } 2843 2844 void KFileItemModel::slotListerError(KIO::Job *job) 2845 { 2846 if (job->error() == KIO::ERR_IS_FILE) { 2847 if (auto *listJob = qobject_cast<KIO::ListJob *>(job)) { 2848 Q_EMIT urlIsFileError(listJob->url()); 2849 } 2850 } else { 2851 const QString errorString = job->errorString(); 2852 Q_EMIT errorMessage(!errorString.isEmpty() ? errorString : i18nc("@info:status", "Unknown error.")); 2853 } 2854 } 2855 2856 #include "moc_kfileitemmodel.cpp"