timelinecontroller.cpp 160 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/***************************************************************************
 *   Copyright (C) 2017 by Jean-Baptiste Mardelle                                  *
 *   This file is part of Kdenlive. See www.kdenlive.org.                  *
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) version 3 or any later version accepted by the       *
 *   membership of KDE e.V. (or its successor approved  by the membership  *
 *   of KDE e.V.), which shall act as a proxy defined in Section 14 of     *
 *   version 3 of the license.                                             *
 *                                                                         *
 *   This program is distributed in the hope that it will be useful,       *
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
 *   GNU General Public License for more details.                          *
 *                                                                         *
 *   You should have received a copy of the GNU General Public License     *
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>. *
 ***************************************************************************/

#include "timelinecontroller.h"
Nicolas Carion's avatar
Nicolas Carion committed
23
#include "../model/timelinefunctions.hpp"
24
#include "assets/keyframes/model/keyframemodellist.hpp"
25
#include "bin/bin.h"
26
#include "bin/clipcreator.hpp"
Nicolas Carion's avatar
linting    
Nicolas Carion committed
27
#include "bin/model/markerlistmodel.hpp"
28
#include "bin/model/subtitlemodel.hpp"
29
#include "bin/projectclip.h"
30
#include "bin/projectfolder.h"
Nicolas Carion's avatar
Nicolas Carion committed
31
#include "bin/projectitemmodel.h"
Nicolas Carion's avatar
linting    
Nicolas Carion committed
32
#include "core.h"
Nicolas Carion's avatar
Nicolas Carion committed
33
#include "dialogs/spacerdialog.h"
34
#include "dialogs/speeddialog.h"
35
#include "dialogs/speechdialog.h"
36
#include "doc/kdenlivedoc.h"
37
#include "effects/effectsrepository.hpp"
Nicolas Carion's avatar
Nicolas Carion committed
38
#include "effects/effectstack/model/effectstackmodel.hpp"
Nicolas Carion's avatar
linting    
Nicolas Carion committed
39
#include "kdenlivesettings.h"
40
#include "lib/audio/audioEnvelope.h"
41
42
#include "mainwindow.h"
#include "monitor/monitormanager.h"
Nicolas Carion's avatar
Nicolas Carion committed
43
#include "previewmanager.h"
Nicolas Carion's avatar
linting    
Nicolas Carion committed
44
#include "project/projectmanager.h"
45
#include "timeline2/model/clipmodel.hpp"
46
#include "timeline2/model/compositionmodel.hpp"
47
#include "timeline2/model/groupsmodel.hpp"
Nicolas Carion's avatar
Nicolas Carion committed
48
49
#include "timeline2/model/timelineitemmodel.hpp"
#include "timeline2/model/trackmodel.hpp"
50
#include "timeline2/view/dialogs/clipdurationdialog.h"
Nicolas Carion's avatar
Nicolas Carion committed
51
#include "timeline2/view/dialogs/trackdialog.h"
Nicolas Carion's avatar
Nicolas Carion committed
52
#include "transitions/transitionsrepository.hpp"
53
#include "audiomixer/mixermanager.hpp"
54
#include "ui_import_subtitle_ui.h"
55
#include "dialogs/timeremap.h"
56

57
#include <KColorScheme>
58
#include <KMessageBox>
59
#include <KUrlRequesterDialog>
60
#include <KRecentDirs>
61
#include <QApplication>
Nicolas Carion's avatar
Nicolas Carion committed
62
#include <QClipboard>
Nicolas Carion's avatar
Nicolas Carion committed
63
#include <QQuickItem>
Nicolas Carion's avatar
Nicolas Carion committed
64
#include <memory>
65
#include <unistd.h>
66
67
68

int TimelineController::m_duration = 0;

69
TimelineController::TimelineController(QObject *parent)
Nicolas Carion's avatar
linting    
Nicolas Carion committed
70
    : QObject(parent)
71
    , m_root(nullptr)
72
    , m_usePreview(false)
73
    , m_audioRef(-1)
Vincent Pinon's avatar
Vincent Pinon committed
74
    , m_zone(-1, -1)
75
    , m_activeTrack(-1)
76
    , m_scale(QFontMetrics(QApplication::font()).maxWidth() / 250)
77
    , m_timelinePreview(nullptr)
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
78
    , m_ready(false)
