timelinecontroller.cpp 188 KB
Newer Older
1
/*
2
    SPDX-FileCopyrightText: 2017 Jean-Baptiste Mardelle
Camille Moulin's avatar
Camille Moulin committed
3
    SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
4
*/
5
6

#include "timelinecontroller.h"
Nicolas Carion's avatar
Nicolas Carion committed
7
#include "../model/timelinefunctions.hpp"
8
#include "assets/keyframes/model/keyframemodellist.hpp"
9
#include "audiomixer/mixermanager.hpp"
10
#include "bin/bin.h"
11
#include "bin/clipcreator.hpp"
Nicolas Carion's avatar
linting    
Nicolas Carion committed
12
#include "bin/model/markerlistmodel.hpp"
13
#include "bin/model/subtitlemodel.hpp"
14
#include "bin/projectclip.h"
15
#include "bin/projectfolder.h"
Nicolas Carion's avatar
Nicolas Carion committed
16
#include "bin/projectitemmodel.h"
Nicolas Carion's avatar
linting    
Nicolas Carion committed
17
#include "core.h"
Nicolas Carion's avatar
Nicolas Carion committed
18
#include "dialogs/spacerdialog.h"
19
#include "dialogs/speechdialog.h"
20
21
#include "dialogs/speeddialog.h"
#include "dialogs/timeremap.h"
22
#include "doc/kdenlivedoc.h"
23
#include "effects/effectsrepository.hpp"
Nicolas Carion's avatar
Nicolas Carion committed
24
#include "effects/effectstack/model/effectstackmodel.hpp"
Nicolas Carion's avatar
linting    
Nicolas Carion committed
25
#include "kdenlivesettings.h"
26
#include "lib/audio/audioEnvelope.h"
27
28
#include "mainwindow.h"
#include "monitor/monitormanager.h"
Nicolas Carion's avatar
Nicolas Carion committed
29
#include "previewmanager.h"
Nicolas Carion's avatar
linting    
Nicolas Carion committed
30
#include "project/projectmanager.h"
31
#include "timeline2/model/clipmodel.hpp"
32
#include "timeline2/model/compositionmodel.hpp"
33
#include "timeline2/model/groupsmodel.hpp"
Nicolas Carion's avatar
Nicolas Carion committed
34
#include "timeline2/model/trackmodel.hpp"
35
#include "timeline2/view/dialogs/clipdurationdialog.h"
Nicolas Carion's avatar
Nicolas Carion committed
36
#include "timeline2/view/dialogs/trackdialog.h"
37
#include "timeline2/view/timelinewidget.h"
Nicolas Carion's avatar
Nicolas Carion committed
38
#include "transitions/transitionsrepository.hpp"
39
#include "ui_import_subtitle_ui.h"
40

41
#include <KColorScheme>
42
#include <KMessageBox>
43
#include <KRecentDirs>
44
#include <KUrlRequesterDialog>
Nicolas Carion's avatar
Nicolas Carion committed
45
#include <QClipboard>
46
#include <QFontDatabase>
Nicolas Carion's avatar
Nicolas Carion committed
47
#include <QQuickItem>
48
49
#include <QtMath>

Nicolas Carion's avatar
Nicolas Carion committed
50
#include <memory>
51
#include <unistd.h>
52
53
54

int TimelineController::m_duration = 0;

55
TimelineController::TimelineController(QObject *parent)
Nicolas Carion's avatar
linting    
Nicolas Carion committed
56
    : QObject(parent)
57
    , multicamIn(-1)
58
    , m_root(nullptr)
59
    , m_usePreview(false)
60
    , m_audioRef(-1)
Vincent Pinon's avatar
Vincent Pinon committed
61
    , m_zone(-1, -1)
62
    , m_activeTrack(-1)
63
    , m_scale(QFontMetrics(QApplication::font()).maxWidth() / 250)
64
    , m_timelinePreview(nullptr)
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
65
    , m_ready(false)
66
    , m_snapStackIndex(-1)
67
    , m_effectZone({0, 0})
68
{
69
70
71
    m_disablePreview = pCore->currentDoc()->getAction(QStringLiteral("disable_preview"));
    connect(m_disablePreview, &QAction::triggered, this, &TimelineController::disablePreview);
    m_disablePreview->setEnabled(false);
72
    connect(pCore.get(), &Core::finalizeRecording, this, &TimelineController::finishRecording);
73
    connect(pCore.get(), &Core::autoScrollChanged, this, &TimelineController::autoScrollChanged);
74
    connect(pCore.get(), &Core::recordAudio, this, &TimelineController::switchRecording);
75
76
}

77
TimelineController::~TimelineController() {}
78
79
80

