Warning, file /multimedia/kdenlive/src/dialogs/timeremap.cpp was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).

0001 /*
0002     SPDX-FileCopyrightText: 2021 Jean-Baptiste Mardelle <jb@kdenlive.org>
0003 
0004 SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005 */
0006 
0007 #include "timeremap.h"
0008 
0009 #include "bin/projectclip.h"
0010 #include "core.h"
0011 #include "doc/kthumb.h"
0012 #include "kdenlivesettings.h"
0013 #include "macros.hpp"
0014 #include "mainwindow.h"
0015 #include "monitor/monitor.h"
0016 #include "profiles/profilemodel.hpp"
0017 #include "project/projectmanager.h"
0018 #include "timeline2/model/clipmodel.hpp"
0019 #include "timeline2/model/groupsmodel.hpp"
0020 #include "timeline2/view/timelinecontroller.h"
0021 #include "timeline2/view/timelinewidget.h"
0022 #include "widgets/timecodedisplay.h"
0023 
0024 #include "kdenlive_debug.h"
0025 #include <QFontDatabase>
0026 #include <QStylePainter>
0027 #include <QWheelEvent>
0028 #include <QtMath>
0029 
0030 #include "klocalizedstring.h"
0031 #include <KColorScheme>
0032 
0033 RemapView::RemapView(QWidget *parent)
0034     : QWidget(parent)
0035     , m_inFrame(0)
0036     , m_duration(1)
0037     , m_position(0)
0038     , m_bottomPosition(0)
0039     , m_scale(1.)
0040     , m_zoomFactor(1)
0041     , m_zoomStart(0)
0042     , m_zoomHandle(0, 1)
0043     , m_clip(nullptr)
0044     , m_service(nullptr)
0045     , m_moveKeyframeMode(NoMove)
0046     , m_clickPoint(-1)
0047     , m_moveNext(true)
0048 {
0049     setMouseTracking(true);
0050     setMinimumSize(QSize(150, 80));
0051     setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont));
0052     int size = QFontInfo(font()).pixelSize() * 3;
0053     setFixedHeight(size * 4);
0054     // Reference height of the rulers
0055     m_lineHeight = int(size / 2.);
0056     // Height of the zoom bar
0057     m_zoomHeight = m_lineHeight * 0.5;
0058     // Center of the view
0059     m_centerPos = (size * 4 - m_zoomHeight - 2) / 2 - 1;
0060     m_offset = qCeil(m_lineHeight / 4);
0061     // Bottom of the view (just above zoombar)
0062     m_bottomView = height() - m_zoomHeight - 2;
0063     setSizePolicy(QSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed));
0064     int maxWidth = width() - (2 * m_offset);
0065     m_scale = 1.;
0066     m_zoomStart = m_zoomHandle.x() * maxWidth;
0067     m_zoomFactor = maxWidth / (m_zoomHandle.y() * maxWidth - m_zoomStart);
0068     timer.setInterval(500);
0069     timer.setSingleShot(true);
0070     connect(&timer, &QTimer::timeout, this, &RemapView::reloadProducer);
0071 }
0072 
0073 bool RemapView::isInRange() const
0074 {
0075     return m_bottomPosition != -1;
0076 }
0077 
0078 void RemapView::updateInPos(int pos)
0079 {
0080     if (m_currentKeyframe.second > -1) {
0081         if (m_moveNext) {
0082             int offset = pos - m_currentKeyframe.second;
0083             QMap<int, int>::iterator it = m_keyframes.find(m_currentKeyframe.first);
0084             while (it != m_keyframes.end()) {
0085                 m_keyframes.insert(it.key(), it.value() + offset);
0086                 it++;
0087             }
0088             m_currentKeyframe.second = pos;
0089         } else {
0090             m_currentKeyframe.second = pos;
0091             m_keyframes.insert(m_currentKeyframe.first, m_currentKeyframe.second);
0092         }
0093         slotSetPosition(pos);
0094         std::pair<double, double> speeds = getSpeed(m_currentKeyframe);
0095         emit updateSpeeds(speeds);
0096         emit updateKeyframes(true);
0097         update();
0098     }
0099 }
0100 
0101 void RemapView::updateOutPos(int pos)
0102 {
0103     if (m_currentKeyframe.first > -1) {
0104         if (m_keyframes.contains(pos)) {
0105             // Cannot move kfr over an existing one
0106             qDebug() << "==== KEYFRAME ALREADY EXISTS AT: " << pos;
0107             return;
0108         }
0109         QMap<int, int> updated;
0110         int offset = pos - m_currentKeyframe.first;
0111         if (m_moveNext) {
0112             QMap<int, int>::iterator it = m_keyframes.find(m_currentKeyframe.first);
0113             while (it != m_keyframes.end()) {
0114                 updated.insert(it.key(), it.value());
0115                 it++;
0116             }
0117             m_currentKeyframe.first = pos;
0118         } else {
0119             m_keyframes.remove(m_currentKeyframe.first);
0120             m_currentKeyframe.first = pos;
0121             m_keyframes.insert(m_currentKeyframe.first, m_currentKeyframe.second);
0122         }
0123         m_selectedKeyframes = {m_currentKeyframe};
0124         QMapIterator<int, int> i(updated);
0125         while (i.hasNext()) {
0126             i.next();
0127             m_keyframes.remove(i.key());
0128         }
0129         i.toFront();
0130         while (i.hasNext()) {
0131             i.next();
0132             m_keyframes.insert(i.key() + offset, i.value());
0133         }
0134         m_bottomPosition = pos - m_inFrame;
0135         std::pair<double, double> speeds = getSpeed(m_currentKeyframe);
0136         emit updateSpeeds(speeds);
0137         emit updateKeyframes(true);
0138         update();
0139     }
0140 }
0141 
0142 int RemapView::remapDuration() const
0143 {
0144     int maxDuration = 0;
0145     if (m_keyframes.isEmpty()) {
0146         return 0;
0147     }
0148     QMapIterator<int, int> i(m_keyframes);
0149     while (i.hasNext()) {
0150         i.next();
0151         if (i.key() > maxDuration) {
0152             maxDuration = i.key();
0153         }
0154     }
0155     return maxDuration - m_inFrame + 1;
0156 }
0157 
0158 int RemapView::remapMax() const
0159 {
0160     int maxDuration = 0;
0161     if (m_keyframes.isEmpty()) {
0162         return 0;
0163     }
0164     QMapIterator<int, int> i(m_keyframes);
0165     while (i.hasNext()) {
0166         i.next();
0167         if (i.value() > maxDuration) {
0168             maxDuration = i.value();
0169         }
0170         if (i.key() > maxDuration) {
0171             maxDuration = i.key();
0172         }
0173     }
0174     return maxDuration - m_inFrame + 1;
0175 }
0176 
0177 bool RemapView::movingKeyframe() const
0178 {
0179     return m_moveKeyframeMode == BottomMove;
0180 }
0181 
0182 void RemapView::setBinClipDuration(std::shared_ptr<ProjectClip> clip, int duration)
0183 {
0184     m_clip = clip;
0185     m_service = clip->originalProducer();
0186     m_duration = duration;
0187     int maxWidth = width() - (2 * m_offset);
0188     m_scale = maxWidth / double(qMax(1, remapMax()));
0189     m_currentKeyframe = m_currentKeyframeOriginal = {-1, -1};
0190 }
0191 
0192 void RemapView::setDuration(std::shared_ptr<Mlt::Producer> service, int duration, int sourceDuration)
0193 {
0194     m_clip = nullptr;
0195     m_sourceDuration = sourceDuration;
0196     if (duration < 0) {
0197         // reset
0198         m_service = nullptr;
0199         m_inFrame = 0;
0200         m_duration = -1;
0201         m_selectedKeyframes.clear();
0202         m_keyframes.clear();
0203         return;
0204     }
0205     if (service) {
0206         m_service = service;
0207         m_inFrame = 0;
0208         m_duration = -1;
0209     }
0210     bool keyframeAdded = false;
0211     if (m_duration > 0 && m_service && !m_keyframes.isEmpty()) {
0212         m_keyframesOrigin = m_keyframes;
0213         if (duration > m_duration) {
0214             // The clip was resized, ensure we have a keyframe at the end of the clip will freeze at last keyframe
0215             QMap<int, int>::const_iterator it = m_keyframes.constEnd();
0216             it--;
0217             int lastKeyframePos = it.key();
0218             int lastKeyframeValue = it.value();
0219             int lastPos = m_inFrame + duration - 1;
0220             if (lastPos > lastKeyframePos) {
0221                 keyframeAdded = true;
0222                 std::pair<double, double> speeds = getSpeed({lastKeyframePos, lastKeyframeValue});
0223                 // Move last keyframe
0224                 it--;
0225                 int updatedVal = it.value() + ((lastPos - it.key()) * speeds.first);
0226                 m_keyframes.remove(lastKeyframePos);
0227                 m_keyframes.insert(lastPos, updatedVal);
0228             }
0229         } else if (duration < m_duration) {
0230             keyframeAdded = true;
0231             // Remove all Keyframes after duration
0232             int lastPos = m_inFrame + duration - 1;
0233             QList<int> toDelete;
0234             QMapIterator<int, int> i(m_keyframes);
0235             while (i.hasNext()) {
0236                 i.next();
0237                 if (i.key() > duration + m_inFrame) {
0238                     toDelete << i.key();
0239                 }
0240             }
0241             if (!toDelete.isEmpty()) {
0242                 // Move last keyframe to end pos
0243                 int posToMove = toDelete.takeFirst();
0244                 if (!m_keyframes.contains(lastPos)) {
0245                     int lastKeyframeValue = m_keyframes.value(posToMove);
0246                     std::pair<double, double> speeds = getSpeed({posToMove, lastKeyframeValue});
0247                     m_keyframes.remove(posToMove);
0248                     while (!toDelete.isEmpty()) {
0249                         m_keyframes.remove(toDelete.takeFirst());
0250                     }
0251                     int updatedVal = m_keyframes.value(m_keyframes.lastKey()) + ((lastPos - m_keyframes.lastKey()) / speeds.first);
0252                     m_keyframes.insert(lastPos, updatedVal);
0253                 }
0254             } else {
0255                 while (!toDelete.isEmpty()) {
0256                     m_keyframes.remove(toDelete.takeFirst());
0257                 }
0258             }
0259         }
0260         if (m_keyframes != m_keyframesOrigin) {
0261             emit updateKeyframesWithUndo(m_keyframes, m_keyframesOrigin);
0262         }
0263     }
0264     if (service == nullptr) {
0265         // We are updating an existing remap effect due to duration change
0266         m_duration = qMax(duration, remapMax());
0267     } else {
0268         m_duration = duration;
0269         m_inFrame = m_service->get_in();
0270         m_originalRange = {m_inFrame, duration + m_inFrame};
0271     }
0272     int maxWidth = width() - (2 * m_offset);
0273     m_scale = maxWidth / double(qMax(1, remapMax() - 1));
0274     m_zoomStart = m_zoomHandle.x() * maxWidth;
0275     m_zoomFactor = maxWidth / (m_zoomHandle.y() * maxWidth - m_zoomStart);
0276     if (!m_keyframes.contains(m_currentKeyframe.first)) {
0277         m_currentKeyframe = m_currentKeyframeOriginal = {-1, -1};
0278         update();
0279     }
0280     if (keyframeAdded) {
0281         emit updateKeyframes(false);
0282     }
0283 }
0284 
0285 void RemapView::loadKeyframes(const QString &mapData)
0286 {
0287     m_keyframes.clear();
0288     if (mapData.isEmpty()) {
0289         if (m_inFrame > 0) {
0290             // Necessary otherwise clip will freeze before first keyframe
0291             m_keyframes.insert(0, 0);
0292         }
0293         m_currentKeyframe = {m_inFrame, m_inFrame};
0294         m_keyframes.insert(m_currentKeyframe.first, m_currentKeyframe.second);
0295         m_keyframes.insert(m_inFrame + m_duration - 1, m_inFrame + m_duration - 1);
0296         std::pair<double, double> speeds = getSpeed(m_currentKeyframe);
0297         std::pair<bool, bool> atEnd = {m_currentKeyframe.first == m_inFrame, m_currentKeyframe.first == m_keyframes.lastKey()};
0298         emit selectedKf(m_currentKeyframe, speeds, atEnd);
0299         emit atKeyframe(true, true);
0300         emit updateKeyframes(false);
0301     } else {
0302         QStringList str = mapData.split(QLatin1Char(';'));
0303         for (auto &s : str) {
0304             int pos = m_service->time_to_frames(s.section(QLatin1Char('='), 0, 0).toUtf8().constData());
0305             int val = GenTime(s.section(QLatin1Char('='), 1).toDouble()).frames(pCore->getCurrentFps());
0306             // HACK: we always set last keyframe 1 frame after in MLT to ensure we have a correct last frame
0307             if (s == str.constLast()) {
0308                 pos--;
0309             }
0310             m_keyframes.insert(pos, val);
0311             m_duration = qMax(m_duration, pos - m_inFrame);
0312             m_duration = qMax(m_duration, val - m_inFrame);
0313         }
0314         bool isKfr = m_keyframes.contains(m_bottomPosition + m_inFrame);
0315         if (isKfr) {
0316             bool isLast = m_bottomPosition + m_inFrame == m_keyframes.firstKey() || m_bottomPosition + m_inFrame == m_keyframes.lastKey();
0317             emit atKeyframe(isKfr, isLast);
0318         } else {
0319             emit atKeyframe(false, false);
0320         }
0321         if (m_keyframes.contains(m_currentKeyframe.first)) {
0322             // bool isLast = m_currentKeyframe.first == m_keyframes.firstKey() || m_currentKeyframe.first == m_keyframes.lastKey();
0323             // emit atKeyframe(true, isLast);
0324             std::pair<double, double> speeds = getSpeed(m_currentKeyframe);
0325             std::pair<bool, bool> atEnd = {m_currentKeyframe.first == m_inFrame, m_currentKeyframe.first == m_keyframes.lastKey()};
0326             emit selectedKf(m_currentKeyframe, speeds, atEnd);
0327         } else {
0328             m_currentKeyframe = {-1, -1};
0329             emit selectedKf(m_currentKeyframe, {-1, -1});
0330         }
0331     }
0332     int maxWidth = width() - (2 * m_offset);
0333     m_scale = maxWidth / double(qMax(1, remapMax()));
0334     m_zoomStart = m_zoomHandle.x() * maxWidth;
0335     m_zoomFactor = maxWidth / (m_zoomHandle.y() * maxWidth - m_zoomStart);
0336     emit updateMaxDuration();
0337     update();
0338 }
0339 
0340 void RemapView::mouseMoveEvent(QMouseEvent *event)
0341 {
0342     event->accept();
0343     // Check for start/end snapping on drag
0344     int pos = -1;
0345     if (m_moveKeyframeMode == BottomMove || m_moveKeyframeMode == TopMove) {
0346         double snapPos = ((m_originalRange.first - m_inFrame) * m_scale) - m_zoomStart;
0347         snapPos *= m_zoomFactor;
0348         snapPos += m_offset;
0349 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0350         if (qAbs(snapPos - event->x()) < QApplication::startDragDistance()) {
0351 #else
0352         if (qAbs(snapPos - event->position().x()) < QApplication::startDragDistance()) {
0353 #endif
0354             pos = m_originalRange.first - m_inFrame;
0355         } else {
0356             snapPos = ((m_originalRange.second - m_inFrame) * m_scale) - m_zoomStart;
0357             snapPos *= m_zoomFactor;
0358             snapPos += m_offset;
0359 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0360             if (qAbs(snapPos - event->x()) < QApplication::startDragDistance()) {
0361 #else
0362             if (qAbs(snapPos - event->position().x()) < QApplication::startDragDistance()) {
0363 #endif
0364                 pos = m_originalRange.second - m_inFrame;
0365             }
0366         }
0367         if (pos == -1) {
0368 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0369             pos = int(((event->x() - m_offset) / m_zoomFactor + m_zoomStart) / m_scale);
0370 #else
0371             pos = int(((event->position().x() - m_offset) / m_zoomFactor + m_zoomStart) / m_scale);
0372 #endif
0373         }
0374     } else {
0375 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0376         pos = int(((event->x() - m_offset) / m_zoomFactor + m_zoomStart) / m_scale);
0377 #else
0378         pos = int(((event->position().x() - m_offset) / m_zoomFactor + m_zoomStart) / m_scale);
0379 #endif
0380     }
0381     pos = qBound(0, pos, m_maxLength - 1);
0382     GenTime position(pos, pCore->getCurrentFps());
0383     if (event->buttons() == Qt::NoButton) {
0384         bool hoverKeyframe = false;
0385 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0386         if (event->y() < 2 * m_lineHeight && event->y() > m_lineHeight) {
0387 #else
0388         if (event->position().y() < 2 * m_lineHeight && event->position().y() > m_lineHeight) {
0389 #endif
0390             // mouse move in top keyframes area
0391             std::pair<int, int> keyframe = getClosestKeyframe(pos + m_inFrame);
0392             if (keyframe.first > -1 && qAbs(keyframe.second - pos - m_inFrame) * m_scale * m_zoomFactor <= m_lineHeight / 2) {
0393                 hoverKeyframe = true;
0394             }
0395 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0396         } else if (event->y() > m_bottomView - 2 * m_lineHeight && event->y() < m_bottomView - m_lineHeight) {
0397 #else
0398         } else if (event->position().y() > m_bottomView - 2 * m_lineHeight && event->position().y() < m_bottomView - m_lineHeight) {
0399 #endif
0400             // move in bottom keyframe area
0401             std::pair<int, int> keyframe = getClosestKeyframe(pos + m_inFrame, true);
0402             if (keyframe.first > -1 && qAbs(keyframe.first - pos - m_inFrame) * m_scale * m_zoomFactor <= m_lineHeight / 2) {
0403                 hoverKeyframe = true;
0404             }
0405         }
0406         if (hoverKeyframe) {
0407             setCursor(Qt::PointingHandCursor);
0408         } else {
0409             setCursor(Qt::ArrowCursor);
0410         }
0411     } else if ((event->buttons() & Qt::LeftButton) != 0u) {
0412         if (m_hoverZoomIn || m_hoverZoomOut || m_hoverZoom) {
0413             // Moving zoom handles
0414             if (m_hoverZoomIn) {
0415 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0416                 m_zoomHandle.setX(qMin(qMax(0., double(event->x() - m_offset) / (width() - 2 * m_offset)), m_zoomHandle.y() - 0.015));
0417 #else
0418                 m_zoomHandle.setX(qMin(qMax(0., double(event->position().x() - m_offset) / (width() - 2 * m_offset)), m_zoomHandle.y() - 0.015));
0419 #endif
0420                 int maxWidth = width() - (2 * m_offset);
0421                 m_zoomStart = m_zoomHandle.x() * maxWidth;
0422                 m_zoomFactor = maxWidth / (m_zoomHandle.y() * maxWidth - m_zoomStart);
0423                 update();
0424                 return;
0425             }
0426             if (m_hoverZoomOut) {
0427 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0428                 m_zoomHandle.setY(qMax(qMin(1., double(event->x() - m_offset) / (width() - 2 * m_offset)), m_zoomHandle.x() + 0.015));
0429 #else
0430                 m_zoomHandle.setY(qMax(qMin(1., double(event->position().x() - m_offset) / (width() - 2 * m_offset)), m_zoomHandle.x() + 0.015));
0431 #endif
0432                 int maxWidth = width() - (2 * m_offset);
0433                 m_zoomStart = m_zoomHandle.x() * maxWidth;
0434                 m_zoomFactor = maxWidth / (m_zoomHandle.y() * maxWidth - m_zoomStart);
0435                 update();
0436                 return;
0437             }
0438             // moving zoom zone
0439             if (m_hoverZoom) {
0440 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0441                 double clickOffset = (double(event->x()) - m_offset) / (width() - 2 * m_offset) - m_clickOffset;
0442 #else
0443                 double clickOffset = (double(event->position().x()) - m_offset) / (width() - 2 * m_offset) - m_clickOffset;
0444 #endif
0445                 double newX = m_zoomHandle.x() + clickOffset;
0446                 if (newX < 0) {
0447                     clickOffset = -m_zoomHandle.x();
0448                     newX = 0;
0449                 }
0450                 double newY = m_zoomHandle.y() + clickOffset;
0451                 if (newY > 1) {
0452                     clickOffset = 1 - m_zoomHandle.y();
0453                     newY = 1;
0454                     newX = m_zoomHandle.x() + clickOffset;
0455                 }
0456 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0457                 m_clickOffset = (double(event->x()) - m_offset) / (width() - 2 * m_offset);
0458 #else
0459                 m_clickOffset = (double(event->position().x()) - m_offset) / (width() - 2 * m_offset);
0460 #endif
0461                 m_zoomHandle = QPointF(newX, newY);
0462                 int maxWidth = width() - (2 * m_offset);
0463                 m_zoomStart = m_zoomHandle.x() * maxWidth;
0464                 m_zoomFactor = maxWidth / (m_zoomHandle.y() * maxWidth - m_zoomStart);
0465                 update();
0466             }
0467             return;
0468         }
0469         // qDebug()<<"=== MOVING MOUSE: "<<pos<<" = "<<m_currentKeyframe<<", MOVE KFMODE: "<<m_moveKeyframeMode;
0470         if ((m_currentKeyframe.second == pos + m_inFrame && m_moveKeyframeMode == TopMove) ||
0471             (m_currentKeyframe.first == pos + m_inFrame && m_moveKeyframeMode == BottomMove)) {
0472             return;
0473         }
0474         if (m_currentKeyframe.first >= 0 && m_moveKeyframeMode != NoMove) {
0475             if (m_moveKeyframeMode == BottomMove) {
0476                 // Moving bottom keyframe
0477                 if (!m_keyframes.contains(pos + m_inFrame)) {
0478                     int delta = pos + m_inFrame - m_currentKeyframe.first;
0479                     // Check that the move is possible
0480                     QMapIterator<int, int> i(m_selectedKeyframes);
0481                     while (i.hasNext()) {
0482                         i.next();
0483                         int updatedPos = i.key() + delta;
0484                         if (!m_selectedKeyframes.contains(updatedPos) && m_keyframes.contains(updatedPos)) {
0485                             // Don't allow moving over another keyframe
0486                             qDebug() << "== MOVE ABORTED; OVERLAPPING EXISTING";
0487                             return;
0488                         }
0489                     }
0490                     i.toFront();
0491                     QMap<int, int> updated;
0492                     while (i.hasNext()) {
0493                         i.next();
0494                         if (i.key() < m_currentKeyframe.first) {
0495                             continue;
0496                         }
0497                         // qDebug()<<"=== MOVING KFR: "<<i.key()<<" > "<<(i.key() + delta);
0498                         // m_keyframes.insert(i.key() + delta, i.value());
0499                         updated.insert(i.key() + delta, i.value());
0500                         m_keyframes.remove(i.key());
0501                     }
0502                     QMapIterator<int, int> j(updated);
0503                     while (j.hasNext()) {
0504                         j.next();
0505                         m_keyframes.insert(j.key(), j.value());
0506                     }
0507                     QMap<int, int> updatedSelection;
0508                     QMapIterator<int, int> k(m_previousSelection);
0509                     while (k.hasNext()) {
0510                         k.next();
0511                         updatedSelection.insert(k.key() + delta, k.value());
0512                     }
0513                     m_previousSelection = updatedSelection;
0514                     m_currentKeyframe.first += delta;
0515                     std::pair<double, double> speeds = getSpeed(m_currentKeyframe);
0516                     std::pair<bool, bool> atEnd = {m_currentKeyframe.first == m_inFrame, m_currentKeyframe.first == m_keyframes.lastKey()};
0517                     emit selectedKf(m_currentKeyframe, speeds, atEnd);
0518 
0519                     m_selectedKeyframes = updated;
0520                     emit seekToPos(-1, pos);
0521                     emit updateKeyframes(false);
0522                     if (remapMax() > m_lastMaxDuration) {
0523                         m_lastMaxDuration = remapMax();
0524                         int maxWidth = width() - (2 * m_offset);
0525                         m_scale = maxWidth / double(qMax(1, m_lastMaxDuration));
0526                         m_zoomStart = m_zoomHandle.x() * maxWidth;
0527                         m_zoomFactor = maxWidth / (m_zoomHandle.y() * maxWidth - m_zoomStart);
0528                     }
0529                     update();
0530                     return;
0531                 } else {
0532                     qDebug() << "=== KEYFRAME :" << pos << " ALREADY EXISTS";
0533                 }
0534             } else if (m_moveKeyframeMode == TopMove) {
0535                 // Moving top keyframe
0536                 int realPos = qMax(m_inFrame, pos + m_inFrame);
0537                 // pos = GenTime(m_remapLink->anim_get_double("map", pos)).frames(pCore->getCurrentFps());
0538                 int delta = realPos - m_currentKeyframe.second;
0539                 // Check that the move is possible
0540                 QMapIterator<int, int> i(m_selectedKeyframes);
0541                 while (i.hasNext()) {
0542                     i.next();
0543                     if (i.value() + delta >= m_sourceDuration) {
0544                         delta = qMin(delta, m_sourceDuration - i.value() - 1);
0545                         realPos = m_currentKeyframe.second + delta;
0546                         pos = realPos - m_inFrame;
0547                         if (delta == 0) {
0548                             pCore->displayMessage(i18n("Cannot move last source keyframe past clip end"), MessageType::ErrorMessage, 500);
0549                             return;
0550                         }
0551                     }
0552                     if (i.value() + delta < 0) {
0553                         delta = qMax(delta, -i.value());
0554                         realPos = m_currentKeyframe.second + delta;
0555                         pos = realPos - m_inFrame;
0556                         if (delta == 0) {
0557                             pCore->displayMessage(i18n("Cannot move first source keyframe before clip start"), MessageType::ErrorMessage, 500);
0558                             return;
0559                         }
0560                     }
0561                 }
0562                 i.toFront();
0563                 QMap<int, int> updated;
0564                 while (i.hasNext()) {
0565                     i.next();
0566                     m_keyframes.insert(i.key(), i.value() + delta);
0567                     updated.insert(i.key(), i.value() + delta);
0568                     if (i.value() == m_currentKeyframe.second) {
0569                         m_currentKeyframe.second = realPos;
0570                     }
0571                 }
0572                 QMap<int, int> updatedSelection;
0573                 QMapIterator<int, int> k(m_previousSelection);
0574                 while (k.hasNext()) {
0575                     k.next();
0576                     updatedSelection.insert(k.key(), k.value() + delta);
0577                 }
0578                 m_previousSelection = updatedSelection;
0579                 std::pair<double, double> speeds = getSpeed(m_currentKeyframe);
0580                 std::pair<bool, bool> atEnd = {m_currentKeyframe.first == m_inFrame, m_currentKeyframe.first == m_keyframes.lastKey()};
0581                 emit selectedKf(m_currentKeyframe, speeds, atEnd);
0582                 m_selectedKeyframes = updated;
0583                 slotSetPosition(pos + m_inFrame);
0584                 emit seekToPos(pos + m_inFrame, -1);
0585                 emit updateKeyframes(false);
0586                 if (remapMax() > m_lastMaxDuration) {
0587                     m_lastMaxDuration = remapMax();
0588                     int maxWidth = width() - (2 * m_offset);
0589                     m_scale = maxWidth / double(qMax(1, m_lastMaxDuration));
0590                     m_zoomStart = m_zoomHandle.x() * maxWidth;
0591                     m_zoomFactor = maxWidth / (m_zoomHandle.y() * maxWidth - m_zoomStart);
0592                 }
0593                 update();
0594                 return;
0595             }
0596         }
0597         // Rubberband selection
0598         if (m_clickPoint >= 0) {
0599             m_clickEnd = pos + m_inFrame;
0600             int min = qMin(m_clickPoint, m_clickEnd);
0601             int max = qMax(m_clickPoint, m_clickEnd);
0602             min = qMax(1, min);
0603             m_selectedKeyframes.clear();
0604             m_currentKeyframeOriginal = m_currentKeyframe = {-1, -1};
0605             QMapIterator<int, int> i(m_keyframes);
0606             while (i.hasNext()) {
0607                 i.next();
0608                 if (i.key() > min && i.key() <= max) {
0609                     m_selectedKeyframes.insert(i.key(), i.value());
0610                 }
0611             }
0612             if (!m_selectedKeyframes.isEmpty()) {
0613                 m_currentKeyframe = {m_selectedKeyframes.firstKey(), m_selectedKeyframes.value(m_selectedKeyframes.firstKey())};
0614                 m_currentKeyframeOriginal = m_currentKeyframe;
0615             }
0616             update();
0617             return;
0618         }
0619 
0620         if (m_moveKeyframeMode == CursorMove) {
0621             if (pos != m_position) {
0622                 slotSetPosition(pos + m_inFrame);
0623                 emit seekToPos(pos + m_inFrame, -1);
0624             }
0625         }
0626         if (m_moveKeyframeMode == CursorMoveBottom) {
0627             pos = qMin(pos, m_keyframes.lastKey() - m_inFrame);
0628             if (pos != m_bottomPosition) {
0629                 m_bottomPosition = pos;
0630                 bool isKfr = m_keyframes.contains(m_bottomPosition + m_inFrame);
0631                 if (isKfr) {
0632                     bool isLast = m_bottomPosition + m_inFrame == m_keyframes.firstKey() || m_bottomPosition + m_inFrame == m_keyframes.lastKey();
0633                     emit atKeyframe(isKfr, isLast);
0634                 } else {
0635                     emit atKeyframe(false, false);
0636                 }
0637                 pos = GenTime(m_remapLink->anim_get_double("map", pos + m_inFrame)).frames(pCore->getCurrentFps());
0638                 slotSetPosition(pos);
0639                 emit seekToPos(-1, m_bottomPosition);
0640             }
0641         }
0642         return;
0643     }
0644 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0645     if (event->y() > m_bottomView) {
0646         // Moving in zoom area
0647         if (qAbs(event->x() - m_offset - (m_zoomHandle.x() * (width() - 2 * m_offset))) < QApplication::startDragDistance()) {
0648 #else
0649     if (event->position().y() > m_bottomView) {
0650         // Moving in zoom area
0651         if (qAbs(event->position().x() - m_offset - (m_zoomHandle.x() * (width() - 2 * m_offset))) < QApplication::startDragDistance()) {
0652 #endif
0653             setCursor(Qt::SizeHorCursor);
0654             m_hoverZoomIn = true;
0655             m_hoverZoomOut = false;
0656             m_hoverZoom = false;
0657             update();
0658             return;
0659         }
0660 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0661         if (qAbs(event->x() - m_offset - (m_zoomHandle.y() * (width() - 2 * m_offset))) < QApplication::startDragDistance()) {
0662 #else
0663         if (qAbs(event->position().x() - m_offset - (m_zoomHandle.y() * (width() - 2 * m_offset))) < QApplication::startDragDistance()) {
0664 #endif
0665             setCursor(Qt::SizeHorCursor);
0666             m_hoverZoomOut = true;
0667             m_hoverZoomIn = false;
0668             m_hoverZoom = false;
0669             update();
0670             return;
0671         }
0672 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0673         if (m_zoomHandle != QPointF(0, 1) && event->x() > m_offset + (m_zoomHandle.x() * (width() - 2 * m_offset)) &&
0674             event->x() < m_offset + (m_zoomHandle.y() * (width() - 2 * m_offset))) {
0675 #else
0676         if (m_zoomHandle != QPointF(0, 1) && event->position().x() > m_offset + (m_zoomHandle.x() * (width() - 2 * m_offset)) &&
0677             event->position().x() < m_offset + (m_zoomHandle.y() * (width() - 2 * m_offset))) {
0678 #endif
0679             setCursor(Qt::PointingHandCursor);
0680             m_hoverZoom = true;
0681             m_hoverZoomIn = false;
0682             m_hoverZoomOut = false;
0683             update();
0684             return;
0685         }
0686     }
0687 
0688     if (m_hoverZoomOut || m_hoverZoomIn || m_hoverZoom) {
0689         m_hoverZoomOut = false;
0690         m_hoverZoomIn = false;
0691         m_hoverZoom = false;
0692         setCursor(Qt::ArrowCursor);
0693         update();
0694     }
0695 }
0696 
0697 int RemapView::position() const
0698 {
0699     return m_position;
0700 }
0701 
0702 void RemapView::centerCurrentKeyframe()
0703 {
0704     if (m_currentKeyframe.first == -1) {
0705         // No keyframe selected, abort
0706         return;
0707     }
0708     QMap<int, int> nextKeyframes;
0709     if (m_moveNext) {
0710         QMap<int, int>::iterator it = m_keyframes.find(m_currentKeyframe.first);
0711         if (it != m_keyframes.end() && *it != m_keyframes.last()) {
0712             it++;
0713             while (it != m_keyframes.end()) {
0714                 nextKeyframes.insert(it.key(), it.value());
0715                 it++;
0716             }
0717         }
0718     }
0719     m_keyframes.remove(m_currentKeyframe.first);
0720     int offset = m_bottomPosition + m_inFrame - m_currentKeyframe.first;
0721     m_currentKeyframe.first = m_bottomPosition + m_inFrame;
0722     if (offset == 0) {
0723         // no move
0724         return;
0725     }
0726     m_keyframes.insert(m_currentKeyframe.first, m_currentKeyframe.second);
0727     QMapIterator<int, int> i(nextKeyframes);
0728     while (i.hasNext()) {
0729         i.next();
0730         m_keyframes.remove(i.key());
0731     }
0732     i.toFront();
0733     while (i.hasNext()) {
0734         i.next();
0735         m_keyframes.insert(i.key() + offset, i.value());
0736     }
0737     std::pair<double, double> speeds = getSpeed(m_currentKeyframe);
0738     std::pair<bool, bool> atEnd = {m_currentKeyframe.first == m_inFrame, m_currentKeyframe.first == m_keyframes.lastKey()};
0739     emit selectedKf(m_currentKeyframe, speeds, atEnd);
0740     bool isLast = m_currentKeyframe.first == m_keyframes.firstKey() || m_currentKeyframe.first == m_keyframes.lastKey();
0741     emit atKeyframe(true, isLast);
0742     emit updateKeyframes(true);
0743     update();
0744 }
0745 
0746 void RemapView::centerCurrentTopKeyframe()
0747 {
0748     if (m_currentKeyframe.first == -1) {
0749         // No keyframe selected, abort
0750         return;
0751     }
0752     // std::pair<int,int> range = getRange(m_currentKeyframe);
0753     QMap<int, int> nextKeyframes;
0754     int offset = m_position + m_inFrame - m_currentKeyframe.second;
0755     if (m_moveNext) {
0756         QMap<int, int>::iterator it = m_keyframes.find(m_currentKeyframe.first);
0757         if (it != m_keyframes.end() && *it != m_keyframes.last()) {
0758             it++;
0759             while (it != m_keyframes.end()) {
0760                 nextKeyframes.insert(it.key(), it.value());
0761                 // Check that the move is possible
0762                 if (it.value() + offset >= m_sourceDuration) {
0763                     pCore->displayMessage(i18n("Cannot move last source keyframe past clip end"), MessageType::ErrorMessage, 500);
0764                     return;
0765                 }
0766                 if (it.value() + offset < 0) {
0767                     pCore->displayMessage(i18n("Cannot move first source keyframe before clip start"), MessageType::ErrorMessage, 500);
0768                     return;
0769                 }
0770                 it++;
0771             }
0772         }
0773     }
0774     m_currentKeyframe.second = m_position + m_inFrame;
0775     m_keyframes.insert(m_currentKeyframe.first, m_currentKeyframe.second);
0776     QMapIterator<int, int> i(nextKeyframes);
0777     while (i.hasNext()) {
0778         i.next();
0779         m_keyframes.insert(i.key(), i.value() + offset);
0780     }
0781     std::pair<double, double> speeds = getSpeed(m_currentKeyframe);
0782     std::pair<bool, bool> atEnd = {m_currentKeyframe.first == m_inFrame, m_currentKeyframe.first == m_keyframes.lastKey()};
0783     emit selectedKf(m_currentKeyframe, speeds, atEnd);
0784     bool isLast = m_currentKeyframe.first == m_keyframes.firstKey() || m_currentKeyframe.first == m_keyframes.lastKey();
0785     emit atKeyframe(true, isLast);
0786     emit updateKeyframes(true);
0787     update();
0788 }
0789 
0790 std::pair<int, int> RemapView::getClosestKeyframe(int pos, bool bottomKeyframe) const
0791 {
0792     int deltaMin = -1;
0793     std::pair<int, int> result = {-1, -1};
0794     QMapIterator<int, int> i(m_keyframes);
0795     while (i.hasNext()) {
0796         i.next();
0797         int val = bottomKeyframe ? i.key() : i.value();
0798         int delta = qAbs(val - pos);
0799         if (deltaMin == -1 || delta < deltaMin) {
0800             deltaMin = delta;
0801             result = {i.key(), i.value()};
0802         }
0803     }
0804     return result;
0805 }
0806 
0807 void RemapView::mouseReleaseEvent(QMouseEvent *event)
0808 {
0809     event->accept();
0810     bool keyframesEdited = m_keyframesOrigin != m_keyframes;
0811     if (m_moveKeyframeMode == TopMove || m_moveKeyframeMode == BottomMove) {
0812         // Restore original selection
0813         m_selectedKeyframes = m_previousSelection;
0814         int maxWidth = width() - (2 * m_offset);
0815         m_scale = maxWidth / double(qMax(1, remapMax()));
0816         m_zoomStart = m_zoomHandle.x() * maxWidth;
0817         m_zoomFactor = maxWidth / (m_zoomHandle.y() * maxWidth - m_zoomStart);
0818         update();
0819         if (!keyframesEdited) {
0820             emit seekToPos(m_currentKeyframeOriginal.second, m_bottomPosition);
0821         }
0822     }
0823     m_moveKeyframeMode = NoMove;
0824     if (keyframesEdited) {
0825         emit updateKeyframesWithUndo(m_keyframes, m_keyframesOrigin);
0826     }
0827 }
0828 
0829 void RemapView::mousePressEvent(QMouseEvent *event)
0830 {
0831     event->accept();
0832     m_lastMaxDuration = remapMax();
0833 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0834     int pos = int(((event->x() - m_offset) / m_zoomFactor + m_zoomStart) / m_scale);
0835 #else
0836     int pos = int(((event->position().x() - m_offset) / m_zoomFactor + m_zoomStart) / m_scale);
0837 #endif
0838     pos = qBound(0, pos, m_maxLength);
0839     m_moveKeyframeMode = NoMove;
0840     m_keyframesOrigin = m_keyframes;
0841     m_oldInFrame = m_inFrame;
0842     if (event->button() == Qt::LeftButton) {
0843 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0844         if (event->y() < m_centerPos) {
0845 #else
0846         if (event->position().y() < m_centerPos) {
0847 #endif
0848             // mouse click in top area
0849             if (event->modifiers() & Qt::ShiftModifier) {
0850                 m_clickPoint = pos + m_inFrame;
0851                 return;
0852             }
0853 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0854             if (event->y() < 2 * m_lineHeight && event->y() > m_lineHeight) {
0855 #else
0856             if (event->position().y() < 2 * m_lineHeight && event->position().y() > m_lineHeight) {
0857 #endif
0858                 std::pair<int, int> keyframe = getClosestKeyframe(pos + m_inFrame);
0859                 if (keyframe.first > -1 && qAbs(keyframe.second - (pos + m_inFrame)) * m_scale * m_zoomFactor <= m_lineHeight / 2) {
0860                     // Clicked on a top keyframe
0861                     m_currentKeyframeOriginal = keyframe;
0862                     if (event->modifiers() & Qt::ControlModifier) {
0863                         if (m_selectedKeyframes.contains(m_currentKeyframeOriginal.first)) {
0864                             m_selectedKeyframes.remove(m_currentKeyframeOriginal.first);
0865                             m_currentKeyframeOriginal.first = -1;
0866                         } else {
0867                             m_selectedKeyframes.insert(m_currentKeyframeOriginal.first, m_currentKeyframeOriginal.second);
0868                         }
0869                     } else if (!m_selectedKeyframes.contains(m_currentKeyframeOriginal.first)) {
0870                         m_selectedKeyframes = {m_currentKeyframeOriginal};
0871                     }
0872                     // Select and seek to keyframe
0873                     m_currentKeyframe = m_currentKeyframeOriginal;
0874                     // Calculate speeds
0875                     std::pair<double, double> speeds = getSpeed(m_currentKeyframe);
0876                     std::pair<bool, bool> atEnd = {m_currentKeyframe.first == m_inFrame, m_currentKeyframe.first == m_keyframes.lastKey()};
0877                     emit selectedKf(m_currentKeyframe, speeds, atEnd);
0878                     if (m_currentKeyframeOriginal.first > -1) {
0879                         m_moveKeyframeMode = TopMove;
0880                         m_previousSelection = m_selectedKeyframes;
0881                         if (m_moveNext) {
0882                             QMapIterator<int, int> i(m_keyframes);
0883                             while (i.hasNext()) {
0884                                 i.next();
0885                                 if (i.key() > m_currentKeyframeOriginal.first) {
0886                                     m_selectedKeyframes.insert(i.key(), i.value());
0887                                 }
0888                             }
0889                         }
0890                         if (KdenliveSettings::keyframeseek()) {
0891                             slotSetPosition(m_currentKeyframeOriginal.second);
0892                             m_bottomPosition = m_currentKeyframeOriginal.first - m_inFrame;
0893                             bool isLast = m_currentKeyframe.first == m_keyframes.firstKey() || m_currentKeyframe.first == m_keyframes.lastKey();
0894                             emit atKeyframe(true, isLast);
0895                         } else {
0896                             update();
0897                         }
0898                     } else {
0899                         update();
0900                     }
0901                     return;
0902                 }
0903             }
0904             // no keyframe next to mouse
0905             // m_selectedKeyframes.clear();
0906             // m_currentKeyframe = m_currentKeyframeOriginal = {-1,-1};
0907             m_moveKeyframeMode = CursorMove;
0908             if (pos != m_position) {
0909                 slotSetPosition(pos + m_inFrame);
0910                 emit seekToPos(pos + m_inFrame, -1);
0911                 update();
0912             }
0913             return;
0914 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0915         } else if (event->y() > m_bottomView) {
0916 #else
0917         } else if (event->position().y() > m_bottomView) {
0918 #endif
0919             // click on zoom area
0920             if (m_hoverZoom) {
0921 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0922                 m_clickOffset = (double(event->x()) - m_offset) / (width() - 2 * m_offset);
0923 #else
0924                 m_clickOffset = (double(event->position().x()) - m_offset) / (width() - 2 * m_offset);
0925 #endif
0926             }
0927             return;
0928 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0929         } else if (event->y() > m_centerPos && event->y() < m_bottomView) {
0930 #else
0931         } else if (event->position().y() > m_centerPos && event->position().y() < m_bottomView) {
0932 #endif
0933             // click in bottom area
0934             if (event->modifiers() & Qt::ShiftModifier) {
0935                 m_clickPoint = pos + m_inFrame;
0936                 return;
0937             }
0938 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0939             if (event->y() > (m_bottomView - 2 * m_lineHeight) && (event->y() < m_bottomView - m_lineHeight)) {
0940 #else
0941             if (event->position().y() > (m_bottomView - 2 * m_lineHeight) && (event->position().y() < m_bottomView - m_lineHeight)) {
0942 #endif
0943                 std::pair<int, int> keyframe = getClosestKeyframe(pos + m_inFrame, true);
0944                 if (keyframe.first > -1 && qAbs(keyframe.first - (pos + m_inFrame)) * m_scale * m_zoomFactor <= m_lineHeight / 2) {
0945                     m_currentKeyframeOriginal = keyframe;
0946                     if (event->modifiers() & Qt::ControlModifier) {
0947                         if (m_selectedKeyframes.contains(m_currentKeyframeOriginal.first)) {
0948                             m_selectedKeyframes.remove(m_currentKeyframeOriginal.first);
0949                             m_currentKeyframeOriginal.second = -1;
0950                         } else {
0951                             m_selectedKeyframes.insert(m_currentKeyframeOriginal.first, m_currentKeyframeOriginal.second);
0952                         }
0953                     } else if (!m_selectedKeyframes.contains(m_currentKeyframeOriginal.first)) {
0954                         m_selectedKeyframes = {m_currentKeyframeOriginal};
0955                     }
0956                     // Select and seek to keyframe
0957                     m_currentKeyframe = m_currentKeyframeOriginal;
0958                     std::pair<double, double> speeds = getSpeed(m_currentKeyframe);
0959                     std::pair<bool, bool> atEnd = {m_currentKeyframe.first == m_inFrame, m_currentKeyframe.first == m_keyframes.lastKey()};
0960                     emit selectedKf(m_currentKeyframe, speeds, atEnd);
0961                     if (m_currentKeyframeOriginal.first > -1) {
0962                         m_moveKeyframeMode = BottomMove;
0963                         m_previousSelection = m_selectedKeyframes;
0964                         if (m_moveNext) {
0965                             QMapIterator<int, int> i(m_keyframes);
0966                             while (i.hasNext()) {
0967                                 i.next();
0968                                 if (i.key() > m_currentKeyframeOriginal.first) {
0969                                     m_selectedKeyframes.insert(i.key(), i.value());
0970                                 }
0971                             }
0972                         }
0973                         if (KdenliveSettings::keyframeseek()) {
0974                             m_bottomPosition = m_currentKeyframeOriginal.first - m_inFrame;
0975                             int topPos = GenTime(m_remapLink->anim_get_double("map", m_currentKeyframeOriginal.first)).frames(pCore->getCurrentFps());
0976                             m_position = topPos - m_inFrame;
0977                             bool isLast = m_currentKeyframe.first == m_keyframes.firstKey() || m_currentKeyframe.first == m_keyframes.lastKey();
0978                             emit atKeyframe(true, isLast);
0979                         } else {
0980                             update();
0981                         }
0982                     } else {
0983                         update();
0984                     }
0985                     return;
0986                 }
0987             }
0988             // no keyframe next to mouse
0989             // m_selectedKeyframes.clear();
0990             // m_currentKeyframe = m_currentKeyframeOriginal = {-1,-1};
0991             m_moveKeyframeMode = CursorMoveBottom;
0992             pos = qMin(pos, m_keyframes.lastKey() - m_inFrame);
0993             if (pos != m_bottomPosition) {
0994                 m_bottomPosition = pos;
0995                 bool isKfr = m_keyframes.contains(m_bottomPosition + m_inFrame);
0996                 if (isKfr) {
0997                     bool isLast = m_bottomPosition + m_inFrame == m_keyframes.firstKey() || m_bottomPosition + m_inFrame == m_keyframes.lastKey();
0998                     emit atKeyframe(isKfr, isLast);
0999                 } else {
1000                     emit atKeyframe(false, false);
1001                 }
1002                 // slotUpdatePosition();
1003                 emit seekToPos(-1, pos);
1004                 update();
1005             }
1006             /*int topPos = GenTime(m_remapLink->anim_get_double("map", pos + m_inFrame)).frames(pCore->getCurrentFps());
1007             if (topPos != m_position + m_inFrame) {
1008 
1009                 slotSetPosition(topPos);
1010                 emit seekToPos(-1, pos);
1011                 update();
1012             }*/
1013             return;
1014         }
1015 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
1016     } else if (event->button() == Qt::RightButton && event->y() > m_bottomView) {
1017 #else
1018     } else if (event->button() == Qt::RightButton && event->position().y() > m_bottomView) {
1019 #endif
1020         // Right click on zoom, switch between no zoom and last zoom status
1021         if (m_zoomHandle == QPointF(0, 1)) {
1022             if (!m_lastZoomHandle.isNull()) {
1023                 m_zoomHandle = m_lastZoomHandle;
1024                 int maxWidth = width() - (2 * m_offset);
1025                 m_zoomStart = m_zoomHandle.x() * maxWidth;
1026                 m_zoomFactor = maxWidth / (m_zoomHandle.y() * maxWidth - m_zoomStart);
1027                 update();
1028                 return;
1029             }
1030         } else {
1031             m_lastZoomHandle = m_zoomHandle;
1032             m_zoomHandle = QPointF(0, 1);
1033             int maxWidth = width() - (2 * m_offset);
1034             m_zoomStart = m_zoomHandle.x() * maxWidth;
1035             m_zoomFactor = maxWidth / (m_zoomHandle.y() * maxWidth - m_zoomStart);
1036             update();
1037             return;
1038         }
1039     }
1040     if (pos != m_position) {
1041         // emit seekToPos(pos);
1042         update();
1043     }
1044 }
1045 
1046 void RemapView::wheelEvent(QWheelEvent *event)
1047 {
1048     if (event->modifiers() & Qt::AltModifier) {
1049         // Alt modifier seems to invert x/y axis
1050         if (event->angleDelta().x() > 0) {
1051             goPrev();
1052         } else {
1053             goNext();
1054         }
1055         return;
1056     }
1057     if (event->modifiers() & Qt::ControlModifier) {
1058         int maxWidth = width() - 2 * m_offset;
1059         double scaledPos = m_position * m_scale;
1060         double zoomRange = (m_zoomHandle.y() - m_zoomHandle.x()) * maxWidth;
1061         if (event->angleDelta().y() > 0) {
1062             zoomRange /= 1.5;
1063         } else {
1064             zoomRange *= 1.5;
1065         }
1066         if (zoomRange < 5) {
1067             // Don't allow too small zoombar
1068             return;
1069         }
1070         double length = (scaledPos - zoomRange / 2) / maxWidth;
1071         m_zoomHandle.setX(qMax(0., length));
1072         if (length < 0) {
1073             m_zoomHandle.setY(qMin(1.0, (scaledPos + zoomRange / 2) / maxWidth - length));
1074         } else {
1075             m_zoomHandle.setY(qMin(1.0, (scaledPos + zoomRange / 2) / maxWidth));
1076         }
1077         m_zoomStart = m_zoomHandle.x() * maxWidth;
1078         m_zoomFactor = maxWidth / (m_zoomHandle.y() * maxWidth - m_zoomStart);
1079         update();
1080         return;
1081     }
1082     if (event->position().y() < m_bottomView) {
1083         int change = event->angleDelta().y() > 0 ? -1 : 1;
1084         int pos = qBound(0, m_position + change, m_duration - 1);
1085         emit seekToPos(pos + m_inFrame, -1);
1086     } else {
1087         // Wheel on zoom bar, scroll
1088         double pos = m_zoomHandle.x();
1089         double zoomWidth = m_zoomHandle.y() - pos;
1090         int maxWidth = width() - 2 * m_offset;
1091         if (event->angleDelta().y() > 0) {
1092             if (zoomWidth / 2 > pos) {
1093                 pos = 0;
1094             } else {
1095                 pos -= zoomWidth / 2;
1096             }
1097         } else {
1098             if (pos + zoomWidth + zoomWidth / 2 > 1.) {
1099                 pos = 1. - zoomWidth;
1100             } else {
1101                 pos += zoomWidth / 2;
1102             }
1103         }
1104         m_zoomHandle = QPointF(pos, pos + zoomWidth);
1105         m_zoomStart = m_zoomHandle.x() * maxWidth;
1106         m_zoomFactor = maxWidth / (m_zoomHandle.y() * maxWidth - m_zoomStart);
1107         update();
1108     }
1109 }
1110 
1111 void RemapView::slotSetPosition(int pos)
1112 {
1113     if (pos != m_position + m_inFrame) {
1114         m_position = pos - m_inFrame;
1115         // int offset = pCore->getItemIn(m_model->getOwnerId());
1116         // emit atKeyframe(m_keyframes.contains(pos));
1117         double zoomPos = double(m_position) / m_duration;
1118         if (zoomPos < m_zoomHandle.x()) {
1119             double interval = m_zoomHandle.y() - m_zoomHandle.x();
1120             zoomPos = qBound(0.0, zoomPos - interval / 5, 1.0);
1121             m_zoomHandle.setX(zoomPos);
1122             m_zoomHandle.setY(zoomPos + interval);
1123         } else if (zoomPos > m_zoomHandle.y()) {
1124             double interval = m_zoomHandle.y() - m_zoomHandle.x();
1125             zoomPos = qBound(0.0, zoomPos + interval / 5, 1.0);
1126             m_zoomHandle.setX(zoomPos - interval);
1127             m_zoomHandle.setY(zoomPos);
1128         }
1129         update();
1130     }
1131 }
1132 
1133 void RemapView::slotSetBottomPosition(int pos)
1134 {
1135     if (pos < 0 || pos + m_inFrame > m_keyframes.lastKey()) {
1136         pos = -1;
1137     }
1138     if (pos != m_bottomPosition) {
1139         m_bottomPosition = pos;
1140         if (m_bottomPosition > -1) {
1141             bool isKfr = m_keyframes.contains(m_bottomPosition + m_inFrame);
1142             if (isKfr) {
1143                 bool isLast = m_bottomPosition + m_inFrame == m_keyframes.firstKey() || m_bottomPosition + m_inFrame == m_keyframes.lastKey();
1144                 emit atKeyframe(isKfr, isLast);
1145             } else {
1146                 emit atKeyframe(false, false);
1147             }
1148         } else {
1149             emit atKeyframe(false, false);
1150         }
1151         update();
1152     }
1153 }
1154 
1155 void RemapView::goNext()
1156 {
1157     // Seek to next keyframe
1158     QMapIterator<int, int> i(m_keyframes);
1159     while (i.hasNext()) {
1160         i.next();
1161         if (i.key() > m_bottomPosition + m_inFrame) {
1162             m_currentKeyframe = {i.key(), i.value()};
1163             m_selectedKeyframes = {m_currentKeyframe};
1164             slotSetPosition(i.key());
1165             m_bottomPosition = m_currentKeyframe.first - m_inFrame;
1166             emit seekToPos(i.value(), m_bottomPosition);
1167             std::pair<double, double> speeds = getSpeed(m_currentKeyframe);
1168             std::pair<bool, bool> atEnd = {m_currentKeyframe.first == m_inFrame, m_currentKeyframe.first == m_keyframes.lastKey()};
1169             emit selectedKf(m_currentKeyframe, speeds, atEnd);
1170             bool isLast = m_currentKeyframe.first == m_keyframes.firstKey() || m_currentKeyframe.first == m_keyframes.lastKey();
1171             emit atKeyframe(true, isLast);
1172             break;
1173         }
1174     }
1175 }
1176 
1177 void RemapView::goPrev()
1178 {
1179     // insert keyframe at interpolated position
1180     bool previousFound = false;
1181     QMap<int, int>::const_iterator it = m_keyframes.constBegin();
1182     while (it.key() < m_bottomPosition + m_inFrame && it != m_keyframes.constEnd()) {
1183         it++;
1184     }
1185     if (it != m_keyframes.constEnd()) {
1186         if (it != m_keyframes.constBegin()) {
1187             it--;
1188         }
1189         m_currentKeyframe = {it.key(), it.value()};
1190         m_selectedKeyframes = {m_currentKeyframe};
1191         slotSetPosition(m_currentKeyframe.second);
1192         m_bottomPosition = m_currentKeyframe.first - m_inFrame;
1193         emit seekToPos(m_currentKeyframe.second, m_bottomPosition);
1194         std::pair<double, double> speeds = getSpeed(m_currentKeyframe);
1195         std::pair<bool, bool> atEnd = {m_currentKeyframe.first == m_inFrame, m_currentKeyframe.first == m_keyframes.lastKey()};
1196         emit selectedKf(m_currentKeyframe, speeds, atEnd);
1197         bool isLast = m_currentKeyframe.first == m_keyframes.firstKey() || m_currentKeyframe.first == m_keyframes.lastKey();
1198         emit atKeyframe(true, isLast);
1199         previousFound = true;
1200     }
1201 
1202     if (!previousFound && !m_keyframes.isEmpty()) {
1203         // We are after the last keyframe
1204         m_currentKeyframe = {m_keyframes.lastKey(), m_keyframes.value(m_keyframes.lastKey())};
1205         m_selectedKeyframes = {m_currentKeyframe};
1206         slotSetPosition(m_currentKeyframe.second);
1207         m_bottomPosition = m_currentKeyframe.first - m_inFrame;
1208         emit seekToPos(m_currentKeyframe.second, m_bottomPosition);
1209         std::pair<double, double> speeds = getSpeed(m_currentKeyframe);
1210         std::pair<bool, bool> atEnd = {m_currentKeyframe.first == m_inFrame, m_currentKeyframe.first == m_keyframes.lastKey()};
1211         emit selectedKf(m_currentKeyframe, speeds, atEnd);
1212     }
1213 }
1214 
1215 void RemapView::updateBeforeSpeed(double speed)
1216 {
1217     QMutexLocker lock(&m_kfrMutex);
1218     QMap<int, int>::const_iterator it = m_keyframes.constFind(m_currentKeyframe.first);
1219     QMap<int, int> updatedKfrs;
1220     QList<int> toDelete;
1221     if (*it != m_keyframes.first() && it != m_keyframes.constEnd()) {
1222         m_keyframesOrigin = m_keyframes;
1223         it--;
1224         int updatedLength = qFuzzyIsNull(speed) ? 0 : (m_currentKeyframe.second - it.value()) * 100. / speed;
1225         int offset = it.key() + updatedLength - m_currentKeyframe.first;
1226         m_keyframes.remove(m_currentKeyframe.first);
1227         m_currentKeyframe.first = it.key() + updatedLength;
1228         m_keyframes.insert(m_currentKeyframe.first, m_currentKeyframe.second);
1229         m_bottomPosition = m_currentKeyframe.first;
1230         m_selectedKeyframes.clear();
1231         m_selectedKeyframes.insert(m_currentKeyframe.first, m_currentKeyframe.second);
1232         it = m_keyframes.constFind(m_currentKeyframe.first);
1233         if (*it != m_keyframes.last()) {
1234             it++;
1235             // Update all keyframes after that so that we don't alter the speeds
1236             while (m_moveNext && it != m_keyframes.constEnd()) {
1237                 toDelete << it.key();
1238                 updatedKfrs.insert(it.key() + offset, it.value());
1239                 it++;
1240             }
1241         }
1242         for (int p : qAsConst(toDelete)) {
1243             m_keyframes.remove(p);
1244         }
1245         QMapIterator<int, int> i(updatedKfrs);
1246         while (i.hasNext()) {
1247             i.next();
1248             m_keyframes.insert(i.key(), i.value());
1249         }
1250         int maxWidth = width() - (2 * m_offset);
1251         m_scale = maxWidth / double(qMax(1, remapMax()));
1252         m_zoomStart = m_zoomHandle.x() * maxWidth;
1253         m_zoomFactor = maxWidth / (m_zoomHandle.y() * maxWidth - m_zoomStart);
1254         emit updateKeyframesWithUndo(m_keyframes, m_keyframesOrigin);
1255         update();
1256     }
1257 }
1258 
1259 void RemapView::updateAfterSpeed(double speed)
1260 {
1261     QMutexLocker lock(&m_kfrMutex);
1262     QMap<int, int>::const_iterator it = m_keyframes.constFind(m_currentKeyframe.first);
1263     if (*it != m_keyframes.last()) {
1264         m_keyframesOrigin = m_keyframes;
1265         it++;
1266         QMap<int, int> updatedKfrs;
1267         QList<int> toDelete;
1268         int updatedLength = qFuzzyIsNull(speed) ? 0 : (it.value() - m_currentKeyframe.second) * 100. / speed;
1269         int offset = m_currentKeyframe.first + updatedLength - it.key();
1270         if (m_moveNext) {
1271             while (it != m_keyframes.constEnd()) {
1272                 toDelete << it.key();
1273                 updatedKfrs.insert(it.key() + offset, it.value());
1274                 it++;
1275             }
1276         } else {
1277             m_keyframes.insert(m_currentKeyframe.first + updatedLength, it.value());
1278             m_keyframes.remove(it.key());
1279         }
1280         // Update all keyframes after that so that we don't alter the speeds
1281         for (int p : qAsConst(toDelete)) {
1282             m_keyframes.remove(p);
1283         }
1284         QMapIterator<int, int> i(updatedKfrs);
1285         while (i.hasNext()) {
1286             i.next();
1287             m_keyframes.insert(i.key(), i.value());
1288         }
1289         int maxWidth = width() - (2 * m_offset);
1290         m_scale = maxWidth / double(qMax(1, remapMax()));
1291         m_zoomStart = m_zoomHandle.x() * maxWidth;
1292         m_zoomFactor = maxWidth / (m_zoomHandle.y() * maxWidth - m_zoomStart);
1293         emit updateKeyframesWithUndo(m_keyframes, m_keyframesOrigin);
1294         update();
1295     }
1296 }
1297 
1298 const QString RemapView::getKeyframesData(QMap<int, int> keyframes) const
1299 {
1300     QStringList result;
1301     if (keyframes.isEmpty()) {
1302         keyframes = m_keyframes;
1303     }
1304     QMapIterator<int, int> i(keyframes);
1305     int offset = 0;
1306     while (i.hasNext()) {
1307         i.next();
1308         if (i.key() == keyframes.lastKey()) {
1309             // HACK: we always set last keyframe 1 frame after in MLT to ensure we have a correct last frame
1310             offset = 1;
1311         }
1312         Mlt::Properties props;
1313         props.set("_profile", pCore->getProjectProfile()->get_profile(), 0);
1314         result << QString("%1=%2").arg(props.frames_to_time(i.key() + offset, mlt_time_clock)).arg(GenTime(i.value(), pCore->getCurrentFps()).seconds());
1315     }
1316     return result.join(QLatin1Char(';'));
1317 }
1318 
1319 void RemapView::reloadProducer()
1320 {
1321     if (!m_clip || !m_clip->clipUrl().endsWith(QLatin1String(".mlt"))) {
1322         qDebug() << "==== this is not a playlist clip, aborting";
1323         return;
1324     }
1325     Mlt::Consumer c(pCore->getCurrentProfile()->profile(), "xml", m_clip->clipUrl().toUtf8().constData());
1326     QScopedPointer<Mlt::Service> serv(m_clip->originalProducer()->producer());
1327     if (serv == nullptr) {
1328         return;
1329     }
1330     qDebug() << "==== GOR PLAYLIST SERVICE: " << serv->type() << " / " << serv->consumer()->type() << ", SAVING TO " << m_clip->clipUrl();
1331     Mlt::Multitrack s2(*serv.data());
1332     qDebug() << "==== MULTITRACK: " << s2.count();
1333     Mlt::Tractor s(pCore->getCurrentProfile()->profile());
1334     s.set_track(*s2.track(0), 0);
1335     qDebug() << "==== GOT TRACKS: " << s.count();
1336     int ignore = s.get_int("ignore_points");
1337     if (ignore) {
1338         s.set("ignore_points", 0);
1339     }
1340     c.connect(s);
1341     c.set("time_format", "frames");
1342     c.set("no_meta", 1);
1343     c.set("no_root", 1);
1344     // c.set("no_profile", 1);
1345     c.set("root", "/");
1346     c.set("store", "kdenlive");
1347     c.run();
1348     if (ignore) {
1349         s.set("ignore_points", ignore);
1350     }
1351 }
1352 
1353 std::pair<double, double> RemapView::getSpeed(std::pair<int, int> kf)
1354 {
1355     std::pair<double, double> speeds = {-1, -1};
1356     QMap<int, int>::const_iterator it = m_keyframes.constFind(kf.first);
1357     if (it == m_keyframes.constEnd()) {
1358         // Not a keyframe
1359         return speeds;
1360     }
1361     if (*it != m_keyframes.first()) {
1362         it--;
1363         speeds.first = (double)(kf.second - it.value()) / (kf.first - it.key());
1364         it++;
1365     }
1366     if (*it != m_keyframes.last()) {
1367         it++;
1368         speeds.second = (double)(kf.second - it.value()) / (kf.first - it.key());
1369     }
1370     return speeds;
1371 }
1372 
1373 void RemapView::addKeyframe()
1374 {
1375     // insert or remove keyframe at interpolated position
1376     m_keyframesOrigin = m_keyframes;
1377     if (m_keyframes.contains(m_bottomPosition + m_inFrame)) {
1378         m_keyframes.remove(m_bottomPosition + m_inFrame);
1379         if (m_currentKeyframe.first == m_bottomPosition + m_inFrame) {
1380             m_currentKeyframe = m_currentKeyframeOriginal = {-1, -1};
1381             emit selectedKf(m_currentKeyframe, {-1, -1});
1382         }
1383         emit atKeyframe(false, false);
1384         emit updateKeyframesWithUndo(m_keyframes, m_keyframesOrigin);
1385         update();
1386         return;
1387     }
1388     QMapIterator<int, int> i(m_keyframes);
1389     std::pair<int, int> newKeyframe = {-1, -1};
1390     std::pair<int, int> previous = {-1, -1};
1391     newKeyframe.first = m_bottomPosition + m_inFrame;
1392     while (i.hasNext()) {
1393         i.next();
1394         if (i.key() > m_bottomPosition + m_inFrame) {
1395             if (i.key() == m_keyframes.firstKey()) {
1396                 // This is the first keyframe
1397                 double ratio = (double)(m_bottomPosition + m_inFrame) / i.key();
1398                 newKeyframe.second = i.value() * ratio;
1399                 break;
1400             } else if (previous.first > -1) {
1401                 std::pair<int, int> current = {i.key(), i.value()};
1402                 double ratio = (double)(m_bottomPosition + m_inFrame - previous.first) / (current.first - previous.first);
1403                 qDebug() << "=== RATIO: " << ratio;
1404                 newKeyframe.second = previous.second + (qAbs(current.second - previous.second) * ratio);
1405                 break;
1406             }
1407         }
1408         previous = {i.key(), i.value()};
1409     }
1410     if (newKeyframe.second == -1) {
1411         // We are after the last keyframe
1412         if (m_keyframes.isEmpty()) {
1413             newKeyframe.second = m_position + m_inFrame;
1414         } else {
1415             double ratio = (double)(m_position + m_inFrame - m_keyframes.lastKey()) / (m_duration - m_keyframes.lastKey());
1416             newKeyframe.second = m_keyframes.value(m_keyframes.lastKey()) + (qAbs(m_duration - m_keyframes.value(m_keyframes.lastKey())) * ratio);
1417         }
1418     }
1419     m_keyframes.insert(newKeyframe.first, newKeyframe.second);
1420     m_currentKeyframe = newKeyframe;
1421     m_selectedKeyframes = {m_currentKeyframe};
1422     std::pair<double, double> speeds = getSpeed(m_currentKeyframe);
1423     std::pair<bool, bool> atEnd = {m_currentKeyframe.first == m_inFrame, m_currentKeyframe.first == m_keyframes.lastKey()};
1424     emit selectedKf(m_currentKeyframe, speeds, atEnd);
1425     bool isLast = m_currentKeyframe.first == m_keyframes.firstKey() || m_currentKeyframe.first == m_keyframes.lastKey();
1426     emit atKeyframe(true, isLast);
1427     emit updateKeyframesWithUndo(m_keyframes, m_keyframesOrigin);
1428     update();
1429 }
1430 
1431 void RemapView::toggleMoveNext(bool moveNext)
1432 {
1433     m_moveNext = moveNext;
1434     // Reset keyframe selection
1435     m_selectedKeyframes.clear();
1436 }
1437 
1438 void RemapView::refreshOnDurationChanged(int remapDuration)
1439 {
1440     if (remapDuration != m_duration) {
1441         m_duration = qMax(remapDuration, remapMax());
1442         int maxWidth = width() - (2 * m_offset);
1443         m_scale = maxWidth / double(qMax(1, remapMax()));
1444         m_zoomStart = m_zoomHandle.x() * maxWidth;
1445         m_zoomFactor = maxWidth / (m_zoomHandle.y() * maxWidth - m_zoomStart);
1446     }
1447 }
1448 
1449 void RemapView::resizeEvent(QResizeEvent *event)
1450 {
1451     int maxWidth = width() - (2 * m_offset);
1452     m_scale = maxWidth / double(qMax(1, remapMax()));
1453     m_zoomStart = m_zoomHandle.x() * maxWidth;
1454     m_zoomFactor = maxWidth / (m_zoomHandle.y() * maxWidth - m_zoomStart);
1455     QWidget::resizeEvent(event);
1456     update();
1457 }
1458 
1459 void RemapView::paintEvent(QPaintEvent *event)
1460 {
1461     Q_UNUSED(event)
1462     QPalette pal = palette();
1463     KColorScheme scheme(pal.currentColorGroup(), KColorScheme::Window);
1464     m_colSelected = palette().highlight().color();
1465     m_colKeyframe = scheme.foreground(KColorScheme::NormalText).color();
1466     QColor bg = scheme.background(KColorScheme::AlternateBackground).color();
1467     QStylePainter p(this);
1468     int maxWidth = width() - (2 * m_offset);
1469     int zoomEnd = qCeil(m_zoomHandle.y() * maxWidth);
1470     // Top timeline
1471     p.fillRect(m_offset, 0, maxWidth + 1, m_centerPos, bg);
1472     // Bottom timeline
1473     p.fillRect(m_offset, m_bottomView - m_centerPos, maxWidth + 1, m_centerPos, bg);
1474     /* ticks */
1475     double fps = pCore->getCurrentFps();
1476     int maxLength = remapMax();
1477     if (maxLength == 0) {
1478         return;
1479     }
1480     int displayedLength = int(maxLength / m_zoomFactor / fps);
1481     double factor = 1;
1482     if (displayedLength < 2) {
1483         // 1 frame tick
1484     } else if (displayedLength < 30) {
1485         // 1 sec tick
1486         factor = fps;
1487     } else if (displayedLength < 150) {
1488         // 5 sec tick
1489         factor = 5 * fps;
1490     } else if (displayedLength < 300) {
1491         // 10 sec tick
1492         factor = 10 * fps;
1493     } else if (displayedLength < 900) {
1494         // 30 sec tick
1495         factor = 30 * fps;
1496     } else if (displayedLength < 1800) {
1497         // 1 min. tick
1498         factor = 60 * fps;
1499     } else if (displayedLength < 9000) {
1500         // 5 min tick
1501         factor = 300 * fps;
1502     } else if (displayedLength < 18000) {
1503         // 10 min tick
1504         factor = 600 * fps;
1505     } else {
1506         // 30 min tick
1507         factor = 1800 * fps;
1508     }
1509 
1510     // Position of left border in frames
1511     double tickOffset = m_zoomStart * m_zoomFactor;
1512     double frameSize = factor * m_scale * m_zoomFactor;
1513     int base = int(tickOffset / frameSize);
1514     tickOffset = frameSize - (tickOffset - (base * frameSize));
1515     // Draw frame ticks
1516     for (int i = 0; i < maxWidth / frameSize; i++) {
1517         int scaledTick = int(m_offset + (i * frameSize) + tickOffset);
1518         if (scaledTick >= maxWidth + m_offset) {
1519             break;
1520         }
1521         p.drawLine(QPointF(scaledTick, m_lineHeight + 1), QPointF(scaledTick, m_lineHeight - 3));
1522         p.drawLine(QPointF(scaledTick, m_bottomView - m_lineHeight + 1), QPointF(scaledTick, m_bottomView - m_lineHeight - 3));
1523     }
1524 
1525     /*
1526      * Time-"lines"
1527      * We have a top timeline for the source (clip monitor) and a bottom timeline for the output (project monitor)
1528      */
1529     p.setPen(m_colKeyframe);
1530     // Top timeline
1531     // qDebug()<<"=== MAX KFR WIDTH: "<<maxWidth<<", DURATION SCALED: "<<(m_duration * m_scale)<<", POS: "<<(m_position * m_scale);
1532     int maxPos = maxWidth + m_offset - 1;
1533     p.drawLine(m_offset, m_lineHeight, maxPos, m_lineHeight);
1534     p.drawLine(m_offset, m_lineHeight - m_lineHeight / 4, m_offset, m_lineHeight + m_lineHeight / 4);
1535     p.drawLine(maxPos, m_lineHeight - m_lineHeight / 4, maxPos, m_lineHeight + m_lineHeight / 4);
1536     // Bottom timeline
1537     p.drawLine(m_offset, m_bottomView - m_lineHeight, maxPos, m_bottomView - m_lineHeight);
1538     p.drawLine(m_offset, m_bottomView - m_lineHeight - m_lineHeight / 4, m_offset, m_bottomView - m_lineHeight + m_lineHeight / 4);
1539     p.drawLine(maxPos, m_bottomView - m_lineHeight - m_lineHeight / 4, maxPos, m_bottomView - m_lineHeight + m_lineHeight / 4);
1540 
1541     /*
1542      * Original clip in/out
1543      */
1544     p.setPen(palette().mid().color());
1545     double inPos = (double)(m_originalRange.first - m_inFrame) * m_scale;
1546     double outPos = (double)(m_originalRange.second - m_inFrame) * m_scale;
1547     inPos -= m_zoomStart;
1548     inPos *= m_zoomFactor;
1549     outPos -= m_zoomStart;
1550     outPos *= m_zoomFactor;
1551     if (inPos >= 0) {
1552         inPos += m_offset;
1553         p.drawLine(inPos, m_lineHeight, inPos, m_bottomView - m_lineHeight);
1554     }
1555     if (outPos <= maxWidth) {
1556         outPos += m_offset;
1557         p.drawLine(outPos, m_lineHeight, outPos, m_bottomView - m_lineHeight);
1558     }
1559 
1560     /*
1561      * Keyframes
1562      */
1563     QMapIterator<int, int> i(m_keyframes);
1564     while (i.hasNext()) {
1565         i.next();
1566         double kfOutPos = (double)(i.key() - m_inFrame) * m_scale;
1567         double kfInPos = (double)(i.value() - m_inFrame) * m_scale;
1568         if ((kfInPos < m_zoomStart && kfOutPos < m_zoomStart) || (qFloor(kfInPos) > zoomEnd && qFloor(kfOutPos) > zoomEnd)) {
1569             continue;
1570         }
1571         if (m_currentKeyframe.first == i.key()) {
1572             p.setPen(Qt::red);
1573             p.setBrush(Qt::darkRed);
1574         } else if (m_selectedKeyframes.contains(i.key())) {
1575             p.setPen(m_colSelected);
1576             p.setBrush(m_colSelected);
1577         } else {
1578             p.setPen(m_colKeyframe);
1579             p.setBrush(m_colKeyframe);
1580         }
1581         kfInPos -= m_zoomStart;
1582         kfInPos *= m_zoomFactor;
1583         kfInPos += m_offset;
1584         kfOutPos -= m_zoomStart;
1585         kfOutPos *= m_zoomFactor;
1586         kfOutPos += m_offset;
1587 
1588         p.drawLine(kfInPos, m_lineHeight + m_lineHeight * 0.75, kfOutPos, m_bottomView - m_lineHeight * 1.75);
1589         p.drawLine(kfInPos, m_lineHeight, kfInPos, m_lineHeight + m_lineHeight / 2);
1590         p.drawLine(kfOutPos, m_bottomView - m_lineHeight, kfOutPos, m_bottomView - m_lineHeight * 1.5);
1591         p.drawEllipse(QRectF(kfInPos - m_lineHeight / 4.0, m_lineHeight + m_lineHeight / 2, m_lineHeight / 2, m_lineHeight / 2));
1592         p.drawEllipse(QRectF(kfOutPos - m_lineHeight / 4.0, m_bottomView - 2 * m_lineHeight, m_lineHeight / 2, m_lineHeight / 2));
1593     }
1594 
1595     /*
1596      * current position cursor
1597      */
1598     p.setPen(m_colSelected);
1599     // Top seek cursor
1600     if (m_position >= 0 && m_position < m_duration) {
1601         p.setBrush(m_colSelected);
1602         double scaledPos = m_position * m_scale;
1603         scaledPos -= m_zoomStart;
1604         scaledPos *= m_zoomFactor;
1605         scaledPos += m_offset;
1606         if (scaledPos >= m_offset && qFloor(scaledPos) <= m_offset + maxWidth) {
1607             QPolygonF topCursor;
1608             topCursor << QPointF(-int(m_lineHeight / 3), -m_lineHeight * 0.5) << QPointF(int(m_lineHeight / 3), -m_lineHeight * 0.5) << QPointF(0, 0);
1609             topCursor.translate(scaledPos, m_lineHeight);
1610             p.drawPolygon(topCursor);
1611         }
1612     }
1613 
1614     if (m_bottomPosition >= 0 && m_bottomPosition < m_duration) {
1615         p.setBrush(m_colSelected);
1616         double scaledPos = -1;
1617         if (m_remapLink && !m_keyframes.isEmpty()) {
1618             int topPos = GenTime(m_remapLink->anim_get_double("map", m_bottomPosition + m_inFrame)).frames(pCore->getCurrentFps()) - m_inFrame;
1619             scaledPos = topPos * m_scale;
1620             scaledPos -= m_zoomStart;
1621             scaledPos *= m_zoomFactor;
1622             scaledPos += m_offset;
1623         }
1624         double scaledPos2 = m_bottomPosition * m_scale;
1625         scaledPos2 -= m_zoomStart;
1626         scaledPos2 *= m_zoomFactor;
1627         scaledPos2 += m_offset;
1628         if (scaledPos2 >= m_offset && qFloor(scaledPos2) <= m_offset + maxWidth) {
1629             QPolygonF bottomCursor;
1630             bottomCursor << QPointF(-int(m_lineHeight / 3), m_lineHeight * 0.5) << QPointF(int(m_lineHeight / 3), m_lineHeight * 0.5) << QPointF(0, 0);
1631             bottomCursor.translate(scaledPos2, m_bottomView - m_lineHeight);
1632             p.setBrush(m_colSelected);
1633             p.drawPolygon(bottomCursor);
1634         }
1635         if (scaledPos > -1) {
1636             p.drawLine(scaledPos, m_lineHeight * 1.75, scaledPos2, m_bottomView - (m_lineHeight * 1.75));
1637             p.drawLine(scaledPos, m_lineHeight, scaledPos, m_lineHeight * 1.75);
1638         }
1639         p.drawLine(scaledPos2, m_bottomView - m_lineHeight, scaledPos2, m_bottomView - m_lineHeight * 1.75);
1640     }
1641 
1642     // Zoom bar
1643     p.setPen(Qt::NoPen);
1644     p.setBrush(palette().mid());
1645     p.drawRoundedRect(0, m_bottomView + 2, width() - 2 * 0, m_zoomHeight, m_lineHeight / 3, m_lineHeight / 3);
1646     p.setBrush(palette().highlight());
1647     p.drawRoundedRect(int((width()) * m_zoomHandle.x()), m_bottomView + 2, int((width()) * (m_zoomHandle.y() - m_zoomHandle.x())), m_zoomHeight,
1648                       m_lineHeight / 3, m_lineHeight / 3);
1649 }
1650 
1651 TimeRemap::TimeRemap(QWidget *parent)
1652     : QWidget(parent)
1653     , m_cid(-1)
1654 {
1655     setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont));
1656     setupUi(this);
1657     warningMessage->hide();
1658     QAction *ac = new QAction(i18n("Transcode"), this);
1659     warningMessage->addAction(ac);
1660     connect(ac, &QAction::triggered, this, [&]() {
1661         QMetaObject::invokeMethod(pCore->bin(), "requestTranscoding", Qt::QueuedConnection, Q_ARG(QString, QString()), Q_ARG(QString, m_binId), Q_ARG(int, 0),
1662                                   Q_ARG(bool, false));
1663     });
1664     m_view = new RemapView(this);
1665     speedBefore->setKeyboardTracking(false);
1666     speedAfter->setKeyboardTracking(false);
1667     remapLayout->addWidget(m_view);
1668     connect(m_view, &RemapView::selectedKf, this, [this](std::pair<int, int> selection, std::pair<double, double> speeds, std::pair<bool, bool> atEnd) {
1669         info_frame->setEnabled(selection.first > -1);
1670         QSignalBlocker bk(m_in);
1671         QSignalBlocker bk2(m_out);
1672         m_in->setValue(selection.second - m_view->m_inFrame);
1673         m_out->setValue(selection.first - m_view->m_inFrame);
1674         QSignalBlocker bk3(speedBefore);
1675         QSignalBlocker bk4(speedAfter);
1676         speedBefore->setEnabled(!atEnd.first);
1677         speedBefore->setValue(100. * speeds.first);
1678         speedAfter->setEnabled(!atEnd.second);
1679         speedAfter->setValue(100. * speeds.second);
1680     });
1681     connect(m_view, &RemapView::updateSpeeds, this, [this](std::pair<double, double> speeds) {
1682         QSignalBlocker bk3(speedBefore);
1683         QSignalBlocker bk4(speedAfter);
1684         speedBefore->setEnabled(speeds.first > 0);
1685         speedBefore->setValue(100. * speeds.first);
1686         speedAfter->setEnabled(speeds.second > 0);
1687         speedAfter->setValue(100. * speeds.second);
1688     });
1689     button_add->setIcon(QIcon::fromTheme(QStringLiteral("keyframe-add")));
1690     button_add->setToolTip(i18n("Add keyframe"));
1691     button_next->setIcon(QIcon::fromTheme(QStringLiteral("keyframe-next")));
1692     button_next->setToolTip(i18n("Go to next keyframe"));
1693     button_prev->setIcon(QIcon::fromTheme(QStringLiteral("keyframe-previous")));
1694     button_prev->setToolTip(i18n("Go to previous keyframe"));
1695     connect(m_view, &RemapView::updateKeyframes, this, &TimeRemap::updateKeyframes);
1696     connect(m_view, &RemapView::updateKeyframesWithUndo, this, &TimeRemap::updateKeyframesWithUndo);
1697     connect(m_in, &TimecodeDisplay::timeCodeUpdated, this, [this]() { m_view->updateInPos(m_in->getValue() + m_view->m_inFrame); });
1698     button_center->setToolTip(i18n("Move selected keyframe to cursor"));
1699     button_center_top->setToolTip(i18n("Move selected keyframe to cursor"));
1700     connect(m_out, &TimecodeDisplay::timeCodeUpdated, this, [this]() { m_view->updateOutPos(m_out->getValue() + m_view->m_inFrame); });
1701     connect(button_center, &QToolButton::clicked, m_view, &RemapView::centerCurrentKeyframe);
1702     connect(button_center_top, &QToolButton::clicked, m_view, &RemapView::centerCurrentTopKeyframe);
1703     connect(m_view, &RemapView::atKeyframe, button_add, [&](bool atKeyframe, bool last) {
1704         if (atKeyframe) {
1705             button_add->setIcon(QIcon::fromTheme(QStringLiteral("keyframe-remove")));
1706             button_add->setToolTip(i18n("Delete keyframe"));
1707         } else {
1708             button_add->setIcon(QIcon::fromTheme(QStringLiteral("keyframe-add")));
1709             button_add->setToolTip(i18n("Add keyframe"));
1710         }
1711         button_add->setEnabled(!atKeyframe || !last);
1712     });
1713     connect(speedBefore, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, [&](double speed) { m_view->updateBeforeSpeed(speed); });
1714     connect(speedAfter, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, [&](double speed) { m_view->updateAfterSpeed(speed); });
1715     connect(button_del, &QToolButton::clicked, this, [this]() {
1716         if (m_cid > -1) {
1717             std::shared_ptr<TimelineItemModel> model = pCore->window()->getCurrentTimeline()->model();
1718             model->requestClipTimeRemap(m_cid, false);
1719             selectedClip(-1);
1720         }
1721     });
1722     connect(button_add, &QToolButton::clicked, m_view, &RemapView::addKeyframe);
1723     connect(button_next, &QToolButton::clicked, m_view, &RemapView::goNext);
1724     connect(button_prev, &QToolButton::clicked, m_view, &RemapView::goPrev);
1725     connect(move_next, &QCheckBox::toggled, m_view, &RemapView::toggleMoveNext);
1726     connect(pitch_compensate, &QCheckBox::toggled, this, &TimeRemap::switchRemapParam);
1727     connect(frame_blending, &QCheckBox::toggled, this, &TimeRemap::switchRemapParam);
1728     connect(m_view, &RemapView::updateMaxDuration, this, [this]() {
1729         m_out->setRange(0, INT_MAX);
1730         // m_in->setRange(0, duration - 1);
1731     });
1732     remap_box->setEnabled(false);
1733 }
1734 
1735 const QString &TimeRemap::currentClip() const
1736 {
1737     return m_binId;
1738 }
1739 
1740 void TimeRemap::checkClipUpdate(const QModelIndex &topLeft, const QModelIndex &, const QVector<int> &roles)
1741 {
1742     int id = int(topLeft.internalId());
1743     if (m_cid != id || !roles.contains(TimelineModel::FinalMoveRole)) {
1744         return;
1745     }
1746     // Don't resize view if we are moving a keyframe
1747     if (!m_view->movingKeyframe()) {
1748         int newDuration = pCore->getItemDuration({ObjectType::TimelineClip, m_cid});
1749         // Check if the keyframes were modified by an external resize operation
1750         std::shared_ptr<TimelineItemModel> model = pCore->window()->getCurrentTimeline()->model();
1751         std::shared_ptr<ClipModel> clip = model->getClipPtr(m_cid);
1752         QMap<QString, QString> values = clip->getRemapValues();
1753         if (values.value(QLatin1String("map")) == m_view->getKeyframesData()) {
1754             // Resize was triggered by our keyframe move, nothing to do
1755             return;
1756         }
1757         // Reload keyframes
1758         m_lastLength = newDuration;
1759         m_view->m_remapLink->set("map", values.value(QLatin1String("map")).toUtf8().constData());
1760         int min = pCore->getItemIn({ObjectType::TimelineClip, m_cid});
1761         m_view->m_startPos = pCore->getItemPosition({ObjectType::TimelineClip, m_cid});
1762         m_in->setRange(0, m_view->m_maxLength - min);
1763         m_out->setRange(0, INT_MAX);
1764         m_view->loadKeyframes(values.value(QLatin1String("map")));
1765         m_view->update();
1766     }
1767 }
1768 
1769 void TimeRemap::selectedClip(int cid)
1770 {
1771     if (cid == -1 && cid == m_cid) {
1772         warningMessage->hide();
1773         return;
1774     }
1775     QObject::disconnect(m_seekConnection1);
1776     QObject::disconnect(m_seekConnection2);
1777     QObject::disconnect(m_seekConnection3);
1778     connect(pCore->getMonitor(Kdenlive::ClipMonitor), &Monitor::seekRemap, m_view, &RemapView::slotSetPosition, Qt::UniqueConnection);
1779     std::shared_ptr<TimelineItemModel> model = pCore->window()->getCurrentTimeline()->model();
1780     disconnect(model.get(), &TimelineItemModel::dataChanged, this, &TimeRemap::checkClipUpdate);
1781     m_cid = cid;
1782     if (cid == -1) {
1783         m_binId.clear();
1784         m_view->setDuration(nullptr, -1);
1785         remap_box->setEnabled(false);
1786         return;
1787     }
1788     m_binId = model->getClipBinId(cid);
1789     std::shared_ptr<Mlt::Producer> prod = model->getClipProducer(cid);
1790     // Check for B Frames and warn
1791     if (prod->parent().get_int("meta.media.has_b_frames") == 1) {
1792         m_view->setDuration(nullptr, -1);
1793         remap_box->setEnabled(false);
1794         warningMessage->setText(i18n("Time remap does not work on clip with B frames."));
1795         warningMessage->animatedShow();
1796         return;
1797     } else {
1798         warningMessage->hide();
1799         remap_box->setEnabled(true);
1800     }
1801     m_view->m_remapLink.reset();
1802     m_splitId = model->m_groups->getSplitPartner(cid);
1803     m_lastLength = pCore->getItemDuration({ObjectType::TimelineClip, cid});
1804     m_view->m_startPos = pCore->getItemPosition({ObjectType::TimelineClip, cid});
1805     model->requestClipTimeRemap(cid);
1806     connect(model.get(), &TimelineItemModel::dataChanged, this, &TimeRemap::checkClipUpdate);
1807     m_view->m_maxLength = prod->get_length();
1808     m_in->setRange(0, m_view->m_maxLength - prod->get_in());
1809     // m_in->setRange(0, m_lastLength - 1);
1810     m_out->setRange(0, INT_MAX);
1811     m_view->setDuration(prod, m_lastLength, prod->parent().get_length());
1812     qDebug() << "===== GOT PRODUCER TYPE: " << prod->parent().type();
1813     if (prod->parent().type() == mlt_service_chain_type) {
1814         Mlt::Chain fromChain(prod->parent());
1815         int count = fromChain.link_count();
1816         for (int i = 0; i < count; i++) {
1817             QScopedPointer<Mlt::Link> fromLink(fromChain.link(i));
1818             if (fromLink && fromLink->is_valid() && fromLink->get("mlt_service")) {
1819                 if (fromLink->get("mlt_service") == QLatin1String("timeremap")) {
1820                     // Found a timeremap effect, read params
1821                     m_view->m_remapLink = std::make_shared<Mlt::Link>(fromChain.link(i)->get_link());
1822                     if (m_splitId > -1) {
1823                         std::shared_ptr<Mlt::Producer> prod2 = model->getClipProducer(m_splitId);
1824                         if (prod2->parent().type() == mlt_service_chain_type) {
1825                             Mlt::Chain fromChain2(prod2->parent());
1826                             count = fromChain2.link_count();
1827                             for (int j = 0; j < count; j++) {
1828                                 QScopedPointer<Mlt::Link> fromLink2(fromChain2.link(j));
1829                                 if (fromLink2 && fromLink2->is_valid() && fromLink2->get("mlt_service")) {
1830                                     if (fromLink2->get("mlt_service") == QLatin1String("timeremap")) {
1831                                         m_splitRemap = std::make_shared<Mlt::Link>(fromChain2.link(j)->get_link());
1832                                     }
1833                                 }
1834                             }
1835                         }
1836                     }
1837                     QString mapData(fromLink->get("map"));
1838                     m_view->loadKeyframes(mapData);
1839                     if (mapData.isEmpty()) {
1840                         // We are just adding the remap effect, set default params
1841                         fromLink->set("pitch", 1);
1842                         fromLink->set("image_mode", "nearest");
1843                     }
1844                     QSignalBlocker bk(pitch_compensate);
1845                     QSignalBlocker bk2(frame_blending);
1846                     pitch_compensate->setChecked(fromLink->get_int("pitch") == 1);
1847                     frame_blending->setChecked(fromLink->get("image_mode") != QLatin1String("nearest"));
1848                     remap_box->setEnabled(true);
1849                     break;
1850                 }
1851             }
1852         }
1853     } else {
1854         qDebug() << "/// PRODUCER IS NOT A CHAIN!!!!";
1855     }
1856     if (!m_binId.isEmpty() && pCore->getMonitor(Kdenlive::ClipMonitor)->activeClipId() == m_binId) {
1857         connect(pCore->getMonitor(Kdenlive::ClipMonitor), &Monitor::seekPosition, pCore->getMonitor(Kdenlive::ClipMonitor), &Monitor::seekRemap,
1858                 Qt::UniqueConnection);
1859     }
1860     m_seekConnection1 = connect(m_view, &RemapView::seekToPos, this, [this](int topPos, int bottomPos) {
1861         if (topPos > -1) {
1862             if (pCore->getMonitor(Kdenlive::ClipMonitor)->activeClipId() != m_binId) {
1863                 int min = pCore->getItemIn({ObjectType::TimelineClip, m_cid});
1864                 int lastLength = pCore->getItemDuration({ObjectType::TimelineClip, m_cid});
1865                 int max = min + lastLength;
1866                 pCore->selectBinClip(m_binId, true, min, {min, max});
1867             }
1868             pCore->getMonitor(Kdenlive::ClipMonitor)->requestSeekIfVisible(topPos);
1869         }
1870         if (bottomPos > -1) {
1871             pCore->getMonitor(Kdenlive::ProjectMonitor)->requestSeek(bottomPos + m_view->m_startPos);
1872         }
1873     });
1874     m_seekConnection2 = connect(pCore->getMonitor(Kdenlive::ProjectMonitor), &Monitor::seekPosition, this,
1875                                 [this](int pos) { m_view->slotSetBottomPosition(pos - m_view->m_startPos); });
1876 }
1877 
1878 void TimeRemap::setClip(std::shared_ptr<ProjectClip> clip, int in, int out)
1879 {
1880     if (m_cid > -1 && clip == nullptr) {
1881         return;
1882     }
1883     QObject::disconnect(m_seekConnection1);
1884     QObject::disconnect(m_seekConnection2);
1885     QObject::disconnect(m_seekConnection3);
1886     m_cid = -1;
1887     m_binId.clear();
1888     if (clip == nullptr || !clip->statusReady() || clip->clipType() != ClipType::Playlist) {
1889         m_view->setDuration(nullptr, -1);
1890         remap_box->setEnabled(false);
1891         return;
1892     }
1893     m_view->m_remapLink.reset();
1894 
1895     bool keyframesLoaded = false;
1896     int min = in == -1 ? 0 : in;
1897     int max = out == -1 ? clip->getFramePlaytime() : out;
1898     m_in->setRange(0, max - min);
1899     m_out->setRange(min, INT_MAX);
1900     m_view->m_startPos = 0;
1901     m_view->setBinClipDuration(clip, max - min);
1902     if (clip->clipType() == ClipType::Playlist) {
1903         Mlt::Service service(clip->originalProducer()->producer()->get_service());
1904         qDebug() << "==== producer type: " << service.type();
1905         if (service.type() == mlt_service_multitrack_type) {
1906             Mlt::Multitrack multi(service);
1907             for (int i = 0; i < multi.count(); i++) {
1908                 std::unique_ptr<Mlt::Producer> track(multi.track(i));
1909                 qDebug() << "==== GOT TRACK TYPE: " << track->type();
1910                 switch (track->type()) {
1911                 case mlt_service_chain_type: {
1912                     Mlt::Chain fromChain(*track.get());
1913                     int count = fromChain.link_count();
1914                     for (int j = 0; j < count; j++) {
1915                         QScopedPointer<Mlt::Link> fromLink(fromChain.link(j));
1916                         if (fromLink && fromLink->is_valid() && fromLink->get("mlt_service")) {
1917                             if (fromLink->get("mlt_service") == QLatin1String("timeremap")) {
1918                                 // Found a timeremap effect, read params
1919                                 m_view->m_remapLink = std::make_shared<Mlt::Link>(fromChain.link(j)->get_link());
1920                                 QString mapData(fromLink->get("map"));
1921                                 m_view->loadKeyframes(mapData);
1922                                 keyframesLoaded = true;
1923                                 break;
1924                             }
1925                         }
1926                     }
1927                     break;
1928                 }
1929                 case mlt_service_playlist_type: {
1930                     // that is a single track
1931                     Mlt::Playlist local_playlist(*track);
1932                     int count = local_playlist.count();
1933                     qDebug() << "==== PLAYLIST COUNT: " << count;
1934                     if (count == 1) {
1935                         Mlt::Producer prod = local_playlist.get_clip(0)->parent();
1936                         qDebug() << "==== GOT PROD TYPE: " << prod.type() << " = " << prod.get("mlt_service") << " = " << prod.get("resource");
1937                         if (prod.type() == mlt_service_chain_type) {
1938                             Mlt::Chain fromChain(prod);
1939                             int linkCount = fromChain.link_count();
1940                             for (int j = 0; j < linkCount; j++) {
1941                                 QScopedPointer<Mlt::Link> fromLink(fromChain.link(j));
1942                                 if (fromLink && fromLink->is_valid() && fromLink->get("mlt_service")) {
1943                                     if (fromLink->get("mlt_service") == QLatin1String("timeremap")) {
1944                                         // Found a timeremap effect, read params
1945                                         m_view->m_remapLink = std::make_shared<Mlt::Link>(fromChain.link(j)->get_link());
1946                                         QString mapData(fromLink->get("map"));
1947                                         m_view->loadKeyframes(mapData);
1948                                         keyframesLoaded = true;
1949                                         break;
1950                                     }
1951                                 }
1952                             }
1953                         }
1954                     }
1955                     break;
1956                 }
1957                 default:
1958                     qDebug() << "=== UNHANDLED TRACK TYPE";
1959                     break;
1960                 }
1961             }
1962         }
1963     }
1964     if (!keyframesLoaded) {
1965         m_view->loadKeyframes(QString());
1966     }
1967     m_seekConnection1 = connect(m_view, &RemapView::seekToPos, pCore->getMonitor(Kdenlive::ClipMonitor), &Monitor::requestSeek, Qt::UniqueConnection);
1968     m_seekConnection2 = connect(pCore->getMonitor(Kdenlive::ClipMonitor), &Monitor::seekPosition, this, [&](int pos) { m_view->slotSetPosition(pos); });
1969     remap_box->setEnabled(m_view->m_remapLink != nullptr);
1970 }
1971 
1972 void TimeRemap::updateKeyframes()
1973 {
1974     QString kfData = m_view->getKeyframesData();
1975     if (m_view->m_remapLink) {
1976         m_view->m_remapLink->set("map", kfData.toUtf8().constData());
1977         if (m_splitRemap) {
1978             m_splitRemap->set("map", kfData.toUtf8().constData());
1979         }
1980         if (m_cid == -1) {
1981             // This is a playlist clip
1982             m_view->timer.start();
1983         }
1984     }
1985 }
1986 
1987 void TimeRemap::updateKeyframesWithUndo(const QMap<int, int> &updatedKeyframes, const QMap<int, int> &previousKeyframes)
1988 {
1989     if (m_view->m_remapLink == nullptr) {
1990         return;
1991     }
1992     bool usePitch = pitch_compensate->isChecked();
1993     bool useBlend = frame_blending->isChecked();
1994     bool hadPitch = m_view->m_remapLink->get_int("pitch") == 1;
1995     bool hadBlend = m_view->m_remapLink->get("image_mode") != QLatin1String("nearest");
1996     bool durationChanged = updatedKeyframes.isEmpty() ? false
1997                                                       : updatedKeyframes.lastKey() - pCore->getItemIn({ObjectType::TimelineClip, m_cid}) + 1 !=
1998                                                             pCore->getItemDuration({ObjectType::TimelineClip, m_cid});
1999     Fun undo = []() { return true; };
2000     Fun redo = []() { return true; };
2001     Fun local_undo = [this, link = m_view->m_remapLink, splitLink = m_splitRemap, previousKeyframes, cid = m_cid, oldIn = m_view->m_oldInFrame, hadPitch,
2002                       hadBlend]() {
2003         QString oldKfData;
2004         bool keyframesChanged = false;
2005         if (!previousKeyframes.isEmpty()) {
2006             oldKfData = m_view->getKeyframesData(previousKeyframes);
2007             keyframesChanged = true;
2008         }
2009         if (keyframesChanged) {
2010             link->set("map", oldKfData.toUtf8().constData());
2011         }
2012         link->set("pitch", hadPitch ? 1 : 0);
2013         link->set("image_mode", hadBlend ? "blend" : "nearest");
2014         if (splitLink) {
2015             if (keyframesChanged) {
2016                 splitLink->set("map", oldKfData.toUtf8().constData());
2017             }
2018             splitLink->set("pitch", hadPitch ? 1 : 0);
2019             splitLink->set("image_mode", hadBlend ? "blend" : "nearest");
2020         }
2021         if (cid == m_cid) {
2022             QSignalBlocker bk(pitch_compensate);
2023             QSignalBlocker bk2(frame_blending);
2024             pitch_compensate->setChecked(hadPitch);
2025             frame_blending->setChecked(hadBlend);
2026             if (keyframesChanged) {
2027                 m_lastLength = previousKeyframes.lastKey() - oldIn;
2028                 // This clip is currently displayed in remap view
2029                 m_view->m_remapLink->set("map", oldKfData.toUtf8().constData());
2030                 m_view->loadKeyframes(oldKfData);
2031                 update();
2032             }
2033         }
2034         return true;
2035     };
2036 
2037     Fun local_redo = [this, link = m_view->m_remapLink, splitLink = m_splitRemap, updatedKeyframes, cid = m_cid, usePitch, in = m_view->m_inFrame, useBlend]() {
2038         QString newKfData;
2039         bool keyframesChanged = false;
2040         if (!updatedKeyframes.isEmpty()) {
2041             newKfData = m_view->getKeyframesData(updatedKeyframes);
2042             keyframesChanged = true;
2043         }
2044         if (keyframesChanged) {
2045             link->set("map", newKfData.toUtf8().constData());
2046         }
2047         link->set("pitch", usePitch ? 1 : 0);
2048         link->set("image_mode", useBlend ? "blend" : "nearest");
2049         if (splitLink) {
2050             if (keyframesChanged) {
2051                 splitLink->set("map", newKfData.toUtf8().constData());
2052             }
2053             splitLink->set("pitch", usePitch ? 1 : 0);
2054             splitLink->set("image_mode", useBlend ? "blend" : "nearest");
2055         }
2056         if (cid == m_cid) {
2057             QSignalBlocker bk(pitch_compensate);
2058             QSignalBlocker bk2(frame_blending);
2059             pitch_compensate->setChecked(usePitch);
2060             frame_blending->setChecked(useBlend);
2061             if (keyframesChanged) {
2062                 // This clip is currently displayed in remap view
2063                 m_lastLength = updatedKeyframes.lastKey() - in;
2064                 m_view->m_remapLink->set("map", newKfData.toUtf8().constData());
2065                 m_view->loadKeyframes(newKfData);
2066                 update();
2067             }
2068         }
2069         return true;
2070     };
2071     local_redo();
2072     if (durationChanged) {
2073         int length = updatedKeyframes.lastKey() - m_view->m_inFrame + 1;
2074         std::shared_ptr<TimelineItemModel> model = pCore->window()->getCurrentTimeline()->model();
2075         model->requestItemResize(m_cid, length, true, true, undo, redo);
2076         if (m_splitId > 0) {
2077             model->requestItemResize(m_splitId, length, true, true, undo, redo);
2078         }
2079     }
2080     UPDATE_UNDO_REDO_NOLOCK(redo, undo, local_undo, local_redo);
2081     pCore->pushUndo(local_undo, local_redo, i18n("Edit Timeremap keyframes"));
2082 }
2083 
2084 void TimeRemap::switchRemapParam()
2085 {
2086     updateKeyframesWithUndo(QMap<int, int>(), QMap<int, int>());
2087 }
2088 
2089 bool TimeRemap::isInRange() const
2090 {
2091     return m_cid != -1 && m_view->isInRange();
2092 }
2093 
2094 TimeRemap::~TimeRemap()
2095 {
2096     // delete m_previewTimer;
2097 }