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