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