79
    , m_snapStackIndex(-1)
80
    , m_effectZone({0,0})
81
{
82
83
84
    m_disablePreview = pCore->currentDoc()->getAction(QStringLiteral("disable_preview"));
    connect(m_disablePreview, &QAction::triggered, this, &TimelineController::disablePreview);
    m_disablePreview->setEnabled(false);
85
    connect(pCore.get(), &Core::finalizeRecording, this, &TimelineController::finishRecording);
86
    connect(pCore.get(), &Core::autoScrollChanged, this, &TimelineController::autoScrollChanged);
87
    connect(pCore->mixer(), &MixerManager::recordAudio, this, &TimelineController::switchRecording);
88
89
}

90
91
TimelineController::~TimelineController()
{
92
93
94
95
}

void TimelineController::prepareClose()
{
96
    // Clear root so we don't call its methods anymore
97
    QObject::disconnect( m_deleteConnection );
98
    disconnect(this, &TimelineController::selectionChanged, this, &TimelineController::updateClipActions);
99
    disconnect(m_model.get(), &TimelineModel::selectionChanged, this, &TimelineController::selectionChanged);
100
101
    disconnect(this, &TimelineController::videoTargetChanged, this, &TimelineController::updateVideoTarget);
    disconnect(this, &TimelineController::audioTargetChanged, this, &TimelineController::updateAudioTarget);
102
103
    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
104
105
    m_ready = false;
    m_root = nullptr;
106
    // Delete timeline preview before resetting model so that removing clips from timeline doesn't invalidate
107
108
    delete m_timelinePreview;
    m_timelinePreview = nullptr;
109
    m_model.reset();
110
111
}

Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
112
void TimelineController::setModel(std::shared_ptr<TimelineItemModel> model)
113
{
114
    delete m_timelinePreview;
115
    m_zone = QPoint(-1, -1);
116
    m_timelinePreview = nullptr;
117
    m_model = std::move(model);
118
    m_activeSnaps.clear();
119
    connect(m_model.get(), &TimelineItemModel::requestClearAssetView, pCore.get(), &Core::clearAssetPanel);
Vincent Pinon's avatar
Vincent Pinon committed
120
    m_deleteConnection = connect(m_model.get(), &TimelineItemModel::checkItemDeletion, this, [this] (int id) {
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
121
        if (m_ready) {
122
123
124
            QMetaObject::invokeMethod(m_root, "checkDeletion", Qt::QueuedConnection, Q_ARG(QVariant, id));
        }
    });
Julius Künzel's avatar
Julius Künzel committed
125
126
127
128
129
130
131
    connect(m_model.get(), &TimelineItemModel::showTrackEffectStack, this, [&](int tid) {
        if (tid > -1) {
            showTrackAsset(tid);
        } else {
            showMasterEffects();
        }
    });
132
133
134
    connect(this, &TimelineController::selectionChanged, this, &TimelineController::updateClipActions);
    connect(this, &TimelineController::videoTargetChanged, this, &TimelineController::updateVideoTarget);
    connect(this, &TimelineController::audioTargetChanged, this, &TimelineController::updateAudioTarget);
Nicolas Carion's avatar
Nicolas Carion committed
135
    connect(m_model.get(), &TimelineItemModel::requestMonitorRefresh, [&]() { pCore->requestMonitorRefresh(); });
136
    connect(m_model.get(), &TimelineModel::invalidateZone, this, &TimelineController::invalidateZone, Qt::DirectConnection);
137
    connect(m_model.get(), &TimelineModel::durationUpdated, this, &TimelineController::checkDuration);
138
    connect(m_model.get(), &TimelineModel::selectionChanged, this, &TimelineController::selectionChanged);
139
140
    connect(m_model.get(), &TimelineModel::selectedMixChanged, this, &TimelineController::showMixModel);
    connect(m_model.get(), &TimelineModel::selectedMixChanged, this, &TimelineController::selectedMixChanged);
141
    connect(m_model.get(), &TimelineModel::checkTrackDeletion, this, &TimelineController::checkTrackDeletion, Qt::DirectConnection);
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
142
143
}

