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

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