bin.cpp 134 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
/*
Copyright (C) 2012  Till Theato <root@ttill.de>
Copyright (C) 2014  Jean-Baptiste Mardelle <jb@kdenlive.org>
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
11
by the membership of KDE e.V.), which shall act as a proxy
12
13
14
15
16
17
18
19
20
21
22
23
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 "bin.h"
Nicolas Carion's avatar
Nicolas Carion committed
24
#include "bincommands.h"
25
#include "clipcreator.hpp"
Nicolas Carion's avatar
Nicolas Carion committed
26
27
28
29
30
#include "core.h"
#include "dialogs/clipcreationdialog.h"
#include "doc/documentchecker.h"
#include "doc/docundostack.hpp"
#include "doc/kdenlivedoc.h"
Nicolas Carion's avatar
Nicolas Carion committed
31
#include "effects/effectstack/model/effectstackmodel.hpp"
32
#include "jobs/audiothumbjob.hpp"
33
34
#include "jobs/jobmanager.h"
#include "jobs/loadjob.hpp"
35
#include "jobs/thumbjob.hpp"
36
#include "kdenlive_debug.h"
37
#include "kdenlivesettings.h"
Nicolas Carion's avatar
Nicolas Carion committed
38
39
40
41
42
#include "mainwindow.h"
#include "mlt++/Mlt.h"
#include "mltcontroller/clipcontroller.h"
#include "mltcontroller/clippropertiescontroller.h"
#include "monitor/monitor.h"
43
#include "project/dialogs/slideshowclip.h"
Nicolas Carion's avatar
Nicolas Carion committed
44
#include "project/invaliddialog.h"
45
#include "project/projectcommands.h"
Nicolas Carion's avatar
Nicolas Carion committed
46
47
48
49
50
#include "project/projectmanager.h"
#include "projectclip.h"
#include "projectfolder.h"
#include "projectfolderup.h"
#include "projectitemmodel.h"
51
#include "projectsortproxymodel.h"
Nicolas Carion's avatar
Nicolas Carion committed
52
53
54
#include "projectsubclip.h"
#include "titler/titlewidget.h"
#include "ui_qtextclip_ui.h"
Nicolas Carion's avatar
Nicolas Carion committed
55
#include "undohelper.hpp"
56
#include "xml/xml.hpp"
57

58
#include "xml/xml.hpp"
59

60
#include <KColorScheme>
61
#include <KMessageBox>
62
#include <KXMLGUIFactory>
Nicolas Carion's avatar
Nicolas Carion committed
63
#include <QToolBar>
64

Nicolas Carion's avatar
Nicolas Carion committed
65
66
#include "kdenlive_debug.h"
#include <QCryptographicHash>
67
#include <QDesktopServices>
68
#include <QDialogButtonBox>
69
#include <QDrag>
Nicolas Carion's avatar
Nicolas Carion committed
70
#include <QFile>
71
#include <QMenu>
Nicolas Carion's avatar
Nicolas Carion committed
72
73
#include <QSlider>
#include <QTimeLine>
74
#include <QUndoCommand>
Nicolas Carion's avatar
Nicolas Carion committed
75
76
77
#include <QUrl>
#include <QVBoxLayout>
#include <QtConcurrent>
Nicolas Carion's avatar
Nicolas Carion committed
78
#include <utility>
79
80
81
82
83
84
85
86
87
88
/**
 * @class BinItemDelegate
 * @brief This class is responsible for drawing items in the QTreeView.
 */

class BinItemDelegate : public QStyledItemDelegate
{
public:
    explicit BinItemDelegate(QObject *parent = nullptr)
        : QStyledItemDelegate(parent)
Nicolas Carion's avatar
Nicolas Carion committed
89

90
    {
91
        connect(this, &QStyledItemDelegate::closeEditor, [&]() { m_editorOpen = false; });
92
    }
93
    void setDar(double dar) { m_dar = dar; }
94
95
96
97
98
99
100
    void setEditorData(QWidget *w, const QModelIndex &i) const override
    {
        if (!m_editorOpen) {
            QStyledItemDelegate::setEditorData(w, i);
            m_editorOpen = true;
        }
    }
101
    bool editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index) override
