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 | dirModel->requestSequenceIcon(index, sequenceIndex: currentSequenceIndex); |
204 | iconSequenceTimer.start(); // Some upper-bound interval is needed, in case items are not generated |
205 | } |
206 | } |
207 | |
208 | void 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 | |
220 | void 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 | |
235 | void 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 | |
250 | AnimationState *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 | |
317 | AnimationState *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 | |
334 | void 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 | |
349 | void DelegateAnimationHandler::restartAnimation(AnimationState *state) |
350 | { |
351 | startAnimation(state); |
352 | } |
353 | |
354 | void 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 | |
364 | int 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 | |
406 | void DelegateAnimationHandler::viewDeleted(QObject *view) |
407 | { |
408 | AnimationList *list = animationLists.take(key: static_cast<QAbstractItemView *>(view)); |
409 | qDeleteAll(c: *list); |
410 | delete list; |
411 | } |
412 | |
413 | void 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 | |