]> cloud.milkyroute.net Git - dolphin.git/blob - src/kitemviews/kfileitemlistwidget.cpp
Icons Mode: Fix wrong width calculation of additional roles
[dolphin.git] / src / kitemviews / kfileitemlistwidget.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 "kfileitemlistwidget.h"
21
22 #include "kfileitemclipboard_p.h"
23 #include "kfileitemlistview.h"
24 #include "kfileitemmodel.h"
25 #include "kpixmapmodifier_p.h"
26
27 #include <KIcon>
28 #include <KIconEffect>
29 #include <KIconLoader>
30 #include <KLocale>
31 #include <kratingpainter.h>
32 #include <KStringHandler>
33 #include <KDebug>
34
35 #include <QFontMetricsF>
36 #include <QGraphicsSceneResizeEvent>
37 #include <QPainter>
38 #include <QStyleOption>
39 #include <QTextLayout>
40 #include <QTextLine>
41
42 // #define KFILEITEMLISTWIDGET_DEBUG
43
44 KFileItemListWidget::KFileItemListWidget(QGraphicsItem* parent) :
45 KItemListWidget(parent),
46 m_isCut(false),
47 m_isHidden(false),
48 m_isExpandable(false),
49 m_supportsItemExpanding(false),
50 m_dirtyLayout(true),
51 m_dirtyContent(true),
52 m_dirtyContentRoles(),
53 m_layout(IconsLayout),
54 m_pixmapPos(),
55 m_pixmap(),
56 m_scaledPixmapSize(),
57 m_iconRect(),
58 m_hoverPixmap(),
59 m_textInfo(),
60 m_textRect(),
61 m_sortedVisibleRoles(),
62 m_expansionArea(),
63 m_customTextColor(),
64 m_additionalInfoTextColor(),
65 m_overlay(),
66 m_rating()
67 {
68 }
69
70 KFileItemListWidget::~KFileItemListWidget()
71 {
72 qDeleteAll(m_textInfo);
73 m_textInfo.clear();
74 }
75
76 void KFileItemListWidget::setLayout(Layout layout)
77 {
78 if (m_layout != layout) {
79 m_layout = layout;
80 m_dirtyLayout = true;
81 updateAdditionalInfoTextColor();
82 update();
83 }
84 }
85
86 KFileItemListWidget::Layout KFileItemListWidget::layout() const
87 {
88 return m_layout;
89 }
90
91 void KFileItemListWidget::setSupportsItemExpanding(bool supportsItemExpanding)
92 {
93 if (m_supportsItemExpanding != supportsItemExpanding) {
94 m_supportsItemExpanding = supportsItemExpanding;
95 m_dirtyLayout = true;
96 update();
97 }
98 }
99
100 bool KFileItemListWidget::supportsItemExpanding() const
101 {
102 return m_supportsItemExpanding;
103 }
104
105 void KFileItemListWidget::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget)
106 {
107 const_cast<KFileItemListWidget*>(this)->triggerCacheRefreshing();
108
109 KItemListWidget::paint(painter, option, widget);
110
111 if (!m_expansionArea.isEmpty()) {
112 drawSiblingsInformation(painter);
113 }
114
115 const KItemListStyleOption& itemListStyleOption = styleOption();
116 if (isHovered()) {
117 // Blend the unhovered and hovered pixmap if the hovering
118 // animation is ongoing
119 if (hoverOpacity() < 1.0) {
120 drawPixmap(painter, m_pixmap);
121 }
122
123 const qreal opacity = painter->opacity();
124 painter->setOpacity(hoverOpacity() * opacity);
125 drawPixmap(painter, m_hoverPixmap);
126 painter->setOpacity(opacity);
127 } else {
128 drawPixmap(painter, m_pixmap);
129 }
130
131 painter->setFont(itemListStyleOption.font);
132 painter->setPen(textColor());
133 const TextInfo* textInfo = m_textInfo.value("name");
134 painter->drawStaticText(textInfo->pos, textInfo->staticText);
135
136 bool clipAdditionalInfoBounds = false;
137 if (m_supportsItemExpanding) {
138 // Prevent a possible overlapping of the additional-information texts
139 // with the icon. This can happen if the user has minimized the width
140 // of the name-column to a very small value.
141 const qreal minX = m_pixmapPos.x() + m_pixmap.width() + 4 * itemListStyleOption.padding;
142 if (textInfo->pos.x() + columnWidth("name") > minX) {
143 clipAdditionalInfoBounds = true;
144 painter->save();
145 painter->setClipRect(minX, 0, size().width() - minX, size().height(), Qt::IntersectClip);
146 }
147 }
148
149 painter->setPen(m_additionalInfoTextColor);
150 painter->setFont(itemListStyleOption.font);
151
152 for (int i = 1; i < m_sortedVisibleRoles.count(); ++i) {
153 const TextInfo* textInfo = m_textInfo.value(m_sortedVisibleRoles[i]);
154 painter->drawStaticText(textInfo->pos, textInfo->staticText);
155 }
156
157 if (!m_rating.isNull()) {
158 const TextInfo* ratingTextInfo = m_textInfo.value("rating");
159 QPointF pos = ratingTextInfo->pos;
160 const Qt::Alignment align = ratingTextInfo->staticText.textOption().alignment();
161 if (align & Qt::AlignHCenter) {
162 pos.rx() += (size().width() - m_rating.width()) / 2;
163 }
164 painter->drawPixmap(pos, m_rating);
165 }
166
167 if (clipAdditionalInfoBounds) {
168 painter->restore();
169 }
170
171 #ifdef KFILEITEMLISTWIDGET_DEBUG
172 painter->setBrush(Qt::NoBrush);
173 painter->setPen(Qt::green);
174 painter->drawRect(m_iconRect);
175
176 painter->setPen(Qt::red);
177 painter->drawText(QPointF(0, itemListStyleOption.fontMetrics.height()), QString::number(index()));
178 painter->drawRect(rect());
179 #endif
180 }
181
182 QRectF KFileItemListWidget::iconRect() const
183 {
184 const_cast<KFileItemListWidget*>(this)->triggerCacheRefreshing();
185 return m_iconRect;
186 }
187
188 QRectF KFileItemListWidget::textRect() const
189 {
190 const_cast<KFileItemListWidget*>(this)->triggerCacheRefreshing();
191 return m_textRect;
192 }
193
194 QRectF KFileItemListWidget::textFocusRect() const
195 {
196 const_cast<KFileItemListWidget*>(this)->triggerCacheRefreshing();
197 if (m_layout == CompactLayout) {
198 // In the compact layout a larger textRect() is returned to be aligned
199 // with the iconRect(). This is useful to have a larger selection/hover-area
200 // when having a quite large icon size but only one line of text. Still the
201 // focus rectangle should be shown as narrow as possible around the text.
202 QRectF rect = m_textRect;
203 const TextInfo* topText = m_textInfo.value(m_sortedVisibleRoles.first());
204 const TextInfo* bottomText = m_textInfo.value(m_sortedVisibleRoles.last());
205 rect.setTop(topText->pos.y());
206 rect.setBottom(bottomText->pos.y() + bottomText->staticText.size().height());
207 return rect;
208 }
209 return m_textRect;
210 }
211
212 QRectF KFileItemListWidget::expansionToggleRect() const
213 {
214 const_cast<KFileItemListWidget*>(this)->triggerCacheRefreshing();
215 return m_isExpandable ? m_expansionArea : QRectF();
216 }
217
218 QRectF KFileItemListWidget::selectionToggleRect() const
219 {
220 const_cast<KFileItemListWidget*>(this)->triggerCacheRefreshing();
221
222 const int iconHeight = styleOption().iconSize;
223
224 int toggleSize = KIconLoader::SizeSmall;
225 if (iconHeight >= KIconLoader::SizeEnormous) {
226 toggleSize = KIconLoader::SizeMedium;
227 } else if (iconHeight >= KIconLoader::SizeLarge) {
228 toggleSize = KIconLoader::SizeSmallMedium;
229 }
230
231 QPointF pos = iconRect().topLeft();
232
233 // If the selection toggle has a very small distance to the
234 // widget borders, the size of the selection toggle will get
235 // increased to prevent an accidental clicking of the item
236 // when trying to hit the toggle.
237 const int widgetHeight = size().height();
238 const int widgetWidth = size().width();
239 const int minMargin = 2;
240
241 if (toggleSize + minMargin * 2 >= widgetHeight) {
242 pos.rx() -= (widgetHeight - toggleSize) / 2;
243 toggleSize = widgetHeight;
244 pos.setY(0);
245 }
246 if (toggleSize + minMargin * 2 >= widgetWidth) {
247 pos.ry() -= (widgetWidth - toggleSize) / 2;
248 toggleSize = widgetWidth;
249 pos.setX(0);
250 }
251
252 return QRectF(pos, QSizeF(toggleSize, toggleSize));
253 }
254
255 QSizeF KFileItemListWidget::itemSizeHint(int index, const KItemListView* view)
256 {
257 const QHash<QByteArray, QVariant> values = view->model()->data(index);
258 const KItemListStyleOption& option = view->styleOption();
259 const int additionalRolesCount = qMax(view->visibleRoles().count() - 1, 0);
260
261 switch (static_cast<const KFileItemListView*>(view)->itemLayout()) {
262 case IconsLayout: {
263 const QString text = KStringHandler::preProcessWrap(values["name"].toString());
264
265 const qreal maxWidth = view->itemSize().width() - 2 * option.padding;
266 QTextLine line;
267
268 // Calculate the number of lines required for wrapping the name
269 QTextOption textOption(Qt::AlignHCenter);
270 textOption.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
271
272 qreal textHeight = 0;
273 QTextLayout layout(text, option.font);
274 layout.setTextOption(textOption);
275 layout.beginLayout();
276 while ((line = layout.createLine()).isValid()) {
277 line.setLineWidth(maxWidth);
278 line.naturalTextWidth();
279 textHeight += line.height();
280 }
281 layout.endLayout();
282
283 // Add one line for each additional information
284 const qreal height = textHeight +
285 additionalRolesCount * option.fontMetrics.lineSpacing() +
286 option.iconSize +
287 option.padding * 3;
288 return QSizeF(view->itemSize().width(), height);
289 }
290
291 case CompactLayout: {
292 // For each row exactly one role is shown. Calculate the maximum required width that is necessary
293 // to show all roles without horizontal clipping.
294 qreal maximumRequiredWidth = 0.0;
295
296 foreach (const QByteArray& role, view->visibleRoles()) {
297 const QString text = KFileItemListWidget::roleText(role, values);
298 const qreal requiredWidth = option.fontMetrics.width(text);
299 maximumRequiredWidth = qMax(maximumRequiredWidth, requiredWidth);
300 }
301
302 const qreal width = option.padding * 4 + option.iconSize + maximumRequiredWidth;
303 const qreal height = option.padding * 2 + qMax(option.iconSize, (1 + additionalRolesCount) * option.fontMetrics.lineSpacing());
304 return QSizeF(width, height);
305 }
306
307 case DetailsLayout: {
308 const qreal height = option.padding * 2 + qMax(option.iconSize, option.fontMetrics.height());
309 return QSizeF(-1, height);
310 }
311
312 default:
313 Q_ASSERT(false);
314 break;
315 }
316
317 return QSize();
318 }
319
320 qreal KFileItemListWidget::preferredRoleColumnWidth(const QByteArray& role,
321 int index,
322 const KItemListView* view)
323 {
324
325 const QHash<QByteArray, QVariant> values = view->model()->data(index);
326 const KItemListStyleOption& option = view->styleOption();
327
328 const QString text = KFileItemListWidget::roleText(role, values);
329 qreal width = columnPadding(option);
330
331 if (role == "rating") {
332 width += preferredRatingSize(option).width();
333 } else {
334 width += option.fontMetrics.width(text);
335
336 if (role == "name") {
337 // Increase the width by the expansion-toggle and the current expansion level
338 const int expandedParentsCount = values.value("expandedParentsCount", 0).toInt();
339 width += option.padding + (expandedParentsCount + 1) * view->itemSize().height() + KIconLoader::SizeSmall;
340
341 // Increase the width by the required space for the icon
342 width += option.padding * 2 + option.iconSize;
343 }
344 }
345
346 return width;
347 }
348
349 void KFileItemListWidget::invalidateCache()
350 {
351 m_dirtyLayout = true;
352 m_dirtyContent = true;
353 }
354
355 void KFileItemListWidget::refreshCache()
356 {
357 }
358
359 void KFileItemListWidget::setTextColor(const QColor& color)
360 {
361 if (color != m_customTextColor) {
362 m_customTextColor = color;
363 updateAdditionalInfoTextColor();
364 update();
365 }
366 }
367
368 QColor KFileItemListWidget::textColor() const
369 {
370 if (m_customTextColor.isValid() && !isSelected()) {
371 return m_customTextColor;
372 }
373
374 const QPalette::ColorGroup group = isActiveWindow() ? QPalette::Active : QPalette::Inactive;
375 const QPalette::ColorRole role = isSelected() ? QPalette::HighlightedText : QPalette::Text;
376 return styleOption().palette.brush(group, role).color();
377 }
378
379 void KFileItemListWidget::setOverlay(const QPixmap& overlay)
380 {
381 m_overlay = overlay;
382 m_dirtyContent = true;
383 update();
384 }
385
386 QPixmap KFileItemListWidget::overlay() const
387 {
388 return m_overlay;
389 }
390
391 void KFileItemListWidget::dataChanged(const QHash<QByteArray, QVariant>& current,
392 const QSet<QByteArray>& roles)
393 {
394 Q_UNUSED(current);
395
396 m_dirtyContent = true;
397
398 QSet<QByteArray> dirtyRoles;
399 if (roles.isEmpty()) {
400 dirtyRoles = visibleRoles().toSet();
401 dirtyRoles.insert("iconPixmap");
402 dirtyRoles.insert("iconName");
403 } else {
404 dirtyRoles = roles;
405 }
406
407 QSetIterator<QByteArray> it(dirtyRoles);
408 while (it.hasNext()) {
409 const QByteArray& role = it.next();
410 m_dirtyContentRoles.insert(role);
411 }
412 }
413
414 void KFileItemListWidget::visibleRolesChanged(const QList<QByteArray>& current,
415 const QList<QByteArray>& previous)
416 {
417 Q_UNUSED(previous);
418 m_sortedVisibleRoles = current;
419 m_dirtyLayout = true;
420 }
421
422 void KFileItemListWidget::columnWidthChanged(const QByteArray& role,
423 qreal current,
424 qreal previous)
425 {
426 Q_UNUSED(role);
427 Q_UNUSED(current);
428 Q_UNUSED(previous);
429 m_dirtyLayout = true;
430 }
431
432 void KFileItemListWidget::styleOptionChanged(const KItemListStyleOption& current,
433 const KItemListStyleOption& previous)
434 {
435 Q_UNUSED(current);
436 Q_UNUSED(previous);
437 updateAdditionalInfoTextColor();
438 m_dirtyLayout = true;
439 }
440
441 void KFileItemListWidget::hoveredChanged(bool hovered)
442 {
443 Q_UNUSED(hovered);
444 m_dirtyLayout = true;
445 }
446
447 void KFileItemListWidget::selectedChanged(bool selected)
448 {
449 Q_UNUSED(selected);
450 updateAdditionalInfoTextColor();
451 }
452
453 void KFileItemListWidget::siblingsInformationChanged(const QBitArray& current, const QBitArray& previous)
454 {
455 Q_UNUSED(current);
456 Q_UNUSED(previous);
457 m_dirtyLayout = true;
458 }
459
460
461 void KFileItemListWidget::resizeEvent(QGraphicsSceneResizeEvent* event)
462 {
463 KItemListWidget::resizeEvent(event);
464 m_dirtyLayout = true;
465 }
466
467 void KFileItemListWidget::showEvent(QShowEvent* event)
468 {
469 KItemListWidget::showEvent(event);
470
471 // Listen to changes of the clipboard to mark the item as cut/uncut
472 KFileItemClipboard* clipboard = KFileItemClipboard::instance();
473
474 const KUrl itemUrl = data().value("url").value<KUrl>();
475 m_isCut = clipboard->isCut(itemUrl);
476
477 connect(clipboard, SIGNAL(cutItemsChanged()),
478 this, SLOT(slotCutItemsChanged()));
479 }
480
481 void KFileItemListWidget::hideEvent(QHideEvent* event)
482 {
483 disconnect(KFileItemClipboard::instance(), SIGNAL(cutItemsChanged()),
484 this, SLOT(slotCutItemsChanged()));
485
486 KItemListWidget::hideEvent(event);
487 }
488
489 void KFileItemListWidget::slotCutItemsChanged()
490 {
491 const KUrl itemUrl = data().value("url").value<KUrl>();
492 const bool isCut = KFileItemClipboard::instance()->isCut(itemUrl);
493 if (m_isCut != isCut) {
494 m_isCut = isCut;
495 m_pixmap = QPixmap();
496 m_dirtyContent = true;
497 update();
498 }
499 }
500
501 void KFileItemListWidget::triggerCacheRefreshing()
502 {
503 if ((!m_dirtyContent && !m_dirtyLayout) || index() < 0) {
504 return;
505 }
506
507 refreshCache();
508
509 const QHash<QByteArray, QVariant> values = data();
510 m_isExpandable = m_supportsItemExpanding && values["isExpandable"].toBool();
511 m_isHidden = values["name"].toString().startsWith(QLatin1Char('.'));
512
513 updateExpansionArea();
514 updateTextsCache();
515 updatePixmapCache();
516
517 m_dirtyLayout = false;
518 m_dirtyContent = false;
519 m_dirtyContentRoles.clear();
520 }
521
522 void KFileItemListWidget::updateExpansionArea()
523 {
524 if (m_supportsItemExpanding) {
525 const QHash<QByteArray, QVariant> values = data();
526 Q_ASSERT(values.contains("expandedParentsCount"));
527 const int expandedParentsCount = values.value("expandedParentsCount", 0).toInt();
528 if (expandedParentsCount >= 0) {
529 const qreal widgetHeight = size().height();
530 const qreal inc = (widgetHeight - KIconLoader::SizeSmall) / 2;
531 const qreal x = expandedParentsCount * widgetHeight + inc;
532 const qreal y = inc;
533 m_expansionArea = QRectF(x, y, KIconLoader::SizeSmall, KIconLoader::SizeSmall);
534 return;
535 }
536 }
537
538 m_expansionArea = QRectF();
539 }
540
541 void KFileItemListWidget::updatePixmapCache()
542 {
543 // Precondition: Requires already updated m_textPos values to calculate
544 // the remaining height when the alignment is vertical.
545
546 const QSizeF widgetSize = size();
547 const bool iconOnTop = (m_layout == IconsLayout);
548 const KItemListStyleOption& option = styleOption();
549 const qreal padding = option.padding;
550
551 const int maxIconWidth = iconOnTop ? widgetSize.width() - 2 * padding : option.iconSize;
552 const int maxIconHeight = option.iconSize;
553
554 const QHash<QByteArray, QVariant> values = data();
555
556 bool updatePixmap = (m_pixmap.width() != maxIconWidth || m_pixmap.height() != maxIconHeight);
557 if (!updatePixmap && m_dirtyContent) {
558 updatePixmap = m_dirtyContentRoles.isEmpty()
559 || m_dirtyContentRoles.contains("iconPixmap")
560 || m_dirtyContentRoles.contains("iconName")
561 || m_dirtyContentRoles.contains("iconOverlays");
562 }
563
564 if (updatePixmap) {
565 m_pixmap = values["iconPixmap"].value<QPixmap>();
566 if (m_pixmap.isNull()) {
567 // Use the icon that fits to the MIME-type
568 QString iconName = values["iconName"].toString();
569 if (iconName.isEmpty()) {
570 // The icon-name has not been not resolved by KFileItemModelRolesUpdater,
571 // use a generic icon as fallback
572 iconName = QLatin1String("unknown");
573 }
574 m_pixmap = pixmapForIcon(iconName, maxIconHeight);
575 } else if (m_pixmap.width() != maxIconWidth || m_pixmap.height() != maxIconHeight) {
576 // A custom pixmap has been applied. Assure that the pixmap
577 // is scaled to the maximum available size.
578 KPixmapModifier::scale(m_pixmap, QSize(maxIconWidth, maxIconHeight));
579 }
580
581 const QStringList overlays = values["iconOverlays"].toStringList();
582
583 // Strangely KFileItem::overlays() returns empty string-values, so
584 // we need to check first whether an overlay must be drawn at all.
585 // It is more efficient to do it here, as KIconLoader::drawOverlays()
586 // assumes that an overlay will be drawn and has some additional
587 // setup time.
588 foreach (const QString& overlay, overlays) {
589 if (!overlay.isEmpty()) {
590 // There is at least one overlay, draw all overlays above m_pixmap
591 // and cancel the check
592 KIconLoader::global()->drawOverlays(overlays, m_pixmap, KIconLoader::Desktop);
593 break;
594 }
595 }
596
597 if (m_isCut) {
598 applyCutEffect(m_pixmap);
599 }
600
601 if (m_isHidden) {
602 applyHiddenEffect(m_pixmap);
603 }
604 }
605
606 if (!m_overlay.isNull()) {
607 QPainter painter(&m_pixmap);
608 painter.drawPixmap(0, m_pixmap.height() - m_overlay.height(), m_overlay);
609 }
610
611 int scaledIconSize = 0;
612 if (iconOnTop) {
613 const TextInfo* textInfo = m_textInfo.value("name");
614 scaledIconSize = static_cast<int>(textInfo->pos.y() - 2 * padding);
615 } else {
616 const int textRowsCount = (m_layout == CompactLayout) ? visibleRoles().count() : 1;
617 const qreal requiredTextHeight = textRowsCount * option.fontMetrics.height();
618 scaledIconSize = (requiredTextHeight < maxIconHeight) ?
619 widgetSize.height() - 2 * padding : maxIconHeight;
620 }
621
622 const int maxScaledIconWidth = iconOnTop ? widgetSize.width() - 2 * padding : scaledIconSize;
623 const int maxScaledIconHeight = scaledIconSize;
624
625 m_scaledPixmapSize = m_pixmap.size();
626 m_scaledPixmapSize.scale(maxScaledIconWidth, maxScaledIconHeight, Qt::KeepAspectRatio);
627
628 if (iconOnTop) {
629 // Center horizontally and align on bottom within the icon-area
630 m_pixmapPos.setX((widgetSize.width() - m_scaledPixmapSize.width()) / 2);
631 m_pixmapPos.setY(padding + scaledIconSize - m_scaledPixmapSize.height());
632 } else {
633 // Center horizontally and vertically within the icon-area
634 const TextInfo* textInfo = m_textInfo.value("name");
635 m_pixmapPos.setX(textInfo->pos.x() - 2 * padding
636 - (scaledIconSize + m_scaledPixmapSize.width()) / 2);
637 m_pixmapPos.setY(padding
638 + (scaledIconSize - m_scaledPixmapSize.height()) / 2);
639 }
640
641 m_iconRect = QRectF(m_pixmapPos, QSizeF(m_scaledPixmapSize));
642
643 // Prepare the pixmap that is used when the item gets hovered
644 if (isHovered()) {
645 m_hoverPixmap = m_pixmap;
646 KIconEffect* effect = KIconLoader::global()->iconEffect();
647 // In the KIconLoader terminology, active = hover.
648 if (effect->hasEffect(KIconLoader::Desktop, KIconLoader::ActiveState)) {
649 m_hoverPixmap = effect->apply(m_pixmap, KIconLoader::Desktop, KIconLoader::ActiveState);
650 } else {
651 m_hoverPixmap = m_pixmap;
652 }
653 } else if (hoverOpacity() <= 0.0) {
654 // No hover animation is ongoing. Clear m_hoverPixmap to save memory.
655 m_hoverPixmap = QPixmap();
656 }
657 }
658
659 void KFileItemListWidget::updateTextsCache()
660 {
661 QTextOption textOption;
662 switch (m_layout) {
663 case IconsLayout:
664 textOption.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
665 textOption.setAlignment(Qt::AlignHCenter);
666 break;
667 case CompactLayout:
668 case DetailsLayout:
669 textOption.setAlignment(Qt::AlignLeft);
670 textOption.setWrapMode(QTextOption::NoWrap);
671 break;
672 default:
673 Q_ASSERT(false);
674 break;
675 }
676
677 qDeleteAll(m_textInfo);
678 m_textInfo.clear();
679 for (int i = 0; i < m_sortedVisibleRoles.count(); ++i) {
680 TextInfo* textInfo = new TextInfo();
681 textInfo->staticText.setTextFormat(Qt::PlainText);
682 textInfo->staticText.setPerformanceHint(QStaticText::AggressiveCaching);
683 textInfo->staticText.setTextOption(textOption);
684 m_textInfo.insert(m_sortedVisibleRoles[i], textInfo);
685 }
686
687 switch (m_layout) {
688 case IconsLayout: updateIconsLayoutTextCache(); break;
689 case CompactLayout: updateCompactLayoutTextCache(); break;
690 case DetailsLayout: updateDetailsLayoutTextCache(); break;
691 default: Q_ASSERT(false); break;
692 }
693
694 const TextInfo* ratingTextInfo = m_textInfo.value("rating");
695 if (ratingTextInfo) {
696 // The text of the rating-role has been set to empty to get
697 // replaced by a rating-image showing the rating as stars.
698 const KItemListStyleOption& option = styleOption();
699 QSizeF ratingSize = preferredRatingSize(option);
700
701 const qreal availableWidth = (m_layout == DetailsLayout)
702 ? columnWidth("rating") - columnPadding(option)
703 : m_textRect.width();
704 if (ratingSize.width() > availableWidth) {
705 ratingSize.rwidth() = availableWidth;
706 }
707 m_rating = QPixmap(ratingSize.toSize());
708 m_rating.fill(Qt::transparent);
709
710 QPainter painter(&m_rating);
711 const QRect rect(0, 0, m_rating.width(), m_rating.height());
712 const int rating = data().value("rating").toInt();
713 KRatingPainter::paintRating(&painter, rect, Qt::AlignJustify | Qt::AlignVCenter, rating);
714 } else if (!m_rating.isNull()) {
715 m_rating = QPixmap();
716 }
717 }
718
719 void KFileItemListWidget::updateIconsLayoutTextCache()
720 {
721 // +------+
722 // | Icon |
723 // +------+
724 //
725 // Name role that
726 // might get wrapped above
727 // several lines.
728 // Additional role 1
729 // Additional role 2
730
731 const QHash<QByteArray, QVariant> values = data();
732
733 const KItemListStyleOption& option = styleOption();
734 const qreal padding = option.padding;
735 const qreal maxWidth = size().width() - 2 * padding;
736 const qreal widgetHeight = size().height();
737 const qreal lineSpacing = option.fontMetrics.lineSpacing();
738
739 // Initialize properties for the "name" role. It will be used as anchor
740 // for initializing the position of the other roles.
741 TextInfo* nameTextInfo = m_textInfo.value("name");
742 nameTextInfo->staticText.setText(KStringHandler::preProcessWrap(values["name"].toString()));
743
744 // Calculate the number of lines required for the name and the required width
745 qreal nameWidth = 0;
746 qreal nameHeight = 0;
747 QTextLine line;
748
749 QTextLayout layout(nameTextInfo->staticText.text(), option.font);
750 layout.setTextOption(nameTextInfo->staticText.textOption());
751 layout.beginLayout();
752 while ((line = layout.createLine()).isValid()) {
753 line.setLineWidth(maxWidth);
754 nameWidth = qMax(nameWidth, line.naturalTextWidth());
755 nameHeight += line.height();
756 }
757 layout.endLayout();
758
759 // Use one line for each additional information
760 const int additionalRolesCount = qMax(visibleRoles().count() - 1, 0);
761 nameTextInfo->staticText.setTextWidth(maxWidth);
762 nameTextInfo->pos = QPointF(padding, widgetHeight -
763 nameHeight -
764 additionalRolesCount * lineSpacing -
765 padding);
766 m_textRect = QRectF(padding + (maxWidth - nameWidth) / 2,
767 nameTextInfo->pos.y(),
768 nameWidth,
769 nameHeight);
770
771 // Calculate the position for each additional information
772 qreal y = nameTextInfo->pos.y() + nameHeight;
773 foreach (const QByteArray& role, m_sortedVisibleRoles) {
774 if (role == "name") {
775 continue;
776 }
777
778 const QString text = roleText(role, values);
779 TextInfo* textInfo = m_textInfo.value(role);
780 textInfo->staticText.setText(text);
781
782 qreal requiredWidth = 0;
783
784 QTextLayout layout(text, option.font);
785 QTextOption textOption;
786 textOption.setWrapMode(QTextOption::NoWrap);
787 layout.setTextOption(textOption);
788
789 layout.beginLayout();
790 QTextLine textLine = layout.createLine();
791 if (textLine.isValid()) {
792 textLine.setLineWidth(maxWidth);
793 requiredWidth = textLine.naturalTextWidth();
794 if (requiredWidth > maxWidth) {
795 const QString elidedText = option.fontMetrics.elidedText(text, Qt::ElideRight, maxWidth);
796 textInfo->staticText.setText(elidedText);
797 requiredWidth = option.fontMetrics.width(elidedText);
798 }
799 }
800 layout.endLayout();
801
802 textInfo->pos = QPointF(padding, y);
803 textInfo->staticText.setTextWidth(maxWidth);
804
805 const QRectF textRect(padding + (maxWidth - requiredWidth) / 2, y, requiredWidth, lineSpacing);
806 m_textRect |= textRect;
807
808 y += lineSpacing;
809 }
810
811 // Add a padding to the text rectangle
812 m_textRect.adjust(-padding, -padding, padding, padding);
813 }
814
815 void KFileItemListWidget::updateCompactLayoutTextCache()
816 {
817 // +------+ Name role
818 // | Icon | Additional role 1
819 // +------+ Additional role 2
820
821 const QHash<QByteArray, QVariant> values = data();
822
823 const KItemListStyleOption& option = styleOption();
824 const qreal widgetHeight = size().height();
825 const qreal lineSpacing = option.fontMetrics.lineSpacing();
826 const qreal textLinesHeight = qMax(visibleRoles().count(), 1) * lineSpacing;
827 const int scaledIconSize = (textLinesHeight < option.iconSize) ? widgetHeight - 2 * option.padding : option.iconSize;
828
829 qreal maximumRequiredTextWidth = 0;
830 const qreal x = option.padding * 3 + scaledIconSize;
831 qreal y = qRound((widgetHeight - textLinesHeight) / 2);
832 const qreal maxWidth = size().width() - x - option.padding;
833 foreach (const QByteArray& role, m_sortedVisibleRoles) {
834 const QString text = roleText(role, values);
835 TextInfo* textInfo = m_textInfo.value(role);
836 textInfo->staticText.setText(text);
837
838 qreal requiredWidth = option.fontMetrics.width(text);
839 if (requiredWidth > maxWidth) {
840 requiredWidth = maxWidth;
841 const QString elidedText = option.fontMetrics.elidedText(text, Qt::ElideRight, maxWidth);
842 textInfo->staticText.setText(elidedText);
843 }
844
845 textInfo->pos = QPointF(x, y);
846 textInfo->staticText.setTextWidth(maxWidth);
847
848 maximumRequiredTextWidth = qMax(maximumRequiredTextWidth, requiredWidth);
849
850 y += lineSpacing;
851 }
852
853 m_textRect = QRectF(x - option.padding, 0, maximumRequiredTextWidth + 2 * option.padding, widgetHeight);
854 }
855
856 void KFileItemListWidget::updateDetailsLayoutTextCache()
857 {
858 // Precondition: Requires already updated m_expansionArea
859 // to determine the left position.
860
861 // +------+
862 // | Icon | Name role Additional role 1 Additional role 2
863 // +------+
864 m_textRect = QRectF();
865
866 const KItemListStyleOption& option = styleOption();
867 const QHash<QByteArray, QVariant> values = data();
868
869 const qreal widgetHeight = size().height();
870 const int scaledIconSize = widgetHeight - 2 * option.padding;
871 const int fontHeight = option.fontMetrics.height();
872
873 const qreal columnWidthInc = columnPadding(option);
874 qreal firstColumnInc = scaledIconSize;
875 if (m_supportsItemExpanding) {
876 firstColumnInc += (m_expansionArea.left() + m_expansionArea.right() + widgetHeight) / 2;
877 } else {
878 firstColumnInc += option.padding;
879 }
880
881 qreal x = firstColumnInc;
882 const qreal y = qMax(qreal(option.padding), (widgetHeight - fontHeight) / 2);
883
884 foreach (const QByteArray& role, m_sortedVisibleRoles) {
885 const RoleType type = roleType(role);
886
887 QString text = roleText(role, values);
888
889 // Elide the text in case it does not fit into the available column-width
890 qreal requiredWidth = option.fontMetrics.width(text);
891 const qreal roleWidth = columnWidth(role);
892 qreal availableTextWidth = roleWidth - columnWidthInc;
893 if (type == Name) {
894 availableTextWidth -= firstColumnInc;
895 }
896
897 if (requiredWidth > availableTextWidth) {
898 text = option.fontMetrics.elidedText(text, Qt::ElideRight, availableTextWidth);
899 requiredWidth = option.fontMetrics.width(text);
900 }
901
902 TextInfo* textInfo = m_textInfo.value(role);
903 textInfo->staticText.setText(text);
904 textInfo->pos = QPointF(x + columnWidthInc / 2, y);
905 x += roleWidth;
906
907 switch (type) {
908 case Name: {
909 const qreal textWidth = option.extendedSelectionRegion
910 ? size().width() - textInfo->pos.x()
911 : requiredWidth + 2 * option.padding;
912 m_textRect = QRectF(textInfo->pos.x() - option.padding, 0,
913 textWidth, size().height());
914
915 // The column after the name should always be aligned on the same x-position independent
916 // from the expansion-level shown in the name column
917 x -= firstColumnInc;
918 break;
919 }
920 case Size:
921 // The values for the size should be right aligned
922 textInfo->pos.rx() += roleWidth - requiredWidth - columnWidthInc;
923 break;
924
925 default:
926 break;
927 }
928 }
929 }
930
931 void KFileItemListWidget::updateAdditionalInfoTextColor()
932 {
933 QColor c1;
934 if (m_customTextColor.isValid()) {
935 c1 = m_customTextColor;
936 } else if (isSelected() && m_layout != DetailsLayout) {
937 c1 = styleOption().palette.highlightedText().color();
938 } else {
939 c1 = styleOption().palette.text().color();
940 }
941
942 // For the color of the additional info the inactive text color
943 // is not used as this might lead to unreadable text for some color schemes. Instead
944 // the text color c1 is slightly mixed with the background color.
945 const QColor c2 = styleOption().palette.base().color();
946 const int p1 = 70;
947 const int p2 = 100 - p1;
948 m_additionalInfoTextColor = QColor((c1.red() * p1 + c2.red() * p2) / 100,
949 (c1.green() * p1 + c2.green() * p2) / 100,
950 (c1.blue() * p1 + c2.blue() * p2) / 100);
951 }
952
953 void KFileItemListWidget::drawPixmap(QPainter* painter, const QPixmap& pixmap)
954 {
955 if (m_scaledPixmapSize != pixmap.size()) {
956 QPixmap scaledPixmap = pixmap;
957 KPixmapModifier::scale(scaledPixmap, m_scaledPixmapSize);
958 painter->drawPixmap(m_pixmapPos, scaledPixmap);
959
960 #ifdef KFILEITEMLISTWIDGET_DEBUG
961 painter->setPen(Qt::blue);
962 painter->drawRect(QRectF(m_pixmapPos, QSizeF(m_scaledPixmapSize)));
963 #endif
964 } else {
965 painter->drawPixmap(m_pixmapPos, pixmap);
966 }
967 }
968
969 void KFileItemListWidget::drawSiblingsInformation(QPainter* painter)
970 {
971 const int siblingSize = size().height();
972 const int x = (m_expansionArea.left() + m_expansionArea.right() - siblingSize) / 2;
973 QRect siblingRect(x, 0, siblingSize, siblingSize);
974
975 QStyleOption option;
976 bool isItemSibling = true;
977
978 const QBitArray siblings = siblingsInformation();
979 for (int i = siblings.count() - 1; i >= 0; --i) {
980 option.rect = siblingRect;
981 option.state = siblings.at(i) ? QStyle::State_Sibling : QStyle::State_None;
982
983 if (isItemSibling) {
984 option.state |= QStyle::State_Item;
985 if (m_isExpandable) {
986 option.state |= QStyle::State_Children;
987 }
988 if (data()["isExpanded"].toBool()) {
989 option.state |= QStyle::State_Open;
990 }
991 isItemSibling = false;
992 }
993
994 style()->drawPrimitive(QStyle::PE_IndicatorBranch, &option, painter);
995
996 siblingRect.translate(-siblingRect.width(), 0);
997 }
998 }
999
1000 QPixmap KFileItemListWidget::pixmapForIcon(const QString& name, int size)
1001 {
1002 const KIcon icon(name);
1003
1004 int requestedSize;
1005 if (size <= KIconLoader::SizeSmall) {
1006 requestedSize = KIconLoader::SizeSmall;
1007 } else if (size <= KIconLoader::SizeSmallMedium) {
1008 requestedSize = KIconLoader::SizeSmallMedium;
1009 } else if (size <= KIconLoader::SizeMedium) {
1010 requestedSize = KIconLoader::SizeMedium;
1011 } else if (size <= KIconLoader::SizeLarge) {
1012 requestedSize = KIconLoader::SizeLarge;
1013 } else if (size <= KIconLoader::SizeHuge) {
1014 requestedSize = KIconLoader::SizeHuge;
1015 } else if (size <= KIconLoader::SizeEnormous) {
1016 requestedSize = KIconLoader::SizeEnormous;
1017 } else if (size <= KIconLoader::SizeEnormous * 2) {
1018 requestedSize = KIconLoader::SizeEnormous * 2;
1019 } else {
1020 requestedSize = size;
1021 }
1022
1023 QPixmap pixmap = icon.pixmap(requestedSize, requestedSize);
1024 if (requestedSize != size) {
1025 KPixmapModifier::scale(pixmap, QSize(size, size));
1026 }
1027
1028 return pixmap;
1029 }
1030
1031 void KFileItemListWidget::applyCutEffect(QPixmap& pixmap)
1032 {
1033 KIconEffect* effect = KIconLoader::global()->iconEffect();
1034 pixmap = effect->apply(pixmap, KIconLoader::Desktop, KIconLoader::DisabledState);
1035 }
1036
1037 void KFileItemListWidget::applyHiddenEffect(QPixmap& pixmap)
1038 {
1039 KIconEffect::semiTransparent(pixmap);
1040 }
1041
1042 KFileItemListWidget::RoleType KFileItemListWidget::roleType(const QByteArray& role)
1043 {
1044 static QHash<QByteArray, RoleType> rolesHash;
1045 if (rolesHash.isEmpty()) {
1046 rolesHash.insert("name", Name);
1047 rolesHash.insert("size", Size);
1048 rolesHash.insert("date", Date);
1049 rolesHash.insert("rating", Rating);
1050 }
1051
1052 return rolesHash.value(role, Generic);
1053 }
1054
1055 QString KFileItemListWidget::roleText(const QByteArray& role, const QHash<QByteArray, QVariant>& values)
1056 {
1057 QString text;
1058 const QVariant roleValue = values.value(role);
1059
1060 switch (roleType(role)) {
1061 case Size: {
1062 if (values.value("isDir").toBool()) {
1063 // The item represents a directory. Show the number of sub directories
1064 // instead of the file size of the directory.
1065 if (!roleValue.isNull()) {
1066 const int count = roleValue.toInt();
1067 if (count < 0) {
1068 text = i18nc("@item:intable", "Unknown");
1069 } else {
1070 text = i18ncp("@item:intable", "%1 item", "%1 items", count);
1071 }
1072 }
1073 } else {
1074 // Show the size in kilobytes (always round up)
1075 const KLocale* locale = KGlobal::locale();
1076 const int roundInc = (locale->binaryUnitDialect() == KLocale::MetricBinaryDialect) ? 499 : 511;
1077 const KIO::filesize_t size = roleValue.value<KIO::filesize_t>() + roundInc;
1078 text = locale->formatByteSize(size, 0, KLocale::DefaultBinaryDialect, KLocale::UnitKiloByte);
1079 }
1080 break;
1081 }
1082
1083 case Date: {
1084 const QDateTime dateTime = roleValue.toDateTime();
1085 text = KGlobal::locale()->formatDateTime(dateTime);
1086 break;
1087 }
1088
1089 case Rating:
1090 // Always use an empty text, as the rating is shown by the image m_rating.
1091 break;
1092
1093 case Name:
1094 case Generic:
1095 text = roleValue.toString();
1096 break;
1097
1098 default:
1099 Q_ASSERT(false);
1100 break;
1101 }
1102
1103 return text;
1104 }
1105
1106 QSizeF KFileItemListWidget::preferredRatingSize(const KItemListStyleOption& option)
1107 {
1108 const qreal height = option.fontMetrics.ascent();
1109 return QSizeF(height * 5, height);
1110 }
1111
1112 qreal KFileItemListWidget::columnPadding(const KItemListStyleOption& option)
1113 {
1114 return option.padding * 6;
1115 }
1116
1117 #include "kfileitemlistwidget.moc"