102
    {
103
104
105
        Q_UNUSED(model);
        Q_UNUSED(option);
        Q_UNUSED(index);
106
        if (event->type() == QEvent::MouseButtonPress) {
Nicolas Carion's avatar
Nicolas Carion committed
107
            auto *me = (QMouseEvent *)event;
108
109
110
111
112
113
114
115
116
117
118
119
120
121
            if (m_audioDragRect.contains(me->pos())) {
                dragType = PlaylistState::AudioOnly;
            } else if (m_videoDragRect.contains(me->pos())) {
                dragType = PlaylistState::VideoOnly;
            } else {
                dragType = PlaylistState::Disabled;
            }
        }
        event->ignore();
        return false;
    }
    void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const override
    {
        if (index.column() != 0) {
122
123
            QStyledItemDelegate::updateEditorGeometry(editor, option, index);
            return;
124
125
126
127
128
        }
        QStyleOptionViewItem opt = option;
        initStyleOption(&opt, index);
        QRect r1 = option.rect;
        int type = index.data(AbstractProjectItem::ItemTypeRole).toInt();
129
        int decoWidth = 0;
130
131
        if (opt.decorationSize.height() > 0) {
            decoWidth += r1.height() * m_dar;
132
        }
133
        int mid = 0;
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
        if (type == AbstractProjectItem::ClipItem || type == AbstractProjectItem::SubClipItem) {
            mid = (int)((r1.height() / 2));
        }
        r1.adjust(decoWidth, 0, 0, -mid);
        QFont ft = option.font;
        ft.setBold(true);
        QFontMetricsF fm(ft);
        QRect r2 = fm.boundingRect(r1, Qt::AlignLeft | Qt::AlignTop, index.data(AbstractProjectItem::DataName).toString()).toRect();
        editor->setGeometry(r2);
    }

    QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override
    {
        QSize hint = QStyledItemDelegate::sizeHint(option, index);
        QString text = index.data(AbstractProjectItem::DataName).toString();
        QRectF r = option.rect;
        QFont ft = option.font;
        ft.setBold(true);
        QFontMetricsF fm(ft);
        QStyle *style = option.widget ? option.widget->style() : QApplication::style();
        const int textMargin = style->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1;
        int width = fm.boundingRect(r, Qt::AlignLeft | Qt::AlignTop, text).width() + option.decorationSize.width() + 2 * textMargin;
        hint.setWidth(width);
        int type = index.data(AbstractProjectItem::ItemTypeRole).toInt();
        if (type == AbstractProjectItem::FolderItem || type == AbstractProjectItem::FolderUpItem) {
            return QSize(hint.width(), qMin(option.fontMetrics.lineSpacing() + 4, hint.height()));
        }
        if (type == AbstractProjectItem::ClipItem) {
            return QSize(hint.width(), qMax(option.fontMetrics.lineSpacing() * 2 + 4, qMax(hint.height(), option.decorationSize.height())));
        }
        if (type == AbstractProjectItem::SubClipItem) {
            return QSize(hint.width(), qMax(option.fontMetrics.lineSpacing() * 2 + 4, qMin(hint.height(), (int)(option.decorationSize.height() / 1.5))));
        }
        QIcon icon = qvariant_cast<QIcon>(index.data(Qt::DecorationRole));
        QString line1 = index.data(Qt::DisplayRole).toString();
        QString line2 = index.data(Qt::UserRole).toString();

Vincent Pinon's avatar
Vincent Pinon committed
171
        int textW = qMax(option.fontMetrics.horizontalAdvance(line1), option.fontMetrics.horizontalAdvance(line2));
172
        QSize iconSize = icon.actualSize(option.decorationSize);
Nicolas Carion's avatar
Nicolas Carion committed
173
        return {qMax(textW, iconSize.width()) + 4, option.fontMetrics.lineSpacing() * 2 + 4};
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
    }

    void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override
    {
        if (index.column() == 0 && !index.data().isNull()) {
            QRect r1 = option.rect;
            painter->save();
            painter->setClipRect(r1);
            QStyleOptionViewItem opt(option);
            initStyleOption(&opt, index);
            int type = index.data(AbstractProjectItem::ItemTypeRole).toInt();
            QStyle *style = opt.widget ? opt.widget->style() : QApplication::style();
            const int textMargin = style->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1;
            // QRect r = QStyle::alignedRect(opt.direction, Qt::AlignVCenter | Qt::AlignLeft, opt.decorationSize, r1);

            style->drawPrimitive(QStyle::PE_PanelItemViewItem, &opt, painter, opt.widget);
190
            if ((option.state & static_cast<int>(QStyle::State_Selected)) != 0) {
191
192
193
194
195
196
197
198
199
                painter->setPen(option.palette.highlightedText().color());
            } else {
                painter->setPen(option.palette.text().color());
            }
            QRect r = r1;
            QFont font = painter->font();
            font.setBold(true);
            painter->setFont(font);
            if (type == AbstractProjectItem::ClipItem || type == AbstractProjectItem::SubClipItem) {
200
201
                int decoWidth = 0;
                if (opt.decorationSize.height() > 0) {
202
203
                    r.setWidth(r.height() * m_dar);
                    QPixmap pix = opt.icon.pixmap(opt.icon.actualSize(r.size()));
204
                    // Draw icon
205
206
207
                    decoWidth += r.width() + textMargin;
                    r.setWidth(r.height() * pix.width() / pix.height());
                    painter->drawPixmap(r, pix, QRect(0, 0, pix.width(), pix.height()));
208
                    m_thumbRect = r;
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
                }
                int mid = (int)((r1.height() / 2));
                r1.adjust(decoWidth, 0, 0, -mid);
                QRect r2 = option.rect;
                r2.adjust(decoWidth, mid, 0, 0);
                QRectF bounding;
                painter->drawText(r1, Qt::AlignLeft | Qt::AlignTop, index.data(AbstractProjectItem::DataName).toString(), &bounding);
                font.setBold(false);
                painter->setFont(font);
                QString subText = index.data(AbstractProjectItem::DataDuration).toString();
                if (!subText.isEmpty()) {
                    r2.adjust(0, bounding.bottom() - r2.top(), 0, 0);
                    QColor subTextColor = painter->pen().color();
                    subTextColor.setAlphaF(.5);
                    painter->setPen(subTextColor);
                    // Draw usage counter
                    int usage = index.data(AbstractProjectItem::UsageCount).toInt();
                    if (usage > 0) {
                        subText.append(QString().sprintf(" [%d]", usage));
                    }
                    painter->drawText(r2, Qt::AlignLeft | Qt::AlignTop, subText, &bounding);

                    // Add audio/video icons for selective drag
                    int cType = index.data(AbstractProjectItem::ClipType).toInt();
233
234
                    bool hasAudioAndVideo = index.data(AbstractProjectItem::ClipHasAudioAndVideo).toBool();
                    if (hasAudioAndVideo && (cType == ClipType::AV || cType == ClipType::Playlist) && (opt.state & QStyle::State_MouseOver)) {
235
236
237
238
239
240
241
242
243
244
245
                        bounding.moveLeft(bounding.right() + (2 * textMargin));
                        bounding.adjust(0, textMargin, 0, -textMargin);
                        QIcon aDrag = QIcon::fromTheme(QStringLiteral("audio-volume-medium"));
                        m_audioDragRect = bounding.toRect();
                        m_audioDragRect.setWidth(m_audioDragRect.height());
                        aDrag.paint(painter, m_audioDragRect, Qt::AlignLeft);
                        m_videoDragRect = m_audioDragRect;
                        m_videoDragRect.moveLeft(m_audioDragRect.right());
                        QIcon vDrag = QIcon::fromTheme(QStringLiteral("kdenlive-show-video"));
                        vDrag.paint(painter, m_videoDragRect, Qt::AlignLeft);
                    } else {
246
247
                        //m_audioDragRect = QRect();
                        //m_videoDragRect = QRect();
248
249
250
251
252
253
254
255
256
257
258
259
260
                    }
                }
                if (type == AbstractProjectItem::ClipItem) {
                    // Overlay icon if necessary
                    QVariant v = index.data(AbstractProjectItem::IconOverlay);
                    if (!v.isNull()) {
                        QIcon reload = QIcon::fromTheme(v.toString());
                        r.setTop(r.bottom() - bounding.height());
                        r.setWidth(bounding.height());
                        reload.paint(painter, r);
                    }

                    int jobProgress = index.data(AbstractProjectItem::JobProgress).toInt();
Nicolas Carion's avatar
Nicolas Carion committed
261
                    auto status = index.data(AbstractProjectItem::JobStatus).value<JobManagerStatus>();
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
                    if (status == JobManagerStatus::Pending || status == JobManagerStatus::Running) {
                        // Draw job progress bar
                        int progressWidth = option.fontMetrics.averageCharWidth() * 8;
                        int progressHeight = option.fontMetrics.ascent() / 4;
                        QRect progress(r1.x() + 1, opt.rect.bottom() - progressHeight - 2, progressWidth, progressHeight);
                        painter->setPen(Qt::NoPen);
                        painter->setBrush(Qt::darkGray);
                        if (status == JobManagerStatus::Running) {
                            painter->drawRoundedRect(progress, 2, 2);
                            painter->setBrush((option.state & static_cast<int>((QStyle::State_Selected) != 0)) != 0 ? option.palette.text()
                                                                                                                    : option.palette.highlight());
                            progress.setWidth((progressWidth - 2) * jobProgress / 100);
                            painter->drawRoundedRect(progress, 2, 2);
                        } else {
                            // Draw kind of a pause icon
                            progress.setWidth(3);
                            painter->drawRect(progress);
                            progress.moveLeft(progress.right() + 3);
                            painter->drawRect(progress);
                        }
                    }
                    bool jobsucceeded = index.data(AbstractProjectItem::JobSuccess).toBool();
                    if (!jobsucceeded) {
                        QIcon warning = QIcon::fromTheme(QStringLiteral("process-stop"));
                        warning.paint(painter, r2);
                    }
                }
            } else {
                // Folder or Folder Up items
291
292
                int decoWidth = 0;
                if (opt.decorationSize.height() > 0) {
293
294
                    r.setWidth(r.height() * m_dar);
                    QPixmap pix = opt.icon.pixmap(opt.icon.actualSize(r.size()));
295
                    // Draw icon
296
297
298
                    decoWidth += r.width() + textMargin;
                    r.setWidth(r.height() * pix.width() / pix.height());
                    painter->drawPixmap(r, pix, QRect(0, 0, pix.width(), pix.height()));
299
300
301
302
303
304
305
306
307
308
                }
                r1.adjust(decoWidth, 0, 0, 0);
                QRectF bounding;
                painter->drawText(r1, Qt::AlignLeft | Qt::AlignTop, index.data(AbstractProjectItem::DataName).toString(), &bounding);
            }
            painter->restore();
        } else {
            QStyledItemDelegate::paint(painter, option, index);
        }
    }
