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