]> cloud.milkyroute.net Git - dolphin.git/blob - src/kitemviews/kfileitemlistview.cpp
Simplify creation of drag-pixmap
[dolphin.git] / src / kitemviews / kfileitemlistview.cpp
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 "kfileitemlistview.h"
21
22 #include "kfileitemlistgroupheader.h"
23 #include "kfileitemmodelrolesupdater.h"
24 #include "kfileitemlistwidget.h"
25 #include "kfileitemmodel.h"
26 #include "kpixmapmodifier_p.h"
27 #include <KLocale>
28 #include <KStringHandler>
29
30 #include <KDebug>
31 #include <KIcon>
32
33 #include <QPainter>
34 #include <QTextLine>
35 #include <QTimer>
36
37 #define KFILEITEMLISTVIEW_DEBUG
38
39 namespace {
40 const int ShortInterval = 50;
41 const int LongInterval = 300;
42 }
43
44 KFileItemListView::KFileItemListView(QGraphicsWidget* parent) :
45 KItemListView(parent),
46 m_itemLayout(IconsLayout),
47 m_modelRolesUpdater(0),
48 m_updateVisibleIndexRangeTimer(0),
49 m_updateIconSizeTimer(0),
50 m_minimumRolesWidths()
51 {
52 setAcceptDrops(true);
53
54 setScrollOrientation(Qt::Vertical);
55 setWidgetCreator(new KItemListWidgetCreator<KFileItemListWidget>());
56 setGroupHeaderCreator(new KItemListGroupHeaderCreator<KFileItemListGroupHeader>());
57
58 m_updateVisibleIndexRangeTimer = new QTimer(this);
59 m_updateVisibleIndexRangeTimer->setSingleShot(true);
60 m_updateVisibleIndexRangeTimer->setInterval(ShortInterval);
61 connect(m_updateVisibleIndexRangeTimer, SIGNAL(timeout()), this, SLOT(updateVisibleIndexRange()));
62
63 m_updateIconSizeTimer = new QTimer(this);
64 m_updateIconSizeTimer->setSingleShot(true);
65 m_updateIconSizeTimer->setInterval(ShortInterval);
66 connect(m_updateIconSizeTimer, SIGNAL(timeout()), this, SLOT(updateIconSize()));
67
68 updateMinimumRolesWidths();
69 }
70
71 KFileItemListView::~KFileItemListView()
72 {
73 // The group headers are children of the widgets created by
74 // widgetCreator(). So it is mandatory to delete the group headers
75 // first.
76 delete groupHeaderCreator();
77 delete widgetCreator();
78
79 delete m_modelRolesUpdater;
80 m_modelRolesUpdater = 0;
81 }
82
83 void KFileItemListView::setPreviewsShown(bool show)
84 {
85 if (m_modelRolesUpdater) {
86 m_modelRolesUpdater->setPreviewShown(show);
87 }
88 }
89
90 bool KFileItemListView::previewsShown() const
91 {
92 return m_modelRolesUpdater->isPreviewShown();
93 }
94
95 void KFileItemListView::setItemLayout(Layout layout)
96 {
97 if (m_itemLayout != layout) {
98 m_itemLayout = layout;
99 updateLayoutOfVisibleItems();
100 }
101 }
102
103 KFileItemListView::Layout KFileItemListView::itemLayout() const
104 {
105 return m_itemLayout;
106 }
107
108 QSizeF KFileItemListView::itemSizeHint(int index) const
109 {
110 const QHash<QByteArray, QVariant> values = model()->data(index);
111 const KItemListStyleOption& option = styleOption();
112 const int additionalRolesCount = qMax(visibleRoles().count() - 1, 0);
113
114 switch (m_itemLayout) {
115 case IconsLayout: {
116 const QString text = KStringHandler::preProcessWrap(values["name"].toString());
117
118 const qreal maxWidth = itemSize().width() - 2 * option.margin;
119 int textLinesCount = 0;
120 QTextLine line;
121
122 // Calculate the number of lines required for wrapping the name
123 QTextOption textOption(Qt::AlignHCenter);
124 textOption.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
125
126 QTextLayout layout(text, option.font);
127 layout.setTextOption(textOption);
128 layout.beginLayout();
129 while ((line = layout.createLine()).isValid()) {
130 line.setLineWidth(maxWidth);
131 line.naturalTextWidth();
132 ++textLinesCount;
133 }
134 layout.endLayout();
135
136 // Add one line for each additional information
137 textLinesCount += additionalRolesCount;
138
139 const qreal height = textLinesCount * option.fontMetrics.height() +
140 option.iconSize +
141 option.margin * 4;
142 return QSizeF(itemSize().width(), height);
143 }
144
145 case CompactLayout: {
146 // For each row exactly one role is shown. Calculate the maximum required width that is necessary
147 // to show all roles without horizontal clipping.
148 qreal maximumRequiredWidth = 0.0;
149
150 foreach (const QByteArray& role, visibleRoles()) {
151 const QString text = KFileItemListWidget::roleText(role, values);
152 const qreal requiredWidth = option.fontMetrics.width(text);
153 maximumRequiredWidth = qMax(maximumRequiredWidth, requiredWidth);
154 }
155
156 const qreal width = option.margin * 4 + option.iconSize + maximumRequiredWidth;
157 const qreal height = option.margin * 2 + qMax(option.iconSize, (1 + additionalRolesCount) * option.fontMetrics.height());
158 return QSizeF(width, height);
159 }
160
161 case DetailsLayout: {
162 // The width will be determined dynamically by KFileItemListView::visibleRoleSizes()
163 const qreal height = option.margin * 2 + qMax(option.iconSize, option.fontMetrics.height());
164 return QSizeF(-1, height);
165 }
166
167 default:
168 Q_ASSERT(false);
169 break;
170 }
171
172 return QSize();
173 }
174
175 QHash<QByteArray, QSizeF> KFileItemListView::visibleRolesSizes(const KItemRangeList& itemRanges) const
176 {
177 QElapsedTimer timer;
178 timer.start();
179
180 QHash<QByteArray, QSizeF> sizes;
181
182 int calculatedItemCount = 0;
183 bool maxTimeExceeded = false;
184 foreach (const KItemRange& itemRange, itemRanges) {
185 const int startIndex = itemRange.index;
186 const int endIndex = startIndex + itemRange.count - 1;
187
188 for (int i = startIndex; i <= endIndex; ++i) {
189 foreach (const QByteArray& visibleRole, visibleRoles()) {
190 QSizeF maxSize = sizes.value(visibleRole, QSizeF(0, 0));
191 const QSizeF itemSize = visibleRoleSizeHint(i, visibleRole);
192 maxSize = maxSize.expandedTo(itemSize);
193 sizes.insert(visibleRole, maxSize);
194 }
195
196 if (calculatedItemCount > 100 && timer.elapsed() > 200) {
197 // When having several thousands of items calculating the sizes can get
198 // very expensive. We accept a possibly too small role-size in favour
199 // of having no blocking user interface.
200 #ifdef KFILEITEMLISTVIEW_DEBUG
201 kDebug() << "Timer exceeded, stopped after" << calculatedItemCount << "items";
202 #endif
203 maxTimeExceeded = true;
204 break;
205 }
206 ++calculatedItemCount;
207 }
208 if (maxTimeExceeded) {
209 break;
210 }
211 }
212
213 #ifdef KFILEITEMLISTVIEW_DEBUG
214 int rangesItemCount = 0;
215 foreach (const KItemRange& itemRange, itemRanges) {
216 rangesItemCount += itemRange.count;
217 }
218 kDebug() << "[TIME] Calculated dynamic item size for " << rangesItemCount << "items:" << timer.elapsed();
219 #endif
220 return sizes;
221 }
222
223 QPixmap KFileItemListView::createDragPixmap(const QSet<int>& indexes) const
224 {
225 if (!model()) {
226 return QPixmap();
227 }
228
229 const int itemCount = indexes.count();
230 Q_ASSERT(itemCount > 0);
231
232 // If more than one item is dragged, align the items inside a
233 // rectangular grid. The maximum grid size is limited to 5 x 5 items.
234 int xCount;
235 int size;
236 if (itemCount > 16) {
237 xCount = 5;
238 size = KIconLoader::SizeSmall;
239 } else if (itemCount > 9) {
240 xCount = 4;
241 size = KIconLoader::SizeSmallMedium;
242 } else {
243 xCount = 3;
244 size = KIconLoader::SizeMedium;
245 }
246
247 if (itemCount < xCount) {
248 xCount = itemCount;
249 }
250
251 int yCount = itemCount / xCount;
252 if (itemCount % xCount != 0) {
253 ++yCount;
254 }
255 if (yCount > xCount) {
256 yCount = xCount;
257 }
258
259 // Draw the selected items into the grid cells.
260 QPixmap dragPixmap(xCount * size + xCount, yCount * size + yCount);
261 dragPixmap.fill(Qt::transparent);
262
263 QPainter painter(&dragPixmap);
264 int x = 0;
265 int y = 0;
266 QSetIterator<int> it(indexes);
267 while (it.hasNext()) {
268 const int index = it.next();
269
270 QPixmap pixmap = model()->data(index).value("iconPixmap").value<QPixmap>();
271 if (pixmap.isNull()) {
272 KIcon icon(model()->data(index).value("iconName").toString());
273 pixmap = icon.pixmap(size, size);
274 } else {
275 KPixmapModifier::scale(pixmap, QSize(size, size));
276 }
277
278 painter.drawPixmap(x, y, pixmap);
279
280 x += size + 1;
281 if (x >= dragPixmap.width()) {
282 x = 0;
283 y += size + 1;
284 }
285
286 if (y >= dragPixmap.height()) {
287 break;
288 }
289 }
290
291 return dragPixmap;
292 }
293
294 void KFileItemListView::initializeItemListWidget(KItemListWidget* item)
295 {
296 KFileItemListWidget* fileItemListWidget = static_cast<KFileItemListWidget*>(item);
297
298 switch (m_itemLayout) {
299 case IconsLayout: fileItemListWidget->setLayout(KFileItemListWidget::IconsLayout); break;
300 case CompactLayout: fileItemListWidget->setLayout(KFileItemListWidget::CompactLayout); break;
301 case DetailsLayout: fileItemListWidget->setLayout(KFileItemListWidget::DetailsLayout); break;
302 default: Q_ASSERT(false); break;
303 }
304
305 fileItemListWidget->setAlternatingBackgroundColors(m_itemLayout == DetailsLayout);
306 }
307
308 bool KFileItemListView::itemSizeHintUpdateRequired(const QSet<QByteArray>& changedRoles) const
309 {
310 // Even if the icons have a different size they are always aligned within
311 // the area defined by KItemStyleOption.iconSize and hence result in no
312 // change of the item-size.
313 const bool containsIconName = changedRoles.contains("iconName");
314 const bool containsIconPixmap = changedRoles.contains("iconPixmap");
315 const int count = changedRoles.count();
316
317 const bool iconChanged = (containsIconName && containsIconPixmap && count == 2) ||
318 (containsIconName && count == 1) ||
319 (containsIconPixmap && count == 1);
320 return !iconChanged;
321 }
322
323 void KFileItemListView::onModelChanged(KItemModelBase* current, KItemModelBase* previous)
324 {
325 Q_UNUSED(previous);
326 Q_ASSERT(qobject_cast<KFileItemModel*>(current));
327
328 if (m_modelRolesUpdater) {
329 delete m_modelRolesUpdater;
330 }
331
332 m_modelRolesUpdater = new KFileItemModelRolesUpdater(static_cast<KFileItemModel*>(current), this);
333 const int size = styleOption().iconSize;
334 m_modelRolesUpdater->setIconSize(QSize(size, size));
335 }
336
337 void KFileItemListView::onScrollOrientationChanged(Qt::Orientation current, Qt::Orientation previous)
338 {
339 Q_UNUSED(current);
340 Q_UNUSED(previous);
341 updateLayoutOfVisibleItems();
342 }
343
344 void KFileItemListView::onItemSizeChanged(const QSizeF& current, const QSizeF& previous)
345 {
346 Q_UNUSED(current);
347 Q_UNUSED(previous);
348 triggerVisibleIndexRangeUpdate();
349 }
350
351 void KFileItemListView::onScrollOffsetChanged(qreal current, qreal previous)
352 {
353 Q_UNUSED(current);
354 Q_UNUSED(previous);
355 triggerVisibleIndexRangeUpdate();
356 }
357
358 void KFileItemListView::onVisibleRolesChanged(const QList<QByteArray>& current, const QList<QByteArray>& previous)
359 {
360 Q_UNUSED(current);
361 Q_UNUSED(previous);
362 applyRolesToModel();
363 }
364
365 void KFileItemListView::onStyleOptionChanged(const KItemListStyleOption& current, const KItemListStyleOption& previous)
366 {
367 Q_UNUSED(current);
368 Q_UNUSED(previous);
369 triggerIconSizeUpdate();
370 }
371
372 void KFileItemListView::onTransactionBegin()
373 {
374 m_modelRolesUpdater->setPaused(true);
375 }
376
377 void KFileItemListView::onTransactionEnd()
378 {
379 // Only unpause the model-roles-updater if no timer is active. If one
380 // timer is still active the model-roles-updater will be unpaused later as
381 // soon as the timer has been exceeded.
382 const bool timerActive = m_updateVisibleIndexRangeTimer->isActive() ||
383 m_updateIconSizeTimer->isActive();
384 if (!timerActive) {
385 m_modelRolesUpdater->setPaused(false);
386 }
387 }
388
389 void KFileItemListView::resizeEvent(QGraphicsSceneResizeEvent* event)
390 {
391 KItemListView::resizeEvent(event);
392 triggerVisibleIndexRangeUpdate();
393 }
394
395 void KFileItemListView::slotItemsRemoved(const KItemRangeList& itemRanges)
396 {
397 KItemListView::slotItemsRemoved(itemRanges);
398 updateTimersInterval();
399 }
400
401 void KFileItemListView::slotSortRoleChanged(const QByteArray& current, const QByteArray& previous)
402 {
403 const QByteArray sortRole = model()->sortRole();
404 if (!visibleRoles().contains(sortRole)) {
405 applyRolesToModel();
406 }
407
408 KItemListView::slotSortRoleChanged(current, previous);
409 }
410
411 void KFileItemListView::triggerVisibleIndexRangeUpdate()
412 {
413 m_modelRolesUpdater->setPaused(true);
414 m_updateVisibleIndexRangeTimer->start();
415 }
416
417 void KFileItemListView::updateVisibleIndexRange()
418 {
419 if (!m_modelRolesUpdater) {
420 return;
421 }
422
423 const int index = firstVisibleIndex();
424 const int count = lastVisibleIndex() - index + 1;
425 m_modelRolesUpdater->setVisibleIndexRange(index, count);
426
427 if (m_updateIconSizeTimer->isActive()) {
428 // If the icon-size update is pending do an immediate update
429 // of the icon-size before unpausing m_modelRolesUpdater. This prevents
430 // an unnecessary expensive recreation of all previews afterwards.
431 m_updateIconSizeTimer->stop();
432 const KItemListStyleOption& option = styleOption();
433 m_modelRolesUpdater->setIconSize(QSize(option.iconSize, option.iconSize));
434 }
435
436 m_modelRolesUpdater->setPaused(isTransactionActive());
437 updateTimersInterval();
438 }
439
440 void KFileItemListView::triggerIconSizeUpdate()
441 {
442 m_modelRolesUpdater->setPaused(true);
443 m_updateIconSizeTimer->start();
444 }
445
446 void KFileItemListView::updateIconSize()
447 {
448 if (!m_modelRolesUpdater) {
449 return;
450 }
451
452 const KItemListStyleOption& option = styleOption();
453 m_modelRolesUpdater->setIconSize(QSize(option.iconSize, option.iconSize));
454
455 if (m_updateVisibleIndexRangeTimer->isActive()) {
456 // If the visibility-index-range update is pending do an immediate update
457 // of the range before unpausing m_modelRolesUpdater. This prevents
458 // an unnecessary expensive recreation of all previews afterwards.
459 m_updateVisibleIndexRangeTimer->stop();
460 const int index = firstVisibleIndex();
461 const int count = lastVisibleIndex() - index + 1;
462 m_modelRolesUpdater->setVisibleIndexRange(index, count);
463 }
464
465 m_modelRolesUpdater->setPaused(isTransactionActive());
466 updateTimersInterval();
467 }
468
469 QSizeF KFileItemListView::visibleRoleSizeHint(int index, const QByteArray& role) const
470 {
471 const KItemListStyleOption& option = styleOption();
472
473 qreal width = m_minimumRolesWidths.value(role, 0);
474 const qreal height = option.margin * 2 + option.fontMetrics.height();
475
476 const QHash<QByteArray, QVariant> values = model()->data(index);
477 const QString text = KFileItemListWidget::roleText(role, values);
478 if (!text.isEmpty()) {
479 const qreal columnMargin = option.margin * 3;
480 width = qMax(width, qreal(2 * columnMargin + option.fontMetrics.width(text)));
481 }
482
483 if (role == "name") {
484 // Increase the width by the expansion-toggle and the current expansion level
485 const int expansionLevel = values.value("expansionLevel", 0).toInt();
486 width += option.margin + expansionLevel * itemSize().height() + KIconLoader::SizeSmall;
487
488 // Increase the width by the required space for the icon
489 width += option.margin * 2 + option.iconSize;
490 }
491
492 return QSizeF(width, height);
493 }
494
495 void KFileItemListView::updateLayoutOfVisibleItems()
496 {
497 foreach (KItemListWidget* widget, visibleItemListWidgets()) {
498 initializeItemListWidget(widget);
499 }
500 triggerVisibleIndexRangeUpdate();
501 }
502
503 void KFileItemListView::updateTimersInterval()
504 {
505 if (!model()) {
506 return;
507 }
508
509 // The ShortInterval is used for cases like switching the directory: If the
510 // model is empty and filled later the creation of the previews should be done
511 // as soon as possible. The LongInterval is used when the model already contains
512 // items and assures that operations like zooming don't result in too many temporary
513 // recreations of the previews.
514
515 const int interval = (model()->count() <= 0) ? ShortInterval : LongInterval;
516 m_updateVisibleIndexRangeTimer->setInterval(interval);
517 m_updateIconSizeTimer->setInterval(interval);
518 }
519
520 void KFileItemListView::updateMinimumRolesWidths()
521 {
522 m_minimumRolesWidths.clear();
523
524 const KItemListStyleOption& option = styleOption();
525 const QString sizeText = QLatin1String("888888") + i18nc("@item:intable", "items");
526 m_minimumRolesWidths.insert("size", option.fontMetrics.width(sizeText));
527 }
528
529 void KFileItemListView::applyRolesToModel()
530 {
531 Q_ASSERT(qobject_cast<KFileItemModel*>(model()));
532 KFileItemModel* fileItemModel = static_cast<KFileItemModel*>(model());
533
534 // KFileItemModel does not distinct between "visible" and "invisible" roles.
535 // Add all roles that are mandatory for having a working KFileItemListView:
536 QSet<QByteArray> roles = visibleRoles().toSet();
537 roles.insert("iconPixmap");
538 roles.insert("iconName");
539 roles.insert("name");
540 roles.insert("isDir");
541 if (m_itemLayout == DetailsLayout) {
542 roles.insert("isExpanded");
543 roles.insert("expansionLevel");
544 }
545
546 // Assure that the role that is used for sorting will be determined
547 roles.insert(fileItemModel->sortRole());
548
549 fileItemModel->setRoles(roles);
550 m_modelRolesUpdater->setRoles(roles);
551 }
552
553 #include "kfileitemlistview.moc"