diff --git a/src/bin/bin.cpp b/src/bin/bin.cpp index faa1083cc..a230664c6 100644 --- a/src/bin/bin.cpp +++ b/src/bin/bin.cpp @@ -1,3171 +1,3172 @@ /* 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); 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))); wheelAccumulatedDelta = 0; } 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) { wheelAccumulatedDelta += e->delta(); if (abs(wheelAccumulatedDelta) >= QWheelEvent::DefaultDeltasPerStep) { slotZoomView(wheelAccumulatedDelta > 0); } // emit zoomView(e->delta() > 0); return true; } } return QWidget::eventFilter(obj, event); } void Bin::refreshIcons() { QList allMenus = this->findChildren(); for (int i = 0; i < allMenus.count(); i++) { QMenu *m = allMenus.at(i); QIcon ic = m->icon(); if (ic.isNull() || ic.name().isEmpty()) { continue; } QIcon newIcon = QIcon::fromTheme(ic.name()); m->setIcon(newIcon); } QList allButtons = this->findChildren(); for (int i = 0; i < allButtons.count(); i++) { QToolButton *m = allButtons.at(i); QIcon ic = m->icon(); if (ic.isNull() || ic.name().isEmpty()) { continue; } QIcon newIcon = QIcon::fromTheme(ic.name()); m->setIcon(newIcon); } } void Bin::slotSaveHeaders() { if ((m_itemView != nullptr) && m_listType == BinTreeView) { // save current treeview state (column width) auto *view = static_cast(m_itemView); m_headerInfo = view->header()->saveState(); KdenliveSettings::setTreeviewheaders(m_headerInfo.toBase64()); } } void Bin::slotZoomView(bool zoomIn) { wheelAccumulatedDelta = 0; if (m_itemModel->rowCount() == 0) { // Don't zoom on empty bin return; } int progress = (zoomIn) ? 1 : -1; m_slider->setValue(m_slider->value() + progress); } Monitor *Bin::monitor() { return m_monitor; } void Bin::slotAddClip() { // Check if we are in a folder QString parentFolder = getCurrentFolder(); ClipCreationDialog::createClipsCommand(m_doc, parentFolder, m_itemModel); } std::shared_ptr Bin::getFirstSelectedClip() { const QModelIndexList indexes = m_proxyModel->selectionModel()->selectedIndexes(); if (indexes.isEmpty()) { return std::shared_ptr(); } for (const QModelIndex &ix : indexes) { std::shared_ptr item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(ix)); if (item->itemType() == AbstractProjectItem::ClipItem) { auto clip = std::static_pointer_cast(item); if (clip) { return clip; } } } return nullptr; } void Bin::slotDeleteClip() { const QModelIndexList indexes = m_proxyModel->selectionModel()->selectedIndexes(); std::vector> items; bool included = false; bool usedFolder = false; auto checkInclusion = [](bool accum, std::shared_ptr item) { return accum || std::static_pointer_cast(item)->isIncludedInTimeline(); }; for (const QModelIndex &ix : indexes) { if (!ix.isValid() || ix.column() != 0) { continue; } std::shared_ptr item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(ix)); if (!item) { qDebug() << "Suspicious: item not found when trying to delete"; continue; } included = included || item->accumulate(false, checkInclusion); // Check if we are deleting non-empty folders: usedFolder = usedFolder || item->childCount() > 0; items.push_back(item); } if (included && (KMessageBox::warningContinueCancel(this, i18n("This will delete all selected clips from the timeline")) != KMessageBox::Continue)) { return; } if (usedFolder && (KMessageBox::warningContinueCancel(this, i18n("This will delete all folder content")) != KMessageBox::Continue)) { return; } Fun undo = []() { return true; }; Fun redo = []() { return true; }; for (const auto &item : items) { m_itemModel->requestBinClipDeletion(item, undo, redo); } pCore->pushUndo(undo, redo, i18n("Delete bin Clips")); } void Bin::slotReloadClip() { const QModelIndexList indexes = m_proxyModel->selectionModel()->selectedIndexes(); for (const QModelIndex &ix : indexes) { if (!ix.isValid() || ix.column() != 0) { continue; } std::shared_ptr item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(ix)); std::shared_ptr currentItem = nullptr; if (item->itemType() == AbstractProjectItem::ClipItem) { currentItem = std::static_pointer_cast(item); } else if (item->itemType() == AbstractProjectItem::SubClipItem) { currentItem = std::static_pointer_cast(item)->getMasterClip(); } if (currentItem) { emit openClip(std::shared_ptr()); if (currentItem->clipType() == ClipType::Playlist) { // Check if a clip inside playlist is missing QString path = currentItem->url(); QFile f(path); QDomDocument doc; doc.setContent(&f, false); f.close(); DocumentChecker d(QUrl::fromLocalFile(path), doc); if (!d.hasErrorInClips() && doc.documentElement().hasAttribute(QStringLiteral("modified"))) { QString backupFile = path + QStringLiteral(".backup"); KIO::FileCopyJob *copyjob = KIO::file_copy(QUrl::fromLocalFile(path), QUrl::fromLocalFile(backupFile)); if (copyjob->exec()) { if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) { KMessageBox::sorry(this, i18n("Unable to write to file %1", path)); } else { QTextStream out(&f); out << doc.toString(); f.close(); KMessageBox::information( this, i18n("Your project file was modified by Kdenlive.\nTo make sure you do not lose data, a backup copy called %1 was created.", backupFile)); } } } } currentItem->reloadProducer(false, true); } } } void Bin::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); } void Bin::slotAddFolder() { 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, i18n("Folder"), parentFolder->clipId(), undo, redo); pCore->pushUndo(undo, redo, i18n("Create bin folder")); // Edit folder name 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)); } } 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_renameAction->setEnabled(true); 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); m_renameAction->setVisible(true); if (m_transcodeAction) { m_transcodeAction->setEnabled(enableClipActions); m_transcodeAction->menuAction()->setVisible(itemType != AbstractProjectItem::FolderItem && (type == ClipType::Playlist || type == ClipType::Text || clipService.contains(QStringLiteral("avformat")))); } m_clipsActionsMenu->menuAction()->setVisible( itemType != AbstractProjectItem::FolderItem && (clipService.contains(QStringLiteral("avformat")) || clipService.contains(QStringLiteral("xml")) || clipService.contains(QStringLiteral("consumer")))); m_extractAudioAction->menuAction()->setVisible(!isFolder && !audioCodec.isEmpty()); m_locateAction->setVisible(itemType != AbstractProjectItem::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); } } QAction *Bin::addAction(const QString &name, const QString &text, const QIcon &icon) { auto *action = new QAction(text, this); if (!icon.isNull()) { action->setIcon(icon); } pCore->window()->addAction(name, action); return action; } void Bin::setupAddClipAction(QMenu *addClipMenu, ClipType::ProducerType type, const QString &name, const QString &text, const QIcon &icon) { QAction *action = addAction(name, text, icon); action->setData(static_cast(type)); addClipMenu->addAction(action); connect(action, &QAction::triggered, this, &Bin::slotCreateProjectClip); } void Bin::showClipProperties(const std::shared_ptr &clip, bool forceRefresh) { if ((clip == nullptr) || !clip->isReady()) { for (QWidget *w : m_propertiesPanel->findChildren()) { delete w; } m_propertiesPanel->setProperty("clipId", QString()); m_propertiesPanel->setEnabled(false); 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() { auto *addClipMenu = new QMenu(this); QAction *addClip = addAction(QStringLiteral("add_clip"), i18n("Add Clip or Folder"), QIcon::fromTheme(QStringLiteral("kdenlive-add-clip"))); addClipMenu->addAction(addClip); connect(addClip, &QAction::triggered, this, &Bin::slotAddClip); setupAddClipAction(addClipMenu, ClipType::Color, QStringLiteral("add_color_clip"), i18n("Add Color Clip"), QIcon::fromTheme(QStringLiteral("kdenlive-add-color-clip"))); setupAddClipAction(addClipMenu, ClipType::SlideShow, QStringLiteral("add_slide_clip"), i18n("Add Slideshow Clip"), QIcon::fromTheme(QStringLiteral("kdenlive-add-slide-clip"))); setupAddClipAction(addClipMenu, ClipType::Text, QStringLiteral("add_text_clip"), i18n("Add Title Clip"), QIcon::fromTheme(QStringLiteral("kdenlive-add-text-clip"))); setupAddClipAction(addClipMenu, ClipType::TextTemplate, QStringLiteral("add_text_template_clip"), i18n("Add Template Title"), QIcon::fromTheme(QStringLiteral("kdenlive-add-text-clip"))); QAction *downloadResourceAction = addAction(QStringLiteral("download_resource"), i18n("Online Resources"), QIcon::fromTheme(QStringLiteral("edit-download"))); addClipMenu->addAction(downloadResourceAction); connect(downloadResourceAction, &QAction::triggered, pCore->window(), &MainWindow::slotDownloadResources); m_locateAction = addAction(QStringLiteral("locate_clip"), i18n("Locate Clip..."), QIcon::fromTheme(QStringLiteral("edit-file"))); m_locateAction->setData("locate_clip"); m_locateAction->setEnabled(false); connect(m_locateAction, &QAction::triggered, this, &Bin::slotLocateClip); m_reloadAction = addAction(QStringLiteral("reload_clip"), i18n("Reload Clip"), QIcon::fromTheme(QStringLiteral("view-refresh"))); m_reloadAction->setData("reload_clip"); m_reloadAction->setEnabled(false); connect(m_reloadAction, &QAction::triggered, this, &Bin::slotReloadClip); m_duplicateAction = addAction(QStringLiteral("duplicate_clip"), i18n("Duplicate Clip"), QIcon::fromTheme(QStringLiteral("edit-copy"))); m_duplicateAction->setData("duplicate_clip"); m_duplicateAction->setEnabled(false); connect(m_duplicateAction, &QAction::triggered, this, &Bin::slotDuplicateClip); m_proxyAction = new QAction(i18n("Proxy Clip"), pCore->window()); pCore->window()->addAction(QStringLiteral("proxy_clip"), m_proxyAction); m_proxyAction->setData(QStringList() << QString::number(static_cast(AbstractClipJob::PROXYJOB))); m_proxyAction->setCheckable(true); m_proxyAction->setChecked(false); m_editAction = addAction(QStringLiteral("clip_properties"), i18n("Clip Properties"), QIcon::fromTheme(QStringLiteral("document-edit"))); m_editAction->setData("clip_properties"); connect(m_editAction, &QAction::triggered, this, static_cast(&Bin::slotSwitchClipProperties)); m_openAction = addAction(QStringLiteral("edit_clip"), i18n("Edit Clip"), QIcon::fromTheme(QStringLiteral("document-open"))); m_openAction->setData("edit_clip"); m_openAction->setEnabled(false); connect(m_openAction, &QAction::triggered, this, &Bin::slotOpenClip); m_renameAction = addAction(QStringLiteral("rename_clip"), i18n("Rename Clip"), QIcon::fromTheme(QStringLiteral("document-edit"))); m_renameAction->setData("rename_clip"); m_renameAction->setEnabled(false); connect(m_renameAction, &QAction::triggered, this, &Bin::slotRenameItem); m_deleteAction = addAction(QStringLiteral("delete_clip"), i18n("Delete Clip"), QIcon::fromTheme(QStringLiteral("edit-delete"))); m_deleteAction->setData("delete_clip"); m_deleteAction->setEnabled(false); connect(m_deleteAction, &QAction::triggered, this, &Bin::slotDeleteClip); QAction *createFolder = addAction(QStringLiteral("create_folder"), i18n("Create Folder"), QIcon::fromTheme(QStringLiteral("folder-new"))); connect(createFolder, &QAction::triggered, this, &Bin::slotAddFolder); // Setup actions QAction *first = m_toolbar->actions().at(0); m_toolbar->insertAction(first, m_deleteAction); m_toolbar->insertAction(m_deleteAction, createFolder); auto *m = new QMenu(this); m->addActions(addClipMenu->actions()); m_addButton = new QToolButton(this); m_addButton->setMenu(m); m_addButton->setDefaultAction(addClip); m_addButton->setPopupMode(QToolButton::MenuButtonPopup); m_toolbar->insertWidget(createFolder, m_addButton); m_menu = new QMenu(this); m_propertiesDock = pCore->window()->addDock(i18n("Clip Properties"), QStringLiteral("clip_properties"), m_propertiesPanel); m_propertiesDock->close(); } const QString Bin::getDocumentProperty(const QString &key) { return m_doc->getDocumentProperty(key); } void Bin::slotUpdateJobStatus(const QString &id, int jobType, int status, const QString &label, const QString &actionName, const QString &details) { Q_UNUSED(id) Q_UNUSED(jobType) Q_UNUSED(status) Q_UNUSED(label) Q_UNUSED(actionName) Q_UNUSED(details) // TODO refac /* std::shared_ptr clip = m_itemModel->getClipByBinID(id); if (clip) { clip->setJobStatus((AbstractClipJob::JOBTYPE)jobType, (ClipJobStatus)status); } if (status == JobCrashed) { QList actions = m_infoMessage->actions(); if (m_infoMessage->isHidden()) { if (!details.isEmpty()) { m_infoMessage->setText(label + QStringLiteral(" ") + i18n("Show log") + QStringLiteral("")); } else { m_infoMessage->setText(label); } m_infoMessage->setWordWrap(m_infoMessage->text().length() > 35); m_infoMessage->setMessageType(KMessageWidget::Warning); } if (!actionName.isEmpty()) { QAction *action = nullptr; QList collections = KActionCollection::allCollections(); for (int i = 0; i < collections.count(); ++i) { KActionCollection *coll = collections.at(i); action = coll->action(actionName); if (action) { break; } } if ((action != nullptr) && !actions.contains(action)) { m_infoMessage->addAction(action); } } if (!details.isEmpty()) { m_errorLog.append(details); } m_infoMessage->setCloseButtonVisible(true); m_infoMessage->animatedShow(); } */ } void Bin::doDisplayMessage(const QString &text, KMessageWidget::MessageType type, const QList &actions) { // 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(); 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); bool res = false; if (effectData.count() == 4) { // Paste effect from another stack std::shared_ptr sourceStack = pCore->getItemEffectStack(effectData.at(1).toInt(), effectData.at(2).toInt()); res = std::static_pointer_cast(parentItem)->copyEffect(sourceStack, effectData.at(3).toInt()); } else { res = std::static_pointer_cast(parentItem)->addEffect(effectData.constFirst()); } if (!res) { pCore->displayMessage(i18n("Cannot add effect to clip"), InformationMessage); } } } void Bin::editMasterEffect(const std::shared_ptr &clip) { if (m_gainedFocus) { // Widget just gained focus, updating stack is managed in the eventfilter event, not from item return; } if (clip) { if (clip->itemType() == AbstractProjectItem::ClipItem) { std::shared_ptr clp = std::static_pointer_cast(clip); emit requestShowEffectStack(clp->clipName(), clp->m_effectStack, clp->getFrameSize(), false); return; } if (clip->itemType() == AbstractProjectItem::SubClipItem) { if (auto ptr = clip->parentItem().lock()) { std::shared_ptr clp = std::static_pointer_cast(ptr); emit requestShowEffectStack(clp->clipName(), clp->m_effectStack, clp->getFrameSize(), false); } return; } } emit requestShowEffectStack(QString(), nullptr, QSize(), false); } void Bin::slotGotFocus() { m_gainedFocus = true; } void Bin::doMoveClip(const QString &id, const QString &newParentId) { std::shared_ptr currentItem = m_itemModel->getClipByBinID(id); if (!currentItem) { return; } std::shared_ptr currentParent = currentItem->parent(); std::shared_ptr newParent = m_itemModel->getFolderByBinId(newParentId); currentItem->changeParent(newParent); } void Bin::doMoveFolder(const QString &id, const QString &newParentId) { std::shared_ptr currentItem = m_itemModel->getFolderByBinId(id); std::shared_ptr currentParent = currentItem->parent(); std::shared_ptr newParent = m_itemModel->getFolderByBinId(newParentId); currentParent->removeChild(currentItem); currentItem->changeParent(newParent); } void Bin::droppedUrls(const QList &urls, const QString &folderInfo) { QModelIndex current; if (folderInfo.isEmpty()) { current = m_proxyModel->mapToSource(m_proxyModel->selectionModel()->currentIndex()); } else { // get index for folder std::shared_ptr folder = m_itemModel->getFolderByBinId(folderInfo); if (!folder) { folder = m_itemModel->getRootFolder(); } current = m_itemModel->getIndexFromItem(folder); } slotItemDropped(urls, current); } void Bin::slotAddClipToProject(const QUrl &url) { QList urls; urls << url; QModelIndex current = m_proxyModel->mapToSource(m_proxyModel->selectionModel()->currentIndex()); slotItemDropped(urls, current); } void Bin::slotItemDropped(const QList &urls, const QModelIndex &parent) { QString parentFolder = m_itemModel->getRootFolder()->clipId(); if (parent.isValid()) { // Check if drop occurred on a folder std::shared_ptr parentItem = m_itemModel->getBinItemByIndex(parent); while (parentItem->itemType() != AbstractProjectItem::FolderItem) { parentItem = parentItem->parent(); } parentFolder = parentItem->clipId(); } 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->updateZones(); emit itemUpdated(sub); } Timecode Bin::projectTimecode() const { return m_doc->timecode(); } void Bin::slotStartFilterJob(const ItemInfo &info, const QString &id, QMap &filterParams, QMap &consumerParams, QMap &extraParams) { Q_UNUSED(info) Q_UNUSED(id) Q_UNUSED(filterParams) Q_UNUSED(consumerParams) Q_UNUSED(extraParams) // TODO refac /* std::shared_ptr clip = getBinClip(id); if (!clip) { return; } QMap producerParams = QMap(); producerParams.insert(QStringLiteral("producer"), clip->url()); if (info.cropDuration != GenTime()) { producerParams.insert(QStringLiteral("in"), QString::number((int)info.cropStart.frames(pCore->getCurrentFps()))); producerParams.insert(QStringLiteral("out"), QString::number((int)(info.cropStart + info.cropDuration).frames(pCore->getCurrentFps()))); extraParams.insert(QStringLiteral("clipStartPos"), QString::number((int)info.startPos.frames(pCore->getCurrentFps()))); extraParams.insert(QStringLiteral("clipTrack"), QString::number(info.track)); } else { // We want to process whole clip producerParams.insert(QStringLiteral("in"), QString::number(0)); producerParams.insert(QStringLiteral("out"), QString::number(-1)); } */ } void Bin::focusBinView() const { m_itemView->setFocus(); } void Bin::slotOpenClip() { std::shared_ptr clip = getFirstSelectedClip(); if (!clip) { return; } switch (clip->clipType()) { case ClipType::Text: case ClipType::TextTemplate: showTitleWidget(clip); break; case ClipType::Image: if (KdenliveSettings::defaultimageapp().isEmpty()) { KMessageBox::sorry(QApplication::activeWindow(), i18n("Please set a default application to open images in the Settings dialog")); } else { QProcess::startDetached(KdenliveSettings::defaultimageapp(), QStringList() << clip->url()); } break; case ClipType::Audio: if (KdenliveSettings::defaultaudioapp().isEmpty()) { KMessageBox::sorry(QApplication::activeWindow(), i18n("Please set a default application to open audio files in the Settings dialog")); } else { QProcess::startDetached(KdenliveSettings::defaultaudioapp(), QStringList() << clip->url()); } break; default: break; } } void Bin::updateTimecodeFormat() { emit refreshTimeCode(); } /* void Bin::slotGotFilterJobResults(const QString &id, int startPos, int track, const stringMap &results, const stringMap &filterInfo) { if (filterInfo.contains(QStringLiteral("finalfilter"))) { if (filterInfo.contains(QStringLiteral("storedata"))) { // Store returned data as clip extra data std::shared_ptr clip = getBinClip(id); if (clip) { QString key = filterInfo.value(QStringLiteral("key")); QStringList newValue = clip->updatedAnalysisData(key, results.value(key), filterInfo.value(QStringLiteral("offset")).toInt()); slotAddClipExtraData(id, newValue.at(0), newValue.at(1)); } } if (startPos == -1) { // Processing bin clip std::shared_ptr currentItem = m_itemModel->getClipByBinID(id); if (!currentItem) { return; } std::shared_ptr ctl = std::static_pointer_cast(currentItem); EffectsList list = ctl->effectList(); QDomElement effect = list.effectById(filterInfo.value(QStringLiteral("finalfilter"))); QDomDocument doc; QDomElement e = doc.createElement(QStringLiteral("test")); doc.appendChild(e); e.appendChild(doc.importNode(effect, true)); if (!effect.isNull()) { QDomElement newEffect = effect.cloneNode().toElement(); QMap::const_iterator i = results.constBegin(); while (i != results.constEnd()) { EffectsList::setParameter(newEffect, i.key(), i.value()); ++i; } ctl->updateEffect(newEffect, effect.attribute(QStringLiteral("kdenlive_ix")).toInt()); emit requestClipShow(currentItem); // TODO use undo / redo for bin clip edit effect // EditEffectCommand *command = new EditEffectCommand(this, clip->track(), clip->startPos(), effect, newEffect, clip->selectedEffectIndex(), true, true); m_commandStack->push(command); emit clipItemSelected(clip); } // emit gotFilterJobResults(id, startPos, track, results, filterInfo); return; } // This is a timeline filter, forward results emit gotFilterJobResults(id, startPos, track, results, filterInfo); return; } // Currently, only the first value of results is used std::shared_ptr clip = getBinClip(id); if (!clip) { return; } // Check for return value int markersType = -1; if (filterInfo.contains(QStringLiteral("addmarkers"))) { markersType = filterInfo.value(QStringLiteral("addmarkers")).toInt(); } if (results.isEmpty()) { emit displayBinMessage(i18n("No data returned from clip analysis"), KMessageWidget::Warning); return; } bool dataProcessed = false; QString label = filterInfo.value(QStringLiteral("label")); QString key = filterInfo.value(QStringLiteral("key")); int offset = filterInfo.value(QStringLiteral("offset")).toInt(); 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())) { // duration changed, we need to update duration newprops.insert(QStringLiteral("out"), clip->framesToTime(dia_ui.duration() - 1)); int currentLength = clip->getProducerDuration(); if (currentLength != dia_ui.duration()) { newprops.insert(QStringLiteral("kdenlive:duration"), clip->framesToTime(dia_ui.duration())); } } // trigger producer reload newprops.insert(QStringLiteral("force_reload"), QStringLiteral("2")); if (!path.isEmpty()) { // we are editing an external file, asked if we want to detach from that file or save the result to that title file. if (KMessageBox::questionYesNo(pCore->window(), i18n("You are editing an external title clip (%1). Do you want to save your changes to the title " "file or save the changes for this project only?", path), i18n("Save Title"), KGuiItem(i18n("Save to title file")), KGuiItem(i18n("Save in project only"))) == KMessageBox::Yes) { // save to external file dia_ui.saveTitle(QUrl::fromLocalFile(path)); + return; } else { newprops.insert(QStringLiteral("resource"), QString()); } } slotEditClipCommand(clip->AbstractProjectItem::clipId(), clip->currentProperties(newprops), newprops); } } void Bin::slotResetInfoMessage() { m_errorLog.clear(); 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 { 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, false, false); // Make sure we reload clip length xml.removeAttribute(QStringLiteral("out")); Xml::removeXmlProperty(xml, QStringLiteral("length")); clip->resetProducerProperty(QStringLiteral("kdenlive:duration")); clip->resetProducerProperty(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/clipcreator.cpp b/src/bin/clipcreator.cpp index 6f2000160..b23182941 100644 --- a/src/bin/clipcreator.cpp +++ b/src/bin/clipcreator.cpp @@ -1,329 +1,318 @@ /*************************************************************************** * 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 "clipcreator.hpp" #include "bin/bin.h" #include "core.h" #include "doc/kdenlivedoc.h" #include "kdenlivesettings.h" #include "klocalizedstring.h" #include "macros.hpp" #include "mainwindow.h" #include "projectitemmodel.h" #include "titler/titledocument.h" #include "utils/devices.hpp" #include "xml/xml.hpp" #include #include #include #include #include #include #include namespace { QDomElement createProducer(QDomDocument &xml, ClipType::ProducerType type, const QString &resource, const QString &name, int duration, const QString &service) { QDomElement prod = xml.createElement(QStringLiteral("producer")); xml.appendChild(prod); prod.setAttribute(QStringLiteral("type"), (int)type); prod.setAttribute(QStringLiteral("in"), QStringLiteral("0")); prod.setAttribute(QStringLiteral("length"), duration); std::unordered_map properties; properties[QStringLiteral("resource")] = resource; if (!name.isEmpty()) { properties[QStringLiteral("kdenlive:clipname")] = name; } if (!service.isEmpty()) { properties[QStringLiteral("mlt_service")] = service; } Xml::addXmlProperties(prod, properties); return prod; } } // namespace QString ClipCreator::createTitleClip(const std::unordered_map &properties, int duration, const QString &name, const QString &parentFolder, const std::shared_ptr &model) { QDomDocument xml; auto prod = createProducer(xml, ClipType::Text, QString(), name, duration, QStringLiteral("kdenlivetitle")); Xml::addXmlProperties(prod, properties); QString id; bool res = model->requestAddBinClip(id, xml.documentElement(), parentFolder, i18n("Create title clip")); return res ? id : QStringLiteral("-1"); } QString ClipCreator::createColorClip(const QString &color, int duration, const QString &name, const QString &parentFolder, const std::shared_ptr &model) { QDomDocument xml; auto prod = createProducer(xml, ClipType::Color, color, name, duration, QStringLiteral("color")); QString id; bool res = model->requestAddBinClip(id, xml.documentElement(), parentFolder, i18n("Create color clip")); return res ? id : QStringLiteral("-1"); } QString ClipCreator::createClipFromFile(const QString &path, const QString &parentFolder, const std::shared_ptr &model, Fun &undo, Fun &redo, const std::function &readyCallBack) { QDomDocument xml; QMimeDatabase db; QMimeType type = db.mimeTypeForUrl(QUrl::fromLocalFile(path)); qDebug() << "/////////// createClipFromFile" << path << parentFolder << path << type.name(); QDomElement prod; if (type.name().startsWith(QLatin1String("image/"))) { int duration = pCore->currentDoc()->getFramePos(KdenliveSettings::image_duration()); prod = createProducer(xml, ClipType::Image, path, QString(), duration, QString()); } else if (type.inherits(QStringLiteral("application/x-kdenlivetitle"))) { // opening a title file QDomDocument txtdoc(QStringLiteral("titledocument")); QFile txtfile(path); if (txtfile.open(QIODevice::ReadOnly) && txtdoc.setContent(&txtfile)) { txtfile.close(); // extract embedded images QDomNodeList items = txtdoc.elementsByTagName(QStringLiteral("content")); for (int j = 0; j < items.count(); ++j) { QDomElement content = items.item(j).toElement(); if (content.hasAttribute(QStringLiteral("base64"))) { QString titlesFolder = pCore->currentDoc()->projectDataFolder() + QStringLiteral("/titles/"); QString imgPath = TitleDocument::extractBase64Image(titlesFolder, content.attribute(QStringLiteral("base64"))); if (!imgPath.isEmpty()) { content.setAttribute(QStringLiteral("url"), imgPath); content.removeAttribute(QStringLiteral("base64")); } } } - prod.setAttribute(QStringLiteral("in"), 0); - int duration = 0; - if (txtdoc.documentElement().hasAttribute(QStringLiteral("duration"))) { - duration = txtdoc.documentElement().attribute(QStringLiteral("duration")).toInt(); - } else if (txtdoc.documentElement().hasAttribute(QStringLiteral("out"))) { - duration = txtdoc.documentElement().attribute(QStringLiteral("out")).toInt(); - } - if (duration <= 0) { - duration = pCore->currentDoc()->getFramePos(KdenliveSettings::title_duration()) - 1; - } - prod = createProducer(xml, ClipType::Text, path, QString(), duration, QString()); - txtdoc.documentElement().setAttribute(QStringLiteral("kdenlive:duration"), duration); + prod = createProducer(xml, ClipType::Text, path, QString(), -1, QString()); QString titleData = txtdoc.toString(); prod.setAttribute(QStringLiteral("xmldata"), titleData); } else { txtfile.close(); return QStringLiteral("-1"); } } else { // it is a "normal" file, just use a producer prod = xml.createElement(QStringLiteral("producer")); xml.appendChild(prod); QMap properties; properties.insert(QStringLiteral("resource"), path); Xml::addXmlProperties(prod, properties); } if (pCore->bin()->isEmpty() && (KdenliveSettings::default_profile().isEmpty() || KdenliveSettings::checkfirstprojectclip())) { prod.setAttribute(QStringLiteral("_checkProfile"), 1); } qDebug() << "/////////// final xml" << xml.toString(); QString id; bool res = model->requestAddBinClip(id, xml.documentElement(), parentFolder, undo, redo, readyCallBack); return res ? id : QStringLiteral("-1"); } bool ClipCreator::createClipFromFile(const QString &path, const QString &parentFolder, std::shared_ptr model) { Fun undo = []() { return true; }; Fun redo = []() { return true; }; auto id = ClipCreator::createClipFromFile(path, parentFolder, std::move(model), undo, redo); bool ok = (id != QStringLiteral("-1")); if (ok) { pCore->pushUndo(undo, redo, i18n("Add clip")); } return ok; } QString ClipCreator::createSlideshowClip(const QString &path, int duration, const QString &name, const QString &parentFolder, const std::unordered_map &properties, const std::shared_ptr &model) { QDomDocument xml; auto prod = createProducer(xml, ClipType::SlideShow, path, name, duration, QString()); Xml::addXmlProperties(prod, properties); QString id; bool res = model->requestAddBinClip(id, xml.documentElement(), parentFolder, i18n("Create slideshow clip")); return res ? id : QStringLiteral("-1"); } QString ClipCreator::createTitleTemplate(const QString &path, const QString &text, const QString &name, const QString &parentFolder, const std::shared_ptr &model) { QDomDocument xml; // We try to retrieve duration for template int duration = 0; QDomDocument titledoc; QFile txtfile(path); if (txtfile.open(QIODevice::ReadOnly) && titledoc.setContent(&txtfile)) { if (titledoc.documentElement().hasAttribute(QStringLiteral("duration"))) { duration = titledoc.documentElement().attribute(QStringLiteral("duration")).toInt(); } else { // keep some time for backwards compatibility - 26/12/12 duration = titledoc.documentElement().attribute(QStringLiteral("out")).toInt(); } } txtfile.close(); // Duration not found, we fall-back to defaults if (duration == 0) { duration = pCore->currentDoc()->getFramePos(KdenliveSettings::title_duration()); } auto prod = createProducer(xml, ClipType::TextTemplate, path, name, duration, QString()); if (!text.isEmpty()) { prod.setAttribute(QStringLiteral("templatetext"), text); } QString id; bool res = model->requestAddBinClip(id, xml.documentElement(), parentFolder, i18n("Create title template")); return res ? id : QStringLiteral("-1"); } bool ClipCreator::createClipsFromList(const QList &list, bool checkRemovable, const QString &parentFolder, const std::shared_ptr &model, Fun &undo, Fun &redo, bool topLevel) { QScopedPointer progressDialog; if (topLevel) { progressDialog.reset(new QProgressDialog(pCore->window())); progressDialog->setWindowTitle(i18n("Loading clips")); progressDialog->setCancelButton(nullptr); progressDialog->setLabelText(i18n("Importing bin clips...")); progressDialog->setMaximum(0); progressDialog->show(); progressDialog->repaint(); qApp->processEvents(); } qDebug() << "/////////// creatclipsfromlist" << list << checkRemovable << parentFolder; bool created = false; QMimeDatabase db; for (const QUrl &file : list) { if (!QFile::exists(file.toLocalFile())) { continue; } QMimeType mType = db.mimeTypeForUrl(file); if (mType.inherits(QLatin1String("inode/directory"))) { // user dropped a folder, import its files QDir dir(file.path()); QString folderId; Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; bool folderCreated = pCore->projectItemModel()->requestAddFolder(folderId, dir.dirName(), parentFolder, local_undo, local_redo); if (!folderCreated) { continue; } QStringList result = dir.entryList(QDir::Files); QStringList subfolders = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); QList folderFiles; for (const QString &path : result) { QUrl url = QUrl::fromLocalFile(dir.absoluteFilePath(path)); // Check file is of a supported type mType = db.mimeTypeForUrl(url); QString mimeAliases = mType.name(); bool isValid = mimeAliases.contains(QLatin1String("video/")); if (!isValid) { isValid = mimeAliases.contains(QLatin1String("audio/")); } if (!isValid) { isValid = mimeAliases.contains(QLatin1String("image/")); } if (!isValid && (mType.inherits(QLatin1String("video/mlt-playlist")) || mType.inherits(QLatin1String("application/x-kdenlivetitle")))) { isValid = true; } if (isValid) { folderFiles.append(url); } } if (folderFiles.isEmpty()) { QList sublist; for (const QString &sub : subfolders) { QUrl url = QUrl::fromLocalFile(dir.absoluteFilePath(sub)); if (!list.contains(url)) { sublist << url; } } if (!sublist.isEmpty()) { // load subfolders created = created || createClipsFromList(sublist, checkRemovable, folderId, model, undo, redo, false); } } else { bool clipsCreated = createClipsFromList(folderFiles, checkRemovable, folderId, model, local_undo, local_redo, false); created = true; if (!clipsCreated) { local_undo(); } else { UPDATE_UNDO_REDO_NOLOCK(local_redo, local_undo, undo, redo) } // Check subfolders QList sublist; for (const QString &sub : subfolders) { QUrl url = QUrl::fromLocalFile(dir.absoluteFilePath(sub)); if (!list.contains(url)) { sublist << url; } } if (!sublist.isEmpty()) { // load subfolders createClipsFromList(sublist, checkRemovable, folderId, model, undo, redo, false); } } } else { // file is not a directory if (checkRemovable && isOnRemovableDevice(file) && !isOnRemovableDevice(pCore->currentDoc()->projectDataFolder())) { int answer = KMessageBox::warningContinueCancel( QApplication::activeWindow(), i18n("Clip %1
is on a removable device, will not be available when device is unplugged or mounted at a different position. You " "may want to copy it first to your hard-drive. Would you like to add it anyways?", file.path()), i18n("Removable device"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), QStringLiteral("confirm_removable_device")); if (answer == KMessageBox::Cancel) continue; } QString id = ClipCreator::createClipFromFile(file.toLocalFile(), parentFolder, model, undo, redo); created = created || (id != QStringLiteral("-1")); } } qDebug() << "/////////// creatclipsfromlist return" << created; return created; } bool ClipCreator::createClipsFromList(const QList &list, bool checkRemovable, const QString &parentFolder, std::shared_ptr model) { Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool ok = ClipCreator::createClipsFromList(list, checkRemovable, parentFolder, std::move(model), undo, redo); if (ok) { pCore->pushUndo(undo, redo, i18np("Add clip", "Add clips", list.size())); } return ok; } diff --git a/src/bin/filewatcher.cpp b/src/bin/filewatcher.cpp index b0f8dfd5a..846447e53 100644 --- a/src/bin/filewatcher.cpp +++ b/src/bin/filewatcher.cpp @@ -1,116 +1,123 @@ /*************************************************************************** * Copyright (C) 2017 by Jean-Baptiste Mardelle * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #include "filewatcher.hpp" #include #include FileWatcher::FileWatcher(QObject *parent) : QObject(parent) , m_fileWatcher(new KDirWatch()) { // Init clip modification tracker m_modifiedTimer.setInterval(1500); connect(m_fileWatcher.get(), &KDirWatch::dirty, this, &FileWatcher::slotUrlModified); connect(m_fileWatcher.get(), &KDirWatch::deleted, this, &FileWatcher::slotUrlMissing); + connect(m_fileWatcher.get(), &KDirWatch::created, this, &FileWatcher::slotUrlAdded); connect(&m_modifiedTimer, &QTimer::timeout, this, &FileWatcher::slotProcessModifiedUrls); } void FileWatcher::addFile(const QString &binId, const QString &url) { if (url.isEmpty()) { return; } QFileInfo check_file(url); // check if file exists and if yes: Is it really a file and no directory? if (!check_file.exists() || !check_file.isFile()) { return; } if (m_occurences.count(url) == 0) { m_fileWatcher->addFile(url); } m_occurences[url].insert(binId); m_binClipPaths[binId] = url; } void FileWatcher::removeFile(const QString &binId) { if (m_binClipPaths.count(binId) == 0) { return; } QString url = m_binClipPaths[binId]; m_occurences[url].erase(binId); m_binClipPaths.erase(binId); if (m_occurences[url].empty()) { m_fileWatcher->removeFile(url); m_occurences.erase(url); } } void FileWatcher::slotUrlModified(const QString &path) { if (m_modifiedUrls.count(path) == 0) { m_modifiedUrls.insert(path); for (const QString &id : m_occurences[path]) { emit binClipWaiting(id); } } if (!m_modifiedTimer.isActive()) { m_modifiedTimer.start(); } } +void FileWatcher::slotUrlAdded(const QString &path) +{ + for (const QString &id : m_occurences[path]) { + emit binClipModified(id); + } +} + void FileWatcher::slotUrlMissing(const QString &path) { - // TODO handle missing clips by replacing producer with an invalid producer for (const QString &id : m_occurences[path]) { emit binClipMissing(id); } } void FileWatcher::slotProcessModifiedUrls() { auto checkList = m_modifiedUrls; for (const QString &path : checkList) { if (m_fileWatcher->ctime(path).msecsTo(QDateTime::currentDateTime()) > 1000) { for (const QString &id : m_occurences[path]) { emit binClipModified(id); } m_modifiedUrls.erase(path); } } if (m_modifiedUrls.empty()) { m_modifiedTimer.stop(); } } void FileWatcher::clear() { m_fileWatcher->stopScan(); for (const auto &f : m_occurences) { m_fileWatcher->removeFile(f.first); } m_occurences.clear(); m_modifiedUrls.clear(); m_binClipPaths.clear(); m_fileWatcher->startScan(); } diff --git a/src/bin/filewatcher.hpp b/src/bin/filewatcher.hpp index 3ad068329..15baa4d61 100644 --- a/src/bin/filewatcher.hpp +++ b/src/bin/filewatcher.hpp @@ -1,76 +1,77 @@ /*************************************************************************** * Copyright (C) 2017 by Jean-Baptiste Mardelle * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #ifndef FILEWATCHER_H #define FILEWATCHER_H #include "definitions.h" #include #include #include #include /** @brief This class is responsible for watching all files used in the project and triggers a reload notification when a file changes. */ class FileWatcher : public QObject { Q_OBJECT public: // Constructor explicit FileWatcher(QObject *parent = nullptr); // Add a file to the list of watched items void addFile(const QString &binId, const QString &url); // Remove a binId from the list of watched items void removeFile(const QString &binId); // Reset all watched files void clear(); signals: /** @brief This signal is triggered whenever the file corresponding to a bin clip has been modified and should be reloaded. Note that this signal is sent no * more than every 1500 ms. We also make sure that at least 1000ms has passed since the last modification of the file. */ void binClipModified(const QString &binId); /** @brief Same signal than binClipModified, but triggers immediately. Can be useful to refresh UI without actually reloading the file (yet)*/ void binClipWaiting(const QString &binId); void binClipMissing(const QString &binId); private slots: void slotUrlModified(const QString &path); void slotUrlMissing(const QString &path); + void slotUrlAdded(const QString &path); void slotProcessModifiedUrls(); private: // This is a handle to the watcher singleton, not owned by this class. std::unique_ptr m_fileWatcher; // A list with urls as keys, and the corresponding clip ids as value std::unordered_map> m_occurences; // keys are binId, keys are stored paths std::unordered_map m_binClipPaths; // List of files for which we received an update since the last send std::unordered_set m_modifiedUrls; QTimer m_modifiedTimer; }; #endif diff --git a/src/bin/projectclip.cpp b/src/bin/projectclip.cpp index 94b6c8288..ff625ed10 100644 --- a/src/bin/projectclip.cpp +++ b/src/bin/projectclip.cpp @@ -1,1394 +1,1397 @@ /* Copyright (C) 2012 Till Theato Copyright (C) 2014 Jean-Baptiste Mardelle This file is part of Kdenlive. See www.kdenlive.org. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "projectclip.h" #include "bin.h" #include "core.h" #include "doc/docundostack.hpp" #include "doc/kdenlivedoc.h" #include "doc/kthumb.h" #include "effects/effectstack/model/effectstackmodel.hpp" #include "jobs/audiothumbjob.hpp" #include "jobs/jobmanager.h" #include "jobs/loadjob.hpp" #include "jobs/thumbjob.hpp" #include "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); QMetaObject::invokeMethod(model.get(), "loadSubClips", Qt::QueuedConnection, Q_ARG(const QString&, id), Q_ARG(const QString&, self->getProducerProperty(QStringLiteral("kdenlive:clipzones")))); return self; } void ProjectClip::importEffects(const std::shared_ptr &producer) { m_effectStack->importEffects(producer, PlaylistState::Disabled, true); } ProjectClip::ProjectClip(const QString &id, const QDomElement &description, const QIcon &thumb, const std::shared_ptr &model) : AbstractProjectItem(AbstractProjectItem::ClipItem, id, model) , ClipController(id) , m_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::dataChanged, [&]() { if (auto ptr = m_model.lock()) { std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this()), AbstractProjectItem::IconOverlay); } }); } QString ProjectClip::getToolTip() const { 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(QList audioLevels) { audioFrameCache = audioLevels; m_audioThumbCreated = true; if (auto ptr = m_model.lock()) { emit std::static_pointer_cast(ptr)->refreshAudioThumbs(m_binId); } } 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); } if (QFile::exists(m_path) && !hasProxy()) { clearBackupProperties(); } QDomDocument doc; QDomElement xml = toXml(doc); if (!xml.isNull()) { pCore->jobManager()->discardJobs(clipId(), AbstractClipJob::THUMBJOB); m_thumbsProducer.reset(); - xml.removeAttribute("out"); + ClipType::ProducerType type = clipType(); + if (type != ClipType::Color && type != ClipType::Image) { + xml.removeAttribute("out"); + } 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, bool includeProfile) { getProducerXML(document, includeMeta, includeProfile); 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); // Required to make get_playtime() return > 1 m_thumbsProducer->set("out", m_thumbsProducer->get_length() -1); m_thumbsProducer->attach(scaler); m_thumbsProducer->attach(padder); m_thumbsProducer->attach(converter); } } return m_thumbsProducer; } void ProjectClip::createDisabledMasterProducer() { if (!m_disabledProducer) { m_disabledProducer = cloneProducer(); m_disabledProducer->set("set.test_audio", 1); m_disabledProducer->set("set.test_image", 1); m_effectStack->addService(m_disabledProducer); } } std::shared_ptr ProjectClip::getTimelineProducer(int trackId, int clipId, PlaylistState::ClipState state, 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 Mlt::Properties original(m_masterProducer->get_properties()); Mlt::Properties cloneProps(warpProducer->get_properties()); cloneProps.pass_list(original, ClipController::getPassPropertiesList(false)); warpProducer->set("length", (int) (original_length / std::abs(speed) + 0.5)); } qDebug() << "warp LENGTH" << warpProducer->get_length(); warpProducer->set("set.test_audio", 1); warpProducer->set("set.test_image", 1); if (state == PlaylistState::AudioOnly) { warpProducer->set("set.test_audio", 0); } if (state == PlaylistState::VideoOnly) { warpProducer->set("set.test_image", 0); } m_timewarpProducers[clipId] = warpProducer; m_effectStack->addService(m_timewarpProducers[clipId]); return std::shared_ptr(warpProducer->cut()); } std::pair, bool> ProjectClip::giveMasterAndGetTimelineProducer(int clipId, std::shared_ptr master, PlaylistState::ClipState state) { int 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) { if (!m_disabledProducer) { qDebug() << "Warning: weird, we found a disabled clip whose master is already loaded but we don't have any yet"; createDisabledMasterProducer(); } return {std::shared_ptr(m_disabledProducer->cut(in, out)), false}; } // We have a good id, this clip can be used return {master, true}; } else { master->parent().set("_loaded", 1); if (timeWarp) { m_timewarpProducers[clipId] = std::make_shared(&master->parent()); m_effectStack->loadService(m_timewarpProducers[clipId]); return {master, true}; } if (state == PlaylistState::AudioOnly) { 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) { if (!m_disabledProducer) { createDisabledMasterProducer(); } return {std::make_shared(m_disabledProducer->cut(master->get_in(), master->get_out())), true}; } qDebug() << "Warning: weird, we found a clip whose master is not loaded but we already have a master"; Q_ASSERT(false); } } else if (master->is_valid()) { // in that case, we have a master qDebug() << "Warning: weird, we received a master clip in lieue of a cut"; double speed = 1.0; if (QString::fromUtf8(master->parent().get("mlt_service")) == QLatin1String("timewarp")) { speed = master->get_double("warp_speed"); } return {getTimelineProducer(-1, clipId, state, 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"); } // we pass some properties that wouldn't be passed because of the novalidate const char *prefix = "meta."; const size_t prefix_len = strlen(prefix); for (int i = 0; i < m_masterProducer->count(); ++i) { char *current = m_masterProducer->get_name(i); if (strlen(current) >= prefix_len && strncmp(current, prefix, prefix_len) == 0) { prod->set(current, m_masterProducer->get(i)); } } if (removeEffects) { int ct = 0; Mlt::Filter *filter = prod->filter(ct); while (filter) { qDebug() << "// EFFECT " << ct << " : " << filter->get("mlt_service"); QString ix = QString::fromLatin1(filter->get("kdenlive_id")); if (!ix.isEmpty()) { qDebug() << "/ + + 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()); backupOriginalProperties(); 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() && (!reload || refreshOnly)) { 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); } } 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::replaceInTimeline() { for (const auto &clip : m_registeredClips) { if (auto timeline = clip.second.lock()) { timeline->requestClipReload(clip.first); } else { qDebug() << "Error while reloading clip: timeline unavailable"; Q_ASSERT(false); } } } void ProjectClip::updateTimelineClips(const QVector &roles) { for (const auto &clip : m_registeredClips) { if (auto timeline = clip.second.lock()) { timeline->requestClipUpdate(clip.first, roles); } else { qDebug() << "Error while reloading clip thumb: timeline unavailable"; Q_ASSERT(false); return; } } } void ProjectClip::updateZones() { int zonesCount = childCount(); if (zonesCount == 0) { resetProducerProperty(QStringLiteral("kdenlive:clipzones")); return; } QJsonArray list; for (int i = 0; i < zonesCount; ++i) { std::shared_ptr clip = std::static_pointer_cast(child(i)); if (clip) { QJsonObject currentZone; currentZone.insert(QLatin1String("name"), QJsonValue(clip->name())); QPoint zone = clip->zone(); currentZone.insert(QLatin1String("in"), QJsonValue(zone.x())); currentZone.insert(QLatin1String("out"), QJsonValue(zone.y())); list.push_back(currentZone); } } QJsonDocument json(list); setProducerProperty(QStringLiteral("kdenlive:clipzones"), QString(json.toJson())); } diff --git a/src/jobs/loadjob.cpp b/src/jobs/loadjob.cpp index a5950594b..634eb96fc 100644 --- a/src/jobs/loadjob.cpp +++ b/src/jobs/loadjob.cpp @@ -1,602 +1,619 @@ /*************************************************************************** * 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 "loadjob.hpp" #include "bin/projectclip.h" #include "bin/projectfolder.h" #include "bin/projectitemmodel.h" #include "core.h" #include "doc/kdenlivedoc.h" #include "doc/kthumb.h" #include "kdenlivesettings.h" #include "klocalizedstring.h" #include "macros.hpp" #include "profiles/profilemodel.hpp" #include "project/dialogs/slideshowclip.h" #include "monitor/monitor.h" #include "xml/xml.hpp" #include #include #include #include #include LoadJob::LoadJob(const QString &binId, const QDomElement &xml, const std::function &readyCallBack) : AbstractClipJob(LOADJOB, binId) , m_xml(xml) , m_readyCallBack(readyCallBack) { } const QString LoadJob::getDescription() const { return i18n("Loading clip %1", m_clipId); } namespace { ClipType::ProducerType getTypeForService(const QString &id, const QString &path) { if (id.isEmpty()) { QString ext = path.section(QLatin1Char('.'), -1); if (ext == QLatin1String("mlt") || ext == QLatin1String("kdenlive")) { return ClipType::Playlist; } return ClipType::Unknown; } if (id == QLatin1String("color") || id == QLatin1String("colour")) { return ClipType::Color; } if (id == QLatin1String("kdenlivetitle")) { return ClipType::Text; } if (id == QLatin1String("qtext")) { return ClipType::QText; } if (id == QLatin1String("xml") || id == QLatin1String("consumer")) { return ClipType::Playlist; } if (id == QLatin1String("webvfx")) { return ClipType::WebVfx; } return ClipType::Unknown; } // Read the properties of the xml and pass them to the producer. Note that some properties like resource are ignored void processProducerProperties(const std::shared_ptr &prod, const QDomElement &xml) { // TODO: there is some duplication with clipcontroller > updateproducer that also copies properties QString value; QStringList internalProperties; internalProperties << QStringLiteral("bypassDuplicate") << QStringLiteral("resource") << QStringLiteral("mlt_service") << QStringLiteral("audio_index") << QStringLiteral("video_index") << QStringLiteral("mlt_type") << QStringLiteral("length"); QDomNodeList props; if (xml.tagName() == QLatin1String("producer")) { props = xml.childNodes(); } else { props = xml.firstChildElement(QStringLiteral("producer")).childNodes(); } for (int i = 0; i < props.count(); ++i) { if (props.at(i).toElement().tagName() != QStringLiteral("property")) { continue; } QString propertyName = props.at(i).toElement().attribute(QStringLiteral("name")); if (!internalProperties.contains(propertyName) && !propertyName.startsWith(QLatin1Char('_'))) { value = props.at(i).firstChild().nodeValue(); if (propertyName.startsWith(QLatin1String("kdenlive-force."))) { // this is a special forced property, pass it propertyName.remove(0, 15); } prod->set(propertyName.toUtf8().constData(), value.toUtf8().constData()); } } } } // namespace // static std::shared_ptr LoadJob::loadResource(QString &resource, const QString &type) { if (!resource.startsWith(type)) { resource.prepend(type); } return std::make_shared(pCore->getCurrentProfile()->profile(), nullptr, resource.toUtf8().constData()); } std::shared_ptr LoadJob::loadPlaylist(QString &resource) { std::unique_ptr xmlProfile(new Mlt::Profile()); xmlProfile->set_explicit(0); std::unique_ptr producer(new Mlt::Producer(*xmlProfile, "xml", resource.toUtf8().constData())); if (!producer->is_valid()) { qDebug() << "////// ERROR, CANNOT LOAD SELECTED PLAYLIST: " << resource; return nullptr; } if (pCore->getCurrentProfile()->isCompatible(xmlProfile.get())) { // We can use the "xml" producer since profile is the same (using it with different profiles corrupts the project. // Beware that "consumer" currently crashes on audio mixes! //resource.prepend(QStringLiteral("xml:")); } else { // This is currently crashing so I guess we'd better reject it for now m_errorMessage.append(i18n("Playlist has a different framerate (%1/%2fps), not recommended.", xmlProfile->frame_rate_num(), xmlProfile->frame_rate_den())); QString loader = resource; loader.prepend(QStringLiteral("consumer:")); pCore->getCurrentProfile()->set_explicit(1); return std::make_shared(pCore->getCurrentProfile()->profile(), loader.toUtf8().constData()); } pCore->getCurrentProfile()->set_explicit(1); return std::make_shared(pCore->getCurrentProfile()->profile(), "xml", resource.toUtf8().constData()); } void LoadJob::checkProfile(const QString &clipId, const QDomElement &xml, const std::shared_ptr &producer) { // Check if clip profile matches QString service = producer->get("mlt_service"); // Check for image producer if (service == QLatin1String("qimage") || service == QLatin1String("pixbuf")) { // This is an image, create profile from image size int width = producer->get_int("meta.media.width"); if (width % 8 > 0) { width += 8 - width % 8; } int height = producer->get_int("meta.media.height"); height += height % 2; if (width > 100 && height > 100) { std::unique_ptr projectProfile(new ProfileParam(pCore->getCurrentProfile().get())); projectProfile->m_width = width; projectProfile->m_height = height; projectProfile->m_sample_aspect_num = 1; projectProfile->m_sample_aspect_den = 1; projectProfile->m_display_aspect_num = width; projectProfile->m_display_aspect_den = height; projectProfile->m_description.clear(); pCore->currentDoc()->switchProfile(projectProfile, clipId, xml); } else { // Very small image, we probably don't want to use this as profile } } else if (service.contains(QStringLiteral("avformat"))) { std::unique_ptr blankProfile(new Mlt::Profile()); blankProfile->set_explicit(0); blankProfile->from_producer(*producer); std::unique_ptr clipProfile(new ProfileParam(blankProfile.get())); std::unique_ptr projectProfile(new ProfileParam(pCore->getCurrentProfile().get())); clipProfile->adjustDimensions(); if (*clipProfile.get() == *projectProfile.get()) { if (KdenliveSettings::default_profile().isEmpty()) { // Confirm default project format KdenliveSettings::setDefault_profile(pCore->getCurrentProfile()->path()); } } else { // Profiles do not match, propose profile adjustment pCore->currentDoc()->switchProfile(clipProfile, clipId, xml); } } } void LoadJob::processSlideShow() { int ttl = Xml::getXmlProperty(m_xml, QStringLiteral("ttl")).toInt(); QString anim = Xml::getXmlProperty(m_xml, QStringLiteral("animation")); if (!anim.isEmpty()) { auto *filter = new Mlt::Filter(pCore->getCurrentProfile()->profile(), "affine"); if ((filter != nullptr) && filter->is_valid()) { int cycle = ttl; QString geometry = SlideshowClip::animationToGeometry(anim, cycle); if (!geometry.isEmpty()) { if (anim.contains(QStringLiteral("low-pass"))) { auto *blur = new Mlt::Filter(pCore->getCurrentProfile()->profile(), "boxblur"); if ((blur != nullptr) && blur->is_valid()) { m_producer->attach(*blur); } } filter->set("transition.geometry", geometry.toUtf8().data()); filter->set("transition.cycle", cycle); m_producer->attach(*filter); } } } QString fade = Xml::getXmlProperty(m_xml, QStringLiteral("fade")); if (fade == QLatin1String("1")) { // user wants a fade effect to slideshow auto *filter = new Mlt::Filter(pCore->getCurrentProfile()->profile(), "luma"); if ((filter != nullptr) && filter->is_valid()) { if (ttl != 0) { filter->set("cycle", ttl); } QString luma_duration = Xml::getXmlProperty(m_xml, QStringLiteral("luma_duration")); QString luma_file = Xml::getXmlProperty(m_xml, QStringLiteral("luma_file")); if (!luma_duration.isEmpty()) { filter->set("duration", luma_duration.toInt()); } if (!luma_file.isEmpty()) { filter->set("luma.resource", luma_file.toUtf8().constData()); QString softness = Xml::getXmlProperty(m_xml, QStringLiteral("softness")); if (!softness.isEmpty()) { int soft = softness.toInt(); filter->set("luma.softness", (double)soft / 100.0); } } m_producer->attach(*filter); } } QString crop = Xml::getXmlProperty(m_xml, QStringLiteral("crop")); if (crop == QLatin1String("1")) { // user wants to center crop the slides auto *filter = new Mlt::Filter(pCore->getCurrentProfile()->profile(), "crop"); if ((filter != nullptr) && filter->is_valid()) { filter->set("center", 1); m_producer->attach(*filter); } } } bool LoadJob::startJob() { if (m_done) { return true; } pCore->getMonitor(Kdenlive::ClipMonitor)->resetPlayOrLoopZone(m_clipId); m_resource = Xml::getXmlProperty(m_xml, QStringLiteral("resource")); + int duration = 0; ClipType::ProducerType type = static_cast(m_xml.attribute(QStringLiteral("type")).toInt()); QString service = Xml::getXmlProperty(m_xml, QStringLiteral("mlt_service")); if (type == ClipType::Unknown) { type = getTypeForService(service, m_resource); } switch (type) { case ClipType::Color: m_producer = loadResource(m_resource, QStringLiteral("color:")); break; case ClipType::Text: - case ClipType::TextTemplate: - m_producer = loadResource(m_resource, QStringLiteral("kdenlivetitle:")); + case ClipType::TextTemplate: { + QFile txtfile(m_resource); + QDomDocument txtdoc(QStringLiteral("titledocument")); + if (txtfile.open(QIODevice::ReadOnly) && txtdoc.setContent(&txtfile)) { + txtfile.close(); + if (txtdoc.documentElement().hasAttribute(QStringLiteral("duration"))) { + duration = txtdoc.documentElement().attribute(QStringLiteral("duration")).toInt(); + } else if (txtdoc.documentElement().hasAttribute(QStringLiteral("out"))) { + duration = txtdoc.documentElement().attribute(QStringLiteral("out")).toInt(); + } + } + m_producer = loadResource(m_resource, QStringLiteral("kdenlivetitle:")); + if (duration <= 0) { + duration = pCore->currentDoc()->getFramePos(KdenliveSettings::title_duration()) - 1; + } + m_producer->set("length", duration); + m_producer->set("kdenlive:duration", duration); + m_producer->set("out", duration - 1); + } break; case ClipType::QText: m_producer = loadResource(m_resource, QStringLiteral("qtext:")); break; case ClipType::Playlist: { m_producer = loadPlaylist(m_resource); if (!m_errorMessage.isEmpty()) { QMetaObject::invokeMethod(pCore.get(), "displayBinMessage", Qt::QueuedConnection, Q_ARG(const QString &, m_errorMessage), Q_ARG(int, (int)KMessageWidget::Warning)); } if (m_resource.endsWith(QLatin1String(".kdenlive"))) { QFile f(m_resource); QDomDocument doc; doc.setContent(&f, false); f.close(); QDomElement pl = doc.documentElement().firstChildElement(QStringLiteral("playlist")); if (!pl.isNull()) { QString offsetData = Xml::getXmlProperty(pl, QStringLiteral("kdenlive:docproperties.seekOffset")); if (offsetData.isEmpty() && Xml::getXmlProperty(pl, QStringLiteral("kdenlive:docproperties.version")) == QLatin1String("0.98")) { offsetData = QStringLiteral("30000"); } if (!offsetData.isEmpty()) { bool ok = false; int offset = offsetData.toInt(&ok); if (ok) { qDebug()<<" / / / FIXING OFFSET DATA: "<get_playtime() - offset - 1; m_producer->set("out", offset - 1); m_producer->set("length", offset); m_producer->set("kdenlive:duration", offset); } } else { qDebug()<<"// NO OFFSET DAT FOUND\n\n"; } } else { qDebug()<<":_______\n______(pCore->getCurrentProfile()->profile(), nullptr, m_resource.toUtf8().constData()); } break; } if (!m_producer || m_producer->is_blank() || !m_producer->is_valid()) { qCDebug(KDENLIVE_LOG) << " / / / / / / / / ERROR / / / / // CANNOT LOAD PRODUCER: " << m_resource; m_done = true; m_successful = false; if (m_producer) { m_producer.reset(); } QMetaObject::invokeMethod(pCore.get(), "displayBinMessage", Qt::QueuedConnection, Q_ARG(const QString &, i18n("Cannot open file %1", m_resource)), Q_ARG(int, (int)KMessageWidget::Warning)); m_errorMessage.append(i18n("ERROR: Could not load clip %1: producer is invalid", m_resource)); return false; } processProducerProperties(m_producer, m_xml); QString clipName = Xml::getXmlProperty(m_xml, QStringLiteral("kdenlive:clipname")); if (clipName.isEmpty()) { clipName = QFileInfo(Xml::getXmlProperty(m_xml, QStringLiteral("kdenlive:originalurl"))).fileName(); } m_producer->set("kdenlive:clipname", clipName.toUtf8().constData()); QString groupId = Xml::getXmlProperty(m_xml, QStringLiteral("kdenlive:folderid")); if (!groupId.isEmpty()) { m_producer->set("kdenlive:folderid", groupId.toUtf8().constData()); } - int clipOut = 0, duration = 0; + int clipOut = 0; if (m_xml.hasAttribute(QStringLiteral("out"))) { clipOut = m_xml.attribute(QStringLiteral("out")).toInt(); } // setup length here as otherwise default length (currently 15000 frames in MLT) will be taken even if outpoint is larger - if (type == ClipType::Color || type == ClipType::Text || type == ClipType::TextTemplate || type == ClipType::QText || type == ClipType::Image || - type == ClipType::SlideShow) { + if (duration == 0 && (type == ClipType::Color || type == ClipType::Text || type == ClipType::TextTemplate || type == ClipType::QText || type == ClipType::Image || + type == ClipType::SlideShow)) { int length; if (m_xml.hasAttribute(QStringLiteral("length"))) { length = m_xml.attribute(QStringLiteral("length")).toInt(); clipOut = qMax(1, length - 1); } else { length = Xml::getXmlProperty(m_xml, QStringLiteral("length")).toInt(); clipOut -= m_xml.attribute(QStringLiteral("in")).toInt(); if (length < clipOut) { length = clipOut == 1 ? 1 : clipOut + 1; } } // Pass duration if it was forced if (m_xml.hasAttribute(QStringLiteral("duration"))) { duration = m_xml.attribute(QStringLiteral("duration")).toInt(); if (length < duration) { length = duration; if (clipOut > 0) { clipOut = length - 1; } } } if (duration == 0) { duration = length; } m_producer->set("length", m_producer->frames_to_time(length, mlt_time_clock)); int kdenlive_duration = m_producer->time_to_frames(Xml::getXmlProperty(m_xml, QStringLiteral("kdenlive:duration")).toUtf8().constData()); if (kdenlive_duration > 0) { m_producer->set("kdenlive:duration", m_producer->frames_to_time(kdenlive_duration, mlt_time_clock)); } else { m_producer->set("kdenlive:duration", m_producer->get("length")); } } if (clipOut > 0) { m_producer->set_in_and_out(m_xml.attribute(QStringLiteral("in")).toInt(), clipOut); } if (m_xml.hasAttribute(QStringLiteral("templatetext"))) { m_producer->set("templatetext", m_xml.attribute(QStringLiteral("templatetext")).toUtf8().constData()); } - duration = duration > 0 ? duration : m_producer->get_playtime(); if (type == ClipType::SlideShow) { processSlideShow(); } int vindex = -1; double fps = -1; const QString mltService = m_producer->get("mlt_service"); if (mltService == QLatin1String("xml") || mltService == QLatin1String("consumer")) { // MLT playlist, create producer with blank profile to get real profile info QString tmpPath = m_resource; if (tmpPath.startsWith(QLatin1String("consumer:"))) { tmpPath = "xml:" + tmpPath.section(QLatin1Char(':'), 1); } Mlt::Profile original_profile; std::unique_ptr tmpProd(new Mlt::Producer(original_profile, nullptr, tmpPath.toUtf8().constData())); original_profile.set_explicit(1); double originalFps = original_profile.fps(); fps = originalFps; if (originalFps > 0 && !qFuzzyCompare(originalFps, pCore->getCurrentFps())) { int originalLength = tmpProd->get_length(); int fixedLength = (int)(originalLength * pCore->getCurrentFps() / originalFps); m_producer->set("length", fixedLength); m_producer->set("out", fixedLength - 1); } } else if (mltService == QLatin1String("avformat")) { // check if there are multiple streams vindex = m_producer->get_int("video_index"); // List streams int streams = m_producer->get_int("meta.media.nb_streams"); m_audio_list.clear(); m_video_list.clear(); for (int i = 0; i < streams; ++i) { QByteArray propertyName = QStringLiteral("meta.media.%1.stream.type").arg(i).toLocal8Bit(); QString stype = m_producer->get(propertyName.data()); if (stype == QLatin1String("audio")) { m_audio_list.append(i); } else if (stype == QLatin1String("video")) { m_video_list.append(i); } } if (vindex > -1) { char property[200]; snprintf(property, sizeof(property), "meta.media.%d.stream.frame_rate", vindex); fps = m_producer->get_double(property); } if (fps <= 0) { if (m_producer->get_double("meta.media.frame_rate_den") > 0) { fps = m_producer->get_double("meta.media.frame_rate_num") / m_producer->get_double("meta.media.frame_rate_den"); } else { fps = m_producer->get_double("source_fps"); } } } if (fps <= 0 && type == ClipType::Unknown) { // something wrong, maybe audio file with embedded image QMimeDatabase db; QString mime = db.mimeTypeForFile(m_resource).name(); if (mime.startsWith(QLatin1String("audio"))) { m_producer->set("video_index", -1); vindex = -1; } } m_done = m_successful = true; return true; } void LoadJob::processMultiStream() { auto m_binClip = pCore->projectItemModel()->getClipByBinID(m_clipId); // We retrieve the folder containing our clip, because we will set the other streams in the same auto parent = pCore->projectItemModel()->getRootFolder()->clipId(); if (auto ptr = m_binClip->parentItem().lock()) { parent = std::static_pointer_cast(ptr)->clipId(); } else { qDebug() << "Warning, something went wrong while accessing parent of bin clip"; } // This helper lambda request addition of a given stream auto addStream = [this, parentId = std::move(parent)](int vindex, int aindex, Fun &undo, Fun &redo) { auto clone = ProjectClip::cloneProducer(m_producer); clone->set("video_index", vindex); clone->set("audio_index", aindex); QString id; pCore->projectItemModel()->requestAddBinClip(id, clone, parentId, undo, redo); }; Fun undo = []() { return true; }; Fun redo = []() { return true; }; if (KdenliveSettings::automultistreams()) { for (int i = 1; i < m_video_list.count(); ++i) { int vindex = m_video_list.at(i); int aindex = 0; if (i <= m_audio_list.count() - 1) { aindex = m_audio_list.at(i); } addStream(vindex, aindex, undo, redo); } return; } int width = 60.0 * pCore->getCurrentDar(); if (width % 2 == 1) { width++; } QScopedPointer dialog(new QDialog(qApp->activeWindow())); dialog->setWindowTitle(QStringLiteral("Multi Stream Clip")); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); QWidget *mainWidget = new QWidget(dialog.data()); auto *mainLayout = new QVBoxLayout; dialog->setLayout(mainLayout); mainLayout->addWidget(mainWidget); QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); dialog->connect(buttonBox, &QDialogButtonBox::accepted, dialog.data(), &QDialog::accept); dialog->connect(buttonBox, &QDialogButtonBox::rejected, dialog.data(), &QDialog::reject); okButton->setText(i18n("Import selected clips")); QLabel *lab1 = new QLabel(i18n("Additional streams for clip\n %1", m_resource), mainWidget); mainLayout->addWidget(lab1); QList groupList; QList comboList; // We start loading the list at 1, video index 0 should already be loaded for (int j = 1; j < m_video_list.count(); ++j) { m_producer->set("video_index", m_video_list.at(j)); // TODO this keyframe should be cached QImage thumb = KThumb::getFrame(m_producer.get(), 0, width, 60); QGroupBox *streamFrame = new QGroupBox(i18n("Video stream %1", m_video_list.at(j)), mainWidget); mainLayout->addWidget(streamFrame); streamFrame->setProperty("vindex", m_video_list.at(j)); groupList << streamFrame; streamFrame->setCheckable(true); streamFrame->setChecked(true); auto *vh = new QVBoxLayout(streamFrame); QLabel *iconLabel = new QLabel(mainWidget); mainLayout->addWidget(iconLabel); iconLabel->setPixmap(QPixmap::fromImage(thumb)); vh->addWidget(iconLabel); if (m_audio_list.count() > 1) { auto *cb = new QComboBox(mainWidget); mainLayout->addWidget(cb); for (int k = 0; k < m_audio_list.count(); ++k) { cb->addItem(i18n("Audio stream %1", m_audio_list.at(k)), m_audio_list.at(k)); } comboList << cb; cb->setCurrentIndex(qMin(j, m_audio_list.count() - 1)); vh->addWidget(cb); } mainLayout->addWidget(streamFrame); } m_producer->set("video_index", m_video_list.at(0)); mainLayout->addWidget(buttonBox); if (dialog->exec() == QDialog::Accepted) { // import selected streams for (int i = 0; i < groupList.count(); ++i) { if (groupList.at(i)->isChecked()) { int vindex = groupList.at(i)->property("vindex").toInt(); int ax = qMin(i, comboList.size() - 1); int aindex = -1; if (ax >= 0) { // only check audio index if we have several audio streams aindex = comboList.at(ax)->itemData(comboList.at(ax)->currentIndex()).toInt(); } addStream(vindex, aindex, undo, redo); } } pCore->pushUndo(undo, redo, i18n("Add additional streams for clip")); } } bool LoadJob::commitResult(Fun &undo, Fun &redo) { qDebug() << "################### loadjob COMMIT"; Q_ASSERT(!m_resultConsumed); if (!m_done) { qDebug() << "ERROR: Trying to consume invalid results"; return false; } m_resultConsumed = true; auto m_binClip = pCore->projectItemModel()->getClipByBinID(m_clipId); if (!m_successful) { // TODO: Deleting cannot happen at this stage or we endup in a mutex lock pCore->projectItemModel()->requestBinClipDeletion(m_binClip, undo, redo); return false; } if (m_xml.hasAttribute(QStringLiteral("_checkProfile")) && m_producer->get_int("video_index") > -1) { checkProfile(m_clipId, m_xml, m_producer); } if (m_video_list.size() > 1) { processMultiStream(); } // note that the image is moved into lambda, it won't be available from this class anymore auto operation = [clip = m_binClip, prod = std::move(m_producer)]() { clip->setProducer(prod, true); return true; }; auto reverse = []() { // This is probably not invertible, leave as is. return true; }; bool ok = operation(); if (ok) { m_readyCallBack(); if (pCore->projectItemModel()->clipsCount() == 1) { // Always select first added clip pCore->selectBinClip(m_clipId); } UPDATE_UNDO_REDO_NOLOCK(operation, reverse, undo, redo); } return ok; } diff --git a/src/titler/titledocument.cpp b/src/titler/titledocument.cpp index d7266045f..3413a4fb2 100644 --- a/src/titler/titledocument.cpp +++ b/src/titler/titledocument.cpp @@ -1,742 +1,744 @@ /*************************************************************************** titledocument.h - description ------------------- begin : Feb 28 2008 copyright : (C) 2008 by Marco Gittler email : g.marco@freenet.de ***************************************************************************/ /*************************************************************************** * * * 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. * * * ***************************************************************************/ #include "titledocument.h" #include "gradientwidget.h" #include "graphicsscenerectmove.h" #include "kdenlivesettings.h" #include "timecode.h" #include #include #include #include "kdenlive_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef Q_OS_MAC #include #endif #include #include #include #include QByteArray fileToByteArray(const QString &filename) { QByteArray ret; QFile file(filename); if (file.open(QIODevice::ReadOnly)) { while (!file.atEnd()) { ret.append(file.readLine()); } } return ret; } TitleDocument::TitleDocument() { m_scene = nullptr; m_width = 0; m_height = 0; m_missingElements = 0; } void TitleDocument::setScene(QGraphicsScene *_scene, int width, int height) { m_scene = _scene; m_width = width; m_height = height; } int TitleDocument::base64ToUrl(QGraphicsItem *item, QDomElement &content, bool embed) { if (embed) { if (!item->data(Qt::UserRole + 1).toString().isEmpty()) { content.setAttribute(QStringLiteral("base64"), item->data(Qt::UserRole + 1).toString()); } else if (!item->data(Qt::UserRole).toString().isEmpty()) { content.setAttribute(QStringLiteral("base64"), fileToByteArray(item->data(Qt::UserRole).toString()).toBase64().data()); } content.removeAttribute(QStringLiteral("url")); } else { // save for project files to disk QString base64 = item->data(Qt::UserRole + 1).toString(); if (!base64.isEmpty()) { QString titlePath; if (!m_projectPath.isEmpty()) { titlePath = m_projectPath; } else { titlePath = QStringLiteral("/tmp/titles"); } QString filename = extractBase64Image(titlePath, base64); if (!filename.isEmpty()) { content.setAttribute(QStringLiteral("url"), filename); content.removeAttribute(QStringLiteral("base64")); } } else { return 1; } } return 0; } // static const QString TitleDocument::extractBase64Image(const QString &titlePath, const QString &data) { QString filename = titlePath + QString(QCryptographicHash::hash(data.toLatin1(), QCryptographicHash::Md5).toHex().append(".titlepart")); QDir dir; dir.mkpath(titlePath); QFile f(filename); if (f.open(QIODevice::WriteOnly)) { f.write(QByteArray::fromBase64(data.toLatin1())); f.close(); return filename; } return QString(); } QDomDocument TitleDocument::xml(QGraphicsRectItem *startv, QGraphicsRectItem *endv, bool embed) { QDomDocument doc; QDomElement main = doc.createElement(QStringLiteral("kdenlivetitle")); main.setAttribute(QStringLiteral("width"), m_width); main.setAttribute(QStringLiteral("height"), m_height); // Save locale #ifndef Q_OS_MAC const char *locale = setlocale(LC_NUMERIC, nullptr); #else const char *locale = setlocale(LC_NUMERIC_MASK, nullptr); #endif main.setAttribute(QStringLiteral("LC_NUMERIC"), locale); doc.appendChild(main); QTextCursor cur; QTextBlockFormat format; for (QGraphicsItem *item : m_scene->items()) { if (!(item->flags() & QGraphicsItem::ItemIsSelectable)) { continue; } QDomElement e = doc.createElement(QStringLiteral("item")); QDomElement content = doc.createElement(QStringLiteral("content")); QFont font; QString gradient; MyTextItem *t; double xPosition = item->pos().x(); switch (item->type()) { case 7: e.setAttribute(QStringLiteral("type"), QStringLiteral("QGraphicsPixmapItem")); content.setAttribute(QStringLiteral("url"), item->data(Qt::UserRole).toString()); base64ToUrl(item, content, embed); break; case 13: e.setAttribute(QStringLiteral("type"), QStringLiteral("QGraphicsSvgItem")); content.setAttribute(QStringLiteral("url"), item->data(Qt::UserRole).toString()); base64ToUrl(item, content, embed); break; case 3: e.setAttribute(QStringLiteral("type"), QStringLiteral("QGraphicsRectItem")); content.setAttribute(QStringLiteral("rect"), rectFToString(static_cast(item)->rect().normalized())); content.setAttribute(QStringLiteral("pencolor"), colorToString(static_cast(item)->pen().color())); if (static_cast(item)->pen() == Qt::NoPen) { content.setAttribute(QStringLiteral("penwidth"), 0); } else { content.setAttribute(QStringLiteral("penwidth"), static_cast(item)->pen().width()); } content.setAttribute(QStringLiteral("brushcolor"), colorToString(static_cast(item)->brush().color())); gradient = item->data(TitleDocument::Gradient).toString(); if (!gradient.isEmpty()) { content.setAttribute(QStringLiteral("gradient"), gradient); } break; case 8: e.setAttribute(QStringLiteral("type"), QStringLiteral("QGraphicsTextItem")); t = static_cast(item); // Don't save empty text nodes if (t->toPlainText().simplified().isEmpty()) { continue; } // content.appendChild(doc.createTextNode(((QGraphicsTextItem*)item)->toHtml())); content.appendChild(doc.createTextNode(t->toPlainText())); font = t->font(); content.setAttribute(QStringLiteral("font"), font.family()); content.setAttribute(QStringLiteral("font-weight"), font.weight()); content.setAttribute(QStringLiteral("font-pixel-size"), font.pixelSize()); content.setAttribute(QStringLiteral("font-italic"), static_cast(font.italic())); content.setAttribute(QStringLiteral("font-underline"), static_cast(font.underline())); content.setAttribute(QStringLiteral("letter-spacing"), QString::number(font.letterSpacing())); gradient = item->data(TitleDocument::Gradient).toString(); if (!gradient.isEmpty()) { content.setAttribute(QStringLiteral("gradient"), gradient); } cur = QTextCursor(t->document()); cur.select(QTextCursor::Document); format = cur.blockFormat(); if (t->toPlainText() == QLatin1String("%s")) { // template text box, adjust size for later remplacement text if (t->alignment() == Qt::AlignHCenter) { // grow dimensions on both sides double xcenter = item->pos().x() + (t->baseBoundingRect().width()) / 2; double offset = qMin(xcenter, m_width - xcenter); xPosition = xcenter - offset; content.setAttribute(QStringLiteral("box-width"), QString::number(2 * offset)); } else if (t->alignment() == Qt::AlignRight) { // grow to the left double offset = item->pos().x() + (t->baseBoundingRect().width()); xPosition = 0; content.setAttribute(QStringLiteral("box-width"), QString::number(offset)); } else { // left align, grow on right side double offset = m_width - item->pos().x(); content.setAttribute(QStringLiteral("box-width"), QString::number(offset)); } } else { content.setAttribute(QStringLiteral("box-width"), QString::number(t->baseBoundingRect().width())); } content.setAttribute(QStringLiteral("box-height"), QString::number(t->baseBoundingRect().height())); if (!t->data(TitleDocument::LineSpacing).isNull()) { content.setAttribute(QStringLiteral("line-spacing"), QString::number(t->data(TitleDocument::LineSpacing).toInt())); } { QTextCursor cursor(t->document()); cursor.select(QTextCursor::Document); QColor fontcolor = cursor.charFormat().foreground().color(); content.setAttribute(QStringLiteral("font-color"), colorToString(fontcolor)); if (!t->data(TitleDocument::OutlineWidth).isNull()) { content.setAttribute(QStringLiteral("font-outline"), QString::number(t->data(TitleDocument::OutlineWidth).toDouble())); } if (!t->data(TitleDocument::OutlineColor).isNull()) { QVariant variant = t->data(TitleDocument::OutlineColor); QColor outlineColor = variant.value(); content.setAttribute(QStringLiteral("font-outline-color"), colorToString(outlineColor)); } } if (!t->data(100).isNull()) { QStringList effectParams = t->data(100).toStringList(); QString effectName = effectParams.takeFirst(); content.setAttribute(QStringLiteral("textwidth"), QString::number(t->sceneBoundingRect().width())); content.setAttribute(effectName, effectParams.join(QLatin1Char(';'))); } // Only save when necessary. if (t->data(OriginXLeft).toInt() == AxisInverted) { content.setAttribute(QStringLiteral("kdenlive-axis-x-inverted"), t->data(OriginXLeft).toInt()); } if (t->data(OriginYTop).toInt() == AxisInverted) { content.setAttribute(QStringLiteral("kdenlive-axis-y-inverted"), t->data(OriginYTop).toInt()); } if (t->textWidth() > 0) { content.setAttribute(QStringLiteral("alignment"), (int)t->alignment()); } content.setAttribute(QStringLiteral("shadow"), t->shadowInfo().join(QLatin1Char(';'))); break; default: continue; } // position QDomElement pos = doc.createElement(QStringLiteral("position")); pos.setAttribute(QStringLiteral("x"), QString::number(xPosition)); pos.setAttribute(QStringLiteral("y"), QString::number(item->pos().y())); QTransform transform = item->transform(); QDomElement tr = doc.createElement(QStringLiteral("transform")); if (!item->data(TitleDocument::ZoomFactor).isNull()) { tr.setAttribute(QStringLiteral("zoom"), QString::number(item->data(TitleDocument::ZoomFactor).toInt())); } if (!item->data(TitleDocument::RotateFactor).isNull()) { QList rotlist = item->data(TitleDocument::RotateFactor).toList(); tr.setAttribute(QStringLiteral("rotation"), QStringLiteral("%1,%2,%3").arg(rotlist[0].toDouble()).arg(rotlist[1].toDouble()).arg(rotlist[2].toDouble())); } tr.appendChild(doc.createTextNode(QStringLiteral("%1,%2,%3,%4,%5,%6,%7,%8,%9") .arg(transform.m11()) .arg(transform.m12()) .arg(transform.m13()) .arg(transform.m21()) .arg(transform.m22()) .arg(transform.m23()) .arg(transform.m31()) .arg(transform.m32()) .arg(transform.m33()))); e.setAttribute(QStringLiteral("z-index"), item->zValue()); pos.appendChild(tr); e.appendChild(pos); e.appendChild(content); if (item->zValue() > -1000) { main.appendChild(e); } } if ((startv != nullptr) && (endv != nullptr)) { QDomElement endport = doc.createElement(QStringLiteral("endviewport")); QDomElement startport = doc.createElement(QStringLiteral("startviewport")); QRectF r(endv->pos().x(), endv->pos().y(), endv->rect().width(), endv->rect().height()); endport.setAttribute(QStringLiteral("rect"), rectFToString(r)); QRectF r2(startv->pos().x(), startv->pos().y(), startv->rect().width(), startv->rect().height()); startport.setAttribute(QStringLiteral("rect"), rectFToString(r2)); main.appendChild(startport); main.appendChild(endport); } QDomElement backgr = doc.createElement(QStringLiteral("background")); QColor color = getBackgroundColor(); backgr.setAttribute(QStringLiteral("color"), colorToString(color)); main.appendChild(backgr); return doc; } /** \brief Get the background color (incl. alpha) from the document, if possibly * \returns The background color of the document, inclusive alpha. If none found, returns (0,0,0,0) */ QColor TitleDocument::getBackgroundColor() const { QColor color(0, 0, 0, 0); if (m_scene) { QList items = m_scene->items(); for (auto item : items) { if ((int)item->zValue() == -1100) { color = static_cast(item)->brush().color(); return color; } } } return color; } bool TitleDocument::saveDocument(const QUrl &url, QGraphicsRectItem *startv, QGraphicsRectItem *endv, int duration, bool embed) { if (!m_scene) { return false; } QDomDocument doc = xml(startv, endv, embed); doc.documentElement().setAttribute(QStringLiteral("duration"), duration); // keep some time for backwards compatibility (opening projects with older versions) - 26/12/12 doc.documentElement().setAttribute(QStringLiteral("out"), duration); QTemporaryFile tmpfile; if (!tmpfile.open()) { qCWarning(KDENLIVE_LOG) << "///// CANNOT CREATE TMP FILE in: " << tmpfile.fileName(); return false; } QFile xmlf(tmpfile.fileName()); if (!xmlf.open(QIODevice::WriteOnly)) { return false; } xmlf.write(doc.toString().toUtf8()); if (xmlf.error() != QFile::NoError) { xmlf.close(); return false; } xmlf.close(); KIO::FileCopyJob *copyjob = KIO::file_copy(QUrl::fromLocalFile(tmpfile.fileName()), url, -1, KIO::Overwrite); - return copyjob->exec(); + bool result = copyjob->exec(); + delete copyjob; + return result; } int TitleDocument::loadFromXml(const QDomDocument &doc, QGraphicsRectItem *startv, QGraphicsRectItem *endv, int *duration, const QString &projectpath) { m_projectPath = projectpath; m_missingElements = 0; QDomNodeList titles = doc.elementsByTagName(QStringLiteral("kdenlivetitle")); // TODO: Check if the opened title size is equal to project size, otherwise warn user and rescale if (doc.documentElement().hasAttribute(QStringLiteral("width")) && doc.documentElement().hasAttribute(QStringLiteral("height"))) { int doc_width = doc.documentElement().attribute(QStringLiteral("width")).toInt(); int doc_height = doc.documentElement().attribute(QStringLiteral("height")).toInt(); if (doc_width != m_width || doc_height != m_height) { KMessageBox::information(QApplication::activeWindow(), i18n("This title clip was created with a different frame size."), i18n("Title Profile")); // TODO: convert using QTransform m_width = doc_width; m_height = doc_height; } } else { // Document has no size info, it is likely an old version title, so ignore viewport data QDomNodeList viewportlist = doc.documentElement().elementsByTagName(QStringLiteral("startviewport")); if (!viewportlist.isEmpty()) { doc.documentElement().removeChild(viewportlist.at(0)); } viewportlist = doc.documentElement().elementsByTagName(QStringLiteral("endviewport")); if (!viewportlist.isEmpty()) { doc.documentElement().removeChild(viewportlist.at(0)); } } if (doc.documentElement().hasAttribute(QStringLiteral("duration"))) { *duration = doc.documentElement().attribute(QStringLiteral("duration")).toInt(); } else if (doc.documentElement().hasAttribute(QStringLiteral("out"))) { *duration = doc.documentElement().attribute(QStringLiteral("out")).toInt(); } else { *duration = Timecode().getFrameCount(KdenliveSettings::title_duration()); } int maxZValue = 0; if (!titles.isEmpty()) { QDomNodeList items = titles.item(0).childNodes(); for (int i = 0; i < items.count(); ++i) { QGraphicsItem *gitem = nullptr; QDomNode itemNode = items.item(i); // qCDebug(KDENLIVE_LOG) << items.item(i).attributes().namedItem("type").nodeValue(); int zValue = itemNode.attributes().namedItem(QStringLiteral("z-index")).nodeValue().toInt(); double xPosition = itemNode.namedItem(QStringLiteral("position")).attributes().namedItem(QStringLiteral("x")).nodeValue().toDouble(); if (zValue > -1000) { if (itemNode.attributes().namedItem(QStringLiteral("type")).nodeValue() == QLatin1String("QGraphicsTextItem")) { QDomNamedNodeMap txtProperties = itemNode.namedItem(QStringLiteral("content")).attributes(); QFont font(txtProperties.namedItem(QStringLiteral("font")).nodeValue()); QDomNode node = txtProperties.namedItem(QStringLiteral("font-bold")); if (!node.isNull()) { // Old: Bold/Not bold. font.setBold(node.nodeValue().toInt() != 0); } else { // New: Font weight (QFont::) font.setWeight(txtProperties.namedItem(QStringLiteral("font-weight")).nodeValue().toInt()); } // font.setBold(txtProperties.namedItem("font-bold").nodeValue().toInt()); font.setItalic(txtProperties.namedItem(QStringLiteral("font-italic")).nodeValue().toInt() != 0); font.setUnderline(txtProperties.namedItem(QStringLiteral("font-underline")).nodeValue().toInt() != 0); // Older Kdenlive version did not store pixel size but point size if (txtProperties.namedItem(QStringLiteral("font-pixel-size")).isNull()) { KMessageBox::information(QApplication::activeWindow(), i18n("Some of your text clips were saved with size in points, which means " "different sizes on different displays. They will be converted to pixel " "size, making them portable, but you could have to adjust their size."), i18n("Text Clips Updated")); QFont f2; f2.setPointSize(txtProperties.namedItem(QStringLiteral("font-size")).nodeValue().toInt()); font.setPixelSize(QFontInfo(f2).pixelSize()); } else { font.setPixelSize(txtProperties.namedItem(QStringLiteral("font-pixel-size")).nodeValue().toInt()); } font.setLetterSpacing(QFont::AbsoluteSpacing, txtProperties.namedItem(QStringLiteral("letter-spacing")).nodeValue().toInt()); QColor col(stringToColor(txtProperties.namedItem(QStringLiteral("font-color")).nodeValue())); MyTextItem *txt = new MyTextItem(itemNode.namedItem(QStringLiteral("content")).firstChild().nodeValue(), nullptr); m_scene->addItem(txt); txt->setFont(font); txt->setTextInteractionFlags(Qt::NoTextInteraction); QTextCursor cursor(txt->document()); cursor.select(QTextCursor::Document); QTextCharFormat cformat = cursor.charFormat(); if (txtProperties.namedItem(QStringLiteral("font-outline")).nodeValue().toDouble() > 0.0) { txt->setData(TitleDocument::OutlineWidth, txtProperties.namedItem(QStringLiteral("font-outline")).nodeValue().toDouble()); txt->setData(TitleDocument::OutlineColor, stringToColor(txtProperties.namedItem(QStringLiteral("font-outline-color")).nodeValue())); cformat.setTextOutline(QPen(QColor(stringToColor(txtProperties.namedItem(QStringLiteral("font-outline-color")).nodeValue())), txtProperties.namedItem(QStringLiteral("font-outline")).nodeValue().toDouble(), Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin)); } if (!txtProperties.namedItem(QStringLiteral("line-spacing")).isNull()) { int lineSpacing = txtProperties.namedItem(QStringLiteral("line-spacing")).nodeValue().toInt(); QTextBlockFormat format = cursor.blockFormat(); format.setLineHeight(lineSpacing, QTextBlockFormat::LineDistanceHeight); cursor.setBlockFormat(format); txt->setData(TitleDocument::LineSpacing, lineSpacing); } txt->setTextColor(col); cformat.setForeground(QBrush(col)); cursor.setCharFormat(cformat); if (!txtProperties.namedItem(QStringLiteral("gradient")).isNull()) { // Gradient color QString data = txtProperties.namedItem(QStringLiteral("gradient")).nodeValue(); txt->setData(TitleDocument::Gradient, data); QLinearGradient gr = GradientWidget::gradientFromString(data, txt->boundingRect().width(), txt->boundingRect().height()); cformat.setForeground(QBrush(gr)); cursor.setCharFormat(cformat); } if (!txtProperties.namedItem(QStringLiteral("alignment")).isNull()) { txt->setAlignment((Qt::Alignment)txtProperties.namedItem(QStringLiteral("alignment")).nodeValue().toInt()); } if (!txtProperties.namedItem(QStringLiteral("kdenlive-axis-x-inverted")).isNull()) { txt->setData(OriginXLeft, txtProperties.namedItem(QStringLiteral("kdenlive-axis-x-inverted")).nodeValue().toInt()); } if (!txtProperties.namedItem(QStringLiteral("kdenlive-axis-y-inverted")).isNull()) { txt->setData(OriginYTop, txtProperties.namedItem(QStringLiteral("kdenlive-axis-y-inverted")).nodeValue().toInt()); } if (!txtProperties.namedItem(QStringLiteral("shadow")).isNull()) { QString info = txtProperties.namedItem(QStringLiteral("shadow")).nodeValue(); txt->loadShadow(info.split(QLatin1Char(';'))); } // Effects if (!txtProperties.namedItem(QStringLiteral("typewriter")).isNull()) { QStringList effData = QStringList() << QStringLiteral("typewriter") << txtProperties.namedItem(QStringLiteral("typewriter")).nodeValue(); txt->setData(100, effData); } if (txt->toPlainText() == QLatin1String("%s")) { // template text box, adjust size for later remplacement text if (txt->alignment() == Qt::AlignHCenter) { // grow dimensions on both sides double width = txtProperties.namedItem(QStringLiteral("box-width")).nodeValue().toDouble(); double xcenter = (width - xPosition) / 2.0; xPosition = xcenter - txt->boundingRect().width() / 2; } else if (txt->alignment() == Qt::AlignRight) { // grow to the left xPosition = xPosition + txtProperties.namedItem(QStringLiteral("box-width")).nodeValue().toDouble() - txt->boundingRect().width(); } else { // left align, grow on right side, nothing to do } } gitem = txt; } else if (itemNode.attributes().namedItem(QStringLiteral("type")).nodeValue() == QLatin1String("QGraphicsRectItem")) { QDomNamedNodeMap rectProperties = itemNode.namedItem(QStringLiteral("content")).attributes(); QString rect = rectProperties.namedItem(QStringLiteral("rect")).nodeValue(); QString br_str = rectProperties.namedItem(QStringLiteral("brushcolor")).nodeValue(); QString pen_str = rectProperties.namedItem(QStringLiteral("pencolor")).nodeValue(); double penwidth = rectProperties.namedItem(QStringLiteral("penwidth")).nodeValue().toDouble(); auto *rec = new MyRectItem(); rec->setRect(stringToRect(rect)); if (penwidth > 0) { rec->setPen(QPen(QBrush(stringToColor(pen_str)), penwidth, Qt::SolidLine, Qt::SquareCap, Qt::RoundJoin)); } else { rec->setPen(Qt::NoPen); } if (!rectProperties.namedItem(QStringLiteral("gradient")).isNull()) { // Gradient color QString data = rectProperties.namedItem(QStringLiteral("gradient")).nodeValue(); rec->setData(TitleDocument::Gradient, data); QLinearGradient gr = GradientWidget::gradientFromString(data, rec->rect().width(), rec->rect().height()); rec->setBrush(QBrush(gr)); } else { rec->setBrush(QBrush(stringToColor(br_str))); } m_scene->addItem(rec); gitem = rec; } else if (itemNode.attributes().namedItem(QStringLiteral("type")).nodeValue() == QLatin1String("QGraphicsPixmapItem")) { QString url = itemNode.namedItem(QStringLiteral("content")).attributes().namedItem(QStringLiteral("url")).nodeValue(); QString base64 = itemNode.namedItem(QStringLiteral("content")).attributes().namedItem(QStringLiteral("base64")).nodeValue(); QPixmap pix; bool missing = false; if (base64.isEmpty()) { pix.load(url); if (pix.isNull()) { pix = createInvalidPixmap(url); m_missingElements++; missing = true; } } else { pix.loadFromData(QByteArray::fromBase64(base64.toLatin1())); } auto *rec = new MyPixmapItem(pix); if (missing) { rec->setData(Qt::UserRole + 2, 1); } m_scene->addItem(rec); rec->setShapeMode(QGraphicsPixmapItem::BoundingRectShape); rec->setData(Qt::UserRole, url); if (!base64.isEmpty()) { rec->setData(Qt::UserRole + 1, base64); } gitem = rec; } else if (itemNode.attributes().namedItem(QStringLiteral("type")).nodeValue() == QLatin1String("QGraphicsSvgItem")) { QString url = itemNode.namedItem(QStringLiteral("content")).attributes().namedItem(QStringLiteral("url")).nodeValue(); QString base64 = itemNode.namedItem(QStringLiteral("content")).attributes().namedItem(QStringLiteral("base64")).nodeValue(); QGraphicsSvgItem *rec = nullptr; if (base64.isEmpty()) { if (QFile::exists(url)) { rec = new MySvgItem(url); } } else { rec = new MySvgItem(); QSvgRenderer *renderer = new QSvgRenderer(QByteArray::fromBase64(base64.toLatin1()), rec); rec->setSharedRenderer(renderer); // QString elem=rec->elementId(); // QRectF bounds = renderer->boundsOnElement(elem); } if (rec) { m_scene->addItem(rec); rec->setData(Qt::UserRole, url); if (!base64.isEmpty()) { rec->setData(Qt::UserRole + 1, base64); } gitem = rec; } else { QPixmap pix = createInvalidPixmap(url); m_missingElements++; auto *rec2 = new MyPixmapItem(pix); rec2->setData(Qt::UserRole + 2, 1); m_scene->addItem(rec2); rec2->setShapeMode(QGraphicsPixmapItem::BoundingRectShape); rec2->setData(Qt::UserRole, url); gitem = rec2; } } } // pos and transform if (gitem) { QPointF p(xPosition, itemNode.namedItem(QStringLiteral("position")).attributes().namedItem(QStringLiteral("y")).nodeValue().toDouble()); gitem->setPos(p); QDomElement trans = itemNode.namedItem(QStringLiteral("position")).firstChild().toElement(); gitem->setTransform(stringToTransform(trans.firstChild().nodeValue())); QString rotate = trans.attribute(QStringLiteral("rotation")); if (!rotate.isEmpty()) { gitem->setData(TitleDocument::RotateFactor, stringToList(rotate)); } QString zoom = trans.attribute(QStringLiteral("zoom")); if (!zoom.isEmpty()) { gitem->setData(TitleDocument::ZoomFactor, zoom.toInt()); } if (zValue >= maxZValue) { maxZValue = zValue + 1; } gitem->setZValue(zValue); gitem->setFlags(QGraphicsItem::ItemIsMovable | QGraphicsItem::ItemIsSelectable | QGraphicsItem::ItemSendsGeometryChanges); // effects QDomNode eff = itemNode.namedItem(QStringLiteral("effect")); if (!eff.isNull()) { QDomElement e = eff.toElement(); if (e.attribute(QStringLiteral("type")) == QLatin1String("blur")) { auto *blur = new QGraphicsBlurEffect(); blur->setBlurRadius(e.attribute(QStringLiteral("blurradius")).toInt()); gitem->setGraphicsEffect(blur); } else if (e.attribute(QStringLiteral("type")) == QLatin1String("shadow")) { auto *shadow = new QGraphicsDropShadowEffect(); shadow->setBlurRadius(e.attribute(QStringLiteral("blurradius")).toInt()); shadow->setOffset(e.attribute(QStringLiteral("xoffset")).toInt(), e.attribute(QStringLiteral("yoffset")).toInt()); gitem->setGraphicsEffect(shadow); } } } if (itemNode.nodeName() == QLatin1String("background")) { // qCDebug(KDENLIVE_LOG) << items.item(i).attributes().namedItem("color").nodeValue(); QColor color = QColor(stringToColor(itemNode.attributes().namedItem(QStringLiteral("color")).nodeValue())); // color.setAlpha(itemNode.attributes().namedItem("alpha").nodeValue().toInt()); QList sceneItems = m_scene->items(); for (auto sceneItem : sceneItems) { if ((int)sceneItem->zValue() == -1100) { static_cast(sceneItem)->setBrush(QBrush(color)); break; } } } else if (itemNode.nodeName() == QLatin1String("startviewport") && (startv != nullptr)) { QString rect = itemNode.attributes().namedItem(QStringLiteral("rect")).nodeValue(); QRectF r = stringToRect(rect); startv->setRect(0, 0, r.width(), r.height()); startv->setPos(r.topLeft()); } else if (itemNode.nodeName() == QLatin1String("endviewport") && (endv != nullptr)) { QString rect = itemNode.attributes().namedItem(QStringLiteral("rect")).nodeValue(); QRectF r = stringToRect(rect); endv->setRect(0, 0, r.width(), r.height()); endv->setPos(r.topLeft()); } } } return maxZValue; } int TitleDocument::invalidCount() const { return m_missingElements; } QPixmap TitleDocument::createInvalidPixmap(const QString &url) { int missingHeight = m_height / 10; QPixmap pix(missingHeight, missingHeight); QIcon icon = QIcon::fromTheme(QStringLiteral("messagebox_warning")); pix.fill(QColor(255, 0, 0, 50)); QPainter ptr(&pix); icon.paint(&ptr, 0, 0, missingHeight / 2, missingHeight / 2); QPen pen(Qt::red); pen.setWidth(3); ptr.setPen(pen); ptr.drawText(QRectF(2, 2, missingHeight - 4, missingHeight - 4), Qt::AlignHCenter | Qt::AlignBottom, QFileInfo(url).fileName()); ptr.drawRect(2, 1, missingHeight - 4, missingHeight - 4); ptr.end(); return pix; } QString TitleDocument::colorToString(const QColor &c) { QString ret = QStringLiteral("%1,%2,%3,%4"); ret = ret.arg(c.red()).arg(c.green()).arg(c.blue()).arg(c.alpha()); return ret; } QString TitleDocument::rectFToString(const QRectF &c) { QString ret = QStringLiteral("%1,%2,%3,%4"); ret = ret.arg(c.left()).arg(c.top()).arg(c.width()).arg(c.height()); return ret; } QRectF TitleDocument::stringToRect(const QString &s) { QStringList l = s.split(QLatin1Char(',')); if (l.size() < 4) { return {}; } return QRectF(l.at(0).toDouble(), l.at(1).toDouble(), l.at(2).toDouble(), l.at(3).toDouble()).normalized(); } QColor TitleDocument::stringToColor(const QString &s) { QStringList l = s.split(QLatin1Char(',')); if (l.size() < 4) { return QColor(); } return {l.at(0).toInt(), l.at(1).toInt(), l.at(2).toInt(), l.at(3).toInt()}; } QTransform TitleDocument::stringToTransform(const QString &s) { QStringList l = s.split(QLatin1Char(',')); if (l.size() < 9) { return QTransform(); } return {l.at(0).toDouble(), l.at(1).toDouble(), l.at(2).toDouble(), l.at(3).toDouble(), l.at(4).toDouble(), l.at(5).toDouble(), l.at(6).toDouble(), l.at(7).toDouble(), l.at(8).toDouble()}; } QList TitleDocument::stringToList(const QString &s) { QStringList l = s.split(QLatin1Char(',')); if (l.size() < 3) { return QList(); } return QList() << QVariant(l.at(0).toDouble()) << QVariant(l.at(1).toDouble()) << QVariant(l.at(2).toDouble()); } int TitleDocument::frameWidth() const { return m_width; } int TitleDocument::frameHeight() const { return m_height; }