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