diff --git a/src/bin/bin.cpp b/src/bin/bin.cpp index 4cda56b25..0051f63d5 100644 --- a/src/bin/bin.cpp +++ b/src/bin/bin.cpp @@ -1,4103 +1,4111 @@ /* Copyright (C) 2012 Till Theato Copyright (C) 2014 Jean-Baptiste Mardelle This file is part of Kdenlive. See www.kdenlive.org. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "bin.h" #include "bincommands.h" #include "clipcreator.hpp" #include "core.h" #include "dialogs/clipcreationdialog.h" #include "doc/documentchecker.h" #include "doc/docundostack.hpp" #include "doc/kdenlivedoc.h" #include "effects/effectstack/model/effectstackmodel.hpp" #include "jobs/audiothumbjob.hpp" #include "jobs/jobmanager.h" #include "jobs/loadjob.hpp" #include "jobs/thumbjob.hpp" #include "kdenlive_debug.h" #include "kdenlivesettings.h" #include "mainwindow.h" #include "mlt++/Mlt.h" #include "mltcontroller/clipcontroller.h" #include "mltcontroller/clippropertiescontroller.h" #include "monitor/monitor.h" #include "project/dialogs/slideshowclip.h" #include "project/invaliddialog.h" #include "project/projectcommands.h" #include "project/projectmanager.h" #include "projectclip.h" #include "projectfolder.h" #include "projectitemmodel.h" #include "projectsortproxymodel.h" #include "projectsubclip.h" #include "tagwidget.hpp" #include "titler/titlewidget.h" #include "ui_qtextclip_ui.h" #include "undohelper.hpp" #include "xml/xml.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /** * @class BinItemDelegate * @brief This class is responsible for drawing items in the QTreeView. */ class BinItemDelegate : public QStyledItemDelegate { public: explicit BinItemDelegate(QObject *parent = nullptr) : QStyledItemDelegate(parent) { connect(this, &QStyledItemDelegate::closeEditor, [&]() { m_editorOpen = false; }); } void setEditorData(QWidget *w, const QModelIndex &i) const override { if (!m_editorOpen) { QStyledItemDelegate::setEditorData(w, i); m_editorOpen = true; } } bool editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index) override { if (event->type() == QEvent::MouseButtonPress) { auto *me = (QMouseEvent *)event; if (index.column() == 0) { if (m_audioDragRect.contains(me->pos())) { dragType = PlaylistState::AudioOnly; } else if (m_videoDragRect.contains(me->pos())) { dragType = PlaylistState::VideoOnly; } else { dragType = PlaylistState::Disabled; } } else { dragType = PlaylistState::Disabled; if (index.column() == 7) { // Rating QRect rect = option.rect; rect.adjust(option.rect.width() / 12, 0, 0, 0); int rate = 0; if (me->pos().x() > rect.x()) { - rate = KRatingPainter::getRatingFromPosition(rect, Qt::AlignLeft, qApp->layoutDirection(), me->pos()); + rate = KRatingPainter::getRatingFromPosition(rect, Qt::AlignLeft | Qt::AlignVCenter, qApp->layoutDirection(), me->pos()); } if (rate > -1) { // Full star rating only if (rate %2 == 1) { rate++; } static_cast(model)->updateRating(index, (uint) rate); } } } } event->ignore(); return false; } void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const override { if (index.column() != 0) { QStyledItemDelegate::updateEditorGeometry(editor, option, index); return; } QStyleOptionViewItem opt = option; initStyleOption(&opt, index); QRect r1 = option.rect; int type = index.data(AbstractProjectItem::ItemTypeRole).toInt(); int decoWidth = 0; if (opt.decorationSize.height() > 0) { decoWidth += r1.height() * pCore->getCurrentDar(); } int mid = 0; if (type == AbstractProjectItem::ClipItem || type == AbstractProjectItem::SubClipItem) { mid = (int)((r1.height() / 2)); } r1.adjust(decoWidth, 0, 0, -mid); QFont ft = option.font; ft.setBold(true); QFontMetricsF fm(ft); QRect r2 = fm.boundingRect(r1, Qt::AlignLeft | Qt::AlignTop, index.data(AbstractProjectItem::DataName).toString()).toRect(); editor->setGeometry(r2); } QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override { QSize hint = QStyledItemDelegate::sizeHint(option, index); QString text = index.data(AbstractProjectItem::DataName).toString(); QRectF r = option.rect; QFont ft = option.font; ft.setBold(true); QFontMetricsF fm(ft); QStyle *style = option.widget ? option.widget->style() : QApplication::style(); const int textMargin = style->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1; int width = fm.boundingRect(r, Qt::AlignLeft | Qt::AlignTop, text).width() + option.decorationSize.width() + 2 * textMargin; hint.setWidth(width); int type = index.data(AbstractProjectItem::ItemTypeRole).toInt(); if (type == AbstractProjectItem::FolderItem) { return QSize(hint.width(), qMin(option.fontMetrics.lineSpacing() + 4, hint.height())); } if (type == AbstractProjectItem::ClipItem) { return QSize(hint.width(), qMax(option.fontMetrics.lineSpacing() * 2 + 4, qMax(hint.height(), option.decorationSize.height()))); } if (type == AbstractProjectItem::SubClipItem) { return QSize(hint.width(), qMax(option.fontMetrics.lineSpacing() * 2 + 4, qMin(hint.height(), (int)(option.decorationSize.height() / 1.5)))); } QIcon icon = qvariant_cast(index.data(Qt::DecorationRole)); QString line1 = index.data(Qt::DisplayRole).toString(); QString line2 = index.data(Qt::UserRole).toString(); int textW = qMax(option.fontMetrics.horizontalAdvance(line1), option.fontMetrics.horizontalAdvance(line2)); QSize iconSize = icon.actualSize(option.decorationSize); return {qMax(textW, iconSize.width()) + 4, option.fontMetrics.lineSpacing() * 2 + 4}; } void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override { if (index.column() == 0 && !index.data().isNull()) { QRect r1 = option.rect; painter->save(); painter->setClipRect(r1); QStyleOptionViewItem opt(option); initStyleOption(&opt, index); int type = index.data(AbstractProjectItem::ItemTypeRole).toInt(); QStyle *style = opt.widget ? opt.widget->style() : QApplication::style(); const int textMargin = style->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1; // QRect r = QStyle::alignedRect(opt.direction, Qt::AlignVCenter | Qt::AlignLeft, opt.decorationSize, r1); style->drawPrimitive(QStyle::PE_PanelItemViewItem, &opt, painter, opt.widget); if ((option.state & static_cast(QStyle::State_Selected)) != 0) { painter->setPen(option.palette.highlightedText().color()); } else { painter->setPen(option.palette.text().color()); } QRect r = r1; QFont font = painter->font(); font.setBold(true); painter->setFont(font); if (type == AbstractProjectItem::ClipItem || type == AbstractProjectItem::SubClipItem) { int decoWidth = 0; AbstractProjectItem::CLIPSTATUS clipStatus = (AbstractProjectItem::CLIPSTATUS)index.data(AbstractProjectItem::ClipStatus).toInt(); if (opt.decorationSize.height() > 0) { r.setWidth(r.height() * pCore->getCurrentDar()); QPixmap pix = opt.icon.pixmap(opt.icon.actualSize(r.size())); if (!pix.isNull()) { // Draw icon decoWidth += r.width() + textMargin; r.setWidth(r.height() * pix.width() / pix.height()); painter->drawPixmap(r, pix, QRect(0, 0, pix.width(), pix.height())); } m_thumbRect = r; } // Add audio/video icons for selective drag int cType = index.data(AbstractProjectItem::ClipType).toInt(); if (clipStatus == AbstractProjectItem::StatusMissing) { painter->save(); painter->setPen(QPen(Qt::red, 3)); painter->drawRect(m_thumbRect); painter->restore(); } else if (cType == ClipType::Image || cType == ClipType::SlideShow) { // Draw 'photo' frame to identify image clips painter->save(); int penWidth = m_thumbRect.height() / 14; penWidth += penWidth % 2; painter->setPen(QPen(QColor(255, 255, 255, 160), penWidth)); penWidth /= 2; painter->drawRoundedRect(m_thumbRect.adjusted(penWidth, penWidth, -penWidth - 1, -penWidth - 1), 4, 4); painter->setPen(QPen(Qt::black, 1)); painter->drawRoundedRect(m_thumbRect.adjusted(0, 0, -1, -1), 4, 4); painter->restore(); } int mid = (int)((r1.height() / 2)); r1.adjust(decoWidth, 0, 0, -mid); QRect r2 = option.rect; r2.adjust(decoWidth, mid, 0, 0); QRectF bounding; painter->drawText(r1, Qt::AlignLeft | Qt::AlignTop, index.data(AbstractProjectItem::DataName).toString(), &bounding); font.setBold(false); painter->setFont(font); QString subText = index.data(AbstractProjectItem::DataDuration).toString(); QString tags = index.data(AbstractProjectItem::DataTag).toString(); if (!tags.isEmpty()) { QStringList t = tags.split(QLatin1Char(';')); QRectF tagRect = m_thumbRect.adjusted(2, 2, 0, 2); tagRect.setWidth(r1.height() / 3.5); tagRect.setHeight(tagRect.width()); for (const QString &color : t) { painter->setBrush(QColor(color)); painter->drawRoundedRect(tagRect, tagRect.height() / 2, tagRect.height() / 2); tagRect.moveTop(tagRect.bottom() + tagRect.height() / 4); } } if (!subText.isEmpty()) { r2.adjust(0, bounding.bottom() - r2.top(), 0, 0); QColor subTextColor = painter->pen().color(); subTextColor.setAlphaF(.5); painter->setPen(subTextColor); // Draw usage counter int usage = index.data(AbstractProjectItem::UsageCount).toInt(); if (usage > 0) { subText.append(QString::asprintf(" [%d]", usage)); } painter->drawText(r2, Qt::AlignLeft | Qt::AlignTop, subText, &bounding); bool hasAudioAndVideo = index.data(AbstractProjectItem::ClipHasAudioAndVideo).toBool(); if (hasAudioAndVideo && (cType == ClipType::AV || cType == ClipType::Playlist) && (opt.state & QStyle::State_MouseOver)) { bounding.moveLeft(bounding.right() + (2 * textMargin)); bounding.adjust(0, textMargin, 0, -textMargin); QIcon aDrag = QIcon::fromTheme(QStringLiteral("audio-volume-medium")); m_audioDragRect = bounding.toRect(); m_audioDragRect.setWidth(m_audioDragRect.height()); aDrag.paint(painter, m_audioDragRect, Qt::AlignLeft); m_videoDragRect = m_audioDragRect; m_videoDragRect.moveLeft(m_audioDragRect.right()); QIcon vDrag = QIcon::fromTheme(QStringLiteral("kdenlive-show-video")); vDrag.paint(painter, m_videoDragRect, Qt::AlignLeft); } else { //m_audioDragRect = QRect(); //m_videoDragRect = QRect(); } } if (type == AbstractProjectItem::ClipItem) { // Overlay icon if necessary QVariant v = index.data(AbstractProjectItem::IconOverlay); if (!v.isNull()) { QIcon reload = QIcon::fromTheme(v.toString()); r.setTop(r.bottom() - bounding.height()); r.setWidth(bounding.height()); reload.paint(painter, r); } int jobProgress = index.data(AbstractProjectItem::JobProgress).toInt(); auto status = index.data(AbstractProjectItem::JobStatus).value(); if (status == JobManagerStatus::Pending || status == JobManagerStatus::Running) { // Draw job progress bar int progressWidth = option.fontMetrics.averageCharWidth() * 8; int progressHeight = option.fontMetrics.ascent() / 4; QRect progress(r1.x() + 1, opt.rect.bottom() - progressHeight - 2, progressWidth, progressHeight); painter->setPen(Qt::NoPen); painter->setBrush(Qt::darkGray); if (status == JobManagerStatus::Running) { painter->drawRoundedRect(progress, 2, 2); painter->setBrush((option.state & static_cast((QStyle::State_Selected) != 0)) != 0 ? option.palette.text() : option.palette.highlight()); progress.setWidth((progressWidth - 2) * jobProgress / 100); painter->drawRoundedRect(progress, 2, 2); } else { // Draw kind of a pause icon progress.setWidth(3); painter->drawRect(progress); progress.moveLeft(progress.right() + 3); painter->drawRect(progress); } } bool jobsucceeded = index.data(AbstractProjectItem::JobSuccess).toBool(); if (!jobsucceeded) { QIcon warning = QIcon::fromTheme(QStringLiteral("process-stop")); warning.paint(painter, r2); } } } else { // Folder int decoWidth = 0; if (opt.decorationSize.height() > 0) { r.setWidth(r.height() * pCore->getCurrentDar()); QPixmap pix = opt.icon.pixmap(opt.icon.actualSize(r.size())); // Draw icon decoWidth += r.width() + textMargin; r.setWidth(r.height() * pix.width() / pix.height()); painter->drawPixmap(r, pix, QRect(0, 0, pix.width(), pix.height())); } r1.adjust(decoWidth, 0, 0, 0); QRectF bounding; painter->drawText(r1, Qt::AlignLeft | Qt::AlignTop, index.data(AbstractProjectItem::DataName).toString(), &bounding); } painter->restore(); } else if (index.column() == 7) { QStyleOptionViewItem opt(option); initStyleOption(&opt, index); QRect r1 = opt.rect; // Tweak bg opacity since breeze dark star has same color as highlighted background painter->setOpacity(0.5); QStyle *style = opt.widget ? opt.widget->style() : QApplication::style(); style->drawPrimitive(QStyle::PE_PanelItemViewItem, &opt, painter, opt.widget); painter->setOpacity(1); if (index.data(AbstractProjectItem::ItemTypeRole).toInt() != AbstractProjectItem::FolderItem) { r1.adjust(r1.width() / 12, 0, 0, 0); - KRatingPainter::paintRating(painter, r1, Qt::AlignLeft, index.data().toInt()); + KRatingPainter::paintRating(painter, r1, Qt::AlignLeft | Qt::AlignVCenter, index.data().toInt()); } } else { QStyledItemDelegate::paint(painter, option, index); } } int getFrame(QModelIndex index, int mouseX) { int type = index.data(AbstractProjectItem::ItemTypeRole).toInt(); if ((type != AbstractProjectItem::ClipItem && type != AbstractProjectItem::SubClipItem) || mouseX < m_thumbRect.x() || mouseX > m_thumbRect.right()) { return 0; } return 100 * (mouseX - m_thumbRect.x()) / m_thumbRect.width(); } private: mutable bool m_editorOpen{false}; mutable QRect m_audioDragRect; mutable QRect m_videoDragRect; mutable QRect m_thumbRect; public: PlaylistState::ClipState dragType{PlaylistState::Disabled}; }; /** * @class BinListItemDelegate * @brief This class is responsible for drawing items in the QListView. */ class BinListItemDelegate : public QStyledItemDelegate { public: explicit BinListItemDelegate(QObject *parent = nullptr) : QStyledItemDelegate(parent) { connect(this, &QStyledItemDelegate::closeEditor, [&]() { m_editorOpen = false; }); } bool editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index) override { Q_UNUSED(model); Q_UNUSED(option); Q_UNUSED(index); if (event->type() == QEvent::MouseButtonPress) { auto *me = (QMouseEvent *)event; if (m_audioDragRect.contains(me->pos())) { dragType = PlaylistState::AudioOnly; } else if (m_videoDragRect.contains(me->pos())) { dragType = PlaylistState::VideoOnly; } else { dragType = PlaylistState::Disabled; } } event->ignore(); return false; } void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override { if (!index.data().isNull()) { QStyleOptionViewItem opt(option); initStyleOption(&opt, index); QStyledItemDelegate::paint(painter, option, index); int adjust = (opt.rect.width() - opt.decorationSize.width()) / 2; QRect rect(opt.rect.x(), opt.rect.y(), opt.decorationSize.width(), opt.decorationSize.height()); m_thumbRect = adjust > 0 && adjust < rect.width() ? rect.adjusted(adjust, 0, -adjust, 0) : rect; //Tags QString tags = index.data(AbstractProjectItem::DataTag).toString(); if (!tags.isEmpty()) { QStringList t = tags.split(QLatin1Char(';')); QRectF tagRect = m_thumbRect.adjusted(2, 2, 0, 2); tagRect.setWidth(m_thumbRect.height() / 5); tagRect.setHeight(tagRect.width()); for (const QString &color : t) { painter->setBrush(QColor(color)); painter->drawRoundedRect(tagRect, tagRect.height() / 2, tagRect.height() / 2); tagRect.moveTop(tagRect.bottom() + tagRect.height() / 4); } } // Add audio/video icons for selective drag int cType = index.data(AbstractProjectItem::ClipType).toInt(); bool hasAudioAndVideo = index.data(AbstractProjectItem::ClipHasAudioAndVideo).toBool(); if (hasAudioAndVideo && (cType == ClipType::AV || cType == ClipType::Playlist) && (opt.state & QStyle::State_MouseOver)) { QRect thumbRect = m_thumbRect; int iconSize = painter->boundingRect(thumbRect, Qt::AlignLeft, QStringLiteral("O")).height(); thumbRect.setLeft(opt.rect.right() - iconSize - 4); thumbRect.setWidth(iconSize); thumbRect.setBottom(m_thumbRect.top() + iconSize); QIcon aDrag = QIcon::fromTheme(QStringLiteral("audio-volume-medium")); m_audioDragRect = thumbRect; aDrag.paint(painter, m_audioDragRect, Qt::AlignRight); m_videoDragRect = m_audioDragRect; m_videoDragRect.moveTop(thumbRect.bottom()); QIcon vDrag = QIcon::fromTheme(QStringLiteral("kdenlive-show-video")); vDrag.paint(painter, m_videoDragRect, Qt::AlignRight); } else { //m_audioDragRect = QRect(); //m_videoDragRect = QRect(); } } } int getFrame(QModelIndex index, int mouseX) { int type = index.data(AbstractProjectItem::ItemTypeRole).toInt(); if ((type != AbstractProjectItem::ClipItem && type != AbstractProjectItem::SubClipItem)|| mouseX < m_thumbRect.x() || mouseX > m_thumbRect.right()) { return 0; } return 100 * (mouseX - m_thumbRect.x()) / m_thumbRect.width(); } private: mutable bool m_editorOpen{false}; mutable QRect m_audioDragRect; mutable QRect m_videoDragRect; mutable QRect m_thumbRect; public: PlaylistState::ClipState dragType{PlaylistState::Disabled}; }; MyListView::MyListView(QWidget *parent) : QListView(parent) { setViewMode(QListView::IconMode); setMovement(QListView::Static); setResizeMode(QListView::Adjust); setWordWrap(true); setDragDropMode(QAbstractItemView::DragDrop); setAcceptDrops(true); setDragEnabled(true); viewport()->setAcceptDrops(true); } void MyListView::focusInEvent(QFocusEvent *event) { QListView::focusInEvent(event); if (event->reason() == Qt::MouseFocusReason) { emit focusView(); } } void MyListView::mousePressEvent(QMouseEvent *event) { QListView::mousePressEvent(event); if (event->button() == Qt::LeftButton) { m_startPos = event->pos(); QModelIndex ix = indexAt(m_startPos); if (ix.isValid()) { QAbstractItemDelegate *del = itemDelegate(ix); m_dragType = static_cast(del)->dragType; } else { m_dragType = PlaylistState::Disabled; } emit updateDragMode(m_dragType); } } void MyListView::mouseMoveEvent(QMouseEvent *event) { if (event->modifiers() == Qt::ShiftModifier) { QModelIndex index = indexAt(event->pos()); if (index.isValid()) { QAbstractItemDelegate *del = itemDelegate(index); if (del) { auto delegate = static_cast(del); QRect vRect = visualRect(index); int frame = delegate->getFrame(index, event->pos().x() - vRect.x()); emit displayBinFrame(index, frame); } else { qDebug()<<"<<< NO DELEGATE!!!"; } } } QListView::mouseMoveEvent(event); } MyTreeView::MyTreeView(QWidget *parent) : QTreeView(parent) { setEditing(false); setAcceptDrops(true); } void MyTreeView::mousePressEvent(QMouseEvent *event) { QTreeView::mousePressEvent(event); if (event->button() == Qt::LeftButton) { m_startPos = event->pos(); QModelIndex ix = indexAt(m_startPos); if (ix.isValid()) { QAbstractItemDelegate *del = itemDelegate(ix); m_dragType = static_cast(del)->dragType; } else { m_dragType = PlaylistState::Disabled; } } } void MyTreeView::focusInEvent(QFocusEvent *event) { QTreeView::focusInEvent(event); if (event->reason() == Qt::MouseFocusReason) { emit focusView(); } } void MyTreeView::mouseMoveEvent(QMouseEvent *event) { bool dragged = false; if ((event->buttons() & Qt::LeftButton) != 0u) { int distance = (event->pos() - m_startPos).manhattanLength(); if (distance >= QApplication::startDragDistance()) { dragged = performDrag(); } } else if (event->modifiers() == Qt::ShiftModifier) { QModelIndex index = indexAt(event->pos()); if (index.isValid()) { QAbstractItemDelegate *del = itemDelegate(index); int frame = static_cast(del)->getFrame(index, event->pos().x()); emit displayBinFrame(index, frame); } } if (!dragged) { QTreeView::mouseMoveEvent(event); } } void MyTreeView::closeEditor(QWidget *editor, QAbstractItemDelegate::EndEditHint hint) { QAbstractItemView::closeEditor(editor, hint); setEditing(false); } void MyTreeView::editorDestroyed(QObject *editor) { QAbstractItemView::editorDestroyed(editor); setEditing(false); } bool MyTreeView::isEditing() const { return state() == QAbstractItemView::EditingState; } void MyTreeView::setEditing(bool edit) { setState(edit ? QAbstractItemView::EditingState : QAbstractItemView::NoState); } bool MyTreeView::performDrag() { QModelIndexList bases = selectedIndexes(); QModelIndexList indexes; for (int i = 0; i < bases.count(); i++) { if (bases.at(i).column() == 0) { indexes << bases.at(i); } } if (indexes.isEmpty()) { return false; } // Check if we want audio or video only emit updateDragMode(m_dragType); auto *drag = new QDrag(this); drag->setMimeData(model()->mimeData(indexes)); QModelIndex ix = indexes.constFirst(); if (ix.isValid()) { QIcon icon = ix.data(AbstractProjectItem::DataThumbnail).value(); QPixmap pix = icon.pixmap(iconSize()); QSize size = pix.size(); QImage image(size, QImage::Format_ARGB32_Premultiplied); image.fill(Qt::transparent); QPainter p(&image); p.setOpacity(0.7); p.drawPixmap(0, 0, pix); p.setOpacity(1); if (indexes.count() > 1) { QPalette palette; int radius = size.height() / 3; p.setBrush(palette.highlight()); p.setPen(palette.highlightedText().color()); p.drawEllipse(QPoint(size.width() / 2, size.height() / 2), radius, radius); p.drawText(size.width() / 2 - radius, size.height() / 2 - radius, 2 * radius, 2 * radius, Qt::AlignCenter, QString::number(indexes.count())); } p.end(); drag->setPixmap(QPixmap::fromImage(image)); } drag->exec(); emit processDragEnd(); return true; } SmallJobLabel::SmallJobLabel(QWidget *parent) : QPushButton(parent) { setFixedWidth(0); setFlat(true); m_timeLine = new QTimeLine(500, this); QObject::connect(m_timeLine, &QTimeLine::valueChanged, this, &SmallJobLabel::slotTimeLineChanged); QObject::connect(m_timeLine, &QTimeLine::finished, this, &SmallJobLabel::slotTimeLineFinished); hide(); } const QString SmallJobLabel::getStyleSheet(const QPalette &p) { KColorScheme scheme(p.currentColorGroup(), KColorScheme::Window); QColor bg = scheme.background(KColorScheme::LinkBackground).color(); QColor fg = scheme.foreground(KColorScheme::LinkText).color(); QString style = QStringLiteral("QPushButton {margin:3px;padding:2px;background-color: rgb(%1, %2, %3);border-radius: 4px;border: none;color: rgb(%4, %5, %6)}") .arg(bg.red()) .arg(bg.green()) .arg(bg.blue()) .arg(fg.red()) .arg(fg.green()) .arg(fg.blue()); bg = scheme.background(KColorScheme::ActiveBackground).color(); fg = scheme.foreground(KColorScheme::ActiveText).color(); style.append( QStringLiteral("\nQPushButton:hover {margin:3px;padding:2px;background-color: rgb(%1, %2, %3);border-radius: 4px;border: none;color: rgb(%4, %5, %6)}") .arg(bg.red()) .arg(bg.green()) .arg(bg.blue()) .arg(fg.red()) .arg(fg.green()) .arg(fg.blue())); return style; } void SmallJobLabel::setAction(QAction *action) { m_action = action; } void SmallJobLabel::slotTimeLineChanged(qreal value) { setFixedWidth(qMin(value * 2, qreal(1.0)) * sizeHint().width()); update(); } void SmallJobLabel::slotTimeLineFinished() { if (m_timeLine->direction() == QTimeLine::Forward) { // Show m_action->setVisible(true); } else { // Hide m_action->setVisible(false); setText(QString()); } } void SmallJobLabel::slotSetJobCount(int jobCount) { QMutexLocker lk(&m_locker); if (jobCount > 0) { // prepare animation setText(i18np("%1 job", "%1 jobs", jobCount)); setToolTip(i18np("%1 pending job", "%1 pending jobs", jobCount)); if (style()->styleHint(QStyle::SH_Widget_Animate, nullptr, this) != 0) { setFixedWidth(sizeHint().width()); m_action->setVisible(true); return; } if (m_action->isVisible()) { setFixedWidth(sizeHint().width()); update(); return; } setFixedWidth(0); m_action->setVisible(true); int wantedWidth = sizeHint().width(); setGeometry(-wantedWidth, 0, wantedWidth, height()); m_timeLine->setDirection(QTimeLine::Forward); if (m_timeLine->state() == QTimeLine::NotRunning) { m_timeLine->start(); } } else { if (style()->styleHint(QStyle::SH_Widget_Animate, nullptr, this) != 0) { setFixedWidth(0); m_action->setVisible(false); return; } // hide m_timeLine->setDirection(QTimeLine::Backward); if (m_timeLine->state() == QTimeLine::NotRunning) { m_timeLine->start(); } } } LineEventEater::LineEventEater(QObject *parent) : QObject(parent) { } bool LineEventEater::eventFilter(QObject *obj, QEvent *event) { switch (event->type()) { case QEvent::ShortcutOverride: if (((QKeyEvent *)event)->key() == Qt::Key_Escape) { emit clearSearchLine(); } break; case QEvent::Resize: // Workaround Qt BUG 54676 emit showClearButton(((QResizeEvent *)event)->size().width() > QFontMetrics(QApplication::font()).averageCharWidth() * 8); break; default: break; } return QObject::eventFilter(obj, event); } Bin::Bin(std::shared_ptr model, QWidget *parent) : QWidget(parent) , isLoading(false) , m_itemModel(std::move(model)) , m_itemView(nullptr) , m_binTreeViewDelegate(nullptr) , m_binListViewDelegate(nullptr) , m_doc(nullptr) , m_extractAudioAction(nullptr) , m_transcodeAction(nullptr) , m_clipsActionsMenu(nullptr) , m_inTimelineAction(nullptr) , m_listType((BinViewType)KdenliveSettings::binMode()) , m_iconSize(160, 90) , m_propertiesPanel(nullptr) , m_blankThumb() , m_filterGroup(this) , m_filterRateGroup(this) , m_filterTypeGroup(this) , m_invalidClipDialog(nullptr) , m_gainedFocus(false) , m_audioDuration(0) , m_processedAudio(0) { m_layout = new QVBoxLayout(this); // Create toolbar for buttons m_toolbar = new QToolBar(this); int size = style()->pixelMetric(QStyle::PM_SmallIconSize); QSize iconSize(size, size); m_toolbar->setIconSize(iconSize); m_toolbar->setToolButtonStyle(Qt::ToolButtonIconOnly); m_layout->addWidget(m_toolbar); // Tags panel m_tagsWidget = new TagWidget(this); connect(m_tagsWidget, &TagWidget::switchTag, this, &Bin::switchTag); connect(m_tagsWidget, &TagWidget::updateProjectTags, this, &Bin::updateTags); m_tagsWidget->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Maximum); m_layout->addWidget(m_tagsWidget); m_tagsWidget->setVisible(false); m_layout->setSpacing(0); m_layout->setContentsMargins(0, 0, 0, 0); // Search line m_searchLine = new QLineEdit(this); m_searchLine->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred); m_searchLine->setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)); // m_searchLine->setClearButtonEnabled(true); m_searchLine->setPlaceholderText(i18n("Search...")); m_searchLine->setFocusPolicy(Qt::ClickFocus); auto *leventEater = new LineEventEater(this); m_searchLine->installEventFilter(leventEater); connect(leventEater, &LineEventEater::clearSearchLine, m_searchLine, &QLineEdit::clear); connect(leventEater, &LineEventEater::showClearButton, this, &Bin::showClearButton); setFocusPolicy(Qt::ClickFocus); connect(m_itemModel.get(), &ProjectItemModel::refreshPanel, this, &Bin::refreshPanel); connect(m_itemModel.get(), &ProjectItemModel::refreshClip, this, &Bin::refreshClip); connect(m_itemModel.get(), static_cast(&ProjectItemModel::itemDropped), this, static_cast(&Bin::slotItemDropped)); connect(m_itemModel.get(), static_cast &, const QModelIndex &)>(&ProjectItemModel::itemDropped), this, static_cast &, const QModelIndex &)>(&Bin::slotItemDropped)); connect(m_itemModel.get(), &ProjectItemModel::effectDropped, this, &Bin::slotEffectDropped); connect(m_itemModel.get(), &ProjectItemModel::addTag, this, &Bin::slotTagDropped); connect(m_itemModel.get(), &QAbstractItemModel::dataChanged, this, &Bin::slotItemEdited); connect(this, &Bin::refreshPanel, this, &Bin::doRefreshPanel); // Zoom slider QWidget *container = new QWidget(this); auto *lay = new QHBoxLayout; m_slider = new QSlider(Qt::Horizontal, this); m_slider->setMaximumWidth(100); m_slider->setMinimumWidth(40); m_slider->setRange(0, 10); m_slider->setValue(KdenliveSettings::bin_zoom()); connect(m_slider, &QAbstractSlider::valueChanged, this, &Bin::slotSetIconSize); auto *tb1 = new QToolButton(this); tb1->setIcon(QIcon::fromTheme(QStringLiteral("zoom-in"))); connect(tb1, &QToolButton::clicked, [&]() { m_slider->setValue(qMin(m_slider->value() + 1, m_slider->maximum())); }); auto *tb2 = new QToolButton(this); tb2->setIcon(QIcon::fromTheme(QStringLiteral("zoom-out"))); connect(tb2, &QToolButton::clicked, [&]() { m_slider->setValue(qMax(m_slider->value() - 1, m_slider->minimum())); }); lay->addWidget(tb2); lay->addWidget(m_slider); lay->addWidget(tb1); container->setLayout(lay); auto *widgetslider = new QWidgetAction(this); widgetslider->setDefaultWidget(container); // View type KSelectAction *listType = new KSelectAction(QIcon::fromTheme(QStringLiteral("view-list-tree")), i18n("View Mode"), this); pCore->window()->actionCollection()->addAction(QStringLiteral("bin_view_mode"), listType); QAction *treeViewAction = listType->addAction(QIcon::fromTheme(QStringLiteral("view-list-tree")), i18n("Tree View")); listType->addAction(treeViewAction); treeViewAction->setData(BinTreeView); if (m_listType == treeViewAction->data().toInt()) { listType->setCurrentAction(treeViewAction); } pCore->window()->actionCollection()->addAction(QStringLiteral("bin_view_mode_tree"), treeViewAction); QAction *iconViewAction = listType->addAction(QIcon::fromTheme(QStringLiteral("view-list-icons")), i18n("Icon View")); iconViewAction->setData(BinIconView); if (m_listType == iconViewAction->data().toInt()) { listType->setCurrentAction(iconViewAction); } pCore->window()->actionCollection()->addAction(QStringLiteral("bin_view_mode_icon"), iconViewAction); // Sort menu m_sortDescend = new QAction(i18n("Descending"), this); m_sortDescend->setCheckable(true); m_sortDescend->setChecked(KdenliveSettings::binSorting() > 99); connect(m_sortDescend, &QAction::triggered, [&] () { if (m_sortGroup->checkedAction()) { int actionData = m_sortGroup->checkedAction()->data().toInt(); if ((m_itemView != nullptr) && m_listType == BinTreeView) { auto *view = static_cast(m_itemView); view->header()->setSortIndicator(actionData, m_sortDescend->isChecked() ? Qt::DescendingOrder : Qt::AscendingOrder); } else { m_proxyModel->sort(actionData, m_sortDescend->isChecked() ? Qt::DescendingOrder : Qt::AscendingOrder); } KdenliveSettings::setBinSorting(actionData + (m_sortDescend->isChecked() ? 100 : 0)); } }); QMenu *sort = new QMenu(i18n("Sort By"), this); int binSort = KdenliveSettings::binSorting() % 100; m_sortGroup = new QActionGroup(sort); QAction *sortByName = new QAction(i18n("Name"), m_sortGroup); sortByName->setCheckable(true); sortByName->setData(0); sortByName->setChecked(binSort == 0); QAction *sortByDate = new QAction(i18n("Date"), m_sortGroup); sortByDate->setCheckable(true); sortByDate->setData(1); sortByDate->setChecked(binSort == 1); QAction *sortByDesc = new QAction(i18n("Description"), m_sortGroup); sortByDesc->setCheckable(true); sortByDesc->setData(2); sortByDesc->setChecked(binSort == 2); QAction *sortByType = new QAction(i18n("Type"), m_sortGroup); sortByType->setCheckable(true); sortByType->setData(3); sortByType->setChecked(binSort == 3); QAction *sortByDuration = new QAction(i18n("Duration"), m_sortGroup); sortByDuration->setCheckable(true); sortByDuration->setData(5); sortByDuration->setChecked(binSort == 5); QAction *sortByInsert = new QAction(i18n("Insert Order"), m_sortGroup); sortByInsert->setCheckable(true); sortByInsert->setData(6); sortByInsert->setChecked(binSort == 6); QAction *sortByRating = new QAction(i18n("Rating"), m_sortGroup); sortByRating->setCheckable(true); sortByRating->setData(7); sortByRating->setChecked(binSort == 7); sort->addAction(sortByName); sort->addAction(sortByDate); sort->addAction(sortByDuration); sort->addAction(sortByRating); sort->addAction(sortByType); sort->addAction(sortByInsert); sort->addAction(sortByDesc); sort->addSeparator(); sort->addAction(m_sortDescend); connect(m_sortGroup, &QActionGroup::triggered, [&] (QAction *ac) { int actionData = ac->data().toInt(); if ((m_itemView != nullptr) && m_listType == BinTreeView) { auto *view = static_cast(m_itemView); view->header()->setSortIndicator(actionData, m_sortDescend->isChecked() ? Qt::DescendingOrder : Qt::AscendingOrder); } else { m_proxyModel->sort(actionData, m_sortDescend->isChecked() ? Qt::DescendingOrder : Qt::AscendingOrder); } KdenliveSettings::setBinSorting(actionData + (m_sortDescend->isChecked() ? 100 : 0)); }); QAction *disableEffects = new QAction(i18n("Disable Bin Effects"), this); connect(disableEffects, &QAction::triggered, [this](bool disable) { this->setBinEffectsEnabled(!disable); }); disableEffects->setIcon(QIcon::fromTheme(QStringLiteral("favorite"))); disableEffects->setData("disable_bin_effects"); disableEffects->setCheckable(true); disableEffects->setChecked(false); pCore->window()->actionCollection()->addAction(QStringLiteral("disable_bin_effects"), disableEffects); listType->setToolBarMode(KSelectAction::MenuMode); connect(listType, static_cast(&KSelectAction::triggered), this, &Bin::slotInitView); // Settings menu QMenu *settingsMenu = new QMenu(i18n("Settings"), this); settingsMenu->addAction(listType); QMenu *sliderMenu = new QMenu(i18n("Zoom"), this); sliderMenu->setIcon(QIcon::fromTheme(QStringLiteral("zoom-in"))); sliderMenu->addAction(widgetslider); settingsMenu->addMenu(sliderMenu); settingsMenu->addMenu(sort); // Column show / hide actions m_showDate = new QAction(i18n("Show date"), this); m_showDate->setCheckable(true); m_showDate->setData(1); connect(m_showDate, &QAction::triggered, this, &Bin::slotShowColumn); m_showDesc = new QAction(i18n("Show description"), this); m_showDesc->setCheckable(true); m_showDesc->setData(2); connect(m_showDesc, &QAction::triggered, this, &Bin::slotShowColumn); m_showRating = new QAction(i18n("Show rating"), this); m_showRating->setCheckable(true); m_showRating->setData(7); connect(m_showRating, &QAction::triggered, this, &Bin::slotShowColumn); settingsMenu->addAction(m_showDate); settingsMenu->addAction(m_showDesc); settingsMenu->addAction(m_showRating); settingsMenu->addAction(disableEffects); // Show tags panel m_tagAction = new QAction(QIcon::fromTheme(QStringLiteral("tag")), i18n("Tags Panel"), this); m_tagAction->setCheckable(true); m_toolbar->addAction(m_tagAction); connect(m_tagAction, &QAction::triggered, [&] (bool triggered) { if (triggered) { m_tagsWidget->setVisible(true); } else { m_tagsWidget->setVisible(false); } }); // Filter menu m_filterGroup.setExclusive(false); m_filterMenu = new QMenu(i18n("Filter"), this); m_filterButton = new QToolButton; m_filterButton->setCheckable(true); m_filterButton->setPopupMode(QToolButton::MenuButtonPopup); m_filterButton->setIcon(QIcon::fromTheme(QStringLiteral("view-filter"))); m_filterButton->setToolTip(i18n("Filter")); m_filterButton->setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)); m_filterButton->setMenu(m_filterMenu); connect(m_filterButton, &QToolButton::toggled, [this] (bool toggle) { if (!toggle) { m_proxyModel->slotClearSearchFilters(); return; } QList list = m_filterMenu->actions(); int rateFilters = 0; int typeFilters = 0; QStringList tagFilters; for (QAction *ac : list) { if (ac->isChecked()) { QString actionData = ac->data().toString(); if (actionData.startsWith(QLatin1Char('#'))) { // Filter by tag tagFilters << actionData; } else if (actionData.startsWith(QLatin1Char('.'))) { // Filter by rating rateFilters = actionData.remove(0, 1).toInt(); } } } // Type actions list = m_filterTypeGroup.actions(); for (QAction *ac : list) { if (ac->isChecked()) { typeFilters = ac->data().toInt(); break; } } QSignalBlocker bkt(m_filterButton); if (rateFilters > 0 || !tagFilters.isEmpty() ||typeFilters > 0) { m_filterButton->setChecked(true); } else { m_filterButton->setChecked(false); } m_proxyModel->slotSetFilters(tagFilters, rateFilters, typeFilters); }); connect(m_filterMenu, &QMenu::triggered, [this](QAction *action) { if (action->data().toString().isEmpty()) { // Clear filters action QSignalBlocker bk(m_filterMenu); QList list = m_filterMenu->actions(); list << m_filterTypeGroup.actions(); for (QAction *ac : list) { ac->setChecked(false); } m_proxyModel->slotClearSearchFilters(); m_filterButton->setChecked(false); return; } QList list = m_filterMenu->actions(); int rateFilters = 0; int typeFilters = 0; QStringList tagFilters; for (QAction *ac : list) { if (ac->isChecked()) { QString actionData = ac->data().toString(); if (actionData.startsWith(QLatin1Char('#'))) { // Filter by tag tagFilters << actionData; } else if (actionData.startsWith(QLatin1Char('.'))) { // Filter by rating rateFilters = actionData.remove(0, 1).toInt(); } } } // Type actions list = m_filterTypeGroup.actions(); for (QAction *ac : list) { if (ac->isChecked()) { typeFilters = ac->data().toInt(); break; } } QSignalBlocker bkt(m_filterButton); if (rateFilters > 0 || !tagFilters.isEmpty() ||typeFilters > 0) { m_filterButton->setChecked(true); } else { m_filterButton->setChecked(false); } m_proxyModel->slotSetFilters(tagFilters, rateFilters, typeFilters); }); m_tagAction->setCheckable(true); m_toolbar->addAction(m_tagAction); auto *button = new QToolButton; button->setIcon(QIcon::fromTheme(QStringLiteral("kdenlive-menu"))); button->setToolTip(i18n("Options")); button->setMenu(settingsMenu); button->setPopupMode(QToolButton::InstantPopup); m_toolbar->addWidget(button); // small info button for pending jobs m_infoLabel = new SmallJobLabel(this); m_infoLabel->setStyleSheet(SmallJobLabel::getStyleSheet(palette())); connect(pCore->jobManager().get(), &JobManager::jobCount, m_infoLabel, &SmallJobLabel::slotSetJobCount); QAction *infoAction = m_toolbar->addWidget(m_infoLabel); m_jobsMenu = new QMenu(this); // connect(m_jobsMenu, &QMenu::aboutToShow, this, &Bin::slotPrepareJobsMenu); m_cancelJobs = new QAction(i18n("Cancel All Jobs"), this); m_cancelJobs->setCheckable(false); m_discardCurrentClipJobs = new QAction(i18n("Cancel Current Clip Jobs"), this); m_discardCurrentClipJobs->setCheckable(false); m_discardPendingJobs = new QAction(i18n("Cancel Pending Jobs"), this); m_discardPendingJobs->setCheckable(false); m_jobsMenu->addAction(m_cancelJobs); m_jobsMenu->addAction(m_discardCurrentClipJobs); m_jobsMenu->addAction(m_discardPendingJobs); m_infoLabel->setMenu(m_jobsMenu); m_infoLabel->setAction(infoAction); connect(m_discardCurrentClipJobs, &QAction::triggered, [&]() { const QString currentId = m_monitor->activeClipId(); if (!currentId.isEmpty()) { pCore->jobManager()->discardJobs(currentId); } }); connect(m_cancelJobs, &QAction::triggered, [&]() { pCore->jobManager()->slotCancelJobs(); }); connect(m_discardPendingJobs, &QAction::triggered, [&]() { pCore->jobManager()->slotCancelPendingJobs(); }); // Hack, create toolbar spacer QWidget *spacer = new QWidget(); spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); m_toolbar->addWidget(spacer); // Add filter and search line m_toolbar->addWidget(m_filterButton); m_toolbar->addWidget(m_searchLine); // connect(pCore->projectManager(), SIGNAL(projectOpened(Project*)), this, SLOT(setProject(Project*))); m_headerInfo = QByteArray::fromBase64(KdenliveSettings::treeviewheaders().toLatin1()); m_propertiesPanel = new QScrollArea(this); m_propertiesPanel->setFrameShape(QFrame::NoFrame); // Insert listview m_itemView = new MyTreeView(this); m_layout->addWidget(m_itemView); // Info widget for failed jobs, other errors m_infoMessage = new KMessageWidget(this); m_layout->addWidget(m_infoMessage); m_infoMessage->setCloseButtonVisible(false); connect(m_infoMessage, &KMessageWidget::hideAnimationFinished, this, &Bin::slotResetInfoMessage); // m_infoMessage->setWordWrap(true); m_infoMessage->hide(); connect(this, &Bin::requesteInvalidRemoval, this, &Bin::slotQueryRemoval); connect(this, SIGNAL(displayBinMessage(QString, KMessageWidget::MessageType)), this, SLOT(doDisplayMessage(QString, KMessageWidget::MessageType))); wheelAccumulatedDelta = 0; } Bin::~Bin() { pCore->jobManager()->slotCancelJobs(); blockSignals(true); m_proxyModel->selectionModel()->blockSignals(true); setEnabled(false); m_propertiesPanel = nullptr; abortOperations(); m_itemModel->clean(); } QDockWidget *Bin::clipPropertiesDock() { return m_propertiesDock; } void Bin::abortOperations() { m_infoMessage->hide(); blockSignals(true); if (m_propertiesPanel) { for (QWidget *w : m_propertiesPanel->findChildren()) { delete w; } } delete m_itemView; m_itemView = nullptr; blockSignals(false); } bool Bin::eventFilter(QObject *obj, QEvent *event) { if (event->type() == QEvent::MouseButtonRelease) { if (!m_monitor->isActive()) { m_monitor->slotActivateMonitor(); } bool success = QWidget::eventFilter(obj, event); if (m_gainedFocus) { auto *mouseEvent = static_cast(event); auto *view = qobject_cast(obj->parent()); if (view) { QModelIndex idx = view->indexAt(mouseEvent->pos()); m_gainedFocus = false; if (idx.isValid() && m_proxyModel) { std::shared_ptr item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(idx)); if (item->itemType() == AbstractProjectItem::ClipItem) { auto clip = std::static_pointer_cast(item); if (clip && clip->isReady()) { editMasterEffect(item); } } else if (item->itemType() == AbstractProjectItem::SubClipItem) { auto clip = std::static_pointer_cast(item)->getMasterClip(); if (clip && clip->isReady()) { editMasterEffect(item); } } } else { editMasterEffect(nullptr); } } // make sure we discard the focus indicator m_gainedFocus = false; } return success; } if (event->type() == QEvent::MouseButtonDblClick) { auto *mouseEvent = static_cast(event); auto *view = qobject_cast(obj->parent()); if (view) { QModelIndex idx = view->indexAt(mouseEvent->pos()); if (!idx.isValid()) { // User double clicked on empty area slotAddClip(); } else { slotItemDoubleClicked(idx, mouseEvent->pos()); } } else { qCDebug(KDENLIVE_LOG) << " +++++++ NO VIEW-------!!"; } return true; } if (event->type() == QEvent::Wheel) { auto *e = static_cast(event); if ((e != nullptr) && e->modifiers() == Qt::ControlModifier) { wheelAccumulatedDelta += e->delta(); if (abs(wheelAccumulatedDelta) >= QWheelEvent::DefaultDeltasPerStep) { slotZoomView(wheelAccumulatedDelta > 0); } // emit zoomView(e->delta() > 0); return true; } } return QWidget::eventFilter(obj, event); } void Bin::refreshIcons() { QList allMenus = this->findChildren(); for (int i = 0; i < allMenus.count(); i++) { QMenu *m = allMenus.at(i); QIcon ic = m->icon(); if (ic.isNull() || ic.name().isEmpty()) { continue; } QIcon newIcon = QIcon::fromTheme(ic.name()); m->setIcon(newIcon); } QList allButtons = this->findChildren(); for (int i = 0; i < allButtons.count(); i++) { QToolButton *m = allButtons.at(i); QIcon ic = m->icon(); if (ic.isNull() || ic.name().isEmpty()) { continue; } QIcon newIcon = QIcon::fromTheme(ic.name()); m->setIcon(newIcon); } } void Bin::slotSaveHeaders() { if ((m_itemView != nullptr) && m_listType == BinTreeView) { // save current treeview state (column width) auto *view = static_cast(m_itemView); m_headerInfo = view->header()->saveState(); KdenliveSettings::setTreeviewheaders(m_headerInfo.toBase64()); } } void Bin::updateSortingAction(int ix) { for (QAction *ac : m_sortGroup->actions()) { if (ac->data().toInt() == ix) { ac->setChecked(true); } } } void Bin::slotZoomView(bool zoomIn) { wheelAccumulatedDelta = 0; if (m_itemModel->rowCount() == 0) { // Don't zoom on empty bin return; } int progress = (zoomIn) ? 1 : -1; m_slider->setValue(m_slider->value() + progress); } Monitor *Bin::monitor() { return m_monitor; } void Bin::slotAddClip() { // Check if we are in a folder QString parentFolder = getCurrentFolder(); ClipCreationDialog::createClipsCommand(m_doc, parentFolder, m_itemModel); } std::shared_ptr Bin::getFirstSelectedClip() { const QModelIndexList indexes = m_proxyModel->selectionModel()->selectedIndexes(); if (indexes.isEmpty()) { return std::shared_ptr(); } for (const QModelIndex &ix : indexes) { std::shared_ptr item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(ix)); if (item->itemType() == AbstractProjectItem::ClipItem) { auto clip = std::static_pointer_cast(item); if (clip) { return clip; } } } return nullptr; } void Bin::slotDeleteClip() { const QModelIndexList indexes = m_proxyModel->selectionModel()->selectedIndexes(); std::vector> items; bool included = false; bool usedFolder = false; auto checkInclusion = [](bool accum, std::shared_ptr item) { return accum || std::static_pointer_cast(item)->isIncludedInTimeline(); }; for (const QModelIndex &ix : indexes) { if (!ix.isValid() || ix.column() != 0) { continue; } std::shared_ptr item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(ix)); if (!item) { qDebug() << "Suspicious: item not found when trying to delete"; continue; } included = included || item->accumulate(false, checkInclusion); // Check if we are deleting non-empty folders: usedFolder = usedFolder || item->childCount() > 0; items.push_back(item); } if (included && (KMessageBox::warningContinueCancel(this, i18n("This will delete all selected clips from the timeline")) != KMessageBox::Continue)) { return; } if (usedFolder && (KMessageBox::warningContinueCancel(this, i18n("This will delete all folder content")) != KMessageBox::Continue)) { return; } Fun undo = []() { return true; }; Fun redo = []() { return true; }; for (const auto &item : items) { m_itemModel->requestBinClipDeletion(item, undo, redo); } pCore->pushUndo(undo, redo, i18n("Delete bin Clips")); } void Bin::slotReloadClip() { const QModelIndexList indexes = m_proxyModel->selectionModel()->selectedIndexes(); for (const QModelIndex &ix : indexes) { if (!ix.isValid() || ix.column() != 0) { continue; } std::shared_ptr item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(ix)); std::shared_ptr currentItem = nullptr; if (item->itemType() == AbstractProjectItem::ClipItem) { currentItem = std::static_pointer_cast(item); } else if (item->itemType() == AbstractProjectItem::SubClipItem) { currentItem = std::static_pointer_cast(item)->getMasterClip(); } if (currentItem) { emit openClip(std::shared_ptr()); if (currentItem->clipStatus() == AbstractProjectItem::StatusMissing) { // Don't attempt to reload missing clip emit displayBinMessage(i18n("Missing source clip"), KMessageWidget::Warning); return; } if (currentItem->clipType() == ClipType::Playlist) { // Check if a clip inside playlist is missing QString path = currentItem->url(); QFile f(path); QDomDocument doc; doc.setContent(&f, false); f.close(); DocumentChecker d(QUrl::fromLocalFile(path), doc); if (!d.hasErrorInClips() && doc.documentElement().hasAttribute(QStringLiteral("modified"))) { QString backupFile = path + QStringLiteral(".backup"); KIO::FileCopyJob *copyjob = KIO::file_copy(QUrl::fromLocalFile(path), QUrl::fromLocalFile(backupFile)); if (copyjob->exec()) { if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) { KMessageBox::sorry(this, i18n("Unable to write to file %1", path)); } else { QTextStream out(&f); out << doc.toString(); f.close(); KMessageBox::information( this, i18n("Your project file was modified by Kdenlive.\nTo make sure you do not lose data, a backup copy called %1 was created.", backupFile)); } } } } currentItem->reloadProducer(false, true); } } } void Bin::slotReplaceClip() { const QModelIndexList indexes = m_proxyModel->selectionModel()->selectedIndexes(); for (const QModelIndex &ix : indexes) { if (!ix.isValid() || ix.column() != 0) { continue; } std::shared_ptr item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(ix)); std::shared_ptr currentItem = nullptr; if (item->itemType() == AbstractProjectItem::ClipItem) { currentItem = std::static_pointer_cast(item); } else if (item->itemType() == AbstractProjectItem::SubClipItem) { currentItem = std::static_pointer_cast(item)->getMasterClip(); } if (currentItem) { emit openClip(std::shared_ptr()); QString fileName = QFileDialog::getOpenFileName(this, i18n("Open replacement file"), QFileInfo(currentItem->url()).absolutePath(), ClipCreationDialog::getExtensionsFilter()); if (!fileName.isEmpty()) { QMap sourceProps; QMap newProps; sourceProps.insert(QStringLiteral("resource"), currentItem->url()); sourceProps.insert(QStringLiteral("kdenlive:clipname"), currentItem->clipName()); newProps.insert(QStringLiteral("resource"), fileName); newProps.insert(QStringLiteral("kdenlive:clipname"), QFileInfo(fileName).fileName()); // Check if replacement clip is long enough if (currentItem->hasLimitedDuration() && currentItem->isIncludedInTimeline()) { // Clip is used in timeline, make sure lentgh is similar std::unique_ptr replacementProd(new Mlt::Producer(pCore->getCurrentProfile()->profile(), fileName.toUtf8().constData())); int currentDuration = (int)currentItem->frameDuration(); if (replacementProd->is_valid()) { int replacementDuration = replacementProd->get_length(); if (replacementDuration < currentDuration) { if (KMessageBox::warningContinueCancel(this, i18n("You are replacing a clip with a shorter one, this might cause issues in timeline.\nReplacement is %1 frames shorter.", (currentDuration - replacementDuration))) != KMessageBox::Continue) { continue; } } } else { KMessageBox::sorry(this, i18n("The selected file %1 is invalid.", fileName)); continue; } } slotEditClipCommand(currentItem->clipId(), sourceProps, newProps); } } } } void Bin::slotLocateClip() { const QModelIndexList indexes = m_proxyModel->selectionModel()->selectedIndexes(); for (const QModelIndex &ix : indexes) { if (!ix.isValid() || ix.column() != 0) { continue; } std::shared_ptr item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(ix)); std::shared_ptr currentItem = nullptr; if (item->itemType() == AbstractProjectItem::ClipItem) { currentItem = std::static_pointer_cast(item); } else if (item->itemType() == AbstractProjectItem::SubClipItem) { currentItem = std::static_pointer_cast(item)->getMasterClip(); } if (currentItem) { QUrl url = QUrl::fromLocalFile(currentItem->url()); //.adjusted(QUrl::RemoveFilename); bool exists = QFile(url.toLocalFile()).exists(); if (currentItem->hasUrl() && exists) { KIO::highlightInFileManager({url}); qCDebug(KDENLIVE_LOG) << " / / " + url.toString(); } else { if (!exists) { - pCore->displayMessage(i18n("Could not locate %1", url.toString()), ErrorMessage, 300); + pCore->displayMessage(i18n("Could not locate %1", url.toString()), MessageType::ErrorMessage, 300); } return; } } } } void Bin::slotDuplicateClip() { const QModelIndexList indexes = m_proxyModel->selectionModel()->selectedIndexes(); QList < std::shared_ptr > items; for (const QModelIndex &ix : indexes) { if (!ix.isValid() || ix.column() != 0) { continue; } items << m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(ix)); } QString lastId; for (auto item : items) { if (item->itemType() == AbstractProjectItem::ClipItem) { auto currentItem = std::static_pointer_cast(item); if (currentItem) { QDomDocument doc; QDomElement xml = currentItem->toXml(doc); if (!xml.isNull()) { QString currentName = Xml::getXmlProperty(xml, QStringLiteral("kdenlive:clipname")); if (currentName.isEmpty()) { QUrl url = QUrl::fromLocalFile(Xml::getXmlProperty(xml, QStringLiteral("resource"))); if (url.isValid()) { currentName = url.fileName(); } } if (!currentName.isEmpty()) { currentName.append(i18nc("append to clip name to indicate a copied idem", " (copy)")); Xml::setXmlProperty(xml, QStringLiteral("kdenlive:clipname"), currentName); } QString id; m_itemModel->requestAddBinClip(id, xml, item->parent()->clipId(), i18n("Duplicate clip")); lastId = id; } } } else if (item->itemType() == AbstractProjectItem::SubClipItem) { auto currentItem = std::static_pointer_cast(item); QString id; QPoint clipZone = currentItem->zone(); m_itemModel->requestAddBinSubClip(id, clipZone.x(), clipZone.y(), {}, currentItem->getMasterClip()->clipId()); lastId = id; } } if (!lastId.isEmpty()) { selectClipById(lastId); } } void Bin::setMonitor(Monitor *monitor) { m_monitor = monitor; connect(m_monitor, &Monitor::addClipToProject, this, &Bin::slotAddClipToProject); connect(m_monitor, &Monitor::refreshCurrentClip, this, &Bin::slotOpenCurrent); connect(this, &Bin::openClip, [&](std::shared_ptr clip, int in, int out) { m_monitor->slotOpenClip(clip, in, out); }); } void Bin::setDocument(KdenliveDoc *project) { blockSignals(true); if (m_proxyModel) { m_proxyModel->selectionModel()->blockSignals(true); } setEnabled(false); // Cleanup previous project m_itemModel->clean(); delete m_itemView; m_itemView = nullptr; m_doc = project; m_infoLabel->slotSetJobCount(0); int iconHeight = QFontInfo(font()).pixelSize() * 3.5; m_iconSize = QSize(iconHeight * pCore->getCurrentDar(), iconHeight); setEnabled(true); blockSignals(false); if (m_proxyModel) { m_proxyModel->selectionModel()->blockSignals(false); } // reset filtering QSignalBlocker bk(m_filterButton); m_filterButton->setChecked(false); m_filterButton->setToolTip(i18n("Filter")); connect(m_proxyAction, SIGNAL(toggled(bool)), m_doc, SLOT(slotProxyCurrentItem(bool))); // connect(m_itemModel, SIGNAL(dataChanged(QModelIndex,QModelIndex)), m_itemView // connect(m_itemModel, SIGNAL(updateCurrentItem()), this, SLOT(autoSelect())); slotInitView(nullptr); bool binEffectsDisabled = getDocumentProperty(QStringLiteral("disablebineffects")).toInt() == 1; setBinEffectsEnabled(!binEffectsDisabled); QMap projectTags = m_doc->getProjectTags(); m_tagsWidget->rebuildTags(projectTags); rebuildFilters(projectTags); } void Bin::rebuildFilters(QMap tags) { m_filterMenu->clear(); // Add tag filters QAction *clearFilter = new QAction(QIcon::fromTheme(QStringLiteral("edit-clear")), i18n("Clear filters"), this); m_filterMenu->addAction(clearFilter); int tagsCount = tags.size(); for (int i = 1; i <= tagsCount; i++) { QAction *tag = pCore->window()->actionCollection()->action(QString("tag_%1").arg(i)); if (tag) { QAction *tagFilter = new QAction(tag->icon(), tag->text(), &m_filterGroup); tagFilter->setData(tag->data()); tagFilter->setCheckable(true); m_filterMenu->addAction(tagFilter); } } // Add rating filters m_filterMenu->addSeparator(); QAction *rateFilter; for (int i = 1; i< 6; ++i) { rateFilter = new QAction(QIcon::fromTheme(QStringLiteral("favorite")), i18np("%1 star", "%1 stars", i), &m_filterRateGroup); rateFilter->setData(QString(".%1").arg(2 * i)); rateFilter->setCheckable(true); m_filterMenu->addAction(rateFilter); } // Add type filters m_filterMenu->addSeparator(); QMenu *typeMenu = new QMenu(i18n("Filter by type"), m_filterMenu); m_filterMenu->addMenu(typeMenu); m_filterMenu->addSeparator(); QAction *typeFilter = new QAction(QIcon::fromTheme(QStringLiteral("video-x-generic")), i18n("AV Clip"), &m_filterTypeGroup); typeFilter->setData(ClipType::AV); typeFilter->setCheckable(true); typeMenu->addAction(typeFilter); typeFilter = new QAction(QIcon::fromTheme(QStringLiteral("video-x-matroska")), i18n("Mute Video"), &m_filterTypeGroup); typeFilter->setData(ClipType::Video); typeFilter->setCheckable(true); typeMenu->addAction(typeFilter); typeFilter = new QAction(QIcon::fromTheme(QStringLiteral("audio-x-generic")), i18n("Audio"), &m_filterTypeGroup); typeFilter->setData(ClipType::Audio); typeFilter->setCheckable(true); typeMenu->addAction(typeFilter); typeFilter = new QAction(QIcon::fromTheme(QStringLiteral("image-jpeg")), i18n("Image"), &m_filterTypeGroup); typeFilter->setData(ClipType::Image); typeFilter->setCheckable(true); typeMenu->addAction(typeFilter); typeFilter = new QAction(QIcon::fromTheme(QStringLiteral("kdenlive-add-slide-clip")), i18n("Slideshow"), &m_filterTypeGroup); typeFilter->setData(ClipType::SlideShow); typeFilter->setCheckable(true); typeMenu->addAction(typeFilter); typeFilter = new QAction(QIcon::fromTheme(QStringLiteral("video-mlt-playlist")), i18n("Playlist"), &m_filterTypeGroup); typeFilter->setData(ClipType::Playlist); typeFilter->setCheckable(true); typeMenu->addAction(typeFilter); typeFilter = new QAction(QIcon::fromTheme(QStringLiteral("draw-text")), i18n("Title"), &m_filterTypeGroup); typeFilter->setData(ClipType::Text); typeFilter->setCheckable(true); typeMenu->addAction(typeFilter); typeFilter = new QAction(QIcon::fromTheme(QStringLiteral("draw-text")), i18n("Title Template"), &m_filterTypeGroup); typeFilter->setData(ClipType::TextTemplate); typeFilter->setCheckable(true); typeMenu->addAction(typeFilter); typeFilter = new QAction(QIcon::fromTheme(QStringLiteral("kdenlive-add-color-clip")), i18n("Color"), &m_filterTypeGroup); typeFilter->setData(ClipType::Color); typeFilter->setCheckable(true); typeMenu->addAction(typeFilter); } void Bin::createClip(const QDomElement &xml) { // Check if clip should be in a folder QString groupId = ProjectClip::getXmlProperty(xml, QStringLiteral("kdenlive:folderid")); std::shared_ptr parentFolder = m_itemModel->getFolderByBinId(groupId); if (!parentFolder) { parentFolder = m_itemModel->getRootFolder(); } QString path = Xml::getXmlProperty(xml, QStringLiteral("resource")); if (path.endsWith(QStringLiteral(".mlt")) || path.endsWith(QStringLiteral(".kdenlive"))) { QFile f(path); QDomDocument doc; doc.setContent(&f, false); f.close(); DocumentChecker d(QUrl::fromLocalFile(path), doc); if (!d.hasErrorInClips() && doc.documentElement().hasAttribute(QStringLiteral("modified"))) { QString backupFile = path + QStringLiteral(".backup"); KIO::FileCopyJob *copyjob = KIO::file_copy(QUrl::fromLocalFile(path), QUrl::fromLocalFile(backupFile)); if (copyjob->exec()) { if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) { KMessageBox::sorry(this, i18n("Unable to write to file %1", path)); } else { QTextStream out(&f); out << doc.toString(); f.close(); KMessageBox::information( this, i18n("Your project file was modified by Kdenlive.\nTo make sure you do not lose data, a backup copy called %1 was created.", backupFile)); } } } } QString id = Xml::getTagContentByAttribute(xml, QStringLiteral("property"), QStringLiteral("name"), QStringLiteral("kdenlive:id")); if (id.isEmpty()) { id = QString::number(m_itemModel->getFreeClipId()); } auto newClip = ProjectClip::construct(id, xml, m_blankThumb, m_itemModel); parentFolder->appendChild(newClip); } void Bin::slotAddFolder() { auto parentFolder = m_itemModel->getFolderByBinId(getCurrentFolder()); qDebug() << "parent folder id" << parentFolder->clipId(); QString newId; Fun undo = []() { return true; }; Fun redo = []() { return true; }; m_itemModel->requestAddFolder(newId, i18n("Folder"), parentFolder->clipId(), undo, redo); pCore->pushUndo(undo, redo, i18n("Create bin folder")); if (m_listType == BinTreeView) { // Make sure parent folder is expanded if (parentFolder->clipId().toInt() > -1) { auto ix = m_itemModel->getIndexFromItem(parentFolder); auto *view = static_cast(m_itemView); view->expand(m_proxyModel->mapFromSource(ix)); } } // Edit folder name auto folder = m_itemModel->getFolderByBinId(newId); auto ix = m_itemModel->getIndexFromItem(folder); // Scroll to ensure folder is visible m_itemView->scrollTo(m_proxyModel->mapFromSource(ix), QAbstractItemView::PositionAtCenter); qDebug() << "selecting" << ix; if (ix.isValid()) { qDebug() << "ix valid"; m_proxyModel->selectionModel()->clearSelection(); int row = ix.row(); const QModelIndex id = m_itemModel->index(row, 0, ix.parent()); const QModelIndex id2 = m_itemModel->index(row, m_itemModel->columnCount() - 1, ix.parent()); if (id.isValid() && id2.isValid()) { m_proxyModel->selectionModel()->select(QItemSelection(m_proxyModel->mapFromSource(id), m_proxyModel->mapFromSource(id2)), QItemSelectionModel::Select); } m_itemView->edit(m_proxyModel->mapFromSource(ix)); } } QModelIndex Bin::getIndexForId(const QString &id, bool folderWanted) const { QModelIndexList items = m_itemModel->match(m_itemModel->index(0, 0), AbstractProjectItem::DataId, QVariant::fromValue(id), 1, Qt::MatchRecursive); for (int i = 0; i < items.count(); i++) { std::shared_ptr currentItem = m_itemModel->getBinItemByIndex(items.at(i)); AbstractProjectItem::PROJECTITEMTYPE type = currentItem->itemType(); if (folderWanted && type == AbstractProjectItem::FolderItem) { // We found our folder return items.at(i); } if (!folderWanted && type == AbstractProjectItem::ClipItem) { // We found our clip return items.at(i); } } return {}; } void Bin::selectAll() { m_proxyModel->selectAll(); } void Bin::selectClipById(const QString &clipId, int frame, const QPoint &zone) { if (m_monitor->activeClipId() == clipId) { std::shared_ptr clip = m_itemModel->getClipByBinID(clipId); if (clip) { QModelIndex ix = m_itemModel->getIndexFromItem(clip); int row = ix.row(); const QModelIndex id = m_itemModel->index(row, 0, ix.parent()); const QModelIndex id2 = m_itemModel->index(row, m_itemModel->columnCount() - 1, ix.parent()); if (id.isValid() && id2.isValid()) { m_proxyModel->selectionModel()->select(QItemSelection(m_proxyModel->mapFromSource(id), m_proxyModel->mapFromSource(id2)), QItemSelectionModel::SelectCurrent); } m_itemView->scrollTo(m_proxyModel->mapFromSource(ix), QAbstractItemView::PositionAtCenter); } } else { m_proxyModel->selectionModel()->clearSelection(); std::shared_ptr clip = getBinClip(clipId); if (clip == nullptr) { return; } selectClip(clip); } if (frame > -1) { m_monitor->slotSeek(frame); } else { m_monitor->slotActivateMonitor(); } if (!zone.isNull()) { m_monitor->slotLoadClipZone(zone); } } void Bin::selectProxyModel(const QModelIndex &id) { if (isLoading) { // return; } if (id.isValid()) { if (id.column() != 0) { return; } std::shared_ptr currentItem = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(id)); if (currentItem) { // Set item as current so that it displays its content in clip monitor setCurrent(currentItem); if (currentItem->itemType() == AbstractProjectItem::ClipItem) { m_reloadAction->setEnabled(true); m_replaceAction->setEnabled(true); m_locateAction->setEnabled(true); m_duplicateAction->setEnabled(true); std::shared_ptr clip = std::static_pointer_cast(currentItem); m_tagsWidget->setTagData(clip->tags()); ClipType::ProducerType type = clip->clipType(); m_openAction->setEnabled(type == ClipType::Image || type == ClipType::Audio || type == ClipType::Text || type == ClipType::TextTemplate); showClipProperties(clip, false); m_deleteAction->setText(i18n("Delete Clip")); m_proxyAction->setText(i18n("Proxy Clip")); } else if (currentItem->itemType() == AbstractProjectItem::FolderItem) { // A folder was selected, disable editing clip m_tagsWidget->setTagData(); m_openAction->setEnabled(false); m_reloadAction->setEnabled(false); m_replaceAction->setEnabled(false); m_locateAction->setEnabled(false); m_duplicateAction->setEnabled(false); m_deleteAction->setText(i18n("Delete Folder")); m_proxyAction->setText(i18n("Proxy Folder")); } else if (currentItem->itemType() == AbstractProjectItem::SubClipItem) { m_tagsWidget->setTagData(currentItem->tags()); showClipProperties(std::static_pointer_cast(currentItem->parent()), false); m_openAction->setEnabled(false); m_reloadAction->setEnabled(false); m_replaceAction->setEnabled(false); m_locateAction->setEnabled(false); m_duplicateAction->setEnabled(false); m_deleteAction->setText(i18n("Delete Clip")); m_proxyAction->setText(i18n("Proxy Clip")); } m_deleteAction->setEnabled(true); m_renameAction->setEnabled(true); } else { m_reloadAction->setEnabled(false); m_replaceAction->setEnabled(false); m_locateAction->setEnabled(false); m_duplicateAction->setEnabled(false); m_openAction->setEnabled(false); m_deleteAction->setEnabled(false); m_renameAction->setEnabled(false); } } else { // No item selected in bin m_openAction->setEnabled(false); m_deleteAction->setEnabled(false); m_renameAction->setEnabled(false); showClipProperties(nullptr); emit requestClipShow(nullptr); // clear effect stack emit requestShowEffectStack(QString(), nullptr, QSize(), false); // Display black bg in clip monitor emit openClip(std::shared_ptr()); } } std::vector Bin::selectedClipsIds(bool allowSubClips) { const QModelIndexList indexes = m_proxyModel->selectionModel()->selectedIndexes(); std::vector ids; // We define the lambda that will be executed on each item of the subset of nodes of the tree that are selected auto itemAdder = [&ids, allowSubClips](std::vector &ids_vec, std::shared_ptr item) { auto binItem = std::static_pointer_cast(item); if (binItem->itemType() == AbstractProjectItem::ClipItem) { ids.push_back(binItem->clipId()); } else if (allowSubClips && binItem->itemType() == AbstractProjectItem::SubClipItem) { auto subClipItem = std::static_pointer_cast(item); ids.push_back(subClipItem->cutClipId()); } return ids_vec; }; for (const QModelIndex &ix : indexes) { if (!ix.isValid() || ix.column() != 0) { continue; } std::shared_ptr item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(ix)); item->accumulate(ids, itemAdder); } return ids; } QList> Bin::selectedClips() { auto ids = selectedClipsIds(); QList> ret; for (const auto &id : ids) { ret.push_back(m_itemModel->getClipByBinID(id)); } return ret; } void Bin::slotInitView(QAction *action) { if (action) { if (m_proxyModel) { m_proxyModel->selectionModel()->clearSelection(); } int viewType = action->data().toInt(); KdenliveSettings::setBinMode(viewType); if (viewType == m_listType) { return; } if (m_listType == BinTreeView) { // save current treeview state (column width) auto *view = static_cast(m_itemView); m_headerInfo = view->header()->saveState(); m_showDate->setEnabled(true); m_showDesc->setEnabled(true); m_showRating->setEnabled(true); m_upAction->setEnabled(false); } m_listType = static_cast(viewType); } if (m_itemView) { delete m_itemView; } delete m_binTreeViewDelegate; delete m_binListViewDelegate; m_binTreeViewDelegate = nullptr; m_binListViewDelegate = nullptr; switch (m_listType) { case BinIconView: m_itemView = new MyListView(this); m_binListViewDelegate = new BinListItemDelegate(this); m_showDate->setEnabled(false); m_showDesc->setEnabled(false); m_showRating->setEnabled(false); m_upAction->setVisible(true); break; default: m_itemView = new MyTreeView(this); m_binTreeViewDelegate = new BinItemDelegate(this); m_showDate->setEnabled(true); m_showDesc->setEnabled(true); m_showRating->setEnabled(true); m_upAction->setVisible(false); break; } m_itemView->setMouseTracking(true); m_itemView->viewport()->installEventFilter(this); QSize zoom = m_iconSize * (m_slider->value() / 4.0); m_itemView->setIconSize(zoom); QPixmap pix(zoom); pix.fill(Qt::lightGray); m_blankThumb.addPixmap(pix); m_proxyModel.reset(new ProjectSortProxyModel(this)); // Connect models m_proxyModel->setSourceModel(m_itemModel.get()); connect(m_itemModel.get(), &QAbstractItemModel::dataChanged, m_proxyModel.get(), &ProjectSortProxyModel::slotDataChanged); connect(m_proxyModel.get(), &ProjectSortProxyModel::updateRating, [&] (const QModelIndex &ix, uint rating) { const QModelIndex index = m_proxyModel->mapToSource(ix); std::shared_ptr item = m_itemModel->getBinItemByIndex(index); if (item) { uint previousRating = item->rating(); Fun undo = [this, item, index, previousRating]() { item->setRating(previousRating); emit m_itemModel->dataChanged(index, index, {AbstractProjectItem::DataRating}); return true; }; Fun redo = [this, item, index, rating]() { item->setRating(rating); emit m_itemModel->dataChanged(index, index, {AbstractProjectItem::DataRating}); return true; }; redo(); pCore->pushUndo(undo, redo, i18n("Edit rating")); } else { emit displayBinMessage(i18n("Cannot set rating on this item"), KMessageWidget::Information); } }); connect(m_proxyModel.get(), &ProjectSortProxyModel::selectModel, this, &Bin::selectProxyModel); connect(m_proxyModel.get(), &QAbstractItemModel::layoutAboutToBeChanged, this, &Bin::slotSetSorting); connect(m_searchLine, &QLineEdit::textChanged, m_proxyModel.get(), &ProjectSortProxyModel::slotSetSearchString); m_itemView->setModel(m_proxyModel.get()); m_itemView->setSelectionModel(m_proxyModel->selectionModel()); m_proxyModel->setDynamicSortFilter(true); m_layout->insertWidget(2, m_itemView); // Reset drag type to normal m_itemModel->setDragType(PlaylistState::Disabled); // setup some default view specific parameters if (m_listType == BinTreeView) { m_itemView->setItemDelegate(m_binTreeViewDelegate); auto *view = static_cast(m_itemView); view->setSortingEnabled(true); view->setWordWrap(true); connect(view, &MyTreeView::updateDragMode, m_itemModel.get(), &ProjectItemModel::setDragType, Qt::DirectConnection); connect(view, &MyTreeView::processDragEnd, this, &Bin::processDragEnd); connect(view, &MyTreeView::displayBinFrame, this, &Bin::showBinFrame); if (!m_headerInfo.isEmpty()) { view->header()->restoreState(m_headerInfo); } else { view->header()->resizeSections(QHeaderView::ResizeToContents); view->resizeColumnToContents(0); // Date Column view->setColumnHidden(1, true); // Description Column view->setColumnHidden(2, true); // Rating column view->setColumnHidden(7, true); } // Type column view->setColumnHidden(3, true); // Tags column view->setColumnHidden(4, true); // Duration column view->setColumnHidden(5, true); // ID column view->setColumnHidden(6, true); // Rating column view->header()->resizeSection(7, QFontInfo(font()).pixelSize() * 4); m_showDate->setChecked(!view->isColumnHidden(1)); m_showDesc->setChecked(!view->isColumnHidden(2)); m_showRating->setChecked(!view->isColumnHidden(7)); connect(view->header(), &QHeaderView::sectionResized, this, &Bin::slotSaveHeaders); connect(view->header(), &QHeaderView::sectionClicked, this, &Bin::slotSaveHeaders); connect(view, &MyTreeView::focusView, this, &Bin::slotGotFocus); } else if (m_listType == BinIconView) { m_itemView->setItemDelegate(m_binListViewDelegate); auto *view = static_cast(m_itemView); connect(view, &MyListView::updateDragMode, m_itemModel.get(), &ProjectItemModel::setDragType, Qt::DirectConnection); view->setGridSize(QSize(zoom.width() * 1.2, zoom.width())); connect(view, &MyListView::focusView, this, &Bin::slotGotFocus); connect(view, &MyListView::displayBinFrame, this, &Bin::showBinFrame); connect(view, &MyListView::processDragEnd, this, &Bin::processDragEnd); } m_itemView->setEditTriggers(QAbstractItemView::NoEditTriggers); // DoubleClicked); m_itemView->setSelectionMode(QAbstractItemView::ExtendedSelection); m_itemView->setDragDropMode(QAbstractItemView::DragDrop); m_itemView->setAlternatingRowColors(true); m_itemView->setFocus(); } void Bin::slotSetIconSize(int size) { if (!m_itemView) { return; } KdenliveSettings::setBin_zoom(size); QSize zoom = m_iconSize; zoom = zoom * (size / 4.0); m_itemView->setIconSize(zoom); if (m_listType == BinIconView) { auto *view = static_cast(m_itemView); view->setGridSize(QSize(zoom.width() * 1.2, zoom.width())); } QPixmap pix(zoom); pix.fill(Qt::lightGray); m_blankThumb.addPixmap(pix); } void Bin::rebuildMenu() { m_transcodeAction = static_cast(pCore->window()->factory()->container(QStringLiteral("transcoders"), pCore->window())); m_extractAudioAction = static_cast(pCore->window()->factory()->container(QStringLiteral("extract_audio"), pCore->window())); m_clipsActionsMenu = static_cast(pCore->window()->factory()->container(QStringLiteral("clip_actions"), pCore->window())); m_menu->insertMenu(m_reloadAction, m_extractAudioAction); m_menu->insertMenu(m_reloadAction, m_transcodeAction); m_menu->insertMenu(m_reloadAction, m_clipsActionsMenu); } void Bin::contextMenuEvent(QContextMenuEvent *event) { bool enableClipActions = false; ClipType::ProducerType type = ClipType::Unknown; bool isFolder = false; bool isImported = false; AbstractProjectItem::PROJECTITEMTYPE itemType = AbstractProjectItem::FolderItem; QString clipService; QString audioCodec; bool clickInView = false; if (m_itemView) { QRect viewRect(m_itemView->mapToGlobal(m_itemView->geometry().topLeft()), m_itemView->mapToGlobal(m_itemView->geometry().bottomRight())); if (viewRect.contains(event->globalPos())) { clickInView = true; QModelIndex idx = m_itemView->indexAt(m_itemView->viewport()->mapFromGlobal(event->globalPos())); if (idx.isValid()) { // User right clicked on a clip std::shared_ptr currentItem = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(idx)); itemType = currentItem->itemType(); if (currentItem) { enableClipActions = true; std::shared_ptr clip = nullptr; if (itemType == AbstractProjectItem::ClipItem) { clip = std::static_pointer_cast(currentItem); } else if (itemType == AbstractProjectItem::SubClipItem) { auto subClip = std::static_pointer_cast(currentItem); clip = subClip->getMasterClip(); } else if (itemType == AbstractProjectItem::FolderItem) { isFolder = true; } if (clip) { m_proxyAction->blockSignals(true); if (itemType == AbstractProjectItem::ClipItem) { emit findInTimeline(clip->clipId(), clip->timelineInstances()); } clipService = clip->getProducerProperty(QStringLiteral("mlt_service")); m_proxyAction->setChecked(clip->hasProxy()); QList transcodeActions; if (m_transcodeAction) { transcodeActions = m_transcodeAction->actions(); } QStringList dataList; QString condition; audioCodec = clip->codec(true); QString videoCodec = clip->codec(false); type = clip->clipType(); if (clip->hasUrl()) { isImported = true; } bool noCodecInfo = false; if (audioCodec.isEmpty() && videoCodec.isEmpty()) { noCodecInfo = true; } for (int i = 0; i < transcodeActions.count(); ++i) { dataList = transcodeActions.at(i)->data().toStringList(); if (dataList.count() > 4) { condition = dataList.at(4); if (condition.isEmpty()) { transcodeActions.at(i)->setEnabled(true); continue; } if (noCodecInfo) { // No audio / video codec, this is an MLT clip, disable conditionnal transcoding transcodeActions.at(i)->setEnabled(false); continue; } if (condition.startsWith(QLatin1String("vcodec"))) { transcodeActions.at(i)->setEnabled(condition.section(QLatin1Char('='), 1, 1) == videoCodec); } else if (condition.startsWith(QLatin1String("acodec"))) { transcodeActions.at(i)->setEnabled(condition.section(QLatin1Char('='), 1, 1) == audioCodec); } } } m_proxyAction->blockSignals(false); } else { // Disable find in timeline option emit findInTimeline(QString()); } } } } } if (!clickInView) { return; } // Enable / disable clip actions m_proxyAction->setEnabled((m_doc->getDocumentProperty(QStringLiteral("enableproxy")).toInt() != 0) && enableClipActions); m_openAction->setEnabled(type == ClipType::Image || type == ClipType::Audio || type == ClipType::TextTemplate || type == ClipType::Text); m_reloadAction->setEnabled(enableClipActions); m_replaceAction->setEnabled(enableClipActions); m_locateAction->setEnabled(enableClipActions); m_duplicateAction->setEnabled(enableClipActions); m_editAction->setVisible(!isFolder); m_clipsActionsMenu->setEnabled(enableClipActions); m_extractAudioAction->setEnabled(enableClipActions); m_openAction->setVisible(itemType != AbstractProjectItem::FolderItem); m_reloadAction->setVisible(itemType != AbstractProjectItem::FolderItem); m_replaceAction->setVisible(itemType == AbstractProjectItem::ClipItem); m_duplicateAction->setVisible(itemType != AbstractProjectItem::FolderItem); m_inTimelineAction->setVisible(itemType == AbstractProjectItem::ClipItem); if (m_transcodeAction) { m_transcodeAction->setEnabled(enableClipActions); m_transcodeAction->menuAction()->setVisible(itemType != AbstractProjectItem::FolderItem && (type == ClipType::Playlist || type == ClipType::Text || clipService.contains(QStringLiteral("avformat")))); } m_clipsActionsMenu->menuAction()->setVisible( itemType != AbstractProjectItem::FolderItem && (clipService.contains(QStringLiteral("avformat")) || clipService.contains(QStringLiteral("xml")) || clipService.contains(QStringLiteral("consumer")))); m_extractAudioAction->menuAction()->setVisible(!isFolder && !audioCodec.isEmpty()); m_locateAction->setVisible(itemType == AbstractProjectItem::ClipItem && (isImported)); // Show menu event->setAccepted(true); if (enableClipActions) { m_menu->exec(event->globalPos()); } else { // Clicked in empty area m_addButton->menu()->exec(event->globalPos()); } } void Bin::slotItemDoubleClicked(const QModelIndex &ix, const QPoint &pos) { std::shared_ptr item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(ix)); if (m_listType == BinIconView) { if (item->childCount() > 0 || item->itemType() == AbstractProjectItem::FolderItem) { m_itemView->setRootIndex(ix); m_upAction->setEnabled(true); return; } } else { if (ix.column() == 0 && item->childCount() > 0) { QRect IconRect = m_itemView->visualRect(ix); IconRect.setWidth((double)IconRect.height() / m_itemView->iconSize().height() * m_itemView->iconSize().width()); if (!pos.isNull() && (IconRect.contains(pos) || pos.y() > (IconRect.y() + IconRect.height() / 2))) { auto *view = static_cast(m_itemView); view->setExpanded(ix, !view->isExpanded(ix)); return; } } } if (ix.isValid()) { QRect IconRect = m_itemView->visualRect(ix); IconRect.setWidth((double)IconRect.height() / m_itemView->iconSize().height() * m_itemView->iconSize().width()); if (!pos.isNull() && ((ix.column() == 2 && item->itemType() == AbstractProjectItem::ClipItem) || (!IconRect.contains(pos) && pos.y() < (IconRect.y() + IconRect.height() / 2)))) { // User clicked outside icon, trigger rename m_itemView->edit(ix); return; } if (item->itemType() == AbstractProjectItem::ClipItem) { std::shared_ptr clip = std::static_pointer_cast(item); if (clip) { if (clip->clipType() == ClipType::Text || clip->clipType() == ClipType::TextTemplate) { // m_propertiesPanel->setEnabled(false); showTitleWidget(clip); } else { slotSwitchClipProperties(clip); } } } } } void Bin::slotEditClip() { QString panelId = m_propertiesPanel->property("clipId").toString(); QModelIndex current = m_proxyModel->selectionModel()->currentIndex(); std::shared_ptr item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(current)); if (item->clipId() != panelId) { // wrong clip return; } auto clip = std::static_pointer_cast(item); QString parentFolder = getCurrentFolder(); switch (clip->clipType()) { case ClipType::Text: case ClipType::TextTemplate: showTitleWidget(clip); break; case ClipType::SlideShow: showSlideshowWidget(clip); break; case ClipType::QText: ClipCreationDialog::createQTextClip(m_doc, parentFolder, this, clip.get()); break; default: break; } } void Bin::slotSwitchClipProperties() { QModelIndex current = m_proxyModel->selectionModel()->currentIndex(); if (current.isValid()) { // User clicked in the icon, open clip properties std::shared_ptr item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(current)); std::shared_ptr currentItem = nullptr; if (item->itemType() == AbstractProjectItem::ClipItem) { currentItem = std::static_pointer_cast(item); } else if (item->itemType() == AbstractProjectItem::SubClipItem) { currentItem = std::static_pointer_cast(item)->getMasterClip(); } if (currentItem) { slotSwitchClipProperties(currentItem); return; } } slotSwitchClipProperties(nullptr); } void Bin::slotSwitchClipProperties(const std::shared_ptr &clip) { if (clip == nullptr) { m_propertiesPanel->setEnabled(false); return; } if (clip->clipType() == ClipType::SlideShow) { m_propertiesPanel->setEnabled(false); showSlideshowWidget(clip); } else if (clip->clipType() == ClipType::QText) { m_propertiesPanel->setEnabled(false); QString parentFolder = getCurrentFolder(); ClipCreationDialog::createQTextClip(m_doc, parentFolder, this, clip.get()); } else { m_propertiesPanel->setEnabled(true); showClipProperties(clip); m_propertiesDock->show(); m_propertiesDock->raise(); } // Check if properties panel is not tabbed under Bin // if (!pCore->window()->isTabbedWith(m_propertiesDock, QStringLiteral("project_bin"))) { } void Bin::doRefreshPanel(const QString &id) { std::shared_ptr currentItem = getFirstSelectedClip(); if ((currentItem != nullptr) && currentItem->AbstractProjectItem::clipId() == id) { showClipProperties(currentItem, true); } } QAction *Bin::addAction(const QString &name, const QString &text, const QIcon &icon) { auto *action = new QAction(text, this); if (!icon.isNull()) { action->setIcon(icon); } pCore->window()->addAction(name, action); return action; } void Bin::setupAddClipAction(QMenu *addClipMenu, ClipType::ProducerType type, const QString &name, const QString &text, const QIcon &icon) { QAction *action = addAction(name, text, icon); action->setData(static_cast(type)); addClipMenu->addAction(action); connect(action, &QAction::triggered, this, &Bin::slotCreateProjectClip); } void Bin::showClipProperties(const std::shared_ptr &clip, bool forceRefresh) { if ((clip == nullptr) || !clip->isReady()) { for (QWidget *w : m_propertiesPanel->findChildren()) { delete w; } m_propertiesPanel->setProperty("clipId", QString()); m_propertiesPanel->setEnabled(false); emit setupTargets(false, {}); return; } m_propertiesPanel->setEnabled(true); QString panelId = m_propertiesPanel->property("clipId").toString(); if (!forceRefresh && panelId == clip->AbstractProjectItem::clipId()) { // the properties panel is already displaying current clip, do nothing return; } // Cleanup widget for new content for (QWidget *w : m_propertiesPanel->findChildren()) { delete w; } m_propertiesPanel->setProperty("clipId", clip->AbstractProjectItem::clipId()); // Setup timeline targets emit setupTargets(clip->hasVideo(), clip->activeStreams()); auto *lay = static_cast(m_propertiesPanel->layout()); if (lay == nullptr) { lay = new QVBoxLayout(m_propertiesPanel); m_propertiesPanel->setLayout(lay); } ClipPropertiesController *panel = clip->buildProperties(m_propertiesPanel); connect(this, &Bin::refreshTimeCode, panel, &ClipPropertiesController::slotRefreshTimeCode); connect(this, &Bin::deleteMarkers, panel, &ClipPropertiesController::slotDeleteSelectedMarkers); connect(this, &Bin::selectMarkers, panel, &ClipPropertiesController::slotSelectAllMarkers); connect(panel, &ClipPropertiesController::updateClipProperties, this, &Bin::slotEditClipCommand); connect(panel, &ClipPropertiesController::seekToFrame, m_monitor, static_cast(&Monitor::slotSeek)); connect(panel, &ClipPropertiesController::editClip, this, &Bin::slotEditClip); connect(panel, SIGNAL(editAnalysis(QString, QString, QString)), this, SLOT(slotAddClipExtraData(QString, QString, QString))); lay->addWidget(panel); } void Bin::slotEditClipCommand(const QString &id, const QMap &oldProps, const QMap &newProps) { auto *command = new EditClipCommand(this, id, oldProps, newProps, true); m_doc->commandStack()->push(command); } void Bin::reloadClip(const QString &id, bool reloadAudio) { std::shared_ptr clip = m_itemModel->getClipByBinID(id); if (!clip) { return; } clip->reloadProducer(false, false, reloadAudio); } void Bin::reloadMonitorIfActive(const QString &id) { if (m_monitor->activeClipId() == id || m_monitor->activeClipId().isEmpty()) { slotOpenCurrent(); } } void Bin::reloadMonitorStreamIfActive(const QString &id) { if (m_monitor->activeClipId() == id) { m_monitor->reloadActiveStream(); } } void Bin::updateTargets(const QString &id) { if (m_monitor->activeClipId() != id) { return; } std::shared_ptr clip = m_itemModel->getClipByBinID(id); if (clip) { emit setupTargets(clip->hasVideo(), clip->activeStreams()); } } QStringList Bin::getBinFolderClipIds(const QString &id) const { QStringList ids; std::shared_ptr folder = m_itemModel->getFolderByBinId(id); if (folder) { for (int i = 0; i < folder->childCount(); i++) { std::shared_ptr child = std::static_pointer_cast(folder->child(i)); if (child->itemType() == AbstractProjectItem::ClipItem) { ids << child->clipId(); } } } return ids; } std::shared_ptr Bin::getBinClip(const QString &id) { std::shared_ptr clip = nullptr; if (id.contains(QLatin1Char('_'))) { clip = m_itemModel->getClipByBinID(id.section(QLatin1Char('_'), 0, 0)); } else if (!id.isEmpty()) { clip = m_itemModel->getClipByBinID(id); } return clip; } void Bin::setWaitingStatus(const QString &id) { std::shared_ptr clip = m_itemModel->getClipByBinID(id); if (clip) { clip->setClipStatus(AbstractProjectItem::StatusWaiting); } } void Bin::slotRemoveInvalidClip(const QString &id, bool replace, const QString &errorMessage) { Q_UNUSED(replace); std::shared_ptr clip = m_itemModel->getClipByBinID(id); if (!clip) { return; } emit requesteInvalidRemoval(id, clip->url(), errorMessage); } void Bin::selectClip(const std::shared_ptr &clip) { QModelIndex ix = m_itemModel->getIndexFromItem(clip); int row = ix.row(); const QModelIndex id = m_itemModel->index(row, 0, ix.parent()); const QModelIndex id2 = m_itemModel->index(row, m_itemModel->columnCount() - 1, ix.parent()); if (id.isValid() && id2.isValid()) { m_proxyModel->selectionModel()->select(QItemSelection(m_proxyModel->mapFromSource(id), m_proxyModel->mapFromSource(id2)), QItemSelectionModel::SelectCurrent); } // Ensure parent folder is expanded if (m_listType == BinTreeView) { // Make sure parent folder is expanded auto *view = static_cast(m_itemView); view->expand(m_proxyModel->mapFromSource(ix.parent())); } m_itemView->scrollTo(m_proxyModel->mapFromSource(ix), QAbstractItemView::PositionAtCenter); } void Bin::slotOpenCurrent() { std::shared_ptr currentItem = getFirstSelectedClip(); if (currentItem) { emit openClip(currentItem); } } void Bin::openProducer(std::shared_ptr controller) { emit openClip(std::move(controller)); } void Bin::openProducer(std::shared_ptr controller, int in, int out) { emit openClip(std::move(controller), in, out); } void Bin::emitItemUpdated(std::shared_ptr item) { emit itemUpdated(std::move(item)); } void Bin::emitRefreshPanel(const QString &id) { emit refreshPanel(id); } void Bin::setupGeneratorMenu() { if (!m_menu) { qCDebug(KDENLIVE_LOG) << "Warning, menu was not created, something is wrong"; return; } auto *addMenu = qobject_cast(pCore->window()->factory()->container(QStringLiteral("generators"), pCore->window())); if (addMenu) { QMenu *menu = m_addButton->menu(); menu->addMenu(addMenu); addMenu->setEnabled(!addMenu->isEmpty()); m_addButton->setMenu(menu); } addMenu = qobject_cast(pCore->window()->factory()->container(QStringLiteral("extract_audio"), pCore->window())); if (addMenu) { m_menu->addMenu(addMenu); addMenu->setEnabled(!addMenu->isEmpty()); m_extractAudioAction = addMenu; } addMenu = qobject_cast(pCore->window()->factory()->container(QStringLiteral("transcoders"), pCore->window())); if (addMenu) { m_menu->addMenu(addMenu); addMenu->setEnabled(!addMenu->isEmpty()); m_transcodeAction = addMenu; } addMenu = qobject_cast(pCore->window()->factory()->container(QStringLiteral("clip_actions"), pCore->window())); if (addMenu) { m_menu->addMenu(addMenu); addMenu->setEnabled(!addMenu->isEmpty()); m_clipsActionsMenu = addMenu; } addMenu = qobject_cast(pCore->window()->factory()->container(QStringLiteral("clip_in_timeline"), pCore->window())); if (addMenu) { m_inTimelineAction = m_menu->addMenu(addMenu); } if (m_locateAction) { m_menu->addAction(m_locateAction); } if (m_reloadAction) { m_menu->addAction(m_reloadAction); } if (m_replaceAction) { m_menu->addAction(m_replaceAction); } if (m_duplicateAction) { m_menu->addAction(m_duplicateAction); } if (m_proxyAction) { m_menu->addAction(m_proxyAction); } addMenu = qobject_cast(pCore->window()->factory()->container(QStringLiteral("clip_timeline"), pCore->window())); if (addMenu) { m_menu->addMenu(addMenu); addMenu->setEnabled(false); } m_menu->addAction(m_editAction); m_menu->addAction(m_openAction); m_menu->addAction(m_renameAction); m_menu->addAction(m_deleteAction); m_menu->insertSeparator(m_deleteAction); } void Bin::setupMenu() { auto *addClipMenu = new QMenu(this); QAction *addClip = addAction(QStringLiteral("add_clip"), i18n("Add Clip or Folder"), QIcon::fromTheme(QStringLiteral("kdenlive-add-clip"))); addClipMenu->addAction(addClip); connect(addClip, &QAction::triggered, this, &Bin::slotAddClip); setupAddClipAction(addClipMenu, ClipType::Color, QStringLiteral("add_color_clip"), i18n("Add Color Clip"), QIcon::fromTheme(QStringLiteral("kdenlive-add-color-clip"))); setupAddClipAction(addClipMenu, ClipType::SlideShow, QStringLiteral("add_slide_clip"), i18n("Add Slideshow Clip"), QIcon::fromTheme(QStringLiteral("kdenlive-add-slide-clip"))); setupAddClipAction(addClipMenu, ClipType::Text, QStringLiteral("add_text_clip"), i18n("Add Title Clip"), QIcon::fromTheme(QStringLiteral("kdenlive-add-text-clip"))); setupAddClipAction(addClipMenu, ClipType::TextTemplate, QStringLiteral("add_text_template_clip"), i18n("Add Template Title"), QIcon::fromTheme(QStringLiteral("kdenlive-add-text-clip"))); QAction *downloadResourceAction = addAction(QStringLiteral("download_resource"), i18n("Online Resources"), QIcon::fromTheme(QStringLiteral("edit-download"))); addClipMenu->addAction(downloadResourceAction); connect(downloadResourceAction, &QAction::triggered, pCore->window(), &MainWindow::slotDownloadResources); m_locateAction = addAction(QStringLiteral("locate_clip"), i18n("Locate Clip..."), QIcon::fromTheme(QStringLiteral("find-location"))); m_locateAction->setData("locate_clip"); m_locateAction->setEnabled(false); connect(m_locateAction, &QAction::triggered, this, &Bin::slotLocateClip); m_reloadAction = addAction(QStringLiteral("reload_clip"), i18n("Reload Clip"), QIcon::fromTheme(QStringLiteral("view-refresh"))); m_reloadAction->setData("reload_clip"); m_reloadAction->setEnabled(false); connect(m_reloadAction, &QAction::triggered, this, &Bin::slotReloadClip); m_replaceAction = addAction(QStringLiteral("replace_clip"), i18n("Replace Clip"), QIcon::fromTheme(QStringLiteral("edit-find-replace"))); m_replaceAction->setData("replace_clip"); m_replaceAction->setEnabled(false); connect(m_replaceAction, &QAction::triggered, this, &Bin::slotReplaceClip); m_duplicateAction = addAction(QStringLiteral("duplicate_clip"), i18n("Duplicate Clip"), QIcon::fromTheme(QStringLiteral("edit-copy"))); m_duplicateAction->setData("duplicate_clip"); m_duplicateAction->setEnabled(false); connect(m_duplicateAction, &QAction::triggered, this, &Bin::slotDuplicateClip); m_proxyAction = new QAction(i18n("Proxy Clip"), pCore->window()); pCore->window()->addAction(QStringLiteral("proxy_clip"), m_proxyAction); m_proxyAction->setData(QStringList() << QString::number(static_cast(AbstractClipJob::PROXYJOB))); m_proxyAction->setCheckable(true); m_proxyAction->setChecked(false); m_editAction = addAction(QStringLiteral("clip_properties"), i18n("Clip Properties"), QIcon::fromTheme(QStringLiteral("document-edit"))); m_editAction->setData("clip_properties"); connect(m_editAction, &QAction::triggered, this, static_cast(&Bin::slotSwitchClipProperties)); m_openAction = addAction(QStringLiteral("edit_clip"), i18n("Edit Clip"), QIcon::fromTheme(QStringLiteral("document-open"))); m_openAction->setData("edit_clip"); m_openAction->setEnabled(false); connect(m_openAction, &QAction::triggered, this, &Bin::slotOpenClip); m_renameAction = KStandardAction::renameFile(this, SLOT(slotRenameItem()), pCore->window()->actionCollection()); m_renameAction->setEnabled(false); m_deleteAction = addAction(QStringLiteral("delete_clip"), i18n("Delete Clip"), QIcon::fromTheme(QStringLiteral("edit-delete"))); m_deleteAction->setData("delete_clip"); m_deleteAction->setEnabled(false); connect(m_deleteAction, &QAction::triggered, this, &Bin::slotDeleteClip); QAction *createFolder = addAction(QStringLiteral("create_folder"), i18n("Create Folder"), QIcon::fromTheme(QStringLiteral("folder-new"))); connect(createFolder, &QAction::triggered, this, &Bin::slotAddFolder); m_upAction = KStandardAction::up(this, SLOT(slotBack()), pCore->window()->actionCollection()); // Setup actions QAction *first = m_toolbar->actions().at(0); m_toolbar->insertAction(first, m_deleteAction); m_toolbar->insertAction(m_deleteAction, createFolder); m_toolbar->insertAction(createFolder, m_upAction); auto *m = new QMenu(this); m->addActions(addClipMenu->actions()); m_addButton = new QToolButton(this); m_addButton->setMenu(m); m_addButton->setDefaultAction(addClip); m_addButton->setPopupMode(QToolButton::MenuButtonPopup); m_toolbar->insertWidget(m_upAction, m_addButton); m_menu = new QMenu(this); m_propertiesDock = pCore->window()->addDock(i18n("Clip Properties"), QStringLiteral("clip_properties"), m_propertiesPanel); m_propertiesDock->close(); } const QString Bin::getDocumentProperty(const QString &key) { return m_doc->getDocumentProperty(key); } void Bin::slotUpdateJobStatus(const QString &id, int jobType, int status, const QString &label, const QString &actionName, const QString &details) { Q_UNUSED(id) Q_UNUSED(jobType) Q_UNUSED(status) Q_UNUSED(label) Q_UNUSED(actionName) Q_UNUSED(details) // TODO refac /* std::shared_ptr clip = m_itemModel->getClipByBinID(id); if (clip) { clip->setJobStatus((AbstractClipJob::JOBTYPE)jobType, (ClipJobStatus)status); } if (status == JobCrashed) { QList actions = m_infoMessage->actions(); if (m_infoMessage->isHidden()) { if (!details.isEmpty()) { m_infoMessage->setText(label + QStringLiteral(" ") + i18n("Show log") + QStringLiteral("")); } else { m_infoMessage->setText(label); } m_infoMessage->setWordWrap(m_infoMessage->text().length() > 35); m_infoMessage->setMessageType(KMessageWidget::Warning); } if (!actionName.isEmpty()) { QAction *action = nullptr; QList collections = KActionCollection::allCollections(); for (int i = 0; i < collections.count(); ++i) { KActionCollection *coll = collections.at(i); action = coll->action(actionName); if (action) { break; } } if ((action != nullptr) && !actions.contains(action)) { m_infoMessage->addAction(action); } } if (!details.isEmpty()) { m_errorLog.append(details); } m_infoMessage->setCloseButtonVisible(true); m_infoMessage->animatedShow(); } */ } -void Bin::doDisplayMessage(const QString &text, KMessageWidget::MessageType type, const QList &actions, bool showCloseButton) +void Bin::doDisplayMessage(const QString &text, KMessageWidget::MessageType type, const QList &actions, bool showCloseButton, BinMessage::BinCategory messageCategory) { // Remove existing actions if any QList acts = m_infoMessage->actions(); while (!acts.isEmpty()) { QAction *a = acts.takeFirst(); m_infoMessage->removeAction(a); delete a; } + m_currentMessage = messageCategory; m_infoMessage->setText(text); m_infoMessage->setWordWrap(text.length() > 35); for (QAction *action : actions) { m_infoMessage->addAction(action); connect(action, &QAction::triggered, this, &Bin::slotMessageActionTriggered); } m_infoMessage->setCloseButtonVisible(showCloseButton || actions.isEmpty()); m_infoMessage->setMessageType(type); m_infoMessage->animatedShow(); } void Bin::doDisplayMessage(const QString &text, KMessageWidget::MessageType type, const QString &logInfo) { // Remove existing actions if any + m_currentMessage = BinMessage::BinCategory::InformationMessage; QList acts = m_infoMessage->actions(); while (!acts.isEmpty()) { QAction *a = acts.takeFirst(); m_infoMessage->removeAction(a); delete a; } m_infoMessage->setText(text); m_infoMessage->setWordWrap(text.length() > 35); QAction *ac = new QAction(i18n("Show log"), this); m_infoMessage->addAction(ac); connect(ac, &QAction::triggered, [this, logInfo](bool) { KMessageBox::sorry(this, logInfo, i18n("Detailed log")); slotMessageActionTriggered(); }); m_infoMessage->setCloseButtonVisible(false); m_infoMessage->setMessageType(type); m_infoMessage->animatedShow(); } void Bin::refreshClip(const QString &id) { if (m_monitor->activeClipId() == id) { m_monitor->refreshMonitorIfActive(); } } void Bin::slotCreateProjectClip() { auto *act = qobject_cast(sender()); if (act == nullptr) { // Cannot access triggering action, something is wrong qCDebug(KDENLIVE_LOG) << "// Error in clip creation action"; return; } ClipType::ProducerType type = (ClipType::ProducerType)act->data().toInt(); QString parentFolder = getCurrentFolder(); switch (type) { case ClipType::Color: ClipCreationDialog::createColorClip(m_doc, parentFolder, m_itemModel); break; case ClipType::SlideShow: ClipCreationDialog::createSlideshowClip(m_doc, parentFolder, m_itemModel); break; case ClipType::Text: ClipCreationDialog::createTitleClip(m_doc, parentFolder, QString(), m_itemModel); break; case ClipType::TextTemplate: ClipCreationDialog::createTitleTemplateClip(m_doc, parentFolder, m_itemModel); break; case ClipType::QText: ClipCreationDialog::createQTextClip(m_doc, parentFolder, this); break; default: break; } } void Bin::slotItemDropped(const QStringList &ids, const QModelIndex &parent) { std::shared_ptr parentItem; if (parent.isValid()) { parentItem = m_itemModel->getBinItemByIndex(parent); parentItem = parentItem->getEnclosingFolder(false); } else { parentItem = m_itemModel->getRootFolder(); } auto *moveCommand = new QUndoCommand(); moveCommand->setText(i18np("Move Clip", "Move Clips", ids.count())); QStringList folderIds; for (const QString &id : ids) { if (id.contains(QLatin1Char('/'))) { // trying to move clip zone, not allowed. Ignore continue; } if (id.startsWith(QLatin1Char('#'))) { // moving a folder, keep it for later folderIds << id; continue; } std::shared_ptr currentItem = m_itemModel->getClipByBinID(id); if (!currentItem) { continue; } std::shared_ptr currentParent = currentItem->parent(); if (currentParent != parentItem) { // Item was dropped on a different folder new MoveBinClipCommand(this, id, currentParent->clipId(), parentItem->clipId(), moveCommand); } } if (!folderIds.isEmpty()) { for (QString id : folderIds) { id.remove(0, 1); std::shared_ptr currentItem = m_itemModel->getFolderByBinId(id); if (!currentItem) { continue; } std::shared_ptr currentParent = currentItem->parent(); if (currentParent != parentItem) { // Item was dropped on a different folder new MoveBinFolderCommand(this, id, currentParent->clipId(), parentItem->clipId(), moveCommand); } } } if (moveCommand->childCount() == 0) { - pCore->displayMessage(i18n("No valid clip to insert"), InformationMessage, 500); + pCore->displayMessage(i18n("No valid clip to insert"), MessageType::InformationMessage, 500); } else { m_doc->commandStack()->push(moveCommand); } } void Bin::slotAddEffect(QString id, const QStringList &effectData) { if (id.isEmpty()) { id = m_monitor->activeClipId(); } if (!id.isEmpty()) { std::shared_ptr clip = m_itemModel->getClipByBinID(id); if (clip) { if (effectData.count() == 4) { // Paste effect from another stack std::shared_ptr sourceStack = pCore->getItemEffectStack(effectData.at(1).toInt(), effectData.at(2).toInt()); clip->copyEffect(sourceStack, effectData.at(3).toInt()); } else { clip->addEffect(effectData.constFirst()); } return; } } - pCore->displayMessage(i18n("Select a clip to apply an effect"), InformationMessage, 500); + pCore->displayMessage(i18n("Select a clip to apply an effect"), MessageType::InformationMessage, 500); } void Bin::slotEffectDropped(const QStringList &effectData, const QModelIndex &parent) { if (parent.isValid()) { std::shared_ptr parentItem = m_itemModel->getBinItemByIndex(parent); if (parentItem->itemType() == AbstractProjectItem::FolderItem) { // effect not supported on folder items displayBinMessage(i18n("Cannot apply effects on folders"), KMessageWidget::Information); return; } int row = 0; QModelIndex parentIndex; if (parentItem->itemType() == AbstractProjectItem::SubClipItem) { // effect only supported on clip items parentItem = std::static_pointer_cast(parentItem)->getMasterClip(); QModelIndex ix = m_itemModel->getIndexFromItem(parentItem); row = ix.row(); parentIndex = ix.parent(); } else if (parentItem->itemType() == AbstractProjectItem::ClipItem) { // effect only supported on clip items row = parent.row(); parentIndex = parent.parent(); } bool res = false; if (effectData.count() == 4) { // Paste effect from another stack std::shared_ptr sourceStack = pCore->getItemEffectStack(effectData.at(1).toInt(), effectData.at(2).toInt()); res = std::static_pointer_cast(parentItem)->copyEffect(sourceStack, effectData.at(3).toInt()); } else { res = std::static_pointer_cast(parentItem)->addEffect(effectData.constFirst()); } if (!res) { - pCore->displayMessage(i18n("Cannot add effect to clip"), InformationMessage); + pCore->displayMessage(i18n("Cannot add effect to clip"), MessageType::InformationMessage); } else { m_proxyModel->selectionModel()->clearSelection(); const QModelIndex id = m_itemModel->index(row, 0, parentIndex); const QModelIndex id2 = m_itemModel->index(row, m_itemModel->columnCount() - 1, parentIndex); if (id.isValid() && id2.isValid()) { m_proxyModel->selectionModel()->select(QItemSelection(m_proxyModel->mapFromSource(id), m_proxyModel->mapFromSource(id2)), QItemSelectionModel::Select); } setCurrent(parentItem); } } } void Bin::slotTagDropped(const QString &tag, const QModelIndex &parent) { if (parent.isValid()) { std::shared_ptr parentItem = m_itemModel->getBinItemByIndex(parent); if (parentItem->itemType() == AbstractProjectItem::ClipItem || parentItem->itemType() == AbstractProjectItem::SubClipItem) { if (parentItem->itemType() == AbstractProjectItem::SubClipItem) { qDebug()<<"TAG DROPPED ON CLIPZOINE\n\n!!!!!!!!!!!!!!!"; } // effect only supported on clip/subclip items QString currentTag = parentItem->tags(); QMap oldProps; oldProps.insert(QStringLiteral("kdenlive:tags"), currentTag); QMap newProps; if (currentTag.isEmpty()) { currentTag = tag; } else if (!currentTag.contains(tag)) { currentTag.append(QStringLiteral(";") + tag); } newProps.insert(QStringLiteral("kdenlive:tags"), currentTag); slotEditClipCommand(parentItem->clipId(), oldProps, newProps); return; } if (parentItem->itemType() == AbstractProjectItem::FolderItem) { QList allClips; QList> children = std::static_pointer_cast(parentItem)->childClips(); for (auto &clp : children) { allClips <clipId(); } editTags(allClips, tag, true); return; } } - pCore->displayMessage(i18n("Select a clip to add a tag"), InformationMessage); + pCore->displayMessage(i18n("Select a clip to add a tag"), MessageType::InformationMessage); } void Bin::switchTag(const QString &tag, bool add) { const QModelIndexList indexes = m_proxyModel->selectionModel()->selectedIndexes(); if (indexes.isEmpty()) { - pCore->displayMessage(i18n("Select a clip to add a tag"), InformationMessage); + pCore->displayMessage(i18n("Select a clip to add a tag"), MessageType::InformationMessage); } // Check for folders QList allClips; for (const QModelIndex &ix : indexes) { if (!ix.isValid() || ix.column() != 0) { continue; } std::shared_ptr parentItem = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(ix)); if (parentItem->itemType() == AbstractProjectItem::FolderItem) { QList> children = std::static_pointer_cast(parentItem)->childClips(); for (auto &clp : children) { allClips <clipId(); } } else if (parentItem->itemType() != AbstractProjectItem::FolderItem) { allClips <clipId(); } } editTags(allClips, tag, add); } void Bin::updateTags(QMap tags) { rebuildFilters(tags); pCore->updateProjectTags(tags); } void Bin::editTags(QList allClips, const QString &tag, bool add) { for (const QString &id : allClips) { std::shared_ptr clip = m_itemModel->getItemByBinId(id); if (clip) { // effect only supported on clip/subclip items QString currentTag = clip->tags(); QMap oldProps; oldProps.insert(QStringLiteral("kdenlive:tags"), currentTag); QMap newProps; if (add) { if (currentTag.isEmpty()) { currentTag = tag; } else if (!currentTag.contains(tag)) { currentTag.append(QStringLiteral(";") + tag); } newProps.insert(QStringLiteral("kdenlive:tags"), currentTag); } else { QStringList tags = currentTag.split(QLatin1Char(';')); tags.removeAll(tag); newProps.insert(QStringLiteral("kdenlive:tags"), tags.join(QLatin1Char(';'))); } slotEditClipCommand(id, oldProps, newProps); } } } void Bin::editMasterEffect(const std::shared_ptr &clip) { if (m_gainedFocus) { // Widget just gained focus, updating stack is managed in the eventfilter event, not from item return; } if (clip) { if (clip->itemType() == AbstractProjectItem::ClipItem) { std::shared_ptr clp = std::static_pointer_cast(clip); emit requestShowEffectStack(clp->clipName(), clp->m_effectStack, clp->getFrameSize(), false); return; } if (clip->itemType() == AbstractProjectItem::SubClipItem) { if (auto ptr = clip->parentItem().lock()) { std::shared_ptr clp = std::static_pointer_cast(ptr); emit requestShowEffectStack(clp->clipName(), clp->m_effectStack, clp->getFrameSize(), false); } return; } } emit requestShowEffectStack(QString(), nullptr, QSize(), false); } void Bin::slotGotFocus() { m_gainedFocus = true; } void Bin::doMoveClip(const QString &id, const QString &newParentId) { std::shared_ptr currentItem = m_itemModel->getClipByBinID(id); if (!currentItem) { return; } std::shared_ptr currentParent = currentItem->parent(); std::shared_ptr newParent = m_itemModel->getFolderByBinId(newParentId); currentItem->changeParent(newParent); } void Bin::doMoveFolder(const QString &id, const QString &newParentId) { std::shared_ptr currentItem = m_itemModel->getFolderByBinId(id); std::shared_ptr currentParent = currentItem->parent(); std::shared_ptr newParent = m_itemModel->getFolderByBinId(newParentId); currentParent->removeChild(currentItem); currentItem->changeParent(newParent); } void Bin::droppedUrls(const QList &urls, const QString &folderInfo) { QModelIndex current; if (folderInfo.isEmpty()) { current = m_proxyModel->mapToSource(m_proxyModel->selectionModel()->currentIndex()); } else { // get index for folder std::shared_ptr folder = m_itemModel->getFolderByBinId(folderInfo); if (!folder) { folder = m_itemModel->getRootFolder(); } current = m_itemModel->getIndexFromItem(folder); } slotItemDropped(urls, current); } void Bin::slotAddClipToProject(const QUrl &url) { QList urls; urls << url; QModelIndex current = m_proxyModel->mapToSource(m_proxyModel->selectionModel()->currentIndex()); slotItemDropped(urls, current); } void Bin::slotItemDropped(const QList &urls, const QModelIndex &parent) { QString parentFolder = m_itemModel->getRootFolder()->clipId(); if (parent.isValid()) { // Check if drop occurred on a folder std::shared_ptr parentItem = m_itemModel->getBinItemByIndex(parent); while (parentItem->itemType() != AbstractProjectItem::FolderItem) { parentItem = parentItem->parent(); } parentFolder = parentItem->clipId(); } const QString id = ClipCreator::createClipsFromList(urls, true, parentFolder, m_itemModel); if (!id.isEmpty()) { std::shared_ptr item = m_itemModel->getItemByBinId(id); if (item) { QModelIndex ix = m_itemModel->getIndexFromItem(item); m_itemView->scrollTo(m_proxyModel->mapFromSource(ix), QAbstractItemView::PositionAtCenter); } } } void Bin::slotExpandUrl(const ItemInfo &info, const QString &url, QUndoCommand *command) { Q_UNUSED(info) Q_UNUSED(url) Q_UNUSED(command) // TODO reimplement this /* // Create folder to hold imported clips QString folderName = QFileInfo(url).fileName().section(QLatin1Char('.'), 0, 0); QString folderId = QString::number(getFreeFolderId()); new AddBinFolderCommand(this, folderId, folderName.isEmpty() ? i18n("Folder") : folderName, m_itemModel->getRootFolder()->clipId(), false, command); // Parse playlist clips QDomDocument doc; QFile file(url); doc.setContent(&file, false); file.close(); bool invalid = false; if (doc.documentElement().isNull()) { invalid = true; } QDomNodeList producers = doc.documentElement().elementsByTagName(QStringLiteral("producer")); QDomNodeList tracks = doc.documentElement().elementsByTagName(QStringLiteral("track")); if (invalid || producers.isEmpty()) { doDisplayMessage(i18n("Playlist clip %1 is invalid.", QFileInfo(url).fileName()), KMessageWidget::Warning); delete command; return; } if (tracks.count() > pCore->projectManager()->currentTimeline()->visibleTracksCount() + 1) { doDisplayMessage( i18n("Playlist clip %1 has too many tracks (%2) to be imported. Add new tracks to your project.", QFileInfo(url).fileName(), tracks.count()), KMessageWidget::Warning); delete command; return; } // Maps playlist producer IDs to (project) bin producer IDs. QMap idMap; // Maps hash IDs to (project) first playlist producer instance ID. This is // necessary to detect duplicate producer serializations produced by MLT. // This covers, for instance, images and titles. QMap hashToIdMap; QDir mltRoot(doc.documentElement().attribute(QStringLiteral("root"))); for (int i = 0; i < producers.count(); i++) { QDomElement prod = producers.at(i).toElement(); QString originalId = prod.attribute(QStringLiteral("id")); // track producer if (originalId.contains(QLatin1Char('_'))) { originalId = originalId.section(QLatin1Char('_'), 0, 0); } // slowmotion producer if (originalId.contains(QLatin1Char(':'))) { originalId = originalId.section(QLatin1Char(':'), 1, 1); } // We already have seen and mapped this producer. if (idMap.contains(originalId)) { continue; } // Check for duplicate producers, based on hash value of producer. // Be careful as to the kdenlive:file_hash! It is not unique for // title clips, nor color clips. Also not sure about image sequences. // So we use mlt service-specific hashes to identify duplicate producers. QString hash; QString mltService = Xml::getXmlProperty(prod, QStringLiteral("mlt_service")); if (mltService == QLatin1String("pixbuf") || mltService == QLatin1String("qimage") || mltService == QLatin1String("kdenlivetitle") || mltService == QLatin1String("color") || mltService == QLatin1String("colour")) { hash = mltService + QLatin1Char(':') + Xml::getXmlProperty(prod, QStringLiteral("kdenlive:clipname")) + QLatin1Char(':') + Xml::getXmlProperty(prod, QStringLiteral("kdenlive:folderid")) + QLatin1Char(':'); if (mltService == QLatin1String("kdenlivetitle")) { // Calculate hash based on title contents. hash.append( QString(QCryptographicHash::hash(Xml::getXmlProperty(prod, QStringLiteral("xmldata")).toUtf8(), QCryptographicHash::Md5).toHex())); } else if (mltService == QLatin1String("pixbuf") || mltService == QLatin1String("qimage") || mltService == QLatin1String("color") || mltService == QLatin1String("colour")) { hash.append(Xml::getXmlProperty(prod, QStringLiteral("resource"))); } QString singletonId = hashToIdMap.value(hash, QString()); if (singletonId.length() != 0) { // map duplicate producer ID to single bin clip producer ID. qCDebug(KDENLIVE_LOG) << "found duplicate producer:" << hash << ", reusing newID:" << singletonId; idMap.insert(originalId, singletonId); continue; } } // First occurrence of a producer, so allocate new bin clip producer ID. QString newId = QString::number(getFreeClipId()); idMap.insert(originalId, newId); qCDebug(KDENLIVE_LOG) << "originalId: " << originalId << ", newId: " << newId; // Ensure to register new bin clip producer ID in hash hashmap for // those clips that MLT likes to serialize multiple times. This is // indicated by having a hash "value" unqual "". See also above. if (hash.length() != 0) { hashToIdMap.insert(hash, newId); } // Add clip QDomElement clone = prod.cloneNode(true).toElement(); EffectsList::setProperty(clone, QStringLiteral("kdenlive:folderid"), folderId); // Do we have a producer that uses a resource property that contains a path? if (mltService == QLatin1String("avformat-novalidate") // av clip || mltService == QLatin1String("avformat") // av clip || mltService == QLatin1String("pixbuf") // image (sequence) clip || mltService == QLatin1String("qimage") // image (sequence) clip || mltService == QLatin1String("xml") // MLT playlist clip, someone likes recursion :) ) { // Make sure to correctly resolve relative resource paths based on // the playlist's root, not on this project's root QString resource = Xml::getXmlProperty(clone, QStringLiteral("resource")); if (QFileInfo(resource).isRelative()) { QFileInfo rootedResource(mltRoot, resource); qCDebug(KDENLIVE_LOG) << "fixed resource path for producer, newId:" << newId << "resource:" << rootedResource.absoluteFilePath(); EffectsList::setProperty(clone, QStringLiteral("resource"), rootedResource.absoluteFilePath()); } } ClipCreationDialog::createClipsCommand(this, clone, newId, command); } pCore->projectManager()->currentTimeline()->importPlaylist(info, idMap, doc, command); */ } void Bin::slotItemEdited(const QModelIndex &ix, const QModelIndex &, const QVector &roles) { if (ix.isValid() && roles.contains(AbstractProjectItem::DataName)) { // Clip renamed std::shared_ptr item = m_itemModel->getBinItemByIndex(ix); auto clip = std::static_pointer_cast(item); if (clip) { emit clipNameChanged(clip->AbstractProjectItem::clipId()); } } } void Bin::renameSubClipCommand(const QString &id, const QString &newName, const QString &oldName, int in, int out) { auto *command = new RenameBinSubClipCommand(this, id, newName, oldName, in, out); m_doc->commandStack()->push(command); } void Bin::renameSubClip(const QString &id, const QString &newName, int in, int out) { std::shared_ptr clip = m_itemModel->getClipByBinID(id); if (!clip) { return; } std::shared_ptr sub = clip->getSubClip(in, out); if (!sub) { return; } sub->setName(newName); clip->updateZones(); emit itemUpdated(sub); } Timecode Bin::projectTimecode() const { return m_doc->timecode(); } void Bin::slotStartFilterJob(const ItemInfo &info, const QString &id, QMap &filterParams, QMap &consumerParams, QMap &extraParams) { Q_UNUSED(info) Q_UNUSED(id) Q_UNUSED(filterParams) Q_UNUSED(consumerParams) Q_UNUSED(extraParams) // TODO refac /* std::shared_ptr clip = getBinClip(id); if (!clip) { return; } QMap producerParams = QMap(); producerParams.insert(QStringLiteral("producer"), clip->url()); if (info.cropDuration != GenTime()) { producerParams.insert(QStringLiteral("in"), QString::number((int)info.cropStart.frames(pCore->getCurrentFps()))); producerParams.insert(QStringLiteral("out"), QString::number((int)(info.cropStart + info.cropDuration).frames(pCore->getCurrentFps()))); extraParams.insert(QStringLiteral("clipStartPos"), QString::number((int)info.startPos.frames(pCore->getCurrentFps()))); extraParams.insert(QStringLiteral("clipTrack"), QString::number(info.track)); } else { // We want to process whole clip producerParams.insert(QStringLiteral("in"), QString::number(0)); producerParams.insert(QStringLiteral("out"), QString::number(-1)); } */ } void Bin::focusBinView() const { m_itemView->setFocus(); } void Bin::slotOpenClip() { std::shared_ptr clip = getFirstSelectedClip(); if (!clip) { return; } switch (clip->clipType()) { case ClipType::Text: case ClipType::TextTemplate: showTitleWidget(clip); break; case ClipType::Image: if (KdenliveSettings::defaultimageapp().isEmpty()) { KMessageBox::sorry(QApplication::activeWindow(), i18n("Please set a default application to open images in the Settings dialog")); } else { QProcess::startDetached(KdenliveSettings::defaultimageapp(), QStringList() << clip->url()); } break; case ClipType::Audio: if (KdenliveSettings::defaultaudioapp().isEmpty()) { KMessageBox::sorry(QApplication::activeWindow(), i18n("Please set a default application to open audio files in the Settings dialog")); } else { QProcess::startDetached(KdenliveSettings::defaultaudioapp(), QStringList() << clip->url()); } break; default: break; } } void Bin::updateTimecodeFormat() { emit refreshTimeCode(); } /* void Bin::slotGotFilterJobResults(const QString &id, int startPos, int track, const stringMap &results, const stringMap &filterInfo) { if (filterInfo.contains(QStringLiteral("finalfilter"))) { if (filterInfo.contains(QStringLiteral("storedata"))) { // Store returned data as clip extra data std::shared_ptr clip = getBinClip(id); if (clip) { QString key = filterInfo.value(QStringLiteral("key")); QStringList newValue = clip->updatedAnalysisData(key, results.value(key), filterInfo.value(QStringLiteral("offset")).toInt()); slotAddClipExtraData(id, newValue.at(0), newValue.at(1)); } } if (startPos == -1) { // Processing bin clip std::shared_ptr currentItem = m_itemModel->getClipByBinID(id); if (!currentItem) { return; } std::shared_ptr ctl = std::static_pointer_cast(currentItem); EffectsList list = ctl->effectList(); QDomElement effect = list.effectById(filterInfo.value(QStringLiteral("finalfilter"))); QDomDocument doc; QDomElement e = doc.createElement(QStringLiteral("test")); doc.appendChild(e); e.appendChild(doc.importNode(effect, true)); if (!effect.isNull()) { QDomElement newEffect = effect.cloneNode().toElement(); QMap::const_iterator i = results.constBegin(); while (i != results.constEnd()) { EffectsList::setParameter(newEffect, i.key(), i.value()); ++i; } ctl->updateEffect(newEffect, effect.attribute(QStringLiteral("kdenlive_ix")).toInt()); emit requestClipShow(currentItem); // TODO use undo / redo for bin clip edit effect // EditEffectCommand *command = new EditEffectCommand(this, clip->track(), clip->startPos(), effect, newEffect, clip->selectedEffectIndex(), true, true); m_commandStack->push(command); emit clipItemSelected(clip); } // emit gotFilterJobResults(id, startPos, track, results, filterInfo); return; } // This is a timeline filter, forward results emit gotFilterJobResults(id, startPos, track, results, filterInfo); return; } // Currently, only the first value of results is used std::shared_ptr clip = getBinClip(id); if (!clip) { return; } // Check for return value int markersType = -1; if (filterInfo.contains(QStringLiteral("addmarkers"))) { markersType = filterInfo.value(QStringLiteral("addmarkers")).toInt(); } if (results.isEmpty()) { emit displayBinMessage(i18n("No data returned from clip analysis"), KMessageWidget::Warning); return; } bool dataProcessed = false; QString label = filterInfo.value(QStringLiteral("label")); QString key = filterInfo.value(QStringLiteral("key")); int offset = filterInfo.value(QStringLiteral("offset")).toInt(); #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) QStringList value = results.value(key).split(QLatin1Char(';'), QString::SkipEmptyParts); #else QStringList value = results.value(key).split(QLatin1Char(';'), Qt::SkipEmptyParts); #endif // qCDebug(KDENLIVE_LOG)<<"// RESULT; "<setText(i18n("Auto Split Clip")); for (const QString &pos : value) { if (!pos.contains(QLatin1Char('='))) { continue; } int newPos = pos.section(QLatin1Char('='), 0, 0).toInt(); // Don't use scenes shorter than 1 second if (newPos - cutPos < 24) { continue; } new AddBinClipCutCommand(this, id, cutPos + offset, newPos + offset, true, command); cutPos = newPos; } if (command->childCount() == 0) { delete command; } else { m_doc->commandStack()->push(command); } } if (markersType >= 0) { // Add markers from returned data dataProcessed = true; int cutPos = 0; int index = 1; bool simpleList = false; double sourceFps = clip->getOriginalFps(); if (qFuzzyIsNull(sourceFps)) { sourceFps = pCore->getCurrentFps(); } if (filterInfo.contains(QStringLiteral("simplelist"))) { // simple list simpleList = true; } for (const QString &pos : value) { if (simpleList) { clip->getMarkerModel()->addMarker(GenTime((int)(pos.toInt() * pCore->getCurrentFps() / sourceFps), pCore->getCurrentFps()), label + pos, markersType); index++; continue; } if (!pos.contains(QLatin1Char('='))) { continue; } int newPos = pos.section(QLatin1Char('='), 0, 0).toInt(); // Don't use scenes shorter than 1 second if (newPos - cutPos < 24) { continue; } clip->getMarkerModel()->addMarker(GenTime(newPos + offset, pCore->getCurrentFps()), label + QString::number(index), markersType); index++; cutPos = newPos; } } if (!dataProcessed || filterInfo.contains(QStringLiteral("storedata"))) { // Store returned data as clip extra data QStringList newValue = clip->updatedAnalysisData(key, results.value(key), offset); slotAddClipExtraData(id, newValue.at(0), newValue.at(1)); } } */ // TODO: move title editing into a better place... void Bin::showTitleWidget(const std::shared_ptr &clip) { QString path = clip->getProducerProperty(QStringLiteral("resource")); QDir titleFolder(m_doc->projectDataFolder() + QStringLiteral("/titles")); titleFolder.mkpath(QStringLiteral(".")); TitleWidget dia_ui(QUrl(), m_doc->timecode(), titleFolder.absolutePath(), pCore->monitorManager()->projectMonitor(), pCore->window()); QDomDocument doc; QString xmldata = clip->getProducerProperty(QStringLiteral("xmldata")); if (xmldata.isEmpty() && QFile::exists(path)) { QFile file(path); doc.setContent(&file, false); file.close(); } else { doc.setContent(xmldata); } dia_ui.setXml(doc, clip->AbstractProjectItem::clipId()); if (dia_ui.exec() == QDialog::Accepted) { QMap newprops; newprops.insert(QStringLiteral("xmldata"), dia_ui.xml().toString()); if (dia_ui.duration() != clip->duration().frames(pCore->getCurrentFps())) { // duration changed, we need to update duration newprops.insert(QStringLiteral("out"), clip->framesToTime(dia_ui.duration() - 1)); int currentLength = clip->getProducerDuration(); if (currentLength != dia_ui.duration()) { newprops.insert(QStringLiteral("kdenlive:duration"), clip->framesToTime(dia_ui.duration())); } } // trigger producer reload newprops.insert(QStringLiteral("force_reload"), QStringLiteral("2")); if (!path.isEmpty()) { // we are editing an external file, asked if we want to detach from that file or save the result to that title file. if (KMessageBox::questionYesNo(pCore->window(), i18n("You are editing an external title clip (%1). Do you want to save your changes to the title " "file or save the changes for this project only?", path), i18n("Save Title"), KGuiItem(i18n("Save to title file")), KGuiItem(i18n("Save in project only"))) == KMessageBox::Yes) { // save to external file dia_ui.saveTitle(QUrl::fromLocalFile(path)); return; } else { newprops.insert(QStringLiteral("resource"), QString()); } } slotEditClipCommand(clip->AbstractProjectItem::clipId(), clip->currentProperties(newprops), newprops); } } void Bin::slotResetInfoMessage() { m_errorLog.clear(); + m_currentMessage = BinMessage::BinCategory::NoMessage; QList actions = m_infoMessage->actions(); for (int i = 0; i < actions.count(); ++i) { m_infoMessage->removeAction(actions.at(i)); } } void Bin::slotSetSorting() { if (m_listType == BinIconView) { m_proxyModel->setFilterKeyColumn(0); return; } auto *view = qobject_cast(m_itemView); if (view) { int ix = view->header()->sortIndicatorSection(); m_proxyModel->setFilterKeyColumn(ix); } } void Bin::slotShowColumn(bool show) { auto *act = qobject_cast(sender()); if (act == nullptr) { return; } auto *view = qobject_cast(m_itemView); if (view) { view->setColumnHidden(act->data().toInt(), !show); } } void Bin::slotQueryRemoval(const QString &id, const QString &url, const QString &errorMessage) { if (m_invalidClipDialog) { if (!url.isEmpty()) { m_invalidClipDialog->addClip(id, url); } return; } QString message = i18n("Clip is invalid, will be removed from project."); if (!errorMessage.isEmpty()) { message.append("\n" + errorMessage); } m_invalidClipDialog = new InvalidDialog(i18n("Invalid clip"), message, true, this); m_invalidClipDialog->addClip(id, url); int result = m_invalidClipDialog->exec(); if (result == QDialog::Accepted) { const QStringList ids = m_invalidClipDialog->getIds(); Fun undo = []() { return true; }; Fun redo = []() { return true; }; for (const QString &i : ids) { auto item = m_itemModel->getClipByBinID(i); m_itemModel->requestBinClipDeletion(item, undo, redo); } } delete m_invalidClipDialog; m_invalidClipDialog = nullptr; } void Bin::slotRefreshClipThumbnail(const QString &id) { std::shared_ptr clip = m_itemModel->getClipByBinID(id); if (!clip) { return; } clip->reloadProducer(true); } void Bin::slotAddClipExtraData(const QString &id, const QString &key, const QString &clipData, QUndoCommand *groupCommand) { std::shared_ptr clip = m_itemModel->getClipByBinID(id); if (!clip) { return; } QString oldValue = clip->getProducerProperty(key); QMap oldProps; oldProps.insert(key, oldValue); QMap newProps; newProps.insert(key, clipData); auto *command = new EditClipCommand(this, id, oldProps, newProps, true, groupCommand); if (!groupCommand) { m_doc->commandStack()->push(command); } } void Bin::slotUpdateClipProperties(const QString &id, const QMap &properties, bool refreshPropertiesPanel) { std::shared_ptr item = m_itemModel->getItemByBinId(id); if (item->itemType() == AbstractProjectItem::ClipItem) { std::shared_ptr clip = std::static_pointer_cast(item); if (clip) { clip->setProperties(properties, refreshPropertiesPanel); } } else if (item->itemType() == AbstractProjectItem::SubClipItem) { std::shared_ptr clip = std::static_pointer_cast(item); if (clip) { clip->setProperties(properties); } } } void Bin::showSlideshowWidget(const std::shared_ptr &clip) { QString folder = QFileInfo(clip->url()).absolutePath(); qCDebug(KDENLIVE_LOG) << " ** * CLIP ABS PATH: " << clip->url() << " = " << folder; SlideshowClip *dia = new SlideshowClip(m_doc->timecode(), folder, clip.get(), this); if (dia->exec() == QDialog::Accepted) { // edit clip properties QMap properties; properties.insert(QStringLiteral("out"), clip->framesToTime(m_doc->getFramePos(dia->clipDuration()) * dia->imageCount() - 1)); properties.insert(QStringLiteral("kdenlive:duration"), clip->framesToTime(m_doc->getFramePos(dia->clipDuration()) * dia->imageCount())); properties.insert(QStringLiteral("kdenlive:clipname"), dia->clipName()); properties.insert(QStringLiteral("ttl"), QString::number(m_doc->getFramePos(dia->clipDuration()))); properties.insert(QStringLiteral("loop"), QString::number(static_cast(dia->loop()))); properties.insert(QStringLiteral("crop"), QString::number(static_cast(dia->crop()))); properties.insert(QStringLiteral("fade"), QString::number(static_cast(dia->fade()))); properties.insert(QStringLiteral("luma_duration"), QString::number(m_doc->getFramePos(dia->lumaDuration()))); properties.insert(QStringLiteral("luma_file"), dia->lumaFile()); properties.insert(QStringLiteral("softness"), QString::number(dia->softness())); properties.insert(QStringLiteral("animation"), dia->animation()); QMap oldProperties; oldProperties.insert(QStringLiteral("out"), clip->getProducerProperty(QStringLiteral("out"))); oldProperties.insert(QStringLiteral("kdenlive:duration"), clip->getProducerProperty(QStringLiteral("kdenlive:duration"))); oldProperties.insert(QStringLiteral("kdenlive:clipname"), clip->name()); oldProperties.insert(QStringLiteral("ttl"), clip->getProducerProperty(QStringLiteral("ttl"))); oldProperties.insert(QStringLiteral("loop"), clip->getProducerProperty(QStringLiteral("loop"))); oldProperties.insert(QStringLiteral("crop"), clip->getProducerProperty(QStringLiteral("crop"))); oldProperties.insert(QStringLiteral("fade"), clip->getProducerProperty(QStringLiteral("fade"))); oldProperties.insert(QStringLiteral("luma_duration"), clip->getProducerProperty(QStringLiteral("luma_duration"))); oldProperties.insert(QStringLiteral("luma_file"), clip->getProducerProperty(QStringLiteral("luma_file"))); oldProperties.insert(QStringLiteral("softness"), clip->getProducerProperty(QStringLiteral("softness"))); oldProperties.insert(QStringLiteral("animation"), clip->getProducerProperty(QStringLiteral("animation"))); slotEditClipCommand(clip->AbstractProjectItem::clipId(), oldProperties, properties); } delete dia; } void Bin::setBinEffectsEnabled(bool enabled) { QAction *disableEffects = pCore->window()->actionCollection()->action(QStringLiteral("disable_bin_effects")); if (disableEffects) { if (enabled == disableEffects->isChecked()) { return; } disableEffects->blockSignals(true); disableEffects->setChecked(!enabled); disableEffects->blockSignals(false); } m_itemModel->setBinEffectsEnabled(enabled); pCore->projectManager()->disableBinEffects(!enabled); } void Bin::slotRenameItem() { if (!hasFocus() && !m_itemView->hasFocus()) { return; } const QModelIndexList indexes = m_proxyModel->selectionModel()->selectedRows(0); for (const QModelIndex &ix : indexes) { if (!ix.isValid()) { continue; } m_itemView->setCurrentIndex(ix); m_itemView->edit(ix); return; } } void Bin::refreshProxySettings() { QList> clipList = m_itemModel->getRootFolder()->childClips(); auto *masterCommand = new QUndoCommand(); masterCommand->setText(m_doc->useProxy() ? i18n("Enable proxies") : i18n("Disable proxies")); // en/disable proxy option in clip properties for (QWidget *w : m_propertiesPanel->findChildren()) { static_cast(w)->enableProxy(m_doc->useProxy()); } if (!m_doc->useProxy()) { // Disable all proxies m_doc->slotProxyCurrentItem(false, clipList, false, masterCommand); } else { QList> toProxy; for (const std::shared_ptr &clp : clipList) { ClipType::ProducerType t = clp->clipType(); if (t == ClipType::Playlist) { toProxy << clp; continue; } else if ((t == ClipType::AV || t == ClipType::Video) && m_doc->autoGenerateProxy(clp->getProducerIntProperty(QStringLiteral("meta.media.width")))) { // Start proxy toProxy << clp; continue; } else if (t == ClipType::Image && m_doc->autoGenerateImageProxy(clp->getProducerIntProperty(QStringLiteral("meta.media.width")))) { // Start proxy toProxy << clp; continue; } } if (!toProxy.isEmpty()) { m_doc->slotProxyCurrentItem(true, toProxy, false, masterCommand); } } if (masterCommand->childCount() > 0) { m_doc->commandStack()->push(masterCommand); } else { delete masterCommand; } } QStringList Bin::getProxyHashList() { QStringList list; QList> clipList = m_itemModel->getRootFolder()->childClips(); for (const std::shared_ptr &clp : clipList) { if (clp->clipType() == ClipType::AV || clp->clipType() == ClipType::Video || clp->clipType() == ClipType::Playlist) { list << clp->hash(); } } return list; } bool Bin::isEmpty() const { if (m_itemModel->getRootFolder() == nullptr) { return true; } return !m_itemModel->getRootFolder()->hasChildClips(); } void Bin::reloadAllProducers(bool reloadThumbs) { if (m_itemModel->getRootFolder() == nullptr || m_itemModel->getRootFolder()->childCount() == 0 || !isEnabled()) { return; } QList> clipList = m_itemModel->getRootFolder()->childClips(); emit openClip(std::shared_ptr()); for (const std::shared_ptr &clip : clipList) { QDomDocument doc; QDomElement xml = clip->toXml(doc, false, false); // Make sure we reload clip length if (clip->clipType() == ClipType::AV || clip->clipType() == ClipType::Video || clip->clipType() == ClipType::Audio || clip->clipType() == ClipType::Playlist) { xml.removeAttribute(QStringLiteral("out")); Xml::removeXmlProperty(xml, QStringLiteral("length")); } if (clip->isValid()) { clip->resetProducerProperty(QStringLiteral("kdenlive:duration")); clip->resetProducerProperty(QStringLiteral("length")); } if (!xml.isNull()) { clip->setClipStatus(AbstractProjectItem::StatusWaiting); pCore->jobManager()->slotDiscardClipJobs(clip->clipId()); clip->discardAudioThumb(); // We need to set a temporary id before all outdated producers are replaced; int jobId = pCore->jobManager()->startJob({clip->clipId()}, -1, QString(), xml); if (reloadThumbs) { ThumbnailCache::get()->invalidateThumbsForClip(clip->clipId(), true); } pCore->jobManager()->startJob({clip->clipId()}, jobId, QString(), -1, true, true); pCore->jobManager()->startJob({clip->clipId()}, jobId, QString()); } } } void Bin::checkAudioThumbs() { if (!KdenliveSettings::audiothumbnails() || m_itemModel->getRootFolder() == nullptr || m_itemModel->getRootFolder()->childCount() == 0 || !isEnabled()) { return; } QList> clipList = m_itemModel->getRootFolder()->childClips(); for (auto clip : clipList) { ClipType::ProducerType type = clip->clipType(); if (type == ClipType::AV || type == ClipType::Audio || type == ClipType::Playlist || type == ClipType::Unknown) { pCore->jobManager()->startJob({clip->clipId()}, -1, QString()); } } } void Bin::slotMessageActionTriggered() { m_infoMessage->animatedHide(); } void Bin::resetUsageCount() { const QList> clipList = m_itemModel->getRootFolder()->childClips(); for (const std::shared_ptr &clip : clipList) { clip->setRefCount(0); } } void Bin::getBinStats(uint *used, uint *unused, qint64 *usedSize, qint64 *unusedSize) { QList> clipList = m_itemModel->getRootFolder()->childClips(); for (const std::shared_ptr &clip : clipList) { if (clip->refCount() == 0) { *unused += 1; *unusedSize += clip->getProducerInt64Property(QStringLiteral("kdenlive:file_size")); } else { *used += 1; *usedSize += clip->getProducerInt64Property(QStringLiteral("kdenlive:file_size")); } } } QDir Bin::getCacheDir(CacheType type, bool *ok) const { return m_doc->getCacheDir(type, ok); } void Bin::rebuildProxies() { QList> clipList = m_itemModel->getRootFolder()->childClips(); QList> toProxy; for (const std::shared_ptr &clp : clipList) { if (clp->hasProxy()) { toProxy << clp; // Abort all pending jobs pCore->jobManager()->discardJobs(clp->clipId(), AbstractClipJob::PROXYJOB); clp->deleteProxy(); } } if (toProxy.isEmpty()) { return; } auto *masterCommand = new QUndoCommand(); masterCommand->setText(i18n("Rebuild proxies")); m_doc->slotProxyCurrentItem(true, toProxy, true, masterCommand); if (masterCommand->childCount() > 0) { m_doc->commandStack()->push(masterCommand); } else { delete masterCommand; } } void Bin::showClearButton(bool show) { m_searchLine->setClearButtonEnabled(show); } void Bin::saveZone(const QStringList &info, const QDir &dir) { if (info.size() != 3) { return; } std::shared_ptr clip = getBinClip(info.constFirst()); if (clip) { QPoint zone(info.at(1).toInt(), info.at(2).toInt()); clip->saveZone(zone, dir); } } void Bin::setCurrent(const std::shared_ptr &item) { switch (item->itemType()) { case AbstractProjectItem::ClipItem: { std::shared_ptr clp = std::static_pointer_cast(item); if (clp && clp->isReady()) { openProducer(clp); emit requestShowEffectStack(clp->clipName(), clp->m_effectStack, clp->getFrameSize(), false); } break; } case AbstractProjectItem::SubClipItem: { auto subClip = std::static_pointer_cast(item); QPoint zone = subClip->zone(); std::shared_ptr master = subClip->getMasterClip(); if (master && master->isReady()) { openProducer(master, zone.x(), zone.y()); } break; } case AbstractProjectItem::FolderItem: openProducer(nullptr); default: break; } } void Bin::cleanup() { m_itemModel->requestCleanup(); } std::shared_ptr Bin::getClipEffectStack(int itemId) { std::shared_ptr clip = m_itemModel->getClipByBinID(QString::number(itemId)); Q_ASSERT(clip != nullptr); std::shared_ptr effectStack = std::static_pointer_cast(clip)->m_effectStack; return effectStack; } size_t Bin::getClipDuration(int itemId) const { std::shared_ptr clip = m_itemModel->getClipByBinID(QString::number(itemId)); Q_ASSERT(clip != nullptr); return clip->frameDuration(); } PlaylistState::ClipState Bin::getClipState(int itemId) const { std::shared_ptr clip = m_itemModel->getClipByBinID(QString::number(itemId)); Q_ASSERT(clip != nullptr); bool audio = clip->hasAudio(); bool video = clip->hasVideo(); return audio ? (video ? PlaylistState::Disabled : PlaylistState::AudioOnly) : PlaylistState::VideoOnly; } QString Bin::getCurrentFolder() { // Check parent item QModelIndex ix = m_proxyModel->selectionModel()->currentIndex(); std::shared_ptr parentFolder = m_itemModel->getRootFolder(); if (ix.isValid() && m_proxyModel->selectionModel()->isSelected(ix)) { std::shared_ptr currentItem = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(ix)); parentFolder = std::static_pointer_cast(currentItem->getEnclosingFolder()); } return parentFolder->clipId(); } void Bin::adjustProjectProfileToItem() { QModelIndex current = m_proxyModel->selectionModel()->currentIndex(); if (current.isValid()) { // User clicked in the icon, open clip properties std::shared_ptr item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(current)); auto clip = std::static_pointer_cast(item); if (clip) { QDomDocument doc; LoadJob::checkProfile(clip->clipId(), clip->toXml(doc, false), clip->originalProducer()); } } } void Bin::showBinFrame(QModelIndex ix, int frame) { std::shared_ptr item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(ix)); if (item) { ClipType::ProducerType type = item->clipType(); if (type != ClipType::AV && type != ClipType::Video && type != ClipType::Playlist && type != ClipType::SlideShow) { return; } if (item->itemType() == AbstractProjectItem::ClipItem) { auto clip = std::static_pointer_cast(item); if (clip && (clip->clipType() == ClipType::AV || clip->clipType() == ClipType::Video || clip->clipType() == ClipType::Playlist)) { clip->getThumbFromPercent(frame); } } else if (item->itemType() == AbstractProjectItem::SubClipItem) { auto clip = std::static_pointer_cast(item); if (clip && (clip->clipType() == ClipType::AV || clip->clipType() == ClipType::Video || clip->clipType() == ClipType::Playlist)) { clip->getThumbFromPercent(frame); } } } } void Bin::invalidateClip(const QString &binId) { std::shared_ptr clip = getBinClip(binId); if (clip && clip->clipType() != ClipType::Audio) { QList ids = clip->timelineInstances(); for (int i : ids) { pCore->invalidateItem({ObjectType::TimelineClip,i}); } } } QSize Bin::sizeHint() const { return QSize(350, pCore->window()->height() / 2); } void Bin::slotBack() { QModelIndex currentRootIx = m_itemView->rootIndex(); if (!currentRootIx.isValid()) { return; } std::shared_ptr item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(currentRootIx)); if (!item) { qDebug()<<"=== ERRO CANNOT FIND ROOT FOR CURRENT VIEW"; return; } std::shared_ptr parentItem = item->parent(); if (!parentItem) { qDebug()<<"=== ERRO CANNOT FIND PARENT FOR CURRENT VIEW"; return; } if (parentItem != m_itemModel->getRootFolder()) { // We are entering a parent folder QModelIndex parentId = getIndexForId(parentItem->clipId(), parentItem->itemType() == AbstractProjectItem::FolderItem); if (parentId.isValid()) { m_itemView->setRootIndex(m_proxyModel->mapFromSource(parentId)); } } else { m_itemView->setRootIndex(QModelIndex()); m_upAction->setEnabled(false); } } -void Bin::checkProjectAudioTracks(int minimumTracksCount) +void Bin::checkProjectAudioTracks(QString clipId, int minimumTracksCount) { - if (m_infoMessage->isVisible()) { + if (m_currentMessage == BinMessage::BinCategory::ProfileMessage) { // Don't show this message if another one is active return; } int requestedTracks = minimumTracksCount - pCore->projectManager()->tracksCount().second; - const QString currentClipId = m_monitor->activeClipId(); if (requestedTracks > 0) { + if (clipId.isEmpty()) { + clipId = m_monitor->activeClipId(); + } QList list; QAction *ac = new QAction(QIcon::fromTheme(QStringLiteral("dialog-ok")), i18n("Add Tracks"), this); connect(ac, &QAction::triggered, [requestedTracks]() { pCore->projectManager()->addAudioTracks(requestedTracks); }); QAction *ac2 = new QAction(QIcon::fromTheme(QStringLiteral("document-edit")), i18n("Edit Streams"), this); - connect(ac2, &QAction::triggered, [this, currentClipId]() { - selectClipById(currentClipId); + connect(ac2, &QAction::triggered, [this, clipId]() { + selectClipById(clipId); for (QWidget *w : m_propertiesPanel->findChildren()) { if (w->parentWidget() && w->parentWidget()->parentWidget()) { // Raise panel w->parentWidget()->parentWidget()->raise(); } // Show audio tab static_cast(w)->activatePage(2); } }); QAction *ac3 = new QAction(QIcon::fromTheme(QStringLiteral("dialog-ok")), i18n("Don't ask again"), this); connect(ac3, &QAction::triggered, [&]() { KdenliveSettings::setMultistream_checktrack(false); }); //QAction *ac4 = new QAction(QIcon::fromTheme(QStringLiteral("dialog-cancel")), i18n("Cancel"), this); list << ac << ac2 << ac3; // << ac4; - doDisplayMessage(i18n("Your project needs more audio tracks to handle all streams. Add %1 audio tracks ?", requestedTracks), KMessageWidget::Information, list, true); + doDisplayMessage(i18n("Your project needs more audio tracks to handle all streams. Add %1 audio tracks ?", requestedTracks), KMessageWidget::Information, list, true, BinMessage::BinCategory::StreamsMessage); + } else if (m_currentMessage == BinMessage::BinCategory::StreamsMessage) { + // Clip streams number ok for the project, hide message + m_infoMessage->animatedHide(); } } diff --git a/src/bin/bin.h b/src/bin/bin.h index 89033225a..3604a8526 100644 --- a/src/bin/bin.h +++ b/src/bin/bin.h @@ -1,517 +1,521 @@ /* Copyright (C) 2012 Till Theato Copyright (C) 2014 Jean-Baptiste Mardelle This file is part of Kdenlive. See www.kdenlive.org. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef KDENLIVE_BIN_H #define KDENLIVE_BIN_H #include "abstractprojectitem.h" #include "timecode.h" #include #include #include #include #include #include #include #include #include #include #include #include #include class AbstractProjectItem; class BinItemDelegate; class BinListItemDelegate; class ClipController; class EffectStackModel; class InvalidDialog; class KdenliveDoc; class TagWidget; class Monitor; class ProjectClip; class ProjectFolder; class ProjectItemModel; class ProjectSortProxyModel; class QDockWidget; class QMenu; class QScrollArea; class QTimeLine; class QToolBar; class QToolButton; class QUndoCommand; class QVBoxLayout; class SmallJobLabel; namespace Mlt { class Producer; } class MyListView : public QListView { Q_OBJECT public: explicit MyListView(QWidget *parent = nullptr); protected: void mousePressEvent(QMouseEvent *event) override; void focusInEvent(QFocusEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override; signals: void focusView(); void updateDragMode(PlaylistState::ClipState type); void displayBinFrame(QModelIndex ix, int frame); void processDragEnd(); private: QPoint m_startPos; PlaylistState::ClipState m_dragType; }; class MyTreeView : public QTreeView { Q_OBJECT Q_PROPERTY(bool editing READ isEditing WRITE setEditing) public: explicit MyTreeView(QWidget *parent = nullptr); void setEditing(bool edit); protected: void mousePressEvent(QMouseEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override; void focusInEvent(QFocusEvent *event) override; protected slots: void closeEditor(QWidget *editor, QAbstractItemDelegate::EndEditHint hint) override; void editorDestroyed(QObject *editor) override; private: QPoint m_startPos; PlaylistState::ClipState m_dragType; bool m_editing; bool performDrag(); bool isEditing() const; signals: void focusView(); void updateDragMode(PlaylistState::ClipState type); void displayBinFrame(QModelIndex ix, int frame); void processDragEnd(); }; class SmallJobLabel : public QPushButton { Q_OBJECT public: explicit SmallJobLabel(QWidget *parent = nullptr); static const QString getStyleSheet(const QPalette &p); void setAction(QAction *action); private: enum ItemRole { NameRole = Qt::UserRole, DurationRole, UsageRole }; QTimeLine *m_timeLine; QAction *m_action{nullptr}; QMutex m_locker; public slots: void slotSetJobCount(int jobCount); private slots: void slotTimeLineChanged(qreal value); void slotTimeLineFinished(); }; class LineEventEater : public QObject { Q_OBJECT public: explicit LineEventEater(QObject *parent = nullptr); protected: bool eventFilter(QObject *obj, QEvent *event) override; signals: void clearSearchLine(); void showClearButton(bool); }; /** * @class Bin * @brief The bin widget takes care of both item model and view upon project opening. */ class Bin : public QWidget { Q_OBJECT /** @brief Defines the view types (icon view, tree view,...) */ enum BinViewType { BinTreeView, BinIconView }; public: explicit Bin(std::shared_ptr model, QWidget *parent = nullptr); ~Bin() override; bool isLoading; /** @brief Sets the document for the bin and initialize some stuff */ void setDocument(KdenliveDoc *project); /** @brief Create a clip item from its xml description */ void createClip(const QDomElement &xml); /** @brief Used to notify the Model View that an item was updated */ void emitItemUpdated(std::shared_ptr item); /** @brief Set monitor associated with this bin (clipmonitor) */ void setMonitor(Monitor *monitor); /** @brief Returns the clip monitor */ Monitor *monitor(); /** @brief Open a producer in the clip monitor */ void openProducer(std::shared_ptr controller); void openProducer(std::shared_ptr controller, int in, int out); /** @brief Get a clip from it's id */ std::shared_ptr getBinClip(const QString &id); /** @brief Returns a list of selected clip ids. * @param allowSubClips: if true, will include subclip ids in the form: "master clip id/in/out" */ std::vector selectedClipsIds(bool allowSubClips = false); // Returns the selected clips QList> selectedClips(); /** @brief Current producer has changed, refresh monitor and timeline*/ void refreshClip(const QString &id); void setupMenu(); /** @brief The source file was modified, we will reload it soon, disable item in the meantime */ void setWaitingStatus(const QString &id); const QString getDocumentProperty(const QString &key); /** @brief Ask MLT to reload this clip's producer */ void reloadClip(const QString &id, bool reloadAudio = true); /** @brief refresh monitor (if clip changed) */ void reloadMonitorIfActive(const QString &id); /** @brief refresh monitor stream selector */ void reloadMonitorStreamIfActive(const QString &id); /** @brief Update timeline targets according to selected audio streams */ void updateTargets(const QString &id); void doMoveClip(const QString &id, const QString &newParentId); void doMoveFolder(const QString &id, const QString &newParentId); void setupGeneratorMenu(); /** @brief Set focus to the Bin view. */ void focusBinView() const; /** @brief Get a string list of all clip ids that are inside a folder defined by id. */ QStringList getBinFolderClipIds(const QString &id) const; /** @brief Build a rename subclip command. */ void renameSubClipCommand(const QString &id, const QString &newName, const QString &oldName, int in, int out); /** @brief Rename a clip zone (subclip). */ void renameSubClip(const QString &id, const QString &newName, int in, int out); /** @brief Returns current project's timecode. */ Timecode projectTimecode() const; /** @brief Trigger timecode format refresh where needed. */ void updateTimecodeFormat(); /** @brief Edit an effect settings to a bin clip. */ void editMasterEffect(const std::shared_ptr &clip); /** @brief An effect setting was changed, update stack if displayed. */ void updateMasterEffect(ClipController *ctl); void rebuildMenu(); void refreshIcons(); /** @brief This function change the global enabled state of the bin effects */ void setBinEffectsEnabled(bool enabled); void requestAudioThumbs(const QString &id, long duration); /** @brief Proxy status for the project changed, update. */ void refreshProxySettings(); /** @brief A clip is ready, update its info panel if displayed. */ void emitRefreshPanel(const QString &id); /** @brief Returns true if there is no clip. */ bool isEmpty() const; /** @brief Trigger reload of all clips. */ void reloadAllProducers(bool reloadThumbs = true); /** @brief Ensure all audio thumbs have been created */ void checkAudioThumbs(); /** @brief Get usage stats for project bin. */ void getBinStats(uint *used, uint *unused, qint64 *usedSize, qint64 *unusedSize); /** @brief Returns the clip properties dockwidget. */ QDockWidget *clipPropertiesDock(); /** @brief Returns a document's cache dir. ok is set to false if folder does not exist */ QDir getCacheDir(CacheType type, bool *ok) const; void rebuildProxies(); /** @brief Return a list of all clips hashes used in this project */ QStringList getProxyHashList(); /** @brief Get binId of the current selected folder */ QString getCurrentFolder(); /** @brief Save a clip zone as MLT playlist */ void saveZone(const QStringList &info, const QDir &dir); /** @brief A bin clip changed (its effects), invalidate preview */ void invalidateClip(const QString &binId); // TODO refac: remove this and call directly the function in ProjectItemModel void cleanup(); void selectAll(); private slots: void slotAddClip(); /** @brief Reload clip from disk */ void slotReloadClip(); /** @brief Replace clip with another file */ void slotReplaceClip(); /** @brief Set sorting column */ void slotSetSorting(); /** @brief Show/hide date column */ void slotShowColumn(bool show); /** @brief Go to parent folder */ void slotBack(); /** @brief Setup the bin view type (icon view, tree view, ...). * @param action The action whose data defines the view type or nullptr to keep default view */ void slotInitView(QAction *action); /** @brief Update status for clip jobs */ void slotUpdateJobStatus(const QString &, int, int, const QString &label = QString(), const QString &actionName = QString(), const QString &details = QString()); void slotSetIconSize(int size); void selectProxyModel(const QModelIndex &id); void slotSaveHeaders(); void slotItemDropped(const QStringList &ids, const QModelIndex &parent); void slotItemDropped(const QList &urls, const QModelIndex &parent); void slotEffectDropped(const QStringList &effectData, const QModelIndex &parent); void slotTagDropped(const QString &tag, const QModelIndex &parent); void slotItemEdited(const QModelIndex &, const QModelIndex &, const QVector &); /** @brief Reset all text and log data from info message widget. */ void slotResetInfoMessage(); /** @brief Show dialog prompting for removal of invalid clips. */ void slotQueryRemoval(const QString &id, const QString &url, const QString &errorMessage); /** @brief Request display of current clip in monitor. */ void slotOpenCurrent(); void slotZoomView(bool zoomIn); /** @brief Widget gained focus, make sure we display effects for master clip. */ void slotGotFocus(); /** @brief Rename a Bin Item. */ void slotRenameItem(); void doRefreshPanel(const QString &id); /** @brief Enable item view and hide message */ void slotMessageActionTriggered(); /** @brief Request editing of title or slideshow clip */ void slotEditClip(); /** @brief Enable / disable clear button on search line * this is a workaround foq Qt bug 54676 */ void showClearButton(bool show); /** @brief Display a defined frame in bin clip thumbnail */ void showBinFrame(QModelIndex ix, int frame); /** @brief Switch a tag on/off on current selection */ void switchTag(const QString &tag, bool add); /** @brief Update project tags */ void updateTags(QMap tags); void rebuildFilters(QMap tags); /** @brief Switch a tag on a clip list */ void editTags(QList allClips, const QString &tag, bool add); public slots: void slotRemoveInvalidClip(const QString &id, bool replace, const QString &errorMessage); /** @brief Reload clip thumbnail - when frame for thumbnail changed */ void slotRefreshClipThumbnail(const QString &id); void slotDeleteClip(); void slotItemDoubleClicked(const QModelIndex &ix, const QPoint &pos); void slotSwitchClipProperties(const std::shared_ptr &clip); void slotSwitchClipProperties(); /** @brief Creates a new folder with optional name, and returns new folder's id */ void slotAddFolder(); void slotCreateProjectClip(); void slotEditClipCommand(const QString &id, const QMap &oldProps, const QMap &newProps); /** @brief Start a filter job requested by a filter applied in timeline */ void slotStartFilterJob(const ItemInfo &info, const QString &id, QMap &filterParams, QMap &consumerParams, QMap &extraParams); /** @brief Open current clip in an external editing application */ void slotOpenClip(); void slotDuplicateClip(); void slotLocateClip(); /** @brief Add extra data to a clip. */ void slotAddClipExtraData(const QString &id, const QString &key, const QString &data = QString(), QUndoCommand *groupCommand = nullptr); void slotUpdateClipProperties(const QString &id, const QMap &properties, bool refreshPropertiesPanel); /** @brief Add effect to active Bin clip (used when double clicking an effect in list). */ void slotAddEffect(QString id, const QStringList &effectData); void slotExpandUrl(const ItemInfo &info, const QString &url, QUndoCommand *command); /** @brief Abort all ongoing operations to prepare close. */ void abortOperations(); - void doDisplayMessage(const QString &text, KMessageWidget::MessageType type, const QList &actions = QList(), bool showCloseButton = false); + void doDisplayMessage(const QString &text, KMessageWidget::MessageType type, const QList &actions = QList(), bool showCloseButton = false, BinMessage::BinCategory messageCategory = BinMessage::BinCategory::NoMessage); void doDisplayMessage(const QString &text, KMessageWidget::MessageType type, const QString &logInfo); /** @brief Reset all clip usage to 0 */ void resetUsageCount(); /** @brief Select a clip in the Bin from its id. */ void selectClipById(const QString &id, int frame = -1, const QPoint &zone = QPoint()); void slotAddClipToProject(const QUrl &url); void droppedUrls(const QList &urls, const QString &folderInfo = QString()); /** @brief Returns the effectstack of a given clip. */ std::shared_ptr getClipEffectStack(int itemId); /** @brief Returns the duration of a given clip. */ size_t getClipDuration(int itemId) const; /** @brief Returns the state of a given clip: AudioOnly, VideoOnly, Disabled (Disabled means it has audio and video capabilities */ PlaylistState::ClipState getClipState(int itemId) const; /** @brief Adjust project profile to current clip. */ void adjustProjectProfileToItem(); - /** @brief Check and propose auto adding audio tracks. */ - void checkProjectAudioTracks(int minimumTracksCount); + /** @brief Check and propose auto adding audio tracks. + * @param clipId The clip whose streams have to be checked + * @param minimumTracksCount the number of active streams for this clip + */ + void checkProjectAudioTracks(QString clipId, int minimumTracksCount); protected: /* This function is called whenever an item is selected to propagate signals (for ex request to show the clip in the monitor) */ void setCurrent(const std::shared_ptr &item); void selectClip(const std::shared_ptr &clip); void contextMenuEvent(QContextMenuEvent *event) override; bool eventFilter(QObject *obj, QEvent *event) override; QSize sizeHint() const override; private: std::shared_ptr m_itemModel; QAbstractItemView *m_itemView; BinItemDelegate *m_binTreeViewDelegate; BinListItemDelegate *m_binListViewDelegate; std::unique_ptr m_proxyModel; QToolBar *m_toolbar; KdenliveDoc *m_doc; QLineEdit *m_searchLine; QToolButton *m_addButton; QMenu *m_extractAudioAction; QMenu *m_transcodeAction; QMenu *m_clipsActionsMenu; QAction *m_inTimelineAction; QAction *m_showDate; QAction *m_showDesc; QAction *m_showRating; QAction *m_sortDescend; /** @brief Default view type (icon, tree, ...) */ BinViewType m_listType; /** @brief Default icon size for the views. */ QSize m_iconSize; /** @brief Keeps the column width info of the tree view. */ QByteArray m_headerInfo; QVBoxLayout *m_layout; QDockWidget *m_propertiesDock; QScrollArea *m_propertiesPanel; QSlider *m_slider; Monitor *m_monitor; QIcon m_blankThumb; QMenu *m_menu; QAction *m_openAction; QAction *m_editAction; QAction *m_reloadAction; QAction *m_replaceAction; QAction *m_duplicateAction; QAction *m_locateAction; QAction *m_proxyAction; QAction *m_deleteAction; QAction *m_renameAction; QMenu *m_jobsMenu; QAction *m_cancelJobs; QAction *m_discardCurrentClipJobs; QAction *m_discardPendingJobs; QAction *m_upAction; QAction *m_tagAction; QActionGroup *m_sortGroup; SmallJobLabel *m_infoLabel; TagWidget *m_tagsWidget; QMenu *m_filterMenu; QActionGroup m_filterGroup; QActionGroup m_filterRateGroup; QActionGroup m_filterTypeGroup; QToolButton *m_filterButton; /** @brief The info widget for failed jobs. */ KMessageWidget *m_infoMessage; + BinMessage::BinCategory m_currentMessage; QStringList m_errorLog; InvalidDialog *m_invalidClipDialog; /** @brief Set to true if widget just gained focus (means we have to update effect stack . */ bool m_gainedFocus; /** @brief List of Clip Ids that want an audio thumb. */ QStringList m_audioThumbsList; QString m_processingAudioThumb; QMutex m_audioThumbMutex; /** @brief Total number of milliseconds to process for audio thumbnails */ long m_audioDuration; /** @brief Total number of milliseconds already processed for audio thumbnails */ long m_processedAudio; /** @brief Indicates whether audio thumbnail creation is running. */ QFuture m_audioThumbsThread; QAction *addAction(const QString &name, const QString &text, const QIcon &icon); void setupAddClipAction(QMenu *addClipMenu, ClipType::ProducerType type, const QString &name, const QString &text, const QIcon &icon); void showClipProperties(const std::shared_ptr &clip, bool forceRefresh = false); /** @brief Get the QModelIndex value for an item in the Bin. */ QModelIndex getIndexForId(const QString &id, bool folderWanted) const; std::shared_ptr getFirstSelectedClip(); void showTitleWidget(const std::shared_ptr &clip); void showSlideshowWidget(const std::shared_ptr &clip); void processAudioThumbs(); void updateSortingAction(int ix); int wheelAccumulatedDelta; signals: void itemUpdated(std::shared_ptr); void producerReady(const QString &id); /** @brief Save folder info into MLT. */ void storeFolder(const QString &folderId, const QString &parentId, const QString &oldParentId, const QString &folderName); void gotFilterJobResults(const QString &, int, int, stringMap, stringMap); /** @brief Trigger timecode format refresh where needed. */ void refreshTimeCode(); /** @brief Request display of effect stack for a Bin clip. */ void requestShowEffectStack(const QString &clipName, std::shared_ptr, QSize frameSize, bool showKeyframes); /** @brief Request that the given clip is displayed in the clip monitor */ void requestClipShow(std::shared_ptr); void displayBinMessage(const QString &, KMessageWidget::MessageType); void requesteInvalidRemoval(const QString &, const QString &, const QString &); /** @brief Analysis data changed, refresh panel. */ void updateAnalysisData(const QString &); void openClip(std::shared_ptr c, int in = -1, int out = -1); /** @brief Fill context menu with occurrences of this clip in timeline. */ void findInTimeline(const QString &, QList ids = QList()); void clipNameChanged(const QString &); /** @brief A clip was updated, request panel update. */ void refreshPanel(const QString &id); /** @brief Upon selection, activate timeline target tracks. */ void setupTargets(bool hasVideo, QMap audioStreams); /** @brief A drag event ended, inform timeline. */ void processDragEnd(); /** @brief Delete selected markers in clip properties dialog. */ void deleteMarkers(); /** @brief Selected all markers in clip properties dialog. */ void selectMarkers(); }; #endif diff --git a/src/bin/projectclip.cpp b/src/bin/projectclip.cpp index 3b4e86caf..823da024a 100644 --- a/src/bin/projectclip.cpp +++ b/src/bin/projectclip.cpp @@ -1,1592 +1,1589 @@ /* Copyright (C) 2012 Till Theato Copyright (C) 2014 Jean-Baptiste Mardelle This file is part of Kdenlive. See www.kdenlive.org. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "projectclip.h" #include "bin.h" #include "core.h" #include "doc/docundostack.hpp" #include "doc/kdenlivedoc.h" #include "doc/kthumb.h" #include "effects/effectstack/model/effectstackmodel.hpp" #include "jobs/audiothumbjob.hpp" #include "jobs/jobmanager.h" #include "jobs/loadjob.hpp" #include "jobs/thumbjob.hpp" #include "jobs/cachejob.hpp" #include "kdenlivesettings.h" #include "lib/audio/audioStreamInfo.h" #include "mltcontroller/clipcontroller.h" #include "mltcontroller/clippropertiescontroller.h" #include "model/markerlistmodel.hpp" #include "profiles/profilemodel.hpp" #include "project/projectcommands.h" #include "project/projectmanager.h" #include "projectfolder.h" #include "projectitemmodel.h" #include "projectsubclip.h" #include "timecode.h" #include "timeline2/model/snapmodel.hpp" #include "utils/thumbnailcache.hpp" #include "xml/xml.hpp" #include #include #include #include "kdenlive_debug.h" #include "logger.hpp" #include #include #include #include #include #include #include #include #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-parameter" #pragma GCC diagnostic ignored "-Wsign-conversion" #pragma GCC diagnostic ignored "-Wfloat-equal" #pragma GCC diagnostic ignored "-Wshadow" #pragma GCC diagnostic ignored "-Wpedantic" #include #pragma GCC diagnostic pop RTTR_REGISTRATION { using namespace rttr; registration::class_("ProjectClip"); } ProjectClip::ProjectClip(const QString &id, const QIcon &thumb, const std::shared_ptr &model, std::shared_ptr producer) : AbstractProjectItem(AbstractProjectItem::ClipItem, id, model) , ClipController(id, std::move(producer)) { m_markerModel = std::make_shared(id, pCore->projectManager()->undoStack()); if (producer->get_int("_placeholder") == 1 || producer->get_int("_missingsource") == 1) { m_clipStatus = StatusMissing; } else { m_clipStatus = StatusReady; } m_name = clipName(); m_duration = getStringDuration(); m_inPoint = 0; m_outPoint = 0; m_date = date; m_description = ClipController::description(); if (m_clipType == ClipType::Audio) { m_thumbnail = QIcon::fromTheme(QStringLiteral("audio-x-generic")); } else { m_thumbnail = thumb; } // Make sure we have a hash for this clip hash(); connect(m_markerModel.get(), &MarkerListModel::modelChanged, [&]() { setProducerProperty(QStringLiteral("kdenlive:markers"), m_markerModel->toJson()); }); QString markers = getProducerProperty(QStringLiteral("kdenlive:markers")); if (!markers.isEmpty()) { QMetaObject::invokeMethod(m_markerModel.get(), "importFromJson", Qt::QueuedConnection, Q_ARG(const QString &, markers), Q_ARG(bool, true), Q_ARG(bool, false)); } setTags(getProducerProperty(QStringLiteral("kdenlive:tags"))); AbstractProjectItem::setRating((uint) getProducerIntProperty(QStringLiteral("kdenlive:rating"))); connectEffectStack(); } // static std::shared_ptr ProjectClip::construct(const QString &id, const QIcon &thumb, const std::shared_ptr &model, const std::shared_ptr &producer) { std::shared_ptr self(new ProjectClip(id, thumb, model, producer)); baseFinishConstruct(self); QMetaObject::invokeMethod(model.get(), "loadSubClips", Qt::QueuedConnection, Q_ARG(const QString&, id), Q_ARG(const QString&, self->getProducerProperty(QStringLiteral("kdenlive:clipzones")))); return self; } void ProjectClip::importEffects(const std::shared_ptr &producer) { m_effectStack->importEffects(producer, PlaylistState::Disabled, true); } ProjectClip::ProjectClip(const QString &id, const QDomElement &description, const QIcon &thumb, const std::shared_ptr &model) : AbstractProjectItem(AbstractProjectItem::ClipItem, id, model) , ClipController(id) { m_clipStatus = StatusWaiting; m_thumbnail = thumb; m_markerModel = std::make_shared(m_binId, pCore->projectManager()->undoStack()); if (description.hasAttribute(QStringLiteral("type"))) { m_clipType = (ClipType::ProducerType)description.attribute(QStringLiteral("type")).toInt(); if (m_clipType == ClipType::Audio) { m_thumbnail = QIcon::fromTheme(QStringLiteral("audio-x-generic")); } } m_temporaryUrl = getXmlProperty(description, QStringLiteral("resource")); QString clipName = getXmlProperty(description, QStringLiteral("kdenlive:clipname")); if (!clipName.isEmpty()) { m_name = clipName; } else if (!m_temporaryUrl.isEmpty()) { m_name = QFileInfo(m_temporaryUrl).fileName(); } else { m_name = i18n("Untitled"); } connect(m_markerModel.get(), &MarkerListModel::modelChanged, [&]() { setProducerProperty(QStringLiteral("kdenlive:markers"), m_markerModel->toJson()); }); } std::shared_ptr ProjectClip::construct(const QString &id, const QDomElement &description, const QIcon &thumb, std::shared_ptr model) { std::shared_ptr self(new ProjectClip(id, description, thumb, std::move(model))); baseFinishConstruct(self); return self; } ProjectClip::~ProjectClip() { // controller is deleted in bincontroller m_thumbMutex.lock(); m_requestedThumbs.clear(); m_thumbMutex.unlock(); m_thumbThread.waitForFinished(); } void ProjectClip::connectEffectStack() { connect(m_effectStack.get(), &EffectStackModel::dataChanged, [&]() { if (auto ptr = m_model.lock()) { std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this()), AbstractProjectItem::IconOverlay); } }); } QString ProjectClip::getToolTip() const { if (m_clipType == ClipType::Color && m_path.contains(QLatin1Char('/'))) { return m_path.section(QLatin1Char('/'), -1); } return m_path; } QString ProjectClip::getXmlProperty(const QDomElement &producer, const QString &propertyName, const QString &defaultValue) { QString value = defaultValue; QDomNodeList props = producer.elementsByTagName(QStringLiteral("property")); for (int i = 0; i < props.count(); ++i) { if (props.at(i).toElement().attribute(QStringLiteral("name")) == propertyName) { value = props.at(i).firstChild().nodeValue(); break; } } return value; } void ProjectClip::updateAudioThumbnail() { if (!KdenliveSettings::audiothumbnails()) { return; } m_audioThumbCreated = true; audioThumbReady(); updateTimelineClips({TimelineModel::ReloadThumbRole}); } bool ProjectClip::audioThumbCreated() const { return (m_audioThumbCreated); } ClipType::ProducerType ProjectClip::clipType() const { return m_clipType; } bool ProjectClip::hasParent(const QString &id) const { std::shared_ptr par = parent(); while (par) { if (par->clipId() == id) { return true; } par = par->parent(); } return false; } std::shared_ptr ProjectClip::clip(const QString &id) { if (id == m_binId) { return std::static_pointer_cast(shared_from_this()); } return std::shared_ptr(); } std::shared_ptr ProjectClip::folder(const QString &id) { Q_UNUSED(id) return std::shared_ptr(); } std::shared_ptr ProjectClip::getSubClip(int in, int out) { for (int i = 0; i < childCount(); ++i) { std::shared_ptr clip = std::static_pointer_cast(child(i))->subClip(in, out); if (clip) { return clip; } } return std::shared_ptr(); } QStringList ProjectClip::subClipIds() const { QStringList subIds; for (int i = 0; i < childCount(); ++i) { std::shared_ptr clip = std::static_pointer_cast(child(i)); if (clip) { subIds << clip->clipId(); } } return subIds; } std::shared_ptr ProjectClip::clipAt(int ix) { if (ix == row()) { return std::static_pointer_cast(shared_from_this()); } return std::shared_ptr(); } /*bool ProjectClip::isValid() const { return m_controller->isValid(); }*/ bool ProjectClip::hasUrl() const { if ((m_clipType != ClipType::Color) && (m_clipType != ClipType::Unknown)) { return (!clipUrl().isEmpty()); } return false; } const QString ProjectClip::url() const { return clipUrl(); } GenTime ProjectClip::duration() const { return getPlaytime(); } size_t ProjectClip::frameDuration() const { return (size_t)getFramePlaytime(); } void ProjectClip::reloadProducer(bool refreshOnly, bool audioStreamChanged, bool reloadAudio) { // we find if there are some loading job on that clip int loadjobId = -1; pCore->jobManager()->hasPendingJob(clipId(), AbstractClipJob::LOADJOB, &loadjobId); QMutexLocker lock(&m_thumbMutex); if (refreshOnly) { // In that case, we only want a new thumbnail. // We thus set up a thumb job. We must make sure that there is no pending LOADJOB // Clear cache first ThumbnailCache::get()->invalidateThumbsForClip(clipId(), false); pCore->jobManager()->discardJobs(clipId(), AbstractClipJob::THUMBJOB); m_thumbsProducer.reset(); pCore->jobManager()->startJob({clipId()}, loadjobId, QString(), -1, true, true); } else { // If another load job is running? if (loadjobId > -1) { pCore->jobManager()->discardJobs(clipId(), AbstractClipJob::LOADJOB); } if (QFile::exists(m_path) && !hasProxy()) { clearBackupProperties(); } QDomDocument doc; QDomElement xml = toXml(doc); if (!xml.isNull()) { pCore->jobManager()->discardJobs(clipId(), AbstractClipJob::THUMBJOB); m_thumbsProducer.reset(); ClipType::ProducerType type = clipType(); if (type != ClipType::Color && type != ClipType::Image && type != ClipType::SlideShow) { xml.removeAttribute("out"); } ThumbnailCache::get()->invalidateThumbsForClip(clipId(), reloadAudio); int loadJob = pCore->jobManager()->startJob({clipId()}, loadjobId, QString(), xml); pCore->jobManager()->startJob({clipId()}, loadJob, QString(), -1, true, true); if (audioStreamChanged) { discardAudioThumb(); pCore->jobManager()->startJob({clipId()}, loadjobId, QString()); } } } } QDomElement ProjectClip::toXml(QDomDocument &document, bool includeMeta, bool includeProfile) { getProducerXML(document, includeMeta, includeProfile); QDomElement prod; if (document.documentElement().tagName() == QLatin1String("producer")) { prod = document.documentElement(); } else { prod = document.documentElement().firstChildElement(QStringLiteral("producer")); } if (m_clipType != ClipType::Unknown) { prod.setAttribute(QStringLiteral("type"), (int)m_clipType); } return prod; } void ProjectClip::setThumbnail(const QImage &img) { if (img.isNull()) { return; } QPixmap thumb = roundedPixmap(QPixmap::fromImage(img)); if (hasProxy() && !thumb.isNull()) { // Overlay proxy icon QPainter p(&thumb); QColor c(220, 220, 10, 200); QRect r(0, 0, thumb.height() / 2.5, thumb.height() / 2.5); p.fillRect(r, c); QFont font = p.font(); font.setPixelSize(r.height()); font.setBold(true); p.setFont(font); p.setPen(Qt::black); p.drawText(r, Qt::AlignCenter, i18nc("The first letter of Proxy, used as abbreviation", "P")); } m_thumbnail = QIcon(thumb); if (auto ptr = m_model.lock()) { std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this()), AbstractProjectItem::DataThumbnail); } } bool ProjectClip::hasAudioAndVideo() const { return hasAudio() && hasVideo() && m_masterProducer->get_int("set.test_image") == 0 && m_masterProducer->get_int("set.test_audio") == 0; } bool ProjectClip::isCompatible(PlaylistState::ClipState state) const { switch (state) { case PlaylistState::AudioOnly: return hasAudio() && (m_masterProducer->get_int("set.test_audio") == 0); case PlaylistState::VideoOnly: return hasVideo() && (m_masterProducer->get_int("set.test_image") == 0); default: return true; } } QPixmap ProjectClip::thumbnail(int width, int height) { return m_thumbnail.pixmap(width, height); } bool ProjectClip::setProducer(std::shared_ptr producer, bool replaceProducer) { Q_UNUSED(replaceProducer) qDebug() << "################### ProjectClip::setproducer"; QMutexLocker locker(&m_producerMutex); updateProducer(producer); m_thumbsProducer.reset(); connectEffectStack(); // Update info if (m_name.isEmpty()) { m_name = clipName(); } m_date = date; m_description = ClipController::description(); m_temporaryUrl.clear(); if (m_clipType == ClipType::Audio) { m_thumbnail = QIcon::fromTheme(QStringLiteral("audio-x-generic")); } else if (m_clipType == ClipType::Image) { if (producer->get_int("meta.media.width") < 8 || producer->get_int("meta.media.height") < 8) { KMessageBox::information(QApplication::activeWindow(), i18n("Image dimension smaller than 8 pixels.\nThis is not correctly supported by our video framework.")); } } m_duration = getStringDuration(); m_clipStatus = StatusReady; setTags(getProducerProperty(QStringLiteral("kdenlive:tags"))); AbstractProjectItem::setRating((uint) getProducerIntProperty(QStringLiteral("kdenlive:rating"))); if (!hasProxy()) { if (auto ptr = m_model.lock()) emit std::static_pointer_cast(ptr)->refreshPanel(m_binId); } if (auto ptr = m_model.lock()) { std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this()), AbstractProjectItem::DataDuration); std::static_pointer_cast(ptr)->updateWatcher(std::static_pointer_cast(shared_from_this())); } // Make sure we have a hash for this clip getFileHash(); // set parent again (some info need to be stored in producer) updateParent(parentItem().lock()); if (pCore->currentDoc()->getDocumentProperty(QStringLiteral("enableproxy")).toInt() == 1) { QList> clipList; // automatic proxy generation enabled if (m_clipType == ClipType::Image && pCore->currentDoc()->getDocumentProperty(QStringLiteral("generateimageproxy")).toInt() == 1) { if (getProducerIntProperty(QStringLiteral("meta.media.width")) >= KdenliveSettings::proxyimageminsize() && getProducerProperty(QStringLiteral("kdenlive:proxy")) == QStringLiteral()) { clipList << std::static_pointer_cast(shared_from_this()); } } else if (pCore->currentDoc()->getDocumentProperty(QStringLiteral("generateproxy")).toInt() == 1 && (m_clipType == ClipType::AV || m_clipType == ClipType::Video) && getProducerProperty(QStringLiteral("kdenlive:proxy")) == QStringLiteral()) { bool skipProducer = false; if (pCore->currentDoc()->getDocumentProperty(QStringLiteral("enableexternalproxy")).toInt() == 1) { QStringList externalParams = pCore->currentDoc()->getDocumentProperty(QStringLiteral("externalproxyparams")).split(QLatin1Char(';')); // We have a camcorder profile, check if we have opened a proxy clip if (externalParams.count() >= 6) { QFileInfo info(m_path); QDir dir = info.absoluteDir(); dir.cd(externalParams.at(3)); QString fileName = info.fileName(); if (!externalParams.at(2).isEmpty()) { fileName.chop(externalParams.at(2).size()); } fileName.append(externalParams.at(5)); if (dir.exists(fileName)) { setProducerProperty(QStringLiteral("kdenlive:proxy"), m_path); m_path = dir.absoluteFilePath(fileName); setProducerProperty(QStringLiteral("kdenlive:originalurl"), m_path); getFileHash(); skipProducer = true; } } } if (!skipProducer && getProducerIntProperty(QStringLiteral("meta.media.width")) >= KdenliveSettings::proxyminsize()) { clipList << std::static_pointer_cast(shared_from_this()); } } if (!clipList.isEmpty()) { pCore->currentDoc()->slotProxyCurrentItem(true, clipList, false); } } pCore->bin()->reloadMonitorIfActive(clipId()); for (auto &p : m_audioProducers) { m_effectStack->removeService(p.second); } for (auto &p : m_videoProducers) { m_effectStack->removeService(p.second); } for (auto &p : m_timewarpProducers) { m_effectStack->removeService(p.second); } // Release audio producers m_audioProducers.clear(); m_videoProducers.clear(); m_timewarpProducers.clear(); emit refreshPropertiesPanel(); if (m_clipType == ClipType::AV || m_clipType == ClipType::Video || m_clipType == ClipType::Playlist) { QTimer::singleShot(1000, this, [this]() { int loadjobId; if (!pCore->jobManager()->hasPendingJob(m_binId, AbstractClipJob::CACHEJOB, &loadjobId)) { pCore->jobManager()->startJob({m_binId}, -1, QString()); } }); } - if (KdenliveSettings::multistream_checktrack() && !isIncludedInTimeline() && activeStreams().count() > 1) { - // Check we have enough tracks in the project for its audio streams - //pCore->bin()->checkProjectAudioTracks(activeStreams().count()); - } replaceInTimeline(); updateTimelineClips({TimelineModel::IsProxyRole}); return true; } std::shared_ptr ProjectClip::thumbProducer() { if (m_thumbsProducer) { return m_thumbsProducer; } if (clipType() == ClipType::Unknown) { return nullptr; } QMutexLocker lock(&m_thumbMutex); std::shared_ptr prod = originalProducer(); if (!prod->is_valid()) { return nullptr; } if (KdenliveSettings::gpu_accel()) { // TODO: when the original producer changes, we must reload this thumb producer m_thumbsProducer = softClone(ClipController::getPassPropertiesList()); } else { QString mltService = m_masterProducer->get("mlt_service"); const QString mltResource = m_masterProducer->get("resource"); if (mltService == QLatin1String("avformat")) { mltService = QStringLiteral("avformat-novalidate"); } m_thumbsProducer.reset(new Mlt::Producer(*pCore->thumbProfile(), mltService.toUtf8().constData(), mltResource.toUtf8().constData())); if (m_thumbsProducer->is_valid()) { Mlt::Properties original(m_masterProducer->get_properties()); Mlt::Properties cloneProps(m_thumbsProducer->get_properties()); cloneProps.pass_list(original, ClipController::getPassPropertiesList()); Mlt::Filter scaler(*pCore->thumbProfile(), "swscale"); Mlt::Filter padder(*pCore->thumbProfile(), "resize"); Mlt::Filter converter(*pCore->thumbProfile(), "avcolor_space"); m_thumbsProducer->set("audio_index", -1); // Required to make get_playtime() return > 1 m_thumbsProducer->set("out", m_thumbsProducer->get_length() -1); m_thumbsProducer->attach(scaler); m_thumbsProducer->attach(padder); m_thumbsProducer->attach(converter); } } return m_thumbsProducer; } void ProjectClip::createDisabledMasterProducer() { if (!m_disabledProducer) { m_disabledProducer = cloneProducer(); m_disabledProducer->set("set.test_audio", 1); m_disabledProducer->set("set.test_image", 1); m_effectStack->addService(m_disabledProducer); } } std::shared_ptr ProjectClip::getTimelineProducer(int trackId, int clipId, PlaylistState::ClipState state, int audioStream, double speed) { if (!m_masterProducer) { return nullptr; } if (qFuzzyCompare(speed, 1.0)) { // we are requesting a normal speed producer bool byPassTrackProducer = false; if (trackId == -1 && (state != PlaylistState::AudioOnly || audioStream == m_masterProducer->get_int("audio_index"))) { byPassTrackProducer = true; } if (byPassTrackProducer || (state == PlaylistState::VideoOnly && (m_clipType == ClipType::Color || m_clipType == ClipType::Image || m_clipType == ClipType::Text|| m_clipType == ClipType::TextTemplate || m_clipType == ClipType::Qml))) { // Temporary copy, return clone of master int duration = m_masterProducer->time_to_frames(m_masterProducer->get("kdenlive:duration")); return std::shared_ptr(m_masterProducer->cut(-1, duration > 0 ? duration - 1 : -1)); } if (m_timewarpProducers.count(clipId) > 0) { m_effectStack->removeService(m_timewarpProducers[clipId]); m_timewarpProducers.erase(clipId); } if (state == PlaylistState::AudioOnly) { // We need to get an audio producer, if none exists if (audioStream > -1) { if (trackId >= 0) { trackId += 100 * audioStream; } else { trackId -= 100 * audioStream; } } if (m_audioProducers.count(trackId) == 0) { m_audioProducers[trackId] = cloneProducer(true); m_audioProducers[trackId]->set("set.test_audio", 0); m_audioProducers[trackId]->set("set.test_image", 1); if (audioStream > -1) { m_audioProducers[trackId]->set("audio_index", audioStream); } m_effectStack->addService(m_audioProducers[trackId]); } return std::shared_ptr(m_audioProducers[trackId]->cut()); } if (m_audioProducers.count(trackId) > 0) { m_effectStack->removeService(m_audioProducers[trackId]); m_audioProducers.erase(trackId); } if (state == PlaylistState::VideoOnly) { // we return the video producer // We need to get an audio producer, if none exists if (m_videoProducers.count(trackId) == 0) { m_videoProducers[trackId] = cloneProducer(true); m_videoProducers[trackId]->set("set.test_audio", 1); m_videoProducers[trackId]->set("set.test_image", 0); m_effectStack->addService(m_videoProducers[trackId]); } int duration = m_masterProducer->time_to_frames(m_masterProducer->get("kdenlive:duration")); return std::shared_ptr(m_videoProducers[trackId]->cut(-1, duration > 0 ? duration - 1: -1)); } if (m_videoProducers.count(trackId) > 0) { m_effectStack->removeService(m_videoProducers[trackId]); m_videoProducers.erase(trackId); } Q_ASSERT(state == PlaylistState::Disabled); createDisabledMasterProducer(); int duration = m_masterProducer->time_to_frames(m_masterProducer->get("kdenlive:duration")); return std::shared_ptr(m_disabledProducer->cut(-1, duration > 0 ? duration - 1: -1)); } // For timewarp clips, we keep one separate producer for each clip. std::shared_ptr warpProducer; if (m_timewarpProducers.count(clipId) > 0) { // remove in all cases, we add it unconditionally anyways m_effectStack->removeService(m_timewarpProducers[clipId]); if (qFuzzyCompare(m_timewarpProducers[clipId]->get_double("warp_speed"), speed)) { // the producer we have is good, use it ! warpProducer = m_timewarpProducers[clipId]; qDebug() << "Reusing producer!"; } else { m_timewarpProducers.erase(clipId); } } if (!warpProducer) { QString resource(originalProducer()->get("resource")); if (resource.isEmpty() || resource == QLatin1String("")) { resource = m_service; } QString url = QString("timewarp:%1:%2").arg(QString::fromStdString(std::to_string(speed))).arg(resource); warpProducer.reset(new Mlt::Producer(*originalProducer()->profile(), url.toUtf8().constData())); qDebug() << "new producer: " << url; qDebug() << "warp LENGTH before" << warpProducer->get_length(); int original_length = originalProducer()->get_length(); // this is a workaround to cope with Mlt erroneous rounding Mlt::Properties original(m_masterProducer->get_properties()); Mlt::Properties cloneProps(warpProducer->get_properties()); cloneProps.pass_list(original, ClipController::getPassPropertiesList(false)); warpProducer->set("length", (int) (original_length / std::abs(speed) + 0.5)); } qDebug() << "warp LENGTH" << warpProducer->get_length(); warpProducer->set("set.test_audio", 1); warpProducer->set("set.test_image", 1); if (state == PlaylistState::AudioOnly) { warpProducer->set("set.test_audio", 0); } if (state == PlaylistState::VideoOnly) { warpProducer->set("set.test_image", 0); } m_timewarpProducers[clipId] = warpProducer; m_effectStack->addService(m_timewarpProducers[clipId]); return std::shared_ptr(warpProducer->cut()); } std::pair, bool> ProjectClip::giveMasterAndGetTimelineProducer(int clipId, std::shared_ptr master, PlaylistState::ClipState state, int tid) { int in = master->get_in(); int out = master->get_out(); if (master->parent().is_valid()) { // in that case, we have a cut // check whether it's a timewarp double speed = 1.0; bool timeWarp = false; if (QString::fromUtf8(master->parent().get("mlt_service")) == QLatin1String("timewarp")) { speed = master->parent().get_double("warp_speed"); timeWarp = true; } if (master->parent().get_int("_loaded") == 1) { // we already have a clip that shares the same master if (state != PlaylistState::Disabled || timeWarp) { // In that case, we must create copies std::shared_ptr prod(getTimelineProducer(tid, clipId, state, master->parent().get_int("audio_index"), speed)->cut(in, out)); return {prod, false}; } if (state == PlaylistState::Disabled) { if (!m_disabledProducer) { qDebug() << "Warning: weird, we found a disabled clip whose master is already loaded but we don't have any yet"; createDisabledMasterProducer(); } return {std::shared_ptr(m_disabledProducer->cut(in, out)), false}; } // We have a good id, this clip can be used return {master, true}; } else { master->parent().set("_loaded", 1); if (timeWarp) { m_timewarpProducers[clipId] = std::make_shared(&master->parent()); m_effectStack->loadService(m_timewarpProducers[clipId]); return {master, true}; } if (state == PlaylistState::AudioOnly) { int producerId = tid; int audioStream = master->parent().get_int("audio_index"); if (audioStream > -1) { producerId += 100 * audioStream; } m_audioProducers[tid] = std::make_shared(&master->parent()); m_effectStack->loadService(m_audioProducers[tid]); return {master, true}; } if (state == PlaylistState::VideoOnly) { // good, we found a master video producer, and we didn't have any if (m_clipType != ClipType::Color && m_clipType != ClipType::Image && m_clipType != ClipType::Text) { // Color, image and text clips always use master producer in timeline m_videoProducers[tid] = std::make_shared(&master->parent()); m_effectStack->loadService(m_videoProducers[tid]); } return {master, true}; } if (state == PlaylistState::Disabled) { if (!m_disabledProducer) { createDisabledMasterProducer(); } return {std::make_shared(m_disabledProducer->cut(master->get_in(), master->get_out())), true}; } qDebug() << "Warning: weird, we found a clip whose master is not loaded but we already have a master"; Q_ASSERT(false); } } else if (master->is_valid()) { // in that case, we have a master qDebug() << "Warning: weird, we received a master clip in lieue of a cut"; double speed = 1.0; if (QString::fromUtf8(master->parent().get("mlt_service")) == QLatin1String("timewarp")) { speed = master->get_double("warp_speed"); } return {getTimelineProducer(-1, clipId, state, master->get_int("audio_index"), speed), false}; } // we have a problem return {std::shared_ptr(ClipController::mediaUnavailable->cut()), false}; } std::shared_ptr ProjectClip::cloneProducer(bool removeEffects) { Mlt::Consumer c(pCore->getCurrentProfile()->profile(), "xml", "string"); Mlt::Service s(m_masterProducer->get_service()); int ignore = s.get_int("ignore_points"); if (ignore) { s.set("ignore_points", 0); } c.connect(s); c.set("time_format", "frames"); c.set("no_meta", 1); c.set("no_root", 1); c.set("no_profile", 1); c.set("root", "/"); c.set("store", "kdenlive"); c.run(); if (ignore) { s.set("ignore_points", ignore); } const QByteArray clipXml = c.get("string"); std::shared_ptr prod; prod.reset(new Mlt::Producer(pCore->getCurrentProfile()->profile(), "xml-string", clipXml.constData())); if (strcmp(prod->get("mlt_service"), "avformat") == 0) { prod->set("mlt_service", "avformat-novalidate"); prod->set("mute_on_pause", 0); } // we pass some properties that wouldn't be passed because of the novalidate const char *prefix = "meta."; const size_t prefix_len = strlen(prefix); for (int i = 0; i < m_masterProducer->count(); ++i) { char *current = m_masterProducer->get_name(i); if (strlen(current) >= prefix_len && strncmp(current, prefix, prefix_len) == 0) { prod->set(current, m_masterProducer->get(i)); } } if (removeEffects) { int ct = 0; Mlt::Filter *filter = prod->filter(ct); while (filter) { qDebug() << "// EFFECT " << ct << " : " << filter->get("mlt_service"); QString ix = QString::fromLatin1(filter->get("kdenlive_id")); if (!ix.isEmpty()) { qDebug() << "/ + + DELETING"; if (prod->detach(*filter) == 0) { } else { ct++; } } else { ct++; } delete filter; filter = prod->filter(ct); } } prod->set("id", (char *)nullptr); return prod; } std::shared_ptr ProjectClip::cloneProducer(const std::shared_ptr &producer) { Mlt::Consumer c(*producer->profile(), "xml", "string"); Mlt::Service s(producer->get_service()); int ignore = s.get_int("ignore_points"); if (ignore) { s.set("ignore_points", 0); } c.connect(s); c.set("time_format", "frames"); c.set("no_meta", 1); c.set("no_root", 1); c.set("no_profile", 1); c.set("root", "/"); c.set("store", "kdenlive"); c.start(); if (ignore) { s.set("ignore_points", ignore); } const QByteArray clipXml = c.get("string"); std::shared_ptr prod(new Mlt::Producer(*producer->profile(), "xml-string", clipXml.constData())); if (strcmp(prod->get("mlt_service"), "avformat") == 0) { prod->set("mlt_service", "avformat-novalidate"); prod->set("mute_on_pause", 0); } return prod; } std::shared_ptr ProjectClip::softClone(const char *list) { QString service = QString::fromLatin1(m_masterProducer->get("mlt_service")); QString resource = QString::fromUtf8(m_masterProducer->get("resource")); std::shared_ptr clone(new Mlt::Producer(*pCore->thumbProfile(), service.toUtf8().constData(), resource.toUtf8().constData())); Mlt::Filter scaler(*pCore->thumbProfile(), "swscale"); Mlt::Filter converter(pCore->getCurrentProfile()->profile(), "avcolor_space"); clone->attach(scaler); clone->attach(converter); Mlt::Properties original(m_masterProducer->get_properties()); Mlt::Properties cloneProps(clone->get_properties()); cloneProps.pass_list(original, list); return clone; } std::unique_ptr ProjectClip::getClone() { const char *list = ClipController::getPassPropertiesList(); QString service = QString::fromLatin1(m_masterProducer->get("mlt_service")); QString resource = QString::fromUtf8(m_masterProducer->get("resource")); std::unique_ptr clone(new Mlt::Producer(*m_masterProducer->profile(), service.toUtf8().constData(), resource.toUtf8().constData())); Mlt::Properties original(m_masterProducer->get_properties()); Mlt::Properties cloneProps(clone->get_properties()); cloneProps.pass_list(original, list); return clone; } bool ProjectClip::isReady() const { return m_clipStatus == StatusReady; } QPoint ProjectClip::zone() const { return ClipController::zone(); } const QString ProjectClip::hash() { QString clipHash = getProducerProperty(QStringLiteral("kdenlive:file_hash")); if (!clipHash.isEmpty()) { return clipHash; } return getFileHash(); } const QString ProjectClip::getFileHash() { QByteArray fileData; QByteArray fileHash; switch (m_clipType) { case ClipType::SlideShow: fileData = clipUrl().toUtf8(); fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5); break; case ClipType::Text: fileData = getProducerProperty(QStringLiteral("xmldata")).toUtf8(); fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5); break; case ClipType::TextTemplate: fileData = getProducerProperty(QStringLiteral("resource")).toUtf8(); fileData.append(getProducerProperty(QStringLiteral("templatetext")).toUtf8()); fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5); break; case ClipType::QText: fileData = getProducerProperty(QStringLiteral("text")).toUtf8(); fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5); break; case ClipType::Color: fileData = getProducerProperty(QStringLiteral("resource")).toUtf8(); fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5); break; default: QFile file(clipUrl()); if (file.open(QIODevice::ReadOnly)) { // write size and hash only if resource points to a file /* * 1 MB = 1 second per 450 files (or faster) * 10 MB = 9 seconds per 450 files (or faster) */ if (file.size() > 2000000) { fileData = file.read(1000000); if (file.seek(file.size() - 1000000)) { fileData.append(file.readAll()); } } else { fileData = file.readAll(); } file.close(); ClipController::setProducerProperty(QStringLiteral("kdenlive:file_size"), QString::number(file.size())); fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5); } break; } if (fileHash.isEmpty()) { qDebug() << "// WARNING EMPTY CLIP HASH: "; return QString(); } QString result = fileHash.toHex(); ClipController::setProducerProperty(QStringLiteral("kdenlive:file_hash"), result); return result; } double ProjectClip::getOriginalFps() const { return originalFps(); } bool ProjectClip::hasProxy() const { QString proxy = getProducerProperty(QStringLiteral("kdenlive:proxy")); return proxy.size() > 2; } void ProjectClip::setProperties(const QMap &properties, bool refreshPanel) { qDebug() << "// SETTING CLIP PROPERTIES: " << properties; QMapIterator i(properties); QMap passProperties; bool refreshAnalysis = false; bool reload = false; bool refreshOnly = true; if (properties.contains(QStringLiteral("templatetext"))) { m_description = properties.value(QStringLiteral("templatetext")); if (auto ptr = m_model.lock()) std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this()), AbstractProjectItem::ClipStatus); refreshPanel = true; } // Some properties also need to be passed to track producers QStringList timelineProperties{ QStringLiteral("force_aspect_ratio"), QStringLiteral("set.force_full_luma"), QStringLiteral("full_luma"), QStringLiteral("threads"), QStringLiteral("force_colorspace"), QStringLiteral("force_tff"), QStringLiteral("force_progressive"), QStringLiteral("video_delay") }; QStringList forceReloadProperties{QStringLiteral("autorotate"), QStringLiteral("templatetext"), QStringLiteral("resource"), QStringLiteral("force_fps"), QStringLiteral("set.test_image"), QStringLiteral("video_index")}; QStringList keys{QStringLiteral("luma_duration"), QStringLiteral("luma_file"), QStringLiteral("fade"), QStringLiteral("ttl"), QStringLiteral("softness"), QStringLiteral("crop"), QStringLiteral("animation")}; QVector updateRoles; while (i.hasNext()) { i.next(); setProducerProperty(i.key(), i.value()); if (m_clipType == ClipType::SlideShow && keys.contains(i.key())) { reload = true; refreshOnly = false; } if (i.key().startsWith(QLatin1String("kdenlive:clipanalysis"))) { refreshAnalysis = true; } if (timelineProperties.contains(i.key())) { passProperties.insert(i.key(), i.value()); } } if (properties.contains(QStringLiteral("kdenlive:proxy"))) { QString value = properties.value(QStringLiteral("kdenlive:proxy")); // If value is "-", that means user manually disabled proxy on this clip if (value.isEmpty() || value == QLatin1String("-")) { // reset proxy int id; if (pCore->jobManager()->hasPendingJob(clipId(), AbstractClipJob::PROXYJOB, &id)) { // The proxy clip is being created, abort pCore->jobManager()->discardJobs(clipId(), AbstractClipJob::PROXYJOB); } else { reload = true; refreshOnly = false; } } else { // A proxy was requested, make sure to keep original url setProducerProperty(QStringLiteral("kdenlive:originalurl"), url()); backupOriginalProperties(); pCore->jobManager()->startJob({clipId()}, -1, QString()); } } else if (!reload) { const QList propKeys = properties.keys(); for (const QString &k : propKeys) { if (forceReloadProperties.contains(k)) { refreshPanel = true; reload = true; if (m_clipType == ClipType::Color) { refreshOnly = true; updateRoles << TimelineModel::ResourceRole; } else { // Clip resource changed, update thumbnail, name, clear hash refreshOnly = false; if (propKeys.contains(QStringLiteral("resource"))) { resetProducerProperty(QStringLiteral("kdenlive:file_hash")); setProducerProperty(QStringLiteral("kdenlive:originalurl"), url()); updateRoles << TimelineModel::ResourceRole << TimelineModel::MaxDurationRole << TimelineModel::NameRole; } } break; } } } if (!reload && (properties.contains(QStringLiteral("xmldata")) || !passProperties.isEmpty())) { reload = true; } if (refreshAnalysis) { emit refreshAnalysisPanel(); } if (properties.contains(QStringLiteral("length")) || properties.contains(QStringLiteral("kdenlive:duration"))) { // Make sure length is >= kdenlive:duration int producerLength = getProducerIntProperty(QStringLiteral("length")); int kdenliveLength = getFramePlaytime(); if (producerLength < kdenliveLength) { setProducerProperty(QStringLiteral("length"), kdenliveLength); } m_duration = getStringDuration(); if (auto ptr = m_model.lock()) std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this()), AbstractProjectItem::DataDuration); refreshOnly = false; reload = true; } if (properties.contains(QStringLiteral("kdenlive:tags"))) { setTags(properties.value(QStringLiteral("kdenlive:tags"))); if (auto ptr = m_model.lock()) { std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this()), AbstractProjectItem::DataTag); } } if (properties.contains(QStringLiteral("kdenlive:clipname"))) { m_name = properties.value(QStringLiteral("kdenlive:clipname")); refreshPanel = true; if (auto ptr = m_model.lock()) { std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this()), AbstractProjectItem::DataName); } // update timeline clips if (!reload) { updateTimelineClips({TimelineModel::NameRole}); } } bool audioStreamChanged = properties.contains(QStringLiteral("audio_index")); if (reload) { // producer has changed, refresh monitor and thumbnail if (hasProxy()) { pCore->jobManager()->discardJobs(clipId(), AbstractClipJob::PROXYJOB); setProducerProperty(QStringLiteral("_overwriteproxy"), 1); pCore->jobManager()->startJob({clipId()}, -1, QString()); } else { reloadProducer(refreshOnly, audioStreamChanged, audioStreamChanged || (!refreshOnly && !properties.contains(QStringLiteral("kdenlive:proxy")))); } if (refreshOnly) { if (auto ptr = m_model.lock()) { emit std::static_pointer_cast(ptr)->refreshClip(m_binId); } } if (!updateRoles.isEmpty()) { updateTimelineClips(updateRoles); } } else { if (properties.contains(QStringLiteral("kdenlive:active_streams")) && m_audioInfo) { // Clip is a multi audio stream and currently in clip monitor, update target tracks m_audioInfo->updateActiveStreams(properties.value(QStringLiteral("kdenlive:active_streams"))); pCore->bin()->updateTargets(clipId()); if (!audioStreamChanged) { pCore->bin()->reloadMonitorStreamIfActive(clipId()); + pCore->bin()->checkProjectAudioTracks(clipId(), m_audioInfo->activeStreams().count()); refreshPanel = true; } } if (audioStreamChanged) { refreshAudioInfo(); audioThumbReady(); pCore->bin()->reloadMonitorStreamIfActive(clipId()); refreshPanel = true; } } if (refreshPanel) { // Some of the clip properties have changed through a command, update properties panel emit refreshPropertiesPanel(); } if (!passProperties.isEmpty() && (!reload || refreshOnly)) { for (auto &p : m_audioProducers) { QMapIterator pr(passProperties); while (pr.hasNext()) { pr.next(); p.second->set(pr.key().toUtf8().constData(), pr.value().toUtf8().constData()); } } for (auto &p : m_videoProducers) { QMapIterator pr(passProperties); while (pr.hasNext()) { pr.next(); p.second->set(pr.key().toUtf8().constData(), pr.value().toUtf8().constData()); } } for (auto &p : m_timewarpProducers) { QMapIterator pr(passProperties); while (pr.hasNext()) { pr.next(); p.second->set(pr.key().toUtf8().constData(), pr.value().toUtf8().constData()); } } } } ClipPropertiesController *ProjectClip::buildProperties(QWidget *parent) { auto ptr = m_model.lock(); Q_ASSERT(ptr); auto *panel = new ClipPropertiesController(static_cast(this), parent); connect(this, &ProjectClip::refreshPropertiesPanel, panel, &ClipPropertiesController::slotReloadProperties); connect(this, &ProjectClip::refreshAnalysisPanel, panel, &ClipPropertiesController::slotFillAnalysisData); connect(panel, &ClipPropertiesController::requestProxy, [this](bool doProxy) { QList> clipList{std::static_pointer_cast(shared_from_this())}; pCore->currentDoc()->slotProxyCurrentItem(doProxy, clipList); }); connect(panel, &ClipPropertiesController::deleteProxy, this, &ProjectClip::deleteProxy); return panel; } void ProjectClip::deleteProxy() { // Disable proxy file QString proxy = getProducerProperty(QStringLiteral("kdenlive:proxy")); QList> clipList{std::static_pointer_cast(shared_from_this())}; pCore->currentDoc()->slotProxyCurrentItem(false, clipList); // Delete bool ok; QDir dir = pCore->currentDoc()->getCacheDir(CacheProxy, &ok); if (ok && proxy.length() > 2) { proxy = QFileInfo(proxy).fileName(); if (dir.exists(proxy)) { dir.remove(proxy); } } } void ProjectClip::updateParent(std::shared_ptr parent) { if (parent) { auto item = std::static_pointer_cast(parent); ClipController::setProducerProperty(QStringLiteral("kdenlive:folderid"), item->clipId()); } AbstractProjectItem::updateParent(parent); } bool ProjectClip::matches(const QString &condition) { // TODO Q_UNUSED(condition) return true; } bool ProjectClip::rename(const QString &name, int column) { QMap newProperites; QMap oldProperites; bool edited = false; switch (column) { case 0: if (m_name == name) { return false; } // Rename clip oldProperites.insert(QStringLiteral("kdenlive:clipname"), m_name); newProperites.insert(QStringLiteral("kdenlive:clipname"), name); m_name = name; edited = true; break; case 2: if (m_description == name) { return false; } // Rename clip if (m_clipType == ClipType::TextTemplate) { oldProperites.insert(QStringLiteral("templatetext"), m_description); newProperites.insert(QStringLiteral("templatetext"), name); } else { oldProperites.insert(QStringLiteral("kdenlive:description"), m_description); newProperites.insert(QStringLiteral("kdenlive:description"), name); } m_description = name; edited = true; break; } if (edited) { pCore->bin()->slotEditClipCommand(m_binId, oldProperites, newProperites); } return edited; } QVariant ProjectClip::getData(DataType type) const { switch (type) { case AbstractProjectItem::IconOverlay: if (m_clipStatus == AbstractProjectItem::StatusMissing) { return QVariant("window-close"); } if (m_clipStatus == AbstractProjectItem::StatusWaiting) { return QVariant("view-refresh"); } return m_effectStack && m_effectStack->rowCount() > 0 ? QVariant("kdenlive-track_has_effect") : QVariant(); default: return AbstractProjectItem::getData(type); } } int ProjectClip::audioChannels() const { if (!audioInfo()) { return 0; } return audioInfo()->channels(); } void ProjectClip::discardAudioThumb() { if (!m_audioInfo) { return; } QString audioThumbPath = getAudioThumbPath(audioInfo()->ffmpeg_audio_index()); if (!audioThumbPath.isEmpty()) { QFile::remove(audioThumbPath); } qCDebug(KDENLIVE_LOG) << "//////////////////// DISCARD AUDIO THUMBS"; m_audioThumbCreated = false; refreshAudioInfo(); pCore->jobManager()->discardJobs(clipId(), AbstractClipJob::AUDIOTHUMBJOB); } int ProjectClip::getAudioStreamFfmpegIndex(int mltStream) { if (!m_masterProducer || !audioInfo()) { return -1; } QList audioStreams = audioInfo()->streams().keys(); if (audioStreams.contains(mltStream)) { return audioStreams.indexOf(mltStream); } return -1; } const QString ProjectClip::getAudioThumbPath(int stream, bool miniThumb) { if (audioInfo() == nullptr && !miniThumb) { return QString(); } bool ok = false; QDir thumbFolder = pCore->currentDoc()->getCacheDir(CacheAudio, &ok); if (!ok) { return QString(); } const QString clipHash = hash(); if (clipHash.isEmpty()) { return QString(); } QString audioPath = thumbFolder.absoluteFilePath(clipHash); audioPath.append(QLatin1Char('_') + QString::number(stream)); if (miniThumb) { audioPath.append(QStringLiteral(".png")); return audioPath; } int roundedFps = (int)pCore->getCurrentFps(); audioPath.append(QStringLiteral("_%1_audio.png").arg(roundedFps)); return audioPath; } QStringList ProjectClip::updatedAnalysisData(const QString &name, const QString &data, int offset) { if (data.isEmpty()) { // Remove data return QStringList() << QString("kdenlive:clipanalysis." + name) << QString(); // m_controller->resetProperty("kdenlive:clipanalysis." + name); } QString current = getProducerProperty("kdenlive:clipanalysis." + name); if (!current.isEmpty()) { if (KMessageBox::questionYesNo(QApplication::activeWindow(), i18n("Clip already contains analysis data %1", name), QString(), KGuiItem(i18n("Merge")), KGuiItem(i18n("Add"))) == KMessageBox::Yes) { // Merge data auto &profile = pCore->getCurrentProfile(); Mlt::Geometry geometry(current.toUtf8().data(), duration().frames(profile->fps()), profile->width(), profile->height()); Mlt::Geometry newGeometry(data.toUtf8().data(), duration().frames(profile->fps()), profile->width(), profile->height()); Mlt::GeometryItem item; int pos = 0; while (newGeometry.next_key(&item, pos) == 0) { pos = item.frame(); item.frame(pos + offset); pos++; geometry.insert(item); } return QStringList() << QString("kdenlive:clipanalysis." + name) << geometry.serialise(); // m_controller->setProperty("kdenlive:clipanalysis." + name, geometry.serialise()); } // Add data with another name int i = 1; QString previous = getProducerProperty("kdenlive:clipanalysis." + name + QString::number(i)); while (!previous.isEmpty()) { ++i; previous = getProducerProperty("kdenlive:clipanalysis." + name + QString::number(i)); } return QStringList() << QString("kdenlive:clipanalysis." + name + QString::number(i)) << geometryWithOffset(data, offset); // m_controller->setProperty("kdenlive:clipanalysis." + name + QLatin1Char(' ') + QString::number(i), geometryWithOffset(data, offset)); } return QStringList() << QString("kdenlive:clipanalysis." + name) << geometryWithOffset(data, offset); // m_controller->setProperty("kdenlive:clipanalysis." + name, geometryWithOffset(data, offset)); } QMap ProjectClip::analysisData(bool withPrefix) { return getPropertiesFromPrefix(QStringLiteral("kdenlive:clipanalysis."), withPrefix); } const QString ProjectClip::geometryWithOffset(const QString &data, int offset) { if (offset == 0) { return data; } auto &profile = pCore->getCurrentProfile(); Mlt::Geometry geometry(data.toUtf8().data(), duration().frames(profile->fps()), profile->width(), profile->height()); Mlt::Geometry newgeometry(nullptr, duration().frames(profile->fps()), profile->width(), profile->height()); Mlt::GeometryItem item; int pos = 0; while (geometry.next_key(&item, pos) == 0) { pos = item.frame(); item.frame(pos + offset); pos++; newgeometry.insert(item); } return newgeometry.serialise(); } bool ProjectClip::isSplittable() const { return (m_clipType == ClipType::AV || m_clipType == ClipType::Playlist); } void ProjectClip::setBinEffectsEnabled(bool enabled) { ClipController::setBinEffectsEnabled(enabled); } void ProjectClip::registerService(std::weak_ptr timeline, int clipId, const std::shared_ptr &service, bool forceRegister) { if (!service->is_cut() || forceRegister) { int hasAudio = service->get_int("set.test_audio") == 0; int hasVideo = service->get_int("set.test_image") == 0; if (hasVideo && m_videoProducers.count(clipId) == 0) { // This is an undo producer, register it! m_videoProducers[clipId] = service; m_effectStack->addService(m_videoProducers[clipId]); } else if (hasAudio && m_audioProducers.count(clipId) == 0) { // This is an undo producer, register it! m_audioProducers[clipId] = service; m_effectStack->addService(m_audioProducers[clipId]); } } registerTimelineClip(std::move(timeline), clipId); } void ProjectClip::registerTimelineClip(std::weak_ptr timeline, int clipId) { Q_ASSERT(m_registeredClips.count(clipId) == 0); Q_ASSERT(!timeline.expired()); m_registeredClips[clipId] = std::move(timeline); setRefCount((uint)m_registeredClips.size()); } void ProjectClip::deregisterTimelineClip(int clipId) { qDebug() << " ** * DEREGISTERING TIMELINE CLIP: " << clipId; Q_ASSERT(m_registeredClips.count(clipId) > 0); m_registeredClips.erase(clipId); if (m_videoProducers.count(clipId) > 0) { m_effectStack->removeService(m_videoProducers[clipId]); m_videoProducers.erase(clipId); } if (m_audioProducers.count(clipId) > 0) { m_effectStack->removeService(m_audioProducers[clipId]); m_audioProducers.erase(clipId); } setRefCount((uint)m_registeredClips.size()); } QList ProjectClip::timelineInstances() const { QList ids; for (const auto &m_registeredClip : m_registeredClips) { ids.push_back(m_registeredClip.first); } return ids; } bool ProjectClip::selfSoftDelete(Fun &undo, Fun &redo) { auto toDelete = m_registeredClips; // we cannot use m_registeredClips directly, because it will be modified during loop for (const auto &clip : toDelete) { if (m_registeredClips.count(clip.first) == 0) { // clip already deleted, was probably grouped with another one continue; } if (auto timeline = clip.second.lock()) { timeline->requestItemDeletion(clip.first, undo, redo); } else { qDebug() << "Error while deleting clip: timeline unavailable"; Q_ASSERT(false); return false; } } return AbstractProjectItem::selfSoftDelete(undo, redo); } bool ProjectClip::isIncludedInTimeline() { return m_registeredClips.size() > 0; } void ProjectClip::replaceInTimeline() { for (const auto &clip : m_registeredClips) { if (auto timeline = clip.second.lock()) { timeline->requestClipReload(clip.first); } else { qDebug() << "Error while reloading clip: timeline unavailable"; Q_ASSERT(false); } } } void ProjectClip::updateTimelineClips(const QVector &roles) { for (const auto &clip : m_registeredClips) { if (auto timeline = clip.second.lock()) { timeline->requestClipUpdate(clip.first, roles); } else { qDebug() << "Error while reloading clip thumb: timeline unavailable"; Q_ASSERT(false); return; } } } void ProjectClip::updateZones() { int zonesCount = childCount(); if (zonesCount == 0) { resetProducerProperty(QStringLiteral("kdenlive:clipzones")); return; } QJsonArray list; for (int i = 0; i < zonesCount; ++i) { std::shared_ptr clip = std::static_pointer_cast(child(i)); if (clip) { QJsonObject currentZone; currentZone.insert(QLatin1String("name"), QJsonValue(clip->name())); QPoint zone = clip->zone(); currentZone.insert(QLatin1String("in"), QJsonValue(zone.x())); currentZone.insert(QLatin1String("out"), QJsonValue(zone.y())); if (clip->rating() > 0) { currentZone.insert(QLatin1String("rating"), QJsonValue((int)clip->rating())); } if (!clip->tags().isEmpty()) { currentZone.insert(QLatin1String("tags"), QJsonValue(clip->tags())); } list.push_back(currentZone); } } QJsonDocument json(list); setProducerProperty(QStringLiteral("kdenlive:clipzones"), QString(json.toJson())); } void ProjectClip::getThumbFromPercent(int percent) { // extract a maximum of 50 frames for bin preview percent += percent%2; int duration = getFramePlaytime(); int framePos = duration * percent / 100; if (ThumbnailCache::get()->hasThumbnail(m_binId, framePos)) { setThumbnail(ThumbnailCache::get()->getThumbnail(m_binId, framePos)); } else { // Generate percent thumbs int id; if (!pCore->jobManager()->hasPendingJob(m_binId, AbstractClipJob::CACHEJOB, &id)) { pCore->jobManager()->startJob({m_binId}, -1, QString(), 50); } } } void ProjectClip::setRating(uint rating) { AbstractProjectItem::setRating(rating); setProducerProperty(QStringLiteral("kdenlive:rating"), (int) rating); pCore->currentDoc()->setModified(true); } QVector ProjectClip::audioFrameCache(int stream) { QVector audioLevels; if (stream == -1) { if (m_audioInfo) { stream = m_audioInfo->ffmpeg_audio_index(); } else { return audioLevels; } } // convert cached image const QString cachePath = getAudioThumbPath(stream); // checking for cached thumbs QImage image(cachePath); if (!image.isNull()) { int channels = m_audioInfo->channels(); int n = image.width() * image.height(); for (int i = 0; i < n; i++) { QRgb p = image.pixel(i / channels, i % channels); audioLevels << (uint8_t)qRed(p); audioLevels << (uint8_t)qGreen(p); audioLevels << (uint8_t)qBlue(p); audioLevels << (uint8_t)qAlpha(p); } } return audioLevels; } void ProjectClip::setClipStatus(AbstractProjectItem::CLIPSTATUS status) { AbstractProjectItem::setClipStatus(status); if (auto ptr = m_model.lock()) { std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this()), AbstractProjectItem::IconOverlay); } } void ProjectClip::renameAudioStream(int id, QString name) { if (m_audioInfo) { m_audioInfo->renameStream(id, name); QString prop = QString("kdenlive:streamname.%1").arg(id); m_masterProducer->set(prop.toUtf8().constData(), name.toUtf8().constData()); if (m_audioInfo->activeStreams().keys().contains(id)) { pCore->bin()->updateTargets(clipId()); } pCore->bin()->reloadMonitorStreamIfActive(clipId()); } } diff --git a/src/core.cpp b/src/core.cpp index 1a9eaa77d..01c7721e0 100644 --- a/src/core.cpp +++ b/src/core.cpp @@ -1,927 +1,927 @@ /* Copyright (C) 2014 Till Theato This file is part of kdenlive. See www.kdenlive.org. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. */ #include "core.h" #include "bin/bin.h" #include "bin/projectitemmodel.h" #include "capture/mediacapture.h" #include "doc/docundostack.hpp" #include "doc/kdenlivedoc.h" #include "jobs/jobmanager.h" #include "kdenlive_debug.h" #include "kdenlivesettings.h" #include "library/librarywidget.h" #include "audiomixer/mixermanager.hpp" #include "mainwindow.h" #include "mltconnection.h" #include "mltcontroller/clipcontroller.h" #include "monitor/monitormanager.h" #include "profiles/profilemodel.hpp" #include "profiles/profilerepository.hpp" #include "project/projectmanager.h" #include "timeline2/model/timelineitemmodel.hpp" #include "timeline2/view/timelinecontroller.h" #include "timeline2/view/timelinewidget.h" #include #include #include #include #include #include #include #ifdef Q_OS_MAC #include #endif std::unique_ptr Core::m_self; Core::Core() : m_thumbProfile(nullptr) , m_capture(new MediaCapture(this)) { } void Core::prepareShutdown() { m_guiConstructed = false; m_mainWindow->getCurrentTimeline()->controller()->prepareClose(); projectItemModel()->blockSignals(true); QThreadPool::globalInstance()->clear(); } Core::~Core() { qDebug() << "deleting core"; if (m_monitorManager) { delete m_monitorManager; } // delete m_binWidget; if (m_projectManager) { delete m_projectManager; } ClipController::mediaUnavailable.reset(); } void Core::build(bool isAppImage, const QString &MltPath) { if (m_self) { return; } m_self.reset(new Core()); m_self->initLocale(); qRegisterMetaType("audioShortVector"); qRegisterMetaType>("QVector"); qRegisterMetaType("MessageType"); qRegisterMetaType("stringMap"); qRegisterMetaType("audioByteArray"); qRegisterMetaType>("QList"); qRegisterMetaType>("std::shared_ptr"); qRegisterMetaType>(); qRegisterMetaType("QDomElement"); qRegisterMetaType("requestClipInfo"); if (isAppImage) { QString appPath = qApp->applicationDirPath(); KdenliveSettings::setFfmpegpath(QDir::cleanPath(appPath + QStringLiteral("/ffmpeg"))); KdenliveSettings::setFfplaypath(QDir::cleanPath(appPath + QStringLiteral("/ffplay"))); KdenliveSettings::setFfprobepath(QDir::cleanPath(appPath + QStringLiteral("/ffprobe"))); KdenliveSettings::setRendererpath(QDir::cleanPath(appPath + QStringLiteral("/melt"))); MltConnection::construct(QDir::cleanPath(appPath + QStringLiteral("/../share/mlt/profiles"))); } else { // Open connection with Mlt MltConnection::construct(MltPath); } // load the profile from disk ProfileRepository::get()->refresh(); // load default profile m_self->m_profile = KdenliveSettings::default_profile(); if (m_self->m_profile.isEmpty()) { m_self->m_profile = ProjectManager::getDefaultProjectFormat(); KdenliveSettings::setDefault_profile(m_self->m_profile); } // Init producer shown for unavailable media // TODO make it a more proper image, it currently causes a crash on exit ClipController::mediaUnavailable = std::make_shared(ProfileRepository::get()->getProfile(m_self->m_profile)->profile(), "color:blue"); ClipController::mediaUnavailable->set("length", 99999999); m_self->m_projectItemModel = ProjectItemModel::construct(); // Job manager must be created before bin to correctly connect m_self->m_jobManager.reset(new JobManager(m_self.get())); } void Core::initGUI(const QUrl &Url, const QString &clipsToLoad) { m_profile = KdenliveSettings::default_profile(); m_currentProfile = m_profile; profileChanged(); m_mainWindow = new MainWindow(); m_guiConstructed = true; QStringList styles = QQuickStyle::availableStyles(); if (styles.contains(QLatin1String("org.kde.desktop"))) { QQuickStyle::setStyle("org.kde.desktop"); } else if (styles.contains(QLatin1String("Fusion"))) { QQuickStyle::setStyle("Fusion"); } connect(this, &Core::showConfigDialog, m_mainWindow, &MainWindow::slotPreferences); // load default profile and ask user to select one if not found. if (m_profile.isEmpty()) { m_profile = ProjectManager::getDefaultProjectFormat(); profileChanged(); KdenliveSettings::setDefault_profile(m_profile); } if (!ProfileRepository::get()->profileExists(m_profile)) { KMessageBox::sorry(m_mainWindow, i18n("The default profile of Kdenlive is not set or invalid, press OK to set it to a correct value.")); // TODO this simple widget should be improved and probably use profileWidget // we get the list of profiles QVector> all_profiles = ProfileRepository::get()->getAllProfiles(); QStringList all_descriptions; for (const auto &profile : all_profiles) { all_descriptions << profile.first; } // ask the user bool ok; QString item = QInputDialog::getItem(m_mainWindow, i18n("Select Default Profile"), i18n("Profile:"), all_descriptions, 0, false, &ok); if (ok) { ok = false; for (const auto &profile : all_profiles) { if (profile.first == item) { m_profile = profile.second; ok = true; } } } if (!ok) { KMessageBox::error( m_mainWindow, i18n("The given profile is invalid. We default to the profile \"dv_pal\", but you can change this from Kdenlive's settings panel")); m_profile = QStringLiteral("dv_pal"); } KdenliveSettings::setDefault_profile(m_profile); profileChanged(); } m_projectManager = new ProjectManager(this); m_binWidget = new Bin(m_projectItemModel, m_mainWindow); m_library = new LibraryWidget(m_projectManager, m_mainWindow); m_mixerWidget = new MixerManager(m_mainWindow); connect(m_library, SIGNAL(addProjectClips(QList)), m_binWidget, SLOT(droppedUrls(QList))); connect(this, &Core::updateLibraryPath, m_library, &LibraryWidget::slotUpdateLibraryPath); connect(m_capture.get(), &MediaCapture::recordStateChanged, m_mixerWidget, &MixerManager::recordStateChanged); connect(m_mixerWidget, &MixerManager::updateRecVolume, m_capture.get(), &MediaCapture::setAudioVolume); m_monitorManager = new MonitorManager(this); connect(m_monitorManager, &MonitorManager::cleanMixer, m_mixerWidget, &MixerManager::clearMixers); // Producer queue, creating MLT::Producers on request /* m_producerQueue = new ProducerQueue(m_binController); connect(m_producerQueue, &ProducerQueue::gotFileProperties, m_binWidget, &Bin::slotProducerReady); connect(m_producerQueue, &ProducerQueue::replyGetImage, m_binWidget, &Bin::slotThumbnailReady); connect(m_producerQueue, &ProducerQueue::requestProxy, [this](const QString &id){ m_binWidget->startJob(id, AbstractClipJob::PROXYJOB);}); connect(m_producerQueue, &ProducerQueue::removeInvalidClip, m_binWidget, &Bin::slotRemoveInvalidClip, Qt::DirectConnection); connect(m_producerQueue, SIGNAL(addClip(QString, QMap)), m_binWidget, SLOT(slotAddUrl(QString, QMap))); connect(m_binController.get(), SIGNAL(createThumb(QDomElement, QString, int)), m_producerQueue, SLOT(getFileProperties(QDomElement, QString, int))); connect(m_binWidget, &Bin::producerReady, m_producerQueue, &ProducerQueue::slotProcessingDone, Qt::DirectConnection); // TODO connect(m_producerQueue, SIGNAL(removeInvalidProxy(QString,bool)), m_binWidget, SLOT(slotRemoveInvalidProxy(QString,bool)));*/ m_mainWindow->init(); projectManager()->init(Url, clipsToLoad); if (qApp->isSessionRestored()) { // NOTE: we are restoring only one window, because Kdenlive only uses one MainWindow m_mainWindow->restore(1, false); } QMetaObject::invokeMethod(pCore->projectManager(), "slotLoadOnOpen", Qt::QueuedConnection); m_mainWindow->show(); QThreadPool::globalInstance()->setMaxThreadCount(qMin(4, QThreadPool::globalInstance()->maxThreadCount())); } void Core::buildLumaThumbs(const QStringList &values) { for (auto &entry : values) { if (MainWindow::m_lumacache.contains(entry)) { continue; } QImage pix(entry); if (!pix.isNull()) { MainWindow::m_lumacache.insert(entry, pix.scaled(50, 30, Qt::KeepAspectRatio, Qt::SmoothTransformation)); } } } std::unique_ptr &Core::self() { if (!m_self) { qDebug() << "Error : Core has not been created"; } return m_self; } MainWindow *Core::window() { return m_mainWindow; } ProjectManager *Core::projectManager() { return m_projectManager; } MonitorManager *Core::monitorManager() { return m_monitorManager; } Monitor *Core::getMonitor(int id) { if (id == Kdenlive::ClipMonitor) { return m_monitorManager->clipMonitor(); } return m_monitorManager->projectMonitor(); } Bin *Core::bin() { return m_binWidget; } void Core::selectBinClip(const QString &clipId, int frame, const QPoint &zone) { m_binWidget->selectClipById(clipId, frame, zone); } std::shared_ptr Core::jobManager() { return m_jobManager; } LibraryWidget *Core::library() { return m_library; } MixerManager *Core::mixer() { return m_mixerWidget; } void Core::initLocale() { QLocale systemLocale = QLocale(); #ifndef Q_OS_MAC setlocale(LC_NUMERIC, nullptr); #else setlocale(LC_NUMERIC_MASK, nullptr); #endif // localeconv()->decimal_point does not give reliable results on Windows #ifndef Q_OS_WIN char *separator = localeconv()->decimal_point; if (QString::fromUtf8(separator) != QChar(systemLocale.decimalPoint())) { // qCDebug(KDENLIVE_LOG)<<"------\n!!! system locale is not similar to Qt's locale... be prepared for bugs!!!\n------"; // HACK: There is a locale conflict, so set locale to C // Make sure to override exported values or it won't work qputenv("LANG", "C"); #ifndef Q_OS_MAC setlocale(LC_NUMERIC, "C"); #else setlocale(LC_NUMERIC_MASK, "C"); #endif systemLocale = QLocale::c(); } #endif systemLocale.setNumberOptions(QLocale::OmitGroupSeparator); QLocale::setDefault(systemLocale); } std::unique_ptr &Core::getMltRepository() { return MltConnection::self()->getMltRepository(); } std::unique_ptr &Core::getCurrentProfile() const { return ProfileRepository::get()->getProfile(m_currentProfile); } Mlt::Profile *Core::getProjectProfile() { if (!m_projectProfile) { m_projectProfile = std::make_unique(m_currentProfile.toStdString().c_str()); m_projectProfile->set_explicit(1); } return m_projectProfile.get(); } const QString &Core::getCurrentProfilePath() const { return m_currentProfile; } bool Core::setCurrentProfile(const QString &profilePath) { if (m_currentProfile == profilePath) { // no change required, ensure timecode has correct fps m_timecode.setFormat(getCurrentProfile()->fps()); return true; } if (ProfileRepository::get()->profileExists(profilePath)) { m_currentProfile = profilePath; m_thumbProfile.reset(); if (m_projectProfile) { m_projectProfile->set_colorspace(getCurrentProfile()->colorspace()); m_projectProfile->set_frame_rate(getCurrentProfile()->frame_rate_num(), getCurrentProfile()->frame_rate_den()); m_projectProfile->set_height(getCurrentProfile()->height()); m_projectProfile->set_progressive(getCurrentProfile()->progressive()); m_projectProfile->set_sample_aspect(getCurrentProfile()->sample_aspect_num(), getCurrentProfile()->sample_aspect_den()); m_projectProfile->set_display_aspect(getCurrentProfile()->display_aspect_num(), getCurrentProfile()->display_aspect_den()); m_projectProfile->set_width(getCurrentProfile()->width()); m_projectProfile->set_explicit(true); } // inform render widget m_timecode.setFormat(getCurrentProfile()->fps()); profileChanged(); m_mainWindow->updateRenderWidgetProfile(); pCore->monitorManager()->resetProfiles(); pCore->monitorManager()->updatePreviewScaling(); if (m_guiConstructed && m_mainWindow->getCurrentTimeline()->controller()->getModel()) { m_mainWindow->getCurrentTimeline()->controller()->getModel()->updateProfile(getProjectProfile()); checkProfileValidity(); } return true; } return false; } void Core::checkProfileValidity() { int offset = (getCurrentProfile()->profile().width() % 8) + (getCurrentProfile()->profile().height() % 2); if (offset > 0) { // Profile is broken, warn user if (m_binWidget) { m_binWidget->displayBinMessage(i18n("Your project profile is invalid, rendering might fail."), KMessageWidget::Warning); } } } double Core::getCurrentSar() const { return getCurrentProfile()->sar(); } double Core::getCurrentDar() const { return getCurrentProfile()->dar(); } double Core::getCurrentFps() const { return getCurrentProfile()->fps(); } QSize Core::getCurrentFrameDisplaySize() const { return {(int)(getCurrentProfile()->height() * getCurrentDar() + 0.5), getCurrentProfile()->height()}; } QSize Core::getCurrentFrameSize() const { return {getCurrentProfile()->width(), getCurrentProfile()->height()}; } void Core::requestMonitorRefresh() { if (!m_guiConstructed) return; m_monitorManager->refreshProjectMonitor(); } void Core::refreshProjectRange(QSize range) { if (!m_guiConstructed) return; m_monitorManager->refreshProjectRange(range); } int Core::getItemPosition(const ObjectId &id) { if (!m_guiConstructed) return 0; switch (id.first) { case ObjectType::TimelineClip: if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isClip(id.second)) { return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getClipPosition(id.second); } break; case ObjectType::TimelineComposition: if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isComposition(id.second)) { return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getCompositionPosition(id.second); } break; case ObjectType::BinClip: case ObjectType::TimelineTrack: case ObjectType::Master: return 0; break; default: qDebug() << "ERROR: unhandled object type"; } return 0; } int Core::getItemIn(const ObjectId &id) { if (!m_guiConstructed || !m_mainWindow->getCurrentTimeline() || !m_mainWindow->getCurrentTimeline()->controller()->getModel()) { qDebug() << "/ / // QUERYING ITEM IN BUT GUI NOT BUILD!!"; return 0; } switch (id.first) { case ObjectType::TimelineClip: if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isClip(id.second)) { return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getClipIn(id.second); } else { qDebug()<<"// ERROR QUERYING NON CLIP PROPERTIES\n\n!!!!!!!!!!!!!!!!!!!!!!!!!!"; } break; case ObjectType::TimelineComposition: case ObjectType::BinClip: case ObjectType::TimelineTrack: case ObjectType::Master: return 0; break; default: qDebug() << "ERROR: unhandled object type"; } return 0; } PlaylistState::ClipState Core::getItemState(const ObjectId &id) { if (!m_guiConstructed) return PlaylistState::Disabled; switch (id.first) { case ObjectType::TimelineClip: if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isClip(id.second)) { return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getClipState(id.second); } break; case ObjectType::TimelineComposition: return PlaylistState::VideoOnly; break; case ObjectType::BinClip: return m_binWidget->getClipState(id.second); break; case ObjectType::TimelineTrack: return m_mainWindow->getCurrentTimeline()->controller()->getModel()->isAudioTrack(id.second) ? PlaylistState::AudioOnly : PlaylistState::VideoOnly; case ObjectType::Master: return PlaylistState::Disabled; break; default: qDebug() << "ERROR: unhandled object type"; break; } return PlaylistState::Disabled; } int Core::getItemDuration(const ObjectId &id) { if (!m_guiConstructed) return 0; switch (id.first) { case ObjectType::TimelineClip: if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isClip(id.second)) { return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getClipPlaytime(id.second); } break; case ObjectType::TimelineComposition: if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isComposition(id.second)) { return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getCompositionPlaytime(id.second); } break; case ObjectType::BinClip: return (int)m_binWidget->getClipDuration(id.second); break; case ObjectType::TimelineTrack: case ObjectType::Master: return m_mainWindow->getCurrentTimeline()->controller()->duration() - 1; default: qDebug() << "ERROR: unhandled object type"; } return 0; } int Core::getItemTrack(const ObjectId &id) { if (!m_guiConstructed) return 0; switch (id.first) { case ObjectType::TimelineClip: case ObjectType::TimelineComposition: return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getItemTrackId(id.second); break; default: qDebug() << "ERROR: unhandled object type"; } return 0; } void Core::refreshProjectItem(const ObjectId &id) { if (!m_guiConstructed || m_mainWindow->getCurrentTimeline()->loading) return; switch (id.first) { case ObjectType::TimelineClip: if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isClip(id.second)) { m_mainWindow->getCurrentTimeline()->controller()->refreshItem(id.second); } break; case ObjectType::TimelineComposition: if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isComposition(id.second)) { m_mainWindow->getCurrentTimeline()->controller()->refreshItem(id.second); } break; case ObjectType::TimelineTrack: if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isTrack(id.second)) { requestMonitorRefresh(); } break; case ObjectType::BinClip: m_monitorManager->activateMonitor(Kdenlive::ClipMonitor); m_monitorManager->refreshClipMonitor(); if (m_monitorManager->projectMonitorVisible() && m_mainWindow->getCurrentTimeline()->controller()->refreshIfVisible(id.second)) { m_monitorManager->refreshTimer.start(); } break; case ObjectType::Master: requestMonitorRefresh(); break; default: qDebug() << "ERROR: unhandled object type"; } } bool Core::hasTimelinePreview() const { if (!m_guiConstructed) { return false; } return m_mainWindow->getCurrentTimeline()->controller()->renderedChunks().size() > 0; } KdenliveDoc *Core::currentDoc() { return m_projectManager->current(); } Timecode Core::timecode() const { return m_timecode; } void Core::setDocumentModified() { m_projectManager->current()->setModified();; } int Core::projectDuration() const { if (!m_guiConstructed) { return 0; } return m_mainWindow->getCurrentTimeline()->controller()->duration(); } void Core::profileChanged() { GenTime::setFps(getCurrentFps()); } void Core::pushUndo(const Fun &undo, const Fun &redo, const QString &text) { undoStack()->push(new FunctionalUndoCommand(undo, redo, text)); } void Core::pushUndo(QUndoCommand *command) { undoStack()->push(command); } int Core::undoIndex() const { return m_projectManager->undoStack()->index(); } void Core::displayMessage(const QString &message, MessageType type, int timeout) { if (m_mainWindow) { if (type == ProcessingJobMessage || type == OperationCompletedMessage) { m_mainWindow->displayProgressMessage(message, type, timeout); } else { m_mainWindow->displayMessage(message, type, timeout); } } else { qDebug() << message; } } -void Core::displayBinMessage(const QString &text, int type, const QList &actions) +void Core::displayBinMessage(const QString &text, int type, const QList &actions, bool showClose, BinMessage::BinCategory messageCategory) { - m_binWidget->doDisplayMessage(text, (KMessageWidget::MessageType)type, actions); + m_binWidget->doDisplayMessage(text, (KMessageWidget::MessageType)type, actions, showClose, messageCategory); } void Core::displayBinLogMessage(const QString &text, int type, const QString &logInfo) { m_binWidget->doDisplayMessage(text, (KMessageWidget::MessageType)type, logInfo); } void Core::clearAssetPanel(int itemId) { if (m_guiConstructed) m_mainWindow->clearAssetPanel(itemId); } std::shared_ptr Core::getItemEffectStack(int itemType, int itemId) { if (!m_guiConstructed) return nullptr; switch (itemType) { case (int)ObjectType::TimelineClip: return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getClipEffectStack(itemId); case (int)ObjectType::TimelineTrack: return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getTrackEffectStackModel(itemId); break; case (int)ObjectType::BinClip: return m_binWidget->getClipEffectStack(itemId); case (int)ObjectType::Master: return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getMasterEffectStackModel(); default: return nullptr; } } std::shared_ptr Core::undoStack() { return projectManager()->undoStack(); } QMap Core::getTrackNames(bool videoOnly) { if (!m_guiConstructed) return QMap(); return m_mainWindow->getCurrentTimeline()->controller()->getTrackNames(videoOnly); } QPair Core::getCompositionATrack(int cid) const { if (!m_guiConstructed) return {}; return m_mainWindow->getCurrentTimeline()->controller()->getCompositionATrack(cid); } bool Core::compositionAutoTrack(int cid) const { return m_mainWindow->getCurrentTimeline()->controller()->compositionAutoTrack(cid); } void Core::setCompositionATrack(int cid, int aTrack) { if (!m_guiConstructed) return; m_mainWindow->getCurrentTimeline()->controller()->setCompositionATrack(cid, aTrack); } std::shared_ptr Core::projectItemModel() { return m_projectItemModel; } void Core::invalidateRange(QSize range) { if (!m_guiConstructed || m_mainWindow->getCurrentTimeline()->loading) return; m_mainWindow->getCurrentTimeline()->controller()->invalidateZone(range.width(), range.height()); } void Core::invalidateItem(ObjectId itemId) { if (!m_guiConstructed || !m_mainWindow->getCurrentTimeline() || m_mainWindow->getCurrentTimeline()->loading) return; switch (itemId.first) { case ObjectType::TimelineClip: case ObjectType::TimelineComposition: m_mainWindow->getCurrentTimeline()->controller()->invalidateItem(itemId.second); break; case ObjectType::TimelineTrack: m_mainWindow->getCurrentTimeline()->controller()->invalidateTrack(itemId.second); break; case ObjectType::BinClip: m_binWidget->invalidateClip(QString::number(itemId.second)); break; case ObjectType::Master: m_mainWindow->getCurrentTimeline()->controller()->invalidateZone(0, -1); break; default: // compositions should not have effects break; } } double Core::getClipSpeed(int id) const { return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getClipSpeed(id); } void Core::updateItemKeyframes(ObjectId id) { if (id.first == ObjectType::TimelineClip && m_guiConstructed) { m_mainWindow->getCurrentTimeline()->controller()->updateClip(id.second, {TimelineModel::KeyframesRole}); } } void Core::updateItemModel(ObjectId id, const QString &service) { if (m_guiConstructed && id.first == ObjectType::TimelineClip && !m_mainWindow->getCurrentTimeline()->loading && service.startsWith(QLatin1String("fade"))) { bool startFade = service == QLatin1String("fadein") || service == QLatin1String("fade_from_black"); m_mainWindow->getCurrentTimeline()->controller()->updateClip(id.second, {startFade ? TimelineModel::FadeInRole : TimelineModel::FadeOutRole}); } } void Core::showClipKeyframes(ObjectId id, bool enable) { if (id.first == ObjectType::TimelineClip) { m_mainWindow->getCurrentTimeline()->controller()->showClipKeyframes(id.second, enable); } else if (id.first == ObjectType::TimelineComposition) { m_mainWindow->getCurrentTimeline()->controller()->showCompositionKeyframes(id.second, enable); } } Mlt::Profile *Core::thumbProfile() { QMutexLocker lck(&m_thumbProfileMutex); if (!m_thumbProfile) { m_thumbProfile = std::make_unique(m_currentProfile.toStdString().c_str()); double factor = 144. / m_thumbProfile->height(); m_thumbProfile->set_height(144); int width = m_thumbProfile->width() * factor + 0.5; if (width % 2 > 0) { width ++; } m_thumbProfile->set_width(width); } return m_thumbProfile.get(); } int Core::getTimelinePosition() const { if (m_guiConstructed) { return m_monitorManager->projectMonitor()->position(); } return 0; } void Core::triggerAction(const QString &name) { QAction *action = m_mainWindow->actionCollection()->action(name); if (action) { action->trigger(); } } void Core::clean() { m_self.reset(); } void Core::startMediaCapture(int tid, bool checkAudio, bool checkVideo) { if (checkAudio && checkVideo) { m_capture->recordVideo(tid, true); } else if (checkAudio) { m_capture->recordAudio(tid, true); } m_mediaCaptureFile = m_capture->getCaptureOutputLocation(); } void Core::stopMediaCapture(int tid, bool checkAudio, bool checkVideo) { if (checkAudio && checkVideo) { m_capture->recordVideo(tid, false); } else if (checkAudio) { m_capture->recordAudio(tid, false); } } QStringList Core::getAudioCaptureDevices() { return m_capture->getAudioCaptureDevices(); } int Core::getMediaCaptureState() { return m_capture->getState(); } bool Core::isMediaCapturing() { return m_capture->isRecording(); } MediaCapture *Core::getAudioDevice() { return m_capture.get(); } QString Core::getProjectFolderName() { if (currentDoc()) { return currentDoc()->projectDataFolder() + QDir::separator(); } return QString(); } QString Core::getTimelineClipBinId(int cid) { if (!m_guiConstructed) { return QString(); } if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isClip(cid)) { return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getClipBinId(cid); } return QString(); } int Core::getDurationFromString(const QString &time) { if (!m_guiConstructed) { return 0; } const QString duration = currentDoc()->timecode().reformatSeparators(time); return currentDoc()->timecode().getFrameCount(duration); } void Core::processInvalidFilter(const QString service, const QString id, const QString message) { if (m_guiConstructed) m_mainWindow->assetPanelWarning(service, id, message); } void Core::updateProjectTags(QMap tags) { // Clear previous tags for (int i = 1 ; i< 20; i++) { QString current = currentDoc()->getDocumentProperty(QString("tag%1").arg(i)); if (current.isEmpty()) { break; } else { currentDoc()->setDocumentProperty(QString("tag%1").arg(i), QString()); } } QMapIterator j(tags); int i = 1; while (j.hasNext()) { j.next(); currentDoc()->setDocumentProperty(QString("tag%1").arg(i), QString("%1:%2").arg(j.key()).arg(j.value())); i++; } } std::unique_ptr Core::getMasterProducerInstance() { if (m_guiConstructed && m_mainWindow->getCurrentTimeline()) { std::unique_ptr producer(m_mainWindow->getCurrentTimeline()->controller()->tractor()->cut(0, m_mainWindow->getCurrentTimeline()->controller()->duration() - 1)); return producer; } return nullptr; } std::unique_ptr Core::getTrackProducerInstance(int tid) { if (m_guiConstructed && m_mainWindow->getCurrentTimeline()) { std::unique_ptr producer(new Mlt::Producer(m_mainWindow->getCurrentTimeline()->controller()->trackProducer(tid))); return producer; } return nullptr; } bool Core::enableMultiTrack(bool enable) { if (!m_guiConstructed || !m_mainWindow->getCurrentTimeline()) { return false; } bool isMultiTrack = pCore->monitorManager()->isMultiTrack(); if (isMultiTrack || enable) { pCore->window()->getMainTimeline()->controller()->slotMultitrackView(enable, enable); return true; } return false; } int Core::audioChannels() { if (m_projectManager && m_projectManager->current()) { return m_projectManager->current()->audioChannels(); } return 2; } diff --git a/src/core.h b/src/core.h index 7729e6af3..bb2b2f696 100644 --- a/src/core.h +++ b/src/core.h @@ -1,275 +1,275 @@ /* Copyright (C) 2014 Till Theato This file is part of kdenlive. See www.kdenlive.org. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. */ #ifndef CORE_H #define CORE_H #include "definitions.h" #include "kdenlivecore_export.h" #include "undohelper.hpp" #include #include #include #include #include #include "timecode.h" class Bin; class DocUndoStack; class EffectStackModel; class JobManager; class KdenliveDoc; class LibraryWidget; class MainWindow; class MediaCapture; class MixerManager; class Monitor; class MonitorManager; class ProfileModel; class ProjectItemModel; class ProjectManager; namespace Mlt { class Repository; class Producer; class Profile; } // namespace Mlt #define EXIT_RESTART (42) #define EXIT_CLEAN_RESTART (43) #define pCore Core::self() /** * @class Core * @brief Singleton that provides access to the different parts of Kdenlive * * Needs to be initialize before any widgets are created in MainWindow. * Plugins should be loaded after the widget setup. */ class /*KDENLIVECORE_EXPORT*/ Core : public QObject { Q_OBJECT public: Core(const Core &) = delete; Core &operator=(const Core &) = delete; Core(Core &&) = delete; Core &operator=(Core &&) = delete; ~Core() override; /** * @brief Setup the basics of the application, in particular the connection * with Mlt * @param isAppImage do we expect an AppImage (if yes, we use App path to deduce * other binaries paths (melt, ffmpeg, etc) * @param MltPath (optional) path to MLT environment */ static void build(bool isAppImage, const QString &MltPath = QString()); /** * @brief Init the GUI part of the app and show the main window * @param Url (optional) file to open * If Url is present, it will be opened, otherwise, if openlastproject is * set, latest project will be opened. If no file is open after trying this, * a default new file will be created. */ void initGUI(const QUrl &Url, const QString &clipsToLoad = QString()); /** @brief Returns a pointer to the singleton object. */ static std::unique_ptr &self(); /** @brief Delete the global core instance */ static void clean(); /** @brief Returns a pointer to the main window. */ MainWindow *window(); /** @brief Returns a pointer to the project manager. */ ProjectManager *projectManager(); /** @brief Returns a pointer to the current project. */ KdenliveDoc *currentDoc(); /** @brief Returns project's timecode. */ Timecode timecode() const; /** @brief Set current project modified. */ void setDocumentModified(); /** @brief Returns a pointer to the monitor manager. */ MonitorManager *monitorManager(); /** @brief Returns a pointer to the view of the project bin. */ Bin *bin(); /** @brief Select a clip in the Bin from its id. */ void selectBinClip(const QString &id, int frame = -1, const QPoint &zone = QPoint()); /** @brief Returns a pointer to the model of the project bin. */ std::shared_ptr projectItemModel(); /** @brief Returns a pointer to the job manager. Please do not store it. */ std::shared_ptr jobManager(); /** @brief Returns a pointer to the library. */ LibraryWidget *library(); /** @brief Returns a pointer to the audio mixer. */ MixerManager *mixer(); /** @brief Returns a pointer to MLT's repository */ std::unique_ptr &getMltRepository(); /** @brief Returns a pointer to the current profile */ std::unique_ptr &getCurrentProfile() const; const QString &getCurrentProfilePath() const; /** @brief Define the active profile * @returns true if profile exists, false if not found */ bool setCurrentProfile(const QString &profilePath); /** @brief Returns Sample Aspect Ratio of current profile */ double getCurrentSar() const; /** @brief Returns Display Aspect Ratio of current profile */ double getCurrentDar() const; /** @brief Returns frame rate of current profile */ double getCurrentFps() const; /** @brief Returns the frame size (width x height) of current profile */ QSize getCurrentFrameSize() const; /** @brief Returns the frame display size (width x height) of current profile */ QSize getCurrentFrameDisplaySize() const; /** @brief Request project monitor refresh */ void requestMonitorRefresh(); /** @brief Request project monitor refresh if current position is inside range*/ void refreshProjectRange(QSize range); /** @brief Request project monitor refresh if referenced item is under cursor */ void refreshProjectItem(const ObjectId &id); /** @brief Returns a reference to a monitor (clip or project monitor) */ Monitor *getMonitor(int id); /** @brief This function must be called whenever the profile used changes */ void profileChanged(); /** @brief Create and push and undo object based on the corresponding functions Note that if you class permits and requires it, you should use the macro PUSH_UNDO instead*/ void pushUndo(const Fun &undo, const Fun &redo, const QString &text); void pushUndo(QUndoCommand *command); /** @brief display a user info/warning message in statusbar */ void displayMessage(const QString &message, MessageType type, int timeout = -1); /** @brief Clear asset view if itemId is displayed. */ void clearAssetPanel(int itemId); /** @brief Returns the effectstack of a given bin clip. */ std::shared_ptr getItemEffectStack(int itemType, int itemId); int getItemPosition(const ObjectId &id); int getItemIn(const ObjectId &id); int getItemTrack(const ObjectId &id); int getItemDuration(const ObjectId &id); /** @brief Returns the capabilities of a clip: AudioOnly, VideoOnly or Disabled if both are allowed */ PlaylistState::ClipState getItemState(const ObjectId &id); /** @brief Get a list of video track names with indexes */ QMap getTrackNames(bool videoOnly); /** @brief Returns the composition A track (MLT index / Track id) */ QPair getCompositionATrack(int cid) const; void setCompositionATrack(int cid, int aTrack); /* @brief Return true if composition's a_track is automatic (no forced track) */ bool compositionAutoTrack(int cid) const; std::shared_ptr undoStack(); double getClipSpeed(int id) const; /** @brief Mark an item as invalid for timeline preview */ void invalidateItem(ObjectId itemId); void invalidateRange(QSize range); void prepareShutdown(); /** the keyframe model changed (effect added, deleted, active effect changed), inform timeline */ void updateItemKeyframes(ObjectId id); /** A fade for clip id changed, update timeline */ void updateItemModel(ObjectId id, const QString &service); /** Show / hide keyframes for a timeline clip */ void showClipKeyframes(ObjectId id, bool enable); Mlt::Profile *thumbProfile(); /** @brief Returns the current project duration */ int projectDuration() const; /** @brief Returns true if current project has some rendered timeline preview */ bool hasTimelinePreview() const; /** @brief Returns current timeline cursor position */ int getTimelinePosition() const; /** @brief Handles audio and video capture **/ void startMediaCapture(int tid, bool, bool); void stopMediaCapture(int tid, bool, bool); QStringList getAudioCaptureDevices(); int getMediaCaptureState(); bool isMediaCapturing(); MediaCapture *getAudioDevice(); /** @brief Returns Project Folder name for capture output location */ QString getProjectFolderName(); /** @brief Returns a timeline clip's bin id */ QString getTimelineClipBinId(int cid); /** @brief Returns a frame duration from a timecode */ int getDurationFromString(const QString &time); /** @brief An error occurred within a filter, inform user */ void processInvalidFilter(const QString service, const QString id, const QString message); /** @brief Update current project's tags */ void updateProjectTags(QMap tags); /** @brief Returns the consumer profile, that will be scaled * according to preview settings. Should only be used on the consumer */ Mlt::Profile *getProjectProfile(); /** @brief Returns a copy of current timeline's master playlist */ std::unique_ptr getMasterProducerInstance(); /** @brief Returns a copy of a track's playlist */ std::unique_ptr getTrackProducerInstance(int tid); /** @brief Returns the undo stack index (position). */ int undoIndex() const; /** @brief Enable / disable monitor multitrack view. Returns false if multitrack was not previously enabled */ bool enableMultiTrack(bool enable); /** @brief Returns number of audio channels for this project. */ int audioChannels(); private: explicit Core(); static std::unique_ptr m_self; /** @brief Makes sure Qt's locale and system locale settings match. */ void initLocale(); MainWindow *m_mainWindow{nullptr}; ProjectManager *m_projectManager{nullptr}; MonitorManager *m_monitorManager{nullptr}; std::shared_ptr m_projectItemModel; std::shared_ptr m_jobManager; Bin *m_binWidget{nullptr}; LibraryWidget *m_library{nullptr}; MixerManager *m_mixerWidget{nullptr}; /** @brief Current project's profile path */ QString m_currentProfile; QString m_profile; Timecode m_timecode; std::unique_ptr m_thumbProfile; /** @brief Mlt profile used in the consumer 's monitors */ std::unique_ptr m_projectProfile; bool m_guiConstructed = false; /** @brief Check that the profile is valid (width is a multiple of 8 and height a multiple of 2 */ void checkProfileValidity(); std::unique_ptr m_capture; QUrl m_mediaCaptureFile; QMutex m_thumbProfileMutex; public slots: void triggerAction(const QString &name); /** @brief display a user info/warning message in the project bin */ - void displayBinMessage(const QString &text, int type, const QList &actions = QList()); + void displayBinMessage(const QString &text, int type, const QList &actions = QList(), bool showClose = false, BinMessage::BinCategory messageCategory = BinMessage::BinCategory::NoMessage); void displayBinLogMessage(const QString &text, int type, const QString &logInfo); /** @brief Create small thumbnails for luma used in compositions */ void buildLumaThumbs(const QStringList &values); signals: void coreIsReady(); void updateLibraryPath(); void updateMonitorProfile(); /** @brief Call config dialog on a selected page / tab */ void showConfigDialog(int, int); void finalizeRecording(const QString &captureFile); void autoScrollChanged(); }; #endif diff --git a/src/definitions.h b/src/definitions.h index deadb60a1..a723e1000 100644 --- a/src/definitions.h +++ b/src/definitions.h @@ -1,334 +1,338 @@ /*************************************************************************** * Copyright (C) 2007 by Jean-Baptiste Mardelle (jb@kdenlive.org) * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #ifndef DEFINITIONS_H #define DEFINITIONS_H #include "gentime.h" #include "kdenlive_debug.h" #include #include #include #include #include #include const int MAXCLIPDURATION = 15000; namespace Kdenlive { enum MonitorId { NoMonitor = 0x01, ClipMonitor = 0x02, ProjectMonitor = 0x04, RecordMonitor = 0x08, StopMotionMonitor = 0x10, DvdMonitor = 0x20 }; const int DefaultThumbHeight = 100; } // namespace Kdenlive enum class GroupType { Normal, Selection, // in that case, the group is used to emulate a selection AVSplit, // in that case, the group links the audio and video of the same clip Leaf // This is a leaf (clip or composition) }; const QString groupTypeToStr(GroupType t); GroupType groupTypeFromStr(const QString &s); enum class ObjectType { TimelineClip, TimelineComposition, TimelineTrack, BinClip, Master, NoItem }; using ObjectId = std::pair; enum OperationType { None = 0, WaitingForConfirm, MoveOperation, ResizeStart, ResizeEnd, RollingStart, RollingEnd, RippleStart, RippleEnd, FadeIn, FadeOut, TransitionStart, TransitionEnd, MoveGuide, KeyFrame, Seek, Spacer, RubberSelection, ScrollTimeline, ZoomTimeline }; namespace PlaylistState { Q_NAMESPACE enum ClipState { VideoOnly = 1, AudioOnly = 2, Disabled = 3 }; Q_ENUM_NS(ClipState) } // namespace PlaylistState // returns a pair corresponding to (video, audio) std::pair stateToBool(PlaylistState::ClipState state); PlaylistState::ClipState stateFromBool(std::pair av); namespace TimelineMode { enum EditMode { NormalEdit = 0, OverwriteEdit = 1, InsertEdit = 2 }; } namespace AssetListType { Q_NAMESPACE enum AssetType { Preferred, Video, Audio, Custom, CustomAudio, Favorites, AudioComposition, VideoShortComposition, VideoComposition, AudioTransition, VideoTransition, Hidden = -1 }; Q_ENUM_NS(AssetType) } namespace ClipType { Q_NAMESPACE enum ProducerType { Unknown = 0, Audio = 1, Video = 2, AV = 3, Color = 4, Image = 5, Text = 6, SlideShow = 7, Virtual = 8, Playlist = 9, WebVfx = 10, TextTemplate = 11, QText = 12, Composition = 13, Track = 14, Qml = 15 }; Q_ENUM_NS(ProducerType) } // namespace ClipType enum ProjectItemType { ProjectClipType = 0, ProjectFolderType, ProjectSubclipType }; enum GraphicsRectItem { AVWidget = 70000, LabelWidget, TransitionWidget, GroupWidget }; enum ProjectTool { SelectTool = 0, RazorTool = 1, SpacerTool = 2 }; enum MonitorSceneType { MonitorSceneNone = 0, MonitorSceneDefault, MonitorSceneGeometry, MonitorSceneCorners, MonitorSceneRoto, MonitorSceneSplit, MonitorSceneRipple, MonitorSplitTrack }; enum MessageType { DefaultMessage, ProcessingJobMessage, OperationCompletedMessage, InformationMessage, ErrorMessage, MltError }; +namespace BinMessage { + enum BinCategory { NoMessage = 0, ProfileMessage, StreamsMessage, InformationMessage }; +}; + enum TrackType { AudioTrack = 0, VideoTrack = 1, AnyTrack = 2 }; enum CacheType { SystemCacheRoot = -1, CacheRoot = 0, CacheBase = 1, CachePreview = 2, CacheProxy = 3, CacheAudio = 4, CacheThumbs = 5 }; enum TrimMode { NormalTrim, RippleTrim, RollingTrim, SlipTrim, SlideTrim }; class TrackInfo { public: TrackType type; QString trackName; bool isMute; bool isBlind; bool isLocked; int duration; /*EffectsList effectsList; TrackInfo() : type(VideoTrack) , isMute(0) , isBlind(0) , isLocked(0) , duration(0) , effectsList(true) { }*/ }; struct requestClipInfo { QDomElement xml; QString clipId; int imageHeight; bool replaceProducer; bool operator==(const requestClipInfo &a) const { return clipId == a.clipId; } }; typedef QMap stringMap; typedef QMap> audioByteArray; using audioShortVector = QVector; class ItemInfo { public: /** startPos is the position where the clip starts on the track */ GenTime startPos; /** endPos is the duration where the clip ends on the track */ GenTime endPos; /** cropStart is the position where the sub-clip starts, relative to the clip's 0 position */ GenTime cropStart; /** cropDuration is the duration of the clip */ GenTime cropDuration; /** Track number */ int track{0}; ItemInfo() = default; bool isValid() const { return startPos != endPos; } bool contains(GenTime pos) const { if (startPos == endPos) { return true; } return (pos <= endPos && pos >= startPos); } bool operator==(const ItemInfo &a) const { return startPos == a.startPos && endPos == a.endPos && track == a.track && cropStart == a.cropStart; } }; class TransitionInfo { public: /** startPos is the position where the clip starts on the track */ GenTime startPos; /** endPos is the duration where the clip ends on the track */ GenTime endPos; /** the track on which the transition is (b_track)*/ int b_track{0}; /** the track on which the transition is applied (a_track)*/ int a_track{0}; /** Does the user request for a special a_track */ bool forceTrack{0}; TransitionInfo() = default; }; class CommentedTime { public: CommentedTime(); CommentedTime(const GenTime &time, QString comment, int markerType = 0); CommentedTime(const QString &hash, const GenTime &time); QString comment() const; GenTime time() const; /** @brief Returns a string containing infos needed to store marker info. string equals marker type + QLatin1Char(':') + marker comment */ QString hash() const; void setComment(const QString &comm); void setMarkerType(int t); int markerType() const; static QColor markerColor(int type); /* Implementation of > operator; Works identically as with basic types. */ bool operator>(const CommentedTime &op) const; /* Implementation of < operator; Works identically as with basic types. */ bool operator<(const CommentedTime &op) const; /* Implementation of >= operator; Works identically as with basic types. */ bool operator>=(const CommentedTime &op) const; /* Implementation of <= operator; Works identically as with basic types. */ bool operator<=(const CommentedTime &op) const; /* Implementation of == operator; Works identically as with basic types. */ bool operator==(const CommentedTime &op) const; /* Implementation of != operator; Works identically as with basic types. */ bool operator!=(const CommentedTime &op) const; private: GenTime m_time; QString m_comment; int m_type{0}; }; QDebug operator<<(QDebug qd, const ItemInfo &info); // we provide hash function for qstring and QPersistentModelIndex namespace std { #if (QT_VERSION < QT_VERSION_CHECK(5, 14, 0)) template <> struct hash { std::size_t operator()(const QString &k) const { return qHash(k); } }; #endif template <> struct hash { std::size_t operator()(const QPersistentModelIndex &k) const { return qHash(k); } }; } // namespace std // The following is a hack that allows one to use shared_from_this in the case of a multiple inheritance. // Credit: https://stackoverflow.com/questions/14939190/boost-shared-from-this-and-multiple-inheritance template struct enable_shared_from_this_virtual; class enable_shared_from_this_virtual_base : public std::enable_shared_from_this { using base_type = std::enable_shared_from_this; template friend struct enable_shared_from_this_virtual; std::shared_ptr shared_from_this() { return base_type::shared_from_this(); } std::shared_ptr shared_from_this() const { return base_type::shared_from_this(); } }; template struct enable_shared_from_this_virtual : virtual enable_shared_from_this_virtual_base { using base_type = enable_shared_from_this_virtual_base; public: std::shared_ptr shared_from_this() { std::shared_ptr result(base_type::shared_from_this(), static_cast(this)); return result; } std::shared_ptr shared_from_this() const { std::shared_ptr result(base_type::shared_from_this(), static_cast(this)); return result; } }; // This is a small trick to have a QAbstractItemModel with shared_from_this enabled without multiple inheritance // Be careful, if you use this class, you have to make sure to init weak_this_ when you construct a shared_ptr to your object template class QAbstractItemModel_shared_from_this : public QAbstractItemModel { protected: QAbstractItemModel_shared_from_this() : QAbstractItemModel() { } public: std::shared_ptr shared_from_this() { std::shared_ptr p(weak_this_); assert(p.get() == this); return p; } std::shared_ptr shared_from_this() const { std::shared_ptr p(weak_this_); assert(p.get() == this); return p; } public: // actually private, but avoids compiler template friendship issues mutable std::weak_ptr weak_this_; }; #endif diff --git a/src/doc/kdenlivedoc.cpp b/src/doc/kdenlivedoc.cpp index d07ba10a1..ee89baeb1 100644 --- a/src/doc/kdenlivedoc.cpp +++ b/src/doc/kdenlivedoc.cpp @@ -1,1768 +1,1768 @@ /*************************************************************************** * Copyright (C) 2007 by Jean-Baptiste Mardelle (jb@kdenlive.org) * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "kdenlivedoc.h" #include "bin/bin.h" #include "bin/bincommands.h" #include "bin/binplaylist.hpp" #include "bin/clipcreator.hpp" #include "bin/model/markerlistmodel.hpp" #include "bin/projectclip.h" #include "bin/projectitemmodel.h" #include "core.h" #include "dialogs/profilesdialog.h" #include "documentchecker.h" #include "documentvalidator.h" #include "docundostack.hpp" #include "effects/effectsrepository.hpp" #include "jobs/jobmanager.h" #include "kdenlivesettings.h" #include "mainwindow.h" #include "mltcontroller/clipcontroller.h" #include "profiles/profilemodel.hpp" #include "profiles/profilerepository.hpp" #include "project/projectcommands.h" #include "titler/titlewidget.h" #include "transitions/transitionsrepository.hpp" #include #include #include #include #include #include #include #include #include "kdenlive_debug.h" #include #include #include #include #include #include #include #include #include #include #ifdef Q_OS_MAC #include #endif const double DOCUMENTVERSION = 0.99; KdenliveDoc::KdenliveDoc(const QUrl &url, QString projectFolder, QUndoGroup *undoGroup, const QString &profileName, const QMap &properties, const QMap &metadata, const QPair &tracks, int audioChannels, bool *openBackup, MainWindow *parent) : QObject(parent) , m_autosave(nullptr) , m_url(url) , m_clipsCount(0) , m_commandStack(std::make_shared(undoGroup)) , m_modified(false) , m_documentOpenStatus(CleanProject) , m_projectFolder(std::move(projectFolder)) { m_guideModel.reset(new MarkerListModel(m_commandStack, this)); connect(m_guideModel.get(), &MarkerListModel::modelChanged, this, &KdenliveDoc::guidesChanged); connect(this, SIGNAL(updateCompositionMode(int)), parent, SLOT(slotUpdateCompositeAction(int))); bool success = false; connect(m_commandStack.get(), &QUndoStack::indexChanged, this, &KdenliveDoc::slotModified); connect(m_commandStack.get(), &DocUndoStack::invalidate, this, &KdenliveDoc::checkPreviewStack); // connect(m_commandStack, SIGNAL(cleanChanged(bool)), this, SLOT(setModified(bool))); // init default document properties m_documentProperties[QStringLiteral("zoom")] = QLatin1Char('8'); m_documentProperties[QStringLiteral("verticalzoom")] = QLatin1Char('1'); m_documentProperties[QStringLiteral("zonein")] = QLatin1Char('0'); m_documentProperties[QStringLiteral("zoneout")] = QStringLiteral("-1"); m_documentProperties[QStringLiteral("enableproxy")] = QString::number((int)KdenliveSettings::enableproxy()); m_documentProperties[QStringLiteral("proxyparams")] = KdenliveSettings::proxyparams(); m_documentProperties[QStringLiteral("proxyextension")] = KdenliveSettings::proxyextension(); m_documentProperties[QStringLiteral("previewparameters")] = KdenliveSettings::previewparams(); m_documentProperties[QStringLiteral("previewextension")] = KdenliveSettings::previewextension(); m_documentProperties[QStringLiteral("externalproxyparams")] = KdenliveSettings::externalProxyProfile(); m_documentProperties[QStringLiteral("enableexternalproxy")] = QString::number((int)KdenliveSettings::externalproxy()); m_documentProperties[QStringLiteral("generateproxy")] = QString::number((int)KdenliveSettings::generateproxy()); m_documentProperties[QStringLiteral("proxyminsize")] = QString::number(KdenliveSettings::proxyminsize()); m_documentProperties[QStringLiteral("generateimageproxy")] = QString::number((int)KdenliveSettings::generateimageproxy()); m_documentProperties[QStringLiteral("proxyimageminsize")] = QString::number(KdenliveSettings::proxyimageminsize()); m_documentProperties[QStringLiteral("proxyimagesize")] = QString::number(KdenliveSettings::proxyimagesize()); m_documentProperties[QStringLiteral("videoTarget")] = QString::number(tracks.second); m_documentProperties[QStringLiteral("audioTarget")] = QString::number(tracks.second - 1); m_documentProperties[QStringLiteral("activeTrack")] = QString::number(tracks.second); m_documentProperties[QStringLiteral("audioChannels")] = QString::number(audioChannels); m_documentProperties[QStringLiteral("enableTimelineZone")] = QLatin1Char('0'); m_documentProperties[QStringLiteral("zonein")] = QLatin1Char('0'); m_documentProperties[QStringLiteral("zoneout")] = QStringLiteral("75"); m_documentProperties[QStringLiteral("seekOffset")] = QString::number(TimelineModel::seekDuration); // Load properties QMapIterator i(properties); while (i.hasNext()) { i.next(); m_documentProperties[i.key()] = i.value(); } // Load metadata QMapIterator j(metadata); while (j.hasNext()) { j.next(); m_documentMetadata[j.key()] = j.value(); } /*if (QLocale().decimalPoint() != QLocale::system().decimalPoint()) { qDebug()<<"* * ** AARCH DOCUMENT PROBLEM;"; exit(1); setlocale(LC_NUMERIC, ""); QLocale systemLocale = QLocale::system(); systemLocale.setNumberOptions(QLocale::OmitGroupSeparator); QLocale::setDefault(systemLocale); // locale conversion might need to be redone ///TODO: how to reset repositories... //EffectsRepository::get()->init(); //TransitionsRepository::get()->init(); //initEffects::parseEffectFiles(pCore->getMltRepository(), QString::fromLatin1(setlocale(LC_NUMERIC, nullptr))); }*/ *openBackup = false; if (url.isValid()) { QFile file(url.toLocalFile()); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { // The file cannot be opened if (KMessageBox::warningContinueCancel(parent, i18n("Cannot open the project file,\nDo you want to open a backup file?"), i18n("Error opening file"), KGuiItem(i18n("Open Backup"))) == KMessageBox::Continue) { *openBackup = true; } // KMessageBox::error(parent, KIO::NetAccess::lastErrorString()); } else { qCDebug(KDENLIVE_LOG) << " // / processing file open"; QString errorMsg; int line; int col; QDomImplementation::setInvalidDataPolicy(QDomImplementation::DropInvalidChars); success = m_document.setContent(&file, false, &errorMsg, &line, &col); file.close(); if (!success) { // It is corrupted int answer = KMessageBox::warningYesNoCancel( parent, i18n("Cannot open the project file, error is:\n%1 (line %2, col %3)\nDo you want to open a backup file?", errorMsg, line, col), i18n("Error opening file"), KGuiItem(i18n("Open Backup")), KGuiItem(i18n("Recover"))); if (answer == KMessageBox::Yes) { *openBackup = true; } else if (answer == KMessageBox::No) { // Try to recover broken file produced by Kdenlive 0.9.4 if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { int correction = 0; QString playlist = QString::fromUtf8(file.readAll()); while (!success && correction < 2) { int errorPos = 0; line--; col = col - 2; for (int k = 0; k < line && errorPos < playlist.length(); ++k) { errorPos = playlist.indexOf(QLatin1Char('\n'), errorPos); errorPos++; } errorPos += col; if (errorPos >= playlist.length()) { break; } playlist.remove(errorPos, 1); line = 0; col = 0; success = m_document.setContent(playlist, false, &errorMsg, &line, &col); correction++; } if (!success) { KMessageBox::sorry(parent, i18n("Cannot recover this project file")); } else { // Document was modified, ask for backup QDomElement mlt = m_document.documentElement(); mlt.setAttribute(QStringLiteral("modified"), 1); } } } } else { qCDebug(KDENLIVE_LOG) << " // / processing file open: validate"; pCore->displayMessage(i18n("Validating"), OperationCompletedMessage, 100); qApp->processEvents(); DocumentValidator validator(m_document, url); success = validator.isProject(); if (!success) { // It is not a project file pCore->displayMessage(i18n("File %1 is not a Kdenlive project file", m_url.toLocalFile()), OperationCompletedMessage, 100); if (KMessageBox::warningContinueCancel( parent, i18n("File %1 is not a valid project file.\nDo you want to open a backup file?", m_url.toLocalFile()), i18n("Error opening file"), KGuiItem(i18n("Open Backup"))) == KMessageBox::Continue) { *openBackup = true; } } else { /* * Validate the file against the current version (upgrade * and recover it if needed). It is NOT a passive operation */ // TODO: backup the document or alert the user? success = validator.validate(DOCUMENTVERSION); if (success && !KdenliveSettings::gpu_accel()) { success = validator.checkMovit(); } if (success) { // Let the validator handle error messages qCDebug(KDENLIVE_LOG) << " // / processing file validate ok"; pCore->displayMessage(i18n("Check missing clips"), InformationMessage, 300); qApp->processEvents(); DocumentChecker d(m_url, m_document); success = !d.hasErrorInClips(); if (success) { loadDocumentProperties(); if (m_document.documentElement().hasAttribute(QStringLiteral("upgraded"))) { m_documentOpenStatus = UpgradedProject; pCore->displayMessage(i18n("Your project was upgraded, a backup will be created on next save"), ErrorMessage); } else if (m_document.documentElement().hasAttribute(QStringLiteral("modified")) || validator.isModified()) { m_documentOpenStatus = ModifiedProject; pCore->displayMessage(i18n("Your project was modified on opening, a backup will be created on next save"), ErrorMessage); setModified(true); } pCore->displayMessage(QString(), OperationCompletedMessage); } } } } } } // Something went wrong, or a new file was requested: create a new project if (!success) { m_url.clear(); pCore->setCurrentProfile(profileName); m_document = createEmptyDocument(tracks.first, tracks.second); updateProjectProfile(false); } else { m_clipsCount = m_document.elementsByTagName(QLatin1String("entry")).size(); } if (!m_projectFolder.isEmpty()) { // Ask to create the project directory if it does not exist QDir folder(m_projectFolder); if (!folder.mkpath(QStringLiteral("."))) { // Project folder is not writable m_projectFolder = m_url.toString(QUrl::RemoveFilename | QUrl::RemoveScheme); folder.setPath(m_projectFolder); if (folder.exists()) { KMessageBox::sorry( parent, i18n("The project directory %1, could not be created.\nPlease make sure you have the required permissions.\nDefaulting to system folders", m_projectFolder)); } else { KMessageBox::information(parent, i18n("Document project folder is invalid, using system default folders")); } m_projectFolder.clear(); } } initCacheDirs(); updateProjectFolderPlacesEntry(); } KdenliveDoc::~KdenliveDoc() { if (m_url.isEmpty()) { // Document was never saved, delete cache folder QString documentId = QDir::cleanPath(getDocumentProperty(QStringLiteral("documentid"))); bool ok; documentId.toLongLong(&ok, 10); if (ok && !documentId.isEmpty()) { QDir baseCache = getCacheDir(CacheBase, &ok); if (baseCache.dirName() == documentId && baseCache.entryList(QDir::Files).isEmpty()) { baseCache.removeRecursively(); } } } // qCDebug(KDENLIVE_LOG) << "// DEL CLP MAN"; // Clean up guide model m_guideModel.reset(); // qCDebug(KDENLIVE_LOG) << "// DEL CLP MAN done"; if (m_autosave) { if (!m_autosave->fileName().isEmpty()) { m_autosave->remove(); } delete m_autosave; } } int KdenliveDoc::clipsCount() const { return m_clipsCount; } const QByteArray KdenliveDoc::getProjectXml() { const QByteArray result = m_document.toString().toUtf8(); // We don't need the xml data anymore, throw away m_document.clear(); return result; } QDomDocument KdenliveDoc::createEmptyDocument(int videotracks, int audiotracks) { QList tracks; // Tracks are added «backwards», so we need to reverse the track numbering // mbt 331: http://www.kdenlive.org/mantis/view.php?id=331 // Better default names for tracks: Audio 1 etc. instead of blank numbers tracks.reserve(audiotracks + videotracks); for (int i = 0; i < audiotracks; ++i) { TrackInfo audioTrack; audioTrack.type = AudioTrack; audioTrack.isMute = false; audioTrack.isBlind = true; audioTrack.isLocked = false; // audioTrack.trackName = i18n("Audio %1", audiotracks - i); audioTrack.duration = 0; tracks.append(audioTrack); } for (int i = 0; i < videotracks; ++i) { TrackInfo videoTrack; videoTrack.type = VideoTrack; videoTrack.isMute = false; videoTrack.isBlind = false; videoTrack.isLocked = false; // videoTrack.trackName = i18n("Video %1", i + 1); videoTrack.duration = 0; tracks.append(videoTrack); } return createEmptyDocument(tracks); } QDomDocument KdenliveDoc::createEmptyDocument(const QList &tracks) { // Creating new document QDomDocument doc; Mlt::Profile docProfile; Mlt::Consumer xmlConsumer(docProfile, "xml:kdenlive_playlist"); xmlConsumer.set("no_profile", 1); xmlConsumer.set("terminate_on_pause", 1); xmlConsumer.set("store", "kdenlive"); Mlt::Tractor tractor(docProfile); Mlt::Producer bk(docProfile, "color:black"); tractor.insert_track(bk, 0); for (int i = 0; i < tracks.count(); ++i) { Mlt::Tractor track(docProfile); track.set("kdenlive:track_name", tracks.at(i).trackName.toUtf8().constData()); track.set("kdenlive:timeline_active", 1); track.set("kdenlive:trackheight", KdenliveSettings::trackheight()); if (tracks.at(i).type == AudioTrack) { track.set("kdenlive:audio_track", 1); } if (tracks.at(i).isLocked) { track.set("kdenlive:locked_track", 1); } if (tracks.at(i).isMute) { if (tracks.at(i).isBlind) { track.set("hide", 3); } else { track.set("hide", 2); } } else if (tracks.at(i).isBlind) { track.set("hide", 1); } Mlt::Playlist playlist1(docProfile); Mlt::Playlist playlist2(docProfile); track.insert_track(playlist1, 0); track.insert_track(playlist2, 1); tractor.insert_track(track, i + 1); } QScopedPointer field(tractor.field()); QString compositeService = TransitionsRepository::get()->getCompositingTransition(); if (!compositeService.isEmpty()) { for (int i = 0; i <= tracks.count(); i++) { if (i > 0 && tracks.at(i - 1).type == AudioTrack) { Mlt::Transition tr(docProfile, "mix"); tr.set("a_track", 0); tr.set("b_track", i); tr.set("always_active", 1); tr.set("sum", 1); tr.set("internal_added", 237); field->plant_transition(tr, 0, i); } if (i > 0 && tracks.at(i - 1).type == VideoTrack) { Mlt::Transition tr(docProfile, compositeService.toUtf8().constData()); tr.set("a_track", 0); tr.set("b_track", i); tr.set("always_active", 1); tr.set("internal_added", 237); field->plant_transition(tr, 0, i); } } } Mlt::Producer prod(tractor.get_producer()); xmlConsumer.connect(prod); xmlConsumer.run(); QString playlist = QString::fromUtf8(xmlConsumer.get("kdenlive_playlist")); doc.setContent(playlist); return doc; } bool KdenliveDoc::useProxy() const { return m_documentProperties.value(QStringLiteral("enableproxy")).toInt() != 0; } bool KdenliveDoc::useExternalProxy() const { return m_documentProperties.value(QStringLiteral("enableexternalproxy")).toInt() != 0; } bool KdenliveDoc::autoGenerateProxy(int width) const { return (m_documentProperties.value(QStringLiteral("generateproxy")).toInt() != 0) && width > m_documentProperties.value(QStringLiteral("proxyminsize")).toInt(); } bool KdenliveDoc::autoGenerateImageProxy(int width) const { return (m_documentProperties.value(QStringLiteral("generateimageproxy")).toInt() != 0) && width > m_documentProperties.value(QStringLiteral("proxyimageminsize")).toInt(); } void KdenliveDoc::slotAutoSave(const QString &scene) { if (m_autosave != nullptr) { if (!m_autosave->isOpen() && !m_autosave->open(QIODevice::ReadWrite)) { // show error: could not open the autosave file qCDebug(KDENLIVE_LOG) << "ERROR; CANNOT CREATE AUTOSAVE FILE"; pCore->displayMessage(i18n("Cannot create autosave file %1", m_autosave->fileName()), ErrorMessage); return; } if (scene.isEmpty()) { // Make sure we don't save if scenelist is corrupted KMessageBox::error(QApplication::activeWindow(), i18n("Cannot write to file %1, scene list is corrupted.", m_autosave->fileName())); return; } m_autosave->resize(0); if (m_autosave->write(scene.toUtf8()) < 0) { pCore->displayMessage(i18n("Cannot create autosave file %1", m_autosave->fileName()), ErrorMessage); }; m_autosave->flush(); } } void KdenliveDoc::setZoom(int horizontal, int vertical) { m_documentProperties[QStringLiteral("zoom")] = QString::number(horizontal); if (vertical > -1) { m_documentProperties[QStringLiteral("verticalzoom")] = QString::number(vertical); } } QPoint KdenliveDoc::zoom() const { return QPoint(m_documentProperties.value(QStringLiteral("zoom")).toInt(), m_documentProperties.value(QStringLiteral("verticalzoom")).toInt()); } void KdenliveDoc::setZone(int start, int end) { m_documentProperties[QStringLiteral("zonein")] = QString::number(start); m_documentProperties[QStringLiteral("zoneout")] = QString::number(end); } QPoint KdenliveDoc::zone() const { return QPoint(m_documentProperties.value(QStringLiteral("zonein")).toInt(), m_documentProperties.value(QStringLiteral("zoneout")).toInt()); } QPair KdenliveDoc::targetTracks() const { return {m_documentProperties.value(QStringLiteral("videoTarget")).toInt(), m_documentProperties.value(QStringLiteral("audioTarget")).toInt()}; } QDomDocument KdenliveDoc::xmlSceneList(const QString &scene) { QDomDocument sceneList; sceneList.setContent(scene, true); QDomElement mlt = sceneList.firstChildElement(QStringLiteral("mlt")); if (mlt.isNull() || !mlt.hasChildNodes()) { // scenelist is corrupted return QDomDocument(); } // Set playlist audio volume to 100% QDomNodeList tractors = mlt.elementsByTagName(QStringLiteral("tractor")); for (int i = 0; i < tractors.count(); ++i) { if (tractors.at(i).toElement().hasAttribute(QStringLiteral("global_feed"))) { // This is our main tractor QDomElement tractor = tractors.at(i).toElement(); if (Xml::hasXmlProperty(tractor, QLatin1String("meta.volume"))) { Xml::setXmlProperty(tractor, QStringLiteral("meta.volume"), QStringLiteral("1")); } break; } } QDomNodeList tracks = mlt.elementsByTagName(QStringLiteral("track")); if (tracks.isEmpty()) { // Something is very wrong, inform user. qDebug()<<" = = = = = = CORRUPTED DOC\n"< effectIds; for (int i = 0; i < maxEffects; ++i) { QDomNode m = effects.at(i); QDomNodeList params = m.childNodes(); QString id; QString tag; for (int j = 0; j < params.count(); ++j) { QDomElement e = params.item(j).toElement(); if (e.attribute(QStringLiteral("name")) == QLatin1String("kdenlive_id")) { id = e.firstChild().nodeValue(); } if (e.attribute(QStringLiteral("name")) == QLatin1String("tag")) { tag = e.firstChild().nodeValue(); } if (!id.isEmpty() && !tag.isEmpty()) { effectIds.insert(id, tag); } } } // TODO: find a way to process this before rendering MLT scenelist to xml /*QDomDocument customeffects = initEffects::getUsedCustomEffects(effectIds); if (!customeffects.documentElement().childNodes().isEmpty()) { Xml::setXmlProperty(mainPlaylist, QStringLiteral("kdenlive:customeffects"), customeffects.toString()); }*/ // addedXml.appendChild(sceneList.importNode(customeffects.documentElement(), true)); // TODO: move metadata to previous step in saving process QDomElement docmetadata = sceneList.createElement(QStringLiteral("documentmetadata")); QMapIterator j(m_documentMetadata); while (j.hasNext()) { j.next(); docmetadata.setAttribute(j.key(), j.value()); } // addedXml.appendChild(docmetadata); return sceneList; } bool KdenliveDoc::saveSceneList(const QString &path, const QString &scene) { QDomDocument sceneList = xmlSceneList(scene); if (sceneList.isNull()) { // Make sure we don't save if scenelist is corrupted KMessageBox::error(QApplication::activeWindow(), i18n("Cannot write to file %1, scene list is corrupted.", path)); return false; } // Backup current version backupLastSavedVersion(path); if (m_documentOpenStatus != CleanProject) { // create visible backup file and warn user QString baseFile = path.section(QStringLiteral(".kdenlive"), 0, 0); int ct = 0; QString backupFile = baseFile + QStringLiteral("_backup") + QString::number(ct) + QStringLiteral(".kdenlive"); while (QFile::exists(backupFile)) { ct++; backupFile = baseFile + QStringLiteral("_backup") + QString::number(ct) + QStringLiteral(".kdenlive"); } QString message; if (m_documentOpenStatus == UpgradedProject) { message = i18n("Your project file was upgraded to the latest Kdenlive document version.\nTo make sure you do not lose data, a backup copy called %1 " "was created.", backupFile); } else { message = i18n("Your project file was modified by Kdenlive.\nTo make sure you do not lose data, a backup copy called %1 was created.", backupFile); } KIO::FileCopyJob *copyjob = KIO::file_copy(QUrl::fromLocalFile(path), QUrl::fromLocalFile(backupFile)); if (copyjob->exec()) { KMessageBox::information(QApplication::activeWindow(), message); m_documentOpenStatus = CleanProject; } else { KMessageBox::information( QApplication::activeWindow(), i18n("Your project file was upgraded to the latest Kdenlive document version, but it was not possible to create the backup copy %1.", backupFile)); } } QSaveFile file(path); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { qCWarning(KDENLIVE_LOG) << "////// ERROR writing to file: " << path; KMessageBox::error(QApplication::activeWindow(), i18n("Cannot write to file %1", path)); return false; } const QByteArray sceneData = sceneList.toString().toUtf8(); file.write(sceneData); if (!file.commit()) { KMessageBox::error(QApplication::activeWindow(), i18n("Cannot write to file %1", path)); return false; } cleanupBackupFiles(); QFileInfo info(path); QString fileName = QUrl::fromLocalFile(path).fileName().section(QLatin1Char('.'), 0, -2); fileName.append(QLatin1Char('-') + m_documentProperties.value(QStringLiteral("documentid"))); fileName.append(info.lastModified().toString(QStringLiteral("-yyyy-MM-dd-hh-mm"))); fileName.append(QStringLiteral(".kdenlive.png")); QDir backupFolder(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/.backup")); emit saveTimelinePreview(backupFolder.absoluteFilePath(fileName)); return true; } QString KdenliveDoc::projectTempFolder() const { if (m_projectFolder.isEmpty()) { return QStandardPaths::writableLocation(QStandardPaths::CacheLocation); } return m_projectFolder; } QString KdenliveDoc::projectDataFolder() const { if (m_projectFolder.isEmpty()) { if (KdenliveSettings::customprojectfolder()) { return KdenliveSettings::defaultprojectfolder(); } return QStandardPaths::writableLocation(QStandardPaths::MoviesLocation); } return m_projectFolder; } void KdenliveDoc::setProjectFolder(const QUrl &url) { if (url == QUrl::fromLocalFile(m_projectFolder)) { return; } setModified(true); QDir dir(url.toLocalFile()); if (!dir.exists()) { dir.mkpath(dir.absolutePath()); } dir.mkdir(QStringLiteral("titles")); /*if (KMessageBox::questionYesNo(QApplication::activeWindow(), i18n("You have changed the project folder. Do you want to copy the cached data from %1 to the * new folder %2?", m_projectFolder, url.path())) == KMessageBox::Yes) moveProjectData(url);*/ m_projectFolder = url.toLocalFile(); updateProjectFolderPlacesEntry(); } void KdenliveDoc::moveProjectData(const QString & /*src*/, const QString &dest) { // Move proxies QList cacheUrls; auto binClips = pCore->projectItemModel()->getAllClipIds(); // First step: all clips referenced by the bin model exist and are inserted for (const auto &binClip : binClips) { auto projClip = pCore->projectItemModel()->getClipByBinID(binClip); if (projClip->clipType() == ClipType::Text) { // the image for title clip must be moved QUrl oldUrl = QUrl::fromLocalFile(projClip->clipUrl()); if (!oldUrl.isEmpty()) { QUrl newUrl = QUrl::fromLocalFile(dest + QStringLiteral("/titles/") + oldUrl.fileName()); KIO::Job *job = KIO::copy(oldUrl, newUrl); if (job->exec()) { projClip->setProducerProperty(QStringLiteral("resource"), newUrl.toLocalFile()); } } continue; } QString proxy = projClip->getProducerProperty(QStringLiteral("kdenlive:proxy")); if (proxy.length() > 2 && QFile::exists(proxy)) { QUrl pUrl = QUrl::fromLocalFile(proxy); if (!cacheUrls.contains(pUrl)) { cacheUrls << pUrl; } } } if (!cacheUrls.isEmpty()) { QDir proxyDir(dest + QStringLiteral("/proxy/")); if (proxyDir.mkpath(QStringLiteral("."))) { KIO::CopyJob *job = KIO::move(cacheUrls, QUrl::fromLocalFile(proxyDir.absolutePath())); KJobWidgets::setWindow(job, QApplication::activeWindow()); if (static_cast(job->exec()) > 0) { KMessageBox::sorry(QApplication::activeWindow(), i18n("Moving proxy clips failed: %1", job->errorText())); } } } } bool KdenliveDoc::profileChanged(const QString &profile) const { return pCore->getCurrentProfile() != ProfileRepository::get()->getProfile(profile); } Render *KdenliveDoc::renderer() { return nullptr; } std::shared_ptr KdenliveDoc::commandStack() { return m_commandStack; } int KdenliveDoc::getFramePos(const QString &duration) { return m_timecode.getFrameCount(duration); } Timecode KdenliveDoc::timecode() const { return m_timecode; } int KdenliveDoc::width() const { return pCore->getCurrentProfile()->width(); } int KdenliveDoc::height() const { return pCore->getCurrentProfile()->height(); } QUrl KdenliveDoc::url() const { return m_url; } void KdenliveDoc::setUrl(const QUrl &url) { m_url = url; } void KdenliveDoc::slotModified() { setModified(!m_commandStack->isClean()); } void KdenliveDoc::setModified(bool mod) { // fix mantis#3160: The document may have an empty URL if not saved yet, but should have a m_autosave in any case if ((m_autosave != nullptr) && mod && KdenliveSettings::crashrecovery()) { emit startAutoSave(); } if (mod == m_modified) { return; } m_modified = mod; emit docModified(m_modified); } bool KdenliveDoc::isModified() const { return m_modified; } const QString KdenliveDoc::description() const { if (!m_url.isValid()) { return i18n("Untitled") + QStringLiteral("[*] / ") + pCore->getCurrentProfile()->description(); } return m_url.fileName() + QStringLiteral(" [*]/ ") + pCore->getCurrentProfile()->description(); } QString KdenliveDoc::searchFileRecursively(const QDir &dir, const QString &matchSize, const QString &matchHash) const { QString foundFileName; QByteArray fileData; QByteArray fileHash; QStringList filesAndDirs = dir.entryList(QDir::Files | QDir::Readable); for (int i = 0; i < filesAndDirs.size() && foundFileName.isEmpty(); ++i) { QFile file(dir.absoluteFilePath(filesAndDirs.at(i))); if (file.open(QIODevice::ReadOnly)) { if (QString::number(file.size()) == matchSize) { /* * 1 MB = 1 second per 450 files (or faster) * 10 MB = 9 seconds per 450 files (or faster) */ if (file.size() > 1000000 * 2) { fileData = file.read(1000000); if (file.seek(file.size() - 1000000)) { fileData.append(file.readAll()); } } else { fileData = file.readAll(); } file.close(); fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5); if (QString::fromLatin1(fileHash.toHex()) == matchHash) { return file.fileName(); } qCDebug(KDENLIVE_LOG) << filesAndDirs.at(i) << "size match but not hash"; } } ////qCDebug(KDENLIVE_LOG) << filesAndDirs.at(i) << file.size() << fileHash.toHex(); } filesAndDirs = dir.entryList(QDir::Dirs | QDir::Readable | QDir::Executable | QDir::NoDotAndDotDot); for (int i = 0; i < filesAndDirs.size() && foundFileName.isEmpty(); ++i) { foundFileName = searchFileRecursively(dir.absoluteFilePath(filesAndDirs.at(i)), matchSize, matchHash); if (!foundFileName.isEmpty()) { break; } } return foundFileName; } QStringList KdenliveDoc::getBinFolderClipIds(const QString &folderId) const { return pCore->bin()->getBinFolderClipIds(folderId); } void KdenliveDoc::slotCreateTextTemplateClip(const QString &group, const QString &groupId, QUrl path) { Q_UNUSED(group) // TODO refac: this seem to be a duplicate of ClipCreationDialog::createTitleTemplateClip. See if we can merge QString titlesFolder = QDir::cleanPath(m_projectFolder + QStringLiteral("/titles/")); if (path.isEmpty()) { QPointer d = new QFileDialog(QApplication::activeWindow(), i18n("Enter Template Path"), titlesFolder); d->setMimeTypeFilters(QStringList() << QStringLiteral("application/x-kdenlivetitle")); d->setFileMode(QFileDialog::ExistingFile); if (d->exec() == QDialog::Accepted && !d->selectedUrls().isEmpty()) { path = d->selectedUrls().first(); } delete d; } if (path.isEmpty()) { return; } // TODO: rewrite with new title system (just set resource) QString id = ClipCreator::createTitleTemplate(path.toString(), QString(), i18n("Template title clip"), groupId, pCore->projectItemModel()); emit selectLastAddedClip(id); } void KdenliveDoc::cacheImage(const QString &fileId, const QImage &img) const { bool ok = false; QDir dir = getCacheDir(CacheThumbs, &ok); if (ok) { img.save(dir.absoluteFilePath(fileId + QStringLiteral(".png"))); } } void KdenliveDoc::setDocumentProperty(const QString &name, const QString &value) { if (value.isEmpty()) { m_documentProperties.remove(name); return; } m_documentProperties[name] = value; } const QString KdenliveDoc::getDocumentProperty(const QString &name, const QString &defaultValue) const { return m_documentProperties.value(name, defaultValue); } QMap KdenliveDoc::getRenderProperties() const { QMap renderProperties; QMapIterator i(m_documentProperties); while (i.hasNext()) { i.next(); if (i.key().startsWith(QLatin1String("render"))) { if (i.key() == QLatin1String("renderurl")) { // Check that we have a full path QString value = i.value(); if (QFileInfo(value).isRelative()) { value.prepend(m_documentRoot); } renderProperties.insert(i.key(), value); } else { renderProperties.insert(i.key(), i.value()); } } } return renderProperties; } void KdenliveDoc::saveCustomEffects(const QDomNodeList &customeffects) { QDomElement e; QStringList importedEffects; int maxchild = customeffects.count(); QStringList newPaths; for (int i = 0; i < maxchild; ++i) { e = customeffects.at(i).toElement(); const QString id = e.attribute(QStringLiteral("id")); const QString tag = e.attribute(QStringLiteral("tag")); if (!id.isEmpty()) { // Check if effect exists or save it if (EffectsRepository::get()->exists(id)) { QDomDocument doc; doc.appendChild(doc.importNode(e, true)); QString path = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/effects"); path += id + QStringLiteral(".xml"); if (!QFile::exists(path)) { importedEffects << id; newPaths << path; QFile file(path); if (file.open(QFile::WriteOnly | QFile::Truncate)) { QTextStream out(&file); out << doc.toString(); } } } } } if (!importedEffects.isEmpty()) { KMessageBox::informationList(QApplication::activeWindow(), i18n("The following effects were imported from the project:"), importedEffects); } if (!importedEffects.isEmpty()) { emit reloadEffects(newPaths); } } void KdenliveDoc::updateProjectFolderPlacesEntry() { /* * For similar and more code have a look at kfileplacesmodel.cpp and the included files: * https://api.kde.org/frameworks/kio/html/kfileplacesmodel_8cpp_source.html */ const QString file = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/user-places.xbel"); KBookmarkManager *bookmarkManager = KBookmarkManager::managerForExternalFile(file); if (!bookmarkManager) { return; } KBookmarkGroup root = bookmarkManager->root(); KBookmark bookmark = root.first(); QString kdenliveName = QCoreApplication::applicationName(); QUrl documentLocation = QUrl::fromLocalFile(m_projectFolder); bool exists = false; while (!bookmark.isNull()) { // UDI not empty indicates a device QString udi = bookmark.metaDataItem(QStringLiteral("UDI")); QString appName = bookmark.metaDataItem(QStringLiteral("OnlyInApp")); if (udi.isEmpty() && appName == kdenliveName && bookmark.text() == i18n("Project Folder")) { if (bookmark.url() != documentLocation) { bookmark.setUrl(documentLocation); bookmarkManager->emitChanged(root); } exists = true; break; } bookmark = root.next(bookmark); } // if entry does not exist yet (was not found), well, create it then if (!exists) { bookmark = root.addBookmark(i18n("Project Folder"), documentLocation, QStringLiteral("folder-favorites")); // Make this user selectable ? bookmark.setMetaDataItem(QStringLiteral("OnlyInApp"), kdenliveName); bookmarkManager->emitChanged(root); } } // static double KdenliveDoc::getDisplayRatio(const QString &path) { QFile file(path); QDomDocument doc; if (!file.open(QIODevice::ReadOnly)) { qCWarning(KDENLIVE_LOG) << "ERROR, CANNOT READ: " << path; return 0; } if (!doc.setContent(&file)) { qCWarning(KDENLIVE_LOG) << "ERROR, CANNOT READ: " << path; file.close(); return 0; } file.close(); QDomNodeList list = doc.elementsByTagName(QStringLiteral("profile")); if (list.isEmpty()) { return 0; } QDomElement profile = list.at(0).toElement(); double den = profile.attribute(QStringLiteral("display_aspect_den")).toDouble(); if (den > 0) { return profile.attribute(QStringLiteral("display_aspect_num")).toDouble() / den; } return 0; } void KdenliveDoc::backupLastSavedVersion(const QString &path) { // Ensure backup folder exists if (path.isEmpty()) { return; } QFile file(path); QDir backupFolder(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/.backup")); QString fileName = QUrl::fromLocalFile(path).fileName().section(QLatin1Char('.'), 0, -2); QFileInfo info(file); fileName.append(QLatin1Char('-') + m_documentProperties.value(QStringLiteral("documentid"))); fileName.append(info.lastModified().toString(QStringLiteral("-yyyy-MM-dd-hh-mm"))); fileName.append(QStringLiteral(".kdenlive")); QString backupFile = backupFolder.absoluteFilePath(fileName); if (file.exists()) { // delete previous backup if it was done less than 60 seconds ago QFile::remove(backupFile); if (!QFile::copy(path, backupFile)) { KMessageBox::information(QApplication::activeWindow(), i18n("Cannot create backup copy:\n%1", backupFile)); } } } void KdenliveDoc::cleanupBackupFiles() { QDir backupFolder(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/.backup")); QString projectFile = url().fileName().section(QLatin1Char('.'), 0, -2); projectFile.append(QLatin1Char('-') + m_documentProperties.value(QStringLiteral("documentid"))); projectFile.append(QStringLiteral("-??")); projectFile.append(QStringLiteral("??")); projectFile.append(QStringLiteral("-??")); projectFile.append(QStringLiteral("-??")); projectFile.append(QStringLiteral("-??")); projectFile.append(QStringLiteral("-??.kdenlive")); QStringList filter; filter << projectFile; backupFolder.setNameFilters(filter); QFileInfoList resultList = backupFolder.entryInfoList(QDir::Files, QDir::Time); QDateTime d = QDateTime::currentDateTime(); QStringList hourList; QStringList dayList; QStringList weekList; QStringList oldList; for (int i = 0; i < resultList.count(); ++i) { if (d.secsTo(resultList.at(i).lastModified()) < 3600) { // files created in the last hour hourList.append(resultList.at(i).absoluteFilePath()); } else if (d.secsTo(resultList.at(i).lastModified()) < 43200) { // files created in the day dayList.append(resultList.at(i).absoluteFilePath()); } else if (d.daysTo(resultList.at(i).lastModified()) < 8) { // files created in the week weekList.append(resultList.at(i).absoluteFilePath()); } else { // older files oldList.append(resultList.at(i).absoluteFilePath()); } } if (hourList.count() > 20) { int step = hourList.count() / 10; for (int i = 0; i < hourList.count(); i += step) { // qCDebug(KDENLIVE_LOG)<<"REMOVE AT: "< 20) { int step = dayList.count() / 10; for (int i = 0; i < dayList.count(); i += step) { dayList.removeAt(i); --i; } } else { dayList.clear(); } if (weekList.count() > 20) { int step = weekList.count() / 10; for (int i = 0; i < weekList.count(); i += step) { weekList.removeAt(i); --i; } } else { weekList.clear(); } if (oldList.count() > 20) { int step = oldList.count() / 10; for (int i = 0; i < oldList.count(); i += step) { oldList.removeAt(i); --i; } } else { oldList.clear(); } QString f; while (hourList.count() > 0) { f = hourList.takeFirst(); QFile::remove(f); QFile::remove(f + QStringLiteral(".png")); } while (dayList.count() > 0) { f = dayList.takeFirst(); QFile::remove(f); QFile::remove(f + QStringLiteral(".png")); } while (weekList.count() > 0) { f = weekList.takeFirst(); QFile::remove(f); QFile::remove(f + QStringLiteral(".png")); } while (oldList.count() > 0) { f = oldList.takeFirst(); QFile::remove(f); QFile::remove(f + QStringLiteral(".png")); } } const QMap KdenliveDoc::metadata() const { return m_documentMetadata; } void KdenliveDoc::setMetadata(const QMap &meta) { setModified(true); m_documentMetadata = meta; } void KdenliveDoc::slotProxyCurrentItem(bool doProxy, QList> clipList, bool force, QUndoCommand *masterCommand) { if (clipList.isEmpty()) { clipList = pCore->bin()->selectedClips(); } bool hasParent = true; if (masterCommand == nullptr) { masterCommand = new QUndoCommand(); if (doProxy) { masterCommand->setText(i18np("Add proxy clip", "Add proxy clips", clipList.count())); } else { masterCommand->setText(i18np("Remove proxy clip", "Remove proxy clips", clipList.count())); } hasParent = false; } // Make sure the proxy folder exists bool ok = false; QDir dir = getCacheDir(CacheProxy, &ok); if (!ok) { // Error return; } if (m_proxyExtension.isEmpty()) { initProxySettings(); } QString extension = QLatin1Char('.') + m_proxyExtension; // getDocumentProperty(QStringLiteral("proxyextension")); /*QString params = getDocumentProperty(QStringLiteral("proxyparams")); if (params.contains(QStringLiteral("-s "))) { QString proxySize = params.section(QStringLiteral("-s "), 1).section(QStringLiteral("x"), 0, 0); extension.prepend(QStringLiteral("-") + proxySize); }*/ // Prepare updated properties QMap newProps; QMap oldProps; if (!doProxy) { newProps.insert(QStringLiteral("kdenlive:proxy"), QStringLiteral("-")); } // Parse clips QStringList externalProxyParams = m_documentProperties.value(QStringLiteral("externalproxyparams")).split(QLatin1Char(';')); for (int i = 0; i < clipList.count(); ++i) { const std::shared_ptr &item = clipList.at(i); ClipType::ProducerType t = item->clipType(); // Only allow proxy on some clip types if ((t == ClipType::Video || t == ClipType::AV || t == ClipType::Unknown || t == ClipType::Image || t == ClipType::Playlist || t == ClipType::SlideShow) && item->isReady()) { if ((doProxy && !force && item->hasProxy()) || (!doProxy && !item->hasProxy() && pCore->projectItemModel()->hasClip(item->AbstractProjectItem::clipId()))) { continue; } if (doProxy) { newProps.clear(); QString path; if (useExternalProxy() && item->hasLimitedDuration()) { if (externalProxyParams.count() >= 3) { QFileInfo info(item->url()); QDir clipDir = info.absoluteDir(); if (clipDir.cd(externalProxyParams.at(0))) { // Find correct file QString fileName = info.fileName(); if (!externalProxyParams.at(1).isEmpty()) { fileName.prepend(externalProxyParams.at(1)); } if (!externalProxyParams.at(2).isEmpty()) { fileName = fileName.section(QLatin1Char('.'), 0, -2); fileName.append(externalProxyParams.at(2)); } if (clipDir.exists(fileName)) { path = clipDir.absoluteFilePath(fileName); } } } } if (path.isEmpty()) { path = dir.absoluteFilePath(item->hash() + (t == ClipType::Image ? QStringLiteral(".png") : extension)); } newProps.insert(QStringLiteral("kdenlive:proxy"), path); // We need to insert empty proxy so that undo will work // TODO: how to handle clip properties // oldProps = clip->currentProperties(newProps); oldProps.insert(QStringLiteral("kdenlive:proxy"), QStringLiteral("-")); } else { if (t == ClipType::SlideShow) { // Revert to picture aspect ratio newProps.insert(QStringLiteral("aspect_ratio"), QStringLiteral("1")); } // Reset to original url newProps.insert(QStringLiteral("resource"), item->url()); } new EditClipCommand(pCore->bin(), item->AbstractProjectItem::clipId(), oldProps, newProps, true, masterCommand); } else { // Cannot proxy this clip type pCore->bin()->doDisplayMessage(i18n("Clip type does not support proxies"), KMessageWidget::Information); } } if (!hasParent) { if (masterCommand->childCount() > 0) { m_commandStack->push(masterCommand); } else { delete masterCommand; } } } QMap KdenliveDoc::documentProperties() { m_documentProperties.insert(QStringLiteral("version"), QString::number(DOCUMENTVERSION)); m_documentProperties.insert(QStringLiteral("kdenliveversion"), QStringLiteral(KDENLIVE_VERSION)); if (!m_projectFolder.isEmpty()) { m_documentProperties.insert(QStringLiteral("storagefolder"), m_projectFolder + QLatin1Char('/') + m_documentProperties.value(QStringLiteral("documentid"))); } m_documentProperties.insert(QStringLiteral("profile"), pCore->getCurrentProfile()->path()); ; if (!m_documentProperties.contains(QStringLiteral("decimalPoint"))) { m_documentProperties.insert(QStringLiteral("decimalPoint"), QLocale().decimalPoint()); } return m_documentProperties; } void KdenliveDoc::loadDocumentProperties() { QDomNodeList list = m_document.elementsByTagName(QStringLiteral("playlist")); QDomElement baseElement = m_document.documentElement(); m_documentRoot = baseElement.attribute(QStringLiteral("root")); if (!m_documentRoot.isEmpty()) { m_documentRoot = QDir::cleanPath(m_documentRoot) + QDir::separator(); } if (!list.isEmpty()) { QDomElement pl = list.at(0).toElement(); if (pl.isNull()) { return; } QDomNodeList props = pl.elementsByTagName(QStringLiteral("property")); QString name; QDomElement e; for (int i = 0; i < props.count(); i++) { e = props.at(i).toElement(); name = e.attribute(QStringLiteral("name")); if (name.startsWith(QLatin1String("kdenlive:docproperties."))) { name = name.section(QLatin1Char('.'), 1); if (name == QStringLiteral("storagefolder")) { // Make sure we have an absolute path QString value = e.firstChild().nodeValue(); if (QFileInfo(value).isRelative()) { value.prepend(m_documentRoot); } m_documentProperties.insert(name, value); } else if (name == QStringLiteral("guides")) { QString guides = e.firstChild().nodeValue(); if (!guides.isEmpty()) { QMetaObject::invokeMethod(m_guideModel.get(), "importFromJson", Qt::QueuedConnection, Q_ARG(const QString &, guides), Q_ARG(bool, true), Q_ARG(bool, false)); } } else { m_documentProperties.insert(name, e.firstChild().nodeValue()); } } else if (name.startsWith(QLatin1String("kdenlive:docmetadata."))) { name = name.section(QLatin1Char('.'), 1); m_documentMetadata.insert(name, e.firstChild().nodeValue()); } } } QString path = m_documentProperties.value(QStringLiteral("storagefolder")); if (!path.isEmpty()) { QDir dir(path); dir.cdUp(); m_projectFolder = dir.absolutePath(); bool ok; // Ensure document storage folder is writable QString documentId = QDir::cleanPath(getDocumentProperty(QStringLiteral("documentid"))); documentId.toLongLong(&ok, 10); if (ok) { if (!dir.exists(documentId)) { if (!dir.mkpath(documentId)) { // Invalid storage folder, reset to default m_projectFolder.clear(); } } } else { // Something is wrong, documentid not readable qDebug()<<"=========\n\nCannot read document id: "<setCurrentProfile(profile); if (!profileFound) { // try to find matching profile from MLT profile properties list = m_document.elementsByTagName(QStringLiteral("profile")); if (!list.isEmpty()) { std::unique_ptr xmlProfile(new ProfileParam(list.at(0).toElement())); QString profilePath = ProfileRepository::get()->findMatchingProfile(xmlProfile.get()); // Document profile does not exist, create it as custom profile if (profilePath.isEmpty()) { profilePath = ProfileRepository::get()->saveProfile(xmlProfile.get()); } profileFound = pCore->setCurrentProfile(profilePath); } } if (!profileFound) { qDebug() << "ERROR, no matching profile found"; } updateProjectProfile(false); } void KdenliveDoc::updateProjectProfile(bool reloadProducers, bool reloadThumbs) { pCore->jobManager()->slotCancelJobs(); double fps = pCore->getCurrentFps(); double fpsChanged = m_timecode.fps() / fps; m_timecode.setFormat(fps); if (!reloadProducers) { return; } emit updateFps(fpsChanged); pCore->bin()->reloadAllProducers(reloadThumbs); } void KdenliveDoc::resetProfile(bool reloadThumbs) { updateProjectProfile(true, reloadThumbs); emit docModified(true); } void KdenliveDoc::slotSwitchProfile(const QString &profile_path, bool reloadThumbs) { // Discard all current jobs pCore->jobManager()->slotCancelJobs(); pCore->setCurrentProfile(profile_path); updateProjectProfile(true, reloadThumbs); emit docModified(true); } void KdenliveDoc::switchProfile(std::unique_ptr &profile, const QString &id, const QDomElement &xml) { Q_UNUSED(id) Q_UNUSED(xml) // Request profile update // Check profile fps so that we don't end up with an fps = 30.003 which would mess things up QString adjustMessage; double fps = (double)profile->frame_rate_num() / profile->frame_rate_den(); double fps_int; double fps_frac = std::modf(fps, &fps_int); if (fps_frac < 0.4) { profile->m_frame_rate_num = (int)fps_int; profile->m_frame_rate_den = 1; } else { // Check for 23.98, 29.97, 59.94 if (qFuzzyCompare(fps_int, 23.0)) { if (qFuzzyCompare(fps, 23.98)) { profile->m_frame_rate_num = 24000; profile->m_frame_rate_den = 1001; } } else if (qFuzzyCompare(fps_int, 29.0)) { if (qFuzzyCompare(fps, 29.97)) { profile->m_frame_rate_num = 30000; profile->m_frame_rate_den = 1001; } } else if (qFuzzyCompare(fps_int, 59.0)) { if (qFuzzyCompare(fps, 59.94)) { profile->m_frame_rate_num = 60000; profile->m_frame_rate_den = 1001; } } else { // Unknown profile fps, warn user adjustMessage = i18n("\nWarning: unknown non integer fps, might cause incorrect duration display."); } } QString matchingProfile = ProfileRepository::get()->findMatchingProfile(profile.get()); if (matchingProfile.isEmpty() && (profile->width() % 8 != 0)) { // Make sure profile width is a multiple of 8, required by some parts of mlt profile->adjustDimensions(); matchingProfile = ProfileRepository::get()->findMatchingProfile(profile.get()); } if (!matchingProfile.isEmpty()) { // We found a known matching profile, switch and inform user profile->m_path = matchingProfile; profile->m_description = ProfileRepository::get()->getProfile(matchingProfile)->description(); if (KdenliveSettings::default_profile().isEmpty()) { // Default project format not yet confirmed, propose QString currentProfileDesc = pCore->getCurrentProfile()->description(); KMessageBox::ButtonCode answer = KMessageBox::questionYesNoCancel( QApplication::activeWindow(), i18n("Your default project profile is %1, but your clip's profile is %2.\nDo you want to change default profile for future projects?", currentProfileDesc, profile->description()), i18n("Change default project profile"), KGuiItem(i18n("Change default to %1", profile->description())), KGuiItem(i18n("Keep current default %1", currentProfileDesc)), KGuiItem(i18n("Ask me later"))); switch (answer) { case KMessageBox::Yes: // Discard all current jobs pCore->jobManager()->slotCancelJobs(); KdenliveSettings::setDefault_profile(profile->path()); pCore->setCurrentProfile(profile->path()); updateProjectProfile(true); emit docModified(true); return; break; case KMessageBox::No: return; break; default: break; } } // Build actions for the info message (switch / cancel) QList list; const QString profilePath = profile->path(); QAction *ac = new QAction(QIcon::fromTheme(QStringLiteral("dialog-ok")), i18n("Switch"), this); connect(ac, &QAction::triggered, [this, profilePath]() { this->slotSwitchProfile(profilePath, true); }); QAction *ac2 = new QAction(QIcon::fromTheme(QStringLiteral("dialog-cancel")), i18n("Cancel"), this); list << ac << ac2; - pCore->displayBinMessage(i18n("Switch to clip profile %1?", profile->descriptiveString()), KMessageWidget::Information, list); + pCore->displayBinMessage(i18n("Switch to clip profile %1?", profile->descriptiveString()), KMessageWidget::Information, list, false, BinMessage::BinCategory::ProfileMessage); } else { // No known profile, ask user if he wants to use clip profile anyway if (qFuzzyCompare((double)profile->m_frame_rate_num / profile->m_frame_rate_den, fps)) { adjustMessage = i18n("\nProfile fps adjusted from original %1", QString::number(fps, 'f', 4)); } if (KMessageBox::warningContinueCancel(QApplication::activeWindow(), i18n("No profile found for your clip.\nCreate and switch to new profile (%1x%2, %3fps)?%4", profile->m_width, profile->m_height, QString::number((double)profile->m_frame_rate_num / profile->m_frame_rate_den, 'f', 2), adjustMessage)) == KMessageBox::Continue) { profile->m_description = QStringLiteral("%1x%2 %3fps") .arg(profile->m_width) .arg(profile->m_height) .arg(QString::number((double)profile->m_frame_rate_num / profile->m_frame_rate_den, 'f', 2)); QString profilePath = ProfileRepository::get()->saveProfile(profile.get()); // Discard all current jobs pCore->jobManager()->slotCancelJobs(); pCore->setCurrentProfile(profilePath); updateProjectProfile(true); emit docModified(true); } } } void KdenliveDoc::doAddAction(const QString &name, QAction *a, const QKeySequence &shortcut) { pCore->window()->actionCollection()->addAction(name, a); a->setShortcut(shortcut); pCore->window()->actionCollection()->setDefaultShortcut(a, a->shortcut()); } QAction *KdenliveDoc::getAction(const QString &name) { return pCore->window()->actionCollection()->action(name); } void KdenliveDoc::previewProgress(int p) { pCore->window()->setPreviewProgress(p); } void KdenliveDoc::displayMessage(const QString &text, MessageType type, int timeOut) { pCore->window()->displayMessage(text, type, timeOut); } void KdenliveDoc::selectPreviewProfile() { // Read preview profiles and find the best match if (!KdenliveSettings::previewparams().isEmpty()) { setDocumentProperty(QStringLiteral("previewparameters"), KdenliveSettings::previewparams()); setDocumentProperty(QStringLiteral("previewextension"), KdenliveSettings::previewextension()); return; } KConfig conf(QStringLiteral("encodingprofiles.rc"), KConfig::CascadeConfig, QStandardPaths::AppDataLocation); KConfigGroup group(&conf, "timelinepreview"); QMap values = group.entryMap(); if (KdenliveSettings::nvencEnabled() && values.contains(QStringLiteral("x264-nvenc"))) { const QString bestMatch = values.value(QStringLiteral("x264-nvenc")); setDocumentProperty(QStringLiteral("previewparameters"), bestMatch.section(QLatin1Char(';'), 0, 0)); setDocumentProperty(QStringLiteral("previewextension"), bestMatch.section(QLatin1Char(';'), 1, 1)); return; } if (KdenliveSettings::vaapiEnabled() && values.contains(QStringLiteral("x264-vaapi"))) { const QString bestMatch = values.value(QStringLiteral("x264-vaapi")); setDocumentProperty(QStringLiteral("previewparameters"), bestMatch.section(QLatin1Char(';'), 0, 0)); setDocumentProperty(QStringLiteral("previewextension"), bestMatch.section(QLatin1Char(';'), 1, 1)); return; } QMapIterator i(values); QStringList matchingProfiles; QStringList fallBackProfiles; QSize pSize = pCore->getCurrentFrameDisplaySize(); QString profileSize = QStringLiteral("%1x%2").arg(pSize.width()).arg(pSize.height()); while (i.hasNext()) { i.next(); // Check for frame rate QString params = i.value(); QStringList data = i.value().split(QLatin1Char(' ')); // Check for size mismatch if (params.contains(QStringLiteral("s="))) { QString paramSize = params.section(QStringLiteral("s="), 1).section(QLatin1Char(' '), 0, 0); if (paramSize != profileSize) { continue; } } bool rateFound = false; for (const QString &arg : data) { if (arg.startsWith(QStringLiteral("r="))) { rateFound = true; double fps = arg.section(QLatin1Char('='), 1).toDouble(); if (fps > 0) { if (qAbs((int)(pCore->getCurrentFps() * 100) - (fps * 100)) <= 1) { matchingProfiles << i.value(); break; } } } } if (!rateFound) { // Profile without fps, can be used as fallBack fallBackProfiles << i.value(); } } QString bestMatch; if (!matchingProfiles.isEmpty()) { bestMatch = matchingProfiles.first(); } else if (!fallBackProfiles.isEmpty()) { bestMatch = fallBackProfiles.first(); } if (!bestMatch.isEmpty()) { setDocumentProperty(QStringLiteral("previewparameters"), bestMatch.section(QLatin1Char(';'), 0, 0)); setDocumentProperty(QStringLiteral("previewextension"), bestMatch.section(QLatin1Char(';'), 1, 1)); } else { setDocumentProperty(QStringLiteral("previewparameters"), QString()); setDocumentProperty(QStringLiteral("previewextension"), QString()); } } QString KdenliveDoc::getAutoProxyProfile() { if (m_proxyExtension.isEmpty() || m_proxyParams.isEmpty()) { initProxySettings(); } return m_proxyParams; } void KdenliveDoc::initProxySettings() { // Read preview profiles and find the best match KConfig conf(QStringLiteral("encodingprofiles.rc"), KConfig::CascadeConfig, QStandardPaths::AppDataLocation); KConfigGroup group(&conf, "proxy"); QString params; QMap values = group.entryMap(); // Select best proxy profile depending on hw encoder support if (KdenliveSettings::nvencEnabled() && values.contains(QStringLiteral("x264-nvenc"))) { params = values.value(QStringLiteral("x264-nvenc")); } else if (KdenliveSettings::vaapiEnabled() && values.contains(QStringLiteral("x264-vaapi"))) { params = values.value(QStringLiteral("x264-vaapi")); } else { params = values.value(QStringLiteral("MJPEG")); } m_proxyParams = params.section(QLatin1Char(';'), 0, 0); m_proxyExtension = params.section(QLatin1Char(';'), 1); } void KdenliveDoc::checkPreviewStack() { // A command was pushed in the middle of the stack, remove all cached data from last undos emit removeInvalidUndo(m_commandStack->count()); } void KdenliveDoc::saveMltPlaylist(const QString &fileName) { Q_UNUSED(fileName) // TODO REFAC // m_render->preparePreviewRendering(fileName); } void KdenliveDoc::initCacheDirs() { bool ok = false; QString kdenliveCacheDir; QString documentId = QDir::cleanPath(getDocumentProperty(QStringLiteral("documentid"))); documentId.toLongLong(&ok, 10); if (m_projectFolder.isEmpty()) { kdenliveCacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation); } else { kdenliveCacheDir = m_projectFolder; } if (!ok || documentId.isEmpty() || kdenliveCacheDir.isEmpty()) { return; } QString basePath = kdenliveCacheDir + QLatin1Char('/') + documentId; QDir dir(basePath); dir.mkpath(QStringLiteral(".")); dir.mkdir(QStringLiteral("preview")); dir.mkdir(QStringLiteral("audiothumbs")); dir.mkdir(QStringLiteral("videothumbs")); QDir cacheDir(kdenliveCacheDir); cacheDir.mkdir(QStringLiteral("proxy")); } QDir KdenliveDoc::getCacheDir(CacheType type, bool *ok) const { QString basePath; QString kdenliveCacheDir; QString documentId = QDir::cleanPath(getDocumentProperty(QStringLiteral("documentid"))); documentId.toLongLong(ok, 10); if (m_projectFolder.isEmpty()) { kdenliveCacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation); if (!*ok || documentId.isEmpty() || kdenliveCacheDir.isEmpty()) { *ok = false; return QDir(kdenliveCacheDir); } } else { // Use specified folder to store all files kdenliveCacheDir = m_projectFolder; } basePath = kdenliveCacheDir + QLatin1Char('/') + documentId; switch (type) { case SystemCacheRoot: return QStandardPaths::writableLocation(QStandardPaths::CacheLocation); case CacheRoot: basePath = kdenliveCacheDir; break; case CachePreview: basePath.append(QStringLiteral("/preview")); break; case CacheProxy: basePath = kdenliveCacheDir; basePath.append(QStringLiteral("/proxy")); break; case CacheAudio: basePath.append(QStringLiteral("/audiothumbs")); break; case CacheThumbs: basePath.append(QStringLiteral("/videothumbs")); break; default: break; } QDir dir(basePath); if (!dir.exists()) { *ok = false; } return dir; } QStringList KdenliveDoc::getProxyHashList() { return pCore->bin()->getProxyHashList(); } std::shared_ptr KdenliveDoc::getGuideModel() const { return m_guideModel; } void KdenliveDoc::guidesChanged() { m_documentProperties[QStringLiteral("guides")] = m_guideModel->toJson(); } void KdenliveDoc::groupsChanged(const QString &groups) { m_documentProperties[QStringLiteral("groups")] = groups; } const QString KdenliveDoc::documentRoot() const { return m_documentRoot; } bool KdenliveDoc::updatePreviewSettings(const QString &profile) { if (profile.isEmpty()) { return false; } QString params = profile.section(QLatin1Char(';'), 0, 0); QString ext = profile.section(QLatin1Char(';'), 1, 1); if (params != getDocumentProperty(QStringLiteral("previewparameters")) || ext != getDocumentProperty(QStringLiteral("previewextension"))) { // Timeline preview params changed, delete all existing previews. setDocumentProperty(QStringLiteral("previewparameters"), params); setDocumentProperty(QStringLiteral("previewextension"), ext); return true; } return false; } QMap KdenliveDoc::getProjectTags() { QMap tags; for (int i = 1 ; i< 20; i++) { QString current = getDocumentProperty(QString("tag%1").arg(i)); if (current.isEmpty()) { break; } else { tags.insert(current.section(QLatin1Char(':'), 0, 0), current.section(QLatin1Char(':'), 1)); } } if (tags.isEmpty()) { tags.insert(QStringLiteral("#ff0000"), i18n("Red")); tags.insert(QStringLiteral("#00ff00"), i18n("Green")); tags.insert(QStringLiteral("#0000ff"), i18n("Blue")); tags.insert(QStringLiteral("#ffff00"), i18n("Yellow")); tags.insert(QStringLiteral("#00ffff"), i18n("Cyan")); } return tags; } int KdenliveDoc::audioChannels() const { return getDocumentProperty(QStringLiteral("audioChannels"), QStringLiteral("2")).toInt(); } diff --git a/src/timeline2/view/timelinecontroller.cpp b/src/timeline2/view/timelinecontroller.cpp index 8d265aacd..6da1cf906 100644 --- a/src/timeline2/view/timelinecontroller.cpp +++ b/src/timeline2/view/timelinecontroller.cpp @@ -1,3423 +1,3423 @@ /*************************************************************************** * Copyright (C) 2017 by Jean-Baptiste Mardelle * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #include "timelinecontroller.h" #include "../model/timelinefunctions.hpp" #include "assets/keyframes/model/keyframemodellist.hpp" #include "bin/bin.h" #include "bin/clipcreator.hpp" #include "bin/model/markerlistmodel.hpp" #include "bin/projectclip.h" #include "bin/projectfolder.h" #include "bin/projectitemmodel.h" #include "core.h" #include "dialogs/spacerdialog.h" #include "dialogs/speeddialog.h" #include "doc/kdenlivedoc.h" #include "effects/effectsrepository.hpp" #include "effects/effectstack/model/effectstackmodel.hpp" #include "kdenlivesettings.h" #include "lib/audio/audioEnvelope.h" #include "mainwindow.h" #include "monitor/monitormanager.h" #include "previewmanager.h" #include "project/projectmanager.h" #include "timeline2/model/clipmodel.hpp" #include "timeline2/model/compositionmodel.hpp" #include "timeline2/model/groupsmodel.hpp" #include "timeline2/model/timelineitemmodel.hpp" #include "timeline2/model/trackmodel.hpp" #include "timeline2/view/dialogs/clipdurationdialog.h" #include "timeline2/view/dialogs/trackdialog.h" #include "transitions/transitionsrepository.hpp" #include "audiomixer/mixermanager.hpp" #include #include #include #include #include #include int TimelineController::m_duration = 0; TimelineController::TimelineController(QObject *parent) : QObject(parent) , m_root(nullptr) , m_usePreview(false) , m_activeTrack(-1) , m_audioRef(-1) , m_zone(-1, -1) , m_scale(QFontMetrics(QApplication::font()).maxWidth() / 250) , m_timelinePreview(nullptr) , m_ready(false) , m_snapStackIndex(-1) { m_disablePreview = pCore->currentDoc()->getAction(QStringLiteral("disable_preview")); connect(m_disablePreview, &QAction::triggered, this, &TimelineController::disablePreview); connect(this, &TimelineController::selectionChanged, this, &TimelineController::updateClipActions); connect(this, &TimelineController::videoTargetChanged, this, &TimelineController::updateVideoTarget); connect(this, &TimelineController::audioTargetChanged, this, &TimelineController::updateAudioTarget); m_disablePreview->setEnabled(false); connect(pCore.get(), &Core::finalizeRecording, this, &TimelineController::finishRecording); connect(pCore.get(), &Core::autoScrollChanged, this, &TimelineController::autoScrollChanged); connect(pCore->mixer(), &MixerManager::recordAudio, this, &TimelineController::switchRecording); } TimelineController::~TimelineController() { prepareClose(); } void TimelineController::prepareClose() { // Clear roor so we don't call its methods anymore m_ready = false; m_root = nullptr; // Delete timeline preview before resetting model so that removing clips from timeline doesn't invalidate delete m_timelinePreview; m_timelinePreview = nullptr; } void TimelineController::setModel(std::shared_ptr model) { delete m_timelinePreview; m_zone = QPoint(-1, -1); m_timelinePreview = nullptr; m_model = std::move(model); m_activeSnaps.clear(); connect(m_model.get(), &TimelineItemModel::requestClearAssetView, pCore.get(), &Core::clearAssetPanel); connect(m_model.get(), &TimelineItemModel::checkItemDeletion, [this] (int id) { if (m_ready) { QMetaObject::invokeMethod(m_root, "checkDeletion", Qt::QueuedConnection, Q_ARG(QVariant, id)); } }); connect(m_model.get(), &TimelineItemModel::requestMonitorRefresh, [&]() { pCore->requestMonitorRefresh(); }); connect(m_model.get(), &TimelineModel::invalidateZone, this, &TimelineController::invalidateZone, Qt::DirectConnection); connect(m_model.get(), &TimelineModel::durationUpdated, this, &TimelineController::checkDuration); connect(m_model.get(), &TimelineModel::selectionChanged, this, &TimelineController::selectionChanged); connect(m_model.get(), &TimelineModel::checkTrackDeletion, this, &TimelineController::checkTrackDeletion, Qt::DirectConnection); } void TimelineController::setTargetTracks(bool hasVideo, QMap audioTargets) { int videoTrack = -1; m_model->m_binAudioTargets = audioTargets; QMap audioTracks; m_hasVideoTarget = hasVideo; m_hasAudioTarget = audioTargets.size(); if (m_hasVideoTarget) { videoTrack = m_model->getFirstVideoTrackIndex(); } if (m_hasAudioTarget > 0) { QVector tracks; auto it = m_model->m_allTracks.cbegin(); while (it != m_model->m_allTracks.cend()) { if ((*it)->isAudioTrack()) { tracks << (*it)->getId(); } ++it; } if (KdenliveSettings::multistream_checktrack() && audioTargets.count() > tracks.count()) { - pCore->bin()->checkProjectAudioTracks(audioTargets.count()); + pCore->bin()->checkProjectAudioTracks(QString(), audioTargets.count()); } QMapIterator st(audioTargets); while (st.hasNext()) { st.next(); if (tracks.isEmpty()) { break; } audioTracks.insert(tracks.takeLast(), st.key()); } } emit hasAudioTargetChanged(); emit hasVideoTargetChanged(); setVideoTarget(m_hasVideoTarget && (m_lastVideoTarget > -1) ? m_lastVideoTarget : videoTrack); setAudioTarget(audioTracks); } std::shared_ptr TimelineController::getModel() const { return m_model; } void TimelineController::setRoot(QQuickItem *root) { m_root = root; m_ready = true; } Mlt::Tractor *TimelineController::tractor() { return m_model->tractor(); } Mlt::Producer TimelineController::trackProducer(int tid) { return *(m_model->getTrackById(tid).get()); } double TimelineController::scaleFactor() const { return m_scale; } const QString TimelineController::getTrackNameFromMltIndex(int trackPos) { if (trackPos == -1) { return i18n("unknown"); } if (trackPos == 0) { return i18n("Black"); } return m_model->getTrackTagById(m_model->getTrackIndexFromPosition(trackPos - 1)); } const QString TimelineController::getTrackNameFromIndex(int trackIndex) { QString trackName = m_model->getTrackFullName(trackIndex); return trackName.isEmpty() ? m_model->getTrackTagById(trackIndex) : trackName; } QMap TimelineController::getTrackNames(bool videoOnly) { QMap names; for (const auto &track : m_model->m_iteratorTable) { if (videoOnly && m_model->getTrackById_const(track.first)->isAudioTrack()) { continue; } QString trackName = m_model->getTrackFullName(track.first); names[m_model->getTrackMltIndex(track.first)] = trackName; } return names; } void TimelineController::setScaleFactorOnMouse(double scale, bool zoomOnMouse) { if (m_root) { m_root->setProperty("zoomOnMouse", zoomOnMouse ? qBound(0, getMousePos(), duration()) : -1); m_scale = scale; emit scaleFactorChanged(); } else { qWarning("Timeline root not created, impossible to zoom in"); } } void TimelineController::setScaleFactor(double scale) { m_scale = scale; // Update mainwindow's zoom slider emit updateZoom(scale); // inform qml emit scaleFactorChanged(); } int TimelineController::duration() const { return m_duration; } int TimelineController::fullDuration() const { return m_duration + TimelineModel::seekDuration; } void TimelineController::checkDuration() { int currentLength = m_model->duration(); if (currentLength != m_duration) { m_duration = currentLength; emit durationChanged(); } } int TimelineController::selectedTrack() const { std::unordered_set sel = m_model->getCurrentSelection(); if (sel.empty()) return -1; std::vector> selected_tracks; // contains pairs of (track position, track id) for each selected item for (int s : sel) { int tid = m_model->getItemTrackId(s); selected_tracks.push_back({m_model->getTrackPosition(tid), tid}); } // sort by track position std::sort(selected_tracks.begin(), selected_tracks.begin(), [](const auto &a, const auto &b) { return a.first < b.first; }); return selected_tracks.front().second; } void TimelineController::selectCurrentItem(ObjectType type, bool select, bool addToCurrent) { QList toSelect; int currentClip = type == ObjectType::TimelineClip ? m_model->getClipByPosition(m_activeTrack, pCore->getTimelinePosition()) : m_model->getCompositionByPosition(m_activeTrack, pCore->getTimelinePosition()); if (currentClip == -1) { pCore->displayMessage(i18n("No item under timeline cursor in active track"), InformationMessage, 500); return; } if (!select) { m_model->requestRemoveFromSelection(currentClip); } else { m_model->requestAddToSelection(currentClip, !addToCurrent); } } QList TimelineController::selection() const { if (!m_root) return QList(); std::unordered_set sel = m_model->getCurrentSelection(); QList items; for (int id : sel) { items << id; } return items; } void TimelineController::selectItems(const QList &ids) { std::unordered_set ids_s(ids.begin(), ids.end()); m_model->requestSetSelection(ids_s); } void TimelineController::setScrollPos(int pos) { if (pos > 0 && m_root) { QMetaObject::invokeMethod(m_root, "setScrollPos", Qt::QueuedConnection, Q_ARG(QVariant, pos)); } } void TimelineController::resetView() { m_model->_resetView(); if (m_root) { QMetaObject::invokeMethod(m_root, "updatePalette"); } emit colorsChanged(); } bool TimelineController::snap() { return KdenliveSettings::snaptopoints(); } bool TimelineController::ripple() { return false; } bool TimelineController::scrub() { return false; } int TimelineController::insertClip(int tid, int position, const QString &data_str, bool logUndo, bool refreshView, bool useTargets) { int id; if (tid == -1) { tid = m_activeTrack; } if (position == -1) { position = pCore->getTimelinePosition(); } if (!m_model->requestClipInsertion(data_str, tid, position, id, logUndo, refreshView, useTargets)) { id = -1; } return id; } QList TimelineController::insertClips(int tid, int position, const QStringList &binIds, bool logUndo, bool refreshView) { QList clipIds; if (tid == -1) { tid = m_activeTrack; } if (position == -1) { position = pCore->getTimelinePosition(); } TimelineFunctions::requestMultipleClipsInsertion(m_model, binIds, tid, position, clipIds, logUndo, refreshView); // we don't need to check the return value of the above function, in case of failure it will return an empty list of ids. return clipIds; } int TimelineController::insertNewComposition(int tid, int position, const QString &transitionId, bool logUndo) { int clipId = m_model->getTrackById_const(tid)->getClipByPosition(position); if (clipId > 0) { int minimum = m_model->getClipPosition(clipId); return insertNewComposition(tid, clipId, position - minimum, transitionId, logUndo); } return insertComposition(tid, position, transitionId, logUndo); } int TimelineController::insertNewComposition(int tid, int clipId, int offset, const QString &transitionId, bool logUndo) { int id; int minimumPos = clipId > -1 ? m_model->getClipPosition(clipId) : offset; int clip_duration = clipId > -1 ? m_model->getClipPlaytime(clipId) : pCore->currentDoc()->getFramePos(KdenliveSettings::transition_duration()); int endPos = minimumPos + clip_duration; int position = minimumPos; int duration = qMin(clip_duration, pCore->currentDoc()->getFramePos(KdenliveSettings::transition_duration())); int lowerVideoTrackId = m_model->getPreviousVideoTrackIndex(tid); bool revert = offset > clip_duration / 2; int bottomId = 0; if (lowerVideoTrackId > 0) { bottomId = m_model->getTrackById_const(lowerVideoTrackId)->getClipByPosition(position + offset); } if (bottomId <= 0) { // No video track underneath if (offset < duration && duration < 2 * clip_duration) { // Composition dropped close to start, keep default composition duration } else if (clip_duration - offset < duration * 1.2 && duration < 2 * clip_duration) { // Composition dropped close to end, keep default composition duration position = endPos - duration; } else { // Use full clip length for duration duration = m_model->getTrackById_const(tid)->suggestCompositionLength(position); } } else { duration = qMin(duration, m_model->getTrackById_const(tid)->suggestCompositionLength(position)); QPair bottom(m_model->m_allClips[bottomId]->getPosition(), m_model->m_allClips[bottomId]->getPlaytime()); if (bottom.first > minimumPos) { // Lower clip is after top clip if (position + offset > bottom.first) { int test_duration = m_model->getTrackById_const(tid)->suggestCompositionLength(bottom.first); if (test_duration > 0) { offset -= (bottom.first - position); position = bottom.first; duration = test_duration; revert = position > minimumPos; } } } else if (position >= bottom.first) { // Lower clip is before or at same pos as top clip int test_duration = m_model->getTrackById_const(lowerVideoTrackId)->suggestCompositionLength(position); if (test_duration > 0) { duration = qMin(test_duration, clip_duration); } } } int defaultLength = pCore->currentDoc()->getFramePos(KdenliveSettings::transition_duration()); bool isShortComposition = TransitionsRepository::get()->getType(transitionId) == AssetListType::AssetType::VideoShortComposition; if (duration < 0 || (isShortComposition && duration > 1.5 * defaultLength)) { duration = defaultLength; } else if (duration <= 1) { // if suggested composition duration is lower than 4 frames, use default duration = pCore->currentDoc()->getFramePos(KdenliveSettings::transition_duration()); if (minimumPos + clip_duration - position < 3) { position = minimumPos + clip_duration - duration; } } QPair finalPos = m_model->getTrackById_const(tid)->validateCompositionLength(position, offset, duration, endPos); position = finalPos.first; duration = finalPos.second; std::unique_ptr props(nullptr); if (revert) { props = std::make_unique(); if (transitionId == QLatin1String("dissolve")) { props->set("reverse", 1); } else if (transitionId == QLatin1String("composite") || transitionId == QLatin1String("slide")) { props->set("invert", 1); } else if (transitionId == QLatin1String("wipe")) { props->set("geometry", "0%/0%:100%x100%:100;-1=0%/0%:100%x100%:0"); } } if (!m_model->requestCompositionInsertion(transitionId, tid, position, duration, std::move(props), id, logUndo)) { id = -1; pCore->displayMessage(i18n("Could not add composition at selected position"), InformationMessage, 500); } return id; } int TimelineController::insertComposition(int tid, int position, const QString &transitionId, bool logUndo) { int id; int duration = pCore->currentDoc()->getFramePos(KdenliveSettings::transition_duration()); if (!m_model->requestCompositionInsertion(transitionId, tid, position, duration, nullptr, id, logUndo)) { id = -1; } return id; } void TimelineController::deleteSelectedClips() { auto sel = m_model->getCurrentSelection(); if (sel.empty()) { return; } // only need to delete the first item, the others will be deleted in cascade m_model->requestItemDeletion(*sel.begin()); } int TimelineController::getMainSelectedItem(bool restrictToCurrentPos, bool allowComposition) { auto sel = m_model->getCurrentSelection(); if (sel.empty() || sel.size() > 2) { return -1; } int itemId = *(sel.begin()); if (sel.size() == 2) { int parentGroup = m_model->m_groups->getRootId(itemId); if (parentGroup == -1 || m_model->m_groups->getType(parentGroup) != GroupType::AVSplit) { return -1; } } if (!restrictToCurrentPos) { if (m_model->isClip(itemId) || (allowComposition && m_model->isComposition(itemId))) { return itemId; } } if (m_model->isClip(itemId)) { int position = pCore->getTimelinePosition(); int start = m_model->getClipPosition(itemId); int end = start + m_model->getClipPlaytime(itemId); if (position >= start && position <= end) { return itemId; } } return -1; } void TimelineController::copyItem() { std::unordered_set selectedIds = m_model->getCurrentSelection(); if (selectedIds.empty()) { return; } int clipId = *(selectedIds.begin()); QString copyString = TimelineFunctions::copyClips(m_model, selectedIds); QClipboard *clipboard = QApplication::clipboard(); clipboard->setText(copyString); m_root->setProperty("copiedClip", clipId); m_model->requestSetSelection(selectedIds); } bool TimelineController::pasteItem(int position, int tid) { QClipboard *clipboard = QApplication::clipboard(); QString txt = clipboard->text(); if (tid == -1) { tid = getMouseTrack(); } if (position == -1) { position = getMousePos(); } if (tid == -1) { tid = m_activeTrack; } if (position == -1) { position = pCore->getTimelinePosition(); } return TimelineFunctions::pasteClips(m_model, txt, tid, position); } void TimelineController::triggerAction(const QString &name) { pCore->triggerAction(name); } QString TimelineController::timecode(int frames) const { return KdenliveSettings::frametimecode() ? QString::number(frames) : m_model->tractor()->frames_to_time(frames, mlt_time_smpte_df); } QString TimelineController::framesToClock(int frames) const { return m_model->tractor()->frames_to_time(frames, mlt_time_clock); } QString TimelineController::simplifiedTC(int frames) const { if (KdenliveSettings::frametimecode()) { return QString::number(frames); } QString s = m_model->tractor()->frames_to_time(frames, mlt_time_smpte_df); return s.startsWith(QLatin1String("00:")) ? s.remove(0, 3) : s; } bool TimelineController::showThumbnails() const { return KdenliveSettings::videothumbnails(); } bool TimelineController::showAudioThumbnails() const { return KdenliveSettings::audiothumbnails(); } bool TimelineController::showMarkers() const { return KdenliveSettings::showmarkers(); } bool TimelineController::audioThumbFormat() const { return KdenliveSettings::displayallchannels(); } bool TimelineController::showWaveforms() const { return KdenliveSettings::audiothumbnails(); } void TimelineController::addTrack(int tid) { if (tid == -1) { tid = m_activeTrack; } QPointer d = new TrackDialog(m_model, tid, qApp->activeWindow()); if (d->exec() == QDialog::Accepted) { bool audioRecTrack = d->addRecTrack(); bool addAVTrack = d->addAVTrack(); int tracksCount = d->tracksCount(); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool result = true; for (int ix = 0; ix < tracksCount; ++ix) { int newTid; result = m_model->requestTrackInsertion(d->selectedTrackPosition(), newTid, d->trackName(), d->addAudioTrack(), undo, redo); if (result) { m_model->setTrackProperty(newTid, "kdenlive:timeline_active", QStringLiteral("1")); if (addAVTrack) { int newTid2; int mirrorPos = 0; int mirrorId = m_model->getMirrorAudioTrackId(newTid); if (mirrorId > -1) { mirrorPos = m_model->getTrackMltIndex(mirrorId); } result = m_model->requestTrackInsertion(mirrorPos, newTid2, d->trackName(), true, undo, redo); if (result) { m_model->setTrackProperty(newTid2, "kdenlive:timeline_active", QStringLiteral("1")); } } if (audioRecTrack) { m_model->setTrackProperty(newTid, "kdenlive:audio_rec", QStringLiteral("1")); } } else { break; } } if (result) { pCore->pushUndo(undo, redo, addAVTrack || tracksCount > 1 ? i18n("Insert Tracks") : i18n("Insert Track")); } else { pCore->displayMessage(i18n("Could not insert track"), InformationMessage, 500); undo(); } } } void TimelineController::deleteTrack(int tid) { if (tid == -1) { tid = m_activeTrack; } QPointer d = new TrackDialog(m_model, tid, qApp->activeWindow(), true); if (d->exec() == QDialog::Accepted) { int selectedTrackIx = d->selectedTrackId(); m_model->requestTrackDeletion(selectedTrackIx); if (m_activeTrack == -1) { setActiveTrack(m_model->getTrackIndexFromPosition(m_model->getTracksCount() - 1)); } } } void TimelineController::switchTrackRecord(int tid) { if (tid == -1) { tid = m_activeTrack; } if (!m_model->getTrackById_const(tid)->isAudioTrack()) { pCore->displayMessage(i18n("Select an audio track to display record controls"), InformationMessage, 500); } int recDisplayed = m_model->getTrackProperty(tid, QStringLiteral("kdenlive:audio_rec")).toInt(); if (recDisplayed == 1) { // Disable rec controls m_model->setTrackProperty(tid, QStringLiteral("kdenlive:audio_rec"), QStringLiteral("0")); } else { // Enable rec controls m_model->setTrackProperty(tid, QStringLiteral("kdenlive:audio_rec"), QStringLiteral("1")); } QModelIndex ix = m_model->makeTrackIndexFromID(tid); if (ix.isValid()) { m_model->dataChanged(ix, ix, {TimelineModel::AudioRecordRole}); } } void TimelineController::checkTrackDeletion(int selectedTrackIx) { if (m_activeTrack == selectedTrackIx) { // Make sure we don't keep an index on a deleted track m_activeTrack = -1; emit activeTrackChanged(); } if (m_model->m_audioTarget.contains(selectedTrackIx)) { QMap selection = m_model->m_audioTarget; selection.remove(selectedTrackIx); setAudioTarget(selection); } if (m_model->m_videoTarget == selectedTrackIx) { setVideoTarget(-1); } if (m_lastAudioTarget.contains(selectedTrackIx)) { m_lastAudioTarget.remove(selectedTrackIx); emit lastAudioTargetChanged(); } if (m_lastVideoTarget == selectedTrackIx) { m_lastVideoTarget = -1; emit lastVideoTargetChanged(); } } void TimelineController::showConfig(int page, int tab) { pCore->showConfigDialog(page, tab); } void TimelineController::gotoNextSnap() { if (m_activeSnaps.empty() || pCore->undoIndex() != m_snapStackIndex) { m_snapStackIndex = pCore->undoIndex(); m_activeSnaps.clear(); m_activeSnaps = pCore->projectManager()->current()->getGuideModel()->getSnapPoints(); m_activeSnaps.push_back(m_zone.x()); m_activeSnaps.push_back(m_zone.y() - 1); } int nextSnap = m_model->getNextSnapPos(pCore->getTimelinePosition(), m_activeSnaps); if (nextSnap > pCore->getTimelinePosition()) { setPosition(nextSnap); } } void TimelineController::gotoPreviousSnap() { if (pCore->getTimelinePosition() > 0) { if (m_activeSnaps.empty() || pCore->undoIndex() != m_snapStackIndex) { m_snapStackIndex = pCore->undoIndex(); m_activeSnaps.clear(); m_activeSnaps = pCore->projectManager()->current()->getGuideModel()->getSnapPoints(); m_activeSnaps.push_back(m_zone.x()); m_activeSnaps.push_back(m_zone.y() - 1); } setPosition(m_model->getPreviousSnapPos(pCore->getTimelinePosition(), m_activeSnaps)); } } void TimelineController::gotoNextGuide() { QList guides = pCore->projectManager()->current()->getGuideModel()->getAllMarkers(); int pos = pCore->getTimelinePosition(); double fps = pCore->getCurrentFps(); for (auto &guide : guides) { if (guide.time().frames(fps) > pos) { setPosition(guide.time().frames(fps)); return; } } setPosition(m_duration - 1); } void TimelineController::gotoPreviousGuide() { if (pCore->getTimelinePosition() > 0) { QList guides = pCore->projectManager()->current()->getGuideModel()->getAllMarkers(); int pos = pCore->getTimelinePosition(); double fps = pCore->getCurrentFps(); int lastGuidePos = 0; for (auto &guide : guides) { if (guide.time().frames(fps) >= pos) { setPosition(lastGuidePos); return; } lastGuidePos = guide.time().frames(fps); } setPosition(lastGuidePos); } } void TimelineController::groupSelection() { const auto selection = m_model->getCurrentSelection(); if (selection.size() < 2) { pCore->displayMessage(i18n("Select at least 2 items to group"), InformationMessage, 500); return; } m_model->requestClearSelection(); m_model->requestClipsGroup(selection); m_model->requestSetSelection(selection); } void TimelineController::unGroupSelection(int cid) { auto ids = m_model->getCurrentSelection(); // ask to unselect if needed m_model->requestClearSelection(); if (cid > -1) { ids.insert(cid); } if (!ids.empty()) { m_model->requestClipsUngroup(ids); } } bool TimelineController::dragOperationRunning() { QVariant returnedValue; QMetaObject::invokeMethod(m_root, "isDragging", Q_RETURN_ARG(QVariant, returnedValue)); return returnedValue.toBool(); } void TimelineController::setInPoint() { if (dragOperationRunning()) { // Don't allow timeline operation while drag in progress qDebug() << "Cannot operate while dragging"; return; } int cursorPos = pCore->getTimelinePosition(); const auto selection = m_model->getCurrentSelection(); bool selectionFound = false; if (!selection.empty()) { for (int id : selection) { int start = m_model->getItemPosition(id); if (start == cursorPos) { continue; } int size = start + m_model->getItemPlaytime(id) - cursorPos; m_model->requestItemResize(id, size, false, true, 0, false); selectionFound = true; } } if (!selectionFound) { if (m_activeTrack >= 0) { int cid = m_model->getClipByPosition(m_activeTrack, cursorPos); if (cid < 0) { // Check first item after timeline position int maximumSpace = m_model->getTrackById_const(m_activeTrack)->getBlankEnd(cursorPos); if (maximumSpace < INT_MAX) { cid = m_model->getClipByPosition(m_activeTrack, maximumSpace + 1); } } if (cid >= 0) { int start = m_model->getItemPosition(cid); if (start != cursorPos) { int size = start + m_model->getItemPlaytime(cid) - cursorPos; m_model->requestItemResize(cid, size, false, true, 0, false); } } } } } void TimelineController::setOutPoint() { if (dragOperationRunning()) { // Don't allow timeline operation while drag in progress qDebug() << "Cannot operate while dragging"; return; } int cursorPos = pCore->getTimelinePosition(); const auto selection = m_model->getCurrentSelection(); bool selectionFound = false; if (!selection.empty()) { for (int id : selection) { int start = m_model->getItemPosition(id); if (start + m_model->getItemPlaytime(id) == cursorPos) { continue; } int size = cursorPos - start; m_model->requestItemResize(id, size, true, true, 0, false); selectionFound = true; } } if (!selectionFound) { if (m_activeTrack >= 0) { int cid = m_model->getClipByPosition(m_activeTrack, cursorPos); if (cid < 0) { // Check first item after timeline position int minimumSpace = m_model->getTrackById_const(m_activeTrack)->getBlankStart(cursorPos); cid = m_model->getClipByPosition(m_activeTrack, qMax(0, minimumSpace - 1)); } if (cid >= 0) { int start = m_model->getItemPosition(cid); if (start + m_model->getItemPlaytime(cid) != cursorPos) { int size = cursorPos - start; m_model->requestItemResize(cid, size, true, true, 0, false); } } } } } void TimelineController::editMarker(int cid, int position) { if (cid == -1) { cid = m_root->property("mainItemId").toInt(); } Q_ASSERT(m_model->isClip(cid)); double speed = m_model->getClipSpeed(cid); if (position == -1) { // Calculate marker position relative to timeline cursor position = pCore->getTimelinePosition() - m_model->getClipPosition(cid) + m_model->getClipIn(cid); position = position * speed; } if (position < (m_model->getClipIn(cid) * speed) || position > (m_model->getClipIn(cid) * speed + m_model->getClipPlaytime(cid))) { pCore->displayMessage(i18n("Cannot find clip to edit marker"), InformationMessage, 500); return; } std::shared_ptr clip = pCore->bin()->getBinClip(getClipBinId(cid)); if (clip->getMarkerModel()->hasMarker(position)) { GenTime pos(position, pCore->getCurrentFps()); clip->getMarkerModel()->editMarkerGui(pos, qApp->activeWindow(), false, clip.get()); } else { pCore->displayMessage(i18n("Cannot find clip to edit marker"), InformationMessage, 500); } } void TimelineController::addMarker(int cid, int position) { if (cid == -1) { cid = m_root->property("mainItemId").toInt(); } Q_ASSERT(m_model->isClip(cid)); double speed = m_model->getClipSpeed(cid); if (position == -1) { // Calculate marker position relative to timeline cursor position = pCore->getTimelinePosition() - m_model->getClipPosition(cid) + m_model->getClipIn(cid); position = position * speed; } if (position < (m_model->getClipIn(cid) * speed) || position > (m_model->getClipIn(cid) * speed + m_model->getClipPlaytime(cid))) { pCore->displayMessage(i18n("Cannot find clip to edit marker"), InformationMessage, 500); return; } std::shared_ptr clip = pCore->bin()->getBinClip(getClipBinId(cid)); GenTime pos(position, pCore->getCurrentFps()); clip->getMarkerModel()->editMarkerGui(pos, qApp->activeWindow(), true, clip.get()); } void TimelineController::addQuickMarker(int cid, int position) { if (cid == -1) { cid = m_root->property("mainItemId").toInt(); } Q_ASSERT(m_model->isClip(cid)); double speed = m_model->getClipSpeed(cid); if (position == -1) { // Calculate marker position relative to timeline cursor position = pCore->getTimelinePosition() - m_model->getClipPosition(cid); position = position * speed; } if (position < (m_model->getClipIn(cid) * speed) || position > ((m_model->getClipIn(cid) + m_model->getClipPlaytime(cid) * speed))) { pCore->displayMessage(i18n("Cannot find clip to edit marker"), InformationMessage, 500); return; } std::shared_ptr clip = pCore->bin()->getBinClip(getClipBinId(cid)); GenTime pos(position, pCore->getCurrentFps()); CommentedTime marker(pos, pCore->currentDoc()->timecode().getDisplayTimecode(pos, false), KdenliveSettings::default_marker_type()); clip->getMarkerModel()->addMarker(marker.time(), marker.comment(), marker.markerType()); } void TimelineController::deleteMarker(int cid, int position) { if (cid == -1) { cid = m_root->property("mainItemId").toInt(); } Q_ASSERT(m_model->isClip(cid)); double speed = m_model->getClipSpeed(cid); if (position == -1) { // Calculate marker position relative to timeline cursor position = pCore->getTimelinePosition() - m_model->getClipPosition(cid) + m_model->getClipIn(cid); position = position * speed; } if (position < (m_model->getClipIn(cid) * speed) || position > (m_model->getClipIn(cid) * speed + m_model->getClipPlaytime(cid))) { pCore->displayMessage(i18n("Cannot find clip to edit marker"), InformationMessage, 500); return; } std::shared_ptr clip = pCore->bin()->getBinClip(getClipBinId(cid)); GenTime pos(position, pCore->getCurrentFps()); clip->getMarkerModel()->removeMarker(pos); } void TimelineController::deleteAllMarkers(int cid) { if (cid == -1) { cid = m_root->property("mainItemId").toInt(); } Q_ASSERT(m_model->isClip(cid)); std::shared_ptr clip = pCore->bin()->getBinClip(getClipBinId(cid)); clip->getMarkerModel()->removeAllMarkers(); } void TimelineController::editGuide(int frame) { if (frame == -1) { frame = pCore->getTimelinePosition(); } auto guideModel = pCore->projectManager()->current()->getGuideModel(); GenTime pos(frame, pCore->getCurrentFps()); guideModel->editMarkerGui(pos, qApp->activeWindow(), false); } void TimelineController::moveGuide(int frame, int newFrame) { auto guideModel = pCore->projectManager()->current()->getGuideModel(); GenTime pos(frame, pCore->getCurrentFps()); GenTime newPos(newFrame, pCore->getCurrentFps()); guideModel->editMarker(pos, newPos); } void TimelineController::switchGuide(int frame, bool deleteOnly) { bool markerFound = false; if (frame == -1) { frame = pCore->getTimelinePosition(); } CommentedTime marker = pCore->projectManager()->current()->getGuideModel()->getMarker(GenTime(frame, pCore->getCurrentFps()), &markerFound); if (!markerFound) { if (deleteOnly) { pCore->displayMessage(i18n("No guide found at current position"), InformationMessage, 500); return; } GenTime pos(frame, pCore->getCurrentFps()); pCore->projectManager()->current()->getGuideModel()->addMarker(pos, i18n("guide")); } else { pCore->projectManager()->current()->getGuideModel()->removeMarker(marker.time()); } } void TimelineController::addAsset(const QVariantMap &data) { QString effect = data.value(QStringLiteral("kdenlive/effect")).toString(); const auto selection = m_model->getCurrentSelection(); if (!selection.empty()) { QList effectSelection; for (int id : selection) { if (m_model->isClip(id)) { effectSelection << id; } } bool foundMatch = false; for (int id : effectSelection) { if (m_model->addClipEffect(id, effect, false)) { foundMatch = true; } } if (!foundMatch) { QString effectName = EffectsRepository::get()->getName(effect); pCore->displayMessage(i18n("Cannot add effect %1 to selected clip", effectName), InformationMessage, 500); } } else { pCore->displayMessage(i18n("Select a clip to apply an effect"), InformationMessage, 500); } } void TimelineController::requestRefresh() { pCore->requestMonitorRefresh(); } void TimelineController::showAsset(int id) { if (m_model->isComposition(id)) { emit showTransitionModel(id, m_model->getCompositionParameterModel(id)); } else if (m_model->isClip(id)) { QModelIndex clipIx = m_model->makeClipIndexFromID(id); QString clipName = m_model->data(clipIx, Qt::DisplayRole).toString(); bool showKeyframes = m_model->data(clipIx, TimelineModel::ShowKeyframesRole).toInt(); qDebug() << "-----\n// SHOW KEYFRAMES: " << showKeyframes; emit showItemEffectStack(clipName, m_model->getClipEffectStackModel(id), m_model->getClipFrameSize(id), showKeyframes); } } void TimelineController::showTrackAsset(int trackId) { emit showItemEffectStack(getTrackNameFromIndex(trackId), m_model->getTrackEffectStackModel(trackId), pCore->getCurrentFrameSize(), false); } void TimelineController::adjustAllTrackHeight(int trackId, int height) { bool isAudio = m_model->getTrackById_const(trackId)->isAudioTrack(); auto it = m_model->m_allTracks.cbegin(); while (it != m_model->m_allTracks.cend()) { int target_track = (*it)->getId(); if (target_track != trackId && m_model->getTrackById_const(target_track)->isAudioTrack() == isAudio) { m_model->getTrackById(target_track)->setProperty(QStringLiteral("kdenlive:trackheight"), QString::number(height)); } ++it; } int tracksCount = m_model->getTracksCount(); QModelIndex modelStart = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(0)); QModelIndex modelEnd = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(tracksCount - 1)); m_model->dataChanged(modelStart, modelEnd, {TimelineModel::HeightRole}); } void TimelineController::collapseAllTrackHeight(int trackId, bool collapse, int collapsedHeight) { bool isAudio = m_model->getTrackById_const(trackId)->isAudioTrack(); auto it = m_model->m_allTracks.cbegin(); while (it != m_model->m_allTracks.cend()) { int target_track = (*it)->getId(); if (m_model->getTrackById_const(target_track)->isAudioTrack() == isAudio) { if (collapse) { m_model->setTrackProperty(target_track, "kdenlive:collapsed", QString::number(collapsedHeight)); } else { m_model->setTrackProperty(target_track, "kdenlive:collapsed", QStringLiteral("0")); } } ++it; } int tracksCount = m_model->getTracksCount(); QModelIndex modelStart = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(0)); QModelIndex modelEnd = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(tracksCount - 1)); m_model->dataChanged(modelStart, modelEnd, {TimelineModel::HeightRole}); } void TimelineController::defaultTrackHeight(int trackId) { if (trackId > -1) { m_model->getTrackById(trackId)->setProperty(QStringLiteral("kdenlive:trackheight"), QString::number(KdenliveSettings::trackheight())); QModelIndex modelStart = m_model->makeTrackIndexFromID(trackId); m_model->dataChanged(modelStart, modelStart, {TimelineModel::HeightRole}); return; } auto it = m_model->m_allTracks.cbegin(); while (it != m_model->m_allTracks.cend()) { int target_track = (*it)->getId(); m_model->getTrackById(target_track)->setProperty(QStringLiteral("kdenlive:trackheight"), QString::number(KdenliveSettings::trackheight())); ++it; } int tracksCount = m_model->getTracksCount(); QModelIndex modelStart = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(0)); QModelIndex modelEnd = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(tracksCount - 1)); m_model->dataChanged(modelStart, modelEnd, {TimelineModel::HeightRole}); } void TimelineController::setPosition(int position) { // Process seek request emit seeked(position); } void TimelineController::setAudioTarget(QMap tracks) { if ((!tracks.isEmpty() && !m_model->isTrack(tracks.firstKey())) || m_hasAudioTarget == 0) { return; } // Clear targets before re-adding to trigger qml refresh m_model->m_audioTarget.clear(); emit audioTargetChanged(); m_model->m_audioTarget = tracks; emit audioTargetChanged(); } void TimelineController::switchAudioTarget(int trackId) { if (m_model->m_audioTarget.contains(trackId)) { m_model->m_audioTarget.remove(trackId); } else { //TODO: use track description int ix = getFirstUnassignedStream(); if (ix > -1) { m_model->m_audioTarget.insert(trackId, ix); } } emit audioTargetChanged(); } void TimelineController::assignAudioTarget(int trackId, int stream) { QList assignedStreams = m_model->m_audioTarget.values(); if (assignedStreams.contains(stream)) { // This stream was assigned to another track, remove m_model->m_audioTarget.remove(m_model->m_audioTarget.key(stream)); } //Remove and re-add target track to trigger a refresh in qml track headers m_model->m_audioTarget.remove(trackId); emit audioTargetChanged(); m_model->m_audioTarget.insert(trackId, stream); emit audioTargetChanged(); } int TimelineController::getFirstUnassignedStream() const { QList keys = m_model->m_binAudioTargets.keys(); QList assigned = m_model->m_audioTarget.values(); for (int k : keys) { if (!assigned.contains(k)) { return k; } } return -1; } void TimelineController::setVideoTarget(int track) { if ((track > -1 && !m_model->isTrack(track)) || !m_hasVideoTarget) { return; } m_model->m_videoTarget = track; emit videoTargetChanged(); } void TimelineController::setActiveTrack(int track) { if (track > -1 && !m_model->isTrack(track)) { return; } m_activeTrack = track; emit activeTrackChanged(); } void TimelineController::setZone(const QPoint &zone, bool withUndo) { if (m_zone.x() > 0) { m_model->removeSnap(m_zone.x()); } if (m_zone.y() > 0) { m_model->removeSnap(m_zone.y() - 1); } if (zone.x() > 0) { m_model->addSnap(zone.x()); } if (zone.y() > 0) { m_model->addSnap(zone.y() - 1); } updateZone(m_zone, zone, withUndo); } void TimelineController::updateZone(const QPoint oldZone, const QPoint newZone, bool withUndo) { if (!withUndo) { m_zone = newZone; emit zoneChanged(); // Update monitor zone emit zoneMoved(m_zone); return; } std::function undo = []() { return true; }; std::function redo = []() { return true; }; Fun undo_zone = [this, oldZone]() { setZone(oldZone, false); return true; }; Fun redo_zone = [this, newZone]() { setZone(newZone, false); return true; }; redo_zone(); UPDATE_UNDO_REDO_NOLOCK(redo_zone, undo_zone, undo, redo); pCore->pushUndo(undo, redo, i18n("Set Zone")); } void TimelineController::setZoneIn(int inPoint) { if (m_zone.x() > 0) { m_model->removeSnap(m_zone.x()); } if (inPoint > 0) { m_model->addSnap(inPoint); } m_zone.setX(inPoint); emit zoneChanged(); // Update monitor zone emit zoneMoved(m_zone); } void TimelineController::setZoneOut(int outPoint) { if (m_zone.y() > 0) { m_model->removeSnap(m_zone.y() - 1); } if (outPoint > 0) { m_model->addSnap(outPoint - 1); } m_zone.setY(outPoint); emit zoneChanged(); emit zoneMoved(m_zone); } void TimelineController::selectItems(const QVariantList &tracks, int startFrame, int endFrame, bool addToSelect, bool selectBottomCompositions) { std::unordered_set itemsToSelect; if (addToSelect) { itemsToSelect = m_model->getCurrentSelection(); } for (int i = 0; i < tracks.count(); i++) { if (m_model->getTrackById_const(tracks.at(i).toInt())->isLocked()) { continue; } auto currentClips = m_model->getItemsInRange(tracks.at(i).toInt(), startFrame, endFrame, i < tracks.count() - 1 ? true : selectBottomCompositions); itemsToSelect.insert(currentClips.begin(), currentClips.end()); } m_model->requestSetSelection(itemsToSelect); } void TimelineController::requestClipCut(int clipId, int position) { if (position == -1) { position = pCore->getTimelinePosition(); } TimelineFunctions::requestClipCut(m_model, clipId, position); } void TimelineController::cutClipUnderCursor(int position, int track) { if (position == -1) { position = pCore->getTimelinePosition(); } QMutexLocker lk(&m_metaMutex); bool foundClip = false; const auto selection = m_model->getCurrentSelection(); if (track == -1) { for (int cid : selection) { if (m_model->isClip(cid) && positionIsInItem(cid)) { if (TimelineFunctions::requestClipCut(m_model, cid, position)) { foundClip = true; // Cutting clips in the selection group is handled in TimelineFunctions break; } } } } if (!foundClip) { if (track == -1) { track = m_activeTrack; } if (track >= 0) { int cid = m_model->getClipByPosition(track, position); if (cid >= 0 && TimelineFunctions::requestClipCut(m_model, cid, position)) { foundClip = true; } } } if (!foundClip) { pCore->displayMessage(i18n("No clip to cut"), InformationMessage, 500); } } void TimelineController::cutAllClipsUnderCursor(int position) { if (position == -1) { position = pCore->getTimelinePosition(); } QMutexLocker lk(&m_metaMutex); TimelineFunctions::requestClipCutAll(m_model, position); } int TimelineController::requestSpacerStartOperation(int trackId, int position) { QMutexLocker lk(&m_metaMutex); int itemId = TimelineFunctions::requestSpacerStartOperation(m_model, trackId, position); return itemId; } bool TimelineController::requestSpacerEndOperation(int clipId, int startPosition, int endPosition) { QMutexLocker lk(&m_metaMutex); bool result = TimelineFunctions::requestSpacerEndOperation(m_model, clipId, startPosition, endPosition); return result; } void TimelineController::seekCurrentClip(bool seekToEnd) { const auto selection = m_model->getCurrentSelection(); if (!selection.empty()) { int cid = *selection.begin(); int start = m_model->getItemPosition(cid); if (seekToEnd) { start += m_model->getItemPlaytime(cid); } setPosition(start); } } void TimelineController::seekToClip(int cid, bool seekToEnd) { int start = m_model->getItemPosition(cid); if (seekToEnd) { start += m_model->getItemPlaytime(cid); } setPosition(start); } void TimelineController::seekToMouse() { int mousePos = getMousePos(); if (mousePos > -1) { setPosition(mousePos); } } int TimelineController::getMousePos() { QVariant returnedValue; QMetaObject::invokeMethod(m_root, "getMousePos", Q_RETURN_ARG(QVariant, returnedValue)); return returnedValue.toInt(); } int TimelineController::getMouseTrack() { QVariant returnedValue; QMetaObject::invokeMethod(m_root, "getMouseTrack", Q_RETURN_ARG(QVariant, returnedValue)); return returnedValue.toInt(); } bool TimelineController::positionIsInItem(int id) { int in = m_model->getItemPosition(id); int position = pCore->getTimelinePosition(); if (in > position) { return false; } if (position <= in + m_model->getItemPlaytime(id)) { return true; } return false; } void TimelineController::refreshItem(int id) { if (m_model->isClip(id) && m_model->m_allClips[id]->isAudioOnly()) { return; } if (positionIsInItem(id)) { pCore->requestMonitorRefresh(); } } QPair TimelineController::getTracksCount() const { QVariant returnedValue; QMetaObject::invokeMethod(m_root, "getTracksCount", Q_RETURN_ARG(QVariant, returnedValue)); QVariantList tracks = returnedValue.toList(); return {tracks.at(0).toInt(), tracks.at(1).toInt()}; } QStringList TimelineController::extractCompositionLumas() const { return m_model->extractCompositionLumas(); } void TimelineController::addEffectToCurrentClip(const QStringList &effectData) { QList activeClips; for (int track = m_model->getTracksCount() - 1; track >= 0; track--) { int trackIx = m_model->getTrackIndexFromPosition(track); int cid = m_model->getClipByPosition(trackIx, pCore->getTimelinePosition()); if (cid > -1) { activeClips << cid; } } if (!activeClips.isEmpty()) { if (effectData.count() == 4) { QString effectString = effectData.at(1) + QStringLiteral("-") + effectData.at(2) + QStringLiteral("-") + effectData.at(3); m_model->copyClipEffect(activeClips.first(), effectString); } else { m_model->addClipEffect(activeClips.first(), effectData.constFirst()); } } } void TimelineController::adjustFade(int cid, const QString &effectId, int duration, int initialDuration) { if (initialDuration == -2) { // Add default fade duration = pCore->currentDoc()->getFramePos(KdenliveSettings::fade_duration()); initialDuration = 0; } if (duration <= 0) { // remove fade m_model->removeFade(cid, effectId == QLatin1String("fadein")); } else { m_model->adjustEffectLength(cid, effectId, duration, initialDuration); } } QPair TimelineController::getCompositionATrack(int cid) const { QPair result; std::shared_ptr compo = m_model->getCompositionPtr(cid); if (compo) { result = QPair(compo->getATrack(), m_model->getTrackMltIndex(compo->getCurrentTrackId())); } return result; } void TimelineController::setCompositionATrack(int cid, int aTrack) { TimelineFunctions::setCompositionATrack(m_model, cid, aTrack); } bool TimelineController::compositionAutoTrack(int cid) const { std::shared_ptr compo = m_model->getCompositionPtr(cid); return compo && compo->getForcedTrack() == -1; } const QString TimelineController::getClipBinId(int clipId) const { return m_model->getClipBinId(clipId); } void TimelineController::focusItem(int itemId) { int start = m_model->getItemPosition(itemId); setPosition(start); } int TimelineController::headerWidth() const { return qMax(10, KdenliveSettings::headerwidth()); } void TimelineController::setHeaderWidth(int width) { KdenliveSettings::setHeaderwidth(width); } bool TimelineController::createSplitOverlay(int clipId, std::shared_ptr filter) { if (m_timelinePreview && m_timelinePreview->hasOverlayTrack()) { return true; } if (clipId == -1) { pCore->displayMessage(i18n("Select a clip to compare effect"), InformationMessage, 500); return false; } std::shared_ptr clip = m_model->getClipPtr(clipId); const QString binId = clip->binId(); // Get clean bin copy of the clip std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(binId); std::shared_ptr binProd(binClip->masterProducer()->cut(clip->getIn(), clip->getOut())); // Get copy of timeline producer std::shared_ptr clipProducer(new Mlt::Producer(*clip)); // Built tractor and compositing Mlt::Tractor trac(*m_model->m_tractor->profile()); Mlt::Playlist play(*m_model->m_tractor->profile()); Mlt::Playlist play2(*m_model->m_tractor->profile()); play.append(*clipProducer.get()); play2.append(*binProd); trac.set_track(play, 0); trac.set_track(play2, 1); play2.attach(*filter.get()); QString splitTransition = TransitionsRepository::get()->getCompositingTransition(); Mlt::Transition t(*m_model->m_tractor->profile(), splitTransition.toUtf8().constData()); t.set("always_active", 1); trac.plant_transition(t, 0, 1); int startPos = m_model->getClipPosition(clipId); // plug in overlay playlist auto *overlay = new Mlt::Playlist(*m_model->m_tractor->profile()); overlay->insert_blank(0, startPos); Mlt::Producer split(trac.get_producer()); overlay->insert_at(startPos, &split, 1); // insert in tractor if (!m_timelinePreview) { initializePreview(); } m_timelinePreview->setOverlayTrack(overlay); m_model->m_overlayTrackCount = m_timelinePreview->addedTracks(); return true; } void TimelineController::removeSplitOverlay() { if (!m_timelinePreview || !m_timelinePreview->hasOverlayTrack()) { return; } // disconnect m_timelinePreview->removeOverlayTrack(); m_model->m_overlayTrackCount = m_timelinePreview->addedTracks(); } void TimelineController::addPreviewRange(bool add) { if (m_zone.isNull()) { return; } if (!m_timelinePreview) { initializePreview(); } if (m_timelinePreview) { m_timelinePreview->addPreviewRange(m_zone, add); } } void TimelineController::clearPreviewRange(bool resetZones) { if (m_timelinePreview) { m_timelinePreview->clearPreviewRange(resetZones); } } void TimelineController::startPreviewRender() { // Timeline preview stuff if (!m_timelinePreview) { initializePreview(); } else if (m_disablePreview->isChecked()) { m_disablePreview->setChecked(false); disablePreview(false); } if (m_timelinePreview) { if (!m_usePreview) { m_timelinePreview->buildPreviewTrack(); m_usePreview = true; m_model->m_overlayTrackCount = m_timelinePreview->addedTracks(); } m_timelinePreview->startPreviewRender(); } } void TimelineController::stopPreviewRender() { if (m_timelinePreview) { m_timelinePreview->abortRendering(); } } void TimelineController::initializePreview() { if (m_timelinePreview) { // Update parameters if (!m_timelinePreview->loadParams()) { if (m_usePreview) { // Disconnect preview track m_timelinePreview->disconnectTrack(); m_usePreview = false; } delete m_timelinePreview; m_timelinePreview = nullptr; } } else { m_timelinePreview = new PreviewManager(this, m_model->m_tractor.get()); if (!m_timelinePreview->initialize()) { // TODO warn user delete m_timelinePreview; m_timelinePreview = nullptr; } else { } } QAction *previewRender = pCore->currentDoc()->getAction(QStringLiteral("prerender_timeline_zone")); if (previewRender) { previewRender->setEnabled(m_timelinePreview != nullptr); } m_disablePreview->setEnabled(m_timelinePreview != nullptr); m_disablePreview->blockSignals(true); m_disablePreview->setChecked(false); m_disablePreview->blockSignals(false); } bool TimelineController::hasPreviewTrack() const { return (m_timelinePreview && (m_timelinePreview->hasOverlayTrack() || m_timelinePreview->hasPreviewTrack())); } void TimelineController::updatePreviewConnection(bool enable) { if (m_timelinePreview) { if (enable) { m_timelinePreview->enable(); } else { m_timelinePreview->disable(); } } } void TimelineController::disablePreview(bool disable) { if (disable) { m_timelinePreview->deletePreviewTrack(); m_usePreview = false; m_model->m_overlayTrackCount = m_timelinePreview->addedTracks(); } else { if (!m_usePreview) { if (!m_timelinePreview->buildPreviewTrack()) { // preview track already exists, reconnect m_model->m_tractor->lock(); m_timelinePreview->reconnectTrack(); m_model->m_tractor->unlock(); } m_timelinePreview->loadChunks(QVariantList(), QVariantList(), QDateTime()); m_usePreview = true; } } m_model->m_overlayTrackCount = m_timelinePreview->addedTracks(); } QVariantList TimelineController::dirtyChunks() const { return m_timelinePreview ? m_timelinePreview->m_dirtyChunks : QVariantList(); } QVariantList TimelineController::renderedChunks() const { return m_timelinePreview ? m_timelinePreview->m_renderedChunks : QVariantList(); } int TimelineController::workingPreview() const { return m_timelinePreview ? m_timelinePreview->workingPreview : -1; } bool TimelineController::useRuler() const { return pCore->currentDoc()->getDocumentProperty(QStringLiteral("enableTimelineZone")).toInt() == 1; } void TimelineController::resetPreview() { if (m_timelinePreview) { m_timelinePreview->clearPreviewRange(true); initializePreview(); } } void TimelineController::loadPreview(const QString &chunks, const QString &dirty, const QDateTime &documentDate, int enable) { if (chunks.isEmpty() && dirty.isEmpty()) { return; } if (!m_timelinePreview) { initializePreview(); } QVariantList renderedChunks; QVariantList dirtyChunks; #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) QStringList chunksList = chunks.split(QLatin1Char(','), QString::SkipEmptyParts); #else QStringList chunksList = chunks.split(QLatin1Char(','), Qt::SkipEmptyParts); #endif #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) QStringList dirtyList = dirty.split(QLatin1Char(','), QString::SkipEmptyParts); #else QStringList dirtyList = dirty.split(QLatin1Char(','), Qt::SkipEmptyParts); #endif for (const QString &frame : chunksList) { renderedChunks << frame.toInt(); } for (const QString &frame : dirtyList) { dirtyChunks << frame.toInt(); } m_disablePreview->blockSignals(true); m_disablePreview->setChecked(enable); m_disablePreview->blockSignals(false); if (!enable) { m_timelinePreview->buildPreviewTrack(); m_usePreview = true; m_model->m_overlayTrackCount = m_timelinePreview->addedTracks(); } m_timelinePreview->loadChunks(renderedChunks, dirtyChunks, documentDate); } QMap TimelineController::documentProperties() { QMap props = pCore->currentDoc()->documentProperties(); int audioTarget = m_model->m_audioTarget.isEmpty() ? -1 : m_model->getTrackPosition(m_model->m_audioTarget.firstKey()); int videoTarget = m_model->m_videoTarget == -1 ? -1 : m_model->getTrackPosition(m_model->m_videoTarget); int activeTrack = m_activeTrack == -1 ? -1 : m_model->getTrackPosition(m_activeTrack); props.insert(QStringLiteral("audioTarget"), QString::number(audioTarget)); props.insert(QStringLiteral("videoTarget"), QString::number(videoTarget)); props.insert(QStringLiteral("activeTrack"), QString::number(activeTrack)); props.insert(QStringLiteral("position"), QString::number(pCore->getTimelinePosition())); QVariant returnedValue; QMetaObject::invokeMethod(m_root, "getScrollPos", Q_RETURN_ARG(QVariant, returnedValue)); int scrollPos = returnedValue.toInt(); props.insert(QStringLiteral("scrollPos"), QString::number(scrollPos)); props.insert(QStringLiteral("zonein"), QString::number(m_zone.x())); props.insert(QStringLiteral("zoneout"), QString::number(m_zone.y())); if (m_timelinePreview) { QPair chunks = m_timelinePreview->previewChunks(); props.insert(QStringLiteral("previewchunks"), chunks.first.join(QLatin1Char(','))); props.insert(QStringLiteral("dirtypreviewchunks"), chunks.second.join(QLatin1Char(','))); } props.insert(QStringLiteral("disablepreview"), QString::number((int)m_disablePreview->isChecked())); return props; } int TimelineController::getMenuOrTimelinePos() const { int frame = m_root->property("mainFrame").toInt(); if (frame == -1) { frame = pCore->getTimelinePosition(); } return frame; } void TimelineController::insertSpace(int trackId, int frame) { if (frame == -1) { frame = getMenuOrTimelinePos(); } if (trackId == -1) { trackId = m_activeTrack; } QPointer d = new SpacerDialog(GenTime(65, pCore->getCurrentFps()), pCore->currentDoc()->timecode(), qApp->activeWindow()); if (d->exec() != QDialog::Accepted) { delete d; return; } int cid = requestSpacerStartOperation(d->affectAllTracks() ? -1 : trackId, frame); int spaceDuration = d->selectedDuration().frames(pCore->getCurrentFps()); delete d; if (cid == -1) { pCore->displayMessage(i18n("No clips found to insert space"), InformationMessage, 500); return; } int start = m_model->getItemPosition(cid); requestSpacerEndOperation(cid, start, start + spaceDuration); } void TimelineController::removeSpace(int trackId, int frame, bool affectAllTracks) { if (frame == -1) { frame = getMenuOrTimelinePos(); } if (trackId == -1) { trackId = m_activeTrack; } bool res = TimelineFunctions::requestDeleteBlankAt(m_model, trackId, frame, affectAllTracks); if (!res) { pCore->displayMessage(i18n("Cannot remove space at given position"), InformationMessage, 500); } } void TimelineController::invalidateItem(int cid) { if (!m_timelinePreview || !m_model->isItem(cid)) { return; } const int tid = m_model->getItemTrackId(cid); if (tid == -1 || m_model->getTrackById_const(tid)->isAudioTrack()) { return; } int start = m_model->getItemPosition(cid); int end = start + m_model->getItemPlaytime(cid); m_timelinePreview->invalidatePreview(start, end); } void TimelineController::invalidateTrack(int tid) { if (!m_timelinePreview || !m_model->isTrack(tid) || m_model->getTrackById_const(tid)->isAudioTrack()) { return; } for (auto clp : m_model->getTrackById_const(tid)->m_allClips) { invalidateItem(clp.first); } } void TimelineController::invalidateZone(int in, int out) { if (!m_timelinePreview) { return; } m_timelinePreview->invalidatePreview(in, out == -1 ? m_duration : out); } void TimelineController::changeItemSpeed(int clipId, double speed) { /*if (clipId == -1) { clipId = getMainSelectedItem(false, true); }*/ if (clipId == -1) { clipId = m_root->property("mainItemId").toInt(); } if (clipId == -1) { pCore->displayMessage(i18n("No item to edit"), InformationMessage, 500); return; } bool pitchCompensate = m_model->m_allClips[clipId]->getIntProperty(QStringLiteral("warp_pitch")); if (qFuzzyCompare(speed, -1)) { speed = 100 * m_model->getClipSpeed(clipId); double duration = m_model->getItemPlaytime(clipId); // this is the max speed so that the clip is at least one frame long double maxSpeed = 100. * duration * qAbs(m_model->getClipSpeed(clipId)); // this is the min speed so that the clip doesn't bump into the next one on track double minSpeed = 100. * duration * qAbs(m_model->getClipSpeed(clipId)) / (duration + double(m_model->getBlankSizeNearClip(clipId, true))); // if there is a split partner, we must also take it into account int partner = m_model->getClipSplitPartner(clipId); if (partner != -1) { double duration2 = m_model->getItemPlaytime(partner); double maxSpeed2 = 100. * duration2 * qAbs(m_model->getClipSpeed(partner)); double minSpeed2 = 100. * duration2 * qAbs(m_model->getClipSpeed(partner)) / (duration2 + double(m_model->getBlankSizeNearClip(partner, true))); minSpeed = std::max(minSpeed, minSpeed2); maxSpeed = std::min(maxSpeed, maxSpeed2); } QScopedPointer d(new SpeedDialog(QApplication::activeWindow(), std::abs(speed), minSpeed, maxSpeed, speed < 0, pitchCompensate)); if (d->exec() != QDialog::Accepted) { return; } speed = d->getValue(); pitchCompensate = d->getPitchCompensate(); qDebug() << "requesting speed " << speed; } m_model->requestClipTimeWarp(clipId, speed, pitchCompensate, true); } void TimelineController::switchCompositing(int mode) { // m_model->m_tractor->lock(); pCore->currentDoc()->setDocumentProperty(QStringLiteral("compositing"), QString::number(mode)); QScopedPointer service(m_model->m_tractor->field()); QScopedPointerfield(m_model->m_tractor->field()); field->lock(); while ((service != nullptr) && service->is_valid()) { if (service->type() == transition_type) { Mlt::Transition t((mlt_transition)service->get_service()); service.reset(service->producer()); QString serviceName = t.get("mlt_service"); if (t.get_int("internal_added") == 237 && serviceName != QLatin1String("mix")) { // remove all compositing transitions field->disconnect_service(t); t.disconnect_all_producers(); } } else { service.reset(service->producer()); } } if (mode > 0) { const QString compositeGeometry = QStringLiteral("0=0/0:%1x%2").arg(m_model->m_tractor->profile()->width()).arg(m_model->m_tractor->profile()->height()); // Loop through tracks for (int track = 0; track < m_model->getTracksCount(); track++) { if (m_model->getTrackById(m_model->getTrackIndexFromPosition(track))->getProperty("kdenlive:audio_track").toInt() == 0) { // This is a video track Mlt::Transition t(*m_model->m_tractor->profile(), mode == 1 ? "composite" : TransitionsRepository::get()->getCompositingTransition().toUtf8().constData()); t.set("always_active", 1); t.set_tracks(0, track + 1); if (mode == 1) { t.set("valign", "middle"); t.set("halign", "centre"); t.set("fill", 1); t.set("aligned", 0); t.set("geometry", compositeGeometry.toUtf8().constData()); } t.set("internal_added", 237); field->plant_transition(t, 0, track + 1); } } } field->unlock(); pCore->requestMonitorRefresh(); } void TimelineController::extractZone(QPoint zone, bool liftOnly) { QVector tracks; auto it = m_model->m_allTracks.cbegin(); while (it != m_model->m_allTracks.cend()) { int target_track = (*it)->getId(); if (m_model->getTrackById_const(target_track)->shouldReceiveTimelineOp()) { tracks << target_track; } ++it; } if (tracks.isEmpty()) { pCore->displayMessage(i18n("Please activate a track for this operation by clicking on its label"), InformationMessage); } if (m_zone == QPoint()) { // Use current timeline position and clip zone length zone.setY(pCore->getTimelinePosition() + zone.y() - zone.x()); zone.setX(pCore->getTimelinePosition()); } TimelineFunctions::extractZone(m_model, tracks, m_zone == QPoint() ? zone : m_zone, liftOnly); } void TimelineController::extract(int clipId) { if (clipId == -1) { clipId = m_root->property("mainItemId").toInt(); } int in = m_model->getClipPosition(clipId); int out = in + m_model->getClipPlaytime(clipId); QVector tracks; tracks << m_model->getClipTrackId(clipId); if (m_model->m_groups->isInGroup(clipId)) { int targetRoot = m_model->m_groups->getRootId(clipId); if (m_model->isGroup(targetRoot)) { std::unordered_set sub = m_model->m_groups->getLeaves(targetRoot); for (int current_id : sub) { if (current_id == clipId) { continue; } if (m_model->isClip(current_id)) { int newIn = m_model->getClipPosition(current_id); int tk = m_model->getClipTrackId(current_id); in = qMin(in, newIn); out = qMax(out, newIn + m_model->getClipPlaytime(current_id)); if (!tracks.contains(tk)) { tracks << tk; } } } } } TimelineFunctions::extractZone(m_model, tracks, QPoint(in, out), false); } void TimelineController::saveZone(int clipId) { if (clipId == -1) { clipId = m_root->property("mainItemId").toInt(); } int in = m_model->getClipIn(clipId); int out = in + m_model->getClipPlaytime(clipId); QString id; pCore->projectItemModel()->requestAddBinSubClip(id, in, out, {}, m_model->m_allClips[clipId]->binId()); } bool TimelineController::insertClipZone(const QString &binId, int tid, int position) { QStringList binIdData = binId.split(QLatin1Char('/')); int in = 0; int out = -1; if (binIdData.size() >= 3) { in = binIdData.at(1).toInt(); out = binIdData.at(2).toInt(); } QString bid = binIdData.first(); // dropType indicates if we want a normal drop (disabled), audio only or video only drop PlaylistState::ClipState dropType = PlaylistState::Disabled; if (bid.startsWith(QLatin1Char('A'))) { dropType = PlaylistState::AudioOnly; bid = bid.remove(0, 1); } else if (bid.startsWith(QLatin1Char('V'))) { dropType = PlaylistState::VideoOnly; bid = bid.remove(0, 1); } int aTrack = -1; int vTrack = -1; std::shared_ptr clip = pCore->bin()->getBinClip(bid); if (out <= in) { out = (int)clip->frameDuration() - 1; } if (dropType == PlaylistState::VideoOnly) { vTrack = tid; } else if (dropType == PlaylistState::AudioOnly) { aTrack = tid; } else { if (m_model->getTrackById_const(tid)->isAudioTrack()) { aTrack = tid; vTrack = clip->hasAudioAndVideo() ? m_model->getMirrorVideoTrackId(aTrack) : -1; } else { vTrack = tid; aTrack = clip->hasAudioAndVideo() ? m_model->getMirrorAudioTrackId(vTrack) : -1; } } QList target_tracks; if (vTrack > -1) { target_tracks << vTrack; } if (aTrack > -1) { target_tracks << aTrack; } std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool overwrite = m_model->m_editMode == TimelineMode::OverwriteEdit; QPoint zone(in, out + 1); bool res = TimelineFunctions::insertZone(m_model, target_tracks, binId, position, zone, overwrite, false, undo, redo); if (res) { int newPos = position + (zone.y() - zone.x()); int currentPos = pCore->getTimelinePosition(); Fun redoPos = [this, newPos]() { Kdenlive::MonitorId activeMonitor = pCore->monitorManager()->activeMonitor()->id(); pCore->monitorManager()->activateMonitor(Kdenlive::ProjectMonitor); pCore->monitorManager()->refreshProjectMonitor(); setPosition(newPos); pCore->monitorManager()->activateMonitor(activeMonitor); return true; }; Fun undoPos = [this, currentPos]() { Kdenlive::MonitorId activeMonitor = pCore->monitorManager()->activeMonitor()->id(); pCore->monitorManager()->activateMonitor(Kdenlive::ProjectMonitor); pCore->monitorManager()->refreshProjectMonitor(); setPosition(currentPos); pCore->monitorManager()->activateMonitor(activeMonitor); return true; }; redoPos(); UPDATE_UNDO_REDO_NOLOCK(redoPos, undoPos, undo, redo); pCore->pushUndo(undo, redo, overwrite ? i18n("Overwrite zone") : i18n("Insert zone")); } else { pCore->displayMessage(i18n("Could not insert zone"), InformationMessage); undo(); } return res; } int TimelineController::insertZone(const QString &binId, QPoint zone, bool overwrite) { std::shared_ptr clip = pCore->bin()->getBinClip(binId); int aTrack = -1; int vTrack = -1; if (clip->hasAudio()) { aTrack = m_model->m_audioTarget.firstKey(); } if (clip->hasVideo()) { vTrack = videoTarget(); } /*if (aTrack == -1 && vTrack == -1) { // No target tracks defined, use active track if (m_model->getTrackById_const(m_activeTrack)->isAudioTrack()) { aTrack = m_activeTrack; vTrack = m_model->getMirrorVideoTrackId(aTrack); } else { vTrack = m_activeTrack; aTrack = m_model->getMirrorAudioTrackId(vTrack); } }*/ int insertPoint; QPoint sourceZone; if (useRuler() && m_zone != QPoint()) { // We want to use timeline zone for in/out insert points insertPoint = m_zone.x(); sourceZone = QPoint(zone.x(), zone.x() + m_zone.y() - m_zone.x()); } else { // Use current timeline pos and clip zone for in/out insertPoint = pCore->getTimelinePosition(); sourceZone = zone; } QList target_tracks; if (vTrack > -1) { target_tracks << vTrack; } if (aTrack > -1) { target_tracks << aTrack; } if (target_tracks.isEmpty()) { pCore->displayMessage(i18n("Please select a target track by clicking on a track's target zone"), InformationMessage); return -1; } std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool res = TimelineFunctions::insertZone(m_model, target_tracks, binId, insertPoint, sourceZone, overwrite, true, undo, redo); if (res) { int newPos = insertPoint + (sourceZone.y() - sourceZone.x()); int currentPos = pCore->getTimelinePosition(); Fun redoPos = [this, newPos]() { Kdenlive::MonitorId activeMonitor = pCore->monitorManager()->activeMonitor()->id(); pCore->monitorManager()->activateMonitor(Kdenlive::ProjectMonitor); pCore->monitorManager()->refreshProjectMonitor(); setPosition(newPos); pCore->monitorManager()->activateMonitor(activeMonitor); return true; }; Fun undoPos = [this, currentPos]() { Kdenlive::MonitorId activeMonitor = pCore->monitorManager()->activeMonitor()->id(); pCore->monitorManager()->activateMonitor(Kdenlive::ProjectMonitor); pCore->monitorManager()->refreshProjectMonitor(); setPosition(currentPos); pCore->monitorManager()->activateMonitor(activeMonitor); return true; }; redoPos(); UPDATE_UNDO_REDO_NOLOCK(redoPos, undoPos, undo, redo); pCore->pushUndo(undo, redo, overwrite ? i18n("Overwrite zone") : i18n("Insert zone")); } else { pCore->displayMessage(i18n("Could not insert zone"), InformationMessage); undo(); } return res; } void TimelineController::updateClip(int clipId, const QVector &roles) { QModelIndex ix = m_model->makeClipIndexFromID(clipId); if (ix.isValid()) { m_model->dataChanged(ix, ix, roles); } } void TimelineController::showClipKeyframes(int clipId, bool value) { TimelineFunctions::showClipKeyframes(m_model, clipId, value); } void TimelineController::showCompositionKeyframes(int clipId, bool value) { TimelineFunctions::showCompositionKeyframes(m_model, clipId, value); } void TimelineController::switchEnableState(std::unordered_set selection) { if (selection.empty()) { selection = m_model->getCurrentSelection(); //clipId = getMainSelectedItem(false, false); } if (selection.empty()) { return; } TimelineFunctions::switchEnableState(m_model, selection); } void TimelineController::addCompositionToClip(const QString &assetId, int clipId, int offset) { if (clipId == -1) { clipId = m_root->property("mainItemId").toInt(); } if (offset == -1) { offset = m_root->property("mainFrame").toInt(); } int track = clipId > -1 ? m_model->getClipTrackId(clipId) : m_activeTrack; int compoId = -1; if (assetId.isEmpty()) { QStringList compositions = KdenliveSettings::favorite_transitions(); if (compositions.isEmpty()) { pCore->displayMessage(i18n("Select a favorite composition"), InformationMessage, 500); return; } compoId = insertNewComposition(track, clipId, offset, compositions.first(), true); } else { compoId = insertNewComposition(track, clipId, offset, assetId, true); } if (compoId > 0) { m_model->requestSetSelection({compoId}); } } void TimelineController::addEffectToClip(const QString &assetId, int clipId) { if (clipId == -1) { clipId = m_root->property("mainItemId").toInt(); } qDebug() << "/// ADDING ASSET: " << assetId; m_model->addClipEffect(clipId, assetId); } bool TimelineController::splitAV() { int cid = *m_model->getCurrentSelection().begin(); if (m_model->isClip(cid)) { std::shared_ptr clip = m_model->getClipPtr(cid); if (clip->clipState() == PlaylistState::AudioOnly) { return TimelineFunctions::requestSplitVideo(m_model, cid, videoTarget()); } else { return TimelineFunctions::requestSplitAudio(m_model, cid, m_model->m_audioTarget.firstKey()); } } pCore->displayMessage(i18n("No clip found to perform AV split operation"), InformationMessage, 500); return false; } void TimelineController::splitAudio(int clipId) { TimelineFunctions::requestSplitAudio(m_model, clipId, m_model->m_audioTarget.firstKey()); } void TimelineController::splitVideo(int clipId) { TimelineFunctions::requestSplitVideo(m_model, clipId, videoTarget()); } void TimelineController::setAudioRef(int clipId) { if (clipId == -1) { clipId = m_root->property("mainItemId").toInt(); } m_audioRef = clipId; std::unique_ptr envelope(new AudioEnvelope(getClipBinId(clipId), clipId)); m_audioCorrelator.reset(new AudioCorrelation(std::move(envelope))); connect(m_audioCorrelator.get(), &AudioCorrelation::gotAudioAlignData, [&](int cid, int shift) { int pos = m_model->getClipPosition(m_audioRef) + shift - m_model->getClipIn(m_audioRef); bool result = m_model->requestClipMove(cid, m_model->getClipTrackId(cid), pos, true, true, true); if (!result) { pCore->displayMessage(i18n("Cannot move clip to frame %1.", (pos + shift)), InformationMessage, 500); } }); connect(m_audioCorrelator.get(), &AudioCorrelation::displayMessage, pCore.get(), &Core::displayMessage); } void TimelineController::alignAudio(int clipId) { // find other clip if (clipId == -1) { clipId = m_root->property("mainItemId").toInt(); } if (m_audioRef == -1 || m_audioRef == clipId) { pCore->displayMessage(i18n("Set audio reference before attempting to align"), InformationMessage, 500); return; } const QString masterBinClipId = getClipBinId(m_audioRef); std::unordered_set clipsToAnalyse; if (m_model->m_groups->isInGroup(clipId)) { clipsToAnalyse = m_model->getGroupElements(clipId); m_model->requestClearSelection(); } else { clipsToAnalyse.insert(clipId); } QList processedGroups; int processed = 0; for (int cid : clipsToAnalyse) { if (!m_model->isClip(cid) || cid == m_audioRef) { continue; } const QString otherBinId = getClipBinId(cid); if (m_model->m_groups->isInGroup(cid)) { int parentGroup = m_model->m_groups->getRootId(cid); if (processedGroups.contains(parentGroup)) { continue; } // Only process one clip from the group std::shared_ptr clip = pCore->bin()->getBinClip(otherBinId); if (clip->hasAudio()) { processedGroups << parentGroup; } else { continue; } } if (!pCore->bin()->getBinClip(otherBinId)->hasAudio()) { // Cannot process non audi clips continue; } if (otherBinId == masterBinClipId) { // easy, same clip. int newPos = m_model->getClipPosition(m_audioRef) - m_model->getClipIn(m_audioRef) + m_model->getClipIn(cid); if (newPos) { bool result = m_model->requestClipMove(cid, m_model->getClipTrackId(cid), newPos, true, true, true); processed ++; if (!result) { pCore->displayMessage(i18n("Cannot move clip to frame %1.", newPos), InformationMessage, 500); } continue; } } processed ++; // Perform audio calculation AudioEnvelope *envelope = new AudioEnvelope(otherBinId, cid, (size_t)m_model->getClipIn(cid), (size_t)m_model->getClipPlaytime(cid), (size_t)m_model->getClipPosition(cid)); m_audioCorrelator->addChild(envelope); } if (processed == 0) { //TODO: improve feedback message after freeze pCore->displayMessage(i18n("Select a clip to apply an effect"), InformationMessage, 500); } } void TimelineController::switchTrackActive(int trackId) { if (trackId == -1) { trackId = m_activeTrack; } bool active = m_model->getTrackById_const(trackId)->isTimelineActive(); m_model->setTrackProperty(trackId, QStringLiteral("kdenlive:timeline_active"), active ? QStringLiteral("0") : QStringLiteral("1")); } void TimelineController::switchAllTrackActive() { auto it = m_model->m_allTracks.cbegin(); while (it != m_model->m_allTracks.cend()) { bool active = (*it)->isTimelineActive(); int target_track = (*it)->getId(); m_model->setTrackProperty(target_track, QStringLiteral("kdenlive:timeline_active"), active ? QStringLiteral("0") : QStringLiteral("1")); ++it; } } void TimelineController::makeAllTrackActive() { // Check current status auto it = m_model->m_allTracks.cbegin(); bool makeActive = false; while (it != m_model->m_allTracks.cend()) { if (!(*it)->isTimelineActive()) { // There is an inactive track, activate all makeActive = true; break; } ++it; } it = m_model->m_allTracks.cbegin(); while (it != m_model->m_allTracks.cend()) { int target_track = (*it)->getId(); m_model->setTrackProperty(target_track, QStringLiteral("kdenlive:timeline_active"), makeActive ? QStringLiteral("1") : QStringLiteral("0")); ++it; } } void TimelineController::switchTrackLock(bool applyToAll) { if (!applyToAll) { // apply to active track only bool locked = m_model->getTrackById_const(m_activeTrack)->isLocked(); m_model->setTrackLockedState(m_activeTrack, !locked); } else { // Invert track lock const auto ids = m_model->getAllTracksIds(); // count the number of tracks to be locked int toBeLockedCount = std::accumulate(ids.begin(), ids.end(), 0, [this](int s, int id) { return s + (m_model->getTrackById_const(id)->isLocked() ? 0 : 1); }); bool leaveOneUnlocked = toBeLockedCount == m_model->getTracksCount(); for (const int id : ids) { // leave active track unlocked if (leaveOneUnlocked && id == m_activeTrack) { continue; } bool isLocked = m_model->getTrackById_const(id)->isLocked(); m_model->setTrackLockedState(id, !isLocked); } } } void TimelineController::switchTargetTrack() { bool isAudio = m_model->getTrackById_const(m_activeTrack)->getProperty("kdenlive:audio_track").toInt() == 1; if (isAudio) { QMap current = m_model->m_audioTarget; if (current.contains(m_activeTrack)) { current.remove(m_activeTrack); } else { int ix = getFirstUnassignedStream(); if (ix > -1) { current.insert(m_activeTrack, ix); } } setAudioTarget(current); } else { setVideoTarget(videoTarget() == m_activeTrack ? -1 : m_activeTrack); } } QVariantList TimelineController::audioTarget() const { QVariantList audioTracks; QMapIterator i(m_model->m_audioTarget); while (i.hasNext()) { i.next(); audioTracks << i.key(); } return audioTracks; } QVariantList TimelineController::lastAudioTarget() const { QVariantList audioTracks; QMapIterator i(m_lastAudioTarget); while (i.hasNext()) { i.next(); audioTracks << i.key(); } return audioTracks; } const QString TimelineController::audioTargetName(int tid) const { if (m_model->m_audioTarget.contains(tid) && m_model->m_binAudioTargets.size() > 1) { int streamIndex = m_model->m_audioTarget.value(tid); if (m_model->m_binAudioTargets.contains(streamIndex)) { QString targetName = m_model->m_binAudioTargets.value(streamIndex); return targetName.isEmpty() ? QChar('x') : targetName.at(0); } else { qDebug()<<"STREAM INDEX NOT IN TARGET : "<m_binAudioTargets; } } else { qDebug()<<"TRACK NOT IN TARGET : "<m_audioTarget.keys(); } return QString(); } int TimelineController::videoTarget() const { return m_model->m_videoTarget; } int TimelineController::hasAudioTarget() const { return m_hasAudioTarget; } bool TimelineController::hasVideoTarget() const { return m_hasVideoTarget; } bool TimelineController::autoScroll() const { return KdenliveSettings::autoscroll(); } void TimelineController::resetTrackHeight() { int tracksCount = m_model->getTracksCount(); for (int track = tracksCount - 1; track >= 0; track--) { int trackIx = m_model->getTrackIndexFromPosition(track); m_model->getTrackById(trackIx)->setProperty(QStringLiteral("kdenlive:trackheight"), QString::number(KdenliveSettings::trackheight())); } QModelIndex modelStart = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(0)); QModelIndex modelEnd = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(tracksCount - 1)); m_model->dataChanged(modelStart, modelEnd, {TimelineModel::HeightRole}); } void TimelineController::selectAll() { std::unordered_set ids; for (auto clp : m_model->m_allClips) { ids.insert(clp.first); } for (auto clp : m_model->m_allCompositions) { ids.insert(clp.first); } m_model->requestSetSelection(ids); } void TimelineController::selectCurrentTrack() { std::unordered_set ids; for (auto clp : m_model->getTrackById_const(m_activeTrack)->m_allClips) { ids.insert(clp.first); } for (auto clp : m_model->getTrackById_const(m_activeTrack)->m_allCompositions) { ids.insert(clp.first); } m_model->requestSetSelection(ids); } void TimelineController::pasteEffects(int targetId) { std::unordered_set targetIds; if (targetId == -1) { std::unordered_set sel = m_model->getCurrentSelection(); if (sel.empty()) { pCore->displayMessage(i18n("No clip selected"), InformationMessage, 500); } for (int s : sel) { if (m_model->isGroup(s)) { std::unordered_set sub = m_model->m_groups->getLeaves(s); for (int current_id : sub) { if (m_model->isClip(current_id)) { targetIds.insert(current_id); } } } else if (m_model->isClip(s)) { targetIds.insert(s); } } } else { if (m_model->m_groups->isInGroup(targetId)) { targetId = m_model->m_groups->getRootId(targetId); } if (m_model->isGroup(targetId)) { std::unordered_set sub = m_model->m_groups->getLeaves(targetId); for (int current_id : sub) { if (m_model->isClip(current_id)) { targetIds.insert(current_id); } } } else if (m_model->isClip(targetId)) { targetIds.insert(targetId); } } if (targetIds.empty()) { pCore->displayMessage(i18n("No clip selected"), InformationMessage, 500); } QClipboard *clipboard = QApplication::clipboard(); QString txt = clipboard->text(); if (txt.isEmpty()) { pCore->displayMessage(i18n("No information in clipboard"), InformationMessage, 500); return; } QDomDocument copiedItems; copiedItems.setContent(txt); if (copiedItems.documentElement().tagName() != QLatin1String("kdenlive-scene")) { pCore->displayMessage(i18n("No information in clipboard"), InformationMessage, 500); return; } QDomNodeList clips = copiedItems.documentElement().elementsByTagName(QStringLiteral("clip")); if (clips.isEmpty()) { pCore->displayMessage(i18n("No information in clipboard"), InformationMessage, 500); return; } std::function undo = []() { return true; }; std::function redo = []() { return true; }; QDomElement effects = clips.at(0).firstChildElement(QStringLiteral("effects")); effects.setAttribute(QStringLiteral("parentIn"), clips.at(0).toElement().attribute(QStringLiteral("in"))); for (int i = 1; i < clips.size(); i++) { QDomElement subeffects = clips.at(i).firstChildElement(QStringLiteral("effects")); QDomNodeList subs = subeffects.childNodes(); while (!subs.isEmpty()) { subs.at(0).toElement().setAttribute(QStringLiteral("parentIn"), clips.at(i).toElement().attribute(QStringLiteral("in"))); effects.appendChild(subs.at(0)); } } int insertedEffects = 0; for (int target : targetIds) { std::shared_ptr destStack = m_model->getClipEffectStackModel(target); if (destStack->fromXml(effects, undo, redo)) { insertedEffects++; } } if (insertedEffects > 0) { pCore->pushUndo(undo, redo, i18n("Paste effects")); } else { pCore->displayMessage(i18n("Cannot paste effect on selected clip"), InformationMessage, 500); undo(); } } double TimelineController::fps() const { return pCore->getCurrentFps(); } void TimelineController::editItemDuration(int id) { if (id == -1) { id = m_root->property("mainItemId").toInt(); //getMainSelectedItem(false, true); } if (id == -1 || !m_model->isItem(id)) { pCore->displayMessage(i18n("No item to edit"), InformationMessage, 500); return; } int start = m_model->getItemPosition(id); int in = 0; int duration = m_model->getItemPlaytime(id); int maxLength = -1; bool isComposition = false; if (m_model->isClip(id)) { in = m_model->getClipIn(id); std::shared_ptr clip = pCore->bin()->getBinClip(getClipBinId(id)); if (clip && clip->hasLimitedDuration()) { maxLength = clip->getProducerDuration(); } } else if (m_model->isComposition(id)) { // nothing to do isComposition = true; } else { pCore->displayMessage(i18n("No item to edit"), InformationMessage, 500); return; } int trackId = m_model->getItemTrackId(id); int maxFrame = qMax(0, start + duration + (isComposition ? m_model->getTrackById(trackId)->getBlankSizeNearComposition(id, true) : m_model->getTrackById(trackId)->getBlankSizeNearClip(id, true))); int minFrame = qMax(0, in - (isComposition ? m_model->getTrackById(trackId)->getBlankSizeNearComposition(id, false) : m_model->getTrackById(trackId)->getBlankSizeNearClip(id, false))); int partner = isComposition ? -1 : m_model->getClipSplitPartner(id); QPointer dialog = new ClipDurationDialog(id, pCore->currentDoc()->timecode(), start, minFrame, in, in + duration, maxLength, maxFrame, qApp->activeWindow()); if (dialog->exec() == QDialog::Accepted) { std::function undo = []() { return true; }; std::function redo = []() { return true; }; int newPos = dialog->startPos().frames(pCore->getCurrentFps()); int newIn = dialog->cropStart().frames(pCore->getCurrentFps()); int newDuration = dialog->duration().frames(pCore->getCurrentFps()); bool result = true; if (newPos < start) { if (!isComposition) { result = m_model->requestClipMove(id, trackId, newPos, true, true, true, true, undo, redo); if (result && partner > -1) { result = m_model->requestClipMove(partner, m_model->getItemTrackId(partner), newPos, true, true, true, true, undo, redo); } } else { result = m_model->requestCompositionMove(id, trackId, m_model->m_allCompositions[id]->getForcedTrack(), newPos, true, true, undo, redo); } if (result && newIn != in) { m_model->requestItemResize(id, duration + (in - newIn), false, true, undo, redo); if (result && partner > -1) { result = m_model->requestItemResize(partner, duration + (in - newIn), false, true, undo, redo); } } if (newDuration != duration + (in - newIn)) { result = result && m_model->requestItemResize(id, newDuration, true, true, undo, redo); if (result && partner > -1) { result = m_model->requestItemResize(partner, newDuration, false, true, undo, redo); } } } else { // perform resize first if (newIn != in) { result = m_model->requestItemResize(id, duration + (in - newIn), false, true, undo, redo); if (result && partner > -1) { result = m_model->requestItemResize(partner, duration + (in - newIn), false, true, undo, redo); } } if (newDuration != duration + (in - newIn)) { result = result && m_model->requestItemResize(id, newDuration, start == newPos, true, undo, redo); if (result && partner > -1) { result = m_model->requestItemResize(partner, newDuration, start == newPos, true, undo, redo); } } if (start != newPos || newIn != in) { if (!isComposition) { result = result && m_model->requestClipMove(id, trackId, newPos, true, true, true, true, undo, redo); if (result && partner > -1) { result = m_model->requestClipMove(partner, m_model->getItemTrackId(partner), newPos, true, true, true, true, undo, redo); } } else { result = result && m_model->requestCompositionMove(id, trackId, m_model->m_allCompositions[id]->getForcedTrack(), newPos, true, true, undo, redo); } } } if (result) { pCore->pushUndo(undo, redo, i18n("Edit item")); } else { undo(); } } } void TimelineController::updateClipActions() { if (m_model->getCurrentSelection().empty()) { for (QAction *act : clipActions) { act->setEnabled(false); } emit timelineClipSelected(false); // nothing selected emit showItemEffectStack(QString(), nullptr, QSize(), false); return; } std::shared_ptr clip(nullptr); int item = *m_model->getCurrentSelection().begin(); if (m_model->getCurrentSelection().size() == 1 && (m_model->isClip(item) || m_model->isComposition(item))) { showAsset(item); } if (m_model->isClip(item)) { clip = m_model->getClipPtr(item); } bool enablePositionActions = positionIsInItem(item); for (QAction *act : clipActions) { bool enableAction = true; const QChar actionData = act->data().toChar(); if (actionData == QLatin1Char('G')) { enableAction = isInSelection(item) && m_model->getCurrentSelection().size() > 1; } else if (actionData == QLatin1Char('U')) { enableAction = m_model->m_groups->isInGroup(item); } else if (actionData == QLatin1Char('A')) { enableAction = clip && clip->clipState() == PlaylistState::AudioOnly; } else if (actionData == QLatin1Char('V')) { enableAction = clip && clip->clipState() == PlaylistState::VideoOnly; } else if (actionData == QLatin1Char('D')) { enableAction = clip && clip->clipState() == PlaylistState::Disabled; } else if (actionData == QLatin1Char('E')) { enableAction = clip && clip->clipState() != PlaylistState::Disabled; } else if (actionData == QLatin1Char('X') || actionData == QLatin1Char('S')) { enableAction = clip && clip->canBeVideo() && clip->canBeAudio(); if (enableAction && actionData == QLatin1Char('S')) { act->setText(clip->clipState() == PlaylistState::AudioOnly ? i18n("Split video") : i18n("Split audio")); } } else if (actionData == QLatin1Char('W')) { enableAction = clip != nullptr; if (enableAction) { act->setText(clip->clipState() == PlaylistState::Disabled ? i18n("Enable clip") : i18n("Disable clip")); } } else if (actionData == QLatin1Char('C') && clip == nullptr) { enableAction = false; } else if (actionData == QLatin1Char('P')) { // Position actions should stay enabled in clip monitor //enableAction = enablePositionActions; } act->setEnabled(enableAction); } emit timelineClipSelected(clip != nullptr); } const QString TimelineController::getAssetName(const QString &assetId, bool isTransition) { return isTransition ? TransitionsRepository::get()->getName(assetId) : EffectsRepository::get()->getName(assetId); } void TimelineController::grabCurrent() { std::unordered_set ids = m_model->getCurrentSelection(); std::unordered_set items_list; int mainId = -1; for (int i : ids) { if (m_model->isGroup(i)) { std::unordered_set children = m_model->m_groups->getLeaves(i); items_list.insert(children.begin(), children.end()); } else { items_list.insert(i); } } for (int id : items_list) { if (mainId == -1 && m_model->getItemTrackId(id) == m_activeTrack) { mainId = id; continue; } if (m_model->isClip(id)) { std::shared_ptr clip = m_model->getClipPtr(id); clip->setGrab(!clip->isGrabbed()); } else if (m_model->isComposition(id)) { std::shared_ptr clip = m_model->getCompositionPtr(id); clip->setGrab(!clip->isGrabbed()); } } if (mainId > -1) { if (m_model->isClip(mainId)) { std::shared_ptr clip = m_model->getClipPtr(mainId); clip->setGrab(!clip->isGrabbed()); } else if (m_model->isComposition(mainId)) { std::shared_ptr clip = m_model->getCompositionPtr(mainId); clip->setGrab(!clip->isGrabbed()); } } } int TimelineController::getItemMovingTrack(int itemId) const { if (m_model->isClip(itemId)) { int trackId = -1; if (m_model->m_editMode != TimelineMode::NormalEdit) { trackId = m_model->m_allClips[itemId]->getFakeTrackId(); } return trackId < 0 ? m_model->m_allClips[itemId]->getCurrentTrackId() : trackId; } return m_model->m_allCompositions[itemId]->getCurrentTrackId(); } bool TimelineController::endFakeMove(int clipId, int position, bool updateView, bool logUndo, bool invalidateTimeline) { Q_ASSERT(m_model->m_allClips.count(clipId) > 0); int trackId = m_model->m_allClips[clipId]->getFakeTrackId(); if (m_model->getClipPosition(clipId) == position && m_model->getClipTrackId(clipId) == trackId) { qDebug() << "* * ** END FAKE; NO MOVE RQSTED"; return true; } if (m_model->m_groups->isInGroup(clipId)) { // element is in a group. int groupId = m_model->m_groups->getRootId(clipId); int current_trackId = m_model->getClipTrackId(clipId); int track_pos1 = m_model->getTrackPosition(trackId); int track_pos2 = m_model->getTrackPosition(current_trackId); int delta_track = track_pos1 - track_pos2; int delta_pos = position - m_model->m_allClips[clipId]->getPosition(); return endFakeGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo); } qDebug() << "//////\n//////\nENDING FAKE MNOVE: " << trackId << ", POS: " << position; std::function undo = []() { return true; }; std::function redo = []() { return true; }; int duration = m_model->getClipPlaytime(clipId); int currentTrack = m_model->m_allClips[clipId]->getCurrentTrackId(); bool res = true; if (currentTrack > -1) { res = res && m_model->getTrackById(currentTrack)->requestClipDeletion(clipId, updateView, invalidateTimeline, undo, redo, false, false); } if (m_model->m_editMode == TimelineMode::OverwriteEdit) { res = res && TimelineFunctions::liftZone(m_model, trackId, QPoint(position, position + duration), undo, redo); } else if (m_model->m_editMode == TimelineMode::InsertEdit) { int startClipId = m_model->getClipByPosition(trackId, position); if (startClipId > -1) { // There is a clip, cut res = res && TimelineFunctions::requestClipCut(m_model, startClipId, position, undo, redo); } res = res && TimelineFunctions::requestInsertSpace(m_model, QPoint(position, position + duration), undo, redo); } res = res && m_model->getTrackById(trackId)->requestClipInsertion(clipId, position, updateView, invalidateTimeline, undo, redo); if (res) { // Terminate fake move if (m_model->isClip(clipId)) { m_model->m_allClips[clipId]->setFakeTrackId(-1); } if (logUndo) { pCore->pushUndo(undo, redo, i18n("Move item")); } } else { qDebug() << "//// FAKE FAILED"; undo(); } return res; } bool TimelineController::endFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool logUndo) { std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool res = endFakeGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo, undo, redo); if (res && logUndo) { // Terminate fake move if (m_model->isClip(clipId)) { m_model->m_allClips[clipId]->setFakeTrackId(-1); } pCore->pushUndo(undo, redo, i18n("Move group")); } return res; } bool TimelineController::endFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool finalMove, Fun &undo, Fun &redo) { Q_ASSERT(m_model->m_allGroups.count(groupId) > 0); bool ok = true; auto all_items = m_model->m_groups->getLeaves(groupId); Q_ASSERT(all_items.size() > 1); Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; // Sort clips. We need to delete from right to left to avoid confusing the view std::vector sorted_clips{std::make_move_iterator(std::begin(all_items)), std::make_move_iterator(std::end(all_items))}; std::sort(sorted_clips.begin(), sorted_clips.end(), [this](const int &clipId1, const int &clipId2) { int p1 = m_model->isClip(clipId1) ? m_model->m_allClips[clipId1]->getPosition() : m_model->m_allCompositions[clipId1]->getPosition(); int p2 = m_model->isClip(clipId2) ? m_model->m_allClips[clipId2]->getPosition() : m_model->m_allCompositions[clipId2]->getPosition(); return p2 < p1; }); // Moving groups is a two stage process: first we remove the clips from the tracks, and then try to insert them back at their calculated new positions. // This way, we ensure that no conflict will arise with clips inside the group being moved // First, remove clips int audio_delta, video_delta; audio_delta = video_delta = delta_track; int master_trackId = m_model->getItemTrackId(clipId); if (m_model->getTrackById_const(master_trackId)->isAudioTrack()) { // Master clip is audio, so reverse delta for video clips video_delta = -delta_track; } else { audio_delta = -delta_track; } int min = -1; int max = -1; std::unordered_map old_track_ids, old_position, old_forced_track, new_track_ids; for (int item : sorted_clips) { int old_trackId = m_model->getItemTrackId(item); old_track_ids[item] = old_trackId; if (old_trackId != -1) { bool updateThisView = true; if (m_model->isClip(item)) { int current_track_position = m_model->getTrackPosition(old_trackId); int d = m_model->getTrackById_const(old_trackId)->isAudioTrack() ? audio_delta : video_delta; int target_track_position = current_track_position + d; auto it = m_model->m_allTracks.cbegin(); std::advance(it, target_track_position); int target_track = (*it)->getId(); new_track_ids[item] = target_track; old_position[item] = m_model->m_allClips[item]->getPosition(); int duration = m_model->m_allClips[item]->getPlaytime(); min = min < 0 ? old_position[item] + delta_pos : qMin(min, old_position[item] + delta_pos); max = max < 0 ? old_position[item] + delta_pos + duration : qMax(max, old_position[item] + delta_pos + duration); ok = ok && m_model->getTrackById(old_trackId)->requestClipDeletion(item, updateThisView, finalMove, undo, redo, false, false); } else { // ok = ok && getTrackById(old_trackId)->requestCompositionDeletion(item, updateThisView, local_undo, local_redo); old_position[item] = m_model->m_allCompositions[item]->getPosition(); old_forced_track[item] = m_model->m_allCompositions[item]->getForcedTrack(); } if (!ok) { bool undone = undo(); Q_ASSERT(undone); return false; } } } bool res = true; if (m_model->m_editMode == TimelineMode::OverwriteEdit) { for (int item : sorted_clips) { if (m_model->isClip(item) && new_track_ids.count(item) > 0) { int target_track = new_track_ids[item]; int target_position = old_position[item] + delta_pos; int duration = m_model->m_allClips[item]->getPlaytime(); res = res && TimelineFunctions::liftZone(m_model, target_track, QPoint(target_position, target_position + duration), undo, redo); } } } else if (m_model->m_editMode == TimelineMode::InsertEdit) { QList processedTracks; for (int item : sorted_clips) { int target_track = new_track_ids[item]; if (processedTracks.contains(target_track)) { // already processed continue; } processedTracks << target_track; int target_position = min; int startClipId = m_model->getClipByPosition(target_track, target_position); if (startClipId > -1) { // There is a clip, cut res = res && TimelineFunctions::requestClipCut(m_model, startClipId, target_position, undo, redo); } } res = res && TimelineFunctions::requestInsertSpace(m_model, QPoint(min, max), undo, redo); } for (int item : sorted_clips) { if (m_model->isClip(item)) { int target_track = new_track_ids[item]; int target_position = old_position[item] + delta_pos; ok = ok && m_model->requestClipMove(item, target_track, target_position, true, updateView, finalMove, finalMove, undo, redo); } else { // ok = ok && requestCompositionMove(item, target_track, old_forced_track[item], target_position, updateThisView, local_undo, local_redo); } if (!ok) { bool undone = undo(); Q_ASSERT(undone); return false; } } return true; } QStringList TimelineController::getThumbKeys() { QStringList result; for (const auto &clp : m_model->m_allClips) { const QString binId = getClipBinId(clp.first); std::shared_ptr binClip = pCore->bin()->getBinClip(binId); result << binClip->hash() + QLatin1Char('#') + QString::number(clp.second->getIn()) + QStringLiteral(".png"); result << binClip->hash() + QLatin1Char('#') + QString::number(clp.second->getOut()) + QStringLiteral(".png"); } result.removeDuplicates(); return result; } bool TimelineController::isInSelection(int itemId) { return m_model->getCurrentSelection().count(itemId) > 0; } bool TimelineController::exists(int itemId) { return m_model->isClip(itemId) || m_model->isComposition(itemId); } void TimelineController::slotMultitrackView(bool enable, bool refresh) { QStringList trackNames = TimelineFunctions::enableMultitrackView(m_model, enable, refresh); pCore->monitorManager()->projectMonitor()->slotShowEffectScene(enable ? MonitorSplitTrack : MonitorSceneNone, false, QVariant(trackNames)); QObject::disconnect( m_connection ); if (enable) { connect(m_model.get(), &TimelineItemModel::trackVisibilityChanged, this, &TimelineController::updateMultiTrack, Qt::UniqueConnection); m_connection = connect(this, &TimelineController::activeTrackChanged, [this]() { int ix = 0; auto it = m_model->m_allTracks.cbegin(); while (it != m_model->m_allTracks.cend()) { int target_track = (*it)->getId(); ++it; if (target_track == m_activeTrack) { break; } if (m_model->getTrackById_const(target_track)->isAudioTrack() || m_model->getTrackById_const(target_track)->isHidden()) { continue; } ++ix; } pCore->monitorManager()->projectMonitor()->updateMultiTrackView(ix); }); } else { disconnect(m_model.get(), &TimelineItemModel::trackVisibilityChanged, this, &TimelineController::updateMultiTrack); } } void TimelineController::updateMultiTrack() { QStringList trackNames = TimelineFunctions::enableMultitrackView(m_model, true, true); pCore->monitorManager()->projectMonitor()->slotShowEffectScene(MonitorSplitTrack, false, QVariant(trackNames)); } void TimelineController::activateTrackAndSelect(int trackPosition) { int tid = -1; int ix = 0; auto it = m_model->m_allTracks.cbegin(); while (it != m_model->m_allTracks.cend()) { tid = (*it)->getId(); ++it; if (m_model->getTrackById_const(tid)->isAudioTrack() || m_model->getTrackById_const(tid)->isHidden()) { continue; } if (trackPosition == ix) { break; } ++ix; } if (tid > -1) { m_activeTrack = tid; emit activeTrackChanged(); selectCurrentItem(ObjectType::TimelineClip, true); } } void TimelineController::saveTimelineSelection(const QDir &targetDir) { TimelineFunctions::saveTimelineSelection(m_model, m_model->getCurrentSelection(), targetDir); } void TimelineController::addEffectKeyframe(int cid, int frame, double val) { if (m_model->isClip(cid)) { std::shared_ptr destStack = m_model->getClipEffectStackModel(cid); destStack->addEffectKeyFrame(frame, val); } else if (m_model->isComposition(cid)) { std::shared_ptr listModel = m_model->m_allCompositions[cid]->getKeyframeModel(); listModel->addKeyframe(frame, val); } } void TimelineController::removeEffectKeyframe(int cid, int frame) { if (m_model->isClip(cid)) { std::shared_ptr destStack = m_model->getClipEffectStackModel(cid); destStack->removeKeyFrame(frame); } else if (m_model->isComposition(cid)) { std::shared_ptr listModel = m_model->m_allCompositions[cid]->getKeyframeModel(); listModel->removeKeyframe(GenTime(frame, pCore->getCurrentFps())); } } void TimelineController::updateEffectKeyframe(int cid, int oldFrame, int newFrame, const QVariant &normalizedValue) { if (m_model->isClip(cid)) { std::shared_ptr destStack = m_model->getClipEffectStackModel(cid); destStack->updateKeyFrame(oldFrame, newFrame, normalizedValue); } else if (m_model->isComposition(cid)) { std::shared_ptr listModel = m_model->m_allCompositions[cid]->getKeyframeModel(); listModel->updateKeyframe(GenTime(oldFrame, pCore->getCurrentFps()), GenTime(newFrame, pCore->getCurrentFps()), normalizedValue); } } bool TimelineController::darkBackground() const { KColorScheme scheme(QApplication::palette().currentColorGroup()); return scheme.background(KColorScheme::NormalBackground).color().value() < 0.5; } QColor TimelineController::videoColor() const { KColorScheme scheme(QApplication::palette().currentColorGroup()); return scheme.foreground(KColorScheme::LinkText).color(); } QColor TimelineController::targetColor() const { KColorScheme scheme(QApplication::palette().currentColorGroup()); QColor base = scheme.foreground(KColorScheme::PositiveText).color(); QColor high = QApplication::palette().highlightedText().color(); double factor = 0.3; QColor res = QColor(qBound(0, base.red() + (int)(factor*(high.red() - 128)), 255), qBound(0, base.green() + (int)(factor*(high.green() - 128)), 255), qBound(0, base.blue() + (int)(factor*(high.blue() - 128)), 255), 255); return res; } QColor TimelineController::targetTextColor() const { KColorScheme scheme(QApplication::palette().currentColorGroup()); return scheme.background(KColorScheme::PositiveBackground).color(); } QColor TimelineController::audioColor() const { KColorScheme scheme(QApplication::palette().currentColorGroup()); return scheme.foreground(KColorScheme::PositiveText).color(); } QColor TimelineController::titleColor() const { KColorScheme scheme(QApplication::palette().currentColorGroup()); QColor base = scheme.foreground(KColorScheme::LinkText).color(); QColor high = scheme.foreground(KColorScheme::NegativeText).color(); QColor title = QColor(qBound(0, base.red() + (int)(high.red() - 128), 255), qBound(0, base.green() + (int)(high.green() - 128), 255), qBound(0, base.blue() + (int)(high.blue() - 128), 255), 255); return title; } QColor TimelineController::imageColor() const { KColorScheme scheme(QApplication::palette().currentColorGroup()); return scheme.foreground(KColorScheme::NeutralText).color(); } QColor TimelineController::slideshowColor() const { KColorScheme scheme(QApplication::palette().currentColorGroup()); QColor base = scheme.foreground(KColorScheme::LinkText).color(); QColor high = scheme.foreground(KColorScheme::NeutralText).color(); QColor slide = QColor(qBound(0, base.red() + (int)(high.red() - 128), 255), qBound(0, base.green() + (int)(high.green() - 128), 255), qBound(0, base.blue() + (int)(high.blue() - 128), 255), 255); return slide; } QColor TimelineController::lockedColor() const { KColorScheme scheme(QApplication::palette().currentColorGroup()); return scheme.foreground(KColorScheme::NegativeText).color(); } QColor TimelineController::groupColor() const { KColorScheme scheme(QApplication::palette().currentColorGroup()); return scheme.foreground(KColorScheme::ActiveText).color(); } QColor TimelineController::selectionColor() const { KColorScheme scheme(QApplication::palette().currentColorGroup(), KColorScheme::Complementary); return scheme.foreground(KColorScheme::NeutralText).color(); } void TimelineController::switchRecording(int trackId) { if (!pCore->isMediaCapturing()) { qDebug() << "start recording" << trackId; if (!m_model->isTrack(trackId)) { qDebug() << "ERROR: Starting to capture on invalid track " << trackId; } if (m_model->getTrackById_const(trackId)->isLocked()) { pCore->displayMessage(i18n("Impossible to capture on a locked track"), ErrorMessage, 500); return; } m_recordStart.first = pCore->getTimelinePosition(); m_recordTrack = trackId; int maximumSpace = m_model->getTrackById_const(trackId)->getBlankEnd(m_recordStart.first); if (maximumSpace == INT_MAX) { m_recordStart.second = 0; } else { m_recordStart.second = maximumSpace - m_recordStart.first; if (m_recordStart.second < 8) { pCore->displayMessage(i18n("Impossible to capture here: the capture could override clips. Please remove clips after the current position or " "choose a different track"), ErrorMessage, 500); return; } } pCore->monitorManager()->slotSwitchMonitors(false); pCore->startMediaCapture(trackId, true, false); pCore->monitorManager()->slotPlay(); } else { pCore->stopMediaCapture(trackId, true, false); pCore->monitorManager()->slotPause(); } } void TimelineController::urlDropped(QStringList droppedFile, int frame, int tid) { m_recordTrack = tid; m_recordStart = {frame, -1}; qDebug()<<"=== GOT DROPPED FILED: "< callBack = [this](const QString &binId) { int id = -1; if (m_recordTrack == -1) { return; } qDebug() << "callback " << binId << " " << m_recordTrack << ", MAXIMUM SPACE: " << m_recordStart.second; if (m_recordStart.second > 0) { // Limited space on track std::shared_ptr clip = pCore->bin()->getBinClip(binId); if (!clip) { return; } int out = qMin((int)clip->frameDuration() - 1, m_recordStart.second - 1); QString binClipId = QString("%1/%2/%3").arg(binId).arg(0).arg(out); m_model->requestClipInsertion(binClipId, m_recordTrack, m_recordStart.first, id, true, true, false); } else { m_model->requestClipInsertion(binId, m_recordTrack, m_recordStart.first, id, true, true, false); } }; QString binId = ClipCreator::createClipFromFile(recordedFile, pCore->projectItemModel()->getRootFolder()->clipId(), pCore->projectItemModel(), undo, redo, callBack); if (binId != QStringLiteral("-1")) { pCore->pushUndo(undo, redo, i18n("Record audio")); } } void TimelineController::updateVideoTarget() { if (videoTarget() > -1) { m_lastVideoTarget = videoTarget(); m_videoTargetActive = true; emit lastVideoTargetChanged(); } else { m_videoTargetActive = false; } } void TimelineController::updateAudioTarget() { if (!audioTarget().isEmpty()) { m_lastAudioTarget = m_model->m_audioTarget; m_audioTargetActive = true; emit lastAudioTargetChanged(); } else { m_audioTargetActive = false; } } bool TimelineController::hasActiveTracks() const { auto it = m_model->m_allTracks.cbegin(); while (it != m_model->m_allTracks.cend()) { int target_track = (*it)->getId(); if (m_model->getTrackById_const(target_track)->shouldReceiveTimelineOp()) { return true; } ++it; } return false; } void TimelineController::showMasterEffects() { emit showItemEffectStack(i18n("Master effects"), m_model->getMasterEffectStackModel(), pCore->getCurrentFrameSize(), false); } bool TimelineController::refreshIfVisible(int cid) { auto it = m_model->m_allTracks.cbegin(); while (it != m_model->m_allTracks.cend()) { int target_track = (*it)->getId(); if (m_model->getTrackById_const(target_track)->isAudioTrack() || m_model->getTrackById_const(target_track)->isHidden()) { ++it; continue; } int child = m_model->getClipByPosition(target_track, pCore->getTimelinePosition()); if (child > 0) { if (m_model->m_allClips[child]->binId().toInt() == cid) { return true; } } ++it; } return false; } void TimelineController::collapseActiveTrack() { if (m_activeTrack == -1) { return; } int collapsed = m_model->getTrackProperty(m_activeTrack, QStringLiteral("kdenlive:collapsed")).toInt(); m_model->setTrackProperty(m_activeTrack, QStringLiteral("kdenlive:collapsed"), collapsed > 0 ? QStringLiteral("0") : QStringLiteral("5")); } void TimelineController::setActiveTrackProperty(const QString &name, const QString &value) { if (m_activeTrack > -1) { m_model->setTrackProperty(m_activeTrack, name, value); } } bool TimelineController::isActiveTrackAudio() const { if (m_activeTrack > -1) { if (m_model->getTrackById_const(m_activeTrack)->isAudioTrack()) { return true; } } return false; } const QVariant TimelineController::getActiveTrackProperty(const QString &name) const { if (m_activeTrack > -1) { return m_model->getTrackProperty(m_activeTrack, name); } return QVariant(); } void TimelineController::expandActiveClip() { std::unordered_set ids = m_model->getCurrentSelection(); std::unordered_set items_list; for (int i : ids) { if (m_model->isGroup(i)) { std::unordered_set children = m_model->m_groups->getLeaves(i); items_list.insert(children.begin(), children.end()); } else { items_list.insert(i); } } m_model->requestClearSelection(); bool result = true; for (int id : items_list) { if (result && m_model->isClip(id)) { std::shared_ptr clip = m_model->getClipPtr(id); if (clip->clipType() == ClipType::Playlist) { std::function undo = []() { return true; }; std::function redo = []() { return true; }; if (m_model->m_groups->isInGroup(id)) { int targetRoot = m_model->m_groups->getRootId(id); if (m_model->isGroup(targetRoot)) { m_model->requestClipUngroup(targetRoot, undo, redo); } } int pos = clip->getPosition(); QDomDocument doc = TimelineFunctions::extractClip(m_model, id, getClipBinId(id)); m_model->requestClipDeletion(id, undo, redo); result = TimelineFunctions::pasteClips(m_model, doc.toString(), m_activeTrack, pos, undo, redo); if (result) { pCore->pushUndo(undo, redo, i18n("Expand clip")); } else { undo(); pCore->displayMessage(i18n("Could not expand clip"), InformationMessage, 500); } } } } } QMap TimelineController::getCurrentTargets(int trackId, int &activeTargetStream) { if (m_model->m_binAudioTargets.size() < 2) { activeTargetStream = -1; return QMap (); } if (m_model->m_audioTarget.contains(trackId)) { activeTargetStream = m_model->m_audioTarget.value(trackId); } else { activeTargetStream = -1; } return m_model->m_binAudioTargets; } void TimelineController::addTracks(int videoTracks, int audioTracks) { bool result = false; int total = videoTracks + audioTracks; Fun undo = []() { return true; }; Fun redo = []() { return true; }; for (int ix = 0; videoTracks + audioTracks > 0; ++ix) { int newTid; if (audioTracks > 0) { result = m_model->requestTrackInsertion(0, newTid, QString(), true, undo, redo); audioTracks--; } else { result = m_model->requestTrackInsertion(-1, newTid, QString(), false, undo, redo); videoTracks--; } if (result) { m_model->setTrackProperty(newTid, "kdenlive:timeline_active", QStringLiteral("1")); } else { break; } } if (result) { pCore->pushUndo(undo, redo, i18np("Insert Track", "Insert Tracks", total)); } else { pCore->displayMessage(i18n("Could not insert track"), InformationMessage, 500); undo(); } }