kstandarditemlistwidget.cpp 54.1 KB
Newer Older
1
2
3
4
5
/*
 * SPDX-FileCopyrightText: 2012 Peter Penz <peter.penz19@gmail.com>
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */
6
7
8
9
10

#include "kstandarditemlistwidget.h"

#include "kfileitemlistview.h"
#include "kfileitemmodel.h"
Roman Inflianskas's avatar
Roman Inflianskas committed
11
12
13
#include "private/kfileitemclipboard.h"
#include "private/kitemlistroleeditor.h"
#include "private/kpixmapmodifier.h"
14
15
16

#include <KIconEffect>
#include <KIconLoader>
17
#include <KRatingPainter>
18
19
20
21
22
#include <KStringHandler>

#include <QGraphicsScene>
#include <QGraphicsSceneResizeEvent>
#include <QGraphicsView>
23
#include <QGuiApplication>
Roman Inflianskas's avatar
Roman Inflianskas committed
24
25
#include <QPixmapCache>
#include <QStyleOption>
26

27
// #define KSTANDARDITEMLISTWIDGET_DEBUG
28
29
30
31
32
33
34
35
36
37

KStandardItemListWidgetInformant::KStandardItemListWidgetInformant() :
    KItemListWidgetInformant()
{
}

KStandardItemListWidgetInformant::~KStandardItemListWidgetInformant()
{
}

38
void KStandardItemListWidgetInformant::calculateItemSizeHints(QVector<qreal>& logicalHeightHints, qreal& logicalWidthHint, const KItemListView* view) const
39
40
{
    switch (static_cast<const KStandardItemListView*>(view)->itemLayout()) {
41
    case KStandardItemListView::IconsLayout:
42
        calculateIconsLayoutItemSizeHints(logicalHeightHints, logicalWidthHint, view);
43
        break;
44

45
    case KStandardItemListView::CompactLayout:
46
        calculateCompactLayoutItemSizeHints(logicalHeightHints, logicalWidthHint, view);
47
        break;
48

49
    case KStandardItemListView::DetailsLayout:
50
        calculateDetailsLayoutItemSizeHints(logicalHeightHints, logicalWidthHint, view);
51
        break;
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68

    default:
        Q_ASSERT(false);
        break;
    }
}

qreal KStandardItemListWidgetInformant::preferredRoleColumnWidth(const QByteArray& role,
                                                                 int index,
                                                                 const KItemListView* view) const
{
    const QHash<QByteArray, QVariant> values = view->model()->data(index);
    const KItemListStyleOption& option = view->styleOption();

    const QString text = roleText(role, values);
    qreal width = KStandardItemListWidget::columnPadding(option);

69
70
71
    const QFontMetrics& normalFontMetrics = option.fontMetrics;
    const QFontMetrics linkFontMetrics(customizedFontForLinks(option.font));

72
73
74
    if (role == "rating") {
        width += KStandardItemListWidget::preferredRatingSize(option).width();
    } else {
75
76
77
        // If current item is a link, we use the customized link font metrics instead of the normal font metrics.
        const QFontMetrics& fontMetrics = itemIsLink(index, view) ? linkFontMetrics : normalFontMetrics;

78
        width += fontMetrics.horizontalAdvance(text);
79
80

        if (role == "text") {
81
82
83
            if (view->supportsItemExpanding()) {
                // Increase the width by the expansion-toggle and the current expansion level
                const int expandedParentsCount = values.value("expandedParentsCount", 0).toInt();
84
                const qreal height = option.padding * 2 + qMax(option.iconSize, fontMetrics.height());
85
86
                width += (expandedParentsCount + 1) * height;
            }
87
88
89
90
91
92
93
94
95

            // Increase the width by the required space for the icon
            width += option.padding * 2 + option.iconSize;
        }
    }

    return width;
}

96
97
98
99
100
QString KStandardItemListWidgetInformant::itemText(int index, const KItemListView* view) const
{
    return view->model()->data(index).value("text").toString();
}

101
102
bool KStandardItemListWidgetInformant::itemIsLink(int index, const KItemListView* view) const
{
103
104
    Q_UNUSED(index)
    Q_UNUSED(view)
105
106
107
    return false;
}

108
109
110
111
112
113
114
115
116
117
QString KStandardItemListWidgetInformant::roleText(const QByteArray& role,
                                                   const QHash<QByteArray, QVariant>& values) const
{
    if (role == "rating") {
        // Always use an empty text, as the rating is shown by the image m_rating.
        return QString();
    }
    return values.value(role).toString();
}

118
119
120
121
122
QFont KStandardItemListWidgetInformant::customizedFontForLinks(const QFont& baseFont) const
{
    return baseFont;
}

