diff --git a/libs/image/kis_animation_cycle.cpp b/libs/image/kis_animation_cycle.cpp index 910a0f440a..a475927c27 100644 --- a/libs/image/kis_animation_cycle.cpp +++ b/libs/image/kis_animation_cycle.cpp @@ -1,212 +1,225 @@ /* * Copyright (c) 2018 Jouni Pentikäinen * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_animation_cycle.h" #include #include "kis_time_range.h" #include "kis_keyframe_channel.h" KisAnimationCycle::KisAnimationCycle(KisKeyframeChannel *channel, KisKeyframeSP firstKeyframe, KisKeyframeSP lastKeyframe) : KisKeyframeBase(channel, firstKeyframe->time()) , m_firstSourceKeyframe(firstKeyframe) , m_lastSourceKeyframe(lastKeyframe) {} KisKeyframeSP KisAnimationCycle::firstSourceKeyframe() const { return m_firstSourceKeyframe; } KisKeyframeSP KisAnimationCycle::lastSourceKeyframe() const { return m_lastSourceKeyframe; } KisTimeSpan KisAnimationCycle::originalRange() const { - const KisKeyframeSP firstAfterCycle = m_lastSourceKeyframe->channel()->nextKeyframe(m_lastSourceKeyframe); + const KisKeyframeBaseSP firstAfterCycle = m_lastSourceKeyframe->channel()->nextItem(*m_lastSourceKeyframe); KisTimeSpan range; if (firstAfterCycle.isNull()) { // TODO: semantics of repeat definition without a terminating keyframe? range = KisTimeSpan(m_firstSourceKeyframe->time(), m_lastSourceKeyframe->time()); } else { range = KisTimeSpan(m_firstSourceKeyframe->time(), firstAfterCycle->time() - 1); } return range; } int KisAnimationCycle::duration() const { return originalRange().duration(); } void KisAnimationCycle::addRepeat(QSharedPointer repeat) { m_repeats.append(repeat); } void KisAnimationCycle::removeRepeat(QSharedPointer repeat) { m_repeats.removeAll(repeat); } const QVector>& KisAnimationCycle::repeats() const { return m_repeats; } KisFrameSet KisAnimationCycle::instancesWithin(KisKeyframeSP original, KisTimeSpan range) const { KisFrameSet frames; const int originalTime = original->time(); - const KisKeyframeSP next = original->channel()->nextKeyframe(original); + const KisKeyframeBaseSP next = original->channel()->nextItem(*original); const int frameDuration = (next.isNull()) ? -1 : next->time() - originalTime; const int interval = duration(); int infiniteFrom = frameDuration == -1 ? originalTime : -1; QVector spans; Q_FOREACH(const QWeakPointer repeatFrame, m_repeats) { auto repeat = repeatFrame.toStrongRef(); if (!repeat) continue; - // FIXME! KisKeyframeSP terminatingKey = repeat->channel()->nextKeyframe(*repeat); - KisKeyframeSP terminatingKey = KisKeyframeSP(); + const int endTime = repeat->end(); if (range.isEmpty()) { - if (terminatingKey) { - range = KisTimeSpan(0, terminatingKey->time() -1); + if (endTime != -1) { + range = KisTimeSpan(0, endTime - 1); } else { infiniteFrom = repeat->firstInstanceOf(originalTime); continue; } } - KisTimeSpan repeatRange = terminatingKey ? range.truncateLeft(terminatingKey->time() - 1) : range; + KisTimeSpan repeatRange = (endTime != -1) ? range & KisTimeSpan(repeat->time(), endTime) : range.truncateRight(repeat->time()); int firstInstance = repeat->firstInstanceOf(originalTime); - if (firstInstance < repeatRange.start()) firstInstance += (range.start() - firstInstance) / interval * interval; + if (firstInstance == -1) continue; + + if (firstInstance < repeatRange.start()) { + firstInstance += interval * ((range.start() - firstInstance) / interval); + } for (int repeatTime = firstInstance; repeatTime <= repeatRange.end(); repeatTime += interval) { - const int repeatEndTime = (frameDuration != -1 && repeatTime + frameDuration - 1 <= repeatRange.end()) ? - repeatTime + frameDuration - 1 : repeatRange.end(); + bool endsWithinRange = frameDuration != -1 && repeatTime + frameDuration - 1 <= repeatRange.end(); + const int repeatEndTime = endsWithinRange ? (repeatTime + frameDuration - 1) : repeatRange.end(); spans.append(KisTimeSpan(repeatTime, repeatEndTime)); } } frames |= KisFrameSet(spans); if (infiniteFrom != -1) { frames |= KisFrameSet::infiniteFrom(infiniteFrom); } else { frames |= KisFrameSet::between(originalTime, originalTime + frameDuration - 1); } return frames; } QRect KisAnimationCycle::affectedRect() const { QRect rect; KisKeyframeSP keyframe = m_firstSourceKeyframe; do { rect |= keyframe->affectedRect(); keyframe = channel()->nextKeyframe(keyframe); } while (keyframe && keyframe != m_lastSourceKeyframe); return rect; } KisKeyframeSP KisAnimationCycle::getOriginalKeyframeFor(int time) const { return channel()->activeKeyframeAt(time); } KisRepeatFrame::KisRepeatFrame(KisKeyframeChannel *channel, int time, QSharedPointer cycle) : KisKeyframeBase(channel, time) , m_cycle(cycle) {} QSharedPointer KisRepeatFrame::cycle() const { return m_cycle; } QRect KisRepeatFrame::affectedRect() const { - return m_cycle->affectedRect(); + KisKeyframeSP keyframe = m_cycle->firstSourceKeyframe(); + + QRect rect; + while (!keyframe.isNull() && keyframe->time() <= m_cycle->lastSourceKeyframe()->time()) { + rect |= keyframe->affectedRect(); + keyframe = channel()->nextKeyframe(keyframe); + } + + return rect; } int KisRepeatFrame::getOriginalTimeFor(int time) const { KisTimeSpan originalRange = m_cycle->originalRange(); int timeWithinCycle = (time - this->time()) % originalRange.duration(); return m_cycle->firstSourceKeyframe()->time() + timeWithinCycle; } KisKeyframeSP KisRepeatFrame::getOriginalKeyframeFor(int time) const { return channel()->activeKeyframeAt(getOriginalTimeFor(time)); } int KisRepeatFrame::firstInstanceOf(int originalTime) const { KisTimeSpan originalRange = m_cycle->originalRange(); const int timeWithinCycle = originalTime - originalRange.start(); - const int first = time() + timeWithinCycle; + const int first = this->time() + timeWithinCycle; - const int endTime = end(); - return (endTime == -1 || first <= endTime) ? first : -1; + const KisKeyframeSP next = channel()->nextKeyframe(*this); + if (next && next->time() < first) return -1; + + return first; } int KisRepeatFrame::previousVisibleFrame(int time) const { if (time <= this->time()) return -1; const int earlierOriginalTime = getOriginalTimeFor(time - 1); int originalStart, originalEnd; channel()->activeKeyframeRange(earlierOriginalTime, &originalStart, &originalEnd); if (originalEnd == -1) return -1; const int durationOfOriginalKeyframe = originalEnd + 1 - originalStart; return time - durationOfOriginalKeyframe; } int KisRepeatFrame::nextVisibleFrame(int time) const { const int originalTime = getOriginalTimeFor(time); int originalStart, originalEnd; channel()->activeKeyframeRange(originalTime, &originalStart, &originalEnd); if (originalEnd == -1) return -1; const int durationOfOriginalKeyframe = originalEnd + 1 - originalStart; const int nextFrameTime = time + durationOfOriginalKeyframe; const int endTime = end(); return (endTime == -1 || nextFrameTime < endTime) ? nextFrameTime : -1; } int KisRepeatFrame::end() const { - // TODO: implement - return -1; + const KisKeyframeBaseSP next = channel()->nextItem(*this); + return next ? next->time() - 1 : -1; } diff --git a/libs/image/kis_keyframe_channel.cpp b/libs/image/kis_keyframe_channel.cpp index 4a85b6a2a4..ebded0a0d2 100644 --- a/libs/image/kis_keyframe_channel.cpp +++ b/libs/image/kis_keyframe_channel.cpp @@ -1,1045 +1,1039 @@ /* * Copyright (c) 2015 Jouni Pentikäinen * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_keyframe_channel.h" #include "KoID.h" #include "kis_global.h" #include "kis_node.h" #include "kis_time_range.h" #include "kundo2command.h" #include "kis_keyframe_commands.h" #include "kis_animation_cycle.h" #include const KoID KisKeyframeChannel::Content = KoID("content", ki18n("Content")); const KoID KisKeyframeChannel::Opacity = KoID("opacity", ki18n("Opacity")); const KoID KisKeyframeChannel::TransformArguments = KoID("transform_arguments", ki18n("Transform")); const KoID KisKeyframeChannel::TransformPositionX = KoID("transform_pos_x", ki18n("Position (X)")); const KoID KisKeyframeChannel::TransformPositionY = KoID("transform_pos_y", ki18n("Position (Y)")); const KoID KisKeyframeChannel::TransformScaleX = KoID("transform_scale_x", ki18n("Scale (X)")); const KoID KisKeyframeChannel::TransformScaleY = KoID("transform_scale_y", ki18n("Scale (Y)")); const KoID KisKeyframeChannel::TransformShearX = KoID("transform_shear_x", ki18n("Shear (X)")); const KoID KisKeyframeChannel::TransformShearY = KoID("transform_shear_y", ki18n("Shear (Y)")); const KoID KisKeyframeChannel::TransformRotationX = KoID("transform_rotation_x", ki18n("Rotation (X)")); const KoID KisKeyframeChannel::TransformRotationY = KoID("transform_rotation_y", ki18n("Rotation (Y)")); const KoID KisKeyframeChannel::TransformRotationZ = KoID("transform_rotation_z", ki18n("Rotation (Z)")); /** * Finds the last item the in the map with a key less than the given one. * Returns map.constEnd() if no such key exists. */ template typename QMap::const_iterator findActive(const QMap &map, int maximumKey) { typename QMap::const_iterator i = map.upperBound(maximumKey); if (i == map.constBegin()) return map.constEnd(); return i - 1; } /** * Finds the last item the in the map before the "active" item (see findActive) for the given key. * Returns map.constEnd() if no such key exists. */ template typename QMap::const_iterator findPrevious(const QMap &map, int currentKey) { typename QMap::const_iterator active = findActive(map, currentKey); if (active == map.constEnd()) return map.constEnd(); if (currentKey > active.key()) return active; if (active == map.constBegin()) return map.constEnd(); return active - 1; } /** * Finds the first item the in the map with a key greater than the given one. * Returns map.constEnd() if no such key exists. */ template typename QMap::const_iterator findNext(const QMap &map, int currentKey) { return map.upperBound(currentKey); } struct KisKeyframeChannel::Private { Private() {} Private(const Private &rhs, KisNodeWSP newParentNode) { node = newParentNode; id = rhs.id; defaultBounds = rhs.defaultBounds; haveBrokenFrameTimeBug = rhs.haveBrokenFrameTimeBug; } KeyframesMap keys; QMap> cycles; QMap> repeats; KisNodeWSP node; KoID id; KisDefaultBoundsBaseSP defaultBounds; bool haveBrokenFrameTimeBug = false; void add(KisKeyframeBaseSP item) { auto repeat = item.dynamicCast(); if (repeat) { repeat->cycle()->addRepeat(repeat); repeats.insert(repeat->time(), repeat); } else { auto keyframe = item.dynamicCast(); KIS_ASSERT_RECOVER_RETURN(keyframe); keys.insert(item->time(), keyframe); } } void remove(KisKeyframeBaseSP item) { auto repeat = item.dynamicCast(); if (repeat) { repeat->cycle()->removeRepeat(repeat); repeats.remove(repeat->time()); } else { keys.remove(item->time()); } } void moveKeyframe(KisKeyframeBaseSP keyframe, int newTime) { const QSharedPointer cycle = cycles.value(keyframe->time()); remove(keyframe); keyframe->setTime(newTime); add(keyframe); if (cycle) { KIS_SAFE_ASSERT_RECOVER_NOOP(newTime < cycle->lastSourceKeyframe()->time()); // TODO: make sure this is the case cycles.remove(cycle->time()); cycle->setTime(newTime); cycles.insert(newTime, cycle); } } void addCycle(QSharedPointer cycle) { cycles.insert(cycle->time(), cycle); } void removeCycle(QSharedPointer cycle) { KIS_SAFE_ASSERT_RECOVER_NOOP(cycle->repeats().isEmpty()); cycles.remove(cycle->time()); } }; KisKeyframeChannel::KisKeyframeChannel(const KoID &id, KisDefaultBoundsBaseSP defaultBounds) : m_d(new Private) { m_d->id = id; m_d->node = 0; m_d->defaultBounds = defaultBounds; } KisKeyframeChannel::KisKeyframeChannel(const KisKeyframeChannel &rhs, KisNode *newParentNode) : m_d(new Private(*rhs.m_d, newParentNode)) { KIS_ASSERT_RECOVER_NOOP(&rhs != this); Q_FOREACH(KisKeyframeSP keyframe, rhs.m_d->keys) { const KisKeyframeSP clone = keyframe->cloneFor(this); m_d->add(clone); } Q_FOREACH(const QSharedPointer rhsCycle, rhs.m_d->cycles) { KisKeyframeSP firstFrame = keyframeAt(rhsCycle->firstSourceKeyframe()->time()); KisKeyframeSP lastFrame = keyframeAt(rhsCycle->lastSourceKeyframe()->time()); QSharedPointer cycle = toQShared(new KisAnimationCycle(this, firstFrame, lastFrame)); m_d->addCycle(cycle); Q_FOREACH(auto rhsRepeatWP, rhsCycle->repeats()) { const QSharedPointer rhsRepeat = rhsRepeatWP.toStrongRef(); if (rhsRepeat) { m_d->add(toQShared(new KisRepeatFrame(this, rhsRepeat->time(), cycle))); } } } } KisKeyframeChannel::~KisKeyframeChannel() {} QString KisKeyframeChannel::id() const { return m_d->id.id(); } QString KisKeyframeChannel::name() const { return m_d->id.name(); } void KisKeyframeChannel::setNode(KisNodeWSP node) { m_d->node = node; } KisNodeWSP KisKeyframeChannel::node() const { return m_d->node; } int KisKeyframeChannel::keyframeCount() const { return m_d->keys.count(); } KisKeyframeChannel::KeyframesMap& KisKeyframeChannel::keys() { return m_d->keys; } const KisKeyframeChannel::KeyframesMap& KisKeyframeChannel::constKeys() const { return m_d->keys; } #define LAZY_INITIALIZE_PARENT_COMMAND(cmd) \ QScopedPointer __tempCommand; \ if (!parentCommand) { \ __tempCommand.reset(new KUndo2Command()); \ cmd = __tempCommand.data(); \ } KisKeyframeSP KisKeyframeChannel::addKeyframe(int time, KUndo2Command *parentCommand) { LAZY_INITIALIZE_PARENT_COMMAND(parentCommand); return insertKeyframe(time, KisKeyframeSP(), parentCommand); } -KisKeyframeSP KisKeyframeChannel::copyKeyframe(const KisKeyframeSP keyframe, int newTime, KUndo2Command *parentCommand) -{ - LAZY_INITIALIZE_PARENT_COMMAND(parentCommand); - return insertKeyframe(newTime, keyframe, parentCommand); -} - KisKeyframeBaseSP KisKeyframeChannel::copyItem(const KisKeyframeBaseSP item, int newTime, KUndo2Command *parentCommand) { LAZY_INITIALIZE_PARENT_COMMAND(parentCommand); return insertKeyframe(newTime, item, parentCommand); } KisKeyframeSP KisKeyframeChannel::copyAsKeyframe(const KisKeyframeBaseSP item, int originalTime, int newTime, KUndo2Command *parentCommand) { LAZY_INITIALIZE_PARENT_COMMAND(parentCommand); KisKeyframeSP keyframe = item->getOriginalKeyframeFor(originalTime); return insertKeyframe(newTime, keyframe, parentCommand); } KisKeyframeSP KisKeyframeChannel::linkKeyframe(const KisKeyframeBaseSP, int, KUndo2Command*) { return KisKeyframeSP(); } KisKeyframeSP KisKeyframeChannel::insertKeyframe(int time, const KisKeyframeBaseSP copySrc, KUndo2Command *parentCommand) { KisKeyframeBaseSP oldItem = itemAt(time); if (oldItem) { deleteKeyframeImpl(oldItem, parentCommand, false); } Q_ASSERT(parentCommand); KisKeyframeSP sourceKeyframe = copySrc ? copySrc->getOriginalKeyframeFor(copySrc->time()) : KisKeyframeSP(); KisKeyframeSP keyframe = createKeyframe(time, sourceKeyframe, parentCommand); KUndo2Command *cmd = new KisReplaceKeyframeCommand(this, keyframe->time(), keyframe, parentCommand); cmd->redo(); return keyframe; } bool KisKeyframeChannel::deleteKeyframe(KisKeyframeBaseSP keyframe, KUndo2Command *parentCommand) { return deleteKeyframeImpl(keyframe, parentCommand, true); } bool KisKeyframeChannel::moveKeyframe(KisKeyframeBaseSP keyframe, int newTime, KUndo2Command *parentCommand) { LAZY_INITIALIZE_PARENT_COMMAND(parentCommand); if (newTime == keyframe->time()) return false; KisKeyframeBaseSP other = itemAt(newTime); if (other) { deleteKeyframeImpl(other, parentCommand, false); } const int srcTime = keyframe->time(); KUndo2Command *cmd = new KisMoveFrameCommand(this, keyframe, srcTime, newTime, parentCommand); cmd->redo(); if (srcTime == 0) { addKeyframe(srcTime, parentCommand); } return true; } bool KisKeyframeChannel::swapFrames(int lhsTime, int rhsTime, KUndo2Command *parentCommand) { LAZY_INITIALIZE_PARENT_COMMAND(parentCommand); if (lhsTime == rhsTime) return false; KisKeyframeBaseSP lhsFrame = itemAt(lhsTime); KisKeyframeBaseSP rhsFrame = itemAt(rhsTime); if (!lhsFrame && !rhsFrame) return false; if (lhsFrame && !rhsFrame) { moveKeyframe(lhsFrame, rhsTime, parentCommand); } else if (!lhsFrame && rhsFrame) { moveKeyframe(rhsFrame, lhsTime, parentCommand); } else { KUndo2Command *cmd = new KisSwapFramesCommand(this, lhsFrame, rhsFrame, parentCommand); cmd->redo(); } return true; } bool KisKeyframeChannel::deleteKeyframeImpl(KisKeyframeBaseSP item, KUndo2Command *parentCommand, bool recreate) { LAZY_INITIALIZE_PARENT_COMMAND(parentCommand); Q_ASSERT(parentCommand); KUndo2Command *cmd = new KisReplaceKeyframeCommand(this, item->time(), KisKeyframeSP(), parentCommand); cmd->redo(); KisKeyframeSP keyframe = item.dynamicCast(); if (keyframe) { destroyKeyframe(keyframe, parentCommand); if (recreate && keyframe->time() == 0) { addKeyframe(0, parentCommand); } } return true; } void KisKeyframeChannel::moveKeyframeImpl(KisKeyframeBaseSP keyframe, int newTime) { KIS_ASSERT_RECOVER_RETURN(keyframe); KIS_ASSERT_RECOVER_RETURN(!itemAt(newTime)); KisFrameSet rangeSrc = affectedFrames(keyframe->time()); QRect rectSrc = keyframe->affectedRect(); emit sigKeyframeAboutToBeMoved(keyframe, newTime); int oldTime = keyframe->time(); m_d->moveKeyframe(keyframe, newTime); keyframe->setTime(newTime); emit sigKeyframeMoved(keyframe, oldTime); KisFrameSet rangeDst = affectedFrames(keyframe->time()); QRect rectDst = keyframe->affectedRect(); requestUpdate(rangeSrc, rectSrc); requestUpdate(rangeDst, rectDst); } void KisKeyframeChannel::swapKeyframesImpl(KisKeyframeBaseSP lhsKeyframe, KisKeyframeBaseSP rhsKeyframe) { KIS_ASSERT_RECOVER_RETURN(lhsKeyframe); KIS_ASSERT_RECOVER_RETURN(rhsKeyframe); KisFrameSet rangeLhs = affectedFrames(lhsKeyframe->time()); KisFrameSet rangeRhs = affectedFrames(rhsKeyframe->time()); const QRect rectLhsSrc = lhsKeyframe->affectedRect(); const QRect rectRhsSrc = rhsKeyframe->affectedRect(); const int lhsTime = lhsKeyframe->time(); const int rhsTime = rhsKeyframe->time(); emit sigKeyframeAboutToBeMoved(lhsKeyframe, rhsTime); emit sigKeyframeAboutToBeMoved(rhsKeyframe, lhsTime); m_d->remove(lhsKeyframe); m_d->remove(rhsKeyframe); rhsKeyframe->setTime(lhsTime); lhsKeyframe->setTime(rhsTime); m_d->add(lhsKeyframe); m_d->add(rhsKeyframe); emit sigKeyframeMoved(lhsKeyframe, lhsTime); emit sigKeyframeMoved(rhsKeyframe, rhsTime); const QRect rectLhsDst = lhsKeyframe->affectedRect(); const QRect rectRhsDst = rhsKeyframe->affectedRect(); requestUpdate(rangeLhs, rectLhsSrc | rectRhsDst); requestUpdate(rangeRhs, rectRhsSrc | rectLhsDst); } KisKeyframeBaseSP KisKeyframeChannel::replaceKeyframeAt(int time, KisKeyframeBaseSP newKeyframe) { Q_ASSERT(newKeyframe.isNull() || time == newKeyframe->time()); KisKeyframeBaseSP existingKeyframe = itemAt(time); if (!existingKeyframe.isNull()) { removeKeyframeLogical(existingKeyframe); } if (!newKeyframe.isNull()) { insertKeyframeLogical(newKeyframe); } return existingKeyframe; } void KisKeyframeChannel::insertKeyframeLogical(KisKeyframeBaseSP keyframe) { const int time = keyframe->time(); emit sigKeyframeAboutToBeAdded(keyframe); m_d->add(keyframe); emit sigKeyframeAdded(keyframe); QRect rect = keyframe->affectedRect(); KisFrameSet range = affectedFrames(time); requestUpdate(range, rect); } void KisKeyframeChannel::removeKeyframeLogical(KisKeyframeBaseSP keyframe) { QRect rect = keyframe->affectedRect(); KisFrameSet range = affectedFrames(keyframe->time()); emit sigKeyframeAboutToBeRemoved(keyframe); m_d->remove(keyframe); emit sigKeyframeRemoved(keyframe); requestUpdate(range, rect); } KisKeyframeSP KisKeyframeChannel::keyframeAt(int time) const { return m_d->keys.value(time); } KisKeyframeSP KisKeyframeChannel::activeKeyframeAt(int time) const { KeyframesMap::const_iterator i = findActive(m_d->keys, time); if (i != m_d->keys.constEnd()) { return i.value(); } return KisKeyframeSP(); } KisKeyframeSP KisKeyframeChannel::visibleKeyframeAt(int time) const { const QSharedPointer repeat = activeRepeatAt(time); return repeat ? repeat->getOriginalKeyframeFor(time) : activeKeyframeAt(time); } KisKeyframeSP KisKeyframeChannel::currentlyActiveKeyframe() const { return activeKeyframeAt(currentTime()); } KisKeyframeSP KisKeyframeChannel::firstKeyframe() const { if (m_d->keys.isEmpty()) return KisKeyframeSP(); return m_d->keys.first(); } KisKeyframeSP KisKeyframeChannel::nextKeyframe(KisKeyframeSP keyframe) const { return nextKeyframe(*keyframe); } KisKeyframeSP KisKeyframeChannel::nextKeyframe(const KisKeyframeBase &keyframe) const { KeyframesMap::const_iterator i = findNext(m_d->keys, keyframe.time()); if (i == m_d->keys.constEnd()) return KisKeyframeSP(0); return i.value(); } KisKeyframeSP KisKeyframeChannel::previousKeyframe(KisKeyframeSP keyframe) const { return previousKeyframe(*keyframe); } KisKeyframeSP KisKeyframeChannel::previousKeyframe(const KisKeyframeBase &keyframe) const { KeyframesMap::const_iterator i = findPrevious(m_d->keys, keyframe.time()); if (i == m_d->keys.constEnd()) return KisKeyframeSP(0); return i.value(); } KisKeyframeSP KisKeyframeChannel::lastKeyframe() const { if (m_d->keys.isEmpty()) return KisKeyframeSP(0); return (m_d->keys.end()-1).value(); } KisKeyframeBaseSP KisKeyframeChannel::itemAt(int time) const { const KisKeyframeSP keyframe = keyframeAt(time); if (keyframe) return keyframe; const QSharedPointer repeat = activeRepeatAt(time); if (repeat && time == repeat->time()) return repeat; return KisKeyframeBaseSP(); } KisKeyframeBaseSP KisKeyframeChannel::activeItemAt(int time) const { const KisKeyframeSP keyframe = activeKeyframeAt(time); if (keyframe) return keyframe; return activeRepeatAt(time); } KisKeyframeBaseSP KisKeyframeChannel::nextItem(const KisKeyframeBase &item) const { const KisKeyframeSP keyframe = nextKeyframe(item); auto repeatIter = findNext(m_d->repeats, item.time()); const auto repeat = (repeatIter != m_d->repeats.constEnd()) ? repeatIter.value() : QSharedPointer(); if (keyframe && (!repeat || repeat->time() > keyframe->time())) return keyframe; return repeat; } KisKeyframeBaseSP KisKeyframeChannel::previousItem(const KisKeyframeBase &item) const { const KisKeyframeSP keyframe = previousKeyframe(item); auto repeatIter = findPrevious(m_d->repeats, item.time()); const auto repeat = (repeatIter != m_d->repeats.constEnd()) ? repeatIter.value() : QSharedPointer(); if (keyframe && (!repeat || repeat->time() > keyframe->time())) return keyframe; return repeat; } KisVisibleKeyframeIterator KisKeyframeChannel::visibleKeyframesFrom(int time) const { return KisVisibleKeyframeIterator(visibleKeyframeAt(time)); } KisTimeSpan KisKeyframeChannel::cycledRangeAt(int time) const { QSharedPointer repeat = activeRepeatAt(time); if (repeat) return repeat->cycle()->originalRange(); QSharedPointer cycle = cycleAt(time); if (cycle) return cycle->originalRange(); return KisTimeSpan(); } QSharedPointer KisKeyframeChannel::cycleAt(int time) const { if (m_d->cycles.isEmpty()) return QSharedPointer(); const auto it = findActive(m_d->cycles, time); if (it == m_d->cycles.constEnd()) return QSharedPointer(); const KisKeyframeBaseSP next = nextItem(*it.value()->lastSourceKeyframe()); if (next && next->time() <= time) return QSharedPointer(); return it.value(); }; QSharedPointer KisKeyframeChannel::activeRepeatAt(int time) const { const auto repeat = findActive(m_d->repeats, time); if (repeat == m_d->repeats.constEnd()) return QSharedPointer(); const KisKeyframeSP lastKeyframe = activeKeyframeAt(time); if (lastKeyframe && lastKeyframe->time() > repeat.value()->time()) return QSharedPointer(); return repeat.value(); } void KisKeyframeChannel::activeKeyframeRange(int time, int *first, int *last) const { *first = *last = -1; const KisKeyframeSP currentKeyframe = activeKeyframeAt(time); if (currentKeyframe.isNull()) return; *first = currentKeyframe->time(); const KisKeyframeSP next = nextKeyframe(currentKeyframe); if (!next.isNull()) { *last = next->time() - 1; } } int KisKeyframeChannel::framesHash() const { KeyframesMap::const_iterator it = m_d->keys.constBegin(); KeyframesMap::const_iterator end = m_d->keys.constEnd(); int hash = 0; while (it != end) { hash += it.key(); ++it; } return hash; } QSet KisKeyframeChannel::allKeyframeIds() const { QSet frames; KeyframesMap::const_iterator it = m_d->keys.constBegin(); KeyframesMap::const_iterator end = m_d->keys.constEnd(); while (it != end) { frames.insert(it.key()); ++it; } return frames; } KisFrameSet KisKeyframeChannel::affectedFrames(int time) const { if (m_d->keys.isEmpty()) return KisFrameSet::infiniteFrom(0); KeyframesMap::const_iterator active = findActive(m_d->keys, time); KeyframesMap::const_iterator next; int from; if (active == m_d->keys.constEnd()) { // No active keyframe, ie. time is before the first keyframe from = 0; next = m_d->keys.constBegin(); } else { from = active.key(); next = active + 1; } KisFrameSet frames; QSharedPointer repeat = activeRepeatAt(time); if (repeat) { const KisKeyframeSP original = repeat->getOriginalKeyframeFor(time); return affectedFrames(original->time()); } QSharedPointer cycle = cycleAt(time); if (cycle) { KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(active != m_d->keys.constEnd() && active.value(), KisFrameSet()); frames = cycle->instancesWithin(active.value(), KisTimeSpan()); } if (next == m_d->keys.constEnd()) { frames |= KisFrameSet::infiniteFrom(from); } else { frames |= KisFrameSet::between(from, next.key() - 1); } return frames; } KisFrameSet KisKeyframeChannel::identicalFrames(int time, const KisTimeSpan range) const { const QSharedPointer repeat = activeRepeatAt(time); if (repeat) { const KisKeyframeSP original = repeat->getOriginalKeyframeFor(time); return identicalFrames(original->time(), range); } KeyframesMap::const_iterator active = findActive(m_d->keys, time); const QSharedPointer cycle = cycleAt(time); if (cycle) { return cycle->instancesWithin(active.value(), range); } if (active != m_d->keys.constEnd() && (active+1) != m_d->keys.constEnd()) { if (active->data()->interpolationMode() != KisKeyframe::Constant) { return KisFrameSet::between(time, time); } } return affectedFrames(time); } bool KisKeyframeChannel::areFramesIdentical(int time1, int time2) const { const KisFrameSet identical = identicalFrames(time1, KisTimeSpan(time2, time2)); return identical.contains(time2); } bool KisKeyframeChannel::isFrameAffectedBy(int targetFrame, int changedFrame) const { const KisFrameSet affected = affectedFrames(changedFrame); return affected.contains(targetFrame); } QDomElement KisKeyframeChannel::toXML(QDomDocument doc, const QString &layerFilename) { QDomElement channelElement = doc.createElement("channel"); channelElement.setAttribute("name", id()); Q_FOREACH (const QSharedPointer cycle, m_d->cycles.values()) { QDomElement cycleElement = doc.createElement("cycle"); cycleElement.setAttribute("firstKeyframe", cycle->firstSourceKeyframe()->time()); cycleElement.setAttribute("lastKeyframe", cycle->lastSourceKeyframe()->time()); channelElement.appendChild(cycleElement); } Q_FOREACH (KisKeyframeSP keyframe, m_d->keys.values()) { QDomElement keyframeElement = doc.createElement("keyframe"); keyframeElement.setAttribute("time", keyframe->time()); keyframeElement.setAttribute("color-label", keyframe->colorLabel()); KisRepeatFrame *repeat = dynamic_cast(keyframe.data()); if (repeat) { keyframeElement.setAttribute("type", "repeat"); const int sourceTime = repeat->cycle()->firstSourceKeyframe()->time(); keyframeElement.setAttribute("cycle", sourceTime); } else { saveKeyframe(keyframe, keyframeElement, layerFilename); } channelElement.appendChild(keyframeElement); } return channelElement; } void KisKeyframeChannel::loadXML(const QDomElement &channelNode) { QMap> cyclesByFirstKeyframe; QMap> cyclesByLastKeyframe; for (QDomElement childNode = channelNode.firstChildElement(); !childNode.isNull(); childNode = childNode.nextSiblingElement()) { const QString nodeName = childNode.nodeName().toUpper(); if (nodeName == "CYCLE") { const int firstKeyframeTime = childNode.attribute("firstKeyframe", "-1").toInt(); const int lastKeyframeTime = childNode.attribute("lastKeyframe", "-1").toInt(); if (firstKeyframeTime >= 0 && lastKeyframeTime > firstKeyframeTime) { QSharedPointer cycle = toQShared(new KisAnimationCycle(this, KisKeyframeSP(), KisKeyframeSP())); cyclesByFirstKeyframe.insert(firstKeyframeTime, cycle); cyclesByLastKeyframe.insert(lastKeyframeTime, cycle); } } } for (QDomElement childNode = channelNode.firstChildElement(); !childNode.isNull(); childNode = childNode.nextSiblingElement()) { const QString nodeName = childNode.nodeName().toUpper(); if (nodeName == "KEYFRAME") { const QString keyframeType = childNode.attribute("type", "").toUpper(); KisKeyframeSP keyframe; if (keyframeType == "REPEAT") { // FIXME //keyframe = loadRepeatFrame(childNode, cyclesByFirstKeyframe); } else { keyframe = loadKeyframe(childNode); } KIS_SAFE_ASSERT_RECOVER(keyframe) { continue; } if (childNode.hasAttribute("color-label")) { keyframe->setColorLabel(childNode.attribute("color-label").toUInt()); } m_d->add(keyframe); const int time = keyframe->time(); if (cyclesByFirstKeyframe.contains(time)) { //cyclesByFirstKeyframe[time]->setFirstKeyframe(keyframe); addCycle(cyclesByFirstKeyframe[time]); } if (cyclesByLastKeyframe.contains(time)) { //cyclesByLastKeyframe[time]->setLastKeyframe(keyframe); } } } } bool KisKeyframeChannel::swapExternalKeyframe(KisKeyframeChannel *srcChannel, int srcTime, int dstTime, KUndo2Command *parentCommand) { if (srcChannel->id() != id()) { warnKrita << "Cannot copy frames from different ids:" << ppVar(srcChannel->id()) << ppVar(id()); return KisKeyframeSP(); } LAZY_INITIALIZE_PARENT_COMMAND(parentCommand); KisKeyframeSP srcFrame = srcChannel->keyframeAt(srcTime); KisKeyframeSP dstFrame = keyframeAt(dstTime); if (!dstFrame && srcFrame) { copyExternalKeyframe(srcChannel, srcTime, dstTime, parentCommand); srcChannel->deleteKeyframe(srcFrame, parentCommand); } else if (dstFrame && !srcFrame) { srcChannel->copyExternalKeyframe(this, dstTime, srcTime, parentCommand); deleteKeyframe(dstFrame, parentCommand); } else if (dstFrame && srcFrame) { const int fakeFrameTime = -1; KisKeyframeSP newKeyframe = createKeyframe(fakeFrameTime, KisKeyframeSP(), parentCommand); uploadExternalKeyframe(srcChannel, srcTime, newKeyframe); srcChannel->copyExternalKeyframe(this, dstTime, srcTime, parentCommand); // do not recreate frame! deleteKeyframeImpl(dstFrame, parentCommand, false); newKeyframe->setTime(dstTime); KUndo2Command *cmd = new KisReplaceKeyframeCommand(this, newKeyframe->time(), newKeyframe, parentCommand); cmd->redo(); } return true; } KisKeyframeSP KisKeyframeChannel::copyExternalKeyframe(KisKeyframeChannel *srcChannel, int srcTime, int dstTime, KUndo2Command *parentCommand) { if (srcChannel->id() != id()) { warnKrita << "Cannot copy frames from different ids:" << ppVar(srcChannel->id()) << ppVar(id()); return KisKeyframeSP(); } LAZY_INITIALIZE_PARENT_COMMAND(parentCommand); KisKeyframeSP dstFrame = keyframeAt(dstTime); if (dstFrame) { deleteKeyframeImpl(dstFrame, parentCommand, false); } KisKeyframeSP newKeyframe = createKeyframe(dstTime, KisKeyframeSP(), parentCommand); uploadExternalKeyframe(srcChannel, srcTime, newKeyframe); KUndo2Command *cmd = new KisReplaceKeyframeCommand(this, newKeyframe->time(), newKeyframe, parentCommand); cmd->redo(); return newKeyframe; } KisDefineCycleCommand * KisKeyframeChannel::createCycle(KisKeyframeSP firstKeyframe, KisKeyframeSP lastKeyframe, KUndo2Command *parentCommand) { const QSharedPointer cycle = toQShared(new KisAnimationCycle(this, firstKeyframe, lastKeyframe)); return new KisDefineCycleCommand(this, cycle, false, parentCommand); } void KisKeyframeChannel::addCycle(QSharedPointer cycle) { m_d->addCycle(cycle); } KUndo2Command* KisKeyframeChannel::deleteCycle(QSharedPointer cycle, KUndo2Command *parentCommand) { KisDefineCycleCommand *defineCycleCommand = new KisDefineCycleCommand(this, cycle, true, parentCommand); // Remove repeats of the cycle Q_FOREACH(QWeakPointer repeatWP, cycle->repeats()) { auto repeat = repeatWP.toStrongRef(); if (repeat) { deleteKeyframe(repeat); } } return defineCycleCommand; } void KisKeyframeChannel::removeCycle(QSharedPointer cycle) { m_d->removeCycle(cycle); } QSharedPointer KisKeyframeChannel::addRepeat(QSharedPointer cycle, int time, KUndo2Command *parentCommand) { const QSharedPointer repeatFrame = toQShared(new KisRepeatFrame(this, time, cycle)); KUndo2Command *cmd = new KisReplaceKeyframeCommand(this, time, repeatFrame, parentCommand); cmd->redo(); return repeatFrame; } void KisKeyframeChannel::requestUpdate(const KisFrameSet &range, const QRect &rect) { if (m_d->node) { m_d->node->invalidateFrames(range, rect); int currentTime = m_d->defaultBounds->currentTime(); if (range.contains(currentTime)) { m_d->node->setDirty(rect); } } } void KisKeyframeChannel::workaroundBrokenFrameTimeBug(int *time) { /** * Between Krita 4.1 and 4.4 Krita had a bug which resulted in creating frames * with negative time stamp. The bug has been fixed, but there might be some files * still in the wild. * * TODO: remove this workaround in Krita 5.0, when no such file are left :) */ if (*time < 0) { qWarning() << "WARNING: Loading a file with negative animation frames!"; qWarning() << " The file has been saved with a buggy version of Krita."; qWarning() << " All the frames with negative ids will be dropped!"; qWarning() << " " << ppVar(this->id()) << ppVar(*time); m_d->haveBrokenFrameTimeBug = true; *time = 0; } if (m_d->haveBrokenFrameTimeBug) { while (keyframeAt(*time)) { (*time)++; } } } int KisKeyframeChannel::currentTime() const { return m_d->defaultBounds->currentTime(); } qreal KisKeyframeChannel::minScalarValue() const { return 0; } qreal KisKeyframeChannel::maxScalarValue() const { return 0; } qreal KisKeyframeChannel::scalarValue(const KisKeyframeSP keyframe) const { Q_UNUSED(keyframe); return 0; } void KisKeyframeChannel::setScalarValue(KisKeyframeSP keyframe, qreal value, KUndo2Command *parentCommand) { Q_UNUSED(keyframe); Q_UNUSED(value); Q_UNUSED(parentCommand); } KisVisibleKeyframeIterator::KisVisibleKeyframeIterator() = default; KisVisibleKeyframeIterator::KisVisibleKeyframeIterator(KisKeyframeSP keyframe) : m_channel(keyframe->channel()) , m_keyframe(keyframe) , m_time(keyframe->time()) {} KisVisibleKeyframeIterator& KisVisibleKeyframeIterator::operator--() { const QSharedPointer repeat = m_channel->activeRepeatAt(m_time); if (repeat) { const int time = repeat->previousVisibleFrame(m_time); if (time >= 0) { m_time = time; return *this; } } m_keyframe = m_channel->previousKeyframe(*m_keyframe); if (!m_keyframe) return invalidate(); m_time = m_keyframe->time(); return *this; } KisVisibleKeyframeIterator& KisVisibleKeyframeIterator::operator++() { const QSharedPointer repeat = m_channel->activeRepeatAt(m_time); if (repeat) { const int time = repeat->nextVisibleFrame(m_time); if (time >= 0) { m_time = time; return *this; } } m_keyframe = m_channel->nextKeyframe(*m_keyframe); if (!m_keyframe) return invalidate(); m_time = m_keyframe->time(); return *this; }; KisKeyframeSP KisVisibleKeyframeIterator::operator*() const { const KisRepeatFrame *repeat = dynamic_cast(m_keyframe.data()); if (repeat) { return repeat->getOriginalKeyframeFor(m_time); } return m_keyframe; } KisKeyframeSP KisVisibleKeyframeIterator::operator->() const { return operator*(); } bool KisVisibleKeyframeIterator::isValid() const { return m_channel && m_time >= 0; } KisVisibleKeyframeIterator& KisVisibleKeyframeIterator::invalidate() { m_channel = nullptr; m_keyframe = KisKeyframeSP(); m_time = -1; return *this; } diff --git a/libs/image/kis_keyframe_channel.h b/libs/image/kis_keyframe_channel.h index c8412c8cb3..dd4f2d900e 100644 --- a/libs/image/kis_keyframe_channel.h +++ b/libs/image/kis_keyframe_channel.h @@ -1,236 +1,235 @@ /* * Copyright (c) 2015 Jouni Pentikäinen * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KIS_KEYFRAME_CHANNEL_H #define KIS_KEYFRAME_CHANNEL_H #include #include #include #include "kis_types.h" #include "KoID.h" #include "kritaimage_export.h" #include "kis_keyframe.h" #include "kis_default_bounds.h" class KisFrameSet; class KisTimeSpan; class KisAnimationCycle; class KisRepeatFrame; class KisVisibleKeyframeIterator; class KisDefineCycleCommand; class KRITAIMAGE_EXPORT KisKeyframeChannel : public QObject { Q_OBJECT public: // Standard Keyframe Ids static const KoID Content; static const KoID Opacity; static const KoID TransformArguments; static const KoID TransformPositionX; static const KoID TransformPositionY; static const KoID TransformScaleX; static const KoID TransformScaleY; static const KoID TransformShearX; static const KoID TransformShearY; static const KoID TransformRotationX; static const KoID TransformRotationY; static const KoID TransformRotationZ; public: KisKeyframeChannel(const KoID& id, KisDefaultBoundsBaseSP defaultBounds); KisKeyframeChannel(const KisKeyframeChannel &rhs, KisNode *newParentNode); ~KisKeyframeChannel() override; QString id() const; QString name() const; void setNode(KisNodeWSP node); KisNodeWSP node() const; KisKeyframeSP addKeyframe(int time, KUndo2Command *parentCommand = 0); bool deleteKeyframe(KisKeyframeBaseSP keyframe, KUndo2Command *parentCommand = 0); bool moveKeyframe(KisKeyframeBaseSP keyframe, int newTime, KUndo2Command *parentCommand = 0); bool swapFrames(int lhsTime, int rhsTime, KUndo2Command *parentCommand = 0); KisKeyframeBaseSP copyItem(const KisKeyframeBaseSP item, int newTime, KUndo2Command *parentCommand = 0); - KisKeyframeSP copyKeyframe(const KisKeyframeSP keyframe, int newTime, KUndo2Command *parentCommand = 0); KisKeyframeSP copyAsKeyframe(const KisKeyframeBaseSP item, int originalTime, int newTime, KUndo2Command *parentCommand = 0); virtual KisKeyframeSP linkKeyframe(const KisKeyframeBaseSP keyframe, int newTime, KUndo2Command *parentCommand = 0); KisKeyframeSP copyExternalKeyframe(KisKeyframeChannel *srcChannel, int srcTime, int dstTime, KUndo2Command *parentCommand = 0); KisDefineCycleCommand * createCycle(KisKeyframeSP firstKeyframe, KisKeyframeSP lastKeyframe, KUndo2Command *parentCommand = 0); KUndo2Command * deleteCycle(QSharedPointer cycle, KUndo2Command *parentCommand = 0); QSharedPointer addRepeat(QSharedPointer cycle, int time, KUndo2Command *parentCommand); bool swapExternalKeyframe(KisKeyframeChannel *srcChannel, int srcTime, int dstTime, KUndo2Command *parentCommand = 0); KisKeyframeSP keyframeAt(int time) const; KisKeyframeSP activeKeyframeAt(int time) const; KisKeyframeSP visibleKeyframeAt(int time) const; KisKeyframeSP currentlyActiveKeyframe() const; KisKeyframeSP firstKeyframe() const; KisKeyframeSP nextKeyframe(KisKeyframeSP keyframe) const; KisKeyframeSP previousKeyframe(KisKeyframeSP keyframe) const; KisKeyframeSP nextKeyframe(const KisKeyframeBase &keyframe) const; KisKeyframeSP previousKeyframe(const KisKeyframeBase &keyframe) const; KisKeyframeSP lastKeyframe() const; KisKeyframeBaseSP itemAt(int time) const; KisKeyframeBaseSP activeItemAt(int time) const; KisKeyframeBaseSP nextItem(const KisKeyframeBase &item) const; KisKeyframeBaseSP previousItem(const KisKeyframeBase &item) const; KisVisibleKeyframeIterator visibleKeyframesFrom(int time) const; /** * Finds the original range of the cycle defined or repeated at the given time. * @arg time a time at any frame within the original cycle or any repeat of it. */ KisTimeSpan cycledRangeAt(int time) const; /** * Finds the cycle defined at time, if any. * @arg time a time within the original range of the cycle. */ QSharedPointer cycleAt(int time) const; /** * Finds the repeat of a cycle at the time, if any. */ QSharedPointer activeRepeatAt(int time) const; /** * Finds the span of time of the keyframe active at given time. * If there is no active keyframe, first will be -1. * If the keyframe continues indefinitely, last will be -1. */ void activeKeyframeRange(int time, int *first, int *last) const; /** * Calculates a pseudo-unique keyframes hash. The hash changes * every time any frame is added/removed/moved */ int framesHash() const; QSet allKeyframeIds() const; /** * Get the set of frames affected by any changes to the value * of the active keyframe at the given time. */ virtual KisFrameSet affectedFrames(int time) const; /** * Get a set of frames for which the channel gives identical * results, compared to the given frame. * * Note: this set may be different than the set of affected frames * due to interpolation. */ virtual KisFrameSet identicalFrames(int time, const KisTimeSpan range) const; virtual bool areFramesIdentical(int time1, int time2) const; virtual bool isFrameAffectedBy(int targetFrame, int changedFrame) const; int keyframeCount() const; virtual bool hasScalarValue() const = 0; virtual qreal minScalarValue() const; virtual qreal maxScalarValue() const; virtual qreal scalarValue(const KisKeyframeSP keyframe) const; virtual void setScalarValue(KisKeyframeSP keyframe, qreal value, KUndo2Command *parentCommand = 0); virtual QDomElement toXML(QDomDocument doc, const QString &layerFilename); virtual void loadXML(const QDomElement &channelNode); int currentTime() const; Q_SIGNALS: void sigKeyframeAboutToBeAdded(KisKeyframeBaseSP keyframe); void sigKeyframeAdded(KisKeyframeBaseSP keyframe); void sigKeyframeAboutToBeRemoved(KisKeyframeBaseSP keyframe); void sigKeyframeRemoved(KisKeyframeBaseSP keyframe); void sigKeyframeAboutToBeMoved(KisKeyframeBaseSP keyframe, int toTime); void sigKeyframeMoved(KisKeyframeBaseSP keyframe, int fromTime); void sigKeyframeChanged(KisKeyframeBaseSP keyframe); protected: typedef QMap KeyframesMap; KeyframesMap &keys(); const KeyframesMap &constKeys() const; virtual KisKeyframeSP createKeyframe(int time, const KisKeyframeSP copySrc, KUndo2Command *parentCommand) = 0; virtual void destroyKeyframe(KisKeyframeSP key, KUndo2Command *parentCommand) = 0; virtual void uploadExternalKeyframe(KisKeyframeChannel *srcChannel, int srcTime, KisKeyframeSP dstFrame) = 0; virtual void requestUpdate(const KisFrameSet &range, const QRect &rect); virtual KisKeyframeSP loadKeyframe(const QDomElement &keyframeNode) = 0; virtual void saveKeyframe(KisKeyframeSP keyframe, QDomElement keyframeElement, const QString &layerFilename) = 0; void workaroundBrokenFrameTimeBug(int *time); private: KisKeyframeBaseSP replaceKeyframeAt(int time, KisKeyframeBaseSP newKeyframe); void insertKeyframeLogical(KisKeyframeBaseSP keyframe); void removeKeyframeLogical(KisKeyframeBaseSP keyframe); bool deleteKeyframeImpl(KisKeyframeBaseSP keyframe, KUndo2Command *parentCommand, bool recreate); void moveKeyframeImpl(KisKeyframeBaseSP keyframe, int newTime); void swapKeyframesImpl(KisKeyframeBaseSP lhsKeyframe, KisKeyframeBaseSP rhsKeyframe); void addCycle(QSharedPointer cycle); void removeCycle(QSharedPointer cycle); friend class KisMoveFrameCommand; friend class KisReplaceKeyframeCommand; friend class KisSwapFramesCommand; friend class KisDefineCycleCommand; private: KisKeyframeSP insertKeyframe(int time, const KisKeyframeBaseSP copySrc, KUndo2Command *parentCommand); struct Private; QScopedPointer m_d; }; class KisVisibleKeyframeIterator { public: KisVisibleKeyframeIterator(); explicit KisVisibleKeyframeIterator(KisKeyframeSP keyframe); KisVisibleKeyframeIterator& operator--(); KisVisibleKeyframeIterator& operator++(); bool isValid() const; KisKeyframeSP operator->() const; KisKeyframeSP operator*() const; private: KisVisibleKeyframeIterator &invalidate(); KisKeyframeChannel *m_channel{nullptr}; KisKeyframeSP m_keyframe; int m_time{-1}; }; #endif // KIS_KEYFRAME_CHANNEL_H diff --git a/libs/image/kis_layer_utils.cpp b/libs/image/kis_layer_utils.cpp index 1a4abd7544..bcb2a7a301 100644 --- a/libs/image/kis_layer_utils.cpp +++ b/libs/image/kis_layer_utils.cpp @@ -1,1491 +1,1491 @@ /* * Copyright (c) 2015 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_layer_utils.h" #include #include #include #include #include "kis_painter.h" #include "kis_image.h" #include "kis_node.h" #include "kis_layer.h" #include "kis_paint_layer.h" #include "kis_clone_layer.h" #include "kis_group_layer.h" #include "kis_selection.h" #include "kis_selection_mask.h" #include "kis_meta_data_merge_strategy.h" #include #include "commands/kis_image_layer_add_command.h" #include "commands/kis_image_layer_remove_command.h" #include "commands/kis_image_layer_move_command.h" #include "commands/kis_image_change_layers_command.h" #include "commands_new/kis_activate_selection_mask_command.h" #include "commands/kis_image_change_visibility_command.h" #include "kis_abstract_projection_plane.h" #include "kis_processing_applicator.h" #include "kis_image_animation_interface.h" #include "kis_keyframe_channel.h" #include "kis_command_utils.h" #include "commands_new/kis_change_projection_color_command.h" #include "kis_layer_properties_icons.h" #include "lazybrush/kis_colorize_mask.h" #include "commands/kis_node_property_list_command.h" #include "commands/kis_node_compositeop_command.h" #include #include "krita_utils.h" #include "kis_image_signal_router.h" namespace KisLayerUtils { void fetchSelectionMasks(KisNodeList mergedNodes, QVector &selectionMasks) { foreach (KisNodeSP node, mergedNodes) { KisLayerSP layer = qobject_cast(node.data()); KisSelectionMaskSP mask; if (layer && (mask = layer->selectionMask())) { selectionMasks.append(mask); } } } struct MergeDownInfoBase { MergeDownInfoBase(KisImageSP _image) : image(_image), storage(new SwitchFrameCommand::SharedStorage()) { } virtual ~MergeDownInfoBase() {} KisImageWSP image; QVector selectionMasks; KisNodeSP dstNode; SwitchFrameCommand::SharedStorageSP storage; QSet frames; bool useInTimeline = false; bool enableOnionSkins = false; virtual KisNodeList allSrcNodes() = 0; KisLayerSP dstLayer() { return qobject_cast(dstNode.data()); } }; struct MergeDownInfo : public MergeDownInfoBase { MergeDownInfo(KisImageSP _image, KisLayerSP _prevLayer, KisLayerSP _currLayer) : MergeDownInfoBase(_image), prevLayer(_prevLayer), currLayer(_currLayer) { frames = fetchLayerFramesRecursive(prevLayer) | fetchLayerFramesRecursive(currLayer); useInTimeline = prevLayer->useInTimeline() || currLayer->useInTimeline(); const KisPaintLayer *paintLayer = qobject_cast(currLayer.data()); if (paintLayer) enableOnionSkins |= paintLayer->onionSkinEnabled(); paintLayer = qobject_cast(prevLayer.data()); if (paintLayer) enableOnionSkins |= paintLayer->onionSkinEnabled(); } KisLayerSP prevLayer; KisLayerSP currLayer; KisNodeList allSrcNodes() override { KisNodeList mergedNodes; mergedNodes << currLayer; mergedNodes << prevLayer; return mergedNodes; } }; struct MergeMultipleInfo : public MergeDownInfoBase { MergeMultipleInfo(KisImageSP _image, KisNodeList _mergedNodes) : MergeDownInfoBase(_image), mergedNodes(_mergedNodes) { foreach (KisNodeSP node, mergedNodes) { frames |= fetchLayerFramesRecursive(node); useInTimeline |= node->useInTimeline(); const KisPaintLayer *paintLayer = qobject_cast(node.data()); if (paintLayer) { enableOnionSkins |= paintLayer->onionSkinEnabled(); } } } KisNodeList mergedNodes; bool nodesCompositingVaries = false; KisNodeList allSrcNodes() override { return mergedNodes; } }; typedef QSharedPointer MergeDownInfoBaseSP; typedef QSharedPointer MergeDownInfoSP; typedef QSharedPointer MergeMultipleInfoSP; struct FillSelectionMasks : public KUndo2Command { FillSelectionMasks(MergeDownInfoBaseSP info) : m_info(info) {} void redo() override { fetchSelectionMasks(m_info->allSrcNodes(), m_info->selectionMasks); } private: MergeDownInfoBaseSP m_info; }; struct DisableColorizeKeyStrokes : public KisCommandUtils::AggregateCommand { DisableColorizeKeyStrokes(MergeDownInfoBaseSP info) : m_info(info) {} void populateChildCommands() override { Q_FOREACH (KisNodeSP node, m_info->allSrcNodes()) { recursiveApplyNodes(node, [this] (KisNodeSP node) { if (dynamic_cast(node.data()) && KisLayerPropertiesIcons::nodeProperty(node, KisLayerPropertiesIcons::colorizeEditKeyStrokes, true).toBool()) { KisBaseNode::PropertyList props = node->sectionModelProperties(); KisLayerPropertiesIcons::setNodeProperty(&props, KisLayerPropertiesIcons::colorizeEditKeyStrokes, false); addCommand(new KisNodePropertyListCommand(node, props)); } }); } } private: MergeDownInfoBaseSP m_info; }; struct DisableOnionSkins : public KisCommandUtils::AggregateCommand { DisableOnionSkins(MergeDownInfoBaseSP info) : m_info(info) {} void populateChildCommands() override { Q_FOREACH (KisNodeSP node, m_info->allSrcNodes()) { recursiveApplyNodes(node, [this] (KisNodeSP node) { if (KisLayerPropertiesIcons::nodeProperty(node, KisLayerPropertiesIcons::onionSkins, false).toBool()) { KisBaseNode::PropertyList props = node->sectionModelProperties(); KisLayerPropertiesIcons::setNodeProperty(&props, KisLayerPropertiesIcons::onionSkins, false); addCommand(new KisNodePropertyListCommand(node, props)); } }); } } private: MergeDownInfoBaseSP m_info; }; struct DisableExtraCompositing : public KisCommandUtils::AggregateCommand { DisableExtraCompositing(MergeMultipleInfoSP info) : m_info(info) {} void populateChildCommands() override { /** * We disable extra compositing only in case all the layers have * the same compositing properties, therefore, we can just sum them using * Normal blend mode */ if (m_info->nodesCompositingVaries) return; // we should disable dirty requests on **redo only**, otherwise // the state of the layers will not be recovered on undo m_info->image->disableDirtyRequests(); Q_FOREACH (KisNodeSP node, m_info->allSrcNodes()) { if (node->compositeOpId() != COMPOSITE_OVER) { addCommand(new KisNodeCompositeOpCommand(node, node->compositeOpId(), COMPOSITE_OVER)); } if (KisLayerPropertiesIcons::nodeProperty(node, KisLayerPropertiesIcons::inheritAlpha, false).toBool()) { KisBaseNode::PropertyList props = node->sectionModelProperties(); KisLayerPropertiesIcons::setNodeProperty(&props, KisLayerPropertiesIcons::inheritAlpha, false); addCommand(new KisNodePropertyListCommand(node, props)); } } m_info->image->enableDirtyRequests(); } private: MergeMultipleInfoSP m_info; }; struct DisablePassThroughForHeadsOnly : public KisCommandUtils::AggregateCommand { DisablePassThroughForHeadsOnly(MergeDownInfoBaseSP info, bool skipIfDstIsGroup = false) : m_info(info), m_skipIfDstIsGroup(skipIfDstIsGroup) { } void populateChildCommands() override { if (m_skipIfDstIsGroup && m_info->dstLayer() && m_info->dstLayer()->inherits("KisGroupLayer")) { return; } Q_FOREACH (KisNodeSP node, m_info->allSrcNodes()) { if (KisLayerPropertiesIcons::nodeProperty(node, KisLayerPropertiesIcons::passThrough, false).toBool()) { KisBaseNode::PropertyList props = node->sectionModelProperties(); KisLayerPropertiesIcons::setNodeProperty(&props, KisLayerPropertiesIcons::passThrough, false); addCommand(new KisNodePropertyListCommand(node, props)); } } } private: MergeDownInfoBaseSP m_info; bool m_skipIfDstIsGroup; }; struct RefreshHiddenAreas : public KUndo2Command { RefreshHiddenAreas(MergeDownInfoBaseSP info) : m_info(info) {} void redo() override { KisImageAnimationInterface *interface = m_info->image->animationInterface(); const QRect preparedRect = !interface->externalFrameActive() ? m_info->image->bounds() : QRect(); foreach (KisNodeSP node, m_info->allSrcNodes()) { refreshHiddenAreaAsync(node, preparedRect); } } private: QRect realNodeExactBounds(KisNodeSP rootNode, QRect currentRect = QRect()) { KisNodeSP node = rootNode->firstChild(); while(node) { currentRect |= realNodeExactBounds(node, currentRect); node = node->nextSibling(); } // TODO: it would be better to count up changeRect inside // node's extent() method currentRect |= rootNode->projectionPlane()->changeRect(rootNode->exactBounds()); return currentRect; } void refreshHiddenAreaAsync(KisNodeSP rootNode, const QRect &preparedArea) { QRect realNodeRect = realNodeExactBounds(rootNode); if (!preparedArea.contains(realNodeRect)) { QRegion dirtyRegion = realNodeRect; dirtyRegion -= preparedArea; foreach(const QRect &rc, dirtyRegion.rects()) { m_info->image->refreshGraphAsync(rootNode, rc, realNodeRect); } } } private: MergeDownInfoBaseSP m_info; }; struct RefreshDelayedUpdateLayers : public KUndo2Command { RefreshDelayedUpdateLayers(MergeDownInfoBaseSP info) : m_info(info) {} void redo() override { foreach (KisNodeSP node, m_info->allSrcNodes()) { forceAllDelayedNodesUpdate(node); } } private: MergeDownInfoBaseSP m_info; }; struct KeepMergedNodesSelected : public KisCommandUtils::AggregateCommand { KeepMergedNodesSelected(MergeDownInfoSP info, bool finalizing) : m_singleInfo(info), m_finalizing(finalizing) {} KeepMergedNodesSelected(MergeMultipleInfoSP info, KisNodeSP putAfter, bool finalizing) : m_multipleInfo(info), m_finalizing(finalizing), m_putAfter(putAfter) {} void populateChildCommands() override { KisNodeSP prevNode; KisNodeSP nextNode; KisNodeList prevSelection; KisNodeList nextSelection; KisImageSP image; if (m_singleInfo) { prevNode = m_singleInfo->currLayer; nextNode = m_singleInfo->dstNode; image = m_singleInfo->image; } else if (m_multipleInfo) { prevNode = m_putAfter; nextNode = m_multipleInfo->dstNode; prevSelection = m_multipleInfo->allSrcNodes(); image = m_multipleInfo->image; } if (!m_finalizing) { addCommand(new KeepNodesSelectedCommand(prevSelection, KisNodeList(), prevNode, KisNodeSP(), image, false)); } else { addCommand(new KeepNodesSelectedCommand(KisNodeList(), nextSelection, KisNodeSP(), nextNode, image, true)); } } private: MergeDownInfoSP m_singleInfo; MergeMultipleInfoSP m_multipleInfo; bool m_finalizing; KisNodeSP m_putAfter; }; struct CreateMergedLayer : public KisCommandUtils::AggregateCommand { CreateMergedLayer(MergeDownInfoSP info) : m_info(info) {} void populateChildCommands() override { // actual merging done by KisLayer::createMergedLayer (or specialized descendant) m_info->dstNode = m_info->currLayer->createMergedLayerTemplate(m_info->prevLayer); if (m_info->frames.size() > 0) { m_info->dstNode->enableAnimation(); m_info->dstNode->getKeyframeChannel(KisKeyframeChannel::Content.id(), true); } m_info->dstNode->setUseInTimeline(m_info->useInTimeline); KisPaintLayer *dstPaintLayer = qobject_cast(m_info->dstNode.data()); if (dstPaintLayer) { dstPaintLayer->setOnionSkinEnabled(m_info->enableOnionSkins); } } private: MergeDownInfoSP m_info; }; struct CreateMergedLayerMultiple : public KisCommandUtils::AggregateCommand { CreateMergedLayerMultiple(MergeMultipleInfoSP info, const QString name = QString() ) : m_info(info), m_name(name) {} void populateChildCommands() override { QString mergedLayerName; if (m_name.isEmpty()){ const QString mergedLayerSuffix = i18n("Merged"); mergedLayerName = m_info->mergedNodes.first()->name(); if (!mergedLayerName.endsWith(mergedLayerSuffix)) { mergedLayerName = QString("%1 %2") .arg(mergedLayerName).arg(mergedLayerSuffix); } } else { mergedLayerName = m_name; } KisPaintLayer *dstPaintLayer = new KisPaintLayer(m_info->image, mergedLayerName, OPACITY_OPAQUE_U8); m_info->dstNode = dstPaintLayer; if (m_info->frames.size() > 0) { m_info->dstNode->enableAnimation(); m_info->dstNode->getKeyframeChannel(KisKeyframeChannel::Content.id(), true); } auto channelFlagsLazy = [](KisNodeSP node) { KisLayer *layer = dynamic_cast(node.data()); return layer ? layer->channelFlags() : QBitArray(); }; QString compositeOpId; QBitArray channelFlags; bool compositionVaries = false; bool isFirstCycle = true; foreach (KisNodeSP node, m_info->allSrcNodes()) { if (isFirstCycle) { compositeOpId = node->compositeOpId(); channelFlags = channelFlagsLazy(node); isFirstCycle = false; } else if (compositeOpId != node->compositeOpId() || channelFlags != channelFlagsLazy(node)) { compositionVaries = true; break; } KisLayerSP layer = qobject_cast(node.data()); if (layer && layer->layerStyle()) { compositionVaries = true; break; } } if (!compositionVaries) { if (!compositeOpId.isEmpty()) { m_info->dstNode->setCompositeOpId(compositeOpId); } if (m_info->dstLayer() && !channelFlags.isEmpty()) { m_info->dstLayer()->setChannelFlags(channelFlags); } } m_info->nodesCompositingVaries = compositionVaries; m_info->dstNode->setUseInTimeline(m_info->useInTimeline); dstPaintLayer->setOnionSkinEnabled(m_info->enableOnionSkins); } private: MergeMultipleInfoSP m_info; QString m_name; }; struct MergeLayers : public KisCommandUtils::AggregateCommand { MergeLayers(MergeDownInfoSP info) : m_info(info) {} void populateChildCommands() override { // actual merging done by KisLayer::createMergedLayer (or specialized descendant) m_info->currLayer->fillMergedLayerTemplate(m_info->dstLayer(), m_info->prevLayer); } private: MergeDownInfoSP m_info; }; struct MergeLayersMultiple : public KisCommandUtils::AggregateCommand { MergeLayersMultiple(MergeMultipleInfoSP info) : m_info(info) {} void populateChildCommands() override { KisPainter gc(m_info->dstNode->paintDevice()); foreach (KisNodeSP node, m_info->allSrcNodes()) { QRect rc = node->exactBounds() | m_info->image->bounds(); node->projectionPlane()->apply(&gc, rc); } } private: MergeMultipleInfoSP m_info; }; struct MergeMetaData : public KUndo2Command { MergeMetaData(MergeDownInfoSP info, const KisMetaData::MergeStrategy* strategy) : m_info(info), m_strategy(strategy) {} void redo() override { QRect layerProjectionExtent = m_info->currLayer->projection()->extent(); QRect prevLayerProjectionExtent = m_info->prevLayer->projection()->extent(); int prevLayerArea = prevLayerProjectionExtent.width() * prevLayerProjectionExtent.height(); int layerArea = layerProjectionExtent.width() * layerProjectionExtent.height(); QList scores; double norm = qMax(prevLayerArea, layerArea); scores.append(prevLayerArea / norm); scores.append(layerArea / norm); QList srcs; srcs.append(m_info->prevLayer->metaData()); srcs.append(m_info->currLayer->metaData()); m_strategy->merge(m_info->dstLayer()->metaData(), srcs, scores); } private: MergeDownInfoSP m_info; const KisMetaData::MergeStrategy *m_strategy; }; KeepNodesSelectedCommand::KeepNodesSelectedCommand(const KisNodeList &selectedBefore, const KisNodeList &selectedAfter, KisNodeSP activeBefore, KisNodeSP activeAfter, KisImageSP image, bool finalize, KUndo2Command *parent) : FlipFlopCommand(finalize, parent), m_selectedBefore(selectedBefore), m_selectedAfter(selectedAfter), m_activeBefore(activeBefore), m_activeAfter(activeAfter), m_image(image) { } void KeepNodesSelectedCommand::end() { KisImageSignalType type; if (isFinalizing()) { type = ComplexNodeReselectionSignal(m_activeAfter, m_selectedAfter); } else { type = ComplexNodeReselectionSignal(m_activeBefore, m_selectedBefore); } m_image->signalRouter()->emitNotification(type); } SelectGlobalSelectionMask::SelectGlobalSelectionMask(KisImageSP image) : m_image(image) { } void SelectGlobalSelectionMask::redo() { KisImageSignalType type = ComplexNodeReselectionSignal(m_image->rootLayer()->selectionMask(), KisNodeList()); m_image->signalRouter()->emitNotification(type); } KisLayerSP constructDefaultLayer(KisImageSP image) { return new KisPaintLayer(image.data(), image->nextLayerName(), OPACITY_OPAQUE_U8, image->colorSpace()); } RemoveNodeHelper::~RemoveNodeHelper() { } /** * The removal of two nodes in one go may be a bit tricky, because one * of them may be the clone of another. If we remove the source of a * clone layer, it will reincarnate into a paint layer. In this case * the pointer to the second layer will be lost. * * That's why we need to care about the order of the nodes removal: * the clone --- first, the source --- last. */ void RemoveNodeHelper::safeRemoveMultipleNodes(KisNodeList nodes, KisImageSP image) { const bool lastLayer = scanForLastLayer(image, nodes); while (!nodes.isEmpty()) { KisNodeList::iterator it = nodes.begin(); while (it != nodes.end()) { if (!checkIsSourceForClone(*it, nodes)) { KisNodeSP node = *it; addCommandImpl(new KisImageLayerRemoveCommand(image, node, false, true)); it = nodes.erase(it); } else { ++it; } } } if (lastLayer) { KisLayerSP newLayer = constructDefaultLayer(image); addCommandImpl(new KisImageLayerAddCommand(image, newLayer, image->root(), KisNodeSP(), false, false)); } } bool RemoveNodeHelper::checkIsSourceForClone(KisNodeSP src, const KisNodeList &nodes) { foreach (KisNodeSP node, nodes) { if (node == src) continue; KisCloneLayer *clone = dynamic_cast(node.data()); if (clone && KisNodeSP(clone->copyFrom()) == src) { return true; } } return false; } bool RemoveNodeHelper::scanForLastLayer(KisImageWSP image, KisNodeList nodesToRemove) { bool removeLayers = false; Q_FOREACH(KisNodeSP nodeToRemove, nodesToRemove) { if (qobject_cast(nodeToRemove.data())) { removeLayers = true; break; } } if (!removeLayers) return false; bool lastLayer = true; KisNodeSP node = image->root()->firstChild(); while (node) { if (!nodesToRemove.contains(node) && qobject_cast(node.data())) { lastLayer = false; break; } node = node->nextSibling(); } return lastLayer; } SimpleRemoveLayers::SimpleRemoveLayers(const KisNodeList &nodes, KisImageSP image) : m_nodes(nodes), m_image(image) { } void SimpleRemoveLayers::populateChildCommands() { if (m_nodes.isEmpty()) return; safeRemoveMultipleNodes(m_nodes, m_image); } void SimpleRemoveLayers::addCommandImpl(KUndo2Command *cmd) { addCommand(cmd); } struct InsertNode : public KisCommandUtils::AggregateCommand { InsertNode(MergeDownInfoBaseSP info, KisNodeSP putAfter) : m_info(info), m_putAfter(putAfter) {} void populateChildCommands() override { addCommand(new KisImageLayerAddCommand(m_info->image, m_info->dstNode, m_putAfter->parent(), m_putAfter, true, false)); } private: virtual void addCommandImpl(KUndo2Command *cmd) { addCommand(cmd); } private: MergeDownInfoBaseSP m_info; KisNodeSP m_putAfter; }; struct CleanUpNodes : private RemoveNodeHelper, public KisCommandUtils::AggregateCommand { CleanUpNodes(MergeDownInfoBaseSP info, KisNodeSP putAfter) : m_info(info), m_putAfter(putAfter) {} static void findPerfectParent(KisNodeList nodesToDelete, KisNodeSP &putAfter, KisNodeSP &parent) { if (!putAfter) { putAfter = nodesToDelete.last(); } // Add the new merged node on top of the active node -- checking // whether the parent is going to be deleted parent = putAfter->parent(); while (parent && nodesToDelete.contains(parent)) { parent = parent->parent(); } } void populateChildCommands() override { KisNodeList nodesToDelete = m_info->allSrcNodes(); KisNodeSP parent; findPerfectParent(nodesToDelete, m_putAfter, parent); if (!parent) { KisNodeSP oldRoot = m_info->image->root(); KisNodeSP newRoot(new KisGroupLayer(m_info->image, "root", OPACITY_OPAQUE_U8)); addCommand(new KisImageLayerAddCommand(m_info->image, m_info->dstNode, newRoot, KisNodeSP(), true, false)); addCommand(new KisImageChangeLayersCommand(m_info->image, oldRoot, newRoot)); } else { if (parent == m_putAfter->parent()) { addCommand(new KisImageLayerAddCommand(m_info->image, m_info->dstNode, parent, m_putAfter, true, false)); } else { addCommand(new KisImageLayerAddCommand(m_info->image, m_info->dstNode, parent, parent->lastChild(), true, false)); } /** * We can merge selection masks, in this case dstLayer is not defined! */ if (m_info->dstLayer()) { reparentSelectionMasks(m_info->image, m_info->dstLayer(), m_info->selectionMasks); } KisNodeList safeNodesToDelete = m_info->allSrcNodes(); for (KisNodeList::iterator it = safeNodesToDelete.begin(); it != safeNodesToDelete.end(); ++it) { KisNodeSP node = *it; if (node->userLocked() && node->visible()) { addCommand(new KisImageChangeVisibilityCommand(false, node)); } } KritaUtils::filterContainer(safeNodesToDelete, [](KisNodeSP node) { return !node->userLocked(); }); safeRemoveMultipleNodes(safeNodesToDelete, m_info->image); } } private: void addCommandImpl(KUndo2Command *cmd) override { addCommand(cmd); } void reparentSelectionMasks(KisImageSP image, KisLayerSP newLayer, const QVector &selectionMasks) { KIS_SAFE_ASSERT_RECOVER_RETURN(newLayer); foreach (KisSelectionMaskSP mask, selectionMasks) { addCommand(new KisImageLayerMoveCommand(image, mask, newLayer, newLayer->lastChild())); addCommand(new KisActivateSelectionMaskCommand(mask, false)); } } private: MergeDownInfoBaseSP m_info; KisNodeSP m_putAfter; }; SwitchFrameCommand::SharedStorage::~SharedStorage() { } SwitchFrameCommand::SwitchFrameCommand(KisImageSP image, int time, bool finalize, SharedStorageSP storage) : FlipFlopCommand(finalize), m_image(image), m_newTime(time), m_storage(storage) {} SwitchFrameCommand::~SwitchFrameCommand() {} void SwitchFrameCommand::init() { KisImageAnimationInterface *interface = m_image->animationInterface(); const int currentTime = interface->currentTime(); if (currentTime == m_newTime) { m_storage->value = m_newTime; return; } interface->image()->disableUIUpdates(); interface->saveAndResetCurrentTime(m_newTime, &m_storage->value); } void SwitchFrameCommand::end() { KisImageAnimationInterface *interface = m_image->animationInterface(); const int currentTime = interface->currentTime(); if (currentTime == m_storage->value) { return; } interface->restoreCurrentTime(&m_storage->value); interface->image()->enableUIUpdates(); } struct AddNewFrame : public KisCommandUtils::AggregateCommand { AddNewFrame(MergeDownInfoBaseSP info, int frame) : m_info(info), m_frame(frame) {} void populateChildCommands() override { KUndo2Command *cmd = new KisCommandUtils::SkipFirstRedoWrapper(); KisKeyframeChannel *channel = m_info->dstNode->getKeyframeChannel(KisKeyframeChannel::Content.id()); KisKeyframeSP keyframe = channel->addKeyframe(m_frame, cmd); applyKeyframeColorLabel(keyframe); addCommand(cmd); } void applyKeyframeColorLabel(KisKeyframeSP dstKeyframe) { Q_FOREACH(KisNodeSP srcNode, m_info->allSrcNodes()) { Q_FOREACH(KisKeyframeChannel *channel, srcNode->keyframeChannels().values()) { - KisKeyframeSP keyframe = channel->keyframeAt(m_frame); + KisKeyframeSP keyframe = channel->visibleKeyframeAt(m_frame); if (!keyframe.isNull() && keyframe->colorLabel() != 0) { dstKeyframe->setColorLabel(keyframe->colorLabel()); return; } } } dstKeyframe->setColorLabel(0); } private: MergeDownInfoBaseSP m_info; int m_frame; }; QSet fetchLayerFrames(KisNodeSP node) { KisKeyframeChannel *channel = node->getKeyframeChannel(KisKeyframeChannel::Content.id()); if (!channel) return QSet(); return channel->allKeyframeIds(); } QSet fetchLayerFramesRecursive(KisNodeSP rootNode) { QSet frames = fetchLayerFrames(rootNode); KisNodeSP node = rootNode->firstChild(); while(node) { frames |= fetchLayerFramesRecursive(node); node = node->nextSibling(); } return frames; } void updateFrameJobs(FrameJobs *jobs, KisNodeSP node) { QSet frames = fetchLayerFrames(node); if (frames.isEmpty()) { (*jobs)[0].insert(node); } else { foreach (int frame, frames) { (*jobs)[frame].insert(node); } } } void updateFrameJobsRecursive(FrameJobs *jobs, KisNodeSP rootNode) { updateFrameJobs(jobs, rootNode); KisNodeSP node = rootNode->firstChild(); while(node) { updateFrameJobsRecursive(jobs, node); node = node->nextSibling(); } } void mergeDown(KisImageSP image, KisLayerSP layer, const KisMetaData::MergeStrategy* strategy) { if (!layer->prevSibling()) return; // XXX: this breaks if we allow free mixing of masks and layers KisLayerSP prevLayer = qobject_cast(layer->prevSibling().data()); if (!prevLayer) return; if (!layer->visible() && !prevLayer->visible()) { return; } KisImageSignalVector emitSignals; emitSignals << ModifiedSignal; KisProcessingApplicator applicator(image, 0, KisProcessingApplicator::NONE, emitSignals, kundo2_i18n("Merge Down")); if (layer->visible() && prevLayer->visible()) { MergeDownInfoSP info(new MergeDownInfo(image, prevLayer, layer)); // disable key strokes on all colorize masks, all onion skins on // paint layers and wait until update is finished with a barrier applicator.applyCommand(new DisableColorizeKeyStrokes(info)); applicator.applyCommand(new DisableOnionSkins(info)); applicator.applyCommand(new KUndo2Command(), KisStrokeJobData::BARRIER); applicator.applyCommand(new KeepMergedNodesSelected(info, false)); applicator.applyCommand(new FillSelectionMasks(info)); applicator.applyCommand(new CreateMergedLayer(info), KisStrokeJobData::BARRIER); // NOTE: shape layer may have emitted spontaneous jobs during layer creation, // wait for them to complete! applicator.applyCommand(new RefreshDelayedUpdateLayers(info), KisStrokeJobData::BARRIER); applicator.applyCommand(new KUndo2Command(), KisStrokeJobData::BARRIER); // in two-layer mode we disable pass trhough only when the destination layer // is not a group layer applicator.applyCommand(new DisablePassThroughForHeadsOnly(info, true)); applicator.applyCommand(new KUndo2Command(), KisStrokeJobData::BARRIER); if (info->frames.size() > 0) { foreach (int frame, info->frames) { applicator.applyCommand(new SwitchFrameCommand(info->image, frame, false, info->storage)); applicator.applyCommand(new AddNewFrame(info, frame)); applicator.applyCommand(new RefreshHiddenAreas(info)); applicator.applyCommand(new RefreshDelayedUpdateLayers(info), KisStrokeJobData::BARRIER); applicator.applyCommand(new MergeLayers(info), KisStrokeJobData::BARRIER); applicator.applyCommand(new SwitchFrameCommand(info->image, frame, true, info->storage)); } } else { applicator.applyCommand(new RefreshHiddenAreas(info)); applicator.applyCommand(new RefreshDelayedUpdateLayers(info), KisStrokeJobData::BARRIER); applicator.applyCommand(new MergeLayers(info), KisStrokeJobData::BARRIER); } applicator.applyCommand(new MergeMetaData(info, strategy), KisStrokeJobData::BARRIER); applicator.applyCommand(new CleanUpNodes(info, layer), KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); applicator.applyCommand(new KeepMergedNodesSelected(info, true)); } else if (layer->visible()) { applicator.applyCommand(new KeepNodesSelectedCommand(KisNodeList(), KisNodeList(), layer, KisNodeSP(), image, false)); applicator.applyCommand( new SimpleRemoveLayers(KisNodeList() << prevLayer, image), KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); applicator.applyCommand(new KeepNodesSelectedCommand(KisNodeList(), KisNodeList(), KisNodeSP(), layer, image, true)); } else if (prevLayer->visible()) { applicator.applyCommand(new KeepNodesSelectedCommand(KisNodeList(), KisNodeList(), layer, KisNodeSP(), image, false)); applicator.applyCommand( new SimpleRemoveLayers(KisNodeList() << layer, image), KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); applicator.applyCommand(new KeepNodesSelectedCommand(KisNodeList(), KisNodeList(), KisNodeSP(), prevLayer, image, true)); } applicator.end(); } bool checkIsChildOf(KisNodeSP node, const KisNodeList &parents) { KisNodeList nodeParents; KisNodeSP parent = node->parent(); while (parent) { nodeParents << parent; parent = parent->parent(); } foreach(KisNodeSP perspectiveParent, parents) { if (nodeParents.contains(perspectiveParent)) { return true; } } return false; } bool checkIsCloneOf(KisNodeSP node, const KisNodeList &nodes) { bool result = false; KisCloneLayer *clone = dynamic_cast(node.data()); if (clone) { KisNodeSP cloneSource = KisNodeSP(clone->copyFrom()); Q_FOREACH(KisNodeSP subtree, nodes) { result = recursiveFindNode(subtree, [cloneSource](KisNodeSP node) -> bool { return node == cloneSource; }); if (!result) { result = checkIsCloneOf(cloneSource, nodes); } if (result) { break; } } } return result; } void filterMergableNodes(KisNodeList &nodes, bool allowMasks) { KisNodeList::iterator it = nodes.begin(); while (it != nodes.end()) { if ((!allowMasks && !qobject_cast(it->data())) || checkIsChildOf(*it, nodes)) { //qDebug() << "Skipping node" << ppVar((*it)->name()); it = nodes.erase(it); } else { ++it; } } } void sortMergableNodes(KisNodeSP root, KisNodeList &inputNodes, KisNodeList &outputNodes) { KisNodeList::iterator it = std::find(inputNodes.begin(), inputNodes.end(), root); if (it != inputNodes.end()) { outputNodes << *it; inputNodes.erase(it); } if (inputNodes.isEmpty()) { return; } KisNodeSP child = root->firstChild(); while (child) { sortMergableNodes(child, inputNodes, outputNodes); child = child->nextSibling(); } /** * By the end of recursion \p inputNodes must be empty */ KIS_ASSERT_RECOVER_NOOP(root->parent() || inputNodes.isEmpty()); } KisNodeList sortMergableNodes(KisNodeSP root, KisNodeList nodes) { KisNodeList result; sortMergableNodes(root, nodes, result); return result; } KisNodeList sortAndFilterMergableInternalNodes(KisNodeList nodes, bool allowMasks) { KIS_ASSERT_RECOVER(!nodes.isEmpty()) { return nodes; } KisNodeSP root; Q_FOREACH(KisNodeSP node, nodes) { KisNodeSP localRoot = node; while (localRoot->parent()) { localRoot = localRoot->parent(); } if (!root) { root = localRoot; } KIS_ASSERT_RECOVER(root == localRoot) { return nodes; } } KisNodeList result; sortMergableNodes(root, nodes, result); filterMergableNodes(result, allowMasks); return result; } void addCopyOfNameTag(KisNodeSP node) { const QString prefix = i18n("Copy of"); QString newName = node->name(); if (!newName.startsWith(prefix)) { newName = QString("%1 %2").arg(prefix).arg(newName); node->setName(newName); } } KisNodeList findNodesWithProps(KisNodeSP root, const KoProperties &props, bool excludeRoot) { KisNodeList nodes; if ((!excludeRoot || root->parent()) && root->check(props)) { nodes << root; } KisNodeSP node = root->firstChild(); while (node) { nodes += findNodesWithProps(node, props, excludeRoot); node = node->nextSibling(); } return nodes; } KisNodeList filterInvisibleNodes(const KisNodeList &nodes, KisNodeList *invisibleNodes, KisNodeSP *putAfter) { KIS_ASSERT_RECOVER(invisibleNodes) { return nodes; } KIS_ASSERT_RECOVER(putAfter) { return nodes; } KisNodeList visibleNodes; int putAfterIndex = -1; Q_FOREACH(KisNodeSP node, nodes) { if (node->visible() || node->userLocked()) { visibleNodes << node; } else { *invisibleNodes << node; if (node == *putAfter) { putAfterIndex = visibleNodes.size() - 1; } } } if (!visibleNodes.isEmpty() && putAfterIndex >= 0) { putAfterIndex = qBound(0, putAfterIndex, visibleNodes.size() - 1); *putAfter = visibleNodes[putAfterIndex]; } return visibleNodes; } void changeImageDefaultProjectionColor(KisImageSP image, const KoColor &color) { KisImageSignalVector emitSignals; emitSignals << ModifiedSignal; KisProcessingApplicator applicator(image, image->root(), KisProcessingApplicator::RECURSIVE, emitSignals, kundo2_i18n("Change projection color"), 0, 142857 + 1); applicator.applyCommand(new KisChangeProjectionColorCommand(image, color), KisStrokeJobData::BARRIER, KisStrokeJobData::EXCLUSIVE); applicator.end(); } void mergeMultipleLayersImpl(KisImageSP image, KisNodeList mergedNodes, KisNodeSP putAfter, bool flattenSingleLayer, const KUndo2MagicString &actionName, bool cleanupNodes = true, const QString layerName = QString()) { if (!putAfter) { putAfter = mergedNodes.first(); } filterMergableNodes(mergedNodes); { KisNodeList tempNodes; std::swap(mergedNodes, tempNodes); sortMergableNodes(image->root(), tempNodes, mergedNodes); } if (mergedNodes.size() <= 1 && (!flattenSingleLayer && mergedNodes.size() == 1)) return; KisImageSignalVector emitSignals; emitSignals << ModifiedSignal; emitSignals << ComplexNodeReselectionSignal(KisNodeSP(), KisNodeList(), KisNodeSP(), mergedNodes); KisProcessingApplicator applicator(image, 0, KisProcessingApplicator::NONE, emitSignals, actionName); KisNodeList originalNodes = mergedNodes; KisNodeList invisibleNodes; mergedNodes = filterInvisibleNodes(originalNodes, &invisibleNodes, &putAfter); if (!invisibleNodes.isEmpty()) { applicator.applyCommand( new SimpleRemoveLayers(invisibleNodes, image), KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); } if (mergedNodes.size() > 1 || invisibleNodes.isEmpty()) { MergeMultipleInfoSP info(new MergeMultipleInfo(image, mergedNodes)); // disable key strokes on all colorize masks, all onion skins on // paint layers and wait until update is finished with a barrier applicator.applyCommand(new DisableColorizeKeyStrokes(info)); applicator.applyCommand(new DisableOnionSkins(info)); applicator.applyCommand(new DisablePassThroughForHeadsOnly(info)); applicator.applyCommand(new KUndo2Command(), KisStrokeJobData::BARRIER); applicator.applyCommand(new KeepMergedNodesSelected(info, putAfter, false)); applicator.applyCommand(new FillSelectionMasks(info)); applicator.applyCommand(new CreateMergedLayerMultiple(info, layerName), KisStrokeJobData::BARRIER); applicator.applyCommand(new DisableExtraCompositing(info)); applicator.applyCommand(new KUndo2Command(), KisStrokeJobData::BARRIER); if (info->frames.size() > 0) { foreach (int frame, info->frames) { applicator.applyCommand(new SwitchFrameCommand(info->image, frame, false, info->storage)); applicator.applyCommand(new AddNewFrame(info, frame)); applicator.applyCommand(new RefreshHiddenAreas(info)); applicator.applyCommand(new RefreshDelayedUpdateLayers(info), KisStrokeJobData::BARRIER); applicator.applyCommand(new MergeLayersMultiple(info), KisStrokeJobData::BARRIER); applicator.applyCommand(new SwitchFrameCommand(info->image, frame, true, info->storage)); } } else { applicator.applyCommand(new RefreshHiddenAreas(info)); applicator.applyCommand(new RefreshDelayedUpdateLayers(info), KisStrokeJobData::BARRIER); applicator.applyCommand(new MergeLayersMultiple(info), KisStrokeJobData::BARRIER); } //applicator.applyCommand(new MergeMetaData(info, strategy), KisStrokeJobData::BARRIER); if (cleanupNodes){ applicator.applyCommand(new CleanUpNodes(info, putAfter), KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); } else { applicator.applyCommand(new InsertNode(info, putAfter), KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); } applicator.applyCommand(new KeepMergedNodesSelected(info, putAfter, true)); } applicator.end(); } void mergeMultipleLayers(KisImageSP image, KisNodeList mergedNodes, KisNodeSP putAfter) { mergeMultipleLayersImpl(image, mergedNodes, putAfter, false, kundo2_i18n("Merge Selected Nodes")); } void newLayerFromVisible(KisImageSP image, KisNodeSP putAfter) { KisNodeList mergedNodes; mergedNodes << image->root(); mergeMultipleLayersImpl(image, mergedNodes, putAfter, true, kundo2_i18n("New From Visible"), false, i18nc("New layer created from all the visible layers", "Visible")); } struct MergeSelectionMasks : public KisCommandUtils::AggregateCommand { MergeSelectionMasks(MergeDownInfoBaseSP info, KisNodeSP putAfter) : m_info(info), m_putAfter(putAfter){} void populateChildCommands() override { KisNodeSP parent; CleanUpNodes::findPerfectParent(m_info->allSrcNodes(), m_putAfter, parent); KisLayerSP parentLayer; do { parentLayer = qobject_cast(parent.data()); parent = parent->parent(); } while(!parentLayer && parent); KisSelectionSP selection = new KisSelection(); foreach (KisNodeSP node, m_info->allSrcNodes()) { KisMaskSP mask = dynamic_cast(node.data()); if (!mask) continue; selection->pixelSelection()->applySelection( mask->selection()->pixelSelection(), SELECTION_ADD); } KisSelectionMaskSP mergedMask = new KisSelectionMask(m_info->image); mergedMask->initSelection(parentLayer); mergedMask->setSelection(selection); m_info->dstNode = mergedMask; } private: MergeDownInfoBaseSP m_info; KisNodeSP m_putAfter; }; struct ActivateSelectionMask : public KisCommandUtils::AggregateCommand { ActivateSelectionMask(MergeDownInfoBaseSP info) : m_info(info) {} void populateChildCommands() override { KisSelectionMaskSP mergedMask = dynamic_cast(m_info->dstNode.data()); addCommand(new KisActivateSelectionMaskCommand(mergedMask, true)); } private: MergeDownInfoBaseSP m_info; }; bool tryMergeSelectionMasks(KisImageSP image, KisNodeList mergedNodes, KisNodeSP putAfter) { QList selectionMasks; for (auto it = mergedNodes.begin(); it != mergedNodes.end(); /*noop*/) { KisSelectionMaskSP mask = dynamic_cast(it->data()); if (!mask) { it = mergedNodes.erase(it); } else { selectionMasks.append(mask); ++it; } } if (mergedNodes.isEmpty()) return false; KisLayerSP parentLayer = qobject_cast(selectionMasks.first()->parent().data()); KIS_ASSERT_RECOVER(parentLayer) { return 0; } KisImageSignalVector emitSignals; emitSignals << ModifiedSignal; KisProcessingApplicator applicator(image, 0, KisProcessingApplicator::NONE, emitSignals, kundo2_i18n("Merge Selection Masks")); MergeMultipleInfoSP info(new MergeMultipleInfo(image, mergedNodes)); applicator.applyCommand(new MergeSelectionMasks(info, putAfter)); applicator.applyCommand(new CleanUpNodes(info, putAfter), KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); applicator.applyCommand(new ActivateSelectionMask(info)); applicator.end(); return true; } void flattenLayer(KisImageSP image, KisLayerSP layer) { if (!layer->childCount() && !layer->layerStyle()) return; KisNodeList mergedNodes; mergedNodes << layer; mergeMultipleLayersImpl(image, mergedNodes, layer, true, kundo2_i18n("Flatten Layer")); } void flattenImage(KisImageSP image, KisNodeSP activeNode) { if (!activeNode) { activeNode = image->root()->lastChild(); } KisNodeList mergedNodes; mergedNodes << image->root(); mergeMultipleLayersImpl(image, mergedNodes, activeNode, true, kundo2_i18n("Flatten Image")); } KisSimpleUpdateCommand::KisSimpleUpdateCommand(KisNodeList nodes, bool finalize, KUndo2Command *parent) : FlipFlopCommand(finalize, parent), m_nodes(nodes) { } void KisSimpleUpdateCommand::end() { updateNodes(m_nodes); } void KisSimpleUpdateCommand::updateNodes(const KisNodeList &nodes) { Q_FOREACH(KisNodeSP node, nodes) { node->setDirty(node->extent()); } } KisNodeSP recursiveFindNode(KisNodeSP node, std::function func) { if (func(node)) { return node; } node = node->firstChild(); while (node) { KisNodeSP resultNode = recursiveFindNode(node, func); if (resultNode) { return resultNode; } node = node->nextSibling(); } return 0; } KisNodeSP findNodeByUuid(KisNodeSP root, const QUuid &uuid) { return recursiveFindNode(root, [uuid] (KisNodeSP node) { return node->uuid() == uuid; }); } void forceAllDelayedNodesUpdate(KisNodeSP root) { KisLayerUtils::recursiveApplyNodes(root, [] (KisNodeSP node) { KisDelayedUpdateNodeInterface *delayedUpdate = dynamic_cast(node.data()); if (delayedUpdate) { delayedUpdate->forceUpdateTimedNode(); } }); } KisImageSP findImageByHierarchy(KisNodeSP node) { while (node) { const KisLayer *layer = dynamic_cast(node.data()); if (layer) { return layer->image(); } node = node->parent(); } return 0; } } diff --git a/libs/image/kis_transaction_data.cpp b/libs/image/kis_transaction_data.cpp index 23db9f3146..62e818c8bf 100644 --- a/libs/image/kis_transaction_data.cpp +++ b/libs/image/kis_transaction_data.cpp @@ -1,314 +1,314 @@ /* * Copyright (c) 2002 Patrick Julien * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_transaction_data.h" #include "kis_pixel_selection.h" #include "kis_paint_device.h" #include "kis_paint_device_frames_interface.h" #include "kis_datamanager.h" #include "kis_image.h" //#define DEBUG_TRANSACTIONS #ifdef DEBUG_TRANSACTIONS # define DEBUG_ACTION(action) dbgKrita << action << "for" << m_d->device->dataManager() #else # define DEBUG_ACTION(action) #endif class Q_DECL_HIDDEN KisTransactionData::Private { public: KisPaintDeviceSP device; KisMementoSP memento; bool firstRedo; bool transactionFinished; QPoint oldOffset; QPoint newOffset; bool savedOutlineCacheValid; QPainterPath savedOutlineCache; KUndo2Command *flattenUndoCommand; bool resetSelectionOutlineCache; int transactionTime; int transactionFrameId; KisDataManagerSP savedDataManager; KUndo2Command newFrameCommand; void possiblySwitchCurrentTime(); KisDataManagerSP dataManager(); void moveDevice(const QPoint newOffset); void tryCreateNewFrame(KisPaintDeviceSP device, int time); }; KisTransactionData::KisTransactionData(const KUndo2MagicString& name, KisPaintDeviceSP device, bool resetSelectionOutlineCache, KUndo2Command* parent) : KUndo2Command(name, parent) , m_d(new Private()) { m_d->resetSelectionOutlineCache = resetSelectionOutlineCache; setTimedID(-1); init(device); saveSelectionOutlineCache(); } #include "kis_raster_keyframe_channel.h" #include "kis_image_config.h" void KisTransactionData::Private::tryCreateNewFrame(KisPaintDeviceSP device, int time) { if (!device->framesInterface()) return; KisImageConfig cfg(true); if (!cfg.lazyFrameCreationEnabled()) return; KisRasterKeyframeChannel *channel = device->keyframeChannel(); KIS_ASSERT_RECOVER(channel) { return; } KisKeyframeSP keyframe = channel->keyframeAt(time); if (!keyframe) { keyframe = channel->visibleKeyframeAt(time); - KisKeyframeSP newKeyframe = channel->copyKeyframe(keyframe, time, &newFrameCommand); + KisKeyframeSP newKeyframe = channel->copyAsKeyframe(keyframe, time, time, &newFrameCommand); newKeyframe->setColorLabel(KisImageConfig(true).defaultFrameColorLabel()); } } void KisTransactionData::init(KisPaintDeviceSP device) { m_d->device = device; DEBUG_ACTION("Transaction started"); m_d->oldOffset = QPoint(device->x(), device->y()); m_d->firstRedo = true; m_d->transactionFinished = false; m_d->flattenUndoCommand = 0; m_d->transactionTime = device->defaultBounds()->currentTime(); m_d->tryCreateNewFrame(m_d->device, m_d->transactionTime); m_d->transactionFrameId = device->framesInterface() ? device->framesInterface()->currentFrameId() : -1; m_d->savedDataManager = m_d->transactionFrameId >= 0 ? m_d->device->framesInterface()->frameDataManager(m_d->transactionFrameId) : m_d->device->dataManager(); m_d->memento = m_d->savedDataManager->getMemento(); } KisTransactionData::~KisTransactionData() { Q_ASSERT(m_d->memento); m_d->savedDataManager->purgeHistory(m_d->memento); delete m_d; } void KisTransactionData::Private::moveDevice(const QPoint newOffset) { if (transactionFrameId >= 0) { device->framesInterface()->setFrameOffset(transactionFrameId, newOffset); } else { device->moveTo(newOffset); } } void KisTransactionData::endTransaction() { if(!m_d->transactionFinished) { // make sure the time didn't change during the transaction KIS_ASSERT_RECOVER_RETURN( m_d->transactionTime == m_d->device->defaultBounds()->currentTime()); DEBUG_ACTION("Transaction ended"); m_d->transactionFinished = true; m_d->savedDataManager->commit(); m_d->newOffset = QPoint(m_d->device->x(), m_d->device->y()); } } void KisTransactionData::startUpdates() { if (m_d->transactionFrameId == -1 || m_d->transactionFrameId == m_d->device->framesInterface()->currentFrameId()) { QRect rc; QRect mementoExtent = m_d->memento->extent(); if (m_d->newOffset == m_d->oldOffset) { rc = mementoExtent.translated(m_d->device->x(), m_d->device->y()); } else { QRect totalExtent = m_d->savedDataManager->extent() | mementoExtent; rc = totalExtent.translated(m_d->oldOffset) | totalExtent.translated(m_d->newOffset); } m_d->device->setDirty(rc); } else { m_d->device->framesInterface()->invalidateFrameCache(m_d->transactionFrameId); } } void KisTransactionData::possiblyNotifySelectionChanged() { KisPixelSelectionSP pixelSelection = dynamic_cast(m_d->device.data()); KisSelectionSP selection; if (pixelSelection && (selection = pixelSelection->parentSelection())) { selection->notifySelectionChanged(); } } void KisTransactionData::possiblyResetOutlineCache() { KisPixelSelectionSP pixelSelection; if (m_d->resetSelectionOutlineCache && (pixelSelection = dynamic_cast(m_d->device.data()))) { pixelSelection->invalidateOutlineCache(); } } void KisTransactionData::Private::possiblySwitchCurrentTime() { if (device->defaultBounds()->currentTime() == transactionTime) return; qWarning() << "WARNING: undo command has been executed, when another frame has been active. That shouldn't have happened."; device->requestTimeSwitch(transactionTime); } void KisTransactionData::redo() { //KUndo2QStack calls redo(), so the first call needs to be blocked if (m_d->firstRedo) { m_d->firstRedo = false; possiblyResetOutlineCache(); possiblyNotifySelectionChanged(); return; } restoreSelectionOutlineCache(false); m_d->newFrameCommand.redo(); DEBUG_ACTION("Redo()"); Q_ASSERT(m_d->memento); m_d->savedDataManager->rollforward(m_d->memento); if (m_d->newOffset != m_d->oldOffset) { m_d->moveDevice(m_d->newOffset); } m_d->possiblySwitchCurrentTime(); startUpdates(); possiblyNotifySelectionChanged(); } void KisTransactionData::undo() { DEBUG_ACTION("Undo()"); Q_ASSERT(m_d->memento); m_d->savedDataManager->rollback(m_d->memento); if (m_d->newOffset != m_d->oldOffset) { m_d->moveDevice(m_d->oldOffset); } restoreSelectionOutlineCache(true); m_d->possiblySwitchCurrentTime(); startUpdates(); possiblyNotifySelectionChanged(); m_d->newFrameCommand.undo(); } void KisTransactionData::saveSelectionOutlineCache() { m_d->savedOutlineCacheValid = false; KisPixelSelectionSP pixelSelection = dynamic_cast(m_d->device.data()); if (pixelSelection) { m_d->savedOutlineCacheValid = pixelSelection->outlineCacheValid(); if (m_d->savedOutlineCacheValid) { m_d->savedOutlineCache = pixelSelection->outlineCache(); possiblyResetOutlineCache(); } KisSelectionSP selection = pixelSelection->parentSelection(); if (selection) { m_d->flattenUndoCommand = selection->flatten(); if (m_d->flattenUndoCommand) { m_d->flattenUndoCommand->redo(); } } } } void KisTransactionData::restoreSelectionOutlineCache(bool undo) { KisPixelSelectionSP pixelSelection = dynamic_cast(m_d->device.data()); if (pixelSelection) { bool savedOutlineCacheValid; QPainterPath savedOutlineCache; savedOutlineCacheValid = pixelSelection->outlineCacheValid(); if (savedOutlineCacheValid) { savedOutlineCache = pixelSelection->outlineCache(); } if (m_d->savedOutlineCacheValid) { pixelSelection->setOutlineCache(m_d->savedOutlineCache); } else { pixelSelection->invalidateOutlineCache(); } m_d->savedOutlineCacheValid = savedOutlineCacheValid; if (m_d->savedOutlineCacheValid) { m_d->savedOutlineCache = savedOutlineCache; } if (m_d->flattenUndoCommand) { if (undo) { m_d->flattenUndoCommand->undo(); } else { m_d->flattenUndoCommand->redo(); } } } } diff --git a/libs/image/tests/kis_keyframing_test.cpp b/libs/image/tests/kis_keyframing_test.cpp index 108db90e98..9e9765ebae 100644 --- a/libs/image/tests/kis_keyframing_test.cpp +++ b/libs/image/tests/kis_keyframing_test.cpp @@ -1,583 +1,583 @@ /* * Copyright (c) 2015 Jouni Pentikäinen * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_keyframing_test.h" #include #include #include "kis_paint_device_frames_interface.h" #include "kis_keyframe_channel.h" #include "kis_scalar_keyframe_channel.h" #include "kis_raster_keyframe_channel.h" #include "kis_node.h" #include "kis_time_range.h" #include "kis_animation_cycle.h" #include "kundo2command.h" #include "kis_pointer_utils.h" #include #include "testing_timed_default_bounds.h" void KisKeyframingTest::initTestCase() { cs = KoColorSpaceRegistry::instance()->rgb8(); red = new quint8[cs->pixelSize()]; green = new quint8[cs->pixelSize()]; blue = new quint8[cs->pixelSize()]; cs->fromQColor(Qt::red, red); cs->fromQColor(Qt::green, green); cs->fromQColor(Qt::blue, blue); } void KisKeyframingTest::cleanupTestCase() { delete[] red; delete[] green; delete[] blue; } void KisKeyframingTest::testScalarChannel() { KisScalarKeyframeChannel *channel = new KisScalarKeyframeChannel(KoID(""), -17, 31, 0); KisKeyframeSP key; bool ok; QCOMPARE(channel->hasScalarValue(), true); QCOMPARE(channel->minScalarValue(), -17.0); QCOMPARE(channel->maxScalarValue(), 31.0); QVERIFY(channel->keyframeAt(0) == 0); // Adding new keyframe key = channel->addKeyframe(42); channel->setScalarValue(key, 7.0); key = channel->keyframeAt(42); QCOMPARE(channel->scalarValue(key), 7.0); // Copying a keyframe - KisKeyframeSP key2 = channel->copyKeyframe(key, 13); + KisKeyframeSP key2 = channel->copyAsKeyframe(key, 2, 13); QVERIFY(key2 != 0); QVERIFY(channel->keyframeAt(13) == key2); QCOMPARE(channel->scalarValue(key2), 7.0); // Adding a keyframe where one exists key2 = channel->addKeyframe(13); QVERIFY(key2 != key); QCOMPARE(channel->keyframeCount(), 2); // Moving keyframes ok = channel->moveKeyframe(key, 10); QCOMPARE(ok, true); QVERIFY(channel->keyframeAt(42) == 0); key = channel->keyframeAt(10); QCOMPARE(channel->scalarValue(key), 7.0); // Moving a keyframe where another one exists ok = channel->moveKeyframe(key, 13); QCOMPARE(ok, true); QVERIFY(channel->keyframeAt(13) != key2); // Deleting a keyframe channel->deleteKeyframe(key); QVERIFY(channel->keyframeAt(10) == 0); QCOMPARE(channel->keyframeCount(), 0); delete channel; } void KisKeyframingTest::testScalarChannelUndoRedo() { KisScalarKeyframeChannel *channel = new KisScalarKeyframeChannel(KoID(""), -17, 31, 0); KisKeyframeSP key; QCOMPARE(channel->hasScalarValue(), true); QCOMPARE(channel->minScalarValue(), -17.0); QCOMPARE(channel->maxScalarValue(), 31.0); QVERIFY(channel->keyframeAt(0) == 0); // Adding new keyframe KUndo2Command addCmd; key = channel->addKeyframe(42, &addCmd); channel->setScalarValue(key, 7.0, &addCmd); key = channel->keyframeAt(42); QCOMPARE(channel->scalarValue(key), 7.0); addCmd.undo(); KisKeyframeSP newKey; newKey = channel->keyframeAt(42); QVERIFY(!newKey); addCmd.redo(); newKey = channel->keyframeAt(42); QVERIFY(newKey); QCOMPARE(channel->scalarValue(key), 7.0); QCOMPARE(channel->scalarValue(newKey), 7.0); delete channel; } void KisKeyframingTest::testScalarInterpolation() { KisScalarKeyframeChannel *channel = new KisScalarKeyframeChannel(KoID(""), 0, 30, 0); KisKeyframeSP key1 = channel->addKeyframe(0); channel->setScalarValue(key1, 15); KisKeyframeSP key2 = channel->addKeyframe(10); channel->setScalarValue(key2, 30); // Constant key1->setInterpolationMode(KisKeyframe::Constant); QCOMPARE(channel->interpolatedValue(4), 15.0f); // Bezier key1->setInterpolationMode(KisKeyframe::Bezier); key1->setInterpolationTangents(QPointF(), QPointF(1,4)); key2->setInterpolationTangents(QPointF(-4,2), QPointF()); QVERIFY(qAbs(channel->interpolatedValue(4) - 24.9812f) < 0.1f); // Bezier, self-intersecting curve (auto-correct) channel->setScalarValue(key2, 15); key1->setInterpolationTangents(QPointF(), QPointF(13,10)); key2->setInterpolationTangents(QPointF(-13,10), QPointF()); QVERIFY(qAbs(channel->interpolatedValue(5) - 20.769f) < 0.1f); // Bezier, result outside allowed range (clamp) channel->setScalarValue(key2, 15); key1->setInterpolationTangents(QPointF(), QPointF(0, 50)); key2->setInterpolationTangents(QPointF(0, 50), QPointF()); QCOMPARE(channel->interpolatedValue(5), 30.0f); delete channel; } void KisKeyframingTest::testRasterChannel() { TestUtil::TestingTimedDefaultBounds *bounds = new TestUtil::TestingTimedDefaultBounds(); KisPaintDeviceSP dev = new KisPaintDevice(cs); dev->setDefaultBounds(bounds); KisRasterKeyframeChannel * channel = dev->createKeyframeChannel(KoID()); QCOMPARE(channel->hasScalarValue(), false); QCOMPARE(channel->keyframeCount(), 1); QCOMPARE(dev->framesInterface()->frames().count(), 1); QCOMPARE(channel->frameIdAt(0), 0); QVERIFY(channel->keyframeAt(0) != 0); KisKeyframeSP key_0 = channel->keyframeAt(0); // New keyframe KisKeyframeSP key_10 = channel->addKeyframe(10); QCOMPARE(channel->keyframeCount(), 2); QCOMPARE(dev->framesInterface()->frames().count(), 2); QVERIFY(channel->frameIdAt(10) != 0); dev->fill(0, 0, 512, 512, red); QImage thumb1a = dev->createThumbnail(50, 50); bounds->testingSetTime(10); dev->fill(0, 0, 512, 512, green); QImage thumb2a = dev->createThumbnail(50, 50); bounds->testingSetTime(0); QImage thumb1b = dev->createThumbnail(50, 50); QVERIFY(thumb2a != thumb1a); QVERIFY(thumb1b == thumb1a); // Duplicate keyframe - KisKeyframeSP key_20 = channel->copyKeyframe(key_0, 20); + KisKeyframeSP key_20 = channel->copyAsKeyframe(key_0, 0, 20); bounds->testingSetTime(20); QImage thumb3a = dev->createThumbnail(50, 50); QVERIFY(thumb3a == thumb1b); dev->fill(0, 0, 512, 512, blue); QImage thumb3b = dev->createThumbnail(50, 50); bounds->testingSetTime(0); QImage thumb1c = dev->createThumbnail(50, 50); QVERIFY(thumb3b != thumb3a); QVERIFY(thumb1c == thumb1b); // Delete keyrame QCOMPARE(channel->keyframeCount(), 3); QCOMPARE(dev->framesInterface()->frames().count(), 3); channel->deleteKeyframe(key_20); QCOMPARE(channel->keyframeCount(), 2); QCOMPARE(dev->framesInterface()->frames().count(), 2); QVERIFY(channel->keyframeAt(20) == 0); channel->deleteKeyframe(key_10); QCOMPARE(channel->keyframeCount(), 1); QCOMPARE(dev->framesInterface()->frames().count(), 1); QVERIFY(channel->keyframeAt(10) == 0); channel->deleteKeyframe(key_0); QCOMPARE(channel->keyframeCount(), 1); QCOMPARE(dev->framesInterface()->frames().count(), 1); QVERIFY(channel->keyframeAt(0) != 0); } void KisKeyframingTest::testChannelSignals() { KisScalarKeyframeChannel *channel = new KisScalarKeyframeChannel(KoID(""), -17, 31, 0); KisKeyframeBaseSP key; KisKeyframeBaseSP resKey; qRegisterMetaType("KisKeyframeBaseSP"); QSignalSpy spyPreAdd(channel, SIGNAL(sigKeyframeAboutToBeAdded(KisKeyframeBaseSP))); QSignalSpy spyPostAdd(channel, SIGNAL(sigKeyframeAdded(KisKeyframeBaseSP))); QSignalSpy spyPreRemove(channel, SIGNAL(sigKeyframeAboutToBeRemoved(KisKeyframeBaseSP))); QSignalSpy spyPostRemove(channel, SIGNAL(sigKeyframeRemoved(KisKeyframeBaseSP))); QSignalSpy spyPreMove(channel, SIGNAL(sigKeyframeAboutToBeMoved(KisKeyframeBaseSP,int))); QSignalSpy spyPostMove(channel, SIGNAL(sigKeyframeMoved(KisKeyframeBaseSP,int))); QVERIFY(spyPreAdd.isValid()); QVERIFY(spyPostAdd.isValid()); QVERIFY(spyPreRemove.isValid()); QVERIFY(spyPostRemove.isValid()); QVERIFY(spyPreMove.isValid()); QVERIFY(spyPostMove.isValid()); // Adding a keyframe QCOMPARE(spyPreAdd.count(), 0); QCOMPARE(spyPostAdd.count(), 0); key = channel->addKeyframe(10); QCOMPARE(spyPreAdd.count(), 1); QCOMPARE(spyPostAdd.count(), 1); resKey = spyPreAdd.at(0).at(0).value(); QVERIFY(resKey == key); resKey = spyPostAdd.at(0).at(0).value(); QVERIFY(resKey == key); // Moving a keyframe QCOMPARE(spyPreMove.count(), 0); QCOMPARE(spyPostMove.count(), 0); channel->moveKeyframe(key, 15); QCOMPARE(spyPreMove.count(), 1); QCOMPARE(spyPostMove.count(), 1); resKey = spyPreMove.at(0).at(0).value(); QVERIFY(resKey == key); QCOMPARE(spyPreMove.at(0).at(1).toInt(), 15); resKey = spyPostMove.at(0).at(0).value(); QVERIFY(resKey == key); // No-op move (no signals) channel->moveKeyframe(key, 15); QCOMPARE(spyPreMove.count(), 1); QCOMPARE(spyPostMove.count(), 1); // Deleting a keyframe QCOMPARE(spyPreRemove.count(), 0); QCOMPARE(spyPostRemove.count(), 0); channel->deleteKeyframe(key); QCOMPARE(spyPreRemove.count(), 1); QCOMPARE(spyPostRemove.count(), 1); delete channel; } void KisKeyframingTest::testRasterFrameFetching() { TestUtil::TestingTimedDefaultBounds *bounds = new TestUtil::TestingTimedDefaultBounds(); KisPaintDeviceSP dev = new KisPaintDevice(cs); KisPaintDeviceSP devTarget = new KisPaintDevice(cs); dev->setDefaultBounds(bounds); KisRasterKeyframeChannel * channel = dev->createKeyframeChannel(KoID()); channel->addKeyframe(0); channel->addKeyframe(10); channel->addKeyframe(50); bounds->testingSetTime(0); dev->fill(0, 0, 512, 512, red); QImage frame1 = dev->createThumbnail(50, 50); bounds->testingSetTime(10); dev->fill(0, 256, 512, 512, green); QImage frame2 = dev->createThumbnail(50, 50); bounds->testingSetTime(50); dev->fill(0, 0, 256, 512, blue); QImage frame3 = dev->createThumbnail(50, 50); bounds->testingSetTime(10); KisKeyframeSP keyframe = channel->activeKeyframeAt(0); channel->fetchFrame(keyframe, devTarget); QImage fetched1 = devTarget->createThumbnail(50, 50); keyframe = channel->activeKeyframeAt(10); channel->fetchFrame(keyframe, devTarget); QImage fetched2 = devTarget->createThumbnail(50, 50); keyframe = channel->activeKeyframeAt(50); channel->fetchFrame(keyframe, devTarget); QImage fetched3 = devTarget->createThumbnail(50, 50); QVERIFY(fetched1 == frame1); QVERIFY(fetched2 == frame2); QVERIFY(fetched3 == frame3); } void KisKeyframingTest::testDeleteFirstRasterChannel() { // Test Plan: // // delete // undo delete // move // undo move TestUtil::TestingTimedDefaultBounds *bounds = new TestUtil::TestingTimedDefaultBounds(); KisPaintDeviceSP dev = new KisPaintDevice(cs); dev->setDefaultBounds(bounds); KisRasterKeyframeChannel * channel = dev->createKeyframeChannel(KoID()); QCOMPARE(channel->hasScalarValue(), false); QCOMPARE(channel->keyframeCount(), 1); QCOMPARE(dev->framesInterface()->frames().count(), 1); QCOMPARE(channel->frameIdAt(0), 0); QVERIFY(channel->keyframeAt(0) != 0); KisKeyframeSP key_0 = channel->keyframeAt(0); { KUndo2Command cmd; bool deleteResult = channel->deleteKeyframe(key_0, &cmd); QVERIFY(deleteResult); QCOMPARE(dev->framesInterface()->frames().count(), 1); QVERIFY(channel->frameIdAt(0) != 0); QVERIFY(channel->keyframeAt(0)); QVERIFY(channel->keyframeAt(0) != key_0); cmd.undo(); QCOMPARE(dev->framesInterface()->frames().count(), 1); QVERIFY(channel->frameIdAt(0) == 0); QVERIFY(channel->keyframeAt(0)); QVERIFY(channel->keyframeAt(0) == key_0); } { KUndo2Command cmd; bool moveResult = channel->moveKeyframe(key_0, 1, &cmd); QVERIFY(moveResult); QCOMPARE(dev->framesInterface()->frames().count(), 2); QVERIFY(channel->frameIdAt(0) != 0); QVERIFY(channel->frameIdAt(1) == 0); QVERIFY(channel->keyframeAt(0)); QVERIFY(channel->keyframeAt(1)); QVERIFY(channel->keyframeAt(0) != key_0); QVERIFY(channel->keyframeAt(1) == key_0); cmd.undo(); QCOMPARE(dev->framesInterface()->frames().count(), 1); QVERIFY(channel->frameIdAt(0) == 0); QVERIFY(channel->keyframeAt(0)); QVERIFY(channel->keyframeAt(0) == key_0); } } void KisKeyframingTest::testAffectedFrames() { KisScalarKeyframeChannel *channel = new KisScalarKeyframeChannel(KoID(""), -17, 31, 0); KisFrameSet frames; channel->addKeyframe(10); channel->addKeyframe(20); channel->addKeyframe(30); // At a keyframe frames = channel->affectedFrames(20); QCOMPARE(frames, KisFrameSet::between(20, 29)); // Between frames frames = channel->affectedFrames(25); QCOMPARE(frames, KisFrameSet::between(20, 29)); // Before first frame frames = channel->affectedFrames(5); QCOMPARE(frames, KisFrameSet::between(0, 9)); // After last frame frames = channel->affectedFrames(35); QCOMPARE(frames, KisFrameSet::infiniteFrom(30)); // Linked keyframes KisPaintDeviceSP dev = new KisPaintDevice(cs); KisRasterKeyframeChannel * rasterChannel = dev->createKeyframeChannel(KoID()); auto key0 = rasterChannel->addKeyframe(0); auto key5 = rasterChannel->addKeyframe(5); auto key10 = rasterChannel->linkKeyframe(key0, 10, nullptr); QCOMPARE(rasterChannel->affectedFrames(5), KisFrameSet::between(5,9)); KisFrameSet result = rasterChannel->affectedFrames(1); QCOMPARE(result, KisFrameSet::between(0, 4) | KisFrameSet::infiniteFrom(10)); result = rasterChannel->affectedFrames(15); QCOMPARE(result, KisFrameSet::between(0, 4) | KisFrameSet::infiniteFrom(10)); } void KisKeyframingTest::testMovingFrames() { TestUtil::TestingTimedDefaultBounds *bounds = new TestUtil::TestingTimedDefaultBounds(); KisPaintDeviceSP dev = new KisPaintDevice(cs); dev->setDefaultBounds(bounds); KisRasterKeyframeChannel * srcChannel = dev->createKeyframeChannel(KoID()); srcChannel->addKeyframe(0); srcChannel->addKeyframe(10); srcChannel->addKeyframe(50); KisPaintDeviceSP dev2 = new KisPaintDevice(*dev, KritaUtils::CopyAllFrames); KisRasterKeyframeChannel * dstChannel = dev->keyframeChannel(); for (int i = 0; i < 1000; i++) { const int srcTime = 50 + i; const int dstTime = 60 + i; const int src2Time = 51 + i; { KUndo2Command parentCommand; KisKeyframeSP srcKeyframe = srcChannel->keyframeAt(srcTime); KIS_ASSERT(srcKeyframe); dstChannel->copyExternalKeyframe(srcChannel, srcTime, dstTime, &parentCommand); srcChannel->deleteKeyframe(srcKeyframe, &parentCommand); } for (int j = qMax(0, i-15); j < i+5; ++j) { bounds->testingSetTime(j); QRect rc1 = dev->extent(); QRect rc2 = dev2->extent(); Q_UNUSED(rc1); Q_UNUSED(rc2); } { KUndo2Command parentCommand; KisKeyframeSP dstKeyframe = dstChannel->keyframeAt(dstTime); KIS_ASSERT(dstKeyframe); srcChannel->copyExternalKeyframe(dstChannel, dstTime, src2Time, &parentCommand); dstChannel->deleteKeyframe(dstKeyframe, &parentCommand); } } } void KisKeyframingTest::testCycles() { TestUtil::TestingTimedDefaultBounds *bounds = new TestUtil::TestingTimedDefaultBounds(); KisPaintDeviceSP dev = new KisPaintDevice(cs); dev->setDefaultBounds(bounds); KisRasterKeyframeChannel *channel = dev->createKeyframeChannel(KoID()); KisKeyframeSP frame10 = channel->addKeyframe(10); channel->addKeyframe(12); KisKeyframeSP frame16 = channel->addKeyframe(16); channel->addKeyframe(20); auto cmd = channel->createCycle(frame10, frame16); cmd->redo(); // Cycled range can be queried from by any frame within it QCOMPARE(channel->cycledRangeAt(9), KisTimeSpan()); QCOMPARE(channel->cycledRangeAt(20), KisTimeSpan()); QCOMPARE(channel->cycledRangeAt(10), KisTimeSpan(10, 19)); QCOMPARE(channel->cycledRangeAt(19), KisTimeSpan(10, 19)); QSharedPointer repeatFrame = toQShared(new KisRepeatFrame(channel, 30, channel->cycleAt(10))); KisReplaceKeyframeCommand(channel, 30, repeatFrame, nullptr).redo(); // Repeats also resolve to the original cycled range QCOMPARE(channel->cycledRangeAt(29), KisTimeSpan()); QCOMPARE(channel->cycledRangeAt(30), KisTimeSpan(10, 19)); QCOMPARE(channel->cycledRangeAt(50), KisTimeSpan(10, 19)); QCOMPARE(channel->cycledRangeAt(100), KisTimeSpan(10, 19)); // Affected frames contain original frames and repeats QCOMPARE(channel->affectedFrames(15), KisFrameSet::between(12, 15) | KisFrameSet::infiniteFrom(32)); QCOMPARE(channel->affectedFrames(50), KisFrameSet::between(10, 11) | KisFrameSet::infiniteFrom(30)); // All repeats within the queried range are reported as identical. Original is always included. QCOMPARE(channel->identicalFrames(50, KisTimeSpan(40, 60)), KisFrameSet({{10, 11}, {40, 41}, {50, 51}, {60, 60}})); // Repeat ends at the next keyframe channel->addKeyframe(42); QCOMPARE(channel->cycledRangeAt(41), KisTimeSpan(10, 19)); QCOMPARE(channel->cycledRangeAt(42), KisTimeSpan()); // Finitely many repeats are all included separately as affected QCOMPARE(channel->affectedFrames(40), KisFrameSet({{10, 11}, {30, 31}, {40, 41}})); } QTEST_MAIN(KisKeyframingTest) diff --git a/plugins/dockers/animation/animation_docker.cpp b/plugins/dockers/animation/animation_docker.cpp index 09de6e6b2a..f4e45f3f69 100644 --- a/plugins/dockers/animation/animation_docker.cpp +++ b/plugins/dockers/animation/animation_docker.cpp @@ -1,661 +1,661 @@ /* * Copyright (c) 2015 Jouni Pentikäinen * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "animation_docker.h" #include "kis_global.h" #include "kis_canvas2.h" #include "kis_image.h" #include #include #include #include "KisViewManager.h" #include "kis_action_manager.h" #include "kis_image_animation_interface.h" #include "kis_animation_player.h" #include "kis_time_range.h" #include "kundo2command.h" #include "kis_post_execution_undo_adapter.h" #include "kis_keyframe_channel.h" #include "kis_animation_utils.h" #include "krita_utils.h" #include "kis_image_config.h" #include "kis_config.h" #include "kis_signals_blocker.h" #include "kis_node_manager.h" #include "kis_transform_mask_params_factory_registry.h" #include "ui_wdg_animation.h" void setupActionButton(const QString &text, KisAction::ActivationFlags flags, bool defaultValue, QToolButton *button, KisAction **action) { *action = new KisAction(text, button); (*action)->setActivationFlags(flags); (*action)->setCheckable(true); (*action)->setChecked(defaultValue); button->setDefaultAction(*action); } AnimationDocker::AnimationDocker() : QDockWidget(i18n("Animation")) , m_canvas(0) , m_animationWidget(new Ui_WdgAnimation) , m_mainWindow(0) { QWidget* mainWidget = new QWidget(this); setWidget(mainWidget); m_animationWidget->setupUi(mainWidget); } AnimationDocker::~AnimationDocker() { delete m_animationWidget; } void AnimationDocker::setCanvas(KoCanvasBase * canvas) { if(m_canvas == canvas) return; setEnabled(canvas != 0); if (m_canvas) { m_canvas->disconnectCanvasObserver(this); m_canvas->image()->disconnect(this); m_canvas->image()->animationInterface()->disconnect(this); m_canvas->animationPlayer()->disconnect(this); m_canvas->viewManager()->nodeManager()->disconnect(this); } m_canvas = dynamic_cast(canvas); if (m_canvas && m_canvas->image()) { KisImageAnimationInterface *animation = m_canvas->image()->animationInterface(); { KisSignalsBlocker bloker(m_animationWidget->spinFromFrame, m_animationWidget->spinToFrame, m_animationWidget->intFramerate); m_animationWidget->spinFromFrame->setValue(animation->fullClipRange().start()); m_animationWidget->spinToFrame->setValue(animation->fullClipRange().end()); m_animationWidget->intFramerate->setValue(animation->framerate()); } connect(animation, SIGNAL(sigUiTimeChanged(int)), this, SLOT(slotGlobalTimeChanged())); connect(m_canvas->animationPlayer(), SIGNAL(sigFrameChanged()), this, SLOT(slotGlobalTimeChanged())); connect(m_canvas->animationPlayer(), SIGNAL(sigPlaybackStopped()), this, SLOT(slotGlobalTimeChanged())); connect(m_canvas->animationPlayer(), SIGNAL(sigPlaybackStopped()), this, SLOT(updatePlayPauseIcon())); connect(m_canvas->animationPlayer(), SIGNAL(sigPlaybackStarted()), this, SLOT(updatePlayPauseIcon())); connect(m_canvas->animationPlayer(), SIGNAL(sigPlaybackStatisticsUpdated()), this, SLOT(updateDropFramesIcon())); connect(m_animationWidget->doublePlaySpeed, SIGNAL(valueChanged(double)), m_canvas->animationPlayer(), SLOT(slotUpdatePlaybackSpeed(double))); connect(m_canvas->viewManager()->nodeManager(), SIGNAL(sigNodeActivated(KisNodeSP)), this, SLOT(slotCurrentNodeChanged(KisNodeSP))); connect (animation, SIGNAL(sigFullClipRangeChanged()), this, SLOT(updateClipRange())); slotGlobalTimeChanged(); slotCurrentNodeChanged(m_canvas->viewManager()->nodeManager()->activeNode()); } slotUpdateIcons(); } void AnimationDocker::unsetCanvas() { setCanvas(0); } void AnimationDocker::setViewManager(KisViewManager *view) { setActions(view->actionManager()); slotUpdateIcons(); connect(view->mainWindow(), SIGNAL(themeChanged()), this, SLOT(slotUpdateIcons())); m_mainWindow = view->mainWindow(); } void AnimationDocker::slotAddOpacityKeyframe() { addKeyframe(KisKeyframeChannel::Opacity.id(), false); } void AnimationDocker::slotDeleteOpacityKeyframe() { deleteKeyframe(KisKeyframeChannel::Opacity.id()); } void AnimationDocker::slotAddTransformKeyframe() { if (!m_canvas) return; KisTransformMask *mask = dynamic_cast(m_canvas->viewManager()->activeNode().data()); if (!mask) return; const int time = m_canvas->image()->animationInterface()->currentTime(); KUndo2Command *command = new KUndo2Command(kundo2_i18n("Add transform keyframe")); KisTransformMaskParamsFactoryRegistry::instance()->autoAddKeyframe(mask, time, KisTransformMaskParamsInterfaceSP(), command); command->redo(); m_canvas->currentImage()->postExecutionUndoAdapter()->addCommand(toQShared(command)); } void AnimationDocker::slotDeleteTransformKeyframe() { deleteKeyframe(KisKeyframeChannel::TransformArguments.id()); } void AnimationDocker::slotUIRangeChanged() { if (!m_canvas || !m_canvas->image()) return; int fromTime = m_animationWidget->spinFromFrame->value(); int toTime = m_animationWidget->spinToFrame->value(); m_canvas->image()->animationInterface()->setFullClipRange(KisTimeSpan(fromTime, toTime)); } void AnimationDocker::slotUIFramerateChanged() { if (!m_canvas || !m_canvas->image()) return; m_canvas->image()->animationInterface()->setFramerate(m_animationWidget->intFramerate->value()); } void AnimationDocker::slotOnionSkinOptions() { if (m_mainWindow) { QDockWidget *docker = m_mainWindow->dockWidget("OnionSkinsDocker"); if (docker) { docker->setVisible(!docker->isVisible()); } } } void AnimationDocker::slotGlobalTimeChanged() { int time = m_canvas->animationPlayer()->isPlaying() ? m_canvas->animationPlayer()->currentTime() : m_canvas->image()->animationInterface()->currentUITime(); m_animationWidget->intCurrentTime->setValue(time); const int frameRate = m_canvas->image()->animationInterface()->framerate(); const int msec = 1000 * time / frameRate; QTime realTime; realTime = realTime.addMSecs(msec); QString realTimeString = realTime.toString("hh:mm:ss.zzz"); m_animationWidget->intCurrentTime->setToolTip(realTimeString); } void AnimationDocker::slotTimeSpinBoxChanged() { if (!m_canvas || !m_canvas->image()) return; int newTime = m_animationWidget->intCurrentTime->value(); KisImageAnimationInterface *animation = m_canvas->image()->animationInterface(); if (m_canvas->animationPlayer()->isPlaying() || newTime == animation->currentUITime()) { return; } animation->requestTimeSwitchWithUndo(newTime); } void AnimationDocker::slotPreviousFrame() { if (!m_canvas) return; KisImageAnimationInterface *animation = m_canvas->image()->animationInterface(); int time = animation->currentUITime() - 1; if (time >= 0) { animation->requestTimeSwitchWithUndo(time); } } void AnimationDocker::slotNextFrame() { if (!m_canvas) return; KisImageAnimationInterface *animation = m_canvas->image()->animationInterface(); int time = animation->currentUITime() + 1; animation->requestTimeSwitchWithUndo(time); } void AnimationDocker::slotPreviousKeyFrame() { if (!m_canvas) return; KisNodeSP node = m_canvas->viewManager()->activeNode(); if (!node) return; KisImageAnimationInterface *animation = m_canvas->image()->animationInterface(); int time = animation->currentUITime(); KisKeyframeChannel *content = node->getKeyframeChannel(KisKeyframeChannel::Content.id()); if (!content) return; - KisKeyframeSP dstKeyframe; - KisKeyframeSP keyframe = content->keyframeAt(time); + KisKeyframeBaseSP dstKeyframe; + KisKeyframeBaseSP keyframe = content->itemAt(time); if (!keyframe) { - dstKeyframe = content->activeKeyframeAt(time); + dstKeyframe = content->activeItemAt(time); } else { - dstKeyframe = content->previousKeyframe(keyframe); + dstKeyframe = content->previousItem(*keyframe); } if (dstKeyframe) { animation->requestTimeSwitchWithUndo(dstKeyframe->time()); } } void AnimationDocker::slotNextKeyFrame() { if (!m_canvas) return; KisNodeSP node = m_canvas->viewManager()->activeNode(); if (!node) return; KisImageAnimationInterface *animation = m_canvas->image()->animationInterface(); int time = animation->currentUITime(); KisKeyframeChannel *content = node->getKeyframeChannel(KisKeyframeChannel::Content.id()); if (!content) return; - KisKeyframeSP dstKeyframe; - KisKeyframeSP keyframe = content->activeKeyframeAt(time); + KisKeyframeBaseSP dstKeyframe; + KisKeyframeBaseSP keyframe = content->activeItemAt(time); if (keyframe) { - dstKeyframe = content->nextKeyframe(keyframe); + dstKeyframe = content->nextItem(*keyframe); } if (dstKeyframe) { animation->requestTimeSwitchWithUndo(dstKeyframe->time()); } } void AnimationDocker::slotFirstFrame() { if (!m_canvas) return; KisImageAnimationInterface *animation = m_canvas->image()->animationInterface(); animation->requestTimeSwitchWithUndo(0); } void AnimationDocker::slotLastFrame() { if (!m_canvas) return; KisImageAnimationInterface *animation = m_canvas->image()->animationInterface(); animation->requestTimeSwitchWithUndo(animation->totalLength() - 1); } void AnimationDocker::slotPlayPause() { if (!m_canvas) return; if (m_canvas->animationPlayer()->isPlaying()) { m_canvas->animationPlayer()->stop(); } else { m_canvas->animationPlayer()->play(); } updatePlayPauseIcon(); } void AnimationDocker::updatePlayPauseIcon() { bool isPlaying = m_canvas && m_canvas->animationPlayer() && m_canvas->animationPlayer()->isPlaying(); m_playPauseAction->setIcon(isPlaying ? KisIconUtils::loadIcon("animation_stop") : KisIconUtils::loadIcon("animation_play")); } void AnimationDocker::updateLazyFrameIcon() { KisImageConfig cfg(true); const bool value = cfg.lazyFrameCreationEnabled(); m_lazyFrameAction->setIcon(value ? KisIconUtils::loadIcon("lazyframeOn") : KisIconUtils::loadIcon("lazyframeOff")); m_lazyFrameAction->setText(QString("%1 (%2)") .arg(KisAnimationUtils::lazyFrameCreationActionName) .arg(KritaUtils::toLocalizedOnOff(value))); } void AnimationDocker::updateDropFramesIcon() { qreal effectiveFps = 0.0; qreal realFps = 0.0; qreal framesDropped = 0.0; bool isPlaying = false; KisAnimationPlayer *player = m_canvas && m_canvas->animationPlayer() ? m_canvas->animationPlayer() : 0; if (player) { effectiveFps = player->effectiveFps(); realFps = player->realFps(); framesDropped = player->framesDroppedPortion(); isPlaying = player->isPlaying(); } KisConfig cfg(true); const bool value = cfg.animationDropFrames(); m_dropFramesAction->setIcon(value ? KisIconUtils::loadIcon(framesDropped > 0.05 ? "droppedframes" : "dropframe") : KisIconUtils::loadIcon("dropframe")); QString text; if (!isPlaying) { text = QString("%1 (%2)") .arg(KisAnimationUtils::dropFramesActionName) .arg(KritaUtils::toLocalizedOnOff(value)); } else { text = QString("%1 (%2)\n" "%3\n" "%4\n" "%5") .arg(KisAnimationUtils::dropFramesActionName) .arg(KritaUtils::toLocalizedOnOff(value)) .arg(i18n("Effective FPS:\t%1", effectiveFps)) .arg(i18n("Real FPS:\t%1", realFps)) .arg(i18n("Frames dropped:\t%1\%", framesDropped * 100)); } m_dropFramesAction->setText(text); } void AnimationDocker::slotUpdateIcons() { m_previousFrameAction->setIcon(KisIconUtils::loadIcon("prevframe")); m_nextFrameAction->setIcon(KisIconUtils::loadIcon("nextframe")); m_previousKeyFrameAction->setIcon(KisIconUtils::loadIcon("prevkeyframe")); m_nextKeyFrameAction->setIcon(KisIconUtils::loadIcon("nextkeyframe")); m_firstFrameAction->setIcon(KisIconUtils::loadIcon("firstframe")); m_lastFrameAction->setIcon(KisIconUtils::loadIcon("lastframe")); updatePlayPauseIcon(); updateLazyFrameIcon(); updateDropFramesIcon(); m_animationWidget->btnOnionSkinOptions->setIcon(KisIconUtils::loadIcon("onion_skin_options")); m_animationWidget->btnOnionSkinOptions->setIconSize(QSize(22, 22)); m_animationWidget->btnNextKeyFrame->setIconSize(QSize(22, 22)); m_animationWidget->btnPreviousKeyFrame->setIconSize(QSize(22, 22)); m_animationWidget->btnFirstFrame->setIconSize(QSize(22, 22)); m_animationWidget->btnLastFrame->setIconSize(QSize(22, 22)); m_animationWidget->btnPreviousFrame->setIconSize(QSize(22, 22)); m_animationWidget->btnPlay->setIconSize(QSize(22, 22)); m_animationWidget->btnNextFrame->setIconSize(QSize(22, 22)); m_animationWidget->btnAddKeyframe->setIconSize(QSize(22, 22)); m_animationWidget->btnAddDuplicateFrame->setIconSize(QSize(22, 22)); m_animationWidget->btnDeleteKeyframe->setIconSize(QSize(22, 22)); m_animationWidget->btnLazyFrame->setIconSize(QSize(22, 22)); m_animationWidget->btnDropFrames->setIconSize(QSize(22, 22)); } void AnimationDocker::slotLazyFrameChanged(bool value) { KisImageConfig cfg(false); if (value != cfg.lazyFrameCreationEnabled()) { cfg.setLazyFrameCreationEnabled(value); updateLazyFrameIcon(); } } void AnimationDocker::slotDropFramesChanged(bool value) { KisConfig cfg(false); if (value != cfg.animationDropFrames()) { cfg.setAnimationDropFrames(value); updateDropFramesIcon(); } } void AnimationDocker::slotCurrentNodeChanged(KisNodeSP node) { bool isNodeAnimatable = false; m_newKeyframeMenu->clear(); m_deleteKeyframeMenu->clear(); if (!node.isNull()) { if (KisAnimationUtils::supportsContentFrames(node)) { isNodeAnimatable = true; KisActionManager::safePopulateMenu(m_newKeyframeMenu, "add_blank_frame", m_actionManager); KisActionManager::safePopulateMenu(m_deleteKeyframeMenu, "remove_frames", m_actionManager); } if (node->inherits("KisLayer")) { isNodeAnimatable = true; m_newKeyframeMenu->addAction(m_addOpacityKeyframeAction); m_deleteKeyframeMenu->addAction(m_deleteOpacityKeyframeAction); } /* if (node->inherits("KisTransformMask")) { isNodeAnimatable = true; m_newKeyframeMenu->addAction(m_addTransformKeyframeAction); m_deleteKeyframeMenu->addAction(m_deleteTransformKeyframeAction); } */ } m_animationWidget->btnAddKeyframe->setEnabled(isNodeAnimatable); m_animationWidget->btnAddDuplicateFrame->setEnabled(isNodeAnimatable); m_animationWidget->btnDeleteKeyframe->setEnabled(isNodeAnimatable); } void AnimationDocker::updateClipRange() { m_animationWidget->spinFromFrame->setValue(m_canvas->image()->animationInterface()->fullClipRange().start()); m_animationWidget->spinToFrame->setValue(m_canvas->image()->animationInterface()->fullClipRange().end()); } void AnimationDocker::addKeyframe(const QString &channel, bool copy) { if (!m_canvas) return; KisNodeSP node = m_canvas->viewManager()->activeNode(); if (!node) return; const int time = m_canvas->image()->animationInterface()->currentTime(); KisAnimationUtils::createKeyframeLazy(m_canvas->image(), node, channel, time, copy); } void AnimationDocker::deleteKeyframe(const QString &channel) { if (!m_canvas) return; KisNodeSP node = m_canvas->viewManager()->activeNode(); if (!node) return; const int time = m_canvas->image()->animationInterface()->currentTime(); KisAnimationUtils::removeKeyframe(m_canvas->image(), node, channel, time); } void AnimationDocker::setActions(KisActionManager *actionMan) { m_actionManager = actionMan; if (!m_actionManager) return; m_previousFrameAction = new KisAction(i18n("Previous Frame"), m_animationWidget->btnPreviousFrame); m_previousFrameAction->setActivationFlags(KisAction::ACTIVE_IMAGE); m_animationWidget->btnPreviousFrame->setDefaultAction(m_previousFrameAction); m_nextFrameAction = new KisAction(i18n("Next Frame"), m_animationWidget->btnNextFrame); m_nextFrameAction->setActivationFlags(KisAction::ACTIVE_IMAGE); m_animationWidget->btnNextFrame->setDefaultAction(m_nextFrameAction); m_previousKeyFrameAction = new KisAction(i18n("Previous Key Frame"), m_animationWidget->btnPreviousKeyFrame); m_previousKeyFrameAction->setActivationFlags(KisAction::ACTIVE_IMAGE); m_animationWidget->btnPreviousKeyFrame->setDefaultAction(m_previousKeyFrameAction); m_nextKeyFrameAction = new KisAction(i18n("Next Key Frame"), m_animationWidget->btnNextKeyFrame); m_nextKeyFrameAction->setActivationFlags(KisAction::ACTIVE_IMAGE); m_animationWidget->btnNextKeyFrame->setDefaultAction(m_nextKeyFrameAction); m_firstFrameAction = new KisAction(i18n("First Frame"), m_animationWidget->btnFirstFrame); m_firstFrameAction->setActivationFlags(KisAction::ACTIVE_IMAGE); m_animationWidget->btnFirstFrame->setDefaultAction(m_firstFrameAction); m_lastFrameAction = new KisAction(i18n("Last Frame"), m_animationWidget->btnLastFrame); m_lastFrameAction->setActivationFlags(KisAction::ACTIVE_IMAGE); m_animationWidget->btnLastFrame->setDefaultAction(m_lastFrameAction); m_playPauseAction = new KisAction(i18n("Play / Stop"), m_animationWidget->btnPlay); m_playPauseAction->setActivationFlags(KisAction::ACTIVE_IMAGE); m_animationWidget->btnPlay->setDefaultAction(m_playPauseAction); KisAction *action = 0; action = m_actionManager->createAction("add_blank_frame"); m_animationWidget->btnAddKeyframe->setDefaultAction(action); action = m_actionManager->createAction("add_duplicate_frame"); m_animationWidget->btnAddDuplicateFrame->setDefaultAction(action); action = m_actionManager->createAction("remove_frames"); m_animationWidget->btnDeleteKeyframe->setDefaultAction(action); m_newKeyframeMenu = new QMenu(this); m_animationWidget->btnAddKeyframe->setMenu(m_newKeyframeMenu); m_animationWidget->btnAddKeyframe->setPopupMode(QToolButton::MenuButtonPopup); m_deleteKeyframeMenu = new QMenu(this); m_animationWidget->btnDeleteKeyframe->setMenu(m_deleteKeyframeMenu); m_animationWidget->btnDeleteKeyframe->setPopupMode(QToolButton::MenuButtonPopup); m_addOpacityKeyframeAction = new KisAction(KisAnimationUtils::addOpacityKeyframeActionName); m_deleteOpacityKeyframeAction = new KisAction(KisAnimationUtils::removeOpacityKeyframeActionName); m_addTransformKeyframeAction = new KisAction(KisAnimationUtils::addTransformKeyframeActionName); m_deleteTransformKeyframeAction = new KisAction(KisAnimationUtils::removeTransformKeyframeActionName); // other new stuff m_actionManager->addAction("previous_frame", m_previousFrameAction); m_actionManager->addAction("next_frame", m_nextFrameAction); m_actionManager->addAction("previous_keyframe", m_previousKeyFrameAction); m_actionManager->addAction("next_keyframe", m_nextKeyFrameAction); m_actionManager->addAction("first_frame", m_firstFrameAction); m_actionManager->addAction("last_frame", m_lastFrameAction); { KisImageConfig cfg(true); setupActionButton(KisAnimationUtils::lazyFrameCreationActionName, KisAction::ACTIVE_IMAGE, cfg.lazyFrameCreationEnabled(), m_animationWidget->btnLazyFrame, &m_lazyFrameAction); } { KisConfig cfg(true); setupActionButton(KisAnimationUtils::dropFramesActionName, KisAction::ACTIVE_IMAGE, cfg.animationDropFrames(), m_animationWidget->btnDropFrames, &m_dropFramesAction); } // these actions are created in the setupActionButton() above, so we need to add actions after that m_actionManager->addAction("lazy_frame", m_lazyFrameAction); m_actionManager->addAction("drop_frames", m_dropFramesAction); m_actionManager->addAction("toggle_playback", m_playPauseAction); QFont font; font.setPointSize(1.7 * font.pointSize()); font.setBold(true); m_animationWidget->intCurrentTime->setFont(font); connect(m_previousFrameAction, SIGNAL(triggered()), this, SLOT(slotPreviousFrame())); connect(m_nextFrameAction, SIGNAL(triggered()), this, SLOT(slotNextFrame())); connect(m_previousKeyFrameAction, SIGNAL(triggered()), this, SLOT(slotPreviousKeyFrame())); connect(m_nextKeyFrameAction, SIGNAL(triggered()), this, SLOT(slotNextKeyFrame())); connect(m_firstFrameAction, SIGNAL(triggered()), this, SLOT(slotFirstFrame())); connect(m_lastFrameAction, SIGNAL(triggered()), this, SLOT(slotLastFrame())); connect(m_playPauseAction, SIGNAL(triggered()), this, SLOT(slotPlayPause())); connect(m_lazyFrameAction, SIGNAL(toggled(bool)), this, SLOT(slotLazyFrameChanged(bool))); connect(m_dropFramesAction, SIGNAL(toggled(bool)), this, SLOT(slotDropFramesChanged(bool))); connect(m_addOpacityKeyframeAction, SIGNAL(triggered(bool)), this, SLOT(slotAddOpacityKeyframe())); connect(m_deleteOpacityKeyframeAction, SIGNAL(triggered(bool)), this, SLOT(slotDeleteOpacityKeyframe())); connect(m_addTransformKeyframeAction, SIGNAL(triggered(bool)), this, SLOT(slotAddTransformKeyframe())); connect(m_deleteTransformKeyframeAction, SIGNAL(triggered(bool)), this, SLOT(slotDeleteTransformKeyframe())); m_animationWidget->btnOnionSkinOptions->setToolTip(i18n("Onion Skins")); connect(m_animationWidget->btnOnionSkinOptions, SIGNAL(clicked()), this, SLOT(slotOnionSkinOptions())); connect(m_animationWidget->spinFromFrame, SIGNAL(valueChanged(int)), this, SLOT(slotUIRangeChanged())); connect(m_animationWidget->spinToFrame, SIGNAL(valueChanged(int)), this, SLOT(slotUIRangeChanged())); connect(m_animationWidget->intFramerate, SIGNAL(valueChanged(int)), this, SLOT(slotUIFramerateChanged())); connect(m_animationWidget->intCurrentTime, SIGNAL(valueChanged(int)), SLOT(slotTimeSpinBoxChanged())); } diff --git a/plugins/dockers/animation/kis_animation_utils.cpp b/plugins/dockers/animation/kis_animation_utils.cpp index d0f3f8ccfa..739a6059e3 100644 --- a/plugins/dockers/animation/kis_animation_utils.cpp +++ b/plugins/dockers/animation/kis_animation_utils.cpp @@ -1,351 +1,351 @@ /* * Copyright (c) 2015 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_animation_utils.h" #include "kundo2command.h" #include "kis_algebra_2d.h" #include "kis_image.h" #include "kis_node.h" #include "kis_keyframe_channel.h" #include "kis_post_execution_undo_adapter.h" #include "kis_global.h" #include "kis_tool_utils.h" #include "kis_image_animation_interface.h" #include "kis_command_utils.h" #include "kis_processing_applicator.h" #include "kis_transaction.h" namespace KisAnimationUtils { const QString addFrameActionName = i18n("New Frame"); const QString duplicateFrameActionName = i18n("Copy Frame"); const QString removeFrameActionName = i18n("Remove Frame"); const QString removeFramesActionName = i18n("Remove Frames"); const QString lazyFrameCreationActionName = i18n("Auto Frame Mode"); const QString dropFramesActionName = i18n("Drop Frames"); const QString showLayerActionName = i18n("Show in Timeline"); const QString newLayerActionName = i18n("New Layer"); const QString addExistingLayerActionName = i18n("Add Existing Layer"); const QString removeLayerActionName = i18n("Remove Layer"); const QString addOpacityKeyframeActionName = i18n("Add opacity keyframe"); const QString addTransformKeyframeActionName = i18n("Add transform keyframe"); const QString removeOpacityKeyframeActionName = i18n("Remove opacity keyframe"); const QString removeTransformKeyframeActionName = i18n("Remove transform keyframe"); KUndo2Command* createKeyframeCommand(KisImageSP image, KisNodeSP node, const QString &channelId, int time, bool copy, KUndo2Command *parentCommand) { KUndo2Command *cmd = new KisCommandUtils::LambdaCommand( copy ? kundo2_i18n("Copy Keyframe") : kundo2_i18n("Add Keyframe"), parentCommand, [image, node, channelId, time, copy] () mutable -> KUndo2Command* { bool result = false; QScopedPointer cmd(new KUndo2Command()); KisKeyframeChannel *channel = node->getKeyframeChannel(channelId); bool createdChannel = false; if (!channel) { node->enableAnimation(); channel = node->getKeyframeChannel(channelId, true); if (!channel) return nullptr; createdChannel = true; } if (copy) { if (!channel->itemAt(time)) { KisKeyframeBaseSP srcFrame = channel->activeItemAt(time); channel->copyItem(srcFrame, time, cmd.data()); result = true; } } else { if (channel->keyframeAt(time) && !createdChannel) { if (image->animationInterface()->currentTime() == time && channelId == KisKeyframeChannel::Content.id()) { //shortcut: clearing the image instead KisPaintDeviceSP device = node->paintDevice(); if (device) { KisTransaction transaction(kundo2_i18n("Clear"), device, cmd.data()); device->clear(); (void) transaction.endAndTake(); // saved as 'parent' result = true; } } } else { channel->addKeyframe(time, cmd.data()); result = true; } } return result ? new KisCommandUtils::SkipFirstRedoWrapper(cmd.take()) : nullptr; }); return cmd; } void createKeyframeLazy(KisImageSP image, KisNodeSP node, const QString &channelId, int time, bool copy) { KUndo2Command *cmd = createKeyframeCommand(image, node, channelId, time, copy); KisProcessingApplicator::runSingleCommandStroke(image, cmd, KisStrokeJobData::BARRIER); } void removeKeyframes(KisImageSP image, const FrameItemList &frames) { KIS_SAFE_ASSERT_RECOVER_RETURN(!image->locked()); KUndo2Command *cmd = new KisCommandUtils::LambdaCommand( kundo2_i18np("Remove Keyframe", "Remove Keyframes", frames.size()), [image, frames] () { bool result = false; QScopedPointer cmd(new KUndo2Command()); Q_FOREACH (const FrameItem &item, frames) { const int time = item.time; KisNodeSP node = item.node; KisKeyframeChannel *channel = 0; if (node) { channel = node->getKeyframeChannel(item.channel); } if (!channel) continue; - KisKeyframeSP keyframe = channel->keyframeAt(time); + KisKeyframeBaseSP keyframe = channel->itemAt(time); if (!keyframe) continue; channel->deleteKeyframe(keyframe, cmd.data()); result = true; } return result ? new KisCommandUtils::SkipFirstRedoWrapper(cmd.take()) : 0; }); KisProcessingApplicator::runSingleCommandStroke(image, cmd, KisStrokeJobData::BARRIER); } void removeKeyframe(KisImageSP image, KisNodeSP node, const QString &channel, int time) { QVector frames; frames << FrameItem(node, channel, time); removeKeyframes(image, frames); } struct LessOperator { LessOperator(const QPoint &offset) : m_columnCoeff(-KisAlgebra2D::signPZ(offset.x())), m_rowCoeff(-1000000 * KisAlgebra2D::signZZ(offset.y())) { } bool operator()(const QModelIndex &lhs, const QModelIndex &rhs) { return m_columnCoeff * lhs.column() + m_rowCoeff * lhs.row() < m_columnCoeff * rhs.column() + m_rowCoeff * rhs.row(); } private: int m_columnCoeff; int m_rowCoeff; }; void sortPointsForSafeMove(QModelIndexList *points, const QPoint &offset) { std::sort(points->begin(), points->end(), LessOperator(offset)); } bool supportsContentFrames(KisNodeSP node) { return node->inherits("KisPaintLayer") || node->inherits("KisFilterMask") || node->inherits("KisTransparencyMask") || node->inherits("KisSelectionBasedLayer"); } void swapOneFrameItem(const FrameItem &src, const FrameItem &dst, KUndo2Command *parentCommand) { const int srcTime = src.time; KisNodeSP srcNode = src.node; KisKeyframeChannel *srcChannel = srcNode->getKeyframeChannel(src.channel); const int dstTime = dst.time; KisNodeSP dstNode = dst.node; KisKeyframeChannel *dstChannel = dstNode->getKeyframeChannel(dst.channel, true); if (srcNode == dstNode) { if (!srcChannel) return; // TODO: add warning! srcChannel->swapFrames(srcTime, dstTime, parentCommand); } else { if (!srcChannel || !dstChannel) return; // TODO: add warning! dstChannel->swapExternalKeyframe(srcChannel, srcTime, dstTime, parentCommand); } } void moveOneFrameItem(const FrameItem &src, const FrameItem &dst, KeyframeAction action, bool moveEmptyFrames, KUndo2Command *parentCommand) { const int srcTime = src.time; KisNodeSP srcNode = src.node; KisKeyframeChannel *srcChannel = srcNode->getKeyframeChannel(src.channel); const int dstTime = dst.time; KisNodeSP dstNode = dst.node; KisKeyframeChannel *dstChannel = dstNode->getKeyframeChannel(dst.channel, true); if (srcNode == dstNode) { if (!srcChannel) return; // TODO: add warning! - KisKeyframeSP srcKeyframe = srcChannel->keyframeAt(srcTime); - KisKeyframeSP dstKeyFrame = srcChannel->keyframeAt(dstTime); + KisKeyframeBaseSP srcKeyframe = srcChannel->itemAt(srcTime); + KisKeyframeBaseSP dstKeyFrame = srcChannel->itemAt(dstTime); if (srcKeyframe) { if (action == CopyKeyframes) { - srcChannel->copyKeyframe(srcKeyframe, dstTime, parentCommand); + srcChannel->copyItem(srcKeyframe, dstTime, parentCommand); } else if (action == LinkKeyframes) { srcChannel->linkKeyframe(srcKeyframe, dstTime, parentCommand); } else { srcChannel->moveKeyframe(srcKeyframe, dstTime, parentCommand); } } else { if (dstKeyFrame && moveEmptyFrames && action != CopyKeyframes) { //Destination is effectively replaced by an empty frame. dstChannel->deleteKeyframe(dstKeyFrame, parentCommand); } } } else { if (!srcChannel || !dstChannel) return; // TODO: add warning! KisKeyframeSP srcKeyframe = srcChannel->keyframeAt(srcTime); if (!srcKeyframe) return; // TODO: add warning! dstChannel->copyExternalKeyframe(srcChannel, srcTime, dstTime, parentCommand); if (action == MoveKeyframes) { srcChannel->deleteKeyframe(srcKeyframe, parentCommand); } } } KUndo2Command* createMoveKeyframesCommand(const FrameItemList &srcFrames, const FrameItemList &dstFrames, bool copy, bool moveEmpty, KUndo2Command *parentCommand) { FrameMovePairList srcDstPairs; for (int i = 0; i < srcFrames.size(); i++) { srcDstPairs << std::make_pair(srcFrames[i], dstFrames[i]); } return createMoveKeyframesCommand(srcDstPairs, copy ? CopyKeyframes : MoveKeyframes, moveEmpty, parentCommand); } KUndo2Command* createMoveKeyframesCommand(const FrameMovePairList &srcDstPairs, KeyframeAction action, bool moveEmptyFrames, KUndo2Command *parentCommand) { KUndo2Command *cmd = new KisCommandUtils::LambdaCommand( (action == CopyKeyframes) ? kundo2_i18np("Copy Keyframe", "Copy %1 Keyframes", srcDstPairs.size()) : (action == LinkKeyframes) ? kundo2_i18np("Link Keyframe", "Link %1 Keyframes", srcDstPairs.size()) : kundo2_i18np("Move Keyframe", "Move %1 Keyframes", srcDstPairs.size()), parentCommand, [srcDstPairs, action, moveEmptyFrames] () -> KUndo2Command* { bool result = false; QScopedPointer cmd(new KUndo2Command()); using MoveChain = QList; QHash moveMap; Q_FOREACH (const FrameMovePair &pair, srcDstPairs) { moveMap.insert(pair.first, {pair.second}); } auto it = moveMap.begin(); while (it != moveMap.end()) { MoveChain &chain = it.value(); const FrameItem &previousFrame = chain.last(); auto tailIt = moveMap.find(previousFrame); if (tailIt == it || tailIt == moveMap.end()) { ++it; continue; } chain.append(tailIt.value()); tailIt = moveMap.erase(tailIt); // no incrementing! we are going to check the new tail now! } for (it = moveMap.begin(); it != moveMap.end(); ++it) { MoveChain &chain = it.value(); chain.prepend(it.key()); KIS_SAFE_ASSERT_RECOVER(chain.size() > 1) { continue; } bool isCycle = false; if (chain.last() == chain.first()) { isCycle = true; chain.takeLast(); } auto frameIt = chain.rbegin(); FrameItem dstItem = *frameIt++; while (frameIt != chain.rend()) { FrameItem srcItem = *frameIt++; if (!isCycle) { moveOneFrameItem(srcItem, dstItem, action, moveEmptyFrames, cmd.data()); } else { swapOneFrameItem(srcItem, dstItem, cmd.data()); } dstItem = srcItem; result = true; } } return result ? new KisCommandUtils::SkipFirstRedoWrapper(cmd.take()) : 0; }); return cmd; } QDebug operator<<(QDebug dbg, const FrameItem &item) { dbg.nospace() << "FrameItem(" << item.node->name() << ", " << item.channel << ", " << item.time << ")"; return dbg.space(); } } diff --git a/plugins/dockers/animation/kis_time_based_item_model.cpp b/plugins/dockers/animation/kis_time_based_item_model.cpp index d4bd62aabd..a5057f03ec 100644 --- a/plugins/dockers/animation/kis_time_based_item_model.cpp +++ b/plugins/dockers/animation/kis_time_based_item_model.cpp @@ -1,518 +1,518 @@ /* * Copyright (c) 2016 Jouni Pentikäinen * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_time_based_item_model.h" #include #include #include "kis_animation_frame_cache.h" #include "kis_animation_player.h" #include "kis_signal_compressor_with_param.h" #include "kis_image.h" #include "kis_image_animation_interface.h" #include "kis_time_range.h" #include "kis_animation_utils.h" #include "kis_keyframe_channel.h" #include "kis_processing_applicator.h" #include "KisImageBarrierLockerWithFeedback.h" #include "commands_new/kis_switch_current_time_command.h" #include "kis_command_utils.h" struct KisTimeBasedItemModel::Private { Private() : animationPlayer(0) , numFramesOverride(0) , activeFrameIndex(0) , scrubInProgress(false) , scrubStartFrame(-1) {} KisImageWSP image; KisAnimationFrameCacheWSP framesCache; QPointer animationPlayer; QVector cachedFrames; int numFramesOverride; int activeFrameIndex; bool scrubInProgress; int scrubStartFrame; QScopedPointer > scrubbingCompressor; int baseNumFrames() const { auto imageSP = image.toStrongRef(); if (!imageSP) return 0; KisImageAnimationInterface *i = imageSP->animationInterface(); if (!i) return 1; return i->totalLength(); } int effectiveNumFrames() const { if (image.isNull()) return 0; return qMax(baseNumFrames(), numFramesOverride); } int framesPerSecond() { return image->animationInterface()->framerate(); } }; KisTimeBasedItemModel::KisTimeBasedItemModel(QObject *parent) : QAbstractTableModel(parent) , m_d(new Private()) { KisConfig cfg(true); using namespace std::placeholders; std::function callback( std::bind(&KisTimeBasedItemModel::slotInternalScrubPreviewRequested, this, _1)); m_d->scrubbingCompressor.reset( new KisSignalCompressorWithParam(cfg.scrubbingUpdatesDelay(), callback, KisSignalCompressor::FIRST_ACTIVE)); } KisTimeBasedItemModel::~KisTimeBasedItemModel() {} void KisTimeBasedItemModel::setImage(KisImageWSP image) { KisImageWSP oldImage = m_d->image; m_d->image = image; if (image) { KisImageAnimationInterface *ai = image->animationInterface(); slotCurrentTimeChanged(ai->currentUITime()); connect(ai, SIGNAL(sigFramerateChanged()), SLOT(slotFramerateChanged())); connect(ai, SIGNAL(sigUiTimeChanged(int)), SLOT(slotCurrentTimeChanged(int))); } if (image != oldImage) { beginResetModel(); endResetModel(); } } void KisTimeBasedItemModel::setFrameCache(KisAnimationFrameCacheSP cache) { if (KisAnimationFrameCacheSP(m_d->framesCache) == cache) return; if (m_d->framesCache) { m_d->framesCache->disconnect(this); } m_d->framesCache = cache; if (m_d->framesCache) { connect(m_d->framesCache, SIGNAL(changed()), SLOT(slotCacheChanged())); } } void KisTimeBasedItemModel::setAnimationPlayer(KisAnimationPlayer *player) { if (m_d->animationPlayer == player) return; if (m_d->animationPlayer) { m_d->animationPlayer->disconnect(this); } m_d->animationPlayer = player; if (m_d->animationPlayer) { connect(m_d->animationPlayer, SIGNAL(sigPlaybackStopped()), SLOT(slotPlaybackStopped())); connect(m_d->animationPlayer, SIGNAL(sigFrameChanged()), SLOT(slotPlaybackFrameChanged())); } } void KisTimeBasedItemModel::setLastVisibleFrame(int time) { const int growThreshold = m_d->effectiveNumFrames() - 3; const int growValue = time + 8; const int shrinkThreshold = m_d->effectiveNumFrames() - 12; const int shrinkValue = qMax(m_d->baseNumFrames(), qMin(growValue, shrinkThreshold)); const bool canShrink = m_d->effectiveNumFrames() > m_d->baseNumFrames(); if (time >= growThreshold) { beginInsertColumns(QModelIndex(), m_d->effectiveNumFrames(), growValue - 1); m_d->numFramesOverride = growValue; endInsertColumns(); } else if (time < shrinkThreshold && canShrink) { beginRemoveColumns(QModelIndex(), shrinkValue, m_d->effectiveNumFrames() - 1); m_d->numFramesOverride = shrinkValue; endRemoveColumns(); } } int KisTimeBasedItemModel::columnCount(const QModelIndex &parent) const { Q_UNUSED(parent); return m_d->effectiveNumFrames(); } QVariant KisTimeBasedItemModel::data(const QModelIndex &index, int role) const { switch (role) { case ActiveFrameRole: { return index.column() == m_d->activeFrameIndex; } } return QVariant(); } bool KisTimeBasedItemModel::setData(const QModelIndex &index, const QVariant &value, int role) { if (!index.isValid()) return false; switch (role) { case ActiveFrameRole: { setHeaderData(index.column(), Qt::Horizontal, value, role); break; } } return false; } QVariant KisTimeBasedItemModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal) { switch (role) { case ActiveFrameRole: return section == m_d->activeFrameIndex; case FrameCachedRole: return m_d->cachedFrames.size() > section ? m_d->cachedFrames[section] : false; case FramesPerSecondRole: return m_d->framesPerSecond(); } } return QVariant(); } bool KisTimeBasedItemModel::setHeaderData(int section, Qt::Orientation orientation, const QVariant &value, int role) { if (orientation == Qt::Horizontal) { switch (role) { case ActiveFrameRole: if (value.toBool() && section != m_d->activeFrameIndex) { int prevFrame = m_d->activeFrameIndex; m_d->activeFrameIndex = section; scrubTo(m_d->activeFrameIndex, m_d->scrubInProgress); /** * Optimization Hack Alert: * * ideally, we should emit all four signals, but... The * point is this code is used in a tight loop during * playback, so it should run as fast as possible. To tell * the story short, commenting out these three lines makes * playback run 15% faster ;) */ if (m_d->scrubInProgress) { //emit dataChanged(this->index(0, prevFrame), this->index(rowCount() - 1, prevFrame)); emit dataChanged(this->index(0, m_d->activeFrameIndex), this->index(rowCount() - 1, m_d->activeFrameIndex)); //emit headerDataChanged (Qt::Horizontal, prevFrame, prevFrame); //emit headerDataChanged (Qt::Horizontal, m_d->activeFrameIndex, m_d->activeFrameIndex); } else { emit dataChanged(this->index(0, prevFrame), this->index(rowCount() - 1, prevFrame)); emit dataChanged(this->index(0, m_d->activeFrameIndex), this->index(rowCount() - 1, m_d->activeFrameIndex)); emit headerDataChanged (Qt::Horizontal, prevFrame, prevFrame); emit headerDataChanged (Qt::Horizontal, m_d->activeFrameIndex, m_d->activeFrameIndex); } } } } return false; } bool KisTimeBasedItemModel::removeFrames(const QModelIndexList &indexes) { KisAnimationUtils::FrameItemList frameItems; { KisImageBarrierLockerWithFeedback locker(m_d->image); Q_FOREACH (const QModelIndex &index, indexes) { int time = index.column(); Q_FOREACH(KisKeyframeChannel *channel, channelsAt(index)) { - if (channel->keyframeAt(time)) { + if (channel->itemAt(time)) { frameItems << KisAnimationUtils::FrameItem(channel->node(), channel->id(), index.column()); } } } } if (frameItems.isEmpty()) return false; KisAnimationUtils::removeKeyframes(m_d->image, frameItems); return true; } KUndo2Command* KisTimeBasedItemModel::createOffsetFramesCommand(QModelIndexList srcIndexes, const QPoint &offset, bool copyFrames, bool moveEmptyFrames, KUndo2Command *parentCommand) { if (srcIndexes.isEmpty()) return 0; if (offset.isNull()) return 0; KisAnimationUtils::sortPointsForSafeMove(&srcIndexes, offset); KisAnimationUtils::FrameItemList srcFrameItems; KisAnimationUtils::FrameItemList dstFrameItems; Q_FOREACH (const QModelIndex &srcIndex, srcIndexes) { QModelIndex dstIndex = index( srcIndex.row() + offset.y(), srcIndex.column() + offset.x()); KisNodeSP srcNode = nodeAt(srcIndex); KisNodeSP dstNode = nodeAt(dstIndex); if (!srcNode || !dstNode) return 0; Q_FOREACH(KisKeyframeChannel *channel, channelsAt(srcIndex)) { - if (moveEmptyFrames || channel->keyframeAt(srcIndex.column())) { + if (moveEmptyFrames || channel->itemAt(srcIndex.column())) { srcFrameItems << KisAnimationUtils::FrameItem(srcNode, channel->id(), srcIndex.column()); dstFrameItems << KisAnimationUtils::FrameItem(dstNode, channel->id(), dstIndex.column()); } } } KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(srcFrameItems.size() == dstFrameItems.size(), 0); if (srcFrameItems.isEmpty()) return 0; return KisAnimationUtils::createMoveKeyframesCommand(srcFrameItems, dstFrameItems, copyFrames, moveEmptyFrames, parentCommand); } bool KisTimeBasedItemModel::removeFramesAndOffset(QModelIndexList indicesToRemove) { if (indicesToRemove.isEmpty()) return true; std::sort(indicesToRemove.begin(), indicesToRemove.end(), [] (const QModelIndex &lhs, const QModelIndex &rhs) { return lhs.column() > rhs.column(); }); const int minColumn = indicesToRemove.last().column(); KUndo2Command *parentCommand = new KUndo2Command(kundo2_i18np("Remove frame and shift", "Remove %1 frames and shift", indicesToRemove.size())); { KisImageBarrierLockerWithFeedback locker(m_d->image); Q_FOREACH (const QModelIndex &index, indicesToRemove) { QModelIndexList indicesToOffset; for (int column = index.column() + 1; column < columnCount(); column++) { indicesToOffset << this->index(index.row(), column); } createOffsetFramesCommand(indicesToOffset, QPoint(-1, 0), false, true, parentCommand); } const int oldTime = m_d->image->animationInterface()->currentUITime(); const int newTime = minColumn; new KisSwitchCurrentTimeCommand(m_d->image->animationInterface(), oldTime, newTime, parentCommand); } KisProcessingApplicator::runSingleCommandStroke(m_d->image, parentCommand, KisStrokeJobData::BARRIER); return true; } bool KisTimeBasedItemModel::mirrorFrames(QModelIndexList indexes) { QScopedPointer parentCommand(new KUndo2Command(kundo2_i18n("Mirror Frames"))); { KisImageBarrierLockerWithFeedback locker(m_d->image); QMap rowsList; Q_FOREACH (const QModelIndex &index, indexes) { rowsList[index.row()].append(index); } Q_FOREACH (int row, rowsList.keys()) { QModelIndexList &list = rowsList[row]; KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!list.isEmpty(), false); std::sort(list.begin(), list.end(), [] (const QModelIndex &lhs, const QModelIndex &rhs) { return lhs.column() < rhs.column(); }); auto srcIt = list.begin(); auto dstIt = list.end(); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(srcIt != dstIt, false); --dstIt; QList channels = channelsAt(*srcIt).values(); while (srcIt < dstIt) { Q_FOREACH (KisKeyframeChannel *channel, channels) { channel->swapFrames(srcIt->column(), dstIt->column(), parentCommand.data()); } srcIt++; dstIt--; } } } KisProcessingApplicator::runSingleCommandStroke(m_d->image, new KisCommandUtils::SkipFirstRedoWrapper(parentCommand.take()), KisStrokeJobData::BARRIER); return true; } void KisTimeBasedItemModel::slotInternalScrubPreviewRequested(int time) { if (m_d->animationPlayer && !m_d->animationPlayer->isPlaying()) { m_d->animationPlayer->displayFrame(time); } } void KisTimeBasedItemModel::setScrubState(bool active) { if (!m_d->scrubInProgress && active) { m_d->scrubStartFrame = m_d->activeFrameIndex; m_d->scrubInProgress = true; } if (m_d->scrubInProgress && !active) { m_d->scrubInProgress = false; if (m_d->scrubStartFrame >= 0 && m_d->scrubStartFrame != m_d->activeFrameIndex) { scrubTo(m_d->activeFrameIndex, false); } m_d->scrubStartFrame = -1; } } void KisTimeBasedItemModel::scrubTo(int time, bool preview) { if (m_d->animationPlayer && m_d->animationPlayer->isPlaying()) return; KIS_ASSERT_RECOVER_RETURN(m_d->image); if (preview) { if (m_d->animationPlayer) { m_d->scrubbingCompressor->start(time); } } else { m_d->image->animationInterface()->requestTimeSwitchWithUndo(time); } } void KisTimeBasedItemModel::slotFramerateChanged() { emit headerDataChanged(Qt::Horizontal, 0, columnCount() - 1); } void KisTimeBasedItemModel::slotCurrentTimeChanged(int time) { if (time != m_d->activeFrameIndex) { setHeaderData(time, Qt::Horizontal, true, ActiveFrameRole); } } void KisTimeBasedItemModel::slotCacheChanged() { const int numFrames = columnCount(); m_d->cachedFrames.resize(numFrames); for (int i = 0; i < numFrames; i++) { m_d->cachedFrames[i] = m_d->framesCache->frameStatus(i) == KisAnimationFrameCache::Cached; } emit headerDataChanged(Qt::Horizontal, 0, numFrames); } void KisTimeBasedItemModel::slotPlaybackFrameChanged() { if (!m_d->animationPlayer->isPlaying()) return; setData(index(0, m_d->animationPlayer->currentTime()), true, ActiveFrameRole); } void KisTimeBasedItemModel::slotPlaybackStopped() { setData(index(0, m_d->image->animationInterface()->currentUITime()), true, ActiveFrameRole); } void KisTimeBasedItemModel::setPlaybackRange(const KisTimeSpan &range) { if (m_d->image.isNull()) return; KisImageAnimationInterface *i = m_d->image->animationInterface(); i->setPlaybackRange(range); } bool KisTimeBasedItemModel::isPlaybackActive() const { return m_d->animationPlayer && m_d->animationPlayer->isPlaying(); } int KisTimeBasedItemModel::currentTime() const { return m_d->image->animationInterface()->currentUITime(); } KisImageWSP KisTimeBasedItemModel::image() const { return m_d->image; } int KisTimeBasedItemModel::activeFrameIndex() const { return m_d->activeFrameIndex; } diff --git a/plugins/dockers/animation/timeline_frames_model.cpp b/plugins/dockers/animation/timeline_frames_model.cpp index 5f69c09da8..3690d3bcee 100644 --- a/plugins/dockers/animation/timeline_frames_model.cpp +++ b/plugins/dockers/animation/timeline_frames_model.cpp @@ -1,1119 +1,1114 @@ /* * Copyright (c) 2015 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "timeline_frames_model.h" #include #include #include #include #include #include #include "kis_layer.h" #include "kis_config.h" #include "kis_global.h" #include "kis_debug.h" #include "kis_image.h" #include "kis_image_animation_interface.h" #include "kis_undo_adapter.h" #include "kis_node_dummies_graph.h" #include "kis_dummies_facade_base.h" #include "kis_signal_compressor.h" #include "kis_signal_compressor_with_param.h" #include "kis_keyframe_channel.h" #include "kis_keyframe_commands.h" #include "kundo2command.h" #include "kis_post_execution_undo_adapter.h" #include #include #include "kis_animation_utils.h" #include "timeline_color_scheme.h" #include "kis_node_model.h" #include "kis_projection_leaf.h" #include "kis_time_range.h" #include "kis_animation_cycle.h" #include "kis_node_view_color_scheme.h" #include "krita_utils.h" #include #include "kis_processing_applicator.h" #include #include "kis_node_uuid_info.h" struct TimelineFramesModel::Private { Private() : activeLayerIndex(0), dummiesFacade(0), needFinishInsertRows(false), needFinishRemoveRows(false), updateTimer(200, KisSignalCompressor::FIRST_INACTIVE), parentOfRemovedNode(0) {} int activeLayerIndex; QPointer dummiesFacade; KisImageWSP image; bool needFinishInsertRows; bool needFinishRemoveRows; QList updateQueue; KisSignalCompressor updateTimer; KisNodeDummy* parentOfRemovedNode; QScopedPointer converter; QScopedPointer nodeInterface; QPersistentModelIndex lastClickedIndex; QVariant layerName(int row) const { KisNodeDummy *dummy = converter->dummyFromRow(row); if (!dummy) return QVariant(); return dummy->node()->name(); } bool layerEditable(int row) const { KisNodeDummy *dummy = converter->dummyFromRow(row); if (!dummy) return true; return dummy->node()->visible() && !dummy->node()->userLocked(); } bool frameExists(int row, int column) const { - KisNodeDummy *dummy = converter->dummyFromRow(row); - if (!dummy) return false; + KisKeyframeChannel *primaryChannel = getChannel(row, KisKeyframeChannel::Content.id()); - KisKeyframeChannel *primaryChannel = dummy->node()->getKeyframeChannel(KisKeyframeChannel::Content.id()); - return (primaryChannel && primaryChannel->keyframeAt(column)); + return (primaryChannel && primaryChannel->itemAt(column)); } bool frameHasContent(int row, int column) { - - KisNodeDummy *dummy = converter->dummyFromRow(row); - - KisKeyframeChannel *primaryChannel = dummy->node()->getKeyframeChannel(KisKeyframeChannel::Content.id()); + KisKeyframeChannel *primaryChannel = getChannel(row, KisKeyframeChannel::Content.id()); if (!primaryChannel) return false; // first check if we are a key frame - KisKeyframeSP frame = primaryChannel->activeKeyframeAt(column); + KisKeyframeBaseSP frame = primaryChannel->activeItemAt(column); if (!frame) return false; - return frame->hasContent(); + const KisKeyframe *keyframe = dynamic_cast(frame.data()); + return keyframe && keyframe->hasContent(); } bool specialKeyframeExists(int row, int column) { KisNodeDummy *dummy = converter->dummyFromRow(row); if (!dummy) return false; Q_FOREACH(KisKeyframeChannel *channel, dummy->node()->keyframeChannels()) { if (channel->id() != KisKeyframeChannel::Content.id() && channel->keyframeAt(column)) { return true; } } return false; } bool isIdenticalWith(int row, int column, int frame) { KisNodeDummy *dummy = converter->dummyFromRow(row); if (!dummy) return false; Q_FOREACH(KisKeyframeChannel *channel, dummy->node()->keyframeChannels()) { if (channel->areFramesIdentical(column, frame)) { return true; } } return false; } int frameColorLabel(int row, int column) { - KisNodeDummy *dummy = converter->dummyFromRow(row); - if (!dummy) return -1; - - KisKeyframeChannel *primaryChannel = dummy->node()->getKeyframeChannel(KisKeyframeChannel::Content.id()); + KisKeyframeChannel *primaryChannel = getChannel(row, KisKeyframeChannel::Content.id()); if (!primaryChannel) return -1; - KisKeyframeSP frame = primaryChannel->activeKeyframeAt(column); + KisKeyframeSP frame = primaryChannel->visibleKeyframeAt(column); if (!frame) return -1; return frame->colorLabel(); } void setFrameColorLabel(int row, int column, int color) { - KisNodeDummy *dummy = converter->dummyFromRow(row); - if (!dummy) return; - - KisKeyframeChannel *primaryChannel = dummy->node()->getKeyframeChannel(KisKeyframeChannel::Content.id()); + KisKeyframeChannel *primaryChannel = getChannel(row, KisKeyframeChannel::Content.id()); if (!primaryChannel) return; - KisKeyframeSP frame = primaryChannel->keyframeAt(column); + KisKeyframeSP frame = primaryChannel->visibleKeyframeAt(column); if (!frame) return; frame->setColorLabel(color); } int layerColorLabel(int row) const { KisNodeDummy *dummy = converter->dummyFromRow(row); if (!dummy) return -1; return dummy->node()->colorLabelIndex(); } CycleMode frameCycleMode(int row, int time) const{ - KisNodeDummy *dummy = converter->dummyFromRow(row); - if (!dummy) return NoCycle; - - KisKeyframeChannel *primaryChannel = dummy->node()->getKeyframeChannel(KisKeyframeChannel::Content.id()); + KisKeyframeChannel *primaryChannel = getChannel(row, KisKeyframeChannel::Content.id()); if (!primaryChannel) return NoCycle; - KisTimeSpan cycle = primaryChannel->cycledRangeAt(time); - KisKeyframeSP keyframe = primaryChannel->activeKeyframeAt(time); - KisRepeatFrame *repeatFrame = dynamic_cast(keyframe.data()); + const QSharedPointer cycle = primaryChannel->cycleAt(time); + const QSharedPointer repeat = primaryChannel->activeRepeatAt(time); - if (repeatFrame){ - const int originalTime = repeatFrame->getOriginalTimeFor(time); - if (cycle.start() == originalTime) return BeginsRepeat; - if (cycle.end() == originalTime) return EndsRepeat; + if (repeat) { + const KisTimeSpan range = repeat->cycle()->originalRange(); + const int originalTime = repeat->getOriginalTimeFor(time); + if (originalTime == range.start()) return BeginsRepeat; + if (originalTime == range.end()) return EndsRepeat; return ContinuesRepeat; - } else if (cycle.contains(time)) { - if (cycle.start() == time) return BeginsCycle; - if (cycle.end() == time) return EndsCycle; + } else if (cycle) { + const KisTimeSpan range = cycle->originalRange(); + if (time == range.start()) return BeginsCycle; + if (time == range.end()) return EndsCycle; return ContinuesCycle; } return NoCycle; }; QVariant layerProperties(int row) const { KisNodeDummy *dummy = converter->dummyFromRow(row); if (!dummy) return QVariant(); PropertyList props = dummy->node()->sectionModelProperties(); return QVariant::fromValue(props); } bool setLayerProperties(int row, PropertyList props) { KisNodeDummy *dummy = converter->dummyFromRow(row); if (!dummy) return false; KisNodePropertyListCommand::setNodePropertiesNoUndo(dummy->node(), image, props); return true; } bool addKeyframe(int row, int column, bool copy) { KisNodeDummy *dummy = converter->dummyFromRow(row); if (!dummy) return false; KisNodeSP node = dummy->node(); if (!KisAnimationUtils::supportsContentFrames(node)) return false; KisAnimationUtils::createKeyframeLazy(image, node, KisKeyframeChannel::Content.id(), column, copy); return true; } bool addNewLayer(int row) { Q_UNUSED(row); if (nodeInterface) { KisLayerSP layer = nodeInterface->addPaintLayer(); layer->setUseInTimeline(true); } return true; } bool removeLayer(int row) { KisNodeDummy *dummy = converter->dummyFromRow(row); if (!dummy) return false; if (nodeInterface) { nodeInterface->removeNode(dummy->node()); } return true; } + + KisKeyframeChannel *getChannel(int row, const QString &channelId) const { + KisNodeDummy *dummy = converter->dummyFromRow(row); + if (!dummy) return nullptr; + return dummy->node()->getKeyframeChannel(channelId); + } + }; TimelineFramesModel::TimelineFramesModel(QObject *parent) : ModelWithExternalNotifications(parent), m_d(new Private) { connect(&m_d->updateTimer, SIGNAL(timeout()), SLOT(processUpdateQueue())); } TimelineFramesModel::~TimelineFramesModel() { } bool TimelineFramesModel::hasConnectionToCanvas() const { return m_d->dummiesFacade; } void TimelineFramesModel::setNodeManipulationInterface(NodeManipulationInterface *iface) { m_d->nodeInterface.reset(iface); } KisNodeSP TimelineFramesModel::nodeAt(QModelIndex index) const { /** * The dummy might not exist because the user could (quickly) change * active layer and the list of the nodes in m_d->converter will change. */ KisNodeDummy *dummy = m_d->converter->dummyFromRow(index.row()); return dummy ? dummy->node() : 0; } QMap TimelineFramesModel::channelsAt(QModelIndex index) const { KisNodeDummy *srcDummy = m_d->converter->dummyFromRow(index.row()); return srcDummy->node()->keyframeChannels(); } void TimelineFramesModel::setDummiesFacade(KisDummiesFacadeBase *dummiesFacade, KisImageSP image) { KisDummiesFacadeBase *oldDummiesFacade = m_d->dummiesFacade; if (m_d->dummiesFacade && m_d->image) { m_d->image->animationInterface()->disconnect(this); m_d->image->disconnect(this); m_d->dummiesFacade->disconnect(this); } m_d->image = image; KisTimeBasedItemModel::setImage(image); m_d->dummiesFacade = dummiesFacade; m_d->converter.reset(); if (m_d->dummiesFacade) { m_d->converter.reset(new TimelineNodeListKeeper(this, m_d->dummiesFacade)); connect(m_d->dummiesFacade, SIGNAL(sigDummyChanged(KisNodeDummy*)), SLOT(slotDummyChanged(KisNodeDummy*))); connect(m_d->image->animationInterface(), SIGNAL(sigFullClipRangeChanged()), SIGNAL(sigInfiniteTimelineUpdateNeeded())); connect(m_d->image->animationInterface(), SIGNAL(sigAudioChannelChanged()), SIGNAL(sigAudioChannelChanged())); connect(m_d->image->animationInterface(), SIGNAL(sigAudioVolumeChanged()), SIGNAL(sigAudioChannelChanged())); connect(m_d->image, SIGNAL(sigImageModified()), SLOT(slotImageContentChanged())); } if (m_d->dummiesFacade != oldDummiesFacade) { beginResetModel(); endResetModel(); } if (m_d->dummiesFacade) { emit sigInfiniteTimelineUpdateNeeded(); emit sigAudioChannelChanged(); } } void TimelineFramesModel::slotDummyChanged(KisNodeDummy *dummy) { if (!m_d->updateQueue.contains(dummy)) { m_d->updateQueue.append(dummy); } m_d->updateTimer.start(); } void TimelineFramesModel::slotImageContentChanged() { if (m_d->activeLayerIndex < 0) return; KisNodeDummy *dummy = m_d->converter->dummyFromRow(m_d->activeLayerIndex); if (!dummy) return; slotDummyChanged(dummy); } void TimelineFramesModel::processUpdateQueue() { Q_FOREACH (KisNodeDummy *dummy, m_d->updateQueue) { int row = m_d->converter->rowForDummy(dummy); if (row >= 0) { emit headerDataChanged (Qt::Vertical, row, row); emit dataChanged(this->index(row, 0), this->index(row, columnCount() - 1)); } } m_d->updateQueue.clear(); } void TimelineFramesModel::slotCurrentNodeChanged(KisNodeSP node) { if (!node) { m_d->activeLayerIndex = -1; return; } KisNodeDummy *dummy = m_d->dummiesFacade->dummyForNode(node); if (!dummy) { // It's perfectly normal that dummyForNode returns 0; that happens // when views get activated while Krita is closing down. return; } m_d->converter->updateActiveDummy(dummy); const int row = m_d->converter->rowForDummy(dummy); if (row < 0) { qWarning() << "WARNING: TimelineFramesModel::slotCurrentNodeChanged: node not found!"; } if (row >= 0 && m_d->activeLayerIndex != row) { setData(index(row, 0), true, ActiveLayerRole); } } int TimelineFramesModel::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent); if(!m_d->dummiesFacade) return 0; return m_d->converter->rowCount(); } QVariant TimelineFramesModel::data(const QModelIndex &index, int role) const { if(!m_d->dummiesFacade) return QVariant(); switch (role) { case ActiveLayerRole: { return index.row() == m_d->activeLayerIndex; } case FrameEditableRole: { return m_d->layerEditable(index.row()); } case FrameHasContent: { return m_d->frameHasContent(index.row(), index.column()); } case FrameExistsRole: { return m_d->frameExists(index.row(), index.column()); } case SpecialKeyframeExists: { return m_d->specialKeyframeExists(index.row(), index.column()); } case IdenticalWithActive: { return m_d->isIdenticalWith(index.row(), index.column(), activeFrameIndex()); } case FrameColorLabelIndexRole: { int label = m_d->frameColorLabel(index.row(), index.column()); return label > 0 ? label : QVariant(); } case FrameCycleMode: { return m_d->frameCycleMode(index.row(), index.column()); } case Qt::DisplayRole: { return m_d->layerName(index.row()); } case Qt::TextAlignmentRole: { return QVariant(Qt::AlignHCenter | Qt::AlignVCenter); } case KoResourceModel::LargeThumbnailRole: { KisNodeDummy *dummy = m_d->converter->dummyFromRow(index.row()); if (!dummy) { return QVariant(); } const int maxSize = 200; QSize size = dummy->node()->extent().size(); size.scale(maxSize, maxSize, Qt::KeepAspectRatio); if (size.width() == 0 || size.height() == 0) { // No thumbnail can be shown if there isn't width or height... return QVariant(); } QImage image(dummy->node()->createThumbnailForFrame(size.width(), size.height(), index.column())); return image; } } return ModelWithExternalNotifications::data(index, role); } bool TimelineFramesModel::setData(const QModelIndex &index, const QVariant &value, int role) { if (!index.isValid() || !m_d->dummiesFacade) return false; switch (role) { case ActiveLayerRole: { if (value.toBool() && index.row() != m_d->activeLayerIndex) { int prevLayer = m_d->activeLayerIndex; m_d->activeLayerIndex = index.row(); emit dataChanged(this->index(prevLayer, 0), this->index(prevLayer, columnCount() - 1)); emit dataChanged(this->index(m_d->activeLayerIndex, 0), this->index(m_d->activeLayerIndex, columnCount() - 1)); emit headerDataChanged(Qt::Vertical, prevLayer, prevLayer); emit headerDataChanged(Qt::Vertical, m_d->activeLayerIndex, m_d->activeLayerIndex); KisNodeDummy *dummy = m_d->converter->dummyFromRow(m_d->activeLayerIndex); KIS_ASSERT_RECOVER(dummy) { return true; } emit requestCurrentNodeChanged(dummy->node()); emit sigEnsureRowVisible(m_d->activeLayerIndex); } break; } case FrameColorLabelIndexRole: { m_d->setFrameColorLabel(index.row(), index.column(), value.toInt()); } break; } return ModelWithExternalNotifications::setData(index, value, role); } QVariant TimelineFramesModel::headerData(int section, Qt::Orientation orientation, int role) const { if(!m_d->dummiesFacade) return QVariant(); if (orientation == Qt::Vertical) { switch (role) { case ActiveLayerRole: return section == m_d->activeLayerIndex; case Qt::DisplayRole: { QVariant value = headerData(section, orientation, Qt::ToolTipRole); if (!value.isValid()) return value; QString name = value.toString(); const int maxNameSize = 13; if (name.size() > maxNameSize) { name = QString("%1...").arg(name.left(maxNameSize)); } return name; } case Qt::TextColorRole: { // WARNING: this role doesn't work for header views! Use // bold font to show isolated mode instead! return QVariant(); } case Qt::FontRole: { KisNodeDummy *dummy = m_d->converter->dummyFromRow(section); if (!dummy) return QVariant(); KisNodeSP node = dummy->node(); QFont baseFont; if (node->projectionLeaf()->isDroppedMask()) { baseFont.setStrikeOut(true); } else if (m_d->image && m_d->image->isolatedModeRoot() && KisNodeModel::belongsToIsolatedGroup(m_d->image, node, m_d->dummiesFacade)) { baseFont.setBold(true); } return baseFont; } case Qt::ToolTipRole: { return m_d->layerName(section); } case TimelinePropertiesRole: { return QVariant::fromValue(m_d->layerProperties(section)); } case OtherLayersRole: { TimelineNodeListKeeper::OtherLayersList list = m_d->converter->otherLayersList(); return QVariant::fromValue(list); } case LayerUsedInTimelineRole: { KisNodeDummy *dummy = m_d->converter->dummyFromRow(section); if (!dummy) return QVariant(); return dummy->node()->useInTimeline(); } case Qt::BackgroundRole: { int label = m_d->layerColorLabel(section); if (label > 0) { KisNodeViewColorScheme scm; QColor color = scm.colorLabel(label); QPalette pal = qApp->palette(); color = KritaUtils::blendColors(color, pal.color(QPalette::Button), 0.3); return QBrush(color); } else { return QVariant(); } } } } return ModelWithExternalNotifications::headerData(section, orientation, role); } bool TimelineFramesModel::setHeaderData(int section, Qt::Orientation orientation, const QVariant &value, int role) { if (!m_d->dummiesFacade) return false; if (orientation == Qt::Vertical) { switch (role) { case ActiveLayerRole: { setData(index(section, 0), value, role); break; } case TimelinePropertiesRole: { TimelineFramesModel::PropertyList props = value.value(); int result = m_d->setLayerProperties(section, props); emit headerDataChanged (Qt::Vertical, section, section); return result; } case LayerUsedInTimelineRole: { KisNodeDummy *dummy = m_d->converter->dummyFromRow(section); if (!dummy) return false; dummy->node()->setUseInTimeline(value.toBool()); return true; } } } return ModelWithExternalNotifications::setHeaderData(section, orientation, value, role); } Qt::DropActions TimelineFramesModel::supportedDragActions() const { return Qt::MoveAction | Qt::CopyAction | Qt::LinkAction; } Qt::DropActions TimelineFramesModel::supportedDropActions() const { return Qt::MoveAction | Qt::CopyAction | Qt::LinkAction; } QStringList TimelineFramesModel::mimeTypes() const { QStringList types; types << QLatin1String("application/x-krita-frame"); return types; } void TimelineFramesModel::setLastClickedIndex(const QModelIndex &index) { m_d->lastClickedIndex = index; } QMimeData* TimelineFramesModel::mimeData(const QModelIndexList &indexes) const { return mimeDataExtended(indexes, m_d->lastClickedIndex, UndefinedPolicy); } QMimeData *TimelineFramesModel::mimeDataExtended(const QModelIndexList &indexes, const QModelIndex &baseIndex, TimelineFramesModel::MimeCopyPolicy copyPolicy) const { QMimeData *data = new QMimeData(); QByteArray encoded; QDataStream stream(&encoded, QIODevice::WriteOnly); const int baseRow = baseIndex.row(); const int baseColumn = baseIndex.column(); stream << indexes.size(); stream << baseRow << baseColumn; Q_FOREACH (const QModelIndex &index, indexes) { KisNodeSP node = nodeAt(index); KIS_SAFE_ASSERT_RECOVER(node) { continue; } stream << index.row() - baseRow << index.column() - baseColumn; const QByteArray uuidData = node->uuid().toRfc4122(); stream << int(uuidData.size()); stream.writeRawData(uuidData.data(), uuidData.size()); } stream << int(copyPolicy); data->setData("application/x-krita-frame", encoded); return data; } inline void decodeBaseIndex(QByteArray *encoded, int *row, int *col) { int size_UNUSED = 0; QDataStream stream(encoded, QIODevice::ReadOnly); stream >> size_UNUSED >> *row >> *col; } bool TimelineFramesModel::canDropFrameData(const QMimeData */*data*/, const QModelIndex &index) { if (!index.isValid()) return false; /** * Now we support D&D around any layer, so just return 'true' all * the time. */ return true; } bool TimelineFramesModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) { Q_UNUSED(row); Q_UNUSED(column); return dropMimeDataExtended(data, action, parent); } bool TimelineFramesModel::dropMimeDataExtended(const QMimeData *data, Qt::DropAction action, const QModelIndex &parent, bool *dataMoved) { bool result = false; if ((action != Qt::MoveAction && action != Qt::CopyAction && action != Qt::LinkAction) || !parent.isValid()) return result; QByteArray encoded = data->data("application/x-krita-frame"); QDataStream stream(&encoded, QIODevice::ReadOnly); int size, baseRow, baseColumn; stream >> size >> baseRow >> baseColumn; const QPoint offset(parent.column() - baseColumn, parent.row() - baseRow); KisAnimationUtils::FrameMovePairList frameMoves; for (int i = 0; i < size; i++) { int relRow, relColumn; stream >> relRow >> relColumn; const int srcRow = baseRow + relRow; const int srcColumn = baseColumn + relColumn; int uuidLen = 0; stream >> uuidLen; QByteArray uuidData(uuidLen, '\0'); stream.readRawData(uuidData.data(), uuidLen); QUuid nodeUuid = QUuid::fromRfc4122(uuidData); KisNodeSP srcNode; if (!nodeUuid.isNull()) { KisNodeUuidInfo nodeInfo(nodeUuid); srcNode = nodeInfo.findNode(m_d->image->root()); } else { QModelIndex index = this->index(srcRow, srcColumn); srcNode = nodeAt(index); } KIS_SAFE_ASSERT_RECOVER(srcNode) { continue; } const QModelIndex dstIndex = this->index(srcRow + offset.y(), srcColumn + offset.x()); if (!dstIndex.isValid()) continue; KisNodeSP dstNode = nodeAt(dstIndex); KIS_SAFE_ASSERT_RECOVER(dstNode) { continue; } Q_FOREACH (KisKeyframeChannel *channel, srcNode->keyframeChannels().values()) { KisAnimationUtils::FrameItem srcItem(srcNode, channel->id(), srcColumn); KisAnimationUtils::FrameItem dstItem(dstNode, channel->id(), dstIndex.column()); frameMoves << std::make_pair(srcItem, dstItem); } } MimeCopyPolicy copyPolicy = UndefinedPolicy; if (!stream.atEnd()) { int value = 0; stream >> value; copyPolicy = MimeCopyPolicy(value); } const bool copyFrames = copyPolicy == UndefinedPolicy ? action == Qt::CopyAction : copyPolicy == CopyFramesPolicy; if (dataMoved) { *dataMoved = !copyFrames; } KUndo2Command *cmd = 0; KisAnimationUtils::KeyframeAction frameAction; if (action == Qt::LinkAction) { frameAction = KisAnimationUtils::LinkKeyframes; } else { frameAction = copyFrames ? KisAnimationUtils::CopyKeyframes : KisAnimationUtils::MoveKeyframes; } if (!frameMoves.isEmpty()) { KisImageBarrierLockerWithFeedback locker(m_d->image); cmd = KisAnimationUtils::createMoveKeyframesCommand(frameMoves, frameAction, false, 0); } if (cmd) { KisProcessingApplicator::runSingleCommandStroke(m_d->image, cmd, KisStrokeJobData::BARRIER); } return cmd; } Qt::ItemFlags TimelineFramesModel::flags(const QModelIndex &index) const { Qt::ItemFlags flags = ModelWithExternalNotifications::flags(index); if (!index.isValid()) return flags; if (m_d->frameExists(index.row(), index.column()) || m_d->specialKeyframeExists(index.row(), index.column())) { if (data(index, FrameEditableRole).toBool()) { flags |= Qt::ItemIsDragEnabled; } } /** * Basically we should forbid overrides only if we D&D a single frame * and allow it when we D&D multiple frames. But we cannot distinguish * it here... So allow all the time. */ flags |= Qt::ItemIsDropEnabled; return flags; } bool TimelineFramesModel::insertRows(int row, int count, const QModelIndex &parent) { Q_UNUSED(parent); KIS_ASSERT_RECOVER(count == 1) { return false; } if (row < 0 || row > rowCount()) return false; bool result = m_d->addNewLayer(row); return result; } bool TimelineFramesModel::removeRows(int row, int count, const QModelIndex &parent) { Q_UNUSED(parent); KIS_ASSERT_RECOVER(count == 1) { return false; } if (row < 0 || row >= rowCount()) return false; bool result = m_d->removeLayer(row); return result; } bool TimelineFramesModel::insertOtherLayer(int index, int dstRow) { Q_UNUSED(dstRow); TimelineNodeListKeeper::OtherLayersList list = m_d->converter->otherLayersList(); if (index < 0 || index >= list.size()) return false; list[index].dummy->node()->setUseInTimeline(true); dstRow = m_d->converter->rowForDummy(list[index].dummy); setData(this->index(dstRow, 0), true, ActiveLayerRole); return true; } int TimelineFramesModel::activeLayerRow() const { return m_d->activeLayerIndex; } bool TimelineFramesModel::createFrame(const QModelIndex &dstIndex) { if (!dstIndex.isValid()) return false; return m_d->addKeyframe(dstIndex.row(), dstIndex.column(), false); } bool TimelineFramesModel::copyFrame(const QModelIndex &dstIndex) { if (!dstIndex.isValid()) return false; return m_d->addKeyframe(dstIndex.row(), dstIndex.column(), true); } bool TimelineFramesModel::insertFrames(int dstColumn, const QList &dstRows, int count, int timing) { if (dstRows.isEmpty() || count <= 0) return true; timing = qMax(timing, 1); KUndo2Command *parentCommand = new KUndo2Command(kundo2_i18np("Insert frame", "Insert %1 frames", count)); { KisImageBarrierLockerWithFeedback locker(m_d->image); QModelIndexList indexes; Q_FOREACH (int row, dstRows) { for (int column = dstColumn; column < columnCount(); column++) { indexes << index(row, column); } } setLastVisibleFrame(columnCount() + (count * timing) - 1); createOffsetFramesCommand(indexes, QPoint((count * timing), 0), false, false, parentCommand); Q_FOREACH (int row, dstRows) { KisNodeDummy *dummy = m_d->converter->dummyFromRow(row); if (!dummy) continue; KisNodeSP node = dummy->node(); if (!KisAnimationUtils::supportsContentFrames(node)) continue; for (int column = dstColumn; column < dstColumn + (count * timing); column += timing) { KisAnimationUtils::createKeyframeCommand(m_d->image, node, KisKeyframeChannel::Content.id(), column, false, parentCommand); } } const int oldTime = m_d->image->animationInterface()->currentUITime(); const int newTime = dstColumn > oldTime ? dstColumn : dstColumn + (count * timing) - 1; new KisSwitchCurrentTimeCommand(m_d->image->animationInterface(), oldTime, newTime, parentCommand); } KisProcessingApplicator::runSingleCommandStroke(m_d->image, parentCommand, KisStrokeJobData::BARRIER); return true; } bool TimelineFramesModel::insertHoldFrames(QModelIndexList selectedIndexes, int count) { if (selectedIndexes.isEmpty() || count == 0) return true; QScopedPointer parentCommand(new KUndo2Command(kundo2_i18np("Insert frame", "Insert %1 frames", count))); { KisImageBarrierLockerWithFeedback locker(m_d->image); - QSet uniqueKeyframesInSelection; + QSet uniqueKeyframesInSelection; int minSelectedTime = std::numeric_limits::max(); Q_FOREACH (const QModelIndex &index, selectedIndexes) { KisNodeSP node = nodeAt(index); KIS_SAFE_ASSERT_RECOVER(node) { continue; } KisKeyframeChannel *channel = node->getKeyframeChannel(KisKeyframeChannel::Content.id()); if (!channel) continue; minSelectedTime = qMin(minSelectedTime, index.column()); - KisKeyframeSP keyFrame = channel->activeKeyframeAt(index.column()); + KisKeyframeBaseSP keyFrame = channel->activeItemAt(index.column()); if (keyFrame) { uniqueKeyframesInSelection.insert(keyFrame); } } - QList keyframesToMove; + QList keyframesToMove; for (auto it = uniqueKeyframesInSelection.begin(); it != uniqueKeyframesInSelection.end(); ++it) { - KisKeyframeSP keyframe = *it; + KisKeyframeBaseSP keyframe = *it; KisKeyframeChannel *channel = keyframe->channel(); - KisKeyframeSP nextKeyframe = channel->nextKeyframe(keyframe); + KisKeyframeBaseSP nextKeyframe = channel->nextItem(*keyframe); if (nextKeyframe) { keyframesToMove << nextKeyframe; } } std::sort(keyframesToMove.begin(), keyframesToMove.end(), - [] (KisKeyframeSP lhs, KisKeyframeSP rhs) { + [] (KisKeyframeBaseSP lhs, KisKeyframeBaseSP rhs) { return lhs->time() > rhs->time(); }); if (keyframesToMove.isEmpty()) return true; const int maxColumn = columnCount(); if (count > 0) { setLastVisibleFrame(columnCount() + count); } - Q_FOREACH (KisKeyframeSP keyframe, keyframesToMove) { + Q_FOREACH (KisKeyframeBaseSP keyframe, keyframesToMove) { int plannedFrameMove = count; if (count < 0) { KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(keyframe->time() > 0, false); - KisKeyframeSP prevFrame = keyframe->channel()->previousKeyframe(keyframe); + KisKeyframeBaseSP prevFrame = keyframe->channel()->previousItem(*keyframe); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(prevFrame, false); plannedFrameMove = qMax(count, prevFrame->time() - keyframe->time() + 1); minSelectedTime = qMin(minSelectedTime, prevFrame->time()); } KisNodeDummy *dummy = m_d->dummiesFacade->dummyForNode(keyframe->channel()->node()); KIS_SAFE_ASSERT_RECOVER(dummy) { continue; } const int row = m_d->converter->rowForDummy(dummy); KIS_SAFE_ASSERT_RECOVER(row >= 0) { continue; } QModelIndexList indexes; for (int column = keyframe->time(); column < maxColumn; column++) { indexes << index(row, column); } createOffsetFramesCommand(indexes, QPoint(plannedFrameMove, 0), false, true, parentCommand.data()); } const int oldTime = m_d->image->animationInterface()->currentUITime(); const int newTime = minSelectedTime; new KisSwitchCurrentTimeCommand(m_d->image->animationInterface(), oldTime, newTime, parentCommand.data()); } KisProcessingApplicator::runSingleCommandStroke(m_d->image, parentCommand.take(), KisStrokeJobData::BARRIER); return true; } bool TimelineFramesModel::defineCycles(int timeFrom, int timeTo, QSet rows) { KUndo2Command *parentCommand = new KUndo2Command(kundo2_i18np("Create animation cycle", "Create animation cycles", rows.size())); Q_FOREACH(const int row, rows) { KisNodeDummy *dummy = m_d->converter->dummyFromRow(row); if (dummy) { KisKeyframeChannel *contentChannel = dummy->node()->getKeyframeChannel(KisKeyframeChannel::Content.id()); if (contentChannel) { KisKeyframeSP firstKeyframe = contentChannel->activeKeyframeAt(timeFrom); KisKeyframeSP lastKeyframe = contentChannel->activeKeyframeAt(timeTo); if (!firstKeyframe.isNull() && !lastKeyframe.isNull()) { KisDefineCycleCommand *createCycle = contentChannel->createCycle(firstKeyframe, lastKeyframe, parentCommand); - if (contentChannel->keyframeAt(timeTo + 1).isNull()) { + if (contentChannel->itemAt(timeTo + 1).isNull()) { contentChannel->addRepeat(createCycle->cycle(), timeTo + 1, parentCommand); } } } } } KisProcessingApplicator::runSingleCommandStroke(m_d->image, parentCommand, KisStrokeJobData::BARRIER); return true; } bool TimelineFramesModel::deleteCycles(int timeFrom, int timeTo, QSet rows) { QVector> cycles; Q_FOREACH(const int row, rows) { KisNodeDummy *dummy = m_d->converter->dummyFromRow(row); if (!dummy) continue; KisKeyframeChannel *contentChannel = dummy->node()->getKeyframeChannel(KisKeyframeChannel::Content.id()); if (!contentChannel) continue; for (int time = timeFrom; time <= timeTo; time++) { auto cycle = contentChannel->cycleAt(time); if (!cycle.isNull()) { KisTimeSpan cycleRange = cycle->originalRange(); if (cycleRange.contains(time)) { cycles.append(cycle); time = cycleRange.end(); } } } } KUndo2Command *parentCommand = new KUndo2Command(kundo2_i18np("Delete animation cycle", "Delete animation cycles", cycles.size())); Q_FOREACH(QSharedPointer cycle, cycles) { KisKeyframeChannel *channel = cycle->firstSourceKeyframe()->channel(); channel->deleteCycle(cycle, parentCommand); } KisProcessingApplicator::runSingleCommandStroke(m_d->image, parentCommand, KisStrokeJobData::BARRIER); return true; } void TimelineFramesModel::addRepeatAt(QModelIndex location) { const int time = location.row(); KisNodeDummy *dummy = m_d->converter->dummyFromRow(time); if (dummy) { KisKeyframeChannel *contentChannel = dummy->node()->getKeyframeChannel(KisKeyframeChannel::Content.id()); if (contentChannel) { for (int t = location.column(); t >= 0; t--) { const QSharedPointer cycle = contentChannel->cycleAt(t); if (cycle) { KUndo2Command *parentCommand = new KUndo2Command(kundo2_i18n("Add animation repeat")); contentChannel->addRepeat(cycle, location.column(), parentCommand); KisProcessingApplicator::runSingleCommandStroke(m_d->image, parentCommand, KisStrokeJobData::BARRIER); return; } } } } } QString TimelineFramesModel::audioChannelFileName() const { return m_d->image ? m_d->image->animationInterface()->audioChannelFileName() : QString(); } void TimelineFramesModel::setAudioChannelFileName(const QString &fileName) { KIS_SAFE_ASSERT_RECOVER_RETURN(m_d->image); m_d->image->animationInterface()->setAudioChannelFileName(fileName); } bool TimelineFramesModel::isAudioMuted() const { return m_d->image ? m_d->image->animationInterface()->isAudioMuted() : false; } void TimelineFramesModel::setAudioMuted(bool value) { KIS_SAFE_ASSERT_RECOVER_RETURN(m_d->image); m_d->image->animationInterface()->setAudioMuted(value); } qreal TimelineFramesModel::audioVolume() const { return m_d->image ? m_d->image->animationInterface()->audioVolume() : 0.5; } void TimelineFramesModel::setAudioVolume(qreal value) { KIS_SAFE_ASSERT_RECOVER_RETURN(m_d->image); m_d->image->animationInterface()->setAudioVolume(value); } void TimelineFramesModel::setFullClipRangeStart(int column) { m_d->image->animationInterface()->setFullClipRangeStartTime(column); } void TimelineFramesModel::setFullClipRangeEnd(int column) { m_d->image->animationInterface()->setFullClipRangeEndTime(column); } diff --git a/plugins/dockers/animation/timeline_node_list_keeper.cpp b/plugins/dockers/animation/timeline_node_list_keeper.cpp index 5631972eb1..8feacac117 100644 --- a/plugins/dockers/animation/timeline_node_list_keeper.cpp +++ b/plugins/dockers/animation/timeline_node_list_keeper.cpp @@ -1,249 +1,249 @@ /* * Copyright (c) 2015 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "timeline_node_list_keeper.h" #include "kis_node_dummies_graph.h" #include "kis_dummies_facade_base.h" #include "timeline_frames_index_converter.h" #include #include #include "kis_keyframe_channel.h" struct TimelineNodeListKeeper::Private { Private(TimelineNodeListKeeper *_q, ModelWithExternalNotifications *_model, KisDummiesFacadeBase *_dummiesFacade) : q(_q), model(_model), dummiesFacade(_dummiesFacade), converter(dummiesFacade) { } TimelineNodeListKeeper *q; ModelWithExternalNotifications *model; KisDummiesFacadeBase *dummiesFacade; TimelineFramesIndexConverter converter; QVector dummiesList; QSignalMapper dummiesUpdateMapper; QSet connectionsSet; void populateDummiesList() { const int rowCount = converter.rowCount(); for (int i = 0; i < rowCount; ++i) { KisNodeDummy *dummy = converter.dummyFromRow(i); dummiesList.append(dummy); tryConnectDummy(dummy); } } void tryConnectDummy(KisNodeDummy *dummy); void disconnectDummy(KisNodeDummy *dummy); }; TimelineNodeListKeeper::TimelineNodeListKeeper(ModelWithExternalNotifications *model, KisDummiesFacadeBase *dummiesFacade) : m_d(new Private(this, model, dummiesFacade)) { KIS_ASSERT_RECOVER_RETURN(m_d->dummiesFacade); connect(m_d->dummiesFacade, SIGNAL(sigEndInsertDummy(KisNodeDummy*)), SLOT(slotEndInsertDummy(KisNodeDummy*))); connect(m_d->dummiesFacade, SIGNAL(sigBeginRemoveDummy(KisNodeDummy*)), SLOT(slotBeginRemoveDummy(KisNodeDummy*))); connect(m_d->dummiesFacade, SIGNAL(sigDummyChanged(KisNodeDummy*)), SLOT(slotDummyChanged(KisNodeDummy*))); m_d->populateDummiesList(); connect(&m_d->dummiesUpdateMapper, SIGNAL(mapped(QObject*)), SLOT(slotUpdateDummyContent(QObject*))); } TimelineNodeListKeeper::~TimelineNodeListKeeper() { } KisNodeDummy* TimelineNodeListKeeper::dummyFromRow(int row) { if (row >= 0 && row < m_d->dummiesList.size()) { return m_d->dummiesList[row]; } return 0; } int TimelineNodeListKeeper::rowForDummy(KisNodeDummy *dummy) { return m_d->dummiesList.indexOf(dummy); } int TimelineNodeListKeeper::rowCount() { return m_d->dummiesList.size(); } void TimelineNodeListKeeper::updateActiveDummy(KisNodeDummy *dummy) { bool oldRemoved = false; bool newAdded = false; KisNodeDummy *oldActiveDummy = m_d->converter.activeDummy(); m_d->converter.updateActiveDummy(dummy, &oldRemoved, &newAdded); if (oldRemoved) { slotBeginRemoveDummy(oldActiveDummy); } if (newAdded) { slotEndInsertDummy(dummy); } } void TimelineNodeListKeeper::slotUpdateDummyContent(QObject *_dummy) { KisNodeDummy *dummy = qobject_cast(_dummy); int pos = m_d->converter.rowForDummy(dummy); if (pos < 0) return; QModelIndex index0 = m_d->model->index(pos, 0); QModelIndex index1 = m_d->model->index(pos, m_d->model->columnCount() - 1); m_d->model->callIndexChanged(index0, index1); } void TimelineNodeListKeeper::Private::tryConnectDummy(KisNodeDummy *dummy) { QMap channels = dummy->node()->keyframeChannels(); if (channels.isEmpty()) { if (connectionsSet.contains(dummy)) { connectionsSet.remove(dummy); } return; } if (connectionsSet.contains(dummy)) return; Q_FOREACH(KisKeyframeChannel *channel, channels) { - connect(channel, SIGNAL(sigKeyframeAdded(KisKeyframeSP)), + connect(channel, SIGNAL(sigKeyframeAdded(KisKeyframeBaseSP)), &dummiesUpdateMapper, SLOT(map())); - connect(channel, SIGNAL(sigKeyframeAboutToBeRemoved(KisKeyframeSP)), + connect(channel, SIGNAL(sigKeyframeAboutToBeRemoved(KisKeyframeBaseSP)), &dummiesUpdateMapper, SLOT(map())); - connect(channel, SIGNAL(sigKeyframeMoved(KisKeyframeSP,int)), + connect(channel, SIGNAL(sigKeyframeMoved(KisKeyframeBaseSP,int)), &dummiesUpdateMapper, SLOT(map())); dummiesUpdateMapper.setMapping(channel, (QObject*)dummy); } connectionsSet.insert(dummy); } void TimelineNodeListKeeper::Private::disconnectDummy(KisNodeDummy *dummy) { if (!connectionsSet.contains(dummy)) return; QMap channels = dummy->node()->keyframeChannels(); if (channels.isEmpty()) { if (connectionsSet.contains(dummy)) { connectionsSet.remove(dummy); } return; } Q_FOREACH(KisKeyframeChannel *channel, channels) { channel->disconnect(&dummiesUpdateMapper); } connectionsSet.remove(dummy); } void TimelineNodeListKeeper::slotEndInsertDummy(KisNodeDummy *dummy) { KIS_ASSERT_RECOVER_RETURN(!m_d->dummiesList.contains(dummy)); if (m_d->converter.isDummyVisible(dummy)) { int pos = m_d->converter.rowForDummy(dummy); m_d->model->callBeginInsertRows(QModelIndex(), pos, pos); m_d->dummiesList.insert(pos, 1, dummy); m_d->tryConnectDummy(dummy); m_d->model->callEndInsertRows(); } } void TimelineNodeListKeeper::slotBeginRemoveDummy(KisNodeDummy *dummy) { if (m_d->dummiesList.contains(dummy)) { int pos = m_d->dummiesList.indexOf(dummy); m_d->model->callBeginRemoveRows(QModelIndex(), pos, pos); m_d->disconnectDummy(dummy); m_d->dummiesList.remove(pos); m_d->model->callEndRemoveRows(); } m_d->converter.notifyDummyRemoved(dummy); } void TimelineNodeListKeeper::slotDummyChanged(KisNodeDummy *dummy) { const bool present = m_d->dummiesList.contains(dummy); const bool shouldBe = m_d->converter.isDummyVisible(dummy); m_d->tryConnectDummy(dummy); if (!present && shouldBe) { slotEndInsertDummy(dummy); } else if (present && !shouldBe) { slotBeginRemoveDummy(dummy); } } void findOtherLayers(KisNodeDummy *root, TimelineNodeListKeeper::OtherLayersList *list, const QString &prefix) { KisNodeSP node = root->node(); if (root->parent() && !node->useInTimeline()) { *list << TimelineNodeListKeeper::OtherLayer( QString(prefix + node->name()), root); } KisNodeDummy *dummy = root->lastChild(); while(dummy) { findOtherLayers(dummy, list, prefix + " "); dummy = dummy->prevSibling(); } } TimelineNodeListKeeper::OtherLayersList TimelineNodeListKeeper::otherLayersList() const { OtherLayersList list; findOtherLayers(m_d->dummiesFacade->rootDummy(), &list, ""); return list; }