void TimelineController::prepareClose()
{
81
    // Clear root so we don't call its methods anymore
82
    QObject::disconnect(m_deleteConnection);
83
    disconnect(this, &TimelineController::selectionChanged, this, &TimelineController::updateClipActions);
84
    disconnect(m_model.get(), &TimelineModel::selectionChanged, this, &TimelineController::selectionChanged);
85
86
    disconnect(this, &TimelineController::videoTargetChanged, this, &TimelineController::updateVideoTarget);
    disconnect(this, &TimelineController::audioTargetChanged, this, &TimelineController::updateAudioTarget);
87
88
    disconnect(m_model.get(), &TimelineModel::selectedMixChanged, this, &TimelineController::showMixModel);
    disconnect(m_model.get(), &TimelineModel::selectedMixChanged, this, &TimelineController::selectedMixChanged);
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
89
90
    m_ready = false;
    m_root = nullptr;
91
    // Delete timeline preview before resetting model so that removing clips from timeline doesn't invalidate
92
93
    delete m_timelinePreview;
    m_timelinePreview = nullptr;
94
    m_model.reset();
95
96
}

Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
97
void TimelineController::setModel(std::shared_ptr<TimelineItemModel> model)
98
{
99
    delete m_timelinePreview;
100
    m_zone = QPoint(-1, -1);
101
102
103
    m_hasAudioTarget = 0;
    m_lastVideoTarget = -1;
    m_lastAudioTarget.clear();
104
    m_timelinePreview = nullptr;
105
    m_usePreview = false;
106
    m_model = std::move(model);
107
    m_activeSnaps.clear();
108
    connect(m_model.get(), &TimelineItemModel::requestClearAssetView, pCore.get(), &Core::clearAssetPanel);
109
    m_deleteConnection = connect(m_model.get(), &TimelineItemModel::checkItemDeletion, this, [this](int id) {
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
110
        if (m_ready) {
111
112
113
            QMetaObject::invokeMethod(m_root, "checkDeletion", Qt::QueuedConnection, Q_ARG(QVariant, id));
        }
    });
Julius Künzel's avatar
Julius Künzel committed
114
115
116
117
118
119
120
    connect(m_model.get(), &TimelineItemModel::showTrackEffectStack, this, [&](int tid) {
        if (tid > -1) {
            showTrackAsset(tid);
        } else {
            showMasterEffects();
        }
    });
121
    connect(this, &TimelineController::selectionChanged, this, &TimelineController::updateClipActions);
122
    connect(this, &TimelineController::selectionChanged, this, &TimelineController::updateTrimmingMode);
123
124
    connect(this, &TimelineController::videoTargetChanged, this, &TimelineController::updateVideoTarget);
    connect(this, &TimelineController::audioTargetChanged, this, &TimelineController::updateAudioTarget);
125
    connect(m_model.get(), &TimelineItemModel::requestMonitorRefresh, [&]() { pCore->refreshProjectMonitorOnce(); });
126
    connect(m_model.get(), &TimelineModel::durationUpdated, this, &TimelineController::checkDuration);
127
    connect(m_model.get(), &TimelineModel::selectionChanged, this, &TimelineController::selectionChanged);
128
129
    connect(m_model.get(), &TimelineModel::selectedMixChanged, this, &TimelineController::showMixModel);
    connect(m_model.get(), &TimelineModel::selectedMixChanged, this, &TimelineController::selectedMixChanged);
130
    connect(m_model.get(), &TimelineModel::dataChanged, this, &TimelineController::checkClipPosition);
131
    connect(m_model.get(), &TimelineModel::checkTrackDeletion, this, &TimelineController::checkTrackDeletion, Qt::DirectConnection);
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
132
133
}

134
135
136
137
138
void TimelineController::restoreTargetTracks()
{
    setTargetTracks(m_hasVideoTarget, m_model->m_binAudioTargets);
}

139
void TimelineController::setTargetTracks(bool hasVideo, const QMap<int, QString> &audioTargets)
140
141
{
    int videoTrack = -1;
142
    m_model->m_binAudioTargets = audioTargets;
143
    QMap<int, int> audioTracks;
144
    m_hasVideoTarget = hasVideo;
145
    m_hasAudioTarget = audioTargets.size();
146
147
148
    if (m_hasVideoTarget) {
        videoTrack = m_model->getFirstVideoTrackIndex();
    }
149
    if (m_hasAudioTarget > 0) {
150
151
152
        if (m_lastAudioTarget.count() == audioTargets.count()) {
            // Use existing track targets
            QList<int> audioStreams = audioTargets.keys();
153
            QMapIterator<int, int> st(m_lastAudioTarget);
154
155
156
            while (st.hasNext()) {
                st.next();
                audioTracks.insert(st.key(), audioStreams.takeLast());
157
            }
158
159
        } else {
            // Use audio tracks from the first
160
            QVector<int> tracks;
161
162
163
164
165
166
167
168
169
170
            auto it = m_model->m_allTracks.cbegin();
            while (it != m_model->m_allTracks.cend()) {
                if ((*it)->isAudioTrack()) {
                    tracks << (*it)->getId();
                }
                ++it;
            }
            if (KdenliveSettings::multistream_checktrack() && audioTargets.count() > tracks.count()) {
                pCore->bin()->checkProjectAudioTracks(QString(), audioTargets.count());
            }
171
            QMapIterator<int, QString> st(audioTargets);
172
173
            while (st.hasNext()) {
                st.next();
174
                if (tracks.isEmpty()) {
175
176
177
                    break;
                }
                audioTracks.insert(tracks.takeLast(), st.key());
178
            }
179
180
        }
    }
181
182
    emit hasAudioTargetChanged();
    emit hasVideoTargetChanged();
183
184
    setVideoTarget(m_hasVideoTarget && (m_lastVideoTarget > -1) ? m_lastVideoTarget : videoTrack);
    setAudioTarget(audioTracks);
185
186
}

