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()));
}