1/*
2 SPDX-FileCopyrightText: 2008-2009 Peter Penz <peter.penz@gmx.at>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5*/
6
7#include "kfilepreviewgenerator.h"
8
9#include "defaultviewadapter_p.h"
10#include <KConfigGroup>
11#include <KIconEffect>
12#include <KIconLoader>
13#include <KSharedConfig>
14#include <KUrlMimeData>
15#include <imagefilter_p.h> // from kiowidgets
16#include <kdirlister.h>
17#include <kdirmodel.h>
18#include <kfileitem.h>
19#include <kio/paste.h>
20#include <kio/previewjob.h>
21
22#include <QAbstractItemView>
23#include <QAbstractProxyModel>
24#include <QApplication>
25#include <QClipboard>
26#include <QHash>
27#include <QIcon>
28#include <QList>
29#include <QListView>
30#include <QMimeData>
31#include <QPainter>
32#include <QPixmap>
33#include <QPointer>
34#include <QTimer>
35
36class KFilePreviewGeneratorPrivate
37{
38 class TileSet;
39 class LayoutBlocker;
40
41public:
42 KFilePreviewGeneratorPrivate(KFilePreviewGenerator *qq, KAbstractViewAdapter *viewAdapter, QAbstractItemModel *model);
43
44 ~KFilePreviewGeneratorPrivate();
45 /**
46 * Requests a new icon for the item \a index.
47 * @param sequenceIndex If this is zero, the standard icon is requested, else another one.
48 */
49 void requestSequenceIcon(const QModelIndex &index, int sequenceIndex);
50
51 /**
52 * Generates previews for the items \a items asynchronously.
53 */
54 void updateIcons(const KFileItemList &items);
55
56 /**
57 * Generates previews for the indices within \a topLeft
58 * and \a bottomRight asynchronously.
59 */
60 void updateIcons(const QModelIndex &topLeft, const QModelIndex &bottomRight);
61
62 /**
63 * Adds the preview \a pixmap for the item \a item to the preview
64 * queue and starts a timer which will dispatch the preview queue
65 * later.
66 */
67 void addToPreviewQueue(const KFileItem &item, const QPixmap &pixmap, KIO::PreviewJob *job);
68
69 /**
70 * Is invoked when the preview job has been finished and
71 * removes the job from the m_previewJobs list.
72 */
73 void slotPreviewJobFinished(KJob *job);
74
75 /** Synchronizes the icon of all items with the clipboard of cut items. */
76 void updateCutItems();
77
78 /**
79 * Reset all icons of the items from m_cutItemsCache and clear
80 * the cache.
81 */
82 void clearCutItemsCache();
83
84 /**
85 * Dispatches the preview queue block by block within
86 * time slices.
87 */
88 void dispatchIconUpdateQueue();
89
90 /**
91 * Pauses all icon updates and invokes KFilePreviewGenerator::resumeIconUpdates()
92 * after a short delay. Is invoked as soon as the user has moved
93 * a scrollbar.
94 */
95 void pauseIconUpdates();
96
97 /**
98 * Resumes the icons updates that have been paused after moving the
99 * scrollbar. The previews for the current visible area are
100 * generated first.
101 */
102 void resumeIconUpdates();
103
104 /**
105 * Starts the resolving of the MIME types from
106 * the m_pendingItems queue.
107 */
108 void startMimeTypeResolving();
109
110 /**
111 * Resolves the MIME type for exactly one item of the
112 * m_pendingItems queue.
113 */
114 void resolveMimeType();
115
116 /**
117 * Returns true, if the item \a item has been cut into
118 * the clipboard.
119 */
120 bool isCutItem(const KFileItem &item) const;
121
122 /**
123 * Applies a cut-item effect to all given \a items, if they
124 * are marked as cut in the clipboard.
125 */
126 void applyCutItemEffect(const KFileItemList &items);
127
128 /**
129 * Applies a frame around the icon. False is returned if
130 * no frame has been added because the icon is too small.
131 */
132 bool applyImageFrame(QPixmap &icon);
133
134 /**
135 * Resizes the icon to \a maxSize if the icon size does not
136 * fit into the maximum size. The aspect ratio of the icon
137 * is kept.
138 */
139 void limitToSize(QPixmap &icon, const QSize &maxSize);
140
141 /**
142 * Creates previews by starting new preview jobs for the items
143 * and triggers the preview timer.
144 */
145 void createPreviews(const KFileItemList &items);
146
147 /**
148 * Helper method for createPreviews(): Starts a preview job for the given
149 * items. For each returned preview addToPreviewQueue() will get invoked.
150 */
151 void startPreviewJob(const KFileItemList &items, int width, int height);
152
153 /** Kills all ongoing preview jobs. */
154 void killPreviewJobs();
155
156 /**
157 * Orders the items \a items in a way that the visible items
158 * are moved to the front of the list. When passing this
159 * list to a preview job, the visible items will get generated
160 * first.
161 */
162 void orderItems(KFileItemList &items);
163
164 /**
165 * Helper method for KFilePreviewGenerator::updateIcons(). Adds
166 * recursively all items from the model to the list \a list.
167 */
168 void addItemsToList(const QModelIndex &index, KFileItemList &list);
169
170 /**
171 * Updates the icons of files that are constantly changed due to a copy
172 * operation. See m_changedItems and m_changedItemsTimer for details.
173 */
174 void delayedIconUpdate();
175
176 /**
177 * Any items that are removed from the model are also removed from m_changedItems.
178 */
179 void rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end);
180
181 /** Remembers the pixmap for an item specified by an URL. */
182 struct ItemInfo {
183 QUrl url;
184 QPixmap pixmap;
185 };
186
187 /**
188 * During the lifetime of a DataChangeObtainer instance changing
189 * the data of the model won't trigger generating a preview.
190 */
191 class DataChangeObtainer
192 {
193 public:
194 explicit DataChangeObtainer(KFilePreviewGeneratorPrivate *generator)
195 : m_gen(generator)
196 {
197 ++m_gen->m_internalDataChange;
198 }
199
200 ~DataChangeObtainer()
201 {
202 --m_gen->m_internalDataChange;
203 }
204
205 private:
206 KFilePreviewGeneratorPrivate *m_gen;
207 };
208
209 KFilePreviewGenerator *const q;
210
211 bool m_previewShown = true;
212
213 /**
214 * True, if m_pendingItems and m_dispatchedItems should be
215 * cleared when the preview jobs have been finished.
216 */
217 bool m_clearItemQueues = true;
218
219 /**
220 * True if a selection has been done which should cut items.
221 */
222 bool m_hasCutSelection = false;
223
224 /**
225 * True if the updates of icons has been paused by pauseIconUpdates().
226 * The value is reset by resumeIconUpdates().
227 */
228 bool m_iconUpdatesPaused = false;
229
230 /**
231 * If the value is 0, the slot
232 * updateIcons(const QModelIndex&, const QModelIndex&) has
233 * been triggered by an external data change.
234 */
235 int m_internalDataChange = 0;
236
237 int m_pendingVisibleIconUpdates = 0;
238
239 KAbstractViewAdapter *m_viewAdapter = nullptr;
240 QAbstractItemView *m_itemView = nullptr;
241 QTimer *m_iconUpdateTimer = nullptr;
242 QTimer *m_scrollAreaTimer = nullptr;
243 QList<KJob *> m_previewJobs;
244 QPointer<KDirModel> m_dirModel;
245 QAbstractProxyModel *m_proxyModel = nullptr;
246
247 /**
248 * Set of all items that already have the 'cut' effect applied, together with the pixmap it was applied to
249 * This is used to make sure that the 'cut' effect is applied max. once for each pixmap
250 *
251 * Referencing the pixmaps here imposes no overhead, as they were also given to KDirModel::setData(),
252 * and thus are held anyway.
253 */
254 QHash<QUrl, QPixmap> m_cutItemsCache;
255 QList<ItemInfo> m_previews;
256 QMap<QUrl, int> m_sequenceIndices;
257
258 /**
259 * When huge items are copied, it must be prevented that a preview gets generated
260 * for each item size change. m_changedItems keeps track of the changed items and it
261 * is assured that a final preview is only done if an item does not change within
262 * at least 5 seconds.
263 */
264 QHash<QUrl, bool> m_changedItems;
265 QTimer *m_changedItemsTimer = nullptr;
266
267 /**
268 * Contains all items where a preview must be generated, but
269 * where the preview job has not dispatched the items yet.
270 */
271 KFileItemList m_pendingItems;
272
273 /**
274 * Contains all items, where a preview has already been
275 * generated by the preview jobs.
276 */
277 KFileItemList m_dispatchedItems;
278
279 KFileItemList m_resolvedMimeTypes;
280
281 QStringList m_enabledPlugins;
282
283 std::unique_ptr<TileSet> m_tileSet;
284};
285
286/**
287 * If the passed item view is an instance of QListView, expensive
288 * layout operations are blocked in the constructor and are unblocked
289 * again in the destructor.
290 *
291 * This helper class is a workaround for the following huge performance
292 * problem when having directories with several 1000 items:
293 * - each change of an icon emits a dataChanged() signal from the model
294 * - QListView iterates through all items on each dataChanged() signal
295 * and invokes QItemDelegate::sizeHint()
296 * - the sizeHint() implementation of KFileItemDelegate is quite complex,
297 * invoking it 1000 times for each icon change might block the UI
298 *
299 * QListView does not invoke QItemDelegate::sizeHint() when the
300 * uniformItemSize property has been set to true, so this property is
301 * set before exchanging a block of icons.
302 */
303class KFilePreviewGeneratorPrivate::LayoutBlocker
304{
305public:
306 explicit LayoutBlocker(QAbstractItemView *view)
307 : m_uniformSizes(false)
308 , m_view(qobject_cast<QListView *>(object: view))
309 {
310 if (m_view) {
311 m_uniformSizes = m_view->uniformItemSizes();
312 m_view->setUniformItemSizes(true);
313 }
314 }
315
316 ~LayoutBlocker()
317 {
318 if (m_view) {
319 m_view->setUniformItemSizes(m_uniformSizes);
320 /* The QListView did the layout with uniform item
321 * sizes, so trigger a relayout with the expected sizes. */
322 if (!m_uniformSizes) {
323 m_view->setGridSize(m_view->gridSize());
324 }
325 }
326 }
327
328private:
329 bool m_uniformSizes = false;
330 QListView *m_view = nullptr;
331};
332
333/** Helper class for drawing frames for image previews. */
334class KFilePreviewGeneratorPrivate::TileSet
335{
336public:
337 enum { LeftMargin = 3, TopMargin = 2, RightMargin = 3, BottomMargin = 4 };
338
339 enum Tile {
340 TopLeftCorner = 0,
341 TopSide,
342 TopRightCorner,
343 LeftSide,
344 RightSide,
345 BottomLeftCorner,
346 BottomSide,
347 BottomRightCorner,
348 NumTiles,
349 };
350
351 explicit TileSet()
352 {
353 QImage image(8 * 3, 8 * 3, QImage::Format_ARGB32_Premultiplied);
354
355 QPainter p(&image);
356 p.setCompositionMode(QPainter::CompositionMode_Source);
357 p.fillRect(r: image.rect(), c: Qt::transparent);
358 p.fillRect(r: image.rect().adjusted(xp1: 3, yp1: 3, xp2: -3, yp2: -3), c: Qt::black);
359 p.end();
360
361 KIO::ImageFilter::shadowBlur(image, radius: 3, color: Qt::black);
362
363 QPixmap pixmap = QPixmap::fromImage(image);
364 m_tiles[TopLeftCorner] = pixmap.copy(ax: 0, ay: 0, awidth: 8, aheight: 8);
365 m_tiles[TopSide] = pixmap.copy(ax: 8, ay: 0, awidth: 8, aheight: 8);
366 m_tiles[TopRightCorner] = pixmap.copy(ax: 16, ay: 0, awidth: 8, aheight: 8);
367 m_tiles[LeftSide] = pixmap.copy(ax: 0, ay: 8, awidth: 8, aheight: 8);
368 m_tiles[RightSide] = pixmap.copy(ax: 16, ay: 8, awidth: 8, aheight: 8);
369 m_tiles[BottomLeftCorner] = pixmap.copy(ax: 0, ay: 16, awidth: 8, aheight: 8);
370 m_tiles[BottomSide] = pixmap.copy(ax: 8, ay: 16, awidth: 8, aheight: 8);
371 m_tiles[BottomRightCorner] = pixmap.copy(ax: 16, ay: 16, awidth: 8, aheight: 8);
372 }
373
374 void paint(QPainter *p, const QRect &r)
375 {
376 p->drawPixmap(p: r.topLeft(), pm: m_tiles[TopLeftCorner]);
377 if (r.width() - 16 > 0) {
378 p->drawTiledPixmap(x: r.x() + 8, y: r.y(), w: r.width() - 16, h: 8, pm: m_tiles[TopSide]);
379 }
380 p->drawPixmap(x: r.right() - 8 + 1, y: r.y(), pm: m_tiles[TopRightCorner]);
381 if (r.height() - 16 > 0) {
382 p->drawTiledPixmap(x: r.x(), y: r.y() + 8, w: 8, h: r.height() - 16, pm: m_tiles[LeftSide]);
383 p->drawTiledPixmap(x: r.right() - 8 + 1, y: r.y() + 8, w: 8, h: r.height() - 16, pm: m_tiles[RightSide]);
384 }
385 p->drawPixmap(x: r.x(), y: r.bottom() - 8 + 1, pm: m_tiles[BottomLeftCorner]);
386 if (r.width() - 16 > 0) {
387 p->drawTiledPixmap(x: r.x() + 8, y: r.bottom() - 8 + 1, w: r.width() - 16, h: 8, pm: m_tiles[BottomSide]);
388 }
389 p->drawPixmap(x: r.right() - 8 + 1, y: r.bottom() - 8 + 1, pm: m_tiles[BottomRightCorner]);
390
391 const QRect contentRect = r.adjusted(xp1: LeftMargin + 1, yp1: TopMargin + 1, xp2: -(RightMargin + 1), yp2: -(BottomMargin + 1));
392 p->fillRect(r: contentRect, c: Qt::transparent);
393 }
394
395private:
396 QPixmap m_tiles[NumTiles];
397};
398
399KFilePreviewGeneratorPrivate::KFilePreviewGeneratorPrivate(KFilePreviewGenerator *qq, KAbstractViewAdapter *viewAdapter, QAbstractItemModel *model)
400 : q(qq)
401 , m_viewAdapter(viewAdapter)
402{
403 if (!m_viewAdapter->iconSize().isValid()) {
404 m_previewShown = false;
405 }
406
407 m_proxyModel = qobject_cast<QAbstractProxyModel *>(object: model);
408 m_dirModel = (m_proxyModel == nullptr) ? qobject_cast<KDirModel *>(object: model) : qobject_cast<KDirModel *>(object: m_proxyModel->sourceModel());
409 if (!m_dirModel) {
410 // previews can only get generated for directory models
411 m_previewShown = false;
412 } else {
413 KDirModel *dirModel = m_dirModel.data();
414 q->connect(sender: dirModel->dirLister(), signal: &KCoreDirLister::newItems, context: q, slot: [this](const KFileItemList &items) {
415 updateIcons(items);
416 });
417
418 q->connect(sender: dirModel, signal: &KDirModel::dataChanged, context: q, slot: [this](const QModelIndex &topLeft, const QModelIndex &bottomRight) {
419 updateIcons(topLeft, bottomRight);
420 });
421
422 q->connect(sender: dirModel, signal: &KDirModel::needSequenceIcon, context: q, slot: [this](const QModelIndex &index, int sequenceIndex) {
423 requestSequenceIcon(index, sequenceIndex);
424 });
425
426 q->connect(sender: dirModel, signal: &KDirModel::rowsAboutToBeRemoved, context: q, slot: [this](const QModelIndex &parent, int first, int last) {
427 rowsAboutToBeRemoved(parent, start: first, end: last);
428 });
429 }
430
431 QClipboard *clipboard = QApplication::clipboard();
432 q->connect(sender: clipboard, signal: &QClipboard::dataChanged, context: q, slot: [this]() {
433 updateCutItems();
434 });
435
436 m_iconUpdateTimer = new QTimer(q);
437 m_iconUpdateTimer->setSingleShot(true);
438 m_iconUpdateTimer->setInterval(200);
439 q->connect(sender: m_iconUpdateTimer, signal: &QTimer::timeout, context: q, slot: [this]() {
440 dispatchIconUpdateQueue();
441 });
442
443 // Whenever the scrollbar values have been changed, the pending previews should
444 // be reordered in a way that the previews for the visible items are generated
445 // first. The reordering is done with a small delay, so that during moving the
446 // scrollbars the CPU load is kept low.
447 m_scrollAreaTimer = new QTimer(q);
448 m_scrollAreaTimer->setSingleShot(true);
449 m_scrollAreaTimer->setInterval(200);
450 q->connect(sender: m_scrollAreaTimer, signal: &QTimer::timeout, context: q, slot: [this]() {
451 resumeIconUpdates();
452 });
453 m_viewAdapter->connect(signal: KAbstractViewAdapter::IconSizeChanged, receiver: q, SLOT(updateIcons()));
454 m_viewAdapter->connect(signal: KAbstractViewAdapter::ScrollBarValueChanged, receiver: q, SLOT(pauseIconUpdates()));
455
456 m_changedItemsTimer = new QTimer(q);
457 m_changedItemsTimer->setSingleShot(true);
458 m_changedItemsTimer->setInterval(5000);
459 q->connect(sender: m_changedItemsTimer, signal: &QTimer::timeout, context: q, slot: [this]() {
460 delayedIconUpdate();
461 });
462
463 KConfigGroup globalConfig(KSharedConfig::openConfig(QStringLiteral("dolphinrc")), QStringLiteral("PreviewSettings"));
464 m_enabledPlugins =
465 globalConfig.readEntry(key: "Plugins", aDefault: QStringList{QStringLiteral("directorythumbnail"), QStringLiteral("imagethumbnail"), QStringLiteral("jpegthumbnail")});
466
467 // Compatibility update: in 4.7, jpegrotatedthumbnail was merged into (or
468 // replaced with?) jpegthumbnail
469 if (m_enabledPlugins.contains(str: QLatin1String("jpegrotatedthumbnail"))) {
470 m_enabledPlugins.removeAll(QStringLiteral("jpegrotatedthumbnail"));
471 m_enabledPlugins.append(QStringLiteral("jpegthumbnail"));
472 globalConfig.writeEntry(key: "Plugins", value: m_enabledPlugins);
473 globalConfig.sync();
474 }
475}
476
477KFilePreviewGeneratorPrivate::~KFilePreviewGeneratorPrivate()
478{
479 killPreviewJobs();
480 m_pendingItems.clear();
481 m_dispatchedItems.clear();
482}
483
484void KFilePreviewGeneratorPrivate::requestSequenceIcon(const QModelIndex &index, int sequenceIndex)
485{
486 if (m_pendingItems.isEmpty() || (sequenceIndex == 0)) {
487 KDirModel *dirModel = m_dirModel.data();
488 if (!dirModel) {
489 return;
490 }
491
492 KFileItem item = dirModel->itemForIndex(index);
493 if (sequenceIndex == 0) {
494 m_sequenceIndices.remove(key: item.url());
495 } else {
496 m_sequenceIndices.insert(key: item.url(), value: sequenceIndex);
497 }
498
499 ///@todo Update directly, without using m_sequenceIndices
500 updateIcons(items: KFileItemList{item});
501 }
502}
503
504void KFilePreviewGeneratorPrivate::updateIcons(const KFileItemList &items)
505{
506 if (items.isEmpty()) {
507 return;
508 }
509
510 applyCutItemEffect(items);
511
512 KFileItemList orderedItems = items;
513 orderItems(items&: orderedItems);
514
515 m_pendingItems.reserve(asize: m_pendingItems.size() + orderedItems.size());
516 for (const KFileItem &item : std::as_const(t&: orderedItems)) {
517 m_pendingItems.append(t: item);
518 }
519
520 if (m_previewShown) {
521 createPreviews(items: orderedItems);
522 } else {
523 startMimeTypeResolving();
524 }
525}
526
527void KFilePreviewGeneratorPrivate::updateIcons(const QModelIndex &topLeft, const QModelIndex &bottomRight)
528{
529 if (m_internalDataChange > 0) {
530 // QAbstractItemModel::setData() has been invoked internally by the KFilePreviewGenerator.
531 // The signal dataChanged() is connected with this method, but previews only need
532 // to be generated when an external data change has occurred.
533 return;
534 }
535
536 // dataChanged emitted for the root dir (e.g. permission changes)
537 if (!topLeft.isValid() || !bottomRight.isValid()) {
538 return;
539 }
540
541 KDirModel *dirModel = m_dirModel.data();
542 if (!dirModel) {
543 return;
544 }
545
546 KFileItemList itemList;
547 for (int row = topLeft.row(); row <= bottomRight.row(); ++row) {
548 const QModelIndex index = dirModel->index(row, column: 0);
549 if (!index.isValid()) {
550 continue;
551 }
552 const KFileItem item = dirModel->itemForIndex(index);
553 Q_ASSERT(!item.isNull());
554
555 if (m_previewShown) {
556 const QUrl url = item.url();
557 const bool hasChanged = m_changedItems.contains(key: url); // O(1)
558 m_changedItems.insert(key: url, value: hasChanged);
559 if (!hasChanged) {
560 // only update the icon if it has not been already updated within
561 // the last 5 seconds (the other icons will be updated later with
562 // the help of m_changedItemsTimer)
563 itemList.append(t: item);
564 }
565 } else {
566 itemList.append(t: item);
567 }
568 }
569
570 updateIcons(items: itemList);
571 m_changedItemsTimer->start();
572}
573
574void KFilePreviewGeneratorPrivate::addToPreviewQueue(const KFileItem &item, const QPixmap &pixmap, KIO::PreviewJob *job)
575{
576 Q_ASSERT(job);
577 if (job) {
578 QMap<QUrl, int>::iterator it = m_sequenceIndices.find(key: item.url());
579 if (job->sequenceIndex() && (it == m_sequenceIndices.end() || *it != job->sequenceIndex())) {
580 return; // the sequence index does not match the one we want
581 }
582 if (!job->sequenceIndex() && it != m_sequenceIndices.end()) {
583 return; // the sequence index does not match the one we want
584 }
585
586 if (it != m_sequenceIndices.end()) {
587 m_sequenceIndices.erase(it);
588 }
589 }
590
591 if (!m_previewShown) {
592 // the preview has been canceled in the meantime
593 return;
594 }
595
596 KDirModel *dirModel = m_dirModel.data();
597 if (!dirModel) {
598 return;
599 }
600
601 const QUrl itemParentDir = item.url().adjusted(options: QUrl::RemoveFilename | QUrl::StripTrailingSlash);
602
603 const QList<QUrl> dirs = dirModel->dirLister()->directories();
604
605 // check whether the item is part of the directory lister (it is possible
606 // that a preview from an old directory lister is received)
607 const bool isOldPreview = std::none_of(first: dirs.cbegin(), last: dirs.cend(), pred: [&itemParentDir](const QUrl &dir) {
608 return dir == itemParentDir || dir.path().isEmpty();
609 });
610 if (isOldPreview) {
611 return;
612 }
613
614 QPixmap icon = pixmap;
615
616 const QString mimeType = item.mimetype();
617 const int slashIndex = mimeType.indexOf(c: QLatin1Char('/'));
618 const auto mimeTypeGroup = QStringView(mimeType).left(n: slashIndex);
619 if (mimeTypeGroup != QLatin1String("image") || !applyImageFrame(icon)) {
620 limitToSize(icon, maxSize: m_viewAdapter->iconSize());
621 }
622
623 if (m_hasCutSelection && isCutItem(item)) {
624 // apply the disabled effect to the icon for marking it as "cut item"
625 // and apply the icon to the item
626 KIconEffect *iconEffect = KIconLoader::global()->iconEffect();
627 icon = iconEffect->apply(src: icon, group: KIconLoader::Desktop, state: KIconLoader::DisabledState);
628 }
629
630 KIconLoader::global()->drawOverlays(overlays: item.overlays(), pixmap&: icon, group: KIconLoader::Desktop);
631
632 // remember the preview and URL, so that it can be applied to the model
633 // in KFilePreviewGenerator::dispatchIconUpdateQueue()
634 ItemInfo preview;
635 preview.url = item.url();
636 preview.pixmap = icon;
637 m_previews.append(t: preview);
638
639 m_pendingItems.removeOne(t: item);
640
641 m_dispatchedItems.append(t: item);
642}
643
644void KFilePreviewGeneratorPrivate::slotPreviewJobFinished(KJob *job)
645{
646 const int index = m_previewJobs.indexOf(t: job);
647 m_previewJobs.removeAt(i: index);
648
649 if (m_previewJobs.isEmpty()) {
650 for (const KFileItem &item : std::as_const(t&: m_pendingItems)) {
651 if (item.isMimeTypeKnown()) {
652 m_resolvedMimeTypes.append(t: item);
653 }
654 }
655
656 if (m_clearItemQueues) {
657 m_pendingItems.clear();
658 m_dispatchedItems.clear();
659 m_pendingVisibleIconUpdates = 0;
660 auto dispatchFunc = [this]() {
661 dispatchIconUpdateQueue();
662 };
663 QMetaObject::invokeMethod(object: q, function&: dispatchFunc, type: Qt::QueuedConnection);
664 }
665 m_sequenceIndices.clear(); // just to be sure that we don't leak anything
666 }
667}
668
669void KFilePreviewGeneratorPrivate::updateCutItems()
670{
671 KDirModel *dirModel = m_dirModel.data();
672 if (!dirModel) {
673 return;
674 }
675
676 DataChangeObtainer obt(this);
677 clearCutItemsCache();
678
679 KFileItemList items;
680 KDirLister *dirLister = dirModel->dirLister();
681 const QList<QUrl> dirs = dirLister->directories();
682 items.reserve(asize: dirs.size());
683 for (const QUrl &url : dirs) {
684 items << dirLister->itemsForDir(dirUrl: url);
685 }
686 applyCutItemEffect(items);
687}
688
689void KFilePreviewGeneratorPrivate::clearCutItemsCache()
690{
691 KDirModel *dirModel = m_dirModel.data();
692 if (!dirModel) {
693 return;
694 }
695
696 DataChangeObtainer obt(this);
697 KFileItemList previews;
698 // Reset the icons of all items that are stored in the cache
699 // to use their default MIME type icon.
700 for (auto it = m_cutItemsCache.cbegin(); it != m_cutItemsCache.cend(); ++it) {
701 const QModelIndex index = dirModel->indexForUrl(url: it.key());
702 if (index.isValid()) {
703 dirModel->setData(index, value: QIcon(), role: Qt::DecorationRole);
704 if (m_previewShown) {
705 previews.append(t: dirModel->itemForIndex(index));
706 }
707 }
708 }
709 m_cutItemsCache.clear();
710
711 if (!previews.isEmpty()) {
712 // assure that the previews gets restored
713 Q_ASSERT(m_previewShown);
714 orderItems(items&: previews);
715 updateIcons(items: previews);
716 }
717}
718
719void KFilePreviewGeneratorPrivate::dispatchIconUpdateQueue()
720{
721 KDirModel *dirModel = m_dirModel.data();
722 if (!dirModel) {
723 return;
724 }
725
726 const int count = m_previews.count() + m_resolvedMimeTypes.count();
727 if (count > 0) {
728 LayoutBlocker blocker(m_itemView);
729 DataChangeObtainer obt(this);
730
731 if (m_previewShown) {
732 // dispatch preview queue
733 for (const ItemInfo &preview : std::as_const(t&: m_previews)) {
734 const QModelIndex idx = dirModel->indexForUrl(url: preview.url);
735 if (idx.isValid() && (idx.column() == 0)) {
736 dirModel->setData(index: idx, value: QIcon(preview.pixmap), role: Qt::DecorationRole);
737 }
738 }
739 m_previews.clear();
740 }
741
742 // dispatch MIME type queue
743 for (const KFileItem &item : std::as_const(t&: m_resolvedMimeTypes)) {
744 const QModelIndex idx = dirModel->indexForItem(item);
745 dirModel->itemChanged(index: idx);
746 }
747 m_resolvedMimeTypes.clear();
748
749 m_pendingVisibleIconUpdates -= count;
750 if (m_pendingVisibleIconUpdates < 0) {
751 m_pendingVisibleIconUpdates = 0;
752 }
753 }
754
755 if (m_pendingVisibleIconUpdates > 0) {
756 // As long as there are pending previews for visible items, poll
757 // the preview queue periodically. If there are no pending previews,
758 // the queue is dispatched in slotPreviewJobFinished().
759 m_iconUpdateTimer->start();
760 }
761}
762
763void KFilePreviewGeneratorPrivate::pauseIconUpdates()
764{
765 m_iconUpdatesPaused = true;
766 for (KJob *job : std::as_const(t&: m_previewJobs)) {
767 Q_ASSERT(job);
768 job->suspend();
769 }
770 m_scrollAreaTimer->start();
771}
772
773void KFilePreviewGeneratorPrivate::resumeIconUpdates()
774{
775 m_iconUpdatesPaused = false;
776
777 // Before creating new preview jobs the m_pendingItems queue must be
778 // cleaned up by removing the already dispatched items. Implementation
779 // note: The order of the m_dispatchedItems queue and the m_pendingItems
780 // queue is usually equal. So even when having a lot of elements the
781 // nested loop is no performance bottle neck, as the inner loop is only
782 // entered once in most cases.
783 for (const KFileItem &item : std::as_const(t&: m_dispatchedItems)) {
784 auto it = std::remove_if(first: m_pendingItems.begin(), last: m_pendingItems.end(), pred: [&item](const KFileItem &pending) {
785 return pending.url() == item.url();
786 });
787 m_pendingItems.erase(abegin: it, aend: m_pendingItems.end());
788 }
789
790 m_dispatchedItems.clear();
791
792 m_pendingVisibleIconUpdates = 0;
793 dispatchIconUpdateQueue();
794
795 if (m_previewShown) {
796 KFileItemList orderedItems = m_pendingItems;
797 orderItems(items&: orderedItems);
798
799 // Kill all suspended preview jobs. Usually when a preview job
800 // has been finished, slotPreviewJobFinished() clears all item queues.
801 // This is not wanted in this case, as a new job is created afterwards
802 // for m_pendingItems.
803 m_clearItemQueues = false;
804 killPreviewJobs();
805 m_clearItemQueues = true;
806
807 createPreviews(items: orderedItems);
808 } else {
809 orderItems(items&: m_pendingItems);
810 startMimeTypeResolving();
811 }
812}
813
814void KFilePreviewGeneratorPrivate::startMimeTypeResolving()
815{
816 resolveMimeType();
817 m_iconUpdateTimer->start();
818}
819
820void KFilePreviewGeneratorPrivate::resolveMimeType()
821{
822 if (m_pendingItems.isEmpty()) {
823 return;
824 }
825
826 // resolve at least one MIME type
827 bool resolved = false;
828 do {
829 KFileItem item = m_pendingItems.takeFirst();
830 if (item.isMimeTypeKnown()) {
831 if (m_pendingVisibleIconUpdates > 0) {
832 // The item is visible and the MIME type already known.
833 // Decrease the update counter for dispatchIconUpdateQueue():
834 --m_pendingVisibleIconUpdates;
835 }
836 } else {
837 // The MIME type is unknown and must get resolved. The
838 // directory model is not informed yet, as a single update
839 // would be very expensive. Instead the item is remembered in
840 // m_resolvedMimeTypes and will be dispatched later
841 // by dispatchIconUpdateQueue().
842 item.determineMimeType();
843 m_resolvedMimeTypes.append(t: item);
844 resolved = true;
845 }
846 } while (!resolved && !m_pendingItems.isEmpty());
847
848 if (m_pendingItems.isEmpty()) {
849 // All MIME types have been resolved now. Assure
850 // that the directory model gets informed about
851 // this, so that an update of the icons is done.
852 dispatchIconUpdateQueue();
853 } else if (!m_iconUpdatesPaused) {
854 // assure that the MIME type of the next
855 // item will be resolved asynchronously
856 auto mimeFunc = [this]() {
857 resolveMimeType();
858 };
859 QMetaObject::invokeMethod(object: q, function&: mimeFunc, type: Qt::QueuedConnection);
860 }
861}
862
863bool KFilePreviewGeneratorPrivate::isCutItem(const KFileItem &item) const
864{
865 const QMimeData *mimeData = QApplication::clipboard()->mimeData();
866 const QList<QUrl> cutUrls = KUrlMimeData::urlsFromMimeData(mimeData);
867 return cutUrls.contains(t: item.url());
868}
869
870void KFilePreviewGeneratorPrivate::applyCutItemEffect(const KFileItemList &items)
871{
872 const QMimeData *mimeData = QApplication::clipboard()->mimeData();
873 m_hasCutSelection = mimeData && KIO::isClipboardDataCut(mimeData);
874 if (!m_hasCutSelection) {
875 return;
876 }
877
878 KDirModel *dirModel = m_dirModel.data();
879 if (!dirModel) {
880 return;
881 }
882
883 const QList<QUrl> urlsList = KUrlMimeData::urlsFromMimeData(mimeData);
884 const QSet<QUrl> cutUrls(urlsList.begin(), urlsList.end());
885
886 DataChangeObtainer obt(this);
887 KIconEffect *iconEffect = KIconLoader::global()->iconEffect();
888 for (const KFileItem &item : items) {
889 if (cutUrls.contains(value: item.url())) {
890 const QModelIndex index = dirModel->indexForItem(item);
891 const QVariant value = dirModel->data(index, role: Qt::DecorationRole);
892 if (value.typeId() == QMetaType::QIcon) {
893 const QIcon icon(qvariant_cast<QIcon>(v: value));
894 const QSize actualSize = icon.actualSize(size: m_viewAdapter->iconSize());
895 QPixmap pixmap = icon.pixmap(size: actualSize);
896
897 const auto cacheIt = m_cutItemsCache.constFind(key: item.url());
898 if ((cacheIt == m_cutItemsCache.constEnd()) || (cacheIt->cacheKey() != pixmap.cacheKey())) {
899 pixmap = iconEffect->apply(src: pixmap, group: KIconLoader::Desktop, state: KIconLoader::DisabledState);
900 dirModel->setData(index, value: QIcon(pixmap), role: Qt::DecorationRole);
901
902 m_cutItemsCache.insert(key: item.url(), value: pixmap);
903 }
904 }
905 }
906 }
907}
908
909bool KFilePreviewGeneratorPrivate::applyImageFrame(QPixmap &icon)
910{
911 const QSize maxSize = m_viewAdapter->iconSize();
912 const bool applyFrame = (maxSize.width() > KIconLoader::SizeSmallMedium) && (maxSize.height() > KIconLoader::SizeSmallMedium) && !icon.hasAlpha();
913 if (!applyFrame) {
914 // the maximum size or the image itself is too small for a frame
915 return false;
916 }
917
918 // resize the icon to the maximum size minus the space required for the frame
919 const QSize size(maxSize.width() - TileSet::LeftMargin - TileSet::RightMargin, maxSize.height() - TileSet::TopMargin - TileSet::BottomMargin);
920 limitToSize(icon, maxSize: size);
921
922 if (!m_tileSet) {
923 m_tileSet.reset(p: new TileSet{});
924 }
925
926 QPixmap framedIcon(icon.size().width() + TileSet::LeftMargin + TileSet::RightMargin, icon.size().height() + TileSet::TopMargin + TileSet::BottomMargin);
927 framedIcon.fill(fillColor: Qt::transparent);
928
929 QPainter painter;
930 painter.begin(&framedIcon);
931 painter.setCompositionMode(QPainter::CompositionMode_Source);
932 m_tileSet->paint(p: &painter, r: framedIcon.rect());
933 painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
934 painter.drawPixmap(x: TileSet::LeftMargin, y: TileSet::TopMargin, pm: icon);
935 painter.end();
936
937 icon = framedIcon;
938 return true;
939}
940
941void KFilePreviewGeneratorPrivate::limitToSize(QPixmap &icon, const QSize &maxSize)
942{
943 if ((icon.width() > maxSize.width()) || (icon.height() > maxSize.height())) {
944 icon = icon.scaled(s: maxSize, aspectMode: Qt::KeepAspectRatio, mode: Qt::SmoothTransformation);
945 }
946}
947
948void KFilePreviewGeneratorPrivate::createPreviews(const KFileItemList &items)
949{
950 if (items.isEmpty()) {
951 return;
952 }
953
954 const QMimeData *mimeData = QApplication::clipboard()->mimeData();
955 m_hasCutSelection = mimeData && KIO::isClipboardDataCut(mimeData);
956
957 // PreviewJob internally caches items always with the size of
958 // 128 x 128 pixels or 256 x 256 pixels. A downscaling is done
959 // by PreviewJob if a smaller size is requested. For images KFilePreviewGenerator must
960 // do a downscaling anyhow because of the frame, so in this case only the provided
961 // cache sizes are requested.
962 KFileItemList imageItems;
963 KFileItemList otherItems;
964 QString mimeType;
965 for (const KFileItem &item : items) {
966 mimeType = item.mimetype();
967 const int slashIndex = mimeType.indexOf(c: QLatin1Char('/'));
968 const auto mimeTypeGroup = QStringView(mimeType).left(n: slashIndex);
969 if (mimeTypeGroup == QLatin1String("image")) {
970 imageItems.append(t: item);
971 } else {
972 otherItems.append(t: item);
973 }
974 }
975 const QSize size = m_viewAdapter->iconSize();
976 const int width = size.width();
977 const int height = size.height();
978 startPreviewJob(items: otherItems, width, height);
979
980 const int longer = std::max(a: width, b: height);
981 int cacheSize = 128;
982 if (longer > 512) {
983 cacheSize = 1024;
984 } else if (longer > 256) {
985 cacheSize = 512;
986 } else if (longer > 128) {
987 cacheSize = 256;
988 }
989 startPreviewJob(items: imageItems, width: cacheSize, height: cacheSize);
990
991 m_iconUpdateTimer->start();
992}
993
994void KFilePreviewGeneratorPrivate::startPreviewJob(const KFileItemList &items, int width, int height)
995{
996 if (items.isEmpty()) {
997 return;
998 }
999
1000 KIO::PreviewJob *job = KIO::filePreview(items, size: QSize(width, height), enabledPlugins: &m_enabledPlugins);
1001
1002 // Set the sequence index to the target. We only need to check if items.count() == 1,
1003 // because requestSequenceIcon(..) creates exactly such a request.
1004 if (!m_sequenceIndices.isEmpty() && (items.count() == 1)) {
1005 const auto it = m_sequenceIndices.constFind(key: items[0].url());
1006 if (it != m_sequenceIndices.cend()) {
1007 job->setSequenceIndex(*it);
1008 }
1009 }
1010
1011 q->connect(sender: job, signal: &KIO::PreviewJob::gotPreview, context: q, slot: [this, job](const KFileItem &item, const QPixmap &pixmap) {
1012 addToPreviewQueue(item, pixmap, job);
1013 });
1014
1015 q->connect(sender: job, signal: &KIO::PreviewJob::finished, context: q, slot: [this, job]() {
1016 slotPreviewJobFinished(job);
1017 });
1018 m_previewJobs.append(t: job);
1019}
1020
1021void KFilePreviewGeneratorPrivate::killPreviewJobs()
1022{
1023 for (KJob *job : std::as_const(t&: m_previewJobs)) {
1024 Q_ASSERT(job);
1025 job->kill();
1026 }
1027 m_previewJobs.clear();
1028 m_sequenceIndices.clear();
1029
1030 m_iconUpdateTimer->stop();
1031 m_scrollAreaTimer->stop();
1032 m_changedItemsTimer->stop();
1033}
1034
1035void KFilePreviewGeneratorPrivate::orderItems(KFileItemList &items)
1036{
1037 KDirModel *dirModel = m_dirModel.data();
1038 if (!dirModel) {
1039 return;
1040 }
1041
1042 // Order the items in a way that the preview for the visible items
1043 // is generated first, as this improves the felt performance a lot.
1044 const bool hasProxy = m_proxyModel != nullptr;
1045 const int itemCount = items.count();
1046 const QRect visibleArea = m_viewAdapter->visibleArea();
1047
1048 QModelIndex dirIndex;
1049 QRect itemRect;
1050 int insertPos = 0;
1051 for (int i = 0; i < itemCount; ++i) {
1052 dirIndex = dirModel->indexForItem(items.at(i)); // O(n) (n = number of rows)
1053 if (hasProxy) {
1054 const QModelIndex proxyIndex = m_proxyModel->mapFromSource(sourceIndex: dirIndex);
1055 itemRect = m_viewAdapter->visualRect(index: proxyIndex);
1056 } else {
1057 itemRect = m_viewAdapter->visualRect(index: dirIndex);
1058 }
1059
1060 if (itemRect.intersects(r: visibleArea)) {
1061 // The current item is (at least partly) visible. Move it
1062 // to the front of the list, so that the preview is
1063 // generated earlier.
1064 items.insert(i: insertPos, t: items.at(i));
1065 items.removeAt(i: i + 1);
1066 ++insertPos;
1067 ++m_pendingVisibleIconUpdates;
1068 }
1069 }
1070}
1071
1072void KFilePreviewGeneratorPrivate::addItemsToList(const QModelIndex &index, KFileItemList &list)
1073{
1074 KDirModel *dirModel = m_dirModel.data();
1075 if (!dirModel) {
1076 return;
1077 }
1078
1079 const int rowCount = dirModel->rowCount(parent: index);
1080 for (int row = 0; row < rowCount; ++row) {
1081 const QModelIndex subIndex = dirModel->index(row, column: 0, parent: index);
1082 KFileItem item = dirModel->itemForIndex(index: subIndex);
1083 list.append(t: item);
1084
1085 if (dirModel->rowCount(parent: subIndex) > 0) {
1086 // the model is hierarchical (treeview)
1087 addItemsToList(index: subIndex, list);
1088 }
1089 }
1090}
1091
1092void KFilePreviewGeneratorPrivate::delayedIconUpdate()
1093{
1094 KDirModel *dirModel = m_dirModel.data();
1095 if (!dirModel) {
1096 return;
1097 }
1098
1099 // Precondition: No items have been changed within the last
1100 // 5 seconds. This means that items that have been changed constantly
1101 // due to a copy operation should be updated now.
1102
1103 KFileItemList itemList;
1104
1105 for (auto it = m_changedItems.cbegin(); it != m_changedItems.cend(); ++it) {
1106 const bool hasChanged = it.value();
1107 if (hasChanged) {
1108 const QModelIndex index = dirModel->indexForUrl(url: it.key());
1109 const KFileItem item = dirModel->itemForIndex(index);
1110 itemList.append(t: item);
1111 }
1112 }
1113 m_changedItems.clear();
1114
1115 updateIcons(items: itemList);
1116}
1117
1118void KFilePreviewGeneratorPrivate::rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end)
1119{
1120 if (m_changedItems.isEmpty()) {
1121 return;
1122 }
1123
1124 KDirModel *dirModel = m_dirModel.data();
1125 if (!dirModel) {
1126 return;
1127 }
1128
1129 for (int row = start; row <= end; row++) {
1130 const QModelIndex index = dirModel->index(row, column: 0, parent);
1131
1132 const KFileItem item = dirModel->itemForIndex(index);
1133 if (!item.isNull()) {
1134 m_changedItems.remove(key: item.url());
1135 }
1136
1137 if (dirModel->hasChildren(parent: index)) {
1138 rowsAboutToBeRemoved(parent: index, start: 0, end: dirModel->rowCount(parent: index) - 1);
1139 }
1140 }
1141}
1142
1143KFilePreviewGenerator::KFilePreviewGenerator(QAbstractItemView *parent)
1144 : QObject(parent)
1145 , d(new KFilePreviewGeneratorPrivate(this, new KIO::DefaultViewAdapter(parent, this), parent->model()))
1146{
1147 d->m_itemView = parent;
1148}
1149
1150KFilePreviewGenerator::KFilePreviewGenerator(KAbstractViewAdapter *parent, QAbstractProxyModel *model)
1151 : QObject(parent)
1152 , d(new KFilePreviewGeneratorPrivate(this, parent, model))
1153{
1154}
1155
1156KFilePreviewGenerator::~KFilePreviewGenerator() = default;
1157
1158void KFilePreviewGenerator::setPreviewShown(bool show)
1159{
1160 if (d->m_previewShown == show) {
1161 return;
1162 }
1163
1164 KDirModel *dirModel = d->m_dirModel.data();
1165 if (show && (!d->m_viewAdapter->iconSize().isValid() || !dirModel)) {
1166 // The view must provide an icon size and a directory model,
1167 // otherwise the showing the previews will get ignored
1168 return;
1169 }
1170
1171 d->m_previewShown = show;
1172 if (!show) {
1173 dirModel->clearAllPreviews();
1174 }
1175 updateIcons();
1176}
1177
1178bool KFilePreviewGenerator::isPreviewShown() const
1179{
1180 return d->m_previewShown;
1181}
1182
1183void KFilePreviewGenerator::updateIcons()
1184{
1185 d->killPreviewJobs();
1186
1187 d->clearCutItemsCache();
1188 d->m_pendingItems.clear();
1189 d->m_dispatchedItems.clear();
1190
1191 KFileItemList itemList;
1192 d->addItemsToList(index: QModelIndex(), list&: itemList);
1193
1194 d->updateIcons(items: itemList);
1195}
1196
1197void KFilePreviewGenerator::cancelPreviews()
1198{
1199 d->killPreviewJobs();
1200 d->m_pendingItems.clear();
1201 d->m_dispatchedItems.clear();
1202 updateIcons();
1203}
1204
1205void KFilePreviewGenerator::setEnabledPlugins(const QStringList &plugins)
1206{
1207 d->m_enabledPlugins = plugins;
1208}
1209
1210QStringList KFilePreviewGenerator::enabledPlugins() const
1211{
1212 return d->m_enabledPlugins;
1213}
1214
1215#include "moc_kfilepreviewgenerator.cpp"
1216

source code of kio/src/filewidgets/kfilepreviewgenerator.cpp