bin.cpp 213 KB
Newer Older
1
/*
Camille Moulin's avatar
Camille Moulin committed
2
3
SPDX-FileCopyrightText: 2012 Till Theato <root@ttill.de>
SPDX-FileCopyrightText: 2014 Jean-Baptiste Mardelle <jb@kdenlive.org>
4
5
This file is part of Kdenlive. See www.kdenlive.org.

Camille Moulin's avatar
Camille Moulin committed
6
SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
7
8
9
*/

#include "bin.h"
Nicolas Carion's avatar
Nicolas Carion committed
10
#include "bincommands.h"
11
#include "clipcreator.hpp"
Nicolas Carion's avatar
Nicolas Carion committed
12
13
#include "core.h"
#include "dialogs/clipcreationdialog.h"
14
#include "dialogs/timeremap.h"
Nicolas Carion's avatar
Nicolas Carion committed
15
16
17
#include "doc/documentchecker.h"
#include "doc/docundostack.hpp"
#include "doc/kdenlivedoc.h"
Nicolas Carion's avatar
Nicolas Carion committed
18
#include "effects/effectstack/model/effectstackmodel.hpp"
19
#include "jobs/abstracttask.h"
20
#include "jobs/cliploadtask.h"
21
22
#include "jobs/taskmanager.h"
#include "jobs/transcodetask.h"
23
#include "kdenlive_debug.h"
24
#include "kdenlivesettings.h"
Nicolas Carion's avatar
Nicolas Carion committed
25
26
27
28
29
#include "mainwindow.h"
#include "mlt++/Mlt.h"
#include "mltcontroller/clipcontroller.h"
#include "mltcontroller/clippropertiescontroller.h"
#include "monitor/monitor.h"
Julius Künzel's avatar
Julius Künzel committed
30
#include "monitor/monitormanager.h"
31
#include "project/dialogs/slideshowclip.h"
Nicolas Carion's avatar
Nicolas Carion committed
32
#include "project/invaliddialog.h"
33
#include "project/projectcommands.h"
Nicolas Carion's avatar
Nicolas Carion committed
34
#include "project/projectmanager.h"
35
#include "project/transcodeseek.h"
Nicolas Carion's avatar
Nicolas Carion committed
36
37
38
#include "projectclip.h"
#include "projectfolder.h"
#include "projectitemmodel.h"
39
#include "projectsortproxymodel.h"
Nicolas Carion's avatar
Nicolas Carion committed
40
#include "projectsubclip.h"
41
#include "tagwidget.hpp"
Nicolas Carion's avatar
Nicolas Carion committed
42
43
#include "titler/titlewidget.h"
#include "ui_qtextclip_ui.h"
Nicolas Carion's avatar
Nicolas Carion committed
44
#include "undohelper.hpp"
45
#include "xml/xml.hpp"
46
#include <dialogs/textbasededit.h>
47
48
49
#include <memory>
#include <profiles/profilemodel.hpp>
#include <utils/thumbnailcache.hpp>
50

51
#include <KActionMenu>
52
#include <KColorScheme>
53
#include <KIO/OpenFileManagerWindowJob>
54
#include <KIconEffect>
55
56
57
58
#include <KIconTheme>
#include <KMessageBox>
#include <KRatingPainter>
#include <KXMLGUIFactory>
59
#include <kwidgetsaddons_version.h>
60

61
#include <QActionGroup>
Nicolas Carion's avatar
Nicolas Carion committed
62
#include <QCryptographicHash>
63
#include <QDrag>
Nicolas Carion's avatar
Nicolas Carion committed
64
#include <QFile>
65
#include <QMenu>
Nicolas Carion's avatar
Nicolas Carion committed
66
67
#include <QSlider>
#include <QTimeLine>
68
#include <QToolBar>
69
#include <QUndoCommand>
Nicolas Carion's avatar
Nicolas Carion committed
70
71
#include <QUrl>
#include <QVBoxLayout>
72
#include <jobs/audiolevelstask.h>
73
#include <utility>
74

75
76
77
78
static QImage m_videoIcon;
static QImage m_audioIcon;
static QImage m_audioUsedIcon;
static QImage m_videoUsedIcon;
79
80
static QSize m_iconSize;
static QIcon m_folderIcon;
81

