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 // Account for the spacing between header and item
645 const int spacing = (s_lateralMargin + m_view->spacing());
646 int height = m_view->fontMetrics().height() + spacing;
647 height += 2 * spacing;
648 return height;
649}
650
651int KFilePlacesViewDelegate::actionIconSize() const
652{
653 return qApp->style()->pixelMetric(QStyle::PM_SmallIconSize, nullptr, m_view);
654}
655
656class KFilePlacesViewPrivate
657{
658public:
659 explicit KFilePlacesViewPrivate(KFilePlacesView *qq)
660 : q(qq)
661 , m_watcher(new KFilePlacesEventWatcher(q))
662 , m_delegate(new KFilePlacesViewDelegate(q))
663 {
664 }
665
666 using ActivationSignal = void (KFilePlacesView::*)(const QUrl &);
667
668 enum FadeType {
669 FadeIn = 0,
670 FadeOut,
671 };
672
673 void setCurrentIndex(const QModelIndex &index);
674 // If m_autoResizeItems is true, calculates a proper size for the icons in the places panel
675 void adaptItemSize();
676 void updateHiddenRows();
677 void clearFreeSpaceInfos();
678 bool insertAbove(const QDropEvent *event, const QRect &itemRect) const;
679 bool insertBelow(const QDropEvent *event, const QRect &itemRect) const;
680 int insertIndicatorHeight(int itemHeight) const;
681 int sectionsCount() const;
682
683 void addPlace(const QModelIndex &index);
684 void editPlace(const QModelIndex &index);
685
686 void addDisappearingItem(KFilePlacesViewDelegate *delegate, const QModelIndex &index);
687 void triggerItemAppearingAnimation();
688 void triggerItemDisappearingAnimation();
689 bool shouldAnimate() const;
690
691 void writeConfig();
692 void readConfig();
693 // Sets the size of the icons in the places panel
694 void relayoutIconSize(int size);
695 // Adds the "Icon Size" sub-menu items
696 void setupIconSizeSubMenu(QMenu *submenu);
697
698 void placeClicked(const QModelIndex &index, ActivationSignal activationSignal);
699 void headerAreaEntered(const QModelIndex &index);
700 void headerAreaLeft(const QModelIndex &index);
701 void actionClicked(const QModelIndex &index);
702 void actionEntered(const QModelIndex &index);
703 void actionLeft(const QModelIndex &index);
704 void teardown(const QModelIndex &index);
705 void storageSetupDone(const QModelIndex &index, bool success);
706 void adaptItemsUpdate(qreal value);
707 void itemAppearUpdate(qreal value);
708 void itemDisappearUpdate(qreal value);
709 void enableSmoothItemResizing();
710 void slotEmptyTrash();
711
712 void deviceBusyAnimationValueChanged(const QVariant &value);
713
714 KFilePlacesView *const q;
715
716 KFilePlacesEventWatcher *const m_watcher;
717 KFilePlacesViewDelegate *m_delegate;
718
719 Solid::StorageAccess *m_lastClickedStorage = nullptr;
720 QPersistentModelIndex m_lastClickedIndex;
721 ActivationSignal m_lastActivationSignal = nullptr;
722
723 QTimer *m_dragActivationTimer = nullptr;
724 QPersistentModelIndex m_pendingDragActivation;
725
726 QPersistentModelIndex m_pendingDropUrlsIndex;
727 std::unique_ptr<QDropEvent> m_dropUrlsEvent;
728 std::unique_ptr<QMimeData> m_dropUrlsMimeData;
729
730 KFilePlacesView::TeardownFunction m_teardownFunction = nullptr;
731
732 QTimeLine m_adaptItemsTimeline;
733 QTimeLine m_itemAppearTimeline;
734 QTimeLine m_itemDisappearTimeline;
735
736 QVariantAnimation m_deviceBusyAnimation;
737 QList<QPersistentModelIndex> m_busyDevices;
738
739 QRect m_dropRect;
740 QPersistentModelIndex m_dropIndex;
741
742 QUrl m_currentUrl;
743
744 int m_oldSize = 0;
745 int m_endSize = 0;
746
747 bool m_autoResizeItems = true;
748 bool m_smoothItemResizing = false;
749 bool m_showAll = false;
750 bool m_dropOnPlace = false;
751 bool m_dragging = false;
752};
753
754KFilePlacesView::KFilePlacesView(QWidget *parent)
755 : QListView(parent)
756 , d(std::make_unique<KFilePlacesViewPrivate>(this))
757{
758 setItemDelegate(d->m_delegate);
759
760 d->readConfig();
761
762 setSelectionRectVisible(false);
763 setSelectionMode(SingleSelection);
764
765 setDragEnabled(true);
766 setAcceptDrops(true);
767 setMouseTracking(true);
768 setDropIndicatorShown(false);
769 setFrameStyle(QFrame::NoFrame);
770
771 setResizeMode(Adjust);
772
773 QPalette palette = viewport()->palette();
774 palette.setColor(viewport()->backgroundRole(), Qt::transparent);
775 palette.setColor(viewport()->foregroundRole(), palette.color(QPalette::WindowText));
776 viewport()->setPalette(palette);
777
778 setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
779
780 d->m_watcher->m_scroller = QScroller::scroller(viewport());
781 QScrollerProperties scrollerProp;
783 d->m_watcher->m_scroller->setScrollerProperties(scrollerProp);
784 d->m_watcher->m_scroller->grabGesture(viewport());
785 connect(d->m_watcher->m_scroller, &QScroller::stateChanged, d->m_watcher, &KFilePlacesEventWatcher::qScrollerStateChanged);
786
787 setAttribute(Qt::WA_AcceptTouchEvents);
788 viewport()->grabGesture(Qt::TapGesture);
789 viewport()->grabGesture(Qt::TapAndHoldGesture);
790
791 // Note: Don't connect to the activated() signal, as the behavior when it is
792 // committed depends on the used widget style. The click behavior of
793 // KFilePlacesView should be style independent.
794 connect(this, &KFilePlacesView::clicked, this, [this](const QModelIndex &index) {
795 const auto modifiers = qGuiApp->keyboardModifiers();
797 d->placeClicked(index, &KFilePlacesView::activeTabRequested);
798 } else if (modifiers == Qt::ControlModifier && isSignalConnected(QMetaMethod::fromSignal(&KFilePlacesView::tabRequested))) {
799 d->placeClicked(index, &KFilePlacesView::tabRequested);
800 } else if (modifiers == Qt::ShiftModifier && isSignalConnected(QMetaMethod::fromSignal(&KFilePlacesView::newWindowRequested))) {
801 d->placeClicked(index, &KFilePlacesView::newWindowRequested);
802 } else {
803 d->placeClicked(index, &KFilePlacesView::placeActivated);
804 }
805 });
806
807 connect(this, &QAbstractItemView::iconSizeChanged, this, [this](const QSize &newSize) {
808 d->m_autoResizeItems = (newSize.width() < 1 || newSize.height() < 1);
809
810 if (d->m_autoResizeItems) {
811 d->adaptItemSize();
812 } else {
813 const int iconSize = qMin(newSize.width(), newSize.height());
814 d->relayoutIconSize(iconSize);
815 }
816 d->writeConfig();
817 });
818
819 connect(&d->m_adaptItemsTimeline, &QTimeLine::valueChanged, this, [this](qreal value) {
820 d->adaptItemsUpdate(value);
821 });
822 d->m_adaptItemsTimeline.setDuration(500);
823 d->m_adaptItemsTimeline.setUpdateInterval(5);
824 d->m_adaptItemsTimeline.setEasingCurve(QEasingCurve::InOutSine);
825
826 connect(&d->m_itemAppearTimeline, &QTimeLine::valueChanged, this, [this](qreal value) {
827 d->itemAppearUpdate(value);
828 });
829 d->m_itemAppearTimeline.setDuration(500);
830 d->m_itemAppearTimeline.setUpdateInterval(5);
831 d->m_itemAppearTimeline.setEasingCurve(QEasingCurve::InOutSine);
832
833 connect(&d->m_itemDisappearTimeline, &QTimeLine::valueChanged, this, [this](qreal value) {
834 d->itemDisappearUpdate(value);
835 });
836 d->m_itemDisappearTimeline.setDuration(500);
837 d->m_itemDisappearTimeline.setUpdateInterval(5);
838 d->m_itemDisappearTimeline.setEasingCurve(QEasingCurve::InOutSine);
839
840 // Adapted from KBusyIndicatorWidget
841 d->m_deviceBusyAnimation.setLoopCount(-1);
842 d->m_deviceBusyAnimation.setDuration(2000);
843 d->m_deviceBusyAnimation.setStartValue(0);
844 d->m_deviceBusyAnimation.setEndValue(360);
845 connect(&d->m_deviceBusyAnimation, &QVariantAnimation::valueChanged, this, [this](const QVariant &value) {
846 d->deviceBusyAnimationValueChanged(value);
847 });
848
849 viewport()->installEventFilter(d->m_watcher);
850 connect(d->m_watcher, &KFilePlacesEventWatcher::entryMiddleClicked, this, [this](const QModelIndex &index) {
851 if (qGuiApp->keyboardModifiers() == Qt::ShiftModifier && isSignalConnected(QMetaMethod::fromSignal(&KFilePlacesView::activeTabRequested))) {
852 d->placeClicked(index, &KFilePlacesView::activeTabRequested);
853 } else if (isSignalConnected(QMetaMethod::fromSignal(&KFilePlacesView::tabRequested))) {
854 d->placeClicked(index, &KFilePlacesView::tabRequested);
855 } else {
856 d->placeClicked(index, &KFilePlacesView::placeActivated);
857 }
858 });
859
860 connect(d->m_watcher, &KFilePlacesEventWatcher::headerAreaEntered, this, [this](const QModelIndex &index) {
861 d->headerAreaEntered(index);
862 });
863 connect(d->m_watcher, &KFilePlacesEventWatcher::headerAreaLeft, this, [this](const QModelIndex &index) {
864 d->headerAreaLeft(index);
865 });
866
867 connect(d->m_watcher, &KFilePlacesEventWatcher::actionClicked, this, [this](const QModelIndex &index) {
868 d->actionClicked(index);
869 });
870 connect(d->m_watcher, &KFilePlacesEventWatcher::actionEntered, this, [this](const QModelIndex &index) {
871 d->actionEntered(index);
872 });
873 connect(d->m_watcher, &KFilePlacesEventWatcher::actionLeft, this, [this](const QModelIndex &index) {
874 d->actionLeft(index);
875 });
876
877 connect(d->m_watcher, &KFilePlacesEventWatcher::windowActivated, this, [this] {
878 d->m_delegate->checkFreeSpace();
879 // Start polling even if checkFreeSpace() wouldn't because we might just have checked
880 // free space before the timeout and so the poll timer would never get started again
881 d->m_delegate->startPollingFreeSpace();
882 });
883 connect(d->m_watcher, &KFilePlacesEventWatcher::windowDeactivated, this, [this] {
884 d->m_delegate->stopPollingFreeSpace();
885 });
886
887 connect(d->m_watcher, &KFilePlacesEventWatcher::paletteChanged, this, [this] {
888 d->m_delegate->paletteChange();
889 });
890
891 // FIXME: this is necessary to avoid flashes of black with some widget styles.
892 // could be a bug in Qt (e.g. QAbstractScrollArea) or KFilePlacesView, but has not
893 // yet been tracked down yet. until then, this works and is harmlessly enough.
894 // in fact, some QStyle (Oxygen, Skulpture, others?) do this already internally.
895 // See br #242358 for more information
896 verticalScrollBar()->setAttribute(Qt::WA_OpaquePaintEvent, false);
897}
898
899KFilePlacesView::~KFilePlacesView()
900{
901 viewport()->removeEventFilter(d->m_watcher);
902}
903
905{
906 d->m_dropOnPlace = enabled;
907}
908
909bool KFilePlacesView::isDropOnPlaceEnabled() const
910{
911 return d->m_dropOnPlace;
912}
913
915{
916 if (delay <= 0) {
917 delete d->m_dragActivationTimer;
918 d->m_dragActivationTimer = nullptr;
919 return;
920 }
921
922 if (!d->m_dragActivationTimer) {
923 d->m_dragActivationTimer = new QTimer(this);
924 d->m_dragActivationTimer->setSingleShot(true);
925 connect(d->m_dragActivationTimer, &QTimer::timeout, this, [this] {
926 if (d->m_pendingDragActivation.isValid()) {
927 d->placeClicked(d->m_pendingDragActivation, &KFilePlacesView::placeActivated);
928 }
929 });
930 }
931 d->m_dragActivationTimer->setInterval(delay);
932}
933
934int KFilePlacesView::dragAutoActivationDelay() const
935{
936 return d->m_dragActivationTimer ? d->m_dragActivationTimer->interval() : 0;
937}
938
940{
941 d->m_autoResizeItems = enabled;
942}
943
944bool KFilePlacesView::isAutoResizeItemsEnabled() const
945{
946 return d->m_autoResizeItems;
947}
948
950{
951 d->m_teardownFunction = teardownFunc;
952}
953
954void KFilePlacesView::setUrl(const QUrl &url)
955{
956 KFilePlacesModel *placesModel = qobject_cast<KFilePlacesModel *>(model());
957
958 if (placesModel == nullptr) {
959 return;
960 }
961
962 QModelIndex index = placesModel->closestItem(url);
964
965 if (index.isValid()) {
966 if (current != index && placesModel->isHidden(current) && !d->m_showAll) {
967 d->addDisappearingItem(d->m_delegate, current);
968 }
969
970 if (current != index && placesModel->isHidden(index) && !d->m_showAll) {
971 d->m_delegate->addAppearingItem(index);
972 d->triggerItemAppearingAnimation();
973 setRowHidden(index.row(), false);
974 }
975
976 d->m_currentUrl = url;
977
978 if (placesModel->url(index) == url.adjusted(QUrl::StripTrailingSlash)) {
980 } else {
982 }
983 } else {
984 d->m_currentUrl = QUrl();
986 }
987
988 if (!current.isValid()) {
989 d->updateHiddenRows();
990 }
991}
992
994{
995 return d->m_showAll;
996}
997
998void KFilePlacesView::setShowAll(bool showAll)
999{
1000 KFilePlacesModel *placesModel = qobject_cast<KFilePlacesModel *>(model());
1001
1002 if (placesModel == nullptr) {
1003 return;
1004 }
1005
1006 d->m_showAll = showAll;
1007
1008 int rowCount = placesModel->rowCount();
1009 QModelIndex current = placesModel->closestItem(d->m_currentUrl);
1010
1011 if (showAll) {
1012 d->updateHiddenRows();
1013
1014 for (int i = 0; i < rowCount; ++i) {
1015 QModelIndex index = placesModel->index(i, 0);
1016 if (index != current && placesModel->isHidden(index)) {
1017 d->m_delegate->addAppearingItem(index);
1018 }
1019 }
1020 d->triggerItemAppearingAnimation();
1021 } else {
1022 for (int i = 0; i < rowCount; ++i) {
1023 QModelIndex index = placesModel->index(i, 0);
1024 if (index != current && placesModel->isHidden(index)) {
1025 d->m_delegate->addDisappearingItem(index);
1026 }
1027 }
1028 d->triggerItemDisappearingAnimation();
1029 }
1030
1032}
1033
1034void KFilePlacesView::keyPressEvent(QKeyEvent *event)
1035{
1037 if ((event->key() == Qt::Key_Return) || (event->key() == Qt::Key_Enter)) {
1038 // TODO Modifier keys for requesting tabs
1039 // Browsers do Ctrl+Click but *Alt*+Return for new tab
1040 d->placeClicked(currentIndex(), &KFilePlacesView::placeActivated);
1041 }
1042}
1043
1044void KFilePlacesViewPrivate::readConfig()
1045{
1046 KConfigGroup cg(KSharedConfig::openConfig(), ConfigGroup);
1047 m_autoResizeItems = cg.readEntry(PlacesIconsAutoresize, true);
1048 m_delegate->setIconSize(cg.readEntry(PlacesIconsStaticSize, static_cast<int>(KIconLoader::SizeMedium)));
1049}
1050
1051void KFilePlacesViewPrivate::writeConfig()
1052{
1053 KConfigGroup cg(KSharedConfig::openConfig(), ConfigGroup);
1054 cg.writeEntry(PlacesIconsAutoresize, m_autoResizeItems);
1055
1056 if (!m_autoResizeItems) {
1057 const int iconSize = qMin(q->iconSize().width(), q->iconSize().height());
1058 cg.writeEntry(PlacesIconsStaticSize, iconSize);
1059 }
1060
1061 cg.sync();
1062}
1063
1064void KFilePlacesViewPrivate::slotEmptyTrash()
1065{
1066 auto *parentWindow = q->window();
1067
1069 auto *emptyTrashJob = new KIO::DeleteOrTrashJob(QList<QUrl>{}, //
1072 parentWindow);
1073 emptyTrashJob->start();
1074}
1075
1076void KFilePlacesView::contextMenuEvent(QContextMenuEvent *event)
1077{
1078 KFilePlacesModel *placesModel = qobject_cast<KFilePlacesModel *>(model());
1079
1080 if (!placesModel) {
1081 return;
1082 }
1083
1084 QModelIndex index = event->reason() == QContextMenuEvent::Keyboard ? selectionModel()->currentIndex() : indexAt(event->pos());
1085 if (!selectedIndexes().contains(index)) {
1086 index = QModelIndex();
1087 }
1088 const QString groupName = index.data(KFilePlacesModel::GroupRole).toString();
1089 const QUrl placeUrl = placesModel->url(index);
1090 const bool clickOverHeader = event->reason() == QContextMenuEvent::Keyboard ? false : d->m_delegate->pointIsHeaderArea(event->pos());
1091 const bool clickOverEmptyArea = clickOverHeader || !index.isValid();
1092 const KFilePlacesModel::GroupType type = placesModel->groupType(index);
1093
1094 QMenu menu;
1095 // Polish before creating a native window below. The style could want change the surface format
1096 // of the window which will have no effect when the native window has already been created.
1097 menu.ensurePolished();
1098
1099 QAction *emptyTrash = nullptr;
1100 QAction *eject = nullptr;
1101 QAction *partition = nullptr;
1102 QAction *mount = nullptr;
1103 QAction *teardown = nullptr;
1104
1105 QAction *newTab = nullptr;
1106 QAction *newWindow = nullptr;
1107 QAction *highPriorityActionsPlaceholder = new QAction();
1108 QAction *properties = nullptr;
1109
1110 QAction *add = nullptr;
1111 QAction *edit = nullptr;
1112 QAction *remove = nullptr;
1113
1114 QAction *hide = nullptr;
1115 QAction *hideSection = nullptr;
1116 QAction *showAll = nullptr;
1117 QMenu *iconSizeMenu = nullptr;
1118
1119 if (!clickOverEmptyArea) {
1120 if (placeUrl.scheme() == QLatin1String("trash")) {
1121 emptyTrash = new QAction(QIcon::fromTheme(QStringLiteral("trash-empty")), i18nc("@action:inmenu", "Empty Trash"), &menu);
1122 KConfig trashConfig(QStringLiteral("trashrc"), KConfig::SimpleConfig);
1123 emptyTrash->setEnabled(!trashConfig.group(QStringLiteral("Status")).readEntry("Empty", true));
1124 }
1125
1126 if (placesModel->isDevice(index)) {
1127 eject = placesModel->ejectActionForIndex(index);
1128 if (eject) {
1129 eject->setParent(&menu);
1130 }
1131
1132 partition = placesModel->partitionActionForIndex(index);
1133 if (partition) {
1134 partition->setParent(&menu);
1135 }
1136
1137 teardown = placesModel->teardownActionForIndex(index);
1138 if (teardown) {
1139 teardown->setParent(&menu);
1140 if (!placesModel->isTeardownAllowed(index)) {
1141 teardown->setEnabled(false);
1142 }
1143 }
1144
1145 if (placesModel->setupNeeded(index)) {
1146 mount = new QAction(QIcon::fromTheme(QStringLiteral("media-mount")), i18nc("@action:inmenu", "Mount"), &menu);
1147 }
1148 }
1149
1150 // TODO What about active tab?
1152 newTab = new QAction(QIcon::fromTheme(QStringLiteral("tab-new")), i18nc("@item:inmenu", "Open in New Tab"), &menu);
1153 }
1155 newWindow = new QAction(QIcon::fromTheme(QStringLiteral("window-new")), i18nc("@item:inmenu", "Open in New Window"), &menu);
1156 }
1157
1158 if (placeUrl.isLocalFile()) {
1159 properties = new QAction(QIcon::fromTheme(QStringLiteral("document-properties")), i18n("Properties"), &menu);
1160 }
1161 }
1162
1163 if (clickOverEmptyArea) {
1164 add = new QAction(QIcon::fromTheme(QStringLiteral("document-new")), i18nc("@action:inmenu", "Add Entry…"), &menu);
1165 }
1166
1167 if (index.isValid()) {
1168 if (!clickOverHeader) {
1169 if (!placesModel->isDevice(index)) {
1170 edit = new QAction(QIcon::fromTheme(QStringLiteral("edit-entry")), i18nc("@action:inmenu", "&Edit…"), &menu);
1171
1172 KBookmark bookmark = placesModel->bookmarkForIndex(index);
1173 const bool isSystemItem = bookmark.metaDataItem(QStringLiteral("isSystemItem")) == QLatin1String("true");
1174 if (!isSystemItem) {
1175 remove = new QAction(QIcon::fromTheme(QStringLiteral("bookmark-remove-symbolic")), i18nc("@action:inmenu", "Remove from Places"), &menu);
1176 }
1177 }
1178
1179 hide = new QAction(QIcon::fromTheme(QStringLiteral("hint")), i18nc("@action:inmenu", "&Hide"), &menu);
1180 hide->setCheckable(true);
1181 hide->setChecked(placesModel->isHidden(index));
1182 // if a parent is hidden no interaction should be possible with children, show it first to do so
1183 hide->setEnabled(!placesModel->isGroupHidden(placesModel->groupType(index)));
1184 }
1185
1186 hideSection = new QAction(QIcon::fromTheme(QStringLiteral("hint")),
1187 !groupName.isEmpty() ? i18nc("@item:inmenu", "Hide Section '%1'", groupName) : i18nc("@item:inmenu", "Hide Section"),
1188 &menu);
1189 hideSection->setCheckable(true);
1190 hideSection->setChecked(placesModel->isGroupHidden(type));
1191 }
1192
1193 if (clickOverEmptyArea) {
1194 if (placesModel->hiddenCount() > 0) {
1195 showAll = new QAction(QIcon::fromTheme(QStringLiteral("visibility")), i18n("&Show All Entries"), &menu);
1196 showAll->setCheckable(true);
1197 showAll->setChecked(d->m_showAll);
1198 }
1199
1200 iconSizeMenu = new QMenu(i18nc("@item:inmenu", "Icon Size"), &menu);
1201 d->setupIconSizeSubMenu(iconSizeMenu);
1202 }
1203
1204 auto addActionToMenu = [&menu](QAction *action) {
1205 if (action) { // silence warning when adding null action
1206 menu.addAction(action);
1207 }
1208 };
1209
1210 addActionToMenu(emptyTrash);
1211
1212 addActionToMenu(eject);
1213 addActionToMenu(mount);
1214 addActionToMenu(teardown);
1215 menu.addSeparator();
1216
1217 if (partition) {
1218 addActionToMenu(partition);
1219 menu.addSeparator();
1220 }
1221
1222 addActionToMenu(newTab);
1223 addActionToMenu(newWindow);
1224 addActionToMenu(highPriorityActionsPlaceholder);
1225 addActionToMenu(properties);
1226 menu.addSeparator();
1227
1228 addActionToMenu(add);
1229 addActionToMenu(edit);
1230 addActionToMenu(remove);
1231 addActionToMenu(hide);
1232 addActionToMenu(hideSection);
1233 addActionToMenu(showAll);
1234 if (iconSizeMenu) {
1235 menu.addMenu(iconSizeMenu);
1236 }
1237
1238 menu.addSeparator();
1239
1240 // Clicking a header should be treated as clicking no device, hence passing an invalid model index
1241 // Emit the signal before adding any custom actions to give the user a chance to dynamically add/remove them
1242 Q_EMIT contextMenuAboutToShow(clickOverHeader ? QModelIndex() : index, &menu);
1243
1244 const auto additionalActions = actions();
1245 for (QAction *action : additionalActions) {
1246 if (action->priority() == QAction::HighPriority) {
1247 menu.insertAction(highPriorityActionsPlaceholder, action);
1248 } else {
1249 menu.addAction(action);
1250 }
1251 }
1252 delete highPriorityActionsPlaceholder;
1253
1254 if (window()) {
1255 menu.winId();
1257 }
1258 QAction *result;
1259 if (event->reason() == QContextMenuEvent::Keyboard && index.isValid()) {
1260 const QRect rect = visualRect(index);
1261 result = menu.exec(mapToGlobal(QPoint(rect.x() + rect.width() / 2, rect.y() + rect.height() * 0.9)));
1262 } else {
1263 result = menu.exec(event->globalPos());
1264 }
1265
1266 if (result) {
1267 if (result == emptyTrash) {
1268 d->slotEmptyTrash();
1269
1270 } else if (result == eject) {
1271 placesModel->requestEject(index);
1272 } else if (result == mount) {
1273 placesModel->requestSetup(index);
1274 } else if (result == teardown) {
1275 d->teardown(index);
1276 } else if (result == newTab) {
1277 d->placeClicked(index, &KFilePlacesView::tabRequested);
1278 } else if (result == newWindow) {
1279 d->placeClicked(index, &KFilePlacesView::newWindowRequested);
1280 } else if (result == properties) {
1281 KPropertiesDialog::showDialog(placeUrl, this);
1282 } else if (result == add) {
1283 d->addPlace(index);
1284 } else if (result == edit) {
1285 d->editPlace(index);
1286 } else if (result == remove) {
1287 placesModel->removePlace(index);
1288 } else if (result == hide) {
1289 placesModel->setPlaceHidden(index, hide->isChecked());
1290 QModelIndex current = placesModel->closestItem(d->m_currentUrl);
1291
1292 if (index != current && !d->m_showAll && hide->isChecked()) {
1293 d->m_delegate->addDisappearingItem(index);
1294 d->triggerItemDisappearingAnimation();
1295 }
1296 } else if (result == hideSection) {
1297 placesModel->setGroupHidden(type, hideSection->isChecked());
1298
1299 if (!d->m_showAll && hideSection->isChecked()) {
1300 d->m_delegate->addDisappearingItemGroup(index);
1301 d->triggerItemDisappearingAnimation();
1302 }
1303 } else if (result == showAll) {
1304 setShowAll(showAll->isChecked());
1305 }
1306 }
1307
1308 if (event->reason() != QContextMenuEvent::Keyboard) {
1309 index = placesModel->closestItem(d->m_currentUrl);
1311 }
1312}
1313
1314void KFilePlacesViewPrivate::setupIconSizeSubMenu(QMenu *submenu)
1315{
1316 QActionGroup *group = new QActionGroup(submenu);
1317
1318 auto *autoAct = new QAction(i18nc("@item:inmenu Auto set icon size based on available space in"
1319 "the Places side-panel",
1320 "Auto Resize"),
1321 group);
1322 autoAct->setCheckable(true);
1323 autoAct->setChecked(m_autoResizeItems);
1324 QObject::connect(autoAct, &QAction::toggled, q, [this]() {
1325 q->setIconSize(QSize(-1, -1));
1326 });
1327 submenu->addAction(autoAct);
1328
1329 static constexpr KIconLoader::StdSizes iconSizes[] = {KIconLoader::SizeSmall,
1333
1334 for (const auto iconSize : iconSizes) {
1335 auto *act = new QAction(group);
1336 act->setCheckable(true);
1337
1338 switch (iconSize) {
1340 act->setText(i18nc("Small icon size", "Small (%1x%1)", KIconLoader::SizeSmall));
1341 break;
1343 act->setText(i18nc("Medium icon size", "Medium (%1x%1)", KIconLoader::SizeSmallMedium));
1344 break;
1346 act->setText(i18nc("Large icon size", "Large (%1x%1)", KIconLoader::SizeMedium));
1347 break;
1349 act->setText(i18nc("Huge icon size", "Huge (%1x%1)", KIconLoader::SizeLarge));
1350 break;
1351 default:
1352 break;
1353 }
1354
1355 QObject::connect(act, &QAction::toggled, q, [this, iconSize]() {
1356 q->setIconSize(QSize(iconSize, iconSize));
1357 });
1358
1359 if (!m_autoResizeItems) {
1360 act->setChecked(iconSize == m_delegate->iconSize());
1361 }
1362
1363 submenu->addAction(act);
1364 }
1365}
1366
1367void KFilePlacesView::resizeEvent(QResizeEvent *event)
1368{
1370 d->adaptItemSize();
1371}
1372
1373void KFilePlacesView::showEvent(QShowEvent *event)
1374{
1376
1377 d->m_delegate->checkFreeSpace();
1378 // Start polling even if checkFreeSpace() wouldn't because we might just have checked
1379 // free space before the timeout and so the poll timer would never get started again
1380 d->m_delegate->startPollingFreeSpace();
1381
1382 QTimer::singleShot(100, this, [this]() {
1383 d->enableSmoothItemResizing();
1384 });
1385}
1386
1387void KFilePlacesView::hideEvent(QHideEvent *event)
1388{
1390 d->m_delegate->stopPollingFreeSpace();
1391 d->m_smoothItemResizing = false;
1392}
1393
1394void KFilePlacesView::dragEnterEvent(QDragEnterEvent *event)
1395{
1397 d->m_dragging = true;
1398
1399 d->m_delegate->setShowHoverIndication(false);
1400
1401 d->m_dropRect = QRect();
1402 d->m_dropIndex = QPersistentModelIndex();
1403}
1404
1405void KFilePlacesView::dragLeaveEvent(QDragLeaveEvent *event)
1406{
1408 d->m_dragging = false;
1409
1410 d->m_delegate->setShowHoverIndication(true);
1411
1412 if (d->m_dragActivationTimer) {
1413 d->m_dragActivationTimer->stop();
1414 }
1415 d->m_pendingDragActivation = QPersistentModelIndex();
1416
1417 setDirtyRegion(d->m_dropRect);
1418}
1419
1420void KFilePlacesView::dragMoveEvent(QDragMoveEvent *event)
1421{
1423
1424 bool autoActivate = false;
1425 // update the drop indicator
1426 const QPoint pos = event->position().toPoint();
1427 const QModelIndex index = indexAt(pos);
1428 setDirtyRegion(d->m_dropRect);
1429 if (index.isValid()) {
1430 d->m_dropIndex = index;
1431 const QRect rect = visualRect(index);
1432 const int gap = d->insertIndicatorHeight(rect.height());
1433
1434 if (d->insertAbove(event, rect)) {
1435 // indicate that the item will be inserted above the current place
1436 d->m_dropRect = QRect(rect.left(), rect.top() - gap / 2, rect.width(), gap);
1437 } else if (d->insertBelow(event, rect)) {
1438 // indicate that the item will be inserted below the current place
1439 d->m_dropRect = QRect(rect.left(), rect.bottom() + 1 - gap / 2, rect.width(), gap);
1440 } else {
1441 // indicate that the item be dropped above the current place
1442 d->m_dropRect = rect;
1443 // only auto-activate when dropping ontop of a place, not inbetween
1444 autoActivate = true;
1445 }
1446 }
1447
1448 if (d->m_dragActivationTimer) {
1449 if (autoActivate && !d->m_delegate->pointIsHeaderArea(event->position().toPoint())) {
1450 QPersistentModelIndex persistentIndex(index);
1451 if (!d->m_pendingDragActivation.isValid() || d->m_pendingDragActivation != persistentIndex) {
1452 d->m_pendingDragActivation = persistentIndex;
1453 d->m_dragActivationTimer->start();
1454 }
1455 } else {
1456 d->m_dragActivationTimer->stop();
1457 d->m_pendingDragActivation = QPersistentModelIndex();
1458 }
1459 }
1460
1461 setDirtyRegion(d->m_dropRect);
1462}
1463
1464void KFilePlacesView::dropEvent(QDropEvent *event)
1465{
1466 const QModelIndex index = indexAt(event->position().toPoint());
1467 if (index.isValid()) {
1468 const QRect rect = visualRect(index);
1469 if (!d->insertAbove(event, rect) && !d->insertBelow(event, rect)) {
1470 KFilePlacesModel *placesModel = qobject_cast<KFilePlacesModel *>(model());
1471 Q_ASSERT(placesModel != nullptr);
1472 if (placesModel->setupNeeded(index)) {
1473 d->m_pendingDropUrlsIndex = index;
1474
1475 // Make a full copy of the Mime-Data
1476 d->m_dropUrlsMimeData = std::make_unique<QMimeData>();
1477 const auto formats = event->mimeData()->formats();
1478 for (const auto &format : formats) {
1479 d->m_dropUrlsMimeData->setData(format, event->mimeData()->data(format));
1480 }
1481
1482 d->m_dropUrlsEvent = std::make_unique<QDropEvent>(event->position(),
1483 event->possibleActions(),
1484 d->m_dropUrlsMimeData.get(),
1485 event->buttons(),
1486 event->modifiers());
1487
1488 placesModel->requestSetup(index);
1489 } else {
1490 Q_EMIT urlsDropped(placesModel->url(index), event, this);
1491 }
1492 // HACK Qt eventually calls into QAIM::dropMimeData when a drop event isn't
1493 // accepted by the view. However, QListView::dropEvent calls ignore() on our
1494 // event afterwards when
1495 // "icon view didn't move the data, and moveRows not implemented, so fall back to default"
1496 // overriding the acceptProposedAction() below.
1497 // This special mime type tells KFilePlacesModel to ignore it.
1498 auto *mime = const_cast<QMimeData *>(event->mimeData());
1499 mime->setData(KFilePlacesModelPrivate::ignoreMimeType(), QByteArrayLiteral("1"));
1500 event->acceptProposedAction();
1501 }
1502 }
1503
1505 d->m_dragging = false;
1506
1507 if (d->m_dragActivationTimer) {
1508 d->m_dragActivationTimer->stop();
1509 }
1510 d->m_pendingDragActivation = QPersistentModelIndex();
1511
1512 d->m_delegate->setShowHoverIndication(true);
1513}
1514
1515void KFilePlacesView::paintEvent(QPaintEvent *event)
1516{
1518 if (d->m_dragging && !d->m_dropRect.isEmpty()) {
1519 // draw drop indicator
1520 QPainter painter(viewport());
1521
1522 QRect itemRect = visualRect(d->m_dropIndex);
1523 // Take into account section headers
1524 if (d->m_delegate->indexIsSectionHeader(d->m_dropIndex)) {
1525 const int headerHeight = d->m_delegate->sectionHeaderHeight(d->m_dropIndex);
1526 itemRect.translate(0, headerHeight);
1527 itemRect.setHeight(itemRect.height() - headerHeight);
1528 }
1529 const bool drawInsertIndicator = !d->m_dropOnPlace || d->m_dropRect.height() <= d->insertIndicatorHeight(itemRect.height());
1530
1531 if (drawInsertIndicator) {
1532 // draw indicator for inserting items
1533 QStyleOptionViewItem viewOpts;
1534 initViewItemOption(&viewOpts);
1535
1536 QBrush blendedBrush = viewOpts.palette.brush(QPalette::Normal, QPalette::Highlight);
1537 QColor color = blendedBrush.color();
1538
1539 const int y = (d->m_dropRect.top() + d->m_dropRect.bottom()) / 2;
1540 const int thickness = d->m_dropRect.height() / 2;
1541 Q_ASSERT(thickness >= 1);
1542 int alpha = 255;
1543 const int alphaDec = alpha / (thickness + 1);
1544 for (int i = 0; i < thickness; i++) {
1545 color.setAlpha(alpha);
1546 alpha -= alphaDec;
1547 painter.setPen(color);
1548 painter.drawLine(d->m_dropRect.left(), y - i, d->m_dropRect.right(), y - i);
1549 painter.drawLine(d->m_dropRect.left(), y + i, d->m_dropRect.right(), y + i);
1550 }
1551 } else {
1552 // draw indicator for copying/moving/linking to items
1554 opt.initFrom(this);
1555 opt.index = d->m_dropIndex;
1556 opt.rect = itemRect;
1558 style()->drawPrimitive(QStyle::PE_PanelItemViewItem, &opt, &painter, this);
1559 }
1560 }
1561}
1562
1563void KFilePlacesView::startDrag(Qt::DropActions supportedActions)
1564{
1565 d->m_delegate->startDrag();
1566 QListView::startDrag(supportedActions);
1567}
1568
1569void KFilePlacesView::mousePressEvent(QMouseEvent *event)
1570{
1571 if (event->button() == Qt::LeftButton) {
1572 // does not accept drags from section header area
1573 if (d->m_delegate->pointIsHeaderArea(event->pos())) {
1574 return;
1575 }
1576 // teardown button is handled by KFilePlacesEventWatcher
1577 // NOTE "mouseReleaseEvent" side is also in there.
1578 if (d->m_delegate->pointIsTeardownAction(event->pos())) {
1579 return;
1580 }
1581 }
1583}
1584
1585void KFilePlacesView::setModel(QAbstractItemModel *model)
1586{
1588 d->updateHiddenRows();
1589 // Uses Qt::QueuedConnection to delay the time when the slot will be
1590 // called. In case of an item move the remove+add will be done before
1591 // we adapt the item size (otherwise we'd get it wrong as we'd execute
1592 // it after the remove only).
1593 connect(
1594 model,
1596 this,
1597 [this]() {
1598 d->adaptItemSize();
1599 },
1601
1602 QObject::connect(qobject_cast<KFilePlacesModel *>(model), &KFilePlacesModel::setupDone, this, [this](const QModelIndex &idx, bool success) {
1603 d->storageSetupDone(idx, success);
1604 });
1605
1606 d->m_delegate->clearFreeSpaceInfo();
1607}
1608
1609void KFilePlacesView::rowsInserted(const QModelIndex &parent, int start, int end)
1610{
1612 setUrl(d->m_currentUrl);
1613
1614 KFilePlacesModel *placesModel = static_cast<KFilePlacesModel *>(model());
1615
1616 for (int i = start; i <= end; ++i) {
1617 QModelIndex index = placesModel->index(i, 0, parent);
1618 if (d->m_showAll || !placesModel->isHidden(index)) {
1619 d->m_delegate->addAppearingItem(index);
1620 d->triggerItemAppearingAnimation();
1621 } else {
1622 setRowHidden(i, true);
1623 }
1624 }
1625
1626 d->triggerItemAppearingAnimation();
1627
1628 d->adaptItemSize();
1629}
1630
1631QSize KFilePlacesView::sizeHint() const
1632{
1633 KFilePlacesModel *placesModel = qobject_cast<KFilePlacesModel *>(model());
1634 if (!placesModel) {
1635 return QListView::sizeHint();
1636 }
1637 const int height = QListView::sizeHint().height();
1638 QFontMetrics fm = d->q->fontMetrics();
1639 int textWidth = 0;
1640
1641 for (int i = 0; i < placesModel->rowCount(); ++i) {
1642 QModelIndex index = placesModel->index(i, 0);
1643 if (!placesModel->isHidden(index)) {
1644 textWidth = qMax(textWidth, fm.boundingRect(index.data(Qt::DisplayRole).toString()).width());
1645 }
1646 }
1647
1648 const int iconSize = style()->pixelMetric(QStyle::PM_SmallIconSize) + 3 * s_lateralMargin;
1649 return QSize(iconSize + textWidth + fm.height() / 2, height);
1650}
1651
1652void KFilePlacesViewPrivate::addDisappearingItem(KFilePlacesViewDelegate *delegate, const QModelIndex &index)
1653{
1654 delegate->addDisappearingItem(index);
1655 if (m_itemDisappearTimeline.state() != QTimeLine::Running) {
1656 delegate->setDisappearingItemProgress(0.0);
1657 m_itemDisappearTimeline.start();
1658 }
1659}
1660
1661void KFilePlacesViewPrivate::setCurrentIndex(const QModelIndex &index)
1662{
1663 KFilePlacesModel *placesModel = qobject_cast<KFilePlacesModel *>(q->model());
1664
1665 if (placesModel == nullptr) {
1666 return;
1667 }
1668
1669 QUrl url = placesModel->url(index);
1670
1671 if (url.isValid()) {
1672 m_currentUrl = url;
1673 updateHiddenRows();
1674 Q_EMIT q->urlChanged(KFilePlacesModel::convertedUrl(url));
1675 } else {
1676 q->setUrl(m_currentUrl);
1677 }
1678}
1679
1680void KFilePlacesViewPrivate::adaptItemSize()
1681{
1682 if (!m_autoResizeItems) {
1683 return;
1684 }
1685
1686 KFilePlacesModel *placesModel = qobject_cast<KFilePlacesModel *>(q->model());
1687
1688 if (placesModel == nullptr) {
1689 return;
1690 }
1691
1692 int rowCount = placesModel->rowCount();
1693
1694 if (!m_showAll) {
1695 rowCount -= placesModel->hiddenCount();
1696
1697 QModelIndex current = placesModel->closestItem(m_currentUrl);
1698
1699 if (placesModel->isHidden(current)) {
1700 ++rowCount;
1701 }
1702 }
1703
1704 if (rowCount == 0) {
1705 return; // We've nothing to display anyway
1706 }
1707
1708 const int minSize = q->style()->pixelMetric(QStyle::PM_SmallIconSize);
1709 const int maxSize = 64;
1710
1711 int textWidth = 0;
1712 QFontMetrics fm = q->fontMetrics();
1713 for (int i = 0; i < placesModel->rowCount(); ++i) {
1714 QModelIndex index = placesModel->index(i, 0);
1715
1716 if (!placesModel->isHidden(index)) {
1717 textWidth = qMax(textWidth, fm.boundingRect(index.data(Qt::DisplayRole).toString()).width());
1718 }
1719 }
1720
1721 const int margin = q->style()->pixelMetric(QStyle::PM_FocusFrameHMargin, nullptr, q) + 1;
1722 const int maxWidth = q->viewport()->width() - textWidth - 4 * margin - 1;
1723
1724 const int totalItemsHeight = (fm.height() / 2) * rowCount;
1725 const int totalSectionsHeight = m_delegate->sectionHeaderHeight(QModelIndex()) * sectionsCount();
1726 const int maxHeight = ((q->height() - totalSectionsHeight - totalItemsHeight) / rowCount) - 1;
1727
1728 int size = qMin(maxHeight, maxWidth);
1729
1730 if (size < minSize) {
1731 size = minSize;
1732 } else if (size > maxSize) {
1733 size = maxSize;
1734 } else {
1735 // Make it a multiple of 16
1736 size &= ~0xf;
1737 }
1738
1739 relayoutIconSize(size);
1740}
1741
1742void KFilePlacesViewPrivate::relayoutIconSize(const int size)
1743{
1744 if (size == m_delegate->iconSize()) {
1745 return;
1746 }
1747
1748 if (shouldAnimate() && m_smoothItemResizing) {
1749 m_oldSize = m_delegate->iconSize();
1750 m_endSize = size;
1751 if (m_adaptItemsTimeline.state() != QTimeLine::Running) {
1752 m_adaptItemsTimeline.start();
1753 }
1754 } else {
1755 m_delegate->setIconSize(size);
1756 if (shouldAnimate()) {
1758 }
1759 }
1760}
1761
1762void KFilePlacesViewPrivate::updateHiddenRows()
1763{
1764 KFilePlacesModel *placesModel = qobject_cast<KFilePlacesModel *>(q->model());
1765
1766 if (placesModel == nullptr) {
1767 return;
1768 }
1769
1770 int rowCount = placesModel->rowCount();
1771 QModelIndex current = placesModel->closestItem(m_currentUrl);
1772
1773 for (int i = 0; i < rowCount; ++i) {
1774 QModelIndex index = placesModel->index(i, 0);
1775 if (index != current && placesModel->isHidden(index) && !m_showAll) {
1776 q->setRowHidden(i, true);
1777 } else {
1778 q->setRowHidden(i, false);
1779 }
1780 }
1781
1782 adaptItemSize();
1783}
1784
1785bool KFilePlacesViewPrivate::insertAbove(const QDropEvent *event, const QRect &itemRect) const
1786{
1787 if (m_dropOnPlace && !event->mimeData()->hasFormat(KFilePlacesModelPrivate::internalMimeType(qobject_cast<KFilePlacesModel *>(q->model())))) {
1788 return event->position().y() < itemRect.top() + insertIndicatorHeight(itemRect.height()) / 2;
1789 }
1790
1791 return event->position().y() < itemRect.top() + (itemRect.height() / 2);
1792}
1793
1794bool KFilePlacesViewPrivate::insertBelow(const QDropEvent *event, const QRect &itemRect) const
1795{
1796 if (m_dropOnPlace && !event->mimeData()->hasFormat(KFilePlacesModelPrivate::internalMimeType(qobject_cast<KFilePlacesModel *>(q->model())))) {
1797 return event->position().y() > itemRect.bottom() - insertIndicatorHeight(itemRect.height()) / 2;
1798 }
1799
1800 return event->position().y() >= itemRect.top() + (itemRect.height() / 2);
1801}
1802
1803int KFilePlacesViewPrivate::insertIndicatorHeight(int itemHeight) const
1804{
1805 const int min = 4;
1806 const int max = 12;
1807
1808 int height = itemHeight / 4;
1809 if (height < min) {
1810 height = min;
1811 } else if (height > max) {
1812 height = max;
1813 }
1814 return height;
1815}
1816
1817int KFilePlacesViewPrivate::sectionsCount() const
1818{
1819 int count = 0;
1820 QString prevSection;
1821 const int rowCount = q->model()->rowCount();
1822
1823 for (int i = 0; i < rowCount; i++) {
1824 if (!q->isRowHidden(i)) {
1825 const QModelIndex index = q->model()->index(i, 0);
1826 const QString sectionName = index.data(KFilePlacesModel::GroupRole).toString();
1827 if (prevSection != sectionName) {
1828 prevSection = sectionName;
1829 ++count;
1830 }
1831 }
1832 }
1833
1834 return count;
1835}
1836
1837void KFilePlacesViewPrivate::addPlace(const QModelIndex &index)
1838{
1839 KFilePlacesModel *placesModel = qobject_cast<KFilePlacesModel *>(q->model());
1840
1841 QUrl url = m_currentUrl;
1842 QString label;
1843 QString iconName = QStringLiteral("folder");
1844 bool appLocal = true;
1845 if (KFilePlaceEditDialog::getInformation(true, url, label, iconName, true, appLocal, 64, q)) {
1847 if (appLocal) {
1849 }
1850
1851 placesModel->addPlace(label, url, iconName, appName, index);
1852 }
1853}
1854
1855void KFilePlacesViewPrivate::editPlace(const QModelIndex &index)
1856{
1857 KFilePlacesModel *placesModel = qobject_cast<KFilePlacesModel *>(q->model());
1858
1859 KBookmark bookmark = placesModel->bookmarkForIndex(index);
1860 QUrl url = bookmark.url();
1861 // KBookmark::text() would be untranslated for system bookmarks
1862 QString label = placesModel->text(index);
1863 QString iconName = bookmark.icon();
1864 bool appLocal = !bookmark.metaDataItem(QStringLiteral("OnlyInApp")).isEmpty();
1865
1866 if (KFilePlaceEditDialog::getInformation(true, url, label, iconName, false, appLocal, 64, q)) {
1868 if (appLocal) {
1870 }
1871
1872 placesModel->editPlace(index, label, url, iconName, appName);
1873 }
1874}
1875
1876bool KFilePlacesViewPrivate::shouldAnimate() const
1877{
1878 return q->style()->styleHint(QStyle::SH_Widget_Animation_Duration, nullptr, q) > 0;
1879}
1880
1881void KFilePlacesViewPrivate::triggerItemAppearingAnimation()
1882{
1883 if (m_itemAppearTimeline.state() == QTimeLine::Running) {
1884 return;
1885 }
1886
1887 if (shouldAnimate()) {
1888 m_delegate->setAppearingItemProgress(0.0);
1889 m_itemAppearTimeline.start();
1890 } else {
1891 itemAppearUpdate(1.0);
1892 }
1893}
1894
1895void KFilePlacesViewPrivate::triggerItemDisappearingAnimation()
1896{
1897 if (m_itemDisappearTimeline.state() == QTimeLine::Running) {
1898 return;
1899 }
1900
1901 if (shouldAnimate()) {
1902 m_delegate->setDisappearingItemProgress(0.0);
1903 m_itemDisappearTimeline.start();
1904 } else {
1905 itemDisappearUpdate(1.0);
1906 }
1907}
1908
1909void KFilePlacesViewPrivate::placeClicked(const QModelIndex &index, ActivationSignal activationSignal)
1910{
1911 KFilePlacesModel *placesModel = qobject_cast<KFilePlacesModel *>(q->model());
1912
1913 if (placesModel == nullptr) {
1914 return;
1915 }
1916
1917 m_lastClickedIndex = QPersistentModelIndex();
1918 m_lastActivationSignal = nullptr;
1919
1920 if (placesModel->setupNeeded(index)) {
1921 m_lastClickedIndex = index;
1922 m_lastActivationSignal = activationSignal;
1923 placesModel->requestSetup(index);
1924 return;
1925 }
1926
1927 setCurrentIndex(index);
1928
1929 const QUrl url = KFilePlacesModel::convertedUrl(placesModel->url(index));
1930
1931 /*Q_EMIT*/ std::invoke(activationSignal, q, url);
1932}
1933
1934void KFilePlacesViewPrivate::headerAreaEntered(const QModelIndex &index)
1935{
1936 m_delegate->setHoveredHeaderArea(index);
1937 q->update(index);
1938}
1939
1940void KFilePlacesViewPrivate::headerAreaLeft(const QModelIndex &index)
1941{
1942 m_delegate->setHoveredHeaderArea(QModelIndex());
1943 q->update(index);
1944}
1945
1946void KFilePlacesViewPrivate::actionClicked(const QModelIndex &index)
1947{
1948 KFilePlacesModel *placesModel = qobject_cast<KFilePlacesModel *>(q->model());
1949 if (!placesModel) {
1950 return;
1951 }
1952
1953 Solid::Device device = placesModel->deviceForIndex(index);
1954 if (device.is<Solid::OpticalDisc>()) {
1955 placesModel->requestEject(index);
1956 } else {
1957 teardown(index);
1958 }
1959}
1960
1961void KFilePlacesViewPrivate::actionEntered(const QModelIndex &index)
1962{
1963 m_delegate->setHoveredAction(index);
1964 q->update(index);
1965}
1966
1967void KFilePlacesViewPrivate::actionLeft(const QModelIndex &index)
1968{
1969 m_delegate->setHoveredAction(QModelIndex());
1970 q->update(index);
1971}
1972
1973void KFilePlacesViewPrivate::teardown(const QModelIndex &index)
1974{
1975 if (m_teardownFunction) {
1976 m_teardownFunction(index);
1977 } else if (auto *placesModel = qobject_cast<KFilePlacesModel *>(q->model())) {
1978 placesModel->requestTeardown(index);
1979 }
1980}
1981
1982void KFilePlacesViewPrivate::storageSetupDone(const QModelIndex &index, bool success)
1983{
1984 KFilePlacesModel *placesModel = static_cast<KFilePlacesModel *>(q->model());
1985
1986 if (m_lastClickedIndex.isValid()) {
1987 if (m_lastClickedIndex == index) {
1988 if (success) {
1989 setCurrentIndex(m_lastClickedIndex);
1990 } else {
1991 q->setUrl(m_currentUrl);
1992 }
1993
1994 const QUrl url = KFilePlacesModel::convertedUrl(placesModel->url(index));
1995 /*Q_EMIT*/ std::invoke(m_lastActivationSignal, q, url);
1996
1997 m_lastClickedIndex = QPersistentModelIndex();
1998 m_lastActivationSignal = nullptr;
1999 }
2000 }
2001
2002 if (m_pendingDropUrlsIndex.isValid() && m_dropUrlsEvent) {
2003 if (m_pendingDropUrlsIndex == index) {
2004 if (success) {
2005 Q_EMIT q->urlsDropped(placesModel->url(index), m_dropUrlsEvent.get(), q);
2006 }
2007
2008 m_pendingDropUrlsIndex = QPersistentModelIndex();
2009 m_dropUrlsEvent.reset();
2010 m_dropUrlsMimeData.reset();
2011 }
2012 }
2013}
2014
2015void KFilePlacesViewPrivate::adaptItemsUpdate(qreal value)
2016{
2017 const int add = (m_endSize - m_oldSize) * value;
2018 const int size = m_oldSize + add;
2019
2020 m_delegate->setIconSize(size);
2022}
2023
2024void KFilePlacesViewPrivate::itemAppearUpdate(qreal value)
2025{
2026 m_delegate->setAppearingItemProgress(value);
2028}
2029
2030void KFilePlacesViewPrivate::itemDisappearUpdate(qreal value)
2031{
2032 m_delegate->setDisappearingItemProgress(value);
2033
2034 if (value >= 1.0) {
2035 updateHiddenRows();
2036 }
2037
2039}
2040
2041void KFilePlacesViewPrivate::enableSmoothItemResizing()
2042{
2043 m_smoothItemResizing = true;
2044}
2045
2046void KFilePlacesViewPrivate::deviceBusyAnimationValueChanged(const QVariant &value)
2047{
2048 m_delegate->setDeviceBusyAnimationRotation(value.toReal());
2049 for (const auto &idx : std::as_const(m_busyDevices)) {
2050 q->update(idx);
2051 }
2052}
2053
2054void KFilePlacesView::dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList<int> &roles)
2055{
2056 QListView::dataChanged(topLeft, bottomRight, roles);
2057 d->adaptItemSize();
2058
2059 if ((roles.isEmpty() || roles.contains(KFilePlacesModel::DeviceAccessibilityRole)) && d->shouldAnimate()) {
2060 QList<QPersistentModelIndex> busyDevices;
2061
2062 auto *placesModel = qobject_cast<KFilePlacesModel *>(model());
2063 for (int i = 0; i < placesModel->rowCount(); ++i) {
2064 const QModelIndex idx = placesModel->index(i, 0);
2065 const auto accessibility = placesModel->deviceAccessibility(idx);
2066 if (accessibility == KFilePlacesModel::SetupInProgress || accessibility == KFilePlacesModel::TeardownInProgress) {
2067 busyDevices.append(QPersistentModelIndex(idx));
2068 }
2069 }
2070
2071 d->m_busyDevices = busyDevices;
2072
2073 if (busyDevices.isEmpty()) {
2074 d->m_deviceBusyAnimation.stop();
2075 } else {
2076 d->m_deviceBusyAnimation.start();
2077 }
2078 }
2079}
2080
2081#include "moc_kfileplacesview.cpp"
2082#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 Tue Mar 26 2024 11:18:52 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.