144
145
146
147
148
void TimelineController::restoreTargetTracks()
{
    setTargetTracks(m_hasVideoTarget, m_model->m_binAudioTargets);
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

281
int TimelineController::selectedTrack() const
282
{
283
284
285
286
287
    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);
288
        selected_tracks.emplace_back(m_model->getTrackPosition(tid), tid);
289
    }
290
291
292
    // 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;
293
294
}

295
296
void TimelineController::selectCurrentItem(ObjectType type, bool select, bool addToCurrent)
{
297
298
299
    int currentClip = -1;
    if (type == ObjectType::TimelineClip) {
        currentClip = m_activeTrack == -2 ? m_model->getSubtitleByPosition(pCore->getTimelinePosition()) : m_model->getClipByPosition(m_activeTrack, pCore->getTimelinePosition());
300
    } else if (type == ObjectType::TimelineComposition) {
301
302
303
        currentClip =  m_model->getCompositionByPosition(m_activeTrack, pCore->getTimelinePosition());
    }

304
    if (currentClip == -1) {
305
        pCore->displayMessage(i18n("No item under timeline cursor in active track"), ErrorMessage, 500);
306
307
        return;
    }
308
309
310
    if (!select) {
        m_model->requestRemoveFromSelection(currentClip);
    } else {
311
        bool grouped = m_model->m_groups->isInGroup(currentClip);
312
        m_model->requestAddToSelection(currentClip, !addToCurrent);
313
314
315
316
        if (grouped) {
            // If part of a group, ensure the effect/composition stack displays the selected item's properties
            emit showAsset(currentClip);
        }
317
318
319
320
321
    }
}

QList<int> TimelineController::selection() const
{
322
    if (!m_root) return QList<int>();
323
    std::unordered_set<int> sel = m_model->getCurrentSelection();
324
    QList<int> items;
325
    for (int id : sel) {
326
327
328
        items << id;
    }
    return items;
329
330
}

331
332
333
334
335
int TimelineController::selectedMix() const
{
    return m_model->m_selectedMix;
}

336
337
338
339
340
341
void TimelineController::selectItems(const QList<int> &ids)
{
    std::unordered_set<int> ids_s(ids.begin(), ids.end());
    m_model->requestSetSelection(ids_s);
}

342
343
344
345
346
347
348
void TimelineController::setScrollPos(int pos)
{
    if (pos > 0 && m_root) {
        QMetaObject::invokeMethod(m_root, "setScrollPos", Qt::QueuedConnection, Q_ARG(QVariant, pos));
    }
}

349
350
351
352
353
354
void TimelineController::resetView()
{
    m_model->_resetView();
    if (m_root) {
        QMetaObject::invokeMethod(m_root, "updatePalette");
    }
355
    emit colorsChanged();
356
357
}

358
359
bool TimelineController::snap()
{
360
361
362
    return KdenliveSettings::snaptopoints();
}

363
364
365
366
367
368
369
370
371
372
bool TimelineController::ripple()
{
    return false;
}

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

373
int TimelineController::insertClip(int tid, int position, const QString &data_str, bool logUndo, bool refreshView, bool useTargets)
374
375
{
    int id;
376
    if (tid == -1) {
377
        tid = m_activeTrack;
378
379
    }
    if (position == -1) {
380
        position = pCore->getTimelinePosition();
381
    }
382
    if (!m_model->requestClipInsertion(data_str, tid, position, id, logUndo, refreshView, useTargets)) {
383
384
385
386
387
        id = -1;
    }
    return id;
}

388
389
390
391
392
393
394
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) {
395
        position = pCore->getTimelinePosition();
396
397
398
399
400
401
    }
    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;
}

402
int TimelineController::insertNewComposition(int tid, int position, const QString &transitionId, bool logUndo)
403
404
405
406
407
408
409
410
411
412
{
    int clipId = m_model->getTrackById_const(tid)->getClipByPosition(position);
    if (clipId > 0) {
        int minimum = m_model->getClipPosition(clipId);
        return insertNewComposition(tid, clipId, position - minimum, transitionId, logUndo);
    }
    return insertComposition(tid, position, transitionId, logUndo);
}