187
188
189
190
191
std::shared_ptr<TimelineItemModel> TimelineController::getModel() const
{
    return m_model;
}

Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
192
193
void TimelineController::setRoot(QQuickItem *root)
{
194
    m_root = root;
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
195
    m_ready = true;
196
197
198
199
200
201
202
}

Mlt::Tractor *TimelineController::tractor()
{
    return m_model->tractor();
}

203
204
205
206
207
Mlt::Producer TimelineController::trackProducer(int tid)
{
    return *(m_model->getTrackById(tid).get());
}

208
209
210
211
212
double TimelineController::scaleFactor() const
{
    return m_scale;
}

213
const QString TimelineController::getTrackNameFromMltIndex(int trackPos)
214
{
215
    if (trackPos == -1) {
216
217
        return i18n("unknown");
    }
218
    if (trackPos == 0) {
219
220
        return i18n("Black");
    }
221
    return m_model->getTrackTagById(m_model->getTrackIndexFromPosition(trackPos - 1));
222
223
}

224
225
const QString TimelineController::getTrackNameFromIndex(int trackIndex)
{
226
    QString trackName = m_model->getTrackFullName(trackIndex);
227
    return trackName.isEmpty() ? m_model->getTrackTagById(trackIndex) : trackName;
228
229
}

230
231
232
233
QMap<int, QString> TimelineController::getTrackNames(bool videoOnly)
{
    QMap<int, QString> names;
    for (const auto &track : m_model->m_iteratorTable) {
234
        if (videoOnly && m_model->getTrackById_const(track.first)->isAudioTrack()) {
235
236
            continue;
        }
237
        QString trackName = m_model->getTrackFullName(track.first);
238
        names[m_model->getTrackMltIndex(track.first)] = trackName;
239
240
    }
    return names;
241
242
}

Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
243
244
void TimelineController::setScaleFactorOnMouse(double scale, bool zoomOnMouse)
{
245
    if (m_root) {
246
        m_root->setProperty("zoomOnMouse", zoomOnMouse ? qBound(0, getMousePos(), duration()) : -1);
247
248
249
        m_scale = scale;
        emit scaleFactorChanged();
    } else {
250
        qWarning() << "Timeline root not created, impossible to zoom in";
251
    }
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
252
253
}

254
255
256
void TimelineController::setScaleFactor(double scale)
{
    m_scale = scale;
257
258
259
    // Update mainwindow's zoom slider
    emit updateZoom(scale);
    // inform qml
260
261
262
263
264
265
266
267
    emit scaleFactorChanged();
}

int TimelineController::duration() const
{
    return m_duration;
}

268
269
270
271
272
int TimelineController::fullDuration() const
{
    return m_duration + TimelineModel::seekDuration;
}

273
274
275
276
277
void TimelineController::checkDuration()
{
    int currentLength = m_model->duration();
    if (currentLength != m_duration) {
        m_duration = currentLength;
278
        emit durationChanged(m_duration);
279
280
281
    }
}

282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
void TimelineController::hideTrack(int trackId, bool hide)
{
    bool isAudio = m_model->isAudioTrack(trackId);
    QString state = hide ? (isAudio ? "1" : "2") : "3";
    QString previousState = m_model->getTrackProperty(trackId, QStringLiteral("hide")).toString();
    Fun undo_lambda = [this, trackId, previousState]() {
        m_model->setTrackProperty(trackId, QStringLiteral("hide"), previousState);
        checkDuration();
        return true;
    };
    Fun redo_lambda = [this, trackId, state]() {
        m_model->setTrackProperty(trackId, QStringLiteral("hide"), state);
        checkDuration();
        return true;
    };
    redo_lambda();
    pCore->pushUndo(undo_lambda, redo_lambda, state == QLatin1String("3") ? i18n("Hide Track") : i18n("Enable Track"));
}

301
int TimelineController::selectedTrack() const
302
{
303
304
305
306
307
    std::unordered_set<int> sel = m_model->getCurrentSelection();
    if (sel.empty()) return -1;
    std::vector<std::pair<int, int>> selected_tracks; // contains pairs of (track position, track id) for each selected item
    for (int s : sel) {
        int tid = m_model->getItemTrackId(s);
308
        selected_tracks.emplace_back(m_model->getTrackPosition(tid), tid);
309
    }
310
311
312
    // sort by track position
    std::sort(selected_tracks.begin(), selected_tracks.begin(), [](const auto &a, const auto &b) { return a.first < b.first; });
    return selected_tracks.front().second;
313
314
}

