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