/* * 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 #include #include "animationdriver.h" namespace Plasma { static const int MIN_TICK_RATE_INT = 10; static const qreal MIN_TICK_RATE = 10; 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 frameInterval; int interval; int currentInterval; int id; QObject *receiver; char *slot; }; class AnimatorPrivate { public: AnimatorPrivate() : driver(0), animId(0), timerId(0) { } ~AnimatorPrivate() { cleanupStates(); qDeleteAll(animatedItems); qDeleteAll(animatedElements); qDeleteAll(movingItems); QMutableMapIterator it(customAnims); while (it.hasNext()) { it.next(); delete[] it.value()->slot; delete it.value(); it.remove(); } // Animator is a QObject // and we don't own the items } qreal calculateProgress(int time, int duration, Animator::CurveShape curve) { if (!(KGlobalSettings::graphicEffectsLevel() & KGlobalSettings::SimpleAnimationEffects)) { return qreal(1.0); } timeline.setCurveShape(static_cast(curve)); timeline.setDuration(duration); qreal progress = timeline.valueForTime(time); 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 cleanupStates(); void animatedItemDestroyed(QObject*); void movingItemDestroyed(QObject*); void animatedElementDestroyed(QObject*); void customAnimReceiverDestroyed(QObject*); AnimationDriver *driver; int animId; int timerId; QTime time; QTimeLine timeline; // active items QMap animatedItems; QMap movingItems; QMap animatedElements; QMap customAnims; // items to cull QList animatedItemsToDelete; QList movingItemsToDelete; QList animatedElementsToDelete; QList customAnimsToDelete; }; 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"; if (timerId) { animatedItemsToDelete.append(it.value()); } else { delete it.value(); } it.remove(); } } } void AnimatorPrivate::movingItemDestroyed(QObject *o) { QMutableMapIterator it(movingItems); while (it.hasNext()) { it.next(); if (it.value()->qobj == o) { if (timerId) { movingItemsToDelete.append(it.value()); } else { delete it.value(); } it.remove(); } } } void AnimatorPrivate::animatedElementDestroyed(QObject *o) { QMutableMapIterator it(animatedElements); while (it.hasNext()) { it.next(); if (it.value()->qobj == o) { if (timerId) { animatedElementsToDelete.append(it.value()); } else { delete it.value(); } it.remove(); } } } void AnimatorPrivate::customAnimReceiverDestroyed(QObject *o) { QMutableMapIterator it(customAnims); while (it.hasNext()) { if (it.next().value()->receiver == o) { if (timerId) { customAnimsToDelete.append(it.value()); } else { 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()) { if (d->timerId) { d->animatedItemsToDelete.append(it.value()); } else { 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; } int duration = d->driver->animationDuration(animation); AnimationState *state = new AnimationState; state->id = ++d->animId; state->item = item; state->animation = animation; state->curve = d->driver->animationCurve(animation); state->frames = qMax(1.0, frames * (duration / 1000.0)); state->currentFrame = 0; state->interval = d->driver->animationDuration(animation) / qreal(state->frames); state->interval = qMax(MIN_TICK_RATE_INT, state->interval - (state->interval % MIN_TICK_RATE_INT)); 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()) { if (d->timerId) { d->movingItemsToDelete.append(it.value()); } else { 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 = qMax(1.0, frames * (duration / 1000.0)); state->currentFrame = 0; state->interval = duration / qreal(state->frames); state->interval = qMax(MIN_TICK_RATE_INT, state->interval - (state->interval % MIN_TICK_RATE_INT)); // state->interval = (state->interval / MIN_TICK_RATE) * MIN_TICK_RATE; // kDebug() << "interval of" << state->interval << state->frames << duration << frames; 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->frameInterval = qMax(qreal(1.0), duration / qreal(state->frames)); state->interval = qMax(MIN_TICK_RATE_INT, state->frameInterval - (state->frameInterval % MIN_TICK_RATE_INT)); 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()) { if (d->timerId) { d->customAnimsToDelete.append(it.value()); } else { 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) { if (d->timerId) { d->animatedItemsToDelete.append(it.value()); } else { 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) { if (d->timerId) { d->movingItemsToDelete.append(it.value()); } else { delete it.value(); } it.remove(); return; } } } int Animator::animateElement(QGraphicsItem *item, Animation animation) { //kDebug() << "startElementAnimation(AnimId " << animation << ")"; int frames = d->driver->elementAnimationFps(animation); int duration = d->driver->animationDuration(animation); ElementAnimationState *state = new ElementAnimationState; state->item = item; state->curve = d->driver->elementAnimationCurve(animation); state->animation = animation; state->frames = qMax(1.0, frames * (duration / 1000.0)); state->currentFrame = 0; state->interval = duration / qreal(state->frames); state->interval = qMax(MIN_TICK_RATE_INT, state->interval - (state->interval % MIN_TICK_RATE_INT)); 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()) { if (d->timerId) { d->animatedElementsToDelete.append(it.value()); } else { 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() << "No entry found for id " << id; return; } it.value()->pixmap = pixmap; } QPixmap Animator::currentPixmap(int id) { QMap::const_iterator it = d->animatedElements.constFind(id); if (it == d->animatedElements.constEnd()) { //kDebug() << "Animator::currentPixmap(" << id << ") found no entry for it!"; return QPixmap(); } ElementAnimationState *state = it.value(); qreal progress = d->calculateProgress(state->currentFrame * state->interval, state->frames * state->interval, state->curve); //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) { if (event->timerId() != d->timerId) { QObject::timerEvent(event); return; } 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->currentFrame * state->interval, state->frames * state->interval, state->curve); d->performAnimation(progress, state); state->currentInterval = state->interval; animationsRemain = true; } else { d->performAnimation(1, state); d->animatedItems.erase(d->animatedItems.find(state->item)); emit animationFinished(state->item, state->animation); d->animatedItemsToDelete.append(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"; qreal progress = d->calculateProgress(state->currentFrame * state->interval, state->frames * state->interval, state->curve); d->performMovement(progress, state); animationsRemain = true; } else { //kDebug() << "movement"; d->performMovement(1, state); d->movingItems.erase(d->movingItems.find(state->item)); emit movementFinished(state->item); d->movingItemsToDelete.append(state); } } else { state->currentInterval -= elapsed; animationsRemain = true; } } foreach (ElementAnimationState *state, d->animatedElements) { if (state->currentFrame == state->frames) { //kDebug() << "skipping" << state->id << "as it is 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; state->item->update(); if (state->currentFrame < state->frames) { state->currentInterval = state->interval; animationsRemain = true; } else { d->animatedElements.remove(state->id); emit elementAnimationFinished(state->id); d->animatedElementsToDelete.append(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->frameInterval) : 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 qreal progress = d->calculateProgress(state->currentFrame * state->interval, state->frames * state->interval, state->curve); if (!QMetaObject::invokeMethod(state->receiver, state->slot, Q_ARG(qreal, progress))) { //if fails try to add the animation id QMetaObject::invokeMethod(state->receiver, state->slot, Q_ARG(qreal, progress), 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); d->customAnimsToDelete.append(state); } } else { state->currentInterval -= elapsed; animationsRemain = true; } } if (!animationsRemain && d->timerId) { killTimer(d->timerId); d->timerId = 0; } d->cleanupStates(); } 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; KPluginLoader plugin(*offers.first()); if (Plasma::isPluginVersionCompatible(plugin.pluginVersion())) { driver = offers.first()->createInstance(q, QVariantList(), &error); } if (!driver) { kDebug() << "Could not load requested animator " << offers.first() << ". Error given: " << error; } } } if (!driver) { driver = new AnimationDriver(q); } } void AnimatorPrivate::cleanupStates() { /* kDebug() << animatedItemsToDelete.count() << animatedElementsToDelete.count() << movingItemsToDelete.count() << customAnimsToDelete.count(); */ qDeleteAll(animatedItemsToDelete); animatedItemsToDelete.clear(); qDeleteAll(animatedElementsToDelete); animatedElementsToDelete.clear(); qDeleteAll(movingItemsToDelete); movingItemsToDelete.clear(); QListIterator it(customAnimsToDelete); while (it.hasNext()) { CustomAnimationState *state = it.next(); delete[] state->slot; delete state; } customAnimsToDelete.clear(); } } // namespace Plasma #include