315
bool TimelineController::selectCurrentItem(ObjectType type, bool select, bool addToCurrent, bool showErrorMsg)
316
{
317
318
    int currentClip = -1;
    if (type == ObjectType::TimelineClip) {
319
320
        currentClip = m_model->isSubtitleTrack(m_activeTrack) ? m_model->getSubtitleByPosition(pCore->getTimelinePosition())
                                                              : m_model->getClipByPosition(m_activeTrack, pCore->getTimelinePosition());
321
    } else if (type == ObjectType::TimelineComposition) {
322
        currentClip = m_model->getCompositionByPosition(m_activeTrack, pCore->getTimelinePosition());
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
    } else if (type == ObjectType::TimelineMix) {
        if (m_activeTrack >= 0) {
            currentClip = m_model->getClipByPosition(m_activeTrack, pCore->getTimelinePosition());
        }
        if (currentClip > -1) {
            if (m_model->hasClipEndMix(currentClip)) {
                int mixPartner = m_model->getTrackById_const(m_activeTrack)->getSecondMixPartner(currentClip);
                int clipEnd = m_model->getClipPosition(currentClip) + m_model->getClipPlaytime(currentClip);
                int mixStart = clipEnd - m_model->getMixDuration(mixPartner);
                if (mixStart < pCore->getTimelinePosition() && pCore->getTimelinePosition() < clipEnd) {
                    if (select) {
                        m_model->requestMixSelection(mixPartner);
                        return true;
                    } else if (selectedMix() == mixPartner) {
                        m_model->requestClearSelection();
                        return true;
                    }
                }
            }
            int delta = pCore->getTimelinePosition() - m_model->getClipPosition(currentClip);
            if (m_model->getMixDuration(currentClip) >= delta) {
                if (select) {
                    m_model->requestMixSelection(currentClip);
                    return true;
                } else if (selectedMix() == currentClip) {
                    m_model->requestClearSelection();
                    return true;
                }
                return true;
            } else {
                currentClip = -1;
            }
355
        }
356
357
    }

358
    if (currentClip == -1) {
359
360
361
362
        if (showErrorMsg) {
            pCore->displayMessage(i18n("No item under timeline cursor in active track"), ErrorMessage, 500);
        }
        return false;
363
    }
364
365
366
    if (!select) {
        m_model->requestRemoveFromSelection(currentClip);
    } else {
367
        bool grouped = m_model->m_groups->isInGroup(currentClip);
368
        m_model->requestAddToSelection(currentClip, !addToCurrent);
369
370
        if (grouped) {
            // If part of a group, ensure the effect/composition stack displays the selected item's properties
371
            showAsset(currentClip);
372
        }
373
    }
374
    return true;
375
376
377
378
}

QList<int> TimelineController::selection() const
{
379
    if (!m_root) return QList<int>();
380
    std::unordered_set<int> sel = m_model->getCurrentSelection();
381
    QList<int> items;
382
    for (int id : sel) {
383
384
385
        items << id;
    }
    return items;
386
387
}

388
389
390
391
392
int TimelineController::selectedMix() const
{
    return m_model->m_selectedMix;
}

393
394
395
396
397
398
void TimelineController::selectItems(const QList<int> &ids)
{
    std::unordered_set<int> ids_s(ids.begin(), ids.end());
    m_model->requestSetSelection(ids_s);
}

399
400
401
402
403
404
405
void TimelineController::setScrollPos(int pos)
{
    if (pos > 0 && m_root) {
        QMetaObject::invokeMethod(m_root, "setScrollPos", Qt::QueuedConnection, Q_ARG(QVariant, pos));
    }
}

406
407
408
409
410
411
void TimelineController::resetView()
{
    m_model->_resetView();
    if (m_root) {
        QMetaObject::invokeMethod(m_root, "updatePalette");
    }
412
    emit colorsChanged();
413
414
}

415
416
bool TimelineController::snap()
{
417
418
419
    return KdenliveSettings::snaptopoints();
}

420
421
422
423
424
425
426
427
428
429
bool TimelineController::ripple()
{
    return false;
}

bool TimelineController::scrub()
{
    return false;
}

430
int TimelineController::insertClip(int tid, int position, const QString &data_str, bool logUndo, bool refreshView, bool useTargets)
431
432
{
    int id;
433
    if (tid == -1) {
434
        tid = m_activeTrack;
435
436
    }
    if (position == -1) {
437
        position = pCore->getTimelinePosition();
438
    }
439
    if (!m_model->requestClipInsertion(data_str, tid, position, id, logUndo, refreshView, useTargets)) {
440
441
442
443
444
        id = -1;
    }
    return id;
}

445
446
447
448
449
450
451
QList<int> TimelineController::insertClips(int tid, int position, const QStringList &binIds, bool logUndo, bool refreshView)
{
    QList<int> clipIds;
    if (tid == -1) {
        tid = m_activeTrack;
    }
    if (position == -1) {
452
        position = pCore->getTimelinePosition();
453
454
455
456
457
458
    }
    TimelineFunctions::requestMultipleClipsInsertion(m_model, binIds, tid, position, clipIds, logUndo, refreshView);
    // 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.
    return clipIds;
}

459
void TimelineController::insertNewMix(int tid, int position, const QString &transitionId)
460
461
462
463
464
465
466
{
    int clipId = m_model->getTrackById_const(tid)->getClipByPosition(position);
    if (clipId > 0) {
        m_model->mixClip(clipId, transitionId);
    }
}

467
int TimelineController::insertNewCompositionAtPos(int tid, int position, const QString &transitionId)
468
{
469
    // TODO: adjust position and duration to existing clips ?
470
    return insertComposition(tid, position, transitionId, true);
471
472
473
}

