File indexing completed on 2024-12-22 04:14:45

0001 /*
0002  *  SPDX-FileCopyrightText: 2016 Jouni Pentikäinen <joupent@gmail.com>
0003  *
0004  *  SPDX-License-Identifier: GPL-2.0-or-later
0005  */
0006 
0007 #include "KisTimeBasedItemModel.h"
0008 
0009 #include <QPointer>
0010 #include <kis_config.h>
0011 
0012 #include "kis_animation_frame_cache.h"
0013 #include "KisCanvasAnimationState.h"
0014 #include "animation/KisFrameDisplayProxy.h"
0015 #include "kis_signal_compressor_with_param.h"
0016 #include "kis_image.h"
0017 #include "kis_image_animation_interface.h"
0018 #include "kis_time_span.h"
0019 #include "KisAnimUtils.h"
0020 #include "kis_keyframe_channel.h"
0021 #include "kis_raster_keyframe_channel.h"
0022 #include "kis_processing_applicator.h"
0023 #include "KisImageBarrierLock.h"
0024 #include "commands_new/kis_switch_current_time_command.h"
0025 #include "kis_command_utils.h"
0026 #include "KisPart.h"
0027 #include "KisPlaybackEngine.h"
0028 #include "kis_animation_cache_populator.h"
0029 
0030 struct KisTimeBasedItemModel::Private
0031 {
0032     Private()
0033         : animationPlayer(0)
0034         , document(nullptr)
0035         , numFramesOverride(0)
0036         , activeFrameIndex(0)
0037         , scrubInProgress(false)
0038         , scrubStartFrame(-1)
0039         , shouldReturnToPlay(false)
0040         , scrubHeaderMin(0)
0041         , scrubHeaderMax(0)
0042     {}
0043 
0044     KisImageWSP image;
0045     KisAnimationFrameCacheWSP framesCache;
0046     QPointer<KisCanvasAnimationState> animationPlayer;
0047     KisDocument* document;
0048 
0049     QVector<bool> cachedFrames;
0050 
0051     int numFramesOverride;
0052     int activeFrameIndex;
0053 
0054     bool scrubInProgress;
0055     int scrubStartFrame;
0056     bool shouldReturnToPlay;
0057 
0058     QScopedPointer<KisSignalCompressorWithParam<int>> scrubHeaderUpdateCompressor;
0059     int scrubHeaderMin;
0060     int scrubHeaderMax;
0061 
0062     int baseNumFrames() const {
0063 
0064         auto imageSP = image.toStrongRef();
0065         if (!imageSP) return 0;
0066 
0067         KisImageAnimationInterface *i = imageSP->animationInterface();
0068         if (!i) return 1;
0069 
0070         return i->totalLength();
0071     }
0072 
0073     int effectiveNumFrames() const {
0074         if (image.isNull()) return 0;
0075 
0076         return qMax(baseNumFrames(), numFramesOverride);
0077     }
0078 
0079     int framesPerSecond() {
0080         return image->animationInterface()->framerate();
0081     }
0082 
0083     bool withinClipRange(const int time) {
0084         if (!image) {
0085             return true;
0086         }
0087 
0088         KisTimeSpan clipRange = image->animationInterface()->documentPlaybackRange();
0089         return clipRange.contains(time);
0090     }
0091 };
0092 
0093 KisTimeBasedItemModel::KisTimeBasedItemModel(QObject *parent)
0094     : QAbstractTableModel(parent)
0095     , m_d(new Private())
0096 {
0097     KisConfig cfg(true);
0098 
0099     using namespace std::placeholders;
0100 
0101     std::function<void (int)> scrubHorizHeaderUpdateCallback(
0102         std::bind(&KisTimeBasedItemModel::scrubHorizontalHeaderUpdate, this, _1));
0103 
0104     m_d->scrubHeaderUpdateCompressor.reset(
0105         new KisSignalCompressorWithParam<int>(100, scrubHorizHeaderUpdateCallback, KisSignalCompressor::FIRST_ACTIVE));
0106 }
0107 
0108 KisTimeBasedItemModel::~KisTimeBasedItemModel()
0109 {}
0110 
0111 void KisTimeBasedItemModel::setImage(KisImageWSP p_image)
0112 {
0113     if (m_d->image == p_image ) {
0114         return;
0115     }
0116 
0117     beginResetModel();
0118 
0119     if (m_d->image) {
0120         //Disconnect old image..
0121         const KisImageAnimationInterface *ai = m_d->image->animationInterface();
0122         ai->disconnect(this);
0123     }
0124 
0125     m_d->image = p_image;
0126     m_d->numFramesOverride = m_d->effectiveNumFrames();
0127 
0128     if (m_d->image) {
0129         KisImageAnimationInterface *ai = m_d->image->animationInterface();
0130 
0131         connect(ai, SIGNAL(sigFramerateChanged()), this, SLOT(slotFramerateChanged()));
0132         connect(ai, SIGNAL(sigUiTimeChanged(int)), this, SLOT(slotCurrentTimeChanged(int)));
0133         connect(ai, SIGNAL(sigPlaybackRangeChanged()), this, SLOT(slotPlaybackRangeChanged()));
0134     }
0135 
0136     endResetModel();
0137 }
0138 
0139 void KisTimeBasedItemModel::setFrameCache(KisAnimationFrameCacheSP cache)
0140 {
0141     if (KisAnimationFrameCacheSP(m_d->framesCache) == cache) return;
0142 
0143     if (m_d->framesCache) {
0144         m_d->framesCache->disconnect(this);
0145     }
0146 
0147     m_d->framesCache = cache;
0148 
0149     if (m_d->framesCache) {
0150         connect(m_d->framesCache, SIGNAL(changed()), SLOT(slotCacheChanged()));
0151     }
0152 }
0153 
0154 bool KisTimeBasedItemModel::isFrameCached(const int frame)
0155 {
0156     return m_d->framesCache && m_d->framesCache->frameStatus(frame) == KisAnimationFrameCache::Cached;
0157 }
0158 
0159 void KisTimeBasedItemModel::setAnimationPlayer(KisCanvasAnimationState *player)
0160 {
0161     if (m_d->animationPlayer == player) return;
0162 
0163     if (m_d->animationPlayer) {
0164         m_d->animationPlayer->disconnect(this);
0165     }
0166 
0167     m_d->animationPlayer = player;
0168 
0169     if (m_d->animationPlayer) {
0170         connect(m_d->animationPlayer, SIGNAL(sigPlaybackStateChanged(PlaybackState)), SLOT(slotPlaybackStateChanged(PlaybackState)));
0171         connect(m_d->animationPlayer, SIGNAL(sigFrameChanged()), SLOT(slotPlaybackFrameChanged()));
0172 
0173         const int frame = player ? player->displayProxy()->activeFrame() : m_d->image->animationInterface()->currentUITime();
0174         setHeaderData(frame, Qt::Horizontal, true, ActiveFrameRole);
0175 
0176         // only prioritize the cache, no seek operation to prevent audio from playing
0177         setHeaderData(frame, Qt::Horizontal, QVariant(int(SEEK_NONE)), ScrubToRole);
0178     }
0179 }
0180 
0181 void KisTimeBasedItemModel::setDocument(KisDocument *document)
0182 {
0183     if (m_d->document == document) return;
0184 
0185     m_d->document = document;
0186 }
0187 
0188 KisDocument* KisTimeBasedItemModel::document() const
0189 {
0190     return m_d->document;
0191 }
0192 
0193 void KisTimeBasedItemModel::setLastVisibleFrame(int time)
0194 {
0195     const int growThreshold = m_d->effectiveNumFrames() - 1;
0196     const int growValue = time + 8;
0197 
0198     const int shrinkThreshold = m_d->effectiveNumFrames() - 3;
0199     const int shrinkValue = qMax(m_d->baseNumFrames(), qMin(growValue, shrinkThreshold));
0200     const bool canShrink = m_d->baseNumFrames() < m_d->effectiveNumFrames();
0201 
0202     if (time >= growThreshold) {
0203         beginInsertColumns(QModelIndex(), m_d->effectiveNumFrames(), growValue - 1);
0204         m_d->numFramesOverride = growValue;
0205         endInsertColumns();
0206     } else if (time < shrinkThreshold && canShrink) {
0207         beginRemoveColumns(QModelIndex(), shrinkValue, m_d->effectiveNumFrames() - 1);
0208         m_d->numFramesOverride = shrinkValue;
0209         endRemoveColumns();
0210     }
0211 }
0212 
0213 int KisTimeBasedItemModel::columnCount(const QModelIndex &parent) const
0214 {
0215     Q_UNUSED(parent);
0216     return m_d->numFramesOverride;
0217 }
0218 
0219 QVariant KisTimeBasedItemModel::data(const QModelIndex &index, int role) const
0220 {
0221     switch (role) {
0222         case ActiveFrameRole: {
0223             return index.column() == m_d->activeFrameIndex;
0224         }
0225         case CloneOfActiveFrame: {
0226             return cloneOfActiveFrame(index);
0227         }
0228         case CloneCount: {
0229             return cloneCount(index);
0230         }
0231         case WithinClipRange:
0232             return m_d->withinClipRange(index.column());
0233         }
0234 
0235     return QVariant();
0236 }
0237 
0238 bool KisTimeBasedItemModel::setData(const QModelIndex &index, const QVariant &value, int role)
0239 {
0240     if (!index.isValid()) return false;
0241 
0242     switch (role) {
0243         case ActiveFrameRole:
0244             setHeaderData(index.column(), Qt::Horizontal, value, role);
0245             break;
0246         case ScrubToRole:
0247             setHeaderData(index.column(), Qt::Horizontal, value, role);
0248             break;
0249     }
0250 
0251     return false;
0252 }
0253 
0254 QVariant KisTimeBasedItemModel::headerData(int section, Qt::Orientation orientation, int role) const
0255 {
0256     if (orientation == Qt::Horizontal) {
0257         switch (role) {
0258         case ActiveFrameRole:
0259             return section == m_d->activeFrameIndex;
0260         case FrameCachedRole:
0261             return m_d->cachedFrames.size() > section ? m_d->cachedFrames[section] : false;
0262         case FramesPerSecondRole:
0263             return m_d->framesPerSecond();
0264         case WithinClipRange:
0265             return m_d->withinClipRange(section);
0266         }
0267     }
0268 
0269     return QVariant();
0270 }
0271 
0272 bool KisTimeBasedItemModel::setHeaderData(int section, Qt::Orientation orientation, const QVariant &value, int role)
0273 {
0274     auto prioritizeCache = [this](int frame){
0275         if (m_d->image) {
0276             if(!isFrameCached(frame)) {
0277                 KisPart::instance()->prioritizeFrameForCache(m_d->image, frame);
0278             }
0279         }
0280     };
0281 
0282 
0283     if (orientation == Qt::Horizontal) {
0284         switch (role) {
0285         case ActiveFrameRole:
0286             if (value.toBool() &&
0287                 section != m_d->activeFrameIndex) {
0288 
0289                 int prevFrame = m_d->activeFrameIndex;
0290                 m_d->activeFrameIndex = section;
0291 
0292                 /**
0293                  * Optimization Hack Alert:
0294                  *
0295                  * ideally, we should emit all four signals, but... The
0296                  * point is this code is used in a tight loop during
0297                  * playback, so it should run as fast as possible. To tell
0298                  * the story short, commenting out these three lines makes
0299                  * playback run 15% faster ;)
0300                  */
0301 
0302                 if (m_d->scrubInProgress) {
0303                     emit dataChanged(this->index(0, m_d->activeFrameIndex), this->index(rowCount() - 1, m_d->activeFrameIndex));
0304 
0305                     /*
0306                      * In order to try to correct rendering issues while preserving performance, we will
0307                      * defer updates just long enough that visual artifacts aren't majorly noticeable.
0308                      * By using a signal compressor, we're going to update the range of columns between
0309                      * min / max. That min max is reset every time the update occurs. This should fix
0310                      * rendering issues to a configurable framerate.
0311                      */
0312                     m_d->scrubHeaderMin = qMin(m_d->activeFrameIndex, m_d->scrubHeaderMin);
0313                     m_d->scrubHeaderMax = qMax(m_d->activeFrameIndex, m_d->scrubHeaderMax);
0314                     m_d->scrubHeaderUpdateCompressor->start(m_d->activeFrameIndex);
0315 
0316                     // vvvvvvvvvvvvvvvvvvvvv Read above comment.. This fixes all timeline rendering issues, but at what cost???
0317                     //emit dataChanged(this->index(0, prevFrame), this->index(rowCount() - 1, prevFrame));
0318                     //emit headerDataChanged (Qt::Horizontal, m_d->activeFrameIndex, m_d->activeFrameIndex);
0319                     //emit headerDataChanged (Qt::Horizontal, prevFrame, prevFrame);
0320                 } else {
0321                     emit dataChanged(this->index(0, prevFrame), this->index(rowCount() - 1, prevFrame));
0322                     emit dataChanged(this->index(0, m_d->activeFrameIndex), this->index(rowCount() - 1, m_d->activeFrameIndex));
0323                     emit headerDataChanged (Qt::Horizontal, prevFrame, prevFrame);
0324                     emit headerDataChanged (Qt::Horizontal, m_d->activeFrameIndex, m_d->activeFrameIndex);
0325                 }
0326             }
0327             break;
0328         case ScrubToRole:
0329             SeekOptionFlags seekFlags = SeekOptionFlags(value.toInt());
0330             prioritizeCache(m_d->activeFrameIndex);
0331             if (!m_d->image->hasUpdatesRunning()) {
0332                 KisPart::instance()->playbackEngine()->seek(m_d->activeFrameIndex, seekFlags);
0333             }
0334             break;
0335         }
0336     }
0337 
0338     return false;
0339 }
0340 
0341 void KisTimeBasedItemModel::scrubHorizontalHeaderUpdate(int activeColumn)
0342 {
0343     emit headerDataChanged (Qt::Horizontal, m_d->scrubHeaderMin, m_d->scrubHeaderMax);
0344     m_d->scrubHeaderMin = activeColumn;
0345     m_d->scrubHeaderMax = activeColumn;
0346 }
0347 
0348 bool KisTimeBasedItemModel::removeFrames(const QModelIndexList &indexes)
0349 {
0350     KisAnimUtils::FrameItemList frameItems;
0351 
0352     {
0353         KisImageBarrierLock locker(m_d->image);
0354 
0355         Q_FOREACH (const QModelIndex &index, indexes) {
0356             int time = index.column();
0357             Q_FOREACH(KisKeyframeChannel *channel, channelsAt(index)) {
0358                 if (channel->keyframeAt(time)) {
0359                     frameItems << KisAnimUtils::FrameItem(channel->node(), channel->id(), index.column());
0360                 }
0361             }
0362         }
0363     }
0364 
0365     if (frameItems.isEmpty()) return false;
0366 
0367     KisAnimUtils::removeKeyframes(m_d->image, frameItems);
0368 
0369     return true;
0370 }
0371 
0372 KUndo2Command* KisTimeBasedItemModel::createOffsetFramesCommand(QModelIndexList srcIndexes,
0373                                                                 const QPoint &offset,
0374                                                                 bool copyFrames,
0375                                                                 bool moveEmptyFrames,
0376                                                                 KUndo2Command *parentCommand)
0377 {
0378     if (srcIndexes.isEmpty()) return 0;
0379     if (offset.isNull()) return 0;
0380 
0381     KisAnimUtils::sortPointsForSafeMove(&srcIndexes, offset);
0382 
0383     KisAnimUtils::FrameItemList srcFrameItems;
0384     KisAnimUtils::FrameItemList dstFrameItems;
0385 
0386     Q_FOREACH (const QModelIndex &srcIndex, srcIndexes) {
0387         QModelIndex dstIndex = index(
0388                 srcIndex.row() + offset.y(),
0389                 srcIndex.column() + offset.x());
0390 
0391         KisNodeSP srcNode = nodeAt(srcIndex);
0392         KisNodeSP dstNode = nodeAt(dstIndex);
0393         if (!srcNode || !dstNode) return 0;
0394 
0395         Q_FOREACH(KisKeyframeChannel *channel, channelsAt(srcIndex)) {
0396             if (moveEmptyFrames || channel->keyframeAt(srcIndex.column())) {
0397                 srcFrameItems << KisAnimUtils::FrameItem(srcNode, channel->id(), srcIndex.column());
0398                 dstFrameItems << KisAnimUtils::FrameItem(dstNode, channel->id(), dstIndex.column());
0399             }
0400         }
0401     }
0402 
0403     KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(srcFrameItems.size() == dstFrameItems.size(), 0);
0404     if (srcFrameItems.isEmpty()) return 0;
0405 
0406     return
0407         KisAnimUtils::createMoveKeyframesCommand(srcFrameItems,
0408                                                       dstFrameItems,
0409                                                       copyFrames,
0410                                                       moveEmptyFrames,
0411                                                       parentCommand);
0412 }
0413 
0414 bool KisTimeBasedItemModel::removeFramesAndOffset(QModelIndexList indicesToRemove)
0415 {
0416     if (indicesToRemove.isEmpty()) return true;
0417 
0418     std::sort(indicesToRemove.begin(), indicesToRemove.end(),
0419               [] (const QModelIndex &lhs, const QModelIndex &rhs) {
0420                   return lhs.column() > rhs.column();
0421               });
0422 
0423     const int minColumn = indicesToRemove.last().column();
0424 
0425     KUndo2Command *parentCommand = new KUndo2Command(kundo2_i18np("Remove frame and shift", "Remove %1 frames and shift", indicesToRemove.size()));
0426 
0427     {
0428         KisImageBarrierLock locker(m_d->image);
0429 
0430         Q_FOREACH (const QModelIndex &index, indicesToRemove) {
0431             QModelIndexList indicesToOffset;
0432             for (int column = index.column() + 1; column < columnCount(); column++) {
0433                 indicesToOffset << this->index(index.row(), column);
0434             }
0435             createOffsetFramesCommand(indicesToOffset, QPoint(-1, 0), false, true, parentCommand);
0436         }
0437 
0438         const int oldTime = m_d->image->animationInterface()->currentUITime();
0439         const int newTime = minColumn;
0440 
0441         new KisSwitchCurrentTimeCommand(m_d->image->animationInterface(),
0442                                         oldTime,
0443                                         newTime,
0444                                         parentCommand);
0445     }
0446 
0447     KisProcessingApplicator::runSingleCommandStroke(m_d->image, parentCommand,
0448                                                     KisStrokeJobData::BARRIER,
0449                                                     KisStrokeJobData::EXCLUSIVE);
0450     return true;
0451 }
0452 
0453 bool KisTimeBasedItemModel::mirrorFrames(QModelIndexList indexes)
0454 {
0455     QScopedPointer<KUndo2Command> parentCommand(new KUndo2Command(kundo2_i18n("Mirror Frames")));
0456 
0457     {
0458         KisImageBarrierLock locker(m_d->image);
0459 
0460         QMap<int, QModelIndexList> rowsList;
0461 
0462         Q_FOREACH (const QModelIndex &index, indexes) {
0463             rowsList[index.row()].append(index);
0464         }
0465 
0466 
0467         Q_FOREACH (int row, rowsList.keys()) {
0468             QModelIndexList &list = rowsList[row];
0469 
0470             KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!list.isEmpty(), false);
0471 
0472             std::sort(list.begin(), list.end(),
0473                 [] (const QModelIndex &lhs, const QModelIndex &rhs) {
0474                     return lhs.column() < rhs.column();
0475                 });
0476 
0477             auto srcIt = list.begin();
0478             auto dstIt = list.end();
0479 
0480             KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(srcIt != dstIt, false);
0481             --dstIt;
0482 
0483             QList<KisKeyframeChannel*> channels = channelsAt(*srcIt).values();
0484 
0485             while (srcIt < dstIt) {
0486                 Q_FOREACH (KisKeyframeChannel *channel, channels) {
0487                     if (channel->keyframeAt(srcIt->column()) && channel->keyframeAt(dstIt->column())) {
0488 
0489                         channel->swapKeyframes(srcIt->column(),
0490                                                dstIt->column(),
0491                                                parentCommand.data());
0492                     }
0493                     else if (channel->keyframeAt(srcIt->column())) {
0494 
0495                         channel->insertKeyframe(dstIt->column(),
0496                                                 channel->keyframeAt(srcIt->column()),
0497                                                 parentCommand.data());
0498 
0499                         channel->removeKeyframe(srcIt->column(),
0500                                                 parentCommand.data());
0501                     }
0502                     else if (channel->keyframeAt(dstIt->column())) {
0503 
0504                         channel->insertKeyframe(srcIt->column(),
0505                                                 channel->keyframeAt(dstIt->column()),
0506                                                 parentCommand.data());
0507 
0508                         channel->removeKeyframe(dstIt->column(),
0509                                                 parentCommand.data());
0510                     }
0511                 }
0512 
0513                 srcIt++;
0514                 dstIt--;
0515             }
0516         }
0517     }
0518 
0519     KisProcessingApplicator::runSingleCommandStroke(m_d->image,
0520                                                     parentCommand.take(),
0521                                                     KisStrokeJobData::BARRIER,
0522                                                     KisStrokeJobData::EXCLUSIVE);
0523     return true;
0524 }
0525 
0526 void KisTimeBasedItemModel::setScrubState(bool p_state)
0527 {
0528     if (!m_d->animationPlayer) {
0529         return;
0530     }
0531 
0532     if (m_d->scrubInProgress != p_state) {
0533         m_d->scrubInProgress = p_state;
0534 
0535         if (m_d->scrubInProgress == true) {
0536             m_d->scrubStartFrame = m_d->activeFrameIndex;
0537 
0538             if (m_d->animationPlayer->playbackState() == PLAYING) {
0539                 m_d->shouldReturnToPlay = true;
0540                 m_d->animationPlayer->setPlaybackState(PAUSED);
0541             }
0542 
0543         } else {
0544             if (m_d->shouldReturnToPlay) {
0545                 m_d->animationPlayer->setPlaybackState(PLAYING);
0546             }
0547 
0548             m_d->scrubStartFrame = -1;
0549             m_d->shouldReturnToPlay = false;
0550         }
0551     }
0552 }
0553 
0554 bool KisTimeBasedItemModel::isScrubbing()
0555 {
0556     return m_d->scrubInProgress;
0557 }
0558 
0559 
0560 void KisTimeBasedItemModel::slotCurrentTimeChanged(int time)
0561 {
0562     if (time != m_d->activeFrameIndex) {
0563         setHeaderData(time, Qt::Horizontal, true, ActiveFrameRole);
0564     }
0565 }
0566 
0567 void KisTimeBasedItemModel::slotFramerateChanged()
0568 {
0569     emit headerDataChanged(Qt::Horizontal, 0, columnCount() - 1);
0570 }
0571 
0572 void KisTimeBasedItemModel::slotPlaybackRangeChanged()
0573 {
0574     if (m_d->image && m_d->image->animationInterface() ) {
0575         const KisImageAnimationInterface* const interface = m_d->image->animationInterface();
0576         const int lastFrame = interface->activePlaybackRange().end();
0577         if (lastFrame > m_d->numFramesOverride) {
0578             beginInsertColumns(QModelIndex(), m_d->numFramesOverride, interface->activePlaybackRange().end());
0579             m_d->numFramesOverride = interface->activePlaybackRange().end();
0580             endInsertColumns();
0581         }
0582 
0583         dataChanged(index(0,0), index(rowCount(), columnCount()));
0584     }
0585 }
0586 
0587 void KisTimeBasedItemModel::slotCacheChanged()
0588 {
0589     const int numFrames = columnCount();
0590     m_d->cachedFrames.resize(numFrames);
0591 
0592     for (int i = 0; i < numFrames; i++) {
0593         m_d->cachedFrames[i] =
0594             m_d->framesCache->frameStatus(i) == KisAnimationFrameCache::Cached;
0595     }
0596 
0597     emit headerDataChanged(Qt::Horizontal, 0, numFrames);
0598 }
0599 
0600 
0601 void KisTimeBasedItemModel::slotPlaybackFrameChanged()
0602 {
0603     if (m_d->animationPlayer->playbackState() != PlaybackState::PLAYING) return;
0604     setHeaderData(m_d->animationPlayer->displayProxy()->activeFrame(), Qt::Horizontal, true, ActiveFrameRole);
0605 }
0606 
0607 void KisTimeBasedItemModel::slotPlaybackStateChanged(PlaybackState p_state)
0608 {
0609     if (p_state == PlaybackState::STOPPED) {
0610         setHeaderData(m_d->image->animationInterface()->currentUITime(), Qt::Horizontal, true, ActiveFrameRole);
0611     }
0612 }
0613 
0614 void KisTimeBasedItemModel::setPlaybackRange(const KisTimeSpan &range)
0615 {
0616     if (m_d->image.isNull()) return;
0617 
0618     KisImageAnimationInterface *i = m_d->image->animationInterface();
0619     i->setActivePlaybackRange(range);
0620 }
0621 
0622 bool KisTimeBasedItemModel::isPlaybackActive() const
0623 {
0624     return m_d->animationPlayer && m_d->animationPlayer->playbackState() == PlaybackState::PLAYING;
0625 }
0626 
0627 bool KisTimeBasedItemModel::isPlaybackPaused() const
0628 {
0629     return m_d->animationPlayer && m_d->animationPlayer->playbackState() == PlaybackState::PAUSED;
0630 }
0631 
0632 void KisTimeBasedItemModel::stopPlayback() const {
0633     KisPart::instance()->playbackEngine()->stop();
0634 }
0635 
0636 int KisTimeBasedItemModel::currentTime() const
0637 {
0638     return m_d->image->animationInterface()->currentUITime();
0639 }
0640 
0641 bool KisTimeBasedItemModel::cloneOfActiveFrame(const QModelIndex &index) const {
0642     KisRasterKeyframeChannel *rasterChan = dynamic_cast<KisRasterKeyframeChannel*>(channelByID(index, KisKeyframeChannel::Raster.id()));
0643     if (!rasterChan) return false;
0644 
0645     const int activeKeyframeTime = rasterChan->activeKeyframeTime(m_d->activeFrameIndex);
0646     return rasterChan->areClones(activeKeyframeTime, index.column());
0647 }
0648 
0649 int KisTimeBasedItemModel::cloneCount(const QModelIndex &index) const {
0650     KisRasterKeyframeChannel *rasterChan = dynamic_cast<KisRasterKeyframeChannel*>(channelByID(index, KisKeyframeChannel::Raster.id()));
0651 
0652     if (!rasterChan) {
0653         return 0;
0654     }
0655     return rasterChan->clonesOf(index.column()).count();
0656 }
0657 
0658 KisImageWSP KisTimeBasedItemModel::image() const
0659 {
0660     return m_d->image;
0661 }