309
310
311
312
313
314
315
316
317
318
319
320
321
322
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
355
356
357
358

    int getFrame(QModelIndex index, int mouseX)
    {
        int type = index.data(AbstractProjectItem::ItemTypeRole).toInt();
        if ((type != AbstractProjectItem::ClipItem && type != AbstractProjectItem::SubClipItem) || mouseX < m_thumbRect.x() || mouseX > m_thumbRect.right()) {
            return 0;
        }
        return 100 * (mouseX - m_thumbRect.x()) / m_thumbRect.width();
    }

private:
    mutable bool m_editorOpen{false};
    mutable QRect m_audioDragRect;
    mutable QRect m_videoDragRect;
    mutable QRect m_thumbRect;
    double m_dar{1.778};

public:
    PlaylistState::ClipState dragType{PlaylistState::Disabled};
};

/**
 * @class BinListItemDelegate
 * @brief This class is responsible for drawing items in the QListView.
 */

class BinListItemDelegate : public QStyledItemDelegate
{
public:
    explicit BinListItemDelegate(QObject *parent = nullptr)
        : QStyledItemDelegate(parent)

    {
        connect(this, &QStyledItemDelegate::closeEditor, [&]() { m_editorOpen = false; });
    }
    void setDar(double dar) { m_dar = dar; }

    void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override
    {
        if (!index.data().isNull()) {
            QStyleOptionViewItem opt = option;
            initStyleOption(&opt, index);

            int adjust = (opt.rect.width() - opt.decorationSize.width()) / 2;
            QRect rect(0, 0, opt.rect.width(), opt.rect.height());
            m_thumbRect = adjust > 0 && adjust < rect.width() ? rect.adjusted(adjust, 0, -adjust, 0) : rect;
            QStyledItemDelegate::paint(painter, option, index);
        }
    }

359
360
361
    int getFrame(QModelIndex index, int mouseX)
    {
        int type = index.data(AbstractProjectItem::ItemTypeRole).toInt();
362
        if ((type != AbstractProjectItem::ClipItem && type != AbstractProjectItem::SubClipItem)|| mouseX < m_thumbRect.x() || mouseX > m_thumbRect.right()) {
363
364
365
366
            return 0;
        }
        return 100 * (mouseX - m_thumbRect.x()) / m_thumbRect.width();
    }
367
368

private:
Nicolas Carion's avatar
Nicolas Carion committed
369
    mutable bool m_editorOpen{false};
370
371
    mutable QRect m_audioDragRect;
    mutable QRect m_videoDragRect;
372
    mutable QRect m_thumbRect;
Nicolas Carion's avatar
Nicolas Carion committed
373
    double m_dar{1.778};
374
375

public:
Nicolas Carion's avatar
Nicolas Carion committed
376
    PlaylistState::ClipState dragType{PlaylistState::Disabled};
377
378
};

379

380
381
MyListView::MyListView(QWidget *parent)
    : QListView(parent)
