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