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 // 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
211void 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
223void 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
238void 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
253AnimationState *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
320AnimationState *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
337void 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
352void DelegateAnimationHandler::restartAnimation(AnimationState *state)
353{
354 startAnimation(state);
355}
356
357void 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
367int 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
409void DelegateAnimationHandler::viewDeleted(QObject *view)
410{
411 AnimationList *list = animationLists.take(key: static_cast<QAbstractItemView *>(view));
412 qDeleteAll(c: *list);
413 delete list;
414}
415
416void 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

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