382
383
384
385
{
    setViewMode(QListView::IconMode);
    setMovement(QListView::Static);
    setResizeMode(QListView::Adjust);
386
    setWordWrap(true);
387
388
389
390
391
392
393
394
395
    setDragDropMode(QAbstractItemView::DragDrop);
    setAcceptDrops(true);
    setDragEnabled(true);
    viewport()->setAcceptDrops(true);
}

void MyListView::focusInEvent(QFocusEvent *event)
{
    QListView::focusInEvent(event);
396
397
398
    if (event->reason() == Qt::MouseFocusReason) {
        emit focusView();
    }
399
400
}

401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
void MyListView::mouseMoveEvent(QMouseEvent *event)
{
    if (event->modifiers() == Qt::ShiftModifier) {
        QModelIndex index = indexAt(event->pos());
        if (index.isValid()) {
            QAbstractItemDelegate *del = itemDelegate(index);
            if (del) {
                auto delegate = static_cast<BinListItemDelegate *>(del);
                QRect vRect = visualRect(index);
                int frame = delegate->getFrame(index, event->pos().x() - vRect.x());
                emit displayBinFrame(index, frame);
            } else {
                qDebug()<<"<<< NO DELEGATE!!!";
            }
        }
    }
    QListView::mouseMoveEvent(event);
}

420
421
MyTreeView::MyTreeView(QWidget *parent)
    : QTreeView(parent)
422
423
424
{
    setEditing(false);
}
425
426
427

void MyTreeView::mousePressEvent(QMouseEvent *event)
{
428
    QTreeView::mousePressEvent(event);
429
430
    if (event->button() == Qt::LeftButton) {
        m_startPos = event->pos();
431
432
433
        QModelIndex ix = indexAt(m_startPos);
        if (ix.isValid()) {
            QAbstractItemDelegate *del = itemDelegate(ix);
434
            m_dragType = static_cast<BinItemDelegate *>(del)->dragType;
435
436
437
        } else {
            m_dragType = PlaylistState::Disabled;
        }
438
439
440
    }
}

441
442
443
void MyTreeView::focusInEvent(QFocusEvent *event)
{
    QTreeView::focusInEvent(event);
444
445
446
    if (event->reason() == Qt::MouseFocusReason) {
        emit focusView();
    }
447
448
}

449
void MyTreeView::mouseMoveEvent(QMouseEvent *event)
450
451
{
    bool dragged = false;
452
    if ((event->buttons() & Qt::LeftButton) != 0u) {
453
        int distance = (event->pos() - m_startPos).manhattanLength();
454
455
456
        if (distance >= QApplication::startDragDistance()) {
            dragged = performDrag();
        }
457
458
459
460
461
462
463
    } else if (event->modifiers() == Qt::ShiftModifier) {
        QModelIndex index = indexAt(event->pos());
        if (index.isValid()) {
            QAbstractItemDelegate *del = itemDelegate(index);
            int frame = static_cast<BinItemDelegate *>(del)->getFrame(index, event->pos().x());
            emit displayBinFrame(index, frame);
        }
464
465
466
    }
    if (!dragged) {
        QTreeView::mouseMoveEvent(event);
467
468
469
    }
}

470
471
472
473
474
475
476
477
478
479
480
481
void MyTreeView::closeEditor(QWidget *editor, QAbstractItemDelegate::EndEditHint hint)
{
    QAbstractItemView::closeEditor(editor, hint);
    setEditing(false);
}

void MyTreeView::editorDestroyed(QObject *editor)
{
    QAbstractItemView::editorDestroyed(editor);
    setEditing(false);
}

Laurent Montel's avatar
Laurent Montel committed
482
bool MyTreeView::isEditing() const
483
{
484
    return state() == QAbstractItemView::EditingState;
485
486
487
488
}

void MyTreeView::setEditing(bool edit)
{
489
    setState(edit ? QAbstractItemView::EditingState : QAbstractItemView::NoState);
490
491
}

492
493
494
495
496
497
498
499
500
bool MyTreeView::performDrag()
{
    QModelIndexList bases = selectedIndexes();
    QModelIndexList indexes;
    for (int i = 0; i < bases.count(); i++) {
        if (bases.at(i).column() == 0) {
            indexes << bases.at(i);
        }
    }
501
502
503
    if (indexes.isEmpty()) {
        return false;
    }
504
505
    // Check if we want audio or video only
    emit updateDragMode(m_dragType);
Nicolas Carion's avatar
Nicolas Carion committed
506
    auto *drag = new QDrag(this);
507
    drag->setMimeData(model()->mimeData(indexes));
Laurent Montel's avatar
Laurent Montel committed
508
    QModelIndex ix = indexes.constFirst();
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
    if (ix.isValid()) {
        QIcon icon = ix.data(AbstractProjectItem::DataThumbnail).value<QIcon>();
        QPixmap pix = icon.pixmap(iconSize());
        QSize size = pix.size();
        QImage image(size, QImage::Format_ARGB32_Premultiplied);
        image.fill(Qt::transparent);
        QPainter p(&image);
        p.setOpacity(0.7);
        p.drawPixmap(0, 0, pix);
        p.setOpacity(1);
        if (indexes.count() > 1) {
            QPalette palette;
            int radius = size.height() / 3;
            p.setBrush(palette.highlight());
            p.setPen(palette.highlightedText().color());
            p.drawEllipse(QPoint(size.width() / 2, size.height() / 2), radius, radius);
            p.drawText(size.width() / 2 - radius, size.height() / 2 - radius, 2 * radius, 2 * radius, Qt::AlignCenter, QString::number(indexes.count()));
        }
        p.end();
        drag->setPixmap(QPixmap::fromImage(image));
    }
    drag->exec();
    return true;
}

534
535
SmallJobLabel::SmallJobLabel(QWidget *parent)
    : QPushButton(parent)
Nicolas Carion's avatar
Nicolas Carion committed
536

537
538
539
540
{
    setFixedWidth(0);
    setFlat(true);
    m_timeLine = new QTimeLine(500, this);
Laurent Montel's avatar
Laurent Montel committed
541
542
    QObject::connect(m_timeLine, &QTimeLine::valueChanged, this, &SmallJobLabel::slotTimeLineChanged);
    QObject::connect(m_timeLine, &QTimeLine::finished, this, &SmallJobLabel::slotTimeLineFinished);
543
544
545
546
547
    hide();
}

