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("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); }