/* * Copyright 2007 Aaron Seigo * 2007 Alexis Ménard * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "animator.h" #include #include #include #include #include #include #include "animationdriver.h" namespace Plasma { static const int MIN_TICK_RATE_INT = 40; static const qreal MIN_TICK_RATE = 40; struct AnimationState { QGraphicsItem *item; QObject *qobj; Animator::Animation animation; Animator::CurveShape curve; int interval; int currentInterval; int frames; int currentFrame; int id; }; struct ElementAnimationState { QGraphicsItem *item; QObject *qobj; Animator::CurveShape curve; Animator::Animation animation; int interval; int currentInterval; int frames; int currentFrame; int id; QPixmap pixmap; }; struct MovementState { QGraphicsItem *item; QObject *qobj; Animator::CurveShape curve; Animator::Movement movement; int interval; int currentInterval; int frames; int currentFrame; QPoint start; QPoint destination; int id; }; struct CustomAnimationState { Animator::CurveShape curve; int frames; int currentFrame; int interval; int currentInterval; int id; QObject* receiver; char* slot; }; class AnimatorPrivate { public: AnimatorPrivate() : driver(0), animId(0), timerId(0) { } ~AnimatorPrivate() { qDeleteAll(animatedItems); qDeleteAll(animatedElements); qDeleteAll(movingItems); QMutableMapIterator it(customAnims); while (it.hasNext()) { delete it.value()->slot; delete it.value(); it.remove(); } // Animator is a QObject // and we don't own the items } qreal calculateProgress(int frames, int currentFrame) { if (!(KGlobalSettings::graphicEffectsLevel() & KGlobalSettings::SimpleAnimationEffects)) { return qreal(1.0); } qreal progress = frames; progress = currentFrame / progress; progress = qMin(qreal(1.0), qMax(qreal(0.0), progress)); return progress; } void performAnimation(qreal amount, const AnimationState* state) { switch (state->animation) { case Animator::AppearAnimation: driver->itemAppear(amount, state->item); break; case Animator::DisappearAnimation: driver->itemDisappear(amount, state->item); if (amount >= 1) { state->item->hide(); } break; case Animator::ActivateAnimation: driver->itemActivated(amount, state->item); break; } } void performMovement(qreal amount, const MovementState* state) { switch (state->movement) { case Animator::SlideInMovement: case Animator::FastSlideInMovement: //kDebug() << "performMovement, SlideInMovement"; driver->itemSlideIn(amount, state->item, state->start, state->destination); break; case Animator::SlideOutMovement: case Animator::FastSlideOutMovement: //kDebug() << "performMovement, SlideOutMovement"; driver->itemSlideOut(amount, state->item, state->start, state->destination); break; } } void init(Animator *q); void animatedItemDestroyed(QObject*); void movingItemDestroyed(QObject*); void animatedElementDestroyed(QObject*); void customAnimReceiverDestroyed(QObject*); AnimationDriver* driver; int animId; int timerId; QTime time; //TODO: eventually perhaps we should allow multiple animations simulataneously // which would imply changing this to a QMap > // and really making the code fun ;) QMap animatedItems; QMap movingItems; QMap animatedElements; QMap customAnims; }; class AnimatorSingleton { public: Animator self; }; K_GLOBAL_STATIC( AnimatorSingleton, privateSelf ) Animator* Animator::self() { return &privateSelf->self; } Animator::Animator(QObject * parent) : QObject(parent), d(new AnimatorPrivate) { d->init(this); } Animator::~Animator() { delete d; } void AnimatorPrivate::animatedItemDestroyed(QObject* o) { //kDebug() << "testing for" << (void*)o; QMutableMapIterator it(animatedItems); while (it.hasNext()) { it.next(); //kDebug() << "comparing against" << it.value()->qobj; if (it.value()->qobj == o) { kDebug() << "found deleted animated item"; delete it.value(); it.remove(); } } } void AnimatorPrivate::movingItemDestroyed(QObject* o) { QMutableMapIterator it(movingItems); while (it.hasNext()) { it.next(); if (it.value()->qobj == o) { delete it.value(); it.remove(); } } } void AnimatorPrivate::animatedElementDestroyed(QObject* o) { QMutableMapIterator it(animatedElements); while (it.hasNext()) { it.next(); if (it.value()->qobj == o) { delete it.value(); it.remove(); } } } void AnimatorPrivate::customAnimReceiverDestroyed(QObject* o) { QMutableMapIterator it(customAnims); while (it.hasNext()) { if (it.next().value()->receiver == o) { delete it.value()->slot; delete it.value(); it.remove(); } } } int Animator::animateItem(QGraphicsItem* item, Animation animation) { //kDebug(); // get rid of any existing animations on this item. //TODO: shoudl we allow multiple anims per item? QMap::iterator it = d->animatedItems.find(item); if (it != d->animatedItems.end()) { delete it.value(); d->animatedItems.erase(it); } int frames = d->driver->animationFps(animation); if (frames < 1) { // evidently this animator doesn't have an implementation // for this Animation return -1; } AnimationState* state = new AnimationState; state->id = ++d->animId; state->item = item; state->animation = animation; state->curve = d->driver->animationCurve(animation); //TODO: variance in times based on the value of animation state->frames = frames / 3; state->currentFrame = 0; state->interval = d->driver->animationDuration(animation) / state->frames; state->interval = (state->interval / MIN_TICK_RATE) * MIN_TICK_RATE; state->currentInterval = state->interval; state->qobj = dynamic_cast(item); if (state->qobj) { //kDebug() << "!!!!!!!!!!!!!!!!!!!!!!!!! got us an object!"; disconnect(state->qobj, SIGNAL(destroyed(QObject*)), this, SLOT(animatedItemDestroyed(QObject*))); connect(state->qobj, SIGNAL(destroyed(QObject*)), this, SLOT(animatedItemDestroyed(QObject*))); } d->animatedItems[item] = state; d->performAnimation(0, state); if (!d->timerId) { d->timerId = startTimer(MIN_TICK_RATE); d->time.restart(); } return state->id; } int Animator::moveItem(QGraphicsItem* item, Movement movement, const QPoint &destination) { //kDebug(); QMap::iterator it = d->movingItems.find(item); if (it != d->movingItems.end()) { delete it.value(); d->movingItems.erase(it); } int frames = d->driver->movementAnimationFps(movement); if (frames <= 1) { // evidently this animator doesn't have an implementation // for this Animation return -1; } MovementState* state = new MovementState; state->id = ++d->animId; state->destination = destination; state->start = item->pos().toPoint(); state->item = item; state->movement = movement; state->curve = d->driver->movementAnimationCurve(movement); //TODO: variance in times based on the value of animation int duration = d->driver->movementAnimationDuration(movement); state->frames = (duration / 1000.0) * frames; state->currentFrame = 0; state->interval = duration / state->frames; state->interval -= qMax(MIN_TICK_RATE_INT, state->interval % MIN_TICK_RATE_INT); // state->interval = (state->interval / MIN_TICK_RATE) * MIN_TICK_RATE; //kDebug() << "interval of" << state->interval; state->currentInterval = state->interval; state->qobj = dynamic_cast(item); if (state->qobj) { disconnect(state->qobj, SIGNAL(destroyed(QObject*)), this, SLOT(movingItemDestroyed(QObject*))); connect(state->qobj, SIGNAL(destroyed(QObject*)), this, SLOT(movingItemDestroyed(QObject*))); } d->movingItems[item] = state; d->performMovement(0, state); if (!d->timerId) { d->timerId = startTimer(MIN_TICK_RATE); d->time.restart(); } return state->id; } int Animator::customAnimation(int frames, int duration, Animator::CurveShape curve, QObject* receiver, const char* slot) { if (frames < 1 || duration < 1 || !receiver || !slot) { return -1; } CustomAnimationState *state = new CustomAnimationState; state->id = ++d->animId; state->frames = frames; state->currentFrame = 0; state->curve = curve; state->interval = duration / qreal(state->frames); state->interval = qMax( 1, state->interval ); state->interval = (state->interval / MIN_TICK_RATE) * MIN_TICK_RATE; state->currentInterval = state->interval; state->receiver = receiver; state->slot = qstrdup(slot); d->customAnims[state->id] = state; disconnect(receiver, SIGNAL(destroyed(QObject*)), this, SLOT(customAnimReceiverDestroyed(QObject*))); connect(receiver, SIGNAL(destroyed(QObject*)), this, SLOT(customAnimReceiverDestroyed(QObject*))); // try with only progress as argument if (!QMetaObject::invokeMethod(receiver, slot, Q_ARG(qreal, 0))) { //try to pass also the animation id QMetaObject::invokeMethod(receiver, slot, Q_ARG(qreal, 0), Q_ARG(int, state->id)); } if (!d->timerId) { d->timerId = startTimer(MIN_TICK_RATE); d->time.restart(); } return state->id; } void Animator::stopCustomAnimation(int id) { QMap::iterator it = d->customAnims.find(id); if (it != d->customAnims.end()) { delete [] it.value()->slot; delete it.value(); d->customAnims.erase(it); } //kDebug() << "stopCustomAnimation(AnimId " << id << ") done"; } void Animator::stopItemAnimation(int id) { QMutableMapIterator it(d->animatedItems); while (it.hasNext()) { it.next(); if (it.value()->id == id) { delete it.value(); it.remove(); return; } } } void Animator::stopItemMovement(int id) { QMutableMapIterator it(d->movingItems); while (it.hasNext()) { it.next(); if (it.value()->id == id) { delete it.value(); it.remove(); return; } } } int Animator::animateElement(QGraphicsItem *item, Animation animation) { //kDebug() << "startElementAnimation(AnimId " << animation << ")"; ElementAnimationState *state = new ElementAnimationState; state->item = item; state->curve = d->driver->elementAnimationCurve(animation); state->animation = animation; //TODO: variance in times based on the value of animation state->frames = d->driver->elementAnimationFps(animation) / 5; state->currentFrame = 0; state->interval = d->driver->elementAnimationDuration(animation) / state->frames; state->interval = (state->interval / MIN_TICK_RATE) * MIN_TICK_RATE; state->currentInterval = state->interval; state->id = ++d->animId; state->qobj = dynamic_cast(item); if (state->qobj) { disconnect(state->qobj, SIGNAL(destroyed(QObject*)), this, SLOT(animatedElementDestroyed(QObject*))); connect(state->qobj, SIGNAL(destroyed(QObject*)), this, SLOT(animatedElementDestroyed(QObject*))); } //kDebug() << "animateElement " << animation << ", interval: " << state->interval << ", frames: " << state->frames; bool needTimer = true; if (state->frames < 1) { state->frames = 1; state->currentFrame = 1; needTimer = false; } d->animatedElements[state->id] = state; //kDebug() << "startElementAnimation(AnimId " << animation << ") returning " << state->id; if (needTimer && !d->timerId) { // start a 20fps timer; //TODO: should be started at the maximum frame rate needed only? d->timerId = startTimer(MIN_TICK_RATE); d->time.restart(); } return state->id; } void Animator::stopElementAnimation(int id) { QMap::iterator it = d->animatedElements.find(id); if (it != d->animatedElements.end()) { delete it.value(); d->animatedElements.erase(it); } //kDebug() << "stopElementAnimation(AnimId " << id << ") done"; } void Animator::setInitialPixmap(int id, const QPixmap &pixmap) { QMap::iterator it = d->animatedElements.find(id); if (it == d->animatedElements.end()) { kDebug() << "Animator::setInitialPixmap(" << id << ") found no entry for it!"; return; } it.value()->pixmap = pixmap; } QPixmap Animator::currentPixmap(int id) { QMap::const_iterator it = d->animatedElements.find(id); if (it == d->animatedElements.constEnd()) { //kDebug() << "Animator::currentPixmap(" << id << ") found no entry for it!"; return QPixmap(); } ElementAnimationState* state = it.value(); qreal progress = state->frames; //kDebug() << "Animator::currentPixmap(" << id << " at " << progress; progress = state->currentFrame / progress; progress = qMin(qreal(1.0), qMax(qreal(0.0), progress)); //kDebug() << "Animator::currentPixmap(" << id << " at " << progress; switch (state->animation) { case AppearAnimation: return d->driver->elementAppear(progress, state->pixmap); break; case DisappearAnimation: return d->driver->elementDisappear(progress, state->pixmap); break; case ActivateAnimation: break; } return state->pixmap; } bool Animator::isAnimating() const { return (!d->animatedItems.isEmpty() || !d->movingItems.isEmpty() || !d->animatedElements.isEmpty() || !d->customAnims.isEmpty()); } void Animator::timerEvent(QTimerEvent *event) { Q_UNUSED(event) bool animationsRemain = false; int elapsed = MIN_TICK_RATE; if (d->time.elapsed() > elapsed) { elapsed = d->time.elapsed(); } d->time.restart(); //kDebug() << "timeEvent, elapsed time: " << elapsed; foreach (AnimationState* state, d->animatedItems) { if (state->currentInterval <= elapsed) { // we need to step forward! state->currentFrame += (KGlobalSettings::graphicEffectsLevel() & KGlobalSettings::SimpleAnimationEffects) ? qMax(1, elapsed / state->interval) : state->frames - state->currentFrame; if (state->currentFrame < state->frames) { qreal progress = d->calculateProgress(state->frames, state->currentFrame); d->performAnimation(progress, state); state->currentInterval = state->interval; //TODO: calculate a proper interval based on the curve state->interval *= 1 - progress; animationsRemain = true; } else { d->performAnimation(1, state); d->animatedItems.erase(d->animatedItems.find(state->item)); emit animationFinished(state->item, state->animation); delete state; } } else { state->currentInterval -= elapsed; animationsRemain = true; } } foreach (MovementState* state, d->movingItems) { if (state->currentInterval <= elapsed) { // we need to step forward! state->currentFrame += (KGlobalSettings::graphicEffectsLevel() & KGlobalSettings::SimpleAnimationEffects) ? qMax(1, elapsed / state->interval) : state->frames - state->currentFrame; if (state->currentFrame < state->frames) { //kDebug() << "movement"; d->performMovement(d->calculateProgress(state->frames, state->currentFrame), state); //TODO: calculate a proper interval based on the curve state->currentInterval = state->interval; animationsRemain = true; } else { //kDebug() << "movement"; d->performMovement(1, state); d->movingItems.erase(d->movingItems.find(state->item)); emit movementFinished(state->item); delete state; } } else { state->currentInterval -= elapsed; animationsRemain = true; } } foreach (ElementAnimationState* state, d->animatedElements) { if (state->currentFrame == state->frames) { //kDebug() << "skipping" << state->id << "as its already at frame" << state->currentFrame << "of" << state->frames; // since we keep element animations around until they are // removed, we will end up with finished animations in the queue; // just skip them //TODO: should we move them to a separate QMap? continue; } if (state->currentInterval <= elapsed) { // we need to step forward! /*kDebug() << "stepping forwards element anim " << state->id << " from " << state->currentFrame << " by " << qMax(1, elapsed / state->interval) << " to " << state->currentFrame + qMax(1, elapsed / state->interval) << endl;*/ state->currentFrame += (KGlobalSettings::graphicEffectsLevel() & KGlobalSettings::SimpleAnimationEffects) ? qMax(1, elapsed / state->interval) : state->frames - state->currentFrame; if (state->currentFrame < state->frames) { state->currentInterval = state->interval; //TODO: calculate a proper interval based on the curve state->interval *= 1 - d->calculateProgress(state->frames, state->currentFrame); animationsRemain = true; } else { d->animatedElements.remove(state->id); emit elementAnimationFinished(state->id); delete state; } } else { state->currentInterval -= elapsed; animationsRemain = true; } } foreach (CustomAnimationState *state, d->customAnims) { if (state->currentInterval <= elapsed) { // advance the frame state->currentFrame += (KGlobalSettings::graphicEffectsLevel() & KGlobalSettings::SimpleAnimationEffects) ? qMax(1, elapsed / state->interval) : state->frames - state->currentFrame; /*kDebug() << "custom anim for" << state->receiver << "to slot" << state->slot << "with interval of" << state->interval << "at frame" << state->currentFrame;*/ if (state->currentFrame < state->frames) { //kDebug () << "not the final frame"; //TODO: calculate a proper interval based on the curve state->currentInterval = state->interval; animationsRemain = true; // signal the object // try with only progress as argument if (!QMetaObject::invokeMethod(state->receiver, state->slot, Q_ARG(qreal, d->calculateProgress(state->frames, state->currentFrame)))) { //if fails try to add the animation id QMetaObject::invokeMethod(state->receiver, state->slot, Q_ARG(qreal, d->calculateProgress(state->frames, state->currentFrame)), Q_ARG(int, state->id)); } } else { if (!QMetaObject::invokeMethod(state->receiver, state->slot, Q_ARG(qreal, 1))) { QMetaObject::invokeMethod(state->receiver, state->slot, Q_ARG(qreal, 1), Q_ARG(int, state->id)); } d->customAnims.erase(d->customAnims.find(state->id)); emit customAnimationFinished(state->id); delete [] state->slot; delete state; } } else { state->currentInterval -= elapsed; animationsRemain = true; } } if (!animationsRemain && d->timerId) { killTimer(d->timerId); d->timerId = 0; } } void AnimatorPrivate::init(Animator *q) { //FIXME: usage between different applications? KConfig c("plasmarc"); KConfigGroup cg(&c, "Animator"); QString pluginName = cg.readEntry("driver", "default"); if (!pluginName.isEmpty()) { QString constraint = QString("[X-KDE-PluginInfo-Name] == '%1'").arg(pluginName); KService::List offers = KServiceTypeTrader::self()->query("Plasma/Animator", constraint); if (!offers.isEmpty()) { QString error; driver = offers.first()->createInstance(0, QVariantList(), &error); if (!driver) { kDebug() << "Could not load requested animator " << offers.first() << ". Error given: " << error; } } } if (!driver) { driver = new AnimationDriver(q); } } } // namespace Plasma #include