const QString SmallJobLabel::getStyleSheet(const QPalette &p)
{
548
    KColorScheme scheme(p.currentColorGroup(), KColorScheme::Window);
549
550
    QColor bg = scheme.background(KColorScheme::LinkBackground).color();
    QColor fg = scheme.foreground(KColorScheme::LinkText).color();
Nicolas Carion's avatar
Nicolas Carion committed
551
552
553
554
555
556
557
558
    QString style =
        QStringLiteral("QPushButton {margin:3px;padding:2px;background-color: rgb(%1, %2, %3);border-radius: 4px;border: none;color: rgb(%4, %5, %6)}")
            .arg(bg.red())
            .arg(bg.green())
            .arg(bg.blue())
            .arg(fg.red())
            .arg(fg.green())
            .arg(fg.blue());
559

560
561
    bg = scheme.background(KColorScheme::ActiveBackground).color();
    fg = scheme.foreground(KColorScheme::ActiveText).color();
Nicolas Carion's avatar
Nicolas Carion committed
562
563
564
565
566
567
568
569
    style.append(
        QStringLiteral("\nQPushButton:hover {margin:3px;padding:2px;background-color: rgb(%1, %2, %3);border-radius: 4px;border: none;color: rgb(%4, %5, %6)}")
            .arg(bg.red())
            .arg(bg.green())
            .arg(bg.blue())
            .arg(fg.red())
            .arg(fg.green())
            .arg(fg.blue()));
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
570

571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
    return style;
}

void SmallJobLabel::setAction(QAction *action)
{
    m_action = action;
}

void SmallJobLabel::slotTimeLineChanged(qreal value)
{
    setFixedWidth(qMin(value * 2, qreal(1.0)) * sizeHint().width());
    update();
}

void SmallJobLabel::slotTimeLineFinished()
{
    if (m_timeLine->direction() == QTimeLine::Forward) {
        // Show
        m_action->setVisible(true);
    } else {
        // Hide
        m_action->setVisible(false);
        setText(QString());
    }
}

void SmallJobLabel::slotSetJobCount(int jobCount)
{
Nicolas Carion's avatar
Nicolas Carion committed
599
    QMutexLocker lk(&m_locker);
600
601
602
603
    if (jobCount > 0) {
        // prepare animation
        setText(i18np("%1 job", "%1 jobs", jobCount));
        setToolTip(i18np("%1 pending job", "%1 pending jobs", jobCount));
604

Nicolas Carion's avatar
Nicolas Carion committed
605
        if (style()->styleHint(QStyle::SH_Widget_Animate, nullptr, this) != 0) {
606
607
608
609
            setFixedWidth(sizeHint().width());
            m_action->setVisible(true);
            return;
        }
610

611
612
613
614
615
        if (m_action->isVisible()) {
            setFixedWidth(sizeHint().width());
            update();
            return;
        }
616

617
618
619
620
621
622
623
624
        setFixedWidth(0);
        m_action->setVisible(true);
        int wantedWidth = sizeHint().width();
        setGeometry(-wantedWidth, 0, wantedWidth, height());
        m_timeLine->setDirection(QTimeLine::Forward);
        if (m_timeLine->state() == QTimeLine::NotRunning) {
            m_timeLine->start();
        }
625
    } else {
Nicolas Carion's avatar
Nicolas Carion committed
626
        if (style()->styleHint(QStyle::SH_Widget_Animate, nullptr, this) != 0) {
627
628
629
630
631
632
633
634
635
636
637
638
            setFixedWidth(0);
            m_action->setVisible(false);
            return;
        }
        // hide
        m_timeLine->setDirection(QTimeLine::Backward);
        if (m_timeLine->state() == QTimeLine::NotRunning) {
            m_timeLine->start();
        }
    }
}

639
640
LineEventEater::LineEventEater(QObject *parent)
    : QObject(parent)
641
642
643
644
645
{
}

bool LineEventEater::eventFilter(QObject *obj, QEvent *event)
{
646
    switch (event->type()) {
647
648
649
650
651
652
653
654
655
656
657
    case QEvent::ShortcutOverride:
        if (((QKeyEvent *)event)->key() == Qt::Key_Escape) {
            emit clearSearchLine();
        }
        break;
    case QEvent::Resize:
        // Workaround Qt BUG 54676
        emit showClearButton(((QResizeEvent *)event)->size().width() > QFontMetrics(QApplication::font()).averageCharWidth() * 8);
        break;
    default:
        break;
658
659
660
661
    }
    return QObject::eventFilter(obj, event);
}

Nicolas Carion's avatar
Nicolas Carion committed
662
Bin::Bin(std::shared_ptr<ProjectItemModel> model, QWidget *parent)
663
664
    : QWidget(parent)
    , isLoading(false)
Nicolas Carion's avatar
Nicolas Carion committed
665
    , m_itemModel(std::move(model))
666
667
668
669
670
671
672
673
674
675
676
677
678
679
    , m_itemView(nullptr)
    , m_doc(nullptr)
    , m_extractAudioAction(nullptr)
    , m_transcodeAction(nullptr)
    , m_clipsActionsMenu(nullptr)
    , m_inTimelineAction(nullptr)
    , m_listType((BinViewType)KdenliveSettings::binMode())
    , m_iconSize(160, 90)
    , m_propertiesPanel(nullptr)
    , m_blankThumb()
    , m_invalidClipDialog(nullptr)
    , m_gainedFocus(false)
    , m_audioDuration(0)
    , m_processedAudio(0)
680
{
681
    m_layout = new QVBoxLayout(this);
682
683

    // Create toolbar for buttons
684
    m_toolbar = new QToolBar(this);
685
686
687
    int size = style()->pixelMetric(QStyle::PM_SmallIconSize);
    QSize iconSize(size, size);
    m_toolbar->setIconSize(iconSize);
688
    m_toolbar->setToolButtonStyle(Qt::ToolButtonIconOnly);
689
    m_layout->addWidget(m_toolbar);
690
691
    m_layout->setSpacing(0);
    m_layout->setContentsMargins(0, 0, 0, 0);
692
    // Search line
693
694
    m_searchLine = new QLineEdit(this);
    m_searchLine->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred);
