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

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