123
void KStandardItemListWidgetInformant::calculateIconsLayoutItemSizeHints(QVector<qreal>& logicalHeightHints, qreal& logicalWidthHint, const KItemListView* view) const
124
125
{
    const KItemListStyleOption& option = view->styleOption();
126
    const QFont& normalFont = option.font;
127
128
129
130
131
132
133
    const int additionalRolesCount = qMax(view->visibleRoles().count() - 1, 0);

    const qreal itemWidth = view->itemSize().width();
    const qreal maxWidth = itemWidth - 2 * option.padding;
    const qreal additionalRolesSpacing = additionalRolesCount * option.fontMetrics.lineSpacing();
    const qreal spacingAndIconHeight = option.iconSize + option.padding * 3;

134
135
    const QFont linkFont = customizedFontForLinks(normalFont);

136
137
138
    QTextOption textOption(Qt::AlignHCenter);
    textOption.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);

139
140
    for (int index = 0; index < logicalHeightHints.count(); ++index) {
        if (logicalHeightHints.at(index) > 0.0) {
141
142
143
            continue;
        }

144
145
146
        // If the current item is a link, we use the customized link font instead of the normal font.
        const QFont& font = itemIsLink(index, view) ? linkFont : normalFont;

147
148
149
150
151
152
153
154
        const QString& text = KStringHandler::preProcessWrap(itemText(index, view));

        // Calculate the number of lines required for wrapping the name
        qreal textHeight = 0;
        QTextLayout layout(text, font);
        layout.setTextOption(textOption);
        layout.beginLayout();
        QTextLine line;
155
        int lineCount = 0;
156
157
158
159
        while ((line = layout.createLine()).isValid()) {
            line.setLineWidth(maxWidth);
            line.naturalTextWidth();
            textHeight += line.height();
160
161
162
163
164

            ++lineCount;
            if (lineCount == option.maxTextLines) {
                break;
            }
165
166
167
168
169
170
        }
        layout.endLayout();

        // Add one line for each additional information
        textHeight += additionalRolesSpacing;

171
        logicalHeightHints[index] = textHeight + spacingAndIconHeight;
172
    }
173
174

    logicalWidthHint = itemWidth;
175
176
}

177
void KStandardItemListWidgetInformant::calculateCompactLayoutItemSizeHints(QVector<qreal>& logicalHeightHints, qreal& logicalWidthHint, const KItemListView* view) const
178
179
{
    const KItemListStyleOption& option = view->styleOption();
180
    const QFontMetrics& normalFontMetrics = option.fontMetrics;
181
182
183
184
    const int additionalRolesCount = qMax(view->visibleRoles().count() - 1, 0);

    const QList<QByteArray>& visibleRoles = view->visibleRoles();
    const bool showOnlyTextRole = (visibleRoles.count() == 1) && (visibleRoles.first() == "text");
185
    const qreal maxWidth = option.maxTextWidth;
186
    const qreal paddingAndIconWidth = option.padding * 4 + option.iconSize;
187
188
189
    const qreal height = option.padding * 2 + qMax(option.iconSize, (1 + additionalRolesCount) * normalFontMetrics.lineSpacing());

    const QFontMetrics linkFontMetrics(customizedFontForLinks(option.font));
190

191
192
    for (int index = 0; index < logicalHeightHints.count(); ++index) {
        if (logicalHeightHints.at(index) > 0.0) {
193
194
195
            continue;
        }

196
197
198
        // If the current item is a link, we use the customized link font metrics instead of the normal font metrics.
        const QFontMetrics& fontMetrics = itemIsLink(index, view) ? linkFontMetrics : normalFontMetrics;

199
200
201
202
203
        // For each row exactly one role is shown. Calculate the maximum required width that is necessary
        // to show all roles without horizontal clipping.
        qreal maximumRequiredWidth = 0.0;

        if (showOnlyTextRole) {
204
            maximumRequiredWidth = fontMetrics.horizontalAdvance(itemText(index, view));
205
206
        } else {
            const QHash<QByteArray, QVariant>& values = view->model()->data(index);
Alexander Lohnau's avatar
Alexander Lohnau committed
207
            for (const QByteArray& role : visibleRoles) {
208
                const QString& text = roleText(role, values);
209
                const qreal requiredWidth = fontMetrics.horizontalAdvance(text);
210
211
212
213
214
215
216
217
218
                maximumRequiredWidth = qMax(maximumRequiredWidth, requiredWidth);
            }
        }

        qreal width = paddingAndIconWidth + maximumRequiredWidth;
        if (maxWidth > 0 && width > maxWidth) {
            width = maxWidth;
        }

219
        logicalHeightHints[index] = width;
220
    }
221
222

    logicalWidthHint = height;
223
224
}