Nicolas Carion's avatar
Nicolas Carion committed
695
    // m_searchLine->setClearButtonEnabled(true);
696
    m_searchLine->setPlaceholderText(i18n("Search..."));
697
    m_searchLine->setFocusPolicy(Qt::ClickFocus);
698

Nicolas Carion's avatar
Nicolas Carion committed
699
    auto *leventEater = new LineEventEater(this);
700
    m_searchLine->installEventFilter(leventEater);
Laurent Montel's avatar
Laurent Montel committed
701
    connect(leventEater, &LineEventEater::clearSearchLine, m_searchLine, &QLineEdit::clear);
702
    connect(leventEater, &LineEventEater::showClearButton, this, &Bin::showClearButton);
703

704
    setFocusPolicy(Qt::ClickFocus);
705

706
    connect(m_itemModel.get(), &ProjectItemModel::refreshPanel, this, &Bin::refreshPanel);
707
    connect(m_itemModel.get(), &ProjectItemModel::refreshAudioThumbs, this, &Bin::doRefreshAudioThumbs);
708
709
710
    connect(m_itemModel.get(), &ProjectItemModel::refreshClip, this, &Bin::refreshClip);
    connect(m_itemModel.get(), &ProjectItemModel::updateTimelineProducers, this, &Bin::updateTimelineProducers);
    connect(m_itemModel.get(), &ProjectItemModel::emitMessage, this, &Bin::emitMessage);
711

Nicolas Carion's avatar
Nicolas Carion committed
712
713
714
715
    connect(m_itemModel.get(), static_cast<void (ProjectItemModel::*)(const QStringList &, const QModelIndex &)>(&ProjectItemModel::itemDropped), this,
            static_cast<void (Bin::*)(const QStringList &, const QModelIndex &)>(&Bin::slotItemDropped));
    connect(m_itemModel.get(), static_cast<void (ProjectItemModel::*)(const QList<QUrl> &, const QModelIndex &)>(&ProjectItemModel::itemDropped), this,
            static_cast<void (Bin::*)(const QList<QUrl> &, const QModelIndex &)>(&Bin::slotItemDropped));
716
    connect(m_itemModel.get(), &ProjectItemModel::effectDropped, this, &Bin::slotEffectDropped);
717
    connect(m_itemModel.get(), &QAbstractItemModel::dataChanged, this, &Bin::slotItemEdited);
718
    connect(this, &Bin::refreshPanel, this, &Bin::doRefreshPanel);
719

720
    // Zoom slider
721
    QWidget *container = new QWidget(this);
Nicolas Carion's avatar
Nicolas Carion committed
722
    auto *lay = new QHBoxLayout;
723
724
    m_slider = new QSlider(Qt::Horizontal, this);
    m_slider->setMaximumWidth(100);
725
    m_slider->setMinimumWidth(40);
726
    m_slider->setRange(0, 10);
727
    m_slider->setValue(KdenliveSettings::bin_zoom());
Laurent Montel's avatar
Laurent Montel committed
728
    connect(m_slider, &QAbstractSlider::valueChanged, this, &Bin::slotSetIconSize);
Nicolas Carion's avatar
Nicolas Carion committed
729
    auto *tb1 = new QToolButton(this);
730
    tb1->setIcon(QIcon::fromTheme(QStringLiteral("zoom-in")));
731
    connect(tb1, &QToolButton::clicked, [&]() { m_slider->setValue(qMin(m_slider->value() + 1, m_slider->maximum())); });
Nicolas Carion's avatar
Nicolas Carion committed
732
    auto *tb2 = new QToolButton(this);
733
    tb2->setIcon(QIcon::fromTheme(QStringLiteral("zoom-out")));
734
    connect(tb2, &QToolButton::clicked, [&]() { m_slider->setValue(qMax(m_slider->value() - 1, m_slider->minimum())); });
735
    lay->addWidget(tb2);
736
737
    lay->addWidget(m_slider);
    lay->addWidget(tb1);
738
    container->setLayout(lay);
Nicolas Carion's avatar
Nicolas Carion committed
739
    auto *widgetslider = new QWidgetAction(this);
740
    widgetslider->setDefaultWidget(container);
741
742

    // View type
743
    KSelectAction *listType = new KSelectAction(QIcon::fromTheme(QStringLiteral("view-list-tree")), i18n("View Mode"), this);
744
    pCore->window()->actionCollection()->addAction(QStringLiteral("bin_view_mode"), listType);
745
    QAction *treeViewAction = listType->addAction(QIcon::fromTheme(QStringLiteral("view-list-tree")), i18n("Tree View"));
746
    listType->addAction(treeViewAction);
747
748
749
750
    treeViewAction->setData(BinTreeView);
    if (m_listType == treeViewAction->data().toInt()) {
        listType->setCurrentAction(treeViewAction);
    }
751
    pCore->window()->actionCollection()->addAction(QStringLiteral("bin_view_mode_tree"), treeViewAction);
752

753
    QAction *iconViewAction = listType->addAction(QIcon::fromTheme(QStringLiteral("view-list-icons")), i18n("Icon View"));
754
755
756
757
    iconViewAction->setData(BinIconView);
    if (m_listType == iconViewAction->data().toInt()) {
        listType->setCurrentAction(iconViewAction);
    }
758
    pCore->window()->actionCollection()->addAction(QStringLiteral("bin_view_mode_icon"), iconViewAction);
759
760

    QAction *disableEffects = new QAction(i18n("Disable Bin Effects"), this);
761
    connect(disableEffects, &QAction::triggered, [this](bool disable) { this->setBinEffectsEnabled(!disable); });
762
    disableEffects->setIcon(QIcon::fromTheme(QStringLiteral("favorite")));
763
764
765
    disableEffects->setData("disable_bin_effects");
    disableEffects->setCheckable(true);
    disableEffects->setChecked(false);
