1/*
2 This file is part of the KDE project
3 SPDX-FileCopyrightText: 2008 Rafael Fernández López <ereslibre@kde.org>
4 SPDX-FileCopyrightText: 2022 Kai Uwe Broulik <kde@broulik.de>
5
6 SPDX-License-Identifier: LGPL-2.0-only
7*/
8
9#ifndef KFILEPLACESVIEW_P_H
10#define KFILEPLACESVIEW_P_H
11
12#include <KIO/FileSystemFreeSpaceJob>
13#include <KIO/Global>
14
15#include <QAbstractItemDelegate>
16#include <QDateTime>
17#include <QDeadlineTimer>
18#include <QGestureEvent>
19#include <QMouseEvent>
20#include <QPointer>
21#include <QScroller>
22#include <QTimer>
23
24#include <set>
25
26class KFilePlacesView;
27class QTimeLine;
28
29struct PlaceFreeSpaceInfo {
30 QDeadlineTimer timeout;
31 KIO::filesize_t used = 0;
32 KIO::filesize_t size = 0;
33 QPointer<KIO::FileSystemFreeSpaceJob> job;
34};
35
36class KFilePlacesViewDelegate : public QAbstractItemDelegate
37{
38 Q_OBJECT
39public:
40 explicit KFilePlacesViewDelegate(KFilePlacesView *parent);
41 ~KFilePlacesViewDelegate() override;
42 QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override;
43 void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
44
45 int iconSize() const;
46 void setIconSize(int newSize);
47
48 void paletteChange();
49
50 void addAppearingItem(const QModelIndex &index);
51 void setAppearingItemProgress(qreal value);
52 void addDisappearingItem(const QModelIndex &index);
53 void addDisappearingItemGroup(const QModelIndex &index);
54 void setDisappearingItemProgress(qreal value);
55 void setDeviceBusyAnimationRotation(qreal angle);
56
57 void setShowHoverIndication(bool show);
58 void setHoveredHeaderArea(const QModelIndex &index);
59 void setHoveredAction(const QModelIndex &index);
60
61 QModelIndex emptyingTrashIndex() const;
62 void setEmptyingTrashIndex(const QModelIndex &index);
63
64 qreal contentsOpacity(const QModelIndex &index) const;
65
66 bool pointIsHeaderArea(const QPoint &pos) const;
67 bool pointIsTeardownAction(const QPoint &pos) const;
68
69 void startDrag();
70
71 int sectionHeaderHeight(const QModelIndex &index) const;
72 bool indexIsSectionHeader(const QModelIndex &index) const;
73 int actionIconSize() const;
74
75 void checkFreeSpace();
76 void checkFreeSpace(const QModelIndex &index) const;
77 void startPollingFreeSpace() const;
78 void stopPollingFreeSpace() const;
79
80 void clearFreeSpaceInfo();
81
82protected:
83 bool helpEvent(QHelpEvent *event, QAbstractItemView *view, const QStyleOptionViewItem &option, const QModelIndex &index) override;
84
85private:
86 QString groupNameFromIndex(const QModelIndex &index) const;
87 QModelIndex previousVisibleIndex(const QModelIndex &index) const;
88 void drawSectionHeader(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const;
89
90 QColor textColor(const QStyleOption &option) const;
91 QColor baseColor(const QStyleOption &option) const;
92 QColor mixedColor(const QColor &c1, const QColor &c2, int c1Percent) const;
93
94 KFilePlacesView *m_view;
95 int m_iconSize;
96
97 QList<QPersistentModelIndex> m_appearingItems;
98 qreal m_appearingHeightScale;
99 qreal m_appearingOpacity;
100
101 QList<QPersistentModelIndex> m_disappearingItems;
102 qreal m_disappearingHeightScale;
103 qreal m_disappearingOpacity;
104
105 qreal m_busyAnimationRotation = 0.0;
106 QPersistentModelIndex m_emptyingTrashIndex;
107
108 bool m_showHoverIndication;
109 QPersistentModelIndex m_hoveredHeaderArea;
110 QPersistentModelIndex m_hoveredAction;
111 mutable bool m_dragStarted;
112
113 QMap<QPersistentModelIndex, QTimeLine *> m_timeLineMap;
114 QMap<QTimeLine *, QPersistentModelIndex> m_timeLineInverseMap;
115
116 mutable QTimer m_pollFreeSpace;
117 mutable QMap<QPersistentModelIndex, PlaceFreeSpaceInfo> m_freeSpaceInfo;
118
119 mutable std::set<QPersistentModelIndex> m_elidedTexts;
120
121 // constructing KColorScheme is expensive, cache the negative color
122 mutable QColor m_warningCapacityBarColor;
123};
124
125class KFilePlacesEventWatcher : public QObject
126{
127 Q_OBJECT
128
129public:
130 explicit KFilePlacesEventWatcher(KFilePlacesView *parent = nullptr)
131 : QObject(parent)
132 , m_scroller(nullptr)
133 , q(parent)
134 , m_rubberBand(nullptr)
135 , m_isTouchEvent(false)
136 , m_mousePressed(false)
137 , m_tapAndHoldActive(false)
138 , m_lastMouseSource(Qt::MouseEventNotSynthesized)
139 {
140 m_rubberBand = new QRubberBand(QRubberBand::Rectangle, parent);
141 }
142
143 const QModelIndex hoveredHeaderAreaIndex()
144 {
145 return m_hoveredHeaderAreaIndex;
146 }
147
148 const QModelIndex hoveredActionIndex()
149 {
150 return m_hoveredActionIndex;
151 }
152
153 QScroller *m_scroller;
154
155public Q_SLOTS:
156 void qScrollerStateChanged(const QScroller::State newState)
157 {
158 if (newState == QScroller::Inactive) {
159 m_isTouchEvent = false;
160 }
161 }
162
163Q_SIGNALS:
164 void entryMiddleClicked(const QModelIndex &index);
165
166 void headerAreaEntered(const QModelIndex &index);
167 void headerAreaLeft(const QModelIndex &index);
168
169 void actionEntered(const QModelIndex &index);
170 void actionLeft(const QModelIndex &index);
171 void actionClicked(const QModelIndex &index);
172
173 void windowActivated();
174 void windowDeactivated();
175
176 void paletteChanged();
177
178protected:
179 bool eventFilter(QObject *watched, QEvent *event) override
180 {
181 switch (event->type()) {
182 case QEvent::MouseMove: {
183 if (m_isTouchEvent && !m_tapAndHoldActive) {
184 return true;
185 }
186
187 m_tapAndHoldActive = false;
188 if (m_rubberBand->isVisible()) {
189 m_rubberBand->hide();
190 }
191
192 QAbstractItemView *view = qobject_cast<QAbstractItemView *>(object: watched->parent());
193 const QPoint pos = static_cast<QMouseEvent *>(event)->pos();
194 const QModelIndex index = view->indexAt(point: pos);
195
196 QModelIndex headerAreaIndex;
197 QModelIndex actionIndex;
198 if (index.isValid()) {
199 if (auto *delegate = qobject_cast<KFilePlacesViewDelegate *>(object: view->itemDelegate())) {
200 if (delegate->pointIsHeaderArea(pos)) {
201 headerAreaIndex = index;
202 } else if (delegate->pointIsTeardownAction(pos)) {
203 actionIndex = index;
204 }
205 }
206 }
207
208 if (headerAreaIndex != m_hoveredHeaderAreaIndex) {
209 if (m_hoveredHeaderAreaIndex.isValid()) {
210 Q_EMIT headerAreaLeft(index: m_hoveredHeaderAreaIndex);
211 }
212 m_hoveredHeaderAreaIndex = headerAreaIndex;
213 if (headerAreaIndex.isValid()) {
214 Q_EMIT headerAreaEntered(index: headerAreaIndex);
215 }
216 }
217
218 if (actionIndex != m_hoveredActionIndex) {
219 if (m_hoveredActionIndex.isValid()) {
220 Q_EMIT actionLeft(index: m_hoveredActionIndex);
221 }
222 m_hoveredActionIndex = actionIndex;
223 if (actionIndex.isValid()) {
224 Q_EMIT actionEntered(index: actionIndex);
225 }
226 }
227
228 break;
229 }
230 case QEvent::Leave:
231 if (m_hoveredHeaderAreaIndex.isValid()) {
232 Q_EMIT headerAreaLeft(index: m_hoveredHeaderAreaIndex);
233 }
234 m_hoveredHeaderAreaIndex = QModelIndex();
235
236 if (m_hoveredActionIndex.isValid()) {
237 Q_EMIT actionLeft(index: m_hoveredActionIndex);
238 }
239 m_hoveredActionIndex = QModelIndex();
240
241 break;
242 case QEvent::MouseButtonPress: {
243 QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
244 m_mousePressed = true;
245 m_lastMouseSource = mouseEvent->source();
246
247 if (m_isTouchEvent) {
248 return true;
249 }
250 onPressed(mouseEvent);
251 Q_FALLTHROUGH();
252 }
253 case QEvent::MouseButtonDblClick: {
254 // Prevent the selection clearing by clicking on the viewport directly
255 QAbstractItemView *view = qobject_cast<QAbstractItemView *>(object: watched->parent());
256 if (!view->indexAt(point: static_cast<QMouseEvent *>(event)->pos()).isValid()) {
257 return true;
258 }
259 break;
260 }
261 case QEvent::MouseButtonRelease: {
262 QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
263 if (mouseEvent->button() == Qt::LeftButton || mouseEvent->button() == Qt::MiddleButton) {
264 QAbstractItemView *view = qobject_cast<QAbstractItemView *>(object: watched->parent());
265 const QModelIndex index = view->indexAt(point: mouseEvent->pos());
266
267 if (mouseEvent->button() == Qt::LeftButton) {
268 if (m_clickedActionIndex.isValid()) {
269 if (auto *delegate = qobject_cast<KFilePlacesViewDelegate *>(object: view->itemDelegate())) {
270 if (delegate->pointIsTeardownAction(pos: mouseEvent->pos())) {
271 if (m_clickedActionIndex == index) {
272 Q_EMIT actionClicked(index: m_clickedActionIndex);
273 // filter out, avoid QAbstractItemView::clicked being emitted
274 return true;
275 }
276 }
277 }
278 }
279 m_clickedActionIndex = index;
280 } else if (mouseEvent->button() == Qt::MiddleButton) {
281 if (m_middleClickedIndex.isValid() && m_middleClickedIndex == index) {
282 Q_EMIT entryMiddleClicked(index: m_middleClickedIndex);
283 }
284 m_middleClickedIndex = QPersistentModelIndex();
285 }
286 }
287 break;
288 }
289 case QEvent::WindowActivate:
290 Q_EMIT windowActivated();
291 break;
292 case QEvent::WindowDeactivate:
293 Q_EMIT windowDeactivated();
294 break;
295 case QEvent::PaletteChange:
296 Q_EMIT paletteChanged();
297 break;
298 case QEvent::TouchBegin: {
299 m_isTouchEvent = true;
300 m_mousePressed = false;
301 break;
302 }
303 case QEvent::Gesture: {
304 gestureEvent(event: static_cast<QGestureEvent *>(event));
305 event->accept();
306 return true;
307 }
308 default:
309 return false;
310 }
311
312 return false;
313 }
314
315 void onPressed(QMouseEvent *mouseEvent)
316 {
317 if (mouseEvent->button() == Qt::LeftButton || mouseEvent->button() == Qt::MiddleButton) {
318 QAbstractItemView *view = qobject_cast<QAbstractItemView *>(object: q);
319 const QModelIndex index = view->indexAt(point: mouseEvent->pos());
320 if (index.isValid()) {
321 if (mouseEvent->button() == Qt::LeftButton) {
322 if (auto *delegate = qobject_cast<KFilePlacesViewDelegate *>(object: view->itemDelegate())) {
323 if (delegate->pointIsTeardownAction(pos: mouseEvent->pos())) {
324 m_clickedActionIndex = index;
325 }
326 }
327 } else if (mouseEvent->button() == Qt::MiddleButton) {
328 m_middleClickedIndex = index;
329 }
330 }
331 }
332 }
333
334 void gestureEvent(QGestureEvent *event)
335 {
336 if (QGesture *gesture = event->gesture(type: Qt::TapGesture)) {
337 tapTriggered(tap: static_cast<QTapGesture *>(gesture));
338 }
339 if (QGesture *gesture = event->gesture(type: Qt::TapAndHoldGesture)) {
340 tapAndHoldTriggered(tap: static_cast<QTapAndHoldGesture *>(gesture));
341 }
342 }
343
344 void tapAndHoldTriggered(QTapAndHoldGesture *tap)
345 {
346 if (tap->state() == Qt::GestureFinished) {
347 if (!m_mousePressed) {
348 return;
349 }
350
351 // the TapAndHold gesture is triggerable with the mouse and stylus, we don't want this
352 if (m_lastMouseSource == Qt::MouseEventNotSynthesized || !m_isTouchEvent) {
353 return;
354 }
355
356 m_tapAndHoldActive = true;
357 m_scroller->stop();
358
359 // simulate a mousePressEvent, to allow KFilePlacesView to select the items
360 const QPointF tapGlobalPos = tap->position(); // QTapAndHoldGesture::position is global
361 const QPointF tapViewportPos(q->viewport()->mapFromGlobal(tapGlobalPos));
362 QMouseEvent fakeMousePress(QEvent::MouseButtonPress, tapViewportPos, tapGlobalPos, Qt::LeftButton, Qt::LeftButton, Qt::NoModifier);
363 onPressed(mouseEvent: &fakeMousePress);
364 q->mousePressEvent(event: &fakeMousePress);
365
366 const QPoint tapIndicatorSize(80, 80);
367 const QPoint pos(q->mapFromGlobal(tapGlobalPos.toPoint()));
368 const QRect tapIndicatorRect(pos - (tapIndicatorSize / 2), pos + (tapIndicatorSize / 2));
369 m_rubberBand->setGeometry(tapIndicatorRect.normalized());
370 m_rubberBand->show();
371 }
372 }
373
374 void tapTriggered(QTapGesture *tap)
375 {
376 static bool scrollerWasScrolling = false;
377
378 if (tap->state() == Qt::GestureStarted) {
379 m_tapAndHoldActive = false;
380 // if QScroller state is Scrolling or Dragging, the user makes the tap to stop the scrolling
381 auto const scrollerState = m_scroller->state();
382 if (scrollerState == QScroller::Scrolling || scrollerState == QScroller::Dragging) {
383 scrollerWasScrolling = true;
384 } else {
385 scrollerWasScrolling = false;
386 }
387 }
388
389 if (tap->state() == Qt::GestureFinished && !scrollerWasScrolling) {
390 m_isTouchEvent = false;
391
392 // with touch you can touch multiple widgets at the same time, but only one widget will get a mousePressEvent.
393 // we use this to select the right window
394 if (!m_mousePressed) {
395 return;
396 }
397
398 if (m_rubberBand->isVisible()) {
399 m_rubberBand->hide();
400 }
401 // simulate a mousePressEvent, to allow KFilePlacesView to select the items
402 const QPointF tapPosition = tap->position(); // QTapGesture::position is local
403 const QPointF globalTapPosition = q->mapToGlobal(tapPosition);
404 QMouseEvent fakeMousePress(QEvent::MouseButtonPress,
405 tapPosition,
406 globalTapPosition,
407 m_tapAndHoldActive ? Qt::RightButton : Qt::LeftButton,
408 m_tapAndHoldActive ? Qt::RightButton : Qt::LeftButton,
409 Qt::NoModifier);
410 onPressed(mouseEvent: &fakeMousePress);
411 q->mousePressEvent(event: &fakeMousePress);
412
413 if (m_tapAndHoldActive) {
414 // simulate a contextMenuEvent
415 QContextMenuEvent fakeContextMenu(QContextMenuEvent::Mouse, tapPosition.toPoint(), globalTapPosition.toPoint());
416 q->contextMenuEvent(event: &fakeContextMenu);
417 }
418 m_tapAndHoldActive = false;
419 }
420 }
421
422private:
423 QPersistentModelIndex m_hoveredHeaderAreaIndex;
424 QPersistentModelIndex m_middleClickedIndex;
425 QPersistentModelIndex m_hoveredActionIndex;
426 QPersistentModelIndex m_clickedActionIndex;
427
428 KFilePlacesView *const q;
429
430 QRubberBand *m_rubberBand;
431 bool m_isTouchEvent;
432 bool m_mousePressed;
433 bool m_tapAndHoldActive;
434 Qt::MouseEventSource m_lastMouseSource;
435};
436
437#endif
438

source code of kio/src/filewidgets/kfileplacesview_p.h