X-Git-Url: https://cloud.milkyroute.net/gitweb/dolphin.git/blobdiff_plain/8297e0a8c97c6e1f6d46b9ecc9c5f495f294d5b8..0464ea82a6850f58805bc4d6fc1df5369d83c3df:/src/kitemviews/kstandarditemlistwidget.cpp diff --git a/src/kitemviews/kstandarditemlistwidget.cpp b/src/kitemviews/kstandarditemlistwidget.cpp index 54e1fa50b..566c4dec2 100644 --- a/src/kitemviews/kstandarditemlistwidget.cpp +++ b/src/kitemviews/kstandarditemlistwidget.cpp @@ -7,15 +7,17 @@ #include "kstandarditemlistwidget.h" #include "kfileitemlistview.h" -#include "kfileitemmodel.h" #include "private/kfileitemclipboard.h" #include "private/kitemlistroleeditor.h" +#include "private/kitemviewsutils.h" #include "private/kpixmapmodifier.h" #include #include +#include #include #include +#include #include #include @@ -23,6 +25,8 @@ #include #include #include +#include +#include // #define KSTANDARDITEMLISTWIDGET_DEBUG @@ -105,11 +109,20 @@ bool KStandardItemListWidgetInformant::itemIsLink(int index, const KItemListView return false; } -QString KStandardItemListWidgetInformant::roleText(const QByteArray &role, const QHash &values) const +QString KStandardItemListWidgetInformant::roleText(const QByteArray &role, const QHash &values, ForUsageAs forUsageAs) const { if (role == "rating") { - // Always use an empty text, as the rating is shown by the image m_rating. - return QString(); + if (forUsageAs == ForUsageAs::DisplayedText) { + // Always use an empty text, as the rating is shown by the image m_rating. + return QString(); + } else { + const int rating{values.value(role).toInt()}; + // Check if there are half stars + if (rating % 2) { + return i18ncp("@accessible rating", "%1 and a half stars", "%1 and a half stars", rating / 2); + } + return i18ncp("@accessible rating", "%1 star", "%1 stars", rating / 2); + } } return values.value(role).toString(); } @@ -263,7 +276,7 @@ KStandardItemListWidget::KStandardItemListWidget(KItemListWidgetInformant *infor , m_expansionArea() , m_customTextColor() , m_additionalInfoTextColor() - , m_overlay() + , m_overlays() , m_rating() , m_roleEditor(nullptr) , m_oldRoleEditor(nullptr) @@ -294,11 +307,6 @@ void KStandardItemListWidget::setLayout(Layout layout) } } -KStandardItemListWidget::Layout KStandardItemListWidget::layout() const -{ - return m_layout; -} - void KStandardItemListWidget::setHighlightEntireRow(bool highlightEntireRow) { if (m_highlightEntireRow != highlightEntireRow) { @@ -337,24 +345,39 @@ void KStandardItemListWidget::paint(QPainter *painter, const QStyleOptionGraphic drawSiblingsInformation(painter); } + auto pixmap = isHovered() ? m_hoverPixmap : m_pixmap; + if (!m_overlays.isEmpty()) { + const qreal dpr = KItemViewsUtils::devicePixelRatio(this); + + const bool iconOnTop = (m_layout == IconsLayout); + const KItemListStyleOption &option = styleOption(); + const qreal padding = option.padding; + + const int widgetIconSize = iconSize(); + const int maxIconWidth = iconOnTop ? size().width() - 2 * padding : widgetIconSize; + const int maxIconHeight = widgetIconSize; + + pixmap = addOverlays(pixmap, m_overlays, QSize(maxIconWidth, maxIconHeight), dpr); + } + const KItemListStyleOption &itemListStyleOption = styleOption(); - if (isHovered() && !m_pixmap.isNull()) { + if (isHovered() && !pixmap.isNull()) { if (hoverOpacity() < 1.0) { /* - * Linear interpolation between m_pixmap and m_hoverPixmap. + * Linear interpolation between pixmap and m_hoverPixmap. * * Note that this cannot be achieved by painting m_hoverPixmap over - * m_pixmap, even if the opacities are adjusted. For details see + * pixmap, even if the opacities are adjusted. For details see * https://git.reviewboard.kde.org/r/109614/ */ - // Paint pixmap1 so that pixmap1 = m_pixmap * (1.0 - hoverOpacity()) - QPixmap pixmap1(m_pixmap.size()); - pixmap1.setDevicePixelRatio(m_pixmap.devicePixelRatio()); + // Paint pixmap1 so that pixmap1 = pixmap * (1.0 - hoverOpacity()) + QPixmap pixmap1(pixmap.size()); + pixmap1.setDevicePixelRatio(pixmap.devicePixelRatio()); pixmap1.fill(Qt::transparent); { QPainter p(&pixmap1); p.setOpacity(1.0 - hoverOpacity()); - p.drawPixmap(0, 0, m_pixmap); + p.drawPixmap(0, 0, pixmap); } // Paint pixmap2 so that pixmap2 = m_hoverPixmap * hoverOpacity() @@ -368,8 +391,8 @@ void KStandardItemListWidget::paint(QPainter *painter, const QStyleOptionGraphic } // Paint pixmap2 on pixmap1 using CompositionMode_Plus - // Now pixmap1 = pixmap2 + m_pixmap * (1.0 - hoverOpacity()) - // = m_hoverPixmap * hoverOpacity() + m_pixmap * (1.0 - hoverOpacity()) + // Now pixmap1 = pixmap2 + pixmap * (1.0 - hoverOpacity()) + // = m_hoverPixmap * hoverOpacity() + pixmap * (1.0 - hoverOpacity()) { QPainter p(&pixmap1); p.setCompositionMode(QPainter::CompositionMode_Plus); @@ -379,10 +402,10 @@ void KStandardItemListWidget::paint(QPainter *painter, const QStyleOptionGraphic // Finally paint pixmap1 on the widget drawPixmap(painter, pixmap1); } else { - drawPixmap(painter, m_hoverPixmap); + drawPixmap(painter, pixmap); } - } else if (!m_pixmap.isNull()) { - drawPixmap(painter, m_pixmap); + } else if (!pixmap.isNull()) { + drawPixmap(painter, pixmap); } painter->setFont(m_customizedFont); @@ -401,15 +424,26 @@ void KStandardItemListWidget::paint(QPainter *painter, const QStyleOptionGraphic painter->drawStaticText(textInfo->pos, textInfo->staticText); bool clipAdditionalInfoBounds = false; - if (m_supportsItemExpanding) { - // Prevent a possible overlapping of the additional-information texts - // with the icon. This can happen if the user has minimized the width - // of the name-column to a very small value. - const qreal minX = m_pixmapPos.x() + m_pixmap.width() + 4 * itemListStyleOption.padding; - if (textInfo->pos.x() + columnWidth("text") > minX) { - clipAdditionalInfoBounds = true; - painter->save(); - painter->setClipRect(minX, 0, size().width() - minX, size().height(), Qt::IntersectClip); + if (m_supportsItemExpanding && m_sortedVisibleRoles.count() > 1) { + // Prevent a possible overlapping of the additional-information-texts with the icon. + // This will happen if the user has resized the width of the name-column to be very narrow while having folders expanded. + // We only want to draw additional info text outside the area of the icon or expansion area, so we set a clip rect that does not contain the icon area. + // This needs to work both for left-to-right as well as right-to-left layout directions. + const TextInfo *potentiallyOverlappingRoleText = m_textInfo.value(m_sortedVisibleRoles[1]); // Only the first column after the name column can overlap. + if (layoutDirection() == Qt::LeftToRight) { // In left-to-right languages the left end of text would overlap. This is mirrored for right-to-left. + const qreal minX = m_iconRect.right() + 2 * itemListStyleOption.padding; + if (potentiallyOverlappingRoleText->pos.x() < minX) { + clipAdditionalInfoBounds = true; + painter->save(); + painter->setClipRect(minX, 0, size().width() - minX, size().height(), Qt::IntersectClip); + } + } else { + const qreal maxX = m_iconRect.left() - 2 * itemListStyleOption.padding; + if (potentiallyOverlappingRoleText->pos.x() + m_customizedFontMetrics.horizontalAdvance(potentiallyOverlappingRoleText->staticText.text()) > maxX) { + clipAdditionalInfoBounds = true; + painter->save(); + painter->setClipRect(0, 0, maxX, size().height(), Qt::IntersectClip); + } } } @@ -516,7 +550,11 @@ QRectF KStandardItemListWidget::selectionRect() const QRectF adjustedIconRect = iconRect().adjusted(-padding, -padding, padding, padding); QRectF result = adjustedIconRect | m_textRect; if (m_highlightEntireRow) { - result.setRight(m_columnWidthSum + sidePadding()); + if (layoutDirection() == Qt::LeftToRight) { + result.setRight(leftPadding() + m_columnWidthSum); + } else { + result.setLeft(size().width() - m_columnWidthSum - rightPadding()); + } } return result; } @@ -539,6 +577,7 @@ QRectF KStandardItemListWidget::selectionToggleRect() const { const_cast(this)->triggerCacheRefreshing(); + const QRectF widgetIconRect = iconRect(); const int widgetIconSize = iconSize(); int toggleSize = KIconLoader::SizeSmall; if (widgetIconSize >= KIconLoader::SizeEnormous) { @@ -547,7 +586,7 @@ QRectF KStandardItemListWidget::selectionToggleRect() const toggleSize = KIconLoader::SizeSmallMedium; } - QPointF pos = iconRect().topLeft(); + QPointF pos = widgetIconRect.topLeft(); // If the selection toggle has a very small distance to the // widget borders, the size of the selection toggle will get @@ -568,6 +607,10 @@ QRectF KStandardItemListWidget::selectionToggleRect() const pos.setX(0); } + if (QApplication::isRightToLeft()) { + pos.setX(widgetIconRect.right() - (pos.x() + toggleSize - widgetIconRect.left())); + } + return QRectF(pos, QSizeF(toggleSize, toggleSize)); } @@ -593,6 +636,55 @@ QPixmap KStandardItemListWidget::createDragPixmap(const QStyleOptionGraphicsItem return clippedPixmap; } +void KStandardItemListWidget::startActivateSoonAnimation(int timeUntilActivation) +{ + if (m_activateSoonAnimation) { + m_activateSoonAnimation->stop(); // automatically DeleteWhenStopped + } + + m_activateSoonAnimation = new QVariantAnimation{this}; + m_activateSoonAnimation->setStartValue(0.0); + m_activateSoonAnimation->setEndValue(1.0); + m_activateSoonAnimation->setDuration(timeUntilActivation); + + const QVariant originalIconName{value("iconName")}; + connect(m_activateSoonAnimation, &QVariantAnimation::valueChanged, this, [originalIconName, this](const QVariant &value) { + auto progress = value.toFloat(); + + QVariant wantedIconName; + if (progress < 0.333) { + wantedIconName = "folder-open"; + } else if (progress < 0.666) { + wantedIconName = originalIconName; + } else { + wantedIconName = "folder-open"; + } + + QHash itemData{data()}; + if (itemData["iconName"] != wantedIconName) { + itemData.insert("iconName", wantedIconName); + setData(itemData); + invalidateIconCache(); + } + }); + + connect(m_activateSoonAnimation, &QObject::destroyed, this, [originalIconName, this]() { + QHash itemData{data()}; + if (itemData["iconName"] == "folder-open") { + itemData.insert("iconName", originalIconName); + setData(itemData); + invalidateIconCache(); + } + }); + + m_activateSoonAnimation->start(QAbstractAnimation::DeleteWhenStopped); +} + +bool KStandardItemListWidget::isIconControlledByActivateSoonAnimation() const +{ + return m_activateSoonAnimation && value("iconName") == "folder-open"; +} + KItemListWidgetInformant *KStandardItemListWidget::createInformant() { return new KStandardItemListWidgetInformant(); @@ -608,7 +700,6 @@ void KStandardItemListWidget::invalidateIconCache() { m_dirtyContent = true; m_dirtyContentRoles.insert("iconPixmap"); - m_dirtyContentRoles.insert("iconOverlays"); } void KStandardItemListWidget::refreshCache() @@ -660,16 +751,21 @@ QColor KStandardItemListWidget::textColor(const QWidget &widget) const return styleOption().palette.color(group, role); } -void KStandardItemListWidget::setOverlay(const QPixmap &overlay) +void KStandardItemListWidget::setOverlays(QHash &overlays) { - m_overlay = overlay; + if (overlays == m_overlays) { + return; + } + + m_overlays = overlays; m_dirtyContent = true; + m_dirtyContentRoles.insert("iconOverlays"); update(); } -QPixmap KStandardItemListWidget::overlay() const +QHash KStandardItemListWidget::overlays() const { - return m_overlay; + return m_overlays; } QString KStandardItemListWidget::roleText(const QByteArray &role, const QHash &values) const @@ -724,9 +820,10 @@ void KStandardItemListWidget::columnWidthChanged(const QByteArray &role, qreal c m_dirtyLayout = true; } -void KStandardItemListWidget::sidePaddingChanged(qreal padding) +void KStandardItemListWidget::sidePaddingChanged(qreal leftPaddingWidth, qreal rightPaddingWidth) { - Q_UNUSED(padding) + Q_UNUSED(leftPaddingWidth) + Q_UNUSED(rightPaddingWidth) m_dirtyLayout = true; } @@ -740,7 +837,9 @@ void KStandardItemListWidget::styleOptionChanged(const KItemListStyleOption &cur void KStandardItemListWidget::hoveredChanged(bool hovered) { - Q_UNUSED(hovered) + if (!hovered && m_activateSoonAnimation) { + m_activateSoonAnimation->stop(); // automatically DeleteWhenStopped + } m_dirtyLayout = true; } @@ -758,9 +857,19 @@ void KStandardItemListWidget::siblingsInformationChanged(const QBitArray ¤ m_dirtyLayout = true; } +int KStandardItemListWidget::numberOfUnicodeCharactersIn(const QString &text) +{ + int count = 0; + QTextBoundaryFinder boundaryFinder(QTextBoundaryFinder::Grapheme, text); + while (boundaryFinder.toNextBoundary() != -1) { + ++count; + } + return count; +} + int KStandardItemListWidget::selectionLength(const QString &text) const { - return text.length(); + return numberOfUnicodeCharactersIn(text); } void KStandardItemListWidget::editedRoleChanged(const QByteArray ¤t, const QByteArray &previous) @@ -771,16 +880,7 @@ void KStandardItemListWidget::editedRoleChanged(const QByteArray ¤t, const if (current.isEmpty() || !parent || current != "text") { if (m_roleEditor) { Q_EMIT roleEditingCanceled(index(), current, data().value(current)); - - disconnect(m_roleEditor, &KItemListRoleEditor::roleEditingCanceled, this, &KStandardItemListWidget::slotRoleEditingCanceled); - disconnect(m_roleEditor, &KItemListRoleEditor::roleEditingFinished, this, &KStandardItemListWidget::slotRoleEditingFinished); - - if (m_oldRoleEditor) { - m_oldRoleEditor->deleteLater(); - } - m_oldRoleEditor = m_roleEditor; - m_roleEditor->hide(); - m_roleEditor = nullptr; + closeRoleEditor(); } return; } @@ -947,8 +1047,13 @@ void KStandardItemListWidget::updateExpansionArea() const qreal inc = (widgetHeight - widgetIconSize) / 2; const qreal x = expandedParentsCount * widgetHeight + inc; const qreal y = inc; - const qreal xPadding = m_highlightEntireRow ? sidePadding() : 0; - m_expansionArea = QRectF(xPadding + x, y, widgetIconSize, widgetIconSize); + if (layoutDirection() == Qt::LeftToRight) { + const qreal leftPaddingWidth = m_highlightEntireRow ? leftPadding() : 0; + m_expansionArea = QRectF(leftPaddingWidth + x, y, widgetIconSize, widgetIconSize); + return; + } + const qreal rightPaddingWidth = m_highlightEntireRow ? rightPadding() : 0; + m_expansionArea = QRectF(size().width() - rightPaddingWidth - x - widgetIconSize, y, widgetIconSize, widgetIconSize); return; } } @@ -965,6 +1070,7 @@ void KStandardItemListWidget::updatePixmapCache() const bool iconOnTop = (m_layout == IconsLayout); const KItemListStyleOption &option = styleOption(); const qreal padding = option.padding; + const qreal dpr = KItemViewsUtils::devicePixelRatio(this); const int widgetIconSize = iconSize(); const int maxIconWidth = iconOnTop ? widgetSize.width() - 2 * padding : widgetIconSize; @@ -983,7 +1089,7 @@ void KStandardItemListWidget::updatePixmapCache() int sequenceIndex = hoverSequenceIndex(); - if (values.contains("hoverSequencePixmaps")) { + if (values.contains("hoverSequencePixmaps") && !isIconControlledByActivateSoonAnimation()) { // Use one of the hover sequence pixmaps instead of the default // icon pixmap. @@ -1003,7 +1109,7 @@ void KStandardItemListWidget::updatePixmapCache() } } - if (m_pixmap.isNull()) { + if (m_pixmap.isNull() && !isIconControlledByActivateSoonAnimation()) { m_pixmap = values["iconPixmap"].value(); } @@ -1015,17 +1121,17 @@ void KStandardItemListWidget::updatePixmapCache() // use a generic icon as fallback iconName = QStringLiteral("unknown"); } - const QStringList overlays = values["iconOverlays"].toStringList(); const bool hasFocus = scene()->views()[0]->parentWidget()->hasFocus(); m_pixmap = pixmapForIcon(iconName, - overlays, - maxIconHeight, + QSize(maxIconWidth, maxIconHeight), m_layout != IconsLayout && isActiveWindow() && isSelected() && hasFocus ? QIcon::Selected : QIcon::Normal); - } else if (m_pixmap.width() / m_pixmap.devicePixelRatio() != maxIconWidth || m_pixmap.height() / m_pixmap.devicePixelRatio() != maxIconHeight) { - // A custom pixmap has been applied. Assure that the pixmap - // is scaled to the maximum available size. - KPixmapModifier::scale(m_pixmap, QSize(maxIconWidth, maxIconHeight) * qApp->devicePixelRatio()); + } else { + if (m_pixmap.width() / m_pixmap.devicePixelRatio() != maxIconWidth || m_pixmap.height() / m_pixmap.devicePixelRatio() != maxIconHeight) { + // A custom pixmap has been applied. Assure that the pixmap + // is scaled to the maximum available size. + KPixmapModifier::scale(m_pixmap, QSize(maxIconWidth, maxIconHeight) * dpr); + } } if (m_pixmap.isNull()) { @@ -1034,8 +1140,7 @@ void KStandardItemListWidget::updatePixmapCache() } if (m_isCut) { - KIconEffect *effect = KIconLoader::global()->iconEffect(); - m_pixmap = effect->apply(m_pixmap, KIconLoader::Desktop, KIconLoader::DisabledState); + KIconEffect::toDisabled(m_pixmap); } if (m_isHidden) { @@ -1054,11 +1159,6 @@ void KStandardItemListWidget::updatePixmapCache() } } - if (!m_overlay.isNull()) { - QPainter painter(&m_pixmap); - painter.drawPixmap(0, (m_pixmap.height() - m_overlay.height()) / m_pixmap.devicePixelRatio(), m_overlay); - } - int scaledIconSize = 0; if (iconOnTop) { const TextInfo *textInfo = m_textInfo.value("text"); @@ -1073,8 +1173,8 @@ void KStandardItemListWidget::updatePixmapCache() const int maxScaledIconHeight = scaledIconSize; m_scaledPixmapSize = m_pixmap.size(); - m_scaledPixmapSize.scale(maxScaledIconWidth * qApp->devicePixelRatio(), maxScaledIconHeight * qApp->devicePixelRatio(), Qt::KeepAspectRatio); - m_scaledPixmapSize = m_scaledPixmapSize / qApp->devicePixelRatio(); + m_scaledPixmapSize.scale(maxScaledIconWidth * dpr, maxScaledIconHeight * dpr, Qt::KeepAspectRatio); + m_scaledPixmapSize = m_scaledPixmapSize / dpr; if (iconOnTop) { // Center horizontally and align on bottom within the icon-area @@ -1083,7 +1183,11 @@ void KStandardItemListWidget::updatePixmapCache() } else { // Center horizontally and vertically within the icon-area const TextInfo *textInfo = m_textInfo.value("text"); - m_pixmapPos.setX(textInfo->pos.x() - 2.0 * padding - (scaledIconSize + m_scaledPixmapSize.width()) / 2.0); + if (QApplication::isRightToLeft()) { + m_pixmapPos.setX(m_textRect.right() + 2.0 * padding); + } else { + m_pixmapPos.setX(textInfo->pos.x() - 2.0 * padding - (scaledIconSize + m_scaledPixmapSize.width()) / 2.0); + } // Derive icon's vertical center from the center of the text frame, including // any necessary adjustment if the font's midline is offset from the frame center @@ -1104,13 +1208,7 @@ void KStandardItemListWidget::updatePixmapCache() // Prepare the pixmap that is used when the item gets hovered if (isHovered()) { m_hoverPixmap = m_pixmap; - KIconEffect *effect = KIconLoader::global()->iconEffect(); - // In the KIconLoader terminology, active = hover. - if (effect->hasEffect(KIconLoader::Desktop, KIconLoader::ActiveState)) { - m_hoverPixmap = effect->apply(m_pixmap, KIconLoader::Desktop, KIconLoader::ActiveState); - } else { - m_hoverPixmap = m_pixmap; - } + KIconEffect::toActive(m_hoverPixmap); } else if (hoverOpacity() <= 0.0) { // No hover animation is ongoing. Clear m_hoverPixmap to save memory. m_hoverPixmap = QPixmap(); @@ -1126,6 +1224,9 @@ void KStandardItemListWidget::updateTextsCache() textOption.setAlignment(Qt::AlignHCenter); break; case CompactLayout: + textOption.setAlignment(QApplication::isRightToLeft() ? Qt::AlignRight : Qt::AlignLeft); + textOption.setWrapMode(QTextOption::NoWrap); + break; case DetailsLayout: textOption.setAlignment(Qt::AlignLeft); textOption.setWrapMode(QTextOption::NoWrap); @@ -1171,7 +1272,7 @@ void KStandardItemListWidget::updateTextsCache() if (ratingSize.width() > availableWidth) { ratingSize.rwidth() = availableWidth; } - const qreal dpr = qApp->devicePixelRatio(); + const qreal dpr = KItemViewsUtils::devicePixelRatio(this); m_rating = QPixmap(ratingSize.toSize() * dpr); m_rating.setDevicePixelRatio(dpr); m_rating.fill(Qt::transparent); @@ -1185,27 +1286,6 @@ void KStandardItemListWidget::updateTextsCache() } } -QString KStandardItemListWidget::elideRightKeepExtension(const QString &text, int elidingWidth) const -{ - const auto extensionIndex = text.lastIndexOf('.'); - if (extensionIndex != -1) { - // has file extension - const auto extensionLength = text.length() - extensionIndex; - const auto extensionWidth = m_customizedFontMetrics.horizontalAdvance(text.right(extensionLength)); - if (elidingWidth > extensionWidth && extensionLength < 6 && (float(extensionWidth) / float(elidingWidth)) < 0.3) { - // if we have room to display the file extension and the extension is not too long - QString ret = m_customizedFontMetrics.elidedText(text.chopped(extensionLength), Qt::ElideRight, elidingWidth - extensionWidth); -#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) - ret.append(text.rightRef(extensionLength)); -#else - ret.append(QStringView(text).right(extensionLength)); -#endif - return ret; - } - } - return m_customizedFontMetrics.elidedText(text, Qt::ElideRight, elidingWidth); -} - QString KStandardItemListWidget::escapeString(const QString &text) const { QString escaped(text); @@ -1266,7 +1346,7 @@ void KStandardItemListWidget::updateIconsLayoutTextCache() qreal lastLineWidth; do { QString lastTextLine = nameText.mid(line.textStart()); - lastTextLine = elideRightKeepExtension(lastTextLine, elidingWidth); + lastTextLine = m_customizedFontMetrics.elidedText(lastTextLine, Qt::ElideMiddle, elidingWidth); const QString elidedText = nameText.left(line.textStart()) + lastTextLine; nameTextInfo->staticText.setText(elidedText); @@ -1314,7 +1394,7 @@ void KStandardItemListWidget::updateIconsLayoutTextCache() textLine.setLineWidth(maxWidth); requiredWidth = textLine.naturalTextWidth(); if (requiredWidth > maxWidth) { - const QString elidedText = elideRightKeepExtension(text, maxWidth); + const QString elidedText = m_customizedFontMetrics.elidedText(text, Qt::ElideMiddle, maxWidth); textInfo->staticText.setText(elidedText); requiredWidth = m_customizedFontMetrics.horizontalAdvance(elidedText); } else if (role == "rating") { @@ -1355,9 +1435,9 @@ void KStandardItemListWidget::updateCompactLayoutTextCache() const qreal textLinesHeight = qMax(visibleRoles().count(), 1) * lineSpacing; qreal maximumRequiredTextWidth = 0; - const qreal x = option.padding * 3 + iconSize(); + const qreal x = QApplication::isRightToLeft() ? option.padding : option.padding * 3 + iconSize(); qreal y = qRound((widgetHeight - textLinesHeight) / 2); - const qreal maxWidth = size().width() - x - option.padding; + const qreal maxWidth = size().width() - iconSize() - 4 * option.padding; for (const QByteArray &role : std::as_const(m_sortedVisibleRoles)) { const QString text = escapeString(roleText(role, values)); TextInfo *textInfo = m_textInfo.value(role); @@ -1366,7 +1446,7 @@ void KStandardItemListWidget::updateCompactLayoutTextCache() qreal requiredWidth = m_customizedFontMetrics.horizontalAdvance(text); if (requiredWidth > maxWidth) { requiredWidth = maxWidth; - const QString elidedText = elideRightKeepExtension(text, maxWidth); + const QString elidedText = m_customizedFontMetrics.elidedText(text, Qt::ElideMiddle, maxWidth); textInfo->staticText.setText(elidedText); } @@ -1389,6 +1469,8 @@ void KStandardItemListWidget::updateDetailsLayoutTextCache() // +------+ // | Icon | Name role Additional role 1 Additional role 2 // +------+ + // Mirror the above for right-to-left languages. + const bool isLeftToRight = QApplication::layoutDirection() == Qt::LeftToRight; m_textRect = QRectF(); const KItemListStyleOption &option = styleOption(); @@ -1400,9 +1482,10 @@ void KStandardItemListWidget::updateDetailsLayoutTextCache() const qreal columnWidthInc = columnPadding(option); qreal firstColumnInc = iconSize(); if (m_supportsItemExpanding) { - firstColumnInc += (m_expansionArea.left() + m_expansionArea.right() + widgetHeight) / 2; + firstColumnInc += isLeftToRight ? (m_expansionArea.left() + m_expansionArea.right() + widgetHeight) / 2 + : ((size().width() - m_expansionArea.left()) + (size().width() - m_expansionArea.right()) + widgetHeight) / 2; } else { - firstColumnInc += option.padding + sidePadding(); + firstColumnInc += option.padding + (isLeftToRight ? leftPadding() : rightPadding()); } qreal x = firstColumnInc; @@ -1419,27 +1502,26 @@ void KStandardItemListWidget::updateDetailsLayoutTextCache() const bool isTextRole = (role == "text"); if (isTextRole) { text = escapeString(text); - availableTextWidth -= firstColumnInc - sidePadding(); + availableTextWidth -= firstColumnInc - (isLeftToRight ? leftPadding() : rightPadding()); } if (requiredWidth > availableTextWidth) { - text = elideRightKeepExtension(text, availableTextWidth); + text = m_customizedFontMetrics.elidedText(text, Qt::ElideMiddle, availableTextWidth); requiredWidth = m_customizedFontMetrics.horizontalAdvance(text); } TextInfo *textInfo = m_textInfo.value(role); textInfo->staticText.setText(text); - textInfo->pos = QPointF(x + columnWidthInc / 2, y); + textInfo->pos = QPointF(isLeftToRight ? (x + columnWidthInc / 2) : (size().width() - (x + columnWidthInc / 2) - requiredWidth), y); x += roleWidth; if (isTextRole) { - const qreal textWidth = option.extendedSelectionRegion ? size().width() - textInfo->pos.x() : requiredWidth + 2 * option.padding; - m_textRect = QRectF(textInfo->pos.x() - option.padding, 0, textWidth, size().height()); + m_textRect = QRectF(textInfo->pos.x() - option.padding, 0, requiredWidth + 2 * option.padding, size().height()); // The column after the name should always be aligned on the same x-position independent // from the expansion-level shown in the name column - x -= firstColumnInc - sidePadding(); - } else if (isRoleRightAligned(role)) { + x -= firstColumnInc - (isLeftToRight ? leftPadding() : rightPadding()); + } else if (isRoleRightAligned(role) && isLeftToRight) { textInfo->pos.rx() += roleWidth - requiredWidth - columnWidthInc; } } @@ -1452,8 +1534,8 @@ void KStandardItemListWidget::updateAdditionalInfoTextColor() if (m_customTextColor.isValid()) { c1 = m_customTextColor; } else if (isSelected() && hasFocus && (m_layout != DetailsLayout || m_highlightEntireRow)) { - // The detail text colour needs to match the main text (HighlightedText) for the same level - // of readability. We short circuit early here to avoid interpolating with another colour. + // The detail text color needs to match the main text (HighlightedText) for the same level + // of readability. We short circuit early here to avoid interpolating with another color. m_additionalInfoTextColor = styleOption().palette.color(QPalette::HighlightedText); return; } else { @@ -1470,12 +1552,83 @@ void KStandardItemListWidget::updateAdditionalInfoTextColor() QColor((c1.red() * p1 + c2.red() * p2) / 100, (c1.green() * p1 + c2.green() * p2) / 100, (c1.blue() * p1 + c2.blue() * p2) / 100); } +QPixmap +KStandardItemListWidget::addOverlays(const QPixmap &pixmap, const QHash &overlays, const QSize &size, qreal dpr, QIcon::Mode mode) const +{ + // similar to KIconUtils::addOverlays, keep in sync preferrably + if (overlays.isEmpty()) { + return pixmap; + } + + int width = size.width(); + int height = size.height(); + const int iconSize = qMin(width, height); + + // Determine the overlay icon + int overlaySize; + if (iconSize < 32) { + overlaySize = 8; + } else if (iconSize <= 48) { + overlaySize = 16; + } else if (iconSize <= 96) { + overlaySize = 22; + } else if (iconSize < 256) { + overlaySize = 32; + } else { + overlaySize = 64; + } + + auto phyiscalSize = QSize(std::clamp(pixmap.width(), qFloor(2 * overlaySize * dpr), qFloor(size.width() * dpr)), + std::clamp(pixmap.height(), qFloor(2 * overlaySize * dpr), qFloor(size.height() * dpr))); + + QPixmap output(phyiscalSize); + output.setDevicePixelRatio(dpr); + output.fill(Qt::transparent); + + QPainter painter(&output); + painter.drawPixmap(qFloor(phyiscalSize.width() / dpr / 2) - qFloor(pixmap.width() / pixmap.devicePixelRatio() / 2), + // align the icon to the bottom to match the behavior elsewhere + qFloor(phyiscalSize.height() / dpr) - qFloor(pixmap.height() / pixmap.devicePixelRatio()), + pixmap); + + width = qCeil(phyiscalSize.width() / dpr); + height = qCeil(phyiscalSize.height() / dpr); + + // Iterate over stored overlays + for (const auto &[corner, overlay] : overlays.asKeyValueRange()) { + const QPixmap overlayPixmap = QIcon::fromTheme(overlay).pixmap(QSize{overlaySize, overlaySize}, dpr, mode); + if (overlayPixmap.isNull()) { + continue; + } + + QPoint startPoint; + switch (corner) { + case Qt::BottomLeftCorner: + startPoint = QPoint{0, height - overlaySize}; + break; + case Qt::BottomRightCorner: + startPoint = QPoint{width - overlaySize, height - overlaySize}; + break; + case Qt::TopRightCorner: + startPoint = QPoint{width - overlaySize, 0}; + break; + case Qt::TopLeftCorner: + startPoint = QPoint{}; + break; + } + painter.drawPixmap(startPoint, overlayPixmap); + } + + return output; +} + void KStandardItemListWidget::drawPixmap(QPainter *painter, const QPixmap &pixmap) { if (m_scaledPixmapSize != pixmap.size() / pixmap.devicePixelRatio()) { + const qreal dpr = KItemViewsUtils::devicePixelRatio(this); QPixmap scaledPixmap = pixmap; - KPixmapModifier::scale(scaledPixmap, m_scaledPixmapSize * qApp->devicePixelRatio()); - scaledPixmap.setDevicePixelRatio(qApp->devicePixelRatio()); + KPixmapModifier::scale(scaledPixmap, m_scaledPixmapSize * dpr); + scaledPixmap.setDevicePixelRatio(dpr); painter->drawPixmap(m_pixmapPos, scaledPixmap); #ifdef KSTANDARDITEMLISTWIDGET_DEBUG @@ -1518,7 +1671,7 @@ void KStandardItemListWidget::drawSiblingsInformation(QPainter *painter) style()->drawPrimitive(QStyle::PE_IndicatorBranch, &option, painter); - siblingRect.translate(-siblingRect.width(), 0); + siblingRect.translate(layoutDirection() == Qt::LeftToRight ? -siblingRect.width() : siblingRect.width(), 0); } } @@ -1556,18 +1709,15 @@ void KStandardItemListWidget::closeRoleEditor() m_roleEditor = nullptr; } -QPixmap KStandardItemListWidget::pixmapForIcon(const QString &name, const QStringList &overlays, int size, QIcon::Mode mode) const +QPixmap KStandardItemListWidget::pixmapForIcon(const QString &name, const QSize &size, QIcon::Mode mode) const { static const QIcon fallbackIcon = QIcon::fromTheme(QStringLiteral("unknown")); - qreal dpr = qApp->devicePixelRatio(); - if (scene() && !scene()->views().isEmpty()) { - dpr = scene()->views().constFirst()->devicePixelRatioF(); - } + const qreal dpr = KItemViewsUtils::devicePixelRatio(this); - size *= dpr; + int iconHeight = size.height(); + QSize iconSize = QSize(iconHeight, iconHeight); - const QString key = "KStandardItemListWidget:" % name % ":" % overlays.join(QLatin1Char(':')) % ":" % QString::number(size) % "@" % QString::number(dpr) - % ":" % QString::number(mode); + const QString key = "KStandardItemListWidget:" % name % ":" % QString::number(iconHeight) % "@" % QString::number(dpr) % ":" % QString::number(mode); QPixmap pixmap; if (!QPixmapCache::find(key, &pixmap)) { @@ -1575,43 +1725,15 @@ QPixmap KStandardItemListWidget::pixmapForIcon(const QString &name, const QStrin if (icon.isNull()) { icon = QIcon(name); } - if (icon.isNull() || icon.pixmap(size / dpr, size / dpr, mode).isNull()) { - icon = fallbackIcon; + if (!icon.isNull()) { + pixmap = icon.pixmap(iconSize, dpr, mode); } - - pixmap = icon.pixmap(QSize(size / dpr, size / dpr), dpr, mode); - if (pixmap.width() != size || pixmap.height() != size) { - KPixmapModifier::scale(pixmap, QSize(size, size)); + if (pixmap.isNull()) { + icon = fallbackIcon; + pixmap = icon.pixmap(iconSize, dpr, mode); } - - // Strangely KFileItem::overlays() returns empty string-values, so - // we need to check first whether an overlay must be drawn at all. - // It is more efficient to do it here, as KIconLoader::drawOverlays() - // assumes that an overlay will be drawn and has some additional - // setup time. - for (const QString &overlay : overlays) { - if (!overlay.isEmpty()) { - int state = KIconLoader::DefaultState; - - switch (mode) { - case QIcon::Normal: - break; - case QIcon::Active: - state = KIconLoader::ActiveState; - break; - case QIcon::Disabled: - state = KIconLoader::DisabledState; - break; - case QIcon::Selected: - state = KIconLoader::SelectedState; - break; - } - - // There is at least one overlay, draw all overlays above m_pixmap - // and cancel the check - KIconLoader::global()->drawOverlays(overlays, pixmap, KIconLoader::Desktop, state); - break; - } + if (pixmap.width() != iconHeight * dpr || pixmap.height() != iconHeight * dpr) { + KPixmapModifier::scale(pixmap, iconSize * dpr); } QPixmapCache::insert(key, pixmap);