File indexing completed on 2025-04-27 13:10:02
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"