82
83
84
85
86
87
88
89
90
/**
 * @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
91

92
    {
93
        connect(this, &QStyledItemDelegate::closeEditor, [&]() { m_editorOpen = false; });
94
    }
95
96
97
98
99
100
101
    void setEditorData(QWidget *w, const QModelIndex &i) const override
    {
        if (!m_editorOpen) {
            QStyledItemDelegate::setEditorData(w, i);
            m_editorOpen = true;
        }
    }
102
    bool editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index) override
103
104
    {
        if (event->type() == QEvent::MouseButtonPress) {
105
            auto *me = static_cast<QMouseEvent *>(event);
106
            if (index.column() == 0) {
107
108
                QPoint pos = me->pos();
                if (m_audioDragRect.contains(pos)) {
109
                    dragType = PlaylistState::AudioOnly;
110
                } else if (m_videoDragRect.contains(pos)) {
111
112
113
114
                    dragType = PlaylistState::VideoOnly;
                } else {
                    dragType = PlaylistState::Disabled;
                }
115
116
            } else {
                dragType = PlaylistState::Disabled;
117
118
                if (index.column() == 7) {
                    // Rating
119
                    QRect rect = option.rect;
120
                    rect.adjust(option.rect.width() / 12, 0, 0, 0);
121
122
                    int rate = 0;
                    if (me->pos().x() > rect.x()) {
123
                        rate = KRatingPainter::getRatingFromPosition(rect, Qt::AlignLeft | Qt::AlignVCenter, qApp->layoutDirection(), me->pos());
124
                    }
125
126
127
128
129
                    if (rate > -1) {
                        // Full star rating only
                        if (rate %2 == 1) {
                            rate++;
                        }
130
                        emit static_cast<ProjectSortProxyModel *>(model)->updateRating(index, uint(rate));
131
132
                    }
                }
133
134
135
136
137
138
139
140
            }
        }
        event->ignore();
        return false;
    }
    void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const override
    {
        if (index.column() != 0) {
141
142
            QStyledItemDelegate::updateEditorGeometry(editor, option, index);
            return;
143
144
145
146
147
        }
        QStyleOptionViewItem opt = option;
        initStyleOption(&opt, index);
        QRect r1 = option.rect;
        int type = index.data(AbstractProjectItem::ItemTypeRole).toInt();
148
        int decoWidth = 0;
149
        if (opt.decorationSize.height() > 0) {
Vincent Pinon's avatar
Vincent Pinon committed
150
            decoWidth += int(r1.height() * pCore->getCurrentDar());
151
        }
152
        int mid = 0;
153
        if (type == AbstractProjectItem::ClipItem || type == AbstractProjectItem::SubClipItem) {
154
            mid = int((r1.height() / 2));
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
        }
        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;
174
        int width = int(fm.boundingRect(r, Qt::AlignLeft | Qt::AlignTop, text).width() + option.decorationSize.width()) + 2 * textMargin;
175
176
        hint.setWidth(width);
        int type = index.data(AbstractProjectItem::ItemTypeRole).toInt();
177
        if (type == AbstractProjectItem::FolderItem) {
178
179
180
181
182
183
            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) {
184
            return QSize(hint.width(), qMax(option.fontMetrics.lineSpacing() * 2 + 4, qMin(hint.height(), int(option.decorationSize.height() / 1.5))));
185
186
187
188
189
        }
        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
190
        int textW = qMax(option.fontMetrics.horizontalAdvance(line1), option.fontMetrics.horizontalAdvance(line2));
191
        QSize iconSize = icon.actualSize(option.decorationSize);
Nicolas Carion's avatar
Nicolas Carion committed
192
        return {qMax(textW, iconSize.width()) + 4, option.fontMetrics.lineSpacing() * 2 + 4};
193
194
195
196
197
198
199
200
201
202
203
204
205
206
    }

    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);
207
            // Draw alternate background
208
            style->drawPrimitive(QStyle::PE_PanelItemViewItem, &opt, painter, opt.widget);
209
            if ((option.state & static_cast<int>(QStyle::State_Selected)) != 0) {
210
211
212
213
214
215
216
217
218
                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) {
219
                int decoWidth = 0;
220
                FileStatus::ClipStatus clipStatus = FileStatus::ClipStatus(index.data(AbstractProjectItem::ClipStatus).toInt());
221
                if (opt.decorationSize.height() > 0) {
222
                    r.setWidth(int(r.height() * pCore->getCurrentDar()));
223
                    QPixmap pix = opt.icon.pixmap(opt.icon.actualSize(r.size()));
224
225
226
227
228
229
                    if (!pix.isNull()) {
                        // Draw icon
                        decoWidth += r.width() + textMargin;
                        r.setWidth(r.height() * pix.width() / pix.height());
                        painter->drawPixmap(r, pix, QRect(0, 0, pix.width(), pix.height()));
                    }
230
                    m_thumbRect = r;
231
                }
232
                // Draw frame in case of missing source
233
                int cType = index.data(AbstractProjectItem::ClipType).toInt();
234
                if (clipStatus == FileStatus::StatusMissing || clipStatus == FileStatus::StatusProxyOnly) {
235
                    painter->save();
236
                    painter->setPen(QPen(clipStatus == FileStatus::StatusProxyOnly ? Qt::yellow : Qt::red, 3));
237
                    painter->drawRect(m_thumbRect.adjusted(0, 0, -1, -1));
238
                    painter->restore();
239
240
241
242
243
244
245
246
247
248
249
                } else if (cType == ClipType::Image || cType == ClipType::SlideShow) {
                    // Draw 'photo' frame to identify image clips
                    painter->save();
                    int penWidth = m_thumbRect.height() / 14;
                    penWidth += penWidth % 2;
                    painter->setPen(QPen(QColor(255, 255, 255, 160), penWidth));
                    penWidth /= 2;
                    painter->drawRoundedRect(m_thumbRect.adjusted(penWidth, penWidth, -penWidth - 1, -penWidth - 1), 4, 4);
                    painter->setPen(QPen(Qt::black, 1));
                    painter->drawRoundedRect(m_thumbRect.adjusted(0, 0, -1, -1), 4, 4);
                    painter->restore();
250
                }
251
                int mid = int((r1.height() / 2));
252
253
254
255
256
257
258
259
                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();
260
261
262
                QString tags = index.data(AbstractProjectItem::DataTag).toString();
                if (!tags.isEmpty()) {
                    QStringList t = tags.split(QLatin1Char(';'));
263
264
265
                    QRectF tagRect = m_thumbRect.adjusted(2, 2, 0, 2);
                    tagRect.setWidth(r1.height() / 3.5);
                    tagRect.setHeight(tagRect.width());
Vincent Pinon's avatar
Vincent Pinon committed
266
                    for (const QString &color : qAsConst(t)) {
267
268
269
                        painter->setBrush(QColor(color));
                        painter->drawRoundedRect(tagRect, tagRect.height() / 2, tagRect.height() / 2);
                        tagRect.moveTop(tagRect.bottom() + tagRect.height() / 4);
270
                    }
271
                    painter->setBrush(Qt::NoBrush);
272
                }
273
                if (!subText.isEmpty()) {
274
                    r2.adjust(0, int(bounding.bottom() - r2.top()), 0, 0);
275
                    QColor subTextColor = painter->pen().color();
276
277
278
279
                    bool selected = opt.state & QStyle::State_Selected;
                    if (!selected) {
                        subTextColor.setAlphaF(.5);
                    }
280
281
282
283
                    painter->setPen(subTextColor);
                    // Draw usage counter
                    int usage = index.data(AbstractProjectItem::UsageCount).toInt();
                    if (usage > 0) {
Laurent Montel's avatar
Laurent Montel committed
284
                        subText.append(QString::asprintf(" [%d]", usage));
285
286
                    }
                    painter->drawText(r2, Qt::AlignLeft | Qt::AlignTop, subText, &bounding);
287
                    // Add audio/video icons for selective drag
288
                    bool hasAudioAndVideo = index.data(AbstractProjectItem::ClipHasAudioAndVideo).toBool();
289
                    if (hasAudioAndVideo && (cType == ClipType::AV || cType == ClipType::Playlist)) {
290
291
292
293
294
                        QRect audioRect(0, 0, m_audioIcon.width(), m_audioIcon.height());
                        audioRect.moveLeft(bounding.right() + (2 * textMargin) + 1);
                        audioRect.moveTop(bounding.top() + 1);
                        QRect videoIconRect = audioRect;
                        videoIconRect.moveLeft(audioRect.right() + (2 * textMargin));
295
                        if (opt.state & QStyle::State_MouseOver) {
296
297
298
299
                            m_audioDragRect = audioRect.adjusted(-1, -1, 1, 1);
                            m_videoDragRect = videoIconRect.adjusted(-1, -1, 1, 1);
                            painter->drawImage(audioRect.topLeft(), m_audioIcon);
                            painter->drawImage(videoIconRect.topLeft(), m_videoIcon);
300
301
302
                            painter->setPen(opt.palette.highlight().color());
                            painter->drawRect(m_audioDragRect);
                            painter->drawRect(m_videoDragRect);
303
304
305
                        } else if (usage > 0) {
                            int audioUsage = index.data(AbstractProjectItem::AudioUsageCount).toInt();
                            if (audioUsage > 0) {
306
                                painter->drawImage(audioRect.topLeft(), selected ? m_audioIcon : m_audioUsedIcon);
307
308
                            }
                            if (usage - audioUsage > 0) {
309
                                painter->drawImage(videoIconRect.topLeft(), selected ? m_videoIcon : m_videoUsedIcon);
310
311
                            }
                        }
312
                    } /*else if (usage > 0) {
313
314
315
316
317
318
319
320
321
322
323
                        QRect audioRect(0, 0, m_audioIcon.width(), m_audioIcon.height());
                        audioRect.moveLeft(bounding.right() + (2 * textMargin) + 1);
                        audioRect.moveTop(bounding.top() + 1);
                        QRect videoIconRect = audioRect;
                        videoIconRect.moveLeft(audioRect.right() + (2 * textMargin));
                        int audioUsage = index.data(AbstractProjectItem::AudioUsageCount).toInt();
                        if (audioUsage > 0) {
                            painter->drawImage(audioRect.topLeft(), selected ? m_audioIcon : m_audioUsedIcon);
                        }
                        if (usage - audioUsage > 0) {
                            painter->drawImage(videoIconRect.topLeft(), selected ? m_videoIcon : m_videoUsedIcon);
324
                        }
325
                    }*/
