1/*
2 This file is part of the KDE project
3 SPDX-FileCopyrightText: 2007 Kevin Ottens <ervin@kde.org>
4 SPDX-FileCopyrightText: 2008 Rafael Fernández López <ereslibre@kde.org>
5 SPDX-FileCopyrightText: 2022 Kai Uwe Broulik <kde@broulik.de>
6 SPDX-FileCopyrightText: 2023 Harald Sitter <sitter@kde.org>
7
8 SPDX-License-Identifier: LGPL-2.0-only
9*/
10
11#include "kfileplacesview.h"
12#include "kfileplacesmodel_p.h"
13#include "kfileplacesview_p.h"
14
15#include <QAbstractItemDelegate>
16#include <QActionGroup>
17#include <QApplication>
18#include <QDir>
19#include <QKeyEvent>
20#include <QMenu>
21#include <QMetaMethod>
22#include <QMimeData>
23#include <QPainter>
24#include <QPointer>
25#include <QScrollBar>
26#include <QScroller>
27#include <QTimeLine>
28#include <QTimer>
29#include <QToolTip>
30#include <QVariantAnimation>
31#include <QWindow>
32#include <kio/deleteortrashjob.h>
33
34#include <KColorScheme>
35#include <KColorUtils>
36#include <KConfig>
37#include <KConfigGroup>
38#include <KJob>
39#include <KLocalizedString>
40#include <KSharedConfig>
41#include <defaults-kfile.h> // ConfigGroup, PlacesIconsAutoresize, PlacesIconsStaticSize
42#include <kdirnotify.h>
43#include <kio/filesystemfreespacejob.h>
44#include <kmountpoint.h>
45#include <kpropertiesdialog.h>
46#include <solid/opticaldisc.h>
47#include <solid/opticaldrive.h>
48#include <solid/storageaccess.h>
49#include <solid/storagedrive.h>
50#include <solid/storagevolume.h>
51
52#include <chrono>
53#include <cmath>
54
55#include "kfileplaceeditdialog.h"
56#include "kfileplacesmodel.h"
57
58using namespace std::chrono_literals;
59
60static constexpr int s_lateralMargin = 4;
61static constexpr int s_capacitybarHeight = 6;
62static constexpr auto s_pollFreeSpaceInterval = 1min;
63
64KFilePlacesViewDelegate::KFilePlacesViewDelegate(KFilePlacesView *parent)
65 : QAbstractItemDelegate(parent)
66 , m_view(parent)
67 , m_iconSize(48)
68 , m_appearingHeightScale(1.0)
69 , m_appearingOpacity(0.0)
70 , m_disappearingHeightScale(1.0)
71 , m_disappearingOpacity(0.0)
72 , m_showHoverIndication(true)
73 , m_dragStarted(false)
74{
75 m_pollFreeSpace.setInterval(s_pollFreeSpaceInterval);
76 connect(sender: &m_pollFreeSpace, signal: &QTimer::timeout, context: this, slot: QOverload<>::of(ptr: &KFilePlacesViewDelegate::checkFreeSpace));
77}
78
79KFilePlacesViewDelegate::~KFilePlacesViewDelegate()
80{
81}
82
83QSize KFilePlacesViewDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
84{
85 int height = std::max(a: m_iconSize, b: option.fontMetrics.height()) + s_lateralMargin;
86
87 if (m_appearingItems.contains(t: index)) {
88 height *= m_appearingHeightScale;
89 } else if (m_disappearingItems.contains(t: index)) {
90 height *= m_disappearingHeightScale;
91 }
92
93 if (indexIsSectionHeader(index)) {
94 height += sectionHeaderHeight(index);
95 }
96
97 return QSize(option.rect.width(), height);
98}
99
100void KFilePlacesViewDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
101{
102 painter->save();
103
104 QStyleOptionViewItem opt = option;
105
106 const KFilePlacesModel *placesModel = static_cast<const KFilePlacesModel *>(index.model());
107
108 // draw header when necessary
109 if (indexIsSectionHeader(index)) {
110 // If we are drawing the floating element used by drag/drop, do not draw the header
111 if (!m_dragStarted) {
112 drawSectionHeader(painter, option: opt, index);
113 }
114
115 // Move the target rect to the actual item rect
116 const int headerHeight = sectionHeaderHeight(index);
117 opt.rect.translate(dx: 0, dy: headerHeight);
118 opt.rect.setHeight(opt.rect.height() - headerHeight);
119 }
120
121 // draw item
122 if (m_appearingItems.contains(t: index)) {
123 painter->setOpacity(m_appearingOpacity);
124 } else if (m_disappearingItems.contains(t: index)) {
125 painter->setOpacity(m_disappearingOpacity);
126 }
127
128 if (placesModel->isHidden(index)) {
129 painter->setOpacity(painter->opacity() * 0.6);
130 }
131
132 if (!m_showHoverIndication) {
133 opt.state &= ~QStyle::State_MouseOver;
134 }
135
136 if (opt.state & QStyle::State_MouseOver) {
137 if (index == m_hoveredHeaderArea) {
138 opt.state &= ~QStyle::State_MouseOver;
139 }
140 }
141
142 // Avoid a solid background for the drag pixmap so the drop indicator
143 // is more easily seen.
144 if (m_dragStarted) {
145 opt.state.setFlag(flag: QStyle::State_MouseOver, on: true);
146 opt.state.setFlag(flag: QStyle::State_Active, on: false);
147 opt.state.setFlag(flag: QStyle::State_Selected, on: false);
148 }
149
150 m_dragStarted = false;
151
152 QApplication::style()->drawPrimitive(pe: QStyle::PE_PanelItemViewItem, opt: &opt, p: painter);
153
154 const auto accessibility = placesModel->deviceAccessibility(index);
155 const bool isBusy = (accessibility == KFilePlacesModel::SetupInProgress || accessibility == KFilePlacesModel::TeardownInProgress);
156
157 QIcon actionIcon;
158 if (isBusy) {
159 actionIcon = QIcon::fromTheme(QStringLiteral("view-refresh"));
160 } else if (placesModel->isTeardownOverlayRecommended(index)) {
161 actionIcon = QIcon::fromTheme(QStringLiteral("media-eject"));
162 }
163
164 bool isLTR = opt.direction == Qt::LeftToRight;
165 const int iconAreaWidth = s_lateralMargin + m_iconSize;
166 const int actionAreaWidth = !actionIcon.isNull() ? s_lateralMargin + actionIconSize() : 0;
167 QRect rectText((isLTR ? iconAreaWidth : actionAreaWidth) + s_lateralMargin,
168 opt.rect.top(),
169 opt.rect.width() - iconAreaWidth - actionAreaWidth - 2 * s_lateralMargin,
170 opt.rect.height());
171
172 const QPalette activePalette = KIconLoader::global()->customPalette();
173 const bool changePalette = activePalette != opt.palette;
174 if (changePalette) {
175 KIconLoader::global()->setCustomPalette(opt.palette);
176 }
177
178 const bool selectedAndActive = (opt.state & QStyle::State_Selected) && (opt.state & QStyle::State_Active);
179 QIcon::Mode mode = selectedAndActive ? QIcon::Selected : QIcon::Normal;
180 QIcon icon = index.model()->data(index, role: Qt::DecorationRole).value<QIcon>();
181 QPixmap pm = icon.pixmap(w: m_iconSize, h: m_iconSize, mode);
182 QPoint point(isLTR ? opt.rect.left() + s_lateralMargin : opt.rect.right() - s_lateralMargin - m_iconSize,
183 opt.rect.top() + (opt.rect.height() - m_iconSize) / 2);
184 painter->drawPixmap(p: point, pm);
185
186 if (!actionIcon.isNull()) {
187 const int iconSize = actionIconSize();
188 QIcon::Mode mode = QIcon::Normal;
189 if (selectedAndActive) {
190 mode = QIcon::Selected;
191 } else if (m_hoveredAction == index) {
192 mode = QIcon::Active;
193 }
194
195 const QPixmap pixmap = actionIcon.pixmap(w: iconSize, h: iconSize, mode);
196
197 const QRectF rect(isLTR ? opt.rect.right() - actionAreaWidth : opt.rect.left() + s_lateralMargin,
198 opt.rect.top() + (opt.rect.height() - iconSize) / 2,
199 iconSize,
200 iconSize);
201
202 if (isBusy) {
203 painter->save();
204 painter->setRenderHint(hint: QPainter::SmoothPixmapTransform);
205 painter->translate(offset: rect.center());
206 painter->rotate(a: m_busyAnimationRotation);
207 painter->translate(offset: QPointF(-rect.width() / 2.0, -rect.height() / 2.0));
208 painter->drawPixmap(x: 0, y: 0, pm: pixmap);
209 painter->restore();
210 } else {
211 painter->drawPixmap(p: rect.topLeft(), pm: pixmap);
212 }
213 }
214
215 if (changePalette) {
216 if (activePalette == QPalette()) {
217 KIconLoader::global()->resetPalette();
218 } else {
219 KIconLoader::global()->setCustomPalette(activePalette);
220 }
221 }
222
223 if (selectedAndActive) {
224 painter->setPen(opt.palette.highlightedText().color());
225 } else {
226 painter->setPen(opt.palette.text().color());
227 }
228
229 if (placesModel->data(index, role: KFilePlacesModel::CapacityBarRecommendedRole).toBool()) {
230 QPersistentModelIndex persistentIndex(index);
231 const auto info = m_freeSpaceInfo.value(key: persistentIndex);
232
233 checkFreeSpace(index); // async
234
235 if (info.size > 0) {
236 const int capacityBarHeight = std::ceil(x: m_iconSize / 8.0);
237 const qreal usedSpace = info.used / qreal(info.size);
238
239 // Vertically center text + capacity bar, so move text up a bit
240 rectText.setTop(opt.rect.top() + (opt.rect.height() - opt.fontMetrics.height() - capacityBarHeight) / 2);
241 rectText.setHeight(opt.fontMetrics.height());
242
243 const int radius = capacityBarHeight / 2;
244 QRect capacityBgRect(rectText.x(), rectText.bottom(), rectText.width(), capacityBarHeight);
245 capacityBgRect.adjust(dx1: 0.5, dy1: 0.5, dx2: -0.5, dy2: -0.5);
246 QRect capacityFillRect = capacityBgRect;
247 capacityFillRect.setWidth(capacityFillRect.width() * usedSpace);
248
249 QPalette::ColorGroup cg = QPalette::Active;
250 if (!(opt.state & QStyle::State_Enabled)) {
251 cg = QPalette::Disabled;
252 } else if (!m_view->isActiveWindow()) {
253 cg = QPalette::Inactive;
254 }
255
256 // Adapted from Breeze style's progress bar rendering
257 QColor capacityBgColor(opt.palette.color(cr: QPalette::WindowText));
258 capacityBgColor.setAlphaF(0.2 * capacityBgColor.alphaF());
259
260 QColor capacityFgColor(selectedAndActive ? opt.palette.color(cg, cr: QPalette::HighlightedText) : opt.palette.color(cg, cr: QPalette::Highlight));
261 if (usedSpace > 0.95) {
262 if (!m_warningCapacityBarColor.isValid()) {
263 m_warningCapacityBarColor = KColorScheme(cg, KColorScheme::View).foreground(KColorScheme::NegativeText).color();
264 }
265 capacityFgColor = m_warningCapacityBarColor;
266 }
267
268 painter->save();
269
270 painter->setRenderHint(hint: QPainter::Antialiasing, on: true);
271 painter->setPen(Qt::NoPen);
272
273 painter->setBrush(capacityBgColor);
274 painter->drawRoundedRect(rect: capacityBgRect, xRadius: radius, yRadius: radius);
275
276 painter->setBrush(capacityFgColor);
277 painter->drawRoundedRect(rect: capacityFillRect, xRadius: radius, yRadius: radius);
278
279 painter->restore();
280 }
281 }
282
283 painter->drawText(r: rectText,
284 flags: Qt::AlignLeft | Qt::AlignVCenter,
285 text: opt.fontMetrics.elidedText(text: index.model()->data(index).toString(), mode: Qt::ElideRight, width: rectText.width()));
286
287 painter->restore();
288}
289
290bool KFilePlacesViewDelegate::helpEvent(QHelpEvent *event, QAbstractItemView *view, const QStyleOptionViewItem &option, const QModelIndex &index)
291{
292 if (event->type() == QHelpEvent::ToolTip) {
293 if (pointIsTeardownAction(pos: event->pos())) {
294 if (auto *placesModel = qobject_cast<const KFilePlacesModel *>(object: index.model())) {
295 Q_ASSERT(placesModel->isTeardownOverlayRecommended(index));
296
297 QString toolTipText;
298
299 if (auto eject = std::unique_ptr<QAction>{placesModel->ejectActionForIndex(index)}) {
300 toolTipText = eject->toolTip();
301 } else if (auto teardown = std::unique_ptr<QAction>{placesModel->teardownActionForIndex(index)}) {
302 toolTipText = teardown->toolTip();
303 }
304
305 if (!toolTipText.isEmpty()) {
306 // TODO rect
307 QToolTip::showText(pos: event->globalPos(), text: toolTipText, w: m_view);
308 event->setAccepted(true);
309 return true;
310 }
311 }
312 }
313 }
314 return QAbstractItemDelegate::helpEvent(event, view, option, index);
315}
316
317int KFilePlacesViewDelegate::iconSize() const
318{
319 return m_iconSize;
320}
321
322void KFilePlacesViewDelegate::setIconSize(int newSize)
323{
324 m_iconSize = newSize;
325}
326
327void KFilePlacesViewDelegate::addAppearingItem(const QModelIndex &index)
328{
329 m_appearingItems << index;
330}
331
332void KFilePlacesViewDelegate::setAppearingItemProgress(qreal value)
333{
334 if (value <= 0.25) {
335 m_appearingOpacity = 0.0;
336 m_appearingHeightScale = std::min(a: 1.0, b: value * 4);
337 } else {
338 m_appearingHeightScale = 1.0;
339 m_appearingOpacity = (value - 0.25) * 4 / 3;
340
341 if (value >= 1.0) {
342 m_appearingItems.clear();
343 }
344 }
345}
346
347void KFilePlacesViewDelegate::setDeviceBusyAnimationRotation(qreal angle)
348{
349 m_busyAnimationRotation = angle;
350}
351
352void KFilePlacesViewDelegate::addDisappearingItem(const QModelIndex &index)
353{
354 m_disappearingItems << index;
355}
356
357void KFilePlacesViewDelegate::addDisappearingItemGroup(const QModelIndex &index)
358{
359 const KFilePlacesModel *placesModel = static_cast<const KFilePlacesModel *>(index.model());
360 const QModelIndexList indexesGroup = placesModel->groupIndexes(type: placesModel->groupType(index));
361
362 m_disappearingItems.reserve(asize: m_disappearingItems.count() + indexesGroup.count());
363 std::transform(first: indexesGroup.begin(), last: indexesGroup.end(), result: std::back_inserter(x&: m_disappearingItems), unary_op: [](const QModelIndex &idx) {
364 return QPersistentModelIndex(idx);
365 });
366}
367
368void KFilePlacesViewDelegate::setDisappearingItemProgress(qreal value)
369{
370 value = 1.0 - value;
371
372 if (value <= 0.25) {
373 m_disappearingOpacity = 0.0;
374 m_disappearingHeightScale = std::min(a: 1.0, b: value * 4);
375
376 if (value <= 0.0) {
377 m_disappearingItems.clear();
378 }
379 } else {
380 m_disappearingHeightScale = 1.0;
381 m_disappearingOpacity = (value - 0.25) * 4 / 3;
382 }
383}
384
385void KFilePlacesViewDelegate::setShowHoverIndication(bool show)
386{
387 m_showHoverIndication = show;
388}
389
390void KFilePlacesViewDelegate::setHoveredHeaderArea(const QModelIndex &index)
391{
392 m_hoveredHeaderArea = index;
393}
394
395void KFilePlacesViewDelegate::setHoveredAction(const QModelIndex &index)
396{
397 m_hoveredAction = index;
398}
399
400bool KFilePlacesViewDelegate::pointIsHeaderArea(const QPoint &pos) const
401{
402 // we only accept drag events starting from item body, ignore drag request from header
403 QModelIndex index = m_view->indexAt(p: pos);
404 if (!index.isValid()) {
405 return false;
406 }
407
408 if (indexIsSectionHeader(index)) {
409 const QRect vRect = m_view->visualRect(index);
410 const int delegateY = pos.y() - vRect.y();
411 if (delegateY <= sectionHeaderHeight(index)) {
412 return true;
413 }
414 }
415 return false;
416}
417
418bool KFilePlacesViewDelegate::pointIsTeardownAction(const QPoint &pos) const
419{
420 QModelIndex index = m_view->indexAt(p: pos);
421 if (!index.isValid()) {
422 return false;
423 }
424
425 if (!index.data(arole: KFilePlacesModel::TeardownOverlayRecommendedRole).toBool()) {
426 return false;
427 }
428
429 const QRect vRect = m_view->visualRect(index);
430 const bool isLTR = m_view->layoutDirection() == Qt::LeftToRight;
431
432 const int delegateX = pos.x() - vRect.x();
433
434 if (isLTR) {
435 if (delegateX < (vRect.width() - 2 * s_lateralMargin - actionIconSize())) {
436 return false;
437 }
438 } else {
439 if (delegateX >= 2 * s_lateralMargin + actionIconSize()) {
440 return false;
441 }
442 }
443
444 return true;
445}
446
447void KFilePlacesViewDelegate::startDrag()
448{
449 m_dragStarted = true;
450}
451
452void KFilePlacesViewDelegate::checkFreeSpace()
453{
454 if (!m_view->model()) {
455 return;
456 }
457
458 bool hasChecked = false;
459
460 for (int i = 0; i < m_view->model()->rowCount(); ++i) {
461 if (m_view->isRowHidden(row: i)) {
462 continue;
463 }
464
465 const QModelIndex idx = m_view->model()->index(row: i, column: 0);
466 if (!idx.data(arole: KFilePlacesModel::CapacityBarRecommendedRole).toBool()) {
467 continue;
468 }
469
470 checkFreeSpace(index: idx);
471 hasChecked = true;
472 }
473
474 if (!hasChecked) {
475 // Stop timer, there are no more devices
476 stopPollingFreeSpace();
477 }
478}
479
480void KFilePlacesViewDelegate::startPollingFreeSpace() const
481{
482 if (m_pollFreeSpace.isActive()) {
483 return;
484 }
485
486 if (!m_view->isActiveWindow() || !m_view->isVisible()) {
487 return;
488 }
489
490 m_pollFreeSpace.start();
491}
492
493void KFilePlacesViewDelegate::stopPollingFreeSpace() const
494{
495 m_pollFreeSpace.stop();
496}
497
498void KFilePlacesViewDelegate::checkFreeSpace(const QModelIndex &index) const
499{
500 Q_ASSERT(index.data(KFilePlacesModel::CapacityBarRecommendedRole).toBool());
501
502 const QUrl url = index.data(arole: KFilePlacesModel::UrlRole).toUrl();
503
504 QPersistentModelIndex persistentIndex{index};
505
506 auto &info = m_freeSpaceInfo[persistentIndex];
507
508 if (info.job || !info.timeout.hasExpired()) {
509 return;
510 }
511
512 // Restarting timeout before job finishes, so that when we poll all devices
513 // and then get the result, the next poll will again update and not have
514 // a remaining time of 99% because it came in shortly afterwards.
515 // Also allow a bit of Timer slack.
516 info.timeout.setRemainingTime(remaining: s_pollFreeSpaceInterval - 100ms);
517
518 info.job = KIO::fileSystemFreeSpace(url);
519 QObject::connect(sender: info.job, signal: &KJob::result, context: this, slot: [this, info, persistentIndex]() {
520 if (!persistentIndex.isValid()) {
521 return;
522 }
523
524 const auto job = info.job;
525 if (job->error()) {
526 return;
527 }
528
529 PlaceFreeSpaceInfo &info = m_freeSpaceInfo[persistentIndex];
530
531 info.size = job->size();
532 info.used = job->size() - job->availableSize();
533
534 m_view->update(index: persistentIndex);
535 });
536
537 startPollingFreeSpace();
538}
539
540void KFilePlacesViewDelegate::clearFreeSpaceInfo()
541{
542 m_freeSpaceInfo.clear();
543}
544
545QString KFilePlacesViewDelegate::groupNameFromIndex(const QModelIndex &index) const
546{
547 if (index.isValid()) {
548 return index.data(arole: KFilePlacesModel::GroupRole).toString();
549 } else {
550 return QString();
551 }
552}
553
554QModelIndex KFilePlacesViewDelegate::previousVisibleIndex(const QModelIndex &index) const
555{
556 if (!index.isValid() || index.row() == 0) {
557 return QModelIndex();
558 }
559
560 const QAbstractItemModel *model = index.model();
561 QModelIndex prevIndex = model->index(row: index.row() - 1, column: index.column(), parent: index.parent());
562
563 while (m_view->isRowHidden(row: prevIndex.row())) {
564 if (prevIndex.row() == 0) {
565 return QModelIndex();
566 }
567 prevIndex = model->index(row: prevIndex.row() - 1, column: index.column(), parent: index.parent());
568 }
569
570 return prevIndex;
571}
572
573bool KFilePlacesViewDelegate::indexIsSectionHeader(const QModelIndex &index) const
574{
575 if (m_view->isRowHidden(row: index.row())) {
576 return false;
577 }
578
579 const auto groupName = groupNameFromIndex(index);
580 const auto previousGroupName = groupNameFromIndex(index: previousVisibleIndex(index));
581 return groupName != previousGroupName;
582}
583
584void KFilePlacesViewDelegate::drawSectionHeader(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
585{
586 const KFilePlacesModel *placesModel = static_cast<const KFilePlacesModel *>(index.model());
587
588 const QString groupLabel = index.data(arole: KFilePlacesModel::GroupRole).toString();
589 const QString category = placesModel->isGroupHidden(index)
590 // Avoid showing "(hidden)" during disappear animation when hiding a group
591 && !m_disappearingItems.contains(t: index)
592 ? i18n("%1 (hidden)", groupLabel)
593 : groupLabel;
594
595 QRect textRect(option.rect);
596 textRect.setLeft(textRect.left() + 6);
597 /* Take spacing into account:
598 The spacing to the previous section compensates for the spacing to the first item.*/
599 textRect.setY(textRect.y() /* + qMax(2, m_view->spacing()) - qMax(2, m_view->spacing())*/);
600 textRect.setHeight(sectionHeaderHeight(index) - s_lateralMargin - m_view->spacing());
601
602 painter->save();
603
604 // based on dolphin colors
605 const QColor c1 = textColor(option);
606 const QColor c2 = baseColor(option);
607 QColor penColor = mixedColor(c1, c2, c1Percent: 60);
608
609 painter->setPen(penColor);
610 painter->drawText(r: textRect, flags: Qt::AlignLeft | Qt::AlignBottom, text: option.fontMetrics.elidedText(text: category, mode: Qt::ElideRight, width: textRect.width()));
611 painter->restore();
612}
613
614void KFilePlacesViewDelegate::paletteChange()
615{
616 // Reset cache, will be re-created when painted
617 m_warningCapacityBarColor = QColor();
618}
619
620QColor KFilePlacesViewDelegate::textColor(const QStyleOption &option) const
621{
622 const QPalette::ColorGroup group = m_view->isActiveWindow() ? QPalette::Active : QPalette::Inactive;
623 return option.palette.color(cg: group, cr: QPalette::WindowText);
624}
625
626QColor KFilePlacesViewDelegate::baseColor(const QStyleOption &option) const
627{
628 const QPalette::ColorGroup group = m_view->isActiveWindow() ? QPalette::Active : QPalette::Inactive;
629 return option.palette.color(cg: group, cr: QPalette::Window);
630}
631
632QColor KFilePlacesViewDelegate::mixedColor(const QColor &c1, const QColor &c2, int c1Percent) const
633{
634 Q_ASSERT(c1Percent >= 0 && c1Percent <= 100);
635
636 const int c2Percent = 100 - c1Percent;
637 return QColor((c1.red() * c1Percent + c2.red() * c2Percent) / 100,
638 (c1.green() * c1Percent + c2.green() * c2Percent) / 100,
639 (c1.blue() * c1Percent + c2.blue() * c2Percent) / 100);
640}
641
642int KFilePlacesViewDelegate::sectionHeaderHeight(const QModelIndex &index) const
643{
644 Q_UNUSED(index);
645 // Account for the spacing between header and item
646 const int spacing = (s_lateralMargin + m_view->spacing());
647 int height = m_view->fontMetrics().height() + spacing;
648 height += 2 * spacing;
649 return height;
650}
651
652int KFilePlacesViewDelegate::actionIconSize() const
653{
654 return qApp->style()->pixelMetric(metric: QStyle::PM_SmallIconSize, option: nullptr, widget: m_view);
655}
656
657class KFilePlacesViewPrivate
658{
659public:
660 explicit KFilePlacesViewPrivate(KFilePlacesView *qq)
661 : q(qq)
662 , m_watcher(new KFilePlacesEventWatcher(q))
663 , m_delegate(new KFilePlacesViewDelegate(q))
664 {
665 }
666
667 using ActivationSignal = void (KFilePlacesView::*)(const QUrl &);
668
669 enum FadeType {
670 FadeIn = 0,
671 FadeOut,
672 };
673
674 void setCurrentIndex(const QModelIndex &index);
675 // If m_autoResizeItems is true, calculates a proper size for the icons in the places panel
676 void adaptItemSize();
677 void updateHiddenRows();
678 void clearFreeSpaceInfos();
679 bool insertAbove(const QDropEvent *event, const QRect &itemRect) const;
680 bool insertBelow(const QDropEvent *event, const QRect &itemRect) const;
681 int insertIndicatorHeight(int itemHeight) const;
682 int sectionsCount() const;
683
684 void addPlace(const QModelIndex &index);
685 void editPlace(const QModelIndex &index);
686
687 void addDisappearingItem(KFilePlacesViewDelegate *delegate, const QModelIndex &index);
688 void triggerItemAppearingAnimation();
689 void triggerItemDisappearingAnimation();
690 bool shouldAnimate() const;
691
692 void writeConfig();
693 void readConfig();
694 // Sets the size of the icons in the places panel
695 void relayoutIconSize(int size);
696 // Adds the "Icon Size" sub-menu items
697 void setupIconSizeSubMenu(QMenu *submenu);
698
699 void placeClicked(const QModelIndex &index, ActivationSignal activationSignal);
700 void headerAreaEntered(const QModelIndex &index);
701 void headerAreaLeft(const QModelIndex &index);
702 void actionClicked(const QModelIndex &index);
703 void actionEntered(const QModelIndex &index);
704 void actionLeft(const QModelIndex &index);
705 void teardown(const QModelIndex &index);
706 void storageSetupDone(const QModelIndex &index, bool success);
707 void adaptItemsUpdate(qreal value);
708 void itemAppearUpdate(qreal value);
709 void itemDisappearUpdate(qreal value);
710 void enableSmoothItemResizing();
711 void slotEmptyTrash();
712
713 void deviceBusyAnimationValueChanged(const QVariant &value);
714
715 KFilePlacesView *const q;
716
717 KFilePlacesEventWatcher *const m_watcher;
718 KFilePlacesViewDelegate *m_delegate;
719
720 Solid::StorageAccess *m_lastClickedStorage = nullptr;
721 QPersistentModelIndex m_lastClickedIndex;
722 ActivationSignal m_lastActivationSignal = nullptr;
723
724 QTimer *m_dragActivationTimer = nullptr;
725 QPersistentModelIndex m_pendingDragActivation;
726
727 QPersistentModelIndex m_pendingDropUrlsIndex;
728 std::unique_ptr<QDropEvent> m_dropUrlsEvent;
729 std::unique_ptr<QMimeData> m_dropUrlsMimeData;
730
731 KFilePlacesView::TeardownFunction m_teardownFunction = nullptr;
732
733 QTimeLine m_adaptItemsTimeline;
734 QTimeLine m_itemAppearTimeline;
735 QTimeLine m_itemDisappearTimeline;
736
737 QVariantAnimation m_deviceBusyAnimation;
738 QList<QPersistentModelIndex> m_busyDevices;
739
740 QRect m_dropRect;
741 QPersistentModelIndex m_dropIndex;
742
743 QUrl m_currentUrl;
744
745 int m_oldSize = 0;
746 int m_endSize = 0;
747
748 bool m_autoResizeItems = true;
749 bool m_smoothItemResizing = false;
750 bool m_showAll = false;
751 bool m_dropOnPlace = false;
752 bool m_dragging = false;
753};
754
755KFilePlacesView::KFilePlacesView(QWidget *parent)
756 : QListView(parent)
757 , d(std::make_unique<KFilePlacesViewPrivate>(args: this))
758{
759 setItemDelegate(d->m_delegate);
760
761 d->readConfig();
762
763 setSelectionRectVisible(false);
764 setSelectionMode(SingleSelection);
765
766 setDragEnabled(true);
767 setAcceptDrops(true);
768 setMouseTracking(true);
769 setDropIndicatorShown(false);
770 setFrameStyle(QFrame::NoFrame);
771
772 setResizeMode(Adjust);
773
774 QPalette palette = viewport()->palette();
775 palette.setColor(acr: viewport()->backgroundRole(), acolor: Qt::transparent);
776 palette.setColor(acr: viewport()->foregroundRole(), acolor: palette.color(cr: QPalette::WindowText));
777 viewport()->setPalette(palette);
778
779 setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
780
781 d->m_watcher->m_scroller = QScroller::scroller(target: viewport());
782 QScrollerProperties scrollerProp;
783 scrollerProp.setScrollMetric(metric: QScrollerProperties::AcceleratingFlickMaximumTime, value: 0.2); // QTBUG-88249
784 d->m_watcher->m_scroller->setScrollerProperties(scrollerProp);
785 d->m_watcher->m_scroller->grabGesture(target: viewport());
786 connect(sender: d->m_watcher->m_scroller, signal: &QScroller::stateChanged, context: d->m_watcher, slot: &KFilePlacesEventWatcher::qScrollerStateChanged);
787
788 setAttribute(Qt::WA_AcceptTouchEvents);
789 viewport()->grabGesture(type: Qt::TapGesture);
790 viewport()->grabGesture(type: Qt::TapAndHoldGesture);
791
792 // Note: Don't connect to the activated() signal, as the behavior when it is
793 // committed depends on the used widget style. The click behavior of
794 // KFilePlacesView should be style independent.
795 connect(sender: this, signal: &KFilePlacesView::clicked, context: this, slot: [this](const QModelIndex &index) {
796 const auto modifiers = qGuiApp->keyboardModifiers();
797 if (modifiers == (Qt::ControlModifier | Qt::ShiftModifier) && isSignalConnected(signal: QMetaMethod::fromSignal(signal: &KFilePlacesView::activeTabRequested))) {
798 d->placeClicked(index, activationSignal: &KFilePlacesView::activeTabRequested);
799 } else if (modifiers == Qt::ControlModifier && isSignalConnected(signal: QMetaMethod::fromSignal(signal: &KFilePlacesView::tabRequested))) {
800 d->placeClicked(index, activationSignal: &KFilePlacesView::tabRequested);
801 } else if (modifiers == Qt::ShiftModifier && isSignalConnected(signal: QMetaMethod::fromSignal(signal: &KFilePlacesView::newWindowRequested))) {
802 d->placeClicked(index, activationSignal: &KFilePlacesView::newWindowRequested);
803 } else {
804 d->placeClicked(index, activationSignal: &KFilePlacesView::placeActivated);
805 }
806 });
807
808 connect(sender: this, signal: &QAbstractItemView::iconSizeChanged, context: this, slot: [this](const QSize &newSize) {
809 d->m_autoResizeItems = (newSize.width() < 1 || newSize.height() < 1);
810
811 if (d->m_autoResizeItems) {
812 d->adaptItemSize();
813 } else {
814 const int iconSize = qMin(a: newSize.width(), b: newSize.height());
815 d->relayoutIconSize(size: iconSize);
816 }
817 d->writeConfig();
818 });
819
820 connect(sender: &d->m_adaptItemsTimeline, signal: &QTimeLine::valueChanged, context: this, slot: [this](qreal value) {
821 d->adaptItemsUpdate(value);
822 });
823 d->m_adaptItemsTimeline.setDuration(500);
824 d->m_adaptItemsTimeline.setUpdateInterval(5);
825 d->m_adaptItemsTimeline.setEasingCurve(QEasingCurve::InOutSine);
826
827 connect(sender: &d->m_itemAppearTimeline, signal: &QTimeLine::valueChanged, context: this, slot: [this](qreal value) {
828 d->itemAppearUpdate(value);
829 });
830 d->m_itemAppearTimeline.setDuration(500);
831 d->m_itemAppearTimeline.setUpdateInterval(5);
832 d->m_itemAppearTimeline.setEasingCurve(QEasingCurve::InOutSine);
833
834 connect(sender: &d->m_itemDisappearTimeline, signal: &QTimeLine::valueChanged, context: this, slot: [this](qreal value) {
835 d->itemDisappearUpdate(value);
836 });
837 d->m_itemDisappearTimeline.setDuration(500);
838 d->m_itemDisappearTimeline.setUpdateInterval(5);
839 d->m_itemDisappearTimeline.setEasingCurve(QEasingCurve::InOutSine);
840
841 // Adapted from KBusyIndicatorWidget
842 d->m_deviceBusyAnimation.setLoopCount(-1);
843 d->m_deviceBusyAnimation.setDuration(2000);
844 d->m_deviceBusyAnimation.setStartValue(0);
845 d->m_deviceBusyAnimation.setEndValue(360);
846 connect(sender: &d->m_deviceBusyAnimation, signal: &QVariantAnimation::valueChanged, context: this, slot: [this](const QVariant &value) {
847 d->deviceBusyAnimationValueChanged(value);
848 });
849
850 viewport()->installEventFilter(filterObj: d->m_watcher);
851 connect(sender: d->m_watcher, signal: &KFilePlacesEventWatcher::entryMiddleClicked, context: this, slot: [this](const QModelIndex &index) {
852 if (qGuiApp->keyboardModifiers() == Qt::ShiftModifier && isSignalConnected(signal: QMetaMethod::fromSignal(signal: &KFilePlacesView::activeTabRequested))) {
853 d->placeClicked(index, activationSignal: &KFilePlacesView::activeTabRequested);
854 } else if (isSignalConnected(signal: QMetaMethod::fromSignal(signal: &KFilePlacesView::tabRequested))) {
855 d->placeClicked(index, activationSignal: &KFilePlacesView::tabRequested);
856 } else {
857 d->placeClicked(index, activationSignal: &KFilePlacesView::placeActivated);
858 }
859 });
860
861 connect(sender: d->m_watcher, signal: &KFilePlacesEventWatcher::headerAreaEntered, context: this, slot: [this](const QModelIndex &index) {
862 d->headerAreaEntered(index);
863 });
864 connect(sender: d->m_watcher, signal: &KFilePlacesEventWatcher::headerAreaLeft, context: this, slot: [this](const QModelIndex &index) {
865 d->headerAreaLeft(index);
866 });
867
868 connect(sender: d->m_watcher, signal: &KFilePlacesEventWatcher::actionClicked, context: this, slot: [this](const QModelIndex &index) {
869 d->actionClicked(index);
870 });
871 connect(sender: d->m_watcher, signal: &KFilePlacesEventWatcher::actionEntered, context: this, slot: [this](const QModelIndex &index) {
872 d->actionEntered(index);
873 });
874 connect(sender: d->m_watcher, signal: &KFilePlacesEventWatcher::actionLeft, context: this, slot: [this](const QModelIndex &index) {
875 d->actionLeft(index);
876 });
877
878 connect(sender: d->m_watcher, signal: &KFilePlacesEventWatcher::windowActivated, context: this, slot: [this] {
879 d->m_delegate->checkFreeSpace();
880 // Start polling even if checkFreeSpace() wouldn't because we might just have checked
881 // free space before the timeout and so the poll timer would never get started again
882 d->m_delegate->startPollingFreeSpace();
883 });
884 connect(sender: d->m_watcher, signal: &KFilePlacesEventWatcher::windowDeactivated, context: this, slot: [this] {
885 d->m_delegate->stopPollingFreeSpace();
886 });
887
888 connect(sender: d->m_watcher, signal: &KFilePlacesEventWatcher::paletteChanged, context: this, slot: [this] {
889 d->m_delegate->paletteChange();
890 });
891
892 // FIXME: this is necessary to avoid flashes of black with some widget styles.
893 // could be a bug in Qt (e.g. QAbstractScrollArea) or KFilePlacesView, but has not
894 // yet been tracked down yet. until then, this works and is harmlessly enough.
895 // in fact, some QStyle (Oxygen, Skulpture, others?) do this already internally.
896 // See br #242358 for more information
897 verticalScrollBar()->setAttribute(Qt::WA_OpaquePaintEvent, on: false);
898}
899
900KFilePlacesView::~KFilePlacesView()
901{
902 viewport()->removeEventFilter(obj: d->m_watcher);
903}
904
905void KFilePlacesView::setDropOnPlaceEnabled(bool enabled)
906{
907 d->m_dropOnPlace = enabled;
908}
909
910bool KFilePlacesView::isDropOnPlaceEnabled() const
911{
912 return d->m_dropOnPlace;
913}
914
915void KFilePlacesView::setDragAutoActivationDelay(int delay)
916{
917 if (delay <= 0) {
918 delete d->m_dragActivationTimer;
919 d->m_dragActivationTimer = nullptr;
920 return;
921 }
922
923 if (!d->m_dragActivationTimer) {
924 d->m_dragActivationTimer = new QTimer(this);
925 d->m_dragActivationTimer->setSingleShot(true);
926 connect(sender: d->m_dragActivationTimer, signal: &QTimer::timeout, context: this, slot: [this] {
927 if (d->m_pendingDragActivation.isValid()) {
928 d->placeClicked(index: d->m_pendingDragActivation, activationSignal: &KFilePlacesView::placeActivated);
929 }
930 });
931 }
932 d->m_dragActivationTimer->setInterval(delay);
933}
934
935int KFilePlacesView::dragAutoActivationDelay() const
936{
937 return d->m_dragActivationTimer ? d->m_dragActivationTimer->interval() : 0;
938}
939
940void KFilePlacesView::setAutoResizeItemsEnabled(bool enabled)
941{
942 d->m_autoResizeItems = enabled;
943}
944
945bool KFilePlacesView::isAutoResizeItemsEnabled() const
946{
947 return d->m_autoResizeItems;
948}
949
950void KFilePlacesView::setTeardownFunction(TeardownFunction teardownFunc)
951{
952 d->m_teardownFunction = teardownFunc;
953}
954
955void KFilePlacesView::setUrl(const QUrl &url)
956{
957 KFilePlacesModel *placesModel = qobject_cast<KFilePlacesModel *>(object: model());
958
959 if (placesModel == nullptr) {
960 return;
961 }
962
963 QModelIndex index = placesModel->closestItem(url);
964 QModelIndex current = selectionModel()->currentIndex();
965
966 if (index.isValid()) {
967 if (current != index && placesModel->isHidden(index: current) && !d->m_showAll) {
968 d->addDisappearingItem(delegate: d->m_delegate, index: current);
969 }
970
971 if (current != index && placesModel->isHidden(index) && !d->m_showAll) {
972 d->m_delegate->addAppearingItem(index);
973 d->triggerItemAppearingAnimation();
974 setRowHidden(row: index.row(), hide: false);
975 }
976
977 d->m_currentUrl = url;
978
979 if (placesModel->url(index) == url.adjusted(options: QUrl::StripTrailingSlash)) {
980 selectionModel()->setCurrentIndex(index, command: QItemSelectionModel::ClearAndSelect);
981 } else {
982 selectionModel()->clear();
983 }
984 } else {
985 d->m_currentUrl = QUrl();
986 selectionModel()->clear();
987 }
988
989 if (!current.isValid()) {
990 d->updateHiddenRows();
991 }
992}
993
994bool KFilePlacesView::allPlacesShown() const
995{
996 return d->m_showAll;
997}
998
999void KFilePlacesView::setShowAll(bool showAll)
1000{
1001 KFilePlacesModel *placesModel = qobject_cast<KFilePlacesModel *>(object: model());
1002
1003 if (placesModel == nullptr) {
1004 return;
1005 }
1006
1007 d->m_showAll = showAll;
1008
1009 int rowCount = placesModel->rowCount();
1010 QModelIndex current = placesModel->closestItem(url: d->m_currentUrl);
1011
1012 if (showAll) {
1013 d->updateHiddenRows();
1014
1015 for (int i = 0; i < rowCount; ++i) {
1016 QModelIndex index = placesModel->index(row: i, column: 0);
1017 if (index != current && placesModel->isHidden(index)) {
1018 d->m_delegate->addAppearingItem(index);
1019 }
1020 }
1021 d->triggerItemAppearingAnimation();
1022 } else {
1023 for (int i = 0; i < rowCount; ++i) {
1024 QModelIndex index = placesModel->index(row: i, column: 0);
1025 if (index != current && placesModel->isHidden(index)) {
1026 d->m_delegate->addDisappearingItem(index);
1027 }
1028 }
1029 d->triggerItemDisappearingAnimation();
1030 }
1031
1032 Q_EMIT allPlacesShownChanged(allPlacesShown: showAll);
1033}
1034
1035void KFilePlacesView::keyPressEvent(QKeyEvent *event)
1036{
1037 QListView::keyPressEvent(event);
1038 if ((event->key() == Qt::Key_Return) || (event->key() == Qt::Key_Enter)) {
1039 // TODO Modifier keys for requesting tabs
1040 // Browsers do Ctrl+Click but *Alt*+Return for new tab
1041 d->placeClicked(index: currentIndex(), activationSignal: &KFilePlacesView::placeActivated);
1042 }
1043}
1044
1045void KFilePlacesViewPrivate::readConfig()
1046{
1047 KConfigGroup cg(KSharedConfig::openConfig(), ConfigGroup);
1048 m_autoResizeItems = cg.readEntry(key: PlacesIconsAutoresize, aDefault: true);
1049 m_delegate->setIconSize(cg.readEntry(key: PlacesIconsStaticSize, aDefault: static_cast<int>(KIconLoader::SizeMedium)));
1050}
1051
1052void KFilePlacesViewPrivate::writeConfig()
1053{
1054 KConfigGroup cg(KSharedConfig::openConfig(), ConfigGroup);
1055 cg.writeEntry(key: PlacesIconsAutoresize, value: m_autoResizeItems);
1056
1057 if (!m_autoResizeItems) {
1058 const int iconSize = qMin(a: q->iconSize().width(), b: q->iconSize().height());
1059 cg.writeEntry(key: PlacesIconsStaticSize, value: iconSize);
1060 }
1061
1062 cg.sync();
1063}
1064
1065void KFilePlacesViewPrivate::slotEmptyTrash()
1066{
1067 auto *parentWindow = q->window();
1068
1069 using AskIface = KIO::AskUserActionInterface;
1070 auto *emptyTrashJob = new KIO::DeleteOrTrashJob(QList<QUrl>{}, //
1071 AskIface::EmptyTrash,
1072 AskIface::DefaultConfirmation,
1073 parentWindow);
1074 emptyTrashJob->start();
1075}
1076
1077void KFilePlacesView::contextMenuEvent(QContextMenuEvent *event)
1078{
1079 KFilePlacesModel *placesModel = qobject_cast<KFilePlacesModel *>(object: model());
1080
1081 if (!placesModel) {
1082 return;
1083 }
1084
1085 QModelIndex index = event->reason() == QContextMenuEvent::Keyboard ? selectionModel()->currentIndex() : indexAt(p: event->pos());
1086 if (!selectedIndexes().contains(t: index)) {
1087 index = QModelIndex();
1088 }
1089 const QString groupName = index.data(arole: KFilePlacesModel::GroupRole).toString();
1090 const QUrl placeUrl = placesModel->url(index);
1091 const bool clickOverHeader = event->reason() == QContextMenuEvent::Keyboard ? false : d->m_delegate->pointIsHeaderArea(pos: event->pos());
1092 const bool clickOverEmptyArea = clickOverHeader || !index.isValid();
1093 const KFilePlacesModel::GroupType type = placesModel->groupType(index);
1094
1095 QMenu menu;
1096 // Polish before creating a native window below. The style could want change the surface format
1097 // of the window which will have no effect when the native window has already been created.
1098 menu.ensurePolished();
1099
1100 QAction *emptyTrash = nullptr;
1101 QAction *eject = nullptr;
1102 QAction *partition = nullptr;
1103 QAction *mount = nullptr;
1104 QAction *teardown = nullptr;
1105
1106 QAction *newTab = nullptr;
1107 QAction *newWindow = nullptr;
1108 QAction *highPriorityActionsPlaceholder = new QAction();
1109 QAction *properties = nullptr;
1110
1111 QAction *add = nullptr;
1112 QAction *edit = nullptr;
1113 QAction *remove = nullptr;
1114
1115 QAction *hide = nullptr;
1116 QAction *hideSection = nullptr;
1117 QAction *showAll = nullptr;
1118 QMenu *iconSizeMenu = nullptr;
1119
1120 if (!clickOverEmptyArea) {
1121 if (placeUrl.scheme() == QLatin1String("trash")) {
1122 emptyTrash = new QAction(QIcon::fromTheme(QStringLiteral("trash-empty")), i18nc("@action:inmenu", "Empty Trash"), &menu);
1123 KConfig trashConfig(QStringLiteral("trashrc"), KConfig::SimpleConfig);
1124 emptyTrash->setEnabled(!trashConfig.group(QStringLiteral("Status")).readEntry(key: "Empty", defaultValue: true));
1125 }
1126
1127 if (placesModel->isDevice(index)) {
1128 eject = placesModel->ejectActionForIndex(index);
1129 if (eject) {
1130 eject->setParent(&menu);
1131 }
1132
1133 partition = placesModel->partitionActionForIndex(index);
1134 if (partition) {
1135 partition->setParent(&menu);
1136 }
1137
1138 teardown = placesModel->teardownActionForIndex(index);
1139 if (teardown) {
1140 teardown->setParent(&menu);
1141 if (!placesModel->isTeardownAllowed(index)) {
1142 teardown->setEnabled(false);
1143 }
1144 }
1145
1146 if (placesModel->setupNeeded(index)) {
1147 mount = new QAction(QIcon::fromTheme(QStringLiteral("media-mount")), i18nc("@action:inmenu", "Mount"), &menu);
1148 }
1149 }
1150
1151 // TODO What about active tab?
1152 if (isSignalConnected(signal: QMetaMethod::fromSignal(signal: &KFilePlacesView::tabRequested))) {
1153 newTab = new QAction(QIcon::fromTheme(QStringLiteral("tab-new")), i18nc("@item:inmenu", "Open in New Tab"), &menu);
1154 }
1155 if (isSignalConnected(signal: QMetaMethod::fromSignal(signal: &KFilePlacesView::newWindowRequested))) {
1156 newWindow = new QAction(QIcon::fromTheme(QStringLiteral("window-new")), i18nc("@item:inmenu", "Open in New Window"), &menu);
1157 }
1158
1159 if (placeUrl.isLocalFile()) {
1160 properties = new QAction(QIcon::fromTheme(QStringLiteral("document-properties")), i18n("Properties"), &menu);
1161 }
1162 }
1163
1164 if (clickOverEmptyArea) {
1165 add = new QAction(QIcon::fromTheme(QStringLiteral("document-new")), i18nc("@action:inmenu", "Add Entry…"), &menu);
1166 }
1167
1168 if (index.isValid()) {
1169 if (!clickOverHeader) {
1170 if (!placesModel->isDevice(index)) {
1171 edit = new QAction(QIcon::fromTheme(QStringLiteral("edit-entry")), i18nc("@action:inmenu", "&Edit…"), &menu);
1172
1173 KBookmark bookmark = placesModel->bookmarkForIndex(index);
1174 const bool isSystemItem = bookmark.metaDataItem(QStringLiteral("isSystemItem")) == QLatin1String("true");
1175 if (!isSystemItem) {
1176 remove = new QAction(QIcon::fromTheme(QStringLiteral("bookmark-remove-symbolic")), i18nc("@action:inmenu", "Remove from Places"), &menu);
1177 }
1178 }
1179
1180 hide = new QAction(QIcon::fromTheme(QStringLiteral("hint")), i18nc("@action:inmenu", "&Hide"), &menu);
1181 hide->setCheckable(true);
1182 hide->setChecked(placesModel->isHidden(index));
1183 // if a parent is hidden no interaction should be possible with children, show it first to do so
1184 hide->setEnabled(!placesModel->isGroupHidden(type: placesModel->groupType(index)));
1185 }
1186
1187 hideSection = new QAction(QIcon::fromTheme(QStringLiteral("hint")),
1188 !groupName.isEmpty() ? i18nc("@item:inmenu", "Hide Section '%1'", groupName) : i18nc("@item:inmenu", "Hide Section"),
1189 &menu);
1190 hideSection->setCheckable(true);
1191 hideSection->setChecked(placesModel->isGroupHidden(type));
1192 }
1193
1194 if (clickOverEmptyArea) {
1195 if (placesModel->hiddenCount() > 0) {
1196 showAll = new QAction(QIcon::fromTheme(QStringLiteral("visibility")), i18n("&Show All Entries"), &menu);
1197 showAll->setCheckable(true);
1198 showAll->setChecked(d->m_showAll);
1199 }
1200
1201 iconSizeMenu = new QMenu(i18nc("@item:inmenu", "Icon Size"), &menu);
1202 d->setupIconSizeSubMenu(iconSizeMenu);
1203 }
1204
1205 auto addActionToMenu = [&menu](QAction *action) {
1206 if (action) { // silence warning when adding null action
1207 menu.addAction(action);
1208 }
1209 };
1210
1211 addActionToMenu(emptyTrash);
1212
1213 addActionToMenu(eject);
1214 addActionToMenu(mount);
1215 addActionToMenu(teardown);
1216 menu.addSeparator();
1217
1218 if (partition) {
1219 addActionToMenu(partition);
1220 menu.addSeparator();
1221 }
1222
1223 addActionToMenu(newTab);
1224 addActionToMenu(newWindow);
1225 addActionToMenu(highPriorityActionsPlaceholder);
1226 addActionToMenu(properties);
1227 menu.addSeparator();
1228
1229 addActionToMenu(add);
1230 addActionToMenu(edit);
1231 addActionToMenu(remove);
1232 addActionToMenu(hide);
1233 addActionToMenu(hideSection);
1234 addActionToMenu(showAll);
1235 if (iconSizeMenu) {
1236 menu.addMenu(menu: iconSizeMenu);
1237 }
1238
1239 menu.addSeparator();
1240
1241 // Clicking a header should be treated as clicking no device, hence passing an invalid model index
1242 // Emit the signal before adding any custom actions to give the user a chance to dynamically add/remove them
1243 Q_EMIT contextMenuAboutToShow(index: clickOverHeader ? QModelIndex() : index, menu: &menu);
1244
1245 const auto additionalActions = actions();
1246 for (QAction *action : additionalActions) {
1247 if (action->priority() == QAction::HighPriority) {
1248 menu.insertAction(before: highPriorityActionsPlaceholder, action);
1249 } else {
1250 menu.addAction(action);
1251 }
1252 }
1253 delete highPriorityActionsPlaceholder;
1254
1255 if (window()) {
1256 menu.winId();
1257 menu.windowHandle()->setTransientParent(window()->windowHandle());
1258 }
1259 QAction *result;
1260 if (event->reason() == QContextMenuEvent::Keyboard && index.isValid()) {
1261 const QRect rect = visualRect(index);
1262 result = menu.exec(pos: mapToGlobal(QPoint(rect.x() + rect.width() / 2, rect.y() + rect.height() * 0.9)));
1263 } else {
1264 result = menu.exec(pos: event->globalPos());
1265 }
1266
1267 if (result) {
1268 if (result == emptyTrash) {
1269 d->slotEmptyTrash();
1270
1271 } else if (result == eject) {
1272 placesModel->requestEject(index);
1273 } else if (result == mount) {
1274 placesModel->requestSetup(index);
1275 } else if (result == teardown) {
1276 d->teardown(index);
1277 } else if (result == newTab) {
1278 d->placeClicked(index, activationSignal: &KFilePlacesView::tabRequested);
1279 } else if (result == newWindow) {
1280 d->placeClicked(index, activationSignal: &KFilePlacesView::newWindowRequested);
1281 } else if (result == properties) {
1282 KPropertiesDialog::showDialog(url: placeUrl, parent: this);
1283 } else if (result == add) {
1284 d->addPlace(index);
1285 } else if (result == edit) {
1286 d->editPlace(index);
1287 } else if (result == remove) {
1288 placesModel->removePlace(index);
1289 } else if (result == hide) {
1290 placesModel->setPlaceHidden(index, hidden: hide->isChecked());
1291 QModelIndex current = placesModel->closestItem(url: d->m_currentUrl);
1292
1293 if (index != current && !d->m_showAll && hide->isChecked()) {
1294 d->m_delegate->addDisappearingItem(index);
1295 d->triggerItemDisappearingAnimation();
1296 }
1297 } else if (result == hideSection) {
1298 placesModel->setGroupHidden(type, hidden: hideSection->isChecked());
1299
1300 if (!d->m_showAll && hideSection->isChecked()) {
1301 d->m_delegate->addDisappearingItemGroup(index);
1302 d->triggerItemDisappearingAnimation();
1303 }
1304 } else if (result == showAll) {
1305 setShowAll(showAll->isChecked());
1306 }
1307 }
1308
1309 if (event->reason() != QContextMenuEvent::Keyboard) {
1310 index = placesModel->closestItem(url: d->m_currentUrl);
1311 selectionModel()->setCurrentIndex(index, command: QItemSelectionModel::ClearAndSelect);
1312 }
1313}
1314
1315void KFilePlacesViewPrivate::setupIconSizeSubMenu(QMenu *submenu)
1316{
1317 QActionGroup *group = new QActionGroup(submenu);
1318
1319 auto *autoAct = new QAction(i18nc("@item:inmenu Auto set icon size based on available space in"
1320 "the Places side-panel",
1321 "Auto Resize"),
1322 group);
1323 autoAct->setCheckable(true);
1324 autoAct->setChecked(m_autoResizeItems);
1325 QObject::connect(sender: autoAct, signal: &QAction::toggled, context: q, slot: [this]() {
1326 q->setIconSize(QSize(-1, -1));
1327 });
1328 submenu->addAction(action: autoAct);
1329
1330 static constexpr KIconLoader::StdSizes iconSizes[] = {KIconLoader::SizeSmall,
1331 KIconLoader::SizeSmallMedium,
1332 KIconLoader::SizeMedium,
1333 KIconLoader::SizeLarge};
1334
1335 for (const auto iconSize : iconSizes) {
1336 auto *act = new QAction(group);
1337 act->setCheckable(true);
1338
1339 switch (iconSize) {
1340 case KIconLoader::SizeSmall:
1341 act->setText(i18nc("Small icon size", "Small (%1x%1)", KIconLoader::SizeSmall));
1342 break;
1343 case KIconLoader::SizeSmallMedium:
1344 act->setText(i18nc("Medium icon size", "Medium (%1x%1)", KIconLoader::SizeSmallMedium));
1345 break;
1346 case KIconLoader::SizeMedium:
1347 act->setText(i18nc("Large icon size", "Large (%1x%1)", KIconLoader::SizeMedium));
1348 break;
1349 case KIconLoader::SizeLarge:
1350 act->setText(i18nc("Huge icon size", "Huge (%1x%1)", KIconLoader::SizeLarge));
1351 break;
1352 default:
1353 break;
1354 }
1355
1356 QObject::connect(sender: act, signal: &QAction::toggled, context: q, slot: [this, iconSize]() {
1357 q->setIconSize(QSize(iconSize, iconSize));
1358 });
1359
1360 if (!m_autoResizeItems) {
1361 act->setChecked(iconSize == m_delegate->iconSize());
1362 }
1363
1364 submenu->addAction(action: act);
1365 }
1366}
1367
1368void KFilePlacesView::resizeEvent(QResizeEvent *event)
1369{
1370 QListView::resizeEvent(e: event);
1371 d->adaptItemSize();
1372}
1373
1374void KFilePlacesView::showEvent(QShowEvent *event)
1375{
1376 QListView::showEvent(event);
1377
1378 d->m_delegate->checkFreeSpace();
1379 // Start polling even if checkFreeSpace() wouldn't because we might just have checked
1380 // free space before the timeout and so the poll timer would never get started again
1381 d->m_delegate->startPollingFreeSpace();
1382
1383 QTimer::singleShot(interval: 100, receiver: this, slot: [this]() {
1384 d->enableSmoothItemResizing();
1385 });
1386}
1387
1388void KFilePlacesView::hideEvent(QHideEvent *event)
1389{
1390 QListView::hideEvent(event);
1391 d->m_delegate->stopPollingFreeSpace();
1392 d->m_smoothItemResizing = false;
1393}
1394
1395void KFilePlacesView::dragEnterEvent(QDragEnterEvent *event)
1396{
1397 QListView::dragEnterEvent(event);
1398 d->m_dragging = true;
1399
1400 d->m_delegate->setShowHoverIndication(false);
1401
1402 d->m_dropRect = QRect();
1403 d->m_dropIndex = QPersistentModelIndex();
1404}
1405
1406void KFilePlacesView::dragLeaveEvent(QDragLeaveEvent *event)
1407{
1408 QListView::dragLeaveEvent(e: event);
1409 d->m_dragging = false;
1410
1411 d->m_delegate->setShowHoverIndication(true);
1412
1413 if (d->m_dragActivationTimer) {
1414 d->m_dragActivationTimer->stop();
1415 }
1416 d->m_pendingDragActivation = QPersistentModelIndex();
1417
1418 setDirtyRegion(d->m_dropRect);
1419}
1420
1421void KFilePlacesView::dragMoveEvent(QDragMoveEvent *event)
1422{
1423 QListView::dragMoveEvent(e: event);
1424
1425 bool autoActivate = false;
1426 // update the drop indicator
1427 const QPoint pos = event->position().toPoint();
1428 const QModelIndex index = indexAt(p: pos);
1429 setDirtyRegion(d->m_dropRect);
1430 if (index.isValid()) {
1431 d->m_dropIndex = index;
1432 const QRect rect = visualRect(index);
1433 const int gap = d->insertIndicatorHeight(itemHeight: rect.height());
1434
1435 if (d->insertAbove(event, itemRect: rect)) {
1436 // indicate that the item will be inserted above the current place
1437 d->m_dropRect = QRect(rect.left(), rect.top() - gap / 2, rect.width(), gap);
1438 } else if (d->insertBelow(event, itemRect: rect)) {
1439 // indicate that the item will be inserted below the current place
1440 d->m_dropRect = QRect(rect.left(), rect.bottom() + 1 - gap / 2, rect.width(), gap);
1441 } else {
1442 // indicate that the item be dropped above the current place
1443 d->m_dropRect = rect;
1444 // only auto-activate when dropping ontop of a place, not inbetween
1445 autoActivate = true;
1446 }
1447 }
1448
1449 if (d->m_dragActivationTimer) {
1450 if (autoActivate && !d->m_delegate->pointIsHeaderArea(pos: event->position().toPoint())) {
1451 QPersistentModelIndex persistentIndex(index);
1452 if (!d->m_pendingDragActivation.isValid() || d->m_pendingDragActivation != persistentIndex) {
1453 d->m_pendingDragActivation = persistentIndex;
1454 d->m_dragActivationTimer->start();
1455 }
1456 } else {
1457 d->m_dragActivationTimer->stop();
1458 d->m_pendingDragActivation = QPersistentModelIndex();
1459 }
1460 }
1461
1462 setDirtyRegion(d->m_dropRect);
1463}
1464
1465void KFilePlacesView::dropEvent(QDropEvent *event)
1466{
1467 const QModelIndex index = indexAt(p: event->position().toPoint());
1468 if (index.isValid()) {
1469 const QRect rect = visualRect(index);
1470 if (!d->insertAbove(event, itemRect: rect) && !d->insertBelow(event, itemRect: rect)) {
1471 KFilePlacesModel *placesModel = qobject_cast<KFilePlacesModel *>(object: model());
1472 Q_ASSERT(placesModel != nullptr);
1473 if (placesModel->setupNeeded(index)) {
1474 d->m_pendingDropUrlsIndex = index;
1475
1476 // Make a full copy of the Mime-Data
1477 d->m_dropUrlsMimeData = std::make_unique<QMimeData>();
1478 const auto formats = event->mimeData()->formats();
1479 for (const auto &format : formats) {
1480 d->m_dropUrlsMimeData->setData(mimetype: format, data: event->mimeData()->data(mimetype: format));
1481 }
1482
1483 d->m_dropUrlsEvent = std::make_unique<QDropEvent>(args: event->position(),
1484 args: event->possibleActions(),
1485 args: d->m_dropUrlsMimeData.get(),
1486 args: event->buttons(),
1487 args: event->modifiers());
1488
1489 placesModel->requestSetup(index);
1490 } else {
1491 Q_EMIT urlsDropped(dest: placesModel->url(index), event, parent: this);
1492 }
1493 // HACK Qt eventually calls into QAIM::dropMimeData when a drop event isn't
1494 // accepted by the view. However, QListView::dropEvent calls ignore() on our
1495 // event afterwards when
1496 // "icon view didn't move the data, and moveRows not implemented, so fall back to default"
1497 // overriding the acceptProposedAction() below.
1498 // This special mime type tells KFilePlacesModel to ignore it.
1499 auto *mime = const_cast<QMimeData *>(event->mimeData());
1500 mime->setData(mimetype: KFilePlacesModelPrivate::ignoreMimeType(), QByteArrayLiteral("1"));
1501 event->acceptProposedAction();
1502 }
1503 }
1504
1505 QListView::dropEvent(e: event);
1506 d->m_dragging = false;
1507
1508 if (d->m_dragActivationTimer) {
1509 d->m_dragActivationTimer->stop();
1510 }
1511 d->m_pendingDragActivation = QPersistentModelIndex();
1512
1513 d->m_delegate->setShowHoverIndication(true);
1514}
1515
1516void KFilePlacesView::paintEvent(QPaintEvent *event)
1517{
1518 QListView::paintEvent(e: event);
1519 if (d->m_dragging && !d->m_dropRect.isEmpty()) {
1520 // draw drop indicator
1521 QPainter painter(viewport());
1522
1523 QRect itemRect = visualRect(index: d->m_dropIndex);
1524 // Take into account section headers
1525 if (d->m_delegate->indexIsSectionHeader(index: d->m_dropIndex)) {
1526 const int headerHeight = d->m_delegate->sectionHeaderHeight(index: d->m_dropIndex);
1527 itemRect.translate(dx: 0, dy: headerHeight);
1528 itemRect.setHeight(itemRect.height() - headerHeight);
1529 }
1530 const bool drawInsertIndicator = !d->m_dropOnPlace || d->m_dropRect.height() <= d->insertIndicatorHeight(itemHeight: itemRect.height());
1531
1532 if (drawInsertIndicator) {
1533 // draw indicator for inserting items
1534 QStyleOptionViewItem viewOpts;
1535 initViewItemOption(option: &viewOpts);
1536
1537 QBrush blendedBrush = viewOpts.palette.brush(cg: QPalette::Normal, cr: QPalette::Highlight);
1538 QColor color = blendedBrush.color();
1539
1540 const int y = (d->m_dropRect.top() + d->m_dropRect.bottom()) / 2;
1541 const int thickness = d->m_dropRect.height() / 2;
1542 Q_ASSERT(thickness >= 1);
1543 int alpha = 255;
1544 const int alphaDec = alpha / (thickness + 1);
1545 for (int i = 0; i < thickness; i++) {
1546 color.setAlpha(alpha);
1547 alpha -= alphaDec;
1548 painter.setPen(color);
1549 painter.drawLine(x1: d->m_dropRect.left(), y1: y - i, x2: d->m_dropRect.right(), y2: y - i);
1550 painter.drawLine(x1: d->m_dropRect.left(), y1: y + i, x2: d->m_dropRect.right(), y2: y + i);
1551 }
1552 } else {
1553 // draw indicator for copying/moving/linking to items
1554 QStyleOptionViewItem opt;
1555 opt.initFrom(w: this);
1556 opt.index = d->m_dropIndex;
1557 opt.rect = itemRect;
1558 opt.state = QStyle::State_Enabled | QStyle::State_MouseOver;
1559 style()->drawPrimitive(pe: QStyle::PE_PanelItemViewItem, opt: &opt, p: &painter, w: this);
1560 }
1561 }
1562}
1563
1564void KFilePlacesView::startDrag(Qt::DropActions supportedActions)
1565{
1566 d->m_delegate->startDrag();
1567 QListView::startDrag(supportedActions);
1568}
1569
1570void KFilePlacesView::mousePressEvent(QMouseEvent *event)
1571{
1572 if (event->button() == Qt::LeftButton) {
1573 // does not accept drags from section header area
1574 if (d->m_delegate->pointIsHeaderArea(pos: event->pos())) {
1575 return;
1576 }
1577 // teardown button is handled by KFilePlacesEventWatcher
1578 // NOTE "mouseReleaseEvent" side is also in there.
1579 if (d->m_delegate->pointIsTeardownAction(pos: event->pos())) {
1580 return;
1581 }
1582 }
1583 QListView::mousePressEvent(event);
1584}
1585
1586void KFilePlacesView::setModel(QAbstractItemModel *model)
1587{
1588 QListView::setModel(model);
1589 d->updateHiddenRows();
1590 // Uses Qt::QueuedConnection to delay the time when the slot will be
1591 // called. In case of an item move the remove+add will be done before
1592 // we adapt the item size (otherwise we'd get it wrong as we'd execute
1593 // it after the remove only).
1594 connect(
1595 sender: model,
1596 signal: &QAbstractItemModel::rowsRemoved,
1597 context: this,
1598 slot: [this]() {
1599 d->adaptItemSize();
1600 },
1601 type: Qt::QueuedConnection);
1602
1603 QObject::connect(sender: qobject_cast<KFilePlacesModel *>(object: model), signal: &KFilePlacesModel::setupDone, context: this, slot: [this](const QModelIndex &idx, bool success) {
1604 d->storageSetupDone(index: idx, success);
1605 });
1606
1607 d->m_delegate->clearFreeSpaceInfo();
1608}
1609
1610void KFilePlacesView::rowsInserted(const QModelIndex &parent, int start, int end)
1611{
1612 QListView::rowsInserted(parent, start, end);
1613 setUrl(d->m_currentUrl);
1614
1615 KFilePlacesModel *placesModel = static_cast<KFilePlacesModel *>(model());
1616
1617 for (int i = start; i <= end; ++i) {
1618 QModelIndex index = placesModel->index(row: i, column: 0, parent);
1619 if (d->m_showAll || !placesModel->isHidden(index)) {
1620 d->m_delegate->addAppearingItem(index);
1621 d->triggerItemAppearingAnimation();
1622 } else {
1623 setRowHidden(row: i, hide: true);
1624 }
1625 }
1626
1627 d->triggerItemAppearingAnimation();
1628
1629 d->adaptItemSize();
1630}
1631
1632QSize KFilePlacesView::sizeHint() const
1633{
1634 KFilePlacesModel *placesModel = qobject_cast<KFilePlacesModel *>(object: model());
1635 if (!placesModel) {
1636 return QListView::sizeHint();
1637 }
1638 const int height = QListView::sizeHint().height();
1639 QFontMetrics fm = d->q->fontMetrics();
1640 int textWidth = 0;
1641
1642 for (int i = 0; i < placesModel->rowCount(); ++i) {
1643 QModelIndex index = placesModel->index(row: i, column: 0);
1644 if (!placesModel->isHidden(index)) {
1645 textWidth = qMax(a: textWidth, b: fm.boundingRect(text: index.data(arole: Qt::DisplayRole).toString()).width());
1646 }
1647 }
1648
1649 const int iconSize = style()->pixelMetric(metric: QStyle::PM_SmallIconSize) + 3 * s_lateralMargin;
1650 return QSize(iconSize + textWidth + fm.height() / 2, height);
1651}
1652
1653void KFilePlacesViewPrivate::addDisappearingItem(KFilePlacesViewDelegate *delegate, const QModelIndex &index)
1654{
1655 delegate->addDisappearingItem(index);
1656 if (m_itemDisappearTimeline.state() != QTimeLine::Running) {
1657 delegate->setDisappearingItemProgress(0.0);
1658 m_itemDisappearTimeline.start();
1659 }
1660}
1661
1662void KFilePlacesViewPrivate::setCurrentIndex(const QModelIndex &index)
1663{
1664 KFilePlacesModel *placesModel = qobject_cast<KFilePlacesModel *>(object: q->model());
1665
1666 if (placesModel == nullptr) {
1667 return;
1668 }
1669
1670 QUrl url = placesModel->url(index);
1671
1672 if (url.isValid()) {
1673 m_currentUrl = url;
1674 updateHiddenRows();
1675 Q_EMIT q->urlChanged(url: KFilePlacesModel::convertedUrl(url));
1676 } else {
1677 q->setUrl(m_currentUrl);
1678 }
1679}
1680
1681void KFilePlacesViewPrivate::adaptItemSize()
1682{
1683 if (!m_autoResizeItems) {
1684 return;
1685 }
1686
1687 KFilePlacesModel *placesModel = qobject_cast<KFilePlacesModel *>(object: q->model());
1688
1689 if (placesModel == nullptr) {
1690 return;
1691 }
1692
1693 int rowCount = placesModel->rowCount();
1694
1695 if (!m_showAll) {
1696 rowCount -= placesModel->hiddenCount();
1697
1698 QModelIndex current = placesModel->closestItem(url: m_currentUrl);
1699
1700 if (placesModel->isHidden(index: current)) {
1701 ++rowCount;
1702 }
1703 }
1704
1705 if (rowCount == 0) {
1706 return; // We've nothing to display anyway
1707 }
1708
1709 const int minSize = q->style()->pixelMetric(metric: QStyle::PM_SmallIconSize);
1710 const int maxSize = 64;
1711
1712 int textWidth = 0;
1713 QFontMetrics fm = q->fontMetrics();
1714 for (int i = 0; i < placesModel->rowCount(); ++i) {
1715 QModelIndex index = placesModel->index(row: i, column: 0);
1716
1717 if (!placesModel->isHidden(index)) {
1718 textWidth = qMax(a: textWidth, b: fm.boundingRect(text: index.data(arole: Qt::DisplayRole).toString()).width());
1719 }
1720 }
1721
1722 const int margin = q->style()->pixelMetric(metric: QStyle::PM_FocusFrameHMargin, option: nullptr, widget: q) + 1;
1723 const int maxWidth = q->viewport()->width() - textWidth - 4 * margin - 1;
1724
1725 const int totalItemsHeight = (fm.height() / 2) * rowCount;
1726 const int totalSectionsHeight = m_delegate->sectionHeaderHeight(index: QModelIndex()) * sectionsCount();
1727 const int maxHeight = ((q->height() - totalSectionsHeight - totalItemsHeight) / rowCount) - 1;
1728
1729 int size = qMin(a: maxHeight, b: maxWidth);
1730
1731 if (size < minSize) {
1732 size = minSize;
1733 } else if (size > maxSize) {
1734 size = maxSize;
1735 } else {
1736 // Make it a multiple of 16
1737 size &= ~0xf;
1738 }
1739
1740 relayoutIconSize(size);
1741}
1742
1743void KFilePlacesViewPrivate::relayoutIconSize(const int size)
1744{
1745 if (size == m_delegate->iconSize()) {
1746 return;
1747 }
1748
1749 if (shouldAnimate() && m_smoothItemResizing) {
1750 m_oldSize = m_delegate->iconSize();
1751 m_endSize = size;
1752 if (m_adaptItemsTimeline.state() != QTimeLine::Running) {
1753 m_adaptItemsTimeline.start();
1754 }
1755 } else {
1756 m_delegate->setIconSize(size);
1757 if (shouldAnimate()) {
1758 q->scheduleDelayedItemsLayout();
1759 }
1760 }
1761}
1762
1763void KFilePlacesViewPrivate::updateHiddenRows()
1764{
1765 KFilePlacesModel *placesModel = qobject_cast<KFilePlacesModel *>(object: q->model());
1766
1767 if (placesModel == nullptr) {
1768 return;
1769 }
1770
1771 int rowCount = placesModel->rowCount();
1772 QModelIndex current = placesModel->closestItem(url: m_currentUrl);
1773
1774 for (int i = 0; i < rowCount; ++i) {
1775 QModelIndex index = placesModel->index(row: i, column: 0);
1776 if (index != current && placesModel->isHidden(index) && !m_showAll) {
1777 q->setRowHidden(row: i, hide: true);
1778 } else {
1779 q->setRowHidden(row: i, hide: false);
1780 }
1781 }
1782
1783 adaptItemSize();
1784}
1785
1786bool KFilePlacesViewPrivate::insertAbove(const QDropEvent *event, const QRect &itemRect) const
1787{
1788 if (m_dropOnPlace && !event->mimeData()->hasFormat(mimetype: KFilePlacesModelPrivate::internalMimeType(model: qobject_cast<KFilePlacesModel *>(object: q->model())))) {
1789 return event->position().y() < itemRect.top() + insertIndicatorHeight(itemHeight: itemRect.height()) / 2;
1790 }
1791
1792 return event->position().y() < itemRect.top() + (itemRect.height() / 2);
1793}
1794
1795bool KFilePlacesViewPrivate::insertBelow(const QDropEvent *event, const QRect &itemRect) const
1796{
1797 if (m_dropOnPlace && !event->mimeData()->hasFormat(mimetype: KFilePlacesModelPrivate::internalMimeType(model: qobject_cast<KFilePlacesModel *>(object: q->model())))) {
1798 return event->position().y() > itemRect.bottom() - insertIndicatorHeight(itemHeight: itemRect.height()) / 2;
1799 }
1800
1801 return event->position().y() >= itemRect.top() + (itemRect.height() / 2);
1802}
1803
1804int KFilePlacesViewPrivate::insertIndicatorHeight(int itemHeight) const
1805{
1806 const int min = 4;
1807 const int max = 12;
1808
1809 int height = itemHeight / 4;
1810 if (height < min) {
1811 height = min;
1812 } else if (height > max) {
1813 height = max;
1814 }
1815 return height;
1816}
1817
1818int KFilePlacesViewPrivate::sectionsCount() const
1819{
1820 int count = 0;
1821 QString prevSection;
1822 const int rowCount = q->model()->rowCount();
1823
1824 for (int i = 0; i < rowCount; i++) {
1825 if (!q->isRowHidden(row: i)) {
1826 const QModelIndex index = q->model()->index(row: i, column: 0);
1827 const QString sectionName = index.data(arole: KFilePlacesModel::GroupRole).toString();
1828 if (prevSection != sectionName) {
1829 prevSection = sectionName;
1830 ++count;
1831 }
1832 }
1833 }
1834
1835 return count;
1836}
1837
1838void KFilePlacesViewPrivate::addPlace(const QModelIndex &index)
1839{
1840 KFilePlacesModel *placesModel = qobject_cast<KFilePlacesModel *>(object: q->model());
1841
1842 QUrl url = m_currentUrl;
1843 QString label;
1844 QString iconName = QStringLiteral("folder");
1845 bool appLocal = true;
1846 if (KFilePlaceEditDialog::getInformation(allowGlobal: true, url, label, icon&: iconName, isAddingNewPlace: true, appLocal, iconSize: 64, parent: q)) {
1847 QString appName;
1848 if (appLocal) {
1849 appName = QCoreApplication::instance()->applicationName();
1850 }
1851
1852 placesModel->addPlace(text: label, url, iconName, appName, after: index);
1853 }
1854}
1855
1856void KFilePlacesViewPrivate::editPlace(const QModelIndex &index)
1857{
1858 KFilePlacesModel *placesModel = qobject_cast<KFilePlacesModel *>(object: q->model());
1859
1860 KBookmark bookmark = placesModel->bookmarkForIndex(index);
1861 QUrl url = bookmark.url();
1862 // KBookmark::text() would be untranslated for system bookmarks
1863 QString label = placesModel->text(index);
1864 QString iconName = bookmark.icon();
1865 bool appLocal = !bookmark.metaDataItem(QStringLiteral("OnlyInApp")).isEmpty();
1866
1867 if (KFilePlaceEditDialog::getInformation(allowGlobal: true, url, label, icon&: iconName, isAddingNewPlace: false, appLocal, iconSize: 64, parent: q)) {
1868 QString appName;
1869 if (appLocal) {
1870 appName = QCoreApplication::instance()->applicationName();
1871 }
1872
1873 placesModel->editPlace(index, text: label, url, iconName, appName);
1874 }
1875}
1876
1877bool KFilePlacesViewPrivate::shouldAnimate() const
1878{
1879 return q->style()->styleHint(stylehint: QStyle::SH_Widget_Animation_Duration, opt: nullptr, widget: q) > 0;
1880}
1881
1882void KFilePlacesViewPrivate::triggerItemAppearingAnimation()
1883{
1884 if (m_itemAppearTimeline.state() == QTimeLine::Running) {
1885 return;
1886 }
1887
1888 if (shouldAnimate()) {
1889 m_delegate->setAppearingItemProgress(0.0);
1890 m_itemAppearTimeline.start();
1891 } else {
1892 itemAppearUpdate(value: 1.0);
1893 }
1894}
1895
1896void KFilePlacesViewPrivate::triggerItemDisappearingAnimation()
1897{
1898 if (m_itemDisappearTimeline.state() == QTimeLine::Running) {
1899 return;
1900 }
1901
1902 if (shouldAnimate()) {
1903 m_delegate->setDisappearingItemProgress(0.0);
1904 m_itemDisappearTimeline.start();
1905 } else {
1906 itemDisappearUpdate(value: 1.0);
1907 }
1908}
1909
1910void KFilePlacesViewPrivate::placeClicked(const QModelIndex &index, ActivationSignal activationSignal)
1911{
1912 KFilePlacesModel *placesModel = qobject_cast<KFilePlacesModel *>(object: q->model());
1913
1914 if (placesModel == nullptr) {
1915 return;
1916 }
1917
1918 m_lastClickedIndex = QPersistentModelIndex();
1919 m_lastActivationSignal = nullptr;
1920
1921 if (placesModel->setupNeeded(index)) {
1922 m_lastClickedIndex = index;
1923 m_lastActivationSignal = activationSignal;
1924 placesModel->requestSetup(index);
1925 return;
1926 }
1927
1928 setCurrentIndex(index);
1929
1930 const QUrl url = KFilePlacesModel::convertedUrl(url: placesModel->url(index));
1931
1932 /*Q_EMIT*/ std::invoke(fn&: activationSignal, args: q, args: url);
1933}
1934
1935void KFilePlacesViewPrivate::headerAreaEntered(const QModelIndex &index)
1936{
1937 m_delegate->setHoveredHeaderArea(index);
1938 q->update(index);
1939}
1940
1941void KFilePlacesViewPrivate::headerAreaLeft(const QModelIndex &index)
1942{
1943 m_delegate->setHoveredHeaderArea(QModelIndex());
1944 q->update(index);
1945}
1946
1947void KFilePlacesViewPrivate::actionClicked(const QModelIndex &index)
1948{
1949 KFilePlacesModel *placesModel = qobject_cast<KFilePlacesModel *>(object: q->model());
1950 if (!placesModel) {
1951 return;
1952 }
1953
1954 Solid::Device device = placesModel->deviceForIndex(index);
1955 if (device.is<Solid::OpticalDisc>()) {
1956 placesModel->requestEject(index);
1957 } else {
1958 teardown(index);
1959 }
1960}
1961
1962void KFilePlacesViewPrivate::actionEntered(const QModelIndex &index)
1963{
1964 m_delegate->setHoveredAction(index);
1965 q->update(index);
1966}
1967
1968void KFilePlacesViewPrivate::actionLeft(const QModelIndex &index)
1969{
1970 m_delegate->setHoveredAction(QModelIndex());
1971 q->update(index);
1972}
1973
1974void KFilePlacesViewPrivate::teardown(const QModelIndex &index)
1975{
1976 if (m_teardownFunction) {
1977 m_teardownFunction(index);
1978 } else if (auto *placesModel = qobject_cast<KFilePlacesModel *>(object: q->model())) {
1979 placesModel->requestTeardown(index);
1980 }
1981}
1982
1983void KFilePlacesViewPrivate::storageSetupDone(const QModelIndex &index, bool success)
1984{
1985 KFilePlacesModel *placesModel = static_cast<KFilePlacesModel *>(q->model());
1986
1987 if (m_lastClickedIndex.isValid()) {
1988 if (m_lastClickedIndex == index) {
1989 if (success) {
1990 setCurrentIndex(m_lastClickedIndex);
1991 } else {
1992 q->setUrl(m_currentUrl);
1993 }
1994
1995 const QUrl url = KFilePlacesModel::convertedUrl(url: placesModel->url(index));
1996 /*Q_EMIT*/ std::invoke(fn&: m_lastActivationSignal, args: q, args: url);
1997
1998 m_lastClickedIndex = QPersistentModelIndex();
1999 m_lastActivationSignal = nullptr;
2000 }
2001 }
2002
2003 if (m_pendingDropUrlsIndex.isValid() && m_dropUrlsEvent) {
2004 if (m_pendingDropUrlsIndex == index) {
2005 if (success) {
2006 Q_EMIT q->urlsDropped(dest: placesModel->url(index), event: m_dropUrlsEvent.get(), parent: q);
2007 }
2008
2009 m_pendingDropUrlsIndex = QPersistentModelIndex();
2010 m_dropUrlsEvent.reset();
2011 m_dropUrlsMimeData.reset();
2012 }
2013 }
2014}
2015
2016void KFilePlacesViewPrivate::adaptItemsUpdate(qreal value)
2017{
2018 const int add = (m_endSize - m_oldSize) * value;
2019 const int size = m_oldSize + add;
2020
2021 m_delegate->setIconSize(size);
2022 q->scheduleDelayedItemsLayout();
2023}
2024
2025void KFilePlacesViewPrivate::itemAppearUpdate(qreal value)
2026{
2027 m_delegate->setAppearingItemProgress(value);
2028 q->scheduleDelayedItemsLayout();
2029}
2030
2031void KFilePlacesViewPrivate::itemDisappearUpdate(qreal value)
2032{
2033 m_delegate->setDisappearingItemProgress(value);
2034
2035 if (value >= 1.0) {
2036 updateHiddenRows();
2037 }
2038
2039 q->scheduleDelayedItemsLayout();
2040}
2041
2042void KFilePlacesViewPrivate::enableSmoothItemResizing()
2043{
2044 m_smoothItemResizing = true;
2045}
2046
2047void KFilePlacesViewPrivate::deviceBusyAnimationValueChanged(const QVariant &value)
2048{
2049 m_delegate->setDeviceBusyAnimationRotation(value.toReal());
2050 for (const auto &idx : std::as_const(t&: m_busyDevices)) {
2051 q->update(index: idx);
2052 }
2053}
2054
2055void KFilePlacesView::dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList<int> &roles)
2056{
2057 QListView::dataChanged(topLeft, bottomRight, roles);
2058 d->adaptItemSize();
2059
2060 if ((roles.isEmpty() || roles.contains(t: KFilePlacesModel::DeviceAccessibilityRole)) && d->shouldAnimate()) {
2061 QList<QPersistentModelIndex> busyDevices;
2062
2063 auto *placesModel = qobject_cast<KFilePlacesModel *>(object: model());
2064 for (int i = 0; i < placesModel->rowCount(); ++i) {
2065 const QModelIndex idx = placesModel->index(row: i, column: 0);
2066 const auto accessibility = placesModel->deviceAccessibility(index: idx);
2067 if (accessibility == KFilePlacesModel::SetupInProgress || accessibility == KFilePlacesModel::TeardownInProgress) {
2068 busyDevices.append(t: QPersistentModelIndex(idx));
2069 }
2070 }
2071
2072 d->m_busyDevices = busyDevices;
2073
2074 if (busyDevices.isEmpty()) {
2075 d->m_deviceBusyAnimation.stop();
2076 } else {
2077 d->m_deviceBusyAnimation.start();
2078 }
2079 }
2080}
2081
2082#include "moc_kfileplacesview.cpp"
2083#include "moc_kfileplacesview_p.cpp"
2084

source code of kio/src/filewidgets/kfileplacesview.cpp