int TimelineController::insertNewComposition(int tid, int clipId, int offset, const QString &transitionId, bool logUndo)
474
475
{
    int id;
476
    int minimumPos = clipId > -1 ? m_model->getClipPosition(clipId) : offset;
477
    int clip_duration = clipId > -1 ? m_model->getClipPlaytime(clipId) : pCore->getDurationFromString(KdenliveSettings::transition_duration());
478
479
    int endPos = minimumPos + clip_duration;
    int position = minimumPos;
480
    int duration = qMin(clip_duration, pCore->getDurationFromString(KdenliveSettings::transition_duration()));
481
    int lowerVideoTrackId = m_model->getPreviousVideoTrackIndex(tid);
482
    bool revert = offset > clip_duration / 2;
483
    int bottomId = 0;
484
    if (lowerVideoTrackId > 0) {
485
486
487
488
489
490
        bottomId = m_model->getTrackById_const(lowerVideoTrackId)->getClipByPosition(position + offset);
    }
    if (bottomId <= 0) {
        // No video track underneath
        if (offset < duration && duration < 2 * clip_duration) {
            // Composition dropped close to start, keep default composition duration
491
        } else if (clip_duration - offset < duration * 1.2 && duration < 2 * clip_duration) {
492
493
494
495
496
497
498
499
500
501
502
503
504
            // Composition dropped close to end, keep default composition duration
            position = endPos - duration;
        } else {
            // Use full clip length for duration
            duration = m_model->getTrackById_const(tid)->suggestCompositionLength(position);
        }
    } else {
        duration = qMin(duration, m_model->getTrackById_const(tid)->suggestCompositionLength(position));
        QPair<int, int> bottom(m_model->m_allClips[bottomId]->getPosition(), m_model->m_allClips[bottomId]->getPlaytime());
        if (bottom.first > minimumPos) {
            // Lower clip is after top clip
            if (position + offset > bottom.first) {
                int test_duration = m_model->getTrackById_const(tid)->suggestCompositionLength(bottom.first);
505
                if (test_duration > 0) {
506
507
508
509
                    offset -= (bottom.first - position);
                    position = bottom.first;
                    duration = test_duration;
                    revert = position > minimumPos;
510
511
                }
            }
512
513
514
515
516
517
        } else if (position >= bottom.first) {
            // Lower clip is before or at same pos as top clip
            int test_duration = m_model->getTrackById_const(lowerVideoTrackId)->suggestCompositionLength(position);
            if (test_duration > 0) {
                duration = qMin(test_duration, clip_duration);
            }
518
519
        }
    }
520
    int defaultLength = pCore->getDurationFromString(KdenliveSettings::transition_duration());
521
    bool isShortComposition = TransitionsRepository::get()->getType(transitionId) == AssetListType::AssetType::VideoShortComposition;
522
523
    if (duration < 0 || (isShortComposition && duration > 1.5 * defaultLength)) {
        duration = defaultLength;
524
    } else if (duration <= 1) {
525
        // if suggested composition duration is lower than 4 frames, use default
526
        duration = pCore->getDurationFromString(KdenliveSettings::transition_duration());
527
528
        if (minimumPos + clip_duration - position < 3) {
            position = minimumPos + clip_duration - duration;
529
        }
530
    }
531
532
533
534
    QPair<int, int> finalPos = m_model->getTrackById_const(tid)->validateCompositionLength(position, offset, duration, endPos);
    position = finalPos.first;
    duration = finalPos.second;

535
    std::unique_ptr<Mlt::Properties> props(nullptr);
536
    if (revert) {
Nicolas Carion's avatar
Nicolas Carion committed
537
        props = std::make_unique<Mlt::Properties>();
538
539
        if (transitionId == QLatin1String("dissolve")) {
            props->set("reverse", 1);
540
        } else if (transitionId == QLatin1String("composite")) {
541
542
            props->set("invert", 1);
        } else if (transitionId == QLatin1String("wipe")) {
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
543
            props->set("geometry", "0=0% 0% 100% 100% 100%;-1=0% 0% 100% 100% 0%");
544
545
        } else if (transitionId == QLatin1String("slide")) {
            props->set("rect", "0=0% 0% 100% 100% 100%;-1=100% 0% 100% 100% 100%");
546
547
        }
    }
548
    if (!m_model->requestCompositionInsertion(transitionId, tid, position, duration, std::move(props), id, logUndo)) {
549
        id = -1;
550
        pCore->displayMessage(i18n("Could not add composition at selected position"), ErrorMessage, 500);
551
552
553
554
    }
    return id;
}

555
556
557
558
559
560
561
int TimelineController::isOnCut(int cid) const
{
    Q_ASSERT(m_model->isComposition(cid));
    int tid = m_model->getItemTrackId(cid);
    return m_model->getTrackById_const(tid)->isOnCut(cid);
}