326
327
328
329
330
331
                }
                if (type == AbstractProjectItem::ClipItem) {
                    // Overlay icon if necessary
                    QVariant v = index.data(AbstractProjectItem::IconOverlay);
                    if (!v.isNull()) {
                        QIcon reload = QIcon::fromTheme(v.toString());
332
333
                        int size = style->pixelMetric(QStyle::PM_SmallIconSize);
                        reload.paint(painter, QRect(r.left() + 2, r.bottom() - size - 2, size, size));
334
335
                    }
                    int jobProgress = index.data(AbstractProjectItem::JobProgress).toInt();
336
337
                    auto status = index.data(AbstractProjectItem::JobStatus).value<TaskManagerStatus>();
                    if (status == TaskManagerStatus::Pending || status == TaskManagerStatus::Running) {
338
339
340
341
342
343
                        // 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);
344
                        if (status == TaskManagerStatus::Running) {
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
                            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 {
365
                // Folder
366
367
                int decoWidth = 0;
                if (opt.decorationSize.height() > 0) {
368
                    r.setWidth(int(r.height() * pCore->getCurrentDar()));
369
                    QPixmap pix = m_folderIcon.pixmap(m_folderIcon.actualSize(r.size()));
370
                    // Draw icon
371
372
373
                    decoWidth += r.width() + textMargin;
                    r.setWidth(r.height() * pix.width() / pix.height());
                    painter->drawPixmap(r, pix, QRect(0, 0, pix.width(), pix.height()));
374
375
376
377
378
379
                }
                r1.adjust(decoWidth, 0, 0, 0);
                QRectF bounding;
                painter->drawText(r1, Qt::AlignLeft | Qt::AlignTop, index.data(AbstractProjectItem::DataName).toString(), &bounding);
            }
            painter->restore();
380
        } else if (index.column() == 7) {
381
            // Rating
382
383
384
            QStyleOptionViewItem opt(option);
            initStyleOption(&opt, index);
            QRect r1 = opt.rect;
385
386
            // Tweak bg opacity since breeze dark star has same color as highlighted background
            painter->setOpacity(0.5);
387
388
            QStyle *style = opt.widget ? opt.widget->style() : QApplication::style();
            style->drawPrimitive(QStyle::PE_PanelItemViewItem, &opt, painter, opt.widget);
389
            painter->setOpacity(1);
390
            if (index.data(AbstractProjectItem::ItemTypeRole).toInt() != AbstractProjectItem::FolderItem) {
391
                r1.adjust(r1.width() / 12, 0, 0, 0);
392
                KRatingPainter::paintRating(painter, r1, Qt::AlignLeft | Qt::AlignVCenter, index.data().toInt());
393
            }
394
395
396
397
        } else {
            QStyledItemDelegate::paint(painter, option, index);
        }
    }
398

399
    int getFrame(const QModelIndex &index, int mouseX)
400
401
    {
        int type = index.data(AbstractProjectItem::ItemTypeRole).toInt();
402
        if ((type != AbstractProjectItem::ClipItem && type != AbstractProjectItem::SubClipItem)) {
403
404
            return 0;
        }
405
406
407
        if (mouseX < m_thumbRect.x() || mouseX > m_thumbRect.right()) {
            return -1;
        }
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
        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;

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

/**
 * @class BinListItemDelegate
423
 * @brief This class is responsible for drawing items in the QListView (Icon view).
424
425
426
427
428
429
430
431
432
433
434
 */

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

    {
        connect(this, &QStyledItemDelegate::closeEditor, [&]() { m_editorOpen = false; });
    }
435
436
437
438
439
440
    bool editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index) override
    {
        Q_UNUSED(model);
        Q_UNUSED(option);
        Q_UNUSED(index);
        if (event->type() == QEvent::MouseButtonPress) {
441
            auto *me = static_cast<QMouseEvent *>(event);
442
443
444
445
446
447
448
449
450
451
452
            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;
    }
453
454
455
456

    void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override
    {
        if (!index.data().isNull()) {
457
            QStyleOptionViewItem opt(option);
458
            initStyleOption(&opt, index);
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
            //QStyledItemDelegate::paint(painter, opt, index);
            QStyle *style = opt.widget ? opt.widget->style() : QApplication::style();
            style->drawPrimitive(QStyle::PE_PanelItemViewItem, &opt, painter, opt.widget);
            //style->drawControl(QStyle::CE_ItemViewItem, &opt, painter, opt.widget);
            QRect r = opt.rect;
            r.setHeight(r.width() / pCore->getCurrentDar());
            int type = index.data(AbstractProjectItem::ItemTypeRole).toInt();
            bool isFolder = type == AbstractProjectItem::FolderItem;
            QPixmap pix = isFolder ? m_folderIcon.pixmap(m_folderIcon.actualSize(r.size())) : opt.icon.pixmap(opt.icon.actualSize(r.size()));
            if (!pix.isNull()) {
                // Draw icon
                painter->drawPixmap(r, pix, QRect(0, 0, pix.width(), pix.height()));
            }
            m_thumbRect = r;
            QRect textRect = opt.rect;
            const int textMargin = style->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1;
            textRect.adjust(textMargin, 0, -textMargin, -textMargin);
            QRectF bounding;
            QString itemText = index.data(AbstractProjectItem::DataName).toString();
478
            // Draw usage counter
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
            int usage = isFolder ? 0 : index.data(AbstractProjectItem::UsageCount).toInt();
            if (usage > 0) {
                int usageWidth = option.fontMetrics.horizontalAdvance(QString::asprintf(" [%d]", usage));
                int availableWidth = textRect.width() - usageWidth;
                if (option.fontMetrics.horizontalAdvance(itemText) > availableWidth) {
                    itemText = option.fontMetrics.elidedText(itemText, Qt::ElideRight, availableWidth);
                }
                itemText.append(QString::asprintf(" [%d]", usage));
            } else {
                if (option.fontMetrics.horizontalAdvance(itemText) > textRect.width()) {
                    itemText = option.fontMetrics.elidedText(itemText, Qt::ElideRight, textRect.width());
                }
            }
            painter->drawText(textRect, Qt::AlignCenter | Qt::AlignBottom, itemText, &bounding);

            if (isFolder) {
                return;
496
            }
497

498
499
500
501
502
503
504
            //Tags
            QString tags = index.data(AbstractProjectItem::DataTag).toString();
            if (!tags.isEmpty()) {
                QStringList t = tags.split(QLatin1Char(';'));
                QRectF tagRect = m_thumbRect.adjusted(2, 2, 0, 2);
                tagRect.setWidth(m_thumbRect.height() / 5);
                tagRect.setHeight(tagRect.width());
505
                painter->save();
Vincent Pinon's avatar
Vincent Pinon committed
506
                for (const QString &color : qAsConst(t)) {
507
508
509
510
                    painter->setBrush(QColor(color));
                    painter->drawRoundedRect(tagRect, tagRect.height() / 2, tagRect.height() / 2);
                    tagRect.moveTop(tagRect.bottom() + tagRect.height() / 4);
                }
511
                painter->restore();
512
            }
513

514
            // Add audio/video icons for selective drag
515
516
            int cType = index.data(AbstractProjectItem::ClipType).toInt();
            bool hasAudioAndVideo = index.data(AbstractProjectItem::ClipHasAudioAndVideo).toBool();
517
518
            if (hasAudioAndVideo && (cType == ClipType::AV || cType == ClipType::Playlist) && m_thumbRect.height() > 2.5 * m_audioIcon.height()) {
                QRect thumbRect = m_thumbRect;
519
                thumbRect.setLeft(opt.rect.right() - m_audioIcon.width() - 6);
520
                if (opt.state & QStyle::State_MouseOver || usage > 0) {
521
522
523
524
                    QColor bgColor = option.palette.window().color();
                    bgColor.setAlphaF(.7);
                    painter->fillRect(thumbRect, bgColor);
                }
525
                thumbRect.setSize(m_audioIcon.size());
526
                thumbRect.translate(3, 2);
527
528
                QRect videoThumbRect = thumbRect;
                videoThumbRect.moveTop(thumbRect.bottom() + 2);
529
                if (opt.state & QStyle::State_MouseOver) {
530
531
                    m_audioDragRect = thumbRect;
                    m_videoDragRect = videoThumbRect;
532
533
534
535
536
                    painter->drawImage(m_audioDragRect.topLeft(), m_audioIcon);
                    painter->drawImage(m_videoDragRect.topLeft(), m_videoIcon);
                } else if (usage > 0) {
                    int audioUsage = index.data(AbstractProjectItem::AudioUsageCount).toInt();
                    if (audioUsage > 0) {
537
                        painter->drawImage(thumbRect.topLeft(), m_audioUsedIcon);
538
539
                    }
                    if (usage - audioUsage > 0) {
540
                        painter->drawImage(videoThumbRect.topLeft(), m_videoUsedIcon);
541
542
                    }
                }
543
            }
544
            // Draw frame in case of missing source
545
            FileStatus::ClipStatus clipStatus = FileStatus::ClipStatus(index.data(AbstractProjectItem::ClipStatus).toInt());
546
            if (clipStatus == FileStatus::StatusMissing || clipStatus == FileStatus::StatusProxyOnly) {
547
                painter->save();
548
                painter->setPen(QPen(clipStatus == FileStatus::StatusProxyOnly ? Qt::yellow : Qt::red, 3));
549
550
551
552
553
554
555
556
557
558
559
560
561
562
                painter->drawRect(m_thumbRect);
                painter->restore();
            } else if (cType == ClipType::Image || cType == ClipType::SlideShow) {
                // Draw 'photo' frame to identify image clips
                painter->save();
                int penWidth = m_thumbRect.height() / 14;
                penWidth += penWidth % 2;
                painter->setPen(QPen(QColor(255, 255, 255, 160), penWidth));
                penWidth /= 2;
                painter->drawRoundedRect(m_thumbRect.adjusted(penWidth, penWidth, -penWidth - 1, -penWidth + 1), 4, 4);
                painter->setPen(QPen(Qt::black, 1));
                painter->drawRoundedRect(m_thumbRect.adjusted(0, 0, -1, 1), 4, 4);
                painter->restore();
            }
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
            // Overlay icon if necessary
            QVariant v = index.data(AbstractProjectItem::IconOverlay);
            if (!v.isNull()) {
                QRect r = m_thumbRect;
                QIcon reload = QIcon::fromTheme(v.toString());
                r.setTop(r.bottom() - (opt.rect.height() - r.height()));
                r.setWidth(r.height());
                reload.paint(painter, r);
            }
            int jobProgress = index.data(AbstractProjectItem::JobProgress).toInt();
            auto status = index.data(AbstractProjectItem::JobStatus).value<TaskManagerStatus>();
            if (status == TaskManagerStatus::Pending || status == TaskManagerStatus::Running) {
                // Draw job progress bar
                int progressHeight = option.fontMetrics.ascent() / 4;
                QRect thumbRect = m_thumbRect.adjusted(2, 2, -2, -2);
                QRect progress(thumbRect.x(), thumbRect.bottom() - progressHeight - 2, thumbRect.width(), progressHeight);
                painter->setPen(Qt::NoPen);
                painter->setBrush(Qt::darkGray);
                if (status == TaskManagerStatus::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((thumbRect.width() - 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);
593
594
                }
            }
595
596
597
598
599
600
            bool jobsucceeded = index.data(AbstractProjectItem::JobSuccess).toBool();
            if (!jobsucceeded) {
                QIcon warning = QIcon::fromTheme(QStringLiteral("process-stop"));
                QRect thumbRect = m_thumbRect.adjusted(2, 2, 2, 2);
                warning.paint(painter, thumbRect);
            }
601
602
603
        }
    }

604
    int getFrame(const QModelIndex &index, QPoint pos)
605
606
    {
        int type = index.data(AbstractProjectItem::ItemTypeRole).toInt();
607
        if ((type != AbstractProjectItem::ClipItem && type != AbstractProjectItem::SubClipItem) || !m_thumbRect.contains(pos)) {
608
609
            return 0;
        }
610
        return 100 * (pos.x() - m_thumbRect.x()) / m_thumbRect.width();
611
    }
612

613
614
615
616
617
618
    QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& ) const override
    {
        int textHeight = int(option.fontMetrics.height() * 1.5);
        return (QSize(m_iconSize.width(), m_iconSize.height() + textHeight));
    }

619
private:
Nicolas Carion's avatar
Nicolas Carion committed
620
    mutable bool m_editorOpen{false};
621
622
    mutable QRect m_audioDragRect;
    mutable QRect m_videoDragRect;
623
    mutable QRect m_thumbRect;
624
625

public:
Nicolas Carion's avatar
Nicolas Carion committed
626
    PlaylistState::ClipState dragType{PlaylistState::Disabled};
627
628
};

629

630
631
MyListView::MyListView(QWidget *parent)
    : QListView(parent)
632
633
634
635
{
    setViewMode(QListView::IconMode);
    setMovement(QListView::Static);
    setResizeMode(QListView::Adjust);
636
    setWordWrap(true);
637
    setDragDropMode(QAbstractItemView::DragDrop);
638
    setUniformItemSizes(true);
639
    setDragEnabled(true);
640
    setAcceptDrops(true);
641
    //setDropIndicatorShown(true);
642
643
644
645
646
647
    viewport()->setAcceptDrops(true);
}

void MyListView::focusInEvent(QFocusEvent *event)
{
    QListView::focusInEvent(event);
648
649
650
    if (event->reason() == Qt::MouseFocusReason) {
        emit focusView();
    }
651
652
}

653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
void MyListView::dropEvent(QDropEvent *event)
{
    if (event->mimeData()->hasFormat(QStringLiteral("kdenlive/producerslist"))) {
        // Internal drag/drop, ensure it is not a zone drop
        if (!QString(event->mimeData()->data(QStringLiteral("kdenlive/producerslist"))).contains(QLatin1Char('/'))) {
            bool isSameRoot = false;
            QString rootId = QString(event->mimeData()->data(QStringLiteral("kdenlive/rootId")));
            if (rootIndex().data(AbstractProjectItem::DataId).toString() == rootId) {
                isSameRoot = true;
            }
            if (isSameRoot && !indexAt(event->pos()).isValid()) {
                event->ignore();
                return;
            }
        }
    }
    QListView::dropEvent(event);
}

672
673
674
void MyListView::enterEvent(QEvent *event)
{
    QListView::enterEvent(event);
675
    pCore->setWidgetKeyBinding(i18n("<b>Double click</b> to add a file to the project"));
676
677
678
679
680
}

void MyListView::leaveEvent(QEvent *event)
{
    QListView::leaveEvent(event);
681
    pCore->setWidgetKeyBinding();
682
683
}

684
685
686
687
void MyListView::mousePressEvent(QMouseEvent *event)
{
    QListView::mousePressEvent(event);
    if (event->button() == Qt::LeftButton) {
688
        QModelIndex ix = indexAt(event->pos());
689
690
691
        if (ix.isValid()) {
            QAbstractItemDelegate *del = itemDelegate(ix);
            m_dragType = static_cast<BinListItemDelegate *>(del)->dragType;
692
            m_startPos = event->pos();
693
694
        } else {
            m_dragType = PlaylistState::Disabled;
695
            m_startPos = QPoint();
696
697
698
        }
        emit updateDragMode(m_dragType);
    }
699
    event->accept();
700
701
}

702
703
void MyListView::mouseMoveEvent(QMouseEvent *event)
{
704
705
706
707
708
709
    if ((event->buttons() & Qt::LeftButton) != 0u) {
        if (!m_startPos.isNull() && (event->pos() - m_startPos).manhattanLength() > QApplication::startDragDistance()) {
            QModelIndexList indexes = selectedIndexes();
            if (indexes.isEmpty()) {
                // Dragging from empty zone, abort
                return;
710
            }
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
            auto *drag = new QDrag(this);
            drag->setMimeData(model()->mimeData(indexes));
            QModelIndex ix = indexes.constFirst();
            if (ix.isValid()) {
                QIcon icon = ix.data(AbstractProjectItem::DataThumbnail).value<QIcon>();
                QPixmap pix = icon.pixmap(iconSize());
                QSize size = pix.size() / 2;
                QImage image(size, QImage::Format_ARGB32_Premultiplied);
                image.fill(Qt::transparent);
                QPainter p(&image);
                p.setOpacity(0.7);
                p.drawPixmap(0, 0, image.width(), image.height(), 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();
            emit processDragEnd();
737
        }
738
739
        return;
    }
740
741
742
    QModelIndex index = indexAt(event->pos());
    if (index.isValid()) {
        if (KdenliveSettings::hoverPreview()) {
743
            QAbstractItemDelegate *del = itemDelegate(index);
744
745
746
            if (del) {
                auto delegate = static_cast<BinListItemDelegate *>(del);
                QRect vRect = visualRect(index);
747
                if (vRect.contains(event->pos())) {
748
749
750
751
752
753
                    if (m_lastHoveredItem != index) {
                        if (m_lastHoveredItem.isValid()) {
                            emit displayBinFrame(m_lastHoveredItem, -1);
                        }
                        m_lastHoveredItem = index;
                    }
754
                    int frame = delegate->getFrame(index, event->pos());
755
                    emit displayBinFrame(index, frame, event->modifiers() & Qt::ShiftModifier);
756
757
758
                } else if (m_lastHoveredItem == index) {
                    emit displayBinFrame(m_lastHoveredItem, -1);
                    m_lastHoveredItem = QModelIndex();
759
                }
760
            } else {
761
762
                if (m_lastHoveredItem.isValid()) {
                    emit displayBinFrame(m_lastHoveredItem, -1);
763
                    m_lastHoveredItem = QModelIndex();
764
765
                }
            }
766
            pCore->bin()->updateKeyBinding(i18n("<b>Shift+seek</b> over thumbnail to set default thumbnail, <b>F2</b> to rename selected item"));
767
        } else {
768
            pCore->bin()->updateKeyBinding(i18n("<b>F2</b> to rename selected item"));
769
        }
770
    } else {
771
        pCore->bin()->updateKeyBinding();
772
773
774
775
        if (m_lastHoveredItem.isValid()) {
            emit displayBinFrame(m_lastHoveredItem, -1);
            m_lastHoveredItem = QModelIndex();
        }
776
777
778
779
    }
    QListView::mouseMoveEvent(event);
}

780
781
MyTreeView::MyTreeView(QWidget *parent)
    : QTreeView(parent)
782
783
{
    setEditing(false);
784
    setAcceptDrops(true);
785
}
786
787
788

void MyTreeView::mousePressEvent(QMouseEvent *event)
{
789
    QTreeView::mousePressEvent(event);
790
    if (event->button() == Qt::LeftButton) {
791
        QModelIndex ix = indexAt(event->pos());
792
793
        if (ix.isValid()) {
            QAbstractItemDelegate *del = itemDelegate(ix);
794
            m_dragType = static_cast<BinItemDelegate *>(del)->dragType;
795
            m_startPos = event->pos();
796
797
        } else {
            m_dragType = PlaylistState::Disabled;
798
            m_startPos = QPoint();
799
        }
800
    }
801
    event->accept();
802
803
}

804
805
806
void MyTreeView::focusInEvent(QFocusEvent *event)
{
    QTreeView::focusInEvent(event);
807
808
809
    if (event->reason() == Qt::MouseFocusReason) {
        emit focusView();
    }
810
811
}

812
813
814
void MyTreeView::enterEvent(QEvent *event)
{
    QTreeView::enterEvent(event);
815
    pCore->setWidgetKeyBinding(i18n("<b>Double click</b> to add a file to the project"));
816
817
818
819
820
}

void MyTreeView::leaveEvent(QEvent *event)
{
    QTreeView::leaveEvent(event);
821
    pCore->setWidgetKeyBinding();
822
823
}

824
void MyTreeView::mouseMoveEvent(QMouseEvent *event)
825
826
{
    bool dragged = false;
827
    if ((event->buttons() & Qt::LeftButton) != 0u) {
828
829
830
831
832
        if (!m_startPos.isNull()) {
            int distance = (event->pos() - m_startPos).manhattanLength();
            if (distance >= QApplication::startDragDistance()) {
                dragged = performDrag();
            }
833
        }
834
        return;
835
    } else {
836
837
        QModelIndex index = indexAt(event->pos());
        if (index.isValid()) {
838
839
840
            if (KdenliveSettings::hoverPreview()) {
                QAbstractItemDelegate *del = itemDelegate(index);
                int frame = static_cast<BinItemDelegate *>(del)->getFrame(index, event->pos().x());
841
842
843
844
845
846
847
848
                if (frame >= 0) {
                    emit displayBinFrame(index, frame, event->modifiers() & Qt::ShiftModifier);
                    if (m_lastHoveredItem != index) {
                        if (m_lastHoveredItem.isValid()) {
                            emit displayBinFrame(m_lastHoveredItem, -1);
                        }
                        m_lastHoveredItem = index;
                    }
849
850
851
                } else if (m_lastHoveredItem.isValid()) {
                    emit displayBinFrame(m_lastHoveredItem, -1);
                    m_lastHoveredItem = QModelIndex();
852
                }
853
                pCore->bin()->updateKeyBinding(i18n("<b>Shift+seek</b> over thumbnail to set default thumbnail, <b>F2</b> to rename selected item"));
854
            } else {
855
                pCore->bin()->updateKeyBinding(i18n("<b>F2</b> to rename selected item"));
856
857
            }
        } else {
858
859
860
861
            if (m_lastHoveredItem.isValid()) {
                emit displayBinFrame(m_lastHoveredItem, -1);
                m_lastHoveredItem = QModelIndex();
            }
862
            pCore->bin()->updateKeyBinding();
863
        }
864
865
866
    }
    if (!dragged) {
        QTreeView::mouseMoveEvent(event);
867
868
869
    }
}

870
871
872
873
874
875
876
877
878
879
880
881
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
882
bool MyTreeView::isEditing() const
883
{
884
    return state() == QAbstractItemView::EditingState;
885
886
887
888
}

void MyTreeView::setEditing(bool edit)
{
889
    setState(edit ? QAbstractItemView::EditingState : QAbstractItemView::NoState);
890
891
892
893
    if (!edit) {
        // Ensure edited item is selected
        emit selectCurrent();
    }
894
895
}

896
897
898
899
900
901
902
903
904
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);
        }
    }