225
void KStandardItemListWidgetInformant::calculateDetailsLayoutItemSizeHints(QVector<qreal>& logicalHeightHints, qreal& logicalWidthHint, const KItemListView* view) const
226
227
228
{
    const KItemListStyleOption& option = view->styleOption();
    const qreal height = option.padding * 2 + qMax(option.iconSize, option.fontMetrics.height());
229
230
    logicalHeightHints.fill(height);
    logicalWidthHint = -1.0;
231
232
}

233
234
KStandardItemListWidget::KStandardItemListWidget(KItemListWidgetInformant* informant, QGraphicsItem* parent) :
    KItemListWidget(informant, parent),
235
    m_textInfo(),
236
237
    m_isCut(false),
    m_isHidden(false),
238
239
    m_customizedFont(),
    m_customizedFontMetrics(m_customizedFont),
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
    m_isExpandable(false),
    m_supportsItemExpanding(false),
    m_dirtyLayout(true),
    m_dirtyContent(true),
    m_dirtyContentRoles(),
    m_layout(IconsLayout),
    m_pixmapPos(),
    m_pixmap(),
    m_scaledPixmapSize(),
    m_iconRect(),
    m_hoverPixmap(),
    m_textRect(),
    m_sortedVisibleRoles(),
    m_expansionArea(),
    m_customTextColor(),
    m_additionalInfoTextColor(),
    m_overlay(),
    m_rating(),
Kevin Funk's avatar
Kevin Funk committed
258
259
    m_roleEditor(nullptr),
    m_oldRoleEditor(nullptr)
260
261
262
263
264
265
266
267
{
}

KStandardItemListWidget::~KStandardItemListWidget()
{
    qDeleteAll(m_textInfo);
    m_textInfo.clear();

268
269
270
271
272
273
274
    if (m_roleEditor) {
        m_roleEditor->deleteLater();
    }

    if (m_oldRoleEditor) {
        m_oldRoleEditor->deleteLater();
    }
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
}

void KStandardItemListWidget::setLayout(Layout layout)
{
    if (m_layout != layout) {
        m_layout = layout;
        m_dirtyLayout = true;
        updateAdditionalInfoTextColor();
        update();
    }
}

KStandardItemListWidget::Layout KStandardItemListWidget::layout() const
{
    return m_layout;
}

void KStandardItemListWidget::setSupportsItemExpanding(bool supportsItemExpanding)
{
    if (m_supportsItemExpanding != supportsItemExpanding) {
        m_supportsItemExpanding = supportsItemExpanding;
        m_dirtyLayout = true;
        update();
    }
}

bool KStandardItemListWidget::supportsItemExpanding() const
{
    return m_supportsItemExpanding;
}

void KStandardItemListWidget::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget)
{
    const_cast<KStandardItemListWidget*>(this)->triggerCacheRefreshing();

    KItemListWidget::paint(painter, option, widget);

    if (!m_expansionArea.isEmpty()) {
        drawSiblingsInformation(painter);
    }

    const KItemListStyleOption& itemListStyleOption = styleOption();
317
    if (isHovered() && !m_pixmap.isNull()) {
318
        if (hoverOpacity() < 1.0) {
319
320
321
322
323
324
325
326
            /*
             * Linear interpolation between m_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
             * https://git.reviewboard.kde.org/r/109614/
             */
            // Paint pixmap1 so that pixmap1 = m_pixmap * (1.0 - hoverOpacity())
327
            QPixmap pixmap1(m_pixmap.size());
328
            pixmap1.setDevicePixelRatio(m_pixmap.devicePixelRatio());
329
330
331
332
            pixmap1.fill(Qt::transparent);
            {
                QPainter p(&pixmap1);
                p.setOpacity(1.0 - hoverOpacity());
333
                p.drawPixmap(0, 0, m_pixmap);
334
335
336
            }

            // Paint pixmap2 so that pixmap2 = m_hoverPixmap * hoverOpacity()
337
            QPixmap pixmap2(pixmap1.size());
338
            pixmap2.setDevicePixelRatio(pixmap1.devicePixelRatio());
339
340
341
342
            pixmap2.fill(Qt::transparent);
            {
                QPainter p(&pixmap2);
                p.setOpacity(hoverOpacity());
343
                p.drawPixmap(0, 0, m_hoverPixmap);
344
            }
345

346
347
348
349
350
351
352
353
354
355
            // Paint pixmap2 on pixmap1 using CompositionMode_Plus
            // Now pixmap1 = pixmap2 + m_pixmap * (1.0 - hoverOpacity())
            //             = m_hoverPixmap * hoverOpacity() + m_pixmap * (1.0 - hoverOpacity())
            {
                QPainter p(&pixmap1);
                p.setCompositionMode(QPainter::CompositionMode_Plus);
                p.drawPixmap(0, 0, pixmap2);
            }

            // Finally paint pixmap1 on the widget
356
            drawPixmap(painter, pixmap1);
357
358
359
        } else {
            drawPixmap(painter, m_hoverPixmap);
        }
360
    } else if (!m_pixmap.isNull()) {
361
362
363
        drawPixmap(painter, m_pixmap);
    }

364
    painter->setFont(m_customizedFont);
365
    painter->setPen(textColor());
366
    const TextInfo* textInfo = m_textInfo.value("text");
367
368
369
370
371
372
373
374
375
376

    if (!textInfo) {
        // It seems that we can end up here even if m_textInfo does not contain
        // the key "text", see bug 306167. According to triggerCacheRefreshing(),
        // this can only happen if the index is negative. This can happen when
        // the item is about to be removed, see KItemListView::slotItemsRemoved().
        // TODO: try to reproduce the crash and find a better fix.
        return;
    }

377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
    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);
        }
    }

    painter->setPen(m_additionalInfoTextColor);
