diff --git a/libs/image/kis_keyframe_channel.cpp b/libs/image/kis_keyframe_channel.cpp index aadd9700a6..385106bacc 100644 --- a/libs/image/kis_keyframe_channel.cpp +++ b/libs/image/kis_keyframe_channel.cpp @@ -1,638 +1,642 @@ /* * 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 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)")); struct KisKeyframeChannel::Private { Private() {} Private(const Private &rhs, KisNodeWSP newParentNode) { node = newParentNode; id = rhs.id; defaultBounds = rhs.defaultBounds; } KeyframesMap keys; KisNodeWSP node; KoID id; KisDefaultBoundsBaseSP defaultBounds; }; 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) { m_d->keys.insert(keyframe->time(), keyframe->cloneFor(this)); } } 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); } +KisKeyframeSP KisKeyframeChannel::linkKeyframe(const KisKeyframeSP, int, KUndo2Command*) { + return KisKeyframeSP(); +} + KisKeyframeSP KisKeyframeChannel::insertKeyframe(int time, const KisKeyframeSP copySrc, KUndo2Command *parentCommand) { KisKeyframeSP keyframe = keyframeAt(time); if (keyframe) { deleteKeyframeImpl(keyframe, parentCommand, false); } Q_ASSERT(parentCommand); keyframe = createKeyframe(time, copySrc, parentCommand); KUndo2Command *cmd = new KisReplaceKeyframeCommand(this, keyframe->time(), keyframe, parentCommand); cmd->redo(); return keyframe; } bool KisKeyframeChannel::deleteKeyframe(KisKeyframeSP keyframe, KUndo2Command *parentCommand) { return deleteKeyframeImpl(keyframe, parentCommand, true); } bool KisKeyframeChannel::moveKeyframe(KisKeyframeSP keyframe, int newTime, KUndo2Command *parentCommand) { LAZY_INITIALIZE_PARENT_COMMAND(parentCommand); if (newTime == keyframe->time()) return false; KisKeyframeSP other = keyframeAt(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; KisKeyframeSP lhsFrame = keyframeAt(lhsTime); KisKeyframeSP rhsFrame = keyframeAt(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(KisKeyframeSP keyframe, KUndo2Command *parentCommand, bool recreate) { LAZY_INITIALIZE_PARENT_COMMAND(parentCommand); Q_ASSERT(parentCommand); KUndo2Command *cmd = new KisReplaceKeyframeCommand(this, keyframe->time(), KisKeyframeSP(), parentCommand); cmd->redo(); destroyKeyframe(keyframe, parentCommand); if (recreate && keyframe->time() == 0) { addKeyframe(0, parentCommand); } return true; } void KisKeyframeChannel::moveKeyframeImpl(KisKeyframeSP keyframe, int newTime) { KIS_ASSERT_RECOVER_RETURN(keyframe); KIS_ASSERT_RECOVER_RETURN(!keyframeAt(newTime)); KisTimeRange rangeSrc = affectedFrames(keyframe->time()); QRect rectSrc = affectedRect(keyframe); emit sigKeyframeAboutToBeMoved(keyframe, newTime); m_d->keys.remove(keyframe->time()); int oldTime = keyframe->time(); keyframe->setTime(newTime); m_d->keys.insert(newTime, keyframe); emit sigKeyframeMoved(keyframe, oldTime); KisTimeRange rangeDst = affectedFrames(keyframe->time()); QRect rectDst = affectedRect(keyframe); requestUpdate(rangeSrc, rectSrc); requestUpdate(rangeDst, rectDst); } void KisKeyframeChannel::swapKeyframesImpl(KisKeyframeSP lhsKeyframe, KisKeyframeSP rhsKeyframe) { KIS_ASSERT_RECOVER_RETURN(lhsKeyframe); KIS_ASSERT_RECOVER_RETURN(rhsKeyframe); KisTimeRange rangeLhs = affectedFrames(lhsKeyframe->time()); KisTimeRange rangeRhs = affectedFrames(rhsKeyframe->time()); const QRect rectLhsSrc = affectedRect(lhsKeyframe); const QRect rectRhsSrc = affectedRect(rhsKeyframe); const int lhsTime = lhsKeyframe->time(); const int rhsTime = rhsKeyframe->time(); emit sigKeyframeAboutToBeMoved(lhsKeyframe, rhsTime); emit sigKeyframeAboutToBeMoved(rhsKeyframe, lhsTime); m_d->keys.remove(lhsTime); m_d->keys.remove(rhsTime); rhsKeyframe->setTime(lhsTime); lhsKeyframe->setTime(rhsTime); m_d->keys.insert(lhsTime, rhsKeyframe); m_d->keys.insert(rhsTime, lhsKeyframe); emit sigKeyframeMoved(lhsKeyframe, lhsTime); emit sigKeyframeMoved(rhsKeyframe, rhsTime); const QRect rectLhsDst = affectedRect(lhsKeyframe); const QRect rectRhsDst = affectedRect(rhsKeyframe); requestUpdate(rangeLhs, rectLhsSrc | rectRhsDst); requestUpdate(rangeRhs, rectRhsSrc | rectLhsDst); } KisKeyframeSP KisKeyframeChannel::replaceKeyframeAt(int time, KisKeyframeSP newKeyframe) { Q_ASSERT(newKeyframe.isNull() || time == newKeyframe->time()); KisKeyframeSP existingKeyframe = keyframeAt(time); if (!existingKeyframe.isNull()) { removeKeyframeLogical(existingKeyframe); } if (!newKeyframe.isNull()) { insertKeyframeLogical(newKeyframe); } return existingKeyframe; } void KisKeyframeChannel::insertKeyframeLogical(KisKeyframeSP keyframe) { const int time = keyframe->time(); emit sigKeyframeAboutToBeAdded(keyframe); m_d->keys.insert(time, keyframe); emit sigKeyframeAdded(keyframe); QRect rect = affectedRect(keyframe); KisTimeRange range = affectedFrames(time); requestUpdate(range, rect); } void KisKeyframeChannel::removeKeyframeLogical(KisKeyframeSP keyframe) { QRect rect = affectedRect(keyframe); KisTimeRange range = affectedFrames(keyframe->time()); emit sigKeyframeAboutToBeRemoved(keyframe); m_d->keys.remove(keyframe->time()); emit sigKeyframeRemoved(keyframe); requestUpdate(range, rect); } KisKeyframeSP KisKeyframeChannel::keyframeAt(int time) const { KeyframesMap::const_iterator i = m_d->keys.constFind(time); if (i != m_d->keys.constEnd()) { return i.value(); } return KisKeyframeSP(); } KisKeyframeSP KisKeyframeChannel::activeKeyframeAt(int time) const { KeyframesMap::const_iterator i = activeKeyIterator(time); if (i != m_d->keys.constEnd()) { return i.value(); } return KisKeyframeSP(); } 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 { KeyframesMap::const_iterator i = m_d->keys.constFind(keyframe->time()); if (i == m_d->keys.constEnd()) return KisKeyframeSP(0); i++; if (i == m_d->keys.constEnd()) return KisKeyframeSP(0); return i.value(); } KisKeyframeSP KisKeyframeChannel::previousKeyframe(KisKeyframeSP keyframe) const { KeyframesMap::const_iterator i = m_d->keys.constFind(keyframe->time()); if (i == m_d->keys.constBegin() || i == m_d->keys.constEnd()) return KisKeyframeSP(0); i--; return i.value(); } KisKeyframeSP KisKeyframeChannel::lastKeyframe() const { if (m_d->keys.isEmpty()) return KisKeyframeSP(0); return (m_d->keys.end()-1).value(); } 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; } KisTimeRange KisKeyframeChannel::affectedFrames(int time) const { if (m_d->keys.isEmpty()) return KisTimeRange::infinite(0); KeyframesMap::const_iterator active = activeKeyIterator(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; } if (next == m_d->keys.constEnd()) { return KisTimeRange::infinite(from); } else { return KisTimeRange::fromTime(from, next.key() - 1); } } KisTimeRange KisKeyframeChannel::identicalFrames(int time) const { KeyframesMap::const_iterator active = activeKeyIterator(time); if (active != m_d->keys.constEnd() && (active+1) != m_d->keys.constEnd()) { if (active->data()->interpolationMode() != KisKeyframe::Constant) { return KisTimeRange::fromTime(time, time); } } return affectedFrames(time); } int KisKeyframeChannel::keyframeRowIndexOf(KisKeyframeSP keyframe) const { KeyframesMap::const_iterator it = m_d->keys.constBegin(); KeyframesMap::const_iterator end = m_d->keys.constEnd(); int row = 0; for (; it != end; ++it) { if (it.value().data() == keyframe) { return row; } row++; } return -1; } KisKeyframeSP KisKeyframeChannel::keyframeAtRow(int row) const { KeyframesMap::const_iterator it = m_d->keys.constBegin(); KeyframesMap::const_iterator end = m_d->keys.constEnd(); for (; it != end; ++it) { if (row <= 0) { return it.value(); } row--; } return KisKeyframeSP(); } int KisKeyframeChannel::keyframeInsertionRow(int time) const { KeyframesMap::const_iterator it = m_d->keys.constBegin(); KeyframesMap::const_iterator end = m_d->keys.constEnd(); int row = 0; for (; it != end; ++it) { if (it.value()->time() > time) { break; } row++; } return row; } QDomElement KisKeyframeChannel::toXML(QDomDocument doc, const QString &layerFilename) { QDomElement channelElement = doc.createElement("channel"); channelElement.setAttribute("name", id()); Q_FOREACH (KisKeyframeSP keyframe, m_d->keys.values()) { QDomElement keyframeElement = doc.createElement("keyframe"); keyframeElement.setAttribute("time", keyframe->time()); keyframeElement.setAttribute("color-label", keyframe->colorLabel()); saveKeyframe(keyframe, keyframeElement, layerFilename); channelElement.appendChild(keyframeElement); } return channelElement; } void KisKeyframeChannel::loadXML(const QDomElement &channelNode) { for (QDomElement keyframeNode = channelNode.firstChildElement(); !keyframeNode.isNull(); keyframeNode = keyframeNode.nextSiblingElement()) { if (keyframeNode.nodeName().toUpper() != "KEYFRAME") continue; KisKeyframeSP keyframe = loadKeyframe(keyframeNode); if (keyframeNode.hasAttribute("color-label")) { keyframe->setColorLabel(keyframeNode.attribute("color-label").toUInt()); } m_d->keys.insert(keyframe->time(), 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; } KisKeyframeChannel::KeyframesMap::const_iterator KisKeyframeChannel::activeKeyIterator(int time) const { KeyframesMap::const_iterator i = const_cast(&m_d->keys)->upperBound(time); if (i == m_d->keys.constBegin()) return m_d->keys.constEnd(); return --i; } void KisKeyframeChannel::requestUpdate(const KisTimeRange &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); } } } 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); } diff --git a/libs/image/kis_keyframe_channel.h b/libs/image/kis_keyframe_channel.h index 7e87abf386..c6a2afe835 100644 --- a/libs/image/kis_keyframe_channel.h +++ b/libs/image/kis_keyframe_channel.h @@ -1,170 +1,171 @@ /* * 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 KisTimeRange; 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(KisKeyframeSP keyframe, KUndo2Command *parentCommand = 0); bool moveKeyframe(KisKeyframeSP keyframe, int newTime, KUndo2Command *parentCommand = 0); bool swapFrames(int lhsTime, int rhsTime, KUndo2Command *parentCommand = 0); KisKeyframeSP copyKeyframe(const KisKeyframeSP keyframe, int newTime, KUndo2Command *parentCommand = 0); + virtual KisKeyframeSP linkKeyframe(const KisKeyframeSP keyframe, int newTime, KUndo2Command *parentCommand = 0); KisKeyframeSP copyExternalKeyframe(KisKeyframeChannel *srcChannel, int srcTime, int dstTime, KUndo2Command *parentCommand = 0); bool swapExternalKeyframe(KisKeyframeChannel *srcChannel, int srcTime, int dstTime, KUndo2Command *parentCommand = 0); KisKeyframeSP keyframeAt(int time) const; KisKeyframeSP activeKeyframeAt(int time) const; KisKeyframeSP currentlyActiveKeyframe() const; KisKeyframeSP firstKeyframe() const; KisKeyframeSP nextKeyframe(KisKeyframeSP keyframe) const; KisKeyframeSP previousKeyframe(KisKeyframeSP keyframe) const; KisKeyframeSP lastKeyframe() 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. */ KisTimeRange 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. */ KisTimeRange identicalFrames(int time) const; int keyframeCount() const; int keyframeRowIndexOf(KisKeyframeSP keyframe) const; KisKeyframeSP keyframeAtRow(int row) const; int keyframeInsertionRow(int time) 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(KisKeyframeSP keyframe); void sigKeyframeAdded(KisKeyframeSP keyframe); void sigKeyframeAboutToBeRemoved(KisKeyframeSP keyframe); void sigKeyframeRemoved(KisKeyframeSP keyframe); void sigKeyframeAboutToBeMoved(KisKeyframeSP keyframe, int toTime); void sigKeyframeMoved(KisKeyframeSP keyframe, int fromTime); void sigKeyframeChanged(KisKeyframeSP keyframe); protected: typedef QMap KeyframesMap; KeyframesMap &keys(); const KeyframesMap &constKeys() const; KeyframesMap::const_iterator activeKeyIterator(int time) 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 QRect affectedRect(KisKeyframeSP key) = 0; virtual void requestUpdate(const KisTimeRange &range, const QRect &rect); virtual KisKeyframeSP loadKeyframe(const QDomElement &keyframeNode) = 0; virtual void saveKeyframe(KisKeyframeSP keyframe, QDomElement keyframeElement, const QString &layerFilename) = 0; private: KisKeyframeSP replaceKeyframeAt(int time, KisKeyframeSP newKeyframe); void insertKeyframeLogical(KisKeyframeSP keyframe); void removeKeyframeLogical(KisKeyframeSP keyframe); bool deleteKeyframeImpl(KisKeyframeSP keyframe, KUndo2Command *parentCommand, bool recreate); void moveKeyframeImpl(KisKeyframeSP keyframe, int newTime); void swapKeyframesImpl(KisKeyframeSP lhsKeyframe, KisKeyframeSP rhsKeyframe); friend class KisMoveFrameCommand; friend class KisReplaceKeyframeCommand; friend class KisSwapFramesCommand; private: KisKeyframeSP insertKeyframe(int time, const KisKeyframeSP copySrc, KUndo2Command *parentCommand); struct Private; QScopedPointer m_d; }; #endif // KIS_KEYFRAME_CHANNEL_H diff --git a/libs/image/kis_raster_keyframe_channel.cpp b/libs/image/kis_raster_keyframe_channel.cpp index 8fbc73df2f..48493f690a 100644 --- a/libs/image/kis_raster_keyframe_channel.cpp +++ b/libs/image/kis_raster_keyframe_channel.cpp @@ -1,312 +1,345 @@ /* * 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_raster_keyframe_channel.h" #include "kis_node.h" #include "kis_dom_utils.h" #include "kis_global.h" #include "kis_paint_device.h" #include "kis_paint_device_frames_interface.h" #include "kis_time_range.h" #include "kundo2command.h" #include "kis_onion_skin_compositor.h" +#include "kis_keyframe_commands.h" struct KisRasterKeyframe : public KisKeyframe { KisRasterKeyframe(KisRasterKeyframeChannel *channel, int time, int frameId) : KisKeyframe(channel, time) , frameId(frameId) {} KisRasterKeyframe(const KisRasterKeyframe *rhs, KisKeyframeChannel *channel) : KisKeyframe(rhs, channel) , frameId(rhs->frameId) {} int frameId; KisKeyframeSP cloneFor(KisKeyframeChannel *channel) const override { return toQShared(new KisRasterKeyframe(this, channel)); } bool hasContent() const override { KisRasterKeyframeChannel *channel = dynamic_cast(this->channel()); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(channel, true); return channel->keyframeHasContent(this); } }; struct KisRasterKeyframeChannel::Private { Private(KisPaintDeviceWSP paintDevice, const QString filenameSuffix) : paintDevice(paintDevice), filenameSuffix(filenameSuffix), onionSkinsEnabled(false) {} KisPaintDeviceWSP paintDevice; + QMap framesByFilename; QMap frameFilenames; + QMap> frameInstances; QString filenameSuffix; bool onionSkinsEnabled; }; KisRasterKeyframeChannel::KisRasterKeyframeChannel(const KoID &id, const KisPaintDeviceWSP paintDevice, KisDefaultBoundsBaseSP defaultBounds) : KisKeyframeChannel(id, defaultBounds), m_d(new Private(paintDevice, QString())) { } KisRasterKeyframeChannel::KisRasterKeyframeChannel(const KisRasterKeyframeChannel &rhs, KisNode *newParentNode, const KisPaintDeviceWSP newPaintDevice) : KisKeyframeChannel(rhs, newParentNode), m_d(new Private(newPaintDevice, rhs.m_d->filenameSuffix)) { KIS_ASSERT_RECOVER_NOOP(&rhs != this); m_d->frameFilenames = rhs.m_d->frameFilenames; m_d->onionSkinsEnabled = rhs.m_d->onionSkinsEnabled; } KisRasterKeyframeChannel::~KisRasterKeyframeChannel() { } +KisKeyframeSP KisRasterKeyframeChannel::linkKeyframe(const KisKeyframeSP sourceKeyframe, int newTime, KUndo2Command *parentCommand) +{ + KisKeyframeSP newKeyframe = toQShared(new KisRasterKeyframe(this, newTime, frameId(sourceKeyframe))); + + KUndo2Command *cmd = new KisReplaceKeyframeCommand(this, newTime, newKeyframe, parentCommand); + cmd->redo(); + + return newKeyframe; +} + int KisRasterKeyframeChannel::frameId(KisKeyframeSP keyframe) const { return frameId(keyframe.data()); } int KisRasterKeyframeChannel::frameId(const KisKeyframe *keyframe) const { const KisRasterKeyframe *key = dynamic_cast(keyframe); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(key, -1); return key->frameId; } int KisRasterKeyframeChannel::frameIdAt(int time) const { KisKeyframeSP activeKey = activeKeyframeAt(time); if (activeKey.isNull()) return -1; return frameId(activeKey); } void KisRasterKeyframeChannel::fetchFrame(KisKeyframeSP keyframe, KisPaintDeviceSP targetDevice) { m_d->paintDevice->framesInterface()->fetchFrame(frameId(keyframe), targetDevice); } void KisRasterKeyframeChannel::importFrame(int time, KisPaintDeviceSP sourceDevice, KUndo2Command *parentCommand) { KisKeyframeSP keyframe = addKeyframe(time, parentCommand); const int frame = frameId(keyframe); m_d->paintDevice->framesInterface()->uploadFrame(frame, sourceDevice); } QRect KisRasterKeyframeChannel::frameExtents(KisKeyframeSP keyframe) { return m_d->paintDevice->framesInterface()->frameBounds(frameId(keyframe)); } QString KisRasterKeyframeChannel::frameFilename(int frameId) const { return m_d->frameFilenames.value(frameId, QString()); } void KisRasterKeyframeChannel::setFilenameSuffix(const QString &suffix) { m_d->filenameSuffix = suffix; } void KisRasterKeyframeChannel::setFrameFilename(int frameId, const QString &filename) { - Q_ASSERT(!m_d->frameFilenames.contains(frameId)); + KIS_SAFE_ASSERT_RECOVER_NOOP(!m_d->frameFilenames.contains(frameId)); m_d->frameFilenames.insert(frameId, filename); + m_d->framesByFilename.insert(filename, frameId); } QString KisRasterKeyframeChannel::chooseFrameFilename(int frameId, const QString &layerFilename) { QString filename; if (m_d->frameFilenames.isEmpty()) { // Use legacy naming convention for first keyframe filename = layerFilename + m_d->filenameSuffix; } else { filename = layerFilename + m_d->filenameSuffix + ".f" + QString::number(frameId); } setFrameFilename(frameId, filename); return filename; } KisKeyframeSP KisRasterKeyframeChannel::createKeyframe(int time, const KisKeyframeSP copySrc, KUndo2Command *parentCommand) { KisRasterKeyframe *keyframe; if (!copySrc) { int frameId = m_d->paintDevice->framesInterface()->createFrame(false, 0, QPoint(), parentCommand); keyframe = new KisRasterKeyframe(this, time, frameId); } else { int srcFrame = frameId(copySrc); int frameId = m_d->paintDevice->framesInterface()->createFrame(true, srcFrame, QPoint(), parentCommand); KisRasterKeyframe *srcKeyframe = dynamic_cast(copySrc.data()); Q_ASSERT(srcKeyframe); keyframe = new KisRasterKeyframe(srcKeyframe, this); keyframe->setTime(time); keyframe->frameId = frameId; } - return toQShared(keyframe); + KisKeyframeSP key = toQShared(keyframe); + + m_d->frameInstances[keyframe->frameId].append(key); + + return key; } void KisRasterKeyframeChannel::destroyKeyframe(KisKeyframeSP key, KUndo2Command *parentCommand) { - m_d->paintDevice->framesInterface()->deleteFrame(frameId(key), parentCommand); + int id = frameId(key); + + QVector &instances = m_d->frameInstances[id]; + instances.removeAll(key); + + if (instances.isEmpty()) { + m_d->paintDevice->framesInterface()->deleteFrame(id, parentCommand); + m_d->frameInstances.remove(id); + } } void KisRasterKeyframeChannel::uploadExternalKeyframe(KisKeyframeChannel *srcChannel, int srcTime, KisKeyframeSP dstFrame) { KisRasterKeyframeChannel *srcRasterChannel = dynamic_cast(srcChannel); KIS_ASSERT_RECOVER_RETURN(srcRasterChannel); const int srcId = srcRasterChannel->frameIdAt(srcTime); const int dstId = frameId(dstFrame); m_d->paintDevice->framesInterface()-> uploadFrame(srcId, dstId, srcRasterChannel->m_d->paintDevice); } QRect KisRasterKeyframeChannel::affectedRect(KisKeyframeSP key) { KeyframesMap::iterator it = keys().find(key->time()); QRect rect; // Calculate changed area as the union of the current and previous keyframe. // This makes sure there are no artifacts left over from the previous frame // where the new one doesn't cover the area. if (it == keys().begin()) { // Using the *next* keyframe at the start of the timeline avoids artifacts // when deleting or moving the first key it++; } else { it--; } if (it != keys().end()) { rect = m_d->paintDevice->framesInterface()->frameBounds(frameId(it.value())); } rect |= m_d->paintDevice->framesInterface()->frameBounds(frameId(key)); if (m_d->onionSkinsEnabled) { const QRect dirtyOnionSkinsRect = KisOnionSkinCompositor::instance()->calculateFullExtent(m_d->paintDevice); rect |= dirtyOnionSkinsRect; } return rect; } QDomElement KisRasterKeyframeChannel::toXML(QDomDocument doc, const QString &layerFilename) { m_d->frameFilenames.clear(); return KisKeyframeChannel::toXML(doc, layerFilename); } void KisRasterKeyframeChannel::loadXML(const QDomElement &channelNode) { m_d->frameFilenames.clear(); KisKeyframeChannel::loadXML(channelNode); } void KisRasterKeyframeChannel::saveKeyframe(KisKeyframeSP keyframe, QDomElement keyframeElement, const QString &layerFilename) { int frame = frameId(keyframe); QString filename = frameFilename(frame); if (filename.isEmpty()) { filename = chooseFrameFilename(frame, layerFilename); } keyframeElement.setAttribute("frame", filename); QPoint offset = m_d->paintDevice->framesInterface()->frameOffset(frame); KisDomUtils::saveValue(&keyframeElement, "offset", offset); } KisKeyframeSP KisRasterKeyframeChannel::loadKeyframe(const QDomElement &keyframeNode) { int time = keyframeNode.attribute("time").toUInt(); QPoint offset; KisDomUtils::loadValue(keyframeNode, "offset", &offset); QString frameFilename = keyframeNode.attribute("frame"); KisKeyframeSP keyframe; if (m_d->frameFilenames.isEmpty()) { // First keyframe loaded: use the existing frame - Q_ASSERT(keyframeCount() == 1); + KIS_SAFE_ASSERT_RECOVER_NOOP(keyframeCount() == 1); + keyframe = constKeys().begin().value(); + const int id = frameId(keyframe); + setFrameFilename(id, frameFilename); // Remove from keys. It will get reinserted with new time once we return keys().remove(keyframe->time()); keyframe->setTime(time); - m_d->paintDevice->framesInterface()->setFrameOffset(frameId(keyframe), offset); + m_d->paintDevice->framesInterface()->setFrameOffset(id, offset); } else { - KUndo2Command tempCommand; - int frameId = m_d->paintDevice->framesInterface()->createFrame(false, 0, offset, &tempCommand); + int frameId = m_d->framesByFilename.value(frameFilename, -1); + + if (frameId == -1) { + KUndo2Command tempCommand; + frameId = m_d->paintDevice->framesInterface()->createFrame(false, 0, offset, &tempCommand); + setFrameFilename(frameId, frameFilename); + } keyframe = toQShared(new KisRasterKeyframe(this, time, frameId)); + m_d->frameInstances[frameId].append(keyframe); } - setFrameFilename(frameId(keyframe), frameFilename); - return keyframe; } bool KisRasterKeyframeChannel::keyframeHasContent(const KisKeyframe *keyframe) const { return !m_d->paintDevice->framesInterface()->frameBounds(frameId(keyframe)).isEmpty(); } bool KisRasterKeyframeChannel::hasScalarValue() const { return false; } void KisRasterKeyframeChannel::setOnionSkinsEnabled(bool value) { m_d->onionSkinsEnabled = value; } bool KisRasterKeyframeChannel::onionSkinsEnabled() const { return m_d->onionSkinsEnabled; } diff --git a/libs/image/kis_raster_keyframe_channel.h b/libs/image/kis_raster_keyframe_channel.h index 2ccec11eec..8d10d844d5 100644 --- a/libs/image/kis_raster_keyframe_channel.h +++ b/libs/image/kis_raster_keyframe_channel.h @@ -1,95 +1,97 @@ /* * 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_RASTER_KEYFRAME_CHANNEL_H #define _KIS_RASTER_KEYFRAME_CHANNEL_H #include "kis_keyframe_channel.h" class KRITAIMAGE_EXPORT KisRasterKeyframeChannel : public KisKeyframeChannel { Q_OBJECT public: KisRasterKeyframeChannel(const KoID& id, const KisPaintDeviceWSP paintDevice, KisDefaultBoundsBaseSP defaultBounds); KisRasterKeyframeChannel(const KisRasterKeyframeChannel &rhs, KisNode *newParentNode, const KisPaintDeviceWSP newPaintDevice); ~KisRasterKeyframeChannel() override; + KisKeyframeSP linkKeyframe(const KisKeyframeSP keyframe, int newTime, KUndo2Command *parentCommand = 0) override; + public: /** * Return the ID of the active frame at a given time. The active frame is * defined by the keyframe at the given time or the last keyframe before it. * @param time * @return active frame id */ int frameIdAt(int time) const; /** * Copy the active frame at given time to target device. * @param keyframe keyframe to copy from * @param targetDevice device to copy the frame to */ void fetchFrame(KisKeyframeSP keyframe, KisPaintDeviceSP targetDevice); /** * Copy the content of the sourceDevice into a new keyframe at given time * @param time position of new keyframe * @param sourceDevice source for content */ void importFrame(int time, KisPaintDeviceSP sourceDevice, KUndo2Command *parentCommand); QRect frameExtents(KisKeyframeSP keyframe); QString frameFilename(int frameId) const; /** * When choosing filenames for frames, this will be appended to the node filename */ void setFilenameSuffix(const QString &suffix); bool hasScalarValue() const override; QDomElement toXML(QDomDocument doc, const QString &layerFilename) override; void loadXML(const QDomElement &channelNode) override; void setOnionSkinsEnabled(bool value); bool onionSkinsEnabled() const; protected: KisKeyframeSP createKeyframe(int time, const KisKeyframeSP copySrc, KUndo2Command *parentCommand) override; void destroyKeyframe(KisKeyframeSP key, KUndo2Command *parentCommand) override; void uploadExternalKeyframe(KisKeyframeChannel *srcChannel, int srcTime, KisKeyframeSP dstFrame) override; QRect affectedRect(KisKeyframeSP key) override; void saveKeyframe(KisKeyframeSP keyframe, QDomElement keyframeElement, const QString &layerFilename) override; KisKeyframeSP loadKeyframe(const QDomElement &keyframeNode) override; friend class KisRasterKeyframe; bool keyframeHasContent(const KisKeyframe *keyframe) const; private: void setFrameFilename(int frameId, const QString &filename); QString chooseFrameFilename(int frameId, const QString &layerFilename); int frameId(KisKeyframeSP keyframe) const; int frameId(const KisKeyframe *keyframe) const; struct Private; QScopedPointer m_d; }; #endif diff --git a/plugins/dockers/animation/kis_animation_utils.cpp b/plugins/dockers/animation/kis_animation_utils.cpp index 0f0c528f4d..9957235d29 100644 --- a/plugins/dockers/animation/kis_animation_utils.cpp +++ b/plugins/dockers/animation/kis_animation_utils.cpp @@ -1,345 +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->keyframeAt(time)) { KisKeyframeSP srcFrame = channel->activeKeyframeAt(time); channel->copyKeyframe(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); 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, bool copy, bool moveEmptyFrames, KUndo2Command *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); if (srcKeyframe) { - if (copy) { + if (action == CopyKeyframes) { srcChannel->copyKeyframe(srcKeyframe, dstTime, parentCommand); + } else if (action == LinkKeyframes) { + srcChannel->linkKeyframe(srcKeyframe, dstTime, parentCommand); } else { srcChannel->moveKeyframe(srcKeyframe, dstTime, parentCommand); } } else { - if (dstKeyFrame && moveEmptyFrames && !copy) { + 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 (!copy) { + 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, moveEmpty, parentCommand); + return createMoveKeyframesCommand(srcDstPairs, copy ? CopyKeyframes : MoveKeyframes, moveEmpty, parentCommand); } KUndo2Command* createMoveKeyframesCommand(const FrameMovePairList &srcDstPairs, - bool copy, + KeyframeAction action, bool moveEmptyFrames, KUndo2Command *parentCommand) { KUndo2Command *cmd = new KisCommandUtils::LambdaCommand( - !copy ? - kundo2_i18np("Move Keyframe", - "Move %1 Keyframes", - srcDstPairs.size()) : + (action == CopyKeyframes) ? kundo2_i18np("Copy Keyframe", "Copy %1 Keyframes", - srcDstPairs.size()), + srcDstPairs.size()) : + (action == LinkKeyframes) ? + kundo2_i18np("Link Keyframe", + "Link %1 Keyframes", + srcDstPairs.size()) : + kundo2_i18np("Move Keyframe", + "Move %1 Keyframes", + srcDstPairs.size()), parentCommand, - [srcDstPairs, copy, moveEmptyFrames] () -> KUndo2Command* { + [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, copy, moveEmptyFrames, cmd.data()); + 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_animation_utils.h b/plugins/dockers/animation/kis_animation_utils.h index aa86fc14a2..8b08bbd4e1 100644 --- a/plugins/dockers/animation/kis_animation_utils.h +++ b/plugins/dockers/animation/kis_animation_utils.h @@ -1,104 +1,110 @@ /* * 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. */ #ifndef __KIS_ANIMATION_UTILS_H #define __KIS_ANIMATION_UTILS_H #include "kis_types.h" #include #include #include namespace KisAnimationUtils { + enum KeyframeAction { + CopyKeyframes, + MoveKeyframes, + LinkKeyframes + }; + KUndo2Command* createKeyframeCommand(KisImageSP image, KisNodeSP node, const QString &channelId, int time, bool copy, KUndo2Command *parentCommand = 0); void createKeyframeLazy(KisImageSP image, KisNodeSP node, const QString &channel, int time, bool copy); struct KRITAANIMATIONDOCKER_EXPORT FrameItem : public boost::equality_comparable { FrameItem() : time(-1) {} FrameItem(KisNodeSP _node, const QString &_channel, int _time) : node(_node), channel(_channel), time(_time) {} bool operator==(const FrameItem &rhs) const { return rhs.node == node && rhs.channel == channel && rhs.time == time; } KisNodeSP node; QString channel; int time; }; KRITAANIMATIONDOCKER_EXPORT QDebug operator<<(QDebug dbg, const FrameItem &item); inline uint qHash(const FrameItem &item) { return ::qHash(item.node.data()) + ::qHash(item.channel) + ::qHash(item.time); } typedef QVector FrameItemList; typedef std::pair FrameMovePair; typedef QVector FrameMovePairList; void removeKeyframes(KisImageSP image, const FrameItemList &frames); void removeKeyframe(KisImageSP image, KisNodeSP node, const QString &channel, int time); void sortPointsForSafeMove(QModelIndexList *points, const QPoint &offset); KUndo2Command* createMoveKeyframesCommand(const FrameItemList &srcFrames, const FrameItemList &dstFrames, bool copy, bool moveEmpty, KUndo2Command *parentCommand = 0); /** * @brief implements safe moves of the frames (even if there are cycling move dependencies) * @param movePairs the jobs for the moves * @param copy shows if the frames should be copied or not * @param moveEmpty allows an empty frame to replace a populated one * @param parentCommand the command that should be a parent of the created command * @return a created undo command */ KRITAANIMATIONDOCKER_EXPORT KUndo2Command* createMoveKeyframesCommand(const FrameMovePairList &movePairs, - bool copy, bool moveEmptyFrames, KUndo2Command *parentCommand = 0); + KeyframeAction action, bool moveEmptyFrames, KUndo2Command *parentCommand = 0); bool supportsContentFrames(KisNodeSP node); extern const QString addFrameActionName; extern const QString duplicateFrameActionName; extern const QString removeFrameActionName; extern const QString removeFramesActionName; extern const QString lazyFrameCreationActionName; extern const QString dropFramesActionName; extern const QString showLayerActionName; extern const QString newLayerActionName; extern const QString addExistingLayerActionName; extern const QString removeLayerActionName; extern const QString addOpacityKeyframeActionName; extern const QString addTransformKeyframeActionName; extern const QString removeOpacityKeyframeActionName; extern const QString removeTransformKeyframeActionName; }; #endif /* __KIS_ANIMATION_UTILS_H */ diff --git a/plugins/dockers/animation/tests/kis_animation_utils_test.cpp b/plugins/dockers/animation/tests/kis_animation_utils_test.cpp index 31437888a9..3823505311 100644 --- a/plugins/dockers/animation/tests/kis_animation_utils_test.cpp +++ b/plugins/dockers/animation/tests/kis_animation_utils_test.cpp @@ -1,227 +1,227 @@ #include "kis_animation_utils_test.h" #include #include #include #include "kis_image_animation_interface.h" #include "kis_paint_layer.h" #include "kis_keyframe_channel.h" #include #include "kis_animation_utils.h" #include bool verifyFrames(TestUtil::MaskParent &p, const QVector &nodes, const QVector> &offsets) { KisImageAnimationInterface *i = p.image->animationInterface(); Q_FOREACH (const auto &offset, offsets) { int time = 0; QRect rc1; QRect rc2; std::tie(time, rc1, rc2) = offset; i->switchCurrentTimeAsync(time); p.image->waitForDone(); KIS_SAFE_ASSERT_RECOVER_NOOP(nodes[0]->paintDevice()->defaultBounds()->currentTime() == time); KIS_SAFE_ASSERT_RECOVER_NOOP(nodes[1]->paintDevice()->defaultBounds()->currentTime() == time); KisKeyframeChannel *channel1 = nodes[0]->getKeyframeChannel("content"); KisKeyframeChannel *channel2 = nodes[1]->getKeyframeChannel("content"); if (!rc1.isValid() && !channel1->keyframeAt(time)) { // noop } else if (nodes[0]->paintDevice()->exactBounds() != rc1) { qWarning() << "Compared values are not the same:"; qWarning() << " " << ppVar(nodes[0]->paintDevice()->exactBounds()); qWarning() << " " << ppVar(rc1); qWarning() << " " << ppVar(time); return false; } if (!rc2.isValid() && !channel2->keyframeAt(time)) { // noop } else if (nodes[1]->paintDevice()->exactBounds() != rc2) { qWarning() << "Compared values are not the same:"; qWarning() << " " << ppVar(nodes[1]->paintDevice()->exactBounds()); qWarning() << " " << ppVar(rc2); qWarning() << " " << ppVar(time); return false; } } return true; } void KisAnimationUtilsTest::test() { QRect refRect(QRect(0,0,512,512)); TestUtil::MaskParent p(refRect); const KoColor fillColor(Qt::red, p.image->colorSpace()); KisPaintLayerSP layer1 = p.layer; KisPaintLayerSP layer2 = new KisPaintLayer(p.image, "paint2", OPACITY_OPAQUE_U8); p.image->addNode(layer2); QVector nodes({layer1, layer2}); KisPaintDeviceSP dev1 = layer1->paintDevice(); KisPaintDeviceSP dev2 = layer2->paintDevice(); KisKeyframeChannel *channel1 = layer1->getKeyframeChannel(KisKeyframeChannel::Content.id(), true); KisKeyframeChannel *channel2 = layer2->getKeyframeChannel(KisKeyframeChannel::Content.id(), true); channel1->addKeyframe(0); channel2->addKeyframe(0); channel1->addKeyframe(10); channel2->addKeyframe(10); channel1->addKeyframe(20); channel2->addKeyframe(20); KisImageAnimationInterface *i = p.image->animationInterface(); i->switchCurrentTimeAsync(0); p.image->waitForDone(); dev1->fill(QRect(0, 0, 10, 10), fillColor); dev2->fill(QRect(0, 10, 10, 10), fillColor); i->switchCurrentTimeAsync(10); p.image->waitForDone(); dev1->fill(QRect(10, 0, 10, 10), fillColor); dev2->fill(QRect(10, 10, 10, 10), fillColor); i->switchCurrentTimeAsync(20); p.image->waitForDone(); dev1->fill(QRect(20, 0, 10, 10), fillColor); dev2->fill(QRect(20, 10, 10, 10), fillColor); QVector> initialReferenceRects; initialReferenceRects << std::make_tuple( 0, QRect( 0, 0, 10, 10), QRect( 0, 10, 10, 10)); initialReferenceRects << std::make_tuple(10, QRect(10, 0, 10, 10), QRect(10, 10, 10, 10)); initialReferenceRects << std::make_tuple(20, QRect(20, 0, 10, 10), QRect(20, 10, 10, 10)); QVERIFY(verifyFrames(p, nodes, initialReferenceRects)); using namespace KisAnimationUtils; FrameMovePairList frameMoves; // // Cycling single-layer move // frameMoves << std::make_pair(FrameItem(layer1, "content", 0), FrameItem(layer1, "content", 10)); frameMoves << std::make_pair(FrameItem(layer1, "content", 10), FrameItem(layer1, "content", 20)); frameMoves << std::make_pair(FrameItem(layer1, "content", 20), FrameItem(layer1, "content", 0)); - QScopedPointer cmd(createMoveKeyframesCommand(frameMoves, false, false)); + QScopedPointer cmd(createMoveKeyframesCommand(frameMoves, MoveKeyframes, false)); cmd->redo(); QVector> referenceRects; referenceRects << std::make_tuple( 0, QRect(20, 0, 10, 10), QRect( 0, 10, 10, 10)); referenceRects << std::make_tuple(10, QRect( 0, 0, 10, 10), QRect(10, 10, 10, 10)); referenceRects << std::make_tuple(20, QRect(10, 0, 10, 10), QRect(20, 10, 10, 10)); QVERIFY(verifyFrames(p, nodes, referenceRects)); cmd->undo(); QVERIFY(verifyFrames(p, nodes, initialReferenceRects)); frameMoves.clear(); referenceRects.clear(); cmd.reset(); // // Just a complex non-cycling move // frameMoves << std::make_pair(FrameItem(layer1, "content", 0), FrameItem(layer2, "content", 10)); frameMoves << std::make_pair(FrameItem(layer2, "content", 10), FrameItem(layer1, "content", 10)); frameMoves << std::make_pair(FrameItem(layer1, "content", 20), FrameItem(layer2, "content", 20)); - cmd.reset(createMoveKeyframesCommand(frameMoves, false, false)); + cmd.reset(createMoveKeyframesCommand(frameMoves, MoveKeyframes, false)); cmd->redo(); referenceRects << std::make_tuple( 0, QRect() , QRect( 0, 10, 10, 10)); referenceRects << std::make_tuple(10, QRect(10, 10, 10, 10), QRect( 0, 0, 10, 10)); referenceRects << std::make_tuple(20, QRect() , QRect(20, 0, 10, 10)); QVERIFY(verifyFrames(p, nodes, referenceRects)); cmd->undo(); QVERIFY(verifyFrames(p, nodes, initialReferenceRects)); frameMoves.clear(); referenceRects.clear(); cmd.reset(); // // Cross-node swap of the frames // frameMoves << std::make_pair(FrameItem(layer1, "content", 0), FrameItem(layer2, "content", 0)); frameMoves << std::make_pair(FrameItem(layer1, "content", 10), FrameItem(layer2, "content", 10)); frameMoves << std::make_pair(FrameItem(layer1, "content", 20), FrameItem(layer2, "content", 20)); frameMoves << std::make_pair(FrameItem(layer2, "content", 0), FrameItem(layer1, "content", 0)); frameMoves << std::make_pair(FrameItem(layer2, "content", 10), FrameItem(layer1, "content", 10)); frameMoves << std::make_pair(FrameItem(layer2, "content", 20), FrameItem(layer1, "content", 20)); - cmd.reset(createMoveKeyframesCommand(frameMoves, false, false)); + cmd.reset(createMoveKeyframesCommand(frameMoves, MoveKeyframes, false)); cmd->redo(); referenceRects << std::make_tuple( 0, QRect( 0, 10, 10, 10), QRect( 0, 0, 10, 10)); referenceRects << std::make_tuple(10, QRect(10, 10, 10, 10), QRect(10, 0, 10, 10)); referenceRects << std::make_tuple(20, QRect(20, 10, 10, 10), QRect(20, 0, 10, 10)); QVERIFY(verifyFrames(p, nodes, referenceRects)); cmd->undo(); QVERIFY(verifyFrames(p, nodes, initialReferenceRects)); frameMoves.clear(); referenceRects.clear(); cmd.reset(); // // Cross-node move and swap // frameMoves << std::make_pair(FrameItem(layer1, "content", 0), FrameItem(layer2, "content", 0)); frameMoves << std::make_pair(FrameItem(layer1, "content", 10), FrameItem(layer2, "content", 10)); frameMoves << std::make_pair(FrameItem(layer1, "content", 20), FrameItem(layer2, "content", 20)); frameMoves << std::make_pair(FrameItem(layer2, "content", 0), FrameItem(layer1, "content", 0)); frameMoves << std::make_pair(FrameItem(layer2, "content", 10), FrameItem(layer1, "content", 9)); frameMoves << std::make_pair(FrameItem(layer2, "content", 20), FrameItem(layer1, "content", 20)); - cmd.reset(createMoveKeyframesCommand(frameMoves, false, false)); + cmd.reset(createMoveKeyframesCommand(frameMoves, MoveKeyframes, false)); cmd->redo(); referenceRects << std::make_tuple( 0, QRect( 0, 10, 10, 10), QRect( 0, 0, 10, 10)); referenceRects << std::make_tuple( 9, QRect(10, 10, 10, 10), QRect() ); referenceRects << std::make_tuple(10, QRect() , QRect(10, 0, 10, 10)); referenceRects << std::make_tuple(20, QRect(20, 10, 10, 10), QRect(20, 0, 10, 10)); QVERIFY(verifyFrames(p, nodes, referenceRects)); cmd->undo(); QVERIFY(verifyFrames(p, nodes, initialReferenceRects)); frameMoves.clear(); referenceRects.clear(); cmd.reset(); } QTEST_MAIN(KisAnimationUtilsTest) diff --git a/plugins/dockers/animation/timeline_frames_model.cpp b/plugins/dockers/animation/timeline_frames_model.cpp index 1bbd094bf4..5e41851128 100644 --- a/plugins/dockers/animation/timeline_frames_model.cpp +++ b/plugins/dockers/animation/timeline_frames_model.cpp @@ -1,980 +1,987 @@ /* * 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 "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_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 = dummy->node()->getKeyframeChannel(KisKeyframeChannel::Content.id()); return (primaryChannel && primaryChannel->keyframeAt(column)); } bool frameHasContent(int row, int column) { KisNodeDummy *dummy = converter->dummyFromRow(row); KisKeyframeChannel *primaryChannel = dummy->node()->getKeyframeChannel(KisKeyframeChannel::Content.id()); if (!primaryChannel) return false; // first check if we are a key frame KisKeyframeSP frame = primaryChannel->activeKeyframeAt(column); if (!frame) return false; return frame->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; } int frameColorLabel(int row, int column) { KisNodeDummy *dummy = converter->dummyFromRow(row); if (!dummy) return -1; KisKeyframeChannel *primaryChannel = dummy->node()->getKeyframeChannel(KisKeyframeChannel::Content.id()); if (!primaryChannel) return -1; KisKeyframeSP frame = primaryChannel->activeKeyframeAt(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()); if (!primaryChannel) return; KisKeyframeSP frame = primaryChannel->keyframeAt(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(); } 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; } }; 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 FrameColorLabelIndexRole: { int label = m_d->frameColorLabel(index.row(), index.column()); return label > 0 ? label : QVariant(); } 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; + return Qt::MoveAction | Qt::CopyAction | Qt::LinkAction; } Qt::DropActions TimelineFramesModel::supportedDropActions() const { - return Qt::MoveAction | Qt::CopyAction; + 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) || + 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, copyFrames, false, 0); + 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; 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()); if (keyFrame) { uniqueKeyframesInSelection.insert(keyFrame); } } QList keyframesToMove; for (auto it = uniqueKeyframesInSelection.begin(); it != uniqueKeyframesInSelection.end(); ++it) { KisKeyframeSP keyframe = *it; KisKeyframeChannel *channel = keyframe->channel(); KisKeyframeSP nextKeyframe = channel->nextKeyframe(keyframe); if (nextKeyframe) { keyframesToMove << nextKeyframe; } } std::sort(keyframesToMove.begin(), keyframesToMove.end(), [] (KisKeyframeSP lhs, KisKeyframeSP 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) { int plannedFrameMove = count; if (count < 0) { KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(keyframe->time() > 0, false); KisKeyframeSP prevFrame = keyframe->channel()->previousKeyframe(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, false, 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; } 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); }