timelinecontroller.cpp 159 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

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

int TimelineController::m_duration = 0;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

473
    std::unique_ptr<Mlt::Properties> props(nullptr);
474
    if (revert) {
Nicolas Carion's avatar
Nicolas Carion committed
475
        props = std::make_unique<Mlt::Properties>();
476
477
478
479
480
481
482
483
        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")) {
            props->set("geometry", "0%/0%:100%x100%:100;-1=0%/0%:100%x100%:0");
        }
    }
484
    if (!m_model->requestCompositionInsertion(transitionId, tid, position, duration, std::move(props), id, logUndo)) {
485
        id = -1;
486
        pCore->displayMessage(i18n("Could not add composition at selected position"), ErrorMessage, 500);
487
488
489
490
    }
    return id;
}

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

void TimelineController::deleteSelectedClips()
{
503
504
    auto sel = m_model->getCurrentSelection();
    if (sel.empty()) {
505
506
507
        // 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);
508
509
            m_model->clearAssetView(m_model->m_selectedMix);
            m_model->requestClearSelection(true);
510
        }
511
        return;
512
    }
513
    // only need to delete the first item, the others will be deleted in cascade
514
515
516
517
518
519
520
    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());
    }
521
522
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

876
    int cursorPos = pCore->getTimelinePosition();
877
    const auto selection = m_model->getCurrentSelection();
878
    bool selectionFound = false;
879
880
    if (!selection.empty()) {
        for (int id : selection) {
881
882
883
884
885
886
            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);
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
            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);
905
                    selectionFound = true;
906
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
        } 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);
936
937
938
939
940
941
        }
    }
}

void TimelineController::setOutPoint()
{
942
943
    if (dragOperationRunning()) {
        // Don't allow timeline operation while drag in progress
944
        pCore->displayMessage(i18n("Cannot perform operation while dragging in timeline"), ErrorMessage);
945
        qDebug() << "Cannot operate while dragging";
946
947
        return;
    }
948
    int cursorPos = pCore->getTimelinePosition();
949
    const auto selection = m_model->getCurrentSelection();
950
    bool selectionFound = false;
951
952
    if (!selection.empty()) {
        for (int id : selection) {
953
954
955
956
            int start = m_model->getItemPosition(id);
            if (start + m_model->getItemPlaytime(id) == cursorPos) {
                continue;
            }
957
            int size = cursorPos - start;
958
            m_model->requestItemResize(id, size, true, true, 0, false);
959
960
961
962
963
964
            selectionFound = true;
        }
    }
    if (!selectionFound) {
        if (m_activeTrack >= 0) {
            int cid = m_model->getClipByPosition(m_activeTrack, cursorPos);
965
966
967
968
969
970
971
            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));
972
973
974
975
            }
            if (cid >= 0) {
                int start = m_model->getItemPosition(cid);
                if (start + m_model->getItemPlaytime(cid) != cursorPos) {
976
                    int size = cursorPos - start;
977
                    m_model->requestItemResize(cid, size, true, true, 0, false);
978
                    selectionFound = true;
979
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
        } 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);
1009
1010
1011
1012
        }
    }
}

1013
void TimelineController::editMarker(int cid, int position)
1014
{
1015
    if (cid == -1) {
1016
1017
        cid = getMainSelectedClip();
        if (cid == -1) {
1018
            pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);