562
563
564
int TimelineController::insertComposition(int tid, int position, const QString &transitionId, bool logUndo)
{
    int id;
565
    int duration = pCore->getDurationFromString(KdenliveSettings::transition_duration());
566
567
568
569
    // Check if composition should be reversed (top clip at beginning, bottom at end)
    int a_track = m_model->getPreviousVideoTrackPos(tid);
    int topClip = m_model->getTrackById_const(tid)->getClipByPosition(position);
    int bottomClip = -1;
570
571
572
573
574
575
    if (a_track > 0) {
        // There is a video track below, check its clip
        int bottomTid = m_model->getTrackIndexFromPosition(a_track - 1);
        if (bottomTid > -1) {
            bottomClip = m_model->getTrackById_const(bottomTid)->getClipByPosition(position);
        }
576
577
578
    }
    bool reverse = false;
    if (topClip > -1 && bottomClip > -1) {
579
580
        if (m_model->getClipPosition(topClip) + m_model->getClipPlaytime(topClip) <
            m_model->getClipPosition(bottomClip) + m_model->getClipPlaytime(bottomClip)) {
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
            reverse = true;
        }
    }
    std::unique_ptr<Mlt::Properties> props(nullptr);
    if (reverse) {
        props = std::make_unique<Mlt::Properties>();
        if (transitionId == QLatin1String("dissolve")) {
            props->set("reverse", 1);
        } else if (transitionId == QLatin1String("composite")) {
            props->set("invert", 1);
        } else if (transitionId == QLatin1String("wipe")) {
            props->set("geometry", "0=0% 0% 100% 100% 100%;-1=0% 0% 100% 100% 0%");
        } else if (transitionId == QLatin1String("slide")) {
            props->set("rect", "0=0% 0% 100% 100% 100%;-1=100% 0% 100% 100% 100%");
        }
    }
    if (!m_model->requestCompositionInsertion(transitionId, tid, position, duration, std::move(props), id, logUndo)) {
598
599
600
601
602
603
604
        id = -1;
    }
    return id;
}

void TimelineController::deleteSelectedClips()
{
605
606
607
608
609
    if (dragOperationRunning()) {
        // Don't allow timeline operation while drag in progress
        pCore->displayMessage(i18n("Cannot perform operation while dragging in timeline"), ErrorMessage);
        return;
    }
610
611
    auto sel = m_model->getCurrentSelection();
    if (sel.empty()) {
612
613
614
        // Check if a mix is selected
        if (m_model->m_selectedMix > -1 && m_model->isClip(m_model->m_selectedMix)) {
            m_model->removeMix(m_model->m_selectedMix);
615
            m_model->requestClearAssetView(m_model->m_selectedMix);
616
            m_model->requestClearSelection(true);
617
        }
618
        return;
619
    }
620
    // only need to delete the first item, the others will be deleted in cascade
621
622
623
    if (m_model->m_editMode == TimelineMode::InsertEdit) {
        // In insert mode, perform an extract operation (don't leave gaps)
        extract(*sel.begin());
624
    } else {
625
626
        m_model->requestItemDeletion(*sel.begin());
    }
627
628
}

629
int TimelineController::getMainSelectedItem(bool restrictToCurrentPos, bool allowComposition)
630
631
632
633
634
635
636
637
638
639
640
641
{
    auto sel = m_model->getCurrentSelection();
    if (sel.empty() || sel.size() > 2) {
        return -1;
    }
    int itemId = *(sel.begin());
    if (sel.size() == 2) {
        int parentGroup = m_model->m_groups->getRootId(itemId);
        if (parentGroup == -1 || m_model->m_groups->getType(parentGroup) != GroupType::AVSplit) {
            return -1;
        }
    }
642
643
644
645
646
    if (!restrictToCurrentPos) {
        if (m_model->isClip(itemId) || (allowComposition && m_model->isComposition(itemId))) {
            return itemId;
        }
    }
647
    if (m_model->isClip(itemId)) {
648
        int position = pCore->getTimelinePosition();
649
650
651
652
653
654
655
656
657
        int start = m_model->getClipPosition(itemId);
        int end = start + m_model->getClipPlaytime(itemId);
        if (position >= start && position <= end) {
            return itemId;
        }
    }
    return -1;
}

658
659
void TimelineController::copyItem()
{
660
661
    std::unordered_set<int> selectedIds = m_model->getCurrentSelection();
    if (selectedIds.empty()) {
662
663
        return;
    }
664
665
666
667
    int clipId = *(selectedIds.begin());
    QString copyString = TimelineFunctions::copyClips(m_model, selectedIds);
    QClipboard *clipboard = QApplication::clipboard();
    clipboard->setText(copyString);
668
669
670
    m_root->setProperty("copiedClip", clipId);
}

671
bool TimelineController::pasteItem(int position, int tid)
672
{
673
674
    QClipboard *clipboard = QApplication::clipboard();
    QString txt = clipboard->text();
675
    if (tid == -1) {
676
        tid = m_activeTrack;
677
678
    }
    if (position == -1) {
679
        position = getMenuOrTimelinePos();
680
    }
681
    return TimelineFunctions::pasteClips(m_model, txt, tid, position);
682
683
}

684
685
void TimelineController::triggerAction(const QString &name)
{
686
    pCore->triggerAction(name);
687
688
}

689
690
691
692
693
const QString TimelineController::actionText(const QString &name)
{
    return pCore->actionText(name);
}

