1/*
2 This file is part of the KDE project
3 SPDX-FileCopyrightText: 2009 Shaun Reich <shaun.reich@kdemail.net>
4 SPDX-FileCopyrightText: 2006-2007, 2008 Fredrik Höglund <fredrik@kde.org>
5
6 SPDX-License-Identifier: LGPL-2.0-or-later
7*/
8
9#include "kfileitemdelegate.h"
10#include "imagefilter_p.h"
11
12#include <QApplication>
13#include <QCache>
14#include <QImage>
15#include <QListView>
16#include <QLocale>
17#include <QMimeDatabase>
18#include <QModelIndex>
19#include <QPaintEngine>
20#include <QPainter>
21#include <QPainterPath>
22#include <QStyle>
23#include <QTextEdit>
24#include <QTextLayout>
25#include <qmath.h>
26
27#include <KIconEffect>
28#include <KIconLoader>
29#include <KLocalizedString>
30#include <KStatefulBrush>
31#include <KStringHandler>
32#include <kdirmodel.h>
33#include <kfileitem.h>
34
35#include "delegateanimationhandler_p.h"
36
37struct Margin {
38 int left, right, top, bottom;
39};
40
41class Q_DECL_HIDDEN KFileItemDelegate::Private
42{
43public:
44 enum MarginType {
45 ItemMargin = 0,
46 TextMargin,
47 IconMargin,
48 NMargins
49 };
50
51 explicit Private(KFileItemDelegate *parent);
52 ~Private()
53 {
54 }
55
56 QSize decorationSizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const;
57 QSize displaySizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const;
58 QString replaceNewlines(const QString &string) const;
59 inline KFileItem fileItem(const QModelIndex &index) const;
60 QString elidedText(QTextLayout &layout, const QStyleOptionViewItem &option, const QSize &maxSize) const;
61 QSize layoutText(QTextLayout &layout, const QStyleOptionViewItem &option, const QString &text, const QSize &constraints) const;
62 QSize layoutText(QTextLayout &layout, const QString &text, int maxWidth) const;
63 inline void setLayoutOptions(QTextLayout &layout, const QStyleOptionViewItem &options) const;
64 inline bool verticalLayout(const QStyleOptionViewItem &option) const;
65 inline QBrush brush(const QVariant &value, const QStyleOptionViewItem &option) const;
66 QBrush foregroundBrush(const QStyleOptionViewItem &option, const QModelIndex &index) const;
67 inline void setActiveMargins(Qt::Orientation layout);
68 void setVerticalMargin(MarginType type, int left, int right, int top, int bottom);
69 void setHorizontalMargin(MarginType type, int left, int right, int top, int bottom);
70 inline void setVerticalMargin(MarginType type, int hor, int ver);
71 inline void setHorizontalMargin(MarginType type, int hor, int ver);
72 inline QRect addMargin(const QRect &rect, MarginType type) const;
73 inline QRect subtractMargin(const QRect &rect, MarginType type) const;
74 inline QSize addMargin(const QSize &size, MarginType type) const;
75 inline QSize subtractMargin(const QSize &size, MarginType type) const;
76 QString itemSize(const QModelIndex &index, const KFileItem &item) const;
77 QString information(const QStyleOptionViewItem &option, const QModelIndex &index, const KFileItem &item) const;
78 bool isListView(const QStyleOptionViewItem &option) const;
79 QString display(const QModelIndex &index) const;
80 QIcon decoration(const QStyleOptionViewItem &option, const QModelIndex &index) const;
81 QPoint iconPosition(const QStyleOptionViewItem &option) const;
82 QRect labelRectangle(const QStyleOptionViewItem &option, const QModelIndex &index) const;
83 void layoutTextItems(const QStyleOptionViewItem &option,
84 const QModelIndex &index,
85 QTextLayout *labelLayout,
86 QTextLayout *infoLayout,
87 QRect *textBoundingRect) const;
88 void drawTextItems(QPainter *painter,
89 const QTextLayout &labelLayout,
90 const QColor &labelColor,
91 const QTextLayout &infoLayout,
92 const QColor &infoColor,
93 const QRect &textBoundingRect) const;
94 KIO::AnimationState *animationState(const QStyleOptionViewItem &option, const QModelIndex &index, const QAbstractItemView *view) const;
95 void restartAnimation(KIO::AnimationState *state);
96 QPixmap applyHoverEffect(const QPixmap &icon) const;
97 QPixmap transition(const QPixmap &from, const QPixmap &to, qreal amount) const;
98 void initStyleOption(QStyleOptionViewItem *option, const QModelIndex &index) const;
99 void drawFocusRect(QPainter *painter, const QStyleOptionViewItem &option, const QRect &rect) const;
100
101 void gotNewIcon(const QModelIndex &index);
102
103 void paintJobTransfers(QPainter *painter, const qreal &jobAnimationAngle, const QPoint &iconPos, const QStyleOptionViewItem &opt);
104 int scaledEmblemSize(int iconSize) const;
105
106public:
107 KFileItemDelegate::InformationList informationList;
108 QColor shadowColor;
109 QPointF shadowOffset;
110 qreal shadowBlur;
111 QSize maximumSize;
112 bool showToolTipWhenElided;
113 QTextOption::WrapMode wrapMode;
114 bool jobTransfersVisible;
115 QIcon downArrowIcon;
116 QRect emblemRect;
117
118private:
119 KIO::DelegateAnimationHandler *animationHandler;
120 Margin verticalMargin[NMargins];
121 Margin horizontalMargin[NMargins];
122 Margin *activeMargins;
123};
124
125KFileItemDelegate::Private::Private(KFileItemDelegate *parent)
126 : shadowColor(Qt::transparent)
127 , shadowOffset(1, 1)
128 , shadowBlur(2)
129 , maximumSize(0, 0)
130 , showToolTipWhenElided(true)
131 , wrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere)
132 , jobTransfersVisible(false)
133 , emblemRect(QRect())
134 , animationHandler(new KIO::DelegateAnimationHandler(parent))
135 , activeMargins(nullptr)
136{
137}
138
139void KFileItemDelegate::Private::setActiveMargins(Qt::Orientation layout)
140{
141 activeMargins = (layout == Qt::Horizontal ? horizontalMargin : verticalMargin);
142}
143
144void KFileItemDelegate::Private::setVerticalMargin(MarginType type, int left, int top, int right, int bottom)
145{
146 verticalMargin[type].left = left;
147 verticalMargin[type].right = right;
148 verticalMargin[type].top = top;
149 verticalMargin[type].bottom = bottom;
150}
151
152void KFileItemDelegate::Private::setHorizontalMargin(MarginType type, int left, int top, int right, int bottom)
153{
154 horizontalMargin[type].left = left;
155 horizontalMargin[type].right = right;
156 horizontalMargin[type].top = top;
157 horizontalMargin[type].bottom = bottom;
158}
159
160void KFileItemDelegate::Private::setVerticalMargin(MarginType type, int horizontal, int vertical)
161{
162 setVerticalMargin(type, left: horizontal, top: vertical, right: horizontal, bottom: vertical);
163}
164
165void KFileItemDelegate::Private::setHorizontalMargin(MarginType type, int horizontal, int vertical)
166{
167 setHorizontalMargin(type, left: horizontal, top: vertical, right: horizontal, bottom: vertical);
168}
169
170QRect KFileItemDelegate::Private::addMargin(const QRect &rect, MarginType type) const
171{
172 Q_ASSERT(activeMargins != nullptr);
173 const Margin &m = activeMargins[type];
174 return rect.adjusted(xp1: -m.left, yp1: -m.top, xp2: m.right, yp2: m.bottom);
175}
176
177QRect KFileItemDelegate::Private::subtractMargin(const QRect &rect, MarginType type) const
178{
179 Q_ASSERT(activeMargins != nullptr);
180 const Margin &m = activeMargins[type];
181 return rect.adjusted(xp1: m.left, yp1: m.top, xp2: -m.right, yp2: -m.bottom);
182}
183
184QSize KFileItemDelegate::Private::addMargin(const QSize &size, MarginType type) const
185{
186 Q_ASSERT(activeMargins != nullptr);
187 const Margin &m = activeMargins[type];
188 return QSize(size.width() + m.left + m.right, size.height() + m.top + m.bottom);
189}
190
191QSize KFileItemDelegate::Private::subtractMargin(const QSize &size, MarginType type) const
192{
193 Q_ASSERT(activeMargins != nullptr);
194 const Margin &m = activeMargins[type];
195 return QSize(size.width() - m.left - m.right, size.height() - m.top - m.bottom);
196}
197
198// Returns the size of a file, or the number of items in a directory, as a QString
199QString KFileItemDelegate::Private::itemSize(const QModelIndex &index, const KFileItem &item) const
200{
201 // Return a formatted string containing the file size, if the item is a file
202 if (item.isFile()) {
203 return KIO::convertSize(size: item.size());
204 }
205
206 // Return the number of items in the directory
207 const QVariant value = index.data(arole: KDirModel::ChildCountRole);
208 const int count = value.typeId() == QMetaType::Int ? value.toInt() : KDirModel::ChildCountUnknown;
209
210 if (count == KDirModel::ChildCountUnknown) {
211 // was: i18nc("Items in a folder", "? items");
212 // but this just looks useless in a remote directory listing,
213 // better not show anything.
214 return QString();
215 }
216
217 return i18ncp("Items in a folder", "1 item", "%1 items", count);
218}
219
220// Returns the additional information string, if one should be shown, or an empty string otherwise
221QString KFileItemDelegate::Private::information(const QStyleOptionViewItem &option, const QModelIndex &index, const KFileItem &item) const
222{
223 QString string;
224
225 if (informationList.isEmpty() || item.isNull() || !isListView(option)) {
226 return string;
227 }
228
229 for (KFileItemDelegate::Information info : informationList) {
230 if (info == KFileItemDelegate::NoInformation) {
231 continue;
232 }
233
234 if (!string.isEmpty()) {
235 string += QChar::LineSeparator;
236 }
237
238 switch (info) {
239 case KFileItemDelegate::Size:
240 string += itemSize(index, item);
241 break;
242
243 case KFileItemDelegate::Permissions:
244 string += item.permissionsString();
245 break;
246
247 case KFileItemDelegate::OctalPermissions:
248 string += QLatin1Char('0') + QString::number(item.permissions(), base: 8);
249 break;
250
251 case KFileItemDelegate::Owner:
252 string += item.user();
253 break;
254
255 case KFileItemDelegate::OwnerAndGroup:
256 string += item.user() + QLatin1Char(':') + item.group();
257 break;
258
259 case KFileItemDelegate::CreationTime:
260 string += item.timeString(which: KFileItem::CreationTime);
261 break;
262
263 case KFileItemDelegate::ModificationTime:
264 string += item.timeString(which: KFileItem::ModificationTime);
265 break;
266
267 case KFileItemDelegate::AccessTime:
268 string += item.timeString(which: KFileItem::AccessTime);
269 break;
270
271 case KFileItemDelegate::MimeType:
272 string += item.isMimeTypeKnown() ? item.mimetype() : i18nc("@info mimetype", "Unknown");
273 break;
274
275 case KFileItemDelegate::FriendlyMimeType:
276 string += item.isMimeTypeKnown() ? item.mimeComment() : i18nc("@info mimetype", "Unknown");
277 break;
278
279 case KFileItemDelegate::LinkDest:
280 string += item.linkDest();
281 break;
282
283 case KFileItemDelegate::LocalPathOrUrl:
284 if (!item.localPath().isEmpty()) {
285 string += item.localPath();
286 } else {
287 string += item.url().toDisplayString();
288 }
289 break;
290
291 case KFileItemDelegate::Comment:
292 string += item.comment();
293 break;
294
295 default:
296 break;
297 } // switch (info)
298 } // for (info, list)
299
300 return string;
301}
302
303// Returns the KFileItem for the index
304KFileItem KFileItemDelegate::Private::fileItem(const QModelIndex &index) const
305{
306 const QVariant value = index.data(arole: KDirModel::FileItemRole);
307 return qvariant_cast<KFileItem>(v: value);
308}
309
310// Replaces any newline characters in the provided string, with QChar::LineSeparator
311QString KFileItemDelegate::Private::replaceNewlines(const QString &text) const
312{
313 QString string = text;
314 string.replace(before: QLatin1Char('\n'), after: QChar::LineSeparator);
315 return string;
316}
317
318// Lays the text out in a rectangle no larger than constraints, eliding it as necessary
319QSize KFileItemDelegate::Private::layoutText(QTextLayout &layout, const QStyleOptionViewItem &option, const QString &text, const QSize &constraints) const
320{
321 const QSize size = layoutText(layout, text, maxWidth: constraints.width());
322
323 if (size.width() > constraints.width() || size.height() > constraints.height()) {
324 const QString elided = elidedText(layout, option, maxSize: constraints);
325 return layoutText(layout, text: elided, maxWidth: constraints.width());
326 }
327
328 return size;
329}
330
331// Lays the text out in a rectangle no wider than maxWidth
332QSize KFileItemDelegate::Private::layoutText(QTextLayout &layout, const QString &text, int maxWidth) const
333{
334 QFontMetrics metrics(layout.font());
335 int leading = metrics.leading();
336 int height = 0;
337 qreal widthUsed = 0;
338 QTextLine line;
339
340 layout.setText(text);
341
342 layout.beginLayout();
343 while ((line = layout.createLine()).isValid()) {
344 line.setLineWidth(maxWidth);
345 height += leading;
346 line.setPosition(QPoint(0, height));
347 height += int(line.height());
348 widthUsed = qMax(a: widthUsed, b: line.naturalTextWidth());
349 }
350 layout.endLayout();
351
352 return QSize(qCeil(v: widthUsed), height);
353}
354
355// Elides the text in the layout, by iterating over each line in the layout, eliding
356// or word breaking the line if it's wider than the max width, and finally adding an
357// ellipses at the end of the last line, if there are more lines than will fit within
358// the vertical size constraints.
359QString KFileItemDelegate::Private::elidedText(QTextLayout &layout, const QStyleOptionViewItem &option, const QSize &size) const
360{
361 const QString text = layout.text();
362 int maxWidth = size.width();
363 int maxHeight = size.height();
364 qreal height = 0;
365 bool wrapText = (option.features & QStyleOptionViewItem::WrapText);
366
367 // If the string contains a single line of text that shouldn't be word wrapped
368 if (!wrapText && text.indexOf(ch: QChar::LineSeparator) == -1) {
369 return option.fontMetrics.elidedText(text, mode: option.textElideMode, width: maxWidth);
370 }
371
372 // Elide each line that has already been laid out in the layout.
373 QString elided;
374 elided.reserve(asize: text.length());
375
376 for (int i = 0; i < layout.lineCount(); i++) {
377 QTextLine line = layout.lineAt(i);
378 const int start = line.textStart();
379 const int length = line.textLength();
380
381 height += option.fontMetrics.leading();
382 if (height + line.height() + option.fontMetrics.lineSpacing() > maxHeight) {
383 // Unfortunately, if the line ends because of a line separator, elidedText() will be too
384 // clever and keep adding lines until it finds one that's too wide.
385 if (line.naturalTextWidth() < maxWidth && text[start + length - 1] == QChar::LineSeparator) {
386 elided += QStringView(text).mid(pos: start, n: length - 1);
387 } else {
388 elided += option.fontMetrics.elidedText(text: text.mid(position: start), mode: option.textElideMode, width: maxWidth);
389 }
390 break;
391 } else if (line.naturalTextWidth() > maxWidth) {
392 elided += option.fontMetrics.elidedText(text: text.mid(position: start, n: length), mode: option.textElideMode, width: maxWidth);
393 if (!elided.endsWith(c: QChar::LineSeparator)) {
394 elided += QChar::LineSeparator;
395 }
396 } else {
397 elided += QStringView(text).mid(pos: start, n: length);
398 }
399
400 height += line.height();
401 }
402
403 return elided;
404}
405
406void KFileItemDelegate::Private::setLayoutOptions(QTextLayout &layout, const QStyleOptionViewItem &option) const
407{
408 QTextOption textoption;
409 textoption.setTextDirection(option.direction);
410 textoption.setAlignment(QStyle::visualAlignment(direction: option.direction, alignment: option.displayAlignment));
411 textoption.setWrapMode((option.features & QStyleOptionViewItem::WrapText) ? wrapMode : QTextOption::NoWrap);
412
413 layout.setFont(option.font);
414 layout.setTextOption(textoption);
415}
416
417QSize KFileItemDelegate::Private::displaySizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
418{
419 QString label = option.text;
420 int maxWidth = 0;
421 if (maximumSize.isEmpty()) {
422 maxWidth = verticalLayout(option) && (option.features & QStyleOptionViewItem::WrapText) ? option.decorationSize.width() + 10 : 32757;
423 } else {
424 const Margin &itemMargin = activeMargins[ItemMargin];
425 const Margin &textMargin = activeMargins[TextMargin];
426 maxWidth = maximumSize.width() - (itemMargin.left + itemMargin.right) - (textMargin.left + textMargin.right);
427 }
428
429 KFileItem item = fileItem(index);
430
431 // To compute the nominal size for the label + info, we'll just append
432 // the information string to the label
433 const QString info = information(option, index, item);
434 if (!info.isEmpty()) {
435 label += QChar(QChar::LineSeparator) + info;
436 }
437
438 QTextLayout layout;
439 setLayoutOptions(layout, option);
440
441 QSize size = layoutText(layout, text: label, maxWidth);
442 if (!info.isEmpty()) {
443 // As soon as additional information is shown, it might be necessary that
444 // the label and/or the additional information must get elided. To prevent
445 // an expensive eliding in the scope of displaySizeHint, the maximum
446 // width is reserved instead.
447 size.setWidth(maxWidth);
448 }
449
450 return addMargin(size, type: TextMargin);
451}
452
453QSize KFileItemDelegate::Private::decorationSizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
454{
455 if (index.column() > 0) {
456 return QSize(0, 0);
457 }
458
459 QSize iconSize = option.icon.actualSize(size: option.decorationSize);
460 if (!verticalLayout(option)) {
461 iconSize.rwidth() = option.decorationSize.width();
462 } else if (iconSize.width() < option.decorationSize.width()) {
463 iconSize.rwidth() = qMin(a: iconSize.width() + 10, b: option.decorationSize.width());
464 }
465 if (iconSize.height() < option.decorationSize.height()) {
466 iconSize.rheight() = option.decorationSize.height();
467 }
468
469 return addMargin(size: iconSize, type: IconMargin);
470}
471
472bool KFileItemDelegate::Private::verticalLayout(const QStyleOptionViewItem &option) const
473{
474 return (option.decorationPosition == QStyleOptionViewItem::Top || option.decorationPosition == QStyleOptionViewItem::Bottom);
475}
476
477// Converts a QVariant of type Brush or Color to a QBrush
478QBrush KFileItemDelegate::Private::brush(const QVariant &value, const QStyleOptionViewItem &option) const
479{
480 if (value.userType() == qMetaTypeId<KStatefulBrush>()) {
481 return qvariant_cast<KStatefulBrush>(v: value).brush(option.palette);
482 }
483 switch (value.typeId()) {
484 case QMetaType::QColor:
485 return QBrush(qvariant_cast<QColor>(v: value));
486
487 case QMetaType::QBrush:
488 return qvariant_cast<QBrush>(v: value);
489
490 default:
491 return QBrush(Qt::NoBrush);
492 }
493}
494
495QBrush KFileItemDelegate::Private::foregroundBrush(const QStyleOptionViewItem &option, const QModelIndex &index) const
496{
497 QPalette::ColorGroup cg = QPalette::Active;
498 if (!(option.state & QStyle::State_Enabled)) {
499 cg = QPalette::Disabled;
500 } else if (!(option.state & QStyle::State_Active)) {
501 cg = QPalette::Inactive;
502 }
503
504 // Always use the highlight color for selected items
505 if (option.state & QStyle::State_Selected) {
506 return option.palette.brush(cg, cr: QPalette::HighlightedText);
507 }
508
509 // If the model provides its own foreground color/brush for this item
510 const QVariant value = index.data(arole: Qt::ForegroundRole);
511 if (value.isValid()) {
512 return brush(value, option);
513 }
514
515 return option.palette.brush(cg, cr: QPalette::Text);
516}
517
518bool KFileItemDelegate::Private::isListView(const QStyleOptionViewItem &option) const
519{
520 if (qobject_cast<const QListView *>(object: option.widget) || verticalLayout(option)) {
521 return true;
522 }
523
524 return false;
525}
526
527QPixmap KFileItemDelegate::Private::applyHoverEffect(const QPixmap &icon) const
528{
529 QPixmap result = icon;
530 KIconEffect::toActive(pixmap&: result);
531 return result;
532}
533
534void KFileItemDelegate::Private::gotNewIcon(const QModelIndex &index)
535{
536 animationHandler->gotNewIcon(index);
537}
538
539void KFileItemDelegate::Private::restartAnimation(KIO::AnimationState *state)
540{
541 animationHandler->restartAnimation(state);
542}
543
544KIO::AnimationState *
545KFileItemDelegate::Private::animationState(const QStyleOptionViewItem &option, const QModelIndex &index, const QAbstractItemView *view) const
546{
547 if (!option.widget->style()->styleHint(stylehint: QStyle::SH_Widget_Animate, opt: nullptr, widget: option.widget)) {
548 return nullptr;
549 }
550
551 if (index.column() == KDirModel::Name) {
552 return animationHandler->animationState(option, index, view);
553 }
554
555 return nullptr;
556}
557
558QPixmap KFileItemDelegate::Private::transition(const QPixmap &from, const QPixmap &to, qreal amount) const
559{
560 int value = int(0xff * amount);
561
562 if (value == 0 || to.isNull()) {
563 return from;
564 }
565
566 if (value == 0xff || from.isNull()) {
567 return to;
568 }
569
570 QColor color;
571 color.setAlphaF(amount);
572
573// FIXME: Somehow this doesn't work on Mac OS..
574#if defined(Q_OS_MAC)
575 const bool usePixmap = false;
576#else
577 const bool usePixmap = from.paintEngine()->hasFeature(feature: QPaintEngine::PorterDuff) && from.paintEngine()->hasFeature(feature: QPaintEngine::BlendModes);
578#endif
579
580 // If the native paint engine supports Porter/Duff compositing and CompositionMode_Plus
581 if (usePixmap) {
582 QPixmap under = from;
583 QPixmap over = to;
584
585 QPainter p;
586 p.begin(&over);
587 p.setCompositionMode(QPainter::CompositionMode_DestinationIn);
588 p.fillRect(over.rect(), color);
589 p.end();
590
591 p.begin(&under);
592 p.setCompositionMode(QPainter::CompositionMode_DestinationOut);
593 p.fillRect(under.rect(), color);
594 p.setCompositionMode(QPainter::CompositionMode_Plus);
595 p.drawPixmap(x: 0, y: 0, pm: over);
596 p.end();
597
598 return under;
599 } else {
600 // Fall back to using QRasterPaintEngine to do the transition.
601 QImage under = from.toImage();
602 QImage over = to.toImage();
603
604 QPainter p;
605 p.begin(&over);
606 p.setCompositionMode(QPainter::CompositionMode_DestinationIn);
607 p.fillRect(over.rect(), color);
608 p.end();
609
610 p.begin(&under);
611 p.setCompositionMode(QPainter::CompositionMode_DestinationOut);
612 p.fillRect(under.rect(), color);
613 p.setCompositionMode(QPainter::CompositionMode_Plus);
614 p.drawImage(x: 0, y: 0, image: over);
615 p.end();
616
617 return QPixmap::fromImage(image: under);
618 }
619}
620
621void KFileItemDelegate::Private::layoutTextItems(const QStyleOptionViewItem &option,
622 const QModelIndex &index,
623 QTextLayout *labelLayout,
624 QTextLayout *infoLayout,
625 QRect *textBoundingRect) const
626{
627 KFileItem item = fileItem(index);
628 const QString info = information(option, index, item);
629 bool showInformation = false;
630
631 setLayoutOptions(layout&: *labelLayout, option);
632
633 const QRect textArea = labelRectangle(option, index);
634 QRect textRect = subtractMargin(rect: textArea, type: Private::TextMargin);
635
636 // Sizes and constraints for the different text parts
637 QSize maxLabelSize = textRect.size();
638 QSize maxInfoSize = textRect.size();
639 QSize labelSize;
640 QSize infoSize;
641
642 // If we have additional info text, and there's space for at least two lines of text,
643 // adjust the max label size to make room for at least one line of the info text
644 if (!info.isEmpty() && textRect.height() >= option.fontMetrics.lineSpacing() * 2) {
645 infoLayout->setFont(labelLayout->font());
646 infoLayout->setTextOption(labelLayout->textOption());
647
648 maxLabelSize.rheight() -= option.fontMetrics.lineSpacing();
649 showInformation = true;
650 }
651
652 // Lay out the label text, and adjust the max info size based on the label size
653 labelSize = layoutText(layout&: *labelLayout, option, text: option.text, constraints: maxLabelSize);
654 maxInfoSize.rheight() -= labelSize.height();
655
656 // Lay out the info text
657 if (showInformation) {
658 infoSize = layoutText(layout&: *infoLayout, option, text: info, constraints: maxInfoSize);
659 } else {
660 infoSize = QSize(0, 0);
661 }
662
663 // Compute the bounding rect of the text
664 const QSize size(qMax(a: labelSize.width(), b: infoSize.width()), labelSize.height() + infoSize.height());
665 *textBoundingRect = QStyle::alignedRect(direction: option.direction, alignment: option.displayAlignment, size, rectangle: textRect);
666
667 // Compute the positions where we should draw the layouts
668 labelLayout->setPosition(QPointF(textRect.x(), textBoundingRect->y()));
669 infoLayout->setPosition(QPointF(textRect.x(), textBoundingRect->y() + labelSize.height()));
670}
671
672void KFileItemDelegate::Private::drawTextItems(QPainter *painter,
673 const QTextLayout &labelLayout,
674 const QColor &labelColor,
675 const QTextLayout &infoLayout,
676 const QColor &infoColor,
677 const QRect &boundingRect) const
678{
679 if (shadowColor.alpha() > 0) {
680 QPixmap pixmap(boundingRect.size());
681 pixmap.fill(fillColor: Qt::transparent);
682
683 QPainter p(&pixmap);
684 p.translate(offset: -boundingRect.topLeft());
685 p.setPen(labelColor);
686 labelLayout.draw(p: &p, pos: QPoint());
687
688 if (!infoLayout.text().isEmpty()) {
689 p.setPen(infoColor);
690 infoLayout.draw(p: &p, pos: QPoint());
691 }
692 p.end();
693
694 int padding = qCeil(v: shadowBlur);
695 int blurFactor = qRound(d: shadowBlur);
696
697 QImage image(boundingRect.size() + QSize(padding * 2, padding * 2), QImage::Format_ARGB32_Premultiplied);
698 image.fill(pixel: 0);
699 p.begin(&image);
700 p.drawImage(x: padding, y: padding, image: pixmap.toImage());
701 p.end();
702
703 KIO::ImageFilter::shadowBlur(image, radius: blurFactor, color: shadowColor);
704
705 painter->drawImage(p: boundingRect.topLeft() - QPoint(padding, padding) + shadowOffset.toPoint(), image);
706 painter->drawPixmap(p: boundingRect.topLeft(), pm: pixmap);
707 return;
708 }
709
710 painter->save();
711 painter->setPen(labelColor);
712
713 labelLayout.draw(p: painter, pos: QPoint());
714 if (!infoLayout.text().isEmpty()) {
715 // TODO - for apps not doing funny things with the color palette,
716 // KColorScheme::InactiveText would be a much more correct choice. We
717 // should provide an API to specify what color to use for information.
718 painter->setPen(infoColor);
719 infoLayout.draw(p: painter, pos: QPoint());
720 }
721
722 painter->restore();
723}
724
725void KFileItemDelegate::Private::initStyleOption(QStyleOptionViewItem *option, const QModelIndex &index) const
726{
727 const KFileItem item = fileItem(index);
728 bool updateFontMetrics = false;
729
730 // Try to get the font from the model
731 QVariant value = index.data(arole: Qt::FontRole);
732 if (value.isValid()) {
733 option->font = qvariant_cast<QFont>(v: value).resolve(option->font);
734 updateFontMetrics = true;
735 }
736
737 // Use an italic font for symlinks
738 if (!item.isNull() && item.isLink()) {
739 option->font.setItalic(true);
740 updateFontMetrics = true;
741 }
742
743 if (updateFontMetrics) {
744 option->fontMetrics = QFontMetrics(option->font);
745 }
746
747 // Try to get the alignment for the item from the model
748 value = index.data(arole: Qt::TextAlignmentRole);
749 if (value.isValid()) {
750 option->displayAlignment = Qt::Alignment(value.toInt());
751 }
752
753 value = index.data(arole: Qt::BackgroundRole);
754 if (value.isValid()) {
755 option->backgroundBrush = brush(value, option: *option);
756 }
757
758 option->text = display(index);
759 if (!option->text.isEmpty()) {
760 option->features |= QStyleOptionViewItem::HasDisplay;
761 }
762
763 option->icon = decoration(option: *option, index);
764 // Note that even null icons are still drawn for alignment
765 if (!option->icon.isNull()) {
766 option->features |= QStyleOptionViewItem::HasDecoration;
767 }
768
769 // ### Make sure this value is always true for now
770 option->showDecorationSelected = true;
771}
772
773void KFileItemDelegate::Private::paintJobTransfers(QPainter *painter, const qreal &jobAnimationAngle, const QPoint &iconPos, const QStyleOptionViewItem &opt)
774{
775 painter->save();
776 QSize iconSize = opt.icon.actualSize(size: opt.decorationSize);
777 QPixmap downArrow = downArrowIcon.pixmap(size: iconSize * 0.30);
778 // corner (less x and y than bottom-right corner) that we will center the painter around
779 QPoint bottomRightCorner = QPoint(iconPos.x() + iconSize.width() * 0.75, iconPos.y() + iconSize.height() * 0.60);
780
781 QPainter pixmapPainter(&downArrow);
782 // make the icon transparent and such
783 pixmapPainter.setCompositionMode(QPainter::CompositionMode_DestinationIn);
784 pixmapPainter.fillRect(downArrow.rect(), color: QColor(255, 255, 255, 110));
785
786 painter->translate(offset: bottomRightCorner);
787
788 painter->drawPixmap(x: -downArrow.size().width() * .50, y: -downArrow.size().height() * .50, pm: downArrow);
789
790 // animate the circles by rotating the painter around the center point..
791 painter->rotate(a: jobAnimationAngle);
792 painter->setPen(QColor(20, 20, 20, 80));
793 painter->setBrush(QColor(250, 250, 250, 90));
794
795 int radius = iconSize.width() * 0.04;
796 int spacing = radius * 4.5;
797
798 // left
799 painter->drawEllipse(center: QPoint(-spacing, 0), rx: radius, ry: radius);
800 // right
801 painter->drawEllipse(center: QPoint(spacing, 0), rx: radius, ry: radius);
802 // up
803 painter->drawEllipse(center: QPoint(0, -spacing), rx: radius, ry: radius);
804 // down
805 painter->drawEllipse(center: QPoint(0, spacing), rx: radius, ry: radius);
806 painter->restore();
807}
808
809// ---------------------------------------------------------------------------
810
811KFileItemDelegate::KFileItemDelegate(QObject *parent)
812 : QAbstractItemDelegate(parent)
813 , d(new Private(this))
814{
815 int focusHMargin = QApplication::style()->pixelMetric(metric: QStyle::PM_FocusFrameHMargin);
816 int focusVMargin = QApplication::style()->pixelMetric(metric: QStyle::PM_FocusFrameVMargin);
817
818 // Margins for horizontal mode (list views, tree views, table views)
819 const int textMargin = focusHMargin * 4;
820 if (QApplication::isRightToLeft()) {
821 d->setHorizontalMargin(type: Private::TextMargin, left: textMargin, top: focusVMargin, right: focusHMargin, bottom: focusVMargin);
822 } else {
823 d->setHorizontalMargin(type: Private::TextMargin, left: focusHMargin, top: focusVMargin, right: textMargin, bottom: focusVMargin);
824 }
825
826 d->setHorizontalMargin(type: Private::IconMargin, horizontal: focusHMargin, vertical: focusVMargin);
827 d->setHorizontalMargin(type: Private::ItemMargin, horizontal: 0, vertical: 0);
828
829 // Margins for vertical mode (icon views)
830 d->setVerticalMargin(type: Private::TextMargin, horizontal: 6, vertical: 0);
831 d->setVerticalMargin(type: Private::IconMargin, horizontal: focusHMargin, vertical: focusVMargin);
832 d->setVerticalMargin(type: Private::ItemMargin, horizontal: 0, vertical: 0);
833
834 setShowInformation(NoInformation);
835}
836
837KFileItemDelegate::~KFileItemDelegate() = default;
838
839QSize KFileItemDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
840{
841 // If the model wants to provide its own size hint for the item
842 const QVariant value = index.data(arole: Qt::SizeHintRole);
843 if (value.isValid()) {
844 return qvariant_cast<QSize>(v: value);
845 }
846
847 QStyleOptionViewItem opt(option);
848 d->initStyleOption(option: &opt, index);
849 d->setActiveMargins(d->verticalLayout(option: opt) ? Qt::Vertical : Qt::Horizontal);
850
851 const QSize displaySize = d->displaySizeHint(option: opt, index);
852 const QSize decorationSize = d->decorationSizeHint(option: opt, index);
853
854 QSize size;
855
856 if (d->verticalLayout(option: opt)) {
857 size.rwidth() = qMax(a: displaySize.width(), b: decorationSize.width());
858 size.rheight() = decorationSize.height() + displaySize.height() + 1;
859 } else {
860 size.rwidth() = decorationSize.width() + displaySize.width() + 1;
861 size.rheight() = qMax(a: decorationSize.height(), b: displaySize.height());
862 }
863
864 size = d->addMargin(size, type: Private::ItemMargin);
865 if (!d->maximumSize.isEmpty()) {
866 size = size.boundedTo(otherSize: d->maximumSize);
867 }
868
869 return size;
870}
871
872QString KFileItemDelegate::Private::display(const QModelIndex &index) const
873{
874 const QVariant value = index.data(arole: Qt::DisplayRole);
875
876 switch (value.typeId()) {
877 case QMetaType::QString: {
878 if (index.column() == KDirModel::Size) {
879 return itemSize(index, item: fileItem(index));
880 } else {
881 const QString text = replaceNewlines(text: value.toString());
882 return KStringHandler::preProcessWrap(text);
883 }
884 }
885
886 case QMetaType::Double:
887 return QLocale().toString(f: value.toDouble(), format: 'f');
888
889 case QMetaType::Int:
890 case QMetaType::UInt:
891 return QLocale().toString(i: value.toInt());
892
893 default:
894 return QString();
895 }
896}
897
898void KFileItemDelegate::setShowInformation(const InformationList &list)
899{
900 d->informationList = list;
901}
902
903void KFileItemDelegate::setShowInformation(Information value)
904{
905 if (value != NoInformation) {
906 d->informationList = InformationList() << value;
907 } else {
908 d->informationList = InformationList();
909 }
910}
911
912KFileItemDelegate::InformationList KFileItemDelegate::showInformation() const
913{
914 return d->informationList;
915}
916
917void KFileItemDelegate::setShadowColor(const QColor &color)
918{
919 d->shadowColor = color;
920}
921
922QColor KFileItemDelegate::shadowColor() const
923{
924 return d->shadowColor;
925}
926
927void KFileItemDelegate::setShadowOffset(const QPointF &offset)
928{
929 d->shadowOffset = offset;
930}
931
932QPointF KFileItemDelegate::shadowOffset() const
933{
934 return d->shadowOffset;
935}
936
937void KFileItemDelegate::setShadowBlur(qreal factor)
938{
939 d->shadowBlur = factor;
940}
941
942qreal KFileItemDelegate::shadowBlur() const
943{
944 return d->shadowBlur;
945}
946
947void KFileItemDelegate::setMaximumSize(const QSize &size)
948{
949 d->maximumSize = size;
950}
951
952QSize KFileItemDelegate::maximumSize() const
953{
954 return d->maximumSize;
955}
956
957void KFileItemDelegate::setShowToolTipWhenElided(bool showToolTip)
958{
959 d->showToolTipWhenElided = showToolTip;
960}
961
962bool KFileItemDelegate::showToolTipWhenElided() const
963{
964 return d->showToolTipWhenElided;
965}
966
967void KFileItemDelegate::setWrapMode(QTextOption::WrapMode wrapMode)
968{
969 d->wrapMode = wrapMode;
970}
971
972QTextOption::WrapMode KFileItemDelegate::wrapMode() const
973{
974 return d->wrapMode;
975}
976
977QRect KFileItemDelegate::iconRect(const QStyleOptionViewItem &option, const QModelIndex &index) const
978{
979 if (index.column() > 0) {
980 return QRect(0, 0, 0, 0);
981 }
982 QStyleOptionViewItem opt(option);
983 d->initStyleOption(option: &opt, index);
984 return QRect(d->iconPosition(option: opt), opt.icon.actualSize(size: opt.decorationSize));
985}
986
987void KFileItemDelegate::setJobTransfersVisible(bool jobTransfersVisible)
988{
989 d->downArrowIcon = QIcon::fromTheme(QStringLiteral("go-down"));
990 d->jobTransfersVisible = jobTransfersVisible;
991}
992
993bool KFileItemDelegate::jobTransfersVisible() const
994{
995 return d->jobTransfersVisible;
996}
997
998QIcon KFileItemDelegate::Private::decoration(const QStyleOptionViewItem &option, const QModelIndex &index) const
999{
1000 const QVariant value = index.data(arole: Qt::DecorationRole);
1001 QIcon icon;
1002
1003 switch (value.typeId()) {
1004 case QMetaType::QIcon:
1005 icon = qvariant_cast<QIcon>(v: value);
1006 break;
1007
1008 case QMetaType::QPixmap:
1009 icon.addPixmap(pixmap: qvariant_cast<QPixmap>(v: value));
1010 break;
1011
1012 case QMetaType::QColor: {
1013 QPixmap pixmap(option.decorationSize);
1014 pixmap.fill(fillColor: qvariant_cast<QColor>(v: value));
1015 icon.addPixmap(pixmap);
1016 break;
1017 }
1018
1019 default:
1020 break;
1021 }
1022
1023 return icon;
1024}
1025
1026QRect KFileItemDelegate::Private::labelRectangle(const QStyleOptionViewItem &option, const QModelIndex &index) const
1027{
1028 const QSize decoSize = (index.column() == 0) ? addMargin(size: option.decorationSize, type: Private::IconMargin) : QSize(0, 0);
1029 const QRect itemRect = subtractMargin(rect: option.rect, type: Private::ItemMargin);
1030 QRect textArea(QPoint(0, 0), itemRect.size());
1031
1032 switch (option.decorationPosition) {
1033 case QStyleOptionViewItem::Top:
1034 textArea.setTop(decoSize.height() + 1);
1035 break;
1036
1037 case QStyleOptionViewItem::Bottom:
1038 textArea.setBottom(itemRect.height() - decoSize.height() - 1);
1039 break;
1040
1041 case QStyleOptionViewItem::Left:
1042 textArea.setLeft(decoSize.width() + 1);
1043 break;
1044
1045 case QStyleOptionViewItem::Right:
1046 textArea.setRight(itemRect.width() - decoSize.width() - 1);
1047 break;
1048 }
1049
1050 textArea.translate(p: itemRect.topLeft());
1051 return QStyle::visualRect(direction: option.direction, boundingRect: option.rect, logicalRect: textArea);
1052}
1053
1054QPoint KFileItemDelegate::Private::iconPosition(const QStyleOptionViewItem &option) const
1055{
1056 if (option.index.column() > 0) {
1057 return QPoint(0, 0);
1058 }
1059
1060 const QRect itemRect = subtractMargin(rect: option.rect, type: Private::ItemMargin);
1061 Qt::Alignment alignment;
1062
1063 // Convert decorationPosition to the alignment the decoration will have in option.rect
1064 switch (option.decorationPosition) {
1065 case QStyleOptionViewItem::Top:
1066 alignment = Qt::AlignHCenter | Qt::AlignTop;
1067 break;
1068
1069 case QStyleOptionViewItem::Bottom:
1070 alignment = Qt::AlignHCenter | Qt::AlignBottom;
1071 break;
1072
1073 case QStyleOptionViewItem::Left:
1074 alignment = Qt::AlignVCenter | Qt::AlignLeft;
1075 break;
1076
1077 case QStyleOptionViewItem::Right:
1078 alignment = Qt::AlignVCenter | Qt::AlignRight;
1079 break;
1080 }
1081
1082 // Compute the nominal decoration rectangle
1083 const QSize size = addMargin(size: option.decorationSize, type: Private::IconMargin);
1084 const QRect rect = QStyle::alignedRect(direction: option.direction, alignment, size, rectangle: itemRect);
1085
1086 // Position the icon in the center of the rectangle
1087 QRect iconRect = QRect(QPoint(), option.icon.actualSize(size: option.decorationSize));
1088 iconRect.moveCenter(p: rect.center());
1089
1090 return iconRect.topLeft();
1091}
1092
1093void KFileItemDelegate::Private::drawFocusRect(QPainter *painter, const QStyleOptionViewItem &option, const QRect &rect) const
1094{
1095 if (!(option.state & QStyle::State_HasFocus)) {
1096 return;
1097 }
1098
1099 QStyleOptionFocusRect opt;
1100 opt.direction = option.direction;
1101 opt.fontMetrics = option.fontMetrics;
1102 opt.palette = option.palette;
1103 opt.rect = rect;
1104 opt.state = option.state | QStyle::State_KeyboardFocusChange | QStyle::State_Item;
1105 opt.backgroundColor = option.palette.color(cr: option.state & QStyle::State_Selected ? QPalette::Highlight : QPalette::Base);
1106
1107 // Apparently some widget styles expect this hint to not be set
1108 painter->setRenderHint(hint: QPainter::Antialiasing, on: false);
1109
1110 QStyle *style = option.widget ? option.widget->style() : QApplication::style();
1111 style->drawPrimitive(pe: QStyle::PE_FrameFocusRect, opt: &opt, p: painter, w: option.widget);
1112
1113 painter->setRenderHint(hint: QPainter::Antialiasing);
1114}
1115
1116void KFileItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
1117{
1118 if (!index.isValid()) {
1119 return;
1120 }
1121
1122 QStyleOptionViewItem opt(option);
1123 d->initStyleOption(option: &opt, index);
1124 d->setActiveMargins(d->verticalLayout(option: opt) ? Qt::Vertical : Qt::Horizontal);
1125
1126 if (!(option.state & QStyle::State_Enabled)) {
1127 opt.palette.setCurrentColorGroup(QPalette::Disabled);
1128 }
1129
1130 // Unset the mouse over bit if we're not drawing the first column
1131 if (index.column() > 0) {
1132 opt.state &= ~QStyle::State_MouseOver;
1133 } else {
1134 opt.viewItemPosition = QStyleOptionViewItem::OnlyOne;
1135 }
1136
1137 const QAbstractItemView *view = qobject_cast<const QAbstractItemView *>(object: opt.widget);
1138 const qreal dpr = painter->device()->devicePixelRatioF();
1139
1140 // Check if the item is being animated
1141 // ========================================================================
1142 KIO::AnimationState *state = d->animationState(option: opt, index, view);
1143 KIO::CachedRendering *cache = nullptr;
1144 qreal progress = ((opt.state & QStyle::State_MouseOver) && index.column() == KDirModel::Name) ? 1.0 : 0.0;
1145 const QPoint iconPos = d->iconPosition(option: opt);
1146 QIcon::Mode iconMode;
1147
1148 if (!(option.state & QStyle::State_Enabled)) {
1149 iconMode = QIcon::Disabled;
1150 } else if ((option.state & QStyle::State_Selected) && (option.state & QStyle::State_Active)) {
1151 iconMode = QIcon::Selected;
1152 } else {
1153 iconMode = QIcon::Normal;
1154 }
1155
1156 QIcon::State iconState = option.state & QStyle::State_Open ? QIcon::On : QIcon::Off;
1157 QPixmap icon = opt.icon.pixmap(size: opt.decorationSize, devicePixelRatio: dpr, mode: iconMode, state: iconState);
1158
1159 const KFileItem fileItem = d->fileItem(index);
1160 if (fileItem.isHidden()) {
1161 KIconEffect::semiTransparent(pixmap&: icon);
1162 }
1163
1164 if (state && !state->hasJobAnimation()) {
1165 cache = state->cachedRendering();
1166 progress = state->hoverProgress();
1167 // Clear the mouse over bit temporarily
1168 opt.state &= ~QStyle::State_MouseOver;
1169
1170 // If we have a cached rendering, draw the item from the cache
1171 if (cache) {
1172 if (cache->checkValidity(current: opt.state) && cache->regular.size() == opt.rect.size()) {
1173 QPixmap pixmap = d->transition(from: cache->regular, to: cache->hover, amount: progress);
1174
1175 if (state->cachedRenderingFadeFrom() && state->fadeProgress() != 1.0) {
1176 // Apply icon fading animation
1177 KIO::CachedRendering *fadeFromCache = state->cachedRenderingFadeFrom();
1178 const QPixmap fadeFromPixmap = d->transition(from: fadeFromCache->regular, to: fadeFromCache->hover, amount: progress);
1179
1180 pixmap = d->transition(from: fadeFromPixmap, to: pixmap, amount: state->fadeProgress());
1181 }
1182 painter->drawPixmap(p: option.rect.topLeft(), pm: pixmap);
1183 drawSelectionEmblem(option, painter, index);
1184 if (d->jobTransfersVisible && index.column() == 0) {
1185 if (index.data(arole: KDirModel::HasJobRole).toBool()) {
1186 d->paintJobTransfers(painter, jobAnimationAngle: state->jobAnimationAngle(), iconPos, opt);
1187 }
1188 }
1189 return;
1190 }
1191
1192 if (!cache->checkValidity(current: opt.state)) {
1193 if (opt.widget->style()->styleHint(stylehint: QStyle::SH_Widget_Animate, opt: nullptr, widget: opt.widget)) {
1194 // Fade over from the old icon to the new one
1195 // Only start a new fade if the previous one is ready
1196 // Else we may start racing when checkValidity() always returns false
1197 if (state->fadeProgress() == 1) {
1198 state->setCachedRenderingFadeFrom(state->takeCachedRendering());
1199 }
1200 }
1201 d->gotNewIcon(index);
1202 }
1203 // If it wasn't valid, delete it
1204 state->setCachedRendering(nullptr);
1205 } else {
1206 // The cache may have been discarded, but the animation handler still needs to know about new icons
1207 d->gotNewIcon(index);
1208 }
1209 }
1210
1211 // Compute the metrics, and lay out the text items
1212 // ========================================================================
1213 QColor labelColor = d->foregroundBrush(option: opt, index).color();
1214 QColor infoColor = labelColor;
1215 if (!(option.state & QStyle::State_Selected)) {
1216 // the code below is taken from Dolphin
1217 const QColor c2 = option.palette.base().color();
1218 const int p1 = 70;
1219 const int p2 = 100 - p1;
1220 infoColor = QColor((labelColor.red() * p1 + c2.red() * p2) / 100,
1221 (labelColor.green() * p1 + c2.green() * p2) / 100,
1222 (labelColor.blue() * p1 + c2.blue() * p2) / 100);
1223
1224 if (fileItem.isHidden()) {
1225 labelColor = infoColor;
1226 }
1227 }
1228
1229 // ### Apply the selection effect to the icon when the item is selected and
1230 // showDecorationSelected is false.
1231
1232 QTextLayout labelLayout;
1233 QTextLayout infoLayout;
1234 QRect textBoundingRect;
1235
1236 d->layoutTextItems(option: opt, index, labelLayout: &labelLayout, infoLayout: &infoLayout, textBoundingRect: &textBoundingRect);
1237
1238 QStyle *style = opt.widget ? opt.widget->style() : QApplication::style();
1239
1240 int focusHMargin = style->pixelMetric(metric: QStyle::PM_FocusFrameHMargin);
1241 int focusVMargin = style->pixelMetric(metric: QStyle::PM_FocusFrameVMargin);
1242 QRect focusRect = textBoundingRect.adjusted(xp1: -focusHMargin, yp1: -focusVMargin, xp2: +focusHMargin, yp2: +focusVMargin);
1243
1244 auto drawBackground = [textBoundingRect, iconPos, style](QPainter &painter, QStyleOptionViewItem &opt) {
1245 if (opt.decorationPosition != QStyleOptionViewItem::Position::Top) {
1246 style->drawPrimitive(pe: QStyle::PE_PanelItemViewItem, opt: &opt, p: &painter, w: opt.widget);
1247 } else {
1248 auto oldrect = opt.rect;
1249 opt.rect = textBoundingRect;
1250 style->drawPrimitive(pe: QStyle::PE_PanelItemViewItem, opt: &opt, p: &painter, w: opt.widget);
1251 opt.rect = QRect(iconPos, opt.decorationSize);
1252 style->drawPrimitive(pe: QStyle::PE_PanelItemViewItem, opt: &opt, p: &painter, w: opt.widget);
1253 opt.rect = oldrect;
1254 }
1255 };
1256
1257 // Create a new cached rendering of a hovered and an unhovered item.
1258 // We don't create a new cache for a fully hovered item, since we don't
1259 // know yet if a hover out animation will be run.
1260 // ========================================================================
1261 if (state && (state->hoverProgress() < 1 || state->fadeProgress() < 1)) {
1262 cache = new KIO::CachedRendering(opt.state, option.rect.size(), index, dpr);
1263
1264 QPainter p;
1265 p.begin(&cache->regular);
1266 p.translate(offset: -option.rect.topLeft());
1267 p.setRenderHint(hint: QPainter::Antialiasing);
1268 drawBackground(p, opt);
1269 p.drawPixmap(p: iconPos, pm: icon);
1270 drawSelectionEmblem(option, painter, index);
1271 d->drawTextItems(painter: &p, labelLayout, labelColor, infoLayout, infoColor, boundingRect: textBoundingRect);
1272 d->drawFocusRect(painter: &p, option: opt, rect: focusRect);
1273 p.end();
1274
1275 opt.state |= QStyle::State_MouseOver;
1276 icon = d->applyHoverEffect(icon);
1277
1278 p.begin(&cache->hover);
1279 p.translate(offset: -option.rect.topLeft());
1280 p.setRenderHint(hint: QPainter::Antialiasing);
1281 drawBackground(p, opt);
1282 p.drawPixmap(p: iconPos, pm: icon);
1283 drawSelectionEmblem(option, painter, index);
1284 d->drawTextItems(painter: &p, labelLayout, labelColor, infoLayout, infoColor, boundingRect: textBoundingRect);
1285 d->drawFocusRect(painter: &p, option: opt, rect: focusRect);
1286 p.end();
1287
1288 state->setCachedRendering(cache);
1289
1290 QPixmap pixmap = d->transition(from: cache->regular, to: cache->hover, amount: progress);
1291
1292 if (state->cachedRenderingFadeFrom() && state->fadeProgress() == 0) {
1293 // Apply icon fading animation
1294 KIO::CachedRendering *fadeFromCache = state->cachedRenderingFadeFrom();
1295 const QPixmap fadeFromPixmap = d->transition(from: fadeFromCache->regular, to: fadeFromCache->hover, amount: progress);
1296
1297 pixmap = d->transition(from: fadeFromPixmap, to: pixmap, amount: state->fadeProgress());
1298
1299 d->restartAnimation(state);
1300 }
1301
1302 painter->drawPixmap(p: option.rect.topLeft(), pm: pixmap);
1303 drawSelectionEmblem(option, painter, index);
1304 painter->setRenderHint(hint: QPainter::Antialiasing);
1305 if (d->jobTransfersVisible && index.column() == 0) {
1306 if (index.data(arole: KDirModel::HasJobRole).toBool()) {
1307 d->paintJobTransfers(painter, jobAnimationAngle: state->jobAnimationAngle(), iconPos, opt);
1308 }
1309 }
1310 return;
1311 }
1312
1313 // Render the item directly if we're not using a cached rendering
1314 // ========================================================================
1315 painter->save();
1316 painter->setRenderHint(hint: QPainter::Antialiasing);
1317
1318 if (progress > 0 && !(opt.state & QStyle::State_MouseOver)) {
1319 opt.state |= QStyle::State_MouseOver;
1320 icon = d->applyHoverEffect(icon);
1321 }
1322
1323 drawBackground(*painter, opt);
1324 painter->drawPixmap(p: iconPos, pm: icon);
1325
1326 d->drawTextItems(painter, labelLayout, labelColor, infoLayout, infoColor, boundingRect: textBoundingRect);
1327 d->drawFocusRect(painter, option: opt, rect: focusRect);
1328 drawSelectionEmblem(option, painter, index);
1329
1330 if (d->jobTransfersVisible && index.column() == 0 && state) {
1331 if (index.data(arole: KDirModel::HasJobRole).toBool()) {
1332 d->paintJobTransfers(painter, jobAnimationAngle: state->jobAnimationAngle(), iconPos, opt);
1333 }
1334 }
1335 painter->restore();
1336}
1337
1338void KFileItemDelegate::drawSelectionEmblem(QStyleOptionViewItem option, QPainter *painter, const QModelIndex &index) const
1339{
1340 if (index.column() != 0 || !qApp->style()->styleHint(stylehint: QStyle::SH_ItemView_ActivateItemOnSingleClick)) {
1341 return;
1342 }
1343 const auto state = option.state;
1344 if ((state & QStyle::State_MouseOver && !fileItem(index).isDir()) || (state & QStyle::State_Selected)) {
1345 const QString selectionEmblem = state & QStyle::State_Selected ? QStringLiteral("emblem-remove") : QStringLiteral("emblem-added");
1346 const auto emblem = QIcon::fromTheme(name: selectionEmblem).pixmap(size: d->emblemRect.size(), mode: state & QStyle::State_MouseOver ? QIcon::Active : QIcon::Disabled);
1347
1348 painter->drawPixmap(p: d->emblemRect.topLeft(), pm: emblem);
1349 }
1350}
1351
1352int KFileItemDelegate::Private::scaledEmblemSize(int iconSize) const
1353{
1354 if (iconSize <= KIconLoader::SizeSmallMedium) {
1355 return KIconLoader::SizeSmall;
1356 } else if (iconSize <= KIconLoader::SizeHuge) {
1357 return KIconLoader::SizeSmallMedium;
1358 } else if (iconSize <= KIconLoader::SizeEnormous) {
1359 return KIconLoader::SizeMedium;
1360 }
1361
1362 return KIconLoader::SizeHuge;
1363}
1364
1365QWidget *KFileItemDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const
1366{
1367 QStyleOptionViewItem opt(option);
1368 d->initStyleOption(option: &opt, index);
1369
1370 QTextEdit *edit = new QTextEdit(parent);
1371 edit->setAcceptRichText(false);
1372 edit->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
1373 edit->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
1374 edit->setAlignment(opt.displayAlignment);
1375 edit->setEnabled(false); // Disable the text-edit to mark it as un-initialized
1376 return edit;
1377}
1378
1379bool KFileItemDelegate::editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index)
1380{
1381 Q_UNUSED(event)
1382 Q_UNUSED(model)
1383 Q_UNUSED(option)
1384 Q_UNUSED(index)
1385
1386 return false;
1387}
1388
1389void KFileItemDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const
1390{
1391 QTextEdit *textedit = qobject_cast<QTextEdit *>(object: editor);
1392 Q_ASSERT(textedit != nullptr);
1393
1394 // Do not update existing text that the user may already have edited.
1395 // The models will call setEditorData(..) whenever the icon has changed,
1396 // and this makes the editing work correctly despite that.
1397 if (textedit->isEnabled()) {
1398 return;
1399 }
1400 textedit->setEnabled(true); // Enable the text-edit to mark it as initialized
1401
1402 const QVariant value = index.data(arole: Qt::EditRole);
1403 const QString text = value.toString();
1404 textedit->insertPlainText(text);
1405 textedit->selectAll();
1406
1407 QMimeDatabase db;
1408 const QString extension = db.suffixForFileName(fileName: text);
1409 if (!extension.isEmpty()) {
1410 // The filename contains an extension. Assure that only the filename
1411 // gets selected.
1412 const int selectionLength = text.length() - extension.length() - 1;
1413 QTextCursor cursor = textedit->textCursor();
1414 cursor.movePosition(op: QTextCursor::StartOfBlock);
1415 cursor.movePosition(op: QTextCursor::NextCharacter, QTextCursor::KeepAnchor, n: selectionLength);
1416 textedit->setTextCursor(cursor);
1417 }
1418}
1419
1420void KFileItemDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const
1421{
1422 QTextEdit *textedit = qobject_cast<QTextEdit *>(object: editor);
1423 Q_ASSERT(textedit != nullptr);
1424
1425 model->setData(index, value: textedit->toPlainText(), role: Qt::EditRole);
1426}
1427
1428void KFileItemDelegate::updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const
1429{
1430 QStyleOptionViewItem opt(option);
1431 d->initStyleOption(option: &opt, index);
1432 d->setActiveMargins(d->verticalLayout(option: opt) ? Qt::Vertical : Qt::Horizontal);
1433
1434 QRect r = d->labelRectangle(option: opt, index);
1435
1436 // Use the full available width for the editor when maximumSize is set
1437 if (!d->maximumSize.isEmpty()) {
1438 if (d->verticalLayout(option)) {
1439 int diff = qMax(a: r.width(), b: d->maximumSize.width()) - r.width();
1440 if (diff > 1) {
1441 r.adjust(dx1: -(diff / 2), dy1: 0, dx2: diff / 2, dy2: 0);
1442 }
1443 } else {
1444 int diff = qMax(a: r.width(), b: d->maximumSize.width() - opt.decorationSize.width()) - r.width();
1445 if (diff > 0) {
1446 if (opt.decorationPosition == QStyleOptionViewItem::Left) {
1447 r.adjust(dx1: 0, dy1: 0, dx2: diff, dy2: 0);
1448 } else {
1449 r.adjust(dx1: -diff, dy1: 0, dx2: 0, dy2: 0);
1450 }
1451 }
1452 }
1453 }
1454
1455 QTextEdit *textedit = qobject_cast<QTextEdit *>(object: editor);
1456 Q_ASSERT(textedit != nullptr);
1457 const int frame = textedit->frameWidth();
1458 r.adjust(dx1: -frame, dy1: -frame, dx2: frame, dy2: frame);
1459
1460 editor->setGeometry(r);
1461}
1462
1463bool KFileItemDelegate::helpEvent(QHelpEvent *event, QAbstractItemView *view, const QStyleOptionViewItem &option, const QModelIndex &index)
1464{
1465 Q_UNUSED(event)
1466 Q_UNUSED(view)
1467
1468 // if the tooltip information the model keeps is different from the display information,
1469 // show it always
1470 const QVariant toolTip = index.data(arole: Qt::ToolTipRole);
1471
1472 if (!toolTip.isValid()) {
1473 return false;
1474 }
1475
1476 if (index.data() != toolTip) {
1477 return QAbstractItemDelegate::helpEvent(event, view, option, index);
1478 }
1479
1480 if (!d->showToolTipWhenElided) {
1481 return false;
1482 }
1483
1484 // in the case the tooltip information is the same as the display information,
1485 // show it only in the case the display information is elided
1486 QStyleOptionViewItem opt(option);
1487 d->initStyleOption(option: &opt, index);
1488 d->setActiveMargins(d->verticalLayout(option: opt) ? Qt::Vertical : Qt::Horizontal);
1489
1490 QTextLayout labelLayout;
1491 QTextLayout infoLayout;
1492 QRect textBoundingRect;
1493 d->layoutTextItems(option: opt, index, labelLayout: &labelLayout, infoLayout: &infoLayout, textBoundingRect: &textBoundingRect);
1494 const QString elidedText = d->elidedText(layout&: labelLayout, option: opt, size: textBoundingRect.size());
1495
1496 if (elidedText != d->display(index)) {
1497 return QAbstractItemDelegate::helpEvent(event, view, option, index);
1498 }
1499
1500 return false;
1501}
1502
1503QRegion KFileItemDelegate::shape(const QStyleOptionViewItem &option, const QModelIndex &index)
1504{
1505 QStyleOptionViewItem opt(option);
1506 d->initStyleOption(option: &opt, index);
1507 d->setActiveMargins(d->verticalLayout(option: opt) ? Qt::Vertical : Qt::Horizontal);
1508
1509 QTextLayout labelLayout;
1510 QTextLayout infoLayout;
1511 QRect textBoundingRect;
1512 d->layoutTextItems(option: opt, index, labelLayout: &labelLayout, infoLayout: &infoLayout, textBoundingRect: &textBoundingRect);
1513
1514 const QPoint pos = d->iconPosition(option: opt);
1515 QRect iconRect = QRect(pos, opt.icon.actualSize(size: opt.decorationSize));
1516
1517 // Extend the icon rect so it touches the text rect
1518 switch (opt.decorationPosition) {
1519 case QStyleOptionViewItem::Top:
1520 if (iconRect.width() < textBoundingRect.width()) {
1521 iconRect.setBottom(textBoundingRect.top());
1522 } else {
1523 textBoundingRect.setTop(iconRect.bottom());
1524 }
1525 break;
1526 case QStyleOptionViewItem::Bottom:
1527 if (iconRect.width() < textBoundingRect.width()) {
1528 iconRect.setTop(textBoundingRect.bottom());
1529 } else {
1530 textBoundingRect.setBottom(iconRect.top());
1531 }
1532 break;
1533 case QStyleOptionViewItem::Left:
1534 iconRect.setRight(textBoundingRect.left());
1535 break;
1536 case QStyleOptionViewItem::Right:
1537 iconRect.setLeft(textBoundingRect.right());
1538 break;
1539 }
1540
1541 QRegion region;
1542 region += iconRect;
1543 region += textBoundingRect;
1544 return region;
1545}
1546
1547bool KFileItemDelegate::eventFilter(QObject *object, QEvent *event)
1548{
1549 QTextEdit *editor = qobject_cast<QTextEdit *>(object);
1550 if (!editor) {
1551 return false;
1552 }
1553
1554 switch (event->type()) {
1555 case QEvent::KeyPress: {
1556 QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
1557 switch (keyEvent->key()) {
1558 case Qt::Key_Tab:
1559 case Qt::Key_Backtab:
1560 Q_EMIT commitData(editor);
1561 Q_EMIT closeEditor(editor, hint: NoHint);
1562 return true;
1563
1564 case Qt::Key_Enter:
1565 case Qt::Key_Return: {
1566 const QString text = editor->toPlainText();
1567 if (text.isEmpty() || (text == QLatin1Char('.')) || (text == QLatin1String(".."))) {
1568 return true; // So a newline doesn't get inserted
1569 }
1570
1571 Q_EMIT commitData(editor);
1572 Q_EMIT closeEditor(editor, hint: SubmitModelCache);
1573 return true;
1574 }
1575
1576 case Qt::Key_Escape:
1577 Q_EMIT closeEditor(editor, hint: RevertModelCache);
1578 return true;
1579
1580 default:
1581 return false;
1582 } // switch (keyEvent->key())
1583 } // case QEvent::KeyPress
1584
1585 case QEvent::FocusOut: {
1586 const QWidget *w = QApplication::activePopupWidget();
1587 if (!w || w->parent() != editor) {
1588 Q_EMIT commitData(editor);
1589 Q_EMIT closeEditor(editor, hint: NoHint);
1590 return true;
1591 } else {
1592 return false;
1593 }
1594 }
1595
1596 default:
1597 return false;
1598 } // switch (event->type())
1599}
1600
1601void KFileItemDelegate::setSelectionEmblemRect(QRect rect, int iconSize)
1602{
1603 const auto emblemSize = d->scaledEmblemSize(iconSize);
1604
1605 // With small icons, try to center the emblem on top of the icon
1606 if (iconSize <= KIconLoader::SizeSmallMedium) {
1607 d->emblemRect = QRect(rect.topLeft().x() + emblemSize / 4, rect.topLeft().y() + emblemSize / 4, emblemSize, emblemSize);
1608 } else {
1609 d->emblemRect = QRect(rect.topLeft().x(), rect.topLeft().y(), emblemSize, emblemSize);
1610 }
1611}
1612
1613QRect KFileItemDelegate::selectionEmblemRect() const
1614{
1615 return d->emblemRect;
1616}
1617
1618KFileItem KFileItemDelegate::fileItem(const QModelIndex &index) const
1619{
1620 return d->fileItem(index);
1621}
1622
1623#include "moc_kfileitemdelegate.cpp"
1624

source code of kio/src/widgets/kfileitemdelegate.cpp