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