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

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