KIO

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

KDE's Doxygen guidelines are available online.