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
21namespace KIO
22{
23// Needed because state() is a protected method
24class ProtectedAccessor : public QAbstractItemView
25{
26 Q_OBJECT
27public:
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
40CachedRendering::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
58void 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
66void CachedRendering::modelReset()
67{
68 valid = false;
69}
70
71// ---------------------------------------------------------------------------
72
73AnimationState::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
87AnimationState::~AnimationState()
88{
89 delete renderCache;
90 delete fadeFromRenderCache;
91}
92
93bool 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
136static constexpr double s_mPI2 = 1.57079632679489661923;
137
138qreal AnimationState::hoverProgress() const
139{
140 return qRound(d: 255.0 * std::sin(x: progress * s_mPI2)) / 255.0;
141}
142
143qreal AnimationState::fadeProgress() const
144{
145 return qRound(d: 255.0 * std::sin(x: m_fadeProgress * s_mPI2)) / 255.0;
146}
147
148qreal AnimationState::jobAnimationAngle() const
149{
150 return m_jobAnimationAngle;
151}
152
153bool AnimationState::hasJobAnimation() const
154{
155 return jobAnimation;
156}
157
158void AnimationState::setJobAnimation(bool value)
159{
160 jobAnimation = value;
161}
162
163// ---------------------------------------------------------------------------
164
165static const int switchIconInterval = 1000; ///@todo Eventually configurable interval?
166
167DelegateAnimationHandler::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
176DelegateAnimationHandler::~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
189void 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 dirModel->requestSequenceIcon(index, sequenceIndex: currentSequenceIndex);
204 iconSequenceTimer.start(); // Some upper-bound interval is needed, in case items are not generated
205 }
206}
207
208void DelegateAnimationHandler::gotNewIcon(const QModelIndex &index)
209{
210 Q_UNUSED(index);
211
212 // qDebug() << currentSequenceIndex;
213 if (sequenceModelIndex.isValid() && currentSequenceIndex) {
214 iconSequenceTimer.start();
215 }
216 // if(index ==sequenceModelIndex) //Leads to problems
217 ++currentSequenceIndex;
218}
219
220void DelegateAnimationHandler::setSequenceIndex(int sequenceIndex)
221{
222 // qDebug() << sequenceIndex;
223
224 if (sequenceIndex > 0) {
225 currentSequenceIndex = sequenceIndex;
226 iconSequenceTimer.start();
227 } else {
228 currentSequenceIndex = 0;
229 sequenceTimerTimeout(); // Set the icon back to the standard one
230 currentSequenceIndex = 0; // currentSequenceIndex was incremented, set it back to 0
231 iconSequenceTimer.stop();
232 }
233}
234
235void DelegateAnimationHandler::eventuallyStartIteration(const QModelIndex &index)
236{
237 // if (KGlobalSettings::graphicEffectsLevel() & KGlobalSettings::SimpleAnimationEffects) {
238 /// Think about it.
239
240 if (sequenceModelIndex.isValid()) {
241 setSequenceIndex(0); // Stop old iteration, and reset the icon for the old iteration
242 }
243
244 // Start sequence iteration
245 sequenceModelIndex = index;
246 setSequenceIndex(1);
247 // }
248}
249
250AnimationState *DelegateAnimationHandler::animationState(const QStyleOption &option, const QModelIndex &index, const QAbstractItemView *view)
251{
252 // We can't do animations reliably when an item is being dragged, since that
253 // item will be drawn in two locations at the same time and hovered in one and
254 // not the other. We can't tell them apart because they both have the same index.
255 if (!view || static_cast<const ProtectedAccessor *>(view)->draggingState()) {
256 return nullptr;
257 }
258
259 AnimationState *state = findAnimationState(view, index);
260 bool hover = option.state & QStyle::State_MouseOver;
261
262 // If the cursor has entered an item
263 if (!state && hover) {
264 state = new AnimationState(index);
265 addAnimationState(state, view);
266
267 if (!fadeInAddTime.isValid() || (fadeInAddTime.isValid() && fadeInAddTime.elapsed() > 300)) {
268 startAnimation(state);
269 } else {
270 state->animating = false;
271 state->progress = 1.0;
272 state->direction = QTimeLine::Forward;
273 }
274
275 fadeInAddTime.restart();
276
277 eventuallyStartIteration(index);
278 } else if (state) {
279 // If the cursor has exited an item
280 if (!hover && (!state->animating || state->direction == QTimeLine::Forward)) {
281 state->direction = QTimeLine::Backward;
282
283 if (state->creationTime.elapsed() < 200) {
284 state->progress = 0.0;
285 }
286
287 startAnimation(state);
288
289 // Stop sequence iteration
290 if (index == sequenceModelIndex) {
291 setSequenceIndex(0);
292 sequenceModelIndex = QPersistentModelIndex();
293 }
294 } else if (hover && state->direction == QTimeLine::Backward) {
295 // This is needed to handle the case where an item is dragged within
296 // the view, and dropped in a different location. State_MouseOver will
297 // initially not be set causing a "hover out" animation to start.
298 // This reverses the direction as soon as we see the bit being set.
299 state->direction = QTimeLine::Forward;
300
301 if (!state->animating) {
302 startAnimation(state);
303 }
304
305 eventuallyStartIteration(index);
306 }
307 } else if (!state && index.model()->data(index, role: KDirModel::HasJobRole).toBool()) {
308 state = new AnimationState(index);
309 addAnimationState(state, view);
310 startAnimation(state);
311 state->setJobAnimation(true);
312 }
313
314 return state;
315}
316
317AnimationState *DelegateAnimationHandler::findAnimationState(const QAbstractItemView *view, const QModelIndex &index) const
318{
319 // Try to find a list of animation states for the view
320 const AnimationList *list = animationLists.value(key: view);
321
322 if (list) {
323 auto it = std::find_if(first: list->cbegin(), last: list->cend(), pred: [&index](AnimationState *state) {
324 return state->index == index;
325 });
326 if (it != list->cend()) {
327 return *it;
328 }
329 }
330
331 return nullptr;
332}
333
334void DelegateAnimationHandler::addAnimationState(AnimationState *state, const QAbstractItemView *view)
335{
336 AnimationList *list = animationLists.value(key: view);
337
338 // If this is the first time we've seen this view
339 if (!list) {
340 connect(sender: view, signal: &QObject::destroyed, context: this, slot: &DelegateAnimationHandler::viewDeleted);
341
342 list = new AnimationList;
343 animationLists.insert(key: view, value: list);
344 }
345
346 list->append(t: state);
347}
348
349void DelegateAnimationHandler::restartAnimation(AnimationState *state)
350{
351 startAnimation(state);
352}
353
354void DelegateAnimationHandler::startAnimation(AnimationState *state)
355{
356 state->time.start();
357 state->animating = true;
358
359 if (!timer.isActive()) {
360 timer.start(msec: 1000 / 30, obj: this); // 30 fps
361 }
362}
363
364int DelegateAnimationHandler::runAnimations(AnimationList *list, const QAbstractItemView *view)
365{
366 int activeAnimations = 0;
367 QRegion region;
368
369 QMutableListIterator<AnimationState *> i(*list);
370 while (i.hasNext()) {
371 AnimationState *state = i.next();
372
373 if (!state->animating) {
374 continue;
375 }
376
377 // We need to make sure the index is still valid, since it could be removed
378 // while the animation is running.
379 if (state->index.isValid()) {
380 bool finished = state->update();
381 region += view->visualRect(index: state->index);
382
383 if (!finished) {
384 activeAnimations++;
385 continue;
386 }
387 }
388
389 // If the direction is Forward, the state object needs to stick around
390 // after the animation has finished, so we know that we've already done
391 // a "hover in" for the index.
392 if (state->direction == QTimeLine::Backward || !state->index.isValid()) {
393 delete state;
394 i.remove();
395 }
396 }
397
398 // Trigger a repaint of the animated indexes
399 if (!region.isEmpty()) {
400 const_cast<QAbstractItemView *>(view)->viewport()->update(region);
401 }
402
403 return activeAnimations;
404}
405
406void DelegateAnimationHandler::viewDeleted(QObject *view)
407{
408 AnimationList *list = animationLists.take(key: static_cast<QAbstractItemView *>(view));
409 qDeleteAll(c: *list);
410 delete list;
411}
412
413void DelegateAnimationHandler::timerEvent(QTimerEvent *)
414{
415 int activeAnimations = 0;
416
417 AnimationListsIterator i(animationLists);
418 while (i.hasNext()) {
419 i.next();
420 AnimationList *list = i.value();
421 const QAbstractItemView *view = i.key();
422
423 activeAnimations += runAnimations(list, view);
424 }
425
426 if (activeAnimations == 0 && timer.isActive()) {
427 timer.stop();
428 }
429}
430
431}
432
433#include "delegateanimationhandler.moc"
434

source code of kio/src/widgets/delegateanimationhandler.cpp