diff --git a/src/bin/model/markerlistmodel.hpp b/src/bin/model/markerlistmodel.hpp
index df2eac964..90ea6dd63 100644
--- a/src/bin/model/markerlistmodel.hpp
+++ b/src/bin/model/markerlistmodel.hpp
@@ -1,192 +1,191 @@
/***************************************************************************
* 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 . *
***************************************************************************/
#ifndef MARKERLISTMODEL_H
#define MARKERLISTMODEL_H
#include "definitions.h"
#include "gentime.h"
#include "undohelper.hpp"
#include
#include
#include
#include
#include
class ClipController;
class DocUndoStack;
class SnapInterface;
/* @brief This class is the model for a list of markers.
A marker is defined by a time, a type (the color used to represent it) and a comment string.
We store them in a sorted fashion using a std::map
A marker is essentially bound to a clip. We can also define guides, that are timeline-wise markers. For that, use the constructors without clipId
*/
class MarkerListModel : public QAbstractListModel
{
Q_OBJECT
public:
/* @brief Construct a marker list bound to the bin clip with given id */
explicit MarkerListModel(QString clipId, std::weak_ptr undo_stack, QObject *parent = nullptr);
/* @brief Construct a guide list (bound to the timeline) */
MarkerListModel(std::weak_ptr undo_stack, QObject *parent = nullptr);
enum { CommentRole = Qt::UserRole + 1, PosRole, FrameRole, ColorRole, TypeRole };
/* @brief Adds a marker at the given position. If there is already one, the comment will be overridden
@param pos defines the position of the marker, relative to the clip
@param comment is the text associated with the marker
@param type is the type (color) associated with the marker. If -1 is passed, then the value is pulled from kdenlive's defaults
*/
bool addMarker(GenTime pos, const QString &comment, int type = -1);
protected:
/* @brief Same function but accumulates undo/redo */
bool addMarker(GenTime pos, const QString &comment, int type, Fun &undo, Fun &redo);
public:
/* @brief Removes the marker at the given position.
Returns false if no marker was found at given pos
*/
bool removeMarker(GenTime pos);
/* @brief Delete all the markers of the model */
bool removeAllMarkers();
-protected:
/* @brief Same function but accumulates undo/redo */
bool removeMarker(GenTime pos, Fun &undo, Fun &redo);
public:
/* @brief Edit a marker
@param oldPos is the old position of the marker
@param pos defines the new position of the marker, relative to the clip
@param comment is the text associated with the marker
@param type is the type (color) associated with the marker. If -1 is passed, then the value is pulled from kdenlive's defaults
*/
bool editMarker(GenTime oldPos, GenTime pos, QString comment = QString(), int type = -1);
/* @brief This describes the available markers type and their corresponding colors */
static std::array markerTypes;
/* @brief Returns a marker data at given pos */
CommentedTime getMarker(const GenTime &pos, bool *ok) const;
/* @brief Returns all markers in model */
QList getAllMarkers() const;
/* @brief Returns all markers positions in model */
std::vector getSnapPoints() const;
/* @brief Returns true if a marker exists at given pos
Notice that add/remove queries are done in real time (gentime), but this request is made in frame
*/
Q_INVOKABLE bool hasMarker(int frame) const;
/* @brief Registers a snapModel to the marker model.
This is intended to be used for a guide model, so that the timelines can register their snapmodel to be updated when the guide moves. This is also used
on the clip monitor to keep tracking the clip markers
The snap logic for clips is managed from the Timeline
Note that no deregistration is necessary, the weak_ptr will be discarded as soon as it becomes invalid.
*/
void registerSnapModel(const std::weak_ptr &snapModel);
/* @brief Exports the model to json using format above */
QString toJson() const;
/* @brief Shows a dialog to edit a marker/guide
@param pos: position of the marker to edit, or new position for a marker
@param widget: qt widget that will be the parent of the dialog
@param createIfNotFound: if true, we create a marker if none is found at pos
@param clip: pointer to the clip if we are editing a marker
@return true if dialog was accepted and modification successful
*/
bool editMarkerGui(const GenTime &pos, QWidget *parent, bool createIfNotFound, ClipController *clip = nullptr);
// Mandatory overloads
QVariant data(const QModelIndex &index, int role) const override;
QHash roleNames() const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
public slots:
/* @brief Imports a list of markers from json data
The data should be formatted as follows:
[{"pos":0.2, "comment":"marker 1", "type":1}, {...}, ...]
return true on success and logs undo object
@param ignoreConflicts: if set to false, it aborts if the data contains a marker with same position but different comment and/or type. If set to true,
such markers are overridden silently
@param pushUndo: if true, create an undo object
*/
bool importFromJson(const QString &data, bool ignoreConflicts, bool pushUndo = true);
bool importFromJson(const QString &data, bool ignoreConflicts, Fun &undo, Fun &redo);
protected:
/* @brief Adds a snap point at marker position in the registered snap models
(those that are still valid)*/
void addSnapPoint(GenTime pos);
/* @brief Deletes a snap point at marker position in the registered snap models
(those that are still valid)*/
void removeSnapPoint(GenTime pos);
/** @brief Helper function that generate a lambda to change comment / type of given marker */
Fun changeComment_lambda(GenTime pos, const QString &comment, int type);
/** @brief Helper function that generate a lambda to add given marker */
Fun addMarker_lambda(GenTime pos, const QString &comment, int type);
/** @brief Helper function that generate a lambda to remove given marker */
Fun deleteMarker_lambda(GenTime pos);
/** @brief Helper function that retrieves a pointer to the markermodel, given whether it's a guide model and its clipId*/
static std::shared_ptr getModel(bool guide, const QString &clipId);
/* @brief Connects the signals of this object */
void setup();
private:
std::weak_ptr m_undoStack;
bool m_guide; // whether this model represents timeline-wise guides
QString m_clipId; // the Id of the clip this model corresponds to, if any.
mutable QReadWriteLock m_lock; // This is a lock that ensures safety in case of concurrent access
std::map> m_markerList;
std::vector> m_registeredSnaps;
signals:
void modelChanged();
public:
// this is to enable for range loops
auto begin() -> decltype(m_markerList.begin()) { return m_markerList.begin(); }
auto end() -> decltype(m_markerList.end()) { return m_markerList.end(); }
};
Q_DECLARE_METATYPE(MarkerListModel *)
#endif
diff --git a/src/mltcontroller/clippropertiescontroller.cpp b/src/mltcontroller/clippropertiescontroller.cpp
index 3d1cfc32d..653770620 100644
--- a/src/mltcontroller/clippropertiescontroller.cpp
+++ b/src/mltcontroller/clippropertiescontroller.cpp
@@ -1,1366 +1,1378 @@
/*
Copyright (C) 2015 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 "clippropertiescontroller.h"
#include "bin/model/markerlistmodel.hpp"
#include "clipcontroller.h"
#include "core.h"
#include "dialogs/profilesdialog.h"
#include "doc/kdenlivedoc.h"
#include "kdenlivesettings.h"
#include "profiles/profilerepository.hpp"
#include "project/projectmanager.h"
#include "timecodedisplay.h"
#include
#include "widgets/choosecolorwidget.h"
#include
#include
#ifdef KF5_USE_FILEMETADATA
#include
#include
#include
#include
#endif
#include
#include "kdenlive_debug.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
AnalysisTree::AnalysisTree(QWidget *parent)
: QTreeWidget(parent)
{
setRootIsDecorated(false);
setColumnCount(2);
setAlternatingRowColors(true);
setHeaderHidden(true);
setDragEnabled(true);
}
// virtual
QMimeData *AnalysisTree::mimeData(const QList list) const
{
QString mimeData;
for (QTreeWidgetItem *item : list) {
if ((item->flags() & Qt::ItemIsDragEnabled) != 0) {
mimeData.append(item->text(1));
}
}
auto *mime = new QMimeData;
mime->setData(QStringLiteral("kdenlive/geometry"), mimeData.toUtf8());
return mime;
}
#ifdef KF5_USE_FILEMETADATA
class ExtractionResult : public KFileMetaData::ExtractionResult
{
public:
ExtractionResult(const QString &filename, const QString &mimetype, QTreeWidget *tree)
: KFileMetaData::ExtractionResult(filename, mimetype, KFileMetaData::ExtractionResult::ExtractMetaData)
, m_tree(tree)
{
}
void append(const QString & /*text*/) override {}
void addType(KFileMetaData::Type::Type /*type*/) override {}
void add(KFileMetaData::Property::Property property, const QVariant &value) override
{
bool decode = false;
switch (property) {
case KFileMetaData::Property::ImageMake:
case KFileMetaData::Property::ImageModel:
case KFileMetaData::Property::ImageDateTime:
case KFileMetaData::Property::BitRate:
case KFileMetaData::Property::TrackNumber:
case KFileMetaData::Property::ReleaseYear:
case KFileMetaData::Property::Composer:
case KFileMetaData::Property::Genre:
case KFileMetaData::Property::Artist:
case KFileMetaData::Property::Album:
case KFileMetaData::Property::Title:
case KFileMetaData::Property::Comment:
case KFileMetaData::Property::Copyright:
case KFileMetaData::Property::PhotoFocalLength:
case KFileMetaData::Property::PhotoExposureTime:
case KFileMetaData::Property::PhotoFNumber:
case KFileMetaData::Property::PhotoApertureValue:
case KFileMetaData::Property::PhotoWhiteBalance:
case KFileMetaData::Property::PhotoGpsLatitude:
case KFileMetaData::Property::PhotoGpsLongitude:
decode = true;
break;
default:
break;
}
if (decode) {
KFileMetaData::PropertyInfo info(property);
if (info.valueType() == QVariant::DateTime) {
new QTreeWidgetItem(m_tree, QStringList() << info.displayName() << value.toDateTime().toString(Qt::DefaultLocaleShortDate));
} else if (info.valueType() == QVariant::Int) {
int val = value.toInt();
if (property == KFileMetaData::Property::BitRate) {
// Adjust unit for bitrate
new QTreeWidgetItem(m_tree, QStringList() << info.displayName()
<< QString::number(val / 1000) + QLatin1Char(' ') + i18nc("Kilobytes per seconds", "kb/s"));
} else {
new QTreeWidgetItem(m_tree, QStringList() << info.displayName() << QString::number(val));
}
} else if (info.valueType() == QVariant::Double) {
new QTreeWidgetItem(m_tree, QStringList() << info.displayName() << QString::number(value.toDouble()));
} else {
new QTreeWidgetItem(m_tree, QStringList() << info.displayName() << value.toString());
}
}
}
private:
QTreeWidget *m_tree;
};
#endif
ClipPropertiesController::ClipPropertiesController(ClipController *controller, QWidget *parent)
: QWidget(parent)
, m_controller(controller)
, m_tc(Timecode(Timecode::HH_MM_SS_HH, pCore->getCurrentFps()))
, m_id(controller->binId())
, m_type(controller->clipType())
, m_properties(new Mlt::Properties(controller->properties()))
, m_textEdit(nullptr)
{
m_controller->mirrorOriginalProperties(m_sourceProperties);
setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont));
setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
auto *lay = new QVBoxLayout;
lay->setContentsMargins(0, 0, 0, 0);
m_clipLabel = new QLabel(controller->clipName());
lay->addWidget(m_clipLabel);
m_tabWidget = new QTabWidget(this);
lay->addWidget(m_tabWidget);
setLayout(lay);
m_tabWidget->setDocumentMode(true);
m_tabWidget->setTabPosition(QTabWidget::East);
auto *forcePage = new QScrollArea(this);
m_propertiesPage = new QWidget(this);
m_markersPage = new QWidget(this);
m_metaPage = new QWidget(this);
m_analysisPage = new QWidget(this);
// Clip properties
auto *propsBox = new QVBoxLayout;
m_propertiesTree = new QTreeWidget(this);
m_propertiesTree->setRootIsDecorated(false);
m_propertiesTree->setColumnCount(2);
m_propertiesTree->setAlternatingRowColors(true);
m_propertiesTree->sortByColumn(0, Qt::AscendingOrder);
m_propertiesTree->setHeaderHidden(true);
propsBox->addWidget(m_propertiesTree);
fillProperties();
m_propertiesPage->setLayout(propsBox);
// Clip markers
auto *mBox = new QVBoxLayout;
m_markerTree = new QTreeView;
m_markerTree->setRootIsDecorated(false);
m_markerTree->setAlternatingRowColors(true);
m_markerTree->setHeaderHidden(true);
m_markerTree->setSelectionMode(QAbstractItemView::ExtendedSelection);
m_markerTree->setModel(controller->getMarkerModel().get());
mBox->addWidget(m_markerTree);
auto *bar = new QToolBar;
bar->addAction(QIcon::fromTheme(QStringLiteral("document-new")), i18n("Add marker"), this, SLOT(slotAddMarker()));
bar->addAction(QIcon::fromTheme(QStringLiteral("trash-empty")), i18n("Delete marker"), this, SLOT(slotDeleteMarker()));
bar->addAction(QIcon::fromTheme(QStringLiteral("document-edit")), i18n("Edit marker"), this, SLOT(slotEditMarker()));
bar->addAction(QIcon::fromTheme(QStringLiteral("document-save-as")), i18n("Export markers"), this, SLOT(slotSaveMarkers()));
bar->addAction(QIcon::fromTheme(QStringLiteral("document-open")), i18n("Import markers"), this, SLOT(slotLoadMarkers()));
mBox->addWidget(bar);
m_markersPage->setLayout(mBox);
connect(m_markerTree, &QAbstractItemView::doubleClicked, this, &ClipPropertiesController::slotSeekToMarker);
// metadata
auto *m2Box = new QVBoxLayout;
auto *metaTree = new QTreeWidget;
metaTree->setRootIsDecorated(true);
metaTree->setColumnCount(2);
metaTree->setAlternatingRowColors(true);
metaTree->setHeaderHidden(true);
m2Box->addWidget(metaTree);
slotFillMeta(metaTree);
m_metaPage->setLayout(m2Box);
// Clip analysis
auto *aBox = new QVBoxLayout;
m_analysisTree = new AnalysisTree(this);
aBox->addWidget(new QLabel(i18n("Analysis data")));
aBox->addWidget(m_analysisTree);
auto *bar2 = new QToolBar;
bar2->addAction(QIcon::fromTheme(QStringLiteral("trash-empty")), i18n("Delete analysis"), this, SLOT(slotDeleteAnalysis()));
bar2->addAction(QIcon::fromTheme(QStringLiteral("document-save-as")), i18n("Export analysis"), this, SLOT(slotSaveAnalysis()));
bar2->addAction(QIcon::fromTheme(QStringLiteral("document-open")), i18n("Import analysis"), this, SLOT(slotLoadAnalysis()));
aBox->addWidget(bar2);
slotFillAnalysisData();
m_analysisPage->setLayout(aBox);
// Force properties
auto *vbox = new QVBoxLayout;
vbox->setSpacing(0);
if (m_type == ClipType::Text || m_type == ClipType::SlideShow || m_type == ClipType::TextTemplate) {
QPushButton *editButton = new QPushButton(i18n("Edit Clip"), this);
connect(editButton, &QAbstractButton::clicked, this, &ClipPropertiesController::editClip);
vbox->addWidget(editButton);
}
if (m_type == ClipType::Color || m_type == ClipType::Image || m_type == ClipType::AV || m_type == ClipType::Video || m_type == ClipType::TextTemplate) {
// Edit duration widget
m_originalProperties.insert(QStringLiteral("out"), m_properties->get("out"));
int kdenlive_length = m_properties->time_to_frames(m_properties->get("kdenlive:duration"));
if (kdenlive_length > 0) {
m_originalProperties.insert(QStringLiteral("kdenlive:duration"), m_properties->get("kdenlive:duration"));
}
m_originalProperties.insert(QStringLiteral("length"), m_properties->get("length"));
auto *hlay = new QHBoxLayout;
QCheckBox *box = new QCheckBox(i18n("Duration"), this);
box->setObjectName(QStringLiteral("force_duration"));
hlay->addWidget(box);
auto *timePos = new TimecodeDisplay(m_tc, this);
timePos->setObjectName(QStringLiteral("force_duration_value"));
timePos->setValue(kdenlive_length > 0 ? kdenlive_length : m_properties->get_int("length"));
int original_length = m_properties->get_int("kdenlive:original_length");
if (original_length > 0) {
box->setChecked(true);
} else {
timePos->setEnabled(false);
}
hlay->addWidget(timePos);
vbox->addLayout(hlay);
connect(box, &QAbstractButton::toggled, timePos, &QWidget::setEnabled);
connect(box, &QCheckBox::stateChanged, this, &ClipPropertiesController::slotEnableForce);
connect(timePos, &TimecodeDisplay::timeCodeEditingFinished, this, &ClipPropertiesController::slotDurationChanged);
connect(this, &ClipPropertiesController::updateTimeCodeFormat, timePos, &TimecodeDisplay::slotUpdateTimeCodeFormat);
connect(this, SIGNAL(modified(int)), timePos, SLOT(setValue(int)));
// connect(this, static_cast(&ClipPropertiesController::modified), timePos, &TimecodeDisplay::setValue);
}
if (m_type == ClipType::TextTemplate) {
// Edit text widget
QString currentText = m_properties->get("templatetext");
m_originalProperties.insert(QStringLiteral("templatetext"), currentText);
m_textEdit = new QTextEdit(this);
m_textEdit->setAcceptRichText(false);
m_textEdit->setPlainText(currentText);
m_textEdit->setPlaceholderText(i18n("Enter template text here"));
vbox->addWidget(m_textEdit);
QPushButton *button = new QPushButton(i18n("Apply"), this);
vbox->addWidget(button);
connect(button, &QPushButton::clicked, this, &ClipPropertiesController::slotTextChanged);
} else if (m_type == ClipType::Color) {
// Edit color widget
m_originalProperties.insert(QStringLiteral("resource"), m_properties->get("resource"));
mlt_color color = m_properties->get_color("resource");
ChooseColorWidget *choosecolor = new ChooseColorWidget(i18n("Color"), QColor::fromRgb(color.r, color.g, color.b).name(), "", false, this);
vbox->addWidget(choosecolor);
// connect(choosecolor, SIGNAL(displayMessage(QString,int)), this, SIGNAL(displayMessage(QString,int)));
connect(choosecolor, &ChooseColorWidget::modified, this, &ClipPropertiesController::slotColorModified);
connect(this, static_cast(&ClipPropertiesController::modified), choosecolor,
&ChooseColorWidget::slotColorModified);
}
if (m_type == ClipType::AV || m_type == ClipType::Video || m_type == ClipType::Image) {
// Aspect ratio
int force_ar_num = m_properties->get_int("force_aspect_num");
int force_ar_den = m_properties->get_int("force_aspect_den");
m_originalProperties.insert(QStringLiteral("force_aspect_den"), (force_ar_den == 0) ? QString() : QString::number(force_ar_den));
m_originalProperties.insert(QStringLiteral("force_aspect_num"), (force_ar_num == 0) ? QString() : QString::number(force_ar_num));
auto *hlay = new QHBoxLayout;
QCheckBox *box = new QCheckBox(i18n("Aspect ratio"), this);
box->setObjectName(QStringLiteral("force_ar"));
vbox->addWidget(box);
connect(box, &QCheckBox::stateChanged, this, &ClipPropertiesController::slotEnableForce);
auto *spin1 = new QSpinBox(this);
spin1->setMaximum(8000);
spin1->setObjectName(QStringLiteral("force_aspect_num_value"));
hlay->addWidget(spin1);
hlay->addWidget(new QLabel(QStringLiteral(":")));
auto *spin2 = new QSpinBox(this);
spin2->setMinimum(1);
spin2->setMaximum(8000);
spin2->setObjectName(QStringLiteral("force_aspect_den_value"));
hlay->addWidget(spin2);
if (force_ar_num == 0) {
// use current ratio
int num = m_properties->get_int("meta.media.sample_aspect_num");
int den = m_properties->get_int("meta.media.sample_aspect_den");
if (den == 0) {
num = 1;
den = 1;
}
spin1->setEnabled(false);
spin2->setEnabled(false);
spin1->setValue(num);
spin2->setValue(den);
} else {
box->setChecked(true);
spin1->setEnabled(true);
spin2->setEnabled(true);
spin1->setValue(force_ar_num);
spin2->setValue(force_ar_den);
}
connect(spin2, static_cast(&QSpinBox::valueChanged), this, &ClipPropertiesController::slotAspectValueChanged);
connect(spin1, static_cast(&QSpinBox::valueChanged), this, &ClipPropertiesController::slotAspectValueChanged);
connect(box, &QAbstractButton::toggled, spin1, &QWidget::setEnabled);
connect(box, &QAbstractButton::toggled, spin2, &QWidget::setEnabled);
vbox->addLayout(hlay);
// Proxy
QString proxy = m_properties->get("kdenlive:proxy");
m_originalProperties.insert(QStringLiteral("kdenlive:proxy"), proxy);
hlay = new QHBoxLayout;
auto *bg = new QGroupBox(this);
bg->setCheckable(false);
bg->setFlat(true);
auto *groupLay = new QHBoxLayout;
groupLay->setContentsMargins(0, 0, 0, 0);
auto *pbox = new QCheckBox(i18n("Proxy clip"), this);
pbox->setTristate(true);
// Proxy codec label
QLabel *lab = new QLabel(this);
pbox->setObjectName(QStringLiteral("kdenlive:proxy"));
bool hasProxy = proxy.length() > 2;
if (hasProxy) {
bg->setToolTip(proxy);
bool proxyReady = (QFileInfo(proxy).fileName() == QFileInfo(m_properties->get("resource")).fileName());
if (proxyReady) {
pbox->setCheckState(Qt::Checked);
lab->setText(m_properties->get(QString("meta.media.%1.codec.name").arg(m_properties->get_int("video_index")).toUtf8().constData()));
} else {
pbox->setCheckState(Qt::PartiallyChecked);
}
} else {
pbox->setCheckState(Qt::Unchecked);
}
pbox->setEnabled(pCore->projectManager()->current()->getDocumentProperty(QStringLiteral("enableproxy")).toInt() != 0);
connect(pbox, &QCheckBox::stateChanged, [this, pbox](int state) {
emit requestProxy(state == Qt::PartiallyChecked);
if (state == Qt::Checked) {
QSignalBlocker bk(pbox);
pbox->setCheckState(Qt::Unchecked);
}
});
connect(this, &ClipPropertiesController::enableProxy, pbox, &QCheckBox::setEnabled);
connect(this, &ClipPropertiesController::proxyModified, [this, pbox, bg, lab](const QString &pxy) {
bool hasProxyClip = pxy.length() > 2;
QSignalBlocker bk(pbox);
pbox->setCheckState(hasProxyClip ? Qt::Checked : Qt::Unchecked);
bg->setEnabled(pbox->isChecked());
bg->setToolTip(pxy);
lab->setText(hasProxyClip ? m_properties->get(QString("meta.media.%1.codec.name").arg(m_properties->get_int("video_index")).toUtf8().constData())
: QString());
});
hlay->addWidget(pbox);
bg->setEnabled(pbox->checkState() == Qt::Checked);
groupLay->addWidget(lab);
// Delete button
auto *tb = new QToolButton(this);
tb->setIcon(QIcon::fromTheme(QStringLiteral("edit-delete")));
tb->setAutoRaise(true);
connect(tb, &QToolButton::clicked, [this, proxy]() { emit deleteProxy(); });
tb->setToolTip(i18n("Delete proxy file"));
groupLay->addWidget(tb);
// Folder button
tb = new QToolButton(this);
auto *pMenu = new QMenu(this);
tb->setIcon(QIcon::fromTheme(QStringLiteral("kdenlive-menu")));
tb->setToolTip(i18n("Proxy options"));
tb->setMenu(pMenu);
tb->setAutoRaise(true);
tb->setPopupMode(QToolButton::InstantPopup);
QAction *ac = new QAction(QIcon::fromTheme(QStringLiteral("document-open")), i18n("Open folder"), this);
connect(ac, &QAction::triggered, [this]() {
QString pxy = m_properties->get("kdenlive:proxy");
QDesktopServices::openUrl(QUrl::fromLocalFile(QFileInfo(pxy).path()));
});
pMenu->addAction(ac);
ac = new QAction(QIcon::fromTheme(QStringLiteral("media-playback-start")), i18n("Play proxy clip"), this);
connect(ac, &QAction::triggered, [this]() {
QString pxy = m_properties->get("kdenlive:proxy");
QDesktopServices::openUrl(QUrl::fromLocalFile(pxy));
});
pMenu->addAction(ac);
ac = new QAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Copy file location to clipboard"), this);
connect(ac, &QAction::triggered, [this]() {
QString pxy = m_properties->get("kdenlive:proxy");
QGuiApplication::clipboard()->setText(pxy);
});
pMenu->addAction(ac);
groupLay->addWidget(tb);
bg->setLayout(groupLay);
hlay->addWidget(bg);
vbox->addLayout(hlay);
}
if (m_type == ClipType::AV || m_type == ClipType::Video) {
QLocale locale;
locale.setNumberOptions(QLocale::OmitGroupSeparator);
// Fps
QString force_fps = m_properties->get("force_fps");
m_originalProperties.insert(QStringLiteral("force_fps"), force_fps.isEmpty() ? QStringLiteral("-") : force_fps);
auto *hlay = new QHBoxLayout;
QCheckBox *box = new QCheckBox(i18n("Frame rate"), this);
box->setObjectName(QStringLiteral("force_fps"));
connect(box, &QCheckBox::stateChanged, this, &ClipPropertiesController::slotEnableForce);
auto *spin = new QDoubleSpinBox(this);
spin->setMaximum(1000);
connect(spin, SIGNAL(valueChanged(double)), this, SLOT(slotValueChanged(double)));
// connect(spin, static_cast(&QDoubleSpinBox::valueChanged), this, &ClipPropertiesController::slotValueChanged);
spin->setObjectName(QStringLiteral("force_fps_value"));
if (force_fps.isEmpty()) {
spin->setValue(controller->originalFps());
} else {
spin->setValue(locale.toDouble(force_fps));
}
connect(box, &QAbstractButton::toggled, spin, &QWidget::setEnabled);
box->setChecked(!force_fps.isEmpty());
spin->setEnabled(!force_fps.isEmpty());
hlay->addWidget(box);
hlay->addWidget(spin);
vbox->addLayout(hlay);
// Scanning
QString force_prog = m_properties->get("force_progressive");
m_originalProperties.insert(QStringLiteral("force_progressive"), force_prog.isEmpty() ? QStringLiteral("-") : force_prog);
hlay = new QHBoxLayout;
box = new QCheckBox(i18n("Scanning"), this);
connect(box, &QCheckBox::stateChanged, this, &ClipPropertiesController::slotEnableForce);
box->setObjectName(QStringLiteral("force_progressive"));
auto *combo = new QComboBox(this);
combo->addItem(i18n("Interlaced"), 0);
combo->addItem(i18n("Progressive"), 1);
connect(combo, static_cast(&QComboBox::currentIndexChanged), this, &ClipPropertiesController::slotComboValueChanged);
combo->setObjectName(QStringLiteral("force_progressive_value"));
if (!force_prog.isEmpty()) {
combo->setCurrentIndex(force_prog.toInt());
}
connect(box, &QAbstractButton::toggled, combo, &QWidget::setEnabled);
box->setChecked(!force_prog.isEmpty());
combo->setEnabled(!force_prog.isEmpty());
hlay->addWidget(box);
hlay->addWidget(combo);
vbox->addLayout(hlay);
// Field order
QString force_tff = m_properties->get("force_tff");
m_originalProperties.insert(QStringLiteral("force_tff"), force_tff.isEmpty() ? QStringLiteral("-") : force_tff);
hlay = new QHBoxLayout;
box = new QCheckBox(i18n("Field order"), this);
connect(box, &QCheckBox::stateChanged, this, &ClipPropertiesController::slotEnableForce);
box->setObjectName(QStringLiteral("force_tff"));
combo = new QComboBox(this);
combo->addItem(i18n("Bottom first"), 0);
combo->addItem(i18n("Top first"), 1);
connect(combo, static_cast(&QComboBox::currentIndexChanged), this, &ClipPropertiesController::slotComboValueChanged);
combo->setObjectName(QStringLiteral("force_tff_value"));
if (!force_tff.isEmpty()) {
combo->setCurrentIndex(force_tff.toInt());
}
connect(box, &QAbstractButton::toggled, combo, &QWidget::setEnabled);
box->setChecked(!force_tff.isEmpty());
combo->setEnabled(!force_tff.isEmpty());
hlay->addWidget(box);
hlay->addWidget(combo);
vbox->addLayout(hlay);
// Autorotate
QString autorotate = m_properties->get("autorotate");
m_originalProperties.insert(QStringLiteral("autorotate"), autorotate);
hlay = new QHBoxLayout;
box = new QCheckBox(i18n("Disable autorotate"), this);
connect(box, &QCheckBox::stateChanged, this, &ClipPropertiesController::slotEnableForce);
box->setObjectName(QStringLiteral("autorotate"));
box->setChecked(autorotate == QLatin1String("0"));
hlay->addWidget(box);
vbox->addLayout(hlay);
// Decoding threads
QString threads = m_properties->get("threads");
m_originalProperties.insert(QStringLiteral("threads"), threads);
hlay = new QHBoxLayout;
hlay->addWidget(new QLabel(i18n("Threads")));
auto *spinI = new QSpinBox(this);
spinI->setMaximum(4);
spinI->setMinimum(1);
spinI->setObjectName(QStringLiteral("threads_value"));
if (!threads.isEmpty()) {
spinI->setValue(threads.toInt());
} else {
spinI->setValue(1);
}
connect(spinI, static_cast(&QSpinBox::valueChanged), this,
static_cast(&ClipPropertiesController::slotValueChanged));
hlay->addWidget(spinI);
vbox->addLayout(hlay);
// Video index
if (!m_videoStreams.isEmpty()) {
QString vix = m_sourceProperties.get("video_index");
m_originalProperties.insert(QStringLiteral("video_index"), vix);
hlay = new QHBoxLayout;
KDualAction *ac = new KDualAction(i18n("Disable video"), i18n("Enable video"), this);
ac->setInactiveIcon(QIcon::fromTheme(QStringLiteral("kdenlive-show-video")));
ac->setActiveIcon(QIcon::fromTheme(QStringLiteral("kdenlive-hide-video")));
auto *tbv = new QToolButton(this);
tbv->setToolButtonStyle(Qt::ToolButtonIconOnly);
tbv->setDefaultAction(ac);
tbv->setAutoRaise(true);
hlay->addWidget(tbv);
hlay->addWidget(new QLabel(i18n("Video stream")));
auto *videoStream = new QComboBox(this);
int ix = 1;
for (int stream : m_videoStreams) {
videoStream->addItem(i18n("Video stream %1", ix), stream);
ix++;
}
if (!vix.isEmpty() && vix.toInt() > -1) {
videoStream->setCurrentIndex(videoStream->findData(QVariant(vix)));
}
ac->setActive(vix.toInt() == -1);
videoStream->setEnabled(vix.toInt() > -1);
videoStream->setVisible(m_videoStreams.size() > 1);
connect(ac, &KDualAction::activeChanged, [this, videoStream](bool activated) {
QMap properties;
int vindx = -1;
if (activated) {
videoStream->setEnabled(false);
} else {
videoStream->setEnabled(true);
vindx = videoStream->currentData().toInt();
}
properties.insert(QStringLiteral("video_index"), QString::number(vindx));
properties.insert(QStringLiteral("set.test_image"), vindx > -1 ? QStringLiteral("0") : QStringLiteral("1"));
emit updateClipProperties(m_id, m_originalProperties, properties);
m_originalProperties = properties;
});
QObject::connect(videoStream, static_cast(&QComboBox::currentIndexChanged), [this, videoStream]() {
QMap properties;
properties.insert(QStringLiteral("video_index"), QString::number(videoStream->currentData().toInt()));
emit updateClipProperties(m_id, m_originalProperties, properties);
m_originalProperties = properties;
});
hlay->addWidget(videoStream);
vbox->addLayout(hlay);
}
// Audio index
QMap audioStreamsInfo;
if (m_controller->audioInfo()) {
audioStreamsInfo = m_controller->audioInfo()->streamInfo(m_sourceProperties);
}
if (!audioStreamsInfo.isEmpty()) {
QString vix = m_sourceProperties.get("audio_index");
m_originalProperties.insert(QStringLiteral("audio_index"), vix);
hlay = new QHBoxLayout;
KDualAction *ac = new KDualAction(i18n("Disable audio"), i18n("Enable audio"), this);
ac->setInactiveIcon(QIcon::fromTheme(QStringLiteral("kdenlive-show-audio")));
ac->setActiveIcon(QIcon::fromTheme(QStringLiteral("kdenlive-hide-audio")));
auto *tbv = new QToolButton(this);
tbv->setToolButtonStyle(Qt::ToolButtonIconOnly);
tbv->setDefaultAction(ac);
tbv->setAutoRaise(true);
hlay->addWidget(tbv);
hlay->addWidget(new QLabel(i18n("Audio stream")));
auto *audioStream = new QComboBox(this);
QMapIterator i(audioStreamsInfo);
while (i.hasNext()) {
i.next();
audioStream->addItem(QString("%1: %2").arg(i.key()).arg(i.value()), i.key());
}
if (!vix.isEmpty() && vix.toInt() > -1) {
audioStream->setCurrentIndex(audioStream->findData(QVariant(vix)));
}
ac->setActive(vix.toInt() == -1);
audioStream->setEnabled(vix.toInt() > -1);
audioStream->setVisible(audioStreamsInfo.size() > 0);
connect(ac, &KDualAction::activeChanged, [this, audioStream](bool activated) {
QMap properties;
int vindx = -1;
if (activated) {
audioStream->setEnabled(false);
} else {
audioStream->setEnabled(true);
vindx = audioStream->currentData().toInt();
}
properties.insert(QStringLiteral("audio_index"), QString::number(vindx));
properties.insert(QStringLiteral("set.test_audio"), vindx > -1 ? QStringLiteral("0") : QStringLiteral("1"));
emit updateClipProperties(m_id, m_originalProperties, properties);
m_originalProperties = properties;
});
QObject::connect(audioStream, static_cast(&QComboBox::currentIndexChanged), [this, audioStream]() {
QMap properties;
properties.insert(QStringLiteral("audio_index"), QString::number(audioStream->currentData().toInt()));
emit updateClipProperties(m_id, m_originalProperties, properties);
m_originalProperties = properties;
});
hlay->addWidget(audioStream);
vbox->addLayout(hlay);
// Audio sync
hlay = new QHBoxLayout;
hlay->addWidget(new QLabel(i18n("Audio sync")));
auto *spinSync = new QSpinBox(this);
spinSync->setSuffix(i18n("ms"));
spinSync->setRange(-1000, 1000);
spinSync->setValue(qRound(1000 * m_sourceProperties.get_double("video_delay")));
spinSync->setObjectName(QStringLiteral("video_delay"));
if (spinSync->value() != 0) {
m_originalProperties.insert(QStringLiteral("video_delay"), locale.toString(m_sourceProperties.get_double("video_delay")));
}
//QObject::connect(spinSync, static_cast(&QSpinBox::valueChanged), [this, spinSync]() {
QObject::connect(spinSync, &QSpinBox::editingFinished, [this, spinSync, locale]() {
QMap properties;
properties.insert(QStringLiteral("video_delay"), locale.toString(spinSync->value() / 1000.));
emit updateClipProperties(m_id, m_originalProperties, properties);
m_originalProperties = properties;
});
hlay->addWidget(spinSync);
vbox->addLayout(hlay);
}
// Colorspace
hlay = new QHBoxLayout;
box = new QCheckBox(i18n("Colorspace"), this);
box->setObjectName(QStringLiteral("force_colorspace"));
connect(box, &QCheckBox::stateChanged, this, &ClipPropertiesController::slotEnableForce);
combo = new QComboBox(this);
combo->setObjectName(QStringLiteral("force_colorspace_value"));
combo->addItem(ProfileRepository::getColorspaceDescription(601), 601);
combo->addItem(ProfileRepository::getColorspaceDescription(709), 709);
combo->addItem(ProfileRepository::getColorspaceDescription(240), 240);
int force_colorspace = m_properties->get_int("force_colorspace");
m_originalProperties.insert(QStringLiteral("force_colorspace"), force_colorspace == 0 ? QStringLiteral("-") : QString::number(force_colorspace));
int colorspace = controller->videoCodecProperty(QStringLiteral("colorspace")).toInt();
if (force_colorspace > 0) {
box->setChecked(true);
combo->setEnabled(true);
combo->setCurrentIndex(combo->findData(force_colorspace));
} else if (colorspace > 0) {
combo->setEnabled(false);
combo->setCurrentIndex(combo->findData(colorspace));
} else {
combo->setEnabled(false);
}
connect(box, &QAbstractButton::toggled, combo, &QWidget::setEnabled);
connect(combo, static_cast(&QComboBox::currentIndexChanged), this, &ClipPropertiesController::slotComboValueChanged);
hlay->addWidget(box);
hlay->addWidget(combo);
vbox->addLayout(hlay);
// Full luma
QString force_luma = m_properties->get("set.force_full_luma");
m_originalProperties.insert(QStringLiteral("set.force_full_luma"), force_luma);
hlay = new QHBoxLayout;
box = new QCheckBox(i18n("Full luma range"), this);
connect(box, &QCheckBox::stateChanged, this, &ClipPropertiesController::slotEnableForce);
box->setObjectName(QStringLiteral("set.force_full_luma"));
box->setChecked(!force_luma.isEmpty());
hlay->addWidget(box);
vbox->addLayout(hlay);
hlay->addStretch(10);
}
QWidget *forceProp = new QWidget(this);
forceProp->setLayout(vbox);
forcePage->setWidget(forceProp);
forcePage->setWidgetResizable(true);
vbox->addStretch(10);
m_tabWidget->addTab(m_propertiesPage, QString());
m_tabWidget->addTab(forcePage, QString());
m_tabWidget->addTab(m_markersPage, QString());
m_tabWidget->addTab(m_metaPage, QString());
m_tabWidget->addTab(m_analysisPage, QString());
m_tabWidget->setTabIcon(0, QIcon::fromTheme(QStringLiteral("edit-find")));
m_tabWidget->setTabToolTip(0, i18n("File info"));
m_tabWidget->setTabIcon(1, QIcon::fromTheme(QStringLiteral("document-edit")));
m_tabWidget->setTabToolTip(1, i18n("Properties"));
m_tabWidget->setTabIcon(2, QIcon::fromTheme(QStringLiteral("bookmark-new")));
m_tabWidget->setTabToolTip(2, i18n("Markers"));
m_tabWidget->setTabIcon(3, QIcon::fromTheme(QStringLiteral("view-grid")));
m_tabWidget->setTabToolTip(3, i18n("Metadata"));
m_tabWidget->setTabIcon(4, QIcon::fromTheme(QStringLiteral("visibility")));
m_tabWidget->setTabToolTip(4, i18n("Analysis"));
m_tabWidget->setCurrentIndex(KdenliveSettings::properties_panel_page());
if (m_type == ClipType::Color) {
m_tabWidget->setTabEnabled(0, false);
}
connect(m_tabWidget, &QTabWidget::currentChanged, this, &ClipPropertiesController::updateTab);
}
ClipPropertiesController::~ClipPropertiesController() = default;
void ClipPropertiesController::updateTab(int ix)
{
KdenliveSettings::setProperties_panel_page(ix);
}
void ClipPropertiesController::slotRefreshTimeCode()
{
emit updateTimeCodeFormat();
}
void ClipPropertiesController::slotReloadProperties()
{
mlt_color color;
m_properties.reset(new Mlt::Properties(m_controller->properties()));
m_clipLabel->setText(m_properties->get("kdenlive:clipname"));
switch (m_type) {
case ClipType::Color:
m_originalProperties.insert(QStringLiteral("resource"), m_properties->get("resource"));
m_originalProperties.insert(QStringLiteral("out"), m_properties->get("out"));
m_originalProperties.insert(QStringLiteral("length"), m_properties->get("length"));
emit modified(m_properties->get_int("length"));
color = m_properties->get_color("resource");
emit modified(QColor::fromRgb(color.r, color.g, color.b));
break;
case ClipType::TextTemplate:
m_textEdit->setPlainText(m_properties->get("templatetext"));
break;
case ClipType::Image:
case ClipType::AV:
case ClipType::Video: {
QString proxy = m_properties->get("kdenlive:proxy");
if (proxy != m_originalProperties.value(QStringLiteral("kdenlive:proxy"))) {
m_originalProperties.insert(QStringLiteral("kdenlive:proxy"), proxy);
emit proxyModified(proxy);
}
break;
}
default:
break;
}
}
void ClipPropertiesController::slotColorModified(const QColor &newcolor)
{
QMap properties;
properties.insert(QStringLiteral("resource"), newcolor.name(QColor::HexArgb));
QMap oldProperties;
oldProperties.insert(QStringLiteral("resource"), m_properties->get("resource"));
emit updateClipProperties(m_id, oldProperties, properties);
}
void ClipPropertiesController::slotDurationChanged(int duration)
{
QMap properties;
// kdenlive_length is the default duration for image / title clips
int kdenlive_length = m_properties->time_to_frames(m_properties->get("kdenlive:duration"));
int current_length = m_properties->get_int("length");
if (kdenlive_length > 0) {
// special case, image/title clips store default duration in kdenlive:duration property
properties.insert(QStringLiteral("kdenlive:duration"), m_properties->frames_to_time(duration));
if (duration > current_length) {
properties.insert(QStringLiteral("length"), m_properties->frames_to_time(duration));
properties.insert(QStringLiteral("out"), m_properties->frames_to_time(duration - 1));
}
} else {
properties.insert(QStringLiteral("length"), m_properties->frames_to_time(duration));
properties.insert(QStringLiteral("out"), m_properties->frames_to_time(duration - 1));
}
emit updateClipProperties(m_id, m_originalProperties, properties);
m_originalProperties = properties;
}
void ClipPropertiesController::slotEnableForce(int state)
{
auto *box = qobject_cast(sender());
if (!box) {
return;
}
QString param = box->objectName();
QMap properties;
QLocale locale;
locale.setNumberOptions(QLocale::OmitGroupSeparator);
if (state == Qt::Unchecked) {
// The force property was disable, remove it / reset default if necessary
if (param == QLatin1String("force_duration")) {
// special case, reset original duration
auto *timePos = findChild(param + QStringLiteral("_value"));
timePos->setValue(m_properties->get_int("kdenlive:original_length"));
int original = m_properties->get_int("kdenlive:original_length");
m_properties->set("kdenlive:original_length", (char *)nullptr);
slotDurationChanged(original);
return;
}
if (param == QLatin1String("kdenlive:transparency")) {
properties.insert(param, QString());
} else if (param == QLatin1String("force_ar")) {
properties.insert(QStringLiteral("force_aspect_den"), QString());
properties.insert(QStringLiteral("force_aspect_num"), QString());
properties.insert(QStringLiteral("force_aspect_ratio"), QString());
} else if (param == QLatin1String("autorotate")) {
properties.insert(QStringLiteral("autorotate"), QString());
} else {
properties.insert(param, QString());
}
} else {
// A force property was set
if (param == QLatin1String("force_duration")) {
int original_length = m_properties->get_int("kdenlive:original_length");
if (original_length == 0) {
int kdenlive_length = m_properties->time_to_frames(m_properties->get("kdenlive:duration"));
m_properties->set("kdenlive:original_length", kdenlive_length > 0 ? m_properties->get("kdenlive:duration") : m_properties->get("length"));
}
} else if (param == QLatin1String("force_fps")) {
auto *spin = findChild(param + QStringLiteral("_value"));
if (!spin) {
return;
}
properties.insert(param, locale.toString(spin->value()));
} else if (param == QLatin1String("threads")) {
auto *spin = findChild(param + QStringLiteral("_value"));
if (!spin) {
return;
}
properties.insert(param, QString::number(spin->value()));
} else if (param == QLatin1String("force_colorspace") || param == QLatin1String("force_progressive") || param == QLatin1String("force_tff")) {
auto *combo = findChild(param + QStringLiteral("_value"));
if (!combo) {
return;
}
properties.insert(param, QString::number(combo->currentData().toInt()));
} else if (param == QLatin1String("set.force_full_luma")) {
properties.insert(param, QStringLiteral("1"));
} else if (param == QLatin1String("autorotate")) {
properties.insert(QStringLiteral("autorotate"), QStringLiteral("0"));
} else if (param == QLatin1String("force_ar")) {
auto *spin = findChild(QStringLiteral("force_aspect_num_value"));
auto *spin2 = findChild(QStringLiteral("force_aspect_den_value"));
if ((spin == nullptr) || (spin2 == nullptr)) {
return;
}
properties.insert(QStringLiteral("force_aspect_den"), QString::number(spin2->value()));
properties.insert(QStringLiteral("force_aspect_num"), QString::number(spin->value()));
properties.insert(QStringLiteral("force_aspect_ratio"), locale.toString((double)spin->value() / spin2->value()));
}
}
if (properties.isEmpty()) {
return;
}
emit updateClipProperties(m_id, m_originalProperties, properties);
m_originalProperties = properties;
}
void ClipPropertiesController::slotValueChanged(double value)
{
auto *box = qobject_cast(sender());
if (!box) {
return;
}
QString param = box->objectName().section(QLatin1Char('_'), 0, -2);
QMap properties;
QLocale locale;
locale.setNumberOptions(QLocale::OmitGroupSeparator);
properties.insert(param, locale.toString(value));
emit updateClipProperties(m_id, m_originalProperties, properties);
m_originalProperties = properties;
}
void ClipPropertiesController::slotValueChanged(int value)
{
auto *box = qobject_cast(sender());
if (!box) {
return;
}
QString param = box->objectName().section(QLatin1Char('_'), 0, -2);
QMap properties;
properties.insert(param, QString::number(value));
emit updateClipProperties(m_id, m_originalProperties, properties);
m_originalProperties = properties;
}
void ClipPropertiesController::slotAspectValueChanged(int)
{
auto *spin = findChild(QStringLiteral("force_aspect_num_value"));
auto *spin2 = findChild(QStringLiteral("force_aspect_den_value"));
if ((spin == nullptr) || (spin2 == nullptr)) {
return;
}
QMap properties;
properties.insert(QStringLiteral("force_aspect_den"), QString::number(spin2->value()));
properties.insert(QStringLiteral("force_aspect_num"), QString::number(spin->value()));
QLocale locale;
locale.setNumberOptions(QLocale::OmitGroupSeparator);
properties.insert(QStringLiteral("force_aspect_ratio"), locale.toString((double)spin->value() / spin2->value()));
emit updateClipProperties(m_id, m_originalProperties, properties);
m_originalProperties = properties;
}
void ClipPropertiesController::slotComboValueChanged()
{
auto *box = qobject_cast(sender());
if (!box) {
return;
}
QString param = box->objectName().section(QLatin1Char('_'), 0, -2);
QMap properties;
properties.insert(param, QString::number(box->currentData().toInt()));
emit updateClipProperties(m_id, m_originalProperties, properties);
m_originalProperties = properties;
}
void ClipPropertiesController::fillProperties()
{
m_clipProperties.clear();
QList propertyMap;
m_propertiesTree->setSortingEnabled(false);
#ifdef KF5_USE_FILEMETADATA
// Read File Metadata through KDE's metadata system
KFileMetaData::ExtractorCollection metaDataCollection;
QMimeDatabase mimeDatabase;
QMimeType mimeType;
mimeType = mimeDatabase.mimeTypeForFile(m_controller->clipUrl());
for (KFileMetaData::Extractor *plugin : metaDataCollection.fetchExtractors(mimeType.name())) {
ExtractionResult extractionResult(m_controller->clipUrl(), mimeType.name(), m_propertiesTree);
plugin->extract(&extractionResult);
}
#endif
// Get MLT's metadata
if (m_type == ClipType::Image) {
int width = m_sourceProperties.get_int("meta.media.width");
int height = m_sourceProperties.get_int("meta.media.height");
propertyMap.append(QStringList() << i18n("Image size") << QString::number(width) + QLatin1Char('x') + QString::number(height));
}
if (m_type == ClipType::AV || m_type == ClipType::Video || m_type == ClipType::Audio) {
int vindex = m_sourceProperties.get_int("video_index");
int default_audio = m_sourceProperties.get_int("audio_index");
// Find maximum stream index values
m_videoStreams.clear();
int aStreams = m_sourceProperties.get_int("meta.media.nb_streams");
for (int ix = 0; ix < aStreams; ++ix) {
char property[200];
snprintf(property, sizeof(property), "meta.media.%d.stream.type", ix);
QString type = m_sourceProperties.get(property);
if (type == QLatin1String("video")) {
m_videoStreams << ix;
}
}
m_clipProperties.insert(QStringLiteral("default_video"), QString::number(vindex));
m_clipProperties.insert(QStringLiteral("default_audio"), QString::number(default_audio));
if (vindex > -1) {
// We have a video stream
QString codecInfo = QString("meta.media.%1.codec.").arg(vindex);
QString streamInfo = QString("meta.media.%1.stream.").arg(vindex);
QString property = codecInfo + QStringLiteral("long_name");
QString codec = m_sourceProperties.get(property.toUtf8().constData());
if (!codec.isEmpty()) {
propertyMap.append({i18n("Video codec"), codec});
}
int width = m_sourceProperties.get_int("meta.media.width");
int height = m_sourceProperties.get_int("meta.media.height");
propertyMap.append({i18n("Frame size"), QString::number(width) + QLatin1Char('x') + QString::number(height)});
property = streamInfo + QStringLiteral("frame_rate");
QString fpsValue = m_sourceProperties.get(property.toUtf8().constData());
if (!fpsValue.isEmpty()) {
propertyMap.append({i18n("Frame rate"), fpsValue});
} else {
int rate_den = m_sourceProperties.get_int("meta.media.frame_rate_den");
if (rate_den > 0) {
double fps = ((double)m_sourceProperties.get_int("meta.media.frame_rate_num")) / rate_den;
propertyMap.append({i18n("Frame rate"), QString::number(fps, 'f', 2)});
}
}
property = codecInfo + QStringLiteral("bit_rate");
int bitrate = m_sourceProperties.get_int(property.toUtf8().constData()) / 1000;
if (bitrate > 0) {
propertyMap.append({i18n("Video bitrate"), QString::number(bitrate) + QLatin1Char(' ') + i18nc("Kilobytes per seconds", "kb/s")});
}
int scan = m_sourceProperties.get_int("meta.media.progressive");
propertyMap.append({i18n("Scanning"), (scan == 1 ? i18n("Progressive") : i18n("Interlaced"))});
property = codecInfo + QStringLiteral("sample_aspect_ratio");
double par = m_sourceProperties.get_double(property.toUtf8().constData());
if (qFuzzyIsNull(par)) {
// Read media aspect ratio
par = m_sourceProperties.get_double("aspect_ratio");
}
propertyMap.append({i18n("Pixel aspect ratio"), QString::number(par, 'f', 3)});
property = codecInfo + QStringLiteral("pix_fmt");
propertyMap.append({i18n("Pixel format"), m_sourceProperties.get(property.toUtf8().constData())});
property = codecInfo + QStringLiteral("colorspace");
int colorspace = m_sourceProperties.get_int(property.toUtf8().constData());
propertyMap.append({i18n("Colorspace"), ProfileRepository::getColorspaceDescription(colorspace)});
}
if (default_audio > -1) {
propertyMap.append({i18n("Audio streams"), QString::number(m_controller->audioStreamsCount())});
QString codecInfo = QString("meta.media.%1.codec.").arg(default_audio);
QString property = codecInfo + QStringLiteral("long_name");
QString codec = m_sourceProperties.get(property.toUtf8().constData());
if (!codec.isEmpty()) {
propertyMap.append({i18n("Audio codec"), codec});
}
property = codecInfo + QStringLiteral("channels");
int channels = m_sourceProperties.get_int(property.toUtf8().constData());
propertyMap.append({i18n("Audio channels"), QString::number(channels)});
property = codecInfo + QStringLiteral("sample_rate");
int srate = m_sourceProperties.get_int(property.toUtf8().constData());
propertyMap.append({i18n("Audio frequency"), QString::number(srate) + QLatin1Char(' ') + i18nc("Herz", "Hz")});
property = codecInfo + QStringLiteral("bit_rate");
int bitrate = m_sourceProperties.get_int(property.toUtf8().constData()) / 1000;
if (bitrate > 0) {
propertyMap.append({i18n("Audio bitrate"), QString::number(bitrate) + QLatin1Char(' ') + i18nc("Kilobytes per seconds", "kb/s")});
}
}
}
qint64 filesize = m_sourceProperties.get_int64("kdenlive:file_size");
if (filesize > 0) {
QLocale locale(QLocale::system()); // use the user's locale for getting proper separators!
propertyMap.append({i18n("File size"), KIO::convertSize((size_t)filesize) + QStringLiteral(" (") + locale.toString(filesize) + QLatin1Char(')')});
}
for (int i = 0; i < propertyMap.count(); i++) {
auto *item = new QTreeWidgetItem(m_propertiesTree, propertyMap.at(i));
item->setToolTip(1, propertyMap.at(i).at(1));
}
m_propertiesTree->setSortingEnabled(true);
m_propertiesTree->resizeColumnToContents(0);
}
void ClipPropertiesController::slotSeekToMarker()
{
auto markerModel = m_controller->getMarkerModel();
auto current = m_markerTree->currentIndex();
if (!current.isValid()) return;
GenTime pos(markerModel->data(current, MarkerListModel::PosRole).toDouble());
emit seekToFrame(pos.frames(pCore->getCurrentFps()));
}
void ClipPropertiesController::slotEditMarker()
{
auto markerModel = m_controller->getMarkerModel();
auto current = m_markerTree->currentIndex();
if (!current.isValid()) return;
GenTime pos(markerModel->data(current, MarkerListModel::PosRole).toDouble());
markerModel->editMarkerGui(pos, this, false, m_controller);
}
void ClipPropertiesController::slotDeleteMarker()
{
auto markerModel = m_controller->getMarkerModel();
- auto current = m_markerTree->currentIndex();
- if (!current.isValid()) return;
- GenTime pos(markerModel->data(current, MarkerListModel::PosRole).toDouble());
- markerModel->removeMarker(pos);
+ QModelIndexList indexes = m_markerTree->selectionModel()->selectedIndexes();
+ QList positions;
+ for (auto &ix : indexes) {
+ if (ix.isValid()) {
+ positions << GenTime(markerModel->data(ix, MarkerListModel::PosRole).toDouble());
+ }
+ }
+ if (!positions.isEmpty()) {
+ Fun undo = []() { return true; };
+ Fun redo = []() { return true; };
+
+ for (GenTime pos : positions) {
+ markerModel->removeMarker(pos, undo, redo);
+ }
+ pCore->pushUndo(undo, redo, i18n("Delete marker"));
+ }
}
void ClipPropertiesController::slotAddMarker()
{
auto markerModel = m_controller->getMarkerModel();
GenTime pos(m_controller->originalProducer()->position(), m_tc.fps());
markerModel->editMarkerGui(pos, this, true, m_controller);
}
void ClipPropertiesController::slotSaveMarkers()
{
QScopedPointer fd(new QFileDialog(this, i18n("Save Clip Markers"), pCore->projectManager()->current()->projectDataFolder()));
fd->setMimeTypeFilters(QStringList() << QStringLiteral("application/json") << QStringLiteral("text/plain"));
fd->setFileMode(QFileDialog::AnyFile);
fd->setAcceptMode(QFileDialog::AcceptSave);
if (fd->exec() != QDialog::Accepted) {
return;
}
QStringList selection = fd->selectedFiles();
QString url;
if (!selection.isEmpty()) {
url = selection.first();
}
if (url.isEmpty()) {
return;
}
QFile file(url);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
KMessageBox::error(this, i18n("Cannot open file %1", QUrl::fromLocalFile(url).fileName()));
return;
}
file.write(m_controller->getMarkerModel()->toJson().toUtf8());
file.close();
}
void ClipPropertiesController::slotLoadMarkers()
{
QScopedPointer fd(new QFileDialog(this, i18n("Load Clip Markers"), pCore->projectManager()->current()->projectDataFolder()));
fd->setMimeTypeFilters(QStringList() << QStringLiteral("application/json") << QStringLiteral("text/plain"));
fd->setFileMode(QFileDialog::ExistingFile);
if (fd->exec() != QDialog::Accepted) {
return;
}
QStringList selection = fd->selectedFiles();
QString url;
if (!selection.isEmpty()) {
url = selection.first();
}
if (url.isEmpty()) {
return;
}
QFile file(url);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
KMessageBox::error(this, i18n("Cannot open file %1", QUrl::fromLocalFile(url).fileName()));
return;
}
QString fileContent = QString::fromUtf8(file.readAll());
file.close();
bool res = m_controller->getMarkerModel()->importFromJson(fileContent, false);
if (!res) {
KMessageBox::error(this, i18n("An error occurred while parsing the marker file"));
}
}
void ClipPropertiesController::slotFillMeta(QTreeWidget *tree)
{
tree->clear();
if (m_type != ClipType::AV && m_type != ClipType::Video && m_type != ClipType::Image) {
// Currently, we only use exiftool on video files
return;
}
int exifUsed = m_controller->getProducerIntProperty(QStringLiteral("kdenlive:exiftool"));
if (exifUsed == 1) {
Mlt::Properties subProperties;
subProperties.pass_values(*m_properties, "kdenlive:meta.exiftool.");
if (subProperties.count() > 0) {
QTreeWidgetItem *exif = new QTreeWidgetItem(tree, QStringList() << i18n("Exif") << QString());
exif->setExpanded(true);
for (int i = 0; i < subProperties.count(); i++) {
new QTreeWidgetItem(exif, QStringList() << subProperties.get_name(i) << subProperties.get(i));
}
}
} else if (KdenliveSettings::use_exiftool()) {
QString url = m_controller->clipUrl();
// Check for Canon THM file
url = url.section(QLatin1Char('.'), 0, -2) + QStringLiteral(".THM");
if (QFile::exists(url)) {
// Read the exif metadata embedded in the THM file
QProcess p;
QStringList args;
args << QStringLiteral("-g") << QStringLiteral("-args") << url;
p.start(QStringLiteral("exiftool"), args);
p.waitForFinished();
QString res = p.readAllStandardOutput();
m_controller->setProducerProperty(QStringLiteral("kdenlive:exiftool"), 1);
QTreeWidgetItem *exif = nullptr;
QStringList list = res.split(QLatin1Char('\n'));
for (const QString &tagline : list) {
if (tagline.startsWith(QLatin1String("-File")) || tagline.startsWith(QLatin1String("-ExifTool"))) {
continue;
}
QString tag = tagline.section(QLatin1Char(':'), 1).simplified();
if (tag.startsWith(QLatin1String("ImageWidth")) || tag.startsWith(QLatin1String("ImageHeight"))) {
continue;
}
if (!tag.section(QLatin1Char('='), 0, 0).isEmpty() && !tag.section(QLatin1Char('='), 1).simplified().isEmpty()) {
if (!exif) {
exif = new QTreeWidgetItem(tree, QStringList() << i18n("Exif") << QString());
exif->setExpanded(true);
}
m_controller->setProducerProperty("kdenlive:meta.exiftool." + tag.section(QLatin1Char('='), 0, 0),
tag.section(QLatin1Char('='), 1).simplified());
new QTreeWidgetItem(exif, QStringList() << tag.section(QLatin1Char('='), 0, 0) << tag.section(QLatin1Char('='), 1).simplified());
}
}
} else {
if (m_type == ClipType::Image || m_controller->codec(false) == QLatin1String("h264")) {
QProcess p;
QStringList args;
args << QStringLiteral("-g") << QStringLiteral("-args") << m_controller->clipUrl();
p.start(QStringLiteral("exiftool"), args);
p.waitForFinished();
QString res = p.readAllStandardOutput();
if (m_type != ClipType::Image) {
m_controller->setProducerProperty(QStringLiteral("kdenlive:exiftool"), 1);
}
QTreeWidgetItem *exif = nullptr;
QStringList list = res.split(QLatin1Char('\n'));
for (const QString &tagline : list) {
if (m_type != ClipType::Image && !tagline.startsWith(QLatin1String("-H264"))) {
continue;
}
QString tag = tagline.section(QLatin1Char(':'), 1);
if (tag.startsWith(QLatin1String("ImageWidth")) || tag.startsWith(QLatin1String("ImageHeight"))) {
continue;
}
if (!exif) {
exif = new QTreeWidgetItem(tree, QStringList() << i18n("Exif") << QString());
exif->setExpanded(true);
}
if (m_type != ClipType::Image) {
// Do not store image exif metadata in project file, would be too much noise
m_controller->setProducerProperty("kdenlive:meta.exiftool." + tag.section(QLatin1Char('='), 0, 0),
tag.section(QLatin1Char('='), 1).simplified());
}
new QTreeWidgetItem(exif, QStringList() << tag.section(QLatin1Char('='), 0, 0) << tag.section(QLatin1Char('='), 1).simplified());
}
}
}
}
int magic = m_controller->getProducerIntProperty(QStringLiteral("kdenlive:magiclantern"));
if (magic == 1) {
Mlt::Properties subProperties;
subProperties.pass_values(*m_properties, "kdenlive:meta.magiclantern.");
QTreeWidgetItem *magicL = nullptr;
for (int i = 0; i < subProperties.count(); i++) {
if (!magicL) {
magicL = new QTreeWidgetItem(tree, QStringList() << i18n("Magic Lantern") << QString());
QIcon icon(QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("meta_magiclantern.png")));
magicL->setIcon(0, icon);
magicL->setExpanded(true);
}
new QTreeWidgetItem(magicL, QStringList() << subProperties.get_name(i) << subProperties.get(i));
}
} else if (m_type != ClipType::Image && KdenliveSettings::use_magicLantern()) {
QString url = m_controller->clipUrl();
url = url.section(QLatin1Char('.'), 0, -2) + QStringLiteral(".LOG");
if (QFile::exists(url)) {
QFile file(url);
if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
m_controller->setProducerProperty(QStringLiteral("kdenlive:magiclantern"), 1);
QTreeWidgetItem *magicL = nullptr;
while (!file.atEnd()) {
QString line = file.readLine().simplified();
if (line.startsWith('#') || line.isEmpty() || !line.contains(QLatin1Char(':'))) {
continue;
}
if (line.startsWith(QLatin1String("CSV data"))) {
break;
}
m_controller->setProducerProperty("kdenlive:meta.magiclantern." + line.section(QLatin1Char(':'), 0, 0).simplified(),
line.section(QLatin1Char(':'), 1).simplified());
if (!magicL) {
magicL = new QTreeWidgetItem(tree, QStringList() << i18n("Magic Lantern") << QString());
QIcon icon(QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("meta_magiclantern.png")));
magicL->setIcon(0, icon);
magicL->setExpanded(true);
}
new QTreeWidgetItem(magicL, QStringList()
<< line.section(QLatin1Char(':'), 0, 0).simplified() << line.section(QLatin1Char(':'), 1).simplified());
}
}
}
// if (!meta.isEmpty())
// clip->setMetadata(meta, "Magic Lantern");
// clip->setProperty("magiclantern", "1");
}
tree->resizeColumnToContents(0);
}
void ClipPropertiesController::slotFillAnalysisData()
{
m_analysisTree->clear();
Mlt::Properties subProperties;
subProperties.pass_values(*m_properties, "kdenlive:clipanalysis.");
if (subProperties.count() > 0) {
for (int i = 0; i < subProperties.count(); i++) {
new QTreeWidgetItem(m_analysisTree, QStringList() << subProperties.get_name(i) << subProperties.get(i));
}
}
m_analysisTree->resizeColumnToContents(0);
}
void ClipPropertiesController::slotDeleteAnalysis()
{
QTreeWidgetItem *current = m_analysisTree->currentItem();
if (!current) {
return;
}
emit editAnalysis(m_id, "kdenlive:clipanalysis." + current->text(0), QString());
}
void ClipPropertiesController::slotSaveAnalysis()
{
const QString url =
QFileDialog::getSaveFileName(this, i18n("Save Analysis Data"), QFileInfo(m_controller->clipUrl()).absolutePath(), i18n("Text File (*.txt)"));
if (url.isEmpty()) {
return;
}
KSharedConfigPtr config = KSharedConfig::openConfig(url, KConfig::SimpleConfig);
KConfigGroup analysisConfig(config, "Analysis");
QTreeWidgetItem *current = m_analysisTree->currentItem();
analysisConfig.writeEntry(current->text(0), current->text(1));
}
void ClipPropertiesController::slotLoadAnalysis()
{
const QString url =
QFileDialog::getOpenFileName(this, i18n("Open Analysis Data"), QFileInfo(m_controller->clipUrl()).absolutePath(), i18n("Text File (*.txt)"));
if (url.isEmpty()) {
return;
}
KSharedConfigPtr config = KSharedConfig::openConfig(url, KConfig::SimpleConfig);
KConfigGroup transConfig(config, "Analysis");
// read the entries
QMap profiles = transConfig.entryMap();
QMapIterator i(profiles);
while (i.hasNext()) {
i.next();
emit editAnalysis(m_id, "kdenlive:clipanalysis." + i.key(), i.value());
}
}
void ClipPropertiesController::slotTextChanged()
{
QMap properties;
properties.insert(QStringLiteral("templatetext"), m_textEdit->toPlainText());
emit updateClipProperties(m_id, m_originalProperties, properties);
m_originalProperties = properties;
}