694
QString TimelineController::timecode(int frames) const
695
696
697
{
    return KdenliveSettings::frametimecode() ? QString::number(frames) : m_model->tractor()->frames_to_time(frames, mlt_time_smpte_df);
}
698

699
700
701
702
703
704
QString TimelineController::framesToClock(int frames) const
{
    return m_model->tractor()->frames_to_time(frames, mlt_time_clock);
}

QString TimelineController::simplifiedTC(int frames) const
705
706
707
708
709
710
711
712
{
    if (KdenliveSettings::frametimecode()) {
        return QString::number(frames);
    }
    QString s = m_model->tractor()->frames_to_time(frames, mlt_time_smpte_df);
    return s.startsWith(QLatin1String("00:")) ? s.remove(0, 3) : s;
}

713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
bool TimelineController::showThumbnails() const
{
    return KdenliveSettings::videothumbnails();
}

bool TimelineController::showAudioThumbnails() const
{
    return KdenliveSettings::audiothumbnails();
}

bool TimelineController::showMarkers() const
{
    return KdenliveSettings::showmarkers();
}

bool TimelineController::audioThumbFormat() const
{
    return KdenliveSettings::displayallchannels();
}

733
734
735
736
737
bool TimelineController::audioThumbNormalize() const
{
    return KdenliveSettings::normalizechannels();
}

738
739
740
741
742
bool TimelineController::showWaveforms() const
{
    return KdenliveSettings::audiothumbnails();
}

743
void TimelineController::beginAddTrack(int tid)
744
{
745
746
747
    if (tid == -1) {
        tid = m_activeTrack;
    }
748
    QPointer<TrackDialog> d = new TrackDialog(m_model, tid, qApp->activeWindow());
749
    if (d->exec() == QDialog::Accepted) {
750
        auto trackName = d->trackName();
751
752
        bool result =
            m_model->addTracksAtPosition(d->selectedTrackPosition(), d->tracksCount(), trackName, d->addAudioTrack(), d->addAVTrack(), d->addRecTrack());
753
        if (!result) {
754
            pCore->displayMessage(i18n("Could not insert track"), ErrorMessage, 500);
755
        }
756
    }
757
758
}

759
void TimelineController::deleteMultipleTracks(int tid)
760
{
761
762
    Fun undo = []() { return true; };
    Fun redo = []() { return true; };
763
    QPointer<TrackDialog> d = new TrackDialog(m_model, tid, qApp->activeWindow(), true, m_activeTrack);
764
    if (d->exec() == QDialog::Accepted) {
Julius Künzel's avatar
Julius Künzel committed
765
        bool result = true;
766
        QList<int> allIds = d->toDeleteTrackIds();
767
        for (int selectedTrackIx : qAsConst(allIds)) {
768
769
770
771
            result = m_model->requestTrackDeletion(selectedTrackIx, undo, redo);
            if (!result) {
                break;
            }
772
773
774
            if (m_activeTrack == -1) {
                setActiveTrack(m_model->getTrackIndexFromPosition(m_model->getTracksCount() - 1));
            }
775
        }
776
        if (result) {
777
            pCore->pushUndo(undo, redo, allIds.count() > 1 ? i18n("Delete Tracks") : i18n("Delete Track"));
778
        }
779
780
781
    }
}

782
void TimelineController::switchTrackRecord(int tid, bool monitor)
783
784
785
786
787
{
    if (tid == -1) {
        tid = m_activeTrack;
    }
    if (!m_model->getTrackById_const(tid)->isAudioTrack()) {
788
        pCore->displayMessage(i18n("Select an audio track to display record controls"), ErrorMessage, 500);
789
790
    }
    int recDisplayed = m_model->getTrackProperty(tid, QStringLiteral("kdenlive:audio_rec")).toInt();
791
    if (monitor == false) {
792
        // Disable rec controls
793
794
795
796
        if (recDisplayed == 0) {
            // Already hidden
            return;
        }
797
798
799
        m_model->setTrackProperty(tid, QStringLiteral("kdenlive:audio_rec"), QStringLiteral("0"));
    } else {
        // Enable rec controls
800
801
802
803
        if (recDisplayed == 1) {
            // Already displayed
            return;
        }
804
805
806
807
        m_model->setTrackProperty(tid, QStringLiteral("kdenlive:audio_rec"), QStringLiteral("1"));
    }
    QModelIndex ix = m_model->makeTrackIndexFromID(tid);
    if (ix.isValid()) {
Vincent Pinon's avatar
Vincent Pinon committed
808
        emit m_model->dataChanged(ix, ix, {TimelineModel::AudioRecordRole});
809
810
811
    }
}

812
813
814
void TimelineController::checkTrackDeletion(int selectedTrackIx)
{
    if (m_activeTrack == selectedTrackIx) {
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
        // Make sure we don't keep an index on a deleted track
        m_activeTrack = -1;
        emit activeTrackChanged();
    }
    if (m_model->m_audioTarget.contains(selectedTrackIx)) {
        QMap<int, int> selection = m_model->m_audioTarget;
        selection.remove(selectedTrackIx);
        setAudioTarget(selection);
    }
    if (m_model->m_videoTarget == selectedTrackIx) {
        setVideoTarget(-1);
    }
    if (m_lastAudioTarget.contains(selectedTrackIx)) {
        m_lastAudioTarget.remove(selectedTrackIx);
        emit lastAudioTargetChanged();
    }
    if (m_lastVideoTarget == selectedTrackIx) {
        m_lastVideoTarget = -1;
        emit lastVideoTargetChanged();
    }
835
836
}

