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

KDE's Doxygen guidelines are available online.