diff --git a/plugins/dockers/animation/kis_animation_curve_docker.cpp b/plugins/dockers/animation/kis_animation_curve_docker.cpp index e52f2617d5..3e069fe38c 100644 --- a/plugins/dockers/animation/kis_animation_curve_docker.cpp +++ b/plugins/dockers/animation/kis_animation_curve_docker.cpp @@ -1,160 +1,167 @@ /* * 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 #include #include #include "kis_animation_curve_docker.h" #include "kis_animation_curves_model.h" #include "kis_animation_curves_view.h" #include "kis_animation_curve_channel_list_model.h" #include "kis_animation_curve_channel_list_delegate.h" #include "KisDocument.h" #include "kis_canvas2.h" #include "kis_shape_controller.h" #include "kis_signal_auto_connection.h" #include "KisViewManager.h" #include "kis_node_manager.h" #include "kis_animation_frame_cache.h" #include "klocalizedstring.h" #include "kis_icon_utils.h" #include "ui_wdg_animation_curves.h" struct KisAnimationCurveDocker::Private { Private(QWidget *parent) : curvesWidget() , curvesModel(new KisAnimationCurvesModel(parent)) { channelListModel = new KisAnimationCurveChannelListModel(curvesModel, parent); } Ui_WdgAnimationCurves curvesWidget; KisAnimationCurvesModel *curvesModel; KisAnimationCurveChannelListModel *channelListModel; QPointer canvas; KisSignalAutoConnectionsStore canvasConnections; }; KisAnimationCurveDocker::KisAnimationCurveDocker() : QDockWidget(i18n("Animation curves")) , m_d(new Private(this)) { QWidget *mainWidget = new QWidget(this); setWidget(mainWidget); m_d->curvesWidget.setupUi(mainWidget); KisAnimationCurvesView *curvesView = m_d->curvesWidget.curvesView; QTreeView *channelListView = m_d->curvesWidget.channelListView; KisAnimationCurveChannelListDelegate *listDelegate = new KisAnimationCurveChannelListDelegate(channelListView); curvesView->setModel(m_d->curvesModel); curvesView->setZoomButtons(m_d->curvesWidget.btnHorizontalZoom, m_d->curvesWidget.btnVerticalZoom); channelListView->setModel(m_d->channelListModel); channelListView->setItemDelegate(listDelegate); channelListView->setHeaderHidden(true); m_d->curvesWidget.splitter->setStretchFactor(0, 1); m_d->curvesWidget.splitter->setStretchFactor(1, 4); connect(m_d->channelListModel, &KisAnimationCurveChannelListModel::rowsInserted, this, &KisAnimationCurveDocker::slotListRowsInserted); connect(m_d->curvesWidget.btnConstantInterpolation, &QToolButton::clicked, curvesView, &KisAnimationCurvesView::applyConstantMode); connect(m_d->curvesWidget.btnLinearInterpolation, &QToolButton::clicked, curvesView, &KisAnimationCurvesView::applyLinearMode); connect(m_d->curvesWidget.btnBezierInterpolation, &QToolButton::clicked, curvesView, &KisAnimationCurvesView::applyBezierMode); connect(m_d->curvesWidget.btnSmooth, &QToolButton::clicked, curvesView, &KisAnimationCurvesView::applySmoothMode); connect(m_d->curvesWidget.btnSharp, &QToolButton::clicked, curvesView, &KisAnimationCurvesView::applySharpMode); + connect(m_d->curvesWidget.btnAddKeyframe, &QToolButton::clicked, + curvesView, &KisAnimationCurvesView::createKeyframe); + connect(m_d->curvesWidget.btnRemoveKeyframes, &QToolButton::clicked, + curvesView, &KisAnimationCurvesView::removeKeyframes); } KisAnimationCurveDocker::~KisAnimationCurveDocker() {} void KisAnimationCurveDocker::setCanvas(KoCanvasBase * canvas) { if (canvas && m_d->canvas == canvas) return; if (m_d->canvas) { m_d->canvasConnections.clear(); m_d->canvas->disconnectCanvasObserver(this); m_d->channelListModel->selectedNodesChanged(KisNodeList()); } m_d->canvas = dynamic_cast(canvas); setEnabled(m_d->canvas != 0); if(m_d->canvas) { KisDocument *doc = static_cast(m_d->canvas->imageView()->document()); KisShapeController *kritaShapeController = dynamic_cast(doc->shapeController()); m_d->channelListModel->setDummiesFacade(kritaShapeController); m_d->curvesModel->setImage(m_d->canvas->image()); m_d->curvesModel->setFrameCache(m_d->canvas->frameCache()); m_d->curvesModel->setAnimationPlayer(m_d->canvas->animationPlayer()); m_d->canvasConnections.addConnection( m_d->canvas->viewManager()->nodeManager(), SIGNAL(sigUiNeedChangeSelectedNodes(KisNodeList)), m_d->channelListModel, SLOT(selectedNodesChanged(KisNodeList)) ); m_d->channelListModel->selectedNodesChanged(m_d->canvas->viewManager()->nodeManager()->selectedNodes()); } } void KisAnimationCurveDocker::unsetCanvas() { setCanvas(0); } void KisAnimationCurveDocker::setMainWindow(KisViewManager *kisview) { connect(kisview->mainWindow(), SIGNAL(themeChanged()), this, SLOT(slotUpdateIcons())); slotUpdateIcons(); } void KisAnimationCurveDocker::slotUpdateIcons() { m_d->curvesWidget.btnConstantInterpolation->setIcon(KisIconUtils::loadIcon("interpolation_constant")); m_d->curvesWidget.btnLinearInterpolation->setIcon(KisIconUtils::loadIcon("interpolation_linear")); m_d->curvesWidget.btnBezierInterpolation->setIcon(KisIconUtils::loadIcon("interpolation_bezier")); m_d->curvesWidget.btnSmooth->setIcon(KisIconUtils::loadIcon("interpolation_smooth")); m_d->curvesWidget.btnSharp->setIcon(KisIconUtils::loadIcon("interpolation_sharp")); m_d->curvesWidget.btnHorizontalZoom->setIcon(KisIconUtils::loadIcon("zoom-in")); m_d->curvesWidget.btnVerticalZoom->setIcon(KisIconUtils::loadIcon("zoom-in")); + + m_d->curvesWidget.btnAddKeyframe->setIcon(KisIconUtils::loadIcon("addlayer")); + m_d->curvesWidget.btnRemoveKeyframes->setIcon(KisIconUtils::loadIcon("deletelayer")); } void KisAnimationCurveDocker::slotListRowsInserted(const QModelIndex &parentIndex, int first, int last) { // Auto-expand nodes on the tree for (int r=first; r<=last; r++) { QModelIndex index = m_d->channelListModel->index(r, 0, parentIndex); m_d->curvesWidget.channelListView->expand(index); } } diff --git a/plugins/dockers/animation/kis_animation_curves_model.cpp b/plugins/dockers/animation/kis_animation_curves_model.cpp index 2d300e3aaa..6ef5f5fa5a 100644 --- a/plugins/dockers/animation/kis_animation_curves_model.cpp +++ b/plugins/dockers/animation/kis_animation_curves_model.cpp @@ -1,366 +1,364 @@ /* * 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" 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) { 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) { 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) { - QPointF leftTangent = (role == LeftTangentRole ? value.toPointF() : keyframe->leftTangent()); - QPointF rightTangent = (role == RightTangentRole ? value.toPointF() : keyframe->rightTangent()); + if (!keyframe) return false; - KUndo2Command *command = m_d->undoCommand ? m_d->undoCommand : new KUndo2Command(kundo2_i18n("Adjust tangent")); - channel->setInterpolationTangents(keyframe, keyframe->tangentsMode(), leftTangent, rightTangent, command); + QPointF leftTangent = (role == LeftTangentRole ? value.toPointF() : keyframe->leftTangent()); + QPointF rightTangent = (role == RightTangentRole ? value.toPointF() : keyframe->rightTangent()); - if (!m_d->undoCommand) { - image()->postExecutionUndoAdapter()->addCommand(toQShared(command)); - } - - return true; - } + 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) { - KUndo2Command *command = m_d->undoCommand ? m_d->undoCommand : new KUndo2Command(kundo2_i18n("Set interpolation mode")); - channel->setInterpolationMode(keyframe, (KisKeyframe::InterpolationMode)value.toInt(), command); + if (!keyframe) return false; - if (!m_d->undoCommand) { - image()->postExecutionUndoAdapter()->addCommand(toQShared(command)); - } - } + 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) { - KUndo2Command *command = m_d->undoCommand ? m_d->undoCommand : new KUndo2Command(kundo2_i18n("Set interpolation mode")); - - KisKeyframe::InterpolationTangentsMode mode = (KisKeyframe::InterpolationTangentsMode)value.toInt(); - QPointF leftTangent = keyframe->leftTangent(); - QPointF rightTangent = keyframe->rightTangent(); + if (!keyframe) return false; - channel->setInterpolationTangents(keyframe, mode, leftTangent, rightTangent, command); + KisKeyframe::InterpolationTangentsMode mode = (KisKeyframe::InterpolationTangentsMode)value.toInt(); + QPointF leftTangent = keyframe->leftTangent(); + QPointF rightTangent = keyframe->rightTangent(); - if (!m_d->undoCommand) { - image()->postExecutionUndoAdapter()->addCommand(toQShared(command)); - } - } + if (!command) command = new KUndo2Command(kundo2_i18n("Set interpolation mode")); + channel->setInterpolationTangents(keyframe, mode, leftTangent, rightTangent, command); } break; default: - break; + return KisTimeBasedItemModel::setData(index, value, role); + } + + if (command && !m_d->undoCommand) { + image()->postExecutionUndoAdapter()->addCommand(toQShared(command)); } - return KisTimeBasedItemModel::setData(index, value, role); + 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::removeFrames(const QModelIndexList &indexes) -{ - // TODO - return false; -} - 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; } Q_FOREACH(QModelIndex oldIndex, indexes) { KisScalarKeyframeChannel *channel = m_d->getCurveAt(oldIndex)->channel(); KIS_ASSERT_RECOVER_RETURN_VALUE(channel, false); KisKeyframeSP keyframe = channel->keyframeAt(oldIndex.column() + timeOffset); KIS_ASSERT_RECOVER_RETURN_VALUE(!keyframe.isNull(), false); qreal currentValue = channel->scalarValue(keyframe); channel->setScalarValue(keyframe, currentValue + valueOffset, command); }; image()->postExecutionUndoAdapter()->addCommand(toQShared(command)); 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 { return KisNodeSP(m_d->getCurveAt(index)->channel()->node()); } QList KisAnimationCurvesModel::channelsAt(QModelIndex index) const { KisKeyframeChannel *channel = m_d->getCurveAt(index)->channel(); QList list({channel}); return list; } void KisAnimationCurvesModel::slotKeyframeChanged(KisKeyframe *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_curves_model.h b/plugins/dockers/animation/kis_animation_curves_model.h index ec8f863b94..8862900010 100644 --- a/plugins/dockers/animation/kis_animation_curves_model.h +++ b/plugins/dockers/animation/kis_animation_curves_model.h @@ -1,99 +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_ANIMATION_CURVES_MODEL_H #define _KIS_ANIMATION_CURVES_MODEL_H #include #include #include #include "kis_time_based_item_model.h" #include "kis_types.h" #include "kundo2command.h" class KisScalarKeyframeChannel; class KisAnimationCurve { public: KisAnimationCurve(KisScalarKeyframeChannel *channel, QColor color); KisScalarKeyframeChannel *channel() const; QColor color() const; void setVisible(bool visible); bool visible() const; private: struct Private; const QScopedPointer m_d; }; class KisAnimationCurvesModel : public KisTimeBasedItemModel { Q_OBJECT public: KisAnimationCurvesModel(QObject *parent); ~KisAnimationCurvesModel(); bool hasConnectionToCanvas() const; KisAnimationCurve *addCurve(KisScalarKeyframeChannel *channel); void removeCurve(KisAnimationCurve *curve); void setCurveVisible(KisAnimationCurve *curve, bool visible); int rowCount(const QModelIndex &parent = QModelIndex()) const; QVariant data(const QModelIndex &index, int role) const; bool setData(const QModelIndex &index, const QVariant &value, int role); QVariant headerData(int section, Qt::Orientation orientation, int role) const; /** * Begins a block of commands. The following calls to setData will be grouped to a single undo step. * Note: MUST be followed by a call to endCommand(). */ void beginCommand(const KUndo2MagicString &text); void endCommand(); - bool removeFrames(const QModelIndexList &indexes); bool adjustKeyframes(const QModelIndexList &indexes, int timeOffset, qreal valueOffset); enum ItemDataRole { ScalarValueRole = KisTimeBasedItemModel::UserRole + 101, InterpolationModeRole, TangentsModeRole, LeftTangentRole, RightTangentRole, CurveColorRole, CurveVisibleRole, PreviousKeyframeTime, NextKeyframeTime }; protected: KisNodeSP nodeAt(QModelIndex index) const; QList channelsAt(QModelIndex index) const; private Q_SLOTS: void slotKeyframeChanged(KisKeyframe *keyframe); private: struct Private; const QScopedPointer m_d; }; #endif diff --git a/plugins/dockers/animation/kis_animation_curves_view.cpp b/plugins/dockers/animation/kis_animation_curves_view.cpp index 49c45563ac..427a7ca4f4 100644 --- a/plugins/dockers/animation/kis_animation_curves_view.cpp +++ b/plugins/dockers/animation/kis_animation_curves_view.cpp @@ -1,655 +1,672 @@ /* * 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_view.h" #include #include #include #include #include #include #include "kis_animation_curves_model.h" #include "timeline_ruler_header.h" #include "kis_animation_curves_value_ruler.h" #include "kis_animation_curves_keyframe_delegate.h" #include "kis_scalar_keyframe_channel.h" #include "kis_zoom_button.h" #include "kis_custom_modifiers_catcher.h" const int VERTICAL_PADDING = 30; struct KisAnimationCurvesView::Private { Private() : model(0) , isDraggingKeyframe(false) , isAdjustingHandle(false) {} KisAnimationCurvesModel *model; TimelineRulerHeader *horizontalHeader; KisAnimationCurvesValueRuler *verticalHeader; KisAnimationCurvesKeyframeDelegate *itemDelegate; KisZoomButton *horizontalZoomButton; KisZoomButton *verticalZoomButton; KisCustomModifiersCatcher *modifiersCatcher; bool isDraggingKeyframe; bool isAdjustingHandle; int adjustedHandle; // 0 = left, 1 = right QPoint dragStart; QPoint dragOffset; int horizontalZoomStillPointIndex; int horizontalZoomStillPointOriginalOffset; qreal verticalZoomStillPoint; qreal verticalZoomStillPointOriginalOffset; }; KisAnimationCurvesView::KisAnimationCurvesView(QWidget *parent) : QAbstractItemView(parent) , m_d(new Private()) { m_d->horizontalHeader = new TimelineRulerHeader(this); m_d->verticalHeader = new KisAnimationCurvesValueRuler(this); m_d->itemDelegate = new KisAnimationCurvesKeyframeDelegate(m_d->horizontalHeader, m_d->verticalHeader, this); m_d->modifiersCatcher = new KisCustomModifiersCatcher(this); m_d->modifiersCatcher->addModifier("pan-zoom", Qt::Key_Space); setSelectionMode(QAbstractItemView::ExtendedSelection); setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel); setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); } KisAnimationCurvesView::~KisAnimationCurvesView() {} void KisAnimationCurvesView::setModel(QAbstractItemModel *model) { m_d->model = dynamic_cast(model); QAbstractItemView::setModel(model); m_d->horizontalHeader->setModel(model); connect(model, &QAbstractItemModel::rowsInserted, this, &KisAnimationCurvesView::slotRowsChanged); connect(model, &QAbstractItemModel::rowsRemoved, this, &KisAnimationCurvesView::slotRowsChanged); connect(model, &QAbstractItemModel::dataChanged, this, &KisAnimationCurvesView::slotDataChanged); connect(model, &QAbstractItemModel::headerDataChanged, this, &KisAnimationCurvesView::slotHeaderDataChanged); } void KisAnimationCurvesView::setZoomButtons(KisZoomButton *horizontal, KisZoomButton *vertical) { m_d->horizontalZoomButton = horizontal; m_d->verticalZoomButton = vertical; connect(horizontal, &KisZoomButton::zoomStarted, this, &KisAnimationCurvesView::slotHorizontalZoomStarted); connect(horizontal, &KisZoomButton::zoomLevelChanged, this, &KisAnimationCurvesView::slotHorizontalZoomLevelChanged); connect(vertical, &KisZoomButton::zoomStarted, this, &KisAnimationCurvesView::slotVerticalZoomStarted); connect(vertical, &KisZoomButton::zoomLevelChanged, this, &KisAnimationCurvesView::slotVerticalZoomLevelChanged); } QRect KisAnimationCurvesView::visualRect(const QModelIndex &index) const { return m_d->itemDelegate->itemRect(index); } void KisAnimationCurvesView::scrollTo(const QModelIndex &index, QAbstractItemView::ScrollHint hint) { // TODO } QModelIndex KisAnimationCurvesView::indexAt(const QPoint &point) const { if (!model()) return QModelIndex(); int time = m_d->horizontalHeader->logicalIndexAt(point.x()); int rows = model()->rowCount(); for (int row=0; row < rows; row++) { QModelIndex index = model()->index(row, time); if (index.data(KisTimeBasedItemModel::SpecialKeyframeExists).toBool()) { QRect nodePos = m_d->itemDelegate->itemRect(index); if (nodePos.contains(point)) { return index; } } } return QModelIndex(); } void KisAnimationCurvesView::paintEvent(QPaintEvent *e) { QPainter painter(viewport()); QRect r = e->rect(); r.translate(dirtyRegionOffset()); int firstFrame = m_d->horizontalHeader->logicalIndexAt(r.left()); int lastFrame = m_d->horizontalHeader->logicalIndexAt(r.right()); if (lastFrame == -1) lastFrame = model()->columnCount(); paintCurves(painter, firstFrame, lastFrame); paintKeyframes(painter, firstFrame, lastFrame); } void KisAnimationCurvesView::paintCurves(QPainter &painter, int firstFrame, int lastFrame) { int channels = model()->rowCount(); for (int channel = 0; channel < channels; channel++) { QModelIndex index0 = model()->index(channel, 0); if (!isIndexHidden(index0)) { QColor color = index0.data(KisAnimationCurvesModel::CurveColorRole).value(); painter.setPen(QPen(color, 1)); paintCurve(channel, firstFrame, lastFrame, painter); } } } void KisAnimationCurvesView::paintCurve(int channel, int firstFrame, int lastFrame, QPainter &painter) { int selectionOffset = m_d->isDraggingKeyframe ? (m_d->dragOffset.x() / m_d->horizontalHeader->defaultSectionSize()) : 0; QModelIndex index = findNextKeyframeIndex(channel, firstFrame+1, selectionOffset, true); if (!index.isValid()) { index = findNextKeyframeIndex(channel, firstFrame, selectionOffset, false); } if (!index.isValid()) return; QPointF previousKeyPos = m_d->itemDelegate->nodeCenter(index, selectionModel()->isSelected(index)); QPointF rightTangent = m_d->itemDelegate->rightHandle(index, index == currentIndex()); while(index.column() <= lastFrame) { int interpolationMode = index.data(KisAnimationCurvesModel::InterpolationModeRole).toInt(); int time = (m_d->isDraggingKeyframe && selectionModel()->isSelected(index)) ? index.column() + selectionOffset : index.column(); index = findNextKeyframeIndex(channel, time, selectionOffset, false); if (!index.isValid()) return; bool active = (index == currentIndex()); QPointF nextKeyPos = m_d->itemDelegate->nodeCenter(index, selectionModel()->isSelected(index)); QPointF leftTangent = m_d->itemDelegate->leftHandle(index, active); if (interpolationMode == KisKeyframe::Constant) { painter.drawLine(previousKeyPos, QPointF(nextKeyPos.x(), previousKeyPos.y())); } else if (interpolationMode == KisKeyframe::Linear) { painter.drawLine(previousKeyPos, nextKeyPos); } else { paintCurveSegment(painter, previousKeyPos, rightTangent, leftTangent, nextKeyPos); } previousKeyPos = nextKeyPos; rightTangent = m_d->itemDelegate->rightHandle(index, active); } } void KisAnimationCurvesView::paintCurveSegment(QPainter &painter, QPointF pos1, QPointF rightTangent, QPointF leftTangent, QPointF pos2) { const int steps = 16; QPointF previousPos; for (int step = 0; step <= steps; step++) { qreal t = 1.0 * step / steps; QPointF nextPos = KisScalarKeyframeChannel::interpolate(pos1, rightTangent, leftTangent, pos2, t); if (step > 0) { painter.drawLine(previousPos, nextPos); } previousPos = nextPos; } } void KisAnimationCurvesView::paintKeyframes(QPainter &painter, int firstFrame, int lastFrame) { int channels = model()->rowCount(); for (int channel = 0; channel < channels; channel++) { for (int time=firstFrame; time <= lastFrame; time++) { QModelIndex index = model()->index(channel, time); bool keyframeExists = model()->data(index, KisAnimationCurvesModel::SpecialKeyframeExists).toReal(); if (keyframeExists && !isIndexHidden(index)) { QStyleOptionViewItem opt; if (selectionModel()->isSelected(index)) { opt.state |= QStyle::State_Selected; } if (index == selectionModel()->currentIndex()) { opt.state |= QStyle::State_HasFocus; } m_d->itemDelegate->paint(&painter, opt, index); } } } } QModelIndex KisAnimationCurvesView::findNextKeyframeIndex(int channel, int time, int selectionOffset, bool backward) { KisAnimationCurvesModel::ItemDataRole role = backward ? KisAnimationCurvesModel::PreviousKeyframeTime : KisAnimationCurvesModel::NextKeyframeTime; QModelIndex currentIndex = model()->index(channel, time); if (!selectionOffset) { QVariant next = currentIndex.data(role); return (next.isValid()) ? model()->index(channel, next.toInt()) : QModelIndex(); } else { // Find the next unselected index QModelIndex nextIndex = currentIndex; do { QVariant next = nextIndex.data(role); nextIndex = (next.isValid()) ? model()->index(channel, next.toInt()) : QModelIndex(); } while(nextIndex.isValid() && selectionModel()->isSelected(nextIndex)); // Find the next selected index, accounting for D&D offset QModelIndex draggedIndex = model()->index(channel, qMax(0, time - selectionOffset)); do { QVariant next = draggedIndex.data(role); draggedIndex = (next.isValid()) ? model()->index(channel, next.toInt()) : QModelIndex(); } while(draggedIndex.isValid() && !selectionModel()->isSelected(draggedIndex)); // Choose the earlier of the two if (draggedIndex.isValid() && nextIndex.isValid()) { if (draggedIndex.column() + selectionOffset <= nextIndex.column()) { return draggedIndex; } else { return nextIndex; } } else if (draggedIndex.isValid()) { return draggedIndex; } else { return nextIndex; } } } void KisAnimationCurvesView::updateVerticalRange() { if (!model()) return; qreal min = 0; qreal max = 0; int rows = model()->rowCount(); for (int row = 0; row < rows; row++) { QModelIndex index = model()->index(row, 0); QVariant nextTime; do { qreal value = index.data(KisAnimationCurvesModel::ScalarValueRole).toReal(); if (value < min) min = value; if (value > max) max = value; nextTime = index.data(KisAnimationCurvesModel::NextKeyframeTime); if (nextTime.isValid()) index = model()->index(row, nextTime.toInt()); } while (nextTime.isValid()); } int viewMin = max * m_d->verticalHeader->scaleFactor(); int viewMax = min * m_d->verticalHeader->scaleFactor(); viewMin -= VERTICAL_PADDING; viewMax += VERTICAL_PADDING; verticalScrollBar()->setRange(viewMin, viewMax - viewport()->height()); } QModelIndex KisAnimationCurvesView::moveCursor(QAbstractItemView::CursorAction cursorAction, Qt::KeyboardModifiers modifiers) { // TODO return QModelIndex(); } int KisAnimationCurvesView::horizontalOffset() const { return m_d->horizontalHeader->offset(); } int KisAnimationCurvesView::verticalOffset() const { return m_d->verticalHeader->offset(); } bool KisAnimationCurvesView::isIndexHidden(const QModelIndex &index) const { return !index.data(KisAnimationCurvesModel::CurveVisibleRole).toBool(); } void KisAnimationCurvesView::setSelection(const QRect &rect, QItemSelectionModel::SelectionFlags command) { int timeFrom = m_d->horizontalHeader->logicalIndexAt(rect.left()); int timeTo = m_d->horizontalHeader->logicalIndexAt(rect.right()); QItemSelection selection; int rows = model()->rowCount(); for (int row=0; row < rows; row++) { for (int time = timeFrom; time <= timeTo; time++) { QModelIndex index = model()->index(row, time); if (index.data(KisTimeBasedItemModel::SpecialKeyframeExists).toBool()) { QRect itemRect = m_d->itemDelegate->itemRect(index); if (itemRect.intersects(rect)) { selection.select(index, index); } } } } selectionModel()->select(selection, command); } QRegion KisAnimationCurvesView::visualRegionForSelection(const QItemSelection &selection) const { QRegion region; Q_FOREACH(QModelIndex index, selection.indexes()) { region += m_d->itemDelegate->visualRect(index); } return region; } void KisAnimationCurvesView::mousePressEvent(QMouseEvent *e) { if (m_d->modifiersCatcher->modifierPressed("pan-zoom")) { if (e->button() == Qt::LeftButton) { // TODO: pan } else { qreal horizontalStaticPoint = m_d->horizontalHeader->logicalIndexAt(e->pos().x()); qreal verticalStaticPoint = m_d->verticalHeader->mapViewToValue(e->pos().y()); m_d->horizontalZoomButton->beginZoom(QPoint(e->pos().x(), 0), horizontalStaticPoint); m_d->verticalZoomButton->beginZoom(QPoint(0, e->pos().y()), verticalStaticPoint); } } else if (e->button() == Qt::LeftButton) { m_d->dragStart = e->pos(); Q_FOREACH(QModelIndex index, selectedIndexes()) { QPointF center = m_d->itemDelegate->nodeCenter(index, false); QPointF leftHandle = center + m_d->itemDelegate->leftHandle(index, false); QPointF rightHandle = center + m_d->itemDelegate->rightHandle(index, false); if ((e->localPos() - leftHandle).manhattanLength() < 8) { m_d->isAdjustingHandle = true; m_d->adjustedHandle = 0; setCurrentIndex(index); return; } else if ((e->localPos() - rightHandle).manhattanLength() < 8) { m_d->isAdjustingHandle = true; m_d->adjustedHandle = 1; setCurrentIndex(index); return; } } } QAbstractItemView::mousePressEvent(e); } void KisAnimationCurvesView::mouseMoveEvent(QMouseEvent *e) { if (m_d->modifiersCatcher->modifierPressed("pan-zoom")) { if (e->buttons() & Qt::LeftButton) { // TODO: pan } else { m_d->horizontalZoomButton->continueZoom(QPoint(e->pos().x(), 0)); m_d->verticalZoomButton->continueZoom(QPoint(0, e->pos().y())); } } else if (e->buttons() & Qt::LeftButton) { m_d->dragOffset = e->pos() - m_d->dragStart; if (m_d->isAdjustingHandle) { m_d->itemDelegate->setHandleAdjustment(m_d->dragOffset, m_d->adjustedHandle); viewport()->update(); return; } else if (m_d->isDraggingKeyframe) { m_d->itemDelegate->setSelectedItemVisualOffset(m_d->dragOffset); viewport()->update(); return; } else if (selectionModel()->hasSelection()) { if ((e->pos() - m_d->dragStart).manhattanLength() > QApplication::startDragDistance()) { m_d->isDraggingKeyframe = true; } } } QAbstractItemView::mouseMoveEvent(e); } void KisAnimationCurvesView::mouseReleaseEvent(QMouseEvent *e) { if (e->button() == Qt::LeftButton) { if (m_d->isDraggingKeyframe) { QModelIndexList indexes = selectedIndexes(); int timeOffset = m_d->dragOffset.x() / m_d->horizontalHeader->defaultSectionSize(); qreal valueOffset = m_d->dragOffset.y() / m_d->verticalHeader->scaleFactor(); KisAnimationCurvesModel *curvesModel = dynamic_cast(model()); curvesModel->adjustKeyframes(indexes, timeOffset, valueOffset); m_d->isDraggingKeyframe = false; m_d->itemDelegate->setSelectedItemVisualOffset(QPointF()); viewport()->update(); } else if (m_d->isAdjustingHandle) { QModelIndex index = currentIndex(); int mode = index.data(KisAnimationCurvesModel::TangentsModeRole).toInt(); m_d->model->beginCommand(kundo2_i18n("Adjust tangent")); if (mode == KisKeyframe::Smooth) { QPointF leftHandle = m_d->itemDelegate->leftHandle(index, true); QPointF rightHandle = m_d->itemDelegate->rightHandle(index, true); QPointF leftTangent = m_d->itemDelegate->unscaledTangent(leftHandle); QPointF rightTangent = m_d->itemDelegate->unscaledTangent(rightHandle); model()->setData(index, leftTangent, KisAnimationCurvesModel::LeftTangentRole); model()->setData(index, rightTangent, KisAnimationCurvesModel::RightTangentRole); } else { if (m_d->adjustedHandle == 0) { QPointF leftHandle = m_d->itemDelegate->leftHandle(index, true); model()->setData(index, m_d->itemDelegate->unscaledTangent(leftHandle), KisAnimationCurvesModel::LeftTangentRole); } else { QPointF rightHandle = m_d->itemDelegate->rightHandle(index, true); model()->setData(index, m_d->itemDelegate->unscaledTangent(rightHandle), KisAnimationCurvesModel::RightTangentRole); } } m_d->model->endCommand(); m_d->isAdjustingHandle = false; m_d->itemDelegate->setHandleAdjustment(QPointF(), m_d->adjustedHandle); } } QAbstractItemView::mouseReleaseEvent(e); } void KisAnimationCurvesView::scrollContentsBy(int dx, int dy) { m_d->horizontalHeader->setOffset(horizontalScrollBar()->value()); m_d->verticalHeader->setOffset(verticalScrollBar()->value()); scrollDirtyRegion(dx, dy); viewport()->scroll(dx, dy); } void KisAnimationCurvesView::updateGeometries() { int topMargin = qMax(m_d->horizontalHeader->minimumHeight(), m_d->horizontalHeader->sizeHint().height()); int leftMargin = m_d->verticalHeader->sizeHint().width(); setViewportMargins(leftMargin, topMargin, 0, 0); QRect viewRect = viewport()->geometry(); m_d->horizontalHeader->setGeometry(leftMargin, 0, viewRect.width(), topMargin); m_d->verticalHeader->setGeometry(0, topMargin, leftMargin, viewRect.height()); horizontalScrollBar()->setRange(0, m_d->horizontalHeader->length() - viewport()->width()); updateVerticalRange(); QAbstractItemView::updateGeometries(); } void KisAnimationCurvesView::slotRowsChanged(const QModelIndex &parentIndex, int first, int last) { Q_UNUSED(parentIndex); Q_UNUSED(first); Q_UNUSED(last); updateVerticalRange(); viewport()->update(); } void KisAnimationCurvesView::slotDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) { updateVerticalRange(); viewport()->update(); } void KisAnimationCurvesView::slotHeaderDataChanged(Qt::Orientation orientation, int first, int last) { viewport()->update(); } void KisAnimationCurvesView::slotHorizontalZoomStarted(qreal staticPoint) { m_d->horizontalZoomStillPointIndex = qIsNaN(staticPoint) ? currentIndex().column() : staticPoint; const int w = m_d->horizontalHeader->defaultSectionSize(); m_d->horizontalZoomStillPointOriginalOffset = w * m_d->horizontalZoomStillPointIndex - horizontalScrollBar()->value(); } void KisAnimationCurvesView::slotHorizontalZoomLevelChanged(qreal zoomLevel) { if (m_d->horizontalHeader->setZoom(zoomLevel)) { const int w = m_d->horizontalHeader->defaultSectionSize(); horizontalScrollBar()->setValue(w * m_d->horizontalZoomStillPointIndex - m_d->horizontalZoomStillPointOriginalOffset); viewport()->update(); } } void KisAnimationCurvesView::slotVerticalZoomStarted(qreal staticPoint) { m_d->verticalZoomStillPoint = qIsNaN(staticPoint) ? 0 : staticPoint; const float scale = m_d->verticalHeader->scaleFactor(); m_d->verticalZoomStillPointOriginalOffset = scale * m_d->verticalZoomStillPoint - m_d->verticalHeader->offset(); } void KisAnimationCurvesView::slotVerticalZoomLevelChanged(qreal zoomLevel) { if (!qFuzzyCompare((float)zoomLevel, m_d->verticalHeader->scaleFactor())) { m_d->verticalHeader->setScale(zoomLevel); m_d->verticalHeader->setOffset(-zoomLevel * m_d->verticalZoomStillPoint - m_d->verticalZoomStillPointOriginalOffset); verticalScrollBar()->setValue(m_d->verticalHeader->offset()); viewport()->update(); } } void KisAnimationCurvesView::applyConstantMode() { m_d->model->beginCommand(kundo2_i18n("Set interpolation mode")); Q_FOREACH(QModelIndex index, selectedIndexes()) { m_d->model->setData(index, KisKeyframe::Constant, KisAnimationCurvesModel::InterpolationModeRole); } m_d->model->endCommand(); } void KisAnimationCurvesView::applyLinearMode() { m_d->model->beginCommand(kundo2_i18n("Set interpolation mode")); Q_FOREACH(QModelIndex index, selectedIndexes()) { m_d->model->setData(index, KisKeyframe::Linear, KisAnimationCurvesModel::InterpolationModeRole); } m_d->model->endCommand(); } void KisAnimationCurvesView::applyBezierMode() { m_d->model->beginCommand(kundo2_i18n("Set interpolation mode")); Q_FOREACH(QModelIndex index, selectedIndexes()) { m_d->model->setData(index, KisKeyframe::Bezier, KisAnimationCurvesModel::InterpolationModeRole); } m_d->model->endCommand(); } void KisAnimationCurvesView::applySmoothMode() { m_d->model->beginCommand(kundo2_i18n("Set interpolation mode")); Q_FOREACH(QModelIndex index, selectedIndexes()) { QVector2D leftVisualTangent(m_d->itemDelegate->leftHandle(index, false)); QVector2D rightVisualTangent(m_d->itemDelegate->rightHandle(index, false)); if (leftVisualTangent.lengthSquared() > 0 && rightVisualTangent.lengthSquared() > 0) { float leftAngle = qAtan2(-leftVisualTangent.y(), -leftVisualTangent.x()); float rightAngle = qAtan2(rightVisualTangent.y(), rightVisualTangent.x()); float angle = (leftAngle + rightAngle) / 2; QVector2D unit(qCos(angle), qSin(angle)); leftVisualTangent = -unit * QVector2D(leftVisualTangent).length(); rightVisualTangent = unit * QVector2D(rightVisualTangent).length(); QPointF leftTangent = m_d->itemDelegate->unscaledTangent(leftVisualTangent.toPointF()); QPointF rightTangent = m_d->itemDelegate->unscaledTangent(rightVisualTangent.toPointF()); model()->setData(index, leftTangent, KisAnimationCurvesModel::LeftTangentRole); model()->setData(index, rightTangent, KisAnimationCurvesModel::RightTangentRole); } model()->setData(index, KisKeyframe::Smooth, KisAnimationCurvesModel::TangentsModeRole); } m_d->model->endCommand(); } void KisAnimationCurvesView::applySharpMode() { m_d->model->beginCommand(kundo2_i18n("Set interpolation mode")); Q_FOREACH(QModelIndex index, selectedIndexes()) { model()->setData(index, KisKeyframe::Sharp, KisAnimationCurvesModel::TangentsModeRole); } m_d->model->endCommand(); } + +void KisAnimationCurvesView::createKeyframe() +{ + QModelIndex active = currentIndex(); + int channel = active.isValid() ? active.row() : 0; + + int time = m_d->model->currentTime(); + QModelIndex index = m_d->model->index(channel, time); + + qreal value = index.data(KisAnimationCurvesModel::ScalarValueRole).toReal(); + m_d->model->setData(index, value, KisAnimationCurvesModel::ScalarValueRole); +} + +void KisAnimationCurvesView::removeKeyframes() +{ + m_d->model->removeFrames(selectedIndexes()); +} diff --git a/plugins/dockers/animation/kis_animation_curves_view.h b/plugins/dockers/animation/kis_animation_curves_view.h index 3e75da8db4..0bba901a77 100644 --- a/plugins/dockers/animation/kis_animation_curves_view.h +++ b/plugins/dockers/animation/kis_animation_curves_view.h @@ -1,89 +1,92 @@ /* * 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_ANIMATION_CURVES_VIEW_H #define _KIS_ANIMATION_CURVES_VIEW_H #include #include class KisAction; class KisZoomButton; class KisAnimationCurvesView : public QAbstractItemView { Q_OBJECT public: KisAnimationCurvesView(QWidget *parent); ~KisAnimationCurvesView(); void setModel(QAbstractItemModel *model); void setZoomButtons(KisZoomButton *horizontal, KisZoomButton *vertical); QRect visualRect(const QModelIndex &index) const; void scrollTo(const QModelIndex &index, ScrollHint hint); QModelIndex indexAt(const QPoint &point) const; protected: void paintEvent(QPaintEvent *); QModelIndex moveCursor(CursorAction cursorAction, Qt::KeyboardModifiers modifiers); int horizontalOffset() const; int verticalOffset() const; bool isIndexHidden(const QModelIndex &index) const; void setSelection(const QRect &rect, QItemSelectionModel::SelectionFlags command); QRegion visualRegionForSelection(const QItemSelection &selection) const; void scrollContentsBy(int dx, int dy); void mousePressEvent(QMouseEvent *); void mouseMoveEvent(QMouseEvent *); void mouseReleaseEvent(QMouseEvent *); public Q_SLOTS: void applyConstantMode(); void applyLinearMode(); void applyBezierMode(); void applySmoothMode(); void applySharpMode(); + void createKeyframe(); + void removeKeyframes(); + protected Q_SLOTS: void updateGeometries(); private Q_SLOTS: void slotRowsChanged(const QModelIndex &parentIndex, int first, int last); void slotDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight); void slotHeaderDataChanged(Qt::Orientation orientation, int first, int last); void slotHorizontalZoomStarted(qreal staticPoint); void slotVerticalZoomStarted(qreal staticPoint); void slotHorizontalZoomLevelChanged(qreal level); void slotVerticalZoomLevelChanged(qreal level); private: struct Private; const QScopedPointer m_d; void paintCurves(QPainter &painter, int firstFrame, int lastFrame); void paintCurve(int channel, int firstFrame, int lastFrame, QPainter &painter); void paintCurveSegment(QPainter &painter, QPointF pos1, QPointF rightTangent, QPointF leftTangent, QPointF pos2); void paintKeyframes(QPainter &painter, int firstFrame, int lastFrame); QModelIndex findNextKeyframeIndex(int channel, int time, int selectionOffset, bool backward); void updateVerticalRange(); }; #endif diff --git a/plugins/dockers/animation/kis_time_based_item_model.cpp b/plugins/dockers/animation/kis_time_based_item_model.cpp index 8e72d6b81b..b60caa53a6 100644 --- a/plugins/dockers/animation/kis_time_based_item_model.cpp +++ b/plugins/dockers/animation/kis_time_based_item_model.cpp @@ -1,398 +1,431 @@ /* * 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" 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.scribbingUpdatesDelay(), 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(sigTimeChanged(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(); + + 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) { bool result = false; if (srcIndexes.isEmpty()) return result; if (offset.isNull()) return result; 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); 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; } updateIndexes << dstIndex; } result = KisAnimationUtils::moveKeyframes(m_d->image, srcFrameItems, dstFrameItems, copyFrames, parentCommand); Q_FOREACH (const QModelIndex &index, updateIndexes) { emit dataChanged(index, index); } return result; } 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 e215c25e3b..6e567615fd 100644 --- a/plugins/dockers/animation/kis_time_based_item_model.h +++ b/plugins/dockers/animation/kis_time_based_item_model.h @@ -1,95 +1,96 @@ /* * 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); - virtual bool removeFrames(const QModelIndexList &indexes) = 0; + bool removeFrames(const QModelIndexList &indexes); bool offsetFrames(QModelIndexList srcIndexes, const QPoint &offset, bool copyFrames, KUndo2Command *parentCommand=0); 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; private Q_SLOTS: - void slotFramerateChanged(); + 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 efa28dc8ef..1826b2a983 100644 --- a/plugins/dockers/animation/timeline_frames_model.cpp +++ b/plugins/dockers/animation/timeline_frames_model.cpp @@ -1,640 +1,608 @@ /* * 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 "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" struct TimelineFramesModel::Private { Private() : activeLayerIndex(0), dummiesFacade(0), needFinishInsertRows(false), needFinishRemoveRows(false), updateTimer(200, KisSignalCompressor::FIRST_INACTIVE), parentOfRemovedNode(0) {} int activeLayerIndex; KisDummiesFacadeBase *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; } 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; return KisAnimationUtils::createKeyframeLazy(image, dummy->node(), KisKeyframeChannel::Content.id(), column, copy); } 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 { return m_d->converter->dummyFromRow(index.row())->node(); } 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->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())); } if (m_d->dummiesFacade != oldDummiesFacade) { reset(); } if (m_d->dummiesFacade) { emit sigInfiniteTimelineUpdateNeeded(); } } 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 Qt::DisplayRole: { return QVariant(); } case Qt::TextAlignmentRole: { return QVariant(Qt::AlignHCenter | Qt::AlignVCenter); } } 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()); } 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->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(); } } } 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; } 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; } - -bool TimelineFramesModel::removeFrames(const QModelIndexList &indexes) -{ - KisAnimationUtils::FrameItemList frameItems; - - Q_FOREACH (const QModelIndex &index, indexes) { - KisNodeDummy *dummy = m_d->converter->dummyFromRow(index.row()); - if (!dummy) continue; - - int time = index.column(); - - QList channels = dummy->node()->keyframeChannels(); - Q_FOREACH(KisKeyframeChannel *channel, channels) { - if (channel->keyframeAt(time)) { - frameItems << KisAnimationUtils::FrameItem(dummy->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; -} diff --git a/plugins/dockers/animation/timeline_frames_model.h b/plugins/dockers/animation/timeline_frames_model.h index a484798b91..336f9fb959 100644 --- a/plugins/dockers/animation/timeline_frames_model.h +++ b/plugins/dockers/animation/timeline_frames_model.h @@ -1,121 +1,120 @@ /* * 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 __TIMELINE_FRAMES_MODEL_H #define __TIMELINE_FRAMES_MODEL_H #include #include #include "kritaanimationdocker_export.h" #include "kis_node_model.h" #include "kis_types.h" #include "kis_node.h" #include "timeline_node_list_keeper.h" class KisNodeDummy; class KisDummiesFacadeBase; class KisAnimationPlayer; class KRITAANIMATIONDOCKER_EXPORT TimelineFramesModel : public TimelineNodeListKeeper::ModelWithExternalNotifications { Q_OBJECT public: TimelineFramesModel(QObject *parent); ~TimelineFramesModel(); bool hasConnectionToCanvas() const; void setDummiesFacade(KisDummiesFacadeBase *dummiesFacade, KisImageSP image); bool canDropFrameData(const QMimeData *data, const QModelIndex &index); bool insertOtherLayer(int index, int dstRow); int activeLayerRow() const; bool createFrame(const QModelIndex &dstIndex); bool copyFrame(const QModelIndex &dstIndex); - bool removeFrames(const QModelIndexList &indexes); void setLastClickedIndex(const QModelIndex &index); int rowCount(const QModelIndex &parent = QModelIndex()) const; QVariant data(const QModelIndex &index, int role) const; bool setData(const QModelIndex &index, const QVariant &value, int role); QVariant headerData(int section, Qt::Orientation orientation, int role) const; bool setHeaderData(int section, Qt::Orientation orientation, const QVariant &value, int role); Qt::DropActions supportedDragActions() const; Qt::DropActions supportedDropActions() const; QStringList mimeTypes() const; QMimeData * mimeData(const QModelIndexList &indexes) const; bool dropMimeData(const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent); Qt::ItemFlags flags(const QModelIndex &index) const; bool insertRows(int row, int count, const QModelIndex &parent); bool removeRows(int row, int count, const QModelIndex &parent); enum ItemDataRole { ActiveLayerRole = KisTimeBasedItemModel::UserRole, TimelinePropertiesRole, OtherLayersRole, LayerUsedInTimelineRole }; // metatype is added by the original implementation typedef KisBaseNode::Property Property; typedef KisBaseNode::PropertyList PropertyList; typedef TimelineNodeListKeeper::OtherLayer OtherLayer; typedef TimelineNodeListKeeper::OtherLayersList OtherLayersList; struct NodeManipulationInterface { virtual ~NodeManipulationInterface() {}; virtual KisLayerSP addPaintLayer() const = 0; virtual void removeNode(KisNodeSP node) const = 0; }; /** * NOTE: the model has an ownership over the interface, that is it'll * be deleted automatically later */ void setNodeManipulationInterface(NodeManipulationInterface *iface); protected: KisNodeSP nodeAt(QModelIndex index) const; QList channelsAt(QModelIndex index) const; private Q_SLOTS: void slotDummyChanged(KisNodeDummy *dummy); void processUpdateQueue(); public Q_SLOTS: void slotCurrentNodeChanged(KisNodeSP node); Q_SIGNALS: void requestCurrentNodeChanged(KisNodeSP node); void sigInfiniteTimelineUpdateNeeded(); private: struct Private; const QScopedPointer m_d; }; #endif /* __TIMELINE_FRAMES_MODEL_H */ diff --git a/plugins/dockers/animation/wdg_animation_curves.ui b/plugins/dockers/animation/wdg_animation_curves.ui index 5840ec8ebe..f669107fdf 100644 --- a/plugins/dockers/animation/wdg_animation_curves.ui +++ b/plugins/dockers/animation/wdg_animation_curves.ui @@ -1,159 +1,192 @@ WdgAnimationCurves 0 0 448 421 Form Zoom time Zoom values + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Add new keyframe + + + + + + + + + + Remove selected keyframes + + + + + + Qt::Horizontal 40 20 Hold value Linear interpolation Custom interpolation Qt::Horizontal QSizePolicy::Fixed 20 20 Sharp node Smooth node Qt::Horizontal 40 20 Qt::Horizontal KisAnimationCurvesView QFrame
kis_animation_curves_view.h
1
KisZoomButton QToolButton
kis_zoom_button.h