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