905
906
907
    if (indexes.isEmpty()) {
        return false;
    }
908
909
    // Check if we want audio or video only
    emit updateDragMode(m_dragType);
Nicolas Carion's avatar
Nicolas Carion committed
910
    auto *drag = new QDrag(this);
911
    drag->setMimeData(model()->mimeData(indexes));
Laurent Montel's avatar
Laurent Montel committed
912
    QModelIndex ix = indexes.constFirst();
913
914
915
    if (ix.isValid()) {
        QIcon icon = ix.data(AbstractProjectItem::DataThumbnail).value<QIcon>();
        QPixmap pix = icon.pixmap(iconSize());
916
        QSize size = pix.size() / 2;
917
918
919
920
        QImage image(size, QImage::Format_ARGB32_Premultiplied);
        image.fill(Qt::transparent);
        QPainter p(&image);
        p.setOpacity(0.7);
921
        p.drawPixmap(0, 0, image.width(), image.height(), pix);
922
923
924
925
926
927
928
929
930
931
932
933
934
        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();
935
    emit processDragEnd();
936
937
938
    return true;
}

939
940
SmallJobLabel::SmallJobLabel(QWidget *parent)
    : QPushButton(parent)
Nicolas Carion's avatar
Nicolas Carion committed
941

942
943
944
945
{
    setFixedWidth(0);
    setFlat(true);
    m_timeLine = new QTimeLine(500, this);
Laurent Montel's avatar
Laurent Montel committed
946
947
    QObject::connect(m_timeLine, &QTimeLine::valueChanged, this, &SmallJobLabel::slotTimeLineChanged);
    QObject::connect(m_timeLine, &QTimeLine::finished, this, &SmallJobLabel::slotTimeLineFinished);
948
949
950
951
952
    hide();
}

const QString SmallJobLabel::getStyleSheet(const QPalette &p)
{
953
    KColorScheme scheme(p.currentColorGroup(), KColorScheme::Window);
954
955
    QColor bg = scheme.background(KColorScheme::LinkBackground).color();
    QColor fg = scheme.foreground(KColorScheme::LinkText).color();
Nicolas Carion's avatar
Nicolas Carion committed
956
957
958
959
960
961
962
963
    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());
964

965
966
    bg = scheme.background(KColorScheme::ActiveBackground).color();