1 /*
2 SPDX-FileCopyrightText: 2017 Jean-Baptiste Mardelle
3 SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
4 */
5
6 #include "timelinecontroller.h"
7 #include "../model/timelinefunctions.hpp"
8 #include "assets/keyframes/model/keyframemodellist.hpp"
9 #include "bin/bin.h"
10 #include "bin/clipcreator.hpp"
11 #include "bin/model/markerlistmodel.hpp"
12 #include "bin/model/subtitlemodel.hpp"
13 #include "bin/projectclip.h"
14 #include "bin/projectfolder.h"
15 #include "bin/projectitemmodel.h"
16 #include "core.h"
17 #include "dialogs/spacerdialog.h"
18 #include "dialogs/speeddialog.h"
19 #include "dialogs/speechdialog.h"
20 #include "doc/kdenlivedoc.h"
21 #include "effects/effectsrepository.hpp"
22 #include "effects/effectstack/model/effectstackmodel.hpp"
23 #include "kdenlivesettings.h"
24 #include "lib/audio/audioEnvelope.h"
25 #include "mainwindow.h"
26 #include "monitor/monitormanager.h"
27 #include "previewmanager.h"
28 #include "project/projectmanager.h"
29 #include "timeline2/model/clipmodel.hpp"
30 #include "timeline2/model/compositionmodel.hpp"
31 #include "timeline2/model/groupsmodel.hpp"
32 #include "timeline2/model/timelineitemmodel.hpp"
33 #include "timeline2/model/trackmodel.hpp"
34 #include "timeline2/view/dialogs/clipdurationdialog.h"
35 #include "timeline2/view/dialogs/trackdialog.h"
36 #include "transitions/transitionsrepository.hpp"
37 #include "audiomixer/mixermanager.hpp"
38 #include "ui_import_subtitle_ui.h"
39 #include "timeline2/view/timelinewidget.h"
40 #include "dialogs/timeremap.h"
41
42 #include <KColorScheme>
43 #include <KMessageBox>
44 #include <KUrlRequesterDialog>
45 #include <KRecentDirs>
46 #include <QApplication>
47 #include <QClipboard>
48 #include <QQuickItem>
49 #include <memory>
50 #include <unistd.h>
51
52 int TimelineController::m_duration = 0;
53
TimelineController(QObject * parent)54 TimelineController::TimelineController(QObject *parent)
55 : QObject(parent)
56 , multicamIn(-1)
57 , m_root(nullptr)
58 , m_usePreview(false)
59 , m_audioRef(-1)
60 , m_zone(-1, -1)
61 , m_activeTrack(-1)
62 , m_scale(QFontMetrics(QApplication::font()).maxWidth() / 250)
63 , m_timelinePreview(nullptr)
64 , m_ready(false)
65 , m_snapStackIndex(-1)
66 , m_effectZone({0,0})
67 {
68 m_disablePreview = pCore->currentDoc()->getAction(QStringLiteral("disable_preview"));
69 connect(m_disablePreview, &QAction::triggered, this, &TimelineController::disablePreview);
70 m_disablePreview->setEnabled(false);
71 connect(pCore.get(), &Core::finalizeRecording, this, &TimelineController::finishRecording);
72 connect(pCore.get(), &Core::autoScrollChanged, this, &TimelineController::autoScrollChanged);
73 connect(pCore->mixer(), &MixerManager::recordAudio, this, &TimelineController::switchRecording);
74 }
75
~TimelineController()76 TimelineController::~TimelineController()
77 {
78 }
79
prepareClose()80 void TimelineController::prepareClose()
81 {
82 // Clear root so we don't call its methods anymore
83 QObject::disconnect( m_deleteConnection );
84 disconnect(this, &TimelineController::selectionChanged, this, &TimelineController::updateClipActions);
85 disconnect(m_model.get(), &TimelineModel::selectionChanged, this, &TimelineController::selectionChanged);
86 disconnect(this, &TimelineController::videoTargetChanged, this, &TimelineController::updateVideoTarget);
87 disconnect(this, &TimelineController::audioTargetChanged, this, &TimelineController::updateAudioTarget);
88 disconnect(m_model.get(), &TimelineModel::selectedMixChanged, this, &TimelineController::showMixModel);
89 disconnect(m_model.get(), &TimelineModel::selectedMixChanged, this, &TimelineController::selectedMixChanged);
90 m_ready = false;
91 m_root = nullptr;
92 // Delete timeline preview before resetting model so that removing clips from timeline doesn't invalidate
93 delete m_timelinePreview;
94 m_timelinePreview = nullptr;
95 m_model.reset();
96 }
97
setModel(std::shared_ptr<TimelineItemModel> model)98 void TimelineController::setModel(std::shared_ptr<TimelineItemModel> model)
99 {
100 delete m_timelinePreview;
101 m_zone = QPoint(-1, -1);
102 m_timelinePreview = nullptr;
103 m_model = std::move(model);
104 m_activeSnaps.clear();
105 connect(m_model.get(), &TimelineItemModel::requestClearAssetView, pCore.get(), &Core::clearAssetPanel);
106 m_deleteConnection = connect(m_model.get(), &TimelineItemModel::checkItemDeletion, this, [this] (int id) {
107 if (m_ready) {
108 QMetaObject::invokeMethod(m_root, "checkDeletion", Qt::QueuedConnection, Q_ARG(QVariant, id));
109 }
110 });
111 connect(m_model.get(), &TimelineItemModel::showTrackEffectStack, this, [&](int tid) {
112 if (tid > -1) {
113 showTrackAsset(tid);
114 } else {
115 showMasterEffects();
116 }
117 });
118 connect(this, &TimelineController::selectionChanged, this, &TimelineController::updateClipActions);
119 connect(this, &TimelineController::selectionChanged, this, &TimelineController::updateTrimmingMode);
120 connect(this, &TimelineController::videoTargetChanged, this, &TimelineController::updateVideoTarget);
121 connect(this, &TimelineController::audioTargetChanged, this, &TimelineController::updateAudioTarget);
122 connect(m_model.get(), &TimelineItemModel::requestMonitorRefresh, [&]() { pCore->requestMonitorRefresh(); });
123 connect(m_model.get(), &TimelineModel::invalidateZone, this, &TimelineController::invalidateZone, Qt::DirectConnection);
124 connect(m_model.get(), &TimelineModel::durationUpdated, this, &TimelineController::checkDuration);
125 connect(m_model.get(), &TimelineModel::selectionChanged, this, &TimelineController::selectionChanged);
126 connect(m_model.get(), &TimelineModel::selectedMixChanged, this, &TimelineController::showMixModel);
127 connect(m_model.get(), &TimelineModel::selectedMixChanged, this, &TimelineController::selectedMixChanged);
128 connect(m_model.get(), &TimelineModel::checkTrackDeletion, this, &TimelineController::checkTrackDeletion, Qt::DirectConnection);
129 }
130
restoreTargetTracks()131 void TimelineController::restoreTargetTracks()
132 {
133 setTargetTracks(m_hasVideoTarget, m_model->m_binAudioTargets);
134 }
135
setTargetTracks(bool hasVideo,QMap<int,QString> audioTargets)136 void TimelineController::setTargetTracks(bool hasVideo, QMap <int, QString> audioTargets)
137 {
138 int videoTrack = -1;
139 m_model->m_binAudioTargets = audioTargets;
140 QMap<int, int> audioTracks;
141 m_hasVideoTarget = hasVideo;
142 m_hasAudioTarget = audioTargets.size();
143 if (m_hasVideoTarget) {
144 videoTrack = m_model->getFirstVideoTrackIndex();
145 }
146 if (m_hasAudioTarget > 0) {
147 QVector <int> tracks;
148 auto it = m_model->m_allTracks.cbegin();
149 while (it != m_model->m_allTracks.cend()) {
150 if ((*it)->isAudioTrack()) {
151 tracks << (*it)->getId();
152 }
153 ++it;
154 }
155 if (KdenliveSettings::multistream_checktrack() && audioTargets.count() > tracks.count()) {
156 pCore->bin()->checkProjectAudioTracks(QString(), audioTargets.count());
157 }
158 QMapIterator <int, QString>st(audioTargets);
159 while (st.hasNext()) {
160 st.next();
161 if (tracks.isEmpty()) {
162 break;
163 }
164 audioTracks.insert(tracks.takeLast(), st.key());
165 }
166 }
167 emit hasAudioTargetChanged();
168 emit hasVideoTargetChanged();
169 setVideoTarget(m_hasVideoTarget && (m_lastVideoTarget > -1) ? m_lastVideoTarget : videoTrack);
170 setAudioTarget(audioTracks);
171 }
172
getModel() const173 std::shared_ptr<TimelineItemModel> TimelineController::getModel() const
174 {
175 return m_model;
176 }
177
setRoot(QQuickItem * root)178 void TimelineController::setRoot(QQuickItem *root)
179 {
180 m_root = root;
181 m_ready = true;
182 }
183
tractor()184 Mlt::Tractor *TimelineController::tractor()
185 {
186 return m_model->tractor();
187 }
188
trackProducer(int tid)189 Mlt::Producer TimelineController::trackProducer(int tid)
190 {
191 return *(m_model->getTrackById(tid).get());
192 }
193
scaleFactor() const194 double TimelineController::scaleFactor() const
195 {
196 return m_scale;
197 }
198
getTrackNameFromMltIndex(int trackPos)199 const QString TimelineController::getTrackNameFromMltIndex(int trackPos)
200 {
201 if (trackPos == -1) {
202 return i18n("unknown");
203 }
204 if (trackPos == 0) {
205 return i18n("Black");
206 }
207 return m_model->getTrackTagById(m_model->getTrackIndexFromPosition(trackPos - 1));
208 }
209
getTrackNameFromIndex(int trackIndex)210 const QString TimelineController::getTrackNameFromIndex(int trackIndex)
211 {
212 QString trackName = m_model->getTrackFullName(trackIndex);
213 return trackName.isEmpty() ? m_model->getTrackTagById(trackIndex) : trackName;
214 }
215
getTrackNames(bool videoOnly)216 QMap<int, QString> TimelineController::getTrackNames(bool videoOnly)
217 {
218 QMap<int, QString> names;
219 for (const auto &track : m_model->m_iteratorTable) {
220 if (videoOnly && m_model->getTrackById_const(track.first)->isAudioTrack()) {
221 continue;
222 }
223 QString trackName = m_model->getTrackFullName(track.first);
224 names[m_model->getTrackMltIndex(track.first)] = trackName;
225 }
226 return names;
227 }
228
setScaleFactorOnMouse(double scale,bool zoomOnMouse)229 void TimelineController::setScaleFactorOnMouse(double scale, bool zoomOnMouse)
230 {
231 if (m_root) {
232 m_root->setProperty("zoomOnMouse", zoomOnMouse ? qBound(0, getMousePos(), duration()) : -1);
233 m_scale = scale;
234 emit scaleFactorChanged();
235 } else {
236 qWarning("Timeline root not created, impossible to zoom in");
237 }
238 }
239
setScaleFactor(double scale)240 void TimelineController::setScaleFactor(double scale)
241 {
242 m_scale = scale;
243 // Update mainwindow's zoom slider
244 emit updateZoom(scale);
245 // inform qml
246 emit scaleFactorChanged();
247 }
248
duration() const249 int TimelineController::duration() const
250 {
251 return m_duration;
252 }
253
fullDuration() const254 int TimelineController::fullDuration() const
255 {
256 return m_duration + TimelineModel::seekDuration;
257 }
258
checkDuration()259 void TimelineController::checkDuration()
260 {
261 int currentLength = m_model->duration();
262 if (currentLength != m_duration) {
263 m_duration = currentLength;
264 emit durationChanged();
265 }
266 }
267
selectedTrack() const268 int TimelineController::selectedTrack() const
269 {
270 std::unordered_set<int> sel = m_model->getCurrentSelection();
271 if (sel.empty()) return -1;
272 std::vector<std::pair<int, int>> selected_tracks; // contains pairs of (track position, track id) for each selected item
273 for (int s : sel) {
274 int tid = m_model->getItemTrackId(s);
275 selected_tracks.emplace_back(m_model->getTrackPosition(tid), tid);
276 }
277 // sort by track position
278 std::sort(selected_tracks.begin(), selected_tracks.begin(), [](const auto &a, const auto &b) { return a.first < b.first; });
279 return selected_tracks.front().second;
280 }
281
selectCurrentItem(ObjectType type,bool select,bool addToCurrent,bool showErrorMsg)282 bool TimelineController::selectCurrentItem(ObjectType type, bool select, bool addToCurrent, bool showErrorMsg)
283 {
284 int currentClip = -1;
285 if (type == ObjectType::TimelineClip) {
286 currentClip = m_activeTrack == -2 ? m_model->getSubtitleByPosition(pCore->getTimelinePosition()) : m_model->getClipByPosition(m_activeTrack, pCore->getTimelinePosition());
287 } else if (type == ObjectType::TimelineComposition) {
288 currentClip = m_model->getCompositionByPosition(m_activeTrack, pCore->getTimelinePosition());
289 } else if (type == ObjectType::TimelineMix) {
290 if (m_activeTrack >= 0) {
291 currentClip = m_model->getClipByPosition(m_activeTrack, pCore->getTimelinePosition());
292 }
293 if (currentClip > -1) {
294 if (m_model->hasClipEndMix(currentClip)) {
295 int mixPartner = m_model->getTrackById_const(m_activeTrack)->getSecondMixPartner(currentClip);
296 int clipEnd = m_model->getClipPosition(currentClip) + m_model->getClipPlaytime(currentClip);
297 int mixStart = clipEnd - m_model->getMixDuration(mixPartner);
298 if (mixStart < pCore->getTimelinePosition() && pCore->getTimelinePosition() < clipEnd) {
299 if (select) {
300 m_model->requestMixSelection(mixPartner);
301 return true;
302 } else if (selectedMix() == mixPartner) {
303 m_model->requestClearSelection();
304 return true;
305 }
306 }
307 }
308 int delta = pCore->getTimelinePosition() - m_model->getClipPosition(currentClip);
309 if (m_model->getMixDuration(currentClip) >= delta) {
310 if (select) {
311 m_model->requestMixSelection(currentClip);
312 return true;
313 } else if (selectedMix() == currentClip) {
314 m_model->requestClearSelection();
315 return true;
316 }
317 return true;
318 } else {
319 currentClip = -1;
320 }
321 }
322 }
323
324 if (currentClip == -1) {
325 if (showErrorMsg) {
326 pCore->displayMessage(i18n("No item under timeline cursor in active track"), ErrorMessage, 500);
327 }
328 return false;
329 }
330 if (!select) {
331 m_model->requestRemoveFromSelection(currentClip);
332 } else {
333 bool grouped = m_model->m_groups->isInGroup(currentClip);
334 m_model->requestAddToSelection(currentClip, !addToCurrent);
335 if (grouped) {
336 // If part of a group, ensure the effect/composition stack displays the selected item's properties
337 showAsset(currentClip);
338 }
339 }
340 return true;
341 }
342
selection() const343 QList<int> TimelineController::selection() const
344 {
345 if (!m_root) return QList<int>();
346 std::unordered_set<int> sel = m_model->getCurrentSelection();
347 QList<int> items;
348 for (int id : sel) {
349 items << id;
350 }
351 return items;
352 }
353
selectedMix() const354 int TimelineController::selectedMix() const
355 {
356 return m_model->m_selectedMix;
357 }
358
selectItems(const QList<int> & ids)359 void TimelineController::selectItems(const QList<int> &ids)
360 {
361 std::unordered_set<int> ids_s(ids.begin(), ids.end());
362 m_model->requestSetSelection(ids_s);
363 }
364
setScrollPos(int pos)365 void TimelineController::setScrollPos(int pos)
366 {
367 if (pos > 0 && m_root) {
368 QMetaObject::invokeMethod(m_root, "setScrollPos", Qt::QueuedConnection, Q_ARG(QVariant, pos));
369 }
370 }
371
resetView()372 void TimelineController::resetView()
373 {
374 m_model->_resetView();
375 if (m_root) {
376 QMetaObject::invokeMethod(m_root, "updatePalette");
377 }
378 emit colorsChanged();
379 }
380
snap()381 bool TimelineController::snap()
382 {
383 return KdenliveSettings::snaptopoints();
384 }
385
ripple()386 bool TimelineController::ripple()
387 {
388 return false;
389 }
390
scrub()391 bool TimelineController::scrub()
392 {
393 return false;
394 }
395
insertClip(int tid,int position,const QString & data_str,bool logUndo,bool refreshView,bool useTargets)396 int TimelineController::insertClip(int tid, int position, const QString &data_str, bool logUndo, bool refreshView, bool useTargets)
397 {
398 int id;
399 if (tid == -1) {
400 tid = m_activeTrack;
401 }
402 if (position == -1) {
403 position = pCore->getTimelinePosition();
404 }
405 if (!m_model->requestClipInsertion(data_str, tid, position, id, logUndo, refreshView, useTargets)) {
406 id = -1;
407 }
408 return id;
409 }
410
insertClips(int tid,int position,const QStringList & binIds,bool logUndo,bool refreshView)411 QList<int> TimelineController::insertClips(int tid, int position, const QStringList &binIds, bool logUndo, bool refreshView)
412 {
413 QList<int> clipIds;
414 if (tid == -1) {
415 tid = m_activeTrack;
416 }
417 if (position == -1) {
418 position = pCore->getTimelinePosition();
419 }
420 TimelineFunctions::requestMultipleClipsInsertion(m_model, binIds, tid, position, clipIds, logUndo, refreshView);
421 // we don't need to check the return value of the above function, in case of failure it will return an empty list of ids.
422 return clipIds;
423 }
424
insertNewMix(int tid,int position,const QString transitionId)425 void TimelineController::insertNewMix(int tid, int position, const QString transitionId)
426 {
427 int clipId = m_model->getTrackById_const(tid)->getClipByPosition(position);
428 if (clipId > 0) {
429 m_model->mixClip(clipId, transitionId);
430 }
431 }
432
insertNewComposition(int tid,int position,const QString & transitionId,bool logUndo)433 int TimelineController::insertNewComposition(int tid, int position, const QString &transitionId, bool logUndo)
434 {
435 int clipId = m_model->getTrackById_const(tid)->getClipByPosition(position);
436 if (clipId > 0) {
437 int minimum = m_model->getClipPosition(clipId);
438 return insertNewComposition(tid, clipId, position - minimum, transitionId, logUndo);
439 }
440 return insertComposition(tid, position, transitionId, logUndo);
441 }
442
insertNewComposition(int tid,int clipId,int offset,const QString & transitionId,bool logUndo)443 int TimelineController::insertNewComposition(int tid, int clipId, int offset, const QString &transitionId, bool logUndo)
444 {
445 int id;
446 int minimumPos = clipId > -1 ? m_model->getClipPosition(clipId) : offset;
447 int clip_duration = clipId > -1 ? m_model->getClipPlaytime(clipId) : pCore->getDurationFromString(KdenliveSettings::transition_duration());
448 int endPos = minimumPos + clip_duration;
449 int position = minimumPos;
450 int duration = qMin(clip_duration, pCore->getDurationFromString(KdenliveSettings::transition_duration()));
451 int lowerVideoTrackId = m_model->getPreviousVideoTrackIndex(tid);
452 bool revert = offset > clip_duration / 2;
453 int bottomId = 0;
454 if (lowerVideoTrackId > 0) {
455 bottomId = m_model->getTrackById_const(lowerVideoTrackId)->getClipByPosition(position + offset);
456 }
457 if (bottomId <= 0) {
458 // No video track underneath
459 if (offset < duration && duration < 2 * clip_duration) {
460 // Composition dropped close to start, keep default composition duration
461 } else if (clip_duration - offset < duration * 1.2 && duration < 2 * clip_duration) {
462 // Composition dropped close to end, keep default composition duration
463 position = endPos - duration;
464 } else {
465 // Use full clip length for duration
466 duration = m_model->getTrackById_const(tid)->suggestCompositionLength(position);
467 }
468 } else {
469 duration = qMin(duration, m_model->getTrackById_const(tid)->suggestCompositionLength(position));
470 QPair<int, int> bottom(m_model->m_allClips[bottomId]->getPosition(), m_model->m_allClips[bottomId]->getPlaytime());
471 if (bottom.first > minimumPos) {
472 // Lower clip is after top clip
473 if (position + offset > bottom.first) {
474 int test_duration = m_model->getTrackById_const(tid)->suggestCompositionLength(bottom.first);
475 if (test_duration > 0) {
476 offset -= (bottom.first - position);
477 position = bottom.first;
478 duration = test_duration;
479 revert = position > minimumPos;
480 }
481 }
482 } else if (position >= bottom.first) {
483 // Lower clip is before or at same pos as top clip
484 int test_duration = m_model->getTrackById_const(lowerVideoTrackId)->suggestCompositionLength(position);
485 if (test_duration > 0) {
486 duration = qMin(test_duration, clip_duration);
487 }
488 }
489 }
490 int defaultLength = pCore->getDurationFromString(KdenliveSettings::transition_duration());
491 bool isShortComposition = TransitionsRepository::get()->getType(transitionId) == AssetListType::AssetType::VideoShortComposition;
492 if (duration < 0 || (isShortComposition && duration > 1.5 * defaultLength)) {
493 duration = defaultLength;
494 } else if (duration <= 1) {
495 // if suggested composition duration is lower than 4 frames, use default
496 duration = pCore->getDurationFromString(KdenliveSettings::transition_duration());
497 if (minimumPos + clip_duration - position < 3) {
498 position = minimumPos + clip_duration - duration;
499 }
500 }
501 QPair<int, int> finalPos = m_model->getTrackById_const(tid)->validateCompositionLength(position, offset, duration, endPos);
502 position = finalPos.first;
503 duration = finalPos.second;
504
505 std::unique_ptr<Mlt::Properties> props(nullptr);
506 if (revert) {
507 props = std::make_unique<Mlt::Properties>();
508 if (transitionId == QLatin1String("dissolve")) {
509 props->set("reverse", 1);
510 } else if (transitionId == QLatin1String("composite") || transitionId == QLatin1String("slide")) {
511 props->set("invert", 1);
512 } else if (transitionId == QLatin1String("wipe")) {
513 props->set("geometry", "0=0% 0% 100% 100% 100%;-1=0% 0% 100% 100% 0%");
514 }
515 }
516 if (!m_model->requestCompositionInsertion(transitionId, tid, position, duration, std::move(props), id, logUndo)) {
517 id = -1;
518 pCore->displayMessage(i18n("Could not add composition at selected position"), ErrorMessage, 500);
519 }
520 return id;
521 }
522
isOnCut(int cid) const523 int TimelineController::isOnCut(int cid) const
524 {
525 Q_ASSERT(m_model->isComposition(cid));
526 int tid = m_model->getItemTrackId(cid);
527 return m_model->getTrackById_const(tid)->isOnCut(cid);
528 }
529
insertComposition(int tid,int position,const QString & transitionId,bool logUndo)530 int TimelineController::insertComposition(int tid, int position, const QString &transitionId, bool logUndo)
531 {
532 int id;
533 int duration = pCore->getDurationFromString(KdenliveSettings::transition_duration());
534 if (!m_model->requestCompositionInsertion(transitionId, tid, position, duration, nullptr, id, logUndo)) {
535 id = -1;
536 }
537 return id;
538 }
539
deleteSelectedClips()540 void TimelineController::deleteSelectedClips()
541 {
542 if (dragOperationRunning()) {
543 // Don't allow timeline operation while drag in progress
544 pCore->displayMessage(i18n("Cannot perform operation while dragging in timeline"), ErrorMessage);
545 return;
546 }
547 auto sel = m_model->getCurrentSelection();
548 if (sel.empty()) {
549 // Check if a mix is selected
550 if (m_model->m_selectedMix > -1 && m_model->isClip(m_model->m_selectedMix)) {
551 m_model->removeMix(m_model->m_selectedMix);
552 m_model->clearAssetView(m_model->m_selectedMix);
553 m_model->requestClearSelection(true);
554 }
555 return;
556 }
557 // only need to delete the first item, the others will be deleted in cascade
558 if (m_model->m_editMode == TimelineMode::InsertEdit) {
559 // In insert mode, perform an extract operation (don't leave gaps)
560 extract(*sel.begin());
561 }
562 else {
563 m_model->requestItemDeletion(*sel.begin());
564 }
565 }
566
getMainSelectedItem(bool restrictToCurrentPos,bool allowComposition)567 int TimelineController::getMainSelectedItem(bool restrictToCurrentPos, bool allowComposition)
568 {
569 auto sel = m_model->getCurrentSelection();
570 if (sel.empty() || sel.size() > 2) {
571 return -1;
572 }
573 int itemId = *(sel.begin());
574 if (sel.size() == 2) {
575 int parentGroup = m_model->m_groups->getRootId(itemId);
576 if (parentGroup == -1 || m_model->m_groups->getType(parentGroup) != GroupType::AVSplit) {
577 return -1;
578 }
579 }
580 if (!restrictToCurrentPos) {
581 if (m_model->isClip(itemId) || (allowComposition && m_model->isComposition(itemId))) {
582 return itemId;
583 }
584 }
585 if (m_model->isClip(itemId)) {
586 int position = pCore->getTimelinePosition();
587 int start = m_model->getClipPosition(itemId);
588 int end = start + m_model->getClipPlaytime(itemId);
589 if (position >= start && position <= end) {
590 return itemId;
591 }
592 }
593 return -1;
594 }
595
copyItem()596 void TimelineController::copyItem()
597 {
598 std::unordered_set<int> selectedIds = m_model->getCurrentSelection();
599 if (selectedIds.empty()) {
600 return;
601 }
602 int clipId = *(selectedIds.begin());
603 QString copyString = TimelineFunctions::copyClips(m_model, selectedIds);
604 QClipboard *clipboard = QApplication::clipboard();
605 clipboard->setText(copyString);
606 m_root->setProperty("copiedClip", clipId);
607 m_model->requestSetSelection(selectedIds);
608 }
609
pasteItem(int position,int tid)610 bool TimelineController::pasteItem(int position, int tid)
611 {
612 QClipboard *clipboard = QApplication::clipboard();
613 QString txt = clipboard->text();
614 if (tid == -1) {
615 tid = m_activeTrack;
616 }
617 if (position == -1) {
618 position = getMenuOrTimelinePos();
619 }
620 return TimelineFunctions::pasteClips(m_model, txt, tid, position);
621 }
622
triggerAction(const QString & name)623 void TimelineController::triggerAction(const QString &name)
624 {
625 pCore->triggerAction(name);
626 }
627
actionText(const QString & name)628 const QString TimelineController::actionText(const QString &name)
629 {
630 return pCore->actionText(name);
631 }
632
timecode(int frames) const633 QString TimelineController::timecode(int frames) const
634 {
635 return KdenliveSettings::frametimecode() ? QString::number(frames) : m_model->tractor()->frames_to_time(frames, mlt_time_smpte_df);
636 }
637
framesToClock(int frames) const638 QString TimelineController::framesToClock(int frames) const
639 {
640 return m_model->tractor()->frames_to_time(frames, mlt_time_clock);
641 }
642
simplifiedTC(int frames) const643 QString TimelineController::simplifiedTC(int frames) const
644 {
645 if (KdenliveSettings::frametimecode()) {
646 return QString::number(frames);
647 }
648 QString s = m_model->tractor()->frames_to_time(frames, mlt_time_smpte_df);
649 return s.startsWith(QLatin1String("00:")) ? s.remove(0, 3) : s;
650 }
651
showThumbnails() const652 bool TimelineController::showThumbnails() const
653 {
654 return KdenliveSettings::videothumbnails();
655 }
656
showAudioThumbnails() const657 bool TimelineController::showAudioThumbnails() const
658 {
659 return KdenliveSettings::audiothumbnails();
660 }
661
showMarkers() const662 bool TimelineController::showMarkers() const
663 {
664 return KdenliveSettings::showmarkers();
665 }
666
audioThumbFormat() const667 bool TimelineController::audioThumbFormat() const
668 {
669 return KdenliveSettings::displayallchannels();
670 }
671
audioThumbNormalize() const672 bool TimelineController::audioThumbNormalize() const
673 {
674 return KdenliveSettings::normalizechannels();
675 }
676
showWaveforms() const677 bool TimelineController::showWaveforms() const
678 {
679 return KdenliveSettings::audiothumbnails();
680 }
681
addTrack(int tid)682 void TimelineController::addTrack(int tid)
683 {
684 if (tid == -1) {
685 tid = m_activeTrack;
686 }
687 QPointer<TrackDialog> d = new TrackDialog(m_model, tid, qApp->activeWindow());
688 if (d->exec() == QDialog::Accepted) {
689 bool audioRecTrack = d->addRecTrack();
690 bool addAVTrack = d->addAVTrack();
691 int tracksCount = d->tracksCount();
692 Fun undo = []() { return true; };
693 Fun redo = []() { return true; };
694 bool result = true;
695 for (int ix = 0; ix < tracksCount; ++ix) {
696 int newTid;
697 result = m_model->requestTrackInsertion(d->selectedTrackPosition(), newTid, d->trackName(), d->addAudioTrack(), undo, redo);
698 if (result) {
699 if (addAVTrack) {
700 int newTid2;
701 int mirrorPos = 0;
702 int mirrorId = m_model->getMirrorAudioTrackId(newTid);
703 if (mirrorId > -1) {
704 mirrorPos = m_model->getTrackMltIndex(mirrorId);
705 }
706 result = m_model->requestTrackInsertion(mirrorPos, newTid2, d->trackName(), true, undo, redo);
707 }
708 if (audioRecTrack) {
709 m_model->setTrackProperty(newTid, "kdenlive:audio_rec", QStringLiteral("1"));
710 }
711 } else {
712 break;
713 }
714 }
715 if (result) {
716 pCore->pushUndo(undo, redo, addAVTrack || tracksCount > 1 ? i18nc("@action", "Insert Tracks") : i18nc("@action", "Insert Track"));
717 } else {
718 pCore->displayMessage(i18n("Could not insert track"), ErrorMessage, 500);
719 undo();
720 }
721 }
722 }
723
deleteMultipleTracks(int tid)724 void TimelineController::deleteMultipleTracks(int tid)
725 {
726 Fun undo = []() { return true; };
727 Fun redo = []() { return true; };
728 bool result = true;
729 QPointer<TrackDialog> d = new TrackDialog(m_model, tid, qApp->activeWindow(), true, m_activeTrack);
730 if (tid == -1) {
731 tid = m_activeTrack;
732 }
733 if (d->exec() == QDialog::Accepted) {
734 QList<int> allIds = d->toDeleteTrackIds();
735 for (int selectedTrackIx : qAsConst(allIds)) {
736 result = m_model->requestTrackDeletion(selectedTrackIx, undo, redo);
737 if (!result) {
738 break;
739 }
740 if (m_activeTrack == -1) {
741 setActiveTrack(m_model->getTrackIndexFromPosition(m_model->getTracksCount() - 1));
742 }
743 }
744 if (result) {
745 pCore->pushUndo(undo, redo, allIds.count() > 1 ? i18n("Delete Tracks") : i18n("Delete Track"));
746 }
747 }
748 }
749
switchTrackRecord(int tid)750 void TimelineController::switchTrackRecord(int tid)
751 {
752 if (tid == -1) {
753 tid = m_activeTrack;
754 }
755 if (!m_model->getTrackById_const(tid)->isAudioTrack()) {
756 pCore->displayMessage(i18n("Select an audio track to display record controls"), ErrorMessage, 500);
757 }
758 int recDisplayed = m_model->getTrackProperty(tid, QStringLiteral("kdenlive:audio_rec")).toInt();
759 if (recDisplayed == 1) {
760 // Disable rec controls
761 m_model->setTrackProperty(tid, QStringLiteral("kdenlive:audio_rec"), QStringLiteral("0"));
762 } else {
763 // Enable rec controls
764 m_model->setTrackProperty(tid, QStringLiteral("kdenlive:audio_rec"), QStringLiteral("1"));
765 }
766 QModelIndex ix = m_model->makeTrackIndexFromID(tid);
767 if (ix.isValid()) {
768 emit m_model->dataChanged(ix, ix, {TimelineModel::AudioRecordRole});
769 }
770 }
771
checkTrackDeletion(int selectedTrackIx)772 void TimelineController::checkTrackDeletion(int selectedTrackIx)
773 {
774 if (m_activeTrack == selectedTrackIx) {
775 // Make sure we don't keep an index on a deleted track
776 m_activeTrack = -1;
777 emit activeTrackChanged();
778 }
779 if (m_model->m_audioTarget.contains(selectedTrackIx)) {
780 QMap<int, int> selection = m_model->m_audioTarget;
781 selection.remove(selectedTrackIx);
782 setAudioTarget(selection);
783 }
784 if (m_model->m_videoTarget == selectedTrackIx) {
785 setVideoTarget(-1);
786 }
787 if (m_lastAudioTarget.contains(selectedTrackIx)) {
788 m_lastAudioTarget.remove(selectedTrackIx);
789 emit lastAudioTargetChanged();
790 }
791 if (m_lastVideoTarget == selectedTrackIx) {
792 m_lastVideoTarget = -1;
793 emit lastVideoTargetChanged();
794 }
795 }
796
showConfig(int page,int tab)797 void TimelineController::showConfig(int page, int tab)
798 {
799 emit pCore->showConfigDialog(page, tab);
800 }
801
gotoNextSnap()802 void TimelineController::gotoNextSnap()
803 {
804 if (m_activeSnaps.empty() || pCore->undoIndex() != m_snapStackIndex) {
805 m_snapStackIndex = pCore->undoIndex();
806 m_activeSnaps.clear();
807 m_activeSnaps = pCore->currentDoc()->getGuideModel()->getSnapPoints();
808 m_activeSnaps.push_back(m_zone.x());
809 m_activeSnaps.push_back(m_zone.y() - 1);
810 }
811 int nextSnap = m_model->getNextSnapPos(pCore->getTimelinePosition(), m_activeSnaps);
812 if (nextSnap > pCore->getTimelinePosition()) {
813 setPosition(nextSnap);
814 }
815 }
816
gotoPreviousSnap()817 void TimelineController::gotoPreviousSnap()
818 {
819 if (pCore->getTimelinePosition() > 0) {
820 if (m_activeSnaps.empty() || pCore->undoIndex() != m_snapStackIndex) {
821 m_snapStackIndex = pCore->undoIndex();
822 m_activeSnaps.clear();
823 m_activeSnaps = pCore->currentDoc()->getGuideModel()->getSnapPoints();
824 m_activeSnaps.push_back(m_zone.x());
825 m_activeSnaps.push_back(m_zone.y() - 1);
826 }
827 setPosition(m_model->getPreviousSnapPos(pCore->getTimelinePosition(), m_activeSnaps));
828 }
829 }
830
gotoNextGuide()831 void TimelineController::gotoNextGuide()
832 {
833 QList<CommentedTime> guides = pCore->currentDoc()->getGuideModel()->getAllMarkers();
834 int pos = pCore->getTimelinePosition();
835 double fps = pCore->getCurrentFps();
836 for (auto &guide : guides) {
837 if (guide.time().frames(fps) > pos) {
838 setPosition(guide.time().frames(fps));
839 return;
840 }
841 }
842 setPosition(m_duration - 1);
843 }
844
gotoPreviousGuide()845 void TimelineController::gotoPreviousGuide()
846 {
847 if (pCore->getTimelinePosition() > 0) {
848 QList<CommentedTime> guides = pCore->currentDoc()->getGuideModel()->getAllMarkers();
849 int pos = pCore->getTimelinePosition();
850 double fps = pCore->getCurrentFps();
851 int lastGuidePos = 0;
852 for (auto &guide : guides) {
853 if (guide.time().frames(fps) >= pos) {
854 setPosition(lastGuidePos);
855 return;
856 }
857 lastGuidePos = guide.time().frames(fps);
858 }
859 setPosition(lastGuidePos);
860 }
861 }
862
groupSelection()863 void TimelineController::groupSelection()
864 {
865 if (dragOperationRunning()) {
866 // Don't allow timeline operation while drag in progress
867 pCore->displayMessage(i18n("Cannot perform operation while dragging in timeline"), ErrorMessage);
868 return;
869 }
870 const auto selection = m_model->getCurrentSelection();
871 if (selection.size() < 2) {
872 pCore->displayMessage(i18n("Select at least 2 items to group"), ErrorMessage, 500);
873 return;
874 }
875 m_model->requestClearSelection();
876 m_model->requestClipsGroup(selection);
877 m_model->requestSetSelection(selection);
878 }
879
unGroupSelection(int cid)880 void TimelineController::unGroupSelection(int cid)
881 {
882 if (dragOperationRunning()) {
883 // Don't allow timeline operation while drag in progress
884 pCore->displayMessage(i18n("Cannot perform operation while dragging in timeline"), ErrorMessage);
885 return;
886 }
887 auto ids = m_model->getCurrentSelection();
888 // ask to unselect if needed
889 m_model->requestClearSelection();
890 if (cid > -1) {
891 ids.insert(cid);
892 }
893 if (!ids.empty()) {
894 m_model->requestClipsUngroup(ids);
895 }
896 }
897
dragOperationRunning()898 bool TimelineController::dragOperationRunning()
899 {
900 QVariant returnedValue;
901 QMetaObject::invokeMethod(m_root, "isDragging", Q_RETURN_ARG(QVariant, returnedValue));
902 return returnedValue.toBool();
903 }
904
trimmingActive()905 bool TimelineController::trimmingActive()
906 {
907 ToolType::ProjectTool tool = pCore->window()->getCurrentTimeline()->activeTool();
908 return tool == ToolType::SlideTool || tool == ToolType::SlipTool || tool == ToolType::RippleTool || tool == ToolType::RollTool;
909 }
910
setInPoint(bool ripple)911 void TimelineController::setInPoint(bool ripple)
912 {
913 if (dragOperationRunning()) {
914 // Don't allow timeline operation while drag in progress
915 pCore->displayMessage(i18n("Cannot perform operation while dragging in timeline"), ErrorMessage);
916 qDebug()<< "Cannot operate while dragging";
917 return;
918 }
919 auto requestResize = [this, ripple](int id, int size){
920 if (ripple) {
921 m_model->requestItemRippleResize(m_model, id, size, false, true, 0, false);
922 setPosition(m_model->getItemPosition(id));
923 } else {
924 m_model->requestItemResize(id, size, false, true, 0, false);
925 }
926 };
927 int cursorPos = pCore->getTimelinePosition();
928 const auto selection = m_model->getCurrentSelection();
929 bool selectionFound = false;
930 if (!selection.empty()) {
931 for (int id : selection) {
932 int start = m_model->getItemPosition(id);
933 if (start == cursorPos) {
934 continue;
935 }
936 int size = start + m_model->getItemPlaytime(id) - cursorPos;
937 requestResize(id, size);
938 selectionFound = true;
939 }
940 }
941 if (!selectionFound) {
942 if (m_activeTrack >= 0) {
943 int cid = m_model->getClipByPosition(m_activeTrack, cursorPos);
944 if (cid < 0) {
945 // Check first item after timeline position
946 int maximumSpace = m_model->getTrackById_const(m_activeTrack)->getBlankEnd(cursorPos);
947 if (maximumSpace < INT_MAX) {
948 cid = m_model->getClipByPosition(m_activeTrack, maximumSpace + 1);
949 }
950 }
951 if (cid >= 0) {
952 int start = m_model->getItemPosition(cid);
953 if (start != cursorPos) {
954 int size = start + m_model->getItemPlaytime(cid) - cursorPos;
955 requestResize(cid, size);
956 selectionFound = true;
957 }
958 }
959 } else if (m_activeTrack == -2) {
960 // Subtitle track
961 auto subtitleModel = pCore->getSubtitleModel();
962 if (subtitleModel) {
963 int sid = -1;
964 std::unordered_set<int> sids = subtitleModel->getItemsInRange(cursorPos, cursorPos);
965 if (sids.empty()) {
966 sids = subtitleModel->getItemsInRange(cursorPos, -1);
967 for (int s : sids) {
968 if (sid == -1 || subtitleModel->getStartPosForId(s) < subtitleModel->getStartPosForId(sid)) {
969 sid = s;
970 }
971 }
972 } else {
973 sid = *sids.begin();
974 }
975 if (sid > -1) {
976 int start = m_model->getItemPosition(sid);
977 if (start != cursorPos) {
978 int size = start + m_model->getItemPlaytime(sid) - cursorPos;
979 requestResize(sid, size);
980 selectionFound = true;
981 }
982 }
983 }
984 }
985 if (!selectionFound) {
986 pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
987 }
988 }
989 }
990
setOutPoint(bool ripple)991 void TimelineController::setOutPoint(bool ripple)
992 {
993 if (dragOperationRunning()) {
994 // Don't allow timeline operation while drag in progress
995 pCore->displayMessage(i18n("Cannot perform operation while dragging in timeline"), ErrorMessage);
996 qDebug() << "Cannot operate while dragging";
997 return;
998 }
999 auto requestResize = [this, ripple](int id, int size){
1000 if (ripple) {
1001 m_model->requestItemRippleResize(m_model, id, size, true, true, 0, false);
1002 } else {
1003 m_model->requestItemResize(id, size, true, true, 0, false);
1004 }
1005 };
1006 int cursorPos = pCore->getTimelinePosition();
1007 const auto selection = m_model->getCurrentSelection();
1008 bool selectionFound = false;
1009 if (!selection.empty()) {
1010 for (int id : selection) {
1011 int start = m_model->getItemPosition(id);
1012 if (start + m_model->getItemPlaytime(id) == cursorPos) {
1013 continue;
1014 }
1015 int size = cursorPos - start;
1016 requestResize(id, size);
1017 selectionFound = true;
1018 }
1019 }
1020 if (!selectionFound) {
1021 if (m_activeTrack >= 0) {
1022 int cid = m_model->getClipByPosition(m_activeTrack, cursorPos);
1023 if (cid < 0 || cursorPos == m_model->getItemPosition(cid)) {
1024 // If no clip found at cursor pos or we are at the first frame of a clip, try to find previous clip
1025 // Check first item before timeline position
1026 // If we are at a clip start, check space before this clip
1027 int offset = cid >= 0 ? 1 : 0;
1028 int previousPos = m_model->getTrackById_const(m_activeTrack)->getBlankStart(cursorPos - offset);
1029 cid = m_model->getClipByPosition(m_activeTrack, qMax(0, previousPos - 1));
1030 }
1031 if (cid >= 0) {
1032 int start = m_model->getItemPosition(cid);
1033 if (start + m_model->getItemPlaytime(cid) != cursorPos) {
1034 int size = cursorPos - start;
1035 requestResize(cid, size);
1036 selectionFound = true;
1037 }
1038 }
1039 } else if (m_activeTrack == -2) {
1040 // Subtitle track
1041 auto subtitleModel = pCore->getSubtitleModel();
1042 if (subtitleModel) {
1043 int sid = -1;
1044 std::unordered_set<int> sids = subtitleModel->getItemsInRange(cursorPos, cursorPos);
1045 if (sids.empty()) {
1046 sids = subtitleModel->getItemsInRange(0, cursorPos);
1047 for (int s : sids) {
1048 if (sid == -1 || subtitleModel->getSubtitleEnd(s) > subtitleModel->getSubtitleEnd(sid)) {
1049 sid = s;
1050 }
1051 }
1052 } else {
1053 sid = *sids.begin();
1054 }
1055 if (sid > -1) {
1056 int start = m_model->getItemPosition(sid);
1057 if (start + m_model->getItemPlaytime(sid) != cursorPos) {
1058 int size = cursorPos - start;
1059 requestResize(sid, size);
1060 selectionFound = true;
1061 }
1062 }
1063 }
1064 }
1065 if (!selectionFound) {
1066 pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
1067 }
1068 }
1069 }
1070
editMarker(int cid,int position)1071 void TimelineController::editMarker(int cid, int position)
1072 {
1073 if (cid == -1) {
1074 cid = getMainSelectedClip();
1075 if (cid == -1) {
1076 pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
1077 return;
1078 }
1079 }
1080 Q_ASSERT(m_model->isClip(cid));
1081 double speed = m_model->getClipSpeed(cid);
1082 if (position == -1) {
1083 // Calculate marker position relative to timeline cursor
1084 position = pCore->getTimelinePosition() - m_model->getClipPosition(cid) + m_model->getClipIn(cid);
1085 position = int(position * speed);
1086 }
1087 if (position < (m_model->getClipIn(cid) * speed) || position > (m_model->getClipIn(cid) * speed + m_model->getClipPlaytime(cid))) {
1088 pCore->displayMessage(i18n("Cannot find clip to edit marker"), ErrorMessage, 500);
1089 return;
1090 }
1091 std::shared_ptr<ProjectClip> clip = pCore->bin()->getBinClip(getClipBinId(cid));
1092 if (clip->getMarkerModel()->hasMarker(position)) {
1093 GenTime pos(position, pCore->getCurrentFps());
1094 clip->getMarkerModel()->editMarkerGui(pos, qApp->activeWindow(), false, clip.get());
1095 } else {
1096 pCore->displayMessage(i18n("Cannot find clip to edit marker"), ErrorMessage, 500);
1097 }
1098 }
1099
addMarker(int cid,int position)1100 void TimelineController::addMarker(int cid, int position)
1101 {
1102 if (cid == -1) {
1103 cid = getMainSelectedClip();
1104 if (cid == -1) {
1105 pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
1106 return;
1107 }
1108 }
1109 Q_ASSERT(m_model->isClip(cid));
1110 double speed = m_model->getClipSpeed(cid);
1111 if (position == -1) {
1112 // Calculate marker position relative to timeline cursor
1113 position = pCore->getTimelinePosition() - m_model->getClipPosition(cid) + m_model->getClipIn(cid);
1114 position = int(position * speed);
1115 }
1116 if (position < (m_model->getClipIn(cid) * speed) || position > (m_model->getClipIn(cid) * speed + m_model->getClipPlaytime(cid))) {
1117 pCore->displayMessage(i18n("Cannot find clip to edit marker"), ErrorMessage, 500);
1118 return;
1119 }
1120 std::shared_ptr<ProjectClip> clip = pCore->bin()->getBinClip(getClipBinId(cid));
1121 GenTime pos(position, pCore->getCurrentFps());
1122 clip->getMarkerModel()->editMarkerGui(pos, qApp->activeWindow(), true, clip.get());
1123 }
1124
getMainSelectedClip()1125 int TimelineController::getMainSelectedClip()
1126 {
1127 int clipId = m_root->property("mainItemId").toInt();
1128 if (clipId == -1 || !isInSelection(clipId)) {
1129 std::unordered_set<int> sel = m_model->getCurrentSelection();
1130 for (int i : sel) {
1131 if (m_model->isClip(i)) {
1132 clipId = i;
1133 break;
1134 }
1135 }
1136 }
1137 return m_model->isClip(clipId) ? clipId : -1;
1138 }
1139
addQuickMarker(int cid,int position)1140 void TimelineController::addQuickMarker(int cid, int position)
1141 {
1142 if (cid == -1) {
1143 cid = getMainSelectedClip();
1144 if (cid == -1) {
1145 pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
1146 return;
1147 }
1148 }
1149 Q_ASSERT(m_model->isClip(cid));
1150 double speed = m_model->getClipSpeed(cid);
1151 if (position == -1) {
1152 // Calculate marker position relative to timeline cursor
1153 position = pCore->getTimelinePosition() - m_model->getClipPosition(cid);
1154 position = int(position * speed);
1155 }
1156 if (position < (m_model->getClipIn(cid) * speed) || position > ((m_model->getClipIn(cid) + m_model->getClipPlaytime(cid) * speed))) {
1157 pCore->displayMessage(i18n("Cannot find clip to edit marker"), ErrorMessage, 500);
1158 return;
1159 }
1160 std::shared_ptr<ProjectClip> clip = pCore->bin()->getBinClip(getClipBinId(cid));
1161 GenTime pos(position, pCore->getCurrentFps());
1162 CommentedTime marker(pos, pCore->currentDoc()->timecode().getDisplayTimecode(pos, false), KdenliveSettings::default_marker_type());
1163 clip->getMarkerModel()->addMarker(marker.time(), marker.comment(), marker.markerType());
1164 }
1165
deleteMarker(int cid,int position)1166 void TimelineController::deleteMarker(int cid, int position)
1167 {
1168 if (cid == -1) {
1169 cid = getMainSelectedClip();
1170 if (cid == -1) {
1171 pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
1172 return;
1173 }
1174 }
1175 Q_ASSERT(m_model->isClip(cid));
1176 double speed = m_model->getClipSpeed(cid);
1177 if (position == -1) {
1178 // Calculate marker position relative to timeline cursor
1179 position = pCore->getTimelinePosition() - m_model->getClipPosition(cid) + m_model->getClipIn(cid);
1180 position = int(position * speed);
1181 }
1182 if (position < (m_model->getClipIn(cid) * speed) || position > (m_model->getClipIn(cid) * speed + m_model->getClipPlaytime(cid))) {
1183 pCore->displayMessage(i18n("Cannot find clip to edit marker"), ErrorMessage, 500);
1184 return;
1185 }
1186 std::shared_ptr<ProjectClip> clip = pCore->bin()->getBinClip(getClipBinId(cid));
1187 GenTime pos(position, pCore->getCurrentFps());
1188 clip->getMarkerModel()->removeMarker(pos);
1189 }
1190
deleteAllMarkers(int cid)1191 void TimelineController::deleteAllMarkers(int cid)
1192 {
1193 if (cid == -1) {
1194 cid = getMainSelectedClip();
1195 if (cid == -1) {
1196 pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
1197 return;
1198 }
1199 }
1200 Q_ASSERT(m_model->isClip(cid));
1201 std::shared_ptr<ProjectClip> clip = pCore->bin()->getBinClip(getClipBinId(cid));
1202 clip->getMarkerModel()->removeAllMarkers();
1203 }
1204
editGuide(int frame)1205 void TimelineController::editGuide(int frame)
1206 {
1207 if (frame == -1) {
1208 frame = pCore->getTimelinePosition();
1209 }
1210 auto guideModel = pCore->currentDoc()->getGuideModel();
1211 GenTime pos(frame, pCore->getCurrentFps());
1212 guideModel->editMarkerGui(pos, qApp->activeWindow(), false);
1213 }
1214
moveGuide(int frame,int newFrame)1215 void TimelineController::moveGuide(int frame, int newFrame)
1216 {
1217 auto guideModel = pCore->currentDoc()->getGuideModel();
1218 GenTime pos(frame, pCore->getCurrentFps());
1219 GenTime newPos(newFrame, pCore->getCurrentFps());
1220 guideModel->editMarker(pos, newPos);
1221 }
1222
moveGuideWithoutUndo(int mid,int newFrame)1223 void TimelineController::moveGuideWithoutUndo(int mid, int newFrame)
1224 {
1225 auto guideModel = pCore->currentDoc()->getGuideModel();
1226 GenTime newPos(newFrame, pCore->getCurrentFps());
1227 guideModel->moveMarker(mid, newPos);
1228 }
1229
moveGuidesInRange(int start,int end,int offset)1230 bool TimelineController::moveGuidesInRange(int start, int end, int offset)
1231 {
1232 std::function<bool(void)> undo = []() { return true; };
1233 std::function<bool(void)> redo = []() { return true; };
1234 bool final = false;
1235 final = moveGuidesInRange(start, end, offset, undo, redo);
1236 if (final) {
1237 if (offset > 0) {
1238 pCore->pushUndo(undo, redo, i18n("Insert space"));
1239 } else {
1240 pCore->pushUndo(undo, redo, i18n("Remove space"));
1241 }
1242 return true;
1243 } else {
1244 undo();
1245 }
1246 return false;
1247 }
1248
moveGuidesInRange(int start,int end,int offset,Fun & undo,Fun & redo)1249 bool TimelineController::moveGuidesInRange(int start, int end, int offset, Fun &undo, Fun &redo)
1250 {
1251 GenTime fromPos(start, pCore->getCurrentFps());
1252 GenTime toPos(start + offset, pCore->getCurrentFps());
1253 QList<CommentedTime> guides = pCore->currentDoc()->getGuideModel()->getMarkersInRange(start, end);
1254 return pCore->currentDoc()->getGuideModel()->moveMarkers(guides, fromPos, toPos, undo, redo);
1255 }
1256
switchGuide(int frame,bool deleteOnly,bool showGui)1257 void TimelineController::switchGuide(int frame, bool deleteOnly, bool showGui)
1258 {
1259 bool markerFound = false;
1260 if (frame == -1) {
1261 frame = pCore->getTimelinePosition();
1262 }
1263 CommentedTime marker = pCore->currentDoc()->getGuideModel()->getMarker(GenTime(frame, pCore->getCurrentFps()), &markerFound);
1264 if (!markerFound) {
1265 if (deleteOnly) {
1266 pCore->displayMessage(i18n("No guide found at current position"), ErrorMessage, 500);
1267 return;
1268 }
1269 GenTime pos(frame, pCore->getCurrentFps());
1270
1271 if(showGui) {
1272 pCore->currentDoc()->getGuideModel()->editMarkerGui(pos, qApp->activeWindow(), true);
1273 } else {
1274 pCore->currentDoc()->getGuideModel()->addMarker(pos, i18n("guide"));
1275 }
1276 } else {
1277 pCore->currentDoc()->getGuideModel()->removeMarker(marker.time());
1278 }
1279 }
1280
addAsset(const QVariantMap & data)1281 void TimelineController::addAsset(const QVariantMap &data)
1282 {
1283 QString effect = data.value(QStringLiteral("kdenlive/effect")).toString();
1284 const auto selection = m_model->getCurrentSelection();
1285 bool audioEffect = EffectsRepository::get()->isAudioEffect(effect);
1286 if (!selection.empty()) {
1287 QList<int> effectSelection;
1288 for (int id : selection) {
1289 if (m_model->isClip(id) && audioEffect == m_model->m_allClips.at(id)->isAudioOnly()) {
1290 effectSelection << id;
1291 }
1292 }
1293 bool foundMatch = false;
1294 for (int id : qAsConst(effectSelection)) {
1295 if (m_model->addClipEffect(id, effect, false)) {
1296 foundMatch = true;
1297 }
1298 }
1299 if (!foundMatch) {
1300 QString effectName = EffectsRepository::get()->getName(effect);
1301 pCore->displayMessage(i18n("Cannot add effect %1 to selected clip", effectName), ErrorMessage, 500);
1302 }
1303 } else {
1304 pCore->displayMessage(i18n("Select a clip to apply an effect"), ErrorMessage, 500);
1305 }
1306 }
1307
requestRefresh()1308 void TimelineController::requestRefresh()
1309 {
1310 pCore->requestMonitorRefresh();
1311 }
1312
showAsset(int id)1313 void TimelineController::showAsset(int id)
1314 {
1315 if (m_model->isComposition(id)) {
1316 emit showTransitionModel(id, m_model->getCompositionParameterModel(id));
1317 } else if (m_model->isClip(id)) {
1318 QModelIndex clipIx = m_model->makeClipIndexFromID(id);
1319 QString clipName = m_model->data(clipIx, Qt::DisplayRole).toString();
1320 bool showKeyframes = m_model->data(clipIx, TimelineModel::ShowKeyframesRole).toInt();
1321 qDebug() << "-----\n// SHOW KEYFRAMES: " << showKeyframes;
1322 emit showItemEffectStack(clipName, m_model->getClipEffectStackModel(id), m_model->getClipFrameSize(id), showKeyframes);
1323 } else if (m_model->isSubTitle(id)) {
1324 emit showSubtitle(id);
1325 }
1326 }
1327
showTrackAsset(int trackId)1328 void TimelineController::showTrackAsset(int trackId)
1329 {
1330 emit showItemEffectStack(getTrackNameFromIndex(trackId), m_model->getTrackEffectStackModel(trackId), pCore->getCurrentFrameSize(), false);
1331 }
1332
adjustAllTrackHeight(int trackId,int height)1333 void TimelineController::adjustAllTrackHeight(int trackId, int height)
1334 {
1335 bool isAudio = m_model->getTrackById_const(trackId)->isAudioTrack();
1336 auto it = m_model->m_allTracks.cbegin();
1337 while (it != m_model->m_allTracks.cend()) {
1338 int target_track = (*it)->getId();
1339 if (target_track != trackId && m_model->getTrackById_const(target_track)->isAudioTrack() == isAudio) {
1340 m_model->getTrackById(target_track)->setProperty(QStringLiteral("kdenlive:trackheight"), QString::number(height));
1341 }
1342 ++it;
1343 }
1344 int tracksCount = m_model->getTracksCount();
1345 QModelIndex modelStart = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(0));
1346 QModelIndex modelEnd = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(tracksCount - 1));
1347 emit m_model->dataChanged(modelStart, modelEnd, {TimelineModel::HeightRole});
1348 }
1349
collapseAllTrackHeight(int trackId,bool collapse,int collapsedHeight)1350 void TimelineController::collapseAllTrackHeight(int trackId, bool collapse, int collapsedHeight)
1351 {
1352 bool isAudio = m_model->getTrackById_const(trackId)->isAudioTrack();
1353 auto it = m_model->m_allTracks.cbegin();
1354 while (it != m_model->m_allTracks.cend()) {
1355 int target_track = (*it)->getId();
1356 if (m_model->getTrackById_const(target_track)->isAudioTrack() == isAudio) {
1357 if (collapse) {
1358 m_model->setTrackProperty(target_track, "kdenlive:collapsed", QString::number(collapsedHeight));
1359 } else {
1360 m_model->setTrackProperty(target_track, "kdenlive:collapsed", QStringLiteral("0"));
1361 }
1362 }
1363 ++it;
1364 }
1365 int tracksCount = m_model->getTracksCount();
1366 QModelIndex modelStart = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(0));
1367 QModelIndex modelEnd = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(tracksCount - 1));
1368 emit m_model->dataChanged(modelStart, modelEnd, {TimelineModel::HeightRole});
1369 }
1370
defaultTrackHeight(int trackId)1371 void TimelineController::defaultTrackHeight(int trackId)
1372 {
1373 if (trackId > -1) {
1374 m_model->getTrackById(trackId)->setProperty(QStringLiteral("kdenlive:trackheight"), QString::number(KdenliveSettings::trackheight()));
1375 QModelIndex modelStart = m_model->makeTrackIndexFromID(trackId);
1376 emit m_model->dataChanged(modelStart, modelStart, {TimelineModel::HeightRole});
1377 return;
1378 }
1379 auto it = m_model->m_allTracks.cbegin();
1380 while (it != m_model->m_allTracks.cend()) {
1381 int target_track = (*it)->getId();
1382 m_model->getTrackById(target_track)->setProperty(QStringLiteral("kdenlive:trackheight"), QString::number(KdenliveSettings::trackheight()));
1383 ++it;
1384 }
1385 int tracksCount = m_model->getTracksCount();
1386 QModelIndex modelStart = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(0));
1387 QModelIndex modelEnd = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(tracksCount - 1));
1388 emit m_model->dataChanged(modelStart, modelEnd, {TimelineModel::HeightRole});
1389 }
1390
setPosition(int position)1391 void TimelineController::setPosition(int position)
1392 {
1393 // Process seek request
1394 emit seeked(position);
1395 }
1396
setAudioTarget(QMap<int,int> tracks)1397 void TimelineController::setAudioTarget(QMap<int, int> tracks)
1398 {
1399 // Clear targets before re-adding to trigger qml refresh
1400 m_model->m_audioTarget.clear();
1401 emit audioTargetChanged();
1402
1403 if ((!tracks.isEmpty() && !m_model->isTrack(tracks.firstKey())) || m_hasAudioTarget == 0) {
1404 return;
1405 }
1406
1407 m_model->m_audioTarget = tracks;
1408 emit audioTargetChanged();
1409 }
1410
switchAudioTarget(int trackId)1411 void TimelineController::switchAudioTarget(int trackId)
1412 {
1413 if (m_model->m_audioTarget.contains(trackId)) {
1414 m_model->m_audioTarget.remove(trackId);
1415 } else {
1416 //TODO: use track description
1417 if (m_model->m_binAudioTargets.count() == 1) {
1418 // Only one audio stream, remove previous and switch
1419 m_model->m_audioTarget.clear();
1420 }
1421 int ix = getFirstUnassignedStream();
1422 if (ix > -1) {
1423 m_model->m_audioTarget.insert(trackId, ix);
1424 }
1425 }
1426 emit audioTargetChanged();
1427 }
1428
assignCurrentTarget(int index)1429 void TimelineController::assignCurrentTarget(int index)
1430 {
1431 if (m_activeTrack == -1 || !m_model->isTrack(m_activeTrack)) {
1432 pCore->displayMessage(i18n("No active track"), ErrorMessage, 500);
1433 return;
1434 }
1435 bool isAudio = m_model->isAudioTrack(m_activeTrack);
1436 if (isAudio) {
1437 // Select audio target stream
1438 if (index >= 0 && index < m_model->m_binAudioTargets.size()) {
1439 // activate requested stream
1440 int stream = m_model->m_binAudioTargets.keys().at(index);
1441 assignAudioTarget(m_activeTrack, stream);
1442 } else {
1443 // Remove audio target
1444 m_model->m_audioTarget.remove(m_activeTrack);
1445 emit audioTargetChanged();
1446 }
1447 return;
1448 } else {
1449 // Select video target stream
1450 setVideoTarget(m_activeTrack);
1451 return;
1452 }
1453 }
1454
assignAudioTarget(int trackId,int stream)1455 void TimelineController::assignAudioTarget(int trackId, int stream)
1456 {
1457 QList <int> assignedStreams = m_model->m_audioTarget.values();
1458 if (assignedStreams.contains(stream)) {
1459 // This stream was assigned to another track, remove
1460 m_model->m_audioTarget.remove(m_model->m_audioTarget.key(stream));
1461 }
1462 //Remove and re-add target track to trigger a refresh in qml track headers
1463 m_model->m_audioTarget.remove(trackId);
1464 emit audioTargetChanged();
1465
1466 m_model->m_audioTarget.insert(trackId, stream);
1467 emit audioTargetChanged();
1468 }
1469
1470
getFirstUnassignedStream() const1471 int TimelineController::getFirstUnassignedStream() const
1472 {
1473 QList <int> keys = m_model->m_binAudioTargets.keys();
1474 QList <int> assigned = m_model->m_audioTarget.values();
1475 for (int k : qAsConst(keys)) {
1476 if (!assigned.contains(k)) {
1477 return k;
1478 }
1479 }
1480 return -1;
1481 }
1482
setVideoTarget(int track)1483 void TimelineController::setVideoTarget(int track)
1484 {
1485 if ((track > -1 && !m_model->isTrack(track)) || !m_hasVideoTarget) {
1486 m_model->m_videoTarget = -1;
1487 return;
1488 }
1489 m_model->m_videoTarget = track;
1490 emit videoTargetChanged();
1491 }
1492
setActiveTrack(int track)1493 void TimelineController::setActiveTrack(int track)
1494 {
1495 if (track > -1 && !m_model->isTrack(track)) {
1496 return;
1497 }
1498 m_activeTrack = track;
1499 emit activeTrackChanged();
1500 }
1501
setZone(const QPoint & zone,bool withUndo)1502 void TimelineController::setZone(const QPoint &zone, bool withUndo)
1503 {
1504 if (m_zone.x() > 0) {
1505 m_model->removeSnap(m_zone.x());
1506 }
1507 if (m_zone.y() > 0) {
1508 m_model->removeSnap(m_zone.y() - 1);
1509 }
1510 if (zone.x() > 0) {
1511 m_model->addSnap(zone.x());
1512 }
1513 if (zone.y() > 0) {
1514 m_model->addSnap(zone.y() - 1);
1515 }
1516 updateZone(m_zone, zone, withUndo);
1517 }
1518
updateZone(const QPoint oldZone,const QPoint newZone,bool withUndo)1519 void TimelineController::updateZone(const QPoint oldZone, const QPoint newZone, bool withUndo)
1520 {
1521 if (!withUndo) {
1522 m_zone = newZone;
1523 emit zoneChanged();
1524 // Update monitor zone
1525 emit zoneMoved(m_zone);
1526 return;
1527 }
1528 Fun undo_zone = [this, oldZone]() {
1529 setZone(oldZone, false);
1530 return true;
1531 };
1532 Fun redo_zone = [this, newZone]() {
1533 setZone(newZone, false);
1534 return true;
1535 };
1536 redo_zone();
1537 pCore->pushUndo(undo_zone, redo_zone, i18n("Set Zone"));
1538 }
1539
updateEffectZone(const QPoint oldZone,const QPoint newZone,bool withUndo)1540 void TimelineController::updateEffectZone(const QPoint oldZone, const QPoint newZone, bool withUndo)
1541 {
1542 Q_UNUSED(oldZone)
1543 emit pCore->updateEffectZone(newZone, withUndo);
1544 }
1545
setZoneIn(int inPoint)1546 void TimelineController::setZoneIn(int inPoint)
1547 {
1548 if (m_zone.x() > 0) {
1549 m_model->removeSnap(m_zone.x());
1550 }
1551 if (inPoint > 0) {
1552 m_model->addSnap(inPoint);
1553 }
1554 m_zone.setX(inPoint);
1555 emit zoneChanged();
1556 // Update monitor zone
1557 emit zoneMoved(m_zone);
1558 }
1559
setZoneOut(int outPoint)1560 void TimelineController::setZoneOut(int outPoint)
1561 {
1562 if (m_zone.y() > 0) {
1563 m_model->removeSnap(m_zone.y() - 1);
1564 }
1565 if (outPoint > 0) {
1566 m_model->addSnap(outPoint - 1);
1567 }
1568 m_zone.setY(outPoint);
1569 emit zoneChanged();
1570 emit zoneMoved(m_zone);
1571 }
1572
selectItems(const QVariantList & tracks,int startFrame,int endFrame,bool addToSelect,bool selectBottomCompositions,bool selectSubTitles)1573 void TimelineController::selectItems(const QVariantList &tracks, int startFrame, int endFrame, bool addToSelect, bool selectBottomCompositions, bool selectSubTitles)
1574 {
1575 std::unordered_set<int> itemsToSelect;
1576 std::unordered_set<int> subtitlesToSelect;
1577 if (addToSelect) {
1578 itemsToSelect = m_model->getCurrentSelection();
1579 }
1580 for (int i = 0; i < tracks.count(); i++) {
1581 if (m_model->getTrackById_const(tracks.at(i).toInt())->isLocked()) {
1582 continue;
1583 }
1584 auto currentClips = m_model->getItemsInRange(tracks.at(i).toInt(), startFrame, endFrame, i < tracks.count() - 1 ? true : selectBottomCompositions);
1585 itemsToSelect.insert(currentClips.begin(), currentClips.end());
1586 }
1587 if (selectSubTitles) {
1588 auto subtitleModel = pCore->getSubtitleModel();
1589 if (subtitleModel) {
1590 auto currentSubs = subtitleModel->getItemsInRange(startFrame, endFrame);
1591 itemsToSelect.insert(currentSubs.begin(), currentSubs.end());
1592 }
1593
1594 }
1595 m_model->requestSetSelection(itemsToSelect);
1596 }
1597
requestClipCut(int clipId,int position)1598 void TimelineController::requestClipCut(int clipId, int position)
1599 {
1600 if (position == -1) {
1601 position = pCore->getTimelinePosition();
1602 }
1603 TimelineFunctions::requestClipCut(m_model, clipId, position);
1604 }
1605
cutClipUnderCursor(int position,int track)1606 void TimelineController::cutClipUnderCursor(int position, int track)
1607 {
1608 if (position == -1) {
1609 position = pCore->getTimelinePosition();
1610 }
1611 QMutexLocker lk(&m_metaMutex);
1612 bool foundClip = false;
1613 const auto selection = m_model->getCurrentSelection();
1614 if (track == -2) {
1615 // Subtitle cut
1616 auto subtitleModel = pCore->getSubtitleModel();
1617 subtitleModel->cutSubtitle(position);
1618 return;
1619 }
1620 if (track == -1) {
1621 for (int cid : selection) {
1622 if ((m_model->isClip(cid) || m_model->isSubTitle(cid)) && positionIsInItem(cid)) {
1623 if (TimelineFunctions::requestClipCut(m_model, cid, position)) {
1624 foundClip = true;
1625 // Cutting clips in the selection group is handled in TimelineFunctions
1626 break;
1627 }
1628 }
1629 }
1630 }
1631 if (!foundClip) {
1632 if (track == -1) {
1633 track = m_activeTrack;
1634 }
1635 if (track >= 0) {
1636 int cid = m_model->getClipByPosition(track, position);
1637 if (cid >= 0 && TimelineFunctions::requestClipCut(m_model, cid, position)) {
1638 foundClip = true;
1639 }
1640 } else if (track == -2) {
1641 // Subtitle cut
1642 auto subtitleModel = pCore->getSubtitleModel();
1643 foundClip = subtitleModel->cutSubtitle(position);
1644 }
1645 }
1646 if (!foundClip) {
1647 pCore->displayMessage(i18n("No clip to cut"), ErrorMessage, 500);
1648 }
1649 }
1650
cutSubtitle(int id,int cursorPos)1651 void TimelineController::cutSubtitle(int id, int cursorPos)
1652 {
1653 Q_ASSERT(m_model->isSubTitle(id));
1654 if (cursorPos <= 0) {
1655 return requestClipCut(id, -1);
1656 }
1657 // Cut subtitle at edit position
1658 int timelinePos = pCore->getTimelinePosition();
1659 GenTime position(timelinePos, pCore->getCurrentFps());
1660 GenTime start = m_model->m_allSubtitles.at(id);
1661 auto subtitleModel = pCore->getSubtitleModel();
1662 SubtitledTime subData = subtitleModel->getSubtitle(start);
1663 if (position > start && position < subData.end()) {
1664 QString originalText = subData.subtitle();
1665 QString firstText = originalText;
1666 QString secondText = originalText.right(originalText.length() - cursorPos);
1667 firstText.truncate(cursorPos);
1668 Fun undo = []() { return true; };
1669 Fun redo = []() { return true; };
1670 int newId = subtitleModel->cutSubtitle(timelinePos, undo, redo);
1671 if (newId > -1) {
1672 Fun local_redo = [subtitleModel, id, newId, firstText, secondText]() {
1673 subtitleModel->editSubtitle(id, firstText);
1674 subtitleModel->editSubtitle(newId, secondText);
1675 return true;
1676 };
1677 Fun local_undo = [subtitleModel, id, originalText]() {
1678 subtitleModel->editSubtitle(id, originalText);
1679 return true;
1680 };
1681 local_redo();
1682 UPDATE_UNDO_REDO_NOLOCK(local_redo, local_undo, undo, redo);
1683 pCore->pushUndo(undo, redo, i18n("Cut clip"));
1684 }
1685 }
1686 }
1687
cutAllClipsUnderCursor(int position)1688 void TimelineController::cutAllClipsUnderCursor(int position)
1689 {
1690 if (position == -1) {
1691 position = pCore->getTimelinePosition();
1692 }
1693 QMutexLocker lk(&m_metaMutex);
1694 TimelineFunctions::requestClipCutAll(m_model, position);
1695 }
1696
requestSpacerStartOperation(int trackId,int position)1697 int TimelineController::requestSpacerStartOperation(int trackId, int position)
1698 {
1699 QMutexLocker lk(&m_metaMutex);
1700 int itemId = TimelineFunctions::requestSpacerStartOperation(m_model, trackId, position);
1701 return itemId;
1702 }
1703
spacerMinPos() const1704 int TimelineController::spacerMinPos() const
1705 {
1706 return TimelineFunctions::spacerMinPos();
1707 }
1708
spacerMoveGuides(QVector<int> ids,int offset)1709 void TimelineController::spacerMoveGuides(QVector<int> ids, int offset)
1710 {
1711 pCore->currentDoc()->getGuideModel()->moveMarkersWithoutUndo(ids, offset);
1712 }
1713
spacerSelection(int startFrame)1714 QVector<int> TimelineController::spacerSelection(int startFrame)
1715 {
1716 return pCore->currentDoc()->getGuideModel()->getMarkersIdInRange(startFrame, -1);
1717 }
1718
getGuidePosition(int id)1719 int TimelineController::getGuidePosition(int id)
1720 {
1721 return pCore->currentDoc()->getGuideModel()->getMarkerPos(id);
1722 }
1723
requestSpacerEndOperation(int clipId,int startPosition,int endPosition,int affectedTrack,QVector<int> selectedGuides,int guideStart)1724 bool TimelineController::requestSpacerEndOperation(int clipId, int startPosition, int endPosition, int affectedTrack, QVector <int> selectedGuides, int guideStart)
1725 {
1726 QMutexLocker lk(&m_metaMutex);
1727 // Start undoable command
1728 std::function<bool(void)> undo = []() { return true; };
1729 std::function<bool(void)> redo = []() { return true; };
1730 if(guideStart > -1) {
1731 // Move guides back to original position
1732 pCore->currentDoc()->getGuideModel()->moveMarkersWithoutUndo(selectedGuides, startPosition - endPosition, false);
1733 moveGuidesInRange(guideStart, -1, endPosition - startPosition, undo, redo);
1734 }
1735 bool result = TimelineFunctions::requestSpacerEndOperation(m_model, clipId, startPosition, endPosition, affectedTrack, false, undo, redo);
1736 return result;
1737 }
1738
seekCurrentClip(bool seekToEnd)1739 void TimelineController::seekCurrentClip(bool seekToEnd)
1740 {
1741 const auto selection = m_model->getCurrentSelection();
1742 int cid = -1;
1743 if (!selection.empty()) {
1744 cid = *selection.begin();
1745 } else {
1746 int cursorPos = pCore->getTimelinePosition();
1747 cid = m_model->getClipByPosition(m_activeTrack, cursorPos);
1748 if (cid < 0) {
1749 /* If the cursor is at the clip end it is one frame after the clip,
1750 * make it possible to jump to the clip start in that situation too
1751 */
1752 cid = m_model->getClipByPosition(m_activeTrack, cursorPos - 1);
1753 }
1754 }
1755 if (cid > -1) {
1756 seekToClip(cid, seekToEnd);
1757 }
1758 }
1759
seekToClip(int cid,bool seekToEnd)1760 void TimelineController::seekToClip(int cid, bool seekToEnd)
1761 {
1762 int start = m_model->getItemPosition(cid);
1763 if (seekToEnd) {
1764 start += m_model->getItemPlaytime(cid);
1765 }
1766 setPosition(start);
1767 }
1768
seekToMouse()1769 void TimelineController::seekToMouse()
1770 {
1771 int mousePos = getMousePos();
1772 if (mousePos > -1) {
1773 setPosition(mousePos);
1774 }
1775 }
1776
getMousePos()1777 int TimelineController::getMousePos()
1778 {
1779 QVariant returnedValue;
1780 QMetaObject::invokeMethod(m_root, "getMousePos", Q_RETURN_ARG(QVariant, returnedValue));
1781 return returnedValue.toInt();
1782 }
1783
getMouseTrack()1784 int TimelineController::getMouseTrack()
1785 {
1786 QVariant returnedValue;
1787 QMetaObject::invokeMethod(m_root, "getMouseTrack", Q_RETURN_ARG(QVariant, returnedValue));
1788 return returnedValue.toInt();
1789 }
1790
positionIsInItem(int id)1791 bool TimelineController::positionIsInItem(int id)
1792 {
1793 int in = m_model->getItemPosition(id);
1794 int position = pCore->getTimelinePosition();
1795 if (in > position) {
1796 return false;
1797 }
1798 if (position <= in + m_model->getItemPlaytime(id)) {
1799 return true;
1800 }
1801 return false;
1802 }
1803
refreshItem(int id)1804 void TimelineController::refreshItem(int id)
1805 {
1806 if (m_model->isClip(id) && m_model->m_allClips[id]->isAudioOnly()) {
1807 return;
1808 }
1809 if (positionIsInItem(id)) {
1810 pCore->requestMonitorRefresh();
1811 }
1812 }
1813
getTracksCount() const1814 QPair<int, int> TimelineController::getTracksCount() const
1815 {
1816 return m_model->getAVtracksCount();
1817 }
1818
extractCompositionLumas() const1819 QStringList TimelineController::extractCompositionLumas() const
1820 {
1821 return m_model->extractCompositionLumas();
1822 }
1823
extractExternalEffectFiles() const1824 QStringList TimelineController::extractExternalEffectFiles() const
1825 {
1826 return m_model->extractExternalEffectFiles();
1827 }
1828
addEffectToCurrentClip(const QStringList & effectData)1829 void TimelineController::addEffectToCurrentClip(const QStringList &effectData)
1830 {
1831 QList<int> activeClips;
1832 for (int track = m_model->getTracksCount() - 1; track >= 0; track--) {
1833 int trackIx = m_model->getTrackIndexFromPosition(track);
1834 int cid = m_model->getClipByPosition(trackIx, pCore->getTimelinePosition());
1835 if (cid > -1) {
1836 activeClips << cid;
1837 }
1838 }
1839 if (!activeClips.isEmpty()) {
1840 if (effectData.count() == 4) {
1841 QString effectString = effectData.at(1) + QStringLiteral("-") + effectData.at(2) + QStringLiteral("-") + effectData.at(3);
1842 m_model->copyClipEffect(activeClips.first(), effectString);
1843 } else {
1844 m_model->addClipEffect(activeClips.first(), effectData.constFirst());
1845 }
1846 }
1847 }
1848
adjustFade(int cid,const QString & effectId,int duration,int initialDuration)1849 void TimelineController::adjustFade(int cid, const QString &effectId, int duration, int initialDuration)
1850 {
1851 if (initialDuration == -2) {
1852 // Add default fade
1853 duration = pCore->getDurationFromString(KdenliveSettings::fade_duration());
1854 initialDuration = 0;
1855 }
1856 if (duration <= 0) {
1857 // remove fade
1858 if (initialDuration > 0) {
1859 // Restore original fade duration
1860 m_model->adjustEffectLength(cid, effectId, initialDuration, -1);
1861 }
1862 m_model->removeFade(cid, effectId == QLatin1String("fadein"));
1863 } else {
1864 m_model->adjustEffectLength(cid, effectId, duration, initialDuration);
1865 }
1866 }
1867
getCompositionATrack(int cid) const1868 QPair<int, int> TimelineController::getCompositionATrack(int cid) const
1869 {
1870 QPair<int, int> result;
1871 std::shared_ptr<CompositionModel> compo = m_model->getCompositionPtr(cid);
1872 if (compo) {
1873 result = QPair<int, int>(compo->getATrack(), m_model->getTrackMltIndex(compo->getCurrentTrackId()));
1874 }
1875 return result;
1876 }
1877
setCompositionATrack(int cid,int aTrack)1878 void TimelineController::setCompositionATrack(int cid, int aTrack)
1879 {
1880 TimelineFunctions::setCompositionATrack(m_model, cid, aTrack);
1881 }
1882
compositionAutoTrack(int cid) const1883 bool TimelineController::compositionAutoTrack(int cid) const
1884 {
1885 std::shared_ptr<CompositionModel> compo = m_model->getCompositionPtr(cid);
1886 return compo && compo->getForcedTrack() == -1;
1887 }
1888
getClipBinId(int clipId) const1889 const QString TimelineController::getClipBinId(int clipId) const
1890 {
1891 return m_model->getClipBinId(clipId);
1892 }
1893
focusItem(int itemId)1894 void TimelineController::focusItem(int itemId)
1895 {
1896 int start = m_model->getItemPosition(itemId);
1897 setPosition(start);
1898 emit centerView();
1899 }
1900
headerWidth() const1901 int TimelineController::headerWidth() const
1902 {
1903 return qMax(10, KdenliveSettings::headerwidth());
1904 }
1905
setHeaderWidth(int width)1906 void TimelineController::setHeaderWidth(int width)
1907 {
1908 KdenliveSettings::setHeaderwidth(width);
1909 }
1910
createSplitOverlay(int clipId,std::shared_ptr<Mlt::Filter> filter)1911 bool TimelineController::createSplitOverlay(int clipId, std::shared_ptr<Mlt::Filter> filter)
1912 {
1913 if (m_timelinePreview && m_timelinePreview->hasOverlayTrack()) {
1914 return true;
1915 }
1916 if (clipId == -1) {
1917 pCore->displayMessage(i18n("Select a clip to compare effect"), ErrorMessage, 500);
1918 return false;
1919 }
1920 std::shared_ptr<ClipModel> clip = m_model->getClipPtr(clipId);
1921 const QString binId = clip->binId();
1922
1923 // Get clean bin copy of the clip
1924 std::shared_ptr<ProjectClip> binClip = pCore->projectItemModel()->getClipByBinID(binId);
1925 std::shared_ptr<Mlt::Producer> binProd(binClip->masterProducer()->cut(clip->getIn(), clip->getOut()));
1926
1927 // Get copy of timeline producer
1928 std::shared_ptr<Mlt::Producer> clipProducer(new Mlt::Producer(*clip));
1929
1930 // Built tractor and compositing
1931 Mlt::Tractor trac(*m_model->m_tractor->profile());
1932 Mlt::Playlist play(*m_model->m_tractor->profile());
1933 Mlt::Playlist play2(*m_model->m_tractor->profile());
1934 play.append(*clipProducer.get());
1935 play2.append(*binProd);
1936 trac.set_track(play, 0);
1937 trac.set_track(play2, 1);
1938 play2.attach(*filter.get());
1939 QString splitTransition = TransitionsRepository::get()->getCompositingTransition();
1940 Mlt::Transition t(*m_model->m_tractor->profile(), splitTransition.toUtf8().constData());
1941 t.set("always_active", 1);
1942 trac.plant_transition(t, 0, 1);
1943 int startPos = m_model->getClipPosition(clipId);
1944
1945 // plug in overlay playlist
1946 auto *overlay = new Mlt::Playlist(*m_model->m_tractor->profile());
1947 overlay->insert_blank(0, startPos);
1948 Mlt::Producer split(trac.get_producer());
1949 overlay->insert_at(startPos, &split, 1);
1950
1951 // insert in tractor
1952 if (!m_timelinePreview) {
1953 initializePreview();
1954 }
1955 if(m_timelinePreview){
1956 m_timelinePreview->setOverlayTrack(overlay);
1957 m_model->m_overlayTrackCount = m_timelinePreview->addedTracks();
1958 }
1959 return true;
1960 }
1961
removeSplitOverlay()1962 void TimelineController::removeSplitOverlay()
1963 {
1964 if (!m_timelinePreview || !m_timelinePreview->hasOverlayTrack()) {
1965 return;
1966 }
1967 // disconnect
1968 m_timelinePreview->removeOverlayTrack();
1969 m_model->m_overlayTrackCount = m_timelinePreview->addedTracks();
1970 }
1971
requestItemRippleResize(int itemId,int size,bool right,bool logUndo,int snapDistance,bool allowSingleResize)1972 int TimelineController::requestItemRippleResize(int itemId, int size, bool right, bool logUndo, int snapDistance, bool allowSingleResize) {
1973 return m_model->requestItemRippleResize(m_model, itemId, size, right, logUndo, snapDistance, allowSingleResize);
1974 }
1975
updateTrimmingMode()1976 void TimelineController::updateTrimmingMode() {
1977 if (trimmingActive()) {
1978 requestStartTrimmingMode();
1979 } else {
1980 requestEndTrimmingMode();
1981 }
1982 }
1983
trimmingBoundOffset(int offset)1984 int TimelineController::trimmingBoundOffset(int offset) {
1985 std::shared_ptr<ClipModel> mainClip = m_model->getClipPtr(m_trimmingMainClip);
1986 return qBound(mainClip->getOut() - mainClip->getMaxDuration() + 1, offset, mainClip->getIn());
1987 }
1988
slipPosChanged(int offset)1989 void TimelineController::slipPosChanged(int offset) {
1990 if (!m_model->isClip(m_trimmingMainClip) || !pCore->monitorManager()->isTrimming()) {
1991 return;
1992 }
1993 std::shared_ptr<ClipModel> mainClip = m_model->getClipPtr(m_trimmingMainClip);
1994 offset = qBound(mainClip->getOut() - mainClip->getMaxDuration() + 1, offset, mainClip->getIn());
1995 int outPoint = mainClip->getOut() - offset;
1996 int inPoint = mainClip->getIn() - offset;
1997
1998 pCore->monitorManager()->projectMonitor()->slotTrimmingPos(inPoint, offset, inPoint, outPoint);
1999 showToolTip(i18n("In:%1, Out:%2 (%3%4)", simplifiedTC(inPoint), simplifiedTC(outPoint), (offset < 0 ? "-" : "+"), simplifiedTC(qFabs(offset))));
2000 }
2001
ripplePosChanged(int size,bool right)2002 void TimelineController::ripplePosChanged(int size, bool right) {
2003 if (!m_model->isClip(m_trimmingMainClip) || !pCore->monitorManager()->isTrimming()) {
2004 return;
2005 }
2006 if (size < 0) {
2007 return;
2008 }
2009 qDebug() << "ripplePosChanged" << size << right;
2010 std::shared_ptr<ClipModel> mainClip = m_model->getClipPtr(m_trimmingMainClip);
2011 int delta = size - mainClip->getPlaytime();
2012 if (!right) {
2013 delta *= -1;
2014 }
2015 int pos = right ? mainClip->getOut() : mainClip->getIn();
2016 pos += delta;
2017 if (mainClip->getMaxDuration() > -1) {
2018 pos = qBound(0, pos, mainClip->getMaxDuration());
2019 } else {
2020 pos = qMax(0, pos);
2021 }
2022 pCore->monitorManager()->projectMonitor()->slotTrimmingPos(pos + 1, delta, right ? mainClip->getIn() : pos, right ? pos : mainClip->getOut());
2023 }
2024
slipProcessSelection(int mainClipId,bool addToSelection)2025 bool TimelineController::slipProcessSelection(int mainClipId, bool addToSelection) {
2026 std::unordered_set<int> sel = m_model->getCurrentSelection();
2027 std::unordered_set<int> newSel;
2028
2029 for (int i : sel) {
2030 if (m_model->isClip(i) && m_model->getClipPtr(i)->getMaxDuration() != -1) {
2031 newSel.insert(i);
2032 }
2033 }
2034
2035 if (mainClipId != -1 && !m_model->isClip(mainClipId) && m_model->getClipPtr(mainClipId)->getMaxDuration() == -1) {
2036 mainClipId = -1;
2037 }
2038
2039 if ((newSel.empty() || !isInSelection(mainClipId)) && mainClipId != -1) {
2040 m_trimmingMainClip = mainClipId;
2041 emit trimmingMainClipChanged();
2042 if (!addToSelection) {
2043 newSel.clear();
2044 }
2045 newSel.insert(mainClipId);
2046 }
2047
2048 if (newSel != sel) {
2049 m_model->requestSetSelection(newSel);
2050 return false;
2051 }
2052
2053 if (sel.empty()) {
2054 return false;
2055 }
2056
2057 Q_ASSERT(!sel.empty());
2058
2059 if (mainClipId == -1) {
2060 mainClipId = getMainSelectedClip();
2061 }
2062
2063 if (m_model->getTrackById(m_model->getClipTrackId(mainClipId))->isLocked()) {
2064 int partnerId = m_model->m_groups->getSplitPartner(mainClipId);
2065 if (partnerId == -1 || m_model->getTrackById(m_model->getClipTrackId(partnerId))->isLocked()) {
2066 mainClipId = -1;
2067 for (int i : sel) {
2068 if (i != mainClipId && !m_model->getTrackById(m_model->getClipTrackId(i))->isLocked()) {
2069 mainClipId = i;
2070 break;
2071 }
2072 }
2073 } else {
2074 mainClipId = partnerId;
2075 }
2076 }
2077
2078 if (mainClipId == -1) {
2079 pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
2080 return false;
2081 }
2082
2083 std::shared_ptr<ClipModel> mainClip = m_model->getClipPtr(mainClipId);
2084
2085 if (mainClip->getMaxDuration() == -1) {
2086 return false;
2087 }
2088
2089 int partnerId = m_model->m_groups->getSplitPartner(mainClipId);
2090
2091 if (mainClip->isAudioOnly() && partnerId != -1 && !m_model->getTrackById(m_model->getClipTrackId(partnerId))->isLocked()) {
2092 mainClip = m_model->getClipPtr(partnerId);
2093 }
2094
2095 m_trimmingMainClip = mainClip->getId();
2096 emit trimmingMainClipChanged();
2097 return true;
2098 }
2099
requestStartTrimmingMode(int mainClipId,bool addToSelection,bool right)2100 bool TimelineController::requestStartTrimmingMode(int mainClipId, bool addToSelection, bool right)
2101 {
2102
2103 if (pCore->monitorManager()->isTrimming() && m_trimmingMainClip == mainClipId) {
2104 return true;
2105 }
2106
2107 if (pCore->activeTool() == ToolType::SlipTool && !slipProcessSelection(mainClipId, addToSelection)) {
2108 return false;
2109 }
2110
2111 if (pCore->activeTool() == ToolType::RippleTool) {
2112 if (m_model.get()->isClip(mainClipId)) {
2113 m_trimmingMainClip = mainClipId;
2114 emit trimmingMainClipChanged();
2115 } else {
2116 return false;
2117 }
2118 }
2119
2120 std::shared_ptr<ClipModel> mainClip = m_model->getClipPtr(m_trimmingMainClip);
2121
2122 const int previousClipId = m_model->getTrackById_const(mainClip->getCurrentTrackId())->getClipByPosition(mainClip->getPosition() - 1);
2123 std::shared_ptr<Mlt::Producer> previousFrame;
2124 if (pCore->activeTool() == ToolType::SlipTool && previousClipId > -1) {
2125 std::shared_ptr<ClipModel> previousClip = m_model->getClipPtr(previousClipId);
2126 previousFrame = std::shared_ptr<Mlt::Producer>(previousClip->getProducer()->cut(0));
2127 Mlt::Filter filter(*m_model->m_tractor->profile(), "freeze");
2128 filter.set("mlt_service", "freeze");
2129 filter.set("frame", previousClip->getOut());
2130 previousFrame->attach(filter);
2131 } else {
2132 previousFrame = std::shared_ptr<Mlt::Producer>(new Mlt::Producer(*m_model->m_tractor->profile(), "color:black"));
2133 }
2134
2135 const int nextClipId = m_model->getTrackById_const(mainClip->getCurrentTrackId())->getClipByPosition(mainClip->getPosition() + mainClip->getPlaytime());
2136 std::shared_ptr<Mlt::Producer> nextFrame;
2137 if (pCore->activeTool() == ToolType::SlipTool && nextClipId > -1) {
2138 std::shared_ptr<ClipModel> nextClip = m_model->getClipPtr(nextClipId);
2139 nextFrame = std::shared_ptr<Mlt::Producer>(nextClip->getProducer()->cut(0));
2140 Mlt::Filter filter(*m_model->m_tractor->profile(), "freeze");
2141 filter.set("mlt_service", "freeze");
2142 filter.set("frame", nextClip->getIn());
2143 nextFrame->attach(filter);
2144 } else {
2145 nextFrame = std::shared_ptr<Mlt::Producer>(new Mlt::Producer(*m_model->m_tractor->profile(), "color:black"));
2146 }
2147
2148 std::shared_ptr<Mlt::Producer> inOutFrame;
2149 if (pCore->activeTool() == ToolType::RippleTool) {
2150 inOutFrame = std::shared_ptr<Mlt::Producer>(mainClip->getProducer()->cut(0));
2151 Mlt::Filter filter(*m_model->m_tractor->profile(), "freeze");
2152 filter.set("mlt_service", "freeze");
2153 filter.set("frame", right ? mainClip->getIn() : mainClip->getOut());
2154 inOutFrame->attach(filter);
2155 }
2156
2157 std::vector<std::shared_ptr<Mlt::Producer>> producers;
2158 int previewLength = 0;
2159 switch (pCore->activeTool()) {
2160 case ToolType::SlipTool:
2161 // Get copy of timeline producer
2162 /* This is an example using a playlist. This does not work with switch -> use if else instead
2163 Mlt::Producer test(mainClip->getProducer()->cut(0));
2164 Mlt::Playlist list(*m_model->m_tractor->profile());
2165 list.append(test);
2166 producers.push_back(Mlt::Producer(list)); */
2167 producers.push_back(std::shared_ptr<Mlt::Producer>(previousFrame));
2168 producers.push_back(std::shared_ptr<Mlt::Producer>(mainClip->getProducer()->cut(0)));
2169 producers.push_back(std::shared_ptr<Mlt::Producer>(mainClip->getProducer()->cut(mainClip->getOut() - mainClip->getIn())));
2170 producers.push_back(nextFrame);
2171 previewLength = producers[1]->get_length();
2172 break;
2173 case ToolType::SlideTool:
2174 break;
2175 case ToolType::RollTool:
2176 break;
2177 case ToolType::RippleTool:
2178 if (right) {
2179 producers.push_back(std::shared_ptr<Mlt::Producer>(inOutFrame));
2180 }
2181 producers.push_back(std::shared_ptr<Mlt::Producer>(mainClip->getProducer()->cut(0)));
2182 if (!right) {
2183 producers.push_back(std::shared_ptr<Mlt::Producer>(inOutFrame));
2184 previewLength = producers[0]->get_length();
2185 } else {
2186 previewLength = producers[1]->get_length();
2187 }
2188 break;
2189 default:
2190 return false;
2191 }
2192
2193 // Built tractor
2194 Mlt::Tractor trac(*m_model->m_tractor->profile());
2195
2196 // Now that we know the length of the preview create and add black background producer
2197 std::shared_ptr<Mlt::Producer> black(new Mlt::Producer(*m_model->m_tractor->profile(), "color:black"));
2198 black->set("length", previewLength);
2199 black->set_in_and_out(0, previewLength);
2200 trac.set_track(*black.get(), 0);
2201 //trac.set_track( 1);
2202
2203
2204 if (!mainClip->isAudioOnly()) {
2205 int count = 1; // 0 is background track so we start at 1
2206 for (auto const &producer : producers) {
2207 trac.set_track(*producer.get(), count);
2208 count++;
2209 }
2210
2211 // Add "composite" transitions for multi clip view
2212 for (int i = 0; i < int(producers.size()); i++) {
2213 // Construct transition
2214 Mlt::Transition transition(*trac.profile(), "composite");
2215 transition.set("mlt_service", "composite");
2216 transition.set("a_track", 0);
2217 transition.set("b_track", i + 1);
2218 transition.set("distort", 0);
2219 transition.set("aligned", 0);
2220 // 200 is an arbitrary number so we can easily remove these transition later
2221 //transition.set("internal_added", 200);
2222
2223 QString geometry;
2224 switch (pCore->activeTool()) {
2225 case ToolType::RollTool:
2226 case ToolType::RippleTool:
2227 switch (i) {
2228 case 0:
2229 geometry = QStringLiteral("0 0 50% 100%");
2230 break;
2231 case 1:
2232 geometry = QStringLiteral("50% 0 50% 100%");
2233 break;
2234 }
2235 break;
2236 case ToolType::SlipTool:
2237 switch (i) {
2238 case 0:
2239 geometry = QStringLiteral("0 0 25% 25%");
2240 break;
2241 case 1:
2242 geometry = QStringLiteral("0 25% 50% 50%");
2243 break;
2244 case 2:
2245 geometry = QStringLiteral("50% 25% 50% 50%");
2246 break;
2247 case 3:
2248 geometry = QStringLiteral("75% 75% 25% 25%");
2249 break;
2250 }
2251 break;
2252 case ToolType::SlideTool:
2253 switch (i) {
2254 case 0:
2255 geometry = QStringLiteral("0 0 25% 25%");
2256 break;
2257 case 1:
2258 geometry = QStringLiteral("50% 25% 50% 50%");
2259 break;
2260 case 2:
2261 geometry = QStringLiteral("0 25% 50% 50%");
2262 break;
2263 case 3:
2264 geometry = QStringLiteral("50% 75% 25% 25%");
2265 break;
2266 }
2267 break;
2268 default:
2269 break;
2270 }
2271
2272 // Add transition to track:
2273 transition.set("geometry", geometry.toUtf8().constData());
2274 transition.set("always_active", 1);
2275 trac.plant_transition(transition, 0, i + 1);
2276 }
2277
2278 }
2279
2280 pCore->monitorManager()->projectMonitor()->setProducer(std::make_shared<Mlt::Producer>(trac), -2);
2281 pCore->monitorManager()->projectMonitor()->slotSwitchTrimming(true);
2282
2283 switch (pCore->activeTool()) {
2284 case ToolType::RollTool:
2285 case ToolType::RippleTool:
2286 ripplePosChanged(mainClip->getPlaytime(), right);
2287 break;
2288 case ToolType::SlipTool:
2289 slipPosChanged(0);
2290 break;
2291 case ToolType::SlideTool:
2292 break;
2293 default:
2294 break;
2295 }
2296
2297 return true;
2298 }
2299
requestEndTrimmingMode()2300 void TimelineController::requestEndTrimmingMode() {
2301 if (pCore->monitorManager()->isTrimming()) {
2302 pCore->monitorManager()->projectMonitor()->setProducer(pCore->window()->getCurrentTimeline()->model()->producer(), 0);
2303 pCore->monitorManager()->projectMonitor()->slotSwitchTrimming(false);
2304 }
2305 }
2306
addPreviewRange(bool add)2307 void TimelineController::addPreviewRange(bool add)
2308 {
2309 if (m_zone.isNull()) {
2310 return;
2311 }
2312 if (!m_timelinePreview) {
2313 initializePreview();
2314 }
2315 if (m_timelinePreview) {
2316 m_timelinePreview->addPreviewRange(m_zone, add);
2317 }
2318 }
2319
clearPreviewRange(bool resetZones)2320 void TimelineController::clearPreviewRange(bool resetZones)
2321 {
2322 if (m_timelinePreview) {
2323 m_timelinePreview->clearPreviewRange(resetZones);
2324 }
2325 }
2326
startPreviewRender()2327 void TimelineController::startPreviewRender()
2328 {
2329 // Timeline preview stuff
2330 if (!m_timelinePreview) {
2331 initializePreview();
2332 } else if (m_disablePreview->isChecked()) {
2333 m_disablePreview->setChecked(false);
2334 disablePreview(false);
2335 }
2336 if (m_timelinePreview) {
2337 if (!m_usePreview) {
2338 m_timelinePreview->buildPreviewTrack();
2339 m_usePreview = true;
2340 m_model->m_overlayTrackCount = m_timelinePreview->addedTracks();
2341 }
2342 m_timelinePreview->startPreviewRender();
2343 }
2344 }
2345
stopPreviewRender()2346 void TimelineController::stopPreviewRender()
2347 {
2348 if (m_timelinePreview) {
2349 m_timelinePreview->abortRendering();
2350 }
2351 }
2352
initializePreview()2353 void TimelineController::initializePreview()
2354 {
2355 if (m_timelinePreview) {
2356 // Update parameters
2357 if (!m_timelinePreview->loadParams()) {
2358 if (m_usePreview) {
2359 // Disconnect preview track
2360 m_timelinePreview->disconnectTrack();
2361 m_usePreview = false;
2362 }
2363 delete m_timelinePreview;
2364 m_timelinePreview = nullptr;
2365 }
2366 } else {
2367 m_timelinePreview = new PreviewManager(this, m_model->m_tractor.get());
2368 if (!m_timelinePreview->initialize()) {
2369 // TODO warn user
2370 delete m_timelinePreview;
2371 m_timelinePreview = nullptr;
2372 } else {
2373 }
2374 }
2375 QAction *previewRender = pCore->currentDoc()->getAction(QStringLiteral("prerender_timeline_zone"));
2376 if (previewRender) {
2377 previewRender->setEnabled(m_timelinePreview != nullptr);
2378 }
2379 m_disablePreview->setEnabled(m_timelinePreview != nullptr);
2380 m_disablePreview->blockSignals(true);
2381 m_disablePreview->setChecked(false);
2382 m_disablePreview->blockSignals(false);
2383 }
2384
hasPreviewTrack() const2385 bool TimelineController::hasPreviewTrack() const
2386 {
2387 return (m_timelinePreview && (m_timelinePreview->hasOverlayTrack() || m_timelinePreview->hasPreviewTrack()));
2388 }
2389
updatePreviewConnection(bool enable)2390 void TimelineController::updatePreviewConnection(bool enable)
2391 {
2392 if (m_timelinePreview) {
2393 if (enable) {
2394 m_timelinePreview->enable();
2395 } else {
2396 m_timelinePreview->disable();
2397 }
2398 }
2399 }
2400
disablePreview(bool disable)2401 void TimelineController::disablePreview(bool disable)
2402 {
2403 if (disable) {
2404 m_timelinePreview->deletePreviewTrack();
2405 m_usePreview = false;
2406 m_model->m_overlayTrackCount = m_timelinePreview->addedTracks();
2407 } else {
2408 if (!m_usePreview) {
2409 if (!m_timelinePreview->buildPreviewTrack()) {
2410 // preview track already exists, reconnect
2411 m_model->m_tractor->lock();
2412 m_timelinePreview->reconnectTrack();
2413 m_model->m_tractor->unlock();
2414 }
2415 Mlt::Playlist playlist;
2416 m_timelinePreview->loadChunks(QVariantList(), QVariantList(), QDateTime(), playlist);
2417 m_usePreview = true;
2418 }
2419 }
2420 m_model->m_overlayTrackCount = m_timelinePreview->addedTracks();
2421 }
2422
dirtyChunks() const2423 QVariantList TimelineController::dirtyChunks() const
2424 {
2425 return m_timelinePreview ? m_timelinePreview->m_dirtyChunks : QVariantList();
2426 }
2427
renderedChunks() const2428 QVariantList TimelineController::renderedChunks() const
2429 {
2430 return m_timelinePreview ? m_timelinePreview->m_renderedChunks : QVariantList();
2431 }
2432
workingPreview() const2433 int TimelineController::workingPreview() const
2434 {
2435 return m_timelinePreview ? m_timelinePreview->workingPreview : -1;
2436 }
2437
useRuler() const2438 bool TimelineController::useRuler() const
2439 {
2440 return pCore->currentDoc()->getDocumentProperty(QStringLiteral("enableTimelineZone")).toInt() == 1;
2441 }
2442
scrollVertically() const2443 bool TimelineController::scrollVertically() const
2444 {
2445 return KdenliveSettings::scrollvertically() == 1;
2446 }
2447
resetPreview()2448 void TimelineController::resetPreview()
2449 {
2450 if (m_timelinePreview) {
2451 m_timelinePreview->clearPreviewRange(true);
2452 initializePreview();
2453 }
2454 }
2455
loadPreview(const QString & chunks,const QString & dirty,const QDateTime & documentDate,int enable,Mlt::Playlist & playlist)2456 void TimelineController::loadPreview(const QString &chunks, const QString &dirty, const QDateTime &documentDate, int enable, Mlt::Playlist &playlist)
2457 {
2458 if (chunks.isEmpty() && dirty.isEmpty()) {
2459 return;
2460 }
2461 if (!m_timelinePreview) {
2462 initializePreview();
2463 }
2464 QVariantList renderedChunks;
2465 QVariantList dirtyChunks;
2466 #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
2467 QStringList chunksList = chunks.split(QLatin1Char(','), QString::SkipEmptyParts);
2468 #else
2469 QStringList chunksList = chunks.split(QLatin1Char(','), Qt::SkipEmptyParts);
2470 #endif
2471 #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
2472 QStringList dirtyList = dirty.split(QLatin1Char(','), QString::SkipEmptyParts);
2473 #else
2474 QStringList dirtyList = dirty.split(QLatin1Char(','), Qt::SkipEmptyParts);
2475 #endif
2476 for (const QString &frame : qAsConst(chunksList)) {
2477 renderedChunks << frame.toInt();
2478 }
2479 for (const QString &frame : qAsConst(dirtyList)) {
2480 dirtyChunks << frame.toInt();
2481 }
2482
2483 if ( m_disablePreview ) {
2484 m_disablePreview->blockSignals(true);
2485 m_disablePreview->setChecked(enable);
2486 m_disablePreview->blockSignals(false);
2487 }
2488 if ( m_timelinePreview ) {
2489 if (!enable) {
2490 m_timelinePreview->buildPreviewTrack();
2491 m_usePreview = true;
2492 m_model->m_overlayTrackCount = m_timelinePreview->addedTracks();
2493 }
2494 m_timelinePreview->loadChunks(renderedChunks, dirtyChunks, documentDate, playlist);
2495 }
2496 }
2497
documentProperties()2498 QMap<QString, QString> TimelineController::documentProperties()
2499 {
2500 QMap<QString, QString> props = pCore->currentDoc()->documentProperties();
2501 int audioTarget = m_model->m_audioTarget.isEmpty() ? -1 : m_model->getTrackPosition(m_model->m_audioTarget.firstKey());
2502 int videoTarget = m_model->m_videoTarget == -1 ? -1 : m_model->getTrackPosition(m_model->m_videoTarget);
2503 int activeTrack = m_activeTrack < 0 ? m_activeTrack : m_model->getTrackPosition(m_activeTrack);
2504 props.insert(QStringLiteral("audioTarget"), QString::number(audioTarget));
2505 props.insert(QStringLiteral("videoTarget"), QString::number(videoTarget));
2506 props.insert(QStringLiteral("activeTrack"), QString::number(activeTrack));
2507 props.insert(QStringLiteral("position"), QString::number(pCore->getTimelinePosition()));
2508 QVariant returnedValue;
2509 QMetaObject::invokeMethod(m_root, "getScrollPos", Q_RETURN_ARG(QVariant, returnedValue));
2510 int scrollPos = returnedValue.toInt();
2511 props.insert(QStringLiteral("scrollPos"), QString::number(scrollPos));
2512 props.insert(QStringLiteral("zonein"), QString::number(m_zone.x()));
2513 props.insert(QStringLiteral("zoneout"), QString::number(m_zone.y()));
2514 if (m_timelinePreview) {
2515 QPair<QStringList, QStringList> chunks = m_timelinePreview->previewChunks();
2516 props.insert(QStringLiteral("previewchunks"), chunks.first.join(QLatin1Char(',')));
2517 props.insert(QStringLiteral("dirtypreviewchunks"), chunks.second.join(QLatin1Char(',')));
2518 }
2519 props.insert(QStringLiteral("disablepreview"), QString::number(m_disablePreview->isChecked()));
2520 return props;
2521 }
2522
getMenuOrTimelinePos() const2523 int TimelineController::getMenuOrTimelinePos() const
2524 {
2525 int frame = m_root->property("clickFrame").toInt();
2526 if (frame == -1) {
2527 frame = pCore->getTimelinePosition();
2528 }
2529 return frame;
2530 }
2531
insertSpace(int trackId,int frame)2532 void TimelineController::insertSpace(int trackId, int frame)
2533 {
2534 if (frame == -1) {
2535 frame = getMenuOrTimelinePos();
2536 }
2537 if (trackId == -1) {
2538 trackId = m_activeTrack;
2539 }
2540 QPointer<SpacerDialog> d = new SpacerDialog(GenTime(65, pCore->getCurrentFps()), pCore->currentDoc()->timecode(), qApp->activeWindow());
2541 if (d->exec() != QDialog::Accepted) {
2542 delete d;
2543 return;
2544 }
2545 bool affectAllTracks = d->affectAllTracks();
2546 int cid = requestSpacerStartOperation(affectAllTracks ? -1 : trackId, frame);
2547 int spaceDuration = d->selectedDuration().frames(pCore->getCurrentFps());
2548 delete d;
2549 if (cid == -1) {
2550 pCore->displayMessage(i18n("No clips found to insert space"), ErrorMessage, 500);
2551 return;
2552 }
2553 int start = m_model->getItemPosition(cid);
2554 requestSpacerEndOperation(cid, start, start + spaceDuration, affectAllTracks ? -1 : trackId);
2555 }
2556
removeSpace(int trackId,int frame,bool affectAllTracks)2557 void TimelineController::removeSpace(int trackId, int frame, bool affectAllTracks)
2558 {
2559 if (frame == -1) {
2560 frame = getMenuOrTimelinePos();
2561 }
2562 if (trackId == -1) {
2563 trackId = m_activeTrack;
2564 }
2565 bool res = TimelineFunctions::requestDeleteBlankAt(m_model, trackId, frame, affectAllTracks);
2566 if (!res) {
2567 pCore->displayMessage(i18n("Cannot remove space at given position"), ErrorMessage, 500);
2568 }
2569 }
2570
invalidateItem(int cid)2571 void TimelineController::invalidateItem(int cid)
2572 {
2573 if (!m_timelinePreview || !m_model->isItem(cid)) {
2574 return;
2575 }
2576 const int tid = m_model->getItemTrackId(cid);
2577 if (tid == -1 || m_model->getTrackById_const(tid)->isAudioTrack()) {
2578 return;
2579 }
2580 int start = m_model->getItemPosition(cid);
2581 int end = start + m_model->getItemPlaytime(cid);
2582 m_timelinePreview->invalidatePreview(start, end);
2583 }
2584
invalidateTrack(int tid)2585 void TimelineController::invalidateTrack(int tid)
2586 {
2587 if (!m_timelinePreview || !m_model->isTrack(tid) || m_model->getTrackById_const(tid)->isAudioTrack()) {
2588 return;
2589 }
2590 for (const auto &clp : m_model->getTrackById_const(tid)->m_allClips) {
2591 invalidateItem(clp.first);
2592 }
2593 }
2594
invalidateZone(int in,int out)2595 void TimelineController::invalidateZone(int in, int out)
2596 {
2597 if (!m_timelinePreview) {
2598 return;
2599 }
2600 m_timelinePreview->invalidatePreview(in, out == -1 ? m_duration : out);
2601 }
2602
remapItemTime(int clipId)2603 void TimelineController::remapItemTime(int clipId)
2604 {
2605 if (clipId == -1) {
2606 clipId = getMainSelectedClip();
2607 }
2608 // Don't allow remaping a clip with speed effect
2609 if (clipId == -1 || !m_model->isClip(clipId) || !qFuzzyCompare(1., m_model->m_allClips[clipId]->getSpeed())) {
2610 pCore->displayMessage(i18n("No item to edit"), ErrorMessage, 500);
2611 return;
2612 }
2613 ClipType::ProducerType type = m_model->m_allClips[clipId]->clipType();
2614 if (type == ClipType::Color || type == ClipType::Image) {
2615 pCore->displayMessage(i18n("No item to edit"), ErrorMessage, 500);
2616 return;
2617 }
2618 if (m_model->m_allClips[clipId]->isChain()) {
2619 // Remove remap effect
2620 m_model->requestClipTimeRemap(clipId, false);
2621 emit pCore->remapClip(-1);
2622 } else {
2623 // Add remap effect
2624 emit pCore->remapClip(clipId);
2625 }
2626 updateClipActions();
2627 }
2628
changeItemSpeed(int clipId,double speed)2629 void TimelineController::changeItemSpeed(int clipId, double speed)
2630 {
2631 /*if (clipId == -1) {
2632 clipId = getMainSelectedItem(false, true);
2633 }*/
2634 if (clipId == -1) {
2635 clipId = getMainSelectedClip();
2636 }
2637 if (clipId == -1) {
2638 pCore->displayMessage(i18n("No item to edit"), ErrorMessage, 500);
2639 return;
2640 }
2641 bool pitchCompensate = m_model->m_allClips[clipId]->getIntProperty(QStringLiteral("warp_pitch"));
2642 if (qFuzzyCompare(speed, -1)) {
2643 speed = 100 * m_model->getClipSpeed(clipId);
2644 int duration = m_model->getItemPlaytime(clipId);
2645 // this is the max speed so that the clip is at least one frame long
2646 double maxSpeed = duration * qAbs(speed);
2647 // this is the min speed so that the clip doesn't bump into the next one on track
2648 double minSpeed = duration * qAbs(speed) / (duration + double(m_model->getBlankSizeNearClip(clipId, true)));
2649
2650 // if there is a split partner, we must also take it into account
2651 int partner = m_model->getClipSplitPartner(clipId);
2652 if (partner != -1) {
2653 double duration2 = m_model->getItemPlaytime(partner);
2654 double maxSpeed2 = 100. * duration2 * qAbs(m_model->getClipSpeed(partner));
2655 double minSpeed2 = 100. * duration2 * qAbs(m_model->getClipSpeed(partner)) / (duration2 + double(m_model->getBlankSizeNearClip(partner, true)));
2656 minSpeed = std::max(minSpeed, minSpeed2);
2657 maxSpeed = std::min(maxSpeed, maxSpeed2);
2658 }
2659 QScopedPointer<SpeedDialog> d(new SpeedDialog(QApplication::activeWindow(), std::abs(speed), duration, minSpeed, maxSpeed, speed < 0, pitchCompensate));
2660 if (d->exec() != QDialog::Accepted) {
2661 emit regainFocus();
2662 return;
2663 }
2664 emit regainFocus();
2665 speed = d->getValue();
2666 pitchCompensate = d->getPitchCompensate();
2667 qDebug() << "requesting speed " << speed;
2668 }
2669 bool res = m_model->requestClipTimeWarp(clipId, speed, pitchCompensate, true);
2670 if (res) {
2671 updateClipActions();
2672 }
2673 }
2674
switchCompositing(int mode)2675 void TimelineController::switchCompositing(int mode)
2676 {
2677 // m_model->m_tractor->lock();
2678 pCore->currentDoc()->setDocumentProperty(QStringLiteral("compositing"), QString::number(mode));
2679 QScopedPointer<Mlt::Service> service(m_model->m_tractor->field());
2680 QScopedPointer<Mlt::Field>field(m_model->m_tractor->field());
2681 field->lock();
2682 while ((service != nullptr) && service->is_valid()) {
2683 if (service->type() == mlt_service_transition_type) {
2684 Mlt::Transition t(mlt_transition(service->get_service()));
2685 service.reset(service->producer());
2686 QString serviceName = t.get("mlt_service");
2687 if (t.get_int("internal_added") == 237 && serviceName != QLatin1String("mix")) {
2688 // remove all compositing transitions
2689 field->disconnect_service(t);
2690 t.disconnect_all_producers();
2691 }
2692 } else {
2693 service.reset(service->producer());
2694 }
2695 }
2696 if (mode > 0) {
2697 // Loop through tracks
2698 for (int track = 0; track < m_model->getTracksCount(); track++) {
2699 if (m_model->getTrackById(m_model->getTrackIndexFromPosition(track))->getProperty("kdenlive:audio_track").toInt() == 0) {
2700 // This is a video track
2701 Mlt::Transition t(*m_model->m_tractor->profile(),
2702 mode == 1 ? "composite" : TransitionsRepository::get()->getCompositingTransition().toUtf8().constData());
2703 t.set("always_active", 1);
2704 t.set_tracks(0, track + 1);
2705 t.set("internal_added", 237);
2706 field->plant_transition(t, 0, track + 1);
2707 }
2708 }
2709 }
2710 field->unlock();
2711 pCore->requestMonitorRefresh();
2712 }
2713
extractZone(QPoint zone,bool liftOnly)2714 void TimelineController::extractZone(QPoint zone, bool liftOnly)
2715 {
2716 QVector<int> tracks;
2717 auto it = m_model->m_allTracks.cbegin();
2718 while (it != m_model->m_allTracks.cend()) {
2719 int target_track = (*it)->getId();
2720 if (m_model->getTrackById_const(target_track)->shouldReceiveTimelineOp()) {
2721 tracks << target_track;
2722 }
2723 ++it;
2724 }
2725 if (tracks.isEmpty()) {
2726 pCore->displayMessage(i18n("Please activate a track for this operation by clicking on its label"), ErrorMessage);
2727 }
2728 if (m_zone.isNull()) {
2729 // Use current timeline position and clip zone length
2730 zone.setY(pCore->getTimelinePosition() + zone.y() - zone.x());
2731 zone.setX(pCore->getTimelinePosition());
2732 }
2733 TimelineFunctions::extractZone(m_model, tracks, m_zone == QPoint() ? zone : m_zone, liftOnly);
2734 if (!liftOnly && !m_zone.isNull()) {
2735 setPosition(m_zone.x());
2736 }
2737 }
2738
extract(int clipId)2739 void TimelineController::extract(int clipId)
2740 {
2741 if (clipId == -1) {
2742 std::unordered_set<int> sel = m_model->getCurrentSelection();
2743 for (int i : sel) {
2744 if (m_model->isClip(i)) {
2745 clipId = i;
2746 break;
2747 }
2748 }
2749 if (clipId == -1) {
2750 pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
2751 return;
2752 }
2753 }
2754 int in = m_model->getClipPosition(clipId);
2755 int out = in + m_model->getClipPlaytime(clipId);
2756 int tid = m_model->getClipTrackId(clipId);
2757 std::pair<MixInfo,MixInfo> mixData = m_model->getTrackById_const(tid)->getMixInfo(clipId);
2758 if (mixData.first.firstClipId > -1) {
2759 // Clip has a start mix, adjust in point
2760 in += (mixData.first.firstClipInOut.second - mixData.first.secondClipInOut.first - mixData.first.mixOffset);
2761 }
2762 if (mixData.second.firstClipId > -1) {
2763 // Clip has end mix, adjust out point
2764 out -= mixData.second.mixOffset;
2765 }
2766 QVector <int> tracks = {tid};
2767 if (m_model->m_groups->isInGroup(clipId)) {
2768 int targetRoot = m_model->m_groups->getRootId(clipId);
2769 if (m_model->isGroup(targetRoot)) {
2770 std::unordered_set<int> sub = m_model->m_groups->getLeaves(targetRoot);
2771 for (int current_id : sub) {
2772 if (current_id == clipId) {
2773 continue;
2774 }
2775 if (m_model->isClip(current_id)) {
2776 int newIn = m_model->getClipPosition(current_id);
2777 int newOut = newIn + m_model->getClipPlaytime(current_id);
2778 int tk = m_model->getClipTrackId(current_id);
2779 std::pair<MixInfo,MixInfo> mixData = m_model->getTrackById_const(tk)->getMixInfo(current_id);
2780 if (mixData.first.firstClipId > -1) {
2781 // Clip has a start mix, adjust in point
2782 newIn += (mixData.first.firstClipInOut.second - mixData.first.secondClipInOut.first - mixData.first.mixOffset);
2783 }
2784 if (mixData.second.firstClipId > -1) {
2785 // Clip has end mix, adjust out point
2786 newOut -= mixData.second.mixOffset;
2787 }
2788 in = qMin(in, newIn);
2789 out = qMax(out, newOut);
2790 if (!tracks.contains(tk)) {
2791 tracks << tk;
2792 }
2793 }
2794 }
2795 }
2796 }
2797 TimelineFunctions::extractZone(m_model, tracks, QPoint(in, out), false);
2798 }
2799
saveZone(int clipId)2800 void TimelineController::saveZone(int clipId)
2801 {
2802 if (clipId == -1) {
2803 clipId = getMainSelectedClip();
2804 if (clipId == -1) {
2805 pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
2806 return;
2807 }
2808 }
2809 int in = m_model->getClipIn(clipId);
2810 int out = in + m_model->getClipPlaytime(clipId);
2811 QString id;
2812 pCore->projectItemModel()->requestAddBinSubClip(id, in, out, {}, m_model->m_allClips[clipId]->binId());
2813 }
2814
insertClipZone(const QString & binId,int tid,int position)2815 bool TimelineController::insertClipZone(const QString &binId, int tid, int position)
2816 {
2817 QStringList binIdData = binId.split(QLatin1Char('/'));
2818 int in = 0;
2819 int out = -1;
2820 if (binIdData.size() >= 3) {
2821 in = binIdData.at(1).toInt();
2822 out = binIdData.at(2).toInt();
2823 }
2824
2825 QString bid = binIdData.first();
2826 // dropType indicates if we want a normal drop (disabled), audio only or video only drop
2827 PlaylistState::ClipState dropType = PlaylistState::Disabled;
2828 if (bid.startsWith(QLatin1Char('A'))) {
2829 dropType = PlaylistState::AudioOnly;
2830 bid = bid.remove(0, 1);
2831 } else if (bid.startsWith(QLatin1Char('V'))) {
2832 dropType = PlaylistState::VideoOnly;
2833 bid = bid.remove(0, 1);
2834 }
2835 QList <int> audioTracks;
2836 int vTrack = -1;
2837 std::shared_ptr<ProjectClip> clip = pCore->bin()->getBinClip(bid);
2838 if (out <= in) {
2839 out = int(clip->frameDuration() - 1);
2840 }
2841 QList <int> audioStreams = m_model->m_binAudioTargets.keys();
2842 if (dropType == PlaylistState::VideoOnly) {
2843 vTrack = tid;
2844 } else if (dropType == PlaylistState::AudioOnly) {
2845 audioTracks << tid;
2846 if (audioStreams.size() > 1) {
2847 // insert the other audio streams
2848 QList <int> lower = m_model->getLowerTracksId(tid, TrackType::AudioTrack);
2849 while (audioStreams.size() > 1 && !lower.isEmpty()) {
2850 audioTracks << lower.takeFirst();
2851 audioStreams.takeFirst();
2852 }
2853 }
2854 } else {
2855 if (m_model->getTrackById_const(tid)->isAudioTrack()) {
2856 audioTracks << tid;
2857 if (audioStreams.size() > 1) {
2858 // insert the other audio streams
2859 QList <int> lower = m_model->getLowerTracksId(tid, TrackType::AudioTrack);
2860 while (audioStreams.size() > 1 && !lower.isEmpty()) {
2861 audioTracks << lower.takeFirst();
2862 audioStreams.takeFirst();
2863 }
2864 }
2865 vTrack = clip->hasAudioAndVideo() ? m_model->getMirrorVideoTrackId(tid) : -1;
2866 } else {
2867 vTrack = tid;
2868 if (clip->hasAudioAndVideo()) {
2869 int firstAudio = m_model->getMirrorAudioTrackId(vTrack);
2870 audioTracks << firstAudio;
2871 if (audioStreams.size() > 1) {
2872 // insert the other audio streams
2873 QList <int> lower = m_model->getLowerTracksId(firstAudio, TrackType::AudioTrack);
2874 while (audioStreams.size() > 1 && !lower.isEmpty()) {
2875 audioTracks << lower.takeFirst();
2876 audioStreams.takeFirst();
2877 }
2878 }
2879 }
2880 }
2881 }
2882 QList<int> target_tracks;
2883 if (vTrack > -1) {
2884 target_tracks << vTrack;
2885 }
2886 if (!audioTracks.isEmpty()) {
2887 target_tracks << audioTracks;
2888 }
2889 qDebug()<<"=====================\n\nREADY TO INSERT IN TRACKS: "<<audioTracks<<" / VIDEO: "<<vTrack<<"\n\n=========";
2890 std::function<bool(void)> undo = []() { return true; };
2891 std::function<bool(void)> redo = []() { return true; };
2892 bool overwrite = m_model->m_editMode == TimelineMode::OverwriteEdit;
2893 QPoint zone(in, out + 1);
2894 bool res = TimelineFunctions::insertZone(m_model, target_tracks, binId, position, zone, overwrite, false, undo, redo);
2895 if (res) {
2896 int newPos = position + (zone.y() - zone.x());
2897 int currentPos = pCore->getTimelinePosition();
2898 Fun redoPos = [this, newPos]() {
2899 Kdenlive::MonitorId activeMonitor = pCore->monitorManager()->activeMonitor()->id();
2900 pCore->monitorManager()->activateMonitor(Kdenlive::ProjectMonitor);
2901 pCore->monitorManager()->refreshProjectMonitor();
2902 setPosition(newPos);
2903 pCore->monitorManager()->activateMonitor(activeMonitor);
2904 return true;
2905 };
2906 Fun undoPos = [this, currentPos]() {
2907 Kdenlive::MonitorId activeMonitor = pCore->monitorManager()->activeMonitor()->id();
2908 pCore->monitorManager()->activateMonitor(Kdenlive::ProjectMonitor);
2909 pCore->monitorManager()->refreshProjectMonitor();
2910 setPosition(currentPos);
2911 pCore->monitorManager()->activateMonitor(activeMonitor);
2912 return true;
2913 };
2914 redoPos();
2915 UPDATE_UNDO_REDO_NOLOCK(redoPos, undoPos, undo, redo);
2916 pCore->pushUndo(undo, redo, overwrite ? i18n("Overwrite zone") : i18n("Insert zone"));
2917 } else {
2918 pCore->displayMessage(i18n("Could not insert zone"), ErrorMessage);
2919 undo();
2920 }
2921 return res;
2922 }
2923
insertZone(const QString & binId,QPoint zone,bool overwrite)2924 int TimelineController::insertZone(const QString &binId, QPoint zone, bool overwrite)
2925 {
2926 std::shared_ptr<ProjectClip> clip = pCore->bin()->getBinClip(binId);
2927 int aTrack = -1;
2928 int vTrack = -1;
2929 if (clip->hasAudio() && !m_model->m_audioTarget.isEmpty()) {
2930 QList<int> audioTracks = m_model->m_audioTarget.keys();
2931 for (int tid : qAsConst(audioTracks)) {
2932 if (m_model->getTrackById_const(tid)->shouldReceiveTimelineOp()) {
2933 aTrack = tid;
2934 break;
2935 }
2936 }
2937 }
2938 if (clip->hasVideo()) {
2939 vTrack = videoTarget();
2940 }
2941
2942 int insertPoint;
2943 QPoint sourceZone;
2944 if (useRuler() && m_zone != QPoint()) {
2945 // We want to use timeline zone for in/out insert points
2946 insertPoint = m_zone.x();
2947 sourceZone = QPoint(zone.x(), zone.x() + m_zone.y() - m_zone.x());
2948 } else {
2949 // Use current timeline pos and clip zone for in/out
2950 insertPoint = pCore->getTimelinePosition();
2951 sourceZone = zone;
2952 }
2953 QList<int> target_tracks;
2954 if (vTrack > -1) {
2955 target_tracks << vTrack;
2956 }
2957 if (aTrack > -1) {
2958 target_tracks << aTrack;
2959 }
2960 if (target_tracks.isEmpty()) {
2961 pCore->displayMessage(i18n("Please select a target track by clicking on a track's target zone"), ErrorMessage);
2962 return -1;
2963 }
2964 std::function<bool(void)> undo = []() { return true; };
2965 std::function<bool(void)> redo = []() { return true; };
2966 bool res = TimelineFunctions::insertZone(m_model, target_tracks, binId, insertPoint, sourceZone, overwrite, true, undo, redo);
2967 if (res) {
2968 int newPos = insertPoint + (sourceZone.y() - sourceZone.x());
2969 int currentPos = pCore->getTimelinePosition();
2970 Fun redoPos = [this, newPos]() {
2971 setPosition(newPos);
2972 pCore->getMonitor(Kdenlive::ProjectMonitor)->refreshMonitorIfActive();
2973 return true;
2974 };
2975 Fun undoPos = [this, currentPos]() {
2976 setPosition(currentPos);
2977 pCore->getMonitor(Kdenlive::ProjectMonitor)->refreshMonitorIfActive();
2978 return true;
2979 };
2980 redoPos();
2981 UPDATE_UNDO_REDO_NOLOCK(redoPos, undoPos, undo, redo);
2982 pCore->pushUndo(undo, redo, overwrite ? i18n("Overwrite zone") : i18n("Insert zone"));
2983 } else {
2984 pCore->displayMessage(i18n("Could not insert zone"), ErrorMessage);
2985 undo();
2986 }
2987 return res;
2988 }
2989
updateClip(int clipId,const QVector<int> & roles)2990 void TimelineController::updateClip(int clipId, const QVector<int> &roles)
2991 {
2992 QModelIndex ix = m_model->makeClipIndexFromID(clipId);
2993 if (ix.isValid()) {
2994 emit m_model->dataChanged(ix, ix, roles);
2995 }
2996 }
2997
showClipKeyframes(int clipId,bool value)2998 void TimelineController::showClipKeyframes(int clipId, bool value)
2999 {
3000 TimelineFunctions::showClipKeyframes(m_model, clipId, value);
3001 }
3002
showCompositionKeyframes(int clipId,bool value)3003 void TimelineController::showCompositionKeyframes(int clipId, bool value)
3004 {
3005 TimelineFunctions::showCompositionKeyframes(m_model, clipId, value);
3006 }
3007
switchEnableState(std::unordered_set<int> selection)3008 void TimelineController::switchEnableState(std::unordered_set<int> selection)
3009 {
3010 if (selection.empty()) {
3011 selection = m_model->getCurrentSelection();
3012 //clipId = getMainSelectedItem(false, false);
3013 }
3014 if (selection.empty()) {
3015 return;
3016 }
3017 TimelineFunctions::switchEnableState(m_model, selection);
3018 }
3019
addCompositionToClip(const QString & assetId,int clipId,int offset)3020 void TimelineController::addCompositionToClip(const QString &assetId, int clipId, int offset)
3021 {
3022 if (clipId == -1) {
3023 clipId = getMainSelectedClip();
3024 if (clipId == -1) {
3025 pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
3026 return;
3027 }
3028 }
3029 if (offset == -1) {
3030 offset = m_root->property("clickFrame").toInt();
3031 }
3032 int track = clipId > -1 ? m_model->getClipTrackId(clipId) : m_activeTrack;
3033 int compoId = -1;
3034 if (assetId.isEmpty()) {
3035 QStringList compositions = KdenliveSettings::favorite_transitions();
3036 if (compositions.isEmpty()) {
3037 pCore->displayMessage(i18n("Select a favorite composition"), ErrorMessage, 500);
3038 return;
3039 }
3040 compoId = insertNewComposition(track, clipId, offset, compositions.first(), true);
3041 } else {
3042 compoId = insertNewComposition(track, clipId, offset, assetId, true);
3043 }
3044 if (compoId > 0) {
3045 m_model->requestSetSelection({compoId});
3046 }
3047 }
3048
addEffectToClip(const QString & assetId,int clipId)3049 void TimelineController::addEffectToClip(const QString &assetId, int clipId)
3050 {
3051 if (clipId == -1) {
3052 clipId = getMainSelectedClip();
3053 if (clipId == -1) {
3054 pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
3055 return;
3056 }
3057 }
3058 qDebug() << "/// ADDING ASSET: " << assetId;
3059 m_model->addClipEffect(clipId, assetId);
3060 }
3061
splitAV()3062 bool TimelineController::splitAV()
3063 {
3064 int cid = *m_model->getCurrentSelection().begin();
3065 if (m_model->isClip(cid)) {
3066 std::shared_ptr<ClipModel> clip = m_model->getClipPtr(cid);
3067 if (clip->clipState() == PlaylistState::AudioOnly) {
3068 return TimelineFunctions::requestSplitVideo(m_model, cid, videoTarget());
3069 } else {
3070 QVariantList aTargets = audioTarget();
3071 int targetTrack = aTargets.isEmpty() ? -1 : aTargets.first().toInt();
3072 return TimelineFunctions::requestSplitAudio(m_model, cid, targetTrack);
3073 }
3074 }
3075 pCore->displayMessage(i18n("No clip found to perform AV split operation"), ErrorMessage, 500);
3076 return false;
3077 }
3078
splitAudio(int clipId)3079 void TimelineController::splitAudio(int clipId)
3080 {
3081 QVariantList aTargets = audioTarget();
3082 int targetTrack = aTargets.isEmpty() ? -1 : aTargets.first().toInt();
3083 TimelineFunctions::requestSplitAudio(m_model, clipId, targetTrack);
3084 }
3085
splitVideo(int clipId)3086 void TimelineController::splitVideo(int clipId)
3087 {
3088 TimelineFunctions::requestSplitVideo(m_model, clipId, videoTarget());
3089 }
3090
setAudioRef(int clipId)3091 void TimelineController::setAudioRef(int clipId)
3092 {
3093 if (clipId == -1) {
3094 clipId = getMainSelectedClip();
3095 if (clipId == -1) {
3096 pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
3097 return;
3098 }
3099 }
3100 m_audioRef = clipId;
3101 std::unique_ptr<AudioEnvelope> envelope(new AudioEnvelope(getClipBinId(clipId), clipId));
3102 m_audioCorrelator.reset(new AudioCorrelation(std::move(envelope)));
3103 connect(m_audioCorrelator.get(), &AudioCorrelation::gotAudioAlignData, this, [&](int cid, int shift) {
3104 // Ensure the clip was not deleted while processing calculations
3105 if (m_model->isClip(cid)) {
3106 int pos = m_model->getClipPosition(m_audioRef) + shift - m_model->getClipIn(m_audioRef);
3107 bool result = m_model->requestClipMove(cid, m_model->getClipTrackId(cid), pos, true, true, true);
3108 if (!result) {
3109 pCore->displayMessage(i18n("Cannot move clip to frame %1.", (pos + shift)), ErrorMessage, 500);
3110 }
3111 } else {
3112 // Clip was deleted, discard audio reference
3113 m_audioRef = -1;
3114 }
3115 });
3116 connect(m_audioCorrelator.get(), &AudioCorrelation::displayMessage, pCore.get(), &Core::displayMessage);
3117 }
3118
alignAudio(int clipId)3119 void TimelineController::alignAudio(int clipId)
3120 {
3121 // find other clip
3122 if (clipId == -1) {
3123 clipId = getMainSelectedClip();
3124 if (clipId == -1) {
3125 pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
3126 return;
3127 }
3128 }
3129 if (m_audioRef == -1 || m_audioRef == clipId || !m_model->isClip(m_audioRef)) {
3130 pCore->displayMessage(i18n("Set audio reference before attempting to align"), InformationMessage, 500);
3131 return;
3132 }
3133 const QString masterBinClipId = getClipBinId(m_audioRef);
3134 std::unordered_set<int> clipsToAnalyse;
3135 if (m_model->m_groups->isInGroup(clipId)) {
3136 clipsToAnalyse = m_model->getGroupElements(clipId);
3137 m_model->requestClearSelection();
3138 } else {
3139 clipsToAnalyse.insert(clipId);
3140 }
3141 QList <int> processedGroups;
3142 int processed = 0;
3143 for (int cid : clipsToAnalyse) {
3144 if (!m_model->isClip(cid) || cid == m_audioRef) {
3145 continue;
3146 }
3147 const QString otherBinId = getClipBinId(cid);
3148 if (m_model->m_groups->isInGroup(cid)) {
3149 int parentGroup = m_model->m_groups->getRootId(cid);
3150 if (processedGroups.contains(parentGroup)) {
3151 continue;
3152 }
3153 // Only process one clip from the group
3154 std::shared_ptr<ProjectClip> clip = pCore->bin()->getBinClip(otherBinId);
3155 if (clip->hasAudio()) {
3156 processedGroups << parentGroup;
3157 } else {
3158 continue;
3159 }
3160 }
3161 if (!pCore->bin()->getBinClip(otherBinId)->hasAudio()) {
3162 // Cannot process non audi clips
3163 continue;
3164 }
3165 if (otherBinId == masterBinClipId) {
3166 // easy, same clip.
3167 int newPos = m_model->getClipPosition(m_audioRef) - m_model->getClipIn(m_audioRef) + m_model->getClipIn(cid);
3168 if (newPos) {
3169 bool result = m_model->requestClipMove(cid, m_model->getClipTrackId(cid), newPos, true, true, true);
3170 processed ++;
3171 if (!result) {
3172 pCore->displayMessage(i18n("Cannot move clip to frame %1.", newPos), ErrorMessage, 500);
3173 }
3174 continue;
3175 }
3176 }
3177 processed ++;
3178 // Perform audio calculation
3179 auto *envelope = new AudioEnvelope(otherBinId, cid,
3180 size_t(m_model->getClipIn(cid)),
3181 size_t(m_model->getClipPlaytime(cid)),
3182 size_t(m_model->getClipPosition(cid)));
3183 m_audioCorrelator->addChild(envelope);
3184 }
3185 if (processed == 0) {
3186 //TODO: improve feedback message after freeze
3187 pCore->displayMessage(i18n("Select a clip to apply an effect"), ErrorMessage, 500);
3188 }
3189 }
3190
switchTrackActive(int trackId)3191 void TimelineController::switchTrackActive(int trackId)
3192 {
3193 if (trackId == -1) {
3194 trackId = m_activeTrack;
3195 }
3196 if (trackId < 0) {
3197 return;
3198 }
3199 bool active = m_model->getTrackById_const(trackId)->isTimelineActive();
3200 m_model->setTrackProperty(trackId, QStringLiteral("kdenlive:timeline_active"), active ? QStringLiteral("0") : QStringLiteral("1"));
3201 m_activeSnaps.clear();
3202 }
3203
switchAllTrackActive()3204 void TimelineController::switchAllTrackActive()
3205 {
3206 auto it = m_model->m_allTracks.cbegin();
3207 while (it != m_model->m_allTracks.cend()) {
3208 bool active = (*it)->isTimelineActive();
3209 int target_track = (*it)->getId();
3210 m_model->setTrackProperty(target_track, QStringLiteral("kdenlive:timeline_active"), active ? QStringLiteral("0") : QStringLiteral("1"));
3211 ++it;
3212 }
3213 m_activeSnaps.clear();
3214 }
3215
makeAllTrackActive()3216 void TimelineController::makeAllTrackActive()
3217 {
3218 // Check current status
3219 auto it = m_model->m_allTracks.cbegin();
3220 bool makeActive = false;
3221 while (it != m_model->m_allTracks.cend()) {
3222 if (!(*it)->isTimelineActive()) {
3223 // There is an inactive track, activate all
3224 makeActive = true;
3225 break;
3226 }
3227 ++it;
3228 }
3229 it = m_model->m_allTracks.cbegin();
3230 while (it != m_model->m_allTracks.cend()) {
3231 int target_track = (*it)->getId();
3232 m_model->setTrackProperty(target_track, QStringLiteral("kdenlive:timeline_active"), makeActive ? QStringLiteral("1") : QStringLiteral("0"));
3233 ++it;
3234 }
3235 m_activeSnaps.clear();
3236 }
3237
switchTrackLock(bool applyToAll)3238 void TimelineController::switchTrackLock(bool applyToAll)
3239 {
3240 if (!applyToAll) {
3241 // apply to active track only
3242 if (m_activeTrack == -2) {
3243 // Subtitle track
3244 switchSubtitleLock();
3245 } else {
3246 bool locked = m_model->getTrackById_const(m_activeTrack)->isLocked();
3247 m_model->setTrackLockedState(m_activeTrack, !locked);
3248 }
3249 } else {
3250 // Invert track lock
3251 const auto ids = m_model->getAllTracksIds();
3252 // count the number of tracks to be locked
3253 int toBeLockedCount =
3254 std::accumulate(ids.begin(), ids.end(), 0, [this](int s, int id) { return s + (m_model->getTrackById_const(id)->isLocked() ? 0 : 1); });
3255 auto subtitleModel = pCore->getSubtitleModel();
3256 bool subLocked = false;
3257 bool hasSubtitleTrack = false;
3258 if (subtitleModel) {
3259 hasSubtitleTrack = true;
3260 subLocked= subtitleModel->isLocked();
3261 if (!subLocked) {
3262 toBeLockedCount++;
3263 }
3264 }
3265 bool leaveOneUnlocked = toBeLockedCount == m_model->getTracksCount() + hasSubtitleTrack ? true : false;
3266 for (const int id : ids) {
3267 // leave active track unlocked
3268 if (leaveOneUnlocked && id == m_activeTrack) {
3269 continue;
3270 }
3271 bool isLocked = m_model->getTrackById_const(id)->isLocked();
3272 m_model->setTrackLockedState(id, !isLocked);
3273 }
3274 if (hasSubtitleTrack) {
3275 if (!leaveOneUnlocked || m_activeTrack != -2) {
3276 switchSubtitleLock();
3277 }
3278 }
3279 }
3280 }
3281
switchTargetTrack()3282 void TimelineController::switchTargetTrack()
3283 {
3284 if (m_activeTrack < 0) {
3285 return;
3286 }
3287 bool isAudio = m_model->getTrackById_const(m_activeTrack)->getProperty("kdenlive:audio_track").toInt() == 1;
3288 if (isAudio) {
3289 QMap<int, int> current = m_model->m_audioTarget;
3290 if (current.contains(m_activeTrack)) {
3291 current.remove(m_activeTrack);
3292 } else {
3293 int ix = getFirstUnassignedStream();
3294 if (ix > -1) {
3295 current.insert(m_activeTrack, ix);
3296 }
3297 }
3298 setAudioTarget(current);
3299 } else {
3300 setVideoTarget(videoTarget() == m_activeTrack ? -1 : m_activeTrack);
3301 }
3302 }
3303
audioTarget() const3304 QVariantList TimelineController::audioTarget() const
3305 {
3306 QVariantList audioTracks;
3307 QMapIterator <int, int>i(m_model->m_audioTarget);
3308 while (i.hasNext()) {
3309 i.next();
3310 audioTracks << i.key();
3311 }
3312 return audioTracks;
3313 }
3314
lastAudioTarget() const3315 QVariantList TimelineController::lastAudioTarget() const
3316 {
3317 QVariantList audioTracks;
3318 QMapIterator <int, int>i(m_lastAudioTarget);
3319 while (i.hasNext()) {
3320 i.next();
3321 audioTracks << i.key();
3322 }
3323 return audioTracks;
3324 }
3325
audioTargetName(int tid) const3326 const QString TimelineController::audioTargetName(int tid) const
3327 {
3328 if (m_model->m_audioTarget.contains(tid) && m_model->m_binAudioTargets.size() > 1) {
3329 int streamIndex = m_model->m_audioTarget.value(tid);
3330 if (m_model->m_binAudioTargets.contains(streamIndex)) {
3331 QString targetName = m_model->m_binAudioTargets.value(streamIndex);
3332 return targetName.isEmpty() ? QChar('x') : targetName.section(QLatin1Char('|'), 0, 0);
3333 } else {
3334 qDebug()<<"STREAM INDEX NOT IN TARGET : "<<streamIndex<<" = "<<m_model->m_binAudioTargets;
3335 }
3336 } else {
3337 qDebug()<<"TRACK NOT IN TARGET : "<<tid<<" = "<<m_model->m_audioTarget.keys();
3338 }
3339 return QString();
3340 }
3341
videoTarget() const3342 int TimelineController::videoTarget() const
3343 {
3344 return m_model->m_videoTarget;
3345 }
3346
hasAudioTarget() const3347 int TimelineController::hasAudioTarget() const
3348 {
3349 return m_hasAudioTarget;
3350 }
3351
clipTargets() const3352 int TimelineController::clipTargets() const
3353 {
3354 return m_model->m_binAudioTargets.size();
3355 }
3356
hasVideoTarget() const3357 bool TimelineController::hasVideoTarget() const
3358 {
3359 return m_hasVideoTarget;
3360 }
3361
autoScroll() const3362 bool TimelineController::autoScroll() const
3363 {
3364 return !pCore->monitorManager()->projectMonitor()->isPlaying() || KdenliveSettings::autoscroll();
3365 }
3366
resetTrackHeight()3367 void TimelineController::resetTrackHeight()
3368 {
3369 int tracksCount = m_model->getTracksCount();
3370 for (int track = tracksCount - 1; track >= 0; track--) {
3371 int trackIx = m_model->getTrackIndexFromPosition(track);
3372 m_model->getTrackById(trackIx)->setProperty(QStringLiteral("kdenlive:trackheight"), QString::number(KdenliveSettings::trackheight()));
3373 }
3374 QModelIndex modelStart = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(0));
3375 QModelIndex modelEnd = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(tracksCount - 1));
3376 emit m_model->dataChanged(modelStart, modelEnd, {TimelineModel::HeightRole});
3377 }
3378
selectAll()3379 void TimelineController::selectAll()
3380 {
3381 std::unordered_set<int> ids;
3382 for (const auto &clp : m_model->m_allClips) {
3383 ids.insert(clp.first);
3384 }
3385 for (const auto &clp : m_model->m_allCompositions) {
3386 ids.insert(clp.first);
3387 }
3388 // Subtitles
3389 for (const auto &sub : m_model->m_allSubtitles) {
3390 ids.insert(sub.first);
3391 }
3392 m_model->requestSetSelection(ids);
3393 }
3394
selectCurrentTrack()3395 void TimelineController::selectCurrentTrack()
3396 {
3397 if (m_activeTrack == -1) {
3398 return;
3399 }
3400 std::unordered_set<int> ids;
3401 if (m_activeTrack == -2) {
3402 for (const auto &sub : m_model->m_allSubtitles) {
3403 ids.insert(sub.first);
3404 }
3405 } else {
3406 for (const auto &clp : m_model->getTrackById_const(m_activeTrack)->m_allClips) {
3407 ids.insert(clp.first);
3408 }
3409 for (const auto &clp : m_model->getTrackById_const(m_activeTrack)->m_allCompositions) {
3410 ids.insert(clp.first);
3411 }
3412 }
3413 m_model->requestSetSelection(ids);
3414 }
3415
deleteEffects(int targetId)3416 void TimelineController::deleteEffects(int targetId)
3417 {
3418 std::unordered_set<int> targetIds;
3419 std::unordered_set<int> sel;
3420 if (targetId == -1) {
3421 sel = m_model->getCurrentSelection();
3422 } else {
3423 if (m_model->m_groups->isInGroup(targetId)) {
3424 sel = {m_model->m_groups->getRootId(targetId)};
3425 } else {
3426 sel = {targetId};
3427 }
3428 }
3429 if (sel.empty()) {
3430 pCore->displayMessage(i18n("No clip selected"), InformationMessage, 500);
3431 }
3432 for (int s : sel) {
3433 if (m_model->isGroup(s)) {
3434 std::unordered_set<int> sub = m_model->m_groups->getLeaves(s);
3435 for (int current_id : sub) {
3436 if (m_model->isClip(current_id)) {
3437 targetIds.insert(current_id);
3438 }
3439 }
3440 } else if (m_model->isClip(s)) {
3441 targetIds.insert(s);
3442 }
3443 }
3444 if (targetIds.empty()) {
3445 pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
3446 }
3447 std::function<bool(void)> undo = []() { return true; };
3448 std::function<bool(void)> redo = []() { return true; };
3449 for (int target : targetIds) {
3450 std::shared_ptr<EffectStackModel> destStack = m_model->getClipEffectStackModel(target);
3451 destStack->removeAllEffects(undo, redo);
3452 }
3453 pCore->pushUndo(undo, redo, i18n("Delete effects"));
3454 }
3455
pasteEffects(int targetId)3456 void TimelineController::pasteEffects(int targetId)
3457 {
3458 std::unordered_set<int> targetIds;
3459 std::unordered_set<int> sel;
3460 if (targetId == -1) {
3461 sel = m_model->getCurrentSelection();
3462 } else {
3463 if (m_model->m_groups->isInGroup(targetId)) {
3464 sel = {m_model->m_groups->getRootId(targetId)};
3465 } else {
3466 sel = {targetId};
3467 }
3468 }
3469 if (sel.empty()) {
3470 pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
3471 }
3472 for (int s : sel) {
3473 if (m_model->isGroup(s)) {
3474 std::unordered_set<int> sub = m_model->m_groups->getLeaves(s);
3475 for (int current_id : sub) {
3476 if (m_model->isClip(current_id)) {
3477 targetIds.insert(current_id);
3478 }
3479 }
3480 } else if (m_model->isClip(s)) {
3481 targetIds.insert(s);
3482 }
3483 }
3484 if (targetIds.empty()) {
3485 pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
3486 }
3487
3488 QClipboard *clipboard = QApplication::clipboard();
3489 QString txt = clipboard->text();
3490 if (txt.isEmpty()) {
3491 pCore->displayMessage(i18n("No information in clipboard"), ErrorMessage, 500);
3492 return;
3493 }
3494 QDomDocument copiedItems;
3495 copiedItems.setContent(txt);
3496 if (copiedItems.documentElement().tagName() != QLatin1String("kdenlive-scene")) {
3497 pCore->displayMessage(i18n("No information in clipboard"), ErrorMessage, 500);
3498 return;
3499 }
3500 QDomNodeList clips = copiedItems.documentElement().elementsByTagName(QStringLiteral("clip"));
3501 if (clips.isEmpty()) {
3502 pCore->displayMessage(i18n("No information in clipboard"), ErrorMessage, 500);
3503 return;
3504 }
3505 std::function<bool(void)> undo = []() { return true; };
3506 std::function<bool(void)> redo = []() { return true; };
3507 QDomElement effects = clips.at(0).firstChildElement(QStringLiteral("effects"));
3508 effects.setAttribute(QStringLiteral("parentIn"), clips.at(0).toElement().attribute(QStringLiteral("in")));
3509 for (int i = 1; i < clips.size(); i++) {
3510 QDomElement subeffects = clips.at(i).firstChildElement(QStringLiteral("effects"));
3511 QDomNodeList subs = subeffects.childNodes();
3512 while (!subs.isEmpty()) {
3513 subs.at(0).toElement().setAttribute(QStringLiteral("parentIn"), clips.at(i).toElement().attribute(QStringLiteral("in")));
3514 effects.appendChild(subs.at(0));
3515 }
3516 }
3517 int insertedEffects = 0;
3518 for (int target : targetIds) {
3519 std::shared_ptr<EffectStackModel> destStack = m_model->getClipEffectStackModel(target);
3520 if (destStack->fromXml(effects, undo, redo)) {
3521 insertedEffects++;
3522 }
3523 }
3524 if (insertedEffects > 0) {
3525 pCore->pushUndo(undo, redo, i18n("Paste effects"));
3526 } else {
3527 pCore->displayMessage(i18n("Cannot paste effect on selected clip"), ErrorMessage, 500);
3528 undo();
3529 }
3530 }
3531
fps() const3532 double TimelineController::fps() const
3533 {
3534 return pCore->getCurrentFps();
3535 }
3536
editItemDuration(int id)3537 void TimelineController::editItemDuration(int id)
3538 {
3539 if (id == -1) {
3540 id = m_root->property("mainItemId").toInt();
3541 if (id == -1) {
3542 std::unordered_set<int> sel = m_model->getCurrentSelection();
3543 if (!sel.empty()) {
3544 id = *sel.begin();
3545 }
3546 if (id == -1) {
3547 pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
3548 return;
3549 }
3550 }
3551 }
3552 if (id == -1 || !m_model->isItem(id)) {
3553 pCore->displayMessage(i18n("No item to edit"), ErrorMessage, 500);
3554 return;
3555 }
3556 int start = m_model->getItemPosition(id);
3557 int in = 0;
3558 int duration = m_model->getItemPlaytime(id);
3559 int maxLength = -1;
3560 bool isComposition = false;
3561 if (m_model->isClip(id)) {
3562 in = m_model->getClipIn(id);
3563 std::shared_ptr<ProjectClip> clip = pCore->bin()->getBinClip(getClipBinId(id));
3564 if (clip && clip->hasLimitedDuration()) {
3565 maxLength = clip->getProducerDuration();
3566 }
3567 } else if (m_model->isComposition(id)) {
3568 // nothing to do
3569 isComposition = true;
3570 } else {
3571 pCore->displayMessage(i18n("No item to edit"), ErrorMessage, 500);
3572 return;
3573 }
3574 int trackId = m_model->getItemTrackId(id);
3575 int maxFrame = qMax(0, start + duration +
3576 (isComposition ? m_model->getTrackById(trackId)->getBlankSizeNearComposition(id, true)
3577 : m_model->getTrackById(trackId)->getBlankSizeNearClip(id, true)));
3578 int minFrame = qMax(0, in - (isComposition ? m_model->getTrackById(trackId)->getBlankSizeNearComposition(id, false)
3579 : m_model->getTrackById(trackId)->getBlankSizeNearClip(id, false)));
3580 int partner = isComposition ? -1 : m_model->getClipSplitPartner(id);
3581 QPointer<ClipDurationDialog> dialog =
3582 new ClipDurationDialog(id, pCore->currentDoc()->timecode(), start, minFrame, in, in + duration, maxLength, maxFrame, qApp->activeWindow());
3583 if (dialog->exec() == QDialog::Accepted) {
3584 std::function<bool(void)> undo = []() { return true; };
3585 std::function<bool(void)> redo = []() { return true; };
3586 int newPos = dialog->startPos().frames(pCore->getCurrentFps());
3587 int newIn = dialog->cropStart().frames(pCore->getCurrentFps());
3588 int newDuration = dialog->duration().frames(pCore->getCurrentFps());
3589 bool result = true;
3590 if (newPos < start) {
3591 if (!isComposition) {
3592 result = m_model->requestClipMove(id, trackId, newPos, true, true, true, true, undo, redo);
3593 if (result && partner > -1) {
3594 result = m_model->requestClipMove(partner, m_model->getItemTrackId(partner), newPos, true, true, true, true, undo, redo);
3595 }
3596 } else {
3597 result = m_model->requestCompositionMove(id, trackId, m_model->m_allCompositions[id]->getForcedTrack(), newPos, true, true, undo, redo);
3598 }
3599 if (result && newIn != in) {
3600 int updatedDuration = duration + (in - newIn);
3601 m_model->requestItemResize(id, updatedDuration, false, true, undo, redo);
3602 if (result && partner > -1) {
3603 result = m_model->requestItemResize(partner, updatedDuration, false, true, undo, redo);
3604 }
3605 }
3606 if (newDuration != duration + (in - newIn)) {
3607 result = result && m_model->requestItemResize(id, newDuration, true, true, undo, redo);
3608 if (result && partner > -1) {
3609 result = m_model->requestItemResize(partner, newDuration, false, true, undo, redo);
3610 }
3611 }
3612 } else {
3613 // perform resize first
3614 if (newIn != in) {
3615 int updatedDuration = duration + (in - newIn);
3616 result = m_model->requestItemResize(id, updatedDuration, false, true, undo, redo);
3617 if (result && partner > -1) {
3618 result = m_model->requestItemResize(partner, updatedDuration, false, true, undo, redo);
3619 }
3620 }
3621 if (newDuration != duration + (in - newIn)) {
3622 result = result && m_model->requestItemResize(id, newDuration, start == newPos, true, undo, redo);
3623 if (result && partner > -1) {
3624 result = m_model->requestItemResize(partner, newDuration, start == newPos, true, undo, redo);
3625 }
3626 }
3627 if (start != newPos || newIn != in) {
3628 if (!isComposition) {
3629 result = result && m_model->requestClipMove(id, trackId, newPos, true, true, true, true, undo, redo);
3630 if (result && partner > -1) {
3631 result = m_model->requestClipMove(partner, m_model->getItemTrackId(partner), newPos, true, true, true, true, undo, redo);
3632 }
3633 } else {
3634 result = result &&
3635 m_model->requestCompositionMove(id, trackId, m_model->m_allCompositions[id]->getForcedTrack(), newPos, true, true, undo, redo);
3636 }
3637 }
3638 }
3639 if (result) {
3640 pCore->pushUndo(undo, redo, i18n("Edit item"));
3641 } else {
3642 undo();
3643 }
3644 }
3645 emit regainFocus();
3646 }
3647
editTitleClip(int id)3648 void TimelineController::editTitleClip(int id)
3649 {
3650 if (id == -1) {
3651 id = m_root->property("mainItemId").toInt();
3652 if (id == -1) {
3653 std::unordered_set<int> sel = m_model->getCurrentSelection();
3654 if (!sel.empty()) {
3655 id = *sel.begin();
3656 }
3657 if (id == -1 || !m_model->isItem(id) || !m_model->isClip(id)) {
3658 pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
3659 return;
3660 }
3661 }
3662 }
3663 std::shared_ptr<ProjectClip> binClip = pCore->projectItemModel()->getClipByBinID(getClipBinId(id));
3664 if(binClip->clipType() != ClipType::Text && binClip->clipType() != ClipType::TextTemplate) {
3665 pCore->displayMessage(i18n("Item is not a title clip"), ErrorMessage, 500);
3666 return;
3667 }
3668 seekToMouse();
3669 pCore->bin()->showTitleWidget(binClip);
3670 }
3671
selectionInOut() const3672 QPoint TimelineController::selectionInOut() const
3673 {
3674 std::unordered_set<int> ids = m_model->getCurrentSelection();
3675 std::unordered_set<int> items_list;
3676 for (int i : ids) {
3677 if (m_model->isGroup(i)) {
3678 std::unordered_set<int> children = m_model->m_groups->getLeaves(i);
3679 items_list.insert(children.begin(), children.end());
3680 } else {
3681 items_list.insert(i);
3682 }
3683 }
3684 int in = -1;
3685 int out = -1;
3686 for (int id : items_list) {
3687 if (m_model->isClip(id) || m_model->isComposition(id)) {
3688 int itemIn = m_model->getItemPosition(id);
3689 int itemOut = itemIn + m_model->getItemPlaytime(id);
3690 if (in < 0 || itemIn < in) {
3691 in = itemIn;
3692 }
3693 if (itemOut > out) {
3694 out = itemOut;
3695 }
3696 }
3697 }
3698 return QPoint(in, out);
3699 }
3700
updateClipActions()3701 void TimelineController::updateClipActions()
3702 {
3703 if (m_model->getCurrentSelection().empty()) {
3704 for (QAction *act : qAsConst(clipActions)) {
3705 act->setEnabled(false);
3706 }
3707 emit timelineClipSelected(false);
3708 // nothing selected
3709 emit showItemEffectStack(QString(), nullptr, QSize(), false);
3710 pCore->timeRemapWidget()->selectedClip(-1);
3711 emit showSubtitle(-1);
3712 return;
3713 }
3714 std::shared_ptr<ClipModel> clip(nullptr);
3715 int item = *m_model->getCurrentSelection().begin();
3716 int selectionSize = m_model->getCurrentSelection().size();
3717 if (selectionSize == 1) {
3718 if (m_model->isClip(item) || m_model->isComposition(item)) {
3719 showAsset(item);
3720 emit showSubtitle(-1);
3721 } else if (m_model->isSubTitle(item)) {
3722 emit showSubtitle(item);
3723 }
3724 }
3725 if (m_model->isClip(item)) {
3726 clip = m_model->getClipPtr(item);
3727 if (clip->isChain()) {
3728 emit pCore->remapClip(item);
3729 }
3730 }
3731 bool isInGroup = m_model->m_groups->isInGroup(item);
3732 PlaylistState::ClipState state = PlaylistState::ClipState::Unknown;
3733 ClipType::ProducerType type = ClipType::Unknown;
3734 if (clip) {
3735 state = clip->clipState();
3736 type = clip->clipType();
3737 }
3738 for (QAction *act : qAsConst(clipActions)) {
3739 bool enableAction = true;
3740 const QChar actionData = act->data().toChar();
3741 if (actionData == QLatin1Char('G')) {
3742 enableAction = isInSelection(item) && selectionSize > 1;
3743 } else if (actionData == QLatin1Char('U')) {
3744 enableAction = isInGroup;
3745 } else if (actionData == QLatin1Char('A')) {
3746 if (isInGroup && m_model->m_groups->getType(m_model->m_groups->getRootId(item)) == GroupType::AVSplit) {
3747 enableAction = true;
3748 } else {
3749 enableAction = state == PlaylistState::AudioOnly;
3750 }
3751 } else if (actionData == QLatin1Char('V')) {
3752 enableAction = state == PlaylistState::VideoOnly;
3753 } else if (actionData == QLatin1Char('D')) {
3754 enableAction = state == PlaylistState::Disabled;
3755 } else if (actionData == QLatin1Char('E')) {
3756 enableAction = state != PlaylistState::Disabled && state != PlaylistState::Unknown;
3757 } else if (actionData == QLatin1Char('X') || actionData == QLatin1Char('S')) {
3758 enableAction = clip && clip->canBeVideo() && clip->canBeAudio();
3759 if (enableAction && actionData == QLatin1Char('S')) {
3760 if (isInGroup) {
3761 // Check if all clips in the group have have same state (audio or video)
3762 int targetRoot = m_model->m_groups->getRootId(item);
3763 if (m_model->isGroup(targetRoot)) {
3764 std::unordered_set<int> sub = m_model->m_groups->getLeaves(targetRoot);
3765 for (int current_id : sub) {
3766 if (current_id == item) {
3767 continue;
3768 }
3769 if (m_model->isClip(current_id) && m_model->getClipPtr(current_id)->clipState() != state) {
3770 // Group with audio and video clips, disable split action
3771 enableAction = false;
3772 break;
3773 }
3774 }
3775 }
3776 }
3777 act->setText(state == PlaylistState::AudioOnly ? i18n("Restore video") : i18n("Restore audio"));
3778 }
3779 } else if (actionData == QLatin1Char('W')) {
3780 enableAction = clip != nullptr;
3781 if (enableAction) {
3782 act->setText(clip->clipState() == PlaylistState::Disabled ? i18n("Enable clip") : i18n("Disable clip"));
3783 }
3784 } else if (actionData == QLatin1Char('C') && clip == nullptr) {
3785 enableAction = false;
3786 } else if (actionData == QLatin1Char('P')) {
3787 // Position actions should stay enabled in clip monitor
3788 enableAction = true;
3789 } else if (actionData == QLatin1Char('R')) {
3790 // Time remap action
3791 enableAction = clip != nullptr && type != ClipType::Color && type != ClipType::Image
3792 && qFuzzyCompare(1., m_model->m_allClips[item]->getSpeed());
3793 if (enableAction) {
3794 act->setChecked(clip->isChain());
3795 }
3796 } else if (actionData == QLatin1Char('Q')) {
3797 // Speed change action
3798 enableAction = clip != nullptr && type != ClipType::Color && type != ClipType::Image
3799 && !clip->isChain();
3800 }
3801 act->setEnabled(enableAction);
3802 }
3803 emit timelineClipSelected(clip != nullptr);
3804 }
3805
getAssetName(const QString & assetId,bool isTransition)3806 const QString TimelineController::getAssetName(const QString &assetId, bool isTransition)
3807 {
3808 return isTransition ? TransitionsRepository::get()->getName(assetId) : EffectsRepository::get()->getName(assetId);
3809 }
3810
grabCurrent()3811 void TimelineController::grabCurrent()
3812 {
3813 if (trimmingActive()) {
3814 return;
3815 }
3816 std::unordered_set<int> ids = m_model->getCurrentSelection();
3817 std::unordered_set<int> items_list;
3818 int mainId = -1;
3819 for (int i : ids) {
3820 if (m_model->isGroup(i)) {
3821 std::unordered_set<int> children = m_model->m_groups->getLeaves(i);
3822 items_list.insert(children.begin(), children.end());
3823 } else {
3824 items_list.insert(i);
3825 }
3826 }
3827 for (int id : items_list) {
3828 if (mainId == -1 && m_model->getItemTrackId(id) == m_activeTrack) {
3829 mainId = id;
3830 continue;
3831 }
3832 if (m_model->isClip(id)) {
3833 std::shared_ptr<ClipModel> clip = m_model->getClipPtr(id);
3834 clip->setGrab(!clip->isGrabbed());
3835 } else if (m_model->isComposition(id)) {
3836 std::shared_ptr<CompositionModel> clip = m_model->getCompositionPtr(id);
3837 clip->setGrab(!clip->isGrabbed());
3838 } else if (m_model->isSubTitle(id)) {
3839 pCore->getSubtitleModel()->switchGrab(id);
3840 }
3841 }
3842 if (mainId > -1) {
3843 if (m_model->isClip(mainId)) {
3844 std::shared_ptr<ClipModel> clip = m_model->getClipPtr(mainId);
3845 clip->setGrab(!clip->isGrabbed());
3846 } else if (m_model->isComposition(mainId)) {
3847 std::shared_ptr<CompositionModel> clip = m_model->getCompositionPtr(mainId);
3848 clip->setGrab(!clip->isGrabbed());
3849 } else if (m_model->isSubTitle(mainId)) {
3850 pCore->getSubtitleModel()->switchGrab(mainId);
3851 }
3852 }
3853 }
3854
getItemMovingTrack(int itemId) const3855 int TimelineController::getItemMovingTrack(int itemId) const
3856 {
3857 if (m_model->isClip(itemId)) {
3858 int trackId = -1;
3859 if (m_model->m_editMode != TimelineMode::NormalEdit) {
3860 trackId = m_model->m_allClips[itemId]->getFakeTrackId();
3861 }
3862 return trackId < 0 ? m_model->m_allClips[itemId]->getCurrentTrackId() : trackId;
3863 }
3864 return m_model->m_allCompositions[itemId]->getCurrentTrackId();
3865 }
3866
endFakeMove(int clipId,int position,bool updateView,bool logUndo,bool invalidateTimeline)3867 bool TimelineController::endFakeMove(int clipId, int position, bool updateView, bool logUndo, bool invalidateTimeline)
3868 {
3869 Q_ASSERT(m_model->m_allClips.count(clipId) > 0);
3870 int trackId = m_model->m_allClips[clipId]->getFakeTrackId();
3871 if (m_model->getClipPosition(clipId) == position && m_model->getClipTrackId(clipId) == trackId) {
3872 qDebug() << "* * ** END FAKE; NO MOVE RQSTED";
3873 return true;
3874 }
3875 if (m_model->m_groups->isInGroup(clipId)) {
3876 // element is in a group.
3877 int groupId = m_model->m_groups->getRootId(clipId);
3878 int current_trackId = m_model->getClipTrackId(clipId);
3879 int track_pos1 = m_model->getTrackPosition(trackId);
3880 int track_pos2 = m_model->getTrackPosition(current_trackId);
3881 int delta_track = track_pos1 - track_pos2;
3882 int delta_pos = position - m_model->m_allClips[clipId]->getPosition();
3883 return endFakeGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo);
3884 }
3885 qDebug() << "//////\n//////\nENDING FAKE MOVE: " << trackId << ", POS: " << position;
3886 std::function<bool(void)> undo = []() { return true; };
3887 std::function<bool(void)> redo = []() { return true; };
3888 int startPos = m_model->getClipPosition(clipId);
3889 int duration = m_model->getClipPlaytime(clipId);
3890 int currentTrack = m_model->m_allClips[clipId]->getCurrentTrackId();
3891 bool res = true;
3892 if (currentTrack > -1) {
3893 std::pair<MixInfo,MixInfo> mixData = m_model->getTrackById_const(currentTrack)->getMixInfo(clipId);
3894 if (mixData.first.firstClipId > -1) {
3895 m_model->removeMixWithUndo(mixData.first.secondClipId, undo, redo);
3896 }
3897 if (mixData.second.firstClipId > -1) {
3898 m_model->removeMixWithUndo(mixData.second.secondClipId, undo, redo);
3899 }
3900 res = m_model->getTrackById(currentTrack)->requestClipDeletion(clipId, updateView, invalidateTimeline, undo, redo, false, false);
3901 }
3902 if (m_model->m_editMode == TimelineMode::OverwriteEdit) {
3903 res = res && TimelineFunctions::liftZone(m_model, trackId, QPoint(position, position + duration), undo, redo);
3904 } else if (m_model->m_editMode == TimelineMode::InsertEdit) {
3905 // Remove space from previous location
3906 if (currentTrack > -1) {
3907 res = res && TimelineFunctions::removeSpace(m_model, {startPos,startPos + duration}, undo, redo, {currentTrack}, false);
3908 }
3909 int startClipId = m_model->getClipByPosition(trackId, position);
3910 if (startClipId > -1) {
3911 // There is a clip at insert pos
3912 if (m_model->getClipPosition(startClipId) != position) {
3913 // If position is in the middle of the clip, cut it
3914 res = res && TimelineFunctions::requestClipCut(m_model, startClipId, position, undo, redo);
3915 }
3916 }
3917 res = res && TimelineFunctions::requestInsertSpace(m_model, QPoint(position, position + duration), undo, redo, {trackId});
3918 }
3919 res = res && m_model->getTrackById(trackId)->requestClipInsertion(clipId, position, updateView, invalidateTimeline, undo, redo);
3920 if (res) {
3921 // Terminate fake move
3922 if (m_model->isClip(clipId)) {
3923 m_model->m_allClips[clipId]->setFakeTrackId(-1);
3924 }
3925 if (logUndo) {
3926 pCore->pushUndo(undo, redo, i18n("Move item"));
3927 }
3928 } else {
3929 qDebug() << "//// FAKE FAILED";
3930 undo();
3931 }
3932 return res;
3933 }
3934
endFakeGroupMove(int clipId,int groupId,int delta_track,int delta_pos,bool updateView,bool logUndo)3935 bool TimelineController::endFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool logUndo)
3936 {
3937 std::function<bool(void)> undo = []() { return true; };
3938 std::function<bool(void)> redo = []() { return true; };
3939 bool res = endFakeGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo, undo, redo);
3940 if (res && logUndo) {
3941 // Terminate fake move
3942 if (m_model->isClip(clipId)) {
3943 m_model->m_allClips[clipId]->setFakeTrackId(-1);
3944 }
3945 pCore->pushUndo(undo, redo, i18n("Move group"));
3946 }
3947 return res;
3948 }
3949
endFakeGroupMove(int clipId,int groupId,int delta_track,int delta_pos,bool updateView,bool finalMove,Fun & undo,Fun & redo)3950 bool TimelineController::endFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool finalMove, Fun &undo, Fun &redo)
3951 {
3952 Q_ASSERT(m_model->m_allGroups.count(groupId) > 0);
3953 bool ok = true;
3954 auto all_items = m_model->m_groups->getLeaves(groupId);
3955 Q_ASSERT(all_items.size() > 1);
3956 Fun local_undo = []() { return true; };
3957 Fun local_redo = []() { return true; };
3958
3959 // Sort clips. We need to delete from right to left to avoid confusing the view
3960 std::vector<int> sorted_clips{std::make_move_iterator(std::begin(all_items)), std::make_move_iterator(std::end(all_items))};
3961 std::sort(sorted_clips.begin(), sorted_clips.end(), [this](const int &clipId1, const int &clipId2) {
3962 int p1 = m_model->isClip(clipId1) ? m_model->m_allClips[clipId1]->getPosition() : m_model->m_allCompositions[clipId1]->getPosition();
3963 int p2 = m_model->isClip(clipId2) ? m_model->m_allClips[clipId2]->getPosition() : m_model->m_allCompositions[clipId2]->getPosition();
3964 return p2 < p1;
3965 });
3966
3967 // Moving groups is a two stage process: first we remove the clips from the tracks, and then try to insert them back at their calculated new positions.
3968 // This way, we ensure that no conflict will arise with clips inside the group being moved
3969 // We are moving a group on another track, delete and re-add
3970
3971 // First, remove clips
3972 int audio_delta, video_delta;
3973 audio_delta = video_delta = delta_track;
3974 int master_trackId = m_model->getItemTrackId(clipId);
3975 if (m_model->getTrackById_const(master_trackId)->isAudioTrack()) {
3976 // Master clip is audio, so reverse delta for video clips
3977 video_delta = -delta_track;
3978 } else {
3979 audio_delta = -delta_track;
3980 }
3981 int min = -1;
3982 int max = -1;
3983 std::unordered_map<int, int> old_track_ids, old_position, old_forced_track, new_track_ids;
3984 std::vector<int> affected_trackIds;
3985 std::unordered_map<int, std::pair<QString,QVector<QPair<QString, QVariant>>>> mixToMove;
3986 std::unordered_map<int, MixInfo> mixInfoToMove;
3987 // Remove mixes not part of the group move
3988 for (int item : sorted_clips) {
3989 if (m_model->isClip(item)) {
3990 int tid = m_model->getItemTrackId(item);
3991 affected_trackIds.emplace_back(tid);
3992 std::pair<MixInfo,MixInfo> mixData = m_model->getTrackById_const(tid)->getMixInfo(item);
3993 if (mixData.first.firstClipId > -1) {
3994 if (std::find(sorted_clips.begin(), sorted_clips.end(), mixData.first.firstClipId) == sorted_clips.end()) {
3995 // Clip has startMix
3996 m_model->removeMixWithUndo(mixData.first.secondClipId, undo, redo);
3997 } else {
3998 // Get mix properties
3999 std::pair<QString,QVector<QPair<QString, QVariant>>> mixParams = m_model->getTrackById_const(tid)->getMixParams(mixData.first.secondClipId);
4000 mixToMove[item] = mixParams;
4001 mixInfoToMove[item] = mixData.first;
4002 }
4003 }
4004 if (mixData.second.firstClipId > -1 && std::find(sorted_clips.begin(), sorted_clips.end(), mixData.second.secondClipId) == sorted_clips.end()) {
4005 m_model->removeMixWithUndo(mixData.second.secondClipId, undo, redo);
4006 }
4007 }
4008 }
4009 for (int item : sorted_clips) {
4010 int old_trackId = m_model->getItemTrackId(item);
4011 old_track_ids[item] = old_trackId;
4012 if (old_trackId != -1) {
4013 bool updateThisView = true;
4014 if (m_model->isClip(item)) {
4015 int current_track_position = m_model->getTrackPosition(old_trackId);
4016 int d = m_model->getTrackById_const(old_trackId)->isAudioTrack() ? audio_delta : video_delta;
4017 int target_track_position = current_track_position + d;
4018 auto it = m_model->m_allTracks.cbegin();
4019 std::advance(it, target_track_position);
4020 int target_track = (*it)->getId();
4021 new_track_ids[item] = target_track;
4022 affected_trackIds.emplace_back(target_track);
4023 old_position[item] = m_model->m_allClips[item]->getPosition();
4024 int duration = m_model->m_allClips[item]->getPlaytime();
4025 min = min < 0 ? old_position[item] + delta_pos : qMin(min, old_position[item] + delta_pos);
4026 max = max < 0 ? old_position[item] + delta_pos + duration : qMax(max, old_position[item] + delta_pos + duration);
4027 ok = ok && m_model->getTrackById(old_trackId)->requestClipDeletion(item, updateThisView, finalMove, undo, redo, false, false);
4028 if (m_model->m_editMode == TimelineMode::InsertEdit) {
4029 // Lift space left by removed clip
4030 ok = ok && TimelineFunctions::removeSpace(m_model, {old_position[item],old_position[item] + duration}, undo, redo, {old_trackId}, false);
4031 }
4032 } else {
4033 // ok = ok && getTrackById(old_trackId)->requestCompositionDeletion(item, updateThisView, local_undo, local_redo);
4034 old_position[item] = m_model->m_allCompositions[item]->getPosition();
4035 old_forced_track[item] = m_model->m_allCompositions[item]->getForcedTrack();
4036 }
4037 if (!ok) {
4038 bool undone = undo();
4039 Q_ASSERT(undone);
4040 return false;
4041 }
4042 }
4043 }
4044 bool res = true;
4045 if (m_model->m_editMode == TimelineMode::OverwriteEdit) {
4046 for (int item : sorted_clips) {
4047 if (m_model->isClip(item) && new_track_ids.count(item) > 0) {
4048 int target_track = new_track_ids[item];
4049 int target_position = old_position[item] + delta_pos;
4050 int duration = m_model->m_allClips[item]->getPlaytime();
4051 res = res && TimelineFunctions::liftZone(m_model, target_track, QPoint(target_position, target_position + duration), undo, redo);
4052 }
4053 }
4054 } else if (m_model->m_editMode == TimelineMode::InsertEdit) {
4055 QVector<int> processedTracks;
4056 for (int item : sorted_clips) {
4057 int target_track = new_track_ids[item];
4058 if (processedTracks.contains(target_track)) {
4059 // already processed
4060 continue;
4061 }
4062 processedTracks << target_track;
4063 int target_position = min;
4064 int startClipId = m_model->getClipByPosition(target_track, target_position);
4065 if (startClipId > -1) {
4066 // There is a clip, cut
4067 res = res && TimelineFunctions::requestClipCut(m_model, startClipId, target_position, undo, redo);
4068 }
4069 }
4070 res = res && TimelineFunctions::requestInsertSpace(m_model, QPoint(min, max), undo, redo, processedTracks);
4071 }
4072 for (int item : sorted_clips) {
4073 if (m_model->isClip(item)) {
4074 int target_track = new_track_ids[item];
4075 int target_position = old_position[item] + delta_pos;
4076 ok = ok && m_model->requestClipMove(item, target_track, target_position, true, updateView, finalMove, finalMove, undo, redo);
4077 } else {
4078 // ok = ok && requestCompositionMove(item, target_track, old_forced_track[item], target_position, updateThisView, local_undo, local_redo);
4079 }
4080 if (!ok) {
4081 bool undone = undo();
4082 Q_ASSERT(undone);
4083 return false;
4084 }
4085 }
4086
4087 if (delta_track == 0) {
4088 Fun sync_mix = [this, affected_trackIds, finalMove]() {
4089 for (int t : affected_trackIds) {
4090 m_model->getTrackById_const(t)->syncronizeMixes(finalMove);
4091 }
4092 return true;
4093 };
4094 sync_mix();
4095 PUSH_LAMBDA(sync_mix, redo);
4096 return true;
4097 }
4098 for (int item : sorted_clips) {
4099 if (mixToMove.find(item) == mixToMove.end()) {
4100 continue;
4101 }
4102 int trackId = new_track_ids[item];
4103 int previous_track = old_track_ids[item];
4104 MixInfo mixData = mixInfoToMove[item];
4105 std::pair<QString,QVector<QPair<QString, QVariant>>> mixParams = mixToMove[item];
4106 Fun simple_move_mix = [this, previous_track, trackId, finalMove, mixData, mixParams]() {
4107 // Insert mix on new track
4108 bool result = m_model->getTrackById_const(trackId)->createMix(mixData, mixParams, finalMove);
4109 // Remove mix on old track
4110 m_model->getTrackById_const(previous_track)->removeMix(mixData);
4111 return result;
4112 };
4113 Fun simple_restore_mix = [this, previous_track, trackId, finalMove, mixData, mixParams]() {
4114 bool result = m_model->getTrackById_const(previous_track)->createMix(mixData, mixParams, finalMove);
4115 // Remove mix on old track
4116 m_model->getTrackById_const(trackId)->removeMix(mixData);
4117 return result;
4118 };
4119 simple_move_mix();
4120 if (finalMove) {
4121 PUSH_LAMBDA(simple_restore_mix, undo);
4122 PUSH_LAMBDA(simple_move_mix, redo);
4123 }
4124 }
4125 return true;
4126 }
4127
getThumbKeys()4128 QStringList TimelineController::getThumbKeys()
4129 {
4130 QStringList result;
4131 for (const auto &clp : m_model->m_allClips) {
4132 const QString binId = getClipBinId(clp.first);
4133 std::shared_ptr<ProjectClip> binClip = pCore->bin()->getBinClip(binId);
4134 result << binClip->hash() + QLatin1Char('#') + QString::number(clp.second->getIn()) + QStringLiteral(".jpg");
4135 result << binClip->hash() + QLatin1Char('#') + QString::number(clp.second->getOut()) + QStringLiteral(".jpg");
4136 }
4137 result.removeDuplicates();
4138 return result;
4139 }
4140
isInSelection(int itemId)4141 bool TimelineController::isInSelection(int itemId)
4142 {
4143 return m_model->getCurrentSelection().count(itemId) > 0;
4144 }
4145
exists(int itemId)4146 bool TimelineController::exists(int itemId)
4147 {
4148 return m_model->isClip(itemId) || m_model->isComposition(itemId);
4149 }
4150
slotMultitrackView(bool enable,bool refresh)4151 void TimelineController::slotMultitrackView(bool enable, bool refresh)
4152 {
4153 QStringList trackNames = TimelineFunctions::enableMultitrackView(m_model, enable, refresh);
4154 if (!refresh) {
4155 // This is just a temporary state (disable multitrack view for playlist save, don't change scene
4156 return;
4157 }
4158 pCore->monitorManager()->projectMonitor()->slotShowEffectScene(enable ? MonitorSplitTrack : MonitorSceneNone, false, QVariant(trackNames));
4159 QObject::disconnect( m_connection );
4160 if (enable) {
4161 connect(m_model.get(), &TimelineItemModel::trackVisibilityChanged, this, &TimelineController::updateMultiTrack, Qt::UniqueConnection);
4162 m_connection = connect(this, &TimelineController::activeTrackChanged, [this]() {
4163 int ix = 0;
4164 auto it = m_model->m_allTracks.cbegin();
4165 while (it != m_model->m_allTracks.cend()) {
4166 int target_track = (*it)->getId();
4167 ++it;
4168 if (target_track == m_activeTrack) {
4169 break;
4170 }
4171 if (m_model->getTrackById_const(target_track)->isAudioTrack() || m_model->getTrackById_const(target_track)->isHidden()) {
4172 continue;
4173 }
4174 ++ix;
4175 }
4176 pCore->monitorManager()->projectMonitor()->updateMultiTrackView(ix);
4177 });
4178 int ix = 0;
4179 auto it = m_model->m_allTracks.cbegin();
4180 while (it != m_model->m_allTracks.cend()) {
4181 int target_track = (*it)->getId();
4182 ++it;
4183 if (target_track == m_activeTrack) {
4184 break;
4185 }
4186 if (m_model->getTrackById_const(target_track)->isAudioTrack() || m_model->getTrackById_const(target_track)->isHidden()) {
4187 continue;
4188 }
4189 ++ix;
4190 }
4191 pCore->monitorManager()->projectMonitor()->updateMultiTrackView(ix);
4192 } else {
4193 disconnect(m_model.get(), &TimelineItemModel::trackVisibilityChanged, this, &TimelineController::updateMultiTrack);
4194 }
4195 }
4196
updateMultiTrack()4197 void TimelineController::updateMultiTrack()
4198 {
4199 QStringList trackNames = TimelineFunctions::enableMultitrackView(m_model, true, true);
4200 pCore->monitorManager()->projectMonitor()->slotShowEffectScene(MonitorSplitTrack, false, QVariant(trackNames));
4201 }
4202
activateTrackAndSelect(int trackPosition,bool notesMode)4203 void TimelineController::activateTrackAndSelect(int trackPosition, bool notesMode)
4204 {
4205 int tid = -1;
4206 int ix = 0;
4207 if (notesMode && trackPosition == -2) {
4208 m_activeTrack = -2;
4209 emit activeTrackChanged();
4210 return;
4211 }
4212 auto it = m_model->m_allTracks.cbegin();
4213 while (it != m_model->m_allTracks.cend()) {
4214 tid = (*it)->getId();
4215 ++it;
4216 if (!notesMode && (m_model->getTrackById_const(tid)->isAudioTrack() || m_model->getTrackById_const(tid)->isHidden())) {
4217 continue;
4218 }
4219 if (trackPosition == ix) {
4220 break;
4221 }
4222 ++ix;
4223 }
4224 if (tid > -1) {
4225 m_activeTrack = tid;
4226 emit activeTrackChanged();
4227 if (!notesMode && pCore->window()->getCurrentTimeline()->activeTool() != ToolType::MulticamTool) {
4228 selectCurrentItem(ObjectType::TimelineClip, true);
4229 }
4230 }
4231 }
4232
saveTimelineSelection(const QDir & targetDir)4233 void TimelineController::saveTimelineSelection(const QDir &targetDir)
4234 {
4235 TimelineFunctions::saveTimelineSelection(m_model, m_model->getCurrentSelection(), targetDir);
4236 }
4237
addEffectKeyframe(int cid,int frame,double val)4238 void TimelineController::addEffectKeyframe(int cid, int frame, double val)
4239 {
4240 if (m_model->isClip(cid)) {
4241 std::shared_ptr<EffectStackModel> destStack = m_model->getClipEffectStackModel(cid);
4242 destStack->addEffectKeyFrame(frame, val);
4243 } else if (m_model->isComposition(cid)) {
4244 std::shared_ptr<KeyframeModelList> listModel = m_model->m_allCompositions[cid]->getKeyframeModel();
4245 listModel->addKeyframe(frame, val);
4246 }
4247 }
4248
removeEffectKeyframe(int cid,int frame)4249 void TimelineController::removeEffectKeyframe(int cid, int frame)
4250 {
4251 if (m_model->isClip(cid)) {
4252 std::shared_ptr<EffectStackModel> destStack = m_model->getClipEffectStackModel(cid);
4253 destStack->removeKeyFrame(frame);
4254 } else if (m_model->isComposition(cid)) {
4255 std::shared_ptr<KeyframeModelList> listModel = m_model->m_allCompositions[cid]->getKeyframeModel();
4256 listModel->removeKeyframe(GenTime(frame, pCore->getCurrentFps()));
4257 }
4258 }
4259
updateEffectKeyframe(int cid,int oldFrame,int newFrame,const QVariant & normalizedValue)4260 void TimelineController::updateEffectKeyframe(int cid, int oldFrame, int newFrame, const QVariant &normalizedValue)
4261 {
4262 if (m_model->isClip(cid)) {
4263 std::shared_ptr<EffectStackModel> destStack = m_model->getClipEffectStackModel(cid);
4264 destStack->updateKeyFrame(oldFrame, newFrame, normalizedValue);
4265 } else if (m_model->isComposition(cid)) {
4266 std::shared_ptr<KeyframeModelList> listModel = m_model->m_allCompositions[cid]->getKeyframeModel();
4267 listModel->updateKeyframe(GenTime(oldFrame, pCore->getCurrentFps()), GenTime(newFrame, pCore->getCurrentFps()), normalizedValue);
4268 }
4269 }
4270
hasKeyframeAt(int cid,int frame)4271 bool TimelineController::hasKeyframeAt(int cid, int frame)
4272 {
4273 if (m_model->isClip(cid)) {
4274 std::shared_ptr<EffectStackModel> destStack = m_model->getClipEffectStackModel(cid);
4275 return destStack->hasKeyFrame(frame);
4276 } else if (m_model->isComposition(cid)) {
4277 std::shared_ptr<KeyframeModelList> listModel = m_model->m_allCompositions[cid]->getKeyframeModel();
4278 return listModel->hasKeyframe(frame);
4279 }
4280 return false;
4281 }
4282
darkBackground() const4283 bool TimelineController::darkBackground() const
4284 {
4285 KColorScheme scheme(QApplication::palette().currentColorGroup());
4286 return scheme.background(KColorScheme::NormalBackground).color().value() < 0.5;
4287 }
4288
videoColor() const4289 QColor TimelineController::videoColor() const
4290 {
4291 KColorScheme scheme(QApplication::palette().currentColorGroup());
4292 return scheme.foreground(KColorScheme::LinkText).color();
4293 }
4294
targetColor() const4295 QColor TimelineController::targetColor() const
4296 {
4297 KColorScheme scheme(QApplication::palette().currentColorGroup());
4298 QColor base = scheme.foreground(KColorScheme::PositiveText).color();
4299 QColor high = QApplication::palette().highlightedText().color();
4300 double factor = 0.3;
4301 QColor res = QColor(qBound(0, base.red() + int(factor*(high.red() - 128)), 255),
4302 qBound(0, base.green() + int(factor*(high.green() - 128)), 255),
4303 qBound(0, base.blue() + int(factor*(high.blue() - 128)), 255),
4304 255);
4305 return res;
4306 }
4307
targetTextColor() const4308 QColor TimelineController::targetTextColor() const
4309 {
4310 KColorScheme scheme(QApplication::palette().currentColorGroup());
4311 return scheme.background(KColorScheme::PositiveBackground).color();
4312 }
4313
audioColor() const4314 QColor TimelineController::audioColor() const
4315 {
4316 KColorScheme scheme(QApplication::palette().currentColorGroup());
4317 return scheme.foreground(KColorScheme::PositiveText).color().darker(200);
4318 }
4319
titleColor() const4320 QColor TimelineController::titleColor() const
4321 {
4322 KColorScheme scheme(QApplication::palette().currentColorGroup());
4323 QColor base = scheme.foreground(KColorScheme::LinkText).color();
4324 QColor high = scheme.foreground(KColorScheme::NegativeText).color();
4325 QColor title = QColor(qBound(0, base.red() + int(high.red() - 128), 255),
4326 qBound(0, base.green() + int(high.green() - 128), 255),
4327 qBound(0, base.blue() + int(high.blue() - 128), 255),
4328 255);
4329 return title;
4330 }
4331
imageColor() const4332 QColor TimelineController::imageColor() const
4333 {
4334 KColorScheme scheme(QApplication::palette().currentColorGroup());
4335 return scheme.foreground(KColorScheme::NeutralText).color();
4336 }
4337
thumbColor1() const4338 QColor TimelineController::thumbColor1() const
4339 {
4340 return KdenliveSettings::thumbColor1();
4341 }
4342
thumbColor2() const4343 QColor TimelineController::thumbColor2() const
4344 {
4345 return KdenliveSettings::thumbColor2();
4346 }
4347
slideshowColor() const4348 QColor TimelineController::slideshowColor() const
4349 {
4350 KColorScheme scheme(QApplication::palette().currentColorGroup());
4351 QColor base = scheme.foreground(KColorScheme::LinkText).color();
4352 QColor high = scheme.foreground(KColorScheme::NeutralText).color();
4353 QColor slide = QColor(qBound(0, base.red() + int(high.red() - 128), 255),
4354 qBound(0, base.green() + int(high.green() - 128), 255),
4355 qBound(0, base.blue() + int(high.blue() - 128), 255),
4356 255);
4357 return slide;
4358 }
4359
lockedColor() const4360 QColor TimelineController::lockedColor() const
4361 {
4362 KColorScheme scheme(QApplication::palette().currentColorGroup());
4363 return scheme.foreground(KColorScheme::NegativeText).color();
4364 }
4365
groupColor() const4366 QColor TimelineController::groupColor() const
4367 {
4368 KColorScheme scheme(QApplication::palette().currentColorGroup());
4369 return scheme.foreground(KColorScheme::ActiveText).color();
4370 }
4371
selectionColor() const4372 QColor TimelineController::selectionColor() const
4373 {
4374 KColorScheme scheme(QApplication::palette().currentColorGroup(), KColorScheme::Complementary);
4375 return scheme.foreground(KColorScheme::NeutralText).color();
4376 }
4377
switchRecording(int trackId)4378 void TimelineController::switchRecording(int trackId)
4379 {
4380 if (!pCore->isMediaCapturing()) {
4381 qDebug() << "start recording" << trackId;
4382 if (!m_model->isTrack(trackId)) {
4383 qDebug() << "ERROR: Starting to capture on invalid track " << trackId;
4384 }
4385 if (m_model->getTrackById_const(trackId)->isLocked()) {
4386 pCore->displayMessage(i18n("Impossible to capture on a locked track"), ErrorMessage, 500);
4387 return;
4388 }
4389 m_recordStart.first = pCore->getTimelinePosition();
4390 m_recordTrack = trackId;
4391 int maximumSpace = m_model->getTrackById_const(trackId)->getBlankEnd(m_recordStart.first);
4392 if (maximumSpace == INT_MAX) {
4393 m_recordStart.second = 0;
4394 } else {
4395 m_recordStart.second = maximumSpace - m_recordStart.first;
4396 if (m_recordStart.second < 8) {
4397 pCore->displayMessage(i18n("Impossible to capture here: the capture could override clips. Please remove clips after the current position or "
4398 "choose a different track"),
4399 ErrorMessage, 500);
4400 return;
4401 }
4402 }
4403 pCore->monitorManager()->slotSwitchMonitors(false);
4404 pCore->startMediaCapture(trackId, true, false);
4405 pCore->monitorManager()->slotPlay();
4406 } else {
4407 pCore->stopMediaCapture(trackId, true, false);
4408 pCore->monitorManager()->slotPause();
4409 }
4410 }
4411
urlDropped(QStringList droppedFile,int frame,int tid)4412 void TimelineController::urlDropped(QStringList droppedFile, int frame, int tid)
4413 {
4414 if (droppedFile.isEmpty()) {
4415 // Empty url passed, abort
4416 return;
4417 }
4418 m_recordTrack = tid;
4419 m_recordStart = {frame, -1};
4420 qDebug()<<"=== GOT DROPPED FILED: "<<droppedFile<<"\n======";
4421 if (droppedFile.first().endsWith(QLatin1String(".ass")) || droppedFile.first().endsWith(QLatin1String(".srt"))) {
4422 // Subtitle dropped, import
4423 pCore->window()->showSubtitleTrack();
4424 importSubtitle(droppedFile.first());
4425 } else {
4426 finishRecording(QUrl(droppedFile.first()).toLocalFile());
4427 }
4428 }
4429
finishRecording(const QString & recordedFile)4430 void TimelineController::finishRecording(const QString &recordedFile)
4431 {
4432 if (recordedFile.isEmpty()) {
4433 return;
4434 }
4435
4436 Fun undo = []() { return true; };
4437 Fun redo = []() { return true; };
4438 std::function<void(const QString &)> callBack = [this](const QString &binId) {
4439 int id = -1;
4440 if (m_recordTrack == -1) {
4441 return;
4442 }
4443 std::shared_ptr<ProjectClip> clip = pCore->bin()->getBinClip(binId);
4444 if (!clip) {
4445 return;
4446 }
4447 pCore->activeBin()->selectClipById(binId);
4448 qDebug() << "callback " << binId << " " << m_recordTrack << ", MAXIMUM SPACE: " << m_recordStart.second;
4449 if (m_recordStart.second > 0) {
4450 // Limited space on track
4451 int out = qMin(int(clip->frameDuration() - 1), m_recordStart.second - 1);
4452 QString binClipId = QString("%1/%2/%3").arg(binId).arg(0).arg(out);
4453 m_model->requestClipInsertion(binClipId, m_recordTrack, m_recordStart.first, id, true, true, false);
4454 } else {
4455 m_model->requestClipInsertion(binId, m_recordTrack, m_recordStart.first, id, true, true, false);
4456 }
4457 };
4458 QString binId =
4459 ClipCreator::createClipFromFile(recordedFile, pCore->projectItemModel()->getRootFolder()->clipId(), pCore->projectItemModel(), undo, redo, callBack);
4460 pCore->window()->raiseBin();
4461 if (binId != QStringLiteral("-1")) {
4462 pCore->pushUndo(undo, redo, i18n("Record audio"));
4463 }
4464 }
4465
4466
updateVideoTarget()4467 void TimelineController::updateVideoTarget()
4468 {
4469 if (videoTarget() > -1) {
4470 m_lastVideoTarget = videoTarget();
4471 m_videoTargetActive = true;
4472 emit lastVideoTargetChanged();
4473 } else {
4474 m_videoTargetActive = false;
4475 }
4476 }
4477
updateAudioTarget()4478 void TimelineController::updateAudioTarget()
4479 {
4480 if (!audioTarget().isEmpty()) {
4481 m_lastAudioTarget = m_model->m_audioTarget;
4482 m_audioTargetActive = true;
4483 emit lastAudioTargetChanged();
4484 } else {
4485 m_audioTargetActive = false;
4486 }
4487 }
4488
hasActiveTracks() const4489 bool TimelineController::hasActiveTracks() const
4490 {
4491 auto it = m_model->m_allTracks.cbegin();
4492 while (it != m_model->m_allTracks.cend()) {
4493 int target_track = (*it)->getId();
4494 if (m_model->getTrackById_const(target_track)->shouldReceiveTimelineOp()) {
4495 return true;
4496 }
4497 ++it;
4498 }
4499 return false;
4500 }
4501
showMasterEffects()4502 void TimelineController::showMasterEffects()
4503 {
4504 emit showItemEffectStack(i18n("Master effects"), m_model->getMasterEffectStackModel(), pCore->getCurrentFrameSize(), false);
4505 }
4506
refreshIfVisible(int cid)4507 bool TimelineController::refreshIfVisible(int cid)
4508 {
4509 auto it = m_model->m_allTracks.cbegin();
4510 while (it != m_model->m_allTracks.cend()) {
4511 int target_track = (*it)->getId();
4512 if (m_model->getTrackById_const(target_track)->isAudioTrack() || m_model->getTrackById_const(target_track)->isHidden()) {
4513 ++it;
4514 continue;
4515 }
4516 int child = m_model->getClipByPosition(target_track, pCore->getTimelinePosition());
4517 if (child > 0) {
4518 if (m_model->m_allClips[child]->binId().toInt() == cid) {
4519 return true;
4520 }
4521 }
4522 ++it;
4523 }
4524 return false;
4525 }
4526
collapseActiveTrack()4527 void TimelineController::collapseActiveTrack()
4528 {
4529 if (m_activeTrack == -1) {
4530 return;
4531 }
4532 if (m_activeTrack == -2) {
4533 // Subtitle track
4534 QMetaObject::invokeMethod(m_root, "switchSubtitleTrack", Qt::QueuedConnection);
4535 return;
4536 }
4537 int collapsed = m_model->getTrackProperty(m_activeTrack, QStringLiteral("kdenlive:collapsed")).toInt();
4538 // Default unit for timeline.qml objects size
4539 int baseUnit = qMax(28, int(QFontInfo(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)).pixelSize() * 1.8 + 0.5));
4540 m_model->setTrackProperty(m_activeTrack, QStringLiteral("kdenlive:collapsed"), collapsed > 0 ? QStringLiteral("0") : QString::number(baseUnit));
4541 }
4542
setActiveTrackProperty(const QString & name,const QString & value)4543 void TimelineController::setActiveTrackProperty(const QString &name, const QString &value)
4544 {
4545 if (m_activeTrack > -1) {
4546 m_model->setTrackProperty(m_activeTrack, name, value);
4547 }
4548 }
4549
isActiveTrackAudio() const4550 bool TimelineController::isActiveTrackAudio() const
4551 {
4552 if (m_activeTrack > -1) {
4553 if (m_model->getTrackById_const(m_activeTrack)->isAudioTrack()) {
4554 return true;
4555 }
4556 }
4557 return false;
4558 }
4559
getActiveTrackProperty(const QString & name) const4560 const QVariant TimelineController::getActiveTrackProperty(const QString &name) const
4561 {
4562 if (m_activeTrack > -1) {
4563 return m_model->getTrackProperty(m_activeTrack, name);
4564 }
4565 return QVariant();
4566 }
4567
expandActiveClip()4568 void TimelineController::expandActiveClip()
4569 {
4570 std::unordered_set<int> ids = m_model->getCurrentSelection();
4571 std::unordered_set<int> items_list;
4572 for (int i : ids) {
4573 if (m_model->isGroup(i)) {
4574 std::unordered_set<int> children = m_model->m_groups->getLeaves(i);
4575 items_list.insert(children.begin(), children.end());
4576 } else {
4577 items_list.insert(i);
4578 }
4579 }
4580 m_model->requestClearSelection();
4581 bool result = true;
4582 for (int id : items_list) {
4583 if (result && m_model->isClip(id)) {
4584 std::shared_ptr<ClipModel> clip = m_model->getClipPtr(id);
4585 if (clip->clipType() == ClipType::Playlist) {
4586 std::function<bool(void)> undo = []() { return true; };
4587 std::function<bool(void)> redo = []() { return true; };
4588 if (m_model->m_groups->isInGroup(id)) {
4589 int targetRoot = m_model->m_groups->getRootId(id);
4590 if (m_model->isGroup(targetRoot)) {
4591 m_model->requestClipUngroup(targetRoot, undo, redo);
4592 }
4593 }
4594 int pos = clip->getPosition();
4595 QDomDocument doc = TimelineFunctions::extractClip(m_model, id, getClipBinId(id));
4596 m_model->requestClipDeletion(id, undo, redo);
4597 result = TimelineFunctions::pasteClips(m_model, doc.toString(), m_activeTrack, pos, undo, redo);
4598 if (result) {
4599 pCore->pushUndo(undo, redo, i18n("Expand clip"));
4600 } else {
4601 undo();
4602 pCore->displayMessage(i18n("Could not expand clip"), ErrorMessage, 500);
4603 }
4604 }
4605 }
4606 }
4607 }
4608
getCurrentTargets(int trackId,int & activeTargetStream)4609 QMap <int, QString> TimelineController::getCurrentTargets(int trackId, int &activeTargetStream)
4610 {
4611 if (m_model->m_binAudioTargets.size() < 2) {
4612 activeTargetStream = -1;
4613 return QMap <int, QString>();
4614 }
4615 if (m_model->m_audioTarget.contains(trackId)) {
4616 activeTargetStream = m_model->m_audioTarget.value(trackId);
4617 } else {
4618 activeTargetStream = -1;
4619 }
4620 return m_model->m_binAudioTargets;
4621 }
4622
addTracks(int videoTracks,int audioTracks)4623 void TimelineController::addTracks(int videoTracks, int audioTracks)
4624 {
4625 bool result = false;
4626 int total = videoTracks + audioTracks;
4627 Fun undo = []() { return true; };
4628 Fun redo = []() { return true; };
4629 for (int ix = 0; videoTracks + audioTracks > 0; ++ix) {
4630 int newTid;
4631 if (audioTracks > 0) {
4632 result = m_model->requestTrackInsertion(0, newTid, QString(), true, undo, redo);
4633 audioTracks--;
4634 } else {
4635 result = m_model->requestTrackInsertion(-1, newTid, QString(), false, undo, redo);
4636 videoTracks--;
4637 }
4638 if (!result) {
4639 break;
4640 }
4641 }
4642 if (result) {
4643 pCore->pushUndo(undo, redo, i18np("Insert Track", "Insert Tracks", total));
4644 } else {
4645 pCore->displayMessage(i18n("Could not insert track"), ErrorMessage, 500);
4646 undo();
4647 }
4648 }
4649
mixClip(int cid,int delta)4650 void TimelineController::mixClip(int cid, int delta)
4651 {
4652 m_model->mixClip(cid, QStringLiteral("luma"), delta);
4653 }
4654
temporaryUnplug(QList<int> clipIds,bool hide)4655 void TimelineController::temporaryUnplug(QList<int> clipIds, bool hide)
4656 {
4657 for (auto &cid : clipIds) {
4658 int tid = m_model->getItemTrackId(cid);
4659 if (tid == -1) {
4660 continue;
4661 }
4662 if (hide) {
4663 m_model->getTrackById_const(tid)->temporaryUnplugClip(cid);
4664 } else {
4665 m_model->getTrackById_const(tid)->temporaryReplugClip(cid);
4666 }
4667 }
4668 }
4669
editSubtitle(int id,QString newText,QString oldText)4670 void TimelineController::editSubtitle(int id, QString newText, QString oldText)
4671 {
4672 qDebug()<<"Editing existing subtitle :"<<id;
4673 if (oldText == newText) {
4674 return;
4675 }
4676 auto subtitleModel = pCore->getSubtitleModel();
4677 Fun local_redo = [subtitleModel, id, newText]() {
4678 subtitleModel->editSubtitle(id, newText);
4679 QPair<int, int> range = subtitleModel->getInOut(id);
4680 pCore->invalidateRange(range);
4681 pCore->refreshProjectRange(range);
4682 return true;
4683 };
4684 Fun local_undo = [subtitleModel, id, oldText]() {
4685 subtitleModel->editSubtitle(id, oldText);
4686 QPair<int, int> range = subtitleModel->getInOut(id);
4687 pCore->invalidateRange(range);
4688 pCore->refreshProjectRange(range);
4689 return true;
4690 };
4691 local_redo();
4692 pCore->pushUndo(local_undo, local_redo, i18n("Edit subtitle"));
4693 }
4694
resizeSubtitle(int startFrame,int endFrame,int oldEndFrame,bool refreshModel)4695 void TimelineController::resizeSubtitle(int startFrame, int endFrame, int oldEndFrame, bool refreshModel)
4696 {
4697 qDebug()<<"Editing existing subtitle in controller at:"<<startFrame;
4698 auto subtitleModel = pCore->getSubtitleModel();
4699 int max = qMax(endFrame, oldEndFrame);
4700 Fun local_redo = [subtitleModel, startFrame, endFrame, max, refreshModel]() {
4701 subtitleModel->editEndPos(GenTime(startFrame, pCore->getCurrentFps()), GenTime(endFrame, pCore->getCurrentFps()), refreshModel);
4702 pCore->refreshProjectRange({startFrame, max});
4703 return true;
4704 };
4705 Fun local_undo = [subtitleModel, startFrame, oldEndFrame, max, refreshModel]() {
4706 subtitleModel->editEndPos(GenTime(startFrame, pCore->getCurrentFps()), GenTime(oldEndFrame, pCore->getCurrentFps()), refreshModel);
4707 pCore->refreshProjectRange({startFrame, max});
4708 return true;
4709 };
4710 local_redo();
4711 if (refreshModel) {
4712 pCore->pushUndo(local_undo, local_redo, i18n("Resize subtitle"));
4713 }
4714 }
4715
4716
addSubtitle(int startframe,QString text)4717 void TimelineController::addSubtitle(int startframe, QString text)
4718 {
4719 if (startframe == -1) {
4720 startframe = pCore->getTimelinePosition();
4721 }
4722 int endframe = startframe + pCore->getDurationFromString(KdenliveSettings::subtitle_duration());
4723 auto subtitleModel = pCore->getSubtitleModel(true);
4724 int id = TimelineModel::getNextId();
4725 if (text.isEmpty()) {
4726 text = i18n("Add text");
4727 }
4728 Fun local_undo = [subtitleModel, id, startframe, endframe]() {
4729 subtitleModel->removeSubtitle(id);
4730 QPair<int, int> range = {startframe, endframe};
4731 pCore->invalidateRange(range);
4732 pCore->refreshProjectRange(range);
4733 return true;
4734 };
4735 Fun local_redo = [subtitleModel, id, startframe, endframe, text]() {
4736 if (subtitleModel->addSubtitle(id, GenTime(startframe, pCore->getCurrentFps()), GenTime(endframe, pCore->getCurrentFps()), text)) {
4737 QPair<int, int> range = {startframe, endframe};
4738 pCore->invalidateRange(range);
4739 pCore->refreshProjectRange(range);
4740 return true;
4741 }
4742 return false;
4743 };
4744 if (local_redo()) {
4745 m_model->requestAddToSelection(id, true);
4746 pCore->pushUndo(local_undo, local_redo, i18n("Add subtitle"));
4747 int index = m_model->positionForIndex(id);
4748 if (index > -1) {
4749 QMetaObject::invokeMethod(m_root, "highlightSub", Qt::QueuedConnection, Q_ARG(QVariant, index));
4750 }
4751 }
4752 }
4753
importSubtitle(const QString path)4754 void TimelineController::importSubtitle(const QString path)
4755 {
4756 QPointer<QDialog> d = new QDialog;
4757 Ui::ImportSub_UI view;
4758 view.setupUi(d);
4759 if (!path.isEmpty()) {
4760 view.subtitle_url->setText(path);
4761 }
4762 d->setWindowTitle(i18n("Import Subtitle"));
4763 if (d->exec() == QDialog::Accepted && !view.subtitle_url->url().isEmpty()) {
4764 auto subtitleModel = pCore->getSubtitleModel(true);
4765 int offset = 0;
4766 if (view.cursor_pos->isChecked()) {
4767 offset = pCore->getTimelinePosition();
4768 }
4769 subtitleModel->importSubtitle(view.subtitle_url->url().toLocalFile(), offset, true);
4770 }
4771 emit regainFocus();
4772 }
4773
exportSubtitle()4774 void TimelineController::exportSubtitle()
4775 {
4776 auto subtitleModel = pCore->getSubtitleModel();
4777 if (subtitleModel == nullptr) {
4778 return;
4779 }
4780 QString currentSub = subtitleModel->getUrl();
4781 if (currentSub.isEmpty()) {
4782 pCore->displayMessage(i18n("No subtitles in current project"), ErrorMessage);
4783 return;
4784 }
4785 const QString url =
4786 QFileDialog::getSaveFileName(qApp->activeWindow(), i18n("Export subtitle file"), pCore->currentDoc()->url().toLocalFile(), i18n("Subtitle File (*.srt)"));
4787 if (url.isEmpty()) {
4788 return;
4789 }
4790 QFile srcFile(url);
4791 if (!url.endsWith(QStringLiteral(".srt"))) {
4792 KMessageBox::error(qApp->activeWindow(), i18n("Cannot write to file %1", url));
4793 return;
4794 }
4795 if (srcFile.exists()) {
4796 srcFile.remove();
4797 }
4798 QFile src(currentSub);
4799 if (!src.copy(srcFile.fileName())) {
4800 KMessageBox::error(qApp->activeWindow(), i18n("Cannot write to file %1", srcFile.fileName()));
4801 }
4802 }
4803
subtitleSpeechRecognition()4804 void TimelineController::subtitleSpeechRecognition()
4805 {
4806 SpeechDialog d(m_model, m_zone, false, false, qApp->activeWindow());
4807 d.exec();
4808 }
4809
deleteSubtitle(int startframe,int endframe,QString text)4810 void TimelineController::deleteSubtitle(int startframe, int endframe, QString text)
4811 {
4812 auto subtitleModel = pCore->getSubtitleModel();
4813 int id = subtitleModel->getIdForStartPos(GenTime(startframe, pCore->getCurrentFps()));
4814 Fun local_redo = [subtitleModel, id, startframe, endframe]() {
4815 subtitleModel->removeSubtitle(id);
4816 pCore->refreshProjectRange({startframe, endframe});
4817 return true;
4818 };
4819 Fun local_undo = [subtitleModel, id, startframe, endframe, text]() {
4820 subtitleModel->addSubtitle(id, GenTime(startframe, pCore->getCurrentFps()), GenTime(endframe, pCore->getCurrentFps()), text);
4821 pCore->refreshProjectRange({startframe, endframe});
4822 return true;
4823 };
4824 local_redo();
4825 pCore->pushUndo(local_undo, local_redo, i18n("Delete subtitle"));
4826 }
4827
switchSubtitleDisable()4828 void TimelineController::switchSubtitleDisable()
4829 {
4830 auto subtitleModel = pCore->getSubtitleModel();
4831 if (subtitleModel) {
4832 bool disabled = subtitleModel->isDisabled();
4833 Fun local_switch = [this, subtitleModel]() {
4834 subtitleModel->switchDisabled();
4835 emit subtitlesDisabledChanged();
4836 pCore->requestMonitorRefresh();
4837 return true;
4838 };
4839 local_switch();
4840 pCore->pushUndo(local_switch, local_switch, disabled ? i18n("Show subtitle track") : i18n("Hide subtitle track"));
4841 }
4842 }
4843
subtitlesDisabled() const4844 bool TimelineController::subtitlesDisabled() const
4845 {
4846 auto subtitleModel = pCore->getSubtitleModel();
4847 if (subtitleModel) {
4848 return subtitleModel->isDisabled();
4849 }
4850 return false;
4851 }
4852
switchSubtitleLock()4853 void TimelineController::switchSubtitleLock()
4854 {
4855 auto subtitleModel = pCore->getSubtitleModel();
4856 if (subtitleModel) {
4857 bool locked = subtitleModel->isLocked();
4858 Fun local_switch = [this, subtitleModel]() {
4859 subtitleModel->switchLocked();
4860 emit subtitlesLockedChanged();
4861 return true;
4862 };
4863 local_switch();
4864 pCore->pushUndo(local_switch, local_switch, locked ? i18n("Unlock subtitle track") : i18n("Lock subtitle track"));
4865 }
4866 }
subtitlesLocked() const4867 bool TimelineController::subtitlesLocked() const
4868 {
4869 auto subtitleModel = pCore->getSubtitleModel();
4870 if (subtitleModel) {
4871 return subtitleModel->isLocked();
4872 }
4873 return false;
4874 }
4875
guidesLocked() const4876 bool TimelineController::guidesLocked() const
4877 {
4878 return KdenliveSettings::lockedGuides();
4879 }
4880
showToolTip(const QString & info) const4881 void TimelineController::showToolTip(const QString &info) const
4882 {
4883 pCore->displayMessage(info, TooltipMessage);
4884 }
4885
showKeyBinding(const QString & info) const4886 void TimelineController::showKeyBinding(const QString &info) const
4887 {
4888 pCore->window()->showKeyBinding(info);
4889 }
4890
4891
showTimelineToolInfo(bool show) const4892 void TimelineController::showTimelineToolInfo(bool show) const
4893 {
4894 if (show) {
4895 pCore->window()->showToolMessage();
4896 } else {
4897 pCore->window()->setWidgetKeyBinding();
4898 }
4899 }
4900
showRulerEffectZone(QPair<int,int> inOut,bool checked)4901 void TimelineController::showRulerEffectZone(QPair <int, int>inOut, bool checked)
4902 {
4903 m_effectZone = checked ? QPoint(inOut.first, inOut.second) : QPoint();
4904 emit effectZoneChanged();
4905 }
4906
updateMasterZones(QVariantList zones)4907 void TimelineController::updateMasterZones(QVariantList zones)
4908 {
4909 m_masterEffectZones = zones;
4910 emit masterZonesChanged();
4911 }
4912
clipMaxDuration(int cid)4913 int TimelineController::clipMaxDuration(int cid)
4914 {
4915 if (!m_model->isClip(cid)) {
4916 return -1;
4917 }
4918 return m_model->m_allClips[cid]->getMaxDuration();
4919 }
4920
resizeMix(int cid,int duration,MixAlignment align,int rightFrames)4921 void TimelineController::resizeMix(int cid, int duration, MixAlignment align, int rightFrames)
4922 {
4923 if (cid > -1) {
4924 m_model->requestResizeMix(cid, duration, align, rightFrames);
4925 }
4926 }
4927
getMixCutPos(int cid) const4928 int TimelineController::getMixCutPos(int cid) const
4929 {
4930 return m_model->getMixCutPos(cid);
4931 }
4932
getMixAlign(int cid) const4933 MixAlignment TimelineController::getMixAlign(int cid) const
4934 {
4935 return m_model->getMixAlign(cid);
4936 }
4937
processMultitrackOperation(int tid,int in)4938 void TimelineController::processMultitrackOperation(int tid, int in)
4939 {
4940 int out = pCore->getTimelinePosition();
4941 if (out == in) {
4942 // Simply change the reference track, nothing to do here
4943 return;
4944 }
4945 QVector<int> tracks;
4946 auto it = m_model->m_allTracks.cbegin();
4947 // Lift all tracks except tid
4948 while (it != m_model->m_allTracks.cend()) {
4949 int target_track = (*it)->getId();
4950 if (target_track != tid && !(*it)->isAudioTrack() && m_model->getTrackById_const(target_track)->shouldReceiveTimelineOp()) {
4951 tracks << target_track;
4952 }
4953 ++it;
4954 }
4955 if (tracks.isEmpty()) {
4956 pCore->displayMessage(i18n("Please activate a track for this operation by clicking on its label"), ErrorMessage);
4957 }
4958 TimelineFunctions::extractZone(m_model, tracks, QPoint(in, out), true);
4959 }
4960
setMulticamIn(int pos)4961 void TimelineController::setMulticamIn(int pos)
4962 {
4963 if (multicamIn != -1) {
4964 // remove previous snap
4965 m_model->removeSnap(multicamIn);
4966 }
4967 multicamIn = pos;
4968 m_model->addSnap(multicamIn);
4969 emit multicamInChanged();
4970 }
4971