1 /*
2  * Copyright (c) 2013-2021 Meltytech, LLC
3  *
4  * This program is free software: you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation, either version 3 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
16  */
17 
18 #include "timelinedock.h"
19 #include "ui_timelinedock.h"
20 #include "models/audiolevelstask.h"
21 #include "models/multitrackmodel.h"
22 #include "qmltypes/thumbnailprovider.h"
23 #include "mainwindow.h"
24 #include "commands/timelinecommands.h"
25 #include "qmltypes/qmlutilities.h"
26 #include "qmltypes/qmlview.h"
27 #include "shotcut_mlt_properties.h"
28 #include "settings.h"
29 #include "util.h"
30 #include "proxymanager.h"
31 #include "dialogs/longuitask.h"
32 
33 #include <QAction>
34 #include <QtQml>
35 #include <QtQuick>
36 #include <Logger.h>
37 
38 static const char* kFileUrlProtocol = "file://";
39 static const char* kFilesUrlDelimiter = ",file://";
40 
TimelineDock(QWidget * parent)41 TimelineDock::TimelineDock(QWidget *parent) :
42     QDockWidget(parent),
43     ui(new Ui::TimelineDock),
44     m_quickView(QmlUtilities::sharedEngine(), this),
45     m_position(-1),
46     m_ignoreNextPositionChange(false),
47     m_trimDelta(0),
48     m_transitionDelta(0),
49     m_blockSetSelection(false)
50 {
51     LOG_DEBUG() << "begin";
52     m_selection.selectedTrack = -1;
53     m_selection.isMultitrackSelected = false;
54 
55     ui->setupUi(this);
56     toggleViewAction()->setIcon(windowIcon());
57 
58     qmlRegisterType<MultitrackModel>("Shotcut.Models", 1, 0, "MultitrackModel");
59 
60     QDir importPath = QmlUtilities::qmlDir();
61     importPath.cd("modules");
62     m_quickView.engine()->addImportPath(importPath.path());
63     m_quickView.engine()->addImageProvider(QString("thumbnail"), new ThumbnailProvider);
64     QmlUtilities::setCommonProperties(m_quickView.rootContext());
65     m_quickView.rootContext()->setContextProperty("view", new QmlView(&m_quickView));
66     m_quickView.rootContext()->setContextProperty("timeline", this);
67     m_quickView.rootContext()->setContextProperty("multitrack", &m_model);
68     m_quickView.setResizeMode(QQuickWidget::SizeRootObjectToView);
69     m_quickView.setClearColor(palette().window().color());
70     m_quickView.quickWindow()->setPersistentSceneGraph(false);
71 #ifndef Q_OS_MAC
72     m_quickView.setAttribute(Qt::WA_AcceptTouchEvents);
73 #endif
74 
75     connect(&m_model, SIGNAL(modified()), this, SLOT(clearSelectionIfInvalid()));
76     connect(&m_model, &MultitrackModel::appended, this, &TimelineDock::selectClip, Qt::QueuedConnection);
77     connect(&m_model, &MultitrackModel::inserted, this, &TimelineDock::selectClip, Qt::QueuedConnection);
78     connect(&m_model, &MultitrackModel::overWritten, this, &TimelineDock::selectClip, Qt::QueuedConnection);
79     connect(&m_model, SIGNAL(rowsInserted(QModelIndex,int,int)), SLOT(onRowsInserted(QModelIndex,int,int)));
80     connect(&m_model, SIGNAL(rowsRemoved(QModelIndex,int,int)), SLOT(onRowsRemoved(QModelIndex,int,int)));
81 
82     setWidget(&m_quickView);
83 
84     connect(this, SIGNAL(clipMoved(int,int,int,int,bool)), SLOT(onClipMoved(int,int,int,int,bool)), Qt::QueuedConnection);
85     connect(this, SIGNAL(transitionAdded(int,int,int,bool)), SLOT(onTransitionAdded(int,int,int,bool)), Qt::QueuedConnection);
86     connect(MLT.videoWidget(), SIGNAL(frameDisplayed(const SharedFrame&)), this, SLOT(onShowFrame(const SharedFrame&)));
87     connect(this, SIGNAL(visibilityChanged(bool)), this, SLOT(load()));
88     connect(this, SIGNAL(topLevelChanged(bool)), this, SLOT(onTopLevelChanged(bool)));
89     LOG_DEBUG() << "end";
90 }
91 
~TimelineDock()92 TimelineDock::~TimelineDock()
93 {
94     delete ui;
95 }
96 
setPosition(int position)97 void TimelineDock::setPosition(int position)
98 {
99     if (!m_model.tractor()) return;
100     if (position <= m_model.tractor()->get_length()) {
101         emit seeked(position);
102     } else {
103         m_position = m_model.tractor()->get_length();
104         emit positionChanged();
105     }
106 }
107 
getClipInfo(int trackIndex,int clipIndex)108 Mlt::ClipInfo *TimelineDock::getClipInfo(int trackIndex, int clipIndex)
109 {
110     Mlt::ClipInfo* result = nullptr;
111     if (clipIndex >= 0 && trackIndex >= 0 && trackIndex < m_model.trackList().size()) {
112         int i = m_model.trackList().at(trackIndex).mlt_index;
113         QScopedPointer<Mlt::Producer> track(m_model.tractor()->track(i));
114         if (track) {
115             Mlt::Playlist playlist(*track);
116             result = playlist.clip_info(clipIndex);
117         }
118     }
119     return result;
120 }
121 
producerForClip(int trackIndex,int clipIndex)122 Mlt::Producer TimelineDock::producerForClip(int trackIndex, int clipIndex)
123 {
124     Mlt::Producer result;
125     Mlt::ClipInfo* info = getClipInfo(trackIndex, clipIndex);
126     if (info) {
127         result = Mlt::Producer(info->producer);
128         delete info;
129     }
130     return result;
131 }
132 
clipIndexAtPlayhead(int trackIndex)133 int TimelineDock::clipIndexAtPlayhead(int trackIndex)
134 {
135     return clipIndexAtPosition(trackIndex, m_position);
136 }
137 
clipIndexAtPosition(int trackIndex,int position)138 int TimelineDock::clipIndexAtPosition(int trackIndex, int position)
139 {
140     int result = -1;
141     if (trackIndex < 0)
142         trackIndex = currentTrack();
143     if (trackIndex >= 0 && trackIndex < m_model.trackList().size()) {
144         int i = m_model.trackList().at(trackIndex).mlt_index;
145         QScopedPointer<Mlt::Producer> track(m_model.tractor()->track(i));
146         if (track) {
147             Mlt::Playlist playlist(*track);
148             result = playlist.get_clip_index_at(position);
149             if (result >= playlist.count())
150                 result = -1;
151         }
152     }
153     return result;
154 }
155 
isBlank(int trackIndex,int clipIndex)156 bool TimelineDock::isBlank(int trackIndex, int clipIndex)
157 {
158     return trackIndex >= 0 && clipIndex >= 0 &&
159         m_model.index(clipIndex, 0, m_model.index(trackIndex))
160         .data(MultitrackModel::IsBlankRole).toBool();
161 }
162 
pulseLockButtonOnTrack(int trackIndex)163 void TimelineDock::pulseLockButtonOnTrack(int trackIndex)
164 {
165     QMetaObject::invokeMethod(m_quickView.rootObject(), "pulseLockButtonOnTrack",
166             Qt::DirectConnection, Q_ARG(QVariant, trackIndex));
167     emit showStatusMessage(tr("This track is locked"));
168 }
169 
emitNonSeekableWarning()170 void TimelineDock::emitNonSeekableWarning()
171 {
172     emit showStatusMessage(tr("You cannot add a non-seekable source."));
173 }
174 
chooseClipAtPosition(int position,int & trackIndex,int & clipIndex)175 void TimelineDock::chooseClipAtPosition(int position, int& trackIndex, int& clipIndex)
176 {
177     QScopedPointer<Mlt::Producer> clip;
178 
179     // Start by checking for a hit at the specified track
180     if (trackIndex != -1 && !isTrackLocked(trackIndex)) {
181         clipIndex = clipIndexAtPosition(trackIndex, position);
182         if (clipIndex != -1 && !isBlank(trackIndex, clipIndex))
183             return;
184     }
185 
186     // Next we try the current track
187     trackIndex = currentTrack();
188     clipIndex = qMin(clipIndexAtPosition(trackIndex, position), clipCount(trackIndex) - 1);
189 
190     if (!isTrackLocked(trackIndex) && clipIndex != -1 && !isBlank(trackIndex, clipIndex)) {
191         return;
192     }
193 
194     // if there was no hit, look through the other tracks
195     for (trackIndex = 0; trackIndex < m_model.trackList().size(); (trackIndex)++) {
196         if (trackIndex == currentTrack())
197             continue;
198         if (isTrackLocked(trackIndex))
199             continue;
200         clipIndex = clipIndexAtPosition(trackIndex, position);
201         if (clipIndex != -1 && !isBlank(trackIndex, clipIndex))
202             return;
203     }
204 
205     // As last resort choose blank on current track
206     trackIndex = currentTrack();
207     if (!isTrackLocked(trackIndex)) {
208         clipIndex = clipIndexAtPosition(trackIndex, position);
209         if (clipIndex != -1)
210             return;
211     }
212 
213     trackIndex = -1;
214     clipIndex = -1;
215 }
216 
clipCount(int trackIndex) const217 int TimelineDock::clipCount(int trackIndex) const
218 {
219     if (trackIndex < 0)
220         trackIndex = currentTrack();
221     if (trackIndex >= 0 && trackIndex < m_model.trackList().size()) {
222         int i = m_model.trackList().at(trackIndex).mlt_index;
223         QScopedPointer<Mlt::Producer> track(m_model.tractor()->track(i));
224         if (track) {
225             Mlt::Playlist playlist(*track);
226             return playlist.count();
227         }
228     }
229     return 0;
230 }
231 
setCurrentTrack(int currentTrack)232 void TimelineDock::setCurrentTrack(int currentTrack)
233 {
234     if (!m_quickView.rootObject())
235         return;
236     m_quickView.rootObject()->setProperty("currentTrack", qBound(0, currentTrack, m_model.trackList().size() - 1));
237 }
238 
currentTrack() const239 int TimelineDock::currentTrack() const
240 {
241     if (!m_quickView.rootObject())
242         return 0;
243     return m_quickView.rootObject()->property("currentTrack").toInt();
244 }
245 
setSelectionFromJS(const QVariantList & list)246 void TimelineDock::setSelectionFromJS(const QVariantList& list)
247 {
248     QList<QPoint> points;
249     for (const auto& v : list) {
250         auto p = v.toPoint();
251         if (!isBlank(p.y(), p.x()))
252             points << p;
253     }
254     setSelection(points);
255 }
256 
setSelection(QList<QPoint> newSelection,int trackIndex,bool isMultitrack)257 void TimelineDock::setSelection(QList<QPoint> newSelection, int trackIndex, bool isMultitrack)
258 {
259     if (!m_blockSetSelection)
260     if (newSelection != selection()
261             || trackIndex != m_selection.selectedTrack
262             || isMultitrack != m_selection.isMultitrackSelected) {
263         LOG_DEBUG() << "Changing selection to" << newSelection << " trackIndex" << trackIndex << "isMultitrack" << isMultitrack;
264         m_selection.selectedClips = newSelection;
265         m_selection.selectedTrack = trackIndex;
266         m_selection.isMultitrackSelected = isMultitrack;
267         emit selectionChanged();
268 
269         if (!m_selection.selectedClips.isEmpty())
270             emitSelectedFromSelection();
271         else
272             emit selected(nullptr);
273     }
274 }
275 
selectionForJS() const276 QVariantList TimelineDock::selectionForJS() const
277 {
278     QVariantList result;
279     foreach (auto point, selection())
280         result << QVariant(point);
281     return result;
282 }
283 
selection() const284 const QList<QPoint> TimelineDock::selection() const
285 {
286     if (!m_quickView.rootObject())
287         return QList<QPoint>();
288     return m_selection.selectedClips;
289 }
290 
selectionUuids()291 const QVector<QUuid> TimelineDock::selectionUuids()
292 {
293     QVector<QUuid> result;
294     for (const auto& clip : selection()) {
295         QScopedPointer<Mlt::ClipInfo> info(getClipInfo(clip.y(), clip.x()));
296         if (info && info->cut && info->cut->is_valid())
297             result << MLT.ensureHasUuid(*info->cut);
298     }
299     return result;
300 }
301 
saveAndClearSelection()302 void TimelineDock::saveAndClearSelection()
303 {
304     m_savedSelection = m_selection;
305     m_selection.selectedClips = QList<QPoint>();
306     m_selection.selectedTrack = -1;
307     m_selection.isMultitrackSelected = false;
308     emit selectionChanged();
309 }
310 
restoreSelection()311 void TimelineDock::restoreSelection()
312 {
313     m_selection = m_savedSelection;
314     emit selectionChanged();
315     emitSelectedFromSelection();
316 }
317 
selectClipUnderPlayhead()318 void TimelineDock::selectClipUnderPlayhead()
319 {
320     int track = -1, clip = -1;
321     chooseClipAtPosition(m_position, track, clip);
322     if (clip == -1) {
323         if (isTrackLocked(currentTrack())) {
324             pulseLockButtonOnTrack(currentTrack());
325             return;
326         }
327         int idx = clipIndexAtPlayhead(-1);
328         if (idx == -1)
329             setSelection();
330         else
331             setSelection(QList<QPoint>() << QPoint(idx, track));
332         return;
333     }
334 
335     if (track != -1) {
336         setCurrentTrack(track);
337         setSelection(QList<QPoint>() << QPoint(clip, track));
338     }
339 }
340 
centerOfClip(int trackIndex,int clipIndex)341 int TimelineDock::centerOfClip(int trackIndex, int clipIndex)
342 {
343     QScopedPointer<Mlt::ClipInfo> clip(getClipInfo(trackIndex, clipIndex));
344     return clip? clip->start + clip->frame_count / 2 : -1;
345 }
346 
isTrackLocked(int trackIndex) const347 bool TimelineDock::isTrackLocked(int trackIndex) const
348 {
349     if (trackIndex < 0 || trackIndex >= m_model.trackList().size())
350         return false;
351     int i = m_model.trackList().at(trackIndex).mlt_index;
352     QScopedPointer<Mlt::Producer> track(m_model.tractor()->track(i));
353     return track->get_int(kTrackLockProperty);
354 }
355 
trimClipAtPlayhead(TrimLocation location,bool ripple)356 void TimelineDock::trimClipAtPlayhead(TrimLocation location, bool ripple)
357 {
358     int trackIndex = currentTrack(), clipIndex = -1;
359     chooseClipAtPosition(m_position, trackIndex, clipIndex);
360     if (trackIndex < 0 || clipIndex < 0)
361         return;
362     setCurrentTrack(trackIndex);
363 
364     int i = m_model.trackList().at(trackIndex).mlt_index;
365     QScopedPointer<Mlt::Producer> track(m_model.tractor()->track(i));
366     if (!track)
367         return;
368 
369     QScopedPointer<Mlt::ClipInfo> info(getClipInfo(trackIndex, clipIndex));
370     if (!info)
371         return;
372 
373     if (location == TrimInPoint) {
374         MAIN.undoStack()->push(
375             new Timeline::TrimClipInCommand(m_model, trackIndex, clipIndex, m_position - info->start, ripple));
376         if (ripple)
377             setPosition(info->start);
378         if (m_updateCommand && m_updateCommand->trackIndex() == trackIndex && m_updateCommand->clipIndex() == clipIndex)
379             m_updateCommand->setPosition(trackIndex, clipIndex, m_updateCommand->position() + m_position - info->start);
380     } else {
381         MAIN.undoStack()->push(
382             new Timeline::TrimClipOutCommand(m_model, trackIndex, clipIndex, info->start + info->frame_count - m_position, ripple));
383         if (m_updateCommand && m_updateCommand->trackIndex() == trackIndex && m_updateCommand->clipIndex() == clipIndex)
384             m_updateCommand->setPosition(trackIndex, clipIndex,-1);
385     }
386 }
387 
isRipple() const388 bool TimelineDock::isRipple() const
389 {
390     return m_quickView.rootObject()->property("ripple").toBool();
391 }
392 
copyToSource()393 void TimelineDock::copyToSource()
394 {
395     if (model()->tractor() && model()->tractor()->is_valid()) {
396         if (MAIN.on_actionSave_triggered()) {
397             if (!MLT.openXML(MAIN.fileName())) {
398                 MLT.producer()->set(kExportFromProperty, 1);
399                 MAIN.open(MLT.producer());
400             } else {
401                 emit showStatusMessage(tr("Failed to open ") + MAIN.fileName());
402             }
403         } else {
404             emit showStatusMessage(tr("You must save to Copy Timeline to Source."));
405         }
406     }
407 }
408 
openProperties()409 void TimelineDock::openProperties()
410 {
411     MAIN.onPropertiesDockTriggered(true);
412 }
413 
emitSelectedChanged(const QVector<int> & roles)414 void TimelineDock::emitSelectedChanged(const QVector<int> &roles)
415 {
416     if (selection().isEmpty())
417         return;
418     auto point = selection().first();
419     auto index = model()->makeIndex(point.y(), point.x());
420     emit model()->dataChanged(index, index, roles);
421 }
422 
clearSelectionIfInvalid()423 void TimelineDock::clearSelectionIfInvalid()
424 {
425     QList<QPoint> newSelection;
426     foreach (auto clip, selection()) {
427         if (clip.x() >= clipCount(clip.y()))
428             continue;
429 
430         newSelection << QPoint(clip.x(), clip.y());
431     }
432     setSelection(newSelection);
433 }
434 
insertTrack()435 void TimelineDock::insertTrack()
436 {
437     if (m_selection.selectedTrack != -1)
438         setSelection();
439     MAIN.undoStack()->push(
440                 new Timeline::InsertTrackCommand(m_model, currentTrack()));
441 }
442 
removeTrack()443 void TimelineDock::removeTrack()
444 {
445     if (m_model.trackList().size() > 0) {
446         int trackIndex = currentTrack();
447         MAIN.undoStack()->push(
448                 new Timeline::RemoveTrackCommand(m_model, trackIndex));
449         if (trackIndex >= m_model.trackList().count())
450             setCurrentTrack(m_model.trackList().count() - 1);
451     }
452 }
453 
mergeClipWithNext(int trackIndex,int clipIndex,bool dryrun)454 bool TimelineDock::mergeClipWithNext(int trackIndex, int clipIndex, bool dryrun)
455 {
456     if (dryrun)
457         return m_model.mergeClipWithNext(trackIndex, clipIndex, true);
458 
459     MAIN.undoStack()->push(
460         new Timeline::MergeCommand(m_model, trackIndex, clipIndex));
461 
462     return true;
463 }
464 
onProducerChanged(Mlt::Producer * after)465 void TimelineDock::onProducerChanged(Mlt::Producer* after)
466 {
467     int trackIndex = currentTrack();
468     if (trackIndex < 0 || selection().isEmpty() || !m_updateCommand || !after || !after->is_valid())
469         return;
470     if (isTrackLocked(trackIndex)) {
471         pulseLockButtonOnTrack(trackIndex);
472         return;
473     }
474     int i = m_model.trackList().at(trackIndex).mlt_index;
475     QScopedPointer<Mlt::Producer> track(m_model.tractor()->track(i));
476     if (track) {
477         // Ensure the new XML has same in/out point as selected clip by making
478         // a copy of the changed producer and copying the in/out from timeline.
479         Mlt::Playlist playlist(*track);
480         int clipIndex = selection().first().x();
481         QScopedPointer<Mlt::ClipInfo> info(playlist.clip_info(clipIndex));
482         if (info) {
483             double oldSpeed = qstrcmp("timewarp", info->producer->get("mlt_service")) ? 1.0 : info->producer->get_double("warp_speed");
484             double newSpeed = qstrcmp("timewarp", after->get("mlt_service")) ? 1.0 : after->get_double("warp_speed");
485             double speedRatio = oldSpeed / newSpeed;
486 
487             int length = qRound(info->length * speedRatio);
488             int in = qMin(qRound(info->frame_in * speedRatio), length - 1);
489             int out = qMin(qRound(info->frame_out * speedRatio), length - 1);
490             after->set("length", after->frames_to_time(length, mlt_time_clock));
491             after->set_in_and_out(in, out);
492 
493             // Adjust filters.
494             int n = after->filter_count();
495             for (int j = 0; j < n; j++) {
496                 QScopedPointer<Mlt::Filter> filter(after->filter(j));
497                 if (filter && filter->is_valid() && !filter->get_int("_loader")) {
498                     in = qMin(qRound(filter->get_in() * speedRatio), length - 1);
499                     out = qMin(qRound(filter->get_out() * speedRatio), length - 1);
500                     filter->set_in_and_out(in, out);
501                     //TODO: keyframes
502                 }
503             }
504         }
505     }
506     QString xmlAfter = MLT.XML(after);
507     m_updateCommand->setXmlAfter(xmlAfter);
508     setSelection(); // clearing selection prevents a crash
509     MAIN.undoStack()->push(m_updateCommand.take());
510 }
511 
addAudioTrack()512 void TimelineDock::addAudioTrack()
513 {
514     if (m_selection.selectedTrack != -1)
515         setSelection();
516     MAIN.undoStack()->push(
517         new Timeline::AddTrackCommand(m_model, false));
518 }
519 
addVideoTrack()520 void TimelineDock::addVideoTrack()
521 {
522     if (m_selection.selectedTrack != -1)
523         setSelection();
524     MAIN.undoStack()->push(
525         new Timeline::AddTrackCommand(m_model, true));
526 }
527 
onShowFrame(const SharedFrame & frame)528 void TimelineDock::onShowFrame(const SharedFrame& frame)
529 {
530     if (m_ignoreNextPositionChange) {
531         m_ignoreNextPositionChange = false;
532     } else if (MLT.isMultitrack() && m_position != frame.get_position()) {
533         m_position = frame.get_position();
534         emit positionChanged();
535     }
536 }
537 
onSeeked(int position)538 void TimelineDock::onSeeked(int position)
539 {
540     if (MLT.isMultitrack() && m_position != position) {
541         m_position = position;
542         emit positionChanged();
543     }
544 }
545 
append(int trackIndex)546 void TimelineDock::append(int trackIndex)
547 {
548     if (trackIndex < 0)
549         trackIndex = currentTrack();
550     if (isTrackLocked(trackIndex)) {
551         pulseLockButtonOnTrack(trackIndex);
552         return;
553     }
554     if (MAIN.isSourceClipMyProject()) return;
555     if (MLT.isSeekableClip() || MLT.savedProducer()) {
556         Mlt::Producer producer(MLT.isClip()? MLT.producer() : MLT.savedProducer());
557         ProxyManager::generateIfNotExists(producer);
558         MAIN.undoStack()->push(
559             new Timeline::AppendCommand(m_model, trackIndex, MLT.XML(&producer)));
560     } else if (!MLT.isSeekableClip()) {
561         emitNonSeekableWarning();
562     }
563 }
564 
remove(int trackIndex,int clipIndex)565 void TimelineDock::remove(int trackIndex, int clipIndex)
566 {
567     if (!m_model.trackList().count())
568         return;
569     if (isTrackLocked(trackIndex)) {
570         pulseLockButtonOnTrack(trackIndex);
571         return;
572     }
573     Q_ASSERT(trackIndex >= 0 && clipIndex >= 0);
574     Mlt::Producer clip = producerForClip(trackIndex, clipIndex);
575     if (clip.is_valid()) {
576         MAIN.undoStack()->push(
577             new Timeline::RemoveCommand(m_model, trackIndex, clipIndex));
578     }
579 }
580 
lift(int trackIndex,int clipIndex)581 void TimelineDock::lift(int trackIndex, int clipIndex)
582 {
583     if (!m_model.trackList().count())
584         return;
585     if (isTrackLocked(trackIndex)) {
586         pulseLockButtonOnTrack(trackIndex);
587         return;
588     }
589     if (trackIndex < 0 || clipIndex < 0) return;
590     Mlt::Producer clip(producerForClip(trackIndex, clipIndex));
591     if (clip.is_valid()) {
592         if (clip.is_blank())
593             return;
594         MAIN.undoStack()->push(
595             new Timeline::LiftCommand(m_model, trackIndex, clipIndex));
596         setSelection();
597     }
598 }
599 
removeSelection(bool withCopy)600 void TimelineDock::removeSelection(bool withCopy)
601 {
602     if (isTrackLocked(currentTrack())) {
603         pulseLockButtonOnTrack(currentTrack());
604         return;
605     }
606     if (selection().isEmpty())
607         selectClipUnderPlayhead();
608     if (selection().isEmpty() || currentTrack() < 0)
609         return;
610 
611     // Cut
612     if (withCopy) {
613         auto clip = selection().first();
614         copyClip(clip.y(), clip.x());
615         remove(clip.y(), clip.x());
616         return;
617     }
618 
619     // Ripple delete
620     int n = selection().size();
621     if (n > 1)
622         MAIN.undoStack()->beginMacro(tr("Remove %1 from timeline").arg(n));
623     int trackIndex, clipIndex;
624     for (const auto& uuid : selectionUuids()) {
625         delete m_model.findClipByUuid(uuid, trackIndex, clipIndex);
626         remove(trackIndex, clipIndex);
627     }
628     if (n > 1)
629         MAIN.undoStack()->endMacro();
630 }
631 
liftSelection()632 void TimelineDock::liftSelection()
633 {
634     if (isTrackLocked(currentTrack())) {
635         pulseLockButtonOnTrack(currentTrack());
636         return;
637     }
638     if (selection().isEmpty())
639         selectClipUnderPlayhead();
640     if (selection().isEmpty())
641         return;
642     int n = selection().size();
643     if (n > 1)
644         MAIN.undoStack()->beginMacro(tr("Lift %1 from timeline").arg(n));
645     int trackIndex, clipIndex;
646     for (const auto& uuid : selectionUuids()) {
647         delete m_model.findClipByUuid(uuid, trackIndex, clipIndex);
648         lift(trackIndex, clipIndex);
649     }
650     if (n > 1)
651         MAIN.undoStack()->endMacro();
652 }
653 
incrementCurrentTrack(int by)654 void TimelineDock::incrementCurrentTrack(int by)
655 {
656     int newTrack = currentTrack();
657     if (by < 0)
658         newTrack = qMax(0, newTrack + by);
659     else
660         newTrack = qMin(m_model.trackList().size() - 1, newTrack + by);
661     setCurrentTrack(newTrack);
662 }
663 
selectTrackHead(int trackIndex)664 void TimelineDock::selectTrackHead(int trackIndex)
665 {
666     if (trackIndex >= 0) {
667         setSelection(QList<QPoint>(), trackIndex);
668         int i = m_model.trackList().at(trackIndex).mlt_index;
669         Mlt::Producer* producer = m_model.tractor()->track(i);
670         if (producer && producer->is_valid())
671             emit selected(producer);
672         delete producer;
673     }
674 }
675 
selectMultitrack()676 void TimelineDock::selectMultitrack()
677 {
678     setSelection(QList<QPoint>(), -1, true);
679     QMetaObject::invokeMethod(m_quickView.rootObject(), "selectMultitrack");
680     emit selected(m_model.tractor());
681 }
682 
copyClip(int trackIndex,int clipIndex)683 void TimelineDock::copyClip(int trackIndex, int clipIndex)
684 {
685     if (trackIndex < 0)
686         trackIndex = currentTrack();
687     if (clipIndex < 0)
688         clipIndex = clipIndexAtPlayhead(trackIndex);
689     Q_ASSERT(trackIndex >= 0 && clipIndex >= 0);
690     QScopedPointer<Mlt::ClipInfo> info(getClipInfo(trackIndex, clipIndex));
691     if (info) {
692         QString xml = MLT.XML(info->producer);
693         Mlt::Producer p(MLT.profile(), "xml-string", xml.toUtf8().constData());
694         p.set_speed(0);
695         p.seek(info->frame_in);
696         p.set_in_and_out(info->frame_in, info->frame_out);
697         MLT.setSavedProducer(&p);
698         emit clipCopied();
699     }
700 }
701 
emitSelectedFromSelection()702 void TimelineDock::emitSelectedFromSelection()
703 {
704     if (!m_model.trackList().count()) {
705         if (m_model.tractor())
706             selectMultitrack();
707         else
708             emit selected(nullptr);
709         return;
710     }
711 
712     int trackIndex = selection().isEmpty()? currentTrack() : selection().first().y();
713     int clipIndex  = selection().isEmpty()? 0              : selection().first().x();
714     QScopedPointer<Mlt::ClipInfo> info(getClipInfo(trackIndex, clipIndex));
715     if (info && info->producer && info->producer->is_valid()) {
716         m_updateCommand.reset(new Timeline::UpdateCommand(*this, trackIndex, clipIndex, info->start));
717         // We need to set these special properties so time-based filters
718         // can get information about the cut while still applying filters
719         // to the cut parent.
720         QScopedPointer<Mlt::ClipInfo> info2(getClipInfo(trackIndex, clipIndex - 1));
721         if (info2 && info2->producer && info2->producer->is_valid()
722                   && info2->producer->get(kShotcutTransitionProperty)) {
723             // Factor in a transition left of the clip.
724             info->producer->set(kFilterInProperty, info->frame_in - info2->frame_count);
725             info->producer->set(kPlaylistStartProperty, info2->start);
726         } else {
727             info->producer->set(kFilterInProperty, info->frame_in);
728             info->producer->set(kPlaylistStartProperty, info->start);
729         }
730         info2.reset(getClipInfo(trackIndex, clipIndex + 1));
731         if (info2 && info2->producer && info2->producer->is_valid()
732                   && info2->producer->get(kShotcutTransitionProperty)) {
733             // Factor in a transition right of the clip.
734             info->producer->set(kFilterOutProperty, info->frame_out + info2->frame_count);
735         } else {
736             info->producer->set(kFilterOutProperty, info->frame_out);
737         }
738         info->producer->set(kMultitrackItemProperty, QString("%1:%2").arg(clipIndex).arg(trackIndex).toLatin1().constData());
739         m_ignoreNextPositionChange = true;
740         emit selected(info->producer);
741     }
742     m_model.tractor()->set(kFilterInProperty, 0);
743     m_model.tractor()->set(kFilterOutProperty, m_model.tractor()->get_length() - 1);
744 }
745 
remakeAudioLevels(int trackIndex,int clipIndex,bool force)746 void TimelineDock::remakeAudioLevels(int trackIndex, int clipIndex, bool force)
747 {
748     if (Settings.timelineShowWaveforms()) {
749         QModelIndex modelIndex = m_model.index(clipIndex, 0, m_model.index(trackIndex));
750         QScopedPointer<Mlt::ClipInfo> info(getClipInfo(trackIndex, clipIndex));
751         AudioLevelsTask::start(*info->producer, &m_model, modelIndex, force);
752     }
753 }
754 
commitTrimCommand()755 void TimelineDock::commitTrimCommand()
756 {
757     if (m_trimCommand && (m_trimDelta || m_transitionDelta)) {
758         if (m_undoHelper) m_trimCommand->setUndoHelper(m_undoHelper.take());
759         MAIN.undoStack()->push(m_trimCommand.take());
760     }
761     m_trimDelta = 0;
762     m_transitionDelta = 0;
763 }
764 
onRowsInserted(const QModelIndex & parent,int first,int last)765 void TimelineDock::onRowsInserted(const QModelIndex& parent, int first, int last)
766 {
767     Q_UNUSED(parent)
768     // Adjust selected clips for changed indices.
769     if (-1 == m_selection.selectedTrack) {
770         QList<QPoint> newSelection;
771         int n = last - first + 1;
772         if (parent.isValid()) {
773             foreach (auto i, m_selection.selectedClips) {
774                 if (i.x() < first)
775                     newSelection << QPoint(i.x(), parent.row());
776                 else
777                     newSelection << QPoint(i.x() + n, parent.row());
778             }
779         } else {
780             foreach (auto i, m_selection.selectedClips) {
781                 if (i.y() < first)
782                     newSelection << QPoint(i.x(), i.y());
783                 else
784                     newSelection << QPoint(i.x(), i.y() + n);
785             }
786         }
787         setSelection(newSelection);
788         if (!parent.isValid())
789             model()->reload(true);
790     }
791 }
792 
onRowsRemoved(const QModelIndex & parent,int first,int last)793 void TimelineDock::onRowsRemoved(const QModelIndex& parent, int first, int last)
794 {
795     Q_UNUSED(parent)
796     // Adjust selected clips for changed indices.
797     if (-1 == m_selection.selectedTrack && parent.isValid()) {
798         QList<QPoint> newSelection;
799         int n = last - first + 1;
800         if (parent.isValid()) {
801             foreach (auto i, m_selection.selectedClips) {
802                 if (i.x() < first)
803                     newSelection << QPoint(i.x(), parent.row());
804                 else if (i.x() > last)
805                     newSelection << QPoint(i.x() - n, parent.row());
806             }
807         } else {
808             foreach (auto i, m_selection.selectedClips) {
809                 if (i.y() < first)
810                     newSelection << QPoint(i.x(), i.y());
811                 else if (i.y() > last)
812                     newSelection << QPoint(i.x(), i.y() - n);
813             }
814         }
815         setSelection(newSelection);
816         if (!parent.isValid())
817             model()->reload(true);
818     }
819 }
820 
detachAudio(int trackIndex,int clipIndex)821 void TimelineDock::detachAudio(int trackIndex, int clipIndex)
822 {
823     if (!m_model.trackList().count())
824         return;
825     Q_ASSERT(trackIndex >= 0 && clipIndex >= 0);
826     QScopedPointer<Mlt::ClipInfo> info(getClipInfo(trackIndex, clipIndex));
827     if (info && info->producer && info->producer->is_valid() && !info->producer->is_blank()
828              && info->producer->get("audio_index") && info->producer->get_int("audio_index") >= 0) {
829         if (!info->producer->property_exists(kDefaultAudioIndexProperty)) {
830             info->producer->set(kDefaultAudioIndexProperty, info->producer->get_int("audio_index"));
831         }
832         Mlt::Producer clip(MLT.profile(), "xml-string", MLT.XML(info->producer).toUtf8().constData());
833         clip.set_in_and_out(info->frame_in, info->frame_out);
834         MAIN.undoStack()->push(
835             new Timeline::DetachAudioCommand(m_model, trackIndex, clipIndex, info->start, MLT.XML(&clip)));
836     }
837 }
838 
selectAll()839 void TimelineDock::selectAll()
840 {
841     QList<QPoint> selection;
842     for (int y = 0; y < m_model.rowCount(); y++) {
843         for (int x = 0; x < m_model.rowCount(m_model.index(y)); x++) {
844             if (!isBlank(y, x) && !isTrackLocked(y))
845                 selection << QPoint(x, y);
846         }
847     }
848     setSelection(selection);
849 }
850 
blockSelection(bool block)851 bool TimelineDock::blockSelection(bool block)
852 {
853     m_blockSetSelection = block;
854     return m_blockSetSelection;
855 }
856 
onProducerModified()857 void TimelineDock::onProducerModified()
858 {
859     // The clip name may have changed.
860     emitSelectedChanged(QVector<int>() << MultitrackModel::NameRole);
861 }
862 
replace(int trackIndex,int clipIndex,const QString & xml)863 void TimelineDock::replace(int trackIndex, int clipIndex, const QString& xml)
864 {
865     if (xml.isEmpty() && !MLT.isClip() && !MLT.savedProducer()) {
866         emit showStatusMessage(tr("There is nothing in the Source player."));
867         return;
868     }
869     if (!m_model.trackList().count() || MAIN.isSourceClipMyProject())
870         return;
871     if (trackIndex < 0)
872         trackIndex = currentTrack();
873     if (isTrackLocked(trackIndex)) {
874         pulseLockButtonOnTrack(trackIndex);
875         return;
876     }
877     if (clipIndex < 0)
878         clipIndex = clipIndexAtPlayhead(trackIndex);
879     Mlt::Producer producer(producerForClip(trackIndex, clipIndex));
880     if (producer.is_valid() && producer.type() == tractor_type) {
881         emit showStatusMessage(tr("You cannot replace a transition."));
882         return;
883     }
884     if (MLT.isSeekableClip() || MLT.savedProducer() || !xml.isEmpty()) {
885         Q_ASSERT(trackIndex >= 0 && clipIndex >= 0);
886         QString xmlToUse = !xml.isEmpty()? xml
887             : MLT.XML(MLT.isClip()? nullptr : MLT.savedProducer());
888         MAIN.undoStack()->push(
889             new Timeline::ReplaceCommand(m_model, trackIndex, clipIndex, xmlToUse));
890     } else if (!MLT.isSeekableClip()) {
891         emitNonSeekableWarning();
892     }
893 }
894 
setTrackName(int trackIndex,const QString & value)895 void TimelineDock::setTrackName(int trackIndex, const QString &value)
896 {
897     MAIN.undoStack()->push(
898         new Timeline::NameTrackCommand(m_model, trackIndex, value));
899 }
900 
toggleTrackMute(int trackIndex)901 void TimelineDock::toggleTrackMute(int trackIndex)
902 {
903     MAIN.undoStack()->push(
904         new Timeline::MuteTrackCommand(m_model, trackIndex));
905 }
906 
toggleTrackHidden(int trackIndex)907 void TimelineDock::toggleTrackHidden(int trackIndex)
908 {
909     MAIN.undoStack()->push(
910         new Timeline::HideTrackCommand(m_model, trackIndex));
911 }
912 
setTrackComposite(int trackIndex,bool composite)913 void TimelineDock::setTrackComposite(int trackIndex, bool composite)
914 {
915     MAIN.undoStack()->push(
916         new Timeline::CompositeTrackCommand(m_model, trackIndex, composite));
917 }
918 
setTrackLock(int trackIndex,bool lock)919 void TimelineDock::setTrackLock(int trackIndex, bool lock)
920 {
921     MAIN.undoStack()->push(
922         new Timeline::LockTrackCommand(m_model, trackIndex, lock));
923 }
924 
moveClip(int fromTrack,int toTrack,int clipIndex,int position,bool ripple)925 bool TimelineDock::moveClip(int fromTrack, int toTrack, int clipIndex, int position, bool ripple)
926 {
927     if (toTrack >= 0 && clipIndex >= 0) {
928         int length = 0;
929         int i = m_model.trackList().at(fromTrack).mlt_index;
930         Mlt::Producer track(m_model.tractor()->track(i));
931         if (track.is_valid()) {
932             Mlt::Playlist playlist(track);
933             length = playlist.clip_length(clipIndex);
934         }
935         i = m_model.trackList().at(toTrack).mlt_index;
936         track = Mlt::Producer(m_model.tractor()->track(i));
937         if (track.is_valid()) {
938             Mlt::Playlist playlist(track);
939             if (m_model.isTransition(playlist, playlist.get_clip_index_at(position)) ||
940                 m_model.isTransition(playlist, playlist.get_clip_index_at(position + length - 1))) {
941                 return false;
942             }
943         }
944     }
945     if (selection().size() <= 1 && m_model.addTransitionValid(fromTrack, toTrack, clipIndex, position, ripple)) {
946         emit transitionAdded(fromTrack, clipIndex, position, ripple);
947         if (m_updateCommand)
948             m_updateCommand->setPosition(toTrack, clipIndex, position);
949     } else {
950         // Check for locked tracks
951         auto trackDelta = toTrack - fromTrack;
952         for (const auto& clip : selection()) {
953             auto trackIndex = clip.y() + trackDelta;
954             if (isTrackLocked(clip.y())) {
955                 pulseLockButtonOnTrack(clip.y());
956                 return false;
957             }
958             if (isTrackLocked(trackIndex)) {
959                 pulseLockButtonOnTrack(trackIndex);
960                 return false;
961             }
962         }
963 
964         // Workaround bug #326 moving clips between tracks stops allowing drag-n-drop
965         // into Timeline, which appeared with Qt 5.6 upgrade.
966         emit clipMoved(fromTrack, toTrack, clipIndex, position, ripple);
967         if (m_updateCommand)
968             m_updateCommand->setPosition(toTrack, clipIndex, position);
969     }
970     return true;
971 }
972 
onClipMoved(int fromTrack,int toTrack,int clipIndex,int position,bool ripple)973 void TimelineDock::onClipMoved(int fromTrack, int toTrack, int clipIndex, int position, bool ripple)
974 {
975     int n = selection().size();
976     if (n > 0) {
977         // determine the position delta
978         for (const auto& clip : selection()) {
979             if (clip.y() == fromTrack && clip.x() == clipIndex) {
980                 QScopedPointer<Mlt::ClipInfo> info(getClipInfo(clip.y(), clip.x()));
981                 if (info) {
982                     position -= info->start;
983                     break;
984                 }
985             }
986         }
987         auto command = new Timeline::MoveClipCommand(m_model, toTrack - fromTrack, ripple);
988 
989         // Copy selected
990         for (const auto& clip : selection()) {
991             QScopedPointer<Mlt::ClipInfo> info(getClipInfo(clip.y(), clip.x()));
992             if (info && info->cut) {
993                 LOG_DEBUG() << "moving clip at" << clip << "start" << info->start << "+" << position << "=" << info->start + position;
994                 info->cut->set(kPlaylistStartProperty, info->start + position);
995                 command->selection().insert(info->start, *info->cut);
996             }
997         }
998         setSelection();
999         TimelineSelectionBlocker blocker(*this);
1000         MAIN.undoStack()->push(command);
1001     }
1002 }
1003 
trimClipIn(int trackIndex,int clipIndex,int oldClipIndex,int delta,bool ripple)1004 bool TimelineDock::trimClipIn(int trackIndex, int clipIndex, int oldClipIndex, int delta, bool ripple)
1005 {
1006     if (!ripple && m_model.addTransitionByTrimInValid(trackIndex, clipIndex, delta)) {
1007         clipIndex = m_model.addTransitionByTrimIn(trackIndex, clipIndex, delta);
1008         m_transitionDelta += delta;
1009         m_trimCommand.reset(new Timeline::AddTransitionByTrimInCommand(m_model, trackIndex, clipIndex - 1, m_transitionDelta, m_trimDelta, false));
1010         if (m_updateCommand && m_updateCommand->trackIndex() == trackIndex && m_updateCommand->clipIndex() == clipIndex)
1011             m_updateCommand->setPosition(trackIndex, clipIndex, -1);
1012     }
1013     else if (!ripple && m_model.removeTransitionByTrimInValid(trackIndex, clipIndex, delta)) {
1014         Q_ASSERT(trackIndex >= 0 && clipIndex >= 0);
1015         QModelIndex modelIndex = m_model.makeIndex(trackIndex, clipIndex - 1);
1016         int n = m_model.data(modelIndex, MultitrackModel::DurationRole).toInt();
1017         m_model.liftClip(trackIndex, clipIndex - 1);
1018         m_model.trimClipIn(trackIndex, clipIndex, -n, false, false);
1019         m_trimDelta += delta;
1020         m_trimCommand.reset(new Timeline::RemoveTransitionByTrimInCommand(m_model, trackIndex, clipIndex - 1, m_trimDelta, false));
1021         if (m_updateCommand && m_updateCommand->trackIndex() == trackIndex && m_updateCommand->clipIndex() == clipIndex)
1022             m_updateCommand->setPosition(trackIndex, clipIndex - 1, -1);
1023     }
1024     else if (!ripple && m_model.trimTransitionOutValid(trackIndex, clipIndex, delta)) {
1025         m_model.trimTransitionOut(trackIndex, clipIndex, delta);
1026         m_trimDelta += delta;
1027         m_trimCommand.reset(new Timeline::TrimTransitionOutCommand(m_model, trackIndex, clipIndex, m_trimDelta, false));
1028     }
1029     else if (m_model.trimClipInValid(trackIndex, clipIndex, delta, ripple)) {
1030         if (!m_undoHelper) {
1031             m_undoHelper.reset(new UndoHelper(m_model));
1032             if (ripple) {
1033                 m_undoHelper->setHints(UndoHelper::SkipXML);
1034             } else {
1035                 m_undoHelper->setHints(UndoHelper::RestoreTracks);
1036             }
1037             m_undoHelper->recordBeforeState();
1038         }
1039         clipIndex = m_model.trimClipIn(trackIndex, clipIndex, delta, ripple, Settings.timelineRippleAllTracks());
1040 
1041         m_trimDelta += delta;
1042         m_trimCommand.reset(new Timeline::TrimClipInCommand(m_model, trackIndex, oldClipIndex, m_trimDelta, ripple, false));
1043         if (m_updateCommand && m_updateCommand->trackIndex() == trackIndex && m_updateCommand->clipIndex() == clipIndex)
1044             m_updateCommand->setPosition(trackIndex, clipIndex, m_updateCommand->position() + delta);
1045     }
1046     else return false;
1047 
1048     // Update duration in properties
1049     QScopedPointer<Mlt::ClipInfo> info(getClipInfo(trackIndex, clipIndex));
1050     if (info && !info->producer->get_int(kShotcutSequenceProperty))
1051         emit durationChanged();
1052 
1053     return true;
1054 }
1055 
trimClipOut(int trackIndex,int clipIndex,int delta,bool ripple)1056 bool TimelineDock::trimClipOut(int trackIndex, int clipIndex, int delta, bool ripple)
1057 {
1058     if (!ripple && m_model.addTransitionByTrimOutValid(trackIndex, clipIndex, delta)) {
1059         m_model.addTransitionByTrimOut(trackIndex, clipIndex, delta);
1060         m_transitionDelta += delta;
1061         m_trimCommand.reset(new Timeline::AddTransitionByTrimOutCommand(m_model, trackIndex, clipIndex, m_transitionDelta, m_trimDelta, false));
1062         if (m_updateCommand && m_updateCommand->trackIndex() == trackIndex && m_updateCommand->clipIndex() == clipIndex)
1063             m_updateCommand->setPosition(trackIndex, clipIndex, -1);
1064     }
1065     else if (!ripple && m_model.removeTransitionByTrimOutValid(trackIndex, clipIndex, delta)) {
1066         Q_ASSERT(trackIndex >= 0 && clipIndex >= 0);
1067         QModelIndex modelIndex = m_model.makeIndex(trackIndex, clipIndex + 1);
1068         int n = m_model.data(modelIndex, MultitrackModel::DurationRole).toInt();
1069         m_model.liftClip(trackIndex, clipIndex + 1);
1070         m_model.trimClipOut(trackIndex, clipIndex, -n, false, false);
1071         m_trimDelta += delta;
1072         m_trimCommand.reset(new Timeline::RemoveTransitionByTrimOutCommand(m_model, trackIndex, clipIndex + 1, m_trimDelta, false));
1073         if (m_updateCommand && m_updateCommand->trackIndex() == trackIndex && m_updateCommand->clipIndex() == clipIndex)
1074             m_updateCommand->setPosition(trackIndex, clipIndex, -1);
1075     }
1076     else if (!ripple && m_model.trimTransitionInValid(trackIndex, clipIndex, delta)) {
1077         m_model.trimTransitionIn(trackIndex, clipIndex, delta);
1078         m_trimDelta += delta;
1079         m_trimCommand.reset(new Timeline::TrimTransitionInCommand(m_model, trackIndex, clipIndex, m_trimDelta, false));
1080     }
1081     else if (m_model.trimClipOutValid(trackIndex, clipIndex, delta, ripple)) {
1082         if (!m_undoHelper) {
1083             m_undoHelper.reset(new UndoHelper(m_model));
1084             if (ripple) m_undoHelper->setHints(UndoHelper::SkipXML);
1085             m_undoHelper->recordBeforeState();
1086         }
1087         m_model.trimClipOut(trackIndex, clipIndex, delta, ripple, Settings.timelineRippleAllTracks());
1088 
1089         m_trimDelta += delta;
1090         m_trimCommand.reset(new Timeline::TrimClipOutCommand(m_model, trackIndex, clipIndex, m_trimDelta, ripple, false));
1091         if (m_updateCommand && m_updateCommand->trackIndex() == trackIndex && m_updateCommand->clipIndex() == clipIndex)
1092             m_updateCommand->setPosition(trackIndex, clipIndex,-1);
1093     }
1094     else return false;
1095 
1096     // Update duration in properties
1097     QScopedPointer<Mlt::ClipInfo> info(getClipInfo(trackIndex, clipIndex));
1098     if (info && !info->producer->get_int(kShotcutSequenceProperty))
1099         emit durationChanged();
1100 
1101     return true;
1102 }
1103 
convertUrlsToXML(const QString & xml)1104 static QString convertUrlsToXML(const QString& xml)
1105 {
1106     if (xml.startsWith(kFileUrlProtocol)) {
1107         LongUiTask longTask(QObject::tr("Drop Files"));
1108         Mlt::Playlist playlist(MLT.profile());
1109         QList<QUrl> urls;
1110         const auto& strings = xml.split(kFilesUrlDelimiter);
1111         for (auto s : strings) {
1112 #ifdef Q_OS_WIN
1113             if (!s.startsWith(kFileUrlProtocol)) {
1114                 s.prepend(kFileUrlProtocol);
1115             }
1116 #endif
1117             QUrl url(s);
1118             urls << Util::removeFileScheme(url);
1119         }
1120         int i = 0, count = urls.size();
1121         for (const auto& path : Util::sortedFileList(urls)) {
1122             if (MAIN.isSourceClipMyProject(path, /* withDialog */ false)) continue;
1123             longTask.reportProgress(Util::baseName(path), i++, count);
1124             Mlt::Producer p;
1125             if (path.endsWith(".mlt") || path.endsWith(".xml")) {
1126                 p = Mlt::Producer(MLT.profile(), "xml", path.toUtf8().constData());
1127                 if (p.is_valid()) {
1128                     p.set(kShotcutVirtualClip, 1);
1129                     p.set("resource", path.toUtf8().constData());
1130                 }
1131             } else {
1132                 p = Mlt::Producer(MLT.profile(), path.toUtf8().constData());
1133             }
1134             if (p.is_valid()) {
1135                 // Convert avformat to avformat-novalidate so that XML loads faster.
1136                 if (!qstrcmp(p.get("mlt_service"), "avformat")) {
1137                     if (!p.get_int("seekable")) {
1138                         MAIN.showStatusMessage(QObject::tr("Not adding non-seekable file: ") + Util::baseName(path));
1139                         continue;
1140                     }
1141                     p.set("mlt_service", "avformat-novalidate");
1142                     p.set("mute_on_pause", 0);
1143                 }
1144                 ProxyManager::generateIfNotExists(p);
1145                 MLT.setImageDurationFromDefault(&p);
1146                 MLT.lockCreationTime(&p);
1147                 p.get_length_time(mlt_time_clock);
1148                 playlist.append(p);
1149             }
1150         }
1151         return MLT.XML(&playlist);
1152     }
1153     return xml;
1154 }
1155 
insert(int trackIndex,int position,const QString & xml,bool seek)1156 void TimelineDock::insert(int trackIndex, int position, const QString &xml, bool seek)
1157 {
1158     // Validations
1159     if (trackIndex < 0)
1160         trackIndex = currentTrack();
1161     if (isTrackLocked(trackIndex)) {
1162         pulseLockButtonOnTrack(trackIndex);
1163         return;
1164     }
1165     if (xml.contains(MAIN.fileName()) && MAIN.isSourceClipMyProject()) return;
1166 
1167     // Handle drop from file manager to empty project.
1168     if ((!MLT.producer() || !MLT.producer()->is_valid()) && xml.startsWith(kFileUrlProtocol)) {
1169         QUrl url = xml.split(kFilesUrlDelimiter).first();
1170         Mlt::Properties properties;
1171         properties.set(kShotcutSkipConvertProperty, 1);
1172         MAIN.open(Util::removeFileScheme(url), &properties, false /* play */ );
1173     }
1174 
1175     if (MLT.isSeekableClip() || MLT.savedProducer() || !xml.isEmpty()) {
1176         QString xmlToUse;
1177         QScopedPointer<TimelineSelectionBlocker> selectBlocker;
1178         if (xml.isEmpty()) {
1179             Mlt::Producer producer(MLT.isClip()? MLT.producer() : MLT.savedProducer());
1180             ProxyManager::generateIfNotExists(producer);
1181             xmlToUse = MLT.XML(&producer);
1182         } else {
1183             xmlToUse = convertUrlsToXML(xml);
1184             if (xml.startsWith(kFileUrlProtocol) && xml.split(kFilesUrlDelimiter).size() > 1) {
1185                 selectBlocker.reset(new TimelineSelectionBlocker(*this));
1186             }
1187         }
1188         if (position < 0)
1189             position = m_position;
1190         if (m_model.trackList().size() == 0)
1191             position = 0;
1192         MAIN.undoStack()->push(
1193             new Timeline::InsertCommand(m_model, trackIndex, position, xmlToUse, seek));
1194     } else if (!MLT.isSeekableClip()) {
1195         emitNonSeekableWarning();
1196     }
1197 }
1198 
selectClip(int trackIndex,int clipIndex)1199 void TimelineDock::selectClip(int trackIndex, int clipIndex)
1200 {
1201     setSelection(QList<QPoint>() << QPoint(clipIndex, trackIndex));
1202 }
1203 
overwrite(int trackIndex,int position,const QString & xml,bool seek)1204 void TimelineDock::overwrite(int trackIndex, int position, const QString &xml, bool seek)
1205 {
1206     // Validations
1207     if (trackIndex < 0)
1208         trackIndex = currentTrack();
1209     if (isTrackLocked(trackIndex)) {
1210         pulseLockButtonOnTrack(trackIndex);
1211         return;
1212     }
1213     if (xml.contains(MAIN.fileName()) && MAIN.isSourceClipMyProject()) return;
1214 
1215     // Handle drop from file manager to empty project.
1216     if ((!MLT.producer() || !MLT.producer()->is_valid()) && xml.startsWith(kFileUrlProtocol)) {
1217         QUrl url = xml.split(kFilesUrlDelimiter).first();
1218         Mlt::Properties properties;
1219         properties.set(kShotcutSkipConvertProperty, 1);
1220         MAIN.open(Util::removeFileScheme(url), &properties, false /* play */ );
1221     }
1222 
1223     if (MLT.isSeekableClip() || MLT.savedProducer() || !xml.isEmpty()) {
1224         QString xmlToUse;
1225         QScopedPointer<TimelineSelectionBlocker> selectBlocker;
1226         if (xml.isEmpty()) {
1227             Mlt::Producer producer(MLT.isClip()? MLT.producer() : MLT.savedProducer());
1228             ProxyManager::generateIfNotExists(producer);
1229             xmlToUse = MLT.XML(&producer);
1230         } else {
1231             xmlToUse = convertUrlsToXML(xml);
1232             if (xml.startsWith(kFileUrlProtocol) && xml.split(kFilesUrlDelimiter).size() > 1) {
1233                 selectBlocker.reset(new TimelineSelectionBlocker(*this));
1234             }
1235         }
1236         if (position < 0)
1237             position = m_position;
1238         if (m_model.trackList().size() == 0)
1239             position = 0;
1240         MAIN.undoStack()->push(
1241             new Timeline::OverwriteCommand(m_model, trackIndex, position, xmlToUse, seek));
1242     } else if (!MLT.isSeekableClip()) {
1243         emitNonSeekableWarning();
1244     }
1245 }
1246 
appendFromPlaylist(Mlt::Playlist * playlist,bool skipProxy)1247 void TimelineDock::appendFromPlaylist(Mlt::Playlist *playlist, bool skipProxy)
1248 {
1249     int trackIndex = currentTrack();
1250     if (isTrackLocked(trackIndex)) {
1251         pulseLockButtonOnTrack(trackIndex);
1252         return;
1253     }
1254     // Workaround a bug with first slide of slideshow animation not working.
1255     if (skipProxy) {
1256         // Initialize the multitrack with a bogus clip and remove it.
1257         Mlt::Producer producer(playlist->get_clip(0));
1258         auto clipIndex = m_model.appendClip(trackIndex, producer);
1259         if (clipIndex >= 0)
1260             m_model.removeClip(trackIndex, clipIndex, Settings.timelineRippleAllTracks());
1261     }
1262     disconnect(&m_model, &MultitrackModel::appended, this, &TimelineDock::selectClip);
1263     MAIN.undoStack()->push(
1264         new Timeline::AppendCommand(m_model, trackIndex, MLT.XML(playlist), skipProxy));
1265     connect(&m_model, &MultitrackModel::appended, this, &TimelineDock::selectClip, Qt::QueuedConnection);
1266 }
1267 
splitClip(int trackIndex,int clipIndex)1268 void TimelineDock::splitClip(int trackIndex, int clipIndex)
1269 {
1270     if (trackIndex < 0 || clipIndex < 0)
1271         chooseClipAtPosition(m_position, trackIndex, clipIndex);
1272     if (trackIndex < 0 || clipIndex < 0)
1273         return;
1274     setCurrentTrack(trackIndex);
1275     if (clipIndex >= 0 && trackIndex >= 0) {
1276         int i = m_model.trackList().at(trackIndex).mlt_index;
1277         QScopedPointer<Mlt::Producer> track(m_model.tractor()->track(i));
1278         if (track) {
1279             Mlt::Playlist playlist(*track);
1280             if (!m_model.isTransition(playlist, clipIndex)) {
1281                 QScopedPointer<Mlt::ClipInfo> info(getClipInfo(trackIndex, clipIndex));
1282                 if (info && m_position > info->start && m_position < info->start + info->frame_count) {
1283                     MAIN.undoStack()->push(
1284                         new Timeline::SplitCommand(m_model, trackIndex, clipIndex, m_position));
1285                 }
1286             } else {
1287                 emit showStatusMessage(tr("You cannot split a transition."));
1288             }
1289         }
1290     }
1291 }
1292 
fadeIn(int trackIndex,int clipIndex,int duration)1293 void TimelineDock::fadeIn(int trackIndex, int clipIndex, int duration)
1294 {
1295     if (isTrackLocked(trackIndex)) {
1296         pulseLockButtonOnTrack(trackIndex);
1297         return;
1298     }
1299     if (duration < 0) return;
1300     Q_ASSERT(trackIndex >= 0 && clipIndex >= 0);
1301     MAIN.undoStack()->push(
1302         new Timeline::FadeInCommand(m_model, trackIndex, clipIndex, duration));
1303     emit fadeInChanged(duration);
1304 }
1305 
fadeOut(int trackIndex,int clipIndex,int duration)1306 void TimelineDock::fadeOut(int trackIndex, int clipIndex, int duration)
1307 {
1308     if (isTrackLocked(trackIndex)) {
1309         pulseLockButtonOnTrack(trackIndex);
1310         return;
1311     }
1312     if (duration < 0) return;
1313     Q_ASSERT(trackIndex >= 0 && clipIndex >= 0);
1314     MAIN.undoStack()->push(
1315         new Timeline::FadeOutCommand(m_model, trackIndex, clipIndex, duration));
1316     emit fadeOutChanged(duration);
1317 }
1318 
seekPreviousEdit()1319 void TimelineDock::seekPreviousEdit()
1320 {
1321     if (!MLT.isMultitrack()) return;
1322     if (!m_model.tractor()) return;
1323 
1324     int newPosition = -1;
1325     int n = m_model.tractor()->count();
1326     for (int i = 0; i < n; i++) {
1327         QScopedPointer<Mlt::Producer> track(m_model.tractor()->track(i));
1328         if (track) {
1329             Mlt::Playlist playlist(*track);
1330             int clipIndex = playlist.get_clip_index_at(m_position);
1331             if (clipIndex >= 0 && m_position == playlist.clip_start(clipIndex))
1332                 --clipIndex;
1333             if (clipIndex >= 0)
1334                 newPosition = qMax(newPosition, playlist.clip_start(clipIndex));
1335         }
1336     }
1337     if (newPosition != m_position)
1338         setPosition(newPosition);
1339 }
1340 
seekNextEdit()1341 void TimelineDock::seekNextEdit()
1342 {
1343     if (!MLT.isMultitrack()) return;
1344     if (!m_model.tractor()) return;
1345 
1346     int newPosition = std::numeric_limits<int>::max();
1347     int n = m_model.tractor()->count();
1348     for (int i = 0; i < n; i++) {
1349         QScopedPointer<Mlt::Producer> track(m_model.tractor()->track(i));
1350         if (track) {
1351             Mlt::Playlist playlist(*track);
1352             int clipIndex = playlist.get_clip_index_at(m_position) + 1;
1353             if (clipIndex < playlist.count())
1354                 newPosition = qMin(newPosition, playlist.clip_start(clipIndex));
1355             else if (clipIndex == playlist.count())
1356                 newPosition = qMin(newPosition, playlist.clip_start(clipIndex) + playlist.clip_length(clipIndex));
1357         }
1358     }
1359     if (newPosition != m_position)
1360         setPosition(newPosition);
1361 }
1362 
seekInPoint(int clipIndex)1363 void TimelineDock::seekInPoint(int clipIndex)
1364 {
1365     if (!MLT.isMultitrack()) return;
1366     if (!m_model.tractor()) return;
1367     if (clipIndex < 0) return;
1368 
1369     int mltTrackIndex = m_model.trackList().at(currentTrack()).mlt_index;
1370     QScopedPointer<Mlt::Producer> track(m_model.tractor()->track(mltTrackIndex));
1371     if (track) {
1372         Mlt::Playlist playlist(*track);
1373         if (m_position != playlist.clip_start(clipIndex))
1374             setPosition(playlist.clip_start(clipIndex));
1375     }
1376 }
1377 
dragEnterEvent(QDragEnterEvent * event)1378 void TimelineDock::dragEnterEvent(QDragEnterEvent *event)
1379 {
1380     LOG_DEBUG() << event->mimeData()->hasFormat(Mlt::XmlMimeType);
1381     if (event->mimeData()->hasFormat(Mlt::XmlMimeType)) {
1382         event->acceptProposedAction();
1383     }
1384 }
1385 
dragMoveEvent(QDragMoveEvent * event)1386 void TimelineDock::dragMoveEvent(QDragMoveEvent *event)
1387 {
1388     emit dragging(event->posF(), event->mimeData()->text().toInt());
1389 }
1390 
dragLeaveEvent(QDragLeaveEvent * event)1391 void TimelineDock::dragLeaveEvent(QDragLeaveEvent *event)
1392 {
1393     Q_UNUSED(event);
1394     emit dropped();
1395 }
1396 
dropEvent(QDropEvent * event)1397 void TimelineDock::dropEvent(QDropEvent *event)
1398 {
1399     if (event->mimeData()->hasFormat(Mlt::XmlMimeType)) {
1400         int trackIndex = currentTrack();
1401         if (trackIndex >= 0) {
1402             emit dropAccepted(QString::fromUtf8(event->mimeData()->data(Mlt::XmlMimeType)));
1403             event->acceptProposedAction();
1404         }
1405     }
1406     emit dropped();
1407 }
1408 
event(QEvent * event)1409 bool TimelineDock::event(QEvent *event)
1410 {
1411     bool result = QDockWidget::event(event);
1412     if (event->type() == QEvent::PaletteChange || event->type() == QEvent::StyleChange)
1413         load(true);
1414     return result;
1415 }
1416 
keyPressEvent(QKeyEvent * event)1417 void TimelineDock::keyPressEvent(QKeyEvent* event)
1418 {
1419     QDockWidget::keyPressEvent(event);
1420     if (!event->isAccepted())
1421         MAIN.keyPressEvent(event);
1422 }
1423 
keyReleaseEvent(QKeyEvent * event)1424 void TimelineDock::keyReleaseEvent(QKeyEvent* event)
1425 {
1426     QDockWidget::keyReleaseEvent(event);
1427     if (!event->isAccepted())
1428         MAIN.keyReleaseEvent(event);
1429 }
1430 
load(bool force)1431 void TimelineDock::load(bool force)
1432 {
1433     if (m_quickView.source().isEmpty() || force) {
1434         QDir sourcePath = QmlUtilities::qmlDir();
1435         sourcePath.cd("views");
1436         sourcePath.cd("timeline");
1437         m_quickView.setFocusPolicy(isFloating()? Qt::NoFocus : Qt::StrongFocus);
1438         m_quickView.setSource(QUrl::fromLocalFile(sourcePath.filePath("timeline.qml")));
1439         connect(m_quickView.rootObject(), SIGNAL(currentTrackChanged()),
1440                 this, SIGNAL(currentTrackChanged()));
1441         connect(m_quickView.rootObject(), SIGNAL(clipClicked()),
1442                 this, SIGNAL(clipClicked()));
1443         if (force && Settings.timelineShowWaveforms())
1444             m_model.reload();
1445     } else if (Settings.timelineShowWaveforms()) {
1446         m_model.reload();
1447     }
1448 }
1449 
onTopLevelChanged(bool floating)1450 void TimelineDock::onTopLevelChanged(bool floating)
1451 {
1452     m_quickView.setFocusPolicy(floating? Qt::NoFocus : Qt::StrongFocus);
1453 }
1454 
onTransitionAdded(int trackIndex,int clipIndex,int position,bool ripple)1455 void TimelineDock::onTransitionAdded(int trackIndex, int clipIndex, int position, bool ripple)
1456 {
1457     setSelection(); // cleared
1458     Timeline::AddTransitionCommand* command = new Timeline::AddTransitionCommand(*this, trackIndex, clipIndex, position, ripple);
1459     MAIN.undoStack()->push(command);
1460     // Select the transition.
1461     setSelection(QList<QPoint>() << QPoint(command->getTransitionIndex(), trackIndex));
1462 }
1463 
1464 class FindProducersByHashParser : public Mlt::Parser
1465 {
1466 private:
1467     QString m_hash;
1468     QList<Mlt::Producer> m_producers;
1469 
1470 public:
FindProducersByHashParser(const QString & hash)1471     FindProducersByHashParser(const QString& hash)
1472         : Mlt::Parser()
1473         , m_hash(hash)
1474     {}
1475 
producers()1476     QList<Mlt::Producer>& producers() { return m_producers; }
1477 
on_start_filter(Mlt::Filter *)1478     int on_start_filter(Mlt::Filter*) { return 0; }
on_start_producer(Mlt::Producer * producer)1479     int on_start_producer(Mlt::Producer* producer) {
1480         if (producer->is_cut() && Util::getHash(producer->parent()) == m_hash)
1481             m_producers << Mlt::Producer(producer);
1482         return 0;
1483     }
on_end_producer(Mlt::Producer *)1484     int on_end_producer(Mlt::Producer*) { return 0; }
on_start_playlist(Mlt::Playlist *)1485     int on_start_playlist(Mlt::Playlist*) { return 0; }
on_end_playlist(Mlt::Playlist *)1486     int on_end_playlist(Mlt::Playlist*) { return 0; }
on_start_tractor(Mlt::Tractor *)1487     int on_start_tractor(Mlt::Tractor*) { return 0; }
on_end_tractor(Mlt::Tractor *)1488     int on_end_tractor(Mlt::Tractor*) { return 0; }
on_start_multitrack(Mlt::Multitrack *)1489     int on_start_multitrack(Mlt::Multitrack*) { return 0; }
on_end_multitrack(Mlt::Multitrack *)1490     int on_end_multitrack(Mlt::Multitrack*) { return 0; }
on_start_track()1491     int on_start_track() { return 0; }
on_end_track()1492     int on_end_track() { return 0; }
on_end_filter(Mlt::Filter *)1493     int on_end_filter(Mlt::Filter*) { return 0; }
on_start_transition(Mlt::Transition *)1494     int on_start_transition(Mlt::Transition*) { return 0; }
on_end_transition(Mlt::Transition *)1495     int on_end_transition(Mlt::Transition*) { return 0; }
1496 };
1497 
replaceClipsWithHash(const QString & hash,Mlt::Producer & producer)1498 void TimelineDock::replaceClipsWithHash(const QString& hash, Mlt::Producer& producer)
1499 {
1500     FindProducersByHashParser parser(hash);
1501     parser.start(*model()->tractor());
1502     auto n = parser.producers().size();
1503     if (n > 1)
1504         MAIN.undoStack()->beginMacro(tr("Replace %n timeline clips", nullptr, n));
1505     for (auto& clip : parser.producers()) {
1506         int trackIndex = -1;
1507         int clipIndex = -1;
1508         // lookup the current track and clip index by UUID
1509         QScopedPointer<Mlt::ClipInfo> info(MAIN.timelineClipInfoByUuid(clip.get(kUuidProperty), trackIndex, clipIndex));
1510 
1511         if (info && info->producer->is_valid() && trackIndex >= 0 && clipIndex >= 0 && info->producer->type() != tractor_type) {
1512             if (producer.get_int(kIsProxyProperty) && info->producer->get_int(kIsProxyProperty)) {
1513                 // Not much to do on a proxy clip but change its resource
1514                 info->producer->set(kOriginalResourceProperty, producer.get("resource"));
1515                 auto caption = Util::baseName(ProxyManager::resource(*info->producer));
1516                 if (!::qstrcmp(info->producer->get("mlt_service"), "timewarp")) {
1517                     caption = QString("%1 (%2x)").arg(caption, info->producer->get("warp_speed"));
1518                 }
1519                 info->producer->set(kShotcutCaptionProperty, caption.toUtf8().constData());
1520             } else {
1521                 int in = clip.get_in();
1522                 int out = clip.get_out();
1523 
1524                 // Factor in a transition left of the clip.
1525                 QScopedPointer<Mlt::ClipInfo> info2(getClipInfo(trackIndex, clipIndex - 1));
1526                 if (info2 && info2->producer && info2->producer->is_valid()
1527                           && info2->producer->get(kShotcutTransitionProperty)) {
1528                     in -= info2->frame_count;
1529                 }
1530                 // Factor in a transition right of the clip.
1531                 info2.reset(getClipInfo(trackIndex, clipIndex + 1));
1532                 if (info2 && info2->producer && info2->producer->is_valid()
1533                           && info2->producer->get(kShotcutTransitionProperty)) {
1534                     out += info2->frame_count;
1535                 }
1536                 Util::applyCustomProperties(producer, *info->producer, in, out);
1537 
1538                 replace(trackIndex, clipIndex, MLT.XML(&producer));
1539             }
1540         }
1541     }
1542     if (n > 1)
1543         MAIN.undoStack()->endMacro();
1544 }
1545