File indexing completed on 2024-04-21 03:55:21

0001 /*
0002     This file is part of the KDE project
0003     SPDX-FileCopyrightText: 2007 Kevin Ottens <ervin@kde.org>
0004     SPDX-FileCopyrightText: 2008 Rafael Fernández López <ereslibre@kde.org>
0005     SPDX-FileCopyrightText: 2022 Kai Uwe Broulik <kde@broulik.de>
0006     SPDX-FileCopyrightText: 2023 Harald Sitter <sitter@kde.org>
0007 
0008     SPDX-License-Identifier: LGPL-2.0-only
0009 */
0010 
0011 #include "kfileplacesview.h"
0012 #include "kfileplacesmodel_p.h"
0013 #include "kfileplacesview_p.h"
0014 
0015 #include <QAbstractItemDelegate>
0016 #include <QActionGroup>
0017 #include <QApplication>
0018 #include <QDir>
0019 #include <QKeyEvent>
0020 #include <QMenu>
0021 #include <QMetaMethod>
0022 #include <QMimeData>
0023 #include <QPainter>
0024 #include <QPointer>
0025 #include <QScrollBar>
0026 #include <QScroller>
0027 #include <QTimeLine>
0028 #include <QTimer>
0029 #include <QToolTip>
0030 #include <QVariantAnimation>
0031 #include <QWindow>
0032 #include <kio/deleteortrashjob.h>
0033 
0034 #include <KColorScheme>
0035 #include <KColorUtils>
0036 #include <KConfig>
0037 #include <KConfigGroup>
0038 #include <KJob>
0039 #include <KLocalizedString>
0040 #include <KSharedConfig>
0041 #include <defaults-kfile.h> // ConfigGroup, PlacesIconsAutoresize, PlacesIconsStaticSize
0042 #include <kdirnotify.h>
0043 #include <kio/filesystemfreespacejob.h>
0044 #include <kmountpoint.h>
0045 #include <kpropertiesdialog.h>
0046 #include <solid/opticaldisc.h>
0047 #include <solid/opticaldrive.h>
0048 #include <solid/storageaccess.h>
0049 #include <solid/storagedrive.h>
0050 #include <solid/storagevolume.h>
0051 
0052 #include <chrono>
0053 #include <cmath>
0054 
0055 #include "kfileplaceeditdialog.h"
0056 #include "kfileplacesmodel.h"
0057 
0058 using namespace std::chrono_literals;
0059 
0060 static constexpr int s_lateralMargin = 4;
0061 static constexpr int s_capacitybarHeight = 6;
0062 static constexpr auto s_pollFreeSpaceInterval = 1min;
0063 
0064 KFilePlacesViewDelegate::KFilePlacesViewDelegate(KFilePlacesView *parent)
0065     : QAbstractItemDelegate(parent)
0066     , m_view(parent)
0067     , m_iconSize(48)
0068     , m_appearingHeightScale(1.0)
0069     , m_appearingOpacity(0.0)
0070     , m_disappearingHeightScale(1.0)
0071     , m_disappearingOpacity(0.0)
0072     , m_showHoverIndication(true)
0073     , m_dragStarted(false)
0074 {
0075     m_pollFreeSpace.setInterval(s_pollFreeSpaceInterval);
0076     connect(&m_pollFreeSpace, &QTimer::timeout, this, QOverload<>::of(&KFilePlacesViewDelegate::checkFreeSpace));
0077 }
0078 
0079 KFilePlacesViewDelegate::~KFilePlacesViewDelegate()
0080 {
0081 }
0082 
0083 QSize KFilePlacesViewDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
0084 {
0085     int height = std::max(m_iconSize, option.fontMetrics.height()) + s_lateralMargin;
0086 
0087     if (m_appearingItems.contains(index)) {
0088         height *= m_appearingHeightScale;
0089     } else if (m_disappearingItems.contains(index)) {
0090         height *= m_disappearingHeightScale;
0091     }
0092 
0093     if (indexIsSectionHeader(index)) {
0094         height += sectionHeaderHeight(index);
0095     }
0096 
0097     return QSize(option.rect.width(), height);
0098 }
0099 
0100 void KFilePlacesViewDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
0101 {
0102     painter->save();
0103 
0104     QStyleOptionViewItem opt = option;
0105 
0106     const KFilePlacesModel *placesModel = static_cast<const KFilePlacesModel *>(index.model());
0107 
0108     // draw header when necessary
0109     if (indexIsSectionHeader(index)) {
0110         // If we are drawing the floating element used by drag/drop, do not draw the header
0111         if (!m_dragStarted) {
0112             drawSectionHeader(painter, opt, index);
0113         }
0114 
0115         // Move the target rect to the actual item rect
0116         const int headerHeight = sectionHeaderHeight(index);
0117         opt.rect.translate(0, headerHeight);
0118         opt.rect.setHeight(opt.rect.height() - headerHeight);
0119     }
0120 
0121     // draw item
0122     if (m_appearingItems.contains(index)) {
0123         painter->setOpacity(m_appearingOpacity);
0124     } else if (m_disappearingItems.contains(index)) {
0125         painter->setOpacity(m_disappearingOpacity);
0126     }
0127 
0128     if (placesModel->isHidden(index)) {
0129         painter->setOpacity(painter->opacity() * 0.6);
0130     }
0131 
0132     if (!m_showHoverIndication) {
0133         opt.state &= ~QStyle::State_MouseOver;
0134     }
0135 
0136     if (opt.state & QStyle::State_MouseOver) {
0137         if (index == m_hoveredHeaderArea) {
0138             opt.state &= ~QStyle::State_MouseOver;
0139         }
0140     }
0141 
0142     // Avoid a solid background for the drag pixmap so the drop indicator
0143     // is more easily seen.
0144     if (m_dragStarted) {
0145         opt.state.setFlag(QStyle::State_MouseOver, true);
0146         opt.state.setFlag(QStyle::State_Active, false);
0147         opt.state.setFlag(QStyle::State_Selected, false);
0148     }
0149 
0150     m_dragStarted = false;
0151 
0152     QApplication::style()->drawPrimitive(QStyle::PE_PanelItemViewItem, &opt, painter);
0153 
0154     const auto accessibility = placesModel->deviceAccessibility(index);
0155     const bool isBusy = (accessibility == KFilePlacesModel::SetupInProgress || accessibility == KFilePlacesModel::TeardownInProgress);
0156 
0157     QIcon actionIcon;
0158     if (isBusy) {
0159         actionIcon = QIcon::fromTheme(QStringLiteral("view-refresh"));
0160     } else if (placesModel->isTeardownOverlayRecommended(index)) {
0161         actionIcon = QIcon::fromTheme(QStringLiteral("media-eject"));
0162     }
0163 
0164     bool isLTR = opt.direction == Qt::LeftToRight;
0165     const int iconAreaWidth = s_lateralMargin + m_iconSize;
0166     const int actionAreaWidth = !actionIcon.isNull() ? s_lateralMargin + actionIconSize() : 0;
0167     QRect rectText((isLTR ? iconAreaWidth : actionAreaWidth) + s_lateralMargin,
0168                    opt.rect.top(),
0169                    opt.rect.width() - iconAreaWidth - actionAreaWidth - 2 * s_lateralMargin,
0170                    opt.rect.height());
0171 
0172     const QPalette activePalette = KIconLoader::global()->customPalette();
0173     const bool changePalette = activePalette != opt.palette;
0174     if (changePalette) {
0175         KIconLoader::global()->setCustomPalette(opt.palette);
0176     }
0177 
0178     const bool selectedAndActive = (opt.state & QStyle::State_Selected) && (opt.state & QStyle::State_Active);
0179     QIcon::Mode mode = selectedAndActive ? QIcon::Selected : QIcon::Normal;
0180     QIcon icon = index.model()->data(index, Qt::DecorationRole).value<QIcon>();
0181     QPixmap pm = icon.pixmap(m_iconSize, m_iconSize, mode);
0182     QPoint point(isLTR ? opt.rect.left() + s_lateralMargin : opt.rect.right() - s_lateralMargin - m_iconSize,
0183                  opt.rect.top() + (opt.rect.height() - m_iconSize) / 2);
0184     painter->drawPixmap(point, pm);
0185 
0186     if (!actionIcon.isNull()) {
0187         const int iconSize = actionIconSize();
0188         QIcon::Mode mode = QIcon::Normal;
0189         if (selectedAndActive) {
0190             mode = QIcon::Selected;
0191         } else if (m_hoveredAction == index) {
0192             mode = QIcon::Active;
0193         }
0194 
0195         const QPixmap pixmap = actionIcon.pixmap(iconSize, iconSize, mode);
0196 
0197         const QRectF rect(isLTR ? opt.rect.right() - actionAreaWidth : opt.rect.left() + s_lateralMargin,
0198                           opt.rect.top() + (opt.rect.height() - iconSize) / 2,
0199                           iconSize,
0200                           iconSize);
0201 
0202         if (isBusy) {
0203             painter->save();
0204             painter->setRenderHint(QPainter::SmoothPixmapTransform);
0205             painter->translate(rect.center());
0206             painter->rotate(m_busyAnimationRotation);
0207             painter->translate(QPointF(-rect.width() / 2.0, -rect.height() / 2.0));
0208             painter->drawPixmap(0, 0, pixmap);
0209             painter->restore();
0210         } else {
0211             painter->drawPixmap(rect.topLeft(), pixmap);
0212         }
0213     }
0214 
0215     if (changePalette) {
0216         if (activePalette == QPalette()) {
0217             KIconLoader::global()->resetPalette();
0218         } else {
0219             KIconLoader::global()->setCustomPalette(activePalette);
0220         }
0221     }
0222 
0223     if (selectedAndActive) {
0224         painter->setPen(opt.palette.highlightedText().color());
0225     } else {
0226         painter->setPen(opt.palette.text().color());
0227     }
0228 
0229     if (placesModel->data(index, KFilePlacesModel::CapacityBarRecommendedRole).toBool()) {
0230         QPersistentModelIndex persistentIndex(index);
0231         const auto info = m_freeSpaceInfo.value(persistentIndex);
0232 
0233         checkFreeSpace(index); // async
0234 
0235         if (info.size > 0) {
0236             const int capacityBarHeight = std::ceil(m_iconSize / 8.0);
0237             const qreal usedSpace = info.used / qreal(info.size);
0238 
0239             // Vertically center text + capacity bar, so move text up a bit
0240             rectText.setTop(opt.rect.top() + (opt.rect.height() - opt.fontMetrics.height() - capacityBarHeight) / 2);
0241             rectText.setHeight(opt.fontMetrics.height());
0242 
0243             const int radius = capacityBarHeight / 2;
0244             QRect capacityBgRect(rectText.x(), rectText.bottom(), rectText.width(), capacityBarHeight);
0245             capacityBgRect.adjust(0.5, 0.5, -0.5, -0.5);
0246             QRect capacityFillRect = capacityBgRect;
0247             capacityFillRect.setWidth(capacityFillRect.width() * usedSpace);
0248 
0249             QPalette::ColorGroup cg = QPalette::Active;
0250             if (!(opt.state & QStyle::State_Enabled)) {
0251                 cg = QPalette::Disabled;
0252             } else if (!m_view->isActiveWindow()) {
0253                 cg = QPalette::Inactive;
0254             }
0255 
0256             // Adapted from Breeze style's progress bar rendering
0257             QColor capacityBgColor(opt.palette.color(QPalette::WindowText));
0258             capacityBgColor.setAlphaF(0.2 * capacityBgColor.alphaF());
0259 
0260             QColor capacityFgColor(selectedAndActive ? opt.palette.color(cg, QPalette::HighlightedText) : opt.palette.color(cg, QPalette::Highlight));
0261             if (usedSpace > 0.95) {
0262                 if (!m_warningCapacityBarColor.isValid()) {
0263                     m_warningCapacityBarColor = KColorScheme(cg, KColorScheme::View).foreground(KColorScheme::NegativeText).color();
0264                 }
0265                 capacityFgColor = m_warningCapacityBarColor;
0266             }
0267 
0268             painter->save();
0269 
0270             painter->setRenderHint(QPainter::Antialiasing, true);
0271             painter->setPen(Qt::NoPen);
0272 
0273             painter->setBrush(capacityBgColor);
0274             painter->drawRoundedRect(capacityBgRect, radius, radius);
0275 
0276             painter->setBrush(capacityFgColor);
0277             painter->drawRoundedRect(capacityFillRect, radius, radius);
0278 
0279             painter->restore();
0280         }
0281     }
0282 
0283     painter->drawText(rectText,
0284                       Qt::AlignLeft | Qt::AlignVCenter,
0285                       opt.fontMetrics.elidedText(index.model()->data(index).toString(), Qt::ElideRight, rectText.width()));
0286 
0287     painter->restore();
0288 }
0289 
0290 bool KFilePlacesViewDelegate::helpEvent(QHelpEvent *event, QAbstractItemView *view, const QStyleOptionViewItem &option, const QModelIndex &index)
0291 {
0292     if (event->type() == QHelpEvent::ToolTip) {
0293         if (pointIsTeardownAction(event->pos())) {
0294             if (auto *placesModel = qobject_cast<const KFilePlacesModel *>(index.model())) {
0295                 Q_ASSERT(placesModel->isTeardownOverlayRecommended(index));
0296 
0297                 QString toolTipText;
0298 
0299                 if (auto eject = std::unique_ptr<QAction>{placesModel->ejectActionForIndex(index)}) {
0300                     toolTipText = eject->toolTip();
0301                 } else if (auto teardown = std::unique_ptr<QAction>{placesModel->teardownActionForIndex(index)}) {
0302                     toolTipText = teardown->toolTip();
0303                 }
0304 
0305                 if (!toolTipText.isEmpty()) {
0306                     // TODO rect
0307                     QToolTip::showText(event->globalPos(), toolTipText, m_view);
0308                     event->setAccepted(true);
0309                     return true;
0310                 }
0311             }
0312         }
0313     }
0314     return QAbstractItemDelegate::helpEvent(event, view, option, index);
0315 }
0316 
0317 int KFilePlacesViewDelegate::iconSize() const
0318 {
0319     return m_iconSize;
0320 }
0321 
0322 void KFilePlacesViewDelegate::setIconSize(int newSize)
0323 {
0324     m_iconSize = newSize;
0325 }
0326 
0327 void KFilePlacesViewDelegate::addAppearingItem(const QModelIndex &index)
0328 {
0329     m_appearingItems << index;
0330 }
0331 
0332 void KFilePlacesViewDelegate::setAppearingItemProgress(qreal value)
0333 {
0334     if (value <= 0.25) {
0335         m_appearingOpacity = 0.0;
0336         m_appearingHeightScale = std::min(1.0, value * 4);
0337     } else {
0338         m_appearingHeightScale = 1.0;
0339         m_appearingOpacity = (value - 0.25) * 4 / 3;
0340 
0341         if (value >= 1.0) {
0342             m_appearingItems.clear();
0343         }
0344     }
0345 }
0346 
0347 void KFilePlacesViewDelegate::setDeviceBusyAnimationRotation(qreal angle)
0348 {
0349     m_busyAnimationRotation = angle;
0350 }
0351 
0352 void KFilePlacesViewDelegate::addDisappearingItem(const QModelIndex &index)
0353 {
0354     m_disappearingItems << index;
0355 }
0356 
0357 void KFilePlacesViewDelegate::addDisappearingItemGroup(const QModelIndex &index)
0358 {
0359     const KFilePlacesModel *placesModel = static_cast<const KFilePlacesModel *>(index.model());
0360     const QModelIndexList indexesGroup = placesModel->groupIndexes(placesModel->groupType(index));
0361 
0362     m_disappearingItems.reserve(m_disappearingItems.count() + indexesGroup.count());
0363     std::transform(indexesGroup.begin(), indexesGroup.end(), std::back_inserter(m_disappearingItems), [](const QModelIndex &idx) {
0364         return QPersistentModelIndex(idx);
0365     });
0366 }
0367 
0368 void KFilePlacesViewDelegate::setDisappearingItemProgress(qreal value)
0369 {
0370     value = 1.0 - value;
0371 
0372     if (value <= 0.25) {
0373         m_disappearingOpacity = 0.0;
0374         m_disappearingHeightScale = std::min(1.0, value * 4);
0375 
0376         if (value <= 0.0) {
0377             m_disappearingItems.clear();
0378         }
0379     } else {
0380         m_disappearingHeightScale = 1.0;
0381         m_disappearingOpacity = (value - 0.25) * 4 / 3;
0382     }
0383 }
0384 
0385 void KFilePlacesViewDelegate::setShowHoverIndication(bool show)
0386 {
0387     m_showHoverIndication = show;
0388 }
0389 
0390 void KFilePlacesViewDelegate::setHoveredHeaderArea(const QModelIndex &index)
0391 {
0392     m_hoveredHeaderArea = index;
0393 }
0394 
0395 void KFilePlacesViewDelegate::setHoveredAction(const QModelIndex &index)
0396 {
0397     m_hoveredAction = index;
0398 }
0399 
0400 bool KFilePlacesViewDelegate::pointIsHeaderArea(const QPoint &pos) const
0401 {
0402     // we only accept drag events starting from item body, ignore drag request from header
0403     QModelIndex index = m_view->indexAt(pos);
0404     if (!index.isValid()) {
0405         return false;
0406     }
0407 
0408     if (indexIsSectionHeader(index)) {
0409         const QRect vRect = m_view->visualRect(index);
0410         const int delegateY = pos.y() - vRect.y();
0411         if (delegateY <= sectionHeaderHeight(index)) {
0412             return true;
0413         }
0414     }
0415     return false;
0416 }
0417 
0418 bool KFilePlacesViewDelegate::pointIsTeardownAction(const QPoint &pos) const
0419 {
0420     QModelIndex index = m_view->indexAt(pos);
0421     if (!index.isValid()) {
0422         return false;
0423     }
0424 
0425     if (!index.data(KFilePlacesModel::TeardownOverlayRecommendedRole).toBool()) {
0426         return false;
0427     }
0428 
0429     const QRect vRect = m_view->visualRect(index);
0430     const bool isLTR = m_view->layoutDirection() == Qt::LeftToRight;
0431 
0432     const int delegateX = pos.x() - vRect.x();
0433 
0434     if (isLTR) {
0435         if (delegateX < (vRect.width() - 2 * s_lateralMargin - actionIconSize())) {
0436             return false;
0437         }
0438     } else {
0439         if (delegateX >= 2 * s_lateralMargin + actionIconSize()) {
0440             return false;
0441         }
0442     }
0443 
0444     return true;
0445 }
0446 
0447 void KFilePlacesViewDelegate::startDrag()
0448 {
0449     m_dragStarted = true;
0450 }
0451 
0452 void KFilePlacesViewDelegate::checkFreeSpace()
0453 {
0454     if (!m_view->model()) {
0455         return;
0456     }
0457 
0458     bool hasChecked = false;
0459 
0460     for (int i = 0; i < m_view->model()->rowCount(); ++i) {
0461         if (m_view->isRowHidden(i)) {
0462             continue;
0463         }
0464 
0465         const QModelIndex idx = m_view->model()->index(i, 0);
0466         if (!idx.data(KFilePlacesModel::CapacityBarRecommendedRole).toBool()) {
0467             continue;
0468         }
0469 
0470         checkFreeSpace(idx);
0471         hasChecked = true;
0472     }
0473 
0474     if (!hasChecked) {
0475         // Stop timer, there are no more devices
0476         stopPollingFreeSpace();
0477     }
0478 }
0479 
0480 void KFilePlacesViewDelegate::startPollingFreeSpace() const
0481 {
0482     if (m_pollFreeSpace.isActive()) {
0483         return;
0484     }
0485 
0486     if (!m_view->isActiveWindow() || !m_view->isVisible()) {
0487         return;
0488     }
0489 
0490     m_pollFreeSpace.start();
0491 }
0492 
0493 void KFilePlacesViewDelegate::stopPollingFreeSpace() const
0494 {
0495     m_pollFreeSpace.stop();
0496 }
0497 
0498 void KFilePlacesViewDelegate::checkFreeSpace(const QModelIndex &index) const
0499 {
0500     Q_ASSERT(index.data(KFilePlacesModel::CapacityBarRecommendedRole).toBool());
0501 
0502     const QUrl url = index.data(KFilePlacesModel::UrlRole).toUrl();
0503 
0504     QPersistentModelIndex persistentIndex{index};
0505 
0506     auto &info = m_freeSpaceInfo[persistentIndex];
0507 
0508     if (info.job || !info.timeout.hasExpired()) {
0509         return;
0510     }
0511 
0512     // Restarting timeout before job finishes, so that when we poll all devices
0513     // and then get the result, the next poll will again update and not have
0514     // a remaining time of 99% because it came in shortly afterwards.
0515     // Also allow a bit of Timer slack.
0516     info.timeout.setRemainingTime(s_pollFreeSpaceInterval - 100ms);
0517 
0518     info.job = KIO::fileSystemFreeSpace(url);
0519     QObject::connect(info.job, &KJob::result, this, [this, info, persistentIndex]() {
0520         if (!persistentIndex.isValid()) {
0521             return;
0522         }
0523 
0524         const auto job = info.job;
0525         if (job->error()) {
0526             return;
0527         }
0528 
0529         PlaceFreeSpaceInfo &info = m_freeSpaceInfo[persistentIndex];
0530 
0531         info.size = job->size();
0532         info.used = job->size() - job->availableSize();
0533 
0534         m_view->update(persistentIndex);
0535     });
0536 
0537     startPollingFreeSpace();
0538 }
0539 
0540 void KFilePlacesViewDelegate::clearFreeSpaceInfo()
0541 {
0542     m_freeSpaceInfo.clear();
0543 }
0544 
0545 QString KFilePlacesViewDelegate::groupNameFromIndex(const QModelIndex &index) const
0546 {
0547     if (index.isValid()) {
0548         return index.data(KFilePlacesModel::GroupRole).toString();
0549     } else {
0550         return QString();
0551     }
0552 }
0553 
0554 QModelIndex KFilePlacesViewDelegate::previousVisibleIndex(const QModelIndex &index) const
0555 {
0556     if (!index.isValid() || index.row() == 0) {
0557         return QModelIndex();
0558     }
0559 
0560     const QAbstractItemModel *model = index.model();
0561     QModelIndex prevIndex = model->index(index.row() - 1, index.column(), index.parent());
0562 
0563     while (m_view->isRowHidden(prevIndex.row())) {
0564         if (prevIndex.row() == 0) {
0565             return QModelIndex();
0566         }
0567         prevIndex = model->index(prevIndex.row() - 1, index.column(), index.parent());
0568     }
0569 
0570     return prevIndex;
0571 }
0572 
0573 bool KFilePlacesViewDelegate::indexIsSectionHeader(const QModelIndex &index) const
0574 {
0575     if (m_view->isRowHidden(index.row())) {
0576         return false;
0577     }
0578 
0579     const auto groupName = groupNameFromIndex(index);
0580     const auto previousGroupName = groupNameFromIndex(previousVisibleIndex(index));
0581     return groupName != previousGroupName;
0582 }
0583 
0584 void KFilePlacesViewDelegate::drawSectionHeader(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
0585 {
0586     const KFilePlacesModel *placesModel = static_cast<const KFilePlacesModel *>(index.model());
0587 
0588     const QString groupLabel = index.data(KFilePlacesModel::GroupRole).toString();
0589     const QString category = placesModel->isGroupHidden(index)
0590             // Avoid showing "(hidden)" during disappear animation when hiding a group
0591             && !m_disappearingItems.contains(index)
0592         ? i18n("%1 (hidden)", groupLabel)
0593         : groupLabel;
0594 
0595     QRect textRect(option.rect);
0596     textRect.setLeft(textRect.left() + 6);
0597     /* Take spacing into account:
0598        The spacing to the previous section compensates for the spacing to the first item.*/
0599     textRect.setY(textRect.y() /* + qMax(2, m_view->spacing()) - qMax(2, m_view->spacing())*/);
0600     textRect.setHeight(sectionHeaderHeight(index) - s_lateralMargin - m_view->spacing());
0601 
0602     painter->save();
0603 
0604     // based on dolphin colors
0605     const QColor c1 = textColor(option);
0606     const QColor c2 = baseColor(option);
0607     QColor penColor = mixedColor(c1, c2, 60);
0608 
0609     painter->setPen(penColor);
0610     painter->drawText(textRect, Qt::AlignLeft | Qt::AlignBottom, option.fontMetrics.elidedText(category, Qt::ElideRight, textRect.width()));
0611     painter->restore();
0612 }
0613 
0614 void KFilePlacesViewDelegate::paletteChange()
0615 {
0616     // Reset cache, will be re-created when painted
0617     m_warningCapacityBarColor = QColor();
0618 }
0619 
0620 QColor KFilePlacesViewDelegate::textColor(const QStyleOption &option) const
0621 {
0622     const QPalette::ColorGroup group = m_view->isActiveWindow() ? QPalette::Active : QPalette::Inactive;
0623     return option.palette.color(group, QPalette::WindowText);
0624 }
0625 
0626 QColor KFilePlacesViewDelegate::baseColor(const QStyleOption &option) const
0627 {
0628     const QPalette::ColorGroup group = m_view->isActiveWindow() ? QPalette::Active : QPalette::Inactive;
0629     return option.palette.color(group, QPalette::Window);
0630 }
0631 
0632 QColor KFilePlacesViewDelegate::mixedColor(const QColor &c1, const QColor &c2, int c1Percent) const
0633 {
0634     Q_ASSERT(c1Percent >= 0 && c1Percent <= 100);
0635 
0636     const int c2Percent = 100 - c1Percent;
0637     return QColor((c1.red() * c1Percent + c2.red() * c2Percent) / 100,
0638                   (c1.green() * c1Percent + c2.green() * c2Percent) / 100,
0639                   (c1.blue() * c1Percent + c2.blue() * c2Percent) / 100);
0640 }
0641 
0642 int KFilePlacesViewDelegate::sectionHeaderHeight(const QModelIndex &index) const
0643 {
0644     // Account for the spacing between header and item
0645     const int spacing = (s_lateralMargin + m_view->spacing());
0646     int height = m_view->fontMetrics().height() + spacing;
0647     height += 2 * spacing;
0648     return height;
0649 }
0650 
0651 int KFilePlacesViewDelegate::actionIconSize() const
0652 {
0653     return qApp->style()->pixelMetric(QStyle::PM_SmallIconSize, nullptr, m_view);
0654 }
0655 
0656 class KFilePlacesViewPrivate
0657 {
0658 public:
0659     explicit KFilePlacesViewPrivate(KFilePlacesView *qq)
0660         : q(qq)
0661         , m_watcher(new KFilePlacesEventWatcher(q))
0662         , m_delegate(new KFilePlacesViewDelegate(q))
0663     {
0664     }
0665 
0666     using ActivationSignal = void (KFilePlacesView::*)(const QUrl &);
0667 
0668     enum FadeType {
0669         FadeIn = 0,
0670         FadeOut,
0671     };
0672 
0673     void setCurrentIndex(const QModelIndex &index);
0674     // If m_autoResizeItems is true, calculates a proper size for the icons in the places panel
0675     void adaptItemSize();
0676     void updateHiddenRows();
0677     void clearFreeSpaceInfos();
0678     bool insertAbove(const QDropEvent *event, const QRect &itemRect) const;
0679     bool insertBelow(const QDropEvent *event, const QRect &itemRect) const;
0680     int insertIndicatorHeight(int itemHeight) const;
0681     int sectionsCount() const;
0682 
0683     void addPlace(const QModelIndex &index);
0684     void editPlace(const QModelIndex &index);
0685 
0686     void addDisappearingItem(KFilePlacesViewDelegate *delegate, const QModelIndex &index);
0687     void triggerItemAppearingAnimation();
0688     void triggerItemDisappearingAnimation();
0689     bool shouldAnimate() const;
0690 
0691     void writeConfig();
0692     void readConfig();
0693     // Sets the size of the icons in the places panel
0694     void relayoutIconSize(int size);
0695     // Adds the "Icon Size" sub-menu items
0696     void setupIconSizeSubMenu(QMenu *submenu);
0697 
0698     void placeClicked(const QModelIndex &index, ActivationSignal activationSignal);
0699     void headerAreaEntered(const QModelIndex &index);
0700     void headerAreaLeft(const QModelIndex &index);
0701     void actionClicked(const QModelIndex &index);
0702     void actionEntered(const QModelIndex &index);
0703     void actionLeft(const QModelIndex &index);
0704     void teardown(const QModelIndex &index);
0705     void storageSetupDone(const QModelIndex &index, bool success);
0706     void adaptItemsUpdate(qreal value);
0707     void itemAppearUpdate(qreal value);
0708     void itemDisappearUpdate(qreal value);
0709     void enableSmoothItemResizing();
0710     void slotEmptyTrash();
0711 
0712     void deviceBusyAnimationValueChanged(const QVariant &value);
0713 
0714     KFilePlacesView *const q;
0715 
0716     KFilePlacesEventWatcher *const m_watcher;
0717     KFilePlacesViewDelegate *m_delegate;
0718 
0719     Solid::StorageAccess *m_lastClickedStorage = nullptr;
0720     QPersistentModelIndex m_lastClickedIndex;
0721     ActivationSignal m_lastActivationSignal = nullptr;
0722 
0723     QTimer *m_dragActivationTimer = nullptr;
0724     QPersistentModelIndex m_pendingDragActivation;
0725 
0726     QPersistentModelIndex m_pendingDropUrlsIndex;
0727     std::unique_ptr<QDropEvent> m_dropUrlsEvent;
0728     std::unique_ptr<QMimeData> m_dropUrlsMimeData;
0729 
0730     KFilePlacesView::TeardownFunction m_teardownFunction = nullptr;
0731 
0732     QTimeLine m_adaptItemsTimeline;
0733     QTimeLine m_itemAppearTimeline;
0734     QTimeLine m_itemDisappearTimeline;
0735 
0736     QVariantAnimation m_deviceBusyAnimation;
0737     QList<QPersistentModelIndex> m_busyDevices;
0738 
0739     QRect m_dropRect;
0740     QPersistentModelIndex m_dropIndex;
0741 
0742     QUrl m_currentUrl;
0743 
0744     int m_oldSize = 0;
0745     int m_endSize = 0;
0746 
0747     bool m_autoResizeItems = true;
0748     bool m_smoothItemResizing = false;
0749     bool m_showAll = false;
0750     bool m_dropOnPlace = false;
0751     bool m_dragging = false;
0752 };
0753 
0754 KFilePlacesView::KFilePlacesView(QWidget *parent)
0755     : QListView(parent)
0756     , d(std::make_unique<KFilePlacesViewPrivate>(this))
0757 {
0758     setItemDelegate(d->m_delegate);
0759 
0760     d->readConfig();
0761 
0762     setSelectionRectVisible(false);
0763     setSelectionMode(SingleSelection);
0764 
0765     setDragEnabled(true);
0766     setAcceptDrops(true);
0767     setMouseTracking(true);
0768     setDropIndicatorShown(false);
0769     setFrameStyle(QFrame::NoFrame);
0770 
0771     setResizeMode(Adjust);
0772 
0773     QPalette palette = viewport()->palette();
0774     palette.setColor(viewport()->backgroundRole(), Qt::transparent);
0775     palette.setColor(viewport()->foregroundRole(), palette.color(QPalette::WindowText));
0776     viewport()->setPalette(palette);
0777 
0778     setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
0779 
0780     d->m_watcher->m_scroller = QScroller::scroller(viewport());
0781     QScrollerProperties scrollerProp;
0782     scrollerProp.setScrollMetric(QScrollerProperties::AcceleratingFlickMaximumTime, 0.2); // QTBUG-88249
0783     d->m_watcher->m_scroller->setScrollerProperties(scrollerProp);
0784     d->m_watcher->m_scroller->grabGesture(viewport());
0785     connect(d->m_watcher->m_scroller, &QScroller::stateChanged, d->m_watcher, &KFilePlacesEventWatcher::qScrollerStateChanged);
0786 
0787     setAttribute(Qt::WA_AcceptTouchEvents);
0788     viewport()->grabGesture(Qt::TapGesture);
0789     viewport()->grabGesture(Qt::TapAndHoldGesture);
0790 
0791     // Note: Don't connect to the activated() signal, as the behavior when it is
0792     // committed depends on the used widget style. The click behavior of
0793     // KFilePlacesView should be style independent.
0794     connect(this, &KFilePlacesView::clicked, this, [this](const QModelIndex &index) {
0795         const auto modifiers = qGuiApp->keyboardModifiers();
0796         if (modifiers == (Qt::ControlModifier | Qt::ShiftModifier) && isSignalConnected(QMetaMethod::fromSignal(&KFilePlacesView::activeTabRequested))) {
0797             d->placeClicked(index, &KFilePlacesView::activeTabRequested);
0798         } else if (modifiers == Qt::ControlModifier && isSignalConnected(QMetaMethod::fromSignal(&KFilePlacesView::tabRequested))) {
0799             d->placeClicked(index, &KFilePlacesView::tabRequested);
0800         } else if (modifiers == Qt::ShiftModifier && isSignalConnected(QMetaMethod::fromSignal(&KFilePlacesView::newWindowRequested))) {
0801             d->placeClicked(index, &KFilePlacesView::newWindowRequested);
0802         } else {
0803             d->placeClicked(index, &KFilePlacesView::placeActivated);
0804         }
0805     });
0806 
0807     connect(this, &QAbstractItemView::iconSizeChanged, this, [this](const QSize &newSize) {
0808         d->m_autoResizeItems = (newSize.width() < 1 || newSize.height() < 1);
0809 
0810         if (d->m_autoResizeItems) {
0811             d->adaptItemSize();
0812         } else {
0813             const int iconSize = qMin(newSize.width(), newSize.height());
0814             d->relayoutIconSize(iconSize);
0815         }
0816         d->writeConfig();
0817     });
0818 
0819     connect(&d->m_adaptItemsTimeline, &QTimeLine::valueChanged, this, [this](qreal value) {
0820         d->adaptItemsUpdate(value);
0821     });
0822     d->m_adaptItemsTimeline.setDuration(500);
0823     d->m_adaptItemsTimeline.setUpdateInterval(5);
0824     d->m_adaptItemsTimeline.setEasingCurve(QEasingCurve::InOutSine);
0825 
0826     connect(&d->m_itemAppearTimeline, &QTimeLine::valueChanged, this, [this](qreal value) {
0827         d->itemAppearUpdate(value);
0828     });
0829     d->m_itemAppearTimeline.setDuration(500);
0830     d->m_itemAppearTimeline.setUpdateInterval(5);
0831     d->m_itemAppearTimeline.setEasingCurve(QEasingCurve::InOutSine);
0832 
0833     connect(&d->m_itemDisappearTimeline, &QTimeLine::valueChanged, this, [this](qreal value) {
0834         d->itemDisappearUpdate(value);
0835     });
0836     d->m_itemDisappearTimeline.setDuration(500);
0837     d->m_itemDisappearTimeline.setUpdateInterval(5);
0838     d->m_itemDisappearTimeline.setEasingCurve(QEasingCurve::InOutSine);
0839 
0840     // Adapted from KBusyIndicatorWidget
0841     d->m_deviceBusyAnimation.setLoopCount(-1);
0842     d->m_deviceBusyAnimation.setDuration(2000);
0843     d->m_deviceBusyAnimation.setStartValue(0);
0844     d->m_deviceBusyAnimation.setEndValue(360);
0845     connect(&d->m_deviceBusyAnimation, &QVariantAnimation::valueChanged, this, [this](const QVariant &value) {
0846         d->deviceBusyAnimationValueChanged(value);
0847     });
0848 
0849     viewport()->installEventFilter(d->m_watcher);
0850     connect(d->m_watcher, &KFilePlacesEventWatcher::entryMiddleClicked, this, [this](const QModelIndex &index) {
0851         if (qGuiApp->keyboardModifiers() == Qt::ShiftModifier && isSignalConnected(QMetaMethod::fromSignal(&KFilePlacesView::activeTabRequested))) {
0852             d->placeClicked(index, &KFilePlacesView::activeTabRequested);
0853         } else if (isSignalConnected(QMetaMethod::fromSignal(&KFilePlacesView::tabRequested))) {
0854             d->placeClicked(index, &KFilePlacesView::tabRequested);
0855         } else {
0856             d->placeClicked(index, &KFilePlacesView::placeActivated);
0857         }
0858     });
0859 
0860     connect(d->m_watcher, &KFilePlacesEventWatcher::headerAreaEntered, this, [this](const QModelIndex &index) {
0861         d->headerAreaEntered(index);
0862     });
0863     connect(d->m_watcher, &KFilePlacesEventWatcher::headerAreaLeft, this, [this](const QModelIndex &index) {
0864         d->headerAreaLeft(index);
0865     });
0866 
0867     connect(d->m_watcher, &KFilePlacesEventWatcher::actionClicked, this, [this](const QModelIndex &index) {
0868         d->actionClicked(index);
0869     });
0870     connect(d->m_watcher, &KFilePlacesEventWatcher::actionEntered, this, [this](const QModelIndex &index) {
0871         d->actionEntered(index);
0872     });
0873     connect(d->m_watcher, &KFilePlacesEventWatcher::actionLeft, this, [this](const QModelIndex &index) {
0874         d->actionLeft(index);
0875     });
0876 
0877     connect(d->m_watcher, &KFilePlacesEventWatcher::windowActivated, this, [this] {
0878         d->m_delegate->checkFreeSpace();
0879         // Start polling even if checkFreeSpace() wouldn't because we might just have checked
0880         // free space before the timeout and so the poll timer would never get started again
0881         d->m_delegate->startPollingFreeSpace();
0882     });
0883     connect(d->m_watcher, &KFilePlacesEventWatcher::windowDeactivated, this, [this] {
0884         d->m_delegate->stopPollingFreeSpace();
0885     });
0886 
0887     connect(d->m_watcher, &KFilePlacesEventWatcher::paletteChanged, this, [this] {
0888         d->m_delegate->paletteChange();
0889     });
0890 
0891     // FIXME: this is necessary to avoid flashes of black with some widget styles.
0892     // could be a bug in Qt (e.g. QAbstractScrollArea) or KFilePlacesView, but has not
0893     // yet been tracked down yet. until then, this works and is harmlessly enough.
0894     // in fact, some QStyle (Oxygen, Skulpture, others?) do this already internally.
0895     // See br #242358 for more information
0896     verticalScrollBar()->setAttribute(Qt::WA_OpaquePaintEvent, false);
0897 }
0898 
0899 KFilePlacesView::~KFilePlacesView()
0900 {
0901     viewport()->removeEventFilter(d->m_watcher);
0902 }
0903 
0904 void KFilePlacesView::setDropOnPlaceEnabled(bool enabled)
0905 {
0906     d->m_dropOnPlace = enabled;
0907 }
0908 
0909 bool KFilePlacesView::isDropOnPlaceEnabled() const
0910 {
0911     return d->m_dropOnPlace;
0912 }
0913 
0914 void KFilePlacesView::setDragAutoActivationDelay(int delay)
0915 {
0916     if (delay <= 0) {
0917         delete d->m_dragActivationTimer;
0918         d->m_dragActivationTimer = nullptr;
0919         return;
0920     }
0921 
0922     if (!d->m_dragActivationTimer) {
0923         d->m_dragActivationTimer = new QTimer(this);
0924         d->m_dragActivationTimer->setSingleShot(true);
0925         connect(d->m_dragActivationTimer, &QTimer::timeout, this, [this] {
0926             if (d->m_pendingDragActivation.isValid()) {
0927                 d->placeClicked(d->m_pendingDragActivation, &KFilePlacesView::placeActivated);
0928             }
0929         });
0930     }
0931     d->m_dragActivationTimer->setInterval(delay);
0932 }
0933 
0934 int KFilePlacesView::dragAutoActivationDelay() const
0935 {
0936     return d->m_dragActivationTimer ? d->m_dragActivationTimer->interval() : 0;
0937 }
0938 
0939 void KFilePlacesView::setAutoResizeItemsEnabled(bool enabled)
0940 {
0941     d->m_autoResizeItems = enabled;
0942 }
0943 
0944 bool KFilePlacesView::isAutoResizeItemsEnabled() const
0945 {
0946     return d->m_autoResizeItems;
0947 }
0948 
0949 void KFilePlacesView::setTeardownFunction(TeardownFunction teardownFunc)
0950 {
0951     d->m_teardownFunction = teardownFunc;
0952 }
0953 
0954 void KFilePlacesView::setUrl(const QUrl &url)
0955 {
0956     KFilePlacesModel *placesModel = qobject_cast<KFilePlacesModel *>(model());
0957 
0958     if (placesModel == nullptr) {
0959         return;
0960     }
0961 
0962     QModelIndex index = placesModel->closestItem(url);
0963     QModelIndex current = selectionModel()->currentIndex();
0964 
0965     if (index.isValid()) {
0966         if (current != index && placesModel->isHidden(current) && !d->m_showAll) {
0967             d->addDisappearingItem(d->m_delegate, current);
0968         }
0969 
0970         if (current != index && placesModel->isHidden(index) && !d->m_showAll) {
0971             d->m_delegate->addAppearingItem(index);
0972             d->triggerItemAppearingAnimation();
0973             setRowHidden(index.row(), false);
0974         }
0975 
0976         d->m_currentUrl = url;
0977 
0978         if (placesModel->url(index) == url.adjusted(QUrl::StripTrailingSlash)) {
0979             selectionModel()->setCurrentIndex(index, QItemSelectionModel::ClearAndSelect);
0980         } else {
0981             selectionModel()->clear();
0982         }
0983     } else {
0984         d->m_currentUrl = QUrl();
0985         selectionModel()->clear();
0986     }
0987 
0988     if (!current.isValid()) {
0989         d->updateHiddenRows();
0990     }
0991 }
0992 
0993 bool KFilePlacesView::allPlacesShown() const
0994 {
0995     return d->m_showAll;
0996 }
0997 
0998 void KFilePlacesView::setShowAll(bool showAll)
0999 {
1000     KFilePlacesModel *placesModel = qobject_cast<KFilePlacesModel *>(model());
1001 
1002     if (placesModel == nullptr) {
1003         return;
1004     }
1005 
1006     d->m_showAll = showAll;
1007 
1008     int rowCount = placesModel->rowCount();
1009     QModelIndex current = placesModel->closestItem(d->m_currentUrl);
1010 
1011     if (showAll) {
1012         d->updateHiddenRows();
1013 
1014         for (int i = 0; i < rowCount; ++i) {
1015             QModelIndex index = placesModel->index(i, 0);
1016             if (index != current && placesModel->isHidden(index)) {
1017                 d->m_delegate->addAppearingItem(index);
1018             }
1019         }
1020         d->triggerItemAppearingAnimation();
1021     } else {
1022         for (int i = 0; i < rowCount; ++i) {
1023             QModelIndex index = placesModel->index(i, 0);
1024             if (index != current && placesModel->isHidden(index)) {
1025                 d->m_delegate->addDisappearingItem(index);
1026             }
1027         }
1028         d->triggerItemDisappearingAnimation();
1029     }
1030 
1031     Q_EMIT allPlacesShownChanged(showAll);
1032 }
1033 
1034 void KFilePlacesView::keyPressEvent(QKeyEvent *event)
1035 {
1036     QListView::keyPressEvent(event);
1037     if ((event->key() == Qt::Key_Return) || (event->key() == Qt::Key_Enter)) {
1038         // TODO Modifier keys for requesting tabs
1039         // Browsers do Ctrl+Click but *Alt*+Return for new tab
1040         d->placeClicked(currentIndex(), &KFilePlacesView::placeActivated);
1041     }
1042 }
1043 
1044 void KFilePlacesViewPrivate::readConfig()
1045 {
1046     KConfigGroup cg(KSharedConfig::openConfig(), ConfigGroup);
1047     m_autoResizeItems = cg.readEntry(PlacesIconsAutoresize, true);
1048     m_delegate->setIconSize(cg.readEntry(PlacesIconsStaticSize, static_cast<int>(KIconLoader::SizeMedium)));
1049 }
1050 
1051 void KFilePlacesViewPrivate::writeConfig()
1052 {
1053     KConfigGroup cg(KSharedConfig::openConfig(), ConfigGroup);
1054     cg.writeEntry(PlacesIconsAutoresize, m_autoResizeItems);
1055 
1056     if (!m_autoResizeItems) {
1057         const int iconSize = qMin(q->iconSize().width(), q->iconSize().height());
1058         cg.writeEntry(PlacesIconsStaticSize, iconSize);
1059     }
1060 
1061     cg.sync();
1062 }
1063 
1064 void KFilePlacesViewPrivate::slotEmptyTrash()
1065 {
1066     auto *parentWindow = q->window();
1067 
1068     using AskIface = KIO::AskUserActionInterface;
1069     auto *emptyTrashJob = new KIO::DeleteOrTrashJob(QList<QUrl>{}, //
1070                                                     AskIface::EmptyTrash,
1071                                                     AskIface::DefaultConfirmation,
1072                                                     parentWindow);
1073     emptyTrashJob->start();
1074 }
1075 
1076 void KFilePlacesView::contextMenuEvent(QContextMenuEvent *event)
1077 {
1078     KFilePlacesModel *placesModel = qobject_cast<KFilePlacesModel *>(model());
1079 
1080     if (!placesModel) {
1081         return;
1082     }
1083 
1084     QModelIndex index = event->reason() == QContextMenuEvent::Keyboard ? selectionModel()->currentIndex() : indexAt(event->pos());
1085     if (!selectedIndexes().contains(index)) {
1086         index = QModelIndex();
1087     }
1088     const QString groupName = index.data(KFilePlacesModel::GroupRole).toString();
1089     const QUrl placeUrl = placesModel->url(index);
1090     const bool clickOverHeader = event->reason() == QContextMenuEvent::Keyboard ? false : d->m_delegate->pointIsHeaderArea(event->pos());
1091     const bool clickOverEmptyArea = clickOverHeader || !index.isValid();
1092     const KFilePlacesModel::GroupType type = placesModel->groupType(index);
1093 
1094     QMenu menu;
1095     // Polish before creating a native window below. The style could want change the surface format
1096     // of the window which will have no effect when the native window has already been created.
1097     menu.ensurePolished();
1098 
1099     QAction *emptyTrash = nullptr;
1100     QAction *eject = nullptr;
1101     QAction *partition = nullptr;
1102     QAction *mount = nullptr;
1103     QAction *teardown = nullptr;
1104 
1105     QAction *newTab = nullptr;
1106     QAction *newWindow = nullptr;
1107     QAction *highPriorityActionsPlaceholder = new QAction();
1108     QAction *properties = nullptr;
1109 
1110     QAction *add = nullptr;
1111     QAction *edit = nullptr;
1112     QAction *remove = nullptr;
1113 
1114     QAction *hide = nullptr;
1115     QAction *hideSection = nullptr;
1116     QAction *showAll = nullptr;
1117     QMenu *iconSizeMenu = nullptr;
1118 
1119     if (!clickOverEmptyArea) {
1120         if (placeUrl.scheme() == QLatin1String("trash")) {
1121             emptyTrash = new QAction(QIcon::fromTheme(QStringLiteral("trash-empty")), i18nc("@action:inmenu", "Empty Trash"), &menu);
1122             KConfig trashConfig(QStringLiteral("trashrc"), KConfig::SimpleConfig);
1123             emptyTrash->setEnabled(!trashConfig.group(QStringLiteral("Status")).readEntry("Empty", true));
1124         }
1125 
1126         if (placesModel->isDevice(index)) {
1127             eject = placesModel->ejectActionForIndex(index);
1128             if (eject) {
1129                 eject->setParent(&menu);
1130             }
1131 
1132             partition = placesModel->partitionActionForIndex(index);
1133             if (partition) {
1134                 partition->setParent(&menu);
1135             }
1136 
1137             teardown = placesModel->teardownActionForIndex(index);
1138             if (teardown) {
1139                 teardown->setParent(&menu);
1140                 if (!placesModel->isTeardownAllowed(index)) {
1141                     teardown->setEnabled(false);
1142                 }
1143             }
1144 
1145             if (placesModel->setupNeeded(index)) {
1146                 mount = new QAction(QIcon::fromTheme(QStringLiteral("media-mount")), i18nc("@action:inmenu", "Mount"), &menu);
1147             }
1148         }
1149 
1150         // TODO What about active tab?
1151         if (isSignalConnected(QMetaMethod::fromSignal(&KFilePlacesView::tabRequested))) {
1152             newTab = new QAction(QIcon::fromTheme(QStringLiteral("tab-new")), i18nc("@item:inmenu", "Open in New Tab"), &menu);
1153         }
1154         if (isSignalConnected(QMetaMethod::fromSignal(&KFilePlacesView::newWindowRequested))) {
1155             newWindow = new QAction(QIcon::fromTheme(QStringLiteral("window-new")), i18nc("@item:inmenu", "Open in New Window"), &menu);
1156         }
1157 
1158         if (placeUrl.isLocalFile()) {
1159             properties = new QAction(QIcon::fromTheme(QStringLiteral("document-properties")), i18n("Properties"), &menu);
1160         }
1161     }
1162 
1163     if (clickOverEmptyArea) {
1164         add = new QAction(QIcon::fromTheme(QStringLiteral("document-new")), i18nc("@action:inmenu", "Add Entry…"), &menu);
1165     }
1166 
1167     if (index.isValid()) {
1168         if (!clickOverHeader) {
1169             if (!placesModel->isDevice(index)) {
1170                 edit = new QAction(QIcon::fromTheme(QStringLiteral("edit-entry")), i18nc("@action:inmenu", "&Edit…"), &menu);
1171 
1172                 KBookmark bookmark = placesModel->bookmarkForIndex(index);
1173                 const bool isSystemItem = bookmark.metaDataItem(QStringLiteral("isSystemItem")) == QLatin1String("true");
1174                 if (!isSystemItem) {
1175                     remove = new QAction(QIcon::fromTheme(QStringLiteral("bookmark-remove-symbolic")), i18nc("@action:inmenu", "Remove from Places"), &menu);
1176                 }
1177             }
1178 
1179             hide = new QAction(QIcon::fromTheme(QStringLiteral("hint")), i18nc("@action:inmenu", "&Hide"), &menu);
1180             hide->setCheckable(true);
1181             hide->setChecked(placesModel->isHidden(index));
1182             // if a parent is hidden no interaction should be possible with children, show it first to do so
1183             hide->setEnabled(!placesModel->isGroupHidden(placesModel->groupType(index)));
1184         }
1185 
1186         hideSection = new QAction(QIcon::fromTheme(QStringLiteral("hint")),
1187                                   !groupName.isEmpty() ? i18nc("@item:inmenu", "Hide Section '%1'", groupName) : i18nc("@item:inmenu", "Hide Section"),
1188                                   &menu);
1189         hideSection->setCheckable(true);
1190         hideSection->setChecked(placesModel->isGroupHidden(type));
1191     }
1192 
1193     if (clickOverEmptyArea) {
1194         if (placesModel->hiddenCount() > 0) {
1195             showAll = new QAction(QIcon::fromTheme(QStringLiteral("visibility")), i18n("&Show All Entries"), &menu);
1196             showAll->setCheckable(true);
1197             showAll->setChecked(d->m_showAll);
1198         }
1199 
1200         iconSizeMenu = new QMenu(i18nc("@item:inmenu", "Icon Size"), &menu);
1201         d->setupIconSizeSubMenu(iconSizeMenu);
1202     }
1203 
1204     auto addActionToMenu = [&menu](QAction *action) {
1205         if (action) { // silence warning when adding null action
1206             menu.addAction(action);
1207         }
1208     };
1209 
1210     addActionToMenu(emptyTrash);
1211 
1212     addActionToMenu(eject);
1213     addActionToMenu(mount);
1214     addActionToMenu(teardown);
1215     menu.addSeparator();
1216 
1217     if (partition) {
1218         addActionToMenu(partition);
1219         menu.addSeparator();
1220     }
1221 
1222     addActionToMenu(newTab);
1223     addActionToMenu(newWindow);
1224     addActionToMenu(highPriorityActionsPlaceholder);
1225     addActionToMenu(properties);
1226     menu.addSeparator();
1227 
1228     addActionToMenu(add);
1229     addActionToMenu(edit);
1230     addActionToMenu(remove);
1231     addActionToMenu(hide);
1232     addActionToMenu(hideSection);
1233     addActionToMenu(showAll);
1234     if (iconSizeMenu) {
1235         menu.addMenu(iconSizeMenu);
1236     }
1237 
1238     menu.addSeparator();
1239 
1240     // Clicking a header should be treated as clicking no device, hence passing an invalid model index
1241     // Emit the signal before adding any custom actions to give the user a chance to dynamically add/remove them
1242     Q_EMIT contextMenuAboutToShow(clickOverHeader ? QModelIndex() : index, &menu);
1243 
1244     const auto additionalActions = actions();
1245     for (QAction *action : additionalActions) {
1246         if (action->priority() == QAction::HighPriority) {
1247             menu.insertAction(highPriorityActionsPlaceholder, action);
1248         } else {
1249             menu.addAction(action);
1250         }
1251     }
1252     delete highPriorityActionsPlaceholder;
1253 
1254     if (window()) {
1255         menu.winId();
1256         menu.windowHandle()->setTransientParent(window()->windowHandle());
1257     }
1258     QAction *result;
1259     if (event->reason() == QContextMenuEvent::Keyboard && index.isValid()) {
1260         const QRect rect = visualRect(index);
1261         result = menu.exec(mapToGlobal(QPoint(rect.x() + rect.width() / 2, rect.y() + rect.height() * 0.9)));
1262     } else {
1263         result = menu.exec(event->globalPos());
1264     }
1265 
1266     if (result) {
1267         if (result == emptyTrash) {
1268             d->slotEmptyTrash();
1269 
1270         } else if (result == eject) {
1271             placesModel->requestEject(index);
1272         } else if (result == mount) {
1273             placesModel->requestSetup(index);
1274         } else if (result == teardown) {
1275             d->teardown(index);
1276         } else if (result == newTab) {
1277             d->placeClicked(index, &KFilePlacesView::tabRequested);
1278         } else if (result == newWindow) {
1279             d->placeClicked(index, &KFilePlacesView::newWindowRequested);
1280         } else if (result == properties) {
1281             KPropertiesDialog::showDialog(placeUrl, this);
1282         } else if (result == add) {
1283             d->addPlace(index);
1284         } else if (result == edit) {
1285             d->editPlace(index);
1286         } else if (result == remove) {
1287             placesModel->removePlace(index);
1288         } else if (result == hide) {
1289             placesModel->setPlaceHidden(index, hide->isChecked());
1290             QModelIndex current = placesModel->closestItem(d->m_currentUrl);
1291 
1292             if (index != current && !d->m_showAll && hide->isChecked()) {
1293                 d->m_delegate->addDisappearingItem(index);
1294                 d->triggerItemDisappearingAnimation();
1295             }
1296         } else if (result == hideSection) {
1297             placesModel->setGroupHidden(type, hideSection->isChecked());
1298 
1299             if (!d->m_showAll && hideSection->isChecked()) {
1300                 d->m_delegate->addDisappearingItemGroup(index);
1301                 d->triggerItemDisappearingAnimation();
1302             }
1303         } else if (result == showAll) {
1304             setShowAll(showAll->isChecked());
1305         }
1306     }
1307 
1308     if (event->reason() != QContextMenuEvent::Keyboard) {
1309         index = placesModel->closestItem(d->m_currentUrl);
1310         selectionModel()->setCurrentIndex(index, QItemSelectionModel::ClearAndSelect);
1311     }
1312 }
1313 
1314 void KFilePlacesViewPrivate::setupIconSizeSubMenu(QMenu *submenu)
1315 {
1316     QActionGroup *group = new QActionGroup(submenu);
1317 
1318     auto *autoAct = new QAction(i18nc("@item:inmenu Auto set icon size based on available space in"
1319                                       "the Places side-panel",
1320                                       "Auto Resize"),
1321                                 group);
1322     autoAct->setCheckable(true);
1323     autoAct->setChecked(m_autoResizeItems);
1324     QObject::connect(autoAct, &QAction::toggled, q, [this]() {
1325         q->setIconSize(QSize(-1, -1));
1326     });
1327     submenu->addAction(autoAct);
1328 
1329     static constexpr KIconLoader::StdSizes iconSizes[] = {KIconLoader::SizeSmall,
1330                                                           KIconLoader::SizeSmallMedium,
1331                                                           KIconLoader::SizeMedium,
1332                                                           KIconLoader::SizeLarge};
1333 
1334     for (const auto iconSize : iconSizes) {
1335         auto *act = new QAction(group);
1336         act->setCheckable(true);
1337 
1338         switch (iconSize) {
1339         case KIconLoader::SizeSmall:
1340             act->setText(i18nc("Small icon size", "Small (%1x%1)", KIconLoader::SizeSmall));
1341             break;
1342         case KIconLoader::SizeSmallMedium:
1343             act->setText(i18nc("Medium icon size", "Medium (%1x%1)", KIconLoader::SizeSmallMedium));
1344             break;
1345         case KIconLoader::SizeMedium:
1346             act->setText(i18nc("Large icon size", "Large (%1x%1)", KIconLoader::SizeMedium));
1347             break;
1348         case KIconLoader::SizeLarge:
1349             act->setText(i18nc("Huge icon size", "Huge (%1x%1)", KIconLoader::SizeLarge));
1350             break;
1351         default:
1352             break;
1353         }
1354 
1355         QObject::connect(act, &QAction::toggled, q, [this, iconSize]() {
1356             q->setIconSize(QSize(iconSize, iconSize));
1357         });
1358 
1359         if (!m_autoResizeItems) {
1360             act->setChecked(iconSize == m_delegate->iconSize());
1361         }
1362 
1363         submenu->addAction(act);
1364     }
1365 }
1366 
1367 void KFilePlacesView::resizeEvent(QResizeEvent *event)
1368 {
1369     QListView::resizeEvent(event);
1370     d->adaptItemSize();
1371 }
1372 
1373 void KFilePlacesView::showEvent(QShowEvent *event)
1374 {
1375     QListView::showEvent(event);
1376 
1377     d->m_delegate->checkFreeSpace();
1378     // Start polling even if checkFreeSpace() wouldn't because we might just have checked
1379     // free space before the timeout and so the poll timer would never get started again
1380     d->m_delegate->startPollingFreeSpace();
1381 
1382     QTimer::singleShot(100, this, [this]() {
1383         d->enableSmoothItemResizing();
1384     });
1385 }
1386 
1387 void KFilePlacesView::hideEvent(QHideEvent *event)
1388 {
1389     QListView::hideEvent(event);
1390     d->m_delegate->stopPollingFreeSpace();
1391     d->m_smoothItemResizing = false;
1392 }
1393 
1394 void KFilePlacesView::dragEnterEvent(QDragEnterEvent *event)
1395 {
1396     QListView::dragEnterEvent(event);
1397     d->m_dragging = true;
1398 
1399     d->m_delegate->setShowHoverIndication(false);
1400 
1401     d->m_dropRect = QRect();
1402     d->m_dropIndex = QPersistentModelIndex();
1403 }
1404 
1405 void KFilePlacesView::dragLeaveEvent(QDragLeaveEvent *event)
1406 {
1407     QListView::dragLeaveEvent(event);
1408     d->m_dragging = false;
1409 
1410     d->m_delegate->setShowHoverIndication(true);
1411 
1412     if (d->m_dragActivationTimer) {
1413         d->m_dragActivationTimer->stop();
1414     }
1415     d->m_pendingDragActivation = QPersistentModelIndex();
1416 
1417     setDirtyRegion(d->m_dropRect);
1418 }
1419 
1420 void KFilePlacesView::dragMoveEvent(QDragMoveEvent *event)
1421 {
1422     QListView::dragMoveEvent(event);
1423 
1424     bool autoActivate = false;
1425     // update the drop indicator
1426     const QPoint pos = event->position().toPoint();
1427     const QModelIndex index = indexAt(pos);
1428     setDirtyRegion(d->m_dropRect);
1429     if (index.isValid()) {
1430         d->m_dropIndex = index;
1431         const QRect rect = visualRect(index);
1432         const int gap = d->insertIndicatorHeight(rect.height());
1433 
1434         if (d->insertAbove(event, rect)) {
1435             // indicate that the item will be inserted above the current place
1436             d->m_dropRect = QRect(rect.left(), rect.top() - gap / 2, rect.width(), gap);
1437         } else if (d->insertBelow(event, rect)) {
1438             // indicate that the item will be inserted below the current place
1439             d->m_dropRect = QRect(rect.left(), rect.bottom() + 1 - gap / 2, rect.width(), gap);
1440         } else {
1441             // indicate that the item be dropped above the current place
1442             d->m_dropRect = rect;
1443             // only auto-activate when dropping ontop of a place, not inbetween
1444             autoActivate = true;
1445         }
1446     }
1447 
1448     if (d->m_dragActivationTimer) {
1449         if (autoActivate && !d->m_delegate->pointIsHeaderArea(event->position().toPoint())) {
1450             QPersistentModelIndex persistentIndex(index);
1451             if (!d->m_pendingDragActivation.isValid() || d->m_pendingDragActivation != persistentIndex) {
1452                 d->m_pendingDragActivation = persistentIndex;
1453                 d->m_dragActivationTimer->start();
1454             }
1455         } else {
1456             d->m_dragActivationTimer->stop();
1457             d->m_pendingDragActivation = QPersistentModelIndex();
1458         }
1459     }
1460 
1461     setDirtyRegion(d->m_dropRect);
1462 }
1463 
1464 void KFilePlacesView::dropEvent(QDropEvent *event)
1465 {
1466     const QModelIndex index = indexAt(event->position().toPoint());
1467     if (index.isValid()) {
1468         const QRect rect = visualRect(index);
1469         if (!d->insertAbove(event, rect) && !d->insertBelow(event, rect)) {
1470             KFilePlacesModel *placesModel = qobject_cast<KFilePlacesModel *>(model());
1471             Q_ASSERT(placesModel != nullptr);
1472             if (placesModel->setupNeeded(index)) {
1473                 d->m_pendingDropUrlsIndex = index;
1474 
1475                 // Make a full copy of the Mime-Data
1476                 d->m_dropUrlsMimeData = std::make_unique<QMimeData>();
1477                 const auto formats = event->mimeData()->formats();
1478                 for (const auto &format : formats) {
1479                     d->m_dropUrlsMimeData->setData(format, event->mimeData()->data(format));
1480                 }
1481 
1482                 d->m_dropUrlsEvent = std::make_unique<QDropEvent>(event->position(),
1483                                                                   event->possibleActions(),
1484                                                                   d->m_dropUrlsMimeData.get(),
1485                                                                   event->buttons(),
1486                                                                   event->modifiers());
1487 
1488                 placesModel->requestSetup(index);
1489             } else {
1490                 Q_EMIT urlsDropped(placesModel->url(index), event, this);
1491             }
1492             // HACK Qt eventually calls into QAIM::dropMimeData when a drop event isn't
1493             // accepted by the view. However, QListView::dropEvent calls ignore() on our
1494             // event afterwards when
1495             // "icon view didn't move the data, and moveRows not implemented, so fall back to default"
1496             // overriding the acceptProposedAction() below.
1497             // This special mime type tells KFilePlacesModel to ignore it.
1498             auto *mime = const_cast<QMimeData *>(event->mimeData());
1499             mime->setData(KFilePlacesModelPrivate::ignoreMimeType(), QByteArrayLiteral("1"));
1500             event->acceptProposedAction();
1501         }
1502     }
1503 
1504     QListView::dropEvent(event);
1505     d->m_dragging = false;
1506 
1507     if (d->m_dragActivationTimer) {
1508         d->m_dragActivationTimer->stop();
1509     }
1510     d->m_pendingDragActivation = QPersistentModelIndex();
1511 
1512     d->m_delegate->setShowHoverIndication(true);
1513 }
1514 
1515 void KFilePlacesView::paintEvent(QPaintEvent *event)
1516 {
1517     QListView::paintEvent(event);
1518     if (d->m_dragging && !d->m_dropRect.isEmpty()) {
1519         // draw drop indicator
1520         QPainter painter(viewport());
1521 
1522         QRect itemRect = visualRect(d->m_dropIndex);
1523         // Take into account section headers
1524         if (d->m_delegate->indexIsSectionHeader(d->m_dropIndex)) {
1525             const int headerHeight = d->m_delegate->sectionHeaderHeight(d->m_dropIndex);
1526             itemRect.translate(0, headerHeight);
1527             itemRect.setHeight(itemRect.height() - headerHeight);
1528         }
1529         const bool drawInsertIndicator = !d->m_dropOnPlace || d->m_dropRect.height() <= d->insertIndicatorHeight(itemRect.height());
1530 
1531         if (drawInsertIndicator) {
1532             // draw indicator for inserting items
1533             QStyleOptionViewItem viewOpts;
1534             initViewItemOption(&viewOpts);
1535 
1536             QBrush blendedBrush = viewOpts.palette.brush(QPalette::Normal, QPalette::Highlight);
1537             QColor color = blendedBrush.color();
1538 
1539             const int y = (d->m_dropRect.top() + d->m_dropRect.bottom()) / 2;
1540             const int thickness = d->m_dropRect.height() / 2;
1541             Q_ASSERT(thickness >= 1);
1542             int alpha = 255;
1543             const int alphaDec = alpha / (thickness + 1);
1544             for (int i = 0; i < thickness; i++) {
1545                 color.setAlpha(alpha);
1546                 alpha -= alphaDec;
1547                 painter.setPen(color);
1548                 painter.drawLine(d->m_dropRect.left(), y - i, d->m_dropRect.right(), y - i);
1549                 painter.drawLine(d->m_dropRect.left(), y + i, d->m_dropRect.right(), y + i);
1550             }
1551         } else {
1552             // draw indicator for copying/moving/linking to items
1553             QStyleOptionViewItem opt;
1554             opt.initFrom(this);
1555             opt.index = d->m_dropIndex;
1556             opt.rect = itemRect;
1557             opt.state = QStyle::State_Enabled | QStyle::State_MouseOver;
1558             style()->drawPrimitive(QStyle::PE_PanelItemViewItem, &opt, &painter, this);
1559         }
1560     }
1561 }
1562 
1563 void KFilePlacesView::startDrag(Qt::DropActions supportedActions)
1564 {
1565     d->m_delegate->startDrag();
1566     QListView::startDrag(supportedActions);
1567 }
1568 
1569 void KFilePlacesView::mousePressEvent(QMouseEvent *event)
1570 {
1571     if (event->button() == Qt::LeftButton) {
1572         // does not accept drags from section header area
1573         if (d->m_delegate->pointIsHeaderArea(event->pos())) {
1574             return;
1575         }
1576         // teardown button is handled by KFilePlacesEventWatcher
1577         // NOTE "mouseReleaseEvent" side is also in there.
1578         if (d->m_delegate->pointIsTeardownAction(event->pos())) {
1579             return;
1580         }
1581     }
1582     QListView::mousePressEvent(event);
1583 }
1584 
1585 void KFilePlacesView::setModel(QAbstractItemModel *model)
1586 {
1587     QListView::setModel(model);
1588     d->updateHiddenRows();
1589     // Uses Qt::QueuedConnection to delay the time when the slot will be
1590     // called. In case of an item move the remove+add will be done before
1591     // we adapt the item size (otherwise we'd get it wrong as we'd execute
1592     // it after the remove only).
1593     connect(
1594         model,
1595         &QAbstractItemModel::rowsRemoved,
1596         this,
1597         [this]() {
1598             d->adaptItemSize();
1599         },
1600         Qt::QueuedConnection);
1601 
1602     QObject::connect(qobject_cast<KFilePlacesModel *>(model), &KFilePlacesModel::setupDone, this, [this](const QModelIndex &idx, bool success) {
1603         d->storageSetupDone(idx, success);
1604     });
1605 
1606     d->m_delegate->clearFreeSpaceInfo();
1607 }
1608 
1609 void KFilePlacesView::rowsInserted(const QModelIndex &parent, int start, int end)
1610 {
1611     QListView::rowsInserted(parent, start, end);
1612     setUrl(d->m_currentUrl);
1613 
1614     KFilePlacesModel *placesModel = static_cast<KFilePlacesModel *>(model());
1615 
1616     for (int i = start; i <= end; ++i) {
1617         QModelIndex index = placesModel->index(i, 0, parent);
1618         if (d->m_showAll || !placesModel->isHidden(index)) {
1619             d->m_delegate->addAppearingItem(index);
1620             d->triggerItemAppearingAnimation();
1621         } else {
1622             setRowHidden(i, true);
1623         }
1624     }
1625 
1626     d->triggerItemAppearingAnimation();
1627 
1628     d->adaptItemSize();
1629 }
1630 
1631 QSize KFilePlacesView::sizeHint() const
1632 {
1633     KFilePlacesModel *placesModel = qobject_cast<KFilePlacesModel *>(model());
1634     if (!placesModel) {
1635         return QListView::sizeHint();
1636     }
1637     const int height = QListView::sizeHint().height();
1638     QFontMetrics fm = d->q->fontMetrics();
1639     int textWidth = 0;
1640 
1641     for (int i = 0; i < placesModel->rowCount(); ++i) {
1642         QModelIndex index = placesModel->index(i, 0);
1643         if (!placesModel->isHidden(index)) {
1644             textWidth = qMax(textWidth, fm.boundingRect(index.data(Qt::DisplayRole).toString()).width());
1645         }
1646     }
1647 
1648     const int iconSize = style()->pixelMetric(QStyle::PM_SmallIconSize) + 3 * s_lateralMargin;
1649     return QSize(iconSize + textWidth + fm.height() / 2, height);
1650 }
1651 
1652 void KFilePlacesViewPrivate::addDisappearingItem(KFilePlacesViewDelegate *delegate, const QModelIndex &index)
1653 {
1654     delegate->addDisappearingItem(index);
1655     if (m_itemDisappearTimeline.state() != QTimeLine::Running) {
1656         delegate->setDisappearingItemProgress(0.0);
1657         m_itemDisappearTimeline.start();
1658     }
1659 }
1660 
1661 void KFilePlacesViewPrivate::setCurrentIndex(const QModelIndex &index)
1662 {
1663     KFilePlacesModel *placesModel = qobject_cast<KFilePlacesModel *>(q->model());
1664 
1665     if (placesModel == nullptr) {
1666         return;
1667     }
1668 
1669     QUrl url = placesModel->url(index);
1670 
1671     if (url.isValid()) {
1672         m_currentUrl = url;
1673         updateHiddenRows();
1674         Q_EMIT q->urlChanged(KFilePlacesModel::convertedUrl(url));
1675     } else {
1676         q->setUrl(m_currentUrl);
1677     }
1678 }
1679 
1680 void KFilePlacesViewPrivate::adaptItemSize()
1681 {
1682     if (!m_autoResizeItems) {
1683         return;
1684     }
1685 
1686     KFilePlacesModel *placesModel = qobject_cast<KFilePlacesModel *>(q->model());
1687 
1688     if (placesModel == nullptr) {
1689         return;
1690     }
1691 
1692     int rowCount = placesModel->rowCount();
1693 
1694     if (!m_showAll) {
1695         rowCount -= placesModel->hiddenCount();
1696 
1697         QModelIndex current = placesModel->closestItem(m_currentUrl);
1698 
1699         if (placesModel->isHidden(current)) {
1700             ++rowCount;
1701         }
1702     }
1703 
1704     if (rowCount == 0) {
1705         return; // We've nothing to display anyway
1706     }
1707 
1708     const int minSize = q->style()->pixelMetric(QStyle::PM_SmallIconSize);
1709     const int maxSize = 64;
1710 
1711     int textWidth = 0;
1712     QFontMetrics fm = q->fontMetrics();
1713     for (int i = 0; i < placesModel->rowCount(); ++i) {
1714         QModelIndex index = placesModel->index(i, 0);
1715 
1716         if (!placesModel->isHidden(index)) {
1717             textWidth = qMax(textWidth, fm.boundingRect(index.data(Qt::DisplayRole).toString()).width());
1718         }
1719     }
1720 
1721     const int margin = q->style()->pixelMetric(QStyle::PM_FocusFrameHMargin, nullptr, q) + 1;
1722     const int maxWidth = q->viewport()->width() - textWidth - 4 * margin - 1;
1723 
1724     const int totalItemsHeight = (fm.height() / 2) * rowCount;
1725     const int totalSectionsHeight = m_delegate->sectionHeaderHeight(QModelIndex()) * sectionsCount();
1726     const int maxHeight = ((q->height() - totalSectionsHeight - totalItemsHeight) / rowCount) - 1;
1727 
1728     int size = qMin(maxHeight, maxWidth);
1729 
1730     if (size < minSize) {
1731         size = minSize;
1732     } else if (size > maxSize) {
1733         size = maxSize;
1734     } else {
1735         // Make it a multiple of 16
1736         size &= ~0xf;
1737     }
1738 
1739     relayoutIconSize(size);
1740 }
1741 
1742 void KFilePlacesViewPrivate::relayoutIconSize(const int size)
1743 {
1744     if (size == m_delegate->iconSize()) {
1745         return;
1746     }
1747 
1748     if (shouldAnimate() && m_smoothItemResizing) {
1749         m_oldSize = m_delegate->iconSize();
1750         m_endSize = size;
1751         if (m_adaptItemsTimeline.state() != QTimeLine::Running) {
1752             m_adaptItemsTimeline.start();
1753         }
1754     } else {
1755         m_delegate->setIconSize(size);
1756         if (shouldAnimate()) {
1757             q->scheduleDelayedItemsLayout();
1758         }
1759     }
1760 }
1761 
1762 void KFilePlacesViewPrivate::updateHiddenRows()
1763 {
1764     KFilePlacesModel *placesModel = qobject_cast<KFilePlacesModel *>(q->model());
1765 
1766     if (placesModel == nullptr) {
1767         return;
1768     }
1769 
1770     int rowCount = placesModel->rowCount();
1771     QModelIndex current = placesModel->closestItem(m_currentUrl);
1772 
1773     for (int i = 0; i < rowCount; ++i) {
1774         QModelIndex index = placesModel->index(i, 0);
1775         if (index != current && placesModel->isHidden(index) && !m_showAll) {
1776             q->setRowHidden(i, true);
1777         } else {
1778             q->setRowHidden(i, false);
1779         }
1780     }
1781 
1782     adaptItemSize();
1783 }
1784 
1785 bool KFilePlacesViewPrivate::insertAbove(const QDropEvent *event, const QRect &itemRect) const
1786 {
1787     if (m_dropOnPlace && !event->mimeData()->hasFormat(KFilePlacesModelPrivate::internalMimeType(qobject_cast<KFilePlacesModel *>(q->model())))) {
1788         return event->position().y() < itemRect.top() + insertIndicatorHeight(itemRect.height()) / 2;
1789     }
1790 
1791     return event->position().y() < itemRect.top() + (itemRect.height() / 2);
1792 }
1793 
1794 bool KFilePlacesViewPrivate::insertBelow(const QDropEvent *event, const QRect &itemRect) const
1795 {
1796     if (m_dropOnPlace && !event->mimeData()->hasFormat(KFilePlacesModelPrivate::internalMimeType(qobject_cast<KFilePlacesModel *>(q->model())))) {
1797         return event->position().y() > itemRect.bottom() - insertIndicatorHeight(itemRect.height()) / 2;
1798     }
1799 
1800     return event->position().y() >= itemRect.top() + (itemRect.height() / 2);
1801 }
1802 
1803 int KFilePlacesViewPrivate::insertIndicatorHeight(int itemHeight) const
1804 {
1805     const int min = 4;
1806     const int max = 12;
1807 
1808     int height = itemHeight / 4;
1809     if (height < min) {
1810         height = min;
1811     } else if (height > max) {
1812         height = max;
1813     }
1814     return height;
1815 }
1816 
1817 int KFilePlacesViewPrivate::sectionsCount() const
1818 {
1819     int count = 0;
1820     QString prevSection;
1821     const int rowCount = q->model()->rowCount();
1822 
1823     for (int i = 0; i < rowCount; i++) {
1824         if (!q->isRowHidden(i)) {
1825             const QModelIndex index = q->model()->index(i, 0);
1826             const QString sectionName = index.data(KFilePlacesModel::GroupRole).toString();
1827             if (prevSection != sectionName) {
1828                 prevSection = sectionName;
1829                 ++count;
1830             }
1831         }
1832     }
1833 
1834     return count;
1835 }
1836 
1837 void KFilePlacesViewPrivate::addPlace(const QModelIndex &index)
1838 {
1839     KFilePlacesModel *placesModel = qobject_cast<KFilePlacesModel *>(q->model());
1840 
1841     QUrl url = m_currentUrl;
1842     QString label;
1843     QString iconName = QStringLiteral("folder");
1844     bool appLocal = true;
1845     if (KFilePlaceEditDialog::getInformation(true, url, label, iconName, true, appLocal, 64, q)) {
1846         QString appName;
1847         if (appLocal) {
1848             appName = QCoreApplication::instance()->applicationName();
1849         }
1850 
1851         placesModel->addPlace(label, url, iconName, appName, index);
1852     }
1853 }
1854 
1855 void KFilePlacesViewPrivate::editPlace(const QModelIndex &index)
1856 {
1857     KFilePlacesModel *placesModel = qobject_cast<KFilePlacesModel *>(q->model());
1858 
1859     KBookmark bookmark = placesModel->bookmarkForIndex(index);
1860     QUrl url = bookmark.url();
1861     // KBookmark::text() would be untranslated for system bookmarks
1862     QString label = placesModel->text(index);
1863     QString iconName = bookmark.icon();
1864     bool appLocal = !bookmark.metaDataItem(QStringLiteral("OnlyInApp")).isEmpty();
1865 
1866     if (KFilePlaceEditDialog::getInformation(true, url, label, iconName, false, appLocal, 64, q)) {
1867         QString appName;
1868         if (appLocal) {
1869             appName = QCoreApplication::instance()->applicationName();
1870         }
1871 
1872         placesModel->editPlace(index, label, url, iconName, appName);
1873     }
1874 }
1875 
1876 bool KFilePlacesViewPrivate::shouldAnimate() const
1877 {
1878     return q->style()->styleHint(QStyle::SH_Widget_Animation_Duration, nullptr, q) > 0;
1879 }
1880 
1881 void KFilePlacesViewPrivate::triggerItemAppearingAnimation()
1882 {
1883     if (m_itemAppearTimeline.state() == QTimeLine::Running) {
1884         return;
1885     }
1886 
1887     if (shouldAnimate()) {
1888         m_delegate->setAppearingItemProgress(0.0);
1889         m_itemAppearTimeline.start();
1890     } else {
1891         itemAppearUpdate(1.0);
1892     }
1893 }
1894 
1895 void KFilePlacesViewPrivate::triggerItemDisappearingAnimation()
1896 {
1897     if (m_itemDisappearTimeline.state() == QTimeLine::Running) {
1898         return;
1899     }
1900 
1901     if (shouldAnimate()) {
1902         m_delegate->setDisappearingItemProgress(0.0);
1903         m_itemDisappearTimeline.start();
1904     } else {
1905         itemDisappearUpdate(1.0);
1906     }
1907 }
1908 
1909 void KFilePlacesViewPrivate::placeClicked(const QModelIndex &index, ActivationSignal activationSignal)
1910 {
1911     KFilePlacesModel *placesModel = qobject_cast<KFilePlacesModel *>(q->model());
1912 
1913     if (placesModel == nullptr) {
1914         return;
1915     }
1916 
1917     m_lastClickedIndex = QPersistentModelIndex();
1918     m_lastActivationSignal = nullptr;
1919 
1920     if (placesModel->setupNeeded(index)) {
1921         m_lastClickedIndex = index;
1922         m_lastActivationSignal = activationSignal;
1923         placesModel->requestSetup(index);
1924         return;
1925     }
1926 
1927     setCurrentIndex(index);
1928 
1929     const QUrl url = KFilePlacesModel::convertedUrl(placesModel->url(index));
1930 
1931     /*Q_EMIT*/ std::invoke(activationSignal, q, url);
1932 }
1933 
1934 void KFilePlacesViewPrivate::headerAreaEntered(const QModelIndex &index)
1935 {
1936     m_delegate->setHoveredHeaderArea(index);
1937     q->update(index);
1938 }
1939 
1940 void KFilePlacesViewPrivate::headerAreaLeft(const QModelIndex &index)
1941 {
1942     m_delegate->setHoveredHeaderArea(QModelIndex());
1943     q->update(index);
1944 }
1945 
1946 void KFilePlacesViewPrivate::actionClicked(const QModelIndex &index)
1947 {
1948     KFilePlacesModel *placesModel = qobject_cast<KFilePlacesModel *>(q->model());
1949     if (!placesModel) {
1950         return;
1951     }
1952 
1953     Solid::Device device = placesModel->deviceForIndex(index);
1954     if (device.is<Solid::OpticalDisc>()) {
1955         placesModel->requestEject(index);
1956     } else {
1957         teardown(index);
1958     }
1959 }
1960 
1961 void KFilePlacesViewPrivate::actionEntered(const QModelIndex &index)
1962 {
1963     m_delegate->setHoveredAction(index);
1964     q->update(index);
1965 }
1966 
1967 void KFilePlacesViewPrivate::actionLeft(const QModelIndex &index)
1968 {
1969     m_delegate->setHoveredAction(QModelIndex());
1970     q->update(index);
1971 }
1972 
1973 void KFilePlacesViewPrivate::teardown(const QModelIndex &index)
1974 {
1975     if (m_teardownFunction) {
1976         m_teardownFunction(index);
1977     } else if (auto *placesModel = qobject_cast<KFilePlacesModel *>(q->model())) {
1978         placesModel->requestTeardown(index);
1979     }
1980 }
1981 
1982 void KFilePlacesViewPrivate::storageSetupDone(const QModelIndex &index, bool success)
1983 {
1984     KFilePlacesModel *placesModel = static_cast<KFilePlacesModel *>(q->model());
1985 
1986     if (m_lastClickedIndex.isValid()) {
1987         if (m_lastClickedIndex == index) {
1988             if (success) {
1989                 setCurrentIndex(m_lastClickedIndex);
1990             } else {
1991                 q->setUrl(m_currentUrl);
1992             }
1993 
1994             const QUrl url = KFilePlacesModel::convertedUrl(placesModel->url(index));
1995             /*Q_EMIT*/ std::invoke(m_lastActivationSignal, q, url);
1996 
1997             m_lastClickedIndex = QPersistentModelIndex();
1998             m_lastActivationSignal = nullptr;
1999         }
2000     }
2001 
2002     if (m_pendingDropUrlsIndex.isValid() && m_dropUrlsEvent) {
2003         if (m_pendingDropUrlsIndex == index) {
2004             if (success) {
2005                 Q_EMIT q->urlsDropped(placesModel->url(index), m_dropUrlsEvent.get(), q);
2006             }
2007 
2008             m_pendingDropUrlsIndex = QPersistentModelIndex();
2009             m_dropUrlsEvent.reset();
2010             m_dropUrlsMimeData.reset();
2011         }
2012     }
2013 }
2014 
2015 void KFilePlacesViewPrivate::adaptItemsUpdate(qreal value)
2016 {
2017     const int add = (m_endSize - m_oldSize) * value;
2018     const int size = m_oldSize + add;
2019 
2020     m_delegate->setIconSize(size);
2021     q->scheduleDelayedItemsLayout();
2022 }
2023 
2024 void KFilePlacesViewPrivate::itemAppearUpdate(qreal value)
2025 {
2026     m_delegate->setAppearingItemProgress(value);
2027     q->scheduleDelayedItemsLayout();
2028 }
2029 
2030 void KFilePlacesViewPrivate::itemDisappearUpdate(qreal value)
2031 {
2032     m_delegate->setDisappearingItemProgress(value);
2033 
2034     if (value >= 1.0) {
2035         updateHiddenRows();
2036     }
2037 
2038     q->scheduleDelayedItemsLayout();
2039 }
2040 
2041 void KFilePlacesViewPrivate::enableSmoothItemResizing()
2042 {
2043     m_smoothItemResizing = true;
2044 }
2045 
2046 void KFilePlacesViewPrivate::deviceBusyAnimationValueChanged(const QVariant &value)
2047 {
2048     m_delegate->setDeviceBusyAnimationRotation(value.toReal());
2049     for (const auto &idx : std::as_const(m_busyDevices)) {
2050         q->update(idx);
2051     }
2052 }
2053 
2054 void KFilePlacesView::dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList<int> &roles)
2055 {
2056     QListView::dataChanged(topLeft, bottomRight, roles);
2057     d->adaptItemSize();
2058 
2059     if ((roles.isEmpty() || roles.contains(KFilePlacesModel::DeviceAccessibilityRole)) && d->shouldAnimate()) {
2060         QList<QPersistentModelIndex> busyDevices;
2061 
2062         auto *placesModel = qobject_cast<KFilePlacesModel *>(model());
2063         for (int i = 0; i < placesModel->rowCount(); ++i) {
2064             const QModelIndex idx = placesModel->index(i, 0);
2065             const auto accessibility = placesModel->deviceAccessibility(idx);
2066             if (accessibility == KFilePlacesModel::SetupInProgress || accessibility == KFilePlacesModel::TeardownInProgress) {
2067                 busyDevices.append(QPersistentModelIndex(idx));
2068             }
2069         }
2070 
2071         d->m_busyDevices = busyDevices;
2072 
2073         if (busyDevices.isEmpty()) {
2074             d->m_deviceBusyAnimation.stop();
2075         } else {
2076             d->m_deviceBusyAnimation.start();
2077         }
2078     }
2079 }
2080 
2081 #include "moc_kfileplacesview.cpp"
2082 #include "moc_kfileplacesview_p.cpp"