diff --git a/libs/command/kis_command_utils.cpp b/libs/command/kis_command_utils.cpp index ad63adec90..3075c81b54 100644 --- a/libs/command/kis_command_utils.cpp +++ b/libs/command/kis_command_utils.cpp @@ -1,157 +1,200 @@ /* * 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_command_utils.h" namespace KisCommandUtils { - AggregateCommand::AggregateCommand() - : m_firstRedo(true) {} + AggregateCommand::AggregateCommand(KUndo2Command *parent) + : KUndo2Command(parent), + m_firstRedo(true) {} + + AggregateCommand::AggregateCommand(const KUndo2MagicString &text, KUndo2Command *parent) + : KUndo2Command(text, parent), + m_firstRedo(true) {} void AggregateCommand::redo() { if (m_firstRedo) { m_firstRedo = false; populateChildCommands(); } m_store.redoAll(); } void AggregateCommand::undo() { m_store.undoAll(); } void AggregateCommand::addCommand(KUndo2Command *cmd) { + if (!cmd) return; m_store.addCommand(cmd); } + LambdaCommand::LambdaCommand(std::function createCommandFunc) + : m_createCommandFunc(createCommandFunc) + { + + } + + LambdaCommand::LambdaCommand(const KUndo2MagicString &text, + std::function createCommandFunc) + : AggregateCommand(text), + m_createCommandFunc(createCommandFunc) + { + + } + + LambdaCommand::LambdaCommand(const KUndo2MagicString &text, + KUndo2Command *parent, + std::function createCommandFunc) + : AggregateCommand(text, parent), + m_createCommandFunc(createCommandFunc) + { + } + + LambdaCommand::LambdaCommand(KUndo2Command *parent, + std::function createCommandFunc) + : AggregateCommand(parent), + m_createCommandFunc(createCommandFunc) + { + } + + void LambdaCommand::populateChildCommands() + { + if (m_createCommandFunc) { + addCommand(m_createCommandFunc()); + } + } + SkipFirstRedoWrapper::SkipFirstRedoWrapper(KUndo2Command *child, KUndo2Command *parent) : KUndo2Command(child ? child->text() : kundo2_noi18n(""), parent), m_firstRedo(true), m_child(child) {} void SkipFirstRedoWrapper::redo() { if (m_firstRedo) { m_firstRedo = false; } else { if (m_child) { m_child->redo(); } KUndo2Command::redo(); } } void SkipFirstRedoWrapper::undo() { KUndo2Command::undo(); if (m_child) { m_child->undo(); } } SkipFirstRedoBase::SkipFirstRedoBase(bool skipFirstRedo, KUndo2Command *parent) : KUndo2Command(parent), m_firstRedo(skipFirstRedo) { } SkipFirstRedoBase::SkipFirstRedoBase(bool skipFirstRedo, const KUndo2MagicString &text, KUndo2Command *parent) : KUndo2Command(text, parent), m_firstRedo(skipFirstRedo) { } void SkipFirstRedoBase::redo() { if (m_firstRedo) { m_firstRedo = false; } else { redoImpl(); KUndo2Command::redo(); } } void SkipFirstRedoBase::undo() { KUndo2Command::undo(); undoImpl(); } void SkipFirstRedoBase::setSkipOneRedo(bool value) { m_firstRedo = true; } FlipFlopCommand::FlipFlopCommand(bool finalize, KUndo2Command *parent) : KUndo2Command(parent), m_finalize(finalize), m_firstRedo(true) { } void FlipFlopCommand::redo() { if (!m_finalize) { init(); } else { end(); } m_firstRedo = false; } void FlipFlopCommand::undo() { if (m_finalize) { init(); } else { end(); } } void FlipFlopCommand::init() {} void FlipFlopCommand::end() {} CompositeCommand::CompositeCommand(KUndo2Command *parent) : KUndo2Command(parent) {} CompositeCommand::~CompositeCommand() { qDeleteAll(m_commands); } void CompositeCommand::addCommand(KUndo2Command *cmd) { if (cmd) { m_commands << cmd; } } void CompositeCommand::redo() { Q_FOREACH (KUndo2Command *cmd, m_commands) { cmd->redo(); } } void CompositeCommand::undo() { Q_FOREACH (KUndo2Command *cmd, m_commands) { cmd->undo(); } } + } diff --git a/libs/command/kis_command_utils.h b/libs/command/kis_command_utils.h index 87ab9a5773..f61204e770 100644 --- a/libs/command/kis_command_utils.h +++ b/libs/command/kis_command_utils.h @@ -1,102 +1,138 @@ /* * 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_COMMAND_UTILS_H #define __KIS_COMMAND_UTILS_H #include "kundo2command.h" #include "kis_undo_stores.h" #include "kritacommand_export.h" +#include namespace KisCommandUtils { + + /** + * @brief The AggregateCommand struct is a command with delayed + * initialization. On first redo() populateChildCommands() is called + * and the descendants add the desired commands to the internal list. + * After that, the added commands are executed on every undo()/redo(). + * + * This structure is used when the commands should be populated from + * the context of the stroke, not from the GUI thread. + */ struct KRITACOMMAND_EXPORT AggregateCommand : public KUndo2Command { - AggregateCommand(); + AggregateCommand(KUndo2Command *parent = 0); + AggregateCommand(const KUndo2MagicString &text, + KUndo2Command *parent = 0); void redo(); void undo(); protected: virtual void populateChildCommands() = 0; void addCommand(KUndo2Command *cmd); private: bool m_firstRedo; KisSurrogateUndoStore m_store; }; + /** + * @brief The LambdaCommand struct is a shorthand for creation of + * AggregateCommand commands using C++ lambda feature. Just pass + * a lambda object into a command and it will be called from within + * the context of the strokes thread to populate the command content. + */ + struct KRITACOMMAND_EXPORT LambdaCommand : public AggregateCommand { + LambdaCommand(std::function createCommandFunc); + LambdaCommand(const KUndo2MagicString &text, + std::function createCommandFunc); + LambdaCommand(const KUndo2MagicString &text, + KUndo2Command *parent, + std::function createCommandFunc); + LambdaCommand(KUndo2Command *parent, + std::function createCommandFunc); + + protected: + void populateChildCommands() override; + + private: + std::function m_createCommandFunc; + }; + struct KRITACOMMAND_EXPORT SkipFirstRedoWrapper : public KUndo2Command { SkipFirstRedoWrapper(KUndo2Command *child = 0, KUndo2Command *parent = 0); void redo() override; void undo() override; private: bool m_firstRedo; QScopedPointer m_child; }; struct KRITACOMMAND_EXPORT SkipFirstRedoBase : public KUndo2Command { SkipFirstRedoBase(bool skipFirstRedo, KUndo2Command *parent = 0); SkipFirstRedoBase(bool skipFirstRedo, const KUndo2MagicString &text, KUndo2Command *parent = 0); void redo() final; void undo() final; void setSkipOneRedo(bool value); protected: virtual void redoImpl() = 0; virtual void undoImpl() = 0; private: bool m_firstRedo; }; struct KRITACOMMAND_EXPORT FlipFlopCommand : public KUndo2Command { FlipFlopCommand(bool finalize, KUndo2Command *parent = 0); void redo(); void undo(); protected: virtual void init(); virtual void end(); bool isFinalizing() const { return m_finalize; } bool isFirstRedo() const { return m_firstRedo; } private: bool m_finalize; bool m_firstRedo; }; struct KRITACOMMAND_EXPORT CompositeCommand : public KUndo2Command { CompositeCommand(KUndo2Command *parent = 0); ~CompositeCommand(); void addCommand(KUndo2Command *cmd); void redo(); void undo(); private: QVector m_commands; }; } #endif /* __KIS_COMMAND_UTILS_H */ diff --git a/libs/image/kis_processing_applicator.cpp b/libs/image/kis_processing_applicator.cpp index 476ed2ebc5..989ee821db 100644 --- a/libs/image/kis_processing_applicator.cpp +++ b/libs/image/kis_processing_applicator.cpp @@ -1,323 +1,333 @@ /* * Copyright (c) 2011 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_processing_applicator.h" #include "kis_image.h" #include "kis_node.h" #include "kis_clone_layer.h" #include "kis_processing_visitor.h" #include "commands_new/kis_processing_command.h" #include "kis_stroke_strategy_undo_command_based.h" #include "kis_layer_utils.h" #include "kis_command_utils.h" class DisableUIUpdatesCommand : public KisCommandUtils::FlipFlopCommand { public: DisableUIUpdatesCommand(KisImageWSP image, bool finalUpdate) : FlipFlopCommand(finalUpdate), m_image(image) { } void init() override { m_image->disableUIUpdates(); } void end() override { m_image->enableUIUpdates(); } private: KisImageWSP m_image; }; class UpdateCommand : public KisCommandUtils::FlipFlopCommand { public: UpdateCommand(KisImageWSP image, KisNodeSP node, KisProcessingApplicator::ProcessingFlags flags, bool finalUpdate) : FlipFlopCommand(finalUpdate), m_image(image), m_node(node), m_flags(flags) { } private: void init() override { /** * We disable all non-centralized updates here. Everything * should be done by this command's explicit updates. * * If you still need third-party updates work, please add a * flag to the applicator. */ m_image->disableDirtyRequests(); } void end() override { m_image->enableDirtyRequests(); if(m_flags.testFlag(KisProcessingApplicator::RECURSIVE)) { m_image->refreshGraphAsync(m_node); } m_node->setDirty(m_image->bounds()); updateClones(m_node); } void updateClones(KisNodeSP node) { // simple tail-recursive iteration KisNodeSP prevNode = node->lastChild(); while(prevNode) { updateClones(prevNode); prevNode = prevNode->prevSibling(); } KisLayer *layer = qobject_cast(m_node.data()); if(layer && layer->hasClones()) { Q_FOREACH (KisCloneLayerSP clone, layer->registeredClones()) { if(!clone) continue; QPoint offset(clone->x(), clone->y()); QRegion dirtyRegion(m_image->bounds()); dirtyRegion -= m_image->bounds().translated(offset); clone->setDirty(dirtyRegion); } } } private: KisImageWSP m_image; KisNodeSP m_node; KisProcessingApplicator::ProcessingFlags m_flags; }; class EmitImageSignalsCommand : public KisCommandUtils::FlipFlopCommand { public: EmitImageSignalsCommand(KisImageWSP image, KisImageSignalVector emitSignals, bool finalUpdate) : FlipFlopCommand(finalUpdate), m_image(image), m_emitSignals(emitSignals) { } void end() override { if (isFinalizing()) { doUpdate(m_emitSignals); } else { KisImageSignalVector reverseSignals; KisImageSignalVector::iterator i = m_emitSignals.end(); while (i != m_emitSignals.begin()) { --i; reverseSignals.append(i->inverted()); } doUpdate(reverseSignals); } } private: void doUpdate(KisImageSignalVector emitSignals) { Q_FOREACH (KisImageSignalType type, emitSignals) { m_image->signalRouter()->emitNotification(type); } } private: KisImageWSP m_image; KisImageSignalVector m_emitSignals; }; KisProcessingApplicator::KisProcessingApplicator(KisImageWSP image, KisNodeSP node, ProcessingFlags flags, KisImageSignalVector emitSignals, const KUndo2MagicString &name, KUndo2CommandExtraData *extraData, int macroId) : m_image(image), m_node(node), m_flags(flags), m_emitSignals(emitSignals), m_finalSignalsEmitted(false) { KisStrokeStrategyUndoCommandBased *strategy = new KisStrokeStrategyUndoCommandBased(name, false, m_image.data()); if (m_flags.testFlag(SUPPORTS_WRAPAROUND_MODE)) { strategy->setSupportsWrapAroundMode(true); } if (extraData) { strategy->setCommandExtraData(extraData); } strategy->setMacroId(macroId); m_strokeId = m_image->startStroke(strategy); if(!m_emitSignals.isEmpty()) { applyCommand(new EmitImageSignalsCommand(m_image, m_emitSignals, false), KisStrokeJobData::BARRIER); } if(m_flags.testFlag(NO_UI_UPDATES)) { applyCommand(new DisableUIUpdatesCommand(m_image, false), KisStrokeJobData::BARRIER); } if (m_node) { applyCommand(new UpdateCommand(m_image, m_node, m_flags, false)); } } KisProcessingApplicator::~KisProcessingApplicator() { } void KisProcessingApplicator::applyVisitor(KisProcessingVisitorSP visitor, KisStrokeJobData::Sequentiality sequentiality, KisStrokeJobData::Exclusivity exclusivity) { if(!m_flags.testFlag(RECURSIVE)) { applyCommand(new KisProcessingCommand(visitor, m_node), sequentiality, exclusivity); } else { visitRecursively(m_node, visitor, sequentiality, exclusivity); } } void KisProcessingApplicator::applyVisitorAllFrames(KisProcessingVisitorSP visitor, KisStrokeJobData::Sequentiality sequentiality, KisStrokeJobData::Exclusivity exclusivity) { KisLayerUtils::FrameJobs jobs; if (m_flags.testFlag(RECURSIVE)) { KisLayerUtils::updateFrameJobsRecursive(&jobs, m_node); } else { KisLayerUtils::updateFrameJobsRecursive(&jobs, m_node); } if (jobs.isEmpty()) { applyVisitor(visitor, sequentiality, exclusivity); return; } KisLayerUtils::FrameJobs::const_iterator it = jobs.constBegin(); KisLayerUtils::FrameJobs::const_iterator end = jobs.constEnd(); KisLayerUtils::SwitchFrameCommand::SharedStorageSP switchFrameStorage( new KisLayerUtils::SwitchFrameCommand::SharedStorage()); for (; it != end; ++it) { const int frame = it.key(); const QSet &nodes = it.value(); applyCommand(new KisLayerUtils::SwitchFrameCommand(m_image, frame, false, switchFrameStorage), KisStrokeJobData::BARRIER, KisStrokeJobData::EXCLUSIVE); foreach (KisNodeSP node, nodes) { applyCommand(new KisProcessingCommand(visitor, node), sequentiality, exclusivity); } applyCommand(new KisLayerUtils::SwitchFrameCommand(m_image, frame, true, switchFrameStorage), KisStrokeJobData::BARRIER, KisStrokeJobData::EXCLUSIVE); } } void KisProcessingApplicator::visitRecursively(KisNodeSP node, KisProcessingVisitorSP visitor, KisStrokeJobData::Sequentiality sequentiality, KisStrokeJobData::Exclusivity exclusivity) { // simple tail-recursive iteration KisNodeSP prevNode = node->lastChild(); while(prevNode) { visitRecursively(prevNode, visitor, sequentiality, exclusivity); prevNode = prevNode->prevSibling(); } applyCommand(new KisProcessingCommand(visitor, node), sequentiality, exclusivity); } void KisProcessingApplicator::applyCommand(KUndo2Command *command, KisStrokeJobData::Sequentiality sequentiality, KisStrokeJobData::Exclusivity exclusivity) { /* * One should not add commands after the final signals have been * emitted, only end or cancel the stroke */ KIS_ASSERT_RECOVER_RETURN(!m_finalSignalsEmitted); m_image->addJob(m_strokeId, new KisStrokeStrategyUndoCommandBased::Data(KUndo2CommandSP(command), false, sequentiality, exclusivity)); } void KisProcessingApplicator::explicitlyEmitFinalSignals() { KIS_ASSERT_RECOVER_RETURN(!m_finalSignalsEmitted); if (m_node) { applyCommand(new UpdateCommand(m_image, m_node, m_flags, true)); } if(m_flags.testFlag(NO_UI_UPDATES)) { applyCommand(new DisableUIUpdatesCommand(m_image, true), KisStrokeJobData::BARRIER); } if(!m_emitSignals.isEmpty()) { applyCommand(new EmitImageSignalsCommand(m_image, m_emitSignals, true), KisStrokeJobData::BARRIER); } // simple consistency check m_finalSignalsEmitted = true; } void KisProcessingApplicator::end() { if (!m_finalSignalsEmitted) { explicitlyEmitFinalSignals(); } m_image->endStroke(m_strokeId); } void KisProcessingApplicator::cancel() { m_image->cancelStroke(m_strokeId); } + +void KisProcessingApplicator::runSingleCommandStroke(KisImageSP image, KUndo2Command *cmd, KisStrokeJobData::Sequentiality sequentiality, KisStrokeJobData::Exclusivity exclusivity) +{ + KisProcessingApplicator applicator(image, 0, + KisProcessingApplicator::NONE, + KisImageSignalVector() << ModifiedSignal, + cmd->text()); + applicator.applyCommand(cmd, sequentiality, exclusivity); + applicator.end(); +} diff --git a/libs/image/kis_processing_applicator.h b/libs/image/kis_processing_applicator.h index 7e563f62e9..49a30ea14f 100644 --- a/libs/image/kis_processing_applicator.h +++ b/libs/image/kis_processing_applicator.h @@ -1,99 +1,112 @@ /* * Copyright (c) 2011 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_PROCESSING_APPLICATOR_H #define __KIS_PROCESSING_APPLICATOR_H #include "kritaimage_export.h" #include "kis_types.h" #include "kis_stroke_job_strategy.h" #include "kis_image_signal_router.h" #include "kundo2magicstring.h" #include "kundo2commandextradata.h" class KRITAIMAGE_EXPORT KisProcessingApplicator { public: enum ProcessingFlag { NONE = 0x0, RECURSIVE = 0x1, NO_UI_UPDATES = 0x2, SUPPORTS_WRAPAROUND_MODE = 0x4 }; Q_DECLARE_FLAGS(ProcessingFlags, ProcessingFlag) public: KisProcessingApplicator(KisImageWSP image, KisNodeSP node, ProcessingFlags flags = NONE, KisImageSignalVector emitSignals = KisImageSignalVector(), const KUndo2MagicString &name = KUndo2MagicString(), KUndo2CommandExtraData *extraData = 0, int macroId = -1); ~KisProcessingApplicator(); void applyVisitor(KisProcessingVisitorSP visitor, KisStrokeJobData::Sequentiality sequentiality = KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::Exclusivity exclusivity = KisStrokeJobData::NORMAL); void applyCommand(KUndo2Command *command, KisStrokeJobData::Sequentiality sequentiality = KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::Exclusivity exclusivity = KisStrokeJobData::NORMAL); void applyVisitorAllFrames(KisProcessingVisitorSP visitor, KisStrokeJobData::Sequentiality sequentiality = KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::Exclusivity exclusivity = KisStrokeJobData::NORMAL); /** * This method emits all the final update signals of the stroke * without actually ending the stroke. This can be used for * long-running strokes which are kept open to implement preview * of the actions. * * WARNING: you cannot add new commands/processings after the * final signals has been emitted. You should either call end() or * cancel(). */ void explicitlyEmitFinalSignals(); void end(); void cancel(); + /** + * @brief runSingleCommandStroke creates a stroke and runs \p cmd in it. + * The text() field fo \p cmd is used as a title of the stroke. + * @param image the image to run the stroke on + * @param cmd the command to be executed + * @param sequentiality sequentiality property of the command being executed (see strokes documentation) + * @param exclusivity sequentiality property of the command being executed (see strokes documentation) + */ + static void runSingleCommandStroke(KisImageSP image, + KUndo2Command *cmd, + KisStrokeJobData::Sequentiality sequentiality = KisStrokeJobData::SEQUENTIAL, + KisStrokeJobData::Exclusivity exclusivity = KisStrokeJobData::NORMAL); + private: void visitRecursively(KisNodeSP node, KisProcessingVisitorSP visitor, KisStrokeJobData::Sequentiality sequentiality, KisStrokeJobData::Exclusivity exclusivity); private: KisImageWSP m_image; KisNodeSP m_node; ProcessingFlags m_flags; KisImageSignalVector m_emitSignals; KisStrokeId m_strokeId; bool m_finalSignalsEmitted; }; Q_DECLARE_OPERATORS_FOR_FLAGS(KisProcessingApplicator::ProcessingFlags) #endif /* __KIS_PROCESSING_APPLICATOR_H */ diff --git a/libs/ui/KisImageBarrierLockerWithFeedback.h b/libs/ui/KisImageBarrierLockerWithFeedback.h index 5eae50cf9f..e583ee94b6 100644 --- a/libs/ui/KisImageBarrierLockerWithFeedback.h +++ b/libs/ui/KisImageBarrierLockerWithFeedback.h @@ -1,63 +1,64 @@ /* * Copyright (c) 2017 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 KISIMAGEBARRIERLOCKERWITHFEEDBACK_H #define KISIMAGEBARRIERLOCKERWITHFEEDBACK_H #include "kis_types.h" #include "kis_image_barrier_locker.h" +#include "kritaui_export.h" namespace KisImageBarrierLockerWithFeedbackImplPrivate { -void blockWithFeedback(KisImageSP image); +void KRITAUI_EXPORT blockWithFeedback(KisImageSP image); } /** * The wrapper around KisImageBarrierLocker or KisImageBarrierLockerAllowNull * that adds GUI feedback with a progress bar when the locking is going to be * long enough. */ template class KisImageBarrierLockerWithFeedbackImpl { public: KisImageBarrierLockerWithFeedbackImpl(KisImageSP image) { KisImageBarrierLockerWithFeedbackImplPrivate::blockWithFeedback(image); m_locker.reset(new InternalLocker(image)); } ~KisImageBarrierLockerWithFeedbackImpl() { } private: QScopedPointer m_locker; }; /** * @brief KisImageBarrierLockerWithFeedback is a simple KisImageBarrierLocker with a * progress dialog feedback shown before locking. */ typedef KisImageBarrierLockerWithFeedbackImpl KisImageBarrierLockerWithFeedback; /** * @brief KisImageBarrierLockerWithFeedback is a simple KisImageBarrierLockerAllowEmpty with a * progress dialog feedback shown before locking. */ typedef KisImageBarrierLockerWithFeedbackImpl KisImageBarrierLockerWithFeedbackAllowNull; #endif // KISIMAGEBARRIERLOCKERWITHFEEDBACK_H diff --git a/plugins/dockers/animation/kis_animation_curves_model.cpp b/plugins/dockers/animation/kis_animation_curves_model.cpp index f12d709e64..a61751a9dc 100644 --- a/plugins/dockers/animation/kis_animation_curves_model.cpp +++ b/plugins/dockers/animation/kis_animation_curves_model.cpp @@ -1,372 +1,412 @@ /* * Copyright (c) 2016 Jouni Pentikäinen * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_animation_curves_model.h" #include #include "kis_global.h" #include "kis_image.h" #include "kis_node.h" #include "kis_keyframe_channel.h" #include "kis_scalar_keyframe_channel.h" #include "kis_post_execution_undo_adapter.h" +#include "kis_animation_utils.h" +#include "kis_processing_applicator.h" +#include "kis_command_utils.h" +#include "KisImageBarrierLockerWithFeedback.h" struct KisAnimationCurve::Private { Private(KisScalarKeyframeChannel *channel, QColor color) : channel(channel) , color(color) , visible(true) {} KisScalarKeyframeChannel *channel; QColor color; bool visible; }; KisAnimationCurve::KisAnimationCurve(KisScalarKeyframeChannel *channel, QColor color) : m_d(new Private(channel, color)) {} KisScalarKeyframeChannel *KisAnimationCurve::channel() const { return m_d->channel; } QColor KisAnimationCurve::color() const { return m_d->color; } void KisAnimationCurve::setVisible(bool visible) { m_d->visible = visible; } bool KisAnimationCurve::visible() const { return m_d->visible; } struct KisAnimationCurvesModel::Private { QList curves; int nextColorHue; KUndo2Command *undoCommand; Private() : nextColorHue(0) , undoCommand(0) {} KisAnimationCurve *getCurveAt(const QModelIndex& index) { if (!index.isValid()) return 0; int row = index.row(); if (row < 0 || row >= curves.size()) { return 0; } return curves.at(row); } int rowForCurve(KisAnimationCurve *curve) { return curves.indexOf(curve); } int rowForChannel(KisKeyframeChannel *channel) { for (int row = 0; row < curves.count(); row++) { if (curves.at(row)->channel() == channel) return row; } return -1; } QColor chooseNextColor() { if (curves.isEmpty()) nextColorHue = 0; QColor color = QColor::fromHsv(nextColorHue, 255, 255); nextColorHue += 94; // Value chosen experimentally for providing distinct colors nextColorHue = nextColorHue & 0xff; return color; } }; KisAnimationCurvesModel::KisAnimationCurvesModel(QObject *parent) : KisTimeBasedItemModel(parent) , m_d(new Private()) {} KisAnimationCurvesModel::~KisAnimationCurvesModel() { qDeleteAll(m_d->curves); } int KisAnimationCurvesModel::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent); return m_d->curves.size(); } QVariant KisAnimationCurvesModel::data(const QModelIndex &index, int role) const { KisAnimationCurve *curve = m_d->getCurveAt(index); if (curve) { KisScalarKeyframeChannel *channel = curve->channel(); int time = index.column(); KisKeyframeSP keyframe = channel->keyframeAt(time); switch (role) { case SpecialKeyframeExists: return !keyframe.isNull(); case ScalarValueRole: return channel->interpolatedValue(time); case LeftTangentRole: return (keyframe.isNull()) ? QVariant() : keyframe->leftTangent(); case RightTangentRole: return (keyframe.isNull()) ? QVariant() : keyframe->rightTangent(); case InterpolationModeRole: return (keyframe.isNull()) ? QVariant() : keyframe->interpolationMode(); case TangentsModeRole: return (keyframe.isNull()) ? QVariant() : keyframe->tangentsMode(); case CurveColorRole: return curve->color(); case CurveVisibleRole: return curve->visible(); case PreviousKeyframeTime: { KisKeyframeSP active = channel->activeKeyframeAt(time); if (active.isNull()) return QVariant(); if (active->time() < time) { return active->time(); } KisKeyframeSP previous = channel->previousKeyframe(active); if (previous.isNull()) return QVariant(); return previous->time(); } case NextKeyframeTime: { KisKeyframeSP active = channel->activeKeyframeAt(time); if (active.isNull()) { KisKeyframeSP first = channel->firstKeyframe(); if (!first.isNull() && first->time() > time) { return first->time(); } return QVariant(); } KisKeyframeSP next = channel->nextKeyframe(active); if (next.isNull()) return QVariant(); return next->time(); } default: break; } } return KisTimeBasedItemModel::data(index, role); } bool KisAnimationCurvesModel::setData(const QModelIndex &index, const QVariant &value, int role) { if (!index.isValid()) return false; KisScalarKeyframeChannel *channel = m_d->getCurveAt(index)->channel(); KUndo2Command *command = m_d->undoCommand; switch (role) { case ScalarValueRole: { KisKeyframeSP keyframe = channel->keyframeAt(index.column()); if (keyframe) { if (!command) command = new KUndo2Command(kundo2_i18n("Adjust keyframe")); channel->setScalarValue(keyframe, value.toReal(), command); } else { if (!command) command = new KUndo2Command(kundo2_i18n("Insert keyframe")); auto *addKeyframeCommand = new KisScalarKeyframeChannel::AddKeyframeCommand( channel, index.column(), value.toReal(), command); addKeyframeCommand->redo(); } } break; case LeftTangentRole: case RightTangentRole: { KisKeyframeSP keyframe = channel->keyframeAt(index.column()); if (!keyframe) return false; QPointF leftTangent = (role == LeftTangentRole ? value.toPointF() : keyframe->leftTangent()); QPointF rightTangent = (role == RightTangentRole ? value.toPointF() : keyframe->rightTangent()); if (!command) command = new KUndo2Command(kundo2_i18n("Adjust tangent")); channel->setInterpolationTangents(keyframe, keyframe->tangentsMode(), leftTangent, rightTangent, command); } break; case InterpolationModeRole: { KisKeyframeSP keyframe = channel->keyframeAt(index.column()); if (!keyframe) return false; if (!command) command = new KUndo2Command(kundo2_i18n("Set interpolation mode")); channel->setInterpolationMode(keyframe, (KisKeyframe::InterpolationMode)value.toInt(), command); } break; case TangentsModeRole: { KisKeyframeSP keyframe = channel->keyframeAt(index.column()); if (!keyframe) return false; KisKeyframe::InterpolationTangentsMode mode = (KisKeyframe::InterpolationTangentsMode)value.toInt(); QPointF leftTangent = keyframe->leftTangent(); QPointF rightTangent = keyframe->rightTangent(); if (!command) command = new KUndo2Command(kundo2_i18n("Set interpolation mode")); channel->setInterpolationTangents(keyframe, mode, leftTangent, rightTangent, command); } break; default: return KisTimeBasedItemModel::setData(index, value, role); } if (command && !m_d->undoCommand) { image()->postExecutionUndoAdapter()->addCommand(toQShared(command)); } return true; } QVariant KisAnimationCurvesModel::headerData(int section, Qt::Orientation orientation, int role) const { // TODO return KisTimeBasedItemModel::headerData(section, orientation, role); } void KisAnimationCurvesModel::beginCommand(const KUndo2MagicString &text) { KIS_ASSERT_RECOVER_RETURN(!m_d->undoCommand); m_d->undoCommand = new KUndo2Command(text); } void KisAnimationCurvesModel::endCommand() { KIS_ASSERT_RECOVER_RETURN(m_d->undoCommand); image()->postExecutionUndoAdapter()->addCommand(toQShared(m_d->undoCommand)); m_d->undoCommand = 0; } + bool KisAnimationCurvesModel::adjustKeyframes(const QModelIndexList &indexes, int timeOffset, qreal valueOffset) { - KUndo2Command *command = new KUndo2Command( - kundo2_i18np("Adjust Keyframe", - "Adjust %1 Keyframes", - indexes.size())); - - if (timeOffset != 0) { - bool ok = offsetFrames(indexes, QPoint(timeOffset, 0), false, command); - if (!ok) return false; - } + QScopedPointer command( + new KUndo2Command( + kundo2_i18np("Adjust Keyframe", + "Adjust %1 Keyframes", + indexes.size()))); + + { + KisImageBarrierLockerWithFeedback locker(image()); + + if (timeOffset != 0) { + bool ok = createOffsetFramesCommand(indexes, QPoint(timeOffset, 0), false, command.data()); + if (!ok) return false; + } + + + using KisAnimationUtils::FrameItem; + using KisAnimationUtils::FrameItemList; + FrameItemList frameItems; + + Q_FOREACH(QModelIndex index, indexes) { + KisScalarKeyframeChannel *channel = m_d->getCurveAt(index)->channel(); + KIS_ASSERT_RECOVER_RETURN_VALUE(channel, false); - Q_FOREACH(QModelIndex oldIndex, indexes) { - KisScalarKeyframeChannel *channel = m_d->getCurveAt(oldIndex)->channel(); - KIS_ASSERT_RECOVER_RETURN_VALUE(channel, false); + frameItems << FrameItem(channel->node(), + channel->id(), + index.column() + timeOffset); + }; - KisKeyframeSP keyframe = channel->keyframeAt(oldIndex.column() + timeOffset); - KIS_ASSERT_RECOVER_RETURN_VALUE(!keyframe.isNull(), false); + new KisCommandUtils::LambdaCommand( + command.data(), + [frameItems, valueOffset] () -> KUndo2Command* { - qreal currentValue = channel->scalarValue(keyframe); - channel->setScalarValue(keyframe, currentValue + valueOffset, command); - }; + QScopedPointer cmd(new KUndo2Command()); + + bool result = false; + + Q_FOREACH (const FrameItem &item, frameItems) { + const int time = item.time; + KisNodeSP node = item.node; + + KisKeyframeChannel *channel = node->getKeyframeChannel(item.channel); + + if (!channel) continue; + + KisKeyframeSP keyframe = channel->keyframeAt(time); + if (!keyframe) continue; + + const qreal currentValue = channel->scalarValue(keyframe); + channel->setScalarValue(keyframe, currentValue + valueOffset, cmd.data()); + result = true; + } + + return result ? new KisCommandUtils::SkipFirstRedoWrapper(cmd.take()) : 0; + }); + } - image()->postExecutionUndoAdapter()->addCommand(toQShared(command)); + KisProcessingApplicator::runSingleCommandStroke(image(), command.take(), KisStrokeJobData::BARRIER); return true; } KisAnimationCurve *KisAnimationCurvesModel::addCurve(KisScalarKeyframeChannel *channel) { beginInsertRows(QModelIndex(), m_d->curves.size(), m_d->curves.size()); KisAnimationCurve *curve = new KisAnimationCurve(channel, m_d->chooseNextColor()); m_d->curves.append(curve); endInsertRows(); connect(channel, &KisScalarKeyframeChannel::sigKeyframeAdded, this, &KisAnimationCurvesModel::slotKeyframeChanged); connect(channel, &KisScalarKeyframeChannel::sigKeyframeMoved, this, &KisAnimationCurvesModel::slotKeyframeChanged); connect(channel, &KisScalarKeyframeChannel::sigKeyframeRemoved, this, &KisAnimationCurvesModel::slotKeyframeChanged); connect(channel, &KisScalarKeyframeChannel::sigKeyframeChanged, this, &KisAnimationCurvesModel::slotKeyframeChanged); return curve; } void KisAnimationCurvesModel::removeCurve(KisAnimationCurve *curve) { int index = m_d->curves.indexOf(curve); if (index < 0) return; curve->channel()->disconnect(this); beginRemoveRows(QModelIndex(), index, index); m_d->curves.removeAt(index); delete curve; endRemoveRows(); } void KisAnimationCurvesModel::setCurveVisible(KisAnimationCurve *curve, bool visible) { curve->setVisible(visible); int row = m_d->rowForCurve(curve); emit dataChanged(index(row, 0), index(row, columnCount())); } KisNodeSP KisAnimationCurvesModel::nodeAt(QModelIndex index) const { KisAnimationCurve *curve = m_d->getCurveAt(index); if (curve && curve->channel() && curve->channel()->node()) { return KisNodeSP(curve->channel()->node()); } return 0; } QList KisAnimationCurvesModel::channelsAt(QModelIndex index) const { KisKeyframeChannel *channel = m_d->getCurveAt(index)->channel(); QList list({channel}); return list; } void KisAnimationCurvesModel::slotKeyframeChanged(KisKeyframeSP keyframe) { int row = m_d->rowForChannel(keyframe->channel()); QModelIndex changedIndex = index(row, keyframe->time()); emit dataChanged(changedIndex, changedIndex); } diff --git a/plugins/dockers/animation/kis_animation_utils.cpp b/plugins/dockers/animation/kis_animation_utils.cpp index 37d18e5e95..6b8c246a56 100644 --- a/plugins/dockers/animation/kis_animation_utils.cpp +++ b/plugins/dockers/animation/kis_animation_utils.cpp @@ -1,235 +1,266 @@ /* * 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"); - bool createKeyframeLazy(KisImageSP image, KisNodeSP node, const QString &channelId, int time, bool copy) { - KisKeyframeChannel *channel = node->getKeyframeChannel(channelId); - bool createdChannel = false; - - if (!channel) { - node->enableAnimation(); - channel = node->getKeyframeChannel(channelId, true); - if (!channel) return false; + void createKeyframeLazy(KisImageSP image, KisNodeSP node, const QString &channelId, int time, bool copy) { + KIS_SAFE_ASSERT_RECOVER_RETURN(!image->locked()); - createdChannel = true; - } + KUndo2Command *cmd = new KisCommandUtils::LambdaCommand( + copy ? kundo2_i18n("Copy Keyframe") : + kundo2_i18n("Add Keyframe"), - if (copy) { - if (channel->keyframeAt(time)) return false; + [image, node, channelId, time, copy] () mutable -> KUndo2Command* { + bool result = false; - KUndo2Command *cmd = new KUndo2Command(kundo2_i18n("Copy Keyframe")); - KisKeyframeSP srcFrame = channel->activeKeyframeAt(time); + QScopedPointer cmd(new KUndo2Command()); - channel->copyKeyframe(srcFrame, time, cmd); - image->postExecutionUndoAdapter()->addCommand(toQShared(cmd)); - } else { - if (channel->keyframeAt(time)) { + KisKeyframeChannel *channel = node->getKeyframeChannel(channelId); + bool createdChannel = false; - if (createdChannel) return false; + if (!channel) { + node->enableAnimation(); + channel = node->getKeyframeChannel(channelId, true); + if (!channel) return nullptr; - if (image->animationInterface()->currentTime() == time && channelId == KisKeyframeChannel::Content.id()) { - //shortcut: clearing the image instead + createdChannel = true; + } - if (KisToolUtils::clearImage(image, node, 0)) { - return 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; } } - //fallback: erasing the keyframe and creating it again - } - KUndo2Command *cmd = new KUndo2Command(kundo2_i18n("Add Keyframe")); - channel->addKeyframe(time, cmd); - image->postExecutionUndoAdapter()->addCommand(toQShared(cmd)); - } - return true; + return result ? new KisCommandUtils::SkipFirstRedoWrapper(cmd.take()) : nullptr; + }); + + KisProcessingApplicator::runSingleCommandStroke(image, cmd, KisStrokeJobData::BARRIER); } - bool removeKeyframes(KisImageSP image, const FrameItemList &frames) { - bool result = false; + void removeKeyframes(KisImageSP image, const FrameItemList &frames) { + KIS_SAFE_ASSERT_RECOVER_RETURN(!image->locked()); - QScopedPointer cmd( - new KUndo2Command(kundo2_i18np("Remove Keyframe", - "Remove Keyframes", - frames.size()))); // lisp-lovers present ;) + KUndo2Command *cmd = new KisCommandUtils::LambdaCommand( + kundo2_i18np("Remove Keyframe", + "Remove Keyframes", + frames.size()), - Q_FOREACH (const FrameItem &item, frames) { - const int time = item.time; - KisNodeSP node = item.node; + [image, frames] () { + bool result = false; - KisKeyframeChannel *channel = node->getKeyframeChannel(item.channel); + QScopedPointer cmd(new KUndo2Command()); - if (!channel) continue; + Q_FOREACH (const FrameItem &item, frames) { + const int time = item.time; + KisNodeSP node = item.node; - KisKeyframeSP keyframe = channel->keyframeAt(time); - if (!keyframe) continue; + KisKeyframeChannel *channel = node->getKeyframeChannel(item.channel); - channel->deleteKeyframe(keyframe, cmd.data()); + if (!channel) continue; - result = true; - } + KisKeyframeSP keyframe = channel->keyframeAt(time); + if (!keyframe) continue; - if (result) { - image->postExecutionUndoAdapter()->addCommand(toQShared(cmd.take())); - } + channel->deleteKeyframe(keyframe, cmd.data()); + + result = true; + } + + return result ? new KisCommandUtils::SkipFirstRedoWrapper(cmd.take()) : 0; + }); - return result; + KisProcessingApplicator::runSingleCommandStroke(image, cmd, KisStrokeJobData::BARRIER); } - bool removeKeyframe(KisImageSP image, KisNodeSP node, const QString &channel, int time) { + void removeKeyframe(KisImageSP image, KisNodeSP node, const QString &channel, int time) { QVector frames; frames << FrameItem(node, channel, time); - return removeKeyframes(image, frames); + 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) { qSort(points->begin(), points->end(), LessOperator(offset)); } - bool moveKeyframes(KisImageSP image, - const FrameItemList &srcFrames, - const FrameItemList &dstFrames, - bool copy, - KUndo2Command *parentCommand) { - - if (srcFrames.size() != dstFrames.size()) return false; - - bool result = false; - - QScopedPointer cmd( - new KUndo2Command(!copy ? - kundo2_i18np("Move Keyframe", - "Move %1 Keyframes", - srcFrames.size()) : - kundo2_i18np("Copy Keyframe", - "Copy %1 Keyframes", - srcFrames.size()), - parentCommand)); - - for (int i = 0; i < srcFrames.size(); i++) { - const int srcTime = srcFrames[i].time; - KisNodeSP srcNode = srcFrames[i].node; - KisKeyframeChannel *srcChannel = srcNode->getKeyframeChannel(srcFrames[i].channel); - - const int dstTime = dstFrames[i].time; - KisNodeSP dstNode = dstFrames[i].node; - KisKeyframeChannel *dstChannel = dstNode->getKeyframeChannel(dstFrames[i].channel, true); - - if (srcNode == dstNode) { - if (!srcChannel) continue; - - KisKeyframeSP srcKeyframe = srcChannel->keyframeAt(srcTime); - if (srcKeyframe) { - if (copy) { - srcChannel->copyKeyframe(srcKeyframe, dstTime, cmd.data()); + KUndo2Command* createMoveKeyframesCommand(const FrameItemList &srcFrames, + const FrameItemList &dstFrames, + bool copy, + KUndo2Command *parentCommand) { + + KUndo2Command *cmd = new KisCommandUtils::LambdaCommand( + + !copy ? + kundo2_i18np("Move Keyframe", + "Move %1 Keyframes", + srcFrames.size()) : + kundo2_i18np("Copy Keyframe", + "Copy %1 Keyframes", + srcFrames.size()), + + parentCommand, + + [srcFrames, dstFrames, copy] () -> KUndo2Command* { + bool result = false; + + QScopedPointer cmd(new KUndo2Command()); + + for (int i = 0; i < srcFrames.size(); i++) { + const int srcTime = srcFrames[i].time; + KisNodeSP srcNode = srcFrames[i].node; + KisKeyframeChannel *srcChannel = srcNode->getKeyframeChannel(srcFrames[i].channel); + + const int dstTime = dstFrames[i].time; + KisNodeSP dstNode = dstFrames[i].node; + KisKeyframeChannel *dstChannel = dstNode->getKeyframeChannel(dstFrames[i].channel, true); + + if (srcNode == dstNode) { + if (!srcChannel) continue; + + KisKeyframeSP srcKeyframe = srcChannel->keyframeAt(srcTime); + if (srcKeyframe) { + if (copy) { + srcChannel->copyKeyframe(srcKeyframe, dstTime, cmd.data()); + } else { + srcChannel->moveKeyframe(srcKeyframe, dstTime, cmd.data()); + } + } } else { - srcChannel->moveKeyframe(srcKeyframe, dstTime, cmd.data()); + if (!srcChannel|| !dstChannel) continue; + + KisKeyframeSP srcKeyframe = srcChannel->keyframeAt(srcTime); + if (!srcKeyframe) continue; + + dstChannel->copyExternalKeyframe(srcChannel, srcTime, dstTime, cmd.data()); + + if (!copy) { + srcChannel->deleteKeyframe(srcKeyframe, cmd.data()); + } } + + result = true; } - } else { - if (!srcChannel|| !dstChannel) continue; - KisKeyframeSP srcKeyframe = srcChannel->keyframeAt(srcTime); - if (!srcKeyframe) continue; + return result ? new KisCommandUtils::SkipFirstRedoWrapper(cmd.take()) : 0; + }); - dstChannel->copyExternalKeyframe(srcChannel, srcTime, dstTime, cmd.data()); + return cmd; + } - if (!copy) { - srcChannel->deleteKeyframe(srcKeyframe, cmd.data()); - } - } + void moveKeyframes(KisImageSP image, + const FrameItemList &srcFrames, + const FrameItemList &dstFrames, + bool copy) { - result = true; - } + KIS_SAFE_ASSERT_RECOVER_RETURN(srcFrames.size() != dstFrames.size()); + KIS_SAFE_ASSERT_RECOVER_RETURN(!image->locked()); - if (parentCommand) { - cmd.take(); - } else if (result) { - image->postExecutionUndoAdapter()->addCommand(toQShared(cmd.take())); - } + KUndo2Command *cmd = + createMoveKeyframesCommand(srcFrames, dstFrames, copy); - return result; + KisProcessingApplicator::runSingleCommandStroke(image, cmd, KisStrokeJobData::BARRIER); } - bool moveKeyframe(KisImageSP image, KisNodeSP node, const QString &channel, int srcTime, int dstTime) { + void moveKeyframe(KisImageSP image, KisNodeSP node, const QString &channel, int srcTime, int dstTime) { QVector srcFrames; srcFrames << FrameItem(node, channel, srcTime); QVector dstFrames; dstFrames << FrameItem(node, channel, dstTime); - return moveKeyframes(image, srcFrames, dstFrames); + moveKeyframes(image, srcFrames, dstFrames); } bool supportsContentFrames(KisNodeSP node) { return node->inherits("KisPaintLayer") || node->inherits("KisFilterMask") || node->inherits("KisTransparencyMask") || node->inherits("KisSelectionBasedLayer"); } } diff --git a/plugins/dockers/animation/kis_animation_utils.h b/plugins/dockers/animation/kis_animation_utils.h index f158877ec8..902ffca80f 100644 --- a/plugins/dockers/animation/kis_animation_utils.h +++ b/plugins/dockers/animation/kis_animation_utils.h @@ -1,72 +1,77 @@ /* * 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 namespace KisAnimationUtils { - bool createKeyframeLazy(KisImageSP image, KisNodeSP node, const QString &channel, int time, bool copy); + void createKeyframeLazy(KisImageSP image, KisNodeSP node, const QString &channel, int time, bool copy); struct FrameItem { FrameItem() : time(-1) {} FrameItem(KisNodeSP _node, const QString &_channel, int _time) : node(_node), channel(_channel), time(_time) {} KisNodeSP node; QString channel; int time; }; typedef QVector FrameItemList; - bool removeKeyframes(KisImageSP image, const FrameItemList &frames); - bool removeKeyframe(KisImageSP image, KisNodeSP node, const QString &channel, int time); + 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); - bool moveKeyframes(KisImageSP image, + + KUndo2Command* createMoveKeyframesCommand(const FrameItemList &srcFrames, + const FrameItemList &dstFrames, + bool copy, KUndo2Command *parentCommand = 0); + + void moveKeyframes(KisImageSP image, const FrameItemList &srcFrames, const FrameItemList &dstFrames, - bool copy = false, - KUndo2Command *parentCommand = 0); - bool moveKeyframe(KisImageSP image, KisNodeSP node, const QString &channel, int srcTime, int dstTime); + bool copy = false); + + void moveKeyframe(KisImageSP image, KisNodeSP node, const QString &channel, int srcTime, int dstTime); 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/kis_time_based_item_model.cpp b/plugins/dockers/animation/kis_time_based_item_model.cpp index 74f133b854..6376856feb 100644 --- a/plugins/dockers/animation/kis_time_based_item_model.cpp +++ b/plugins/dockers/animation/kis_time_based_item_model.cpp @@ -1,434 +1,439 @@ /* * Copyright (c) 2016 Jouni Pentikäinen * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_time_based_item_model.h" #include #include #include "kis_animation_frame_cache.h" #include "kis_animation_player.h" #include "kis_signal_compressor_with_param.h" #include "kis_image.h" #include "kis_image_animation_interface.h" #include "kis_time_range.h" #include "kis_animation_utils.h" #include "kis_keyframe_channel.h" +#include "kis_processing_applicator.h" +#include "KisImageBarrierLockerWithFeedback.h" struct KisTimeBasedItemModel::Private { Private() : animationPlayer(0) , numFramesOverride(0) , activeFrameIndex(0) , scrubInProgress(false) , scrubStartFrame(-1) {} KisImageWSP image; KisAnimationFrameCacheWSP framesCache; QPointer animationPlayer; QVector cachedFrames; int numFramesOverride; int activeFrameIndex; bool scrubInProgress; int scrubStartFrame; QScopedPointer > scrubbingCompressor; int baseNumFrames() const { if (image.isNull()) return 0; KisImageAnimationInterface *i = image->animationInterface(); if (!i) return 1; return i->totalLength(); } int effectiveNumFrames() const { if (image.isNull()) return 0; return qMax(baseNumFrames(), numFramesOverride); } int framesPerSecond() { return image->animationInterface()->framerate(); } }; KisTimeBasedItemModel::KisTimeBasedItemModel(QObject *parent) : QAbstractTableModel(parent) , m_d(new Private()) { KisConfig cfg; using namespace std::placeholders; std::function callback( std::bind(&KisTimeBasedItemModel::slotInternalScrubPreviewRequested, this, _1)); m_d->scrubbingCompressor.reset( new KisSignalCompressorWithParam(cfg.scrubbingUpdatesDelay(), callback, KisSignalCompressor::FIRST_ACTIVE)); } KisTimeBasedItemModel::~KisTimeBasedItemModel() {} void KisTimeBasedItemModel::setImage(KisImageWSP image) { KisImageWSP oldImage = m_d->image; m_d->image = image; if (image) { KisImageAnimationInterface *ai = image->animationInterface(); slotCurrentTimeChanged(ai->currentUITime()); connect(ai, SIGNAL(sigFramerateChanged()), SLOT(slotFramerateChanged())); connect(ai, SIGNAL(sigUiTimeChanged(int)), SLOT(slotCurrentTimeChanged(int))); } if (image != oldImage) { reset(); } } void KisTimeBasedItemModel::setFrameCache(KisAnimationFrameCacheSP cache) { if (KisAnimationFrameCacheSP(m_d->framesCache) == cache) return; if (m_d->framesCache) { m_d->framesCache->disconnect(this); } m_d->framesCache = cache; if (m_d->framesCache) { connect(m_d->framesCache, SIGNAL(changed()), SLOT(slotCacheChanged())); } } void KisTimeBasedItemModel::setAnimationPlayer(KisAnimationPlayer *player) { if (m_d->animationPlayer == player) return; if (m_d->animationPlayer) { m_d->animationPlayer->disconnect(this); } m_d->animationPlayer = player; if (m_d->animationPlayer) { connect(m_d->animationPlayer, SIGNAL(sigPlaybackStopped()), SLOT(slotPlaybackStopped())); connect(m_d->animationPlayer, SIGNAL(sigFrameChanged()), SLOT(slotPlaybackFrameChanged())); } } void KisTimeBasedItemModel::setLastVisibleFrame(int time) { const int growThreshold = m_d->effectiveNumFrames() - 3; const int growValue = time + 8; const int shrinkThreshold = m_d->effectiveNumFrames() - 12; const int shrinkValue = qMax(m_d->baseNumFrames(), qMin(growValue, shrinkThreshold)); const bool canShrink = m_d->effectiveNumFrames() > m_d->baseNumFrames(); if (time >= growThreshold) { beginInsertColumns(QModelIndex(), m_d->effectiveNumFrames(), growValue - 1); m_d->numFramesOverride = growValue; endInsertColumns(); } else if (time < shrinkThreshold && canShrink) { beginRemoveColumns(QModelIndex(), shrinkValue, m_d->effectiveNumFrames() - 1); m_d->numFramesOverride = shrinkValue; endRemoveColumns(); } } int KisTimeBasedItemModel::columnCount(const QModelIndex &parent) const { Q_UNUSED(parent); return m_d->effectiveNumFrames(); } QVariant KisTimeBasedItemModel::data(const QModelIndex &index, int role) const { switch (role) { case ActiveFrameRole: { return index.column() == m_d->activeFrameIndex; } } return QVariant(); } bool KisTimeBasedItemModel::setData(const QModelIndex &index, const QVariant &value, int role) { if (!index.isValid()) return false; switch (role) { case ActiveFrameRole: { setHeaderData(index.column(), Qt::Horizontal, value, role); break; } } return false; } QVariant KisTimeBasedItemModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal) { switch (role) { case ActiveFrameRole: return section == m_d->activeFrameIndex; case FrameCachedRole: return m_d->cachedFrames.size() > section ? m_d->cachedFrames[section] : false; case FramesPerSecondRole: return m_d->framesPerSecond(); } } return QVariant(); } bool KisTimeBasedItemModel::setHeaderData(int section, Qt::Orientation orientation, const QVariant &value, int role) { if (orientation == Qt::Horizontal) { switch (role) { case ActiveFrameRole: if (value.toBool() && section != m_d->activeFrameIndex) { int prevFrame = m_d->activeFrameIndex; m_d->activeFrameIndex = section; scrubTo(m_d->activeFrameIndex, m_d->scrubInProgress); /** * Optimization Hack Alert: * * ideally, we should emit all four signals, but... The * point is this code is used in a tight loop during * playback, so it should run as fast as possible. To tell * the story short, commenting out these three lines makes * playback run 15% faster ;) */ if (m_d->scrubInProgress) { //emit dataChanged(this->index(0, prevFrame), this->index(rowCount() - 1, prevFrame)); emit dataChanged(this->index(0, m_d->activeFrameIndex), this->index(rowCount() - 1, m_d->activeFrameIndex)); //emit headerDataChanged (Qt::Horizontal, prevFrame, prevFrame); //emit headerDataChanged (Qt::Horizontal, m_d->activeFrameIndex, m_d->activeFrameIndex); } else { emit dataChanged(this->index(0, prevFrame), this->index(rowCount() - 1, prevFrame)); emit dataChanged(this->index(0, m_d->activeFrameIndex), this->index(rowCount() - 1, m_d->activeFrameIndex)); emit headerDataChanged (Qt::Horizontal, prevFrame, prevFrame); emit headerDataChanged (Qt::Horizontal, m_d->activeFrameIndex, m_d->activeFrameIndex); } } } } return false; } bool KisTimeBasedItemModel::removeFrames(const QModelIndexList &indexes) { KisAnimationUtils::FrameItemList frameItems; - Q_FOREACH (const QModelIndex &index, indexes) { - int time = index.column(); + { + KisImageBarrierLockerWithFeedback locker(m_d->image); - QList channels = channelsAt(index); - Q_FOREACH(KisKeyframeChannel *channel, channels) { - if (channel->keyframeAt(time)) { - frameItems << KisAnimationUtils::FrameItem(channel->node(), channel->id(), index.column()); + Q_FOREACH (const QModelIndex &index, indexes) { + int time = index.column(); + + QList channels = channelsAt(index); + Q_FOREACH(KisKeyframeChannel *channel, channels) { + if (channel->keyframeAt(time)) { + frameItems << KisAnimationUtils::FrameItem(channel->node(), channel->id(), index.column()); + } } } } if (frameItems.isEmpty()) return false; KisAnimationUtils::removeKeyframes(m_d->image, frameItems); - Q_FOREACH (const QModelIndex &index, indexes) { - if (index.isValid()) { - emit dataChanged(index, index); - } - } - return true; } -bool KisTimeBasedItemModel::offsetFrames(QModelIndexList srcIndexes, const QPoint &offset, bool copyFrames, KUndo2Command *parentCommand) +KUndo2Command* KisTimeBasedItemModel::createOffsetFramesCommand(QModelIndexList srcIndexes, const QPoint &offset, bool copyFrames, KUndo2Command *parentCommand) { - bool result = false; - if (srcIndexes.isEmpty()) return result; - if (offset.isNull()) return result; + if (srcIndexes.isEmpty()) return 0; + if (offset.isNull()) return 0; KisAnimationUtils::sortPointsForSafeMove(&srcIndexes, offset); KisAnimationUtils::FrameItemList srcFrameItems; KisAnimationUtils::FrameItemList dstFrameItems; - QModelIndexList updateIndexes; Q_FOREACH (const QModelIndex &srcIndex, srcIndexes) { QModelIndex dstIndex = index( srcIndex.row() + offset.y(), srcIndex.column() + offset.x()); KisNodeSP srcNode = nodeAt(srcIndex); KisNodeSP dstNode = nodeAt(dstIndex); if (!srcNode || !dstNode) { - return false; + return 0; } QList channels = channelsAt(srcIndex); Q_FOREACH(KisKeyframeChannel *channel, channels) { if (channel->keyframeAt(srcIndex.column())) { srcFrameItems << KisAnimationUtils::FrameItem(srcNode, channel->id(), srcIndex.column()); dstFrameItems << KisAnimationUtils::FrameItem(dstNode, channel->id(), dstIndex.column()); } } + } - if (!copyFrames) { - updateIndexes << srcIndex; - } + KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(srcFrameItems.size() == dstFrameItems.size(), 0); + if (srcFrameItems.isEmpty()) return 0; - updateIndexes << dstIndex; - } + return + KisAnimationUtils::createMoveKeyframesCommand(srcFrameItems, + dstFrameItems, + copyFrames, + parentCommand); +} - result = KisAnimationUtils::moveKeyframes(m_d->image, - srcFrameItems, - dstFrameItems, - copyFrames, - parentCommand); +bool KisTimeBasedItemModel::offsetFrames(QModelIndexList srcIndexes, const QPoint &offset, bool copyFrames) +{ + KUndo2Command *cmd = 0; + + { + KisImageBarrierLockerWithFeedback locker(m_d->image); + cmd = createOffsetFramesCommand(srcIndexes, offset, copyFrames); + } - Q_FOREACH (const QModelIndex &index, updateIndexes) { - emit dataChanged(index, index); + if (cmd) { + KisProcessingApplicator::runSingleCommandStroke(m_d->image, cmd, KisStrokeJobData::BARRIER); } - return result; + return cmd; } void KisTimeBasedItemModel::slotInternalScrubPreviewRequested(int time) { if (m_d->animationPlayer && !m_d->animationPlayer->isPlaying()) { m_d->animationPlayer->displayFrame(time); } } void KisTimeBasedItemModel::setScrubState(bool active) { if (!m_d->scrubInProgress && active) { m_d->scrubStartFrame = m_d->activeFrameIndex; m_d->scrubInProgress = true; } if (m_d->scrubInProgress && !active) { m_d->scrubInProgress = false; if (m_d->scrubStartFrame >= 0 && m_d->scrubStartFrame != m_d->activeFrameIndex) { scrubTo(m_d->activeFrameIndex, false); } m_d->scrubStartFrame = -1; } } void KisTimeBasedItemModel::scrubTo(int time, bool preview) { if (m_d->animationPlayer && m_d->animationPlayer->isPlaying()) return; KIS_ASSERT_RECOVER_RETURN(m_d->image); if (preview) { if (m_d->animationPlayer) { m_d->scrubbingCompressor->start(time); } } else { m_d->image->animationInterface()->requestTimeSwitchWithUndo(time); } } void KisTimeBasedItemModel::slotFramerateChanged() { emit headerDataChanged(Qt::Horizontal, 0, columnCount() - 1); } void KisTimeBasedItemModel::slotCurrentTimeChanged(int time) { if (time != m_d->activeFrameIndex) { setHeaderData(time, Qt::Horizontal, true, ActiveFrameRole); } } void KisTimeBasedItemModel::slotCacheChanged() { const int numFrames = columnCount(); m_d->cachedFrames.resize(numFrames); for (int i = 0; i < numFrames; i++) { m_d->cachedFrames[i] = m_d->framesCache->frameStatus(i) == KisAnimationFrameCache::Cached; } emit headerDataChanged(Qt::Horizontal, 0, numFrames); } void KisTimeBasedItemModel::slotPlaybackFrameChanged() { if (!m_d->animationPlayer->isPlaying()) return; setData(index(0, m_d->animationPlayer->currentTime()), true, ActiveFrameRole); } void KisTimeBasedItemModel::slotPlaybackStopped() { setData(index(0, m_d->image->animationInterface()->currentUITime()), true, ActiveFrameRole); } void KisTimeBasedItemModel::setPlaybackRange(const KisTimeRange &range) { if (m_d->image.isNull()) return; KisImageAnimationInterface *i = m_d->image->animationInterface(); i->setPlaybackRange(range); } bool KisTimeBasedItemModel::isPlaybackActive() const { return m_d->animationPlayer && m_d->animationPlayer->isPlaying(); } int KisTimeBasedItemModel::currentTime() const { return m_d->image->animationInterface()->currentUITime(); } KisImageWSP KisTimeBasedItemModel::image() const { return m_d->image; } diff --git a/plugins/dockers/animation/kis_time_based_item_model.h b/plugins/dockers/animation/kis_time_based_item_model.h index 6e567615fd..458b6aa6b6 100644 --- a/plugins/dockers/animation/kis_time_based_item_model.h +++ b/plugins/dockers/animation/kis_time_based_item_model.h @@ -1,96 +1,98 @@ /* * Copyright (c) 2016 Jouni Pentikäinen * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef _KIS_TIME_BASED_ITEM_MODEL_H #define _KIS_TIME_BASED_ITEM_MODEL_H #include #include #include "kritaanimationdocker_export.h" #include "kis_types.h" class KisTimeRange; class KisAnimationPlayer; class KisKeyframeChannel; class KRITAANIMATIONDOCKER_EXPORT KisTimeBasedItemModel : public QAbstractTableModel { Q_OBJECT public: KisTimeBasedItemModel(QObject *parent); ~KisTimeBasedItemModel(); void setImage(KisImageWSP image); void setFrameCache(KisAnimationFrameCacheSP cache); void setAnimationPlayer(KisAnimationPlayer *player); void setLastVisibleFrame(int time); int columnCount(const QModelIndex &parent = QModelIndex()) const; virtual QVariant data(const QModelIndex &index, int role) const; virtual bool setData(const QModelIndex &index, const QVariant &value, int role); virtual QVariant headerData(int section, Qt::Orientation orientation, int role) const; virtual bool setHeaderData(int section, Qt::Orientation orientation, const QVariant &value, int role); bool removeFrames(const QModelIndexList &indexes); - bool offsetFrames(QModelIndexList srcIndexes, const QPoint &offset, bool copyFrames, KUndo2Command *parentCommand=0); + bool offsetFrames(QModelIndexList srcIndexes, const QPoint &offset, bool copyFrames); void setScrubState(bool active); void scrubTo(int time, bool preview); void setPlaybackRange(const KisTimeRange &range); bool isPlaybackActive() const; int currentTime() const; enum ItemDataRole { ActiveFrameRole = Qt::UserRole + 101, FrameExistsRole, SpecialKeyframeExists, FrameCachedRole, FrameEditableRole, FramesPerSecondRole, UserRole }; protected: virtual KisNodeSP nodeAt(QModelIndex index) const = 0; virtual QList channelsAt(QModelIndex index) const = 0; KisImageWSP image() const; + KUndo2Command* createOffsetFramesCommand(QModelIndexList srcIndexes, const QPoint &offset, bool copyFrames, KUndo2Command *parentCommand = 0); + private Q_SLOTS: void slotFramerateChanged(); void slotCurrentTimeChanged(int time); void slotCacheChanged(); void slotInternalScrubPreviewRequested(int time); void slotPlaybackFrameChanged(); void slotPlaybackStopped(); private: struct Private; const QScopedPointer m_d; }; #endif diff --git a/plugins/dockers/animation/timeline_frames_model.cpp b/plugins/dockers/animation/timeline_frames_model.cpp index e29ebfbfb8..f23f5c3b59 100644 --- a/plugins/dockers/animation/timeline_frames_model.cpp +++ b/plugins/dockers/animation/timeline_frames_model.cpp @@ -1,729 +1,720 @@ /* * 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 "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 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 specialKeyframeExists(int row, int column) { KisNodeDummy *dummy = converter->dummyFromRow(row); if (!dummy) return false; QList channels = dummy->node()->keyframeChannels(); Q_FOREACH(KisKeyframeChannel *channel, channels) { 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->keyframeAt(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; - return KisAnimationUtils::createKeyframeLazy(image, node, KisKeyframeChannel::Content.id(), column, copy); + 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; } QList 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())); } if (m_d->dummiesFacade != oldDummiesFacade) { reset(); } 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::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); KIS_ASSERT_RECOVER_RETURN(dummy); 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 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; } Qt::DropActions TimelineFramesModel::supportedDropActions() const { return Qt::MoveAction | Qt::CopyAction; } 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 { QMimeData *data = new QMimeData(); QByteArray encoded; QDataStream stream(&encoded, QIODevice::WriteOnly); const int baseRow = m_d->lastClickedIndex.row(); const int baseColumn = m_d->lastClickedIndex.column(); stream << indexes.size(); stream << baseRow << baseColumn; Q_FOREACH (const QModelIndex &index, indexes) { stream << index.row() - baseRow << index.column() - baseColumn; } 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); bool result = false; if ((action != Qt::MoveAction && action != Qt::CopyAction) || !parent.isValid()) return result; const bool copyFrames = action == Qt::CopyAction; QByteArray encoded = data->data("application/x-krita-frame"); QDataStream stream(&encoded, QIODevice::ReadOnly); int size, baseRow, baseColumn; stream >> size >> baseRow >> baseColumn; QModelIndexList srcIndexes; for (int i = 0; i < size; i++) { int relRow, relColumn; stream >> relRow >> relColumn; int srcRow = baseRow + relRow; int srcColumn = baseColumn + relColumn; srcIndexes << index(srcRow, srcColumn); } const QPoint offset(parent.column() - baseColumn, parent.row() - baseRow); return offsetFrames(srcIndexes, offset, copyFrames); } 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; - bool result = m_d->addKeyframe(dstIndex.row(), dstIndex.column(), false); - if (result) { - emit dataChanged(dstIndex, dstIndex); - } - - return result; + return m_d->addKeyframe(dstIndex.row(), dstIndex.column(), false); } bool TimelineFramesModel::copyFrame(const QModelIndex &dstIndex) { if (!dstIndex.isValid()) return false; - bool result = m_d->addKeyframe(dstIndex.row(), dstIndex.column(), true); - if (result) { - emit dataChanged(dstIndex, dstIndex); - } - - return result; + return m_d->addKeyframe(dstIndex.row(), dstIndex.column(), 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); }