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> ¤t, 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> ¤t, 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 ¤t, 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 ¤t, 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 ¤t, 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"