1 /*
2     SPDX-FileCopyrightText: 2021 Jean-Baptiste Mardelle <jb@kdenlive.org>
3 
4 SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
5 */
6 
7 #include "timeremap.h"
8 
9 #include "core.h"
10 #include "doc/kthumb.h"
11 #include "timecodedisplay.h"
12 #include "kdenlivesettings.h"
13 #include "bin/projectclip.h"
14 #include "project/projectmanager.h"
15 #include "monitor/monitor.h"
16 #include "profiles/profilemodel.hpp"
17 #include "mainwindow.h"
18 #include "timeline2/view/timelinewidget.h"
19 #include "timeline2/view/timelinecontroller.h"
20 #include "timeline2/model/groupsmodel.hpp"
21 #include "timeline2/model/clipmodel.hpp"
22 #include "macros.hpp"
23 
24 #include "kdenlive_debug.h"
25 #include <QFontDatabase>
26 #include <QWheelEvent>
27 #include <QStylePainter>
28 #include <QtMath>
29 
30 #include <KColorScheme>
31 #include "klocalizedstring.h"
32 
RemapView(QWidget * parent)33 RemapView::RemapView(QWidget *parent)
34     : QWidget(parent)
35     , m_inFrame(0)
36     , m_duration(1)
37     , m_position(0)
38     , m_bottomPosition(0)
39     , m_scale(1.)
40     , m_zoomFactor(1)
41     , m_zoomStart(0)
42     , m_zoomHandle(0,1)
43     , m_clip(nullptr)
44     , m_service(nullptr)
45     , m_moveKeyframeMode(NoMove)
46     , m_clickPoint(-1)
47     , m_moveNext(true)
48 {
49     setMouseTracking(true);
50     setMinimumSize(QSize(150, 80));
51     setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont));
52     int size = QFontInfo(font()).pixelSize() * 3;
53     setFixedHeight(size * 4);
54     // Reference height of the rulers
55     m_lineHeight = int(size / 2.);
56     // Height of the zoom bar
57     m_zoomHeight = m_lineHeight * 0.5;
58     // Center of the view
59     m_centerPos = (size * 4 - m_zoomHeight - 2) / 2 - 1;
60     m_offset = qCeil(m_lineHeight / 4);
61     // Bottom of the view (just above zoombar)
62     m_bottomView = height() - m_zoomHeight - 2;
63     setSizePolicy(QSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed));
64     int maxWidth = width() - (2 * m_offset);
65     m_scale = 1.;
66     m_zoomStart = m_zoomHandle.x() * maxWidth;
67     m_zoomFactor = maxWidth / (m_zoomHandle.y() * maxWidth - m_zoomStart);
68     timer.setInterval(500);
69     timer.setSingleShot(true);
70     connect(&timer, &QTimer::timeout, this, &RemapView::reloadProducer);
71 }
72 
isInRange() const73 bool RemapView::isInRange() const
74 {
75     return m_bottomPosition != -1;
76 }
77 
updateInPos(int pos)78 void RemapView::updateInPos(int pos)
79 {
80     if (m_currentKeyframe.second > -1) {
81         if (m_moveNext) {
82             int offset = pos - m_currentKeyframe.second;
83             QMap<int, int>::iterator it = m_keyframes.find(m_currentKeyframe.first);
84             while (it != m_keyframes.end()) {
85                 m_keyframes.insert(it.key(), it.value() + offset);
86                 it++;
87             }
88             m_currentKeyframe.second = pos;
89         } else {
90             m_currentKeyframe.second = pos;
91             m_keyframes.insert(m_currentKeyframe.first, m_currentKeyframe.second);
92         }
93         slotSetPosition(pos);
94         std::pair<double,double>speeds = getSpeed(m_currentKeyframe);
95         emit updateSpeeds(speeds);
96         emit updateKeyframes(true);
97         update();
98     }
99 }
100 
updateOutPos(int pos)101 void RemapView::updateOutPos(int pos)
102 {
103     if (m_currentKeyframe.first > -1) {
104         if (m_keyframes.contains(pos)) {
105             // Cannot move kfr over an existing one
106             qDebug()<<"==== KEYFRAME ALREADY EXISTS AT: "<<pos;
107             return;
108         }
109         QMap<int,int>updated;
110         int offset = pos - m_currentKeyframe.first;
111         if (m_moveNext) {
112             QMap<int, int>::iterator it = m_keyframes.find(m_currentKeyframe.first);
113             while (it != m_keyframes.end()) {
114                 updated.insert(it.key(), it.value());
115                 it++;
116             }
117             m_currentKeyframe.first = pos;
118         } else {
119             m_keyframes.remove(m_currentKeyframe.first);
120             m_currentKeyframe.first = pos;
121             m_keyframes.insert(m_currentKeyframe.first, m_currentKeyframe.second);
122         }
123         m_selectedKeyframes = {m_currentKeyframe};
124         QMapIterator<int, int> i(updated);
125         while (i.hasNext()) {
126             i.next();
127             m_keyframes.remove(i.key());
128         }
129         i.toFront();
130         while (i.hasNext()) {
131             i.next();
132             m_keyframes.insert(i.key() + offset, i.value());
133         }
134         m_bottomPosition = pos - m_inFrame;
135         std::pair<double,double>speeds = getSpeed(m_currentKeyframe);
136         emit updateSpeeds(speeds);
137         emit updateKeyframes(true);
138         update();
139     }
140 }
141 
remapDuration() const142 int RemapView::remapDuration() const
143 {
144     int maxDuration = 0;
145     if (m_keyframes.isEmpty()) {
146         return 0;
147     }
148     QMapIterator<int, int> i(m_keyframes);
149     while (i.hasNext()) {
150         i.next();
151         if (i.key() > maxDuration) {
152             maxDuration = i.key();
153         }
154     }
155     return maxDuration - m_inFrame + 1;
156 }
157 
remapMax() const158 int RemapView::remapMax() const
159 {
160     int maxDuration = 0;
161     if (m_keyframes.isEmpty()) {
162         return 0;
163     }
164     QMapIterator<int, int> i(m_keyframes);
165     while (i.hasNext()) {
166         i.next();
167         if (i.value() > maxDuration) {
168             maxDuration = i.value();
169         }
170         if (i.key() > maxDuration) {
171             maxDuration = i.key();
172         }
173     }
174     return maxDuration - m_inFrame + 1;
175 }
176 
movingKeyframe() const177 bool RemapView::movingKeyframe() const
178 {
179     return m_moveKeyframeMode == BottomMove;
180 }
181 
setBinClipDuration(std::shared_ptr<ProjectClip> clip,int duration)182 void RemapView::setBinClipDuration(std::shared_ptr<ProjectClip> clip, int duration)
183 {
184     m_clip = clip;
185     m_service = clip->originalProducer();
186     m_duration = duration;
187     int maxWidth = width() - (2 * m_offset);
188     m_scale = maxWidth / double(qMax(1, remapMax()));
189     m_currentKeyframe = m_currentKeyframeOriginal = {-1,-1};
190 }
191 
setDuration(std::shared_ptr<Mlt::Producer> service,int duration,int sourceDuration)192 void RemapView::setDuration(std::shared_ptr<Mlt::Producer> service, int duration, int sourceDuration)
193 {
194     m_clip = nullptr;
195     m_sourceDuration = sourceDuration;
196     if (duration < 0) {
197         // reset
198         m_service = nullptr;
199         m_inFrame = 0;
200         m_duration = -1;
201         m_selectedKeyframes.clear();
202         m_keyframes.clear();
203         return;
204     }
205     if (service) {
206         m_service = service;
207         m_inFrame = 0;
208         m_duration = -1;
209     }
210     bool keyframeAdded = false;
211     if (m_duration > 0 && m_service && !m_keyframes.isEmpty()) {
212         m_keyframesOrigin = m_keyframes;
213         if (duration > m_duration) {
214             // The clip was resized, ensure we have a keyframe at the end of the clip will freeze at last keyframe
215             QMap<int, int>::const_iterator it = m_keyframes.constEnd();
216             it--;
217             int lastKeyframePos = it.key();
218             int lastKeyframeValue = it.value();
219             int lastPos = m_inFrame + duration - 1;
220             if (lastPos > lastKeyframePos) {
221                 keyframeAdded = true;
222                 std::pair<double,double>speeds = getSpeed({lastKeyframePos,lastKeyframeValue});
223                 // Move last keyframe
224                 it--;
225                 int updatedVal = it.value() + ((lastPos - it.key()) * speeds.first);
226                 m_keyframes.remove(lastKeyframePos);
227                 m_keyframes.insert(lastPos, updatedVal);
228             }
229         } else if (duration < m_duration) {
230             keyframeAdded = true;
231             // Remove all Keyframes after duration
232             int lastPos = m_inFrame + duration - 1;
233             QList <int> toDelete;
234             QMapIterator<int, int> i(m_keyframes);
235             while (i.hasNext()) {
236                 i.next();
237                 if (i.key() > duration + m_inFrame) {
238                     toDelete << i.key();
239                 }
240             }
241             if (!toDelete.isEmpty()) {
242                 // Move last keyframe to end pos
243                 int posToMove = toDelete.takeFirst();
244                 if (!m_keyframes.contains(lastPos)) {
245                     int lastKeyframeValue = m_keyframes.value(posToMove);
246                     std::pair<double,double>speeds = getSpeed({posToMove,lastKeyframeValue});
247                     m_keyframes.remove(posToMove);
248                     while (!toDelete.isEmpty()) {
249                         m_keyframes.remove(toDelete.takeFirst());
250                     }
251                     int updatedVal = m_keyframes.value(m_keyframes.lastKey()) + ((lastPos - m_keyframes.lastKey()) / speeds.first);
252                     m_keyframes.insert(lastPos, updatedVal);
253                 }
254             } else {
255                 while (!toDelete.isEmpty()) {
256                     m_keyframes.remove(toDelete.takeFirst());
257                 }
258             }
259         }
260         if (m_keyframes != m_keyframesOrigin) {
261             emit updateKeyframesWithUndo(m_keyframes, m_keyframesOrigin);
262         }
263     }
264     if (service == nullptr) {
265         // We are updating an existing remap effect due to duration change
266         m_duration = qMax(duration, remapMax());
267     } else {
268         m_duration = duration;
269         m_inFrame = m_service->get_in();
270         m_originalRange = {m_inFrame, duration + m_inFrame};
271     }
272     int maxWidth = width() - (2 * m_offset);
273     m_scale = maxWidth / double(qMax(1, remapMax() - 1));
274     m_zoomStart = m_zoomHandle.x() * maxWidth;
275     m_zoomFactor = maxWidth / (m_zoomHandle.y() * maxWidth - m_zoomStart);
276     if (!m_keyframes.contains(m_currentKeyframe.first)) {
277         m_currentKeyframe = m_currentKeyframeOriginal = {-1,-1};
278         update();
279     }
280     if (keyframeAdded) {
281         emit updateKeyframes(false);
282     }
283 }
284 
loadKeyframes(const QString & mapData)285 void RemapView::loadKeyframes(const QString &mapData)
286 {
287     m_keyframes.clear();
288     if (mapData.isEmpty()) {
289         if (m_inFrame > 0) {
290             // Necessary otherwise clip will freeze before first keyframe
291             m_keyframes.insert(0, 0);
292         }
293         m_currentKeyframe = {m_inFrame, m_inFrame};
294         m_keyframes.insert(m_currentKeyframe.first, m_currentKeyframe.second);
295         m_keyframes.insert(m_inFrame + m_duration - 1, m_inFrame + m_duration - 1);
296         std::pair<double,double>speeds = getSpeed(m_currentKeyframe);
297         std::pair<bool,bool> atEnd = {m_currentKeyframe.first == m_inFrame, m_currentKeyframe.first == m_keyframes.lastKey()};
298         emit selectedKf(m_currentKeyframe, speeds, atEnd);
299         emit atKeyframe(true, true);
300         emit updateKeyframes(false);
301     } else {
302         QStringList str = mapData.split(QLatin1Char(';'));
303         for (auto &s : str) {
304             int pos = m_service->time_to_frames(s.section(QLatin1Char('='), 0, 0).toUtf8().constData());
305             int val = GenTime(s.section(QLatin1Char('='), 1).toDouble()).frames(pCore->getCurrentFps());
306             // HACK: we always set last keyframe 1 frame after in MLT to ensure we have a correct last frame
307             if (s == str.constLast()) {
308                 pos--;
309             }
310             m_keyframes.insert(pos, val);
311             m_duration = qMax(m_duration, pos - m_inFrame);
312             m_duration = qMax(m_duration, val - m_inFrame);
313         }
314         bool isKfr = m_keyframes.contains(m_bottomPosition + m_inFrame);
315         if (isKfr) {
316             bool isLast = m_bottomPosition + m_inFrame == m_keyframes.firstKey() || m_bottomPosition + m_inFrame == m_keyframes.lastKey();
317             emit atKeyframe(isKfr, isLast);
318         } else {
319             emit atKeyframe(false, false);
320         }
321         if (m_keyframes.contains(m_currentKeyframe.first)) {
322             //bool isLast = m_currentKeyframe.first == m_keyframes.firstKey() || m_currentKeyframe.first == m_keyframes.lastKey();
323             //emit atKeyframe(true, isLast);
324             std::pair<double,double>speeds = getSpeed(m_currentKeyframe);
325             std::pair<bool,bool> atEnd = {m_currentKeyframe.first == m_inFrame, m_currentKeyframe.first == m_keyframes.lastKey()};
326             emit selectedKf(m_currentKeyframe, speeds, atEnd);
327         } else {
328             m_currentKeyframe = {-1,-1};
329             emit selectedKf(m_currentKeyframe, {-1,-1} );
330         }
331     }
332     int maxWidth = width() - (2 * m_offset);
333     m_scale = maxWidth / double(qMax(1, remapMax()));
334     m_zoomStart = m_zoomHandle.x() * maxWidth;
335     m_zoomFactor = maxWidth / (m_zoomHandle.y() * maxWidth - m_zoomStart);
336     emit updateMaxDuration();
337     update();
338 }
339 
mouseMoveEvent(QMouseEvent * event)340 void RemapView::mouseMoveEvent(QMouseEvent *event)
341 {
342     event->accept();
343     // Check for start/end snapping on drag
344     int pos = -1;
345     if (m_moveKeyframeMode == BottomMove || m_moveKeyframeMode == TopMove) {
346         double snapPos = ((m_originalRange.first - m_inFrame) * m_scale) - m_zoomStart;
347         snapPos *= m_zoomFactor;
348         snapPos += m_offset;
349         if (qAbs(snapPos - event->x()) < QApplication::startDragDistance()) {
350             pos = m_originalRange.first - m_inFrame;
351         } else {
352             snapPos = ((m_originalRange.second - m_inFrame) * m_scale) - m_zoomStart;
353             snapPos *= m_zoomFactor;
354             snapPos += m_offset;
355             if (qAbs(snapPos - event->x()) < QApplication::startDragDistance()) {
356                 pos = m_originalRange.second - m_inFrame;
357             }
358         }
359         if (pos == -1) {
360             pos = int(((event->x() - m_offset) / m_zoomFactor + m_zoomStart ) / m_scale);
361         }
362     } else {
363         pos = int(((event->x() - m_offset) / m_zoomFactor + m_zoomStart ) / m_scale);
364     }
365     pos = qBound(0, pos, m_maxLength - 1);
366     GenTime position(pos, pCore->getCurrentFps());
367     if (event->buttons() == Qt::NoButton) {
368         bool hoverKeyframe = false;
369         if (event->y() < 2 * m_lineHeight && event->y() > m_lineHeight) {
370             // mouse move in top keyframes area
371             std::pair<int,int> keyframe = getClosestKeyframe(pos + m_inFrame);
372             if (keyframe.first > -1 && qAbs(keyframe.second - pos - m_inFrame) * m_scale * m_zoomFactor <= m_lineHeight / 2) {
373                 hoverKeyframe = true;
374             }
375         } else if (event->y() > m_bottomView - 2 * m_lineHeight && event->y() < m_bottomView - m_lineHeight) {
376             // move in bottom keyframe area
377             std::pair<int,int> keyframe = getClosestKeyframe(pos + m_inFrame, true);
378             if (keyframe.first > -1 && qAbs(keyframe.first - pos - m_inFrame) * m_scale * m_zoomFactor <= m_lineHeight / 2) {
379                 hoverKeyframe = true;
380             }
381         }
382         if (hoverKeyframe) {
383             setCursor(Qt::PointingHandCursor);
384         } else {
385             setCursor(Qt::ArrowCursor);
386         }
387     } else if ((event->buttons() & Qt::LeftButton) != 0u) {
388         if (m_hoverZoomIn || m_hoverZoomOut || m_hoverZoom) {
389             // Moving zoom handles
390             if (m_hoverZoomIn) {
391                 m_zoomHandle.setX(qMin(qMax(0., double(event->x() - m_offset) / (width() - 2 * m_offset)), m_zoomHandle.y() - 0.015));
392                 int maxWidth = width() - (2 * m_offset);
393                 m_zoomStart = m_zoomHandle.x() * maxWidth;
394                 m_zoomFactor = maxWidth / (m_zoomHandle.y() * maxWidth - m_zoomStart);
395                 update();
396                 return;
397             }
398             if (m_hoverZoomOut) {
399                 m_zoomHandle.setY(qMax(qMin(1., double(event->x() - m_offset) / (width() - 2 * m_offset)), m_zoomHandle.x() + 0.015));
400                 int maxWidth = width() - (2 * m_offset);
401                 m_zoomStart = m_zoomHandle.x() * maxWidth;
402                 m_zoomFactor = maxWidth / (m_zoomHandle.y() * maxWidth - m_zoomStart);
403                 update();
404                 return;
405             }
406             // moving zoom zone
407             if (m_hoverZoom) {
408                 double clickOffset = (double(event->x()) - m_offset) / (width() - 2 * m_offset) - m_clickOffset;
409                 double newX = m_zoomHandle.x() + clickOffset;
410                 if (newX < 0) {
411                     clickOffset = - m_zoomHandle.x();
412                     newX = 0;
413                 }
414                 double newY = m_zoomHandle.y() + clickOffset;
415                 if (newY > 1) {
416                     clickOffset = 1 - m_zoomHandle.y();
417                     newY = 1;
418                     newX = m_zoomHandle.x() + clickOffset;
419                 }
420                 m_clickOffset = (double(event->x()) - m_offset) / (width() - 2 * m_offset);
421                 m_zoomHandle = QPointF(newX, newY);
422                 int maxWidth = width() - (2 * m_offset);
423                 m_zoomStart = m_zoomHandle.x() * maxWidth;
424                 m_zoomFactor = maxWidth / (m_zoomHandle.y() * maxWidth - m_zoomStart);
425                 update();
426             }
427             return;
428         }
429         //qDebug()<<"=== MOVING MOUSE: "<<pos<<" = "<<m_currentKeyframe<<", MOVE KFMODE: "<<m_moveKeyframeMode;
430         if ((m_currentKeyframe.second == pos + m_inFrame && m_moveKeyframeMode == TopMove) || (m_currentKeyframe.first == pos + m_inFrame && m_moveKeyframeMode == BottomMove)) {
431             return;
432         }
433         if (m_currentKeyframe.first >= 0 && m_moveKeyframeMode != NoMove) {
434             if (m_moveKeyframeMode == BottomMove) {
435                 // Moving bottom keyframe
436                 if (!m_keyframes.contains(pos + m_inFrame)) {
437                     int delta = pos + m_inFrame - m_currentKeyframe.first;
438                     // Check that the move is possible
439                     QMapIterator<int, int> i(m_selectedKeyframes);
440                     while (i.hasNext()) {
441                         i.next();
442                         int updatedPos = i.key() + delta;
443                         if (!m_selectedKeyframes.contains(updatedPos) && m_keyframes.contains(updatedPos)) {
444                             // Don't allow moving over another keyframe
445                             qDebug()<<"== MOVE ABORTED; OVERLAPPING EXISTING";
446                             return;
447                         }
448                     }
449                     i.toFront();
450                     QMap <int,int> updated;
451                     while (i.hasNext()) {
452                         i.next();
453                         if (i.key() < m_currentKeyframe.first) {
454                             continue;
455                         }
456                         //qDebug()<<"=== MOVING KFR: "<<i.key()<<" > "<<(i.key() + delta);
457                         //m_keyframes.insert(i.key() + delta, i.value());
458                         updated.insert(i.key() + delta, i.value());
459                         m_keyframes.remove(i.key());
460                     }
461                     QMapIterator<int, int> j(updated);
462                     while (j.hasNext()) {
463                         j.next();
464                         m_keyframes.insert(j.key(), j.value());
465                     }
466                     QMap<int,int> updatedSelection;
467                     QMapIterator<int, int> k(m_previousSelection);
468                     while (k.hasNext()) {
469                         k.next();
470                         updatedSelection.insert(k.key() + delta, k.value());
471                     }
472                     m_previousSelection = updatedSelection;
473                     m_currentKeyframe.first += delta;
474                     std::pair<double,double>speeds = getSpeed(m_currentKeyframe);
475                     std::pair<bool,bool> atEnd = {m_currentKeyframe.first == m_inFrame, m_currentKeyframe.first == m_keyframes.lastKey()};
476                     emit selectedKf(m_currentKeyframe, speeds, atEnd);
477 
478                     m_selectedKeyframes = updated;
479                     emit seekToPos(-1, pos);
480                     emit updateKeyframes(false);
481                     if (remapMax() > m_lastMaxDuration) {
482                         m_lastMaxDuration = remapMax();
483                         int maxWidth = width() - (2 * m_offset);
484                         m_scale = maxWidth / double(qMax(1, m_lastMaxDuration));
485                         m_zoomStart = m_zoomHandle.x() * maxWidth;
486                         m_zoomFactor = maxWidth / (m_zoomHandle.y() * maxWidth - m_zoomStart);
487                     }
488                     update();
489                     return;
490                 } else {
491                     qDebug()<<"=== KEYFRAME :"<< pos<<" ALREADY EXISTS";
492                 }
493             } else if (m_moveKeyframeMode == TopMove) {
494                 // Moving top keyframe
495                 int realPos = qMax(m_inFrame, pos + m_inFrame);
496                 //pos = GenTime(m_remapLink->anim_get_double("map", pos)).frames(pCore->getCurrentFps());
497                 int delta = realPos - m_currentKeyframe.second;
498                 // Check that the move is possible
499                 QMapIterator<int, int> i(m_selectedKeyframes);
500                 while (i.hasNext()) {
501                     i.next();
502                     if (i.value() + delta >= m_sourceDuration) {
503                         delta = qMin(delta, m_sourceDuration - i.value() - 1);
504                         realPos = m_currentKeyframe.second + delta;
505                         pos = realPos - m_inFrame;
506                         if (delta == 0) {
507                             pCore->displayMessage(i18n("Cannot move last source keyframe past clip end"), MessageType::ErrorMessage, 500);
508                             return;
509                         }
510                     }
511                     if (i.value() + delta < 0) {
512                         delta = qMax(delta, -i.value());
513                         realPos = m_currentKeyframe.second + delta;
514                         pos = realPos - m_inFrame;
515                         if (delta == 0) {
516                             pCore->displayMessage(i18n("Cannot move first source keyframe before clip start"), MessageType::ErrorMessage, 500);
517                             return;
518                         }
519                     }
520                 }
521                 i.toFront();
522                 QMap<int,int>updated;
523                 while (i.hasNext()) {
524                     i.next();
525                     m_keyframes.insert(i.key(), i.value() + delta);
526                     updated.insert(i.key(), i.value() + delta);
527                     if (i.value() == m_currentKeyframe.second) {
528                         m_currentKeyframe.second = realPos;
529                     }
530                 }
531                 QMap<int,int> updatedSelection;
532                 QMapIterator<int, int> k(m_previousSelection);
533                 while (k.hasNext()) {
534                     k.next();
535                     updatedSelection.insert(k.key(), k.value() + delta);
536                 }
537                 m_previousSelection = updatedSelection;
538                 std::pair<double,double> speeds = getSpeed(m_currentKeyframe);
539                 std::pair<bool,bool> atEnd = {m_currentKeyframe.first == m_inFrame, m_currentKeyframe.first == m_keyframes.lastKey()};
540                 emit selectedKf(m_currentKeyframe, speeds, atEnd);
541                 m_selectedKeyframes = updated;
542                 slotSetPosition(pos + m_inFrame);
543                 emit seekToPos(pos + m_inFrame, -1);
544                 emit updateKeyframes(false);
545                 if (remapMax() > m_lastMaxDuration) {
546                     m_lastMaxDuration = remapMax();
547                     int maxWidth = width() - (2 * m_offset);
548                     m_scale = maxWidth / double(qMax(1, m_lastMaxDuration));
549                     m_zoomStart = m_zoomHandle.x() * maxWidth;
550                     m_zoomFactor = maxWidth / (m_zoomHandle.y() * maxWidth - m_zoomStart);
551                 }
552                 update();
553                 return;
554             }
555         }
556         // Rubberband selection
557         if (m_clickPoint >= 0) {
558             m_clickEnd = pos + m_inFrame;
559             int min = qMin(m_clickPoint, m_clickEnd);
560             int max = qMax(m_clickPoint, m_clickEnd);
561             min = qMax(1, min);
562             m_selectedKeyframes.clear();
563             m_currentKeyframeOriginal = m_currentKeyframe = {-1,-1};
564             QMapIterator<int, int> i(m_keyframes);
565             while (i.hasNext()) {
566                 i.next();
567                 if (i.key() > min && i.key() <= max) {
568                     m_selectedKeyframes.insert(i.key(), i.value());
569                 }
570             }
571             if (!m_selectedKeyframes.isEmpty()) {
572                 m_currentKeyframe = {m_selectedKeyframes.firstKey(), m_selectedKeyframes.value(m_selectedKeyframes.firstKey())};
573                 m_currentKeyframeOriginal = m_currentKeyframe;
574             }
575             update();
576             return;
577         }
578 
579         if (m_moveKeyframeMode == CursorMove) {
580             if (pos != m_position) {
581                 slotSetPosition(pos + m_inFrame);
582                 emit seekToPos(pos + m_inFrame, -1);
583             }
584         }
585         if (m_moveKeyframeMode == CursorMoveBottom) {
586             pos = qMin(pos, m_keyframes.lastKey() - m_inFrame);
587             if (pos != m_bottomPosition) {
588                 m_bottomPosition = pos;
589                 bool isKfr = m_keyframes.contains(m_bottomPosition + m_inFrame);
590                 if (isKfr) {
591                     bool isLast = m_bottomPosition + m_inFrame == m_keyframes.firstKey() || m_bottomPosition + m_inFrame == m_keyframes.lastKey();
592                     emit atKeyframe(isKfr, isLast);
593                 } else {
594                     emit atKeyframe(false, false);
595                 }
596                 pos = GenTime(m_remapLink->anim_get_double("map", pos + m_inFrame)).frames(pCore->getCurrentFps());
597                 slotSetPosition(pos);
598                 emit seekToPos(-1, m_bottomPosition);
599             }
600         }
601         return;
602     }
603     if (event->y() > m_bottomView) {
604         // Moving in zoom area
605         if (qAbs(event->x() - m_offset - (m_zoomHandle.x() * (width() - 2 * m_offset))) < QApplication::startDragDistance()) {
606             setCursor(Qt::SizeHorCursor);
607             m_hoverZoomIn = true;
608             m_hoverZoomOut = false;
609             m_hoverZoom = false;
610             update();
611             return;
612         }
613         if (qAbs(event->x() - m_offset - (m_zoomHandle.y() * (width() - 2 * m_offset))) < QApplication::startDragDistance()) {
614             setCursor(Qt::SizeHorCursor);
615             m_hoverZoomOut = true;
616             m_hoverZoomIn = false;
617             m_hoverZoom = false;
618             update();
619             return;
620         }
621         if (m_zoomHandle != QPointF(0, 1) && event->x() > m_offset + (m_zoomHandle.x() * (width() - 2 * m_offset)) && event->x() < m_offset + (m_zoomHandle.y() * (width() - 2 * m_offset))) {
622             setCursor(Qt::PointingHandCursor);
623             m_hoverZoom = true;
624             m_hoverZoomIn = false;
625             m_hoverZoomOut = false;
626             update();
627             return;
628         }
629     }
630 
631     if (m_hoverZoomOut || m_hoverZoomIn || m_hoverZoom) {
632         m_hoverZoomOut = false;
633         m_hoverZoomIn = false;
634         m_hoverZoom = false;
635         setCursor(Qt::ArrowCursor);
636         update();
637     }
638 }
639 
position() const640 int RemapView::position() const
641 {
642     return m_position;
643 }
644 
centerCurrentKeyframe()645 void RemapView::centerCurrentKeyframe()
646 {
647     if (m_currentKeyframe.first == -1) {
648         // No keyframe selected, abort
649         return;
650     }
651     QMap<int,int>nextKeyframes;
652     if (m_moveNext) {
653         QMap<int, int>::iterator it = m_keyframes.find(m_currentKeyframe.first);
654         if (*it != m_keyframes.last() && it != m_keyframes.end()) {
655             it++;
656             while (it != m_keyframes.end()) {
657                 nextKeyframes.insert(it.key(), it.value());
658                 it++;
659             }
660         }
661     }
662     m_keyframes.remove(m_currentKeyframe.first);
663     int offset = m_bottomPosition + m_inFrame - m_currentKeyframe.first;
664     m_currentKeyframe.first = m_bottomPosition + m_inFrame;
665     if (offset == 0) {
666         // no move
667         return;
668     }
669     m_keyframes.insert(m_currentKeyframe.first, m_currentKeyframe.second);
670     QMapIterator<int, int> i(nextKeyframes);
671     while (i.hasNext()) {
672         i.next();
673         m_keyframes.remove(i.key());
674     }
675     i.toFront();
676     while (i.hasNext()) {
677         i.next();
678         m_keyframes.insert(i.key() + offset, i.value());
679     }
680     std::pair<double,double> speeds = getSpeed(m_currentKeyframe);
681     std::pair<bool,bool> atEnd = {m_currentKeyframe.first == m_inFrame, m_currentKeyframe.first == m_keyframes.lastKey()};
682     emit selectedKf(m_currentKeyframe, speeds, atEnd);
683     bool isLast = m_currentKeyframe.first == m_keyframes.firstKey() || m_currentKeyframe.first == m_keyframes.lastKey();
684     emit atKeyframe(true, isLast);
685     emit updateKeyframes(true);
686     update();
687 }
688 
centerCurrentTopKeyframe()689 void RemapView::centerCurrentTopKeyframe()
690 {
691     if (m_currentKeyframe.first == -1) {
692         // No keyframe selected, abort
693         return;
694     }
695     //std::pair<int,int> range = getRange(m_currentKeyframe);
696     QMap<int,int>nextKeyframes;
697     int offset = m_position + m_inFrame - m_currentKeyframe.second;
698     if (m_moveNext) {
699         QMap<int, int>::iterator it = m_keyframes.find(m_currentKeyframe.first);
700         if (*it != m_keyframes.last() && it != m_keyframes.end()) {
701             it++;
702             while (it != m_keyframes.end()) {
703                 nextKeyframes.insert(it.key(), it.value());
704                 // Check that the move is possible
705                 if (it.value() + offset >= m_sourceDuration) {
706                     pCore->displayMessage(i18n("Cannot move last source keyframe past clip end"), MessageType::ErrorMessage, 500);
707                     return;
708                 }
709                 if (it.value() + offset < 0) {
710                     pCore->displayMessage(i18n("Cannot move first source keyframe before clip start"), MessageType::ErrorMessage, 500);
711                     return;
712                 }
713                 it++;
714             }
715         }
716     }
717     m_currentKeyframe.second = m_position + m_inFrame;
718     m_keyframes.insert(m_currentKeyframe.first, m_currentKeyframe.second);
719     QMapIterator<int, int> i(nextKeyframes);
720     while (i.hasNext()) {
721         i.next();
722         m_keyframes.insert(i.key(), i.value() + offset);
723     }
724     std::pair<double,double> speeds = getSpeed(m_currentKeyframe);
725     std::pair<bool,bool> atEnd = {m_currentKeyframe.first == m_inFrame, m_currentKeyframe.first == m_keyframes.lastKey()};
726     emit selectedKf(m_currentKeyframe, speeds, atEnd);
727     bool isLast = m_currentKeyframe.first == m_keyframes.firstKey() || m_currentKeyframe.first == m_keyframes.lastKey();
728     emit atKeyframe(true, isLast);
729     emit updateKeyframes(true);
730     update();
731 }
732 
getClosestKeyframe(int pos,bool bottomKeyframe) const733 std::pair<int,int> RemapView::getClosestKeyframe(int pos, bool bottomKeyframe) const
734 {
735     int deltaMin = -1;
736     std::pair<int,int> result = {-1,-1};
737     QMapIterator<int, int> i(m_keyframes);
738     while (i.hasNext()) {
739         i.next();
740         int val = bottomKeyframe ? i.key() : i.value();
741         int delta = qAbs(val - pos);
742         if (deltaMin == -1 || delta < deltaMin) {
743             deltaMin = delta;
744             result = {i.key(), i.value()};
745         }
746     }
747     return result;
748 }
749 
mouseReleaseEvent(QMouseEvent * event)750 void RemapView::mouseReleaseEvent(QMouseEvent *event)
751 {
752     event->accept();
753     bool keyframesEdited = m_keyframesOrigin != m_keyframes;
754     if (m_moveKeyframeMode == TopMove || m_moveKeyframeMode == BottomMove) {
755         // Restore original selection
756         m_selectedKeyframes = m_previousSelection;
757         int maxWidth = width() - (2 * m_offset);
758         m_scale = maxWidth / double(qMax(1, remapMax()));
759         m_zoomStart = m_zoomHandle.x() * maxWidth;
760         m_zoomFactor = maxWidth / (m_zoomHandle.y() * maxWidth - m_zoomStart);
761         update();
762         if (!keyframesEdited) {
763             emit seekToPos(m_currentKeyframeOriginal.second, m_bottomPosition);
764         }
765     }
766     m_moveKeyframeMode = NoMove;
767     if (keyframesEdited) {
768         emit updateKeyframesWithUndo(m_keyframes, m_keyframesOrigin);
769     }
770 }
771 
mousePressEvent(QMouseEvent * event)772 void RemapView::mousePressEvent(QMouseEvent *event)
773 {
774     event->accept();
775     m_lastMaxDuration = remapMax();
776     int pos = int(((event->x() - m_offset) / m_zoomFactor + m_zoomStart ) / m_scale);
777     pos = qBound(0, pos, m_maxLength);
778     m_moveKeyframeMode = NoMove;
779     m_keyframesOrigin = m_keyframes;
780     m_oldInFrame = m_inFrame;
781     if (event->button() == Qt::LeftButton) {
782         if (event->y() < m_centerPos) {
783             // mouse click in top area
784             if (event->modifiers() & Qt::ShiftModifier) {
785                 m_clickPoint = pos + m_inFrame;
786                 return;
787             }
788             if (event->y() < 2 *m_lineHeight && event->y() > m_lineHeight) {
789                 std::pair<int,int> keyframe = getClosestKeyframe(pos + m_inFrame);
790                 if (keyframe.first > -1 && qAbs(keyframe.second - (pos + m_inFrame)) * m_scale * m_zoomFactor <= m_lineHeight / 2) {
791                     // Clicked on a top keyframe
792                     m_currentKeyframeOriginal = keyframe;
793                     if (event->modifiers() & Qt::ControlModifier) {
794                         if (m_selectedKeyframes.contains(m_currentKeyframeOriginal.first)) {
795                             m_selectedKeyframes.remove(m_currentKeyframeOriginal.first);
796                             m_currentKeyframeOriginal.first = -1;
797                         } else {
798                             m_selectedKeyframes.insert(m_currentKeyframeOriginal.first, m_currentKeyframeOriginal.second);
799                         }
800                     } else if (!m_selectedKeyframes.contains(m_currentKeyframeOriginal.first)) {
801                         m_selectedKeyframes = {m_currentKeyframeOriginal};
802                     }
803                     // Select and seek to keyframe
804                     m_currentKeyframe = m_currentKeyframeOriginal;
805                     // Calculate speeds
806                     std::pair<double,double> speeds = getSpeed(m_currentKeyframe);
807                     std::pair<bool,bool> atEnd = {m_currentKeyframe.first == m_inFrame, m_currentKeyframe.first == m_keyframes.lastKey()};
808                     emit selectedKf(m_currentKeyframe, speeds, atEnd);
809                     if (m_currentKeyframeOriginal.first > -1) {
810                         m_moveKeyframeMode = TopMove;
811                         m_previousSelection = m_selectedKeyframes;
812                         if (m_moveNext) {
813                             QMapIterator<int, int> i(m_keyframes);
814                             while (i.hasNext()) {
815                                 i.next();
816                                 if (i.key() > m_currentKeyframeOriginal.first) {
817                                     m_selectedKeyframes.insert(i.key(), i.value());
818                                 }
819                             }
820                         }
821                         if (KdenliveSettings::keyframeseek()) {
822                             slotSetPosition(m_currentKeyframeOriginal.second);
823                             m_bottomPosition = m_currentKeyframeOriginal.first - m_inFrame;
824                             bool isLast = m_currentKeyframe.first == m_keyframes.firstKey() || m_currentKeyframe.first == m_keyframes.lastKey();
825                             emit atKeyframe(true, isLast);
826                         } else {
827                             update();
828                         }
829                     } else {
830                         update();
831                     }
832                     return;
833                 }
834             }
835             // no keyframe next to mouse
836             //m_selectedKeyframes.clear();
837             //m_currentKeyframe = m_currentKeyframeOriginal = {-1,-1};
838             m_moveKeyframeMode = CursorMove;
839             if (pos != m_position) {
840                 slotSetPosition(pos + m_inFrame);
841                 emit seekToPos(pos + m_inFrame, -1);
842                 update();
843             }
844             return;
845         } else if (event->y() > m_bottomView) {
846             // click on zoom area
847             if (m_hoverZoom) {
848                 m_clickOffset = (double(event->x()) - m_offset) / (width() - 2 * m_offset);
849             }
850             return;
851         } else if (event->y() > m_centerPos && event->y() < m_bottomView) {
852             // click in bottom area
853             if (event->modifiers() & Qt::ShiftModifier) {
854                 m_clickPoint = pos + m_inFrame;
855                 return;
856             }
857             if (event->y() > (m_bottomView - 2 * m_lineHeight) && (event->y() < m_bottomView - m_lineHeight)) {
858                 std::pair<int,int> keyframe = getClosestKeyframe(pos + m_inFrame, true);
859                 if (keyframe.first > -1 && qAbs(keyframe.first - (pos + m_inFrame)) * m_scale * m_zoomFactor <= m_lineHeight / 2) {
860                     m_currentKeyframeOriginal = keyframe;
861                     if (event->modifiers() & Qt::ControlModifier) {
862                         if (m_selectedKeyframes.contains(m_currentKeyframeOriginal.first)) {
863                             m_selectedKeyframes.remove(m_currentKeyframeOriginal.first);
864                             m_currentKeyframeOriginal.second = -1;
865                         } else {
866                             m_selectedKeyframes.insert(m_currentKeyframeOriginal.first, m_currentKeyframeOriginal.second);
867                         }
868                     } else if (!m_selectedKeyframes.contains(m_currentKeyframeOriginal.first)) {
869                         m_selectedKeyframes = {m_currentKeyframeOriginal};
870                     }
871                     // Select and seek to keyframe
872                     m_currentKeyframe = m_currentKeyframeOriginal;
873                     std::pair<double,double> speeds = getSpeed(m_currentKeyframe);
874                     std::pair<bool,bool> atEnd = {m_currentKeyframe.first == m_inFrame, m_currentKeyframe.first == m_keyframes.lastKey()};
875                     emit selectedKf(m_currentKeyframe, speeds, atEnd);
876                     if (m_currentKeyframeOriginal.first > -1) {
877                         m_moveKeyframeMode = BottomMove;
878                         m_previousSelection = m_selectedKeyframes;
879                         if (m_moveNext) {
880                             QMapIterator<int, int> i(m_keyframes);
881                             while (i.hasNext()) {
882                                 i.next();
883                                 if (i.key() > m_currentKeyframeOriginal.first) {
884                                     m_selectedKeyframes.insert(i.key(), i.value());
885                                 }
886                             }
887                         }
888                         if (KdenliveSettings::keyframeseek()) {
889                             m_bottomPosition = m_currentKeyframeOriginal.first - m_inFrame;
890                             int topPos = GenTime(m_remapLink->anim_get_double("map", m_currentKeyframeOriginal.first)).frames(pCore->getCurrentFps());
891                             m_position = topPos - m_inFrame;
892                             bool isLast = m_currentKeyframe.first == m_keyframes.firstKey() || m_currentKeyframe.first == m_keyframes.lastKey();
893                             emit atKeyframe(true, isLast);
894                         } else {
895                             update();
896                         }
897                     } else {
898                         update();
899                     }
900                     return;
901                 }
902             }
903             // no keyframe next to mouse
904             //m_selectedKeyframes.clear();
905             //m_currentKeyframe = m_currentKeyframeOriginal = {-1,-1};
906             m_moveKeyframeMode = CursorMoveBottom;
907             pos = qMin(pos, m_keyframes.lastKey() - m_inFrame);
908             if (pos != m_bottomPosition) {
909                 m_bottomPosition = pos;
910                 bool isKfr = m_keyframes.contains(m_bottomPosition + m_inFrame);
911                 if (isKfr) {
912                     bool isLast = m_bottomPosition + m_inFrame == m_keyframes.firstKey() || m_bottomPosition + m_inFrame == m_keyframes.lastKey();
913                     emit atKeyframe(isKfr, isLast);
914                 } else {
915                     emit atKeyframe(false, false);
916                 }
917                 //slotUpdatePosition();
918                 emit seekToPos(-1, pos);
919                 update();
920             }
921             /*int topPos = GenTime(m_remapLink->anim_get_double("map", pos + m_inFrame)).frames(pCore->getCurrentFps());
922             if (topPos != m_position + m_inFrame) {
923 
924                 slotSetPosition(topPos);
925                 emit seekToPos(-1, pos);
926                 update();
927             }*/
928             return;
929         }
930     } else if (event->button() == Qt::RightButton && event->y() > m_bottomView) {
931         // Right click on zoom, switch between no zoom and last zoom status
932         if (m_zoomHandle == QPointF(0, 1)) {
933             if (!m_lastZoomHandle.isNull()) {
934                 m_zoomHandle = m_lastZoomHandle;
935                 int maxWidth = width() - (2 * m_offset);
936                 m_zoomStart = m_zoomHandle.x() * maxWidth;
937                 m_zoomFactor = maxWidth / (m_zoomHandle.y() * maxWidth - m_zoomStart);
938                 update();
939                 return;
940             }
941         } else {
942             m_lastZoomHandle = m_zoomHandle;
943             m_zoomHandle = QPointF(0, 1);
944             int maxWidth = width() - (2 * m_offset);
945             m_zoomStart = m_zoomHandle.x() * maxWidth;
946             m_zoomFactor = maxWidth / (m_zoomHandle.y() * maxWidth - m_zoomStart);
947             update();
948             return;
949         }
950     }
951     if (pos != m_position) {
952         //emit seekToPos(pos);
953         update();
954     }
955 }
956 
wheelEvent(QWheelEvent * event)957 void RemapView::wheelEvent(QWheelEvent *event)
958 {
959     if (event->modifiers() & Qt::AltModifier) {
960         // Alt modifier seems to invert x/y axis
961         if (event->angleDelta().x() > 0) {
962             goPrev();
963         } else {
964             goNext();
965         }
966         return;
967     }
968     if (event->modifiers() & Qt::ControlModifier) {
969         int maxWidth = width() - 2 * m_offset;
970         double scaledPos = m_position * m_scale;
971         double zoomRange = (m_zoomHandle.y() - m_zoomHandle.x()) * maxWidth;
972         if (event->angleDelta().y() > 0) {
973             zoomRange /= 1.5;
974         } else {
975             zoomRange *= 1.5;
976         }
977         if (zoomRange < 5) {
978             // Don't allow too small zoombar
979             return;
980         }
981         double length = (scaledPos - zoomRange / 2) / maxWidth;
982         m_zoomHandle.setX(qMax(0., length));
983         if (length < 0) {
984             m_zoomHandle.setY(qMin(1.0, (scaledPos + zoomRange / 2) / maxWidth - length));
985         } else {
986             m_zoomHandle.setY(qMin(1.0, (scaledPos + zoomRange / 2) / maxWidth));
987         }
988         m_zoomStart = m_zoomHandle.x() * maxWidth;
989         m_zoomFactor = maxWidth / (m_zoomHandle.y() * maxWidth - m_zoomStart);
990         update();
991         return;
992     }
993     int change = event->angleDelta().y() > 0 ? -1 : 1;
994     int pos = qBound(0, m_position + change, m_duration - 1);
995 #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
996     if (event->position().y() < m_bottomView) {
997 #else
998     if (event->y() < m_bottomView) {
999 #endif
1000         emit seekToPos(pos + m_inFrame, -1);
1001     } else {
1002         // Wheel on zoom bar, scroll
1003         double pos = m_zoomHandle.x();
1004         double zoomWidth = m_zoomHandle.y() - pos;
1005         int maxWidth = width() - 2 * m_offset;
1006         if (event->angleDelta().y() > 0) {
1007             if (zoomWidth / 2 > pos) {
1008                 pos = 0;
1009             } else {
1010                 pos -= zoomWidth / 2;
1011             }
1012         } else {
1013             if (pos + zoomWidth + zoomWidth / 2 > 1.) {
1014                 pos = 1. - zoomWidth;
1015             } else {
1016                 pos += zoomWidth / 2;
1017             }
1018         }
1019         m_zoomHandle = QPointF(pos, pos + zoomWidth);
1020         m_zoomStart = m_zoomHandle.x() * maxWidth;
1021         m_zoomFactor = maxWidth / (m_zoomHandle.y() * maxWidth - m_zoomStart);
1022         update();
1023     }
1024 }
1025 
1026 
1027 void RemapView::slotSetPosition(int pos)
1028 {
1029     if (pos != m_position + m_inFrame) {
1030         m_position = pos - m_inFrame;
1031         //int offset = pCore->getItemIn(m_model->getOwnerId());
1032         //emit atKeyframe(m_keyframes.contains(pos));
1033         double zoomPos = double(m_position) / m_duration;
1034         if (zoomPos < m_zoomHandle.x()) {
1035             double interval = m_zoomHandle.y() - m_zoomHandle.x();
1036             zoomPos = qBound(0.0, zoomPos - interval / 5, 1.0);
1037             m_zoomHandle.setX(zoomPos);
1038             m_zoomHandle.setY(zoomPos + interval);
1039         } else if (zoomPos > m_zoomHandle.y()) {
1040             double interval = m_zoomHandle.y() - m_zoomHandle.x();
1041             zoomPos = qBound(0.0, zoomPos + interval / 5, 1.0);
1042             m_zoomHandle.setX(zoomPos - interval);
1043             m_zoomHandle.setY(zoomPos);
1044         }
1045         update();
1046     }
1047 }
1048 
1049 void RemapView::slotSetBottomPosition(int pos)
1050 {
1051     if (pos < 0 || pos + m_inFrame > m_keyframes.lastKey()) {
1052         pos = -1;
1053     }
1054     if (pos != m_bottomPosition) {
1055         m_bottomPosition = pos;
1056         if (m_bottomPosition > -1) {
1057             bool isKfr = m_keyframes.contains(m_bottomPosition + m_inFrame);
1058             if (isKfr) {
1059                 bool isLast = m_bottomPosition + m_inFrame == m_keyframes.firstKey() || m_bottomPosition + m_inFrame == m_keyframes.lastKey();
1060                 emit atKeyframe(isKfr, isLast);
1061             } else {
1062                 emit atKeyframe(false, false);
1063             }
1064         } else {
1065             emit atKeyframe(false, false);
1066         }
1067         update();
1068     }
1069 }
1070 
1071 void RemapView::goNext()
1072 {
1073     // Seek to next keyframe
1074     QMapIterator<int, int> i(m_keyframes);
1075     while (i.hasNext()) {
1076         i.next();
1077         if (i.key() > m_bottomPosition + m_inFrame) {
1078             m_currentKeyframe = {i.key(),i.value()};
1079             m_selectedKeyframes = {m_currentKeyframe};
1080             slotSetPosition(i.key());
1081             m_bottomPosition = m_currentKeyframe.first - m_inFrame;
1082             emit seekToPos(i.value(), m_bottomPosition);
1083             std::pair<double,double> speeds = getSpeed(m_currentKeyframe);
1084             std::pair<bool,bool> atEnd = {m_currentKeyframe.first == m_inFrame, m_currentKeyframe.first == m_keyframes.lastKey()};
1085             emit selectedKf(m_currentKeyframe, speeds, atEnd);
1086             bool isLast = m_currentKeyframe.first == m_keyframes.firstKey() || m_currentKeyframe.first == m_keyframes.lastKey();
1087             emit atKeyframe(true, isLast);
1088             break;
1089         }
1090     }
1091 }
1092 
1093 void RemapView::goPrev()
1094 {
1095     // insert keyframe at interpolated position
1096     bool previousFound = false;
1097     QMap<int, int>::const_iterator it = m_keyframes.constBegin();
1098     while (it.key() < m_bottomPosition + m_inFrame && it != m_keyframes.constEnd()) {
1099         it++;
1100     }
1101     if (it != m_keyframes.constEnd()) {
1102         if (it != m_keyframes.constBegin()) {
1103             it--;
1104         }
1105         m_currentKeyframe = {it.key(), it.value()};
1106         m_selectedKeyframes = {m_currentKeyframe};
1107         slotSetPosition(m_currentKeyframe.second);
1108         m_bottomPosition = m_currentKeyframe.first - m_inFrame;
1109         emit seekToPos(m_currentKeyframe.second, m_bottomPosition);
1110         std::pair<double,double> speeds = getSpeed(m_currentKeyframe);
1111         std::pair<bool,bool> atEnd = {m_currentKeyframe.first == m_inFrame, m_currentKeyframe.first == m_keyframes.lastKey()};
1112         emit selectedKf(m_currentKeyframe, speeds, atEnd);
1113         bool isLast = m_currentKeyframe.first == m_keyframes.firstKey() || m_currentKeyframe.first == m_keyframes.lastKey();
1114         emit atKeyframe(true, isLast);
1115         previousFound = true;
1116     }
1117 
1118     if (!previousFound && !m_keyframes.isEmpty()) {
1119         // We are after the last keyframe
1120         m_currentKeyframe = {m_keyframes.lastKey(), m_keyframes.value(m_keyframes.lastKey())};
1121         m_selectedKeyframes = {m_currentKeyframe};
1122         slotSetPosition(m_currentKeyframe.second);
1123         m_bottomPosition = m_currentKeyframe.first - m_inFrame;
1124         emit seekToPos(m_currentKeyframe.second, m_bottomPosition);
1125         std::pair<double,double> speeds = getSpeed(m_currentKeyframe);
1126         std::pair<bool,bool> atEnd = {m_currentKeyframe.first == m_inFrame, m_currentKeyframe.first == m_keyframes.lastKey()};
1127         emit selectedKf(m_currentKeyframe, speeds, atEnd);
1128     }
1129 }
1130 
1131 void RemapView::updateBeforeSpeed(double speed)
1132 {
1133     QMutexLocker lock(&m_kfrMutex);
1134     QMap<int, int>::const_iterator it = m_keyframes.constFind(m_currentKeyframe.first);
1135     QMap<int, int> updatedKfrs;
1136     QList<int> toDelete;
1137     if (*it != m_keyframes.first() && it != m_keyframes.constEnd()) {
1138         m_keyframesOrigin = m_keyframes;
1139         it--;
1140         int updatedLength = qFuzzyIsNull(speed) ? 0 : (m_currentKeyframe.second - it.value()) * 100. / speed;
1141         int offset = it.key() + updatedLength - m_currentKeyframe.first;
1142         m_keyframes.remove(m_currentKeyframe.first);
1143         m_currentKeyframe.first = it.key() + updatedLength;
1144         m_keyframes.insert(m_currentKeyframe.first, m_currentKeyframe.second);
1145         m_bottomPosition = m_currentKeyframe.first;
1146         m_selectedKeyframes.clear();
1147         m_selectedKeyframes.insert(m_currentKeyframe.first, m_currentKeyframe.second);
1148         it = m_keyframes.constFind(m_currentKeyframe.first);
1149         if (*it != m_keyframes.last()) {
1150             it++;
1151             // Update all keyframes after that so that we don't alter the speeds
1152             while (m_moveNext && it != m_keyframes.constEnd()) {
1153                 toDelete << it.key();
1154                 updatedKfrs.insert(it.key() + offset, it.value());
1155                 it++;
1156             }
1157         }
1158         for (int p : qAsConst(toDelete)) {
1159             m_keyframes.remove(p);
1160         }
1161         QMapIterator<int, int> i(updatedKfrs);
1162         while (i.hasNext()) {
1163             i.next();
1164             m_keyframes.insert(i.key(), i.value());
1165         }
1166         int maxWidth = width() - (2 * m_offset);
1167         m_scale = maxWidth / double(qMax(1, remapMax()));
1168         m_zoomStart = m_zoomHandle.x() * maxWidth;
1169         m_zoomFactor = maxWidth / (m_zoomHandle.y() * maxWidth - m_zoomStart);
1170         emit updateKeyframesWithUndo(m_keyframes, m_keyframesOrigin);
1171         update();
1172     }
1173 }
1174 
1175 void RemapView::updateAfterSpeed(double speed)
1176 {
1177     QMutexLocker lock(&m_kfrMutex);
1178     QMap<int, int>::const_iterator it = m_keyframes.constFind(m_currentKeyframe.first);
1179     if (*it != m_keyframes.last()) {
1180         m_keyframesOrigin = m_keyframes;
1181         it++;
1182         QMap<int, int> updatedKfrs;
1183         QList<int> toDelete;
1184         int updatedLength = qFuzzyIsNull(speed) ? 0 : (it.value() - m_currentKeyframe.second) * 100. / speed;
1185         int offset = m_currentKeyframe.first + updatedLength - it.key();
1186         if (m_moveNext) {
1187             while (it != m_keyframes.constEnd()) {
1188                 toDelete << it.key();
1189                 updatedKfrs.insert(it.key() + offset, it.value());
1190                 it++;
1191             }
1192         } else {
1193             m_keyframes.insert(m_currentKeyframe.first + updatedLength, it.value());
1194             m_keyframes.remove(it.key());
1195         }
1196         // Update all keyframes after that so that we don't alter the speeds
1197         for (int p : qAsConst(toDelete)) {
1198             m_keyframes.remove(p);
1199         }
1200         QMapIterator<int, int> i(updatedKfrs);
1201         while (i.hasNext()) {
1202             i.next();
1203             m_keyframes.insert(i.key(), i.value());
1204         }
1205         int maxWidth = width() - (2 * m_offset);
1206         m_scale = maxWidth / double(qMax(1, remapMax()));
1207         m_zoomStart = m_zoomHandle.x() * maxWidth;
1208         m_zoomFactor = maxWidth / (m_zoomHandle.y() * maxWidth - m_zoomStart);
1209         emit updateKeyframesWithUndo(m_keyframes, m_keyframesOrigin);
1210         update();
1211     }
1212 }
1213 
1214 const QString RemapView::getKeyframesData(QMap<int,int> keyframes) const
1215 {
1216     QStringList result;
1217     if (keyframes.isEmpty()) {
1218         keyframes = m_keyframes;
1219     }
1220     QMapIterator<int, int> i(keyframes);
1221     int offset = 0;
1222     while (i.hasNext()) {
1223         i.next();
1224         if (i.key() == keyframes.lastKey()) {
1225             // HACK: we always set last keyframe 1 frame after in MLT to ensure we have a correct last frame
1226             offset = 1;
1227         }
1228         Mlt::Properties props;
1229         props.set("_profile", pCore->getProjectProfile()->get_profile(), 0);
1230         result << QString("%1=%2").arg(props.frames_to_time(i.key() + offset, mlt_time_clock)).arg(GenTime(i.value(), pCore->getCurrentFps()).seconds());
1231     }
1232     return result.join(QLatin1Char(';'));
1233 }
1234 
1235 void RemapView::reloadProducer()
1236 {
1237     if (!m_clip || !m_clip->clipUrl().endsWith(QLatin1String(".mlt"))) {
1238         qDebug()<<"==== this is not a playlist clip, aborting";
1239         return;
1240     }
1241     Mlt::Consumer c(pCore->getCurrentProfile()->profile(), "xml", m_clip->clipUrl().toUtf8().constData());
1242     QScopedPointer<Mlt::Service> serv(m_clip->originalProducer()->producer());
1243     if (serv == nullptr) {
1244         return;
1245     }
1246     qDebug()<<"==== GOR PLAYLIST SERVICE: "<<serv->type()<<" / "<<serv->consumer()->type()<<", SAVING TO "<<m_clip->clipUrl();
1247     Mlt::Multitrack s2(*serv.data());
1248     qDebug()<<"==== MULTITRACK: "<<s2.count();
1249     Mlt::Tractor s(pCore->getCurrentProfile()->profile());
1250     s.set_track(*s2.track(0), 0);
1251     qDebug()<<"==== GOT TRACKS: "<<s.count();
1252     int ignore = s.get_int("ignore_points");
1253     if (ignore) {
1254         s.set("ignore_points", 0);
1255     }
1256     c.connect(s);
1257     c.set("time_format", "frames");
1258     c.set("no_meta", 1);
1259     c.set("no_root", 1);
1260     //c.set("no_profile", 1);
1261     c.set("root", "/");
1262     c.set("store", "kdenlive");
1263     c.run();
1264     if (ignore) {
1265         s.set("ignore_points", ignore);
1266     }
1267 }
1268 
1269 std::pair<double,double> RemapView::getSpeed(std::pair<int,int>kf)
1270 {
1271     std::pair<double,double> speeds = {-1,-1};
1272     QMap<int, int>::const_iterator it = m_keyframes.constFind(kf.first);
1273     if (it == m_keyframes.constEnd()) {
1274         // Not a keyframe
1275         return speeds;
1276     }
1277     if (*it != m_keyframes.first()) {
1278         it--;
1279         speeds.first = (double)(kf.second - it.value()) / (kf.first - it.key());
1280         it++;
1281     }
1282     if (*it != m_keyframes.last()) {
1283         it++;
1284         speeds.second = (double)(kf.second - it.value()) / (kf.first - it.key());
1285     }
1286     return speeds;
1287 }
1288 
1289 void RemapView::addKeyframe()
1290 {
1291     // insert or remove keyframe at interpolated position
1292     m_keyframesOrigin = m_keyframes;
1293     if (m_keyframes.contains(m_bottomPosition + m_inFrame)) {
1294         m_keyframes.remove(m_bottomPosition + m_inFrame);
1295         if (m_currentKeyframe.first == m_bottomPosition + m_inFrame) {
1296             m_currentKeyframe = m_currentKeyframeOriginal = {-1,-1};
1297             emit selectedKf(m_currentKeyframe, {-1,-1});
1298         }
1299         emit atKeyframe(false, false);
1300         emit updateKeyframesWithUndo(m_keyframes, m_keyframesOrigin);
1301         update();
1302         return;
1303     }
1304     QMapIterator<int, int> i(m_keyframes);
1305     std::pair<int, int> newKeyframe = {-1,-1};
1306     std::pair<int, int> previous = {-1,-1};
1307     newKeyframe.first = m_bottomPosition + m_inFrame;
1308     while (i.hasNext()) {
1309         i.next();
1310         if (i.key() > m_bottomPosition + m_inFrame) {
1311             if (i.key() == m_keyframes.firstKey()) {
1312                 // This is the first keyframe
1313                 double ratio = (double)(m_bottomPosition + m_inFrame) / i.key();
1314                 newKeyframe.second = i.value() * ratio;
1315                 break;
1316             } else if (previous.first > -1) {
1317                 std::pair<int,int> current = {i.key(), i.value()};
1318                 double ratio = (double)(m_bottomPosition + m_inFrame - previous.first) / (current.first - previous.first);
1319                 qDebug()<<"=== RATIO: "<<ratio;
1320                 newKeyframe.second = previous.second + (qAbs(current.second - previous.second) * ratio);
1321                 break;
1322             }
1323         }
1324         previous = {i.key(), i.value()};
1325     }
1326     if (newKeyframe.second == -1) {
1327         // We are after the last keyframe
1328         if (m_keyframes.isEmpty()) {
1329             newKeyframe.second = m_position + m_inFrame;
1330         } else {
1331             double ratio = (double)(m_position + m_inFrame - m_keyframes.lastKey()) / (m_duration - m_keyframes.lastKey());
1332             newKeyframe.second = m_keyframes.value(m_keyframes.lastKey()) + (qAbs(m_duration - m_keyframes.value(m_keyframes.lastKey())) * ratio);
1333         }
1334     }
1335     m_keyframes.insert(newKeyframe.first, newKeyframe.second);
1336     m_currentKeyframe = newKeyframe;
1337     m_selectedKeyframes = {m_currentKeyframe};
1338     std::pair<double,double> speeds = getSpeed(m_currentKeyframe);
1339     std::pair<bool,bool> atEnd = {m_currentKeyframe.first == m_inFrame, m_currentKeyframe.first == m_keyframes.lastKey()};
1340     emit selectedKf(m_currentKeyframe, speeds, atEnd);
1341     bool isLast = m_currentKeyframe.first == m_keyframes.firstKey() || m_currentKeyframe.first == m_keyframes.lastKey();
1342     emit atKeyframe(true, isLast);
1343     emit updateKeyframesWithUndo(m_keyframes, m_keyframesOrigin);
1344     update();
1345 }
1346 
1347 void RemapView::toggleMoveNext(bool moveNext)
1348 {
1349     m_moveNext = moveNext;
1350     // Reset keyframe selection
1351     m_selectedKeyframes.clear();
1352 }
1353 
1354 void RemapView::refreshOnDurationChanged(int remapDuration)
1355 {
1356     if (remapDuration != m_duration) {
1357         m_duration = qMax(remapDuration, remapMax());
1358         int maxWidth = width() - (2 * m_offset);
1359         m_scale = maxWidth / double(qMax(1, remapMax()));
1360         m_zoomStart = m_zoomHandle.x() * maxWidth;
1361         m_zoomFactor = maxWidth / (m_zoomHandle.y() * maxWidth - m_zoomStart);
1362     }
1363 }
1364 
1365 void RemapView::resizeEvent(QResizeEvent *event)
1366 {
1367     int maxWidth = width() - (2 * m_offset);
1368     m_scale = maxWidth / double(qMax(1, remapMax()));
1369     m_zoomStart = m_zoomHandle.x() * maxWidth;
1370     m_zoomFactor = maxWidth / (m_zoomHandle.y() * maxWidth - m_zoomStart);
1371     QWidget::resizeEvent(event);
1372     update();
1373 }
1374 
1375 void RemapView::paintEvent(QPaintEvent *event)
1376 {
1377     Q_UNUSED(event)
1378     QPalette pal = palette();
1379     KColorScheme scheme(pal.currentColorGroup(), KColorScheme::Window);
1380     m_colSelected = palette().highlight().color();
1381     m_colKeyframe = scheme.foreground(KColorScheme::NormalText).color();
1382     QColor bg = scheme.background(KColorScheme::AlternateBackground ).color();
1383     QStylePainter p(this);
1384     int maxWidth = width() - (2 * m_offset);
1385     int zoomEnd = qCeil(m_zoomHandle.y() * maxWidth);
1386     // Top timeline
1387     p.fillRect(m_offset, 0, maxWidth + 1, m_centerPos, bg);
1388     // Bottom timeline
1389     p.fillRect(m_offset, m_bottomView - m_centerPos, maxWidth + 1, m_centerPos, bg);
1390     /* ticks */
1391     double fps = pCore->getCurrentFps();
1392     int maxLength = remapMax();
1393     if (maxLength == 0) {
1394         return;
1395     }
1396     int displayedLength = int(maxLength / m_zoomFactor / fps);
1397     double factor = 1;
1398     if (displayedLength < 2) {
1399         // 1 frame tick
1400     } else if (displayedLength < 30 ) {
1401         // 1 sec tick
1402         factor = fps;
1403     } else if (displayedLength < 150) {
1404         // 5 sec tick
1405         factor = 5 * fps;
1406     } else if (displayedLength < 300) {
1407         // 10 sec tick
1408         factor = 10 * fps;
1409     } else if (displayedLength < 900) {
1410         // 30 sec tick
1411         factor = 30 * fps;
1412     } else if (displayedLength < 1800) {
1413         // 1 min. tick
1414         factor = 60 * fps;
1415     } else if (displayedLength < 9000) {
1416         // 5 min tick
1417         factor = 300 * fps;
1418     } else if (displayedLength < 18000) {
1419         // 10 min tick
1420         factor = 600 * fps;
1421     } else {
1422         // 30 min tick
1423         factor = 1800 * fps;
1424     }
1425 
1426     // Position of left border in frames
1427     double tickOffset = m_zoomStart * m_zoomFactor;
1428     double frameSize = factor * m_scale * m_zoomFactor;
1429     int base = int(tickOffset / frameSize);
1430     tickOffset = frameSize - (tickOffset - (base * frameSize));
1431     // Draw frame ticks
1432     int scaledTick = 0;
1433     for (int i = 0; i < maxWidth / frameSize; i++) {
1434         scaledTick = int(m_offset + (i * frameSize) + tickOffset);
1435         if (scaledTick >= maxWidth + m_offset) {
1436             break;
1437         }
1438         p.drawLine(QPointF(scaledTick , m_lineHeight + 1), QPointF(scaledTick, m_lineHeight - 3));
1439         p.drawLine(QPointF(scaledTick , m_bottomView - m_lineHeight + 1), QPointF(scaledTick, m_bottomView - m_lineHeight - 3));
1440     }
1441 
1442     /*
1443      * Time-"lines"
1444      * We have a top timeline for the source (clip monitor) and a bottom timeline for the output (project monitor)
1445      */
1446     p.setPen(m_colKeyframe);
1447     // Top timeline
1448     //qDebug()<<"=== MAX KFR WIDTH: "<<maxWidth<<", DURATION SCALED: "<<(m_duration * m_scale)<<", POS: "<<(m_position * m_scale);
1449     int maxPos = maxWidth + m_offset - 1;
1450     p.drawLine(m_offset, m_lineHeight, maxPos, m_lineHeight);
1451     p.drawLine(m_offset, m_lineHeight - m_lineHeight / 4, m_offset, m_lineHeight + m_lineHeight / 4);
1452     p.drawLine(maxPos, m_lineHeight - m_lineHeight / 4, maxPos, m_lineHeight + m_lineHeight / 4);
1453     // Bottom timeline
1454     p.drawLine(m_offset, m_bottomView - m_lineHeight, maxPos, m_bottomView - m_lineHeight);
1455     p.drawLine(m_offset, m_bottomView  - m_lineHeight - m_lineHeight / 4, m_offset, m_bottomView  - m_lineHeight + m_lineHeight / 4);
1456     p.drawLine(maxPos, m_bottomView - m_lineHeight - m_lineHeight / 4, maxPos, m_bottomView  - m_lineHeight + m_lineHeight / 4);
1457 
1458     /*
1459      * Original clip in/out
1460      */
1461     p.setPen(palette().mid().color());
1462     double inPos = (double)(m_originalRange.first - m_inFrame) * m_scale;
1463     double outPos = (double)(m_originalRange.second - m_inFrame) * m_scale;
1464     inPos -= m_zoomStart;
1465     inPos *= m_zoomFactor;
1466     outPos -= m_zoomStart;
1467     outPos *= m_zoomFactor;
1468     if (inPos >= 0) {
1469         inPos += m_offset;
1470         p.drawLine(inPos, m_lineHeight, inPos, m_bottomView - m_lineHeight);
1471     }
1472     if (outPos <= maxWidth) {
1473         outPos += m_offset;
1474         p.drawLine(outPos, m_lineHeight, outPos, m_bottomView - m_lineHeight);
1475     }
1476 
1477     /*
1478      * Keyframes
1479      */
1480     QMapIterator<int, int> i(m_keyframes);
1481     while (i.hasNext()) {
1482         i.next();
1483         double outPos = (double)(i.key() - m_inFrame) * m_scale;
1484         double inPos = (double)(i.value() - m_inFrame) * m_scale;
1485         if ((inPos < m_zoomStart && outPos < m_zoomStart) || (qFloor(inPos) > zoomEnd && qFloor(outPos) > zoomEnd)) {
1486             continue;
1487         }
1488         if (m_currentKeyframe.first == i.key()) {
1489             p.setPen(Qt::red);
1490             p.setBrush(Qt::darkRed);
1491         } else if (m_selectedKeyframes.contains(i.key())) {
1492             p.setPen(m_colSelected);
1493             p.setBrush(m_colSelected);
1494         } else {
1495             p.setPen(m_colKeyframe);
1496             p.setBrush(m_colKeyframe);
1497         }
1498         inPos -= m_zoomStart;
1499         inPos *= m_zoomFactor;
1500         inPos += m_offset;
1501         outPos -= m_zoomStart;
1502         outPos *= m_zoomFactor;
1503         outPos += m_offset;
1504 
1505         p.drawLine(inPos, m_lineHeight + m_lineHeight * 0.75, outPos, m_bottomView - m_lineHeight * 1.75);
1506         p.drawLine(inPos, m_lineHeight, inPos, m_lineHeight + m_lineHeight / 2);
1507         p.drawLine(outPos, m_bottomView - m_lineHeight, outPos, m_bottomView - m_lineHeight * 1.5);
1508         p.drawEllipse(QRectF(inPos - m_lineHeight / 4.0, m_lineHeight + m_lineHeight / 2, m_lineHeight / 2, m_lineHeight / 2));
1509         p.drawEllipse(QRectF(outPos - m_lineHeight / 4.0, m_bottomView - 2 * m_lineHeight, m_lineHeight / 2, m_lineHeight / 2));
1510     }
1511 
1512     /*
1513      * current position cursor
1514      */
1515     p.setPen(m_colSelected);
1516     // Top seek cursor
1517     if (m_position >= 0 && m_position < m_duration) {
1518         p.setBrush(m_colSelected);
1519         double scaledPos = m_position * m_scale;
1520         scaledPos -= m_zoomStart;
1521         scaledPos *= m_zoomFactor;
1522         scaledPos += m_offset;
1523         if (scaledPos >= m_offset && qFloor(scaledPos) <= m_offset + maxWidth) {
1524             QPolygonF topCursor;
1525             topCursor << QPointF(-int(m_lineHeight / 3), -m_lineHeight * 0.5) << QPointF(int(m_lineHeight / 3), -m_lineHeight * 0.5) << QPointF(0, 0);
1526             topCursor.translate(scaledPos, m_lineHeight);
1527             p.drawPolygon(topCursor);
1528         }
1529     }
1530 
1531     if (m_bottomPosition >= 0 && m_bottomPosition < m_duration) {
1532         p.setBrush(m_colSelected);
1533         int topPos = -1;
1534         double scaledPos = -1;
1535         if (m_remapLink && !m_keyframes.isEmpty()) {
1536             topPos = GenTime(m_remapLink->anim_get_double("map", m_bottomPosition + m_inFrame)).frames(pCore->getCurrentFps()) - m_inFrame;
1537             scaledPos = topPos * m_scale;
1538             scaledPos -= m_zoomStart;
1539             scaledPos *= m_zoomFactor;
1540             scaledPos += m_offset;
1541         }
1542         double scaledPos2 = m_bottomPosition * m_scale;
1543         scaledPos2 -= m_zoomStart;
1544         scaledPos2 *= m_zoomFactor;
1545         scaledPos2 += m_offset;
1546         if (scaledPos2 >= m_offset && qFloor(scaledPos2) <= m_offset + maxWidth) {
1547             QPolygonF bottomCursor;
1548             bottomCursor << QPointF(-int(m_lineHeight / 3), m_lineHeight * 0.5) << QPointF(int(m_lineHeight / 3), m_lineHeight * 0.5) << QPointF(0, 0);
1549             bottomCursor.translate(scaledPos2, m_bottomView - m_lineHeight);
1550             p.setBrush(m_colSelected);
1551             p.drawPolygon(bottomCursor  );
1552         }
1553         if (scaledPos > -1) {
1554             p.drawLine(scaledPos, m_lineHeight * 1.75, scaledPos2, m_bottomView - (m_lineHeight * 1.75));
1555             p.drawLine(scaledPos, m_lineHeight, scaledPos, m_lineHeight * 1.75);
1556         }
1557         p.drawLine(scaledPos2, m_bottomView - m_lineHeight, scaledPos2, m_bottomView - m_lineHeight * 1.75);
1558     }
1559 
1560     // Zoom bar
1561     p.setPen(Qt::NoPen);
1562     p.setBrush(palette().mid());
1563     p.drawRoundedRect(0, m_bottomView + 2, width() - 2 * 0, m_zoomHeight, m_lineHeight / 3, m_lineHeight / 3);
1564     p.setBrush(palette().highlight());
1565     p.drawRoundedRect(int((width()) * m_zoomHandle.x()),
1566                       m_bottomView + 2,
1567                       int((width()) * (m_zoomHandle.y() - m_zoomHandle.x())),
1568                       m_zoomHeight,
1569                       m_lineHeight / 3, m_lineHeight / 3);
1570 }
1571 
1572 
1573 TimeRemap::TimeRemap(QWidget *parent)
1574     : QWidget(parent)
1575     , m_cid(-1)
1576 {
1577     setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont));
1578     setupUi(this);
1579 
1580     m_in = new TimecodeDisplay(pCore->timecode(), this);
1581     inLayout->addWidget(m_in);
1582     m_out = new TimecodeDisplay(pCore->timecode(), this);
1583     outLayout->addWidget(m_out);
1584     m_view = new RemapView(this);
1585     speedBefore->setKeyboardTracking(false);
1586     speedAfter->setKeyboardTracking(false);
1587     remapLayout->addWidget(m_view);
1588     connect(m_view, &RemapView::selectedKf, this, [this](std::pair<int,int>selection, std::pair<double,double>speeds, std::pair<bool,bool>atEnd) {
1589         info_frame->setEnabled(selection.first > -1);
1590         QSignalBlocker bk(m_in);
1591         QSignalBlocker bk2(m_out);
1592         m_in->setValue(selection.second - m_view->m_inFrame);
1593         m_out->setValue(selection.first - m_view->m_inFrame);
1594         QSignalBlocker bk3(speedBefore);
1595         QSignalBlocker bk4(speedAfter);
1596         speedBefore->setEnabled(!atEnd.first);
1597         speedBefore->setValue(100. * speeds.first);
1598         speedAfter->setEnabled(!atEnd.second);
1599         speedAfter->setValue(100. * speeds.second);
1600     });
1601     connect(m_view, &RemapView::updateSpeeds, this, [this](std::pair<double,double>speeds) {
1602         QSignalBlocker bk3(speedBefore);
1603         QSignalBlocker bk4(speedAfter);
1604         speedBefore->setEnabled(speeds.first > 0);
1605         speedBefore->setValue(100. * speeds.first);
1606         speedAfter->setEnabled(speeds.second > 0);
1607         speedAfter->setValue(100. * speeds.second);
1608     });
1609     button_add->setIcon(QIcon::fromTheme(QStringLiteral("keyframe-add")));
1610     button_add->setToolTip(i18n("Add keyframe"));
1611     button_next->setIcon(QIcon::fromTheme(QStringLiteral("keyframe-next")));
1612     button_next->setToolTip(i18n("Go to next keyframe"));
1613     button_prev->setIcon(QIcon::fromTheme(QStringLiteral("keyframe-previous")));
1614     button_prev->setToolTip(i18n("Go to previous keyframe"));
1615     connect(m_view, &RemapView::updateKeyframes, this, &TimeRemap::updateKeyframes);
1616     connect(m_view, &RemapView::updateKeyframesWithUndo, this, &TimeRemap::updateKeyframesWithUndo);
1617     connect(m_in, &TimecodeDisplay::timeCodeUpdated, this, [this]() {
1618         m_view->updateInPos(m_in->getValue() + m_view->m_inFrame);
1619     });
1620     button_center->setToolTip(i18n("Move selected keyframe to cursor"));
1621     button_center_top->setToolTip(i18n("Move selected keyframe to cursor"));
1622     connect(m_out, &TimecodeDisplay::timeCodeUpdated, this, [this]() {
1623         m_view->updateOutPos(m_out->getValue() + m_view->m_inFrame);
1624     });
1625     connect(button_center, &QToolButton::clicked, m_view, &RemapView::centerCurrentKeyframe);
1626     connect(button_center_top, &QToolButton::clicked, m_view, &RemapView::centerCurrentTopKeyframe);
1627     connect(m_view, &RemapView::atKeyframe, button_add, [&](bool atKeyframe, bool last) {
1628         if (atKeyframe) {
1629             button_add->setIcon(QIcon::fromTheme(QStringLiteral("keyframe-remove")));
1630             button_add->setToolTip(i18n("Delete keyframe"));
1631         } else {
1632             button_add->setIcon(QIcon::fromTheme(QStringLiteral("keyframe-add")));
1633             button_add->setToolTip(i18n("Add keyframe"));
1634         }
1635         button_add->setEnabled(!atKeyframe || !last);
1636     });
1637     connect(speedBefore, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, [&](double speed) {
1638         m_view->updateBeforeSpeed(speed);
1639     });
1640     connect(speedAfter, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, [&](double speed) {
1641         m_view->updateAfterSpeed(speed);
1642     });
1643     connect(button_del, &QToolButton::clicked, this, [this]() {
1644         if (m_cid > -1) {
1645             std::shared_ptr<TimelineItemModel> model = pCore->window()->getCurrentTimeline()->controller()->getModel();
1646             model->requestClipTimeRemap(m_cid, false);
1647             selectedClip(-1);
1648         }
1649     });
1650     connect(button_add, &QToolButton::clicked, m_view, &RemapView::addKeyframe);
1651     connect(button_next, &QToolButton::clicked, m_view, &RemapView::goNext);
1652     connect(button_prev, &QToolButton::clicked, m_view, &RemapView::goPrev);
1653     connect(move_next, &QCheckBox::toggled, m_view, &RemapView::toggleMoveNext);
1654     connect(pitch_compensate, &QCheckBox::toggled, this, &TimeRemap::switchRemapParam);
1655     connect(frame_blending, &QCheckBox::toggled, this, &TimeRemap::switchRemapParam);
1656     connect(m_view, &RemapView::updateMaxDuration, this, [this]() {
1657         m_out->setRange(0, INT_MAX);
1658         //m_in->setRange(0, duration - 1);
1659     });
1660     setEnabled(false);
1661 }
1662 
1663 const QString &TimeRemap::currentClip() const
1664 {
1665     return m_binId;
1666 }
1667 
1668 void TimeRemap::checkClipUpdate(const QModelIndex &topLeft, const QModelIndex &, const QVector<int>& roles)
1669 {
1670     int id = int(topLeft.internalId());
1671     if (m_cid != id || !roles.contains(TimelineModel::FinalMoveRole)) {
1672         return;
1673     }
1674     // Don't resize view if we are moving a keyframe
1675     if (!m_view->movingKeyframe()) {
1676         int newDuration = pCore->getItemDuration({ObjectType::TimelineClip,m_cid});
1677         // Check if the keyframes were modified by an external resize operation
1678         std::shared_ptr<TimelineItemModel> model = pCore->window()->getCurrentTimeline()->controller()->getModel();
1679         std::shared_ptr<ClipModel> clip = model->getClipPtr(m_cid);
1680         QMap<QString,QString> values = clip->getRemapValues();
1681         if (values.value(QLatin1String("map")) == m_view->getKeyframesData()) {
1682             // Resize was triggered by our keyframe move, nothing to do
1683             return;
1684         }
1685         // Reload keyframes
1686         m_lastLength = newDuration;
1687         m_view->m_remapLink->set("map", values.value(QLatin1String("map")).toUtf8().constData());
1688         int min = pCore->getItemIn({ObjectType::TimelineClip,m_cid});
1689         m_view->m_startPos = pCore->getItemPosition({ObjectType::TimelineClip,m_cid});
1690         m_in->setRange(0, m_view->m_maxLength - min);
1691         m_out->setRange(0, INT_MAX);
1692         m_view->loadKeyframes(values.value(QLatin1String("map")));
1693         m_view->update();
1694     }
1695 }
1696 
1697 void TimeRemap::selectedClip(int cid)
1698 {
1699     if (cid == -1 && cid == m_cid) {
1700         return;
1701     }
1702     QObject::disconnect( m_seekConnection1 );
1703     QObject::disconnect( m_seekConnection2 );
1704     QObject::disconnect( m_seekConnection3 );
1705     connect(pCore->getMonitor(Kdenlive::ClipMonitor), &Monitor::seekRemap, m_view, &RemapView::slotSetPosition, Qt::UniqueConnection);
1706     m_cid = cid;
1707     std::shared_ptr<TimelineItemModel> model = pCore->window()->getCurrentTimeline()->controller()->getModel();
1708     disconnect(model.get(), &TimelineItemModel::dataChanged, this, &TimeRemap::checkClipUpdate);
1709     if (cid == -1) {
1710         m_binId.clear();
1711         m_view->setDuration(nullptr, -1);
1712         setEnabled(false);
1713         return;
1714     }
1715     m_view->m_remapLink.reset();
1716     connect(model.get(), &TimelineItemModel::dataChanged, this, &TimeRemap::checkClipUpdate);
1717     model->requestClipTimeRemap(cid);
1718     m_splitId = model->m_groups->getSplitPartner(cid);
1719     m_binId = model->getClipBinId(cid);
1720     m_lastLength = pCore->getItemDuration({ObjectType::TimelineClip,cid});
1721     m_view->m_startPos = pCore->getItemPosition({ObjectType::TimelineClip,cid});
1722     std::shared_ptr<Mlt::Producer> prod = model->getClipProducer(cid);
1723     m_view->m_maxLength = prod->get_length();
1724     m_in->setRange(0, m_view->m_maxLength - prod->get_in());
1725     //m_in->setRange(0, m_lastLength - 1);
1726     m_out->setRange(0, INT_MAX);
1727     m_view->setDuration(prod, m_lastLength, prod->parent().get_length());
1728     qDebug()<<"===== GOT PRODUCER TYPE: "<<prod->parent().type();
1729     if (prod->parent().type() == mlt_service_chain_type) {
1730         Mlt::Chain fromChain(prod->parent());
1731         int count = fromChain.link_count();
1732         for (int i = 0; i < count; i++) {
1733             QScopedPointer<Mlt::Link> fromLink(fromChain.link(i));
1734             if (fromLink && fromLink->is_valid() && fromLink->get("mlt_service")) {
1735                 if (fromLink->get("mlt_service") == QLatin1String("timeremap")) {
1736                     // Found a timeremap effect, read params
1737                     m_view->m_remapLink = std::make_shared<Mlt::Link>(fromChain.link(i)->get_link());
1738                     if (m_splitId > -1) {
1739                         std::shared_ptr<Mlt::Producer> prod2 = model->getClipProducer(m_splitId);
1740                         if (prod2->parent().type() == mlt_service_chain_type) {
1741                             Mlt::Chain fromChain2(prod2->parent());
1742                             count = fromChain2.link_count();
1743                             for (int j = 0; j < count; j++) {
1744                                 QScopedPointer<Mlt::Link> fromLink2(fromChain2.link(j));
1745                                 if (fromLink2 && fromLink2->is_valid() && fromLink2->get("mlt_service")) {
1746                                     if (fromLink2->get("mlt_service") == QLatin1String("timeremap")) {
1747                                         m_splitRemap = std::make_shared<Mlt::Link>(fromChain2.link(j)->get_link());
1748                                     }
1749                                 }
1750                             }
1751                         }
1752                     }
1753                     QString mapData(fromLink->get("map"));
1754                     m_view->loadKeyframes(mapData);
1755                     if (mapData.isEmpty()) {
1756                         // We are just adding the remap effect, set default params
1757                         fromLink->set("pitch", 1);
1758                         fromLink->set("image_mode", "nearest");
1759                     }
1760                     QSignalBlocker bk(pitch_compensate);
1761                     QSignalBlocker bk2(frame_blending);
1762                     pitch_compensate->setChecked(fromLink->get_int("pitch") == 1);
1763                     frame_blending->setChecked(fromLink->get("image_mode") != QLatin1String("nearest"));
1764                     setEnabled(true);
1765                     break;
1766                 }
1767             }
1768         }
1769     } else {
1770         qDebug()<<"/// PRODUCER IS NOT A CHAIN!!!!";
1771     }
1772     if (!m_binId.isEmpty() && pCore->getMonitor(Kdenlive::ClipMonitor)->activeClipId() == m_binId) {
1773         connect(pCore->getMonitor(Kdenlive::ClipMonitor), &Monitor::seekPosition, pCore->getMonitor(Kdenlive::ClipMonitor), &Monitor::seekRemap, Qt::UniqueConnection);
1774     }
1775     m_seekConnection1 = connect(m_view, &RemapView::seekToPos, this, [this](int topPos, int bottomPos) {
1776         if (topPos > -1) {
1777             if (pCore->getMonitor(Kdenlive::ClipMonitor)->activeClipId() != m_binId) {
1778                 int min = pCore->getItemIn({ObjectType::TimelineClip,m_cid});
1779                 int lastLength = pCore->getItemDuration({ObjectType::TimelineClip,m_cid});
1780                 int max = min + lastLength;
1781                 pCore->selectBinClip(m_binId, true, min, {min,max});
1782             }
1783             pCore->getMonitor(Kdenlive::ClipMonitor)->requestSeek(topPos);
1784         }
1785         if (bottomPos > -1) {
1786             pCore->getMonitor(Kdenlive::ProjectMonitor)->requestSeek(bottomPos + m_view->m_startPos);
1787         }
1788     });
1789     m_seekConnection2 = connect(pCore->getMonitor(Kdenlive::ProjectMonitor), &Monitor::seekPosition, this, [this](int pos) {
1790         m_view->slotSetBottomPosition(pos - m_view->m_startPos);
1791     });
1792 }
1793 
1794 void TimeRemap::setClip(std::shared_ptr<ProjectClip> clip, int in, int out)
1795 {
1796     if (m_cid > -1 && clip == nullptr) {
1797         return;
1798     }
1799     QObject::disconnect( m_seekConnection1 );
1800     QObject::disconnect( m_seekConnection2 );
1801     QObject::disconnect( m_seekConnection3 );
1802     m_cid = -1;
1803     m_binId.clear();
1804     if (clip == nullptr || !clip->statusReady() || clip->clipType() != ClipType::Playlist) {
1805         m_view->setDuration(nullptr, -1);
1806         setEnabled(false);
1807         return;
1808     }
1809     m_view->m_remapLink.reset();
1810     bool keyframesLoaded = false;
1811     if (clip != nullptr) {
1812         int min = in == -1 ? 0 : in;
1813         int max = out == -1 ? clip->getFramePlaytime() : out;
1814         m_in->setRange(0, max - min);
1815         m_out->setRange(min, INT_MAX);
1816         m_view->m_startPos = 0;
1817         m_view->setBinClipDuration(clip, max - min);
1818         if (clip->clipType() == ClipType::Playlist) {
1819             Mlt::Service service(clip->originalProducer()->producer()->get_service());
1820             qDebug()<<"==== producer type: "<<service.type();
1821             if (service.type() == mlt_service_multitrack_type) {
1822                 Mlt::Multitrack multi(service);
1823                 for (int i = 0; i < multi.count(); i++) {
1824                     std::unique_ptr<Mlt::Producer> track(multi.track(i));
1825                     qDebug()<<"==== GOT TRACK TYPE: "<<track->type();
1826                     switch (track->type()) {
1827                         case mlt_service_chain_type: {
1828                             Mlt::Chain fromChain(*track.get());
1829                             int count = fromChain.link_count();
1830                             for (int i = 0; i < count; i++) {
1831                                 QScopedPointer<Mlt::Link> fromLink(fromChain.link(i));
1832                                 if (fromLink && fromLink->is_valid() && fromLink->get("mlt_service")) {
1833                                     if (fromLink->get("mlt_service") == QLatin1String("timeremap")) {
1834                                         // Found a timeremap effect, read params
1835                                         m_view->m_remapLink = std::make_shared<Mlt::Link>(fromChain.link(i)->get_link());
1836                                         QString mapData(fromLink->get("map"));
1837                                         m_view->loadKeyframes(mapData);
1838                                         keyframesLoaded = true;
1839                                         break;
1840                                     }
1841                                 }
1842                             }
1843                             break;
1844                         }
1845                         case mlt_service_playlist_type: {
1846                             // that is a single track
1847                             Mlt::Playlist local_playlist(*track);
1848                             int max = local_playlist.count();
1849                             qDebug()<<"==== PLAYLIST COUNT: "<<max;
1850                             if (max == 1) {
1851                                 Mlt::Producer prod = local_playlist.get_clip(0)->parent();
1852                                 qDebug()<<"==== GOT PROD TYPE: "<<prod.type()<<" = "<<prod.get("mlt_service")<<" = "<<prod.get("resource");
1853                                 if (prod.type() == mlt_service_chain_type) {
1854                                     Mlt::Chain fromChain(prod);
1855                                     int count = fromChain.link_count();
1856                                     for (int i = 0; i < count; i++) {
1857                                         QScopedPointer<Mlt::Link> fromLink(fromChain.link(i));
1858                                         if (fromLink && fromLink->is_valid() && fromLink->get("mlt_service")) {
1859                                             if (fromLink->get("mlt_service") == QLatin1String("timeremap")) {
1860                                                 // Found a timeremap effect, read params
1861                                                 m_view->m_remapLink = std::make_shared<Mlt::Link>(fromChain.link(i)->get_link());
1862                                                 QString mapData(fromLink->get("map"));
1863                                                 m_view->loadKeyframes(mapData);
1864                                                 keyframesLoaded = true;
1865                                                 break;
1866                                             }
1867                                         }
1868                                     }
1869                                 }
1870                             }
1871                             break;
1872                         }
1873                         default:
1874                             qDebug()<<"=== UNHANDLED TRACK TYPE";
1875                             break;
1876                     }
1877                 }
1878             }
1879         }
1880         if (!keyframesLoaded) {
1881             m_view->loadKeyframes(QString());
1882         }
1883         m_seekConnection1 = connect(m_view, &RemapView::seekToPos, pCore->getMonitor(Kdenlive::ClipMonitor), &Monitor::requestSeek, Qt::UniqueConnection);
1884         m_seekConnection2 = connect(pCore->getMonitor(Kdenlive::ClipMonitor), &Monitor::seekPosition, this, [&](int pos) {
1885             m_view->slotSetPosition(pos);
1886         });
1887         setEnabled(m_view->m_remapLink != nullptr);
1888     } else {
1889         setEnabled(false);
1890     }
1891 }
1892 
1893 void TimeRemap::updateKeyframes(bool resize)
1894 {
1895     QString kfData = m_view->getKeyframesData();
1896     if (m_view->m_remapLink) {
1897         m_view->m_remapLink->set("map", kfData.toUtf8().constData());
1898         if (m_splitRemap) {
1899             m_splitRemap->set("map", kfData.toUtf8().constData());
1900         }
1901         if (m_cid == -1) {
1902             // This is a playlist clip
1903             m_view->timer.start();
1904         }
1905     }
1906 }
1907 
1908 void TimeRemap::updateKeyframesWithUndo(QMap<int,int>updatedKeyframes, QMap<int,int>previousKeyframes)
1909 {
1910     if (m_view->m_remapLink == nullptr) {
1911         return;
1912     }
1913     bool usePitch = pitch_compensate->isChecked();
1914     bool useBlend = frame_blending->isChecked();
1915     bool hadPitch = m_view->m_remapLink->get_int("pitch") == 1;
1916     bool hadBlend = m_view->m_remapLink->get("image_mode") != QLatin1String("nearest");
1917     bool durationChanged = updatedKeyframes.isEmpty() ? false : updatedKeyframes.lastKey() - pCore->getItemIn({ObjectType::TimelineClip,m_cid}) + 1 != pCore->getItemDuration({ObjectType::TimelineClip,m_cid});
1918     Fun undo = []() { return true; };
1919     Fun redo = []() { return true; };
1920     Fun local_undo = [this, link = m_view->m_remapLink, splitLink = m_splitRemap, previousKeyframes, cid = m_cid, oldIn = m_view->m_oldInFrame, hadPitch, hadBlend]() {
1921         QString oldKfData;
1922         bool keyframesChanged = false;
1923         if (!previousKeyframes.isEmpty()) {
1924             oldKfData = m_view->getKeyframesData(previousKeyframes);
1925             keyframesChanged = true;
1926         }
1927         if (keyframesChanged) {
1928             link->set("map", oldKfData.toUtf8().constData());
1929         }
1930         link->set("pitch", hadPitch ? 1 : 0);
1931         link->set("image_mode", hadBlend ? "blend" : "nearest");
1932         if (splitLink) {
1933             if (keyframesChanged) {
1934                 splitLink->set("map", oldKfData.toUtf8().constData());
1935             }
1936             splitLink->set("pitch", hadPitch ? 1 : 0);
1937             splitLink->set("image_mode", hadBlend ? "blend" : "nearest");
1938         }
1939         if (cid == m_cid) {
1940             QSignalBlocker bk(pitch_compensate);
1941             QSignalBlocker bk2(frame_blending);
1942             pitch_compensate->setChecked(hadPitch);
1943             frame_blending->setChecked(hadBlend);
1944             if (keyframesChanged) {
1945                 m_lastLength = previousKeyframes.lastKey() - oldIn;
1946                 // This clip is currently displayed in remap view
1947                 m_view->m_remapLink->set("map", oldKfData.toUtf8().constData());
1948                 m_view->loadKeyframes(oldKfData);
1949                 update();
1950             }
1951         }
1952         return true;
1953     };
1954 
1955     Fun local_redo = [this, link = m_view->m_remapLink, splitLink = m_splitRemap, updatedKeyframes, cid = m_cid, usePitch, in = m_view->m_inFrame, useBlend]() {
1956         QString newKfData;
1957         bool keyframesChanged = false;
1958         if (!updatedKeyframes.isEmpty()) {
1959             newKfData = m_view->getKeyframesData(updatedKeyframes);
1960             keyframesChanged = true;
1961         }
1962         if (keyframesChanged) {
1963             link->set("map", newKfData.toUtf8().constData());
1964         }
1965         link->set("pitch", usePitch ? 1 : 0);
1966         link->set("image_mode", useBlend ? "blend" : "nearest");
1967         if (splitLink) {
1968             if (keyframesChanged) {
1969                 splitLink->set("map", newKfData.toUtf8().constData());
1970             }
1971             splitLink->set("pitch", usePitch ? 1 : 0);
1972             splitLink->set("image_mode", useBlend ? "blend" : "nearest");
1973         }
1974         if (cid == m_cid) {
1975             QSignalBlocker bk(pitch_compensate);
1976             QSignalBlocker bk2(frame_blending);
1977             pitch_compensate->setChecked(usePitch);
1978             frame_blending->setChecked(useBlend);
1979             if (keyframesChanged) {
1980                 // This clip is currently displayed in remap view
1981                 m_lastLength = updatedKeyframes.lastKey() - in;
1982                 m_view->m_remapLink->set("map", newKfData.toUtf8().constData());
1983                 m_view->loadKeyframes(newKfData);
1984                 update();
1985             }
1986         }
1987         return true;
1988     };
1989     local_redo();
1990     if (durationChanged) {
1991         int length = updatedKeyframes.lastKey() - m_view->m_inFrame + 1;
1992         std::shared_ptr<TimelineItemModel> model = pCore->window()->getCurrentTimeline()->controller()->getModel();
1993         model->requestItemResize(m_cid, length, true, true, undo, redo);
1994         if (m_splitId > 0) {
1995             model->requestItemResize(m_splitId, length, true, true, undo, redo);
1996         }
1997     }
1998     UPDATE_UNDO_REDO_NOLOCK(redo, undo, local_undo, local_redo);
1999     pCore->pushUndo(local_undo, local_redo, i18n("Edit Timeremap keyframes"));
2000 }
2001 
2002 void TimeRemap::switchRemapParam()
2003 {
2004     updateKeyframesWithUndo(QMap<int,int>(),QMap<int,int>());
2005 }
2006 
2007 bool TimeRemap::isInRange() const
2008 {
2009     return m_cid != -1 && m_view->isInRange();
2010 }
2011 
2012 TimeRemap::~TimeRemap()
2013 {
2014     //delete m_previewTimer;
2015 }
2016