393
    painter->setFont(m_customizedFont);
394
395
396
397
398
399
400
401
402
403
404

    for (int i = 1; i < m_sortedVisibleRoles.count(); ++i) {
        const TextInfo* textInfo = m_textInfo.value(m_sortedVisibleRoles[i]);
        painter->drawStaticText(textInfo->pos, textInfo->staticText);
    }

    if (!m_rating.isNull()) {
        const TextInfo* ratingTextInfo = m_textInfo.value("rating");
        QPointF pos = ratingTextInfo->pos;
        const Qt::Alignment align = ratingTextInfo->staticText.textOption().alignment();
        if (align & Qt::AlignHCenter) {
405
            pos.rx() += (size().width() - m_rating.width()) / 2 - 2;
406
407
408
409
410
411
412
413
        }
        painter->drawPixmap(pos, m_rating);
    }

    if (clipAdditionalInfoBounds) {
        painter->restore();
    }

414
#ifdef KSTANDARDITEMLISTWIDGET_DEBUG
415
416
417
418
    painter->setBrush(Qt::NoBrush);
    painter->setPen(Qt::green);
    painter->drawRect(m_iconRect);

419
420
421
    painter->setPen(Qt::blue);
    painter->drawRect(m_textRect);

422
    painter->setPen(Qt::red);
423
    painter->drawText(QPointF(0, m_customizedFontMetrics.height()), QString::number(index()));
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
    painter->drawRect(rect());
#endif
}

QRectF KStandardItemListWidget::iconRect() const
{
    const_cast<KStandardItemListWidget*>(this)->triggerCacheRefreshing();
    return m_iconRect;
}

QRectF KStandardItemListWidget::textRect() const
{
    const_cast<KStandardItemListWidget*>(this)->triggerCacheRefreshing();
    return m_textRect;
}

QRectF KStandardItemListWidget::textFocusRect() const
{
    // In the compact- and details-layout a larger textRect() is returned to be aligned
    // with the iconRect(). This is useful to have a larger selection/hover-area
    // when having a quite large icon size but only one line of text. Still the
    // focus rectangle should be shown as narrow as possible around the text.

    const_cast<KStandardItemListWidget*>(this)->triggerCacheRefreshing();

    switch (m_layout) {
    case CompactLayout: {
        QRectF rect = m_textRect;
        const TextInfo* topText    = m_textInfo.value(m_sortedVisibleRoles.first());
        const TextInfo* bottomText = m_textInfo.value(m_sortedVisibleRoles.last());
        rect.setTop(topText->pos.y());
        rect.setBottom(bottomText->pos.y() + bottomText->staticText.size().height());
        return rect;
    }

    case DetailsLayout: {
        QRectF rect = m_textRect;
461
        const TextInfo* textInfo = m_textInfo.value(m_sortedVisibleRoles.first());
462
463
        rect.setTop(textInfo->pos.y());
        rect.setBottom(textInfo->pos.y() + textInfo->staticText.size().height());
464
465
466
467

        const KItemListStyleOption& option = styleOption();
        if (option.extendedSelectionRegion) {
            const QString text = textInfo->staticText.text();
468
            rect.setWidth(m_customizedFontMetrics.horizontalAdvance(text) + 2 * option.padding);
469
470
        }

471
472
473
474
475
476
477
478
479
480
        return rect;
    }

    default:
        break;
    }

    return m_textRect;
}

481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
QRectF KStandardItemListWidget::selectionRect() const
{
    const_cast<KStandardItemListWidget*>(this)->triggerCacheRefreshing();

    switch (m_layout) {
    case IconsLayout:
        return m_textRect;

    case CompactLayout:
    case DetailsLayout: {
        const int padding = styleOption().padding;
        QRectF adjustedIconRect = iconRect().adjusted(-padding, -padding, padding, padding);
        return adjustedIconRect | m_textRect;
    }

    default:
        Q_ASSERT(false);
        break;
    }

    return m_textRect;
}