int TimelineController::insertNewComposition(int tid, int clipId, int offset, const QString &transitionId, bool logUndo)
413
414
{
    int id;
415
    int minimumPos = clipId > -1 ? m_model->getClipPosition(clipId) : offset;
416
    int clip_duration = clipId > -1 ? m_model->getClipPlaytime(clipId) : pCore->getDurationFromString(KdenliveSettings::transition_duration());
417
418
    int endPos = minimumPos + clip_duration;
    int position = minimumPos;
419
    int duration = qMin(clip_duration, pCore->getDurationFromString(KdenliveSettings::transition_duration()));
420
    int lowerVideoTrackId = m_model->getPreviousVideoTrackIndex(tid);
421
    bool revert = offset > clip_duration / 2;
422
    int bottomId = 0;
423
    if (lowerVideoTrackId > 0) {
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
        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
        } else if (clip_duration - offset < duration * 1.2  && duration < 2 * clip_duration) {
            // 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);
444
                if (test_duration > 0) {
445
446
447
448
                    offset -= (bottom.first - position);
                    position = bottom.first;
                    duration = test_duration;
                    revert = position > minimumPos;
449
450
                }
            }
451
452
453
454
455
456
        } 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);
            }
457
458
        }
    }
459
    int defaultLength = pCore->getDurationFromString(KdenliveSettings::transition_duration());
460
    bool isShortComposition = TransitionsRepository::get()->getType(transitionId) == AssetListType::AssetType::VideoShortComposition;
461
462
    if (duration < 0 || (isShortComposition && duration > 1.5 * defaultLength)) {
        duration = defaultLength;
463
    } else if (duration <= 1) {
464
        // if suggested composition duration is lower than 4 frames, use default
465
        duration = pCore->getDurationFromString(KdenliveSettings::transition_duration());
466
467
        if (minimumPos + clip_duration - position < 3) {
            position = minimumPos + clip_duration - duration;
468
        }
469
    }
470
471
472
473
    QPair<int, int> finalPos = m_model->getTrackById_const(tid)->validateCompositionLength(position, offset, duration, endPos);
    position = finalPos.first;
    duration = finalPos.second;

474
    std::unique_ptr<Mlt::Properties> props(nullptr);
475
    if (revert) {
Nicolas Carion's avatar
Nicolas Carion committed
476
        props = std::make_unique<Mlt::Properties>();
477
478
479
480
481
        if (transitionId == QLatin1String("dissolve")) {
            props->set("reverse", 1);
        } else if (transitionId == QLatin1String("composite") || transitionId == QLatin1String("slide")) {
            props->set("invert", 1);
        } else if (transitionId == QLatin1String("wipe")) {
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
482
            props->set("geometry", "0=0% 0% 100% 100% 100%;-1=0% 0% 100% 100% 0%");
483
484
        }
    }
485
    if (!m_model->requestCompositionInsertion(transitionId, tid, position, duration, std::move(props), id, logUndo)) {
486
        id = -1;
487
        pCore->displayMessage(i18n("Could not add composition at selected position"), ErrorMessage, 500);
488
489
490
491
    }
    return id;
}

492
493
494
int TimelineController::insertComposition(int tid, int position, const QString &transitionId, bool logUndo)
{
    int id;
495
    int duration = pCore->getDurationFromString(KdenliveSettings::transition_duration());
496
    if (!m_model->requestCompositionInsertion(transitionId, tid, position, duration, nullptr, id, logUndo)) {
497
498
499
500
501
502
503
        id = -1;
    }
    return id;
}

void TimelineController::deleteSelectedClips()
{
504
505
    auto sel = m_model->getCurrentSelection();
    if (sel.empty()) {
506
507
508
        // 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);
509
510
            m_model->clearAssetView(m_model->m_selectedMix);
            m_model->requestClearSelection(true);
511
        }
512
        return;
513
    }
514
    // only need to delete the first item, the others will be deleted in cascade
515
516
517
518
519
520
521
    if (m_model->m_editMode == TimelineMode::InsertEdit) {
        // In insert mode, perform an extract operation (don't leave gaps)
        extract(*sel.begin());
    }
    else {
        m_model->requestItemDeletion(*sel.begin());
    }
522
523
}

524
int TimelineController::getMainSelectedItem(bool restrictToCurrentPos, bool allowComposition)
525
526
527
528
529
530
531
532
533
534
535
536
{
    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;
        }
    }
537
538
539
540
541
    if (!restrictToCurrentPos) {
        if (m_model->isClip(itemId) || (allowComposition && m_model->isComposition(itemId))) {
            return itemId;
        }
    }
