diff --git a/src/assets/keyframes/model/keyframemodellist.cpp b/src/assets/keyframes/model/keyframemodellist.cpp
index 833178fbc..2b3a4f419 100644
--- a/src/assets/keyframes/model/keyframemodellist.cpp
+++ b/src/assets/keyframes/model/keyframemodellist.cpp
@@ -1,502 +1,501 @@
/***************************************************************************
* 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 "keyframemodellist.hpp"
#include "assets/model/assetcommand.hpp"
#include "assets/model/assetparametermodel.hpp"
#include "core.h"
#include "doc/docundostack.hpp"
#include "keyframemodel.hpp"
#include "klocalizedstring.h"
#include "macros.hpp"
#include
#include
#include
KeyframeModelList::KeyframeModelList(std::weak_ptr model, const QModelIndex &index, std::weak_ptr undo_stack)
: m_model(std::move(model))
, m_undoStack(std::move(undo_stack))
, m_lock(QReadWriteLock::Recursive)
{
qDebug() << "Construct keyframemodellist. Checking model:" << m_model.expired();
addParameter(index);
connect(m_parameters.begin()->second.get(), &KeyframeModel::modelChanged, this, &KeyframeModelList::modelChanged);
}
ObjectId KeyframeModelList::getOwnerId() const
{
if (auto ptr = m_model.lock()) {
return ptr->getOwnerId();
}
return {};
}
void KeyframeModelList::addParameter(const QModelIndex &index)
{
std::shared_ptr parameter(new KeyframeModel(m_model, index, m_undoStack));
m_parameters.insert({index, std::move(parameter)});
}
bool KeyframeModelList::applyOperation(const std::function, Fun &, Fun &)> &op, const QString &undoString)
{
QWriteLocker locker(&m_lock);
Q_ASSERT(m_parameters.size() > 0);
Fun undo = []() { return true; };
Fun redo = []() { return true; };
bool res = true;
for (const auto ¶m : m_parameters) {
res = op(param.second, undo, redo);
if (!res) {
bool undone = undo();
Q_ASSERT(undone);
return res;
}
}
if (res && !undoString.isEmpty()) {
PUSH_UNDO(undo, redo, undoString);
}
return res;
}
bool KeyframeModelList::addKeyframe(GenTime pos, KeyframeType type)
{
QWriteLocker locker(&m_lock);
Q_ASSERT(m_parameters.size() > 0);
bool update = (m_parameters.begin()->second->hasKeyframe(pos) > 0);
auto op = [pos, type](std::shared_ptr param, Fun &undo, Fun &redo) {
QVariant value = param->getInterpolatedValue(pos);
return param->addKeyframe(pos, type, value, true, undo, redo);
};
return applyOperation(op, update ? i18n("Change keyframe type") : i18n("Add keyframe"));
}
bool KeyframeModelList::addKeyframe(int frame, double val)
{
QWriteLocker locker(&m_lock);
GenTime pos(frame, pCore->getCurrentFps());
Q_ASSERT(m_parameters.size() > 0);
bool update = (m_parameters.begin()->second->hasKeyframe(pos) > 0);
bool isRectParam = false;
if (m_inTimelineIndex.isValid()) {
if (auto ptr = m_model.lock()) {
auto tp = ptr->data(m_inTimelineIndex, AssetParameterModel::TypeRole).value();
if (tp == ParamType::AnimatedRect) {
isRectParam = true;
}
}
}
auto op = [this, pos, val, isRectParam](std::shared_ptr param, Fun &undo, Fun &redo) {
QVariant value;
if (m_inTimelineIndex.isValid()) {
if (m_parameters.at(m_inTimelineIndex) == param) {
if (isRectParam) {
value = param->getInterpolatedValue(pos);
value = param->updateInterpolated(value, val);
} else {
value = param->getNormalizedValue(val);
}
} else {
value = param->getInterpolatedValue(pos);
}
} else if (m_parameters.begin()->second == param) {
value = param->getNormalizedValue(val);
} else {
value = param->getInterpolatedValue(pos);
}
return param->addKeyframe(pos, (KeyframeType)KdenliveSettings::defaultkeyframeinterp(), value, true, undo, redo);
};
return applyOperation(op, update ? i18n("Change keyframe type") : i18n("Add keyframe"));
}
bool KeyframeModelList::removeKeyframe(GenTime pos)
{
QWriteLocker locker(&m_lock);
Q_ASSERT(m_parameters.size() > 0);
auto op = [pos](std::shared_ptr param, Fun &undo, Fun &redo) { return param->removeKeyframe(pos, undo, redo); };
return applyOperation(op, i18n("Delete keyframe"));
}
bool KeyframeModelList::removeAllKeyframes()
{
QWriteLocker locker(&m_lock);
Q_ASSERT(m_parameters.size() > 0);
auto op = [](std::shared_ptr param, Fun &undo, Fun &redo) { return param->removeAllKeyframes(undo, redo); };
return applyOperation(op, i18n("Delete all keyframes"));
}
bool KeyframeModelList::removeNextKeyframes(GenTime pos)
{
QWriteLocker locker(&m_lock);
Q_ASSERT(m_parameters.size() > 0);
auto op = [pos](std::shared_ptr param, Fun &undo, Fun &redo) { return param->removeNextKeyframes(pos, undo, redo); };
return applyOperation(op, i18n("Delete keyframes"));
}
bool KeyframeModelList::moveKeyframe(GenTime oldPos, GenTime pos, bool logUndo)
{
QWriteLocker locker(&m_lock);
Q_ASSERT(m_parameters.size() > 0);
auto op = [oldPos, pos](std::shared_ptr param, Fun &undo, Fun &redo) { return param->moveKeyframe(oldPos, pos, QVariant(), undo, redo); };
return applyOperation(op, logUndo ? i18n("Move keyframe") : QString());
}
bool KeyframeModelList::updateKeyframe(GenTime oldPos, GenTime pos, const QVariant &normalizedVal, bool logUndo)
{
QWriteLocker locker(&m_lock);
Q_ASSERT(m_parameters.size() > 0);
bool isRectParam = false;
if (m_inTimelineIndex.isValid()) {
if (auto ptr = m_model.lock()) {
auto tp = ptr->data(m_inTimelineIndex, AssetParameterModel::TypeRole).value();
if (tp == ParamType::AnimatedRect) {
isRectParam = true;
}
}
}
auto op = [this, oldPos, pos, normalizedVal, isRectParam](std::shared_ptr param, Fun &undo, Fun &redo) {
QVariant value;
if (m_inTimelineIndex.isValid()) {
if (m_parameters.at(m_inTimelineIndex) == param) {
if (isRectParam) {
if (normalizedVal.isValid()) {
value = param->getInterpolatedValue(oldPos);
value = param->updateInterpolated(value, normalizedVal.toDouble());
}
} else {
value = normalizedVal;
}
}
} else if (m_parameters.begin()->second == param) {
value = normalizedVal;
}
return param->moveKeyframe(oldPos, pos, value, undo, redo);
};
return applyOperation(op, logUndo ? i18n("Move keyframe") : QString());
}
bool KeyframeModelList::updateKeyframe(GenTime pos, const QVariant &value, const QPersistentModelIndex &index)
{
if (singleKeyframe()) {
bool ok = false;
Keyframe kf = m_parameters.begin()->second->getNextKeyframe(GenTime(-1), &ok);
pos = kf.first;
}
if (auto ptr = m_model.lock()) {
auto *command = new AssetKeyframeCommand(ptr, index, value, pos);
pCore->pushUndo(command);
}
return true;
QWriteLocker locker(&m_lock);
Q_ASSERT(m_parameters.count(index) > 0);
Fun undo = []() { return true; };
Fun redo = []() { return true; };
if (singleKeyframe()) {
bool ok = false;
Keyframe kf = m_parameters.begin()->second->getNextKeyframe(GenTime(-1), &ok);
pos = kf.first;
}
bool res = m_parameters.at(index)->updateKeyframe(pos, value, undo, redo);
if (res) {
PUSH_UNDO(undo, redo, i18n("Update keyframe"));
}
return res;
}
bool KeyframeModelList::updateKeyframeType(GenTime pos, int type, const QPersistentModelIndex &index)
{
QWriteLocker locker(&m_lock);
Q_ASSERT(m_parameters.count(index) > 0);
Fun undo = []() { return true; };
Fun redo = []() { return true; };
if (singleKeyframe()) {
bool ok = false;
Keyframe kf = m_parameters.begin()->second->getNextKeyframe(GenTime(-1), &ok);
pos = kf.first;
}
// Update kf type in all parameters
bool res = true;
for (const auto ¶m : m_parameters) {
res = res && param.second->updateKeyframeType(pos, type, undo, redo);
}
if (res) {
PUSH_UNDO(undo, redo, i18n("Update keyframe"));
}
return res;
}
KeyframeType KeyframeModelList::keyframeType(GenTime pos) const
{
QWriteLocker locker(&m_lock);
if (singleKeyframe()) {
bool ok = false;
Keyframe kf = m_parameters.begin()->second->getNextKeyframe(GenTime(-1), &ok);
return kf.second;
}
bool ok = false;
Keyframe kf = m_parameters.begin()->second->getKeyframe(pos, &ok);
return kf.second;
}
Keyframe KeyframeModelList::getKeyframe(const GenTime &pos, bool *ok) const
{
READ_LOCK();
Q_ASSERT(m_parameters.size() > 0);
return m_parameters.begin()->second->getKeyframe(pos, ok);
}
bool KeyframeModelList::singleKeyframe() const
{
READ_LOCK();
Q_ASSERT(m_parameters.size() > 0);
return m_parameters.begin()->second->singleKeyframe();
}
bool KeyframeModelList::isEmpty() const
{
READ_LOCK();
return (m_parameters.size() == 0 || m_parameters.begin()->second->rowCount() == 0);
}
Keyframe KeyframeModelList::getNextKeyframe(const GenTime &pos, bool *ok) const
{
READ_LOCK();
Q_ASSERT(m_parameters.size() > 0);
return m_parameters.begin()->second->getNextKeyframe(pos, ok);
}
Keyframe KeyframeModelList::getPrevKeyframe(const GenTime &pos, bool *ok) const
{
READ_LOCK();
Q_ASSERT(m_parameters.size() > 0);
return m_parameters.begin()->second->getPrevKeyframe(pos, ok);
}
Keyframe KeyframeModelList::getClosestKeyframe(const GenTime &pos, bool *ok) const
{
READ_LOCK();
Q_ASSERT(m_parameters.size() > 0);
return m_parameters.begin()->second->getClosestKeyframe(pos, ok);
}
bool KeyframeModelList::hasKeyframe(int frame) const
{
READ_LOCK();
Q_ASSERT(m_parameters.size() > 0);
return m_parameters.begin()->second->hasKeyframe(frame);
}
void KeyframeModelList::refresh()
{
QWriteLocker locker(&m_lock);
for (const auto ¶m : m_parameters) {
param.second->refresh();
}
}
void KeyframeModelList::reset()
{
QWriteLocker locker(&m_lock);
for (const auto ¶m : m_parameters) {
param.second->reset();
}
}
QVariant KeyframeModelList::getInterpolatedValue(int pos, const QPersistentModelIndex &index) const
{
READ_LOCK();
Q_ASSERT(m_parameters.count(index) > 0);
return m_parameters.at(index)->getInterpolatedValue(pos);
}
KeyframeModel *KeyframeModelList::getKeyModel()
{
if (m_inTimelineIndex.isValid()) {
return m_parameters.at(m_inTimelineIndex).get();
}
if (auto ptr = m_model.lock()) {
for (const auto ¶m : m_parameters) {
if (ptr->data(param.first, AssetParameterModel::ShowInTimelineRole) == true) {
m_inTimelineIndex = param.first;
return param.second.get();
}
}
}
return nullptr;
}
KeyframeModel *KeyframeModelList::getKeyModel(const QPersistentModelIndex &index)
{
if (m_parameters.size() > 0 && m_parameters.find(index) != m_parameters.end()) {
return m_parameters.at(index).get();
}
return nullptr;
}
void KeyframeModelList::resizeKeyframes(int oldIn, int oldOut, int in, int out, int offset, bool adjustFromEnd, Fun &undo, Fun &redo)
{
bool ok;
bool ok2;
QList positions;
if (!adjustFromEnd) {
if (offset != 0) {
// this is an endless resize clip
GenTime old_in(oldIn, pCore->getCurrentFps());
GenTime new_in(in + offset, pCore->getCurrentFps());
getKeyframe(new_in, &ok2);
positions = m_parameters.begin()->second->getKeyframePos();
std::sort(positions.begin(), positions.end());
for (const auto ¶m : m_parameters) {
if (offset > 0) {
QVariant value = param.second->getInterpolatedValue(new_in);
param.second->updateKeyframe(old_in, value, undo, redo);
}
for (auto frame : positions) {
if (new_in > GenTime()) {
if (frame > new_in) {
param.second->moveKeyframe(frame, frame - new_in, QVariant(), undo, redo);
continue;
}
} else if (frame > GenTime()) {
param.second->moveKeyframe(frame, frame - new_in, QVariant(), undo, redo);
continue;
}
if (frame != GenTime()) {
param.second->removeKeyframe(frame, undo, redo);
}
}
}
} else if (oldIn != in) {
GenTime old_in(oldIn, pCore->getCurrentFps());
GenTime new_in(in, pCore->getCurrentFps());
Keyframe kf = getKeyframe(old_in, &ok);
KeyframeType type = kf.second;
getKeyframe(new_in, &ok2);
if (!ok2) {
// Add new in point
for (const auto ¶m : m_parameters) {
QVariant value = param.second->getInterpolatedValue(new_in);
param.second->addKeyframe(new_in, type, value, true, undo, redo);
}
}
if (ok) {
// Remove previous in point
for (const auto ¶m : m_parameters) {
param.second->removeKeyframe(old_in, undo, redo);
}
}
// Remove all keyframes before in
bool nextOk = false;
kf = m_parameters.begin()->second->getNextKeyframe(GenTime(-1), &nextOk);
GenTime pos;
while (nextOk) {
pos = kf.first;
if (pos < new_in) {
for (const auto ¶m : m_parameters) {
param.second->removeKeyframe(pos, undo, redo);
}
kf = m_parameters.begin()->second->getNextKeyframe(pos, &nextOk);
} else {
break;
}
}
// qDebug()<<"/// \n\nKEYS TO DELETE: "<getCurrentFps());
GenTime new_out(out, pCore->getCurrentFps());
Keyframe kf = getKeyframe(old_out, &ok);
KeyframeType type = kf.second;
getKeyframe(new_out, &ok2);
// Check keyframes after last position
bool ok3;
Keyframe toDel = getNextKeyframe(new_out, &ok3);
if (ok && !ok2) {
// Check if we have only 2 keyframes (in/out), in which case we move the out keyframe to new position
bool ok4;
kf = getPrevKeyframe(old_out, &ok4);
if (ok4) {
GenTime current_in(oldIn, pCore->getCurrentFps());
qDebug()<<" = = = = = = = \n\nGOT 2 KF SITUATION: "<moveKeyframe(old_out, new_out, QVariant(), undo, redo);
}
return;
}
}
positions << old_out;
}
if (toDel.first == GenTime()) {
// No keyframes
return;
}
while (ok3) {
if (!positions.contains(toDel.first)) {
positions << toDel.first;
}
toDel = getNextKeyframe(toDel.first, &ok3);
}
if ((ok || positions.size() > 0) && !ok2) {
for (const auto ¶m : m_parameters) {
QVariant value = param.second->getInterpolatedValue(new_out);
param.second->addKeyframe(new_out, type, value, true, undo, redo);
for (auto frame : positions) {
param.second->removeKeyframe(frame, undo, redo);
}
}
}
}
}
void KeyframeModelList::checkConsistency()
{
if (m_parameters.size() < 2) {
return;
}
// Check keyframes in all parameters
QList fullList;
for (const auto ¶m : m_parameters) {
QList list = param.second->getKeyframePos();
for (auto &time : list) {
if (!fullList.contains(time)) {
fullList << time;
}
}
}
Fun local_update = []() { return true; };
KeyframeType type = (KeyframeType)KdenliveSettings::defaultkeyframeinterp();
for (const auto ¶m : m_parameters) {
QList list = param.second->getKeyframePos();
for (auto &time : fullList) {
if (!list.contains(time)) {
qDebug()<<" = = = \n\n = = = = \n\nWARNING; MISSING KF DETECTED AT: "<displayMessage(i18n("Keyframe interpolation"), ErrorMessage);
+ pCore->displayMessage(i18n("Missing keyframe detected at %1, automatically re-added", time.seconds()), ErrorMessage);
QVariant missingVal = param.second->getInterpolatedValue(time);
local_update = param.second->addKeyframe_lambda(time, type, missingVal, false);
local_update();
}
}
}
}
diff --git a/src/assets/model/assetparametermodel.cpp b/src/assets/model/assetparametermodel.cpp
index c761a55c5..1922f35e4 100644
--- a/src/assets/model/assetparametermodel.cpp
+++ b/src/assets/model/assetparametermodel.cpp
@@ -1,865 +1,870 @@
/***************************************************************************
* 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 "assetparametermodel.hpp"
#include "assets/keyframes/model/keyframemodellist.hpp"
#include "core.h"
#include "kdenlivesettings.h"
#include "klocalizedstring.h"
#include "profiles/profilemodel.hpp"
#include
+#include
#include
#include
#include
#include
AssetParameterModel::AssetParameterModel(std::unique_ptr asset, const QDomElement &assetXml, const QString &assetId, ObjectId ownerId,
QObject *parent)
: QAbstractListModel(parent)
, monitorId(ownerId.first == ObjectType::BinClip ? Kdenlive::ClipMonitor : Kdenlive::ProjectMonitor)
, m_assetId(assetId)
, m_ownerId(ownerId)
, m_asset(std::move(asset))
, m_keyframes(nullptr)
{
Q_ASSERT(m_asset->is_valid());
QDomNodeList nodeList = assetXml.elementsByTagName(QStringLiteral("parameter"));
m_hideKeyframesByDefault = assetXml.hasAttribute(QStringLiteral("hideKeyframes"));
m_isAudio = assetXml.attribute(QStringLiteral("type")) == QLatin1String("audio");
bool needsLocaleConversion = false;
QChar separator, oldSeparator;
// Check locale, default effects xml has no LC_NUMERIC defined and always uses the C locale
QLocale locale;
if (assetXml.hasAttribute(QStringLiteral("LC_NUMERIC"))) {
QLocale effectLocale = QLocale(assetXml.attribute(QStringLiteral("LC_NUMERIC")));
if (QLocale::c().decimalPoint() != effectLocale.decimalPoint()) {
needsLocaleConversion = true;
separator = QLocale::c().decimalPoint();
oldSeparator = effectLocale.decimalPoint();
}
}
qDebug() << "XML parsing of " << assetId << ". found : " << nodeList.count();
for (int i = 0; i < nodeList.count(); ++i) {
QDomElement currentParameter = nodeList.item(i).toElement();
// Convert parameters if we need to
if (needsLocaleConversion) {
QDomNamedNodeMap attrs = currentParameter.attributes();
for (int k = 0; k < attrs.count(); ++k) {
QString nodeName = attrs.item(k).nodeName();
if (nodeName != QLatin1String("type") && nodeName != QLatin1String("name")) {
QString val = attrs.item(k).nodeValue();
if (val.contains(oldSeparator)) {
QString newVal = val.replace(oldSeparator, separator);
attrs.item(k).setNodeValue(newVal);
}
}
}
}
// Parse the basic attributes of the parameter
QString name = currentParameter.attribute(QStringLiteral("name"));
QString type = currentParameter.attribute(QStringLiteral("type"));
QString value = currentParameter.attribute(QStringLiteral("value"));
ParamRow currentRow;
currentRow.type = paramTypeFromStr(type);
currentRow.xml = currentParameter;
if (value.isEmpty()) {
QVariant defaultValue = parseAttribute(m_ownerId, QStringLiteral("default"), currentParameter);
value = defaultValue.type() == QVariant::Double ? locale.toString(defaultValue.toDouble()) : defaultValue.toString();
}
bool isFixed = (type == QLatin1String("fixed"));
if (isFixed) {
m_fixedParams[name] = value;
} else if (currentRow.type == ParamType::Position) {
int val = value.toInt();
if (val < 0) {
int in = pCore->getItemIn(m_ownerId);
int out = in + pCore->getItemDuration(m_ownerId) - 1;
val += out;
value = QString::number(val);
}
} else if (currentRow.type == ParamType::KeyframeParam || currentRow.type == ParamType::AnimatedRect) {
if (!value.contains(QLatin1Char('='))) {
value.prepend(QStringLiteral("%1=").arg(pCore->getItemIn(m_ownerId)));
}
}
if (!name.isEmpty()) {
internalSetParameter(name, value);
// Keep track of param order
m_paramOrder.push_back(name);
}
if (isFixed) {
// fixed parameters are not displayed so we don't store them.
continue;
}
currentRow.value = value;
QString title = i18n(currentParameter.firstChildElement(QStringLiteral("name")).text().toUtf8().data());
currentRow.name = title.isEmpty() ? name : title;
m_params[name] = currentRow;
m_rows.push_back(name);
}
if (m_assetId.startsWith(QStringLiteral("sox_"))) {
// Sox effects need to have a special "Effect" value set
QStringList effectParam = {m_assetId.section(QLatin1Char('_'), 1)};
for (const QString &pName : m_paramOrder) {
effectParam << m_asset->get(pName.toUtf8().constData());
}
m_asset->set("effect", effectParam.join(QLatin1Char(' ')).toUtf8().constData());
}
qDebug() << "END parsing of " << assetId << ". Number of found parameters" << m_rows.size();
emit modelChanged();
}
void AssetParameterModel::prepareKeyframes()
{
if (m_keyframes) return;
int ix = 0;
for (const auto &name : m_rows) {
if (m_params.at(name).type == ParamType::KeyframeParam || m_params.at(name).type == ParamType::AnimatedRect ||
m_params.at(name).type == ParamType::Roto_spline) {
addKeyframeParam(index(ix, 0));
}
ix++;
}
if (m_keyframes) {
// Make sure we have keyframes at same position for all parameters
m_keyframes->checkConsistency();
}
}
QStringList AssetParameterModel::getKeyframableParameters() const
{
QStringList paramNames;
int ix = 0;
for (const auto &name : m_rows) {
if (m_params.at(name).type == ParamType::KeyframeParam || m_params.at(name).type == ParamType::AnimatedRect) {
//addKeyframeParam(index(ix, 0));
paramNames << name;
}
ix++;
}
return paramNames;
}
void AssetParameterModel::setParameter(const QString &name, int value, bool update)
{
Q_ASSERT(m_asset->is_valid());
m_asset->set(name.toLatin1().constData(), value);
if (m_fixedParams.count(name) == 0) {
m_params[name].value = value;
} else {
m_fixedParams[name] = value;
}
if (m_assetId.startsWith(QStringLiteral("sox_"))) {
// Warning, SOX effect, need unplug/replug
qDebug() << "// Warning, SOX effect, need unplug/replug";
QStringList effectParam = {m_assetId.section(QLatin1Char('_'), 1)};
for (const QString &pName : m_paramOrder) {
effectParam << m_asset->get(pName.toUtf8().constData());
}
m_asset->set("effect", effectParam.join(QLatin1Char(' ')).toUtf8().constData());
emit replugEffect(shared_from_this());
} else if (m_assetId == QLatin1String("autotrack_rectangle") || m_assetId.startsWith(QStringLiteral("ladspa"))) {
// these effects don't understand param change and need to be rebuild
emit replugEffect(shared_from_this());
}
if (update) {
emit modelChanged();
emit dataChanged(index(0, 0), index(m_rows.count() - 1, 0), {});
// Update fades in timeline
pCore->updateItemModel(m_ownerId, m_assetId);
if (!m_isAudio) {
// Trigger monitor refresh
pCore->refreshProjectItem(m_ownerId);
// Invalidate timeline preview
pCore->invalidateItem(m_ownerId);
}
}
}
void AssetParameterModel::internalSetParameter(const QString &name, const QString ¶mValue, const QModelIndex ¶mIndex)
{
Q_ASSERT(m_asset->is_valid());
QLocale locale;
locale.setNumberOptions(QLocale::OmitGroupSeparator);
// TODO: this does not really belong here, but I don't see another way to do it so that undo works
if (data(paramIndex, AssetParameterModel::TypeRole).value() == ParamType::Curve) {
QStringList vals = paramValue.split(QLatin1Char(';'), QString::SkipEmptyParts);
int points = vals.size();
m_asset->set("3", points / 10.);
// for the curve, inpoints are numbered: 6, 8, 10, 12, 14
// outpoints, 7, 9, 11, 13,15 so we need to deduce these enums
for (int i = 0; i < points; i++) {
const QString &pointVal = vals.at(i);
int idx = 2 * i + 6;
m_asset->set(QString::number(idx).toLatin1().constData(), pointVal.section(QLatin1Char('/'), 0, 0).toDouble());
idx++;
m_asset->set(QString::number(idx).toLatin1().constData(), pointVal.section(QLatin1Char('/'), 1, 1).toDouble());
}
}
bool conversionSuccess;
double doubleValue = locale.toDouble(paramValue, &conversionSuccess);
if (conversionSuccess) {
m_asset->set(name.toLatin1().constData(), doubleValue);
if (m_fixedParams.count(name) == 0) {
m_params[name].value = doubleValue;
} else {
m_fixedParams[name] = doubleValue;
}
} else {
m_asset->set(name.toLatin1().constData(), paramValue.toUtf8().constData());
qDebug() << " = = SET EFFECT PARAM: " << name << " = " << paramValue;
if (m_fixedParams.count(name) == 0) {
m_params[name].value = paramValue;
if (m_keyframes) {
KeyframeModel *km = m_keyframes->getKeyModel(paramIndex);
if (km) {
km->refresh();
}
//m_keyframes->refresh();
}
} else {
m_fixedParams[name] = paramValue;
}
}
}
void AssetParameterModel::setParameter(const QString &name, const QString ¶mValue, bool update, const QModelIndex ¶mIndex)
{
//qDebug() << "// PROCESSING PARAM CHANGE: " << name << ", UPDATE: " << update << ", VAL: " << paramValue;
internalSetParameter(name, paramValue, paramIndex);
bool updateChildRequired = true;
if (m_assetId.startsWith(QStringLiteral("sox_"))) {
// Warning, SOX effect, need unplug/replug
QStringList effectParam = {m_assetId.section(QLatin1Char('_'), 1)};
for (const QString &pName : m_paramOrder) {
effectParam << m_asset->get(pName.toUtf8().constData());
}
m_asset->set("effect", effectParam.join(QLatin1Char(' ')).toUtf8().constData());
emit replugEffect(shared_from_this());
updateChildRequired = false;
} else if (m_assetId == QLatin1String("autotrack_rectangle") || m_assetId.startsWith(QStringLiteral("ladspa"))) {
// these effects don't understand param change and need to be rebuild
emit replugEffect(shared_from_this());
updateChildRequired = false;
} else if (update) {
qDebug() << "// SENDING DATA CHANGE....";
if (paramIndex.isValid()) {
emit dataChanged(paramIndex, paramIndex);
} else {
QModelIndex ix = index(m_rows.indexOf(name), 0);
emit dataChanged(ix, ix);
}
emit modelChanged();
}
if (updateChildRequired) {
emit updateChildren(name);
}
// Update timeline view if necessary
if (m_ownerId.first == ObjectType::NoItem) {
// Used for generator clips
if (!update) emit modelChanged();
} else {
// Update fades in timeline
pCore->updateItemModel(m_ownerId, m_assetId);
if (!m_isAudio) {
// Trigger monitor refresh
pCore->refreshProjectItem(m_ownerId);
// Invalidate timeline preview
pCore->invalidateItem(m_ownerId);
}
}
}
AssetParameterModel::~AssetParameterModel() = default;
QVariant AssetParameterModel::data(const QModelIndex &index, int role) const
{
if (index.row() < 0 || index.row() >= m_rows.size() || !index.isValid()) {
return QVariant();
}
QString paramName = m_rows[index.row()];
Q_ASSERT(m_params.count(paramName) > 0);
const QDomElement &element = m_params.at(paramName).xml;
switch (role) {
case Qt::DisplayRole:
case Qt::EditRole:
return m_params.at(paramName).name;
case NameRole:
return paramName;
case TypeRole:
return QVariant::fromValue(m_params.at(paramName).type);
case CommentRole: {
QDomElement commentElem = element.firstChildElement(QStringLiteral("comment"));
QString comment;
if (!commentElem.isNull()) {
comment = i18n(commentElem.text().toUtf8().data());
}
return comment;
}
case InRole:
return m_asset->get_int("in");
case OutRole:
return m_asset->get_int("out");
case ParentInRole:
return pCore->getItemIn(m_ownerId);
case ParentDurationRole:
return pCore->getItemDuration(m_ownerId);
case ParentPositionRole:
return pCore->getItemPosition(m_ownerId);
case HideKeyframesFirstRole:
return m_hideKeyframesByDefault;
case MinRole:
return parseAttribute(m_ownerId, QStringLiteral("min"), element);
case MaxRole:
return parseAttribute(m_ownerId, QStringLiteral("max"), element);
case FactorRole:
return parseAttribute(m_ownerId, QStringLiteral("factor"), element, 1);
case ScaleRole:
return parseAttribute(m_ownerId, QStringLiteral("scale"), element, 0);
case DecimalsRole:
return parseAttribute(m_ownerId, QStringLiteral("decimals"), element);
case OddRole:
return element.attribute(QStringLiteral("odd")) == QLatin1String("1");
case DefaultRole:
return parseAttribute(m_ownerId, QStringLiteral("default"), element);
case FilterRole:
return parseAttribute(m_ownerId, QStringLiteral("filter"), element);
case FilterParamsRole:
return parseAttribute(m_ownerId, QStringLiteral("filterparams"), element);
case FilterJobParamsRole:
return parseSubAttributes(QStringLiteral("jobparam"), element);
case AlternateNameRole: {
QDomNode child = element.firstChildElement(QStringLiteral("name"));
if (child.toElement().hasAttribute(QStringLiteral("conditional"))) {
return child.toElement().attribute(QStringLiteral("conditional"));
}
return m_params.at(paramName).name;
}
case SuffixRole:
return element.attribute(QStringLiteral("suffix"));
case OpacityRole:
return element.attribute(QStringLiteral("opacity")) != QLatin1String("false");
case RelativePosRole:
return element.attribute(QStringLiteral("relative")) == QLatin1String("true");
case ShowInTimelineRole:
return !element.hasAttribute(QStringLiteral("notintimeline"));
case AlphaRole:
return element.attribute(QStringLiteral("alpha")) == QLatin1String("1");
case ValueRole: {
QString value(m_asset->get(paramName.toUtf8().constData()));
return value.isEmpty() ? (element.attribute(QStringLiteral("value")).isNull() ? parseAttribute(m_ownerId, QStringLiteral("default"), element)
: element.attribute(QStringLiteral("value")))
: value;
}
case ListValuesRole:
return element.attribute(QStringLiteral("paramlist")).split(QLatin1Char(';'));
case ListNamesRole: {
QDomElement namesElem = element.firstChildElement(QStringLiteral("paramlistdisplay"));
return i18n(namesElem.text().toUtf8().data()).split(QLatin1Char(','));
}
case List1Role:
return parseAttribute(m_ownerId, QStringLiteral("list1"), element);
case List2Role:
return parseAttribute(m_ownerId, QStringLiteral("list2"), element);
case Enum1Role:
return m_asset->get_double("1");
case Enum2Role:
return m_asset->get_double("2");
case Enum3Role:
return m_asset->get_double("3");
case Enum4Role:
return m_asset->get_double("4");
case Enum5Role:
return m_asset->get_double("5");
case Enum6Role:
return m_asset->get_double("6");
case Enum7Role:
return m_asset->get_double("7");
case Enum8Role:
return m_asset->get_double("8");
case Enum9Role:
return m_asset->get_double("9");
case Enum10Role:
return m_asset->get_double("10");
case Enum11Role:
return m_asset->get_double("11");
case Enum12Role:
return m_asset->get_double("12");
case Enum13Role:
return m_asset->get_double("13");
case Enum14Role:
return m_asset->get_double("14");
case Enum15Role:
return m_asset->get_double("15");
}
return QVariant();
}
int AssetParameterModel::rowCount(const QModelIndex &parent) const
{
qDebug() << "===================================================== Requested rowCount" << parent << m_rows.size();
if (parent.isValid()) return 0;
return m_rows.size();
}
// static
ParamType AssetParameterModel::paramTypeFromStr(const QString &type)
{
if (type == QLatin1String("double") || type == QLatin1String("float") || type == QLatin1String("constant")) {
return ParamType::Double;
}
if (type == QLatin1String("list")) {
return ParamType::List;
}
if (type == QLatin1String("bool")) {
return ParamType::Bool;
}
if (type == QLatin1String("switch")) {
return ParamType::Switch;
} else if (type == QLatin1String("simplekeyframe")) {
return ParamType::KeyframeParam;
} else if (type == QLatin1String("animatedrect")) {
return ParamType::AnimatedRect;
} else if (type == QLatin1String("geometry")) {
return ParamType::Geometry;
} else if (type == QLatin1String("addedgeometry")) {
return ParamType::Addedgeometry;
} else if (type == QLatin1String("keyframe") || type == QLatin1String("animated")) {
return ParamType::KeyframeParam;
} else if (type == QLatin1String("color")) {
return ParamType::Color;
} else if (type == QLatin1String("colorwheel")) {
return ParamType::ColorWheel;
} else if (type == QLatin1String("position")) {
return ParamType::Position;
} else if (type == QLatin1String("curve")) {
return ParamType::Curve;
} else if (type == QLatin1String("bezier_spline")) {
return ParamType::Bezier_spline;
} else if (type == QLatin1String("roto-spline")) {
return ParamType::Roto_spline;
} else if (type == QLatin1String("wipe")) {
return ParamType::Wipe;
} else if (type == QLatin1String("url")) {
return ParamType::Url;
} else if (type == QLatin1String("keywords")) {
return ParamType::Keywords;
} else if (type == QLatin1String("fontfamily")) {
return ParamType::Fontfamily;
} else if (type == QLatin1String("filterjob")) {
return ParamType::Filterjob;
} else if (type == QLatin1String("readonly")) {
return ParamType::Readonly;
} else if (type == QLatin1String("hidden")) {
return ParamType::Hidden;
}
qDebug() << "WARNING: Unknown type :" << type;
return ParamType::Double;
}
// static
QString AssetParameterModel::getDefaultKeyframes(int start, const QString &defaultValue, bool linearOnly)
{
QString keyframes = QString::number(start);
if (linearOnly) {
keyframes.append(QLatin1Char('='));
} else {
switch (KdenliveSettings::defaultkeyframeinterp()) {
case mlt_keyframe_discrete:
keyframes.append(QStringLiteral("|="));
break;
case mlt_keyframe_smooth:
keyframes.append(QStringLiteral("~="));
break;
default:
keyframes.append(QLatin1Char('='));
break;
}
}
keyframes.append(defaultValue);
return keyframes;
}
// static
QVariant AssetParameterModel::parseAttribute(const ObjectId &owner, const QString &attribute, const QDomElement &element, QVariant defaultValue)
{
if (!element.hasAttribute(attribute) && !defaultValue.isNull()) {
return defaultValue;
}
ParamType type = paramTypeFromStr(element.attribute(QStringLiteral("type")));
QString content = element.attribute(attribute);
if (content.contains(QLatin1Char('%'))) {
std::unique_ptr &profile = pCore->getCurrentProfile();
int width = profile->width();
int height = profile->height();
int in = pCore->getItemIn(owner);
int out = in + pCore->getItemDuration(owner);
int frame_duration = pCore->getDurationFromString(KdenliveSettings::fade_duration());
// replace symbols in the double parameter
content.replace(QLatin1String("%maxWidth"), QString::number(width))
.replace(QLatin1String("%maxHeight"), QString::number(height))
.replace(QLatin1String("%width"), QString::number(width))
.replace(QLatin1String("%height"), QString::number(height))
.replace(QLatin1String("%out"), QString::number(out))
.replace(QLatin1String("%fade"), QString::number(frame_duration));
if (type == ParamType::Double || type == ParamType::Hidden) {
// Use a Mlt::Properties to parse mathematical operators
Mlt::Properties p;
p.set("eval", content.prepend(QLatin1Char('@')).toLatin1().constData());
return p.get_double("eval");
}
} else if (type == ParamType::Double || type == ParamType::Hidden) {
if (attribute == QLatin1String("default")) {
return content.toDouble();
}
QLocale locale;
locale.setNumberOptions(QLocale::OmitGroupSeparator);
return locale.toDouble(content);
}
if (attribute == QLatin1String("default")) {
if (type == ParamType::RestrictedAnim) {
content = getDefaultKeyframes(0, content, true);
} else if (type == ParamType::KeyframeParam) {
return content.toDouble();
} else if (type == ParamType::List) {
bool ok;
double res = content.toDouble(&ok);
if (ok) {
return res;
}
return defaultValue.isNull() ? content : defaultValue;
} else if (type == ParamType::Bezier_spline) {
QLocale locale;
if (locale.decimalPoint() != QLocale::c().decimalPoint()) {
return content.replace(QLocale::c().decimalPoint(), locale.decimalPoint());
}
}
}
return content;
}
QVariant AssetParameterModel::parseSubAttributes(const QString &attribute, const QDomElement &element) const
{
QDomNodeList nodeList = element.elementsByTagName(attribute);
if (nodeList.isEmpty()) {
return QVariant();
}
QVariantList jobDataList;
for (int i = 0; i < nodeList.count(); ++i) {
QDomElement currentParameter = nodeList.item(i).toElement();
QStringList jobData {currentParameter.attribute(QStringLiteral("name")), currentParameter.text()};
jobDataList << jobData;
}
return jobDataList;
}
QString AssetParameterModel::getAssetId() const
{
return m_assetId;
}
QVector> AssetParameterModel::getAllParameters() const
{
QVector> res;
res.reserve((int)m_fixedParams.size() + (int)m_params.size());
for (const auto &fixed : m_fixedParams) {
res.push_back(QPair(fixed.first, fixed.second));
}
for (const auto ¶m : m_params) {
res.push_back(QPair(param.first, param.second.value));
}
return res;
}
QJsonDocument AssetParameterModel::toJson(bool includeFixed) const
{
QJsonArray list;
QLocale locale;
if (includeFixed) {
for (const auto &fixed : m_fixedParams) {
QJsonObject currentParam;
QModelIndex ix = index(m_rows.indexOf(fixed.first), 0);
currentParam.insert(QLatin1String("name"), QJsonValue(fixed.first));
currentParam.insert(QLatin1String("value"), fixed.second.type() == QVariant::Double ? QJsonValue(fixed.second.toDouble()) : QJsonValue(fixed.second.toString()));
int type = data(ix, AssetParameterModel::TypeRole).toInt();
double min = data(ix, AssetParameterModel::MinRole).toDouble();
double max = data(ix, AssetParameterModel::MaxRole).toDouble();
double factor = data(ix, AssetParameterModel::FactorRole).toDouble();
int in = data(ix, AssetParameterModel::ParentInRole).toInt();
int out = in + data(ix, AssetParameterModel::ParentDurationRole).toInt();
if (factor > 0) {
min /= factor;
max /= factor;
}
currentParam.insert(QLatin1String("type"), QJsonValue(type));
currentParam.insert(QLatin1String("min"), QJsonValue(min));
currentParam.insert(QLatin1String("max"), QJsonValue(max));
currentParam.insert(QLatin1String("in"), QJsonValue(in));
currentParam.insert(QLatin1String("out"), QJsonValue(out));
list.push_back(currentParam);
}
}
for (const auto ¶m : m_params) {
if (!includeFixed && param.second.type != ParamType::KeyframeParam && param.second.type != ParamType::AnimatedRect) {
continue;
}
QJsonObject currentParam;
QModelIndex ix = index(m_rows.indexOf(param.first), 0);
currentParam.insert(QLatin1String("name"), QJsonValue(param.first));
currentParam.insert(QLatin1String("value"), param.second.value.type() == QVariant::Double ? QJsonValue(param.second.value.toDouble()) : QJsonValue(param.second.value.toString()));
int type = data(ix, AssetParameterModel::TypeRole).toInt();
double min = data(ix, AssetParameterModel::MinRole).toDouble();
double max = data(ix, AssetParameterModel::MaxRole).toDouble();
double factor = data(ix, AssetParameterModel::FactorRole).toDouble();
int in = data(ix, AssetParameterModel::ParentInRole).toInt();
int out = in + data(ix, AssetParameterModel::ParentDurationRole).toInt();
if (factor > 0) {
min /= factor;
max /= factor;
}
currentParam.insert(QLatin1String("type"), QJsonValue(type));
currentParam.insert(QLatin1String("min"), QJsonValue(min));
currentParam.insert(QLatin1String("max"), QJsonValue(max));
currentParam.insert(QLatin1String("in"), QJsonValue(in));
currentParam.insert(QLatin1String("out"), QJsonValue(out));
list.push_back(currentParam);
}
return QJsonDocument(list);
}
void AssetParameterModel::deletePreset(const QString &presetFile, const QString &presetName)
{
QJsonObject object;
QJsonArray array;
QFile loadFile(presetFile);
if (loadFile.exists()) {
if (loadFile.open(QIODevice::ReadOnly)) {
QByteArray saveData = loadFile.readAll();
QJsonDocument loadDoc(QJsonDocument::fromJson(saveData));
if (loadDoc.isArray()) {
- qDebug() << " * * ** JSON IS AN ARRAY, DELETING: " << presetName;
array = loadDoc.array();
QList toDelete;
for (int i = 0; i < array.size(); i++) {
QJsonValue val = array.at(i);
if (val.isObject() && val.toObject().keys().contains(presetName)) {
toDelete << i;
}
}
for (int i : toDelete) {
array.removeAt(i);
}
} else if (loadDoc.isObject()) {
QJsonObject obj = loadDoc.object();
qDebug() << " * * ** JSON IS AN OBJECT, DELETING: " << presetName;
if (obj.keys().contains(presetName)) {
obj.remove(presetName);
} else {
qDebug() << " * * ** JSON DOES NOT CONTAIN: " << obj.keys();
}
array.append(obj);
}
loadFile.close();
- } else if (!loadFile.open(QIODevice::ReadWrite)) {
- // TODO: error message
}
}
if (!loadFile.open(QIODevice::WriteOnly)) {
- // TODO: error message
+ pCore->displayMessage(i18n("Cannot open preset file %1", presetFile), ErrorMessage);
+ return;
+ }
+ if (array.isEmpty()) {
+ QDir dir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/effects/presets/"));
+ if (dir.exists(presetFile)) {
+ // Ensure we don't delete an unwanted file
+ loadFile.remove();
+ }
+ } else {
+ loadFile.write(QJsonDocument(array).toJson());
}
- //TODO: delete file if there are no more presets in it
- loadFile.write(QJsonDocument(array).toJson());
}
void AssetParameterModel::savePreset(const QString &presetFile, const QString &presetName)
{
QJsonObject object;
QJsonArray array;
QJsonDocument doc = toJson();
QFile loadFile(presetFile);
if (loadFile.exists()) {
if (loadFile.open(QIODevice::ReadOnly)) {
QByteArray saveData = loadFile.readAll();
QJsonDocument loadDoc(QJsonDocument::fromJson(saveData));
if (loadDoc.isArray()) {
array = loadDoc.array();
QList toDelete;
for (int i = 0; i < array.size(); i++) {
QJsonValue val = array.at(i);
if (val.isObject() && val.toObject().keys().contains(presetName)) {
toDelete << i;
}
}
for (int i : toDelete) {
array.removeAt(i);
}
} else if (loadDoc.isObject()) {
QJsonObject obj = loadDoc.object();
if (obj.keys().contains(presetName)) {
obj.remove(presetName);
}
array.append(obj);
}
loadFile.close();
- } else if (!loadFile.open(QIODevice::ReadWrite)) {
- // TODO: error message
}
}
if (!loadFile.open(QIODevice::WriteOnly)) {
- // TODO: error message
+ pCore->displayMessage(i18n("Cannot open preset file %1", presetFile), ErrorMessage);
+ return;
}
object[presetName] = doc.array();
array.append(object);
loadFile.write(QJsonDocument(array).toJson());
}
const QStringList AssetParameterModel::getPresetList(const QString &presetFile) const
{
QFile loadFile(presetFile);
if (loadFile.exists() && loadFile.open(QIODevice::ReadOnly)) {
QByteArray saveData = loadFile.readAll();
QJsonDocument loadDoc(QJsonDocument::fromJson(saveData));
if (loadDoc.isObject()) {
qDebug() << "// PRESET LIST IS AN OBJECT!!!";
return loadDoc.object().keys();
} else if (loadDoc.isArray()) {
qDebug() << "// PRESET LIST IS AN ARRAY!!!";
QStringList result;
QJsonArray array = loadDoc.array();
for (auto &&i : array) {
QJsonValue val = i;
if (val.isObject()) {
result << val.toObject().keys();
}
}
return result;
}
}
return QStringList();
}
const QVector> AssetParameterModel::loadPreset(const QString &presetFile, const QString &presetName)
{
QFile loadFile(presetFile);
QVector> params;
if (loadFile.exists() && loadFile.open(QIODevice::ReadOnly)) {
QByteArray saveData = loadFile.readAll();
QJsonDocument loadDoc(QJsonDocument::fromJson(saveData));
if (loadDoc.isObject() && loadDoc.object().contains(presetName)) {
qDebug() << "..........\n..........\nLOADING OBJECT JSON";
QJsonValue val = loadDoc.object().value(presetName);
if (val.isObject()) {
QVariantMap map = val.toObject().toVariantMap();
QMap::const_iterator i = map.constBegin();
while (i != map.constEnd()) {
params.append({i.key(), i.value()});
++i;
}
}
} else if (loadDoc.isArray()) {
QJsonArray array = loadDoc.array();
for (auto &&i : array) {
QJsonValue val = i;
if (val.isObject() && val.toObject().contains(presetName)) {
QJsonValue preset = val.toObject().value(presetName);
if (preset.isArray()) {
QJsonArray paramArray = preset.toArray();
for (auto &&j : paramArray) {
QJsonValue v1 = j;
if (v1.isObject()) {
QJsonObject ob = v1.toObject();
params.append({ob.value("name").toString(), ob.value("value").toVariant()});
}
}
}
qDebug() << "// LOADED PRESET: " << presetName << "\n" << params;
break;
}
}
}
}
return params;
}
void AssetParameterModel::setParameters(const QVector> ¶ms)
{
QLocale locale;
for (const auto ¶m : params) {
if (param.second.type() == QVariant::Double) {
setParameter(param.first, locale.toString(param.second.toDouble()), false);
} else {
setParameter(param.first, param.second.toString(), false);
}
}
if (m_keyframes) {
m_keyframes->refresh();
}
emit dataChanged(index(0), index(m_rows.count()), {});
}
ObjectId AssetParameterModel::getOwnerId() const
{
return m_ownerId;
}
void AssetParameterModel::addKeyframeParam(const QModelIndex &index)
{
if (m_keyframes) {
m_keyframes->addParameter(index);
} else {
m_keyframes.reset(new KeyframeModelList(shared_from_this(), index, pCore->undoStack()));
}
}
std::shared_ptr AssetParameterModel::getKeyframeModel()
{
return m_keyframes;
}
void AssetParameterModel::resetAsset(std::unique_ptr asset)
{
m_asset = std::move(asset);
}
bool AssetParameterModel::hasMoreThanOneKeyframe() const
{
if (m_keyframes) {
return (!m_keyframes->isEmpty() && !m_keyframes->singleKeyframe());
}
return false;
}
int AssetParameterModel::time_to_frames(const QString &time)
{
return m_asset->time_to_frames(time.toUtf8().constData());
}
void AssetParameterModel::passProperties(Mlt::Properties &target)
{
target.set("_profile", pCore->getCurrentProfile()->get_profile(), 0);
target.set_lcnumeric(m_asset->get_lcnumeric());
}
diff --git a/src/jobs/cutclipjob.cpp b/src/jobs/cutclipjob.cpp
index 569c95a43..20b977af9 100644
--- a/src/jobs/cutclipjob.cpp
+++ b/src/jobs/cutclipjob.cpp
@@ -1,222 +1,221 @@
/***************************************************************************
* *
* Copyright (C) 2011 by Jean-Baptiste Mardelle (jb@kdenlive.org) *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program; if not, write to the *
* Free Software Foundation, Inc., *
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *
***************************************************************************/
#include "cutclipjob.h"
#include "bin/bin.h"
#include "bin/clipcreator.hpp"
#include "jobmanager.h"
#include "bin/projectclip.h"
#include "bin/projectitemmodel.h"
#include "core.h"
#include "doc/kdenlivedoc.h"
#include "kdenlive_debug.h"
#include "kdenlivesettings.h"
#include "macros.hpp"
#include "ui_cutjobdialog_ui.h"
#include
#include
#include
#include
#include
#include
CutClipJob::CutClipJob(const QString &binId, const QString sourcePath, GenTime inTime, GenTime outTime, const QString destPath, QStringList encodingParams)
: AbstractClipJob(CUTJOB, binId)
, m_sourceUrl(sourcePath)
, m_destUrl(destPath)
, m_done(false)
, m_jobProcess(nullptr)
, m_in(inTime)
, m_out(outTime)
, m_encodingParams(encodingParams)
, m_jobDuration((int)(outTime - inTime).seconds())
{
}
const QString CutClipJob::getDescription() const
{
- //TODO: add better description after string freeze
- return i18n("Extract Zone");
+ return i18n("Extract Clip Zone");
}
// static
int CutClipJob::prepareJob(const std::shared_ptr &ptr, const std::vector &binIds, int parentId, QString undoString, GenTime inTime, GenTime outTime)
{
if (binIds.empty()) {
return -1;
}
const QString mainId = *binIds.begin();
auto binClip = pCore->projectItemModel()->getClipByBinID(mainId);
ClipType::ProducerType type = binClip->clipType();
if (type != ClipType::AV && type != ClipType::Audio && type != ClipType::Video) {
//m_errorMessage.prepend(i18n("Cannot extract zone for this clip type."));
return -1;
}
if (KdenliveSettings::ffmpegpath().isEmpty()) {
// FFmpeg not detected, cannot process the Job
//m_errorMessage.prepend(i18n("Failed to create cut. FFmpeg not found, please set path in Kdenlive's settings Environment"));
return -1;
}
const QString source = binClip->url();
QString transcoderExt = source.section(QLatin1Char('.'), -1);
QFileInfo finfo(source);
QString fileName = finfo.fileName().section(QLatin1Char('.'), 0, -2);
QDir dir = finfo.absoluteDir();
QString inString(binClip->framesToTime(inTime.frames(pCore->getCurrentFps())));
QString outString(binClip->framesToTime(outTime.frames(pCore->getCurrentFps())));
QString path = dir.absoluteFilePath(fileName + QString("-%1-%2.").arg(inString).arg(outString) + transcoderExt);
QPointer d = new QDialog(QApplication::activeWindow());
Ui::CutJobDialog_UI ui;
ui.setupUi(d);
ui.extra_params->setVisible(false);
ui.add_clip->setChecked(KdenliveSettings::add_new_clip());
ui.file_url->setMode(KFile::File);
ui.extra_params->setMaximumHeight(QFontMetrics(QApplication::font()).lineSpacing() * 5);
ui.file_url->setUrl(QUrl::fromLocalFile(path));
QFontMetrics fm = ui.file_url->lineEdit()->fontMetrics();
ui.file_url->setMinimumWidth(fm.boundingRect(ui.file_url->text().left(50)).width() * 1.4);
ui.button_more->setIcon(QIcon::fromTheme(QStringLiteral("configure")));
ui.extra_params->setPlainText(QStringLiteral("-acodec copy -vcodec copy"));
QString mess = i18n("Extracting %1 out of %2", Timecode::getStringTimecode((outTime -inTime).frames(pCore->getCurrentFps()), pCore->getCurrentFps(), true), binClip->getStringDuration());
ui.info_label->setText(mess);
if (d->exec() != QDialog::Accepted) {
delete d;
return -1;
}
path = ui.file_url->url().toLocalFile();
QStringList encodingParams = ui.extra_params->toPlainText().split(QLatin1Char(' '), QString::SkipEmptyParts);
KdenliveSettings::setAdd_new_clip(ui.add_clip->isChecked());
delete d;
if (QFile::exists(path)) {
KIO::RenameDialog renameDialog(qApp->activeWindow(), i18n("File already exists"), QUrl::fromLocalFile(path), QUrl::fromLocalFile(path), KIO::RenameDialog_Option::RenameDialog_Overwrite );
if (renameDialog.exec() != QDialog::Rejected) {
QUrl final = renameDialog.newDestUrl();
if (final.isValid()) {
path = final.toLocalFile();
}
} else {
return -1;
}
}
return ptr->startJob_noprepare(binIds, parentId, std::move(undoString), source, inTime, outTime, dir.absoluteFilePath(path), encodingParams);
//return ptr->startJob_noprepare(binIds, parentId, std::move(undoString), mainId, source, inTime, outTime, dir.absoluteFilePath(path), encodingParams);
//return ptr->startJob(binIds, parentId, std::move(undoString), std::make_shared(mainId, source, inTime, outTime, dir.absoluteFilePath(path)));
}
bool CutClipJob::startJob()
{
QLocale locale;
bool result;
if (m_destUrl == m_sourceUrl) {
m_errorMessage.append(i18n("You cannot overwrite original clip."));
m_done = true;
return false;
}
QString startString = locale.toString(m_in.seconds());
QString durationString = locale.toString((m_out - m_in).seconds());
qDebug()<<"// STARTING CUT JOB FROM: "<(new QProcess);
connect(m_jobProcess.get(), &QProcess::readyReadStandardError, this, &CutClipJob::processLogInfo);
connect(this, &CutClipJob::jobCanceled, m_jobProcess.get(), &QProcess::kill, Qt::DirectConnection);
m_jobProcess->start(KdenliveSettings::ffmpegpath(), params, QIODevice::ReadOnly);
m_jobProcess->waitForFinished(-1);
result = m_jobProcess->exitStatus() == QProcess::NormalExit;
// remove temporary playlist if it exists
if (result) {
if (QFileInfo(m_destUrl).size() == 0) {
QFile::remove(m_destUrl);
// File was not created
m_done = false;
m_errorMessage.append(i18n("Failed to create file."));
} else {
m_done = true;
}
} else {
// Proxy process crashed
QFile::remove(m_destUrl);
m_done = false;
m_errorMessage.append(QString::fromUtf8(m_jobProcess->readAll()));
}
return result;
}
void CutClipJob::processLogInfo()
{
const QString buffer = QString::fromUtf8(m_jobProcess->readAllStandardError());
m_logDetails.append(buffer);
int progress = 0;
// Parse FFmpeg output
if (m_jobDuration == 0) {
if (buffer.contains(QLatin1String("Duration:"))) {
QString data = buffer.section(QStringLiteral("Duration:"), 1, 1).section(QLatin1Char(','), 0, 0).simplified();
if (!data.isEmpty()) {
QStringList numbers = data.split(QLatin1Char(':'));
if (numbers.size() < 3) {
return;
}
m_jobDuration = (int)(numbers.at(0).toInt() * 3600 + numbers.at(1).toInt() * 60 + numbers.at(2).toDouble());
}
}
} else if (buffer.contains(QLatin1String("time="))) {
QString time = buffer.section(QStringLiteral("time="), 1, 1).simplified().section(QLatin1Char(' '), 0, 0);
if (!time.isEmpty()) {
QStringList numbers = time.split(QLatin1Char(':'));
if (numbers.size() < 3) {
progress = (int)time.toDouble();
if (progress == 0) {
return;
}
} else {
progress = numbers.at(0).toInt() * 3600 + numbers.at(1).toInt() * 60 + numbers.at(2).toDouble();
}
}
emit jobProgress((int)(100.0 * progress / m_jobDuration));
}
}
bool CutClipJob::commitResult(Fun &undo, Fun &redo)
{
Q_ASSERT(!m_resultConsumed);
if (!m_done) {
qDebug() << "ERROR: Trying to consume invalid results";
return false;
}
m_resultConsumed = true;
if (!KdenliveSettings::add_new_clip()) {
return true;
}
QStringList ids = pCore->projectItemModel()->getClipByUrl(QFileInfo(m_destUrl));
if (!ids.isEmpty()) {
// Clip was already inserted in bin, will be reloaded automatically, don't add twice
return true;
}
QString folderId = QStringLiteral("-1");
auto id = ClipCreator::createClipFromFile(m_destUrl, folderId, pCore->projectItemModel(), undo, redo);
return id != QStringLiteral("-1");
}
diff --git a/src/jobs/filterclipjob.cpp b/src/jobs/filterclipjob.cpp
index 487d0a7a1..195a6af6a 100644
--- a/src/jobs/filterclipjob.cpp
+++ b/src/jobs/filterclipjob.cpp
@@ -1,123 +1,123 @@
/***************************************************************************
* *
* Copyright (C) 2019 by Jean-Baptiste Mardelle (jb@kdenlive.org) *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program; if not, write to the *
* Free Software Foundation, Inc., *
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *
***************************************************************************/
#include "filterclipjob.h"
#include "assets/model/assetparametermodel.hpp"
#include "bin/projectclip.h"
#include "bin/projectitemmodel.h"
#include "core.h"
#include "kdenlivesettings.h"
#include "macros.hpp"
#include
#include
FilterClipJob::FilterClipJob(const QString &binId, int cid, std::weak_ptr model, const QString &assetId, int in, int out, const QString &filterName, std::unordered_map filterParams, std::unordered_map filterData)
: MeltJob(binId, FILTERCLIPJOB, false, in, out)
, m_model(model)
, m_filterName(filterName)
, m_timelineClipId(cid)
, m_assetId(assetId)
, m_filterParams(std::move(filterParams))
, m_filterData(std::move(filterData))
{
}
const QString FilterClipJob::getDescription() const
{
//TODO: add better description after string freeze
- return i18n("Analyse clip");
+ return i18n("Apply Filter on Clip");
}
void FilterClipJob::configureConsumer()
{
m_consumer = std::make_unique(*m_profile.get(), "null");
m_consumer->set("all", 1);
m_consumer->set("terminate_on_pause", 1);
m_consumer->set("real_time", -KdenliveSettings::mltthreads());
}
void FilterClipJob::configureFilter()
{
m_filter = std::make_unique(*m_profile.get(), m_filterName.toUtf8().data());
if ((m_filter == nullptr) || !m_filter->is_valid()) {
m_errorMessage.append(i18n("Cannot create filter %1", m_filterName));
return;
}
// Process filter params
qDebug()<<" = = = = = CONFIGURING FILTER PARAMS = = = = = ";
for (const auto &it : m_filterParams) {
qDebug()<<". . ."<set(it.first.toUtf8().constData(), it.second.toDouble());
} else {
m_filter->set(it.first.toUtf8().constData(), it.second.toString().toUtf8().constData());
}
}
}
bool FilterClipJob::commitResult(Fun &undo, Fun &redo)
{
Q_ASSERT(!m_resultConsumed);
if (!m_done) {
qDebug() << "ERROR: Trying to consume invalid results";
return false;
}
m_resultConsumed = true;
if (!m_successful) {
return false;
}
QVector> params;
QString key("results");
if (m_filterData.find(QStringLiteral("key")) != m_filterData.end()) {
key = m_filterData.at(QStringLiteral("key"));
}
QString resultData = QString::fromLatin1(m_filter->get(key.toUtf8().constData()));
params.append({key,QVariant(resultData)});
qDebug()<<"= = = GOT FILTER RESULTS: "<projectItemModel()->getClipByBinID(m_clipId);
binClip->updatedAnalysisData(dataName, resultData, m_in);
}
auto operation = [assetModel = m_model, filterParams = std::move(params)]() {
if (auto ptr = assetModel.lock()) {
ptr->setParameters(filterParams);
}
return true;
};
auto reverse = [assetModel = m_model, keyName = key]() {
QVector> fParams;
fParams.append({keyName,QVariant()});
if (auto ptr = assetModel.lock()) {
ptr->setParameters(fParams);
}
return true;
};
bool ok = operation();
if (ok) {
UPDATE_UNDO_REDO_NOLOCK(operation, reverse, undo, redo);
}
return true;
}
diff --git a/src/jobs/transcodeclipjob.cpp b/src/jobs/transcodeclipjob.cpp
index 02539e4bf..5e9befd80 100644
--- a/src/jobs/transcodeclipjob.cpp
+++ b/src/jobs/transcodeclipjob.cpp
@@ -1,236 +1,236 @@
/***************************************************************************
* *
* Copyright (C) 2019 by Jean-Baptiste Mardelle (jb@kdenlive.org) *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program; if not, write to the *
* Free Software Foundation, Inc., *
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *
***************************************************************************/
#include "transcodeclipjob.h"
#include "bin/bin.h"
#include "bin/clipcreator.hpp"
#include "bin/projectclip.h"
#include "bin/projectitemmodel.h"
#include "core.h"
#include "doc/kdenlivedoc.h"
#include "kdenlive_debug.h"
#include "kdenlivesettings.h"
#include "macros.hpp"
#include
#include
#include
TranscodeJob::TranscodeJob(const QString &binId, QString params)
: AbstractClipJob(TRANSCODEJOB, binId)
, m_jobDuration(0)
, m_isFfmpegJob(true)
, m_jobProcess(nullptr)
, m_done(false)
, m_transcodeParams(params)
{
}
const QString TranscodeJob::getDescription() const
{
//TODO: add better description after string freeze
- return i18n("Transcode");
+ return i18n("Transcode Clip");
}
bool TranscodeJob::startJob()
{
auto binClip = pCore->projectItemModel()->getClipByBinID(m_clipId);
const QString source = binClip->url();
ClipType::ProducerType type = binClip->clipType();
QString transcoderExt = m_transcodeParams.section(QLatin1String("%1"), 1).section(QLatin1Char(' '), 0, 0);
if (transcoderExt.isEmpty()) {
qDebug()<<"// INVALID TRANSCODING PROFILE";
return false;
}
QFileInfo finfo(source);
QString fileName = finfo.fileName().section(QLatin1Char('.'), 0, -2);
QDir dir = finfo.absoluteDir();
QString path = fileName + transcoderExt;
int fileCount = 1;
bool updatedPath = false;
while (dir.exists(path)) {
QString num = QString::number(fileCount).rightJustified(4, '0', false);
path = fileName + num + transcoderExt;
++fileCount;
updatedPath = true;
}
m_destUrl = dir.absoluteFilePath(fileName);
if (updatedPath) {
m_destUrl.append(QString::number(fileCount).rightJustified(4, '0', false));
}
bool result;
if (type == ClipType::Playlist || type == ClipType::SlideShow) {
// change FFmpeg params to MLT format
m_isFfmpegJob = false;
// insert transcoded filename
m_transcodeParams.replace(QStringLiteral("%1"), QString("-consumer %1"));
// Convert param style
QStringList params = m_transcodeParams.split(QLatin1Char('-'), QString::SkipEmptyParts);
QStringList mltParameters;
for (const QString &s : params) {
QString t = s.simplified();
if (t.count(QLatin1Char(' ')) == 0) {
t.append(QLatin1String("=1"));
} else {
if (t.contains(QLatin1String("%1"))) {
// file name
mltParameters.prepend(t.section(QLatin1Char(' '), 1).replace(QLatin1String("%1"), QString("avformat:%1").arg(m_destUrl)));
mltParameters.prepend(QStringLiteral("-consumer"));
continue;
}
if (t.startsWith(QLatin1String("aspect "))) {
// Fix aspect ratio calculation
t.replace(QLatin1Char(' '), QLatin1String("=@"));
t.replace(QLatin1Char(':'), QLatin1String("/"));
} else {
t.replace(QLatin1Char(' '), QLatin1String("="));
}
}
mltParameters << t;
}
int threadCount = QThread::idealThreadCount();
if (threadCount > 2) {
threadCount = qMin(threadCount - 1, 4);
} else {
threadCount = 1;
}
mltParameters.append(QStringLiteral("real_time=-%1").arg(threadCount));
mltParameters.append(QStringLiteral("threads=%1").arg(threadCount));
// Ask for progress reporting
mltParameters << QStringLiteral("progress=1");
mltParameters.prepend(source);
m_jobProcess = new QProcess;
// m_jobProcess->setProcessChannelMode(QProcess::MergedChannels);
connect(this, &TranscodeJob::jobCanceled, m_jobProcess, &QProcess::kill, Qt::DirectConnection);
connect(m_jobProcess, &QProcess::readyReadStandardError, this, &TranscodeJob::processLogInfo);
m_jobProcess->start(KdenliveSettings::rendererpath(), mltParameters);
m_jobProcess->waitForFinished(-1);
result = m_jobProcess->exitStatus() == QProcess::NormalExit;
} else {
m_isFfmpegJob = true;
QStringList parameters;
if (KdenliveSettings::ffmpegpath().isEmpty()) {
// FFmpeg not detected, cannot process the Job
m_errorMessage.prepend(i18n("Failed to create proxy. FFmpeg not found, please set path in Kdenlive's settings Environment"));
m_done = true;
return false;
}
m_jobDuration = (int)binClip->duration().seconds();
//parameters << QStringLiteral("-y");
parameters << QStringLiteral("-stats") << QStringLiteral("-i") << source;
// Only output error data
parameters << QStringLiteral("-v") << QStringLiteral("error");
QStringList params = m_transcodeParams.split(QLatin1Char(' '));
QStringList finalParams{QStringLiteral("-i"),source};
for (const QString &s : params) {
QString t = s.simplified();
if (t.startsWith(QLatin1String("%1"))) {
parameters << t.replace(QLatin1String("%1"), m_destUrl);
} else {
parameters << t;
}
}
qDebug()<<"/// FULL PROXY PARAMS:\n"<setProcessChannelMode(QProcess::MergedChannels);
connect(m_jobProcess, &QProcess::readyReadStandardError, this, &TranscodeJob::processLogInfo);
connect(this, &TranscodeJob::jobCanceled, m_jobProcess, &QProcess::kill, Qt::DirectConnection);
m_jobProcess->start(KdenliveSettings::ffmpegpath(), parameters, QIODevice::ReadOnly);
m_jobProcess->waitForFinished(-1);
result = m_jobProcess->exitStatus() == QProcess::NormalExit;
}
m_destUrl.append(transcoderExt);
// remove temporary playlist if it exists
if (result) {
if (QFileInfo(m_destUrl).size() == 0) {
QFile::remove(m_destUrl);
// File was not created
m_done = false;
m_errorMessage.append(i18n("Failed to create file."));
} else {
m_done = true;
}
} else {
// Proxy process crashed
QFile::remove(m_destUrl);
m_done = false;
m_errorMessage.append(QString::fromUtf8(m_jobProcess->readAll()));
}
m_jobProcess->deleteLater();
return result;
}
void TranscodeJob::processLogInfo()
{
const QString buffer = QString::fromUtf8(m_jobProcess->readAllStandardError());
m_logDetails.append(buffer);
int progress = 0;
if (m_isFfmpegJob) {
// Parse FFmpeg output
if (m_jobDuration == 0) {
if (buffer.contains(QLatin1String("Duration:"))) {
QString data = buffer.section(QStringLiteral("Duration:"), 1, 1).section(QLatin1Char(','), 0, 0).simplified();
if (!data.isEmpty()) {
QStringList numbers = data.split(QLatin1Char(':'));
if (numbers.size() < 3) {
return;
}
m_jobDuration = (int)(numbers.at(0).toInt() * 3600 + numbers.at(1).toInt() * 60 + numbers.at(2).toDouble());
}
}
} else if (buffer.contains(QLatin1String("time="))) {
QString time = buffer.section(QStringLiteral("time="), 1, 1).simplified().section(QLatin1Char(' '), 0, 0);
if (!time.isEmpty()) {
QStringList numbers = time.split(QLatin1Char(':'));
if (numbers.size() < 3) {
progress = (int)time.toDouble();
if (progress == 0) {
return;
}
} else {
progress = numbers.at(0).toInt() * 3600 + numbers.at(1).toInt() * 60 + numbers.at(2).toDouble();
}
}
emit jobProgress((int)(100.0 * progress / m_jobDuration));
}
} else {
// Parse MLT output
if (buffer.contains(QLatin1String("percentage:"))) {
progress = buffer.section(QStringLiteral("percentage:"), 1).simplified().section(QLatin1Char(' '), 0, 0).toInt();
emit jobProgress(progress);
}
}
}
bool TranscodeJob::commitResult(Fun &undo, Fun &redo)
{
Q_ASSERT(!m_resultConsumed);
if (!m_done) {
qDebug() << "ERROR: Trying to consume invalid results";
return false;
}
m_resultConsumed = true;
QString folderId = QStringLiteral("-1");
auto id = ClipCreator::createClipFromFile(m_destUrl, folderId, pCore->projectItemModel(), undo, redo);
return id != QStringLiteral("-1");
}
diff --git a/src/library/librarywidget.cpp b/src/library/librarywidget.cpp
index 0fe2ab2bf..55669bf02 100644
--- a/src/library/librarywidget.cpp
+++ b/src/library/librarywidget.cpp
@@ -1,616 +1,616 @@
/***************************************************************************
* Copyright (C) 2016 by Jean-Baptiste Mardelle (jb@kdenlive.org) *
* 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 "librarywidget.h"
#include "core.h"
#include "doc/kthumb.h"
#include "kdenlivesettings.h"
#include "project/projectmanager.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
enum LibraryItem { PlayList, Clip, Folder };
LibraryTree::LibraryTree(QWidget *parent)
: QTreeWidget(parent)
{
int size = QFontInfo(font()).pixelSize();
setIconSize(QSize(size * 4, size * 2));
setDragDropMode(QAbstractItemView::DragDrop);
setAcceptDrops(true);
setDragEnabled(true);
setDropIndicatorShown(true);
viewport()->setAcceptDrops(true);
}
// virtual
QMimeData *LibraryTree::mimeData(const QList list) const
{
QList urls;
urls.reserve(list.count());
for (QTreeWidgetItem *item : list) {
urls << QUrl::fromLocalFile(item->data(0, Qt::UserRole).toString());
}
auto *mime = new QMimeData;
mime->setUrls(urls);
return mime;
}
QStringList LibraryTree::mimeTypes() const
{
return QStringList() << QStringLiteral("text/uri-list") << QStringLiteral("kdenlive/clip") << QStringLiteral("kdenlive/producerslist");
}
void LibraryTree::slotUpdateThumb(const QString &path, const QString &iconPath)
{
QString name = QUrl::fromLocalFile(path).fileName();
QList list = findItems(name, Qt::MatchExactly | Qt::MatchRecursive);
for (QTreeWidgetItem *item : list) {
if (item->data(0, Qt::UserRole).toString() == path) {
// We found our item
blockSignals(true);
item->setData(0, Qt::DecorationRole, QIcon(iconPath));
blockSignals(false);
break;
}
}
}
void LibraryTree::slotUpdateThumb(const QString &path, const QPixmap &pix)
{
QString name = QUrl::fromLocalFile(path).fileName();
QList list = findItems(name, Qt::MatchExactly | Qt::MatchRecursive);
for (QTreeWidgetItem *item : list) {
if (item->data(0, Qt::UserRole).toString() == path) {
// We found our item
blockSignals(true);
item->setData(0, Qt::DecorationRole, QIcon(pix));
blockSignals(false);
break;
}
}
}
void LibraryTree::mousePressEvent(QMouseEvent *event)
{
QTreeWidgetItem *clicked = this->itemAt(event->pos());
QList act = actions();
if (clicked) {
for (QAction *a : act) {
a->setEnabled(true);
}
} else {
// Clicked in empty area, disable clip actions
clearSelection();
for (QAction *a : act) {
if (a->data().toInt() == 1) {
a->setEnabled(false);
}
}
}
QTreeWidget::mousePressEvent(event);
}
void LibraryTree::dropEvent(QDropEvent *event)
{
// QTreeWidget::dropEvent(event);
const QMimeData *qMimeData = event->mimeData();
QTreeWidgetItem *dropped = this->itemAt(event->pos());
QString dest;
if (dropped) {
dest = dropped->data(0, Qt::UserRole).toString();
if (dropped->data(0, Qt::UserRole + 2).toInt() != LibraryItem::Folder) {
dest = QUrl::fromLocalFile(dest).adjusted(QUrl::RemoveFilename).toLocalFile();
}
}
if (qMimeData->hasUrls()) {
QList urls = qMimeData->urls();
emit moveData(urls, dest);
} else if (qMimeData->hasFormat(QStringLiteral("kdenlive/clip"))) {
emit importSequence(QString(qMimeData->data(QStringLiteral("kdenlive/clip"))).split(QLatin1Char(';')), dest);
} else if (qMimeData->hasFormat(QStringLiteral("kdenlive/producerslist"))) {
QStringList list = QString(qMimeData->data(QStringLiteral("kdenlive/producerslist"))).split(QLatin1Char(';'));
for (const QString &prodslist : list) {
if (prodslist.startsWith(QLatin1Char('#'))) {
// Bin folder, not supported yet
continue;
}
if (prodslist.contains(QLatin1Char('/'))) {
// Clip zone
emit importSequence(prodslist.split(QLatin1Char('/')), dest);
} else {
// Full clip
emit importSequence(QStringList() << prodslist << QStringLiteral("-1") << QStringLiteral("-1"), dest);
}
}
}
event->accept();
}
LibraryWidget::LibraryWidget(ProjectManager *manager, QWidget *parent)
: QWidget(parent)
, m_manager(manager)
, m_previewJob(nullptr)
{
auto *lay = new QVBoxLayout(this);
m_libraryTree = new LibraryTree(this);
m_libraryTree->setColumnCount(1);
m_libraryTree->setHeaderHidden(true);
m_libraryTree->setDragEnabled(true);
m_libraryTree->setItemDelegate(new LibraryItemDelegate(this));
m_libraryTree->setAlternatingRowColors(true);
lay->addWidget(m_libraryTree);
// Library path
QString path;
if (KdenliveSettings::librarytodefaultfolder() || KdenliveSettings::libraryfolder().isEmpty()) {
path = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/library");
} else {
path = KdenliveSettings::libraryfolder();
}
// Info message
m_infoWidget = new KMessageWidget;
lay->addWidget(m_infoWidget);
m_infoWidget->hide();
// Download progress bar
m_progressBar = new QProgressBar(this);
lay->addWidget(m_progressBar);
m_toolBar = new QToolBar(this);
m_progressBar->setRange(0, 100);
m_progressBar->setOrientation(Qt::Horizontal);
m_progressBar->setVisible(false);
lay->addWidget(m_toolBar);
setLayout(lay);
m_directory = QDir(path);
if (!m_directory.exists()) {
m_directory.mkpath(QStringLiteral("."));
}
QFileInfo fi(m_directory.absolutePath());
if (!m_directory.exists() || !fi.isWritable()) {
// Something went wrong
showMessage(i18n("Check your settings, Library path is invalid: %1", m_directory.absolutePath()), KMessageWidget::Warning);
setEnabled(false);
}
m_libraryTree->setContextMenuPolicy(Qt::ActionsContextMenu);
m_timer.setSingleShot(true);
m_timer.setInterval(4000);
connect(&m_timer, &QTimer::timeout, m_infoWidget, &KMessageWidget::animatedHide);
connect(m_libraryTree, &LibraryTree::moveData, this, &LibraryWidget::slotMoveData);
connect(m_libraryTree, &LibraryTree::importSequence, this, &LibraryWidget::slotSaveSequence);
m_coreLister = new KCoreDirLister(this);
m_coreLister->setDelayedMimeTypes(false);
connect(m_coreLister, &KCoreDirLister::itemsAdded, this, &LibraryWidget::slotItemsAdded);
connect(m_coreLister, &KCoreDirLister::itemsDeleted, this, &LibraryWidget::slotItemsDeleted);
connect(m_coreLister, SIGNAL(clear()), this, SLOT(slotClearAll()));
m_coreLister->openUrl(QUrl::fromLocalFile(m_directory.absolutePath()));
m_libraryTree->setSortingEnabled(true);
m_libraryTree->sortByColumn(0, Qt::AscendingOrder);
connect(m_libraryTree, &LibraryTree::itemChanged, this, &LibraryWidget::slotItemEdited, Qt::UniqueConnection);
}
void LibraryWidget::setupActions(const QList &list)
{
QList menuList;
m_addAction = new QAction(QIcon::fromTheme(QStringLiteral("kdenlive-add-clip")), i18n("Add Clip to Project"), this);
connect(m_addAction, &QAction::triggered, this, &LibraryWidget::slotAddToProject);
m_addAction->setData(1);
m_deleteAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18n("Delete Clip from Library"), this);
connect(m_deleteAction, &QAction::triggered, this, &LibraryWidget::slotDeleteFromLibrary);
m_deleteAction->setData(1);
QAction *addFolder = new QAction(QIcon::fromTheme(QStringLiteral("folder-new")), i18n("Create Library Folder"), this);
connect(addFolder, &QAction::triggered, this, &LibraryWidget::slotAddFolder);
QAction *renameFolder = new QAction(QIcon(), i18n("Rename Library Clip"), this);
renameFolder->setData(1);
connect(renameFolder, &QAction::triggered, this, &LibraryWidget::slotRenameItem);
menuList << m_addAction << addFolder << renameFolder << m_deleteAction;
m_toolBar->addAction(m_addAction);
m_toolBar->addSeparator();
m_toolBar->addAction(addFolder);
for (QAction *action : list) {
m_toolBar->addAction(action);
menuList << action;
connect(this, &LibraryWidget::enableAddSelection, action, &QAction::setEnabled);
}
// Create spacer
QWidget *spacer = new QWidget();
spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
m_toolBar->addWidget(spacer);
m_toolBar->addSeparator();
m_toolBar->addAction(m_deleteAction);
m_libraryTree->addActions(menuList);
connect(m_libraryTree, &QTreeWidget::itemSelectionChanged, this, &LibraryWidget::updateActions);
}
void LibraryWidget::slotAddToLibrary()
{
if (!isEnabled()) {
return;
}
emit saveTimelineSelection(m_directory);
}
void LibraryWidget::showMessage(const QString &text, KMessageWidget::MessageType type)
{
m_timer.stop();
m_infoWidget->setText(text);
m_infoWidget->setWordWrap(m_infoWidget->text().length() > 35);
m_infoWidget->setMessageType(type);
m_infoWidget->animatedShow();
m_timer.start();
}
void LibraryWidget::slotAddToProject()
{
QTreeWidgetItem *current = m_libraryTree->currentItem();
if (!current) {
return;
}
const QList list = {QUrl::fromLocalFile(current->data(0, Qt::UserRole).toString())};
emit addProjectClips(list);
}
void LibraryWidget::updateActions()
{
QTreeWidgetItem *current = m_libraryTree->currentItem();
if (!current) {
m_addAction->setEnabled(false);
m_deleteAction->setEnabled(false);
return;
}
m_addAction->setEnabled(true);
m_deleteAction->setEnabled(true);
}
void LibraryWidget::slotDeleteFromLibrary()
{
QTreeWidgetItem *current = m_libraryTree->currentItem();
if (!current) {
qCDebug(KDENLIVE_LOG) << " * * *Deleting no item ";
return;
}
QString path = current->data(0, Qt::UserRole).toString();
if (path.isEmpty()) {
return;
}
if (current->data(0, Qt::UserRole + 2).toInt() == LibraryItem::Folder) {
// Deleting a folder
QDir dir(path);
// Make sure we are really trying to remove a directory located in the library folder
if (!path.startsWith(m_directory.absolutePath())) {
showMessage(i18n("You are trying to remove an invalid folder: %1", path));
return;
}
const QStringList fileList = dir.entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot);
if (!fileList.isEmpty()) {
if (KMessageBox::warningContinueCancel(this, i18n("This will delete the folder %1, including all playlists in it.\nThis cannot be undone", path)) !=
KMessageBox::Continue) {
return;
}
}
dir.removeRecursively();
} else {
QString message;
if (current->data(0, Qt::UserRole + 2).toInt() == LibraryItem::PlayList) {
message = i18n("This will delete the MLT playlist:\n%1", path);
} else {
message = i18n("This will delete the file:\n%1", path);
}
if (KMessageBox::warningContinueCancel(this, message) != KMessageBox::Continue) {
return;
}
// Remove playlist
if (!QFile::remove(path)) {
showMessage(i18n("Error removing %1", path));
}
}
}
void LibraryWidget::slotAddFolder()
{
bool ok;
QString name = QInputDialog::getText(this, i18n("Add Folder to Library"), i18n("Enter a folder name"), QLineEdit::Normal, QString(), &ok);
if (name.isEmpty() || !ok) {
return;
}
QTreeWidgetItem *current = m_libraryTree->currentItem();
QString parentFolder;
if (current) {
if (current->data(0, Qt::UserRole + 2).toInt() == LibraryItem::Folder) {
// Creating a subfolder
parentFolder = current->data(0, Qt::UserRole).toString();
} else {
QTreeWidgetItem *parentItem = current->parent();
if (parentItem) {
parentFolder = parentItem->data(0, Qt::UserRole).toString();
}
}
}
if (parentFolder.isEmpty()) {
parentFolder = m_directory.absolutePath();
}
QDir dir(parentFolder);
if (dir.exists(name)) {
- // TODO: warn user
+ showMessage(i18n("Folder %1 already exists", name));
return;
}
if (!dir.mkdir(name)) {
showMessage(i18n("Error creating folder %1", name));
return;
}
}
void LibraryWidget::slotRenameItem()
{
QTreeWidgetItem *current = m_libraryTree->currentItem();
if (!current) {
// This is not a folder, abort
return;
}
m_libraryTree->editItem(current);
}
void LibraryWidget::slotMoveData(const QList &urls, QString dest)
{
if (urls.isEmpty()) {
return;
}
if (dest.isEmpty()) {
// moving to library's root
dest = m_directory.absolutePath();
}
QDir dir(dest);
if (!dir.exists()) {
return;
}
for (const QUrl &url : urls) {
if (!url.toLocalFile().startsWith(m_directory.absolutePath())) {
// Dropped an external file, attempt to copy it to library
KIO::FileCopyJob *copyJob = KIO::file_copy(url, QUrl::fromLocalFile(dir.absoluteFilePath(url.fileName())));
connect(copyJob, &KJob::result, this, &LibraryWidget::slotDownloadFinished);
connect(copyJob, SIGNAL(percent(KJob *, ulong)), this, SLOT(slotDownloadProgress(KJob *, ulong)));
} else {
// Internal drag/drop
dir.rename(url.toLocalFile(), url.fileName());
}
}
}
void LibraryWidget::slotSaveSequence(const QStringList &info, QString dest)
{
if (info.isEmpty()) {
return;
}
if (dest.isEmpty()) {
// moving to library's root
dest = m_directory.absolutePath();
}
QDir dir(dest);
if (!dir.exists()) {
return;
}
m_manager->saveZone(info, dir);
}
void LibraryWidget::slotItemEdited(QTreeWidgetItem *item, int column)
{
if ((item == nullptr) || column != 0) {
return;
}
if (item->data(0, Qt::UserRole + 2).toInt() == LibraryItem::Folder) {
QDir dir(item->data(0, Qt::UserRole).toString());
dir.cdUp();
dir.rename(item->data(0, Qt::UserRole).toString(), item->text(0));
// item->setData(0, Qt::UserRole, dir.absoluteFilePath(item->text(0)));
} else {
QString oldPath = item->data(0, Qt::UserRole).toString();
QDir dir(QUrl::fromLocalFile(oldPath).adjusted(QUrl::RemoveFilename).toLocalFile());
dir.rename(oldPath, item->text(0));
// item->setData(0, Qt::UserRole, dir.absoluteFilePath(item->text(0)));
}
}
void LibraryWidget::slotDownloadFinished(KJob *)
{
m_progressBar->setValue(100);
m_progressBar->setVisible(false);
}
void LibraryWidget::slotDownloadProgress(KJob *, int progress)
{
m_progressBar->setVisible(true);
m_progressBar->setValue(progress);
}
void LibraryWidget::slotUpdateLibraryPath()
{
// Library path changed, reload library with updated path
m_libraryTree->blockSignals(true);
m_folders.clear();
m_libraryTree->clear();
QString defaultPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/library");
if (KdenliveSettings::librarytodefaultfolder() || KdenliveSettings::libraryfolder().isEmpty()) {
m_directory.setPath(defaultPath);
if (!m_directory.exists()) {
m_directory.mkpath(QStringLiteral("."));
}
showMessage(i18n("Library path set to default: %1", defaultPath), KMessageWidget::Information);
} else {
m_directory.setPath(KdenliveSettings::libraryfolder());
if (!m_directory.exists()) {
m_directory.mkpath(QStringLiteral("."));
}
showMessage(i18n("Library path set to custom: %1", KdenliveSettings::libraryfolder()), KMessageWidget::Information);
}
QFileInfo fi(m_directory.absolutePath());
if (!m_directory.exists() || !fi.isWritable()) {
// Cannot write to new Library, try default one
if (m_directory.absolutePath() != defaultPath) {
showMessage(i18n("Cannot write to Library path: %1, using default", KdenliveSettings::libraryfolder()), KMessageWidget::Warning);
m_directory.setPath(defaultPath);
if (!m_directory.exists()) {
m_directory.mkpath(QStringLiteral("."));
}
}
}
fi.setFile(m_directory.absolutePath());
if (!m_directory.exists() || !fi.isWritable()) {
// Something is really broken, disable library
showMessage(i18n("Check your settings, Library path is invalid: %1", m_directory.absolutePath()), KMessageWidget::Warning);
setEnabled(false);
} else {
m_coreLister->openUrl(QUrl::fromLocalFile(m_directory.absolutePath()));
setEnabled(true);
}
m_libraryTree->blockSignals(false);
}
void LibraryWidget::slotGotPreview(const KFileItem &item, const QPixmap &pix)
{
const QString path = item.url().toLocalFile();
m_libraryTree->blockSignals(true);
m_libraryTree->slotUpdateThumb(path, pix);
m_libraryTree->blockSignals(false);
}
void LibraryWidget::slotItemsDeleted(const KFileItemList &list)
{
m_libraryTree->blockSignals(true);
QMutexLocker lock(&m_treeMutex);
for (const KFileItem &fitem : list) {
QUrl fileUrl = fitem.url();
QString path;
if (fitem.isDir()) {
path = fileUrl.toLocalFile();
} else {
path = fileUrl.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).toLocalFile();
}
QTreeWidgetItem *matchingFolder = nullptr;
if (path != m_directory.absolutePath()) {
for (QTreeWidgetItem *folder : m_folders) {
if (folder->data(0, Qt::UserRole).toString() == path) {
// Found parent folder
matchingFolder = folder;
break;
}
}
}
if (fitem.isDir()) {
if (matchingFolder) {
m_folders.removeAll(matchingFolder);
// warning, we also need to remove all subfolders since they will be recreated
QList subList;
for (QTreeWidgetItem *folder : m_folders) {
if (folder->data(0, Qt::UserRole).toString().startsWith(path)) {
subList << folder;
}
}
for (QTreeWidgetItem *sub : subList) {
m_folders.removeAll(sub);
}
delete matchingFolder;
}
} else {
if (matchingFolder == nullptr) {
matchingFolder = m_libraryTree->invisibleRootItem();
}
for (int i = 0; i < matchingFolder->childCount(); i++) {
QTreeWidgetItem *item = matchingFolder->child(i);
if (item->data(0, Qt::UserRole).toString() == fileUrl.toLocalFile()) {
// Found deleted item
delete item;
break;
}
}
}
}
m_libraryTree->blockSignals(false);
}
void LibraryWidget::slotItemsAdded(const QUrl &url, const KFileItemList &list)
{
m_libraryTree->blockSignals(true);
QMutexLocker lock(&m_treeMutex);
for (const KFileItem &fitem : list) {
QUrl fileUrl = fitem.url();
QString name = fileUrl.fileName();
QTreeWidgetItem *treeItem;
QTreeWidgetItem *parent = nullptr;
if (url != QUrl::fromLocalFile(m_directory.absolutePath())) {
// not a top level item
QString directory = fileUrl.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).toLocalFile();
for (QTreeWidgetItem *folder : m_folders) {
if (folder->data(0, Qt::UserRole).toString() == directory) {
// Found parent folder
parent = folder;
break;
}
}
}
if (parent) {
treeItem = new QTreeWidgetItem(parent, QStringList() << name);
} else {
treeItem = new QTreeWidgetItem(m_libraryTree, QStringList() << name);
}
treeItem->setData(0, Qt::UserRole, fileUrl.toLocalFile());
treeItem->setData(0, Qt::UserRole + 1, fitem.timeString());
if (fitem.isDir()) {
treeItem->setData(0, Qt::UserRole + 2, (int)LibraryItem::Folder);
m_folders << treeItem;
m_coreLister->openUrl(fileUrl, KCoreDirLister::Keep);
} else if (name.endsWith(QLatin1String(".mlt")) || name.endsWith(QLatin1String(".kdenlive"))) {
treeItem->setData(0, Qt::UserRole + 2, (int)LibraryItem::PlayList);
} else {
treeItem->setData(0, Qt::UserRole + 2, (int)LibraryItem::Clip);
}
treeItem->setData(0, Qt::DecorationRole, QIcon::fromTheme(fitem.iconName()));
treeItem->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | Qt::ItemIsEditable);
}
QStringList plugins = KIO::PreviewJob::availablePlugins();
m_previewJob = KIO::filePreview(list, QSize(80, 80), &plugins);
m_previewJob->setIgnoreMaximumSize();
connect(m_previewJob, &KIO::PreviewJob::gotPreview, this, &LibraryWidget::slotGotPreview);
m_libraryTree->blockSignals(false);
}
void LibraryWidget::slotClearAll()
{
m_libraryTree->blockSignals(true);
m_folders.clear();
m_libraryTree->clear();
m_libraryTree->blockSignals(false);
}