504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
QRectF KStandardItemListWidget::expansionToggleRect() const
{
    const_cast<KStandardItemListWidget*>(this)->triggerCacheRefreshing();
    return m_isExpandable ? m_expansionArea : QRectF();
}

QRectF KStandardItemListWidget::selectionToggleRect() const
{
    const_cast<KStandardItemListWidget*>(this)->triggerCacheRefreshing();

    const int iconHeight = styleOption().iconSize;

    int toggleSize = KIconLoader::SizeSmall;
    if (iconHeight >= KIconLoader::SizeEnormous) {
        toggleSize = KIconLoader::SizeMedium;
    } else if (iconHeight >= KIconLoader::SizeLarge) {
        toggleSize = KIconLoader::SizeSmallMedium;
    }

    QPointF pos = iconRect().topLeft();

    // If the selection toggle has a very small distance to the
    // widget borders, the size of the selection toggle will get
    // increased to prevent an accidental clicking of the item
    // when trying to hit the toggle.
    const int widgetHeight = size().height();
    const int widgetWidth = size().width();
    const int minMargin = 2;

    if (toggleSize + minMargin * 2 >= widgetHeight) {
        pos.rx() -= (widgetHeight - toggleSize) / 2;
        toggleSize = widgetHeight;
        pos.setY(0);
    }
    if (toggleSize + minMargin * 2 >= widgetWidth) {
        pos.ry() -= (widgetWidth - toggleSize) / 2;
        toggleSize = widgetWidth;
        pos.setX(0);
    }

    return QRectF(pos, QSizeF(toggleSize, toggleSize));
}

547
548
549
550
QPixmap KStandardItemListWidget::createDragPixmap(const QStyleOptionGraphicsItem* option,
                                                  QWidget* widget)
{
    QPixmap pixmap = KItemListWidget::createDragPixmap(option, widget);
Peter Penz's avatar
Peter Penz committed
551
    if (m_layout != DetailsLayout) {
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
        return pixmap;
    }

    // Only return the content of the text-column as pixmap
    const int leftClip = m_pixmapPos.x();

    const TextInfo* textInfo = m_textInfo.value("text");
    const int rightClip = textInfo->pos.x() +
                          textInfo->staticText.size().width() +
                          2 * styleOption().padding;

    QPixmap clippedPixmap(rightClip - leftClip + 1, pixmap.height());
    clippedPixmap.fill(Qt::transparent);

    QPainter painter(&clippedPixmap);
    painter.drawPixmap(-leftClip, 0, pixmap);

    return clippedPixmap;
}


573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
KItemListWidgetInformant* KStandardItemListWidget::createInformant()
{
    return new KStandardItemListWidgetInformant();
}

void KStandardItemListWidget::invalidateCache()
{
    m_dirtyLayout = true;
    m_dirtyContent = true;
}

void KStandardItemListWidget::refreshCache()
{
}

bool KStandardItemListWidget::isRoleRightAligned(const QByteArray& role) const
{
590
    Q_UNUSED(role)
591
592
593
    return false;
}

594
595
596
597
598
bool KStandardItemListWidget::isHidden() const
{
    return false;
}

599
600
601
602
603
QFont KStandardItemListWidget::customizedFont(const QFont& baseFont) const
{
    return baseFont;
}

604
QPalette::ColorRole KStandardItemListWidget::normalTextColorRole() const
605
606
607
608
{
    return QPalette::Text;
}

609
610
611
612
613
614
615
616
617
618
619
void KStandardItemListWidget::setTextColor(const QColor& color)
{
    if (color != m_customTextColor) {
        m_customTextColor = color;
        updateAdditionalInfoTextColor();
        update();
    }
}

QColor KStandardItemListWidget::textColor() const
{
620
621
622
623
624
625
    if (!isSelected()) {
        if (m_isHidden) {
            return m_additionalInfoTextColor;
        } else if (m_customTextColor.isValid()) {
            return m_customTextColor;
        }
626
627
628
    }

    const QPalette::ColorGroup group = isActiveWindow() ? QPalette::Active : QPalette::Inactive;
629
630
    const QPalette::ColorRole role = isSelected() ? QPalette::HighlightedText : normalTextColorRole();
    return styleOption().palette.color(group, role);
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
}

void KStandardItemListWidget::setOverlay(const QPixmap& overlay)
{
    m_overlay = overlay;
    m_dirtyContent = true;
    update();
}

QPixmap KStandardItemListWidget::overlay() const
{
    return m_overlay;
}


QString KStandardItemListWidget::roleText(const QByteArray& role,
                                          const QHash<QByteArray, QVariant>& values) const
{
    return static_cast<const KStandardItemListWidgetInformant*>(informant())->roleText(role, values);
}

