File indexing completed on 2024-04-14 04:46:10

0001 /*
0002 SPDX-FileCopyrightText: 2012 Till Theato <root@ttill.de>
0003 SPDX-FileCopyrightText: 2014 Jean-Baptiste Mardelle <jb@kdenlive.org>
0004 This file is part of Kdenlive. See www.kdenlive.org.
0005 
0006 SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0007 */
0008 
0009 #include "bin.h"
0010 #include "bincommands.h"
0011 #include "clipcreator.hpp"
0012 #include "core.h"
0013 #include "dialogs/clipcreationdialog.h"
0014 #include "dialogs/clipjobmanager.h"
0015 #include "dialogs/kdenlivesettingsdialog.h"
0016 #include "dialogs/textbasededit.h"
0017 #include "dialogs/timeremap.h"
0018 #include "doc/documentchecker.h"
0019 #include "doc/docundostack.hpp"
0020 #include "doc/kdenlivedoc.h"
0021 #include "doc/kthumb.h"
0022 #include "effects/effectstack/model/effectstackmodel.hpp"
0023 #include "glaxnimatelauncher.h"
0024 #include "jobs/abstracttask.h"
0025 #include "jobs/audiolevelstask.h"
0026 #include "jobs/cliploadtask.h"
0027 #include "jobs/taskmanager.h"
0028 #include "jobs/transcodetask.h"
0029 #include "kdenlive_debug.h"
0030 #include "kdenlivesettings.h"
0031 #include "macros.hpp"
0032 #include "mainwindow.h"
0033 #include "mediabrowser.h"
0034 #include "mlt++/Mlt.h"
0035 #include "mltcontroller/clipcontroller.h"
0036 #include "mltcontroller/clippropertiescontroller.h"
0037 #include "monitor/monitor.h"
0038 #include "monitor/monitormanager.h"
0039 #include "profiles/profilemodel.hpp"
0040 #include "project/dialogs/guideslist.h"
0041 #include "project/dialogs/slideshowclip.h"
0042 #include "project/invaliddialog.h"
0043 #include "project/projectmanager.h"
0044 #include "project/transcodeseek.h"
0045 #include "projectclip.h"
0046 #include "projectfolder.h"
0047 #include "projectitemmodel.h"
0048 #include "projectsortproxymodel.h"
0049 #include "projectsubclip.h"
0050 #include "tagwidget.hpp"
0051 #include "titler/titlewidget.h"
0052 #include "ui_newtimeline_ui.h"
0053 #include "ui_qtextclip_ui.h"
0054 #include "undohelper.hpp"
0055 #include "utils/thumbnailcache.hpp"
0056 #include "xml/xml.hpp"
0057 
0058 #include "utils/KMessageBox_KdenliveCompat.h"
0059 #include <KActionMenu>
0060 #include <KColorScheme>
0061 #include <KIO/ApplicationLauncherJob>
0062 #include <KIO/FileCopyJob>
0063 #include <KIO/OpenFileManagerWindowJob>
0064 #include <KIconEffect>
0065 #include <KIconTheme>
0066 #include <KMessageBox>
0067 #include <KOpenWithDialog>
0068 #include <KRatingPainter>
0069 #include <KService>
0070 #include <KUrlRequesterDialog>
0071 #include <KXMLGUIFactory>
0072 #include <kwidgetsaddons_version.h>
0073 
0074 #include <QCryptographicHash>
0075 #include <QDrag>
0076 #include <QFile>
0077 #include <QMenu>
0078 #include <QMimeData>
0079 #include <QSlider>
0080 #include <QStyledItemDelegate>
0081 #include <QTimeLine>
0082 #include <QToolBar>
0083 #include <QUndoCommand>
0084 #include <QUrl>
0085 #include <QVBoxLayout>
0086 
0087 #include <memory>
0088 #include <utility>
0089 
0090 static QImage m_videoIcon;
0091 static QImage m_audioIcon;
0092 static QImage m_audioUsedIcon;
0093 static QImage m_videoUsedIcon;
0094 static QSize m_iconSize;
0095 static QIcon m_folderIcon;
0096 static QIcon m_sequenceFolderIcon;
0097 
0098 /**
0099  * @class BinItemDelegate
0100  * @brief This class is responsible for drawing items in the QTreeView.
0101  */
0102 class BinItemDelegate : public QStyledItemDelegate
0103 {
0104 public:
0105     explicit BinItemDelegate(QObject *parent = nullptr)
0106         : QStyledItemDelegate(parent)
0107 
0108     {
0109         connect(this, &QStyledItemDelegate::closeEditor, [&]() { m_editorOpen = false; });
0110     }
0111     void setEditorData(QWidget *w, const QModelIndex &i) const override
0112     {
0113         if (!m_editorOpen) {
0114             QStyledItemDelegate::setEditorData(w, i);
0115             m_editorOpen = true;
0116         }
0117     }
0118     bool editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index) override
0119     {
0120         if (event->type() == QEvent::MouseButtonPress) {
0121             auto *me = static_cast<QMouseEvent *>(event);
0122             if (index.column() == 0) {
0123                 QPoint pos = me->pos();
0124                 if (m_audioDragRect.contains(pos)) {
0125                     dragType = PlaylistState::AudioOnly;
0126                 } else if (m_videoDragRect.contains(pos)) {
0127                     dragType = PlaylistState::VideoOnly;
0128                 } else {
0129                     dragType = PlaylistState::Disabled;
0130                 }
0131             } else {
0132                 dragType = PlaylistState::Disabled;
0133                 if (index.column() == 7) {
0134                     // Rating
0135                     QRect rect = option.rect;
0136                     rect.adjust(option.rect.width() / 12, 0, 0, 0);
0137                     int rate = 0;
0138                     if (me->pos().x() > rect.x()) {
0139                         rate = KRatingPainter::getRatingFromPosition(rect, Qt::AlignLeft | Qt::AlignVCenter, qApp->layoutDirection(), me->pos());
0140                     }
0141                     if (rate > -1) {
0142                         // Full star rating only
0143                         if (rate % 2 == 1) {
0144                             rate++;
0145                         }
0146                         Q_EMIT static_cast<ProjectSortProxyModel *>(model)->updateRating(index, uint(rate));
0147                     }
0148                 }
0149             }
0150         }
0151         event->ignore();
0152         return false;
0153     }
0154     void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const override
0155     {
0156         if (index.column() != 0) {
0157             QStyledItemDelegate::updateEditorGeometry(editor, option, index);
0158             return;
0159         }
0160         QStyleOptionViewItem opt = option;
0161         initStyleOption(&opt, index);
0162         QRect r1 = option.rect;
0163         int type = index.data(AbstractProjectItem::ItemTypeRole).toInt();
0164         int decoWidth = 0;
0165         if (opt.decorationSize.height() > 0) {
0166             decoWidth += int(r1.height() * pCore->getCurrentDar());
0167         }
0168         int mid = 0;
0169         if (type == AbstractProjectItem::ClipItem || type == AbstractProjectItem::SubClipItem) {
0170             mid = int((r1.height() / 2));
0171         }
0172         r1.adjust(decoWidth, 0, 0, -mid);
0173         QFont ft = option.font;
0174         ft.setBold(true);
0175         QFontMetricsF fm(ft);
0176         QRect r2 = fm.boundingRect(r1, Qt::AlignLeft | Qt::AlignTop, index.data(AbstractProjectItem::DataName).toString()).toRect();
0177         editor->setGeometry(r2);
0178     }
0179 
0180     QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override
0181     {
0182         QSize hint = QStyledItemDelegate::sizeHint(option, index);
0183         QString text = index.data(AbstractProjectItem::DataName).toString();
0184         QRectF r = option.rect;
0185         QFont ft = option.font;
0186         ft.setBold(true);
0187         QFontMetricsF fm(ft);
0188         QStyle *style = option.widget ? option.widget->style() : QApplication::style();
0189         const int textMargin = style->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1;
0190         int width = int(fm.boundingRect(r, Qt::AlignLeft | Qt::AlignTop, text).width() + option.decorationSize.width()) + 2 * textMargin;
0191         hint.setWidth(width);
0192         int type = index.data(AbstractProjectItem::ItemTypeRole).toInt();
0193         if (type == AbstractProjectItem::FolderItem) {
0194             return QSize(hint.width(), qMin(option.fontMetrics.lineSpacing() + 4, hint.height()));
0195         }
0196         if (type == AbstractProjectItem::ClipItem) {
0197             return QSize(hint.width(), qMax(option.fontMetrics.lineSpacing() * 2 + 4, qMax(hint.height(), option.decorationSize.height())));
0198         }
0199         if (type == AbstractProjectItem::SubClipItem) {
0200             return QSize(hint.width(), qMax(option.fontMetrics.lineSpacing() * 2 + 4, qMin(hint.height(), int(option.decorationSize.height() / 1.5))));
0201         }
0202         QIcon icon = qvariant_cast<QIcon>(index.data(Qt::DecorationRole));
0203         QString line1 = index.data(Qt::DisplayRole).toString();
0204         QString line2 = index.data(Qt::UserRole).toString();
0205 
0206         int textW = qMax(option.fontMetrics.horizontalAdvance(line1), option.fontMetrics.horizontalAdvance(line2));
0207         QSize iconSize = icon.actualSize(option.decorationSize);
0208         return {qMax(textW, iconSize.width()) + 4, option.fontMetrics.lineSpacing() * 2 + 4};
0209     }
0210 
0211     void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override
0212     {
0213         if (index.column() == 0 && !index.data().isNull()) {
0214             QRect r1 = option.rect;
0215             painter->save();
0216             painter->setClipRect(r1);
0217             QStyleOptionViewItem opt(option);
0218             initStyleOption(&opt, index);
0219             int type = index.data(AbstractProjectItem::ItemTypeRole).toInt();
0220             QStyle *style = opt.widget ? opt.widget->style() : QApplication::style();
0221             const int textMargin = style->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1;
0222             // QRect r = QStyle::alignedRect(opt.direction, Qt::AlignVCenter | Qt::AlignLeft, opt.decorationSize, r1);
0223             // Draw alternate background
0224             style->drawPrimitive(QStyle::PE_PanelItemViewItem, &opt, painter, opt.widget);
0225             if ((option.state & static_cast<int>(QStyle::State_Selected)) != 0) {
0226                 painter->setPen(option.palette.highlightedText().color());
0227             } else {
0228                 painter->setPen(option.palette.text().color());
0229             }
0230             QRect r = r1;
0231             QFont font = painter->font();
0232             font.setBold(true);
0233             painter->setFont(font);
0234             if (type == AbstractProjectItem::ClipItem || type == AbstractProjectItem::SubClipItem) {
0235                 int decoWidth = 0;
0236                 FileStatus::ClipStatus clipStatus = FileStatus::ClipStatus(index.data(AbstractProjectItem::ClipStatus).toInt());
0237                 int cType = index.data(AbstractProjectItem::ClipType).toInt();
0238                 if (opt.decorationSize.height() > 0) {
0239                     r.setWidth(int(r.height() * pCore->getCurrentDar()));
0240                     QPixmap pix = opt.icon.pixmap(opt.icon.actualSize(r.size()));
0241                     if (!pix.isNull()) {
0242                         // Draw icon
0243                         decoWidth += r.width() + textMargin;
0244                         r.setWidth(r.height() * pix.width() / pix.height());
0245                         painter->drawPixmap(r, pix, QRect(0, 0, pix.width(), pix.height()));
0246                     }
0247                     m_thumbRect = r;
0248 
0249                     // Draw frame in case of missing source
0250                     if (clipStatus == FileStatus::StatusMissing || clipStatus == FileStatus::StatusProxyOnly) {
0251                         painter->save();
0252                         painter->setPen(QPen(clipStatus == FileStatus::StatusProxyOnly ? Qt::yellow : Qt::red, 3));
0253                         painter->drawRect(m_thumbRect.adjusted(0, 0, -1, -1));
0254                         painter->restore();
0255                     } else if (cType == ClipType::Image || cType == ClipType::SlideShow) {
0256                         // Draw 'photo' frame to identify image clips
0257                         painter->save();
0258                         int penWidth = m_thumbRect.height() / 14;
0259                         penWidth += penWidth % 2;
0260                         painter->setPen(QPen(QColor(255, 255, 255, 160), penWidth));
0261                         penWidth /= 2;
0262                         painter->drawRoundedRect(m_thumbRect.adjusted(penWidth, penWidth, -penWidth - 1, -penWidth - 1), 4, 4);
0263                         painter->setPen(QPen(Qt::black, 1));
0264                         painter->drawRoundedRect(m_thumbRect.adjusted(0, 0, -1, -1), 4, 4);
0265                         painter->restore();
0266                     }
0267                 }
0268                 int mid = int((r1.height() / 2));
0269                 r1.adjust(decoWidth, 0, 0, -mid);
0270                 QRect r2 = option.rect;
0271                 r2.adjust(decoWidth, mid, 0, 0);
0272                 QRectF bounding;
0273                 painter->drawText(r1, Qt::AlignLeft | Qt::AlignTop, index.data(AbstractProjectItem::DataName).toString(), &bounding);
0274                 font.setBold(false);
0275                 painter->setFont(font);
0276                 QString subText = index.data(AbstractProjectItem::DataDuration).toString();
0277                 QString tags = index.data(AbstractProjectItem::DataTag).toString();
0278                 if (!tags.isEmpty()) {
0279                     QStringList t = tags.split(QLatin1Char(';'));
0280                     QRectF tagRect = m_thumbRect.adjusted(2, 2, 0, 2);
0281                     tagRect.setWidth(r1.height() / 3.5);
0282                     tagRect.setHeight(tagRect.width());
0283                     for (const QString &color : qAsConst(t)) {
0284                         painter->setBrush(QColor(color));
0285                         painter->drawRoundedRect(tagRect, tagRect.height() / 2, tagRect.height() / 2);
0286                         tagRect.moveTop(tagRect.bottom() + tagRect.height() / 4);
0287                     }
0288                     painter->setBrush(Qt::NoBrush);
0289                 }
0290                 if (!subText.isEmpty()) {
0291                     r2.adjust(0, int(bounding.bottom() - r2.top()), 0, 0);
0292                     QColor subTextColor = painter->pen().color();
0293                     bool selected = opt.state & QStyle::State_Selected;
0294                     if (!selected) {
0295                         subTextColor.setAlphaF(.7);
0296                     }
0297                     painter->setPen(subTextColor);
0298                     // Draw usage counter
0299                     const QString usage = index.data(AbstractProjectItem::UsageCount).toString();
0300                     if (!usage.isEmpty()) {
0301                         subText.append(QString(" [%1]").arg(usage));
0302                     }
0303                     painter->drawText(r2, Qt::AlignLeft | Qt::AlignTop, subText, &bounding);
0304                     // Add audio/video icons for selective drag
0305                     bool hasAudioAndVideo = index.data(AbstractProjectItem::ClipHasAudioAndVideo).toBool();
0306                     if (hasAudioAndVideo && (cType == ClipType::AV || cType == ClipType::Playlist || cType == ClipType::Timeline)) {
0307                         QRect audioRect(0, 0, m_audioIcon.width(), m_audioIcon.height());
0308                         audioRect.moveTop(bounding.top() + 1);
0309                         QRect videoIconRect = audioRect;
0310                         videoIconRect.moveRight(
0311                             qMax(int(bounding.right() + (2 * textMargin) + 2 * (1 + m_audioIcon.width())), option.rect.right() - (2 * textMargin)));
0312                         audioRect.moveRight(videoIconRect.left() - (2 * textMargin) - 1);
0313                         if (opt.state & QStyle::State_MouseOver) {
0314                             m_audioDragRect = audioRect.adjusted(-1, -1, 1, 1);
0315                             m_videoDragRect = videoIconRect.adjusted(-1, -1, 1, 1);
0316                             painter->drawImage(audioRect.topLeft(), m_audioIcon);
0317                             painter->drawImage(videoIconRect.topLeft(), m_videoIcon);
0318                             painter->setPen(opt.palette.highlight().color());
0319                             painter->drawRect(m_audioDragRect);
0320                             painter->drawRect(m_videoDragRect);
0321                         } else if (!usage.isEmpty()) {
0322                             if (index.data(AbstractProjectItem::AudioUsed).toBool()) {
0323                                 painter->drawImage(audioRect.topLeft(), selected ? m_audioIcon : m_audioUsedIcon);
0324                             }
0325                             if (index.data(AbstractProjectItem::VideoUsed).toBool()) {
0326                                 painter->drawImage(videoIconRect.topLeft(), selected ? m_videoIcon : m_videoUsedIcon);
0327                             }
0328                         }
0329                     } else if (!usage.isEmpty()) {
0330                         QRect iconRect(0, 0, m_audioIcon.width(), m_audioIcon.height());
0331                         iconRect.moveTop(bounding.top() + 1);
0332                         int minPos = bounding.right() + (2 * textMargin) + m_audioIcon.width();
0333                         iconRect.moveRight(qMax(minPos, option.rect.right() - (2 * textMargin)));
0334                         if (index.data(AbstractProjectItem::AudioUsed).toBool()) {
0335                             painter->drawImage(iconRect.topLeft(), selected ? m_audioIcon : m_audioUsedIcon);
0336                         } else {
0337                             painter->drawImage(iconRect.topLeft(), selected ? m_videoIcon : m_videoUsedIcon);
0338                         }
0339                     }
0340                 }
0341                 if (type == AbstractProjectItem::ClipItem) {
0342                     // Overlay icon if necessary
0343                     QVariant v = index.data(AbstractProjectItem::IconOverlay);
0344                     if (!v.isNull()) {
0345                         QIcon reload = QIcon::fromTheme(v.toString());
0346                         int size = style->pixelMetric(QStyle::PM_SmallIconSize);
0347                         reload.paint(painter, QRect(r.left() + 2, r.bottom() - size - 2, size, size));
0348                     }
0349                     int jobProgress = index.data(AbstractProjectItem::JobProgress).toInt();
0350                     auto status = index.data(AbstractProjectItem::JobStatus).value<TaskManagerStatus>();
0351                     if (jobProgress < 100 && (status == TaskManagerStatus::Pending || status == TaskManagerStatus::Running)) {
0352                         // Draw job progress bar
0353                         int progressWidth = option.fontMetrics.averageCharWidth() * 8;
0354                         int progressHeight = option.fontMetrics.ascent() / 4;
0355                         QRect progress(r1.x() + 1, opt.rect.bottom() - progressHeight - 2, progressWidth, progressHeight);
0356                         painter->setPen(Qt::NoPen);
0357                         painter->setBrush(Qt::darkGray);
0358                         if (status == TaskManagerStatus::Running) {
0359                             painter->drawRoundedRect(progress, 2, 2);
0360                             painter->setBrush((option.state & static_cast<int>((QStyle::State_Selected) != 0)) != 0 ? option.palette.text()
0361                                                                                                                     : option.palette.highlight());
0362                             progress.setWidth((progressWidth - 2) * jobProgress / 100);
0363                             painter->drawRoundedRect(progress, 2, 2);
0364                         } else {
0365                             // Draw kind of a pause icon
0366                             progress.setWidth(3);
0367                             painter->drawRect(progress);
0368                             progress.moveLeft(progress.right() + 3);
0369                             painter->drawRect(progress);
0370                         }
0371                     }
0372                     bool jobsucceeded = index.data(AbstractProjectItem::JobSuccess).toBool();
0373                     if (!jobsucceeded) {
0374                         QIcon warning = QIcon::fromTheme(QStringLiteral("process-stop"));
0375                         warning.paint(painter, r2);
0376                     }
0377                 }
0378             } else {
0379                 // Folder
0380                 int decoWidth = 0;
0381                 if (opt.decorationSize.height() > 0) {
0382                     QIcon icon = index.data(AbstractProjectItem::SequenceFolder).toBool() ? m_sequenceFolderIcon : m_folderIcon;
0383                     r.setWidth(int(r.height() * pCore->getCurrentDar()));
0384                     QPixmap pix = icon.pixmap(icon.actualSize(r.size()));
0385                     // Draw icon
0386                     decoWidth += r.width() + textMargin;
0387                     r.setWidth(r.height() * pix.width() / pix.height());
0388                     painter->drawPixmap(r, pix, QRect(0, 0, pix.width(), pix.height()));
0389                 }
0390                 r1.adjust(decoWidth, 0, 0, 0);
0391                 QRectF bounding;
0392                 painter->drawText(r1, Qt::AlignLeft | Qt::AlignTop, index.data(AbstractProjectItem::DataName).toString(), &bounding);
0393             }
0394             painter->restore();
0395         } else if (index.column() == 7) {
0396             // Rating
0397             QStyleOptionViewItem opt(option);
0398             initStyleOption(&opt, index);
0399             QRect r1 = opt.rect;
0400             // Tweak bg opacity since breeze dark star has same color as highlighted background
0401             painter->setOpacity(0.5);
0402             QStyle *style = opt.widget ? opt.widget->style() : QApplication::style();
0403             style->drawPrimitive(QStyle::PE_PanelItemViewItem, &opt, painter, opt.widget);
0404             painter->setOpacity(1);
0405             if (index.data(AbstractProjectItem::ItemTypeRole).toInt() != AbstractProjectItem::FolderItem) {
0406                 r1.adjust(r1.width() / 12, 0, 0, 0);
0407                 KRatingPainter::paintRating(painter, r1, Qt::AlignLeft | Qt::AlignVCenter, index.data().toInt());
0408             }
0409         } else {
0410             QStyledItemDelegate::paint(painter, option, index);
0411         }
0412     }
0413 
0414     int getFrame(const QModelIndex &index, int mouseX)
0415     {
0416         int type = index.data(AbstractProjectItem::ItemTypeRole).toInt();
0417         if ((type != AbstractProjectItem::ClipItem && type != AbstractProjectItem::SubClipItem)) {
0418             return 0;
0419         }
0420         if (mouseX < m_thumbRect.x() || mouseX > m_thumbRect.right()) {
0421             return -1;
0422         }
0423         return 100 * (mouseX - m_thumbRect.x()) / m_thumbRect.width();
0424     }
0425 
0426 private:
0427     mutable bool m_editorOpen{false};
0428     mutable QRect m_audioDragRect;
0429     mutable QRect m_videoDragRect;
0430     mutable QRect m_thumbRect;
0431 
0432 public:
0433     PlaylistState::ClipState dragType{PlaylistState::Disabled};
0434 };
0435 
0436 /**
0437  * @class BinListItemDelegate
0438  * @brief This class is responsible for drawing items in the QListView (Icon view).
0439  */
0440 
0441 class BinListItemDelegate : public QStyledItemDelegate
0442 {
0443 public:
0444     explicit BinListItemDelegate(QObject *parent = nullptr)
0445         : QStyledItemDelegate(parent)
0446 
0447     {
0448         connect(this, &QStyledItemDelegate::closeEditor, [&]() { m_editorOpen = false; });
0449     }
0450     bool editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index) override
0451     {
0452         Q_UNUSED(model);
0453         Q_UNUSED(option);
0454         Q_UNUSED(index);
0455         if (event->type() == QEvent::MouseButtonPress) {
0456             auto *me = static_cast<QMouseEvent *>(event);
0457             if (m_audioDragRect.contains(me->pos())) {
0458                 dragType = PlaylistState::AudioOnly;
0459             } else if (m_videoDragRect.contains(me->pos())) {
0460                 dragType = PlaylistState::VideoOnly;
0461             } else {
0462                 dragType = PlaylistState::Disabled;
0463             }
0464         }
0465         event->ignore();
0466         return false;
0467     }
0468 
0469     void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override
0470     {
0471         if (!index.data().isNull()) {
0472             QStyleOptionViewItem opt(option);
0473             initStyleOption(&opt, index);
0474             // QStyledItemDelegate::paint(painter, opt, index);
0475             QStyle *style = opt.widget ? opt.widget->style() : QApplication::style();
0476             style->drawPrimitive(QStyle::PE_PanelItemViewItem, &opt, painter, opt.widget);
0477             // style->drawControl(QStyle::CE_ItemViewItem, &opt, painter, opt.widget);
0478             QRect r = opt.rect;
0479             r.setHeight(r.width() / pCore->getCurrentDar());
0480             int type = index.data(AbstractProjectItem::ItemTypeRole).toInt();
0481             bool isFolder = type == AbstractProjectItem::FolderItem;
0482             bool isSequenceFolder = index.data(AbstractProjectItem::SequenceFolder).toBool();
0483             QPixmap pix = isFolder ? (isSequenceFolder ? m_sequenceFolderIcon.pixmap(m_sequenceFolderIcon.actualSize(r.size()))
0484                                                        : m_folderIcon.pixmap(m_folderIcon.actualSize(r.size())))
0485                                    : opt.icon.pixmap(opt.icon.actualSize(r.size()));
0486             if (!pix.isNull()) {
0487                 // Draw icon
0488                 painter->drawPixmap(r, pix, QRect(0, 0, pix.width(), pix.height()));
0489             }
0490             m_thumbRect = r;
0491             QRect textRect = opt.rect;
0492             const int textMargin = style->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1;
0493             textRect.adjust(textMargin, 0, -textMargin, -textMargin);
0494             QRectF bounding;
0495             QString itemText = index.data(AbstractProjectItem::DataName).toString();
0496             // Draw usage counter
0497             const QString usage = isFolder ? QString() : index.data(AbstractProjectItem::UsageCount).toString();
0498             if (!usage.isEmpty()) {
0499                 int usageWidth = option.fontMetrics.horizontalAdvance(QString(" [%1]").arg(usage));
0500                 int availableWidth = textRect.width() - usageWidth;
0501                 if (option.fontMetrics.horizontalAdvance(itemText) > availableWidth) {
0502                     itemText = option.fontMetrics.elidedText(itemText, Qt::ElideRight, availableWidth);
0503                 }
0504                 itemText.append(QString(" [%1]").arg(usage));
0505             } else {
0506                 if (option.fontMetrics.horizontalAdvance(itemText) > textRect.width()) {
0507                     itemText = option.fontMetrics.elidedText(itemText, Qt::ElideRight, textRect.width());
0508                 }
0509             }
0510             painter->drawText(textRect, Qt::AlignCenter | Qt::AlignBottom, itemText, &bounding);
0511 
0512             if (isFolder) {
0513                 return;
0514             }
0515 
0516             // Tags
0517             QString tags = index.data(AbstractProjectItem::DataTag).toString();
0518             if (!tags.isEmpty()) {
0519                 QStringList t = tags.split(QLatin1Char(';'));
0520                 QRectF tagRect = m_thumbRect.adjusted(2, 2, 0, 2);
0521                 tagRect.setWidth(m_thumbRect.height() / 5);
0522                 tagRect.setHeight(tagRect.width());
0523                 painter->save();
0524                 for (const QString &color : qAsConst(t)) {
0525                     painter->setBrush(QColor(color));
0526                     painter->drawRoundedRect(tagRect, tagRect.height() / 2, tagRect.height() / 2);
0527                     tagRect.moveTop(tagRect.bottom() + tagRect.height() / 4);
0528                 }
0529                 painter->restore();
0530             }
0531 
0532             // Add audio/video icons for selective drag
0533             int cType = index.data(AbstractProjectItem::ClipType).toInt();
0534             bool hasAudioAndVideo = index.data(AbstractProjectItem::ClipHasAudioAndVideo).toBool();
0535             if (hasAudioAndVideo && (cType == ClipType::AV || cType == ClipType::Playlist || cType == ClipType::Timeline) &&
0536                 m_thumbRect.height() > 2.5 * m_audioIcon.height()) {
0537                 QRect thumbRect = m_thumbRect;
0538                 thumbRect.setLeft(opt.rect.right() - m_audioIcon.width() - 6);
0539                 if (opt.state & QStyle::State_MouseOver || !usage.isEmpty()) {
0540                     QColor bgColor = option.palette.window().color();
0541                     bgColor.setAlphaF(.7);
0542                     painter->fillRect(thumbRect, bgColor);
0543                 }
0544                 thumbRect.setSize(m_audioIcon.size());
0545                 thumbRect.translate(3, 2);
0546                 QRect videoThumbRect = thumbRect;
0547                 videoThumbRect.moveTop(thumbRect.bottom() + 2);
0548                 if (opt.state & QStyle::State_MouseOver) {
0549                     m_audioDragRect = thumbRect;
0550                     m_videoDragRect = videoThumbRect;
0551                     painter->drawImage(m_audioDragRect.topLeft(), m_audioIcon);
0552                     painter->drawImage(m_videoDragRect.topLeft(), m_videoIcon);
0553                 } else if (!usage.isEmpty()) {
0554                     if (index.data(AbstractProjectItem::AudioUsed).toBool()) {
0555                         painter->drawImage(thumbRect.topLeft(), m_audioUsedIcon);
0556                     }
0557                     if (index.data(AbstractProjectItem::VideoUsed).toBool()) {
0558                         painter->drawImage(videoThumbRect.topLeft(), m_videoUsedIcon);
0559                     }
0560                 }
0561             }
0562             // Draw frame in case of missing source
0563             FileStatus::ClipStatus clipStatus = FileStatus::ClipStatus(index.data(AbstractProjectItem::ClipStatus).toInt());
0564             if (clipStatus == FileStatus::StatusMissing || clipStatus == FileStatus::StatusProxyOnly) {
0565                 painter->save();
0566                 painter->setPen(QPen(clipStatus == FileStatus::StatusProxyOnly ? Qt::yellow : Qt::red, 3));
0567                 painter->drawRect(m_thumbRect);
0568                 painter->restore();
0569             } else if (cType == ClipType::Image || cType == ClipType::SlideShow) {
0570                 // Draw 'photo' frame to identify image clips
0571                 painter->save();
0572                 int penWidth = m_thumbRect.height() / 14;
0573                 penWidth += penWidth % 2;
0574                 painter->setPen(QPen(QColor(255, 255, 255, 160), penWidth));
0575                 penWidth /= 2;
0576                 painter->drawRoundedRect(m_thumbRect.adjusted(penWidth, penWidth, -penWidth - 1, -penWidth + 1), 4, 4);
0577                 painter->setPen(QPen(Qt::black, 1));
0578                 painter->drawRoundedRect(m_thumbRect.adjusted(0, 0, -1, 1), 4, 4);
0579                 painter->restore();
0580             }
0581             // Overlay icon if necessary
0582             QVariant v = index.data(AbstractProjectItem::IconOverlay);
0583             if (!v.isNull()) {
0584                 QRect r = m_thumbRect;
0585                 QIcon reload = QIcon::fromTheme(v.toString());
0586                 r.setTop(r.bottom() - (opt.rect.height() - r.height()));
0587                 r.setWidth(r.height());
0588                 reload.paint(painter, r);
0589             }
0590             int jobProgress = index.data(AbstractProjectItem::JobProgress).toInt();
0591             auto status = index.data(AbstractProjectItem::JobStatus).value<TaskManagerStatus>();
0592             if (jobProgress < 100 && (status == TaskManagerStatus::Pending || status == TaskManagerStatus::Running)) {
0593                 // Draw job progress bar
0594                 int progressHeight = option.fontMetrics.ascent() / 4;
0595                 QRect thumbRect = m_thumbRect.adjusted(2, 2, -2, -2);
0596                 QRect progress(thumbRect.x(), thumbRect.bottom() - progressHeight - 2, thumbRect.width(), progressHeight);
0597                 painter->setPen(Qt::NoPen);
0598                 painter->setBrush(Qt::darkGray);
0599                 if (status == TaskManagerStatus::Running) {
0600                     painter->drawRoundedRect(progress, 2, 2);
0601                     painter->setBrush((option.state & static_cast<int>((QStyle::State_Selected) != 0)) != 0 ? option.palette.text()
0602                                                                                                             : option.palette.highlight());
0603                     progress.setWidth((thumbRect.width() - 2) * jobProgress / 100);
0604                     painter->drawRoundedRect(progress, 2, 2);
0605                 } else {
0606                     // Draw kind of a pause icon
0607                     progress.setWidth(3);
0608                     painter->drawRect(progress);
0609                     progress.moveLeft(progress.right() + 3);
0610                     painter->drawRect(progress);
0611                 }
0612             }
0613             bool jobsucceeded = index.data(AbstractProjectItem::JobSuccess).toBool();
0614             if (!jobsucceeded) {
0615                 QIcon warning = QIcon::fromTheme(QStringLiteral("process-stop"));
0616                 QRect thumbRect = m_thumbRect.adjusted(2, 2, 2, 2);
0617                 warning.paint(painter, thumbRect);
0618             }
0619         }
0620     }
0621 
0622     int getFrame(const QModelIndex &index, QPoint pos)
0623     {
0624         int type = index.data(AbstractProjectItem::ItemTypeRole).toInt();
0625         if ((type != AbstractProjectItem::ClipItem && type != AbstractProjectItem::SubClipItem) || !m_thumbRect.contains(pos)) {
0626             return 0;
0627         }
0628         return 100 * (pos.x() - m_thumbRect.x()) / m_thumbRect.width();
0629     }
0630 
0631     QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &) const override
0632     {
0633         int textHeight = int(option.fontMetrics.height() * 1.5);
0634         return (QSize(m_iconSize.width(), m_iconSize.height() + textHeight));
0635     }
0636 
0637 private:
0638     mutable bool m_editorOpen{false};
0639     mutable QRect m_audioDragRect;
0640     mutable QRect m_videoDragRect;
0641     mutable QRect m_thumbRect;
0642 
0643 public:
0644     PlaylistState::ClipState dragType{PlaylistState::Disabled};
0645 };
0646 
0647 MyListView::MyListView(QWidget *parent)
0648     : QListView(parent)
0649 {
0650     setViewMode(QListView::IconMode);
0651     setMovement(QListView::Static);
0652     setResizeMode(QListView::Adjust);
0653     setWordWrap(true);
0654     setDragDropMode(QAbstractItemView::DragDrop);
0655     setUniformItemSizes(true);
0656     setDragEnabled(true);
0657     setAcceptDrops(true);
0658     // setDropIndicatorShown(true);
0659     viewport()->setAcceptDrops(true);
0660 }
0661 
0662 void MyListView::focusInEvent(QFocusEvent *event)
0663 {
0664     QListView::focusInEvent(event);
0665     if (event->reason() == Qt::MouseFocusReason) {
0666         Q_EMIT focusView();
0667     }
0668 }
0669 
0670 void MyListView::dropEvent(QDropEvent *event)
0671 {
0672     if (event->mimeData()->hasFormat(QStringLiteral("text/producerslist"))) {
0673         // Internal drag/drop, ensure it is not a zone drop
0674         if (!QString(event->mimeData()->data(QStringLiteral("text/producerslist"))).contains(QLatin1Char('/'))) {
0675             bool isSameRoot = false;
0676             QString rootId = QString(event->mimeData()->data(QStringLiteral("text/rootId")));
0677             if (rootIndex().data(AbstractProjectItem::DataId).toString() == rootId) {
0678                 isSameRoot = true;
0679             }
0680 #if QT_VERSION > QT_VERSION_CHECK(6, 0, 0)
0681             if (isSameRoot && !indexAt(event->position().toPoint()).isValid()) {
0682 #else
0683             if (isSameRoot && !indexAt(event->pos()).isValid()) {
0684 #endif
0685                 event->ignore();
0686                 return;
0687             }
0688         }
0689     }
0690     QListView::dropEvent(event);
0691 }
0692 
0693 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
0694 void MyListView::enterEvent(QEnterEvent *event)
0695 #else
0696 void MyListView::enterEvent(QEvent *event)
0697 #endif
0698 {
0699     QListView::enterEvent(event);
0700     pCore->setWidgetKeyBinding(i18n("<b>Double click</b> to add a file to the project"));
0701 }
0702 
0703 void MyListView::leaveEvent(QEvent *event)
0704 {
0705     QListView::leaveEvent(event);
0706     pCore->setWidgetKeyBinding();
0707 }
0708 
0709 void MyListView::mousePressEvent(QMouseEvent *event)
0710 {
0711     if (event->button() == Qt::LeftButton) {
0712         QModelIndex ix = indexAt(event->pos());
0713         if (ix.isValid()) {
0714 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0715             QAbstractItemDelegate *del = itemDelegate(ix);
0716 #else
0717             QAbstractItemDelegate *del = itemDelegateForIndex(ix);
0718 #endif
0719             m_dragType = static_cast<BinListItemDelegate *>(del)->dragType;
0720             m_startPos = event->pos();
0721         } else {
0722             m_dragType = PlaylistState::Disabled;
0723             m_startPos = QPoint();
0724         }
0725         Q_EMIT updateDragMode(m_dragType);
0726     }
0727     QListView::mousePressEvent(event);
0728 }
0729 
0730 void MyListView::mouseReleaseEvent(QMouseEvent *event)
0731 {
0732     m_startPos = QPoint();
0733     QListView::mouseReleaseEvent(event);
0734 }
0735 
0736 void MyListView::mouseMoveEvent(QMouseEvent *event)
0737 {
0738     if ((event->buttons() & Qt::LeftButton) != 0u) {
0739         if (!m_startPos.isNull() && (event->pos() - m_startPos).manhattanLength() > QApplication::startDragDistance()) {
0740             QModelIndexList indexes = selectedIndexes();
0741             if (indexes.isEmpty()) {
0742                 // Dragging from empty zone, abort
0743                 QListView::mouseMoveEvent(event);
0744                 return;
0745             }
0746             auto *drag = new QDrag(this);
0747             drag->setMimeData(model()->mimeData(indexes));
0748             QModelIndex ix = indexes.constFirst();
0749             if (ix.isValid()) {
0750                 QIcon icon = ix.data(AbstractProjectItem::DataThumbnail).value<QIcon>();
0751                 QPixmap pix = icon.pixmap(iconSize());
0752                 QSize size = pix.size() / 2;
0753                 QImage image(size, QImage::Format_ARGB32_Premultiplied);
0754                 image.fill(Qt::transparent);
0755                 QPainter p(&image);
0756                 p.setOpacity(0.7);
0757                 p.drawPixmap(0, 0, image.width(), image.height(), pix);
0758                 p.setOpacity(1);
0759                 if (indexes.count() > 1) {
0760                     QPalette palette;
0761                     int radius = size.height() / 3;
0762                     p.setBrush(palette.highlight());
0763                     p.setPen(palette.highlightedText().color());
0764                     p.drawEllipse(QPoint(size.width() / 2, size.height() / 2), radius, radius);
0765                     p.drawText(size.width() / 2 - radius, size.height() / 2 - radius, 2 * radius, 2 * radius, Qt::AlignCenter,
0766                                QString::number(indexes.count()));
0767                 }
0768                 p.end();
0769                 drag->setPixmap(QPixmap::fromImage(image));
0770             }
0771             drag->exec();
0772             Q_EMIT processDragEnd();
0773             return;
0774         }
0775         QListView::mouseMoveEvent(event);
0776         return;
0777     }
0778     QModelIndex index = indexAt(event->pos());
0779     if (index.isValid()) {
0780         if (KdenliveSettings::hoverPreview()) {
0781 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0782             QAbstractItemDelegate *del = itemDelegate(index);
0783 #else
0784             QAbstractItemDelegate *del = itemDelegateForIndex(index);
0785 #endif
0786             if (del) {
0787                 auto delegate = static_cast<BinListItemDelegate *>(del);
0788                 QRect vRect = visualRect(index);
0789                 if (vRect.contains(event->pos())) {
0790                     if (m_lastHoveredItem != index) {
0791                         if (m_lastHoveredItem.isValid()) {
0792                             Q_EMIT displayBinFrame(m_lastHoveredItem, -1);
0793                         }
0794                         m_lastHoveredItem = index;
0795                     }
0796                     int frame = delegate->getFrame(index, event->pos());
0797                     Q_EMIT displayBinFrame(index, frame, event->modifiers() & Qt::ShiftModifier);
0798                 } else if (m_lastHoveredItem == index) {
0799                     Q_EMIT displayBinFrame(m_lastHoveredItem, -1);
0800                     m_lastHoveredItem = QModelIndex();
0801                 }
0802             } else {
0803                 if (m_lastHoveredItem.isValid()) {
0804                     Q_EMIT displayBinFrame(m_lastHoveredItem, -1);
0805                     m_lastHoveredItem = QModelIndex();
0806                 }
0807             }
0808             pCore->bin()->updateKeyBinding(i18n("<b>Shift+seek</b> over thumbnail to set default thumbnail, <b>F2</b> to rename selected item"));
0809         } else {
0810             pCore->bin()->updateKeyBinding(i18n("<b>F2</b> to rename selected item"));
0811         }
0812     } else {
0813         pCore->bin()->updateKeyBinding();
0814         if (m_lastHoveredItem.isValid()) {
0815             Q_EMIT displayBinFrame(m_lastHoveredItem, -1);
0816             m_lastHoveredItem = QModelIndex();
0817         }
0818     }
0819     if (m_startPos.isNull() && event->buttons() == Qt::NoButton) {
0820         QListView::mouseMoveEvent(event);
0821     }
0822 }
0823 
0824 MyTreeView::MyTreeView(QWidget *parent)
0825     : QTreeView(parent)
0826 {
0827     setEditing(false);
0828     setAcceptDrops(true);
0829 }
0830 
0831 void MyTreeView::mousePressEvent(QMouseEvent *event)
0832 {
0833     QTreeView::mousePressEvent(event);
0834     if (event->button() == Qt::LeftButton) {
0835         QModelIndex ix = indexAt(event->pos());
0836         if (ix.isValid()) {
0837 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0838             QAbstractItemDelegate *del = itemDelegate(ix);
0839 #else
0840             QAbstractItemDelegate *del = itemDelegateForIndex(ix);
0841 #endif
0842             m_dragType = static_cast<BinItemDelegate *>(del)->dragType;
0843             m_startPos = event->pos();
0844 
0845         } else {
0846             m_dragType = PlaylistState::Disabled;
0847             m_startPos = QPoint();
0848         }
0849     }
0850     event->accept();
0851 }
0852 
0853 void MyTreeView::mouseReleaseEvent(QMouseEvent *event)
0854 {
0855     m_startPos = QPoint();
0856     QTreeView::mouseReleaseEvent(event);
0857 }
0858 
0859 void MyTreeView::focusInEvent(QFocusEvent *event)
0860 {
0861     QTreeView::focusInEvent(event);
0862     if (event->reason() == Qt::MouseFocusReason) {
0863         Q_EMIT focusView();
0864     }
0865 }
0866 
0867 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
0868 void MyTreeView::enterEvent(QEnterEvent *event)
0869 #else
0870 void MyTreeView::enterEvent(QEvent *event)
0871 #endif
0872 {
0873     QTreeView::enterEvent(event);
0874     pCore->setWidgetKeyBinding(i18n("<b>Double click</b> to add a file to the project"));
0875 }
0876 
0877 void MyTreeView::leaveEvent(QEvent *event)
0878 {
0879     QTreeView::leaveEvent(event);
0880     pCore->setWidgetKeyBinding();
0881 }
0882 
0883 void MyTreeView::mouseMoveEvent(QMouseEvent *event)
0884 {
0885     if ((event->buttons() & Qt::LeftButton) != 0u) {
0886         if (!m_startPos.isNull()) {
0887             int distance = (event->pos() - m_startPos).manhattanLength();
0888             if (distance >= QApplication::startDragDistance()) {
0889                 performDrag();
0890                 return;
0891             }
0892         }
0893         QTreeView::mouseMoveEvent(event);
0894         return;
0895     } else {
0896         QModelIndex index = indexAt(event->pos());
0897         if (index.isValid()) {
0898             if (KdenliveSettings::hoverPreview()) {
0899 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0900                 QAbstractItemDelegate *del = itemDelegate(index);
0901 #else
0902                 QAbstractItemDelegate *del = itemDelegateForIndex(index);
0903 #endif
0904                 int frame = static_cast<BinItemDelegate *>(del)->getFrame(index, event->pos().x());
0905                 if (frame >= 0) {
0906                     Q_EMIT displayBinFrame(index, frame, event->modifiers() & Qt::ShiftModifier);
0907                     if (m_lastHoveredItem != index) {
0908                         if (m_lastHoveredItem.isValid()) {
0909                             Q_EMIT displayBinFrame(m_lastHoveredItem, -1);
0910                         }
0911                         m_lastHoveredItem = index;
0912                     }
0913                 } else if (m_lastHoveredItem.isValid()) {
0914                     Q_EMIT displayBinFrame(m_lastHoveredItem, -1);
0915                     m_lastHoveredItem = QModelIndex();
0916                 }
0917                 pCore->bin()->updateKeyBinding(i18n("<b>Shift+seek</b> over thumbnail to set default thumbnail, <b>F2</b> to rename selected item"));
0918             } else {
0919                 pCore->bin()->updateKeyBinding(i18n("<b>F2</b> to rename selected item"));
0920             }
0921         } else {
0922             if (m_lastHoveredItem.isValid()) {
0923                 Q_EMIT displayBinFrame(m_lastHoveredItem, -1);
0924                 m_lastHoveredItem = QModelIndex();
0925             }
0926             pCore->bin()->updateKeyBinding();
0927         }
0928     }
0929     if (m_startPos.isNull() && event->buttons() == Qt::NoButton) {
0930         QTreeView::mouseMoveEvent(event);
0931     }
0932 }
0933 
0934 void MyTreeView::closeEditor(QWidget *editor, QAbstractItemDelegate::EndEditHint hint)
0935 {
0936     QAbstractItemView::closeEditor(editor, hint);
0937     setEditing(false);
0938 }
0939 
0940 void MyTreeView::editorDestroyed(QObject *editor)
0941 {
0942     QAbstractItemView::editorDestroyed(editor);
0943     setEditing(false);
0944 }
0945 
0946 bool MyTreeView::isEditing() const
0947 {
0948     return state() == QAbstractItemView::EditingState;
0949 }
0950 
0951 void MyTreeView::setEditing(bool edit)
0952 {
0953     setState(edit ? QAbstractItemView::EditingState : QAbstractItemView::NoState);
0954     if (!edit) {
0955         // Ensure edited item is selected
0956         Q_EMIT selectCurrent();
0957     }
0958 }
0959 
0960 bool MyTreeView::performDrag()
0961 {
0962     QModelIndexList bases = selectedIndexes();
0963     QModelIndexList indexes;
0964     for (int i = 0; i < bases.count(); i++) {
0965         if (bases.at(i).column() == 0) {
0966             indexes << bases.at(i);
0967         }
0968     }
0969     if (indexes.isEmpty()) {
0970         return false;
0971     }
0972     // Check if we want audio or video only
0973     Q_EMIT updateDragMode(m_dragType);
0974     auto *drag = new QDrag(this);
0975     drag->setMimeData(model()->mimeData(indexes));
0976     QModelIndex ix = indexes.constFirst();
0977     if (ix.isValid()) {
0978         QIcon icon = ix.data(AbstractProjectItem::DataThumbnail).value<QIcon>();
0979         QPixmap pix = icon.pixmap(iconSize());
0980         QSize size = pix.size() / 2;
0981         QImage image(size, QImage::Format_ARGB32_Premultiplied);
0982         image.fill(Qt::transparent);
0983         QPainter p(&image);
0984         p.setOpacity(0.7);
0985         p.drawPixmap(0, 0, image.width(), image.height(), pix);
0986         p.setOpacity(1);
0987         if (indexes.count() > 1) {
0988             QPalette palette;
0989             int radius = size.height() / 3;
0990             p.setBrush(palette.highlight());
0991             p.setPen(palette.highlightedText().color());
0992             p.drawEllipse(QPoint(size.width() / 2, size.height() / 2), radius, radius);
0993             p.drawText(size.width() / 2 - radius, size.height() / 2 - radius, 2 * radius, 2 * radius, Qt::AlignCenter, QString::number(indexes.count()));
0994         }
0995         p.end();
0996         drag->setPixmap(QPixmap::fromImage(image));
0997     }
0998     drag->exec();
0999     drag->deleteLater();
1000     Q_EMIT processDragEnd();
1001     return true;
1002 }
1003 
1004 SmallJobLabel::SmallJobLabel(QWidget *parent)
1005     : QPushButton(parent)
1006 
1007 {
1008     setFixedWidth(0);
1009     setFlat(true);
1010     m_timeLine = new QTimeLine(500, this);
1011     QObject::connect(m_timeLine, &QTimeLine::valueChanged, this, &SmallJobLabel::slotTimeLineChanged);
1012     QObject::connect(m_timeLine, &QTimeLine::finished, this, &SmallJobLabel::slotTimeLineFinished);
1013     hide();
1014 }
1015 
1016 const QString SmallJobLabel::getStyleSheet(const QPalette &p)
1017 {
1018     KColorScheme scheme(p.currentColorGroup(), KColorScheme::Window);
1019     QColor bg = scheme.background(KColorScheme::LinkBackground).color();
1020     QColor fg = scheme.foreground(KColorScheme::LinkText).color();
1021     QString style =
1022         QStringLiteral("QPushButton {margin:3px;padding:2px;background-color: rgb(%1, %2, %3);border-radius: 4px;border: none;color: rgb(%4, %5, %6)}")
1023             .arg(bg.red())
1024             .arg(bg.green())
1025             .arg(bg.blue())
1026             .arg(fg.red())
1027             .arg(fg.green())
1028             .arg(fg.blue());
1029 
1030     bg = scheme.background(KColorScheme::ActiveBackground).color();
1031     fg = scheme.foreground(KColorScheme::ActiveText).color();
1032     style.append(
1033         QStringLiteral("\nQPushButton:hover {margin:3px;padding:2px;background-color: rgb(%1, %2, %3);border-radius: 4px;border: none;color: rgb(%4, %5, %6)}")
1034             .arg(bg.red())
1035             .arg(bg.green())
1036             .arg(bg.blue())
1037             .arg(fg.red())
1038             .arg(fg.green())
1039             .arg(fg.blue()));
1040 
1041     return style;
1042 }
1043 
1044 void SmallJobLabel::setAction(QAction *action)
1045 {
1046     m_action = action;
1047 }
1048 
1049 void SmallJobLabel::slotTimeLineChanged(qreal value)
1050 {
1051     setFixedWidth(int(qMin(value * 2, qreal(1.0)) * sizeHint().width()));
1052     update();
1053 }
1054 
1055 void SmallJobLabel::slotTimeLineFinished()
1056 {
1057     if (m_timeLine->direction() == QTimeLine::Forward) {
1058         // Show
1059         m_action->setVisible(true);
1060     } else {
1061         // Hide
1062         m_action->setVisible(false);
1063         setText(QString());
1064     }
1065 }
1066 
1067 void SmallJobLabel::slotSetJobCount(int jobCount)
1068 {
1069     QMutexLocker lk(&m_locker);
1070     if (jobCount > 0) {
1071         // prepare animation
1072         setText(i18np("%1 job", "%1 jobs", jobCount));
1073         setToolTip(i18np("%1 pending job", "%1 pending jobs", jobCount));
1074 
1075         if (style()->styleHint(QStyle::SH_Widget_Animate, nullptr, this) != 0) {
1076             setFixedWidth(sizeHint().width());
1077             m_action->setVisible(true);
1078             return;
1079         }
1080 
1081         if (m_action->isVisible()) {
1082             setFixedWidth(sizeHint().width());
1083             update();
1084             return;
1085         }
1086 
1087         setFixedWidth(0);
1088         m_action->setVisible(true);
1089         int wantedWidth = sizeHint().width();
1090         setGeometry(-wantedWidth, 0, wantedWidth, height());
1091         m_timeLine->setDirection(QTimeLine::Forward);
1092         if (m_timeLine->state() == QTimeLine::NotRunning) {
1093             m_timeLine->start();
1094         }
1095     } else {
1096         if (style()->styleHint(QStyle::SH_Widget_Animate, nullptr, this) != 0) {
1097             setFixedWidth(0);
1098             m_action->setVisible(false);
1099             return;
1100         }
1101         // hide
1102         m_timeLine->setDirection(QTimeLine::Backward);
1103         if (m_timeLine->state() == QTimeLine::NotRunning) {
1104             m_timeLine->start();
1105         }
1106     }
1107 }
1108 
1109 LineEventEater::LineEventEater(QObject *parent)
1110     : QObject(parent)
1111 {
1112 }
1113 
1114 bool LineEventEater::eventFilter(QObject *obj, QEvent *event)
1115 {
1116     switch (event->type()) {
1117     case QEvent::ShortcutOverride:
1118         if (static_cast<QKeyEvent *>(event)->key() == Qt::Key_Escape) {
1119             Q_EMIT clearSearchLine();
1120         }
1121         break;
1122     case QEvent::Resize:
1123         // Workaround Qt BUG 54676
1124         Q_EMIT showClearButton(static_cast<QResizeEvent *>(event)->size().width() > QFontMetrics(QApplication::font()).averageCharWidth() * 8);
1125         break;
1126     default:
1127         break;
1128     }
1129     return QObject::eventFilter(obj, event);
1130 }
1131 
1132 Bin::Bin(std::shared_ptr<ProjectItemModel> model, QWidget *parent, bool isMainBin)
1133     : QWidget(parent)
1134     , isLoading(false)
1135     , shouldCheckProfile(false)
1136     , m_isMainBin(isMainBin)
1137     , m_itemModel(std::move(model))
1138     , m_itemView(nullptr)
1139     , m_binTreeViewDelegate(nullptr)
1140     , m_binListViewDelegate(nullptr)
1141     , m_doc(nullptr)
1142     , m_extractAudioAction(nullptr)
1143     , m_transcodeAction(nullptr)
1144     , m_clipsActionsMenu(nullptr)
1145     , m_inTimelineAction(nullptr)
1146     , m_listType(BinViewType(KdenliveSettings::binMode()))
1147     , m_baseIconSize(160, 90)
1148     , m_propertiesDock(nullptr)
1149     , m_propertiesPanel(nullptr)
1150     , m_monitor(nullptr)
1151     , m_blankThumb()
1152     , m_filterTagGroup(this)
1153     , m_filterRateGroup(this)
1154     , m_filterUsageGroup(this)
1155     , m_filterTypeGroup(this)
1156     , m_invalidClipDialog(nullptr)
1157     , m_transcodingDialog(nullptr)
1158     , m_gainedFocus(false)
1159     , m_audioDuration(0)
1160     , m_processedAudio(0)
1161 {
1162     m_layout = new QVBoxLayout(this);
1163 
1164     // Create toolbar for buttons
1165     m_toolbar = new QToolBar(this);
1166     int iconSize = style()->pixelMetric(QStyle::PM_SmallIconSize);
1167     m_toolbar->setIconSize(QSize(iconSize, iconSize));
1168     m_toolbar->setToolButtonStyle(Qt::ToolButtonIconOnly);
1169     m_layout->addWidget(m_toolbar);
1170 
1171     if (m_isMainBin) {
1172         // Init icons
1173         m_audioIcon = QImage(iconSize, iconSize, QImage::Format_ARGB32_Premultiplied);
1174         m_videoIcon = QImage(iconSize, iconSize, QImage::Format_ARGB32_Premultiplied);
1175         m_folderIcon = QIcon::fromTheme(QStringLiteral("folder"));
1176         m_sequenceFolderIcon = QIcon::fromTheme(QStringLiteral("folder-yellow"));
1177     }
1178 
1179     // Tags panel
1180     m_tagsWidget = new TagWidget(this);
1181     connect(m_tagsWidget, &TagWidget::switchTag, this, &Bin::switchTag);
1182     connect(m_tagsWidget, &TagWidget::updateProjectTags, this, &Bin::updateTags);
1183     m_tagsWidget->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
1184     m_layout->addWidget(m_tagsWidget);
1185     m_tagsWidget->setVisible(false);
1186 
1187     m_layout->setSpacing(0);
1188     m_layout->setContentsMargins(0, 0, 0, 0);
1189     // Search line
1190     m_searchLine = new QLineEdit(this);
1191     m_searchLine->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred);
1192     m_searchLine->setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont));
1193     // m_searchLine->setClearButtonEnabled(true);
1194     m_searchLine->setPlaceholderText(i18n("Search…"));
1195     m_searchLine->setFocusPolicy(Qt::ClickFocus);
1196     m_searchLine->setAccessibleName(i18n("Bin Search"));
1197     connect(m_searchLine, &QLineEdit::textChanged, this, [this](const QString &str) {
1198         m_proxyModel->slotSetSearchString(str);
1199         if (str.isEmpty()) {
1200             // focus last selected item when clearing search line
1201             QModelIndex current = m_proxyModel->selectionModel()->currentIndex();
1202             if (current.isValid()) {
1203                 m_itemView->scrollTo(current, QAbstractItemView::EnsureVisible);
1204             }
1205         }
1206     });
1207 
1208     auto *leventEater = new LineEventEater(this);
1209     m_searchLine->installEventFilter(leventEater);
1210     connect(leventEater, &LineEventEater::clearSearchLine, m_searchLine, &QLineEdit::clear);
1211     connect(leventEater, &LineEventEater::showClearButton, this, &Bin::showClearButton);
1212 
1213     setFocusPolicy(Qt::ClickFocus);
1214     connect(this, &Bin::refreshPanel, this, &Bin::doRefreshPanel);
1215 
1216     // Zoom slider
1217     QWidget *container = new QWidget(this);
1218     auto *lay = new QHBoxLayout;
1219     m_slider = new QSlider(Qt::Horizontal, this);
1220     m_slider->setMinimumWidth(40);
1221     m_slider->setRange(0, 10);
1222     m_slider->setValue(KdenliveSettings::bin_zoom());
1223     connect(m_slider, &QAbstractSlider::valueChanged, this, &Bin::slotSetIconSize);
1224     auto *tb1 = new QToolButton(this);
1225     tb1->setIcon(QIcon::fromTheme(QStringLiteral("zoom-in")));
1226     connect(tb1, &QToolButton::clicked, this, [&]() { m_slider->setValue(qMin(m_slider->value() + 1, m_slider->maximum())); });
1227     auto *tb2 = new QToolButton(this);
1228     tb2->setIcon(QIcon::fromTheme(QStringLiteral("zoom-out")));
1229     connect(tb2, &QToolButton::clicked, this, [&]() { m_slider->setValue(qMax(m_slider->value() - 1, m_slider->minimum())); });
1230     lay->addWidget(tb2);
1231     lay->addWidget(m_slider);
1232     lay->addWidget(tb1);
1233     container->setLayout(lay);
1234     auto *zoomWidget = new QWidgetAction(this);
1235     zoomWidget->setDefaultWidget(container);
1236 
1237     // View type
1238     KSelectAction *listType = new KSelectAction(QIcon::fromTheme(QStringLiteral("view-list-tree")), i18n("View Mode"), this);
1239     pCore->window()->actionCollection()->addAction(QStringLiteral("bin_view_mode"), listType);
1240     pCore->window()->actionCollection()->setShortcutsConfigurable(listType, false);
1241     QAction *treeViewAction = listType->addAction(QIcon::fromTheme(QStringLiteral("view-list-tree")), i18n("Tree View"));
1242     listType->addAction(treeViewAction);
1243     treeViewAction->setData(BinTreeView);
1244     if (m_listType == treeViewAction->data().toInt()) {
1245         listType->setCurrentAction(treeViewAction);
1246     }
1247     pCore->window()->actionCollection()->addAction(QStringLiteral("bin_view_mode_tree"), treeViewAction);
1248     QAction *profileAction = new QAction(i18n("Adjust Profile to Current Clip"), this);
1249     connect(profileAction, &QAction::triggered, this, &Bin::adjustProjectProfileToItem);
1250     pCore->window()->actionCollection()->addAction(QStringLiteral("project_adjust_profile"), profileAction);
1251 
1252     QAction *iconViewAction = listType->addAction(QIcon::fromTheme(QStringLiteral("view-list-icons")), i18n("Icon View"));
1253     iconViewAction->setData(BinIconView);
1254     if (m_listType == iconViewAction->data().toInt()) {
1255         listType->setCurrentAction(iconViewAction);
1256     }
1257     pCore->window()->actionCollection()->addAction(QStringLiteral("bin_view_mode_icon"), iconViewAction);
1258 
1259     // Sort menu
1260     m_sortDescend = new QAction(i18n("Descending"), this);
1261     m_sortDescend->setCheckable(true);
1262     m_sortDescend->setChecked(KdenliveSettings::binSorting() > 99);
1263     connect(m_sortDescend, &QAction::triggered, this, [&]() {
1264         if (m_sortGroup->checkedAction()) {
1265             int actionData = m_sortGroup->checkedAction()->data().toInt();
1266             if ((m_itemView != nullptr) && m_listType == BinTreeView) {
1267                 auto *view = static_cast<QTreeView *>(m_itemView);
1268                 view->header()->setSortIndicator(actionData, m_sortDescend->isChecked() ? Qt::DescendingOrder : Qt::AscendingOrder);
1269             } else {
1270                 m_proxyModel->sort(actionData, m_sortDescend->isChecked() ? Qt::DescendingOrder : Qt::AscendingOrder);
1271             }
1272             KdenliveSettings::setBinSorting(actionData + (m_sortDescend->isChecked() ? 100 : 0));
1273         }
1274     });
1275 
1276     auto *sortAction = new KActionMenu(i18n("Sort By"), this);
1277     int binSort = KdenliveSettings::binSorting() % 100;
1278     m_sortGroup = new QActionGroup(sortAction);
1279     QAction *sortByName = new QAction(i18n("Name"), m_sortGroup);
1280     sortByName->setCheckable(true);
1281     sortByName->setData(0);
1282     sortByName->setChecked(binSort == 0);
1283     QAction *sortByDate = new QAction(i18n("Date"), m_sortGroup);
1284     sortByDate->setCheckable(true);
1285     sortByDate->setData(1);
1286     sortByDate->setChecked(binSort == 1);
1287     QAction *sortByDesc = new QAction(i18n("Description"), m_sortGroup);
1288     sortByDesc->setCheckable(true);
1289     sortByDesc->setData(2);
1290     sortByDesc->setChecked(binSort == 2);
1291     QAction *sortByType = new QAction(i18n("Type"), m_sortGroup);
1292     sortByType->setCheckable(true);
1293     sortByType->setData(3);
1294     sortByType->setChecked(binSort == 3);
1295     QAction *sortByDuration = new QAction(i18n("Duration"), m_sortGroup);
1296     sortByDuration->setCheckable(true);
1297     sortByDuration->setData(5);
1298     sortByDuration->setChecked(binSort == 5);
1299     QAction *sortByInsert = new QAction(i18n("Insert Order"), m_sortGroup);
1300     sortByInsert->setCheckable(true);
1301     sortByInsert->setData(6);
1302     sortByInsert->setChecked(binSort == 6);
1303     QAction *sortByRating = new QAction(i18n("Rating"), m_sortGroup);
1304     sortByRating->setCheckable(true);
1305     sortByRating->setData(7);
1306     sortByRating->setChecked(binSort == 7);
1307     sortAction->addAction(sortByName);
1308     sortAction->addAction(sortByDate);
1309     sortAction->addAction(sortByDuration);
1310     sortAction->addAction(sortByRating);
1311     sortAction->addAction(sortByType);
1312     sortAction->addAction(sortByInsert);
1313     sortAction->addAction(sortByDesc);
1314     sortAction->addSeparator();
1315     sortAction->addAction(m_sortDescend);
1316     connect(m_sortGroup, &QActionGroup::triggered, this, [&](QAction *ac) {
1317         int actionData = ac->data().toInt();
1318         if ((m_itemView != nullptr) && m_listType == BinTreeView) {
1319             auto *view = static_cast<QTreeView *>(m_itemView);
1320             view->header()->setSortIndicator(actionData, m_sortDescend->isChecked() ? Qt::DescendingOrder : Qt::AscendingOrder);
1321         } else {
1322             m_proxyModel->sort(actionData, m_sortDescend->isChecked() ? Qt::DescendingOrder : Qt::AscendingOrder);
1323         }
1324         KdenliveSettings::setBinSorting(actionData + (m_sortDescend->isChecked() ? 100 : 0));
1325     });
1326 
1327     QAction *disableEffects = new QAction(i18n("Disable Bin Effects"), this);
1328     disableEffects->setIcon(QIcon::fromTheme(QStringLiteral("favorite")));
1329     disableEffects->setData("disable_bin_effects");
1330     disableEffects->setCheckable(true);
1331     disableEffects->setChecked(false);
1332     connect(disableEffects, &QAction::triggered, this, [this](bool disable) { this->setBinEffectsEnabled(!disable); });
1333     pCore->window()->actionCollection()->addAction(QStringLiteral("disable_bin_effects"), disableEffects);
1334 
1335     QAction *hoverPreview = new QAction(i18n("Show Video Preview in Thumbnails"), this);
1336     hoverPreview->setCheckable(true);
1337     hoverPreview->setChecked(KdenliveSettings::hoverPreview());
1338     connect(hoverPreview, &QAction::triggered, [](bool checked) { KdenliveSettings::setHoverPreview(checked); });
1339 
1340     listType->setToolBarMode(KSelectAction::MenuMode);
1341 #if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 240, 0)
1342     connect(listType, &KSelectAction::actionTriggered, this, &Bin::slotInitView);
1343 #else
1344     connect(listType, static_cast<void (KSelectAction::*)(QAction *)>(&KSelectAction::triggered), this, &Bin::slotInitView);
1345 #endif
1346 
1347     // Settings menu
1348     auto *settingsAction = new KActionMenu(QIcon::fromTheme(QStringLiteral("application-menu")), i18n("Options"), this);
1349     settingsAction->setWhatsThis(xi18nc("@info:whatsthis", "Opens a window to configure the project bin (e.g. view mode, sort, show rating)."));
1350     settingsAction->setPopupMode(QToolButton::InstantPopup);
1351     settingsAction->addAction(zoomWidget);
1352     settingsAction->addAction(listType);
1353     settingsAction->addAction(sortAction);
1354 
1355     // Column show / hide actions
1356     m_showDate = new QAction(i18n("Show Date"), this);
1357     m_showDate->setCheckable(true);
1358     m_showDate->setData(1);
1359     connect(m_showDate, &QAction::triggered, this, &Bin::slotShowColumn);
1360     m_showDesc = new QAction(i18n("Show Description"), this);
1361     m_showDesc->setCheckable(true);
1362     m_showDesc->setData(2);
1363     connect(m_showDesc, &QAction::triggered, this, &Bin::slotShowColumn);
1364     m_showRating = new QAction(i18n("Show Rating"), this);
1365     m_showRating->setCheckable(true);
1366     m_showRating->setData(7);
1367     connect(m_showRating, &QAction::triggered, this, &Bin::slotShowColumn);
1368 
1369     settingsAction->addAction(m_showDate);
1370     settingsAction->addAction(m_showDesc);
1371     settingsAction->addAction(m_showRating);
1372     settingsAction->addAction(disableEffects);
1373     settingsAction->addAction(hoverPreview);
1374 
1375     if (!m_isMainBin) {
1376         // Add close action
1377         QAction *close = KStandardAction::close(this, SIGNAL(requestBinClose()), this);
1378         settingsAction->addAction(close);
1379     }
1380 
1381     // Show tags panel
1382     m_tagAction = new QAction(QIcon::fromTheme(QStringLiteral("tag")), i18n("Tags Panel"), this);
1383     m_tagAction->setCheckable(true);
1384     m_toolbar->addAction(m_tagAction);
1385     connect(m_tagAction, &QAction::triggered, this, [&](bool triggered) {
1386         if (triggered) {
1387             m_tagsWidget->setVisible(true);
1388         } else {
1389             m_tagsWidget->setVisible(false);
1390         }
1391     });
1392 
1393     // Filter menu
1394     m_filterTagGroup.setExclusive(false);
1395     m_filterRateGroup.setExclusive(false);
1396     m_filterUsageGroup.setExclusive(true);
1397     m_filterTypeGroup.setExclusive(false);
1398     m_filterMenu = new QMenu(i18n("Filter"), this);
1399     m_filterButton = new QToolButton;
1400     m_filterButton->setCheckable(true);
1401     m_filterButton->setPopupMode(QToolButton::MenuButtonPopup);
1402     m_filterButton->setIcon(QIcon::fromTheme(QStringLiteral("view-filter")));
1403     m_filterButton->setToolTip(i18n("Filter"));
1404     m_filterButton->setWhatsThis(xi18nc("@info:whatsthis", "Filter the project bin contents. Click on the filter icon to toggle the filter display. Click on "
1405                                                            "the arrow icon to open a list of possible filter settings."));
1406     m_filterButton->setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont));
1407     m_filterButton->setMenu(m_filterMenu);
1408 
1409     connect(m_filterButton, &QToolButton::toggled, this, [this](bool toggle) {
1410         if (!toggle) {
1411             m_proxyModel->slotClearSearchFilters();
1412             return;
1413         }
1414         slotApplyFilters();
1415     });
1416 
1417     connect(m_filterMenu, &QMenu::triggered, this, [this](QAction *action) {
1418         if (action->data().toString().isEmpty()) {
1419             // Clear filters action
1420             QSignalBlocker bk(m_filterMenu);
1421             QList<QAction *> list = m_filterMenu->actions();
1422             list << m_filterTypeGroup.actions();
1423             for (QAction *ac : qAsConst(list)) {
1424                 ac->setChecked(false);
1425             }
1426             m_proxyModel->slotClearSearchFilters();
1427             m_filterButton->setChecked(false);
1428             return;
1429         }
1430         slotApplyFilters();
1431     });
1432 
1433     m_tagAction->setCheckable(true);
1434     m_toolbar->addAction(m_tagAction);
1435     m_toolbar->addAction(settingsAction);
1436 
1437     if (m_isMainBin) {
1438         // small info button for pending jobs
1439         m_infoLabel = new SmallJobLabel(this);
1440         m_infoLabel->setStyleSheet(SmallJobLabel::getStyleSheet(palette()));
1441         connect(&pCore->taskManager, &TaskManager::jobCount, m_infoLabel, &SmallJobLabel::slotSetJobCount);
1442         QAction *infoAction = m_toolbar->addWidget(m_infoLabel);
1443         m_jobsMenu = new QMenu(this);
1444         // connect(m_jobsMenu, &QMenu::aboutToShow, this, &Bin::slotPrepareJobsMenu);
1445         m_cancelJobs = new QAction(i18n("Cancel All Jobs"), this);
1446         m_cancelJobs->setCheckable(false);
1447         m_discardCurrentClipJobs = new QAction(i18n("Cancel Current Clip Jobs"), this);
1448         m_discardCurrentClipJobs->setCheckable(false);
1449         m_discardPendingJobs = new QAction(i18n("Cancel Pending Jobs"), this);
1450         m_discardPendingJobs->setCheckable(false);
1451         m_jobsMenu->addAction(m_cancelJobs);
1452         m_jobsMenu->addAction(m_discardCurrentClipJobs);
1453         m_jobsMenu->addAction(m_discardPendingJobs);
1454         m_infoLabel->setMenu(m_jobsMenu);
1455         m_infoLabel->setAction(infoAction);
1456 
1457         connect(m_discardCurrentClipJobs, &QAction::triggered, this, [&]() {
1458             const QString currentId = m_monitor->activeClipId();
1459             if (!currentId.isEmpty()) {
1460                 pCore->taskManager.discardJobs(ObjectId(KdenliveObjectType::BinClip, currentId.toInt(), QUuid()), AbstractTask::NOJOBTYPE, true);
1461             }
1462         });
1463         connect(m_cancelJobs, &QAction::triggered, [&]() { pCore->taskManager.slotCancelJobs(); });
1464         connect(m_discardPendingJobs, &QAction::triggered, [&]() {
1465             // TODO: implement pending only deletion
1466             pCore->taskManager.slotCancelJobs();
1467         });
1468     }
1469     // Hack, create toolbar spacer
1470     QWidget *spacer = new QWidget();
1471     spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
1472     m_toolbar->addWidget(spacer);
1473 
1474     // Add filter and search line
1475     m_toolbar->addWidget(m_filterButton);
1476     m_toolbar->addWidget(m_searchLine);
1477 
1478     // connect(pCore->projectManager(), SIGNAL(projectOpened(Project*)), this, SLOT(setProject(Project*)));
1479     m_headerInfo = QByteArray::fromBase64(KdenliveSettings::treeviewheaders().toLatin1());
1480     if (m_isMainBin) {
1481         m_propertiesPanel = new QScrollArea(this);
1482         m_propertiesPanel->setFrameShape(QFrame::NoFrame);
1483         m_propertiesPanel->setAccessibleName(i18n("Bin Clip Properties"));
1484     }
1485     // Insert listview
1486     m_itemView = new MyTreeView(this);
1487     m_layout->addWidget(m_itemView);
1488     // Info widget for failed jobs, other errors
1489     m_infoMessage = new KMessageWidget(this);
1490     m_layout->addWidget(m_infoMessage);
1491     m_infoMessage->setCloseButtonVisible(false);
1492     connect(m_infoMessage, &KMessageWidget::hideAnimationFinished, this, &Bin::slotResetInfoMessage);
1493     // m_infoMessage->setWordWrap(true);
1494     m_infoMessage->hide();
1495     connect(this, &Bin::requesteInvalidRemoval, this, &Bin::slotQueryRemoval);
1496     connect(pCore.get(), &Core::updatePalette, this, &Bin::slotUpdatePalette);
1497     connect(m_itemModel.get(), &QAbstractItemModel::rowsInserted, this, &Bin::updateClipsCount);
1498     connect(m_itemModel.get(), &QAbstractItemModel::rowsRemoved, this, &Bin::updateClipsCount);
1499     connect(this, SIGNAL(displayBinMessage(QString, KMessageWidget::MessageType)), this, SLOT(doDisplayMessage(QString, KMessageWidget::MessageType)));
1500     wheelAccumulatedDelta = 0;
1501 }
1502 
1503 Bin::~Bin()
1504 {
1505     if (m_isMainBin) {
1506         disconnect(&pCore->taskManager, &TaskManager::jobCount, m_infoLabel, &SmallJobLabel::slotSetJobCount);
1507         pCore->taskManager.slotCancelJobs();
1508         blockSignals(true);
1509         m_proxyModel->selectionModel()->blockSignals(true);
1510         setEnabled(false);
1511         m_propertiesPanel = nullptr;
1512         abortOperations();
1513         m_itemModel->clean();
1514     } else {
1515         blockSignals(true);
1516         setEnabled(false);
1517         m_proxyModel->selectionModel()->blockSignals(true);
1518     }
1519 }
1520 
1521 void Bin::slotUpdatePalette()
1522 {
1523     if (m_isMainBin) {
1524         // Refresh icons
1525         QIcon audioIcon = QIcon::fromTheme(QStringLiteral("audio-volume-medium"));
1526         QIcon videoIcon = QIcon::fromTheme(QStringLiteral("kdenlive-show-video"));
1527         m_audioIcon.fill(Qt::transparent);
1528         m_videoIcon.fill(Qt::transparent);
1529         QPainter p(&m_audioIcon);
1530         audioIcon.paint(&p, 0, 0, m_audioIcon.width(), m_audioIcon.height());
1531         p.end();
1532         QPainter p2(&m_videoIcon);
1533         videoIcon.paint(&p2, 0, 0, m_videoIcon.width(), m_videoIcon.height());
1534         p2.end();
1535         m_audioUsedIcon = m_audioIcon;
1536         KIconEffect::toMonochrome(m_audioUsedIcon, palette().link().color(), palette().link().color(), 1);
1537         m_videoUsedIcon = m_videoIcon;
1538         KIconEffect::toMonochrome(m_videoUsedIcon, palette().link().color(), palette().link().color(), 1);
1539     }
1540 }
1541 
1542 QDockWidget *Bin::clipPropertiesDock()
1543 {
1544     return m_propertiesDock;
1545 }
1546 
1547 void Bin::abortOperations()
1548 {
1549     m_infoMessage->hide();
1550     blockSignals(true);
1551     if (m_propertiesPanel) {
1552         for (QWidget *w : m_propertiesPanel->findChildren<ClipPropertiesController *>()) {
1553             delete w;
1554         }
1555     }
1556     delete m_itemView;
1557     m_itemView = nullptr;
1558     blockSignals(false);
1559 }
1560 
1561 bool Bin::eventFilter(QObject *obj, QEvent *event)
1562 {
1563     if (event->type() == QEvent::MouseButtonPress) {
1564         if (m_itemView && m_listType == BinTreeView) {
1565             // Folder state is only valid in tree view mode
1566             auto *mouseEvent = static_cast<QMouseEvent *>(event);
1567             if (mouseEvent->button() == Qt::LeftButton && mouseEvent->modifiers() & Qt::ShiftModifier) {
1568                 QModelIndex idx = m_itemView->indexAt(mouseEvent->pos());
1569                 if (idx.isValid() && idx.column() == 0 && m_proxyModel) {
1570                     std::shared_ptr<AbstractProjectItem> item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(idx));
1571                     if (item->itemType() == AbstractProjectItem::FolderItem) {
1572                         auto *tView = static_cast<QTreeView *>(m_itemView);
1573                         QRect r = tView->visualRect(idx);
1574                         if (mouseEvent->pos().x() < r.x()) {
1575                             if (!tView->isExpanded(idx)) {
1576                                 tView->expandAll();
1577                             } else {
1578                                 tView->collapseAll();
1579                             }
1580                             return true;
1581                         }
1582                     }
1583                 }
1584             }
1585         }
1586     }
1587     if (event->type() == QEvent::MouseButtonRelease) {
1588         if (!m_monitor->isActive()) {
1589             m_monitor->slotActivateMonitor();
1590         } else {
1591             // Force raise
1592             m_monitor->parentWidget()->raise();
1593         }
1594         bool success = QWidget::eventFilter(obj, event);
1595         if (m_gainedFocus) {
1596             auto *mouseEvent = static_cast<QMouseEvent *>(event);
1597             if (m_itemView) {
1598                 QModelIndex idx = m_itemView->indexAt(mouseEvent->pos());
1599                 m_gainedFocus = false;
1600                 if (idx.isValid() && m_proxyModel) {
1601                     std::shared_ptr<AbstractProjectItem> item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(idx));
1602                     if (item->itemType() == AbstractProjectItem::ClipItem) {
1603                         auto clip = std::static_pointer_cast<ProjectClip>(item);
1604                         if (clip && clip->statusReady()) {
1605                             editMasterEffect(item);
1606                         }
1607                     } else if (item->itemType() == AbstractProjectItem::SubClipItem) {
1608                         auto clip = std::static_pointer_cast<ProjectSubClip>(item)->getMasterClip();
1609                         if (clip && clip->statusReady()) {
1610                             editMasterEffect(item);
1611                         }
1612                     }
1613                 } else {
1614                     editMasterEffect(nullptr);
1615                 }
1616             }
1617             // make sure we discard the focus indicator
1618             m_gainedFocus = false;
1619         }
1620         return success;
1621     }
1622     if (event->type() == QEvent::MouseButtonDblClick) {
1623         auto *mouseEvent = static_cast<QMouseEvent *>(event);
1624         if (m_itemView) {
1625             QModelIndex idx = m_itemView->indexAt(mouseEvent->pos());
1626             if (!idx.isValid()) {
1627                 // User double clicked on empty area
1628                 slotAddClip();
1629             } else {
1630                 slotItemDoubleClicked(idx, mouseEvent->pos(), mouseEvent->modifiers());
1631             }
1632         } else {
1633             qCDebug(KDENLIVE_LOG) << " +++++++ NO VIEW-------!!";
1634         }
1635         return true;
1636     }
1637     if (event->type() == QEvent::Wheel) {
1638         auto *e = static_cast<QWheelEvent *>(event);
1639         if ((e != nullptr) && e->modifiers() == Qt::ControlModifier) {
1640             wheelAccumulatedDelta += e->angleDelta().y();
1641             if (abs(wheelAccumulatedDelta) >= QWheelEvent::DefaultDeltasPerStep) {
1642                 slotZoomView(wheelAccumulatedDelta > 0);
1643             }
1644             // Q_EMIT zoomView(e->delta() > 0);
1645             return true;
1646         }
1647     }
1648     return QWidget::eventFilter(obj, event);
1649 }
1650 
1651 void Bin::refreshIcons()
1652 {
1653     QList<QMenu *> allMenus = this->findChildren<QMenu *>();
1654     for (int i = 0; i < allMenus.count(); i++) {
1655         QMenu *m = allMenus.at(i);
1656         QIcon ic = m->icon();
1657         if (ic.isNull() || ic.name().isEmpty()) {
1658             continue;
1659         }
1660         QIcon newIcon = QIcon::fromTheme(ic.name());
1661         m->setIcon(newIcon);
1662     }
1663     QList<QToolButton *> allButtons = this->findChildren<QToolButton *>();
1664     for (int i = 0; i < allButtons.count(); i++) {
1665         QToolButton *m = allButtons.at(i);
1666         QIcon ic = m->icon();
1667         if (ic.isNull() || ic.name().isEmpty()) {
1668             continue;
1669         }
1670         QIcon newIcon = QIcon::fromTheme(ic.name());
1671         m->setIcon(newIcon);
1672     }
1673 }
1674 
1675 void Bin::slotSaveHeaders()
1676 {
1677     if ((m_itemView != nullptr) && m_listType == BinTreeView) {
1678         // save current treeview state (column width)
1679         auto *view = static_cast<QTreeView *>(m_itemView);
1680         m_headerInfo = view->header()->saveState();
1681         KdenliveSettings::setTreeviewheaders(m_headerInfo.toBase64());
1682     }
1683 }
1684 
1685 void Bin::updateSortingAction(int ix)
1686 {
1687     if (ix == KdenliveSettings::binSorting()) {
1688         return;
1689     }
1690     int index = ix % 100;
1691     for (QAction *ac : m_sortGroup->actions()) {
1692         if (ac->data().toInt() == index) {
1693             ac->setChecked(true);
1694             ac->trigger();
1695         }
1696     }
1697     if ((ix > 99) != m_sortDescend->isChecked()) {
1698         m_sortDescend->trigger();
1699     }
1700 }
1701 
1702 void Bin::slotZoomView(bool zoomIn)
1703 {
1704     wheelAccumulatedDelta = 0;
1705     if (m_itemModel->rowCount() == 0) {
1706         // Don't zoom on empty bin
1707         return;
1708     }
1709     int progress = (zoomIn) ? 1 : -1;
1710     m_slider->setValue(m_slider->value() + progress);
1711 }
1712 
1713 Monitor *Bin::monitor()
1714 {
1715     return m_monitor;
1716 }
1717 
1718 void Bin::slotAddClip()
1719 {
1720     // Check if we are in a folder
1721     QString parentFolder = getCurrentFolder();
1722     ClipCreationDialog::createClipsCommand(m_doc, parentFolder, m_itemModel);
1723     pCore->window()->raiseBin();
1724 }
1725 
1726 std::shared_ptr<ProjectClip> Bin::getFirstSelectedClip()
1727 {
1728     const QModelIndexList indexes = m_proxyModel->selectionModel()->selectedIndexes();
1729     if (indexes.isEmpty()) {
1730         return std::shared_ptr<ProjectClip>();
1731     }
1732     for (const QModelIndex &ix : indexes) {
1733         std::shared_ptr<AbstractProjectItem> item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(ix));
1734         if (item->itemType() == AbstractProjectItem::ClipItem) {
1735             auto clip = std::static_pointer_cast<ProjectClip>(item);
1736             if (clip) {
1737                 return clip;
1738             }
1739         }
1740     }
1741     return nullptr;
1742 }
1743 
1744 void Bin::slotDeleteClip()
1745 {
1746     const QModelIndexList indexes = m_proxyModel->selectionModel()->selectedIndexes();
1747     std::vector<std::shared_ptr<AbstractProjectItem>> items;
1748     bool included = false;
1749     bool usedFolder = false;
1750     QList<QUuid> sequences;
1751     auto checkInclusion = [](bool accum, std::shared_ptr<TreeItem> item) {
1752         return accum || std::static_pointer_cast<AbstractProjectItem>(item)->isIncludedInTimeline();
1753     };
1754     for (const QModelIndex &ix : indexes) {
1755         if (!ix.isValid() || ix.column() != 0) {
1756             continue;
1757         }
1758         std::shared_ptr<AbstractProjectItem> item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(ix));
1759         if (!item) {
1760             qDebug() << "Suspicious: item not found when trying to delete";
1761             continue;
1762         }
1763         included = included || item->accumulate(false, checkInclusion);
1764         // Check if we are deleting non-empty folders:
1765         usedFolder = usedFolder || item->childCount() > 0;
1766         if (item->itemType() == AbstractProjectItem::FolderItem) {
1767             QList<std::shared_ptr<ProjectClip>> children = std::static_pointer_cast<ProjectFolder>(item)->childClips();
1768             for (auto &c : children) {
1769                 if (c->clipType() == ClipType::Timeline) {
1770                     const QUuid uuid = c->getSequenceUuid();
1771                     sequences << uuid;
1772                 }
1773             }
1774 
1775         } else if (item->itemType() == AbstractProjectItem::ClipItem) {
1776             auto c = std::static_pointer_cast<ProjectClip>(item);
1777             if (c->clipType() == ClipType::Timeline) {
1778                 const QUuid uuid = c->getSequenceUuid();
1779                 sequences << uuid;
1780             }
1781         }
1782         items.push_back(item);
1783     }
1784     if (!sequences.isEmpty()) {
1785         if (sequences.count() == m_itemModel->sequenceCount()) {
1786             KMessageBox::error(this, i18n("You cannot delete all sequences of a project"));
1787             return;
1788         }
1789         if (KMessageBox::warningContinueCancel(this, i18n("Deleting sequences cannot be undone")) != KMessageBox::Continue) {
1790             return;
1791         }
1792         for (auto seq : sequences) {
1793             pCore->projectManager()->closeTimeline(seq, true);
1794         }
1795     }
1796     if (included && (KMessageBox::warningContinueCancel(this, i18n("This will delete all selected clips from the timeline")) != KMessageBox::Continue)) {
1797         return;
1798     }
1799     if (usedFolder && (KMessageBox::warningContinueCancel(this, i18n("This will delete all folder content")) != KMessageBox::Continue)) {
1800         return;
1801     }
1802     // Clear undostack as it contains references to deleted timeline models
1803     pCore->undoStack()->clear();
1804     Fun undo = []() { return true; };
1805     Fun redo = []() { return true; };
1806     // Ensure we don't delete a parent before a child
1807     // std::sort(items.begin(), items.end(), [](std::shared_ptr<AbstractProjectItem> a, std::shared_ptr<AbstractProjectItem>b) { return a->depth() > b->depth();
1808     // });
1809     QStringList notDeleted;
1810     for (const auto &item : items) {
1811         if (!m_itemModel->requestBinClipDeletion(item, undo, redo)) {
1812             notDeleted << item->name();
1813         }
1814     }
1815     if (!notDeleted.isEmpty()) {
1816         KMessageBox::errorList(this, i18n("Some items could not be deleted. Maybe there are instances on locked tracks?"), notDeleted);
1817     }
1818     pCore->pushUndo(undo, redo, i18n("Delete bin Clips"));
1819 }
1820 
1821 void Bin::slotReloadClip()
1822 {
1823     qDebug() << "---------\nRELOADING CLIP\n----------------";
1824     const QModelIndexList indexes = m_proxyModel->selectionModel()->selectedIndexes();
1825     for (const QModelIndex &ix : indexes) {
1826         if (!ix.isValid() || ix.column() != 0) {
1827             continue;
1828         }
1829         std::shared_ptr<AbstractProjectItem> item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(ix));
1830         std::shared_ptr<ProjectClip> currentItem = nullptr;
1831         if (item->itemType() == AbstractProjectItem::ClipItem) {
1832             currentItem = std::static_pointer_cast<ProjectClip>(item);
1833         } else if (item->itemType() == AbstractProjectItem::SubClipItem) {
1834             currentItem = std::static_pointer_cast<ProjectSubClip>(item)->getMasterClip();
1835         }
1836         if (currentItem) {
1837             Q_EMIT openClip(std::shared_ptr<ProjectClip>());
1838             if (currentItem->clipStatus() == FileStatus::StatusMissing || currentItem->clipStatus() == FileStatus::StatusProxyOnly) {
1839                 // Don't attempt to reload missing clip
1840                 // Check if source file is available
1841                 const QString sourceUrl = currentItem->url();
1842                 if (!QFile::exists(sourceUrl)) {
1843                     Q_EMIT displayBinMessage(i18n("Missing source clip"), KMessageWidget::Warning);
1844                     return;
1845                 }
1846             }
1847             if (currentItem->clipType() == ClipType::Playlist) {
1848                 // Check if a clip inside playlist is missing
1849                 QString path = currentItem->url();
1850                 QFile f(path);
1851                 QDomDocument doc;
1852                 if (!Xml::docContentFromFile(doc, path, false)) {
1853                     DocumentChecker d(QUrl::fromLocalFile(path), doc);
1854                     if (!d.hasErrorInProject() && doc.documentElement().hasAttribute(QStringLiteral("modified"))) {
1855                         QString backupFile = path + QStringLiteral(".backup");
1856                         KIO::FileCopyJob *copyjob = KIO::file_copy(QUrl::fromLocalFile(path), QUrl::fromLocalFile(backupFile));
1857                         if (copyjob->exec()) {
1858                             if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) {
1859                                 KMessageBox::error(this, i18n("Unable to write to file %1", path));
1860                             } else {
1861                                 QTextStream out(&f);
1862 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
1863                                 out.setCodec("UTF-8");
1864 #endif
1865                                 out << doc.toString();
1866                                 f.close();
1867                                 KMessageBox::information(
1868                                     this,
1869                                     i18n("Your project file was modified by Kdenlive.\nTo make sure you do not lose data, a backup copy called %1 was created.",
1870                                          backupFile));
1871                             }
1872                         }
1873                     }
1874                 } else {
1875                     KMessageBox::error(this, i18n("Unable to parse file %1", path));
1876                 }
1877             }
1878             currentItem->reloadProducer(false, false, true);
1879         }
1880     }
1881 }
1882 
1883 void Bin::replaceSingleClip(const QString clipId, const QString &newUrl)
1884 {
1885     if (newUrl.isEmpty() || !QFile::exists(newUrl)) {
1886         Q_EMIT displayBinMessage(i18n("Cannot replace clip with invalid file %1", QFileInfo(newUrl).fileName()), KMessageWidget::Information);
1887         return;
1888     }
1889     std::shared_ptr<ProjectClip> currentItem = getBinClip(clipId);
1890     if (currentItem) {
1891         QMap<QString, QString> sourceProps;
1892         QMap<QString, QString> newProps;
1893         sourceProps.insert(QStringLiteral("resource"), currentItem->url());
1894         sourceProps.insert(QStringLiteral("kdenlive:originalurl"), currentItem->url());
1895         sourceProps.insert(QStringLiteral("kdenlive:clipname"), currentItem->clipName());
1896         sourceProps.insert(QStringLiteral("kdenlive:proxy"), currentItem->getProducerProperty(QStringLiteral("kdenlive:proxy")));
1897         sourceProps.insert(QStringLiteral("_fullreload"), QStringLiteral("1"));
1898         newProps.insert(QStringLiteral("resource"), newUrl);
1899         newProps.insert(QStringLiteral("kdenlive:originalurl"), newUrl);
1900         newProps.insert(QStringLiteral("kdenlive:clipname"), QFileInfo(newUrl).fileName());
1901         newProps.insert(QStringLiteral("kdenlive:proxy"), QStringLiteral("-"));
1902         newProps.insert(QStringLiteral("_fullreload"), QStringLiteral("1"));
1903         // Check if replacement clip is long enough
1904         if (currentItem->hasLimitedDuration() && currentItem->isIncludedInTimeline()) {
1905             // Clip is used in timeline, make sure length is similar
1906             std::unique_ptr<Mlt::Producer> replacementProd(new Mlt::Producer(pCore->getProjectProfile(), newUrl.toUtf8().constData()));
1907             int currentDuration = int(currentItem->frameDuration());
1908             if (replacementProd->is_valid()) {
1909                 int replacementDuration = replacementProd->get_length();
1910                 if (replacementDuration < currentDuration) {
1911                     if (KMessageBox::warningContinueCancel(
1912                             this, i18n("You are replacing a clip with a shorter one, this might cause issues in timeline.\nReplacement is %1 frames shorter.",
1913                                        (currentDuration - replacementDuration))) != KMessageBox::Continue) {
1914                         return;
1915                         ;
1916                     }
1917                 }
1918             } else {
1919                 KMessageBox::error(this, i18n("The selected file %1 is invalid.", newUrl));
1920                 return;
1921             }
1922         }
1923         slotEditClipCommand(currentItem->clipId(), sourceProps, newProps);
1924     } else {
1925         Q_EMIT displayBinMessage(i18n("Cannot find original clip to be replaced"), KMessageWidget::Information);
1926     }
1927 }
1928 
1929 void Bin::slotReplaceClipInTimeline()
1930 {
1931     // Check if we have 2 selected clips
1932     const QModelIndexList indexes = m_proxyModel->selectionModel()->selectedIndexes();
1933     QModelIndex activeIndex = m_proxyModel->selectionModel()->currentIndex();
1934     std::shared_ptr<AbstractProjectItem> selectedItem = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(activeIndex));
1935     std::shared_ptr<AbstractProjectItem> secondaryItem = nullptr;
1936     for (const QModelIndex &ix : indexes) {
1937         if (!ix.isValid() || ix.column() != 0 || ix == activeIndex) {
1938             continue;
1939         }
1940         if (secondaryItem != nullptr) {
1941             // More than 2 clips selected, abort
1942             secondaryItem = nullptr;
1943             break;
1944         }
1945         secondaryItem = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(ix));
1946     }
1947     if (selectedItem == nullptr || secondaryItem == nullptr || selectedItem->itemType() != AbstractProjectItem::ClipItem ||
1948         secondaryItem->itemType() != AbstractProjectItem::ClipItem) {
1949         KMessageBox::information(this, i18n("You need to select 2 clips (the replacement clip and the original clip) for timeline replacement."));
1950         return;
1951     }
1952     // Check that we don't try to embed a sequence onto itself
1953     auto selected = std::static_pointer_cast<ProjectClip>(selectedItem);
1954     if (selected->timelineInstances().isEmpty()) {
1955         KMessageBox::information(this, i18n("The current clip <b>%1</b> is not inserted in the active timeline.", selected->clipName()));
1956         return;
1957     }
1958     auto secondary = std::static_pointer_cast<ProjectClip>(secondaryItem);
1959     if (secondary->clipType() == ClipType::Timeline && secondary->getSequenceUuid() == pCore->currentTimelineId()) {
1960         KMessageBox::information(this, i18n("You cannot insert the sequence <b>%1</b> into itself.", secondary->clipName()));
1961         return;
1962     }
1963     std::pair<bool, bool> hasAV = {selected->hasAudio(), selected->hasVideo()};
1964     std::pair<bool, bool> secondaryHasAV = {secondary->hasAudio(), secondary->hasVideo()};
1965     std::pair<bool, bool> replacementAV = {false, false};
1966     if (secondaryHasAV.first && hasAV.first) {
1967         // Both clips have audio
1968         replacementAV.first = true;
1969         if (secondaryHasAV.second && hasAV.second) {
1970             // Both clips have video, ask what user wants
1971             replacementAV.second = true;
1972         }
1973     } else if (secondaryHasAV.second && hasAV.second) {
1974         replacementAV.second = true;
1975     }
1976     qDebug() << "ANALYSIS RESULT:\nMASTER: " << hasAV << "\nSECONDARY: " << secondaryHasAV << "\nRESULT: " << replacementAV;
1977     if (replacementAV.first == false && replacementAV.second == false) {
1978         KMessageBox::information(this,
1979                                  i18n("The selected clips %1 and %2 don't match, cannot replace audio nor video", selected->clipName(), secondary->clipName()));
1980         return;
1981     }
1982     int sourceDuration = selected->frameDuration();
1983     int replaceDuration = secondary->frameDuration();
1984     QDialog d(this);
1985     QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
1986     auto *l = new QVBoxLayout;
1987     d.setLayout(l);
1988     if (replaceDuration != sourceDuration) {
1989         // Display respective clip duration in case of mismatch
1990         l->addWidget(new QLabel(i18n("Replacing instances of clip <b>%1</b> (%2)<br/>in active timeline\nwith clip <b>%3</b> (%4)", selected->clipName(),
1991                                      selected->getStringDuration(), secondary->clipName(), secondary->getStringDuration()),
1992                                 &d));
1993     } else {
1994         l->addWidget(new QLabel(
1995             i18n("Replacing instances of clip <b>%1</b><br/>in active timeline\nwith clip <b>%2</b>", selected->clipName(), secondary->clipName()), &d));
1996     }
1997     QCheckBox *cbAudio = new QCheckBox(i18n("Replace audio"), this);
1998     cbAudio->setEnabled(replacementAV.first);
1999     cbAudio->setChecked(replacementAV.first);
2000     QCheckBox *cbVideo = new QCheckBox(i18n("Replace video"), this);
2001     cbVideo->setEnabled(replacementAV.second);
2002     cbVideo->setChecked(replacementAV.second && !replacementAV.first);
2003     l->addWidget(cbAudio);
2004     l->addWidget(cbVideo);
2005     if (sourceDuration != replaceDuration) {
2006         KMessageWidget *km = new KMessageWidget(this);
2007         km->setCloseButtonVisible(false);
2008         if (sourceDuration > replaceDuration) {
2009             km->setMessageType(KMessageWidget::Warning);
2010             km->setText(i18n("Replacement clip is shorter than source clip.<br><b>Some clips in timeline might not be replaced</b>"));
2011         } else {
2012             km->setMessageType(KMessageWidget::Information);
2013             km->setText(i18n("Replacement clip duration is longer that your source clip"));
2014         }
2015         l->addWidget(km);
2016     }
2017     l->addWidget(buttonBox);
2018     d.connect(buttonBox, &QDialogButtonBox::rejected, &d, &QDialog::reject);
2019     d.connect(buttonBox, &QDialogButtonBox::accepted, &d, &QDialog::accept);
2020     if (d.exec() != QDialog::Accepted || (!cbVideo->isCheckable() && !cbAudio->isChecked())) {
2021         return;
2022     }
2023     pCore->projectManager()->replaceTimelineInstances(selected->clipId(), secondary->clipId(), cbAudio->isChecked(), cbVideo->isChecked());
2024 }
2025 
2026 void Bin::slotReplaceClip()
2027 {
2028     const QModelIndexList indexes = m_proxyModel->selectionModel()->selectedIndexes();
2029     QModelIndex activeSelection = m_proxyModel->selectionModel()->currentIndex();
2030     for (const QModelIndex &ix : indexes) {
2031         if (!ix.isValid() || ix.column() != 0) {
2032             continue;
2033         }
2034         std::shared_ptr<AbstractProjectItem> item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(ix));
2035         std::shared_ptr<ProjectClip> currentItem = nullptr;
2036         if (item->itemType() == AbstractProjectItem::ClipItem) {
2037             currentItem = std::static_pointer_cast<ProjectClip>(item);
2038             if (ix == activeSelection) {
2039                 qDebug() << "==== FOUND ACTIVE CLIP: " << currentItem->clipUrl();
2040             } else {
2041                 qDebug() << "==== FOUND SELECED CLIP: " << currentItem->clipUrl();
2042             }
2043         } else if (item->itemType() == AbstractProjectItem::SubClipItem) {
2044             currentItem = std::static_pointer_cast<ProjectSubClip>(item)->getMasterClip();
2045         }
2046         if (currentItem) {
2047             Q_EMIT openClip(std::shared_ptr<ProjectClip>());
2048             QString fileName = QFileDialog::getOpenFileName(this, i18nc("@title:window", "Open Replacement for %1", currentItem->clipName()),
2049                                                             QFileInfo(currentItem->url()).absolutePath(), ClipCreationDialog::getExtensionsFilter());
2050             if (!fileName.isEmpty()) {
2051                 QMap<QString, QString> sourceProps;
2052                 QMap<QString, QString> newProps;
2053                 std::pair<bool, bool> hasAV = {currentItem->hasAudio(), currentItem->hasVideo()};
2054                 sourceProps.insert(QStringLiteral("resource"), currentItem->url());
2055                 sourceProps.insert(QStringLiteral("kdenlive:originalurl"), currentItem->url());
2056                 sourceProps.insert(QStringLiteral("kdenlive:clipname"), currentItem->clipName());
2057                 sourceProps.insert(QStringLiteral("kdenlive:proxy"), currentItem->getProducerProperty(QStringLiteral("kdenlive:proxy")));
2058                 sourceProps.insert(QStringLiteral("_fullreload"), QStringLiteral("1"));
2059                 newProps.insert(QStringLiteral("resource"), fileName);
2060                 newProps.insert(QStringLiteral("kdenlive:originalurl"), fileName);
2061                 newProps.insert(QStringLiteral("kdenlive:clipname"), QFileInfo(fileName).fileName());
2062                 newProps.insert(QStringLiteral("kdenlive:proxy"), QStringLiteral("-"));
2063                 newProps.insert(QStringLiteral("_fullreload"), QStringLiteral("1"));
2064                 // Check if replacement clip is long enough
2065                 if (currentItem->hasLimitedDuration() && currentItem->isIncludedInTimeline()) {
2066                     // Clip is used in timeline, make sure length is similar
2067                     std::unique_ptr<Mlt::Producer> replacementProd(new Mlt::Producer(pCore->getProjectProfile(), fileName.toUtf8().constData()));
2068                     int currentDuration = int(currentItem->frameDuration());
2069                     if (replacementProd->is_valid()) {
2070                         replacementProd->probe();
2071                         std::pair<bool, bool> replacementHasAV = {
2072                             (replacementProd->property_exists("audio_index") && replacementProd->get_int("audio_index") > -1),
2073                             (replacementProd->property_exists("video_index") && replacementProd->get_int("video_index") > -1)};
2074                         if (hasAV != replacementHasAV) {
2075                             replacementProd.reset();
2076                             QString message;
2077                             std::pair<bool, bool> replaceAV = {false, false};
2078                             if (hasAV.first && hasAV.second) {
2079                                 if (!replacementHasAV.second) {
2080                                     // Original clip has audio and video, replacement has only audio
2081                                     replaceAV.first = true;
2082                                     message = i18n("Replacement clip does not contain a video stream. Do you want to replace only the audio component of this "
2083                                                    "clip in the active timeline ?");
2084                                 } else if (!replacementHasAV.first) {
2085                                     // Original clip has audio and video, replacement has only video
2086                                     replaceAV.second = true;
2087                                     message = i18n("Replacement clip does not contain an audio stream. Do you want to replace only the video component of this "
2088                                                    "clip in the active timeline ?");
2089                                 }
2090                                 if (replaceAV.first == false && replaceAV.second == false) {
2091                                     KMessageBox::information(this, i18n("You cannot replace a clip with a different type of clip (audio/video not matching)."));
2092                                     return;
2093                                 }
2094                                 if (KMessageBox::questionTwoActions(QApplication::activeWindow(), message, {}, KGuiItem(i18n("Replace in timeline")),
2095                                                                     KStandardGuiItem::cancel()) == KMessageBox::SecondaryAction) {
2096                                     return;
2097                                 }
2098                                 // Replace only one component.
2099                                 QString folder = QStringLiteral("-1");
2100                                 auto containingFolder = std::static_pointer_cast<ProjectFolder>(currentItem->parent());
2101                                 if (containingFolder) {
2102                                     folder = containingFolder->clipId();
2103                                 }
2104                                 Fun undo = []() { return true; };
2105                                 Fun redo = []() { return true; };
2106                                 std::function<void(const QString &)> callBack = [replaceAudio = replaceAV.first,
2107                                                                                  sourceId = currentItem->clipId()](const QString &binId) {
2108                                     qDebug() << "::: READY TO REPLACE: " << sourceId << ", WITH: " << binId;
2109                                     if (!binId.isEmpty()) {
2110                                         pCore->projectManager()->replaceTimelineInstances(sourceId, binId, replaceAudio, !replaceAudio);
2111                                     }
2112                                     return true;
2113                                 };
2114                                 const QString clipId = ClipCreator::createClipFromFile(fileName, folder, m_itemModel, undo, redo, callBack);
2115                                 pCore->pushUndo(undo, redo, i18nc("@action", "Add clip"));
2116                             } else if ((hasAV.first && !replacementHasAV.first) || (hasAV.second && !replacementHasAV.second)) {
2117                                 KMessageBox::information(this, i18n("You cannot replace a clip with a different type of clip (audio/video not matching)."));
2118                             }
2119                             return;
2120                         }
2121                         int replacementDuration = replacementProd->get_length();
2122                         if (replacementDuration < currentDuration) {
2123                             if (KMessageBox::warningContinueCancel(
2124                                     this,
2125                                     i18n("You are replacing a clip with a shorter one, this might cause issues in timeline.\nReplacement is %1 frames shorter.",
2126                                          (currentDuration - replacementDuration))) != KMessageBox::Continue) {
2127                                 continue;
2128                             }
2129                         }
2130                     } else {
2131                         KMessageBox::error(this, i18n("The selected file %1 is invalid.", fileName));
2132                         continue;
2133                     }
2134                 }
2135                 slotEditClipCommand(currentItem->clipId(), sourceProps, newProps);
2136             }
2137         }
2138     }
2139 }
2140 
2141 void Bin::slotLocateClip()
2142 {
2143     const QModelIndexList indexes = m_proxyModel->selectionModel()->selectedIndexes();
2144     for (const QModelIndex &ix : indexes) {
2145         if (!ix.isValid() || ix.column() != 0) {
2146             continue;
2147         }
2148         std::shared_ptr<AbstractProjectItem> item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(ix));
2149         std::shared_ptr<ProjectClip> currentItem = nullptr;
2150         if (item->itemType() == AbstractProjectItem::ClipItem) {
2151             currentItem = std::static_pointer_cast<ProjectClip>(item);
2152         } else if (item->itemType() == AbstractProjectItem::SubClipItem) {
2153             currentItem = std::static_pointer_cast<ProjectSubClip>(item)->getMasterClip();
2154         }
2155         if (currentItem) {
2156             QUrl url = QUrl::fromLocalFile(currentItem->url()); //.adjusted(QUrl::RemoveFilename);
2157             bool exists = QFile(url.toLocalFile()).exists();
2158             if (currentItem->hasUrl() && exists) {
2159                 KIO::highlightInFileManager({url});
2160                 qCDebug(KDENLIVE_LOG) << "  / / " + url.toString();
2161             } else {
2162                 if (!exists) {
2163                     pCore->displayMessage(i18n("Could not locate %1", url.toString()), MessageType::ErrorMessage, 300);
2164                 }
2165                 return;
2166             }
2167         }
2168     }
2169 }
2170 
2171 void Bin::slotDuplicateClip()
2172 {
2173     const QModelIndexList indexes = m_proxyModel->selectionModel()->selectedIndexes();
2174     QList<std::shared_ptr<AbstractProjectItem>> items;
2175     for (const QModelIndex &ix : indexes) {
2176         if (!ix.isValid() || ix.column() != 0) {
2177             continue;
2178         }
2179         items << m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(ix));
2180     }
2181     std::function<void(const QString &)> callBack = [this](const QString &binId) {
2182         if (!binId.isEmpty()) {
2183             selectClipById(binId);
2184             std::shared_ptr<ProjectClip> clip = getBinClip(binId);
2185             if (clip && clip->clipType() == ClipType::Timeline) {
2186                 // For duplicated timeline clips, we need to build the timelinemodel otherwise the producer is not correctly saved
2187                 const QUuid uuid = clip->getSequenceUuid();
2188                 return pCore->projectManager()->openTimeline(binId, uuid, -1, true);
2189             }
2190         }
2191         return true;
2192     };
2193     int ix = 0;
2194     QString lastId;
2195     for (const auto &item : qAsConst(items)) {
2196         ix++;
2197         if (item->itemType() == AbstractProjectItem::ClipItem) {
2198             auto currentItem = std::static_pointer_cast<ProjectClip>(item);
2199             if (currentItem) {
2200                 if (currentItem->clipType() == ClipType::Timeline) {
2201                     const QUuid uuid = currentItem->getSequenceUuid();
2202                     if (m_doc->getTimelinesUuids().contains(uuid)) {
2203                         // Sync last changes for this timeline if it is opened
2204                         m_doc->storeGroups(uuid);
2205                         pCore->projectManager()->syncTimeline(uuid, true);
2206                     }
2207                     QTemporaryFile src(QDir::temp().absoluteFilePath(QString("XXXXXX.mlt")));
2208                     src.setAutoRemove(false);
2209                     if (!src.open()) {
2210                         pCore->displayMessage(i18n("Could not create temporary file in %1", QDir::temp().absolutePath()), MessageType::ErrorMessage, 500);
2211                         return;
2212                     }
2213                     // Save playlist to disk
2214                     currentItem->cloneProducerToFile(src.fileName());
2215                     // extract xml
2216                     QDomDocument xml = ClipCreator::getXmlFromUrl(src.fileName());
2217                     if (xml.isNull()) {
2218                         pCore->displayMessage(i18n("Duplicating sequence failed"), MessageType::ErrorMessage, 500);
2219                         return;
2220                     }
2221                     QDomDocument doc;
2222                     if (!Xml::docContentFromFile(doc, src.fileName(), false)) {
2223                         return;
2224                     }
2225                     const QByteArray result = doc.toString().toUtf8();
2226                     std::shared_ptr<Mlt::Producer> xmlProd(new Mlt::Producer(pCore->getProjectProfile(), "xml-string", result.constData()));
2227                     QString id;
2228                     Fun undo = []() { return true; };
2229                     Fun redo = []() { return true; };
2230                     xmlProd->set("kdenlive:clipname", i18n("%1 (copy)", currentItem->clipName()).toUtf8().constData());
2231                     xmlProd->set("kdenlive:sequenceproperties.documentuuid", m_doc->uuid().toString().toUtf8().constData());
2232                     m_itemModel->requestAddBinClip(id, xmlProd, item->parent()->clipId(), undo, redo, callBack);
2233                     pCore->pushUndo(undo, redo, i18n("Duplicate clip"));
2234                 } else {
2235                     QDomDocument doc;
2236                     QDomElement xml = currentItem->toXml(doc);
2237                     if (!xml.isNull()) {
2238                         QString currentName = Xml::getXmlProperty(xml, QStringLiteral("kdenlive:clipname"));
2239                         if (currentName.isEmpty()) {
2240                             QUrl url = QUrl::fromLocalFile(Xml::getXmlProperty(xml, QStringLiteral("resource")));
2241                             if (url.isValid()) {
2242                                 currentName = url.fileName();
2243                             }
2244                         }
2245                         if (!currentName.isEmpty()) {
2246                             currentName.append(i18nc("append to clip name to indicate a copied idem", " (copy)"));
2247                             Xml::setXmlProperty(xml, QStringLiteral("kdenlive:clipname"), currentName);
2248                         }
2249                         QString id;
2250                         if (ix == items.count()) {
2251                             m_itemModel->requestAddBinClip(id, xml, item->parent()->clipId(), i18n("Duplicate clip"), callBack);
2252                         } else {
2253                             m_itemModel->requestAddBinClip(id, xml, item->parent()->clipId(), i18n("Duplicate clip"));
2254                         }
2255                     }
2256                 }
2257             }
2258         } else if (item->itemType() == AbstractProjectItem::SubClipItem) {
2259             auto currentItem = std::static_pointer_cast<ProjectSubClip>(item);
2260             QString id;
2261             QPoint clipZone = currentItem->zone();
2262             m_itemModel->requestAddBinSubClip(id, clipZone.x(), clipZone.y(), {}, currentItem->getMasterClip()->clipId());
2263             lastId = id;
2264         }
2265     }
2266     if (!lastId.isEmpty()) {
2267         selectClipById(lastId);
2268     }
2269 }
2270 
2271 void Bin::setMonitor(Monitor *monitor)
2272 {
2273     m_monitor = monitor;
2274     connect(m_monitor, &Monitor::addClipToProject, this, &Bin::slotAddClipToProject);
2275     connect(m_monitor, &Monitor::refreshCurrentClip, this, &Bin::slotOpenCurrent);
2276     connect(m_itemModel.get(), &ProjectItemModel::resetPlayOrLoopZone, m_monitor, &Monitor::resetPlayOrLoopZone, Qt::DirectConnection);
2277     connect(this, &Bin::openClip, [&](std::shared_ptr<ProjectClip> clip, int in, int out) {
2278         m_monitor->slotOpenClip(clip, in, out);
2279         if (clip && clip->hasLimitedDuration()) {
2280             clip->refreshBounds();
2281         }
2282         pCore->textEditWidget()->openClip(clip);
2283     });
2284 }
2285 
2286 void Bin::cleanDocument()
2287 {
2288     blockSignals(true);
2289     if (m_proxyModel) {
2290         m_proxyModel->selectionModel()->blockSignals(true);
2291     }
2292     setEnabled(false);
2293     // Cleanup references in the clip properties dialog
2294     Q_EMIT requestShowClipProperties(nullptr);
2295 
2296     // Cleanup previous project
2297     m_itemModel->clean();
2298     if (m_propertiesPanel) {
2299         m_propertiesPanel->setProperty("clipId", QString());
2300         for (QWidget *w : m_propertiesPanel->findChildren<ClipPropertiesController *>()) {
2301             delete w;
2302         }
2303     }
2304     delete m_itemView;
2305     m_itemView = nullptr;
2306     isLoading = false;
2307     shouldCheckProfile = false;
2308     pCore->textEditWidget()->openClip(nullptr);
2309 }
2310 
2311 const QString Bin::setDocument(KdenliveDoc *project, const QString &id)
2312 {
2313     m_doc = project;
2314     QString folderName;
2315     if (m_isMainBin) {
2316         m_infoLabel->slotSetJobCount(0);
2317     }
2318     int iconHeight = int(QFontInfo(font()).pixelSize() * 3.5);
2319     m_baseIconSize = QSize(int(iconHeight * pCore->getCurrentDar()), iconHeight);
2320     setEnabled(true);
2321     blockSignals(false);
2322     if (m_proxyModel) {
2323         m_proxyModel->selectionModel()->blockSignals(false);
2324     }
2325     // reset filtering
2326     QSignalBlocker bk(m_filterButton);
2327     m_filterButton->setChecked(false);
2328     m_filterButton->setToolTip(i18n("Filter"));
2329     connect(m_proxyAction, &QAction::toggled, m_doc, [&](bool doProxy) { m_doc->slotProxyCurrentItem(doProxy); });
2330 
2331     // connect(m_itemModel, SIGNAL(dataChanged(QModelIndex,QModelIndex)), m_itemView
2332     // connect(m_itemModel, SIGNAL(updateCurrentItem()), this, SLOT(autoSelect()));
2333     slotInitView(nullptr);
2334     bool binEffectsDisabled = getDocumentProperty(QStringLiteral("disablebineffects")).toInt() == 1;
2335     // Set media browser url
2336     QString url = getDocumentProperty(QStringLiteral("browserurl"));
2337     const QString sorting = getDocumentProperty(QStringLiteral("binsort"));
2338     if (!sorting.isEmpty()) {
2339         int binSorting = sorting.toInt();
2340         updateSortingAction(binSorting);
2341     }
2342     if (!url.isEmpty()) {
2343         if (QFileInfo(url).isRelative()) {
2344             url.prepend(m_doc->documentRoot());
2345         }
2346         pCore->mediaBrowser()->setUrl(QUrl::fromLocalFile(url));
2347     }
2348     QAction *disableEffects = pCore->window()->actionCollection()->action(QStringLiteral("disable_bin_effects"));
2349     if (disableEffects) {
2350         if (binEffectsDisabled != disableEffects->isChecked()) {
2351             QSignalBlocker effectBk(disableEffects);
2352             disableEffects->setChecked(binEffectsDisabled);
2353         }
2354     }
2355     m_itemModel->setBinEffectsEnabled(!binEffectsDisabled);
2356     if (!id.isEmpty()) {
2357         // Open view in a specific folder
2358         std::shared_ptr<AbstractProjectItem> item = m_itemModel->getItemByBinId(id);
2359         auto parentIx = m_itemModel->getIndexFromItem(item);
2360         m_itemView->setRootIndex(m_proxyModel->mapFromSource(parentIx));
2361         folderName = item->name();
2362         m_upAction->setEnabled(true);
2363         m_upAction->setVisible(true);
2364     }
2365 
2366     // setBinEffectsEnabled(!binEffectsDisabled, false);
2367     QMap<int, QStringList> projectTags = m_doc->getProjectTags();
2368     m_tagsWidget->rebuildTags(projectTags);
2369     rebuildFilters(projectTags.size());
2370     return folderName;
2371 }
2372 
2373 void Bin::rebuildFilters(int tagsCount)
2374 {
2375     m_filterMenu->clear();
2376 
2377     // Add clear entry
2378     QAction *clearFilter = new QAction(QIcon::fromTheme(QStringLiteral("edit-clear")), i18n("Clear Filters"), this);
2379     m_filterMenu->addAction(clearFilter);
2380 
2381     // Add tag filters
2382     m_filterMenu->addSeparator();
2383 
2384     QAction *tagFilter = new QAction(i18n("No Tags"), &m_filterTagGroup);
2385     tagFilter->setData(QStringLiteral("#"));
2386     tagFilter->setCheckable(true);
2387     m_filterMenu->addAction(tagFilter);
2388 
2389     for (int i = 1; i <= tagsCount; i++) {
2390         QAction *tag = pCore->window()->actionCollection()->action(QString("tag_%1").arg(i));
2391         if (tag) {
2392             QAction *tagFilter = new QAction(tag->icon(), tag->text(), &m_filterTagGroup);
2393             tagFilter->setData(tag->data());
2394             tagFilter->setCheckable(true);
2395             m_filterMenu->addAction(tagFilter);
2396         }
2397     }
2398 
2399     // Add rating filters
2400     m_filterMenu->addSeparator();
2401 
2402     for (int i = 0; i < 6; ++i) {
2403         auto *rateFilter = new QAction(QIcon::fromTheme(QStringLiteral("favorite")), i18np("%1 Star", "%1 Stars", i), &m_filterRateGroup);
2404         rateFilter->setData(QString(".%1").arg(2 * i));
2405         rateFilter->setCheckable(true);
2406         m_filterMenu->addAction(rateFilter);
2407     }
2408     // Add usage filter
2409     m_filterMenu->addSeparator();
2410     auto *usageMenu = new QMenu(i18n("Filter by Usage"), m_filterMenu);
2411     m_filterMenu->addMenu(usageMenu);
2412 
2413     auto *usageFilter = new QAction(i18n("All Clips"), &m_filterUsageGroup);
2414     usageFilter->setData(ProjectSortProxyModel::All);
2415     usageFilter->setCheckable(true);
2416     usageFilter->setChecked(true);
2417     usageMenu->addAction(usageFilter);
2418     usageFilter = new QAction(i18n("Unused Clips"), &m_filterUsageGroup);
2419     usageFilter->setData(ProjectSortProxyModel::Unused);
2420     usageFilter->setCheckable(true);
2421     usageMenu->addAction(usageFilter);
2422     usageFilter = new QAction(i18n("Used Clips"), &m_filterUsageGroup);
2423     usageFilter->setData(ProjectSortProxyModel::Used);
2424     usageFilter->setCheckable(true);
2425     usageMenu->addAction(usageFilter);
2426 
2427     // Add type filters
2428     m_filterMenu->addSeparator();
2429     auto *typeMenu = new QMenu(i18n("Filter by Type"), m_filterMenu);
2430     m_filterMenu->addMenu(typeMenu);
2431     m_filterMenu->addSeparator();
2432     auto *typeFilter = new QAction(QIcon::fromTheme(QStringLiteral("video-x-generic")), i18n("AV Clip"), &m_filterTypeGroup);
2433     typeFilter->setData(ClipType::AV);
2434     typeFilter->setCheckable(true);
2435     typeMenu->addAction(typeFilter);
2436     typeFilter = new QAction(QIcon::fromTheme(QStringLiteral("video-x-matroska")), i18n("Mute Video"), &m_filterTypeGroup);
2437     typeFilter->setData(ClipType::Video);
2438     typeFilter->setCheckable(true);
2439     typeMenu->addAction(typeFilter);
2440     typeFilter = new QAction(QIcon::fromTheme(QStringLiteral("audio-x-generic")), i18n("Audio"), &m_filterTypeGroup);
2441     typeFilter->setData(ClipType::Audio);
2442     typeFilter->setCheckable(true);
2443     typeMenu->addAction(typeFilter);
2444     typeFilter = new QAction(QIcon::fromTheme(QStringLiteral("image-jpeg")), i18n("Image"), &m_filterTypeGroup);
2445     typeFilter->setData(ClipType::Image);
2446     typeFilter->setCheckable(true);
2447     typeMenu->addAction(typeFilter);
2448     typeFilter = new QAction(QIcon::fromTheme(QStringLiteral("kdenlive-add-slide-clip")), i18n("Slideshow"), &m_filterTypeGroup);
2449     typeFilter->setData(ClipType::SlideShow);
2450     typeFilter->setCheckable(true);
2451     typeMenu->addAction(typeFilter);
2452     typeFilter = new QAction(QIcon::fromTheme(QStringLiteral("video-mlt-playlist")), i18n("Playlist"), &m_filterTypeGroup);
2453     typeFilter->setData(ClipType::Playlist);
2454     typeFilter->setCheckable(true);
2455     typeMenu->addAction(typeFilter);
2456     typeFilter = new QAction(QIcon::fromTheme(QStringLiteral("video-mlt-playlist")), i18n("Sequences"), &m_filterTypeGroup);
2457     typeFilter->setData(ClipType::Timeline);
2458     typeFilter->setCheckable(true);
2459     typeMenu->addAction(typeFilter);
2460     typeFilter = new QAction(QIcon::fromTheme(QStringLiteral("draw-text")), i18n("Title"), &m_filterTypeGroup);
2461     typeFilter->setData(ClipType::Text);
2462     typeFilter->setCheckable(true);
2463     typeMenu->addAction(typeFilter);
2464     typeFilter = new QAction(QIcon::fromTheme(QStringLiteral("draw-text")), i18n("Title Template"), &m_filterTypeGroup);
2465     typeFilter->setData(ClipType::TextTemplate);
2466     typeFilter->setCheckable(true);
2467     typeMenu->addAction(typeFilter);
2468     typeFilter = new QAction(QIcon::fromTheme(QStringLiteral("kdenlive-add-color-clip")), i18n("Color"), &m_filterTypeGroup);
2469     typeFilter->setData(ClipType::Color);
2470     typeFilter->setCheckable(true);
2471     typeMenu->addAction(typeFilter);
2472 }
2473 
2474 void Bin::slotApplyFilters()
2475 {
2476     QList<QAction *> list = m_filterMenu->actions();
2477     QList<int> rateFilters;
2478     QList<int> typeFilters;
2479     ProjectSortProxyModel::UsageFilter usageFilter = ProjectSortProxyModel::UsageFilter(m_filterUsageGroup.checkedAction()->data().toInt());
2480     QStringList tagFilters;
2481     for (QAction *ac : qAsConst(list)) {
2482         if (ac->isChecked()) {
2483             QString actionData = ac->data().toString();
2484             if (actionData.startsWith(QLatin1Char('#'))) {
2485                 // Filter by tag
2486                 tagFilters << actionData;
2487             } else if (actionData.startsWith(QLatin1Char('.'))) {
2488                 // Filter by rating
2489                 rateFilters << actionData.remove(0, 1).toInt();
2490             }
2491         }
2492     }
2493     // Type actions
2494     list = m_filterTypeGroup.actions();
2495     for (QAction *ac : qAsConst(list)) {
2496         if (ac->isChecked()) {
2497             typeFilters << ac->data().toInt();
2498         }
2499     }
2500     QSignalBlocker bkt(m_filterButton);
2501     if (!rateFilters.isEmpty() || !tagFilters.isEmpty() || !typeFilters.isEmpty() || usageFilter != ProjectSortProxyModel::All) {
2502         m_filterButton->setChecked(true);
2503     } else {
2504         m_filterButton->setChecked(false);
2505     }
2506     m_proxyModel->slotSetFilters(tagFilters, rateFilters, typeFilters, usageFilter);
2507 }
2508 
2509 void Bin::createClip(const QDomElement &xml)
2510 {
2511     // Check if clip should be in a folder
2512     QString groupId = ProjectClip::getXmlProperty(xml, QStringLiteral("kdenlive:folderid"));
2513     std::shared_ptr<ProjectFolder> parentFolder = m_itemModel->getFolderByBinId(groupId);
2514     if (!parentFolder) {
2515         parentFolder = m_itemModel->getRootFolder();
2516     }
2517     QString path = Xml::getXmlProperty(xml, QStringLiteral("resource"));
2518     if (path.endsWith(QStringLiteral(".mlt")) || path.endsWith(QStringLiteral(".kdenlive"))) {
2519         QDomDocument doc;
2520         if (!Xml::docContentFromFile(doc, path, false)) {
2521             return;
2522         }
2523         DocumentChecker d(QUrl::fromLocalFile(path), doc);
2524         if (!d.hasErrorInProject() && doc.documentElement().hasAttribute(QStringLiteral("modified"))) {
2525             QString backupFile = path + QStringLiteral(".backup");
2526             KIO::FileCopyJob *copyjob = KIO::file_copy(QUrl::fromLocalFile(path), QUrl::fromLocalFile(backupFile));
2527             if (copyjob->exec()) {
2528                 QFile f(path);
2529                 if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) {
2530                     KMessageBox::error(this, i18n("Unable to write to file %1", path));
2531                 } else {
2532                     QTextStream out(&f);
2533 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
2534                     out.setCodec("UTF-8");
2535 #endif
2536                     out << doc.toString();
2537                     f.close();
2538                     KMessageBox::information(
2539                         this, i18n("Your project file was modified by Kdenlive.\nTo make sure you do not lose data, a backup copy called %1 was created.",
2540                                    backupFile));
2541                 }
2542             }
2543         }
2544     }
2545     QString id = Xml::getTagContentByAttribute(xml, QStringLiteral("property"), QStringLiteral("name"), QStringLiteral("kdenlive:id"));
2546     if (id.isEmpty()) {
2547         id = QString::number(m_itemModel->getFreeClipId());
2548     }
2549     auto newClip = ProjectClip::construct(id, xml, m_blankThumb, m_itemModel);
2550     parentFolder->appendChild(newClip);
2551 }
2552 
2553 void Bin::slotAddFolder()
2554 {
2555     auto parentFolder = m_itemModel->getFolderByBinId(getCurrentFolder());
2556     qDebug() << "parent folder id" << parentFolder->clipId();
2557     QString newId;
2558     Fun undo = []() { return true; };
2559     Fun redo = []() { return true; };
2560     m_itemModel->requestAddFolder(newId, i18n("Folder"), parentFolder->clipId(), undo, redo);
2561     pCore->pushUndo(undo, redo, i18n("Create bin folder"));
2562     if (m_listType == BinTreeView) {
2563         // Make sure parent folder is expanded
2564         if (parentFolder->clipId().toInt() > -1) {
2565             auto parentIx = m_itemModel->getIndexFromItem(parentFolder);
2566             auto *view = static_cast<QTreeView *>(m_itemView);
2567             view->expand(m_proxyModel->mapFromSource(parentIx));
2568         }
2569     }
2570 
2571     // Edit folder name
2572     auto folder = m_itemModel->getFolderByBinId(newId);
2573     auto ix = m_itemModel->getIndexFromItem(folder);
2574 
2575     // Scroll to ensure folder is visible
2576     m_itemView->scrollTo(m_proxyModel->mapFromSource(ix), QAbstractItemView::EnsureVisible);
2577     qDebug() << "selecting" << ix;
2578     if (ix.isValid()) {
2579         qDebug() << "ix valid";
2580         m_proxyModel->selectionModel()->clearSelection();
2581         int row = ix.row();
2582         const QModelIndex id = m_itemModel->index(row, 0, ix.parent());
2583         const QModelIndex id2 = m_itemModel->index(row, m_itemModel->columnCount() - 1, ix.parent());
2584         if (id.isValid() && id2.isValid()) {
2585             m_proxyModel->selectionModel()->select(QItemSelection(m_proxyModel->mapFromSource(id), m_proxyModel->mapFromSource(id2)),
2586                                                    QItemSelectionModel::Select);
2587         }
2588         m_itemView->edit(m_proxyModel->mapFromSource(ix));
2589     }
2590 }
2591 
2592 void Bin::ensureCurrent()
2593 {
2594     const QModelIndexList indexes = m_proxyModel->selectionModel()->selection().indexes();
2595     for (const QModelIndex &ix : indexes) {
2596         if (!ix.isValid() || ix.column() != 0) {
2597             continue;
2598         }
2599         m_itemView->setCurrentIndex(ix);
2600     }
2601 }
2602 
2603 QModelIndex Bin::getIndexForId(const QString &id, bool folderWanted) const
2604 {
2605     QModelIndexList items = m_itemModel->match(m_itemModel->index(0, 0), AbstractProjectItem::DataId, QVariant::fromValue(id), 1, Qt::MatchRecursive);
2606     for (int i = 0; i < items.count(); i++) {
2607         std::shared_ptr<AbstractProjectItem> currentItem = m_itemModel->getBinItemByIndex(items.at(i));
2608         AbstractProjectItem::PROJECTITEMTYPE type = currentItem->itemType();
2609         if (folderWanted && type == AbstractProjectItem::FolderItem) {
2610             // We found our folder
2611             return items.at(i);
2612         }
2613         if (!folderWanted && type == AbstractProjectItem::ClipItem) {
2614             // We found our clip
2615             return items.at(i);
2616         }
2617     }
2618     return {};
2619 }
2620 
2621 void Bin::selectAll()
2622 {
2623     QModelIndex rootIndex = m_itemView->rootIndex();
2624     QModelIndex currentSelection = m_proxyModel->selectionModel()->currentIndex();
2625     if (currentSelection.isValid()) {
2626         std::shared_ptr<AbstractProjectItem> currentItem = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(currentSelection));
2627         if (currentItem) {
2628             std::shared_ptr<ProjectFolder> parentFolder = std::static_pointer_cast<ProjectFolder>(currentItem->getEnclosingFolder());
2629             if (parentFolder) {
2630                 rootIndex = m_proxyModel->mapFromSource(getIndexForId(parentFolder->clipId(), true));
2631             }
2632         }
2633     }
2634     m_proxyModel->selectAll(rootIndex);
2635 }
2636 
2637 void Bin::selectClipById(const QString &clipId, int frame, const QPoint &zone, bool activateMonitor)
2638 {
2639     if (m_monitor->activeClipId() == clipId) {
2640         std::shared_ptr<ProjectClip> clip = m_itemModel->getClipByBinID(clipId);
2641         if (clip) {
2642             QModelIndex ix = m_itemModel->getIndexFromItem(clip);
2643             int row = ix.row();
2644             const QModelIndex id = m_itemModel->index(row, 0, ix.parent());
2645             const QModelIndex id2 = m_itemModel->index(row, m_itemModel->columnCount() - 1, ix.parent());
2646             if (id.isValid() && id2.isValid()) {
2647                 m_proxyModel->selectionModel()->select(QItemSelection(m_proxyModel->mapFromSource(id), m_proxyModel->mapFromSource(id2)),
2648                                                        QItemSelectionModel::SelectCurrent);
2649             }
2650             m_itemView->scrollTo(m_proxyModel->mapFromSource(ix), QAbstractItemView::EnsureVisible);
2651         }
2652     } else {
2653         std::shared_ptr<ProjectClip> clip = getBinClip(clipId);
2654         if (clip == nullptr) {
2655             return;
2656         }
2657         selectClip(clip);
2658     }
2659     if (!zone.isNull()) {
2660         m_monitor->slotLoadClipZone(zone);
2661     }
2662     if (activateMonitor) {
2663         if (frame > -1) {
2664             m_monitor->slotSeek(frame);
2665             m_monitor->refreshMonitor();
2666         } else {
2667             m_monitor->slotActivateMonitor();
2668         }
2669     }
2670 }
2671 
2672 void Bin::selectProxyModel(const QModelIndex &id)
2673 {
2674     if (isLoading) {
2675         // return;
2676     }
2677     if (id.isValid()) {
2678         if (id.column() != 0 && m_monitor->activeClipId() == QString::number(int(id.internalId()))) {
2679             return;
2680         }
2681         QString clipService;
2682         std::shared_ptr<AbstractProjectItem> currentItem = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(id));
2683         if (currentItem) {
2684             // Set item as current so that it displays its content in clip monitor
2685             setCurrent(currentItem);
2686             AbstractProjectItem::PROJECTITEMTYPE itemType = currentItem->itemType();
2687             bool isClip = itemType == AbstractProjectItem::ClipItem;
2688             bool isFolder = itemType == AbstractProjectItem::FolderItem;
2689             std::shared_ptr<ProjectClip> clip = nullptr;
2690             if (isClip) {
2691                 clip = std::static_pointer_cast<ProjectClip>(currentItem);
2692                 m_tagsWidget->setTagData(clip->tags());
2693                 m_deleteAction->setText(i18n("Delete Clip"));
2694                 m_deleteAction->setWhatsThis(
2695                     xi18nc("@info:whatsthis", "Deletes the currently selected clips from the project bin and also from the timeline."));
2696                 m_proxyAction->setText(i18n("Proxy Clip"));
2697             } else if (itemType == AbstractProjectItem::FolderItem) {
2698                 // A folder was selected, disable editing clip
2699                 m_tagsWidget->setTagData();
2700                 m_deleteAction->setText(i18n("Delete Folder"));
2701                 m_proxyAction->setText(i18n("Proxy Folder"));
2702             } else if (itemType == AbstractProjectItem::SubClipItem) {
2703                 m_tagsWidget->setTagData(currentItem->tags());
2704                 auto subClip = std::static_pointer_cast<ProjectSubClip>(currentItem);
2705                 clip = subClip->getMasterClip();
2706                 m_deleteAction->setText(i18n("Delete Clip"));
2707                 m_deleteAction->setWhatsThis(
2708                     xi18nc("@info:whatsthis", "Deletes the currently selected clips from the project bin and also from the timeline."));
2709                 m_proxyAction->setText(i18n("Proxy Clip"));
2710             }
2711             bool isImported = false;
2712             bool hasAudio = false;
2713             ClipType::ProducerType type = ClipType::Unknown;
2714             // don't need to wait for the clip to be ready to get its type
2715             if (clip) {
2716                 type = clip->clipType();
2717             }
2718             if (clip && clip->statusReady()) {
2719                 Q_EMIT requestShowClipProperties(clip, false);
2720                 m_proxyAction->blockSignals(true);
2721                 clipService = clip->getProducerProperty(QStringLiteral("mlt_service"));
2722                 hasAudio = clip->hasAudio();
2723                 m_proxyAction->setChecked(clip->hasProxy());
2724                 m_proxyAction->blockSignals(false);
2725                 if (clip->hasUrl()) {
2726                     isImported = true;
2727                 }
2728             } else if (clip && clip->clipStatus() == FileStatus::StatusMissing) {
2729                 m_openAction->setEnabled(false);
2730             } else {
2731                 // Disable find in timeline option
2732                 m_openAction->setEnabled(false);
2733             }
2734             if (m_clipsActionsMenu) {
2735                 m_clipsActionsMenu->setEnabled(!isFolder);
2736             }
2737             m_editAction->setVisible(!isFolder);
2738             m_editAction->setEnabled(true);
2739             m_extractAudioAction->menuAction()->setVisible(hasAudio);
2740             m_extractAudioAction->setEnabled(hasAudio);
2741             m_openAction->setEnabled(type == ClipType::Image || type == ClipType::Audio || type == ClipType::TextTemplate || type == ClipType::Text ||
2742                                      type == ClipType::Animation);
2743             m_openAction->setVisible(!isFolder);
2744             m_duplicateAction->setEnabled(isClip);
2745             m_duplicateAction->setVisible(!isFolder);
2746             if (m_inTimelineAction) {
2747                 m_inTimelineAction->setEnabled(isClip);
2748                 m_inTimelineAction->setVisible(isClip);
2749             }
2750             m_locateAction->setEnabled(!isFolder && isImported);
2751             m_locateAction->setVisible(!isFolder && isImported);
2752             m_proxyAction->setEnabled(m_doc->useProxy() && !isFolder);
2753             m_reloadAction->setEnabled(isClip && type != ClipType::Timeline);
2754             m_reloadAction->setVisible(!isFolder);
2755             m_replaceAction->setEnabled(isClip);
2756             m_replaceAction->setVisible(!isFolder);
2757             m_replaceInTimelineAction->setEnabled(isClip);
2758             m_replaceInTimelineAction->setVisible(!isFolder);
2759             // Enable actions depending on clip type
2760             for (auto &a : m_clipsActionsMenu->actions()) {
2761                 qDebug() << "ACTION: " << a->text() << " = " << a->data().toString();
2762                 QString actionType = a->data().toString().section(QLatin1Char(';'), 1);
2763                 qDebug() << ":::: COMPARING ACTIONTYPE: " << actionType << " = " << type;
2764                 if (actionType.isEmpty()) {
2765                     a->setEnabled(true);
2766                 } else if (actionType.contains(QLatin1Char('v')) && (type == ClipType::AV || type == ClipType::Video)) {
2767                     a->setEnabled(true);
2768                 } else if (actionType.contains(QLatin1Char('a')) && (type == ClipType::AV || type == ClipType::Audio)) {
2769                     a->setEnabled(true);
2770                 } else if (actionType.contains(QLatin1Char('i')) && type == ClipType::Image) {
2771                     a->setEnabled(true);
2772                 } else {
2773                     a->setEnabled(false);
2774                 }
2775             }
2776             m_clipsActionsMenu->menuAction()->setVisible(!isFolder && (type == ClipType::AV || type == ClipType::Timeline || type == ClipType::Playlist ||
2777                                                                        type == ClipType::Image || type == ClipType::Video || type == ClipType::Audio));
2778 
2779             m_transcodeAction->setEnabled(!isFolder);
2780             m_transcodeAction->setVisible(!isFolder && (type == ClipType::Playlist || type == ClipType::Timeline || type == ClipType::Text ||
2781                                                         clipService.contains(QStringLiteral("avformat"))));
2782 
2783             m_deleteAction->setEnabled(true);
2784             m_renameAction->setEnabled(true);
2785             updateClipsCount();
2786             return;
2787         }
2788     } else {
2789         // No item selected in bin
2790         Q_EMIT requestShowClipProperties(nullptr);
2791         Q_EMIT requestClipShow(nullptr);
2792         // clear effect stack
2793         Q_EMIT requestShowEffectStack(QString(), nullptr, QSize(), false);
2794         // Display black bg in clip monitor
2795         Q_EMIT openClip(std::shared_ptr<ProjectClip>());
2796     }
2797     m_editAction->setEnabled(false);
2798     m_clipsActionsMenu->setEnabled(false);
2799     m_extractAudioAction->setEnabled(false);
2800     m_transcodeAction->setEnabled(false);
2801     m_proxyAction->setEnabled(false);
2802     m_reloadAction->setEnabled(false);
2803     m_replaceAction->setEnabled(false);
2804     m_replaceInTimelineAction->setEnabled(false);
2805     m_locateAction->setEnabled(false);
2806     m_duplicateAction->setEnabled(false);
2807     m_openAction->setEnabled(false);
2808     m_deleteAction->setEnabled(false);
2809     m_renameAction->setEnabled(false);
2810     updateClipsCount();
2811 }
2812 
2813 std::vector<QString> Bin::selectedClipsIds(bool allowSubClips)
2814 {
2815     const QModelIndexList indexes = m_proxyModel->selectionModel()->selectedIndexes();
2816     std::vector<QString> ids;
2817     for (const QModelIndex &ix : indexes) {
2818         if (!ix.isValid() || ix.column() != 0) {
2819             continue;
2820         }
2821         std::shared_ptr<AbstractProjectItem> item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(ix));
2822         if (item->itemType() == AbstractProjectItem::SubClipItem) {
2823             if (allowSubClips) {
2824                 auto subClipItem = std::static_pointer_cast<ProjectSubClip>(item);
2825                 ids.push_back(subClipItem->cutClipId());
2826             } else {
2827                 ids.push_back(item->clipId());
2828             }
2829         } else if (item->itemType() == AbstractProjectItem::ClipItem) {
2830             ids.push_back(item->clipId());
2831         }
2832     }
2833     return ids;
2834 }
2835 
2836 QList<std::shared_ptr<ProjectClip>> Bin::selectedClips()
2837 {
2838     auto ids = selectedClipsIds();
2839     QList<std::shared_ptr<ProjectClip>> ret;
2840     for (const auto &id : ids) {
2841         ret.push_back(m_itemModel->getClipByBinID(id));
2842     }
2843     return ret;
2844 }
2845 
2846 void Bin::slotInitView(QAction *action)
2847 {
2848     QString rootFolder;
2849     QString selectedItem;
2850     QStringList selectedItems;
2851     if (action) {
2852         QModelIndex currentSelection;
2853         if (m_itemView && m_proxyModel) {
2854             // Check currently selected item
2855             QModelIndexList indexes = m_proxyModel->selectionModel()->selectedIndexes();
2856             for (auto &ix : indexes) {
2857                 if (!ix.isValid()) {
2858                     continue;
2859                 }
2860                 std::shared_ptr<AbstractProjectItem> currentItem = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(ix));
2861                 if (currentItem) {
2862                     selectedItems << currentItem->clipId();
2863                 }
2864             }
2865             currentSelection = m_proxyModel->selectionModel()->currentIndex();
2866             if (currentSelection.isValid()) {
2867                 std::shared_ptr<AbstractProjectItem> currentItem = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(currentSelection));
2868                 if (currentItem) {
2869                     selectedItem = currentItem->clipId();
2870                     std::shared_ptr<ProjectFolder> parentFolder = std::static_pointer_cast<ProjectFolder>(currentItem->getEnclosingFolder());
2871                     if (parentFolder) {
2872                         rootFolder = parentFolder->clipId();
2873                     }
2874                 }
2875             }
2876         }
2877         if (m_proxyModel) {
2878             m_proxyModel->selectionModel()->clearSelection();
2879         }
2880         int viewType = action->data().toInt();
2881         if (m_isMainBin) {
2882             KdenliveSettings::setBinMode(viewType);
2883         }
2884         if (viewType == m_listType) {
2885             return;
2886         }
2887         if (m_listType == BinTreeView) {
2888             // save current treeview state (column width)
2889             auto *view = static_cast<QTreeView *>(m_itemView);
2890             m_headerInfo = view->header()->saveState();
2891             m_showDate->setEnabled(true);
2892             m_showDesc->setEnabled(true);
2893             m_showRating->setEnabled(true);
2894             m_upAction->setEnabled(false);
2895         }
2896         m_listType = static_cast<BinViewType>(viewType);
2897     }
2898     if (m_itemView) {
2899         delete m_itemView;
2900     }
2901     delete m_binTreeViewDelegate;
2902     delete m_binListViewDelegate;
2903     m_binTreeViewDelegate = nullptr;
2904     m_binListViewDelegate = nullptr;
2905 
2906     switch (m_listType) {
2907     case BinIconView:
2908         m_itemView = new MyListView(this);
2909         m_binListViewDelegate = new BinListItemDelegate(this);
2910         m_showDate->setEnabled(false);
2911         m_showDesc->setEnabled(false);
2912         m_showRating->setEnabled(false);
2913         m_upAction->setVisible(true);
2914         break;
2915     default:
2916         m_itemView = new MyTreeView(this);
2917         m_binTreeViewDelegate = new BinItemDelegate(this);
2918         m_showDate->setEnabled(true);
2919         m_showDesc->setEnabled(true);
2920         m_showRating->setEnabled(true);
2921         m_upAction->setVisible(false);
2922         break;
2923     }
2924     m_itemView->setMouseTracking(true);
2925     m_itemView->viewport()->installEventFilter(this);
2926     m_iconSize = m_baseIconSize * ((m_listType == BinIconView ? qMax(1, m_slider->value()) : m_slider->value()) / 4.0);
2927     m_itemView->setIconSize(m_iconSize);
2928     QPixmap pix(m_iconSize);
2929     pix.fill(Qt::lightGray);
2930     m_blankThumb.addPixmap(pix);
2931     m_itemView->addAction(m_renameAction);
2932     m_renameAction->setShortcutContext(Qt::WidgetWithChildrenShortcut);
2933     m_proxyModel = std::make_unique<ProjectSortProxyModel>(this);
2934     // Connect models
2935     m_proxyModel->setSourceModel(m_itemModel.get());
2936     connect(m_itemModel.get(), &QAbstractItemModel::dataChanged, m_proxyModel.get(), &ProjectSortProxyModel::slotDataChanged);
2937     connect(m_proxyModel.get(), &ProjectSortProxyModel::updateRating, this, [&](const QModelIndex &ix, uint rating) {
2938         const QModelIndex index = m_proxyModel->mapToSource(ix);
2939         std::shared_ptr<AbstractProjectItem> item = m_itemModel->getBinItemByIndex(index);
2940         if (item) {
2941             uint previousRating = item->rating();
2942             Fun undo = [this, item, index, previousRating]() {
2943                 item->setRating(previousRating);
2944                 Q_EMIT m_itemModel->dataChanged(index, index, {AbstractProjectItem::DataRating});
2945                 return true;
2946             };
2947             Fun redo = [this, item, index, rating]() {
2948                 item->setRating(rating);
2949                 Q_EMIT m_itemModel->dataChanged(index, index, {AbstractProjectItem::DataRating});
2950                 return true;
2951             };
2952             redo();
2953             pCore->pushUndo(undo, redo, i18n("Edit rating"));
2954         } else {
2955             Q_EMIT displayBinMessage(i18n("Cannot set rating on this item"), KMessageWidget::Information);
2956         }
2957     });
2958     connect(m_proxyModel.get(), &ProjectSortProxyModel::selectModel, this, &Bin::selectProxyModel);
2959     connect(m_proxyModel.get(), &QAbstractItemModel::layoutAboutToBeChanged, this, &Bin::slotSetSorting);
2960     m_itemView->setModel(m_proxyModel.get());
2961     m_itemView->setSelectionModel(m_proxyModel->selectionModel());
2962     m_proxyModel->setDynamicSortFilter(true);
2963     m_layout->insertWidget(2, m_itemView);
2964     // Reset drag type to normal
2965     m_itemModel->setDragType(PlaylistState::Disabled);
2966 
2967     // setup some default view specific parameters
2968     if (m_listType == BinTreeView) {
2969         m_itemView->setItemDelegate(m_binTreeViewDelegate);
2970         auto *view = static_cast<MyTreeView *>(m_itemView);
2971         view->setSortingEnabled(true);
2972         view->setWordWrap(true);
2973         connect(view, &MyTreeView::updateDragMode, m_itemModel.get(), &ProjectItemModel::setDragType, Qt::DirectConnection);
2974         connect(view, &MyTreeView::processDragEnd, this, &Bin::processDragEnd);
2975         connect(view, &MyTreeView::selectCurrent, this, &Bin::ensureCurrent);
2976         connect(view, &MyTreeView::displayBinFrame, this, &Bin::showBinFrame);
2977         if (!m_headerInfo.isEmpty()) {
2978             view->header()->restoreState(m_headerInfo);
2979         } else {
2980             view->header()->resizeSections(QHeaderView::ResizeToContents);
2981             view->resizeColumnToContents(0);
2982             // Date Column
2983             view->setColumnHidden(1, true);
2984             // Description Column
2985             view->setColumnHidden(2, true);
2986             // Rating column
2987             view->setColumnHidden(7, true);
2988         }
2989         // Type column
2990         view->setColumnHidden(3, true);
2991         // Tags column
2992         view->setColumnHidden(4, true);
2993         // Duration column
2994         view->setColumnHidden(5, true);
2995         // ID column
2996         view->setColumnHidden(6, true);
2997         // Rating column
2998         view->header()->resizeSection(7, QFontInfo(font()).pixelSize() * 4);
2999         // Usage column
3000         view->setColumnHidden(8, true);
3001         m_showDate->setChecked(!view->isColumnHidden(1));
3002         m_showDesc->setChecked(!view->isColumnHidden(2));
3003         m_showRating->setChecked(!view->isColumnHidden(7));
3004         if (m_sortGroup->checkedAction()) {
3005             view->header()->setSortIndicator(m_sortGroup->checkedAction()->data().toInt(),
3006                                              m_sortDescend->isChecked() ? Qt::DescendingOrder : Qt::AscendingOrder);
3007         }
3008         connect(view->header(), &QHeaderView::sectionResized, this, &Bin::slotSaveHeaders);
3009         connect(view->header(), &QHeaderView::sectionClicked, this, &Bin::slotSaveHeaders);
3010         connect(view->header(), &QHeaderView::sortIndicatorChanged, this, [this](int ix, Qt::SortOrder order) {
3011             QSignalBlocker bk(m_sortDescend);
3012             QSignalBlocker bk2(m_sortGroup);
3013             m_sortDescend->setChecked(order == Qt::DescendingOrder);
3014             QList<QAction *> actions = m_sortGroup->actions();
3015             for (auto ac : qAsConst(actions)) {
3016                 if (ac->data().toInt() == ix) {
3017                     ac->setChecked(true);
3018                     break;
3019                 }
3020             }
3021             KdenliveSettings::setBinSorting(ix + (order == Qt::DescendingOrder ? 100 : 0));
3022         });
3023         connect(view, &MyTreeView::focusView, this, &Bin::slotGotFocus);
3024     } else if (m_listType == BinIconView) {
3025         m_itemView->setItemDelegate(m_binListViewDelegate);
3026         auto *view = static_cast<MyListView *>(m_itemView);
3027         connect(view, &MyListView::updateDragMode, m_itemModel.get(), &ProjectItemModel::setDragType, Qt::DirectConnection);
3028         int textHeight = int(QFontInfo(font()).pixelSize() * 1.5);
3029         view->setGridSize(QSize(m_iconSize.width() + 2, m_iconSize.height() + textHeight));
3030         connect(view, &MyListView::focusView, this, &Bin::slotGotFocus);
3031         connect(view, &MyListView::displayBinFrame, this, &Bin::showBinFrame);
3032         connect(view, &MyListView::processDragEnd, this, &Bin::processDragEnd);
3033         if (!rootFolder.isEmpty()) {
3034             // Open view in a specific folder
3035             std::shared_ptr<AbstractProjectItem> folder = m_itemModel->getItemByBinId(rootFolder);
3036             auto parentIx = m_itemModel->getIndexFromItem(folder);
3037             m_itemView->setRootIndex(m_proxyModel->mapFromSource(parentIx));
3038             m_upAction->setEnabled(rootFolder != QLatin1String("-1"));
3039         }
3040     }
3041     if (!selectedItems.isEmpty()) {
3042         for (auto &item : selectedItems) {
3043             std::shared_ptr<AbstractProjectItem> clip = m_itemModel->getItemByBinId(item);
3044             QModelIndex ix = m_itemModel->getIndexFromItem(clip);
3045             int row = ix.row();
3046             const QModelIndex id = m_itemModel->index(row, 0, ix.parent());
3047             const QModelIndex id2 = m_itemModel->index(row, m_itemModel->columnCount() - 1, ix.parent());
3048             if (id.isValid() && id2.isValid()) {
3049                 m_proxyModel->selectionModel()->select(QItemSelection(m_proxyModel->mapFromSource(id), m_proxyModel->mapFromSource(id2)),
3050                                                        QItemSelectionModel::Select);
3051             }
3052         }
3053     }
3054     m_itemView->setEditTriggers(QAbstractItemView::NoEditTriggers); // DoubleClicked);
3055     m_itemView->setSelectionMode(QAbstractItemView::ExtendedSelection);
3056     m_itemView->setDragDropMode(QAbstractItemView::DragDrop);
3057     m_itemView->setAlternatingRowColors(true);
3058     m_itemView->setFocus();
3059 }
3060 
3061 void Bin::slotSetIconSize(int size)
3062 {
3063     if (!m_itemView) {
3064         return;
3065     }
3066     KdenliveSettings::setBin_zoom(size);
3067     m_iconSize = m_baseIconSize * ((m_listType == BinIconView ? qMax(1, size) : size) / 4.0);
3068     m_itemView->setIconSize(m_iconSize);
3069     if (m_listType == BinIconView) {
3070         auto *view = static_cast<MyListView *>(m_itemView);
3071         int textHeight = int(QFontInfo(font()).pixelSize() * 1.5);
3072         view->setGridSize(QSize(m_iconSize.width() + 2, m_iconSize.height() + textHeight));
3073     }
3074     QPixmap pix(m_iconSize);
3075     pix.fill(Qt::lightGray);
3076     m_blankThumb.addPixmap(pix);
3077 }
3078 
3079 void Bin::contextMenuEvent(QContextMenuEvent *event)
3080 {
3081     bool enableClipActions = false;
3082     bool isFolder = false;
3083     QString clipService;
3084     bool clickInView = false;
3085     if (m_itemView) {
3086         QRect viewRect(m_itemView->mapToGlobal(QPoint(0, 0)), m_itemView->mapToGlobal(QPoint(m_itemView->width(), m_itemView->height())));
3087         if (viewRect.contains(event->globalPos())) {
3088             clickInView = true;
3089             QModelIndex idx = m_itemView->indexAt(m_itemView->viewport()->mapFromGlobal(event->globalPos()));
3090             if (idx.isValid()) {
3091                 // User right clicked on a clip
3092                 std::shared_ptr<AbstractProjectItem> currentItem = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(idx));
3093                 if (currentItem) {
3094                     enableClipActions = true;
3095                     if (currentItem->itemType() == AbstractProjectItem::FolderItem) {
3096                         isFolder = true;
3097                     }
3098                 }
3099             }
3100         }
3101     }
3102     if (!clickInView) {
3103         return;
3104     }
3105 
3106     // New folder can be created from level of another folder.
3107     if (isFolder) {
3108         m_menu->insertAction(m_deleteAction, m_createFolderAction);
3109         m_menu->insertAction(m_createFolderAction, m_sequencesFolderAction);
3110         m_menu->insertAction(m_createFolderAction, m_audioCapturesFolderAction);
3111         m_menu->insertAction(m_sequencesFolderAction, m_openInBin);
3112     } else {
3113         m_menu->removeAction(m_createFolderAction);
3114         m_menu->removeAction(m_openInBin);
3115         m_menu->removeAction(m_sequencesFolderAction);
3116         m_menu->removeAction(m_audioCapturesFolderAction);
3117     }
3118 
3119     // Show menu
3120     event->setAccepted(true);
3121     if (enableClipActions) {
3122         m_menu->exec(event->globalPos());
3123     } else {
3124         // Clicked in empty area, will show main menu.
3125         // Before that `createFolderAction` is inserted - it allows to distinguish between showing that item by clicking on empty area and by clicking on "Add
3126         // Clip" menu.
3127         m_addButton->menu()->insertAction(m_addClip, m_createFolderAction);
3128 
3129         m_addButton->menu()->exec(event->globalPos());
3130 
3131         // Clear up this action to not show it on "Add Clip" menu from toolbar icon.
3132         m_addButton->menu()->removeAction(m_createFolderAction);
3133     }
3134 }
3135 
3136 void Bin::slotItemDoubleClicked(const QModelIndex &ix, const QPoint &pos, uint modifiers)
3137 {
3138     std::shared_ptr<AbstractProjectItem> item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(ix));
3139     if (m_listType == BinIconView) {
3140         if (item->childCount() > 0 || item->itemType() == AbstractProjectItem::FolderItem) {
3141             m_itemView->setRootIndex(ix);
3142             parentWidget()->setWindowTitle(item->name());
3143             m_upAction->setEnabled(true);
3144             return;
3145         }
3146     } else {
3147         if (!m_isMainBin && item->itemType() == AbstractProjectItem::FolderItem) {
3148             // Double click a folder in secondary bin will set it as bin root
3149             m_itemView->setRootIndex(ix);
3150             parentWidget()->setWindowTitle(item->name());
3151             m_upAction->setEnabled(true);
3152             return;
3153         }
3154         if (ix.column() == 0 && item->childCount() > 0) {
3155             QRect IconRect = m_itemView->visualRect(ix);
3156             IconRect.setWidth(int(double(IconRect.height()) / m_itemView->iconSize().height() * m_itemView->iconSize().width()));
3157             if (!pos.isNull() && (IconRect.contains(pos) || pos.y() > (IconRect.y() + IconRect.height() / 2))) {
3158                 auto *view = static_cast<QTreeView *>(m_itemView);
3159                 bool expand = !view->isExpanded(ix);
3160                 // Expand all items on shift + double click
3161                 if (modifiers & Qt::ShiftModifier) {
3162                     if (expand) {
3163                         view->expandAll();
3164                     } else {
3165                         view->collapseAll();
3166                     }
3167                 } else {
3168                     view->setExpanded(ix, expand);
3169                 }
3170                 return;
3171             }
3172         }
3173     }
3174     if (ix.isValid()) {
3175         QRect IconRect = m_itemView->visualRect(ix);
3176         IconRect.setWidth(int(double(IconRect.height()) / m_itemView->iconSize().height() * m_itemView->iconSize().width()));
3177         if (!pos.isNull() && ((ix.column() == 2 && item->itemType() == AbstractProjectItem::ClipItem) ||
3178                               (!IconRect.contains(pos) && pos.y() < (IconRect.y() + IconRect.height() / 2)))) {
3179             // User clicked outside icon, trigger rename
3180             m_itemView->edit(ix);
3181             return;
3182         }
3183         if (item->itemType() == AbstractProjectItem::ClipItem) {
3184             std::shared_ptr<ProjectClip> clip = std::static_pointer_cast<ProjectClip>(item);
3185             if (clip) {
3186                 if (clip->clipType() == ClipType::Timeline) {
3187                     const QUuid uuid = clip->getSequenceUuid();
3188                     pCore->projectManager()->openTimeline(clip->binId(), uuid);
3189                 } else if (clip->clipType() == ClipType::Text || clip->clipType() == ClipType::TextTemplate) {
3190                     // m_propertiesPanel->setEnabled(false);
3191                     showTitleWidget(clip);
3192                 } else {
3193                     slotSwitchClipProperties(clip);
3194                 }
3195             }
3196         }
3197     }
3198 }
3199 
3200 void Bin::slotEditClip()
3201 {
3202     if (m_propertiesPanel == nullptr) {
3203         return;
3204     }
3205     QString panelId = m_propertiesPanel->property("clipId").toString();
3206     QModelIndex current = m_proxyModel->selectionModel()->currentIndex();
3207     std::shared_ptr<AbstractProjectItem> item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(current));
3208     if (item->clipId() != panelId) {
3209         // wrong clip
3210         return;
3211     }
3212     auto clip = std::static_pointer_cast<ProjectClip>(item);
3213     QString parentFolder = getCurrentFolder();
3214     switch (clip->clipType()) {
3215     case ClipType::Text:
3216     case ClipType::TextTemplate:
3217         showTitleWidget(clip);
3218         break;
3219     case ClipType::SlideShow:
3220         showSlideshowWidget(clip);
3221         break;
3222     case ClipType::QText:
3223         ClipCreationDialog::createQTextClip(parentFolder, this, clip.get());
3224         break;
3225     default:
3226         break;
3227     }
3228 }
3229 
3230 void Bin::slotSwitchClipProperties()
3231 {
3232     QModelIndex current = m_proxyModel->selectionModel()->currentIndex();
3233     if (current.isValid()) {
3234         // User clicked in the icon, open clip properties
3235         std::shared_ptr<AbstractProjectItem> item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(current));
3236         std::shared_ptr<ProjectClip> currentItem = nullptr;
3237         if (item->itemType() == AbstractProjectItem::ClipItem) {
3238             currentItem = std::static_pointer_cast<ProjectClip>(item);
3239         } else if (item->itemType() == AbstractProjectItem::SubClipItem) {
3240             currentItem = std::static_pointer_cast<ProjectSubClip>(item)->getMasterClip();
3241         }
3242         if (currentItem) {
3243             slotSwitchClipProperties(currentItem);
3244             return;
3245         }
3246     }
3247     slotSwitchClipProperties(nullptr);
3248 }
3249 
3250 void Bin::slotSwitchClipProperties(const std::shared_ptr<ProjectClip> &clip)
3251 {
3252     if (m_propertiesPanel == nullptr) {
3253         return;
3254     }
3255     if (clip == nullptr) {
3256         m_propertiesPanel->setEnabled(false);
3257         return;
3258     }
3259     if (clip->clipType() == ClipType::SlideShow) {
3260         m_propertiesPanel->setEnabled(false);
3261         showSlideshowWidget(clip);
3262     } else if (clip->clipType() == ClipType::QText) {
3263         m_propertiesPanel->setEnabled(false);
3264         QString parentFolder = getCurrentFolder();
3265         ClipCreationDialog::createQTextClip(parentFolder, this, clip.get());
3266     } else {
3267         m_propertiesPanel->setEnabled(true);
3268         Q_EMIT requestShowClipProperties(clip);
3269         m_propertiesDock->show();
3270         m_propertiesDock->raise();
3271     }
3272     // Check if properties panel is not tabbed under Bin
3273     // if (!pCore->window()->isTabbedWith(m_propertiesDock, QStringLiteral("project_bin"))) {
3274 }
3275 
3276 void Bin::doRefreshPanel(const QString &id)
3277 {
3278     std::shared_ptr<ProjectClip> currentItem = getFirstSelectedClip();
3279     if ((currentItem != nullptr) && currentItem->AbstractProjectItem::clipId() == id) {
3280         Q_EMIT requestShowClipProperties(currentItem, true);
3281     }
3282 }
3283 
3284 QAction *Bin::addAction(const QString &name, const QString &text, const QIcon &icon, const QString &category)
3285 {
3286     auto *action = new QAction(text, this);
3287     if (!icon.isNull()) {
3288         action->setIcon(icon);
3289     }
3290     pCore->window()->addAction(name, action, {}, category);
3291     return action;
3292 }
3293 
3294 void Bin::setupAddClipAction(QMenu *addClipMenu, ClipType::ProducerType type, const QString &name, const QString &text, const QIcon &icon)
3295 {
3296     QAction *action = addAction(name, text, icon, QStringLiteral("addclip"));
3297     action->setData(static_cast<QVariant>(type));
3298     addClipMenu->addAction(action);
3299     connect(action, &QAction::triggered, this, &Bin::slotCreateProjectClip);
3300     if (name == QLatin1String("add_animation_clip") && !KdenliveSettings::producerslist().contains(QLatin1String("glaxnimate"))) {
3301         action->setEnabled(false);
3302     } else if (name == QLatin1String("add_text_clip") && !KdenliveSettings::producerslist().contains(QLatin1String("kdenlivetitle"))) {
3303         action->setEnabled(false);
3304     }
3305 }
3306 
3307 void Bin::showClipProperties(const std::shared_ptr<ProjectClip> &clip, bool forceRefresh)
3308 {
3309     if (m_propertiesPanel == nullptr) {
3310         return;
3311     }
3312     if ((clip == nullptr) || !clip->statusReady()) {
3313         for (QWidget *w : m_propertiesPanel->findChildren<ClipPropertiesController *>()) {
3314             delete w;
3315         }
3316         m_propertiesPanel->setProperty("clipId", QString());
3317         m_propertiesPanel->setEnabled(false);
3318         Q_EMIT setupTargets(false, {});
3319         return;
3320     }
3321     m_propertiesPanel->setEnabled(true);
3322     QString panelId = m_propertiesPanel->property("clipId").toString();
3323     if (!forceRefresh && panelId == clip->AbstractProjectItem::clipId()) {
3324         // the properties panel is already displaying current clip, do nothing
3325         return;
3326     }
3327     // Cleanup widget for new content
3328     for (QWidget *w : m_propertiesPanel->findChildren<ClipPropertiesController *>()) {
3329         delete w;
3330     }
3331     m_propertiesPanel->setProperty("clipId", clip->AbstractProjectItem::clipId());
3332     // Setup timeline targets
3333     Q_EMIT setupTargets(clip->hasVideo(), clip->activeStreams());
3334     auto *lay = static_cast<QVBoxLayout *>(m_propertiesPanel->layout());
3335     if (lay == nullptr) {
3336         lay = new QVBoxLayout(m_propertiesPanel);
3337         m_propertiesPanel->setLayout(lay);
3338     }
3339     ClipPropertiesController *panel = clip->buildProperties(m_propertiesPanel);
3340     connect(panel, &ClipPropertiesController::updateClipProperties, this, &Bin::slotEditClipCommand);
3341     connect(panel, &ClipPropertiesController::seekToFrame, m_monitor, static_cast<void (Monitor::*)(int)>(&Monitor::slotSeek));
3342     connect(panel, &ClipPropertiesController::editClip, this, &Bin::slotEditClip);
3343     connect(panel, &ClipPropertiesController::editAnalysis, this, &Bin::slotAddClipExtraData);
3344 
3345     lay->addWidget(panel);
3346 }
3347 
3348 void Bin::slotEditClipCommand(const QString &id, const QMap<QString, QString> &oldProps, const QMap<QString, QString> &newProps)
3349 {
3350     auto *command = new EditClipCommand(this, id, oldProps, newProps, true);
3351     m_doc->commandStack()->push(command);
3352 }
3353 
3354 void Bin::reloadClip(const QString &id)
3355 {
3356     std::shared_ptr<ProjectClip> clip = m_itemModel->getClipByBinID(id);
3357     if (!clip) {
3358         return;
3359     }
3360     clip->reloadProducer(false, false);
3361 }
3362 
3363 void Bin::reloadMonitorIfActive(const QString &id)
3364 {
3365     if (m_monitor->activeClipId() == id || m_monitor->activeClipId().isEmpty()) {
3366         slotOpenCurrent();
3367         if (m_monitor->activeClipId() == id) {
3368             // Ensure proxy action is correctly updated
3369             if (m_doc->useProxy()) {
3370                 std::shared_ptr<AbstractProjectItem> item = m_itemModel->getItemByBinId(id);
3371                 if (item) {
3372                     AbstractProjectItem::PROJECTITEMTYPE itemType = item->itemType();
3373                     if (itemType == AbstractProjectItem::ClipItem) {
3374                         std::shared_ptr<ProjectClip> clip = std::static_pointer_cast<ProjectClip>(item);
3375                         if (clip && clip->statusReady()) {
3376                             m_proxyAction->blockSignals(true);
3377                             m_proxyAction->setChecked(clip->hasProxy());
3378                             m_proxyAction->blockSignals(false);
3379                         }
3380                     }
3381                 }
3382             }
3383             Q_EMIT requestShowClipProperties(getBinClip(id), true);
3384         }
3385     }
3386 }
3387 
3388 void Bin::reloadMonitorStreamIfActive(const QString &id)
3389 {
3390     if (m_monitor->activeClipId() == id) {
3391         m_monitor->reloadActiveStream();
3392     }
3393 }
3394 
3395 void Bin::updateTargets(QString id)
3396 {
3397     if (id == QLatin1String("-1")) {
3398         id = m_monitor->activeClipId();
3399     } else if (m_monitor->activeClipId() != id) {
3400         qDebug() << "ABOIRT A";
3401         return;
3402     }
3403     std::shared_ptr<ProjectClip> clip = m_itemModel->getClipByBinID(id);
3404     if (clip) {
3405         Q_EMIT setupTargets(clip->hasVideo(), clip->activeStreams());
3406     } else {
3407         Q_EMIT setupTargets(false, {});
3408     }
3409 }
3410 
3411 QStringList Bin::getBinFolderClipIds(const QString &id) const
3412 {
3413     QStringList ids;
3414     std::shared_ptr<ProjectFolder> folder = m_itemModel->getFolderByBinId(id);
3415     if (folder) {
3416         for (int i = 0; i < folder->childCount(); i++) {
3417             std::shared_ptr<AbstractProjectItem> child = std::static_pointer_cast<AbstractProjectItem>(folder->child(i));
3418             if (child->itemType() == AbstractProjectItem::ClipItem) {
3419                 ids << child->clipId();
3420             }
3421         }
3422     }
3423     return ids;
3424 }
3425 
3426 std::shared_ptr<ProjectClip> Bin::getBinClip(const QString &id)
3427 {
3428     return m_itemModel->getClipByBinID(id);
3429 }
3430 
3431 const QString Bin::getBinClipName(const QString &id) const
3432 {
3433     std::shared_ptr<ProjectClip> clip = nullptr;
3434     if (id.contains(QLatin1Char('_'))) {
3435         clip = m_itemModel->getClipByBinID(id.section(QLatin1Char('_'), 0, 0));
3436     } else if (!id.isEmpty()) {
3437         clip = m_itemModel->getClipByBinID(id);
3438     }
3439     if (clip) {
3440         return clip->clipName();
3441     }
3442     return QString();
3443 }
3444 
3445 void Bin::setWaitingStatus(const QString &id)
3446 {
3447     std::shared_ptr<ProjectClip> clip = m_itemModel->getClipByBinID(id);
3448     if (clip) {
3449         clip->setClipStatus(FileStatus::StatusWaiting);
3450     }
3451 }
3452 
3453 void Bin::slotRemoveInvalidClip(const QString &id, bool replace, const QString &errorMessage)
3454 {
3455     Q_UNUSED(replace);
3456 
3457     std::shared_ptr<ProjectClip> clip = m_itemModel->getClipByBinID(id);
3458     if (!clip) {
3459         return;
3460     }
3461     Q_EMIT requesteInvalidRemoval(id, clip->url(), errorMessage);
3462 }
3463 
3464 void Bin::selectClip(const std::shared_ptr<ProjectClip> &clip)
3465 {
3466     QModelIndex ix = m_itemModel->getIndexFromItem(clip);
3467     int row = ix.row();
3468     const QModelIndex id = m_itemModel->index(row, 0, ix.parent());
3469     // Ensure parent folder is expanded
3470     if (m_listType == BinTreeView) {
3471         // Make sure parent folder is expanded
3472         auto *view = static_cast<QTreeView *>(m_itemView);
3473         view->expand(m_proxyModel->mapFromSource(ix.parent()));
3474         const QModelIndex id2 = m_itemModel->index(row, m_itemModel->columnCount() - 1, ix.parent());
3475         if (id.isValid() && id2.isValid()) {
3476             m_proxyModel->selectionModel()->select(QItemSelection(m_proxyModel->mapFromSource(id), m_proxyModel->mapFromSource(id2)),
3477                                                    QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Current);
3478         }
3479     } else {
3480         // Ensure parent folder is currently opened
3481         m_itemView->setRootIndex(m_proxyModel->mapFromSource(ix.parent()));
3482         m_upAction->setEnabled(!ix.parent().data(AbstractProjectItem::DataId).toString().isEmpty());
3483         if (id.isValid()) {
3484             m_proxyModel->selectionModel()->select(m_proxyModel->mapFromSource(id), QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Current);
3485         }
3486     }
3487     m_itemView->scrollTo(m_proxyModel->mapFromSource(ix), QAbstractItemView::EnsureVisible);
3488 }
3489 
3490 void Bin::slotOpenCurrent()
3491 {
3492     std::shared_ptr<ProjectClip> currentItem = getFirstSelectedClip();
3493     if (currentItem) {
3494         Q_EMIT openClip(currentItem);
3495     }
3496 }
3497 
3498 void Bin::openProducer(std::shared_ptr<ProjectClip> controller)
3499 {
3500     Q_EMIT openClip(std::move(controller));
3501 }
3502 
3503 void Bin::openProducer(std::shared_ptr<ProjectClip> controller, int in, int out)
3504 {
3505     Q_EMIT openClip(std::move(controller), in, out);
3506 }
3507 
3508 void Bin::emitItemUpdated(std::shared_ptr<AbstractProjectItem> item)
3509 {
3510     Q_EMIT itemUpdated(std::move(item));
3511 }
3512 
3513 void Bin::emitRefreshPanel(const QString &id)
3514 {
3515     Q_EMIT refreshPanel(id);
3516 }
3517 
3518 void Bin::setupGeneratorMenu()
3519 {
3520     if (!m_menu) {
3521         qCDebug(KDENLIVE_LOG) << "Warning, menu was not created, something is wrong";
3522         return;
3523     }
3524 
3525     auto *addMenu = qobject_cast<QMenu *>(pCore->window()->factory()->container(QStringLiteral("generators"), pCore->window()));
3526     if (addMenu) {
3527         QMenu *menu = m_addButton->menu();
3528         menu->addMenu(addMenu);
3529         addMenu->setEnabled(!addMenu->isEmpty());
3530         m_addButton->setMenu(menu);
3531     }
3532 
3533     addMenu = qobject_cast<QMenu *>(pCore->window()->factory()->container(QStringLiteral("extract_audio"), pCore->window()));
3534     if (addMenu) {
3535         m_menu->addMenu(addMenu);
3536         addMenu->setEnabled(!addMenu->isEmpty());
3537         m_extractAudioAction = addMenu;
3538         m_extractAudioAction->setEnabled(false);
3539     }
3540 
3541     addMenu = qobject_cast<QMenu *>(pCore->window()->factory()->container(QStringLiteral("clip_actions"), pCore->window()));
3542     if (addMenu) {
3543         m_menu->addMenu(addMenu);
3544         addMenu->setEnabled(!addMenu->isEmpty());
3545         m_clipsActionsMenu = addMenu;
3546         m_clipsActionsMenu->setEnabled(false);
3547     }
3548 
3549     addMenu = qobject_cast<QMenu *>(pCore->window()->factory()->container(QStringLiteral("clip_in_timeline"), pCore->window()));
3550     if (addMenu) {
3551         m_inTimelineAction = m_menu->addMenu(addMenu);
3552     }
3553 
3554     if (m_locateAction) {
3555         m_menu->addAction(m_locateAction);
3556     }
3557     if (m_reloadAction) {
3558         m_menu->addAction(m_reloadAction);
3559     }
3560     if (m_replaceAction) {
3561         m_menu->addAction(m_replaceAction);
3562     }
3563     if (m_replaceInTimelineAction) {
3564         m_menu->addAction(m_replaceInTimelineAction);
3565     }
3566     if (m_duplicateAction) {
3567         m_menu->addAction(m_duplicateAction);
3568     }
3569     if (m_transcodeAction) {
3570         m_menu->addAction(m_transcodeAction);
3571     }
3572     if (m_proxyAction) {
3573         m_menu->addAction(m_proxyAction);
3574     }
3575 
3576     addMenu = qobject_cast<QMenu *>(pCore->window()->factory()->container(QStringLiteral("clip_timeline"), pCore->window()));
3577     if (addMenu) {
3578         m_menu->addMenu(addMenu);
3579         addMenu->setEnabled(false);
3580     }
3581     m_menu->addAction(m_editAction);
3582     m_menu->addAction(m_openAction);
3583     m_menu->addAction(m_renameAction);
3584     m_menu->addAction(m_deleteAction);
3585     m_menu->insertSeparator(m_deleteAction);
3586 }
3587 
3588 void Bin::setupMenu()
3589 {
3590     auto *addClipMenu = new QMenu(this);
3591 
3592     m_addClip =
3593         addAction(QStringLiteral("add_clip"), i18n("Add Clip or Folder…"), QIcon::fromTheme(QStringLiteral("kdenlive-add-clip")), QStringLiteral("addclip"));
3594     m_addClip->setWhatsThis(xi18nc("@info:whatsthis", "Main dialog to add source material to your project bin (videos, images, audio, titles, animations).<nl/>"
3595                                                       "Click on the down-arrow icon to get a list of source types to select from.<nl/>"
3596                                                       "Click on the media icon to open a window to select source files."));
3597     addClipMenu->addAction(m_addClip);
3598     connect(m_addClip, &QAction::triggered, this, &Bin::slotAddClip);
3599 
3600     setupAddClipAction(addClipMenu, ClipType::Color, QStringLiteral("add_color_clip"), i18n("Add Color Clip…"),
3601                        QIcon::fromTheme(QStringLiteral("kdenlive-add-color-clip")));
3602     setupAddClipAction(addClipMenu, ClipType::SlideShow, QStringLiteral("add_slide_clip"), i18n("Add Image Sequence…"),
3603                        QIcon::fromTheme(QStringLiteral("kdenlive-add-slide-clip")));
3604     setupAddClipAction(addClipMenu, ClipType::Text, QStringLiteral("add_text_clip"), i18n("Add Title Clip…"),
3605                        QIcon::fromTheme(QStringLiteral("kdenlive-add-text-clip")));
3606     setupAddClipAction(addClipMenu, ClipType::TextTemplate, QStringLiteral("add_text_template_clip"), i18n("Add Template Title…"),
3607                        QIcon::fromTheme(QStringLiteral("kdenlive-add-text-clip")));
3608     setupAddClipAction(addClipMenu, ClipType::Animation, QStringLiteral("add_animation_clip"), i18n("Create Animation…"),
3609                        QIcon::fromTheme(QStringLiteral("motion_path_animations")));
3610     setupAddClipAction(addClipMenu, ClipType::Timeline, QStringLiteral("add_playlist_clip"), i18n("Add Sequence…"),
3611                        QIcon::fromTheme(QStringLiteral("list-add")));
3612     QAction *downloadResourceAction =
3613         addAction(QStringLiteral("download_resource"), i18n("Online Resources"), QIcon::fromTheme(QStringLiteral("edit-download")));
3614     addClipMenu->addAction(downloadResourceAction);
3615     connect(downloadResourceAction, &QAction::triggered, pCore->window(), &MainWindow::slotDownloadResources);
3616 
3617     m_locateAction = addAction(QStringLiteral("locate_clip"), i18n("Locate Clip…"), QIcon::fromTheme(QStringLiteral("find-location")));
3618     m_locateAction->setData("locate_clip");
3619     m_locateAction->setEnabled(false);
3620     connect(m_locateAction, &QAction::triggered, this, &Bin::slotLocateClip);
3621 
3622     m_reloadAction = addAction(QStringLiteral("reload_clip"), i18n("Reload Clip"), QIcon::fromTheme(QStringLiteral("view-refresh")));
3623     m_reloadAction->setData("reload_clip");
3624     m_reloadAction->setEnabled(false);
3625     connect(m_reloadAction, &QAction::triggered, this, &Bin::slotReloadClip);
3626 
3627     m_transcodeAction =
3628         addAction(QStringLiteral("friendly_transcoder"), i18n("Transcode to Edit Friendly Format…"), QIcon::fromTheme(QStringLiteral("edit-copy")));
3629     m_transcodeAction->setData("transcode_clip");
3630     m_transcodeAction->setEnabled(false);
3631     connect(m_transcodeAction, &QAction::triggered, this, &Bin::requestSelectionTranscoding);
3632 
3633     m_replaceAction = addAction(QStringLiteral("replace_clip"), i18n("Replace Clip…"), QIcon::fromTheme(QStringLiteral("edit-find-replace")));
3634     m_replaceAction->setData("replace_clip");
3635     m_replaceAction->setEnabled(false);
3636     connect(m_replaceAction, &QAction::triggered, this, &Bin::slotReplaceClip);
3637 
3638     m_replaceInTimelineAction =
3639         addAction(QStringLiteral("replace_in_timeline"), i18n("Replace Clip In Timeline…"), QIcon::fromTheme(QStringLiteral("edit-find-replace")));
3640     m_replaceInTimelineAction->setData("replace_timeline_clip");
3641     m_replaceInTimelineAction->setEnabled(false);
3642     connect(m_replaceInTimelineAction, &QAction::triggered, this, &Bin::slotReplaceClipInTimeline);
3643 
3644     m_duplicateAction = addAction(QStringLiteral("duplicate_clip"), i18n("Duplicate Clip"), QIcon::fromTheme(QStringLiteral("edit-copy")));
3645     m_duplicateAction->setData("duplicate_clip");
3646     m_duplicateAction->setEnabled(false);
3647     connect(m_duplicateAction, &QAction::triggered, this, &Bin::slotDuplicateClip);
3648 
3649     m_proxyAction = new QAction(i18n("Proxy Clip"), pCore->window());
3650     pCore->window()->addAction(QStringLiteral("proxy_clip"), m_proxyAction);
3651     m_proxyAction->setData(QStringList() << QString::number(static_cast<int>(AbstractTask::PROXYJOB)));
3652     m_proxyAction->setCheckable(true);
3653     m_proxyAction->setChecked(false);
3654     m_proxyAction->setEnabled(false);
3655 
3656     m_editAction = addAction(QStringLiteral("clip_properties"), i18n("Clip Properties"), QIcon::fromTheme(QStringLiteral("document-edit")));
3657     m_editAction->setData("clip_properties");
3658     m_editAction->setEnabled(false);
3659     connect(m_editAction, &QAction::triggered, this, static_cast<void (Bin::*)()>(&Bin::slotSwitchClipProperties));
3660 
3661     m_openAction = addAction(QStringLiteral("edit_clip"), i18n("Edit Clip"), QIcon::fromTheme(QStringLiteral("document-open")));
3662     m_openAction->setData("edit_clip");
3663     m_openAction->setEnabled(false);
3664     connect(m_openAction, &QAction::triggered, this, &Bin::slotOpenClipExtern);
3665 
3666     m_renameAction = KStandardAction::renameFile(this, SLOT(slotRenameItem()), this);
3667     m_renameAction->setEnabled(false);
3668     if (m_itemView) {
3669         m_itemView->addAction(m_renameAction);
3670         m_renameAction->setShortcutContext(Qt::WidgetWithChildrenShortcut);
3671     }
3672 
3673     m_deleteAction = addAction(QStringLiteral("delete_clip"), i18n("Delete Clip"), QIcon::fromTheme(QStringLiteral("edit-delete")));
3674     m_deleteAction->setData("delete_clip");
3675     m_deleteAction->setEnabled(false);
3676     connect(m_deleteAction, &QAction::triggered, this, &Bin::slotDeleteClip);
3677 
3678     m_openInBin = addAction(QStringLiteral("add_bin"), i18n("Open in new bin"), QIcon::fromTheme(QStringLiteral("document-open")));
3679     connect(m_openInBin, &QAction::triggered, this, [&]() {
3680         QModelIndex ix = m_proxyModel->selectionModel()->currentIndex();
3681         std::shared_ptr<AbstractProjectItem> currentItem = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(ix));
3682         QString id;
3683         if (currentItem) {
3684             id = currentItem->clipId();
3685         }
3686         pCore.get()->addBin(id);
3687     });
3688 
3689     m_sequencesFolderAction =
3690         addAction(QStringLiteral("sequence_folder"), i18n("Default Target Folder for Sequences"), QIcon::fromTheme(QStringLiteral("favorite")));
3691     m_sequencesFolderAction->setCheckable(true);
3692     connect(m_sequencesFolderAction, &QAction::triggered, this, &Bin::setDefaultSequenceFolder);
3693 
3694     m_audioCapturesFolderAction =
3695         addAction(QStringLiteral("audioCapture_folder"), i18n("Default Target Folder for Audio Captures"), QIcon::fromTheme(QStringLiteral("favorite")));
3696     m_audioCapturesFolderAction->setCheckable(true);
3697     connect(m_audioCapturesFolderAction, &QAction::triggered, this, &Bin::setDefaultAudioCaptureFolder);
3698 
3699     m_createFolderAction = addAction(QStringLiteral("create_folder"), i18n("Create Folder"), QIcon::fromTheme(QStringLiteral("folder-new")));
3700     m_createFolderAction->setWhatsThis(
3701         xi18nc("@info:whatsthis",
3702                "Creates a folder in the current position in the project bin. Allows for better organization of source files. Folders can be nested."));
3703     connect(m_createFolderAction, &QAction::triggered, this, &Bin::slotAddFolder);
3704 
3705     m_upAction = KStandardAction::up(this, SLOT(slotBack()), pCore->window()->actionCollection());
3706 
3707     // Setup actions
3708     QAction *first = m_toolbar->actions().at(0);
3709     m_toolbar->insertAction(first, m_deleteAction);
3710     m_toolbar->insertAction(m_deleteAction, m_createFolderAction);
3711     m_toolbar->insertAction(m_createFolderAction, m_upAction);
3712 
3713     auto *m = new QMenu(this);
3714     m->addActions(addClipMenu->actions());
3715     m_addButton = new QToolButton(this);
3716     m_addButton->setMenu(m);
3717     m_addButton->setDefaultAction(m_addClip);
3718     m_addButton->setPopupMode(QToolButton::MenuButtonPopup);
3719     m_toolbar->insertWidget(m_upAction, m_addButton);
3720     m_menu = new QMenu(this);
3721     if (m_isMainBin) {
3722         m_propertiesDock = pCore->window()->addDock(i18n("Clip Properties"), QStringLiteral("clip_properties"), m_propertiesPanel);
3723         m_propertiesDock->close();
3724     }
3725     connect(m_menu, &QMenu::aboutToShow, this, &Bin::updateTimelineOccurrences);
3726 }
3727 
3728 const QString Bin::getDocumentProperty(const QString &key)
3729 {
3730     return m_doc->getDocumentProperty(key);
3731 }
3732 
3733 void Bin::doDisplayMessage(const QString &text, KMessageWidget::MessageType type, const QList<QAction *> &actions, bool showCloseButton,
3734                            BinMessage::BinCategory messageCategory)
3735 {
3736     // Remove existing actions if any
3737     QList<QAction *> acts = m_infoMessage->actions();
3738     while (!acts.isEmpty()) {
3739         QAction *a = acts.takeFirst();
3740         m_infoMessage->removeAction(a);
3741         delete a;
3742     }
3743     m_currentMessage = messageCategory;
3744     m_infoMessage->setText(text);
3745     m_infoMessage->setWordWrap(text.length() > 35);
3746     for (QAction *action : actions) {
3747         m_infoMessage->addAction(action);
3748         connect(action, &QAction::triggered, this, &Bin::slotMessageActionTriggered);
3749     }
3750     m_infoMessage->setCloseButtonVisible(showCloseButton || actions.isEmpty());
3751     m_infoMessage->setMessageType(type);
3752     m_infoMessage->animatedShow();
3753 }
3754 
3755 void Bin::doDisplayMessage(const QString &text, KMessageWidget::MessageType type, const QString logInfo)
3756 {
3757     // Remove existing actions if any
3758     m_currentMessage = BinMessage::BinCategory::InformationMessage;
3759     QList<QAction *> acts = m_infoMessage->actions();
3760     while (!acts.isEmpty()) {
3761         QAction *a = acts.takeFirst();
3762         m_infoMessage->removeAction(a);
3763         delete a;
3764     }
3765     m_infoMessage->setText(text);
3766     m_infoMessage->setWordWrap(text.length() > 35);
3767     QAction *ac = new QAction(i18n("Show log"), this);
3768     m_infoMessage->addAction(ac);
3769     connect(ac, &QAction::triggered, this, [this, logInfo](bool) {
3770         KMessageBox::error(this, logInfo, i18n("Detailed log"));
3771         slotMessageActionTriggered();
3772     });
3773     m_infoMessage->setCloseButtonVisible(false);
3774     m_infoMessage->setMessageType(type);
3775     m_infoMessage->animatedShow();
3776 }
3777 
3778 void Bin::refreshClip(const QString &id)
3779 {
3780     if (m_monitor->activeClipId() == id) {
3781         if (pCore->monitorManager()->clipMonitorVisible()) {
3782             m_monitor->refreshMonitor(true);
3783         }
3784     }
3785 }
3786 
3787 void Bin::slotCreateProjectClip()
3788 {
3789     auto *act = qobject_cast<QAction *>(sender());
3790     if (act == nullptr) {
3791         // Cannot access triggering action, something is wrong
3792         qCDebug(KDENLIVE_LOG) << "// Error in clip creation action";
3793         return;
3794     }
3795     ClipType::ProducerType type = ClipType::ProducerType(act->data().toInt());
3796     QString parentFolder = getCurrentFolder();
3797     switch (type) {
3798     case ClipType::Color:
3799         ClipCreationDialog::createColorClip(m_doc, parentFolder, m_itemModel);
3800         break;
3801     case ClipType::SlideShow:
3802         ClipCreationDialog::createSlideshowClip(m_doc, parentFolder, m_itemModel);
3803         break;
3804     case ClipType::Text:
3805         ClipCreationDialog::createTitleClip(m_doc, parentFolder, QString(), m_itemModel);
3806         break;
3807     case ClipType::TextTemplate:
3808         ClipCreationDialog::createTitleTemplateClip(m_doc, parentFolder, m_itemModel);
3809         break;
3810     case ClipType::QText:
3811         ClipCreationDialog::createQTextClip(parentFolder, this);
3812         break;
3813     case ClipType::Animation:
3814         ClipCreationDialog::createAnimationClip(m_doc, parentFolder);
3815         break;
3816     case ClipType::Timeline:
3817         buildSequenceClip();
3818         break;
3819     default:
3820         break;
3821     }
3822     pCore->window()->raiseBin();
3823 }
3824 
3825 void Bin::buildSequenceClip(int aTracks, int vTracks)
3826 {
3827     QScopedPointer<QDialog> dia(new QDialog(this));
3828     Ui::NewTimeline_UI dia_ui;
3829     dia_ui.setupUi(dia.data());
3830     dia->setWindowTitle(i18nc("@title:window", "Create New Sequence"));
3831     int timelinesCount = pCore->projectManager()->getTimelinesCount() + 1;
3832     dia_ui.sequence_name->setText(i18n("Sequence %1", timelinesCount));
3833     dia_ui.video_tracks->setValue(vTracks == -1 ? KdenliveSettings::videotracks() : vTracks);
3834     dia_ui.audio_tracks->setValue(aTracks == -1 ? KdenliveSettings::audiotracks() : aTracks);
3835     dia_ui.sequence_name->setFocus();
3836     dia_ui.sequence_name->selectAll();
3837     if (dia->exec() == QDialog::Accepted) {
3838         int videoTracks = dia_ui.video_tracks->value();
3839         int audioTracks = dia_ui.audio_tracks->value();
3840         QString parentFolder = getCurrentFolder();
3841         if (m_itemModel->defaultSequencesFolder() > -1) {
3842             const QString sequenceFolder = QString::number(m_itemModel->defaultSequencesFolder());
3843             std::shared_ptr<ProjectFolder> folderItem = m_itemModel->getFolderByBinId(sequenceFolder);
3844             if (folderItem) {
3845                 parentFolder = sequenceFolder;
3846             }
3847         }
3848         ClipCreationDialog::createPlaylistClip(dia_ui.sequence_name->text(), {audioTracks, videoTracks}, parentFolder, m_itemModel);
3849     }
3850 }
3851 
3852 const QString Bin::buildSequenceClipWithUndo(Fun &undo, Fun &redo, int aTracks, int vTracks, QString suggestedName)
3853 {
3854     QScopedPointer<QDialog> dia(new QDialog(this));
3855     Ui::NewTimeline_UI dia_ui;
3856     dia_ui.setupUi(dia.data());
3857     dia->setWindowTitle(i18nc("@title:window", "Create New Sequence"));
3858     if (suggestedName.isEmpty()) {
3859         int timelinesCount = pCore->projectManager()->getTimelinesCount() + 1;
3860         suggestedName = i18n("Sequence %1", timelinesCount);
3861     }
3862     dia_ui.sequence_name->setText(suggestedName);
3863     dia_ui.video_tracks->setValue(vTracks == -1 ? KdenliveSettings::videotracks() : vTracks);
3864     dia_ui.audio_tracks->setValue(aTracks == -1 ? KdenliveSettings::audiotracks() : aTracks);
3865     if (dia->exec() == QDialog::Accepted) {
3866         int videoTracks = dia_ui.video_tracks->value();
3867         int audioTracks = dia_ui.audio_tracks->value();
3868         QString parentFolder = getCurrentFolder();
3869         if (m_itemModel->defaultSequencesFolder() > -1) {
3870             const QString sequenceFolder = QString::number(m_itemModel->defaultSequencesFolder());
3871             std::shared_ptr<ProjectFolder> folderItem = m_itemModel->getFolderByBinId(sequenceFolder);
3872             if (folderItem) {
3873                 parentFolder = sequenceFolder;
3874             }
3875         }
3876         return ClipCreator::createPlaylistClipWithUndo(dia_ui.sequence_name->text(), {audioTracks, videoTracks}, parentFolder, m_itemModel, undo, redo);
3877     }
3878     return QString();
3879 }
3880 
3881 void Bin::slotItemDropped(const QStringList ids, const QModelIndex parent)
3882 {
3883     std::shared_ptr<AbstractProjectItem> parentItem;
3884     if (parent.isValid()) {
3885         parentItem = m_itemModel->getBinItemByIndex(parent);
3886         parentItem = parentItem->getEnclosingFolder(false);
3887     } else {
3888         parentItem = m_itemModel->getRootFolder();
3889     }
3890     auto *moveCommand = new QUndoCommand();
3891     moveCommand->setText(i18np("Move Clip", "Move Clips", ids.count()));
3892     QStringList folderIds;
3893     for (const QString &id : ids) {
3894         if (id.contains(QLatin1Char('/'))) {
3895             // trying to move clip zone, not allowed. Ignore
3896             continue;
3897         }
3898         if (id.startsWith(QLatin1Char('#'))) {
3899             // moving a folder, keep it for later
3900             folderIds << id;
3901             continue;
3902         }
3903         std::shared_ptr<ProjectClip> currentItem = m_itemModel->getClipByBinID(id);
3904         if (!currentItem) {
3905             continue;
3906         }
3907         std::shared_ptr<AbstractProjectItem> currentParent = currentItem->parent();
3908         if (currentParent != parentItem) {
3909             // Item was dropped on a different folder
3910             new MoveBinClipCommand(this, id, currentParent->clipId(), parentItem->clipId(), moveCommand);
3911         }
3912     }
3913     if (!folderIds.isEmpty()) {
3914         for (QString id : qAsConst(folderIds)) {
3915             id.remove(0, 1);
3916             std::shared_ptr<ProjectFolder> currentItem = m_itemModel->getFolderByBinId(id);
3917             if (!currentItem || currentItem == parentItem) {
3918                 continue;
3919             }
3920             std::shared_ptr<AbstractProjectItem> currentParent = currentItem->parent();
3921             if (currentParent != parentItem) {
3922                 // Item was dropped on a different folder
3923                 new MoveBinFolderCommand(this, id, currentParent->clipId(), parentItem->clipId(), moveCommand);
3924             }
3925         }
3926     }
3927     if (moveCommand->childCount() == 0) {
3928         pCore->displayMessage(i18n("No valid clip to insert"), MessageType::ErrorMessage, 500);
3929     } else {
3930         m_doc->commandStack()->push(moveCommand);
3931     }
3932 }
3933 
3934 void Bin::slotAddEffect(std::vector<QString> ids, const QStringList &effectData)
3935 {
3936     if (ids.size() == 0) {
3937         // Apply effect to all selected clips
3938         ids = selectedClipsIds();
3939     }
3940     if (ids.size() == 0) {
3941         pCore->displayMessage(i18n("Select a clip to apply an effect"), MessageType::ErrorMessage, 500);
3942     }
3943     for (auto &id : ids) {
3944         std::shared_ptr<ProjectClip> clip = m_itemModel->getClipByBinID(id);
3945         if (clip) {
3946             if (effectData.count() == 5) {
3947                 // Paste effect from another stack
3948                 std::shared_ptr<EffectStackModel> sourceStack =
3949                     pCore->getItemEffectStack(QUuid(effectData.at(4)), effectData.at(1).toInt(), effectData.at(2).toInt());
3950                 clip->copyEffect(sourceStack, effectData.at(3).toInt());
3951             } else {
3952                 clip->addEffect(effectData.constFirst());
3953             }
3954         }
3955     }
3956 }
3957 
3958 void Bin::slotEffectDropped(const QStringList &effectData, const QModelIndex &parent)
3959 {
3960     if (parent.isValid()) {
3961         std::shared_ptr<AbstractProjectItem> parentItem = m_itemModel->getBinItemByIndex(parent);
3962         if (parentItem->itemType() == AbstractProjectItem::FolderItem) {
3963             // effect not supported on folder items
3964             Q_EMIT displayBinMessage(i18n("Cannot apply effects on folders"), KMessageWidget::Information);
3965             return;
3966         }
3967         int row = 0;
3968         QModelIndex parentIndex;
3969         if (parentItem->itemType() == AbstractProjectItem::SubClipItem) {
3970             // effect only supported on clip items
3971             parentItem = std::static_pointer_cast<ProjectSubClip>(parentItem)->getMasterClip();
3972             QModelIndex ix = m_itemModel->getIndexFromItem(parentItem);
3973             row = ix.row();
3974             parentIndex = ix.parent();
3975         } else if (parentItem->itemType() == AbstractProjectItem::ClipItem) {
3976             // effect only supported on clip items
3977             row = parent.row();
3978             parentIndex = parent.parent();
3979         }
3980         bool res = false;
3981         if (effectData.count() == 5) {
3982             // Paste effect from another stack
3983             std::shared_ptr<EffectStackModel> sourceStack =
3984                 pCore->getItemEffectStack(QUuid(effectData.at(4)), effectData.at(1).toInt(), effectData.at(2).toInt());
3985             res = std::static_pointer_cast<ProjectClip>(parentItem)->copyEffect(sourceStack, effectData.at(3).toInt());
3986         } else {
3987             res = std::static_pointer_cast<ProjectClip>(parentItem)->addEffect(effectData.constFirst());
3988         }
3989         if (!res) {
3990             pCore->displayMessage(i18n("Cannot add effect to clip"), MessageType::ErrorMessage);
3991         } else {
3992             m_proxyModel->selectionModel()->clearSelection();
3993             const QModelIndex id = m_itemModel->index(row, 0, parentIndex);
3994             const QModelIndex id2 = m_itemModel->index(row, m_itemModel->columnCount() - 1, parentIndex);
3995             if (id.isValid() && id2.isValid()) {
3996                 m_proxyModel->selectionModel()->select(QItemSelection(m_proxyModel->mapFromSource(id), m_proxyModel->mapFromSource(id2)),
3997                                                        QItemSelectionModel::Select);
3998             }
3999             setCurrent(parentItem);
4000         }
4001     }
4002 }
4003 
4004 void Bin::slotTagDropped(const QString &tag, const QModelIndex &parent)
4005 {
4006     if (parent.isValid()) {
4007         std::shared_ptr<AbstractProjectItem> parentItem = m_itemModel->getBinItemByIndex(parent);
4008         if (parentItem->itemType() == AbstractProjectItem::ClipItem || parentItem->itemType() == AbstractProjectItem::SubClipItem) {
4009             if (parentItem->itemType() == AbstractProjectItem::SubClipItem) {
4010                 qDebug() << "TAG DROPPED ON CLIPZOINE\n\n!!!!!!!!!!!!!!!";
4011             }
4012             // effect only supported on clip/subclip items
4013             QString currentTag = parentItem->tags();
4014             QMap<QString, QString> oldProps;
4015             oldProps.insert(QStringLiteral("kdenlive:tags"), currentTag);
4016             QMap<QString, QString> newProps;
4017             if (currentTag.isEmpty()) {
4018                 currentTag = tag;
4019             } else if (!currentTag.contains(tag)) {
4020                 currentTag.append(QStringLiteral(";") + tag);
4021             }
4022             newProps.insert(QStringLiteral("kdenlive:tags"), currentTag);
4023             slotEditClipCommand(parentItem->clipId(), oldProps, newProps);
4024             return;
4025         }
4026         if (parentItem->itemType() == AbstractProjectItem::FolderItem) {
4027             QList<QString> allClips;
4028             QList<std::shared_ptr<ProjectClip>> children = std::static_pointer_cast<ProjectFolder>(parentItem)->childClips();
4029             for (auto &clp : children) {
4030                 allClips << clp->clipId();
4031             }
4032             editTags(allClips, tag, true);
4033             return;
4034         }
4035     }
4036     pCore->displayMessage(i18n("Select a clip to add a tag"), MessageType::ErrorMessage);
4037 }
4038 
4039 void Bin::switchTag(const QString &tag, bool add)
4040 {
4041     const QModelIndexList indexes = m_proxyModel->selectionModel()->selectedIndexes();
4042     if (indexes.isEmpty()) {
4043         pCore->displayMessage(i18n("Select a clip to add a tag"), MessageType::ErrorMessage);
4044     }
4045     // Check for folders
4046     QList<QString> allClips;
4047     for (const QModelIndex &ix : indexes) {
4048         if (!ix.isValid() || ix.column() != 0) {
4049             continue;
4050         }
4051         std::shared_ptr<AbstractProjectItem> parentItem = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(ix));
4052         if (parentItem->itemType() == AbstractProjectItem::FolderItem) {
4053             QList<std::shared_ptr<ProjectClip>> children = std::static_pointer_cast<ProjectFolder>(parentItem)->childClips();
4054             for (auto &clp : children) {
4055                 allClips << clp->clipId();
4056             }
4057         } else {
4058             allClips << parentItem->clipId();
4059         }
4060     }
4061     editTags(allClips, tag, add);
4062 }
4063 
4064 void Bin::updateTags(const QMap<int, QStringList> &previousTags, const QMap<int, QStringList> &tags)
4065 {
4066     Fun undo = [this, previousTags, tags]() {
4067         m_tagsWidget->rebuildTags(previousTags);
4068         rebuildFilters(previousTags.size());
4069         pCore->updateProjectTags(tags.size(), previousTags);
4070         return true;
4071     };
4072     Fun redo = [this, previousTags, tags]() {
4073         m_tagsWidget->rebuildTags(tags);
4074         rebuildFilters(tags.size());
4075         pCore->updateProjectTags(previousTags.size(), tags);
4076         return true;
4077     };
4078     // Check if some tags were removed
4079     QList<QStringList> previous = previousTags.values();
4080     QList<QStringList> updated = tags.values();
4081     QStringList deletedTags;
4082     QMap<QString, QString> modifiedTags;
4083     for (auto p : previous) {
4084         bool tagExists = false;
4085         for (auto n : updated) {
4086             if (n.first() == p.first()) {
4087                 // Tag still exists
4088                 if (n.at(1) != p.at(1)) {
4089                     // Tag color has changed
4090                     modifiedTags.insert(p.at(1), n.at(1));
4091                 }
4092                 tagExists = true;
4093                 break;
4094             }
4095         }
4096         if (!tagExists) {
4097             // Tag was removed
4098             deletedTags << p.at(1);
4099         }
4100     }
4101     if (!deletedTags.isEmpty()) {
4102         // Remove tag from clips
4103         for (auto &t : deletedTags) {
4104             // Find clips with the tag
4105             const QList<QString> clips = getAllClipsWithTag(t);
4106             Fun update_tags_redo = [this, clips, t]() {
4107                 for (auto &cid : clips) {
4108                     std::shared_ptr<ProjectClip> clip = getBinClip(cid);
4109                     if (clip) {
4110                         QString tags = clip->tags();
4111                         QStringList tagsList = tags.split(QLatin1Char(';'));
4112                         tagsList.removeAll(t);
4113                         QMap<QString, QString> props;
4114                         props.insert(QStringLiteral("kdenlive:tags"), tagsList.join(QLatin1Char(';')));
4115                         slotUpdateClipProperties(cid, props, false);
4116                     }
4117                 }
4118                 return true;
4119             };
4120             Fun update_tags_undo = [this, clips, t]() {
4121                 for (auto &cid : clips) {
4122                     std::shared_ptr<ProjectClip> clip = getBinClip(cid);
4123                     if (clip) {
4124                         QString tags = clip->tags();
4125                         QStringList tagsList = tags.split(QLatin1Char(';'));
4126                         if (!tagsList.contains(t)) {
4127                             tagsList << t;
4128                         }
4129                         QMap<QString, QString> props;
4130                         props.insert(QStringLiteral("kdenlive:tags"), tagsList.join(QLatin1Char(';')));
4131                         slotUpdateClipProperties(cid, props, false);
4132                     }
4133                 }
4134                 return true;
4135             };
4136             UPDATE_UNDO_REDO(update_tags_redo, update_tags_undo, undo, redo);
4137         }
4138     }
4139     if (!modifiedTags.isEmpty()) {
4140         // Replace tag in clips
4141         QMapIterator<QString, QString> i(modifiedTags);
4142         while (i.hasNext()) {
4143             // Find clips with the tag
4144             i.next();
4145             const QList<QString> clips = getAllClipsWithTag(i.key());
4146             Fun update_tags_redo = [this, clips, previous = i.key(), updated = i.value()]() {
4147                 for (auto &cid : clips) {
4148                     std::shared_ptr<ProjectClip> clip = getBinClip(cid);
4149                     if (clip) {
4150                         QString tags = clip->tags();
4151                         QStringList tagsList = tags.split(QLatin1Char(';'));
4152                         tagsList.removeAll(previous);
4153                         tagsList << updated;
4154                         QMap<QString, QString> props;
4155                         props.insert(QStringLiteral("kdenlive:tags"), tagsList.join(QLatin1Char(';')));
4156                         slotUpdateClipProperties(cid, props, false);
4157                     }
4158                 }
4159                 return true;
4160             };
4161             Fun update_tags_undo = [this, clips, previous = i.key(), updated = i.value()]() {
4162                 for (auto &cid : clips) {
4163                     std::shared_ptr<ProjectClip> clip = getBinClip(cid);
4164                     if (clip) {
4165                         QString tags = clip->tags();
4166                         QStringList tagsList = tags.split(QLatin1Char(';'));
4167                         tagsList.removeAll(updated);
4168                         if (!tagsList.contains(previous)) {
4169                             tagsList << previous;
4170                         }
4171                         QMap<QString, QString> props;
4172                         props.insert(QStringLiteral("kdenlive:tags"), tagsList.join(QLatin1Char(';')));
4173                         slotUpdateClipProperties(cid, props, false);
4174                     }
4175                 }
4176                 return true;
4177             };
4178             UPDATE_UNDO_REDO(update_tags_redo, update_tags_undo, undo, redo);
4179         }
4180     }
4181     redo();
4182     pCore->pushUndo(undo, redo, i18n("Edit Tags"));
4183 }
4184 
4185 const QList<QString> Bin::getAllClipsWithTag(const QString &tag)
4186 {
4187     QList<QString> list;
4188     QList<std::shared_ptr<ProjectClip>> allClipIds = m_itemModel->getRootFolder()->childClips();
4189     for (const auto &clip : qAsConst(allClipIds)) {
4190         if (clip->tags().contains(tag)) {
4191             list << clip->clipId();
4192         }
4193     }
4194     return list;
4195 }
4196 
4197 void Bin::editTags(const QList<QString> &allClips, const QString &tag, bool add)
4198 {
4199     for (const QString &id : allClips) {
4200         std::shared_ptr<AbstractProjectItem> clip = m_itemModel->getItemByBinId(id);
4201         if (clip) {
4202             // effect only supported on clip/subclip items
4203             QString currentTag = clip->tags();
4204             QMap<QString, QString> oldProps;
4205             oldProps.insert(QStringLiteral("kdenlive:tags"), currentTag);
4206             QMap<QString, QString> newProps;
4207             if (add) {
4208                 if (currentTag.isEmpty()) {
4209                     currentTag = tag;
4210                 } else if (!currentTag.contains(tag)) {
4211                     currentTag.append(QStringLiteral(";") + tag);
4212                 }
4213                 newProps.insert(QStringLiteral("kdenlive:tags"), currentTag);
4214             } else {
4215                 QStringList tags = currentTag.split(QLatin1Char(';'));
4216                 tags.removeAll(tag);
4217                 newProps.insert(QStringLiteral("kdenlive:tags"), tags.join(QLatin1Char(';')));
4218             }
4219             slotEditClipCommand(id, oldProps, newProps);
4220         }
4221     }
4222 }
4223 
4224 void Bin::editMasterEffect(const std::shared_ptr<AbstractProjectItem> &clip)
4225 {
4226     if (m_gainedFocus) {
4227         // Widget just gained focus, updating stack is managed in the eventfilter event, not from item
4228         return;
4229     }
4230     if (clip) {
4231         if (clip->itemType() == AbstractProjectItem::ClipItem) {
4232             std::shared_ptr<ProjectClip> clp = std::static_pointer_cast<ProjectClip>(clip);
4233             Q_EMIT requestShowEffectStack(clp->clipName(), clp->m_effectStack, clp->getFrameSize(), false);
4234             return;
4235         }
4236         if (clip->itemType() == AbstractProjectItem::SubClipItem) {
4237             if (auto ptr = clip->parentItem().lock()) {
4238                 std::shared_ptr<ProjectClip> clp = std::static_pointer_cast<ProjectClip>(ptr);
4239                 Q_EMIT requestShowEffectStack(clp->clipName(), clp->m_effectStack, clp->getFrameSize(), false);
4240             }
4241             return;
4242         }
4243     }
4244     Q_EMIT requestShowEffectStack(QString(), nullptr, QSize(), false);
4245 }
4246 
4247 void Bin::slotGotFocus()
4248 {
4249     m_gainedFocus = true;
4250 }
4251 
4252 void Bin::doMoveClip(const QString &id, const QString &newParentId)
4253 {
4254     std::shared_ptr<ProjectClip> currentItem = m_itemModel->getClipByBinID(id);
4255     if (!currentItem) {
4256         return;
4257     }
4258     std::shared_ptr<AbstractProjectItem> currentParent = currentItem->parent();
4259     std::shared_ptr<ProjectFolder> newParent = m_itemModel->getFolderByBinId(newParentId);
4260     currentItem->changeParent(newParent);
4261 }
4262 
4263 void Bin::doMoveFolder(const QString &id, const QString &newParentId)
4264 {
4265     std::shared_ptr<ProjectFolder> currentItem = m_itemModel->getFolderByBinId(id);
4266     std::shared_ptr<AbstractProjectItem> currentParent = currentItem->parent();
4267     std::shared_ptr<ProjectFolder> newParent = m_itemModel->getFolderByBinId(newParentId);
4268     currentParent->removeChild(currentItem);
4269     currentItem->changeParent(newParent);
4270 }
4271 
4272 void Bin::droppedUrls(const QList<QUrl> &urls, const QString &folderInfo)
4273 {
4274     QModelIndex current;
4275     if (folderInfo.isEmpty()) {
4276         current = m_proxyModel->mapToSource(m_proxyModel->selectionModel()->currentIndex());
4277     } else {
4278         // get index for folder
4279         std::shared_ptr<ProjectFolder> folder = m_itemModel->getFolderByBinId(folderInfo);
4280         if (!folder) {
4281             folder = m_itemModel->getRootFolder();
4282         }
4283         current = m_itemModel->getIndexFromItem(folder);
4284     }
4285     slotUrlsDropped(urls, current);
4286 }
4287 
4288 const QString Bin::slotAddClipToProject(const QUrl &url)
4289 {
4290     QList<QUrl> urls;
4291     urls << url;
4292     QModelIndex current = m_proxyModel->mapToSource(m_proxyModel->selectionModel()->currentIndex());
4293     return slotUrlsDropped(urls, current);
4294 }
4295 
4296 const QString Bin::slotUrlsDropped(const QList<QUrl> urls, const QModelIndex parent)
4297 {
4298     QString parentFolder = m_itemModel->getRootFolder()->clipId();
4299     if (parent.isValid()) {
4300         // Check if drop occurred on a folder
4301         std::shared_ptr<AbstractProjectItem> parentItem = m_itemModel->getBinItemByIndex(parent);
4302         while (parentItem->itemType() != AbstractProjectItem::FolderItem) {
4303             parentItem = parentItem->parent();
4304         }
4305         parentFolder = parentItem->clipId();
4306     }
4307     const QString id = ClipCreator::createClipsFromList(urls, true, parentFolder, m_itemModel);
4308     if (!id.isEmpty()) {
4309         std::shared_ptr<AbstractProjectItem> item = m_itemModel->getItemByBinId(id);
4310         if (item) {
4311             QModelIndex ix = m_itemModel->getIndexFromItem(item);
4312             m_itemView->scrollTo(m_proxyModel->mapFromSource(ix), QAbstractItemView::EnsureVisible);
4313         }
4314     }
4315     return id;
4316 }
4317 
4318 void Bin::slotExpandUrl(const ItemInfo &info, const QString &url, QUndoCommand *command)
4319 {
4320     Q_UNUSED(info)
4321     Q_UNUSED(url)
4322     Q_UNUSED(command)
4323     // TODO reimplement this
4324     /*
4325     // Create folder to hold imported clips
4326     QString folderName = QFileInfo(url).fileName().section(QLatin1Char('.'), 0, 0);
4327     QString folderId = QString::number(getFreeFolderId());
4328     new AddBinFolderCommand(this, folderId, folderName.isEmpty() ? i18n("Folder") : folderName, m_itemModel->getRootFolder()->clipId(), false, command);
4329 
4330     // Parse playlist clips
4331     QDomDocument doc;
4332     QFile file(url);
4333     doc.setContent(&file, false); //TODO: use Xml::docContentFromFile
4334     file.close();
4335     bool invalid = false;
4336     if (doc.documentElement().isNull()) {
4337         invalid = true;
4338     }
4339     QDomNodeList producers = doc.documentElement().elementsByTagName(QStringLiteral("producer"));
4340     QDomNodeList tracks = doc.documentElement().elementsByTagName(QStringLiteral("track"));
4341     if (invalid || producers.isEmpty()) {
4342         doDisplayMessage(i18n("Playlist clip %1 is invalid.", QFileInfo(url).fileName()), KMessageWidget::Warning);
4343         delete command;
4344         return;
4345     }
4346     if (tracks.count() > pCore->projectManager()->currentTimeline()->visibleTracksCount() + 1) {
4347         doDisplayMessage(
4348             i18n("Playlist clip %1 has too many tracks (%2) to be imported. Add new tracks to your project.", QFileInfo(url).fileName(), tracks.count()),
4349             KMessageWidget::Warning);
4350         delete command;
4351         return;
4352     }
4353     // Maps playlist producer IDs to (project) bin producer IDs.
4354     QMap<QString, QString> idMap;
4355     // Maps hash IDs to (project) first playlist producer instance ID. This is
4356     // necessary to detect duplicate producer serializations produced by MLT.
4357     // This covers, for instance, images and titles.
4358     QMap<QString, QString> hashToIdMap;
4359     QDir mltRoot(doc.documentElement().attribute(QStringLiteral("root")));
4360     for (int i = 0; i < producers.count(); i++) {
4361         QDomElement prod = producers.at(i).toElement();
4362         QString originalId = prod.attribute(QStringLiteral("id"));
4363         // track producer
4364         if (originalId.contains(QLatin1Char('_'))) {
4365             originalId = originalId.section(QLatin1Char('_'), 0, 0);
4366         }
4367         // slowmotion producer
4368         if (originalId.contains(QLatin1Char(':'))) {
4369             originalId = originalId.section(QLatin1Char(':'), 1, 1);
4370         }
4371 
4372         // We already have seen and mapped this producer.
4373         if (idMap.contains(originalId)) {
4374             continue;
4375         }
4376 
4377         // Check for duplicate producers, based on hash value of producer.
4378         // Be careful as to the kdenlive:file_hash! It is not unique for
4379         // title clips, nor color clips. Also not sure about image sequences.
4380         // So we use mlt service-specific hashes to identify duplicate producers.
4381         QString hash;
4382         QString mltService = Xml::getXmlProperty(prod, QStringLiteral("mlt_service"));
4383         if (mltService == QLatin1String("pixbuf") || mltService == QLatin1String("qimage") || mltService == QLatin1String("kdenlivetitle") ||
4384             mltService == QLatin1String("color") || mltService == QLatin1String("colour")) {
4385             hash = mltService + QLatin1Char(':') + Xml::getXmlProperty(prod, QStringLiteral("kdenlive:clipname")) + QLatin1Char(':') +
4386                    Xml::getXmlProperty(prod, QStringLiteral("kdenlive:folderid")) + QLatin1Char(':');
4387             if (mltService == QLatin1String("kdenlivetitle")) {
4388                 // Calculate hash based on title contents.
4389                 hash.append(
4390                     QString(QCryptographicHash::hash(Xml::getXmlProperty(prod, QStringLiteral("xmldata")).toUtf8(), QCryptographicHash::Md5).toHex()));
4391             } else if (mltService == QLatin1String("pixbuf") || mltService == QLatin1String("qimage") || mltService == QLatin1String("color") ||
4392                        mltService == QLatin1String("colour")) {
4393                 hash.append(Xml::getXmlProperty(prod, QStringLiteral("resource")));
4394             }
4395 
4396             QString singletonId = hashToIdMap.value(hash, QString());
4397             if (singletonId.length() != 0) {
4398                 // map duplicate producer ID to single bin clip producer ID.
4399                 qCDebug(KDENLIVE_LOG) << "found duplicate producer:" << hash << ", reusing newID:" << singletonId;
4400                 idMap.insert(originalId, singletonId);
4401                 continue;
4402             }
4403         }
4404 
4405         // First occurrence of a producer, so allocate new bin clip producer ID.
4406         QString newId = QString::number(getFreeClipId());
4407         idMap.insert(originalId, newId);
4408         qCDebug(KDENLIVE_LOG) << "originalId: " << originalId << ", newId: " << newId;
4409 
4410         // Ensure to register new bin clip producer ID in hash hashmap for
4411         // those clips that MLT likes to serialize multiple times. This is
4412         // indicated by having a hash "value" unqual "". See also above.
4413         if (hash.length() != 0) {
4414             hashToIdMap.insert(hash, newId);
4415         }
4416 
4417         // Add clip
4418         QDomElement clone = prod.cloneNode(true).toElement();
4419         EffectsList::setProperty(clone, QStringLiteral("kdenlive:folderid"), folderId);
4420         // Do we have a producer that uses a resource property that contains a path?
4421         if (mltService == QLatin1String("avformat-novalidate") // av clip
4422             || mltService == QLatin1String("avformat")         // av clip
4423             || mltService == QLatin1String("pixbuf")           // image (sequence) clip
4424             || mltService == QLatin1String("qimage")           // image (sequence) clip
4425             || mltService == QLatin1String("xml")              // MLT playlist clip, someone likes recursion :)
4426             ) {
4427             // Make sure to correctly resolve relative resource paths based on
4428             // the playlist's root, not on this project's root
4429             QString resource = Xml::getXmlProperty(clone, QStringLiteral("resource"));
4430             if (QFileInfo(resource).isRelative()) {
4431                 QFileInfo rootedResource(mltRoot, resource);
4432                 qCDebug(KDENLIVE_LOG) << "fixed resource path for producer, newId:" << newId << "resource:" << rootedResource.absoluteFilePath();
4433                 EffectsList::setProperty(clone, QStringLiteral("resource"), rootedResource.absoluteFilePath());
4434             }
4435         }
4436 
4437         ClipCreationDialog::createClipsCommand(this, clone, newId, command);
4438     }
4439     pCore->projectManager()->currentTimeline()->importPlaylist(info, idMap, doc, command);
4440     */
4441 }
4442 
4443 void Bin::slotItemEdited(const QModelIndex &ix, const QModelIndex &, const QVector<int> &roles)
4444 {
4445     if (ix.isValid() && roles.contains(AbstractProjectItem::DataName)) {
4446         // Clip renamed
4447         std::shared_ptr<AbstractProjectItem> item = m_itemModel->getBinItemByIndex(ix);
4448         auto clip = std::static_pointer_cast<ProjectClip>(item);
4449         if (clip) {
4450             Q_EMIT clipNameChanged(clip->AbstractProjectItem::clipId().toInt(), clip->clipName());
4451         }
4452     }
4453 }
4454 
4455 void Bin::renameSubClipCommand(const QString &id, const QString &newName, const QString &oldName, int in, int out)
4456 {
4457     auto *command = new RenameBinSubClipCommand(this, id, newName, oldName, in, out);
4458     m_doc->commandStack()->push(command);
4459 }
4460 
4461 void Bin::renameSubClip(const QString &id, const QString &newName, int in, int out)
4462 {
4463     std::shared_ptr<ProjectClip> clip = m_itemModel->getClipByBinID(id);
4464     if (!clip) {
4465         return;
4466     }
4467     std::shared_ptr<ProjectSubClip> sub = clip->getSubClip(in, out);
4468     if (!sub) {
4469         return;
4470     }
4471     sub->setName(newName.isEmpty() ? i18n("Unnamed") : newName);
4472     clip->updateZones();
4473     Q_EMIT itemUpdated(sub);
4474 }
4475 
4476 void Bin::slotStartFilterJob(const ItemInfo &info, const QString &id, QMap<QString, QString> &filterParams, QMap<QString, QString> &consumerParams,
4477                              QMap<QString, QString> &extraParams)
4478 {
4479     Q_UNUSED(info)
4480     Q_UNUSED(id)
4481     Q_UNUSED(filterParams)
4482     Q_UNUSED(consumerParams)
4483     Q_UNUSED(extraParams)
4484     // TODO refac
4485     /*
4486     std::shared_ptr<ProjectClip> clip = getBinClip(id);
4487     if (!clip) {
4488         return;
4489     }
4490 
4491     QMap<QString, QString> producerParams = QMap<QString, QString>();
4492     producerParams.insert(QStringLiteral("producer"), clip->url());
4493     if (info.cropDuration != GenTime()) {
4494         producerParams.insert(QStringLiteral("in"), QString::number((int)info.cropStart.frames(pCore->getCurrentFps())));
4495         producerParams.insert(QStringLiteral("out"), QString::number((int)(info.cropStart + info.cropDuration).frames(pCore->getCurrentFps())));
4496         extraParams.insert(QStringLiteral("clipStartPos"), QString::number((int)info.startPos.frames(pCore->getCurrentFps())));
4497         extraParams.insert(QStringLiteral("clipTrack"), QString::number(info.track));
4498     } else {
4499         // We want to process whole clip
4500         producerParams.insert(QStringLiteral("in"), QString::number(0));
4501         producerParams.insert(QStringLiteral("out"), QString::number(-1));
4502     }
4503     */
4504 }
4505 
4506 void Bin::focusBinView() const
4507 {
4508     m_itemView->setFocus();
4509 }
4510 
4511 void Bin::slotOpenClipExtern()
4512 {
4513     std::shared_ptr<ProjectClip> clip = getFirstSelectedClip();
4514     if (!clip) {
4515         return;
4516     }
4517     QString errorString;
4518     switch (clip->clipType()) {
4519     case ClipType::Text:
4520     case ClipType::TextTemplate:
4521         showTitleWidget(clip);
4522         break;
4523     case ClipType::Image: {
4524         if (KdenliveSettings::defaultimageapp().isEmpty()) {
4525             QUrl url = KUrlRequesterDialog::getUrl(QUrl(), this, i18n("Enter path for your image editing application"));
4526             if (!url.isEmpty()) {
4527                 KdenliveSettings::setDefaultimageapp(url.toLocalFile());
4528                 KdenliveSettingsDialog *d = static_cast<KdenliveSettingsDialog *>(KConfigDialog::exists(QStringLiteral("settings")));
4529                 if (d) {
4530                     d->updateExternalApps();
4531                 }
4532             }
4533         }
4534         if (!KdenliveSettings::defaultimageapp().isEmpty()) {
4535             errorString = pCore->openExternalApp(KdenliveSettings::defaultimageapp(), {clip->url()});
4536         } else {
4537             KMessageBox::error(QApplication::activeWindow(), i18n("Please set a default application to open image files"));
4538         }
4539     } break;
4540     case ClipType::Audio: {
4541         if (KdenliveSettings::defaultaudioapp().isEmpty()) {
4542             QUrl url = KUrlRequesterDialog::getUrl(QUrl(), this, i18n("Enter path for your audio editing application"));
4543             if (!url.isEmpty()) {
4544                 KdenliveSettings::setDefaultaudioapp(url.toLocalFile());
4545                 KdenliveSettingsDialog *d = static_cast<KdenliveSettingsDialog *>(KConfigDialog::exists(QStringLiteral("settings")));
4546                 if (d) {
4547                     d->updateExternalApps();
4548                 }
4549             }
4550         }
4551         if (!KdenliveSettings::defaultaudioapp().isEmpty()) {
4552             errorString = pCore->openExternalApp(KdenliveSettings::defaultaudioapp(), {clip->url()});
4553         } else {
4554             KMessageBox::error(QApplication::activeWindow(), i18n("Please set a default application to open audio files"));
4555         }
4556     } break;
4557     case ClipType::Animation: {
4558         GlaxnimateLauncher::instance().openFile(clip->url());
4559     } break;
4560     default:
4561         break;
4562     }
4563     if (!errorString.isEmpty()) {
4564         KMessageBox::detailedError(QApplication::activeWindow(), i18n("Cannot open file %1", clip->url()), errorString);
4565     }
4566 }
4567 
4568 /*
4569 void Bin::slotGotFilterJobResults(const QString &id, int startPos, int track, const stringMap &results, const stringMap &filterInfo)
4570 {
4571     if (filterInfo.contains(QStringLiteral("finalfilter"))) {
4572         if (filterInfo.contains(QStringLiteral("storedata"))) {
4573             // Store returned data as clip extra data
4574             std::shared_ptr<ProjectClip> clip = getBinClip(id);
4575             if (clip) {
4576                 QString key = filterInfo.value(QStringLiteral("key"));
4577                 QStringList newValue = clip->updatedAnalysisData(key, results.value(key), filterInfo.value(QStringLiteral("offset")).toInt());
4578                 slotAddClipExtraData(id, newValue.at(0), newValue.at(1));
4579             }
4580         }
4581         if (startPos == -1) {
4582             // Processing bin clip
4583             std::shared_ptr<ProjectClip> currentItem = m_itemModel->getClipByBinID(id);
4584             if (!currentItem) {
4585                 return;
4586             }
4587             std::shared_ptr<ClipController> ctl = std::static_pointer_cast<ClipController>(currentItem);
4588             EffectsList list = ctl->effectList();
4589             QDomElement effect = list.effectById(filterInfo.value(QStringLiteral("finalfilter")));
4590             QDomDocument doc;
4591             QDomElement e = doc.createElement(QStringLiteral("test"));
4592             doc.appendChild(e);
4593             e.appendChild(doc.importNode(effect, true));
4594             if (!effect.isNull()) {
4595                 QDomElement newEffect = effect.cloneNode().toElement();
4596                 QMap<QString, QString>::const_iterator i = results.constBegin();
4597                 while (i != results.constEnd()) {
4598                     EffectsList::setParameter(newEffect, i.key(), i.value());
4599                     ++i;
4600                 }
4601                 ctl->updateEffect(newEffect, effect.attribute(QStringLiteral("kdenlive_ix")).toInt());
4602                 Q_EMIT requestClipShow(currentItem);
4603                 // TODO use undo / redo for bin clip edit effect
4604                 //
4605                 EditEffectCommand *command = new EditEffectCommand(this, clip->track(), clip->startPos(), effect, newEffect, clip->selectedEffectIndex(),
4606                 true, true);
4607                 m_commandStack->push(command);
4608                 Q_EMIT clipItemSelected(clip);
4609             }
4610 
4611             // Q_EMIT gotFilterJobResults(id, startPos, track, results, filterInfo);
4612             return;
4613         }
4614         // This is a timeline filter, forward results
4615         Q_EMIT gotFilterJobResults(id, startPos, track, results, filterInfo);
4616         return;
4617     }
4618     // Currently, only the first value of results is used
4619     std::shared_ptr<ProjectClip> clip = getBinClip(id);
4620     if (!clip) {
4621         return;
4622     }
4623     // Check for return value
4624     int markersType = -1;
4625     if (filterInfo.contains(QStringLiteral("addmarkers"))) {
4626         markersType = filterInfo.value(QStringLiteral("addmarkers")).toInt();
4627     }
4628     if (results.isEmpty()) {
4629         Q_EMIT displayBinMessage(i18n("No data returned from clip analysis"), KMessageWidget::Warning);
4630         return;
4631     }
4632     bool dataProcessed = false;
4633     QString label = filterInfo.value(QStringLiteral("label"));
4634     QString key = filterInfo.value(QStringLiteral("key"));
4635     int offset = filterInfo.value(QStringLiteral("offset")).toInt();
4636     QStringList value = results.value(key).split(QLatin1Char(';'), Qt::SkipEmptyParts);
4637     // qCDebug(KDENLIVE_LOG)<<"// RESULT; "<<key<<" = "<<value;
4638     if (filterInfo.contains(QStringLiteral("resultmessage"))) {
4639         QString mess = filterInfo.value(QStringLiteral("resultmessage"));
4640         mess.replace(QLatin1String("%count"), QString::number(value.count()));
4641         Q_EMIT displayBinMessage(mess, KMessageWidget::Information);
4642     } else {
4643         Q_EMIT displayBinMessage(i18n("Processing data analysis"), KMessageWidget::Information);
4644     }
4645     if (filterInfo.contains(QStringLiteral("cutscenes"))) {
4646         // Check if we want to cut scenes from returned data
4647         dataProcessed = true;
4648         int cutPos = 0;
4649         auto *command = new QUndoCommand();
4650         command->setText(i18n("Auto Split Clip"));
4651         for (const QString &pos : value) {
4652             if (!pos.contains(QLatin1Char('='))) {
4653                 continue;
4654             }
4655             int newPos = pos.section(QLatin1Char('='), 0, 0).toInt();
4656             // Don't use scenes shorter than 1 second
4657             if (newPos - cutPos < 24) {
4658                 continue;
4659             }
4660             new AddBinClipCutCommand(this, id, cutPos + offset, newPos + offset, true, command);
4661             cutPos = newPos;
4662         }
4663         if (command->childCount() == 0) {
4664             delete command;
4665         } else {
4666             m_doc->commandStack()->push(command);
4667         }
4668     }
4669     if (markersType >= 0) {
4670         // Add markers from returned data
4671         dataProcessed = true;
4672         int cutPos = 0;
4673         int index = 1;
4674         bool simpleList = false;
4675         double sourceFps = clip->getOriginalFps();
4676         if (qFuzzyIsNull(sourceFps)) {
4677             sourceFps = pCore->getCurrentFps();
4678         }
4679         if (filterInfo.contains(QStringLiteral("simplelist"))) {
4680             // simple list
4681             simpleList = true;
4682         }
4683         for (const QString &pos : value) {
4684             if (simpleList) {
4685                 clip->getMarkerModel()->addMarker(GenTime((int)(pos.toInt() * pCore->getCurrentFps() / sourceFps), pCore->getCurrentFps()), label + pos,
4686                                                   markersType);
4687                 index++;
4688                 continue;
4689             }
4690             if (!pos.contains(QLatin1Char('='))) {
4691                 continue;
4692             }
4693             int newPos = pos.section(QLatin1Char('='), 0, 0).toInt();
4694             // Don't use scenes shorter than 1 second
4695             if (newPos - cutPos < 24) {
4696                 continue;
4697             }
4698             clip->getMarkerModel()->addMarker(GenTime(newPos + offset, pCore->getCurrentFps()), label + QString::number(index), markersType);
4699             index++;
4700             cutPos = newPos;
4701         }
4702     }
4703     if (!dataProcessed || filterInfo.contains(QStringLiteral("storedata"))) {
4704         // Store returned data as clip extra data
4705         QStringList newValue = clip->updatedAnalysisData(key, results.value(key), offset);
4706         slotAddClipExtraData(id, newValue.at(0), newValue.at(1));
4707     }
4708 }
4709 */
4710 
4711 // TODO: move title editing into a better place...
4712 void Bin::showTitleWidget(const std::shared_ptr<ProjectClip> &clip)
4713 {
4714     QString path = clip->getProducerProperty(QStringLiteral("resource"));
4715     QDir titleFolder(m_doc->projectDataFolder() + QStringLiteral("/titles"));
4716     titleFolder.mkpath(QStringLiteral("."));
4717     QList<int> clips = clip->timelineInstances();
4718     // Temporarily hide this title clip in timeline so that it does not appear when requesting background frame
4719     pCore->temporaryUnplug(clips, true);
4720     TitleWidget dia_ui(QUrl(), titleFolder.absolutePath(), pCore->monitorManager()->projectMonitor(), pCore->window());
4721     QDomDocument doc;
4722     QString xmldata = clip->getProducerProperty(QStringLiteral("xmldata"));
4723     if (xmldata.isEmpty() && QFile::exists(path)) {
4724         if (!Xml::docContentFromFile(doc, path, false)) {
4725             return;
4726         }
4727     } else {
4728         doc.setContent(xmldata);
4729     }
4730     dia_ui.setXml(doc, clip->clipId());
4731     int res = dia_ui.exec();
4732     if (res == QDialog::Accepted) {
4733         pCore->temporaryUnplug(clips, false);
4734         QMap<QString, QString> newprops;
4735         newprops.insert(QStringLiteral("xmldata"), dia_ui.xml().toString());
4736         if (dia_ui.duration() != clip->duration().frames(pCore->getCurrentFps())) {
4737             // duration changed, we need to update duration
4738             newprops.insert(QStringLiteral("out"), clip->framesToTime(dia_ui.duration() - 1));
4739             int currentLength = clip->getProducerDuration();
4740             if (currentLength != dia_ui.duration()) {
4741                 newprops.insert(QStringLiteral("kdenlive:duration"), clip->framesToTime(dia_ui.duration()));
4742             }
4743         }
4744         if (clip->clipName().contains(i18n("(copy)"))) {
4745             // We edited a duplicated title clip, update name from new content text
4746             newprops.insert(QStringLiteral("kdenlive:clipname"), dia_ui.titleSuggest());
4747             if (!path.isEmpty()) {
4748                 newprops.insert(QStringLiteral("resource"), QString());
4749             }
4750         }
4751         if (!path.isEmpty()) {
4752             // we are editing an external file, asked if we want to detach from that file or save the result to that title file.
4753             if (KMessageBox::questionTwoActions(pCore->window(),
4754                                                 i18n("You are editing an external title clip (%1). Do you want to save your changes to the title "
4755                                                      "file or save the changes for this project only?",
4756                                                      path),
4757                                                 i18n("Save Title"), KGuiItem(i18n("Save to title file")),
4758                                                 KGuiItem(i18n("Save in project only"))) == KMessageBox::PrimaryAction) {
4759                 // save to external file
4760                 dia_ui.saveTitle(QUrl::fromLocalFile(path));
4761                 return;
4762             } else {
4763                 newprops.insert(QStringLiteral("resource"), QString());
4764             }
4765         }
4766         // trigger producer reload
4767         newprops.insert(QStringLiteral("force_reload"), QStringLiteral("1"));
4768         slotEditClipCommand(clip->AbstractProjectItem::clipId(), clip->currentProperties(newprops), newprops);
4769         // when edit is triggered from the timeline, project monitor refresh is necessary after an edit is made
4770         pCore->refreshProjectMonitorOnce();
4771     } else {
4772         pCore->temporaryUnplug(clips, false);
4773         if (res == QDialog::Accepted + 1) {
4774             // Ready, create clip xml
4775             std::unordered_map<QString, QString> properties;
4776             properties[QStringLiteral("xmldata")] = dia_ui.xml().toString();
4777             QString titleSuggestion = dia_ui.titleSuggest();
4778             ClipCreator::createTitleClip(properties, dia_ui.duration(), titleSuggestion.isEmpty() ? i18n("Title clip") : titleSuggestion,
4779                                          clip->parent()->clipId(), m_itemModel);
4780         }
4781     }
4782 }
4783 
4784 void Bin::slotResetInfoMessage()
4785 {
4786     m_errorLog.clear();
4787     m_currentMessage = BinMessage::BinCategory::NoMessage;
4788     // We cannot delete actions here because of concurrency, it might delete actions meant for the upcoming message
4789     /*QList<QAction *> actions = m_infoMessage->actions();
4790     for (int i = 0; i < actions.count(); ++i) {
4791         m_infoMessage->removeAction(actions.at(i));
4792     }*/
4793 }
4794 
4795 void Bin::slotSetSorting()
4796 {
4797     if (m_listType == BinIconView) {
4798         m_proxyModel->setFilterKeyColumn(0);
4799         return;
4800     }
4801     auto *view = qobject_cast<QTreeView *>(m_itemView);
4802     if (view) {
4803         int ix = view->header()->sortIndicatorSection();
4804         m_proxyModel->setFilterKeyColumn(ix);
4805     }
4806 }
4807 
4808 void Bin::slotShowColumn(bool show)
4809 {
4810     auto *act = qobject_cast<QAction *>(sender());
4811     if (act == nullptr) {
4812         return;
4813     }
4814     auto *view = qobject_cast<QTreeView *>(m_itemView);
4815     if (view) {
4816         view->setColumnHidden(act->data().toInt(), !show);
4817     }
4818 }
4819 
4820 void Bin::slotQueryRemoval(const QString &id, const QString &url, const QString &errorMessage)
4821 {
4822     if (m_invalidClipDialog) {
4823         if (!url.isEmpty()) {
4824             m_invalidClipDialog->addClip(id, url);
4825         }
4826         return;
4827     }
4828     QString message = i18n("Clip is invalid, will be removed from project.");
4829     if (!errorMessage.isEmpty()) {
4830         message.append("\n" + errorMessage);
4831     }
4832     m_invalidClipDialog = new InvalidDialog(i18n("Invalid clip"), message, true, this);
4833     m_invalidClipDialog->addClip(id, url);
4834     int result = m_invalidClipDialog->exec();
4835     if (result == QDialog::Accepted) {
4836         const QStringList ids = m_invalidClipDialog->getIds();
4837         Fun undo = []() { return true; };
4838         Fun redo = []() { return true; };
4839         for (const QString &i : ids) {
4840             auto item = m_itemModel->getClipByBinID(i);
4841             m_itemModel->requestBinClipDeletion(item, undo, redo);
4842         }
4843     }
4844     delete m_invalidClipDialog;
4845     m_invalidClipDialog = nullptr;
4846 }
4847 
4848 void Bin::slotRefreshClipThumbnail(const QString &id)
4849 {
4850     std::shared_ptr<ProjectClip> clip = m_itemModel->getClipByBinID(id);
4851     if (!clip) {
4852         return;
4853     }
4854     ClipLoadTask::start(ObjectId(KdenliveObjectType::BinClip, id.toInt(), QUuid()), QDomElement(), true, -1, -1, this);
4855 }
4856 
4857 void Bin::slotAddClipExtraData(const QString &id, const QString &key, const QString &clipData)
4858 {
4859     std::shared_ptr<ProjectClip> clip = m_itemModel->getClipByBinID(id);
4860     if (!clip) {
4861         return;
4862     }
4863     QString oldValue = clip->getProducerProperty(key);
4864     QMap<QString, QString> oldProps;
4865     oldProps.insert(key, oldValue);
4866     QMap<QString, QString> newProps;
4867     newProps.insert(key, clipData);
4868     auto *command = new EditClipCommand(this, id, oldProps, newProps, true);
4869     m_doc->commandStack()->push(command);
4870 }
4871 
4872 void Bin::slotUpdateClipProperties(const QString &id, const QMap<QString, QString> &properties, bool refreshPropertiesPanel)
4873 {
4874     std::shared_ptr<AbstractProjectItem> item = m_itemModel->getItemByBinId(id);
4875     if (!item) {
4876         // Clip might have been deleted
4877         return;
4878     }
4879     if (item->itemType() == AbstractProjectItem::ClipItem) {
4880         std::shared_ptr<ProjectClip> clip = std::static_pointer_cast<ProjectClip>(item);
4881         if (clip) {
4882             clip->setProperties(properties, refreshPropertiesPanel);
4883         }
4884     } else if (item->itemType() == AbstractProjectItem::SubClipItem) {
4885         std::shared_ptr<ProjectSubClip> clip = std::static_pointer_cast<ProjectSubClip>(item);
4886         if (clip) {
4887             clip->setProperties(properties);
4888         }
4889     }
4890 }
4891 
4892 void Bin::showSlideshowWidget(const std::shared_ptr<ProjectClip> &clip)
4893 {
4894     QString folder = QFileInfo(clip->url()).absolutePath();
4895     qCDebug(KDENLIVE_LOG) << " ** * CLIP ABS PATH: " << clip->url() << " = " << folder;
4896     SlideshowClip *dia = new SlideshowClip(m_doc->timecode(), folder, clip.get(), this);
4897     if (dia->exec() == QDialog::Accepted) {
4898         // edit clip properties
4899         QMap<QString, QString> properties;
4900         properties.insert(QStringLiteral("out"), clip->framesToTime(m_doc->getFramePos(dia->clipDuration()) * dia->imageCount() - 1));
4901         properties.insert(QStringLiteral("kdenlive:duration"), clip->framesToTime(m_doc->getFramePos(dia->clipDuration()) * dia->imageCount()));
4902         properties.insert(QStringLiteral("kdenlive:clipname"), dia->clipName());
4903         properties.insert(QStringLiteral("ttl"), QString::number(m_doc->getFramePos(dia->clipDuration())));
4904         properties.insert(QStringLiteral("loop"), QString::number(static_cast<int>(dia->loop())));
4905         properties.insert(QStringLiteral("crop"), QString::number(static_cast<int>(dia->crop())));
4906         properties.insert(QStringLiteral("fade"), QString::number(static_cast<int>(dia->fade())));
4907         properties.insert(QStringLiteral("luma_duration"), QString::number(m_doc->getFramePos(dia->lumaDuration())));
4908         properties.insert(QStringLiteral("luma_file"), dia->lumaFile());
4909         properties.insert(QStringLiteral("softness"), QString::number(dia->softness()));
4910         properties.insert(QStringLiteral("animation"), dia->animation());
4911         properties.insert(QStringLiteral("low-pass"), QString::number(dia->lowPass()));
4912 
4913         QMap<QString, QString> oldProperties;
4914         oldProperties.insert(QStringLiteral("out"), clip->getProducerProperty(QStringLiteral("out")));
4915         oldProperties.insert(QStringLiteral("kdenlive:duration"), clip->getProducerProperty(QStringLiteral("kdenlive:duration")));
4916         oldProperties.insert(QStringLiteral("kdenlive:clipname"), clip->name());
4917         oldProperties.insert(QStringLiteral("ttl"), clip->getProducerProperty(QStringLiteral("ttl")));
4918         oldProperties.insert(QStringLiteral("loop"), clip->getProducerProperty(QStringLiteral("loop")));
4919         oldProperties.insert(QStringLiteral("crop"), clip->getProducerProperty(QStringLiteral("crop")));
4920         oldProperties.insert(QStringLiteral("fade"), clip->getProducerProperty(QStringLiteral("fade")));
4921         oldProperties.insert(QStringLiteral("luma_duration"), clip->getProducerProperty(QStringLiteral("luma_duration")));
4922         oldProperties.insert(QStringLiteral("luma_file"), clip->getProducerProperty(QStringLiteral("luma_file")));
4923         oldProperties.insert(QStringLiteral("softness"), clip->getProducerProperty(QStringLiteral("softness")));
4924         oldProperties.insert(QStringLiteral("animation"), clip->getProducerProperty(QStringLiteral("animation")));
4925         oldProperties.insert(QStringLiteral("low-pass"), clip->getProducerProperty(QStringLiteral("low-pass")));
4926 
4927         slotEditClipCommand(clip->AbstractProjectItem::clipId(), oldProperties, properties);
4928     }
4929     delete dia;
4930 }
4931 
4932 void Bin::setBinEffectsEnabled(bool enabled, bool refreshMonitor)
4933 {
4934     m_itemModel->setBinEffectsEnabled(enabled);
4935     pCore->projectManager()->disableBinEffects(!enabled, refreshMonitor);
4936 }
4937 
4938 void Bin::slotRenameItem()
4939 {
4940     const QModelIndexList indexes = m_proxyModel->selectionModel()->selection().indexes();
4941     for (const QModelIndex &ix : indexes) {
4942         if (!ix.isValid() || ix.column() != 0) {
4943             continue;
4944         }
4945         m_itemView->setCurrentIndex(ix);
4946         m_itemView->edit(ix);
4947         return;
4948     }
4949 }
4950 
4951 void Bin::refreshProxySettings()
4952 {
4953     QList<std::shared_ptr<ProjectClip>> clipList = m_itemModel->getRootFolder()->childClips();
4954     auto *masterCommand = new QUndoCommand();
4955     masterCommand->setText(m_doc->useProxy() ? i18n("Enable proxies") : i18n("Disable proxies"));
4956     // en/disable proxy option in clip properties
4957     if (m_propertiesPanel) {
4958         for (QWidget *w : m_propertiesPanel->findChildren<ClipPropertiesController *>()) {
4959             Q_EMIT static_cast<ClipPropertiesController *>(w)->enableProxy(m_doc->useProxy());
4960         }
4961     }
4962     bool isFolder = false;
4963     const QModelIndexList indexes = m_proxyModel->selectionModel()->selectedIndexes();
4964     for (const QModelIndex &ix : indexes) {
4965         if (!ix.isValid() || ix.column() != 0) {
4966             continue;
4967         }
4968         std::shared_ptr<AbstractProjectItem> currentItem = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(ix));
4969         if (currentItem) {
4970             AbstractProjectItem::PROJECTITEMTYPE itemType = currentItem->itemType();
4971             isFolder = itemType == AbstractProjectItem::FolderItem;
4972         }
4973         break;
4974     }
4975     m_proxyAction->setEnabled(m_doc->useProxy() && !isFolder);
4976     if (!m_doc->useProxy()) {
4977         // Disable all proxies
4978         m_doc->slotProxyCurrentItem(false, clipList, false, masterCommand);
4979     } else if (m_doc->autoGenerateProxy(-1) || m_doc->autoGenerateImageProxy(-1)) {
4980         QList<std::shared_ptr<ProjectClip>> toProxy;
4981         for (const std::shared_ptr<ProjectClip> &clp : qAsConst(clipList)) {
4982             ClipType::ProducerType t = clp->clipType();
4983             if (t == ClipType::Playlist && m_doc->autoGenerateProxy(pCore->getCurrentFrameDisplaySize().width())) {
4984                 toProxy << clp;
4985                 continue;
4986             } else if ((t == ClipType::AV || t == ClipType::Video) &&
4987                        m_doc->autoGenerateProxy(clp->getProducerIntProperty(QStringLiteral("meta.media.width")))) {
4988                 // Start proxy
4989                 toProxy << clp;
4990                 continue;
4991             } else if (t == ClipType::Image && m_doc->autoGenerateImageProxy(clp->getProducerIntProperty(QStringLiteral("meta.media.width")))) {
4992                 // Start proxy
4993                 toProxy << clp;
4994                 continue;
4995             }
4996         }
4997         if (!toProxy.isEmpty()) {
4998             m_doc->slotProxyCurrentItem(true, toProxy, false, masterCommand);
4999         }
5000     }
5001     if (masterCommand->childCount() > 0) {
5002         m_doc->commandStack()->push(masterCommand);
5003     } else {
5004         delete masterCommand;
5005     }
5006 }
5007 
5008 QStringList Bin::getProxyHashList()
5009 {
5010     QStringList list;
5011     QList<std::shared_ptr<ProjectClip>> clipList = m_itemModel->getRootFolder()->childClips();
5012     for (const std::shared_ptr<ProjectClip> &clp : qAsConst(clipList)) {
5013         if (clp->clipType() == ClipType::AV || clp->clipType() == ClipType::Video || clp->clipType() == ClipType::Playlist) {
5014             list << clp->hash();
5015         }
5016     }
5017     return list;
5018 }
5019 
5020 bool Bin::hasUserClip() const
5021 {
5022     QList<std::shared_ptr<ProjectClip>> allClips = m_itemModel->getRootFolder()->childClips();
5023     for (auto &c : allClips) {
5024         if (c->clipType() != ClipType::Timeline) {
5025             return true;
5026         }
5027     }
5028     return false;
5029 }
5030 
5031 void Bin::reloadAllProducers(bool reloadThumbs)
5032 {
5033     if (m_itemModel->getRootFolder() == nullptr || m_itemModel->getRootFolder()->childCount() == 0 || !isEnabled()) {
5034         return;
5035     }
5036     QList<std::shared_ptr<ProjectClip>> clipList = m_itemModel->getRootFolder()->childClips();
5037     Q_EMIT openClip(std::shared_ptr<ProjectClip>());
5038     if (clipList.count() == 1) {
5039         // We only have one clip in the project, so this was called on a reset profile event.
5040         // Check if the clip is included in timeline to update it afterwards
5041         clipList.first()->updateTimelineOnReload();
5042     }
5043     for (const std::shared_ptr<ProjectClip> &clip : qAsConst(clipList)) {
5044         ClipType::ProducerType type = clip->clipType();
5045         if (type == ClipType::Timeline) {
5046             continue;
5047         }
5048         QDomDocument doc;
5049         QDomElement xml = clip->toXml(doc, false, false);
5050         // Make sure we reload clip length
5051         if (type == ClipType::AV || type == ClipType::Video || type == ClipType::Audio || type == ClipType::Playlist) {
5052             xml.removeAttribute(QStringLiteral("out"));
5053             Xml::removeXmlProperty(xml, QStringLiteral("length"));
5054             Xml::removeXmlProperty(xml, QStringLiteral("kdenlive:duration"));
5055         }
5056         if (clip->isValid()) {
5057             clip->resetProducerProperty(QStringLiteral("kdenlive:duration"));
5058             if (clip->hasLimitedDuration()) {
5059                 clip->resetProducerProperty(QStringLiteral("length"));
5060             }
5061         }
5062         if (!xml.isNull()) {
5063             clip->discardAudioThumb();
5064             if (reloadThumbs) {
5065                 ThumbnailCache::get()->invalidateThumbsForClip(clip->clipId());
5066             }
5067             clip->setClipStatus(FileStatus::StatusWaiting);
5068             ObjectId oid(KdenliveObjectType::BinClip, clip->clipId().toInt(), QUuid());
5069             pCore->taskManager.discardJobs(oid, AbstractTask::NOJOBTYPE, true,
5070                                            {AbstractTask::TRANSCODEJOB, AbstractTask::PROXYJOB, AbstractTask::AUDIOTHUMBJOB});
5071             ClipLoadTask::start(oid, xml, false, -1, -1, this);
5072         }
5073     }
5074 }
5075 
5076 void Bin::checkAudioThumbs()
5077 {
5078     if (!KdenliveSettings::audiothumbnails() || m_itemModel->getRootFolder() == nullptr || m_itemModel->getRootFolder()->childCount() == 0 || !isEnabled()) {
5079         return;
5080     }
5081     QList<std::shared_ptr<ProjectClip>> clipList = m_itemModel->getRootFolder()->childClips();
5082     for (const auto &clip : qAsConst(clipList)) {
5083         ClipType::ProducerType type = clip->clipType();
5084         if (type == ClipType::AV || type == ClipType::Audio || type == ClipType::Playlist || type == ClipType::Unknown) {
5085             AudioLevelsTask::start(ObjectId(KdenliveObjectType::BinClip, clip->clipId().toInt(), QUuid()), clip.get(), false);
5086         }
5087     }
5088 }
5089 
5090 void Bin::slotMessageActionTriggered()
5091 {
5092     m_infoMessage->animatedHide();
5093 }
5094 
5095 void Bin::getBinStats(uint *used, uint *unused, qint64 *usedSize, qint64 *unusedSize)
5096 {
5097     QList<std::shared_ptr<ProjectClip>> clipList = m_itemModel->getRootFolder()->childClips();
5098     for (const std::shared_ptr<ProjectClip> &clip : qAsConst(clipList)) {
5099         // Don't count sequence clips here
5100         if (clip->clipType() == ClipType::Timeline) {
5101             continue;
5102         }
5103         if (clip->refCount() == 0) {
5104             *unused += 1;
5105             *unusedSize += clip->getProducerInt64Property(QStringLiteral("kdenlive:file_size"));
5106         } else {
5107             *used += 1;
5108             *usedSize += clip->getProducerInt64Property(QStringLiteral("kdenlive:file_size"));
5109         }
5110     }
5111 }
5112 
5113 void Bin::rebuildProxies()
5114 {
5115     QList<std::shared_ptr<ProjectClip>> clipList = m_itemModel->getRootFolder()->childClips();
5116     QList<std::shared_ptr<ProjectClip>> toProxy;
5117     for (const std::shared_ptr<ProjectClip> &clp : qAsConst(clipList)) {
5118         if (clp->hasProxy()) {
5119             toProxy << clp;
5120             // Abort all pending jobs
5121             pCore->taskManager.discardJobs(ObjectId(KdenliveObjectType::BinClip, clp->clipId().toInt(), QUuid()), AbstractTask::PROXYJOB);
5122             clp->deleteProxy(false);
5123         }
5124     }
5125     if (toProxy.isEmpty()) {
5126         return;
5127     }
5128     auto *masterCommand = new QUndoCommand();
5129     masterCommand->setText(i18n("Rebuild proxies"));
5130     m_doc->slotProxyCurrentItem(true, toProxy, true, masterCommand);
5131     if (masterCommand->childCount() > 0) {
5132         m_doc->commandStack()->push(masterCommand);
5133     } else {
5134         delete masterCommand;
5135     }
5136 }
5137 
5138 void Bin::showClearButton(bool show)
5139 {
5140     m_searchLine->setClearButtonEnabled(show);
5141 }
5142 
5143 void Bin::saveZone(const QStringList &info, const QDir &dir)
5144 {
5145     if (info.size() != 3) {
5146         return;
5147     }
5148     std::shared_ptr<ProjectClip> clip = getBinClip(info.constFirst());
5149     if (clip) {
5150         QPoint zone(info.at(1).toInt(), info.at(2).toInt());
5151         clip->saveZone(zone, dir);
5152     }
5153 }
5154 
5155 void Bin::setCurrent(const std::shared_ptr<AbstractProjectItem> &item)
5156 {
5157     switch (item->itemType()) {
5158     case AbstractProjectItem::ClipItem: {
5159         std::shared_ptr<ProjectClip> clp = std::static_pointer_cast<ProjectClip>(item);
5160         if (clp && clp->statusReady()) {
5161             openProducer(clp);
5162             Q_EMIT requestShowEffectStack(clp->clipName(), clp->m_effectStack, clp->getFrameSize(), false);
5163         }
5164         break;
5165     }
5166     case AbstractProjectItem::SubClipItem: {
5167         auto subClip = std::static_pointer_cast<ProjectSubClip>(item);
5168         QPoint zone = subClip->zone();
5169         std::shared_ptr<ProjectClip> master = subClip->getMasterClip();
5170         if (master && master->statusReady()) {
5171             openProducer(master, zone.x(), zone.y() + 1);
5172         }
5173         break;
5174     }
5175     case AbstractProjectItem::FolderItem:
5176         openProducer(nullptr);
5177     }
5178 }
5179 
5180 void Bin::cleanupUnused()
5181 {
5182     m_itemModel->requestCleanupUnused();
5183 }
5184 
5185 size_t Bin::getClipDuration(int itemId) const
5186 {
5187     std::shared_ptr<ProjectClip> clip = m_itemModel->getClipByBinID(QString::number(itemId));
5188     Q_ASSERT(clip != nullptr);
5189     return clip->frameDuration();
5190 }
5191 
5192 QSize Bin::getFrameSize(int itemId) const
5193 {
5194     std::shared_ptr<ProjectClip> clip = m_itemModel->getClipByBinID(QString::number(itemId));
5195     Q_ASSERT(clip != nullptr);
5196     return clip->frameSize();
5197 }
5198 
5199 PlaylistState::ClipState Bin::getClipState(int itemId) const
5200 {
5201     std::shared_ptr<ProjectClip> clip = m_itemModel->getClipByBinID(QString::number(itemId));
5202     Q_ASSERT(clip != nullptr);
5203     bool audio = clip->hasAudio();
5204     bool video = clip->hasVideo();
5205     return audio ? (video ? PlaylistState::Disabled : PlaylistState::AudioOnly) : PlaylistState::VideoOnly;
5206 }
5207 
5208 QString Bin::getCurrentFolder()
5209 {
5210     // Check parent item
5211     QModelIndex ix = m_proxyModel->selectionModel()->currentIndex();
5212     std::shared_ptr<ProjectFolder> parentFolder = m_itemModel->getRootFolder();
5213     if (ix.isValid() && m_proxyModel->selectionModel()->isSelected(ix)) {
5214         std::shared_ptr<AbstractProjectItem> currentItem = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(ix));
5215         parentFolder = std::static_pointer_cast<ProjectFolder>(currentItem->getEnclosingFolder());
5216     } else {
5217         QModelIndex parentIx = m_itemView->rootIndex();
5218         if (parentIx.isValid()) {
5219             std::shared_ptr<AbstractProjectItem> item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(parentIx));
5220             if (item && item != parentFolder) {
5221                 parentFolder = std::static_pointer_cast<ProjectFolder>(item->getEnclosingFolder());
5222             }
5223         }
5224     }
5225     return parentFolder->clipId();
5226 }
5227 
5228 void Bin::adjustProjectProfileToItem()
5229 {
5230     const QString clipId = m_monitor->activeClipId();
5231     slotCheckProfile(clipId);
5232 }
5233 
5234 void Bin::slotCheckProfile(const QString &binId)
5235 {
5236     std::shared_ptr<ProjectClip> clip = m_itemModel->getClipByBinID(binId);
5237     if (clip && clip->statusReady()) {
5238         checkProfile(clip->originalProducer());
5239     }
5240 }
5241 
5242 // static
5243 void Bin::checkProfile(const std::shared_ptr<Mlt::Producer> &producer)
5244 {
5245     // Check if clip profile matches
5246     QString service = producer->get("mlt_service");
5247     // Check for image producer
5248     if (service == QLatin1String("qimage") || service == QLatin1String("pixbuf")) {
5249         // This is an image, create profile from image size
5250         int width = producer->get_int("meta.media.width");
5251         if (width % 2 > 0) {
5252             width += width % 2;
5253         }
5254         int height = producer->get_int("meta.media.height");
5255         height += height % 2;
5256         if (width > 100 && height > 100 && pCore->getCurrentFrameSize() != QSize(width, height)) {
5257             std::unique_ptr<ProfileParam> projectProfile(new ProfileParam(pCore->getCurrentProfile().get()));
5258             projectProfile->m_width = width;
5259             projectProfile->m_height = height;
5260             projectProfile->m_sample_aspect_num = 1;
5261             projectProfile->m_sample_aspect_den = 1;
5262             projectProfile->m_display_aspect_num = width;
5263             projectProfile->m_display_aspect_den = height;
5264             projectProfile->m_description.clear();
5265             QMetaObject::invokeMethod(pCore->currentDoc(), "switchProfile", Q_ARG(ProfileParam *, new ProfileParam(projectProfile.get())),
5266                                       Q_ARG(QString, QFileInfo(producer->get("resource")).fileName()));
5267         } else {
5268             // Very small image, we probably don't want to use this as profile
5269         }
5270     } else if (service.contains(QStringLiteral("avformat"))) {
5271         std::unique_ptr<Mlt::Profile> blankProfile(new Mlt::Profile());
5272         blankProfile->set_explicit(0);
5273         blankProfile->from_producer(*producer);
5274         std::unique_ptr<ProfileParam> clipProfile(new ProfileParam(blankProfile.get()));
5275         std::unique_ptr<ProfileParam> projectProfile(new ProfileParam(pCore->getCurrentProfile().get()));
5276         clipProfile->adjustDimensions();
5277         if (*clipProfile.get() == *projectProfile.get()) {
5278             if (KdenliveSettings::default_profile().isEmpty()) {
5279                 // Confirm default project format
5280                 KdenliveSettings::setDefault_profile(pCore->getCurrentProfile()->path());
5281             }
5282         } else {
5283             // Profiles do not match, propose profile adjustment
5284             QMetaObject::invokeMethod(pCore->currentDoc(), "switchProfile", Q_ARG(ProfileParam *, new ProfileParam(clipProfile.get())),
5285                                       Q_ARG(QString, QFileInfo(producer->get("resource")).fileName()));
5286         }
5287     }
5288 }
5289 
5290 void Bin::showBinFrame(const QModelIndex &ix, int frame, bool storeFrame)
5291 {
5292     std::shared_ptr<AbstractProjectItem> item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(ix));
5293     if (item) {
5294         ClipType::ProducerType type = item->clipType();
5295         if (type != ClipType::AV && type != ClipType::Video && type != ClipType::Playlist && type != ClipType::SlideShow) {
5296             return;
5297         }
5298         if (item->itemType() == AbstractProjectItem::ClipItem) {
5299             auto clip = std::static_pointer_cast<ProjectClip>(item);
5300             if (clip && (clip->clipType() == ClipType::AV || clip->clipType() == ClipType::Video || clip->clipType() == ClipType::Playlist)) {
5301                 clip->getThumbFromPercent(frame, storeFrame);
5302             }
5303         } else if (item->itemType() == AbstractProjectItem::SubClipItem) {
5304             auto clip = std::static_pointer_cast<ProjectSubClip>(item);
5305             if (clip && (clip->clipType() == ClipType::AV || clip->clipType() == ClipType::Video || clip->clipType() == ClipType::Playlist)) {
5306                 clip->getThumbFromPercent(frame);
5307             }
5308         }
5309     }
5310 }
5311 
5312 void Bin::invalidateClip(const QString &binId)
5313 {
5314     std::shared_ptr<ProjectClip> clip = getBinClip(binId);
5315     if (clip && clip->clipType() != ClipType::Audio) {
5316         QMap<QUuid, QList<int>> allIds = clip->getAllTimelineInstances();
5317         QMapIterator<QUuid, QList<int>> i(allIds);
5318         while (i.hasNext()) {
5319             i.next();
5320             QList<int> values = i.value();
5321             for (int j : qAsConst(values)) {
5322                 pCore->invalidateItem(ObjectId(KdenliveObjectType::TimelineClip, j, i.key()));
5323             }
5324         }
5325     }
5326 }
5327 
5328 QSize Bin::sizeHint() const
5329 {
5330     return QSize(350, pCore->window()->height() / 2);
5331 }
5332 
5333 void Bin::slotBack()
5334 {
5335     QModelIndex currentRootIx = m_itemView->rootIndex();
5336     if (!currentRootIx.isValid()) {
5337         return;
5338     }
5339     std::shared_ptr<AbstractProjectItem> item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(currentRootIx));
5340     if (!item) {
5341         qDebug() << "=== ERROR CANNOT FIND ROOT FOR CURRENT VIEW";
5342         return;
5343     }
5344     std::shared_ptr<AbstractProjectItem> parentItem = item->parent();
5345     if (!parentItem) {
5346         qDebug() << "=== ERROR CANNOT FIND PARENT FOR CURRENT VIEW";
5347         return;
5348     }
5349     if (parentItem != m_itemModel->getRootFolder()) {
5350         // We are entering a parent folder
5351         QModelIndex parentId = getIndexForId(parentItem->clipId(), parentItem->itemType() == AbstractProjectItem::FolderItem);
5352         if (parentId.isValid()) {
5353             m_itemView->setRootIndex(m_proxyModel->mapFromSource(parentId));
5354             parentWidget()->setWindowTitle(parentItem->name());
5355         }
5356     } else {
5357         m_itemView->setRootIndex(QModelIndex());
5358         m_upAction->setEnabled(false);
5359         parentWidget()->setWindowTitle(i18nc("@title:window", "Project Bin"));
5360     }
5361 }
5362 
5363 void Bin::checkProjectAudioTracks(QString clipId, int minimumTracksCount)
5364 {
5365     if (m_currentMessage == BinMessage::BinCategory::ProfileMessage) {
5366         // Don't show this message if another one is active
5367         return;
5368     }
5369     int requestedTracks = minimumTracksCount - pCore->projectManager()->avTracksCount().first;
5370     if (requestedTracks > 0) {
5371         if (clipId.isEmpty()) {
5372             clipId = m_monitor->activeClipId();
5373         }
5374         QList<QAction *> list;
5375         QAction *ac = new QAction(QIcon::fromTheme(QStringLiteral("dialog-ok")), i18n("Add Tracks"), this);
5376         connect(ac, &QAction::triggered, [requestedTracks]() { pCore->projectManager()->addAudioTracks(requestedTracks); });
5377         QAction *ac2 = new QAction(QIcon::fromTheme(QStringLiteral("document-edit")), i18n("Edit Streams"), this);
5378         connect(ac2, &QAction::triggered, this, [this, clipId]() {
5379             selectClipById(clipId);
5380             if (m_propertiesPanel) {
5381                 for (QWidget *w : m_propertiesPanel->findChildren<ClipPropertiesController *>()) {
5382                     if (w->parentWidget() && w->parentWidget()->parentWidget()) {
5383                         // Raise panel
5384                         w->parentWidget()->parentWidget()->show();
5385                         w->parentWidget()->parentWidget()->raise();
5386                     }
5387                     // Show audio tab
5388                     static_cast<ClipPropertiesController *>(w)->activatePage(2);
5389                 }
5390             }
5391         });
5392         QAction *ac3 = new QAction(QIcon::fromTheme(QStringLiteral("dialog-ok")), i18n("Don't ask again"), this);
5393         connect(ac3, &QAction::triggered, [&]() { KdenliveSettings::setMultistream_checktrack(false); });
5394         // QAction *ac4 = new QAction(QIcon::fromTheme(QStringLiteral("dialog-cancel")), i18n("Cancel"), this);
5395         list << ac << ac2 << ac3; // << ac4;
5396         doDisplayMessage(i18n("Your project needs more audio tracks to handle all streams. Add %1 audio tracks ?", requestedTracks),
5397                          KMessageWidget::Information, list, true, BinMessage::BinCategory::StreamsMessage);
5398     } else if (m_currentMessage == BinMessage::BinCategory::StreamsMessage) {
5399         // Clip streams number ok for the project, hide message
5400         m_infoMessage->animatedHide();
5401     }
5402 }
5403 
5404 void Bin::addClipMarker(const QString &binId, const QList<int> &positions, const QStringList &comments)
5405 {
5406     std::shared_ptr<ProjectClip> clip = getBinClip(binId);
5407     if (!clip) {
5408         pCore->displayMessage(i18n("Cannot find clip to add marker"), ErrorMessage);
5409         return;
5410     }
5411     QMap<GenTime, QString> markers;
5412     int ix = 0;
5413     for (int pos : positions) {
5414         GenTime p(pos, pCore->getCurrentFps());
5415         if (comments.size() == positions.size()) {
5416             markers.insert(p, comments.at(ix));
5417         } else {
5418             markers.insert(p, pCore->currentDoc()->timecode().getDisplayTimecode(p, false));
5419         }
5420         ix++;
5421     }
5422     clip->getMarkerModel()->addMarkers(markers, KdenliveSettings::default_marker_type());
5423 }
5424 
5425 void Bin::checkMissingProxies()
5426 {
5427     if (m_itemModel->getRootFolder() == nullptr || m_itemModel->getRootFolder()->childCount() == 0) {
5428         return;
5429     }
5430     QList<std::shared_ptr<ProjectClip>> clipList = m_itemModel->getRootFolder()->childClips();
5431     QList<std::shared_ptr<ProjectClip>> toProxy;
5432     for (const auto &clip : qAsConst(clipList)) {
5433         if (clip->getProducerIntProperty(QStringLiteral("_replaceproxy")) > 0) {
5434             clip->resetProducerProperty(QStringLiteral("_replaceproxy"));
5435             toProxy << clip;
5436         }
5437     }
5438     if (!toProxy.isEmpty()) {
5439         pCore->currentDoc()->slotProxyCurrentItem(true, toProxy);
5440     }
5441 }
5442 
5443 void Bin::saveFolderState()
5444 {
5445     // Check folder state (expanded or not)
5446     if (m_itemView == nullptr || m_listType != BinTreeView) {
5447         // Folder state is only valid in tree view mode
5448         return;
5449     }
5450     auto *view = static_cast<QTreeView *>(m_itemView);
5451     QList<std::shared_ptr<ProjectFolder>> folders = m_itemModel->getFolders();
5452     QStringList expandedFolders;
5453     for (const auto &folder : qAsConst(folders)) {
5454         QModelIndex ix = m_itemModel->getIndexFromItem(folder);
5455         if (view->isExpanded(m_proxyModel->mapFromSource(ix))) {
5456             // Save expanded state
5457             expandedFolders << folder->clipId();
5458         }
5459     }
5460     m_itemModel->saveProperty(QStringLiteral("kdenlive:expandedFolders"), expandedFolders.join(QLatin1Char(';')));
5461     m_itemModel->saveProperty(QStringLiteral("kdenlive:binZoom"), QString::number(KdenliveSettings::bin_zoom()));
5462 }
5463 
5464 void Bin::loadBinProperties(const QStringList &foldersToExpand, int zoomLevel)
5465 {
5466     // Check folder state (expanded or not)
5467     if (m_itemView == nullptr || m_listType != BinTreeView) {
5468         // Folder state is only valid in tree view mode
5469         return;
5470     }
5471     auto *view = static_cast<QTreeView *>(m_itemView);
5472     for (const QString &id : foldersToExpand) {
5473         std::shared_ptr<ProjectFolder> folder = m_itemModel->getFolderByBinId(id);
5474         if (folder) {
5475             QModelIndex ix = m_itemModel->getIndexFromItem(folder);
5476             view->setExpanded(m_proxyModel->mapFromSource(ix), true);
5477         }
5478     }
5479     if (zoomLevel > -1) {
5480         m_slider->setValue(zoomLevel);
5481     }
5482 }
5483 
5484 QList<int> Bin::getUsedClipIds()
5485 {
5486     QList<int> timelineClipIds;
5487     QList<std::shared_ptr<ProjectClip>> allClipIds = m_itemModel->getRootFolder()->childClips();
5488     for (const auto &clip : qAsConst(allClipIds)) {
5489         if (clip->isIncludedInTimeline()) {
5490             timelineClipIds.push_back(clip->binId().toInt());
5491         }
5492     }
5493     return timelineClipIds;
5494 }
5495 
5496 void Bin::savePlaylist(const QString &binId, const QString &savePath, const QVector<QPoint> &zones, const QMap<QString, QString> &properties, bool createNew)
5497 {
5498     std::shared_ptr<ProjectClip> clip = m_itemModel->getClipByBinID(binId);
5499     if (!clip) {
5500         pCore->displayMessage(i18n("Could not find master clip"), MessageType::ErrorMessage, 300);
5501         return;
5502     }
5503     Mlt::Tractor t(pCore->getProjectProfile());
5504     std::shared_ptr<Mlt::Producer> prod(new Mlt::Producer(clip->originalProducer().get()));
5505     Mlt::Playlist main(pCore->getProjectProfile());
5506     main.set("id", "main_bin");
5507     main.set("xml_retain", 1);
5508     // Here we could store some kdenlive settings in the main playlist
5509     /*QMapIterator<QString, QString> i(properties);
5510     while (i.hasNext()) {
5511         i.next();
5512         main.set(i.key().toUtf8().constData(), i.value().toUtf8().constData());
5513     }*/
5514     main.append(*prod.get());
5515     t.set("xml_retain main_bin", main.get_service(), 0);
5516     Mlt::Playlist pl(pCore->getProjectProfile());
5517     for (auto &zone : zones) {
5518         std::shared_ptr<Mlt::Producer> cut(prod->cut(zone.x(), zone.y()));
5519         pl.append(*cut.get());
5520     }
5521     t.set_track(pl, 0);
5522     Mlt::Consumer cons(pCore->getProjectProfile(), "xml", savePath.toUtf8().constData());
5523     cons.set("store", "kdenlive");
5524     cons.connect(t);
5525     cons.run();
5526     if (createNew) {
5527         const QString id = slotAddClipToProject(QUrl::fromLocalFile(savePath));
5528         // Set properties directly on the clip
5529         std::shared_ptr<ProjectClip> playlistClip = m_itemModel->getClipByBinID(id);
5530         QMapIterator<QString, QString> i(properties);
5531         while (i.hasNext()) {
5532             i.next();
5533             playlistClip->setProducerProperty(i.key(), i.value());
5534         }
5535         selectClipById(id);
5536     }
5537 }
5538 
5539 void Bin::requestSelectionTranscoding(bool forceReplace)
5540 {
5541     if (m_transcodingDialog == nullptr) {
5542         m_transcodingDialog = new TranscodeSeek(false, true, this);
5543         connect(m_transcodingDialog, &QDialog::accepted, this, [&]() {
5544             bool replace = m_transcodingDialog->replace_original->isChecked();
5545             if (!forceReplace) {
5546                 KdenliveSettings::setTranscodingReplace(replace);
5547             }
5548             QMap<QString, QStringList> ids = m_transcodingDialog->ids();
5549             QMapIterator<QString, QStringList> i(ids);
5550             while (i.hasNext()) {
5551                 i.next();
5552                 std::shared_ptr<ProjectClip> clip = m_itemModel->getClipByBinID(i.key());
5553                 ObjectId oid(KdenliveObjectType::BinClip, i.key().toInt(), QUuid());
5554                 if (clip->clipType() == ClipType::Timeline) {
5555                     // Ensure we use the correct out point
5556                     TranscodeTask::start(oid, i.value().first(), m_transcodingDialog->preParams(), m_transcodingDialog->params(i.value().at(1).toInt()), 0,
5557                                          clip->frameDuration(), replace, clip.get(), false, false);
5558                 } else {
5559                     TranscodeTask::start(oid, i.value().first(), m_transcodingDialog->preParams(), m_transcodingDialog->params(i.value().at(1).toInt()), -1, -1,
5560                                          replace, clip.get(), false, false);
5561                 }
5562             }
5563             m_transcodingDialog->deleteLater();
5564             m_transcodingDialog = nullptr;
5565         });
5566         connect(m_transcodingDialog, &QDialog::rejected, this, [&]() {
5567             m_transcodingDialog->deleteLater();
5568             m_transcodingDialog = nullptr;
5569         });
5570     }
5571     std::vector<QString> ids = selectedClipsIds();
5572     for (const auto &id : ids) {
5573         std::shared_ptr<ProjectClip> clip = m_itemModel->getClipByBinID(id);
5574         if (clip) {
5575             const QString clipService = clip->getProducerProperty(QStringLiteral("mlt_service"));
5576             if (clipService.startsWith(QLatin1String("avformat"))) {
5577                 QString resource = clip->clipUrl();
5578                 ClipType::ProducerType type = clip->clipType();
5579                 int integerFps = qRound(clip->originalFps());
5580                 QString suffix = QString("-%1fps").arg(integerFps);
5581                 m_transcodingDialog->addUrl(resource, id, suffix, type, QString());
5582             } else {
5583                 m_transcodingDialog->addUrl(clip->clipName(), id, QString(), clip->clipType(), QString());
5584             }
5585         }
5586     }
5587     m_transcodingDialog->show();
5588 }
5589 
5590 void Bin::requestTranscoding(const QString &url, const QString &id, int type, bool checkProfile, const QString &suffix, const QString &message)
5591 {
5592     if (m_transcodingDialog == nullptr) {
5593         m_transcodingDialog = new TranscodeSeek(false, false, this);
5594         m_transcodingDialog->replace_original->setVisible(false);
5595         connect(m_transcodingDialog, &QDialog::accepted, this, [&, checkProfile]() {
5596             QMap<QString, QStringList> ids = m_transcodingDialog->ids();
5597             if (!ids.isEmpty()) {
5598                 QString firstId = ids.firstKey();
5599                 QMapIterator<QString, QStringList> i(ids);
5600                 while (i.hasNext()) {
5601                     i.next();
5602                     std::shared_ptr<ProjectClip> clip = m_itemModel->getClipByBinID(i.key());
5603                     TranscodeTask::start(ObjectId(KdenliveObjectType::BinClip, i.key().toInt(), QUuid()), i.value().first(), m_transcodingDialog->preParams(),
5604                                          m_transcodingDialog->params(i.value().at(1).toInt()), -1, -1, true, clip.get(), false,
5605                                          i.key() == firstId ? checkProfile : false);
5606                 }
5607             }
5608             m_transcodingDialog->deleteLater();
5609             m_transcodingDialog = nullptr;
5610         });
5611         connect(m_transcodingDialog, &QDialog::rejected, this, [&, checkProfile]() {
5612             QMap<QString, QStringList> ids = m_transcodingDialog->ids();
5613             QString firstId;
5614             if (!ids.isEmpty()) {
5615                 firstId = ids.firstKey();
5616             }
5617             m_transcodingDialog->deleteLater();
5618             m_transcodingDialog = nullptr;
5619             if (checkProfile && !firstId.isEmpty()) {
5620                 pCore->bin()->slotCheckProfile(firstId);
5621             }
5622         });
5623     }
5624     std::shared_ptr<ProjectClip> clip = m_itemModel->getClipByBinID(id);
5625     if (clip) {
5626         ClipType::ProducerType cType = (ClipType::ProducerType)type;
5627         if (cType == ClipType::Unknown) {
5628             cType = clip->clipType();
5629         }
5630         if (url.isEmpty()) {
5631             QString resource = clip->clipUrl();
5632             m_transcodingDialog->addUrl(resource, id, suffix, cType, message);
5633         } else {
5634             m_transcodingDialog->addUrl(url, id, suffix, cType, message);
5635         }
5636     }
5637     m_transcodingDialog->show();
5638 }
5639 
5640 void Bin::addFilterToClip(const QString &sourceClipId, const QString &filterId, stringMap params)
5641 {
5642     std::shared_ptr<ProjectClip> clip = m_itemModel->getClipByBinID(sourceClipId);
5643     if (clip) {
5644         clip->addEffect(filterId, params);
5645     }
5646 }
5647 
5648 bool Bin::addProjectClipInFolder(const QString &path, const QString &sourceClipId, const QString &sourceFolder, const QString &jobId)
5649 {
5650     // Check if the clip is already inserted in the project, if yes exit
5651     QStringList existingIds = m_itemModel->getClipByUrl(QFileInfo(path));
5652     if (!existingIds.isEmpty()) {
5653         return true;
5654     }
5655     Fun undo = []() { return true; };
5656     Fun redo = []() { return true; };
5657     std::pair<ClipJobManager::JobCompletionAction, QString> jobAction = ClipJobManager::getJobAction(jobId);
5658     if (jobAction.first == ClipJobManager::JobCompletionAction::ReplaceOriginal) {
5659         // Simply replace source clip with stabilized version
5660         replaceSingleClip(sourceClipId, path);
5661         return true;
5662     }
5663     // Check if folder exists
5664     QString folderId = QStringLiteral("-1");
5665     std::shared_ptr<ProjectFolder> baseFolder = nullptr;
5666     if (jobAction.first == ClipJobManager::JobCompletionAction::SubFolder) {
5667         baseFolder = m_itemModel->getFolderByBinId(sourceFolder);
5668     }
5669     if (baseFolder == nullptr) {
5670         baseFolder = m_itemModel->getRootFolder();
5671     }
5672     if (jobAction.second.isEmpty()) {
5673         // Put clip in sourceFolder if we don't have a folder name
5674         folderId = sourceFolder;
5675     } else {
5676         bool found = false;
5677         for (int i = 0; i < baseFolder->childCount(); ++i) {
5678             auto currentItem = std::static_pointer_cast<AbstractProjectItem>(baseFolder->child(i));
5679             if (currentItem->itemType() == AbstractProjectItem::FolderItem && currentItem->name() == jobAction.second) {
5680                 found = true;
5681                 folderId = currentItem->clipId();
5682                 break;
5683             }
5684         }
5685 
5686         if (!found) {
5687             // if it was not found, create folder
5688             m_itemModel->requestAddFolder(folderId, jobAction.second, baseFolder->clipId(), undo, redo);
5689         }
5690     }
5691     std::function<void(const QString &)> callBack = [this, sourceClipId, jobAction, path](const QString &binId) {
5692         if (!binId.isEmpty()) {
5693             if (jobAction.first != ClipJobManager::JobCompletionAction::ReplaceOriginal) {
5694                 // Clip was added to Bin, select it if replaced clip is still selected
5695                 QModelIndex ix = m_proxyModel->selectionModel()->currentIndex();
5696                 if (ix.isValid()) {
5697                     std::shared_ptr<AbstractProjectItem> currentItem = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(ix));
5698                     if (currentItem->clipId() == sourceClipId) {
5699                         selectClipById(binId);
5700                     }
5701                 }
5702             }
5703         }
5704     };
5705     auto id = ClipCreator::createClipFromFile(path, folderId, m_itemModel, undo, redo, callBack);
5706     bool ok = (id != QStringLiteral("-1"));
5707     if (ok) {
5708         pCore->pushUndo(undo, redo, i18nc("@action", "Add clip"));
5709     }
5710     return ok;
5711 }
5712 
5713 void Bin::updateClipsCount()
5714 {
5715     int count = m_itemModel->clipsCount();
5716     if (count < 2) {
5717         m_clipsCountMessage = QString();
5718     } else {
5719         int selected = 0;
5720         const QModelIndexList indexes = m_proxyModel->selectionModel()->selection().indexes();
5721         for (const QModelIndex &ix : indexes) {
5722             if (ix.isValid() && ix.column() == 0) {
5723                 std::shared_ptr<AbstractProjectItem> item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(ix));
5724                 if (item->itemType() == AbstractProjectItem::ClipItem) {
5725                     selected++;
5726                 }
5727             }
5728         }
5729         if (selected == 0) {
5730             m_clipsCountMessage = i18n("<b>%1</b> clips | ", count);
5731         } else {
5732             m_clipsCountMessage = i18n("<b>%1</b> clips (%2 selected) | ", count, selected);
5733         }
5734     }
5735     showBinInfo();
5736 }
5737 
5738 void Bin::updateKeyBinding(const QString &bindingMessage)
5739 {
5740     m_keyBindingMessage = bindingMessage;
5741     showBinInfo();
5742 }
5743 
5744 void Bin::showBinInfo()
5745 {
5746     pCore->window()->showKeyBinding(QString("%1%2").arg(m_clipsCountMessage, m_keyBindingMessage));
5747 }
5748 
5749 bool Bin::containsId(const QString &clipId) const
5750 {
5751     if (m_listType == BinIconView) {
5752         // Check if the clip is in our folder
5753         const QString rootId = m_itemView->rootIndex().data(AbstractProjectItem::DataId).toString();
5754         std::shared_ptr<ProjectClip> clip = m_itemModel->getClipByBinID(clipId);
5755         if (clip) {
5756             std::shared_ptr<AbstractProjectItem> par = clip->parent();
5757             if (par && par->clipId() == rootId) {
5758                 return true;
5759             }
5760         }
5761         return false;
5762     } else {
5763     }
5764     return true;
5765 }
5766 
5767 void Bin::processMultiStream(const QString &clipId, QList<int> videoStreams, QList<int> audioStreams)
5768 {
5769     auto binClip = m_itemModel->getClipByBinID(clipId);
5770     // We retrieve the folder containing our clip, because we will set the other streams in the same
5771     std::shared_ptr<AbstractProjectItem> baseFolder = binClip->parent();
5772     if (!baseFolder) {
5773         baseFolder = m_itemModel->getRootFolder();
5774     }
5775     const QString parentId = baseFolder->clipId();
5776     std::shared_ptr<Mlt::Producer> producer = binClip->originalProducer();
5777     // This helper lambda request addition of a given stream
5778     auto addStream = [this, parentId, producer](int vindex, int vstream, int aindex, int astream, Fun &undo, Fun &redo) {
5779         auto clone = ProjectClip::cloneProducer(producer);
5780         clone->set("video_index", vindex);
5781         clone->set("audio_index", aindex);
5782         clone->set("vstream", vstream);
5783         clone->set("astream", astream);
5784         QString id;
5785         m_itemModel->requestAddBinClip(id, clone, parentId, undo, redo);
5786     };
5787     Fun undo = []() { return true; };
5788     Fun redo = []() { return true; };
5789 
5790     if (KdenliveSettings::automultistreams()) {
5791         for (int i = 1; i < videoStreams.count(); ++i) {
5792             int vindex = videoStreams.at(i);
5793             int aindex = 0;
5794             if (i <= audioStreams.count() - 1) {
5795                 aindex = audioStreams.at(i);
5796             }
5797             addStream(vindex, i - 1, aindex, qMin(i - 1, audioStreams.count() - 1), undo, redo);
5798         }
5799         pCore->pushUndo(undo, redo, i18np("Add additional stream for clip", "Add additional streams for clip", videoStreams.count() - 1));
5800         return;
5801     }
5802 
5803     int width = int(60.0 * pCore->getCurrentDar());
5804     if (width % 2 == 1) {
5805         width++;
5806     }
5807 
5808     QScopedPointer<QDialog> dialog(new QDialog(qApp->activeWindow()));
5809     dialog->setWindowTitle(QStringLiteral("Multi Stream Clip"));
5810     QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
5811     QWidget *mainWidget = new QWidget(dialog.data());
5812     auto *mainLayout = new QVBoxLayout;
5813     dialog->setLayout(mainLayout);
5814     mainLayout->addWidget(mainWidget);
5815     QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok);
5816     okButton->setDefault(true);
5817     okButton->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_Return));
5818     dialog->connect(buttonBox, &QDialogButtonBox::accepted, dialog.data(), &QDialog::accept);
5819     dialog->connect(buttonBox, &QDialogButtonBox::rejected, dialog.data(), &QDialog::reject);
5820     okButton->setText(i18n("Import selected clips"));
5821 
5822     QLabel *lab1 = new QLabel(i18n("Additional streams for clip\n %1", binClip->clipName()), mainWidget);
5823     mainLayout->addWidget(lab1);
5824     QList<QGroupBox *> groupList;
5825     QList<QComboBox *> comboList;
5826     // We start loading the list at 1, video index 0 should already be loaded
5827     for (int j = 1; j < videoStreams.count(); ++j) {
5828         auto clone = ProjectClip::cloneProducer(producer);
5829         clone->set("video_index", videoStreams.at(j));
5830         if (clone == nullptr || !clone->is_valid()) {
5831             continue;
5832         }
5833         // TODO this keyframe should be cached
5834         QImage thumb = KThumb::getFrame(clone.get(), 0, width, 60);
5835         QGroupBox *streamFrame = new QGroupBox(i18n("Video stream %1", videoStreams.at(j)), mainWidget);
5836         mainLayout->addWidget(streamFrame);
5837         streamFrame->setProperty("vindex", videoStreams.at(j));
5838         groupList << streamFrame;
5839         streamFrame->setCheckable(true);
5840         streamFrame->setChecked(true);
5841         auto *vh = new QVBoxLayout(streamFrame);
5842         QLabel *iconLabel = new QLabel(mainWidget);
5843         mainLayout->addWidget(iconLabel);
5844         iconLabel->setPixmap(QPixmap::fromImage(thumb));
5845         vh->addWidget(iconLabel);
5846         if (audioStreams.count() > 1) {
5847             auto *cb = new QComboBox(mainWidget);
5848             mainLayout->addWidget(cb);
5849             for (int k = 0; k < audioStreams.count(); ++k) {
5850                 cb->addItem(i18n("Audio stream %1", audioStreams.at(k)), audioStreams.at(k));
5851             }
5852             comboList << cb;
5853             cb->setCurrentIndex(qMin(j, audioStreams.count() - 1));
5854             vh->addWidget(cb);
5855         }
5856         mainLayout->addWidget(streamFrame);
5857     }
5858     mainLayout->addStretch(10);
5859     mainLayout->addWidget(buttonBox);
5860     if (dialog->exec() == QDialog::Accepted) {
5861         // import selected streams
5862         int importedStreams = 0;
5863         for (int i = 0; i < groupList.count(); ++i) {
5864             if (groupList.at(i)->isChecked()) {
5865                 int vindex = groupList.at(i)->property("vindex").toInt();
5866                 int ax = qMin(i, comboList.size() - 1);
5867                 int aindex = -1;
5868                 if (ax >= 0) {
5869                     // only check audio index if we have several audio streams
5870                     aindex = comboList.at(ax)->itemData(comboList.at(ax)->currentIndex()).toInt();
5871                 }
5872                 addStream(vindex, i, aindex, ax, undo, redo);
5873                 importedStreams++;
5874             }
5875         }
5876         pCore->pushUndo(undo, redo, i18np("Add additional stream for clip", "Add additional streams for clip", importedStreams));
5877     }
5878 }
5879 
5880 int Bin::getAllClipMarkers(int category) const
5881 {
5882     int markersCount = 0;
5883     QList<std::shared_ptr<ProjectClip>> clipList = m_itemModel->getRootFolder()->childClips();
5884     for (const std::shared_ptr<ProjectClip> &clip : qAsConst(clipList)) {
5885         markersCount += clip->getMarkerModel()->getAllMarkers(category).count();
5886     }
5887     return markersCount;
5888 }
5889 
5890 void Bin::removeMarkerCategories(QList<int> toRemove, const QMap<int, int> remapCategories)
5891 {
5892     Fun undo = []() { return true; };
5893     Fun redo = []() { return true; };
5894     bool found = false;
5895     QList<std::shared_ptr<ProjectClip>> clipList = m_itemModel->getRootFolder()->childClips();
5896     for (const std::shared_ptr<ProjectClip> &clip : qAsConst(clipList)) {
5897         bool res = clip->removeMarkerCategories(toRemove, remapCategories, undo, redo);
5898         if (!found && res) {
5899             found = true;
5900         }
5901     }
5902     if (found) {
5903         pCore->pushUndo(undo, redo, i18n("Remove clip markers"));
5904     }
5905 }
5906 
5907 QStringList Bin::sequenceReferencedClips(const QUuid &uuid) const
5908 {
5909     QStringList results;
5910     QList<std::shared_ptr<ProjectClip>> clipList = m_itemModel->getRootFolder()->childClips();
5911     for (const std::shared_ptr<ProjectClip> &clip : qAsConst(clipList)) {
5912         if (clip->refCount() > 0) {
5913             const QString referenced = clip->isReferenced(uuid);
5914             if (!referenced.isEmpty()) {
5915                 results << referenced;
5916             }
5917         }
5918     }
5919     return results;
5920 }
5921 
5922 void Bin::updateSequenceClip(const QUuid &uuid, int duration, int pos)
5923 {
5924     if (pos > -1) {
5925         m_doc->setSequenceProperty(uuid, QStringLiteral("position"), pos);
5926     }
5927     const QString binId = m_itemModel->getSequenceId(uuid);
5928     if (!binId.isEmpty() && m_doc->isModified()) {
5929         std::shared_ptr<ProjectClip> clip = m_itemModel->getClipByBinID(binId);
5930         Q_ASSERT(clip != nullptr);
5931         if (m_doc->sequenceThumbRequiresRefresh(uuid)) {
5932             // Store general sequence properties
5933             QMap<QString, QString> properties;
5934             properties.insert(QStringLiteral("length"), QString::number(duration));
5935             properties.insert(QStringLiteral("out"), QString::number(duration - 1));
5936             properties.insert(QStringLiteral("kdenlive:duration"), clip->framesToTime(duration));
5937             properties.insert(QStringLiteral("kdenlive:maxduration"), QString::number(duration));
5938             clip->setProperties(properties);
5939             // Reset thumbs producer
5940             clip->resetSequenceThumbnails();
5941             ClipLoadTask::start(ObjectId(KdenliveObjectType::BinClip, binId.toInt(), QUuid()), QDomElement(), true, -1, -1, this);
5942             m_doc->sequenceThumbUpdated(uuid);
5943             clip->reloadTimeline();
5944         }
5945     }
5946 }
5947 
5948 void Bin::updateTimelineOccurrences()
5949 {
5950     // Update the clip in timeline menu
5951     QModelIndex currentSelection = m_proxyModel->selectionModel()->currentIndex();
5952     bool triggered = false;
5953     if (currentSelection.isValid()) {
5954         std::shared_ptr<AbstractProjectItem> currentItem = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(currentSelection));
5955         if (currentItem) {
5956             if (currentItem->itemType() == AbstractProjectItem::ClipItem) {
5957                 auto clip = std::static_pointer_cast<ProjectClip>(currentItem);
5958                 if (clip) {
5959                     triggered = true;
5960                     Q_EMIT findInTimeline(clip->clipId(), clip->timelineInstances());
5961                 }
5962             } else if (currentItem->itemType() == AbstractProjectItem::FolderItem) {
5963                 m_sequencesFolderAction->setChecked(currentItem->clipId().toInt() == m_itemModel->defaultSequencesFolder());
5964                 m_audioCapturesFolderAction->setChecked(currentItem->clipId().toInt() == m_itemModel->defaultAudioCaptureFolder());
5965             }
5966         }
5967     }
5968     if (!triggered) {
5969         Q_EMIT findInTimeline(QString());
5970     }
5971 }
5972 
5973 void Bin::setSequenceThumbnail(const QUuid &uuid, int frame)
5974 {
5975     const QString bid = m_itemModel->getSequenceId(uuid);
5976     if (!bid.isEmpty()) {
5977         std::shared_ptr<ProjectClip> sequenceClip = getBinClip(bid);
5978         if (sequenceClip) {
5979             m_doc->setSequenceProperty(uuid, QStringLiteral("thumbnailFrame"), frame);
5980             ClipLoadTask::start(ObjectId(KdenliveObjectType::BinClip, bid.toInt(), QUuid()), QDomElement(), true, -1, -1, this);
5981         }
5982     }
5983 }
5984 
5985 void Bin::updateSequenceAVType(const QUuid &uuid, int tracksCount)
5986 {
5987     const QString bId = m_itemModel->getSequenceId(uuid);
5988     if (!bId.isEmpty()) {
5989         std::shared_ptr<ProjectClip> sequenceClip = getBinClip(bId);
5990         if (sequenceClip) {
5991             sequenceClip->refreshTracksState(tracksCount);
5992         }
5993     }
5994 }
5995 
5996 void Bin::setDefaultSequenceFolder(bool enable)
5997 {
5998     QModelIndex currentSelection = m_proxyModel->selectionModel()->currentIndex();
5999     if (currentSelection.isValid()) {
6000         std::shared_ptr<AbstractProjectItem> currentItem = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(currentSelection));
6001         if (currentItem) {
6002             m_itemModel->setSequencesFolder(enable ? currentItem->clipId().toInt() : -1);
6003         }
6004     }
6005 }
6006 
6007 void Bin::setDefaultAudioCaptureFolder(bool enable)
6008 {
6009     QModelIndex currentSelection = m_proxyModel->selectionModel()->currentIndex();
6010     if (currentSelection.isValid()) {
6011         std::shared_ptr<AbstractProjectItem> currentItem = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(currentSelection));
6012         if (currentItem) {
6013             m_itemModel->setAudioCaptureFolder(enable ? currentItem->clipId().toInt() : -1);
6014         }
6015     }
6016 }
6017 
6018 void Bin::moveTimeWarpToFolder(const QDir sequenceFolder, bool copy)
6019 {
6020     QList<std::shared_ptr<ProjectClip>> allClips = m_itemModel->getRootFolder()->childClips();
6021     for (auto &c : allClips) {
6022         c->copyTimeWarpProducers(sequenceFolder, copy);
6023     }
6024 }
6025 
6026 void Bin::sequenceActivated()
6027 {
6028     updateTargets();
6029     QList<std::shared_ptr<ProjectClip>> allClips = m_itemModel->getRootFolder()->childClips();
6030     for (auto &c : allClips) {
6031         c->refreshBounds();
6032     }
6033 }
6034 
6035 bool Bin::usesVariableFpsClip()
6036 {
6037     QList<std::shared_ptr<ProjectClip>> allClips = m_itemModel->getRootFolder()->childClips();
6038     for (auto &c : allClips) {
6039         ClipType::ProducerType type = c->clipType();
6040         if ((type == ClipType::AV || type == ClipType::Video || type == ClipType::Audio) && c->hasVariableFps()) {
6041             return true;
6042         }
6043     }
6044     return false;
6045 }
6046 
6047 void Bin::transcodeUsedClips()
6048 {
6049     if (m_doc->isModified()) {
6050         // Recommend saving before the transcode operation
6051         if (KMessageBox::questionTwoActions(QApplication::activeWindow(),
6052                                             i18nc("@label:textbox", "We recommend that you save the project before the transcode operation"), {},
6053                                             KGuiItem(i18nc("@action:button", "Save Project")),
6054                                             KGuiItem(i18nc("@action:button", "Transcode Without Saving"))) == KMessageBox::PrimaryAction) {
6055             if (!pCore->projectManager()->saveFile()) {
6056                 return;
6057             }
6058         }
6059     }
6060     QList<std::shared_ptr<ProjectClip>> allClips = m_itemModel->getRootFolder()->childClips();
6061     m_proxyModel->selectionModel()->clearSelection();
6062     // Select all variable fps clips
6063     for (auto &c : allClips) {
6064         ClipType::ProducerType type = c->clipType();
6065         if ((type == ClipType::AV || type == ClipType::Video || type == ClipType::Audio) && c->hasVariableFps()) {
6066             QModelIndex ix = m_itemModel->getIndexFromItem(c);
6067             int row = ix.row();
6068             const QModelIndex id = m_itemModel->index(row, 0, ix.parent());
6069             const QModelIndex id2 = m_itemModel->index(row, m_itemModel->columnCount() - 1, ix.parent());
6070             m_proxyModel->selectionModel()->select(QItemSelection(m_proxyModel->mapFromSource(id), m_proxyModel->mapFromSource(id2)),
6071                                                    QItemSelectionModel::Select);
6072         }
6073     }
6074     requestSelectionTranscoding(true);
6075 }