diff --git a/libs/image/kis_keyframe_commands.cpp b/libs/image/kis_keyframe_commands.cpp index dbf5a6888a..5fffb7a182 100644 --- a/libs/image/kis_keyframe_commands.cpp +++ b/libs/image/kis_keyframe_commands.cpp @@ -1,391 +1,393 @@ #include "kis_keyframe_commands.h" #include #include #include "kis_time_range.h" #include "kis_animation_cycle.h" using CycleSP = QSharedPointer; using KeyframeMove = KisKeyframeCommands::KeyframeMove; +using ValidationResult = KisKeyframeCommands::ValidationResult; KisKeyframeCommands::KeyframeMove::KeyframeMove(KisKeyframeBaseSP keyframe, int newTime) : keyframe(keyframe) , oldTime(keyframe->time()) , newTime(newTime) {} class KisMoveKeyframesCommand : public KUndo2Command { public: KisMoveKeyframesCommand(QVector moves, KUndo2Command *parentCommand) : KUndo2Command(parentCommand) , m_moves(moves) {} void redo() override { Q_FOREACH(const KeyframeMove &move, m_moves) { move.keyframe->channel()->moveKeyframeImpl(move.keyframe, move.newTime); } } void undo() override { Q_FOREACH(const KeyframeMove &move, m_moves) { move.keyframe->channel()->moveKeyframeImpl(move.keyframe, move.oldTime); } } private: QVector m_moves; }; struct KeyframeMapping { KisKeyframeChannel *channel; QMap destinations; QMap sources; KeyframeMapping(KisKeyframeChannel *channel, const QVector &moves) : channel(channel) { const int count = moves.count(); for (int i = 0; i < count; i++) { const int time = moves[i].newTime; const KisKeyframeBaseSP key = moves[i].keyframe; if (key->channel() == channel) { destinations[key] = time; sources[time] = key; } } } bool isEmpty() const { return destinations.isEmpty(); } int destination(const KisKeyframeSP &keyframe) const { const int oldTime = keyframe->time(); const int newTime = destinations.value(keyframe, -1); if (newTime == -1 && !sources.value(oldTime, KisKeyframeBaseSP())) { return oldTime; } return newTime; } KisKeyframeSP firstTrueKeyframeAfter(int destinationTime) const { KisKeyframeSP unmovedKeyframe; { int time = destinationTime; KisKeyframeSP key; do { key = channel->nextKeyframe(time); time = key ? key->time() : -1; } while(destinations.contains(key)); unmovedKeyframe = key; } KisKeyframeSP movedKeyframe; int movedTime; { auto sourceIt = KisCollectionUtils::firstAfter(sources, destinationTime); // Skip non-keyframes (e.g. repeat frames) while (sourceIt != sources.cend() && !sourceIt.value().dynamicCast()) { sourceIt++; } movedTime = (sourceIt != sources.cend()) ? sourceIt.key() : -1; } if (movedKeyframe && unmovedKeyframe) { return (movedTime < unmovedKeyframe->time()) ? movedKeyframe : unmovedKeyframe; } else { return movedKeyframe ? movedKeyframe : unmovedKeyframe; } } }; -bool areValidMoveSources(const KisKeyframeChannel *channel, QVector moves) +ValidationResult::Status validateMoveSources(const KisKeyframeChannel *channel, QVector moves) { Q_FOREACH(const KeyframeMove &move, moves) { - if (move.keyframe->channel() != channel) return false; + if (move.keyframe->channel() != channel) return ValidationResult::KeyframesFromDifferentChannels; } std::sort(moves.begin(), moves.end(), [](const KeyframeMove &lhs, const KeyframeMove &rhs){ return lhs.oldTime < rhs.oldTime; } ); for (int i = 1; i < moves.size(); i++) { - if (moves[i - 1].keyframe == moves[i].keyframe) return false; + if (moves[i - 1].keyframe == moves[i].keyframe) return ValidationResult::MultipleDestinations; } - return true; + return ValidationResult::Valid; } CycleSP cycleAfterMove(const CycleSP &cycle, const KeyframeMapping &movedKeyframes) { const KisKeyframeChannel *channel = cycle->channel(); int firstDestination = -1, lastDestination = -1; KisKeyframeSP oldLastKeyframe; KisKeyframeSP newFirstKeyframe, newLastKeyframe; const KisTimeSpan &oldRange = cycle->originalRange(); for (KisKeyframeBaseSP key : channel->itemsWithin(oldRange)) { KisKeyframeSP keyframe = key.dynamicCast(); KIS_SAFE_ASSERT_RECOVER(keyframe) { continue; } const int newTime = movedKeyframes.destination(keyframe); if (newTime >= 0) { if (!newFirstKeyframe || newTime < firstDestination) { firstDestination = newTime; newFirstKeyframe = keyframe; } if (!newLastKeyframe || newTime > lastDestination) { lastDestination = newTime; newLastKeyframe = keyframe; } } oldLastKeyframe = keyframe; } if (!newLastKeyframe) return CycleSP(); const int newEnd = lastDestination; // FIXME const KisTimeSpan &newRange = KisTimeSpan(firstDestination, newEnd); if (newRange == oldRange) { return cycle; } return toQShared(new KisAnimationCycle(*cycle, newRange)); } QVector resolveCycleOverlaps(QVector &cycles, const KeyframeMapping &movedKeyframes) { if (cycles.isEmpty()) return cycles; std::sort(cycles.begin(), cycles.end(), [](const CycleSP &lhs, const CycleSP &rhs) { return lhs->originalRange().start() < rhs->originalRange().start(); }); CycleSP lhs = cycles[0]; for (int i = 1; i < cycles.size(); i++) { const CycleSP &rhs = cycles[i]; const int lhsEnd = lhs->originalRange().end(); const int rhsStart = rhs->originalRange().start(); if (rhsStart < lhsEnd) { const int rhsEnd = rhs->originalRange().end(); if (rhsEnd < lhsEnd) { // Rhs cycle is entirely inside lhs one: drop it cycles[i] = CycleSP(); continue; } else { // TODO: logic for picking the cycle to truncate? const int truncatedStart = movedKeyframes.firstTrueKeyframeAfter(lhsEnd)->time(); cycles[i] = toQShared(new KisAnimationCycle(*rhs, {truncatedStart, rhsEnd})); } } lhs = cycles[i]; } return cycles; } QVector cyclesAfterMoves(const KeyframeMapping &movedKeyframes) { const KisKeyframeChannel *channel = movedKeyframes.destinations.begin().key()->channel(); QVector cycles; Q_FOREACH(const CycleSP cycle, channel->cycles()) { const CycleSP newCycle = cycleAfterMove(cycle, movedKeyframes); if (newCycle) { cycles << newCycle; } } return resolveCycleOverlaps(cycles, movedKeyframes); } bool validateRepeats(const QVector &cycles, const KeyframeMapping &movedKeyframes) { Q_FOREACH(const CycleSP &cycle, cycles) { if (!cycle) continue; const int cycleStart = cycle->originalRange().start(); const int cycleEnd = cycle->originalRange().end(); // If any repeat frame would land within the cycle, refuse the operation. auto keyIt = KisCollectionUtils::firstAfter(movedKeyframes.sources, cycleStart); while (keyIt != movedKeyframes.sources.cend() && keyIt.key() < cycleEnd) { const QSharedPointer repeat = keyIt.value().dynamicCast(); if (repeat) return false; keyIt++; } } return true; } void updateCycles(const KisKeyframeChannel *channel, QVector cyclesAfter, KUndo2Command *parentCommand) { const QList &cyclesBefore = channel->cycles(); // Remove out-of-date definitions Q_FOREACH(const CycleSP &cycle, cyclesBefore) { if (!cyclesAfter.contains(cycle)) { new KisDefineCycleCommand(cycle, nullptr, parentCommand); } } // Add new cycle definitions Q_FOREACH(const CycleSP &cycle, cyclesAfter) { if (!cyclesBefore.contains(cycle)) { new KisDefineCycleCommand(nullptr, cycle, parentCommand); } } } void deleteOverwrittenKeys(KeyframeMapping moves, KUndo2Command *parentCommand) { QVector deletedKeys; for (auto it = moves.destinations.cbegin(); it != moves.destinations.cend(); ++it) { const KisKeyframeBaseSP &keyframe = it.key(); const int newTime = it.value(); const KisKeyframeBaseSP &originalKey = keyframe->channel()->itemAt(newTime); if (originalKey) { const bool isOverwritten = !moves.destinations.contains(originalKey); if (isOverwritten) { deletedKeys.append(originalKey); } } } Q_FOREACH(KisKeyframeBaseSP keyframe, deletedKeys) { new KisReplaceKeyframeCommand(keyframe->channel(), keyframe->time(), KisKeyframeBaseSP(), parentCommand); } } -KUndo2CommandSP KisKeyframeCommands::tryMoveKeyframes(KisKeyframeChannel *channel, QVector moves, KUndo2Command *parentCommand) +ValidationResult KisKeyframeCommands::tryMoveKeyframes(KisKeyframeChannel *channel, QVector moves, KUndo2Command *parentCommand) { KUndo2Command *command = new KUndo2Command(parentCommand); - if (!areValidMoveSources(channel, moves)) return nullptr; + const ValidationResult::Status moveValidation = validateMoveSources(channel, moves); + if (moveValidation != ValidationResult::Valid) return moveValidation; const KeyframeMapping movedKeyframes(channel, moves); - if (movedKeyframes.isEmpty()) return nullptr; + if (movedKeyframes.isEmpty()) return ValidationResult(command); const QVector cycles = cyclesAfterMoves(movedKeyframes); - if (!validateRepeats(cycles, movedKeyframes)) return nullptr; + if (!validateRepeats(cycles, movedKeyframes)) return ValidationResult::RepeatKeyframeWithinCycleDefinition; deleteOverwrittenKeys(movedKeyframes, command); new KisMoveKeyframesCommand(moves, command); updateCycles(channel, cycles, command); - return toQShared(command); + return ValidationResult(command); } KisReplaceKeyframeCommand::KisReplaceKeyframeCommand(KisKeyframeChannel *channel, int time, KisKeyframeBaseSP keyframe, KUndo2Command *parentCommand) : KUndo2Command(parentCommand), m_channel(channel), m_time(time), m_keyframe(keyframe), m_existingKeyframe(0) { } void KisReplaceKeyframeCommand::redo() { m_existingKeyframe = m_channel->replaceKeyframeAt(m_time, m_keyframe); } void KisReplaceKeyframeCommand::undo() { m_channel->replaceKeyframeAt(m_time, m_existingKeyframe); } KisMoveFrameCommand::KisMoveFrameCommand(KisKeyframeChannel *channel, KisKeyframeBaseSP keyframe, int oldTime, int newTime, KUndo2Command *parentCommand) : KUndo2Command(parentCommand), m_channel(channel), m_keyframe(keyframe), m_oldTime(oldTime), m_newTime(newTime) { } void KisMoveFrameCommand::redo() { m_channel->moveKeyframeImpl(m_keyframe, m_newTime); } void KisMoveFrameCommand::undo() { m_channel->moveKeyframeImpl(m_keyframe, m_oldTime); } KisSwapFramesCommand::KisSwapFramesCommand(KisKeyframeChannel *channel, KisKeyframeBaseSP lhsFrame, KisKeyframeBaseSP rhsFrame, KUndo2Command *parentCommand) : KUndo2Command(parentCommand), m_channel(channel), m_lhsFrame(lhsFrame), m_rhsFrame(rhsFrame) { } void KisSwapFramesCommand::redo() { m_channel->swapKeyframesImpl(m_lhsFrame, m_rhsFrame); } void KisSwapFramesCommand::undo() { m_channel->swapKeyframesImpl(m_lhsFrame, m_rhsFrame); } KisDefineCycleCommand::KisDefineCycleCommand(CycleSP oldCycle, CycleSP newCycle, KUndo2Command *parentCommand) : KUndo2Command(parentCommand) , m_channel(oldCycle ? oldCycle->channel() : newCycle->channel()) , m_oldCycle(oldCycle) , m_newCycle(newCycle) {} void KisDefineCycleCommand::redo() { if (m_oldCycle) { m_channel->removeCycle(m_oldCycle); } if (m_newCycle) { m_channel->addCycle(m_newCycle); } } void KisDefineCycleCommand::undo() { if (m_newCycle) { m_channel->removeCycle(m_newCycle); } if (m_oldCycle) { m_channel->addCycle(m_oldCycle); } } QSharedPointer KisDefineCycleCommand::cycle() const { return m_newCycle; } diff --git a/libs/image/kis_keyframe_commands.h b/libs/image/kis_keyframe_commands.h index 03697fd5c6..07e6aa8024 100644 --- a/libs/image/kis_keyframe_commands.h +++ b/libs/image/kis_keyframe_commands.h @@ -1,101 +1,144 @@ /* * 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_COMMANDS_H #define KIS_KEYFRAME_COMMANDS_H #include "kis_keyframe_channel.h" #include "kundo2command.h" #include "kritaimage_export.h" namespace KisKeyframeCommands { + struct ValidationResult { + enum Status { + Valid, + RepeatKeyframeWithinCycleDefinition, + KeyframesFromDifferentChannels, + MultipleDestinations + }; + + ValidationResult(Status error) // NOLINT(google-explicit-constructor) + : status(error) + , command(nullptr) + {} + + explicit ValidationResult(KUndo2Command *command) + : status(Valid) + , command(command) + { + KIS_SAFE_ASSERT_RECOVER_NOOP(command); + } + + bool isValid() const { + return status == Valid; + } + + bool operator==(const ValidationResult &rhs) const + { + return status == rhs.status && command == rhs.command; + } + + bool operator!=(const ValidationResult &rhs) const + { + return !(rhs == *this); + } + + bool operator==(const Status &rhs) const + { + return status == rhs; + } + + Status status; + KUndo2Command *command; + }; + struct KRITAIMAGE_EXPORT KeyframeMove { KisKeyframeBaseSP keyframe; int oldTime, newTime; KeyframeMove() = default; KeyframeMove(KisKeyframeBaseSP keyframe, int newTime); }; /** * Returns either a new command for operations needed to move the keyframes or null if the operation is invalid against the current state of the channel */ - KRITAIMAGE_EXPORT KUndo2CommandSP tryMoveKeyframes(KisKeyframeChannel *channel, QVector moves, KUndo2Command *parentCommand); + KRITAIMAGE_EXPORT ValidationResult tryMoveKeyframes(KisKeyframeChannel *channel, QVector moves, KUndo2Command *parentCommand); } class KRITAIMAGE_EXPORT KisReplaceKeyframeCommand : public KUndo2Command { public: KisReplaceKeyframeCommand(KisKeyframeChannel *channel, int time, KisKeyframeBaseSP keyframe, KUndo2Command *parentCommand); void redo() override; void undo() override; private: KisKeyframeChannel *m_channel; int m_time; KisKeyframeBaseSP m_keyframe; KisKeyframeBaseSP m_existingKeyframe; }; class KRITAIMAGE_EXPORT KisMoveFrameCommand : public KUndo2Command { public: KisMoveFrameCommand(KisKeyframeChannel *channel, KisKeyframeBaseSP keyframe, int oldTime, int newTime, KUndo2Command *parentCommand); void redo() override; void undo() override; private: KisKeyframeChannel *m_channel; KisKeyframeBaseSP m_keyframe; int m_oldTime; int m_newTime; }; class KRITAIMAGE_EXPORT KisSwapFramesCommand : public KUndo2Command { public: KisSwapFramesCommand(KisKeyframeChannel *channel, KisKeyframeBaseSP lhsFrame, KisKeyframeBaseSP rhsFrame, KUndo2Command *parentCommand); void redo() override; void undo() override; private: KisKeyframeChannel *m_channel; KisKeyframeBaseSP m_lhsFrame; KisKeyframeBaseSP m_rhsFrame; }; class KRITAIMAGE_EXPORT KisDefineCycleCommand : public KUndo2Command { public: KisDefineCycleCommand(QSharedPointer oldCycle, QSharedPointer newCycle, KUndo2Command *parentCommand); QSharedPointer cycle() const; void redo() override; void undo() override; private: KisKeyframeChannel *m_channel; QSharedPointer m_oldCycle; QSharedPointer m_newCycle; }; #endif