X-Git-Url: https://cloud.milkyroute.net/gitweb/dolphin.git/blobdiff_plain/629ec98952bcf38bc99f2b11bc37bdc2ec7aabcc..aa3e271ce39e7cbbd546d5f9197a1f83ba53fcde:/src/iconmanager.cpp diff --git a/src/iconmanager.cpp b/src/iconmanager.cpp index 002f4e86d..6cf6f001c 100644 --- a/src/iconmanager.cpp +++ b/src/iconmanager.cpp @@ -19,12 +19,11 @@ #include "iconmanager.h" -#include "dolphinmodel.h" -#include "dolphinsortfilterproxymodel.h" - #include #include #include +#include +#include #include #include @@ -32,19 +31,66 @@ #include #include #include +#include #include #include #include -IconManager::IconManager(QAbstractItemView* parent, DolphinSortFilterProxyModel* model) : +/** + * If the passed item view is an instance of QListView, expensive + * layout operations are blocked in the constructor and are unblocked + * again in the destructor. + * + * This helper class is a workaround for the following huge performance + * problem when having directories with several 1000 items: + * - each change of an icon emits a dataChanged() signal from the model + * - QListView iterates through all items on each dataChanged() signal + * and invokes QItemDelegate::sizeHint() + * - the sizeHint() implementation of KFileItemDelegate is quite complex, + * invoking it 1000 times for each icon change might block the UI + * + * QListView does not invoke QItemDelegate::sizeHint() when the + * uniformItemSize property has been set to true, so this property is + * set before exchanging a block of icons. It is important to reset + * it again before the event loop is entered, otherwise QListView + * would not get the correct size hints after dispatching the layoutChanged() + * signal. + */ +class LayoutBlocker { +public: + LayoutBlocker(QAbstractItemView* view) : + m_uniformSizes(false), + m_view(qobject_cast(view)) + { + if (m_view != 0) { + m_uniformSizes = m_view->uniformItemSizes(); + m_view->setUniformItemSizes(true); + } + } + + ~LayoutBlocker() + { + if (m_view != 0) { + m_view->setUniformItemSizes(m_uniformSizes); + } + } + +private: + bool m_uniformSizes; + QListView* m_view; +}; + +IconManager::IconManager(QAbstractItemView* parent, KDirSortFilterProxyModel* model) : QObject(parent), m_showPreview(false), m_clearItemQueues(true), + m_hasCutSelection(false), + m_pendingVisiblePreviews(0), m_view(parent), m_previewTimer(0), m_scrollAreaTimer(0), m_previewJobs(), - m_dolphinModel(0), + m_dirModel(0), m_proxyModel(model), m_mimeTypeResolver(0), m_cutItemsCache(), @@ -54,8 +100,8 @@ IconManager::IconManager(QAbstractItemView* parent, DolphinSortFilterProxyModel* { Q_ASSERT(m_view->iconSize().isValid()); // each view must provide its current icon size - m_dolphinModel = static_cast(m_proxyModel->sourceModel()); - connect(m_dolphinModel->dirLister(), SIGNAL(newItems(const KFileItemList&)), + m_dirModel = static_cast(m_proxyModel->sourceModel()); + connect(m_dirModel->dirLister(), SIGNAL(newItems(const KFileItemList&)), this, SLOT(generatePreviews(const KFileItemList&))); QClipboard* clipboard = QApplication::clipboard(); @@ -92,7 +138,6 @@ IconManager::~IconManager() } } - void IconManager::setShowPreview(bool show) { if (m_showPreview != show) { @@ -111,7 +156,7 @@ void IconManager::setShowPreview(bool show) } else if (!show && (m_mimeTypeResolver == 0)) { // the preview is turned off: resolve the MIME-types so that // the icons gets updated - m_mimeTypeResolver = new KMimeTypeResolver(m_view, m_dolphinModel); + m_mimeTypeResolver = new KMimeTypeResolver(m_view, m_dirModel); } } @@ -127,10 +172,10 @@ void IconManager::updatePreviews() m_dispatchedItems.clear(); KFileItemList itemList; - const int rowCount = m_dolphinModel->rowCount(); + const int rowCount = m_dirModel->rowCount(); for (int row = 0; row < rowCount; ++row) { - const QModelIndex index = m_dolphinModel->index(row, 0); - KFileItem item = m_dolphinModel->itemForIndex(index); + const QModelIndex index = m_dirModel->index(row, 0); + KFileItem item = m_dirModel->itemForIndex(index); itemList.append(item); } @@ -154,28 +199,11 @@ void IconManager::generatePreviews(const KFileItemList& items) return; } - // Order the items in a way that the preview for the visible items - // is generated first, as this improves the feeled performance a lot. - // Implementation note: using KDirModel::itemForUrl() would lead to a more - // readable code, but it is a lot slower in comparison to itemListContains(). - const QRect visibleArea = m_view->viewport()->rect(); - KFileItemList orderedItems; - const int rowCount = m_proxyModel->rowCount(); - for (int row = 0; row < rowCount; ++row) { - const QModelIndex proxyIndex = m_proxyModel->index(row, 0); - const QRect itemRect = m_view->visualRect(proxyIndex); - const QModelIndex dirIndex = m_proxyModel->mapToSource(proxyIndex); - KFileItem item = m_dolphinModel->itemForIndex(dirIndex); - const KUrl url = item.url(); - if (itemListContains(items, url)) { - if (itemRect.intersects(visibleArea)) { - orderedItems.insert(0, item); - m_pendingItems.insert(0, url); - } else { - orderedItems.append(item); - m_pendingItems.append(url); - } - } + KFileItemList orderedItems = items; + orderItems(orderedItems); + + foreach (const KFileItem& item, orderedItems) { + m_pendingItems.append(item); } startPreviewJob(orderedItems); @@ -183,12 +211,63 @@ void IconManager::generatePreviews(const KFileItemList& items) void IconManager::addToPreviewQueue(const KFileItem& item, const QPixmap& pixmap) { + if (!m_showPreview) { + // the preview has been canceled in the meantime + return; + } + const KUrl url = item.url(); + + // check whether the item is part of the directory lister (it is possible + // that a preview from an old directory lister is received) + KDirLister* dirLister = m_dirModel->dirLister(); + bool isOldPreview = true; + const KUrl::List dirs = dirLister->directories(); + const QString itemDir = url.directory(); + foreach (const KUrl& url, dirs) { + if (url.path() == itemDir) { + isOldPreview = false; + break; + } + } + if (isOldPreview) { + return; + } + + QPixmap icon = pixmap; + + const QString mimeType = item.mimetype(); + const QString mimeTypeGroup = mimeType.left(mimeType.indexOf('/')); + if ((mimeTypeGroup != "image") || !applyImageFrame(icon)) { + limitToSize(icon, m_view->iconSize()); + } + + if (m_hasCutSelection && isCutItem(item)) { + // Remember the current icon in the cache for cut items before + // the disabled effect is applied. This makes it possible restoring + // the uncut version again when cutting other items. + QList::iterator begin = m_cutItemsCache.begin(); + QList::iterator end = m_cutItemsCache.end(); + for (QList::iterator it = begin; it != end; ++it) { + if ((*it).url == item.url()) { + (*it).pixmap = icon; + break; + } + } + + // apply the disabled effect to the icon for marking it as "cut item" + // and apply the icon to the item + KIconEffect iconEffect; + icon = iconEffect.apply(icon, KIconLoader::Desktop, KIconLoader::DisabledState); + } + + // remember the preview and URL, so that it can be applied to the model + // in IconManager::dispatchPreviewQueue() ItemInfo preview; - preview.url = item.url(); - preview.pixmap = pixmap; + preview.url = url; + preview.pixmap = icon; m_previews.append(preview); - m_dispatchedItems.append(item.url()); + m_dispatchedItems.append(item); } void IconManager::slotPreviewJobFinished(KJob* job) @@ -199,6 +278,8 @@ void IconManager::slotPreviewJobFinished(KJob* job) if ((m_previewJobs.count() == 0) && m_clearItemQueues) { m_pendingItems.clear(); m_dispatchedItems.clear(); + m_pendingVisiblePreviews = 0; + QMetaObject::invokeMethod(this, "dispatchPreviewQueue", Qt::QueuedConnection); } } @@ -207,9 +288,9 @@ void IconManager::updateCutItems() // restore the icons of all previously selected items to the // original state... foreach (const ItemInfo& cutItem, m_cutItemsCache) { - const QModelIndex index = m_dolphinModel->indexForUrl(cutItem.url); + const QModelIndex index = m_dirModel->indexForUrl(cutItem.url); if (index.isValid()) { - m_dolphinModel->setData(index, QIcon(cutItem.pixmap), Qt::DecorationRole); + m_dirModel->setData(index, QIcon(cutItem.pixmap), Qt::DecorationRole); } } m_cutItemsCache.clear(); @@ -220,35 +301,33 @@ void IconManager::updateCutItems() void IconManager::dispatchPreviewQueue() { - int previewsCount = m_previews.count(); + const int previewsCount = m_previews.count(); if (previewsCount > 0) { // Applying the previews to the model must be done step by step // in larger blocks: Applying a preview immediately when getting the signal // 'gotPreview()' from the PreviewJob is too expensive, as a relayout // of the view would be triggered for each single preview. + LayoutBlocker blocker(m_view); + for (int i = 0; i < previewsCount; ++i) { + const ItemInfo& preview = m_previews.first(); - int dispatchCount = 30; - if (dispatchCount > previewsCount) { - dispatchCount = previewsCount; - } + const QModelIndex idx = m_dirModel->indexForUrl(preview.url); + if (idx.isValid() && (idx.column() == 0)) { + m_dirModel->setData(idx, QIcon(preview.pixmap), Qt::DecorationRole); + } - for (int i = 0; i < dispatchCount; ++i) { - const ItemInfo& preview = m_previews.first(); - replaceIcon(preview.url, preview.pixmap); m_previews.pop_front(); + if (m_pendingVisiblePreviews > 0) { + --m_pendingVisiblePreviews; + } } - - previewsCount = m_previews.count(); } - const bool workingPreviewJobs = (m_previewJobs.count() > 0); - if (workingPreviewJobs) { - // poll for previews as long as not all preview jobs are finished + if (m_pendingVisiblePreviews > 0) { + // As long as there are pending previews for visible items, poll + // the preview queue each 200 ms. If there are no pending previews, + // the queue is dispatched in slotPreviewJobFinished(). m_previewTimer->start(200); - } else if (previewsCount > 0) { - // all preview jobs are finished but there are still pending previews - // in the queue -> poll more aggressively - m_previewTimer->start(10); } } @@ -269,11 +348,11 @@ void IconManager::resumePreviews() // queue is usually equal. So even when having a lot of elements the // nested loop is no performance bottle neck, as the inner loop is only // entered once in most cases. - foreach (const KUrl& url, m_dispatchedItems) { - QList::iterator begin = m_pendingItems.begin(); - QList::iterator end = m_pendingItems.end(); - for (QList::iterator it = begin; it != end; ++it) { - if ((*it) == url) { + foreach (const KFileItem& item, m_dispatchedItems) { + KFileItemList::iterator begin = m_pendingItems.begin(); + KFileItemList::iterator end = m_pendingItems.end(); + for (KFileItemList::iterator it = begin; it != end; ++it) { + if ((*it).url() == item.url()) { m_pendingItems.erase(it); break; } @@ -281,30 +360,11 @@ void IconManager::resumePreviews() } m_dispatchedItems.clear(); - // Create a new preview job for the remaining items. - // Order the items in a way that the preview for the visible items - // is generated first, as this improves the feeled performance a lot. - // Implementation note: using KDirModel::itemForUrl() would lead to a more - // readable code, but it is a lot slower in comparison - // to m_pendingItems.contains(). - const QRect visibleArea = m_view->viewport()->rect(); - KFileItemList orderedItems; + m_pendingVisiblePreviews = 0; + dispatchPreviewQueue(); - const int rowCount = m_proxyModel->rowCount(); - for (int row = 0; row < rowCount; ++row) { - const QModelIndex proxyIndex = m_proxyModel->index(row, 0); - const QRect itemRect = m_view->visualRect(proxyIndex); - const QModelIndex dirIndex = m_proxyModel->mapToSource(proxyIndex); - KFileItem item = m_dolphinModel->itemForIndex(dirIndex); - const KUrl url = item.url(); - if (m_pendingItems.contains(url)) { - if (itemRect.intersects(visibleArea)) { - orderedItems.insert(0, item); - } else { - orderedItems.append(item); - } - } - } + KFileItemList orderedItems = m_pendingItems; + orderItems(orderedItems); // Kill all suspended preview jobs. Usually when a preview job // has been finished, slotPreviewJobFinished() clears all item queues. @@ -317,66 +377,6 @@ void IconManager::resumePreviews() startPreviewJob(orderedItems); } -void IconManager::replaceIcon(const KUrl& url, const QPixmap& pixmap) -{ - Q_ASSERT(url.isValid()); - if (!m_showPreview) { - // the preview has been canceled in the meantime - return; - } - - // check whether the item is part of the directory lister (it is possible - // that a preview from an old directory lister is received) - KDirLister* dirLister = m_dolphinModel->dirLister(); - bool isOldPreview = true; - const KUrl::List dirs = dirLister->directories(); - const QString itemDir = url.directory(); - foreach (const KUrl& url, dirs) { - if (url.path() == itemDir) { - isOldPreview = false; - break; - } - } - if (isOldPreview) { - return; - } - - const QModelIndex idx = m_dolphinModel->indexForUrl(url); - if (idx.isValid() && (idx.column() == 0)) { - QPixmap icon = pixmap; - - const KFileItem item = m_dolphinModel->itemForIndex(idx); - const QString mimeType = item.mimetype(); - const QString mimeTypeGroup = mimeType.left(mimeType.indexOf('/')); - if ((mimeTypeGroup != "image") || !applyImageFrame(icon)) { - limitToSize(icon, m_view->iconSize()); - } - - const QMimeData* mimeData = QApplication::clipboard()->mimeData(); - if (KonqMimeData::decodeIsCutSelection(mimeData) && isCutItem(item)) { - // Remember the current icon in the cache for cut items before - // the disabled effect is applied. This makes it possible restoring - // the uncut version again when cutting other items. - QList::iterator begin = m_cutItemsCache.begin(); - QList::iterator end = m_cutItemsCache.end(); - for (QList::iterator it = begin; it != end; ++it) { - if ((*it).url == item.url()) { - (*it).pixmap = icon; - break; - } - } - - // apply the disabled effect to the icon for marking it as "cut item" - // and apply the icon to the item - KIconEffect iconEffect; - icon = iconEffect.apply(icon, KIconLoader::Desktop, KIconLoader::DisabledState); - m_dolphinModel->setData(idx, QIcon(icon), Qt::DecorationRole); - } else { - m_dolphinModel->setData(idx, QIcon(icon), Qt::DecorationRole); - } - } -} - bool IconManager::isCutItem(const KFileItem& item) const { const QMimeData* mimeData = QApplication::clipboard()->mimeData(); @@ -395,12 +395,13 @@ bool IconManager::isCutItem(const KFileItem& item) const void IconManager::applyCutItemEffect() { const QMimeData* mimeData = QApplication::clipboard()->mimeData(); - if (!KonqMimeData::decodeIsCutSelection(mimeData)) { + m_hasCutSelection = KonqMimeData::decodeIsCutSelection(mimeData); + if (!m_hasCutSelection) { return; } KFileItemList items; - KDirLister* dirLister = m_dolphinModel->dirLister(); + KDirLister* dirLister = m_dirModel->dirLister(); const KUrl::List dirs = dirLister->directories(); foreach (const KUrl& url, dirs) { items << dirLister->itemsForDir(url); @@ -408,8 +409,8 @@ void IconManager::applyCutItemEffect() foreach (const KFileItem& item, items) { if (isCutItem(item)) { - const QModelIndex index = m_dolphinModel->indexForItem(item); - const QVariant value = m_dolphinModel->data(index, Qt::DecorationRole); + const QModelIndex index = m_dirModel->indexForItem(item); + const QVariant value = m_dirModel->data(index, Qt::DecorationRole); if (value.type() == QVariant::Icon) { const QIcon icon(qvariant_cast(value)); const QSize actualSize = icon.actualSize(m_view->iconSize()); @@ -425,7 +426,7 @@ void IconManager::applyCutItemEffect() // apply icon effect to the cut item KIconEffect iconEffect; pixmap = iconEffect.apply(pixmap, KIconLoader::Desktop, KIconLoader::DisabledState); - m_dolphinModel->setData(index, QIcon(pixmap), Qt::DecorationRole); + m_dirModel->setData(index, QIcon(pixmap), Qt::DecorationRole); } } } @@ -490,7 +491,7 @@ bool IconManager::applyImageFrame(QPixmap& icon) void IconManager::limitToSize(QPixmap& icon, const QSize& maxSize) { if ((icon.width() > maxSize.width()) || (icon.height() > maxSize.height())) { - icon = icon.scaled(maxSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); + icon = icon.scaled(maxSize, Qt::KeepAspectRatio, Qt::FastTransformation); } } @@ -500,8 +501,18 @@ void IconManager::startPreviewJob(const KFileItemList& items) return; } + const QMimeData* mimeData = QApplication::clipboard()->mimeData(); + m_hasCutSelection = KonqMimeData::decodeIsCutSelection(mimeData); + const QSize size = m_view->iconSize(); - KIO::PreviewJob* job = KIO::filePreview(items, 128, 128); + + // PreviewJob internally caches items always with the size of + // 128 x 128 pixels or 256 x 256 pixels. A downscaling is done + // by PreviewJob if a smaller size is requested. As the IconManager must + // do a downscaling anyhow because of the frame, only the provided + // cache sizes are requested. + const int cacheSize = (size.width() > 128) || (size.height() > 128) ? 256 : 128; + KIO::PreviewJob* job = KIO::filePreview(items, cacheSize, cacheSize); connect(job, SIGNAL(gotPreview(const KFileItem&, const QPixmap&)), this, SLOT(addToPreviewQueue(const KFileItem&, const QPixmap&))); connect(job, SIGNAL(finished(KJob*)), @@ -520,14 +531,72 @@ void IconManager::killPreviewJobs() m_previewJobs.clear(); } -bool IconManager::itemListContains(const KFileItemList& items, const KUrl& url) const +void IconManager::orderItems(KFileItemList& items) { - foreach (const KFileItem& item, items) { - if (url == item.url()) { - return true; + // Order the items in a way that the preview for the visible items + // is generated first, as this improves the feeled performance a lot. + // + // Implementation note: 2 different algorithms are used for the sorting. + // Algorithm 1 is faster when having a lot of items in comparison + // to the number of rows in the model. Algorithm 2 is faster + // when having quite less items in comparison to the number of rows in + // the model. Choosing the right algorithm is important when having directories + // with several hundreds or thousands of items. + + const int itemCount = items.count(); + const int rowCount = m_proxyModel->rowCount(); + const QRect visibleArea = m_view->viewport()->rect(); + + int insertPos = 0; + if (itemCount * 10 > rowCount) { + // Algorithm 1: The number of items is > 10 % of the row count. Parse all rows + // and check whether the received row is part of the item list. + for (int row = 0; row < rowCount; ++row) { + const QModelIndex proxyIndex = m_proxyModel->index(row, 0); + const QRect itemRect = m_view->visualRect(proxyIndex); + const QModelIndex dirIndex = m_proxyModel->mapToSource(proxyIndex); + + KFileItem item = m_dirModel->itemForIndex(dirIndex); // O(1) + const KUrl url = item.url(); + + // check whether the item is part of the item list 'items' + int index = -1; + for (int i = 0; i < itemCount; ++i) { + if (items[i].url() == url) { + index = i; + break; + } + } + + if ((index > 0) && itemRect.intersects(visibleArea)) { + // The current item is (at least partly) visible. Move it + // to the front of the list, so that the preview is + // generated earlier. + items.removeAt(index); + items.insert(insertPos, item); + ++insertPos; + ++m_pendingVisiblePreviews; + } + } + } else { + // Algorithm 2: The number of items is <= 10 % of the row count. In this case iterate + // all items and receive the corresponding row from the item. + for (int i = 0; i < itemCount; ++i) { + const QModelIndex dirIndex = m_dirModel->indexForItem(items[i]); // O(n) (n = number of rows) + const QModelIndex proxyIndex = m_proxyModel->mapFromSource(dirIndex); + const QRect itemRect = m_view->visualRect(proxyIndex); + + if (itemRect.intersects(visibleArea)) { + // The current item is (at least partly) visible. Move it + // to the front of the list, so that the preview is + // generated earlier. + items.insert(insertPos, items[i]); + items.removeAt(i + 1); + ++insertPos; + ++m_pendingVisiblePreviews; + } } } - return false; } #include "iconmanager.moc"