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 | class KFilePlacesView; |
25 | class QTimeLine; |
26 | |
27 | struct PlaceFreeSpaceInfo { |
28 | QDeadlineTimer timeout; |
29 | KIO::filesize_t used = 0; |
30 | KIO::filesize_t size = 0; |
31 | QPointer<KIO::FileSystemFreeSpaceJob> job; |
32 | }; |
33 | |
34 | class KFilePlacesViewDelegate : public QAbstractItemDelegate |
35 | { |
36 | Q_OBJECT |
37 | public: |
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 (const QModelIndex &index); |
57 | void setHoveredAction(const QModelIndex &index); |
58 | |
59 | qreal contentsOpacity(const QModelIndex &index) const; |
60 | |
61 | bool (const QPoint &pos) const; |
62 | bool pointIsTeardownAction(const QPoint &pos) const; |
63 | |
64 | void startDrag(); |
65 | |
66 | int (const QModelIndex &index) const; |
67 | bool (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 | |
77 | protected: |
78 | bool helpEvent(QHelpEvent *event, QAbstractItemView *view, const QStyleOptionViewItem &option, const QModelIndex &index) override; |
79 | |
80 | private: |
81 | QString groupNameFromIndex(const QModelIndex &index) const; |
82 | QModelIndex previousVisibleIndex(const QModelIndex &index) const; |
83 | void (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 ; |
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 | |
116 | class KFilePlacesEventWatcher : public QObject |
117 | { |
118 | Q_OBJECT |
119 | |
120 | public: |
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 () |
135 | { |
136 | return m_hoveredHeaderAreaIndex; |
137 | } |
138 | |
139 | const QModelIndex hoveredActionIndex() |
140 | { |
141 | return m_hoveredActionIndex; |
142 | } |
143 | |
144 | QScroller *m_scroller; |
145 | |
146 | public Q_SLOTS: |
147 | void qScrollerStateChanged(const QScroller::State newState) |
148 | { |
149 | if (newState == QScroller::Inactive) { |
150 | m_isTouchEvent = false; |
151 | } |
152 | } |
153 | |
154 | Q_SIGNALS: |
155 | void entryMiddleClicked(const QModelIndex &index); |
156 | |
157 | void (const QModelIndex &index); |
158 | void (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 | |
169 | protected: |
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 ; |
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 (QContextMenuEvent::Mouse, tapPosition.toPoint(), globalTapPosition.toPoint()); |
407 | q->contextMenuEvent(event: &fakeContextMenu); |
408 | } |
409 | m_tapAndHoldActive = false; |
410 | } |
411 | } |
412 | |
413 | private: |
414 | QPersistentModelIndex ; |
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 | |