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 | |
37 | struct Margin { |
38 | int left, right, top, bottom; |
39 | }; |
40 | |
41 | class Q_DECL_HIDDEN KFileItemDelegate::Private |
42 | { |
43 | public: |
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 | |
100 | public: |
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 | |
111 | private: |
112 | KIO::DelegateAnimationHandler *animationHandler; |
113 | Margin verticalMargin[NMargins]; |
114 | Margin horizontalMargin[NMargins]; |
115 | Margin *activeMargins; |
116 | }; |
117 | |
118 | KFileItemDelegate::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 | |
131 | void KFileItemDelegate::Private::setActiveMargins(Qt::Orientation layout) |
132 | { |
133 | activeMargins = (layout == Qt::Horizontal ? horizontalMargin : verticalMargin); |
134 | } |
135 | |
136 | void 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 | |
144 | void 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 | |
152 | void KFileItemDelegate::Private::setVerticalMargin(MarginType type, int horizontal, int vertical) |
153 | { |
154 | setVerticalMargin(type, left: horizontal, top: vertical, right: horizontal, bottom: vertical); |
155 | } |
156 | |
157 | void KFileItemDelegate::Private::setHorizontalMargin(MarginType type, int horizontal, int vertical) |
158 | { |
159 | setHorizontalMargin(type, left: horizontal, top: vertical, right: horizontal, bottom: vertical); |
160 | } |
161 | |
162 | QRect 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 | |
169 | QRect 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 | |
176 | QSize 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 | |
183 | QSize 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 |
191 | QString 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 |
213 | QString 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 |
296 | KFileItem 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 |
303 | QString 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 |
311 | QSize 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 |
324 | QSize 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. |
351 | QString 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 | |
398 | void 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 | |
409 | QSize 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 | |
445 | QSize 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 | |
464 | bool 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 |
470 | QBrush 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 | |
487 | QBrush 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 | |
510 | bool 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 | |
519 | QPixmap 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 | |
533 | void KFileItemDelegate::Private::gotNewIcon(const QModelIndex &index) |
534 | { |
535 | animationHandler->gotNewIcon(index); |
536 | } |
537 | |
538 | void KFileItemDelegate::Private::restartAnimation(KIO::AnimationState *state) |
539 | { |
540 | animationHandler->restartAnimation(state); |
541 | } |
542 | |
543 | KIO::AnimationState * |
544 | KFileItemDelegate::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 | |
557 | QPixmap 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 | |
620 | void 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 | |
671 | void 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 | |
724 | void 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 | |
772 | void 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 | |
810 | KFileItemDelegate::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 | |
836 | KFileItemDelegate::~KFileItemDelegate() = default; |
837 | |
838 | QSize 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 | |
871 | QString 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 | |
897 | void KFileItemDelegate::setShowInformation(const InformationList &list) |
898 | { |
899 | d->informationList = list; |
900 | } |
901 | |
902 | void KFileItemDelegate::setShowInformation(Information value) |
903 | { |
904 | if (value != NoInformation) { |
905 | d->informationList = InformationList() << value; |
906 | } else { |
907 | d->informationList = InformationList(); |
908 | } |
909 | } |
910 | |
911 | KFileItemDelegate::InformationList KFileItemDelegate::showInformation() const |
912 | { |
913 | return d->informationList; |
914 | } |
915 | |
916 | void KFileItemDelegate::setShadowColor(const QColor &color) |
917 | { |
918 | d->shadowColor = color; |
919 | } |
920 | |
921 | QColor KFileItemDelegate::shadowColor() const |
922 | { |
923 | return d->shadowColor; |
924 | } |
925 | |
926 | void KFileItemDelegate::setShadowOffset(const QPointF &offset) |
927 | { |
928 | d->shadowOffset = offset; |
929 | } |
930 | |
931 | QPointF KFileItemDelegate::shadowOffset() const |
932 | { |
933 | return d->shadowOffset; |
934 | } |
935 | |
936 | void KFileItemDelegate::setShadowBlur(qreal factor) |
937 | { |
938 | d->shadowBlur = factor; |
939 | } |
940 | |
941 | qreal KFileItemDelegate::shadowBlur() const |
942 | { |
943 | return d->shadowBlur; |
944 | } |
945 | |
946 | void KFileItemDelegate::setMaximumSize(const QSize &size) |
947 | { |
948 | d->maximumSize = size; |
949 | } |
950 | |
951 | QSize KFileItemDelegate::maximumSize() const |
952 | { |
953 | return d->maximumSize; |
954 | } |
955 | |
956 | void KFileItemDelegate::setShowToolTipWhenElided(bool showToolTip) |
957 | { |
958 | d->showToolTipWhenElided = showToolTip; |
959 | } |
960 | |
961 | bool KFileItemDelegate::showToolTipWhenElided() const |
962 | { |
963 | return d->showToolTipWhenElided; |
964 | } |
965 | |
966 | void KFileItemDelegate::setWrapMode(QTextOption::WrapMode wrapMode) |
967 | { |
968 | d->wrapMode = wrapMode; |
969 | } |
970 | |
971 | QTextOption::WrapMode KFileItemDelegate::wrapMode() const |
972 | { |
973 | return d->wrapMode; |
974 | } |
975 | |
976 | QRect 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 | |
986 | void KFileItemDelegate::setJobTransfersVisible(bool jobTransfersVisible) |
987 | { |
988 | d->downArrowIcon = QIcon::fromTheme(QStringLiteral("go-down" )); |
989 | d->jobTransfersVisible = jobTransfersVisible; |
990 | } |
991 | |
992 | bool KFileItemDelegate::jobTransfersVisible() const |
993 | { |
994 | return d->jobTransfersVisible; |
995 | } |
996 | |
997 | QIcon 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 | |
1025 | QRect 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 | |
1053 | QPoint 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 | |
1092 | void 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 | |
1115 | void 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 | |
1320 | QWidget *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 | |
1334 | bool 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 | |
1344 | void 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 | |
1375 | void 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 | |
1383 | void 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 | |
1418 | bool 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 | |
1458 | QRegion 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 | |
1502 | bool 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 | |