542
    if (m_model->isClip(itemId)) {
543
        int position = pCore->getTimelinePosition();
544
545
546
547
548
549
550
551
552
        int start = m_model->getClipPosition(itemId);
        int end = start + m_model->getClipPlaytime(itemId);
        if (position >= start && position <= end) {
            return itemId;
        }
    }
    return -1;
}

553
554
void TimelineController::copyItem()
{
555
556
    std::unordered_set<int> selectedIds = m_model->getCurrentSelection();
    if (selectedIds.empty()) {
557
558
        return;
    }
559
560
561
562
    int clipId = *(selectedIds.begin());
    QString copyString = TimelineFunctions::copyClips(m_model, selectedIds);
    QClipboard *clipboard = QApplication::clipboard();
    clipboard->setText(copyString);
563
    m_root->setProperty("copiedClip", clipId);
564
    m_model->requestSetSelection(selectedIds);
565
566
}

567
bool TimelineController::pasteItem(int position, int tid)
568
{
569
570
    QClipboard *clipboard = QApplication::clipboard();
    QString txt = clipboard->text();
571
572
573
574
575
576
    if (tid == -1) {
        tid = getMouseTrack();
    }
    if (position == -1) {
        position = getMousePos();
    }
577
    if (tid == -1) {
578
        tid = m_activeTrack;
579
580
    }
    if (position == -1) {
581
        position = pCore->getTimelinePosition();
582
    }
583
    return TimelineFunctions::pasteClips(m_model, txt, tid, position);
584
585
}

586
587
void TimelineController::triggerAction(const QString &name)
{
588
    pCore->triggerAction(name);
589
590
}

591
592
593
594
595
const QString TimelineController::actionText(const QString &name)
{
    return pCore->actionText(name);
}

596
QString TimelineController::timecode(int frames) const
597
598
599
{
    return KdenliveSettings::frametimecode() ? QString::number(frames) : m_model->tractor()->frames_to_time(frames, mlt_time_smpte_df);
}
600

601
602
603
604
605
606
QString TimelineController::framesToClock(int frames) const
{
    return m_model->tractor()->frames_to_time(frames, mlt_time_clock);
}

QString TimelineController::simplifiedTC(int frames) const
607
608
609
610
611
612
613
614
{
    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;
}

615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
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();
}

635
636
637
638
639
bool TimelineController::audioThumbNormalize() const
{
    return KdenliveSettings::normalizechannels();
}

640
641
642
643
644
645
646
bool TimelineController::showWaveforms() const
{
    return KdenliveSettings::audiothumbnails();
}

void TimelineController::addTrack(int tid)
{
647
648
649
    if (tid == -1) {
        tid = m_activeTrack;
    }
650
    QPointer<TrackDialog> d = new TrackDialog(m_model, tid, qApp->activeWindow());
651
    if (d->exec() == QDialog::Accepted) {
652
653
        bool audioRecTrack = d->addRecTrack();
        bool addAVTrack = d->addAVTrack();
654
        int tracksCount = d->tracksCount();
655
656
        Fun undo = []() { return true; };
        Fun redo = []() { return true; };
657
658
659
660
661
662
663
664
665
666
667
668
669
        bool result = true;
        for (int ix = 0; ix < tracksCount; ++ix) {
            int newTid;
            result = m_model->requestTrackInsertion(d->selectedTrackPosition(), newTid, d->trackName(), d->addAudioTrack(), undo, redo);
            if (result) {
                if (addAVTrack) {
                    int newTid2;
                    int mirrorPos = 0;
                    int mirrorId = m_model->getMirrorAudioTrackId(newTid);
                    if (mirrorId > -1) {
                        mirrorPos = m_model->getTrackMltIndex(mirrorId);
                    }
                    result = m_model->requestTrackInsertion(mirrorPos, newTid2, d->trackName(), true, undo, redo);
670
                }
671
672
                if (audioRecTrack) {
                    m_model->setTrackProperty(newTid, "kdenlive:audio_rec", QStringLiteral("1"));
673
                }
674
675
            } else {
                break;
676
677
            }
        }
678
        if (result) {
679
            pCore->pushUndo(undo, redo, addAVTrack || tracksCount > 1 ? i18nc("@action", "Insert Tracks") : i18nc("@action", "Insert Track"));
680
        } else {
681
            pCore->displayMessage(i18n("Could not insert track"), ErrorMessage, 500);
682
            undo();
683
        }
684
    }
685
686
}

