KIO

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

KDE's Doxygen guidelines are available online.