File indexing completed on 2024-07-14 14:33:55

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"