687
void TimelineController::deleteMultipleTracks(int tid)
688
{
689
690
691
    Fun undo = []() { return true; };
    Fun redo = []() { return true; };
    bool result = true;
692
    QPointer<TrackDialog> d = new TrackDialog(m_model, tid, qApp->activeWindow(), true,m_activeTrack);
693
694
695
    if (tid == -1) {
        tid = m_activeTrack;
    }
696
    if (d->exec() == QDialog::Accepted) {
697
        QList<int> allIds = d->toDeleteTrackIds();
698
        for (int selectedTrackIx : qAsConst(allIds)) {
699
700
701
702
            result = m_model->requestTrackDeletion(selectedTrackIx, undo, redo);
            if (!result) {
                break;
            }
703
704
705
            if (m_activeTrack == -1) {
                setActiveTrack(m_model->getTrackIndexFromPosition(m_model->getTracksCount() - 1));
            }
706
        }
707
708
709
        if (result) {
          pCore->pushUndo(undo, redo, allIds.count() > 1 ? i18n("Delete Tracks") : i18n("Delete Track"));
        }
710
711
712
    }
}

713
714
715
716
717
718
void TimelineController::switchTrackRecord(int tid)
{
    if (tid == -1) {
        tid = m_activeTrack;
    }
    if (!m_model->getTrackById_const(tid)->isAudioTrack()) {
719
        pCore->displayMessage(i18n("Select an audio track to display record controls"), ErrorMessage, 500);
720
721
722
723
724
725
726
727
728
729
730
    }
    int recDisplayed = m_model->getTrackProperty(tid, QStringLiteral("kdenlive:audio_rec")).toInt();
    if (recDisplayed == 1) {
        // Disable rec controls
        m_model->setTrackProperty(tid, QStringLiteral("kdenlive:audio_rec"), QStringLiteral("0"));
    } else {
        // Enable rec controls
        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
731
        emit m_model->dataChanged(ix, ix, {TimelineModel::AudioRecordRole});
732
733
734
    }
}

735
736
737
void TimelineController::checkTrackDeletion(int selectedTrackIx)
{
    if (m_activeTrack == selectedTrackIx) {
738
739
            // Make sure we don't keep an index on a deleted track
            m_activeTrack = -1;
740
            emit activeTrackChanged();
741
        }
742
743
744
745
        if (m_model->m_audioTarget.contains(selectedTrackIx)) {
            QMap<int, int> selection = m_model->m_audioTarget;
            selection.remove(selectedTrackIx);
            setAudioTarget(selection);
746
747
748
749
        }
        if (m_model->m_videoTarget == selectedTrackIx) {
            setVideoTarget(-1);
        }
750
        if (m_lastAudioTarget.contains(selectedTrackIx)) {
751
            m_lastAudioTarget.remove(selectedTrackIx);
752
753
754
755
756
757
            emit lastAudioTargetChanged();
        }
        if (m_lastVideoTarget == selectedTrackIx) {
            m_lastVideoTarget = -1;
            emit lastVideoTargetChanged();
        }
758
759
}

760
761
void TimelineController::showConfig(int page, int tab)
{
Vincent Pinon's avatar
Vincent Pinon committed
762
    emit pCore->showConfigDialog(page, tab);
763
764
}

765
766
void TimelineController::gotoNextSnap()
{
767
768
769
    if (m_activeSnaps.empty() || pCore->undoIndex() != m_snapStackIndex) {
        m_snapStackIndex = pCore->undoIndex();
        m_activeSnaps.clear();
770
        m_activeSnaps = pCore->currentDoc()->getGuideModel()->getSnapPoints();
771
772
        m_activeSnaps.push_back(m_zone.x());
        m_activeSnaps.push_back(m_zone.y() - 1);
773
774
    }
    int nextSnap = m_model->getNextSnapPos(pCore->getTimelinePosition(), m_activeSnaps);
775
    if (nextSnap > pCore->getTimelinePosition()) {
776
777
        setPosition(nextSnap);
    }
778
779
780
781
}

