File indexing completed on 2024-04-28 05:45:11

0001 /*
0002  * SPDX-FileCopyrightText: 2012 Peter Penz <peter.penz19@gmail.com>
0003  *
0004  * SPDX-License-Identifier: GPL-2.0-or-later
0005  */
0006 
0007 #include "kstandarditemlistwidget.h"
0008 
0009 #include "kfileitemlistview.h"
0010 #include "kfileitemmodel.h"
0011 #include "private/kfileitemclipboard.h"
0012 #include "private/kitemlistroleeditor.h"
0013 #include "private/kitemviewsutils.h"
0014 #include "private/kpixmapmodifier.h"
0015 
0016 #include <KIconEffect>
0017 #include <KIconLoader>
0018 #include <KRatingPainter>
0019 #include <KStringHandler>
0020 
0021 #include <QApplication>
0022 #include <QGraphicsScene>
0023 #include <QGraphicsSceneResizeEvent>
0024 #include <QGraphicsView>
0025 #include <QPixmapCache>
0026 #include <QStyleOption>
0027 
0028 // #define KSTANDARDITEMLISTWIDGET_DEBUG
0029 
0030 KStandardItemListWidgetInformant::KStandardItemListWidgetInformant()
0031     : KItemListWidgetInformant()
0032 {
0033 }
0034 
0035 KStandardItemListWidgetInformant::~KStandardItemListWidgetInformant()
0036 {
0037 }
0038 
0039 void KStandardItemListWidgetInformant::calculateItemSizeHints(QVector<std::pair<qreal, bool>> &logicalHeightHints,
0040                                                               qreal &logicalWidthHint,
0041                                                               const KItemListView *view) const
0042 {
0043     switch (static_cast<const KStandardItemListView *>(view)->itemLayout()) {
0044     case KStandardItemListView::IconsLayout:
0045         calculateIconsLayoutItemSizeHints(logicalHeightHints, logicalWidthHint, view);
0046         break;
0047 
0048     case KStandardItemListView::CompactLayout:
0049         calculateCompactLayoutItemSizeHints(logicalHeightHints, logicalWidthHint, view);
0050         break;
0051 
0052     case KStandardItemListView::DetailsLayout:
0053         calculateDetailsLayoutItemSizeHints(logicalHeightHints, logicalWidthHint, view);
0054         break;
0055 
0056     default:
0057         Q_ASSERT(false);
0058         break;
0059     }
0060 }
0061 
0062 qreal KStandardItemListWidgetInformant::preferredRoleColumnWidth(const QByteArray &role, int index, const KItemListView *view) const
0063 {
0064     const QHash<QByteArray, QVariant> values = view->model()->data(index);
0065     const KItemListStyleOption &option = view->styleOption();
0066 
0067     const QString text = roleText(role, values);
0068     qreal width = KStandardItemListWidget::columnPadding(option);
0069 
0070     const QFontMetrics &normalFontMetrics = option.fontMetrics;
0071     const QFontMetrics linkFontMetrics(customizedFontForLinks(option.font));
0072 
0073     if (role == "rating") {
0074         width += KStandardItemListWidget::preferredRatingSize(option).width();
0075     } else {
0076         // If current item is a link, we use the customized link font metrics instead of the normal font metrics.
0077         const QFontMetrics &fontMetrics = itemIsLink(index, view) ? linkFontMetrics : normalFontMetrics;
0078 
0079         width += fontMetrics.horizontalAdvance(text);
0080 
0081         if (role == "text") {
0082             if (view->supportsItemExpanding()) {
0083                 // Increase the width by the expansion-toggle and the current expansion level
0084                 const int expandedParentsCount = values.value("expandedParentsCount", 0).toInt();
0085                 const qreal height = option.padding * 2 + qMax(option.iconSize, fontMetrics.height());
0086                 width += (expandedParentsCount + 1) * height;
0087             }
0088 
0089             // Increase the width by the required space for the icon
0090             width += option.padding * 2 + option.iconSize;
0091         }
0092     }
0093 
0094     return width;
0095 }
0096 
0097 QString KStandardItemListWidgetInformant::itemText(int index, const KItemListView *view) const
0098 {
0099     return view->model()->data(index).value("text").toString();
0100 }
0101 
0102 bool KStandardItemListWidgetInformant::itemIsLink(int index, const KItemListView *view) const
0103 {
0104     Q_UNUSED(index)
0105     Q_UNUSED(view)
0106     return false;
0107 }
0108 
0109 QString KStandardItemListWidgetInformant::roleText(const QByteArray &role, const QHash<QByteArray, QVariant> &values) const
0110 {
0111     if (role == "rating") {
0112         // Always use an empty text, as the rating is shown by the image m_rating.
0113         return QString();
0114     }
0115     return values.value(role).toString();
0116 }
0117 
0118 QFont KStandardItemListWidgetInformant::customizedFontForLinks(const QFont &baseFont) const
0119 {
0120     return baseFont;
0121 }
0122 
0123 void KStandardItemListWidgetInformant::calculateIconsLayoutItemSizeHints(QVector<std::pair<qreal, bool>> &logicalHeightHints,
0124                                                                          qreal &logicalWidthHint,
0125                                                                          const KItemListView *view) const
0126 {
0127     const KItemListStyleOption &option = view->styleOption();
0128     const QFont &normalFont = option.font;
0129     const int additionalRolesCount = qMax(view->visibleRoles().count() - 1, 0);
0130 
0131     const qreal itemWidth = view->itemSize().width();
0132     const qreal maxWidth = itemWidth - 2 * option.padding;
0133     const qreal additionalRolesSpacing = additionalRolesCount * option.fontMetrics.lineSpacing();
0134     const qreal spacingAndIconHeight = option.iconSize + option.padding * 3;
0135 
0136     const QFont linkFont = customizedFontForLinks(normalFont);
0137 
0138     QTextOption textOption(Qt::AlignHCenter);
0139     textOption.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
0140 
0141     for (int index = 0; index < logicalHeightHints.count(); ++index) {
0142         if (logicalHeightHints.at(index).first > 0.0) {
0143             continue;
0144         }
0145 
0146         // If the current item is a link, we use the customized link font instead of the normal font.
0147         const QFont &font = itemIsLink(index, view) ? linkFont : normalFont;
0148 
0149         const QString &text = KStringHandler::preProcessWrap(itemText(index, view));
0150 
0151         // Calculate the number of lines required for wrapping the name
0152         qreal textHeight = 0;
0153         QTextLayout layout(text, font);
0154         layout.setTextOption(textOption);
0155         layout.beginLayout();
0156         QTextLine line;
0157         int lineCount = 0;
0158         bool isElided = false;
0159         while ((line = layout.createLine()).isValid()) {
0160             line.setLineWidth(maxWidth);
0161             line.naturalTextWidth();
0162             textHeight += line.height();
0163 
0164             ++lineCount;
0165             if (lineCount == option.maxTextLines) {
0166                 isElided = layout.createLine().isValid();
0167                 break;
0168             }
0169         }
0170         layout.endLayout();
0171 
0172         // Add one line for each additional information
0173         textHeight += additionalRolesSpacing;
0174 
0175         logicalHeightHints[index].first = textHeight + spacingAndIconHeight;
0176         logicalHeightHints[index].second = isElided;
0177     }
0178 
0179     logicalWidthHint = itemWidth;
0180 }
0181 
0182 void KStandardItemListWidgetInformant::calculateCompactLayoutItemSizeHints(QVector<std::pair<qreal, bool>> &logicalHeightHints,
0183                                                                            qreal &logicalWidthHint,
0184                                                                            const KItemListView *view) const
0185 {
0186     const KItemListStyleOption &option = view->styleOption();
0187     const QFontMetrics &normalFontMetrics = option.fontMetrics;
0188     const int additionalRolesCount = qMax(view->visibleRoles().count() - 1, 0);
0189 
0190     const QList<QByteArray> &visibleRoles = view->visibleRoles();
0191     const bool showOnlyTextRole = (visibleRoles.count() == 1) && (visibleRoles.first() == "text");
0192     const qreal maxWidth = option.maxTextWidth;
0193     const qreal paddingAndIconWidth = option.padding * 4 + option.iconSize;
0194     const qreal height = option.padding * 2 + qMax(option.iconSize, (1 + additionalRolesCount) * normalFontMetrics.lineSpacing());
0195 
0196     const QFontMetrics linkFontMetrics(customizedFontForLinks(option.font));
0197 
0198     for (int index = 0; index < logicalHeightHints.count(); ++index) {
0199         if (logicalHeightHints.at(index).first > 0.0) {
0200             continue;
0201         }
0202 
0203         // If the current item is a link, we use the customized link font metrics instead of the normal font metrics.
0204         const QFontMetrics &fontMetrics = itemIsLink(index, view) ? linkFontMetrics : normalFontMetrics;
0205 
0206         // For each row exactly one role is shown. Calculate the maximum required width that is necessary
0207         // to show all roles without horizontal clipping.
0208         qreal maximumRequiredWidth = 0.0;
0209 
0210         if (showOnlyTextRole) {
0211             maximumRequiredWidth = fontMetrics.horizontalAdvance(itemText(index, view));
0212         } else {
0213             const QHash<QByteArray, QVariant> &values = view->model()->data(index);
0214             for (const QByteArray &role : visibleRoles) {
0215                 const QString &text = roleText(role, values);
0216                 const qreal requiredWidth = fontMetrics.horizontalAdvance(text);
0217                 maximumRequiredWidth = qMax(maximumRequiredWidth, requiredWidth);
0218             }
0219         }
0220 
0221         qreal width = paddingAndIconWidth + maximumRequiredWidth;
0222         if (maxWidth > 0 && width > maxWidth) {
0223             width = maxWidth;
0224         }
0225 
0226         logicalHeightHints[index].first = width;
0227     }
0228 
0229     logicalWidthHint = height;
0230 }
0231 
0232 void KStandardItemListWidgetInformant::calculateDetailsLayoutItemSizeHints(QVector<std::pair<qreal, bool>> &logicalHeightHints,
0233                                                                            qreal &logicalWidthHint,
0234                                                                            const KItemListView *view) const
0235 {
0236     const KItemListStyleOption &option = view->styleOption();
0237     const qreal height = option.padding * 2 + qMax(option.iconSize, option.fontMetrics.height());
0238     logicalHeightHints.fill(std::make_pair(height, false));
0239     logicalWidthHint = -1.0;
0240 }
0241 
0242 KStandardItemListWidget::KStandardItemListWidget(KItemListWidgetInformant *informant, QGraphicsItem *parent)
0243     : KItemListWidget(informant, parent)
0244     , m_textInfo()
0245     , m_isCut(false)
0246     , m_isHidden(false)
0247     , m_customizedFont()
0248     , m_customizedFontMetrics(m_customizedFont)
0249     , m_isExpandable(false)
0250     , m_highlightEntireRow(false)
0251     , m_supportsItemExpanding(false)
0252     , m_dirtyLayout(true)
0253     , m_dirtyContent(true)
0254     , m_dirtyContentRoles()
0255     , m_layout(IconsLayout)
0256     , m_pixmapPos()
0257     , m_pixmap()
0258     , m_scaledPixmapSize()
0259     , m_columnWidthSum()
0260     , m_iconRect()
0261     , m_hoverPixmap()
0262     , m_textRect()
0263     , m_sortedVisibleRoles()
0264     , m_expansionArea()
0265     , m_customTextColor()
0266     , m_additionalInfoTextColor()
0267     , m_overlay()
0268     , m_rating()
0269     , m_roleEditor(nullptr)
0270     , m_oldRoleEditor(nullptr)
0271 {
0272 }
0273 
0274 KStandardItemListWidget::~KStandardItemListWidget()
0275 {
0276     qDeleteAll(m_textInfo);
0277     m_textInfo.clear();
0278 
0279     if (m_roleEditor) {
0280         m_roleEditor->deleteLater();
0281     }
0282 
0283     if (m_oldRoleEditor) {
0284         m_oldRoleEditor->deleteLater();
0285     }
0286 }
0287 
0288 void KStandardItemListWidget::setLayout(Layout layout)
0289 {
0290     if (m_layout != layout) {
0291         m_layout = layout;
0292         m_dirtyLayout = true;
0293         updateAdditionalInfoTextColor();
0294         update();
0295     }
0296 }
0297 
0298 void KStandardItemListWidget::setHighlightEntireRow(bool highlightEntireRow)
0299 {
0300     if (m_highlightEntireRow != highlightEntireRow) {
0301         m_highlightEntireRow = highlightEntireRow;
0302         m_dirtyLayout = true;
0303         update();
0304     }
0305 }
0306 
0307 bool KStandardItemListWidget::highlightEntireRow() const
0308 {
0309     return m_highlightEntireRow;
0310 }
0311 
0312 void KStandardItemListWidget::setSupportsItemExpanding(bool supportsItemExpanding)
0313 {
0314     if (m_supportsItemExpanding != supportsItemExpanding) {
0315         m_supportsItemExpanding = supportsItemExpanding;
0316         m_dirtyLayout = true;
0317         update();
0318     }
0319 }
0320 
0321 bool KStandardItemListWidget::supportsItemExpanding() const
0322 {
0323     return m_supportsItemExpanding;
0324 }
0325 
0326 void KStandardItemListWidget::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
0327 {
0328     const_cast<KStandardItemListWidget *>(this)->triggerCacheRefreshing();
0329 
0330     KItemListWidget::paint(painter, option, widget);
0331 
0332     if (!m_expansionArea.isEmpty()) {
0333         drawSiblingsInformation(painter);
0334     }
0335 
0336     const KItemListStyleOption &itemListStyleOption = styleOption();
0337     if (isHovered() && !m_pixmap.isNull()) {
0338         if (hoverOpacity() < 1.0) {
0339             /*
0340              * Linear interpolation between m_pixmap and m_hoverPixmap.
0341              *
0342              * Note that this cannot be achieved by painting m_hoverPixmap over
0343              * m_pixmap, even if the opacities are adjusted. For details see
0344              * https://git.reviewboard.kde.org/r/109614/
0345              */
0346             // Paint pixmap1 so that pixmap1 = m_pixmap * (1.0 - hoverOpacity())
0347             QPixmap pixmap1(m_pixmap.size());
0348             pixmap1.setDevicePixelRatio(m_pixmap.devicePixelRatio());
0349             pixmap1.fill(Qt::transparent);
0350             {
0351                 QPainter p(&pixmap1);
0352                 p.setOpacity(1.0 - hoverOpacity());
0353                 p.drawPixmap(0, 0, m_pixmap);
0354             }
0355 
0356             // Paint pixmap2 so that pixmap2 = m_hoverPixmap * hoverOpacity()
0357             QPixmap pixmap2(pixmap1.size());
0358             pixmap2.setDevicePixelRatio(pixmap1.devicePixelRatio());
0359             pixmap2.fill(Qt::transparent);
0360             {
0361                 QPainter p(&pixmap2);
0362                 p.setOpacity(hoverOpacity());
0363                 p.drawPixmap(0, 0, m_hoverPixmap);
0364             }
0365 
0366             // Paint pixmap2 on pixmap1 using CompositionMode_Plus
0367             // Now pixmap1 = pixmap2 + m_pixmap * (1.0 - hoverOpacity())
0368             //             = m_hoverPixmap * hoverOpacity() + m_pixmap * (1.0 - hoverOpacity())
0369             {
0370                 QPainter p(&pixmap1);
0371                 p.setCompositionMode(QPainter::CompositionMode_Plus);
0372                 p.drawPixmap(0, 0, pixmap2);
0373             }
0374 
0375             // Finally paint pixmap1 on the widget
0376             drawPixmap(painter, pixmap1);
0377         } else {
0378             drawPixmap(painter, m_hoverPixmap);
0379         }
0380     } else if (!m_pixmap.isNull()) {
0381         drawPixmap(painter, m_pixmap);
0382     }
0383 
0384     painter->setFont(m_customizedFont);
0385     painter->setPen(textColor(*widget));
0386     const TextInfo *textInfo = m_textInfo.value("text");
0387 
0388     if (!textInfo) {
0389         // It seems that we can end up here even if m_textInfo does not contain
0390         // the key "text", see bug 306167. According to triggerCacheRefreshing(),
0391         // this can only happen if the index is negative. This can happen when
0392         // the item is about to be removed, see KItemListView::slotItemsRemoved().
0393         // TODO: try to reproduce the crash and find a better fix.
0394         return;
0395     }
0396 
0397     painter->drawStaticText(textInfo->pos, textInfo->staticText);
0398 
0399     bool clipAdditionalInfoBounds = false;
0400     if (m_supportsItemExpanding) {
0401         // Prevent a possible overlapping of the additional-information texts
0402         // with the icon. This can happen if the user has minimized the width
0403         // of the name-column to a very small value.
0404         const qreal minX = m_pixmapPos.x() + m_pixmap.width() + 4 * itemListStyleOption.padding;
0405         if (textInfo->pos.x() + columnWidth("text") > minX) {
0406             clipAdditionalInfoBounds = true;
0407             painter->save();
0408             painter->setClipRect(minX, 0, size().width() - minX, size().height(), Qt::IntersectClip);
0409         }
0410     }
0411 
0412     painter->setPen(m_additionalInfoTextColor);
0413     painter->setFont(m_customizedFont);
0414 
0415     for (int i = 1; i < m_sortedVisibleRoles.count(); ++i) {
0416         const TextInfo *textInfo = m_textInfo.value(m_sortedVisibleRoles[i]);
0417         painter->drawStaticText(textInfo->pos, textInfo->staticText);
0418     }
0419 
0420     if (!m_rating.isNull()) {
0421         const TextInfo *ratingTextInfo = m_textInfo.value("rating");
0422         QPointF pos = ratingTextInfo->pos;
0423         const Qt::Alignment align = ratingTextInfo->staticText.textOption().alignment();
0424         if (align & Qt::AlignHCenter) {
0425             pos.rx() += (size().width() - m_rating.width() / m_rating.devicePixelRatioF()) / 2 - 2;
0426         }
0427         painter->drawPixmap(pos, m_rating);
0428     }
0429 
0430     if (clipAdditionalInfoBounds) {
0431         painter->restore();
0432     }
0433 
0434 #ifdef KSTANDARDITEMLISTWIDGET_DEBUG
0435     painter->setBrush(Qt::NoBrush);
0436     painter->setPen(Qt::green);
0437     painter->drawRect(m_iconRect);
0438 
0439     painter->setPen(Qt::blue);
0440     painter->drawRect(m_textRect);
0441 
0442     painter->setPen(Qt::red);
0443     painter->drawText(QPointF(0, m_customizedFontMetrics.height()), QString::number(index()));
0444     painter->drawRect(rect());
0445 #endif
0446 }
0447 
0448 QRectF KStandardItemListWidget::iconRect() const
0449 {
0450     const_cast<KStandardItemListWidget *>(this)->triggerCacheRefreshing();
0451     return m_iconRect;
0452 }
0453 
0454 QRectF KStandardItemListWidget::textRect() const
0455 {
0456     const_cast<KStandardItemListWidget *>(this)->triggerCacheRefreshing();
0457     return m_textRect;
0458 }
0459 
0460 QRectF KStandardItemListWidget::textFocusRect() const
0461 {
0462     // In the compact- and details-layout a larger textRect() is returned to be aligned
0463     // with the iconRect(). This is useful to have a larger selection/hover-area
0464     // when having a quite large icon size but only one line of text. Still the
0465     // focus rectangle should be shown as narrow as possible around the text.
0466 
0467     const_cast<KStandardItemListWidget *>(this)->triggerCacheRefreshing();
0468 
0469     switch (m_layout) {
0470     case CompactLayout: {
0471         QRectF rect = m_textRect;
0472         const TextInfo *topText = m_textInfo.value(m_sortedVisibleRoles.first());
0473         const TextInfo *bottomText = m_textInfo.value(m_sortedVisibleRoles.last());
0474         rect.setTop(topText->pos.y());
0475         rect.setBottom(bottomText->pos.y() + bottomText->staticText.size().height());
0476         return rect;
0477     }
0478 
0479     case DetailsLayout: {
0480         QRectF rect = m_textRect;
0481         const TextInfo *textInfo = m_textInfo.value(m_sortedVisibleRoles.first());
0482         rect.setTop(textInfo->pos.y());
0483         rect.setBottom(textInfo->pos.y() + textInfo->staticText.size().height());
0484 
0485         const KItemListStyleOption &option = styleOption();
0486         if (option.extendedSelectionRegion) {
0487             const QString text = textInfo->staticText.text();
0488             rect.setWidth(m_customizedFontMetrics.horizontalAdvance(text) + 2 * option.padding);
0489         }
0490 
0491         return rect;
0492     }
0493 
0494     default:
0495         break;
0496     }
0497 
0498     return m_textRect;
0499 }
0500 
0501 QRectF KStandardItemListWidget::selectionRect() const
0502 {
0503     const_cast<KStandardItemListWidget *>(this)->triggerCacheRefreshing();
0504 
0505     switch (m_layout) {
0506     case IconsLayout:
0507         return m_textRect;
0508 
0509     case CompactLayout:
0510     case DetailsLayout: {
0511         const int padding = styleOption().padding;
0512         QRectF adjustedIconRect = iconRect().adjusted(-padding, -padding, padding, padding);
0513         QRectF result = adjustedIconRect | m_textRect;
0514         if (m_highlightEntireRow) {
0515             result.setRight(m_columnWidthSum + sidePadding());
0516         }
0517         return result;
0518     }
0519 
0520     default:
0521         Q_ASSERT(false);
0522         break;
0523     }
0524 
0525     return m_textRect;
0526 }
0527 
0528 QRectF KStandardItemListWidget::expansionToggleRect() const
0529 {
0530     const_cast<KStandardItemListWidget *>(this)->triggerCacheRefreshing();
0531     return m_isExpandable ? m_expansionArea : QRectF();
0532 }
0533 
0534 QRectF KStandardItemListWidget::selectionToggleRect() const
0535 {
0536     const_cast<KStandardItemListWidget *>(this)->triggerCacheRefreshing();
0537 
0538     const int widgetIconSize = iconSize();
0539     int toggleSize = KIconLoader::SizeSmall;
0540     if (widgetIconSize >= KIconLoader::SizeEnormous) {
0541         toggleSize = KIconLoader::SizeMedium;
0542     } else if (widgetIconSize >= KIconLoader::SizeLarge) {
0543         toggleSize = KIconLoader::SizeSmallMedium;
0544     }
0545 
0546     QPointF pos = iconRect().topLeft();
0547 
0548     // If the selection toggle has a very small distance to the
0549     // widget borders, the size of the selection toggle will get
0550     // increased to prevent an accidental clicking of the item
0551     // when trying to hit the toggle.
0552     const int widgetHeight = size().height();
0553     const int widgetWidth = size().width();
0554     const int minMargin = 2;
0555 
0556     if (toggleSize + minMargin * 2 >= widgetHeight) {
0557         pos.rx() -= (widgetHeight - toggleSize) / 2;
0558         toggleSize = widgetHeight;
0559         pos.setY(0);
0560     }
0561     if (toggleSize + minMargin * 2 >= widgetWidth) {
0562         pos.ry() -= (widgetWidth - toggleSize) / 2;
0563         toggleSize = widgetWidth;
0564         pos.setX(0);
0565     }
0566 
0567     return QRectF(pos, QSizeF(toggleSize, toggleSize));
0568 }
0569 
0570 QPixmap KStandardItemListWidget::createDragPixmap(const QStyleOptionGraphicsItem *option, QWidget *widget)
0571 {
0572     QPixmap pixmap = KItemListWidget::createDragPixmap(option, widget);
0573     if (m_layout != DetailsLayout) {
0574         return pixmap;
0575     }
0576 
0577     // Only return the content of the text-column as pixmap
0578     const int leftClip = m_pixmapPos.x();
0579 
0580     const TextInfo *textInfo = m_textInfo.value("text");
0581     const int rightClip = textInfo->pos.x() + textInfo->staticText.size().width() + 2 * styleOption().padding;
0582 
0583     QPixmap clippedPixmap(rightClip - leftClip + 1, pixmap.height());
0584     clippedPixmap.fill(Qt::transparent);
0585 
0586     QPainter painter(&clippedPixmap);
0587     painter.drawPixmap(-leftClip, 0, pixmap);
0588 
0589     return clippedPixmap;
0590 }
0591 
0592 KItemListWidgetInformant *KStandardItemListWidget::createInformant()
0593 {
0594     return new KStandardItemListWidgetInformant();
0595 }
0596 
0597 void KStandardItemListWidget::invalidateCache()
0598 {
0599     m_dirtyLayout = true;
0600     m_dirtyContent = true;
0601 }
0602 
0603 void KStandardItemListWidget::invalidateIconCache()
0604 {
0605     m_dirtyContent = true;
0606     m_dirtyContentRoles.insert("iconPixmap");
0607     m_dirtyContentRoles.insert("iconOverlays");
0608 }
0609 
0610 void KStandardItemListWidget::refreshCache()
0611 {
0612 }
0613 
0614 bool KStandardItemListWidget::isRoleRightAligned(const QByteArray &role) const
0615 {
0616     Q_UNUSED(role)
0617     return false;
0618 }
0619 
0620 bool KStandardItemListWidget::isHidden() const
0621 {
0622     return false;
0623 }
0624 
0625 QFont KStandardItemListWidget::customizedFont(const QFont &baseFont) const
0626 {
0627     return baseFont;
0628 }
0629 
0630 QPalette::ColorRole KStandardItemListWidget::normalTextColorRole() const
0631 {
0632     return QPalette::Text;
0633 }
0634 
0635 void KStandardItemListWidget::setTextColor(const QColor &color)
0636 {
0637     if (color != m_customTextColor) {
0638         m_customTextColor = color;
0639         updateAdditionalInfoTextColor();
0640         update();
0641     }
0642 }
0643 
0644 QColor KStandardItemListWidget::textColor(const QWidget &widget) const
0645 {
0646     if (!isSelected()) {
0647         if (m_isHidden) {
0648             return m_additionalInfoTextColor;
0649         } else if (m_customTextColor.isValid()) {
0650             return m_customTextColor;
0651         }
0652     }
0653 
0654     const QPalette::ColorGroup group = isActiveWindow() && widget.hasFocus() ? QPalette::Active : QPalette::Inactive;
0655     const QPalette::ColorRole role = isSelected() ? QPalette::HighlightedText : normalTextColorRole();
0656     return styleOption().palette.color(group, role);
0657 }
0658 
0659 void KStandardItemListWidget::setOverlay(const QPixmap &overlay)
0660 {
0661     m_overlay = overlay;
0662     m_dirtyContent = true;
0663     update();
0664 }
0665 
0666 QPixmap KStandardItemListWidget::overlay() const
0667 {
0668     return m_overlay;
0669 }
0670 
0671 QString KStandardItemListWidget::roleText(const QByteArray &role, const QHash<QByteArray, QVariant> &values) const
0672 {
0673     return static_cast<const KStandardItemListWidgetInformant *>(informant())->roleText(role, values);
0674 }
0675 
0676 void KStandardItemListWidget::dataChanged(const QHash<QByteArray, QVariant> &current, const QSet<QByteArray> &roles)
0677 {
0678     Q_UNUSED(current)
0679 
0680     m_dirtyContent = true;
0681 
0682     QSet<QByteArray> dirtyRoles;
0683     if (roles.isEmpty()) {
0684         const auto visibleRoles = this->visibleRoles();
0685         dirtyRoles = QSet<QByteArray>(visibleRoles.constBegin(), visibleRoles.constEnd());
0686     } else {
0687         dirtyRoles = roles;
0688     }
0689 
0690     // The URL might have changed (i.e., if the sort order of the items has
0691     // been changed). Therefore, the "is cut" state must be updated.
0692     KFileItemClipboard *clipboard = KFileItemClipboard::instance();
0693     const QUrl itemUrl = data().value("url").toUrl();
0694     m_isCut = clipboard->isCut(itemUrl);
0695 
0696     // The icon-state might depend from other roles and hence is
0697     // marked as dirty whenever a role has been changed
0698     dirtyRoles.insert("iconPixmap");
0699     dirtyRoles.insert("iconName");
0700 
0701     QSetIterator<QByteArray> it(dirtyRoles);
0702     while (it.hasNext()) {
0703         const QByteArray &role = it.next();
0704         m_dirtyContentRoles.insert(role);
0705     }
0706 }
0707 
0708 void KStandardItemListWidget::visibleRolesChanged(const QList<QByteArray> &current, const QList<QByteArray> &previous)
0709 {
0710     Q_UNUSED(previous)
0711     m_sortedVisibleRoles = current;
0712     m_dirtyLayout = true;
0713 }
0714 
0715 void KStandardItemListWidget::columnWidthChanged(const QByteArray &role, qreal current, qreal previous)
0716 {
0717     Q_UNUSED(role)
0718     Q_UNUSED(current)
0719     Q_UNUSED(previous)
0720     m_dirtyLayout = true;
0721 }
0722 
0723 void KStandardItemListWidget::sidePaddingChanged(qreal padding)
0724 {
0725     Q_UNUSED(padding)
0726     m_dirtyLayout = true;
0727 }
0728 
0729 void KStandardItemListWidget::styleOptionChanged(const KItemListStyleOption &current, const KItemListStyleOption &previous)
0730 {
0731     KItemListWidget::styleOptionChanged(current, previous);
0732 
0733     updateAdditionalInfoTextColor();
0734     m_dirtyLayout = true;
0735 }
0736 
0737 void KStandardItemListWidget::hoveredChanged(bool hovered)
0738 {
0739     Q_UNUSED(hovered)
0740     m_dirtyLayout = true;
0741 }
0742 
0743 void KStandardItemListWidget::selectedChanged(bool selected)
0744 {
0745     Q_UNUSED(selected)
0746     updateAdditionalInfoTextColor();
0747     m_dirtyContent = true;
0748 }
0749 
0750 void KStandardItemListWidget::siblingsInformationChanged(const QBitArray &current, const QBitArray &previous)
0751 {
0752     Q_UNUSED(current)
0753     Q_UNUSED(previous)
0754     m_dirtyLayout = true;
0755 }
0756 
0757 int KStandardItemListWidget::selectionLength(const QString &text) const
0758 {
0759     return text.length();
0760 }
0761 
0762 void KStandardItemListWidget::editedRoleChanged(const QByteArray &current, const QByteArray &previous)
0763 {
0764     Q_UNUSED(previous)
0765 
0766     QGraphicsView *parent = scene()->views()[0];
0767     if (current.isEmpty() || !parent || current != "text") {
0768         if (m_roleEditor) {
0769             Q_EMIT roleEditingCanceled(index(), current, data().value(current));
0770 
0771             disconnect(m_roleEditor, &KItemListRoleEditor::roleEditingCanceled, this, &KStandardItemListWidget::slotRoleEditingCanceled);
0772             disconnect(m_roleEditor, &KItemListRoleEditor::roleEditingFinished, this, &KStandardItemListWidget::slotRoleEditingFinished);
0773 
0774             if (m_oldRoleEditor) {
0775                 m_oldRoleEditor->deleteLater();
0776             }
0777             m_oldRoleEditor = m_roleEditor;
0778             m_roleEditor->hide();
0779             m_roleEditor = nullptr;
0780         }
0781         return;
0782     }
0783 
0784     Q_ASSERT(!m_roleEditor);
0785 
0786     const TextInfo *textInfo = m_textInfo.value("text");
0787 
0788     m_roleEditor = new KItemListRoleEditor(parent);
0789     m_roleEditor->setRole(current);
0790     m_roleEditor->setAllowUpDownKeyChainEdit(m_layout != IconsLayout);
0791     m_roleEditor->setFont(styleOption().font);
0792 
0793     const QString text = data().value(current).toString();
0794     m_roleEditor->setPlainText(text);
0795 
0796     QTextOption textOption = textInfo->staticText.textOption();
0797     m_roleEditor->document()->setDefaultTextOption(textOption);
0798 
0799     const int textSelectionLength = selectionLength(text);
0800 
0801     if (textSelectionLength > 0) {
0802         QTextCursor cursor = m_roleEditor->textCursor();
0803         cursor.movePosition(QTextCursor::StartOfBlock);
0804         cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, textSelectionLength);
0805         m_roleEditor->setTextCursor(cursor);
0806     }
0807 
0808     connect(m_roleEditor, &KItemListRoleEditor::roleEditingCanceled, this, &KStandardItemListWidget::slotRoleEditingCanceled);
0809     connect(m_roleEditor, &KItemListRoleEditor::roleEditingFinished, this, &KStandardItemListWidget::slotRoleEditingFinished);
0810 
0811     // Adjust the geometry of the editor
0812     QRectF rect = roleEditingRect(current);
0813     const int frameWidth = m_roleEditor->frameWidth();
0814     rect.adjust(-frameWidth, -frameWidth, frameWidth, frameWidth);
0815     rect.translate(pos());
0816     if (rect.right() > parent->width()) {
0817         rect.setWidth(parent->width() - rect.left());
0818     }
0819     m_roleEditor->setGeometry(rect.toRect());
0820     m_roleEditor->autoAdjustSize();
0821     m_roleEditor->show();
0822     m_roleEditor->setFocus();
0823 }
0824 
0825 void KStandardItemListWidget::iconSizeChanged(int current, int previous)
0826 {
0827     KItemListWidget::iconSizeChanged(current, previous);
0828 
0829     invalidateIconCache();
0830     triggerCacheRefreshing();
0831     update();
0832 }
0833 
0834 void KStandardItemListWidget::resizeEvent(QGraphicsSceneResizeEvent *event)
0835 {
0836     if (m_roleEditor) {
0837         setEditedRole(QByteArray());
0838         Q_ASSERT(!m_roleEditor);
0839     }
0840 
0841     KItemListWidget::resizeEvent(event);
0842 
0843     m_dirtyLayout = true;
0844 }
0845 
0846 void KStandardItemListWidget::showEvent(QShowEvent *event)
0847 {
0848     KItemListWidget::showEvent(event);
0849 
0850     // Listen to changes of the clipboard to mark the item as cut/uncut
0851     KFileItemClipboard *clipboard = KFileItemClipboard::instance();
0852 
0853     const QUrl itemUrl = data().value("url").toUrl();
0854     m_isCut = clipboard->isCut(itemUrl);
0855 
0856     connect(clipboard, &KFileItemClipboard::cutItemsChanged, this, &KStandardItemListWidget::slotCutItemsChanged);
0857 }
0858 
0859 void KStandardItemListWidget::hideEvent(QHideEvent *event)
0860 {
0861     disconnect(KFileItemClipboard::instance(), &KFileItemClipboard::cutItemsChanged, this, &KStandardItemListWidget::slotCutItemsChanged);
0862 
0863     KItemListWidget::hideEvent(event);
0864 }
0865 
0866 bool KStandardItemListWidget::event(QEvent *event)
0867 {
0868     if (event->type() == QEvent::WindowDeactivate || event->type() == QEvent::WindowActivate || event->type() == QEvent::PaletteChange) {
0869         m_dirtyContent = true;
0870     }
0871 
0872     return KItemListWidget::event(event);
0873 }
0874 
0875 void KStandardItemListWidget::finishRoleEditing()
0876 {
0877     if (!editedRole().isEmpty() && m_roleEditor) {
0878         slotRoleEditingFinished(editedRole(), KIO::encodeFileName(m_roleEditor->toPlainText()));
0879     }
0880 }
0881 
0882 void KStandardItemListWidget::slotCutItemsChanged()
0883 {
0884     const QUrl itemUrl = data().value("url").toUrl();
0885     const bool isCut = KFileItemClipboard::instance()->isCut(itemUrl);
0886     if (m_isCut != isCut) {
0887         m_isCut = isCut;
0888         m_pixmap = QPixmap();
0889         m_dirtyContent = true;
0890         update();
0891     }
0892 }
0893 
0894 void KStandardItemListWidget::slotRoleEditingCanceled(const QByteArray &role, const QVariant &value)
0895 {
0896     closeRoleEditor();
0897     Q_EMIT roleEditingCanceled(index(), role, value);
0898     setEditedRole(QByteArray());
0899 }
0900 
0901 void KStandardItemListWidget::slotRoleEditingFinished(const QByteArray &role, const QVariant &value)
0902 {
0903     closeRoleEditor();
0904     Q_EMIT roleEditingFinished(index(), role, value);
0905     setEditedRole(QByteArray());
0906 }
0907 
0908 void KStandardItemListWidget::triggerCacheRefreshing()
0909 {
0910     if ((!m_dirtyContent && !m_dirtyLayout) || index() < 0) {
0911         return;
0912     }
0913 
0914     refreshCache();
0915 
0916     const QHash<QByteArray, QVariant> values = data();
0917     m_isExpandable = m_supportsItemExpanding && values["isExpandable"].toBool();
0918     m_isHidden = isHidden();
0919     m_customizedFont = customizedFont(styleOption().font);
0920     m_customizedFontMetrics = QFontMetrics(m_customizedFont);
0921     m_columnWidthSum = std::accumulate(m_sortedVisibleRoles.begin(), m_sortedVisibleRoles.end(), qreal(), [this](qreal sum, const auto &role) {
0922         return sum + columnWidth(role);
0923     });
0924 
0925     updateExpansionArea();
0926     updateTextsCache();
0927     updatePixmapCache();
0928     clearHoverCache();
0929 
0930     m_dirtyLayout = false;
0931     m_dirtyContent = false;
0932     m_dirtyContentRoles.clear();
0933 }
0934 
0935 void KStandardItemListWidget::updateExpansionArea()
0936 {
0937     if (m_supportsItemExpanding) {
0938         const QHash<QByteArray, QVariant> values = data();
0939         const int expandedParentsCount = values.value("expandedParentsCount", 0).toInt();
0940         if (expandedParentsCount >= 0) {
0941             const int widgetIconSize = iconSize();
0942             const qreal widgetHeight = size().height();
0943             const qreal inc = (widgetHeight - widgetIconSize) / 2;
0944             const qreal x = expandedParentsCount * widgetHeight + inc;
0945             const qreal y = inc;
0946             const qreal xPadding = m_highlightEntireRow ? sidePadding() : 0;
0947             m_expansionArea = QRectF(xPadding + x, y, widgetIconSize, widgetIconSize);
0948             return;
0949         }
0950     }
0951 
0952     m_expansionArea = QRectF();
0953 }
0954 
0955 void KStandardItemListWidget::updatePixmapCache()
0956 {
0957     // Precondition: Requires already updated m_textPos values to calculate
0958     // the remaining height when the alignment is vertical.
0959 
0960     const QSizeF widgetSize = size();
0961     const bool iconOnTop = (m_layout == IconsLayout);
0962     const KItemListStyleOption &option = styleOption();
0963     const qreal padding = option.padding;
0964     const qreal dpr = KItemViewsUtils::devicePixelRatio(this);
0965 
0966     const int widgetIconSize = iconSize();
0967     const int maxIconWidth = iconOnTop ? widgetSize.width() - 2 * padding : widgetIconSize;
0968     const int maxIconHeight = widgetIconSize;
0969 
0970     const QHash<QByteArray, QVariant> values = data();
0971 
0972     bool updatePixmap = (m_pixmap.width() != maxIconWidth || m_pixmap.height() != maxIconHeight);
0973     if (!updatePixmap && m_dirtyContent) {
0974         updatePixmap = m_dirtyContentRoles.isEmpty() || m_dirtyContentRoles.contains("iconPixmap") || m_dirtyContentRoles.contains("iconName")
0975             || m_dirtyContentRoles.contains("iconOverlays");
0976     }
0977 
0978     if (updatePixmap) {
0979         m_pixmap = QPixmap();
0980 
0981         int sequenceIndex = hoverSequenceIndex();
0982 
0983         if (values.contains("hoverSequencePixmaps")) {
0984             // Use one of the hover sequence pixmaps instead of the default
0985             // icon pixmap.
0986 
0987             const QVector<QPixmap> pixmaps = values["hoverSequencePixmaps"].value<QVector<QPixmap>>();
0988 
0989             if (values.contains("hoverSequenceWraparoundPoint")) {
0990                 const float wap = values["hoverSequenceWraparoundPoint"].toFloat();
0991                 if (wap >= 1.0f) {
0992                     sequenceIndex %= static_cast<int>(wap);
0993                 }
0994             }
0995 
0996             const int loadedIndex = qMax(qMin(sequenceIndex, pixmaps.size() - 1), 0);
0997 
0998             if (loadedIndex != 0) {
0999                 m_pixmap = pixmaps[loadedIndex];
1000             }
1001         }
1002 
1003         if (m_pixmap.isNull()) {
1004             m_pixmap = values["iconPixmap"].value<QPixmap>();
1005         }
1006 
1007         if (m_pixmap.isNull()) {
1008             // Use the icon that fits to the MIME-type
1009             QString iconName = values["iconName"].toString();
1010             if (iconName.isEmpty()) {
1011                 // The icon-name has not been not resolved by KFileItemModelRolesUpdater,
1012                 // use a generic icon as fallback
1013                 iconName = QStringLiteral("unknown");
1014             }
1015             const QStringList overlays = values["iconOverlays"].toStringList();
1016             const bool hasFocus = scene()->views()[0]->parentWidget()->hasFocus();
1017             m_pixmap = pixmapForIcon(iconName,
1018                                      overlays,
1019                                      maxIconHeight,
1020                                      m_layout != IconsLayout && isActiveWindow() && isSelected() && hasFocus ? QIcon::Selected : QIcon::Normal);
1021 
1022         } else if (m_pixmap.width() / m_pixmap.devicePixelRatio() != maxIconWidth || m_pixmap.height() / m_pixmap.devicePixelRatio() != maxIconHeight) {
1023             // A custom pixmap has been applied. Assure that the pixmap
1024             // is scaled to the maximum available size.
1025             KPixmapModifier::scale(m_pixmap, QSize(maxIconWidth, maxIconHeight) * dpr);
1026         }
1027 
1028         if (m_pixmap.isNull()) {
1029             m_hoverPixmap = QPixmap();
1030             return;
1031         }
1032 
1033         if (m_isCut) {
1034             KIconEffect *effect = KIconLoader::global()->iconEffect();
1035             m_pixmap = effect->apply(m_pixmap, KIconLoader::Desktop, KIconLoader::DisabledState);
1036         }
1037 
1038         if (m_isHidden) {
1039             KIconEffect::semiTransparent(m_pixmap);
1040         }
1041 
1042         if (m_layout == IconsLayout && isSelected()) {
1043             const QColor color = palette().brush(QPalette::Normal, QPalette::Highlight).color();
1044             QImage image = m_pixmap.toImage();
1045             if (image.isNull()) {
1046                 m_hoverPixmap = QPixmap();
1047                 return;
1048             }
1049             KIconEffect::colorize(image, color, 0.8f);
1050             m_pixmap = QPixmap::fromImage(image);
1051         }
1052     }
1053 
1054     if (!m_overlay.isNull()) {
1055         QPainter painter(&m_pixmap);
1056         painter.drawPixmap(0, (m_pixmap.height() - m_overlay.height()) / m_pixmap.devicePixelRatio(), m_overlay);
1057     }
1058 
1059     int scaledIconSize = 0;
1060     if (iconOnTop) {
1061         const TextInfo *textInfo = m_textInfo.value("text");
1062         scaledIconSize = static_cast<int>(textInfo->pos.y() - 2 * padding);
1063     } else {
1064         const int textRowsCount = (m_layout == CompactLayout) ? visibleRoles().count() : 1;
1065         const qreal requiredTextHeight = textRowsCount * m_customizedFontMetrics.height();
1066         scaledIconSize = (requiredTextHeight < maxIconHeight) ? widgetSize.height() - 2 * padding : maxIconHeight;
1067     }
1068 
1069     const int maxScaledIconWidth = iconOnTop ? widgetSize.width() - 2 * padding : scaledIconSize;
1070     const int maxScaledIconHeight = scaledIconSize;
1071 
1072     m_scaledPixmapSize = m_pixmap.size();
1073     m_scaledPixmapSize.scale(maxScaledIconWidth * dpr, maxScaledIconHeight * dpr, Qt::KeepAspectRatio);
1074     m_scaledPixmapSize = m_scaledPixmapSize / dpr;
1075 
1076     if (iconOnTop) {
1077         // Center horizontally and align on bottom within the icon-area
1078         m_pixmapPos.setX((widgetSize.width() - m_scaledPixmapSize.width()) / 2.0);
1079         m_pixmapPos.setY(padding + scaledIconSize - m_scaledPixmapSize.height());
1080     } else {
1081         // Center horizontally and vertically within the icon-area
1082         const TextInfo *textInfo = m_textInfo.value("text");
1083         m_pixmapPos.setX(textInfo->pos.x() - 2.0 * padding - (scaledIconSize + m_scaledPixmapSize.width()) / 2.0);
1084 
1085         // Derive icon's vertical center from the center of the text frame, including
1086         // any necessary adjustment if the font's midline is offset from the frame center
1087         const qreal midlineShift = m_customizedFontMetrics.height() / 2.0 - m_customizedFontMetrics.descent() - m_customizedFontMetrics.capHeight() / 2.0;
1088         m_pixmapPos.setY(m_textRect.center().y() + midlineShift - m_scaledPixmapSize.height() / 2.0);
1089     }
1090 
1091     if (m_layout == IconsLayout) {
1092         m_iconRect = QRectF(m_pixmapPos, QSizeF(m_scaledPixmapSize));
1093     } else {
1094         const qreal widthOffset = widgetIconSize - m_scaledPixmapSize.width();
1095         const qreal heightOffset = widgetIconSize - m_scaledPixmapSize.height();
1096         const QPointF squareIconPos(m_pixmapPos.x() - 0.5 * widthOffset, m_pixmapPos.y() - 0.5 * heightOffset);
1097         const QSizeF squareIconSize(widgetIconSize, widgetIconSize);
1098         m_iconRect = QRectF(squareIconPos, squareIconSize);
1099     }
1100 
1101     // Prepare the pixmap that is used when the item gets hovered
1102     if (isHovered()) {
1103         m_hoverPixmap = m_pixmap;
1104         KIconEffect *effect = KIconLoader::global()->iconEffect();
1105         // In the KIconLoader terminology, active = hover.
1106         if (effect->hasEffect(KIconLoader::Desktop, KIconLoader::ActiveState)) {
1107             m_hoverPixmap = effect->apply(m_pixmap, KIconLoader::Desktop, KIconLoader::ActiveState);
1108         } else {
1109             m_hoverPixmap = m_pixmap;
1110         }
1111     } else if (hoverOpacity() <= 0.0) {
1112         // No hover animation is ongoing. Clear m_hoverPixmap to save memory.
1113         m_hoverPixmap = QPixmap();
1114     }
1115 }
1116 
1117 void KStandardItemListWidget::updateTextsCache()
1118 {
1119     QTextOption textOption;
1120     switch (m_layout) {
1121     case IconsLayout:
1122         textOption.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
1123         textOption.setAlignment(Qt::AlignHCenter);
1124         break;
1125     case CompactLayout:
1126     case DetailsLayout:
1127         textOption.setAlignment(Qt::AlignLeft);
1128         textOption.setWrapMode(QTextOption::NoWrap);
1129         break;
1130     default:
1131         Q_ASSERT(false);
1132         break;
1133     }
1134 
1135     qDeleteAll(m_textInfo);
1136     m_textInfo.clear();
1137     for (int i = 0; i < m_sortedVisibleRoles.count(); ++i) {
1138         TextInfo *textInfo = new TextInfo();
1139         textInfo->staticText.setTextFormat(Qt::PlainText);
1140         textInfo->staticText.setPerformanceHint(QStaticText::AggressiveCaching);
1141         textInfo->staticText.setTextOption(textOption);
1142         m_textInfo.insert(m_sortedVisibleRoles[i], textInfo);
1143     }
1144 
1145     switch (m_layout) {
1146     case IconsLayout:
1147         updateIconsLayoutTextCache();
1148         break;
1149     case CompactLayout:
1150         updateCompactLayoutTextCache();
1151         break;
1152     case DetailsLayout:
1153         updateDetailsLayoutTextCache();
1154         break;
1155     default:
1156         Q_ASSERT(false);
1157         break;
1158     }
1159 
1160     const TextInfo *ratingTextInfo = m_textInfo.value("rating");
1161     if (ratingTextInfo) {
1162         // The text of the rating-role has been set to empty to get
1163         // replaced by a rating-image showing the rating as stars.
1164         const KItemListStyleOption &option = styleOption();
1165         QSizeF ratingSize = preferredRatingSize(option);
1166 
1167         const qreal availableWidth = (m_layout == DetailsLayout) ? columnWidth("rating") - columnPadding(option) : size().width();
1168         if (ratingSize.width() > availableWidth) {
1169             ratingSize.rwidth() = availableWidth;
1170         }
1171         const qreal dpr = KItemViewsUtils::devicePixelRatio(this);
1172         m_rating = QPixmap(ratingSize.toSize() * dpr);
1173         m_rating.setDevicePixelRatio(dpr);
1174         m_rating.fill(Qt::transparent);
1175 
1176         QPainter painter(&m_rating);
1177         const QRect rect(QPoint(0, 0), ratingSize.toSize());
1178         const int rating = data().value("rating").toInt();
1179         KRatingPainter::paintRating(&painter, rect, Qt::AlignJustify | Qt::AlignVCenter, rating);
1180     } else if (!m_rating.isNull()) {
1181         m_rating = QPixmap();
1182     }
1183 }
1184 
1185 QString KStandardItemListWidget::elideRightKeepExtension(const QString &text, int elidingWidth) const
1186 {
1187     const auto extensionIndex = text.lastIndexOf('.');
1188     if (extensionIndex != -1) {
1189         // has file extension
1190         const auto extensionLength = text.length() - extensionIndex;
1191         const auto extensionWidth = m_customizedFontMetrics.horizontalAdvance(text.right(extensionLength));
1192         if (elidingWidth > extensionWidth && extensionLength < 6 && (float(extensionWidth) / float(elidingWidth)) < 0.3) {
1193             // if we have room to display the file extension and the extension is not too long
1194             QString ret = m_customizedFontMetrics.elidedText(text.chopped(extensionLength), Qt::ElideRight, elidingWidth - extensionWidth);
1195 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
1196             ret.append(text.rightRef(extensionLength));
1197 #else
1198             ret.append(QStringView(text).right(extensionLength));
1199 #endif
1200             return ret;
1201         }
1202     }
1203     return m_customizedFontMetrics.elidedText(text, Qt::ElideRight, elidingWidth);
1204 }
1205 
1206 QString KStandardItemListWidget::escapeString(const QString &text) const
1207 {
1208     QString escaped(text);
1209 
1210     const QChar returnSymbol(0x21b5);
1211     escaped.replace('\n', returnSymbol);
1212 
1213     return escaped;
1214 }
1215 
1216 void KStandardItemListWidget::updateIconsLayoutTextCache()
1217 {
1218     //      +------+
1219     //      | Icon |
1220     //      +------+
1221     //
1222     //    Name role that
1223     // might get wrapped above
1224     //    several lines.
1225     //  Additional role 1
1226     //  Additional role 2
1227 
1228     const QHash<QByteArray, QVariant> values = data();
1229 
1230     const KItemListStyleOption &option = styleOption();
1231     const qreal padding = option.padding;
1232     const qreal maxWidth = size().width() - 2 * padding;
1233     const qreal lineSpacing = m_customizedFontMetrics.lineSpacing();
1234 
1235     // Initialize properties for the "text" role. It will be used as anchor
1236     // for initializing the position of the other roles.
1237     TextInfo *nameTextInfo = m_textInfo.value("text");
1238     const QString nameText = KStringHandler::preProcessWrap(escapeString(values["text"].toString()));
1239     nameTextInfo->staticText.setText(nameText);
1240 
1241     // Calculate the number of lines required for the name and the required width
1242     qreal nameWidth = 0;
1243     qreal nameHeight = 0;
1244     QTextLine line;
1245 
1246     QTextLayout layout(nameTextInfo->staticText.text(), m_customizedFont);
1247     layout.setTextOption(nameTextInfo->staticText.textOption());
1248     layout.beginLayout();
1249     int nameLineIndex = 0;
1250     while ((line = layout.createLine()).isValid()) {
1251         line.setLineWidth(maxWidth);
1252         nameWidth = qMax(nameWidth, line.naturalTextWidth());
1253         nameHeight += line.height();
1254 
1255         ++nameLineIndex;
1256         if (nameLineIndex == option.maxTextLines) {
1257             // The maximum number of textlines has been reached. If this is
1258             // the case provide an elided text if necessary.
1259             const int textLength = line.textStart() + line.textLength();
1260             if (textLength < nameText.length()) {
1261                 // Elide the last line of the text
1262                 qreal elidingWidth = maxWidth;
1263                 qreal lastLineWidth;
1264                 do {
1265                     QString lastTextLine = nameText.mid(line.textStart());
1266                     lastTextLine = elideRightKeepExtension(lastTextLine, elidingWidth);
1267                     const QString elidedText = nameText.left(line.textStart()) + lastTextLine;
1268                     nameTextInfo->staticText.setText(elidedText);
1269 
1270                     lastLineWidth = m_customizedFontMetrics.horizontalAdvance(lastTextLine);
1271 
1272                     // We do the text eliding in a loop with decreasing width (1 px / iteration)
1273                     // to avoid problems related to different width calculation code paths
1274                     // within Qt. (see bug 337104)
1275                     elidingWidth -= 1.0;
1276                 } while (lastLineWidth > maxWidth);
1277 
1278                 nameWidth = qMax(nameWidth, lastLineWidth);
1279             }
1280             break;
1281         }
1282     }
1283     layout.endLayout();
1284 
1285     // Use one line for each additional information
1286     nameTextInfo->staticText.setTextWidth(maxWidth);
1287     nameTextInfo->pos = QPointF(padding, iconSize() + 2 * padding);
1288     m_textRect = QRectF(padding + (maxWidth - nameWidth) / 2, nameTextInfo->pos.y(), nameWidth, nameHeight);
1289 
1290     // Calculate the position for each additional information
1291     qreal y = nameTextInfo->pos.y() + nameHeight;
1292     for (const QByteArray &role : std::as_const(m_sortedVisibleRoles)) {
1293         if (role == "text") {
1294             continue;
1295         }
1296 
1297         const QString text = roleText(role, values);
1298         TextInfo *textInfo = m_textInfo.value(role);
1299         textInfo->staticText.setText(text);
1300 
1301         qreal requiredWidth = 0;
1302 
1303         QTextLayout layout(text, m_customizedFont);
1304         QTextOption textOption;
1305         textOption.setWrapMode(QTextOption::NoWrap);
1306         layout.setTextOption(textOption);
1307 
1308         layout.beginLayout();
1309         QTextLine textLine = layout.createLine();
1310         if (textLine.isValid()) {
1311             textLine.setLineWidth(maxWidth);
1312             requiredWidth = textLine.naturalTextWidth();
1313             if (requiredWidth > maxWidth) {
1314                 const QString elidedText = elideRightKeepExtension(text, maxWidth);
1315                 textInfo->staticText.setText(elidedText);
1316                 requiredWidth = m_customizedFontMetrics.horizontalAdvance(elidedText);
1317             } else if (role == "rating") {
1318                 // Use the width of the rating pixmap, because the rating text is empty.
1319                 requiredWidth = m_rating.width() / m_rating.devicePixelRatioF();
1320             }
1321         }
1322         layout.endLayout();
1323 
1324         textInfo->pos = QPointF(padding, y);
1325         textInfo->staticText.setTextWidth(maxWidth);
1326 
1327         const QRectF textRect(padding + (maxWidth - requiredWidth) / 2, y, requiredWidth, lineSpacing);
1328 
1329         // Ignore empty roles. Avoids a text rect taller than the area that actually contains text.
1330         if (!textRect.isEmpty()) {
1331             m_textRect |= textRect;
1332         }
1333 
1334         y += lineSpacing;
1335     }
1336 
1337     // Add a padding to the text rectangle
1338     m_textRect.adjust(-padding, -padding, padding, padding);
1339 }
1340 
1341 void KStandardItemListWidget::updateCompactLayoutTextCache()
1342 {
1343     // +------+  Name role
1344     // | Icon |  Additional role 1
1345     // +------+  Additional role 2
1346 
1347     const QHash<QByteArray, QVariant> values = data();
1348 
1349     const KItemListStyleOption &option = styleOption();
1350     const qreal widgetHeight = size().height();
1351     const qreal lineSpacing = m_customizedFontMetrics.lineSpacing();
1352     const qreal textLinesHeight = qMax(visibleRoles().count(), 1) * lineSpacing;
1353 
1354     qreal maximumRequiredTextWidth = 0;
1355     const qreal x = option.padding * 3 + iconSize();
1356     qreal y = qRound((widgetHeight - textLinesHeight) / 2);
1357     const qreal maxWidth = size().width() - x - option.padding;
1358     for (const QByteArray &role : std::as_const(m_sortedVisibleRoles)) {
1359         const QString text = escapeString(roleText(role, values));
1360         TextInfo *textInfo = m_textInfo.value(role);
1361         textInfo->staticText.setText(text);
1362 
1363         qreal requiredWidth = m_customizedFontMetrics.horizontalAdvance(text);
1364         if (requiredWidth > maxWidth) {
1365             requiredWidth = maxWidth;
1366             const QString elidedText = elideRightKeepExtension(text, maxWidth);
1367             textInfo->staticText.setText(elidedText);
1368         }
1369 
1370         textInfo->pos = QPointF(x, y);
1371         textInfo->staticText.setTextWidth(maxWidth);
1372 
1373         maximumRequiredTextWidth = qMax(maximumRequiredTextWidth, requiredWidth);
1374 
1375         y += lineSpacing;
1376     }
1377 
1378     m_textRect = QRectF(x - option.padding, 0, maximumRequiredTextWidth + 2 * option.padding, widgetHeight);
1379 }
1380 
1381 void KStandardItemListWidget::updateDetailsLayoutTextCache()
1382 {
1383     // Precondition: Requires already updated m_expansionArea
1384     // to determine the left position.
1385 
1386     // +------+
1387     // | Icon |  Name role   Additional role 1   Additional role 2
1388     // +------+
1389     m_textRect = QRectF();
1390 
1391     const KItemListStyleOption &option = styleOption();
1392     const QHash<QByteArray, QVariant> values = data();
1393 
1394     const qreal widgetHeight = size().height();
1395     const int fontHeight = m_customizedFontMetrics.height();
1396 
1397     const qreal columnWidthInc = columnPadding(option);
1398     qreal firstColumnInc = iconSize();
1399     if (m_supportsItemExpanding) {
1400         firstColumnInc += (m_expansionArea.left() + m_expansionArea.right() + widgetHeight) / 2;
1401     } else {
1402         firstColumnInc += option.padding + sidePadding();
1403     }
1404 
1405     qreal x = firstColumnInc;
1406     const qreal y = qMax(qreal(option.padding), (widgetHeight - fontHeight) / 2);
1407 
1408     for (const QByteArray &role : std::as_const(m_sortedVisibleRoles)) {
1409         QString text = roleText(role, values);
1410 
1411         // Elide the text in case it does not fit into the available column-width
1412         qreal requiredWidth = m_customizedFontMetrics.horizontalAdvance(text);
1413         const qreal roleWidth = columnWidth(role);
1414         qreal availableTextWidth = roleWidth - columnWidthInc;
1415 
1416         const bool isTextRole = (role == "text");
1417         if (isTextRole) {
1418             text = escapeString(text);
1419             availableTextWidth -= firstColumnInc - sidePadding();
1420         }
1421 
1422         if (requiredWidth > availableTextWidth) {
1423             text = elideRightKeepExtension(text, availableTextWidth);
1424             requiredWidth = m_customizedFontMetrics.horizontalAdvance(text);
1425         }
1426 
1427         TextInfo *textInfo = m_textInfo.value(role);
1428         textInfo->staticText.setText(text);
1429         textInfo->pos = QPointF(x + columnWidthInc / 2, y);
1430         x += roleWidth;
1431 
1432         if (isTextRole) {
1433             const qreal textWidth = option.extendedSelectionRegion ? size().width() - textInfo->pos.x() : requiredWidth + 2 * option.padding;
1434             m_textRect = QRectF(textInfo->pos.x() - option.padding, 0, textWidth, size().height());
1435 
1436             // The column after the name should always be aligned on the same x-position independent
1437             // from the expansion-level shown in the name column
1438             x -= firstColumnInc - sidePadding();
1439         } else if (isRoleRightAligned(role)) {
1440             textInfo->pos.rx() += roleWidth - requiredWidth - columnWidthInc;
1441         }
1442     }
1443 }
1444 
1445 void KStandardItemListWidget::updateAdditionalInfoTextColor()
1446 {
1447     QColor c1;
1448     const bool hasFocus = scene()->views()[0]->parentWidget()->hasFocus();
1449     if (m_customTextColor.isValid()) {
1450         c1 = m_customTextColor;
1451     } else if (isSelected() && hasFocus && (m_layout != DetailsLayout || m_highlightEntireRow)) {
1452         // The detail text colour needs to match the main text (HighlightedText) for the same level
1453         // of readability. We short circuit early here to avoid interpolating with another colour.
1454         m_additionalInfoTextColor = styleOption().palette.color(QPalette::HighlightedText);
1455         return;
1456     } else {
1457         c1 = styleOption().palette.text().color();
1458     }
1459 
1460     // For the color of the additional info the inactive text color
1461     // is not used as this might lead to unreadable text for some color schemes. Instead
1462     // the text color c1 is slightly mixed with the background color.
1463     const QColor c2 = styleOption().palette.base().color();
1464     const int p1 = 70;
1465     const int p2 = 100 - p1;
1466     m_additionalInfoTextColor =
1467         QColor((c1.red() * p1 + c2.red() * p2) / 100, (c1.green() * p1 + c2.green() * p2) / 100, (c1.blue() * p1 + c2.blue() * p2) / 100);
1468 }
1469 
1470 void KStandardItemListWidget::drawPixmap(QPainter *painter, const QPixmap &pixmap)
1471 {
1472     if (m_scaledPixmapSize != pixmap.size() / pixmap.devicePixelRatio()) {
1473         const qreal dpr = KItemViewsUtils::devicePixelRatio(this);
1474         QPixmap scaledPixmap = pixmap;
1475         KPixmapModifier::scale(scaledPixmap, m_scaledPixmapSize * dpr);
1476         scaledPixmap.setDevicePixelRatio(dpr);
1477         painter->drawPixmap(m_pixmapPos, scaledPixmap);
1478 
1479 #ifdef KSTANDARDITEMLISTWIDGET_DEBUG
1480         painter->setPen(Qt::blue);
1481         painter->drawRect(QRectF(m_pixmapPos, QSizeF(m_scaledPixmapSize)));
1482 #endif
1483     } else {
1484         painter->drawPixmap(m_pixmapPos, pixmap);
1485     }
1486 }
1487 
1488 void KStandardItemListWidget::drawSiblingsInformation(QPainter *painter)
1489 {
1490     const int siblingSize = size().height();
1491     const int x = (m_expansionArea.left() + m_expansionArea.right() - siblingSize) / 2;
1492     QRect siblingRect(x, 0, siblingSize, siblingSize);
1493 
1494     bool isItemSibling = true;
1495 
1496     const QBitArray siblings = siblingsInformation();
1497     QStyleOption option;
1498     const auto normalColor = option.palette.color(normalTextColorRole());
1499     const auto highlightColor = option.palette.color(expansionAreaHovered() ? QPalette::Highlight : normalTextColorRole());
1500     for (int i = siblings.count() - 1; i >= 0; --i) {
1501         option.rect = siblingRect;
1502         option.state = siblings.at(i) ? QStyle::State_Sibling : QStyle::State_None;
1503         if (isItemSibling) {
1504             option.state |= QStyle::State_Item;
1505             if (m_isExpandable) {
1506                 option.state |= QStyle::State_Children;
1507             }
1508             if (data().value("isExpanded").toBool()) {
1509                 option.state |= QStyle::State_Open;
1510             }
1511             option.palette.setColor(QPalette::Text, highlightColor);
1512             isItemSibling = false;
1513         } else {
1514             option.palette.setColor(QPalette::Text, normalColor);
1515         }
1516 
1517         style()->drawPrimitive(QStyle::PE_IndicatorBranch, &option, painter);
1518 
1519         siblingRect.translate(-siblingRect.width(), 0);
1520     }
1521 }
1522 
1523 QRectF KStandardItemListWidget::roleEditingRect(const QByteArray &role) const
1524 {
1525     const TextInfo *textInfo = m_textInfo.value(role);
1526     if (!textInfo) {
1527         return QRectF();
1528     }
1529 
1530     QRectF rect(textInfo->pos, textInfo->staticText.size());
1531     if (m_layout == DetailsLayout) {
1532         rect.setWidth(columnWidth(role) - rect.x());
1533     }
1534 
1535     return rect;
1536 }
1537 
1538 void KStandardItemListWidget::closeRoleEditor()
1539 {
1540     disconnect(m_roleEditor, &KItemListRoleEditor::roleEditingCanceled, this, &KStandardItemListWidget::slotRoleEditingCanceled);
1541     disconnect(m_roleEditor, &KItemListRoleEditor::roleEditingFinished, this, &KStandardItemListWidget::slotRoleEditingFinished);
1542 
1543     if (m_roleEditor->hasFocus()) {
1544         // If the editing was not ended by a FocusOut event, we have
1545         // to transfer the keyboard focus back to the KItemListContainer.
1546         scene()->views()[0]->parentWidget()->setFocus();
1547     }
1548 
1549     if (m_oldRoleEditor) {
1550         m_oldRoleEditor->deleteLater();
1551     }
1552     m_oldRoleEditor = m_roleEditor;
1553     m_roleEditor->hide();
1554     m_roleEditor = nullptr;
1555 }
1556 
1557 QPixmap KStandardItemListWidget::pixmapForIcon(const QString &name, const QStringList &overlays, int size, QIcon::Mode mode) const
1558 {
1559     static const QIcon fallbackIcon = QIcon::fromTheme(QStringLiteral("unknown"));
1560     const qreal dpr = KItemViewsUtils::devicePixelRatio(this);
1561 
1562     size *= dpr;
1563 
1564     const QString key = "KStandardItemListWidget:" % name % ":" % overlays.join(QLatin1Char(':')) % ":" % QString::number(size) % "@" % QString::number(dpr)
1565         % ":" % QString::number(mode);
1566     QPixmap pixmap;
1567 
1568     if (!QPixmapCache::find(key, &pixmap)) {
1569         QIcon icon = QIcon::fromTheme(name);
1570         if (icon.isNull()) {
1571             icon = QIcon(name);
1572         }
1573         if (icon.isNull() || icon.pixmap(size / dpr, size / dpr, mode).isNull()) {
1574             icon = fallbackIcon;
1575         }
1576 
1577         pixmap = icon.pixmap(QSize(size / dpr, size / dpr), dpr, mode);
1578         if (pixmap.width() != size || pixmap.height() != size) {
1579             KPixmapModifier::scale(pixmap, QSize(size, size));
1580         }
1581 
1582         // Strangely KFileItem::overlays() returns empty string-values, so
1583         // we need to check first whether an overlay must be drawn at all.
1584         // It is more efficient to do it here, as KIconLoader::drawOverlays()
1585         // assumes that an overlay will be drawn and has some additional
1586         // setup time.
1587         for (const QString &overlay : overlays) {
1588             if (!overlay.isEmpty()) {
1589                 int state = KIconLoader::DefaultState;
1590 
1591                 switch (mode) {
1592                 case QIcon::Normal:
1593                     break;
1594                 case QIcon::Active:
1595                     state = KIconLoader::ActiveState;
1596                     break;
1597                 case QIcon::Disabled:
1598                     state = KIconLoader::DisabledState;
1599                     break;
1600                 case QIcon::Selected:
1601                     state = KIconLoader::SelectedState;
1602                     break;
1603                 }
1604 
1605                 // There is at least one overlay, draw all overlays above m_pixmap
1606                 // and cancel the check
1607                 KIconLoader::global()->drawOverlays(overlays, pixmap, KIconLoader::Desktop, state);
1608                 break;
1609             }
1610         }
1611 
1612         QPixmapCache::insert(key, pixmap);
1613     }
1614     pixmap.setDevicePixelRatio(dpr);
1615 
1616     return pixmap;
1617 }
1618 
1619 QSizeF KStandardItemListWidget::preferredRatingSize(const KItemListStyleOption &option)
1620 {
1621     const qreal height = option.fontMetrics.ascent();
1622     return QSizeF(height * 5, height);
1623 }
1624 
1625 qreal KStandardItemListWidget::columnPadding(const KItemListStyleOption &option)
1626 {
1627     return option.padding * 6;
1628 }
1629 
1630 #include "moc_kstandarditemlistwidget.cpp"