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

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