1 /*
2  *  Copyright (c) 2016 Jouni Pentikäinen <joupent@gmail.com>
3  *
4  *  This program is free software; you can redistribute it and/or modify
5  *  it under the terms of the GNU General Public License as published by
6  *  the Free Software Foundation; either version 2 of the License, or
7  *  (at your option) any later version.
8  *
9  *  This program is distributed in the hope that it will be useful,
10  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
11  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  *  GNU General Public License for more details.
13  *
14  *  You should have received a copy of the GNU General Public License
15  *  along with this program; if not, write to the Free Software
16  *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17  */
18 
19 #include "kis_time_based_item_model.h"
20 
21 #include <QPointer>
22 #include <kis_config.h>
23 
24 #include "kis_animation_frame_cache.h"
25 #include "kis_animation_player.h"
26 #include "kis_signal_compressor_with_param.h"
27 #include "kis_image.h"
28 #include "kis_image_animation_interface.h"
29 #include "kis_time_range.h"
30 #include "kis_animation_utils.h"
31 #include "kis_keyframe_channel.h"
32 #include "kis_processing_applicator.h"
33 #include "KisImageBarrierLockerWithFeedback.h"
34 #include "commands_new/kis_switch_current_time_command.h"
35 #include "kis_command_utils.h"
36 #include "KisPart.h"
37 #include "kis_animation_cache_populator.h"
38 
39 struct KisTimeBasedItemModel::Private
40 {
PrivateKisTimeBasedItemModel::Private41     Private()
42         : animationPlayer(0)
43         , numFramesOverride(0)
44         , activeFrameIndex(0)
45         , scrubInProgress(false)
46         , scrubStartFrame(-1)
47     {}
48 
49     KisImageWSP image;
50     KisAnimationFrameCacheWSP framesCache;
51     QPointer<KisAnimationPlayer> animationPlayer;
52 
53     QVector<bool> cachedFrames;
54 
55     int numFramesOverride;
56     int activeFrameIndex;
57 
58     bool scrubInProgress;
59     int scrubStartFrame;
60 
61     QScopedPointer<KisSignalCompressorWithParam<int> > scrubbingCompressor;
62 
baseNumFramesKisTimeBasedItemModel::Private63     int baseNumFrames() const {
64 
65         auto imageSP = image.toStrongRef();
66         if (!imageSP) return 0;
67 
68         KisImageAnimationInterface *i = imageSP->animationInterface();
69         if (!i) return 1;
70 
71         return i->totalLength();
72     }
73 
effectiveNumFramesKisTimeBasedItemModel::Private74     int effectiveNumFrames() const {
75         if (image.isNull()) return 0;
76 
77         return qMax(baseNumFrames(), numFramesOverride);
78     }
79 
framesPerSecondKisTimeBasedItemModel::Private80     int framesPerSecond() {
81         return image->animationInterface()->framerate();
82     }
83 };
84 
KisTimeBasedItemModel(QObject * parent)85 KisTimeBasedItemModel::KisTimeBasedItemModel(QObject *parent)
86     : QAbstractTableModel(parent)
87     , m_d(new Private())
88 {
89     KisConfig cfg(true);
90 
91     using namespace std::placeholders;
92     std::function<void (int)> callback(
93         std::bind(&KisTimeBasedItemModel::slotInternalScrubPreviewRequested, this, _1));
94 
95     m_d->scrubbingCompressor.reset(
96         new KisSignalCompressorWithParam<int>(cfg.scrubbingUpdatesDelay(), callback, KisSignalCompressor::FIRST_ACTIVE));
97 }
98 
~KisTimeBasedItemModel()99 KisTimeBasedItemModel::~KisTimeBasedItemModel()
100 {}
101 
setImage(KisImageWSP image)102 void KisTimeBasedItemModel::setImage(KisImageWSP image)
103 {
104     KisImageWSP oldImage = m_d->image;
105 
106     m_d->image = image;
107 
108     if (image) {
109         KisImageAnimationInterface *ai = image->animationInterface();
110 
111         connect(ai, SIGNAL(sigFramerateChanged()), SLOT(slotFramerateChanged()));
112         connect(ai, SIGNAL(sigUiTimeChanged(int)), SLOT(slotCurrentTimeChanged(int)));
113     }
114 
115     if (image != oldImage) {
116         beginResetModel();
117         endResetModel();
118     }
119 }
120 
setFrameCache(KisAnimationFrameCacheSP cache)121 void KisTimeBasedItemModel::setFrameCache(KisAnimationFrameCacheSP cache)
122 {
123     if (KisAnimationFrameCacheSP(m_d->framesCache) == cache) return;
124 
125     if (m_d->framesCache) {
126         m_d->framesCache->disconnect(this);
127     }
128 
129     m_d->framesCache = cache;
130 
131     if (m_d->framesCache) {
132         connect(m_d->framesCache, SIGNAL(changed()), SLOT(slotCacheChanged()));
133     }
134 }
135 
setAnimationPlayer(KisAnimationPlayer * player)136 void KisTimeBasedItemModel::setAnimationPlayer(KisAnimationPlayer *player)
137 {
138     if (m_d->animationPlayer == player) return;
139 
140     if (m_d->animationPlayer) {
141         m_d->animationPlayer->disconnect(this);
142     }
143 
144     m_d->animationPlayer = player;
145 
146     if (m_d->animationPlayer) {
147         connect(m_d->animationPlayer, SIGNAL(sigPlaybackStopped()), SLOT(slotPlaybackStopped()));
148         connect(m_d->animationPlayer, SIGNAL(sigFrameChanged()), SLOT(slotPlaybackFrameChanged()));
149     }
150 }
151 
setLastVisibleFrame(int time)152 void KisTimeBasedItemModel::setLastVisibleFrame(int time)
153 {
154     const int growThreshold = m_d->effectiveNumFrames() - 3;
155     const int growValue = time + 8;
156 
157     const int shrinkThreshold = m_d->effectiveNumFrames() - 12;
158     const int shrinkValue = qMax(m_d->baseNumFrames(), qMin(growValue, shrinkThreshold));
159     const bool canShrink = m_d->effectiveNumFrames() > m_d->baseNumFrames();
160 
161     if (time >= growThreshold) {
162         beginInsertColumns(QModelIndex(), m_d->effectiveNumFrames(), growValue - 1);
163         m_d->numFramesOverride = growValue;
164         endInsertColumns();
165     } else if (time < shrinkThreshold && canShrink) {
166         beginRemoveColumns(QModelIndex(), shrinkValue, m_d->effectiveNumFrames() - 1);
167         m_d->numFramesOverride = shrinkValue;
168         endRemoveColumns();
169     }
170 }
171 
columnCount(const QModelIndex & parent) const172 int KisTimeBasedItemModel::columnCount(const QModelIndex &parent) const
173 {
174     Q_UNUSED(parent);
175     return m_d->effectiveNumFrames();
176 }
177 
data(const QModelIndex & index,int role) const178 QVariant KisTimeBasedItemModel::data(const QModelIndex &index, int role) const
179 {
180     switch (role) {
181     case ActiveFrameRole: {
182         return index.column() == m_d->activeFrameIndex;
183     }
184     }
185 
186     return QVariant();
187 }
188 
setData(const QModelIndex & index,const QVariant & value,int role)189 bool KisTimeBasedItemModel::setData(const QModelIndex &index, const QVariant &value, int role)
190 {
191     if (!index.isValid()) return false;
192 
193     switch (role) {
194     case ActiveFrameRole: {
195         setHeaderData(index.column(), Qt::Horizontal, value, role);
196         break;
197     }
198     }
199 
200     return false;
201 }
202 
headerData(int section,Qt::Orientation orientation,int role) const203 QVariant KisTimeBasedItemModel::headerData(int section, Qt::Orientation orientation, int role) const
204 {
205     if (orientation == Qt::Horizontal) {
206         switch (role) {
207         case ActiveFrameRole:
208             return section == m_d->activeFrameIndex;
209         case FrameCachedRole:
210             return m_d->cachedFrames.size() > section ? m_d->cachedFrames[section] : false;
211         case FramesPerSecondRole:
212             return m_d->framesPerSecond();
213         }
214     }
215 
216     return QVariant();
217 }
218 
setHeaderData(int section,Qt::Orientation orientation,const QVariant & value,int role)219 bool KisTimeBasedItemModel::setHeaderData(int section, Qt::Orientation orientation, const QVariant &value, int role)
220 {
221     if (orientation == Qt::Horizontal) {
222         switch (role) {
223         case ActiveFrameRole:
224             if (value.toBool() &&
225                 section != m_d->activeFrameIndex) {
226 
227                 int prevFrame = m_d->activeFrameIndex;
228                 m_d->activeFrameIndex = section;
229 
230                 scrubTo(m_d->activeFrameIndex, m_d->scrubInProgress);
231 
232                 /**
233                  * Optimization Hack Alert:
234                  *
235                  * ideally, we should emit all four signals, but... The
236                  * point is this code is used in a tight loop during
237                  * playback, so it should run as fast as possible. To tell
238                  * the story short, commenting out these three lines makes
239                  * playback run 15% faster ;)
240                  */
241 
242                 if (m_d->scrubInProgress) {
243                     //emit dataChanged(this->index(0, prevFrame), this->index(rowCount() - 1, prevFrame));
244                     emit dataChanged(this->index(0, m_d->activeFrameIndex), this->index(rowCount() - 1, m_d->activeFrameIndex));
245                     //emit headerDataChanged (Qt::Horizontal, prevFrame, prevFrame);
246                     //emit headerDataChanged (Qt::Horizontal, m_d->activeFrameIndex, m_d->activeFrameIndex);
247                 } else {
248                     emit dataChanged(this->index(0, prevFrame), this->index(rowCount() - 1, prevFrame));
249                     emit dataChanged(this->index(0, m_d->activeFrameIndex), this->index(rowCount() - 1, m_d->activeFrameIndex));
250                     emit headerDataChanged (Qt::Horizontal, prevFrame, prevFrame);
251                     emit headerDataChanged (Qt::Horizontal, m_d->activeFrameIndex, m_d->activeFrameIndex);
252                 }
253             }
254         }
255     }
256 
257     return false;
258 }
259 
removeFrames(const QModelIndexList & indexes)260 bool KisTimeBasedItemModel::removeFrames(const QModelIndexList &indexes)
261 {
262     KisAnimationUtils::FrameItemList frameItems;
263 
264     {
265         KisImageBarrierLockerWithFeedback locker(m_d->image);
266 
267         Q_FOREACH (const QModelIndex &index, indexes) {
268             int time = index.column();
269             Q_FOREACH(KisKeyframeChannel *channel, channelsAt(index)) {
270                 if (channel->keyframeAt(time)) {
271                     frameItems << KisAnimationUtils::FrameItem(channel->node(), channel->id(), index.column());
272                 }
273             }
274         }
275     }
276 
277     if (frameItems.isEmpty()) return false;
278 
279     KisAnimationUtils::removeKeyframes(m_d->image, frameItems);
280 
281     return true;
282 }
283 
createOffsetFramesCommand(QModelIndexList srcIndexes,const QPoint & offset,bool copyFrames,bool moveEmptyFrames,KUndo2Command * parentCommand)284 KUndo2Command* KisTimeBasedItemModel::createOffsetFramesCommand(QModelIndexList srcIndexes,
285                                                                 const QPoint &offset,
286                                                                 bool copyFrames,
287                                                                 bool moveEmptyFrames,
288                                                                 KUndo2Command *parentCommand)
289 {
290     if (srcIndexes.isEmpty()) return 0;
291     if (offset.isNull()) return 0;
292 
293     KisAnimationUtils::sortPointsForSafeMove(&srcIndexes, offset);
294 
295     KisAnimationUtils::FrameItemList srcFrameItems;
296     KisAnimationUtils::FrameItemList dstFrameItems;
297 
298     Q_FOREACH (const QModelIndex &srcIndex, srcIndexes) {
299         QModelIndex dstIndex = index(
300                 srcIndex.row() + offset.y(),
301                 srcIndex.column() + offset.x());
302 
303         KisNodeSP srcNode = nodeAt(srcIndex);
304         KisNodeSP dstNode = nodeAt(dstIndex);
305         if (!srcNode || !dstNode) return 0;
306 
307         Q_FOREACH(KisKeyframeChannel *channel, channelsAt(srcIndex)) {
308             if (moveEmptyFrames || channel->keyframeAt(srcIndex.column())) {
309                 srcFrameItems << KisAnimationUtils::FrameItem(srcNode, channel->id(), srcIndex.column());
310                 dstFrameItems << KisAnimationUtils::FrameItem(dstNode, channel->id(), dstIndex.column());
311             }
312         }
313     }
314 
315     KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(srcFrameItems.size() == dstFrameItems.size(), 0);
316     if (srcFrameItems.isEmpty()) return 0;
317 
318     return
319         KisAnimationUtils::createMoveKeyframesCommand(srcFrameItems,
320                                                       dstFrameItems,
321                                                       copyFrames,
322                                                       moveEmptyFrames,
323                                                       parentCommand);
324 }
325 
removeFramesAndOffset(QModelIndexList indicesToRemove)326 bool KisTimeBasedItemModel::removeFramesAndOffset(QModelIndexList indicesToRemove)
327 {
328     if (indicesToRemove.isEmpty()) return true;
329 
330     std::sort(indicesToRemove.begin(), indicesToRemove.end(),
331               [] (const QModelIndex &lhs, const QModelIndex &rhs) {
332                   return lhs.column() > rhs.column();
333               });
334 
335     const int minColumn = indicesToRemove.last().column();
336 
337     KUndo2Command *parentCommand = new KUndo2Command(kundo2_i18np("Remove frame and shift", "Remove %1 frames and shift", indicesToRemove.size()));
338 
339     {
340         KisImageBarrierLockerWithFeedback locker(m_d->image);
341 
342         Q_FOREACH (const QModelIndex &index, indicesToRemove) {
343             QModelIndexList indicesToOffset;
344             for (int column = index.column() + 1; column < columnCount(); column++) {
345                 indicesToOffset << this->index(index.row(), column);
346             }
347             createOffsetFramesCommand(indicesToOffset, QPoint(-1, 0), false, true, parentCommand);
348         }
349 
350         const int oldTime = m_d->image->animationInterface()->currentUITime();
351         const int newTime = minColumn;
352 
353         new KisSwitchCurrentTimeCommand(m_d->image->animationInterface(),
354                                         oldTime,
355                                         newTime,
356                                         parentCommand);
357     }
358 
359     KisProcessingApplicator::runSingleCommandStroke(m_d->image, parentCommand,
360                                                     KisStrokeJobData::BARRIER,
361                                                     KisStrokeJobData::EXCLUSIVE);
362     return true;
363 }
364 
mirrorFrames(QModelIndexList indexes)365 bool KisTimeBasedItemModel::mirrorFrames(QModelIndexList indexes)
366 {
367     QScopedPointer<KUndo2Command> parentCommand(new KUndo2Command(kundo2_i18n("Mirror Frames")));
368 
369     {
370         KisImageBarrierLockerWithFeedback locker(m_d->image);
371 
372         QMap<int, QModelIndexList> rowsList;
373 
374         Q_FOREACH (const QModelIndex &index, indexes) {
375             rowsList[index.row()].append(index);
376         }
377 
378 
379         Q_FOREACH (int row, rowsList.keys()) {
380             QModelIndexList &list = rowsList[row];
381 
382             KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!list.isEmpty(), false);
383 
384             std::sort(list.begin(), list.end(),
385                 [] (const QModelIndex &lhs, const QModelIndex &rhs) {
386                     return lhs.column() < rhs.column();
387                 });
388 
389             auto srcIt = list.begin();
390             auto dstIt = list.end();
391 
392             KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(srcIt != dstIt, false);
393             --dstIt;
394 
395             QList<KisKeyframeChannel*> channels = channelsAt(*srcIt).values();
396 
397             while (srcIt < dstIt) {
398                 Q_FOREACH (KisKeyframeChannel *channel, channels) {
399                     channel->swapFrames(srcIt->column(), dstIt->column(), parentCommand.data());
400                 }
401 
402                 srcIt++;
403                 dstIt--;
404             }
405         }
406     }
407 
408     KisProcessingApplicator::runSingleCommandStroke(m_d->image,
409                                                     new KisCommandUtils::SkipFirstRedoWrapper(parentCommand.take()),
410                                                     KisStrokeJobData::BARRIER,
411                                                     KisStrokeJobData::EXCLUSIVE);
412     return true;
413 }
414 
slotInternalScrubPreviewRequested(int time)415 void KisTimeBasedItemModel::slotInternalScrubPreviewRequested(int time)
416 {
417     if (m_d->animationPlayer && !m_d->animationPlayer->isPlaying()) {
418         m_d->animationPlayer->displayFrame(time);
419     }
420 }
421 
setScrubState(bool active)422 void KisTimeBasedItemModel::setScrubState(bool active)
423 {
424     if (!m_d->scrubInProgress && active) {
425 
426         if (m_d->framesCache) {
427             const int currentFrame = m_d->image->animationInterface()->currentUITime();
428             const bool hasCurrentFrameInCache = m_d->framesCache->frameStatus(currentFrame) == KisAnimationFrameCache::Cached;
429             if(!hasCurrentFrameInCache) {
430                 KisPart::instance()->prioritizeFrameForCache(m_d->image, currentFrame);
431             }
432         }
433 
434         m_d->scrubStartFrame = m_d->activeFrameIndex;
435         m_d->scrubInProgress = true;
436     }
437 
438     if (m_d->scrubInProgress && !active) {
439 
440         m_d->scrubInProgress = false;
441 
442         if (m_d->scrubStartFrame >= 0 &&
443             m_d->scrubStartFrame != m_d->activeFrameIndex) {
444 
445             scrubTo(m_d->activeFrameIndex, false);
446         }
447 
448         m_d->scrubStartFrame = -1;
449     }
450 }
451 
scrubTo(int time,bool preview)452 void KisTimeBasedItemModel::scrubTo(int time, bool preview)
453 {
454     if (m_d->animationPlayer && m_d->animationPlayer->isPlaying()) return;
455 
456     KIS_ASSERT_RECOVER_RETURN(m_d->image);
457 
458     if (preview) {
459         if (m_d->animationPlayer) {
460             m_d->scrubbingCompressor->start(time);
461         }
462     } else {
463         m_d->image->animationInterface()->requestTimeSwitchWithUndo(time);
464     }
465 }
466 
slotCurrentTimeChanged(int time)467 void KisTimeBasedItemModel::slotCurrentTimeChanged(int time)
468 {
469     if (time != m_d->activeFrameIndex) {
470         setHeaderData(time, Qt::Horizontal, true, ActiveFrameRole);
471     }
472 }
473 
slotFramerateChanged()474 void KisTimeBasedItemModel::slotFramerateChanged()
475 {
476     emit headerDataChanged(Qt::Horizontal, 0, columnCount() - 1);
477 }
478 
slotCacheChanged()479 void KisTimeBasedItemModel::slotCacheChanged()
480 {
481     const int numFrames = columnCount();
482     m_d->cachedFrames.resize(numFrames);
483 
484     for (int i = 0; i < numFrames; i++) {
485         m_d->cachedFrames[i] =
486             m_d->framesCache->frameStatus(i) == KisAnimationFrameCache::Cached;
487     }
488 
489     emit headerDataChanged(Qt::Horizontal, 0, numFrames);
490 }
491 
492 
slotPlaybackFrameChanged()493 void KisTimeBasedItemModel::slotPlaybackFrameChanged()
494 {
495     if (!m_d->animationPlayer->isPlaying()) return;
496     setData(index(0, m_d->animationPlayer->currentTime()), true, ActiveFrameRole);
497 }
498 
slotPlaybackStopped()499 void KisTimeBasedItemModel::slotPlaybackStopped()
500 {
501     setData(index(0, m_d->image->animationInterface()->currentUITime()), true, ActiveFrameRole);
502 }
503 
setPlaybackRange(const KisTimeRange & range)504 void KisTimeBasedItemModel::setPlaybackRange(const KisTimeRange &range)
505 {
506     if (m_d->image.isNull()) return;
507 
508     KisImageAnimationInterface *i = m_d->image->animationInterface();
509     i->setPlaybackRange(range);
510 }
511 
isPlaybackActive() const512 bool KisTimeBasedItemModel::isPlaybackActive() const
513 {
514     return m_d->animationPlayer && m_d->animationPlayer->isPlaying();
515 }
516 
currentTime() const517 int KisTimeBasedItemModel::currentTime() const
518 {
519     return m_d->image->animationInterface()->currentUITime();
520 }
521 
image() const522 KisImageWSP KisTimeBasedItemModel::image() const
523 {
524     return m_d->image;
525 }
526