diff --git a/src/assets/keyframes/view/keyframeview.cpp b/src/assets/keyframes/view/keyframeview.cpp index 867408fb8..170d68022 100644 --- a/src/assets/keyframes/view/keyframeview.cpp +++ b/src/assets/keyframes/view/keyframeview.cpp @@ -1,344 +1,480 @@ /*************************************************************************** * Copyright (C) 2011 by Till Theato (root@ttill.de) * * Copyright (C) 2017 by Nicolas Carion * * This file is part of Kdenlive (www.kdenlive.org). * * * * Kdenlive 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. * * * * Kdenlive 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 Kdenlive. If not, see . * ***************************************************************************/ #include "keyframeview.hpp" #include "assets/keyframes/model/keyframemodellist.hpp" #include "core.h" #include "kdenlivesettings.h" #include +#include #include #include #include #include KeyframeView::KeyframeView(std::shared_ptr model, int duration, QWidget *parent) : QWidget(parent) , m_model(std::move(model)) , m_duration(duration) , m_position(0) , m_currentKeyframe(-1) , m_currentKeyframeOriginal(-1) , m_hoverKeyframe(-1) , m_scale(1) + , m_zoomHandle(0,1) + , m_hoverZoomIn(false) + , m_hoverZoomOut(false) + , m_hoverZoom(false) + , m_clickOffset(0) { setMouseTracking(true); setMinimumSize(QSize(150, 20)); - setSizePolicy(QSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred)); setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)); QPalette p = palette(); KColorScheme scheme(p.currentColorGroup(), KColorScheme::Window); m_colSelected = palette().highlight().color(); m_colKeyframe = scheme.foreground(KColorScheme::NormalText).color(); - m_size = QFontInfo(font()).pixelSize() * 1.8; - m_lineHeight = m_size / 2; - m_offset = m_lineHeight; + m_size = QFontInfo(font()).pixelSize() * 3; + m_lineHeight = m_size / 2.1; + m_zoomHeight = m_size * 3 / 4; + m_offset = m_size / 4; setFixedHeight(m_size); + setSizePolicy(QSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed)); connect(m_model.get(), &KeyframeModelList::modelChanged, this, &KeyframeView::slotModelChanged); } void KeyframeView::slotModelChanged() { int offset = pCore->getItemIn(m_model->getOwnerId()); emit atKeyframe(m_model->hasKeyframe(m_position + offset), m_model->singleKeyframe()); emit modified(); update(); } void KeyframeView::slotSetPosition(int pos, bool isInRange) { if (!isInRange) { m_position = -1; update(); return; } if (pos != m_position) { m_position = pos; int offset = pCore->getItemIn(m_model->getOwnerId()); emit atKeyframe(m_model->hasKeyframe(pos + offset), m_model->singleKeyframe()); update(); } } void KeyframeView::initKeyframePos() { emit atKeyframe(m_model->hasKeyframe(m_position), m_model->singleKeyframe()); } void KeyframeView::slotAddKeyframe(int pos) { if (pos < 0) { pos = m_position; } int offset = pCore->getItemIn(m_model->getOwnerId()); m_model->addKeyframe(GenTime(size_t(pos + offset), pCore->getCurrentFps()), (KeyframeType)KdenliveSettings::defaultkeyframeinterp()); } void KeyframeView::slotAddRemove() { int offset = pCore->getItemIn(m_model->getOwnerId()); if (m_model->hasKeyframe(m_position + offset)) { slotRemoveKeyframe(m_position); } else { slotAddKeyframe(m_position); } } void KeyframeView::slotEditType(int type, const QPersistentModelIndex &index) { int offset = pCore->getItemIn(m_model->getOwnerId()); if (m_model->hasKeyframe(m_position + offset)) { m_model->updateKeyframeType(GenTime(size_t(m_position + offset), pCore->getCurrentFps()), type, index); } } void KeyframeView::slotRemoveKeyframe(int pos) { if (pos < 0) { pos = m_position; } int offset = pCore->getItemIn(m_model->getOwnerId()); m_model->removeKeyframe(GenTime(size_t(pos + offset), pCore->getCurrentFps())); } void KeyframeView::setDuration(int dur) { m_duration = dur; int offset = pCore->getItemIn(m_model->getOwnerId()); emit atKeyframe(m_model->hasKeyframe(m_position + offset), m_model->singleKeyframe()); update(); } void KeyframeView::slotGoToNext() { if (m_position == m_duration - 1) { return; } bool ok; int offset = pCore->getItemIn(m_model->getOwnerId()); auto next = m_model->getNextKeyframe(GenTime(size_t(m_position + offset), pCore->getCurrentFps()), &ok); if (ok) { emit seekToPos(qMin((int)next.first.frames(pCore->getCurrentFps()) - offset, m_duration - 1)); } else { // no keyframe after current position emit seekToPos(m_duration - 1); } } void KeyframeView::slotGoToPrev() { if (m_position == 0) { return; } bool ok; int offset = pCore->getItemIn(m_model->getOwnerId()); auto prev = m_model->getPrevKeyframe(GenTime(m_position + offset, pCore->getCurrentFps()), &ok); if (ok) { emit seekToPos(qMax(0, (int)prev.first.frames(pCore->getCurrentFps()) - offset)); } else { // no keyframe after current position emit seekToPos(m_duration - 1); } } void KeyframeView::mousePressEvent(QMouseEvent *event) { int offset = pCore->getItemIn(m_model->getOwnerId()); - int pos = (event->x() - m_offset) / m_scale; - if (event->y() < m_lineHeight && event->button() == Qt::LeftButton) { - bool ok; - GenTime position(pos + offset, pCore->getCurrentFps()); - auto keyframe = m_model->getClosestKeyframe(position, &ok); - if (ok && qAbs(keyframe.first.frames(pCore->getCurrentFps()) - pos - offset) * m_scale < ceil(m_lineHeight / 1.5)) { - m_currentKeyframeOriginal = keyframe.first.frames(pCore->getCurrentFps()) - offset; - // Select and seek to keyframe - m_currentKeyframe = m_currentKeyframeOriginal; - emit seekToPos(m_currentKeyframeOriginal); + double zoomStart = m_zoomHandle.x() * (width() - 2 * m_offset); + double zoomEnd = m_zoomHandle.y() * (width() - 2 * m_offset); + double zoomFactor = (width() - 2 * m_offset) / (zoomEnd - zoomStart); + int pos = ((event->x() - m_offset) / zoomFactor + zoomStart ) / m_scale; + pos = qBound(0, pos, m_duration - 1); + if (event->button() == Qt::LeftButton) { + if (event->y() < m_lineHeight) { + // mouse click in keyframes area + bool ok; + GenTime position(pos + offset, pCore->getCurrentFps()); + auto keyframe = m_model->getClosestKeyframe(position, &ok); + if (ok && qAbs(keyframe.first.frames(pCore->getCurrentFps()) - pos - offset) * m_scale < ceil(m_lineHeight / 1.5)) { + m_currentKeyframeOriginal = keyframe.first.frames(pCore->getCurrentFps()) - offset; + // Select and seek to keyframe + m_currentKeyframe = m_currentKeyframeOriginal; + emit seekToPos(m_currentKeyframeOriginal); + return; + } + } else if (event->y() > m_zoomHeight + 2) { + // click on zoom area + if (m_hoverZoom) { + m_clickOffset = ((double) event->x() - m_offset) / (width() - 2 * m_offset); + } + return; + } + } else if (event->button() == Qt::RightButton && event->y() > m_zoomHeight + 2) { + // Right click on zoom, switch between no zoom and last zoom status + if (m_zoomHandle == QPointF(0, 1)) { + if (!m_lastZoomHandle.isNull()) { + m_zoomHandle = m_lastZoomHandle; + update(); + return; + } + } else { + m_lastZoomHandle = m_zoomHandle; + m_zoomHandle = QPointF(0, 1); + update(); return; } } // no keyframe next to mouse m_currentKeyframe = m_currentKeyframeOriginal = -1; emit seekToPos(pos); update(); } void KeyframeView::mouseMoveEvent(QMouseEvent *event) { int offset = pCore->getItemIn(m_model->getOwnerId()); - int pos = qBound(0, (int)((event->x() - m_offset) / m_scale), m_duration - 1); + double zoomStart = m_zoomHandle.x() * (width() - 2 * m_offset); + double zoomEnd = m_zoomHandle.y() * (width() - 2 * m_offset); + double zoomFactor = (width() - 2 * m_offset) / (zoomEnd - zoomStart); + int pos = ((event->x() - m_offset) / zoomFactor + zoomStart ) / m_scale; + pos = qBound(0, pos, m_duration - 1); GenTime position(pos + offset, pCore->getCurrentFps()); if ((event->buttons() & Qt::LeftButton) != 0u) { + if (m_hoverZoomIn || m_hoverZoomOut || m_hoverZoom) { + // Moving zoom handles + if (m_hoverZoomIn) { + m_zoomHandle.setX(qMax(0., (double)(event->x() - m_offset) / (width() - 2 * m_offset))); + update(); + return; + } + if (m_hoverZoomOut) { + m_zoomHandle.setY(qMin(1., (double)(event->x() - m_offset) / (width() - 2 * m_offset))); + update(); + return; + } + // moving zoom zone + if (m_hoverZoom) { + double clickOffset = ((double)event->x() - m_offset) / (width() - 2 * m_offset) - m_clickOffset; + double newX = m_zoomHandle.x() + clickOffset; + if (newX < 0) { + clickOffset = - m_zoomHandle.x(); + newX = 0; + } + double newY = m_zoomHandle.y() + clickOffset; + if (newY > 1) { + clickOffset = 1 - m_zoomHandle.y(); + newY = 1; + newX = m_zoomHandle.x() + clickOffset; + } + m_clickOffset = ((double)event->x() - m_offset) / (width() - 2 * m_offset); + m_zoomHandle = QPointF(newX, newY); + update(); + } + return; + } if (m_currentKeyframe == pos) { return; } if (m_currentKeyframe > 0) { if (!m_model->hasKeyframe(pos + offset)) { GenTime currentPos(m_currentKeyframe + offset, pCore->getCurrentFps()); if (m_model->moveKeyframe(currentPos, position, false)) { m_currentKeyframe = pos; } } } emit seekToPos(pos); return; } if (event->y() < m_lineHeight) { bool ok; auto keyframe = m_model->getClosestKeyframe(position, &ok); if (ok && qAbs(keyframe.first.frames(pCore->getCurrentFps()) - pos - offset) * m_scale < ceil(m_lineHeight / 1.5)) { m_hoverKeyframe = keyframe.first.frames(pCore->getCurrentFps()) - offset; setCursor(Qt::PointingHandCursor); + m_hoverZoomIn = false; + m_hoverZoomOut = false; + m_hoverZoom = false; + update(); + return; + } + } + if (event->y() > m_zoomHeight + 2) { + // Moving in zoom area + if (qAbs(event->x() - m_offset - (m_zoomHandle.x() * (width() - 2 * m_offset))) < QApplication::startDragDistance()) { + setCursor(Qt::SizeHorCursor); + m_hoverZoomIn = true; + m_hoverZoomOut = false; + m_hoverZoom = false; + update(); + return; + } + if (qAbs(event->x() - m_offset - (m_zoomHandle.y() * (width() - 2 * m_offset))) < QApplication::startDragDistance()) { + setCursor(Qt::SizeHorCursor); + m_hoverZoomOut = true; + m_hoverZoomIn = false; + m_hoverZoom = false; + update(); + return; + } + if (m_zoomHandle != QPointF(0, 1) && event->x() > m_offset + (m_zoomHandle.x() * (width() - 2 * m_offset)) && event->x() < m_offset + (m_zoomHandle.y() * (width() - 2 * m_offset))) { + setCursor(Qt::PointingHandCursor); + m_hoverZoom = true; + m_hoverZoomIn = false; + m_hoverZoomOut = false; update(); return; } } - if (m_hoverKeyframe != -1) { + if (m_hoverKeyframe != -1 || m_hoverZoomOut || m_hoverZoomIn || m_hoverZoom) { m_hoverKeyframe = -1; + m_hoverZoomOut = false; + m_hoverZoomIn = false; + m_hoverZoom = false; setCursor(Qt::ArrowCursor); update(); } } void KeyframeView::mouseReleaseEvent(QMouseEvent *event) { Q_UNUSED(event) if (m_currentKeyframe >= 0 && m_currentKeyframeOriginal != m_currentKeyframe) { int offset = pCore->getItemIn(m_model->getOwnerId()); GenTime initPos(m_currentKeyframeOriginal + offset, pCore->getCurrentFps()); GenTime targetPos(m_currentKeyframe + offset, pCore->getCurrentFps()); bool ok1 = m_model->moveKeyframe(targetPos, initPos, false); bool ok2 = m_model->moveKeyframe(initPos, targetPos, true); qDebug() << "RELEASING keyframe move" << ok1 << ok2 << initPos.frames(pCore->getCurrentFps()) << targetPos.frames(pCore->getCurrentFps()); } } void KeyframeView::mouseDoubleClickEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton && event->y() < m_lineHeight) { - int pos = qBound(0, (int)((event->x() - m_offset) / m_scale), m_duration - 1); int offset = pCore->getItemIn(m_model->getOwnerId()); + double zoomStart = m_zoomHandle.x() * (width() - 2 * m_offset); + double zoomEnd = m_zoomHandle.y() * (width() - 2 * m_offset); + double zoomFactor = (width() - 2 * m_offset) / (zoomEnd - zoomStart); + int pos = ((event->x() - m_offset) / zoomFactor + zoomStart ) / m_scale; + pos = qBound(0, pos, m_duration - 1); GenTime position(pos + offset, pCore->getCurrentFps()); bool ok; auto keyframe = m_model->getClosestKeyframe(position, &ok); if (ok && qAbs(keyframe.first.frames(pCore->getCurrentFps()) - pos - offset) * m_scale < ceil(m_lineHeight / 1.5)) { if (keyframe.first.frames(pCore->getCurrentFps()) != offset) { m_model->removeKeyframe(keyframe.first); if (keyframe.first.frames(pCore->getCurrentFps()) == m_currentKeyframe + offset) { m_currentKeyframe = m_currentKeyframeOriginal = -1; } if (keyframe.first.frames(pCore->getCurrentFps()) == m_position + offset) { emit atKeyframe(false, m_model->singleKeyframe()); } } return; } // add new keyframe m_model->addKeyframe(position, (KeyframeType)KdenliveSettings::defaultkeyframeinterp()); } else { QWidget::mouseDoubleClickEvent(event); } } void KeyframeView::wheelEvent(QWheelEvent *event) { if (event->modifiers() & Qt::AltModifier) { if (event->angleDelta().y() > 0) { slotGoToPrev(); } else { slotGoToNext(); } return; } int change = event->angleDelta().y() > 0 ? -1 : 1; int pos = qBound(0, m_position + change, m_duration - 1); emit seekToPos(pos); } void KeyframeView::paintEvent(QPaintEvent *event) { Q_UNUSED(event) QStylePainter p(this); m_scale = (width() - 2 * m_offset) / (double)(m_duration - 1); // p.translate(0, m_lineHeight); - int headOffset = m_lineHeight / 1.5; + int headOffset = m_lineHeight / 1.6; int offset = pCore->getItemIn(m_model->getOwnerId()); + double zoomStart = m_zoomHandle.x() * (width() - 2 * m_offset); + double zoomEnd = m_zoomHandle.y() * (width() - 2 * m_offset); + double zoomFactor = (width() - 2 * m_offset) / (zoomEnd - zoomStart); /* * keyframes */ for (const auto &keyframe : *m_model.get()) { int pos = keyframe.first.frames(pCore->getCurrentFps()) - offset; if (pos < 0) continue; if (pos == m_currentKeyframe || pos == m_hoverKeyframe) { p.setBrush(m_colSelected); } else { p.setBrush(m_colKeyframe); } - double scaledPos = m_offset + (pos * m_scale); - p.drawLine(QPointF(scaledPos, headOffset), QPointF(scaledPos, m_lineHeight + headOffset / 2.0)); + double scaledPos = pos * m_scale; + if (scaledPos < zoomStart || scaledPos > zoomEnd) { + continue; + } + scaledPos -= zoomStart; + scaledPos *= zoomFactor; + scaledPos += m_offset; + p.drawLine(QPointF(scaledPos, headOffset), QPointF(scaledPos, m_lineHeight - 1)); switch (keyframe.second.first) { case KeyframeType::Linear: { QPolygonF position = QPolygonF() << QPointF(-headOffset / 2.0, headOffset / 2.0) << QPointF(0, 0) << QPointF(headOffset / 2.0, headOffset / 2.0) << QPointF(0, headOffset); position.translate(scaledPos, 0); p.drawPolygon(position); break; } case KeyframeType::Discrete: p.drawRect(QRectF(scaledPos - headOffset / 2.0, 0, headOffset, headOffset)); break; default: p.drawEllipse(QRectF(scaledPos - headOffset / 2.0, 0, headOffset, headOffset)); break; } } p.setPen(palette().dark().color()); - + /* * Time-"line" */ p.setPen(m_colKeyframe); - p.drawLine(m_offset, m_lineHeight + (headOffset / 2), width() - m_offset, m_lineHeight + (headOffset / 2)); + p.drawLine(m_offset, m_lineHeight, width() - m_offset, m_lineHeight); + p.drawLine(m_offset, m_lineHeight - headOffset / 2 + 2, m_offset, m_lineHeight + headOffset / 2 + 2); + p.drawLine(width() - m_offset, m_lineHeight - headOffset / 2 + 2, width() - m_offset, m_lineHeight + headOffset / 2 + 2); /* - * current position + * current position cursor */ if (m_position >= 0 && m_position < m_duration) { - QPolygon pa(3); - int cursorwidth = (m_size - (m_lineHeight + headOffset / 2)) / 2 + 1; - QPolygonF position = QPolygonF() << QPointF(-cursorwidth, m_size) << QPointF(cursorwidth, m_size) << QPointF(0, m_lineHeight + (headOffset / 2) + 1); - position.translate(m_offset + (m_position * m_scale), 0); - p.setBrush(m_colKeyframe); - p.drawPolygon(position); + double scaledPos = m_position * m_scale; + if (scaledPos >= zoomStart && scaledPos <= zoomEnd) { + scaledPos -= zoomStart; + scaledPos *= zoomFactor; + scaledPos += m_offset; + QPolygon pa(3); + int cursorwidth = (m_zoomHeight - m_lineHeight) / 1.5; + QPolygonF position = QPolygonF() << QPointF(-cursorwidth, m_zoomHeight - 2) << QPointF(cursorwidth, m_zoomHeight - 2) << QPointF(0, m_lineHeight + 1); + position.translate(scaledPos, 0); + p.setBrush(m_colKeyframe); + p.drawPolygon(position); + } + } + + // Zoom bar + p.setPen(Qt::NoPen); + p.setBrush(palette().mid()); + p.drawRoundedRect(m_offset, m_zoomHeight + 3, width() - 2 * m_offset, m_size - m_zoomHeight - 3, m_lineHeight / 5, m_lineHeight / 5); + p.setBrush(palette().highlight()); + p.drawRoundedRect(m_offset + (width() - m_offset) * m_zoomHandle.x(), m_zoomHeight + 3, (width() - 2 * m_offset) * (m_zoomHandle.y() - m_zoomHandle.x()), m_size - m_zoomHeight - 3, m_lineHeight / 5, m_lineHeight / 5); + + // Zoom handles + if (m_hoverZoomIn) { + p.fillRect(m_offset + (width() - m_offset) * m_zoomHandle.x() - 1, m_zoomHeight + 3, 2, m_size - m_zoomHeight - 3, + Qt::green); + } else if (m_hoverZoomOut) { + p.fillRect((width() - m_offset) * m_zoomHandle.y() - 1, m_zoomHeight + 3, 2, m_size - m_zoomHeight - 3, Qt::red); } - p.setOpacity(0.5); - p.drawLine(m_offset, m_lineHeight, m_offset, m_lineHeight + headOffset); - p.drawLine(width() - m_offset, m_lineHeight, width() - m_offset, m_lineHeight + headOffset); } diff --git a/src/assets/keyframes/view/keyframeview.hpp b/src/assets/keyframes/view/keyframeview.hpp index e79237617..14a08d4de 100644 --- a/src/assets/keyframes/view/keyframeview.hpp +++ b/src/assets/keyframes/view/keyframeview.hpp @@ -1,90 +1,102 @@ /*************************************************************************** * Copyright (C) 2011 by Till Theato (root@ttill.de) * * Copyright (C) 2017 by Nicolas Carion * * This file is part of Kdenlive (www.kdenlive.org). * * * * Kdenlive 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. * * * * Kdenlive 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 Kdenlive. If not, see . * ***************************************************************************/ #ifndef KEYFRAMEVIEW2_H #define KEYFRAMEVIEW2_H #include "assets/keyframes/model/keyframemodel.hpp" #include "assets/keyframes/model/keyframemodellist.hpp" #include #include class KeyframeModelList; class KeyframeView : public QWidget { Q_OBJECT public: explicit KeyframeView(std::shared_ptr model, int duration, QWidget *parent = nullptr); void setDuration(int dur); public slots: /* @brief moves the current position*/ void slotSetPosition(int pos, bool isInRange); /* @brief remove the keyframe at given position If pos is negative, we remove keyframe at current position */ void slotRemoveKeyframe(int pos); /* @brief Add a keyframe with given parameter value at given pos. If pos is negative, then keyframe is added at current position */ void slotAddKeyframe(int pos = -1); /* @brief If there is a keyframe at current position, it is removed. Otherwise, we add a new one with given value. */ void slotAddRemove(); void slotGoToNext(); void slotGoToPrev(); void slotModelChanged(); void slotEditType(int type, const QPersistentModelIndex &index); /* @brief Emit initial info for monitor. */ void initKeyframePos(); protected: void paintEvent(QPaintEvent *event) override; void mousePressEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override; void mouseDoubleClickEvent(QMouseEvent *event) override; void wheelEvent(QWheelEvent *event) override; private: std::shared_ptr m_model; int m_duration; int m_position; int m_currentKeyframe; int m_currentKeyframeOriginal; int m_hoverKeyframe; int m_lineHeight; + int m_zoomHeight; int m_offset; double m_scale; + /** @brief The zoom factor (start, end - between 0 and 1) */ + QPointF m_zoomHandle; + QPointF m_lastZoomHandle; + /** @brief Mouse is the zoom left handle */ + bool m_hoverZoomIn; + /** @brief Mouse is the zoom right handle */ + bool m_hoverZoomOut; + /** @brief Mouse is over the zoom bar */ + bool m_hoverZoom; + /** @brief the x click position offset for moving zoom */ + double m_clickOffset; int m_size; QColor m_colSelected; QColor m_colKeyframe; QColor m_colKeyframeBg; signals: void seekToPos(int pos); void atKeyframe(bool isKeyframe, bool singleKeyframe); void modified(); }; #endif diff --git a/src/assets/view/widgets/keyframewidget.cpp b/src/assets/view/widgets/keyframewidget.cpp index 1d56b187b..186328ed8 100644 --- a/src/assets/view/widgets/keyframewidget.cpp +++ b/src/assets/view/widgets/keyframewidget.cpp @@ -1,499 +1,502 @@ /*************************************************************************** * Copyright (C) 2011 by Till Theato (root@ttill.de) * * Copyright (C) 2017 by Nicolas Carion * * This file is part of Kdenlive (www.kdenlive.org). * * * * Kdenlive 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. * * * * Kdenlive 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 Kdenlive. If not, see . * ***************************************************************************/ #include "keyframewidget.hpp" #include "assets/keyframes/model/corners/cornershelper.hpp" #include "assets/keyframes/model/keyframemodellist.hpp" #include "assets/keyframes/model/rotoscoping/rotohelper.hpp" #include "assets/keyframes/view/keyframeview.hpp" #include "assets/model/assetparametermodel.hpp" #include "assets/view/widgets/keyframeimport.h" #include "core.h" #include "kdenlivesettings.h" #include "monitor/monitor.h" #include "timecode.h" #include "timecodedisplay.h" #include "widgets/doublewidget.h" #include "widgets/geometrywidget.h" #include #include #include #include #include #include #include #include #include #include KeyframeWidget::KeyframeWidget(std::shared_ptr model, QModelIndex index, QSize frameSize, QWidget *parent) : AbstractParamWidget(std::move(model), index, parent) , m_monitorHelper(nullptr) , m_neededScene(MonitorSceneType::MonitorSceneDefault) , m_sourceFrameSize(frameSize.isValid() && !frameSize.isNull() ? frameSize : pCore->getCurrentFrameSize()) , m_baseHeight(0) , m_addedHeight(0) { setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); m_lay = new QVBoxLayout(this); - m_lay->setContentsMargins(2, 2, 2, 0); m_lay->setSpacing(0); bool ok = false; int duration = m_model->data(m_index, AssetParameterModel::ParentDurationRole).toInt(&ok); Q_ASSERT(ok); m_model->prepareKeyframes(); m_keyframes = m_model->getKeyframeModel(); m_keyframeview = new KeyframeView(m_keyframes, duration, this); m_buttonAddDelete = new QToolButton(this); m_buttonAddDelete->setAutoRaise(true); m_buttonAddDelete->setIcon(QIcon::fromTheme(QStringLiteral("list-add"))); m_buttonAddDelete->setToolTip(i18n("Add keyframe")); m_buttonPrevious = new QToolButton(this); m_buttonPrevious->setAutoRaise(true); m_buttonPrevious->setIcon(QIcon::fromTheme(QStringLiteral("media-skip-backward"))); m_buttonPrevious->setToolTip(i18n("Go to previous keyframe")); m_buttonNext = new QToolButton(this); m_buttonNext->setAutoRaise(true); m_buttonNext->setIcon(QIcon::fromTheme(QStringLiteral("media-skip-forward"))); m_buttonNext->setToolTip(i18n("Go to next keyframe")); // Keyframe type widget m_selectType = new KSelectAction(QIcon::fromTheme(QStringLiteral("keyframes")), i18n("Keyframe interpolation"), this); QAction *linear = new QAction(QIcon::fromTheme(QStringLiteral("linear")), i18n("Linear"), this); linear->setData((int)mlt_keyframe_linear); linear->setCheckable(true); m_selectType->addAction(linear); QAction *discrete = new QAction(QIcon::fromTheme(QStringLiteral("discrete")), i18n("Discrete"), this); discrete->setData((int)mlt_keyframe_discrete); discrete->setCheckable(true); m_selectType->addAction(discrete); QAction *curve = new QAction(QIcon::fromTheme(QStringLiteral("smooth")), i18n("Smooth"), this); curve->setData((int)mlt_keyframe_smooth); curve->setCheckable(true); m_selectType->addAction(curve); m_selectType->setCurrentAction(linear); connect(m_selectType, static_cast(&KSelectAction::triggered), this, &KeyframeWidget::slotEditKeyframeType); m_selectType->setToolBarMode(KSelectAction::ComboBoxMode); m_toolbar = new QToolBar(this); Monitor *monitor = pCore->getMonitor(m_model->monitorId); m_time = new TimecodeDisplay(monitor->timecode(), this); m_time->setRange(0, duration - 1); m_toolbar->addWidget(m_buttonPrevious); m_toolbar->addWidget(m_buttonAddDelete); m_toolbar->addWidget(m_buttonNext); m_toolbar->addAction(m_selectType); // copy/paste keyframes from clipboard QAction *copy = new QAction(i18n("Copy keyframes to clipboard"), this); connect(copy, &QAction::triggered, this, &KeyframeWidget::slotCopyKeyframes); QAction *paste = new QAction(i18n("Import keyframes from clipboard"), this); connect(paste, &QAction::triggered, this, &KeyframeWidget::slotImportKeyframes); // Remove keyframes QAction *removeNext = new QAction(i18n("Remove all keyframes after cursor"), this); connect(removeNext, &QAction::triggered, this, &KeyframeWidget::slotRemoveNextKeyframes); // Default kf interpolation KSelectAction *kfType = new KSelectAction(i18n("Default keyframe type"), this); QAction *discrete2 = new QAction(QIcon::fromTheme(QStringLiteral("discrete")), i18n("Discrete"), this); discrete2->setData((int)mlt_keyframe_discrete); discrete2->setCheckable(true); kfType->addAction(discrete2); QAction *linear2 = new QAction(QIcon::fromTheme(QStringLiteral("linear")), i18n("Linear"), this); linear2->setData((int)mlt_keyframe_linear); linear2->setCheckable(true); kfType->addAction(linear2); QAction *curve2 = new QAction(QIcon::fromTheme(QStringLiteral("smooth")), i18n("Smooth"), this); curve2->setData((int)mlt_keyframe_smooth); curve2->setCheckable(true); kfType->addAction(curve2); switch (KdenliveSettings::defaultkeyframeinterp()) { case mlt_keyframe_discrete: kfType->setCurrentAction(discrete2); break; case mlt_keyframe_smooth: kfType->setCurrentAction(curve2); break; default: kfType->setCurrentAction(linear2); break; } connect(kfType, static_cast(&KSelectAction::triggered), [&](QAction *ac) { KdenliveSettings::setDefaultkeyframeinterp(ac->data().toInt()); }); auto *container = new QMenu(this); container->addAction(copy); container->addAction(paste); container->addSeparator(); container->addAction(kfType); container->addAction(removeNext); // Menu toolbutton auto *menuButton = new QToolButton(this); menuButton->setIcon(QIcon::fromTheme(QStringLiteral("kdenlive-menu"))); menuButton->setToolTip(i18n("Options")); menuButton->setMenu(container); menuButton->setPopupMode(QToolButton::InstantPopup); m_toolbar->addWidget(menuButton); m_toolbar->addWidget(m_time); m_lay->addWidget(m_keyframeview); m_lay->addWidget(m_toolbar); monitorSeek(monitor->position()); connect(m_time, &TimecodeDisplay::timeCodeEditingFinished, [&]() { slotSetPosition(-1, true); }); connect(m_keyframeview, &KeyframeView::seekToPos, [&](int p) { slotSetPosition(p, true); }); connect(m_keyframeview, &KeyframeView::atKeyframe, this, &KeyframeWidget::slotAtKeyframe); connect(m_keyframeview, &KeyframeView::modified, this, &KeyframeWidget::slotRefreshParams); connect(m_buttonAddDelete, &QAbstractButton::pressed, m_keyframeview, &KeyframeView::slotAddRemove); connect(m_buttonPrevious, &QAbstractButton::pressed, m_keyframeview, &KeyframeView::slotGoToPrev); connect(m_buttonNext, &QAbstractButton::pressed, m_keyframeview, &KeyframeView::slotGoToNext); - m_baseHeight = m_keyframeview->minimumHeight() + m_toolbar->sizeHint().height() + 2; + //m_baseHeight = m_keyframeview->height() + m_selectType->defaultWidget()->sizeHint().height(); + int tm = 0; + int bm = 0; + m_lay->getContentsMargins(nullptr, &tm, nullptr, &bm); + m_baseHeight = m_keyframeview->height() + m_toolbar->sizeHint().height() + 2 + tm + bm; setFixedHeight(m_baseHeight); addParameter(index); connect(monitor, &Monitor::seekToNextKeyframe, m_keyframeview, &KeyframeView::slotGoToNext, Qt::UniqueConnection); connect(monitor, &Monitor::seekToPreviousKeyframe, m_keyframeview, &KeyframeView::slotGoToPrev, Qt::UniqueConnection); connect(monitor, &Monitor::addRemoveKeyframe, m_keyframeview, &KeyframeView::slotAddRemove, Qt::UniqueConnection); } KeyframeWidget::~KeyframeWidget() { delete m_keyframeview; delete m_buttonAddDelete; delete m_buttonPrevious; delete m_buttonNext; delete m_time; } void KeyframeWidget::monitorSeek(int pos) { int in = pCore->getItemPosition(m_model->getOwnerId()); int out = in + pCore->getItemDuration(m_model->getOwnerId()); bool isInRange = pos >= in && pos < out; m_buttonAddDelete->setEnabled(isInRange && pos > in); connectMonitor(isInRange); int framePos = qBound(in, pos, out) - in; if (isInRange && framePos != m_time->getValue()) { slotSetPosition(framePos, false); } } void KeyframeWidget::slotEditKeyframeType(QAction *action) { int type = action->data().toInt(); m_keyframeview->slotEditType(type, m_index); } void KeyframeWidget::slotRefreshParams() { int pos = getPosition(); KeyframeType keyType = m_keyframes->keyframeType(GenTime(pos, pCore->getCurrentFps())); int i = 0; while (auto ac = m_selectType->action(i)) { if (ac->data().toInt() == (int)keyType) { m_selectType->setCurrentItem(i); break; } i++; } for (const auto &w : m_parameters) { auto type = m_model->data(w.first, AssetParameterModel::TypeRole).value(); if (type == ParamType::KeyframeParam) { ((DoubleWidget *)w.second)->setValue(m_keyframes->getInterpolatedValue(pos, w.first).toDouble()); } else if (type == ParamType::AnimatedRect) { const QString val = m_keyframes->getInterpolatedValue(pos, w.first).toString(); const QStringList vals = val.split(QLatin1Char(' ')); QRect rect; double opacity = -1; if (vals.count() >= 4) { rect = QRect(vals.at(0).toInt(), vals.at(1).toInt(), vals.at(2).toInt(), vals.at(3).toInt()); if (vals.count() > 4) { QLocale locale; opacity = locale.toDouble(vals.at(4)); } } ((GeometryWidget *)w.second)->setValue(rect, opacity); } } if (m_monitorHelper) { m_monitorHelper->refreshParams(pos); return; } } void KeyframeWidget::slotSetPosition(int pos, bool update) { if (pos < 0) { pos = m_time->getValue(); m_keyframeview->slotSetPosition(pos, true); } else { m_time->setValue(pos); m_keyframeview->slotSetPosition(pos, true); } m_buttonAddDelete->setEnabled(pos > 0); slotRefreshParams(); if (update) { emit seekToPos(pos); } } int KeyframeWidget::getPosition() const { return m_time->getValue() + pCore->getItemIn(m_model->getOwnerId()); } void KeyframeWidget::addKeyframe(int pos) { blockSignals(true); m_keyframeview->slotAddKeyframe(pos); blockSignals(false); setEnabled(true); } void KeyframeWidget::updateTimecodeFormat() { m_time->slotUpdateTimeCodeFormat(); } void KeyframeWidget::slotAtKeyframe(bool atKeyframe, bool singleKeyframe) { if (atKeyframe) { m_buttonAddDelete->setIcon(QIcon::fromTheme(QStringLiteral("list-remove"))); m_buttonAddDelete->setToolTip(i18n("Delete keyframe")); } else { m_buttonAddDelete->setIcon(QIcon::fromTheme(QStringLiteral("list-add"))); m_buttonAddDelete->setToolTip(i18n("Add keyframe")); } pCore->getMonitor(m_model->monitorId)->setEffectKeyframe(atKeyframe || singleKeyframe); m_selectType->setEnabled(atKeyframe || singleKeyframe); for (const auto &w : m_parameters) { w.second->setEnabled(atKeyframe || singleKeyframe); } } void KeyframeWidget::slotRefresh() { // update duration bool ok = false; int duration = m_model->data(m_index, AssetParameterModel::ParentDurationRole).toInt(&ok); Q_ASSERT(ok); // m_model->dataChanged(QModelIndex(), QModelIndex()); //->getKeyframeModel()->getKeyModel(m_index)->dataChanged(QModelIndex(), QModelIndex()); m_keyframeview->setDuration(duration); m_time->setRange(0, duration - 1); slotRefreshParams(); } void KeyframeWidget::resetKeyframes() { // update duration bool ok = false; int duration = m_model->data(m_index, AssetParameterModel::ParentDurationRole).toInt(&ok); Q_ASSERT(ok); // reset keyframes m_keyframes->refresh(); // m_model->dataChanged(QModelIndex(), QModelIndex()); m_keyframeview->setDuration(duration); m_time->setRange(0, duration - 1); slotRefreshParams(); } void KeyframeWidget::addParameter(const QPersistentModelIndex &index) { QLocale locale; locale.setNumberOptions(QLocale::OmitGroupSeparator); // Retrieve parameters from the model QString name = m_model->data(index, Qt::DisplayRole).toString(); QString comment = m_model->data(index, AssetParameterModel::CommentRole).toString(); QString suffix = m_model->data(index, AssetParameterModel::SuffixRole).toString(); auto type = m_model->data(index, AssetParameterModel::TypeRole).value(); // Construct object QWidget *paramWidget = nullptr; if (type == ParamType::AnimatedRect) { m_neededScene = MonitorSceneType::MonitorSceneGeometry; int inPos = m_model->data(index, AssetParameterModel::ParentInRole).toInt(); QPair range(inPos, inPos + m_model->data(index, AssetParameterModel::ParentDurationRole).toInt()); const QString value = m_keyframes->getInterpolatedValue(getPosition(), index).toString(); QRect rect; double opacity = 0; QStringList vals = value.split(QLatin1Char(' ')); if (vals.count() >= 4) { rect = QRect(vals.at(0).toInt(), vals.at(1).toInt(), vals.at(2).toInt(), vals.at(3).toInt()); if (vals.count() > 4) { opacity = locale.toDouble(vals.at(4)); } } // qtblend uses an opacity value in the (0-1) range, while older geometry effects use (0-100) bool integerOpacity = m_model->getAssetId() != QLatin1String("qtblend"); GeometryWidget *geomWidget = new GeometryWidget(pCore->getMonitor(m_model->monitorId), range, rect, opacity, m_sourceFrameSize, false, m_model->data(m_index, AssetParameterModel::OpacityRole).toBool(), integerOpacity, this); connect(geomWidget, &GeometryWidget::valueChanged, [this, index](const QString v) { m_keyframes->updateKeyframe(GenTime(getPosition(), pCore->getCurrentFps()), QVariant(v), index); }); paramWidget = geomWidget; } else if (type == ParamType::Roto_spline) { m_monitorHelper = new RotoHelper(pCore->getMonitor(m_model->monitorId), m_model, index, this); connect(m_monitorHelper, &KeyframeMonitorHelper::updateKeyframeData, this, &KeyframeWidget::slotUpdateKeyframesFromMonitor, Qt::UniqueConnection); m_neededScene = MonitorSceneType::MonitorSceneRoto; } else { if (m_model->getAssetId() == QLatin1String("frei0r.c0rners")) { if (m_neededScene == MonitorSceneDefault && !m_monitorHelper) { m_neededScene = MonitorSceneType::MonitorSceneCorners; m_monitorHelper = new CornersHelper(pCore->getMonitor(m_model->monitorId), m_model, index, this); connect(m_monitorHelper, &KeyframeMonitorHelper::updateKeyframeData, this, &KeyframeWidget::slotUpdateKeyframesFromMonitor, Qt::UniqueConnection); connect(this, &KeyframeWidget::addIndex, m_monitorHelper, &CornersHelper::addIndex); } else { if (type == ParamType::KeyframeParam) { int paramName = m_model->data(index, AssetParameterModel::NameRole).toInt(); if (paramName < 8) { emit addIndex(index); } } } } double value = m_keyframes->getInterpolatedValue(getPosition(), index).toDouble(); double min = locale.toDouble(m_model->data(index, AssetParameterModel::MinRole).toString()); double max = locale.toDouble(m_model->data(index, AssetParameterModel::MaxRole).toString()); double defaultValue = m_model->data(index, AssetParameterModel::DefaultRole).toDouble(); int decimals = m_model->data(index, AssetParameterModel::DecimalsRole).toInt(); double factor = locale.toDouble(m_model->data(index, AssetParameterModel::FactorRole).toString()); factor = qFuzzyIsNull(factor) ? 1 : factor; auto doubleWidget = new DoubleWidget(name, value, min, max, factor, defaultValue, comment, -1, suffix, decimals, m_model->data(index, AssetParameterModel::OddRole).toBool(), this); connect(doubleWidget, &DoubleWidget::valueChanged, [this, index](double v) { m_keyframes->updateKeyframe(GenTime(getPosition(), pCore->getCurrentFps()), QVariant(v), index); }); paramWidget = doubleWidget; } if (paramWidget) { m_parameters[index] = paramWidget; m_lay->addWidget(paramWidget); m_addedHeight += paramWidget->minimumHeight(); setFixedHeight(m_baseHeight + m_addedHeight); } } void KeyframeWidget::slotInitMonitor(bool active) { Monitor *monitor = pCore->getMonitor(m_model->monitorId); if (m_keyframeview) { m_keyframeview->initKeyframePos(); connect(monitor, &Monitor::updateScene, m_keyframeview, &KeyframeView::slotModelChanged, Qt::UniqueConnection); } connectMonitor(active); if (active) { connect(monitor, &Monitor::seekPosition, this, &KeyframeWidget::monitorSeek, Qt::UniqueConnection); } else { disconnect(monitor, &Monitor::seekPosition, this, &KeyframeWidget::monitorSeek); } } void KeyframeWidget::connectMonitor(bool active) { if (m_monitorHelper) { if (m_monitorHelper->connectMonitor(active)) { slotRefreshParams(); } } for (const auto &w : m_parameters) { auto type = m_model->data(w.first, AssetParameterModel::TypeRole).value(); if (type == ParamType::AnimatedRect) { ((GeometryWidget *)w.second)->connectMonitor(active); break; } } } void KeyframeWidget::slotUpdateKeyframesFromMonitor(const QPersistentModelIndex &index, const QVariant &res) { if (m_keyframes->isEmpty()) { // Always ensure first keyframe is at clip start GenTime pos(pCore->getItemIn(m_model->getOwnerId()), pCore->getCurrentFps()); m_keyframes->addKeyframe(pos, KeyframeType::Linear); m_keyframes->updateKeyframe(pos, res, index); } else if (m_keyframes->hasKeyframe(getPosition()) || m_keyframes->singleKeyframe()) { GenTime pos(getPosition(), pCore->getCurrentFps()); m_keyframes->updateKeyframe(pos, res, index); } } MonitorSceneType KeyframeWidget::requiredScene() const { qDebug() << "// // // RESULTING REQUIRED SCENE: " << m_neededScene; return m_neededScene; } bool KeyframeWidget::keyframesVisible() const { return m_keyframeview->isVisible(); } void KeyframeWidget::showKeyframes(bool enable) { if (enable && m_toolbar->isVisible()) { return; } m_toolbar->setVisible(enable); m_keyframeview->setVisible(enable); setFixedHeight(m_addedHeight + (enable ? m_baseHeight : 0)); } void KeyframeWidget::slotCopyKeyframes() { QJsonDocument effectDoc = m_model->toJson(false); if (effectDoc.isEmpty()) { return; } QClipboard *clipboard = QApplication::clipboard(); clipboard->setText(QString(effectDoc.toJson())); } void KeyframeWidget::slotImportKeyframes() { QClipboard *clipboard = QApplication::clipboard(); QString values = clipboard->text(); QList indexes; for (const auto &w : m_parameters) { indexes << w.first; } QPointer import = new KeyframeImport(values, m_model, indexes, this); if (import->exec() != QDialog::Accepted) { delete import; return; } import->importSelectedData(); /*m_model->getKeyframeModel()->getKeyModel()->dataChanged(QModelIndex(), QModelIndex());*/ /*m_model->modelChanged(); qDebug()<<"//// UPDATING KEYFRAMES CORE---------"; pCore->updateItemKeyframes(m_model->getOwnerId());*/ delete import; } void KeyframeWidget::slotRemoveNextKeyframes() { int pos = m_time->getValue() + m_model->data(m_index, AssetParameterModel::ParentInRole).toInt(); m_keyframes->removeNextKeyframes(GenTime(pos, pCore->getCurrentFps())); }