File indexing completed on 2024-04-28 08:43:28

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