File indexing completed on 2024-09-01 03:46:10
0001 /* 0002 This file is part of the KDE project 0003 SPDX-FileCopyrightText: 2007 Fredrik Höglund <fredrik@kde.org> 0004 0005 SPDX-License-Identifier: LGPL-2.0-or-later 0006 */ 0007 0008 #include "delegateanimationhandler_p.h" 0009 0010 #include <QAbstractItemView> 0011 #include <QDebug> 0012 #include <QPersistentModelIndex> 0013 #include <QTime> 0014 0015 #include "kdirmodel.h" 0016 #include <QAbstractProxyModel> 0017 #include <cmath> 0018 0019 #include "moc_delegateanimationhandler_p.cpp" 0020 0021 namespace KIO 0022 { 0023 // Needed because state() is a protected method 0024 class ProtectedAccessor : public QAbstractItemView 0025 { 0026 Q_OBJECT 0027 public: 0028 bool draggingState() const 0029 { 0030 return state() == DraggingState; 0031 } 0032 }; 0033 0034 // Debug output is disabled by default, use kdebugdialog to enable it 0035 // static int animationDebugArea() { static int s_area = KDebug::registerArea("kio (delegateanimationhandler)", false); 0036 // return s_area; } 0037 0038 // --------------------------------------------------------------------------- 0039 0040 CachedRendering::CachedRendering(QStyle::State state, const QSize &size, const QModelIndex &index, qreal devicePixelRatio) 0041 : state(state) 0042 , regular(QPixmap(size * devicePixelRatio)) 0043 , hover(QPixmap(size * devicePixelRatio)) 0044 , valid(true) 0045 , validityIndex(index) 0046 { 0047 regular.setDevicePixelRatio(devicePixelRatio); 0048 hover.setDevicePixelRatio(devicePixelRatio); 0049 regular.fill(Qt::transparent); 0050 hover.fill(Qt::transparent); 0051 0052 if (index.model()) { 0053 connect(index.model(), &QAbstractItemModel::dataChanged, this, &CachedRendering::dataChanged); 0054 connect(index.model(), &QAbstractItemModel::modelReset, this, &CachedRendering::modelReset); 0055 } 0056 } 0057 0058 void CachedRendering::dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) 0059 { 0060 if (validityIndex.row() >= topLeft.row() && validityIndex.column() >= topLeft.column() && validityIndex.row() <= bottomRight.row() 0061 && validityIndex.column() <= bottomRight.column()) { 0062 valid = false; 0063 } 0064 } 0065 0066 void CachedRendering::modelReset() 0067 { 0068 valid = false; 0069 } 0070 0071 // --------------------------------------------------------------------------- 0072 0073 AnimationState::AnimationState(const QModelIndex &index) 0074 : index(index) 0075 , direction(QTimeLine::Forward) 0076 , animating(false) 0077 , jobAnimation(false) 0078 , progress(0.0) 0079 , m_fadeProgress(1.0) 0080 , m_jobAnimationAngle(0.0) 0081 , renderCache(nullptr) 0082 , fadeFromRenderCache(nullptr) 0083 { 0084 creationTime.start(); 0085 } 0086 0087 AnimationState::~AnimationState() 0088 { 0089 delete renderCache; 0090 delete fadeFromRenderCache; 0091 } 0092 0093 bool AnimationState::update() 0094 { 0095 const qreal runtime = (direction == QTimeLine::Forward ? 150 : 250); // milliseconds 0096 const qreal increment = 1000. / runtime / 1000.; 0097 const qreal delta = increment * time.restart(); 0098 0099 if (direction == QTimeLine::Forward) { 0100 progress = qMin(qreal(1.0), progress + delta); 0101 animating = (progress < 1.0); 0102 } else { 0103 progress = qMax(qreal(0.0), progress - delta); 0104 animating = (progress > 0.0); 0105 } 0106 0107 if (fadeFromRenderCache) { 0108 // Icon fading goes always forwards 0109 m_fadeProgress = qMin(qreal(1.0), m_fadeProgress + delta); 0110 animating |= (m_fadeProgress < 1.0); 0111 if (m_fadeProgress == 1) { 0112 setCachedRenderingFadeFrom(nullptr); 0113 } 0114 } 0115 0116 if (jobAnimation) { 0117 m_jobAnimationAngle += 1.0; 0118 if (m_jobAnimationAngle == 360) { 0119 m_jobAnimationAngle = 0; 0120 } 0121 0122 if (index.model()->data(index, KDirModel::HasJobRole).toBool()) { 0123 animating = true; 0124 // there is a job here still... 0125 return false; 0126 } else { 0127 animating = false; 0128 // there's no job here anymore, return true so we stop painting this. 0129 return true; 0130 } 0131 } else { 0132 return !animating; 0133 } 0134 } 0135 0136 static constexpr double s_mPI2 = 1.57079632679489661923; 0137 0138 qreal AnimationState::hoverProgress() const 0139 { 0140 return qRound(255.0 * std::sin(progress * s_mPI2)) / 255.0; 0141 } 0142 0143 qreal AnimationState::fadeProgress() const 0144 { 0145 return qRound(255.0 * std::sin(m_fadeProgress * s_mPI2)) / 255.0; 0146 } 0147 0148 qreal AnimationState::jobAnimationAngle() const 0149 { 0150 return m_jobAnimationAngle; 0151 } 0152 0153 bool AnimationState::hasJobAnimation() const 0154 { 0155 return jobAnimation; 0156 } 0157 0158 void AnimationState::setJobAnimation(bool value) 0159 { 0160 jobAnimation = value; 0161 } 0162 0163 // --------------------------------------------------------------------------- 0164 0165 static const int switchIconInterval = 1000; ///@todo Eventually configurable interval? 0166 0167 DelegateAnimationHandler::DelegateAnimationHandler(QObject *parent) 0168 : QObject(parent) 0169 { 0170 iconSequenceTimer.setSingleShot(true); 0171 iconSequenceTimer.setInterval(switchIconInterval); 0172 connect(&iconSequenceTimer, &QTimer::timeout, this, &DelegateAnimationHandler::sequenceTimerTimeout); 0173 ; 0174 } 0175 0176 DelegateAnimationHandler::~DelegateAnimationHandler() 0177 { 0178 timer.stop(); 0179 0180 QMapIterator<const QAbstractItemView *, AnimationList *> i(animationLists); 0181 while (i.hasNext()) { 0182 i.next(); 0183 qDeleteAll(*i.value()); 0184 delete i.value(); 0185 } 0186 animationLists.clear(); 0187 } 0188 0189 void DelegateAnimationHandler::sequenceTimerTimeout() 0190 { 0191 QAbstractItemModel *model = const_cast<QAbstractItemModel *>(sequenceModelIndex.model()); 0192 QAbstractProxyModel *proxy = qobject_cast<QAbstractProxyModel *>(model); 0193 QModelIndex index = sequenceModelIndex; 0194 0195 if (proxy) { 0196 index = proxy->mapToSource(index); 0197 model = proxy->sourceModel(); 0198 } 0199 0200 KDirModel *dirModel = dynamic_cast<KDirModel *>(model); 0201 if (dirModel) { 0202 // qDebug() << "requesting" << currentSequenceIndex; 0203 dirModel->requestSequenceIcon(index, currentSequenceIndex); 0204 iconSequenceTimer.start(); // Some upper-bound interval is needed, in case items are not generated 0205 } 0206 } 0207 0208 void DelegateAnimationHandler::gotNewIcon(const QModelIndex &index) 0209 { 0210 Q_UNUSED(index); 0211 0212 // qDebug() << currentSequenceIndex; 0213 if (sequenceModelIndex.isValid() && currentSequenceIndex) { 0214 iconSequenceTimer.start(); 0215 } 0216 // if(index ==sequenceModelIndex) //Leads to problems 0217 ++currentSequenceIndex; 0218 } 0219 0220 void DelegateAnimationHandler::setSequenceIndex(int sequenceIndex) 0221 { 0222 // qDebug() << sequenceIndex; 0223 0224 if (sequenceIndex > 0) { 0225 currentSequenceIndex = sequenceIndex; 0226 iconSequenceTimer.start(); 0227 } else { 0228 currentSequenceIndex = 0; 0229 sequenceTimerTimeout(); // Set the icon back to the standard one 0230 currentSequenceIndex = 0; // currentSequenceIndex was incremented, set it back to 0 0231 iconSequenceTimer.stop(); 0232 } 0233 } 0234 0235 void DelegateAnimationHandler::eventuallyStartIteration(const QModelIndex &index) 0236 { 0237 // if (KGlobalSettings::graphicEffectsLevel() & KGlobalSettings::SimpleAnimationEffects) { 0238 /// Think about it. 0239 0240 if (sequenceModelIndex.isValid()) { 0241 setSequenceIndex(0); // Stop old iteration, and reset the icon for the old iteration 0242 } 0243 0244 // Start sequence iteration 0245 sequenceModelIndex = index; 0246 setSequenceIndex(1); 0247 // } 0248 } 0249 0250 AnimationState *DelegateAnimationHandler::animationState(const QStyleOption &option, const QModelIndex &index, const QAbstractItemView *view) 0251 { 0252 // We can't do animations reliably when an item is being dragged, since that 0253 // item will be drawn in two locations at the same time and hovered in one and 0254 // not the other. We can't tell them apart because they both have the same index. 0255 if (!view || static_cast<const ProtectedAccessor *>(view)->draggingState()) { 0256 return nullptr; 0257 } 0258 0259 AnimationState *state = findAnimationState(view, index); 0260 bool hover = option.state & QStyle::State_MouseOver; 0261 0262 // If the cursor has entered an item 0263 if (!state && hover) { 0264 state = new AnimationState(index); 0265 addAnimationState(state, view); 0266 0267 if (!fadeInAddTime.isValid() || (fadeInAddTime.isValid() && fadeInAddTime.elapsed() > 300)) { 0268 startAnimation(state); 0269 } else { 0270 state->animating = false; 0271 state->progress = 1.0; 0272 state->direction = QTimeLine::Forward; 0273 } 0274 0275 fadeInAddTime.restart(); 0276 0277 eventuallyStartIteration(index); 0278 } else if (state) { 0279 // If the cursor has exited an item 0280 if (!hover && (!state->animating || state->direction == QTimeLine::Forward)) { 0281 state->direction = QTimeLine::Backward; 0282 0283 if (state->creationTime.elapsed() < 200) { 0284 state->progress = 0.0; 0285 } 0286 0287 startAnimation(state); 0288 0289 // Stop sequence iteration 0290 if (index == sequenceModelIndex) { 0291 setSequenceIndex(0); 0292 sequenceModelIndex = QPersistentModelIndex(); 0293 } 0294 } else if (hover && state->direction == QTimeLine::Backward) { 0295 // This is needed to handle the case where an item is dragged within 0296 // the view, and dropped in a different location. State_MouseOver will 0297 // initially not be set causing a "hover out" animation to start. 0298 // This reverses the direction as soon as we see the bit being set. 0299 state->direction = QTimeLine::Forward; 0300 0301 if (!state->animating) { 0302 startAnimation(state); 0303 } 0304 0305 eventuallyStartIteration(index); 0306 } 0307 } else if (!state && index.model()->data(index, KDirModel::HasJobRole).toBool()) { 0308 state = new AnimationState(index); 0309 addAnimationState(state, view); 0310 startAnimation(state); 0311 state->setJobAnimation(true); 0312 } 0313 0314 return state; 0315 } 0316 0317 AnimationState *DelegateAnimationHandler::findAnimationState(const QAbstractItemView *view, const QModelIndex &index) const 0318 { 0319 // Try to find a list of animation states for the view 0320 const AnimationList *list = animationLists.value(view); 0321 0322 if (list) { 0323 auto it = std::find_if(list->cbegin(), list->cend(), [&index](AnimationState *state) { 0324 return state->index == index; 0325 }); 0326 if (it != list->cend()) { 0327 return *it; 0328 } 0329 } 0330 0331 return nullptr; 0332 } 0333 0334 void DelegateAnimationHandler::addAnimationState(AnimationState *state, const QAbstractItemView *view) 0335 { 0336 AnimationList *list = animationLists.value(view); 0337 0338 // If this is the first time we've seen this view 0339 if (!list) { 0340 connect(view, &QObject::destroyed, this, &DelegateAnimationHandler::viewDeleted); 0341 0342 list = new AnimationList; 0343 animationLists.insert(view, list); 0344 } 0345 0346 list->append(state); 0347 } 0348 0349 void DelegateAnimationHandler::restartAnimation(AnimationState *state) 0350 { 0351 startAnimation(state); 0352 } 0353 0354 void DelegateAnimationHandler::startAnimation(AnimationState *state) 0355 { 0356 state->time.start(); 0357 state->animating = true; 0358 0359 if (!timer.isActive()) { 0360 timer.start(1000 / 30, this); // 30 fps 0361 } 0362 } 0363 0364 int DelegateAnimationHandler::runAnimations(AnimationList *list, const QAbstractItemView *view) 0365 { 0366 int activeAnimations = 0; 0367 QRegion region; 0368 0369 QMutableListIterator<AnimationState *> i(*list); 0370 while (i.hasNext()) { 0371 AnimationState *state = i.next(); 0372 0373 if (!state->animating) { 0374 continue; 0375 } 0376 0377 // We need to make sure the index is still valid, since it could be removed 0378 // while the animation is running. 0379 if (state->index.isValid()) { 0380 bool finished = state->update(); 0381 region += view->visualRect(state->index); 0382 0383 if (!finished) { 0384 activeAnimations++; 0385 continue; 0386 } 0387 } 0388 0389 // If the direction is Forward, the state object needs to stick around 0390 // after the animation has finished, so we know that we've already done 0391 // a "hover in" for the index. 0392 if (state->direction == QTimeLine::Backward || !state->index.isValid()) { 0393 delete state; 0394 i.remove(); 0395 } 0396 } 0397 0398 // Trigger a repaint of the animated indexes 0399 if (!region.isEmpty()) { 0400 const_cast<QAbstractItemView *>(view)->viewport()->update(region); 0401 } 0402 0403 return activeAnimations; 0404 } 0405 0406 void DelegateAnimationHandler::viewDeleted(QObject *view) 0407 { 0408 AnimationList *list = animationLists.take(static_cast<QAbstractItemView *>(view)); 0409 qDeleteAll(*list); 0410 delete list; 0411 } 0412 0413 void DelegateAnimationHandler::timerEvent(QTimerEvent *) 0414 { 0415 int activeAnimations = 0; 0416 0417 AnimationListsIterator i(animationLists); 0418 while (i.hasNext()) { 0419 i.next(); 0420 AnimationList *list = i.value(); 0421 const QAbstractItemView *view = i.key(); 0422 0423 activeAnimations += runAnimations(list, view); 0424 } 0425 0426 if (activeAnimations == 0 && timer.isActive()) { 0427 timer.stop(); 0428 } 0429 } 0430 0431 } 0432 0433 #include "delegateanimationhandler.moc"