File indexing completed on 2024-04-28 15:26:43

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