/* This file is part of the KDE project SPDX-FileCopyrightText: 2007 Fredrik Höglund SPDX-License-Identifier: LGPL-2.0-or-later */ #include "delegateanimationhandler_p.h" #include #include #include #include #include "kdirmodel.h" #include #include #include "moc_delegateanimationhandler_p.cpp" namespace KIO { // Needed because state() is a protected method class ProtectedAccessor : public QAbstractItemView { Q_OBJECT public: bool draggingState() const { return state() == DraggingState; } }; // Debug output is disabled by default, use kdebugdialog to enable it // static int animationDebugArea() { static int s_area = KDebug::registerArea("kio (delegateanimationhandler)", false); // return s_area; } // --------------------------------------------------------------------------- CachedRendering::CachedRendering(QStyle::State state, const QSize &size, const QModelIndex &index, qreal devicePixelRatio) : state(state) , regular(QPixmap(size * devicePixelRatio)) , hover(QPixmap(size * devicePixelRatio)) , valid(true) , validityIndex(index) { regular.setDevicePixelRatio(devicePixelRatio); hover.setDevicePixelRatio(devicePixelRatio); regular.fill(Qt::transparent); hover.fill(Qt::transparent); if (index.model()) { connect(index.model(), &QAbstractItemModel::dataChanged, this, &CachedRendering::dataChanged); connect(index.model(), &QAbstractItemModel::modelReset, this, &CachedRendering::modelReset); } } void CachedRendering::dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) { if (validityIndex.row() >= topLeft.row() && validityIndex.column() >= topLeft.column() && validityIndex.row() <= bottomRight.row() && validityIndex.column() <= bottomRight.column()) { valid = false; } } void CachedRendering::modelReset() { valid = false; } // --------------------------------------------------------------------------- AnimationState::AnimationState(const QModelIndex &index) : index(index) , direction(QTimeLine::Forward) , animating(false) , jobAnimation(false) , progress(0.0) , m_fadeProgress(1.0) , m_jobAnimationAngle(0.0) , renderCache(nullptr) , fadeFromRenderCache(nullptr) { creationTime.start(); } AnimationState::~AnimationState() { delete renderCache; delete fadeFromRenderCache; } bool AnimationState::update() { const qreal runtime = (direction == QTimeLine::Forward ? 150 : 250); // milliseconds const qreal increment = 1000. / runtime / 1000.; const qreal delta = increment * time.restart(); if (direction == QTimeLine::Forward) { progress = qMin(qreal(1.0), progress + delta); animating = (progress < 1.0); } else { progress = qMax(qreal(0.0), progress - delta); animating = (progress > 0.0); } if (fadeFromRenderCache) { // Icon fading goes always forwards m_fadeProgress = qMin(qreal(1.0), m_fadeProgress + delta); animating |= (m_fadeProgress < 1.0); if (m_fadeProgress == 1) { setCachedRenderingFadeFrom(nullptr); } } if (jobAnimation) { m_jobAnimationAngle += 1.0; if (m_jobAnimationAngle == 360) { m_jobAnimationAngle = 0; } if (index.model()->data(index, KDirModel::HasJobRole).toBool()) { animating = true; // there is a job here still... return false; } else { animating = false; // there's no job here anymore, return true so we stop painting this. return true; } } else { return !animating; } } static constexpr double s_mPI2 = 1.57079632679489661923; qreal AnimationState::hoverProgress() const { return qRound(255.0 * std::sin(progress * s_mPI2)) / 255.0; } qreal AnimationState::fadeProgress() const { return qRound(255.0 * std::sin(m_fadeProgress * s_mPI2)) / 255.0; } qreal AnimationState::jobAnimationAngle() const { return m_jobAnimationAngle; } bool AnimationState::hasJobAnimation() const { return jobAnimation; } void AnimationState::setJobAnimation(bool value) { jobAnimation = value; } // --------------------------------------------------------------------------- static const int switchIconInterval = 1000; ///@todo Eventually configurable interval? DelegateAnimationHandler::DelegateAnimationHandler(QObject *parent) : QObject(parent) { iconSequenceTimer.setSingleShot(true); iconSequenceTimer.setInterval(switchIconInterval); connect(&iconSequenceTimer, &QTimer::timeout, this, &DelegateAnimationHandler::sequenceTimerTimeout); ; } DelegateAnimationHandler::~DelegateAnimationHandler() { timer.stop(); QMapIterator i(animationLists); while (i.hasNext()) { i.next(); qDeleteAll(*i.value()); delete i.value(); } animationLists.clear(); } void DelegateAnimationHandler::sequenceTimerTimeout() { QAbstractItemModel *model = const_cast(sequenceModelIndex.model()); QAbstractProxyModel *proxy = qobject_cast(model); QModelIndex index = sequenceModelIndex; if (proxy) { index = proxy->mapToSource(index); model = proxy->sourceModel(); } KDirModel *dirModel = dynamic_cast(model); if (dirModel) { // qDebug() << "requesting" << currentSequenceIndex; dirModel->requestSequenceIcon(index, currentSequenceIndex); iconSequenceTimer.start(); // Some upper-bound interval is needed, in case items are not generated } } void DelegateAnimationHandler::gotNewIcon(const QModelIndex &index) { Q_UNUSED(index); // qDebug() << currentSequenceIndex; if (sequenceModelIndex.isValid() && currentSequenceIndex) { iconSequenceTimer.start(); } // if(index ==sequenceModelIndex) //Leads to problems ++currentSequenceIndex; } void DelegateAnimationHandler::setSequenceIndex(int sequenceIndex) { // qDebug() << sequenceIndex; if (sequenceIndex > 0) { currentSequenceIndex = sequenceIndex; iconSequenceTimer.start(); } else { currentSequenceIndex = 0; sequenceTimerTimeout(); // Set the icon back to the standard one currentSequenceIndex = 0; // currentSequenceIndex was incremented, set it back to 0 iconSequenceTimer.stop(); } } void DelegateAnimationHandler::eventuallyStartIteration(const QModelIndex &index) { // if (KGlobalSettings::graphicEffectsLevel() & KGlobalSettings::SimpleAnimationEffects) { /// Think about it. if (sequenceModelIndex.isValid()) { setSequenceIndex(0); // Stop old iteration, and reset the icon for the old iteration } // Start sequence iteration sequenceModelIndex = index; setSequenceIndex(1); // } } AnimationState *DelegateAnimationHandler::animationState(const QStyleOption &option, const QModelIndex &index, const QAbstractItemView *view) { // We can't do animations reliably when an item is being dragged, since that // item will be drawn in two locations at the same time and hovered in one and // not the other. We can't tell them apart because they both have the same index. if (!view || static_cast(view)->draggingState()) { return nullptr; } AnimationState *state = findAnimationState(view, index); bool hover = option.state & QStyle::State_MouseOver; // If the cursor has entered an item if (!state && hover) { state = new AnimationState(index); addAnimationState(state, view); if (!fadeInAddTime.isValid() || (fadeInAddTime.isValid() && fadeInAddTime.elapsed() > 300)) { startAnimation(state); } else { state->animating = false; state->progress = 1.0; state->direction = QTimeLine::Forward; } fadeInAddTime.restart(); eventuallyStartIteration(index); } else if (state) { // If the cursor has exited an item if (!hover && (!state->animating || state->direction == QTimeLine::Forward)) { state->direction = QTimeLine::Backward; if (state->creationTime.elapsed() < 200) { state->progress = 0.0; } startAnimation(state); // Stop sequence iteration if (index == sequenceModelIndex) { setSequenceIndex(0); sequenceModelIndex = QPersistentModelIndex(); } } else if (hover && state->direction == QTimeLine::Backward) { // This is needed to handle the case where an item is dragged within // the view, and dropped in a different location. State_MouseOver will // initially not be set causing a "hover out" animation to start. // This reverses the direction as soon as we see the bit being set. state->direction = QTimeLine::Forward; if (!state->animating) { startAnimation(state); } eventuallyStartIteration(index); } } else if (!state && index.model()->data(index, KDirModel::HasJobRole).toBool()) { state = new AnimationState(index); addAnimationState(state, view); startAnimation(state); state->setJobAnimation(true); } return state; } AnimationState *DelegateAnimationHandler::findAnimationState(const QAbstractItemView *view, const QModelIndex &index) const { // Try to find a list of animation states for the view const AnimationList *list = animationLists.value(view); if (list) { auto it = std::find_if(list->cbegin(), list->cend(), [&index](AnimationState *state) { return state->index == index; }); if (it != list->cend()) { return *it; } } return nullptr; } void DelegateAnimationHandler::addAnimationState(AnimationState *state, const QAbstractItemView *view) { AnimationList *list = animationLists.value(view); // If this is the first time we've seen this view if (!list) { connect(view, &QObject::destroyed, this, &DelegateAnimationHandler::viewDeleted); list = new AnimationList; animationLists.insert(view, list); } list->append(state); } void DelegateAnimationHandler::restartAnimation(AnimationState *state) { startAnimation(state); } void DelegateAnimationHandler::startAnimation(AnimationState *state) { state->time.start(); state->animating = true; if (!timer.isActive()) { timer.start(1000 / 30, this); // 30 fps } } int DelegateAnimationHandler::runAnimations(AnimationList *list, const QAbstractItemView *view) { int activeAnimations = 0; QRegion region; QMutableListIterator i(*list); while (i.hasNext()) { AnimationState *state = i.next(); if (!state->animating) { continue; } // We need to make sure the index is still valid, since it could be removed // while the animation is running. if (state->index.isValid()) { bool finished = state->update(); region += view->visualRect(state->index); if (!finished) { activeAnimations++; continue; } } // If the direction is Forward, the state object needs to stick around // after the animation has finished, so we know that we've already done // a "hover in" for the index. if (state->direction == QTimeLine::Backward || !state->index.isValid()) { delete state; i.remove(); } } // Trigger a repaint of the animated indexes if (!region.isEmpty()) { const_cast(view)->viewport()->update(region); } return activeAnimations; } void DelegateAnimationHandler::viewDeleted(QObject *view) { AnimationList *list = animationLists.take(static_cast(view)); qDeleteAll(*list); delete list; } void DelegateAnimationHandler::timerEvent(QTimerEvent *) { int activeAnimations = 0; AnimationListsIterator i(animationLists); while (i.hasNext()) { i.next(); AnimationList *list = i.value(); const QAbstractItemView *view = i.key(); activeAnimations += runAnimations(list, view); } if (activeAnimations == 0 && timer.isActive()) { timer.stop(); } } } #include "delegateanimationhandler.moc"