void KStandardItemListWidget::dataChanged(const QHash<QByteArray, QVariant>& current,
                                          const QSet<QByteArray>& roles)
{
655
    Q_UNUSED(current)
656
657
658
659
660

    m_dirtyContent = true;

    QSet<QByteArray> dirtyRoles;
    if (roles.isEmpty()) {
661
662
        const auto visibleRoles = this->visibleRoles();
        dirtyRoles = QSet<QByteArray>(visibleRoles.constBegin(), visibleRoles.constEnd());
663
664
665
666
    } else {
        dirtyRoles = roles;
    }

667
668
669
    // The URL might have changed (i.e., if the sort order of the items has
    // been changed). Therefore, the "is cut" state must be updated.
    KFileItemClipboard* clipboard = KFileItemClipboard::instance();
670
    const QUrl itemUrl = data().value("url").toUrl();
671
672
    m_isCut = clipboard->isCut(itemUrl);

673
674
675
676
677
    // The icon-state might depend from other roles and hence is
    // marked as dirty whenever a role has been changed
    dirtyRoles.insert("iconPixmap");
    dirtyRoles.insert("iconName");

678
679
680
681
682
683
684
685
686
687
    QSetIterator<QByteArray> it(dirtyRoles);
    while (it.hasNext()) {
        const QByteArray& role = it.next();
        m_dirtyContentRoles.insert(role);
    }
}

void KStandardItemListWidget::visibleRolesChanged(const QList<QByteArray>& current,
                                              const QList<QByteArray>& previous)
{
688
    Q_UNUSED(previous)
689
690
691
692
693
694
695
696
    m_sortedVisibleRoles = current;
    m_dirtyLayout = true;
}

void KStandardItemListWidget::columnWidthChanged(const QByteArray& role,
                                             qreal current,
                                             qreal previous)
{
697
698
699
    Q_UNUSED(role)
    Q_UNUSED(current)
    Q_UNUSED(previous)
700
701
702
703
704
705
    m_dirtyLayout = true;
}

void KStandardItemListWidget::styleOptionChanged(const KItemListStyleOption& current,
                                             const KItemListStyleOption& previous)
{
706
707
    Q_UNUSED(current)
    Q_UNUSED(previous)
708
709
710
711
712
713
    updateAdditionalInfoTextColor();
    m_dirtyLayout = true;
}

void KStandardItemListWidget::hoveredChanged(bool hovered)
{
714
    Q_UNUSED(hovered)
715
716
717
718
719
    m_dirtyLayout = true;
}

void KStandardItemListWidget::selectedChanged(bool selected)
{
720
    Q_UNUSED(selected)
721
    updateAdditionalInfoTextColor();
722
    m_dirtyContent = true;
723
724
725
726
}

void KStandardItemListWidget::siblingsInformationChanged(const QBitArray& current, const QBitArray& previous)
{
727
728
    Q_UNUSED(current)
    Q_UNUSED(previous)
729
730
731
    m_dirtyLayout = true;
}

732
733
734
735
736
int KStandardItemListWidget::selectionLength(const QString& text) const
{
    return text.length();
}

737
738
void KStandardItemListWidget::editedRoleChanged(const QByteArray& current, const QByteArray& previous)
{
739
    Q_UNUSED(previous)
740

741
742
    QGraphicsView* parent = scene()->views()[0];
    if (current.isEmpty() || !parent || current != "text") {
743
        if (m_roleEditor) {
Alexander Lohnau's avatar
Alexander Lohnau committed
744
            Q_EMIT roleEditingCanceled(index(), current, data().value(current));
745

746
747
748
749
            disconnect(m_roleEditor, &KItemListRoleEditor::roleEditingCanceled,
                       this, &KStandardItemListWidget::slotRoleEditingCanceled);
            disconnect(m_roleEditor, &KItemListRoleEditor::roleEditingFinished,
                       this, &KStandardItemListWidget::slotRoleEditingFinished);
750
751
752
753

            if (m_oldRoleEditor) {
                m_oldRoleEditor->deleteLater();
            }
754
755
            m_oldRoleEditor = m_roleEditor;
            m_roleEditor->hide();
Kevin Funk's avatar
Kevin Funk committed
756
            m_roleEditor = nullptr;
757
758
759
760
761
762
763
764
765
766
        }
        return;
    }

    Q_ASSERT(!m_roleEditor);

    const TextInfo* textInfo = m_textInfo.value("text");

    m_roleEditor = new KItemListRoleEditor(parent);
    m_roleEditor->setRole(current);
Peter Penz's avatar
Peter Penz committed
767
    m_roleEditor->setFont(styleOption().font);
768
769
770
771
772
773
774

    const QString text = data().value(current).toString();
    m_roleEditor->setPlainText(text);

    QTextOption textOption = textInfo->staticText.textOption();
    m_roleEditor->document()->setDefaultTextOption(textOption);

775
    const int textSelectionLength = selectionLength(text);
776

777
    if (textSelectionLength > 0) {
778
779
        QTextCursor cursor = m_roleEditor->textCursor();
        cursor.movePosition(QTextCursor::StartOfBlock);
780
        cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, textSelectionLength);
781
782
783
        m_roleEditor->setTextCursor(cursor);
    }

