1 | /* |
2 | This file is part of the KDE project |
3 | SPDX-FileCopyrightText: 2007 Fredrik Höglund <fredrik@kde.org> |
4 | |
5 | SPDX-License-Identifier: LGPL-2.0-or-later |
6 | */ |
7 | |
8 | #include "delegateanimationhandler_p.h" |
9 | |
10 | #include <QAbstractItemView> |
11 | #include <QDebug> |
12 | #include <QPersistentModelIndex> |
13 | #include <QTime> |
14 | |
15 | #include "kdirmodel.h" |
16 | #include <QAbstractProxyModel> |
17 | #include <cmath> |
18 | |
19 | #include "moc_delegateanimationhandler_p.cpp" |
20 | |
21 | namespace KIO |
22 | { |
23 | // Needed because state() is a protected method |
24 | class ProtectedAccessor : public QAbstractItemView |
25 | { |
26 | Q_OBJECT |
27 | public: |
28 | bool draggingState() const |
29 | { |
30 | return state() == DraggingState; |
31 | } |
32 | }; |
33 | |
34 | // Debug output is disabled by default, use kdebugdialog to enable it |
35 | // static int animationDebugArea() { static int s_area = KDebug::registerArea("kio (delegateanimationhandler)", false); |
36 | // return s_area; } |
37 | |
38 | // --------------------------------------------------------------------------- |
39 | |
40 | CachedRendering::CachedRendering(QStyle::State state, const QSize &size, const QModelIndex &index, qreal devicePixelRatio) |
41 | : state(state) |
42 | , regular(QPixmap(size * devicePixelRatio)) |
43 | , hover(QPixmap(size * devicePixelRatio)) |
44 | , valid(true) |
45 | , validityIndex(index) |
46 | { |
47 | regular.setDevicePixelRatio(devicePixelRatio); |
48 | hover.setDevicePixelRatio(devicePixelRatio); |
49 | regular.fill(fillColor: Qt::transparent); |
50 | hover.fill(fillColor: Qt::transparent); |
51 | |
52 | if (index.model()) { |
53 | connect(sender: index.model(), signal: &QAbstractItemModel::dataChanged, context: this, slot: &CachedRendering::dataChanged); |
54 | connect(sender: index.model(), signal: &QAbstractItemModel::modelReset, context: this, slot: &CachedRendering::modelReset); |
55 | } |
56 | } |
57 | |
58 | void CachedRendering::dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) |
59 | { |
60 | if (validityIndex.row() >= topLeft.row() && validityIndex.column() >= topLeft.column() && validityIndex.row() <= bottomRight.row() |
61 | && validityIndex.column() <= bottomRight.column()) { |
62 | valid = false; |
63 | } |
64 | } |
65 | |
66 | void CachedRendering::modelReset() |
67 | { |
68 | valid = false; |
69 | } |
70 | |
71 | // --------------------------------------------------------------------------- |
72 | |
73 | AnimationState::AnimationState(const QModelIndex &index) |
74 | : index(index) |
75 | , direction(QTimeLine::Forward) |
76 | , animating(false) |
77 | , jobAnimation(false) |
78 | , progress(0.0) |
79 | , m_fadeProgress(1.0) |
80 | , m_jobAnimationAngle(0.0) |
81 | , renderCache(nullptr) |
82 | , fadeFromRenderCache(nullptr) |
83 | { |
84 | creationTime.start(); |
85 | } |
86 | |
87 | AnimationState::~AnimationState() |
88 | { |
89 | delete renderCache; |
90 | delete fadeFromRenderCache; |
91 | } |
92 | |
93 | bool AnimationState::update() |
94 | { |
95 | const qreal runtime = (direction == QTimeLine::Forward ? 150 : 250); // milliseconds |
96 | const qreal increment = 1000. / runtime / 1000.; |
97 | const qreal delta = increment * time.restart(); |
98 | |
99 | if (direction == QTimeLine::Forward) { |
100 | progress = qMin(a: qreal(1.0), b: progress + delta); |
101 | animating = (progress < 1.0); |
102 | } else { |
103 | progress = qMax(a: qreal(0.0), b: progress - delta); |
104 | animating = (progress > 0.0); |
105 | } |
106 | |
107 | if (fadeFromRenderCache) { |
108 | // Icon fading goes always forwards |
109 | m_fadeProgress = qMin(a: qreal(1.0), b: m_fadeProgress + delta); |
110 | animating |= (m_fadeProgress < 1.0); |
111 | if (m_fadeProgress == 1) { |
112 | setCachedRenderingFadeFrom(nullptr); |
113 | } |
114 | } |
115 | |
116 | if (jobAnimation) { |
117 | m_jobAnimationAngle += 1.0; |
118 | if (m_jobAnimationAngle == 360) { |
119 | m_jobAnimationAngle = 0; |
120 | } |
121 | |
122 | if (index.model()->data(index, role: KDirModel::HasJobRole).toBool()) { |
123 | animating = true; |
124 | // there is a job here still... |
125 | return false; |
126 | } else { |
127 | animating = false; |
128 | // there's no job here anymore, return true so we stop painting this. |
129 | return true; |
130 | } |
131 | } else { |
132 | return !animating; |
133 | } |
134 | } |
135 | |
136 | static constexpr double s_mPI2 = 1.57079632679489661923; |
137 | |
138 | qreal AnimationState::hoverProgress() const |
139 | { |
140 | return qRound(d: 255.0 * std::sin(x: progress * s_mPI2)) / 255.0; |
141 | } |
142 | |
143 | qreal AnimationState::fadeProgress() const |
144 | { |
145 | return qRound(d: 255.0 * std::sin(x: m_fadeProgress * s_mPI2)) / 255.0; |
146 | } |
147 | |
148 | qreal AnimationState::jobAnimationAngle() const |
149 | { |
150 | return m_jobAnimationAngle; |
151 | } |
152 | |
153 | bool AnimationState::hasJobAnimation() const |
154 | { |
155 | return jobAnimation; |
156 | } |
157 | |
158 | void AnimationState::setJobAnimation(bool value) |
159 | { |
160 | jobAnimation = value; |
161 | } |
162 | |
163 | // --------------------------------------------------------------------------- |
164 | |
165 | static const int switchIconInterval = 1000; ///@todo Eventually configurable interval? |
166 | |
167 | DelegateAnimationHandler::DelegateAnimationHandler(QObject *parent) |
168 | : QObject(parent) |
169 | { |
170 | iconSequenceTimer.setSingleShot(true); |
171 | iconSequenceTimer.setInterval(switchIconInterval); |
172 | connect(sender: &iconSequenceTimer, signal: &QTimer::timeout, context: this, slot: &DelegateAnimationHandler::sequenceTimerTimeout); |
173 | ; |
174 | } |
175 | |
176 | DelegateAnimationHandler::~DelegateAnimationHandler() |
177 | { |
178 | timer.stop(); |
179 | |
180 | QMapIterator<const QAbstractItemView *, AnimationList *> i(animationLists); |
181 | while (i.hasNext()) { |
182 | i.next(); |
183 | qDeleteAll(c: *i.value()); |
184 | delete i.value(); |
185 | } |
186 | animationLists.clear(); |
187 | } |
188 | |
189 | void DelegateAnimationHandler::sequenceTimerTimeout() |
190 | { |
191 | QAbstractItemModel *model = const_cast<QAbstractItemModel *>(sequenceModelIndex.model()); |
192 | QAbstractProxyModel *proxy = qobject_cast<QAbstractProxyModel *>(object: model); |
193 | QModelIndex index = sequenceModelIndex; |
194 | |
195 | if (proxy) { |
196 | index = proxy->mapToSource(proxyIndex: index); |
197 | model = proxy->sourceModel(); |
198 | } |
199 | |
200 | KDirModel *dirModel = dynamic_cast<KDirModel *>(model); |
201 | if (dirModel) { |
202 | // qDebug() << "requesting" << currentSequenceIndex; |
203 | // Only request sequence icons for items that have them |
204 | if (dirModel->data(index, role: KDirModel::HandleSequencesRole).toBool()) { |
205 | dirModel->requestSequenceIcon(index, sequenceIndex: currentSequenceIndex); |
206 | iconSequenceTimer.start(); // Some upper-bound interval is needed, in case items are not generated |
207 | } |
208 | } |
209 | } |
210 | |
211 | void DelegateAnimationHandler::gotNewIcon(const QModelIndex &index) |
212 | { |
213 | Q_UNUSED(index); |
214 | |
215 | // qDebug() << currentSequenceIndex; |
216 | if (sequenceModelIndex.isValid() && currentSequenceIndex) { |
217 | iconSequenceTimer.start(); |
218 | } |
219 | // if(index ==sequenceModelIndex) //Leads to problems |
220 | ++currentSequenceIndex; |
221 | } |
222 | |
223 | void DelegateAnimationHandler::setSequenceIndex(int sequenceIndex) |
224 | { |
225 | // qDebug() << sequenceIndex; |
226 | |
227 | if (sequenceIndex > 0) { |
228 | currentSequenceIndex = sequenceIndex; |
229 | iconSequenceTimer.start(); |
230 | } else { |
231 | currentSequenceIndex = 0; |
232 | sequenceTimerTimeout(); // Set the icon back to the standard one |
233 | currentSequenceIndex = 0; // currentSequenceIndex was incremented, set it back to 0 |
234 | iconSequenceTimer.stop(); |
235 | } |
236 | } |
237 | |
238 | void DelegateAnimationHandler::eventuallyStartIteration(const QModelIndex &index) |
239 | { |
240 | // if (KGlobalSettings::graphicEffectsLevel() & KGlobalSettings::SimpleAnimationEffects) { |
241 | /// Think about it. |
242 | |
243 | if (sequenceModelIndex.isValid()) { |
244 | setSequenceIndex(0); // Stop old iteration, and reset the icon for the old iteration |
245 | } |
246 | |
247 | // Start sequence iteration |
248 | sequenceModelIndex = index; |
249 | setSequenceIndex(1); |
250 | // } |
251 | } |
252 | |
253 | AnimationState *DelegateAnimationHandler::animationState(const QStyleOption &option, const QModelIndex &index, const QAbstractItemView *view) |
254 | { |
255 | // We can't do animations reliably when an item is being dragged, since that |
256 | // item will be drawn in two locations at the same time and hovered in one and |
257 | // not the other. We can't tell them apart because they both have the same index. |
258 | if (!view || static_cast<const ProtectedAccessor *>(view)->draggingState()) { |
259 | return nullptr; |
260 | } |
261 | |
262 | AnimationState *state = findAnimationState(view, index); |
263 | bool hover = option.state & QStyle::State_MouseOver; |
264 | |
265 | // If the cursor has entered an item |
266 | if (!state && hover) { |
267 | state = new AnimationState(index); |
268 | addAnimationState(state, view); |
269 | |
270 | if (!fadeInAddTime.isValid() || (fadeInAddTime.isValid() && fadeInAddTime.elapsed() > 300)) { |
271 | startAnimation(state); |
272 | } else { |
273 | state->animating = false; |
274 | state->progress = 1.0; |
275 | state->direction = QTimeLine::Forward; |
276 | } |
277 | |
278 | fadeInAddTime.restart(); |
279 | |
280 | eventuallyStartIteration(index); |
281 | } else if (state) { |
282 | // If the cursor has exited an item |
283 | if (!hover && (!state->animating || state->direction == QTimeLine::Forward)) { |
284 | state->direction = QTimeLine::Backward; |
285 | |
286 | if (state->creationTime.elapsed() < 200) { |
287 | state->progress = 0.0; |
288 | } |
289 | |
290 | startAnimation(state); |
291 | |
292 | // Stop sequence iteration |
293 | if (index == sequenceModelIndex) { |
294 | setSequenceIndex(0); |
295 | sequenceModelIndex = QPersistentModelIndex(); |
296 | } |
297 | } else if (hover && state->direction == QTimeLine::Backward) { |
298 | // This is needed to handle the case where an item is dragged within |
299 | // the view, and dropped in a different location. State_MouseOver will |
300 | // initially not be set causing a "hover out" animation to start. |
301 | // This reverses the direction as soon as we see the bit being set. |
302 | state->direction = QTimeLine::Forward; |
303 | |
304 | if (!state->animating) { |
305 | startAnimation(state); |
306 | } |
307 | |
308 | eventuallyStartIteration(index); |
309 | } |
310 | } else if (!state && index.model()->data(index, role: KDirModel::HasJobRole).toBool()) { |
311 | state = new AnimationState(index); |
312 | addAnimationState(state, view); |
313 | startAnimation(state); |
314 | state->setJobAnimation(true); |
315 | } |
316 | |
317 | return state; |
318 | } |
319 | |
320 | AnimationState *DelegateAnimationHandler::findAnimationState(const QAbstractItemView *view, const QModelIndex &index) const |
321 | { |
322 | // Try to find a list of animation states for the view |
323 | const AnimationList *list = animationLists.value(key: view); |
324 | |
325 | if (list) { |
326 | auto it = std::find_if(first: list->cbegin(), last: list->cend(), pred: [&index](AnimationState *state) { |
327 | return state->index == index; |
328 | }); |
329 | if (it != list->cend()) { |
330 | return *it; |
331 | } |
332 | } |
333 | |
334 | return nullptr; |
335 | } |
336 | |
337 | void DelegateAnimationHandler::addAnimationState(AnimationState *state, const QAbstractItemView *view) |
338 | { |
339 | AnimationList *list = animationLists.value(key: view); |
340 | |
341 | // If this is the first time we've seen this view |
342 | if (!list) { |
343 | connect(sender: view, signal: &QObject::destroyed, context: this, slot: &DelegateAnimationHandler::viewDeleted); |
344 | |
345 | list = new AnimationList; |
346 | animationLists.insert(key: view, value: list); |
347 | } |
348 | |
349 | list->append(t: state); |
350 | } |
351 | |
352 | void DelegateAnimationHandler::restartAnimation(AnimationState *state) |
353 | { |
354 | startAnimation(state); |
355 | } |
356 | |
357 | void DelegateAnimationHandler::startAnimation(AnimationState *state) |
358 | { |
359 | state->time.start(); |
360 | state->animating = true; |
361 | |
362 | if (!timer.isActive()) { |
363 | timer.start(msec: 1000 / 30, obj: this); // 30 fps |
364 | } |
365 | } |
366 | |
367 | int DelegateAnimationHandler::runAnimations(AnimationList *list, const QAbstractItemView *view) |
368 | { |
369 | int activeAnimations = 0; |
370 | QRegion region; |
371 | |
372 | QMutableListIterator<AnimationState *> i(*list); |
373 | while (i.hasNext()) { |
374 | AnimationState *state = i.next(); |
375 | |
376 | if (!state->animating) { |
377 | continue; |
378 | } |
379 | |
380 | // We need to make sure the index is still valid, since it could be removed |
381 | // while the animation is running. |
382 | if (state->index.isValid()) { |
383 | bool finished = state->update(); |
384 | region += view->visualRect(index: state->index); |
385 | |
386 | if (!finished) { |
387 | activeAnimations++; |
388 | continue; |
389 | } |
390 | } |
391 | |
392 | // If the direction is Forward, the state object needs to stick around |
393 | // after the animation has finished, so we know that we've already done |
394 | // a "hover in" for the index. |
395 | if (state->direction == QTimeLine::Backward || !state->index.isValid()) { |
396 | delete state; |
397 | i.remove(); |
398 | } |
399 | } |
400 | |
401 | // Trigger a repaint of the animated indexes |
402 | if (!region.isEmpty()) { |
403 | const_cast<QAbstractItemView *>(view)->viewport()->update(region); |
404 | } |
405 | |
406 | return activeAnimations; |
407 | } |
408 | |
409 | void DelegateAnimationHandler::viewDeleted(QObject *view) |
410 | { |
411 | AnimationList *list = animationLists.take(key: static_cast<QAbstractItemView *>(view)); |
412 | qDeleteAll(c: *list); |
413 | delete list; |
414 | } |
415 | |
416 | void DelegateAnimationHandler::timerEvent(QTimerEvent *) |
417 | { |
418 | int activeAnimations = 0; |
419 | |
420 | AnimationListsIterator i(animationLists); |
421 | while (i.hasNext()) { |
422 | i.next(); |
423 | AnimationList *list = i.value(); |
424 | const QAbstractItemView *view = i.key(); |
425 | |
426 | activeAnimations += runAnimations(list, view); |
427 | } |
428 | |
429 | if (activeAnimations == 0 && timer.isActive()) { |
430 | timer.stop(); |
431 | } |
432 | } |
433 | |
434 | } |
435 | |
436 | #include "delegateanimationhandler.moc" |
437 | |