]> cloud.milkyroute.net Git - dolphin.git/blob - src/kitemviews/kfileitemmodel.cpp
Merge branch 'Applications/17.12'
[dolphin.git] / src / kitemviews / kfileitemmodel.cpp
1 /*****************************************************************************
2 * Copyright (C) 2011 by Peter Penz <peter.penz19@gmail.com> *
3 * Copyright (C) 2013 by Frank Reininghaus <frank78ac@googlemail.com> *
4 * Copyright (C) 2013 by Emmanuel Pescosta <emmanuelpescosta099@gmail.com> *
5 * *
6 * This program is free software; you can redistribute it and/or modify *
7 * it under the terms of the GNU General Public License as published by *
8 * the Free Software Foundation; either version 2 of the License, or *
9 * (at your option) any later version. *
10 * *
11 * This program is distributed in the hope that it will be useful, *
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
14 * GNU General Public License for more details. *
15 * *
16 * You should have received a copy of the GNU General Public License *
17 * along with this program; if not, write to the *
18 * Free Software Foundation, Inc., *
19 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *
20 *****************************************************************************/
21
22 #include "kfileitemmodel.h"
23
24 #include "dolphin_generalsettings.h"
25
26 #include <KLocalizedString>
27 #include <KUrlMimeData>
28
29 #include "dolphindebug.h"
30
31 #include "private/kfileitemmodelsortalgorithm.h"
32 #include "private/kfileitemmodeldirlister.h"
33
34 #include <QElapsedTimer>
35 #include <QMimeData>
36 #include <QTimer>
37 #include <QWidget>
38
39 #include <algorithm>
40 #include <vector>
41
42 // #define KFILEITEMMODEL_DEBUG
43
44 KFileItemModel::KFileItemModel(QObject* parent) :
45 KItemModelBase("text", parent),
46 m_dirLister(nullptr),
47 m_sortDirsFirst(true),
48 m_sortRole(NameRole),
49 m_sortingProgressPercent(-1),
50 m_roles(),
51 m_itemData(),
52 m_items(),
53 m_filter(),
54 m_filteredItems(),
55 m_requestRole(),
56 m_maximumUpdateIntervalTimer(nullptr),
57 m_resortAllItemsTimer(nullptr),
58 m_pendingItemsToInsert(),
59 m_groups(),
60 m_expandedDirs(),
61 m_urlsToExpand()
62 {
63 m_collator.setNumericMode(true);
64
65 loadSortingSettings();
66
67 m_dirLister = new KFileItemModelDirLister(this);
68 m_dirLister->setDelayedMimeTypes(true);
69
70 const QWidget* parentWidget = qobject_cast<QWidget*>(parent);
71 if (parentWidget) {
72 m_dirLister->setMainWindow(parentWidget->window());
73 }
74
75 connect(m_dirLister, &KFileItemModelDirLister::started, this, &KFileItemModel::directoryLoadingStarted);
76 connect(m_dirLister, static_cast<void(KFileItemModelDirLister::*)()>(&KFileItemModelDirLister::canceled), this, &KFileItemModel::slotCanceled);
77 connect(m_dirLister, static_cast<void(KFileItemModelDirLister::*)(const QUrl&)>(&KFileItemModelDirLister::completed), this, &KFileItemModel::slotCompleted);
78 connect(m_dirLister, &KFileItemModelDirLister::itemsAdded, this, &KFileItemModel::slotItemsAdded);
79 connect(m_dirLister, &KFileItemModelDirLister::itemsDeleted, this, &KFileItemModel::slotItemsDeleted);
80 connect(m_dirLister, &KFileItemModelDirLister::refreshItems, this, &KFileItemModel::slotRefreshItems);
81 connect(m_dirLister, static_cast<void(KFileItemModelDirLister::*)()>(&KFileItemModelDirLister::clear), this, &KFileItemModel::slotClear);
82 connect(m_dirLister, &KFileItemModelDirLister::infoMessage, this, &KFileItemModel::infoMessage);
83 connect(m_dirLister, &KFileItemModelDirLister::errorMessage, this, &KFileItemModel::errorMessage);
84 connect(m_dirLister, &KFileItemModelDirLister::percent, this, &KFileItemModel::directoryLoadingProgress);
85 connect(m_dirLister, static_cast<void(KFileItemModelDirLister::*)(const QUrl&, const QUrl&)>(&KFileItemModelDirLister::redirection), this, &KFileItemModel::directoryRedirection);
86 connect(m_dirLister, &KFileItemModelDirLister::urlIsFileError, this, &KFileItemModel::urlIsFileError);
87
88 // Apply default roles that should be determined
89 resetRoles();
90 m_requestRole[NameRole] = true;
91 m_requestRole[IsDirRole] = true;
92 m_requestRole[IsLinkRole] = true;
93 m_roles.insert("text");
94 m_roles.insert("isDir");
95 m_roles.insert("isLink");
96 m_roles.insert("isHidden");
97
98 // For slow KIO-slaves like used for searching it makes sense to show results periodically even
99 // before the completed() or canceled() signal has been emitted.
100 m_maximumUpdateIntervalTimer = new QTimer(this);
101 m_maximumUpdateIntervalTimer->setInterval(2000);
102 m_maximumUpdateIntervalTimer->setSingleShot(true);
103 connect(m_maximumUpdateIntervalTimer, &QTimer::timeout, this, &KFileItemModel::dispatchPendingItemsToInsert);
104
105 // When changing the value of an item which represents the sort-role a resorting must be
106 // triggered. Especially in combination with KFileItemModelRolesUpdater this might be done
107 // for a lot of items within a quite small timeslot. To prevent expensive resortings the
108 // resorting is postponed until the timer has been exceeded.
109 m_resortAllItemsTimer = new QTimer(this);
110 m_resortAllItemsTimer->setInterval(500);
111 m_resortAllItemsTimer->setSingleShot(true);
112 connect(m_resortAllItemsTimer, &QTimer::timeout, this, &KFileItemModel::resortAllItems);
113
114 connect(GeneralSettings::self(), &GeneralSettings::sortingChoiceChanged, this, &KFileItemModel::slotSortingChoiceChanged);
115 }
116
117 KFileItemModel::~KFileItemModel()
118 {
119 qDeleteAll(m_itemData);
120 qDeleteAll(m_filteredItems);
121 qDeleteAll(m_pendingItemsToInsert);
122 }
123
124 void KFileItemModel::loadDirectory(const QUrl &url)
125 {
126 m_dirLister->openUrl(url);
127 }
128
129 void KFileItemModel::refreshDirectory(const QUrl &url)
130 {
131 // Refresh all expanded directories first (Bug 295300)
132 QHashIterator<QUrl, QUrl> expandedDirs(m_expandedDirs);
133 while (expandedDirs.hasNext()) {
134 expandedDirs.next();
135 m_dirLister->openUrl(expandedDirs.value(), KDirLister::Reload);
136 }
137
138 m_dirLister->openUrl(url, KDirLister::Reload);
139 }
140
141 QUrl KFileItemModel::directory() const
142 {
143 return m_dirLister->url();
144 }
145
146 void KFileItemModel::cancelDirectoryLoading()
147 {
148 m_dirLister->stop();
149 }
150
151 int KFileItemModel::count() const
152 {
153 return m_itemData.count();
154 }
155
156 QHash<QByteArray, QVariant> KFileItemModel::data(int index) const
157 {
158 if (index >= 0 && index < count()) {
159 ItemData* data = m_itemData.at(index);
160 if (data->values.isEmpty()) {
161 data->values = retrieveData(data->item, data->parent);
162 }
163
164 return data->values;
165 }
166 return QHash<QByteArray, QVariant>();
167 }
168
169 bool KFileItemModel::setData(int index, const QHash<QByteArray, QVariant>& values)
170 {
171 if (index < 0 || index >= count()) {
172 return false;
173 }
174
175 QHash<QByteArray, QVariant> currentValues = data(index);
176
177 // Determine which roles have been changed
178 QSet<QByteArray> changedRoles;
179 QHashIterator<QByteArray, QVariant> it(values);
180 while (it.hasNext()) {
181 it.next();
182 const QByteArray role = sharedValue(it.key());
183 const QVariant value = it.value();
184
185 if (currentValues[role] != value) {
186 currentValues[role] = value;
187 changedRoles.insert(role);
188 }
189 }
190
191 if (changedRoles.isEmpty()) {
192 return false;
193 }
194
195 m_itemData[index]->values = currentValues;
196 if (changedRoles.contains("text")) {
197 QUrl url = m_itemData[index]->item.url();
198 url = url.adjusted(QUrl::RemoveFilename);
199 url.setPath(url.path() + currentValues["text"].toString());
200 m_itemData[index]->item.setUrl(url);
201 }
202
203 emitItemsChangedAndTriggerResorting(KItemRangeList() << KItemRange(index, 1), changedRoles);
204
205 return true;
206 }
207
208 void KFileItemModel::setSortDirectoriesFirst(bool dirsFirst)
209 {
210 if (dirsFirst != m_sortDirsFirst) {
211 m_sortDirsFirst = dirsFirst;
212 resortAllItems();
213 }
214 }
215
216 bool KFileItemModel::sortDirectoriesFirst() const
217 {
218 return m_sortDirsFirst;
219 }
220
221 void KFileItemModel::setShowHiddenFiles(bool show)
222 {
223 m_dirLister->setShowingDotFiles(show);
224 m_dirLister->emitChanges();
225 if (show) {
226 dispatchPendingItemsToInsert();
227 }
228 }
229
230 bool KFileItemModel::showHiddenFiles() const
231 {
232 return m_dirLister->showingDotFiles();
233 }
234
235 void KFileItemModel::setShowDirectoriesOnly(bool enabled)
236 {
237 m_dirLister->setDirOnlyMode(enabled);
238 }
239
240 bool KFileItemModel::showDirectoriesOnly() const
241 {
242 return m_dirLister->dirOnlyMode();
243 }
244
245 QMimeData* KFileItemModel::createMimeData(const KItemSet& indexes) const
246 {
247 QMimeData* data = new QMimeData();
248
249 // The following code has been taken from KDirModel::mimeData()
250 // (kdelibs/kio/kio/kdirmodel.cpp)
251 // Copyright (C) 2006 David Faure <faure@kde.org>
252 QList<QUrl> urls;
253 QList<QUrl> mostLocalUrls;
254 bool canUseMostLocalUrls = true;
255 const ItemData* lastAddedItem = nullptr;
256
257 for (int index : indexes) {
258 const ItemData* itemData = m_itemData.at(index);
259 const ItemData* parent = itemData->parent;
260
261 while (parent && parent != lastAddedItem) {
262 parent = parent->parent;
263 }
264
265 if (parent && parent == lastAddedItem) {
266 // A parent of 'itemData' has been added already.
267 continue;
268 }
269
270 lastAddedItem = itemData;
271 const KFileItem& item = itemData->item;
272 if (!item.isNull()) {
273 urls << item.url();
274
275 bool isLocal;
276 mostLocalUrls << item.mostLocalUrl(isLocal);
277 if (!isLocal) {
278 canUseMostLocalUrls = false;
279 }
280 }
281 }
282
283 KUrlMimeData::setUrls(urls, mostLocalUrls, data);
284 return data;
285 }
286
287 int KFileItemModel::indexForKeyboardSearch(const QString& text, int startFromIndex) const
288 {
289 startFromIndex = qMax(0, startFromIndex);
290 for (int i = startFromIndex; i < count(); ++i) {
291 if (fileItem(i).text().startsWith(text, Qt::CaseInsensitive)) {
292 return i;
293 }
294 }
295 for (int i = 0; i < startFromIndex; ++i) {
296 if (fileItem(i).text().startsWith(text, Qt::CaseInsensitive)) {
297 return i;
298 }
299 }
300 return -1;
301 }
302
303 bool KFileItemModel::supportsDropping(int index) const
304 {
305 const KFileItem item = fileItem(index);
306 return !item.isNull() && (item.isDir() || item.isDesktopFile());
307 }
308
309 QString KFileItemModel::roleDescription(const QByteArray& role) const
310 {
311 static QHash<QByteArray, QString> description;
312 if (description.isEmpty()) {
313 int count = 0;
314 const RoleInfoMap* map = rolesInfoMap(count);
315 for (int i = 0; i < count; ++i) {
316 description.insert(map[i].role, i18nc(map[i].roleTranslationContext, map[i].roleTranslation));
317 }
318 }
319
320 return description.value(role);
321 }
322
323 QList<QPair<int, QVariant> > KFileItemModel::groups() const
324 {
325 if (!m_itemData.isEmpty() && m_groups.isEmpty()) {
326 #ifdef KFILEITEMMODEL_DEBUG
327 QElapsedTimer timer;
328 timer.start();
329 #endif
330 switch (typeForRole(sortRole())) {
331 case NameRole: m_groups = nameRoleGroups(); break;
332 case SizeRole: m_groups = sizeRoleGroups(); break;
333 case ModificationTimeRole:
334 m_groups = timeRoleGroups([](const ItemData *item) {
335 return item->item.time(KFileItem::ModificationTime);
336 });
337 break;
338 case CreationTimeRole:
339 m_groups = timeRoleGroups([](const ItemData *item) {
340 return item->item.time(KFileItem::CreationTime);
341 });
342 break;
343 case AccessTimeRole:
344 m_groups = timeRoleGroups([](const ItemData *item) {
345 return item->item.time(KFileItem::AccessTime);
346 });
347 break;
348 case DeletionTimeRole:
349 m_groups = timeRoleGroups([](const ItemData *item) {
350 return item->values.value("deletiontime").toDateTime();
351 });
352 break;
353 case PermissionsRole: m_groups = permissionRoleGroups(); break;
354 case RatingRole: m_groups = ratingRoleGroups(); break;
355 default: m_groups = genericStringRoleGroups(sortRole()); break;
356 }
357
358 #ifdef KFILEITEMMODEL_DEBUG
359 qCDebug(DolphinDebug) << "[TIME] Calculating groups for" << count() << "items:" << timer.elapsed();
360 #endif
361 }
362
363 return m_groups;
364 }
365
366 KFileItem KFileItemModel::fileItem(int index) const
367 {
368 if (index >= 0 && index < count()) {
369 return m_itemData.at(index)->item;
370 }
371
372 return KFileItem();
373 }
374
375 KFileItem KFileItemModel::fileItem(const QUrl &url) const
376 {
377 const int indexForUrl = index(url);
378 if (indexForUrl >= 0) {
379 return m_itemData.at(indexForUrl)->item;
380 }
381 return KFileItem();
382 }
383
384 int KFileItemModel::index(const KFileItem& item) const
385 {
386 return index(item.url());
387 }
388
389 int KFileItemModel::index(const QUrl& url) const
390 {
391 const QUrl urlToFind = url.adjusted(QUrl::StripTrailingSlash);
392
393 const int itemCount = m_itemData.count();
394 int itemsInHash = m_items.count();
395
396 int index = m_items.value(urlToFind, -1);
397 while (index < 0 && itemsInHash < itemCount) {
398 // Not all URLs are stored yet in m_items. We grow m_items until either
399 // urlToFind is found, or all URLs have been stored in m_items.
400 // Note that we do not add the URLs to m_items one by one, but in
401 // larger blocks. After each block, we check if urlToFind is in
402 // m_items. We could in principle compare urlToFind with each URL while
403 // we are going through m_itemData, but comparing two QUrls will,
404 // unlike calling qHash for the URLs, trigger a parsing of the URLs
405 // which costs both CPU cycles and memory.
406 const int blockSize = 1000;
407 const int currentBlockEnd = qMin(itemsInHash + blockSize, itemCount);
408 for (int i = itemsInHash; i < currentBlockEnd; ++i) {
409 const QUrl nextUrl = m_itemData.at(i)->item.url();
410 m_items.insert(nextUrl, i);
411 }
412
413 itemsInHash = currentBlockEnd;
414 index = m_items.value(urlToFind, -1);
415 }
416
417 if (index < 0) {
418 // The item could not be found, even though all items from m_itemData
419 // should be in m_items now. We print some diagnostic information which
420 // might help to find the cause of the problem, but only once. This
421 // prevents that obtaining and printing the debugging information
422 // wastes CPU cycles and floods the shell or .xsession-errors.
423 static bool printDebugInfo = true;
424
425 if (m_items.count() != m_itemData.count() && printDebugInfo) {
426 printDebugInfo = false;
427
428 qCWarning(DolphinDebug) << "The model is in an inconsistent state.";
429 qCWarning(DolphinDebug) << "m_items.count() ==" << m_items.count();
430 qCWarning(DolphinDebug) << "m_itemData.count() ==" << m_itemData.count();
431
432 // Check if there are multiple items with the same URL.
433 QMultiHash<QUrl, int> indexesForUrl;
434 for (int i = 0; i < m_itemData.count(); ++i) {
435 indexesForUrl.insert(m_itemData.at(i)->item.url(), i);
436 }
437
438 foreach (const QUrl& url, indexesForUrl.uniqueKeys()) {
439 if (indexesForUrl.count(url) > 1) {
440 qCWarning(DolphinDebug) << "Multiple items found with the URL" << url;
441
442 auto it = indexesForUrl.find(url);
443 while (it != indexesForUrl.end() && it.key() == url) {
444 const ItemData* data = m_itemData.at(it.value());
445 qCWarning(DolphinDebug) << "index" << it.value() << ":" << data->item;
446 if (data->parent) {
447 qCWarning(DolphinDebug) << "parent" << data->parent->item;
448 }
449 ++it;
450 }
451 }
452 }
453 }
454 }
455
456 return index;
457 }
458
459 KFileItem KFileItemModel::rootItem() const
460 {
461 return m_dirLister->rootItem();
462 }
463
464 void KFileItemModel::clear()
465 {
466 slotClear();
467 }
468
469 void KFileItemModel::setRoles(const QSet<QByteArray>& roles)
470 {
471 if (m_roles == roles) {
472 return;
473 }
474
475 const QSet<QByteArray> changedRoles = (roles - m_roles) + (m_roles - roles);
476 m_roles = roles;
477
478 if (count() > 0) {
479 const bool supportedExpanding = m_requestRole[ExpandedParentsCountRole];
480 const bool willSupportExpanding = roles.contains("expandedParentsCount");
481 if (supportedExpanding && !willSupportExpanding) {
482 // No expanding is supported anymore. Take care to delete all items that have an expansion level
483 // that is not 0 (and hence are part of an expanded item).
484 removeExpandedItems();
485 }
486 }
487
488 m_groups.clear();
489 resetRoles();
490
491 QSetIterator<QByteArray> it(roles);
492 while (it.hasNext()) {
493 const QByteArray& role = it.next();
494 m_requestRole[typeForRole(role)] = true;
495 }
496
497 if (count() > 0) {
498 // Update m_data with the changed requested roles
499 const int maxIndex = count() - 1;
500 for (int i = 0; i <= maxIndex; ++i) {
501 m_itemData[i]->values = retrieveData(m_itemData.at(i)->item, m_itemData.at(i)->parent);
502 }
503
504 emit itemsChanged(KItemRangeList() << KItemRange(0, count()), changedRoles);
505 }
506
507 // Clear the 'values' of all filtered items. They will be re-populated with the
508 // correct roles the next time 'values' will be accessed via data(int).
509 QHash<KFileItem, ItemData*>::iterator filteredIt = m_filteredItems.begin();
510 const QHash<KFileItem, ItemData*>::iterator filteredEnd = m_filteredItems.end();
511 while (filteredIt != filteredEnd) {
512 (*filteredIt)->values.clear();
513 ++filteredIt;
514 }
515 }
516
517 QSet<QByteArray> KFileItemModel::roles() const
518 {
519 return m_roles;
520 }
521
522 bool KFileItemModel::setExpanded(int index, bool expanded)
523 {
524 if (!isExpandable(index) || isExpanded(index) == expanded) {
525 return false;
526 }
527
528 QHash<QByteArray, QVariant> values;
529 values.insert(sharedValue("isExpanded"), expanded);
530 if (!setData(index, values)) {
531 return false;
532 }
533
534 const KFileItem item = m_itemData.at(index)->item;
535 const QUrl url = item.url();
536 const QUrl targetUrl = item.targetUrl();
537 if (expanded) {
538 m_expandedDirs.insert(targetUrl, url);
539 m_dirLister->openUrl(url, KDirLister::Keep);
540
541 const QVariantList previouslyExpandedChildren = m_itemData.at(index)->values.value("previouslyExpandedChildren").value<QVariantList>();
542 foreach (const QVariant& var, previouslyExpandedChildren) {
543 m_urlsToExpand.insert(var.toUrl());
544 }
545 } else {
546 // Note that there might be (indirect) children of the folder which is to be collapsed in
547 // m_pendingItemsToInsert. To prevent that they will be inserted into the model later,
548 // possibly without a parent, which might result in a crash, we insert all pending items
549 // right now. All new items which would be without a parent will then be removed.
550 dispatchPendingItemsToInsert();
551
552 // Check if the index of the collapsed folder has changed. If that is the case, then items
553 // were inserted before the collapsed folder, and its index needs to be updated.
554 if (m_itemData.at(index)->item != item) {
555 index = this->index(item);
556 }
557
558 m_expandedDirs.remove(targetUrl);
559 m_dirLister->stop(url);
560
561 const int parentLevel = expandedParentsCount(index);
562 const int itemCount = m_itemData.count();
563 const int firstChildIndex = index + 1;
564
565 QVariantList expandedChildren;
566
567 int childIndex = firstChildIndex;
568 while (childIndex < itemCount && expandedParentsCount(childIndex) > parentLevel) {
569 ItemData* itemData = m_itemData.at(childIndex);
570 if (itemData->values.value("isExpanded").toBool()) {
571 const QUrl targetUrl = itemData->item.targetUrl();
572 const QUrl url = itemData->item.url();
573 m_expandedDirs.remove(targetUrl);
574 m_dirLister->stop(url); // TODO: try to unit-test this, see https://bugs.kde.org/show_bug.cgi?id=332102#c11
575 expandedChildren.append(targetUrl);
576 }
577 ++childIndex;
578 }
579 const int childrenCount = childIndex - firstChildIndex;
580
581 removeFilteredChildren(KItemRangeList() << KItemRange(index, 1 + childrenCount));
582 removeItems(KItemRangeList() << KItemRange(firstChildIndex, childrenCount), DeleteItemData);
583
584 m_itemData.at(index)->values.insert("previouslyExpandedChildren", expandedChildren);
585 }
586
587 return true;
588 }
589
590 bool KFileItemModel::isExpanded(int index) const
591 {
592 if (index >= 0 && index < count()) {
593 return m_itemData.at(index)->values.value("isExpanded").toBool();
594 }
595 return false;
596 }
597
598 bool KFileItemModel::isExpandable(int index) const
599 {
600 if (index >= 0 && index < count()) {
601 // Call data (instead of accessing m_itemData directly)
602 // to ensure that the value is initialized.
603 return data(index).value("isExpandable").toBool();
604 }
605 return false;
606 }
607
608 int KFileItemModel::expandedParentsCount(int index) const
609 {
610 if (index >= 0 && index < count()) {
611 return expandedParentsCount(m_itemData.at(index));
612 }
613 return 0;
614 }
615
616 QSet<QUrl> KFileItemModel::expandedDirectories() const
617 {
618 QSet<QUrl> result;
619 const auto dirs = m_expandedDirs;
620 for (const auto &dir : dirs) {
621 result.insert(dir);
622 }
623 return result;
624 }
625
626 void KFileItemModel::restoreExpandedDirectories(const QSet<QUrl> &urls)
627 {
628 m_urlsToExpand = urls;
629 }
630
631 void KFileItemModel::expandParentDirectories(const QUrl &url)
632 {
633
634 // Assure that each sub-path of the URL that should be
635 // expanded is added to m_urlsToExpand. KDirLister
636 // does not care whether the parent-URL has already been
637 // expanded.
638 QUrl urlToExpand = m_dirLister->url();
639 const int pos = urlToExpand.path().length();
640
641 // first subdir can be empty, if m_dirLister->url().path() does not end with '/'
642 // this happens if baseUrl is not root but a home directory, see FoldersPanel,
643 // so using QString::SkipEmptyParts
644 const QStringList subDirs = url.path().mid(pos).split(QDir::separator(), QString::SkipEmptyParts);
645 for (int i = 0; i < subDirs.count() - 1; ++i) {
646 QString path = urlToExpand.path();
647 if (!path.endsWith(QLatin1Char('/'))) {
648 path.append(QLatin1Char('/'));
649 }
650 urlToExpand.setPath(path + subDirs.at(i));
651 m_urlsToExpand.insert(urlToExpand);
652 }
653
654 // KDirLister::open() must called at least once to trigger an initial
655 // loading. The pending URLs that must be restored are handled
656 // in slotCompleted().
657 QSetIterator<QUrl> it2(m_urlsToExpand);
658 while (it2.hasNext()) {
659 const int idx = index(it2.next());
660 if (idx >= 0 && !isExpanded(idx)) {
661 setExpanded(idx, true);
662 break;
663 }
664 }
665 }
666
667 void KFileItemModel::setNameFilter(const QString& nameFilter)
668 {
669 if (m_filter.pattern() != nameFilter) {
670 dispatchPendingItemsToInsert();
671 m_filter.setPattern(nameFilter);
672 applyFilters();
673 }
674 }
675
676 QString KFileItemModel::nameFilter() const
677 {
678 return m_filter.pattern();
679 }
680
681 void KFileItemModel::setMimeTypeFilters(const QStringList& filters)
682 {
683 if (m_filter.mimeTypes() != filters) {
684 dispatchPendingItemsToInsert();
685 m_filter.setMimeTypes(filters);
686 applyFilters();
687 }
688 }
689
690 QStringList KFileItemModel::mimeTypeFilters() const
691 {
692 return m_filter.mimeTypes();
693 }
694
695
696 void KFileItemModel::applyFilters()
697 {
698 // Check which shown items from m_itemData must get
699 // hidden and hence moved to m_filteredItems.
700 QVector<int> newFilteredIndexes;
701
702 const int itemCount = m_itemData.count();
703 for (int index = 0; index < itemCount; ++index) {
704 ItemData* itemData = m_itemData.at(index);
705
706 // Only filter non-expanded items as child items may never
707 // exist without a parent item
708 if (!itemData->values.value("isExpanded").toBool()) {
709 const KFileItem item = itemData->item;
710 if (!m_filter.matches(item)) {
711 newFilteredIndexes.append(index);
712 m_filteredItems.insert(item, itemData);
713 }
714 }
715 }
716
717 const KItemRangeList removedRanges = KItemRangeList::fromSortedContainer(newFilteredIndexes);
718 removeItems(removedRanges, KeepItemData);
719
720 // Check which hidden items from m_filteredItems should
721 // get visible again and hence removed from m_filteredItems.
722 QList<ItemData*> newVisibleItems;
723
724 QHash<KFileItem, ItemData*>::iterator it = m_filteredItems.begin();
725 while (it != m_filteredItems.end()) {
726 if (m_filter.matches(it.key())) {
727 newVisibleItems.append(it.value());
728 it = m_filteredItems.erase(it);
729 } else {
730 ++it;
731 }
732 }
733
734 insertItems(newVisibleItems);
735 }
736
737 void KFileItemModel::removeFilteredChildren(const KItemRangeList& itemRanges)
738 {
739 if (m_filteredItems.isEmpty() || !m_requestRole[ExpandedParentsCountRole]) {
740 // There are either no filtered items, or it is not possible to expand
741 // folders -> there cannot be any filtered children.
742 return;
743 }
744
745 QSet<ItemData*> parents;
746 foreach (const KItemRange& range, itemRanges) {
747 for (int index = range.index; index < range.index + range.count; ++index) {
748 parents.insert(m_itemData.at(index));
749 }
750 }
751
752 QHash<KFileItem, ItemData*>::iterator it = m_filteredItems.begin();
753 while (it != m_filteredItems.end()) {
754 if (parents.contains(it.value()->parent)) {
755 delete it.value();
756 it = m_filteredItems.erase(it);
757 } else {
758 ++it;
759 }
760 }
761 }
762
763 QList<KFileItemModel::RoleInfo> KFileItemModel::rolesInformation()
764 {
765 static QList<RoleInfo> rolesInfo;
766 if (rolesInfo.isEmpty()) {
767 int count = 0;
768 const RoleInfoMap* map = rolesInfoMap(count);
769 for (int i = 0; i < count; ++i) {
770 if (map[i].roleType != NoRole) {
771 RoleInfo info;
772 info.role = map[i].role;
773 info.translation = i18nc(map[i].roleTranslationContext, map[i].roleTranslation);
774 if (map[i].groupTranslation) {
775 info.group = i18nc(map[i].groupTranslationContext, map[i].groupTranslation);
776 } else {
777 // For top level roles, groupTranslation is 0. We must make sure that
778 // info.group is an empty string then because the code that generates
779 // menus tries to put the actions into sub menus otherwise.
780 info.group = QString();
781 }
782 info.requiresBaloo = map[i].requiresBaloo;
783 info.requiresIndexer = map[i].requiresIndexer;
784 rolesInfo.append(info);
785 }
786 }
787 }
788
789 return rolesInfo;
790 }
791
792 void KFileItemModel::onGroupedSortingChanged(bool current)
793 {
794 Q_UNUSED(current);
795 m_groups.clear();
796 }
797
798 void KFileItemModel::onSortRoleChanged(const QByteArray& current, const QByteArray& previous)
799 {
800 Q_UNUSED(previous);
801 m_sortRole = typeForRole(current);
802
803 if (!m_requestRole[m_sortRole]) {
804 QSet<QByteArray> newRoles = m_roles;
805 newRoles << current;
806 setRoles(newRoles);
807 }
808
809 resortAllItems();
810 }
811
812 void KFileItemModel::onSortOrderChanged(Qt::SortOrder current, Qt::SortOrder previous)
813 {
814 Q_UNUSED(current);
815 Q_UNUSED(previous);
816 resortAllItems();
817 }
818
819 void KFileItemModel::loadSortingSettings()
820 {
821 using Choice = GeneralSettings::EnumSortingChoice;
822 switch (GeneralSettings::sortingChoice()) {
823 case Choice::NaturalSorting:
824 m_naturalSorting = true;
825 m_collator.setCaseSensitivity(Qt::CaseInsensitive);
826 break;
827 case Choice::CaseSensitiveSorting:
828 m_naturalSorting = false;
829 m_collator.setCaseSensitivity(Qt::CaseSensitive);
830 break;
831 case Choice::CaseInsensitiveSorting:
832 m_naturalSorting = false;
833 m_collator.setCaseSensitivity(Qt::CaseInsensitive);
834 break;
835 default:
836 Q_UNREACHABLE();
837 }
838 }
839
840 void KFileItemModel::resortAllItems()
841 {
842 m_resortAllItemsTimer->stop();
843
844 const int itemCount = count();
845 if (itemCount <= 0) {
846 return;
847 }
848
849 #ifdef KFILEITEMMODEL_DEBUG
850 QElapsedTimer timer;
851 timer.start();
852 qCDebug(DolphinDebug) << "===========================================================";
853 qCDebug(DolphinDebug) << "Resorting" << itemCount << "items";
854 #endif
855
856 // Remember the order of the current URLs so
857 // that it can be determined which indexes have
858 // been moved because of the resorting.
859 QList<QUrl> oldUrls;
860 oldUrls.reserve(itemCount);
861 foreach (const ItemData* itemData, m_itemData) {
862 oldUrls.append(itemData->item.url());
863 }
864
865 m_items.clear();
866 m_items.reserve(itemCount);
867
868 // Resort the items
869 sort(m_itemData.begin(), m_itemData.end());
870 for (int i = 0; i < itemCount; ++i) {
871 m_items.insert(m_itemData.at(i)->item.url(), i);
872 }
873
874 // Determine the first index that has been moved.
875 int firstMovedIndex = 0;
876 while (firstMovedIndex < itemCount
877 && firstMovedIndex == m_items.value(oldUrls.at(firstMovedIndex))) {
878 ++firstMovedIndex;
879 }
880
881 const bool itemsHaveMoved = firstMovedIndex < itemCount;
882 if (itemsHaveMoved) {
883 m_groups.clear();
884
885 int lastMovedIndex = itemCount - 1;
886 while (lastMovedIndex > firstMovedIndex
887 && lastMovedIndex == m_items.value(oldUrls.at(lastMovedIndex))) {
888 --lastMovedIndex;
889 }
890
891 Q_ASSERT(firstMovedIndex <= lastMovedIndex);
892
893 // Create a list movedToIndexes, which has the property that
894 // movedToIndexes[i] is the new index of the item with the old index
895 // firstMovedIndex + i.
896 const int movedItemsCount = lastMovedIndex - firstMovedIndex + 1;
897 QList<int> movedToIndexes;
898 movedToIndexes.reserve(movedItemsCount);
899 for (int i = firstMovedIndex; i <= lastMovedIndex; ++i) {
900 const int newIndex = m_items.value(oldUrls.at(i));
901 movedToIndexes.append(newIndex);
902 }
903
904 emit itemsMoved(KItemRange(firstMovedIndex, movedItemsCount), movedToIndexes);
905 } else if (groupedSorting()) {
906 // The groups might have changed even if the order of the items has not.
907 const QList<QPair<int, QVariant> > oldGroups = m_groups;
908 m_groups.clear();
909 if (groups() != oldGroups) {
910 emit groupsChanged();
911 }
912 }
913
914 #ifdef KFILEITEMMODEL_DEBUG
915 qCDebug(DolphinDebug) << "[TIME] Resorting of" << itemCount << "items:" << timer.elapsed();
916 #endif
917 }
918
919 void KFileItemModel::slotCompleted()
920 {
921 dispatchPendingItemsToInsert();
922
923 if (!m_urlsToExpand.isEmpty()) {
924 // Try to find a URL that can be expanded.
925 // Note that the parent folder must be expanded before any of its subfolders become visible.
926 // Therefore, some URLs in m_restoredExpandedUrls might not be visible yet
927 // -> we expand the first visible URL we find in m_restoredExpandedUrls.
928 foreach (const QUrl& url, m_urlsToExpand) {
929 const int indexForUrl = index(url);
930 if (indexForUrl >= 0) {
931 m_urlsToExpand.remove(url);
932 if (setExpanded(indexForUrl, true)) {
933 // The dir lister has been triggered. This slot will be called
934 // again after the directory has been expanded.
935 return;
936 }
937 }
938 }
939
940 // None of the URLs in m_restoredExpandedUrls could be found in the model. This can happen
941 // if these URLs have been deleted in the meantime.
942 m_urlsToExpand.clear();
943 }
944
945 emit directoryLoadingCompleted();
946 }
947
948 void KFileItemModel::slotCanceled()
949 {
950 m_maximumUpdateIntervalTimer->stop();
951 dispatchPendingItemsToInsert();
952
953 emit directoryLoadingCanceled();
954 }
955
956 void KFileItemModel::slotItemsAdded(const QUrl &directoryUrl, const KFileItemList& items)
957 {
958 Q_ASSERT(!items.isEmpty());
959
960 QUrl parentUrl;
961 if (m_expandedDirs.contains(directoryUrl)) {
962 parentUrl = m_expandedDirs.value(directoryUrl);
963 } else {
964 parentUrl = directoryUrl.adjusted(QUrl::StripTrailingSlash);
965 }
966
967 if (m_requestRole[ExpandedParentsCountRole]) {
968 // If the expanding of items is enabled, the call
969 // dirLister->openUrl(url, KDirLister::Keep) in KFileItemModel::setExpanded()
970 // might result in emitting the same items twice due to the Keep-parameter.
971 // This case happens if an item gets expanded, collapsed and expanded again
972 // before the items could be loaded for the first expansion.
973 if (index(items.first().url()) >= 0) {
974 // The items are already part of the model.
975 return;
976 }
977
978 if (directoryUrl != directory()) {
979 // To be able to compare whether the new items may be inserted as children
980 // of a parent item the pending items must be added to the model first.
981 dispatchPendingItemsToInsert();
982 }
983
984 // KDirLister keeps the children of items that got expanded once even if
985 // they got collapsed again with KFileItemModel::setExpanded(false). So it must be
986 // checked whether the parent for new items is still expanded.
987 const int parentIndex = index(parentUrl);
988 if (parentIndex >= 0 && !m_itemData[parentIndex]->values.value("isExpanded").toBool()) {
989 // The parent is not expanded.
990 return;
991 }
992 }
993
994 QList<ItemData*> itemDataList = createItemDataList(parentUrl, items);
995
996 if (!m_filter.hasSetFilters()) {
997 m_pendingItemsToInsert.append(itemDataList);
998 } else {
999 // The name or type filter is active. Hide filtered items
1000 // before inserting them into the model and remember
1001 // the filtered items in m_filteredItems.
1002 foreach (ItemData* itemData, itemDataList) {
1003 if (m_filter.matches(itemData->item)) {
1004 m_pendingItemsToInsert.append(itemData);
1005 } else {
1006 m_filteredItems.insert(itemData->item, itemData);
1007 }
1008 }
1009 }
1010
1011 if (useMaximumUpdateInterval() && !m_maximumUpdateIntervalTimer->isActive()) {
1012 // Assure that items get dispatched if no completed() or canceled() signal is
1013 // emitted during the maximum update interval.
1014 m_maximumUpdateIntervalTimer->start();
1015 }
1016 }
1017
1018 void KFileItemModel::slotItemsDeleted(const KFileItemList& items)
1019 {
1020 dispatchPendingItemsToInsert();
1021
1022 QVector<int> indexesToRemove;
1023 indexesToRemove.reserve(items.count());
1024
1025 foreach (const KFileItem& item, items) {
1026 const int indexForItem = index(item);
1027 if (indexForItem >= 0) {
1028 indexesToRemove.append(indexForItem);
1029 } else {
1030 // Probably the item has been filtered.
1031 QHash<KFileItem, ItemData*>::iterator it = m_filteredItems.find(item);
1032 if (it != m_filteredItems.end()) {
1033 delete it.value();
1034 m_filteredItems.erase(it);
1035 }
1036 }
1037 }
1038
1039 std::sort(indexesToRemove.begin(), indexesToRemove.end());
1040
1041 if (m_requestRole[ExpandedParentsCountRole] && !m_expandedDirs.isEmpty()) {
1042 // Assure that removing a parent item also results in removing all children
1043 QVector<int> indexesToRemoveWithChildren;
1044 indexesToRemoveWithChildren.reserve(m_itemData.count());
1045
1046 const int itemCount = m_itemData.count();
1047 foreach (int index, indexesToRemove) {
1048 indexesToRemoveWithChildren.append(index);
1049
1050 const int parentLevel = expandedParentsCount(index);
1051 int childIndex = index + 1;
1052 while (childIndex < itemCount && expandedParentsCount(childIndex) > parentLevel) {
1053 indexesToRemoveWithChildren.append(childIndex);
1054 ++childIndex;
1055 }
1056 }
1057
1058 indexesToRemove = indexesToRemoveWithChildren;
1059 }
1060
1061 const KItemRangeList itemRanges = KItemRangeList::fromSortedContainer(indexesToRemove);
1062 removeFilteredChildren(itemRanges);
1063 removeItems(itemRanges, DeleteItemData);
1064 }
1065
1066 void KFileItemModel::slotRefreshItems(const QList<QPair<KFileItem, KFileItem> >& items)
1067 {
1068 Q_ASSERT(!items.isEmpty());
1069 #ifdef KFILEITEMMODEL_DEBUG
1070 qCDebug(DolphinDebug) << "Refreshing" << items.count() << "items";
1071 #endif
1072
1073 // Get the indexes of all items that have been refreshed
1074 QList<int> indexes;
1075 indexes.reserve(items.count());
1076
1077 QSet<QByteArray> changedRoles;
1078
1079 QListIterator<QPair<KFileItem, KFileItem> > it(items);
1080 while (it.hasNext()) {
1081 const QPair<KFileItem, KFileItem>& itemPair = it.next();
1082 const KFileItem& oldItem = itemPair.first;
1083 const KFileItem& newItem = itemPair.second;
1084 const int indexForItem = index(oldItem);
1085 if (indexForItem >= 0) {
1086 m_itemData[indexForItem]->item = newItem;
1087
1088 // Keep old values as long as possible if they could not retrieved synchronously yet.
1089 // The update of the values will be done asynchronously by KFileItemModelRolesUpdater.
1090 QHashIterator<QByteArray, QVariant> it(retrieveData(newItem, m_itemData.at(indexForItem)->parent));
1091 QHash<QByteArray, QVariant>& values = m_itemData[indexForItem]->values;
1092 while (it.hasNext()) {
1093 it.next();
1094 const QByteArray& role = it.key();
1095 if (values.value(role) != it.value()) {
1096 values.insert(role, it.value());
1097 changedRoles.insert(role);
1098 }
1099 }
1100
1101 m_items.remove(oldItem.url());
1102 m_items.insert(newItem.url(), indexForItem);
1103 indexes.append(indexForItem);
1104 } else {
1105 // Check if 'oldItem' is one of the filtered items.
1106 QHash<KFileItem, ItemData*>::iterator it = m_filteredItems.find(oldItem);
1107 if (it != m_filteredItems.end()) {
1108 ItemData* itemData = it.value();
1109 itemData->item = newItem;
1110
1111 // The data stored in 'values' might have changed. Therefore, we clear
1112 // 'values' and re-populate it the next time it is requested via data(int).
1113 itemData->values.clear();
1114
1115 m_filteredItems.erase(it);
1116 m_filteredItems.insert(newItem, itemData);
1117 }
1118 }
1119 }
1120
1121 // If the changed items have been created recently, they might not be in m_items yet.
1122 // In that case, the list 'indexes' might be empty.
1123 if (indexes.isEmpty()) {
1124 return;
1125 }
1126
1127 // Extract the item-ranges out of the changed indexes
1128 qSort(indexes);
1129 const KItemRangeList itemRangeList = KItemRangeList::fromSortedContainer(indexes);
1130 emitItemsChangedAndTriggerResorting(itemRangeList, changedRoles);
1131 }
1132
1133 void KFileItemModel::slotClear()
1134 {
1135 #ifdef KFILEITEMMODEL_DEBUG
1136 qCDebug(DolphinDebug) << "Clearing all items";
1137 #endif
1138
1139 qDeleteAll(m_filteredItems);
1140 m_filteredItems.clear();
1141 m_groups.clear();
1142
1143 m_maximumUpdateIntervalTimer->stop();
1144 m_resortAllItemsTimer->stop();
1145
1146 qDeleteAll(m_pendingItemsToInsert);
1147 m_pendingItemsToInsert.clear();
1148
1149 const int removedCount = m_itemData.count();
1150 if (removedCount > 0) {
1151 qDeleteAll(m_itemData);
1152 m_itemData.clear();
1153 m_items.clear();
1154 emit itemsRemoved(KItemRangeList() << KItemRange(0, removedCount));
1155 }
1156
1157 m_expandedDirs.clear();
1158 }
1159
1160 void KFileItemModel::slotSortingChoiceChanged()
1161 {
1162 loadSortingSettings();
1163 resortAllItems();
1164 }
1165
1166 void KFileItemModel::dispatchPendingItemsToInsert()
1167 {
1168 if (!m_pendingItemsToInsert.isEmpty()) {
1169 insertItems(m_pendingItemsToInsert);
1170 m_pendingItemsToInsert.clear();
1171 }
1172 }
1173
1174 void KFileItemModel::insertItems(QList<ItemData*>& newItems)
1175 {
1176 if (newItems.isEmpty()) {
1177 return;
1178 }
1179
1180 #ifdef KFILEITEMMODEL_DEBUG
1181 QElapsedTimer timer;
1182 timer.start();
1183 qCDebug(DolphinDebug) << "===========================================================";
1184 qCDebug(DolphinDebug) << "Inserting" << newItems.count() << "items";
1185 #endif
1186
1187 m_groups.clear();
1188 prepareItemsForSorting(newItems);
1189
1190 if (m_sortRole == NameRole && m_naturalSorting) {
1191 // Natural sorting of items can be very slow. However, it becomes much
1192 // faster if the input sequence is already mostly sorted. Therefore, we
1193 // first sort 'newItems' according to the QStrings returned by
1194 // KFileItem::text() using QString::operator<(), which is quite fast.
1195 parallelMergeSort(newItems.begin(), newItems.end(), nameLessThan, QThread::idealThreadCount());
1196 }
1197
1198 sort(newItems.begin(), newItems.end());
1199
1200 #ifdef KFILEITEMMODEL_DEBUG
1201 qCDebug(DolphinDebug) << "[TIME] Sorting:" << timer.elapsed();
1202 #endif
1203
1204 KItemRangeList itemRanges;
1205 const int existingItemCount = m_itemData.count();
1206 const int newItemCount = newItems.count();
1207 const int totalItemCount = existingItemCount + newItemCount;
1208
1209 if (existingItemCount == 0) {
1210 // Optimization for the common special case that there are no
1211 // items in the model yet. Happens, e.g., when entering a folder.
1212 m_itemData = newItems;
1213 itemRanges << KItemRange(0, newItemCount);
1214 } else {
1215 m_itemData.reserve(totalItemCount);
1216 for (int i = existingItemCount; i < totalItemCount; ++i) {
1217 m_itemData.append(0);
1218 }
1219
1220 // We build the new list m_itemData in reverse order to minimize
1221 // the number of moves and guarantee O(N) complexity.
1222 int targetIndex = totalItemCount - 1;
1223 int sourceIndexExistingItems = existingItemCount - 1;
1224 int sourceIndexNewItems = newItemCount - 1;
1225
1226 int rangeCount = 0;
1227
1228 while (sourceIndexNewItems >= 0) {
1229 ItemData* newItem = newItems.at(sourceIndexNewItems);
1230 if (sourceIndexExistingItems >= 0 && lessThan(newItem, m_itemData.at(sourceIndexExistingItems), m_collator)) {
1231 // Move an existing item to its new position. If any new items
1232 // are behind it, push the item range to itemRanges.
1233 if (rangeCount > 0) {
1234 itemRanges << KItemRange(sourceIndexExistingItems + 1, rangeCount);
1235 rangeCount = 0;
1236 }
1237
1238 m_itemData[targetIndex] = m_itemData.at(sourceIndexExistingItems);
1239 --sourceIndexExistingItems;
1240 } else {
1241 // Insert a new item into the list.
1242 ++rangeCount;
1243 m_itemData[targetIndex] = newItem;
1244 --sourceIndexNewItems;
1245 }
1246 --targetIndex;
1247 }
1248
1249 // Push the final item range to itemRanges.
1250 if (rangeCount > 0) {
1251 itemRanges << KItemRange(sourceIndexExistingItems + 1, rangeCount);
1252 }
1253
1254 // Note that itemRanges is still sorted in reverse order.
1255 std::reverse(itemRanges.begin(), itemRanges.end());
1256 }
1257
1258 // The indexes in m_items are not correct anymore. Therefore, we clear m_items.
1259 // It will be re-populated with the updated indices if index(const QUrl&) is called.
1260 m_items.clear();
1261
1262 emit itemsInserted(itemRanges);
1263
1264 #ifdef KFILEITEMMODEL_DEBUG
1265 qCDebug(DolphinDebug) << "[TIME] Inserting of" << newItems.count() << "items:" << timer.elapsed();
1266 #endif
1267 }
1268
1269 void KFileItemModel::removeItems(const KItemRangeList& itemRanges, RemoveItemsBehavior behavior)
1270 {
1271 if (itemRanges.isEmpty()) {
1272 return;
1273 }
1274
1275 m_groups.clear();
1276
1277 // Step 1: Remove the items from m_itemData, and free the ItemData.
1278 int removedItemsCount = 0;
1279 foreach (const KItemRange& range, itemRanges) {
1280 removedItemsCount += range.count;
1281
1282 for (int index = range.index; index < range.index + range.count; ++index) {
1283 if (behavior == DeleteItemData) {
1284 delete m_itemData.at(index);
1285 }
1286
1287 m_itemData[index] = 0;
1288 }
1289 }
1290
1291 // Step 2: Remove the ItemData pointers from the list m_itemData.
1292 int target = itemRanges.at(0).index;
1293 int source = itemRanges.at(0).index + itemRanges.at(0).count;
1294 int nextRange = 1;
1295
1296 const int oldItemDataCount = m_itemData.count();
1297 while (source < oldItemDataCount) {
1298 m_itemData[target] = m_itemData[source];
1299 ++target;
1300 ++source;
1301
1302 if (nextRange < itemRanges.count() && source == itemRanges.at(nextRange).index) {
1303 // Skip the items in the next removed range.
1304 source += itemRanges.at(nextRange).count;
1305 ++nextRange;
1306 }
1307 }
1308
1309 m_itemData.erase(m_itemData.end() - removedItemsCount, m_itemData.end());
1310
1311 // The indexes in m_items are not correct anymore. Therefore, we clear m_items.
1312 // It will be re-populated with the updated indices if index(const QUrl&) is called.
1313 m_items.clear();
1314
1315 emit itemsRemoved(itemRanges);
1316 }
1317
1318 QList<KFileItemModel::ItemData*> KFileItemModel::createItemDataList(const QUrl& parentUrl, const KFileItemList& items) const
1319 {
1320 if (m_sortRole == TypeRole) {
1321 // Try to resolve the MIME-types synchronously to prevent a reordering of
1322 // the items when sorting by type (per default MIME-types are resolved
1323 // asynchronously by KFileItemModelRolesUpdater).
1324 determineMimeTypes(items, 200);
1325 }
1326
1327 const int parentIndex = index(parentUrl);
1328 ItemData* parentItem = parentIndex < 0 ? 0 : m_itemData.at(parentIndex);
1329
1330 QList<ItemData*> itemDataList;
1331 itemDataList.reserve(items.count());
1332
1333 foreach (const KFileItem& item, items) {
1334 ItemData* itemData = new ItemData();
1335 itemData->item = item;
1336 itemData->parent = parentItem;
1337 itemDataList.append(itemData);
1338 }
1339
1340 return itemDataList;
1341 }
1342
1343 void KFileItemModel::prepareItemsForSorting(QList<ItemData*>& itemDataList)
1344 {
1345 switch (m_sortRole) {
1346 case PermissionsRole:
1347 case OwnerRole:
1348 case GroupRole:
1349 case DestinationRole:
1350 case PathRole:
1351 case DeletionTimeRole:
1352 // These roles can be determined with retrieveData, and they have to be stored
1353 // in the QHash "values" for the sorting.
1354 foreach (ItemData* itemData, itemDataList) {
1355 if (itemData->values.isEmpty()) {
1356 itemData->values = retrieveData(itemData->item, itemData->parent);
1357 }
1358 }
1359 break;
1360
1361 case TypeRole:
1362 // At least store the data including the file type for items with known MIME type.
1363 foreach (ItemData* itemData, itemDataList) {
1364 if (itemData->values.isEmpty()) {
1365 const KFileItem item = itemData->item;
1366 if (item.isDir() || item.isMimeTypeKnown()) {
1367 itemData->values = retrieveData(itemData->item, itemData->parent);
1368 }
1369 }
1370 }
1371 break;
1372
1373 default:
1374 // The other roles are either resolved by KFileItemModelRolesUpdater
1375 // (this includes the SizeRole for directories), or they do not need
1376 // to be stored in the QHash "values" for sorting because the data can
1377 // be retrieved directly from the KFileItem (NameRole, SizeRole for files,
1378 // DateRole).
1379 break;
1380 }
1381 }
1382
1383 int KFileItemModel::expandedParentsCount(const ItemData* data)
1384 {
1385 // The hash 'values' is only guaranteed to contain the key "expandedParentsCount"
1386 // if the corresponding item is expanded, and it is not a top-level item.
1387 const ItemData* parent = data->parent;
1388 if (parent) {
1389 if (parent->parent) {
1390 Q_ASSERT(parent->values.contains("expandedParentsCount"));
1391 return parent->values.value("expandedParentsCount").toInt() + 1;
1392 } else {
1393 return 1;
1394 }
1395 } else {
1396 return 0;
1397 }
1398 }
1399
1400 void KFileItemModel::removeExpandedItems()
1401 {
1402 QVector<int> indexesToRemove;
1403
1404 const int maxIndex = m_itemData.count() - 1;
1405 for (int i = 0; i <= maxIndex; ++i) {
1406 const ItemData* itemData = m_itemData.at(i);
1407 if (itemData->parent) {
1408 indexesToRemove.append(i);
1409 }
1410 }
1411
1412 removeItems(KItemRangeList::fromSortedContainer(indexesToRemove), DeleteItemData);
1413 m_expandedDirs.clear();
1414
1415 // Also remove all filtered items which have a parent.
1416 QHash<KFileItem, ItemData*>::iterator it = m_filteredItems.begin();
1417 const QHash<KFileItem, ItemData*>::iterator end = m_filteredItems.end();
1418
1419 while (it != end) {
1420 if (it.value()->parent) {
1421 delete it.value();
1422 it = m_filteredItems.erase(it);
1423 } else {
1424 ++it;
1425 }
1426 }
1427 }
1428
1429 void KFileItemModel::emitItemsChangedAndTriggerResorting(const KItemRangeList& itemRanges, const QSet<QByteArray>& changedRoles)
1430 {
1431 emit itemsChanged(itemRanges, changedRoles);
1432
1433 // Trigger a resorting if necessary. Note that this can happen even if the sort
1434 // role has not changed at all because the file name can be used as a fallback.
1435 if (changedRoles.contains(sortRole()) || changedRoles.contains(roleForType(NameRole))) {
1436 foreach (const KItemRange& range, itemRanges) {
1437 bool needsResorting = false;
1438
1439 const int first = range.index;
1440 const int last = range.index + range.count - 1;
1441
1442 // Resorting the model is necessary if
1443 // (a) The first item in the range is "lessThan" its predecessor,
1444 // (b) the successor of the last item is "lessThan" the last item, or
1445 // (c) the internal order of the items in the range is incorrect.
1446 if (first > 0
1447 && lessThan(m_itemData.at(first), m_itemData.at(first - 1), m_collator)) {
1448 needsResorting = true;
1449 } else if (last < count() - 1
1450 && lessThan(m_itemData.at(last + 1), m_itemData.at(last), m_collator)) {
1451 needsResorting = true;
1452 } else {
1453 for (int index = first; index < last; ++index) {
1454 if (lessThan(m_itemData.at(index + 1), m_itemData.at(index), m_collator)) {
1455 needsResorting = true;
1456 break;
1457 }
1458 }
1459 }
1460
1461 if (needsResorting) {
1462 m_resortAllItemsTimer->start();
1463 return;
1464 }
1465 }
1466 }
1467
1468 if (groupedSorting() && changedRoles.contains(sortRole())) {
1469 // The position is still correct, but the groups might have changed
1470 // if the changed item is either the first or the last item in a
1471 // group.
1472 // In principle, we could try to find out if the item really is the
1473 // first or last one in its group and then update the groups
1474 // (possibly with a delayed timer to make sure that we don't
1475 // re-calculate the groups very often if items are updated one by
1476 // one), but starting m_resortAllItemsTimer is easier.
1477 m_resortAllItemsTimer->start();
1478 }
1479 }
1480
1481 void KFileItemModel::resetRoles()
1482 {
1483 for (int i = 0; i < RolesCount; ++i) {
1484 m_requestRole[i] = false;
1485 }
1486 }
1487
1488 KFileItemModel::RoleType KFileItemModel::typeForRole(const QByteArray& role) const
1489 {
1490 static QHash<QByteArray, RoleType> roles;
1491 if (roles.isEmpty()) {
1492 // Insert user visible roles that can be accessed with
1493 // KFileItemModel::roleInformation()
1494 int count = 0;
1495 const RoleInfoMap* map = rolesInfoMap(count);
1496 for (int i = 0; i < count; ++i) {
1497 roles.insert(map[i].role, map[i].roleType);
1498 }
1499
1500 // Insert internal roles (take care to synchronize the implementation
1501 // with KFileItemModel::roleForType() in case if a change is done).
1502 roles.insert("isDir", IsDirRole);
1503 roles.insert("isLink", IsLinkRole);
1504 roles.insert("isHidden", IsHiddenRole);
1505 roles.insert("isExpanded", IsExpandedRole);
1506 roles.insert("isExpandable", IsExpandableRole);
1507 roles.insert("expandedParentsCount", ExpandedParentsCountRole);
1508
1509 Q_ASSERT(roles.count() == RolesCount);
1510 }
1511
1512 return roles.value(role, NoRole);
1513 }
1514
1515 QByteArray KFileItemModel::roleForType(RoleType roleType) const
1516 {
1517 static QHash<RoleType, QByteArray> roles;
1518 if (roles.isEmpty()) {
1519 // Insert user visible roles that can be accessed with
1520 // KFileItemModel::roleInformation()
1521 int count = 0;
1522 const RoleInfoMap* map = rolesInfoMap(count);
1523 for (int i = 0; i < count; ++i) {
1524 roles.insert(map[i].roleType, map[i].role);
1525 }
1526
1527 // Insert internal roles (take care to synchronize the implementation
1528 // with KFileItemModel::typeForRole() in case if a change is done).
1529 roles.insert(IsDirRole, "isDir");
1530 roles.insert(IsLinkRole, "isLink");
1531 roles.insert(IsHiddenRole, "isHidden");
1532 roles.insert(IsExpandedRole, "isExpanded");
1533 roles.insert(IsExpandableRole, "isExpandable");
1534 roles.insert(ExpandedParentsCountRole, "expandedParentsCount");
1535
1536 Q_ASSERT(roles.count() == RolesCount);
1537 };
1538
1539 return roles.value(roleType);
1540 }
1541
1542 QHash<QByteArray, QVariant> KFileItemModel::retrieveData(const KFileItem& item, const ItemData* parent) const
1543 {
1544 // It is important to insert only roles that are fast to retrieve. E.g.
1545 // KFileItem::iconName() can be very expensive if the MIME-type is unknown
1546 // and hence will be retrieved asynchronously by KFileItemModelRolesUpdater.
1547 QHash<QByteArray, QVariant> data;
1548 data.insert(sharedValue("url"), item.url());
1549
1550 const bool isDir = item.isDir();
1551 if (m_requestRole[IsDirRole] && isDir) {
1552 data.insert(sharedValue("isDir"), true);
1553 }
1554
1555 if (m_requestRole[IsLinkRole] && item.isLink()) {
1556 data.insert(sharedValue("isLink"), true);
1557 }
1558
1559 if (m_requestRole[IsHiddenRole] && item.isHidden()) {
1560 data.insert(sharedValue("isHidden"), true);
1561 }
1562
1563 if (m_requestRole[NameRole]) {
1564 data.insert(sharedValue("text"), item.text());
1565 }
1566
1567 if (m_requestRole[SizeRole] && !isDir) {
1568 data.insert(sharedValue("size"), item.size());
1569 }
1570
1571 if (m_requestRole[ModificationTimeRole]) {
1572 // Don't use KFileItem::timeString() as this is too expensive when
1573 // having several thousands of items. Instead the formatting of the
1574 // date-time will be done on-demand by the view when the date will be shown.
1575 const QDateTime dateTime = item.time(KFileItem::ModificationTime);
1576 data.insert(sharedValue("modificationtime"), dateTime);
1577 }
1578
1579 if (m_requestRole[CreationTimeRole]) {
1580 // Don't use KFileItem::timeString() as this is too expensive when
1581 // having several thousands of items. Instead the formatting of the
1582 // date-time will be done on-demand by the view when the date will be shown.
1583 const QDateTime dateTime = item.time(KFileItem::CreationTime);
1584 data.insert(sharedValue("creationtime"), dateTime);
1585 }
1586
1587 if (m_requestRole[AccessTimeRole]) {
1588 // Don't use KFileItem::timeString() as this is too expensive when
1589 // having several thousands of items. Instead the formatting of the
1590 // date-time will be done on-demand by the view when the date will be shown.
1591 const QDateTime dateTime = item.time(KFileItem::AccessTime);
1592 data.insert(sharedValue("accesstime"), dateTime);
1593 }
1594
1595 if (m_requestRole[PermissionsRole]) {
1596 data.insert(sharedValue("permissions"), item.permissionsString());
1597 }
1598
1599 if (m_requestRole[OwnerRole]) {
1600 data.insert(sharedValue("owner"), item.user());
1601 }
1602
1603 if (m_requestRole[GroupRole]) {
1604 data.insert(sharedValue("group"), item.group());
1605 }
1606
1607 if (m_requestRole[DestinationRole]) {
1608 QString destination = item.linkDest();
1609 if (destination.isEmpty()) {
1610 destination = QStringLiteral("-");
1611 }
1612 data.insert(sharedValue("destination"), destination);
1613 }
1614
1615 if (m_requestRole[PathRole]) {
1616 QString path;
1617 if (item.url().scheme() == QLatin1String("trash")) {
1618 path = item.entry().stringValue(KIO::UDSEntry::UDS_EXTRA);
1619 } else {
1620 // For performance reasons cache the home-path in a static QString
1621 // (see QDir::homePath() for more details)
1622 static QString homePath;
1623 if (homePath.isEmpty()) {
1624 homePath = QDir::homePath();
1625 }
1626
1627 path = item.localPath();
1628 if (path.startsWith(homePath)) {
1629 path.replace(0, homePath.length(), QLatin1Char('~'));
1630 }
1631 }
1632
1633 const int index = path.lastIndexOf(item.text());
1634 path = path.mid(0, index - 1);
1635 data.insert(sharedValue("path"), path);
1636 }
1637
1638 if (m_requestRole[DeletionTimeRole]) {
1639 QDateTime deletionTime;
1640 if (item.url().scheme() == QLatin1String("trash")) {
1641 deletionTime = QDateTime::fromString(item.entry().stringValue(KIO::UDSEntry::UDS_EXTRA + 1), Qt::ISODate);
1642 }
1643 data.insert(sharedValue("deletiontime"), deletionTime);
1644 }
1645
1646 if (m_requestRole[IsExpandableRole] && isDir) {
1647 data.insert(sharedValue("isExpandable"), true);
1648 }
1649
1650 if (m_requestRole[ExpandedParentsCountRole]) {
1651 if (parent) {
1652 const int level = expandedParentsCount(parent) + 1;
1653 data.insert(sharedValue("expandedParentsCount"), level);
1654 }
1655 }
1656
1657 if (item.isMimeTypeKnown()) {
1658 data.insert(sharedValue("iconName"), item.iconName());
1659
1660 if (m_requestRole[TypeRole]) {
1661 data.insert(sharedValue("type"), item.mimeComment());
1662 }
1663 } else if (m_requestRole[TypeRole] && isDir) {
1664 static const QString folderMimeType = item.mimeComment();
1665 data.insert(sharedValue("type"), folderMimeType);
1666 }
1667
1668 return data;
1669 }
1670
1671 bool KFileItemModel::lessThan(const ItemData* a, const ItemData* b, const QCollator& collator) const
1672 {
1673 int result = 0;
1674
1675 if (a->parent != b->parent) {
1676 const int expansionLevelA = expandedParentsCount(a);
1677 const int expansionLevelB = expandedParentsCount(b);
1678
1679 // If b has a higher expansion level than a, check if a is a parent
1680 // of b, and make sure that both expansion levels are equal otherwise.
1681 for (int i = expansionLevelB; i > expansionLevelA; --i) {
1682 if (b->parent == a) {
1683 return true;
1684 }
1685 b = b->parent;
1686 }
1687
1688 // If a has a higher expansion level than a, check if b is a parent
1689 // of a, and make sure that both expansion levels are equal otherwise.
1690 for (int i = expansionLevelA; i > expansionLevelB; --i) {
1691 if (a->parent == b) {
1692 return false;
1693 }
1694 a = a->parent;
1695 }
1696
1697 Q_ASSERT(expandedParentsCount(a) == expandedParentsCount(b));
1698
1699 // Compare the last parents of a and b which are different.
1700 while (a->parent != b->parent) {
1701 a = a->parent;
1702 b = b->parent;
1703 }
1704 }
1705
1706 if (m_sortDirsFirst || m_sortRole == SizeRole) {
1707 const bool isDirA = a->item.isDir();
1708 const bool isDirB = b->item.isDir();
1709 if (isDirA && !isDirB) {
1710 return true;
1711 } else if (!isDirA && isDirB) {
1712 return false;
1713 }
1714 }
1715
1716 result = sortRoleCompare(a, b, collator);
1717
1718 return (sortOrder() == Qt::AscendingOrder) ? result < 0 : result > 0;
1719 }
1720
1721 /**
1722 * Helper class for KFileItemModel::sort().
1723 */
1724 class KFileItemModelLessThan
1725 {
1726 public:
1727 KFileItemModelLessThan(const KFileItemModel* model, const QCollator& collator) :
1728 m_model(model),
1729 m_collator(collator)
1730 {
1731 }
1732
1733 KFileItemModelLessThan(const KFileItemModelLessThan& other) :
1734 m_model(other.m_model),
1735 m_collator()
1736 {
1737 m_collator.setCaseSensitivity(other.m_collator.caseSensitivity());
1738 m_collator.setIgnorePunctuation(other.m_collator.ignorePunctuation());
1739 m_collator.setLocale(other.m_collator.locale());
1740 m_collator.setNumericMode(other.m_collator.numericMode());
1741 }
1742
1743 ~KFileItemModelLessThan() = default;
1744 //We do not delete m_model as the pointer was passed from outside ant it will be deleted elsewhere.
1745
1746 KFileItemModelLessThan& operator=(const KFileItemModelLessThan& other)
1747 {
1748 m_model = other.m_model;
1749 m_collator = other.m_collator;
1750 return *this;
1751 }
1752
1753 bool operator()(const KFileItemModel::ItemData* a, const KFileItemModel::ItemData* b) const
1754 {
1755 return m_model->lessThan(a, b, m_collator);
1756 }
1757
1758 private:
1759 const KFileItemModel* m_model;
1760 QCollator m_collator;
1761 };
1762
1763 void KFileItemModel::sort(QList<KFileItemModel::ItemData*>::iterator begin,
1764 QList<KFileItemModel::ItemData*>::iterator end) const
1765 {
1766 KFileItemModelLessThan lessThan(this, m_collator);
1767
1768 if (m_sortRole == NameRole) {
1769 // Sorting by name can be expensive, in particular if natural sorting is
1770 // enabled. Use all CPU cores to speed up the sorting process.
1771 static const int numberOfThreads = QThread::idealThreadCount();
1772 parallelMergeSort(begin, end, lessThan, numberOfThreads);
1773 } else {
1774 // Sorting by other roles is quite fast. Use only one thread to prevent
1775 // problems caused by non-reentrant comparison functions, see
1776 // https://bugs.kde.org/show_bug.cgi?id=312679
1777 mergeSort(begin, end, lessThan);
1778 }
1779 }
1780
1781 int KFileItemModel::sortRoleCompare(const ItemData* a, const ItemData* b, const QCollator& collator) const
1782 {
1783 const KFileItem& itemA = a->item;
1784 const KFileItem& itemB = b->item;
1785
1786 int result = 0;
1787
1788 switch (m_sortRole) {
1789 case NameRole:
1790 // The name role is handled as default fallback after the switch
1791 break;
1792
1793 case SizeRole: {
1794 if (itemA.isDir()) {
1795 // See "if (m_sortFoldersFirst || m_sortRole == SizeRole)" in KFileItemModel::lessThan():
1796 Q_ASSERT(itemB.isDir());
1797
1798 const QVariant valueA = a->values.value("size");
1799 const QVariant valueB = b->values.value("size");
1800 if (valueA.isNull() && valueB.isNull()) {
1801 result = 0;
1802 } else if (valueA.isNull()) {
1803 result = -1;
1804 } else if (valueB.isNull()) {
1805 result = +1;
1806 } else {
1807 result = valueA.toInt() - valueB.toInt();
1808 }
1809 } else {
1810 // See "if (m_sortFoldersFirst || m_sortRole == SizeRole)" in KFileItemModel::lessThan():
1811 Q_ASSERT(!itemB.isDir());
1812 const KIO::filesize_t sizeA = itemA.size();
1813 const KIO::filesize_t sizeB = itemB.size();
1814 if (sizeA > sizeB) {
1815 result = +1;
1816 } else if (sizeA < sizeB) {
1817 result = -1;
1818 } else {
1819 result = 0;
1820 }
1821 }
1822 break;
1823 }
1824
1825 case ModificationTimeRole: {
1826 const QDateTime dateTimeA = itemA.time(KFileItem::ModificationTime);
1827 const QDateTime dateTimeB = itemB.time(KFileItem::ModificationTime);
1828 if (dateTimeA < dateTimeB) {
1829 result = -1;
1830 } else if (dateTimeA > dateTimeB) {
1831 result = +1;
1832 }
1833 break;
1834 }
1835
1836 case CreationTimeRole: {
1837 const QDateTime dateTimeA = itemA.time(KFileItem::CreationTime);
1838 const QDateTime dateTimeB = itemB.time(KFileItem::CreationTime);
1839 if (dateTimeA < dateTimeB) {
1840 result = -1;
1841 } else if (dateTimeA > dateTimeB) {
1842 result = +1;
1843 }
1844 break;
1845 }
1846
1847 case DeletionTimeRole: {
1848 const QDateTime dateTimeA = a->values.value("deletiontime").toDateTime();
1849 const QDateTime dateTimeB = b->values.value("deletiontime").toDateTime();
1850 if (dateTimeA < dateTimeB) {
1851 result = -1;
1852 } else if (dateTimeA > dateTimeB) {
1853 result = +1;
1854 }
1855 break;
1856 }
1857
1858 case RatingRole: {
1859 result = a->values.value("rating").toInt() - b->values.value("rating").toInt();
1860 break;
1861 }
1862
1863 case ImageSizeRole: {
1864 // Alway use a natural comparing to interpret the numbers of a string like
1865 // "1600 x 1200" for having a correct sorting.
1866 result = collator.compare(a->values.value("imageSize").toString(),
1867 b->values.value("imageSize").toString());
1868 break;
1869 }
1870
1871 default: {
1872 const QByteArray role = roleForType(m_sortRole);
1873 result = QString::compare(a->values.value(role).toString(),
1874 b->values.value(role).toString());
1875 break;
1876 }
1877
1878 }
1879
1880 if (result != 0) {
1881 // The current sort role was sufficient to define an order
1882 return result;
1883 }
1884
1885 // Fallback #1: Compare the text of the items
1886 result = stringCompare(itemA.text(), itemB.text(), collator);
1887 if (result != 0) {
1888 return result;
1889 }
1890
1891 // Fallback #2: KFileItem::text() may not be unique in case UDS_DISPLAY_NAME is used
1892 result = stringCompare(itemA.name(), itemB.name(), collator);
1893 if (result != 0) {
1894 return result;
1895 }
1896
1897 // Fallback #3: It must be assured that the sort order is always unique even if two values have been
1898 // equal. In this case a comparison of the URL is done which is unique in all cases
1899 // within KDirLister.
1900 return QString::compare(itemA.url().url(), itemB.url().url(), Qt::CaseSensitive);
1901 }
1902
1903 int KFileItemModel::stringCompare(const QString& a, const QString& b, const QCollator& collator) const
1904 {
1905 if (m_naturalSorting) {
1906 return collator.compare(a, b);
1907 }
1908
1909 const int result = QString::compare(a, b, collator.caseSensitivity());
1910 if (result != 0 || collator.caseSensitivity() == Qt::CaseSensitive) {
1911 // Only return the result, if the strings are not equal. If they are equal by a case insensitive
1912 // comparison, still a deterministic sort order is required. A case sensitive
1913 // comparison is done as fallback.
1914 return result;
1915 }
1916
1917 return QString::compare(a, b, Qt::CaseSensitive);
1918 }
1919
1920 bool KFileItemModel::useMaximumUpdateInterval() const
1921 {
1922 return !m_dirLister->url().isLocalFile();
1923 }
1924
1925 QList<QPair<int, QVariant> > KFileItemModel::nameRoleGroups() const
1926 {
1927 Q_ASSERT(!m_itemData.isEmpty());
1928
1929 const int maxIndex = count() - 1;
1930 QList<QPair<int, QVariant> > groups;
1931
1932 QString groupValue;
1933 QChar firstChar;
1934 for (int i = 0; i <= maxIndex; ++i) {
1935 if (isChildItem(i)) {
1936 continue;
1937 }
1938
1939 const QString name = m_itemData.at(i)->item.text();
1940
1941 // Use the first character of the name as group indication
1942 QChar newFirstChar = name.at(0).toUpper();
1943 if (newFirstChar == QLatin1Char('~') && name.length() > 1) {
1944 newFirstChar = name.at(1).toUpper();
1945 }
1946
1947 if (firstChar != newFirstChar) {
1948 QString newGroupValue;
1949 if (newFirstChar.isLetter()) {
1950 // Try to find a matching group in the range 'A' to 'Z'.
1951 static std::vector<QChar> lettersAtoZ;
1952 lettersAtoZ.reserve('Z' - 'A' + 1);
1953 if (lettersAtoZ.empty()) {
1954 for (char c = 'A'; c <= 'Z'; ++c) {
1955 lettersAtoZ.push_back(QLatin1Char(c));
1956 }
1957 }
1958
1959 auto localeAwareLessThan = [this](QChar c1, QChar c2) -> bool {
1960 return m_collator.compare(c1, c2) < 0;
1961 };
1962
1963 std::vector<QChar>::iterator it = std::lower_bound(lettersAtoZ.begin(), lettersAtoZ.end(), newFirstChar, localeAwareLessThan);
1964 if (it != lettersAtoZ.end()) {
1965 if (localeAwareLessThan(newFirstChar, *it) && it != lettersAtoZ.begin()) {
1966 // newFirstChar belongs to the group preceding *it.
1967 // Example: for an umlaut 'A' in the German locale, *it would be 'B' now.
1968 --it;
1969 }
1970 newGroupValue = *it;
1971 } else {
1972 newGroupValue = newFirstChar;
1973 }
1974 } else if (newFirstChar >= QLatin1Char('0') && newFirstChar <= QLatin1Char('9')) {
1975 // Apply group '0 - 9' for any name that starts with a digit
1976 newGroupValue = i18nc("@title:group Groups that start with a digit", "0 - 9");
1977 } else {
1978 newGroupValue = i18nc("@title:group", "Others");
1979 }
1980
1981 if (newGroupValue != groupValue) {
1982 groupValue = newGroupValue;
1983 groups.append(QPair<int, QVariant>(i, newGroupValue));
1984 }
1985
1986 firstChar = newFirstChar;
1987 }
1988 }
1989 return groups;
1990 }
1991
1992 QList<QPair<int, QVariant> > KFileItemModel::sizeRoleGroups() const
1993 {
1994 Q_ASSERT(!m_itemData.isEmpty());
1995
1996 const int maxIndex = count() - 1;
1997 QList<QPair<int, QVariant> > groups;
1998
1999 QString groupValue;
2000 for (int i = 0; i <= maxIndex; ++i) {
2001 if (isChildItem(i)) {
2002 continue;
2003 }
2004
2005 const KFileItem& item = m_itemData.at(i)->item;
2006 const KIO::filesize_t fileSize = !item.isNull() ? item.size() : ~0U;
2007 QString newGroupValue;
2008 if (!item.isNull() && item.isDir()) {
2009 newGroupValue = i18nc("@title:group Size", "Folders");
2010 } else if (fileSize < 5 * 1024 * 1024) {
2011 newGroupValue = i18nc("@title:group Size", "Small");
2012 } else if (fileSize < 10 * 1024 * 1024) {
2013 newGroupValue = i18nc("@title:group Size", "Medium");
2014 } else {
2015 newGroupValue = i18nc("@title:group Size", "Big");
2016 }
2017
2018 if (newGroupValue != groupValue) {
2019 groupValue = newGroupValue;
2020 groups.append(QPair<int, QVariant>(i, newGroupValue));
2021 }
2022 }
2023
2024 return groups;
2025 }
2026
2027 QList<QPair<int, QVariant> > KFileItemModel::timeRoleGroups(std::function<QDateTime(const ItemData *)> fileTimeCb) const
2028 {
2029 Q_ASSERT(!m_itemData.isEmpty());
2030
2031 const int maxIndex = count() - 1;
2032 QList<QPair<int, QVariant> > groups;
2033
2034 const QDate currentDate = QDate::currentDate();
2035
2036 QDate previousFileDate;
2037 QString groupValue;
2038 for (int i = 0; i <= maxIndex; ++i) {
2039 if (isChildItem(i)) {
2040 continue;
2041 }
2042
2043 const QDateTime fileTime = fileTimeCb(m_itemData.at(i));
2044 const QDate fileDate = fileTime.date();
2045 if (fileDate == previousFileDate) {
2046 // The current item is in the same group as the previous item
2047 continue;
2048 }
2049 previousFileDate = fileDate;
2050
2051 const int daysDistance = fileDate.daysTo(currentDate);
2052
2053 QString newGroupValue;
2054 if (currentDate.year() == fileDate.year() &&
2055 currentDate.month() == fileDate.month()) {
2056
2057 switch (daysDistance / 7) {
2058 case 0:
2059 switch (daysDistance) {
2060 case 0: newGroupValue = i18nc("@title:group Date", "Today"); break;
2061 case 1: newGroupValue = i18nc("@title:group Date", "Yesterday"); break;
2062 default:
2063 newGroupValue = fileTime.toString(
2064 i18nc("@title:group Date: The week day name: dddd", "dddd"));
2065 newGroupValue = i18nc("Can be used to script translation of \"dddd\""
2066 "with context @title:group Date", "%1", newGroupValue);
2067 }
2068 break;
2069 case 1:
2070 newGroupValue = i18nc("@title:group Date", "One Week Ago");
2071 break;
2072 case 2:
2073 newGroupValue = i18nc("@title:group Date", "Two Weeks Ago");
2074 break;
2075 case 3:
2076 newGroupValue = i18nc("@title:group Date", "Three Weeks Ago");
2077 break;
2078 case 4:
2079 case 5:
2080 newGroupValue = i18nc("@title:group Date", "Earlier this Month");
2081 break;
2082 default:
2083 Q_ASSERT(false);
2084 }
2085 } else {
2086 const QDate lastMonthDate = currentDate.addMonths(-1);
2087 if (lastMonthDate.year() == fileDate.year() &&
2088 lastMonthDate.month() == fileDate.month()) {
2089
2090 if (daysDistance == 1) {
2091 newGroupValue = fileTime.toString(i18nc("@title:group Date: "
2092 "MMMM is full month name in current locale, and yyyy is "
2093 "full year number", "'Yesterday' (MMMM, yyyy)"));
2094 newGroupValue = i18nc("Can be used to script translation of "
2095 "\"'Yesterday' (MMMM, yyyy)\" with context @title:group Date",
2096 "%1", newGroupValue);
2097 } else if (daysDistance <= 7) {
2098 newGroupValue = fileTime.toString(i18nc("@title:group Date: "
2099 "The week day name: dddd, MMMM is full month name "
2100 "in current locale, and yyyy is full year number",
2101 "dddd (MMMM, yyyy)"));
2102 newGroupValue = i18nc("Can be used to script translation of "
2103 "\"dddd (MMMM, yyyy)\" with context @title:group Date",
2104 "%1", newGroupValue);
2105 } else if (daysDistance <= 7 * 2) {
2106 newGroupValue = fileTime.toString(i18nc("@title:group Date: "
2107 "MMMM is full month name in current locale, and yyyy is "
2108 "full year number", "'One Week Ago' (MMMM, yyyy)"));
2109 newGroupValue = i18nc("Can be used to script translation of "
2110 "\"'One Week Ago' (MMMM, yyyy)\" with context @title:group Date",
2111 "%1", newGroupValue);
2112 } else if (daysDistance <= 7 * 3) {
2113 newGroupValue = fileTime.toString(i18nc("@title:group Date: "
2114 "MMMM is full month name in current locale, and yyyy is "
2115 "full year number", "'Two Weeks Ago' (MMMM, yyyy)"));
2116 newGroupValue = i18nc("Can be used to script translation of "
2117 "\"'Two Weeks Ago' (MMMM, yyyy)\" with context @title:group Date",
2118 "%1", newGroupValue);
2119 } else if (daysDistance <= 7 * 4) {
2120 newGroupValue = fileTime.toString(i18nc("@title:group Date: "
2121 "MMMM is full month name in current locale, and yyyy is "
2122 "full year number", "'Three Weeks Ago' (MMMM, yyyy)"));
2123 newGroupValue = i18nc("Can be used to script translation of "
2124 "\"'Three Weeks Ago' (MMMM, yyyy)\" with context @title:group Date",
2125 "%1", newGroupValue);
2126 } else {
2127 newGroupValue = fileTime.toString(i18nc("@title:group Date: "
2128 "MMMM is full month name in current locale, and yyyy is "
2129 "full year number", "'Earlier on' MMMM, yyyy"));
2130 newGroupValue = i18nc("Can be used to script translation of "
2131 "\"'Earlier on' MMMM, yyyy\" with context @title:group Date",
2132 "%1", newGroupValue);
2133 }
2134 } else {
2135 newGroupValue = fileTime.toString(i18nc("@title:group "
2136 "The month and year: MMMM is full month name in current locale, "
2137 "and yyyy is full year number", "MMMM, yyyy"));
2138 newGroupValue = i18nc("Can be used to script translation of "
2139 "\"MMMM, yyyy\" with context @title:group Date",
2140 "%1", newGroupValue);
2141 }
2142 }
2143
2144 if (newGroupValue != groupValue) {
2145 groupValue = newGroupValue;
2146 groups.append(QPair<int, QVariant>(i, newGroupValue));
2147 }
2148 }
2149
2150 return groups;
2151 }
2152
2153 QList<QPair<int, QVariant> > KFileItemModel::permissionRoleGroups() const
2154 {
2155 Q_ASSERT(!m_itemData.isEmpty());
2156
2157 const int maxIndex = count() - 1;
2158 QList<QPair<int, QVariant> > groups;
2159
2160 QString permissionsString;
2161 QString groupValue;
2162 for (int i = 0; i <= maxIndex; ++i) {
2163 if (isChildItem(i)) {
2164 continue;
2165 }
2166
2167 const ItemData* itemData = m_itemData.at(i);
2168 const QString newPermissionsString = itemData->values.value("permissions").toString();
2169 if (newPermissionsString == permissionsString) {
2170 continue;
2171 }
2172 permissionsString = newPermissionsString;
2173
2174 const QFileInfo info(itemData->item.url().toLocalFile());
2175
2176 // Set user string
2177 QString user;
2178 if (info.permission(QFile::ReadUser)) {
2179 user = i18nc("@item:intext Access permission, concatenated", "Read, ");
2180 }
2181 if (info.permission(QFile::WriteUser)) {
2182 user += i18nc("@item:intext Access permission, concatenated", "Write, ");
2183 }
2184 if (info.permission(QFile::ExeUser)) {
2185 user += i18nc("@item:intext Access permission, concatenated", "Execute, ");
2186 }
2187 user = user.isEmpty() ? i18nc("@item:intext Access permission, concatenated", "Forbidden") : user.mid(0, user.count() - 2);
2188
2189 // Set group string
2190 QString group;
2191 if (info.permission(QFile::ReadGroup)) {
2192 group = i18nc("@item:intext Access permission, concatenated", "Read, ");
2193 }
2194 if (info.permission(QFile::WriteGroup)) {
2195 group += i18nc("@item:intext Access permission, concatenated", "Write, ");
2196 }
2197 if (info.permission(QFile::ExeGroup)) {
2198 group += i18nc("@item:intext Access permission, concatenated", "Execute, ");
2199 }
2200 group = group.isEmpty() ? i18nc("@item:intext Access permission, concatenated", "Forbidden") : group.mid(0, group.count() - 2);
2201
2202 // Set others string
2203 QString others;
2204 if (info.permission(QFile::ReadOther)) {
2205 others = i18nc("@item:intext Access permission, concatenated", "Read, ");
2206 }
2207 if (info.permission(QFile::WriteOther)) {
2208 others += i18nc("@item:intext Access permission, concatenated", "Write, ");
2209 }
2210 if (info.permission(QFile::ExeOther)) {
2211 others += i18nc("@item:intext Access permission, concatenated", "Execute, ");
2212 }
2213 others = others.isEmpty() ? i18nc("@item:intext Access permission, concatenated", "Forbidden") : others.mid(0, others.count() - 2);
2214
2215 const QString newGroupValue = i18nc("@title:group Files and folders by permissions", "User: %1 | Group: %2 | Others: %3", user, group, others);
2216 if (newGroupValue != groupValue) {
2217 groupValue = newGroupValue;
2218 groups.append(QPair<int, QVariant>(i, newGroupValue));
2219 }
2220 }
2221
2222 return groups;
2223 }
2224
2225 QList<QPair<int, QVariant> > KFileItemModel::ratingRoleGroups() const
2226 {
2227 Q_ASSERT(!m_itemData.isEmpty());
2228
2229 const int maxIndex = count() - 1;
2230 QList<QPair<int, QVariant> > groups;
2231
2232 int groupValue = -1;
2233 for (int i = 0; i <= maxIndex; ++i) {
2234 if (isChildItem(i)) {
2235 continue;
2236 }
2237 const int newGroupValue = m_itemData.at(i)->values.value("rating", 0).toInt();
2238 if (newGroupValue != groupValue) {
2239 groupValue = newGroupValue;
2240 groups.append(QPair<int, QVariant>(i, newGroupValue));
2241 }
2242 }
2243
2244 return groups;
2245 }
2246
2247 QList<QPair<int, QVariant> > KFileItemModel::genericStringRoleGroups(const QByteArray& role) const
2248 {
2249 Q_ASSERT(!m_itemData.isEmpty());
2250
2251 const int maxIndex = count() - 1;
2252 QList<QPair<int, QVariant> > groups;
2253
2254 bool isFirstGroupValue = true;
2255 QString groupValue;
2256 for (int i = 0; i <= maxIndex; ++i) {
2257 if (isChildItem(i)) {
2258 continue;
2259 }
2260 const QString newGroupValue = m_itemData.at(i)->values.value(role).toString();
2261 if (newGroupValue != groupValue || isFirstGroupValue) {
2262 groupValue = newGroupValue;
2263 groups.append(QPair<int, QVariant>(i, newGroupValue));
2264 isFirstGroupValue = false;
2265 }
2266 }
2267
2268 return groups;
2269 }
2270
2271 void KFileItemModel::emitSortProgress(int resolvedCount)
2272 {
2273 // Be tolerant against a resolvedCount with a wrong range.
2274 // Although there should not be a case where KFileItemModelRolesUpdater
2275 // (= caller) provides a wrong range, it is important to emit
2276 // a useful progress information even if there is an unexpected
2277 // implementation issue.
2278
2279 const int itemCount = count();
2280 if (resolvedCount >= itemCount) {
2281 m_sortingProgressPercent = -1;
2282 if (m_resortAllItemsTimer->isActive()) {
2283 m_resortAllItemsTimer->stop();
2284 resortAllItems();
2285 }
2286
2287 emit directorySortingProgress(100);
2288 } else if (itemCount > 0) {
2289 resolvedCount = qBound(0, resolvedCount, itemCount);
2290
2291 const int progress = resolvedCount * 100 / itemCount;
2292 if (m_sortingProgressPercent != progress) {
2293 m_sortingProgressPercent = progress;
2294 emit directorySortingProgress(progress);
2295 }
2296 }
2297 }
2298
2299 const KFileItemModel::RoleInfoMap* KFileItemModel::rolesInfoMap(int& count)
2300 {
2301 static const RoleInfoMap rolesInfoMap[] = {
2302 // | role | roleType | role translation | group translation | requires Baloo | requires indexer
2303 { nullptr, NoRole, nullptr, nullptr, nullptr, nullptr, false, false },
2304 { "text", NameRole, I18N_NOOP2_NOSTRIP("@label", "Name"), nullptr, nullptr, false, false },
2305 { "size", SizeRole, I18N_NOOP2_NOSTRIP("@label", "Size"), nullptr, nullptr, false, false },
2306 { "modificationtime", ModificationTimeRole, I18N_NOOP2_NOSTRIP("@label", "Modified"), nullptr, nullptr, false, false },
2307 { "creationtime", CreationTimeRole, I18N_NOOP2_NOSTRIP("@label", "Created"), nullptr, nullptr, false, false },
2308 { "accesstime", AccessTimeRole, I18N_NOOP2_NOSTRIP("@label", "Accessed"), nullptr, nullptr, false, false },
2309 { "type", TypeRole, I18N_NOOP2_NOSTRIP("@label", "Type"), nullptr, nullptr, false, false },
2310 { "rating", RatingRole, I18N_NOOP2_NOSTRIP("@label", "Rating"), nullptr, nullptr, true, false },
2311 { "tags", TagsRole, I18N_NOOP2_NOSTRIP("@label", "Tags"), nullptr, nullptr, true, false },
2312 { "comment", CommentRole, I18N_NOOP2_NOSTRIP("@label", "Comment"), nullptr, nullptr, true, false },
2313 { "title", TitleRole, I18N_NOOP2_NOSTRIP("@label", "Title"), I18N_NOOP2_NOSTRIP("@label", "Document"), true, true },
2314 { "wordCount", WordCountRole, I18N_NOOP2_NOSTRIP("@label", "Word Count"), I18N_NOOP2_NOSTRIP("@label", "Document"), true, true },
2315 { "lineCount", LineCountRole, I18N_NOOP2_NOSTRIP("@label", "Line Count"), I18N_NOOP2_NOSTRIP("@label", "Document"), true, true },
2316 { "imageDateTime", ImageDateTimeRole, I18N_NOOP2_NOSTRIP("@label", "Date Photographed"), I18N_NOOP2_NOSTRIP("@label", "Image"), true, true },
2317 { "imageSize", ImageSizeRole, I18N_NOOP2_NOSTRIP("@label", "Image Size"), I18N_NOOP2_NOSTRIP("@label", "Image"), true, true },
2318 { "orientation", OrientationRole, I18N_NOOP2_NOSTRIP("@label", "Orientation"), I18N_NOOP2_NOSTRIP("@label", "Image"), true, true },
2319 { "artist", ArtistRole, I18N_NOOP2_NOSTRIP("@label", "Artist"), I18N_NOOP2_NOSTRIP("@label", "Audio"), true, true },
2320 { "genre", GenreRole, I18N_NOOP2_NOSTRIP("@label", "Genre"), I18N_NOOP2_NOSTRIP("@label", "Audio"), true, true },
2321 { "album", AlbumRole, I18N_NOOP2_NOSTRIP("@label", "Album"), I18N_NOOP2_NOSTRIP("@label", "Audio"), true, true },
2322 { "duration", DurationRole, I18N_NOOP2_NOSTRIP("@label", "Duration"), I18N_NOOP2_NOSTRIP("@label", "Audio"), true, true },
2323 { "bitrate", BitrateRole, I18N_NOOP2_NOSTRIP("@label", "Bitrate"), I18N_NOOP2_NOSTRIP("@label", "Audio"), true, true },
2324 { "track", TrackRole, I18N_NOOP2_NOSTRIP("@label", "Track"), I18N_NOOP2_NOSTRIP("@label", "Audio"), true, true },
2325 { "releaseYear", ReleaseYearRole, I18N_NOOP2_NOSTRIP("@label", "Release Year"), I18N_NOOP2_NOSTRIP("@label", "Audio"), true, true },
2326 { "path", PathRole, I18N_NOOP2_NOSTRIP("@label", "Path"), I18N_NOOP2_NOSTRIP("@label", "Other"), false, false },
2327 { "deletiontime",DeletionTimeRole,I18N_NOOP2_NOSTRIP("@label", "Deletion Time"), I18N_NOOP2_NOSTRIP("@label", "Other"), false, false },
2328 { "destination", DestinationRole, I18N_NOOP2_NOSTRIP("@label", "Link Destination"), I18N_NOOP2_NOSTRIP("@label", "Other"), false, false },
2329 { "originUrl", OriginUrlRole, I18N_NOOP2_NOSTRIP("@label", "Downloaded From"), I18N_NOOP2_NOSTRIP("@label", "Other"), true, false },
2330 { "permissions", PermissionsRole, I18N_NOOP2_NOSTRIP("@label", "Permissions"), I18N_NOOP2_NOSTRIP("@label", "Other"), false, false },
2331 { "owner", OwnerRole, I18N_NOOP2_NOSTRIP("@label", "Owner"), I18N_NOOP2_NOSTRIP("@label", "Other"), false, false },
2332 { "group", GroupRole, I18N_NOOP2_NOSTRIP("@label", "User Group"), I18N_NOOP2_NOSTRIP("@label", "Other"), false, false },
2333 };
2334
2335 count = sizeof(rolesInfoMap) / sizeof(RoleInfoMap);
2336 return rolesInfoMap;
2337 }
2338
2339 void KFileItemModel::determineMimeTypes(const KFileItemList& items, int timeout)
2340 {
2341 QElapsedTimer timer;
2342 timer.start();
2343 foreach (const KFileItem& item, items) { // krazy:exclude=foreach
2344 // Only determine mime types for files here. For directories,
2345 // KFileItem::determineMimeType() reads the .directory file inside to
2346 // load the icon, but this is not necessary at all if we just need the
2347 // type. Some special code for setting the correct mime type for
2348 // directories is in retrieveData().
2349 if (!item.isDir()) {
2350 item.determineMimeType();
2351 }
2352
2353 if (timer.elapsed() > timeout) {
2354 // Don't block the user interface, let the remaining items
2355 // be resolved asynchronously.
2356 return;
2357 }
2358 }
2359 }
2360
2361 QByteArray KFileItemModel::sharedValue(const QByteArray& value)
2362 {
2363 static QSet<QByteArray> pool;
2364 const QSet<QByteArray>::const_iterator it = pool.constFind(value);
2365
2366 if (it != pool.constEnd()) {
2367 return *it;
2368 } else {
2369 pool.insert(value);
2370 return value;
2371 }
2372 }
2373
2374 bool KFileItemModel::isConsistent() const
2375 {
2376 // m_items may contain less items than m_itemData because m_items
2377 // is populated lazily, see KFileItemModel::index(const QUrl& url).
2378 if (m_items.count() > m_itemData.count()) {
2379 return false;
2380 }
2381
2382 for (int i = 0, iMax = count(); i < iMax; ++i) {
2383 // Check if m_items and m_itemData are consistent.
2384 const KFileItem item = fileItem(i);
2385 if (item.isNull()) {
2386 qCWarning(DolphinDebug) << "Item" << i << "is null";
2387 return false;
2388 }
2389
2390 const int itemIndex = index(item);
2391 if (itemIndex != i) {
2392 qCWarning(DolphinDebug) << "Item" << i << "has a wrong index:" << itemIndex;
2393 return false;
2394 }
2395
2396 // Check if the items are sorted correctly.
2397 if (i > 0 && !lessThan(m_itemData.at(i - 1), m_itemData.at(i), m_collator)) {
2398 qCWarning(DolphinDebug) << "The order of items" << i - 1 << "and" << i << "is wrong:"
2399 << fileItem(i - 1) << fileItem(i);
2400 return false;
2401 }
2402
2403 // Check if all parent-child relationships are consistent.
2404 const ItemData* data = m_itemData.at(i);
2405 const ItemData* parent = data->parent;
2406 if (parent) {
2407 if (expandedParentsCount(data) != expandedParentsCount(parent) + 1) {
2408 qCWarning(DolphinDebug) << "expandedParentsCount is inconsistent for parent" << parent->item << "and child" << data->item;
2409 return false;
2410 }
2411
2412 const int parentIndex = index(parent->item);
2413 if (parentIndex >= i) {
2414 qCWarning(DolphinDebug) << "Index" << parentIndex << "of parent" << parent->item << "is not smaller than index" << i << "of child" << data->item;
2415 return false;
2416 }
2417 }
2418 }
2419
2420 return true;
2421 }