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 KisPart::instance()->playbackEngine()->seek(m_d->activeFrameIndex, seekFlags);
332 break;
333 }
334 }
335
336 return false;
337}
338
340{
341 Q_EMIT headerDataChanged (Qt::Horizontal, m_d->scrubHeaderMin, m_d->scrubHeaderMax);
342 m_d->scrubHeaderMin = activeColumn;
343 m_d->scrubHeaderMax = activeColumn;
344}
345
347{
349
350 {
351 KisImageBarrierLock locker(m_d->image);
352
353 Q_FOREACH (const QModelIndex &index, indexes) {
354 int time = index.column();
355 Q_FOREACH(KisKeyframeChannel *channel, channelsAt(index)) {
356 if (channel->keyframeAt(time)) {
357 frameItems << KisAnimUtils::FrameItem(channel->node(), channel->id(), index.column());
358 }
359 }
360 }
361 }
362
363 if (frameItems.isEmpty()) return false;
364
365 KisAnimUtils::removeKeyframes(m_d->image, frameItems);
366
367 return true;
368}
369
371 const QPoint &offset,
372 bool copyFrames,
373 bool moveEmptyFrames,
374 KUndo2Command *parentCommand)
375{
376 if (srcIndexes.isEmpty()) return 0;
377 if (offset.isNull()) return 0;
378
379 KisAnimUtils::sortPointsForSafeMove(&srcIndexes, offset);
380
381 KisAnimUtils::FrameItemList srcFrameItems;
382 KisAnimUtils::FrameItemList dstFrameItems;
383
384 Q_FOREACH (const QModelIndex &srcIndex, srcIndexes) {
385 QModelIndex dstIndex = index(
386 srcIndex.row() + offset.y(),
387 srcIndex.column() + offset.x());
388
389 KisNodeSP srcNode = nodeAt(srcIndex);
390 KisNodeSP dstNode = nodeAt(dstIndex);
391 if (!srcNode || !dstNode) return 0;
392
393 Q_FOREACH(KisKeyframeChannel *channel, channelsAt(srcIndex)) {
394 if (moveEmptyFrames || channel->keyframeAt(srcIndex.column())) {
395 srcFrameItems << KisAnimUtils::FrameItem(srcNode, channel->id(), srcIndex.column());
396 dstFrameItems << KisAnimUtils::FrameItem(dstNode, channel->id(), dstIndex.column());
397 }
398 }
399 }
400
401 KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(srcFrameItems.size() == dstFrameItems.size(), 0);
402 if (srcFrameItems.isEmpty()) return 0;
403
404 return
406 dstFrameItems,
407 copyFrames,
408 moveEmptyFrames,
409 parentCommand);
410}
411
413{
414 if (indicesToRemove.isEmpty()) return true;
415
416 std::sort(indicesToRemove.begin(), indicesToRemove.end(),
417 [] (const QModelIndex &lhs, const QModelIndex &rhs) {
418 return lhs.column() > rhs.column();
419 });
420
421 const int minColumn = indicesToRemove.last().column();
422
423 KUndo2Command *parentCommand = new KUndo2Command(kundo2_i18np("Remove frame and shift", "Remove %1 frames and shift", indicesToRemove.size()));
424
425 {
426 KisImageBarrierLock locker(m_d->image);
427
428 Q_FOREACH (const QModelIndex &index, indicesToRemove) {
429 QModelIndexList indicesToOffset;
430 for (int column = index.column() + 1; column < columnCount(); column++) {
431 indicesToOffset << this->index(index.row(), column);
432 }
433 createOffsetFramesCommand(indicesToOffset, QPoint(-1, 0), false, true, parentCommand);
434 }
435
436 const int oldTime = m_d->image->animationInterface()->currentUITime();
437 const int newTime = minColumn;
438
439 new KisSwitchCurrentTimeCommand(m_d->image->animationInterface(),
440 oldTime,
441 newTime,
442 parentCommand);
443 }
444
448 return true;
449}
450
452{
453 std::unique_ptr<KUndo2Command> parentCommand(new KUndo2Command(kundo2_i18n("Mirror Frames")));
454
455 {
456 KisImageBarrierLock locker(m_d->image);
457
458 QMap<int, QModelIndexList> rowsList;
459
460 Q_FOREACH (const QModelIndex &index, indexes) {
461 rowsList[index.row()].append(index);
462 }
463
464
465 Q_FOREACH (int row, rowsList.keys()) {
466 QModelIndexList &list = rowsList[row];
467
468 KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!list.isEmpty(), false);
469
470 std::sort(list.begin(), list.end(),
471 [] (const QModelIndex &lhs, const QModelIndex &rhs) {
472 return lhs.column() < rhs.column();
473 });
474
475 auto srcIt = list.begin();
476 auto dstIt = list.end();
477
478 KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(srcIt != dstIt, false);
479 --dstIt;
480
481 QList<KisKeyframeChannel*> channels = channelsAt(*srcIt).values();
482
483 while (srcIt < dstIt) {
484 Q_FOREACH (KisKeyframeChannel *channel, channels) {
485 if (channel->keyframeAt(srcIt->column()) && channel->keyframeAt(dstIt->column())) {
486
487 channel->swapKeyframes(srcIt->column(),
488 dstIt->column(),
489 parentCommand.get());
490 }
491 else if (channel->keyframeAt(srcIt->column())) {
492
493 channel->insertKeyframe(dstIt->column(),
494 channel->keyframeAt(srcIt->column()),
495 parentCommand.get());
496
497 channel->removeKeyframe(srcIt->column(),
498 parentCommand.get());
499 }
500 else if (channel->keyframeAt(dstIt->column())) {
501
502 channel->insertKeyframe(srcIt->column(),
503 channel->keyframeAt(dstIt->column()),
504 parentCommand.get());
505
506 channel->removeKeyframe(dstIt->column(),
507 parentCommand.get());
508 }
509 }
510
511 srcIt++;
512 dstIt--;
513 }
514 }
515 }
516
518 parentCommand.release(),
521 return true;
522}
523
525{
526 if (!m_d->animationPlayer) {
527 return;
528 }
529
530 if (m_d->scrubInProgress != p_state) {
531 m_d->scrubInProgress = p_state;
532
533 if (m_d->scrubInProgress == true) {
534 m_d->scrubStartFrame = m_d->activeFrameIndex;
535
536 if (m_d->animationPlayer->playbackState() == PLAYING) {
537 m_d->shouldReturnToPlay = true;
538 m_d->animationPlayer->setPlaybackState(PAUSED);
539 }
540
541 } else {
542 if (m_d->shouldReturnToPlay) {
543 m_d->animationPlayer->setPlaybackState(PLAYING);
544 }
545
546 m_d->scrubStartFrame = -1;
547 m_d->shouldReturnToPlay = false;
548 }
549 }
550}
551
553{
554 return m_d->scrubInProgress;
555}
556
557
559{
560 if (time != m_d->activeFrameIndex) {
561 setHeaderData(time, Qt::Horizontal, true, ActiveFrameRole);
562 }
563}
564
566{
567 Q_EMIT headerDataChanged(Qt::Horizontal, 0, columnCount() - 1);
568}
569
571{
572 if (m_d->image && m_d->image->animationInterface() ) {
573 const KisImageAnimationInterface* const interface = m_d->image->animationInterface();
574 const int lastFrame = interface->activePlaybackRange().end();
575 if (lastFrame > m_d->numFramesOverride) {
576 beginInsertColumns(QModelIndex(), m_d->numFramesOverride, interface->activePlaybackRange().end());
577 m_d->numFramesOverride = interface->activePlaybackRange().end();
578 endInsertColumns();
579 }
580
581 dataChanged(index(0,0), index(rowCount(), columnCount()));
582 }
583}
584
586{
587 const int numFrames = columnCount();
588 m_d->cachedFrames.resize(numFrames);
589
590 for (int i = 0; i < numFrames; i++) {
591 m_d->cachedFrames[i] =
592 m_d->framesCache->frameStatus(i) == KisAnimationFrameCache::Cached;
593 }
594
595 Q_EMIT headerDataChanged(Qt::Horizontal, 0, numFrames);
596}
597
598
600{
601 if (m_d->animationPlayer->playbackState() != PlaybackState::PLAYING) return;
602 setHeaderData(m_d->animationPlayer->displayProxy()->activeFrame(), Qt::Horizontal, true, ActiveFrameRole);
603}
604
606{
607 if (p_state == PlaybackState::STOPPED) {
608 setHeaderData(m_d->image->animationInterface()->currentUITime(), Qt::Horizontal, true, ActiveFrameRole);
609 }
610}
611
613{
614 if (m_d->image.isNull()) return;
615
617 i->setActivePlaybackRange(range);
618}
619
621{
622 return m_d->animationPlayer && m_d->animationPlayer->playbackState() == PlaybackState::PLAYING;
623}
624
626{
627 return m_d->animationPlayer && m_d->animationPlayer->playbackState() == PlaybackState::PAUSED;
628}
629
633
635{
636 return m_d->image->animationInterface()->currentUITime();
637}
638
639bool KisTimeBasedItemModel::cloneOfActiveFrame(const QModelIndex &index) const {
641 if (!rasterChan) return false;
642
643 const int activeKeyframeTime = rasterChan->activeKeyframeTime(m_d->activeFrameIndex);
644 return rasterChan->areClones(activeKeyframeTime, index.column());
645}
646
647int KisTimeBasedItemModel::cloneCount(const QModelIndex &index) const {
649
650 if (!rasterChan) {
651 return 0;
652 }
653 return rasterChan->clonesOf(index.column()).count();
654}
655
657{
658 return m_d->image;
659}
float value(const T *src, size_t ch)
@ SEEK_NONE
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
std::unique_ptr< KisPlaybackEngine > playbackEngine
Definition KisPart.cpp:111
void prioritizeFrameForCache(KisImageSP image, int frame)
Definition KisPart.cpp:506
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