784
785
786
787
    connect(m_roleEditor, &KItemListRoleEditor::roleEditingCanceled,
            this, &KStandardItemListWidget::slotRoleEditingCanceled);
    connect(m_roleEditor, &KItemListRoleEditor::roleEditingFinished,
            this, &KStandardItemListWidget::slotRoleEditingFinished);
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820

    // Adjust the geometry of the editor
    QRectF rect = roleEditingRect(current);
    const int frameWidth = m_roleEditor->frameWidth();
    rect.adjust(-frameWidth, -frameWidth, frameWidth, frameWidth);
    rect.translate(pos());
    if (rect.right() > parent->width()) {
        rect.setWidth(parent->width() - rect.left());
    }
    m_roleEditor->setGeometry(rect.toRect());
    m_roleEditor->show();
    m_roleEditor->setFocus();
}

void KStandardItemListWidget::resizeEvent(QGraphicsSceneResizeEvent* event)
{
    if (m_roleEditor) {
        setEditedRole(QByteArray());
        Q_ASSERT(!m_roleEditor);
    }

    KItemListWidget::resizeEvent(event);

    m_dirtyLayout = true;
}

void KStandardItemListWidget::showEvent(QShowEvent* event)
{
    KItemListWidget::showEvent(event);

    // Listen to changes of the clipboard to mark the item as cut/uncut
    KFileItemClipboard* clipboard = KFileItemClipboard::instance();

821
    const QUrl itemUrl = data().value("url").toUrl();
822
823
    m_isCut = clipboard->isCut(itemUrl);

824
825
    connect(clipboard, &KFileItemClipboard::cutItemsChanged,
            this, &KStandardItemListWidget::slotCutItemsChanged);
826
827
828
829
}

void KStandardItemListWidget::hideEvent(QHideEvent* event)
{
830
831
    disconnect(KFileItemClipboard::instance(), &KFileItemClipboard::cutItemsChanged,
               this, &KStandardItemListWidget::slotCutItemsChanged);
832
833
834
835

    KItemListWidget::hideEvent(event);
}

836
837
bool KStandardItemListWidget::event(QEvent *event)
{
838
839
    if (event->type() == QEvent::WindowDeactivate || event->type() == QEvent::WindowActivate
            || event->type() == QEvent::PaletteChange) {
840
841
842
843
844
845
        m_dirtyContent = true;
    }

    return KItemListWidget::event(event);
}

846
847
848
849
850
851
852
void KStandardItemListWidget::finishRoleEditing()
{
    if (!editedRole().isEmpty() && m_roleEditor) {
        slotRoleEditingFinished(editedRole(), KIO::encodeFileName(m_roleEditor->toPlainText()));
    }
}

853
854
void KStandardItemListWidget::slotCutItemsChanged()
{
855
    const QUrl itemUrl = data().value("url").toUrl();
856
857
858
859
860
861
862
863
864
    const bool isCut = KFileItemClipboard::instance()->isCut(itemUrl);
    if (m_isCut != isCut) {
        m_isCut = isCut;
        m_pixmap = QPixmap();
        m_dirtyContent = true;
        update();
    }
}

865
void KStandardItemListWidget::slotRoleEditingCanceled(const QByteArray& role,
866
                                                      const QVariant& value)
867
{
868
    closeRoleEditor();
Alexander Lohnau's avatar
Alexander Lohnau committed
869
    Q_EMIT roleEditingCanceled(index(), role, value);
870
871
872
    setEditedRole(QByteArray());
}

873
void KStandardItemListWidget::slotRoleEditingFinished(const QByteArray& role,
874
                                                      const QVariant& value)
875
{
876
    closeRoleEditor();
Alexander Lohnau's avatar
Alexander Lohnau committed
877
    Q_EMIT roleEditingFinished(index(), role, value);
878
879
880
881
882
883
884
885
886
887
888
889
890
    setEditedRole(QByteArray());
}

void KStandardItemListWidget::triggerCacheRefreshing()
{
    if ((!m_dirtyContent && !m_dirtyLayout) || index() < 0) {
        return;
    }

    refreshCache();

    const QHash<QByteArray, QVariant> values = data();
    m_isExpandable = m_supportsItemExpanding && values["isExpandable"].toBool();
891
    m_isHidden = isHidden();
892
893
    m_customizedFont = customizedFont(styleOption().font);
    m_customizedFontMetrics = QFontMetrics(m_customizedFont);
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909

    updateExpansionArea();
    updateTextsCache();
    updatePixmapCache();

    m_dirtyLayout = false;
    m_dirtyContent = false;
    m_dirtyContentRoles.clear();
}

