]> cloud.milkyroute.net Git - dolphin.git/blob - src/kitemviews/kfileitemmodelrolesupdater.cpp
KFileItemModelRolesUpdater: Show emblems even in hover sequence
[dolphin.git] / src / kitemviews / kfileitemmodelrolesupdater.cpp
1 /*
2 * SPDX-FileCopyrightText: 2011 Peter Penz <peter.penz19@gmail.com>
3 *
4 * SPDX-License-Identifier: GPL-2.0-or-later
5 */
6
7 #include "kfileitemmodelrolesupdater.h"
8
9 #include "dolphindebug.h"
10 #include "kfileitemmodel.h"
11 #include "private/kdirectorycontentscounter.h"
12 #include "private/kpixmapmodifier.h"
13
14 #include <KConfig>
15 #include <KConfigGroup>
16 #include <KIO/ListJob>
17 #include <KIO/PreviewJob>
18 #include <KIconLoader>
19 #include <KIconUtils>
20 #include <KJobWidgets>
21 #include <KOverlayIconPlugin>
22 #include <KPluginMetaData>
23 #include <KSharedConfig>
24
25 #include "dolphin_contentdisplaysettings.h"
26
27 #if HAVE_BALOO
28 #include "private/kbaloorolesprovider.h"
29 #include <Baloo/File>
30 #include <Baloo/FileMonitor>
31 #endif
32
33 #include <QApplication>
34 #include <QElapsedTimer>
35 #include <QFileInfo>
36 #include <QPainter>
37 #include <QPluginLoader>
38 #include <QTimer>
39 #include <chrono>
40
41 using namespace std::chrono_literals;
42
43 // #define KFILEITEMMODELROLESUPDATER_DEBUG
44
45 namespace
46 {
47 // Maximum time in ms that the KFileItemModelRolesUpdater
48 // may perform a blocking operation
49 const int MaxBlockTimeout = 200;
50
51 // If the number of items is smaller than ResolveAllItemsLimit,
52 // the roles of all items will be resolved.
53 const int ResolveAllItemsLimit = 500;
54
55 // Not only the visible area, but up to ReadAheadPages before and after
56 // this area will be resolved.
57 const int ReadAheadPages = 5;
58 }
59
60 KFileItemModelRolesUpdater::KFileItemModelRolesUpdater(KFileItemModel *model, QObject *parent)
61 : QObject(parent)
62 , m_state(Idle)
63 , m_previewChangedDuringPausing(false)
64 , m_iconSizeChangedDuringPausing(false)
65 , m_rolesChangedDuringPausing(false)
66 , m_previewShown(false)
67 , m_enlargeSmallPreviews(true)
68 , m_clearPreviews(false)
69 , m_finishedItems()
70 , m_model(model)
71 , m_iconSize()
72 , m_devicePixelRatio(1.0)
73 , m_firstVisibleIndex(0)
74 , m_lastVisibleIndex(-1)
75 , m_maximumVisibleItems(50)
76 , m_roles()
77 , m_resolvableRoles()
78 , m_enabledPlugins()
79 , m_localFileSizePreviewLimit(0)
80 , m_pendingSortRoleItems()
81 , m_pendingIndexes()
82 , m_pendingPreviewItems()
83 , m_previewJob()
84 , m_hoverSequenceItem()
85 , m_hoverSequenceIndex(0)
86 , m_hoverSequencePreviewJob(nullptr)
87 , m_hoverSequenceNumSuccessiveFailures(0)
88 , m_recentlyChangedItemsTimer(nullptr)
89 , m_recentlyChangedItems()
90 , m_changedItems()
91 , m_directoryContentsCounter(nullptr)
92 #if HAVE_BALOO
93 , m_balooFileMonitor(nullptr)
94 #endif
95 {
96 Q_ASSERT(model);
97
98 const KConfigGroup globalConfig(KSharedConfig::openConfig(), QStringLiteral("PreviewSettings"));
99 m_enabledPlugins = globalConfig.readEntry("Plugins", KIO::PreviewJob::defaultPlugins());
100 m_localFileSizePreviewLimit = static_cast<qulonglong>(globalConfig.readEntry("MaximumSize", 0));
101
102 connect(m_model, &KFileItemModel::itemsInserted, this, &KFileItemModelRolesUpdater::slotItemsInserted);
103 connect(m_model, &KFileItemModel::itemsRemoved, this, &KFileItemModelRolesUpdater::slotItemsRemoved);
104 connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
105 connect(m_model, &KFileItemModel::itemsMoved, this, &KFileItemModelRolesUpdater::slotItemsMoved);
106 connect(m_model, &KFileItemModel::sortRoleChanged, this, &KFileItemModelRolesUpdater::slotSortRoleChanged);
107
108 // Use a timer to prevent that each call of slotItemsChanged() results in a synchronous
109 // resolving of the roles. Postpone the resolving until no update has been done for 100 ms.
110 m_recentlyChangedItemsTimer = new QTimer(this);
111 m_recentlyChangedItemsTimer->setInterval(100ms);
112 m_recentlyChangedItemsTimer->setSingleShot(true);
113 connect(m_recentlyChangedItemsTimer, &QTimer::timeout, this, &KFileItemModelRolesUpdater::resolveRecentlyChangedItems);
114
115 m_resolvableRoles.insert("size");
116 m_resolvableRoles.insert("type");
117 m_resolvableRoles.insert("isExpandable");
118 #if HAVE_BALOO
119 m_resolvableRoles += KBalooRolesProvider::instance().roles();
120 #endif
121
122 m_directoryContentsCounter = new KDirectoryContentsCounter(m_model, this);
123 connect(m_directoryContentsCounter, &KDirectoryContentsCounter::result, this, &KFileItemModelRolesUpdater::slotDirectoryContentsCountReceived);
124
125 const auto plugins = KPluginMetaData::findPlugins(QStringLiteral("kf6/overlayicon"), {}, KPluginMetaData::AllowEmptyMetaData);
126 for (const KPluginMetaData &data : plugins) {
127 auto instance = QPluginLoader(data.fileName()).instance();
128 auto plugin = qobject_cast<KOverlayIconPlugin *>(instance);
129 if (plugin) {
130 m_overlayIconsPlugin.append(plugin);
131 connect(plugin, &KOverlayIconPlugin::overlaysChanged, this, &KFileItemModelRolesUpdater::slotOverlaysChanged);
132 } else {
133 // not our/valid plugin, so delete the created object
134 delete instance;
135 }
136 }
137 }
138
139 KFileItemModelRolesUpdater::~KFileItemModelRolesUpdater()
140 {
141 killPreviewJob();
142 }
143
144 void KFileItemModelRolesUpdater::setIconSize(const QSize &size)
145 {
146 if (size != m_iconSize) {
147 m_iconSize = size;
148 if (m_state == Paused) {
149 m_iconSizeChangedDuringPausing = true;
150 } else if (m_previewShown) {
151 // An icon size change requires the regenerating of
152 // all previews
153 m_finishedItems.clear();
154 startUpdating();
155 }
156 }
157 }
158
159 QSize KFileItemModelRolesUpdater::iconSize() const
160 {
161 return m_iconSize;
162 }
163
164 void KFileItemModelRolesUpdater::setDevicePixelRatio(qreal devicePixelRatio)
165 {
166 if (m_devicePixelRatio != devicePixelRatio) {
167 m_devicePixelRatio = devicePixelRatio;
168 if (m_state == Paused) {
169 m_iconSizeChangedDuringPausing = true;
170 } else if (m_previewShown) {
171 // A dpr change requires the regenerating of all previews.
172 m_finishedItems.clear();
173 startUpdating();
174 }
175 }
176 }
177
178 qreal KFileItemModelRolesUpdater::devicePixelRatio() const
179 {
180 return m_devicePixelRatio;
181 }
182
183 void KFileItemModelRolesUpdater::setVisibleIndexRange(int index, int count)
184 {
185 if (index < 0) {
186 index = 0;
187 }
188 if (count < 0) {
189 count = 0;
190 }
191
192 if (index == m_firstVisibleIndex && count == m_lastVisibleIndex - m_firstVisibleIndex + 1) {
193 // The range has not been changed
194 return;
195 }
196
197 m_firstVisibleIndex = index;
198 m_lastVisibleIndex = qMin(index + count - 1, m_model->count() - 1);
199
200 startUpdating();
201 }
202
203 void KFileItemModelRolesUpdater::setMaximumVisibleItems(int count)
204 {
205 m_maximumVisibleItems = count;
206 }
207
208 void KFileItemModelRolesUpdater::setPreviewsShown(bool show)
209 {
210 if (show == m_previewShown) {
211 return;
212 }
213
214 m_previewShown = show;
215 if (!show) {
216 m_clearPreviews = true;
217 }
218
219 updateAllPreviews();
220 }
221
222 bool KFileItemModelRolesUpdater::previewsShown() const
223 {
224 return m_previewShown;
225 }
226
227 void KFileItemModelRolesUpdater::setEnlargeSmallPreviews(bool enlarge)
228 {
229 if (enlarge != m_enlargeSmallPreviews) {
230 m_enlargeSmallPreviews = enlarge;
231 if (m_previewShown) {
232 updateAllPreviews();
233 }
234 }
235 }
236
237 bool KFileItemModelRolesUpdater::enlargeSmallPreviews() const
238 {
239 return m_enlargeSmallPreviews;
240 }
241
242 void KFileItemModelRolesUpdater::setEnabledPlugins(const QStringList &list)
243 {
244 if (m_enabledPlugins != list) {
245 m_enabledPlugins = list;
246 if (m_previewShown) {
247 updateAllPreviews();
248 }
249 }
250 }
251
252 void KFileItemModelRolesUpdater::setPaused(bool paused)
253 {
254 if (paused == (m_state == Paused)) {
255 return;
256 }
257
258 if (paused) {
259 m_state = Paused;
260 killPreviewJob();
261 } else {
262 const bool updatePreviews = (m_iconSizeChangedDuringPausing && m_previewShown) || m_previewChangedDuringPausing;
263 const bool resolveAll = updatePreviews || m_rolesChangedDuringPausing;
264 if (resolveAll) {
265 m_finishedItems.clear();
266 }
267
268 m_iconSizeChangedDuringPausing = false;
269 m_previewChangedDuringPausing = false;
270 m_rolesChangedDuringPausing = false;
271
272 if (!m_pendingSortRoleItems.isEmpty()) {
273 m_state = ResolvingSortRole;
274 resolveNextSortRole();
275 } else {
276 m_state = Idle;
277 }
278
279 startUpdating();
280 }
281 }
282
283 void KFileItemModelRolesUpdater::setRoles(const QSet<QByteArray> &roles)
284 {
285 if (m_roles != roles) {
286 m_roles = roles;
287
288 #if HAVE_BALOO
289 // Check whether there is at least one role that must be resolved
290 // with the help of Baloo. If this is the case, a (quite expensive)
291 // resolving will be done in KFileItemModelRolesUpdater::rolesData() and
292 // the role gets watched for changes.
293 const KBalooRolesProvider &rolesProvider = KBalooRolesProvider::instance();
294 bool hasBalooRole = false;
295 QSetIterator<QByteArray> it(roles);
296 while (it.hasNext()) {
297 const QByteArray &role = it.next();
298 if (rolesProvider.roles().contains(role)) {
299 hasBalooRole = true;
300 break;
301 }
302 }
303
304 if (hasBalooRole && m_balooConfig.fileIndexingEnabled() && !m_balooFileMonitor) {
305 m_balooFileMonitor = new Baloo::FileMonitor(this);
306 connect(m_balooFileMonitor, &Baloo::FileMonitor::fileMetaDataChanged, this, &KFileItemModelRolesUpdater::applyChangedBalooRoles);
307 } else if (!hasBalooRole && m_balooFileMonitor) {
308 delete m_balooFileMonitor;
309 m_balooFileMonitor = nullptr;
310 }
311 #endif
312
313 if (m_state == Paused) {
314 m_rolesChangedDuringPausing = true;
315 } else {
316 startUpdating();
317 }
318 }
319 }
320
321 QSet<QByteArray> KFileItemModelRolesUpdater::roles() const
322 {
323 return m_roles;
324 }
325
326 bool KFileItemModelRolesUpdater::isPaused() const
327 {
328 return m_state == Paused;
329 }
330
331 QStringList KFileItemModelRolesUpdater::enabledPlugins() const
332 {
333 return m_enabledPlugins;
334 }
335
336 void KFileItemModelRolesUpdater::setLocalFileSizePreviewLimit(const qlonglong size)
337 {
338 m_localFileSizePreviewLimit = size;
339 }
340
341 qlonglong KFileItemModelRolesUpdater::localFileSizePreviewLimit() const
342 {
343 return m_localFileSizePreviewLimit;
344 }
345
346 void KFileItemModelRolesUpdater::setHoverSequenceState(const QUrl &itemUrl, int seqIdx)
347 {
348 const KFileItem item = m_model->fileItem(itemUrl);
349
350 if (item != m_hoverSequenceItem) {
351 killHoverSequencePreviewJob();
352 }
353
354 m_hoverSequenceItem = item;
355 m_hoverSequenceIndex = seqIdx;
356
357 if (!m_previewShown) {
358 return;
359 }
360
361 m_hoverSequenceNumSuccessiveFailures = 0;
362
363 loadNextHoverSequencePreview();
364 }
365
366 void KFileItemModelRolesUpdater::slotItemsInserted(const KItemRangeList &itemRanges)
367 {
368 QElapsedTimer timer;
369 timer.start();
370
371 // Determine the sort role synchronously for as many items as possible.
372 if (m_resolvableRoles.contains(m_model->sortRole())) {
373 int insertedCount = 0;
374 for (const KItemRange &range : itemRanges) {
375 const int lastIndex = insertedCount + range.index + range.count - 1;
376 for (int i = insertedCount + range.index; i <= lastIndex; ++i) {
377 if (timer.elapsed() < MaxBlockTimeout) {
378 applySortRole(i);
379 } else {
380 m_pendingSortRoleItems.insert(m_model->fileItem(i));
381 }
382 }
383 insertedCount += range.count;
384 }
385
386 applySortProgressToModel();
387
388 // If there are still items whose sort role is unknown, check if the
389 // asynchronous determination of the sort role is already in progress,
390 // and start it if that is not the case.
391 if (!m_pendingSortRoleItems.isEmpty() && m_state != ResolvingSortRole) {
392 killPreviewJob();
393 m_state = ResolvingSortRole;
394 resolveNextSortRole();
395 }
396 }
397
398 startUpdating();
399 }
400
401 void KFileItemModelRolesUpdater::slotItemsRemoved(const KItemRangeList &itemRanges)
402 {
403 Q_UNUSED(itemRanges)
404
405 const bool allItemsRemoved = (m_model->count() == 0);
406
407 #if HAVE_BALOO
408 if (m_balooFileMonitor) {
409 // Don't let the FileWatcher watch for removed items
410 if (allItemsRemoved) {
411 m_balooFileMonitor->clear();
412 } else {
413 QStringList newFileList;
414 const QStringList oldFileList = m_balooFileMonitor->files();
415 for (const QString &file : oldFileList) {
416 if (m_model->index(QUrl::fromLocalFile(file)) >= 0) {
417 newFileList.append(file);
418 }
419 }
420 m_balooFileMonitor->setFiles(newFileList);
421 }
422 }
423 #endif
424
425 if (allItemsRemoved) {
426 m_state = Idle;
427
428 m_finishedItems.clear();
429 m_pendingSortRoleItems.clear();
430 m_pendingIndexes.clear();
431 m_pendingPreviewItems.clear();
432 m_recentlyChangedItems.clear();
433 m_recentlyChangedItemsTimer->stop();
434 m_changedItems.clear();
435 m_hoverSequenceLoadedItems.clear();
436
437 killPreviewJob();
438 if (!m_model->showDirectoriesOnly()) {
439 m_directoryContentsCounter->stopWorker();
440 }
441 } else {
442 // Only remove the items from m_finishedItems. They will be removed
443 // from the other sets later on.
444 QSet<KFileItem>::iterator it = m_finishedItems.begin();
445 while (it != m_finishedItems.end()) {
446 if (m_model->index(*it) < 0) {
447 it = m_finishedItems.erase(it);
448 } else {
449 ++it;
450 }
451 }
452
453 // Removed items won't have hover previews loaded anymore.
454 for (const KItemRange &itemRange : itemRanges) {
455 int index = itemRange.index;
456 for (int count = itemRange.count; count > 0; --count) {
457 const KFileItem item = m_model->fileItem(index);
458 m_hoverSequenceLoadedItems.remove(item);
459 ++index;
460 }
461 }
462
463 // The visible items might have changed.
464 startUpdating();
465 }
466 }
467
468 void KFileItemModelRolesUpdater::slotItemsMoved(KItemRange itemRange, const QList<int> &movedToIndexes)
469 {
470 Q_UNUSED(itemRange)
471 Q_UNUSED(movedToIndexes)
472
473 // The visible items might have changed.
474 startUpdating();
475 }
476
477 void KFileItemModelRolesUpdater::slotItemsChanged(const KItemRangeList &itemRanges, const QSet<QByteArray> &roles)
478 {
479 Q_UNUSED(roles)
480
481 // Find out if slotItemsChanged() has been done recently. If that is the
482 // case, resolving the roles is postponed until a timer has exceeded
483 // to prevent expensive repeated updates if files are updated frequently.
484 const bool itemsChangedRecently = m_recentlyChangedItemsTimer->isActive();
485
486 QSet<KFileItem> &targetSet = itemsChangedRecently ? m_recentlyChangedItems : m_changedItems;
487
488 for (const KItemRange &itemRange : itemRanges) {
489 int index = itemRange.index;
490 for (int count = itemRange.count; count > 0; --count) {
491 const KFileItem item = m_model->fileItem(index);
492 targetSet.insert(item);
493 ++index;
494 }
495 }
496
497 m_recentlyChangedItemsTimer->start();
498
499 if (!itemsChangedRecently) {
500 updateChangedItems();
501 }
502 }
503
504 void KFileItemModelRolesUpdater::slotSortRoleChanged(const QByteArray &current, const QByteArray &previous)
505 {
506 Q_UNUSED(current)
507 Q_UNUSED(previous)
508
509 if (m_resolvableRoles.contains(current)) {
510 m_pendingSortRoleItems.clear();
511 m_finishedItems.clear();
512
513 const int count = m_model->count();
514 QElapsedTimer timer;
515 timer.start();
516
517 // Determine the sort role synchronously for as many items as possible.
518 for (int index = 0; index < count; ++index) {
519 if (timer.elapsed() < MaxBlockTimeout) {
520 applySortRole(index);
521 } else {
522 m_pendingSortRoleItems.insert(m_model->fileItem(index));
523 }
524 }
525
526 applySortProgressToModel();
527
528 if (!m_pendingSortRoleItems.isEmpty()) {
529 // Trigger the asynchronous determination of the sort role.
530 killPreviewJob();
531 m_state = ResolvingSortRole;
532 resolveNextSortRole();
533 }
534 } else {
535 m_state = Idle;
536 m_pendingSortRoleItems.clear();
537 applySortProgressToModel();
538 }
539 }
540
541 void KFileItemModelRolesUpdater::slotGotPreview(const KFileItem &item, const QPixmap &pixmap)
542 {
543 if (m_state != PreviewJobRunning) {
544 return;
545 }
546
547 m_changedItems.remove(item);
548
549 const int index = m_model->index(item);
550 if (index < 0) {
551 return;
552 }
553
554 QHash<QByteArray, QVariant> data = rolesData(item, index);
555 const QStringList overlays = data["iconOverlays"].toStringList();
556
557 data.insert("iconPixmap", transformPreviewPixmap(pixmap, overlays));
558
559 disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
560 m_model->setData(index, data);
561 connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
562 Q_EMIT previewJobFinished(); // For unit testing
563
564 m_finishedItems.insert(item);
565 }
566
567 void KFileItemModelRolesUpdater::slotPreviewFailed(const KFileItem &item)
568 {
569 if (m_state != PreviewJobRunning) {
570 return;
571 }
572
573 m_changedItems.remove(item);
574
575 const int index = m_model->index(item);
576 if (index >= 0) {
577 QHash<QByteArray, QVariant> data;
578 data.insert("iconPixmap", QPixmap());
579
580 disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
581 m_model->setData(index, data);
582 connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
583
584 applyResolvedRoles(index, ResolveAll);
585 m_finishedItems.insert(item);
586 }
587 }
588
589 void KFileItemModelRolesUpdater::slotPreviewJobFinished()
590 {
591 m_previewJob = nullptr;
592
593 if (m_state != PreviewJobRunning) {
594 return;
595 }
596
597 m_state = Idle;
598
599 if (!m_pendingPreviewItems.isEmpty()) {
600 startPreviewJob();
601 } else {
602 if (!m_changedItems.isEmpty()) {
603 updateChangedItems();
604 }
605 }
606 }
607
608 void KFileItemModelRolesUpdater::slotHoverSequenceGotPreview(const KFileItem &item, const QPixmap &pixmap)
609 {
610 const int index = m_model->index(item);
611 if (index < 0) {
612 return;
613 }
614
615 QHash<QByteArray, QVariant> data = m_model->data(index);
616 QVector<QPixmap> pixmaps = data["hoverSequencePixmaps"].value<QVector<QPixmap>>();
617 const int loadedIndex = pixmaps.size();
618
619 float wap = m_hoverSequencePreviewJob->sequenceIndexWraparoundPoint();
620 if (!m_hoverSequencePreviewJob->handlesSequences()) {
621 wap = 1.0f;
622 }
623 if (wap >= 0.0f) {
624 data["hoverSequenceWraparoundPoint"] = wap;
625 m_model->setData(index, data);
626 }
627
628 // For hover sequence previews we never load index 0, because that's just the regular preview
629 // in "iconPixmap". But that means we'll load index 1 even for thumbnailers that don't support
630 // sequences, in which case we can just throw away the preview because it's the same as for
631 // index 0. Unfortunately we can't find it out earlier :(
632 if (wap < 0.0f || loadedIndex < static_cast<int>(wap)) {
633 // Add the preview to the model data
634
635 QPixmap scaledPixmap = transformPreviewPixmap(pixmap, data["iconOverlays"].toStringList());
636
637 pixmaps.append(scaledPixmap);
638 data["hoverSequencePixmaps"] = QVariant::fromValue(pixmaps);
639
640 m_model->setData(index, data);
641
642 const auto loadedIt = std::find(m_hoverSequenceLoadedItems.begin(), m_hoverSequenceLoadedItems.end(), item);
643 if (loadedIt == m_hoverSequenceLoadedItems.end()) {
644 m_hoverSequenceLoadedItems.push_back(item);
645 trimHoverSequenceLoadedItems();
646 }
647 }
648
649 m_hoverSequenceNumSuccessiveFailures = 0;
650 }
651
652 void KFileItemModelRolesUpdater::slotHoverSequencePreviewFailed(const KFileItem &item)
653 {
654 const int index = m_model->index(item);
655 if (index < 0) {
656 return;
657 }
658
659 static const int numRetries = 2;
660
661 QHash<QByteArray, QVariant> data = m_model->data(index);
662 QVector<QPixmap> pixmaps = data["hoverSequencePixmaps"].value<QVector<QPixmap>>();
663
664 qCDebug(DolphinDebug).nospace() << "Failed to generate hover sequence preview #" << pixmaps.size() << " for file " << item.url().toString() << " (attempt "
665 << (m_hoverSequenceNumSuccessiveFailures + 1) << "/" << (numRetries + 1) << ")";
666
667 if (m_hoverSequenceNumSuccessiveFailures >= numRetries) {
668 // Give up and simply duplicate the previous sequence image (if any)
669
670 pixmaps.append(pixmaps.empty() ? QPixmap() : pixmaps.last());
671 data["hoverSequencePixmaps"] = QVariant::fromValue(pixmaps);
672
673 if (!data.contains("hoverSequenceWraparoundPoint")) {
674 // hoverSequenceWraparoundPoint is only available when PreviewJob succeeds, so unless
675 // it has previously succeeded, it's best to assume that it just doesn't handle
676 // sequences instead of trying to load the next image indefinitely.
677 data["hoverSequenceWraparoundPoint"] = 1.0f;
678 }
679
680 m_model->setData(index, data);
681
682 m_hoverSequenceNumSuccessiveFailures = 0;
683 } else {
684 // Retry
685
686 m_hoverSequenceNumSuccessiveFailures++;
687 }
688
689 // Next image in the sequence (or same one if the retry limit wasn't reached yet) will be
690 // loaded automatically, because slotHoverSequencePreviewJobFinished() will be triggered
691 // even when PreviewJob fails.
692 }
693
694 void KFileItemModelRolesUpdater::slotHoverSequencePreviewJobFinished()
695 {
696 const int index = m_model->index(m_hoverSequenceItem);
697 if (index < 0) {
698 m_hoverSequencePreviewJob = nullptr;
699 return;
700 }
701
702 // Since a PreviewJob can only have one associated sequence index, we can only generate
703 // one sequence image per job, so we have to start another one for the next index.
704
705 // Load the next image in the sequence
706 m_hoverSequencePreviewJob = nullptr;
707 loadNextHoverSequencePreview();
708 }
709
710 void KFileItemModelRolesUpdater::resolveNextSortRole()
711 {
712 if (m_state != ResolvingSortRole) {
713 return;
714 }
715
716 QSet<KFileItem>::iterator it = m_pendingSortRoleItems.begin();
717 while (it != m_pendingSortRoleItems.end()) {
718 const KFileItem item = *it;
719 const int index = m_model->index(item);
720
721 // Continue if the sort role has already been determined for the
722 // item, and the item has not been changed recently.
723 if (!m_changedItems.contains(item) && m_model->data(index).contains(m_model->sortRole())) {
724 it = m_pendingSortRoleItems.erase(it);
725 continue;
726 }
727
728 applySortRole(index);
729 m_pendingSortRoleItems.erase(it);
730 break;
731 }
732
733 if (!m_pendingSortRoleItems.isEmpty()) {
734 applySortProgressToModel();
735 QTimer::singleShot(0, this, &KFileItemModelRolesUpdater::resolveNextSortRole);
736 } else {
737 m_state = Idle;
738
739 // Prevent that we try to update the items twice.
740 disconnect(m_model, &KFileItemModel::itemsMoved, this, &KFileItemModelRolesUpdater::slotItemsMoved);
741 applySortProgressToModel();
742 connect(m_model, &KFileItemModel::itemsMoved, this, &KFileItemModelRolesUpdater::slotItemsMoved);
743 startUpdating();
744 }
745 }
746
747 void KFileItemModelRolesUpdater::resolveNextPendingRoles()
748 {
749 if (m_state != ResolvingAllRoles) {
750 return;
751 }
752
753 while (!m_pendingIndexes.isEmpty()) {
754 const int index = m_pendingIndexes.takeFirst();
755 const KFileItem item = m_model->fileItem(index);
756
757 if (m_finishedItems.contains(item)) {
758 continue;
759 }
760
761 applyResolvedRoles(index, ResolveAll);
762 m_finishedItems.insert(item);
763 m_changedItems.remove(item);
764 break;
765 }
766
767 if (!m_pendingIndexes.isEmpty()) {
768 QTimer::singleShot(0, this, &KFileItemModelRolesUpdater::resolveNextPendingRoles);
769 } else {
770 m_state = Idle;
771
772 if (m_clearPreviews) {
773 // Only go through the list if there are items which might still have previews.
774 if (m_finishedItems.count() != m_model->count()) {
775 QHash<QByteArray, QVariant> data;
776 data.insert("iconPixmap", QPixmap());
777 data.insert("hoverSequencePixmaps", QVariant::fromValue(QVector<QPixmap>()));
778
779 disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
780 for (int index = 0; index <= m_model->count(); ++index) {
781 if (m_model->data(index).contains("iconPixmap") || m_model->data(index).contains("hoverSequencePixmaps")) {
782 m_model->setData(index, data);
783 }
784 }
785 connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
786 }
787 m_clearPreviews = false;
788 }
789
790 if (!m_changedItems.isEmpty()) {
791 updateChangedItems();
792 }
793 }
794 }
795
796 void KFileItemModelRolesUpdater::resolveRecentlyChangedItems()
797 {
798 m_changedItems += m_recentlyChangedItems;
799 m_recentlyChangedItems.clear();
800 updateChangedItems();
801 }
802
803 void KFileItemModelRolesUpdater::applyChangedBalooRoles(const QString &file)
804 {
805 #if HAVE_BALOO
806 const KFileItem item = m_model->fileItem(QUrl::fromLocalFile(file));
807
808 if (item.isNull()) {
809 // itemUrl is not in the model anymore, probably because
810 // the corresponding file has been deleted in the meantime.
811 return;
812 }
813 applyChangedBalooRolesForItem(item);
814 #else
815 Q_UNUSED(file)
816 #endif
817 }
818
819 void KFileItemModelRolesUpdater::applyChangedBalooRolesForItem(const KFileItem &item)
820 {
821 #if HAVE_BALOO
822 Baloo::File file(item.localPath());
823 file.load();
824
825 const KBalooRolesProvider &rolesProvider = KBalooRolesProvider::instance();
826 QHash<QByteArray, QVariant> data;
827
828 const auto roles = rolesProvider.roles();
829 for (const QByteArray &role : roles) {
830 // Overwrite all the role values with an empty QVariant, because the roles
831 // provider doesn't overwrite it when the property value list is empty.
832 // See bug 322348
833 data.insert(role, QVariant());
834 }
835
836 QHashIterator<QByteArray, QVariant> it(rolesProvider.roleValues(file, m_roles));
837 while (it.hasNext()) {
838 it.next();
839 data.insert(it.key(), it.value());
840 }
841
842 disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
843 const int index = m_model->index(item);
844 m_model->setData(index, data);
845 connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
846 #else
847 #ifndef Q_CC_MSVC
848 Q_UNUSED(item)
849 #endif
850 #endif
851 }
852
853 void KFileItemModelRolesUpdater::slotDirectoryContentsCountReceived(const QString &path, int count, long long size)
854 {
855 const bool getIsExpandableRole = m_roles.contains("isExpandable");
856 const bool getSizeRole = m_roles.contains("size");
857
858 if (getSizeRole || getIsExpandableRole) {
859 const int index = m_model->index(QUrl::fromLocalFile(path));
860 if (index >= 0) {
861 QHash<QByteArray, QVariant> data;
862
863 if (getSizeRole) {
864 data.insert("count", count);
865 data.insert("size", QVariant::fromValue(size));
866 }
867 if (getIsExpandableRole) {
868 data.insert("isExpandable", count > 0);
869 }
870
871 disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
872 m_model->setData(index, data);
873 connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
874 }
875 }
876 }
877
878 void KFileItemModelRolesUpdater::startUpdating()
879 {
880 if (m_state == Paused) {
881 return;
882 }
883
884 if (m_finishedItems.count() == m_model->count()) {
885 // All roles have been resolved already.
886 m_state = Idle;
887 return;
888 }
889
890 // Terminate all updates that are currently active.
891 killPreviewJob();
892 m_pendingIndexes.clear();
893
894 QElapsedTimer timer;
895 timer.start();
896
897 // Determine the icons for the visible items synchronously.
898 updateVisibleIcons();
899
900 // A detailed update of the items in and near the visible area
901 // only makes sense if sorting is finished.
902 if (m_state == ResolvingSortRole) {
903 return;
904 }
905
906 // Start the preview job or the asynchronous resolving of all roles.
907 QList<int> indexes = indexesToResolve();
908
909 if (m_previewShown) {
910 m_pendingPreviewItems.clear();
911 m_pendingPreviewItems.reserve(indexes.count());
912
913 for (int index : std::as_const(indexes)) {
914 const KFileItem item = m_model->fileItem(index);
915 if (!m_finishedItems.contains(item)) {
916 m_pendingPreviewItems.append(item);
917 }
918 }
919
920 startPreviewJob();
921 } else {
922 m_pendingIndexes = indexes;
923 // Trigger the asynchronous resolving of all roles.
924 m_state = ResolvingAllRoles;
925 QTimer::singleShot(0, this, &KFileItemModelRolesUpdater::resolveNextPendingRoles);
926 }
927 }
928
929 void KFileItemModelRolesUpdater::updateVisibleIcons()
930 {
931 int lastVisibleIndex = m_lastVisibleIndex;
932 if (lastVisibleIndex <= 0) {
933 // Guess a reasonable value for the last visible index if the view
934 // has not told us about the real value yet.
935 lastVisibleIndex = qMin(m_firstVisibleIndex + m_maximumVisibleItems, m_model->count() - 1);
936 if (lastVisibleIndex <= 0) {
937 lastVisibleIndex = qMin(200, m_model->count() - 1);
938 }
939 }
940
941 QElapsedTimer timer;
942 timer.start();
943
944 // Try to determine the final icons for all visible items.
945 int index;
946 for (index = m_firstVisibleIndex; index <= lastVisibleIndex && timer.elapsed() < MaxBlockTimeout; ++index) {
947 applyResolvedRoles(index, ResolveFast);
948 }
949
950 // KFileItemListView::initializeItemListWidget(KItemListWidget*) will load
951 // preliminary icons (i.e., without mime type determination) for the
952 // remaining items.
953 }
954
955 void KFileItemModelRolesUpdater::startPreviewJob()
956 {
957 m_state = PreviewJobRunning;
958
959 if (m_pendingPreviewItems.isEmpty()) {
960 QTimer::singleShot(0, this, &KFileItemModelRolesUpdater::slotPreviewJobFinished);
961 return;
962 }
963
964 // PreviewJob internally caches items always with the size of
965 // 128 x 128 pixels or 256 x 256 pixels. A (slow) downscaling is done
966 // by PreviewJob if a smaller size is requested. For images KFileItemModelRolesUpdater must
967 // do a downscaling anyhow because of the frame, so in this case only the provided
968 // cache sizes are requested.
969 const QSize cacheSize = (m_iconSize.width() > 128) || (m_iconSize.height() > 128) ? QSize(256, 256) : QSize(128, 128);
970
971 // KIO::filePreview() will request the MIME-type of all passed items, which (in the
972 // worst case) might block the application for several seconds. To prevent such
973 // a blocking, we only pass items with known mime type to the preview job.
974 const int count = m_pendingPreviewItems.count();
975 KFileItemList itemSubSet;
976 itemSubSet.reserve(count);
977
978 if (m_pendingPreviewItems.first().isMimeTypeKnown()) {
979 // Some mime types are known already, probably because they were
980 // determined when loading the icons for the visible items. Start
981 // a preview job for all items at the beginning of the list which
982 // have a known mime type.
983 do {
984 itemSubSet.append(m_pendingPreviewItems.takeFirst());
985 } while (!m_pendingPreviewItems.isEmpty() && m_pendingPreviewItems.first().isMimeTypeKnown());
986 } else {
987 // Determine mime types for MaxBlockTimeout ms, and start a preview
988 // job for the corresponding items.
989 QElapsedTimer timer;
990 timer.start();
991
992 do {
993 const KFileItem item = m_pendingPreviewItems.takeFirst();
994 item.determineMimeType();
995 itemSubSet.append(item);
996 } while (!m_pendingPreviewItems.isEmpty() && timer.elapsed() < MaxBlockTimeout);
997 }
998
999 KIO::PreviewJob *job = new KIO::PreviewJob(itemSubSet, cacheSize, &m_enabledPlugins);
1000 job->setDevicePixelRatio(m_devicePixelRatio);
1001 job->setIgnoreMaximumSize(itemSubSet.first().isLocalFile() && !itemSubSet.first().isSlow() && m_localFileSizePreviewLimit <= 0);
1002 if (job->uiDelegate()) {
1003 KJobWidgets::setWindow(job, qApp->activeWindow());
1004 }
1005
1006 connect(job, &KIO::PreviewJob::gotPreview, this, &KFileItemModelRolesUpdater::slotGotPreview);
1007 connect(job, &KIO::PreviewJob::failed, this, &KFileItemModelRolesUpdater::slotPreviewFailed);
1008 connect(job, &KIO::PreviewJob::finished, this, &KFileItemModelRolesUpdater::slotPreviewJobFinished);
1009
1010 m_previewJob = job;
1011 }
1012
1013 QPixmap KFileItemModelRolesUpdater::transformPreviewPixmap(const QPixmap &pixmap, const QStringList &overlays)
1014 {
1015 QPixmap scaledPixmap = pixmap;
1016
1017 if (pixmap.isNull()) {
1018 return scaledPixmap;
1019 }
1020
1021 if (!pixmap.hasAlpha() && m_iconSize.width() > KIconLoader::SizeSmallMedium && m_iconSize.height() > KIconLoader::SizeSmallMedium) {
1022 if (m_enlargeSmallPreviews) {
1023 KPixmapModifier::applyFrame(scaledPixmap, m_iconSize);
1024 } else {
1025 // Assure that small previews don't get enlarged. Instead they
1026 // should be shown centered within the frame.
1027 const QSize contentSize = KPixmapModifier::sizeInsideFrame(m_iconSize);
1028 const bool enlargingRequired = scaledPixmap.width() < contentSize.width() && scaledPixmap.height() < contentSize.height();
1029 if (enlargingRequired) {
1030 QSize frameSize = scaledPixmap.size() / scaledPixmap.devicePixelRatio();
1031 frameSize.scale(m_iconSize, Qt::KeepAspectRatio);
1032
1033 QPixmap largeFrame(frameSize);
1034 largeFrame.fill(Qt::transparent);
1035
1036 KPixmapModifier::applyFrame(largeFrame, frameSize);
1037
1038 QPainter painter(&largeFrame);
1039 painter.drawPixmap((largeFrame.width() - scaledPixmap.width() / scaledPixmap.devicePixelRatio()) / 2,
1040 (largeFrame.height() - scaledPixmap.height() / scaledPixmap.devicePixelRatio()) / 2,
1041 scaledPixmap);
1042 scaledPixmap = largeFrame;
1043 } else {
1044 // The image must be shrunk as it is too large to fit into
1045 // the available icon size
1046 KPixmapModifier::applyFrame(scaledPixmap, m_iconSize);
1047 }
1048 }
1049 } else {
1050 KPixmapModifier::scale(scaledPixmap, m_iconSize * m_devicePixelRatio);
1051 scaledPixmap.setDevicePixelRatio(m_devicePixelRatio);
1052 }
1053
1054 if (!overlays.isEmpty()) {
1055 const QSize cacheSize = (m_iconSize.width() > 128) || (m_iconSize.height() > 128) ? QSize(256, 256) : QSize(128, 128);
1056 scaledPixmap = KIconUtils::addOverlays(scaledPixmap, overlays).pixmap(cacheSize, m_devicePixelRatio);
1057 }
1058
1059 return scaledPixmap;
1060 }
1061
1062 void KFileItemModelRolesUpdater::loadNextHoverSequencePreview()
1063 {
1064 if (m_hoverSequenceItem.isNull() || m_hoverSequencePreviewJob) {
1065 return;
1066 }
1067
1068 const int index = m_model->index(m_hoverSequenceItem);
1069 if (index < 0) {
1070 return;
1071 }
1072
1073 // We generate the next few sequence indices in advance (buffering)
1074 const int maxSeqIdx = m_hoverSequenceIndex + 5;
1075
1076 QHash<QByteArray, QVariant> data = m_model->data(index);
1077
1078 if (!data.contains("hoverSequencePixmaps")) {
1079 // The pixmap at index 0 isn't used ("iconPixmap" will be used instead)
1080 data.insert("hoverSequencePixmaps", QVariant::fromValue(QVector<QPixmap>() << QPixmap()));
1081 m_model->setData(index, data);
1082 }
1083
1084 const QVector<QPixmap> pixmaps = data["hoverSequencePixmaps"].value<QVector<QPixmap>>();
1085
1086 const int loadSeqIdx = pixmaps.size();
1087
1088 float wap = -1.0f;
1089 if (data.contains("hoverSequenceWraparoundPoint")) {
1090 wap = data["hoverSequenceWraparoundPoint"].toFloat();
1091 }
1092 if (wap >= 1.0f && loadSeqIdx >= static_cast<int>(wap)) {
1093 // Reached the wraparound point -> no more previews to load.
1094 return;
1095 }
1096
1097 if (loadSeqIdx > maxSeqIdx) {
1098 // Wait until setHoverSequenceState() is called with a higher sequence index.
1099 return;
1100 }
1101
1102 // PreviewJob internally caches items always with the size of
1103 // 128 x 128 pixels or 256 x 256 pixels. A (slow) downscaling is done
1104 // by PreviewJob if a smaller size is requested. For images KFileItemModelRolesUpdater must
1105 // do a downscaling anyhow because of the frame, so in this case only the provided
1106 // cache sizes are requested.
1107 const QSize cacheSize = (m_iconSize.width() > 128) || (m_iconSize.height() > 128) ? QSize(256, 256) : QSize(128, 128);
1108
1109 KIO::PreviewJob *job = new KIO::PreviewJob({m_hoverSequenceItem}, cacheSize, &m_enabledPlugins);
1110
1111 job->setSequenceIndex(loadSeqIdx);
1112 job->setIgnoreMaximumSize(m_hoverSequenceItem.isLocalFile() && !m_hoverSequenceItem.isSlow() && m_localFileSizePreviewLimit <= 0);
1113 if (job->uiDelegate()) {
1114 KJobWidgets::setWindow(job, qApp->activeWindow());
1115 }
1116
1117 connect(job, &KIO::PreviewJob::gotPreview, this, &KFileItemModelRolesUpdater::slotHoverSequenceGotPreview);
1118 connect(job, &KIO::PreviewJob::failed, this, &KFileItemModelRolesUpdater::slotHoverSequencePreviewFailed);
1119 connect(job, &KIO::PreviewJob::finished, this, &KFileItemModelRolesUpdater::slotHoverSequencePreviewJobFinished);
1120
1121 m_hoverSequencePreviewJob = job;
1122 }
1123
1124 void KFileItemModelRolesUpdater::killHoverSequencePreviewJob()
1125 {
1126 if (m_hoverSequencePreviewJob) {
1127 disconnect(m_hoverSequencePreviewJob, &KIO::PreviewJob::gotPreview, this, &KFileItemModelRolesUpdater::slotHoverSequenceGotPreview);
1128 disconnect(m_hoverSequencePreviewJob, &KIO::PreviewJob::failed, this, &KFileItemModelRolesUpdater::slotHoverSequencePreviewFailed);
1129 disconnect(m_hoverSequencePreviewJob, &KIO::PreviewJob::finished, this, &KFileItemModelRolesUpdater::slotHoverSequencePreviewJobFinished);
1130 m_hoverSequencePreviewJob->kill();
1131 m_hoverSequencePreviewJob = nullptr;
1132 }
1133 }
1134
1135 void KFileItemModelRolesUpdater::updateChangedItems()
1136 {
1137 if (m_state == Paused) {
1138 return;
1139 }
1140
1141 if (m_changedItems.isEmpty()) {
1142 return;
1143 }
1144
1145 m_finishedItems -= m_changedItems;
1146
1147 if (m_resolvableRoles.contains(m_model->sortRole())) {
1148 m_pendingSortRoleItems += m_changedItems;
1149
1150 if (m_state != ResolvingSortRole) {
1151 // Stop the preview job if necessary, and trigger the
1152 // asynchronous determination of the sort role.
1153 killPreviewJob();
1154 m_state = ResolvingSortRole;
1155 QTimer::singleShot(0, this, &KFileItemModelRolesUpdater::resolveNextSortRole);
1156 }
1157
1158 return;
1159 }
1160
1161 QList<int> visibleChangedIndexes;
1162 QList<int> invisibleChangedIndexes;
1163 visibleChangedIndexes.reserve(m_changedItems.size());
1164 invisibleChangedIndexes.reserve(m_changedItems.size());
1165
1166 auto changedItemsIt = m_changedItems.begin();
1167 while (changedItemsIt != m_changedItems.end()) {
1168 const auto &item = *changedItemsIt;
1169 const int index = m_model->index(item);
1170
1171 if (index < 0) {
1172 changedItemsIt = m_changedItems.erase(changedItemsIt);
1173 continue;
1174 }
1175 ++changedItemsIt;
1176
1177 if (index >= m_firstVisibleIndex && index <= m_lastVisibleIndex) {
1178 visibleChangedIndexes.append(index);
1179 } else {
1180 invisibleChangedIndexes.append(index);
1181 }
1182 }
1183
1184 std::sort(visibleChangedIndexes.begin(), visibleChangedIndexes.end());
1185
1186 if (m_previewShown) {
1187 for (int index : std::as_const(visibleChangedIndexes)) {
1188 m_pendingPreviewItems.append(m_model->fileItem(index));
1189 }
1190
1191 for (int index : std::as_const(invisibleChangedIndexes)) {
1192 m_pendingPreviewItems.append(m_model->fileItem(index));
1193 }
1194
1195 if (!m_previewJob) {
1196 startPreviewJob();
1197 }
1198 } else {
1199 const bool resolvingInProgress = !m_pendingIndexes.isEmpty();
1200 m_pendingIndexes = visibleChangedIndexes + m_pendingIndexes + invisibleChangedIndexes;
1201 if (!resolvingInProgress) {
1202 // Trigger the asynchronous resolving of the changed roles.
1203 m_state = ResolvingAllRoles;
1204 QTimer::singleShot(0, this, &KFileItemModelRolesUpdater::resolveNextPendingRoles);
1205 }
1206 }
1207 }
1208
1209 void KFileItemModelRolesUpdater::applySortRole(int index)
1210 {
1211 QHash<QByteArray, QVariant> data;
1212 const KFileItem item = m_model->fileItem(index);
1213
1214 if (m_model->sortRole() == "type") {
1215 if (!item.isMimeTypeKnown()) {
1216 item.determineMimeType();
1217 }
1218
1219 data.insert("type", item.mimeComment());
1220 } else if (m_model->sortRole() == "size" && item.isLocalFile() && item.isDir()) {
1221 startDirectorySizeCounting(item, index);
1222 return;
1223 } else {
1224 // Probably the sort role is a baloo role - just determine all roles.
1225 data = rolesData(item, index);
1226 }
1227
1228 disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
1229 m_model->setData(index, data);
1230 connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
1231 }
1232
1233 void KFileItemModelRolesUpdater::applySortProgressToModel()
1234 {
1235 // Inform the model about the progress of the resolved items,
1236 // so that it can give an indication when the sorting has been finished.
1237 const int resolvedCount = m_model->count() - m_pendingSortRoleItems.count();
1238 m_model->emitSortProgress(resolvedCount);
1239 }
1240
1241 bool KFileItemModelRolesUpdater::applyResolvedRoles(int index, ResolveHint hint)
1242 {
1243 const KFileItem item = m_model->fileItem(index);
1244 const bool resolveAll = (hint == ResolveAll);
1245
1246 bool iconChanged = false;
1247 if (!item.isMimeTypeKnown() || !item.isFinalIconKnown()) {
1248 item.determineMimeType();
1249 iconChanged = true;
1250 } else if (!m_model->data(index).contains("iconName")) {
1251 iconChanged = true;
1252 }
1253
1254 if (iconChanged || resolveAll || m_clearPreviews) {
1255 if (index < 0) {
1256 return false;
1257 }
1258
1259 QHash<QByteArray, QVariant> data;
1260 if (resolveAll) {
1261 data = rolesData(item, index);
1262 }
1263
1264 if (!item.iconName().isEmpty()) {
1265 data.insert("iconName", item.iconName());
1266 }
1267
1268 if (m_clearPreviews) {
1269 data.insert("iconPixmap", QPixmap());
1270 data.insert("hoverSequencePixmaps", QVariant::fromValue(QVector<QPixmap>()));
1271 }
1272
1273 disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
1274 m_model->setData(index, data);
1275 connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
1276 return true;
1277 }
1278
1279 return false;
1280 }
1281
1282 void KFileItemModelRolesUpdater::startDirectorySizeCounting(const KFileItem &item, int index)
1283 {
1284 if (ContentDisplaySettings::directorySizeMode() == ContentDisplaySettings::EnumDirectorySizeMode::None || !item.isLocalFile()) {
1285 return;
1286 }
1287
1288 if (ContentDisplaySettings::directorySizeMode() == ContentDisplaySettings::EnumDirectorySizeMode::ContentCount || item.isSlow()) {
1289 // fastpath no recursion necessary
1290
1291 auto data = m_model->data(index);
1292 if (data.value("size") == -2) {
1293 // means job already started
1294 return;
1295 }
1296
1297 auto url = item.url();
1298 if (!item.localPath().isEmpty()) {
1299 // optimization for desktop:/, trash:/
1300 url = QUrl::fromLocalFile(item.localPath());
1301 }
1302
1303 data.insert("size", -2); // invalid size, -1 means size unknown
1304
1305 disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
1306 m_model->setData(index, data);
1307 connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
1308
1309 auto listJob = KIO::listDir(url, KIO::HideProgressInfo);
1310 QObject::connect(listJob, &KIO::ListJob::entries, this, [this, item](const KJob * /*job*/, const KIO::UDSEntryList &list) {
1311 int index = m_model->index(item);
1312 if (index < 0) {
1313 return;
1314 }
1315 auto data = m_model->data(index);
1316 int origCount = data.value("count").toInt();
1317 int entryCount = origCount;
1318
1319 for (const KIO::UDSEntry &entry : list) {
1320 const auto name = entry.stringValue(KIO::UDSEntry::UDS_NAME);
1321
1322 if (name == QStringLiteral("..") || name == QStringLiteral(".")) {
1323 continue;
1324 }
1325 if (!m_model->showHiddenFiles() && name.startsWith(QLatin1Char('.'))) {
1326 continue;
1327 }
1328 if (m_model->showDirectoriesOnly() && !entry.isDir()) {
1329 continue;
1330 }
1331 ++entryCount;
1332 }
1333
1334 QHash<QByteArray, QVariant> newData;
1335 QVariant expandable = data.value("isExpandable");
1336 if (expandable.isNull() || expandable.toBool() != (entryCount > 0)) {
1337 // if expandable has changed
1338 newData.insert("isExpandable", entryCount > 0);
1339 }
1340
1341 if (origCount != entryCount) {
1342 // count has changed
1343 newData.insert("count", entryCount);
1344 }
1345
1346 if (!newData.isEmpty()) {
1347 disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
1348 m_model->setData(index, newData);
1349 connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
1350 }
1351 });
1352 return;
1353 }
1354
1355 // Tell m_directoryContentsCounter that we want to count the items
1356 // inside the directory. The result will be received in slotDirectoryContentsCountReceived.
1357 const QString path = item.localPath();
1358 const auto priority = index >= m_firstVisibleIndex && index <= m_lastVisibleIndex ? KDirectoryContentsCounter::PathCountPriority::High
1359 : KDirectoryContentsCounter::PathCountPriority::Normal;
1360
1361 m_directoryContentsCounter->scanDirectory(path, priority);
1362 }
1363
1364 QHash<QByteArray, QVariant> KFileItemModelRolesUpdater::rolesData(const KFileItem &item, int index)
1365 {
1366 QHash<QByteArray, QVariant> data;
1367
1368 const bool getSizeRole = m_roles.contains("size");
1369 const bool getIsExpandableRole = m_roles.contains("isExpandable");
1370
1371 if ((getSizeRole || getIsExpandableRole) && item.isDir()) {
1372 startDirectorySizeCounting(item, index);
1373 }
1374
1375 if (m_roles.contains("extension")) {
1376 // TODO KF6 use KFileItem::suffix 464722
1377 data.insert("extension", QFileInfo(item.name()).suffix());
1378 }
1379
1380 if (m_roles.contains("type")) {
1381 data.insert("type", item.mimeComment());
1382 }
1383
1384 QStringList overlays = item.overlays();
1385 for (KOverlayIconPlugin *it : std::as_const(m_overlayIconsPlugin)) {
1386 overlays.append(it->getOverlays(item.url()));
1387 }
1388 if (!overlays.isEmpty()) {
1389 data.insert("iconOverlays", overlays);
1390 }
1391
1392 #if HAVE_BALOO
1393 if (m_balooFileMonitor) {
1394 m_balooFileMonitor->addFile(item.localPath());
1395 applyChangedBalooRolesForItem(item);
1396 }
1397 #endif
1398 return data;
1399 }
1400
1401 void KFileItemModelRolesUpdater::slotOverlaysChanged(const QUrl &url, const QStringList &)
1402 {
1403 const KFileItem item = m_model->fileItem(url);
1404 if (item.isNull()) {
1405 return;
1406 }
1407 const int index = m_model->index(item);
1408 QHash<QByteArray, QVariant> data = m_model->data(index);
1409 QStringList overlays = item.overlays();
1410 for (KOverlayIconPlugin *it : std::as_const(m_overlayIconsPlugin)) {
1411 overlays.append(it->getOverlays(url));
1412 }
1413 data.insert("iconOverlays", overlays);
1414 m_model->setData(index, data);
1415 }
1416
1417 void KFileItemModelRolesUpdater::updateAllPreviews()
1418 {
1419 if (m_state == Paused) {
1420 m_previewChangedDuringPausing = true;
1421 } else {
1422 m_finishedItems.clear();
1423 startUpdating();
1424 }
1425 }
1426
1427 void KFileItemModelRolesUpdater::killPreviewJob()
1428 {
1429 if (m_previewJob) {
1430 disconnect(m_previewJob, &KIO::PreviewJob::gotPreview, this, &KFileItemModelRolesUpdater::slotGotPreview);
1431 disconnect(m_previewJob, &KIO::PreviewJob::failed, this, &KFileItemModelRolesUpdater::slotPreviewFailed);
1432 disconnect(m_previewJob, &KIO::PreviewJob::finished, this, &KFileItemModelRolesUpdater::slotPreviewJobFinished);
1433 m_previewJob->kill();
1434 m_previewJob = nullptr;
1435 m_pendingPreviewItems.clear();
1436 }
1437 }
1438
1439 QList<int> KFileItemModelRolesUpdater::indexesToResolve() const
1440 {
1441 const int count = m_model->count();
1442
1443 QList<int> result;
1444 result.reserve(qMin(count, (m_lastVisibleIndex - m_firstVisibleIndex + 1) + ResolveAllItemsLimit + (2 * m_maximumVisibleItems)));
1445
1446 // Add visible items.
1447 // Resolve files first, their previews are quicker.
1448 QList<int> visibleDirs;
1449 for (int i = m_firstVisibleIndex; i <= m_lastVisibleIndex; ++i) {
1450 const KFileItem item = m_model->fileItem(i);
1451 if (item.isDir()) {
1452 visibleDirs.append(i);
1453 } else {
1454 result.append(i);
1455 }
1456 }
1457
1458 result.append(visibleDirs);
1459
1460 // We need a reasonable upper limit for number of items to resolve after
1461 // and before the visible range. m_maximumVisibleItems can be quite large
1462 // when using Compact View.
1463 const int readAheadItems = qMin(ReadAheadPages * m_maximumVisibleItems, ResolveAllItemsLimit / 2);
1464
1465 // Add items after the visible range.
1466 const int endExtendedVisibleRange = qMin(m_lastVisibleIndex + readAheadItems, count - 1);
1467 for (int i = m_lastVisibleIndex + 1; i <= endExtendedVisibleRange; ++i) {
1468 result.append(i);
1469 }
1470
1471 // Add items before the visible range in reverse order.
1472 const int beginExtendedVisibleRange = qMax(0, m_firstVisibleIndex - readAheadItems);
1473 for (int i = m_firstVisibleIndex - 1; i >= beginExtendedVisibleRange; --i) {
1474 result.append(i);
1475 }
1476
1477 // Add items on the last page.
1478 const int beginLastPage = qMax(endExtendedVisibleRange + 1, count - m_maximumVisibleItems);
1479 for (int i = beginLastPage; i < count; ++i) {
1480 result.append(i);
1481 }
1482
1483 // Add items on the first page.
1484 const int endFirstPage = qMin(beginExtendedVisibleRange, m_maximumVisibleItems);
1485 for (int i = 0; i < endFirstPage; ++i) {
1486 result.append(i);
1487 }
1488
1489 // Continue adding items until ResolveAllItemsLimit is reached.
1490 int remainingItems = ResolveAllItemsLimit - result.count();
1491
1492 for (int i = endExtendedVisibleRange + 1; i < beginLastPage && remainingItems > 0; ++i) {
1493 result.append(i);
1494 --remainingItems;
1495 }
1496
1497 for (int i = beginExtendedVisibleRange - 1; i >= endFirstPage && remainingItems > 0; --i) {
1498 result.append(i);
1499 --remainingItems;
1500 }
1501
1502 return result;
1503 }
1504
1505 void KFileItemModelRolesUpdater::trimHoverSequenceLoadedItems()
1506 {
1507 static const size_t maxLoadedItems = 20;
1508
1509 size_t loadedItems = m_hoverSequenceLoadedItems.size();
1510 while (loadedItems > maxLoadedItems) {
1511 const KFileItem item = m_hoverSequenceLoadedItems.front();
1512
1513 m_hoverSequenceLoadedItems.pop_front();
1514 loadedItems--;
1515
1516 const int index = m_model->index(item);
1517 if (index >= 0) {
1518 QHash<QByteArray, QVariant> data = m_model->data(index);
1519 data["hoverSequencePixmaps"] = QVariant::fromValue(QVector<QPixmap>() << QPixmap());
1520 m_model->setData(index, data);
1521 }
1522 }
1523 }
1524
1525 #include "moc_kfileitemmodelrolesupdater.cpp"