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