void KStandardItemListWidget::updateExpansionArea()
{
    if (m_supportsItemExpanding) {
        const QHash<QByteArray, QVariant> values = data();
        const int expandedParentsCount = values.value("expandedParentsCount", 0).toInt();
        if (expandedParentsCount >= 0) {
910
            const KItemListStyleOption& option = styleOption();
911
            const qreal widgetHeight = size().height();
912
            const qreal inc = (widgetHeight - option.iconSize) / 2;
913
914
            const qreal x = expandedParentsCount * widgetHeight + inc;
            const qreal y = inc;
915
            m_expansionArea = QRectF(x, y, option.iconSize, option.iconSize);
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
            return;
        }
    }

    m_expansionArea = QRectF();
}

void KStandardItemListWidget::updatePixmapCache()
{
    // Precondition: Requires already updated m_textPos values to calculate
    // the remaining height when the alignment is vertical.

    const QSizeF widgetSize = size();
    const bool iconOnTop = (m_layout == IconsLayout);
    const KItemListStyleOption& option = styleOption();
    const qreal padding = option.padding;

    const int maxIconWidth = iconOnTop ? widgetSize.width() - 2 * padding : option.iconSize;
    const int maxIconHeight = option.iconSize;

    const QHash<QByteArray, QVariant> values = data();

    bool updatePixmap = (m_pixmap.width() != maxIconWidth || m_pixmap.height() != maxIconHeight);
    if (!updatePixmap && m_dirtyContent) {
        updatePixmap = m_dirtyContentRoles.isEmpty()
                       || m_dirtyContentRoles.contains("iconPixmap")
                       || m_dirtyContentRoles.contains("iconName")
                       || m_dirtyContentRoles.contains("iconOverlays");
    }

    if (updatePixmap) {
        m_pixmap = values["iconPixmap"].value<QPixmap>();
        if (m_pixmap.isNull()) {
            // Use the icon that fits to the MIME-type
            QString iconName = values["iconName"].toString();
            if (iconName.isEmpty()) {
                // The icon-name has not been not resolved by KFileItemModelRolesUpdater,
                // use a generic icon as fallback
954
                iconName = QStringLiteral("unknown");
955
            }
956
            const QStringList overlays = values["iconOverlays"].toStringList();
957
            m_pixmap = pixmapForIcon(iconName, overlays, maxIconHeight, m_layout != IconsLayout && isActiveWindow() && isSelected() ? QIcon::Selected : QIcon::Normal);
958

David Edmundson's avatar
David Edmundson committed
959
        } else if (m_pixmap.width() / m_pixmap.devicePixelRatio() != maxIconWidth || m_pixmap.height() / m_pixmap.devicePixelRatio() != maxIconHeight) {
960
961
            // A custom pixmap has been applied. Assure that the pixmap
            // is scaled to the maximum available size.
962
            KPixmapModifier::scale(m_pixmap, QSize(maxIconWidth, maxIconHeight) * qApp->devicePixelRatio());
963
964
        }

965
966
967
968
969
        if (m_pixmap.isNull()) {
            m_hoverPixmap = QPixmap();
            return;
        }

970
        if (m_isCut) {
971
972
            KIconEffect* effect = KIconLoader::global()->iconEffect();
            m_pixmap = effect->apply(m_pixmap, KIconLoader::Desktop, KIconLoader::DisabledState);
973
974
975
        }

        if (m_isHidden) {
976
977
978
            KIconEffect::semiTransparent(m_pixmap);
        }

979
        if (m_layout == IconsLayout && isSelected()) {
980
981
            const QColor color = palette().brush(QPalette::Normal, QPalette::Highlight).color();
            QImage image = m_pixmap.toImage();
982
983
984
985
            if (image.isNull()) {
                m_hoverPixmap = QPixmap();
                return;
            }
986
            KIconEffect::colorize(image, color, 0.8f);
987
            m_pixmap = QPixmap::fromImage(image);
988
989
990
991
992
        }
    }

    if (!m_overlay.isNull()) {
        QPainter painter(&m_pixmap);
993
        painter.drawPixmap(0, (m_pixmap.height() - m_overlay.height()) / m_pixmap.devicePixelRatio(), m_overlay);
994
995
996
997
998
999
1000
    }

    int scaledIconSize = 0;
    if (iconOnTop) {
        const TextInfo* textInfo = m_textInfo.value("text");
        scaledIconSize = static_cast<int>(textInfo->pos.y() - 2 * padding);
    } else {