void TimelineController::gotoPreviousSnap()
{
782
    if (pCore->getTimelinePosition() > 0) {
783
784
785
        if (m_activeSnaps.empty() || pCore->undoIndex() != m_snapStackIndex) {
            m_snapStackIndex = pCore->undoIndex();
            m_activeSnaps.clear();
786
            m_activeSnaps = pCore->currentDoc()->getGuideModel()->getSnapPoints();
787
788
            m_activeSnaps.push_back(m_zone.x());
            m_activeSnaps.push_back(m_zone.y() - 1);
789
790
        }
        setPosition(m_model->getPreviousSnapPos(pCore->getTimelinePosition(), m_activeSnaps));
791
    }
792
793
}

794
795
void TimelineController::gotoNextGuide()
{
796
    QList<CommentedTime> guides = pCore->currentDoc()->getGuideModel()->getAllMarkers();
797
798
799
800
801
802
803
804
805
806
807
808
809
810
    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) {
811
        QList<CommentedTime> guides = pCore->currentDoc()->getGuideModel()->getAllMarkers();
812
813
814
815
816
817
818
819
820
821
822
823
824
825
        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);
    }
}

826
827
void TimelineController::groupSelection()
{
828
829
830
831
832
    if (dragOperationRunning()) {
        // Don't allow timeline operation while drag in progress
        pCore->displayMessage(i18n("Cannot perform operation while dragging in timeline"), ErrorMessage);
        return;
    }
833
834
    const auto selection = m_model->getCurrentSelection();
    if (selection.size() < 2) {
835
        pCore->displayMessage(i18n("Select at least 2 items to group"), ErrorMessage, 500);
836
837
        return;
    }
838
839
840
    m_model->requestClearSelection();
    m_model->requestClipsGroup(selection);
    m_model->requestSetSelection(selection);
841
842
843
844
}

void TimelineController::unGroupSelection(int cid)
{
845
846
847
848
849
    if (dragOperationRunning()) {
        // Don't allow timeline operation while drag in progress
        pCore->displayMessage(i18n("Cannot perform operation while dragging in timeline"), ErrorMessage);
        return;
    }
850
    auto ids = m_model->getCurrentSelection();
851
    // ask to unselect if needed
852
853
    m_model->requestClearSelection();
    if (cid > -1) {
854
855
856
857
        ids.insert(cid);
    }
    if (!ids.empty()) {
        m_model->requestClipsUngroup(ids);
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
858
    }
859
860
}

861
862
863
864
865
866
867
bool TimelineController::dragOperationRunning()
{
    QVariant returnedValue;
    QMetaObject::invokeMethod(m_root, "isDragging", Q_RETURN_ARG(QVariant, returnedValue));
    return returnedValue.toBool();
}

868
869
void TimelineController::setInPoint()
{
870
871
    if (dragOperationRunning()) {
        // Don't allow timeline operation while drag in progress
872
873
        pCore->displayMessage(i18n("Cannot perform operation while dragging in timeline"), ErrorMessage);
        qDebug()<< "Cannot operate while dragging";
874
875
876
        return;
    }

877
    int cursorPos = pCore->getTimelinePosition();
878
    const auto selection = m_model->getCurrentSelection();
879
    bool selectionFound = false;
880
881
    if (!selection.empty()) {
        for (int id : selection) {
882
883
884
885
886
887
            int start = m_model->getItemPosition(id);
            if (start == cursorPos) {
                continue;
            }
            int size = start + m_model->getItemPlaytime(id) - cursorPos;
            m_model->requestItemResize(id, size, false, true, 0, false);
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
            selectionFound = true;
        }
    }
    if (!selectionFound) {
        if (m_activeTrack >= 0) {
            int cid = m_model->getClipByPosition(m_activeTrack, cursorPos);
            if (cid < 0) {
                // Check first item after timeline position
                int maximumSpace = m_model->getTrackById_const(m_activeTrack)->getBlankEnd(cursorPos);
                if (maximumSpace < INT_MAX) {
                    cid = m_model->getClipByPosition(m_activeTrack, maximumSpace + 1);
                }
            }
            if (cid >= 0) {
                int start = m_model->getItemPosition(cid);
                if (start != cursorPos) {
                    int size = start + m_model->getItemPlaytime(cid) - cursorPos;
                    m_model->requestItemResize(cid, size, false, true, 0, false);
906
                    selectionFound = true;
907
908
                }
            }
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
        } else if (m_activeTrack == -2) {
            // Subtitle track
            auto subtitleModel = pCore->getSubtitleModel();
            if (subtitleModel) {
                int sid = -1;
                std::unordered_set<int> sids = subtitleModel->getItemsInRange(cursorPos, cursorPos);
                if (sids.empty()) {
                    sids = subtitleModel->getItemsInRange(cursorPos, -1);
                    for (int s : sids) {
                        if (sid == -1 || subtitleModel->getStartPosForId(s) < subtitleModel->getStartPosForId(sid)) {
                            sid = s;
                        }
                    }
                } else {
                    sid = *sids.begin();
                }
                if (sid > -1) {
                    int start = m_model->getItemPosition(sid);
                    if (start != cursorPos) {
                        int size = start + m_model->getItemPlaytime(sid) - cursorPos;
                        m_model->requestItemResize(sid, size, false, true, 0, false);
                        selectionFound = true;
                    }
                }
            }
        }
        if (!selectionFound) {
            pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
937
938
939
940
941
942
        }
    }
}

