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