File indexing completed on 2024-10-06 03:39:42
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"