766
    pCore->window()->actionCollection()->addAction(QStringLiteral("disable_bin_effects"), disableEffects);
767

768
    listType->setToolBarMode(KSelectAction::MenuMode);
Nicolas Carion's avatar
Nicolas Carion committed
769
    connect(listType, static_cast<void (KSelectAction::*)(QAction *)>(&KSelectAction::triggered), this, &Bin::slotInitView);
770
771
772
773
774

    // Settings menu
    QMenu *settingsMenu = new QMenu(i18n("Settings"), this);
    settingsMenu->addAction(listType);
    QMenu *sliderMenu = new QMenu(i18n("Zoom"), this);
775
    sliderMenu->setIcon(QIcon::fromTheme(QStringLiteral("zoom-in")));
776
777
    sliderMenu->addAction(widgetslider);
    settingsMenu->addMenu(sliderMenu);
778

779
780
781
    // Column show / hide actions
    m_showDate = new QAction(i18n("Show date"), this);
    m_showDate->setCheckable(true);
Laurent Montel's avatar
Laurent Montel committed
782
    connect(m_showDate, &QAction::triggered, this, &Bin::slotShowDateColumn);
783
784
    m_showDesc = new QAction(i18n("Show description"), this);
    m_showDesc->setCheckable(true);
Laurent Montel's avatar
Laurent Montel committed
785
    connect(m_showDesc, &QAction::triggered, this, &Bin::slotShowDescColumn);
786
787
    settingsMenu->addAction(m_showDate);
    settingsMenu->addAction(m_showDesc);
788
    settingsMenu->addAction(disableEffects);
Nicolas Carion's avatar
Nicolas Carion committed
789
    auto *button = new QToolButton;
790
    button->setIcon(QIcon::fromTheme(QStringLiteral("kdenlive-menu")));
791
    button->setToolTip(i18n("Options"));
792
793
794
    button->setMenu(settingsMenu);
    button->setPopupMode(QToolButton::InstantPopup);
    m_toolbar->addWidget(button);
795

796
797
798
    // small info button for pending jobs
    m_infoLabel = new SmallJobLabel(this);
    m_infoLabel->setStyleSheet(SmallJobLabel::getStyleSheet(palette()));
799
    connect(pCore->jobManager().get(), &JobManager::jobCount, m_infoLabel, &SmallJobLabel::slotSetJobCount);
800
801
    QAction *infoAction = m_toolbar->addWidget(m_infoLabel);
    m_jobsMenu = new QMenu(this);
802
    // connect(m_jobsMenu, &QMenu::aboutToShow, this, &Bin::slotPrepareJobsMenu);
803
804
805
806
    m_cancelJobs = new QAction(i18n("Cancel All Jobs"), this);
    m_cancelJobs->setCheckable(false);
    m_discardCurrentClipJobs = new QAction(i18n("Cancel Current Clip Jobs"), this);
    m_discardCurrentClipJobs->setCheckable(false);
807
808
    m_discardPendingJobs = new QAction(i18n("Cancel Pending Jobs"), this);
    m_discardPendingJobs->setCheckable(false);
809
810
    m_jobsMenu->addAction(m_cancelJobs);
    m_jobsMenu->addAction(m_discardCurrentClipJobs);
811
    m_jobsMenu->addAction(m_discardPendingJobs);
812
813
814
    m_infoLabel->setMenu(m_jobsMenu);
    m_infoLabel->setAction(infoAction);

815
816
817
818
819
820
821
822
823
824
825
826
827
    connect(m_discardCurrentClipJobs, &QAction::triggered, [&]() {
        const QString currentId = m_monitor->activeClipId();
        if (!currentId.isEmpty()) {
            pCore->jobManager()->discardJobs(currentId);
        }
    });
    connect(m_cancelJobs, &QAction::triggered, [&]() {
        pCore->jobManager()->slotCancelJobs();
    });
    connect(m_discardPendingJobs, &QAction::triggered, [&]() {
        pCore->jobManager()->slotCancelPendingJobs();
    });

828
    // Hack, create toolbar spacer
829
    QWidget *spacer = new QWidget();
830
831
832
833
    spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
    m_toolbar->addWidget(spacer);

    // Add search line
834
    m_toolbar->addWidget(m_searchLine);
835

836
    m_binTreeViewDelegate = new BinItemDelegate(this);
837
    m_binListViewDelegate = new BinListItemDelegate(this);
Nicolas Carion's avatar
Nicolas Carion committed
838
    // connect(pCore->projectManager(), SIGNAL(projectOpened(Project*)), this, SLOT(setProject(Project*)));
839
    m_headerInfo = QByteArray::fromBase64(KdenliveSettings::treeviewheaders().toLatin1());
840
841
    m_propertiesPanel = new QScrollArea(this);
    m_propertiesPanel->setFrameShape(QFrame::NoFrame);
842
843
844
    // Insert listview
    m_itemView = new MyTreeView(this);
    m_layout->addWidget(m_itemView);
845
    // Info widget for failed jobs, other errors
846
    m_infoMessage = new KMessageWidget(this);
847
    m_layout->addWidget(m_infoMessage);
848
    m_infoMessage->setCloseButtonVisible(false);
849
    connect(m_infoMessage, &KMessageWidget::hideAnimationFinished, this, &Bin::slotResetInfoMessage);
Nicolas Carion's avatar
Nicolas Carion committed
850
    // m_infoMessage->setWordWrap(true);
851
    m_infoMessage->hide();
Vincent Pinon's avatar
Vincent Pinon committed
852
    connect(this, &Bin::requesteInvalidRemoval, this, &Bin::slotQueryRemoval);
Laurent Montel's avatar
Laurent Montel committed
853
    connect(this, SIGNAL(displayBinMessage(QString, KMessageWidget::MessageType)), this, SLOT(doDisplayMessage(QString, KMessageWidget::MessageType)));
854
    wheelAccumulatedDelta = 0;
855
856
857
858
}

Bin::~Bin()
{
859
    blockSignals(true);
860
    m_proxyModel->selectionModel()->blockSignals(true);
861
    setEnabled(false);
862
    m_propertiesPanel = nullptr;
863
    abortOperations();
864
    m_itemModel->clean();
865
866
}