837
838
void TimelineController::showConfig(int page, int tab)
{
Vincent Pinon's avatar
Vincent Pinon committed
839
    emit pCore->showConfigDialog(page, tab);
840
841
}

842
843
void TimelineController::gotoNextSnap()
{
844
845
846
    if (m_activeSnaps.empty() || pCore->undoIndex() != m_snapStackIndex) {
        m_snapStackIndex = pCore->undoIndex();
        m_activeSnaps.clear();
847
        m_activeSnaps = pCore->currentDoc()->getGuideModel()->getSnapPoints();
848
849
        m_activeSnaps.push_back(m_zone.x());
        m_activeSnaps.push_back(m_zone.y() - 1);
850
851
    }
    int nextSnap = m_model->getNextSnapPos(pCore->getTimelinePosition(), m_activeSnaps);
852
    if (nextSnap > pCore->getTimelinePosition()) {
853
854
        setPosition(nextSnap);
    }
855
856
857
858
}

void TimelineController::gotoPreviousSnap()
{
859
    if (pCore->getTimelinePosition() > 0) {
860
861
862
        if (m_activeSnaps.empty() || pCore->undoIndex() != m_snapStackIndex) {
            m_snapStackIndex = pCore->undoIndex();
            m_activeSnaps.clear();
863
            m_activeSnaps = pCore->currentDoc()->getGuideModel()->getSnapPoints();
864
865
            m_activeSnaps.push_back(m_zone.x());
            m_activeSnaps.push_back(m_zone.y() - 1);
866
867
        }
        setPosition(m_model->getPreviousSnapPos(pCore->getTimelinePosition(), m_activeSnaps));
868
    }
869
870
}

871
872
void TimelineController::gotoNextGuide()
{
873
    QList<CommentedTime> guides = pCore->currentDoc()->getGuideModel()->getAllMarkers();
874
875
876
877
878
879
880
881
882
883
884
885
886
887
    int pos = pCore->getTimelinePosition();
    double fps = pCore->getCurrentFps();
    for (auto &guide : guides) {
        if (guide.time().frames(fps) > pos) {
            setPosition(guide.time().frames(fps));
            return;
        }
    }
    setPosition(m_duration - 1);
}

void TimelineController::gotoPreviousGuide()
{
    if (pCore->getTimelinePosition() > 0) {
888
        QList<CommentedTime> guides = pCore->currentDoc()->getGuideModel()->getAllMarkers();
889
890
891
892
893
894
895
896
897
898
899
900
901
902
        int pos = pCore->getTimelinePosition();
        double fps = pCore->getCurrentFps();
        int lastGuidePos = 0;
        for (auto &guide : guides) {
            if (guide.time().frames(fps) >= pos) {
                setPosition(lastGuidePos);
                return;
            }
            lastGuidePos = guide.time().frames(fps);
        }
        setPosition(lastGuidePos);
    }
}

903
904
void TimelineController::groupSelection()
{
905
906
907
908
909
    if (dragOperationRunning()) {
        // Don't allow timeline operation while drag in progress
        pCore->displayMessage(i18n("Cannot perform operation while dragging in timeline"), ErrorMessage);
        return;
    }
910
911
    const auto selection = m_model->getCurrentSelection();
    if (selection.size() < 2) {
912
        pCore->displayMessage(i18n("Select at least 2 items to group"), ErrorMessage, 500);
913
914
        return;
    }
915
916
917
    m_model->requestClearSelection();
    m_model->requestClipsGroup(selection);
    m_model->requestSetSelection(selection);
918
919
920
921
}

void TimelineController::unGroupSelection(int cid)
{
922
923
924
925
926
    if (dragOperationRunning()) {
        // Don't allow timeline operation while drag in progress
        pCore->displayMessage(i18n("Cannot perform operation while dragging in timeline"), ErrorMessage);
        return;
    }
927
    auto ids = m_model->getCurrentSelection();
928
    // ask to unselect if needed
929
930
    m_model->requestClearSelection();
    if (cid > -1) {
931
932
933
934
        ids.insert(cid);
    }
    if (!ids.empty()) {
        m_model->requestClipsUngroup(ids);
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
935
    }
936
937
}

938
939
940
941
942
943
944
bool TimelineController::dragOperationRunning()
{
    QVariant returnedValue;
    QMetaObject::invokeMethod(m_root, "isDragging", Q_RETURN_ARG(QVariant, returnedValue));
    return returnedValue.toBool();
}

945
946
947
948
949
950
bool TimelineController::trimmingActive()
{
    ToolType::ProjectTool tool = pCore->window()->getCurrentTimeline()->activeTool();
    return tool == ToolType::SlideTool || tool == ToolType::SlipTool || tool == ToolType::RippleTool || tool == ToolType::RollTool;
}