diff --git a/src/bin/bin.cpp b/src/bin/bin.cpp index 3ff887dc1..98a69b2b8 100644 --- a/src/bin/bin.cpp +++ b/src/bin/bin.cpp @@ -1,3116 +1,3115 @@ /* 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 "projectfolderup.h" #include "projectitemmodel.h" #include "projectsortproxymodel.h" #include "projectsubclip.h" #include "titler/titlewidget.h" #include "ui_qtextclip_ui.h" #include "undohelper.hpp" #include "xml/xml.hpp" #include "xml/xml.hpp" #include #include #include #include #include "kdenlive_debug.h" #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 setDar(double dar) { m_dar = dar; } 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 { 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 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() * m_dar; } 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 || type == AbstractProjectItem::FolderUpItem) { return QSize(hint.width(), qMin(option.fontMetrics.lineSpacing() + 4, hint.height())); } if (type == AbstractProjectItem::ClipItem) { return QSize(hint.width(), qMax(option.fontMetrics.lineSpacing() * 2 + 4, qMax(hint.height(), option.decorationSize.height()))); } if (type == AbstractProjectItem::SubClipItem) { return QSize(hint.width(), qMax(option.fontMetrics.lineSpacing() * 2 + 4, qMin(hint.height(), (int)(option.decorationSize.height() / 1.5)))); } QIcon icon = qvariant_cast(index.data(Qt::DecorationRole)); QString line1 = index.data(Qt::DisplayRole).toString(); QString line2 = index.data(Qt::UserRole).toString(); int textW = qMax(option.fontMetrics.width(line1), option.fontMetrics.width(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; if (opt.decorationSize.height() > 0) { r.setWidth(r.height() * m_dar); 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())); } int mid = (int)((r1.height() / 2)); r1.adjust(decoWidth, 0, 0, -mid); QRect r2 = option.rect; r2.adjust(decoWidth, mid, 0, 0); QRectF bounding; painter->drawText(r1, Qt::AlignLeft | Qt::AlignTop, index.data(AbstractProjectItem::DataName).toString(), &bounding); font.setBold(false); painter->setFont(font); QString subText = index.data(AbstractProjectItem::DataDuration).toString(); if (!subText.isEmpty()) { r2.adjust(0, bounding.bottom() - r2.top(), 0, 0); QColor subTextColor = painter->pen().color(); subTextColor.setAlphaF(.5); painter->setPen(subTextColor); // Draw usage counter int usage = index.data(AbstractProjectItem::UsageCount).toInt(); if (usage > 0) { subText.append(QString().sprintf(" [%d]", usage)); } painter->drawText(r2, Qt::AlignLeft | Qt::AlignTop, subText, &bounding); // Add audio/video icons for selective drag int cType = index.data(AbstractProjectItem::ClipType).toInt(); 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 or Folder Up items int decoWidth = 0; if (opt.decorationSize.height() > 0) { r.setWidth(r.height() * m_dar); 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 { QStyledItemDelegate::paint(painter, option, index); } } private: mutable bool m_editorOpen{false}; mutable QRect m_audioDragRect; mutable QRect m_videoDragRect; double m_dar{1.778}; public: PlaylistState::ClipState dragType{PlaylistState::Disabled}; }; MyListView::MyListView(QWidget *parent) : QListView(parent) { setViewMode(QListView::IconMode); setMovement(QListView::Static); setResizeMode(QListView::Adjust); setUniformItemSizes(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(); } } MyTreeView::MyTreeView(QWidget *parent) : QTreeView(parent) { setEditing(false); } 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(); } } 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(); 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_doc(nullptr) , m_extractAudioAction(nullptr) , m_transcodeAction(nullptr) , m_clipsActionsMenu(nullptr) , m_inTimelineAction(nullptr) , m_listType((BinViewType)KdenliveSettings::binMode()) , m_iconSize(160, 90) , m_propertiesPanel(nullptr) , m_blankThumb() , m_invalidClipDialog(nullptr) , m_gainedFocus(false) , m_audioDuration(0) , m_processedAudio(0) { 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); m_layout->setSpacing(0); m_layout->setContentsMargins(0, 0, 0, 0); // Search line m_proxyModel = new ProjectSortProxyModel(this); m_proxyModel->setDynamicSortFilter(true); m_searchLine = new QLineEdit(this); m_searchLine->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred); // m_searchLine->setClearButtonEnabled(true); m_searchLine->setPlaceholderText(i18n("Search")); m_searchLine->setFocusPolicy(Qt::ClickFocus); connect(m_searchLine, &QLineEdit::textChanged, m_proxyModel, &ProjectSortProxyModel::slotSetSearchString); 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::refreshAudioThumbs, this, &Bin::doRefreshAudioThumbs); connect(m_itemModel.get(), &ProjectItemModel::refreshClip, this, &Bin::refreshClip); connect(m_itemModel.get(), &ProjectItemModel::updateTimelineProducers, this, &Bin::updateTimelineProducers); connect(m_itemModel.get(), &ProjectItemModel::emitMessage, this, &Bin::emitMessage); // Connect models m_proxyModel->setSourceModel(m_itemModel.get()); connect(m_itemModel.get(), &QAbstractItemModel::dataChanged, m_proxyModel, &ProjectSortProxyModel::slotDataChanged); connect(m_proxyModel, &ProjectSortProxyModel::selectModel, this, &Bin::selectProxyModel); 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(), &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); 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); m_renameAction = KStandardAction::renameFile(this, SLOT(slotRenameItem()), this); m_renameAction->setText(i18n("Rename")); m_renameAction->setData("rename"); pCore->window()->actionCollection()->addAction(QStringLiteral("rename"), m_renameAction); 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); // Column show / hide actions m_showDate = new QAction(i18n("Show date"), this); m_showDate->setCheckable(true); connect(m_showDate, &QAction::triggered, this, &Bin::slotShowDateColumn); m_showDesc = new QAction(i18n("Show description"), this); m_showDesc->setCheckable(true); connect(m_showDesc, &QAction::triggered, this, &Bin::slotShowDescColumn); settingsMenu->addAction(m_showDate); settingsMenu->addAction(m_showDesc); settingsMenu->addAction(disableEffects); 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); // Hack, create toolbar spacer QWidget *spacer = new QWidget(); spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); m_toolbar->addWidget(spacer); // Add search line m_toolbar->addWidget(m_searchLine); m_binTreeViewDelegate = new BinItemDelegate(this); // 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))); } Bin::~Bin() { 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()) { std::shared_ptr item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(idx)); 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) { slotZoomView(e->delta() > 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::slotZoomView(bool zoomIn) { 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; } const QStringList Bin::getFolderInfo(const QModelIndex &selectedIx) { QModelIndexList indexes; if (selectedIx.isValid()) { indexes << selectedIx; } else { indexes = m_proxyModel->selectionModel()->selectedIndexes(); } if (indexes.isEmpty()) { // return root folder info QStringList folderInfo; folderInfo << QString::number(-1); folderInfo << QString(); return folderInfo; } QModelIndex ix = indexes.constFirst(); if (ix.isValid() && (m_proxyModel->selectionModel()->isSelected(ix) || selectedIx.isValid())) { return m_itemModel->getEnclosingFolderInfo(m_proxyModel->mapToSource(ix)); } // return root folder info QStringList folderInfo; folderInfo << QString::number(-1); folderInfo << QString(); return folderInfo; } 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 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->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); } } } 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) { QDesktopServices::openUrl(url); qCDebug(KDENLIVE_LOG) << " / / " + url.toString(); } else { if (!exists) { emitMessage(i18n("Could not locate %1", url.toString()), 100, ErrorMessage); } return; } } } } void Bin::slotDuplicateClip() { 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)); 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")); selectClipById(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(), QString(), currentItem->getMasterClip()->clipId()); selectClipById(id); } } } 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); m_proxyModel->selectionModel()->blockSignals(true); setEnabled(false); // Cleanup previous project m_itemModel->clean(); delete m_itemView; m_itemView = nullptr; m_doc = project; int iconHeight = QFontInfo(font()).pixelSize() * 3.5; m_iconSize = QSize(iconHeight * pCore->getCurrentDar(), iconHeight); setEnabled(true); blockSignals(false); m_proxyModel->selectionModel()->blockSignals(false); 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); } 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); } QString Bin::slotAddFolder(const QString &folderName) { auto parentFolder = m_itemModel->getFolderByBinId(getCurrentFolder()); qDebug() << "pranteforder id" << parentFolder->clipId(); QString newId; Fun undo = []() { return true; }; Fun redo = []() { return true; }; m_itemModel->requestAddFolder(newId, folderName.isEmpty() ? i18n("Folder") : folderName, parentFolder->clipId(), undo, redo); pCore->pushUndo(undo, redo, i18n("Create bin folder")); // Edit folder name if (!folderName.isEmpty()) { // We already have a name, no need to edit return newId; } auto folder = m_itemModel->getFolderByBinId(newId); auto ix = m_itemModel->getIndexFromItem(folder); 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)); } return newId; } QModelIndex Bin::getIndexForId(const QString &id, bool folderWanted) const { QModelIndexList items = m_itemModel->match(m_itemModel->index(0, 0), AbstractProjectItem::DataId, QVariant::fromValue(id), 2, Qt::MatchRecursive); for (int i = 0; i < items.count(); i++) { auto *currentItem = static_cast(items.at(i).internalPointer()); 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::selectClipById(const QString &clipId, int frame, const QPoint &zone) { if (m_monitor->activeClipId() == clipId) { if (frame > -1) { m_monitor->slotSeek(frame); } if (!zone.isNull()) { m_monitor->slotLoadClipZone(zone); } return; } m_proxyModel->selectionModel()->clearSelection(); std::shared_ptr clip = getBinClip(clipId); if (clip) { selectClip(clip); if (frame > -1) { m_monitor->slotSeek(frame); } 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_locateAction->setEnabled(true); m_duplicateAction->setEnabled(true); std::shared_ptr clip = std::static_pointer_cast(currentItem); 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")); emit findInTimeline(clip->clipId(), clip->timelineInstances()); } else if (currentItem->itemType() == AbstractProjectItem::FolderItem) { // A folder was selected, disable editing clip m_openAction->setEnabled(false); m_reloadAction->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) { showClipProperties(std::static_pointer_cast(currentItem->parent()), false); m_openAction->setEnabled(false); m_reloadAction->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); } else { emit findInTimeline(QString()); m_reloadAction->setEnabled(false); m_locateAction->setEnabled(false); m_duplicateAction->setEnabled(false); m_openAction->setEnabled(false); m_deleteAction->setEnabled(false); } } else { // No item selected in bin m_openAction->setEnabled(false); m_deleteAction->setEnabled(false); showClipProperties(nullptr); emit findInTimeline(QString()); 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 excludeFolders) { 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 = [excludeFolders, &ids](std::vector &ids_vec, std::shared_ptr item) { auto binItem = std::static_pointer_cast(item); if (!excludeFolders || (binItem->itemType() != AbstractProjectItem::FolderItem && binItem->itemType() != AbstractProjectItem::FolderUpItem)) { ids.push_back(binItem->clipId()); } 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(true); QList> ret; for (const auto &id : ids) { ret.push_back(m_itemModel->getClipByBinID(id)); } return ret; } void Bin::slotInitView(QAction *action) { if (action) { 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); } else { // remove the current folderUp item if any if (m_folderUp) { if (m_folderUp->parent()) { m_folderUp->parent()->removeChild(m_folderUp); } m_folderUp.reset(); } } m_listType = static_cast(viewType); } if (m_itemView) { delete m_itemView; } switch (m_listType) { case BinIconView: m_itemView = new MyListView(this); m_folderUp = ProjectFolderUp::construct(m_itemModel); m_showDate->setEnabled(false); m_showDesc->setEnabled(false); break; default: m_itemView = new MyTreeView(this); m_showDate->setEnabled(true); m_showDesc->setEnabled(true); 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_binTreeViewDelegate->setDar(pCore->getCurrentDar()); m_itemView->setModel(m_proxyModel); m_itemView->setSelectionModel(m_proxyModel->selectionModel()); m_layout->insertWidget(1, m_itemView); // 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(m_proxyModel, &QAbstractItemModel::layoutAboutToBeChanged, this, &Bin::slotSetSorting); connect(view, &MyTreeView::updateDragMode, m_itemModel.get(), &ProjectItemModel::setDragType, Qt::DirectConnection); m_proxyModel->setDynamicSortFilter(true); if (!m_headerInfo.isEmpty()) { view->header()->restoreState(m_headerInfo); } else { view->header()->resizeSections(QHeaderView::ResizeToContents); view->resizeColumnToContents(0); view->setColumnHidden(1, true); view->setColumnHidden(2, true); } m_showDate->setChecked(!view->isColumnHidden(1)); m_showDesc->setChecked(!view->isColumnHidden(2)); 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) { auto *view = static_cast(m_itemView); connect(view, &MyListView::focusView, this, &Bin::slotGotFocus); } m_itemView->setEditTriggers(QAbstractItemView::NoEditTriggers); // DoubleClicked); m_itemView->setSelectionMode(QAbstractItemView::ExtendedSelection); m_itemView->setDragDropMode(QAbstractItemView::DragDrop); m_itemView->setAlternatingRowColors(true); m_itemView->setAcceptDrops(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); 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); m_inTimelineAction = m_menu->insertMenu(m_reloadAction, static_cast(pCore->window()->factory()->container(QStringLiteral("clip_in_timeline"), pCore->window()))); } 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; if (m_itemView) { 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; if (itemType == AbstractProjectItem::ClipItem) { auto clip = std::static_pointer_cast(currentItem); if (clip) { m_proxyAction->blockSignals(true); 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 if (itemType == AbstractProjectItem::SubClipItem) { } } } } // 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_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_duplicateAction->setVisible(itemType != AbstractProjectItem::FolderItem); m_inTimelineAction->setVisible(itemType != AbstractProjectItem::FolderItem); if (m_transcodeAction) { m_transcodeAction->setEnabled(enableClipActions); m_transcodeAction->menuAction()->setVisible(itemType != AbstractProjectItem::FolderItem && 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::FolderItem && (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_folderUp->changeParent(std::static_pointer_cast(item)); m_itemView->setRootIndex(ix); return; } if (item == m_folderUp) { std::shared_ptr parentItem = item->parent(); QModelIndex parent = getIndexForId(parentItem->parent()->clipId(), parentItem->parent()->itemType() == AbstractProjectItem::FolderItem); if (parentItem->parent() != m_itemModel->getRootFolder()) { // We are entering a parent folder m_folderUp->changeParent(std::static_pointer_cast(parentItem->parent())); } else { m_folderUp->changeParent(std::shared_ptr()); } m_itemView->setRootIndex(m_proxyModel->mapFromSource(parent)); 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); } } 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); 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()); 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(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) { std::shared_ptr clip = m_itemModel->getClipByBinID(id); if (!clip) { return; } clip->reloadProducer(); } void Bin::reloadMonitorIfActive(const QString &id) { if (m_monitor->activeClipId() == id) { slotOpenCurrent(); } } 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::Select); } m_itemView->scrollTo(m_proxyModel->mapFromSource(ix)); } 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); m_inTimelineAction->setEnabled(!addMenu->isEmpty()); } if (m_locateAction) { m_menu->addAction(m_locateAction); } if (m_reloadAction) { m_menu->addAction(m_reloadAction); } 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(QMenu *addMenu, QAction *defaultAction, const QHash &actions) { // Setup actions QAction *first = m_toolbar->actions().at(0); m_deleteAction = actions.value(QStringLiteral("delete")); m_toolbar->insertAction(first, m_deleteAction); QAction *folder = actions.value(QStringLiteral("folder")); m_toolbar->insertAction(m_deleteAction, folder); m_editAction = actions.value(QStringLiteral("properties")); m_openAction = actions.value(QStringLiteral("open")); m_reloadAction = actions.value(QStringLiteral("reload")); m_duplicateAction = actions.value(QStringLiteral("duplicate")); m_locateAction = actions.value(QStringLiteral("locate")); m_proxyAction = actions.value(QStringLiteral("proxy")); auto *m = new QMenu(this); m->addActions(addMenu->actions()); m_addButton = new QToolButton(this); m_addButton->setMenu(m); m_addButton->setDefaultAction(defaultAction); m_addButton->setPopupMode(QToolButton::MenuButtonPopup); m_toolbar->insertWidget(folder, m_addButton); m_menu = new QMenu(this); m_propertiesDock = pCore->window()->addDock(i18n("Clip Properties"), QStringLiteral("clip_properties"), m_propertiesPanel); m_propertiesDock->close(); // m_menu->addActions(addMenu->actions()); } 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) { // Remove existing actions if any 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); for (QAction *action : actions) { m_infoMessage->addAction(action); connect(action, &QAction::triggered, this, &Bin::slotMessageActionTriggered); } m_infoMessage->setCloseButtonVisible(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 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::doRefreshAudioThumbs(const QString &id) { if (m_monitor->activeClipId() == id) { slotSendAudioThumb(id); } } 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(); QStringList folderInfo = getFolderInfo(); 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); } 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); } void Bin::slotEffectDropped(const QStringList &effectData, const QModelIndex &parent) { if (parent.isValid()) { std::shared_ptr parentItem = m_itemModel->getBinItemByIndex(parent); if (parentItem->itemType() != AbstractProjectItem::ClipItem) { // effect only supported on clip items return; } m_proxyModel->selectionModel()->clearSelection(); int row = parent.row(); const QModelIndex id = m_itemModel->index(row, 0, parent.parent()); const QModelIndex id2 = m_itemModel->index(row, m_itemModel->columnCount() - 1, parent.parent()); if (id.isValid() && id2.isValid()) { m_proxyModel->selectionModel()->select(QItemSelection(m_proxyModel->mapFromSource(id), m_proxyModel->mapFromSource(id2)), QItemSelectionModel::Select); } setCurrent(parentItem); if (effectData.count() == 4) { // Paste effect from another stack std::shared_ptr sourceStack = pCore->getItemEffectStack(effectData.at(1).toInt(), effectData.at(2).toInt()); std::static_pointer_cast(parentItem)->copyEffect(sourceStack, effectData.at(3).toInt()); } else { std::static_pointer_cast(parentItem)->addEffect(effectData.constFirst()); } } } 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 QStringList &folderInfo) { QModelIndex current; if (folderInfo.isEmpty()) { current = m_proxyModel->mapToSource(m_proxyModel->selectionModel()->currentIndex()); } else { // get index for folder current = getIndexForId(folderInfo.constFirst(), true); } 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(); } ClipCreator::createClipsFromList(urls, true, parentFolder, m_itemModel); } 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, const QString &oldName, 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->setProducerProperty("kdenlive:clipzone." + oldName, QString()); clip->setProducerProperty("kdenlive:clipzone." + newName, QString::number(in) + QLatin1Char(';') + QString::number(out)); 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(); QStringList value = results.value(key).split(QLatin1Char(';'), QString::SkipEmptyParts); // 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)); } } */ void Bin::slotGetCurrentProjectImage(const QString &clipId, bool request) { Q_UNUSED(clipId) // TODO refact : look at this // if (!clipId.isEmpty()) { // (pCore->projectManager()->currentTimeline()->hideClip(clipId, request)); // } pCore->monitorManager()->projectMonitor()->slotGetCurrentImage(request); } // 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()); connect(&dia_ui, &TitleWidget::requestBackgroundFrame, this, &Bin::slotGetCurrentProjectImage); 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()) + 1) { // 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)); } else { newprops.insert(QStringLiteral("resource"), QString()); } } slotEditClipCommand(clip->AbstractProjectItem::clipId(), clip->currentProperties(newprops), newprops); } } void Bin::slotResetInfoMessage() { m_errorLog.clear(); QList actions = m_infoMessage->actions(); for (int i = 0; i < actions.count(); ++i) { m_infoMessage->removeAction(actions.at(i)); } } void Bin::emitMessage(const QString &text, int progress, MessageType type) { emit displayMessage(text, progress, type); } void Bin::slotSetSorting() { auto *view = qobject_cast(m_itemView); if (view) { int ix = view->header()->sortIndicatorSection(); m_proxyModel->setFilterKeyColumn(ix); } } void Bin::slotShowDateColumn(bool show) { auto *view = qobject_cast(m_itemView); if (view) { view->setColumnHidden(1, !show); } } void Bin::slotShowDescColumn(bool show) { auto *view = qobject_cast(m_itemView); if (view) { view->setColumnHidden(2, !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 clip = m_itemModel->getClipByBinID(id); if (clip) { clip->setProperties(properties, refreshPropertiesPanel); } } void Bin::updateTimelineProducers(const QString &id, const QMap &passProperties) { Q_UNUSED(id) Q_UNUSED(passProperties) // TODO REFAC // pCore->projectManager()->currentTimeline()->updateClipProperties(id, passProperties); // m_doc->renderer()->updateSlowMotionProducers(id, passProperties); } 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() { 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; } void Bin::slotSendAudioThumb(const QString &id) { std::shared_ptr clip = m_itemModel->getClipByBinID(id); if ((clip != nullptr) && clip->audioThumbCreated()) { m_monitor->prepareAudioThumb(clip->audioChannels(), clip->audioFrameCache); } else { - QVariantList list; - m_monitor->prepareAudioThumb(0, list); + m_monitor->prepareAudioThumb(0); } } bool Bin::isEmpty() const { if (m_itemModel->getRootFolder() == nullptr) { return true; } return !m_itemModel->getRootFolder()->hasChildClips(); } void Bin::reloadAllProducers() { 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); // Make sure we reload clip length xml.removeAttribute(QStringLiteral("out")); Xml::removeXmlProperty(xml, QStringLiteral("length")); if (!xml.isNull()) { clip->setClipStatus(AbstractProjectItem::StatusWaiting); clip->discardAudioThumb(); pCore->jobManager()->slotDiscardClipJobs(clip->clipId()); // We need to set a temporary id before all outdated producers are replaced; int jobId = pCore->jobManager()->startJob({clip->clipId()}, -1, QString(), xml); pCore->jobManager()->startJob({clip->clipId()}, jobId, QString(), 150, -1, true, true); pCore->jobManager()->startJob({clip->clipId()}, jobId, 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: { openProducer(std::static_pointer_cast(item)); std::shared_ptr clp = std::static_pointer_cast(item); 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(); openProducer(subClip->getMasterClip(), zone.x(), zone.y()); break; } case AbstractProjectItem::FolderUpItem: case AbstractProjectItem::FolderItem: 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()); } } } diff --git a/src/bin/projectclip.cpp b/src/bin/projectclip.cpp index 561732ee2..ad76e2f07 100644 --- a/src/bin/projectclip.cpp +++ b/src/bin/projectclip.cpp @@ -1,1428 +1,1429 @@ /* 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/jobmanager.h" #include "jobs/loadjob.hpp" #include "jobs/thumbjob.hpp" #include "jobs/audiothumbjob.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 #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_thumbsProducer(nullptr) { m_markerModel = std::make_shared(id, pCore->projectManager()->undoStack()); m_clipStatus = StatusReady; m_name = clipName(); m_duration = getStringDuration(); m_inPoint = 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)); } 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); self->m_effectStack->importEffects(producer, PlaylistState::Disabled, true); model->loadSubClips(id, self->getPropertiesFromPrefix(QStringLiteral("kdenlive:clipzone."))); return self; } ProjectClip::ProjectClip(const QString &id, const QDomElement &description, const QIcon &thumb, const std::shared_ptr &model) : AbstractProjectItem(AbstractProjectItem::ClipItem, id, model) , ClipController(id) , m_thumbsProducer(nullptr) { 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(); audioFrameCache.clear(); } void ProjectClip::connectEffectStack() { connect(m_effectStack.get(), &EffectStackModel::modelChanged, this, &ProjectClip::updateChildProducers); connect(m_effectStack.get(), &EffectStackModel::dataChanged, this, &ProjectClip::updateChildProducers); 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); } }); /*connect(m_effectStack.get(), &EffectStackModel::modelChanged, [&](){ qDebug()<<"/ / / STACK CHANGED"; updateChildProducers(); });*/ } QString ProjectClip::getToolTip() const { return url(); } 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(QVariantList audioLevels) +void ProjectClip::updateAudioThumbnail(QList audioLevels) { - std::swap(audioFrameCache, audioLevels); // avoid second copy + audioFrameCache = audioLevels; m_audioThumbCreated = true; if (auto ptr = m_model.lock()) { emit std::static_pointer_cast(ptr)->refreshAudioThumbs(m_binId); } + qDebug()<<" * ** * YOP AUDIO LEF CHANGED"; updateTimelineClips({TimelineModel::AudioLevelsRole}); } 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 { GenTime d = duration(); return (size_t)d.frames(pCore->getCurrentFps()); } void ProjectClip::reloadProducer(bool refreshOnly, bool audioStreamChanged) { // 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()); pCore->jobManager()->discardJobs(clipId(), AbstractClipJob::THUMBJOB); m_thumbsProducer.reset(); pCore->jobManager()->startJob({clipId()}, loadjobId, QString(), 150, -1, true, true); } else { // If another load job is running? if (loadjobId > -1) { pCore->jobManager()->discardJobs(clipId(), AbstractClipJob::LOADJOB); } QDomDocument doc; QDomElement xml = toXml(doc); if (!xml.isNull()) { pCore->jobManager()->discardJobs(clipId(), AbstractClipJob::THUMBJOB); m_thumbsProducer.reset(); ThumbnailCache::get()->invalidateThumbsForClip(clipId()); int loadJob = pCore->jobManager()->startJob({clipId()}, loadjobId, QString(), xml); pCore->jobManager()->startJob({clipId()}, loadJob, QString(), 150, -1, true, true); if (audioStreamChanged) { discardAudioThumb(); pCore->jobManager()->startJob({clipId()}, loadjobId, QString()); } } } } QDomElement ProjectClip::toXml(QDomDocument &document, bool includeMeta) { getProducerXML(document, includeMeta); QDomElement 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) { 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; 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(); replaceInTimeline(); 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()); Mlt::Filter converter(*prod->profile(), "avcolor_space"); m_thumbsProducer->attach(converter); } 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); 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, double speed) { if (!m_masterProducer) { return nullptr; } if (qFuzzyCompare(speed, 1.0)) { // we are requesting a normal speed producer if (trackId == -1 || (state == PlaylistState::VideoOnly && (m_clipType == ClipType::Color || m_clipType == ClipType::Image || m_clipType == ClipType::Text))) { // 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)); } 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 (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); 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)); } 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)); } // 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) { QLocale locale; QString resource(originalProducer()->get("resource")); if (resource.isEmpty() || resource == QLatin1String("")) { resource = m_service; } QString url = QString("timewarp:%1:%2").arg(locale.toString(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 warpProducer->set("length", double(original_length) / std::abs(speed)); } 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 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(-1, clipId, state, speed)->cut(in, out)); return {prod, false}; } if (state == PlaylistState::Disabled && !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}; } if (state == PlaylistState::Disabled && QString::fromUtf8(m_disabledProducer->get("id")) != QString::fromUtf8(master->parent().get("id"))) { qDebug() << "Warning: weird, we found a disabled clip whose master is already loaded but doesn't match ours"; 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) { m_audioProducers[clipId] = std::make_shared(&master->parent()); m_effectStack->loadService(m_audioProducers[clipId]); return {master, true}; } if (state == PlaylistState::VideoOnly) { // good, we found a master video producer, and we didn't have any m_videoProducers[clipId] = std::make_shared(&master->parent()); m_effectStack->loadService(m_videoProducers[clipId]); return {master, true}; } if (state == PlaylistState::Disabled && !m_disabledProducer) { // good, we found a master disabled producer, and we didn't have any m_disabledProducer.reset(master->parent().cut()); m_effectStack->loadService(m_disabledProducer); return {master, 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, speed), false}; } // we have a problem return {std::shared_ptr(ClipController::mediaUnavailable->cut()), false}; } /* std::shared_ptr ProjectClip::timelineProducer(PlaylistState::ClipState state, int track) { if (!m_service.startsWith(QLatin1String("avformat"))) { std::shared_ptr prod(originalProducer()->cut()); int length = getProducerIntProperty(QStringLiteral("kdenlive:duration")); if (length > 0) { prod->set_in_and_out(0, length); } return prod; } if (state == PlaylistState::VideoOnly) { if (m_timelineProducers.count(0) > 0) { return std::shared_ptr(m_timelineProducers.find(0)->second->cut()); } std::shared_ptr videoProd = cloneProducer(); videoProd->set("audio_index", -1); m_timelineProducers[0] = videoProd; return std::shared_ptr(videoProd->cut()); } if (state == PlaylistState::AudioOnly) { if (m_timelineProducers.count(-track) > 0) { return std::shared_ptr(m_timelineProducers.find(-track)->second->cut()); } std::shared_ptr audioProd = cloneProducer(); audioProd->set("video_index", -1); m_timelineProducers[-track] = audioProd; return std::shared_ptr(audioProd->cut()); } if (m_timelineProducers.count(track) > 0) { return std::shared_ptr(m_timelineProducers.find(track)->second->cut()); } std::shared_ptr normalProd = cloneProducer(); m_timelineProducers[track] = normalProd; return std::shared_ptr(normalProd->cut()); }*/ 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"); } 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() << "/ + + DELTING"; 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"); } return prod; } std::shared_ptr ProjectClip::softClone(const char *list) { QString service = QString::fromLatin1(m_masterProducer->get("mlt_service")); QString resource = QString::fromLatin1(m_masterProducer->get("resource")); std::shared_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: case ClipType::TextTemplate: fileData = getProducerProperty(QStringLiteral("xmldata")).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"), }; QStringList forceReloadProperties{QStringLiteral("autorotate"), QStringLiteral("templatetext"), QStringLiteral("resource"), QStringLiteral("force_fps"), QStringLiteral("set.test_image"), QStringLiteral("set.test_audio"), QStringLiteral("audio_index"), 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()); pCore->jobManager()->startJob({clipId()}, -1, QString()); } } else if (!reload) { const QList propKeys = properties.keys(); for (const QString &k : propKeys) { if (forceReloadProperties.contains(k)) { if (m_clipType != ClipType::Color) { reload = true; refreshOnly = false; } else { // Clip resource changed, update thumbnail reload = true; refreshPanel = true; updateRoles << TimelineModel::ResourceRole; } 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"))) { 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: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 updateTimelineClips(QVector() << TimelineModel::NameRole); } if (refreshPanel) { // Some of the clip properties have changed through a command, update properties panel emit refreshPropertiesPanel(); } 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, properties.contains(QStringLiteral("audio_index"))); } if (refreshOnly) { if (auto ptr = m_model.lock()) { emit std::static_pointer_cast(ptr)->refreshClip(m_binId); } } if (!updateRoles.isEmpty()) { updateTimelineClips(updateRoles); } } if (!passProperties.isEmpty()) { if (auto ptr = m_model.lock()) emit std::static_pointer_cast(ptr)->updateTimelineProducers(m_binId, passProperties); } } 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: return m_effectStack && m_effectStack->rowCount() > 0 ? QVariant("kdenlive-track_has_effect") : QVariant(); default: return AbstractProjectItem::getData(type); } } void ProjectClip::slotExtractImage(const QList &frames) { QMutexLocker lock(&m_thumbMutex); for (int i = 0; i < frames.count(); i++) { if (!m_requestedThumbs.contains(frames.at(i))) { m_requestedThumbs << frames.at(i); } } qSort(m_requestedThumbs); if (!m_thumbThread.isRunning()) { m_thumbThread = QtConcurrent::run(this, &ProjectClip::doExtractImage); } } void ProjectClip::doExtractImage() { // TODO refac: we can probably move that into a ThumbJob std::shared_ptr prod = thumbProducer(); if (prod == nullptr || !prod->is_valid()) { return; } int frameWidth = 150 * prod->profile()->dar() + 0.5; bool ok = false; auto ptr = m_model.lock(); Q_ASSERT(ptr); QDir thumbFolder = pCore->currentDoc()->getCacheDir(CacheThumbs, &ok); int max = prod->get_length(); while (!m_requestedThumbs.isEmpty()) { m_thumbMutex.lock(); int pos = m_requestedThumbs.takeFirst(); m_thumbMutex.unlock(); if (ok && thumbFolder.exists(hash() + QLatin1Char('#') + QString::number(pos) + QStringLiteral(".png"))) { emit thumbReady(pos, QImage(thumbFolder.absoluteFilePath(hash() + QLatin1Char('#') + QString::number(pos) + QStringLiteral(".png")))); continue; } if (pos >= max) { pos = max - 1; } const QString path = url() + QLatin1Char('_') + QString::number(pos); QImage img; if (ThumbnailCache::get()->hasThumbnail(clipId(), pos, true)) { img = ThumbnailCache::get()->getThumbnail(clipId(), pos, true); } if (!img.isNull()) { emit thumbReady(pos, img); continue; } prod->seek(pos); Mlt::Frame *frame = prod->get_frame(); frame->set("deinterlace_method", "onefield"); frame->set("top_field_first", -1); if (frame->is_valid()) { img = KThumb::getFrame(frame, frameWidth, 150, !qFuzzyCompare(prod->profile()->sar(), 1)); ThumbnailCache::get()->storeThumbnail(clipId(), pos, img, false); emit thumbReady(pos, img); } delete frame; } } int ProjectClip::audioChannels() const { if (!audioInfo()) { return 0; } return audioInfo()->channels(); } void ProjectClip::discardAudioThumb() { QString audioThumbPath = getAudioThumbPath(); if (!audioThumbPath.isEmpty()) { QFile::remove(audioThumbPath); } audioFrameCache.clear(); qCDebug(KDENLIVE_LOG) << "//////////////////// DISCARD AUIIO THUMBNS"; m_audioThumbCreated = false; refreshAudioInfo(); pCore->jobManager()->discardJobs(clipId(), AbstractClipJob::AUDIOTHUMBJOB); } const QString ProjectClip::getAudioThumbPath() { if (audioInfo() == nullptr) { return QString(); } int audioStream = audioInfo()->ffmpeg_audio_index(); QString clipHash = hash(); if (clipHash.isEmpty()) { return QString(); } bool ok = false; QDir thumbFolder = pCore->currentDoc()->getCacheDir(CacheAudio, &ok); if (!ok) { return QString(); } QString audioPath = thumbFolder.absoluteFilePath(clipHash); if (audioStream > 0) { audioPath.append(QLatin1Char('_') + QString::number(audioInfo()->audio_index())); } 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::updateChildProducers() { // TODO refac: the effect should be managed by an effectstack on the master /* // pass effect stack on all child producers QMutexLocker locker(&m_producerMutex); for (const auto &clip : m_timelineProducers) { if (auto producer = clip.second) { Clip clp(producer->parent()); clp.deleteEffects(); clp.replaceEffects(*m_masterProducer); } } */ } 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; } } } diff --git a/src/bin/projectclip.h b/src/bin/projectclip.h index f59a914d2..5ee0268ca 100644 --- a/src/bin/projectclip.h +++ b/src/bin/projectclip.h @@ -1,288 +1,288 @@ /* 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 PROJECTCLIP_H #define PROJECTCLIP_H #include "abstractprojectitem.h" #include "definitions.h" #include "mltcontroller/clipcontroller.h" #include "timeline2/model/timelinemodel.hpp" #include #include #include #include class AudioStreamInfo; class ClipPropertiesController; class MarkerListModel; class ProjectFolder; class ProjectSubClip; class QDomElement; class QUndoCommand; namespace Mlt { class Producer; class Properties; } // namespace Mlt /** * @class ProjectClip * @brief Represents a clip in the project (not timeline). * It will be displayed as a bin item that can be dragged onto the timeline. * A single bin clip can be inserted several times on the timeline, and the ProjectClip * keeps track of all the ids of the corresponding ClipModel. * Note that because of a limitation in melt and AvFilter, it is currently difficult to * mix the audio of two producers that are cut from the same master producer * (that produces small but noticeable clicking artifacts) * To workaround this, we need to have a master clip for each instance of the audio clip in the timeline. This class is tracking them all. This track also holds * a master clip for each clip where the timewarp producer has been applied */ class ProjectClip : public AbstractProjectItem, public ClipController { Q_OBJECT public: friend class Bin; friend bool TimelineModel::checkConsistency(); // for testing /** * @brief Constructor; used when loading a project and the producer is already available. */ static std::shared_ptr construct(const QString &id, const QIcon &thumb, const std::shared_ptr &model, const std::shared_ptr &producer); /** * @brief Constructor. * @param description element describing the clip; the "kdenlive:id" attribute and "resource" property are used */ static std::shared_ptr construct(const QString &id, const QDomElement &description, const QIcon &thumb, std::shared_ptr model); protected: ProjectClip(const QString &id, const QIcon &thumb, const std::shared_ptr &model, std::shared_ptr producer); ProjectClip(const QString &id, const QDomElement &description, const QIcon &thumb, const std::shared_ptr &model); public: ~ProjectClip() override; void reloadProducer(bool refreshOnly = false, bool audioStreamChanged = false); /** @brief Returns a unique hash identifier used to store clip thumbnails. */ // virtual void hash() = 0; /** @brief Returns this if @param id matches the clip's id or nullptr otherwise. */ std::shared_ptr clip(const QString &id) override; std::shared_ptr folder(const QString &id) override; std::shared_ptr getSubClip(int in, int out); /** @brief Returns this if @param ix matches the clip's index or nullptr otherwise. */ std::shared_ptr clipAt(int ix) override; /** @brief Returns the clip type as defined in definitions.h */ ClipType::ProducerType clipType() const override; bool selfSoftDelete(Fun &undo, Fun &redo) override; /** @brief Returns true if item has both audio and video enabled. */ bool hasAudioAndVideo() const override; /** @brief Check if clip has a parent folder with id id */ bool hasParent(const QString &id) const; /** @brief Returns true is the clip can have the requested state */ bool isCompatible(PlaylistState::ClipState state) const; ClipPropertiesController *buildProperties(QWidget *parent); QPoint zone() const override; /** @brief Returns whether this clip has a url (=describes a file) or not. */ bool hasUrl() const; /** @brief Returns the clip's url. */ const QString url() const; /** @brief Returns the clip's duration. */ GenTime duration() const; size_t frameDuration() const; /** @brief Returns the original clip's fps. */ double getOriginalFps() const; bool rename(const QString &name, int column) override; QDomElement toXml(QDomDocument &document, bool includeMeta = false) override; QVariant getData(DataType type) const override; /** @brief Sets thumbnail for this clip. */ void setThumbnail(const QImage &); QPixmap thumbnail(int width, int height); /** @brief Sets the MLT producer associated with this clip * @param producer The producer * @param replaceProducer If true, we replace existing producer with this one * @returns true if producer was changed * . */ bool setProducer(std::shared_ptr producer, bool replaceProducer); /** @brief Returns true if this clip already has a producer. */ bool isReady() const; /** @brief Returns this clip's producer. */ std::shared_ptr thumbProducer(); /** @brief Recursively disable/enable bin effects. */ void setBinEffectsEnabled(bool enabled) override; /** @brief Set properties on this clip. TODO: should we store all in MLT or use extra m_properties ?. */ void setProperties(const QMap &properties, bool refreshPanel = false); /** @brief Get an XML property from MLT produced xml. */ static QString getXmlProperty(const QDomElement &producer, const QString &propertyName, const QString &defaultValue = QString()); QString getToolTip() const override; /** @brief The clip hash created from the clip's resource. */ const QString hash(); /** @brief Returns true if we are using a proxy for this clip. */ bool hasProxy() const; /** Cache for every audio Frame with 10 Bytes */ /** format is frame -> channel ->bytes */ - QVariantList audioFrameCache; + QList audioFrameCache; bool audioThumbCreated() const; void setWaitingStatus(const QString &id); /** @brief Returns true if the clip matched a condition, for example vcodec=mpeg1video. */ bool matches(const QString &condition); /** @brief Returns the number of audio channels. */ int audioChannels() const; /** @brief get data analysis value. */ QStringList updatedAnalysisData(const QString &name, const QString &data, int offset); QMap analysisData(bool withPrefix = false); /** @brief Returns the list of this clip's subclip's ids. */ QStringList subClipIds() const; /** @brief Delete cached audio thumb - needs to be recreated */ void discardAudioThumb(); /** @brief Get path for this clip's audio thumbnail */ const QString getAudioThumbPath(); /** @brief Returns true if this producer has audio and can be splitted on timeline*/ bool isSplittable() const; /** @brief Returns true if a clip corresponding to this bin is inserted in a timeline. Note that this function does not account for children, use TreeItem::accumulate if you want to get that information as well. */ bool isIncludedInTimeline() override; /** @brief Returns a list of all timeline clip ids for this bin clip */ QList timelineInstances() const; /** @brief This function returns a cut to the master producer associated to the timeline clip with given ID. Each clip must have a different master producer (see comment of the class) */ std::shared_ptr getTimelineProducer(int trackId, int clipId, PlaylistState::ClipState st, double speed = 1.0); /* @brief This function should only be used at loading. It takes a producer that was read from mlt, and checks whether the master producer is already in use. If yes, then we must create a new one, because of the mixing bug. In any case, we return a cut of the master that can be used in the timeline The bool returned has the following sementic: - if true, then the returned cut still possibly has effect on it. You need to rebuild the effectStack based on this - if false, the the returned cut don't have effects anymore (it's a fresh one), so you need to reload effects from the old producer */ std::pair, bool> giveMasterAndGetTimelineProducer(int clipId, std::shared_ptr master, PlaylistState::ClipState state); std::shared_ptr cloneProducer(bool removeEffects = false); static std::shared_ptr cloneProducer(const std::shared_ptr &producer); std::shared_ptr softClone(const char *list); void updateTimelineClips(const QVector &roles); protected: friend class ClipModel; /** @brief This is a call-back called by a ClipModel when it is created @param timeline ptr to the pointer in which this ClipModel is inserted @param clipId id of the inserted clip */ void registerTimelineClip(std::weak_ptr timeline, int clipId); void registerService(std::weak_ptr timeline, int clipId, const std::shared_ptr &service, bool forceRegister = false); /* @brief update the producer to reflect new parent folder */ void updateParent(std::shared_ptr parent) override; /** @brief This is a call-back called by a ClipModel when it is deleted @param clipId id of the deleted clip */ void deregisterTimelineClip(int clipId); void emitProducerChanged(const QString &id, const std::shared_ptr &producer) override { emit producerChanged(id, producer); }; /** @brief Replace instance of this clip in timeline */ void updateChildProducers(); void replaceInTimeline(); void connectEffectStack() override; public slots: /* @brief Store the audio thumbnails once computed. Note that the parameter is a value and not a reference, fill free to use it as a sink (use std::move to * avoid copy). */ - void updateAudioThumbnail(QVariantList audioLevels); + void updateAudioThumbnail(QList audioLevels); /** @brief Extract image thumbnails for timeline. */ void slotExtractImage(const QList &frames); /** @brief Delete the proxy file */ void deleteProxy(); private: /** @brief Generate and store file hash if not available. */ const QString getFileHash(); /** @brief Store clip url temporarily while the clip controller has not been created. */ QString m_temporaryUrl; std::shared_ptr m_thumbsProducer; QMutex m_producerMutex; QMutex m_thumbMutex; QFuture m_thumbThread; QList m_requestedThumbs; const QString geometryWithOffset(const QString &data, int offset); void doExtractImage(); // This is a helper function that creates the disabled producer. This is a clone of the original one, with audio and video disabled void createDisabledMasterProducer(); std::map> m_registeredClips; // the following holds a producer for each audio clip in the timeline // keys are the id of the clips in the timeline, values are their values std::unordered_map> m_audioProducers; std::unordered_map> m_videoProducers; std::unordered_map> m_timewarpProducers; std::shared_ptr m_disabledProducer; signals: void producerChanged(const QString &, const std::shared_ptr &); void refreshPropertiesPanel(); void refreshAnalysisPanel(); void refreshClipDisplay(); void thumbReady(int, const QImage &); /** @brief Clip is ready, load properties. */ void loadPropertiesPanel(); }; #endif diff --git a/src/bin/projectitemmodel.cpp b/src/bin/projectitemmodel.cpp index 4ce46557f..41c9b7730 100644 --- a/src/bin/projectitemmodel.cpp +++ b/src/bin/projectitemmodel.cpp @@ -1,952 +1,966 @@ /* Copyright (C) 2012 Till Theato Copyright (C) 2014 Jean-Baptiste Mardelle Copyright (C) 2017 Nicolas Carion 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 "projectitemmodel.h" #include "abstractprojectitem.h" #include "binplaylist.hpp" #include "core.h" #include "doc/kdenlivedoc.h" #include "filewatcher.hpp" #include "jobs/audiothumbjob.hpp" #include "jobs/jobmanager.h" #include "jobs/loadjob.hpp" #include "jobs/thumbjob.hpp" #include "kdenlivesettings.h" #include "macros.hpp" #include "profiles/profilemodel.hpp" #include "project/projectmanager.h" #include "projectclip.h" #include "projectfolder.h" #include "projectsubclip.h" #include "xml/xml.hpp" #include #include #include #include #include #include #include ProjectItemModel::ProjectItemModel(QObject *parent) : AbstractTreeModel(parent) , m_lock(QReadWriteLock::Recursive) , m_binPlaylist(new BinPlaylist()) , m_fileWatcher(new FileWatcher()) , m_nextId(1) , m_blankThumb() , m_dragType(PlaylistState::Disabled) { QPixmap pix(QSize(160, 90)); pix.fill(Qt::lightGray); m_blankThumb.addPixmap(pix); connect(m_fileWatcher.get(), &FileWatcher::binClipModified, this, &ProjectItemModel::reloadClip); connect(m_fileWatcher.get(), &FileWatcher::binClipWaiting, this, &ProjectItemModel::setClipWaiting); connect(m_fileWatcher.get(), &FileWatcher::binClipMissing, this, &ProjectItemModel::setClipInvalid); } std::shared_ptr ProjectItemModel::construct(QObject *parent) { std::shared_ptr self(new ProjectItemModel(parent)); self->rootItem = ProjectFolder::construct(self); return self; } ProjectItemModel::~ProjectItemModel() = default; int ProjectItemModel::mapToColumn(int column) const { switch (column) { case 0: return AbstractProjectItem::DataName; break; case 1: return AbstractProjectItem::DataDate; break; case 2: return AbstractProjectItem::DataDescription; break; default: return AbstractProjectItem::DataName; } } QVariant ProjectItemModel::data(const QModelIndex &index, int role) const { READ_LOCK(); if (!index.isValid()) { return QVariant(); } if (role == Qt::DisplayRole || role == Qt::EditRole) { std::shared_ptr item = getBinItemByIndex(index); auto type = static_cast(mapToColumn(index.column())); QVariant ret = item->getData(type); return ret; } if (role == Qt::DecorationRole) { if (index.column() != 0) { return QVariant(); } // Data has to be returned as icon to allow the view to scale it std::shared_ptr item = getBinItemByIndex(index); QVariant thumb = item->getData(AbstractProjectItem::DataThumbnail); QIcon icon; if (thumb.canConvert()) { icon = thumb.value(); } else { qDebug() << "ERROR: invalid icon found"; } return icon; } std::shared_ptr item = getBinItemByIndex(index); return item->getData(static_cast(role)); } bool ProjectItemModel::setData(const QModelIndex &index, const QVariant &value, int role) { QWriteLocker locker(&m_lock); std::shared_ptr item = getBinItemByIndex(index); if (item->rename(value.toString(), index.column())) { emit dataChanged(index, index, {role}); return true; } // Item name was not changed return false; } Qt::ItemFlags ProjectItemModel::flags(const QModelIndex &index) const { /*return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | Qt::ItemIsEditable;*/ if (!index.isValid()) { return Qt::ItemIsDropEnabled; } std::shared_ptr item = getBinItemByIndex(index); AbstractProjectItem::PROJECTITEMTYPE type = item->itemType(); switch (type) { case AbstractProjectItem::FolderItem: return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | Qt::ItemIsEditable; break; case AbstractProjectItem::ClipItem: if (!item->statusReady()) { return Qt::ItemIsSelectable; } return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | Qt::ItemIsEditable; break; case AbstractProjectItem::SubClipItem: return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable | Qt::ItemIsDragEnabled; break; case AbstractProjectItem::FolderUpItem: return Qt::ItemIsEnabled; break; default: return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable; } } // cppcheck-suppress unusedFunction bool ProjectItemModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) { Q_UNUSED(row) Q_UNUSED(column) if (action == Qt::IgnoreAction) { return true; } if (data->hasUrls()) { emit itemDropped(data->urls(), parent); return true; } if (data->hasFormat(QStringLiteral("kdenlive/producerslist"))) { // Dropping an Bin item const QStringList ids = QString(data->data(QStringLiteral("kdenlive/producerslist"))).split(QLatin1Char(';')); if (ids.constFirst().contains(QLatin1Char('/'))) { // subclip zone QStringList clipData = ids.constFirst().split(QLatin1Char('/')); if (clipData.length() >= 3) { QString id; return requestAddBinSubClip(id, clipData.at(1).toInt(), clipData.at(2).toInt(), QString(), clipData.at(0)); } else { // error, malformed clip zone, abort return false; } } else { emit itemDropped(ids, parent); } return true; } if (data->hasFormat(QStringLiteral("kdenlive/effect"))) { // Dropping effect on a Bin item QStringList effectData; effectData << QString::fromUtf8(data->data(QStringLiteral("kdenlive/effect"))); QStringList source = QString::fromUtf8(data->data(QStringLiteral("kdenlive/effectsource"))).split(QLatin1Char('-')); effectData << source; emit effectDropped(effectData, parent); return true; } if (data->hasFormat(QStringLiteral("kdenlive/clip"))) { const QStringList list = QString(data->data(QStringLiteral("kdenlive/clip"))).split(QLatin1Char(';')); QString id; return requestAddBinSubClip(id, list.at(1).toInt(), list.at(2).toInt(), QString(), list.at(0)); } return false; } QVariant ProjectItemModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal && role == Qt::DisplayRole) { QVariant columnName; switch (section) { case 0: columnName = i18n("Name"); break; case 1: columnName = i18n("Date"); break; case 2: columnName = i18n("Description"); break; default: columnName = i18n("Unknown"); break; } return columnName; } return QAbstractItemModel::headerData(section, orientation, role); } int ProjectItemModel::columnCount(const QModelIndex &parent) const { if (parent.isValid()) { return getBinItemByIndex(parent)->supportedDataCount(); } return std::static_pointer_cast(rootItem)->supportedDataCount(); } // cppcheck-suppress unusedFunction Qt::DropActions ProjectItemModel::supportedDropActions() const { return Qt::CopyAction | Qt::MoveAction; } QStringList ProjectItemModel::mimeTypes() const { QStringList types; types << QStringLiteral("kdenlive/producerslist") << QStringLiteral("text/uri-list") << QStringLiteral("kdenlive/clip") << QStringLiteral("kdenlive/effect"); return types; } QMimeData *ProjectItemModel::mimeData(const QModelIndexList &indices) const { // Mime data is a list of id's separated by ';'. // Clip ids are represented like: 2 (where 2 is the clip's id) // Clip zone ids are represented like: 2/10/200 (where 2 is the clip's id, 10 and 200 are in and out points) // Folder ids are represented like: #2 (where 2 is the folder's id) auto *mimeData = new QMimeData(); QStringList list; size_t duration = 0; for (int i = 0; i < indices.count(); i++) { QModelIndex ix = indices.at(i); if (!ix.isValid() || ix.column() != 0) { continue; } std::shared_ptr item = getBinItemByIndex(ix); AbstractProjectItem::PROJECTITEMTYPE type = item->itemType(); if (type == AbstractProjectItem::ClipItem) { ClipType::ProducerType cType = item->clipType(); QString dragId = item->clipId(); if ((cType == ClipType::AV || cType == ClipType::Playlist)) { switch (m_dragType) { case PlaylistState::AudioOnly: dragId.prepend(QLatin1Char('A')); break; case PlaylistState::VideoOnly: dragId.prepend(QLatin1Char('V')); break; default: break; } } list << dragId; duration += (std::static_pointer_cast(item))->frameDuration(); } else if (type == AbstractProjectItem::SubClipItem) { QPoint p = item->zone(); list << std::static_pointer_cast(item)->getMasterClip()->clipId() + QLatin1Char('/') + QString::number(p.x()) + QLatin1Char('/') + QString::number(p.y()); } else if (type == AbstractProjectItem::FolderItem) { list << "#" + item->clipId(); } } if (!list.isEmpty()) { QByteArray data; data.append(list.join(QLatin1Char(';')).toUtf8()); mimeData->setData(QStringLiteral("kdenlive/producerslist"), data); mimeData->setText(QString::number(duration)); } return mimeData; } void ProjectItemModel::onItemUpdated(const std::shared_ptr &item, int role) { auto tItem = std::static_pointer_cast(item); auto ptr = tItem->parentItem().lock(); if (ptr) { auto index = getIndexFromItem(tItem); emit dataChanged(index, index, {role}); } } void ProjectItemModel::onItemUpdated(const QString &binId, int role) { std::shared_ptr item = getItemByBinId(binId); if (item) { onItemUpdated(item, role); } } std::shared_ptr ProjectItemModel::getClipByBinID(const QString &binId) { if (binId.contains(QLatin1Char('_'))) { return getClipByBinID(binId.section(QLatin1Char('_'), 0, 0)); } for (const auto &clip : m_allItems) { auto c = std::static_pointer_cast(clip.second.lock()); if (c->itemType() == AbstractProjectItem::ClipItem && c->clipId() == binId) { return std::static_pointer_cast(c); } } return nullptr; } +const QList ProjectItemModel::getAudioLevelsByBinID(const QString &binId) +{ + if (binId.contains(QLatin1Char('_'))) { + return getAudioLevelsByBinID(binId.section(QLatin1Char('_'), 0, 0)); + } + for (const auto &clip : m_allItems) { + auto c = std::static_pointer_cast(clip.second.lock()); + if (c->itemType() == AbstractProjectItem::ClipItem && c->clipId() == binId) { + return std::static_pointer_cast(c)->audioFrameCache; + } + } + return QList (); +} + bool ProjectItemModel::hasClip(const QString &binId) { return getClipByBinID(binId) != nullptr; } std::shared_ptr ProjectItemModel::getFolderByBinId(const QString &binId) { for (const auto &clip : m_allItems) { auto c = std::static_pointer_cast(clip.second.lock()); if (c->itemType() == AbstractProjectItem::FolderItem && c->clipId() == binId) { return std::static_pointer_cast(c); } } return nullptr; } const QString ProjectItemModel::getFolderIdByName(const QString &folderName) { for (const auto &clip : m_allItems) { auto c = std::static_pointer_cast(clip.second.lock()); if (c->itemType() == AbstractProjectItem::FolderItem && c->name() == folderName) { return c->clipId(); } } return QString(); } std::shared_ptr ProjectItemModel::getItemByBinId(const QString &binId) { for (const auto &clip : m_allItems) { auto c = std::static_pointer_cast(clip.second.lock()); if (c->clipId() == binId) { return c; } } return nullptr; } void ProjectItemModel::setBinEffectsEnabled(bool enabled) { return std::static_pointer_cast(rootItem)->setBinEffectsEnabled(enabled); } QStringList ProjectItemModel::getEnclosingFolderInfo(const QModelIndex &index) const { QStringList noInfo; noInfo << QString::number(-1); noInfo << QString(); if (!index.isValid()) { return noInfo; } std::shared_ptr currentItem = getBinItemByIndex(index); auto folder = currentItem->getEnclosingFolder(true); if ((folder == nullptr) || folder == rootItem) { return noInfo; } QStringList folderInfo; folderInfo << currentItem->clipId(); folderInfo << currentItem->name(); return folderInfo; } void ProjectItemModel::clean() { std::vector> toDelete; toDelete.reserve((size_t)rootItem->childCount()); for (int i = 0; i < rootItem->childCount(); ++i) { toDelete.push_back(std::static_pointer_cast(rootItem->child(i))); } Fun undo = []() { return true; }; Fun redo = []() { return true; }; for (const auto &child : toDelete) { requestBinClipDeletion(child, undo, redo); } Q_ASSERT(rootItem->childCount() == 0); m_nextId = 1; m_fileWatcher->clear(); } std::shared_ptr ProjectItemModel::getRootFolder() const { return std::static_pointer_cast(rootItem); } void ProjectItemModel::loadSubClips(const QString &id, const QMap &dataMap) { Fun undo = []() { return true; }; Fun redo = []() { return true; }; loadSubClips(id, dataMap, undo, redo); } void ProjectItemModel::loadSubClips(const QString &id, const QMap &dataMap, Fun &undo, Fun &redo) { std::shared_ptr clip = getClipByBinID(id); if (!clip) { return; } QMapIterator i(dataMap); QList missingThumbs; int maxFrame = clip->duration().frames(pCore->getCurrentFps()) - 1; while (i.hasNext()) { i.next(); if (!i.value().contains(QLatin1Char(';'))) { // Problem, the zone has no in/out points continue; } int in = i.value().section(QLatin1Char(';'), 0, 0).toInt(); int out = i.value().section(QLatin1Char(';'), 1, 1).toInt(); if (maxFrame > 0) { out = qMin(out, maxFrame); } QString subId; requestAddBinSubClip(subId, in, out, i.key(), id, undo, redo); } } std::shared_ptr ProjectItemModel::getBinItemByIndex(const QModelIndex &index) const { return std::static_pointer_cast(getItemById((int)index.internalId())); } bool ProjectItemModel::requestBinClipDeletion(const std::shared_ptr &clip, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); Q_ASSERT(clip); if (!clip) return false; int parentId = -1; if (auto ptr = clip->parent()) parentId = ptr->getId(); clip->selfSoftDelete(undo, redo); int id = clip->getId(); Fun operation = removeItem_lambda(id); Fun reverse = addItem_lambda(clip, parentId); bool res = operation(); if (res) { UPDATE_UNDO_REDO(operation, reverse, undo, redo); } return res; } void ProjectItemModel::registerItem(const std::shared_ptr &item) { auto clip = std::static_pointer_cast(item); m_binPlaylist->manageBinItemInsertion(clip); AbstractTreeModel::registerItem(item); if (clip->itemType() == AbstractProjectItem::ClipItem) { auto clipItem = std::static_pointer_cast(clip); updateWatcher(clipItem); } } void ProjectItemModel::deregisterItem(int id, TreeItem *item) { auto clip = static_cast(item); m_binPlaylist->manageBinItemDeletion(clip); // TODO : here, we should suspend jobs belonging to the item we delete. They can be restarted if the item is reinserted by undo AbstractTreeModel::deregisterItem(id, item); if (clip->itemType() == AbstractProjectItem::ClipItem) { auto clipItem = static_cast(clip); m_fileWatcher->removeFile(clipItem->clipId()); } } int ProjectItemModel::getFreeFolderId() { while (!isIdFree(QString::number(++m_nextId))) { }; return m_nextId; } int ProjectItemModel::getFreeClipId() { while (!isIdFree(QString::number(++m_nextId))) { }; return m_nextId; } bool ProjectItemModel::addItem(const std::shared_ptr &item, const QString &parentId, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); std::shared_ptr parentItem = getItemByBinId(parentId); if (!parentItem) { qCDebug(KDENLIVE_LOG) << " / / ERROR IN PARENT FOLDER"; return false; } if (item->itemType() == AbstractProjectItem::ClipItem && parentItem->itemType() != AbstractProjectItem::FolderItem) { qCDebug(KDENLIVE_LOG) << " / / ERROR when inserting clip: a clip should be inserted in a folder"; return false; } if (item->itemType() == AbstractProjectItem::SubClipItem && parentItem->itemType() != AbstractProjectItem::ClipItem) { qCDebug(KDENLIVE_LOG) << " / / ERROR when inserting subclip: a subclip should be inserted in a clip"; return false; } Fun operation = addItem_lambda(item, parentItem->getId()); int itemId = item->getId(); Fun reverse = removeItem_lambda(itemId); bool res = operation(); Q_ASSERT(item->isInModel()); if (res) { UPDATE_UNDO_REDO(operation, reverse, undo, redo); } return res; } bool ProjectItemModel::requestAddFolder(QString &id, const QString &name, const QString &parentId, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); if (!id.isEmpty() && !isIdFree(id)) { id = QString(); } if (id.isEmpty()) { id = QString::number(getFreeFolderId()); } std::shared_ptr new_folder = ProjectFolder::construct(id, name, std::static_pointer_cast(shared_from_this())); return addItem(new_folder, parentId, undo, redo); } bool ProjectItemModel::requestAddBinClip(QString &id, const QDomElement &description, const QString &parentId, Fun &undo, Fun &redo) { qDebug() << "/////////// requestAddBinClip" << parentId; QWriteLocker locker(&m_lock); if (id.isEmpty()) { id = Xml::getTagContentByAttribute(description, QStringLiteral("property"), QStringLiteral("name"), QStringLiteral("kdenlive:id"), QStringLiteral("-1")); if (id == QStringLiteral("-1") || !isIdFree(id)) { id = QString::number(getFreeClipId()); } } Q_ASSERT(!id.isEmpty() && isIdFree(id)); qDebug() << "/////////// found id" << id; std::shared_ptr new_clip = ProjectClip::construct(id, description, m_blankThumb, std::static_pointer_cast(shared_from_this())); qDebug() << "/////////// constructed "; bool res = addItem(new_clip, parentId, undo, redo); qDebug() << "/////////// added " << res; if (res) { int loadJob = pCore->jobManager()->startJob({id}, -1, QString(), description); pCore->jobManager()->startJob({id}, loadJob, QString(), 150, 0, true); pCore->jobManager()->startJob({id}, loadJob, QString()); } return res; } bool ProjectItemModel::requestAddBinClip(QString &id, const QDomElement &description, const QString &parentId, const QString &undoText) { Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool res = requestAddBinClip(id, description, parentId, undo, redo); if (res) { pCore->pushUndo(undo, redo, undoText.isEmpty() ? i18n("Add bin clip") : undoText); } return res; } bool ProjectItemModel::requestAddBinClip(QString &id, const std::shared_ptr &producer, const QString &parentId, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); if (id.isEmpty()) { id = QString::number(producer->get_int("kdenlive:id")); if (!isIdFree(id)) { id = QString::number(getFreeClipId()); } } Q_ASSERT(!id.isEmpty() && isIdFree(id)); std::shared_ptr new_clip = ProjectClip::construct(id, m_blankThumb, std::static_pointer_cast(shared_from_this()), producer); bool res = addItem(new_clip, parentId, undo, redo); if (res) { int blocking = pCore->jobManager()->getBlockingJobId(id, AbstractClipJob::LOADJOB); pCore->jobManager()->startJob({id}, blocking, QString(), 150, -1, true); pCore->jobManager()->startJob({id}, blocking, QString()); } return res; } bool ProjectItemModel::requestAddBinSubClip(QString &id, int in, int out, const QString &zoneName, const QString &parentId, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); if (id.isEmpty()) { id = QString::number(getFreeClipId()); } Q_ASSERT(!id.isEmpty() && isIdFree(id)); QString subId = parentId; if (subId.startsWith(QLatin1Char('A')) || subId.startsWith(QLatin1Char('V'))) { subId.remove(0, 1); } auto clip = getClipByBinID(subId); Q_ASSERT(clip->itemType() == AbstractProjectItem::ClipItem); auto tc = pCore->currentDoc()->timecode().getDisplayTimecodeFromFrames(in, KdenliveSettings::frametimecode()); std::shared_ptr new_clip = ProjectSubClip::construct(id, clip, std::static_pointer_cast(shared_from_this()), in, out, tc, zoneName); bool res = addItem(new_clip, subId, undo, redo); if (res) { int parentJob = pCore->jobManager()->getBlockingJobId(subId, AbstractClipJob::LOADJOB); pCore->jobManager()->startJob({id}, parentJob, QString(), 150, -1, true); } return res; } bool ProjectItemModel::requestAddBinSubClip(QString &id, int in, int out, const QString &zoneName, const QString &parentId) { Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool res = requestAddBinSubClip(id, in, out, zoneName, parentId, undo, redo); if (res) { pCore->pushUndo(undo, redo, i18n("Add a sub clip")); } return res; } Fun ProjectItemModel::requestRenameFolder_lambda(const std::shared_ptr &folder, const QString &newName) { int id = folder->getId(); return [this, id, newName]() { auto currentFolder = std::static_pointer_cast(m_allItems[id].lock()); if (!currentFolder) { return false; } currentFolder->setName(newName); m_binPlaylist->manageBinFolderRename(currentFolder); auto index = getIndexFromItem(currentFolder); emit dataChanged(index, index, {AbstractProjectItem::DataName}); return true; }; } bool ProjectItemModel::requestRenameFolder(const std::shared_ptr &folder, const QString &name, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); QString oldName = folder->name(); auto operation = requestRenameFolder_lambda(folder, name); if (operation()) { auto reverse = requestRenameFolder_lambda(folder, oldName); UPDATE_UNDO_REDO(operation, reverse, undo, redo); return true; } return false; } bool ProjectItemModel::requestRenameFolder(std::shared_ptr folder, const QString &name) { Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool res = requestRenameFolder(std::move(folder), name, undo, redo); if (res) { pCore->pushUndo(undo, redo, i18n("Rename Folder")); } return res; } bool ProjectItemModel::requestCleanup() { Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool res = true; std::vector> to_delete; // Iterate to find clips that are not in timeline for (const auto &clip : m_allItems) { auto c = std::static_pointer_cast(clip.second.lock()); if (c->itemType() == AbstractProjectItem::ClipItem && !c->isIncludedInTimeline()) { to_delete.push_back(c); } } // it is important to execute deletion in a separate loop, because otherwise // the iterators of m_allItems get messed up for (const auto &c : to_delete) { res = requestBinClipDeletion(c, undo, redo); if (!res) { bool undone = undo(); Q_ASSERT(undone); return false; } } pCore->pushUndo(undo, redo, i18n("Clean Project")); return true; } std::vector ProjectItemModel::getAllClipIds() const { std::vector result; for (const auto &clip : m_allItems) { auto c = std::static_pointer_cast(clip.second.lock()); if (c->itemType() == AbstractProjectItem::ClipItem) { result.push_back(c->clipId()); } } return result; } QStringList ProjectItemModel::getClipByUrl(const QFileInfo &url) const { QStringList result; for (const auto &clip : m_allItems) { auto c = std::static_pointer_cast(clip.second.lock()); if (c->itemType() == AbstractProjectItem::ClipItem) { if (QFileInfo(std::static_pointer_cast(c)->clipUrl()) == url) { result << c->clipId(); } } } return result; } bool ProjectItemModel::loadFolders(Mlt::Properties &folders) { // At this point, we expect the folders properties to have a name of the form "x.y" where x is the id of the parent folder and y the id of the child. // Note that for root folder, x = -1 // The value of the property is the name of the child folder std::unordered_map> downLinks; // key are parents, value are children std::unordered_map upLinks; // key are children, value are parent std::unordered_map newIds; // we store the correspondence to the new ids std::unordered_map folderNames; newIds[-1] = getRootFolder()->clipId(); if (folders.count() == 0) return true; for (int i = 0; i < folders.count(); i++) { QString folderName = folders.get(i); QString id = folders.get_name(i); int parentId = id.section(QLatin1Char('.'), 0, 0).toInt(); int folderId = id.section(QLatin1Char('.'), 1, 1).toInt(); downLinks[parentId].push_back(folderId); upLinks[folderId] = parentId; folderNames[folderId] = folderName; qDebug() << "Found folder " << folderId << "name = " << folderName << "parent=" << parentId; } // In case there are some non-existant parent, we fall back to root for (const auto &f : downLinks) { if (upLinks.count(f.first) == 0) { upLinks[f.first] = -1; } if (f.first != -1 && downLinks.count(upLinks[f.first]) == 0) { qDebug() << "Warning: parent folder " << upLinks[f.first] << "for folder" << f.first << "is invalid. Folder will be placed in topmost directory."; upLinks[f.first] = -1; } } // We now do a BFS to construct the folders in order Q_ASSERT(downLinks.count(-1) > 0); Fun undo = []() { return true; }; Fun redo = []() { return true; }; std::queue queue; std::unordered_set seen; queue.push(-1); while (!queue.empty()) { int current = queue.front(); seen.insert(current); queue.pop(); if (current != -1) { QString id = QString::number(current); bool res = requestAddFolder(id, folderNames[current], newIds[upLinks[current]], undo, redo); if (!res) { bool undone = undo(); Q_ASSERT(undone); return false; } newIds[current] = id; } for (int c : downLinks[current]) { queue.push(c); } } return true; } bool ProjectItemModel::isIdFree(const QString &id) const { for (const auto &clip : m_allItems) { auto c = std::static_pointer_cast(clip.second.lock()); if (c->clipId() == id) { return false; } } return true; } void ProjectItemModel::loadBinPlaylist(Mlt::Tractor *documentTractor, Mlt::Tractor *modelTractor, std::unordered_map &binIdCorresp) { clean(); Mlt::Properties retainList((mlt_properties)documentTractor->get_data("xml_retain")); qDebug() << "Loading bin playlist..."; if (retainList.is_valid()) { qDebug() << "retain is valid"; Mlt::Playlist playlist((mlt_playlist)retainList.get_data(BinPlaylist::binPlaylistId.toUtf8().constData())); if (playlist.is_valid() && playlist.type() == playlist_type) { qDebug() << "playlist is valid"; // Load bin clips qDebug() << "init bin"; // Load folders Mlt::Properties folderProperties; Mlt::Properties playlistProps(playlist.get_properties()); folderProperties.pass_values(playlistProps, "kdenlive:folder."); loadFolders(folderProperties); // Read notes QString notes = playlistProps.get("kdenlive:documentnotes"); pCore->projectManager()->setDocumentNotes(notes); Fun undo = []() { return true; }; Fun redo = []() { return true; }; qDebug() << "Found " << playlist.count() << "clips"; int max = playlist.count(); for (int i = 0; i < max; i++) { QScopedPointer prod(playlist.get_clip(i)); std::shared_ptr producer(new Mlt::Producer(prod->parent())); qDebug() << "dealing with bin clip" << i; if (producer->is_blank() || !producer->is_valid()) { qDebug() << "producer is not valid or blank"; continue; } QString id = qstrdup(producer->get("kdenlive:id")); QString parentId = qstrdup(producer->get("kdenlive:folderid")); if (parentId.isEmpty()) { parentId = QStringLiteral("-1"); } qDebug() << "clip id" << id; if (id.contains(QLatin1Char('_'))) { // TODO refac ? /* // This is a track producer QString mainId = id.section(QLatin1Char('_'), 0, 0); // QString track = id.section(QStringLiteral("_"), 1, 1); if (m_clipList.contains(mainId)) { // The controller for this track producer already exists } else { // Create empty controller for this clip requestClipInfo info; info.imageHeight = 0; info.clipId = id; info.replaceProducer = true; emit slotProducerReady(info, ClipController::mediaUnavailable); } */ } else { QString newId = isIdFree(id) ? id : QString::number(getFreeClipId()); producer->set("_kdenlive_processed", 1); requestAddBinClip(newId, producer, parentId, undo, redo); binIdCorresp[id] = newId; qDebug() << "Loaded clip " << id << "under id" << newId; } } } } m_binPlaylist->setRetainIn(modelTractor); } /** @brief Save document properties in MLT's bin playlist */ void ProjectItemModel::saveDocumentProperties(const QMap &props, const QMap &metadata, std::shared_ptr guideModel) { m_binPlaylist->saveDocumentProperties(props, metadata, std::move(guideModel)); } void ProjectItemModel::saveProperty(const QString &name, const QString &value) { m_binPlaylist->saveProperty(name, value); } QMap ProjectItemModel::getProxies(const QString &root) { return m_binPlaylist->getProxies(root); } void ProjectItemModel::reloadClip(const QString &binId) { std::shared_ptr clip = getClipByBinID(binId); if (clip) { clip->reloadProducer(); } } void ProjectItemModel::setClipWaiting(const QString &binId) { std::shared_ptr clip = getClipByBinID(binId); if (clip) { clip->setClipStatus(AbstractProjectItem::StatusWaiting); } } void ProjectItemModel::setClipInvalid(const QString &binId) { std::shared_ptr clip = getClipByBinID(binId); if (clip) { clip->setClipStatus(AbstractProjectItem::StatusMissing); // TODO: set producer as blank invalid } } void ProjectItemModel::updateWatcher(const std::shared_ptr &clipItem) { if (clipItem->clipType() == ClipType::AV || clipItem->clipType() == ClipType::Audio || clipItem->clipType() == ClipType::Image || clipItem->clipType() == ClipType::Video || clipItem->clipType() == ClipType::Playlist || clipItem->clipType() == ClipType::TextTemplate) { m_fileWatcher->removeFile(clipItem->clipId()); m_fileWatcher->addFile(clipItem->clipId(), clipItem->clipUrl()); } } void ProjectItemModel::setDragType(PlaylistState::ClipState type) { m_dragType = type; } int ProjectItemModel::clipsCount() const { return m_binPlaylist->count(); } diff --git a/src/bin/projectitemmodel.h b/src/bin/projectitemmodel.h index 5bfc6a710..fc1a6fad9 100644 --- a/src/bin/projectitemmodel.h +++ b/src/bin/projectitemmodel.h @@ -1,260 +1,262 @@ /* Copyright (C) 2012 Till Theato Copyright (C) 2014 Jean-Baptiste Mardelle Copyright (C) 2017 Nicolas Carion 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 PROJECTITEMMODEL_H #define PROJECTITEMMODEL_H #include "abstractmodel/abstracttreemodel.hpp" #include "definitions.h" #include "undohelper.hpp" #include #include #include #include #include class AbstractProjectItem; class BinPlaylist; class FileWatcher; class MarkerListModel; class ProjectClip; class ProjectFolder; namespace Mlt { class Producer; class Properties; class Tractor; } // namespace Mlt /** * @class ProjectItemModel * @brief Acts as an adaptor to be able to use BinModel with item views. */ class ProjectItemModel : public AbstractTreeModel { Q_OBJECT protected: explicit ProjectItemModel(QObject *parent); public: static std::shared_ptr construct(QObject *parent = nullptr); ~ProjectItemModel() override; friend class ProjectClip; /** @brief Returns a clip from the hierarchy, given its id */ std::shared_ptr getClipByBinID(const QString &binId); + /** @brief Returns audio levels for a clip from its id */ + const QList getAudioLevelsByBinID(const QString &binId); /** @brief Returns a list of clips using the given url */ QStringList getClipByUrl(const QFileInfo &url) const; /** @brief Helper to check whether a clip with a given id exists */ bool hasClip(const QString &binId); /** @brief Gets a folder by its id. If none is found, nullptr is returned */ std::shared_ptr getFolderByBinId(const QString &binId); /** @brief Gets a id folder by its name. If none is found, empty string returned */ const QString getFolderIdByName(const QString &folderName); /** @brief Gets any item by its id. */ std::shared_ptr getItemByBinId(const QString &binId); /** @brief This function change the global enabled state of the bin effects */ void setBinEffectsEnabled(bool enabled); /** @brief Returns some info about the folder containing the given index */ QStringList getEnclosingFolderInfo(const QModelIndex &index) const; /** @brief Deletes all element and start a fresh model */ void clean(); /** @brief Returns the id of all the clips (excluding folders) */ std::vector getAllClipIds() const; /** @brief Convenience method to access root folder */ std::shared_ptr getRootFolder() const; /** @brief Create the subclips defined in the parent clip. @param id is the id of the parent clip @param data is a definition of the subclips (keys are subclips' names, value are "in:out")*/ void loadSubClips(const QString &id, const QMap &data); void loadSubClips(const QString &id, const QMap &dataMap, Fun &undo, Fun &redo); /* @brief Convenience method to retrieve a pointer to an element given its index */ std::shared_ptr getBinItemByIndex(const QModelIndex &index) const; /* @brief Load the folders given the property containing them */ bool loadFolders(Mlt::Properties &folders); /* @brief Parse a bin playlist from the document tractor and reconstruct the tree */ void loadBinPlaylist(Mlt::Tractor *documentTractor, Mlt::Tractor *modelTractor, std::unordered_map &binIdCorresp); /** @brief Save document properties in MLT's bin playlist */ void saveDocumentProperties(const QMap &props, const QMap &metadata, std::shared_ptr guideModel); /** @brief Save a property to main bin */ void saveProperty(const QString &name, const QString &value); /** @brief Returns item data depending on role requested */ QVariant data(const QModelIndex &index, int role) const override; /** @brief Called when user edits an item */ bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; /** @brief Allow selection and drag & drop */ Qt::ItemFlags flags(const QModelIndex &index) const override; /** @brief Returns column names in case we want to use columns in QTreeView */ QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; /** @brief Mandatory reimplementation from QAbstractItemModel */ int columnCount(const QModelIndex &parent = QModelIndex()) const override; /** @brief Returns the MIME type used for Drag actions */ QStringList mimeTypes() const override; /** @brief Create data that will be used for Drag events */ QMimeData *mimeData(const QModelIndexList &indices) const override; /** @brief Set size for thumbnails */ void setIconSize(QSize s); bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) override; Qt::DropActions supportedDropActions() const override; /* @brief Request deletion of a bin clip from the project bin @param clip : pointer to the clip to delete @param undo,redo: lambdas that are updated to accumulate operation. */ bool requestBinClipDeletion(const std::shared_ptr &clip, Fun &undo, Fun &redo); /* @brief Request creation of a bin folder @param id Id of the requested bin. If this is empty or invalid (already used, for example), it will be used as a return parameter to give the automatic bin id used. @param name Name of the folder @param parentId Bin id of the parent folder @param undo,redo: lambdas that are updated to accumulate operation. */ bool requestAddFolder(QString &id, const QString &name, const QString &parentId, Fun &undo, Fun &redo); /* @brief Request creation of a bin clip @param id Id of the requested bin. If this is empty, it will be used as a return parameter to give the automatic bin id used. @param description Xml description of the clip @param parentId Bin id of the parent folder @param undo,redo: lambdas that are updated to accumulate operation. */ bool requestAddBinClip(QString &id, const QDomElement &description, const QString &parentId, Fun &undo, Fun &redo); bool requestAddBinClip(QString &id, const QDomElement &description, const QString &parentId, const QString &undoText = QString()); /* @brief This is the addition function when we already have a producer for the clip*/ bool requestAddBinClip(QString &id, const std::shared_ptr &producer, const QString &parentId, Fun &undo, Fun &redo); /* @brief Create a subClip @param id Id of the requested bin. If this is empty, it will be used as a return parameter to give the automatic bin id used. @param parentId Bin id of the parent clip @param in,out : zone that corresponds to the subclip @param undo,redo: lambdas that are updated to accumulate operation. */ bool requestAddBinSubClip(QString &id, int in, int out, const QString &zoneName, const QString &parentId, Fun &undo, Fun &redo); bool requestAddBinSubClip(QString &id, int in, int out, const QString &zoneName, const QString &parentId); /* @brief Request that a folder's name is changed @param clip : pointer to the folder to rename @param name: new name of the folder @param undo,redo: lambdas that are updated to accumulate operation. */ bool requestRenameFolder(const std::shared_ptr &folder, const QString &name, Fun &undo, Fun &redo); /* Same functions but pushes the undo object directly */ bool requestRenameFolder(std::shared_ptr folder, const QString &name); /* @brief Request that the unused clips are deleted */ bool requestCleanup(); /* @brief Retrieves the next id available for attribution to a folder */ int getFreeFolderId(); /* @brief Retrieves the next id available for attribution to a clip */ int getFreeClipId(); /** @brief Retrieve a list of proxy/original urls */ QMap getProxies(const QString &root); /** @brief Request that the producer of a given clip is reloaded */ void reloadClip(const QString &binId); /** @brief Set the status of the clip to "waiting". This happens when the corresponding file has changed*/ void setClipWaiting(const QString &binId); void setClipInvalid(const QString &binId); /** @brief Number of clips in the bin playlist */ int clipsCount() const; protected: /* @brief Register the existence of a new element */ void registerItem(const std::shared_ptr &item) override; /* @brief Deregister the existence of a new element*/ void deregisterItem(int id, TreeItem *item) override; /* @brief Helper function to generate a lambda that rename a folder */ Fun requestRenameFolder_lambda(const std::shared_ptr &folder, const QString &newName); /* @brief Helper function to add a given item to the tree */ bool addItem(const std::shared_ptr &item, const QString &parentId, Fun &undo, Fun &redo); /* @brief Function to be called when the url of a clip changes */ void updateWatcher(const std::shared_ptr &item); public slots: /** @brief An item in the list was modified, notify */ void onItemUpdated(const std::shared_ptr &item, int role); void onItemUpdated(const QString &binId, int role); /** @brief Check whether a given id is currently used or not*/ bool isIdFree(const QString &id) const; void setDragType(PlaylistState::ClipState type); private: /** @brief Return reference to column specific data */ int mapToColumn(int column) const; mutable QReadWriteLock m_lock; // This is a lock that ensures safety in case of concurrent access std::unique_ptr m_binPlaylist; std::unique_ptr m_fileWatcher; int m_nextId; QIcon m_blankThumb; PlaylistState::ClipState m_dragType; signals: // thumbs of the given clip were modified, request update of the monitor if need be void refreshAudioThumbs(const QString &id); void refreshClip(const QString &id); void emitMessage(const QString &, int, MessageType); void updateTimelineProducers(const QString &id, const QMap &passProperties); void refreshPanel(const QString &id); void requestAudioThumbs(const QString &id, long duration); // TODO void markersNeedUpdate(const QString &id, const QList &); void itemDropped(const QStringList &, const QModelIndex &); void itemDropped(const QList &, const QModelIndex &); void effectDropped(const QStringList &, const QModelIndex &); void addClipCut(const QString &, int, int); }; #endif diff --git a/src/jobs/audiothumbjob.cpp b/src/jobs/audiothumbjob.cpp index 6847a89cb..6b63827b8 100644 --- a/src/jobs/audiothumbjob.cpp +++ b/src/jobs/audiothumbjob.cpp @@ -1,343 +1,343 @@ /*************************************************************************** * Copyright (C) 2017 by Nicolas Carion * * 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 "audiothumbjob.hpp" #include "bin/projectclip.h" #include "bin/projectitemmodel.h" #include "core.h" #include "doc/kdenlivedoc.h" #include "doc/kthumb.h" #include "kdenlivesettings.h" #include "klocalizedstring.h" #include "lib/audio/audioStreamInfo.h" #include "macros.hpp" #include "utils/thumbnailcache.hpp" #include #include #include #include #include #include AudioThumbJob::AudioThumbJob(const QString &binId) : AbstractClipJob(AUDIOTHUMBJOB, binId) , m_ffmpegProcess(nullptr) { } const QString AudioThumbJob::getDescription() const { return i18n("Extracting audio thumb from clip %1", m_clipId); } bool AudioThumbJob::computeWithMlt() { m_audioLevels.clear(); m_errorMessage.clear(); // MLT audio thumbs: slower but safer QString service = m_prod->get("mlt_service"); if (service == QLatin1String("avformat-novalidate")) { service = QStringLiteral("avformat"); } else if (service.startsWith(QLatin1String("xml"))) { service = QStringLiteral("xml-nogl"); } QScopedPointer audioProducer(new Mlt::Producer(*m_prod->profile(), service.toUtf8().constData(), m_prod->get("resource"))); if (!audioProducer->is_valid()) { m_errorMessage.append(i18n("Audio thumbs: cannot open file %1", m_prod->get("resource"))); return false; } audioProducer->set("video_index", "-1"); Mlt::Filter chans(*m_prod->profile(), "audiochannels"); Mlt::Filter converter(*m_prod->profile(), "audioconvert"); Mlt::Filter levels(*m_prod->profile(), "audiolevel"); audioProducer->attach(chans); audioProducer->attach(converter); audioProducer->attach(levels); int last_val = 0; double framesPerSecond = audioProducer->get_fps(); mlt_audio_format audioFormat = mlt_audio_s16; QStringList keys; keys.reserve(m_channels); for (int i = 0; i < m_channels; i++) { keys << "meta.media.audio_level." + QString::number(i); } for (int z = 0; z < m_lengthInFrames; ++z) { int val = (int)(100.0 * z / m_lengthInFrames); if (last_val != val) { emit jobProgress(val); last_val = val; } QScopedPointer mltFrame(audioProducer->get_frame()); if ((mltFrame != nullptr) && mltFrame->is_valid() && (mltFrame->get_int("test_audio") == 0)) { int samples = mlt_sample_calculator(float(framesPerSecond), m_frequency, z); mltFrame->get_audio(audioFormat, m_frequency, m_channels, samples); for (int channel = 0; channel < m_channels; ++channel) { double level = 256 * qMin(mltFrame->get_double(keys.at(channel).toUtf8().constData()) * 0.9, 1.0); m_audioLevels << level; } } else if (!m_audioLevels.isEmpty()) { for (int channel = 0; channel < m_channels; channel++) { m_audioLevels << m_audioLevels.last(); } } } m_done = true; return true; } bool AudioThumbJob::computeWithFFMPEG() { m_audioLevels.clear(); QStringList args; std::vector> channelFiles; for (int i = 0; i < m_channels; i++) { std::unique_ptr channelTmpfile(new QTemporaryFile()); if (!channelTmpfile->open()) { m_errorMessage.append(i18n("Audio thumbs: cannot create temporary file, check disk space and permissions\n")); return false; } channelTmpfile->close(); channelFiles.emplace_back(std::move(channelTmpfile)); } args << QStringLiteral("-i") << QUrl::fromLocalFile(m_prod->get("resource")).toLocalFile(); // Output progress info args << QStringLiteral("-progress"); #ifdef Q_OS_WIN args << QStringLiteral("-"); #else args << QStringLiteral("/dev/stdout"); #endif bool isFFmpeg = KdenliveSettings::ffmpegpath().contains(QLatin1String("ffmpeg")); args << QStringLiteral("-filter_complex:a"); if (m_channels == 1) { //TODO: this does not correcty generate the correct stream data args << QStringLiteral("aformat=channel_layouts=mono,%1=100").arg(isFFmpeg ? "aresample=async" : "sample_rates"); args << QStringLiteral("-map") << QStringLiteral("0:a%1").arg(m_audioStream > 0 ? ":" + QString::number(m_audioStream) : QString()) << QStringLiteral("-c:a") << QStringLiteral("pcm_s16le") << QStringLiteral("-y") << QStringLiteral("-f") << QStringLiteral("data") << channelFiles[0]->fileName(); } else { QString aformat = QStringLiteral("[0:a%1]%2=100,channelsplit=channel_layout=%3") .arg(m_audioStream > 0 ? ":" + QString::number(m_audioStream) : QString()) .arg(isFFmpeg ? "aresample=async" : "aformat=sample_rates=") .arg(m_channels > 2 ? "5.1" : "stereo"); for (int i = 0; i < m_channels; ++i) { aformat.append(QStringLiteral("[0:%1]").arg(i)); } args << aformat; for (int i = 0; i < m_channels; i++) { // Channel 1 args << QStringLiteral("-map") << QStringLiteral("[0:%1]").arg(i) << QStringLiteral("-c:a") << QStringLiteral("pcm_s16le") << QStringLiteral("-y") << QStringLiteral("-f") << QStringLiteral("data") << channelFiles[size_t(i)]->fileName(); } } m_ffmpegProcess = new QProcess; m_ffmpegProcess->start(KdenliveSettings::ffmpegpath(), args); connect(m_ffmpegProcess, &QProcess::readyReadStandardOutput, this, &AudioThumbJob::updateFfmpegProgress); m_ffmpegProcess->waitForFinished(-1); if (m_ffmpegProcess->exitStatus() != QProcess::CrashExit) { int dataSize = 0; std::vector rawChannels; std::vector sourceChannels; for (auto &channelFile : channelFiles) { channelFile->open(); sourceChannels.emplace_back(channelFile->readAll()); QByteArray &res = sourceChannels.back(); channelFile->close(); if (dataSize == 0) { dataSize = res.size(); } if (res.isEmpty() || res.size() != dataSize) { // Something went wrong, abort m_errorMessage.append(i18n("Audio thumbs: error reading audio thumbnail created with FFmpeg\n")); return false; } rawChannels.emplace_back((const qint16 *)res.constData()); } int progress = 0; std::vector channelsData; double offset = (double)dataSize / (2.0 * m_lengthInFrames); int intraOffset = 1; if (offset > 1000) { intraOffset = offset / 60; } else if (offset > 250) { intraOffset = offset / 10; } double factor = 800.0 / 32768; for (int i = 0; i < m_lengthInFrames; i++) { channelsData.resize((size_t)rawChannels.size()); std::fill(channelsData.begin(), channelsData.end(), 0); int pos = (int)(i * offset); int steps = 0; for (int j = 0; j < (int)offset && (pos + j < dataSize); j += intraOffset) { steps++; for (size_t k = 0; k < rawChannels.size(); k++) { channelsData[k] += abs(rawChannels[k][pos + j]); } } for (long &k : channelsData) { if (steps != 0) { k /= steps; } m_audioLevels << (int)((double)k * factor); } int p = 80 + (i * 20 / m_lengthInFrames); if (p != progress) { emit jobProgress(p); progress = p; } } m_done = true; return true; } QString err = m_ffmpegProcess->readAllStandardError(); delete m_ffmpegProcess; // m_errorMessage += err; // m_errorMessage.append(i18n("Failed to create FFmpeg audio thumbnails, we now try to use MLT")); qWarning() << "Failed to create FFmpeg audio thumbs:\n" << err << "\n---------------------"; return false; } void AudioThumbJob::updateFfmpegProgress() { QString result = m_ffmpegProcess->readAllStandardOutput(); const QStringList lines = result.split(QLatin1Char('\n')); for (const QString &data : lines) { if (data.startsWith(QStringLiteral("out_time_ms"))) { double ms = data.section(QLatin1Char('='), 1).toDouble(); emit jobProgress((int)(ms / m_binClip->duration().ms() / 10)); } else { m_logDetails += data + QStringLiteral("\n"); } } } bool AudioThumbJob::startJob() { if (m_done) { return true; } m_binClip = pCore->projectItemModel()->getClipByBinID(m_clipId); if (m_binClip->audioChannels() == 0 || m_binClip->audioThumbCreated()) { // nothing to do m_done = true; m_successful = true; return true; } m_prod = m_binClip->originalProducer(); m_frequency = m_binClip->audioInfo()->samplingRate(); m_frequency = m_frequency <= 0 ? 48000 : m_frequency; m_channels = m_binClip->audioInfo()->channels(); m_channels = m_channels <= 0 ? 2 : m_channels; m_lengthInFrames = m_prod->get_length(); m_audioStream = m_binClip->audioInfo()->ffmpeg_audio_index(); if ((m_prod == nullptr) || !m_prod->is_valid()) { m_errorMessage.append(i18n("Audio thumbs: cannot open project file %1", m_binClip->url())); m_done = true; m_successful = false; return false; } m_cachePath = m_binClip->getAudioThumbPath(); // checking for cached thumbs QImage image(m_cachePath); if (!image.isNull()) { // convert cached image int n = image.width() * image.height(); for (int i = 0; i < n; i++) { QRgb p = image.pixel(i / m_channels, i % m_channels); m_audioLevels << qRed(p); m_audioLevels << qGreen(p); m_audioLevels << qBlue(p); m_audioLevels << qAlpha(p); } } if (!m_audioLevels.isEmpty()) { m_done = true; m_successful = true; return true; } bool ok = m_binClip->clipType() == ClipType::Playlist ? false : computeWithFFMPEG(); ok = ok ? ok : computeWithMlt(); Q_ASSERT(ok == m_done); if (ok && m_done && !m_audioLevels.isEmpty()) { // Put into an image for caching. int count = m_audioLevels.size(); image = QImage((int)lrint((count + 3) / 4.0 / m_channels), m_channels, QImage::Format_ARGB32); int n = image.width() * image.height(); for (int i = 0; i < n; i++) { QRgb p; if ((4 * i + 3) < count) { - p = qRgba(m_audioLevels.at(4 * i).toInt(), m_audioLevels.at(4 * i + 1).toInt(), m_audioLevels.at(4 * i + 2).toInt(), - m_audioLevels.at(4 * i + 3).toInt()); + p = qRgba(m_audioLevels.at(4 * i), m_audioLevels.at(4 * i + 1), m_audioLevels.at(4 * i + 2), + m_audioLevels.at(4 * i + 3)); } else { - int last = m_audioLevels.last().toInt(); - int r = (4 * i + 0) < count ? m_audioLevels.at(4 * i + 0).toInt() : last; - int g = (4 * i + 1) < count ? m_audioLevels.at(4 * i + 1).toInt() : last; - int b = (4 * i + 2) < count ? m_audioLevels.at(4 * i + 2).toInt() : last; + int last = m_audioLevels.last(); + int r = (4 * i + 0) < count ? m_audioLevels.at(4 * i + 0) : last; + int g = (4 * i + 1) < count ? m_audioLevels.at(4 * i + 1) : last; + int b = (4 * i + 2) < count ? m_audioLevels.at(4 * i + 2) : last; int a = last; p = qRgba(r, g, b, a); } image.setPixel(i / m_channels, i % m_channels, p); } image.save(m_cachePath); m_successful = true; return true; } m_done = true; m_successful = false; return false; } bool AudioThumbJob::commitResult(Fun &undo, Fun &redo) { Q_ASSERT(!m_resultConsumed); if (!m_done) { qDebug() << "ERROR: Trying to consume invalid results"; return false; } m_resultConsumed = true; if (!m_successful) { return false; } - QVariantList old = m_binClip->audioFrameCache; + QList old = m_binClip->audioFrameCache; // note that the image is moved into lambda, it won't be available from this class anymore auto operation = [clip = m_binClip, audio = std::move(m_audioLevels)]() { clip->updateAudioThumbnail(audio); return true; }; auto reverse = [clip = m_binClip, audio = std::move(old)]() { clip->updateAudioThumbnail(audio); return true; }; bool ok = operation(); if (ok) { UPDATE_UNDO_REDO_NOLOCK(operation, reverse, undo, redo); } return ok; } diff --git a/src/jobs/audiothumbjob.hpp b/src/jobs/audiothumbjob.hpp index a3f8d8fdb..8d4450aa3 100644 --- a/src/jobs/audiothumbjob.hpp +++ b/src/jobs/audiothumbjob.hpp @@ -1,73 +1,73 @@ /*************************************************************************** * Copyright (C) 2017 by Nicolas Carion * * 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 . * ***************************************************************************/ #pragma once #include "abstractclipjob.h" #include /* @brief This class represents the job that corresponds to computing the audio thumb of a clip (waveform) */ class ProjectClip; namespace Mlt { class Producer; } class AudioThumbJob : public AbstractClipJob { Q_OBJECT public: /* @brief Extract a thumb for given clip. @param frameNumber is the frame to extract. Leave to -1 for default @param persistent: if true, we will use the persistent cache (for query and saving) */ AudioThumbJob(const QString &binId); const QString getDescription() const override; bool startJob() override; /** @brief This is to be called after the job finished. By design, the job should store the result of the computation but not share it with the rest of the code. This happens when we call commitResult */ bool commitResult(Fun &undo, Fun &redo) override; protected: bool computeWithFFMPEG(); // MLT audio thumbs: slower but safer bool computeWithMlt(); // process the stdout/stderr from ffmpeg void updateFfmpegProgress(); private: std::shared_ptr m_binClip; std::shared_ptr m_prod; QString m_cachePath; bool m_done{false}, m_successful{false}; int m_channels, m_frequency, m_lengthInFrames, m_audioStream; - QVariantList m_audioLevels; + QList m_audioLevels; QProcess *m_ffmpegProcess; }; diff --git a/src/monitor/glwidget.cpp b/src/monitor/glwidget.cpp index a0024795d..747fc5796 100644 --- a/src/monitor/glwidget.cpp +++ b/src/monitor/glwidget.cpp @@ -1,1982 +1,1982 @@ /* * Copyright (c) 2011-2016 Meltytech, LLC * Original author: Dan Dennedy * Modified for Kdenlive: Jean-Baptiste Mardelle * * GL shader based on BSD licensed code from Peter Bengtsson: * http://www.fourcc.org/source/YUV420P-OpenGL-GLSLang.c * * 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. * * 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 #include #include #include #include #include #include #include #include #include "core.h" #include "glwidget.h" #include "kdenlivesettings.h" #include "monitorproxy.h" #include "profiles/profilemodel.hpp" #include "qml/qmlaudiothumb.h" #include "timeline2/view/qml/timelineitems.h" #include #ifndef GL_UNPACK_ROW_LENGTH #ifdef GL_UNPACK_ROW_LENGTH_EXT #define GL_UNPACK_ROW_LENGTH GL_UNPACK_ROW_LENGTH_EXT #else #error GL_UNPACK_ROW_LENGTH undefined #endif #endif #ifdef QT_NO_DEBUG #define check_error(fn) \ { \ } #else #define check_error(fn) \ { \ uint err = fn->glGetError(); \ if (err != GL_NO_ERROR) { \ qCCritical(KDENLIVE_LOG) << "GL error" << hex << err << dec << "at" << __FILE__ << ":" << __LINE__; \ } \ } #endif #ifndef GL_TIMEOUT_IGNORED #define GL_TIMEOUT_IGNORED 0xFFFFFFFFFFFFFFFFull #endif using namespace Mlt; GLWidget::GLWidget(int id, QObject *parent) : QQuickView((QWindow *)parent) , sendFrameForAnalysis(false) , m_glslManager(nullptr) , m_consumer(nullptr) , m_producer(nullptr) , m_id(id) , m_rulerHeight(QFontMetrics(QApplication::font()).lineSpacing() * 0.7) , m_shader(nullptr) , m_initSem(0) , m_analyseSem(1) , m_isInitialized(false) , m_threadStartEvent(nullptr) , m_threadStopEvent(nullptr) , m_threadCreateEvent(nullptr) , m_threadJoinEvent(nullptr) , m_displayEvent(nullptr) , m_frameRenderer(nullptr) , m_projectionLocation(0) , m_modelViewLocation(0) , m_vertexLocation(0) , m_texCoordLocation(0) , m_colorspaceLocation(0) , m_zoom(1.0f) , m_sendFrame(false) , m_isZoneMode(false) , m_isLoopMode(false) , m_offset(QPoint(0, 0)) , m_audioWaveDisplayed(false) , m_fbo(nullptr) , m_shareContext(nullptr) , m_openGLSync(false) , m_ClientWaitSync(nullptr) { KDeclarative::KDeclarative kdeclarative; kdeclarative.setDeclarativeEngine(engine()); #if KDECLARATIVE_VERSION >= QT_VERSION_CHECK(5, 45, 0) kdeclarative.setupEngine(engine()); kdeclarative.setupContext(); #else kdeclarative.setupBindings(); #endif m_texture[0] = m_texture[1] = m_texture[2] = 0; qRegisterMetaType("Mlt::Frame"); qRegisterMetaType("SharedFrame"); qmlRegisterType("AudioThumb", 1, 0, "QmlAudioThumb"); setPersistentOpenGLContext(true); setPersistentSceneGraph(true); setClearBeforeRendering(false); setResizeMode(QQuickView::SizeRootObjectToView); m_offscreenSurface.setFormat(QWindow::format()); m_offscreenSurface.create(); m_refreshTimer.setSingleShot(true); m_refreshTimer.setInterval(50); m_blackClip.reset(new Mlt::Producer(pCore->getCurrentProfile()->profile(), "color:black")); m_blackClip->set("kdenlive:id", "black"); m_blackClip->set("out", 3); connect(&m_refreshTimer, &QTimer::timeout, this, &GLWidget::refresh); m_producer = m_blackClip; if (!initGPUAccel()) { disableGPUAccel(); } connect(this, &QQuickWindow::sceneGraphInitialized, this, &GLWidget::initializeGL, Qt::DirectConnection); connect(this, &QQuickWindow::beforeRendering, this, &GLWidget::paintGL, Qt::DirectConnection); registerTimelineItems(); m_proxy = new MonitorProxy(this); connect(m_proxy, &MonitorProxy::seekRequestChanged, this, &GLWidget::requestSeek); rootContext()->setContextProperty("controller", m_proxy); } GLWidget::~GLWidget() { // C & D delete m_glslManager; delete m_threadStartEvent; delete m_threadStopEvent; delete m_threadCreateEvent; delete m_threadJoinEvent; delete m_displayEvent; if (m_frameRenderer) { if (m_frameRenderer->isRunning()) { QMetaObject::invokeMethod(m_frameRenderer, "cleanup"); m_frameRenderer->quit(); m_frameRenderer->wait(); m_frameRenderer->deleteLater(); } else { delete m_frameRenderer; } } m_blackClip.reset(); delete m_shareContext; delete m_shader; // delete pCore->getCurrentProfile(); } void GLWidget::updateAudioForAnalysis() { if (m_frameRenderer) { m_frameRenderer->sendAudioForAnalysis = KdenliveSettings::monitor_audio(); } } void GLWidget::initializeGL() { if (m_isInitialized || !isVisible() || (openglContext() == nullptr)) return; openglContext()->makeCurrent(&m_offscreenSurface); initializeOpenGLFunctions(); qCDebug(KDENLIVE_LOG) << "OpenGL vendor: " << QString::fromUtf8((const char *)glGetString(GL_VENDOR)); qCDebug(KDENLIVE_LOG) << "OpenGL renderer: " << QString::fromUtf8((const char *)glGetString(GL_RENDERER)); qCDebug(KDENLIVE_LOG) << "OpenGL Threaded: " << openglContext()->supportsThreadedOpenGL(); qCDebug(KDENLIVE_LOG) << "OpenGL ARG_SYNC: " << openglContext()->hasExtension("GL_ARB_sync"); qCDebug(KDENLIVE_LOG) << "OpenGL OpenGLES: " << openglContext()->isOpenGLES(); // C & D if (onlyGLESGPUAccel()) { disableGPUAccel(); } createShader(); m_openGLSync = initGPUAccelSync(); // C & D if (m_glslManager) { // Create a context sharing with this context for the RenderThread context. // This is needed because openglContext() is active in another thread // at the time that RenderThread is created. // See this Qt bug for more info: https://bugreports.qt.io/browse/QTBUG-44677 // TODO: QTBUG-44677 is closed. still applicable? m_shareContext = new QOpenGLContext; m_shareContext->setFormat(openglContext()->format()); m_shareContext->setShareContext(openglContext()); m_shareContext->create(); } m_frameRenderer = new FrameRenderer(openglContext(), &m_offscreenSurface, m_ClientWaitSync); m_frameRenderer->sendAudioForAnalysis = KdenliveSettings::monitor_audio(); openglContext()->makeCurrent(this); // openglContext()->blockSignals(false); connect(m_frameRenderer, &FrameRenderer::frameDisplayed, this, &GLWidget::frameDisplayed, Qt::QueuedConnection); connect(m_frameRenderer, &FrameRenderer::textureReady, this, &GLWidget::updateTexture, Qt::DirectConnection); connect(m_frameRenderer, &FrameRenderer::frameDisplayed, this, &GLWidget::onFrameDisplayed, Qt::QueuedConnection); connect(m_frameRenderer, &FrameRenderer::audioSamplesSignal, this, &GLWidget::audioSamplesSignal, Qt::QueuedConnection); m_initSem.release(); m_isInitialized = true; reconfigure(); } void GLWidget::resizeGL(int width, int height) { int x, y, w, h; height -= m_rulerHeight; double this_aspect = (double)width / height; double video_aspect = pCore->getCurrentProfile()->dar(); // Special case optimization to negate odd effect of sample aspect ratio // not corresponding exactly with image resolution. if ((int)(this_aspect * 1000) == (int)(video_aspect * 1000)) { w = width; h = height; } // Use OpenGL to normalise sample aspect ratio else if (height * video_aspect > width) { w = width; h = width / video_aspect; } else { w = height * video_aspect; h = height; } x = (width - w) / 2; y = (height - h) / 2; m_rect.setRect(x, y, w, h); double scalex = (double)m_rect.width() / pCore->getCurrentProfile()->width() * m_zoom; double scaley = (double)m_rect.width() / ((double)pCore->getCurrentProfile()->height() * pCore->getCurrentProfile()->dar() / pCore->getCurrentProfile()->width()) / pCore->getCurrentProfile()->width() * m_zoom; QPoint center = m_rect.center(); QQuickItem *rootQml = rootObject(); if (rootQml) { rootQml->setProperty("center", center); rootQml->setProperty("scalex", scalex); rootQml->setProperty("scaley", scaley); if (rootQml->objectName() == QLatin1String("rootsplit")) { // Adjust splitter pos rootQml->setProperty("splitterPos", x + (rootQml->property("realpercent").toDouble() * w)); } } emit rectChanged(); } void GLWidget::resizeEvent(QResizeEvent *event) { resizeGL(event->size().width(), event->size().height()); QQuickView::resizeEvent(event); } void GLWidget::createGPUAccelFragmentProg() { m_shader->addShaderFromSourceCode(QOpenGLShader::Fragment, "uniform sampler2D tex;" "varying highp vec2 coordinates;" "void main(void) {" " gl_FragColor = texture2D(tex, coordinates);" "}"); m_shader->link(); m_textureLocation[0] = m_shader->uniformLocation("tex"); } void GLWidget::createShader() { m_shader = new QOpenGLShaderProgram; m_shader->addShaderFromSourceCode(QOpenGLShader::Vertex, "uniform highp mat4 projection;" "uniform highp mat4 modelView;" "attribute highp vec4 vertex;" "attribute highp vec2 texCoord;" "varying highp vec2 coordinates;" "void main(void) {" " gl_Position = projection * modelView * vertex;" " coordinates = texCoord;" "}"); // C & D if (m_glslManager) { createGPUAccelFragmentProg(); } else { // A & B createYUVTextureProjectFragmentProg(); } m_projectionLocation = m_shader->uniformLocation("projection"); m_modelViewLocation = m_shader->uniformLocation("modelView"); m_vertexLocation = m_shader->attributeLocation("vertex"); m_texCoordLocation = m_shader->attributeLocation("texCoord"); } void GLWidget::createYUVTextureProjectFragmentProg() { m_shader->addShaderFromSourceCode(QOpenGLShader::Fragment, "uniform sampler2D Ytex, Utex, Vtex;" "uniform lowp int colorspace;" "varying highp vec2 coordinates;" "void main(void) {" " mediump vec3 texel;" " texel.r = texture2D(Ytex, coordinates).r - 0.0625;" // Y " texel.g = texture2D(Utex, coordinates).r - 0.5;" // U " texel.b = texture2D(Vtex, coordinates).r - 0.5;" // V " mediump mat3 coefficients;" " if (colorspace == 601) {" " coefficients = mat3(" " 1.1643, 1.1643, 1.1643," // column 1 " 0.0, -0.39173, 2.017," // column 2 " 1.5958, -0.8129, 0.0);" // column 3 " } else {" // ITU-R 709 " coefficients = mat3(" " 1.1643, 1.1643, 1.1643," // column 1 " 0.0, -0.213, 2.112," // column 2 " 1.793, -0.533, 0.0);" // column 3 " }" " gl_FragColor = vec4(coefficients * texel, 1.0);" "}"); m_shader->link(); m_textureLocation[0] = m_shader->uniformLocation("Ytex"); m_textureLocation[1] = m_shader->uniformLocation("Utex"); m_textureLocation[2] = m_shader->uniformLocation("Vtex"); m_colorspaceLocation = m_shader->uniformLocation("colorspace"); } static void uploadTextures(QOpenGLContext *context, const SharedFrame &frame, GLuint texture[]) { int width = frame.get_image_width(); int height = frame.get_image_height(); const uint8_t *image = frame.get_image(); QOpenGLFunctions *f = context->functions(); // The planes of pixel data may not be a multiple of the default 4 bytes. f->glPixelStorei(GL_UNPACK_ALIGNMENT, 1); // Upload each plane of YUV to a texture. if (texture[0] != 0u) { f->glDeleteTextures(3, texture); } check_error(f); f->glGenTextures(3, texture); check_error(f); f->glBindTexture(GL_TEXTURE_2D, texture[0]); check_error(f); f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); check_error(f); f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); check_error(f); f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); check_error(f); f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); check_error(f); f->glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, width, height, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, image); check_error(f); f->glBindTexture(GL_TEXTURE_2D, texture[1]); check_error(f); f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); check_error(f); f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); check_error(f); f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); check_error(f); f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); check_error(f); f->glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, width / 2, height / 2, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, image + width * height); check_error(f); f->glBindTexture(GL_TEXTURE_2D, texture[2]); check_error(f); f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); check_error(f); f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); check_error(f); f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); check_error(f); f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); check_error(f); f->glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, width / 2, height / 2, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, image + width * height + width / 2 * height / 2); check_error(f); } void GLWidget::clear() { stopGlsl(); update(); } void GLWidget::releaseAnalyse() { m_analyseSem.release(); } bool GLWidget::acquireSharedFrameTextures() { // A if ((m_glslManager == nullptr) && !openglContext()->supportsThreadedOpenGL()) { QMutexLocker locker(&m_contextSharedAccess); if (!m_sharedFrame.is_valid()) { return false; } uploadTextures(openglContext(), m_sharedFrame, m_texture); } else if (m_glslManager) { // C & D m_contextSharedAccess.lock(); if (m_sharedFrame.is_valid()) { m_texture[0] = *((const GLuint *)m_sharedFrame.get_image()); } } if (!m_texture[0]) { // C & D if (m_glslManager) m_contextSharedAccess.unlock(); return false; } return true; } void GLWidget::bindShaderProgram() { m_shader->bind(); // C & D if (m_glslManager) { m_shader->setUniformValue(m_textureLocation[0], 0); } else { // A & B m_shader->setUniformValue(m_textureLocation[0], 0); m_shader->setUniformValue(m_textureLocation[1], 1); m_shader->setUniformValue(m_textureLocation[2], 2); m_shader->setUniformValue(m_colorspaceLocation, pCore->getCurrentProfile()->colorspace()); } } void GLWidget::releaseSharedFrameTextures() { // C & D if (m_glslManager) { glFinish(); m_contextSharedAccess.unlock(); } } bool GLWidget::initGPUAccel() { if (!KdenliveSettings::gpu_accel()) return false; m_glslManager = new Mlt::Filter(pCore->getCurrentProfile()->profile(), "glsl.manager"); return m_glslManager->is_valid(); } // C & D // TODO: insure safe, idempotent on all pipelines. void GLWidget::disableGPUAccel() { delete m_glslManager; m_glslManager = nullptr; KdenliveSettings::setGpu_accel(false); // Need to destroy MLT global reference to prevent filters from trying to use GPU. mlt_properties_set_data(mlt_global_properties(), "glslManager", nullptr, 0, nullptr, nullptr); emit gpuNotSupported(); } bool GLWidget::onlyGLESGPUAccel() const { return (m_glslManager != nullptr) && openglContext()->isOpenGLES(); } #if defined(Q_OS_WIN) bool GLWidget::initGPUAccelSync() { // no-op // TODO: getProcAddress is not working on Windows? return false; } #else bool GLWidget::initGPUAccelSync() { if (!KdenliveSettings::gpu_accel()) return false; if (m_glslManager == nullptr) return false; if (!openglContext()->hasExtension("GL_ARB_sync")) return false; m_ClientWaitSync = (ClientWaitSync_fp)openglContext()->getProcAddress("glClientWaitSync"); if (m_ClientWaitSync) { return true; } else { qCDebug(KDENLIVE_LOG) << " / / // NO GL SYNC, ERROR"; // fallback on A || B // TODO: fallback on A || B || C? disableGPUAccel(); return false; } } #endif void GLWidget::paintGL() { QOpenGLFunctions *f = openglContext()->functions(); int width = this->width() * devicePixelRatio(); int height = this->height() * devicePixelRatio(); f->glDisable(GL_BLEND); f->glDisable(GL_DEPTH_TEST); f->glDepthMask(GL_FALSE); f->glViewport(0, (m_rulerHeight * devicePixelRatio() * 0.5 + 0.5), width, height); check_error(f); QColor color(KdenliveSettings::window_background()); f->glClearColor(color.redF(), color.greenF(), color.blueF(), color.alphaF()); f->glClear(GL_COLOR_BUFFER_BIT); check_error(f); if (!acquireSharedFrameTextures()) return; // Bind textures. for (uint i = 0; i < 3; ++i) { if (m_texture[i] != 0u) { f->glActiveTexture(GL_TEXTURE0 + i); f->glBindTexture(GL_TEXTURE_2D, m_texture[i]); check_error(f); } } bindShaderProgram(); check_error(f); // Setup an orthographic projection. QMatrix4x4 projection; projection.scale(2.0f / (float)width, 2.0f / (float)height); m_shader->setUniformValue(m_projectionLocation, projection); check_error(f); // Set model view. QMatrix4x4 modelView; if (!qFuzzyCompare(m_zoom, 1.0f)) { if ((offset().x() != 0) || (offset().y() != 0)) modelView.translate(-offset().x() * devicePixelRatio(), offset().y() * devicePixelRatio()); modelView.scale(zoom(), zoom()); } m_shader->setUniformValue(m_modelViewLocation, modelView); check_error(f); // Provide vertices of triangle strip. QVector vertices; width = m_rect.width() * devicePixelRatio(); height = m_rect.height() * devicePixelRatio(); vertices << QVector2D(float(-width) / 2.0f, float(-height) / 2.0f); vertices << QVector2D(float(-width) / 2.0f, float(height) / 2.0f); vertices << QVector2D(float(width) / 2.0f, float(-height) / 2.0f); vertices << QVector2D(float(width) / 2.0f, float(height) / 2.0f); m_shader->enableAttributeArray(m_vertexLocation); check_error(f); m_shader->setAttributeArray(m_vertexLocation, vertices.constData()); check_error(f); // Provide texture coordinates. QVector texCoord; texCoord << QVector2D(0.0f, 1.0f); texCoord << QVector2D(0.0f, 0.0f); texCoord << QVector2D(1.0f, 1.0f); texCoord << QVector2D(1.0f, 0.0f); m_shader->enableAttributeArray(m_texCoordLocation); check_error(f); m_shader->setAttributeArray(m_texCoordLocation, texCoord.constData()); check_error(f); // Render glDrawArrays(GL_TRIANGLE_STRIP, 0, vertices.size()); check_error(f); if (m_sendFrame && m_analyseSem.tryAcquire(1)) { // Render RGB frame for analysis int fullWidth = pCore->getCurrentProfile()->width(); int fullHeight = pCore->getCurrentProfile()->height(); if ((m_fbo == nullptr) || m_fbo->size() != QSize(fullWidth, fullHeight)) { delete m_fbo; QOpenGLFramebufferObjectFormat fmt; fmt.setSamples(1); fmt.setInternalTextureFormat(GL_RGB); // GL_RGBA32F); // which one is the fastest ? m_fbo = new QOpenGLFramebufferObject(fullWidth, fullHeight, fmt); // GL_TEXTURE_2D); } m_fbo->bind(); glViewport(0, 0, fullWidth, fullHeight); QMatrix4x4 projection2; projection2.scale(2.0f / (float)width, 2.0f / (float)height); m_shader->setUniformValue(m_projectionLocation, projection2); glDrawArrays(GL_TRIANGLE_STRIP, 0, vertices.size()); check_error(f); m_fbo->release(); emit analyseFrame(m_fbo->toImage()); m_sendFrame = false; } // Cleanup m_shader->disableAttributeArray(m_vertexLocation); m_shader->disableAttributeArray(m_texCoordLocation); m_shader->release(); for (uint i = 0; i < 3; ++i) { if (m_texture[i] != 0u) { f->glActiveTexture(GL_TEXTURE0 + i); f->glBindTexture(GL_TEXTURE_2D, 0); check_error(f); } } glActiveTexture(GL_TEXTURE0); check_error(f); releaseSharedFrameTextures(); check_error(f); } void GLWidget::slotZoom(bool zoomIn) { if (zoomIn) { if (qFuzzyCompare(m_zoom, 1.0f)) { setZoom(2.0f); } else if (qFuzzyCompare(m_zoom, 2.0f)) { setZoom(3.0f); } else if (m_zoom < 1.0f) { setZoom(m_zoom * 2); } } else { if (qFuzzyCompare(m_zoom, 3.0f)) { setZoom(2.0); } else if (qFuzzyCompare(m_zoom, 2.0f)) { setZoom(1.0); } else if (m_zoom > 0.2) { setZoom(m_zoom / 2); } } } void GLWidget::wheelEvent(QWheelEvent *event) { if (((event->modifiers() & Qt::ControlModifier) != 0u) && ((event->modifiers() & Qt::ShiftModifier) != 0u)) { slotZoom(event->delta() > 0); return; } emit mouseSeek(event->delta(), (uint)event->modifiers()); event->accept(); } void GLWidget::requestSeek() { if (!m_producer) { return; } if (m_proxy->seeking()) { m_producer->seek(m_proxy->seekPosition()); if (!qFuzzyIsNull(m_producer->get_speed())) { m_consumer->purge(); } if (m_consumer->is_stopped()) { m_consumer->start(); } m_consumer->set("refresh", 1); } } void GLWidget::seek(int pos) { if (!m_proxy->seeking()) { m_proxy->setSeekPosition(pos); m_producer->seek(pos); if (m_consumer->is_stopped()) { m_consumer->start(); } else { m_consumer->purge(); m_consumer->set("refresh", 1); } } else { m_proxy->setSeekPosition(pos); } } void GLWidget::requestRefresh() { if (m_proxy->seeking()) { return; } if (m_producer && qFuzzyIsNull(m_producer->get_speed())) { m_refreshTimer.start(); } } QString GLWidget::frameToTime(int frames) const { return m_consumer ? m_consumer->frames_to_time(frames, mlt_time_smpte_df) : QStringLiteral("-"); } void GLWidget::refresh() { m_refreshTimer.stop(); if (m_proxy->seeking()) { return; } QMutexLocker locker(&m_mltMutex); if (m_consumer->is_stopped()) { m_consumer->start(); } m_consumer->set("refresh", 1); } bool GLWidget::checkFrameNumber(int pos, int offset) { emit consumerPosition(pos); if (!m_proxy->setPosition(pos)) { emit seekPosition(m_proxy->seekOrCurrentPosition()); } const double speed = m_producer->get_speed(); if (m_proxy->seeking()) { m_producer->set_speed(0); m_producer->seek(m_proxy->seekPosition()); if (qFuzzyIsNull(speed)) { m_consumer->set("refresh", 1); } else { m_producer->set_speed(speed); } } else if (qFuzzyIsNull(speed)) { if (m_isLoopMode) { if (pos >= m_producer->get_int("out") - offset) { m_consumer->purge(); m_producer->seek(m_proxy->zoneIn()); m_producer->set_speed(1.0); m_consumer->set("refresh", 1); } return true; } else { if (pos >= m_producer->get_int("out") - offset) { return false; } return true; } } else if (speed < 0. && pos <= 0) { m_producer->set_speed(0); return false; } return true; } void GLWidget::mousePressEvent(QMouseEvent *event) { if ((rootObject() != nullptr) && rootObject()->objectName() != QLatin1String("root") && !(event->modifiers() & Qt::ControlModifier) && !(event->buttons() & Qt::MiddleButton)) { event->ignore(); QQuickView::mousePressEvent(event); return; } if ((event->button() & Qt::LeftButton) != 0u) { if ((event->modifiers() & Qt::ControlModifier) != 0u) { // Pan view m_panStart = event->pos(); setCursor(Qt::ClosedHandCursor); } else { m_dragStart = event->pos(); } } else if ((event->button() & Qt::RightButton) != 0u) { emit showContextMenu(event->globalPos()); } else if ((event->button() & Qt::MiddleButton) != 0u) { m_panStart = event->pos(); setCursor(Qt::ClosedHandCursor); } event->accept(); QQuickView::mousePressEvent(event); } void GLWidget::mouseMoveEvent(QMouseEvent *event) { if ((rootObject() != nullptr) && rootObject()->objectName() != QLatin1String("root") && !(event->modifiers() & Qt::ControlModifier) && !(event->buttons() & Qt::MiddleButton)) { event->ignore(); QQuickView::mouseMoveEvent(event); return; } /* if (event->modifiers() == Qt::ShiftModifier && m_producer) { emit seekTo(m_producer->get_length() * event->x() / width()); return; }*/ QQuickView::mouseMoveEvent(event); if (!m_panStart.isNull()) { emit panView(m_panStart - event->pos()); m_panStart = event->pos(); event->accept(); QQuickView::mouseMoveEvent(event); return; } if (!(event->buttons() & Qt::LeftButton)) { QQuickView::mouseMoveEvent(event); return; } if (!event->isAccepted() && !m_dragStart.isNull() && (event->pos() - m_dragStart).manhattanLength() >= QApplication::startDragDistance()) { m_dragStart = QPoint(); emit startDrag(); } } void GLWidget::keyPressEvent(QKeyEvent *event) { QQuickView::keyPressEvent(event); if (!event->isAccepted()) { emit passKeyEvent(event); } } void GLWidget::createThread(RenderThread **thread, thread_function_t function, void *data) { #ifdef Q_OS_WIN // On Windows, MLT event consumer-thread-create is fired from the Qt main thread. while (!m_isInitialized) { qApp->processEvents(); } #else if (!m_isInitialized) { m_initSem.acquire(); } #endif (*thread) = new RenderThread(function, data, m_shareContext, &m_offscreenSurface); (*thread)->start(); } static void onThreadCreate(mlt_properties owner, GLWidget *self, RenderThread **thread, int *priority, thread_function_t function, void *data) { Q_UNUSED(owner) Q_UNUSED(priority) // self->clearFrameRenderer(); self->createThread(thread, function, data); self->lockMonitor(); } static void onThreadJoin(mlt_properties owner, GLWidget *self, RenderThread *thread) { Q_UNUSED(owner) if (thread) { thread->quit(); thread->wait(); delete thread; // self->clearFrameRenderer(); self->releaseMonitor(); } } void GLWidget::startGlsl() { // C & D if (m_glslManager) { // clearFrameRenderer(); m_glslManager->fire_event("init glsl"); if (m_glslManager->get_int("glsl_supported") == 0) { disableGPUAccel(); } else { emit started(); } } } static void onThreadStarted(mlt_properties owner, GLWidget *self) { Q_UNUSED(owner) self->startGlsl(); } void GLWidget::releaseMonitor() { emit lockMonitor(false); } void GLWidget::lockMonitor() { emit lockMonitor(true); } void GLWidget::stopGlsl() { if (m_consumer) { m_consumer->purge(); } // C & D // TODO This is commented out for now because it is causing crashes. // Technically, this should be the correct thing to do, but it appears // some changes have created regression (see shotcut) // with respect to restarting the consumer in GPU mode. // m_glslManager->fire_event("close glsl"); m_texture[0] = 0; } static void onThreadStopped(mlt_properties owner, GLWidget *self) { Q_UNUSED(owner) self->stopGlsl(); } void GLWidget::slotSwitchAudioOverlay(bool enable) { KdenliveSettings::setDisplayAudioOverlay(enable); if (m_audioWaveDisplayed && !enable) { if (m_producer && m_producer->get_int("video_index") != -1) { // We have a video producer, disable filter removeAudioOverlay(); } } if (enable && !m_audioWaveDisplayed && m_producer) { createAudioOverlay(m_producer->get_int("video_index") == -1); } } int GLWidget::setProducer(const std::shared_ptr &producer, bool isActive, int position) { int error = 0; QString currentId; int consumerPosition = 0; currentId = m_producer->parent().get("kdenlive:id"); if (producer) { m_producer = producer; } else { if (currentId == QLatin1String("black")) { return 0; } if (m_audioWaveDisplayed) { removeAudioOverlay(); } m_producer = m_blackClip; } // redundant check. postcondition of above is m_producer != null if (m_producer) { m_producer->set_speed(0); if (m_consumer) { consumerPosition = m_consumer->position(); m_consumer->stop(); if (!m_consumer->is_stopped()) { m_consumer->stop(); } } error = reconfigure(); if (error == 0) { // The profile display aspect ratio may have changed. resizeGL(width(), height()); } } else { return error; } if (!m_consumer) { return error; } consumerPosition = m_consumer->position(); if (m_producer->get_int("video_index") == -1) { // This is an audio only clip, attach visualization filter. Currently, the filter crashes MLT when Movit accel is used if (!m_audioWaveDisplayed) { createAudioOverlay(true); } else if (m_consumer) { if (KdenliveSettings::gpu_accel()) { removeAudioOverlay(); } else { adjustAudioOverlay(true); } } } else if (m_audioWaveDisplayed && (m_consumer != nullptr)) { // This is not an audio clip, hide wave if (KdenliveSettings::displayAudioOverlay()) { adjustAudioOverlay(m_producer->get_int("video_index") == -1); } else { removeAudioOverlay(); } } else if (KdenliveSettings::displayAudioOverlay()) { createAudioOverlay(false); } if (position == -1 && m_producer->parent().get("kdenlive:id") == currentId) { position = consumerPosition; } if (isActive) { startConsumer(); } m_proxy->requestSeekPosition(position > 0 ? position : m_producer->position()); return error; } int GLWidget::droppedFrames() const { return (m_consumer ? m_consumer->get_int("drop_count") : 0); } void GLWidget::resetDrops() { if (m_consumer) { m_consumer->set("drop_count", 0); } } void GLWidget::createAudioOverlay(bool isAudio) { if (!m_consumer) { return; } if (isAudio && KdenliveSettings::gpu_accel()) { // Audiowaveform filter crashes on Movit + audio clips) return; } Mlt::Filter f(pCore->getCurrentProfile()->profile(), "audiowaveform"); if (f.is_valid()) { // f.set("show_channel", 1); f.set("color.1", "0xffff0099"); f.set("fill", 1); if (isAudio) { // Fill screen f.set("rect", "0,0,100%,100%"); } else { // Overlay on lower part of the screen f.set("rect", "0,80%,100%,20%"); } m_consumer->attach(f); m_audioWaveDisplayed = true; } } void GLWidget::removeAudioOverlay() { Mlt::Service sourceService(m_consumer->get_service()); // move all effects to the correct producer int ct = 0; Mlt::Filter *filter = sourceService.filter(ct); while (filter != nullptr) { QString srv = filter->get("mlt_service"); if (srv == QLatin1String("audiowaveform")) { sourceService.detach(*filter); delete filter; break; } else { ct++; } filter = sourceService.filter(ct); } m_audioWaveDisplayed = false; } void GLWidget::adjustAudioOverlay(bool isAudio) { Mlt::Service sourceService(m_consumer->get_service()); // move all effects to the correct producer int ct = 0; Mlt::Filter *filter = sourceService.filter(ct); while (filter != nullptr) { QString srv = filter->get("mlt_service"); if (srv == QLatin1String("audiowaveform")) { if (isAudio) { filter->set("rect", "0,0,100%,100%"); } else { filter->set("rect", "0,80%,100%,20%"); } break; } else { ct++; } filter = sourceService.filter(ct); } } void GLWidget::stopCapture() { if (strcmp(m_consumer->get("mlt_service"), "multi") == 0) { m_consumer->set("refresh", 0); m_consumer->purge(); m_consumer->stop(); } } int GLWidget::reconfigureMulti(const QString ¶ms, const QString &path, Mlt::Profile *profile) { Q_UNUSED(params); Q_UNUSED(path); Q_UNUSED(profile); // TODO Fix or delete /* QString serviceName = property("mlt_service").toString(); if ((m_consumer == nullptr) || !m_consumer->is_valid() || strcmp(m_consumer->get("mlt_service"), "multi") != 0) { if (m_consumer) { m_consumer->purge(); m_consumer->stop(); m_consumer.reset(); } m_consumer.reset(new Mlt::FilteredConsumer(*profile, "multi")); delete m_threadStartEvent; m_threadStartEvent = nullptr; delete m_threadStopEvent; m_threadStopEvent = nullptr; delete m_threadCreateEvent; delete m_threadJoinEvent; if (m_consumer) { m_threadCreateEvent = m_consumer->listen("consumer-thread-create", this, (mlt_listener)onThreadCreate); m_threadJoinEvent = m_consumer->listen("consumer-thread-join", this, (mlt_listener)onThreadJoin); } } if (m_consumer->is_valid()) { // build sub consumers // m_consumer->set("mlt_image_format", "yuv422"); reloadProfile(); int volume = KdenliveSettings::volume(); m_consumer->set("0", serviceName.toUtf8().constData()); m_consumer->set("0.mlt_image_format", "yuv422"); m_consumer->set("0.terminate_on_pause", 0); // m_consumer->set("0.preview_off", 1); m_consumer->set("0.real_time", 0); m_consumer->set("0.volume", (double)volume / 100); if (serviceName.startsWith(QLatin1String("sdl_audio"))) { #ifdef Q_OS_WIN m_consumer->set("0.audio_buffer", 2048); #else m_consumer->set("0.audio_buffer", 512); #endif QString audioDevice = KdenliveSettings::audiodevicename(); if (!audioDevice.isEmpty()) { m_consumer->set("audio_device", audioDevice.toUtf8().constData()); } QString audioDriver = KdenliveSettings::audiodrivername(); if (!audioDriver.isEmpty()) { m_consumer->set("audio_driver", audioDriver.toUtf8().constData()); } } m_consumer->set("1", "avformat"); m_consumer->set("1.target", path.toUtf8().constData()); // m_consumer->set("1.real_time", -KdenliveSettings::mltthreads()); m_consumer->set("terminate_on_pause", 0); m_consumer->set("1.terminate_on_pause", 0); // m_consumer->set("1.terminate_on_pause", 0);// was commented out. restoring it fixes mantis#3415 - FFmpeg recording freezes QStringList paramList = params.split(' ', QString::SkipEmptyParts); for (int i = 0; i < paramList.count(); ++i) { QString key = "1." + paramList.at(i).section(QLatin1Char('='), 0, 0); QString value = paramList.at(i).section(QLatin1Char('='), 1, 1); if (value == QLatin1String("%threads")) { value = QString::number(QThread::idealThreadCount()); } m_consumer->set(key.toUtf8().constData(), value.toUtf8().constData()); } // Connect the producer to the consumer - tell it to "run" later delete m_displayEvent; // C & D if (m_glslManager) { // D if (m_openGLSync) { m_displayEvent = m_consumer->listen("consumer-frame-show", this, (mlt_listener)on_gl_frame_show); } else { // C m_displayEvent = m_consumer->listen("consumer-frame-show", this, (mlt_listener)on_gl_nosync_frame_show); } } else { // A & B m_displayEvent = m_consumer->listen("consumer-frame-show", this, (mlt_listener)on_frame_show); } m_consumer->connect(*m_producer.get()); m_consumer->start(); return 0; } */ return -1; } int GLWidget::reconfigure(bool reload) { int error = 0; // use SDL for audio, OpenGL for video QString serviceName = property("mlt_service").toString(); if (reload) { reloadProfile(); m_blackClip.reset(new Mlt::Producer(pCore->getCurrentProfile()->profile(), "color:black")); m_blackClip->set("kdenlive:id", "black"); } if ((m_consumer == nullptr) || !m_consumer->is_valid() || strcmp(m_consumer->get("mlt_service"), "multi") == 0) { if (m_consumer) { m_consumer->purge(); m_consumer->stop(); m_consumer.reset(); } QString audioBackend = (KdenliveSettings::external_display()) ? QString("decklink:%1").arg(KdenliveSettings::blackmagic_output_device()) : KdenliveSettings::audiobackend(); if (serviceName.isEmpty() || serviceName != audioBackend) { m_consumer.reset(new Mlt::FilteredConsumer(pCore->getCurrentProfile()->profile(), audioBackend.toLatin1().constData())); if (m_consumer->is_valid()) { serviceName = audioBackend; setProperty("mlt_service", serviceName); if (KdenliveSettings::external_display()) { m_consumer->set("terminate_on_pause", 0); } } else { // Warning, audio backend unavailable on system m_consumer.reset(); QStringList backends = {"sdl2_audio", "sdl_audio", "rtaudio"}; for (const QString &bk : backends) { if (bk == audioBackend) { // Already tested continue; } m_consumer.reset(new Mlt::FilteredConsumer(pCore->getCurrentProfile()->profile(), bk.toLatin1().constData())); if (m_consumer->is_valid()) { if (audioBackend == KdenliveSettings::sdlAudioBackend()) { // switch sdl audio backend KdenliveSettings::setSdlAudioBackend(bk); } qDebug() << "++++++++\nSwitching audio backend to: " << bk << "\n++++++++++"; KdenliveSettings::setAudiobackend(bk); serviceName = bk; setProperty("mlt_service", serviceName); break; } else { m_consumer.reset(); } } if (!m_consumer) { qWarning() << "WARNING, NO AUDIO BACKEND FOUND"; return -1; } } } delete m_threadStartEvent; m_threadStartEvent = nullptr; delete m_threadStopEvent; m_threadStopEvent = nullptr; delete m_threadCreateEvent; delete m_threadJoinEvent; if (m_consumer) { m_threadCreateEvent = m_consumer->listen("consumer-thread-create", this, (mlt_listener)onThreadCreate); m_threadJoinEvent = m_consumer->listen("consumer-thread-join", this, (mlt_listener)onThreadJoin); } } if (m_consumer->is_valid()) { // Connect the producer to the consumer - tell it to "run" later if (m_producer) { m_consumer->connect(*m_producer.get()); // m_producer->set_speed(0.0); } int dropFrames = realTime(); if (!KdenliveSettings::monitor_dropframes()) { dropFrames = -dropFrames; } m_consumer->set("real_time", dropFrames); // C & D if (m_glslManager) { if (!m_threadStartEvent) { m_threadStartEvent = m_consumer->listen("consumer-thread-started", this, (mlt_listener)onThreadStarted); } if (!m_threadStopEvent) { m_threadStopEvent = m_consumer->listen("consumer-thread-stopped", this, (mlt_listener)onThreadStopped); } if (!serviceName.startsWith(QLatin1String("decklink"))) { m_consumer->set("mlt_image_format", "glsl"); } } else { // A & B m_consumer->set("mlt_image_format", "yuv422"); } delete m_displayEvent; // C & D if (m_glslManager) { m_displayEvent = m_consumer->listen("consumer-frame-show", this, (mlt_listener)on_gl_frame_show); } else { // A & B m_displayEvent = m_consumer->listen("consumer-frame-show", this, (mlt_listener)on_frame_show); } int volume = KdenliveSettings::volume(); if (serviceName.startsWith(QLatin1String("sdl_audio"))) { QString audioDevice = KdenliveSettings::audiodevicename(); if (!audioDevice.isEmpty()) { m_consumer->set("audio_device", audioDevice.toUtf8().constData()); } QString audioDriver = KdenliveSettings::audiodrivername(); if (!audioDriver.isEmpty()) { m_consumer->set("audio_driver", audioDriver.toUtf8().constData()); } } /*if (!pCore->getCurrentProfile()->progressive()) m_consumer->set("progressive", property("progressive").toBool());*/ m_consumer->set("volume", volume / 100.0); // m_consumer->set("progressive", 1); m_consumer->set("rescale", KdenliveSettings::mltinterpolation().toUtf8().constData()); m_consumer->set("deinterlace_method", KdenliveSettings::mltdeinterlacer().toUtf8().constData()); /* #ifdef Q_OS_WIN m_consumer->set("audio_buffer", 2048); #else m_consumer->set("audio_buffer", 512); #endif */ m_consumer->set("buffer", 25); m_consumer->set("prefill", 1); m_consumer->set("scrub_audio", 1); if (KdenliveSettings::monitor_gamma() == 0) { m_consumer->set("color_trc", "iec61966_2_1"); } else { m_consumer->set("color_trc", "bt709"); } } else { // Cleanup on error error = 2; } return error; } float GLWidget::zoom() const { return m_zoom; } float GLWidget::scale() const { return (double)m_rect.width() / pCore->getCurrentProfile()->width() * m_zoom; } void GLWidget::reloadProfile() { // The profile display aspect ratio may have changed. resizeGL(width(), height()); refreshSceneLayout(); } QSize GLWidget::profileSize() const { return {pCore->getCurrentProfile()->width(), pCore->getCurrentProfile()->height()}; } QRect GLWidget::displayRect() const { return m_rect; } QPoint GLWidget::offset() const { return {m_offset.x() - ((int)((float)pCore->getCurrentProfile()->width() * m_zoom) - width()) / 2, m_offset.y() - ((int)((float)pCore->getCurrentProfile()->height() * m_zoom) - height()) / 2}; } void GLWidget::setZoom(float zoom) { double zoomRatio = zoom / m_zoom; m_zoom = zoom; emit zoomChanged(); if (rootObject()) { rootObject()->setProperty("zoom", m_zoom); double scalex = rootObject()->property("scalex").toDouble() * zoomRatio; rootObject()->setProperty("scalex", scalex); double scaley = rootObject()->property("scaley").toDouble() * zoomRatio; rootObject()->setProperty("scaley", scaley); } update(); } void GLWidget::onFrameDisplayed(const SharedFrame &frame) { m_contextSharedAccess.lock(); m_sharedFrame = frame; m_sendFrame = sendFrameForAnalysis; m_contextSharedAccess.unlock(); update(); } void GLWidget::mouseReleaseEvent(QMouseEvent *event) { QQuickView::mouseReleaseEvent(event); if (m_dragStart.isNull() && m_panStart.isNull() && (rootObject() != nullptr) && rootObject()->objectName() != QLatin1String("root") && !(event->modifiers() & Qt::ControlModifier)) { event->ignore(); return; } if (!m_dragStart.isNull() && m_panStart.isNull() && ((event->button() & Qt::LeftButton) != 0u) && !event->isAccepted()) { emit monitorPlay(); } m_dragStart = QPoint(); m_panStart = QPoint(); setCursor(Qt::ArrowCursor); } void GLWidget::mouseDoubleClickEvent(QMouseEvent *event) { QQuickView::mouseDoubleClickEvent(event); if (event->isAccepted()) { return; } if ((rootObject() == nullptr) || rootObject()->objectName() != QLatin1String("rooteffectscene")) { emit switchFullScreen(); } event->accept(); } void GLWidget::setOffsetX(int x, int max) { m_offset.setX(x); emit offsetChanged(); if (rootObject()) { rootObject()->setProperty("offsetx", m_zoom > 1.0f ? x - max / 2.0 - 10 : 0); } update(); } void GLWidget::setOffsetY(int y, int max) { m_offset.setY(y); if (rootObject()) { rootObject()->setProperty("offsety", m_zoom > 1.0f ? y - max / 2.0 - 10 : 0); } update(); } int GLWidget::realTime() const { // C & D if (m_glslManager) { return 1; } return KdenliveSettings::mltthreads(); } std::shared_ptr GLWidget::consumer() { return m_consumer; } void GLWidget::updateGamma() { reconfigure(); } void GLWidget::resetConsumer(bool fullReset) { if (fullReset && m_consumer) { m_consumer->purge(); m_consumer->stop(); m_consumer.reset(); } reconfigure(); } const QString GLWidget::sceneList(const QString &root, const QString &fullPath) { QString playlist; qCDebug(KDENLIVE_LOG) << " * * *Setting document xml root: " << root; Mlt::Consumer xmlConsumer(pCore->getCurrentProfile()->profile(), "xml", fullPath.isEmpty() ? "kdenlive_playlist" : fullPath.toUtf8().constData()); if (!root.isEmpty()) { xmlConsumer.set("root", root.toUtf8().constData()); } if (!xmlConsumer.is_valid()) { return QString(); } m_producer->optimise(); xmlConsumer.set("terminate_on_pause", 1); xmlConsumer.set("store", "kdenlive"); xmlConsumer.set("time_format", "clock"); // Disabling meta creates cleaner files, but then we don't have access to metadata on the fly (meta channels, etc) // And we must use "avformat" instead of "avformat-novalidate" on project loading which causes a big delay on project opening // xmlConsumer.set("no_meta", 1); Mlt::Producer prod(m_producer->get_producer()); if (!prod.is_valid()) { return QString(); } xmlConsumer.connect(prod); xmlConsumer.run(); playlist = fullPath.isEmpty() ? QString::fromUtf8(xmlConsumer.get("kdenlive_playlist")) : fullPath; return playlist; } void GLWidget::updateTexture(GLuint yName, GLuint uName, GLuint vName) { m_texture[0] = yName; m_texture[1] = uName; m_texture[2] = vName; m_sendFrame = sendFrameForAnalysis; // update(); } // MLT consumer-frame-show event handler void GLWidget::on_frame_show(mlt_consumer, void *self, mlt_frame frame_ptr) { Mlt::Frame frame(frame_ptr); if (frame.get_int("rendered") != 0) { auto *widget = static_cast(self); int timeout = (widget->consumer()->get_int("real_time") > 0) ? 0 : 1000; if ((widget->m_frameRenderer != nullptr) && widget->m_frameRenderer->semaphore()->tryAcquire(1, timeout)) { QMetaObject::invokeMethod(widget->m_frameRenderer, "showFrame", Qt::QueuedConnection, Q_ARG(Mlt::Frame, frame)); } } } void GLWidget::on_gl_nosync_frame_show(mlt_consumer, void *self, mlt_frame frame_ptr) { Mlt::Frame frame(frame_ptr); if (frame.get_int("rendered") != 0) { auto *widget = static_cast(self); int timeout = (widget->consumer()->get_int("real_time") > 0) ? 0 : 1000; if ((widget->m_frameRenderer != nullptr) && widget->m_frameRenderer->semaphore()->tryAcquire(1, timeout)) { QMetaObject::invokeMethod(widget->m_frameRenderer, "showGLNoSyncFrame", Qt::QueuedConnection, Q_ARG(Mlt::Frame, frame)); } } } void GLWidget::on_gl_frame_show(mlt_consumer, void *self, mlt_frame frame_ptr) { Mlt::Frame frame(frame_ptr); if (frame.get_int("rendered") != 0) { auto *widget = static_cast(self); int timeout = (widget->consumer()->get_int("real_time") > 0) ? 0 : 1000; if ((widget->m_frameRenderer != nullptr) && widget->m_frameRenderer->semaphore()->tryAcquire(1, timeout)) { QMetaObject::invokeMethod(widget->m_frameRenderer, "showGLFrame", Qt::QueuedConnection, Q_ARG(Mlt::Frame, frame)); } } } RenderThread::RenderThread(thread_function_t function, void *data, QOpenGLContext *context, QSurface *surface) : QThread(nullptr) , m_function(function) , m_data(data) , m_context(nullptr) , m_surface(surface) { if (context) { m_context = new QOpenGLContext; m_context->setFormat(context->format()); m_context->setShareContext(context); m_context->create(); m_context->moveToThread(this); } } RenderThread::~RenderThread() { // would otherwise leak if RenderThread is allocated with a context but not run. // safe post-run delete m_context; } // TODO: missing some exception handling? void RenderThread::run() { if (m_context) { m_context->makeCurrent(m_surface); } m_function(m_data); if (m_context) { m_context->doneCurrent(); delete m_context; m_context = nullptr; } } FrameRenderer::FrameRenderer(QOpenGLContext *shareContext, QSurface *surface, GLWidget::ClientWaitSync_fp clientWaitSync) : QThread(nullptr) , m_semaphore(3) , m_context(nullptr) , m_surface(surface) , m_ClientWaitSync(clientWaitSync) , m_gl32(nullptr) , sendAudioForAnalysis(false) { Q_ASSERT(shareContext); m_renderTexture[0] = m_renderTexture[1] = m_renderTexture[2] = 0; m_displayTexture[0] = m_displayTexture[1] = m_displayTexture[2] = 0; // B & C & D if (KdenliveSettings::gpu_accel() || shareContext->supportsThreadedOpenGL()) { m_context = new QOpenGLContext; m_context->setFormat(shareContext->format()); m_context->setShareContext(shareContext); m_context->create(); m_context->moveToThread(this); } setObjectName(QStringLiteral("FrameRenderer")); moveToThread(this); start(); } FrameRenderer::~FrameRenderer() { delete m_context; delete m_gl32; } void FrameRenderer::showFrame(Mlt::Frame frame) { int width = 0; int height = 0; mlt_image_format format = mlt_image_yuv420p; frame.get_image(format, width, height); // Save this frame for future use and to keep a reference to the GL Texture. m_displayFrame = SharedFrame(frame); if ((m_context != nullptr) && m_context->isValid()) { m_context->makeCurrent(m_surface); // Upload each plane of YUV to a texture. QOpenGLFunctions *f = m_context->functions(); uploadTextures(m_context, m_displayFrame, m_renderTexture); f->glBindTexture(GL_TEXTURE_2D, 0); check_error(f); f->glFinish(); for (int i = 0; i < 3; ++i) { std::swap(m_renderTexture[i], m_displayTexture[i]); } emit textureReady(m_displayTexture[0], m_displayTexture[1], m_displayTexture[2]); m_context->doneCurrent(); } // The frame is now done being modified and can be shared with the rest // of the application. emit frameDisplayed(m_displayFrame); m_semaphore.release(); } void FrameRenderer::showGLFrame(Mlt::Frame frame) { if ((m_context != nullptr) && m_context->isValid()) { int width = 0; int height = 0; frame.set("movit.convert.use_texture", 1); mlt_image_format format = mlt_image_glsl_texture; frame.get_image(format, width, height); m_context->makeCurrent(m_surface); pipelineSyncToFrame(frame); m_context->functions()->glFinish(); m_context->doneCurrent(); // Save this frame for future use and to keep a reference to the GL Texture. m_displayFrame = SharedFrame(frame); } // The frame is now done being modified and can be shared with the rest // of the application. emit frameDisplayed(m_displayFrame); m_semaphore.release(); } void FrameRenderer::showGLNoSyncFrame(Mlt::Frame frame) { if ((m_context != nullptr) && m_context->isValid()) { int width = 0; int height = 0; frame.set("movit.convert.use_texture", 1); mlt_image_format format = mlt_image_glsl_texture; frame.get_image(format, width, height); m_context->makeCurrent(m_surface); m_context->functions()->glFinish(); m_context->doneCurrent(); // Save this frame for future use and to keep a reference to the GL Texture. m_displayFrame = SharedFrame(frame); } // The frame is now done being modified and can be shared with the rest // of the application. emit frameDisplayed(m_displayFrame); m_semaphore.release(); } void FrameRenderer::cleanup() { if ((m_renderTexture[0] != 0u) && (m_renderTexture[1] != 0u) && (m_renderTexture[2] != 0u)) { m_context->makeCurrent(m_surface); m_context->functions()->glDeleteTextures(3, m_renderTexture); if ((m_displayTexture[0] != 0u) && (m_displayTexture[1] != 0u) && (m_displayTexture[2] != 0u)) { m_context->functions()->glDeleteTextures(3, m_displayTexture); } m_context->doneCurrent(); m_renderTexture[0] = m_renderTexture[1] = m_renderTexture[2] = 0; m_displayTexture[0] = m_displayTexture[1] = m_displayTexture[2] = 0; } } // D void FrameRenderer::pipelineSyncToFrame(Mlt::Frame &frame) { auto sync = (GLsync)frame.get_data("movit.convert.fence"); if (!sync) return; #ifdef Q_OS_WIN // On Windows, use QOpenGLFunctions_3_2_Core instead of getProcAddress. // TODO: move to initialization of m_ClientWaitSync if (!m_gl32) { m_gl32 = m_context->versionFunctions(); if (m_gl32) { m_gl32->initializeOpenGLFunctions(); } } if (m_gl32) { m_gl32->glClientWaitSync(sync, 0, GL_TIMEOUT_IGNORED); check_error(m_context->functions()); } #else if (m_ClientWaitSync) { m_ClientWaitSync(sync, 0, GL_TIMEOUT_IGNORED); check_error(m_context->functions()); } #endif // Q_OS_WIN } -void GLWidget::setAudioThumb(int channels, const QVariantList &audioCache) +void GLWidget::setAudioThumb(int channels, const QList &audioCache) { if (!rootObject()) return; auto *audioThumbDisplay = rootObject()->findChild(QStringLiteral("audiothumb")); if (!audioThumbDisplay) return; QImage img(width(), height() / 6, QImage::Format_ARGB32_Premultiplied); img.fill(Qt::transparent); if (!audioCache.isEmpty() && channels > 0) { int audioLevelCount = audioCache.count() - 1; // simplified audio QPainter painter(&img); QRectF mappedRect(0, 0, img.width(), img.height()); int channelHeight = mappedRect.height(); double value; double scale = (double)width() / (audioLevelCount / channels); if (scale < 1) { painter.setPen(QColor(80, 80, 150, 200)); for (int i = 0; i < img.width(); i++) { int framePos = i / scale; - value = audioCache.at(qMin(framePos * channels, audioLevelCount)).toDouble() / 256; + value = audioCache.at(qMin(framePos * channels, audioLevelCount)) / 256; for (int channel = 1; channel < channels; channel++) { - value = qMax(value, audioCache.at(qMin(framePos * channels + channel, audioLevelCount)).toDouble() / 256); + value = qMax(value, audioCache.at(qMin(framePos * channels + channel, audioLevelCount)) / 256); } painter.drawLine(i, mappedRect.bottom() - (value * channelHeight), i, mappedRect.bottom()); } } else { QPainterPath positiveChannelPath; positiveChannelPath.moveTo(0, mappedRect.bottom()); for (int i = 0; i < audioLevelCount / channels; i++) { - value = audioCache.at(qMin(i * channels, audioLevelCount)).toDouble() / 256; + value = audioCache.at(qMin(i * channels, audioLevelCount)) / 256; for (int channel = 1; channel < channels; channel++) { - value = qMax(value, audioCache.at(qMin(i * channels + channel, audioLevelCount)).toDouble() / 256); + value = qMax(value, audioCache.at(qMin(i * channels + channel, audioLevelCount)) / 256); } positiveChannelPath.lineTo(i * scale, mappedRect.bottom() - (value * channelHeight)); } positiveChannelPath.lineTo(mappedRect.right(), mappedRect.bottom()); painter.setPen(Qt::NoPen); painter.setBrush(QBrush(QColor(80, 80, 150, 200))); painter.drawPath(positiveChannelPath); } painter.end(); } audioThumbDisplay->setImage(img); } void GLWidget::refreshSceneLayout() { if (!rootObject()) { return; } rootObject()->setProperty("profile", QPoint(pCore->getCurrentProfile()->width(), pCore->getCurrentProfile()->height())); rootObject()->setProperty("scalex", (double)m_rect.width() / pCore->getCurrentProfile()->width() * m_zoom); rootObject()->setProperty("scaley", (double)m_rect.width() / (((double)pCore->getCurrentProfile()->height() * pCore->getCurrentProfile()->dar() / pCore->getCurrentProfile()->width())) / pCore->getCurrentProfile()->width() * m_zoom); } void GLWidget::switchPlay(bool play, double speed) { m_proxy->setSeekPosition(-1); if (!m_producer || !m_consumer) { return; } if (m_isZoneMode) { resetZoneMode(); } if (play) { if (m_id == Kdenlive::ClipMonitor && m_consumer->position() == m_producer->get_out()) { m_producer->seek(0); } m_producer->set_speed(speed); m_consumer->start(); m_consumer->set("refresh", 1); } else { m_producer->set_speed(0); m_producer->seek(m_consumer->position() + 1); m_consumer->purge(); m_consumer->start(); } } bool GLWidget::playZone(bool loop) { if (!m_producer || m_proxy->zoneOut() <= m_proxy->zoneIn()) { pCore->displayMessage(i18n("Select a zone to play"), InformationMessage, 500); return false; } m_proxy->setSeekPosition(-1); m_producer->seek(m_proxy->zoneIn()); m_producer->set_speed(0); m_consumer->purge(); m_producer->set("out", m_proxy->zoneOut()); m_producer->set_speed(1.0); if (m_consumer->is_stopped()) { m_consumer->start(); } m_consumer->set("refresh", 1); m_isZoneMode = true; m_isLoopMode = loop; return true; } bool GLWidget::loopClip() { if (!m_producer || m_proxy->zoneOut() <= m_proxy->zoneIn()) { pCore->displayMessage(i18n("Select a zone to play"), InformationMessage, 500); return false; } m_proxy->setSeekPosition(-1); m_producer->seek(0); m_producer->set_speed(0); m_consumer->purge(); m_producer->set("out", m_producer->get_playtime()); m_producer->set_speed(1.0); if (m_consumer->is_stopped()) { m_consumer->start(); } m_consumer->set("refresh", 1); m_isZoneMode = true; m_isLoopMode = true; return true; } void GLWidget::resetZoneMode() { if (!m_isZoneMode && !m_isLoopMode) { return; } m_producer->set("out", m_producer->get_length()); m_isZoneMode = false; m_isLoopMode = false; } MonitorProxy *GLWidget::getControllerProxy() { return m_proxy; } int GLWidget::getCurrentPos() const { return m_proxy->seeking() ? m_proxy->seekPosition() : m_consumer->position(); } void GLWidget::setRulerInfo(int duration, const std::shared_ptr &model) { rootObject()->setProperty("duration", duration); if (model != nullptr) { // we are resetting marker/snap model, reset zone rootContext()->setContextProperty("markersModel", model.get()); } } void GLWidget::startConsumer() { if (m_consumer == nullptr) { return; } if (m_consumer->is_stopped() && m_consumer->start() == -1) { // ARGH CONSUMER BROKEN!!!! KMessageBox::error( qApp->activeWindow(), i18n("Could not create the video preview window.\nThere is something wrong with your Kdenlive install or your driver settings, please fix it.")); if (m_displayEvent) { delete m_displayEvent; } m_displayEvent = nullptr; m_consumer.reset(); return; } m_consumer->set("refresh", 1); } void GLWidget::stop() { m_refreshTimer.stop(); m_proxy->setSeekPosition(-1); // why this lock? QMutexLocker locker(&m_mltMutex); if (m_producer) { if (m_isZoneMode) { resetZoneMode(); } m_producer->set_speed(0.0); } if (m_consumer) { m_consumer->purge(); if (!m_consumer->is_stopped()) { m_consumer->stop(); } } } double GLWidget::playSpeed() const { if (m_producer) { return m_producer->get_speed(); } return 0.0; } void GLWidget::setDropFrames(bool drop) { // why this lock? QMutexLocker locker(&m_mltMutex); if (m_consumer) { int dropFrames = realTime(); if (!drop) { dropFrames = -dropFrames; } m_consumer->stop(); m_consumer->set("real_time", dropFrames); if (m_consumer->start() == -1) { qCWarning(KDENLIVE_LOG) << "ERROR, Cannot start monitor"; } } } int GLWidget::volume() const { if ((!m_consumer) || (!m_producer)) { return -1; } if (m_consumer->get("mlt_service") == QStringLiteral("multi")) { return ((int)100 * m_consumer->get_double("0.volume")); } return ((int)100 * m_consumer->get_double("volume")); } void GLWidget::setVolume(double volume) { if (m_consumer) { if (m_consumer->get("mlt_service") == QStringLiteral("multi")) { m_consumer->set("0.volume", volume); } else { m_consumer->set("volume", volume); } } } int GLWidget::duration() const { if (!m_producer) { return 0; } return m_producer->get_playtime(); } void GLWidget::setConsumerProperty(const QString &name, const QString &value) { QMutexLocker locker(&m_mltMutex); if (m_consumer) { m_consumer->set(name.toUtf8().constData(), value.toUtf8().constData()); if (m_consumer->start() == -1) { qCWarning(KDENLIVE_LOG) << "ERROR, Cannot start monitor"; } } } diff --git a/src/monitor/glwidget.h b/src/monitor/glwidget.h index 2d83878a2..49f53d77b 100644 --- a/src/monitor/glwidget.h +++ b/src/monitor/glwidget.h @@ -1,340 +1,340 @@ /* * Copyright (c) 2011-2014 Meltytech, LLC * Author: Dan Dennedy * * 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. * * 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 GLWIDGET_H #define GLWIDGET_H #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "bin/model/markerlistmodel.hpp" #include "definitions.h" #include "kdenlivesettings.h" #include "scopes/sharedframe.h" class QOpenGLFunctions_3_2_Core; namespace Mlt { class Filter; class Producer; class Consumer; class Profile; } // namespace Mlt class RenderThread; class FrameRenderer; class MonitorProxy; using thread_function_t = void *(*)(void *); /* QQuickView that renders an . * * Creates an MLT consumer and renders a GL view from the consumer. This pipeline is one of: * * A. YUV gl texture w/o GPU filter acceleration * B. YUV gl texture multithreaded w/o GPU filter acceleration * C. RGB gl texture multithreaded w/ GPU filter acceleration and no sync * D. RGB gl texture multithreaded w/ GPU filter acceleration and sync */ class GLWidget : public QQuickView, protected QOpenGLFunctions { Q_OBJECT Q_PROPERTY(QRect rect READ rect NOTIFY rectChanged) Q_PROPERTY(float zoom READ zoom NOTIFY zoomChanged) Q_PROPERTY(QPoint offset READ offset NOTIFY offsetChanged) public: friend class MonitorController; friend class Monitor; friend class MonitorProxy; using ClientWaitSync_fp = GLenum (*)(GLsync, GLbitfield, GLuint64); GLWidget(int id, QObject *parent = nullptr); ~GLWidget() override; int requestedSeekPosition; void createThread(RenderThread **thread, thread_function_t function, void *data); void startGlsl(); void stopGlsl(); void clear(); // TODO: currently unused int reconfigureMulti(const QString ¶ms, const QString &path, Mlt::Profile *profile); void stopCapture(); int reconfigure(bool reload = false); /** @brief Get the current MLT producer playlist. * @return A string describing the playlist */ const QString sceneList(const QString &root, const QString &fullPath = QString()); int displayWidth() const { return m_rect.width(); } void updateAudioForAnalysis(); int displayHeight() const { return m_rect.height(); } QObject *videoWidget() { return this; } Mlt::Filter *glslManager() const { return m_glslManager; } QRect rect() const { return m_rect; } QRect effectRect() const { return m_effectRect; } float zoom() const; float scale() const; QPoint offset() const; std::shared_ptr consumer(); Mlt::Producer *producer(); QSize profileSize() const; QRect displayRect() const; /** @brief set to true if we want to emit a QImage of the frame for analysis */ bool sendFrameForAnalysis; void updateGamma(); /** @brief delete and rebuild consumer, for example when external display is switched */ void resetConsumer(bool fullReset); void reloadProfile(); void lockMonitor(); void releaseMonitor(); int realTime() const; - void setAudioThumb(int channels = 0, const QVariantList &audioCache = QList()); + void setAudioThumb(int channels = 0, const QList &audioCache = QList()); int droppedFrames() const; void resetDrops(); bool checkFrameNumber(int pos, int offset); /** @brief Return current timeline position */ int getCurrentPos() const; /** @brief Requests a monitor refresh */ void requestRefresh(); void setRulerInfo(int duration, const std::shared_ptr &model = nullptr); MonitorProxy *getControllerProxy(); bool playZone(bool loop = false); bool loopClip(); void startConsumer(); void stop(); int rulerHeight() const; /** @brief return current play producer's playing speed */ double playSpeed() const; /** @brief Turn drop frame feature on/off */ void setDropFrames(bool drop); /** @brief Returns current audio volume */ int volume() const; /** @brief Set audio volume on consumer */ void setVolume(double volume); /** @brief Returns current producer's duration in frames */ int duration() const; /** @brief Set a property on the MLT consumer */ void setConsumerProperty(const QString &name, const QString &value); protected: void mouseReleaseEvent(QMouseEvent *event) override; void mouseDoubleClickEvent(QMouseEvent *event) override; void wheelEvent(QWheelEvent *event) override; /** @brief Update producer, should ONLY be called from monitor */ int setProducer(const std::shared_ptr &producer, bool isActive, int position = -1); QString frameToTime(int frames) const; public slots: void seek(int pos); void requestSeek(); void setZoom(float zoom); void setOffsetX(int x, int max); void setOffsetY(int y, int max); void slotSwitchAudioOverlay(bool enable); void slotZoom(bool zoomIn); void initializeGL(); void releaseAnalyse(); void switchPlay(bool play, double speed = 1.0); signals: void frameDisplayed(const SharedFrame &frame); void dragStarted(); void seekTo(int x); void gpuNotSupported(); void started(); void paused(); void playing(); void rectChanged(); void zoomChanged(); void offsetChanged(); void monitorPlay(); void switchFullScreen(bool minimizeOnly = false); void mouseSeek(int eventDelta, uint modifiers); void startDrag(); void analyseFrame(const QImage &); void audioSamplesSignal(const audioShortVector &, int, int, int); void showContextMenu(const QPoint &); void lockMonitor(bool); void passKeyEvent(QKeyEvent *); void panView(const QPoint &diff); void seekPosition(int); void consumerPosition(int); void activateMonitor(); protected: Mlt::Filter *m_glslManager; // TODO: MTL has lock/unlock of individual nodes. Use those. // keeping this for refactoring ease. QMutex m_mltMutex; std::shared_ptr m_consumer; std::shared_ptr m_producer; int m_id; int m_rulerHeight; private: QRect m_rect; QRect m_effectRect; GLuint m_texture[3]; QOpenGLShaderProgram *m_shader; QPoint m_panStart; QPoint m_dragStart; QSemaphore m_initSem; QSemaphore m_analyseSem; bool m_isInitialized; Mlt::Event *m_threadStartEvent; Mlt::Event *m_threadStopEvent; Mlt::Event *m_threadCreateEvent; Mlt::Event *m_threadJoinEvent; Mlt::Event *m_displayEvent; FrameRenderer *m_frameRenderer; int m_projectionLocation; int m_modelViewLocation; int m_vertexLocation; int m_texCoordLocation; int m_colorspaceLocation; int m_textureLocation[3]; QTimer m_refreshTimer; float m_zoom; bool m_sendFrame; bool m_isZoneMode; bool m_isLoopMode; QPoint m_offset; bool m_audioWaveDisplayed; MonitorProxy *m_proxy; std::shared_ptr m_blackClip; static void on_frame_show(mlt_consumer, void *self, mlt_frame frame); static void on_gl_frame_show(mlt_consumer, void *self, mlt_frame frame_ptr); static void on_gl_nosync_frame_show(mlt_consumer, void *self, mlt_frame frame_ptr); void createAudioOverlay(bool isAudio); void removeAudioOverlay(); void adjustAudioOverlay(bool isAudio); QOpenGLFramebufferObject *m_fbo; void refreshSceneLayout(); void resetZoneMode(); /* OpenGL context management. Interfaces to MLT according to the configured render pipeline. */ private slots: void resizeGL(int width, int height); void updateTexture(GLuint yName, GLuint uName, GLuint vName); void paintGL(); void onFrameDisplayed(const SharedFrame &frame); void refresh(); protected: QMutex m_contextSharedAccess; QOffscreenSurface m_offscreenSurface; SharedFrame m_sharedFrame; QOpenGLContext *m_shareContext; bool acquireSharedFrameTextures(); void bindShaderProgram(); void createGPUAccelFragmentProg(); void createShader(); void createYUVTextureProjectFragmentProg(); void disableGPUAccel(); void releaseSharedFrameTextures(); // pipeline A - YUV gl texture w/o GPU filter acceleration // pipeline B - YUV gl texture multithreaded w/o GPU filter acceleration // pipeline C - RGB gl texture multithreaded w/ GPU filter acceleration and no sync // pipeline D - RGB gl texture multithreaded w/ GPU filter acceleration and sync bool m_openGLSync; bool initGPUAccelSync(); // pipeline C & D bool initGPUAccel(); bool onlyGLESGPUAccel() const; // pipeline A & B & C & D // not null iff D ClientWaitSync_fp m_ClientWaitSync; protected: void resizeEvent(QResizeEvent *event) override; void mousePressEvent(QMouseEvent *) override; void mouseMoveEvent(QMouseEvent *) override; void keyPressEvent(QKeyEvent *event) override; }; class RenderThread : public QThread { Q_OBJECT public: RenderThread(thread_function_t function, void *data, QOpenGLContext *context, QSurface *surface); ~RenderThread() override; protected: void run() override; private: thread_function_t m_function; void *m_data; QOpenGLContext *m_context; QSurface *m_surface; }; class FrameRenderer : public QThread { Q_OBJECT public: explicit FrameRenderer(QOpenGLContext *shareContext, QSurface *surface, GLWidget::ClientWaitSync_fp clientWaitSync); ~FrameRenderer() override; QSemaphore *semaphore() { return &m_semaphore; } QOpenGLContext *context() const { return m_context; } Q_INVOKABLE void showFrame(Mlt::Frame frame); Q_INVOKABLE void showGLFrame(Mlt::Frame frame); Q_INVOKABLE void showGLNoSyncFrame(Mlt::Frame frame); public slots: void cleanup(); signals: void textureReady(GLuint yName, GLuint uName = 0, GLuint vName = 0); void frameDisplayed(const SharedFrame &frame); void audioSamplesSignal(const audioShortVector &, int, int, int); private: QSemaphore m_semaphore; SharedFrame m_displayFrame; QOpenGLContext *m_context; QSurface *m_surface; GLWidget::ClientWaitSync_fp m_ClientWaitSync; void pipelineSyncToFrame(Mlt::Frame &); public: GLuint m_renderTexture[3]; GLuint m_displayTexture[3]; QOpenGLFunctions_3_2_Core *m_gl32; bool sendAudioForAnalysis; }; #endif diff --git a/src/monitor/monitor.cpp b/src/monitor/monitor.cpp index 75a86c1bb..5965009f3 100644 --- a/src/monitor/monitor.cpp +++ b/src/monitor/monitor.cpp @@ -1,2122 +1,2122 @@ /*************************************************************************** * 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 "monitor.h" #include "bin/bin.h" #include "bin/projectclip.h" #include "core.h" #include "dialogs/profilesdialog.h" #include "doc/kdenlivedoc.h" #include "doc/kthumb.h" #include "glwidget.h" #include "kdenlivesettings.h" #include "lib/audio/audioStreamInfo.h" #include "mainwindow.h" #include "mltcontroller/clipcontroller.h" #include "monitorproxy.h" #include "profiles/profilemodel.hpp" #include "project/projectmanager.h" #include "qmlmanager.h" #include "recmanager.h" #include "scopes/monitoraudiolevel.h" #include "timeline2/model/snapmodel.hpp" #include "transitions/transitionsrepository.hpp" #include "klocalizedstring.h" #include #include #include #include #include #include #include #include #include "kdenlive_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define SEEK_INACTIVE (-1) QuickEventEater::QuickEventEater(QObject *parent) : QObject(parent) { } bool QuickEventEater::eventFilter(QObject *obj, QEvent *event) { switch (event->type()) { case QEvent::DragEnter: { auto *ev = reinterpret_cast(event); if (ev->mimeData()->hasFormat(QStringLiteral("kdenlive/effect"))) { ev->acceptProposedAction(); return true; } break; } case QEvent::DragMove: { auto *ev = reinterpret_cast(event); if (ev->mimeData()->hasFormat(QStringLiteral("kdenlive/effect"))) { ev->acceptProposedAction(); return true; } break; } case QEvent::Drop: { auto *ev = static_cast(event); if (ev) { QStringList effectData; effectData << QString::fromUtf8(ev->mimeData()->data(QStringLiteral("kdenlive/effect"))); QStringList source = QString::fromUtf8(ev->mimeData()->data(QStringLiteral("kdenlive/effectsource"))).split(QLatin1Char('-')); effectData << source; emit addEffect(effectData); ev->accept(); return true; } break; } default: break; } return QObject::eventFilter(obj, event); } QuickMonitorEventEater::QuickMonitorEventEater(QWidget *parent) : QObject(parent) { } bool QuickMonitorEventEater::eventFilter(QObject *obj, QEvent *event) { if (event->type() == QEvent::KeyPress) { auto *ev = static_cast(event); if (ev) { emit doKeyPressEvent(ev); return true; } } return QObject::eventFilter(obj, event); } Monitor::Monitor(Kdenlive::MonitorId id, MonitorManager *manager, QWidget *parent) : AbstractMonitor(id, manager, parent) , m_controller(nullptr) , m_glMonitor(nullptr) , m_snaps(new SnapModel()) , m_splitEffect(nullptr) , m_splitProducer(nullptr) , m_dragStarted(false) , m_recManager(nullptr) , m_loopClipAction(nullptr) , m_sceneVisibilityAction(nullptr) , m_multitrackView(nullptr) , m_contextMenu(nullptr) , m_loopClipTransition(true) , m_editMarker(nullptr) , m_forceSizeFactor(0) , m_lastMonitorSceneType(MonitorSceneDefault) { auto *layout = new QVBoxLayout; layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(0); // Create container widget m_glWidget = new QWidget; auto *glayout = new QGridLayout(m_glWidget); glayout->setSpacing(0); glayout->setContentsMargins(0, 0, 0, 0); // Create QML OpenGL widget m_glMonitor = new GLWidget((int)id); connect(m_glMonitor, &GLWidget::passKeyEvent, this, &Monitor::doKeyPressEvent); connect(m_glMonitor, &GLWidget::panView, this, &Monitor::panView); connect(m_glMonitor, &GLWidget::seekPosition, this, &Monitor::seekPosition, Qt::DirectConnection); connect(m_glMonitor, &GLWidget::consumerPosition, this, &Monitor::slotSeekPosition, Qt::DirectConnection); connect(m_glMonitor, &GLWidget::activateMonitor, this, &AbstractMonitor::slotActivateMonitor, Qt::DirectConnection); m_videoWidget = QWidget::createWindowContainer(qobject_cast(m_glMonitor)); m_videoWidget->setAcceptDrops(true); auto *leventEater = new QuickEventEater(this); m_videoWidget->installEventFilter(leventEater); connect(leventEater, &QuickEventEater::addEffect, this, &Monitor::slotAddEffect); m_qmlManager = new QmlManager(m_glMonitor); connect(m_qmlManager, &QmlManager::effectChanged, this, &Monitor::effectChanged); connect(m_qmlManager, &QmlManager::effectPointsChanged, this, &Monitor::effectPointsChanged); auto *monitorEventEater = new QuickMonitorEventEater(this); m_glWidget->installEventFilter(monitorEventEater); connect(monitorEventEater, &QuickMonitorEventEater::doKeyPressEvent, this, &Monitor::doKeyPressEvent); glayout->addWidget(m_videoWidget, 0, 0); m_verticalScroll = new QScrollBar(Qt::Vertical); glayout->addWidget(m_verticalScroll, 0, 1); m_verticalScroll->hide(); m_horizontalScroll = new QScrollBar(Qt::Horizontal); glayout->addWidget(m_horizontalScroll, 1, 0); m_horizontalScroll->hide(); connect(m_horizontalScroll, &QAbstractSlider::valueChanged, this, &Monitor::setOffsetX); connect(m_verticalScroll, &QAbstractSlider::valueChanged, this, &Monitor::setOffsetY); connect(m_glMonitor, &GLWidget::frameDisplayed, this, &Monitor::onFrameDisplayed); connect(m_glMonitor, &GLWidget::mouseSeek, this, &Monitor::slotMouseSeek); connect(m_glMonitor, &GLWidget::monitorPlay, this, &Monitor::slotPlay); connect(m_glMonitor, &GLWidget::startDrag, this, &Monitor::slotStartDrag); connect(m_glMonitor, &GLWidget::switchFullScreen, this, &Monitor::slotSwitchFullScreen); connect(m_glMonitor, &GLWidget::zoomChanged, this, &Monitor::setZoom); connect(m_glMonitor, SIGNAL(lockMonitor(bool)), this, SLOT(slotLockMonitor(bool)), Qt::DirectConnection); connect(m_glMonitor, &GLWidget::showContextMenu, this, &Monitor::slotShowMenu); connect(m_glMonitor, &GLWidget::gpuNotSupported, this, &Monitor::gpuError); m_glWidget->setMinimumSize(QSize(320, 180)); layout->addWidget(m_glWidget, 10); layout->addStretch(); // Tool bar buttons m_toolbar = new QToolBar(this); QWidget *sp1 = new QWidget(this); sp1->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); m_toolbar->addWidget(sp1); if (id == Kdenlive::ClipMonitor) { // Add options for recording m_recManager = new RecManager(this); connect(m_recManager, &RecManager::warningMessage, this, &Monitor::warningMessage); connect(m_recManager, &RecManager::addClipToProject, this, &Monitor::addClipToProject); m_toolbar->addAction(manager->getAction(QStringLiteral("insert_project_tree"))); m_toolbar->setToolTip(i18n("Insert Zone to Project Bin")); m_toolbar->addSeparator(); } if (id != Kdenlive::DvdMonitor) { QAction *markIn = new QAction(QIcon::fromTheme(QStringLiteral("zone-in")), i18n("Set Zone In"), this); QAction *markOut = new QAction(QIcon::fromTheme(QStringLiteral("zone-out")), i18n("Set Zone Out"), this); m_toolbar->addAction(markIn); m_toolbar->addAction(markOut); connect(markIn, &QAction::triggered, [&, manager]() { m_monitorManager->activateMonitor(m_id); manager->getAction(QStringLiteral("mark_in"))->trigger(); }); connect(markOut, &QAction::triggered, [&, manager]() { m_monitorManager->activateMonitor(m_id); manager->getAction(QStringLiteral("mark_out"))->trigger(); }); } m_toolbar->addAction(manager->getAction(QStringLiteral("monitor_seek_backward"))); auto *playButton = new QToolButton(m_toolbar); m_playMenu = new QMenu(i18n("Play..."), this); QAction *originalPlayAction = static_cast(manager->getAction(QStringLiteral("monitor_play"))); m_playAction = new KDualAction(i18n("Play"), i18n("Pause"), this); m_playAction->setInactiveIcon(QIcon::fromTheme(QStringLiteral("media-playback-start"))); m_playAction->setActiveIcon(QIcon::fromTheme(QStringLiteral("media-playback-pause"))); QString strippedTooltip = m_playAction->toolTip().remove(QRegExp(QStringLiteral("\\s\\(.*\\)"))); // append shortcut if it exists for action if (originalPlayAction->shortcut() == QKeySequence(0)) { m_playAction->setToolTip(strippedTooltip); } else { m_playAction->setToolTip(strippedTooltip + QStringLiteral(" (") + originalPlayAction->shortcut().toString() + QLatin1Char(')')); } m_playMenu->addAction(m_playAction); connect(m_playAction, &QAction::triggered, this, &Monitor::slotSwitchPlay); playButton->setMenu(m_playMenu); playButton->setPopupMode(QToolButton::MenuButtonPopup); m_toolbar->addWidget(playButton); m_toolbar->addAction(manager->getAction(QStringLiteral("monitor_seek_forward"))); playButton->setDefaultAction(m_playAction); m_configMenu = new QMenu(i18n("Misc..."), this); if (id != Kdenlive::DvdMonitor) { if (id == Kdenlive::ClipMonitor) { m_markerMenu = new QMenu(i18n("Go to marker..."), this); } else { m_markerMenu = new QMenu(i18n("Go to guide..."), this); } m_markerMenu->setEnabled(false); m_configMenu->addMenu(m_markerMenu); connect(m_markerMenu, &QMenu::triggered, this, &Monitor::slotGoToMarker); m_forceSize = new KSelectAction(QIcon::fromTheme(QStringLiteral("transform-scale")), i18n("Force Monitor Size"), this); QAction *fullAction = m_forceSize->addAction(QIcon(), i18n("Force 100%")); fullAction->setData(100); QAction *halfAction = m_forceSize->addAction(QIcon(), i18n("Force 50%")); halfAction->setData(50); QAction *freeAction = m_forceSize->addAction(QIcon(), i18n("Free Resize")); freeAction->setData(0); m_configMenu->addAction(m_forceSize); m_forceSize->setCurrentAction(freeAction); connect(m_forceSize, static_cast(&KSelectAction::triggered), this, &Monitor::slotForceSize); } // Create Volume slider popup m_audioSlider = new QSlider(Qt::Vertical); m_audioSlider->setRange(0, 100); m_audioSlider->setValue(KdenliveSettings::volume()); connect(m_audioSlider, &QSlider::valueChanged, this, &Monitor::slotSetVolume); auto *widgetslider = new QWidgetAction(this); widgetslider->setText(i18n("Audio volume")); widgetslider->setDefaultWidget(m_audioSlider); auto *menu = new QMenu(i18n("Volume"), this); menu->setIcon(QIcon::fromTheme(QStringLiteral("audio-volume-medium"))); menu->addAction(widgetslider); m_configMenu->addMenu(menu); /*QIcon icon; if (KdenliveSettings::volume() == 0) { icon = QIcon::fromTheme(QStringLiteral("audio-volume-muted")); } else { icon = QIcon::fromTheme(QStringLiteral("audio-volume-medium")); } m_audioButton->setIcon(icon);*/ setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); setLayout(layout); setMinimumHeight(200); connect(this, &Monitor::scopesClear, m_glMonitor, &GLWidget::releaseAnalyse, Qt::DirectConnection); connect(m_glMonitor, &GLWidget::analyseFrame, this, &Monitor::frameUpdated); connect(m_glMonitor, &GLWidget::audioSamplesSignal, this, &Monitor::audioSamplesSignal); if (id != Kdenlive::ClipMonitor) { // TODO: reimplement // connect(render, &Render::durationChanged, this, &Monitor::durationChanged); connect(m_glMonitor->getControllerProxy(), &MonitorProxy::saveZone, this, &Monitor::updateTimelineClipZone); } else { connect(m_glMonitor->getControllerProxy(), &MonitorProxy::saveZone, this, &Monitor::updateClipZone); } connect(m_glMonitor->getControllerProxy(), &MonitorProxy::triggerAction, pCore.get(), &Core::triggerAction); connect(m_glMonitor->getControllerProxy(), &MonitorProxy::seekNextKeyframe, this, &Monitor::seekToNextKeyframe); connect(m_glMonitor->getControllerProxy(), &MonitorProxy::seekPreviousKeyframe, this, &Monitor::seekToPreviousKeyframe); connect(m_glMonitor->getControllerProxy(), &MonitorProxy::addRemoveKeyframe, this, &Monitor::addRemoveKeyframe); connect(m_glMonitor->getControllerProxy(), &MonitorProxy::seekToKeyframe, this, &Monitor::slotSeekToKeyFrame); m_sceneVisibilityAction = new QAction(QIcon::fromTheme(QStringLiteral("transform-crop")), i18n("Show/Hide edit mode"), this); m_sceneVisibilityAction->setCheckable(true); m_sceneVisibilityAction->setChecked(KdenliveSettings::showOnMonitorScene()); connect(m_sceneVisibilityAction, &QAction::triggered, this, &Monitor::slotEnableEffectScene); m_toolbar->addAction(m_sceneVisibilityAction); m_toolbar->addSeparator(); m_timePos = new TimecodeDisplay(m_monitorManager->timecode(), this); m_toolbar->addWidget(m_timePos); auto *configButton = new QToolButton(m_toolbar); configButton->setIcon(QIcon::fromTheme(QStringLiteral("kdenlive-menu"))); configButton->setToolTip(i18n("Options")); configButton->setMenu(m_configMenu); configButton->setPopupMode(QToolButton::InstantPopup); m_toolbar->addWidget(configButton); if (m_recManager) { m_toolbar->addAction(m_recManager->switchAction()); } /*QWidget *spacer = new QWidget(this); spacer->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); m_toolbar->addWidget(spacer);*/ m_toolbar->addSeparator(); int tm = 0; int bm = 0; m_toolbar->getContentsMargins(nullptr, &tm, nullptr, &bm); m_audioMeterWidget = new MonitorAudioLevel(m_toolbar->height() - tm - bm, this); m_toolbar->addWidget(m_audioMeterWidget); if (!m_audioMeterWidget->isValid) { KdenliveSettings::setMonitoraudio(0x01); m_audioMeterWidget->setVisibility(false); } else { m_audioMeterWidget->setVisibility((KdenliveSettings::monitoraudio() & m_id) != 0); } connect(m_timePos, SIGNAL(timeCodeEditingFinished()), this, SLOT(slotSeek())); layout->addWidget(m_toolbar); if (m_recManager) { layout->addWidget(m_recManager->toolbar()); } // Load monitor overlay qml loadQmlScene(MonitorSceneDefault); // Info message widget m_infoMessage = new KMessageWidget(this); layout->addWidget(m_infoMessage); m_infoMessage->hide(); } Monitor::~Monitor() { delete m_splitEffect; delete m_audioMeterWidget; delete m_glMonitor; delete m_videoWidget; delete m_glWidget; delete m_timePos; } void Monitor::setOffsetX(int x) { m_glMonitor->setOffsetX(x, m_horizontalScroll->maximum()); } void Monitor::setOffsetY(int y) { m_glMonitor->setOffsetY(y, m_verticalScroll->maximum()); } void Monitor::slotGetCurrentImage(bool request) { m_glMonitor->sendFrameForAnalysis = request; m_monitorManager->activateMonitor(m_id); refreshMonitorIfActive(); if (request) { // Update analysis state QTimer::singleShot(500, m_monitorManager, &MonitorManager::checkScopes); } else { m_glMonitor->releaseAnalyse(); } } void Monitor::slotAddEffect(const QStringList &effect) { if (m_id == Kdenlive::ClipMonitor) { if (m_controller) { emit addMasterEffect(m_controller->AbstractProjectItem::clipId(), effect); } } else { emit addEffect(effect); } } void Monitor::refreshIcons() { QList allMenus = this->findChildren(); for (int i = 0; i < allMenus.count(); i++) { QAction *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++) { KDualAction *m = allButtons.at(i); QIcon ic = m->activeIcon(); if (ic.isNull() || ic.name().isEmpty()) { continue; } QIcon newIcon = QIcon::fromTheme(ic.name()); m->setActiveIcon(newIcon); ic = m->inactiveIcon(); if (ic.isNull() || ic.name().isEmpty()) { continue; } newIcon = QIcon::fromTheme(ic.name()); m->setInactiveIcon(newIcon); } } QAction *Monitor::recAction() { if (m_recManager) { return m_recManager->switchAction(); } return nullptr; } void Monitor::slotLockMonitor(bool lock) { m_monitorManager->lockMonitor(m_id, lock); } void Monitor::setupMenu(QMenu *goMenu, QMenu *overlayMenu, QAction *playZone, QAction *loopZone, QMenu *markerMenu, QAction *loopClip) { delete m_contextMenu; m_contextMenu = new QMenu(this); m_contextMenu->addMenu(m_playMenu); if (goMenu) { m_contextMenu->addMenu(goMenu); } if (markerMenu) { m_contextMenu->addMenu(markerMenu); QList list = markerMenu->actions(); for (int i = 0; i < list.count(); ++i) { if (list.at(i)->data().toString() == QLatin1String("edit_marker")) { m_editMarker = list.at(i); break; } } } m_playMenu->addAction(playZone); m_playMenu->addAction(loopZone); if (loopClip) { m_loopClipAction = loopClip; m_playMenu->addAction(loopClip); } // TODO: add save zone to timeline monitor when fixed m_contextMenu->addMenu(m_markerMenu); if (m_id == Kdenlive::ClipMonitor) { m_contextMenu->addAction(QIcon::fromTheme(QStringLiteral("document-save")), i18n("Save zone"), this, SLOT(slotSaveZone())); QAction *extractZone = m_configMenu->addAction(QIcon::fromTheme(QStringLiteral("document-new")), i18n("Extract Zone"), this, SLOT(slotExtractCurrentZone())); m_contextMenu->addAction(extractZone); } m_contextMenu->addAction(m_monitorManager->getAction(QStringLiteral("extract_frame"))); m_contextMenu->addAction(m_monitorManager->getAction(QStringLiteral("extract_frame_to_project"))); if (m_id == Kdenlive::ProjectMonitor) { m_multitrackView = m_contextMenu->addAction(QIcon::fromTheme(QStringLiteral("view-split-left-right")), i18n("Multitrack view"), this, SIGNAL(multitrackView(bool))); m_multitrackView->setCheckable(true); m_configMenu->addAction(m_multitrackView); } else if (m_id == Kdenlive::ClipMonitor) { QAction *setThumbFrame = m_contextMenu->addAction(QIcon::fromTheme(QStringLiteral("document-new")), i18n("Set current image as thumbnail"), this, SLOT(slotSetThumbFrame())); m_configMenu->addAction(setThumbFrame); } if (overlayMenu) { m_contextMenu->addMenu(overlayMenu); } QAction *overlayAudio = m_contextMenu->addAction(QIcon(), i18n("Overlay audio waveform")); overlayAudio->setCheckable(true); connect(overlayAudio, &QAction::toggled, m_glMonitor, &GLWidget::slotSwitchAudioOverlay); overlayAudio->setChecked(KdenliveSettings::displayAudioOverlay()); m_configMenu->addAction(overlayAudio); QAction *switchAudioMonitor = m_configMenu->addAction(i18n("Show Audio Levels"), this, SLOT(slotSwitchAudioMonitor())); switchAudioMonitor->setCheckable(true); switchAudioMonitor->setChecked((KdenliveSettings::monitoraudio() & m_id) != 0); // For some reason, the frame in QAbstracSpinBox (base class of TimeCodeDisplay) needs to be displayed once, then hidden // or it will never appear (supposed to appear on hover). m_timePos->setFrame(false); } void Monitor::slotGoToMarker(QAction *action) { int pos = action->data().toInt(); slotSeek(pos); } void Monitor::slotForceSize(QAction *a) { int resizeType = a->data().toInt(); int profileWidth = 320; int profileHeight = 200; if (resizeType > 0) { // calculate size QRect r = QApplication::desktop()->screenGeometry(); profileHeight = m_glMonitor->profileSize().height() * resizeType / 100; profileWidth = pCore->getCurrentProfile()->dar() * profileHeight; if (profileWidth > r.width() * 0.8 || profileHeight > r.height() * 0.7) { // reset action to free resize const QList list = m_forceSize->actions(); for (QAction *ac : list) { if (ac->data().toInt() == m_forceSizeFactor) { m_forceSize->setCurrentAction(ac); break; } } warningMessage(i18n("Your screen resolution is not sufficient for this action")); return; } } switch (resizeType) { case 100: case 50: // resize full size setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); m_videoWidget->setMinimumSize(profileWidth, profileHeight); m_videoWidget->setMaximumSize(profileWidth, profileHeight); setMinimumSize(QSize(profileWidth, profileHeight + m_toolbar->height() + m_glMonitor->getControllerProxy()->rulerHeight())); break; default: // Free resize m_videoWidget->setMinimumSize(profileWidth, profileHeight); m_videoWidget->setMaximumSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX); setMinimumSize(QSize(profileWidth, profileHeight + m_toolbar->height() + m_glMonitor->getControllerProxy()->rulerHeight())); setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); break; } m_forceSizeFactor = resizeType; updateGeometry(); } QString Monitor::getTimecodeFromFrames(int pos) { return m_monitorManager->timecode().getTimecodeFromFrames(pos); } double Monitor::fps() const { return m_monitorManager->timecode().fps(); } Timecode Monitor::timecode() const { return m_monitorManager->timecode(); } void Monitor::updateMarkers() { if (m_controller) { m_markerMenu->clear(); QList markers = m_controller->getMarkerModel()->getAllMarkers(); if (!markers.isEmpty()) { for (int i = 0; i < markers.count(); ++i) { int pos = (int)markers.at(i).time().frames(m_monitorManager->timecode().fps()); QString position = m_monitorManager->timecode().getTimecode(markers.at(i).time()) + QLatin1Char(' ') + markers.at(i).comment(); QAction *go = m_markerMenu->addAction(position); go->setData(pos); } } m_markerMenu->setEnabled(!m_markerMenu->isEmpty()); } } void Monitor::setGuides(const QMap &guides) { // TODO: load guides model m_markerMenu->clear(); QMapIterator i(guides); QList guidesList; while (i.hasNext()) { i.next(); CommentedTime timeGuide(GenTime(i.key()), i.value()); guidesList << timeGuide; int pos = (int)timeGuide.time().frames(m_monitorManager->timecode().fps()); QString position = m_monitorManager->timecode().getTimecode(timeGuide.time()) + QLatin1Char(' ') + timeGuide.comment(); QAction *go = m_markerMenu->addAction(position); go->setData(pos); } // m_ruler->setMarkers(guidesList); m_markerMenu->setEnabled(!m_markerMenu->isEmpty()); checkOverlay(); } void Monitor::slotSeekToPreviousSnap() { if (m_controller) { m_glMonitor->seek(getSnapForPos(true).frames(m_monitorManager->timecode().fps())); } } void Monitor::slotSeekToNextSnap() { if (m_controller) { m_glMonitor->seek(getSnapForPos(false).frames(m_monitorManager->timecode().fps())); } } int Monitor::position() { return m_glMonitor->getCurrentPos(); } GenTime Monitor::getSnapForPos(bool previous) { int frame = previous ? m_snaps->getPreviousPoint(m_glMonitor->getCurrentPos()) : m_snaps->getNextPoint(m_glMonitor->getCurrentPos()); return {frame, pCore->getCurrentFps()}; } void Monitor::slotLoadClipZone(const QPoint &zone) { m_glMonitor->getControllerProxy()->setZone(zone.x(), zone.y()); checkOverlay(); } void Monitor::slotSetZoneStart() { m_glMonitor->getControllerProxy()->setZoneIn(m_glMonitor->getCurrentPos()); if (m_controller) { m_controller->setZone(m_glMonitor->getControllerProxy()->zone()); } else { // timeline emit timelineZoneChanged(); } checkOverlay(); } void Monitor::slotSetZoneEnd(bool discardLastFrame) { Q_UNUSED(discardLastFrame); int pos = m_glMonitor->getCurrentPos(); if (m_controller) { if (pos < (int)m_controller->frameDuration() - 1) { pos++; } } else pos++; m_glMonitor->getControllerProxy()->setZoneOut(pos); if (m_controller) { m_controller->setZone(m_glMonitor->getControllerProxy()->zone()); } checkOverlay(); } // virtual void Monitor::mousePressEvent(QMouseEvent *event) { m_monitorManager->activateMonitor(m_id); if ((event->button() & Qt::RightButton) == 0u) { if (m_glWidget->geometry().contains(event->pos())) { m_DragStartPosition = event->pos(); event->accept(); } } else if (m_contextMenu) { slotActivateMonitor(); m_contextMenu->popup(event->globalPos()); event->accept(); } QWidget::mousePressEvent(event); } void Monitor::slotShowMenu(const QPoint pos) { slotActivateMonitor(); if (m_contextMenu) { m_contextMenu->popup(pos); } } void Monitor::resizeEvent(QResizeEvent *event) { Q_UNUSED(event) if (m_glMonitor->zoom() > 0.0f) { float horizontal = float(m_horizontalScroll->value()) / float(m_horizontalScroll->maximum()); float vertical = float(m_verticalScroll->value()) / float(m_verticalScroll->maximum()); adjustScrollBars(horizontal, vertical); } else { m_horizontalScroll->hide(); m_verticalScroll->hide(); } } void Monitor::adjustScrollBars(float horizontal, float vertical) { if (m_glMonitor->zoom() > 1.0f) { m_horizontalScroll->setPageStep(m_glWidget->width()); m_horizontalScroll->setMaximum((int)((float)m_glMonitor->profileSize().width() * m_glMonitor->zoom()) - m_horizontalScroll->pageStep()); m_horizontalScroll->setValue(qRound(horizontal * float(m_horizontalScroll->maximum()))); emit m_horizontalScroll->valueChanged(m_horizontalScroll->value()); m_horizontalScroll->show(); } else { int max = (int)((float)m_glMonitor->profileSize().width() * m_glMonitor->zoom()) - m_glWidget->width(); emit m_horizontalScroll->valueChanged(qRound(0.5 * max)); m_horizontalScroll->hide(); } if (m_glMonitor->zoom() > 1.0f) { m_verticalScroll->setPageStep(m_glWidget->height()); m_verticalScroll->setMaximum((int)((float)m_glMonitor->profileSize().height() * m_glMonitor->zoom()) - m_verticalScroll->pageStep()); m_verticalScroll->setValue((int)((float)m_verticalScroll->maximum() * vertical)); emit m_verticalScroll->valueChanged(m_verticalScroll->value()); m_verticalScroll->show(); } else { int max = (int)((float)m_glMonitor->profileSize().height() * m_glMonitor->zoom()) - m_glWidget->height(); emit m_verticalScroll->valueChanged(qRound(0.5 * max)); m_verticalScroll->hide(); } } void Monitor::setZoom() { if (qFuzzyCompare(m_glMonitor->zoom(), 1.0f)) { m_horizontalScroll->hide(); m_verticalScroll->hide(); m_glMonitor->setOffsetX(m_horizontalScroll->value(), m_horizontalScroll->maximum()); m_glMonitor->setOffsetY(m_verticalScroll->value(), m_verticalScroll->maximum()); } else { adjustScrollBars(0.5f, 0.5f); } } void Monitor::slotSwitchFullScreen(bool minimizeOnly) { // TODO: disable screensaver? if (!m_glWidget->isFullScreen() && !minimizeOnly) { // Check if we have a multiple monitor setup int monitors = QApplication::desktop()->screenCount(); int screen = -1; if (monitors > 1) { QRect screenres; // Move monitor widget to the second screen (one screen for Kdenlive, the other one for the Monitor widget // int currentScreen = QApplication::desktop()->screenNumber(this); for (int i = 0; screen == -1 && i < QApplication::desktop()->screenCount(); i++) { if (i != QApplication::desktop()->screenNumber(this->parentWidget()->parentWidget())) { screen = i; } } } m_qmlManager->enableAudioThumbs(false); m_glWidget->setParent(QApplication::desktop()->screen(screen)); m_glWidget->move(QApplication::desktop()->screenGeometry(screen).bottomLeft()); m_glWidget->showFullScreen(); } else { m_glWidget->showNormal(); m_qmlManager->enableAudioThumbs(true); auto *lay = (QVBoxLayout *)layout(); lay->insertWidget(0, m_glWidget, 10); } } void Monitor::reparent() { m_glWidget->setParent(nullptr); m_glWidget->showMinimized(); m_glWidget->showNormal(); auto *lay = (QVBoxLayout *)layout(); lay->insertWidget(0, m_glWidget, 10); } // virtual void Monitor::mouseReleaseEvent(QMouseEvent *event) { if (m_dragStarted) { event->ignore(); return; } if (event->button() != Qt::RightButton) { if (m_glMonitor->geometry().contains(event->pos())) { if (isActive()) { slotPlay(); } else { slotActivateMonitor(); } } // else event->ignore(); //QWidget::mouseReleaseEvent(event); } m_dragStarted = false; event->accept(); QWidget::mouseReleaseEvent(event); } void Monitor::slotStartDrag() { if (m_id == Kdenlive::ProjectMonitor || m_controller == nullptr) { // dragging is only allowed for clip monitor return; } auto *drag = new QDrag(this); auto *mimeData = new QMimeData; // Get drag state QQuickItem *root = m_glMonitor->rootObject(); int dragType = 0; if (root) { dragType = root->property("dragType").toInt(); root->setProperty("dragType", 0); } QByteArray prodData; QPoint p = m_glMonitor->getControllerProxy()->zone(); if (p.x() == -1 || p.y() == -1) { prodData = m_controller->AbstractProjectItem::clipId().toUtf8(); } else { QStringList list; list.append(m_controller->AbstractProjectItem::clipId()); list.append(QString::number(p.x())); list.append(QString::number(p.y() - 1)); prodData.append(list.join(QLatin1Char('/')).toUtf8()); } switch (dragType) { case 1: // Audio only drag prodData.prepend('A'); break; case 2: // Audio only drag prodData.prepend('V'); break; default: break; } mimeData->setData(QStringLiteral("kdenlive/producerslist"), prodData); drag->setMimeData(mimeData); /*QPixmap pix = m_currentClip->thumbnail(); drag->setPixmap(pix); drag->setHotSpot(QPoint(0, 50));*/ drag->start(Qt::MoveAction); } void Monitor::enterEvent(QEvent *event) { m_qmlManager->enableAudioThumbs(true); QWidget::enterEvent(event); } void Monitor::leaveEvent(QEvent *event) { m_qmlManager->enableAudioThumbs(false); QWidget::leaveEvent(event); } // virtual void Monitor::mouseMoveEvent(QMouseEvent *event) { if (m_dragStarted || m_controller == nullptr) { return; } if ((event->pos() - m_DragStartPosition).manhattanLength() < QApplication::startDragDistance()) { return; } { auto *drag = new QDrag(this); auto *mimeData = new QMimeData; m_dragStarted = true; QStringList list; list.append(m_controller->AbstractProjectItem::clipId()); QPoint p = m_glMonitor->getControllerProxy()->zone(); list.append(QString::number(p.x())); list.append(QString::number(p.y())); QByteArray clipData; clipData.append(list.join(QLatin1Char(';')).toUtf8()); mimeData->setData(QStringLiteral("kdenlive/clip"), clipData); drag->setMimeData(mimeData); drag->start(Qt::MoveAction); } event->accept(); } /*void Monitor::dragMoveEvent(QDragMoveEvent * event) { event->setDropAction(Qt::IgnoreAction); event->setDropAction(Qt::MoveAction); if (event->mimeData()->hasText()) { event->acceptProposedAction(); } } Qt::DropActions Monitor::supportedDropActions() const { // returns what actions are supported when dropping return Qt::MoveAction; }*/ QStringList Monitor::mimeTypes() const { QStringList qstrList; // list of accepted MIME types for drop qstrList.append(QStringLiteral("kdenlive/clip")); return qstrList; } // virtual void Monitor::wheelEvent(QWheelEvent *event) { slotMouseSeek(event->delta(), event->modifiers()); event->accept(); } void Monitor::mouseDoubleClickEvent(QMouseEvent *event) { slotSwitchFullScreen(); event->accept(); } void Monitor::keyPressEvent(QKeyEvent *event) { if (event->key() == Qt::Key_Escape) { slotSwitchFullScreen(); event->accept(); return; } if (m_glWidget->isFullScreen()) { event->ignore(); emit passKeyPress(event); return; } QWidget::keyPressEvent(event); } void Monitor::slotMouseSeek(int eventDelta, uint modifiers) { if ((modifiers & Qt::ControlModifier) != 0u) { int delta = m_monitorManager->timecode().fps(); if (eventDelta > 0) { delta = 0 - delta; } m_glMonitor->seek(m_glMonitor->getCurrentPos() - delta); } else if ((modifiers & Qt::AltModifier) != 0u) { if (eventDelta >= 0) { emit seekToPreviousSnap(); } else { emit seekToNextSnap(); } } else { if (eventDelta >= 0) { slotRewindOneFrame(); } else { slotForwardOneFrame(); } } } void Monitor::slotSetThumbFrame() { if (m_controller == nullptr) { return; } m_controller->setProducerProperty(QStringLiteral("kdenlive:thumbnailFrame"), m_glMonitor->getCurrentPos()); emit refreshClipThumbnail(m_controller->AbstractProjectItem::clipId()); } void Monitor::slotExtractCurrentZone() { if (m_controller == nullptr) { return; } emit extractZone(m_controller->AbstractProjectItem::clipId()); } std::shared_ptr Monitor::currentController() const { return m_controller; } void Monitor::slotExtractCurrentFrame(QString frameName, bool addToProject) { if (QFileInfo(frameName).fileName().isEmpty()) { // convenience: when extracting an image to be added to the project, // suggest a suitable image file name. In the project monitor, this // suggestion bases on the project file name; in the clip monitor, // the suggestion bases on the clip file name currently shown. // Finally, the frame number is added to this suggestion, prefixed // with "-f", so we get something like clip-f#.png. QString suggestedImageName = QFileInfo(currentController() ? currentController()->clipName() : pCore->currentDoc()->url().isValid() ? pCore->currentDoc()->url().fileName() : i18n("untitled")) .completeBaseName() + QStringLiteral("-f") + QString::number(m_glMonitor->getCurrentPos()).rightJustified(6, QLatin1Char('0')) + QStringLiteral(".png"); frameName = QFileInfo(frameName, suggestedImageName).fileName(); } QString framesFolder = KRecentDirs::dir(QStringLiteral(":KdenliveFramesFolder")); if (framesFolder.isEmpty()) { framesFolder = QDir::homePath(); } QScopedPointer dlg(new QDialog(this)); QScopedPointer fileWidget(new KFileWidget(QUrl::fromLocalFile(framesFolder), dlg.data())); dlg->setWindowTitle(addToProject ? i18n("Save Image") : i18n("Save Image to Project")); auto *layout = new QVBoxLayout; layout->addWidget(fileWidget.data()); QCheckBox *b = nullptr; if (m_id == Kdenlive::ClipMonitor) { b = new QCheckBox(i18n("Export image using source resolution"), dlg.data()); b->setChecked(KdenliveSettings::exportframe_usingsourceres()); fileWidget->setCustomWidget(b); } fileWidget->setConfirmOverwrite(true); fileWidget->okButton()->show(); fileWidget->cancelButton()->show(); QObject::connect(fileWidget->okButton(), &QPushButton::clicked, fileWidget.data(), &KFileWidget::slotOk); QObject::connect(fileWidget.data(), &KFileWidget::accepted, fileWidget.data(), &KFileWidget::accept); QObject::connect(fileWidget.data(), &KFileWidget::accepted, dlg.data(), &QDialog::accept); QObject::connect(fileWidget->cancelButton(), &QPushButton::clicked, dlg.data(), &QDialog::reject); dlg->setLayout(layout); fileWidget->setMimeFilter(QStringList() << QStringLiteral("image/png")); fileWidget->setMode(KFile::File | KFile::LocalOnly); fileWidget->setOperationMode(KFileWidget::Saving); QUrl relativeUrl; relativeUrl.setPath(frameName); #if KIO_VERSION >= QT_VERSION_CHECK(5, 33, 0) fileWidget->setSelectedUrl(relativeUrl); #else fileWidget->setSelection(relativeUrl.toString()); #endif KSharedConfig::Ptr conf = KSharedConfig::openConfig(); QWindow *handle = dlg->windowHandle(); if ((handle != nullptr) && conf->hasGroup("FileDialogSize")) { KWindowConfig::restoreWindowSize(handle, conf->group("FileDialogSize")); dlg->resize(handle->size()); } if (dlg->exec() == QDialog::Accepted) { QString selectedFile = fileWidget->selectedFile(); if (!selectedFile.isEmpty()) { // Create Qimage with frame QImage frame; // check if we are using a proxy if ((m_controller != nullptr) && !m_controller->getProducerProperty(QStringLiteral("kdenlive:proxy")).isEmpty() && m_controller->getProducerProperty(QStringLiteral("kdenlive:proxy")) != QLatin1String("-")) { // using proxy, use original clip url to get frame frame = m_glMonitor->getControllerProxy()->extractFrame(m_glMonitor->getCurrentPos(), m_controller->getProducerProperty(QStringLiteral("kdenlive:originalurl")), -1, -1, b != nullptr ? b->isChecked() : false); } else { frame = m_glMonitor->getControllerProxy()->extractFrame(m_glMonitor->getCurrentPos(), QString(), -1, -1, b != nullptr ? b->isChecked() : false); } frame.save(selectedFile); if (b != nullptr) { KdenliveSettings::setExportframe_usingsourceres(b->isChecked()); } KRecentDirs::add(QStringLiteral(":KdenliveFramesFolder"), QUrl::fromLocalFile(selectedFile).adjusted(QUrl::RemoveFilename).toLocalFile()); if (addToProject) { QStringList folderInfo = pCore->bin()->getFolderInfo(); pCore->bin()->droppedUrls(QList() << QUrl::fromLocalFile(selectedFile), folderInfo); } } } } void Monitor::setTimePos(const QString &pos) { m_timePos->setValue(pos); slotSeek(); } void Monitor::slotSeek() { slotSeek(m_timePos->getValue()); } void Monitor::slotSeek(int pos) { slotActivateMonitor(); m_glMonitor->seek(pos); } void Monitor::checkOverlay(int pos) { if (m_qmlManager->sceneType() != MonitorSceneDefault) { // we are not in main view, ignore return; } QString overlayText; if (pos == -1) { pos = m_timePos->getValue(); } QPoint zone = m_glMonitor->getControllerProxy()->zone(); std::shared_ptr model; if (m_id == Kdenlive::ClipMonitor && m_controller) { model = m_controller->getMarkerModel(); } else if (m_id == Kdenlive::ProjectMonitor && pCore->currentDoc()) { model = pCore->currentDoc()->getGuideModel(); } if (model) { bool found = false; CommentedTime marker = model->getMarker(GenTime(pos, m_monitorManager->timecode().fps()), &found); if (!found) { if (pos == zone.x()) { overlayText = i18n("In Point"); } else if (pos == zone.y() - 1) { overlayText = i18n("Out Point"); } } else { overlayText = marker.comment(); } } m_glMonitor->getControllerProxy()->setMarkerComment(overlayText); } int Monitor::getZoneStart() { return m_glMonitor->getControllerProxy()->zoneIn(); } int Monitor::getZoneEnd() { return m_glMonitor->getControllerProxy()->zoneOut(); } void Monitor::slotZoneStart() { slotActivateMonitor(); m_glMonitor->getControllerProxy()->pauseAndSeek(m_glMonitor->getControllerProxy()->zoneIn()); } void Monitor::slotZoneEnd() { slotActivateMonitor(); m_glMonitor->getControllerProxy()->pauseAndSeek(m_glMonitor->getControllerProxy()->zoneOut() - 1); } void Monitor::slotRewind(double speed) { slotActivateMonitor(); if (qFuzzyIsNull(speed)) { double currentspeed = m_glMonitor->playSpeed(); if (currentspeed > -1) { speed = -1; } else { speed = currentspeed * 1.5; } } m_glMonitor->switchPlay(true, speed); m_playAction->setActive(true); } void Monitor::slotForward(double speed) { slotActivateMonitor(); if (qFuzzyIsNull(speed)) { double currentspeed = m_glMonitor->playSpeed(); if (currentspeed < 1) { speed = 1; } else { speed = currentspeed * 1.2; } } m_glMonitor->switchPlay(true, speed); m_playAction->setActive(true); } void Monitor::slotRewindOneFrame(int diff) { slotActivateMonitor(); m_glMonitor->seek(m_glMonitor->getCurrentPos() - diff); } void Monitor::slotForwardOneFrame(int diff) { slotActivateMonitor(); m_glMonitor->seek(m_glMonitor->getCurrentPos() + diff); } void Monitor::seekCursor(int pos) { Q_UNUSED(pos) // Deprecated should not be used, instead requestSeek /*if (m_ruler->slotNewValue(pos)) { m_timePos->setValue(pos); checkOverlay(pos); if (m_id != Kdenlive::ClipMonitor) { emit renderPosition(pos); } }*/ } void Monitor::adjustRulerSize(int length, const std::shared_ptr &markerModel) { if (m_controller != nullptr) { m_glMonitor->setRulerInfo(length); } else { m_glMonitor->setRulerInfo(length, markerModel); } m_timePos->setRange(0, length); if (markerModel) { connect(markerModel.get(), SIGNAL(dataChanged(const QModelIndex &, const QModelIndex &, const QVector &)), this, SLOT(checkOverlay())); connect(markerModel.get(), SIGNAL(rowsInserted(const QModelIndex &, int, int)), this, SLOT(checkOverlay())); connect(markerModel.get(), SIGNAL(rowsRemoved(const QModelIndex &, int, int)), this, SLOT(checkOverlay())); } } void Monitor::stop() { m_playAction->setActive(false); m_glMonitor->stop(); } void Monitor::mute(bool mute, bool updateIconOnly) { // TODO: we should set the "audio_off" property to 1 to mute the consumer instead of changing volume QIcon icon; if (mute || KdenliveSettings::volume() == 0) { icon = QIcon::fromTheme(QStringLiteral("audio-volume-muted")); } else { icon = QIcon::fromTheme(QStringLiteral("audio-volume-medium")); } if (!updateIconOnly) { m_glMonitor->setVolume(mute ? 0 : (double)KdenliveSettings::volume() / 100.0); } } void Monitor::start() { if (!isVisible() || !isActive()) { return; } m_glMonitor->startConsumer(); } void Monitor::slotRefreshMonitor(bool visible) { if (visible) { if (slotActivateMonitor()) { start(); } } } void Monitor::refreshMonitorIfActive(bool directUpdate) { if (isActive()) { if (directUpdate) { m_glMonitor->refresh(); } else { m_glMonitor->requestRefresh(); } } } void Monitor::pause() { if (!m_playAction->isActive()) { return; } slotActivateMonitor(); m_glMonitor->switchPlay(false); m_playAction->setActive(false); } void Monitor::switchPlay(bool play) { m_playAction->setActive(play); m_glMonitor->switchPlay(play); } void Monitor::slotSwitchPlay() { slotActivateMonitor(); m_glMonitor->switchPlay(m_playAction->isActive()); } void Monitor::slotPlay() { m_playAction->trigger(); } void Monitor::slotPlayZone() { slotActivateMonitor(); bool ok = m_glMonitor->playZone(); if (ok) { m_playAction->setActive(true); } } void Monitor::slotLoopZone() { slotActivateMonitor(); bool ok = m_glMonitor->playZone(true); if (ok) { m_playAction->setActive(true); } } void Monitor::slotLoopClip() { slotActivateMonitor(); bool ok = m_glMonitor->loopClip(); if (ok) { m_playAction->setActive(true); } } void Monitor::updateClipProducer(const std::shared_ptr &prod) { if (m_glMonitor->setProducer(prod, isActive(), -1)) { prod->set_speed(1.0); } } void Monitor::updateClipProducer(const QString &playlist) { Q_UNUSED(playlist) // TODO // Mlt::Producer *prod = new Mlt::Producer(*m_glMonitor->profile(), playlist.toUtf8().constData()); // m_glMonitor->setProducer(prod, isActive(), render->seekFramePosition()); m_glMonitor->switchPlay(true); } void Monitor::slotOpenClip(const std::shared_ptr &controller, int in, int out) { if (m_controller) { disconnect(m_controller->getMarkerModel().get(), SIGNAL(dataChanged(const QModelIndex &, const QModelIndex &, const QVector &)), this, SLOT(checkOverlay())); disconnect(m_controller->getMarkerModel().get(), SIGNAL(rowsInserted(const QModelIndex &, int, int)), this, SLOT(checkOverlay())); disconnect(m_controller->getMarkerModel().get(), SIGNAL(rowsRemoved(const QModelIndex &, int, int)), this, SLOT(checkOverlay())); } m_controller = controller; loadQmlScene(MonitorSceneDefault); m_snaps.reset(new SnapModel()); m_glMonitor->getControllerProxy()->resetZone(); if (controller) { connect(m_controller->getMarkerModel().get(), SIGNAL(dataChanged(const QModelIndex &, const QModelIndex &, const QVector &)), this, SLOT(checkOverlay())); connect(m_controller->getMarkerModel().get(), SIGNAL(rowsInserted(const QModelIndex &, int, int)), this, SLOT(checkOverlay())); connect(m_controller->getMarkerModel().get(), SIGNAL(rowsRemoved(const QModelIndex &, int, int)), this, SLOT(checkOverlay())); if (m_recManager->toolbar()->isVisible()) { // we are in record mode, don't display clip return; } m_glMonitor->setRulerInfo((int)m_controller->frameDuration(), controller->getMarkerModel()); m_timePos->setRange(0, (int)m_controller->frameDuration()); updateMarkers(); connect(m_glMonitor->getControllerProxy(), &MonitorProxy::addSnap, this, &Monitor::addSnapPoint, Qt::DirectConnection); connect(m_glMonitor->getControllerProxy(), &MonitorProxy::removeSnap, this, &Monitor::removeSnapPoint, Qt::DirectConnection); if (out == -1) { m_glMonitor->getControllerProxy()->setZone(m_controller->zone(), false); qDebug() << m_controller->zone(); } else { m_glMonitor->getControllerProxy()->setZone(in, out, false); } m_snaps->addPoint((int)m_controller->frameDuration()); // Loading new clip / zone, stop if playing if (m_playAction->isActive()) { m_playAction->setActive(false); } m_glMonitor->setProducer(m_controller->originalProducer(), isActive(), in); m_audioMeterWidget->audioChannels = controller->audioInfo() ? controller->audioInfo()->channels() : 0; m_glMonitor->setAudioThumb(controller->audioChannels(), controller->audioFrameCache); m_controller->getMarkerModel()->registerSnapModel(m_snaps); // hasEffects = controller->hasEffects(); } else { m_glMonitor->setProducer(nullptr, isActive()); m_glMonitor->setAudioThumb(); m_audioMeterWidget->audioChannels = 0; } if (slotActivateMonitor()) { start(); } checkOverlay(); } const QString Monitor::activeClipId() { if (m_controller) { return m_controller->AbstractProjectItem::clipId(); } return QString(); } void Monitor::slotOpenDvdFile(const QString &file) { // TODO Q_UNUSED(file) m_glMonitor->initializeGL(); // render->loadUrl(file); } void Monitor::slotSaveZone() { // TODO? or deprecate // render->saveZone(pCore->currentDoc()->projectDataFolder(), m_ruler->zone()); } void Monitor::setCustomProfile(const QString &profile, const Timecode &tc) { // TODO or deprecate Q_UNUSED(profile) m_timePos->updateTimeCode(tc); if (/* DISABLES CODE */ (true)) { return; } slotActivateMonitor(); // render->prepareProfileReset(tc.fps()); if (m_multitrackView) { m_multitrackView->setChecked(false); } // TODO: this is a temporary profile for DVD preview, it should not alter project profile // pCore->setCurrentProfile(profile); m_glMonitor->reloadProfile(); } void Monitor::resetProfile() { m_timePos->updateTimeCode(m_monitorManager->timecode()); m_glMonitor->reloadProfile(); m_glMonitor->rootObject()->setProperty("framesize", QRect(0, 0, m_glMonitor->profileSize().width(), m_glMonitor->profileSize().height())); double fps = m_monitorManager->timecode().fps(); // Update drop frame info m_qmlManager->setProperty(QStringLiteral("dropped"), false); m_qmlManager->setProperty(QStringLiteral("fps"), QString::number(fps, 'g', 2)); } void Monitor::resetConsumer(bool fullReset) { m_glMonitor->resetConsumer(fullReset); } const QString Monitor::sceneList(const QString &root, const QString &fullPath) { return m_glMonitor->sceneList(root, fullPath); } void Monitor::updateClipZone() { if (m_controller == nullptr) { return; } m_controller->setZone(m_glMonitor->getControllerProxy()->zone()); } void Monitor::updateTimelineClipZone() { emit zoneUpdated(m_glMonitor->getControllerProxy()->zone()); } void Monitor::switchDropFrames(bool drop) { m_glMonitor->setDropFrames(drop); } void Monitor::switchMonitorInfo(int code) { int currentOverlay; if (m_id == Kdenlive::ClipMonitor) { currentOverlay = KdenliveSettings::displayClipMonitorInfo(); currentOverlay ^= code; KdenliveSettings::setDisplayClipMonitorInfo(currentOverlay); } else { currentOverlay = KdenliveSettings::displayProjectMonitorInfo(); currentOverlay ^= code; KdenliveSettings::setDisplayProjectMonitorInfo(currentOverlay); } updateQmlDisplay(currentOverlay); } void Monitor::updateMonitorGamma() { if (isActive()) { stop(); m_glMonitor->updateGamma(); start(); } else { m_glMonitor->updateGamma(); } } void Monitor::slotEditMarker() { if (m_editMarker) { m_editMarker->trigger(); } } void Monitor::updateTimecodeFormat() { m_timePos->slotUpdateTimeCodeFormat(); m_glMonitor->rootObject()->setProperty("timecode", m_timePos->displayText()); } QPoint Monitor::getZoneInfo() const { if (m_controller == nullptr) { return {}; } return m_controller->zone(); } void Monitor::slotEnableEffectScene(bool enable) { KdenliveSettings::setShowOnMonitorScene(enable); MonitorSceneType sceneType = enable ? m_lastMonitorSceneType : MonitorSceneDefault; slotShowEffectScene(sceneType, true); if (enable) { emit updateScene(); } } void Monitor::slotShowEffectScene(MonitorSceneType sceneType, bool temporary) { if (sceneType == MonitorSceneNone) { // We just want to revert to normal scene if (m_qmlManager->sceneType() == MonitorSceneSplit || m_qmlManager->sceneType() == MonitorSceneDefault) { // Ok, nothing to do return; } sceneType = MonitorSceneDefault; } if (!temporary) { m_lastMonitorSceneType = sceneType; } loadQmlScene(sceneType); } void Monitor::slotSeekToKeyFrame() { if (m_qmlManager->sceneType() == MonitorSceneGeometry) { // Adjust splitter pos int kfr = m_glMonitor->rootObject()->property("requestedKeyFrame").toInt(); emit seekToKeyframe(kfr); } } void Monitor::setUpEffectGeometry(const QRect &r, const QVariantList &list, const QVariantList &types) { QQuickItem *root = m_glMonitor->rootObject(); if (!root) { return; } if (!list.isEmpty()) { root->setProperty("centerPointsTypes", types); root->setProperty("centerPoints", list); } if (!r.isEmpty()) { root->setProperty("framesize", r); } } void Monitor::setEffectSceneProperty(const QString &name, const QVariant &value) { QQuickItem *root = m_glMonitor->rootObject(); if (!root) { return; } root->setProperty(name.toUtf8().constData(), value); } QRect Monitor::effectRect() const { QQuickItem *root = m_glMonitor->rootObject(); if (!root) { return {}; } return root->property("framesize").toRect(); } QVariantList Monitor::effectPolygon() const { QQuickItem *root = m_glMonitor->rootObject(); if (!root) { return QVariantList(); } return root->property("centerPoints").toList(); } QVariantList Monitor::effectRoto() const { QQuickItem *root = m_glMonitor->rootObject(); if (!root) { return QVariantList(); } QVariantList points = root->property("centerPoints").toList(); QVariantList controlPoints = root->property("centerPointsTypes").toList(); // rotoscoping effect needs a list of QVariantList mix; mix.reserve(points.count() * 3); for (int i = 0; i < points.count(); i++) { mix << controlPoints.at(2 * i); mix << points.at(i); mix << controlPoints.at(2 * i + 1); } return mix; } void Monitor::setEffectKeyframe(bool enable) { QQuickItem *root = m_glMonitor->rootObject(); if (root) { root->setProperty("iskeyframe", enable); } } bool Monitor::effectSceneDisplayed(MonitorSceneType effectType) { return m_qmlManager->sceneType() == effectType; } void Monitor::slotSetVolume(int volume) { KdenliveSettings::setVolume(volume); QIcon icon; double renderVolume = m_glMonitor->volume(); m_glMonitor->setVolume((double)volume / 100.0); if (renderVolume > 0 && volume > 0) { return; } /*if (volume == 0) { icon = QIcon::fromTheme(QStringLiteral("audio-volume-muted")); } else { icon = QIcon::fromTheme(QStringLiteral("audio-volume-medium")); }*/ } void Monitor::sendFrameForAnalysis(bool analyse) { m_glMonitor->sendFrameForAnalysis = analyse; } void Monitor::updateAudioForAnalysis() { m_glMonitor->updateAudioForAnalysis(); } void Monitor::onFrameDisplayed(const SharedFrame &frame) { m_monitorManager->frameDisplayed(frame); int position = frame.get_position(); if (!m_glMonitor->checkFrameNumber(position, m_id == Kdenlive::ClipMonitor ? 0 : TimelineModel::seekDuration + 1)) { m_playAction->setActive(false); } checkDrops(m_glMonitor->droppedFrames()); } void Monitor::checkDrops(int dropped) { if (m_droppedTimer.isValid()) { if (m_droppedTimer.hasExpired(1000)) { m_droppedTimer.invalidate(); double fps = m_monitorManager->timecode().fps(); if (dropped == 0) { // No dropped frames since last check m_qmlManager->setProperty(QStringLiteral("dropped"), false); m_qmlManager->setProperty(QStringLiteral("fps"), QString::number(fps, 'g', 2)); } else { m_glMonitor->resetDrops(); fps -= dropped; m_qmlManager->setProperty(QStringLiteral("dropped"), true); m_qmlManager->setProperty(QStringLiteral("fps"), QString::number(fps, 'g', 2)); m_droppedTimer.start(); } } } else if (dropped > 0) { // Start m_dropTimer m_glMonitor->resetDrops(); m_droppedTimer.start(); } } void Monitor::reloadProducer(const QString &id) { if (!m_controller) { return; } if (m_controller->AbstractProjectItem::clipId() == id) { slotOpenClip(m_controller); } } QString Monitor::getMarkerThumb(GenTime pos) { if (!m_controller) { return QString(); } if (!m_controller->getClipHash().isEmpty()) { QString url = m_monitorManager->getCacheFolder(CacheThumbs) .absoluteFilePath(m_controller->getClipHash() + QLatin1Char('#') + QString::number((int)pos.frames(m_monitorManager->timecode().fps())) + QStringLiteral(".png")); if (QFile::exists(url)) { return url; } } return QString(); } const QString Monitor::projectFolder() const { return m_monitorManager->getProjectFolder(); } void Monitor::setPalette(const QPalette &p) { QWidget::setPalette(p); 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); } QQuickItem *root = m_glMonitor->rootObject(); if (root) { QMetaObject::invokeMethod(root, "updatePalette"); } m_audioMeterWidget->refreshPixmap(); } void Monitor::gpuError() { qCWarning(KDENLIVE_LOG) << " + + + + Error initializing Movit GLSL manager"; warningMessage(i18n("Cannot initialize Movit's GLSL manager, please disable Movit"), -1); } void Monitor::warningMessage(const QString &text, int timeout, const QList &actions) { m_infoMessage->setMessageType(KMessageWidget::Warning); m_infoMessage->setText(text); for (QAction *action : actions) { m_infoMessage->addAction(action); } m_infoMessage->setCloseButtonVisible(true); m_infoMessage->animatedShow(); if (timeout > 0) { QTimer::singleShot(timeout, m_infoMessage, &KMessageWidget::animatedHide); } } void Monitor::activateSplit() { loadQmlScene(MonitorSceneSplit); if (isActive()) { m_glMonitor->requestRefresh(); } else if (slotActivateMonitor()) { start(); } } void Monitor::slotSwitchCompare(bool enable) { if (m_id == Kdenlive::ProjectMonitor) { if (enable) { if (m_qmlManager->sceneType() == MonitorSceneSplit) { // Split scene is already active return; } m_splitEffect = new Mlt::Filter(pCore->getCurrentProfile()->profile(), "frei0r.alphagrad"); if ((m_splitEffect != nullptr) && m_splitEffect->is_valid()) { m_splitEffect->set("0", 0.5); // 0 is the Clip left parameter m_splitEffect->set("1", 0); // 1 is gradient width m_splitEffect->set("2", -0.747); // 2 is tilt } else { // frei0r.scal0tilt is not available warningMessage(i18n("The alphagrad filter is required for that feature, please install frei0r and restart Kdenlive")); return; } emit createSplitOverlay(m_splitEffect); return; } // Delete temp track emit removeSplitOverlay(); delete m_splitEffect; m_splitEffect = nullptr; loadQmlScene(MonitorSceneDefault); if (isActive()) { m_glMonitor->requestRefresh(); } else if (slotActivateMonitor()) { start(); } return; } if (m_controller == nullptr || !m_controller->hasEffects()) { // disable split effect if (m_controller) { pCore->displayMessage(i18n("Clip has no effects"), InformationMessage); } else { pCore->displayMessage(i18n("Select a clip in project bin to compare effect"), InformationMessage); } return; } if (enable) { if (m_qmlManager->sceneType() == MonitorSceneSplit) { // Split scene is already active qDebug() << " . . . .. ALREADY ACTIVE"; return; } buildSplitEffect(m_controller->masterProducer()); } else if (m_splitEffect) { // TODO m_glMonitor->setProducer(m_controller->originalProducer(), isActive(), position()); delete m_splitEffect; m_splitProducer.reset(); m_splitEffect = nullptr; loadQmlScene(MonitorSceneDefault); } slotActivateMonitor(); } void Monitor::buildSplitEffect(Mlt::Producer *original) { m_splitEffect = new Mlt::Filter(pCore->getCurrentProfile()->profile(), "frei0r.alphagrad"); if ((m_splitEffect != nullptr) && m_splitEffect->is_valid()) { m_splitEffect->set("0", 0.5); // 0 is the Clip left parameter m_splitEffect->set("1", 0); // 1 is gradient width m_splitEffect->set("2", -0.747); // 2 is tilt } else { // frei0r.scal0tilt is not available pCore->displayMessage(i18n("The alphagrad filter is required for that feature, please install frei0r and restart Kdenlive"), ErrorMessage); return; } QString splitTransition = TransitionsRepository::get()->getCompositingTransition(); Mlt::Transition t(pCore->getCurrentProfile()->profile(), splitTransition.toUtf8().constData()); if (!t.is_valid()) { delete m_splitEffect; pCore->displayMessage(i18n("The cairoblend transition is required for that feature, please install frei0r and restart Kdenlive"), ErrorMessage); return; } Mlt::Tractor trac(pCore->getCurrentProfile()->profile()); std::shared_ptr clone = ProjectClip::cloneProducer(std::make_shared(original)); // Delete all effects int ct = 0; Mlt::Filter *filter = clone->filter(ct); while (filter != nullptr) { QString ix = QString::fromLatin1(filter->get("kdenlive_id")); if (!ix.isEmpty()) { if (clone->detach(*filter) == 0) { } else { ct++; } } else { ct++; } delete filter; filter = clone->filter(ct); } trac.set_track(*original, 0); trac.set_track(*clone.get(), 1); clone.get()->attach(*m_splitEffect); t.set("always_active", 1); trac.plant_transition(t, 0, 1); delete original; m_splitProducer = std::make_shared(trac.get_producer()); m_glMonitor->setProducer(m_splitProducer, isActive(), position()); m_glMonitor->setRulerInfo((int)m_controller->frameDuration(), m_controller->getMarkerModel()); loadQmlScene(MonitorSceneSplit); } QSize Monitor::profileSize() const { return m_glMonitor->profileSize(); } void Monitor::loadQmlScene(MonitorSceneType type) { if (m_id == Kdenlive::DvdMonitor || type == m_qmlManager->sceneType()) { return; } bool sceneWithEdit = type == MonitorSceneGeometry || type == MonitorSceneCorners || type == MonitorSceneRoto; if ((m_sceneVisibilityAction != nullptr) && !m_sceneVisibilityAction->isChecked() && sceneWithEdit) { // User doesn't want effect scenes pCore->displayMessage(i18n("Enable edit mode in monitor to edit effect"), InformationMessage, 500); type = MonitorSceneDefault; } double ratio = (double)m_glMonitor->profileSize().width() / (int)(m_glMonitor->profileSize().height() * pCore->getCurrentProfile()->dar() + 0.5); m_qmlManager->setScene(m_id, type, m_glMonitor->profileSize(), ratio, m_glMonitor->displayRect(), m_glMonitor->zoom(), m_timePos->maximum()); QQuickItem *root = m_glMonitor->rootObject(); switch (type) { case MonitorSceneSplit: QObject::connect(root, SIGNAL(qmlMoveSplit()), this, SLOT(slotAdjustEffectCompare()), Qt::UniqueConnection); break; case MonitorSceneGeometry: case MonitorSceneCorners: case MonitorSceneRoto: break; case MonitorSceneRipple: QObject::connect(root, SIGNAL(doAcceptRipple(bool)), this, SIGNAL(acceptRipple(bool)), Qt::UniqueConnection); QObject::connect(root, SIGNAL(switchTrimMode(int)), this, SIGNAL(switchTrimMode(int)), Qt::UniqueConnection); break; case MonitorSceneDefault: QObject::connect(root, SIGNAL(editCurrentMarker()), this, SLOT(slotEditInlineMarker()), Qt::UniqueConnection); m_qmlManager->setProperty(QStringLiteral("timecode"), m_timePos->displayText()); if (m_id == Kdenlive::ClipMonitor) { updateQmlDisplay(KdenliveSettings::displayClipMonitorInfo()); } else if (m_id == Kdenlive::ProjectMonitor) { updateQmlDisplay(KdenliveSettings::displayProjectMonitorInfo()); } break; default: break; } m_qmlManager->setProperty(QStringLiteral("fps"), QString::number(m_monitorManager->timecode().fps(), 'g', 2)); } void Monitor::setQmlProperty(const QString &name, const QVariant &value) { m_qmlManager->setProperty(name, value); } void Monitor::slotAdjustEffectCompare() { QRect r = m_glMonitor->rect(); double percent = 0.5; if (m_qmlManager->sceneType() == MonitorSceneSplit) { // Adjust splitter pos QQuickItem *root = m_glMonitor->rootObject(); percent = 0.5 - ((root->property("splitterPos").toInt() - r.left() - r.width() / 2.0) / (double)r.width() / 2.0) / 0.75; // Store real frame percentage for resize events root->setProperty("realpercent", percent); } if (m_splitEffect) { m_splitEffect->set("0", percent); } m_glMonitor->refresh(); } void Monitor::slotSwitchRec(bool enable) { if (!m_recManager) { return; } if (enable) { m_toolbar->setVisible(false); m_recManager->toolbar()->setVisible(true); } else if (m_recManager->toolbar()->isVisible()) { m_recManager->stop(); m_toolbar->setVisible(true); emit refreshCurrentClip(); } } void Monitor::doKeyPressEvent(QKeyEvent *ev) { keyPressEvent(ev); } void Monitor::slotEditInlineMarker() { QQuickItem *root = m_glMonitor->rootObject(); if (root) { std::shared_ptr model; if (m_controller) { // We are editing a clip marker model = m_controller->getMarkerModel(); } else { model = pCore->currentDoc()->getGuideModel(); } QString newComment = root->property("markerText").toString(); bool found = false; CommentedTime oldMarker = model->getMarker(m_timePos->gentime(), &found); if (!found || newComment == oldMarker.comment()) { // No change return; } oldMarker.setComment(newComment); model->addMarker(oldMarker.time(), oldMarker.comment(), oldMarker.markerType()); } } -void Monitor::prepareAudioThumb(int channels, QVariantList &audioCache) +void Monitor::prepareAudioThumb(int channels, const QList &audioCache) { m_glMonitor->setAudioThumb(channels, audioCache); } void Monitor::slotSwitchAudioMonitor() { if (!m_audioMeterWidget->isValid) { KdenliveSettings::setMonitoraudio(0x01); m_audioMeterWidget->setVisibility(false); return; } int currentOverlay = KdenliveSettings::monitoraudio(); currentOverlay ^= m_id; KdenliveSettings::setMonitoraudio(currentOverlay); if ((KdenliveSettings::monitoraudio() & m_id) != 0) { // We want to enable this audio monitor, so make monitor active slotActivateMonitor(); } displayAudioMonitor(isActive()); } void Monitor::displayAudioMonitor(bool isActive) { bool enable = isActive && ((KdenliveSettings::monitoraudio() & m_id) != 0); if (enable) { connect(m_monitorManager, &MonitorManager::frameDisplayed, m_audioMeterWidget, &ScopeWidget::onNewFrame, Qt::UniqueConnection); } else { disconnect(m_monitorManager, &MonitorManager::frameDisplayed, m_audioMeterWidget, &ScopeWidget::onNewFrame); } m_audioMeterWidget->setVisibility((KdenliveSettings::monitoraudio() & m_id) != 0); } void Monitor::updateQmlDisplay(int currentOverlay) { m_glMonitor->rootObject()->setVisible((currentOverlay & 0x01) != 0); m_glMonitor->rootObject()->setProperty("showMarkers", currentOverlay & 0x04); m_glMonitor->rootObject()->setProperty("showFps", currentOverlay & 0x20); m_glMonitor->rootObject()->setProperty("showTimecode", currentOverlay & 0x02); m_glMonitor->rootObject()->setProperty("showAudiothumb", currentOverlay & 0x10); } void Monitor::clearDisplay() { m_glMonitor->clear(); } void Monitor::panView(QPoint diff) { // Only pan if scrollbars are visible if (m_horizontalScroll->isVisible()) { m_horizontalScroll->setValue(m_horizontalScroll->value() + diff.x()); } if (m_verticalScroll->isVisible()) { m_verticalScroll->setValue(m_verticalScroll->value() + diff.y()); } } void Monitor::requestSeek(int pos) { m_glMonitor->seek(pos); } void Monitor::setProducer(std::shared_ptr producer, int pos) { m_glMonitor->setProducer(std::move(producer), isActive(), pos); } void Monitor::reconfigure() { m_glMonitor->reconfigure(); } void Monitor::slotSeekPosition(int pos) { m_timePos->setValue(pos); checkOverlay(); } void Monitor::slotStart() { slotActivateMonitor(); m_glMonitor->switchPlay(false); m_glMonitor->seek(0); } void Monitor::slotEnd() { slotActivateMonitor(); m_glMonitor->switchPlay(false); if (m_id == Kdenlive::ClipMonitor) { m_glMonitor->seek(m_glMonitor->duration()); } else { m_glMonitor->seek(pCore->projectDuration()); } } void Monitor::addSnapPoint(int pos) { m_snaps->addPoint(pos); } void Monitor::removeSnapPoint(int pos) { m_snaps->removePoint(pos); } void Monitor::slotZoomIn() { m_glMonitor->slotZoom(true); } void Monitor::slotZoomOut() { m_glMonitor->slotZoom(false); } void Monitor::setConsumerProperty(const QString &name, const QString &value) { m_glMonitor->setConsumerProperty(name, value); } diff --git a/src/monitor/monitor.h b/src/monitor/monitor.h index c57b0aab9..c1e7331be 100644 --- a/src/monitor/monitor.h +++ b/src/monitor/monitor.h @@ -1,367 +1,367 @@ /*************************************************************************** * 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 MONITOR_H #define MONITOR_H #include "abstractmonitor.h" #include "bin/model/markerlistmodel.hpp" #include "definitions.h" #include "gentime.h" #include "scopes/sharedframe.h" #include "timecodedisplay.h" #include #include #include #include #include #include class SnapModel; class ProjectClip; class MonitorManager; class QSlider; class KDualAction; class KSelectAction; class KMessageWidget; class QQuickItem; class QScrollBar; class RecManager; class QToolButton; class QmlManager; class GLWidget; class MonitorAudioLevel; namespace Mlt { class Profile; class Filter; } // namespace Mlt class QuickEventEater : public QObject { Q_OBJECT public: explicit QuickEventEater(QObject *parent = nullptr); protected: bool eventFilter(QObject *obj, QEvent *event) override; signals: void addEffect(const QStringList &); }; class QuickMonitorEventEater : public QObject { Q_OBJECT public: explicit QuickMonitorEventEater(QWidget *parent); protected: bool eventFilter(QObject *obj, QEvent *event) override; signals: void doKeyPressEvent(QKeyEvent *); }; class Monitor : public AbstractMonitor { Q_OBJECT public: friend class MonitorManager; Monitor(Kdenlive::MonitorId id, MonitorManager *manager, QWidget *parent = nullptr); ~Monitor() override; void resetProfile(); /** @brief Rebuild consumers after a property change */ void resetConsumer(bool fullReset); void setCustomProfile(const QString &profile, const Timecode &tc); void setupMenu(QMenu *goMenu, QMenu *overlayMenu, QAction *playZone, QAction *loopZone, QMenu *markerMenu = nullptr, QAction *loopClip = nullptr); const QString sceneList(const QString &root, const QString &fullPath = QString()); const QString activeClipId(); int position(); void updateTimecodeFormat(); void updateMarkers(); /** @brief Controller for the clip currently displayed (only valid for clip monitor). */ std::shared_ptr currentController() const; /** @brief Add timeline guides to the ruler and context menu */ void setGuides(const QMap &guides); void reloadProducer(const QString &id); /** @brief Reimplemented from QWidget, updates the palette colors. */ void setPalette(const QPalette &p); /** @brief Returns a hh:mm:ss timecode from a frame number. */ QString getTimecodeFromFrames(int pos); /** @brief Returns current project's fps. */ double fps() const; /** @brief Returns current project's timecode. */ Timecode timecode() const; /** @brief Get url for the clip's thumbnail */ QString getMarkerThumb(GenTime pos); /** @brief Get current project's folder */ const QString projectFolder() const; int getZoneStart(); int getZoneEnd(); void setUpEffectGeometry(const QRect &r, const QVariantList &list = QVariantList(), const QVariantList &types = QVariantList()); /** @brief Set a property on the effect scene */ void setEffectSceneProperty(const QString &name, const QVariant &value); /** @brief Returns effective display size */ QSize profileSize() const; QRect effectRect() const; QVariantList effectPolygon() const; QVariantList effectRoto() const; void setEffectKeyframe(bool enable); void sendFrameForAnalysis(bool analyse); void updateAudioForAnalysis(); void switchMonitorInfo(int code); void switchDropFrames(bool drop); void updateMonitorGamma(); void mute(bool, bool updateIconOnly = false) override; void reparent(); /** @brief Returns the action displaying record toolbar */ QAction *recAction(); void refreshIcons(); /** @brief Send audio thumb data to qml for on monitor display */ - void prepareAudioThumb(int channels, QVariantList &audioCache); + void prepareAudioThumb(int channels, const QList &audioCache = QList ()); void connectAudioSpectrum(bool activate); /** @brief Set a property on the Qml scene **/ void setQmlProperty(const QString &name, const QVariant &value); void displayAudioMonitor(bool isActive); /** @brief Prepare split effect from timeline clip producer **/ void activateSplit(); /** @brief Clear monitor display **/ void clearDisplay(); void setProducer(std::shared_ptr producer, int pos = -1); void reconfigure(); /** @brief Saves current monitor frame to an image file, and add it to project if addToProject is set to true **/ void slotExtractCurrentFrame(QString frameName = QString(), bool addToProject = false); /** @brief Zoom in active monitor */ void slotZoomIn(); /** @brief Zoom out active monitor */ void slotZoomOut(); /** @brief Set a property on the MLT consumer */ void setConsumerProperty(const QString &name, const QString &value); protected: void mousePressEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; void mouseDoubleClickEvent(QMouseEvent *event) override; void resizeEvent(QResizeEvent *event) override; void keyPressEvent(QKeyEvent *event) override; /** @brief Move to another position on mouse wheel event. * * Moves towards the end of the clip/timeline on mouse wheel down/back, the * opposite on mouse wheel up/forward. * Ctrl + wheel moves by a second, without Ctrl it moves by a single frame. */ void wheelEvent(QWheelEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override; void enterEvent(QEvent *event) override; void leaveEvent(QEvent *event) override; virtual QStringList mimeTypes() const; private: std::shared_ptr m_controller; /** @brief The QQuickView that handles our monitor display (video and qml overlay) **/ GLWidget *m_glMonitor; /** @brief Container for our QQuickView monitor display (QQuickView needs to be embedded) **/ QWidget *m_glWidget; /** @brief Scrollbar for our monitor view, used when zooming the monitor **/ QScrollBar *m_verticalScroll; /** @brief Scrollbar for our monitor view, used when zooming the monitor **/ QScrollBar *m_horizontalScroll; /** @brief Widget holding the window for the QQuickView **/ QWidget *m_videoWidget; /** @brief Manager for qml overlay for the QQuickView **/ QmlManager *m_qmlManager; std::shared_ptr m_snaps; Mlt::Filter *m_splitEffect; std::shared_ptr m_splitProducer; int m_length; bool m_dragStarted; RecManager *m_recManager; /** @brief The widget showing current time position **/ TimecodeDisplay *m_timePos; KDualAction *m_playAction; KSelectAction *m_forceSize; /** Has to be available so we can enable and disable it. */ QAction *m_loopClipAction; QAction *m_sceneVisibilityAction; QAction *m_zoomVisibilityAction; QAction *m_multitrackView; QMenu *m_contextMenu; QMenu *m_configMenu; QMenu *m_playMenu; QMenu *m_markerMenu; QPoint m_DragStartPosition; /** true if selected clip is transition, false = selected clip is clip. * Necessary because sometimes we get two signals, e.g. we get a clip and we get selected transition = nullptr. */ bool m_loopClipTransition; GenTime getSnapForPos(bool previous); QToolBar *m_toolbar; QSlider *m_audioSlider; QAction *m_editMarker; KMessageWidget *m_infoMessage; int m_forceSizeFactor; MonitorSceneType m_lastMonitorSceneType; MonitorAudioLevel *m_audioMeterWidget; QElapsedTimer m_droppedTimer; double m_displayedFps; void adjustScrollBars(float horizontal, float vertical); void loadQmlScene(MonitorSceneType type); void updateQmlDisplay(int currentOverlay); /** @brief Check and display dropped frames */ void checkDrops(int dropped); /** @brief Create temporary Mlt::Tractor holding a clip and it's effectless clone */ void buildSplitEffect(Mlt::Producer *original); private slots: Q_DECL_DEPRECATED void seekCursor(int pos); void slotSetThumbFrame(); void slotSaveZone(); void slotSeek(); void updateClipZone(); void slotGoToMarker(QAction *action); void slotSetVolume(int volume); void slotEditMarker(); void slotExtractCurrentZone(); void onFrameDisplayed(const SharedFrame &frame); void slotStartDrag(); void setZoom(); void slotEnableEffectScene(bool enable); void slotAdjustEffectCompare(); void slotShowMenu(const QPoint pos); void slotForceSize(QAction *a); void slotSeekToKeyFrame(); /** @brief Display a non blocking error message to user **/ void warningMessage(const QString &text, int timeout = 5000, const QList &actions = QList()); void slotLockMonitor(bool lock); void slotAddEffect(const QStringList &effect); void slotSwitchPlay(); void slotEditInlineMarker(); /** @brief Pass keypress event to mainwindow */ void doKeyPressEvent(QKeyEvent *); /** @brief There was an error initializing Movit */ void gpuError(); void setOffsetX(int x); void setOffsetY(int y); /** @brief Pan monitor view */ void panView(QPoint diff); /** @brief Project monitor zone changed, inform timeline */ void updateTimelineClipZone(); void slotSeekPosition(int); void addSnapPoint(int pos); void removeSnapPoint(int pos); public slots: void slotOpenDvdFile(const QString &); // void slotSetClipProducer(DocClipBase *clip, QPoint zone = QPoint(), bool forceUpdate = false, int position = -1); void updateClipProducer(const std::shared_ptr &prod); void updateClipProducer(const QString &playlist); void slotOpenClip(const std::shared_ptr &controller, int in = -1, int out = -1); void slotRefreshMonitor(bool visible); void slotSeek(int pos); void stop() override; void start() override; void switchPlay(bool play); void slotPlay() override; void pause(); void slotPlayZone(); void slotLoopZone(); /** @brief Loops the selected item (clip or transition). */ void slotLoopClip(); void slotForward(double speed = 0); void slotRewind(double speed = 0); void slotRewindOneFrame(int diff = 1); void slotForwardOneFrame(int diff = 1); void slotStart(); void slotEnd(); void slotSetZoneStart(); void slotSetZoneEnd(bool discardLastFrame = false); void slotZoneStart(); void slotZoneEnd(); void slotLoadClipZone(const QPoint &zone); void slotSeekToNextSnap(); void slotSeekToPreviousSnap(); void adjustRulerSize(int length, const std::shared_ptr &markerModel = nullptr); void setTimePos(const QString &pos); QPoint getZoneInfo() const; /** @brief Display the on monitor effect scene (to adjust geometry over monitor). */ void slotShowEffectScene(MonitorSceneType sceneType, bool temporary = false); bool effectSceneDisplayed(MonitorSceneType effectType); /** @brief split screen to compare clip with and without effect */ void slotSwitchCompare(bool enable); void slotMouseSeek(int eventDelta, uint modifiers) override; void slotSwitchFullScreen(bool minimizeOnly = false) override; /** @brief Display or hide the record toolbar */ void slotSwitchRec(bool enable); /** @brief Request QImage of current frame */ void slotGetCurrentImage(bool request); /** @brief Enable/disable display of monitor's audio levels widget */ void slotSwitchAudioMonitor(); /** @brief Request seeking */ void requestSeek(int pos); /** @brief Check current position to show relevant infos in qml view (markers, zone in/out, etc). */ void checkOverlay(int pos = -1); void refreshMonitorIfActive(bool directUpdate = false) override; signals: void seekPosition(int pos); void updateScene(); /** @brief Request a timeline seeking if diff is true, position is a relative offset, otherwise an absolute position */ void seekTimeline(int position); void durationChanged(int); void refreshClipThumbnail(const QString &); void zoneUpdated(const QPoint &); void timelineZoneChanged(); /** @brief Editing transitions / effects over the monitor requires the renderer to send frames as QImage. * This causes a major slowdown, so we only enable it if required */ void requestFrameForAnalysis(bool); /** @brief Request a zone extraction (ffmpeg transcoding). */ void extractZone(const QString &id); void effectChanged(const QRect &); void effectPointsChanged(const QVariantList &); void addRemoveKeyframe(); void seekToNextKeyframe(); void seekToPreviousKeyframe(); void seekToKeyframe(int); void addClipToProject(const QUrl &); void showConfigDialog(int, int); /** @brief Request display of current bin clip. */ void refreshCurrentClip(); void addEffect(const QStringList &); void addMasterEffect(QString, const QStringList &); void passKeyPress(QKeyEvent *); /** @brief Enable / disable project monitor multitrack view (split view with one track in each quarter). */ void multitrackView(bool); void timeCodeUpdated(const QString &); void addMarker(); void deleteMarker(bool deleteGuide = true); void seekToPreviousSnap(); void seekToNextSnap(); void createSplitOverlay(Mlt::Filter *); void removeSplitOverlay(); void acceptRipple(bool); void switchTrimMode(int); }; #endif diff --git a/src/timeline2/model/timelineitemmodel.cpp b/src/timeline2/model/timelineitemmodel.cpp index 255f532ca..c5db02c90 100644 --- a/src/timeline2/model/timelineitemmodel.cpp +++ b/src/timeline2/model/timelineitemmodel.cpp @@ -1,599 +1,600 @@ /*************************************************************************** * Copyright (C) 2017 by Nicolas Carion * * 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 "timelineitemmodel.hpp" #include "assets/keyframes/model/keyframemodel.hpp" #include "bin/model/markerlistmodel.hpp" #include "clipmodel.hpp" #include "compositionmodel.hpp" #include "core.h" #include "doc/docundostack.hpp" #include "groupsmodel.hpp" #include "kdenlivesettings.h" #include "macros.hpp" #include "trackmodel.hpp" #include "transitions/transitionsrepository.hpp" #include #include #include #include #include #include TimelineItemModel::TimelineItemModel(Mlt::Profile *profile, std::weak_ptr undo_stack) : TimelineModel(profile, std::move(undo_stack)) { } void TimelineItemModel::finishConstruct(const std::shared_ptr &ptr, const std::shared_ptr &guideModel) { ptr->weak_this_ = ptr; ptr->m_groups = std::make_unique(ptr); guideModel->registerSnapModel(ptr->m_snaps); } std::shared_ptr TimelineItemModel::construct(Mlt::Profile *profile, std::shared_ptr guideModel, std::weak_ptr undo_stack) { std::shared_ptr ptr(new TimelineItemModel(profile, std::move(undo_stack))); finishConstruct(ptr, std::move(guideModel)); return ptr; } TimelineItemModel::~TimelineItemModel() = default; QModelIndex TimelineItemModel::index(int row, int column, const QModelIndex &parent) const { READ_LOCK(); QModelIndex result; if (parent.isValid()) { auto trackId = int(parent.internalId()); Q_ASSERT(isTrack(trackId)); int clipId = getTrackById_const(trackId)->getClipByRow(row); if (clipId != -1) { result = createIndex(row, 0, quintptr(clipId)); } else if (row < getTrackClipsCount(trackId) + getTrackCompositionsCount(trackId)) { int compoId = getTrackById_const(trackId)->getCompositionByRow(row); if (compoId != -1) { result = createIndex(row, 0, quintptr(compoId)); } } else { // Invalid index requested Q_ASSERT(false); } } else if (row < getTracksCount() && row >= 0) { // Get sort order // row = getTracksCount() - 1 - row; auto it = m_allTracks.cbegin(); std::advance(it, row); int trackId = (*it)->getId(); result = createIndex(row, column, quintptr(trackId)); } return result; } /*QModelIndex TimelineItemModel::makeIndex(int trackIndex, int clipIndex) const { return index(clipIndex, 0, index(trackIndex)); }*/ QModelIndex TimelineItemModel::makeClipIndexFromID(int clipId) const { Q_ASSERT(m_allClips.count(clipId) > 0); int trackId = m_allClips.at(clipId)->getCurrentTrackId(); if (trackId == -1) { // Clip is not inserted in a track qDebug() << "/// WARNING; INVALID CLIP INDEX REQUESTED\n________________"; return {}; } int row = getTrackById_const(trackId)->getRowfromClip(clipId); return index(row, 0, makeTrackIndexFromID(trackId)); } QModelIndex TimelineItemModel::makeCompositionIndexFromID(int compoId) const { Q_ASSERT(m_allCompositions.count(compoId) > 0); int trackId = m_allCompositions.at(compoId)->getCurrentTrackId(); return index(getTrackById_const(trackId)->getRowfromComposition(compoId), 0, makeTrackIndexFromID(trackId)); } QModelIndex TimelineItemModel::makeTrackIndexFromID(int trackId) const { // we retrieve iterator Q_ASSERT(m_iteratorTable.count(trackId) > 0); auto it = m_iteratorTable.at(trackId); int ind = (int)std::distance(m_allTracks.begin(), it); // Get sort order // ind = getTracksCount() - 1 - ind; return index(ind); } QModelIndex TimelineItemModel::parent(const QModelIndex &index) const { READ_LOCK(); // qDebug() << "TimelineItemModel::parent"<< index; if (index == QModelIndex()) { return index; } const int id = static_cast(index.internalId()); if (!index.isValid() || isTrack(id)) { return QModelIndex(); } if (isClip(id)) { const int trackId = getClipTrackId(id); return makeTrackIndexFromID(trackId); } if (isComposition(id)) { const int trackId = getCompositionTrackId(id); return makeTrackIndexFromID(trackId); } return {}; } int TimelineItemModel::rowCount(const QModelIndex &parent) const { READ_LOCK(); if (parent.isValid()) { const int id = (int)parent.internalId(); if (!isTrack(id)) { // clips don't have children // if it is not a track, it is something invalid return 0; } return getTrackClipsCount(id) + getTrackCompositionsCount(id); } return getTracksCount(); } int TimelineItemModel::columnCount(const QModelIndex &parent) const { Q_UNUSED(parent); return 1; } QHash TimelineItemModel::roleNames() const { QHash roles; roles[NameRole] = "name"; roles[ResourceRole] = "resource"; roles[ServiceRole] = "mlt_service"; roles[BinIdRole] = "binId"; roles[TrackIdRole] = "trackId"; roles[FakeTrackIdRole] = "fakeTrackId"; roles[FakePositionRole] = "fakePosition"; roles[StartRole] = "start"; roles[DurationRole] = "duration"; roles[MarkersRole] = "markers"; roles[KeyframesRole] = "keyframeModel"; roles[ShowKeyframesRole] = "showKeyframes"; roles[StatusRole] = "clipStatus"; roles[TypeRole] = "clipType"; roles[InPointRole] = "in"; roles[OutPointRole] = "out"; roles[FramerateRole] = "fps"; roles[GroupedRole] = "grouped"; roles[IsDisabledRole] = "disabled"; roles[IsAudioRole] = "audio"; roles[AudioLevelsRole] = "audioLevels"; roles[AudioChannelsRole] = "audioChannels"; roles[IsCompositeRole] = "composite"; roles[IsLockedRole] = "locked"; roles[FadeInRole] = "fadeIn"; roles[FadeOutRole] = "fadeOut"; roles[FileHashRole] = "hash"; roles[SpeedRole] = "speed"; roles[HeightRole] = "trackHeight"; roles[TrackTagRole] = "trackTag"; roles[ItemIdRole] = "item"; roles[ItemATrack] = "a_track"; roles[HasAudio] = "hasAudio"; roles[CanBeAudioRole] = "canBeAudio"; roles[CanBeVideoRole] = "canBeVideo"; roles[ReloadThumbRole] = "reloadThumb"; roles[ThumbsFormatRole] = "thumbsFormat"; roles[AudioRecordRole] = "audioRecord"; roles[TrackActiveRole] = "trackActive"; roles[EffectNamesRole] = "effectNames"; roles[EffectsEnabledRole] = "isStackEnabled"; roles[GrabbedRole] = "isGrabbed"; return roles; } QVariant TimelineItemModel::data(const QModelIndex &index, int role) const { READ_LOCK(); if (!m_tractor || !index.isValid()) { // qDebug() << "DATA abort. Index validity="< clip = m_allClips.at(id); // Get data for a clip switch (role) { // TODO case NameRole: case Qt::DisplayRole: { QString result = clip->getProperty("kdenlive:clipname"); if (result.isEmpty()) { result = clip->getProperty("kdenlive:originalurl"); if (result.isEmpty()) { result = clip->getProperty("resource"); } if (!result.isEmpty()) { result = QFileInfo(result).fileName(); } else { result = clip->getProperty("mlt_service"); } } return result; } case ResourceRole: { QString result = clip->getProperty("resource"); if (result == QLatin1String("")) { result = clip->getProperty("mlt_service"); } return result; } case FakeTrackIdRole: return clip->getFakeTrackId(); case FakePositionRole: return clip->getFakePosition(); case BinIdRole: return clip->binId(); case TrackIdRole: return clip->getCurrentTrackId(); case ServiceRole: return clip->getProperty("mlt_service"); break; case AudioLevelsRole: - return clip->getAudioWaveform(); + // Dumb property to trigger audio thumbs reload + return true; case AudioChannelsRole: return clip->audioChannels(); case HasAudio: return clip->audioEnabled(); case IsAudioRole: return clip->isAudioOnly(); case CanBeAudioRole: return clip->canBeAudio(); case CanBeVideoRole: return clip->canBeVideo(); case MarkersRole: { return QVariant::fromValue(clip->getMarkerModel().get()); } case KeyframesRole: { return QVariant::fromValue(clip->getKeyframeModel()); } case StatusRole: return QVariant::fromValue(clip->clipState()); case TypeRole: return QVariant::fromValue(clip->clipType()); case StartRole: return clip->getPosition(); case DurationRole: return clip->getPlaytime(); case GroupedRole: return m_groups->isInGroup(id); case EffectNamesRole: return clip->effectNames(); case InPointRole: return clip->getIn(); case OutPointRole: return clip->getOut(); case ShowKeyframesRole: return clip->showKeyframes(); case FadeInRole: return clip->fadeIn(); case FadeOutRole: return clip->fadeOut(); case ReloadThumbRole: return clip->forceThumbReload; case SpeedRole: return clip->getSpeed(); case GrabbedRole: return clip->isGrabbed(); default: break; } } else if (isTrack(id)) { // qDebug() << "DATA REQUESTED FOR TRACK "<< id; switch (role) { case NameRole: case Qt::DisplayRole: { return getTrackById_const(id)->getProperty("kdenlive:track_name").toString(); } case TypeRole: return QVariant::fromValue(ClipType::ProducerType::Track); case DurationRole: // qDebug() << "DATA yielding duration" << m_tractor->get_playtime(); return getTrackById_const(id)->trackDuration(); case IsDisabledRole: // qDebug() << "DATA yielding mute" << 0; return getTrackById_const(id)->isAudioTrack() ? getTrackById_const(id)->isMute() : getTrackById_const(id)->isHidden(); case IsAudioRole: return getTrackById_const(id)->isAudioTrack(); case TrackTagRole: return getTrackTagById(id); case IsLockedRole: return getTrackById_const(id)->getProperty("kdenlive:locked_track").toInt() == 1; case HeightRole: { int collapsed = getTrackById_const(id)->getProperty("kdenlive:collapsed").toInt(); if (collapsed > 0) { return collapsed; } int height = getTrackById_const(id)->getProperty("kdenlive:trackheight").toInt(); // qDebug() << "DATA yielding height" << height; return (height > 0 ? height : 60); } case ThumbsFormatRole: return getTrackById_const(id)->getProperty("kdenlive:thumbs_format").toInt(); case IsCompositeRole: { case AudioRecordRole: return getTrackById_const(id)->getProperty("kdenlive:audio_rec").toInt(); } case TrackActiveRole: { return getTrackById_const(id)->isTimelineActive(); } case EffectNamesRole: { return getTrackById_const(id)->effectNames(); } case EffectsEnabledRole: { return getTrackById_const(id)->stackEnabled(); } default: break; } } else if (isComposition(id)) { std::shared_ptr compo = m_allCompositions.at(id); switch (role) { case NameRole: case Qt::DisplayRole: case ResourceRole: case ServiceRole: return compo->displayName(); break; case TypeRole: return QVariant::fromValue(ClipType::ProducerType::Composition); case StartRole: return compo->getPosition(); case TrackIdRole: return compo->getCurrentTrackId(); case DurationRole: return compo->getPlaytime(); case GroupedRole: return m_groups->isInGroup(id); case InPointRole: return 0; case OutPointRole: return 100; case BinIdRole: return 5; case KeyframesRole: { return QVariant::fromValue(compo->getEffectKeyframeModel()); } case ShowKeyframesRole: return compo->showKeyframes(); case ItemATrack: return compo->getForcedTrack(); case MarkersRole: { QVariantList markersList; return markersList; } case GrabbedRole: return compo->isGrabbed(); default: break; } } else { qDebug() << "UNKNOWN DATA requested " << index << roleNames()[role]; } return QVariant(); } void TimelineItemModel::setTrackProperty(int trackId, const QString &name, const QString &value) { std::shared_ptr track = getTrackById(trackId); track->setProperty(name, value); QVector roles; if (name == QLatin1String("kdenlive:track_name")) { roles.push_back(NameRole); } else if (name == QLatin1String("kdenlive:locked_track")) { roles.push_back(IsLockedRole); } else if (name == QLatin1String("hide")) { roles.push_back(IsDisabledRole); if (!track->isAudioTrack()) { pCore->requestMonitorRefresh(); } } else if (name == QLatin1String("kdenlive:timeline_active")) { roles.push_back(TrackActiveRole); } else if (name == QLatin1String("kdenlive:thumbs_format")) { roles.push_back(ThumbsFormatRole); } else if (name == QLatin1String("kdenlive:audio_rec")) { roles.push_back(AudioRecordRole); } if (!roles.isEmpty()) { QModelIndex ix = makeTrackIndexFromID(trackId); emit dataChanged(ix, ix, roles); } } void TimelineItemModel::setTrackStackEnabled(int tid, bool enable) { std::shared_ptr track = getTrackById(tid); track->setEffectStackEnabled(enable); QModelIndex ix = makeTrackIndexFromID(tid); emit dataChanged(ix, ix, {TimelineModel::EffectsEnabledRole}); } void TimelineItemModel::importTrackEffects(int tid, std::weak_ptr service) { std::shared_ptr track = getTrackById(tid); track->importEffects(std::move(service)); } QVariant TimelineItemModel::getTrackProperty(int tid, const QString &name) const { return getTrackById_const(tid)->getProperty(name); } int TimelineItemModel::getFirstVideoTrackIndex() const { int trackId = -1; auto it = m_allTracks.cbegin(); while (it != m_allTracks.cend()) { trackId = (*it)->getId(); if (!(*it)->isAudioTrack()) { break; } ++it; } return trackId; } const QString TimelineItemModel::getTrackFullName(int tid) const { QString tag = getTrackTagById(tid); QString trackName = getTrackById_const(tid)->getProperty(QStringLiteral("kdenlive:track_name")).toString(); return trackName.isEmpty() ? tag : tag + QStringLiteral(" - ") + trackName; } const QString TimelineItemModel::groupsData() { return m_groups->toJson(); } bool TimelineItemModel::loadGroups(const QString &groupsData) { return m_groups->fromJson(groupsData); } void TimelineItemModel::notifyChange(const QModelIndex &topleft, const QModelIndex &bottomright, bool start, bool duration, bool updateThumb) { QVector roles; if (start) { roles.push_back(TimelineModel::StartRole); if (updateThumb) { roles.push_back(TimelineModel::InPointRole); } } if (duration) { roles.push_back(TimelineModel::DurationRole); if (updateThumb) { roles.push_back(TimelineModel::OutPointRole); } } emit dataChanged(topleft, bottomright, roles); } void TimelineItemModel::notifyChange(const QModelIndex &topleft, const QModelIndex &bottomright, const QVector &roles) { emit dataChanged(topleft, bottomright, roles); } void TimelineItemModel::buildTrackCompositing(bool rebuild) { auto it = m_allTracks.cbegin(); QScopedPointer field(m_tractor->field()); field->lock(); // Make sure all previous track compositing is removed if (rebuild) { QScopedPointer service(new Mlt::Service(field->get_service())); while ((service != nullptr) && service->is_valid()) { if (service->type() == transition_type) { Mlt::Transition t((mlt_transition)service->get_service()); QString serviceName = t.get("mlt_service"); if (t.get_int("internal_added") == 237) { // remove all compositing transitions field->disconnect_service(t); } } service.reset(service->producer()); } } QString composite = TransitionsRepository::get()->getCompositingTransition(); while (it != m_allTracks.cend()) { int trackId = getTrackMltIndex((*it)->getId()); if (!composite.isEmpty() && !(*it)->isAudioTrack()) { // video track, add composition std::unique_ptr transition = TransitionsRepository::get()->getTransition(composite); transition->set("internal_added", 237); transition->set("always_active", 1); field->plant_transition(*transition, 0, trackId); transition->set_tracks(0, trackId); } else if ((*it)->isAudioTrack()) { // audio mix std::unique_ptr transition = TransitionsRepository::get()->getTransition(QStringLiteral("mix")); transition->set("internal_added", 237); transition->set("always_active", 1); transition->set("sum", 1); field->plant_transition(*transition, 0, trackId); transition->set_tracks(0, trackId); } ++it; } field->unlock(); if (composite.isEmpty()) { pCore->displayMessage(i18n("Could not setup track compositing, check your install"), MessageType::ErrorMessage); } } void TimelineItemModel::notifyChange(const QModelIndex &topleft, const QModelIndex &bottomright, int role) { emit dataChanged(topleft, bottomright, {role}); } void TimelineItemModel::_beginRemoveRows(const QModelIndex &i, int j, int k) { // qDebug()<<"FORWARDING beginRemoveRows"< clipRoot.modelStart + clipRoot.clipDuration) || (clipRoot.modelStart > waveform.scrollMax) || clipRoot.audioLevels == '') { return; } //var t0 = new Date(); waveformRepeater.model = Math.ceil(waveform.innerWidth / waveform.maxWidth) var firstWaveRepeater = Math.max(0, Math.floor((waveform.scrollMin - clipRoot.modelStart) / (waveform.maxWidth / timeline.scaleFactor))) var lastWaveRepeater = Math.min(waveformRepeater.count - 1, firstWaveRepeater + Math.ceil((waveform.scrollMax - waveform.scrollMin) / (waveform.maxWidth / timeline.scaleFactor))) for (var i = firstWaveRepeater; i <= lastWaveRepeater; i++) { waveformRepeater.itemAt(i).update() } } Repeater { id: waveformRepeater TimelineWaveform { width: Math.min(waveform.innerWidth, waveform.maxWidth) height: waveform.height channels: clipRoot.audioChannels + binId: clipRoot.binId isFirstChunk: index == 0 showItem: waveform.visible && (index * width) < waveform.scrollEnd && (index * width + width) > waveform.scrollStart format: timeline.audioThumbFormat inPoint: Math.round((clipRoot.inPoint + (index * waveform.maxWidth / clipRoot.timeScale)) * Math.abs(clipRoot.speed)) * channels outPoint: inPoint + Math.round(width / clipRoot.timeScale * Math.abs(clipRoot.speed)) * channels - levels: clipRoot.audioLevels fillColor: activePalette.text } } } diff --git a/src/timeline2/view/qml/timelineitems.cpp b/src/timeline2/view/qml/timelineitems.cpp index b62a3535d..b0436331a 100644 --- a/src/timeline2/view/qml/timelineitems.cpp +++ b/src/timeline2/view/qml/timelineitems.cpp @@ -1,200 +1,213 @@ /* Based on Shotcut, Copyright (c) 2015-2016 Meltytech, LLC Copyright (C) 2019 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 "kdenlivesettings.h" +#include "core.h" +#include "bin/projectitemmodel.h" #include #include #include #include #include const QStringList chanelNames{"L", "R", "C", "LFE", "BL", "BR"}; class TimelineTriangle : public QQuickPaintedItem { Q_OBJECT Q_PROPERTY(QColor fillColor MEMBER m_color) public: TimelineTriangle() { setAntialiasing(true); } void paint(QPainter *painter) override { QPainterPath path; path.moveTo(0, 0); path.lineTo(width(), 0); path.lineTo(0, height()); painter->fillPath(path, m_color); painter->setPen(Qt::white); painter->drawLine(width(), 0, 0, height()); } private: QColor m_color; }; class TimelinePlayhead : public QQuickPaintedItem { Q_OBJECT Q_PROPERTY(QColor fillColor MEMBER m_color NOTIFY colorChanged) public: TimelinePlayhead() { connect(this, SIGNAL(colorChanged(QColor)), this, SLOT(update())); } void paint(QPainter *painter) override { QPainterPath path; path.moveTo(width(), 0); path.lineTo(width() / 2.0, height()); path.lineTo(0, 0); painter->fillPath(path, m_color); } signals: void colorChanged(const QColor &); private: QColor m_color; }; class TimelineWaveform : public QQuickPaintedItem { Q_OBJECT - Q_PROPERTY(QList levels MEMBER m_audioLevels NOTIFY propertyChanged) Q_PROPERTY(QColor fillColor MEMBER m_color NOTIFY propertyChanged) Q_PROPERTY(int inPoint MEMBER m_inPoint NOTIFY inPointChanged) Q_PROPERTY(int channels MEMBER m_channels NOTIFY audioChannelsChanged) + Q_PROPERTY(QString binId MEMBER m_binId NOTIFY levelsChanged) Q_PROPERTY(int outPoint MEMBER m_outPoint NOTIFY outPointChanged) Q_PROPERTY(bool format MEMBER m_format NOTIFY propertyChanged) Q_PROPERTY(bool showItem MEMBER m_showItem) Q_PROPERTY(bool isFirstChunk MEMBER m_firstChunk) public: TimelineWaveform() { setAntialiasing(false); // setClip(true); setEnabled(false); setRenderTarget(QQuickPaintedItem::FramebufferObject); setMipmap(true); setTextureSize(QSize(width(), height())); - connect(this, SIGNAL(propertyChanged()), this, SLOT(update())); + connect(this, &TimelineWaveform::levelsChanged, [&]() { + qDebug()<<"* * ** WARNING AUDIO LEVELS CHANGED for: "<projectItemModel()->getAudioLevelsByBinID(m_binId); + update(); + }); + connect(this, &TimelineWaveform::propertyChanged, [&]() { + update(); + }); + //m_audioLevels = pCore->projectItemModel()->getAudioLevelsByBinID(m_binId); } void paint(QPainter *painter) override { if (!m_showItem || m_audioLevels.isEmpty()) return; const qreal indicesPrPixel = qreal(m_outPoint - m_inPoint) / width(); QPen pen = painter->pen(); pen.setColor(m_color); pen.setWidthF(0); painter->setBrush(m_color); if (!KdenliveSettings::displayallchannels()) { // Draw merged channels QPainterPath path; path.moveTo(-1, height()); double i = 0; double increment = qMax(1., 1 / indicesPrPixel); int lastIdx = -1; for (; i <= width(); i += increment) { int idx = m_inPoint + int(i * indicesPrPixel); if (lastIdx == idx) { continue; } lastIdx = idx; if (idx + m_channels >= m_audioLevels.length()) break; - double level = m_audioLevels.at(idx).toDouble() / 256; + double level = m_audioLevels.at(idx) / 256; for (int j = 1; j < m_channels; j++) { - level = qMax(level, m_audioLevels.at(idx + j).toDouble() / 256); + level = qMax(level, m_audioLevels.at(idx + j) / 256); } path.lineTo(i, height() - level * height()); } path.lineTo(i, height()); painter->drawPath(path); } else { double channelHeight = height() / (2 * m_channels); QFont font = painter->font(); font.setPixelSize(channelHeight - 1); painter->setFont(font); // Draw separate channels double i = 0; double increment = qMax(1., 1 / indicesPrPixel); QRectF bgRect(0, 0, width(), 2 * channelHeight); QVector channelPaths(m_channels); for (int channel = 0; channel < m_channels; channel++) { double y = height() - (2 * channel * channelHeight) - channelHeight; channelPaths[channel].moveTo(-1, y); painter->setOpacity(0.2); if (channel % 2 == 0) { // Add dark background on odd channels bgRect.moveTo(0, y - channelHeight); painter->fillRect(bgRect, Qt::black); } // Draw channel median line painter->setPen(pen); painter->drawLine(QLineF(0., y, width(), y)); painter->setOpacity(1); int lastIdx = -1; for (i = 0; i <= width(); i += increment) { int idx = m_inPoint + ceil(i * indicesPrPixel); if (lastIdx == idx) { continue; } lastIdx = idx; if (idx + channel >= m_audioLevels.length()) break; - qreal level = m_audioLevels.at(idx + channel).toReal() * channelHeight / 256; + qreal level = m_audioLevels.at(idx + channel) * channelHeight / 256; channelPaths[channel].lineTo(i, y - level); } if (m_firstChunk && m_channels > 1 && m_channels < 7) { painter->drawText(2, y + channelHeight, chanelNames[channel]); } channelPaths[channel].lineTo(i, y); painter->setPen(Qt::NoPen); painter->drawPath(channelPaths.value(channel)); QTransform tr(1, 0, 0, -1, 0, 2 * y); painter->drawPath(tr.map(channelPaths.value(channel))); } } } signals: + void levelsChanged(); void propertyChanged(); void inPointChanged(); void outPointChanged(); void audioChannelsChanged(); private: - QList m_audioLevels; + bool m_levels; + QList m_audioLevels; int m_inPoint; int m_outPoint; + QString m_binId; QColor m_color; bool m_format; bool m_showItem; int m_channels; bool m_firstChunk; }; void registerTimelineItems() { qmlRegisterType("Kdenlive.Controls", 1, 0, "TimelineTriangle"); qmlRegisterType("Kdenlive.Controls", 1, 0, "TimelinePlayhead"); qmlRegisterType("Kdenlive.Controls", 1, 0, "TimelineWaveform"); } #include "timelineitems.moc"