1 /*
2  *  Copyright (c) 2015 Dmitry Kazakov <dimula73@gmail.com>
3  *
4  *  This program is free software; you can redistribute it and/or modify
5  *  it under the terms of the GNU General Public License as published by
6  *  the Free Software Foundation; either version 2 of the License, or
7  *  (at your option) any later version.
8  *
9  *  This program is distributed in the hope that it will be useful,
10  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
11  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  *  GNU General Public License for more details.
13  *
14  *  You should have received a copy of the GNU General Public License
15  *  along with this program; if not, write to the Free Software
16  *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17  */
18 
19 #include "kis_animation_utils.h"
20 
21 #include "kundo2command.h"
22 #include "kis_algebra_2d.h"
23 #include "kis_image.h"
24 #include "kis_node.h"
25 #include "kis_keyframe_channel.h"
26 #include "kis_post_execution_undo_adapter.h"
27 #include "kis_global.h"
28 #include "kis_tool_utils.h"
29 #include "kis_image_animation_interface.h"
30 #include "kis_command_utils.h"
31 #include "kis_processing_applicator.h"
32 #include "kis_transaction.h"
33 
34 
35 namespace KisAnimationUtils {
36     const QString lazyFrameCreationActionName = i18n("Auto Frame Mode");
37     const QString dropFramesActionName = i18n("Drop Frames");
38 
39     const QString newLayerActionName = i18n("New Layer");
40     const QString addExistingLayerActionName = i18n("Add Existing Layer");
41     const QString removeLayerActionName = i18n("Remove Layer");
42 
43     const QString addTransformKeyframeActionName = i18n("Add transform keyframe");
44     const QString removeTransformKeyframeActionName = i18n("Remove transform keyframe");
45 
createKeyframeCommand(KisImageSP image,KisNodeSP node,const QString & channelId,int time,bool copy,KUndo2Command * parentCommand)46     KUndo2Command* createKeyframeCommand(KisImageSP image, KisNodeSP node, const QString &channelId, int time, bool copy, KUndo2Command *parentCommand) {
47         KUndo2Command *cmd = new KisCommandUtils::LambdaCommand(
48             copy ? kundo2_i18n("Copy Keyframe") :
49                    kundo2_i18n("Add Keyframe"),
50             parentCommand,
51 
52             [image, node, channelId, time, copy] () mutable -> KUndo2Command* {
53                 bool result = false;
54 
55                 QScopedPointer<KUndo2Command> cmd(new KUndo2Command());
56 
57                 KisKeyframeChannel *channel = node->getKeyframeChannel(channelId);
58                 quint8 originalOpacity = node->opacity();
59                 bool createdChannel = false;
60 
61                 if (!channel) {
62                     node->enableAnimation();
63                     channel = node->getKeyframeChannel(channelId, true);
64                     if (!channel) return nullptr;
65 
66                     createdChannel = true;
67                 }
68 
69                 if (copy) {
70                     if (!channel->keyframeAt(time)) {
71                         KisKeyframeSP srcFrame = channel->activeKeyframeAt(time);
72                         channel->copyKeyframe(srcFrame, time, cmd.data());
73                         result = true;
74                     }
75                 } else {
76                     if (channel->keyframeAt(time) && !createdChannel) {
77                         if (image->animationInterface()->currentTime() == time && channelId == KisKeyframeChannel::Content.id()) {
78 
79                             //shortcut: clearing the image instead
80                             KisPaintDeviceSP device = node->paintDevice();
81                             if (device) {
82                                 const QRect dirtyRect = device->extent();
83 
84                                 KisTransaction transaction(kundo2_i18n("Clear"), device, cmd.data());
85                                 device->clear();
86                                 (void) transaction.endAndTake(); // saved as 'parent'
87 
88                                 node->setDirty(dirtyRect);
89 
90                                 result = true;
91                             }
92                         }
93                     } else {
94                         channel->addKeyframe(time, cmd.data());
95                         result = true;
96                     }
97                 }
98 
99                 // when a new opacity keyframe is created, the opacity is set to 0
100                 // this makes sure to use the opacity that was previously used
101                 // maybe there is a better way to do this
102                 node->setOpacity(originalOpacity);
103 
104                 return result ? new KisCommandUtils::SkipFirstRedoWrapper(cmd.take()) : nullptr;
105         });
106 
107         return cmd;
108     }
109 
createKeyframeLazy(KisImageSP image,KisNodeSP node,const QString & channelId,int time,bool copy)110     void createKeyframeLazy(KisImageSP image, KisNodeSP node, const QString &channelId, int time, bool copy)
111     {
112         KUndo2Command *cmd = createKeyframeCommand(image, node, channelId, time, copy);
113         KisProcessingApplicator::runSingleCommandStroke(image, cmd,
114                                                         KisStrokeJobData::BARRIER,
115                                                         KisStrokeJobData::EXCLUSIVE);
116     }
117 
removeKeyframes(KisImageSP image,const FrameItemList & frames)118     void removeKeyframes(KisImageSP image, const FrameItemList &frames) {
119         KIS_SAFE_ASSERT_RECOVER_RETURN(!image->locked());
120 
121         KUndo2Command *cmd = new KisCommandUtils::LambdaCommand(
122             kundo2_i18np("Remove Keyframe",
123                          "Remove Keyframes",
124                          frames.size()),
125 
126             [image, frames] () {
127                 bool result = false;
128 
129                 QScopedPointer<KUndo2Command> cmd(new KUndo2Command());
130 
131                 Q_FOREACH (const FrameItem &item, frames) {
132                     const int time = item.time;
133                     KisNodeSP node = item.node;
134                     KisKeyframeChannel *channel = 0;
135                     if (node) {
136                         channel = node->getKeyframeChannel(item.channel);
137                     }
138                     if (!channel) continue;
139 
140                     KisKeyframeSP keyframe = channel->keyframeAt(time);
141                     if (!keyframe) continue;
142 
143                     channel->deleteKeyframe(keyframe, cmd.data());
144 
145                     result = true;
146                 }
147 
148                 return result ? new KisCommandUtils::SkipFirstRedoWrapper(cmd.take()) : 0;
149         });
150 
151         KisProcessingApplicator::runSingleCommandStroke(image, cmd,
152                                                         KisStrokeJobData::BARRIER,
153                                                         KisStrokeJobData::EXCLUSIVE);
154     }
155 
removeKeyframe(KisImageSP image,KisNodeSP node,const QString & channel,int time)156     void removeKeyframe(KisImageSP image, KisNodeSP node, const QString &channel, int time) {
157         QVector<FrameItem> frames;
158         frames << FrameItem(node, channel, time);
159         removeKeyframes(image, frames);
160     }
161 
162 
163     struct LessOperator {
LessOperatorKisAnimationUtils::LessOperator164         LessOperator(const QPoint &offset)
165             : m_columnCoeff(-KisAlgebra2D::signPZ(offset.x())),
166               m_rowCoeff(-1000000 * KisAlgebra2D::signZZ(offset.y()))
167         {
168         }
169 
operator ()KisAnimationUtils::LessOperator170         bool operator()(const QModelIndex &lhs, const QModelIndex &rhs) {
171             return
172                 m_columnCoeff * lhs.column() + m_rowCoeff * lhs.row() <
173                 m_columnCoeff * rhs.column() + m_rowCoeff * rhs.row();
174         }
175 
176     private:
177         int m_columnCoeff;
178         int m_rowCoeff;
179     };
180 
sortPointsForSafeMove(QModelIndexList * points,const QPoint & offset)181     void sortPointsForSafeMove(QModelIndexList *points, const QPoint &offset)
182     {
183         std::sort(points->begin(), points->end(), LessOperator(offset));
184     }
185 
supportsContentFrames(KisNodeSP node)186     bool supportsContentFrames(KisNodeSP node)
187     {
188         return node->inherits("KisPaintLayer") || node->inherits("KisFilterMask") || node->inherits("KisTransparencyMask") || node->inherits("KisSelectionBasedLayer");
189     }
190 
swapOneFrameItem(const FrameItem & src,const FrameItem & dst,KUndo2Command * parentCommand)191     void swapOneFrameItem(const FrameItem &src, const FrameItem &dst, KUndo2Command *parentCommand)
192     {
193         const int srcTime = src.time;
194         KisNodeSP srcNode = src.node;
195         KisKeyframeChannel *srcChannel = srcNode->getKeyframeChannel(src.channel);
196 
197         const int dstTime = dst.time;
198         KisNodeSP dstNode = dst.node;
199         KisKeyframeChannel *dstChannel = dstNode->getKeyframeChannel(dst.channel, true);
200 
201         if (srcNode == dstNode) {
202             if (!srcChannel) return; // TODO: add warning!
203 
204             srcChannel->swapFrames(srcTime, dstTime, parentCommand);
205         } else {
206             if (!srcChannel || !dstChannel) return; // TODO: add warning!
207 
208             dstChannel->swapExternalKeyframe(srcChannel, srcTime, dstTime, parentCommand);
209         }
210     }
211 
moveOneFrameItem(const FrameItem & src,const FrameItem & dst,bool copy,bool moveEmptyFrames,KUndo2Command * parentCommand)212     void moveOneFrameItem(const FrameItem &src, const FrameItem &dst, bool copy, bool moveEmptyFrames, KUndo2Command *parentCommand)
213     {
214         const int srcTime = src.time;
215         KisNodeSP srcNode = src.node;
216         KisKeyframeChannel *srcChannel = srcNode->getKeyframeChannel(src.channel);
217 
218         const int dstTime = dst.time;
219         KisNodeSP dstNode = dst.node;
220         KisKeyframeChannel *dstChannel = dstNode->getKeyframeChannel(dst.channel, true);
221 
222         if (srcNode == dstNode) {
223             if (!srcChannel) return; // TODO: add warning!
224 
225             KisKeyframeSP srcKeyframe = srcChannel->keyframeAt(srcTime);
226             KisKeyframeSP dstKeyFrame = srcChannel->keyframeAt(dstTime);
227             if (srcKeyframe) {
228                 if (copy) {
229                     srcChannel->copyKeyframe(srcKeyframe, dstTime, parentCommand);
230                 } else {
231                     srcChannel->moveKeyframe(srcKeyframe, dstTime, parentCommand);
232                 }
233             } else {
234                 if (dstKeyFrame && moveEmptyFrames && !copy) {
235                     //Destination is effectively replaced by an empty frame.
236                     dstChannel->deleteKeyframe(dstKeyFrame, parentCommand);
237                 }
238             }
239         } else {
240             if (!srcChannel || !dstChannel) return; // TODO: add warning!
241 
242             KisKeyframeSP srcKeyframe = srcChannel->keyframeAt(srcTime);
243 
244             if (!srcKeyframe) return; // TODO: add warning!
245 
246             dstChannel->copyExternalKeyframe(srcChannel, srcTime, dstTime, parentCommand);
247 
248             if (!copy) {
249                 srcChannel->deleteKeyframe(srcKeyframe, parentCommand);
250             }
251         }
252     }
253 
createMoveKeyframesCommand(const FrameItemList & srcFrames,const FrameItemList & dstFrames,bool copy,bool moveEmpty,KUndo2Command * parentCommand)254     KUndo2Command* createMoveKeyframesCommand(const FrameItemList &srcFrames,
255                                               const FrameItemList &dstFrames,
256                                               bool copy,
257                                               bool moveEmpty,
258                                               KUndo2Command *parentCommand)
259     {
260         FrameMovePairList srcDstPairs;
261         for (int i = 0; i < srcFrames.size(); i++) {
262             srcDstPairs << std::make_pair(srcFrames[i], dstFrames[i]);
263         }
264         return createMoveKeyframesCommand(srcDstPairs, copy, moveEmpty, parentCommand);
265     }
266 
createMoveKeyframesCommand(const FrameMovePairList & srcDstPairs,bool copy,bool moveEmptyFrames,KUndo2Command * parentCommand)267     KUndo2Command* createMoveKeyframesCommand(const FrameMovePairList &srcDstPairs,
268                                               bool copy,
269                                               bool moveEmptyFrames,
270                                               KUndo2Command *parentCommand)
271     {
272         KUndo2Command *cmd = new KisCommandUtils::LambdaCommand(
273 
274             !copy ?
275                 kundo2_i18np("Move Keyframe",
276                              "Move %1 Keyframes",
277                              srcDstPairs.size()) :
278                 kundo2_i18np("Copy Keyframe",
279                              "Copy %1 Keyframes",
280                              srcDstPairs.size()),
281 
282             parentCommand,
283 
284             [srcDstPairs, copy, moveEmptyFrames] () -> KUndo2Command* {
285                 bool result = false;
286 
287                 QScopedPointer<KUndo2Command> cmd(new KUndo2Command());
288 
289                 using MoveChain = QList<FrameItem>;
290                 QHash<FrameItem, MoveChain> moveMap;
291                 Q_FOREACH (const FrameMovePair &pair, srcDstPairs) {
292                     moveMap.insert(pair.first, {pair.second});
293                 }
294 
295                 auto it = moveMap.begin();
296                 while (it != moveMap.end()) {
297                     MoveChain &chain = it.value();
298                     const FrameItem &previousFrame = chain.last();
299 
300                     auto tailIt = moveMap.find(previousFrame);
301 
302                     if (tailIt == it || tailIt == moveMap.end()) {
303                         ++it;
304                         continue;
305                     }
306 
307                     chain.append(tailIt.value());
308                     tailIt = moveMap.erase(tailIt);
309                     // no incrementing! we are going to check the new tail now!
310                 }
311 
312                 for (it = moveMap.begin(); it != moveMap.end(); ++it) {
313                     MoveChain &chain = it.value();
314                     chain.prepend(it.key());
315                     KIS_SAFE_ASSERT_RECOVER(chain.size() > 1) { continue; }
316 
317                     bool isCycle = false;
318                     if (chain.last() == chain.first()) {
319                         isCycle = true;
320                         chain.takeLast();
321                     }
322 
323                     auto frameIt = chain.rbegin();
324 
325                     FrameItem dstItem = *frameIt++;
326 
327                     while (frameIt != chain.rend()) {
328                         FrameItem srcItem = *frameIt++;
329 
330                         if (!isCycle) {
331                             moveOneFrameItem(srcItem, dstItem, copy, moveEmptyFrames, cmd.data());
332                         } else {
333                             swapOneFrameItem(srcItem, dstItem, cmd.data());
334                         }
335 
336                         dstItem = srcItem;
337                         result = true;
338                     }
339                 }
340 
341                 return result ? new KisCommandUtils::SkipFirstRedoWrapper(cmd.take()) : 0;
342         });
343 
344         return cmd;
345     }
346 
operator <<(QDebug dbg,const FrameItem & item)347     QDebug operator<<(QDebug dbg, const FrameItem &item)
348     {
349         dbg.nospace() << "FrameItem(" << item.node->name() << ", " << item.channel << ", " << item.time << ")";
350         return dbg.space();
351     }
352 
353 }
354 
355