Krita Source Code Documentation
Loading...
Searching...
No Matches
KisTimeBasedItemModel.cpp
Go to the documentation of this file.
1/*
2 * SPDX-FileCopyrightText: 2016 Jouni Pentikäinen <joupent@gmail.com>
3 *
4 * SPDX-License-Identifier: GPL-2.0-or-later
5 */
6
8
9#include <QPointer>
10#include <kis_config.h>
11
16#include "kis_image.h"
18#include "kis_time_span.h"
19#include "KisAnimUtils.h"
23#include "KisImageBarrierLock.h"
25#include "kis_command_utils.h"
26#include "KisPart.h"
27#include "KisPlaybackEngine.h"
29
31{
34 , document(nullptr)
37 , scrubInProgress(false)
38 , scrubStartFrame(-1)
39 , shouldReturnToPlay(false)
42 {}
43
48
50
53
57
58 QScopedPointer<KisSignalCompressorWithParam<int>> scrubHeaderUpdateCompressor;
61
62 int baseNumFrames() const {
63
64 auto imageSP = image.toStrongRef();
65 if (!imageSP) return 0;
66
67 KisImageAnimationInterface *i = imageSP->animationInterface();
68 if (!i) return 1;
69
70 return i->totalLength();
71 }
72
73 int effectiveNumFrames() const {
74 if (image.isNull()) return 0;
75
76 return qMax(baseNumFrames(), numFramesOverride);
77 }
78
81 }
82
83 bool withinClipRange(const int time) {
84 if (!image) {
85 return true;
86 }
87
89 return clipRange.contains(time);
90 }
91};
92
94 : QAbstractTableModel(parent)
95 , m_d(new Private())
96{
97 KisConfig cfg(true);
98
99 using namespace std::placeholders;
100
101 std::function<void (int)> scrubHorizHeaderUpdateCallback(
103
104 m_d->scrubHeaderUpdateCompressor.reset(
105 new KisSignalCompressorWithParam<int>(100, scrubHorizHeaderUpdateCallback, KisSignalCompressor::FIRST_ACTIVE));
106}
107
110
112{
113 if (m_d->image == p_image ) {
114 return;
115 }
116
117 beginResetModel();
118
119 if (m_d->image) {
120 //Disconnect old image..
122 ai->disconnect(this);
123 }
124
125 m_d->image = p_image;
126 m_d->numFramesOverride = m_d->effectiveNumFrames();
127
128 if (m_d->image) {
130
131 connect(ai, SIGNAL(sigFramerateChanged()), this, SLOT(slotFramerateChanged()));
132 connect(ai, SIGNAL(sigUiTimeChanged(int)), this, SLOT(slotCurrentTimeChanged(int)));
133 connect(ai, SIGNAL(sigPlaybackRangeChanged()), this, SLOT(slotPlaybackRangeChanged()));
134 }
135
136 endResetModel();
137}
138
140{
141 if (KisAnimationFrameCacheSP(m_d->framesCache) == cache) return;
142
143 if (m_d->framesCache) {
144 m_d->framesCache->disconnect(this);
145 }
146
147 m_d->framesCache = cache;
148
149 if (m_d->framesCache) {
150 connect(m_d->framesCache, SIGNAL(changed()), SLOT(slotCacheChanged()));
151 }
152}
153
155{
156 return m_d->framesCache && m_d->framesCache->frameStatus(frame) == KisAnimationFrameCache::Cached;
157}
158
160{
161 if (m_d->animationPlayer == player) return;
162
163 if (m_d->animationPlayer) {
164 m_d->animationPlayer->disconnect(this);
165 }
166
167 m_d->animationPlayer = player;
168
169 if (m_d->animationPlayer) {
170 connect(m_d->animationPlayer, SIGNAL(sigPlaybackStateChanged(PlaybackState)), SLOT(slotPlaybackStateChanged(PlaybackState)));
171 connect(m_d->animationPlayer, SIGNAL(sigFrameChanged()), SLOT(slotPlaybackFrameChanged()));
172
173 const int frame = player ? player->displayProxy()->activeFrame() : m_d->image->animationInterface()->currentUITime();
174 setHeaderData(frame, Qt::Horizontal, true, ActiveFrameRole);
175
176 // only prioritize the cache, no seek operation to prevent audio from playing
177 setHeaderData(frame, Qt::Horizontal, QVariant(int(SEEK_NONE)), ScrubToRole);
178 }
179}
180
182{
183 if (m_d->document == document) return;
184
185 m_d->document = document;
186}
187
189{
190 return m_d->document;
191}
192
194{
195 const int growThreshold = m_d->effectiveNumFrames() - 1;
196 const int growValue = time + 8;
197
198 const int shrinkThreshold = m_d->effectiveNumFrames() - 3;
199 const int shrinkValue = qMax(m_d->baseNumFrames(), qMin(growValue, shrinkThreshold));
200 const bool canShrink = m_d->baseNumFrames() < m_d->effectiveNumFrames();
201
202 if (time >= growThreshold) {
203 beginInsertColumns(QModelIndex(), m_d->effectiveNumFrames(), growValue - 1);
204 m_d->numFramesOverride = growValue;
205 endInsertColumns();
206 } else if (time < shrinkThreshold && canShrink) {
207 beginRemoveColumns(QModelIndex(), shrinkValue, m_d->effectiveNumFrames() - 1);
208 m_d->numFramesOverride = shrinkValue;
209 endRemoveColumns();
210 }
211}
212
213int KisTimeBasedItemModel::columnCount(const QModelIndex &parent) const
214{
215 Q_UNUSED(parent);
216 return m_d->numFramesOverride;
217}
218
219QVariant KisTimeBasedItemModel::data(const QModelIndex &index, int role) const
220{
221 switch (role) {
222 case ActiveFrameRole: {
223 return index.column() == m_d->activeFrameIndex;
224 }
225 case CloneOfActiveFrame: {
226 return cloneOfActiveFrame(index);
227 }
228 case CloneCount: {
229 return cloneCount(index);
230 }
231 case WithinClipRange:
232 return m_d->withinClipRange(index.column());
233 }
234
235 return QVariant();
236}
237
238bool KisTimeBasedItemModel::setData(const QModelIndex &index, const QVariant &value, int role)
239{
240 if (!index.isValid()) return false;
241
242 switch (role) {
243 case ActiveFrameRole:
244 setHeaderData(index.column(), Qt::Horizontal, value, role);
245 break;
246 case ScrubToRole:
247 setHeaderData(index.column(), Qt::Horizontal, value, role);
248 break;
249 }
250
251 return false;
252}
253
254QVariant KisTimeBasedItemModel::headerData(int section, Qt::Orientation orientation, int role) const
255{
256 if (orientation == Qt::Horizontal) {
257 switch (role) {
258 case ActiveFrameRole:
259 return section == m_d->activeFrameIndex;
260 case FrameCachedRole:
261 return m_d->cachedFrames.size() > section ? m_d->cachedFrames[section] : false;
263 return m_d->framesPerSecond();
264 case WithinClipRange:
265 return m_d->withinClipRange(section);
266 }
267 }
268
269 return QVariant();
270}
271
272bool KisTimeBasedItemModel::setHeaderData(int section, Qt::Orientation orientation, const QVariant &value, int role)
273{
274 auto prioritizeCache = [this](int frame){
275 if (m_d->image) {
276 if(!isFrameCached(frame)) {
278 }
279 }
280 };
281
282
283 if (orientation == Qt::Horizontal) {
284 switch (role) {
285 case ActiveFrameRole:
286 if (value.toBool() &&
287 section != m_d->activeFrameIndex) {
288
289 int prevFrame = m_d->activeFrameIndex;
290 m_d->activeFrameIndex = section;
291
302 if (m_d->scrubInProgress) {
303 Q_EMIT dataChanged(this->index(0, m_d->activeFrameIndex), this->index(rowCount() - 1, m_d->activeFrameIndex));
304
305 /*
306 * In order to try to correct rendering issues while preserving performance, we will
307 * defer updates just long enough that visual artifacts aren't majorly noticeable.
308 * By using a signal compressor, we're going to update the range of columns between
309 * min / max. That min max is reset every time the update occurs. This should fix
310 * rendering issues to a configurable framerate.
311 */
312 m_d->scrubHeaderMin = qMin(m_d->activeFrameIndex, m_d->scrubHeaderMin);
313 m_d->scrubHeaderMax = qMax(m_d->activeFrameIndex, m_d->scrubHeaderMax);
314 m_d->scrubHeaderUpdateCompressor->start(m_d->activeFrameIndex);
315
316 // vvvvvvvvvvvvvvvvvvvvv Read above comment.. This fixes all timeline rendering issues, but at what cost???
317 //Q_EMIT dataChanged(this->index(0, prevFrame), this->index(rowCount() - 1, prevFrame));
318 //Q_EMIT headerDataChanged (Qt::Horizontal, m_d->activeFrameIndex, m_d->activeFrameIndex);
319 //Q_EMIT headerDataChanged (Qt::Horizontal, prevFrame, prevFrame);
320 } else {
321 Q_EMIT dataChanged(this->index(0, prevFrame), this->index(rowCount() - 1, prevFrame));
322 Q_EMIT dataChanged(this->index(0, m_d->activeFrameIndex), this->index(rowCount() - 1, m_d->activeFrameIndex));
323 Q_EMIT headerDataChanged (Qt::Horizontal, prevFrame, prevFrame);
324 Q_EMIT headerDataChanged (Qt::Horizontal, m_d->activeFrameIndex, m_d->activeFrameIndex);
325 }
326 }
327 break;
328 case ScrubToRole:
329 SeekOptionFlags seekFlags = SeekOptionFlags(value.toInt());
330 prioritizeCache(m_d->activeFrameIndex);
331 if (!m_d->image->hasUpdatesRunning()) {
332 KisPart::instance()->playbackEngine()->seek(m_d->activeFrameIndex, seekFlags);
333 }
334 break;
335 }
336 }
337
338 return false;
339}
340
342{
343 Q_EMIT headerDataChanged (Qt::Horizontal, m_d->scrubHeaderMin, m_d->scrubHeaderMax);
344 m_d->scrubHeaderMin = activeColumn;
345 m_d->scrubHeaderMax = activeColumn;
346}
347
349{
351
352 {
353 KisImageBarrierLock locker(m_d->image);
354
355 Q_FOREACH (const QModelIndex &index, indexes) {
356 int time = index.column();
357 Q_FOREACH(KisKeyframeChannel *channel, channelsAt(index)) {
358 if (channel->keyframeAt(time)) {
359 frameItems << KisAnimUtils::FrameItem(channel->node(), channel->id(), index.column());
360 }
361 }
362 }
363 }
364
365 if (frameItems.isEmpty()) return false;
366
367 KisAnimUtils::removeKeyframes(m_d->image, frameItems);
368
369 return true;
370}
371
373 const QPoint &offset,
374 bool copyFrames,
375 bool moveEmptyFrames,
376 KUndo2Command *parentCommand)
377{
378 if (srcIndexes.isEmpty()) return 0;
379 if (offset.isNull()) return 0;
380
381 KisAnimUtils::sortPointsForSafeMove(&srcIndexes, offset);
382
383 KisAnimUtils::FrameItemList srcFrameItems;
384 KisAnimUtils::FrameItemList dstFrameItems;
385
386 Q_FOREACH (const QModelIndex &srcIndex, srcIndexes) {
387 QModelIndex dstIndex = index(
388 srcIndex.row() + offset.y(),
389 srcIndex.column() + offset.x());
390
391 KisNodeSP srcNode = nodeAt(srcIndex);
392 KisNodeSP dstNode = nodeAt(dstIndex);
393 if (!srcNode || !dstNode) return 0;
394
395 Q_FOREACH(KisKeyframeChannel *channel, channelsAt(srcIndex)) {
396 if (moveEmptyFrames || channel->keyframeAt(srcIndex.column())) {
397 srcFrameItems << KisAnimUtils::FrameItem(srcNode, channel->id(), srcIndex.column());
398 dstFrameItems << KisAnimUtils::FrameItem(dstNode, channel->id(), dstIndex.column());
399 }
400 }
401 }
402
403 KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(srcFrameItems.size() == dstFrameItems.size(), 0);
404 if (srcFrameItems.isEmpty()) return 0;
405
406 return
408 dstFrameItems,
409 copyFrames,
410 moveEmptyFrames,
411 parentCommand);
412}
413
415{
416 if (indicesToRemove.isEmpty()) return true;
417
418 std::sort(indicesToRemove.begin(), indicesToRemove.end(),
419 [] (const QModelIndex &lhs, const QModelIndex &rhs) {
420 return lhs.column() > rhs.column();
421 });
422
423 const int minColumn = indicesToRemove.last().column();
424
425 KUndo2Command *parentCommand = new KUndo2Command(kundo2_i18np("Remove frame and shift", "Remove %1 frames and shift", indicesToRemove.size()));
426
427 {
428 KisImageBarrierLock locker(m_d->image);
429
430 Q_FOREACH (const QModelIndex &index, indicesToRemove) {
431 QModelIndexList indicesToOffset;
432 for (int column = index.column() + 1; column < columnCount(); column++) {
433 indicesToOffset << this->index(index.row(), column);
434 }
435 createOffsetFramesCommand(indicesToOffset, QPoint(-1, 0), false, true, parentCommand);
436 }
437
438 const int oldTime = m_d->image->animationInterface()->currentUITime();
439 const int newTime = minColumn;
440
441 new KisSwitchCurrentTimeCommand(m_d->image->animationInterface(),
442 oldTime,
443 newTime,
444 parentCommand);
445 }
446
450 return true;
451}
452
454{
455 QScopedPointer<KUndo2Command> parentCommand(new KUndo2Command(kundo2_i18n("Mirror Frames")));
456
457 {
458 KisImageBarrierLock locker(m_d->image);
459
460 QMap<int, QModelIndexList> rowsList;
461
462 Q_FOREACH (const QModelIndex &index, indexes) {
463 rowsList[index.row()].append(index);
464 }
465
466
467 Q_FOREACH (int row, rowsList.keys()) {
468 QModelIndexList &list = rowsList[row];
469
470 KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!list.isEmpty(), false);
471
472 std::sort(list.begin(), list.end(),
473 [] (const QModelIndex &lhs, const QModelIndex &rhs) {
474 return lhs.column() < rhs.column();
475 });
476
477 auto srcIt = list.begin();
478 auto dstIt = list.end();
479
480 KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(srcIt != dstIt, false);
481 --dstIt;
482
483 QList<KisKeyframeChannel*> channels = channelsAt(*srcIt).values();
484
485 while (srcIt < dstIt) {
486 Q_FOREACH (KisKeyframeChannel *channel, channels) {
487 if (channel->keyframeAt(srcIt->column()) && channel->keyframeAt(dstIt->column())) {
488
489 channel->swapKeyframes(srcIt->column(),
490 dstIt->column(),
491 parentCommand.data());
492 }
493 else if (channel->keyframeAt(srcIt->column())) {
494
495 channel->insertKeyframe(dstIt->column(),
496 channel->keyframeAt(srcIt->column()),
497 parentCommand.data());
498
499 channel->removeKeyframe(srcIt->column(),
500 parentCommand.data());
501 }
502 else if (channel->keyframeAt(dstIt->column())) {
503
504 channel->insertKeyframe(srcIt->column(),
505 channel->keyframeAt(dstIt->column()),
506 parentCommand.data());
507
508 channel->removeKeyframe(dstIt->column(),
509 parentCommand.data());
510 }
511 }
512
513 srcIt++;
514 dstIt--;
515 }
516 }
517 }
518
520 parentCommand.take(),
523 return true;
524}
525
527{
528 if (!m_d->animationPlayer) {
529 return;
530 }
531
532 if (m_d->scrubInProgress != p_state) {
533 m_d->scrubInProgress = p_state;
534
535 if (m_d->scrubInProgress == true) {
536 m_d->scrubStartFrame = m_d->activeFrameIndex;
537
538 if (m_d->animationPlayer->playbackState() == PLAYING) {
539 m_d->shouldReturnToPlay = true;
540 m_d->animationPlayer->setPlaybackState(PAUSED);
541 }
542
543 } else {
544 if (m_d->shouldReturnToPlay) {
545 m_d->animationPlayer->setPlaybackState(PLAYING);
546 }
547
548 m_d->scrubStartFrame = -1;
549 m_d->shouldReturnToPlay = false;
550 }
551 }
552}
553
555{
556 return m_d->scrubInProgress;
557}
558
559
561{
562 if (time != m_d->activeFrameIndex) {
563 setHeaderData(time, Qt::Horizontal, true, ActiveFrameRole);
564 }
565}
566
568{
569 Q_EMIT headerDataChanged(Qt::Horizontal, 0, columnCount() - 1);
570}
571
573{
574 if (m_d->image && m_d->image->animationInterface() ) {
575 const KisImageAnimationInterface* const interface = m_d->image->animationInterface();
576 const int lastFrame = interface->activePlaybackRange().end();
577 if (lastFrame > m_d->numFramesOverride) {
578 beginInsertColumns(QModelIndex(), m_d->numFramesOverride, interface->activePlaybackRange().end());
579 m_d->numFramesOverride = interface->activePlaybackRange().end();
580 endInsertColumns();
581 }
582
583 dataChanged(index(0,0), index(rowCount(), columnCount()));
584 }
585}
586
588{
589 const int numFrames = columnCount();
590 m_d->cachedFrames.resize(numFrames);
591
592 for (int i = 0; i < numFrames; i++) {
593 m_d->cachedFrames[i] =
594 m_d->framesCache->frameStatus(i) == KisAnimationFrameCache::Cached;
595 }
596
597 Q_EMIT headerDataChanged(Qt::Horizontal, 0, numFrames);
598}
599
600
602{
603 if (m_d->animationPlayer->playbackState() != PlaybackState::PLAYING) return;
604 setHeaderData(m_d->animationPlayer->displayProxy()->activeFrame(), Qt::Horizontal, true, ActiveFrameRole);
605}
606
608{
609 if (p_state == PlaybackState::STOPPED) {
610 setHeaderData(m_d->image->animationInterface()->currentUITime(), Qt::Horizontal, true, ActiveFrameRole);
611 }
612}
613
615{
616 if (m_d->image.isNull()) return;
617
619 i->setActivePlaybackRange(range);
620}
621
623{
624 return m_d->animationPlayer && m_d->animationPlayer->playbackState() == PlaybackState::PLAYING;
625}
626
628{
629 return m_d->animationPlayer && m_d->animationPlayer->playbackState() == PlaybackState::PAUSED;
630}
631
635
637{
638 return m_d->image->animationInterface()->currentUITime();
639}
640
641bool KisTimeBasedItemModel::cloneOfActiveFrame(const QModelIndex &index) const {
643 if (!rasterChan) return false;
644
645 const int activeKeyframeTime = rasterChan->activeKeyframeTime(m_d->activeFrameIndex);
646 return rasterChan->areClones(activeKeyframeTime, index.column());
647}
648
649int KisTimeBasedItemModel::cloneCount(const QModelIndex &index) const {
651
652 if (!rasterChan) {
653 return 0;
654 }
655 return rasterChan->clonesOf(index.column()).count();
656}
657
659{
660 return m_d->image;
661}
float value(const T *src, size_t ch)
@ SEEK_NONE
connect(this, SIGNAL(optionsChanged()), this, SLOT(saveOptions()))
The KisCanvasAnimationState class stores all of the canvas-specific animation state.
class KisFrameDisplayProxy * displayProxy()
int activeFrame() const
Gets the active frame, the frame that is intended to be shown. This should always reflect the actual ...
void setActivePlaybackRange(const KisTimeSpan range)
const KisTimeSpan & documentPlaybackRange() const
documentPlaybackRange
KisImageAnimationInterface * animationInterface() const
KisKeyframeChannel stores and manages KisKeyframes. Maps units of time to virtual keyframe values....
static const KoID Raster
virtual void insertKeyframe(int time, KisKeyframeSP keyframe, KUndo2Command *parentUndoCmd=nullptr)
Insert an existing keyframe into the channel at the specified time.
virtual void removeKeyframe(int time, KUndo2Command *parentUndoCmd=nullptr)
Remove a keyframe from the channel at the specified time.
static void swapKeyframes(KisKeyframeChannel *channelA, int timeA, KisKeyframeChannel *channelB, int timeB, KUndo2Command *parentUndoCmd=nullptr)
Swap two keyframes across channel(s) at the specified times.
KisKeyframeSP keyframeAt(int time) const
Get a keyframe at specified time. Used primarily when the value of a given keyframe is needed.
int activeKeyframeTime(int time) const
Get the time of the active keyframe. Useful for snapping any time to that of the most recent keyframe...
static KisPart * instance()
Definition KisPart.cpp:131
QScopedPointer< KisPlaybackEngine > playbackEngine
Definition KisPart.cpp:111
void prioritizeFrameForCache(KisImageSP image, int frame)
Definition KisPart.cpp:530
static void runSingleCommandStroke(KisImageSP image, KUndo2Command *cmd, KisStrokeJobData::Sequentiality sequentiality=KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::Exclusivity exclusivity=KisStrokeJobData::NORMAL)
runSingleCommandStroke creates a stroke and runs cmd in it. The text() field of cmd is used as a titl...
The KisRasterKeyframeChannel is a concrete KisKeyframeChannel subclass that stores and manages KisRas...
bool areClones(int timeA, int timeB)
bool cloneOfActiveFrame(const QModelIndex &index) const
bool removeFramesAndOffset(QModelIndexList indicesToRemove)
void setFrameCache(KisAnimationFrameCacheSP cache)
KUndo2Command * createOffsetFramesCommand(QModelIndexList srcIndexes, const QPoint &offset, bool copyFrames, bool moveEmptyFrames, KUndo2Command *parentCommand=0)
bool setData(const QModelIndex &index, const QVariant &value, int role) override
bool isFrameCached(const int frame)
void setPlaybackRange(const KisTimeSpan &range)
void setAnimationPlayer(KisCanvasAnimationState *player)
void scrubHorizontalHeaderUpdate(int activeHeader)
QVariant headerData(int section, Qt::Orientation orientation, int role) const override
virtual KisNodeSP nodeAt(QModelIndex index) const =0
void setImage(KisImageWSP image)
KisTimeBasedItemModel(QObject *parent)
KisDocument * document() const
int cloneCount(const QModelIndex &index) const
void slotPlaybackStateChanged(PlaybackState state)
virtual KisKeyframeChannel * channelByID(QModelIndex index, const QString &id) const =0
QVariant data(const QModelIndex &index, int role) const override
int columnCount(const QModelIndex &parent=QModelIndex()) const override
void setDocument(class KisDocument *document)
bool removeFrames(const QModelIndexList &indexes)
const QScopedPointer< Private > m_d
bool setHeaderData(int section, Qt::Orientation orientation, const QVariant &value, int role) override
virtual QMap< QString, KisKeyframeChannel * > channelsAt(QModelIndex index) const =0
bool mirrorFrames(QModelIndexList indexes)
bool contains(int time) const
bool isNull() const
KisSharedPtr< T > toStrongRef() const
toStrongRef returns a KisSharedPtr which may be dereferenced.
#define KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(cond, val)
Definition kis_assert.h:129
typedef void(QOPENGLF_APIENTRYP PFNGLINVALIDATEBUFFERDATAPROC)(GLuint buffer)
KisSharedPtr< KisAnimationFrameCache > KisAnimationFrameCacheSP
Definition kis_types.h:185
KUndo2MagicString kundo2_i18n(const char *text)
KUndo2MagicString kundo2_i18np(const char *sing, const char *plur, const A1 &a1)
void removeKeyframes(KisImageSP image, const FrameItemList &frames)
void sortPointsForSafeMove(QModelIndexList *points, const QPoint &offset)
KUndo2Command * createMoveKeyframesCommand(const FrameItemList &srcFrames, const FrameItemList &dstFrames, bool copy, bool moveEmpty, KUndo2Command *parentCommand)
KisAnimationFrameCacheWSP framesCache
QScopedPointer< KisSignalCompressorWithParam< int > > scrubHeaderUpdateCompressor
QPointer< KisCanvasAnimationState > animationPlayer