File indexing completed on 2024-04-21 04:51:27

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