void TimelineController::setOutPoint()
{
943
944
    if (dragOperationRunning()) {
        // Don't allow timeline operation while drag in progress
945
        pCore->displayMessage(i18n("Cannot perform operation while dragging in timeline"), ErrorMessage);
946
        qDebug() << "Cannot operate while dragging";
947
948
        return;
    }
949
    int cursorPos = pCore->getTimelinePosition();
950
    const auto selection = m_model->getCurrentSelection();
951
    bool selectionFound = false;
952
953
    if (!selection.empty()) {
        for (int id : selection) {
954
955
956
957
            int start = m_model->getItemPosition(id);
            if (start + m_model->getItemPlaytime(id) == cursorPos) {
                continue;
            }
958
            int size = cursorPos - start;
959
            m_model->requestItemResize(id, size, true, true, 0, false);
960
961
962
963
964
965
            selectionFound = true;
        }
    }
    if (!selectionFound) {
        if (m_activeTrack >= 0) {
            int cid = m_model->getClipByPosition(m_activeTrack, cursorPos);
966
967
968
969
970
971
972
            if (cid < 0 || cursorPos == m_model->getItemPosition(cid)) {
                // If no clip found at cursor pos or we are at the first frame of a clip, try to find previous clip
                // Check first item before timeline position
                // If we are at a clip start, check space before this clip
                int offset = cid >= 0 ? 1 : 0;
                int previousPos = m_model->getTrackById_const(m_activeTrack)->getBlankStart(cursorPos - offset);
                cid = m_model->getClipByPosition(m_activeTrack, qMax(0, previousPos - 1));
973
974
975
976
            }
            if (cid >= 0) {
                int start = m_model->getItemPosition(cid);
                if (start + m_model->getItemPlaytime(cid) != cursorPos) {
977
                    int size = cursorPos - start;
978
                    m_model->requestItemResize(cid, size, true, true, 0, false);
979
                    selectionFound = true;
980
981
                }
            }
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
        } else if (m_activeTrack == -2) {
            // Subtitle track
            auto subtitleModel = pCore->getSubtitleModel();
            if (subtitleModel) {
                int sid = -1;
                std::unordered_set<int> sids = subtitleModel->getItemsInRange(cursorPos, cursorPos);
                if (sids.empty()) {
                    sids = subtitleModel->getItemsInRange(0, cursorPos);
                    for (int s : sids) {
                        if (sid == -1 || subtitleModel->getSubtitleEnd(s) > subtitleModel->getSubtitleEnd(sid)) {
                            sid = s;
                        }
                    }
                } else {
                    sid = *sids.begin();
                }
                if (sid > -1) {
                    int start = m_model->getItemPosition(sid);
                    if (start + m_model->getItemPlaytime(sid) != cursorPos) {
                        int size = cursorPos - start;
                        m_model->requestItemResize(sid, size, true, true, 0, false);
                        selectionFound = true;
                    }
                }
            }
        }
        if (!selectionFound) {
            pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
1010
1011
1012
1013
        }
    }
}

1014
void TimelineController::editMarker(int cid, int position)
1015
{
1016
    if (cid == -1) {