867
868
869
870
871
QDockWidget *Bin::clipPropertiesDock()
{
    return m_propertiesDock;
}

872
873
void Bin::abortOperations()
{
874
    m_infoMessage->hide();
875
876
    blockSignals(true);
    if (m_propertiesPanel) {
Nicolas Carion's avatar
Nicolas Carion committed
877
        for (QWidget *w : m_propertiesPanel->findChildren<ClipPropertiesController *>()) {
878
879
880
881
            delete w;
        }
    }
    delete m_itemView;
Laurent Montel's avatar
Laurent Montel committed
882
    m_itemView = nullptr;
883
884
885
    blockSignals(false);
}

886
887
bool Bin::eventFilter(QObject *obj, QEvent *event)
{
888
    if (event->type() == QEvent::MouseButtonRelease) {
889
890
891
        if (!m_monitor->isActive()) {
            m_monitor->slotActivateMonitor();
        }
892
893
        bool success = QWidget::eventFilter(obj, event);
        if (m_gainedFocus) {
Nicolas Carion's avatar
Nicolas Carion committed
894
895
            auto *mouseEvent = static_cast<QMouseEvent *>(event);
            auto *view = qobject_cast<QAbstractItemView *>(obj->parent());
896
897
            if (view) {
                QModelIndex idx = view->indexAt(mouseEvent->pos());
898
                m_gainedFocus = false;
899
                if (idx.isValid() && m_proxyModel) {
900
                    std::shared_ptr<AbstractProjectItem> item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(idx));
901
902
903
                    editMasterEffect(item);
                } else {
                    editMasterEffect(nullptr);
904
905
906
907
908
909
                }
            }
            // make sure we discard the focus indicator
            m_gainedFocus = false;
        }
        return success;
910
911
    }
    if (event->type() == QEvent::MouseButtonDblClick) {
Nicolas Carion's avatar
Nicolas Carion committed
912
913
        auto *mouseEvent = static_cast<QMouseEvent *>(event);
        auto *view = qobject_cast<QAbstractItemView *>(obj->parent());
914
915
916
917
918
        if (view) {
            QModelIndex idx = view->indexAt(mouseEvent->pos());
            if (!idx.isValid()) {
                // User double clicked on empty area
                slotAddClip();
919
            } else {
920
921
                slotItemDoubleClicked(idx, mouseEvent->pos());
            }
922
923
        } else {
            qCDebug(KDENLIVE_LOG) << " +++++++ NO VIEW-------!!";
924
925
        }
        return true;
Nicolas Carion's avatar
Nicolas Carion committed
926
927
    }
    if (event->type() == QEvent::Wheel) {
Nicolas Carion's avatar
Nicolas Carion committed
928
        auto *e = static_cast<QWheelEvent *>(event);
Nicolas Carion's avatar
Nicolas Carion committed
929
        if ((e != nullptr) && e->modifiers() == Qt::ControlModifier) {
930
931
932
933
            wheelAccumulatedDelta += e->delta();
            if (abs(wheelAccumulatedDelta) >= QWheelEvent::DefaultDeltasPerStep) {
                slotZoomView(wheelAccumulatedDelta > 0);
            }
Nicolas Carion's avatar
Nicolas Carion committed
934
            // emit zoomView(e->delta() > 0);
935
936
937
            return true;
        }
    }
938
    return QWidget::eventFilter(obj, event);
939
940
}

941
942
943
944
945
946
void Bin::refreshIcons()
{
    QList<QMenu *> allMenus = this->findChildren<QMenu *>();
    for (int i = 0; i < allMenus.count(); i++) {
        QMenu *m = allMenus.at(i);
        QIcon ic = m->icon();
947
948
949
        if (ic.isNull() || ic.name().isEmpty()) {
            continue;
        }
950
        QIcon newIcon = QIcon::fromTheme(ic.name());
951
952
953
954
955
956
        m->setIcon(newIcon);
    }
    QList<QToolButton *> allButtons = this->findChildren<QToolButton *>();
    for (int i = 0; i < allButtons.count(); i++) {
        QToolButton *m = allButtons.at(i);
        QIcon ic = m->icon();
957
958
959
        if (ic.isNull() || ic.name().isEmpty()) {
            continue;
        }
960
        QIcon newIcon = QIcon::fromTheme(ic.name());
961
962
963
964
        m->setIcon(newIcon);
    }
}

965
966
void Bin::slotSaveHeaders()
{
Nicolas Carion's avatar
Nicolas Carion committed
967
    if ((m_itemView != nullptr) && m_listType == BinTreeView) {
968
        // save current treeview state (column width)
Nicolas Carion's avatar
Nicolas Carion committed
969
        auto *view = static_cast<QTreeView *>(m_itemView);
970
971
972
973
974
        m_headerInfo = view->header()->saveState();
        KdenliveSettings::setTreeviewheaders(m_headerInfo.toBase64());
    }
}

975
976
void Bin::slotZoomView(bool zoomIn)
{
977
    wheelAccumulatedDelta = 0;
978
    if (m_itemModel->rowCount() == 0) {
Nicolas Carion's avatar
Nicolas Carion committed
979
        // Don't zoom on empty bin
980
981
        return;
    }
Nicolas Carion's avatar
Nicolas Carion committed
982
    int progress = (zoomIn) ? 1 : -1;
983
984
985
    m_slider->setValue(m_slider->value() + progress);
}

986
987
988
989
990
Monitor *Bin::monitor()
{
    return m_monitor;
}

991
992
993
void Bin::slotAddClip()
{
    // Check if we are in a folder
994
995
    QString parentFolder = getCurrentFolder();
    ClipCreationDialog::createClipsCommand(m_doc, parentFolder, m_itemModel);
996
997
}

998
std::shared_ptr<ProjectClip> Bin::getFirstSelectedClip()
999
{
Laurent Montel's avatar
Laurent Montel committed
1000
    const QModelIndexList indexes = m_proxyModel->selectionModel()->selectedIndexes();