1 | /*************************************************************************** |
2 | * Copyright (C) 2011 by Peter Penz <peter.penz19@gmail.com> * |
3 | * * |
4 | * This program is free software; you can redistribute it and/or modify * |
5 | * it under the terms of the GNU General Public License as published by * |
6 | * the Free Software Foundation; either version 2 of the License, or * |
7 | * (at your option) any later version. * |
8 | * * |
9 | * This program 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 * |
12 | * GNU General Public License for more details. * |
13 | * * |
14 | * You should have received a copy of the GNU General Public License * |
15 | * along with this program; if not, write to the * |
16 | * Free Software Foundation, Inc., * |
17 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * |
18 | ***************************************************************************/ |
19 | |
20 | #include "kfileitemmodelrolesupdater.h" |
21 | |
22 | #include "kfileitemmodel.h" |
23 | |
24 | #include <KConfig> |
25 | #include <KConfigGroup> |
26 | #include <KDebug> |
27 | #include <KFileItem> |
28 | #include <KGlobal> |
29 | #include <KIO/JobUiDelegate> |
30 | #include <KIO/PreviewJob> |
31 | |
32 | #include "private/kpixmapmodifier.h" |
33 | #include "private/kdirectorycontentscounter.h" |
34 | |
35 | #include <QApplication> |
36 | #include <QPainter> |
37 | #include <QPixmap> |
38 | #include <QElapsedTimer> |
39 | #include <QTimer> |
40 | |
41 | #include <algorithm> |
42 | |
43 | #ifdef HAVE_BALOO |
44 | #include "private/kbaloorolesprovider.h" |
45 | #include <baloo/file.h> |
46 | #include <baloo/filefetchjob.h> |
47 | #include <baloo/filemonitor.h> |
48 | #endif |
49 | |
50 | // #define KFILEITEMMODELROLESUPDATER_DEBUG |
51 | |
52 | namespace { |
53 | // Maximum time in ms that the KFileItemModelRolesUpdater |
54 | // may perform a blocking operation |
55 | const int MaxBlockTimeout = 200; |
56 | |
57 | // If the number of items is smaller than ResolveAllItemsLimit, |
58 | // the roles of all items will be resolved. |
59 | const int ResolveAllItemsLimit = 500; |
60 | |
61 | // Not only the visible area, but up to ReadAheadPages before and after |
62 | // this area will be resolved. |
63 | const int ReadAheadPages = 5; |
64 | } |
65 | |
66 | KFileItemModelRolesUpdater::KFileItemModelRolesUpdater(KFileItemModel* model, QObject* parent) : |
67 | QObject(parent), |
68 | m_state(Idle), |
69 | m_previewChangedDuringPausing(false), |
70 | m_iconSizeChangedDuringPausing(false), |
71 | m_rolesChangedDuringPausing(false), |
72 | m_previewShown(false), |
73 | m_enlargeSmallPreviews(true), |
74 | m_clearPreviews(false), |
75 | m_finishedItems(), |
76 | m_model(model), |
77 | m_iconSize(), |
78 | m_firstVisibleIndex(0), |
79 | m_lastVisibleIndex(-1), |
80 | m_maximumVisibleItems(50), |
81 | m_roles(), |
82 | m_resolvableRoles(), |
83 | m_enabledPlugins(), |
84 | m_pendingSortRoleItems(), |
85 | m_pendingIndexes(), |
86 | m_pendingPreviewItems(), |
87 | m_previewJob(), |
88 | m_recentlyChangedItemsTimer(0), |
89 | m_recentlyChangedItems(), |
90 | m_changedItems(), |
91 | m_directoryContentsCounter(0) |
92 | #ifdef HAVE_BALOO |
93 | , m_balooFileMonitor(0) |
94 | #endif |
95 | { |
96 | Q_ASSERT(model); |
97 | |
98 | const KConfigGroup globalConfig(KGlobal::config(), "PreviewSettings" ); |
99 | m_enabledPlugins = globalConfig.readEntry("Plugins" , QStringList() |
100 | << "directorythumbnail" |
101 | << "imagethumbnail" |
102 | << "jpegthumbnail" ); |
103 | |
104 | connect(m_model, SIGNAL(itemsInserted(KItemRangeList)), |
105 | this, SLOT(slotItemsInserted(KItemRangeList))); |
106 | connect(m_model, SIGNAL(itemsRemoved(KItemRangeList)), |
107 | this, SLOT(slotItemsRemoved(KItemRangeList))); |
108 | connect(m_model, SIGNAL(itemsChanged(KItemRangeList,QSet<QByteArray>)), |
109 | this, SLOT(slotItemsChanged(KItemRangeList,QSet<QByteArray>))); |
110 | connect(m_model, SIGNAL(itemsMoved(KItemRange,QList<int>)), |
111 | this, SLOT(slotItemsMoved(KItemRange,QList<int>))); |
112 | connect(m_model, SIGNAL(sortRoleChanged(QByteArray,QByteArray)), |
113 | this, SLOT(slotSortRoleChanged(QByteArray,QByteArray))); |
114 | |
115 | // Use a timer to prevent that each call of slotItemsChanged() results in a synchronous |
116 | // resolving of the roles. Postpone the resolving until no update has been done for 1 second. |
117 | m_recentlyChangedItemsTimer = new QTimer(this); |
118 | m_recentlyChangedItemsTimer->setInterval(1000); |
119 | m_recentlyChangedItemsTimer->setSingleShot(true); |
120 | connect(m_recentlyChangedItemsTimer, SIGNAL(timeout()), this, SLOT(resolveRecentlyChangedItems())); |
121 | |
122 | m_resolvableRoles.insert("size" ); |
123 | m_resolvableRoles.insert("type" ); |
124 | m_resolvableRoles.insert("isExpandable" ); |
125 | #ifdef HAVE_BALOO |
126 | m_resolvableRoles += KBalooRolesProvider::instance().roles(); |
127 | #endif |
128 | |
129 | m_directoryContentsCounter = new KDirectoryContentsCounter(m_model, this); |
130 | connect(m_directoryContentsCounter, SIGNAL(result(QString,int)), |
131 | this, SLOT(slotDirectoryContentsCountReceived(QString,int))); |
132 | } |
133 | |
134 | KFileItemModelRolesUpdater::~KFileItemModelRolesUpdater() |
135 | { |
136 | killPreviewJob(); |
137 | } |
138 | |
139 | void KFileItemModelRolesUpdater::setIconSize(const QSize& size) |
140 | { |
141 | if (size != m_iconSize) { |
142 | m_iconSize = size; |
143 | if (m_state == Paused) { |
144 | m_iconSizeChangedDuringPausing = true; |
145 | } else if (m_previewShown) { |
146 | // An icon size change requires the regenerating of |
147 | // all previews |
148 | m_finishedItems.clear(); |
149 | startUpdating(); |
150 | } |
151 | } |
152 | } |
153 | |
154 | QSize KFileItemModelRolesUpdater::iconSize() const |
155 | { |
156 | return m_iconSize; |
157 | } |
158 | |
159 | void KFileItemModelRolesUpdater::setVisibleIndexRange(int index, int count) |
160 | { |
161 | if (index < 0) { |
162 | index = 0; |
163 | } |
164 | if (count < 0) { |
165 | count = 0; |
166 | } |
167 | |
168 | if (index == m_firstVisibleIndex && count == m_lastVisibleIndex - m_firstVisibleIndex + 1) { |
169 | // The range has not been changed |
170 | return; |
171 | } |
172 | |
173 | m_firstVisibleIndex = index; |
174 | m_lastVisibleIndex = qMin(index + count - 1, m_model->count() - 1); |
175 | |
176 | startUpdating(); |
177 | } |
178 | |
179 | void KFileItemModelRolesUpdater::setMaximumVisibleItems(int count) |
180 | { |
181 | m_maximumVisibleItems = count; |
182 | } |
183 | |
184 | void KFileItemModelRolesUpdater::setPreviewsShown(bool show) |
185 | { |
186 | if (show == m_previewShown) { |
187 | return; |
188 | } |
189 | |
190 | m_previewShown = show; |
191 | if (!show) { |
192 | m_clearPreviews = true; |
193 | } |
194 | |
195 | updateAllPreviews(); |
196 | } |
197 | |
198 | bool KFileItemModelRolesUpdater::previewsShown() const |
199 | { |
200 | return m_previewShown; |
201 | } |
202 | |
203 | void KFileItemModelRolesUpdater::setEnlargeSmallPreviews(bool enlarge) |
204 | { |
205 | if (enlarge != m_enlargeSmallPreviews) { |
206 | m_enlargeSmallPreviews = enlarge; |
207 | if (m_previewShown) { |
208 | updateAllPreviews(); |
209 | } |
210 | } |
211 | } |
212 | |
213 | bool KFileItemModelRolesUpdater::enlargeSmallPreviews() const |
214 | { |
215 | return m_enlargeSmallPreviews; |
216 | } |
217 | |
218 | void KFileItemModelRolesUpdater::setEnabledPlugins(const QStringList& list) |
219 | { |
220 | if (m_enabledPlugins != list) { |
221 | m_enabledPlugins = list; |
222 | if (m_previewShown) { |
223 | updateAllPreviews(); |
224 | } |
225 | } |
226 | } |
227 | |
228 | void KFileItemModelRolesUpdater::setPaused(bool paused) |
229 | { |
230 | if (paused == (m_state == Paused)) { |
231 | return; |
232 | } |
233 | |
234 | if (paused) { |
235 | m_state = Paused; |
236 | killPreviewJob(); |
237 | } else { |
238 | const bool updatePreviews = (m_iconSizeChangedDuringPausing && m_previewShown) || |
239 | m_previewChangedDuringPausing; |
240 | const bool resolveAll = updatePreviews || m_rolesChangedDuringPausing; |
241 | if (resolveAll) { |
242 | m_finishedItems.clear(); |
243 | } |
244 | |
245 | m_iconSizeChangedDuringPausing = false; |
246 | m_previewChangedDuringPausing = false; |
247 | m_rolesChangedDuringPausing = false; |
248 | |
249 | if (!m_pendingSortRoleItems.isEmpty()) { |
250 | m_state = ResolvingSortRole; |
251 | resolveNextSortRole(); |
252 | } else { |
253 | m_state = Idle; |
254 | } |
255 | |
256 | startUpdating(); |
257 | } |
258 | } |
259 | |
260 | void KFileItemModelRolesUpdater::setRoles(const QSet<QByteArray>& roles) |
261 | { |
262 | if (m_roles != roles) { |
263 | m_roles = roles; |
264 | |
265 | #ifdef HAVE_BALOO |
266 | // Check whether there is at least one role that must be resolved |
267 | // with the help of Baloo. If this is the case, a (quite expensive) |
268 | // resolving will be done in KFileItemModelRolesUpdater::rolesData() and |
269 | // the role gets watched for changes. |
270 | const KBalooRolesProvider& rolesProvider = KBalooRolesProvider::instance(); |
271 | bool hasBalooRole = false; |
272 | QSetIterator<QByteArray> it(roles); |
273 | while (it.hasNext()) { |
274 | const QByteArray& role = it.next(); |
275 | if (rolesProvider.roles().contains(role)) { |
276 | hasBalooRole = true; |
277 | break; |
278 | } |
279 | } |
280 | |
281 | if (hasBalooRole && !m_balooFileMonitor) { |
282 | m_balooFileMonitor = new Baloo::FileMonitor(this); |
283 | connect(m_balooFileMonitor, SIGNAL(fileMetaDataChanged(QString)), |
284 | this, SLOT(applyChangedBalooRoles(QString))); |
285 | } else if (!hasBalooRole && m_balooFileMonitor) { |
286 | delete m_balooFileMonitor; |
287 | m_balooFileMonitor = 0; |
288 | } |
289 | #endif |
290 | |
291 | if (m_state == Paused) { |
292 | m_rolesChangedDuringPausing = true; |
293 | } else { |
294 | startUpdating(); |
295 | } |
296 | } |
297 | } |
298 | |
299 | QSet<QByteArray> KFileItemModelRolesUpdater::roles() const |
300 | { |
301 | return m_roles; |
302 | } |
303 | |
304 | bool KFileItemModelRolesUpdater::isPaused() const |
305 | { |
306 | return m_state == Paused; |
307 | } |
308 | |
309 | QStringList KFileItemModelRolesUpdater::enabledPlugins() const |
310 | { |
311 | return m_enabledPlugins; |
312 | } |
313 | |
314 | void KFileItemModelRolesUpdater::slotItemsInserted(const KItemRangeList& itemRanges) |
315 | { |
316 | QElapsedTimer timer; |
317 | timer.start(); |
318 | |
319 | // Determine the sort role synchronously for as many items as possible. |
320 | if (m_resolvableRoles.contains(m_model->sortRole())) { |
321 | int insertedCount = 0; |
322 | foreach (const KItemRange& range, itemRanges) { |
323 | const int lastIndex = insertedCount + range.index + range.count - 1; |
324 | for (int i = insertedCount + range.index; i <= lastIndex; ++i) { |
325 | if (timer.elapsed() < MaxBlockTimeout) { |
326 | applySortRole(i); |
327 | } else { |
328 | m_pendingSortRoleItems.insert(m_model->fileItem(i)); |
329 | } |
330 | } |
331 | insertedCount += range.count; |
332 | } |
333 | |
334 | applySortProgressToModel(); |
335 | |
336 | // If there are still items whose sort role is unknown, check if the |
337 | // asynchronous determination of the sort role is already in progress, |
338 | // and start it if that is not the case. |
339 | if (!m_pendingSortRoleItems.isEmpty() && m_state != ResolvingSortRole) { |
340 | killPreviewJob(); |
341 | m_state = ResolvingSortRole; |
342 | resolveNextSortRole(); |
343 | } |
344 | } |
345 | |
346 | startUpdating(); |
347 | } |
348 | |
349 | void KFileItemModelRolesUpdater::slotItemsRemoved(const KItemRangeList& itemRanges) |
350 | { |
351 | Q_UNUSED(itemRanges); |
352 | |
353 | const bool allItemsRemoved = (m_model->count() == 0); |
354 | |
355 | #ifdef HAVE_BALOO |
356 | if (m_balooFileMonitor) { |
357 | // Don't let the FileWatcher watch for removed items |
358 | if (allItemsRemoved) { |
359 | m_balooFileMonitor->clear(); |
360 | } else { |
361 | QStringList newFileList; |
362 | foreach (const QString& itemUrl, m_balooFileMonitor->files()) { |
363 | if (m_model->index(itemUrl) >= 0) { |
364 | newFileList.append(itemUrl); |
365 | } |
366 | } |
367 | m_balooFileMonitor->setFiles(newFileList); |
368 | } |
369 | } |
370 | #endif |
371 | |
372 | if (allItemsRemoved) { |
373 | m_state = Idle; |
374 | |
375 | m_finishedItems.clear(); |
376 | m_pendingSortRoleItems.clear(); |
377 | m_pendingIndexes.clear(); |
378 | m_pendingPreviewItems.clear(); |
379 | m_recentlyChangedItems.clear(); |
380 | m_recentlyChangedItemsTimer->stop(); |
381 | m_changedItems.clear(); |
382 | |
383 | killPreviewJob(); |
384 | } else { |
385 | // Only remove the items from m_finishedItems. They will be removed |
386 | // from the other sets later on. |
387 | QSet<KFileItem>::iterator it = m_finishedItems.begin(); |
388 | while (it != m_finishedItems.end()) { |
389 | if (m_model->index(*it) < 0) { |
390 | it = m_finishedItems.erase(it); |
391 | } else { |
392 | ++it; |
393 | } |
394 | } |
395 | |
396 | // The visible items might have changed. |
397 | startUpdating(); |
398 | } |
399 | } |
400 | |
401 | void KFileItemModelRolesUpdater::slotItemsMoved(const KItemRange& itemRange, QList<int> movedToIndexes) |
402 | { |
403 | Q_UNUSED(itemRange); |
404 | Q_UNUSED(movedToIndexes); |
405 | |
406 | // The visible items might have changed. |
407 | startUpdating(); |
408 | } |
409 | |
410 | void KFileItemModelRolesUpdater::slotItemsChanged(const KItemRangeList& itemRanges, |
411 | const QSet<QByteArray>& roles) |
412 | { |
413 | Q_UNUSED(roles); |
414 | |
415 | // Find out if slotItemsChanged() has been done recently. If that is the |
416 | // case, resolving the roles is postponed until a timer has exceeded |
417 | // to prevent expensive repeated updates if files are updated frequently. |
418 | const bool itemsChangedRecently = m_recentlyChangedItemsTimer->isActive(); |
419 | |
420 | QSet<KFileItem>& targetSet = itemsChangedRecently ? m_recentlyChangedItems : m_changedItems; |
421 | |
422 | foreach (const KItemRange& itemRange, itemRanges) { |
423 | int index = itemRange.index; |
424 | for (int count = itemRange.count; count > 0; --count) { |
425 | const KFileItem item = m_model->fileItem(index); |
426 | targetSet.insert(item); |
427 | ++index; |
428 | } |
429 | } |
430 | |
431 | m_recentlyChangedItemsTimer->start(); |
432 | |
433 | if (!itemsChangedRecently) { |
434 | updateChangedItems(); |
435 | } |
436 | } |
437 | |
438 | void KFileItemModelRolesUpdater::slotSortRoleChanged(const QByteArray& current, |
439 | const QByteArray& previous) |
440 | { |
441 | Q_UNUSED(current); |
442 | Q_UNUSED(previous); |
443 | |
444 | if (m_resolvableRoles.contains(current)) { |
445 | m_pendingSortRoleItems.clear(); |
446 | m_finishedItems.clear(); |
447 | |
448 | const int count = m_model->count(); |
449 | QElapsedTimer timer; |
450 | timer.start(); |
451 | |
452 | // Determine the sort role synchronously for as many items as possible. |
453 | for (int index = 0; index < count; ++index) { |
454 | if (timer.elapsed() < MaxBlockTimeout) { |
455 | applySortRole(index); |
456 | } else { |
457 | m_pendingSortRoleItems.insert(m_model->fileItem(index)); |
458 | } |
459 | } |
460 | |
461 | applySortProgressToModel(); |
462 | |
463 | if (!m_pendingSortRoleItems.isEmpty()) { |
464 | // Trigger the asynchronous determination of the sort role. |
465 | killPreviewJob(); |
466 | m_state = ResolvingSortRole; |
467 | resolveNextSortRole(); |
468 | } |
469 | } else { |
470 | m_state = Idle; |
471 | m_pendingSortRoleItems.clear(); |
472 | applySortProgressToModel(); |
473 | } |
474 | } |
475 | |
476 | void KFileItemModelRolesUpdater::slotGotPreview(const KFileItem& item, const QPixmap& pixmap) |
477 | { |
478 | if (m_state != PreviewJobRunning) { |
479 | return; |
480 | } |
481 | |
482 | m_changedItems.remove(item); |
483 | |
484 | const int index = m_model->index(item); |
485 | if (index < 0) { |
486 | return; |
487 | } |
488 | |
489 | QPixmap scaledPixmap = pixmap; |
490 | |
491 | const QString mimeType = item.mimetype(); |
492 | const int slashIndex = mimeType.indexOf(QLatin1Char('/')); |
493 | const QString mimeTypeGroup = mimeType.left(slashIndex); |
494 | if (mimeTypeGroup == QLatin1String("image" )) { |
495 | if (m_enlargeSmallPreviews) { |
496 | KPixmapModifier::applyFrame(scaledPixmap, m_iconSize); |
497 | } else { |
498 | // Assure that small previews don't get enlarged. Instead they |
499 | // should be shown centered within the frame. |
500 | const QSize contentSize = KPixmapModifier::sizeInsideFrame(m_iconSize); |
501 | const bool enlargingRequired = scaledPixmap.width() < contentSize.width() && |
502 | scaledPixmap.height() < contentSize.height(); |
503 | if (enlargingRequired) { |
504 | QSize frameSize = scaledPixmap.size(); |
505 | frameSize.scale(m_iconSize, Qt::KeepAspectRatio); |
506 | |
507 | QPixmap largeFrame(frameSize); |
508 | largeFrame.fill(Qt::transparent); |
509 | |
510 | KPixmapModifier::applyFrame(largeFrame, frameSize); |
511 | |
512 | QPainter painter(&largeFrame); |
513 | painter.drawPixmap((largeFrame.width() - scaledPixmap.width()) / 2, |
514 | (largeFrame.height() - scaledPixmap.height()) / 2, |
515 | scaledPixmap); |
516 | scaledPixmap = largeFrame; |
517 | } else { |
518 | // The image must be shrinked as it is too large to fit into |
519 | // the available icon size |
520 | KPixmapModifier::applyFrame(scaledPixmap, m_iconSize); |
521 | } |
522 | } |
523 | } else { |
524 | KPixmapModifier::scale(scaledPixmap, m_iconSize); |
525 | } |
526 | |
527 | QHash<QByteArray, QVariant> data = rolesData(item); |
528 | |
529 | const QStringList overlays = data["iconOverlays" ].toStringList(); |
530 | // Strangely KFileItem::overlays() returns empty string-values, so |
531 | // we need to check first whether an overlay must be drawn at all. |
532 | // It is more efficient to do it here, as KIconLoader::drawOverlays() |
533 | // assumes that an overlay will be drawn and has some additional |
534 | // setup time. |
535 | foreach (const QString& overlay, overlays) { |
536 | if (!overlay.isEmpty()) { |
537 | // There is at least one overlay, draw all overlays above m_pixmap |
538 | // and cancel the check |
539 | KIconLoader::global()->drawOverlays(overlays, scaledPixmap, KIconLoader::Desktop); |
540 | break; |
541 | } |
542 | } |
543 | |
544 | data.insert("iconPixmap" , scaledPixmap); |
545 | |
546 | disconnect(m_model, SIGNAL(itemsChanged(KItemRangeList,QSet<QByteArray>)), |
547 | this, SLOT(slotItemsChanged(KItemRangeList,QSet<QByteArray>))); |
548 | m_model->setData(index, data); |
549 | connect(m_model, SIGNAL(itemsChanged(KItemRangeList,QSet<QByteArray>)), |
550 | this, SLOT(slotItemsChanged(KItemRangeList,QSet<QByteArray>))); |
551 | |
552 | m_finishedItems.insert(item); |
553 | } |
554 | |
555 | void KFileItemModelRolesUpdater::slotPreviewFailed(const KFileItem& item) |
556 | { |
557 | if (m_state != PreviewJobRunning) { |
558 | return; |
559 | } |
560 | |
561 | m_changedItems.remove(item); |
562 | |
563 | const int index = m_model->index(item); |
564 | if (index >= 0) { |
565 | QHash<QByteArray, QVariant> data; |
566 | data.insert("iconPixmap" , QPixmap()); |
567 | |
568 | disconnect(m_model, SIGNAL(itemsChanged(KItemRangeList,QSet<QByteArray>)), |
569 | this, SLOT(slotItemsChanged(KItemRangeList,QSet<QByteArray>))); |
570 | m_model->setData(index, data); |
571 | connect(m_model, SIGNAL(itemsChanged(KItemRangeList,QSet<QByteArray>)), |
572 | this, SLOT(slotItemsChanged(KItemRangeList,QSet<QByteArray>))); |
573 | |
574 | applyResolvedRoles(index, ResolveAll); |
575 | m_finishedItems.insert(item); |
576 | } |
577 | } |
578 | |
579 | void KFileItemModelRolesUpdater::slotPreviewJobFinished() |
580 | { |
581 | m_previewJob = 0; |
582 | |
583 | if (m_state != PreviewJobRunning) { |
584 | return; |
585 | } |
586 | |
587 | m_state = Idle; |
588 | |
589 | if (!m_pendingPreviewItems.isEmpty()) { |
590 | startPreviewJob(); |
591 | } else { |
592 | if (!m_changedItems.isEmpty()) { |
593 | updateChangedItems(); |
594 | } |
595 | } |
596 | } |
597 | |
598 | void KFileItemModelRolesUpdater::resolveNextSortRole() |
599 | { |
600 | if (m_state != ResolvingSortRole) { |
601 | return; |
602 | } |
603 | |
604 | QSet<KFileItem>::iterator it = m_pendingSortRoleItems.begin(); |
605 | while (it != m_pendingSortRoleItems.end()) { |
606 | const KFileItem item = *it; |
607 | const int index = m_model->index(item); |
608 | |
609 | // Continue if the sort role has already been determined for the |
610 | // item, and the item has not been changed recently. |
611 | if (!m_changedItems.contains(item) && m_model->data(index).contains(m_model->sortRole())) { |
612 | it = m_pendingSortRoleItems.erase(it); |
613 | continue; |
614 | } |
615 | |
616 | applySortRole(index); |
617 | m_pendingSortRoleItems.erase(it); |
618 | break; |
619 | } |
620 | |
621 | if (!m_pendingSortRoleItems.isEmpty()) { |
622 | applySortProgressToModel(); |
623 | QTimer::singleShot(0, this, SLOT(resolveNextSortRole())); |
624 | } else { |
625 | m_state = Idle; |
626 | |
627 | // Prevent that we try to update the items twice. |
628 | disconnect(m_model, SIGNAL(itemsMoved(KItemRange,QList<int>)), |
629 | this, SLOT(slotItemsMoved(KItemRange,QList<int>))); |
630 | applySortProgressToModel(); |
631 | connect(m_model, SIGNAL(itemsMoved(KItemRange,QList<int>)), |
632 | this, SLOT(slotItemsMoved(KItemRange,QList<int>))); |
633 | startUpdating(); |
634 | } |
635 | } |
636 | |
637 | void KFileItemModelRolesUpdater::resolveNextPendingRoles() |
638 | { |
639 | if (m_state != ResolvingAllRoles) { |
640 | return; |
641 | } |
642 | |
643 | while (!m_pendingIndexes.isEmpty()) { |
644 | const int index = m_pendingIndexes.takeFirst(); |
645 | const KFileItem item = m_model->fileItem(index); |
646 | |
647 | if (m_finishedItems.contains(item)) { |
648 | continue; |
649 | } |
650 | |
651 | applyResolvedRoles(index, ResolveAll); |
652 | m_finishedItems.insert(item); |
653 | m_changedItems.remove(item); |
654 | break; |
655 | } |
656 | |
657 | if (!m_pendingIndexes.isEmpty()) { |
658 | QTimer::singleShot(0, this, SLOT(resolveNextPendingRoles())); |
659 | } else { |
660 | m_state = Idle; |
661 | |
662 | if (m_clearPreviews) { |
663 | // Only go through the list if there are items which might still have previews. |
664 | if (m_finishedItems.count() != m_model->count()) { |
665 | QHash<QByteArray, QVariant> data; |
666 | data.insert("iconPixmap" , QPixmap()); |
667 | |
668 | disconnect(m_model, SIGNAL(itemsChanged(KItemRangeList,QSet<QByteArray>)), |
669 | this, SLOT(slotItemsChanged(KItemRangeList,QSet<QByteArray>))); |
670 | for (int index = 0; index <= m_model->count(); ++index) { |
671 | if (m_model->data(index).contains("iconPixmap" )) { |
672 | m_model->setData(index, data); |
673 | } |
674 | } |
675 | connect(m_model, SIGNAL(itemsChanged(KItemRangeList,QSet<QByteArray>)), |
676 | this, SLOT(slotItemsChanged(KItemRangeList,QSet<QByteArray>))); |
677 | |
678 | } |
679 | m_clearPreviews = false; |
680 | } |
681 | |
682 | if (!m_changedItems.isEmpty()) { |
683 | updateChangedItems(); |
684 | } |
685 | } |
686 | } |
687 | |
688 | void KFileItemModelRolesUpdater::resolveRecentlyChangedItems() |
689 | { |
690 | m_changedItems += m_recentlyChangedItems; |
691 | m_recentlyChangedItems.clear(); |
692 | updateChangedItems(); |
693 | } |
694 | |
695 | void KFileItemModelRolesUpdater::applyChangedBalooRoles(const QString& itemUrl) |
696 | { |
697 | #ifdef HAVE_BALOO |
698 | const KFileItem item = m_model->fileItem(itemUrl); |
699 | |
700 | if (item.isNull()) { |
701 | // itemUrl is not in the model anymore, probably because |
702 | // the corresponding file has been deleted in the meantime. |
703 | return; |
704 | } |
705 | |
706 | Baloo::FileFetchJob* job = new Baloo::FileFetchJob(item.localPath()); |
707 | connect(job, SIGNAL(finished(KJob*)), this, SLOT(applyChangedBalooRolesJobFinished(KJob*))); |
708 | job->setProperty("item" , QVariant::fromValue(item)); |
709 | job->start(); |
710 | #else |
711 | #ifndef Q_CC_MSVC |
712 | Q_UNUSED(itemUrl); |
713 | #endif |
714 | #endif |
715 | } |
716 | |
717 | void KFileItemModelRolesUpdater::applyChangedBalooRolesJobFinished(KJob* kjob) |
718 | { |
719 | #ifdef HAVE_BALOO |
720 | const KFileItem item = kjob->property("item" ).value<KFileItem>(); |
721 | |
722 | const KBalooRolesProvider& rolesProvider = KBalooRolesProvider::instance(); |
723 | QHash<QByteArray, QVariant> data; |
724 | |
725 | foreach (const QByteArray& role, rolesProvider.roles()) { |
726 | // Overwrite all the role values with an empty QVariant, because the roles |
727 | // provider doesn't overwrite it when the property value list is empty. |
728 | // See bug 322348 |
729 | data.insert(role, QVariant()); |
730 | } |
731 | |
732 | Baloo::FileFetchJob* job = static_cast<Baloo::FileFetchJob*>(kjob); |
733 | QHashIterator<QByteArray, QVariant> it(rolesProvider.roleValues(job->file(), m_roles)); |
734 | while (it.hasNext()) { |
735 | it.next(); |
736 | data.insert(it.key(), it.value()); |
737 | } |
738 | |
739 | disconnect(m_model, SIGNAL(itemsChanged(KItemRangeList,QSet<QByteArray>)), |
740 | this, SLOT(slotItemsChanged(KItemRangeList,QSet<QByteArray>))); |
741 | const int index = m_model->index(item); |
742 | m_model->setData(index, data); |
743 | connect(m_model, SIGNAL(itemsChanged(KItemRangeList,QSet<QByteArray>)), |
744 | this, SLOT(slotItemsChanged(KItemRangeList,QSet<QByteArray>))); |
745 | #endif |
746 | } |
747 | |
748 | void KFileItemModelRolesUpdater::slotDirectoryContentsCountReceived(const QString& path, int count) |
749 | { |
750 | const bool getSizeRole = m_roles.contains("size" ); |
751 | const bool getIsExpandableRole = m_roles.contains("isExpandable" ); |
752 | |
753 | if (getSizeRole || getIsExpandableRole) { |
754 | const int index = m_model->index(KUrl(path)); |
755 | if (index >= 0) { |
756 | QHash<QByteArray, QVariant> data; |
757 | |
758 | if (getSizeRole) { |
759 | data.insert("size" , count); |
760 | } |
761 | if (getIsExpandableRole) { |
762 | data.insert("isExpandable" , count > 0); |
763 | } |
764 | |
765 | disconnect(m_model, SIGNAL(itemsChanged(KItemRangeList,QSet<QByteArray>)), |
766 | this, SLOT(slotItemsChanged(KItemRangeList,QSet<QByteArray>))); |
767 | m_model->setData(index, data); |
768 | connect(m_model, SIGNAL(itemsChanged(KItemRangeList,QSet<QByteArray>)), |
769 | this, SLOT(slotItemsChanged(KItemRangeList,QSet<QByteArray>))); |
770 | } |
771 | } |
772 | } |
773 | |
774 | void KFileItemModelRolesUpdater::startUpdating() |
775 | { |
776 | if (m_state == Paused) { |
777 | return; |
778 | } |
779 | |
780 | if (m_finishedItems.count() == m_model->count()) { |
781 | // All roles have been resolved already. |
782 | m_state = Idle; |
783 | return; |
784 | } |
785 | |
786 | // Terminate all updates that are currently active. |
787 | killPreviewJob(); |
788 | m_pendingIndexes.clear(); |
789 | |
790 | QElapsedTimer timer; |
791 | timer.start(); |
792 | |
793 | // Determine the icons for the visible items synchronously. |
794 | updateVisibleIcons(); |
795 | |
796 | // A detailed update of the items in and near the visible area |
797 | // only makes sense if sorting is finished. |
798 | if (m_state == ResolvingSortRole) { |
799 | return; |
800 | } |
801 | |
802 | // Start the preview job or the asynchronous resolving of all roles. |
803 | QList<int> indexes = indexesToResolve(); |
804 | |
805 | if (m_previewShown) { |
806 | m_pendingPreviewItems.clear(); |
807 | m_pendingPreviewItems.reserve(indexes.count()); |
808 | |
809 | foreach (int index, indexes) { |
810 | const KFileItem item = m_model->fileItem(index); |
811 | if (!m_finishedItems.contains(item)) { |
812 | m_pendingPreviewItems.append(item); |
813 | } |
814 | } |
815 | |
816 | startPreviewJob(); |
817 | } else { |
818 | m_pendingIndexes = indexes; |
819 | // Trigger the asynchronous resolving of all roles. |
820 | m_state = ResolvingAllRoles; |
821 | QTimer::singleShot(0, this, SLOT(resolveNextPendingRoles())); |
822 | } |
823 | } |
824 | |
825 | void KFileItemModelRolesUpdater::updateVisibleIcons() |
826 | { |
827 | int lastVisibleIndex = m_lastVisibleIndex; |
828 | if (lastVisibleIndex <= 0) { |
829 | // Guess a reasonable value for the last visible index if the view |
830 | // has not told us about the real value yet. |
831 | lastVisibleIndex = qMin(m_firstVisibleIndex + m_maximumVisibleItems, m_model->count() - 1); |
832 | if (lastVisibleIndex <= 0) { |
833 | lastVisibleIndex = qMin(200, m_model->count() - 1); |
834 | } |
835 | } |
836 | |
837 | QElapsedTimer timer; |
838 | timer.start(); |
839 | |
840 | // Try to determine the final icons for all visible items. |
841 | int index; |
842 | for (index = m_firstVisibleIndex; index <= lastVisibleIndex && timer.elapsed() < MaxBlockTimeout; ++index) { |
843 | applyResolvedRoles(index, ResolveFast); |
844 | } |
845 | |
846 | // KFileItemListView::initializeItemListWidget(KItemListWidget*) will load |
847 | // preliminary icons (i.e., without mime type determination) for the |
848 | // remaining items. |
849 | } |
850 | |
851 | void KFileItemModelRolesUpdater::startPreviewJob() |
852 | { |
853 | m_state = PreviewJobRunning; |
854 | |
855 | if (m_pendingPreviewItems.isEmpty()) { |
856 | QTimer::singleShot(0, this, SLOT(slotPreviewJobFinished())); |
857 | return; |
858 | } |
859 | |
860 | // PreviewJob internally caches items always with the size of |
861 | // 128 x 128 pixels or 256 x 256 pixels. A (slow) downscaling is done |
862 | // by PreviewJob if a smaller size is requested. For images KFileItemModelRolesUpdater must |
863 | // do a downscaling anyhow because of the frame, so in this case only the provided |
864 | // cache sizes are requested. |
865 | const QSize cacheSize = (m_iconSize.width() > 128) || (m_iconSize.height() > 128) |
866 | ? QSize(256, 256) : QSize(128, 128); |
867 | |
868 | // KIO::filePreview() will request the MIME-type of all passed items, which (in the |
869 | // worst case) might block the application for several seconds. To prevent such |
870 | // a blocking, we only pass items with known mime type to the preview job. |
871 | const int count = m_pendingPreviewItems.count(); |
872 | KFileItemList itemSubSet; |
873 | itemSubSet.reserve(count); |
874 | |
875 | if (m_pendingPreviewItems.first().isMimeTypeKnown()) { |
876 | // Some mime types are known already, probably because they were |
877 | // determined when loading the icons for the visible items. Start |
878 | // a preview job for all items at the beginning of the list which |
879 | // have a known mime type. |
880 | do { |
881 | itemSubSet.append(m_pendingPreviewItems.takeFirst()); |
882 | } while (!m_pendingPreviewItems.isEmpty() && m_pendingPreviewItems.first().isMimeTypeKnown()); |
883 | } else { |
884 | // Determine mime types for MaxBlockTimeout ms, and start a preview |
885 | // job for the corresponding items. |
886 | QElapsedTimer timer; |
887 | timer.start(); |
888 | |
889 | do { |
890 | const KFileItem item = m_pendingPreviewItems.takeFirst(); |
891 | item.determineMimeType(); |
892 | itemSubSet.append(item); |
893 | } while (!m_pendingPreviewItems.isEmpty() && timer.elapsed() < MaxBlockTimeout); |
894 | } |
895 | |
896 | KIO::PreviewJob* job = new KIO::PreviewJob(itemSubSet, cacheSize, &m_enabledPlugins); |
897 | |
898 | job->setIgnoreMaximumSize(itemSubSet.first().isLocalFile()); |
899 | if (job->ui()) { |
900 | job->ui()->setWindow(qApp->activeWindow()); |
901 | } |
902 | |
903 | connect(job, SIGNAL(gotPreview(KFileItem,QPixmap)), |
904 | this, SLOT(slotGotPreview(KFileItem,QPixmap))); |
905 | connect(job, SIGNAL(failed(KFileItem)), |
906 | this, SLOT(slotPreviewFailed(KFileItem))); |
907 | connect(job, SIGNAL(finished(KJob*)), |
908 | this, SLOT(slotPreviewJobFinished())); |
909 | |
910 | m_previewJob = job; |
911 | } |
912 | |
913 | void KFileItemModelRolesUpdater::updateChangedItems() |
914 | { |
915 | if (m_state == Paused) { |
916 | return; |
917 | } |
918 | |
919 | if (m_changedItems.isEmpty()) { |
920 | return; |
921 | } |
922 | |
923 | m_finishedItems -= m_changedItems; |
924 | |
925 | if (m_resolvableRoles.contains(m_model->sortRole())) { |
926 | m_pendingSortRoleItems += m_changedItems; |
927 | |
928 | if (m_state != ResolvingSortRole) { |
929 | // Stop the preview job if necessary, and trigger the |
930 | // asynchronous determination of the sort role. |
931 | killPreviewJob(); |
932 | m_state = ResolvingSortRole; |
933 | QTimer::singleShot(0, this, SLOT(resolveNextSortRole())); |
934 | } |
935 | |
936 | return; |
937 | } |
938 | |
939 | QList<int> visibleChangedIndexes; |
940 | QList<int> invisibleChangedIndexes; |
941 | |
942 | foreach (const KFileItem& item, m_changedItems) { |
943 | const int index = m_model->index(item); |
944 | |
945 | if (index < 0) { |
946 | m_changedItems.remove(item); |
947 | continue; |
948 | } |
949 | |
950 | if (index >= m_firstVisibleIndex && index <= m_lastVisibleIndex) { |
951 | visibleChangedIndexes.append(index); |
952 | } else { |
953 | invisibleChangedIndexes.append(index); |
954 | } |
955 | } |
956 | |
957 | std::sort(visibleChangedIndexes.begin(), visibleChangedIndexes.end()); |
958 | |
959 | if (m_previewShown) { |
960 | foreach (int index, visibleChangedIndexes) { |
961 | m_pendingPreviewItems.append(m_model->fileItem(index)); |
962 | } |
963 | |
964 | foreach (int index, invisibleChangedIndexes) { |
965 | m_pendingPreviewItems.append(m_model->fileItem(index)); |
966 | } |
967 | |
968 | if (!m_previewJob) { |
969 | startPreviewJob(); |
970 | } |
971 | } else { |
972 | const bool resolvingInProgress = !m_pendingIndexes.isEmpty(); |
973 | m_pendingIndexes = visibleChangedIndexes + m_pendingIndexes + invisibleChangedIndexes; |
974 | if (!resolvingInProgress) { |
975 | // Trigger the asynchronous resolving of the changed roles. |
976 | m_state = ResolvingAllRoles; |
977 | QTimer::singleShot(0, this, SLOT(resolveNextPendingRoles())); |
978 | } |
979 | } |
980 | } |
981 | |
982 | void KFileItemModelRolesUpdater::applySortRole(int index) |
983 | { |
984 | QHash<QByteArray, QVariant> data; |
985 | const KFileItem item = m_model->fileItem(index); |
986 | |
987 | if (m_model->sortRole() == "type" ) { |
988 | if (!item.isMimeTypeKnown()) { |
989 | item.determineMimeType(); |
990 | } |
991 | |
992 | data.insert("type" , item.mimeComment()); |
993 | } else if (m_model->sortRole() == "size" && item.isLocalFile() && item.isDir()) { |
994 | const QString path = item.localPath(); |
995 | data.insert("size" , m_directoryContentsCounter->countDirectoryContentsSynchronously(path)); |
996 | } else { |
997 | // Probably the sort role is a baloo role - just determine all roles. |
998 | data = rolesData(item); |
999 | } |
1000 | |
1001 | disconnect(m_model, SIGNAL(itemsChanged(KItemRangeList,QSet<QByteArray>)), |
1002 | this, SLOT(slotItemsChanged(KItemRangeList,QSet<QByteArray>))); |
1003 | m_model->setData(index, data); |
1004 | connect(m_model, SIGNAL(itemsChanged(KItemRangeList,QSet<QByteArray>)), |
1005 | this, SLOT(slotItemsChanged(KItemRangeList,QSet<QByteArray>))); |
1006 | } |
1007 | |
1008 | void KFileItemModelRolesUpdater::applySortProgressToModel() |
1009 | { |
1010 | // Inform the model about the progress of the resolved items, |
1011 | // so that it can give an indication when the sorting has been finished. |
1012 | const int resolvedCount = m_model->count() - m_pendingSortRoleItems.count(); |
1013 | m_model->emitSortProgress(resolvedCount); |
1014 | } |
1015 | |
1016 | bool KFileItemModelRolesUpdater::applyResolvedRoles(int index, ResolveHint hint) |
1017 | { |
1018 | const KFileItem item = m_model->fileItem(index); |
1019 | const bool resolveAll = (hint == ResolveAll); |
1020 | |
1021 | bool iconChanged = false; |
1022 | if (!item.isMimeTypeKnown() || !item.isFinalIconKnown()) { |
1023 | item.determineMimeType(); |
1024 | iconChanged = true; |
1025 | } else if (!m_model->data(index).contains("iconName" )) { |
1026 | iconChanged = true; |
1027 | } |
1028 | |
1029 | if (iconChanged || resolveAll || m_clearPreviews) { |
1030 | if (index < 0) { |
1031 | return false; |
1032 | } |
1033 | |
1034 | QHash<QByteArray, QVariant> data; |
1035 | if (resolveAll) { |
1036 | data = rolesData(item); |
1037 | } |
1038 | |
1039 | data.insert("iconName" , item.iconName()); |
1040 | |
1041 | if (m_clearPreviews) { |
1042 | data.insert("iconPixmap" , QPixmap()); |
1043 | } |
1044 | |
1045 | disconnect(m_model, SIGNAL(itemsChanged(KItemRangeList,QSet<QByteArray>)), |
1046 | this, SLOT(slotItemsChanged(KItemRangeList,QSet<QByteArray>))); |
1047 | m_model->setData(index, data); |
1048 | connect(m_model, SIGNAL(itemsChanged(KItemRangeList,QSet<QByteArray>)), |
1049 | this, SLOT(slotItemsChanged(KItemRangeList,QSet<QByteArray>))); |
1050 | return true; |
1051 | } |
1052 | |
1053 | return false; |
1054 | } |
1055 | |
1056 | QHash<QByteArray, QVariant> KFileItemModelRolesUpdater::rolesData(const KFileItem& item) |
1057 | { |
1058 | QHash<QByteArray, QVariant> data; |
1059 | |
1060 | const bool getSizeRole = m_roles.contains("size" ); |
1061 | const bool getIsExpandableRole = m_roles.contains("isExpandable" ); |
1062 | |
1063 | if ((getSizeRole || getIsExpandableRole) && item.isDir()) { |
1064 | if (item.isLocalFile()) { |
1065 | // Tell m_directoryContentsCounter that we want to count the items |
1066 | // inside the directory. The result will be received in slotDirectoryContentsCountReceived. |
1067 | const QString path = item.localPath(); |
1068 | m_directoryContentsCounter->addDirectory(path); |
1069 | } else if (getSizeRole) { |
1070 | data.insert("size" , -1); // -1 indicates an unknown number of items |
1071 | } |
1072 | } |
1073 | |
1074 | if (m_roles.contains("type" )) { |
1075 | data.insert("type" , item.mimeComment()); |
1076 | } |
1077 | |
1078 | data.insert("iconOverlays" , item.overlays()); |
1079 | |
1080 | #ifdef HAVE_BALOO |
1081 | if (m_balooFileMonitor) { |
1082 | m_balooFileMonitor->addFile(item.localPath()); |
1083 | applyChangedBalooRoles(item.localPath()); |
1084 | } |
1085 | #endif |
1086 | return data; |
1087 | } |
1088 | |
1089 | void KFileItemModelRolesUpdater::updateAllPreviews() |
1090 | { |
1091 | if (m_state == Paused) { |
1092 | m_previewChangedDuringPausing = true; |
1093 | } else { |
1094 | m_finishedItems.clear(); |
1095 | startUpdating(); |
1096 | } |
1097 | } |
1098 | |
1099 | void KFileItemModelRolesUpdater::killPreviewJob() |
1100 | { |
1101 | if (m_previewJob) { |
1102 | disconnect(m_previewJob, SIGNAL(gotPreview(KFileItem,QPixmap)), |
1103 | this, SLOT(slotGotPreview(KFileItem,QPixmap))); |
1104 | disconnect(m_previewJob, SIGNAL(failed(KFileItem)), |
1105 | this, SLOT(slotPreviewFailed(KFileItem))); |
1106 | disconnect(m_previewJob, SIGNAL(finished(KJob*)), |
1107 | this, SLOT(slotPreviewJobFinished())); |
1108 | m_previewJob->kill(); |
1109 | m_previewJob = 0; |
1110 | m_pendingPreviewItems.clear(); |
1111 | } |
1112 | } |
1113 | |
1114 | QList<int> KFileItemModelRolesUpdater::indexesToResolve() const |
1115 | { |
1116 | const int count = m_model->count(); |
1117 | |
1118 | QList<int> result; |
1119 | result.reserve(ResolveAllItemsLimit); |
1120 | |
1121 | // Add visible items. |
1122 | for (int i = m_firstVisibleIndex; i <= m_lastVisibleIndex; ++i) { |
1123 | result.append(i); |
1124 | } |
1125 | |
1126 | // We need a reasonable upper limit for number of items to resolve after |
1127 | // and before the visible range. m_maximumVisibleItems can be quite large |
1128 | // when using Compace View. |
1129 | const int readAheadItems = qMin(ReadAheadPages * m_maximumVisibleItems, ResolveAllItemsLimit / 2); |
1130 | |
1131 | // Add items after the visible range. |
1132 | const int endExtendedVisibleRange = qMin(m_lastVisibleIndex + readAheadItems, count - 1); |
1133 | for (int i = m_lastVisibleIndex + 1; i <= endExtendedVisibleRange; ++i) { |
1134 | result.append(i); |
1135 | } |
1136 | |
1137 | // Add items before the visible range in reverse order. |
1138 | const int beginExtendedVisibleRange = qMax(0, m_firstVisibleIndex - readAheadItems); |
1139 | for (int i = m_firstVisibleIndex - 1; i >= beginExtendedVisibleRange; --i) { |
1140 | result.append(i); |
1141 | } |
1142 | |
1143 | // Add items on the last page. |
1144 | const int beginLastPage = qMax(qMin(endExtendedVisibleRange + 1, count - 1), count - m_maximumVisibleItems); |
1145 | for (int i = beginLastPage; i < count; ++i) { |
1146 | result.append(i); |
1147 | } |
1148 | |
1149 | // Add items on the first page. |
1150 | const int endFirstPage = qMin(qMax(beginExtendedVisibleRange - 1, 0), m_maximumVisibleItems); |
1151 | for (int i = 0; i <= endFirstPage; ++i) { |
1152 | result.append(i); |
1153 | } |
1154 | |
1155 | // Continue adding items until ResolveAllItemsLimit is reached. |
1156 | int remainingItems = ResolveAllItemsLimit - result.count(); |
1157 | |
1158 | for (int i = endExtendedVisibleRange + 1; i < beginLastPage && remainingItems > 0; ++i) { |
1159 | result.append(i); |
1160 | --remainingItems; |
1161 | } |
1162 | |
1163 | for (int i = beginExtendedVisibleRange - 1; i > endFirstPage && remainingItems > 0; --i) { |
1164 | result.append(i); |
1165 | --remainingItems; |
1166 | } |
1167 | |
1168 | return result; |
1169 | } |
1170 | |
1171 | #include "kfileitemmodelrolesupdater.moc" |
1172 | |