diff --git a/fuzzer/fuzzing.cpp b/fuzzer/fuzzing.cpp index 239eadee4..6da6b9c1b 100644 --- a/fuzzer/fuzzing.cpp +++ b/fuzzer/fuzzing.cpp @@ -1,422 +1,423 @@ /*************************************************************************** * Copyright (C) 2019 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 "fuzzing.hpp" #include "bin/model/markerlistmodel.hpp" #include "doc/docundostack.hpp" #include "fakeit_standalone.hpp" #include "logger.hpp" #include #include #include #include #include #define private public #define protected public #include "assets/keyframes/model/keyframemodel.hpp" #include "assets/model/assetparametermodel.hpp" #include "bin/clipcreator.hpp" #include "bin/projectclip.h" #include "bin/projectfolder.h" #include "bin/projectitemmodel.h" #include "core.h" #include "effects/effectsrepository.hpp" #include "effects/effectstack/model/effectitemmodel.hpp" #include "effects/effectstack/model/effectstackmodel.hpp" #include "mltconnection.h" #include "project/projectmanager.h" #include "timeline2/model/clipmodel.hpp" #include "timeline2/model/compositionmodel.hpp" #include "timeline2/model/groupsmodel.hpp" #include "timeline2/model/timelinefunctions.hpp" #include "timeline2/model/timelineitemmodel.hpp" #include "timeline2/model/timelinemodel.hpp" #include "timeline2/model/trackmodel.hpp" #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-parameter" #pragma GCC diagnostic ignored "-Wsign-conversion" #pragma GCC diagnostic ignored "-Wfloat-equal" #pragma GCC diagnostic ignored "-Wshadow" #pragma GCC diagnostic ignored "-Wpedantic" #include #pragma GCC diagnostic pop using namespace fakeit; namespace { QString createProducer(Mlt::Profile &prof, std::string color, std::shared_ptr binModel, int length, bool limited) { Logger::log_create_producer("test_producer", {color, binModel, length, limited}); std::shared_ptr producer = std::make_shared(prof, "color", color.c_str()); producer->set("length", length); producer->set("out", length - 1); Q_ASSERT(producer->is_valid()); QString binId = QString::number(binModel->getFreeClipId()); auto binClip = ProjectClip::construct(binId, QIcon(), binModel, producer); if (limited) { binClip->forceLimitedDuration(); } Fun undo = []() { return true; }; Fun redo = []() { return true; }; Q_ASSERT(binModel->addItem(binClip, binModel->getRootFolder()->clipId(), undo, redo)); return binId; } QString createProducerWithSound(Mlt::Profile &prof, std::shared_ptr binModel) { Logger::log_create_producer("test_producer_sound", {binModel}); // std::shared_ptr producer = std::make_shared(prof, // QFileInfo("../tests/small.mkv").absoluteFilePath().toStdString().c_str()); // In case the test system does not have avformat support, we can switch to the integrated blipflash producer std::shared_ptr producer = std::make_shared(prof, "blipflash"); producer->set_in_and_out(0, 1); producer->set("kdenlive:duration", 2); Q_ASSERT(producer->is_valid()); QString binId = QString::number(binModel->getFreeClipId()); auto binClip = ProjectClip::construct(binId, QIcon(), binModel, producer); Fun undo = []() { return true; }; Fun redo = []() { return true; }; Q_ASSERT(binModel->addItem(binClip, binModel->getRootFolder()->clipId(), undo, redo)); return binId; } inline int modulo(int a, int b) { const int result = a % b; return result >= 0 ? result : result + b; } namespace { bool isIthParamARef(const rttr::method &method, size_t i) { QString sig = QString::fromStdString(method.get_signature().to_string()); int deb = sig.indexOf("("); int end = sig.lastIndexOf(")"); sig = sig.mid(deb + 1, deb - end - 1); QStringList args = sig.split(QStringLiteral(",")); return args[(int)i].contains("&") && !args[(int)i].contains("const &"); } } // namespace } // namespace void fuzz(const std::string &input) { Logger::init(); std::stringstream ss; ss << input; Mlt::Profile profile; auto binModel = pCore->projectItemModel(); binModel->clean(); std::shared_ptr undoStack = std::make_shared(nullptr); std::shared_ptr guideModel = std::make_shared(undoStack); TimelineModel::next_id = 0; Mock pmMock; When(Method(pmMock, undoStack)).AlwaysReturn(undoStack); ProjectManager &mocked = pmMock.get(); pCore->m_projectManager = &mocked; std::vector> all_timelines; std::unordered_map, std::vector> all_clips, all_tracks, all_compositions; auto update_elems = [&]() { all_clips.clear(); all_tracks.clear(); all_compositions.clear(); for (const auto &timeline : all_timelines) { all_clips[timeline] = {}; all_tracks[timeline] = {}; all_compositions[timeline] = {}; auto &clips = all_clips[timeline]; clips.clear(); for (const auto &c : timeline->m_allClips) { clips.push_back(c.first); } std::sort(clips.begin(), clips.end()); auto &compositions = all_compositions[timeline]; compositions.clear(); for (const auto &c : timeline->m_allCompositions) { compositions.push_back(c.first); } std::sort(compositions.begin(), compositions.end()); auto &tracks = all_tracks[timeline]; tracks.clear(); for (const auto &c : timeline->m_iteratorTable) { tracks.push_back(c.first); } std::sort(tracks.begin(), tracks.end()); } }; auto get_timeline = [&]() -> std::shared_ptr { int id = 0; ss >> id; if (all_timelines.size() == 0) return nullptr; id = modulo(id, (int)all_timelines.size()); return all_timelines[size_t(id)]; }; auto get_clip = [&](std::shared_ptr timeline) { int id = 0; ss >> id; if (!timeline) return -1; if (timeline->isClip(id)) return id; if (all_timelines.size() == 0) return -1; if (all_clips.count(timeline) == 0) return -1; if (all_clips[timeline].size() == 0) return -1; id = modulo(id, (int)all_clips[timeline].size()); return all_clips[timeline][id]; }; auto get_compo = [&](std::shared_ptr timeline) { int id = 0; ss >> id; if (!timeline) return -1; if (timeline->isComposition(id)) return id; if (all_timelines.size() == 0) return -1; if (all_compositions.count(timeline) == 0) return -1; if (all_compositions[timeline].size() == 0) return -1; id = modulo(id, (int)all_compositions[timeline].size()); return all_compositions[timeline][id]; }; auto get_item = [&](std::shared_ptr timeline) { int id = 0; ss >> id; if (!timeline) return -1; if (timeline->isClip(id)) return id; if (timeline->isComposition(id)) return id; if (all_timelines.size() == 0) return -1; int clip_count = 0; if (all_clips.count(timeline) > 0) { clip_count = all_clips[timeline].size(); } int compo_count = 0; if (all_compositions.count(timeline) > 0) { compo_count = all_compositions[timeline].size(); } if (clip_count + compo_count == 0) return -1; id = modulo(id, clip_count + compo_count); if (id < clip_count) { return all_clips[timeline][id]; } return all_compositions[timeline][id - clip_count]; }; auto get_track = [&](std::shared_ptr timeline) { int id = 0; ss >> id; if (!timeline) return -1; if (timeline->isTrack(id)) return id; if (all_timelines.size() == 0) return -1; if (all_tracks.count(timeline) == 0) return -1; if (all_tracks[timeline].size() == 0) return -1; id = modulo(id, (int)all_tracks[timeline].size()); return all_tracks[timeline][id]; }; std::string c; while (ss >> c) { if (Logger::back_translation_table.count(c) > 0) { // std::cout << "found=" << c; c = Logger::back_translation_table[c]; // std::cout << " tranlated=" << c << std::endl; if (c == "constr_TimelineModel") { all_timelines.emplace_back(TimelineItemModel::construct(&profile, guideModel, undoStack)); } else if (c == "constr_TrackModel") { auto timeline = get_timeline(); int id, pos = 0; std::string name; bool audio = false; ss >> id >> pos >> name >> audio; if (name == "$$") { name = ""; } if (pos < -1) pos = 0; pos = std::min((int)all_tracks[timeline].size(), pos); if (timeline) { TrackModel::construct(timeline, -1, pos, QString::fromStdString(name), audio); } } else if (c == "constr_test_producer") { std::string color; int length = 0; bool limited = false; ss >> color >> length >> limited; createProducer(profile, color, binModel, length, limited); } else if (c == "constr_test_producer_sound") { createProducerWithSound(profile, binModel); } else { // std::cout << "executing " << c << std::endl; rttr::type target_type = rttr::type::get(); bool found = false; for (const std::string &t : {"TimelineModel"}) { rttr::type current_type = rttr::type::get_by_name(t); // std::cout << "type " << t << " has methods count=" << current_type.get_methods().size() << std::endl; if (current_type.get_method(c).is_valid()) { found = true; target_type = current_type; break; } } if (found) { bool valid = true; rttr::method target_method = target_type.get_method(c); std::vector arguments; rttr::variant ptr; if (target_type == rttr::type::get()) { if (all_timelines.size() == 0) { valid = false; } ptr = get_timeline(); } int i = -1; for (const auto &p : target_method.get_parameter_infos()) { ++i; std::string arg_name = p.get_name().to_string(); // std::cout << arg_name << std::endl; if (arg_name == "compoId") { std::shared_ptr tim = (ptr.can_convert>() ? ptr.convert>() : nullptr); int compoId = get_compo(tim); valid = valid && (compoId >= 0); // std::cout << "got compo" << compoId << std::endl; arguments.push_back(compoId); } else if (arg_name == "clipId") { std::shared_ptr tim = (ptr.can_convert>() ? ptr.convert>() : nullptr); int clipId = get_clip(tim); valid = valid && (clipId >= 0); arguments.push_back(clipId); // std::cout << "got clipId" << clipId << std::endl; } else if (arg_name == "trackId") { std::shared_ptr tim = (ptr.can_convert>() ? ptr.convert>() : nullptr); int trackId = get_track(tim); valid = valid && (trackId >= 0); arguments.push_back(rttr::variant(trackId)); // std::cout << "got trackId" << trackId << std::endl; } else if (arg_name == "itemId") { std::shared_ptr tim = (ptr.can_convert>() ? ptr.convert>() : nullptr); int itemId = get_item(tim); valid = valid && (itemId >= 0); arguments.push_back(itemId); // std::cout << "got itemId" << itemId << std::endl; } else if (arg_name == "ids") { int count = 0; ss >> count; // std::cout << "got ids. going to read count=" << count << std::endl; if (count > 0) { std::shared_ptr tim = (ptr.can_convert>() ? ptr.convert>() : nullptr); std::unordered_set ids; for (int i = 0; i < count; ++i) { int itemId = get_item(tim); // std::cout << "\t read" << itemId << std::endl; valid = valid && (itemId >= 0); ids.insert(itemId); } arguments.push_back(ids); } else { valid = false; } } else if (!isIthParamARef(target_method, i)) { rttr::type arg_type = p.get_type(); if (arg_type == rttr::type::get()) { int a = 0; ss >> a; // std::cout << "read int " << a << std::endl; arguments.push_back(a); } else if (arg_type == rttr::type::get()) { bool a = false; ss >> a; // std::cout << "read bool " << a << std::endl; arguments.push_back(a); } else if (arg_type == rttr::type::get()) { std::string str = ""; ss >> str; // std::cout << "read str " << str << std::endl; if (str == "$$") { str = ""; } arguments.push_back(QString::fromStdString(str)); } else if (arg_type.is_enumeration()) { int a = 0; ss >> a; rttr::variant var_a = a; var_a.convert((const rttr::type &)arg_type); // std::cout << "read enum " << arg_type.get_enumeration().value_to_name(var_a).to_string() << std::endl; arguments.push_back(var_a); } else { assert(false); } } else { if (p.get_type() == rttr::type::get()) { arguments.push_back(-1); } else { assert(false); } } } if (valid) { // std::cout << "VALID!!!" << std::endl; std::vector args; + args.reserve(arguments.size()); for (const auto &a : arguments) { args.emplace_back(a); // std::cout<<"argument="<checkConsistency()); } } } all_clips.clear(); all_tracks.clear(); all_compositions.clear(); for (size_t i = 0; i < all_timelines.size(); ++i) { all_timelines[i].reset(); } pCore->m_projectManager = nullptr; Core::m_self.reset(); MltConnection::m_self.reset(); std::cout << "---------------------------------------------------------------------------------------------------------------------------------------------" "---------------" << std::endl; } diff --git a/renderer/kdenlive_render.cpp b/renderer/kdenlive_render.cpp index 41836948d..d9b568a28 100644 --- a/renderer/kdenlive_render.cpp +++ b/renderer/kdenlive_render.cpp @@ -1,143 +1,143 @@ /*************************************************************************** * Copyright (C) 2008 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 "framework/mlt_version.h" #include "mlt++/Mlt.h" #include "renderjob.h" #include #include #include #include #include #include #include #include #include int main(int argc, char **argv) { QCoreApplication app(argc, argv); QStringList args = app.arguments(); QStringList preargs; QString locale; if (args.count() >= 4) { // Remove program name args.removeFirst(); // renderer path (melt) QString render = args.at(0); args.removeFirst(); // Source playlist path QString playlist = args.at(0); args.removeFirst(); // target - where to save result QString target = args.at(0); args.removeFirst(); int pid = 0; // pid to send back progress if (args.count() > 0 && args.at(0).startsWith(QLatin1String("-pid:"))) { pid = args.at(0).section(QLatin1Char(':'), 1).toInt(); args.removeFirst(); } // Do we want a split render if (args.count() > 0 && args.at(0) == QLatin1String("-split")) { args.removeFirst(); // chunks to render QStringList chunks = args.at(0).split(QLatin1Char(','), QString::SkipEmptyParts); args.removeFirst(); // chunk size in frames int chunkSize = args.at(0).toInt(); args.removeFirst(); // rendered file extension QString extension = args.at(0); args.removeFirst(); // avformat consumer params QStringList consumerParams = args.at(0).split(QLatin1Char(' '), QString::SkipEmptyParts); args.removeFirst(); QDir baseFolder(target); Mlt::Factory::init(); Mlt::Profile profile; Mlt::Producer prod(profile, nullptr, playlist.toUtf8().constData()); if (!prod.is_valid()) { fprintf(stderr, "INVALID playlist: %s \n", playlist.toUtf8().constData()); } - for (const QString frame : chunks) { + for (const QString &frame : chunks) { fprintf(stderr, "START:%d \n", frame.toInt()); QString fileName = QStringLiteral("%1.%2").arg(frame).arg(extension); if (baseFolder.exists(fileName)) { // Don't overwrite an existing file fprintf(stderr, "DONE:%d \n", frame.toInt()); continue; } QScopedPointer playlst(prod.cut(frame.toInt(), frame.toInt() + chunkSize)); QScopedPointer cons( new Mlt::Consumer(profile, QString("avformat:%1").arg(baseFolder.absoluteFilePath(fileName)).toUtf8().constData())); for (const QString ¶m : consumerParams) { if (param.contains(QLatin1Char('='))) { cons->set(param.section(QLatin1Char('='), 0, 0).toUtf8().constData(), param.section(QLatin1Char('='), 1).toUtf8().constData()); } } cons->set("terminate_on_pause", 1); cons->connect(*playlst); playlst.reset(); cons->run(); cons->stop(); cons->purge(); fprintf(stderr, "DONE:%d \n", frame.toInt()); } // Mlt::Factory::close(); fprintf(stderr, "+ + + RENDERING FINSHED + + + \n"); return 0; } int in = -1; int out = -1; if (LIBMLT_VERSION_INT < 396544) { // older MLT version, does not support consumer in/out, so read it manually QFile f(playlist); QDomDocument doc; doc.setContent(&f, false); f.close(); QDomElement consumer = doc.documentElement().firstChildElement(QStringLiteral("consumer")); if (!consumer.isNull()) { in = consumer.attribute("in").toInt(); out = consumer.attribute("out").toInt(); } } RenderJob *rJob = new RenderJob(render, playlist, target, pid, in, out); rJob->start(); return app.exec(); } else { fprintf(stderr, "Kdenlive video renderer for MLT.\nUsage: " "kdenlive_render [-erase] [-kuiserver] [-locale:LOCALE] [in=pos] [out=pos] [render] [profile] [rendermodule] [player] [src] [dest] [[arg1] " "[arg2] ...]\n" " -erase: if that parameter is present, src file will be erased at the end\n" " -kuiserver: if that parameter is present, use KDE job tracker\n" " -locale:LOCALE : set a locale for rendering. For example, -locale:fr_FR.UTF-8 will use a french locale (comma as numeric separator)\n" " in=pos: start rendering at frame pos\n" " out=pos: end rendering at frame pos\n" " render: path to MLT melt renderer\n" " profile: the MLT video profile\n" " rendermodule: the MLT consumer used for rendering, usually it is avformat\n" " player: path to video player to play when rendering is over, use '-' to disable playing\n" " src: source file (usually MLT XML)\n" " dest: destination file\n" " args: space separated libavformat arguments\n"); return 1; } } diff --git a/src/abstractmodel/abstracttreemodel.cpp b/src/abstractmodel/abstracttreemodel.cpp index f34d21cb5..299c00b95 100644 --- a/src/abstractmodel/abstracttreemodel.cpp +++ b/src/abstractmodel/abstracttreemodel.cpp @@ -1,349 +1,349 @@ /*************************************************************************** * 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 "abstracttreemodel.hpp" #include "treeitem.hpp" #include #include #include #include int AbstractTreeModel::currentTreeId = 0; AbstractTreeModel::AbstractTreeModel(QObject *parent) : QAbstractItemModel(parent) { } std::shared_ptr AbstractTreeModel::construct(QObject *parent) { std::shared_ptr self(new AbstractTreeModel(parent)); self->rootItem = TreeItem::construct(QList(), self, true); return self; } AbstractTreeModel::~AbstractTreeModel() { m_allItems.clear(); rootItem.reset(); } int AbstractTreeModel::columnCount(const QModelIndex &parent) const { if (!parent.isValid()) return rootItem->columnCount(); const auto id = (int)parent.internalId(); auto item = getItemById(id); return item->columnCount(); } QVariant AbstractTreeModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) { return QVariant(); } if (role != Qt::DisplayRole) { return QVariant(); } auto item = getItemById((int)index.internalId()); return item->dataColumn(index.column()); } Qt::ItemFlags AbstractTreeModel::flags(const QModelIndex &index) const { const auto flags = QAbstractItemModel::flags(index); if (index.isValid()) { auto item = getItemById((int)index.internalId()); if (item->depth() == 1) { return flags & ~Qt::ItemIsSelectable; } } return flags; } QVariant AbstractTreeModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal && role == Qt::DisplayRole) return rootItem->dataColumn(section); return QVariant(); } QModelIndex AbstractTreeModel::index(int row, int column, const QModelIndex &parent) const { std::shared_ptr parentItem; if (!parent.isValid()) parentItem = rootItem; else parentItem = getItemById((int)parent.internalId()); if (row >= parentItem->childCount()) return QModelIndex(); std::shared_ptr childItem = parentItem->child(row); if (childItem) return createIndex(row, column, quintptr(childItem->getId())); return QModelIndex(); } QModelIndex AbstractTreeModel::parent(const QModelIndex &index) const { if (!index.isValid()) return QModelIndex(); std::shared_ptr childItem = getItemById((int)index.internalId()); std::shared_ptr parentItem = childItem->parentItem().lock(); Q_ASSERT(parentItem); if (parentItem == rootItem) return QModelIndex(); return createIndex(parentItem->row(), 0, quintptr(parentItem->getId())); } int AbstractTreeModel::rowCount(const QModelIndex &parent) const { if (parent.column() > 0) return 0; std::shared_ptr parentItem; if (!parent.isValid()) parentItem = rootItem; else parentItem = getItemById((int)parent.internalId()); return parentItem->childCount(); } QModelIndex AbstractTreeModel::getIndexFromItem(const std::shared_ptr &item) const { if (item == rootItem) { return QModelIndex(); } auto parentIndex = getIndexFromItem(item->parentItem().lock()); return index(item->row(), 0, parentIndex); } QModelIndex AbstractTreeModel::getIndexFromId(int id) const { if (id == rootItem->getId()) { return QModelIndex(); } Q_ASSERT(m_allItems.count(id) > 0); if (auto ptr = m_allItems.at(id).lock()) return getIndexFromItem(ptr); Q_ASSERT(false); return QModelIndex(); } void AbstractTreeModel::notifyRowAboutToAppend(const std::shared_ptr &item) { auto index = getIndexFromItem(item); beginInsertRows(index, item->childCount(), item->childCount()); } void AbstractTreeModel::notifyRowAppended(const std::shared_ptr &row) { Q_UNUSED(row); endInsertRows(); } void AbstractTreeModel::notifyRowAboutToDelete(std::shared_ptr item, int row) { - auto index = getIndexFromItem(std::move(item)); + auto index = getIndexFromItem(item); beginRemoveRows(index, row, row); } void AbstractTreeModel::notifyRowDeleted() { endRemoveRows(); } // static int AbstractTreeModel::getNextId() { return currentTreeId++; } void AbstractTreeModel::registerItem(const std::shared_ptr &item) { int id = item->getId(); Q_ASSERT(m_allItems.count(id) == 0); m_allItems[id] = item; } void AbstractTreeModel::deregisterItem(int id, TreeItem *item) { Q_UNUSED(item); Q_ASSERT(m_allItems.count(id) > 0); m_allItems.erase(id); } std::shared_ptr AbstractTreeModel::getItemById(int id) const { if (id == rootItem->getId()) { return rootItem; } Q_ASSERT(m_allItems.count(id) > 0); return m_allItems.at(id).lock(); } std::shared_ptr AbstractTreeModel::getRoot() const { return rootItem; } bool AbstractTreeModel::checkConsistency() { // first check that the root is all good if (!rootItem || !rootItem->m_isRoot || !rootItem->isInModel() || m_allItems.count(rootItem->getId()) == 0) { qDebug() << !rootItem->m_isRoot << !rootItem->isInModel() << (m_allItems.count(rootItem->getId()) == 0); qDebug() << "ERROR: Model is not valid because root is not properly constructed"; return false; } // Then we traverse the tree from the root, checking the infos on the way std::unordered_set seenIDs; std::queue>> queue; // store (id, (depth, parentId)) queue.push({rootItem->getId(), {0, rootItem->getId()}}); while (!queue.empty()) { auto current = queue.front(); int currentId = current.first, currentDepth = current.second.first; int parentId = current.second.second; queue.pop(); if (seenIDs.count(currentId) != 0) { qDebug() << "ERROR: Invalid tree: Id found twice." << "It either a cycle or a clash in id attribution"; return false; } if (m_allItems.count(currentId) == 0) { qDebug() << "ERROR: Invalid tree: Id not found. Item is not registered"; return false; } auto currentItem = m_allItems[currentId].lock(); if (currentItem->depth() != currentDepth) { qDebug() << "ERROR: Invalid tree: invalid depth info found"; return false; } if (!currentItem->isInModel()) { qDebug() << "ERROR: Invalid tree: item thinks it is not in a model"; return false; } if (currentId != rootItem->getId()) { if ((currentDepth == 0 || currentItem->m_isRoot)) { qDebug() << "ERROR: Invalid tree: duplicate root"; return false; } if (auto ptr = currentItem->parentItem().lock()) { if (ptr->getId() != parentId || ptr->child(currentItem->row())->getId() != currentItem->getId()) { qDebug() << "ERROR: Invalid tree: invalid parent link"; return false; } } else { qDebug() << "ERROR: Invalid tree: invalid parent"; return false; } } // propagate to children int i = 0; for (const auto &child : currentItem->m_childItems) { if (currentItem->child(i) != child) { qDebug() << "ERROR: Invalid tree: invalid child ordering"; return false; } queue.push({child->getId(), {currentDepth + 1, currentId}}); i++; } } return true; } -Fun AbstractTreeModel::addItem_lambda(std::shared_ptr new_item, int parentId) +Fun AbstractTreeModel::addItem_lambda(const std::shared_ptr &new_item, int parentId) { return [this, new_item, parentId]() { /* Insertion is simply setting the parent of the item.*/ std::shared_ptr parent; if (parentId != -1) { parent = getItemById(parentId); if (!parent) { Q_ASSERT(parent); return false; } } return new_item->changeParent(parent); }; } Fun AbstractTreeModel::removeItem_lambda(int id) { return [this, id]() { /* Deletion simply deregister clip and remove it from parent. The actual object is not actually deleted, because a shared_pointer to it is captured by the reverse operation. Actual deletions occurs when the undo object is destroyed. */ auto item = m_allItems[id].lock(); Q_ASSERT(item); if (!item) { return false; } auto parent = item->parentItem().lock(); parent->removeChild(item); return true; }; } Fun AbstractTreeModel::moveItem_lambda(int id, int destRow, bool force) { Fun lambda = []() { return true; }; std::vector> oldStack; auto item = getItemById(id); if (!force && item->row() == destRow) { // nothing to do return lambda; } if (auto parent = item->parentItem().lock()) { if (destRow > parent->childCount() || destRow < 0) { return []() { return false; }; } int parentId = parent->getId(); // remove the element to move oldStack.push_back(item); Fun oper = removeItem_lambda(id); PUSH_LAMBDA(oper, lambda); // remove the tail of the stack for (int i = destRow; i < parent->childCount(); ++i) { auto current = parent->child(i); if (current->getId() != id) { oldStack.push_back(current); oper = removeItem_lambda(current->getId()); PUSH_LAMBDA(oper, lambda); } } // insert back in order for (const auto &elem : oldStack) { oper = addItem_lambda(elem, parentId); PUSH_LAMBDA(oper, lambda); } return lambda; } return []() { return false; }; } diff --git a/src/abstractmodel/abstracttreemodel.hpp b/src/abstractmodel/abstracttreemodel.hpp index b5671d858..24fec46ed 100644 --- a/src/abstractmodel/abstracttreemodel.hpp +++ b/src/abstractmodel/abstracttreemodel.hpp @@ -1,126 +1,126 @@ /*************************************************************************** * Copyright (C) 2017 by Nicolas Carion * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #ifndef ABSTRACTTREEMODEL_H #define ABSTRACTTREEMODEL_H #include "undohelper.hpp" #include #include #include /* @brief This class represents a generic tree hierarchy */ class TreeItem; class AbstractTreeModel : public QAbstractItemModel, public std::enable_shared_from_this { Q_OBJECT public: /* @brief Construct a TreeModel @param parent is the parent object of the model @return a ptr to the created object */ static std::shared_ptr construct(QObject *parent = nullptr); protected: // This is protected. Call construct instead. explicit AbstractTreeModel(QObject *parent = nullptr); public: virtual ~AbstractTreeModel(); /* @brief Given an item from the hierarchy, construct the corresponding ModelIndex */ QModelIndex getIndexFromItem(const std::shared_ptr &item) const; /* @brief Given an item id, construct the corresponding ModelIndex */ QModelIndex getIndexFromId(int id) const; /* @brief Return a ptr to an item given its id */ std::shared_ptr getItemById(int id) const; /* @brief Return a ptr to the root of the tree */ std::shared_ptr getRoot() const; QVariant data(const QModelIndex &index, int role) const override; // This is reimplemented to prevent selection of the categories Qt::ItemFlags flags(const QModelIndex &index) const override; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; QModelIndex parent(const QModelIndex &index) const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override; int columnCount(const QModelIndex &parent = QModelIndex()) const override; /* @brief Helper function to generate a lambda that adds an item to the tree */ - Fun addItem_lambda(std::shared_ptr new_item, int parentId); + Fun addItem_lambda(const std::shared_ptr &new_item, int parentId); /* @brief Helper function to generate a lambda that removes an item from the tree */ Fun removeItem_lambda(int id); /* @brief Helper function to generate a lambda that changes the row of an item */ Fun moveItem_lambda(int id, int destRow, bool force = false); friend class TreeItem; friend class AbstractProjectItem; protected: /* @brief Register a new item. This is a call-back meant to be called from TreeItem */ virtual void registerItem(const std::shared_ptr &item); /* @brief Deregister an item. This is a call-back meant to be called from TreeItem */ virtual void deregisterItem(int id, TreeItem *item); /* @brief Returns the next valid id to give to a new element */ static int getNextId(); /* @brief Send the appropriate notification related to a row that we are appending @param item is the parent item to which row is appended */ void notifyRowAboutToAppend(const std::shared_ptr &item); /* @brief Send the appropriate notification related to a row that we have appended @param row is the new element */ void notifyRowAppended(const std::shared_ptr &row); /* @brief Send the appropriate notification related to a row that we are deleting @param item is the parent of the row being deleted @param row is the index of the row being deleted */ void notifyRowAboutToDelete(std::shared_ptr item, int row); /* @brief Send the appropriate notification related to a row that we have appended @param row is the old element */ void notifyRowDeleted(); /* @brief This is a convenience function that helps check if the tree is in a valid state */ virtual bool checkConsistency(); protected: std::shared_ptr rootItem; std::unordered_map> m_allItems; static int currentTreeId; }; #endif diff --git a/src/abstractmodel/treeitem.cpp b/src/abstractmodel/treeitem.cpp index 4fde1caf2..b0d97f9f2 100644 --- a/src/abstractmodel/treeitem.cpp +++ b/src/abstractmodel/treeitem.cpp @@ -1,291 +1,291 @@ /*************************************************************************** * 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 "treeitem.hpp" #include "abstracttreemodel.hpp" #include #include #include TreeItem::TreeItem(const QList &data, const std::shared_ptr &model, bool isRoot, int id) : m_itemData(data) , m_model(model) , m_depth(0) , m_id(id == -1 ? AbstractTreeModel::getNextId() : id) , m_isInModel(false) , m_isRoot(isRoot) { } std::shared_ptr TreeItem::construct(const QList &data, std::shared_ptr model, bool isRoot, int id) { - std::shared_ptr self(new TreeItem(data, std::move(model), isRoot, id)); + std::shared_ptr self(new TreeItem(data, model, isRoot, id)); baseFinishConstruct(self); return self; } // static void TreeItem::baseFinishConstruct(const std::shared_ptr &self) { if (self->m_isRoot) { registerSelf(self); } } TreeItem::~TreeItem() { deregisterSelf(); } std::shared_ptr TreeItem::appendChild(const QList &data) { if (auto ptr = m_model.lock()) { auto child = construct(data, ptr, false); appendChild(child); return child; } qDebug() << "ERROR: Something went wrong when appending child in TreeItem. Model is not available anymore"; Q_ASSERT(false); return std::shared_ptr(); } -bool TreeItem::appendChild(std::shared_ptr child) +bool TreeItem::appendChild(const std::shared_ptr &child) { if (hasAncestor(child->getId())) { // in that case, we are trying to create a cycle, abort return false; } if (auto oldParent = child->parentItem().lock()) { if (oldParent->getId() == m_id) { // no change needed return true; } else { // in that case a call to removeChild should have been carried out qDebug() << "ERROR: trying to append a child that alrealdy has a parent"; return false; } } if (auto ptr = m_model.lock()) { ptr->notifyRowAboutToAppend(shared_from_this()); child->updateParent(shared_from_this()); int id = child->getId(); auto it = m_childItems.insert(m_childItems.end(), child); m_iteratorTable[id] = it; registerSelf(child); ptr->notifyRowAppended(child); return true; } qDebug() << "ERROR: Something went wrong when appending child in TreeItem. Model is not available anymore"; Q_ASSERT(false); return false; } -void TreeItem::moveChild(int ix, std::shared_ptr child) +void TreeItem::moveChild(int ix, const std::shared_ptr &child) { if (auto ptr = m_model.lock()) { auto parentPtr = child->m_parentItem.lock(); if (parentPtr && parentPtr->getId() != m_id) { parentPtr->removeChild(child); } else { // deletion of child auto it = m_iteratorTable[child->getId()]; m_childItems.erase(it); } ptr->notifyRowAboutToAppend(shared_from_this()); child->updateParent(shared_from_this()); int id = child->getId(); auto pos = m_childItems.begin(); std::advance(pos, ix); auto it = m_childItems.insert(pos, child); m_iteratorTable[id] = it; ptr->notifyRowAppended(child); m_isInModel = true; } else { qDebug() << "ERROR: Something went wrong when moving child in TreeItem. Model is not available anymore"; Q_ASSERT(false); } } void TreeItem::removeChild(const std::shared_ptr &child) { if (auto ptr = m_model.lock()) { ptr->notifyRowAboutToDelete(shared_from_this(), child->row()); // get iterator corresponding to child Q_ASSERT(m_iteratorTable.count(child->getId()) > 0); auto it = m_iteratorTable[child->getId()]; // deletion of child m_childItems.erase(it); // clean iterator table m_iteratorTable.erase(child->getId()); child->m_depth = 0; child->m_parentItem.reset(); child->deregisterSelf(); ptr->notifyRowDeleted(); } else { qDebug() << "ERROR: Something went wrong when removing child in TreeItem. Model is not available anymore"; Q_ASSERT(false); } } bool TreeItem::changeParent(std::shared_ptr newParent) { Q_ASSERT(!m_isRoot); if (m_isRoot) return false; std::shared_ptr oldParent; if ((oldParent = m_parentItem.lock())) { oldParent->removeChild(shared_from_this()); } bool res = true; if (newParent) { res = newParent->appendChild(shared_from_this()); if (res) { m_parentItem = newParent; } else if (oldParent) { // something went wrong, we have to reset the parent. bool reverse = oldParent->appendChild(shared_from_this()); Q_ASSERT(reverse); } } return res; } std::shared_ptr TreeItem::child(int row) const { Q_ASSERT(row >= 0 && row < (int)m_childItems.size()); auto it = m_childItems.cbegin(); std::advance(it, row); return (*it); } int TreeItem::childCount() const { return (int)m_childItems.size(); } int TreeItem::columnCount() const { return m_itemData.count(); } QVariant TreeItem::dataColumn(int column) const { return m_itemData.value(column); } -void TreeItem::setData(int column, const QVariant dataColumn) +void TreeItem::setData(int column, const QVariant &dataColumn) { m_itemData[column] = dataColumn; } std::weak_ptr TreeItem::parentItem() const { return m_parentItem; } int TreeItem::row() const { if (auto ptr = m_parentItem.lock()) { // we compute the distance in the parent's children list auto it = ptr->m_childItems.begin(); return (int)std::distance(it, (decltype(it))ptr->m_iteratorTable.at(m_id)); } return -1; } int TreeItem::depth() const { return m_depth; } int TreeItem::getId() const { return m_id; } bool TreeItem::isInModel() const { return m_isInModel; } -void TreeItem::registerSelf(std::shared_ptr self) +void TreeItem::registerSelf(const std::shared_ptr &self) { for (const auto &child : self->m_childItems) { registerSelf(child); } if (auto ptr = self->m_model.lock()) { ptr->registerItem(self); self->m_isInModel = true; } else { qDebug() << "Error : construction of treeItem failed because parent model is not available anymore"; Q_ASSERT(false); } } void TreeItem::deregisterSelf() { for (const auto &child : m_childItems) { child->deregisterSelf(); } if (m_isInModel) { if (auto ptr = m_model.lock()) { ptr->deregisterItem(m_id, this); m_isInModel = false; } } } bool TreeItem::hasAncestor(int id) { if (m_id == id) { return true; } if (auto ptr = m_parentItem.lock()) { return ptr->hasAncestor(id); } return false; } bool TreeItem::isRoot() const { return m_isRoot; } void TreeItem::updateParent(std::shared_ptr parent) { m_parentItem = parent; if (parent) { m_depth = parent->m_depth + 1; } } std::vector> TreeItem::getLeaves() { if (childCount() == 0) { return {shared_from_this()}; } std::vector> leaves; for (const auto &c : m_childItems) { for (const auto &l : c->getLeaves()) { leaves.push_back(l); } } return leaves; } diff --git a/src/abstractmodel/treeitem.hpp b/src/abstractmodel/treeitem.hpp index b856fcb47..889c3fe13 100644 --- a/src/abstractmodel/treeitem.hpp +++ b/src/abstractmodel/treeitem.hpp @@ -1,189 +1,189 @@ /*************************************************************************** * Copyright (C) 2017 by Nicolas Carion * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #ifndef TREEITEM_H #define TREEITEM_H #include "definitions.h" #include #include #include #include /* @brief This class is a generic class to represent items of a tree-like model It works in tandem with AbstractTreeModel or one of its derived classes. There is a registration mechanism that takes place: each TreeItem holds a unique Id that can allow to retrieve it directly from the model. A TreeItem registers itself to the model as soon as it gets a proper parent (the node above it in the hierarchy). This means that upon creation, the TreeItem is NOT registered, because at this point it doesn't belong to any parent. The only exception is for the rootItem, which is always registered. Note that the root is a special object. In particular, it must stay at the root and must not be declared as the child of any other item. */ class AbstractTreeModel; class TreeItem : public enable_shared_from_this_virtual { public: /* @brief Construct a TreeItem @param data List of data elements (columns) of the created item @param model Pointer to the model to which this elem belongs to @param parentItem address of the parent if the child is not orphan @param isRoot is true if the object is the topmost item of the tree @param id of the newly created item. If left to -1, the id is assigned automatically @return a ptr to the constructed item */ static std::shared_ptr construct(const QList &data, std::shared_ptr model, bool isRoot, int id = -1); friend class AbstractTreeModel; protected: // This is protected. Call construct instead explicit TreeItem(const QList &data, const std::shared_ptr &model, bool isRoot, int id = -1); public: virtual ~TreeItem(); /* @brief Creates a child of the current item @param data: List of data elements (columns) to init the child with. */ std::shared_ptr appendChild(const QList &data); /* @brief Appends an already created child Useful for example if the child should be a subclass of TreeItem @return true on success. Otherwise, nothing is modified. */ - bool appendChild(std::shared_ptr child); - void moveChild(int ix, std::shared_ptr child); + bool appendChild(const std::shared_ptr &child); + void moveChild(int ix, const std::shared_ptr &child); /* @brief Remove given child from children list. The parent of the child is updated accordingly */ void removeChild(const std::shared_ptr &child); /* @brief Change the parent of the current item. Structures are modified accordingly */ virtual bool changeParent(std::shared_ptr newParent); /* @brief Retrieves a child of the current item @param row is the index of the child to retrieve */ std::shared_ptr child(int row) const; /* @brief Returns a vector containing a pointer to all the leaves in the subtree rooted in this element */ std::vector> getLeaves(); /* @brief Return the number of children */ int childCount() const; /* @brief Return the number of data fields (columns) */ int columnCount() const; /* @brief Return the content of a column @param column Index of the column to look-up */ QVariant dataColumn(int column) const; - void setData(int column, const QVariant dataColumn); + void setData(int column, const QVariant &dataColumn); /* @brief Return the index of current item amongst father's children Returns -1 on error (eg: no parent set) */ int row() const; /* @brief Return a ptr to the parent item */ std::weak_ptr parentItem() const; /* @brief Return the depth of the current item*/ int depth() const; /* @brief Return the id of the current item*/ int getId() const; /* @brief Return true if the current item has been registered */ bool isInModel() const; /* @brief This is similar to the std::accumulate function, except that it operates on the whole subtree @param init is the initial value of the operation @param is the binary op to apply (signature should be (T, shared_ptr)->T) */ template T accumulate(T init, BinaryOperation op); template T accumulate_const(T init, BinaryOperation op) const; /* @brief Return true if the current item has the item with given id as an ancestor */ bool hasAncestor(int id); /* @brief Return true if the item thinks it is a root. Note that it should be consistent with what the model thinks, but it may have been messed up at some point if someone wrongly constructed the object with isRoot = true */ bool isRoot() const; protected: /* @brief Finish construction of object given its pointer This is a separated function so that it can be called from derived classes */ static void baseFinishConstruct(const std::shared_ptr &self); /* @brief Helper functions to handle registration / deregistration to the model */ - static void registerSelf(std::shared_ptr self); + static void registerSelf(const std::shared_ptr &self); void deregisterSelf(); /* @brief Reflect update of the parent ptr (for example set the correct depth) This is meant to be overridden in derived classes @param ptr is the pointer to the new parent */ virtual void updateParent(std::shared_ptr parent); std::list> m_childItems; std::unordered_map>::iterator> m_iteratorTable; // this logs the iterator associated which each child id. This allows easy access of a child based on its id. QList m_itemData; std::weak_ptr m_parentItem; std::weak_ptr m_model; int m_depth; int m_id; bool m_isInModel; bool m_isRoot; }; template T TreeItem::accumulate(T init, BinaryOperation op) { T res = op(init, shared_from_this()); for (const auto &c : m_childItems) { res = c->accumulate(res, op); } return res; } template T TreeItem::accumulate_const(T init, BinaryOperation op) const { T res = op(init, shared_from_this()); for (const auto &c : m_childItems) { res = c->accumulate_const(res, op); } return res; } #endif diff --git a/src/assets/assetlist/model/assetfilter.cpp b/src/assets/assetlist/model/assetfilter.cpp index 1c9c23c54..86a4341fa 100644 --- a/src/assets/assetlist/model/assetfilter.cpp +++ b/src/assets/assetlist/model/assetfilter.cpp @@ -1,176 +1,176 @@ /*************************************************************************** * 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 "assetfilter.hpp" #include "abstractmodel/abstracttreemodel.hpp" #include "abstractmodel/treeitem.hpp" #include "assettreemodel.hpp" #include AssetFilter::AssetFilter(QObject *parent) : QSortFilterProxyModel(parent) , m_name_enabled(false) { setFilterRole(Qt::DisplayRole); setSortRole(Qt::DisplayRole); setDynamicSortFilter(false); } void AssetFilter::setFilterName(bool enabled, const QString &pattern) { m_name_enabled = enabled; m_name_value = pattern; invalidateFilter(); if (rowCount() > 1) { sort(0); } } bool AssetFilter::filterName(const std::shared_ptr &item) const { if (!m_name_enabled) { return true; } QString itemText = item->dataColumn(AssetTreeModel::nameCol).toString(); itemText = itemText.normalized(QString::NormalizationForm_D).remove(QRegExp(QStringLiteral("[^a-zA-Z0-9\\s]"))); QString patt = m_name_value.normalized(QString::NormalizationForm_D).remove(QRegExp(QStringLiteral("[^a-zA-Z0-9\\s]"))); return itemText.contains(patt, Qt::CaseInsensitive); } bool AssetFilter::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const { QModelIndex row = sourceModel()->index(sourceRow, 0, sourceParent); auto *model = static_cast(sourceModel()); std::shared_ptr item = model->getItemById((int)row.internalId()); if (item->dataColumn(AssetTreeModel::idCol) == QStringLiteral("root")) { // In that case, we have a category. We hide it if it does not have children. QModelIndex category = sourceModel()->index(sourceRow, 0, sourceParent); if (!category.isValid()) { return false; } bool accepted = false; for (int i = 0; i < sourceModel()->rowCount(category) && !accepted; ++i) { accepted = filterAcceptsRow(i, category); } return accepted; } return applyAll(item); } bool AssetFilter::isVisible(const QModelIndex &sourceIndex) { auto parent = sourceModel()->parent(sourceIndex); return filterAcceptsRow(sourceIndex.row(), parent); } bool AssetFilter::applyAll(std::shared_ptr item) const { - return filterName(std::move(item)); + return filterName(item); } QModelIndex AssetFilter::getNextChild(const QModelIndex ¤t) { QModelIndex nextItem = current.sibling(current.row() + 1, current.column()); if (!nextItem.isValid()) { QModelIndex folder = index(current.parent().row() + 1, 0, QModelIndex()); if (!folder.isValid()) { return current; } while (folder.isValid() && rowCount(folder) == 0) { folder = folder.sibling(folder.row() + 1, folder.column()); } if (folder.isValid() && rowCount(folder) > 0) { return index(0, current.column(), folder); } nextItem = current; } return nextItem; } QModelIndex AssetFilter::getPreviousChild(const QModelIndex ¤t) { QModelIndex nextItem = current.sibling(current.row() - 1, current.column()); if (!nextItem.isValid()) { QModelIndex folder = index(current.parent().row() - 1, 0, QModelIndex()); if (!folder.isValid()) { return current; } while (folder.isValid() && rowCount(folder) == 0) { folder = folder.sibling(folder.row() - 1, folder.column()); } if (folder.isValid() && rowCount(folder) > 0) { return index(rowCount(folder) - 1, current.column(), folder); } nextItem = current; } return nextItem; } QModelIndex AssetFilter::firstVisibleItem(const QModelIndex ¤t) { if (current.isValid() && isVisible(mapToSource(current))) { return current; } QModelIndex folder = index(0, 0, QModelIndex()); if (!folder.isValid()) { return current; } while (folder.isValid() && rowCount(folder) == 0) { folder = index(folder.row() + 1, 0, QModelIndex()); } if (rowCount(folder) > 0) { return index(0, 0, folder); } return current; } QModelIndex AssetFilter::getCategory(int catRow) { QModelIndex cat = index(catRow, 0, QModelIndex()); return cat; } QVariantList AssetFilter::getCategories() { QVariantList list; for (int i = 0; i < sourceModel()->rowCount(); i++) { QModelIndex cat = getCategory(i); if (cat.isValid()) { list << cat; } } return list; } QModelIndex AssetFilter::getModelIndex(QModelIndex current) { QModelIndex sourceIndex = mapToSource(current); return sourceIndex; // this returns an integer } QModelIndex AssetFilter::getProxyIndex(QModelIndex current) { QModelIndex sourceIndex = mapFromSource(current); return sourceIndex; // this returns an integer } diff --git a/src/assets/assetpanel.cpp b/src/assets/assetpanel.cpp index 7fa04e104..6ea3f0269 100644 --- a/src/assets/assetpanel.cpp +++ b/src/assets/assetpanel.cpp @@ -1,360 +1,360 @@ /*************************************************************************** * 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 "assetpanel.hpp" #include "core.h" #include "definitions.h" #include "effects/effectstack/model/effectitemmodel.hpp" #include "effects/effectstack/model/effectstackmodel.hpp" #include "effects/effectstack/view/effectstackview.hpp" #include "kdenlivesettings.h" #include "model/assetparametermodel.hpp" #include "transitions/transitionsrepository.hpp" #include "transitions/view/transitionstackview.hpp" #include "view/assetparameterview.hpp" #include #include #include #include #include #include #include #include #include #include #include #include AssetPanel::AssetPanel(QWidget *parent) : QWidget(parent) , m_lay(new QVBoxLayout(this)) , m_assetTitle(new KSqueezedTextLabel(this)) , m_container(new QWidget(this)) , m_transitionWidget(new TransitionStackView(this)) , m_effectStackWidget(new EffectStackView(this)) { QToolBar *buttonToolbar = new QToolBar(this); buttonToolbar->addWidget(m_assetTitle); int size = style()->pixelMetric(QStyle::PM_SmallIconSize); QSize iconSize(size, size); buttonToolbar->setIconSize(iconSize); // spacer QWidget *empty = new QWidget(); empty->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Maximum); buttonToolbar->addWidget(empty); m_switchBuiltStack = new QToolButton(this); m_switchBuiltStack->setIcon(QIcon::fromTheme(QStringLiteral("adjustlevels"))); m_switchBuiltStack->setToolTip(i18n("Adjust clip")); m_switchBuiltStack->setCheckable(true); m_switchBuiltStack->setChecked(KdenliveSettings::showbuiltstack()); m_switchBuiltStack->setVisible(false); // connect(m_switchBuiltStack, &QToolButton::toggled, m_effectStackWidget, &EffectStackView::switchBuiltStack); buttonToolbar->addWidget(m_switchBuiltStack); m_splitButton = new KDualAction(i18n("Normal view"), i18n("Compare effect"), this); m_splitButton->setActiveIcon(QIcon::fromTheme(QStringLiteral("view-right-close"))); m_splitButton->setInactiveIcon(QIcon::fromTheme(QStringLiteral("view-split-left-right"))); m_splitButton->setToolTip(i18n("Compare effect")); m_splitButton->setVisible(false); connect(m_splitButton, &KDualAction::activeChangedByUser, this, &AssetPanel::processSplitEffect); buttonToolbar->addAction(m_splitButton); m_enableStackButton = new KDualAction(i18n("Effects disabled"), i18n("Effects enabled"), this); m_enableStackButton->setInactiveIcon(QIcon::fromTheme(QStringLiteral("hint"))); m_enableStackButton->setActiveIcon(QIcon::fromTheme(QStringLiteral("visibility"))); connect(m_enableStackButton, &KDualAction::activeChangedByUser, this, &AssetPanel::enableStack); m_enableStackButton->setVisible(false); buttonToolbar->addAction(m_enableStackButton); m_timelineButton = new KDualAction(i18n("Hide keyframes"), i18n("Display keyframes in timeline"), this); m_timelineButton->setInactiveIcon(QIcon::fromTheme(QStringLiteral("adjustlevels"))); m_timelineButton->setActiveIcon(QIcon::fromTheme(QStringLiteral("adjustlevels"))); m_timelineButton->setToolTip(i18n("Display keyframes in timeline")); m_timelineButton->setVisible(false); connect(m_timelineButton, &KDualAction::activeChangedByUser, this, &AssetPanel::showKeyframes); buttonToolbar->addAction(m_timelineButton); m_lay->addWidget(buttonToolbar); m_lay->setContentsMargins(0, 0, 0, 0); m_lay->setSpacing(0); QVBoxLayout *lay = new QVBoxLayout(m_container); lay->setContentsMargins(0, 0, 0, 0); lay->addWidget(m_transitionWidget); lay->addWidget(m_effectStackWidget); QScrollArea *sc = new QScrollArea; sc->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); sc->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); sc->setFrameStyle(QFrame::NoFrame); sc->setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::MinimumExpanding)); m_container->setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::MinimumExpanding)); sc->setWidgetResizable(true); m_lay->addWidget(sc); sc->setWidget(m_container); m_transitionWidget->setVisible(false); m_effectStackWidget->setVisible(false); updatePalette(); connect(m_effectStackWidget, &EffectStackView::seekToPos, this, &AssetPanel::seekToPos); connect(m_effectStackWidget, &EffectStackView::reloadEffect, this, &AssetPanel::reloadEffect); connect(m_transitionWidget, &TransitionStackView::seekToTransPos, this, &AssetPanel::seekToPos); connect(m_effectStackWidget, &EffectStackView::updateEnabledState, [this]() { m_enableStackButton->setActive(m_effectStackWidget->isStackEnabled()); }); } -void AssetPanel::showTransition(int tid, std::shared_ptr transitionModel) +void AssetPanel::showTransition(int tid, const std::shared_ptr &transitionModel) { Q_UNUSED(tid) ObjectId id = transitionModel->getOwnerId(); if (m_transitionWidget->stackOwner() == id) { // already on this effect stack, do nothing return; } clear(); QString transitionId = transitionModel->getAssetId(); QString transitionName = TransitionsRepository::get()->getName(transitionId); m_assetTitle->setText(i18n("%1 properties", transitionName)); m_transitionWidget->setVisible(true); m_timelineButton->setVisible(true); m_enableStackButton->setVisible(false); m_transitionWidget->setModel(transitionModel, QSize(), true); } -void AssetPanel::showEffectStack(const QString &itemName, std::shared_ptr effectsModel, QSize frameSize, bool showKeyframes) +void AssetPanel::showEffectStack(const QString &itemName, const std::shared_ptr &effectsModel, QSize frameSize, bool showKeyframes) { m_splitButton->setActive(false); if (effectsModel == nullptr) { // Item is not ready m_splitButton->setVisible(false); m_enableStackButton->setVisible(false); clear(); return; } ObjectId id = effectsModel->getOwnerId(); if (m_effectStackWidget->stackOwner() == id) { // already on this effect stack, do nothing return; } clear(); QString title; bool showSplit = false; bool enableKeyframes = false; switch (id.first) { case ObjectType::TimelineClip: title = i18n("%1 effects", itemName); showSplit = true; enableKeyframes = true; break; case ObjectType::TimelineComposition: title = i18n("%1 parameters", itemName); enableKeyframes = true; break; case ObjectType::TimelineTrack: title = i18n("Track %1 effects", itemName); // TODO: track keyframes // enableKeyframes = true; break; case ObjectType::BinClip: title = i18n("Bin %1 effects", itemName); showSplit = true; break; default: title = itemName; break; } m_assetTitle->setText(title); m_splitButton->setVisible(showSplit); m_enableStackButton->setVisible(id.first != ObjectType::TimelineComposition); m_enableStackButton->setActive(effectsModel->isStackEnabled()); if (showSplit) { m_splitButton->setEnabled(effectsModel->rowCount() > 0); QObject::connect(effectsModel.get(), &EffectStackModel::dataChanged, [&]() { if (m_effectStackWidget->isEmpty()) { m_splitButton->setActive(false); } m_splitButton->setEnabled(!m_effectStackWidget->isEmpty()); }); } m_timelineButton->setVisible(enableKeyframes); m_timelineButton->setActive(showKeyframes); // Disable built stack until properly implemented // m_switchBuiltStack->setVisible(true); m_effectStackWidget->setVisible(true); m_effectStackWidget->setModel(effectsModel, frameSize); } void AssetPanel::clearAssetPanel(int itemId) { ObjectId id = m_effectStackWidget->stackOwner(); if (id.first == ObjectType::TimelineClip && id.second == itemId) { clear(); } else { id = m_transitionWidget->stackOwner(); if (id.first == ObjectType::TimelineComposition && id.second == itemId) { clear(); } } } void AssetPanel::clear() { m_transitionWidget->setVisible(false); m_transitionWidget->unsetModel(); m_effectStackWidget->setVisible(false); m_splitButton->setVisible(false); m_timelineButton->setVisible(false); m_switchBuiltStack->setVisible(false); m_effectStackWidget->unsetModel(); m_assetTitle->setText(QString()); } void AssetPanel::updatePalette() { QString styleSheet = getStyleSheet(); setStyleSheet(styleSheet); m_transitionWidget->setStyleSheet(styleSheet); m_effectStackWidget->setStyleSheet(styleSheet); } // static const QString AssetPanel::getStyleSheet() { KColorScheme scheme(QApplication::palette().currentColorGroup(), KColorScheme::View); QColor selected_bg = scheme.decoration(KColorScheme::FocusColor).color(); QColor hgh = KColorUtils::mix(QApplication::palette().window().color(), selected_bg, 0.2); QColor hover_bg = scheme.decoration(KColorScheme::HoverColor).color(); QColor light_bg = scheme.shade(KColorScheme::LightShade); QColor alt_bg = scheme.background(KColorScheme::NormalBackground).color(); QString stylesheet; // effect background stylesheet.append(QStringLiteral("QFrame#decoframe {border-bottom:2px solid " "palette(mid);background: transparent} QFrame#decoframe[active=\"true\"] {background: %1;}") .arg(hgh.name())); // effect in group background stylesheet.append( QStringLiteral("QFrame#decoframesub {border-top:1px solid palette(light);} QFrame#decoframesub[active=\"true\"] {background: %1;}").arg(hgh.name())); // group background stylesheet.append(QStringLiteral("QFrame#decoframegroup {border:2px solid palette(dark);margin:0px;margin-top:2px;} ")); // effect title bar stylesheet.append(QStringLiteral("QFrame#frame {margin-bottom:2px;} QFrame#frame[target=\"true\"] " "{background: palette(highlight);}")); // group effect title bar stylesheet.append(QStringLiteral("QFrame#framegroup {background: palette(dark);} " "QFrame#framegroup[target=\"true\"] {background: palette(highlight);} ")); // draggable effect bar content stylesheet.append(QStringLiteral("QProgressBar::chunk:horizontal {background: palette(button);border-top-left-radius: 4px;border-bottom-left-radius: 4px;} " "QProgressBar::chunk:horizontal#dragOnly {background: %1;border-top-left-radius: 4px;border-bottom-left-radius: 4px;} " "QProgressBar::chunk:horizontal:hover {background: %2;}") .arg(alt_bg.name(), selected_bg.name())); // draggable effect bar stylesheet.append(QStringLiteral("QProgressBar:horizontal {border: 1px solid palette(dark);border-top-left-radius: 4px;border-bottom-left-radius: " "4px;border-right:0px;background:%3;padding: 0px;text-align:left center} QProgressBar:horizontal:disabled {border: 1px " "solid palette(button)} QProgressBar:horizontal#dragOnly {background: %3} QProgressBar:horizontal[inTimeline=\"true\"] { " "border: 1px solid %1;border-right: 0px;background: %2;padding: 0px;text-align:left center } " "QProgressBar::chunk:horizontal[inTimeline=\"true\"] {background: %1;}") .arg(hover_bg.name(), light_bg.name(), alt_bg.name())); // spin box for draggable widget stylesheet.append( QStringLiteral("QAbstractSpinBox#dragBox {border: 1px solid palette(dark);border-top-right-radius: 4px;border-bottom-right-radius: " "4px;padding-right:0px;} QAbstractSpinBox::down-button#dragBox {width:0px;padding:0px;} QAbstractSpinBox:disabled#dragBox {border: 1px " "solid palette(button);} QAbstractSpinBox::up-button#dragBox {width:0px;padding:0px;} QAbstractSpinBox[inTimeline=\"true\"]#dragBox { " "border: 1px solid %1;} QAbstractSpinBox:hover#dragBox {border: 1px solid %2;} ") .arg(hover_bg.name(), selected_bg.name())); // group editable labels stylesheet.append(QStringLiteral("MyEditableLabel { background-color: transparent; color: palette(bright-text); border-radius: 2px;border: 1px solid " "transparent;} MyEditableLabel:hover {border: 1px solid palette(highlight);} ")); // transparent qcombobox stylesheet.append(QStringLiteral("QComboBox { background-color: transparent;} ")); return stylesheet; } void AssetPanel::processSplitEffect(bool enable) { ObjectType id = m_effectStackWidget->stackOwner().first; if (id == ObjectType::TimelineClip) { emit doSplitEffect(enable); } else if (id == ObjectType::BinClip) { emit doSplitBinEffect(enable); } } void AssetPanel::showKeyframes(bool enable) { if (m_transitionWidget->isVisible()) { pCore->showClipKeyframes(m_transitionWidget->stackOwner(), enable); } else { pCore->showClipKeyframes(m_effectStackWidget->stackOwner(), enable); } } ObjectId AssetPanel::effectStackOwner() { if (m_transitionWidget->isVisible()) { return m_transitionWidget->stackOwner(); } if (!m_effectStackWidget->isVisible()) { return ObjectId(ObjectType::NoItem, -1); } return m_effectStackWidget->stackOwner(); } void AssetPanel::parameterChanged(QString name, int value) { Q_UNUSED(name) emit changeSpeed(value); } bool AssetPanel::addEffect(const QString &effectId) { if (!m_effectStackWidget->isVisible()) { return false; } return m_effectStackWidget->addEffect(effectId); } void AssetPanel::enableStack(bool enable) { if (!m_effectStackWidget->isVisible()) { return; } m_effectStackWidget->enableStack(enable); } void AssetPanel::deleteCurrentEffect() { if (m_effectStackWidget->isVisible()) { m_effectStackWidget->removeCurrentEffect(); } } diff --git a/src/assets/assetpanel.hpp b/src/assets/assetpanel.hpp index b44f65749..3dce5c120 100644 --- a/src/assets/assetpanel.hpp +++ b/src/assets/assetpanel.hpp @@ -1,105 +1,105 @@ /*************************************************************************** * Copyright (C) 2017 by Nicolas Carion * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #ifndef ASSETPANEL_H #define ASSETPANEL_H #include #include #include #include "definitions.h" class KSqueezedTextLabel; class KDualAction; class QToolButton; /** @brief This class is the widget that provides interaction with the asset currently selected. That is, it either displays an effectStack or the parameters of a transition */ class AssetParameterModel; class AssetParameterView; class EffectStackModel; class EffectStackView; class TransitionStackView; class QLabel; class AssetPanel : public QWidget { Q_OBJECT public: AssetPanel(QWidget *parent); /* @brief Shows the parameters of the given transition model */ - void showTransition(int tid, std::shared_ptr transition_model); + void showTransition(int tid, const std::shared_ptr &transition_model); /* @brief Shows the parameters of the given effect stack model */ - void showEffectStack(const QString &itemName, std::shared_ptr effectsModel, QSize frameSize, bool showKeyframes); + void showEffectStack(const QString &itemName, const std::shared_ptr &effectsModel, QSize frameSize, bool showKeyframes); /* @brief Clear the panel so that it doesn't display anything */ void clear(); /* @brief This method should be called when the style changes */ void updatePalette(); /* @brief Returns the object type / id of effectstack owner */ ObjectId effectStackOwner(); /* @brief Add an effect to the current stack owner */ bool addEffect(const QString &effectId); public slots: /** @brief Clear panel if displaying itemId */ void clearAssetPanel(int itemId); void parameterChanged(QString name, int value); void deleteCurrentEffect(); protected: /** @brief Return the stylesheet used to display the panel (based on current palette). */ static const QString getStyleSheet(); QVBoxLayout *m_lay; KSqueezedTextLabel *m_assetTitle; QWidget *m_container; TransitionStackView *m_transitionWidget; EffectStackView *m_effectStackWidget; private: QToolButton *m_switchBuiltStack; KDualAction *m_splitButton; KDualAction *m_enableStackButton; KDualAction *m_timelineButton; private slots: void processSplitEffect(bool enable); /** Displays the owner clip keyframes in timeline */ void showKeyframes(bool enable); /** Enable / disable effect stack */ void enableStack(bool enable); signals: void doSplitEffect(bool); void doSplitBinEffect(bool); void seekToPos(int); void changeSpeed(int); void reloadEffect(const QString &path); }; #endif diff --git a/src/assets/keyframes/model/corners/cornershelper.cpp b/src/assets/keyframes/model/corners/cornershelper.cpp index 55ad8b5f4..83ec196f4 100644 --- a/src/assets/keyframes/model/corners/cornershelper.cpp +++ b/src/assets/keyframes/model/corners/cornershelper.cpp @@ -1,99 +1,99 @@ /* Copyright (C) 2018 Jean-Baptiste Mardelle This file is part of Kdenlive. See www.kdenlive.org. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "cornershelper.hpp" #include "assets/keyframes/model/keyframemodellist.hpp" #include "assets/model/assetparametermodel.hpp" #include "core.h" #include "gentime.h" #include "monitor/monitor.h" #include - +#include CornersHelper::CornersHelper(Monitor *monitor, std::shared_ptr model, QPersistentModelIndex index, QObject *parent) - : KeyframeMonitorHelper(monitor, model, index, parent) + : KeyframeMonitorHelper(monitor, std::move(model), std::move(index), parent) { } void CornersHelper::slotUpdateFromMonitorData(const QVariantList &v) { const QVariantList points = QVariant(v).toList(); QSize frameSize = pCore->getCurrentFrameSize(); int ix = 0; for (int i = 0; i < points.size(); i++) { QPointF pt = points.at(i).toPointF(); double x = (pt.x() / frameSize.width() + 1) / 3; double y = (pt.y() / frameSize.height() + 1) / 3; emit updateKeyframeData(m_indexes.at(ix), x); emit updateKeyframeData(m_indexes.at(ix + 1), y); ix += 2; } } void CornersHelper::refreshParams(int pos) { QVariantList points{QPointF(), QPointF(), QPointF(), QPointF()}; QList coords; QSize frameSize = pCore->getCurrentFrameSize(); for (const auto &ix : m_indexes) { ParamType type = m_model->data(ix, AssetParameterModel::TypeRole).value(); if (type != ParamType::KeyframeParam) { continue; } int paramName = m_model->data(ix, AssetParameterModel::NameRole).toInt(); if (paramName > 7) { continue; } double value = m_model->getKeyframeModel()->getInterpolatedValue(pos, ix).toDouble(); value = ((3 * value) - 1) * (paramName % 2 == 0 ? frameSize.width() : frameSize.height()); switch (paramName) { case 0: points[0] = QPointF(value, points.at(0).toPointF().y()); break; case 1: points[0] = QPointF(points.at(0).toPointF().x(), value); break; case 2: points[1] = QPointF(value, points.at(1).toPointF().y()); break; case 3: points[1] = QPointF(points.at(1).toPointF().x(), value); break; case 4: points[2] = QPointF(value, points.at(2).toPointF().y()); break; case 5: points[2] = QPointF(points.at(2).toPointF().x(), value); break; case 6: points[3] = QPointF(value, points.at(3).toPointF().y()); break; case 7: points[3] = QPointF(points.at(3).toPointF().x(), value); break; default: break; } } if (m_monitor) { m_monitor->setUpEffectGeometry(QRect(), points); } } diff --git a/src/assets/keyframes/model/keyframemodel.cpp b/src/assets/keyframes/model/keyframemodel.cpp index ba65767b7..bb9daa382 100644 --- a/src/assets/keyframes/model/keyframemodel.cpp +++ b/src/assets/keyframes/model/keyframemodel.cpp @@ -1,1215 +1,1216 @@ /*************************************************************************** * 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 "keyframemodel.hpp" #include "core.h" #include "doc/docundostack.hpp" #include "macros.hpp" #include "rotoscoping/bpoint.h" #include "rotoscoping/rotohelper.hpp" #include "profiles/profilemodel.hpp" #include #include #include +#include KeyframeModel::KeyframeModel(std::weak_ptr model, const QModelIndex &index, std::weak_ptr undo_stack, QObject *parent) : QAbstractListModel(parent) , m_model(std::move(model)) , m_undoStack(std::move(undo_stack)) , m_index(index) , m_lastData() , m_lock(QReadWriteLock::Recursive) { qDebug() << "Construct keyframemodel. Checking model:" << m_model.expired(); if (auto ptr = m_model.lock()) { m_paramType = ptr->data(m_index, AssetParameterModel::TypeRole).value(); } setup(); refresh(); } void KeyframeModel::setup() { // We connect the signals of the abstractitemmodel to a more generic one. connect(this, &KeyframeModel::columnsMoved, this, &KeyframeModel::modelChanged); connect(this, &KeyframeModel::columnsRemoved, this, &KeyframeModel::modelChanged); connect(this, &KeyframeModel::columnsInserted, this, &KeyframeModel::modelChanged); connect(this, &KeyframeModel::rowsMoved, this, &KeyframeModel::modelChanged); connect(this, &KeyframeModel::rowsRemoved, this, &KeyframeModel::modelChanged); connect(this, &KeyframeModel::rowsInserted, this, &KeyframeModel::modelChanged); connect(this, &KeyframeModel::modelReset, this, &KeyframeModel::modelChanged); connect(this, &KeyframeModel::dataChanged, this, &KeyframeModel::modelChanged); connect(this, &KeyframeModel::modelChanged, this, &KeyframeModel::sendModification); } bool KeyframeModel::addKeyframe(GenTime pos, KeyframeType type, QVariant value, bool notify, Fun &undo, Fun &redo) { qDebug() << "ADD keyframe" << pos.frames(pCore->getCurrentFps()) << value << notify; QWriteLocker locker(&m_lock); Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; if (m_keyframeList.count(pos) > 0) { qDebug() << "already there"; if (std::pair({type, value}) == m_keyframeList.at(pos)) { qDebug() << "nothing to do"; return true; // nothing to do } // In this case we simply change the type and value KeyframeType oldType = m_keyframeList[pos].first; QVariant oldValue = m_keyframeList[pos].second; local_undo = updateKeyframe_lambda(pos, oldType, oldValue, notify); local_redo = updateKeyframe_lambda(pos, type, value, notify); } else { local_redo = addKeyframe_lambda(pos, type, value, notify); local_undo = deleteKeyframe_lambda(pos, notify); } if (local_redo()) { UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } return false; } bool KeyframeModel::addKeyframe(int frame, double normalizedValue) { if (auto ptr = m_model.lock()) { Q_ASSERT(m_index.isValid()); double min = ptr->data(m_index, AssetParameterModel::MinRole).toDouble(); double max = ptr->data(m_index, AssetParameterModel::MaxRole).toDouble(); double factor = ptr->data(m_index, AssetParameterModel::FactorRole).toDouble(); double norm = ptr->data(m_index, AssetParameterModel::DefaultRole).toDouble(); int logRole = ptr->data(m_index, AssetParameterModel::ScaleRole).toInt(); double realValue; if (logRole == -1) { // Logarythmic scale for lower than norm values if (normalizedValue >= 0.5) { realValue = norm + (2 * (normalizedValue - 0.5) * (max / factor - norm)); } else { realValue = norm - pow(2 * (0.5 - normalizedValue), 10.0 / 6) * (norm - min / factor); } } else { realValue = (normalizedValue * (max - min) + min) / factor; } // TODO: Use default configurable kf type return addKeyframe(GenTime(frame, pCore->getCurrentFps()), KeyframeType::Linear, realValue); } return false; } bool KeyframeModel::addKeyframe(GenTime pos, KeyframeType type, QVariant value) { QWriteLocker locker(&m_lock); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool update = (m_keyframeList.count(pos) > 0); - bool res = addKeyframe(pos, type, value, true, undo, redo); + bool res = addKeyframe(pos, type, std::move(value), true, undo, redo); if (res) { PUSH_UNDO(undo, redo, update ? i18n("Change keyframe type") : i18n("Add keyframe")); } return res; } bool KeyframeModel::removeKeyframe(GenTime pos, Fun &undo, Fun &redo, bool notify) { qDebug() << "Going to remove keyframe at " << pos.frames(pCore->getCurrentFps()) << " NOTIFY: " << notify; qDebug() << "before" << getAnimProperty(); QWriteLocker locker(&m_lock); Q_ASSERT(m_keyframeList.count(pos) > 0); KeyframeType oldType = m_keyframeList[pos].first; QVariant oldValue = m_keyframeList[pos].second; Fun local_undo = addKeyframe_lambda(pos, oldType, oldValue, notify); Fun local_redo = deleteKeyframe_lambda(pos, notify); if (local_redo()) { qDebug() << "after" << getAnimProperty(); UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } return false; } bool KeyframeModel::removeKeyframe(int frame) { GenTime pos(frame, pCore->getCurrentFps()); return removeKeyframe(pos); } bool KeyframeModel::removeKeyframe(GenTime pos) { QWriteLocker locker(&m_lock); Fun undo = []() { return true; }; Fun redo = []() { return true; }; if (m_keyframeList.count(pos) > 0 && m_keyframeList.find(pos) == m_keyframeList.begin()) { return false; // initial point must stay } bool res = removeKeyframe(pos, undo, redo); if (res) { PUSH_UNDO(undo, redo, i18n("Delete keyframe")); } return res; } bool KeyframeModel::moveKeyframe(GenTime oldPos, GenTime pos, QVariant newVal, Fun &undo, Fun &redo) { qDebug() << "starting to move keyframe" << oldPos.frames(pCore->getCurrentFps()) << pos.frames(pCore->getCurrentFps()); QWriteLocker locker(&m_lock); Q_ASSERT(m_keyframeList.count(oldPos) > 0); if (oldPos == pos) { if (!newVal.isValid()) { // no change return true; } if (m_paramType == ParamType::AnimatedRect) { return updateKeyframe(pos, newVal); } double realValue = newVal.toDouble(); // Calculate real value from normalized if (auto ptr = m_model.lock()) { double min = ptr->data(m_index, AssetParameterModel::MinRole).toDouble(); double max = ptr->data(m_index, AssetParameterModel::MaxRole).toDouble(); double factor = ptr->data(m_index, AssetParameterModel::FactorRole).toDouble(); double norm = ptr->data(m_index, AssetParameterModel::DefaultRole).toDouble(); int logRole = ptr->data(m_index, AssetParameterModel::ScaleRole).toInt(); if (logRole == -1) { // Logarythmic scale for lower than norm values if (realValue >= 0.5) { realValue = norm + (2 * (realValue - 0.5) * (max / factor - norm)); } else { realValue = norm - pow(2 * (0.5 - realValue), 10.0 / 6) * (norm - min / factor); } } else { realValue = (realValue * (max - min) + min) / factor; } } return updateKeyframe(pos, realValue); } KeyframeType oldType = m_keyframeList[oldPos].first; QVariant oldValue = m_keyframeList[oldPos].second; if (oldPos != pos && hasKeyframe(pos)) return false; Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; qDebug() << getAnimProperty(); // TODO: use the new Animation::key_set_frame to move a keyframe bool res = removeKeyframe(oldPos, local_undo, local_redo); qDebug() << "Move keyframe finished deletion:" << res; qDebug() << getAnimProperty(); if (res) { if (m_paramType == ParamType::AnimatedRect) { if (!newVal.isValid()) { newVal = oldValue; } res = addKeyframe(pos, oldType, newVal, true, local_undo, local_redo); } else if (newVal.isValid()) { if (auto ptr = m_model.lock()) { double min = ptr->data(m_index, AssetParameterModel::MinRole).toDouble(); double max = ptr->data(m_index, AssetParameterModel::MaxRole).toDouble(); double factor = ptr->data(m_index, AssetParameterModel::FactorRole).toDouble(); double norm = ptr->data(m_index, AssetParameterModel::DefaultRole).toDouble(); int logRole = ptr->data(m_index, AssetParameterModel::ScaleRole).toInt(); double realValue = newVal.toDouble(); if (logRole == -1) { // Logarythmic scale for lower than norm values if (newVal >= 0.5) { realValue = norm + (2 * (realValue - 0.5) * (max / factor - norm)); } else { realValue = norm - pow(2 * (0.5 - realValue), 10.0 / 6) * (norm - min / factor); } } else { realValue = (realValue * (max - min) + min) / factor; } res = addKeyframe(pos, oldType, realValue, true, local_undo, local_redo); } } else { res = addKeyframe(pos, oldType, oldValue, true, local_undo, local_redo); } qDebug() << "Move keyframe finished insertion:" << res; qDebug() << getAnimProperty(); } if (res) { UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); } else { bool undone = local_undo(); Q_ASSERT(undone); } return res; } bool KeyframeModel::moveKeyframe(int oldPos, int pos, bool logUndo) { GenTime oPos(oldPos, pCore->getCurrentFps()); GenTime nPos(pos, pCore->getCurrentFps()); return moveKeyframe(oPos, nPos, QVariant(), logUndo); } bool KeyframeModel::offsetKeyframes(int oldPos, int pos, bool logUndo) { if (oldPos == pos) return true; GenTime oldFrame(oldPos, pCore->getCurrentFps()); Q_ASSERT(m_keyframeList.count(oldFrame) > 0); GenTime diff(pos - oldPos, pCore->getCurrentFps()); QWriteLocker locker(&m_lock); Fun undo = []() { return true; }; Fun redo = []() { return true; }; QList times; for (const auto &m : m_keyframeList) { if (m.first < oldFrame) continue; times << m.first; } bool res = true; for (const auto &t : times) { res &= moveKeyframe(t, t + diff, QVariant(), undo, redo); } if (res && logUndo) { PUSH_UNDO(undo, redo, i18n("Move keyframes")); } return res; } bool KeyframeModel::moveKeyframe(int oldPos, int pos, QVariant newVal) { GenTime oPos(oldPos, pCore->getCurrentFps()); GenTime nPos(pos, pCore->getCurrentFps()); - return moveKeyframe(oPos, nPos, newVal, true); + return moveKeyframe(oPos, nPos, std::move(newVal), true); } bool KeyframeModel::moveKeyframe(GenTime oldPos, GenTime pos, QVariant newVal, bool logUndo) { QWriteLocker locker(&m_lock); Q_ASSERT(m_keyframeList.count(oldPos) > 0); if (oldPos == pos) return true; Fun undo = []() { return true; }; Fun redo = []() { return true; }; - bool res = moveKeyframe(oldPos, pos, newVal, undo, redo); + bool res = moveKeyframe(oldPos, pos, std::move(newVal), undo, redo); if (res && logUndo) { PUSH_UNDO(undo, redo, i18n("Move keyframe")); } return res; } bool KeyframeModel::directUpdateKeyframe(GenTime pos, QVariant value) { QWriteLocker locker(&m_lock); Q_ASSERT(m_keyframeList.count(pos) > 0); KeyframeType type = m_keyframeList[pos].first; - auto operation = updateKeyframe_lambda(pos, type, value, true); + auto operation = updateKeyframe_lambda(pos, type, std::move(value), true); return operation(); } -bool KeyframeModel::updateKeyframe(GenTime pos, QVariant value, Fun &undo, Fun &redo, bool update) +bool KeyframeModel::updateKeyframe(GenTime pos, const QVariant &value, Fun &undo, Fun &redo, bool update) { QWriteLocker locker(&m_lock); Q_ASSERT(m_keyframeList.count(pos) > 0); KeyframeType type = m_keyframeList[pos].first; QVariant oldValue = m_keyframeList[pos].second; // Check if keyframe is different if (m_paramType == ParamType::KeyframeParam) { if (qFuzzyCompare(oldValue.toDouble(), value.toDouble())) return true; } auto operation = updateKeyframe_lambda(pos, type, value, update); auto reverse = updateKeyframe_lambda(pos, type, oldValue, update); bool res = operation(); if (res) { UPDATE_UNDO_REDO(operation, reverse, undo, redo); } return res; } bool KeyframeModel::updateKeyframe(int pos, double newVal) { GenTime Pos(pos, pCore->getCurrentFps()); if (auto ptr = m_model.lock()) { double min = ptr->data(m_index, AssetParameterModel::MinRole).toDouble(); double max = ptr->data(m_index, AssetParameterModel::MaxRole).toDouble(); double factor = ptr->data(m_index, AssetParameterModel::FactorRole).toDouble(); double norm = ptr->data(m_index, AssetParameterModel::DefaultRole).toDouble(); int logRole = ptr->data(m_index, AssetParameterModel::ScaleRole).toInt(); double realValue; if (logRole == -1) { // Logarythmic scale for lower than norm values if (newVal >= 0.5) { realValue = norm + (2 * (newVal - 0.5) * (max / factor - norm)); } else { realValue = norm - pow(2 * (0.5 - newVal), 10.0 / 6) * (norm - min / factor); } } else { realValue = (newVal * (max - min) + min) / factor; } return updateKeyframe(Pos, realValue); } return false; } bool KeyframeModel::updateKeyframe(GenTime pos, QVariant value) { QWriteLocker locker(&m_lock); Q_ASSERT(m_keyframeList.count(pos) > 0); Fun undo = []() { return true; }; Fun redo = []() { return true; }; - bool res = updateKeyframe(pos, value, undo, redo); + bool res = updateKeyframe(pos, std::move(value), undo, redo); if (res) { PUSH_UNDO(undo, redo, i18n("Update keyframe")); } return res; } KeyframeType convertFromMltType(mlt_keyframe_type type) { switch (type) { case mlt_keyframe_linear: return KeyframeType::Linear; case mlt_keyframe_discrete: return KeyframeType::Discrete; case mlt_keyframe_smooth: return KeyframeType::Curve; } return KeyframeType::Linear; } bool KeyframeModel::updateKeyframeType(GenTime pos, int type, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); Q_ASSERT(m_keyframeList.count(pos) > 0); KeyframeType oldType = m_keyframeList[pos].first; KeyframeType newType = convertFromMltType((mlt_keyframe_type)type); QVariant value = m_keyframeList[pos].second; // Check if keyframe is different if (m_paramType == ParamType::KeyframeParam) { if (oldType == newType) return true; } auto operation = updateKeyframe_lambda(pos, newType, value, true); auto reverse = updateKeyframe_lambda(pos, oldType, value, true); bool res = operation(); if (res) { UPDATE_UNDO_REDO(operation, reverse, undo, redo); } return res; } -Fun KeyframeModel::updateKeyframe_lambda(GenTime pos, KeyframeType type, QVariant value, bool notify) +Fun KeyframeModel::updateKeyframe_lambda(GenTime pos, KeyframeType type, const QVariant &value, bool notify) { QWriteLocker locker(&m_lock); return [this, pos, type, value, notify]() { qDebug() << "update lambda" << pos.frames(pCore->getCurrentFps()) << value << notify; Q_ASSERT(m_keyframeList.count(pos) > 0); int row = static_cast(std::distance(m_keyframeList.begin(), m_keyframeList.find(pos))); m_keyframeList[pos].first = type; m_keyframeList[pos].second = value; if (notify) emit dataChanged(index(row), index(row), {ValueRole, NormalizedValueRole, TypeRole}); return true; }; } -Fun KeyframeModel::addKeyframe_lambda(GenTime pos, KeyframeType type, QVariant value, bool notify) +Fun KeyframeModel::addKeyframe_lambda(GenTime pos, KeyframeType type, const QVariant &value, bool notify) { QWriteLocker locker(&m_lock); return [this, notify, pos, type, value]() { qDebug() << "add lambda" << pos.frames(pCore->getCurrentFps()) << value << notify; Q_ASSERT(m_keyframeList.count(pos) == 0); // We determine the row of the newly added marker auto insertionIt = m_keyframeList.lower_bound(pos); int insertionRow = static_cast(m_keyframeList.size()); if (insertionIt != m_keyframeList.end()) { insertionRow = static_cast(std::distance(m_keyframeList.begin(), insertionIt)); } if (notify) beginInsertRows(QModelIndex(), insertionRow, insertionRow); m_keyframeList[pos].first = type; m_keyframeList[pos].second = value; if (notify) endInsertRows(); return true; }; } Fun KeyframeModel::deleteKeyframe_lambda(GenTime pos, bool notify) { QWriteLocker locker(&m_lock); return [this, pos, notify]() { qDebug() << "delete lambda" << pos.frames(pCore->getCurrentFps()) << notify; qDebug() << "before" << getAnimProperty(); Q_ASSERT(m_keyframeList.count(pos) > 0); Q_ASSERT(pos != GenTime()); // cannot delete initial point int row = static_cast(std::distance(m_keyframeList.begin(), m_keyframeList.find(pos))); if (notify) beginRemoveRows(QModelIndex(), row, row); m_keyframeList.erase(pos); if (notify) endRemoveRows(); qDebug() << "after" << getAnimProperty(); return true; }; } QHash KeyframeModel::roleNames() const { QHash roles; roles[PosRole] = "position"; roles[FrameRole] = "frame"; roles[TypeRole] = "type"; roles[ValueRole] = "value"; roles[NormalizedValueRole] = "normalizedValue"; return roles; } QVariant KeyframeModel::data(const QModelIndex &index, int role) const { READ_LOCK(); if (index.row() < 0 || index.row() >= static_cast(m_keyframeList.size()) || !index.isValid()) { return QVariant(); } auto it = m_keyframeList.begin(); std::advance(it, index.row()); switch (role) { case Qt::DisplayRole: case Qt::EditRole: case ValueRole: return it->second.second; case NormalizedValueRole: { if (m_paramType == ParamType::AnimatedRect) { const QString &data = it->second.second.toString(); QLocale locale; return locale.toDouble(data.section(QLatin1Char(' '), -1)); } double val = it->second.second.toDouble(); if (auto ptr = m_model.lock()) { Q_ASSERT(m_index.isValid()); double min = ptr->data(m_index, AssetParameterModel::MinRole).toDouble(); double max = ptr->data(m_index, AssetParameterModel::MaxRole).toDouble(); double factor = ptr->data(m_index, AssetParameterModel::FactorRole).toDouble(); double norm = ptr->data(m_index, AssetParameterModel::DefaultRole).toDouble(); int logRole = ptr->data(m_index, AssetParameterModel::ScaleRole).toInt(); double linear = val * factor; if (logRole == -1) { // Logarythmic scale for lower than norm values if (linear >= norm) { return 0.5 + (linear - norm) / (max * factor - norm) * 0.5; } // transform current value to 0..1 scale double scaled = (linear - norm) / (min * factor - norm); // Log scale return 0.5 - pow(scaled, 0.6) * 0.5; } return (linear - min) / (max - min); } else { qDebug() << "// CANNOT LOCK effect MODEL"; } return 1; } case PosRole: return it->first.seconds(); case FrameRole: case Qt::UserRole: return it->first.frames(pCore->getCurrentFps()); case TypeRole: return QVariant::fromValue(it->second.first); } return QVariant(); } int KeyframeModel::rowCount(const QModelIndex &parent) const { READ_LOCK(); if (parent.isValid()) return 0; return static_cast(m_keyframeList.size()); } bool KeyframeModel::singleKeyframe() const { READ_LOCK(); return m_keyframeList.size() <= 1; } Keyframe KeyframeModel::getKeyframe(const GenTime &pos, bool *ok) const { READ_LOCK(); if (m_keyframeList.count(pos) <= 0) { // return empty marker *ok = false; return {GenTime(), KeyframeType::Linear}; } *ok = true; return {pos, m_keyframeList.at(pos).first}; } Keyframe KeyframeModel::getNextKeyframe(const GenTime &pos, bool *ok) const { auto it = m_keyframeList.upper_bound(pos); if (it == m_keyframeList.end()) { // return empty marker *ok = false; return {GenTime(), KeyframeType::Linear}; } *ok = true; return {(*it).first, (*it).second.first}; } Keyframe KeyframeModel::getPrevKeyframe(const GenTime &pos, bool *ok) const { auto it = m_keyframeList.lower_bound(pos); if (it == m_keyframeList.begin()) { // return empty marker *ok = false; return {GenTime(), KeyframeType::Linear}; } --it; *ok = true; return {(*it).first, (*it).second.first}; } Keyframe KeyframeModel::getClosestKeyframe(const GenTime &pos, bool *ok) const { if (m_keyframeList.count(pos) > 0) { return getKeyframe(pos, ok); } bool ok1, ok2; auto next = getNextKeyframe(pos, &ok1); auto prev = getPrevKeyframe(pos, &ok2); *ok = ok1 || ok2; if (ok1 && ok2) { double fps = pCore->getCurrentFps(); if (qAbs(next.first.frames(fps) - pos.frames(fps)) < qAbs(prev.first.frames(fps) - pos.frames(fps))) { return next; } return prev; } else if (ok1) { return next; } else if (ok2) { return prev; } // return empty marker return {GenTime(), KeyframeType::Linear}; } bool KeyframeModel::hasKeyframe(int frame) const { return hasKeyframe(GenTime(frame, pCore->getCurrentFps())); } bool KeyframeModel::hasKeyframe(const GenTime &pos) const { READ_LOCK(); return m_keyframeList.count(pos) > 0; } bool KeyframeModel::removeAllKeyframes(Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); std::vector all_pos; Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; int kfrCount = (int)m_keyframeList.size() - 1; // we trigger only one global remove/insertrow event Fun update_redo_start = [this, kfrCount]() { beginRemoveRows(QModelIndex(), 1, kfrCount); return true; }; Fun update_redo_end = [this]() { endRemoveRows(); return true; }; Fun update_undo_start = [this, kfrCount]() { beginInsertRows(QModelIndex(), 1, kfrCount); return true; }; Fun update_undo_end = [this]() { endInsertRows(); return true; }; PUSH_LAMBDA(update_redo_start, local_redo); PUSH_LAMBDA(update_undo_start, local_undo); for (const auto &m : m_keyframeList) { all_pos.push_back(m.first); } update_redo_start(); bool res = true; bool first = true; for (const auto &p : all_pos) { if (first) { // skip first point first = false; continue; } res = removeKeyframe(p, local_undo, local_redo, false); if (!res) { bool undone = local_undo(); Q_ASSERT(undone); return false; } } update_redo_end(); PUSH_LAMBDA(update_redo_end, local_redo); PUSH_LAMBDA(update_undo_end, local_undo); UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } bool KeyframeModel::removeAllKeyframes() { QWriteLocker locker(&m_lock); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool res = removeAllKeyframes(undo, redo); if (res) { PUSH_UNDO(undo, redo, i18n("Delete all keyframes")); } return res; } mlt_keyframe_type convertToMltType(KeyframeType type) { switch (type) { case KeyframeType::Linear: return mlt_keyframe_linear; case KeyframeType::Discrete: return mlt_keyframe_discrete; case KeyframeType::Curve: return mlt_keyframe_smooth; } return mlt_keyframe_linear; } QString KeyframeModel::getAnimProperty() const { if (m_paramType == ParamType::Roto_spline) { return getRotoProperty(); } Mlt::Properties mlt_prop; if (auto ptr = m_model.lock()) { ptr->passProperties(mlt_prop); } int ix = 0; bool first = true; std::shared_ptr anim; - for (const auto keyframe : m_keyframeList) { + for (const auto &keyframe : m_keyframeList) { if (first) { switch (m_paramType) { case ParamType::AnimatedRect: mlt_prop.anim_set("key", keyframe.second.second.toString().toUtf8().constData(), keyframe.first.frames(pCore->getCurrentFps())); break; default: mlt_prop.anim_set("key", keyframe.second.second.toDouble(), keyframe.first.frames(pCore->getCurrentFps())); break; } anim.reset(mlt_prop.get_anim("key")); anim->key_set_type(ix, convertToMltType(keyframe.second.first)); first = false; ix++; continue; } switch (m_paramType) { case ParamType::AnimatedRect: mlt_prop.anim_set("key", keyframe.second.second.toString().toUtf8().constData(), keyframe.first.frames(pCore->getCurrentFps())); break; default: mlt_prop.anim_set("key", keyframe.second.second.toDouble(), keyframe.first.frames(pCore->getCurrentFps())); break; } anim->key_set_type(ix, convertToMltType(keyframe.second.first)); ix++; } char *cut = anim->serialize_cut(); QString ret(cut); free(cut); return ret; } QString KeyframeModel::getRotoProperty() const { QJsonDocument doc; if (auto ptr = m_model.lock()) { int in = ptr->data(m_index, AssetParameterModel::ParentInRole).toInt(); int out = ptr->data(m_index, AssetParameterModel::ParentDurationRole).toInt(); QMap map; - for (const auto keyframe : m_keyframeList) { + for (const auto &keyframe : m_keyframeList) { map.insert(QString::number(in + keyframe.first.frames(pCore->getCurrentFps())).rightJustified(log10((double)out) + 1, '0'), keyframe.second.second); } doc = QJsonDocument::fromVariant(QVariant(map)); } return doc.toJson(); } void KeyframeModel::parseAnimProperty(const QString &prop) { Fun undo = []() { return true; }; Fun redo = []() { return true; }; QLocale locale; disconnect(this, &KeyframeModel::modelChanged, this, &KeyframeModel::sendModification); removeAllKeyframes(undo, redo); int in = 0; int out = 0; Mlt::Properties mlt_prop; if (auto ptr = m_model.lock()) { in = ptr->data(m_index, AssetParameterModel::ParentInRole).toInt(); out = ptr->data(m_index, AssetParameterModel::ParentDurationRole).toInt(); ptr->passProperties(mlt_prop); } mlt_prop.set("key", prop.toUtf8().constData()); // This is a fake query to force the animation to be parsed (void)mlt_prop.anim_get_double("key", 0, out); Mlt::Animation anim = mlt_prop.get_animation("key"); qDebug() << "Found" << anim.key_count() << ", OUT: "< in) { // Always add a keyframe at start pos addKeyframe(GenTime(in, pCore->getCurrentFps()), convertFromMltType(type), value, true, undo, redo); } else if (frame == in && hasKeyframe(GenTime(in))) { // First keyframe already exists, adjust its value updateKeyframe(GenTime(frame, pCore->getCurrentFps()), value, undo, redo, true); continue; } addKeyframe(GenTime(frame, pCore->getCurrentFps()), convertFromMltType(type), value, true, undo, redo); } connect(this, &KeyframeModel::modelChanged, this, &KeyframeModel::sendModification); } void KeyframeModel::resetAnimProperty(const QString &prop) { Fun undo = []() { return true; }; Fun redo = []() { return true; }; // Delete all existing keyframes disconnect(this, &KeyframeModel::modelChanged, this, &KeyframeModel::sendModification); removeAllKeyframes(undo, redo); Mlt::Properties mlt_prop; QLocale locale; int in = 0; if (auto ptr = m_model.lock()) { in = ptr->data(m_index, AssetParameterModel::ParentInRole).toInt(); ptr->passProperties(mlt_prop); } mlt_prop.set("key", prop.toUtf8().constData()); // This is a fake query to force the animation to be parsed (void)mlt_prop.anim_get_int("key", 0, 0); Mlt::Animation anim = mlt_prop.get_animation("key"); qDebug() << "Found" << anim.key_count() << "animation properties"; for (int i = 0; i < anim.key_count(); ++i) { int frame; mlt_keyframe_type type; anim.key_get(i, frame, type); if (!prop.contains(QLatin1Char('='))) { // TODO: use a default user defined type type = mlt_keyframe_linear; } QVariant value; switch (m_paramType) { case ParamType::AnimatedRect: { mlt_rect rect = mlt_prop.anim_get_rect("key", frame); value = QVariant(QStringLiteral("%1 %2 %3 %4 %5").arg(rect.x).arg(rect.y).arg(rect.w).arg(rect.h).arg(locale.toString(rect.o))); break; } default: value = QVariant(mlt_prop.anim_get_double("key", frame)); break; } if (i == 0 && frame > in) { // Always add a keyframe at start pos addKeyframe(GenTime(in, pCore->getCurrentFps()), convertFromMltType(type), value, false, undo, redo); } else if (frame == in && hasKeyframe(GenTime(in))) { // First keyframe already exists, adjust its value updateKeyframe(GenTime(frame, pCore->getCurrentFps()), value, undo, redo, false); continue; } addKeyframe(GenTime(frame, pCore->getCurrentFps()), convertFromMltType(type), value, false, undo, redo); } QString effectName; if (auto ptr = m_model.lock()) { effectName = ptr->data(m_index, Qt::DisplayRole).toString(); } else { effectName = i18n("effect"); } Fun update_local = [this]() { emit dataChanged(index(0), index((int)m_keyframeList.size()), {}); return true; }; update_local(); PUSH_LAMBDA(update_local, undo); PUSH_LAMBDA(update_local, redo); PUSH_UNDO(undo, redo, i18n("Reset %1", effectName)); connect(this, &KeyframeModel::modelChanged, this, &KeyframeModel::sendModification); } void KeyframeModel::parseRotoProperty(const QString &prop) { Fun undo = []() { return true; }; Fun redo = []() { return true; }; QJsonParseError jsonError; QJsonDocument doc = QJsonDocument::fromJson(prop.toLatin1(), &jsonError); QVariant data = doc.toVariant(); if (data.canConvert(QVariant::Map)) { QList keyframes; QMap map = data.toMap(); QMap::const_iterator i = map.constBegin(); while (i != map.constEnd()) { addKeyframe(GenTime(i.key().toInt(), pCore->getCurrentFps()), KeyframeType::Linear, i.value(), false, undo, redo); ++i; } } } QVariant KeyframeModel::getInterpolatedValue(int p) const { auto pos = GenTime(p, pCore->getCurrentFps()); return getInterpolatedValue(pos); } -QVariant KeyframeModel::updateInterpolated(QVariant interpValue, double val) +QVariant KeyframeModel::updateInterpolated(const QVariant &interpValue, double val) { QStringList vals = interpValue.toString().split(QLatin1Char(' ')); QLocale locale; if (!vals.isEmpty()) { vals[vals.size() - 1] = locale.toString(val); } return vals.join(QLatin1Char(' ')); } QVariant KeyframeModel::getNormalizedValue(double newVal) const { if (auto ptr = m_model.lock()) { double min = ptr->data(m_index, AssetParameterModel::MinRole).toDouble(); double max = ptr->data(m_index, AssetParameterModel::MaxRole).toDouble(); double factor = ptr->data(m_index, AssetParameterModel::FactorRole).toDouble(); double norm = ptr->data(m_index, AssetParameterModel::DefaultRole).toDouble(); int logRole = ptr->data(m_index, AssetParameterModel::ScaleRole).toInt(); double realValue; if (logRole == -1) { // Logarythmic scale for lower than norm values if (newVal >= 0.5) { realValue = norm + (2 * (newVal - 0.5) * (max / factor - norm)); } else { realValue = norm - pow(2 * (0.5 - newVal), 10.0 / 6) * (norm - min / factor); } } else { realValue = (newVal * (max - min) + min) / factor; } return QVariant(realValue); } return QVariant(); } QVariant KeyframeModel::getInterpolatedValue(const GenTime &pos) const { if (m_keyframeList.count(pos) > 0) { return m_keyframeList.at(pos).second; } if (m_keyframeList.size() == 0) { return QVariant(); } auto next = m_keyframeList.upper_bound(pos); if (next == m_keyframeList.cbegin()) { return (m_keyframeList.cbegin())->second.second; } else if (next == m_keyframeList.cend()) { auto it = m_keyframeList.cend(); --it; return it->second.second; } auto prev = next; --prev; // We now have surrounding keyframes, we use mlt to compute the value Mlt::Properties prop; if (auto ptr = m_model.lock()) { ptr->passProperties(prop); } QLocale locale; int p = pos.frames(pCore->getCurrentFps()); if (m_paramType == ParamType::KeyframeParam) { prop.anim_set("keyframe", prev->second.second.toDouble(), prev->first.frames(pCore->getCurrentFps()), next->first.frames(pCore->getCurrentFps()), convertToMltType(prev->second.first)); prop.anim_set("keyframe", next->second.second.toDouble(), next->first.frames(pCore->getCurrentFps()), next->first.frames(pCore->getCurrentFps()), convertToMltType(next->second.first)); return QVariant(prop.anim_get_double("keyframe", p)); } else if (m_paramType == ParamType::AnimatedRect) { QStringList vals = prev->second.second.toString().split(QLatin1Char(' ')); if (vals.count() >= 4) { mlt_rect rect; rect.x = vals.at(0).toInt(); rect.y = vals.at(1).toInt(); rect.w = vals.at(2).toInt(); rect.h = vals.at(3).toInt(); if (vals.count() > 4) { rect.o = locale.toDouble(vals.at(4)); } else { rect.o = 1; } prop.anim_set("keyframe", rect, prev->first.frames(pCore->getCurrentFps()), next->first.frames(pCore->getCurrentFps()), convertToMltType(prev->second.first)); } vals = next->second.second.toString().split(QLatin1Char(' ')); if (vals.count() >= 4) { mlt_rect rect; rect.x = vals.at(0).toInt(); rect.y = vals.at(1).toInt(); rect.w = vals.at(2).toInt(); rect.h = vals.at(3).toInt(); if (vals.count() > 4) { rect.o = locale.toDouble(vals.at(4)); } else { rect.o = 1; } prop.anim_set("keyframe", rect, next->first.frames(pCore->getCurrentFps()), next->first.frames(pCore->getCurrentFps()), convertToMltType(next->second.first)); } mlt_rect rect = prop.anim_get_rect("keyframe", p); const QString res = QStringLiteral("%1 %2 %3 %4 %5").arg((int)rect.x).arg((int)rect.y).arg((int)rect.w).arg((int)rect.h).arg(locale.toString(rect.o)); return QVariant(res); } else if (m_paramType == ParamType::Roto_spline) { // interpolate QSize frame = pCore->getCurrentFrameSize(); QList p1 = RotoHelper::getPoints(prev->second.second, frame); qreal relPos = (p - prev->first.frames(pCore->getCurrentFps())) / (qreal)(((next->first - prev->first).frames(pCore->getCurrentFps())) + 1); QList p2 = RotoHelper::getPoints(next->second.second, frame); int count = qMin(p1.count(), p2.count()); QList vlist; for (int i = 0; i < count; ++i) { BPoint bp; QList pl; for (int j = 0; j < 3; ++j) { if (p1.at(i)[j] != p2.at(i)[j]) { bp[j] = QLineF(p1.at(i)[j], p2.at(i)[j]).pointAt(relPos); } else { bp[j] = p1.at(i)[j]; } pl << QVariant(QList() << QVariant(bp[j].x() / frame.width()) << QVariant(bp[j].y() / frame.height())); } vlist << QVariant(pl); } return vlist; } return QVariant(); } void KeyframeModel::sendModification() { if (auto ptr = m_model.lock()) { Q_ASSERT(m_index.isValid()); QString name = ptr->data(m_index, AssetParameterModel::NameRole).toString(); if (m_paramType == ParamType::KeyframeParam || m_paramType == ParamType::AnimatedRect || m_paramType == ParamType::Roto_spline) { m_lastData = getAnimProperty(); ptr->setParameter(name, m_lastData, false); } else { Q_ASSERT(false); // Not implemented, TODO } } } void KeyframeModel::refresh() { Q_ASSERT(m_index.isValid()); QString animData; if (auto ptr = m_model.lock()) { animData = ptr->data(m_index, AssetParameterModel::ValueRole).toString(); } else { qDebug() << "WARNING : unable to access keyframe's model"; return; } if (animData == m_lastData) { // nothing to do qDebug() << "// DATA WAS ALREADY PARSED, ABORTING REFRESH\n_________________"; return; } if (m_paramType == ParamType::KeyframeParam || m_paramType == ParamType::AnimatedRect) { parseAnimProperty(animData); } else if (m_paramType == ParamType::Roto_spline) { parseRotoProperty(animData); } else { // first, try to convert to double bool ok = false; double value = animData.toDouble(&ok); if (ok) { Fun undo = []() { return true; }; Fun redo = []() { return true; }; addKeyframe(GenTime(), KeyframeType::Linear, QVariant(value), false, undo, redo); } else { Q_ASSERT(false); // Not implemented, TODO } } m_lastData = animData; } void KeyframeModel::reset() { Q_ASSERT(m_index.isValid()); QString animData; if (auto ptr = m_model.lock()) { animData = ptr->data(m_index, AssetParameterModel::ValueRole).toString(); } else { qDebug() << "WARNING : unable to access keyframe's model"; return; } if (animData == m_lastData) { // nothing to do qDebug() << "// DATA WAS ALREADY PARSED, ABORTING\n_________________"; return; } if (m_paramType == ParamType::KeyframeParam || m_paramType == ParamType::AnimatedRect) { qDebug() << "parsing keyframe" << animData; resetAnimProperty(animData); } else if (m_paramType == ParamType::Roto_spline) { // TODO: resetRotoProperty(animData); } else { // first, try to convert to double bool ok = false; double value = animData.toDouble(&ok); if (ok) { Fun undo = []() { return true; }; Fun redo = []() { return true; }; addKeyframe(GenTime(), KeyframeType::Linear, QVariant(value), false, undo, redo); PUSH_UNDO(undo, redo, i18n("Reset effect")); qDebug() << "KEYFRAME ADDED" << value; } else { Q_ASSERT(false); // Not implemented, TODO } } m_lastData = animData; } -QList KeyframeModel::getRanges(const QString &animData, std::shared_ptr model) +QList KeyframeModel::getRanges(const QString &animData, const std::shared_ptr &model) { Mlt::Properties mlt_prop; model->passProperties(mlt_prop); QLocale locale; mlt_prop.set("key", animData.toUtf8().constData()); // This is a fake query to force the animation to be parsed (void)mlt_prop.anim_get_int("key", 0, 0); Mlt::Animation anim = mlt_prop.get_animation("key"); int frame; mlt_keyframe_type type; anim.key_get(0, frame, type); mlt_rect rect = mlt_prop.anim_get_rect("key", frame); QPoint pX(rect.x, rect.x); QPoint pY(rect.y, rect.y); QPoint pW(rect.w, rect.w); QPoint pH(rect.h, rect.h); QPoint pO(rect.o, rect.o); for (int i = 1; i < anim.key_count(); ++i) { anim.key_get(i, frame, type); if (!animData.contains(QLatin1Char('='))) { // TODO: use a default user defined type type = mlt_keyframe_linear; } rect = mlt_prop.anim_get_rect("key", frame); pX.setX(qMin((int)rect.x, pX.x())); pX.setY(qMax((int)rect.x, pX.y())); pY.setX(qMin((int)rect.y, pY.x())); pY.setY(qMax((int)rect.y, pY.y())); pW.setX(qMin((int)rect.w, pW.x())); pW.setY(qMax((int)rect.w, pW.y())); pH.setX(qMin((int)rect.h, pH.x())); pH.setY(qMax((int)rect.h, pH.y())); pO.setX(qMin((int)rect.o, pO.x())); pO.setY(qMax((int)rect.o, pO.y())); // value = QVariant(QStringLiteral("%1 %2 %3 %4 %5").arg(rect.x).arg(rect.y).arg(rect.w).arg(rect.h).arg(locale.toString(rect.o))); } QList result{pX, pY, pW, pH, pO}; return result; } std::shared_ptr KeyframeModel::getAnimation(const QString &animData) { std::shared_ptr mlt_prop(new Mlt::Properties()); mlt_prop->set("key", animData.toUtf8().constData()); // This is a fake query to force the animation to be parsed (void)mlt_prop->anim_get_rect("key", 0, 0); return mlt_prop; } QList KeyframeModel::getKeyframePos() const { QList all_pos; for (const auto &m : m_keyframeList) { all_pos.push_back(m.first); } return all_pos; } bool KeyframeModel::removeNextKeyframes(GenTime pos, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); std::vector all_pos; Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; int firstPos = 0; for (const auto &m : m_keyframeList) { if (m.first <= pos) { firstPos++; continue; } all_pos.push_back(m.first); } int kfrCount = (int)all_pos.size(); // we trigger only one global remove/insertrow event Fun update_redo_start = [this, firstPos, kfrCount]() { beginRemoveRows(QModelIndex(), firstPos, kfrCount); return true; }; Fun update_redo_end = [this]() { endRemoveRows(); return true; }; Fun update_undo_start = [this, firstPos, kfrCount]() { beginInsertRows(QModelIndex(), firstPos, kfrCount); return true; }; Fun update_undo_end = [this]() { endInsertRows(); return true; }; PUSH_LAMBDA(update_redo_start, local_redo); PUSH_LAMBDA(update_undo_start, local_undo); update_redo_start(); bool res = true; for (const auto &p : all_pos) { res = removeKeyframe(p, local_undo, local_redo, false); if (!res) { bool undone = local_undo(); Q_ASSERT(undone); return false; } } update_redo_end(); PUSH_LAMBDA(update_redo_end, local_redo); PUSH_LAMBDA(update_undo_end, local_undo); UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } diff --git a/src/assets/keyframes/model/keyframemodel.hpp b/src/assets/keyframes/model/keyframemodel.hpp index 05cca0fca..b6c6f3052 100644 --- a/src/assets/keyframes/model/keyframemodel.hpp +++ b/src/assets/keyframes/model/keyframemodel.hpp @@ -1,220 +1,220 @@ /*************************************************************************** * Copyright (C) 2017 by Nicolas Carion * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #ifndef KEYFRAMELISTMODEL_H #define KEYFRAMELISTMODEL_H #include "assets/model/assetparametermodel.hpp" #include "definitions.h" #include "gentime.h" #include "undohelper.hpp" #include #include #include #include class AssetParameterModel; class DocUndoStack; class EffectItemModel; /* @brief This class is the model for a list of keyframes. A keyframe is defined by a time, a type and a value We store them in a sorted fashion using a std::map */ enum class KeyframeType { Linear = mlt_keyframe_linear, Discrete = mlt_keyframe_discrete, Curve = mlt_keyframe_smooth }; Q_DECLARE_METATYPE(KeyframeType) using Keyframe = std::pair; class KeyframeModel : public QAbstractListModel { Q_OBJECT public: /* @brief Construct a keyframe list bound to the given effect @param init_value is the value taken by the param at time 0. @param model is the asset this parameter belong to @param index is the index of this parameter in its model */ explicit KeyframeModel(std::weak_ptr model, const QModelIndex &index, std::weak_ptr undo_stack, QObject *parent = nullptr); enum { TypeRole = Qt::UserRole + 1, PosRole, FrameRole, ValueRole, NormalizedValueRole }; friend class KeyframeModelList; friend class KeyframeWidget; protected: /** @brief These methods should ONLY be called by keyframemodellist to ensure synchronisation * with keyframes from other parameters */ /* @brief Adds a keyframe at the given position. If there is already one then we update it. @param pos defines the position of the keyframe, relative to the clip @param type is the type of the keyframe. */ bool addKeyframe(GenTime pos, KeyframeType type, QVariant value); bool addKeyframe(int frame, double normalizedValue); /* @brief Same function but accumulates undo/redo @param notify: if true, send a signal to model */ bool addKeyframe(GenTime pos, KeyframeType type, QVariant value, bool notify, Fun &undo, Fun &redo); /* @brief Removes the keyframe at the given position. */ bool removeKeyframe(int frame); bool moveKeyframe(int oldPos, int pos, QVariant newVal); bool removeKeyframe(GenTime pos); /* @brief Delete all the keyframes of the model */ bool removeAllKeyframes(); bool removeAllKeyframes(Fun &undo, Fun &redo); bool removeNextKeyframes(GenTime pos, Fun &undo, Fun &redo); QList getKeyframePos() const; protected: /* @brief Same function but accumulates undo/redo */ bool removeKeyframe(GenTime pos, Fun &undo, Fun &redo, bool notify = true); public: /* @brief moves a keyframe @param oldPos is the old position of the keyframe @param pos defines the new position of the keyframe, relative to the clip @param logUndo if true, then an undo object is created */ Q_INVOKABLE bool moveKeyframe(int oldPos, int pos, bool logUndo); Q_INVOKABLE bool offsetKeyframes(int oldPos, int pos, bool logUndo); bool moveKeyframe(GenTime oldPos, GenTime pos, QVariant newVal, bool logUndo); bool moveKeyframe(GenTime oldPos, GenTime pos, QVariant newVal, Fun &undo, Fun &redo); /* @brief updates the value of a keyframe @param old is the position of the keyframe @param value is the new value of the param */ Q_INVOKABLE bool updateKeyframe(int pos, double newVal); bool updateKeyframe(GenTime pos, QVariant value); bool updateKeyframeType(GenTime pos, int type, Fun &undo, Fun &redo); - bool updateKeyframe(GenTime pos, QVariant value, Fun &undo, Fun &redo, bool update = true); + bool updateKeyframe(GenTime pos, const QVariant &value, Fun &undo, Fun &redo, bool update = true); /* @brief updates the value of a keyframe, without any management of undo/redo @param pos is the position of the keyframe @param value is the new value of the param */ bool directUpdateKeyframe(GenTime pos, QVariant value); /* @brief Returns a keyframe data at given pos ok is a return parameter, set to true if everything went good */ Keyframe getKeyframe(const GenTime &pos, bool *ok) const; /* @brief Returns true if we only have 1 keyframe */ bool singleKeyframe() const; /* @brief Returns the keyframe located after given position. If there is a keyframe at given position it is ignored. @param ok is a return parameter to tell if a keyframe was found. */ Keyframe getNextKeyframe(const GenTime &pos, bool *ok) const; /* @brief Returns the keyframe located before given position. If there is a keyframe at given position it is ignored. @param ok is a return parameter to tell if a keyframe was found. */ Keyframe getPrevKeyframe(const GenTime &pos, bool *ok) const; /* @brief Returns the closest keyframe from given position. @param ok is a return parameter to tell if a keyframe was found. */ Keyframe getClosestKeyframe(const GenTime &pos, bool *ok) const; /* @brief Returns true if a keyframe exists at given pos Notice that add/remove queries are done in real time (gentime), but this request is made in frame */ Q_INVOKABLE bool hasKeyframe(int frame) const; Q_INVOKABLE bool hasKeyframe(const GenTime &pos) const; /* @brief Read the value from the model and update itself accordingly */ void refresh(); /* @brief Reset all values to their default */ void reset(); /* @brief Return the interpolated value at given pos */ QVariant getInterpolatedValue(int pos) const; QVariant getInterpolatedValue(const GenTime &pos) const; - QVariant updateInterpolated(QVariant interpValue, double val); + QVariant updateInterpolated(const QVariant &interpValue, double val); /* @brief Return the real value from a normalized one */ QVariant getNormalizedValue(double newVal) const; // Mandatory overloads Q_INVOKABLE QVariant data(const QModelIndex &index, int role) const override; QHash roleNames() const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override; - static QList getRanges(const QString &animData, std::shared_ptr model); + static QList getRanges(const QString &animData, const std::shared_ptr &model); static std::shared_ptr getAnimation(const QString &animData); protected: /** @brief Helper function that generate a lambda to change type / value of given keyframe */ - Fun updateKeyframe_lambda(GenTime pos, KeyframeType type, QVariant value, bool notify); + Fun updateKeyframe_lambda(GenTime pos, KeyframeType type, const QVariant &value, bool notify); /** @brief Helper function that generate a lambda to add given keyframe */ - Fun addKeyframe_lambda(GenTime pos, KeyframeType type, QVariant value, bool notify); + Fun addKeyframe_lambda(GenTime pos, KeyframeType type, const QVariant &value, bool notify); /** @brief Helper function that generate a lambda to remove given keyframe */ Fun deleteKeyframe_lambda(GenTime pos, bool notify); /* @brief Connects the signals of this object */ void setup(); /* @brief Commit the modification to the model */ void sendModification(); /** @brief returns the keyframes as a Mlt Anim Property string. It is defined as pairs of frame and value, separated by ; Example : "0|=50; 50|=100; 100=200; 200~=60;" Spaces are ignored by Mlt. |= represents a discrete keyframe, = a linear one and ~= a Catmull-Rom spline */ QString getAnimProperty() const; QString getRotoProperty() const; /* @brief this function clears all existing keyframes, and reloads its data from the string passed */ void resetAnimProperty(const QString &prop); /* @brief this function does the opposite of getAnimProperty: given a MLT representation of an animation, build the corresponding model */ void parseAnimProperty(const QString &prop); void parseRotoProperty(const QString &prop); private: std::weak_ptr m_model; std::weak_ptr m_undoStack; QPersistentModelIndex m_index; QString m_lastData; ParamType m_paramType; mutable QReadWriteLock m_lock; // This is a lock that ensures safety in case of concurrent access std::map> m_keyframeList; signals: void modelChanged(); public: // this is to enable for range loops auto begin() -> decltype(m_keyframeList.begin()) { return m_keyframeList.begin(); } auto end() -> decltype(m_keyframeList.end()) { return m_keyframeList.end(); } }; // Q_DECLARE_METATYPE(KeyframeModel *) #endif diff --git a/src/assets/keyframes/model/keyframemodellist.cpp b/src/assets/keyframes/model/keyframemodellist.cpp index 8f21bad7c..e8be994f5 100644 --- a/src/assets/keyframes/model/keyframemodellist.cpp +++ b/src/assets/keyframes/model/keyframemodellist.cpp @@ -1,447 +1,447 @@ /*************************************************************************** * 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(model) - , m_undoStack(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 ObjectId(); } 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()) { ParamType 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, QVariant normalizedVal, bool logUndo) +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()) { ParamType 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, QVariant value, const QPersistentModelIndex &index) +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()) { AssetKeyframeCommand *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) { 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()); Keyframe kf = getKeyframe(old_in, &ok); KeyframeType type = kf.second; 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 { 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); // Check keyframes after last position if (ok && !ok2 && oldIn != 0) { positions << old_in; } else if (in == 0 && oldIn != 0 && ok && ok2) { // We moved start to 0. As the 0 keyframe is always here, simply remove old position for (const auto ¶m : m_parameters) { param.second->removeKeyframe(old_in, undo, redo); } } // qDebug()<<"/// \n\nKEYS TO DELETE: "<getInterpolatedValue(new_in); param.second->addKeyframe(new_in, type, value, true, undo, redo); for (auto frame : positions) { param.second->removeKeyframe(frame, undo, redo); } } } } } else { GenTime old_out(oldOut, pCore->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) { 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); } } } } } diff --git a/src/assets/keyframes/model/keyframemodellist.hpp b/src/assets/keyframes/model/keyframemodellist.hpp index a56d48e0a..4b5e86935 100644 --- a/src/assets/keyframes/model/keyframemodellist.hpp +++ b/src/assets/keyframes/model/keyframemodellist.hpp @@ -1,161 +1,161 @@ /*************************************************************************** * Copyright (C) 2017 by Nicolas Carion * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #ifndef KEYFRAMELISTMODELLIST_H #define KEYFRAMELISTMODELLIST_H #include "definitions.h" #include "gentime.h" #include "keyframemodel.hpp" #include "undohelper.hpp" #include #include #include #include #include #include class AssetParameterModel; class DocUndoStack; /* @brief This class is a container for the keyframe models. If an asset has several keyframable parameters, each one has its own keyframeModel, but we regroup all of these in a common class to provide unified access. */ class KeyframeModelList : public QObject { Q_OBJECT public: /* @brief Construct a keyframe list bound to the given asset @param init_value and index correspond to the first parameter */ explicit KeyframeModelList(std::weak_ptr model, const QModelIndex &index, std::weak_ptr undo_stack); /* @brief Add a keyframable parameter to be managed by this model */ void addParameter(const QModelIndex &index); /* @brief Adds a keyframe at the given position. If there is already one then we update it. @param pos defines the position of the keyframe, relative to the clip @param type is the type of the keyframe. */ bool addKeyframe(GenTime pos, KeyframeType type); bool addKeyframe(int frame, double val); /* @brief Removes the keyframe at the given position. */ bool removeKeyframe(GenTime pos); /* @brief Delete all the keyframes of the model (except first) */ bool removeAllKeyframes(); /* @brief Delete all the keyframes after a certain position (except first) */ bool removeNextKeyframes(GenTime pos); /* @brief moves a keyframe @param oldPos is the old position of the keyframe @param pos defines the new position of the keyframe, relative to the clip @param logUndo if true, then an undo object is created */ bool moveKeyframe(GenTime oldPos, GenTime pos, bool logUndo); /* @brief updates the value of a keyframe @param old is the position of the keyframe @param value is the new value of the param @param index is the index of the wanted keyframe */ - bool updateKeyframe(GenTime pos, QVariant value, const QPersistentModelIndex &index); + bool updateKeyframe(GenTime pos, const QVariant &value, const QPersistentModelIndex &index); bool updateKeyframeType(GenTime pos, int type, const QPersistentModelIndex &index); - bool updateKeyframe(GenTime oldPos, GenTime pos, QVariant normalizedVal, bool logUndo = true); + bool updateKeyframe(GenTime oldPos, GenTime pos, const QVariant &normalizedVal, bool logUndo = true); KeyframeType keyframeType(GenTime pos) const; /* @brief Returns a keyframe data at given pos ok is a return parameter, set to true if everything went good */ Keyframe getKeyframe(const GenTime &pos, bool *ok) const; /* @brief Returns true if we only have 1 keyframe */ bool singleKeyframe() const; /* @brief Returns true if we only have no keyframe */ bool isEmpty() const; /* @brief Returns the keyframe located after given position. If there is a keyframe at given position it is ignored. @param ok is a return parameter to tell if a keyframe was found. */ Keyframe getNextKeyframe(const GenTime &pos, bool *ok) const; /* @brief Returns the keyframe located before given position. If there is a keyframe at given position it is ignored. @param ok is a return parameter to tell if a keyframe was found. */ Keyframe getPrevKeyframe(const GenTime &pos, bool *ok) const; /* @brief Returns the closest keyframe from given position. @param ok is a return parameter to tell if a keyframe was found. */ Keyframe getClosestKeyframe(const GenTime &pos, bool *ok) const; /* @brief Returns true if a keyframe exists at given pos Notice that add/remove queries are done in real time (gentime), but this request is made in frame */ Q_INVOKABLE bool hasKeyframe(int frame) const; /* @brief Return the interpolated value of a parameter. @param pos is the position where we interpolate @param index is the index of the queried parameter. */ QVariant getInterpolatedValue(int pos, const QPersistentModelIndex &index) const; /* @brief Load keyframes from the current parameter value. */ void refresh(); /* @brief Reset all keyframes and add a default one */ void reset(); Q_INVOKABLE KeyframeModel *getKeyModel(); KeyframeModel *getKeyModel(const QPersistentModelIndex &index); /** @brief Returns parent asset owner id*/ ObjectId getOwnerId() const; /** @brief Parent item size change, update keyframes*/ void resizeKeyframes(int oldIn, int oldOut, int in, int out, int offset, bool adjustFromEnd, Fun &undo, Fun &redo); protected: /** @brief Helper function to apply a given operation on all parameters */ bool applyOperation(const std::function, Fun &, Fun &)> &op, const QString &undoString); signals: void modelChanged(); private: std::weak_ptr m_model; std::weak_ptr m_undoStack; std::unordered_map> m_parameters; // Index of the parameter that is displayed in timeline QModelIndex m_inTimelineIndex; mutable QReadWriteLock m_lock; // This is a lock that ensures safety in case of concurrent access public: // this is to enable for range loops auto begin() -> decltype(m_parameters.begin()->second->begin()) { return m_parameters.begin()->second->begin(); } auto end() -> decltype(m_parameters.begin()->second->end()) { return m_parameters.begin()->second->end(); } }; #endif diff --git a/src/assets/keyframes/model/keyframemonitorhelper.cpp b/src/assets/keyframes/model/keyframemonitorhelper.cpp index 44a924e9e..57330f194 100644 --- a/src/assets/keyframes/model/keyframemonitorhelper.cpp +++ b/src/assets/keyframes/model/keyframemonitorhelper.cpp @@ -1,54 +1,54 @@ /* Copyright (C) 2018 Jean-Baptiste Mardelle This file is part of Kdenlive. See www.kdenlive.org. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "keyframemonitorhelper.hpp" #include "assets/model/assetparametermodel.hpp" #include "monitor/monitor.h" #include - -KeyframeMonitorHelper::KeyframeMonitorHelper(Monitor *monitor, std::shared_ptr model, QPersistentModelIndex index, QObject *parent) +#include +KeyframeMonitorHelper::KeyframeMonitorHelper(Monitor *monitor, std::shared_ptr model, const QPersistentModelIndex &index, QObject *parent) : QObject(parent) , m_monitor(monitor) - , m_model(model) + , m_model(std::move(model)) , m_active(false) { m_indexes << index; } bool KeyframeMonitorHelper::connectMonitor(bool activate) { if (activate == m_active) { return false; } m_active = activate; if (activate) { connect(m_monitor, &Monitor::effectPointsChanged, this, &KeyframeMonitorHelper::slotUpdateFromMonitorData, Qt::UniqueConnection); } else { disconnect(m_monitor, &Monitor::effectPointsChanged, this, &KeyframeMonitorHelper::slotUpdateFromMonitorData); } return m_active; } -void KeyframeMonitorHelper::addIndex(QPersistentModelIndex index) +void KeyframeMonitorHelper::addIndex(const QPersistentModelIndex &index) { m_indexes << index; } diff --git a/src/assets/keyframes/model/keyframemonitorhelper.hpp b/src/assets/keyframes/model/keyframemonitorhelper.hpp index c996004d6..e92719b00 100644 --- a/src/assets/keyframes/model/keyframemonitorhelper.hpp +++ b/src/assets/keyframes/model/keyframemonitorhelper.hpp @@ -1,80 +1,80 @@ /* Copyright (C) 2018 Jean-Baptiste Mardelle This file is part of Kdenlive. See www.kdenlive.org. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef KFRMONITORHELPER_H #define KFRMONITORHELPER_H #include #include #include #include class Monitor; class AssetParameterModel; /** @brief This class helps manage effects that receive data from the monitor's qml overlay to translate the data and pass it to the model */ class KeyframeMonitorHelper : public QObject { Q_OBJECT public: /* @brief Construct a keyframe list bound to the given effect @param init_value is the value taken by the param at time 0. @param model is the asset this parameter belong to @param index is the index of this parameter in its model */ - explicit KeyframeMonitorHelper(Monitor *monitor, std::shared_ptr model, QPersistentModelIndex index, QObject *parent = nullptr); + explicit KeyframeMonitorHelper(Monitor *monitor, std::shared_ptr model, const QPersistentModelIndex &index, QObject *parent = nullptr); /** @brief Send signals to the monitor to update the qml overlay. @param returns : true if the monitor's connection was changed to active. */ bool connectMonitor(bool activate); /** @brief Send data update to the monitor */ virtual void refreshParams(int pos) = 0; protected: Monitor *m_monitor; std::shared_ptr m_model; /** @brief List of indexes managed by this class */ QList m_indexes; bool m_active; private slots: virtual void slotUpdateFromMonitorData(const QVariantList &v) = 0; public slots: /** @brief For classes that manage several parameters, add a param index to the list */ - void addIndex(QPersistentModelIndex index); + void addIndex(const QPersistentModelIndex &index); signals: /** @brief Send updated keyframe data to the parameter @index */ void updateKeyframeData(QPersistentModelIndex index, const QVariant &v); }; #endif diff --git a/src/assets/keyframes/model/rotoscoping/rotohelper.cpp b/src/assets/keyframes/model/rotoscoping/rotohelper.cpp index 990bdbda1..ca302e7bc 100644 --- a/src/assets/keyframes/model/rotoscoping/rotohelper.cpp +++ b/src/assets/keyframes/model/rotoscoping/rotohelper.cpp @@ -1,98 +1,98 @@ /* Copyright (C) 2018 Jean-Baptiste Mardelle This file is part of Kdenlive. See www.kdenlive.org. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "rotohelper.hpp" #include "assets/keyframes/model/keyframemodellist.hpp" #include "core.h" #include "gentime.h" #include "monitor/monitor.h" #include - +#include RotoHelper::RotoHelper(Monitor *monitor, std::shared_ptr model, QPersistentModelIndex index, QObject *parent) - : KeyframeMonitorHelper(monitor, model, index, parent) + : KeyframeMonitorHelper(monitor, std::move(model), std::move(index), parent) { } void RotoHelper::slotUpdateFromMonitorData(const QVariantList &v) { const QVariant res = RotoHelper::getSpline(QVariant(v), pCore->getCurrentFrameSize()); emit updateKeyframeData(m_indexes.first(), res); } -QVariant RotoHelper::getSpline(QVariant value, const QSize frame) +QVariant RotoHelper::getSpline(const QVariant &value, const QSize frame) { QList bPoints; const QVariantList points = value.toList(); for (int i = 0; i < points.size() / 3; i++) { BPoint b(points.at(3 * i).toPointF(), points.at(3 * i + 1).toPointF(), points.at(3 * i + 2).toPointF()); bPoints << b; } QList vlist; foreach (const BPoint &point, bPoints) { QList pl; for (int i = 0; i < 3; ++i) { pl << QVariant(QList() << QVariant(point[i].x() / frame.width()) << QVariant(point[i].y() / frame.height())); } vlist << QVariant(pl); } return vlist; } void RotoHelper::refreshParams(int pos) { QVariantList centerPoints; QVariantList controlPoints; std::shared_ptr keyframes = m_model->getKeyframeModel(); if (!keyframes->isEmpty()) { QVariant splineData = keyframes->getInterpolatedValue(pos, m_indexes.first()); QList p = getPoints(splineData, pCore->getCurrentFrameSize()); for (int i = 0; i < p.size(); i++) { centerPoints << QVariant(p.at(i).p); controlPoints << QVariant(p.at(i).h1); controlPoints << QVariant(p.at(i).h2); } if (m_monitor) { m_monitor->setUpEffectGeometry(QRect(), centerPoints, controlPoints); } } } -QList RotoHelper::getPoints(QVariant value, const QSize frame) +QList RotoHelper::getPoints(const QVariant &value, const QSize frame) { QList points; QList data = value.toList(); // skip tracking flag if (data.count() && data.at(0).canConvert(QVariant::String)) { data.removeFirst(); } foreach (const QVariant &bpoint, data) { QList l = bpoint.toList(); BPoint p; for (int i = 0; i < 3; ++i) { p[i] = QPointF(l.at(i).toList().at(0).toDouble() * frame.width(), l.at(i).toList().at(1).toDouble() * frame.height()); } points << p; } return points; } diff --git a/src/assets/keyframes/model/rotoscoping/rotohelper.hpp b/src/assets/keyframes/model/rotoscoping/rotohelper.hpp index 2fbff5788..a1a8b2cfc 100644 --- a/src/assets/keyframes/model/rotoscoping/rotohelper.hpp +++ b/src/assets/keyframes/model/rotoscoping/rotohelper.hpp @@ -1,59 +1,59 @@ /* Copyright (C) 2018 Jean-Baptiste Mardelle This file is part of Kdenlive. See www.kdenlive.org. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef ROTOHELPER_H #define ROTOHELPER_H #include "assets/keyframes/model/keyframemonitorhelper.hpp" #include "bpoint.h" #include #include #include class Monitor; class RotoHelper : public KeyframeMonitorHelper { Q_OBJECT public: /* @brief Construct a keyframe list bound to the given effect @param init_value is the value taken by the param at time 0. @param model is the asset this parameter belong to @param index is the index of this parameter in its model */ explicit RotoHelper(Monitor *monitor, std::shared_ptr model, QPersistentModelIndex index, QObject *parent = nullptr); /** @brief Send signals to the monitor to update the qml overlay. @param returns : true if the monitor's connection was changed to active. */ - static QVariant getSpline(QVariant value, const QSize frame); + static QVariant getSpline(const QVariant &value, const QSize frame); /** @brief Returns a list of spline control points, based on its string definition and frame size @param value : the spline's string definition @param frame: the frame size */ - static QList getPoints(QVariant value, const QSize frame); + static QList getPoints(const QVariant &value, const QSize frame); void refreshParams(int pos) override; private slots: void slotUpdateFromMonitorData(const QVariantList &v) override; }; #endif diff --git a/src/assets/keyframes/view/keyframeview.cpp b/src/assets/keyframes/view/keyframeview.cpp index 840c809a0..1f1c9035d 100644 --- a/src/assets/keyframes/view/keyframeview.cpp +++ b/src/assets/keyframes/view/keyframeview.cpp @@ -1,340 +1,340 @@ /*************************************************************************** * Copyright (C) 2011 by Till Theato (root@ttill.de) * * Copyright (C) 2017 by Nicolas Carion * * This file is part of Kdenlive (www.kdenlive.org). * * * * Kdenlive 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. * * * * Kdenlive 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 Kdenlive. If not, see . * ***************************************************************************/ #include "keyframeview.hpp" #include "assets/keyframes/model/keyframemodellist.hpp" #include "core.h" #include "kdenlivesettings.h" #include #include #include #include - +#include KeyframeView::KeyframeView(std::shared_ptr model, int duration, QWidget *parent) : QWidget(parent) - , m_model(model) + , m_model(std::move(model)) , m_duration(duration) , m_position(0) , m_currentKeyframe(-1) , m_currentKeyframeOriginal(-1) , m_hoverKeyframe(-1) , m_scale(1) { setMouseTracking(true); setMinimumSize(QSize(150, 20)); setSizePolicy(QSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Maximum)); setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)); QPalette p = palette(); KColorScheme scheme(p.currentColorGroup(), KColorScheme::Window); m_colSelected = palette().highlight().color(); m_colKeyframe = scheme.foreground(KColorScheme::NormalText).color(); m_size = QFontInfo(font()).pixelSize() * 1.8; m_lineHeight = m_size / 2; m_offset = m_lineHeight; setFixedHeight(m_size); connect(m_model.get(), &KeyframeModelList::modelChanged, this, &KeyframeView::slotModelChanged); } void KeyframeView::slotModelChanged() { int offset = pCore->getItemIn(m_model->getOwnerId()); emit atKeyframe(m_model->hasKeyframe(m_position + offset), m_model->singleKeyframe()); emit modified(); update(); } void KeyframeView::slotSetPosition(int pos, bool isInRange) { if (!isInRange) { m_position = -1; update(); return; } if (pos != m_position) { m_position = pos; int offset = pCore->getItemIn(m_model->getOwnerId()); emit atKeyframe(m_model->hasKeyframe(pos + offset), m_model->singleKeyframe()); update(); } } void KeyframeView::initKeyframePos() { emit atKeyframe(m_model->hasKeyframe(m_position), m_model->singleKeyframe()); } void KeyframeView::slotAddKeyframe(int pos) { if (pos < 0) { pos = m_position; } int offset = pCore->getItemIn(m_model->getOwnerId()); m_model->addKeyframe(GenTime(size_t(pos + offset), pCore->getCurrentFps()), (KeyframeType)KdenliveSettings::defaultkeyframeinterp()); } void KeyframeView::slotAddRemove() { int offset = pCore->getItemIn(m_model->getOwnerId()); if (m_model->hasKeyframe(m_position + offset)) { slotRemoveKeyframe(m_position); } else { slotAddKeyframe(m_position); } } void KeyframeView::slotEditType(int type, const QPersistentModelIndex &index) { int offset = pCore->getItemIn(m_model->getOwnerId()); if (m_model->hasKeyframe(m_position + offset)) { m_model->updateKeyframeType(GenTime(size_t(m_position + offset), pCore->getCurrentFps()), type, index); } } void KeyframeView::slotRemoveKeyframe(int pos) { if (pos < 0) { pos = m_position; } int offset = pCore->getItemIn(m_model->getOwnerId()); m_model->removeKeyframe(GenTime(size_t(pos + offset), pCore->getCurrentFps())); } void KeyframeView::setDuration(int dur) { m_duration = dur; int offset = pCore->getItemIn(m_model->getOwnerId()); emit atKeyframe(m_model->hasKeyframe(m_position + offset), m_model->singleKeyframe()); update(); } void KeyframeView::slotGoToNext() { if (m_position == m_duration - 1) { return; } bool ok; int offset = pCore->getItemIn(m_model->getOwnerId()); auto next = m_model->getNextKeyframe(GenTime(size_t(m_position + offset), pCore->getCurrentFps()), &ok); if (ok) { emit seekToPos(qMin((int)next.first.frames(pCore->getCurrentFps()) - offset, m_duration - 1)); } else { // no keyframe after current position emit seekToPos(m_duration - 1); } } void KeyframeView::slotGoToPrev() { if (m_position == 0) { return; } bool ok; int offset = pCore->getItemIn(m_model->getOwnerId()); auto prev = m_model->getPrevKeyframe(GenTime(m_position + offset, pCore->getCurrentFps()), &ok); if (ok) { emit seekToPos(qMax(0, (int)prev.first.frames(pCore->getCurrentFps()) - offset)); } else { // no keyframe after current position emit seekToPos(m_duration - 1); } } void KeyframeView::mousePressEvent(QMouseEvent *event) { int offset = pCore->getItemIn(m_model->getOwnerId()); int pos = (event->x() - m_offset) / m_scale; if (event->y() < m_lineHeight && event->button() == Qt::LeftButton) { bool ok; GenTime position(pos + offset, pCore->getCurrentFps()); auto keyframe = m_model->getClosestKeyframe(position, &ok); if (ok && qAbs(keyframe.first.frames(pCore->getCurrentFps()) - pos - offset) * m_scale < ceil(m_lineHeight / 1.5)) { m_currentKeyframeOriginal = keyframe.first.frames(pCore->getCurrentFps()) - offset; // Select and seek to keyframe m_currentKeyframe = m_currentKeyframeOriginal; emit seekToPos(m_currentKeyframeOriginal); return; } } // no keyframe next to mouse m_currentKeyframe = m_currentKeyframeOriginal = -1; emit seekToPos(pos); update(); } void KeyframeView::mouseMoveEvent(QMouseEvent *event) { int offset = pCore->getItemIn(m_model->getOwnerId()); int pos = qBound(0, (int)((event->x() - m_offset) / m_scale), m_duration - 1); GenTime position(pos + offset, pCore->getCurrentFps()); if ((event->buttons() & Qt::LeftButton) != 0u) { if (m_currentKeyframe == pos) { return; } if (m_currentKeyframe > 0) { if (!m_model->hasKeyframe(pos + offset)) { GenTime currentPos(m_currentKeyframe + offset, pCore->getCurrentFps()); if (m_model->moveKeyframe(currentPos, position, false)) { m_currentKeyframe = pos; } } } emit seekToPos(pos); return; } if (event->y() < m_lineHeight) { bool ok; auto keyframe = m_model->getClosestKeyframe(position, &ok); if (ok && qAbs(keyframe.first.frames(pCore->getCurrentFps()) - pos - offset) * m_scale < ceil(m_lineHeight / 1.5)) { m_hoverKeyframe = keyframe.first.frames(pCore->getCurrentFps()) - offset; setCursor(Qt::PointingHandCursor); update(); return; } } if (m_hoverKeyframe != -1) { m_hoverKeyframe = -1; setCursor(Qt::ArrowCursor); update(); } } void KeyframeView::mouseReleaseEvent(QMouseEvent *event) { Q_UNUSED(event) if (m_currentKeyframe >= 0) { int offset = pCore->getItemIn(m_model->getOwnerId()); GenTime initPos(m_currentKeyframeOriginal + offset, pCore->getCurrentFps()); GenTime targetPos(m_currentKeyframe + offset, pCore->getCurrentFps()); bool ok1 = m_model->moveKeyframe(targetPos, initPos, false); bool ok2 = m_model->moveKeyframe(initPos, targetPos, true); qDebug() << "RELEASING keyframe move" << ok1 << ok2 << initPos.frames(pCore->getCurrentFps()) << targetPos.frames(pCore->getCurrentFps()); } } void KeyframeView::mouseDoubleClickEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton && event->y() < m_lineHeight) { int pos = qBound(0, (int)((event->x() - m_offset) / m_scale), m_duration - 1); int offset = pCore->getItemIn(m_model->getOwnerId()); GenTime position(pos + offset, pCore->getCurrentFps()); bool ok; auto keyframe = m_model->getClosestKeyframe(position, &ok); if (ok && qAbs(keyframe.first.frames(pCore->getCurrentFps()) - pos - offset) * m_scale < ceil(m_lineHeight / 1.5)) { m_model->removeKeyframe(keyframe.first); if (keyframe.first.frames(pCore->getCurrentFps()) == m_currentKeyframe + offset) { m_currentKeyframe = m_currentKeyframeOriginal = -1; } if (keyframe.first.frames(pCore->getCurrentFps()) == m_position + offset) { emit atKeyframe(false, m_model->singleKeyframe()); } return; } // add new keyframe m_model->addKeyframe(position, (KeyframeType)KdenliveSettings::defaultkeyframeinterp()); } else { QWidget::mouseDoubleClickEvent(event); } } void KeyframeView::wheelEvent(QWheelEvent *event) { if (event->modifiers() & Qt::AltModifier) { if (event->delta() > 0) { slotGoToPrev(); } else { slotGoToNext(); } return; } int change = event->delta() > 0 ? -1 : 1; int pos = qBound(0, m_position + change, m_duration - 1); emit seekToPos(pos); } void KeyframeView::paintEvent(QPaintEvent *event) { Q_UNUSED(event) QStylePainter p(this); m_scale = (width() - 2 * m_offset) / (double)(m_duration - 1); // p.translate(0, m_lineHeight); int headOffset = m_lineHeight / 1.5; int offset = pCore->getItemIn(m_model->getOwnerId()); /* * keyframes */ for (const auto &keyframe : *m_model.get()) { int pos = keyframe.first.frames(pCore->getCurrentFps()) - offset; if (pos == m_currentKeyframe || pos == m_hoverKeyframe) { p.setBrush(m_colSelected); } else { p.setBrush(m_colKeyframe); } double scaledPos = m_offset + (pos * m_scale); p.drawLine(QPointF(scaledPos, headOffset), QPointF(scaledPos, m_lineHeight + headOffset / 2.0)); switch (keyframe.second.first) { case KeyframeType::Linear: { QPolygonF position = QPolygonF() << QPointF(-headOffset / 2.0, headOffset / 2.0) << QPointF(0, 0) << QPointF(headOffset / 2.0, headOffset / 2.0) << QPointF(0, headOffset); position.translate(scaledPos, 0); p.drawPolygon(position); break; } case KeyframeType::Discrete: p.drawRect(QRectF(scaledPos - headOffset / 2.0, 0, headOffset, headOffset)); break; default: p.drawEllipse(QRectF(scaledPos - headOffset / 2.0, 0, headOffset, headOffset)); break; } } p.setPen(palette().dark().color()); /* * Time-"line" */ p.setPen(m_colKeyframe); p.drawLine(m_offset, m_lineHeight + (headOffset / 2), width() - m_offset, m_lineHeight + (headOffset / 2)); /* * current position */ if (m_position >= 0 && m_position < m_duration) { QPolygon pa(3); int cursorwidth = (m_size - (m_lineHeight + headOffset / 2)) / 2 + 1; QPolygonF position = QPolygonF() << QPointF(-cursorwidth, m_size) << QPointF(cursorwidth, m_size) << QPointF(0, m_lineHeight + (headOffset / 2) + 1); position.translate(m_offset + (m_position * m_scale), 0); p.setBrush(m_colKeyframe); p.drawPolygon(position); } p.setOpacity(0.5); p.drawLine(m_offset, m_lineHeight, m_offset, m_lineHeight + headOffset ); p.drawLine(width() - m_offset, m_lineHeight, width() - m_offset, m_lineHeight + headOffset ); } diff --git a/src/assets/model/assetcommand.cpp b/src/assets/model/assetcommand.cpp index 0e082e997..0273b06b3 100644 --- a/src/assets/model/assetcommand.cpp +++ b/src/assets/model/assetcommand.cpp @@ -1,151 +1,152 @@ /*************************************************************************** * Copyright (C) 2017 by by Jean-Baptiste Mardelle * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #include "assetcommand.hpp" #include "assets/keyframes/model/keyframemodellist.hpp" #include "effects/effectsrepository.hpp" #include "transitions/transitionsrepository.hpp" #include -AssetCommand::AssetCommand(std::shared_ptr model, const QModelIndex &index, const QString &value, QUndoCommand *parent) +AssetCommand::AssetCommand(const std::shared_ptr &model, const QModelIndex &index, const QString &value, QUndoCommand *parent) : QUndoCommand(parent) , m_model(model) , m_index(index) , m_value(value) , m_updateView(false) , m_stamp(QTime::currentTime()) { QLocale locale; m_name = m_model->data(index, AssetParameterModel::NameRole).toString(); const QString id = model->getAssetId(); if (EffectsRepository::get()->exists(id)) { setText(i18n("Edit %1", EffectsRepository::get()->getName(id))); } else if (TransitionsRepository::get()->exists(id)) { setText(i18n("Edit %1", TransitionsRepository::get()->getName(id))); } QVariant previousVal = m_model->data(index, AssetParameterModel::ValueRole); m_oldValue = previousVal.type() == QVariant::Double ? locale.toString(previousVal.toDouble()) : previousVal.toString(); } void AssetCommand::undo() { m_model->setParameter(m_name, m_oldValue, true, m_index); } // virtual void AssetCommand::redo() { m_model->setParameter(m_name, m_value, m_updateView, m_index); m_updateView = true; } // virtual int AssetCommand::id() const { return 1; } // virtual bool AssetCommand::mergeWith(const QUndoCommand *other) { if (other->id() != id() || static_cast(other)->m_index != m_index || m_stamp.msecsTo(static_cast(other)->m_stamp) > 3000) { return false; } m_value = static_cast(other)->m_value; m_stamp = static_cast(other)->m_stamp; return true; } -AssetKeyframeCommand::AssetKeyframeCommand(std::shared_ptr model, const QModelIndex &index, const QVariant &value, GenTime pos, +AssetKeyframeCommand::AssetKeyframeCommand(const std::shared_ptr &model, const QModelIndex &index, const QVariant &value, GenTime pos, QUndoCommand *parent) : QUndoCommand(parent) , m_model(model) , m_index(index) , m_value(value) , m_pos(pos) , m_updateView(false) , m_stamp(QTime::currentTime()) { const QString id = model->getAssetId(); if (EffectsRepository::get()->exists(id)) { setText(i18n("Edit %1 keyframe", EffectsRepository::get()->getName(id))); } else if (TransitionsRepository::get()->exists(id)) { setText(i18n("Edit %1 keyframe", TransitionsRepository::get()->getName(id))); } m_oldValue = m_model->getKeyframeModel()->getKeyModel(m_index)->getInterpolatedValue(m_pos); } void AssetKeyframeCommand::undo() { m_model->getKeyframeModel()->getKeyModel(m_index)->directUpdateKeyframe(m_pos, m_oldValue); } // virtual void AssetKeyframeCommand::redo() { m_model->getKeyframeModel()->getKeyModel(m_index)->directUpdateKeyframe(m_pos, m_value); m_updateView = true; } // virtual int AssetKeyframeCommand::id() const { return 2; } // virtual bool AssetKeyframeCommand::mergeWith(const QUndoCommand *other) { if (other->id() != id() || static_cast(other)->m_index != m_index || m_stamp.msecsTo(static_cast(other)->m_stamp) > 1000) { return false; } m_value = static_cast(other)->m_value; m_stamp = static_cast(other)->m_stamp; return true; } -AssetUpdateCommand::AssetUpdateCommand(std::shared_ptr model, const QVector> parameters, QUndoCommand *parent) +AssetUpdateCommand::AssetUpdateCommand(const std::shared_ptr &model, const QVector> ¶meters, + QUndoCommand *parent) : QUndoCommand(parent) , m_model(model) , m_value(parameters) { const QString id = model->getAssetId(); if (EffectsRepository::get()->exists(id)) { setText(i18n("Update %1", EffectsRepository::get()->getName(id))); } else if (TransitionsRepository::get()->exists(id)) { setText(i18n("Update %1", TransitionsRepository::get()->getName(id))); } m_oldValue = m_model->getAllParameters(); } void AssetUpdateCommand::undo() { m_model->setParameters(m_oldValue); } // virtual void AssetUpdateCommand::redo() { m_model->setParameters(m_value); } // virtual int AssetUpdateCommand::id() const { return 3; } diff --git a/src/assets/model/assetcommand.hpp b/src/assets/model/assetcommand.hpp index 4d990a520..b579f1d86 100644 --- a/src/assets/model/assetcommand.hpp +++ b/src/assets/model/assetcommand.hpp @@ -1,82 +1,82 @@ /*************************************************************************** * Copyright (C) 2017 by Jean-Baptiste Mardelle * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #ifndef ASSETCOMMAND_H #define ASSETCOMMAND_H #include "assetparametermodel.hpp" #include #include #include class AssetCommand : public QUndoCommand { public: - AssetCommand(std::shared_ptr model, const QModelIndex &index, const QString &value, QUndoCommand *parent = nullptr); + AssetCommand(const std::shared_ptr &model, const QModelIndex &index, const QString &value, QUndoCommand *parent = nullptr); void undo() override; void redo() override; int id() const override; bool mergeWith(const QUndoCommand *other) override; private: std::shared_ptr m_model; QPersistentModelIndex m_index; QString m_value; QString m_name; QString m_oldValue; bool m_updateView; QTime m_stamp; }; class AssetKeyframeCommand : public QUndoCommand { public: - AssetKeyframeCommand(std::shared_ptr model, const QModelIndex &index, const QVariant &value, GenTime pos, + AssetKeyframeCommand(const std::shared_ptr &model, const QModelIndex &index, const QVariant &value, GenTime pos, QUndoCommand *parent = nullptr); void undo() override; void redo() override; int id() const override; bool mergeWith(const QUndoCommand *other) override; private: std::shared_ptr m_model; QPersistentModelIndex m_index; QVariant m_value; QVariant m_oldValue; GenTime m_pos; bool m_updateView; QTime m_stamp; }; class AssetUpdateCommand : public QUndoCommand { public: - AssetUpdateCommand(std::shared_ptr model, QVector> parameters, QUndoCommand *parent = nullptr); + AssetUpdateCommand(const std::shared_ptr &model, const QVector> ¶meters, QUndoCommand *parent = nullptr); void undo() override; void redo() override; int id() const override; private: std::shared_ptr m_model; QVector> m_value; QVector> m_oldValue; }; #endif diff --git a/src/assets/model/assetparametermodel.cpp b/src/assets/model/assetparametermodel.cpp index e592d9241..3c59dff5a 100644 --- a/src/assets/model/assetparametermodel.cpp +++ b/src/assets/model/assetparametermodel.cpp @@ -1,817 +1,817 @@ /*************************************************************************** * 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 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")); 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.isNull()) { 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); 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()) { setParameter(name, value, false); // 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 = currentParameter.firstChildElement(QStringLiteral("name")).text(); 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[name].type == ParamType::KeyframeParam || m_params[name].type == ParamType::AnimatedRect || m_params[name].type == ParamType::Roto_spline) { addKeyframeParam(index(ix, 0)); } ix++; } } void AssetParameterModel::setParameter(const QString &name, const 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 (update) { 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()); } else { emit modelChanged(); emit dataChanged(index(0, 0), index(m_rows.count() - 1, 0), {}); } // Update fades in timeline pCore->updateItemModel(m_ownerId, m_assetId); // Trigger monitor refresh pCore->refreshProjectItem(m_ownerId); // Invalidate timeline preview pCore->invalidateItem(m_ownerId); } } void AssetParameterModel::setParameter(const QString &name, const QString ¶mValue, bool update, const QModelIndex ¶mIndex) { Q_ASSERT(m_asset->is_valid()); QLocale locale; locale.setNumberOptions(QLocale::OmitGroupSeparator); qDebug() << "// PROCESSING PARAM CHANGE: " << name << ", UPDATE: "<() == 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++) { - QString pointVal = vals.at(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; } else { m_fixedParams[name] = paramValue; } } if (update) { 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()); } else { 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(); } } 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); // Trigger monitor refresh pCore->refreshProjectItem(m_ownerId); // Invalidate timeline preview pCore->invalidateItem(m_ownerId); } } void AssetParameterModel::setParameter(const QString &name, double &value) { 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()); } else { emit modelChanged(); } pCore->refreshProjectItem(m_ownerId); 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 DefaultRole: return parseAttribute(m_ownerId, QStringLiteral("default"), element); case FilterRole: return parseAttribute(m_ownerId, QStringLiteral("filter"), element); 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); // 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)); if (type == ParamType::Double) { // Use a Mlt::Properties to parse mathematical operators Mlt::Properties p; p.set("eval", content.toLatin1().constData()); return p.get_double("eval"); } } else if (type == ParamType::Double || type == ParamType::Hidden) { QLocale locale; locale.setNumberOptions(QLocale::OmitGroupSeparator); if (attribute == QLatin1String("default")) { int factor = element.attribute(QStringLiteral("factor"), QStringLiteral("1")).toInt(); if (factor > 0) { return content.toDouble() / factor; } } 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; } } else if (type == ParamType::Bezier_spline) { QLocale locale; if (locale.decimalPoint() != QLocale::c().decimalPoint()) { return content.replace(QLocale::c().decimalPoint(), locale.decimalPoint()); } } } return content; } 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() const { QJsonArray list; QLocale locale; 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.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(); 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)); list.push_back(currentParam); } for (const auto ¶m : m_params) { QJsonObject currentParam; QModelIndex ix = index(m_rows.indexOf(param.first), 0); currentParam.insert(QLatin1String("name"), QJsonValue(param.first)); currentParam.insert(QLatin1String("value"), 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(); 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)); 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 } 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 } 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 (int i = 0; i < array.size(); i++) { QJsonValue val = array.at(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 (int i = 0; i < array.size(); i++) { QJsonValue val = array.at(i); if (val.isObject() && val.toObject().contains(presetName)) { QJsonValue preset = val.toObject().value(presetName); if (preset.isArray()) { QJsonArray paramArray = preset.toArray(); for (int j = 0; j < paramArray.size(); j++) { QJsonValue v1 = paramArray.at(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 modelChanged(); 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) +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/assets/model/assetparametermodel.hpp b/src/assets/model/assetparametermodel.hpp index eb1963b5c..2aff2f604 100644 --- a/src/assets/model/assetparametermodel.hpp +++ b/src/assets/model/assetparametermodel.hpp @@ -1,220 +1,220 @@ /*************************************************************************** * Copyright (C) 2017 by Nicolas Carion * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #ifndef ASSETPARAMETERMODEL_H #define ASSETPARAMETERMODEL_H #include "definitions.h" #include "klocalizedstring.h" #include #include #include #include #include #include #include class KeyframeModelList; /* @brief This class is the model for a list of parameters. The behaviour of a transition or an effect is typically controlled by several parameters. This class exposes this parameters as a list that can be rendered using the relevant widgets. Note that internally parameters are not sorted in any ways, because some effects like sox need a precise order */ enum class ParamType { Double, List, Bool, Switch, RestrictedAnim, // animated 1 dimensional param with linear support only Animated, AnimatedRect, Geometry, Addedgeometry, KeyframeParam, Color, ColorWheel, Position, Curve, Bezier_spline, Roto_spline, Wipe, Url, Keywords, Fontfamily, Filterjob, Readonly, Hidden }; Q_DECLARE_METATYPE(ParamType) class AssetParameterModel : public QAbstractListModel, public enable_shared_from_this_virtual { Q_OBJECT public: explicit AssetParameterModel(std::unique_ptr asset, const QDomElement &assetXml, const QString &assetId, ObjectId ownerId, QObject *parent = nullptr); virtual ~AssetParameterModel(); enum DataRoles { NameRole = Qt::UserRole + 1, TypeRole, CommentRole, MinRole, MaxRole, DefaultRole, SuffixRole, DecimalsRole, ValueRole, AlphaRole, ListValuesRole, ListNamesRole, FactorRole, FilterRole, ScaleRole, OpacityRole, RelativePosRole, // Don't display this param in timeline keyframes ShowInTimelineRole, InRole, OutRole, ParentInRole, ParentPositionRole, ParentDurationRole, HideKeyframesFirstRole, List1Role, List2Role, Enum1Role, Enum2Role, Enum3Role, Enum4Role, Enum5Role, Enum6Role, Enum7Role, Enum8Role, Enum9Role, Enum10Role, Enum11Role, Enum12Role, Enum13Role, Enum14Role, Enum15Role, Enum16Role }; /* @brief Returns the id of the asset represented by this object */ QString getAssetId() const; /* @brief Set the parameter with given name to the given value */ Q_INVOKABLE void setParameter(const QString &name, const QString ¶mValue, bool update = true, const QModelIndex ¶mIndex = QModelIndex()); void setParameter(const QString &name, const int value, bool update = true); Q_INVOKABLE void setParameter(const QString &name, double &value); /* @brief Return all the parameters as pairs (parameter name, parameter value) */ QVector> getAllParameters() const; /* @brief Returns a json definition of the effect with all param values */ QJsonDocument toJson() const; void savePreset(const QString &presetFile, const QString &presetName); void deletePreset(const QString &presetFile, const QString &presetName); const QStringList getPresetList(const QString &presetFile) const; const QVector> loadPreset(const QString &presetFile, const QString &presetName); /* @brief Sets the value of a list of parameters @param params contains the pairs (parameter name, parameter value) */ void setParameters(const QVector> ¶ms); /* Which monitor is attached to this asset (clip/project) */ Kdenlive::MonitorId monitorId; QVariant data(const QModelIndex &index, int role) const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override; /* @brief Returns the id of the actual object associated with this asset */ ObjectId getOwnerId() const; /* @brief Returns the keyframe model associated with this asset Return empty ptr if there is no keyframable parameter in the asset or if prepareKeyframes was not called */ Q_INVOKABLE std::shared_ptr getKeyframeModel(); /* @brief Must be called before using the keyframes of this model */ void prepareKeyframes(); void resetAsset(std::unique_ptr asset); /* @brief Returns true if the effect has more than one keyframe */ bool hasMoreThanOneKeyframe() const; - int time_to_frames(const QString time); + int time_to_frames(const QString &time); void passProperties(Mlt::Properties &target); protected: /* @brief Helper function to retrieve the type of a parameter given the string corresponding to it*/ static ParamType paramTypeFromStr(const QString &type); static QString getDefaultKeyframes(int start, const QString &defaultValue, bool linearOnly); /* @brief Helper function to get an attribute from a dom element, given its name. The function additionally parses following keywords: - %width and %height that are replaced with profile's height and width. If keywords are found, mathematical operations are supported for double type params. For example "%width -1" is a valid value. */ static QVariant parseAttribute(const ObjectId owner, const QString &attribute, const QDomElement &element, QVariant defaultValue = QVariant()); /* @brief Helper function to register one more parameter that is keyframable. @param index is the index corresponding to this parameter */ void addKeyframeParam(const QModelIndex index); struct ParamRow { ParamType type; QDomElement xml; QVariant value; QString name; }; QString m_assetId; ObjectId m_ownerId; std::vector m_paramOrder; // Keep track of parameter order, important for sox std::unordered_map m_params; // Store all parameters by name std::unordered_map m_fixedParams; // We store values of fixed parameters aside QVector m_rows; // We store the params name in order of parsing. The order is important (cf some effects like sox) std::unique_ptr m_asset; std::shared_ptr m_keyframes; // if true, keyframe tools will be hidden by default bool m_hideKeyframesByDefault; signals: void modelChanged(); /** @brief inform child effects (in case of bin effect with timeline producers) * that a change occurred and a param update is needed **/ void updateChildren(const QString &name); void compositionTrackChanged(); void replugEffect(std::shared_ptr asset); void rebuildEffect(std::shared_ptr asset); void enabledChange(bool); }; #endif diff --git a/src/assets/view/widgets/curves/cubic/kis_curve_widget.cpp b/src/assets/view/widgets/curves/cubic/kis_curve_widget.cpp index 0048df125..ee89eee0c 100644 --- a/src/assets/view/widgets/curves/cubic/kis_curve_widget.cpp +++ b/src/assets/view/widgets/curves/cubic/kis_curve_widget.cpp @@ -1,335 +1,335 @@ /* * Copyright (c) 2005 C. Boemann * Copyright (c) 2009 Dmitry Kazakov * * 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. */ // Local includes. #include "kis_curve_widget.h" #include "kdenlivesettings.h" // C++ includes. #include #include // Qt includes. #include #include #include #include #include #include #include #include #include #define bounds(x, a, b) (x < a ? a : (x > b ? b : x)) #define MOUSE_AWAY_THRES 15 #define POINT_AREA 1E-4 #define CURVE_AREA 1E-4 // static bool pointLessThan(const QPointF &a, const QPointF &b); KisCurveWidget::KisCurveWidget(QWidget *parent) : AbstractCurveWidget(parent) { setObjectName(QStringLiteral("KisCurveWidget")); m_guideVisible = false; m_maxPoints = -1; m_grabOffsetX = 0; m_grabOffsetY = 0; m_grabOriginalX = 0; m_grabOriginalY = 0; m_draggedAwayPointIndex = 0; m_pixmapIsDirty = 0; m_pixmapCache = nullptr; m_maxPoints = 0; m_curve = KisCubicCurve(); update(); } KisCurveWidget::~KisCurveWidget() {} QSize KisCurveWidget::sizeHint() const { return QSize(500, 500); } void KisCurveWidget::addPointInTheMiddle() { QPointF pt(0.5, m_curve.value(0.5)); if (!jumpOverExistingPoints(pt, -1)) { return; } m_currentPointIndex = m_curve.addPoint(pt); update(); emit modified(); } void KisCurveWidget::paintEvent(QPaintEvent *) { QPainter p(this); paintBackground(&p); // Draw curve. int x; QPolygonF poly; p.setPen(QPen(palette().text().color(), 1, Qt::SolidLine)); poly.reserve(m_wWidth); for (x = 0; x < m_wWidth; ++x) { double normalizedX = double(x) / m_wWidth; double curY = m_wHeight - m_curve.value(normalizedX) * m_wHeight; poly.append(QPointF(x, curY)); } poly.append(QPointF(x, m_wHeight - m_curve.value(1.0) * m_wHeight)); p.drawPolyline(poly); // Drawing curve handles. for (int i = 0; i < m_curve.points().count(); ++i) { double curveX = m_curve.points().at(i).x(); double curveY = m_curve.points().at(i).y(); if (i == m_currentPointIndex) { p.setPen(QPen(Qt::red, 3, Qt::SolidLine)); p.drawEllipse(QRectF(curveX * m_wWidth - 2, m_wHeight - 2 - curveY * m_wHeight, 4, 4)); } else { p.setPen(QPen(Qt::red, 1, Qt::SolidLine)); p.drawEllipse(QRectF(curveX * m_wWidth - 3, m_wHeight - 3 - curveY * m_wHeight, 6, 6)); } } } void KisCurveWidget::mousePressEvent(QMouseEvent *e) { int wWidth = width() - 1; int wHeight = height() - 1; int offsetX = 1 / 8. * m_zoomLevel * wWidth; int offsetY = 1 / 8. * m_zoomLevel * wHeight; wWidth -= 2 * offsetX; wHeight -= 2 * offsetY; double x = (e->pos().x() - offsetX) / (double)(wWidth); double y = 1.0 - (e->pos().y() - offsetY) / (double)(wHeight); int closest_point_index = nearestPointInRange(QPointF(x, y), width(), height()); if (e->button() == Qt::RightButton && closest_point_index > 0 && closest_point_index < m_curve.points().count() - 1) { m_currentPointIndex = closest_point_index; slotDeleteCurrentPoint(); } else if (e->button() != Qt::LeftButton) { return; } if (closest_point_index < 0) { if (m_maxPoints > 0 && m_curve.points().count() >= m_maxPoints) { return; } QPointF newPoint(x, y); if (!jumpOverExistingPoints(newPoint, -1)) { return; } m_currentPointIndex = m_curve.addPoint(newPoint); } else { m_currentPointIndex = closest_point_index; } m_grabOriginalX = m_curve.points().at(m_currentPointIndex).x(); m_grabOriginalY = m_curve.points().at(m_currentPointIndex).y(); m_grabOffsetX = m_curve.points().at(m_currentPointIndex).x() - x; m_grabOffsetY = m_curve.points().at(m_currentPointIndex).y() - y; QPointF point(x + m_grabOffsetX, y + m_grabOffsetY); m_curve.setPoint(m_currentPointIndex, point); m_draggedAwayPointIndex = -1; m_state = State_t::DRAG; update(); emit currentPoint(point, isCurrentPointExtremal()); } void KisCurveWidget::mouseMoveEvent(QMouseEvent *e) { int wWidth = width() - 1; int wHeight = height() - 1; int offsetX = 1 / 8. * m_zoomLevel * wWidth; int offsetY = 1 / 8. * m_zoomLevel * wHeight; wWidth -= 2 * offsetX; wHeight -= 2 * offsetY; double x = (e->pos().x() - offsetX) / (double)(wWidth); double y = 1.0 - (e->pos().y() - offsetY) / (double)(wHeight); if (m_state == State_t::NORMAL) { // If no point is selected set the cursor shape if on top int nearestPointIndex = nearestPointInRange(QPointF(x, y), width(), height()); if (nearestPointIndex < 0) { setCursor(Qt::ArrowCursor); } else { setCursor(Qt::CrossCursor); } } else { // Else, drag the selected point bool crossedHoriz = e->pos().x() - width() > MOUSE_AWAY_THRES || e->pos().x() < -MOUSE_AWAY_THRES; bool crossedVert = e->pos().y() - height() > MOUSE_AWAY_THRES || e->pos().y() < -MOUSE_AWAY_THRES; bool removePoint = (crossedHoriz || crossedVert); if (!removePoint && m_draggedAwayPointIndex >= 0) { // point is no longer dragged away so reinsert it QPointF newPoint(m_draggedAwayPoint); m_currentPointIndex = m_curve.addPoint(newPoint); m_draggedAwayPointIndex = -1; } if (removePoint && (m_draggedAwayPointIndex >= 0)) { return; } setCursor(Qt::CrossCursor); x += m_grabOffsetX; y += m_grabOffsetY; double leftX; double rightX; if (m_currentPointIndex == 0) { leftX = 0.0; rightX = 0.0; /*if (m_curve.points().count() > 1) rightX = m_curve.points().at(m_currentPointIndex + 1).x() - POINT_AREA; else rightX = 1.0;*/ } else if (m_currentPointIndex == m_curve.points().count() - 1) { leftX = m_curve.points().at(m_currentPointIndex - 1).x() + POINT_AREA; rightX = 1.0; } else { Q_ASSERT(m_currentPointIndex > 0 && m_currentPointIndex < m_curve.points().count() - 1); // the 1E-4 addition so we can grab the dot later. leftX = m_curve.points().at(m_currentPointIndex - 1).x() + POINT_AREA; rightX = m_curve.points().at(m_currentPointIndex + 1).x() - POINT_AREA; } x = bounds(x, leftX, rightX); y = bounds(y, 0., 1.); QPointF point(x, y); m_curve.setPoint(m_currentPointIndex, point); if (removePoint && m_curve.points().count() > 2) { m_draggedAwayPoint = m_curve.points().at(m_currentPointIndex); m_draggedAwayPointIndex = m_currentPointIndex; m_curve.removePoint(m_currentPointIndex); m_currentPointIndex = bounds(m_currentPointIndex, 0, m_curve.points().count() - 1); } update(); emit currentPoint(point, isCurrentPointExtremal()); if (KdenliveSettings::dragvalue_directupdate()) { emit modified(); } } } double KisCurveWidget::io2sp(int x) const { int rangeLen = m_inOutMax - m_inOutMin; return double(x - m_inOutMin) / rangeLen; } int KisCurveWidget::sp2io(double x) const { int rangeLen = m_inOutMax - m_inOutMin; return int(x * rangeLen + 0.5) + m_inOutMin; } bool KisCurveWidget::jumpOverExistingPoints(QPointF &pt, int skipIndex) { for (const QPointF &it : m_curve.points()) { if (m_curve.points().indexOf(it) == skipIndex) { continue; } if (fabs(it.x() - pt.x()) < POINT_AREA) pt.rx() = pt.x() >= it.x() ? it.x() + POINT_AREA : it.x() - POINT_AREA; } return (pt.x() >= 0 && pt.x() <= 1.); } int KisCurveWidget::nearestPointInRange(QPointF pt, int wWidth, int wHeight) const { double nearestDistanceSquared = 1000; int nearestIndex = -1; int i = 0; for (const QPointF &point : m_curve.points()) { double distanceSquared = (pt.x() - point.x()) * (pt.x() - point.x()) + (pt.y() - point.y()) * (pt.y() - point.y()); if (distanceSquared < nearestDistanceSquared) { nearestIndex = i; nearestDistanceSquared = distanceSquared; } ++i; } if (nearestIndex >= 0) { double dx = (pt.x() - m_curve.points().at(nearestIndex).x()) * wWidth; double dy = (pt.y() - m_curve.points().at(nearestIndex).y()) * wHeight; if (dx * dx + dy * dy <= m_grabRadius * m_grabRadius) { return nearestIndex; } } return -1; } // void KisCurveWidget::syncIOControls() // { // if (!m_intIn || !m_intOut) { // return; // } // bool somethingSelected = (m_currentPointIndex >= 0); // m_intIn->setEnabled(somethingSelected); // m_intOut->setEnabled(somethingSelected); // if (m_currentPointIndex >= 0) { // m_intIn->blockSignals(true); // m_intOut->blockSignals(true); // m_intIn->setValue(sp2io(m_curve.points().at(m_currentPointIndex).x())); // m_intOut->setValue(sp2io(m_curve.points().at(m_currentPointIndex).y())); // m_intIn->blockSignals(false); // m_intOut->blockSignals(false); // } else { // /*FIXME: Ideally, these controls should hide away now */ // } // } void KisCurveWidget::setCurve(KisCubicCurve &&curve) { - m_curve = std::move(curve); + m_curve = curve; } QList KisCurveWidget::getPoints() const { return m_curve.points(); } diff --git a/src/assets/view/widgets/keyframeimport.cpp b/src/assets/view/widgets/keyframeimport.cpp index 6e4a28341..fa828e113 100644 --- a/src/assets/view/widgets/keyframeimport.cpp +++ b/src/assets/view/widgets/keyframeimport.cpp @@ -1,596 +1,596 @@ /*************************************************************************** * 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 "klocalizedstring.h" #include #include #include #include #include #include #include #include #include #include #include - -#include "klocalizedstring.h" +#include #include "assets/keyframes/view/keyframeview.hpp" #include "core.h" #include "doc/kdenlivedoc.h" #include "keyframeimport.h" #include "profiles/profilemodel.hpp" #include "widgets/positionwidget.h" #include "mlt++/MltAnimation.h" #include "mlt++/MltProperties.h" KeyframeImport::KeyframeImport(int in, int out, const QString &animData, std::shared_ptr model, QList indexes, QWidget *parent) : QDialog(parent) - , m_model(model) + , m_model(std::move(model)) , m_supportsAnim(false) { auto *lay = new QVBoxLayout(this); auto *l1 = new QHBoxLayout; QLabel *lab = new QLabel(i18n("Data to import: "), this); l1->addWidget(lab); m_dataCombo = new QComboBox(this); l1->addWidget(m_dataCombo); l1->addStretch(10); lay->addLayout(l1); // Set up data auto json = QJsonDocument::fromJson(animData.toUtf8()); if (!json.isArray()) { qDebug() << "Error : Json file should be an array"; return; } auto list = json.array(); int ix = 0; for (const auto &entry : list) { if (!entry.isObject()) { qDebug() << "Warning : Skipping invalid marker data"; continue; } auto entryObj = entry.toObject(); if (!entryObj.contains(QLatin1String("name"))) { qDebug() << "Warning : Skipping invalid marker data (does not contain name)"; continue; } QString name = entryObj[QLatin1String("name")].toString(); QString value = entryObj[QLatin1String("value")].toString(); int type = entryObj[QLatin1String("type")].toInt(0); double min = entryObj[QLatin1String("min")].toDouble(0); double max = entryObj[QLatin1String("max")].toDouble(0); m_dataCombo->insertItem(ix, name); m_dataCombo->setItemData(ix, value, Qt::UserRole); m_dataCombo->setItemData(ix, type, Qt::UserRole + 1); m_dataCombo->setItemData(ix, min, Qt::UserRole + 2); m_dataCombo->setItemData(ix, max, Qt::UserRole + 3); ix++; } m_previewLabel = new QLabel(this); m_previewLabel->setMinimumSize(100, 150); m_previewLabel->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); m_previewLabel->setScaledContents(true); lay->addWidget(m_previewLabel); // Zone in / out m_inPoint = new PositionWidget(i18n("In"), in, in, out, pCore->currentDoc()->timecode(), QString(), this); connect(m_inPoint, &PositionWidget::valueChanged, this, &KeyframeImport::updateDisplay); lay->addWidget(m_inPoint); m_outPoint = new PositionWidget(i18n("Out"), out, in, out, pCore->currentDoc()->timecode(), QString(), this); connect(m_outPoint, &PositionWidget::valueChanged, this, &KeyframeImport::updateDisplay); lay->addWidget(m_outPoint); // Check what kind of parameters are in our target for (const QPersistentModelIndex &idx : indexes) { ParamType type = m_model->data(idx, AssetParameterModel::TypeRole).value(); if (type == ParamType::KeyframeParam) { m_simpleTargets.insert(m_model->data(idx, Qt::DisplayRole).toString(), m_model->data(idx, AssetParameterModel::NameRole).toString()); } else if (type == ParamType::AnimatedRect) { m_geometryTargets.insert(m_model->data(idx, Qt::DisplayRole).toString(), m_model->data(idx, AssetParameterModel::NameRole).toString()); } } l1 = new QHBoxLayout; m_targetCombo = new QComboBox(this); m_sourceCombo = new QComboBox(this); ix = 0; /*if (!m_geometryTargets.isEmpty()) { m_sourceCombo->insertItem(ix, i18n("Geometry")); m_sourceCombo->setItemData(ix, QString::number(10), Qt::UserRole); ix++; m_sourceCombo->insertItem(ix, i18n("Position")); m_sourceCombo->setItemData(ix, QString::number(11), Qt::UserRole); ix++; } if (!m_simpleTargets.isEmpty()) { m_sourceCombo->insertItem(ix, i18n("X")); m_sourceCombo->setItemData(ix, QString::number(0), Qt::UserRole); ix++; m_sourceCombo->insertItem(ix, i18n("Y")); m_sourceCombo->setItemData(ix, QString::number(1), Qt::UserRole); ix++; m_sourceCombo->insertItem(ix, i18n("Width")); m_sourceCombo->setItemData(ix, QString::number(2), Qt::UserRole); ix++; m_sourceCombo->insertItem(ix, i18n("Height")); m_sourceCombo->setItemData(ix, QString::number(3), Qt::UserRole); ix++; }*/ connect(m_sourceCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(updateRange())); m_alignCombo = new QComboBox(this); m_alignCombo->addItems(QStringList() << i18n("Align top left") << i18n("Align center") << i18n("Align bottom right")); lab = new QLabel(i18n("Map "), this); QLabel *lab2 = new QLabel(i18n(" to "), this); l1->addWidget(lab); l1->addWidget(m_sourceCombo); l1->addWidget(lab2); l1->addWidget(m_targetCombo); l1->addWidget(m_alignCombo); l1->addStretch(10); ix = 0; QMap::const_iterator j = m_geometryTargets.constBegin(); while (j != m_geometryTargets.constEnd()) { m_targetCombo->insertItem(ix, j.key()); m_targetCombo->setItemData(ix, j.value(), Qt::UserRole); ++j; ix++; } ix = 0; j = m_simpleTargets.constBegin(); while (j != m_simpleTargets.constEnd()) { m_targetCombo->insertItem(ix, j.key()); m_targetCombo->setItemData(ix, j.value(), Qt::UserRole); ++j; ix++; } if (m_simpleTargets.count() + m_geometryTargets.count() > 1) { // Target contains several animatable parameters, propose choice } lay->addLayout(l1); // Output offset m_offsetPoint = new PositionWidget(i18n("Offset"), 0, 0, out, pCore->currentDoc()->timecode(), "", this); lay->addWidget(m_offsetPoint); // Source range m_sourceRangeLabel = new QLabel(i18n("Source range %1 to %2", 0, 100), this); lay->addWidget(m_sourceRangeLabel); // update range info connect(m_targetCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(updateDestinationRange())); // Destination range l1 = new QHBoxLayout; lab = new QLabel(i18n("Destination range"), this); l1->addWidget(lab); l1->addWidget(&m_destMin); l1->addWidget(&m_destMax); lay->addLayout(l1); l1 = new QHBoxLayout; m_limitRange = new QCheckBox(i18n("Actual range only"), this); connect(m_limitRange, &QAbstractButton::toggled, this, &KeyframeImport::updateRange); connect(m_limitRange, &QAbstractButton::toggled, this, &KeyframeImport::updateDisplay); l1->addWidget(m_limitRange); l1->addStretch(10); lay->addLayout(l1); l1 = new QHBoxLayout; m_limitKeyframes = new QCheckBox(i18n("Limit keyframe number"), this); m_limitKeyframes->setChecked(true); m_limitNumber = new QSpinBox(this); m_limitNumber->setMinimum(1); m_limitNumber->setValue(20); l1->addWidget(m_limitKeyframes); l1->addWidget(m_limitNumber); l1->addStretch(10); lay->addLayout(l1); connect(m_limitKeyframes, &QCheckBox::toggled, m_limitNumber, &QSpinBox::setEnabled); connect(m_limitKeyframes, &QAbstractButton::toggled, this, &KeyframeImport::updateDisplay); connect(m_limitNumber, SIGNAL(valueChanged(int)), this, SLOT(updateDisplay())); connect(m_dataCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(updateDataDisplay())); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); lay->addWidget(buttonBox); updateDestinationRange(); updateDataDisplay(); } KeyframeImport::~KeyframeImport() {} void KeyframeImport::resizeEvent(QResizeEvent *ev) { QWidget::resizeEvent(ev); updateDisplay(); } void KeyframeImport::updateDataDisplay() { QString comboData = m_dataCombo->currentData().toString(); ParamType type = m_dataCombo->currentData(Qt::UserRole + 1).value(); m_maximas = KeyframeModel::getRanges(comboData, m_model); m_sourceCombo->clear(); if (type == ParamType::KeyframeParam) { // 1 dimensional param. m_sourceCombo->addItem(m_dataCombo->currentText()); updateRange(); return; } qDebug() << "DATA: " << comboData << "\nRESULT: " << m_maximas; double wDist = m_maximas.at(2).y() - m_maximas.at(2).x(); double hDist = m_maximas.at(3).y() - m_maximas.at(3).x(); m_sourceCombo->addItem(i18n("Geometry"), 10); m_sourceCombo->addItem(i18n("Position"), 11); m_sourceCombo->addItem(i18n("X"), 0); m_sourceCombo->addItem(i18n("Y"), 1); if (wDist > 0) { m_sourceCombo->addItem(i18n("Width"), 2); } if (hDist > 0) { m_sourceCombo->addItem(i18n("Height"), 3); } updateRange(); if (!m_inPoint->isValid()) { m_inPoint->blockSignals(true); m_outPoint->blockSignals(true); // m_inPoint->setRange(0, m_keyframeView->duration); m_inPoint->setPosition(0); // m_outPoint->setPosition(m_keyframeView->duration); m_inPoint->blockSignals(false); m_outPoint->blockSignals(false); } } void KeyframeImport::updateRange() { int pos = m_sourceCombo->currentData().toInt(); m_alignCombo->setEnabled(pos == 11); QString rangeText; if (m_limitRange->isChecked()) { switch (pos) { case 0: rangeText = i18n("Source range %1 to %2", m_maximas.at(0).x(), m_maximas.at(0).y()); break; case 1: rangeText = i18n("Source range %1 to %2", m_maximas.at(1).x(), m_maximas.at(1).y()); break; case 2: rangeText = i18n("Source range %1 to %2", m_maximas.at(2).x(), m_maximas.at(2).y()); break; case 3: rangeText = i18n("Source range %1 to %2", m_maximas.at(3).x(), m_maximas.at(3).y()); break; default: rangeText = i18n("Source range: (%1-%2), (%3-%4)", m_maximas.at(0).x(), m_maximas.at(0).y(), m_maximas.at(1).x(), m_maximas.at(1).y()); break; } } else { int profileWidth = pCore->getCurrentProfile()->width(); int profileHeight = pCore->getCurrentProfile()->height(); switch (pos) { case 0: rangeText = i18n("Source range %1 to %2", qMin(0, m_maximas.at(0).x()), qMax(profileWidth, m_maximas.at(0).y())); break; case 1: rangeText = i18n("Source range %1 to %2", qMin(0, m_maximas.at(1).x()), qMax(profileHeight, m_maximas.at(1).y())); break; case 2: rangeText = i18n("Source range %1 to %2", qMin(0, m_maximas.at(2).x()), qMax(profileWidth, m_maximas.at(2).y())); break; case 3: rangeText = i18n("Source range %1 to %2", qMin(0, m_maximas.at(3).x()), qMax(profileHeight, m_maximas.at(3).y())); break; default: rangeText = i18n("Source range: (%1-%2), (%3-%4)", qMin(0, m_maximas.at(0).x()), qMax(profileWidth, m_maximas.at(0).y()), qMin(0, m_maximas.at(1).x()), qMax(profileHeight, m_maximas.at(1).y())); break; } } m_sourceRangeLabel->setText(rangeText); updateDisplay(); } void KeyframeImport::updateDestinationRange() { if (m_simpleTargets.contains(m_targetCombo->currentText())) { // 1 dimension target m_destMin.setEnabled(true); m_destMax.setEnabled(true); m_limitRange->setEnabled(true); QString tag = m_targetCombo->currentData().toString(); /* QDomNodeList params = m_xml.elementsByTagName(QStringLiteral("parameter")); for (int i = 0; i < params.count(); i++) { QDomElement e = params.at(i).toElement(); if (e.attribute(QStringLiteral("name")) == tag) { double factor = e.attribute(QStringLiteral("factor")).toDouble(); if (factor == 0) { factor = 1; } double min = e.attribute(QStringLiteral("min")).toDouble() / factor; double max = e.attribute(QStringLiteral("max")).toDouble() / factor; m_destMin.setRange(min, max); m_destMax.setRange(min, max); m_destMin.setValue(min); m_destMax.setValue(max); break; } }*/ } else { // TODO int profileWidth = pCore->getCurrentProfile()->width(); m_destMin.setRange(0, profileWidth); m_destMax.setRange(0, profileWidth); m_destMin.setEnabled(false); m_destMax.setEnabled(false); m_limitRange->setEnabled(false); } } void KeyframeImport::updateDisplay() { QPixmap pix(m_previewLabel->width(), m_previewLabel->height()); pix.fill(Qt::transparent); QList maximas; int selectedtarget = m_sourceCombo->currentData().toInt(); int profileWidth = pCore->getCurrentProfile()->width(); int profileHeight = pCore->getCurrentProfile()->height(); if (!m_maximas.isEmpty()) { if (m_maximas.at(0).x() == m_maximas.at(0).y() || (selectedtarget < 10 && selectedtarget != 0)) { maximas << QPoint(); } else { if (m_limitRange->isChecked()) { maximas << m_maximas.at(0); } else { QPoint p1(qMin(0, m_maximas.at(0).x()), qMax(profileWidth, m_maximas.at(0).y())); maximas << p1; } } } if (m_maximas.count() > 1) { if (m_maximas.at(1).x() == m_maximas.at(1).y() || (selectedtarget < 10 && selectedtarget != 1)) { maximas << QPoint(); } else { if (m_limitRange->isChecked()) { maximas << m_maximas.at(1); } else { QPoint p2(qMin(0, m_maximas.at(1).x()), qMax(profileHeight, m_maximas.at(1).y())); maximas << p2; } } } if (m_maximas.count() > 2) { if (m_maximas.at(2).x() == m_maximas.at(2).y() || (selectedtarget < 10 && selectedtarget != 2)) { maximas << QPoint(); } else { if (m_limitRange->isChecked()) { maximas << m_maximas.at(2); } else { QPoint p3(qMin(0, m_maximas.at(2).x()), qMax(profileWidth, m_maximas.at(2).y())); maximas << p3; } } } if (m_maximas.count() > 3) { if (m_maximas.at(3).x() == m_maximas.at(3).y() || (selectedtarget < 10 && selectedtarget != 3)) { maximas << QPoint(); } else { if (m_limitRange->isChecked()) { maximas << m_maximas.at(3); } else { QPoint p4(qMin(0, m_maximas.at(3).x()), qMax(profileHeight, m_maximas.at(3).y())); maximas << p4; } } } drawKeyFrameChannels(pix, m_inPoint->getPosition(), m_outPoint->getPosition(), m_limitKeyframes->isChecked() ? m_limitNumber->value() : 0, palette().text().color()); m_previewLabel->setPixmap(pix); } QString KeyframeImport::selectedData() const { // return serialized keyframes if (m_simpleTargets.contains(m_targetCombo->currentText())) { // Exporting a 1 dimension animation int ix = m_sourceCombo->currentData().toInt(); QPoint maximas; if (m_limitRange->isChecked()) { maximas = m_maximas.at(ix); } else if (ix == 0 || ix == 2) { // Width maximas maximas = QPoint(qMin(m_maximas.at(ix).x(), 0), qMax(m_maximas.at(ix).y(), pCore->getCurrentProfile()->width())); } else { // Height maximas maximas = QPoint(qMin(m_maximas.at(ix).x(), 0), qMax(m_maximas.at(ix).y(), pCore->getCurrentProfile()->height())); } std::shared_ptr animData = KeyframeModel::getAnimation(m_dataCombo->currentData().toString()); std::shared_ptr anim(new Mlt::Animation(animData->get_animation("key"))); animData->anim_get_double("key", m_inPoint->getPosition(), m_outPoint->getPosition()); return anim->serialize_cut(); // m_keyframeView->getSingleAnimation(ix, m_inPoint->getPosition(), m_outPoint->getPosition(), m_offsetPoint->getPosition(), // m_limitKeyframes->isChecked() ? m_limitNumber->value() : 0, maximas, m_destMin.value(), m_destMax.value()); } // Geometry target QPoint rectOffset; int ix = m_alignCombo->currentIndex(); switch (ix) { case 1: rectOffset = QPoint(pCore->getCurrentProfile()->width() / 2, pCore->getCurrentProfile()->height() / 2); break; case 2: rectOffset = QPoint(pCore->getCurrentProfile()->width(), pCore->getCurrentProfile()->height()); break; default: break; } return QString(); // int pos = m_sourceCombo->currentData().toInt(); // m_keyframeView->getOffsetAnimation(m_inPoint->getPosition(), m_outPoint->getPosition(), m_offsetPoint->getPosition(), m_limitKeyframes->isChecked() ? // m_limitNumber->value() : 0, m_supportsAnim, pos == 11, rectOffset); } QString KeyframeImport::selectedTarget() const { return m_targetCombo->currentData().toString(); } void KeyframeImport::drawKeyFrameChannels(QPixmap &pix, int in, int out, int limitKeyframes, const QColor &textColor) { std::shared_ptr animData = KeyframeModel::getAnimation(m_dataCombo->currentData().toString()); QRect br(0, 0, pix.width(), pix.height()); double frameFactor = (double)(out - in) / br.width(); int offset = 1; if (limitKeyframes > 0) { offset = (out - in) / limitKeyframes / frameFactor; } double min = m_dataCombo->currentData(Qt::UserRole + 2).toDouble(); double max = m_dataCombo->currentData(Qt::UserRole + 3).toDouble(); double xDist; if (max > min) { xDist = max - min; } else { xDist = m_maximas.at(0).y() - m_maximas.at(0).x(); } double yDist = m_maximas.at(1).y() - m_maximas.at(1).x(); double wDist = m_maximas.at(2).y() - m_maximas.at(2).x(); double hDist = m_maximas.at(3).y() - m_maximas.at(3).x(); double xOffset = m_maximas.at(0).x(); double yOffset = m_maximas.at(1).x(); double wOffset = m_maximas.at(2).x(); double hOffset = m_maximas.at(3).x(); QColor cX(255, 0, 0, 100); QColor cY(0, 255, 0, 100); QColor cW(0, 0, 255, 100); QColor cH(255, 255, 0, 100); // Draw curves labels QPainter painter; painter.begin(&pix); QRectF txtRect = painter.boundingRect(br, QStringLiteral("t")); txtRect.setX(2); txtRect.setWidth(br.width() - 4); txtRect.moveTop(br.height() - txtRect.height()); QRectF drawnText; int maxHeight = br.height() - txtRect.height() - 2; painter.setPen(textColor); int rectSize = txtRect.height() / 2; if (xDist > 0) { painter.fillRect(txtRect.x(), txtRect.top() + rectSize / 2, rectSize, rectSize, cX); txtRect.setX(txtRect.x() + rectSize * 2); painter.drawText(txtRect, 0, i18nc("X as in x coordinate", "X") + QStringLiteral(" (%1-%2)").arg(m_maximas.at(0).x()).arg(m_maximas.at(0).y()), &drawnText); } if (yDist > 0) { if (drawnText.isValid()) { txtRect.setX(drawnText.right() + rectSize); } painter.fillRect(txtRect.x(), txtRect.top() + rectSize / 2, rectSize, rectSize, cY); txtRect.setX(txtRect.x() + rectSize * 2); painter.drawText(txtRect, 0, i18nc("Y as in y coordinate", "Y") + QStringLiteral(" (%1-%2)").arg(m_maximas.at(1).x()).arg(m_maximas.at(1).y()), &drawnText); } if (wDist > 0) { if (drawnText.isValid()) { txtRect.setX(drawnText.right() + rectSize); } painter.fillRect(txtRect.x(), txtRect.top() + rectSize / 2, rectSize, rectSize, cW); txtRect.setX(txtRect.x() + rectSize * 2); painter.drawText(txtRect, 0, i18n("Width") + QStringLiteral(" (%1-%2)").arg(m_maximas.at(2).x()).arg(m_maximas.at(2).y()), &drawnText); } if (hDist > 0) { if (drawnText.isValid()) { txtRect.setX(drawnText.right() + rectSize); } painter.fillRect(txtRect.x(), txtRect.top() + rectSize / 2, rectSize, rectSize, cH); txtRect.setX(txtRect.x() + rectSize * 2); painter.drawText(txtRect, 0, i18n("Height") + QStringLiteral(" (%1-%2)").arg(m_maximas.at(3).x()).arg(m_maximas.at(3).y()), &drawnText); } // Draw curves for (int i = 0; i < br.width(); i++) { mlt_rect rect = animData->anim_get_rect("key", (int)(i * frameFactor) + in); qDebug() << "// DRAWINC CURVE IWDTH: " << rect.w << ", WDIST: " << wDist; if (xDist > 0) { painter.setPen(cX); int val = (rect.x - xOffset) * maxHeight / xDist; painter.drawLine(i, maxHeight - val, i, maxHeight); } if (yDist > 0) { painter.setPen(cY); int val = (rect.y - yOffset) * maxHeight / yDist; painter.drawLine(i, maxHeight - val, i, maxHeight); } if (wDist > 0) { painter.setPen(cW); int val = (rect.w - wOffset) * maxHeight / wDist; qDebug() << "// OFFSET: " << wOffset << ", maxH: " << maxHeight << ", wDIst:" << wDist << " = " << val; painter.drawLine(i, maxHeight - val, i, maxHeight); } if (hDist > 0) { painter.setPen(cH); int val = (rect.h - hOffset) * maxHeight / hDist; painter.drawLine(i, maxHeight - val, i, maxHeight); } } if (offset > 1) { // Overlay limited keyframes curve cX.setAlpha(255); cY.setAlpha(255); cW.setAlpha(255); cH.setAlpha(255); mlt_rect rect1 = animData->anim_get_rect("key", in); int prevPos = 0; for (int i = offset; i < br.width(); i += offset) { mlt_rect rect2 = animData->anim_get_rect("key", (int)(i * frameFactor) + in); if (xDist > 0) { painter.setPen(cX); int val1 = (rect1.x - xOffset) * maxHeight / xDist; int val2 = (rect2.x - xOffset) * maxHeight / xDist; painter.drawLine(prevPos, maxHeight - val1, i, maxHeight - val2); } if (yDist > 0) { painter.setPen(cY); int val1 = (rect1.y - yOffset) * maxHeight / yDist; int val2 = (rect2.y - yOffset) * maxHeight / yDist; painter.drawLine(prevPos, maxHeight - val1, i, maxHeight - val2); } if (wDist > 0) { painter.setPen(cW); int val1 = (rect1.w - wOffset) * maxHeight / wDist; int val2 = (rect2.w - wOffset) * maxHeight / wDist; painter.drawLine(prevPos, maxHeight - val1, i, maxHeight - val2); } if (hDist > 0) { painter.setPen(cH); int val1 = (rect1.h - hOffset) * maxHeight / hDist; int val2 = (rect2.h - hOffset) * maxHeight / hDist; painter.drawLine(prevPos, maxHeight - val1, i, maxHeight - val2); } rect1 = rect2; prevPos = i; } } } diff --git a/src/assets/view/widgets/keyframewidget.cpp b/src/assets/view/widgets/keyframewidget.cpp index e81ea4c94..342915db9 100644 --- a/src/assets/view/widgets/keyframewidget.cpp +++ b/src/assets/view/widgets/keyframewidget.cpp @@ -1,505 +1,506 @@ /*************************************************************************** * Copyright (C) 2011 by Till Theato (root@ttill.de) * * Copyright (C) 2017 by Nicolas Carion * * This file is part of Kdenlive (www.kdenlive.org). * * * * Kdenlive 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. * * * * Kdenlive 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 Kdenlive. If not, see . * ***************************************************************************/ #include "keyframewidget.hpp" #include "assets/keyframes/model/corners/cornershelper.hpp" #include "assets/keyframes/model/keyframemodellist.hpp" #include "assets/keyframes/model/rotoscoping/rotohelper.hpp" #include "assets/keyframes/view/keyframeview.hpp" #include "assets/model/assetcommand.hpp" #include "assets/model/assetparametermodel.hpp" #include "assets/view/widgets/keyframeimport.h" #include "core.h" #include "kdenlivesettings.h" #include "monitor/monitor.h" #include "timecode.h" #include "timecodedisplay.h" #include "widgets/doublewidget.h" #include "widgets/geometrywidget.h" #include #include #include #include #include #include #include #include #include #include #include +#include KeyframeWidget::KeyframeWidget(std::shared_ptr model, QModelIndex index, QWidget *parent) - : AbstractParamWidget(model, index, parent) + : AbstractParamWidget(std::move(model), index, parent) , m_monitorHelper(nullptr) , m_neededScene(MonitorSceneType::MonitorSceneDefault) { setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); m_lay = new QVBoxLayout(this); m_lay->setContentsMargins(2, 2, 2, 0); m_lay->setSpacing(0); bool ok = false; int duration = m_model->data(m_index, AssetParameterModel::ParentDurationRole).toInt(&ok); Q_ASSERT(ok); m_model->prepareKeyframes(); m_keyframes = m_model->getKeyframeModel(); m_keyframeview = new KeyframeView(m_keyframes, duration, this); m_buttonAddDelete = new QToolButton(this); m_buttonAddDelete->setAutoRaise(true); m_buttonAddDelete->setIcon(QIcon::fromTheme(QStringLiteral("list-add"))); m_buttonAddDelete->setToolTip(i18n("Add keyframe")); m_buttonPrevious = new QToolButton(this); m_buttonPrevious->setAutoRaise(true); m_buttonPrevious->setIcon(QIcon::fromTheme(QStringLiteral("media-skip-backward"))); m_buttonPrevious->setToolTip(i18n("Go to previous keyframe")); m_buttonNext = new QToolButton(this); m_buttonNext->setAutoRaise(true); m_buttonNext->setIcon(QIcon::fromTheme(QStringLiteral("media-skip-forward"))); m_buttonNext->setToolTip(i18n("Go to next keyframe")); // Keyframe type widget m_selectType = new KSelectAction(QIcon::fromTheme(QStringLiteral("keyframes")), i18n("Keyframe interpolation"), this); QAction *linear = new QAction(QIcon::fromTheme(QStringLiteral("linear")), i18n("Linear"), this); linear->setData((int)mlt_keyframe_linear); linear->setCheckable(true); m_selectType->addAction(linear); QAction *discrete = new QAction(QIcon::fromTheme(QStringLiteral("discrete")), i18n("Discrete"), this); discrete->setData((int)mlt_keyframe_discrete); discrete->setCheckable(true); m_selectType->addAction(discrete); QAction *curve = new QAction(QIcon::fromTheme(QStringLiteral("smooth")), i18n("Smooth"), this); curve->setData((int)mlt_keyframe_smooth); curve->setCheckable(true); m_selectType->addAction(curve); m_selectType->setCurrentAction(linear); connect(m_selectType, static_cast(&KSelectAction::triggered), this, &KeyframeWidget::slotEditKeyframeType); m_selectType->setToolBarMode(KSelectAction::ComboBoxMode); m_toolbar = new QToolBar(this); Monitor *monitor = pCore->getMonitor(m_model->monitorId); m_time = new TimecodeDisplay(monitor->timecode(), this); m_time->setRange(0, duration - 1); m_toolbar->addWidget(m_buttonPrevious); m_toolbar->addWidget(m_buttonAddDelete); m_toolbar->addWidget(m_buttonNext); m_toolbar->addAction(m_selectType); // copy/paste keyframes from clipboard QAction *copy = new QAction(i18n("Copy keyframes to clipboard"), this); connect(copy, &QAction::triggered, this, &KeyframeWidget::slotCopyKeyframes); QAction *paste = new QAction(i18n("Import keyframes from clipboard"), this); connect(paste, &QAction::triggered, this, &KeyframeWidget::slotImportKeyframes); // Remove keyframes QAction *removeNext = new QAction(i18n("Remove all keyframes after cursor"), this); connect(removeNext, &QAction::triggered, this, &KeyframeWidget::slotRemoveNextKeyframes); // Default kf interpolation KSelectAction *kfType = new KSelectAction(i18n("Default keyframe type"), this); QAction *discrete2 = new QAction(QIcon::fromTheme(QStringLiteral("discrete")), i18n("Discrete"), this); discrete2->setData((int)mlt_keyframe_discrete); discrete2->setCheckable(true); kfType->addAction(discrete2); QAction *linear2 = new QAction(QIcon::fromTheme(QStringLiteral("linear")), i18n("Linear"), this); linear2->setData((int)mlt_keyframe_linear); linear2->setCheckable(true); kfType->addAction(linear2); QAction *curve2 = new QAction(QIcon::fromTheme(QStringLiteral("smooth")), i18n("Smooth"), this); curve2->setData((int)mlt_keyframe_smooth); curve2->setCheckable(true); kfType->addAction(curve2); switch (KdenliveSettings::defaultkeyframeinterp()) { case mlt_keyframe_discrete: kfType->setCurrentAction(discrete2); break; case mlt_keyframe_smooth: kfType->setCurrentAction(curve2); break; default: kfType->setCurrentAction(linear2); break; } connect(kfType, static_cast(&KSelectAction::triggered), [&](QAction *ac) { KdenliveSettings::setDefaultkeyframeinterp(ac->data().toInt()); }); auto *container = new QMenu(this); container->addAction(copy); container->addAction(paste); container->addSeparator(); container->addAction(kfType); container->addAction(removeNext); // Menu toolbutton auto *menuButton = new QToolButton(this); menuButton->setIcon(QIcon::fromTheme(QStringLiteral("kdenlive-menu"))); menuButton->setToolTip(i18n("Options")); menuButton->setMenu(container); menuButton->setPopupMode(QToolButton::InstantPopup); m_toolbar->addWidget(menuButton); m_toolbar->addWidget(m_time); m_lay->addWidget(m_keyframeview); m_lay->addWidget(m_toolbar); monitorSeek(monitor->position()); connect(m_time, &TimecodeDisplay::timeCodeEditingFinished, [&]() { slotSetPosition(-1, true); }); connect(m_keyframeview, &KeyframeView::seekToPos, [&](int p) { slotSetPosition(p, true); }); connect(m_keyframeview, &KeyframeView::atKeyframe, this, &KeyframeWidget::slotAtKeyframe); connect(m_keyframeview, &KeyframeView::modified, this, &KeyframeWidget::slotRefreshParams); connect(m_buttonAddDelete, &QAbstractButton::pressed, m_keyframeview, &KeyframeView::slotAddRemove); connect(m_buttonPrevious, &QAbstractButton::pressed, m_keyframeview, &KeyframeView::slotGoToPrev); connect(m_buttonNext, &QAbstractButton::pressed, m_keyframeview, &KeyframeView::slotGoToNext); addParameter(index); connect(monitor, &Monitor::seekToNextKeyframe, m_keyframeview, &KeyframeView::slotGoToNext, Qt::UniqueConnection); connect(monitor, &Monitor::seekToPreviousKeyframe, m_keyframeview, &KeyframeView::slotGoToPrev, Qt::UniqueConnection); connect(monitor, &Monitor::addRemoveKeyframe, m_keyframeview, &KeyframeView::slotAddRemove, Qt::UniqueConnection); } KeyframeWidget::~KeyframeWidget() { delete m_keyframeview; delete m_buttonAddDelete; delete m_buttonPrevious; delete m_buttonNext; delete m_time; } void KeyframeWidget::monitorSeek(int pos) { int in = pCore->getItemPosition(m_model->getOwnerId()); int out = in + pCore->getItemDuration(m_model->getOwnerId()); bool isInRange = pos > in && pos < out; m_buttonAddDelete->setEnabled(isInRange); connectMonitor(isInRange); int framePos = qBound(in, pos, out) - in; if (isInRange && framePos != m_time->getValue()) { slotSetPosition(framePos, false); } } void KeyframeWidget::slotEditKeyframeType(QAction *action) { int type = action->data().toInt(); m_keyframeview->slotEditType(type, m_index); } void KeyframeWidget::slotRefreshParams() { int pos = getPosition(); KeyframeType keyType = m_keyframes->keyframeType(GenTime(pos, pCore->getCurrentFps())); int i = 0; while (auto ac = m_selectType->action(i)) { if (ac->data().toInt() == (int)keyType) { m_selectType->setCurrentItem(i); break; } i++; } for (const auto &w : m_parameters) { ParamType type = m_model->data(w.first, AssetParameterModel::TypeRole).value(); if (type == ParamType::KeyframeParam) { ((DoubleWidget *)w.second)->setValue(m_keyframes->getInterpolatedValue(pos, w.first).toDouble()); } else if (type == ParamType::AnimatedRect) { const QString val = m_keyframes->getInterpolatedValue(pos, w.first).toString(); const QStringList vals = val.split(QLatin1Char(' ')); QRect rect; double opacity = -1; if (vals.count() >= 4) { rect = QRect(vals.at(0).toInt(), vals.at(1).toInt(), vals.at(2).toInt(), vals.at(3).toInt()); if (vals.count() > 4) { QLocale locale; opacity = locale.toDouble(vals.at(4)); } } ((GeometryWidget *)w.second)->setValue(rect, opacity); } } if (m_monitorHelper) { m_monitorHelper->refreshParams(pos); return; } } void KeyframeWidget::slotSetPosition(int pos, bool update) { if (pos < 0) { pos = m_time->getValue(); m_keyframeview->slotSetPosition(pos, true); } else { m_time->setValue(pos); m_keyframeview->slotSetPosition(pos, true); } m_buttonAddDelete->setEnabled(pos > 0); slotRefreshParams(); if (update) { emit seekToPos(pos); } } int KeyframeWidget::getPosition() const { return m_time->getValue() + pCore->getItemIn(m_model->getOwnerId()); } void KeyframeWidget::addKeyframe(int pos) { blockSignals(true); m_keyframeview->slotAddKeyframe(pos); blockSignals(false); setEnabled(true); } void KeyframeWidget::updateTimecodeFormat() { m_time->slotUpdateTimeCodeFormat(); } void KeyframeWidget::slotAtKeyframe(bool atKeyframe, bool singleKeyframe) { if (atKeyframe) { m_buttonAddDelete->setIcon(QIcon::fromTheme(QStringLiteral("list-remove"))); m_buttonAddDelete->setToolTip(i18n("Delete keyframe")); } else { m_buttonAddDelete->setIcon(QIcon::fromTheme(QStringLiteral("list-add"))); m_buttonAddDelete->setToolTip(i18n("Add keyframe")); } pCore->getMonitor(m_model->monitorId)->setEffectKeyframe(atKeyframe || singleKeyframe); m_selectType->setEnabled(atKeyframe || singleKeyframe); for (const auto &w : m_parameters) { w.second->setEnabled(atKeyframe || singleKeyframe); } } void KeyframeWidget::slotRefresh() { // update duration bool ok = false; int duration = m_model->data(m_index, AssetParameterModel::ParentDurationRole).toInt(&ok); Q_ASSERT(ok); // m_model->dataChanged(QModelIndex(), QModelIndex()); //->getKeyframeModel()->getKeyModel(m_index)->dataChanged(QModelIndex(), QModelIndex()); m_keyframeview->setDuration(duration); m_time->setRange(0, duration - 1); slotRefreshParams(); } void KeyframeWidget::resetKeyframes() { // update duration bool ok = false; int duration = m_model->data(m_index, AssetParameterModel::ParentDurationRole).toInt(&ok); Q_ASSERT(ok); // reset keyframes m_keyframes->refresh(); // m_model->dataChanged(QModelIndex(), QModelIndex()); m_keyframeview->setDuration(duration); m_time->setRange(0, duration - 1); slotRefreshParams(); } void KeyframeWidget::addParameter(const QPersistentModelIndex &index) { QLocale locale; locale.setNumberOptions(QLocale::OmitGroupSeparator); // Retrieve parameters from the model QString name = m_model->data(index, Qt::DisplayRole).toString(); QString comment = m_model->data(index, AssetParameterModel::CommentRole).toString(); QString suffix = m_model->data(index, AssetParameterModel::SuffixRole).toString(); ParamType type = m_model->data(index, AssetParameterModel::TypeRole).value(); // Construct object QWidget *paramWidget = nullptr; if (type == ParamType::AnimatedRect) { m_neededScene = MonitorSceneType::MonitorSceneGeometry; int inPos = m_model->data(index, AssetParameterModel::ParentInRole).toInt(); QPair range(inPos, inPos + m_model->data(index, AssetParameterModel::ParentDurationRole).toInt()); QSize frameSize = pCore->getCurrentFrameSize(); const QString value = m_keyframes->getInterpolatedValue(getPosition(), index).toString(); QRect rect; double opacity = 0; QStringList vals = value.split(QLatin1Char(' ')); if (vals.count() >= 4) { rect = QRect(vals.at(0).toInt(), vals.at(1).toInt(), vals.at(2).toInt(), vals.at(3).toInt()); if (vals.count() > 4) { opacity = locale.toDouble(vals.at(4)); } } // qtblend uses an opacity value in the (0-1) range, while older geometry effects use (0-100) bool integerOpacity = m_model->getAssetId() != QLatin1String("qtblend"); GeometryWidget *geomWidget = new GeometryWidget(pCore->getMonitor(m_model->monitorId), range, rect, opacity, frameSize, false, m_model->data(m_index, AssetParameterModel::OpacityRole).toBool(), integerOpacity, this); connect(geomWidget, &GeometryWidget::valueChanged, [this, index](const QString v) { m_keyframes->updateKeyframe(GenTime(getPosition(), pCore->getCurrentFps()), QVariant(v), index); }); paramWidget = geomWidget; } else if (type == ParamType::Roto_spline) { m_monitorHelper = new RotoHelper(pCore->getMonitor(m_model->monitorId), m_model, index, this); connect(m_monitorHelper, &KeyframeMonitorHelper::updateKeyframeData, this, &KeyframeWidget::slotUpdateKeyframesFromMonitor, Qt::UniqueConnection); m_neededScene = MonitorSceneType::MonitorSceneRoto; } else { if (m_model->getAssetId() == QLatin1String("frei0r.c0rners")) { if (m_neededScene == MonitorSceneDefault && !m_monitorHelper) { m_neededScene = MonitorSceneType::MonitorSceneCorners; m_monitorHelper = new CornersHelper(pCore->getMonitor(m_model->monitorId), m_model, index, this); connect(m_monitorHelper, &KeyframeMonitorHelper::updateKeyframeData, this, &KeyframeWidget::slotUpdateKeyframesFromMonitor, Qt::UniqueConnection); connect(this, &KeyframeWidget::addIndex, m_monitorHelper, &CornersHelper::addIndex); } else { if (type == ParamType::KeyframeParam) { int paramName = m_model->data(index, AssetParameterModel::NameRole).toInt(); if (paramName < 8) { emit addIndex(index); } } } } double value = m_keyframes->getInterpolatedValue(getPosition(), index).toDouble(); double min = locale.toDouble(m_model->data(index, AssetParameterModel::MinRole).toString()); double max = locale.toDouble(m_model->data(index, AssetParameterModel::MaxRole).toString()); double defaultValue = m_model->data(index, AssetParameterModel::DefaultRole).toDouble(); int decimals = m_model->data(index, AssetParameterModel::DecimalsRole).toInt(); double factor = locale.toDouble(m_model->data(index, AssetParameterModel::FactorRole).toString()); factor = qFuzzyIsNull(factor) ? 1 : factor; auto doubleWidget = new DoubleWidget(name, value, min, max, factor, defaultValue, comment, -1, suffix, decimals, this); connect(doubleWidget, &DoubleWidget::valueChanged, [this, index](double v) { m_keyframes->updateKeyframe(GenTime(getPosition(), pCore->getCurrentFps()), QVariant(v), index); }); paramWidget = doubleWidget; } if (paramWidget) { m_parameters[index] = paramWidget; m_lay->addWidget(paramWidget); } } void KeyframeWidget::slotInitMonitor(bool active) { if (m_keyframeview) { m_keyframeview->initKeyframePos(); } Monitor *monitor = pCore->getMonitor(m_model->monitorId); connectMonitor(active); if (active) { connect(monitor, &Monitor::seekPosition, this, &KeyframeWidget::monitorSeek, Qt::UniqueConnection); } else { disconnect(monitor, &Monitor::seekPosition, this, &KeyframeWidget::monitorSeek); } } void KeyframeWidget::connectMonitor(bool active) { if (m_monitorHelper) { if (m_monitorHelper->connectMonitor(active)) { slotRefreshParams(); } } for (const auto &w : m_parameters) { ParamType type = m_model->data(w.first, AssetParameterModel::TypeRole).value(); if (type == ParamType::AnimatedRect) { ((GeometryWidget *)w.second)->connectMonitor(active); break; } } } -void KeyframeWidget::slotUpdateKeyframesFromMonitor(QPersistentModelIndex index, const QVariant &res) +void KeyframeWidget::slotUpdateKeyframesFromMonitor(const QPersistentModelIndex &index, const QVariant &res) { if (m_keyframes->isEmpty()) { m_keyframes->addKeyframe(GenTime(getPosition(), pCore->getCurrentFps()), KeyframeType::Linear); m_keyframes->updateKeyframe(GenTime(getPosition(), pCore->getCurrentFps()), res, index); } else if (m_keyframes->hasKeyframe(getPosition()) || m_keyframes->singleKeyframe()) { m_keyframes->updateKeyframe(GenTime(getPosition(), pCore->getCurrentFps()), res, index); } } MonitorSceneType KeyframeWidget::requiredScene() const { qDebug() << "// // // RESULTING REQUIRED SCENE: " << m_neededScene; return m_neededScene; } bool KeyframeWidget::keyframesVisible() const { return m_keyframeview->isVisible(); } void KeyframeWidget::showKeyframes(bool enable) { m_toolbar->setVisible(enable); m_keyframeview->setVisible(enable); } void KeyframeWidget::slotCopyKeyframes() { QJsonDocument effectDoc = m_model->toJson(); if (effectDoc.isEmpty()) { return; } QClipboard *clipboard = QApplication::clipboard(); clipboard->setText(QString(effectDoc.toJson())); } void KeyframeWidget::slotImportKeyframes() { QClipboard *clipboard = QApplication::clipboard(); QString values = clipboard->text(); int inPos = m_model->data(m_index, AssetParameterModel::ParentInRole).toInt(); int outPos = inPos + m_model->data(m_index, AssetParameterModel::ParentDurationRole).toInt(); QList indexes; for (const auto &w : m_parameters) { indexes << w.first; } QPointer import = new KeyframeImport(inPos, outPos, values, m_model, indexes, this); if (import->exec() != QDialog::Accepted) { delete import; return; } QString keyframeData = import->selectedData(); QString tag = import->selectedTarget(); qDebug() << "// CHECKING FOR TARGET PARAM: " << tag; // m_model->setParameter(tag, keyframeData, true); /*for (const auto &w : m_parameters) { qDebug()<<"// GOT PARAM: "<data(w.first, AssetParameterModel::NameRole).toString(); if (tag == m_model->data(w.first, AssetParameterModel::NameRole).toString()) { qDebug()<<"// PASSING DTAT: "<getKeyframeModel()->getKeyModel()->parseAnimProperty(keyframeData); m_model->getKeyframeModel()->getKeyModel()->modelChanged(); break; } }*/ AssetCommand *command = new AssetCommand(m_model, m_index, keyframeData); pCore->pushUndo(command); /*m_model->getKeyframeModel()->getKeyModel()->dataChanged(QModelIndex(), QModelIndex()); m_model->modelChanged(); qDebug()<<"//// UPDATING KEYFRAMES CORE---------"; pCore->updateItemKeyframes(m_model->getOwnerId());*/ qDebug() << "//// UPDATING KEYFRAMES CORE . .. .DONE ---------"; // emit importKeyframes(type, tag, keyframeData); delete import; } void KeyframeWidget::slotRemoveNextKeyframes() { m_keyframes->removeNextKeyframes(GenTime(m_time->getValue(), pCore->getCurrentFps())); } diff --git a/src/assets/view/widgets/keyframewidget.hpp b/src/assets/view/widgets/keyframewidget.hpp index 691c3e8df..d2880c0a8 100644 --- a/src/assets/view/widgets/keyframewidget.hpp +++ b/src/assets/view/widgets/keyframewidget.hpp @@ -1,105 +1,105 @@ /*************************************************************************** * Copyright (C) 2011 by Till Theato (root@ttill.de) * * Copyright (C) 2017 by Nicolas Carion * * This file is part of Kdenlive (www.kdenlive.org). * * * * Kdenlive 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. * * * * Kdenlive 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 Kdenlive. If not, see . * ***************************************************************************/ #ifndef KEYFRAMEWIDGET_H #define KEYFRAMEWIDGET_H #include "abstractparamwidget.hpp" #include "definitions.h" #include #include #include class AssetParameterModel; class DoubleWidget; class KeyframeView; class KeyframeModelList; class QVBoxLayout; class QToolButton; class QToolBar; class TimecodeDisplay; class KSelectAction; class KeyframeMonitorHelper; class KeyframeWidget : public AbstractParamWidget { Q_OBJECT public: explicit KeyframeWidget(std::shared_ptr model, QModelIndex index, QWidget *parent = nullptr); ~KeyframeWidget(); /* @brief Add a new parameter to be managed using the same keyframe viewer */ void addParameter(const QPersistentModelIndex &index); int getPosition() const; void addKeyframe(int pos = -1); /** @brief Returns the monitor scene required for this asset */ MonitorSceneType requiredScene() const; void updateTimecodeFormat(); /** @brief Show / hide keyframe related widgets */ void showKeyframes(bool enable); /** @brief Returns true if keyframes options are visible */ bool keyframesVisible() const; void resetKeyframes(); public slots: void slotRefresh() override; /** @brief initialize qml overlay */ void slotInitMonitor(bool active) override; public slots: void slotSetPosition(int pos = -1, bool update = true); private slots: /* brief Update the value of the widgets to reflect keyframe change */ void slotRefreshParams(); void slotAtKeyframe(bool atKeyframe, bool singleKeyframe); void monitorSeek(int pos); void slotEditKeyframeType(QAction *action); - void slotUpdateKeyframesFromMonitor(QPersistentModelIndex index, const QVariant &res); + void slotUpdateKeyframesFromMonitor(const QPersistentModelIndex &index, const QVariant &res); void slotCopyKeyframes(); void slotImportKeyframes(); void slotRemoveNextKeyframes(); private: QVBoxLayout *m_lay; QToolBar *m_toolbar; std::shared_ptr m_keyframes; KeyframeView *m_keyframeview; KeyframeMonitorHelper *m_monitorHelper; QToolButton *m_buttonAddDelete; QToolButton *m_buttonPrevious; QToolButton *m_buttonNext; KSelectAction *m_selectType; TimecodeDisplay *m_time; MonitorSceneType m_neededScene; void connectMonitor(bool active); std::unordered_map m_parameters; signals: void addIndex(QPersistentModelIndex ix); void setKeyframes(const QString &); }; #endif diff --git a/src/bin/bin.cpp b/src/bin/bin.cpp index b539ed651..c99cc9a83 100644 --- a/src/bin/bin.cpp +++ b/src/bin/bin.cpp @@ -1,3117 +1,3117 @@ /* Copyright (C) 2012 Till Theato Copyright (C) 2014 Jean-Baptiste Mardelle This file is part of Kdenlive. See www.kdenlive.org. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "bin.h" #include "bincommands.h" #include "clipcreator.hpp" #include "core.h" #include "dialogs/clipcreationdialog.h" #include "doc/documentchecker.h" #include "doc/docundostack.hpp" #include "doc/kdenlivedoc.h" #include "effects/effectstack/model/effectstackmodel.hpp" #include "jobs/audiothumbjob.hpp" #include "jobs/jobmanager.h" #include "jobs/loadjob.hpp" #include "jobs/thumbjob.hpp" #include "kdenlive_debug.h" #include "kdenlivesettings.h" #include "mainwindow.h" #include "mlt++/Mlt.h" #include "mltcontroller/clipcontroller.h" #include "mltcontroller/clippropertiescontroller.h" #include "monitor/monitor.h" #include "project/dialogs/slideshowclip.h" #include "project/invaliddialog.h" #include "project/projectcommands.h" #include "project/projectmanager.h" #include "projectclip.h" #include "projectfolder.h" #include "projectfolderup.h" #include "projectitemmodel.h" #include "projectsortproxymodel.h" #include "projectsubclip.h" #include "titler/titlewidget.h" #include "ui_qtextclip_ui.h" #include "undohelper.hpp" #include "xml/xml.hpp" #include "xml/xml.hpp" #include #include #include #include #include "kdenlive_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include - +#include /** * @class BinItemDelegate * @brief This class is responsible for drawing items in the QTreeView. */ class BinItemDelegate : public QStyledItemDelegate { public: explicit BinItemDelegate(QObject *parent = nullptr) : QStyledItemDelegate(parent) , m_editorOpen(false) , m_dar(1.778) , dragType(PlaylistState::Disabled) { connect(this, &QStyledItemDelegate::closeEditor, [&]() { m_editorOpen = false; }); } void setDar(double dar) { m_dar = dar; } void setEditorData(QWidget *w, const QModelIndex &i) const override { if (!m_editorOpen) { QStyledItemDelegate::setEditorData(w, i); m_editorOpen = true; } } bool editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index) override { Q_UNUSED(model); Q_UNUSED(option); Q_UNUSED(index); if (event->type() == QEvent::MouseButtonPress) { QMouseEvent *me = (QMouseEvent *)event; if (m_audioDragRect.contains(me->pos())) { dragType = PlaylistState::AudioOnly; } else if (m_videoDragRect.contains(me->pos())) { dragType = PlaylistState::VideoOnly; } else { dragType = PlaylistState::Disabled; } } event->ignore(); return false; } void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const override { if (index.column() != 0) { QStyledItemDelegate::updateEditorGeometry(editor, option, index); return; } QStyleOptionViewItem opt = option; initStyleOption(&opt, index); QRect r1 = option.rect; int type = index.data(AbstractProjectItem::ItemTypeRole).toInt(); int decoWidth = 0; if (opt.decorationSize.height() > 0) { decoWidth += r1.height() * m_dar; } int mid = 0; if (type == AbstractProjectItem::ClipItem || type == AbstractProjectItem::SubClipItem) { mid = (int)((r1.height() / 2)); } r1.adjust(decoWidth, 0, 0, -mid); QFont ft = option.font; ft.setBold(true); QFontMetricsF fm(ft); QRect r2 = fm.boundingRect(r1, Qt::AlignLeft | Qt::AlignTop, index.data(AbstractProjectItem::DataName).toString()).toRect(); editor->setGeometry(r2); } QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override { QSize hint = QStyledItemDelegate::sizeHint(option, index); QString text = index.data(AbstractProjectItem::DataName).toString(); QRectF r = option.rect; QFont ft = option.font; ft.setBold(true); QFontMetricsF fm(ft); QStyle *style = option.widget ? option.widget->style() : QApplication::style(); const int textMargin = style->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1; int width = fm.boundingRect(r, Qt::AlignLeft | Qt::AlignTop, text).width() + option.decorationSize.width() + 2 * textMargin; hint.setWidth(width); int type = index.data(AbstractProjectItem::ItemTypeRole).toInt(); if (type == AbstractProjectItem::FolderItem || type == AbstractProjectItem::FolderUpItem) { return QSize(hint.width(), qMin(option.fontMetrics.lineSpacing() + 4, hint.height())); } if (type == AbstractProjectItem::ClipItem) { return QSize(hint.width(), qMax(option.fontMetrics.lineSpacing() * 2 + 4, qMax(hint.height(), option.decorationSize.height()))); } if (type == AbstractProjectItem::SubClipItem) { return QSize(hint.width(), qMax(option.fontMetrics.lineSpacing() * 2 + 4, qMin(hint.height(), (int)(option.decorationSize.height() / 1.5)))); } QIcon icon = qvariant_cast(index.data(Qt::DecorationRole)); QString line1 = index.data(Qt::DisplayRole).toString(); QString line2 = index.data(Qt::UserRole).toString(); int textW = qMax(option.fontMetrics.width(line1), option.fontMetrics.width(line2)); QSize iconSize = icon.actualSize(option.decorationSize); return QSize(qMax(textW, iconSize.width()) + 4, option.fontMetrics.lineSpacing() * 2 + 4); } void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override { if (index.column() == 0 && !index.data().isNull()) { QRect r1 = option.rect; painter->save(); painter->setClipRect(r1); QStyleOptionViewItem opt(option); initStyleOption(&opt, index); int type = index.data(AbstractProjectItem::ItemTypeRole).toInt(); QStyle *style = opt.widget ? opt.widget->style() : QApplication::style(); const int textMargin = style->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1; // QRect r = QStyle::alignedRect(opt.direction, Qt::AlignVCenter | Qt::AlignLeft, opt.decorationSize, r1); style->drawPrimitive(QStyle::PE_PanelItemViewItem, &opt, painter, opt.widget); if ((option.state & static_cast(QStyle::State_Selected)) != 0) { painter->setPen(option.palette.highlightedText().color()); } else { painter->setPen(option.palette.text().color()); } QRect r = r1; QFont font = painter->font(); font.setBold(true); painter->setFont(font); if (type == AbstractProjectItem::ClipItem || type == AbstractProjectItem::SubClipItem) { int decoWidth = 0; if (opt.decorationSize.height() > 0) { r.setWidth(r.height() * m_dar); QPixmap pix = opt.icon.pixmap(opt.icon.actualSize(r.size())); // Draw icon decoWidth += r.width() + textMargin; r.setWidth(r.height() * pix.width() / pix.height()); painter->drawPixmap(r, pix, QRect(0, 0, pix.width(), pix.height())); } int mid = (int)((r1.height() / 2)); r1.adjust(decoWidth, 0, 0, -mid); QRect r2 = option.rect; r2.adjust(decoWidth, mid, 0, 0); QRectF bounding; painter->drawText(r1, Qt::AlignLeft | Qt::AlignTop, index.data(AbstractProjectItem::DataName).toString(), &bounding); font.setBold(false); painter->setFont(font); QString subText = index.data(AbstractProjectItem::DataDuration).toString(); if (!subText.isEmpty()) { r2.adjust(0, bounding.bottom() - r2.top(), 0, 0); QColor subTextColor = painter->pen().color(); subTextColor.setAlphaF(.5); painter->setPen(subTextColor); // Draw usage counter int usage = index.data(AbstractProjectItem::UsageCount).toInt(); if (usage > 0) { subText.append(QString().sprintf(" [%d]", usage)); } painter->drawText(r2, Qt::AlignLeft | Qt::AlignTop, subText, &bounding); // Add audio/video icons for selective drag int cType = index.data(AbstractProjectItem::ClipType).toInt(); bool hasAudioAndVideo = index.data(AbstractProjectItem::ClipHasAudioAndVideo).toBool(); if (hasAudioAndVideo && (cType == ClipType::AV || cType == ClipType::Playlist) && (opt.state & QStyle::State_MouseOver)) { bounding.moveLeft(bounding.right() + (2 * textMargin)); bounding.adjust(0, textMargin, 0, -textMargin); QIcon aDrag = QIcon::fromTheme(QStringLiteral("audio-volume-medium")); m_audioDragRect = bounding.toRect(); m_audioDragRect.setWidth(m_audioDragRect.height()); aDrag.paint(painter, m_audioDragRect, Qt::AlignLeft); m_videoDragRect = m_audioDragRect; m_videoDragRect.moveLeft(m_audioDragRect.right()); QIcon vDrag = QIcon::fromTheme(QStringLiteral("kdenlive-show-video")); vDrag.paint(painter, m_videoDragRect, Qt::AlignLeft); } else { m_audioDragRect = QRect(); m_videoDragRect = QRect(); } } if (type == AbstractProjectItem::ClipItem) { // Overlay icon if necessary QVariant v = index.data(AbstractProjectItem::IconOverlay); if (!v.isNull()) { QIcon reload = QIcon::fromTheme(v.toString()); r.setTop(r.bottom() - bounding.height()); r.setWidth(bounding.height()); reload.paint(painter, r); } int jobProgress = index.data(AbstractProjectItem::JobProgress).toInt(); JobManagerStatus status = index.data(AbstractProjectItem::JobStatus).value(); if (status == JobManagerStatus::Pending || status == JobManagerStatus::Running) { // Draw job progress bar int progressWidth = option.fontMetrics.averageCharWidth() * 8; int progressHeight = option.fontMetrics.ascent() / 4; QRect progress(r1.x() + 1, opt.rect.bottom() - progressHeight - 2, progressWidth, progressHeight); painter->setPen(Qt::NoPen); painter->setBrush(Qt::darkGray); if (status == JobManagerStatus::Running) { painter->drawRoundedRect(progress, 2, 2); painter->setBrush((option.state & static_cast((QStyle::State_Selected) != 0)) != 0 ? option.palette.text() : option.palette.highlight()); progress.setWidth((progressWidth - 2) * jobProgress / 100); painter->drawRoundedRect(progress, 2, 2); } else { // Draw kind of a pause icon progress.setWidth(3); painter->drawRect(progress); progress.moveLeft(progress.right() + 3); painter->drawRect(progress); } } bool jobsucceeded = index.data(AbstractProjectItem::JobSuccess).toBool(); if (!jobsucceeded) { QIcon warning = QIcon::fromTheme(QStringLiteral("process-stop")); warning.paint(painter, r2); } } } else { // Folder or Folder Up items int decoWidth = 0; if (opt.decorationSize.height() > 0) { r.setWidth(r.height() * m_dar); QPixmap pix = opt.icon.pixmap(opt.icon.actualSize(r.size())); // Draw icon decoWidth += r.width() + textMargin; r.setWidth(r.height() * pix.width() / pix.height()); painter->drawPixmap(r, pix, QRect(0, 0, pix.width(), pix.height())); } r1.adjust(decoWidth, 0, 0, 0); QRectF bounding; painter->drawText(r1, Qt::AlignLeft | Qt::AlignTop, index.data(AbstractProjectItem::DataName).toString(), &bounding); } painter->restore(); } else { QStyledItemDelegate::paint(painter, option, index); } } private: mutable bool m_editorOpen; mutable QRect m_audioDragRect; mutable QRect m_videoDragRect; double m_dar; public: PlaylistState::ClipState dragType; }; MyListView::MyListView(QWidget *parent) : QListView(parent) { setViewMode(QListView::IconMode); setMovement(QListView::Static); setResizeMode(QListView::Adjust); setUniformItemSizes(true); setDragDropMode(QAbstractItemView::DragDrop); setAcceptDrops(true); setDragEnabled(true); viewport()->setAcceptDrops(true); } void MyListView::focusInEvent(QFocusEvent *event) { QListView::focusInEvent(event); if (event->reason() == Qt::MouseFocusReason) { emit focusView(); } } MyTreeView::MyTreeView(QWidget *parent) : QTreeView(parent) { setEditing(false); } void MyTreeView::mousePressEvent(QMouseEvent *event) { QTreeView::mousePressEvent(event); if (event->button() == Qt::LeftButton) { m_startPos = event->pos(); QModelIndex ix = indexAt(m_startPos); if (ix.isValid()) { QAbstractItemDelegate *del = itemDelegate(ix); m_dragType = static_cast(del)->dragType; } else { m_dragType = PlaylistState::Disabled; } } } void MyTreeView::focusInEvent(QFocusEvent *event) { QTreeView::focusInEvent(event); if (event->reason() == Qt::MouseFocusReason) { emit focusView(); } } void MyTreeView::mouseMoveEvent(QMouseEvent *event) { bool dragged = false; if ((event->buttons() & Qt::LeftButton) != 0u) { int distance = (event->pos() - m_startPos).manhattanLength(); if (distance >= QApplication::startDragDistance()) { dragged = performDrag(); } } if (!dragged) { QTreeView::mouseMoveEvent(event); } } void MyTreeView::closeEditor(QWidget *editor, QAbstractItemDelegate::EndEditHint hint) { QAbstractItemView::closeEditor(editor, hint); setEditing(false); } void MyTreeView::editorDestroyed(QObject *editor) { QAbstractItemView::editorDestroyed(editor); setEditing(false); } bool MyTreeView::isEditing() const { return state() == QAbstractItemView::EditingState; } void MyTreeView::setEditing(bool edit) { setState(edit ? QAbstractItemView::EditingState : QAbstractItemView::NoState); } bool MyTreeView::performDrag() { QModelIndexList bases = selectedIndexes(); QModelIndexList indexes; for (int i = 0; i < bases.count(); i++) { if (bases.at(i).column() == 0) { indexes << bases.at(i); } } if (indexes.isEmpty()) { return false; } // Check if we want audio or video only emit updateDragMode(m_dragType); auto *drag = new QDrag(this); drag->setMimeData(model()->mimeData(indexes)); QModelIndex ix = indexes.constFirst(); if (ix.isValid()) { QIcon icon = ix.data(AbstractProjectItem::DataThumbnail).value(); QPixmap pix = icon.pixmap(iconSize()); QSize size = pix.size(); QImage image(size, QImage::Format_ARGB32_Premultiplied); image.fill(Qt::transparent); QPainter p(&image); p.setOpacity(0.7); p.drawPixmap(0, 0, pix); p.setOpacity(1); if (indexes.count() > 1) { QPalette palette; int radius = size.height() / 3; p.setBrush(palette.highlight()); p.setPen(palette.highlightedText().color()); p.drawEllipse(QPoint(size.width() / 2, size.height() / 2), radius, radius); p.drawText(size.width() / 2 - radius, size.height() / 2 - radius, 2 * radius, 2 * radius, Qt::AlignCenter, QString::number(indexes.count())); } p.end(); drag->setPixmap(QPixmap::fromImage(image)); } drag->exec(); return true; } SmallJobLabel::SmallJobLabel(QWidget *parent) : QPushButton(parent) , m_action(nullptr) { setFixedWidth(0); setFlat(true); m_timeLine = new QTimeLine(500, this); QObject::connect(m_timeLine, &QTimeLine::valueChanged, this, &SmallJobLabel::slotTimeLineChanged); QObject::connect(m_timeLine, &QTimeLine::finished, this, &SmallJobLabel::slotTimeLineFinished); hide(); } const QString SmallJobLabel::getStyleSheet(const QPalette &p) { KColorScheme scheme(p.currentColorGroup(), KColorScheme::Window); QColor bg = scheme.background(KColorScheme::LinkBackground).color(); QColor fg = scheme.foreground(KColorScheme::LinkText).color(); QString style = QStringLiteral("QPushButton {margin:3px;padding:2px;background-color: rgb(%1, %2, %3);border-radius: 4px;border: none;color: rgb(%4, %5, %6)}") .arg(bg.red()) .arg(bg.green()) .arg(bg.blue()) .arg(fg.red()) .arg(fg.green()) .arg(fg.blue()); bg = scheme.background(KColorScheme::ActiveBackground).color(); fg = scheme.foreground(KColorScheme::ActiveText).color(); style.append( QStringLiteral("\nQPushButton:hover {margin:3px;padding:2px;background-color: rgb(%1, %2, %3);border-radius: 4px;border: none;color: rgb(%4, %5, %6)}") .arg(bg.red()) .arg(bg.green()) .arg(bg.blue()) .arg(fg.red()) .arg(fg.green()) .arg(fg.blue())); return style; } void SmallJobLabel::setAction(QAction *action) { m_action = action; } void SmallJobLabel::slotTimeLineChanged(qreal value) { setFixedWidth(qMin(value * 2, qreal(1.0)) * sizeHint().width()); update(); } void SmallJobLabel::slotTimeLineFinished() { if (m_timeLine->direction() == QTimeLine::Forward) { // Show m_action->setVisible(true); } else { // Hide m_action->setVisible(false); setText(QString()); } } void SmallJobLabel::slotSetJobCount(int jobCount) { if (jobCount > 0) { // prepare animation setText(i18np("%1 job", "%1 jobs", jobCount)); setToolTip(i18np("%1 pending job", "%1 pending jobs", jobCount)); if (style()->styleHint(QStyle::SH_Widget_Animate, nullptr, this) != 0) { setFixedWidth(sizeHint().width()); m_action->setVisible(true); return; } if (m_action->isVisible()) { setFixedWidth(sizeHint().width()); update(); return; } setFixedWidth(0); m_action->setVisible(true); int wantedWidth = sizeHint().width(); setGeometry(-wantedWidth, 0, wantedWidth, height()); m_timeLine->setDirection(QTimeLine::Forward); if (m_timeLine->state() == QTimeLine::NotRunning) { m_timeLine->start(); } } else { if (style()->styleHint(QStyle::SH_Widget_Animate, nullptr, this) != 0) { setFixedWidth(0); m_action->setVisible(false); return; } // hide m_timeLine->setDirection(QTimeLine::Backward); if (m_timeLine->state() == QTimeLine::NotRunning) { m_timeLine->start(); } } } LineEventEater::LineEventEater(QObject *parent) : QObject(parent) { } bool LineEventEater::eventFilter(QObject *obj, QEvent *event) { switch (event->type()) { case QEvent::ShortcutOverride: if (((QKeyEvent *)event)->key() == Qt::Key_Escape) { emit clearSearchLine(); } break; case QEvent::Resize: // Workaround Qt BUG 54676 emit showClearButton(((QResizeEvent *)event)->size().width() > QFontMetrics(QApplication::font()).averageCharWidth() * 8); break; default: break; } return QObject::eventFilter(obj, event); } Bin::Bin(const std::shared_ptr &model, QWidget *parent) : QWidget(parent) , isLoading(false) , m_itemModel(model) , m_itemView(nullptr) , m_doc(nullptr) , m_extractAudioAction(nullptr) , m_transcodeAction(nullptr) , m_clipsActionsMenu(nullptr) , m_inTimelineAction(nullptr) , m_listType((BinViewType)KdenliveSettings::binMode()) , m_iconSize(160, 90) , m_propertiesPanel(nullptr) , m_blankThumb() , m_invalidClipDialog(nullptr) , m_gainedFocus(false) , m_audioDuration(0) , m_processedAudio(0) { m_layout = new QVBoxLayout(this); // Create toolbar for buttons m_toolbar = new QToolBar(this); int size = style()->pixelMetric(QStyle::PM_SmallIconSize); QSize iconSize(size, size); m_toolbar->setIconSize(iconSize); m_toolbar->setToolButtonStyle(Qt::ToolButtonIconOnly); m_layout->addWidget(m_toolbar); m_layout->setSpacing(0); m_layout->setContentsMargins(0, 0, 0, 0); // Search line m_proxyModel = new ProjectSortProxyModel(this); m_proxyModel->setDynamicSortFilter(true); m_searchLine = new QLineEdit(this); m_searchLine->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred); // m_searchLine->setClearButtonEnabled(true); m_searchLine->setPlaceholderText(i18n("Search")); m_searchLine->setFocusPolicy(Qt::ClickFocus); connect(m_searchLine, &QLineEdit::textChanged, m_proxyModel, &ProjectSortProxyModel::slotSetSearchString); auto *leventEater = new LineEventEater(this); m_searchLine->installEventFilter(leventEater); connect(leventEater, &LineEventEater::clearSearchLine, m_searchLine, &QLineEdit::clear); connect(leventEater, &LineEventEater::showClearButton, this, &Bin::showClearButton); setFocusPolicy(Qt::ClickFocus); connect(m_itemModel.get(), &ProjectItemModel::refreshPanel, this, &Bin::refreshPanel); connect(m_itemModel.get(), &ProjectItemModel::refreshAudioThumbs, this, &Bin::doRefreshAudioThumbs); connect(m_itemModel.get(), &ProjectItemModel::refreshClip, this, &Bin::refreshClip); connect(m_itemModel.get(), &ProjectItemModel::updateTimelineProducers, this, &Bin::updateTimelineProducers); connect(m_itemModel.get(), &ProjectItemModel::emitMessage, this, &Bin::emitMessage); // Connect models m_proxyModel->setSourceModel(m_itemModel.get()); connect(m_itemModel.get(), &QAbstractItemModel::dataChanged, m_proxyModel, &ProjectSortProxyModel::slotDataChanged); connect(m_proxyModel, &ProjectSortProxyModel::selectModel, this, &Bin::selectProxyModel); connect(m_itemModel.get(), static_cast(&ProjectItemModel::itemDropped), this, static_cast(&Bin::slotItemDropped)); connect(m_itemModel.get(), static_cast &, const QModelIndex &)>(&ProjectItemModel::itemDropped), this, static_cast &, const QModelIndex &)>(&Bin::slotItemDropped)); connect(m_itemModel.get(), &ProjectItemModel::effectDropped, this, &Bin::slotEffectDropped); connect(m_itemModel.get(), &QAbstractItemModel::dataChanged, this, &Bin::slotItemEdited); connect(this, &Bin::refreshPanel, this, &Bin::doRefreshPanel); // Zoom slider QWidget *container = new QWidget(this); QHBoxLayout *lay = new QHBoxLayout; m_slider = new QSlider(Qt::Horizontal, this); m_slider->setMaximumWidth(100); m_slider->setMinimumWidth(40); m_slider->setRange(0, 10); m_slider->setValue(KdenliveSettings::bin_zoom()); connect(m_slider, &QAbstractSlider::valueChanged, this, &Bin::slotSetIconSize); QToolButton *tb1 = new QToolButton(this); tb1->setIcon(QIcon::fromTheme(QStringLiteral("zoom-in"))); connect(tb1, &QToolButton::clicked, [&]() { m_slider->setValue(qMin(m_slider->value() + 1, m_slider->maximum())); }); QToolButton *tb2 = new QToolButton(this); tb2->setIcon(QIcon::fromTheme(QStringLiteral("zoom-out"))); connect(tb2, &QToolButton::clicked, [&]() { m_slider->setValue(qMax(m_slider->value() - 1, m_slider->minimum())); }); lay->addWidget(tb2); lay->addWidget(m_slider); lay->addWidget(tb1); container->setLayout(lay); auto *widgetslider = new QWidgetAction(this); widgetslider->setDefaultWidget(container); // View type KSelectAction *listType = new KSelectAction(QIcon::fromTheme(QStringLiteral("view-list-tree")), i18n("View Mode"), this); pCore->window()->actionCollection()->addAction(QStringLiteral("bin_view_mode"), listType); QAction *treeViewAction = listType->addAction(QIcon::fromTheme(QStringLiteral("view-list-tree")), i18n("Tree View")); listType->addAction(treeViewAction); treeViewAction->setData(BinTreeView); if (m_listType == treeViewAction->data().toInt()) { listType->setCurrentAction(treeViewAction); } pCore->window()->actionCollection()->addAction(QStringLiteral("bin_view_mode_tree"), treeViewAction); QAction *iconViewAction = listType->addAction(QIcon::fromTheme(QStringLiteral("view-list-icons")), i18n("Icon View")); iconViewAction->setData(BinIconView); if (m_listType == iconViewAction->data().toInt()) { listType->setCurrentAction(iconViewAction); } pCore->window()->actionCollection()->addAction(QStringLiteral("bin_view_mode_icon"), iconViewAction); QAction *disableEffects = new QAction(i18n("Disable Bin Effects"), this); connect(disableEffects, &QAction::triggered, [this](bool disable) { this->setBinEffectsEnabled(!disable); }); disableEffects->setIcon(QIcon::fromTheme(QStringLiteral("favorite"))); disableEffects->setData("disable_bin_effects"); disableEffects->setCheckable(true); disableEffects->setChecked(false); pCore->window()->actionCollection()->addAction(QStringLiteral("disable_bin_effects"), disableEffects); m_renameAction = KStandardAction::renameFile(this, SLOT(slotRenameItem()), this); m_renameAction->setText(i18n("Rename")); m_renameAction->setData("rename"); pCore->window()->actionCollection()->addAction(QStringLiteral("rename"), m_renameAction); listType->setToolBarMode(KSelectAction::MenuMode); connect(listType, static_cast(&KSelectAction::triggered), this, &Bin::slotInitView); // Settings menu QMenu *settingsMenu = new QMenu(i18n("Settings"), this); settingsMenu->addAction(listType); QMenu *sliderMenu = new QMenu(i18n("Zoom"), this); sliderMenu->setIcon(QIcon::fromTheme(QStringLiteral("zoom-in"))); sliderMenu->addAction(widgetslider); settingsMenu->addMenu(sliderMenu); // Column show / hide actions m_showDate = new QAction(i18n("Show date"), this); m_showDate->setCheckable(true); connect(m_showDate, &QAction::triggered, this, &Bin::slotShowDateColumn); m_showDesc = new QAction(i18n("Show description"), this); m_showDesc->setCheckable(true); connect(m_showDesc, &QAction::triggered, this, &Bin::slotShowDescColumn); settingsMenu->addAction(m_showDate); settingsMenu->addAction(m_showDesc); settingsMenu->addAction(disableEffects); auto *button = new QToolButton; button->setIcon(QIcon::fromTheme(QStringLiteral("kdenlive-menu"))); button->setToolTip(i18n("Options")); button->setMenu(settingsMenu); button->setPopupMode(QToolButton::InstantPopup); m_toolbar->addWidget(button); // small info button for pending jobs m_infoLabel = new SmallJobLabel(this); m_infoLabel->setStyleSheet(SmallJobLabel::getStyleSheet(palette())); connect(pCore->jobManager().get(), &JobManager::jobCount, m_infoLabel, &SmallJobLabel::slotSetJobCount); QAction *infoAction = m_toolbar->addWidget(m_infoLabel); m_jobsMenu = new QMenu(this); // connect(m_jobsMenu, &QMenu::aboutToShow, this, &Bin::slotPrepareJobsMenu); m_cancelJobs = new QAction(i18n("Cancel All Jobs"), this); m_cancelJobs->setCheckable(false); m_discardCurrentClipJobs = new QAction(i18n("Cancel Current Clip Jobs"), this); m_discardCurrentClipJobs->setCheckable(false); m_discardPendingJobs = new QAction(i18n("Cancel Pending Jobs"), this); m_discardPendingJobs->setCheckable(false); m_jobsMenu->addAction(m_cancelJobs); m_jobsMenu->addAction(m_discardCurrentClipJobs); m_jobsMenu->addAction(m_discardPendingJobs); m_infoLabel->setMenu(m_jobsMenu); m_infoLabel->setAction(infoAction); // Hack, create toolbar spacer QWidget *spacer = new QWidget(); spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); m_toolbar->addWidget(spacer); // Add search line m_toolbar->addWidget(m_searchLine); m_binTreeViewDelegate = new BinItemDelegate(this); // connect(pCore->projectManager(), SIGNAL(projectOpened(Project*)), this, SLOT(setProject(Project*))); m_headerInfo = QByteArray::fromBase64(KdenliveSettings::treeviewheaders().toLatin1()); m_propertiesPanel = new QScrollArea(this); m_propertiesPanel->setFrameShape(QFrame::NoFrame); // Insert listview m_itemView = new MyTreeView(this); m_layout->addWidget(m_itemView); // Info widget for failed jobs, other errors m_infoMessage = new KMessageWidget(this); m_layout->addWidget(m_infoMessage); m_infoMessage->setCloseButtonVisible(false); connect(m_infoMessage, &KMessageWidget::hideAnimationFinished, this, &Bin::slotResetInfoMessage); // m_infoMessage->setWordWrap(true); m_infoMessage->hide(); connect(this, &Bin::requesteInvalidRemoval, this, &Bin::slotQueryRemoval); connect(this, SIGNAL(displayBinMessage(QString, KMessageWidget::MessageType)), this, SLOT(doDisplayMessage(QString, KMessageWidget::MessageType))); } Bin::~Bin() { blockSignals(true); m_proxyModel->selectionModel()->blockSignals(true); setEnabled(false); m_propertiesPanel = nullptr; abortOperations(); m_itemModel->clean(); } QDockWidget *Bin::clipPropertiesDock() { return m_propertiesDock; } void Bin::abortOperations() { m_infoMessage->hide(); blockSignals(true); if (m_propertiesPanel) { for (QWidget *w : m_propertiesPanel->findChildren()) { delete w; } } delete m_itemView; m_itemView = nullptr; blockSignals(false); } bool Bin::eventFilter(QObject *obj, QEvent *event) { if (event->type() == QEvent::MouseButtonRelease) { if (!m_monitor->isActive()) { m_monitor->slotActivateMonitor(); } bool success = QWidget::eventFilter(obj, event); if (m_gainedFocus) { QMouseEvent *mouseEvent = static_cast(event); QAbstractItemView *view = qobject_cast(obj->parent()); if (view) { QModelIndex idx = view->indexAt(mouseEvent->pos()); m_gainedFocus = false; if (idx.isValid()) { std::shared_ptr item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(idx)); editMasterEffect(item); } else { editMasterEffect(nullptr); } } // make sure we discard the focus indicator m_gainedFocus = false; } return success; } if (event->type() == QEvent::MouseButtonDblClick) { QMouseEvent *mouseEvent = static_cast(event); QAbstractItemView *view = qobject_cast(obj->parent()); if (view) { QModelIndex idx = view->indexAt(mouseEvent->pos()); if (!idx.isValid()) { // User double clicked on empty area slotAddClip(); } else { slotItemDoubleClicked(idx, mouseEvent->pos()); } } else { qCDebug(KDENLIVE_LOG) << " +++++++ NO VIEW-------!!"; } return true; } if (event->type() == QEvent::Wheel) { QWheelEvent *e = static_cast(event); if ((e != nullptr) && e->modifiers() == Qt::ControlModifier) { slotZoomView(e->delta() > 0); // emit zoomView(e->delta() > 0); return true; } } return QWidget::eventFilter(obj, event); } void Bin::refreshIcons() { QList allMenus = this->findChildren(); for (int i = 0; i < allMenus.count(); i++) { QMenu *m = allMenus.at(i); QIcon ic = m->icon(); if (ic.isNull() || ic.name().isEmpty()) { continue; } QIcon newIcon = QIcon::fromTheme(ic.name()); m->setIcon(newIcon); } QList allButtons = this->findChildren(); for (int i = 0; i < allButtons.count(); i++) { QToolButton *m = allButtons.at(i); QIcon ic = m->icon(); if (ic.isNull() || ic.name().isEmpty()) { continue; } QIcon newIcon = QIcon::fromTheme(ic.name()); m->setIcon(newIcon); } } void Bin::slotSaveHeaders() { if ((m_itemView != nullptr) && m_listType == BinTreeView) { // save current treeview state (column width) QTreeView *view = static_cast(m_itemView); m_headerInfo = view->header()->saveState(); KdenliveSettings::setTreeviewheaders(m_headerInfo.toBase64()); } } void Bin::slotZoomView(bool zoomIn) { if (m_itemModel->rowCount() == 0) { // Don't zoom on empty bin return; } int progress = (zoomIn) ? 1 : -1; m_slider->setValue(m_slider->value() + progress); } Monitor *Bin::monitor() { return m_monitor; } const QStringList Bin::getFolderInfo(const QModelIndex &selectedIx) { QModelIndexList indexes; if (selectedIx.isValid()) { indexes << selectedIx; } else { indexes = m_proxyModel->selectionModel()->selectedIndexes(); } if (indexes.isEmpty()) { // return root folder info QStringList folderInfo; folderInfo << QString::number(-1); folderInfo << QString(); return folderInfo; } QModelIndex ix = indexes.constFirst(); if (ix.isValid() && (m_proxyModel->selectionModel()->isSelected(ix) || selectedIx.isValid())) { return m_itemModel->getEnclosingFolderInfo(m_proxyModel->mapToSource(ix)); } // return root folder info QStringList folderInfo; folderInfo << QString::number(-1); folderInfo << QString(); return folderInfo; } void Bin::slotAddClip() { // Check if we are in a folder QString parentFolder = getCurrentFolder(); ClipCreationDialog::createClipsCommand(m_doc, parentFolder, m_itemModel); } std::shared_ptr Bin::getFirstSelectedClip() { const QModelIndexList indexes = m_proxyModel->selectionModel()->selectedIndexes(); if (indexes.isEmpty()) { return std::shared_ptr(); } for (const QModelIndex &ix : indexes) { std::shared_ptr item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(ix)); if (item->itemType() == AbstractProjectItem::ClipItem) { auto clip = std::static_pointer_cast(item); if (clip) { return clip; } } } return nullptr; } void Bin::slotDeleteClip() { const QModelIndexList indexes = m_proxyModel->selectionModel()->selectedIndexes(); std::vector> items; bool included = false; bool usedFolder = false; auto checkInclusion = [](bool accum, std::shared_ptr item) { return accum || std::static_pointer_cast(item)->isIncludedInTimeline(); }; for (const QModelIndex &ix : indexes) { if (!ix.isValid() || ix.column() != 0) { continue; } std::shared_ptr item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(ix)); if (!item) { qDebug() << "Suspicious: item not found when trying to delete"; continue; } included = included || item->accumulate(false, checkInclusion); // Check if we are deleting non-empty folders: usedFolder = usedFolder || item->childCount() > 0; items.push_back(item); } if (included && (KMessageBox::warningContinueCancel(this, i18n("This will delete all selected clips from timeline")) != KMessageBox::Continue)) { return; } if (usedFolder && (KMessageBox::warningContinueCancel(this, i18n("This will delete all folder content")) != KMessageBox::Continue)) { return; } Fun undo = []() { return true; }; Fun redo = []() { return true; }; for (const auto &item : items) { m_itemModel->requestBinClipDeletion(item, undo, redo); } pCore->pushUndo(undo, redo, i18n("Delete bin Clips")); } void Bin::slotReloadClip() { const QModelIndexList indexes = m_proxyModel->selectionModel()->selectedIndexes(); for (const QModelIndex &ix : indexes) { if (!ix.isValid() || ix.column() != 0) { continue; } std::shared_ptr item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(ix)); std::shared_ptr currentItem = nullptr; if (item->itemType() == AbstractProjectItem::ClipItem) { currentItem = std::static_pointer_cast(item); } else if (item->itemType() == AbstractProjectItem::SubClipItem) { currentItem = std::static_pointer_cast(item)->getMasterClip(); } if (currentItem) { emit openClip(std::shared_ptr()); if (currentItem->clipType() == ClipType::Playlist) { // Check if a clip inside playlist is missing QString path = currentItem->url(); QFile f(path); QDomDocument doc; doc.setContent(&f, false); f.close(); DocumentChecker d(QUrl::fromLocalFile(path), doc); if (!d.hasErrorInClips() && doc.documentElement().hasAttribute(QStringLiteral("modified"))) { QString backupFile = path + QStringLiteral(".backup"); KIO::FileCopyJob *copyjob = KIO::file_copy(QUrl::fromLocalFile(path), QUrl::fromLocalFile(backupFile)); if (copyjob->exec()) { if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) { KMessageBox::sorry(this, i18n("Unable to write to file %1", path)); } else { QTextStream out(&f); out << doc.toString(); f.close(); KMessageBox::information( this, i18n("Your project file was modified by Kdenlive.\nTo make sure you don't lose data, a backup copy called %1 was created.", backupFile)); } } } } currentItem->reloadProducer(false); } } } void Bin::slotLocateClip() { const QModelIndexList indexes = m_proxyModel->selectionModel()->selectedIndexes(); for (const QModelIndex &ix : indexes) { if (!ix.isValid() || ix.column() != 0) { continue; } std::shared_ptr item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(ix)); std::shared_ptr currentItem = nullptr; if (item->itemType() == AbstractProjectItem::ClipItem) { currentItem = std::static_pointer_cast(item); } else if (item->itemType() == AbstractProjectItem::SubClipItem) { currentItem = std::static_pointer_cast(item)->getMasterClip(); } if (currentItem) { QUrl url = QUrl::fromLocalFile(currentItem->url()).adjusted(QUrl::RemoveFilename); bool exists = QFile(url.toLocalFile()).exists(); if (currentItem->hasUrl() && exists) { QDesktopServices::openUrl(url); qCDebug(KDENLIVE_LOG) << " / / " + url.toString(); } else { if (!exists) { emitMessage(i18n("Couldn't locate ") + QString(" (" + url.toString() + QLatin1Char(')')), 100, ErrorMessage); } return; } } } } void Bin::slotDuplicateClip() { const QModelIndexList indexes = m_proxyModel->selectionModel()->selectedIndexes(); for (const QModelIndex &ix : indexes) { if (!ix.isValid() || ix.column() != 0) { continue; } std::shared_ptr item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(ix)); if (item->itemType() == AbstractProjectItem::ClipItem) { auto currentItem = std::static_pointer_cast(item); if (currentItem) { QDomDocument doc; QDomElement xml = currentItem->toXml(doc); if (!xml.isNull()) { QString currentName = Xml::getXmlProperty(xml, QStringLiteral("kdenlive:clipname")); if (currentName.isEmpty()) { QUrl url = QUrl::fromLocalFile(Xml::getXmlProperty(xml, QStringLiteral("resource"))); if (url.isValid()) { currentName = url.fileName(); } } if (!currentName.isEmpty()) { currentName.append(i18nc("append to clip name to indicate a copied idem", " (copy)")); Xml::setXmlProperty(xml, QStringLiteral("kdenlive:clipname"), currentName); } QString id; m_itemModel->requestAddBinClip(id, xml, item->parent()->clipId(), i18n("Duplicate clip")); selectClipById(id); } } } else if (item->itemType() == AbstractProjectItem::SubClipItem) { auto currentItem = std::static_pointer_cast(item); QString id; QPoint clipZone = currentItem->zone(); m_itemModel->requestAddBinSubClip(id, clipZone.x(), clipZone.y(), QString(), currentItem->getMasterClip()->clipId()); selectClipById(id); } } } void Bin::setMonitor(Monitor *monitor) { m_monitor = monitor; connect(m_monitor, &Monitor::addClipToProject, this, &Bin::slotAddClipToProject); connect(m_monitor, &Monitor::refreshCurrentClip, this, &Bin::slotOpenCurrent); connect(this, &Bin::openClip, [&](std::shared_ptr clip, int in, int out) { m_monitor->slotOpenClip(clip, in, out); }); } void Bin::setDocument(KdenliveDoc *project) { blockSignals(true); m_proxyModel->selectionModel()->blockSignals(true); setEnabled(false); // Cleanup previous project m_itemModel->clean(); delete m_itemView; m_itemView = nullptr; m_doc = project; int iconHeight = QFontInfo(font()).pixelSize() * 3.5; m_iconSize = QSize(iconHeight * pCore->getCurrentDar(), iconHeight); setEnabled(true); blockSignals(false); m_proxyModel->selectionModel()->blockSignals(false); connect(m_proxyAction, SIGNAL(toggled(bool)), m_doc, SLOT(slotProxyCurrentItem(bool))); // connect(m_itemModel, SIGNAL(dataChanged(QModelIndex,QModelIndex)), m_itemView // connect(m_itemModel, SIGNAL(updateCurrentItem()), this, SLOT(autoSelect())); slotInitView(nullptr); bool binEffectsDisabled = getDocumentProperty(QStringLiteral("disablebineffects")).toInt() == 1; setBinEffectsEnabled(!binEffectsDisabled); } void Bin::createClip(const QDomElement &xml) { // Check if clip should be in a folder QString groupId = ProjectClip::getXmlProperty(xml, QStringLiteral("kdenlive:folderid")); std::shared_ptr parentFolder = m_itemModel->getFolderByBinId(groupId); if (!parentFolder) { parentFolder = m_itemModel->getRootFolder(); } QString path = Xml::getXmlProperty(xml, QStringLiteral("resource")); if (path.endsWith(QStringLiteral(".mlt")) || path.endsWith(QStringLiteral(".kdenlive"))) { QFile f(path); QDomDocument doc; doc.setContent(&f, false); f.close(); DocumentChecker d(QUrl::fromLocalFile(path), doc); if (!d.hasErrorInClips() && doc.documentElement().hasAttribute(QStringLiteral("modified"))) { QString backupFile = path + QStringLiteral(".backup"); KIO::FileCopyJob *copyjob = KIO::file_copy(QUrl::fromLocalFile(path), QUrl::fromLocalFile(backupFile)); if (copyjob->exec()) { if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) { KMessageBox::sorry(this, i18n("Unable to write to file %1", path)); } else { QTextStream out(&f); out << doc.toString(); f.close(); KMessageBox::information( this, i18n("Your project file was modified by Kdenlive.\nTo make sure you don't lose data, a backup copy called %1 was created.", backupFile)); } } } } QString id = Xml::getTagContentByAttribute(xml, QStringLiteral("property"), QStringLiteral("name"), QStringLiteral("kdenlive:id")); if (id.isEmpty()) { id = QString::number(m_itemModel->getFreeClipId()); } auto newClip = ProjectClip::construct(id, xml, m_blankThumb, m_itemModel); parentFolder->appendChild(newClip); } QString Bin::slotAddFolder(const QString &folderName) { auto parentFolder = m_itemModel->getFolderByBinId(getCurrentFolder()); qDebug() << "pranteforder id" << parentFolder->clipId(); QString newId; Fun undo = []() { return true; }; Fun redo = []() { return true; }; m_itemModel->requestAddFolder(newId, folderName.isEmpty() ? i18n("Folder") : folderName, parentFolder->clipId(), undo, redo); pCore->pushUndo(undo, redo, i18n("Create bin folder")); // Edit folder name if (!folderName.isEmpty()) { // We already have a name, no need to edit return newId; } auto folder = m_itemModel->getFolderByBinId(newId); auto ix = m_itemModel->getIndexFromItem(folder); qDebug() << "selecting" << ix; if (ix.isValid()) { qDebug() << "ix valid"; m_proxyModel->selectionModel()->clearSelection(); int row = ix.row(); const QModelIndex id = m_itemModel->index(row, 0, ix.parent()); const QModelIndex id2 = m_itemModel->index(row, m_itemModel->columnCount() - 1, ix.parent()); if (id.isValid() && id2.isValid()) { m_proxyModel->selectionModel()->select(QItemSelection(m_proxyModel->mapFromSource(id), m_proxyModel->mapFromSource(id2)), QItemSelectionModel::Select); } m_itemView->edit(m_proxyModel->mapFromSource(ix)); } return newId; } QModelIndex Bin::getIndexForId(const QString &id, bool folderWanted) const { QModelIndexList items = m_itemModel->match(m_itemModel->index(0, 0), AbstractProjectItem::DataId, QVariant::fromValue(id), 2, Qt::MatchRecursive); for (int i = 0; i < items.count(); i++) { AbstractProjectItem *currentItem = static_cast(items.at(i).internalPointer()); AbstractProjectItem::PROJECTITEMTYPE type = currentItem->itemType(); if (folderWanted && type == AbstractProjectItem::FolderItem) { // We found our folder return items.at(i); } if (!folderWanted && type == AbstractProjectItem::ClipItem) { // We found our clip return items.at(i); } } return QModelIndex(); } void Bin::selectClipById(const QString &clipId, int frame, const QPoint &zone) { if (m_monitor->activeClipId() == clipId) { if (frame > -1) { m_monitor->slotSeek(frame); } if (!zone.isNull()) { m_monitor->slotLoadClipZone(zone); } return; } m_proxyModel->selectionModel()->clearSelection(); std::shared_ptr clip = getBinClip(clipId); if (clip) { selectClip(clip); if (frame > -1) { m_monitor->slotSeek(frame); } if (!zone.isNull()) { m_monitor->slotLoadClipZone(zone); } } } void Bin::selectProxyModel(const QModelIndex &id) { if (isLoading) { // return; } if (id.isValid()) { if (id.column() != 0) { return; } std::shared_ptr currentItem = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(id)); if (currentItem) { // Set item as current so that it displays its content in clip monitor setCurrent(currentItem); if (currentItem->itemType() == AbstractProjectItem::ClipItem) { m_reloadAction->setEnabled(true); m_locateAction->setEnabled(true); m_duplicateAction->setEnabled(true); std::shared_ptr clip = std::static_pointer_cast(currentItem); ClipType::ProducerType type = clip->clipType(); m_openAction->setEnabled(type == ClipType::Image || type == ClipType::Audio || type == ClipType::Text || type == ClipType::TextTemplate); showClipProperties(clip, false); m_deleteAction->setText(i18n("Delete Clip")); m_proxyAction->setText(i18n("Proxy Clip")); emit findInTimeline(clip->clipId(), clip->timelineInstances()); } else if (currentItem->itemType() == AbstractProjectItem::FolderItem) { // A folder was selected, disable editing clip m_openAction->setEnabled(false); m_reloadAction->setEnabled(false); m_locateAction->setEnabled(false); m_duplicateAction->setEnabled(false); m_deleteAction->setText(i18n("Delete Folder")); m_proxyAction->setText(i18n("Proxy Folder")); } else if (currentItem->itemType() == AbstractProjectItem::SubClipItem) { showClipProperties(std::static_pointer_cast(currentItem->parent()), false); m_openAction->setEnabled(false); m_reloadAction->setEnabled(false); m_locateAction->setEnabled(false); m_duplicateAction->setEnabled(false); m_deleteAction->setText(i18n("Delete Clip")); m_proxyAction->setText(i18n("Proxy Clip")); } m_deleteAction->setEnabled(true); } else { emit findInTimeline(QString()); m_reloadAction->setEnabled(false); m_locateAction->setEnabled(false); m_duplicateAction->setEnabled(false); m_openAction->setEnabled(false); m_deleteAction->setEnabled(false); } } else { // No item selected in bin m_openAction->setEnabled(false); m_deleteAction->setEnabled(false); showClipProperties(nullptr); emit findInTimeline(QString()); emit requestClipShow(nullptr); // clear effect stack emit requestShowEffectStack(QString(), nullptr, QSize(), false); // Display black bg in clip monitor emit openClip(std::shared_ptr()); } } std::vector Bin::selectedClipsIds(bool excludeFolders) { const QModelIndexList indexes = m_proxyModel->selectionModel()->selectedIndexes(); std::vector ids; // We define the lambda that will be executed on each item of the subset of nodes of the tree that are selected auto itemAdder = [excludeFolders, &ids](std::vector &ids_vec, std::shared_ptr item) { auto binItem = std::static_pointer_cast(item); if (!excludeFolders || (binItem->itemType() != AbstractProjectItem::FolderItem && binItem->itemType() != AbstractProjectItem::FolderUpItem)) { ids.push_back(binItem->clipId()); } return ids_vec; }; for (const QModelIndex &ix : indexes) { if (!ix.isValid() || ix.column() != 0) { continue; } std::shared_ptr item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(ix)); item->accumulate(ids, itemAdder); } return ids; } QList> Bin::selectedClips() { auto ids = selectedClipsIds(true); QList> ret; for (const auto &id : ids) { ret.push_back(m_itemModel->getClipByBinID(id)); } return ret; } void Bin::slotInitView(QAction *action) { if (action) { m_proxyModel->selectionModel()->clearSelection(); int viewType = action->data().toInt(); KdenliveSettings::setBinMode(viewType); if (viewType == m_listType) { return; } if (m_listType == BinTreeView) { // save current treeview state (column width) QTreeView *view = static_cast(m_itemView); m_headerInfo = view->header()->saveState(); m_showDate->setEnabled(true); m_showDesc->setEnabled(true); } else { // remove the current folderUp item if any if (m_folderUp) { if (m_folderUp->parent()) { m_folderUp->parent()->removeChild(m_folderUp); } m_folderUp.reset(); } } m_listType = static_cast(viewType); } if (m_itemView) { delete m_itemView; } switch (m_listType) { case BinIconView: m_itemView = new MyListView(this); m_folderUp = ProjectFolderUp::construct(m_itemModel); m_showDate->setEnabled(false); m_showDesc->setEnabled(false); break; default: m_itemView = new MyTreeView(this); m_showDate->setEnabled(true); m_showDesc->setEnabled(true); break; } m_itemView->setMouseTracking(true); m_itemView->viewport()->installEventFilter(this); QSize zoom = m_iconSize * (m_slider->value() / 4.0); m_itemView->setIconSize(zoom); QPixmap pix(zoom); pix.fill(Qt::lightGray); m_blankThumb.addPixmap(pix); m_binTreeViewDelegate->setDar(pCore->getCurrentDar()); m_itemView->setModel(m_proxyModel); m_itemView->setSelectionModel(m_proxyModel->selectionModel()); m_layout->insertWidget(1, m_itemView); // setup some default view specific parameters if (m_listType == BinTreeView) { m_itemView->setItemDelegate(m_binTreeViewDelegate); MyTreeView *view = static_cast(m_itemView); view->setSortingEnabled(true); view->setWordWrap(true); connect(m_proxyModel, &QAbstractItemModel::layoutAboutToBeChanged, this, &Bin::slotSetSorting); connect(view, &MyTreeView::updateDragMode, m_itemModel.get(), &ProjectItemModel::setDragType, Qt::DirectConnection); m_proxyModel->setDynamicSortFilter(true); if (!m_headerInfo.isEmpty()) { view->header()->restoreState(m_headerInfo); } else { view->header()->resizeSections(QHeaderView::ResizeToContents); view->resizeColumnToContents(0); view->setColumnHidden(1, true); view->setColumnHidden(2, true); } m_showDate->setChecked(!view->isColumnHidden(1)); m_showDesc->setChecked(!view->isColumnHidden(2)); connect(view->header(), &QHeaderView::sectionResized, this, &Bin::slotSaveHeaders); connect(view->header(), &QHeaderView::sectionClicked, this, &Bin::slotSaveHeaders); connect(view, &MyTreeView::focusView, this, &Bin::slotGotFocus); } else if (m_listType == BinIconView) { MyListView *view = static_cast(m_itemView); connect(view, &MyListView::focusView, this, &Bin::slotGotFocus); } m_itemView->setEditTriggers(QAbstractItemView::NoEditTriggers); // DoubleClicked); m_itemView->setSelectionMode(QAbstractItemView::ExtendedSelection); m_itemView->setDragDropMode(QAbstractItemView::DragDrop); m_itemView->setAlternatingRowColors(true); m_itemView->setAcceptDrops(true); m_itemView->setFocus(); } void Bin::slotSetIconSize(int size) { if (!m_itemView) { return; } KdenliveSettings::setBin_zoom(size); QSize zoom = m_iconSize; zoom = zoom * (size / 4.0); m_itemView->setIconSize(zoom); QPixmap pix(zoom); pix.fill(Qt::lightGray); m_blankThumb.addPixmap(pix); } void Bin::rebuildMenu() { m_transcodeAction = static_cast(pCore->window()->factory()->container(QStringLiteral("transcoders"), pCore->window())); m_extractAudioAction = static_cast(pCore->window()->factory()->container(QStringLiteral("extract_audio"), pCore->window())); m_clipsActionsMenu = static_cast(pCore->window()->factory()->container(QStringLiteral("clip_actions"), pCore->window())); m_menu->insertMenu(m_reloadAction, m_extractAudioAction); m_menu->insertMenu(m_reloadAction, m_transcodeAction); m_menu->insertMenu(m_reloadAction, m_clipsActionsMenu); m_inTimelineAction = m_menu->insertMenu(m_reloadAction, static_cast(pCore->window()->factory()->container(QStringLiteral("clip_in_timeline"), pCore->window()))); } void Bin::contextMenuEvent(QContextMenuEvent *event) { bool enableClipActions = false; ClipType::ProducerType type = ClipType::Unknown; bool isFolder = false; bool isImported = false; AbstractProjectItem::PROJECTITEMTYPE itemType = AbstractProjectItem::FolderItem; QString clipService; QString audioCodec; if (m_itemView) { QModelIndex idx = m_itemView->indexAt(m_itemView->viewport()->mapFromGlobal(event->globalPos())); if (idx.isValid()) { // User right clicked on a clip std::shared_ptr currentItem = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(idx)); itemType = currentItem->itemType(); if (currentItem) { enableClipActions = true; if (itemType == AbstractProjectItem::ClipItem) { auto clip = std::static_pointer_cast(currentItem); if (clip) { m_proxyAction->blockSignals(true); emit findInTimeline(clip->clipId(), clip->timelineInstances()); clipService = clip->getProducerProperty(QStringLiteral("mlt_service")); m_proxyAction->setChecked(clip->hasProxy()); QList transcodeActions; if (m_transcodeAction) { transcodeActions = m_transcodeAction->actions(); } QStringList dataList; QString condition; audioCodec = clip->codec(true); QString videoCodec = clip->codec(false); type = clip->clipType(); if (clip->hasUrl()) { isImported = true; } bool noCodecInfo = false; if (audioCodec.isEmpty() && videoCodec.isEmpty()) { noCodecInfo = true; } for (int i = 0; i < transcodeActions.count(); ++i) { dataList = transcodeActions.at(i)->data().toStringList(); if (dataList.count() > 4) { condition = dataList.at(4); if (condition.isEmpty()) { transcodeActions.at(i)->setEnabled(true); continue; } if (noCodecInfo) { // No audio / video codec, this is an MLT clip, disable conditionnal transcoding transcodeActions.at(i)->setEnabled(false); continue; } if (condition.startsWith(QLatin1String("vcodec"))) { transcodeActions.at(i)->setEnabled(condition.section(QLatin1Char('='), 1, 1) == videoCodec); } else if (condition.startsWith(QLatin1String("acodec"))) { transcodeActions.at(i)->setEnabled(condition.section(QLatin1Char('='), 1, 1) == audioCodec); } } } } m_proxyAction->blockSignals(false); } else if (itemType == AbstractProjectItem::SubClipItem) { } } } } // Enable / disable clip actions m_proxyAction->setEnabled((m_doc->getDocumentProperty(QStringLiteral("enableproxy")).toInt() != 0) && enableClipActions); m_openAction->setEnabled(type == ClipType::Image || type == ClipType::Audio || type == ClipType::TextTemplate || type == ClipType::Text); m_reloadAction->setEnabled(enableClipActions); m_locateAction->setEnabled(enableClipActions); m_duplicateAction->setEnabled(enableClipActions); m_editAction->setVisible(!isFolder); m_clipsActionsMenu->setEnabled(enableClipActions); m_extractAudioAction->setEnabled(enableClipActions); m_openAction->setVisible(itemType != AbstractProjectItem::FolderItem); m_reloadAction->setVisible(itemType != AbstractProjectItem::FolderItem); m_duplicateAction->setVisible(itemType != AbstractProjectItem::FolderItem); m_inTimelineAction->setVisible(itemType != AbstractProjectItem::FolderItem); if (m_transcodeAction) { m_transcodeAction->setEnabled(enableClipActions); m_transcodeAction->menuAction()->setVisible(itemType != AbstractProjectItem::FolderItem && clipService.contains(QStringLiteral("avformat"))); } m_clipsActionsMenu->menuAction()->setVisible( itemType != AbstractProjectItem::FolderItem && (clipService.contains(QStringLiteral("avformat")) || clipService.contains(QStringLiteral("xml")) || clipService.contains(QStringLiteral("consumer")))); m_extractAudioAction->menuAction()->setVisible(!isFolder && !audioCodec.isEmpty()); m_locateAction->setVisible(itemType != AbstractProjectItem::FolderItem && (isImported)); // Show menu event->setAccepted(true); if (enableClipActions) { m_menu->exec(event->globalPos()); } else { // Clicked in empty area m_addButton->menu()->exec(event->globalPos()); } } void Bin::slotItemDoubleClicked(const QModelIndex &ix, const QPoint pos) { std::shared_ptr item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(ix)); if (m_listType == BinIconView) { if (item->childCount() > 0 || item->itemType() == AbstractProjectItem::FolderItem) { m_folderUp->changeParent(std::static_pointer_cast(item)); m_itemView->setRootIndex(ix); return; } if (item == m_folderUp) { std::shared_ptr parentItem = item->parent(); QModelIndex parent = getIndexForId(parentItem->parent()->clipId(), parentItem->parent()->itemType() == AbstractProjectItem::FolderItem); if (parentItem->parent() != m_itemModel->getRootFolder()) { // We are entering a parent folder m_folderUp->changeParent(std::static_pointer_cast(parentItem->parent())); } else { m_folderUp->changeParent(std::shared_ptr()); } m_itemView->setRootIndex(m_proxyModel->mapFromSource(parent)); return; } } else { if (ix.column() == 0 && item->childCount() > 0) { QRect IconRect = m_itemView->visualRect(ix); IconRect.setWidth((double)IconRect.height() / m_itemView->iconSize().height() * m_itemView->iconSize().width()); if (!pos.isNull() && (IconRect.contains(pos) || pos.y() > (IconRect.y() + IconRect.height() / 2))) { QTreeView *view = static_cast(m_itemView); view->setExpanded(ix, !view->isExpanded(ix)); return; } } } if (ix.isValid()) { QRect IconRect = m_itemView->visualRect(ix); IconRect.setWidth((double)IconRect.height() / m_itemView->iconSize().height() * m_itemView->iconSize().width()); if (!pos.isNull() && ((ix.column() == 2 && item->itemType() == AbstractProjectItem::ClipItem) || (!IconRect.contains(pos) && pos.y() < (IconRect.y() + IconRect.height() / 2)))) { // User clicked outside icon, trigger rename m_itemView->edit(ix); return; } if (item->itemType() == AbstractProjectItem::ClipItem) { std::shared_ptr clip = std::static_pointer_cast(item); if (clip) { if (clip->clipType() == ClipType::Text || clip->clipType() == ClipType::TextTemplate) { // m_propertiesPanel->setEnabled(false); showTitleWidget(clip); } else { slotSwitchClipProperties(clip); } } } } } void Bin::slotEditClip() { QString panelId = m_propertiesPanel->property("clipId").toString(); QModelIndex current = m_proxyModel->selectionModel()->currentIndex(); std::shared_ptr item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(current)); if (item->clipId() != panelId) { // wrong clip return; } auto clip = std::static_pointer_cast(item); QString parentFolder = getCurrentFolder(); switch (clip->clipType()) { case ClipType::Text: case ClipType::TextTemplate: showTitleWidget(clip); break; case ClipType::SlideShow: showSlideshowWidget(clip); break; case ClipType::QText: ClipCreationDialog::createQTextClip(m_doc, parentFolder, this, clip.get()); break; default: break; } } void Bin::slotSwitchClipProperties() { QModelIndex current = m_proxyModel->selectionModel()->currentIndex(); if (current.isValid()) { // User clicked in the icon, open clip properties std::shared_ptr item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(current)); std::shared_ptr currentItem = nullptr; if (item->itemType() == AbstractProjectItem::ClipItem) { currentItem = std::static_pointer_cast(item); } else if (item->itemType() == AbstractProjectItem::SubClipItem) { currentItem = std::static_pointer_cast(item)->getMasterClip(); } if (currentItem) { slotSwitchClipProperties(currentItem); return; } } slotSwitchClipProperties(nullptr); } -void Bin::slotSwitchClipProperties(std::shared_ptr clip) +void Bin::slotSwitchClipProperties(const std::shared_ptr &clip) { if (clip == nullptr) { m_propertiesPanel->setEnabled(false); return; } if (clip->clipType() == ClipType::SlideShow) { m_propertiesPanel->setEnabled(false); showSlideshowWidget(clip); } else if (clip->clipType() == ClipType::QText) { m_propertiesPanel->setEnabled(false); QString parentFolder = getCurrentFolder(); ClipCreationDialog::createQTextClip(m_doc, parentFolder, this, clip.get()); } else { m_propertiesPanel->setEnabled(true); showClipProperties(clip); m_propertiesDock->show(); m_propertiesDock->raise(); } // Check if properties panel is not tabbed under Bin // if (!pCore->window()->isTabbedWith(m_propertiesDock, QStringLiteral("project_bin"))) { } void Bin::doRefreshPanel(const QString &id) { std::shared_ptr currentItem = getFirstSelectedClip(); if ((currentItem != nullptr) && currentItem->AbstractProjectItem::clipId() == id) { showClipProperties(currentItem, true); } } -void Bin::showClipProperties(std::shared_ptr clip, bool forceRefresh) +void Bin::showClipProperties(const std::shared_ptr &clip, bool forceRefresh) { if ((clip == nullptr) || !clip->isReady()) { for (QWidget *w : m_propertiesPanel->findChildren()) { delete w; } m_propertiesPanel->setProperty("clipId", QString()); m_propertiesPanel->setEnabled(false); return; } m_propertiesPanel->setEnabled(true); QString panelId = m_propertiesPanel->property("clipId").toString(); if (!forceRefresh && panelId == clip->AbstractProjectItem::clipId()) { // the properties panel is already displaying current clip, do nothing return; } // Cleanup widget for new content for (QWidget *w : m_propertiesPanel->findChildren()) { delete w; } m_propertiesPanel->setProperty("clipId", clip->AbstractProjectItem::clipId()); QVBoxLayout *lay = static_cast(m_propertiesPanel->layout()); if (lay == nullptr) { lay = new QVBoxLayout(m_propertiesPanel); m_propertiesPanel->setLayout(lay); } ClipPropertiesController *panel = clip->buildProperties(m_propertiesPanel); connect(this, &Bin::refreshTimeCode, panel, &ClipPropertiesController::slotRefreshTimeCode); connect(panel, &ClipPropertiesController::updateClipProperties, this, &Bin::slotEditClipCommand); connect(panel, &ClipPropertiesController::seekToFrame, m_monitor, static_cast(&Monitor::slotSeek)); connect(panel, &ClipPropertiesController::editClip, this, &Bin::slotEditClip); connect(panel, SIGNAL(editAnalysis(QString, QString, QString)), this, SLOT(slotAddClipExtraData(QString, QString, QString))); lay->addWidget(panel); } void Bin::slotEditClipCommand(const QString &id, const QMap &oldProps, const QMap &newProps) { auto *command = new EditClipCommand(this, id, oldProps, newProps, true); m_doc->commandStack()->push(command); } void Bin::reloadClip(const QString &id) { std::shared_ptr clip = m_itemModel->getClipByBinID(id); if (!clip) { return; } clip->reloadProducer(); } void Bin::reloadMonitorIfActive(const QString &id) { if (m_monitor->activeClipId() == id) { slotOpenCurrent(); } } QStringList Bin::getBinFolderClipIds(const QString &id) const { QStringList ids; std::shared_ptr folder = m_itemModel->getFolderByBinId(id); if (folder) { for (int i = 0; i < folder->childCount(); i++) { std::shared_ptr child = std::static_pointer_cast(folder->child(i)); if (child->itemType() == AbstractProjectItem::ClipItem) { ids << child->clipId(); } } } return ids; } std::shared_ptr Bin::getBinClip(const QString &id) { std::shared_ptr clip = nullptr; if (id.contains(QLatin1Char('_'))) { clip = m_itemModel->getClipByBinID(id.section(QLatin1Char('_'), 0, 0)); } else if (!id.isEmpty()) { clip = m_itemModel->getClipByBinID(id); } return clip; } void Bin::setWaitingStatus(const QString &id) { std::shared_ptr clip = m_itemModel->getClipByBinID(id); if (clip) { clip->setClipStatus(AbstractProjectItem::StatusWaiting); } } void Bin::slotRemoveInvalidClip(const QString &id, bool replace, const QString &errorMessage) { Q_UNUSED(replace); std::shared_ptr clip = m_itemModel->getClipByBinID(id); if (!clip) { return; } emit requesteInvalidRemoval(id, clip->url(), errorMessage); } void Bin::selectClip(const std::shared_ptr &clip) { QModelIndex ix = m_itemModel->getIndexFromItem(clip); int row = ix.row(); const QModelIndex id = m_itemModel->index(row, 0, ix.parent()); const QModelIndex id2 = m_itemModel->index(row, m_itemModel->columnCount() - 1, ix.parent()); if (id.isValid() && id2.isValid()) { m_proxyModel->selectionModel()->select(QItemSelection(m_proxyModel->mapFromSource(id), m_proxyModel->mapFromSource(id2)), QItemSelectionModel::Select); } m_itemView->scrollTo(m_proxyModel->mapFromSource(ix)); } void Bin::slotOpenCurrent() { std::shared_ptr currentItem = getFirstSelectedClip(); if (currentItem) { emit openClip(currentItem); } } void Bin::openProducer(std::shared_ptr controller) { - emit openClip(controller); + emit openClip(std::move(controller)); } void Bin::openProducer(std::shared_ptr controller, int in, int out) { - emit openClip(controller, in, out); + emit openClip(std::move(controller), in, out); } void Bin::emitItemUpdated(std::shared_ptr item) { - emit itemUpdated(item); + emit itemUpdated(std::move(item)); } void Bin::emitRefreshPanel(const QString &id) { emit refreshPanel(id); } void Bin::setupGeneratorMenu() { if (!m_menu) { qCDebug(KDENLIVE_LOG) << "Warning, menu was not created, something is wrong"; return; } QMenu *addMenu = qobject_cast(pCore->window()->factory()->container(QStringLiteral("generators"), pCore->window())); if (addMenu) { QMenu *menu = m_addButton->menu(); menu->addMenu(addMenu); addMenu->setEnabled(!addMenu->isEmpty()); m_addButton->setMenu(menu); } addMenu = qobject_cast(pCore->window()->factory()->container(QStringLiteral("extract_audio"), pCore->window())); if (addMenu) { m_menu->addMenu(addMenu); addMenu->setEnabled(!addMenu->isEmpty()); m_extractAudioAction = addMenu; } addMenu = qobject_cast(pCore->window()->factory()->container(QStringLiteral("transcoders"), pCore->window())); if (addMenu) { m_menu->addMenu(addMenu); addMenu->setEnabled(!addMenu->isEmpty()); m_transcodeAction = addMenu; } addMenu = qobject_cast(pCore->window()->factory()->container(QStringLiteral("clip_actions"), pCore->window())); if (addMenu) { m_menu->addMenu(addMenu); addMenu->setEnabled(!addMenu->isEmpty()); m_clipsActionsMenu = addMenu; } addMenu = qobject_cast(pCore->window()->factory()->container(QStringLiteral("clip_in_timeline"), pCore->window())); if (addMenu) { m_inTimelineAction = m_menu->addMenu(addMenu); m_inTimelineAction->setEnabled(!addMenu->isEmpty()); } if (m_locateAction) { m_menu->addAction(m_locateAction); } if (m_reloadAction) { m_menu->addAction(m_reloadAction); } if (m_duplicateAction) { m_menu->addAction(m_duplicateAction); } if (m_proxyAction) { m_menu->addAction(m_proxyAction); } addMenu = qobject_cast(pCore->window()->factory()->container(QStringLiteral("clip_timeline"), pCore->window())); if (addMenu) { m_menu->addMenu(addMenu); addMenu->setEnabled(false); } m_menu->addAction(m_editAction); m_menu->addAction(m_openAction); m_menu->addAction(m_renameAction); m_menu->addAction(m_deleteAction); m_menu->insertSeparator(m_deleteAction); } void Bin::setupMenu(QMenu *addMenu, QAction *defaultAction, const QHash &actions) { // Setup actions QAction *first = m_toolbar->actions().at(0); m_deleteAction = actions.value(QStringLiteral("delete")); m_toolbar->insertAction(first, m_deleteAction); QAction *folder = actions.value(QStringLiteral("folder")); m_toolbar->insertAction(m_deleteAction, folder); m_editAction = actions.value(QStringLiteral("properties")); m_openAction = actions.value(QStringLiteral("open")); m_reloadAction = actions.value(QStringLiteral("reload")); m_duplicateAction = actions.value(QStringLiteral("duplicate")); m_locateAction = actions.value(QStringLiteral("locate")); m_proxyAction = actions.value(QStringLiteral("proxy")); auto *m = new QMenu(this); m->addActions(addMenu->actions()); m_addButton = new QToolButton(this); m_addButton->setMenu(m); m_addButton->setDefaultAction(defaultAction); m_addButton->setPopupMode(QToolButton::MenuButtonPopup); m_toolbar->insertWidget(folder, m_addButton); m_menu = new QMenu(this); m_propertiesDock = pCore->window()->addDock(i18n("Clip Properties"), QStringLiteral("clip_properties"), m_propertiesPanel); m_propertiesDock->close(); // m_menu->addActions(addMenu->actions()); } const QString Bin::getDocumentProperty(const QString &key) { return m_doc->getDocumentProperty(key); } void Bin::slotUpdateJobStatus(const QString &id, int jobType, int status, const QString &label, const QString &actionName, const QString &details) { Q_UNUSED(id) Q_UNUSED(jobType) Q_UNUSED(status) Q_UNUSED(label) Q_UNUSED(actionName) Q_UNUSED(details) // TODO refac /* std::shared_ptr clip = m_itemModel->getClipByBinID(id); if (clip) { clip->setJobStatus((AbstractClipJob::JOBTYPE)jobType, (ClipJobStatus)status); } if (status == JobCrashed) { QList actions = m_infoMessage->actions(); if (m_infoMessage->isHidden()) { if (!details.isEmpty()) { m_infoMessage->setText(label + QStringLiteral(" ") + i18n("Show log") + QStringLiteral("")); } else { m_infoMessage->setText(label); } m_infoMessage->setWordWrap(m_infoMessage->text().length() > 35); m_infoMessage->setMessageType(KMessageWidget::Warning); } if (!actionName.isEmpty()) { QAction *action = nullptr; QList collections = KActionCollection::allCollections(); for (int i = 0; i < collections.count(); ++i) { KActionCollection *coll = collections.at(i); action = coll->action(actionName); if (action) { break; } } if ((action != nullptr) && !actions.contains(action)) { m_infoMessage->addAction(action); } } if (!details.isEmpty()) { m_errorLog.append(details); } m_infoMessage->setCloseButtonVisible(true); m_infoMessage->animatedShow(); } */ } void Bin::doDisplayMessage(const QString &text, KMessageWidget::MessageType type, const QList &actions) { // Remove existing actions if any QList acts = m_infoMessage->actions(); while (!acts.isEmpty()) { QAction *a = acts.takeFirst(); m_infoMessage->removeAction(a); delete a; } m_infoMessage->setText(text); m_infoMessage->setWordWrap(text.length() > 35); for (QAction *action : actions) { m_infoMessage->addAction(action); connect(action, &QAction::triggered, this, &Bin::slotMessageActionTriggered); } m_infoMessage->setCloseButtonVisible(actions.isEmpty()); m_infoMessage->setMessageType(type); m_infoMessage->animatedShow(); } void Bin::doDisplayMessage(const QString &text, KMessageWidget::MessageType type, const QString &logInfo) { // Remove existing actions if any QList acts = m_infoMessage->actions(); while (!acts.isEmpty()) { QAction *a = acts.takeFirst(); m_infoMessage->removeAction(a); delete a; } m_infoMessage->setText(text); m_infoMessage->setWordWrap(text.length() > 35); QAction *ac = new QAction(i18n("Show log"), this); m_infoMessage->addAction(ac); connect(ac, &QAction::triggered, [this, logInfo](bool) { KMessageBox::sorry(this, logInfo, i18n("Detailed log")); slotMessageActionTriggered(); }); m_infoMessage->setCloseButtonVisible(false); m_infoMessage->setMessageType(type); m_infoMessage->animatedShow(); } void Bin::refreshClip(const QString &id) { if (m_monitor->activeClipId() == id) { m_monitor->refreshMonitorIfActive(); } } void Bin::doRefreshAudioThumbs(const QString &id) { if (m_monitor->activeClipId() == id) { slotSendAudioThumb(id); } } void Bin::slotCreateProjectClip() { QAction *act = qobject_cast(sender()); if (act == nullptr) { // Cannot access triggering action, something is wrong qCDebug(KDENLIVE_LOG) << "// Error in clip creation action"; return; } ClipType::ProducerType type = (ClipType::ProducerType)act->data().toInt(); QStringList folderInfo = getFolderInfo(); QString parentFolder = getCurrentFolder(); switch (type) { case ClipType::Color: ClipCreationDialog::createColorClip(m_doc, parentFolder, m_itemModel); break; case ClipType::SlideShow: ClipCreationDialog::createSlideshowClip(m_doc, parentFolder, m_itemModel); break; case ClipType::Text: ClipCreationDialog::createTitleClip(m_doc, parentFolder, QString(), m_itemModel); break; case ClipType::TextTemplate: ClipCreationDialog::createTitleTemplateClip(m_doc, parentFolder, m_itemModel); break; case ClipType::QText: ClipCreationDialog::createQTextClip(m_doc, parentFolder, this); break; default: break; } } void Bin::slotItemDropped(const QStringList &ids, const QModelIndex &parent) { std::shared_ptr parentItem; if (parent.isValid()) { parentItem = m_itemModel->getBinItemByIndex(parent); parentItem = parentItem->getEnclosingFolder(false); } else { parentItem = m_itemModel->getRootFolder(); } auto *moveCommand = new QUndoCommand(); moveCommand->setText(i18np("Move Clip", "Move Clips", ids.count())); QStringList folderIds; for (const QString &id : ids) { if (id.contains(QLatin1Char('/'))) { // trying to move clip zone, not allowed. Ignore continue; } if (id.startsWith(QLatin1Char('#'))) { // moving a folder, keep it for later folderIds << id; continue; } std::shared_ptr currentItem = m_itemModel->getClipByBinID(id); if (!currentItem) { continue; } std::shared_ptr currentParent = currentItem->parent(); if (currentParent != parentItem) { // Item was dropped on a different folder new MoveBinClipCommand(this, id, currentParent->clipId(), parentItem->clipId(), moveCommand); } } if (!folderIds.isEmpty()) { for (QString id : folderIds) { id.remove(0, 1); std::shared_ptr currentItem = m_itemModel->getFolderByBinId(id); if (!currentItem) { continue; } std::shared_ptr currentParent = currentItem->parent(); if (currentParent != parentItem) { // Item was dropped on a different folder new MoveBinFolderCommand(this, id, currentParent->clipId(), parentItem->clipId(), moveCommand); } } } if (moveCommand->childCount() == 0) { pCore->displayMessage(i18n("No valid clip to insert"), InformationMessage, 500); } else { m_doc->commandStack()->push(moveCommand); } } void Bin::slotAddEffect(QString id, const QStringList &effectData) { if (id.isEmpty()) { id = m_monitor->activeClipId(); } if (!id.isEmpty()) { std::shared_ptr clip = m_itemModel->getClipByBinID(id); if (clip) { if (effectData.count() == 4) { // Paste effect from another stack std::shared_ptr sourceStack = pCore->getItemEffectStack(effectData.at(1).toInt(), effectData.at(2).toInt()); clip->copyEffect(sourceStack, effectData.at(3).toInt()); } else { clip->addEffect(effectData.constFirst()); } return; } } pCore->displayMessage(i18n("Select a clip to apply an effect"), InformationMessage, 500); } void Bin::slotEffectDropped(const QStringList &effectData, const QModelIndex &parent) { if (parent.isValid()) { std::shared_ptr parentItem = m_itemModel->getBinItemByIndex(parent); if (parentItem->itemType() != AbstractProjectItem::ClipItem) { // effect only supported on clip items return; } m_proxyModel->selectionModel()->clearSelection(); int row = parent.row(); const QModelIndex id = m_itemModel->index(row, 0, parent.parent()); const QModelIndex id2 = m_itemModel->index(row, m_itemModel->columnCount() - 1, parent.parent()); if (id.isValid() && id2.isValid()) { m_proxyModel->selectionModel()->select(QItemSelection(m_proxyModel->mapFromSource(id), m_proxyModel->mapFromSource(id2)), QItemSelectionModel::Select); } setCurrent(parentItem); if (effectData.count() == 4) { // Paste effect from another stack std::shared_ptr sourceStack = pCore->getItemEffectStack(effectData.at(1).toInt(), effectData.at(2).toInt()); std::static_pointer_cast(parentItem)->copyEffect(sourceStack, effectData.at(3).toInt()); } else { std::static_pointer_cast(parentItem)->addEffect(effectData.constFirst()); } } } -void Bin::editMasterEffect(std::shared_ptr clip) +void Bin::editMasterEffect(const std::shared_ptr &clip) { if (m_gainedFocus) { // Widget just gained focus, updating stack is managed in the eventfilter event, not from item return; } if (clip) { if (clip->itemType() == AbstractProjectItem::ClipItem) { std::shared_ptr clp = std::static_pointer_cast(clip); emit requestShowEffectStack(clp->clipName(), clp->m_effectStack, clp->getFrameSize(), false); return; } if (clip->itemType() == AbstractProjectItem::SubClipItem) { if (auto ptr = clip->parentItem().lock()) { std::shared_ptr clp = std::static_pointer_cast(ptr); emit requestShowEffectStack(clp->clipName(), clp->m_effectStack, clp->getFrameSize(), false); } return; } } emit requestShowEffectStack(QString(), nullptr, QSize(), false); } void Bin::slotGotFocus() { m_gainedFocus = true; } void Bin::doMoveClip(const QString &id, const QString &newParentId) { std::shared_ptr currentItem = m_itemModel->getClipByBinID(id); if (!currentItem) { return; } std::shared_ptr currentParent = currentItem->parent(); std::shared_ptr newParent = m_itemModel->getFolderByBinId(newParentId); currentItem->changeParent(newParent); } void Bin::doMoveFolder(const QString &id, const QString &newParentId) { std::shared_ptr currentItem = m_itemModel->getFolderByBinId(id); std::shared_ptr currentParent = currentItem->parent(); std::shared_ptr newParent = m_itemModel->getFolderByBinId(newParentId); currentParent->removeChild(currentItem); currentItem->changeParent(newParent); } void Bin::droppedUrls(const QList &urls, const QStringList &folderInfo) { QModelIndex current; if (folderInfo.isEmpty()) { current = m_proxyModel->mapToSource(m_proxyModel->selectionModel()->currentIndex()); } else { // get index for folder current = getIndexForId(folderInfo.constFirst(), true); } slotItemDropped(urls, current); } void Bin::slotAddClipToProject(const QUrl &url) { QList urls; urls << url; QModelIndex current = m_proxyModel->mapToSource(m_proxyModel->selectionModel()->currentIndex()); slotItemDropped(urls, current); } void Bin::slotItemDropped(const QList &urls, const QModelIndex &parent) { QString parentFolder = m_itemModel->getRootFolder()->clipId(); if (parent.isValid()) { // Check if drop occurred on a folder std::shared_ptr parentItem = m_itemModel->getBinItemByIndex(parent); while (parentItem->itemType() != AbstractProjectItem::FolderItem) { parentItem = parentItem->parent(); } parentFolder = parentItem->clipId(); } ClipCreator::createClipsFromList(urls, true, parentFolder, m_itemModel); } void Bin::slotExpandUrl(const ItemInfo &info, const QString &url, QUndoCommand *command) { Q_UNUSED(info) Q_UNUSED(url) Q_UNUSED(command) // TODO reimplement this /* // Create folder to hold imported clips QString folderName = QFileInfo(url).fileName().section(QLatin1Char('.'), 0, 0); QString folderId = QString::number(getFreeFolderId()); new AddBinFolderCommand(this, folderId, folderName.isEmpty() ? i18n("Folder") : folderName, m_itemModel->getRootFolder()->clipId(), false, command); // Parse playlist clips QDomDocument doc; QFile file(url); doc.setContent(&file, false); file.close(); bool invalid = false; if (doc.documentElement().isNull()) { invalid = true; } QDomNodeList producers = doc.documentElement().elementsByTagName(QStringLiteral("producer")); QDomNodeList tracks = doc.documentElement().elementsByTagName(QStringLiteral("track")); if (invalid || producers.isEmpty()) { doDisplayMessage(i18n("Playlist clip %1 is invalid.", QFileInfo(url).fileName()), KMessageWidget::Warning); delete command; return; } if (tracks.count() > pCore->projectManager()->currentTimeline()->visibleTracksCount() + 1) { doDisplayMessage( i18n("Playlist clip %1 has too many tracks (%2) to be imported. Add new tracks to your project.", QFileInfo(url).fileName(), tracks.count()), KMessageWidget::Warning); delete command; return; } // Maps playlist producer IDs to (project) bin producer IDs. QMap idMap; // Maps hash IDs to (project) first playlist producer instance ID. This is // necessary to detect duplicate producer serializations produced by MLT. // This covers, for instance, images and titles. QMap hashToIdMap; QDir mltRoot(doc.documentElement().attribute(QStringLiteral("root"))); for (int i = 0; i < producers.count(); i++) { QDomElement prod = producers.at(i).toElement(); QString originalId = prod.attribute(QStringLiteral("id")); // track producer if (originalId.contains(QLatin1Char('_'))) { originalId = originalId.section(QLatin1Char('_'), 0, 0); } // slowmotion producer if (originalId.contains(QLatin1Char(':'))) { originalId = originalId.section(QLatin1Char(':'), 1, 1); } // We already have seen and mapped this producer. if (idMap.contains(originalId)) { continue; } // Check for duplicate producers, based on hash value of producer. // Be careful as to the kdenlive:file_hash! It is not unique for // title clips, nor color clips. Also not sure about image sequences. // So we use mlt service-specific hashes to identify duplicate producers. QString hash; QString mltService = Xml::getXmlProperty(prod, QStringLiteral("mlt_service")); if (mltService == QLatin1String("pixbuf") || mltService == QLatin1String("qimage") || mltService == QLatin1String("kdenlivetitle") || mltService == QLatin1String("color") || mltService == QLatin1String("colour")) { hash = mltService + QLatin1Char(':') + Xml::getXmlProperty(prod, QStringLiteral("kdenlive:clipname")) + QLatin1Char(':') + Xml::getXmlProperty(prod, QStringLiteral("kdenlive:folderid")) + QLatin1Char(':'); if (mltService == QLatin1String("kdenlivetitle")) { // Calculate hash based on title contents. hash.append( QString(QCryptographicHash::hash(Xml::getXmlProperty(prod, QStringLiteral("xmldata")).toUtf8(), QCryptographicHash::Md5).toHex())); } else if (mltService == QLatin1String("pixbuf") || mltService == QLatin1String("qimage") || mltService == QLatin1String("color") || mltService == QLatin1String("colour")) { hash.append(Xml::getXmlProperty(prod, QStringLiteral("resource"))); } QString singletonId = hashToIdMap.value(hash, QString()); if (singletonId.length() != 0) { // map duplicate producer ID to single bin clip producer ID. qCDebug(KDENLIVE_LOG) << "found duplicate producer:" << hash << ", reusing newID:" << singletonId; idMap.insert(originalId, singletonId); continue; } } // First occurrence of a producer, so allocate new bin clip producer ID. QString newId = QString::number(getFreeClipId()); idMap.insert(originalId, newId); qCDebug(KDENLIVE_LOG) << "originalId: " << originalId << ", newId: " << newId; // Ensure to register new bin clip producer ID in hash hashmap for // those clips that MLT likes to serialize multiple times. This is // indicated by having a hash "value" unqual "". See also above. if (hash.length() != 0) { hashToIdMap.insert(hash, newId); } // Add clip QDomElement clone = prod.cloneNode(true).toElement(); EffectsList::setProperty(clone, QStringLiteral("kdenlive:folderid"), folderId); // Do we have a producer that uses a resource property that contains a path? if (mltService == QLatin1String("avformat-novalidate") // av clip || mltService == QLatin1String("avformat") // av clip || mltService == QLatin1String("pixbuf") // image (sequence) clip || mltService == QLatin1String("qimage") // image (sequence) clip || mltService == QLatin1String("xml") // MLT playlist clip, someone likes recursion :) ) { // Make sure to correctly resolve relative resource paths based on // the playlist's root, not on this project's root QString resource = Xml::getXmlProperty(clone, QStringLiteral("resource")); if (QFileInfo(resource).isRelative()) { QFileInfo rootedResource(mltRoot, resource); qCDebug(KDENLIVE_LOG) << "fixed resource path for producer, newId:" << newId << "resource:" << rootedResource.absoluteFilePath(); EffectsList::setProperty(clone, QStringLiteral("resource"), rootedResource.absoluteFilePath()); } } ClipCreationDialog::createClipsCommand(this, clone, newId, command); } pCore->projectManager()->currentTimeline()->importPlaylist(info, idMap, doc, command); */ } void Bin::slotItemEdited(const QModelIndex &ix, const QModelIndex &, const QVector &roles) { if (ix.isValid() && roles.contains(AbstractProjectItem::DataName)) { // Clip renamed std::shared_ptr item = m_itemModel->getBinItemByIndex(ix); auto clip = std::static_pointer_cast(item); if (clip) { emit clipNameChanged(clip->AbstractProjectItem::clipId()); } } } void Bin::renameSubClipCommand(const QString &id, const QString &newName, const QString &oldName, int in, int out) { auto *command = new RenameBinSubClipCommand(this, id, newName, oldName, in, out); m_doc->commandStack()->push(command); } void Bin::renameSubClip(const QString &id, const QString &newName, const QString &oldName, int in, int out) { std::shared_ptr clip = m_itemModel->getClipByBinID(id); if (!clip) { return; } std::shared_ptr sub = clip->getSubClip(in, out); if (!sub) { return; } sub->setName(newName); clip->setProducerProperty("kdenlive:clipzone." + oldName, QString()); clip->setProducerProperty("kdenlive:clipzone." + newName, QString::number(in) + QLatin1Char(';') + QString::number(out)); emit itemUpdated(sub); } Timecode Bin::projectTimecode() const { return m_doc->timecode(); } void Bin::slotStartFilterJob(const ItemInfo &info, const QString &id, QMap &filterParams, QMap &consumerParams, QMap &extraParams) { Q_UNUSED(info) Q_UNUSED(id) Q_UNUSED(filterParams) Q_UNUSED(consumerParams) Q_UNUSED(extraParams) // TODO refac /* std::shared_ptr clip = getBinClip(id); if (!clip) { return; } QMap producerParams = QMap(); producerParams.insert(QStringLiteral("producer"), clip->url()); if (info.cropDuration != GenTime()) { producerParams.insert(QStringLiteral("in"), QString::number((int)info.cropStart.frames(pCore->getCurrentFps()))); producerParams.insert(QStringLiteral("out"), QString::number((int)(info.cropStart + info.cropDuration).frames(pCore->getCurrentFps()))); extraParams.insert(QStringLiteral("clipStartPos"), QString::number((int)info.startPos.frames(pCore->getCurrentFps()))); extraParams.insert(QStringLiteral("clipTrack"), QString::number(info.track)); } else { // We want to process whole clip producerParams.insert(QStringLiteral("in"), QString::number(0)); producerParams.insert(QStringLiteral("out"), QString::number(-1)); } */ } void Bin::focusBinView() const { m_itemView->setFocus(); } void Bin::slotOpenClip() { std::shared_ptr clip = getFirstSelectedClip(); if (!clip) { return; } switch (clip->clipType()) { case ClipType::Text: case ClipType::TextTemplate: showTitleWidget(clip); break; case ClipType::Image: if (KdenliveSettings::defaultimageapp().isEmpty()) { KMessageBox::sorry(QApplication::activeWindow(), i18n("Please set a default application to open images in the Settings dialog")); } else { QProcess::startDetached(KdenliveSettings::defaultimageapp(), QStringList() << clip->url()); } break; case ClipType::Audio: if (KdenliveSettings::defaultaudioapp().isEmpty()) { KMessageBox::sorry(QApplication::activeWindow(), i18n("Please set a default application to open audio files in the Settings dialog")); } else { QProcess::startDetached(KdenliveSettings::defaultaudioapp(), QStringList() << clip->url()); } break; default: break; } } void Bin::updateTimecodeFormat() { emit refreshTimeCode(); } /* void Bin::slotGotFilterJobResults(const QString &id, int startPos, int track, const stringMap &results, const stringMap &filterInfo) { if (filterInfo.contains(QStringLiteral("finalfilter"))) { if (filterInfo.contains(QStringLiteral("storedata"))) { // Store returned data as clip extra data std::shared_ptr clip = getBinClip(id); if (clip) { QString key = filterInfo.value(QStringLiteral("key")); QStringList newValue = clip->updatedAnalysisData(key, results.value(key), filterInfo.value(QStringLiteral("offset")).toInt()); slotAddClipExtraData(id, newValue.at(0), newValue.at(1)); } } if (startPos == -1) { // Processing bin clip std::shared_ptr currentItem = m_itemModel->getClipByBinID(id); if (!currentItem) { return; } std::shared_ptr ctl = std::static_pointer_cast(currentItem); EffectsList list = ctl->effectList(); QDomElement effect = list.effectById(filterInfo.value(QStringLiteral("finalfilter"))); QDomDocument doc; QDomElement e = doc.createElement(QStringLiteral("test")); doc.appendChild(e); e.appendChild(doc.importNode(effect, true)); if (!effect.isNull()) { QDomElement newEffect = effect.cloneNode().toElement(); QMap::const_iterator i = results.constBegin(); while (i != results.constEnd()) { EffectsList::setParameter(newEffect, i.key(), i.value()); ++i; } ctl->updateEffect(newEffect, effect.attribute(QStringLiteral("kdenlive_ix")).toInt()); emit requestClipShow(currentItem); // TODO use undo / redo for bin clip edit effect // EditEffectCommand *command = new EditEffectCommand(this, clip->track(), clip->startPos(), effect, newEffect, clip->selectedEffectIndex(), true, true); m_commandStack->push(command); emit clipItemSelected(clip); } // emit gotFilterJobResults(id, startPos, track, results, filterInfo); return; } // This is a timeline filter, forward results emit gotFilterJobResults(id, startPos, track, results, filterInfo); return; } // Currently, only the first value of results is used std::shared_ptr clip = getBinClip(id); if (!clip) { return; } // Check for return value int markersType = -1; if (filterInfo.contains(QStringLiteral("addmarkers"))) { markersType = filterInfo.value(QStringLiteral("addmarkers")).toInt(); } if (results.isEmpty()) { emit displayBinMessage(i18n("No data returned from clip analysis"), KMessageWidget::Warning); return; } bool dataProcessed = false; QString label = filterInfo.value(QStringLiteral("label")); QString key = filterInfo.value(QStringLiteral("key")); int offset = filterInfo.value(QStringLiteral("offset")).toInt(); QStringList value = results.value(key).split(QLatin1Char(';'), QString::SkipEmptyParts); // qCDebug(KDENLIVE_LOG)<<"// RESULT; "<setText(i18n("Auto Split Clip")); for (const QString &pos : value) { if (!pos.contains(QLatin1Char('='))) { continue; } int newPos = pos.section(QLatin1Char('='), 0, 0).toInt(); // Don't use scenes shorter than 1 second if (newPos - cutPos < 24) { continue; } new AddBinClipCutCommand(this, id, cutPos + offset, newPos + offset, true, command); cutPos = newPos; } if (command->childCount() == 0) { delete command; } else { m_doc->commandStack()->push(command); } } if (markersType >= 0) { // Add markers from returned data dataProcessed = true; int cutPos = 0; int index = 1; bool simpleList = false; double sourceFps = clip->getOriginalFps(); if (qFuzzyIsNull(sourceFps)) { sourceFps = pCore->getCurrentFps(); } if (filterInfo.contains(QStringLiteral("simplelist"))) { // simple list simpleList = true; } for (const QString &pos : value) { if (simpleList) { clip->getMarkerModel()->addMarker(GenTime((int)(pos.toInt() * pCore->getCurrentFps() / sourceFps), pCore->getCurrentFps()), label + pos, markersType); index++; continue; } if (!pos.contains(QLatin1Char('='))) { continue; } int newPos = pos.section(QLatin1Char('='), 0, 0).toInt(); // Don't use scenes shorter than 1 second if (newPos - cutPos < 24) { continue; } clip->getMarkerModel()->addMarker(GenTime(newPos + offset, pCore->getCurrentFps()), label + QString::number(index), markersType); index++; cutPos = newPos; } } if (!dataProcessed || filterInfo.contains(QStringLiteral("storedata"))) { // Store returned data as clip extra data QStringList newValue = clip->updatedAnalysisData(key, results.value(key), offset); slotAddClipExtraData(id, newValue.at(0), newValue.at(1)); } } */ void Bin::slotGetCurrentProjectImage(const QString &clipId, bool request) { Q_UNUSED(clipId) // TODO refact : look at this // if (!clipId.isEmpty()) { // (pCore->projectManager()->currentTimeline()->hideClip(clipId, request)); // } pCore->monitorManager()->projectMonitor()->slotGetCurrentImage(request); } // TODO: move title editing into a better place... -void Bin::showTitleWidget(std::shared_ptr clip) +void Bin::showTitleWidget(const std::shared_ptr &clip) { QString path = clip->getProducerProperty(QStringLiteral("resource")); QDir titleFolder(m_doc->projectDataFolder() + QStringLiteral("/titles")); titleFolder.mkpath(QStringLiteral(".")); TitleWidget dia_ui(QUrl(), m_doc->timecode(), titleFolder.absolutePath(), pCore->monitorManager()->projectMonitor(), pCore->window()); connect(&dia_ui, &TitleWidget::requestBackgroundFrame, this, &Bin::slotGetCurrentProjectImage); QDomDocument doc; QString xmldata = clip->getProducerProperty(QStringLiteral("xmldata")); if (xmldata.isEmpty() && QFile::exists(path)) { QFile file(path); doc.setContent(&file, false); file.close(); } else { doc.setContent(xmldata); } dia_ui.setXml(doc, clip->AbstractProjectItem::clipId()); if (dia_ui.exec() == QDialog::Accepted) { QMap newprops; newprops.insert(QStringLiteral("xmldata"), dia_ui.xml().toString()); if (dia_ui.duration() != clip->duration().frames(pCore->getCurrentFps()) + 1) { // duration changed, we need to update duration newprops.insert(QStringLiteral("out"), clip->framesToTime(dia_ui.duration() - 1)); int currentLength = clip->getProducerDuration(); if (currentLength != dia_ui.duration()) { newprops.insert(QStringLiteral("kdenlive:duration"), clip->framesToTime(dia_ui.duration())); } } // trigger producer reload newprops.insert(QStringLiteral("force_reload"), QStringLiteral("2")); if (!path.isEmpty()) { // we are editing an external file, asked if we want to detach from that file or save the result to that title file. if (KMessageBox::questionYesNo(pCore->window(), i18n("You are editing an external title clip (%1). Do you want to save your changes to the title " "file or save the changes for this project only?", path), i18n("Save Title"), KGuiItem(i18n("Save to title file")), KGuiItem(i18n("Save in project only"))) == KMessageBox::Yes) { // save to external file dia_ui.saveTitle(QUrl::fromLocalFile(path)); } else { newprops.insert(QStringLiteral("resource"), QString()); } } slotEditClipCommand(clip->AbstractProjectItem::clipId(), clip->currentProperties(newprops), newprops); } } void Bin::slotResetInfoMessage() { m_errorLog.clear(); QList actions = m_infoMessage->actions(); for (int i = 0; i < actions.count(); ++i) { m_infoMessage->removeAction(actions.at(i)); } } void Bin::emitMessage(const QString &text, int progress, MessageType type) { emit displayMessage(text, progress, type); } void Bin::slotSetSorting() { QTreeView *view = qobject_cast(m_itemView); if (view) { int ix = view->header()->sortIndicatorSection(); m_proxyModel->setFilterKeyColumn(ix); } } void Bin::slotShowDateColumn(bool show) { QTreeView *view = qobject_cast(m_itemView); if (view) { view->setColumnHidden(1, !show); } } void Bin::slotShowDescColumn(bool show) { QTreeView *view = qobject_cast(m_itemView); if (view) { view->setColumnHidden(2, !show); } } void Bin::slotQueryRemoval(const QString &id, const QString &url, const QString &errorMessage) { if (m_invalidClipDialog) { if (!url.isEmpty()) { m_invalidClipDialog->addClip(id, url); } return; } QString message = i18n("Clip is invalid, will be removed from project."); if (!errorMessage.isEmpty()) { message.append("\n" + errorMessage); } m_invalidClipDialog = new InvalidDialog(i18n("Invalid clip"), message, true, this); m_invalidClipDialog->addClip(id, url); int result = m_invalidClipDialog->exec(); if (result == QDialog::Accepted) { const QStringList ids = m_invalidClipDialog->getIds(); Fun undo = []() { return true; }; Fun redo = []() { return true; }; for (const QString &i : ids) { auto item = m_itemModel->getClipByBinID(i); m_itemModel->requestBinClipDeletion(item, undo, redo); } } delete m_invalidClipDialog; m_invalidClipDialog = nullptr; } void Bin::slotRefreshClipThumbnail(const QString &id) { std::shared_ptr clip = m_itemModel->getClipByBinID(id); if (!clip) { return; } clip->reloadProducer(true); } void Bin::slotAddClipExtraData(const QString &id, const QString &key, const QString &clipData, QUndoCommand *groupCommand) { std::shared_ptr clip = m_itemModel->getClipByBinID(id); if (!clip) { return; } QString oldValue = clip->getProducerProperty(key); QMap oldProps; oldProps.insert(key, oldValue); QMap newProps; newProps.insert(key, clipData); auto *command = new EditClipCommand(this, id, oldProps, newProps, true, groupCommand); if (!groupCommand) { m_doc->commandStack()->push(command); } } void Bin::slotUpdateClipProperties(const QString &id, const QMap &properties, bool refreshPropertiesPanel) { std::shared_ptr clip = m_itemModel->getClipByBinID(id); if (clip) { clip->setProperties(properties, refreshPropertiesPanel); } } void Bin::updateTimelineProducers(const QString &id, const QMap &passProperties) { Q_UNUSED(id) Q_UNUSED(passProperties) // TODO REFAC // pCore->projectManager()->currentTimeline()->updateClipProperties(id, passProperties); // m_doc->renderer()->updateSlowMotionProducers(id, passProperties); } -void Bin::showSlideshowWidget(std::shared_ptr clip) +void Bin::showSlideshowWidget(const std::shared_ptr &clip) { QString folder = QFileInfo(clip->url()).absolutePath(); qCDebug(KDENLIVE_LOG) << " ** * CLIP ABS PATH: " << clip->url() << " = " << folder; SlideshowClip *dia = new SlideshowClip(m_doc->timecode(), folder, clip.get(), this); if (dia->exec() == QDialog::Accepted) { // edit clip properties QMap properties; properties.insert(QStringLiteral("out"), clip->framesToTime(m_doc->getFramePos(dia->clipDuration()) * dia->imageCount() - 1)); properties.insert(QStringLiteral("kdenlive:duration"), clip->framesToTime(m_doc->getFramePos(dia->clipDuration()) * dia->imageCount())); properties.insert(QStringLiteral("kdenlive:clipname"), dia->clipName()); properties.insert(QStringLiteral("ttl"), QString::number(m_doc->getFramePos(dia->clipDuration()))); properties.insert(QStringLiteral("loop"), QString::number(static_cast(dia->loop()))); properties.insert(QStringLiteral("crop"), QString::number(static_cast(dia->crop()))); properties.insert(QStringLiteral("fade"), QString::number(static_cast(dia->fade()))); properties.insert(QStringLiteral("luma_duration"), QString::number(m_doc->getFramePos(dia->lumaDuration()))); properties.insert(QStringLiteral("luma_file"), dia->lumaFile()); properties.insert(QStringLiteral("softness"), QString::number(dia->softness())); properties.insert(QStringLiteral("animation"), dia->animation()); QMap oldProperties; oldProperties.insert(QStringLiteral("out"), clip->getProducerProperty(QStringLiteral("out"))); oldProperties.insert(QStringLiteral("kdenlive:duration"), clip->getProducerProperty(QStringLiteral("kdenlive:duration"))); oldProperties.insert(QStringLiteral("kdenlive:clipname"), clip->name()); oldProperties.insert(QStringLiteral("ttl"), clip->getProducerProperty(QStringLiteral("ttl"))); oldProperties.insert(QStringLiteral("loop"), clip->getProducerProperty(QStringLiteral("loop"))); oldProperties.insert(QStringLiteral("crop"), clip->getProducerProperty(QStringLiteral("crop"))); oldProperties.insert(QStringLiteral("fade"), clip->getProducerProperty(QStringLiteral("fade"))); oldProperties.insert(QStringLiteral("luma_duration"), clip->getProducerProperty(QStringLiteral("luma_duration"))); oldProperties.insert(QStringLiteral("luma_file"), clip->getProducerProperty(QStringLiteral("luma_file"))); oldProperties.insert(QStringLiteral("softness"), clip->getProducerProperty(QStringLiteral("softness"))); oldProperties.insert(QStringLiteral("animation"), clip->getProducerProperty(QStringLiteral("animation"))); slotEditClipCommand(clip->AbstractProjectItem::clipId(), oldProperties, properties); } delete dia; } void Bin::setBinEffectsEnabled(bool enabled) { QAction *disableEffects = pCore->window()->actionCollection()->action(QStringLiteral("disable_bin_effects")); if (disableEffects) { if (enabled == disableEffects->isChecked()) { return; } disableEffects->blockSignals(true); disableEffects->setChecked(!enabled); disableEffects->blockSignals(false); } m_itemModel->setBinEffectsEnabled(enabled); pCore->projectManager()->disableBinEffects(!enabled); } void Bin::slotRenameItem() { const QModelIndexList indexes = m_proxyModel->selectionModel()->selectedRows(0); for (const QModelIndex &ix : indexes) { if (!ix.isValid()) { continue; } m_itemView->setCurrentIndex(ix); m_itemView->edit(ix); return; } } void Bin::refreshProxySettings() { QList> clipList = m_itemModel->getRootFolder()->childClips(); auto *masterCommand = new QUndoCommand(); masterCommand->setText(m_doc->useProxy() ? i18n("Enable proxies") : i18n("Disable proxies")); // en/disable proxy option in clip properties for (QWidget *w : m_propertiesPanel->findChildren()) { static_cast(w)->enableProxy(m_doc->useProxy()); } if (!m_doc->useProxy()) { // Disable all proxies m_doc->slotProxyCurrentItem(false, clipList, false, masterCommand); } else { QList> toProxy; - for (std::shared_ptr clp : clipList) { + for (const std::shared_ptr &clp : clipList) { ClipType::ProducerType t = clp->clipType(); if (t == ClipType::Playlist) { toProxy << clp; continue; } else if ((t == ClipType::AV || t == ClipType::Video) && m_doc->autoGenerateProxy(clp->getProducerIntProperty(QStringLiteral("meta.media.width")))) { // Start proxy toProxy << clp; continue; } else if (t == ClipType::Image && m_doc->autoGenerateImageProxy(clp->getProducerIntProperty(QStringLiteral("meta.media.width")))) { // Start proxy toProxy << clp; continue; } } if (!toProxy.isEmpty()) { m_doc->slotProxyCurrentItem(true, toProxy, false, masterCommand); } } if (masterCommand->childCount() > 0) { m_doc->commandStack()->push(masterCommand); } else { delete masterCommand; } } QStringList Bin::getProxyHashList() { QStringList list; QList> clipList = m_itemModel->getRootFolder()->childClips(); - for (std::shared_ptr clp : clipList) { + for (const std::shared_ptr &clp : clipList) { if (clp->clipType() == ClipType::AV || clp->clipType() == ClipType::Video || clp->clipType() == ClipType::Playlist) { list << clp->hash(); } } return list; } void Bin::slotSendAudioThumb(const QString &id) { std::shared_ptr clip = m_itemModel->getClipByBinID(id); if ((clip != nullptr) && clip->audioThumbCreated()) { m_monitor->prepareAudioThumb(clip->audioChannels(), clip->audioFrameCache); } else { QVariantList list; m_monitor->prepareAudioThumb(0, list); } } bool Bin::isEmpty() const { if (m_itemModel->getRootFolder() == nullptr) { return true; } return !m_itemModel->getRootFolder()->hasChildClips(); } void Bin::reloadAllProducers() { if (m_itemModel->getRootFolder() == nullptr || m_itemModel->getRootFolder()->childCount() == 0 || !isEnabled()) { return; } QList> clipList = m_itemModel->getRootFolder()->childClips(); emit openClip(std::shared_ptr()); - for (std::shared_ptr clip : clipList) { + for (const std::shared_ptr &clip : clipList) { QDomDocument doc; QDomElement xml = clip->toXml(doc); // Make sure we reload clip length xml.removeAttribute(QStringLiteral("out")); Xml::removeXmlProperty(xml, QStringLiteral("length")); if (!xml.isNull()) { clip->setClipStatus(AbstractProjectItem::StatusWaiting); clip->discardAudioThumb(); pCore->jobManager()->slotDiscardClipJobs(clip->clipId()); // We need to set a temporary id before all outdated producers are replaced; int jobId = pCore->jobManager()->startJob({clip->clipId()}, -1, QString(), xml); pCore->jobManager()->startJob({clip->clipId()}, jobId, QString(), 150, -1, true, true); pCore->jobManager()->startJob({clip->clipId()}, jobId, QString()); } } } void Bin::slotMessageActionTriggered() { m_infoMessage->animatedHide(); } void Bin::resetUsageCount() { const QList> clipList = m_itemModel->getRootFolder()->childClips(); - for (std::shared_ptr clip : clipList) { + for (const std::shared_ptr &clip : clipList) { clip->setRefCount(0); } } void Bin::getBinStats(uint *used, uint *unused, qint64 *usedSize, qint64 *unusedSize) { QList> clipList = m_itemModel->getRootFolder()->childClips(); - for (std::shared_ptr clip : clipList) { + for (const std::shared_ptr &clip : clipList) { if (clip->refCount() == 0) { *unused += 1; *unusedSize += clip->getProducerInt64Property(QStringLiteral("kdenlive:file_size")); } else { *used += 1; *usedSize += clip->getProducerInt64Property(QStringLiteral("kdenlive:file_size")); } } } QDir Bin::getCacheDir(CacheType type, bool *ok) const { return m_doc->getCacheDir(type, ok); } void Bin::rebuildProxies() { QList> clipList = m_itemModel->getRootFolder()->childClips(); QList> toProxy; - for (std::shared_ptr clp : clipList) { + for (const std::shared_ptr &clp : clipList) { if (clp->hasProxy()) { toProxy << clp; // Abort all pending jobs pCore->jobManager()->discardJobs(clp->clipId(), AbstractClipJob::PROXYJOB); clp->deleteProxy(); } } if (toProxy.isEmpty()) { return; } auto *masterCommand = new QUndoCommand(); masterCommand->setText(i18n("Rebuild proxies")); m_doc->slotProxyCurrentItem(true, toProxy, true, masterCommand); if (masterCommand->childCount() > 0) { m_doc->commandStack()->push(masterCommand); } else { delete masterCommand; } } void Bin::showClearButton(bool show) { m_searchLine->setClearButtonEnabled(show); } void Bin::saveZone(const QStringList &info, const QDir &dir) { if (info.size() != 3) { return; } std::shared_ptr clip = getBinClip(info.constFirst()); if (clip) { QPoint zone(info.at(1).toInt(), info.at(2).toInt()); clip->saveZone(zone, dir); } } -void Bin::setCurrent(std::shared_ptr item) +void Bin::setCurrent(const std::shared_ptr &item) { switch (item->itemType()) { case AbstractProjectItem::ClipItem: { openProducer(std::static_pointer_cast(item)); std::shared_ptr clp = std::static_pointer_cast(item); emit requestShowEffectStack(clp->clipName(), clp->m_effectStack, clp->getFrameSize(), false); break; } case AbstractProjectItem::SubClipItem: { auto subClip = std::static_pointer_cast(item); QPoint zone = subClip->zone(); openProducer(subClip->getMasterClip(), zone.x(), zone.y()); break; } case AbstractProjectItem::FolderUpItem: case AbstractProjectItem::FolderItem: default: break; } } void Bin::cleanup() { m_itemModel->requestCleanup(); } std::shared_ptr Bin::getClipEffectStack(int itemId) { std::shared_ptr clip = m_itemModel->getClipByBinID(QString::number(itemId)); Q_ASSERT(clip != nullptr); std::shared_ptr effectStack = std::static_pointer_cast(clip)->m_effectStack; return effectStack; } size_t Bin::getClipDuration(int itemId) const { std::shared_ptr clip = m_itemModel->getClipByBinID(QString::number(itemId)); Q_ASSERT(clip != nullptr); return clip->frameDuration(); } PlaylistState::ClipState Bin::getClipState(int itemId) const { std::shared_ptr clip = m_itemModel->getClipByBinID(QString::number(itemId)); Q_ASSERT(clip != nullptr); bool audio = clip->hasAudio(); bool video = clip->hasVideo(); return audio ? (video ? PlaylistState::Disabled : PlaylistState::AudioOnly) : PlaylistState::VideoOnly; } QString Bin::getCurrentFolder() { // Check parent item QModelIndex ix = m_proxyModel->selectionModel()->currentIndex(); std::shared_ptr parentFolder = m_itemModel->getRootFolder(); if (ix.isValid() && m_proxyModel->selectionModel()->isSelected(ix)) { std::shared_ptr currentItem = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(ix)); parentFolder = std::static_pointer_cast(currentItem->getEnclosingFolder()); } return parentFolder->clipId(); } void Bin::adjustProjectProfileToItem() { QModelIndex current = m_proxyModel->selectionModel()->currentIndex(); if (current.isValid()) { // User clicked in the icon, open clip properties std::shared_ptr item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(current)); auto clip = std::static_pointer_cast(item); if (clip) { QDomDocument doc; LoadJob::checkProfile(clip->clipId(), clip->toXml(doc, false), clip->originalProducer()); } } } diff --git a/src/bin/bin.h b/src/bin/bin.h index f145f236d..0dedf0145 100644 --- a/src/bin/bin.h +++ b/src/bin/bin.h @@ -1,469 +1,469 @@ /* Copyright (C) 2012 Till Theato Copyright (C) 2014 Jean-Baptiste Mardelle This file is part of Kdenlive. See www.kdenlive.org. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef KDENLIVE_BIN_H #define KDENLIVE_BIN_H #include "abstractprojectitem.h" #include "timecode.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include class AbstractProjectItem; class BinItemDelegate; class ClipController; class EffectStackModel; class InvalidDialog; class KdenliveDoc; class Monitor; class ProjectClip; class ProjectFolder; class ProjectFolderUp; class ProjectItemModel; class ProjectSortProxyModel; class QDockWidget; class QMenu; class QScrollArea; class QTimeLine; class QToolBar; class QToolButton; class QUndoCommand; class QVBoxLayout; class SmallJobLabel; namespace Mlt { class Producer; } class MyListView : public QListView { Q_OBJECT public: explicit MyListView(QWidget *parent = nullptr); protected: void focusInEvent(QFocusEvent *event) override; signals: void focusView(); void updateDragMode(ClipType::ProducerType type); }; class MyTreeView : public QTreeView { Q_OBJECT Q_PROPERTY(bool editing READ isEditing WRITE setEditing) public: explicit MyTreeView(QWidget *parent = nullptr); void setEditing(bool edit); protected: void mousePressEvent(QMouseEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override; void focusInEvent(QFocusEvent *event) override; protected slots: void closeEditor(QWidget *editor, QAbstractItemDelegate::EndEditHint hint) override; void editorDestroyed(QObject *editor) override; private: QPoint m_startPos; PlaylistState::ClipState m_dragType; bool m_editing; bool performDrag(); bool isEditing() const; signals: void focusView(); void updateDragMode(PlaylistState::ClipState type); }; class SmallJobLabel : public QPushButton { Q_OBJECT public: explicit SmallJobLabel(QWidget *parent = nullptr); static const QString getStyleSheet(const QPalette &p); void setAction(QAction *action); private: enum ItemRole { NameRole = Qt::UserRole, DurationRole, UsageRole }; QTimeLine *m_timeLine; QAction *m_action; public slots: void slotSetJobCount(int jobCount); private slots: void slotTimeLineChanged(qreal value); void slotTimeLineFinished(); }; class LineEventEater : public QObject { Q_OBJECT public: explicit LineEventEater(QObject *parent = nullptr); protected: bool eventFilter(QObject *obj, QEvent *event) override; signals: void clearSearchLine(); void showClearButton(bool); }; /** * @class Bin * @brief The bin widget takes care of both item model and view upon project opening. */ class Bin : public QWidget { Q_OBJECT /** @brief Defines the view types (icon view, tree view,...) */ enum BinViewType { BinTreeView, BinIconView }; public: explicit Bin(const std::shared_ptr &model, QWidget *parent = nullptr); ~Bin(); bool isLoading; /** @brief Sets the document for the bin and initialize some stuff */ void setDocument(KdenliveDoc *project); /** @brief Create a clip item from its xml description */ void createClip(const QDomElement &xml); /** @brief Used to notify the Model View that an item was updated */ void emitItemUpdated(std::shared_ptr item); /** @brief Set monitor associated with this bin (clipmonitor) */ void setMonitor(Monitor *monitor); /** @brief Returns the clip monitor */ Monitor *monitor(); /** @brief Open a producer in the clip monitor */ void openProducer(std::shared_ptr controller); void openProducer(std::shared_ptr controller, int in, int out); /** @brief Get a clip from it's id */ std::shared_ptr getBinClip(const QString &id); /** @brief Returns a list of selected clip ids @param excludeFolders: if true, ids of folders are not returned */ std::vector selectedClipsIds(bool excludeFolders = true); // Returns the selected clips QList> selectedClips(); /** @brief Current producer has changed, refresh monitor and timeline*/ void refreshClip(const QString &id); void setupMenu(QMenu *addMenu, QAction *defaultAction, const QHash &actions); /** @brief The source file was modified, we will reload it soon, disable item in the meantime */ void setWaitingStatus(const QString &id); const QString getDocumentProperty(const QString &key); /** @brief Ask MLT to reload this clip's producer */ void reloadClip(const QString &id); /** @brief refresh monitor (if clip changed) */ void reloadMonitorIfActive(const QString &id); void doMoveClip(const QString &id, const QString &newParentId); void doMoveFolder(const QString &id, const QString &newParentId); void setupGeneratorMenu(); /** @brief Set focus to the Bin view. */ void focusBinView() const; /** @brief Get a string list of all clip ids that are inside a folder defined by id. */ QStringList getBinFolderClipIds(const QString &id) const; /** @brief Build a rename subclip command. */ void renameSubClipCommand(const QString &id, const QString &newName, const QString &oldName, int in, int out); /** @brief Rename a clip zone (subclip). */ void renameSubClip(const QString &id, const QString &newName, const QString &oldName, int in, int out); /** @brief Returns current project's timecode. */ Timecode projectTimecode() const; /** @brief Trigger timecode format refresh where needed. */ void updateTimecodeFormat(); /** @brief Edit an effect settings to a bin clip. */ - void editMasterEffect(std::shared_ptr clip); + void editMasterEffect(const std::shared_ptr &clip); /** @brief An effect setting was changed, update stack if displayed. */ void updateMasterEffect(ClipController *ctl); /** @brief Display a message about an operation in status bar. */ void emitMessage(const QString &, int, MessageType); void rebuildMenu(); void refreshIcons(); /** @brief This function change the global enabled state of the bin effects */ void setBinEffectsEnabled(bool enabled); void requestAudioThumbs(const QString &id, long duration); /** @brief Proxy status for the project changed, update. */ void refreshProxySettings(); /** @brief A clip is ready, update its info panel if displayed. */ void emitRefreshPanel(const QString &id); /** @brief Returns true if there is no clip. */ bool isEmpty() const; /** @brief Trigger reload of all clips. */ void reloadAllProducers(); /** @brief Get usage stats for project bin. */ void getBinStats(uint *used, uint *unused, qint64 *usedSize, qint64 *unusedSize); /** @brief Returns the clip properties dockwidget. */ QDockWidget *clipPropertiesDock(); /** @brief Returns a document's cache dir. ok is set to false if folder does not exist */ QDir getCacheDir(CacheType type, bool *ok) const; void rebuildProxies(); /** @brief Return a list of all clips hashes used in this project */ QStringList getProxyHashList(); /** @brief Get info (id, name) of a folder (or the currently selected one) */ const QStringList getFolderInfo(const QModelIndex &selectedIx = QModelIndex()); /** @brief Get binId of the current selected folder */ QString getCurrentFolder(); /** @brief Save a clip zone as MLT playlist */ void saveZone(const QStringList &info, const QDir &dir); // TODO refac: remove this and call directly the function in ProjectItemModel void cleanup(); private slots: void slotAddClip(); void slotReloadClip(); /** @brief Set sorting column */ void slotSetSorting(); /** @brief Show/hide date column */ void slotShowDateColumn(bool show); void slotShowDescColumn(bool show); /** @brief Setup the bin view type (icon view, tree view, ...). * @param action The action whose data defines the view type or nullptr to keep default view */ void slotInitView(QAction *action); /** @brief Update status for clip jobs */ void slotUpdateJobStatus(const QString &, int, int, const QString &label = QString(), const QString &actionName = QString(), const QString &details = QString()); void slotSetIconSize(int size); void selectProxyModel(const QModelIndex &id); void slotSaveHeaders(); void slotItemDropped(const QStringList &ids, const QModelIndex &parent); void slotItemDropped(const QList &urls, const QModelIndex &parent); void slotEffectDropped(const QStringList &effectData, const QModelIndex &parent); void slotItemEdited(const QModelIndex &, const QModelIndex &, const QVector &); /** @brief Reset all text and log data from info message widget. */ void slotResetInfoMessage(); /** @brief Show dialog prompting for removal of invalid clips. */ void slotQueryRemoval(const QString &id, const QString &url, const QString &errorMessage); /** @brief Request display of current clip in monitor. */ void slotOpenCurrent(); void slotZoomView(bool zoomIn); /** @brief Widget gained focus, make sure we display effects for master clip. */ void slotGotFocus(); /** @brief Rename a Bin Item. */ void slotRenameItem(); void doRefreshPanel(const QString &id); /** @brief Send audio thumb data to monitor for display. */ void slotSendAudioThumb(const QString &id); void doRefreshAudioThumbs(const QString &id); /** @brief Enable item view and hide message */ void slotMessageActionTriggered(); /** @brief Request editing of title or slideshow clip */ void slotEditClip(); /** @brief Enable / disable clear button on search line * this is a workaround foq Qt bug 54676 */ void showClearButton(bool show); public slots: void slotRemoveInvalidClip(const QString &id, bool replace, const QString &errorMessage); /** @brief Reload clip thumbnail - when frame for thumbnail changed */ void slotRefreshClipThumbnail(const QString &id); void slotDeleteClip(); void slotItemDoubleClicked(const QModelIndex &ix, const QPoint pos); - void slotSwitchClipProperties(std::shared_ptr clip); + void slotSwitchClipProperties(const std::shared_ptr &clip); void slotSwitchClipProperties(); /** @brief Creates a new folder with optional name, and returns new folder's id */ QString slotAddFolder(const QString &folderName = QString()); void slotCreateProjectClip(); void slotEditClipCommand(const QString &id, const QMap &oldProps, const QMap &newProps); /** @brief Start a filter job requested by a filter applied in timeline */ void slotStartFilterJob(const ItemInfo &info, const QString &id, QMap &filterParams, QMap &consumerParams, QMap &extraParams); /** @brief Open current clip in an external editing application */ void slotOpenClip(); void slotDuplicateClip(); void slotLocateClip(); /** @brief Add extra data to a clip. */ void slotAddClipExtraData(const QString &id, const QString &key, const QString &data = QString(), QUndoCommand *groupCommand = nullptr); void slotUpdateClipProperties(const QString &id, const QMap &properties, bool refreshPropertiesPanel); /** @brief Pass some important properties to timeline track producers. */ void updateTimelineProducers(const QString &id, const QMap &passProperties); /** @brief Add effect to active Bin clip (used when double clicking an effect in list). */ void slotAddEffect(QString id, const QStringList &effectData); /** @brief Request current frame from project monitor. * @param clipId is the id of a clip we want to hide from screenshot * @param request true to start capture process, false to end it. It is necessary to emit a false after image is received **/ void slotGetCurrentProjectImage(const QString &clipId, bool request); void slotExpandUrl(const ItemInfo &info, const QString &url, QUndoCommand *command); /** @brief Abort all ongoing operations to prepare close. */ void abortOperations(); void doDisplayMessage(const QString &text, KMessageWidget::MessageType type, const QList &actions = QList()); void doDisplayMessage(const QString &text, KMessageWidget::MessageType type, const QString &logInfo); /** @brief Reset all clip usage to 0 */ void resetUsageCount(); /** @brief Select a clip in the Bin from its id. */ void selectClipById(const QString &id, int frame = -1, const QPoint &zone = QPoint()); void slotAddClipToProject(const QUrl &url); void droppedUrls(const QList &urls, const QStringList &folderInfo = QStringList()); /** @brief Returns the effectstack of a given clip. */ std::shared_ptr getClipEffectStack(int itemId); /** @brief Returns the duration of a given clip. */ size_t getClipDuration(int itemId) const; /** @brief Returns the state of a given clip: AudioOnly, VideoOnly, Disabled (Disabled means it has audio and video capabilities */ PlaylistState::ClipState getClipState(int itemId) const; /** @brief Adjust project profile to current clip. */ void adjustProjectProfileToItem(); protected: /* This function is called whenever an item is selected to propagate signals (for ex request to show the clip in the monitor) */ - void setCurrent(std::shared_ptr item); + void setCurrent(const std::shared_ptr &item); void selectClip(const std::shared_ptr &clip); void contextMenuEvent(QContextMenuEvent *event) override; bool eventFilter(QObject *obj, QEvent *event) override; private: std::shared_ptr m_itemModel; QAbstractItemView *m_itemView; /** @brief An "Up" item that is inserted in bin when using icon view so that user can navigate up */ std::shared_ptr m_folderUp; BinItemDelegate *m_binTreeViewDelegate; ProjectSortProxyModel *m_proxyModel; QToolBar *m_toolbar; KdenliveDoc *m_doc; QLineEdit *m_searchLine; QToolButton *m_addButton; QMenu *m_extractAudioAction; QMenu *m_transcodeAction; QMenu *m_clipsActionsMenu; QAction *m_inTimelineAction; QAction *m_showDate; QAction *m_showDesc; /** @brief Default view type (icon, tree, ...) */ BinViewType m_listType; /** @brief Default icon size for the views. */ QSize m_iconSize; /** @brief Keeps the column width info of the tree view. */ QByteArray m_headerInfo; QVBoxLayout *m_layout; QDockWidget *m_propertiesDock; QScrollArea *m_propertiesPanel; QSlider *m_slider; Monitor *m_monitor; QIcon m_blankThumb; QMenu *m_menu; QAction *m_openAction; QAction *m_editAction; QAction *m_reloadAction; QAction *m_duplicateAction; QAction *m_locateAction; QAction *m_proxyAction; QAction *m_deleteAction; QAction *m_renameAction; QMenu *m_jobsMenu; QAction *m_cancelJobs; QAction *m_discardCurrentClipJobs; QAction *m_discardPendingJobs; SmallJobLabel *m_infoLabel; /** @brief The info widget for failed jobs. */ KMessageWidget *m_infoMessage; QStringList m_errorLog; InvalidDialog *m_invalidClipDialog; /** @brief Set to true if widget just gained focus (means we have to update effect stack . */ bool m_gainedFocus; /** @brief List of Clip Ids that want an audio thumb. */ QStringList m_audioThumbsList; QString m_processingAudioThumb; QMutex m_audioThumbMutex; /** @brief Total number of milliseconds to process for audio thumbnails */ long m_audioDuration; /** @brief Total number of milliseconds already processed for audio thumbnails */ long m_processedAudio; /** @brief Indicates whether audio thumbnail creation is running. */ QFuture m_audioThumbsThread; - void showClipProperties(std::shared_ptr clip, bool forceRefresh = false); + void showClipProperties(const std::shared_ptr &clip, bool forceRefresh = false); /** @brief Get the QModelIndex value for an item in the Bin. */ QModelIndex getIndexForId(const QString &id, bool folderWanted) const; std::shared_ptr getFirstSelectedClip(); - void showTitleWidget(std::shared_ptr clip); - void showSlideshowWidget(std::shared_ptr clip); + void showTitleWidget(const std::shared_ptr &clip); + void showSlideshowWidget(const std::shared_ptr &clip); void processAudioThumbs(); signals: void itemUpdated(std::shared_ptr); void producerReady(const QString &id); /** @brief Save folder info into MLT. */ void storeFolder(const QString &folderId, const QString &parentId, const QString &oldParentId, const QString &folderName); void gotFilterJobResults(const QString &, int, int, stringMap, stringMap); /** @brief Trigger timecode format refresh where needed. */ void refreshTimeCode(); /** @brief Request display of effect stack for a Bin clip. */ void requestShowEffectStack(const QString &clipName, std::shared_ptr, QSize frameSize, bool showKeyframes); /** @brief Request that the given clip is displayed in the clip monitor */ void requestClipShow(std::shared_ptr); void displayBinMessage(const QString &, KMessageWidget::MessageType); void displayMessage(const QString &, int, MessageType); void requesteInvalidRemoval(const QString &, const QString &, const QString &); /** @brief Analysis data changed, refresh panel. */ void updateAnalysisData(const QString &); void openClip(std::shared_ptr c, int in = -1, int out = -1); /** @brief Fill context menu with occurrences of this clip in timeline. */ void findInTimeline(const QString &, QList ids = QList()); void clipNameChanged(const QString &); /** @brief A clip was updated, request panel update. */ void refreshPanel(const QString &id); }; #endif diff --git a/src/bin/clipcreator.cpp b/src/bin/clipcreator.cpp index e96c94195..39083f021 100644 --- a/src/bin/clipcreator.cpp +++ b/src/bin/clipcreator.cpp @@ -1,307 +1,307 @@ /*************************************************************************** * Copyright (C) 2017 by Nicolas Carion * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #include "clipcreator.hpp" #include "bin/bin.h" #include "core.h" #include "doc/kdenlivedoc.h" #include "kdenlivesettings.h" #include "klocalizedstring.h" #include "macros.hpp" #include "projectitemmodel.h" #include "titler/titledocument.h" #include "utils/devices.hpp" #include "xml/xml.hpp" #include #include #include #include #include - +#include namespace { QDomElement createProducer(QDomDocument &xml, ClipType::ProducerType type, const QString &resource, const QString &name, int duration, const QString &service) { QDomElement prod = xml.createElement(QStringLiteral("producer")); xml.appendChild(prod); prod.setAttribute(QStringLiteral("type"), (int)type); prod.setAttribute(QStringLiteral("in"), QStringLiteral("0")); prod.setAttribute(QStringLiteral("length"), duration); std::unordered_map properties; properties[QStringLiteral("resource")] = resource; if (!name.isEmpty()) { properties[QStringLiteral("kdenlive:clipname")] = name; } if (!service.isEmpty()) { properties[QStringLiteral("mlt_service")] = service; } Xml::addXmlProperties(prod, properties); return prod; } } // namespace QString ClipCreator::createTitleClip(const std::unordered_map &properties, int duration, const QString &name, const QString &parentFolder, - std::shared_ptr model) + const std::shared_ptr &model) { QDomDocument xml; auto prod = createProducer(xml, ClipType::Text, QString(), name, duration, QStringLiteral("kdenlivetitle")); Xml::addXmlProperties(prod, properties); QString id; bool res = model->requestAddBinClip(id, xml.documentElement(), parentFolder, i18n("Create title clip")); return res ? id : QStringLiteral("-1"); } QString ClipCreator::createColorClip(const QString &color, int duration, const QString &name, const QString &parentFolder, - std::shared_ptr model) + const std::shared_ptr &model) { QDomDocument xml; auto prod = createProducer(xml, ClipType::Color, color, name, duration, QStringLiteral("color")); QString id; bool res = model->requestAddBinClip(id, xml.documentElement(), parentFolder, i18n("Create color clip")); return res ? id : QStringLiteral("-1"); } -QString ClipCreator::createClipFromFile(const QString &path, const QString &parentFolder, std::shared_ptr model, Fun &undo, Fun &redo) +QString ClipCreator::createClipFromFile(const QString &path, const QString &parentFolder, const std::shared_ptr &model, Fun &undo, Fun &redo) { QDomDocument xml; QMimeDatabase db; QMimeType type = db.mimeTypeForUrl(QUrl::fromLocalFile(path)); qDebug() << "/////////// createClipFromFile" << path << parentFolder << path << type.name(); QDomElement prod; if (type.name().startsWith(QLatin1String("image/"))) { int duration = pCore->currentDoc()->getFramePos(KdenliveSettings::image_duration()); prod = createProducer(xml, ClipType::Image, path, QString(), duration, QString()); } else if (type.inherits(QStringLiteral("application/x-kdenlivetitle"))) { // opening a title file QDomDocument txtdoc(QStringLiteral("titledocument")); QFile txtfile(path); if (txtfile.open(QIODevice::ReadOnly) && txtdoc.setContent(&txtfile)) { txtfile.close(); // extract embedded images QDomNodeList items = txtdoc.elementsByTagName(QStringLiteral("content")); for (int j = 0; j < items.count(); ++j) { QDomElement content = items.item(j).toElement(); if (content.hasAttribute(QStringLiteral("base64"))) { QString titlesFolder = pCore->currentDoc()->projectDataFolder() + QStringLiteral("/titles/"); QString imgPath = TitleDocument::extractBase64Image(titlesFolder, content.attribute(QStringLiteral("base64"))); if (!imgPath.isEmpty()) { content.setAttribute(QStringLiteral("url"), imgPath); content.removeAttribute(QStringLiteral("base64")); } } } prod.setAttribute(QStringLiteral("in"), 0); int duration = 0; if (txtdoc.documentElement().hasAttribute(QStringLiteral("duration"))) { duration = txtdoc.documentElement().attribute(QStringLiteral("duration")).toInt(); } else if (txtdoc.documentElement().hasAttribute(QStringLiteral("out"))) { duration = txtdoc.documentElement().attribute(QStringLiteral("out")).toInt(); } if (duration <= 0) { duration = pCore->currentDoc()->getFramePos(KdenliveSettings::title_duration()) - 1; } prod = createProducer(xml, ClipType::Text, path, QString(), duration, QString()); txtdoc.documentElement().setAttribute(QStringLiteral("kdenlive:duration"), duration); QString titleData = txtdoc.toString(); prod.setAttribute(QStringLiteral("xmldata"), titleData); } else { txtfile.close(); return QStringLiteral("-1"); } } else { // it is a "normal" file, just use a producer prod = xml.createElement(QStringLiteral("producer")); xml.appendChild(prod); QMap properties; properties.insert(QStringLiteral("resource"), path); Xml::addXmlProperties(prod, properties); } if (pCore->bin()->isEmpty() && (KdenliveSettings::default_profile().isEmpty() || KdenliveSettings::checkfirstprojectclip())) { prod.setAttribute(QStringLiteral("_checkProfile"), 1); } qDebug() << "/////////// final xml" << xml.toString(); QString id; bool res = model->requestAddBinClip(id, xml.documentElement(), parentFolder, undo, redo); return res ? id : QStringLiteral("-1"); } bool ClipCreator::createClipFromFile(const QString &path, const QString &parentFolder, std::shared_ptr model) { Fun undo = []() { return true; }; Fun redo = []() { return true; }; - auto id = ClipCreator::createClipFromFile(path, parentFolder, model, undo, redo); + auto id = ClipCreator::createClipFromFile(path, parentFolder, std::move(model), undo, redo); bool ok = (id != QStringLiteral("-1")); if (ok) { pCore->pushUndo(undo, redo, i18n("Add clip")); } return ok; } QString ClipCreator::createSlideshowClip(const QString &path, int duration, const QString &name, const QString &parentFolder, - const std::unordered_map &properties, std::shared_ptr model) + const std::unordered_map &properties, const std::shared_ptr &model) { QDomDocument xml; auto prod = createProducer(xml, ClipType::SlideShow, path, name, duration, QString()); Xml::addXmlProperties(prod, properties); QString id; bool res = model->requestAddBinClip(id, xml.documentElement(), parentFolder, i18n("Create slideshow clip")); return res ? id : QStringLiteral("-1"); } QString ClipCreator::createTitleTemplate(const QString &path, const QString &text, const QString &name, const QString &parentFolder, - std::shared_ptr model) + const std::shared_ptr &model) { QDomDocument xml; // We try to retrieve duration for template int duration = 0; QDomDocument titledoc; QFile txtfile(path); if (txtfile.open(QIODevice::ReadOnly) && titledoc.setContent(&txtfile)) { if (titledoc.documentElement().hasAttribute(QStringLiteral("duration"))) { duration = titledoc.documentElement().attribute(QStringLiteral("duration")).toInt(); } else { // keep some time for backwards compatibility - 26/12/12 duration = titledoc.documentElement().attribute(QStringLiteral("out")).toInt(); } } txtfile.close(); // Duration not found, we fall-back to defaults if (duration == 0) { duration = pCore->currentDoc()->getFramePos(KdenliveSettings::title_duration()); } auto prod = createProducer(xml, ClipType::TextTemplate, path, name, duration, QString()); if (!text.isEmpty()) { prod.setAttribute(QStringLiteral("templatetext"), text); } QString id; bool res = model->requestAddBinClip(id, xml.documentElement(), parentFolder, i18n("Create title template")); return res ? id : QStringLiteral("-1"); } -bool ClipCreator::createClipsFromList(const QList &list, bool checkRemovable, const QString &parentFolder, std::shared_ptr model, +bool ClipCreator::createClipsFromList(const QList &list, bool checkRemovable, const QString &parentFolder, const std::shared_ptr &model, Fun &undo, Fun &redo) { qDebug() << "/////////// creatclipsfromlist" << list << checkRemovable << parentFolder; bool created = false; QMimeDatabase db; for (const QUrl &file : list) { QMimeType mType = db.mimeTypeForUrl(file); if (mType.inherits(QLatin1String("inode/directory"))) { // user dropped a folder, import its files QDir dir(file.path()); QStringList result = dir.entryList(QDir::Files); QStringList subfolders = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); QList folderFiles; // QStringList allExtensions = ClipCreationDialog::getExtensions(); for (const QString &path : result) { QUrl url = QUrl::fromLocalFile(dir.absoluteFilePath(path)); // Check file is of a supported type mType = db.mimeTypeForUrl(url); QString mimeAliases = mType.name(); bool isValid = mimeAliases.contains(QLatin1String("video/")); if (!isValid) { isValid = mimeAliases.contains(QLatin1String("audio/")); } if (!isValid) { isValid = mimeAliases.contains(QLatin1String("image/")); } if (!isValid && (mType.inherits(QLatin1String("video/mlt-playlist")) || mType.inherits(QLatin1String("application/x-kdenlivetitle")))) { isValid = true; } if (isValid) { folderFiles.append(url); } } QString folderId; Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; if (folderFiles.isEmpty()) { QList sublist; for (const QString &sub : subfolders) { QUrl url = QUrl::fromLocalFile(dir.absoluteFilePath(sub)); if (!list.contains(url)) { sublist << url; } } if (!sublist.isEmpty()) { // load subfolders createClipsFromList(sublist, checkRemovable, parentFolder, model, undo, redo); } } else { bool ok = pCore->projectItemModel()->requestAddFolder(folderId, dir.dirName(), parentFolder, local_undo, local_redo); if (ok) { ok = createClipsFromList(folderFiles, checkRemovable, folderId, model, local_undo, local_redo); if (!ok) { local_undo(); } else { UPDATE_UNDO_REDO_NOLOCK(local_redo, local_undo, undo, redo); } // Check subfolders QList sublist; for (const QString &sub : subfolders) { QUrl url = QUrl::fromLocalFile(dir.absoluteFilePath(sub)); if (!list.contains(url)) { sublist << url; } } if (!sublist.isEmpty()) { // load subfolders createClipsFromList(sublist, checkRemovable, folderId, model, undo, redo); } } } continue; } if (checkRemovable && isOnRemovableDevice(file) && !isOnRemovableDevice(pCore->currentDoc()->projectDataFolder())) { int answer = KMessageBox::warningContinueCancel( QApplication::activeWindow(), i18n("Clip %1
is on a removable device, will not be available when device is unplugged or mounted at a different position. You " "may want to copy it first to your hard-drive. Would you like to add it anyways?", file.path()), i18n("Removable device"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), QStringLiteral("confirm_removable_device")); if (answer == KMessageBox::Cancel) continue; } QString id = ClipCreator::createClipFromFile(file.toLocalFile(), parentFolder, model, undo, redo); created = created || (id != QStringLiteral("-1")); } qDebug() << "/////////// creatclipsfromlist return"; return created; } bool ClipCreator::createClipsFromList(const QList &list, bool checkRemovable, const QString &parentFolder, std::shared_ptr model) { Fun undo = []() { return true; }; Fun redo = []() { return true; }; - bool ok = ClipCreator::createClipsFromList(list, checkRemovable, parentFolder, model, undo, redo); + bool ok = ClipCreator::createClipsFromList(list, checkRemovable, parentFolder, std::move(model), undo, redo); if (ok) { pCore->pushUndo(undo, redo, i18np("Add clip", "Add clips", list.size())); } return ok; } diff --git a/src/bin/clipcreator.hpp b/src/bin/clipcreator.hpp index f501e74a9..da1166f61 100644 --- a/src/bin/clipcreator.hpp +++ b/src/bin/clipcreator.hpp @@ -1,99 +1,99 @@ /*************************************************************************** * Copyright (C) 2017 by Nicolas Carion * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #ifndef CLIPCREATOR_H #define CLIPCREATOR_H #include "definitions.h" #include "undohelper.hpp" #include #include #include /** @brief This namespace provides convenience functions to create clips based on various parameters */ class ProjectItemModel; namespace ClipCreator { /* @brief Create and inserts a color clip @param color : a string of the form "0xff0000ff" (solid red in RGBA) @param duration : duration expressed in number of frames @param name: name of the clip @param parentFolder: the binId of the containing folder @param model: a shared pointer to the bin item model @return the binId of the created clip */ -QString createColorClip(const QString &color, int duration, const QString &name, const QString &parentFolder, std::shared_ptr model); +QString createColorClip(const QString &color, int duration, const QString &name, const QString &parentFolder, const std::shared_ptr &model); /* @brief Create a title clip @param properties : title properties (xmldata, etc) @param duration : duration of the clip @param name: name of the clip @param parentFolder: the binId of the containing folder @param model: a shared pointer to the bin item model */ QString createTitleClip(const std::unordered_map &properties, int duration, const QString &name, const QString &parentFolder, - std::shared_ptr model); + const std::shared_ptr &model); /* @brief Create a title template @param path : path to the template @param text : text of the template (optional) @param name: name of the clip @param parentFolder: the binId of the containing folder @param model: a shared pointer to the bin item model @return the binId of the created clip */ QString createTitleTemplate(const QString &path, const QString &text, const QString &name, const QString &parentFolder, - std::shared_ptr model); + const std::shared_ptr &model); /* @brief Create a slideshow clip @param path : path to the selected folder @param duration: this should be nbr of images * duration of one image @param name: name of the clip @param parentFolder: the binId of the containing folder @param properties: description of the slideshow @param model: a shared pointer to the bin item model @return the binId of the created clip */ QString createSlideshowClip(const QString &path, int duration, const QString &name, const QString &parentFolder, - const std::unordered_map &properties, std::shared_ptr model); + const std::unordered_map &properties, const std::shared_ptr &model); /* @brief Reads a file from disk and create the corresponding clip @param path : path to the file @param parentFolder: the binId of the containing folder @param model: a shared pointer to the bin item model @return the binId of the created clip */ -QString createClipFromFile(const QString &path, const QString &parentFolder, std::shared_ptr model, Fun &undo, Fun &redo); +QString createClipFromFile(const QString &path, const QString &parentFolder, const std::shared_ptr &model, Fun &undo, Fun &redo); bool createClipFromFile(const QString &path, const QString &parentFolder, std::shared_ptr model); /* @brief Iterates recursively through the given url list and add the files it finds, recreating a folder structure @param list: the list of items (can be folders) @param checkRemovable: if true, it will check if files are on removable devices, and warn the user if so @param parentFolder: the binId of the containing folder @param model: a shared pointer to the bin item model */ -bool createClipsFromList(const QList &list, bool checkRemovable, const QString &parentFolder, std::shared_ptr model, Fun &undo, +bool createClipsFromList(const QList &list, bool checkRemovable, const QString &parentFolder, const std::shared_ptr &model, Fun &undo, Fun &redo); bool createClipsFromList(const QList &list, bool checkRemovable, const QString &parentFolder, std::shared_ptr model); } // namespace ClipCreator #endif diff --git a/src/bin/model/markerlistmodel.cpp b/src/bin/model/markerlistmodel.cpp index ae01b5f74..5116401e8 100644 --- a/src/bin/model/markerlistmodel.cpp +++ b/src/bin/model/markerlistmodel.cpp @@ -1,468 +1,468 @@ /*************************************************************************** * 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 "markerlistmodel.hpp" #include "bin/bin.h" #include "bin/projectclip.h" #include "core.h" #include "dialogs/markerdialog.h" #include "doc/docundostack.hpp" #include "kdenlivesettings.h" #include "macros.hpp" #include "project/projectmanager.h" #include "timeline2/model/snapmodel.hpp" #include #include #include #include #include std::array MarkerListModel::markerTypes{{Qt::red, Qt::blue, Qt::green, Qt::yellow, Qt::cyan}}; MarkerListModel::MarkerListModel(const QString &clipId, std::weak_ptr undo_stack, QObject *parent) : QAbstractListModel(parent) , m_undoStack(std::move(undo_stack)) , m_guide(false) , m_clipId(clipId) , m_lock(QReadWriteLock::Recursive) { setup(); } MarkerListModel::MarkerListModel(std::weak_ptr undo_stack, QObject *parent) : QAbstractListModel(parent) , m_undoStack(std::move(undo_stack)) , m_guide(true) , m_lock(QReadWriteLock::Recursive) { setup(); } void MarkerListModel::setup() { // We connect the signals of the abstractitemmodel to a more generic one. connect(this, &MarkerListModel::columnsMoved, this, &MarkerListModel::modelChanged); connect(this, &MarkerListModel::columnsRemoved, this, &MarkerListModel::modelChanged); connect(this, &MarkerListModel::columnsInserted, this, &MarkerListModel::modelChanged); connect(this, &MarkerListModel::rowsMoved, this, &MarkerListModel::modelChanged); connect(this, &MarkerListModel::rowsRemoved, this, &MarkerListModel::modelChanged); connect(this, &MarkerListModel::rowsInserted, this, &MarkerListModel::modelChanged); connect(this, &MarkerListModel::modelReset, this, &MarkerListModel::modelChanged); connect(this, &MarkerListModel::dataChanged, this, &MarkerListModel::modelChanged); } bool MarkerListModel::addMarker(GenTime pos, const QString &comment, int type, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; if (type == -1) type = KdenliveSettings::default_marker_type(); Q_ASSERT(type >= 0 && type < (int)markerTypes.size()); if (m_markerList.count(pos) > 0) { // In this case we simply change the comment and type QString oldComment = m_markerList[pos].first; int oldType = m_markerList[pos].second; local_undo = changeComment_lambda(pos, oldComment, oldType); local_redo = changeComment_lambda(pos, comment, type); } else { // In this case we create one local_redo = addMarker_lambda(pos, comment, type); local_undo = deleteMarker_lambda(pos); } if (local_redo()) { UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } return false; } bool MarkerListModel::addMarker(GenTime pos, const QString &comment, int type) { QWriteLocker locker(&m_lock); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool rename = (m_markerList.count(pos) > 0); bool res = addMarker(pos, comment, type, undo, redo); if (res) { if (rename) { PUSH_UNDO(undo, redo, m_guide ? i18n("Rename guide") : i18n("Rename marker")); } else { PUSH_UNDO(undo, redo, m_guide ? i18n("Add guide") : i18n("Add marker")); } } return res; } bool MarkerListModel::removeMarker(GenTime pos, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); Q_ASSERT(m_markerList.count(pos) > 0); QString oldComment = m_markerList[pos].first; int oldType = m_markerList[pos].second; Fun local_undo = addMarker_lambda(pos, oldComment, oldType); Fun local_redo = deleteMarker_lambda(pos); if (local_redo()) { UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } return false; } bool MarkerListModel::removeMarker(GenTime pos) { QWriteLocker locker(&m_lock); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool res = removeMarker(pos, undo, redo); if (res) { PUSH_UNDO(undo, redo, m_guide ? i18n("Delete guide") : i18n("Delete marker")); } return res; } bool MarkerListModel::editMarker(GenTime oldPos, GenTime pos, QString comment, int type) { QWriteLocker locker(&m_lock); Q_ASSERT(m_markerList.count(oldPos) > 0); QString oldComment = m_markerList[oldPos].first; if (comment.isEmpty()) { comment = oldComment; } int oldType = m_markerList[oldPos].second; if (oldPos == pos && oldComment == comment && oldType == type) return true; Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool res = removeMarker(oldPos, undo, redo); if (res) { res = addMarker(pos, comment, type, undo, redo); } if (res) { PUSH_UNDO(undo, redo, m_guide ? i18n("Edit guide") : i18n("Edit marker")); } else { bool undone = undo(); Q_ASSERT(undone); } return res; } Fun MarkerListModel::changeComment_lambda(GenTime pos, const QString &comment, int type) { QWriteLocker locker(&m_lock); auto guide = m_guide; auto clipId = m_clipId; return [guide, clipId, pos, comment, type]() { auto model = getModel(guide, clipId); Q_ASSERT(model->m_markerList.count(pos) > 0); int row = static_cast(std::distance(model->m_markerList.begin(), model->m_markerList.find(pos))); model->m_markerList[pos].first = comment; model->m_markerList[pos].second = type; emit model->dataChanged(model->index(row), model->index(row), QVector() << CommentRole << ColorRole); return true; }; } Fun MarkerListModel::addMarker_lambda(GenTime pos, const QString &comment, int type) { QWriteLocker locker(&m_lock); auto guide = m_guide; auto clipId = m_clipId; return [guide, clipId, pos, comment, type]() { auto model = getModel(guide, clipId); Q_ASSERT(model->m_markerList.count(pos) == 0); // We determine the row of the newly added marker auto insertionIt = model->m_markerList.lower_bound(pos); int insertionRow = static_cast(model->m_markerList.size()); if (insertionIt != model->m_markerList.end()) { insertionRow = static_cast(std::distance(model->m_markerList.begin(), insertionIt)); } model->beginInsertRows(QModelIndex(), insertionRow, insertionRow); model->m_markerList[pos] = {comment, type}; model->endInsertRows(); model->addSnapPoint(pos); return true; }; } Fun MarkerListModel::deleteMarker_lambda(GenTime pos) { QWriteLocker locker(&m_lock); auto guide = m_guide; auto clipId = m_clipId; return [guide, clipId, pos]() { auto model = getModel(guide, clipId); Q_ASSERT(model->m_markerList.count(pos) > 0); int row = static_cast(std::distance(model->m_markerList.begin(), model->m_markerList.find(pos))); model->beginRemoveRows(QModelIndex(), row, row); model->m_markerList.erase(pos); model->endRemoveRows(); model->removeSnapPoint(pos); return true; }; } std::shared_ptr MarkerListModel::getModel(bool guide, const QString &clipId) { if (guide) { return pCore->projectManager()->getGuideModel(); } return pCore->bin()->getBinClip(clipId)->getMarkerModel(); } QHash MarkerListModel::roleNames() const { QHash roles; roles[CommentRole] = "comment"; roles[PosRole] = "position"; roles[FrameRole] = "frame"; roles[ColorRole] = "color"; roles[TypeRole] = "type"; return roles; } void MarkerListModel::addSnapPoint(GenTime pos) { QWriteLocker locker(&m_lock); std::vector> validSnapModels; for (const auto &snapModel : m_registeredSnaps) { if (auto ptr = snapModel.lock()) { validSnapModels.push_back(snapModel); ptr->addPoint(pos.frames(pCore->getCurrentFps())); } } // Update the list of snapModel known to be valid std::swap(m_registeredSnaps, validSnapModels); } void MarkerListModel::removeSnapPoint(GenTime pos) { QWriteLocker locker(&m_lock); std::vector> validSnapModels; for (const auto &snapModel : m_registeredSnaps) { if (auto ptr = snapModel.lock()) { validSnapModels.push_back(snapModel); ptr->removePoint(pos.frames(pCore->getCurrentFps())); } } // Update the list of snapModel known to be valid std::swap(m_registeredSnaps, validSnapModels); } QVariant MarkerListModel::data(const QModelIndex &index, int role) const { READ_LOCK(); if (index.row() < 0 || index.row() >= static_cast(m_markerList.size()) || !index.isValid()) { return QVariant(); } auto it = m_markerList.begin(); std::advance(it, index.row()); switch (role) { case Qt::DisplayRole: case Qt::EditRole: case CommentRole: return it->second.first; case PosRole: return it->first.seconds(); case FrameRole: case Qt::UserRole: return it->first.frames(pCore->getCurrentFps()); case ColorRole: case Qt::DecorationRole: return markerTypes[(size_t)it->second.second]; case TypeRole: return it->second.second; } return QVariant(); } int MarkerListModel::rowCount(const QModelIndex &parent) const { READ_LOCK(); if (parent.isValid()) return 0; return static_cast(m_markerList.size()); } CommentedTime MarkerListModel::getMarker(const GenTime &pos, bool *ok) const { READ_LOCK(); if (m_markerList.count(pos) <= 0) { // return empty marker *ok = false; return CommentedTime(); } *ok = true; CommentedTime t(pos, m_markerList.at(pos).first, m_markerList.at(pos).second); return t; } QList MarkerListModel::getAllMarkers() const { READ_LOCK(); QList markers; for (const auto &marker : m_markerList) { CommentedTime t(marker.first, marker.second.first, marker.second.second); markers << t; } return markers; } bool MarkerListModel::hasMarker(int frame) const { READ_LOCK(); return m_markerList.count(GenTime(frame, pCore->getCurrentFps())) > 0; } -void MarkerListModel::registerSnapModel(std::weak_ptr snapModel) +void MarkerListModel::registerSnapModel(const std::weak_ptr &snapModel) { READ_LOCK(); // make sure ptr is valid if (auto ptr = snapModel.lock()) { // ptr is valid, we store it m_registeredSnaps.push_back(snapModel); // we now add the already existing markers to the snap for (const auto &marker : m_markerList) { GenTime pos = marker.first; ptr->addPoint(pos.frames(pCore->getCurrentFps())); } } else { qDebug() << "Error: added snapmodel is null"; Q_ASSERT(false); } } bool MarkerListModel::importFromJson(const QString &data, bool ignoreConflicts, bool pushUndo) { Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool result = importFromJson(data, ignoreConflicts, undo, redo); if (pushUndo) { PUSH_UNDO(undo, redo, m_guide ? i18n("Import guides") : i18n("Import markers")); } return result; } bool MarkerListModel::importFromJson(const QString &data, bool ignoreConflicts, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); auto json = QJsonDocument::fromJson(data.toUtf8()); if (!json.isArray()) { qDebug() << "Error : Json file should be an array"; return false; } auto list = json.array(); for (const auto &entry : list) { if (!entry.isObject()) { qDebug() << "Warning : Skipping invalid marker data"; continue; } auto entryObj = entry.toObject(); if (!entryObj.contains(QLatin1String("pos"))) { qDebug() << "Warning : Skipping invalid marker data (does not contain position)"; continue; } int pos = entryObj[QLatin1String("pos")].toInt(); QString comment = entryObj[QLatin1String("comment")].toString(i18n("Marker")); int type = entryObj[QLatin1String("type")].toInt(0); if (type < 0 || type >= (int)markerTypes.size()) { qDebug() << "Warning : invalid type found:" << type << " Defaulting to 0"; type = 0; } bool res = true; if (!ignoreConflicts && m_markerList.count(GenTime(pos, pCore->getCurrentFps())) > 0) { // potential conflict found, checking QString oldComment = m_markerList[GenTime(pos, pCore->getCurrentFps())].first; int oldType = m_markerList[GenTime(pos, pCore->getCurrentFps())].second; res = (oldComment == comment) && (type == oldType); } qDebug() << "// ADDING MARKER AT POS: " << pos << ", FPS: " << pCore->getCurrentFps(); res = res && addMarker(GenTime(pos, pCore->getCurrentFps()), comment, type, undo, redo); if (!res) { bool undone = undo(); Q_ASSERT(undone); return false; } } return true; } QString MarkerListModel::toJson() const { READ_LOCK(); QJsonArray list; for (const auto &marker : m_markerList) { QJsonObject currentMarker; currentMarker.insert(QLatin1String("pos"), QJsonValue(marker.first.frames(pCore->getCurrentFps()))); currentMarker.insert(QLatin1String("comment"), QJsonValue(marker.second.first)); currentMarker.insert(QLatin1String("type"), QJsonValue(marker.second.second)); list.push_back(currentMarker); } QJsonDocument json(list); return QString(json.toJson()); } bool MarkerListModel::removeAllMarkers() { QWriteLocker locker(&m_lock); std::vector all_pos; Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; for (const auto &m : m_markerList) { all_pos.push_back(m.first); } bool res = true; for (const auto &p : all_pos) { res = removeMarker(p, local_undo, local_redo); if (!res) { bool undone = local_undo(); Q_ASSERT(undone); return false; } } PUSH_UNDO(local_undo, local_redo, m_guide ? i18n("Delete all guides") : i18n("Delete all markers")); return true; } bool MarkerListModel::editMarkerGui(const GenTime &pos, QWidget *parent, bool createIfNotFound, ClipController *clip) { bool exists; auto marker = getMarker(pos, &exists); Q_ASSERT(exists || createIfNotFound); if (!exists && createIfNotFound) { marker = CommentedTime(pos, QString()); } QScopedPointer dialog( new MarkerDialog(clip, marker, pCore->bin()->projectTimecode(), m_guide ? i18n("Edit guide") : i18n("Edit marker"), parent)); if (dialog->exec() == QDialog::Accepted) { marker = dialog->newMarker(); if (exists) { return editMarker(pos, marker.time(), marker.comment(), marker.markerType()); } return addMarker(marker.time(), marker.comment(), marker.markerType()); } return false; } diff --git a/src/bin/model/markerlistmodel.hpp b/src/bin/model/markerlistmodel.hpp index 2b5d1f7e3..f1daa955e 100644 --- a/src/bin/model/markerlistmodel.hpp +++ b/src/bin/model/markerlistmodel.hpp @@ -1,187 +1,187 @@ /*************************************************************************** * Copyright (C) 2017 by Nicolas Carion * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #ifndef MARKERLISTMODEL_H #define MARKERLISTMODEL_H #include "definitions.h" #include "gentime.h" #include "undohelper.hpp" #include #include #include #include #include class ClipController; class DocUndoStack; class SnapModel; /* @brief This class is the model for a list of markers. A marker is defined by a time, a type (the color used to represent it) and a comment string. We store them in a sorted fashion using a std::map A marker is essentially bound to a clip. We can also define guides, that are timeline-wise markers. For that, use the constructors without clipId */ class MarkerListModel : public QAbstractListModel { Q_OBJECT public: /* @brief Construct a marker list bound to the bin clip with given id */ explicit MarkerListModel(const QString &clipId, std::weak_ptr undo_stack, QObject *parent = nullptr); /* @brief Construct a guide list (bound to the timeline) */ MarkerListModel(std::weak_ptr undo_stack, QObject *parent = nullptr); enum { CommentRole = Qt::UserRole + 1, PosRole, FrameRole, ColorRole, TypeRole }; /* @brief Adds a marker at the given position. If there is already one, the comment will be overridden @param pos defines the position of the marker, relative to the clip @param comment is the text associated with the marker @param type is the type (color) associated with the marker. If -1 is passed, then the value is pulled from kdenlive's defaults */ bool addMarker(GenTime pos, const QString &comment, int type = -1); protected: /* @brief Same function but accumulates undo/redo */ bool addMarker(GenTime pos, const QString &comment, int type, Fun &undo, Fun &redo); public: /* @brief Removes the marker at the given position. */ bool removeMarker(GenTime pos); /* @brief Delete all the markers of the model */ bool removeAllMarkers(); protected: /* @brief Same function but accumulates undo/redo */ bool removeMarker(GenTime pos, Fun &undo, Fun &redo); public: /* @brief Edit a marker @param oldPos is the old position of the marker @param pos defines the new position of the marker, relative to the clip @param comment is the text associated with the marker @param type is the type (color) associated with the marker. If -1 is passed, then the value is pulled from kdenlive's defaults */ bool editMarker(GenTime oldPos, GenTime pos, QString comment = QString(), int type = -1); /* @brief This describes the available markers type and their corresponding colors */ static std::array markerTypes; /* @brief Returns a marker data at given pos */ CommentedTime getMarker(const GenTime &pos, bool *ok) const; /* @brief Returns all markers in model */ QList getAllMarkers() const; /* @brief Returns true if a marker exists at given pos Notice that add/remove queries are done in real time (gentime), but this request is made in frame */ Q_INVOKABLE bool hasMarker(int frame) const; /* @brief Registers a snapModel to the marker model. This is intended to be used for a guide model, so that the timelines can register their snapmodel to be updated when the guide moves. This is also used on the clip monitor to keep tracking the clip markers The snap logic for clips is managed from the Timeline Note that no deregistration is necessary, the weak_ptr will be discarded as soon as it becomes invalid. */ - void registerSnapModel(std::weak_ptr snapModel); + void registerSnapModel(const std::weak_ptr &snapModel); /* @brief Exports the model to json using format above */ QString toJson() const; /* @brief Shows a dialog to edit a marker/guide @param pos: position of the marker to edit, or new position for a marker @param widget: qt widget that will be the parent of the dialog @param createIfNotFound: if true, we create a marker if none is found at pos @param clip: pointer to the clip if we are editing a marker @return true if dialog was accepted and modification successful */ bool editMarkerGui(const GenTime &pos, QWidget *parent, bool createIfNotFound, ClipController *clip = nullptr); // Mandatory overloads QVariant data(const QModelIndex &index, int role) const override; QHash roleNames() const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override; public slots: /* @brief Imports a list of markers from json data The data should be formatted as follows: [{"pos":0.2, "comment":"marker 1", "type":1}, {...}, ...] return true on success and logs undo object @param ignoreConflicts: if set to false, it aborts if the data contains a marker with same position but different comment and/or type. If set to true, such markers are overridden silently @param pushUndo: if true, create an undo object */ bool importFromJson(const QString &data, bool ignoreConflicts, bool pushUndo = true); bool importFromJson(const QString &data, bool ignoreConflicts, Fun &undo, Fun &redo); protected: /* @brief Adds a snap point at marker position in the registered snap models (those that are still valid)*/ void addSnapPoint(GenTime pos); /* @brief Deletes a snap point at marker position in the registered snap models (those that are still valid)*/ void removeSnapPoint(GenTime pos); /** @brief Helper function that generate a lambda to change comment / type of given marker */ Fun changeComment_lambda(GenTime pos, const QString &comment, int type); /** @brief Helper function that generate a lambda to add given marker */ Fun addMarker_lambda(GenTime pos, const QString &comment, int type); /** @brief Helper function that generate a lambda to remove given marker */ Fun deleteMarker_lambda(GenTime pos); /** @brief Helper function that retrieves a pointer to the markermodel, given whether it's a guide model and its clipId*/ static std::shared_ptr getModel(bool guide, const QString &clipId); /* @brief Connects the signals of this object */ void setup(); private: std::weak_ptr m_undoStack; bool m_guide; // whether this model represents timeline-wise guides QString m_clipId; // the Id of the clip this model corresponds to, if any. mutable QReadWriteLock m_lock; // This is a lock that ensures safety in case of concurrent access std::map> m_markerList; std::vector> m_registeredSnaps; signals: void modelChanged(); public: // this is to enable for range loops auto begin() -> decltype(m_markerList.begin()) { return m_markerList.begin(); } auto end() -> decltype(m_markerList.end()) { return m_markerList.end(); } }; Q_DECLARE_METATYPE(MarkerListModel *) #endif diff --git a/src/bin/projectclip.cpp b/src/bin/projectclip.cpp index 3890cbc0c..6761f692d 100644 --- a/src/bin/projectclip.cpp +++ b/src/bin/projectclip.cpp @@ -1,1431 +1,1431 @@ /* Copyright (C) 2012 Till Theato Copyright (C) 2014 Jean-Baptiste Mardelle This file is part of Kdenlive. See www.kdenlive.org. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "projectclip.h" #include "bin.h" #include "core.h" #include "doc/docundostack.hpp" #include "doc/kdenlivedoc.h" #include "doc/kthumb.h" #include "effects/effectstack/model/effectstackmodel.hpp" #include "jobs/jobmanager.h" #include "jobs/loadjob.hpp" #include "jobs/thumbjob.hpp" #include "kdenlivesettings.h" #include "lib/audio/audioStreamInfo.h" #include "mltcontroller/clipcontroller.h" #include "mltcontroller/clippropertiescontroller.h" #include "model/markerlistmodel.hpp" #include "profiles/profilemodel.hpp" #include "project/projectcommands.h" #include "project/projectmanager.h" #include "projectfolder.h" #include "projectitemmodel.h" #include "projectsubclip.h" #include "timecode.h" #include "timeline2/model/snapmodel.hpp" #include "utils/thumbnailcache.hpp" #include "xml/xml.hpp" #include #include #include #include "kdenlive_debug.h" #include "logger.hpp" #include #include #include #include #include #include #include #include #include #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-parameter" #pragma GCC diagnostic ignored "-Wsign-conversion" #pragma GCC diagnostic ignored "-Wfloat-equal" #pragma GCC diagnostic ignored "-Wshadow" #pragma GCC diagnostic ignored "-Wpedantic" #include #pragma GCC diagnostic pop RTTR_REGISTRATION { using namespace rttr; registration::class_("ProjectClip"); } -ProjectClip::ProjectClip(const QString &id, const QIcon &thumb, std::shared_ptr model, std::shared_ptr producer) +ProjectClip::ProjectClip(const QString &id, const QIcon &thumb, const std::shared_ptr &model, std::shared_ptr producer) : AbstractProjectItem(AbstractProjectItem::ClipItem, id, model) - , ClipController(id, producer) + , ClipController(id, std::move(producer)) , m_thumbsProducer(nullptr) { m_markerModel = std::make_shared(id, pCore->projectManager()->undoStack()); m_clipStatus = StatusReady; m_name = clipName(); m_duration = getStringDuration(); m_inPoint = 0; m_date = date; m_description = ClipController::description(); if (m_clipType == ClipType::Audio) { m_thumbnail = QIcon::fromTheme(QStringLiteral("audio-x-generic")); } else { m_thumbnail = thumb; } // Make sure we have a hash for this clip hash(); connect(m_markerModel.get(), &MarkerListModel::modelChanged, [&]() { setProducerProperty(QStringLiteral("kdenlive:markers"), m_markerModel->toJson()); }); QString markers = getProducerProperty(QStringLiteral("kdenlive:markers")); if (!markers.isEmpty()) { QMetaObject::invokeMethod(m_markerModel.get(), "importFromJson", Qt::QueuedConnection, Q_ARG(const QString &, markers), Q_ARG(bool, true), Q_ARG(bool, false)); } connectEffectStack(); } // static -std::shared_ptr ProjectClip::construct(const QString &id, const QIcon &thumb, std::shared_ptr model, - std::shared_ptr producer) +std::shared_ptr ProjectClip::construct(const QString &id, const QIcon &thumb, const std::shared_ptr &model, + const std::shared_ptr &producer) { std::shared_ptr self(new ProjectClip(id, thumb, model, producer)); baseFinishConstruct(self); self->m_effectStack->importEffects(producer, PlaylistState::Disabled, true); model->loadSubClips(id, self->getPropertiesFromPrefix(QStringLiteral("kdenlive:clipzone."))); return self; } -ProjectClip::ProjectClip(const QString &id, const QDomElement &description, const QIcon &thumb, std::shared_ptr model) +ProjectClip::ProjectClip(const QString &id, const QDomElement &description, const QIcon &thumb, const std::shared_ptr &model) : AbstractProjectItem(AbstractProjectItem::ClipItem, id, model) , ClipController(id) , m_thumbsProducer(nullptr) { m_clipStatus = StatusWaiting; m_thumbnail = thumb; m_markerModel = std::make_shared(m_binId, pCore->projectManager()->undoStack()); if (description.hasAttribute(QStringLiteral("type"))) { m_clipType = (ClipType::ProducerType)description.attribute(QStringLiteral("type")).toInt(); if (m_clipType == ClipType::Audio) { m_thumbnail = QIcon::fromTheme(QStringLiteral("audio-x-generic")); } } m_temporaryUrl = getXmlProperty(description, QStringLiteral("resource")); QString clipName = getXmlProperty(description, QStringLiteral("kdenlive:clipname")); if (!clipName.isEmpty()) { m_name = clipName; } else if (!m_temporaryUrl.isEmpty()) { m_name = QFileInfo(m_temporaryUrl).fileName(); } else { m_name = i18n("Untitled"); } connect(m_markerModel.get(), &MarkerListModel::modelChanged, [&]() { setProducerProperty(QStringLiteral("kdenlive:markers"), m_markerModel->toJson()); }); } std::shared_ptr ProjectClip::construct(const QString &id, const QDomElement &description, const QIcon &thumb, std::shared_ptr model) { - std::shared_ptr self(new ProjectClip(id, description, thumb, model)); + std::shared_ptr self(new ProjectClip(id, description, thumb, std::move(model))); baseFinishConstruct(self); return self; } ProjectClip::~ProjectClip() { // controller is deleted in bincontroller m_thumbMutex.lock(); m_requestedThumbs.clear(); m_thumbMutex.unlock(); m_thumbThread.waitForFinished(); audioFrameCache.clear(); } void ProjectClip::connectEffectStack() { connect(m_effectStack.get(), &EffectStackModel::modelChanged, this, &ProjectClip::updateChildProducers); connect(m_effectStack.get(), &EffectStackModel::dataChanged, this, &ProjectClip::updateChildProducers); connect(m_effectStack.get(), &EffectStackModel::dataChanged, [&]() { if (auto ptr = m_model.lock()) { std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this()), AbstractProjectItem::IconOverlay); } }); /*connect(m_effectStack.get(), &EffectStackModel::modelChanged, [&](){ qDebug()<<"/ / / STACK CHANGED"; updateChildProducers(); });*/ } QString ProjectClip::getToolTip() const { return url(); } QString ProjectClip::getXmlProperty(const QDomElement &producer, const QString &propertyName, const QString &defaultValue) { QString value = defaultValue; QDomNodeList props = producer.elementsByTagName(QStringLiteral("property")); for (int i = 0; i < props.count(); ++i) { if (props.at(i).toElement().attribute(QStringLiteral("name")) == propertyName) { value = props.at(i).firstChild().nodeValue(); break; } } return value; } void ProjectClip::updateAudioThumbnail(QVariantList audioLevels) { std::swap(audioFrameCache, audioLevels); // avoid second copy m_audioThumbCreated = true; if (auto ptr = m_model.lock()) { emit std::static_pointer_cast(ptr)->refreshAudioThumbs(m_binId); } updateTimelineClips({TimelineModel::AudioLevelsRole}); } bool ProjectClip::audioThumbCreated() const { return (m_audioThumbCreated); } ClipType::ProducerType ProjectClip::clipType() const { return m_clipType; } bool ProjectClip::hasParent(const QString &id) const { std::shared_ptr par = parent(); while (par) { if (par->clipId() == id) { return true; } par = par->parent(); } return false; } std::shared_ptr ProjectClip::clip(const QString &id) { if (id == m_binId) { return std::static_pointer_cast(shared_from_this()); } return std::shared_ptr(); } std::shared_ptr ProjectClip::folder(const QString &id) { Q_UNUSED(id) return std::shared_ptr(); } std::shared_ptr ProjectClip::getSubClip(int in, int out) { for (int i = 0; i < childCount(); ++i) { std::shared_ptr clip = std::static_pointer_cast(child(i))->subClip(in, out); if (clip) { return clip; } } return std::shared_ptr(); } QStringList ProjectClip::subClipIds() const { QStringList subIds; for (int i = 0; i < childCount(); ++i) { std::shared_ptr clip = std::static_pointer_cast(child(i)); if (clip) { subIds << clip->clipId(); } } return subIds; } std::shared_ptr ProjectClip::clipAt(int ix) { if (ix == row()) { return std::static_pointer_cast(shared_from_this()); } return std::shared_ptr(); } /*bool ProjectClip::isValid() const { return m_controller->isValid(); }*/ bool ProjectClip::hasUrl() const { if ((m_clipType != ClipType::Color) && (m_clipType != ClipType::Unknown)) { return (!clipUrl().isEmpty()); } return false; } const QString ProjectClip::url() const { return clipUrl(); } GenTime ProjectClip::duration() const { return getPlaytime(); } size_t ProjectClip::frameDuration() const { GenTime d = duration(); return (size_t)d.frames(pCore->getCurrentFps()); } void ProjectClip::reloadProducer(bool refreshOnly) { // we find if there are some loading job on that clip int loadjobId = -1; pCore->jobManager()->hasPendingJob(clipId(), AbstractClipJob::LOADJOB, &loadjobId); QMutexLocker lock(&m_thumbMutex); if (refreshOnly) { // In that case, we only want a new thumbnail. // We thus set up a thumb job. We must make sure that there is no pending LOADJOB // Clear cache first ThumbnailCache::get()->invalidateThumbsForClip(clipId()); pCore->jobManager()->discardJobs(clipId(), AbstractClipJob::THUMBJOB); m_thumbsProducer.reset(); pCore->jobManager()->startJob({clipId()}, loadjobId, QString(), 150, -1, true, true); } else { // If another load job is running? if (loadjobId > -1) { pCore->jobManager()->discardJobs(clipId(), AbstractClipJob::LOADJOB); } QDomDocument doc; QDomElement xml = toXml(doc); if (!xml.isNull()) { pCore->jobManager()->discardJobs(clipId(), AbstractClipJob::THUMBJOB); m_thumbsProducer.reset(); ThumbnailCache::get()->invalidateThumbsForClip(clipId()); int loadJob = pCore->jobManager()->startJob({clipId()}, loadjobId, QString(), xml); pCore->jobManager()->startJob({clipId()}, loadJob, QString(), 150, -1, true, true); } } } QDomElement ProjectClip::toXml(QDomDocument &document, bool includeMeta) { getProducerXML(document, includeMeta); QDomElement prod = document.documentElement().firstChildElement(QStringLiteral("producer")); if (m_clipType != ClipType::Unknown) { prod.setAttribute(QStringLiteral("type"), (int)m_clipType); } return prod; } void ProjectClip::setThumbnail(const QImage &img) { QPixmap thumb = roundedPixmap(QPixmap::fromImage(img)); if (hasProxy() && !thumb.isNull()) { // Overlay proxy icon QPainter p(&thumb); QColor c(220, 220, 10, 200); QRect r(0, 0, thumb.height() / 2.5, thumb.height() / 2.5); p.fillRect(r, c); QFont font = p.font(); font.setPixelSize(r.height()); font.setBold(true); p.setFont(font); p.setPen(Qt::black); p.drawText(r, Qt::AlignCenter, i18nc("The first letter of Proxy, used as abbreviation", "P")); } m_thumbnail = QIcon(thumb); if (auto ptr = m_model.lock()) { std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this()), AbstractProjectItem::DataThumbnail); } } bool ProjectClip::hasAudioAndVideo() const { return hasAudio() && hasVideo() && m_masterProducer->get_int("set.test_image") == 0 && m_masterProducer->get_int("set.test_audio") == 0; } bool ProjectClip::isCompatible(PlaylistState::ClipState state) const { switch (state) { case PlaylistState::AudioOnly: return hasAudio() && (m_masterProducer->get_int("set.test_audio") == 0); case PlaylistState::VideoOnly: return hasVideo() && (m_masterProducer->get_int("set.test_image") == 0); default: return true; } } QPixmap ProjectClip::thumbnail(int width, int height) { return m_thumbnail.pixmap(width, height); } bool ProjectClip::setProducer(std::shared_ptr producer, bool replaceProducer) { Q_UNUSED(replaceProducer) qDebug() << "################### ProjectClip::setproducer"; QMutexLocker locker(&m_producerMutex); - updateProducer(std::move(producer)); + updateProducer(producer); m_thumbsProducer.reset(); connectEffectStack(); // Update info if (m_name.isEmpty()) { m_name = clipName(); } m_date = date; m_description = ClipController::description(); m_temporaryUrl.clear(); if (m_clipType == ClipType::Audio) { m_thumbnail = QIcon::fromTheme(QStringLiteral("audio-x-generic")); } else if (m_clipType == ClipType::Image) { if (producer->get_int("meta.media.width") < 8 || producer->get_int("meta.media.height") < 8) { KMessageBox::information(QApplication::activeWindow(), i18n("Image dimension smaller than 8 pixels.\nThis is not correctly supported by our video framework.")); } } m_duration = getStringDuration(); m_clipStatus = StatusReady; if (!hasProxy()) { if (auto ptr = m_model.lock()) emit std::static_pointer_cast(ptr)->refreshPanel(m_binId); } if (auto ptr = m_model.lock()) { std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this()), AbstractProjectItem::DataDuration); std::static_pointer_cast(ptr)->updateWatcher(std::static_pointer_cast(shared_from_this())); } // Make sure we have a hash for this clip getFileHash(); // set parent again (some info need to be stored in producer) updateParent(parentItem().lock()); if (pCore->currentDoc()->getDocumentProperty(QStringLiteral("enableproxy")).toInt() == 1) { QList> clipList; // automatic proxy generation enabled if (m_clipType == ClipType::Image && pCore->currentDoc()->getDocumentProperty(QStringLiteral("generateimageproxy")).toInt() == 1) { if (getProducerIntProperty(QStringLiteral("meta.media.width")) >= KdenliveSettings::proxyimageminsize() && getProducerProperty(QStringLiteral("kdenlive:proxy")) == QStringLiteral()) { clipList << std::static_pointer_cast(shared_from_this()); } } else if (pCore->currentDoc()->getDocumentProperty(QStringLiteral("generateproxy")).toInt() == 1 && (m_clipType == ClipType::AV || m_clipType == ClipType::Video) && getProducerProperty(QStringLiteral("kdenlive:proxy")) == QStringLiteral()) { bool skipProducer = false; if (pCore->currentDoc()->getDocumentProperty(QStringLiteral("enableexternalproxy")).toInt() == 1) { QStringList externalParams = pCore->currentDoc()->getDocumentProperty(QStringLiteral("externalproxyparams")).split(QLatin1Char(';')); // We have a camcorder profile, check if we have opened a proxy clip if (externalParams.count() >= 6) { QFileInfo info(m_path); QDir dir = info.absoluteDir(); dir.cd(externalParams.at(3)); QString fileName = info.fileName(); if (!externalParams.at(2).isEmpty()) { fileName.chop(externalParams.at(2).size()); } fileName.append(externalParams.at(5)); if (dir.exists(fileName)) { setProducerProperty(QStringLiteral("kdenlive:proxy"), m_path); m_path = dir.absoluteFilePath(fileName); setProducerProperty(QStringLiteral("kdenlive:originalurl"), m_path); getFileHash(); skipProducer = true; } } } if (!skipProducer && getProducerIntProperty(QStringLiteral("meta.media.width")) >= KdenliveSettings::proxyminsize()) { clipList << std::static_pointer_cast(shared_from_this()); } } if (!clipList.isEmpty()) { pCore->currentDoc()->slotProxyCurrentItem(true, clipList, false); } } pCore->bin()->reloadMonitorIfActive(clipId()); for (auto &p : m_audioProducers) { m_effectStack->removeService(p.second); } for (auto &p : m_videoProducers) { m_effectStack->removeService(p.second); } for (auto &p : m_timewarpProducers) { m_effectStack->removeService(p.second); } // Release audio producers m_audioProducers.clear(); m_videoProducers.clear(); m_timewarpProducers.clear(); emit refreshPropertiesPanel(); replaceInTimeline(); return true; } std::shared_ptr ProjectClip::thumbProducer() { if (m_thumbsProducer) { return m_thumbsProducer; } if (clipType() == ClipType::Unknown) { return nullptr; } QMutexLocker lock(&m_thumbMutex); std::shared_ptr prod = originalProducer(); if (!prod->is_valid()) { return nullptr; } if (KdenliveSettings::gpu_accel()) { // TODO: when the original producer changes, we must reload this thumb producer m_thumbsProducer = softClone(ClipController::getPassPropertiesList()); Mlt::Filter converter(*prod->profile(), "avcolor_space"); m_thumbsProducer->attach(converter); } else { QString mltService = m_masterProducer->get("mlt_service"); const QString mltResource = m_masterProducer->get("resource"); if (mltService == QLatin1String("avformat")) { mltService = QStringLiteral("avformat-novalidate"); } m_thumbsProducer.reset(new Mlt::Producer(*pCore->thumbProfile(), mltService.toUtf8().constData(), mltResource.toUtf8().constData())); if (m_thumbsProducer->is_valid()) { Mlt::Properties original(m_masterProducer->get_properties()); Mlt::Properties cloneProps(m_thumbsProducer->get_properties()); cloneProps.pass_list(original, ClipController::getPassPropertiesList()); Mlt::Filter scaler(*pCore->thumbProfile(), "swscale"); Mlt::Filter padder(*pCore->thumbProfile(), "resize"); Mlt::Filter converter(*pCore->thumbProfile(), "avcolor_space"); m_thumbsProducer->set("audio_index", -1); m_thumbsProducer->attach(scaler); m_thumbsProducer->attach(padder); m_thumbsProducer->attach(converter); } } return m_thumbsProducer; } void ProjectClip::createDisabledMasterProducer() { if (!m_disabledProducer) { m_disabledProducer = cloneProducer(); m_disabledProducer->set("set.test_audio", 1); m_disabledProducer->set("set.test_image", 1); m_effectStack->addService(m_disabledProducer); } } std::shared_ptr ProjectClip::getTimelineProducer(int clipId, PlaylistState::ClipState state, double speed) { if (!m_masterProducer) { return nullptr; } if (qFuzzyCompare(speed, 1.0)) { // we are requesting a normal speed producer // We can first cleen the speed producers we have for the current id if (m_timewarpProducers.count(clipId) > 0) { m_effectStack->removeService(m_timewarpProducers[clipId]); m_timewarpProducers.erase(clipId); } if (state == PlaylistState::AudioOnly) { // We need to get an audio producer, if none exists if (m_audioProducers.count(clipId) == 0) { m_audioProducers[clipId] = cloneProducer(true); m_audioProducers[clipId]->set("set.test_audio", 0); m_audioProducers[clipId]->set("set.test_image", 1); m_effectStack->addService(m_audioProducers[clipId]); } return std::shared_ptr(m_audioProducers[clipId]->cut()); } if (m_audioProducers.count(clipId) > 0) { m_effectStack->removeService(m_audioProducers[clipId]); m_audioProducers.erase(clipId); } if (state == PlaylistState::VideoOnly) { // we return the video producer // We need to get an audio producer, if none exists if (m_clipType == ClipType::Color || m_clipType == ClipType::Image || m_clipType == ClipType::Text) { int duration = m_masterProducer->time_to_frames(m_masterProducer->get("kdenlive:duration")); return std::shared_ptr(m_masterProducer->cut(-1, duration > 0 ? duration : -1)); } if (m_videoProducers.count(clipId) == 0) { m_videoProducers[clipId] = cloneProducer(true); m_videoProducers[clipId]->set("set.test_audio", 1); m_videoProducers[clipId]->set("set.test_image", 0); m_effectStack->addService(m_videoProducers[clipId]); } int duration = m_masterProducer->time_to_frames(m_masterProducer->get("kdenlive:duration")); return std::shared_ptr(m_videoProducers[clipId]->cut(-1, duration > 0 ? duration : -1)); } if (m_videoProducers.count(clipId) > 0) { m_effectStack->removeService(m_videoProducers[clipId]); m_videoProducers.erase(clipId); } Q_ASSERT(state == PlaylistState::Disabled); createDisabledMasterProducer(); int duration = m_masterProducer->time_to_frames(m_masterProducer->get("kdenlive:duration")); return std::shared_ptr(m_disabledProducer->cut(-1, duration > 0 ? duration : -1)); } // in that case, we need to create a warp producer, if we don't have one if (m_audioProducers.count(clipId) > 0) { m_effectStack->removeService(m_audioProducers[clipId]); m_audioProducers.erase(clipId); } if (m_videoProducers.count(clipId) > 0) { m_effectStack->removeService(m_videoProducers[clipId]); m_videoProducers.erase(clipId); } std::shared_ptr warpProducer; if (m_timewarpProducers.count(clipId) > 0) { // remove in all cases, we add it unconditionally anyways m_effectStack->removeService(m_timewarpProducers[clipId]); if (qFuzzyCompare(m_timewarpProducers[clipId]->get_double("warp_speed"), speed)) { // the producer we have is good, use it ! warpProducer = m_timewarpProducers[clipId]; qDebug() << "Reusing producer!"; } else { m_timewarpProducers.erase(clipId); } } if (!warpProducer) { QLocale locale; QString resource(originalProducer()->get("resource")); if (resource.isEmpty() || resource == QLatin1String("")) { resource = m_service; } QString url = QString("timewarp:%1:%2").arg(locale.toString(speed)).arg(resource); warpProducer.reset(new Mlt::Producer(*originalProducer()->profile(), url.toUtf8().constData())); qDebug() << "new producer: " << url; qDebug() << "warp LENGTH before" << warpProducer->get_length(); int original_length = originalProducer()->get_length(); // this is a workaround to cope with Mlt erroneous rounding warpProducer->set("length", double(original_length) / speed); } qDebug() << "warp LENGTH" << warpProducer->get_length(); warpProducer->set("set.test_audio", 1); warpProducer->set("set.test_image", 1); if (state == PlaylistState::AudioOnly) { warpProducer->set("set.test_audio", 0); } if (state == PlaylistState::VideoOnly) { warpProducer->set("set.test_image", 0); } m_timewarpProducers[clipId] = warpProducer; m_effectStack->addService(m_timewarpProducers[clipId]); return std::shared_ptr(warpProducer->cut()); } std::pair, bool> ProjectClip::giveMasterAndGetTimelineProducer(int clipId, std::shared_ptr master, PlaylistState::ClipState state) { int in = master->get_in(); int out = master->get_out(); if (master->parent().is_valid()) { // in that case, we have a cut // check whether it's a timewarp double speed = 1.0; bool timeWarp = false; if (QString::fromUtf8(master->parent().get("mlt_service")) == QLatin1String("timewarp")) { speed = master->parent().get_double("warp_speed"); timeWarp = true; } if (master->parent().get_int("_loaded") == 1) { // we already have a clip that shares the same master if (state != PlaylistState::Disabled || timeWarp) { // In that case, we must create copies std::shared_ptr prod(getTimelineProducer(clipId, state, speed)->cut(in, out)); return {prod, false}; } if (state == PlaylistState::Disabled && !m_disabledProducer) { qDebug() << "Warning: weird, we found a disabled clip whose master is already loaded but we don't have any yet"; createDisabledMasterProducer(); return {std::shared_ptr(m_disabledProducer->cut(in, out)), false}; } if (state == PlaylistState::Disabled && QString::fromUtf8(m_disabledProducer->get("id")) != QString::fromUtf8(master->parent().get("id"))) { qDebug() << "Warning: weird, we found a disabled clip whose master is already loaded but doesn't match ours"; return {std::shared_ptr(m_disabledProducer->cut(in, out)), false}; } // We have a good id, this clip can be used return {master, true}; } else { master->parent().set("_loaded", 1); if (timeWarp) { m_timewarpProducers[clipId] = std::shared_ptr(new Mlt::Producer(&master->parent())); m_effectStack->loadService(m_timewarpProducers[clipId]); return {master, true}; } if (state == PlaylistState::AudioOnly) { m_audioProducers[clipId] = std::shared_ptr(new Mlt::Producer(&master->parent())); m_effectStack->loadService(m_audioProducers[clipId]); return {master, true}; } if (state == PlaylistState::VideoOnly) { // good, we found a master video producer, and we didn't have any m_videoProducers[clipId] = std::shared_ptr(new Mlt::Producer(&master->parent())); m_effectStack->loadService(m_videoProducers[clipId]); return {master, true}; } if (state == PlaylistState::Disabled && !m_disabledProducer) { // good, we found a master disabled producer, and we didn't have any m_disabledProducer.reset(master->parent().cut()); m_effectStack->loadService(m_disabledProducer); return {master, true}; } qDebug() << "Warning: weird, we found a clip whose master is not loaded but we already have a master"; Q_ASSERT(false); } } else if (master->is_valid()) { // in that case, we have a master qDebug() << "Warning: weird, we received a master clip in lieue of a cut"; double speed = 1.0; if (QString::fromUtf8(master->parent().get("mlt_service")) == QLatin1String("timewarp")) { speed = master->get_double("warp_speed"); } return {getTimelineProducer(clipId, state, speed), false}; } // we have a problem return {std::shared_ptr(ClipController::mediaUnavailable->cut()), false}; } /* std::shared_ptr ProjectClip::timelineProducer(PlaylistState::ClipState state, int track) { if (!m_service.startsWith(QLatin1String("avformat"))) { std::shared_ptr prod(originalProducer()->cut()); int length = getProducerIntProperty(QStringLiteral("kdenlive:duration")); if (length > 0) { prod->set_in_and_out(0, length); } return prod; } if (state == PlaylistState::VideoOnly) { if (m_timelineProducers.count(0) > 0) { return std::shared_ptr(m_timelineProducers.find(0)->second->cut()); } std::shared_ptr videoProd = cloneProducer(); videoProd->set("audio_index", -1); m_timelineProducers[0] = videoProd; return std::shared_ptr(videoProd->cut()); } if (state == PlaylistState::AudioOnly) { if (m_timelineProducers.count(-track) > 0) { return std::shared_ptr(m_timelineProducers.find(-track)->second->cut()); } std::shared_ptr audioProd = cloneProducer(); audioProd->set("video_index", -1); m_timelineProducers[-track] = audioProd; return std::shared_ptr(audioProd->cut()); } if (m_timelineProducers.count(track) > 0) { return std::shared_ptr(m_timelineProducers.find(track)->second->cut()); } std::shared_ptr normalProd = cloneProducer(); m_timelineProducers[track] = normalProd; return std::shared_ptr(normalProd->cut()); }*/ std::shared_ptr ProjectClip::cloneProducer(bool removeEffects) { Mlt::Consumer c(pCore->getCurrentProfile()->profile(), "xml", "string"); Mlt::Service s(m_masterProducer->get_service()); int ignore = s.get_int("ignore_points"); if (ignore) { s.set("ignore_points", 0); } c.connect(s); c.set("time_format", "frames"); c.set("no_meta", 1); c.set("no_root", 1); c.set("no_profile", 1); c.set("root", "/"); c.set("store", "kdenlive"); c.run(); if (ignore) { s.set("ignore_points", ignore); } const QByteArray clipXml = c.get("string"); std::shared_ptr prod; prod.reset(new Mlt::Producer(pCore->getCurrentProfile()->profile(), "xml-string", clipXml.constData())); if (strcmp(prod->get("mlt_service"), "avformat") == 0) { prod->set("mlt_service", "avformat-novalidate"); } if (removeEffects) { int ct = 0; Mlt::Filter *filter = prod->filter(ct); while (filter) { qDebug() << "// EFFECT " << ct << " : " << filter->get("mlt_service"); QString ix = QString::fromLatin1(filter->get("kdenlive_id")); if (!ix.isEmpty()) { qDebug() << "/ + + DELTING"; if (prod->detach(*filter) == 0) { } else { ct++; } } else { ct++; } delete filter; filter = prod->filter(ct); } } prod->set("id", (char *)nullptr); return prod; } -std::shared_ptr ProjectClip::cloneProducer(std::shared_ptr producer) +std::shared_ptr ProjectClip::cloneProducer(const std::shared_ptr &producer) { Mlt::Consumer c(*producer->profile(), "xml", "string"); Mlt::Service s(producer->get_service()); int ignore = s.get_int("ignore_points"); if (ignore) { s.set("ignore_points", 0); } c.connect(s); c.set("time_format", "frames"); c.set("no_meta", 1); c.set("no_root", 1); c.set("no_profile", 1); c.set("root", "/"); c.set("store", "kdenlive"); c.start(); if (ignore) { s.set("ignore_points", ignore); } const QByteArray clipXml = c.get("string"); std::shared_ptr prod(new Mlt::Producer(*producer->profile(), "xml-string", clipXml.constData())); if (strcmp(prod->get("mlt_service"), "avformat") == 0) { prod->set("mlt_service", "avformat-novalidate"); } return prod; } std::shared_ptr ProjectClip::softClone(const char *list) { QString service = QString::fromLatin1(m_masterProducer->get("mlt_service")); QString resource = QString::fromLatin1(m_masterProducer->get("resource")); std::shared_ptr clone(new Mlt::Producer(*m_masterProducer->profile(), service.toUtf8().constData(), resource.toUtf8().constData())); Mlt::Properties original(m_masterProducer->get_properties()); Mlt::Properties cloneProps(clone->get_properties()); cloneProps.pass_list(original, list); return clone; } bool ProjectClip::isReady() const { return m_clipStatus == StatusReady; } QPoint ProjectClip::zone() const { return ClipController::zone(); } const QString ProjectClip::hash() { QString clipHash = getProducerProperty(QStringLiteral("kdenlive:file_hash")); if (!clipHash.isEmpty()) { return clipHash; } return getFileHash(); } const QString ProjectClip::getFileHash() { QByteArray fileData; QByteArray fileHash; switch (m_clipType) { case ClipType::SlideShow: fileData = clipUrl().toUtf8(); fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5); break; case ClipType::Text: case ClipType::TextTemplate: fileData = getProducerProperty(QStringLiteral("xmldata")).toUtf8(); fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5); break; case ClipType::QText: fileData = getProducerProperty(QStringLiteral("text")).toUtf8(); fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5); break; case ClipType::Color: fileData = getProducerProperty(QStringLiteral("resource")).toUtf8(); fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5); break; default: QFile file(clipUrl()); if (file.open(QIODevice::ReadOnly)) { // write size and hash only if resource points to a file /* * 1 MB = 1 second per 450 files (or faster) * 10 MB = 9 seconds per 450 files (or faster) */ if (file.size() > 2000000) { fileData = file.read(1000000); if (file.seek(file.size() - 1000000)) { fileData.append(file.readAll()); } } else { fileData = file.readAll(); } file.close(); ClipController::setProducerProperty(QStringLiteral("kdenlive:file_size"), QString::number(file.size())); fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5); } break; } if (fileHash.isEmpty()) { qDebug() << "// WARNING EMPTY CLIP HASH: "; return QString(); } QString result = fileHash.toHex(); ClipController::setProducerProperty(QStringLiteral("kdenlive:file_hash"), result); return result; } double ProjectClip::getOriginalFps() const { return originalFps(); } bool ProjectClip::hasProxy() const { QString proxy = getProducerProperty(QStringLiteral("kdenlive:proxy")); return proxy.size() > 2; } void ProjectClip::setProperties(const QMap &properties, bool refreshPanel) { qDebug() << "// SETTING CLIP PROPERTIES: " << properties; QMapIterator i(properties); QMap passProperties; bool refreshAnalysis = false; bool reload = false; bool refreshOnly = true; if (properties.contains(QStringLiteral("templatetext"))) { m_description = properties.value(QStringLiteral("templatetext")); if (auto ptr = m_model.lock()) std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this()), AbstractProjectItem::ClipStatus); refreshPanel = true; } // Some properties also need to be passed to track producers QStringList timelineProperties{QStringLiteral("force_aspect_ratio"), QStringLiteral("video_index"), QStringLiteral("audio_index"), QStringLiteral("set.force_full_luma"), QStringLiteral("full_luma"), QStringLiteral("threads"), QStringLiteral("force_colorspace"), QStringLiteral("force_tff"), QStringLiteral("force_progressive"), QStringLiteral("video_index"), QStringLiteral("audio_index")}; QStringList forceReloadProperties{QStringLiteral("autorotate"), QStringLiteral("templatetext"), QStringLiteral("resource"), QStringLiteral("force_fps"), QStringLiteral("set.test_image"), QStringLiteral("set.test_audio")}; QStringList keys{QStringLiteral("luma_duration"), QStringLiteral("luma_file"), QStringLiteral("fade"), QStringLiteral("ttl"), QStringLiteral("softness"), QStringLiteral("crop"), QStringLiteral("animation")}; QVector updateRoles; while (i.hasNext()) { i.next(); setProducerProperty(i.key(), i.value()); if (m_clipType == ClipType::SlideShow && keys.contains(i.key())) { reload = true; refreshOnly = false; } if (i.key().startsWith(QLatin1String("kdenlive:clipanalysis"))) { refreshAnalysis = true; } if (timelineProperties.contains(i.key())) { passProperties.insert(i.key(), i.value()); } } if (properties.contains(QStringLiteral("kdenlive:proxy"))) { QString value = properties.value(QStringLiteral("kdenlive:proxy")); // If value is "-", that means user manually disabled proxy on this clip if (value.isEmpty() || value == QLatin1String("-")) { // reset proxy int id; if (pCore->jobManager()->hasPendingJob(clipId(), AbstractClipJob::PROXYJOB, &id)) { // The proxy clip is being created, abort pCore->jobManager()->discardJobs(clipId(), AbstractClipJob::PROXYJOB); } else { reload = true; refreshOnly = false; } } else { // A proxy was requested, make sure to keep original url setProducerProperty(QStringLiteral("kdenlive:originalurl"), url()); pCore->jobManager()->startJob({clipId()}, -1, QString()); } } else if (!reload) { const QList propKeys = properties.keys(); for (const QString &k : propKeys) { if (forceReloadProperties.contains(k)) { if (m_clipType != ClipType::Color) { reload = true; refreshOnly = false; } else { // Clip resource changed, update thumbnail reload = true; refreshPanel = true; updateRoles << TimelineModel::ResourceRole; } break; } } } if (!reload && (properties.contains(QStringLiteral("xmldata")) || !passProperties.isEmpty())) { reload = true; } if (refreshAnalysis) { emit refreshAnalysisPanel(); } if (properties.contains(QStringLiteral("length")) || properties.contains(QStringLiteral("kdenlive:duration"))) { m_duration = getStringDuration(); if (auto ptr = m_model.lock()) std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this()), AbstractProjectItem::DataDuration); refreshOnly = false; reload = true; } if (properties.contains(QStringLiteral("kdenlive:clipname"))) { m_name = properties.value(QStringLiteral("kdenlive:clipname")); refreshPanel = true; if (auto ptr = m_model.lock()) { std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this()), AbstractProjectItem::DataName); } // update timeline clips updateTimelineClips(QVector() << TimelineModel::NameRole); } if (refreshPanel) { // Some of the clip properties have changed through a command, update properties panel emit refreshPropertiesPanel(); } if (reload) { // producer has changed, refresh monitor and thumbnail if (hasProxy()) { pCore->jobManager()->discardJobs(clipId(), AbstractClipJob::PROXYJOB); setProducerProperty(QStringLiteral("_overwriteproxy"), 1); pCore->jobManager()->startJob({clipId()}, -1, QString()); } else { reloadProducer(refreshOnly); } if (refreshOnly) { if (auto ptr = m_model.lock()) { emit std::static_pointer_cast(ptr)->refreshClip(m_binId); } } if (!updateRoles.isEmpty()) { updateTimelineClips(updateRoles); } } if (!passProperties.isEmpty()) { if (auto ptr = m_model.lock()) emit std::static_pointer_cast(ptr)->updateTimelineProducers(m_binId, passProperties); } } ClipPropertiesController *ProjectClip::buildProperties(QWidget *parent) { auto ptr = m_model.lock(); Q_ASSERT(ptr); ClipPropertiesController *panel = new ClipPropertiesController(static_cast(this), parent); connect(this, &ProjectClip::refreshPropertiesPanel, panel, &ClipPropertiesController::slotReloadProperties); connect(this, &ProjectClip::refreshAnalysisPanel, panel, &ClipPropertiesController::slotFillAnalysisData); connect(panel, &ClipPropertiesController::requestProxy, [this](bool doProxy) { QList> clipList{std::static_pointer_cast(shared_from_this())}; pCore->currentDoc()->slotProxyCurrentItem(doProxy, clipList); }); connect(panel, &ClipPropertiesController::deleteProxy, this, &ProjectClip::deleteProxy); return panel; } void ProjectClip::deleteProxy() { // Disable proxy file QString proxy = getProducerProperty(QStringLiteral("kdenlive:proxy")); QList> clipList{std::static_pointer_cast(shared_from_this())}; pCore->currentDoc()->slotProxyCurrentItem(false, clipList); // Delete bool ok; QDir dir = pCore->currentDoc()->getCacheDir(CacheProxy, &ok); if (ok && proxy.length() > 2) { proxy = QFileInfo(proxy).fileName(); if (dir.exists(proxy)) { dir.remove(proxy); } } } void ProjectClip::updateParent(std::shared_ptr parent) { if (parent) { auto item = std::static_pointer_cast(parent); ClipController::setProducerProperty(QStringLiteral("kdenlive:folderid"), item->clipId()); } AbstractProjectItem::updateParent(parent); } bool ProjectClip::matches(const QString &condition) { // TODO Q_UNUSED(condition) return true; } bool ProjectClip::rename(const QString &name, int column) { QMap newProperites; QMap oldProperites; bool edited = false; switch (column) { case 0: if (m_name == name) { return false; } // Rename clip oldProperites.insert(QStringLiteral("kdenlive:clipname"), m_name); newProperites.insert(QStringLiteral("kdenlive:clipname"), name); m_name = name; edited = true; break; case 2: if (m_description == name) { return false; } // Rename clip if (m_clipType == ClipType::TextTemplate) { oldProperites.insert(QStringLiteral("templatetext"), m_description); newProperites.insert(QStringLiteral("templatetext"), name); } else { oldProperites.insert(QStringLiteral("kdenlive:description"), m_description); newProperites.insert(QStringLiteral("kdenlive:description"), name); } m_description = name; edited = true; break; } if (edited) { pCore->bin()->slotEditClipCommand(m_binId, oldProperites, newProperites); } return edited; } QVariant ProjectClip::getData(DataType type) const { switch (type) { case AbstractProjectItem::IconOverlay: return m_effectStack && m_effectStack->rowCount() > 0 ? QVariant("kdenlive-track_has_effect") : QVariant(); default: return AbstractProjectItem::getData(type); } } void ProjectClip::slotExtractImage(const QList &frames) { QMutexLocker lock(&m_thumbMutex); for (int i = 0; i < frames.count(); i++) { if (!m_requestedThumbs.contains(frames.at(i))) { m_requestedThumbs << frames.at(i); } } qSort(m_requestedThumbs); if (!m_thumbThread.isRunning()) { m_thumbThread = QtConcurrent::run(this, &ProjectClip::doExtractImage); } } void ProjectClip::doExtractImage() { // TODO refac: we can probably move that into a ThumbJob std::shared_ptr prod = thumbProducer(); if (prod == nullptr || !prod->is_valid()) { return; } int frameWidth = 150 * prod->profile()->dar() + 0.5; bool ok = false; auto ptr = m_model.lock(); Q_ASSERT(ptr); QDir thumbFolder = pCore->currentDoc()->getCacheDir(CacheThumbs, &ok); int max = prod->get_length(); while (!m_requestedThumbs.isEmpty()) { m_thumbMutex.lock(); int pos = m_requestedThumbs.takeFirst(); m_thumbMutex.unlock(); if (ok && thumbFolder.exists(hash() + QLatin1Char('#') + QString::number(pos) + QStringLiteral(".png"))) { emit thumbReady(pos, QImage(thumbFolder.absoluteFilePath(hash() + QLatin1Char('#') + QString::number(pos) + QStringLiteral(".png")))); continue; } if (pos >= max) { pos = max - 1; } const QString path = url() + QLatin1Char('_') + QString::number(pos); QImage img; if (ThumbnailCache::get()->hasThumbnail(clipId(), pos, true)) { img = ThumbnailCache::get()->getThumbnail(clipId(), pos, true); } if (!img.isNull()) { emit thumbReady(pos, img); continue; } prod->seek(pos); Mlt::Frame *frame = prod->get_frame(); frame->set("deinterlace_method", "onefield"); frame->set("top_field_first", -1); if (frame->is_valid()) { img = KThumb::getFrame(frame, frameWidth, 150, !qFuzzyCompare(prod->profile()->sar(), 1)); ThumbnailCache::get()->storeThumbnail(clipId(), pos, img, false); emit thumbReady(pos, img); } delete frame; } } int ProjectClip::audioChannels() const { if (!audioInfo()) { return 0; } return audioInfo()->channels(); } void ProjectClip::discardAudioThumb() { QString audioThumbPath = getAudioThumbPath(); if (!audioThumbPath.isEmpty()) { QFile::remove(audioThumbPath); } audioFrameCache.clear(); qCDebug(KDENLIVE_LOG) << "//////////////////// DISCARD AUIIO THUMBNS"; m_audioThumbCreated = false; pCore->jobManager()->discardJobs(clipId(), AbstractClipJob::AUDIOTHUMBJOB); } const QString ProjectClip::getAudioThumbPath() { if (audioInfo() == nullptr) { return QString(); } int audioStream = audioInfo()->ffmpeg_audio_index(); QString clipHash = hash(); if (clipHash.isEmpty()) { return QString(); } bool ok = false; QDir thumbFolder = pCore->currentDoc()->getCacheDir(CacheAudio, &ok); if (!ok) { return QString(); } QString audioPath = thumbFolder.absoluteFilePath(clipHash); if (audioStream > 0) { audioPath.append(QLatin1Char('_') + QString::number(audioInfo()->audio_index())); } int roundedFps = (int)pCore->getCurrentFps(); audioPath.append(QStringLiteral("_%1_audio.png").arg(roundedFps)); return audioPath; } QStringList ProjectClip::updatedAnalysisData(const QString &name, const QString &data, int offset) { if (data.isEmpty()) { // Remove data return QStringList() << QString("kdenlive:clipanalysis." + name) << QString(); // m_controller->resetProperty("kdenlive:clipanalysis." + name); } QString current = getProducerProperty("kdenlive:clipanalysis." + name); if (!current.isEmpty()) { if (KMessageBox::questionYesNo(QApplication::activeWindow(), i18n("Clip already contains analysis data %1", name), QString(), KGuiItem(i18n("Merge")), KGuiItem(i18n("Add"))) == KMessageBox::Yes) { // Merge data auto &profile = pCore->getCurrentProfile(); Mlt::Geometry geometry(current.toUtf8().data(), duration().frames(profile->fps()), profile->width(), profile->height()); Mlt::Geometry newGeometry(data.toUtf8().data(), duration().frames(profile->fps()), profile->width(), profile->height()); Mlt::GeometryItem item; int pos = 0; while (newGeometry.next_key(&item, pos) == 0) { pos = item.frame(); item.frame(pos + offset); pos++; geometry.insert(item); } return QStringList() << QString("kdenlive:clipanalysis." + name) << geometry.serialise(); // m_controller->setProperty("kdenlive:clipanalysis." + name, geometry.serialise()); } // Add data with another name int i = 1; QString previous = getProducerProperty("kdenlive:clipanalysis." + name + QString::number(i)); while (!previous.isEmpty()) { ++i; previous = getProducerProperty("kdenlive:clipanalysis." + name + QString::number(i)); } return QStringList() << QString("kdenlive:clipanalysis." + name + QString::number(i)) << geometryWithOffset(data, offset); // m_controller->setProperty("kdenlive:clipanalysis." + name + QLatin1Char(' ') + QString::number(i), geometryWithOffset(data, offset)); } return QStringList() << QString("kdenlive:clipanalysis." + name) << geometryWithOffset(data, offset); // m_controller->setProperty("kdenlive:clipanalysis." + name, geometryWithOffset(data, offset)); } QMap ProjectClip::analysisData(bool withPrefix) { return getPropertiesFromPrefix(QStringLiteral("kdenlive:clipanalysis."), withPrefix); } const QString ProjectClip::geometryWithOffset(const QString &data, int offset) { if (offset == 0) { return data; } auto &profile = pCore->getCurrentProfile(); Mlt::Geometry geometry(data.toUtf8().data(), duration().frames(profile->fps()), profile->width(), profile->height()); Mlt::Geometry newgeometry(nullptr, duration().frames(profile->fps()), profile->width(), profile->height()); Mlt::GeometryItem item; int pos = 0; while (geometry.next_key(&item, pos) == 0) { pos = item.frame(); item.frame(pos + offset); pos++; newgeometry.insert(item); } return newgeometry.serialise(); } bool ProjectClip::isSplittable() const { return (m_clipType == ClipType::AV || m_clipType == ClipType::Playlist); } void ProjectClip::setBinEffectsEnabled(bool enabled) { ClipController::setBinEffectsEnabled(enabled); } -void ProjectClip::registerService(std::weak_ptr timeline, int clipId, std::shared_ptr service, bool forceRegister) +void ProjectClip::registerService(std::weak_ptr timeline, int clipId, const std::shared_ptr &service, bool forceRegister) { if (!service->is_cut() || forceRegister) { int hasAudio = service->get_int("set.test_audio") == 0; int hasVideo = service->get_int("set.test_image") == 0; if (hasVideo && m_videoProducers.count(clipId) == 0) { // This is an undo producer, register it! m_videoProducers[clipId] = service; m_effectStack->addService(m_videoProducers[clipId]); } else if (hasAudio && m_audioProducers.count(clipId) == 0) { // This is an undo producer, register it! m_audioProducers[clipId] = service; m_effectStack->addService(m_audioProducers[clipId]); } } - registerTimelineClip(timeline, clipId); + registerTimelineClip(std::move(timeline), clipId); } void ProjectClip::registerTimelineClip(std::weak_ptr timeline, int clipId) { Q_ASSERT(m_registeredClips.count(clipId) == 0); Q_ASSERT(!timeline.expired()); m_registeredClips[clipId] = std::move(timeline); setRefCount((uint)m_registeredClips.size()); } void ProjectClip::deregisterTimelineClip(int clipId) { qDebug() << " ** * DEREGISTERING TIMELINE CLIP: " << clipId; Q_ASSERT(m_registeredClips.count(clipId) > 0); m_registeredClips.erase(clipId); if (m_videoProducers.count(clipId) > 0) { m_effectStack->removeService(m_videoProducers[clipId]); m_videoProducers.erase(clipId); } if (m_audioProducers.count(clipId) > 0) { m_effectStack->removeService(m_audioProducers[clipId]); m_audioProducers.erase(clipId); } setRefCount((uint)m_registeredClips.size()); } QList ProjectClip::timelineInstances() const { QList ids; for (std::map>::const_iterator it = m_registeredClips.begin(); it != m_registeredClips.end(); ++it) { ids.push_back(it->first); } return ids; } bool ProjectClip::selfSoftDelete(Fun &undo, Fun &redo) { auto toDelete = m_registeredClips; // we cannot use m_registeredClips directly, because it will be modified during loop for (const auto &clip : toDelete) { if (m_registeredClips.count(clip.first) == 0) { // clip already deleted, was probably grouped with another one continue; } if (auto timeline = clip.second.lock()) { timeline->requestItemDeletion(clip.first, undo, redo); } else { qDebug() << "Error while deleting clip: timeline unavailable"; Q_ASSERT(false); return false; } } return AbstractProjectItem::selfSoftDelete(undo, redo); } bool ProjectClip::isIncludedInTimeline() { return m_registeredClips.size() > 0; } void ProjectClip::updateChildProducers() { // TODO refac: the effect should be managed by an effectstack on the master /* // pass effect stack on all child producers QMutexLocker locker(&m_producerMutex); for (const auto &clip : m_timelineProducers) { if (auto producer = clip.second) { Clip clp(producer->parent()); clp.deleteEffects(); clp.replaceEffects(*m_masterProducer); } } */ } void ProjectClip::replaceInTimeline() { for (const auto &clip : m_registeredClips) { if (auto timeline = clip.second.lock()) { timeline->requestClipReload(clip.first); } else { qDebug() << "Error while reloading clip: timeline unavailable"; Q_ASSERT(false); } } } -void ProjectClip::updateTimelineClips(QVector roles) +void ProjectClip::updateTimelineClips(const QVector &roles) { for (const auto &clip : m_registeredClips) { if (auto timeline = clip.second.lock()) { timeline->requestClipUpdate(clip.first, roles); } else { qDebug() << "Error while reloading clip thumb: timeline unavailable"; Q_ASSERT(false); return; } } } diff --git a/src/bin/projectclip.h b/src/bin/projectclip.h index 56c1d249d..b9c4e2a8a 100644 --- a/src/bin/projectclip.h +++ b/src/bin/projectclip.h @@ -1,288 +1,288 @@ /* Copyright (C) 2012 Till Theato Copyright (C) 2014 Jean-Baptiste Mardelle This file is part of Kdenlive. See www.kdenlive.org. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef PROJECTCLIP_H #define PROJECTCLIP_H #include "abstractprojectitem.h" #include "definitions.h" #include "mltcontroller/clipcontroller.h" #include "timeline2/model/timelinemodel.hpp" #include #include #include #include class AudioStreamInfo; class ClipPropertiesController; class MarkerListModel; class ProjectFolder; class ProjectSubClip; class QDomElement; class QUndoCommand; namespace Mlt { class Producer; class Properties; } // namespace Mlt /** * @class ProjectClip * @brief Represents a clip in the project (not timeline). * It will be displayed as a bin item that can be dragged onto the timeline. * A single bin clip can be inserted several times on the timeline, and the ProjectClip * keeps track of all the ids of the corresponding ClipModel. * Note that because of a limitation in melt and AvFilter, it is currently difficult to * mix the audio of two producers that are cut from the same master producer * (that produces small but noticeable clicking artifacts) * To workaround this, we need to have a master clip for each instance of the audio clip in the timeline. This class is tracking them all. This track also holds * a master clip for each clip where the timewarp producer has been applied */ class ProjectClip : public AbstractProjectItem, public ClipController { Q_OBJECT public: friend class Bin; friend bool TimelineModel::checkConsistency(); // for testing /** * @brief Constructor; used when loading a project and the producer is already available. */ - static std::shared_ptr construct(const QString &id, const QIcon &thumb, std::shared_ptr model, - std::shared_ptr producer); + static std::shared_ptr construct(const QString &id, const QIcon &thumb, const std::shared_ptr &model, + const std::shared_ptr &producer); /** * @brief Constructor. * @param description element describing the clip; the "kdenlive:id" attribute and "resource" property are used */ static std::shared_ptr construct(const QString &id, const QDomElement &description, const QIcon &thumb, std::shared_ptr model); protected: - ProjectClip(const QString &id, const QIcon &thumb, std::shared_ptr model, std::shared_ptr producer); - ProjectClip(const QString &id, const QDomElement &description, const QIcon &thumb, std::shared_ptr model); + ProjectClip(const QString &id, const QIcon &thumb, const std::shared_ptr &model, std::shared_ptr producer); + ProjectClip(const QString &id, const QDomElement &description, const QIcon &thumb, const std::shared_ptr &model); public: virtual ~ProjectClip(); void reloadProducer(bool refreshOnly = false); /** @brief Returns a unique hash identifier used to store clip thumbnails. */ // virtual void hash() = 0; /** @brief Returns this if @param id matches the clip's id or nullptr otherwise. */ std::shared_ptr clip(const QString &id) override; std::shared_ptr folder(const QString &id) override; std::shared_ptr getSubClip(int in, int out); /** @brief Returns this if @param ix matches the clip's index or nullptr otherwise. */ std::shared_ptr clipAt(int ix) override; /** @brief Returns the clip type as defined in definitions.h */ ClipType::ProducerType clipType() const override; bool selfSoftDelete(Fun &undo, Fun &redo) override; /** @brief Returns true if item has both audio and video enabled. */ bool hasAudioAndVideo() const override; /** @brief Check if clip has a parent folder with id id */ bool hasParent(const QString &id) const; /** @brief Returns true is the clip can have the requested state */ bool isCompatible(PlaylistState::ClipState state) const; ClipPropertiesController *buildProperties(QWidget *parent); QPoint zone() const override; /** @brief Returns whether this clip has a url (=describes a file) or not. */ bool hasUrl() const; /** @brief Returns the clip's url. */ const QString url() const; /** @brief Returns the clip's duration. */ GenTime duration() const; size_t frameDuration() const; /** @brief Returns the original clip's fps. */ double getOriginalFps() const; bool rename(const QString &name, int column) override; QDomElement toXml(QDomDocument &document, bool includeMeta = false) override; QVariant getData(DataType type) const override; /** @brief Sets thumbnail for this clip. */ void setThumbnail(const QImage &); QPixmap thumbnail(int width, int height); /** @brief Sets the MLT producer associated with this clip * @param producer The producer * @param replaceProducer If true, we replace existing producer with this one * @returns true if producer was changed * . */ bool setProducer(std::shared_ptr producer, bool replaceProducer); /** @brief Returns true if this clip already has a producer. */ bool isReady() const; /** @brief Returns this clip's producer. */ std::shared_ptr thumbProducer(); /** @brief Recursively disable/enable bin effects. */ void setBinEffectsEnabled(bool enabled) override; /** @brief Set properties on this clip. TODO: should we store all in MLT or use extra m_properties ?. */ void setProperties(const QMap &properties, bool refreshPanel = false); /** @brief Get an XML property from MLT produced xml. */ static QString getXmlProperty(const QDomElement &producer, const QString &propertyName, const QString &defaultValue = QString()); QString getToolTip() const override; /** @brief The clip hash created from the clip's resource. */ const QString hash(); /** @brief Returns true if we are using a proxy for this clip. */ bool hasProxy() const; /** Cache for every audio Frame with 10 Bytes */ /** format is frame -> channel ->bytes */ QVariantList audioFrameCache; bool audioThumbCreated() const; void setWaitingStatus(const QString &id); /** @brief Returns true if the clip matched a condition, for example vcodec=mpeg1video. */ bool matches(const QString &condition); /** @brief Returns the number of audio channels. */ int audioChannels() const; /** @brief get data analysis value. */ QStringList updatedAnalysisData(const QString &name, const QString &data, int offset); QMap analysisData(bool withPrefix = false); /** @brief Returns the list of this clip's subclip's ids. */ QStringList subClipIds() const; /** @brief Delete cached audio thumb - needs to be recreated */ void discardAudioThumb(); /** @brief Get path for this clip's audio thumbnail */ const QString getAudioThumbPath(); /** @brief Returns true if this producer has audio and can be splitted on timeline*/ bool isSplittable() const; /** @brief Returns true if a clip corresponding to this bin is inserted in a timeline. Note that this function does not account for children, use TreeItem::accumulate if you want to get that information as well. */ bool isIncludedInTimeline() override; /** @brief Returns a list of all timeline clip ids for this bin clip */ QList timelineInstances() const; /** @brief This function returns a cut to the master producer associated to the timeline clip with given ID. Each clip must have a different master producer (see comment of the class) */ std::shared_ptr getTimelineProducer(int clipId, PlaylistState::ClipState st, double speed = 1.0); /* @brief This function should only be used at loading. It takes a producer that was read from mlt, and checks whether the master producer is already in use. If yes, then we must create a new one, because of the mixing bug. In any case, we return a cut of the master that can be used in the timeline The bool returned has the following sementic: - if true, then the returned cut still possibly has effect on it. You need to rebuild the effectStack based on this - if false, the the returned cut don't have effects anymore (it's a fresh one), so you need to reload effects from the old producer */ std::pair, bool> giveMasterAndGetTimelineProducer(int clipId, std::shared_ptr master, PlaylistState::ClipState state); std::shared_ptr cloneProducer(bool removeEffects = false); - static std::shared_ptr cloneProducer(std::shared_ptr producer); + static std::shared_ptr cloneProducer(const std::shared_ptr &producer); std::shared_ptr softClone(const char *list); - void updateTimelineClips(QVector roles); + void updateTimelineClips(const QVector &roles); protected: friend class ClipModel; /** @brief This is a call-back called by a ClipModel when it is created @param timeline ptr to the pointer in which this ClipModel is inserted @param clipId id of the inserted clip */ void registerTimelineClip(std::weak_ptr timeline, int clipId); - void registerService(std::weak_ptr timeline, int clipId, std::shared_ptr service, bool forceRegister = false); + void registerService(std::weak_ptr timeline, int clipId, const std::shared_ptr &service, bool forceRegister = false); /* @brief update the producer to reflect new parent folder */ void updateParent(std::shared_ptr parent) override; /** @brief This is a call-back called by a ClipModel when it is deleted @param clipId id of the deleted clip */ void deregisterTimelineClip(int clipId); void emitProducerChanged(const QString &id, const std::shared_ptr &producer) override { emit producerChanged(id, producer); }; /** @brief Replace instance of this clip in timeline */ void updateChildProducers(); void replaceInTimeline(); void connectEffectStack() override; public slots: /* @brief Store the audio thumbnails once computed. Note that the parameter is a value and not a reference, fill free to use it as a sink (use std::move to * avoid copy). */ void updateAudioThumbnail(QVariantList audioLevels); /** @brief Extract image thumbnails for timeline. */ void slotExtractImage(const QList &frames); /** @brief Delete the proxy file */ void deleteProxy(); private: /** @brief Generate and store file hash if not available. */ const QString getFileHash(); /** @brief Store clip url temporarily while the clip controller has not been created. */ QString m_temporaryUrl; std::shared_ptr m_thumbsProducer; QMutex m_producerMutex; QMutex m_thumbMutex; QFuture m_thumbThread; QList m_requestedThumbs; const QString geometryWithOffset(const QString &data, int offset); void doExtractImage(); // This is a helper function that creates the disabled producer. This is a clone of the original one, with audio and video disabled void createDisabledMasterProducer(); std::map> m_registeredClips; // the following holds a producer for each audio clip in the timeline // keys are the id of the clips in the timeline, values are their values std::unordered_map> m_audioProducers; std::unordered_map> m_videoProducers; std::unordered_map> m_timewarpProducers; std::shared_ptr m_disabledProducer; signals: void producerChanged(const QString &, const std::shared_ptr &); void refreshPropertiesPanel(); void refreshAnalysisPanel(); void refreshClipDisplay(); void thumbReady(int, const QImage &); /** @brief Clip is ready, load properties. */ void loadPropertiesPanel(); }; #endif diff --git a/src/bin/projectfolder.cpp b/src/bin/projectfolder.cpp index 6a75d2ca7..47d1f277e 100644 --- a/src/bin/projectfolder.cpp +++ b/src/bin/projectfolder.cpp @@ -1,181 +1,181 @@ /* Copyright (C) 2012 Till Theato Copyright (C) 2014 Jean-Baptiste Mardelle This file is part of Kdenlive. See www.kdenlive.org. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "projectfolder.h" #include "bin.h" #include "core.h" #include "projectclip.h" #include "projectitemmodel.h" #include #include - -ProjectFolder::ProjectFolder(const QString &id, const QString &name, std::shared_ptr model) +#include +ProjectFolder::ProjectFolder(const QString &id, const QString &name, const std::shared_ptr &model) : AbstractProjectItem(AbstractProjectItem::FolderItem, id, model) { m_name = name; m_clipStatus = StatusReady; m_thumbnail = QIcon::fromTheme(QStringLiteral("folder")); } std::shared_ptr ProjectFolder::construct(const QString &id, const QString &name, std::shared_ptr model) { - std::shared_ptr self(new ProjectFolder(id, name, model)); + std::shared_ptr self(new ProjectFolder(id, name, std::move(model))); baseFinishConstruct(self); return self; } -ProjectFolder::ProjectFolder(std::shared_ptr model) +ProjectFolder::ProjectFolder(const std::shared_ptr &model) : AbstractProjectItem(AbstractProjectItem::FolderItem, QString::number(-1), model, true) { m_name = QStringLiteral("root"); } std::shared_ptr ProjectFolder::construct(std::shared_ptr model) { - std::shared_ptr self(new ProjectFolder(model)); + std::shared_ptr self(new ProjectFolder(std::move(model))); baseFinishConstruct(self); return self; } ProjectFolder::~ProjectFolder() {} std::shared_ptr ProjectFolder::clip(const QString &id) { for (int i = 0; i < childCount(); ++i) { std::shared_ptr clip = std::static_pointer_cast(child(i))->clip(id); if (clip) { return clip; } } return std::shared_ptr(); } QList> ProjectFolder::childClips() { QList> allChildren; for (int i = 0; i < childCount(); ++i) { std::shared_ptr childItem = std::static_pointer_cast(child(i)); if (childItem->itemType() == ClipItem) { allChildren << std::static_pointer_cast(childItem); } else if (childItem->itemType() == FolderItem) { allChildren << std::static_pointer_cast(childItem)->childClips(); } } return allChildren; } bool ProjectFolder::hasChildClips() const { for (int i = 0; i < childCount(); ++i) { std::shared_ptr childItem = std::static_pointer_cast(child(i)); if (childItem->itemType() == ClipItem) { return true; } if (childItem->itemType() == FolderItem) { bool hasChildren = std::static_pointer_cast(childItem)->hasChildClips(); if (hasChildren) { return true; } } } return false; } QString ProjectFolder::getToolTip() const { return i18np("%1 clip", "%1 clips", childCount()); } std::shared_ptr ProjectFolder::folder(const QString &id) { if (m_binId == id) { return std::static_pointer_cast(shared_from_this()); } for (int i = 0; i < childCount(); ++i) { std::shared_ptr folderItem = std::static_pointer_cast(child(i))->folder(id); if (folderItem) { return folderItem; } } return std::shared_ptr(); } std::shared_ptr ProjectFolder::clipAt(int index) { if (childCount() == 0) { return std::shared_ptr(); } for (int i = 0; i < childCount(); ++i) { std::shared_ptr clip = std::static_pointer_cast(child(i))->clipAt(index); if (clip) { return clip; } } return std::shared_ptr(); } void ProjectFolder::setBinEffectsEnabled(bool enabled) { for (int i = 0; i < childCount(); ++i) { std::shared_ptr item = std::static_pointer_cast(child(i)); item->setBinEffectsEnabled(enabled); } } QDomElement ProjectFolder::toXml(QDomDocument &document, bool) { QDomElement folder = document.createElement(QStringLiteral("folder")); folder.setAttribute(QStringLiteral("name"), name()); for (int i = 0; i < childCount(); ++i) { folder.appendChild(std::static_pointer_cast(child(i))->toXml(document)); } return folder; } bool ProjectFolder::rename(const QString &name, int column) { Q_UNUSED(column) if (m_name == name) { return false; } // Rename folder if (auto ptr = m_model.lock()) { auto self = std::static_pointer_cast(shared_from_this()); return std::static_pointer_cast(ptr)->requestRenameFolder(self, name); } qDebug() << "ERROR: Impossible to rename folder because model is not available"; Q_ASSERT(false); return false; } ClipType::ProducerType ProjectFolder::clipType() const { return ClipType::Unknown; } bool ProjectFolder::hasAudioAndVideo() const { return false; } diff --git a/src/bin/projectfolder.h b/src/bin/projectfolder.h index 4e6800129..fd5816094 100644 --- a/src/bin/projectfolder.h +++ b/src/bin/projectfolder.h @@ -1,92 +1,92 @@ /* Copyright (C) 2012 Till Theato Copyright (C) 2014 Jean-Baptiste Mardelle This file is part of Kdenlive. See www.kdenlive.org. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef PROJECTFOLDER_H #define PROJECTFOLDER_H #include "abstractprojectitem.h" /** * @class ProjectFolder * @brief A folder in the bin. */ class ProjectClip; class Bin; class ProjectFolder : public AbstractProjectItem { Q_OBJECT public: /** * @brief Creates the supplied folder and loads its children. * @param description element describing the folder and its children */ static std::shared_ptr construct(const QString &id, const QString &name, std::shared_ptr model); /** @brief Creates an empty root folder. */ static std::shared_ptr construct(std::shared_ptr model); protected: - ProjectFolder(const QString &id, const QString &name, std::shared_ptr model); + ProjectFolder(const QString &id, const QString &name, const std::shared_ptr &model); - explicit ProjectFolder(std::shared_ptr model); + explicit ProjectFolder(const std::shared_ptr &model); public: ~ProjectFolder(); /** * @brief Returns the clip if it is a child (also indirect). * @param id id of the child which should be returned */ std::shared_ptr clip(const QString &id) override; /** * @brief Returns itself or a child folder that matches the requested id. * @param id id of the child which should be returned */ std::shared_ptr folder(const QString &id) override; /** @brief Recursively disable/enable bin effects. */ void setBinEffectsEnabled(bool enabled) override; /** * @brief Returns the clip if it is a child (also indirect). * @param index index of the child which should be returned */ std::shared_ptr clipAt(int index) override; /** @brief Returns an xml description of the folder. */ QDomElement toXml(QDomDocument &document, bool includeMeta = false) override; QString getToolTip() const override; bool rename(const QString &name, int column) override; /** @brief Returns a list of all children and sub-children clips. */ QList> childClips(); /** @brief Returns true if folder contains a clip. */ bool hasChildClips() const; ClipType::ProducerType clipType() const override; /** @brief Returns true if item has both audio and video enabled. */ bool hasAudioAndVideo() const override; }; #endif diff --git a/src/bin/projectfolderup.cpp b/src/bin/projectfolderup.cpp index 1447820f9..1a8d38e65 100644 --- a/src/bin/projectfolderup.cpp +++ b/src/bin/projectfolderup.cpp @@ -1,88 +1,88 @@ /* Copyright (C) 2015 Jean-Baptiste Mardelle This file is part of Kdenlive. See www.kdenlive.org. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "projectfolderup.h" #include "projectclip.h" #include #include - -ProjectFolderUp::ProjectFolderUp(std::shared_ptr model) +#include +ProjectFolderUp::ProjectFolderUp(const std::shared_ptr &model) : AbstractProjectItem(AbstractProjectItem::FolderUpItem, QString(), model) { m_thumbnail = QIcon::fromTheme(QStringLiteral("go-previous")); m_name = i18n("Back"); } std::shared_ptr ProjectFolderUp::construct(std::shared_ptr model) { - std::shared_ptr self(new ProjectFolderUp(model)); + std::shared_ptr self(new ProjectFolderUp(std::move(model))); baseFinishConstruct(self); return self; } ProjectFolderUp::~ProjectFolderUp() {} std::shared_ptr ProjectFolderUp::clip(const QString &id) { Q_UNUSED(id) return std::shared_ptr(); } QString ProjectFolderUp::getToolTip() const { return i18n("Go up"); } std::shared_ptr ProjectFolderUp::folder(const QString &id) { Q_UNUSED(id); return std::shared_ptr(); } std::shared_ptr ProjectFolderUp::clipAt(int index) { Q_UNUSED(index); return std::shared_ptr(); } void ProjectFolderUp::setBinEffectsEnabled(bool) {} QDomElement ProjectFolderUp::toXml(QDomDocument &document, bool) { return document.documentElement(); } bool ProjectFolderUp::rename(const QString &, int) { return false; } ClipType::ProducerType ProjectFolderUp::clipType() const { return ClipType::Unknown; } bool ProjectFolderUp::hasAudioAndVideo() const { return false; } diff --git a/src/bin/projectfolderup.h b/src/bin/projectfolderup.h index 820eac4fc..e6ae5fc4c 100644 --- a/src/bin/projectfolderup.h +++ b/src/bin/projectfolderup.h @@ -1,84 +1,84 @@ /* Copyright (C) 2015 Jean-Baptiste Mardelle This file is part of Kdenlive. See www.kdenlive.org. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef PROJECTFOLDERUP_H #define PROJECTFOLDERUP_H #include "abstractprojectitem.h" /** * @class ProjectFolderUpUp * @brief A simple "folder up" item allowing to navigate up when the bin is in icon view. */ class ProjectClip; class ProjectFolderUp : public AbstractProjectItem { Q_OBJECT public: /** * @brief Creates the supplied folder and loads its children. * @param description element describing the folder and its children */ static std::shared_ptr construct(std::shared_ptr model); protected: - explicit ProjectFolderUp(std::shared_ptr model); + explicit ProjectFolderUp(const std::shared_ptr &model); public: ~ProjectFolderUp(); /** * @brief Returns the clip if it is a child (also indirect). * @param id id of the child which should be returned */ std::shared_ptr clip(const QString &id) override; /** * @brief Returns itself or a child folder that matches the requested id. * @param id id of the child which should be returned */ std::shared_ptr folder(const QString &id) override; /** * @brief Returns the clip if it is a child (also indirect). * @param index index of the child which should be returned */ std::shared_ptr clipAt(int index) override; /** @brief Recursively disable/enable bin effects. */ void setBinEffectsEnabled(bool enabled) override; /** @brief Returns an xml description of the folder. */ QDomElement toXml(QDomDocument &document, bool includeMeta = false) override; QString getToolTip() const override; bool rename(const QString &name, int column) override; ClipType::ProducerType clipType() const override; /** @brief Returns true if item has both audio and video enabled. */ bool hasAudioAndVideo() const override; private: Bin *m_bin; }; #endif diff --git a/src/bin/projectitemmodel.cpp b/src/bin/projectitemmodel.cpp index e059b62f9..84601a961 100644 --- a/src/bin/projectitemmodel.cpp +++ b/src/bin/projectitemmodel.cpp @@ -1,950 +1,952 @@ /* Copyright (C) 2012 Till Theato Copyright (C) 2014 Jean-Baptiste Mardelle Copyright (C) 2017 Nicolas Carion This file is part of Kdenlive. See www.kdenlive.org. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "projectitemmodel.h" #include "abstractprojectitem.h" #include "binplaylist.hpp" #include "core.h" #include "doc/kdenlivedoc.h" #include "filewatcher.hpp" #include "jobs/audiothumbjob.hpp" #include "jobs/jobmanager.h" #include "jobs/loadjob.hpp" #include "jobs/thumbjob.hpp" #include "kdenlivesettings.h" #include "macros.hpp" #include "profiles/profilemodel.hpp" #include "project/projectmanager.h" #include "projectclip.h" #include "projectfolder.h" #include "projectsubclip.h" #include "xml/xml.hpp" #include #include #include #include #include #include +#include ProjectItemModel::ProjectItemModel(QObject *parent) : AbstractTreeModel(parent) , m_lock(QReadWriteLock::Recursive) , m_binPlaylist(new BinPlaylist()) , m_fileWatcher(new FileWatcher()) , m_nextId(1) , m_blankThumb() , m_dragType(PlaylistState::Disabled) { QPixmap pix(QSize(160, 90)); pix.fill(Qt::lightGray); m_blankThumb.addPixmap(pix); connect(m_fileWatcher.get(), &FileWatcher::binClipModified, this, &ProjectItemModel::reloadClip); connect(m_fileWatcher.get(), &FileWatcher::binClipWaiting, this, &ProjectItemModel::setClipWaiting); connect(m_fileWatcher.get(), &FileWatcher::binClipMissing, this, &ProjectItemModel::setClipInvalid); } std::shared_ptr ProjectItemModel::construct(QObject *parent) { std::shared_ptr self(new ProjectItemModel(parent)); self->rootItem = ProjectFolder::construct(self); return self; } ProjectItemModel::~ProjectItemModel() {} int ProjectItemModel::mapToColumn(int column) const { switch (column) { case 0: return AbstractProjectItem::DataName; break; case 1: return AbstractProjectItem::DataDate; break; case 2: return AbstractProjectItem::DataDescription; break; default: return AbstractProjectItem::DataName; } } QVariant ProjectItemModel::data(const QModelIndex &index, int role) const { READ_LOCK(); if (!index.isValid()) { return QVariant(); } if (role == Qt::DisplayRole || role == Qt::EditRole) { std::shared_ptr item = getBinItemByIndex(index); auto type = static_cast(mapToColumn(index.column())); QVariant ret = item->getData(type); return ret; } if (role == Qt::DecorationRole) { if (index.column() != 0) { return QVariant(); } // Data has to be returned as icon to allow the view to scale it std::shared_ptr item = getBinItemByIndex(index); QVariant thumb = item->getData(AbstractProjectItem::DataThumbnail); QIcon icon; if (thumb.canConvert()) { icon = thumb.value(); } else { qDebug() << "ERROR: invalid icon found"; } return icon; } std::shared_ptr item = getBinItemByIndex(index); return item->getData(static_cast(role)); } bool ProjectItemModel::setData(const QModelIndex &index, const QVariant &value, int role) { QWriteLocker locker(&m_lock); std::shared_ptr item = getBinItemByIndex(index); if (item->rename(value.toString(), index.column())) { emit dataChanged(index, index, {role}); return true; } // Item name was not changed return false; } Qt::ItemFlags ProjectItemModel::flags(const QModelIndex &index) const { /*return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | Qt::ItemIsEditable;*/ if (!index.isValid()) { return Qt::ItemIsDropEnabled; } std::shared_ptr item = getBinItemByIndex(index); AbstractProjectItem::PROJECTITEMTYPE type = item->itemType(); switch (type) { case AbstractProjectItem::FolderItem: return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | Qt::ItemIsEditable; break; case AbstractProjectItem::ClipItem: if (!item->statusReady()) { return Qt::ItemIsSelectable; } return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | Qt::ItemIsEditable; break; case AbstractProjectItem::SubClipItem: return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable | Qt::ItemIsDragEnabled; break; case AbstractProjectItem::FolderUpItem: return Qt::ItemIsEnabled; break; default: return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable; } } // cppcheck-suppress unusedFunction bool ProjectItemModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) { Q_UNUSED(row) Q_UNUSED(column) if (action == Qt::IgnoreAction) { return true; } if (data->hasUrls()) { emit itemDropped(data->urls(), parent); return true; } if (data->hasFormat(QStringLiteral("kdenlive/producerslist"))) { // Dropping an Bin item const QStringList ids = QString(data->data(QStringLiteral("kdenlive/producerslist"))).split(QLatin1Char(';')); if (ids.constFirst().contains(QLatin1Char('/'))) { // subclip zone QStringList clipData = ids.constFirst().split(QLatin1Char('/')); if (clipData.length() >= 3) { QString id; return requestAddBinSubClip(id, clipData.at(1).toInt(), clipData.at(2).toInt(), QString(), clipData.at(0)); } else { // error, malformed clip zone, abort return false; } } else { emit itemDropped(ids, parent); } return true; } if (data->hasFormat(QStringLiteral("kdenlive/effect"))) { // Dropping effect on a Bin item QStringList effectData; effectData << QString::fromUtf8(data->data(QStringLiteral("kdenlive/effect"))); QStringList source = QString::fromUtf8(data->data(QStringLiteral("kdenlive/effectsource"))).split(QLatin1Char('-')); effectData << source; emit effectDropped(effectData, parent); return true; } if (data->hasFormat(QStringLiteral("kdenlive/clip"))) { const QStringList list = QString(data->data(QStringLiteral("kdenlive/clip"))).split(QLatin1Char(';')); QString id; return requestAddBinSubClip(id, list.at(1).toInt(), list.at(2).toInt(), QString(), list.at(0)); } return false; } QVariant ProjectItemModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal && role == Qt::DisplayRole) { QVariant columnName; switch (section) { case 0: columnName = i18n("Name"); break; case 1: columnName = i18n("Date"); break; case 2: columnName = i18n("Description"); break; default: columnName = i18n("Unknown"); break; } return columnName; } return QAbstractItemModel::headerData(section, orientation, role); } int ProjectItemModel::columnCount(const QModelIndex &parent) const { if (parent.isValid()) { return getBinItemByIndex(parent)->supportedDataCount(); } return std::static_pointer_cast(rootItem)->supportedDataCount(); } // cppcheck-suppress unusedFunction Qt::DropActions ProjectItemModel::supportedDropActions() const { return Qt::CopyAction | Qt::MoveAction; } QStringList ProjectItemModel::mimeTypes() const { QStringList types; types << QStringLiteral("kdenlive/producerslist") << QStringLiteral("text/uri-list") << QStringLiteral("kdenlive/clip") << QStringLiteral("kdenlive/effect"); return types; } QMimeData *ProjectItemModel::mimeData(const QModelIndexList &indices) const { // Mime data is a list of id's separated by ';'. // Clip ids are represented like: 2 (where 2 is the clip's id) // Clip zone ids are represented like: 2/10/200 (where 2 is the clip's id, 10 and 200 are in and out points) // Folder ids are represented like: #2 (where 2 is the folder's id) auto *mimeData = new QMimeData(); QStringList list; size_t duration = 0; for (int i = 0; i < indices.count(); i++) { QModelIndex ix = indices.at(i); if (!ix.isValid() || ix.column() != 0) { continue; } std::shared_ptr item = getBinItemByIndex(ix); AbstractProjectItem::PROJECTITEMTYPE type = item->itemType(); if (type == AbstractProjectItem::ClipItem) { ClipType::ProducerType cType = item->clipType(); QString dragId = item->clipId(); if ((cType == ClipType::AV || cType == ClipType::Playlist)) { switch (m_dragType) { case PlaylistState::AudioOnly: dragId.prepend(QLatin1Char('A')); break; case PlaylistState::VideoOnly: dragId.prepend(QLatin1Char('V')); break; default: break; } } list << dragId; duration += (std::static_pointer_cast(item))->frameDuration(); } else if (type == AbstractProjectItem::SubClipItem) { QPoint p = item->zone(); list << std::static_pointer_cast(item)->getMasterClip()->clipId() + QLatin1Char('/') + QString::number(p.x()) + QLatin1Char('/') + QString::number(p.y()); } else if (type == AbstractProjectItem::FolderItem) { list << "#" + item->clipId(); } } if (!list.isEmpty()) { QByteArray data; data.append(list.join(QLatin1Char(';')).toUtf8()); mimeData->setData(QStringLiteral("kdenlive/producerslist"), data); mimeData->setText(QString::number(duration)); } return mimeData; } -void ProjectItemModel::onItemUpdated(std::shared_ptr item, int role) +void ProjectItemModel::onItemUpdated(const std::shared_ptr &item, int role) { auto tItem = std::static_pointer_cast(item); auto ptr = tItem->parentItem().lock(); if (ptr) { auto index = getIndexFromItem(tItem); emit dataChanged(index, index, {role}); } } void ProjectItemModel::onItemUpdated(const QString &binId, int role) { std::shared_ptr item = getItemByBinId(binId); if (item) { onItemUpdated(item, role); } } std::shared_ptr ProjectItemModel::getClipByBinID(const QString &binId) { if (binId.contains(QLatin1Char('_'))) { return getClipByBinID(binId.section(QLatin1Char('_'), 0, 0)); } for (const auto &clip : m_allItems) { auto c = std::static_pointer_cast(clip.second.lock()); if (c->itemType() == AbstractProjectItem::ClipItem && c->clipId() == binId) { return std::static_pointer_cast(c); } } return nullptr; } bool ProjectItemModel::hasClip(const QString &binId) { return getClipByBinID(binId) != nullptr; } std::shared_ptr ProjectItemModel::getFolderByBinId(const QString &binId) { for (const auto &clip : m_allItems) { auto c = std::static_pointer_cast(clip.second.lock()); if (c->itemType() == AbstractProjectItem::FolderItem && c->clipId() == binId) { return std::static_pointer_cast(c); } } return nullptr; } const QString ProjectItemModel::getFolderIdByName(const QString &folderName) { for (const auto &clip : m_allItems) { auto c = std::static_pointer_cast(clip.second.lock()); if (c->itemType() == AbstractProjectItem::FolderItem && c->name() == folderName) { return c->clipId(); } } return QString(); } std::shared_ptr ProjectItemModel::getItemByBinId(const QString &binId) { for (const auto &clip : m_allItems) { auto c = std::static_pointer_cast(clip.second.lock()); if (c->clipId() == binId) { return c; } } return nullptr; } void ProjectItemModel::setBinEffectsEnabled(bool enabled) { return std::static_pointer_cast(rootItem)->setBinEffectsEnabled(enabled); } QStringList ProjectItemModel::getEnclosingFolderInfo(const QModelIndex &index) const { QStringList noInfo; noInfo << QString::number(-1); noInfo << QString(); if (!index.isValid()) { return noInfo; } std::shared_ptr currentItem = getBinItemByIndex(index); auto folder = currentItem->getEnclosingFolder(true); if ((folder == nullptr) || folder == rootItem) { return noInfo; } QStringList folderInfo; folderInfo << currentItem->clipId(); folderInfo << currentItem->name(); return folderInfo; } void ProjectItemModel::clean() { std::vector> toDelete; + toDelete.reserve((size_t)rootItem->childCount()); for (int i = 0; i < rootItem->childCount(); ++i) { toDelete.push_back(std::static_pointer_cast(rootItem->child(i))); } Fun undo = []() { return true; }; Fun redo = []() { return true; }; for (const auto &child : toDelete) { requestBinClipDeletion(child, undo, redo); } Q_ASSERT(rootItem->childCount() == 0); m_nextId = 1; m_fileWatcher->clear(); } std::shared_ptr ProjectItemModel::getRootFolder() const { return std::static_pointer_cast(rootItem); } void ProjectItemModel::loadSubClips(const QString &id, const QMap &dataMap) { Fun undo = []() { return true; }; Fun redo = []() { return true; }; loadSubClips(id, dataMap, undo, redo); } void ProjectItemModel::loadSubClips(const QString &id, const QMap &dataMap, Fun &undo, Fun &redo) { std::shared_ptr clip = getClipByBinID(id); if (!clip) { return; } QMapIterator i(dataMap); QList missingThumbs; int maxFrame = clip->duration().frames(pCore->getCurrentFps()) - 1; while (i.hasNext()) { i.next(); if (!i.value().contains(QLatin1Char(';'))) { // Problem, the zone has no in/out points continue; } int in = i.value().section(QLatin1Char(';'), 0, 0).toInt(); int out = i.value().section(QLatin1Char(';'), 1, 1).toInt(); if (maxFrame > 0) { out = qMin(out, maxFrame); } QString subId; requestAddBinSubClip(subId, in, out, i.key(), id, undo, redo); } } std::shared_ptr ProjectItemModel::getBinItemByIndex(const QModelIndex &index) const { return std::static_pointer_cast(getItemById((int)index.internalId())); } -bool ProjectItemModel::requestBinClipDeletion(std::shared_ptr clip, Fun &undo, Fun &redo) +bool ProjectItemModel::requestBinClipDeletion(const std::shared_ptr &clip, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); Q_ASSERT(clip); if (!clip) return false; int parentId = -1; if (auto ptr = clip->parent()) parentId = ptr->getId(); clip->selfSoftDelete(undo, redo); int id = clip->getId(); Fun operation = removeItem_lambda(id); Fun reverse = addItem_lambda(clip, parentId); bool res = operation(); if (res) { UPDATE_UNDO_REDO(operation, reverse, undo, redo); } return res; } void ProjectItemModel::registerItem(const std::shared_ptr &item) { auto clip = std::static_pointer_cast(item); m_binPlaylist->manageBinItemInsertion(clip); AbstractTreeModel::registerItem(item); if (clip->itemType() == AbstractProjectItem::ClipItem) { auto clipItem = std::static_pointer_cast(clip); updateWatcher(clipItem); } } void ProjectItemModel::deregisterItem(int id, TreeItem *item) { auto clip = static_cast(item); m_binPlaylist->manageBinItemDeletion(clip); // TODO : here, we should suspend jobs belonging to the item we delete. They can be restarted if the item is reinserted by undo AbstractTreeModel::deregisterItem(id, item); if (clip->itemType() == AbstractProjectItem::ClipItem) { auto clipItem = static_cast(clip); m_fileWatcher->removeFile(clipItem->clipId()); } } int ProjectItemModel::getFreeFolderId() { while (!isIdFree(QString::number(++m_nextId))) { }; return m_nextId; } int ProjectItemModel::getFreeClipId() { while (!isIdFree(QString::number(++m_nextId))) { }; return m_nextId; } -bool ProjectItemModel::addItem(std::shared_ptr item, const QString &parentId, Fun &undo, Fun &redo) +bool ProjectItemModel::addItem(const std::shared_ptr &item, const QString &parentId, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); std::shared_ptr parentItem = getItemByBinId(parentId); if (!parentItem) { qCDebug(KDENLIVE_LOG) << " / / ERROR IN PARENT FOLDER"; return false; } if (item->itemType() == AbstractProjectItem::ClipItem && parentItem->itemType() != AbstractProjectItem::FolderItem) { qCDebug(KDENLIVE_LOG) << " / / ERROR when inserting clip: a clip should be inserted in a folder"; return false; } if (item->itemType() == AbstractProjectItem::SubClipItem && parentItem->itemType() != AbstractProjectItem::ClipItem) { qCDebug(KDENLIVE_LOG) << " / / ERROR when inserting subclip: a subclip should be inserted in a clip"; return false; } Fun operation = addItem_lambda(item, parentItem->getId()); int itemId = item->getId(); Fun reverse = removeItem_lambda(itemId); bool res = operation(); Q_ASSERT(item->isInModel()); if (res) { UPDATE_UNDO_REDO(operation, reverse, undo, redo); } return res; } bool ProjectItemModel::requestAddFolder(QString &id, const QString &name, const QString &parentId, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); if (!id.isEmpty() && !isIdFree(id)) { id = QString(); } if (id.isEmpty()) { id = QString::number(getFreeFolderId()); } std::shared_ptr new_folder = ProjectFolder::construct(id, name, std::static_pointer_cast(shared_from_this())); return addItem(new_folder, parentId, undo, redo); } bool ProjectItemModel::requestAddBinClip(QString &id, const QDomElement &description, const QString &parentId, Fun &undo, Fun &redo) { qDebug() << "/////////// requestAddBinClip" << parentId; QWriteLocker locker(&m_lock); if (id.isEmpty()) { id = Xml::getTagContentByAttribute(description, QStringLiteral("property"), QStringLiteral("name"), QStringLiteral("kdenlive:id"), QStringLiteral("-1")); if (id == QStringLiteral("-1") || !isIdFree(id)) { id = QString::number(getFreeClipId()); } } Q_ASSERT(!id.isEmpty() && isIdFree(id)); qDebug() << "/////////// found id" << id; std::shared_ptr new_clip = ProjectClip::construct(id, description, m_blankThumb, std::static_pointer_cast(shared_from_this())); qDebug() << "/////////// constructed "; bool res = addItem(new_clip, parentId, undo, redo); qDebug() << "/////////// added " << res; if (res) { int loadJob = pCore->jobManager()->startJob({id}, -1, QString(), description); pCore->jobManager()->startJob({id}, loadJob, QString(), 150, 0, true); pCore->jobManager()->startJob({id}, loadJob, QString()); } return res; } bool ProjectItemModel::requestAddBinClip(QString &id, const QDomElement &description, const QString &parentId, const QString &undoText) { Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool res = requestAddBinClip(id, description, parentId, undo, redo); if (res) { pCore->pushUndo(undo, redo, undoText.isEmpty() ? i18n("Add bin clip") : undoText); } return res; } -bool ProjectItemModel::requestAddBinClip(QString &id, std::shared_ptr producer, const QString &parentId, Fun &undo, Fun &redo) +bool ProjectItemModel::requestAddBinClip(QString &id, const std::shared_ptr &producer, const QString &parentId, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); if (id.isEmpty()) { id = QString::number(producer->get_int("kdenlive:id")); if (!isIdFree(id)) { id = QString::number(getFreeClipId()); } } Q_ASSERT(!id.isEmpty() && isIdFree(id)); std::shared_ptr new_clip = ProjectClip::construct(id, m_blankThumb, std::static_pointer_cast(shared_from_this()), producer); bool res = addItem(new_clip, parentId, undo, redo); if (res) { int blocking = pCore->jobManager()->getBlockingJobId(id, AbstractClipJob::LOADJOB); pCore->jobManager()->startJob({id}, blocking, QString(), 150, -1, true); pCore->jobManager()->startJob({id}, blocking, QString()); } return res; } bool ProjectItemModel::requestAddBinSubClip(QString &id, int in, int out, const QString &zoneName, const QString &parentId, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); if (id.isEmpty()) { id = QString::number(getFreeClipId()); } Q_ASSERT(!id.isEmpty() && isIdFree(id)); QString subId = parentId; if (subId.startsWith(QLatin1Char('A')) || subId.startsWith(QLatin1Char('V'))) { subId.remove(0, 1); } auto clip = getClipByBinID(subId); Q_ASSERT(clip->itemType() == AbstractProjectItem::ClipItem); auto tc = pCore->currentDoc()->timecode().getDisplayTimecodeFromFrames(in, KdenliveSettings::frametimecode()); std::shared_ptr new_clip = ProjectSubClip::construct(id, clip, std::static_pointer_cast(shared_from_this()), in, out, tc, zoneName); bool res = addItem(new_clip, subId, undo, redo); if (res) { int parentJob = pCore->jobManager()->getBlockingJobId(subId, AbstractClipJob::LOADJOB); pCore->jobManager()->startJob({id}, parentJob, QString(), 150, -1, true); } return res; } bool ProjectItemModel::requestAddBinSubClip(QString &id, int in, int out, const QString &zoneName, const QString &parentId) { Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool res = requestAddBinSubClip(id, in, out, zoneName, parentId, undo, redo); if (res) { pCore->pushUndo(undo, redo, i18n("Add a sub clip")); } return res; } -Fun ProjectItemModel::requestRenameFolder_lambda(std::shared_ptr folder, const QString &newName) +Fun ProjectItemModel::requestRenameFolder_lambda(const std::shared_ptr &folder, const QString &newName) { int id = folder->getId(); return [this, id, newName]() { auto currentFolder = std::static_pointer_cast(m_allItems[id].lock()); if (!currentFolder) { return false; } currentFolder->setName(newName); m_binPlaylist->manageBinFolderRename(currentFolder); auto index = getIndexFromItem(currentFolder); emit dataChanged(index, index, {AbstractProjectItem::DataName}); return true; }; } -bool ProjectItemModel::requestRenameFolder(std::shared_ptr folder, const QString &name, Fun &undo, Fun &redo) +bool ProjectItemModel::requestRenameFolder(const std::shared_ptr &folder, const QString &name, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); QString oldName = folder->name(); auto operation = requestRenameFolder_lambda(folder, name); if (operation()) { auto reverse = requestRenameFolder_lambda(folder, oldName); UPDATE_UNDO_REDO(operation, reverse, undo, redo); return true; } return false; } bool ProjectItemModel::requestRenameFolder(std::shared_ptr folder, const QString &name) { Fun undo = []() { return true; }; Fun redo = []() { return true; }; - bool res = requestRenameFolder(folder, name, undo, redo); + bool res = requestRenameFolder(std::move(folder), name, undo, redo); if (res) { pCore->pushUndo(undo, redo, i18n("Rename Folder")); } return res; } bool ProjectItemModel::requestCleanup() { Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool res = true; std::vector> to_delete; // Iterate to find clips that are not in timeline for (const auto &clip : m_allItems) { auto c = std::static_pointer_cast(clip.second.lock()); if (c->itemType() == AbstractProjectItem::ClipItem && !c->isIncludedInTimeline()) { to_delete.push_back(c); } } // it is important to execute deletion in a separate loop, because otherwise // the iterators of m_allItems get messed up for (const auto &c : to_delete) { res = requestBinClipDeletion(c, undo, redo); if (!res) { bool undone = undo(); Q_ASSERT(undone); return false; } } pCore->pushUndo(undo, redo, i18n("Clean Project")); return true; } std::vector ProjectItemModel::getAllClipIds() const { std::vector result; for (const auto &clip : m_allItems) { auto c = std::static_pointer_cast(clip.second.lock()); if (c->itemType() == AbstractProjectItem::ClipItem) { result.push_back(c->clipId()); } } return result; } QStringList ProjectItemModel::getClipByUrl(const QFileInfo &url) const { QStringList result; for (const auto &clip : m_allItems) { auto c = std::static_pointer_cast(clip.second.lock()); if (c->itemType() == AbstractProjectItem::ClipItem) { if (QFileInfo(std::static_pointer_cast(c)->clipUrl()) == url) { result << c->clipId(); } } } return result; } bool ProjectItemModel::loadFolders(Mlt::Properties &folders) { // At this point, we expect the folders properties to have a name of the form "x.y" where x is the id of the parent folder and y the id of the child. // Note that for root folder, x = -1 // The value of the property is the name of the child folder std::unordered_map> downLinks; // key are parents, value are children std::unordered_map upLinks; // key are children, value are parent std::unordered_map newIds; // we store the correspondence to the new ids std::unordered_map folderNames; newIds[-1] = getRootFolder()->clipId(); if (folders.count() == 0) return true; for (int i = 0; i < folders.count(); i++) { QString folderName = folders.get(i); QString id = folders.get_name(i); int parentId = id.section(QLatin1Char('.'), 0, 0).toInt(); int folderId = id.section(QLatin1Char('.'), 1, 1).toInt(); downLinks[parentId].push_back(folderId); upLinks[folderId] = parentId; folderNames[folderId] = folderName; qDebug() << "Found folder " << folderId << "name = " << folderName << "parent=" << parentId; } // In case there are some non-existant parent, we fall back to root for (const auto &f : downLinks) { if (upLinks.count(f.first) == 0) { upLinks[f.first] = -1; } if (f.first != -1 && downLinks.count(upLinks[f.first]) == 0) { qDebug() << "Warning: parent folder " << upLinks[f.first] << "for folder" << f.first << "is invalid. Folder will be placed in topmost directory."; upLinks[f.first] = -1; } } // We now do a BFS to construct the folders in order Q_ASSERT(downLinks.count(-1) > 0); Fun undo = []() { return true; }; Fun redo = []() { return true; }; std::queue queue; std::unordered_set seen; queue.push(-1); while (!queue.empty()) { int current = queue.front(); seen.insert(current); queue.pop(); if (current != -1) { QString id = QString::number(current); bool res = requestAddFolder(id, folderNames[current], newIds[upLinks[current]], undo, redo); if (!res) { bool undone = undo(); Q_ASSERT(undone); return false; } newIds[current] = id; } for (int c : downLinks[current]) { queue.push(c); } } return true; } bool ProjectItemModel::isIdFree(const QString &id) const { for (const auto &clip : m_allItems) { auto c = std::static_pointer_cast(clip.second.lock()); if (c->clipId() == id) { return false; } } return true; } void ProjectItemModel::loadBinPlaylist(Mlt::Tractor *documentTractor, Mlt::Tractor *modelTractor, std::unordered_map &binIdCorresp) { clean(); Mlt::Properties retainList((mlt_properties)documentTractor->get_data("xml_retain")); qDebug() << "Loading bin playlist..."; if (retainList.is_valid()) { qDebug() << "retain is valid"; Mlt::Playlist playlist((mlt_playlist)retainList.get_data(BinPlaylist::binPlaylistId.toUtf8().constData())); if (playlist.is_valid() && playlist.type() == playlist_type) { qDebug() << "playlist is valid"; // Load bin clips qDebug() << "init bin"; // Load folders Mlt::Properties folderProperties; Mlt::Properties playlistProps(playlist.get_properties()); folderProperties.pass_values(playlistProps, "kdenlive:folder."); loadFolders(folderProperties); // Read notes QString notes = playlistProps.get("kdenlive:documentnotes"); pCore->projectManager()->setDocumentNotes(notes); Fun undo = []() { return true; }; Fun redo = []() { return true; }; qDebug() << "Found " << playlist.count() << "clips"; int max = playlist.count(); for (int i = 0; i < max; i++) { QScopedPointer prod(playlist.get_clip(i)); std::shared_ptr producer(new Mlt::Producer(prod->parent())); qDebug() << "dealing with bin clip" << i; if (producer->is_blank() || !producer->is_valid()) { qDebug() << "producer is not valid or blank"; continue; } QString id = qstrdup(producer->get("kdenlive:id")); QString parentId = qstrdup(producer->get("kdenlive:folderid")); if (parentId.isEmpty()) { parentId = QStringLiteral("-1"); } qDebug() << "clip id" << id; if (id.contains(QLatin1Char('_'))) { // TODO refac ? /* // This is a track producer QString mainId = id.section(QLatin1Char('_'), 0, 0); // QString track = id.section(QStringLiteral("_"), 1, 1); if (m_clipList.contains(mainId)) { // The controller for this track producer already exists } else { // Create empty controller for this clip requestClipInfo info; info.imageHeight = 0; info.clipId = id; info.replaceProducer = true; emit slotProducerReady(info, ClipController::mediaUnavailable); } */ } else { QString newId = isIdFree(id) ? id : QString::number(getFreeClipId()); producer->set("_kdenlive_processed", 1); requestAddBinClip(newId, producer, parentId, undo, redo); binIdCorresp[id] = newId; qDebug() << "Loaded clip " << id << "under id" << newId; } } } } m_binPlaylist->setRetainIn(modelTractor); } /** @brief Save document properties in MLT's bin playlist */ void ProjectItemModel::saveDocumentProperties(const QMap &props, const QMap &metadata, std::shared_ptr guideModel) { - m_binPlaylist->saveDocumentProperties(props, metadata, guideModel); + m_binPlaylist->saveDocumentProperties(props, metadata, std::move(guideModel)); } void ProjectItemModel::saveProperty(const QString &name, const QString &value) { m_binPlaylist->saveProperty(name, value); } QMap ProjectItemModel::getProxies(const QString &root) { return m_binPlaylist->getProxies(root); } void ProjectItemModel::reloadClip(const QString &binId) { std::shared_ptr clip = getClipByBinID(binId); if (clip) { clip->reloadProducer(); } } void ProjectItemModel::setClipWaiting(const QString &binId) { std::shared_ptr clip = getClipByBinID(binId); if (clip) { clip->setClipStatus(AbstractProjectItem::StatusWaiting); } } void ProjectItemModel::setClipInvalid(const QString &binId) { std::shared_ptr clip = getClipByBinID(binId); if (clip) { clip->setClipStatus(AbstractProjectItem::StatusMissing); // TODO: set producer as blank invalid } } -void ProjectItemModel::updateWatcher(std::shared_ptr clipItem) +void ProjectItemModel::updateWatcher(const std::shared_ptr &clipItem) { if (clipItem->clipType() == ClipType::AV || clipItem->clipType() == ClipType::Audio || clipItem->clipType() == ClipType::Image || clipItem->clipType() == ClipType::Video || clipItem->clipType() == ClipType::Playlist || clipItem->clipType() == ClipType::TextTemplate) { m_fileWatcher->removeFile(clipItem->clipId()); m_fileWatcher->addFile(clipItem->clipId(), clipItem->clipUrl()); } } void ProjectItemModel::setDragType(PlaylistState::ClipState type) { m_dragType = type; } int ProjectItemModel::clipsCount() const { return m_binPlaylist->count(); } diff --git a/src/bin/projectitemmodel.h b/src/bin/projectitemmodel.h index fdea9fb4f..c16061b94 100644 --- a/src/bin/projectitemmodel.h +++ b/src/bin/projectitemmodel.h @@ -1,260 +1,260 @@ /* Copyright (C) 2012 Till Theato Copyright (C) 2014 Jean-Baptiste Mardelle Copyright (C) 2017 Nicolas Carion This file is part of Kdenlive. See www.kdenlive.org. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef PROJECTITEMMODEL_H #define PROJECTITEMMODEL_H #include "abstractmodel/abstracttreemodel.hpp" #include "definitions.h" #include "undohelper.hpp" #include #include #include #include #include class AbstractProjectItem; class BinPlaylist; class FileWatcher; class MarkerListModel; class ProjectClip; class ProjectFolder; namespace Mlt { class Producer; class Properties; class Tractor; } // namespace Mlt /** * @class ProjectItemModel * @brief Acts as an adaptor to be able to use BinModel with item views. */ class ProjectItemModel : public AbstractTreeModel { Q_OBJECT protected: explicit ProjectItemModel(QObject *parent); public: static std::shared_ptr construct(QObject *parent = nullptr); ~ProjectItemModel(); friend class ProjectClip; /** @brief Returns a clip from the hierarchy, given its id */ std::shared_ptr getClipByBinID(const QString &binId); /** @brief Returns a list of clips using the given url */ QStringList getClipByUrl(const QFileInfo &url) const; /** @brief Helper to check whether a clip with a given id exists */ bool hasClip(const QString &binId); /** @brief Gets a folder by its id. If none is found, nullptr is returned */ std::shared_ptr getFolderByBinId(const QString &binId); /** @brief Gets a id folder by its name. If none is found, empty string returned */ const QString getFolderIdByName(const QString &folderName); /** @brief Gets any item by its id. */ std::shared_ptr getItemByBinId(const QString &binId); /** @brief This function change the global enabled state of the bin effects */ void setBinEffectsEnabled(bool enabled); /** @brief Returns some info about the folder containing the given index */ QStringList getEnclosingFolderInfo(const QModelIndex &index) const; /** @brief Deletes all element and start a fresh model */ void clean(); /** @brief Returns the id of all the clips (excluding folders) */ std::vector getAllClipIds() const; /** @brief Convenience method to access root folder */ std::shared_ptr getRootFolder() const; /** @brief Create the subclips defined in the parent clip. @param id is the id of the parent clip @param data is a definition of the subclips (keys are subclips' names, value are "in:out")*/ void loadSubClips(const QString &id, const QMap &data); void loadSubClips(const QString &id, const QMap &dataMap, Fun &undo, Fun &redo); /* @brief Convenience method to retrieve a pointer to an element given its index */ std::shared_ptr getBinItemByIndex(const QModelIndex &index) const; /* @brief Load the folders given the property containing them */ bool loadFolders(Mlt::Properties &folders); /* @brief Parse a bin playlist from the document tractor and reconstruct the tree */ void loadBinPlaylist(Mlt::Tractor *documentTractor, Mlt::Tractor *modelTractor, std::unordered_map &binIdCorresp); /** @brief Save document properties in MLT's bin playlist */ void saveDocumentProperties(const QMap &props, const QMap &metadata, std::shared_ptr guideModel); /** @brief Save a property to main bin */ void saveProperty(const QString &name, const QString &value); /** @brief Returns item data depending on role requested */ QVariant data(const QModelIndex &index, int role) const override; /** @brief Called when user edits an item */ bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; /** @brief Allow selection and drag & drop */ Qt::ItemFlags flags(const QModelIndex &index) const override; /** @brief Returns column names in case we want to use columns in QTreeView */ QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; /** @brief Mandatory reimplementation from QAbstractItemModel */ int columnCount(const QModelIndex &parent = QModelIndex()) const override; /** @brief Returns the MIME type used for Drag actions */ QStringList mimeTypes() const override; /** @brief Create data that will be used for Drag events */ QMimeData *mimeData(const QModelIndexList &indices) const override; /** @brief Set size for thumbnails */ void setIconSize(QSize s); bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) override; Qt::DropActions supportedDropActions() const override; /* @brief Request deletion of a bin clip from the project bin @param clip : pointer to the clip to delete @param undo,redo: lambdas that are updated to accumulate operation. */ - bool requestBinClipDeletion(std::shared_ptr clip, Fun &undo, Fun &redo); + bool requestBinClipDeletion(const std::shared_ptr &clip, Fun &undo, Fun &redo); /* @brief Request creation of a bin folder @param id Id of the requested bin. If this is empty or invalid (already used, for example), it will be used as a return parameter to give the automatic bin id used. @param name Name of the folder @param parentId Bin id of the parent folder @param undo,redo: lambdas that are updated to accumulate operation. */ bool requestAddFolder(QString &id, const QString &name, const QString &parentId, Fun &undo, Fun &redo); /* @brief Request creation of a bin clip @param id Id of the requested bin. If this is empty, it will be used as a return parameter to give the automatic bin id used. @param description Xml description of the clip @param parentId Bin id of the parent folder @param undo,redo: lambdas that are updated to accumulate operation. */ bool requestAddBinClip(QString &id, const QDomElement &description, const QString &parentId, Fun &undo, Fun &redo); bool requestAddBinClip(QString &id, const QDomElement &description, const QString &parentId, const QString &undoText = QString()); /* @brief This is the addition function when we already have a producer for the clip*/ - bool requestAddBinClip(QString &id, std::shared_ptr producer, const QString &parentId, Fun &undo, Fun &redo); + bool requestAddBinClip(QString &id, const std::shared_ptr &producer, const QString &parentId, Fun &undo, Fun &redo); /* @brief Create a subClip @param id Id of the requested bin. If this is empty, it will be used as a return parameter to give the automatic bin id used. @param parentId Bin id of the parent clip @param in,out : zone that corresponds to the subclip @param undo,redo: lambdas that are updated to accumulate operation. */ bool requestAddBinSubClip(QString &id, int in, int out, const QString &zoneName, const QString &parentId, Fun &undo, Fun &redo); bool requestAddBinSubClip(QString &id, int in, int out, const QString &zoneName, const QString &parentId); /* @brief Request that a folder's name is changed @param clip : pointer to the folder to rename @param name: new name of the folder @param undo,redo: lambdas that are updated to accumulate operation. */ - bool requestRenameFolder(std::shared_ptr folder, const QString &name, Fun &undo, Fun &redo); + bool requestRenameFolder(const std::shared_ptr &folder, const QString &name, Fun &undo, Fun &redo); /* Same functions but pushes the undo object directly */ bool requestRenameFolder(std::shared_ptr folder, const QString &name); /* @brief Request that the unused clips are deleted */ bool requestCleanup(); /* @brief Retrieves the next id available for attribution to a folder */ int getFreeFolderId(); /* @brief Retrieves the next id available for attribution to a clip */ int getFreeClipId(); /** @brief Retrieve a list of proxy/original urls */ QMap getProxies(const QString &root); /** @brief Request that the producer of a given clip is reloaded */ void reloadClip(const QString &binId); /** @brief Set the status of the clip to "waiting". This happens when the corresponding file has changed*/ void setClipWaiting(const QString &binId); void setClipInvalid(const QString &binId); /** @brief Number of clips in the bin playlist */ int clipsCount() const; protected: /* @brief Register the existence of a new element */ void registerItem(const std::shared_ptr &item) override; /* @brief Deregister the existence of a new element*/ void deregisterItem(int id, TreeItem *item) override; /* @brief Helper function to generate a lambda that rename a folder */ - Fun requestRenameFolder_lambda(std::shared_ptr folder, const QString &newName); + Fun requestRenameFolder_lambda(const std::shared_ptr &folder, const QString &newName); /* @brief Helper function to add a given item to the tree */ - bool addItem(std::shared_ptr item, const QString &parentId, Fun &undo, Fun &redo); + bool addItem(const std::shared_ptr &item, const QString &parentId, Fun &undo, Fun &redo); /* @brief Function to be called when the url of a clip changes */ - void updateWatcher(std::shared_ptr item); + void updateWatcher(const std::shared_ptr &item); public slots: /** @brief An item in the list was modified, notify */ - void onItemUpdated(std::shared_ptr item, int role); + void onItemUpdated(const std::shared_ptr &item, int role); void onItemUpdated(const QString &binId, int role); /** @brief Check whether a given id is currently used or not*/ bool isIdFree(const QString &id) const; void setDragType(PlaylistState::ClipState type); private: /** @brief Return reference to column specific data */ int mapToColumn(int column) const; mutable QReadWriteLock m_lock; // This is a lock that ensures safety in case of concurrent access std::unique_ptr m_binPlaylist; std::unique_ptr m_fileWatcher; int m_nextId; QIcon m_blankThumb; PlaylistState::ClipState m_dragType; signals: // thumbs of the given clip were modified, request update of the monitor if need be void refreshAudioThumbs(const QString &id); void refreshClip(const QString &id); void emitMessage(const QString &, int, MessageType); void updateTimelineProducers(const QString &id, const QMap &passProperties); void refreshPanel(const QString &id); void requestAudioThumbs(const QString &id, long duration); // TODO void markersNeedUpdate(const QString &id, const QList &); void itemDropped(const QStringList &, const QModelIndex &); void itemDropped(const QList &, const QModelIndex &); void effectDropped(const QStringList &, const QModelIndex &); void addClipCut(const QString &, int, int); }; #endif diff --git a/src/bin/projectsubclip.cpp b/src/bin/projectsubclip.cpp index 5bb2f4c18..b04461fd8 100644 --- a/src/bin/projectsubclip.cpp +++ b/src/bin/projectsubclip.cpp @@ -1,174 +1,175 @@ /* Copyright (C) 2015 Jean-Baptiste Mardelle This file is part of Kdenlive. See www.kdenlive.org. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "projectsubclip.h" #include "projectclip.h" #include "projectitemmodel.h" #include #include #include class ClipController; ProjectSubClip::ProjectSubClip(const QString &id, const std::shared_ptr &parent, const std::shared_ptr &model, int in, int out, const QString &timecode, const QString &name) : AbstractProjectItem(AbstractProjectItem::SubClipItem, id, model) , m_masterClip(parent) , m_out(out) { m_inPoint = in; m_duration = timecode; QPixmap pix(64, 36); pix.fill(Qt::lightGray); m_thumbnail = QIcon(pix); if (name.isEmpty()) { m_name = i18n("Zone %1", parent->childCount() + 1); } else { m_name = name; } m_clipStatus = StatusReady; // Save subclip in MLT parent->setProducerProperty("kdenlive:clipzone." + m_name, QString::number(in) + QLatin1Char(';') + QString::number(out)); connect(parent.get(), &ProjectClip::thumbReady, this, &ProjectSubClip::gotThumb); } -std::shared_ptr ProjectSubClip::construct(const QString &id, std::shared_ptr parent, std::shared_ptr model, - int in, int out, const QString &timecode, const QString &name) +std::shared_ptr ProjectSubClip::construct(const QString &id, const std::shared_ptr &parent, + const std::shared_ptr &model, int in, int out, const QString &timecode, + const QString &name) { std::shared_ptr self(new ProjectSubClip(id, parent, model, in, out, timecode, name)); baseFinishConstruct(self); return self; } ProjectSubClip::~ProjectSubClip() { // controller is deleted in bincontroller } void ProjectSubClip::gotThumb(int pos, const QImage &img) { if (pos == m_inPoint) { setThumbnail(img); disconnect(m_masterClip.get(), &ProjectClip::thumbReady, this, &ProjectSubClip::gotThumb); } } void ProjectSubClip::discard() { if (m_masterClip) { m_masterClip->resetProducerProperty("kdenlive:clipzone." + m_name); } } QString ProjectSubClip::getToolTip() const { return QStringLiteral("test"); } std::shared_ptr ProjectSubClip::clip(const QString &id) { Q_UNUSED(id); return std::shared_ptr(); } std::shared_ptr ProjectSubClip::folder(const QString &id) { Q_UNUSED(id); return std::shared_ptr(); } void ProjectSubClip::setBinEffectsEnabled(bool) {} GenTime ProjectSubClip::duration() const { // TODO return GenTime(); } QPoint ProjectSubClip::zone() const { return QPoint(m_inPoint, m_out); } std::shared_ptr ProjectSubClip::clipAt(int ix) { Q_UNUSED(ix); return std::shared_ptr(); } QDomElement ProjectSubClip::toXml(QDomDocument &document, bool) { QDomElement sub = document.createElement(QStringLiteral("subclip")); sub.setAttribute(QStringLiteral("id"), m_masterClip->AbstractProjectItem::clipId()); sub.setAttribute(QStringLiteral("in"), m_inPoint); sub.setAttribute(QStringLiteral("out"), m_out); return sub; } std::shared_ptr ProjectSubClip::subClip(int in, int out) { if (m_inPoint == in && m_out == out) { return std::static_pointer_cast(shared_from_this()); } return std::shared_ptr(); } void ProjectSubClip::setThumbnail(const QImage &img) { QPixmap thumb = roundedPixmap(QPixmap::fromImage(img)); m_thumbnail = QIcon(thumb); if (auto ptr = m_model.lock()) std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this()), AbstractProjectItem::DataThumbnail); } QPixmap ProjectSubClip::thumbnail(int width, int height) { return m_thumbnail.pixmap(width, height); } bool ProjectSubClip::rename(const QString &name, int column) { // TODO refac: rework this Q_UNUSED(column) if (m_name == name) { return false; } // Rename folder // if (auto ptr = m_model.lock()) std::static_pointer_cast(ptr)->bin()->renameSubClipCommand(m_binId, name, m_name, m_in, m_out); return true; } std::shared_ptr ProjectSubClip::getMasterClip() const { return m_masterClip; } ClipType::ProducerType ProjectSubClip::clipType() const { return m_masterClip->clipType(); } bool ProjectSubClip::hasAudioAndVideo() const { return m_masterClip->hasAudioAndVideo(); } diff --git a/src/bin/projectsubclip.h b/src/bin/projectsubclip.h index f327be913..59a0f2cc5 100644 --- a/src/bin/projectsubclip.h +++ b/src/bin/projectsubclip.h @@ -1,96 +1,97 @@ /* Copyright (C) 2015 Jean-Baptiste Mardelle This file is part of Kdenlive. See www.kdenlive.org. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef PROJECTSUBCLIP_H #define PROJECTSUBCLIP_H #include "abstractprojectitem.h" #include "definitions.h" #include class ProjectFolder; class ProjectClip; class QDomElement; namespace Mlt { } /** * @class ProjectSubClip * @brief Represents a clip in the project (not timeline). * */ class ProjectSubClip : public AbstractProjectItem { Q_OBJECT public: /** * @brief Constructor; used when loading a project and the producer is already available. */ - static std::shared_ptr construct(const QString &id, std::shared_ptr parent, std::shared_ptr model, int in, - int out, const QString &timecode, const QString &name = QString()); + static std::shared_ptr construct(const QString &id, const std::shared_ptr &parent, + const std::shared_ptr &model, int in, int out, const QString &timecode, + const QString &name = QString()); protected: ProjectSubClip(const QString &id, const std::shared_ptr &parent, const std::shared_ptr &model, int in, int out, const QString &timecode, const QString &name = QString()); public: virtual ~ProjectSubClip(); std::shared_ptr clip(const QString &id) override; std::shared_ptr folder(const QString &id) override; std::shared_ptr subClip(int in, int out); std::shared_ptr clipAt(int ix) override; /** @brief Recursively disable/enable bin effects. */ void setBinEffectsEnabled(bool enabled) override; QDomElement toXml(QDomDocument &document, bool includeMeta = false) override; /** @brief Returns the clip's duration. */ GenTime duration() const; /** @brief Sets thumbnail for this clip. */ void setThumbnail(const QImage &); QPixmap thumbnail(int width, int height); /** @brief Remove reference to this subclip in the master clip, to be done before a subclip is deleted. */ void discard(); QPoint zone() const override; QString getToolTip() const override; bool rename(const QString &name, int column) override; /** @brief Returns true if item has both audio and video enabled. */ bool hasAudioAndVideo() const override; /** @brief returns a pointer to the parent clip */ std::shared_ptr getMasterClip() const; ClipType::ProducerType clipType() const override; private: std::shared_ptr m_masterClip; int m_out; private slots: void gotThumb(int pos, const QImage &img); }; #endif diff --git a/src/capture/mltdevicecapture.cpp b/src/capture/mltdevicecapture.cpp index 165623be0..fd0b565c6 100644 --- a/src/capture/mltdevicecapture.cpp +++ b/src/capture/mltdevicecapture.cpp @@ -1,689 +1,689 @@ /*************************************************************************** mltdevicecapture.cpp - description ------------------- begin : Sun May 21 2011 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. * * * ***************************************************************************/ #include "mltdevicecapture.h" #include "definitions.h" #include "kdenlivesettings.h" #include #include "kdenlive_debug.h" #include #include #include #include #include static void consumer_gl_frame_show(mlt_consumer, MltDeviceCapture *self, mlt_frame frame_ptr) { // detect if the producer has finished playing. Is there a better way to do it? Mlt::Frame frame(frame_ptr); self->showFrame(frame); } -MltDeviceCapture::MltDeviceCapture(QString profile, /*VideoSurface *surface, */ QWidget *parent) +MltDeviceCapture::MltDeviceCapture(const QString &profile, /*VideoSurface *surface, */ QWidget *parent) : AbstractRender(Kdenlive::RecordMonitor, parent) , doCapture(0) , processingImage(false) , m_mltConsumer(nullptr) , m_mltProducer(nullptr) , m_mltProfile(nullptr) , m_showFrameEvent(nullptr) , m_droppedFrames(0) , m_livePreview(KdenliveSettings::enable_recording_preview()) { analyseAudio = KdenliveSettings::monitor_audio(); if (profile.isEmpty()) { // profile = KdenliveSettings::current_profile(); } buildConsumer(profile); connect(this, &MltDeviceCapture::unblockPreview, this, &MltDeviceCapture::slotPreparePreview); m_droppedFramesTimer.setSingleShot(false); m_droppedFramesTimer.setInterval(1000); connect(&m_droppedFramesTimer, &QTimer::timeout, this, &MltDeviceCapture::slotCheckDroppedFrames); } MltDeviceCapture::~MltDeviceCapture() { delete m_mltConsumer; delete m_mltProducer; delete m_mltProfile; } bool MltDeviceCapture::buildConsumer(const QString &profileName) { if (!profileName.isEmpty()) { m_activeProfile = profileName; } delete m_mltProfile; char *tmp = qstrdup(m_activeProfile.toUtf8().constData()); qputenv("MLT_PROFILE", tmp); m_mltProfile = new Mlt::Profile(tmp); m_mltProfile->set_explicit(1); delete[] tmp; QString videoDriver = KdenliveSettings::videodrivername(); if (!videoDriver.isEmpty()) { if (videoDriver == QLatin1String("x11_noaccel")) { qputenv("SDL_VIDEO_YUV_HWACCEL", "0"); videoDriver = QStringLiteral("x11"); } else { qunsetenv("SDL_VIDEO_YUV_HWACCEL"); } } qputenv("SDL_VIDEO_ALLOW_SCREENSAVER", "1"); // OpenGL monitor m_mltConsumer = new Mlt::Consumer(*m_mltProfile, KdenliveSettings::audiobackend().toUtf8().constData()); m_mltConsumer->set("preview_off", 1); m_mltConsumer->set("preview_format", mlt_image_rgb24); m_showFrameEvent = m_mltConsumer->listen("consumer-frame-show", this, (mlt_listener)consumer_gl_frame_show); // m_mltConsumer->set("resize", 1); // m_mltConsumer->set("terminate_on_pause", 1); m_mltConsumer->set("window_background", KdenliveSettings::window_background().name().toUtf8().constData()); // m_mltConsumer->set("rescale", "nearest"); QString audioDevice = KdenliveSettings::audiodevicename(); if (!audioDevice.isEmpty()) { m_mltConsumer->set("audio_device", audioDevice.toUtf8().constData()); } if (!videoDriver.isEmpty()) { m_mltConsumer->set("video_driver", videoDriver.toUtf8().constData()); } QString audioDriver = KdenliveSettings::audiodrivername(); if (!audioDriver.isEmpty()) { m_mltConsumer->set("audio_driver", audioDriver.toUtf8().constData()); } // m_mltConsumer->set("progressive", 0); // m_mltConsumer->set("buffer", 1); // m_mltConsumer->set("real_time", 0); if (!m_mltConsumer->is_valid()) { delete m_mltConsumer; m_mltConsumer = nullptr; return false; } return true; } void MltDeviceCapture::pause() { if (m_mltConsumer) { m_mltConsumer->set("refresh", 0); // m_mltProducer->set_speed(0.0); m_mltConsumer->purge(); } } void MltDeviceCapture::stop() { m_droppedFramesTimer.stop(); bool isPlaylist = false; // disconnect(this, SIGNAL(imageReady(QImage)), this, SIGNAL(frameUpdated(QImage))); // m_captureDisplayWidget->stop(); delete m_showFrameEvent; m_showFrameEvent = nullptr; if (m_mltConsumer) { m_mltConsumer->set("refresh", 0); m_mltConsumer->purge(); m_mltConsumer->stop(); // if (!m_mltConsumer->is_stopped()) m_mltConsumer->stop(); } if (m_mltProducer) { QList prods; Mlt::Service service(m_mltProducer->parent().get_service()); mlt_service_lock(service.get_service()); if (service.type() == tractor_type) { isPlaylist = true; Mlt::Tractor tractor(service); mlt_tractor_close(tractor.get_tractor()); Mlt::Field *field = tractor.field(); mlt_service nextservice = mlt_service_get_producer(service.get_service()); mlt_service nextservicetodisconnect; mlt_properties properties = MLT_SERVICE_PROPERTIES(nextservice); QString mlt_type = mlt_properties_get(properties, "mlt_type"); QString resource = mlt_properties_get(properties, "mlt_service"); // Delete all transitions while (mlt_type == QLatin1String("transition")) { nextservicetodisconnect = nextservice; nextservice = mlt_service_producer(nextservice); mlt_field_disconnect_service(field->get_field(), nextservicetodisconnect); if (nextservice == nullptr) { break; } properties = MLT_SERVICE_PROPERTIES(nextservice); mlt_type = mlt_properties_get(properties, "mlt_type"); resource = mlt_properties_get(properties, "mlt_service"); } delete field; field = nullptr; } mlt_service_unlock(service.get_service()); delete m_mltProducer; m_mltProducer = nullptr; } // For some reason, the consumer seems to be deleted by previous stuff when in playlist mode if (!isPlaylist && (m_mltConsumer != nullptr)) { delete m_mltConsumer; } m_mltConsumer = nullptr; } void MltDeviceCapture::emitFrameUpdated(Mlt::Frame &frame) { /* //TEST: is it better to convert the frame in a thread outside of MLT?? if (processingImage) return; mlt_image_format format = (mlt_image_format) frame.get_int("format"); //mlt_image_rgb24; int width = frame.get_int("width"); int height = frame.get_int("height"); unsigned char *buffer = (unsigned char *) frame.get_data("image"); if (format == mlt_image_yuv422) { QtConcurrent::run(this, &MltDeviceCapture::uyvy2rgb, (unsigned char *) buffer, width, height); } */ mlt_image_format format = mlt_image_rgb24; int width = 0; int height = 0; const uchar *image = frame.get_image(format, width, height); QImage qimage(width, height, QImage::Format_RGB888); // QImage qimage(width, height, QImage::Format_ARGB32_Premultiplied); memcpy(qimage.bits(), image, (size_t)(width * height * 3)); emit frameUpdated(qimage); } void MltDeviceCapture::showFrame(Mlt::Frame &frame) { mlt_image_format format = mlt_image_rgb24; int width = 0; int height = 0; const uchar *image = frame.get_image(format, width, height); QImage qimage(width, height, QImage::Format_RGB888); memcpy(qimage.scanLine(0), image, static_cast(width * height * 3)); emit showImageSignal(qimage); if (sendFrameForAnalysis && (frame.get_frame()->convert_image != nullptr)) { emit frameUpdated(qimage.rgbSwapped()); } } void MltDeviceCapture::showAudio(Mlt::Frame &frame) { if (!frame.is_valid() || frame.get_int("test_audio") != 0) { return; } mlt_audio_format audio_format = mlt_audio_s16; int freq = 0; int num_channels = 0; int samples = 0; qint16 *data = (qint16 *)frame.get_audio(audio_format, freq, num_channels, samples); if (!data) { return; } // Data format: [ c00 c10 c01 c11 c02 c12 c03 c13 ... c0{samples-1} c1{samples-1} for 2 channels. // So the vector is of size samples*channels. audioShortVector sampleVector(samples * num_channels); memcpy(sampleVector.data(), data, (size_t)(samples * num_channels) * sizeof(qint16)); if (samples > 0) { emit audioSamplesSignal(sampleVector, freq, num_channels, samples); } } bool MltDeviceCapture::slotStartPreview(const QString &producer, bool xmlFormat) { if (m_mltConsumer == nullptr) { if (!buildConsumer()) { return false; } } char *tmp = qstrdup(producer.toUtf8().constData()); if (xmlFormat) { m_mltProducer = new Mlt::Producer(*m_mltProfile, "xml-string", tmp); } else { m_mltProducer = new Mlt::Producer(*m_mltProfile, tmp); } delete[] tmp; if (m_mltProducer == nullptr || !m_mltProducer->is_valid()) { if (m_mltProducer) { delete m_mltProducer; m_mltProducer = nullptr; } // qCDebug(KDENLIVE_LOG)<<"//// ERROR CREATRING PROD"; return false; } m_mltConsumer->connect(*m_mltProducer); if (m_mltConsumer->start() == -1) { delete m_mltConsumer; m_mltConsumer = nullptr; return false; } m_droppedFramesTimer.start(); // connect(this, SIGNAL(imageReady(QImage)), this, SIGNAL(frameUpdated(QImage))); return true; } void MltDeviceCapture::slotCheckDroppedFrames() { if (m_mltProducer) { int dropped = m_mltProducer->get_int("dropped"); if (dropped > m_droppedFrames) { m_droppedFrames = dropped; emit droppedFrames(m_droppedFrames); } } } void MltDeviceCapture::saveFrame(Mlt::Frame &frame) { mlt_image_format format = mlt_image_rgb24; int width = 0; int height = 0; const uchar *image = frame.get_image(format, width, height); QImage qimage(width, height, QImage::Format_RGB888); memcpy(qimage.bits(), image, static_cast(width * height * 3)); // Re-enable overlay Mlt::Service service(m_mltProducer->parent().get_service()); Mlt::Tractor tractor(service); Mlt::Producer trackProducer(tractor.track(0)); trackProducer.set("hide", 0); qimage.save(m_capturePath); emit frameSaved(m_capturePath); m_capturePath.clear(); } void MltDeviceCapture::captureFrame(const QString &path) { if (m_mltProducer == nullptr || !m_mltProducer->is_valid()) { return; } // Hide overlay track before doing the capture Mlt::Service service(m_mltProducer->parent().get_service()); Mlt::Tractor tractor(service); Mlt::Producer trackProducer(tractor.track(0)); mlt_service_lock(service.get_service()); trackProducer.set("hide", 1); m_mltConsumer->purge(); mlt_service_unlock(service.get_service()); m_capturePath = path; // Wait for 5 frames before capture to make sure overlay is gone doCapture = 5; } bool MltDeviceCapture::slotStartCapture(const QString ¶ms, const QString &path, const QString &playlist, bool livePreview, bool xmlPlaylist) { stop(); m_livePreview = livePreview; m_frameCount = 0; m_droppedFrames = 0; delete m_mltProfile; char *tmp = qstrdup(m_activeProfile.toUtf8().constData()); m_mltProfile = new Mlt::Profile(tmp); delete[] tmp; m_mltConsumer = new Mlt::Consumer(*m_mltProfile, "multi"); if (m_mltConsumer == nullptr || !m_mltConsumer->is_valid()) { delete m_mltConsumer; m_mltConsumer = nullptr; return false; } // Create multi consumer setup auto *renderProps = new Mlt::Properties; renderProps->set("mlt_service", "avformat"); renderProps->set("target", path.toUtf8().constData()); renderProps->set("real_time", -KdenliveSettings::mltthreads()); renderProps->set("terminate_on_pause", 0); // was commented out. restoring it fixes mantis#3415 - FFmpeg recording freezes // without this line a call to mlt_properties_get_int(terminate on pause) for in mlt/src/modules/core/consumer_multi.c is returning 1 // and going into and endless loop. renderProps->set("mlt_profile", m_activeProfile.toUtf8().constData()); const QStringList paramList = params.split(' ', QString::SkipEmptyParts); for (int i = 0; i < paramList.count(); ++i) { tmp = qstrdup(paramList.at(i).section(QLatin1Char('='), 0, 0).toUtf8().constData()); QString value = paramList.at(i).section(QLatin1Char('='), 1, 1); if (value == QLatin1String("%threads")) { value = QString::number(QThread::idealThreadCount()); } char *tmp2 = qstrdup(value.toUtf8().constData()); renderProps->set(tmp, tmp2); delete[] tmp; delete[] tmp2; } mlt_properties consumerProperties = m_mltConsumer->get_properties(); mlt_properties_set_data(consumerProperties, "0", renderProps->get_properties(), 0, (mlt_destructor)mlt_properties_close, nullptr); if (m_livePreview) { // user wants live preview auto *previewProps = new Mlt::Properties; QString videoDriver = KdenliveSettings::videodrivername(); if (!videoDriver.isEmpty()) { if (videoDriver == QLatin1String("x11_noaccel")) { qputenv("SDL_VIDEO_YUV_HWACCEL", "0"); videoDriver = QStringLiteral("x11"); } else { qunsetenv("SDL_VIDEO_YUV_HWACCEL"); } } qputenv("SDL_VIDEO_ALLOW_SCREENSAVER", "1"); // OpenGL monitor previewProps->set("mlt_service", KdenliveSettings::audiobackend().toUtf8().constData()); previewProps->set("preview_off", 1); previewProps->set("preview_format", mlt_image_rgb24); previewProps->set("terminate_on_pause", 0); m_showFrameEvent = m_mltConsumer->listen("consumer-frame-show", this, (mlt_listener)consumer_gl_frame_show); // m_mltConsumer->set("resize", 1); previewProps->set("window_background", KdenliveSettings::window_background().name().toUtf8().constData()); QString audioDevice = KdenliveSettings::audiodevicename(); if (!audioDevice.isEmpty()) { previewProps->set("audio_device", audioDevice.toUtf8().constData()); } if (!videoDriver.isEmpty()) { previewProps->set("video_driver", videoDriver.toUtf8().constData()); } QString audioDriver = KdenliveSettings::audiodrivername(); if (!audioDriver.isEmpty()) { previewProps->set("audio_driver", audioDriver.toUtf8().constData()); } previewProps->set("real_time", "0"); previewProps->set("mlt_profile", m_activeProfile.toUtf8().constData()); mlt_properties_set_data(consumerProperties, "1", previewProps->get_properties(), 0, (mlt_destructor)mlt_properties_close, nullptr); // m_showFrameEvent = m_mltConsumer->listen("consumer-frame-render", this, (mlt_listener) rec_consumer_frame_show); } else { } if (xmlPlaylist) { // create an xml producer m_mltProducer = new Mlt::Producer(*m_mltProfile, "xml-string", playlist.toUtf8().constData()); } else { // create a producer based on mltproducer parameter m_mltProducer = new Mlt::Producer(*m_mltProfile, playlist.toUtf8().constData()); } if (m_mltProducer == nullptr || !m_mltProducer->is_valid()) { // qCDebug(KDENLIVE_LOG)<<"//// ERROR CREATRING PROD"; delete m_mltConsumer; m_mltConsumer = nullptr; delete m_mltProducer; m_mltProducer = nullptr; return false; } m_mltConsumer->connect(*m_mltProducer); if (m_mltConsumer->start() == -1) { delete m_showFrameEvent; m_showFrameEvent = nullptr; delete m_mltConsumer; m_mltConsumer = nullptr; return 0; } m_droppedFramesTimer.start(); return 1; } void MltDeviceCapture::setOverlay(const QString &path) { if (m_mltProducer == nullptr || !m_mltProducer->is_valid()) { return; } Mlt::Producer parentProd(m_mltProducer->parent()); if (parentProd.get_producer() == nullptr) { // qCDebug(KDENLIVE_LOG) << "PLAYLIST BROKEN, CANNOT INSERT CLIP //////"; return; } Mlt::Service service(parentProd.get_service()); if (service.type() != tractor_type) { qCWarning(KDENLIVE_LOG) << "// TRACTOR PROBLEM"; return; } Mlt::Tractor tractor(service); if (tractor.count() < 2) { qCWarning(KDENLIVE_LOG) << "// TRACTOR PROBLEM"; return; } mlt_service_lock(service.get_service()); Mlt::Producer trackProducer(tractor.track(0)); Mlt::Playlist trackPlaylist((mlt_playlist)trackProducer.get_service()); trackPlaylist.remove(0); if (path.isEmpty()) { mlt_service_unlock(service.get_service()); return; } // Add overlay clip char *tmp = qstrdup(path.toUtf8().constData()); auto *clip = new Mlt::Producer(*m_mltProfile, "loader", tmp); delete[] tmp; clip->set_in_and_out(0, 99999); trackPlaylist.insert_at(0, clip, 1); // Add transition mlt_service serv = m_mltProducer->parent().get_service(); mlt_service nextservice = mlt_service_get_producer(serv); mlt_properties properties = MLT_SERVICE_PROPERTIES(nextservice); QString mlt_type = mlt_properties_get(properties, "mlt_type"); if (mlt_type != QLatin1String("transition")) { // transition does not exist, add it Mlt::Field *field = tractor.field(); auto *transition = new Mlt::Transition(*m_mltProfile, "composite"); transition->set_in_and_out(0, 0); transition->set("geometry", "0/0:100%x100%:70"); transition->set("fill", 1); transition->set("operator", "and"); transition->set("a_track", 0); transition->set("b_track", 1); field->plant_transition(*transition, 0, 1); } mlt_service_unlock(service.get_service()); // delete clip; } void MltDeviceCapture::setOverlayEffect(const QString &tag, const QStringList ¶meters) { if (m_mltProducer == nullptr || !m_mltProducer->is_valid()) { return; } Mlt::Service service(m_mltProducer->parent().get_service()); Mlt::Tractor tractor(service); Mlt::Producer trackProducer(tractor.track(0)); Mlt::Playlist trackPlaylist((mlt_playlist)trackProducer.get_service()); Mlt::Service trackService(trackProducer.get_service()); mlt_service_lock(service.get_service()); // delete previous effects Mlt::Filter *filter; filter = trackService.filter(0); if ((filter != nullptr) && !tag.isEmpty()) { QString currentService = filter->get("mlt_service"); if (currentService == tag) { // Effect is already there mlt_service_unlock(service.get_service()); return; } } while (filter != nullptr) { trackService.detach(*filter); delete filter; filter = trackService.filter(0); } if (tag.isEmpty()) { mlt_service_unlock(service.get_service()); return; } char *tmp = qstrdup(tag.toUtf8().constData()); filter = new Mlt::Filter(*m_mltProfile, tmp); delete[] tmp; if ((filter != nullptr) && filter->is_valid()) { for (int j = 0; j < parameters.count(); ++j) { filter->set(parameters.at(j).section(QLatin1Char('='), 0, 0).toUtf8().constData(), parameters.at(j).section(QLatin1Char('='), 1, 1).toUtf8().constData()); } trackService.attach(*filter); } mlt_service_unlock(service.get_service()); } void MltDeviceCapture::mirror(bool activate) { if (m_mltProducer == nullptr || !m_mltProducer->is_valid()) { return; } Mlt::Service service(m_mltProducer->parent().get_service()); Mlt::Tractor tractor(service); Mlt::Producer trackProducer(tractor.track(1)); Mlt::Playlist trackPlaylist((mlt_playlist)trackProducer.get_service()); Mlt::Service trackService(trackProducer.get_service()); mlt_service_lock(service.get_service()); // delete previous effects Mlt::Filter *filter; filter = trackService.filter(0); while (filter != nullptr) { trackService.detach(*filter); delete filter; filter = trackService.filter(0); } if (!activate) { mlt_service_unlock(service.get_service()); return; } filter = new Mlt::Filter(*m_mltProfile, "mirror"); if ((filter != nullptr) && filter->is_valid()) { filter->set("mirror", "flip"); trackService.attach(*filter); } mlt_service_unlock(service.get_service()); } void MltDeviceCapture::uyvy2rgb(unsigned char *yuv_buffer, int width, int height) { processingImage = true; QImage image(width, height, QImage::Format_RGB888); unsigned char *rgb_buffer = image.bits(); int rgb_ptr = 0, y_ptr = 0; int len = width * height / 2; for (int t = 0; t < len; ++t) { int Y = yuv_buffer[y_ptr]; int U = yuv_buffer[y_ptr + 1]; int Y2 = yuv_buffer[y_ptr + 2]; int V = yuv_buffer[y_ptr + 3]; y_ptr += 4; int r = ((298 * (Y - 16) + 409 * (V - 128) + 128) >> 8); int g = ((298 * (Y - 16) - 100 * (U - 128) - 208 * (V - 128) + 128) >> 8); int b = ((298 * (Y - 16) + 516 * (U - 128) + 128) >> 8); if (r > 255) { r = 255; } if (g > 255) { g = 255; } if (b > 255) { b = 255; } if (r < 0) { r = 0; } if (g < 0) { g = 0; } if (b < 0) { b = 0; } rgb_buffer[rgb_ptr] = static_cast(r); rgb_buffer[rgb_ptr + 1] = static_cast(g); rgb_buffer[rgb_ptr + 2] = static_cast(b); rgb_ptr += 3; r = ((298 * (Y2 - 16) + 409 * (V - 128) + 128) >> 8); g = ((298 * (Y2 - 16) - 100 * (U - 128) - 208 * (V - 128) + 128) >> 8); b = ((298 * (Y2 - 16) + 516 * (U - 128) + 128) >> 8); if (r > 255) { r = 255; } if (g > 255) { g = 255; } if (b > 255) { b = 255; } if (r < 0) { r = 0; } if (g < 0) { g = 0; } if (b < 0) { b = 0; } rgb_buffer[rgb_ptr] = static_cast(r); rgb_buffer[rgb_ptr + 1] = static_cast(g); rgb_buffer[rgb_ptr + 2] = static_cast(b); rgb_ptr += 3; } // emit imageReady(image); // m_captureDisplayWidget->setImage(image); emit unblockPreview(); // processingImage = false; } void MltDeviceCapture::slotPreparePreview() { QTimer::singleShot(1000, this, &MltDeviceCapture::slotAllowPreview); } void MltDeviceCapture::slotAllowPreview() { processingImage = false; } diff --git a/src/capture/mltdevicecapture.h b/src/capture/mltdevicecapture.h index dfcd401a0..ed227d2fd 100644 --- a/src/capture/mltdevicecapture.h +++ b/src/capture/mltdevicecapture.h @@ -1,148 +1,148 @@ /*************************************************************************** mltdevicecapture.h - description ------------------- begin : Sun May 21 2011 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. * * * ***************************************************************************/ /*! * @class MltDeviceCapture * @brief Interface for MLT capture. * Capturing started by MltDeviceCapture::slotStartCapture () * * Capturing is stopped by RecMonitor::slotStopCapture() */ #ifndef MLTDEVICECAPTURE_H #define MLTDEVICECAPTURE_H #include "definitions.h" #include "gentime.h" #include "monitor/abstractmonitor.h" #include #include // include after QTimer to have C++ phtreads defined #include namespace Mlt { class Consumer; class Frame; class Event; class Producer; class Profile; } // namespace Mlt class MltDeviceCapture : public AbstractRender { Q_OBJECT public : enum FailStates { OK = 0, APP_NOEXIST }; /** @brief Build a MLT Renderer * @param winid The parent widget identifier (required for SDL display). Set to 0 for OpenGL rendering * @param profile The MLT profile used for the capture (default one will be used if empty). */ - explicit MltDeviceCapture(QString profile, /*VideoSurface *surface,*/ QWidget *parent = nullptr); + explicit MltDeviceCapture(const QString &profile, /*VideoSurface *surface,*/ QWidget *parent = nullptr); /** @brief Destroy the MLT Renderer. */ ~MltDeviceCapture(); int doCapture; /** @brief Someone needs us to send again a frame. */ void sendFrameUpdate() override {} void emitFrameUpdated(Mlt::Frame &); void emitFrameNumber(double position); void emitConsumerStopped(); void showFrame(Mlt::Frame &); void showAudio(Mlt::Frame &); void saveFrame(Mlt::Frame &frame); /** @brief Starts the MLT Video4Linux process. * @param surface The widget onto which the frame should be painted * Called by RecMonitor::slotRecord () */ bool slotStartCapture(const QString ¶ms, const QString &path, const QString &playlist, bool livePreview, bool xmlPlaylist = true); bool slotStartPreview(const QString &producer, bool xmlFormat = false); /** @brief Save current frame to file. */ void captureFrame(const QString &path); /** @brief This will add the video clip from path and add it in the overlay track. */ void setOverlay(const QString &path); /** @brief This will add an MLT video effect to the overlay track. */ void setOverlayEffect(const QString &tag, const QStringList ¶meters); /** @brief This will add a horizontal flip effect, easier to work when filming yourself. */ void mirror(bool activate); /** @brief True if we are processing an image (yuv > rgb) when recording. */ bool processingImage; void pause(); private: Mlt::Consumer *m_mltConsumer; Mlt::Producer *m_mltProducer; Mlt::Profile *m_mltProfile; Mlt::Event *m_showFrameEvent; QString m_activeProfile; int m_droppedFrames; /** @brief When true, images will be displayed on monitor while capturing. */ bool m_livePreview; /** @brief Count captured frames, used to display only one in ten images while capturing. */ int m_frameCount; void uyvy2rgb(unsigned char *yuv_buffer, int width, int height); QString m_capturePath; QTimer m_droppedFramesTimer; QMutex m_mutex; /** @brief Build the MLT Consumer object with initial settings. * @param profileName The MLT profile to use for the consumer * @returns true if consumer is valid */ bool buildConsumer(const QString &profileName = QString()); private slots: void slotPreparePreview(); void slotAllowPreview(); /** @brief When capturing, check every second for dropped frames. */ void slotCheckDroppedFrames(); signals: /** @brief A frame's image has to be shown. * * Used in Mac OS X. */ void showImageSignal(const QImage &); void frameSaved(const QString &); void droppedFrames(int); void unblockPreview(); void imageReady(const QImage &); public slots: /** @brief Stops the consumer. */ void stop(); }; #endif diff --git a/src/dialogs/clipcreationdialog.cpp b/src/dialogs/clipcreationdialog.cpp index d3ed08cfe..c4f64098e 100644 --- a/src/dialogs/clipcreationdialog.cpp +++ b/src/dialogs/clipcreationdialog.cpp @@ -1,439 +1,440 @@ /* Copyright (C) 2015 Jean-Baptiste Mardelle This file is part of Kdenlive. See www.kdenlive.org. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "clipcreationdialog.h" #include "bin/bin.h" #include "bin/bincommands.h" #include "bin/clipcreator.hpp" #include "bin/projectclip.h" #include "bin/projectitemmodel.h" #include "core.h" #include "definitions.h" #include "doc/docundostack.hpp" #include "doc/kdenlivedoc.h" #include "kdenlive_debug.h" #include "kdenlivesettings.h" #include "project/dialogs/slideshowclip.h" #include "timecodedisplay.h" #include "titler/titlewidget.h" #include "titletemplatedialog.h" #include "ui_colorclip_ui.h" #include "ui_qtextclip_ui.h" #include "utils/devices.hpp" #include "xml/xml.hpp" #include "klocalizedstring.h" #include #include #include #include #include "kdenlive_debug.h" #include #include #include #include #include #include #include #include #include - +#include // static QStringList ClipCreationDialog::getExtensions() { // Build list of MIME types QStringList mimeTypes = QStringList() << QStringLiteral("") << QStringLiteral("application/x-kdenlivetitle") << QStringLiteral("video/mlt-playlist") << QStringLiteral("text/plain"); // Video MIMEs mimeTypes << QStringLiteral("video/x-flv") << QStringLiteral("application/vnd.rn-realmedia") << QStringLiteral("video/x-dv") << QStringLiteral("video/dv") << QStringLiteral("video/x-msvideo") << QStringLiteral("video/x-matroska") << QStringLiteral("video/mpeg") << QStringLiteral("video/ogg") << QStringLiteral("video/x-ms-wmv") << QStringLiteral("video/mp4") << QStringLiteral("video/quicktime") << QStringLiteral("video/webm") << QStringLiteral("video/3gpp") << QStringLiteral("video/mp2t"); // Audio MIMEs mimeTypes << QStringLiteral("audio/AMR") << QStringLiteral("audio/x-flac") << QStringLiteral("audio/x-matroska") << QStringLiteral("audio/mp4") << QStringLiteral("audio/mpeg") << QStringLiteral("audio/x-mp3") << QStringLiteral("audio/ogg") << QStringLiteral("audio/x-wav") << QStringLiteral("audio/x-aiff") << QStringLiteral("audio/aiff") << QStringLiteral("application/ogg") << QStringLiteral("application/mxf") << QStringLiteral("application/x-shockwave-flash") << QStringLiteral("audio/ac3"); // Image MIMEs mimeTypes << QStringLiteral("image/gif") << QStringLiteral("image/jpeg") << QStringLiteral("image/png") << QStringLiteral("image/x-tga") << QStringLiteral("image/x-bmp") << QStringLiteral("image/svg+xml") << QStringLiteral("image/tiff") << QStringLiteral("image/x-xcf") << QStringLiteral("image/x-xcf-gimp") << QStringLiteral("image/x-vnd.adobe.photoshop") << QStringLiteral("image/x-pcx") << QStringLiteral("image/x-exr") << QStringLiteral("image/x-portable-pixmap") << QStringLiteral("application/x-krita"); QMimeDatabase db; QStringList allExtensions; for (const QString &mimeType : mimeTypes) { QMimeType mime = db.mimeTypeForName(mimeType); if (mime.isValid()) { allExtensions.append(mime.globPatterns()); } } // process custom user extensions const QStringList customs = KdenliveSettings::addedExtensions().split(' ', QString::SkipEmptyParts); if (!customs.isEmpty()) { for (const QString &ext : customs) { if (ext.startsWith(QLatin1String("*."))) { allExtensions << ext; } else if (ext.startsWith(QLatin1String("."))) { allExtensions << QStringLiteral("*") + ext; } else if (!ext.contains(QLatin1Char('.'))) { allExtensions << QStringLiteral("*.") + ext; } else { // Unrecognized format qCDebug(KDENLIVE_LOG) << "Unrecognized custom format: " << ext; } } } allExtensions.removeDuplicates(); return allExtensions; } // static void ClipCreationDialog::createColorClip(KdenliveDoc *doc, const QString &parentFolder, std::shared_ptr model) { QScopedPointer dia(new QDialog(qApp->activeWindow())); Ui::ColorClip_UI dia_ui; dia_ui.setupUi(dia.data()); dia->setWindowTitle(i18n("Color Clip")); dia_ui.clip_name->setText(i18n("Color Clip")); QScopedPointer t(new TimecodeDisplay(doc->timecode())); t->setValue(KdenliveSettings::color_duration()); dia_ui.clip_durationBox->addWidget(t.data()); dia_ui.clip_color->setColor(KdenliveSettings::colorclipcolor()); if (dia->exec() == QDialog::Accepted) { QString color = dia_ui.clip_color->color().name(); KdenliveSettings::setColorclipcolor(color); color = color.replace(0, 1, QStringLiteral("0x")) + "ff"; int duration = doc->getFramePos(doc->timecode().getTimecode(t->gentime())); QString name = dia_ui.clip_name->text(); - ClipCreator::createColorClip(color, duration, name, parentFolder, model); + ClipCreator::createColorClip(color, duration, name, parentFolder, std::move(model)); } } void ClipCreationDialog::createQTextClip(KdenliveDoc *doc, const QString &parentId, Bin *bin, ProjectClip *clip) { KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup titleConfig(config, "TitleWidget"); QScopedPointer dia(new QDialog(bin)); Ui::QTextClip_UI dia_ui; dia_ui.setupUi(dia.data()); dia->setWindowTitle(i18n("Text Clip")); dia_ui.fgColor->setAlphaChannelEnabled(true); dia_ui.lineColor->setAlphaChannelEnabled(true); dia_ui.bgColor->setAlphaChannelEnabled(true); if (clip) { dia_ui.name->setText(clip->getProducerProperty(QStringLiteral("kdenlive:clipname"))); dia_ui.text->setPlainText(clip->getProducerProperty(QStringLiteral("text"))); dia_ui.fgColor->setColor(clip->getProducerProperty(QStringLiteral("fgcolour"))); dia_ui.bgColor->setColor(clip->getProducerProperty(QStringLiteral("bgcolour"))); dia_ui.pad->setValue(clip->getProducerProperty(QStringLiteral("pad")).toInt()); dia_ui.lineColor->setColor(clip->getProducerProperty(QStringLiteral("olcolour"))); dia_ui.lineWidth->setValue(clip->getProducerProperty(QStringLiteral("outline")).toInt()); dia_ui.font->setCurrentFont(QFont(clip->getProducerProperty(QStringLiteral("family")))); dia_ui.fontSize->setValue(clip->getProducerProperty(QStringLiteral("size")).toInt()); dia_ui.weight->setValue(clip->getProducerProperty(QStringLiteral("weight")).toInt()); dia_ui.italic->setChecked(clip->getProducerProperty(QStringLiteral("style")) == QStringLiteral("italic")); dia_ui.duration->setText(doc->timecode().getTimecodeFromFrames(clip->getProducerProperty(QStringLiteral("out")).toInt())); } else { dia_ui.name->setText(i18n("Text Clip")); dia_ui.fgColor->setColor(titleConfig.readEntry(QStringLiteral("font_color"))); dia_ui.bgColor->setColor(titleConfig.readEntry(QStringLiteral("background_color"))); dia_ui.lineColor->setColor(titleConfig.readEntry(QStringLiteral("font_outline_color"))); dia_ui.lineWidth->setValue(titleConfig.readEntry(QStringLiteral("font_outline")).toInt()); dia_ui.font->setCurrentFont(QFont(titleConfig.readEntry(QStringLiteral("font_family")))); dia_ui.fontSize->setValue(titleConfig.readEntry(QStringLiteral("font_pixel_size")).toInt()); dia_ui.weight->setValue(titleConfig.readEntry(QStringLiteral("font_weight")).toInt()); dia_ui.italic->setChecked(titleConfig.readEntry(QStringLiteral("font_italic")).toInt() != 0); dia_ui.duration->setText(titleConfig.readEntry(QStringLiteral("title_duration"))); } if (dia->exec() == QDialog::Accepted) { // KdenliveSettings::setColorclipcolor(color); QDomDocument xml; QDomElement prod = xml.createElement(QStringLiteral("producer")); xml.appendChild(prod); prod.setAttribute(QStringLiteral("type"), (int)ClipType::QText); int id = pCore->projectItemModel()->getFreeClipId(); prod.setAttribute(QStringLiteral("id"), QString::number(id)); prod.setAttribute(QStringLiteral("in"), QStringLiteral("0")); prod.setAttribute(QStringLiteral("out"), doc->timecode().getFrameCount(dia_ui.duration->text())); QMap properties; properties.insert(QStringLiteral("kdenlive:clipname"), dia_ui.name->text()); if (!parentId.isEmpty()) { properties.insert(QStringLiteral("kdenlive:folderid"), parentId); } properties.insert(QStringLiteral("mlt_service"), QStringLiteral("qtext")); properties.insert(QStringLiteral("out"), QString::number(doc->timecode().getFrameCount(dia_ui.duration->text()))); properties.insert(QStringLiteral("length"), dia_ui.duration->text()); // properties.insert(QStringLiteral("scale"), QStringLiteral("off")); // properties.insert(QStringLiteral("fill"), QStringLiteral("0")); properties.insert(QStringLiteral("text"), dia_ui.text->document()->toPlainText()); properties.insert(QStringLiteral("fgcolour"), dia_ui.fgColor->color().name(QColor::HexArgb)); properties.insert(QStringLiteral("bgcolour"), dia_ui.bgColor->color().name(QColor::HexArgb)); properties.insert(QStringLiteral("olcolour"), dia_ui.lineColor->color().name(QColor::HexArgb)); properties.insert(QStringLiteral("outline"), QString::number(dia_ui.lineWidth->value())); properties.insert(QStringLiteral("pad"), QString::number(dia_ui.pad->value())); properties.insert(QStringLiteral("family"), dia_ui.font->currentFont().family()); properties.insert(QStringLiteral("size"), QString::number(dia_ui.fontSize->value())); properties.insert(QStringLiteral("style"), dia_ui.italic->isChecked() ? QStringLiteral("italic") : QStringLiteral("normal")); properties.insert(QStringLiteral("weight"), QString::number(dia_ui.weight->value())); if (clip) { QMap oldProperties; oldProperties.insert(QStringLiteral("out"), clip->getProducerProperty(QStringLiteral("out"))); oldProperties.insert(QStringLiteral("length"), clip->getProducerProperty(QStringLiteral("length"))); oldProperties.insert(QStringLiteral("kdenlive:clipname"), clip->name()); oldProperties.insert(QStringLiteral("ttl"), clip->getProducerProperty(QStringLiteral("ttl"))); oldProperties.insert(QStringLiteral("loop"), clip->getProducerProperty(QStringLiteral("loop"))); oldProperties.insert(QStringLiteral("crop"), clip->getProducerProperty(QStringLiteral("crop"))); oldProperties.insert(QStringLiteral("fade"), clip->getProducerProperty(QStringLiteral("fade"))); oldProperties.insert(QStringLiteral("luma_duration"), clip->getProducerProperty(QStringLiteral("luma_duration"))); oldProperties.insert(QStringLiteral("luma_file"), clip->getProducerProperty(QStringLiteral("luma_file"))); oldProperties.insert(QStringLiteral("softness"), clip->getProducerProperty(QStringLiteral("softness"))); oldProperties.insert(QStringLiteral("animation"), clip->getProducerProperty(QStringLiteral("animation"))); bin->slotEditClipCommand(clip->AbstractProjectItem::clipId(), oldProperties, properties); } else { Xml::addXmlProperties(prod, properties); QString clipId = QString::number(id); pCore->projectItemModel()->requestAddBinClip(clipId, xml.documentElement(), parentId, i18n("Create Title clip")); } } } // static void ClipCreationDialog::createSlideshowClip(KdenliveDoc *doc, const QString &parentId, std::shared_ptr model) { QScopedPointer dia( new SlideshowClip(doc->timecode(), KRecentDirs::dir(QStringLiteral(":KdenliveSlideShowFolder")), nullptr, QApplication::activeWindow())); if (dia->exec() == QDialog::Accepted) { // Ready, create xml KRecentDirs::add(QStringLiteral(":KdenliveSlideShowFolder"), QUrl::fromLocalFile(dia->selectedPath()).adjusted(QUrl::RemoveFilename).toLocalFile()); std::unordered_map properties; properties[QStringLiteral("ttl")] = QString::number(doc->getFramePos(dia->clipDuration())); properties[QStringLiteral("loop")] = QString::number(static_cast(dia->loop())); properties[QStringLiteral("crop")] = QString::number(static_cast(dia->crop())); properties[QStringLiteral("fade")] = QString::number(static_cast(dia->fade())); properties[QStringLiteral("luma_duration")] = QString::number(doc->getFramePos(dia->lumaDuration())); properties[QStringLiteral("luma_file")] = dia->lumaFile(); properties[QStringLiteral("softness")] = QString::number(dia->softness()); properties[QStringLiteral("animation")] = dia->animation(); int duration = doc->getFramePos(dia->clipDuration()) * dia->imageCount(); - ClipCreator::createSlideshowClip(dia->selectedPath(), duration, dia->clipName(), parentId, properties, model); + ClipCreator::createSlideshowClip(dia->selectedPath(), duration, dia->clipName(), parentId, properties, std::move(model)); } } void ClipCreationDialog::createTitleClip(KdenliveDoc *doc, const QString &parentFolder, const QString &templatePath, std::shared_ptr model) { // Make sure the titles folder exists QDir dir(doc->projectDataFolder() + QStringLiteral("/titles")); dir.mkpath(QStringLiteral(".")); QPointer dia_ui = new TitleWidget(QUrl::fromLocalFile(templatePath), doc->timecode(), dir.absolutePath(), pCore->getMonitor(Kdenlive::ProjectMonitor), pCore->bin()); QObject::connect(dia_ui.data(), &TitleWidget::requestBackgroundFrame, pCore->bin(), &Bin::slotGetCurrentProjectImage); if (dia_ui->exec() == QDialog::Accepted) { // Ready, create clip xml std::unordered_map properties; properties[QStringLiteral("xmldata")] = dia_ui->xml().toString(); QString titleSuggestion = dia_ui->titleSuggest(); - ClipCreator::createTitleClip(properties, dia_ui->duration() - 1, titleSuggestion.isEmpty() ? i18n("Title clip") : titleSuggestion, parentFolder, model); + ClipCreator::createTitleClip(properties, dia_ui->duration() - 1, titleSuggestion.isEmpty() ? i18n("Title clip") : titleSuggestion, parentFolder, + std::move(model)); } delete dia_ui; } void ClipCreationDialog::createTitleTemplateClip(KdenliveDoc *doc, const QString &parentFolder, std::shared_ptr model) { QScopedPointer dia(new TitleTemplateDialog(doc->projectDataFolder(), QApplication::activeWindow())); if (dia->exec() == QDialog::Accepted) { - ClipCreator::createTitleTemplate(dia->selectedTemplate(), dia->selectedText(), i18n("Template title clip"), parentFolder, model); + ClipCreator::createTitleTemplate(dia->selectedTemplate(), dia->selectedText(), i18n("Template title clip"), parentFolder, std::move(model)); } } // void ClipCreationDialog::createClipsCommand(KdenliveDoc *doc, const QList &urls, const QStringList &groupInfo, Bin *bin, // const QMap &data) // { // auto *addClips = new QUndoCommand(); // TODO: check files on removable volume /*listRemovableVolumes(); for (const QUrl &file : urls) { if (QFile::exists(file.path())) { //TODO check for duplicates if (!data.contains("bypassDuplicate") && !getClipByResource(file.path()).empty()) { if (KMessageBox::warningContinueCancel(QApplication::activeWindow(), i18n("Clip %1
already exists in project, what do you want to do?", file.path()), i18n("Clip already exists")) == KMessageBox::Cancel) continue; } if (isOnRemovableDevice(file) && !isOnRemovableDevice(m_doc->projectFolder())) { int answer = KMessageBox::warningYesNoCancel(QApplication::activeWindow(), i18n("Clip %1
is on a removable device, will not be available when device is unplugged", file.path()), i18n("File on a Removable Device"), KGuiItem(i18n("Copy file to project folder")), KGuiItem(i18n("Continue")), KStandardGuiItem::cancel(), QString("copyFilesToProjectFolder")); if (answer == KMessageBox::Cancel) continue; else if (answer == KMessageBox::Yes) { // Copy files to project folder QDir sourcesFolder(m_doc->projectFolder().toLocalFile()); sourcesFolder.cd("clips"); KIO::MkdirJob *mkdirJob = KIO::mkdir(QUrl::fromLocalFile(sourcesFolder.absolutePath())); KJobWidgets::setWindow(mkdirJob, QApplication::activeWindow()); if (!mkdirJob->exec()) { KMessageBox::sorry(QApplication::activeWindow(), i18n("Cannot create directory %1", sourcesFolder.absolutePath())); continue; } //KIO::filesize_t m_requestedSize; KIO::CopyJob *copyjob = KIO::copy(file, QUrl::fromLocalFile(sourcesFolder.absolutePath())); //TODO: for some reason, passing metadata does not work... copyjob->addMetaData("group", data.value("group")); copyjob->addMetaData("groupId", data.value("groupId")); copyjob->addMetaData("comment", data.value("comment")); KJobWidgets::setWindow(copyjob, QApplication::activeWindow()); connect(copyjob, &KIO::CopyJob::copyingDone, this, &ClipManager::slotAddCopiedClip); continue; } }*/ // TODO check folders /*QList< QList > foldersList; QMimeDatabase db; for (const QUrl & file : list) { // Check there is no folder here QMimeType type = db.mimeTypeForUrl(file); if (type.inherits("inode/directory")) { // user dropped a folder, import its files list.removeAll(file); QDir dir(file.path()); QStringList result = dir.entryList(QDir::Files); QList folderFiles; folderFiles << file; for (const QString & path : result) { // TODO: create folder command folderFiles.append(QUrl::fromLocalFile(dir.absoluteFilePath(path))); } if (folderFiles.count() > 1) foldersList.append(folderFiles); } }*/ //} -void ClipCreationDialog::createClipsCommand(KdenliveDoc *doc, const QString &parentFolder, std::shared_ptr model) +void ClipCreationDialog::createClipsCommand(KdenliveDoc *doc, const QString &parentFolder, const std::shared_ptr &model) { qDebug() << "/////////// starting to add bin clips"; QList list; QString allExtensions = getExtensions().join(QLatin1Char(' ')); QString dialogFilter = allExtensions + QLatin1Char('|') + i18n("All Supported Files") + QStringLiteral("\n*|") + i18n("All Files"); QCheckBox *b = new QCheckBox(i18n("Import image sequence")); b->setChecked(KdenliveSettings::autoimagesequence()); QFrame *f = new QFrame(); f->setFrameShape(QFrame::NoFrame); auto *l = new QHBoxLayout; l->addWidget(b); l->addStretch(5); f->setLayout(l); QString clipFolder = KRecentDirs::dir(QStringLiteral(":KdenliveClipFolder")); if (clipFolder.isEmpty()) { clipFolder = QStandardPaths::writableLocation(QStandardPaths::MoviesLocation); } QScopedPointer dlg(new QDialog((QWidget *)doc->parent())); QScopedPointer fileWidget(new KFileWidget(QUrl::fromLocalFile(clipFolder), dlg.data())); auto *layout = new QVBoxLayout; layout->addWidget(fileWidget.data()); fileWidget->setCustomWidget(f); fileWidget->okButton()->show(); fileWidget->cancelButton()->show(); QObject::connect(fileWidget->okButton(), &QPushButton::clicked, fileWidget.data(), &KFileWidget::slotOk); QObject::connect(fileWidget.data(), &KFileWidget::accepted, fileWidget.data(), &KFileWidget::accept); QObject::connect(fileWidget.data(), &KFileWidget::accepted, dlg.data(), &QDialog::accept); QObject::connect(fileWidget->cancelButton(), &QPushButton::clicked, dlg.data(), &QDialog::reject); dlg->setLayout(layout); fileWidget->setFilter(dialogFilter); fileWidget->setMode(KFile::Files | KFile::ExistingOnly | KFile::LocalOnly | KFile::Directory); KSharedConfig::Ptr conf = KSharedConfig::openConfig(); QWindow *handle = dlg->windowHandle(); if ((handle != nullptr) && conf->hasGroup("FileDialogSize")) { KWindowConfig::restoreWindowSize(handle, conf->group("FileDialogSize")); dlg->resize(handle->size()); } int result = dlg->exec(); if (result == QDialog::Accepted) { KdenliveSettings::setAutoimagesequence(b->isChecked()); list = fileWidget->selectedUrls(); if (!list.isEmpty()) { KRecentDirs::add(QStringLiteral(":KdenliveClipFolder"), list.constFirst().adjusted(QUrl::RemoveFilename).toLocalFile()); } if (KdenliveSettings::autoimagesequence() && list.count() >= 1) { // Check for image sequence const QUrl &url = list.at(0); QString fileName = url.fileName().section(QLatin1Char('.'), 0, -2); if (fileName.at(fileName.size() - 1).isDigit()) { KFileItem item(url); if (item.mimetype().startsWith(QLatin1String("image"))) { // import as sequence if we found more than one image in the sequence QStringList patternlist; QString pattern = SlideshowClip::selectedPath(url, false, QString(), &patternlist); qCDebug(KDENLIVE_LOG) << " / // IMPORT PATTERN: " << pattern << " COUNT: " << patternlist.count(); int count = patternlist.count(); if (count > 1) { // get image sequence base name while (fileName.size() > 0 && fileName.at(fileName.size() - 1).isDigit()) { fileName.chop(1); } QString duration = doc->timecode().reformatSeparators(KdenliveSettings::sequence_duration()); std::unordered_map properties; properties[QStringLiteral("ttl")] = QString::number(doc->getFramePos(duration)); properties[QStringLiteral("loop")] = QString::number(0); properties[QStringLiteral("crop")] = QString::number(0); properties[QStringLiteral("fade")] = QString::number(0); properties[QStringLiteral("luma_duration")] = QString::number(doc->getFramePos(doc->timecode().getTimecodeFromFrames(int(ceil(doc->timecode().fps()))))); int frame_duration = doc->getFramePos(duration) * count; ClipCreator::createSlideshowClip(pattern, frame_duration, fileName, parentFolder, properties, model); return; } } } } } qDebug() << "/////////// found list" << list; KConfigGroup group = conf->group("FileDialogSize"); if (handle) { KWindowConfig::saveWindowSize(handle, group); } Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool created = ClipCreator::createClipsFromList(list, true, parentFolder, model, undo, redo); // We reset the state of the "don't ask again" for the question about removable devices KMessageBox::enableMessage(QStringLiteral("removable")); if (created) { pCore->pushUndo(undo, redo, i18np("Add clip", "Add clips", list.size())); } } diff --git a/src/dialogs/clipcreationdialog.h b/src/dialogs/clipcreationdialog.h index 657b12155..64c3bcb5f 100644 --- a/src/dialogs/clipcreationdialog.h +++ b/src/dialogs/clipcreationdialog.h @@ -1,50 +1,50 @@ /* Copyright (C) 2015 Jean-Baptiste Mardelle This file is part of Kdenlive. See www.kdenlive.org. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef CLIPCREATIONDIALOG_H #define CLIPCREATIONDIALOG_H #include "definitions.h" class KdenliveDoc; class QUndoCommand; class Bin; class ProjectClip; class ProjectItemModel; /** * @namespace ClipCreationDialog * @brief This namespace contains a list of static methods displaying widgets * allowing creation of all clip types. */ namespace ClipCreationDialog { QStringList getExtensions(); void createColorClip(KdenliveDoc *doc, const QString &parentFolder, std::shared_ptr model); void createQTextClip(KdenliveDoc *doc, const QString &parentId, Bin *bin, ProjectClip *clip = nullptr); void createSlideshowClip(KdenliveDoc *doc, const QString &parentId, std::shared_ptr model); void createTitleClip(KdenliveDoc *doc, const QString &parentFolder, const QString &templatePath, std::shared_ptr model); void createTitleTemplateClip(KdenliveDoc *doc, const QString &parentFolder, std::shared_ptr model); -void createClipsCommand(KdenliveDoc *doc, const QString &parentFolder, std::shared_ptr model); +void createClipsCommand(KdenliveDoc *doc, const QString &parentFolder, const std::shared_ptr &model); } // namespace ClipCreationDialog #endif diff --git a/src/dialogs/renderwidget.cpp b/src/dialogs/renderwidget.cpp index 1f347e485..538feb6da 100644 --- a/src/dialogs/renderwidget.cpp +++ b/src/dialogs/renderwidget.cpp @@ -1,3405 +1,3405 @@ /*************************************************************************** * Copyright (C) 2008 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 "renderwidget.h" #include "bin/projectitemmodel.h" #include "core.h" #include "dialogs/profilesdialog.h" #include "doc/kdenlivedoc.h" #include "kdenlivesettings.h" #include "monitor/monitor.h" #include "profiles/profilemodel.hpp" #include "profiles/profilerepository.hpp" #include "project/projectmanager.h" #include "timecode.h" #include "ui_saveprofile_ui.h" #include "xml/xml.hpp" #include "klocalizedstring.h" #include #include #include #include #include #include #include #include #include #include "kdenlive_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef KF5_USE_PURPOSE #include #include #endif #include #ifdef Q_OS_MAC #include #endif // Render profiles roles enum { GroupRole = Qt::UserRole, ExtensionRole, StandardRole, RenderRole, ParamsRole, EditableRole, ExtraRole, BitratesRole, DefaultBitrateRole, AudioBitratesRole, DefaultAudioBitrateRole, SpeedsRole, FieldRole, ErrorRole }; // Render job roles const int ParametersRole = Qt::UserRole + 1; const int TimeRole = Qt::UserRole + 2; const int ProgressRole = Qt::UserRole + 3; const int ExtraInfoRole = Qt::UserRole + 5; // Running job status enum JOBSTATUS { WAITINGJOB = 0, STARTINGJOB, RUNNINGJOB, FINISHEDJOB, FAILEDJOB, ABORTEDJOB }; static QStringList acodecsList; static QStringList vcodecsList; static QStringList supportedFormats; RenderJobItem::RenderJobItem(QTreeWidget *parent, const QStringList &strings, int type) : QTreeWidgetItem(parent, strings, type) , m_status(-1) { setSizeHint(1, QSize(parent->columnWidth(1), parent->fontMetrics().height() * 3)); setStatus(WAITINGJOB); } void RenderJobItem::setStatus(int status) { if (m_status == status) { return; } m_status = status; switch (status) { case WAITINGJOB: setIcon(0, QIcon::fromTheme(QStringLiteral("media-playback-pause"))); setData(1, Qt::UserRole, i18n("Waiting...")); break; case FINISHEDJOB: setData(1, Qt::UserRole, i18n("Rendering finished")); setIcon(0, QIcon::fromTheme(QStringLiteral("dialog-ok"))); setData(1, ProgressRole, 100); break; case FAILEDJOB: setData(1, Qt::UserRole, i18n("Rendering crashed")); setIcon(0, QIcon::fromTheme(QStringLiteral("dialog-close"))); setData(1, ProgressRole, 100); break; case ABORTEDJOB: setData(1, Qt::UserRole, i18n("Rendering aborted")); setIcon(0, QIcon::fromTheme(QStringLiteral("dialog-cancel"))); setData(1, ProgressRole, 100); default: break; } } int RenderJobItem::status() const { return m_status; } void RenderJobItem::setMetadata(const QString &data) { m_data = data; } const QString RenderJobItem::metadata() const { return m_data; } RenderWidget::RenderWidget(bool enableProxy, QWidget *parent) : QDialog(parent) , m_blockProcessing(false) { m_view.setupUi(this); int size = style()->pixelMetric(QStyle::PM_SmallIconSize); QSize iconSize(size, size); setWindowTitle(i18n("Rendering")); m_view.buttonDelete->setIconSize(iconSize); m_view.buttonEdit->setIconSize(iconSize); m_view.buttonSave->setIconSize(iconSize); m_view.buttonFavorite->setIconSize(iconSize); m_view.buttonDownload->setIconSize(iconSize); m_view.buttonDelete->setIcon(QIcon::fromTheme(QStringLiteral("trash-empty"))); m_view.buttonDelete->setToolTip(i18n("Delete profile")); m_view.buttonDelete->setEnabled(false); m_view.buttonEdit->setIcon(QIcon::fromTheme(QStringLiteral("document-edit"))); m_view.buttonEdit->setToolTip(i18n("Edit profile")); m_view.buttonEdit->setEnabled(false); m_view.buttonSave->setIcon(QIcon::fromTheme(QStringLiteral("document-new"))); m_view.buttonSave->setToolTip(i18n("Create new profile")); m_view.hide_log->setIcon(QIcon::fromTheme(QStringLiteral("go-down"))); m_view.buttonFavorite->setIcon(QIcon::fromTheme(QStringLiteral("favorite"))); m_view.buttonFavorite->setToolTip(i18n("Copy profile to favorites")); m_view.buttonDownload->setIcon(QIcon::fromTheme(QStringLiteral("edit-download"))); m_view.buttonDownload->setToolTip(i18n("Download New Render Profiles...")); m_view.out_file->button()->setToolTip(i18n("Select output destination")); m_view.advanced_params->setMaximumHeight(QFontMetrics(font()).lineSpacing() * 5); m_view.optionsGroup->setVisible(m_view.options->isChecked()); connect(m_view.options, &QAbstractButton::toggled, m_view.optionsGroup, &QWidget::setVisible); m_view.videoLabel->setVisible(m_view.options->isChecked()); connect(m_view.options, &QAbstractButton::toggled, m_view.videoLabel, &QWidget::setVisible); m_view.video->setVisible(m_view.options->isChecked()); connect(m_view.options, &QAbstractButton::toggled, m_view.video, &QWidget::setVisible); m_view.audioLabel->setVisible(m_view.options->isChecked()); connect(m_view.options, &QAbstractButton::toggled, m_view.audioLabel, &QWidget::setVisible); m_view.audio->setVisible(m_view.options->isChecked()); connect(m_view.options, &QAbstractButton::toggled, m_view.audio, &QWidget::setVisible); connect(m_view.quality, &QAbstractSlider::valueChanged, this, &RenderWidget::adjustAVQualities); connect(m_view.video, static_cast(&QSpinBox::valueChanged), this, &RenderWidget::adjustQuality); connect(m_view.speed, &QAbstractSlider::valueChanged, this, &RenderWidget::adjustSpeed); m_view.buttonRender->setEnabled(false); m_view.buttonGenerateScript->setEnabled(false); setRescaleEnabled(false); m_view.guides_box->setVisible(false); m_view.open_dvd->setVisible(false); m_view.create_chapter->setVisible(false); m_view.open_browser->setVisible(false); m_view.error_box->setVisible(false); m_view.tc_type->setEnabled(false); m_view.checkTwoPass->setEnabled(false); m_view.proxy_render->setHidden(!enableProxy); connect(m_view.proxy_render, &QCheckBox::toggled, this, &RenderWidget::slotProxyWarn); KColorScheme scheme(palette().currentColorGroup(), KColorScheme::Window); QColor bg = scheme.background(KColorScheme::NegativeBackground).color(); m_view.errorBox->setStyleSheet( QStringLiteral("QGroupBox { background-color: rgb(%1, %2, %3); border-radius: 5px;}; ").arg(bg.red()).arg(bg.green()).arg(bg.blue())); int height = QFontInfo(font()).pixelSize(); m_view.errorIcon->setPixmap(QIcon::fromTheme(QStringLiteral("dialog-warning")).pixmap(height, height)); m_view.errorBox->setHidden(true); m_infoMessage = new KMessageWidget; m_view.info->addWidget(m_infoMessage); m_infoMessage->setCloseButtonVisible(false); m_infoMessage->hide(); m_jobInfoMessage = new KMessageWidget; m_view.jobInfo->addWidget(m_jobInfoMessage); m_jobInfoMessage->setCloseButtonVisible(false); m_jobInfoMessage->hide(); m_view.encoder_threads->setMinimum(0); m_view.encoder_threads->setMaximum(QThread::idealThreadCount()); m_view.encoder_threads->setToolTip(i18n("Encoding threads (0 is automatic)")); m_view.encoder_threads->setValue(KdenliveSettings::encodethreads()); connect(m_view.encoder_threads, static_cast(&QSpinBox::valueChanged), this, &RenderWidget::slotUpdateEncodeThreads); m_view.rescale_keep->setChecked(KdenliveSettings::rescalekeepratio()); connect(m_view.rescale_width, static_cast(&QSpinBox::valueChanged), this, &RenderWidget::slotUpdateRescaleWidth); connect(m_view.rescale_height, static_cast(&QSpinBox::valueChanged), this, &RenderWidget::slotUpdateRescaleHeight); m_view.rescale_keep->setIcon(QIcon::fromTheme(QStringLiteral("edit-link"))); m_view.rescale_keep->setToolTip(i18n("Preserve aspect ratio")); connect(m_view.rescale_keep, &QAbstractButton::clicked, this, &RenderWidget::slotSwitchAspectRatio); connect(m_view.buttonRender, SIGNAL(clicked()), this, SLOT(slotPrepareExport())); connect(m_view.buttonGenerateScript, &QAbstractButton::clicked, this, &RenderWidget::slotGenerateScript); m_view.abort_job->setEnabled(false); m_view.start_script->setEnabled(false); m_view.delete_script->setEnabled(false); connect(m_view.export_audio, &QCheckBox::stateChanged, this, &RenderWidget::slotUpdateAudioLabel); m_view.export_audio->setCheckState(Qt::PartiallyChecked); checkCodecs(); parseProfiles(); parseScriptFiles(); m_view.running_jobs->setUniformRowHeights(false); m_view.scripts_list->setUniformRowHeights(false); connect(m_view.start_script, &QAbstractButton::clicked, this, &RenderWidget::slotStartScript); connect(m_view.delete_script, &QAbstractButton::clicked, this, &RenderWidget::slotDeleteScript); connect(m_view.scripts_list, &QTreeWidget::itemSelectionChanged, this, &RenderWidget::slotCheckScript); connect(m_view.running_jobs, &QTreeWidget::itemSelectionChanged, this, &RenderWidget::slotCheckJob); connect(m_view.running_jobs, &QTreeWidget::itemDoubleClicked, this, &RenderWidget::slotPlayRendering); connect(m_view.buttonSave, &QAbstractButton::clicked, this, &RenderWidget::slotSaveProfile); connect(m_view.buttonEdit, &QAbstractButton::clicked, this, &RenderWidget::slotEditProfile); connect(m_view.buttonDelete, &QAbstractButton::clicked, this, &RenderWidget::slotDeleteProfile); connect(m_view.buttonFavorite, &QAbstractButton::clicked, this, &RenderWidget::slotCopyToFavorites); connect(m_view.buttonDownload, &QAbstractButton::clicked, this, &RenderWidget::slotDownloadNewRenderProfiles); connect(m_view.abort_job, &QAbstractButton::clicked, this, &RenderWidget::slotAbortCurrentJob); connect(m_view.start_job, &QAbstractButton::clicked, this, &RenderWidget::slotStartCurrentJob); connect(m_view.clean_up, &QAbstractButton::clicked, this, &RenderWidget::slotCLeanUpJobs); connect(m_view.hide_log, &QAbstractButton::clicked, this, &RenderWidget::slotHideLog); connect(m_view.buttonClose, &QAbstractButton::clicked, this, &QWidget::hide); connect(m_view.buttonClose2, &QAbstractButton::clicked, this, &QWidget::hide); connect(m_view.buttonClose3, &QAbstractButton::clicked, this, &QWidget::hide); connect(m_view.rescale, &QAbstractButton::toggled, this, &RenderWidget::setRescaleEnabled); connect(m_view.out_file, &KUrlRequester::textChanged, this, static_cast(&RenderWidget::slotUpdateButtons)); connect(m_view.out_file, &KUrlRequester::urlSelected, this, static_cast(&RenderWidget::slotUpdateButtons)); connect(m_view.formats, &QTreeWidget::currentItemChanged, this, &RenderWidget::refreshParams); connect(m_view.formats, &QTreeWidget::itemDoubleClicked, this, &RenderWidget::slotEditItem); connect(m_view.render_guide, &QAbstractButton::clicked, this, &RenderWidget::slotUpdateGuideBox); connect(m_view.render_zone, &QAbstractButton::clicked, this, &RenderWidget::slotUpdateGuideBox); connect(m_view.render_full, &QAbstractButton::clicked, this, &RenderWidget::slotUpdateGuideBox); connect(m_view.guide_end, static_cast(&KComboBox::activated), this, &RenderWidget::slotCheckStartGuidePosition); connect(m_view.guide_start, static_cast(&KComboBox::activated), this, &RenderWidget::slotCheckEndGuidePosition); connect(m_view.tc_overlay, &QAbstractButton::toggled, m_view.tc_type, &QWidget::setEnabled); // m_view.splitter->setStretchFactor(1, 5); // m_view.splitter->setStretchFactor(0, 2); m_view.out_file->setMode(KFile::File); #if KIO_VERSION >= QT_VERSION_CHECK(5, 33, 0) m_view.out_file->setAcceptMode(QFileDialog::AcceptSave); #elif !defined(KIOWIDGETS_DEPRECATED) m_view.out_file->fileDialog()->setAcceptMode(QFileDialog::AcceptSave); #endif m_view.out_file->setFocusPolicy(Qt::ClickFocus); m_jobsDelegate = new RenderViewDelegate(this); m_view.running_jobs->setHeaderLabels(QStringList() << QString() << i18n("File")); m_view.running_jobs->setItemDelegate(m_jobsDelegate); QHeaderView *header = m_view.running_jobs->header(); header->setSectionResizeMode(0, QHeaderView::Fixed); header->resizeSection(0, size + 4); header->setSectionResizeMode(1, QHeaderView::Interactive); m_view.scripts_list->setHeaderLabels(QStringList() << QString() << i18n("Stored Playlists")); m_scriptsDelegate = new RenderViewDelegate(this); m_view.scripts_list->setItemDelegate(m_scriptsDelegate); header = m_view.scripts_list->header(); header->setSectionResizeMode(0, QHeaderView::Fixed); header->resizeSection(0, size + 4); // Find path for Kdenlive renderer #ifdef Q_OS_WIN m_renderer = QCoreApplication::applicationDirPath() + QStringLiteral("/kdenlive_render.exe"); #else m_renderer = QCoreApplication::applicationDirPath() + QStringLiteral("/kdenlive_render"); #endif if (!QFile::exists(m_renderer)) { m_renderer = QStandardPaths::findExecutable(QStringLiteral("kdenlive_render")); if (m_renderer.isEmpty()) { m_renderer = QStringLiteral("kdenlive_render"); } } QDBusConnectionInterface *interface = QDBusConnection::sessionBus().interface(); if ((interface == nullptr) || (!interface->isServiceRegistered(QStringLiteral("org.kde.ksmserver")) && !interface->isServiceRegistered(QStringLiteral("org.gnome.SessionManager")))) { m_view.shutdown->setEnabled(false); } #ifdef KF5_USE_PURPOSE m_shareMenu = new Purpose::Menu(); m_view.shareButton->setMenu(m_shareMenu); m_view.shareButton->setIcon(QIcon::fromTheme(QStringLiteral("document-share"))); connect(m_shareMenu, &Purpose::Menu::finished, this, &RenderWidget::slotShareActionFinished); #else m_view.shareButton->setEnabled(false); #endif m_view.parallel_process->setChecked(KdenliveSettings::parallelrender()); connect(m_view.parallel_process, &QCheckBox::stateChanged, [](int state) { KdenliveSettings::setParallelrender(state == Qt::Checked); }); m_view.field_order->setEnabled(false); connect(m_view.scanning_list, QOverload::of(&QComboBox::currentIndexChanged), [this](int index) { m_view.field_order->setEnabled(index == 2); }); refreshView(); focusFirstVisibleItem(); adjustSize(); } void RenderWidget::slotShareActionFinished(const QJsonObject &output, int error, const QString &message) { #ifdef KF5_USE_PURPOSE m_jobInfoMessage->hide(); if (error) { KMessageBox::error(this, i18n("There was a problem sharing the document: %1", message), i18n("Share")); } else { const QString url = output["url"].toString(); if (url.isEmpty()) { m_jobInfoMessage->setMessageType(KMessageWidget::Positive); m_jobInfoMessage->setText(i18n("Document shared successfully")); m_jobInfoMessage->show(); } else { KMessageBox::information(this, i18n("You can find the shared document at: %1", url), i18n("Share"), QString(), KMessageBox::Notify | KMessageBox::AllowLink); } } #endif } QSize RenderWidget::sizeHint() const { // Make sure the widget has minimum size on opening return QSize(200, 200); } RenderWidget::~RenderWidget() { m_view.running_jobs->blockSignals(true); m_view.scripts_list->blockSignals(true); m_view.running_jobs->clear(); m_view.scripts_list->clear(); delete m_jobsDelegate; delete m_scriptsDelegate; delete m_infoMessage; delete m_jobInfoMessage; } void RenderWidget::slotEditItem(QTreeWidgetItem *item) { if (item->parent() == nullptr) { // This is a top level item - group - don't edit return; } const QString edit = item->data(0, EditableRole).toString(); if (edit.isEmpty() || !edit.endsWith(QLatin1String("customprofiles.xml"))) { slotSaveProfile(); } else { slotEditProfile(); } } void RenderWidget::showInfoPanel() { if (m_view.advanced_params->isVisible()) { m_view.advanced_params->setVisible(false); KdenliveSettings::setShowrenderparams(false); } else { m_view.advanced_params->setVisible(true); KdenliveSettings::setShowrenderparams(true); } } void RenderWidget::updateDocumentPath() { if (m_view.out_file->url().isEmpty()) { return; } const QString fileName = m_view.out_file->url().fileName(); m_view.out_file->setUrl(QUrl::fromLocalFile(QDir(pCore->currentDoc()->projectDataFolder()).absoluteFilePath(fileName))); parseScriptFiles(); } void RenderWidget::slotUpdateGuideBox() { m_view.guides_box->setVisible(m_view.render_guide->isChecked()); } void RenderWidget::slotCheckStartGuidePosition() { if (m_view.guide_start->currentIndex() > m_view.guide_end->currentIndex()) { m_view.guide_start->setCurrentIndex(m_view.guide_end->currentIndex()); } } void RenderWidget::slotCheckEndGuidePosition() { if (m_view.guide_end->currentIndex() < m_view.guide_start->currentIndex()) { m_view.guide_end->setCurrentIndex(m_view.guide_start->currentIndex()); } } void RenderWidget::setGuides(const QList &guidesList, double duration) { m_view.guide_start->clear(); m_view.guide_end->clear(); if (!guidesList.isEmpty()) { m_view.guide_start->addItem(i18n("Beginning"), "0"); m_view.render_guide->setEnabled(true); m_view.create_chapter->setEnabled(true); } else { m_view.render_guide->setEnabled(false); m_view.create_chapter->setEnabled(false); } double fps = pCore->getCurrentProfile()->fps(); for (int i = 0; i < guidesList.count(); i++) { - CommentedTime c = guidesList.at(i); + const CommentedTime &c = guidesList.at(i); GenTime pos = c.time(); const QString guidePos = Timecode::getStringTimecode(pos.frames(fps), fps); m_view.guide_start->addItem(c.comment() + QLatin1Char('/') + guidePos, pos.seconds()); m_view.guide_end->addItem(c.comment() + QLatin1Char('/') + guidePos, pos.seconds()); } if (!guidesList.isEmpty()) { m_view.guide_end->addItem(i18n("End"), QString::number(duration)); } } /** * Will be called when the user selects an output file via the file dialog. * File extension will be added automatically. */ void RenderWidget::slotUpdateButtons(const QUrl &url) { if (m_view.out_file->url().isEmpty()) { m_view.buttonGenerateScript->setEnabled(false); m_view.buttonRender->setEnabled(false); } else { updateButtons(); // This also checks whether the selected format is available } if (url.isValid()) { QTreeWidgetItem *item = m_view.formats->currentItem(); if ((item == nullptr) || (item->parent() == nullptr)) { // categories have no parent m_view.buttonRender->setEnabled(false); m_view.buttonGenerateScript->setEnabled(false); return; } const QString extension = item->data(0, ExtensionRole).toString(); m_view.out_file->setUrl(filenameWithExtension(url, extension)); } } /** * Will be called when the user changes the output file path in the text line. * File extension must NOT be added, would make editing impossible! */ void RenderWidget::slotUpdateButtons() { if (m_view.out_file->url().isEmpty()) { m_view.buttonRender->setEnabled(false); m_view.buttonGenerateScript->setEnabled(false); } else { updateButtons(); // This also checks whether the selected format is available } } void RenderWidget::slotSaveProfile() { Ui::SaveProfile_UI ui; QPointer d = new QDialog(this); ui.setupUi(d); QString customGroup; QStringList arguments = m_view.advanced_params->toPlainText().split(' ', QString::SkipEmptyParts); if (!arguments.isEmpty()) { ui.parameters->setText(arguments.join(QLatin1Char(' '))); } ui.profile_name->setFocus(); QTreeWidgetItem *item = m_view.formats->currentItem(); if ((item != nullptr) && (item->parent() != nullptr)) { // not a category // Duplicate current item settings customGroup = item->parent()->text(0); ui.extension->setText(item->data(0, ExtensionRole).toString()); if (ui.parameters->toPlainText().contains(QStringLiteral("%bitrate")) || ui.parameters->toPlainText().contains(QStringLiteral("%quality"))) { if (ui.parameters->toPlainText().contains(QStringLiteral("%quality"))) { ui.vbitrates_label->setText(i18n("Qualities")); ui.default_vbitrate_label->setText(i18n("Default quality")); } else { ui.vbitrates_label->setText(i18n("Bitrates")); ui.default_vbitrate_label->setText(i18n("Default bitrate")); } if (item->data(0, BitratesRole).canConvert(QVariant::StringList) && (item->data(0, BitratesRole).toStringList().count() != 0)) { QStringList bitrates = item->data(0, BitratesRole).toStringList(); ui.vbitrates_list->setText(bitrates.join(QLatin1Char(','))); if (item->data(0, DefaultBitrateRole).canConvert(QVariant::String)) { ui.default_vbitrate->setValue(item->data(0, DefaultBitrateRole).toInt()); } } } else { ui.vbitrates->setHidden(true); } if (ui.parameters->toPlainText().contains(QStringLiteral("%audiobitrate")) || ui.parameters->toPlainText().contains(QStringLiteral("%audioquality"))) { if (ui.parameters->toPlainText().contains(QStringLiteral("%audioquality"))) { ui.abitrates_label->setText(i18n("Qualities")); ui.default_abitrate_label->setText(i18n("Default quality")); } else { ui.abitrates_label->setText(i18n("Bitrates")); ui.default_abitrate_label->setText(i18n("Default bitrate")); } if ((item != nullptr) && item->data(0, AudioBitratesRole).canConvert(QVariant::StringList) && (item->data(0, AudioBitratesRole).toStringList().count() != 0)) { QStringList bitrates = item->data(0, AudioBitratesRole).toStringList(); ui.abitrates_list->setText(bitrates.join(QLatin1Char(','))); if (item->data(0, DefaultAudioBitrateRole).canConvert(QVariant::String)) { ui.default_abitrate->setValue(item->data(0, DefaultAudioBitrateRole).toInt()); } } } else { ui.abitrates->setHidden(true); } if (item->data(0, SpeedsRole).canConvert(QVariant::StringList) && (item->data(0, SpeedsRole).toStringList().count() != 0)) { QStringList speeds = item->data(0, SpeedsRole).toStringList(); ui.speeds_list->setText(speeds.join('\n')); } } if (customGroup.isEmpty()) { customGroup = i18nc("Group Name", "Custom"); } ui.group_name->setText(customGroup); if (d->exec() == QDialog::Accepted && !ui.profile_name->text().simplified().isEmpty()) { QString newProfileName = ui.profile_name->text().simplified(); QString newGroupName = ui.group_name->text().simplified(); if (newGroupName.isEmpty()) { newGroupName = i18nc("Group Name", "Custom"); } QDomDocument doc; QDomElement profileElement = doc.createElement(QStringLiteral("profile")); profileElement.setAttribute(QStringLiteral("name"), newProfileName); profileElement.setAttribute(QStringLiteral("category"), newGroupName); profileElement.setAttribute(QStringLiteral("extension"), ui.extension->text().simplified()); QString args = ui.parameters->toPlainText().simplified(); profileElement.setAttribute(QStringLiteral("args"), args); if (args.contains(QStringLiteral("%bitrate"))) { // profile has a variable bitrate profileElement.setAttribute(QStringLiteral("defaultbitrate"), QString::number(ui.default_vbitrate->value())); profileElement.setAttribute(QStringLiteral("bitrates"), ui.vbitrates_list->text()); } else if (args.contains(QStringLiteral("%quality"))) { profileElement.setAttribute(QStringLiteral("defaultquality"), QString::number(ui.default_vbitrate->value())); profileElement.setAttribute(QStringLiteral("qualities"), ui.vbitrates_list->text()); } if (args.contains(QStringLiteral("%audiobitrate"))) { // profile has a variable bitrate profileElement.setAttribute(QStringLiteral("defaultaudiobitrate"), QString::number(ui.default_abitrate->value())); profileElement.setAttribute(QStringLiteral("audiobitrates"), ui.abitrates_list->text()); } else if (args.contains(QStringLiteral("%audioquality"))) { // profile has a variable bitrate profileElement.setAttribute(QStringLiteral("defaultaudioquality"), QString::number(ui.default_abitrate->value())); profileElement.setAttribute(QStringLiteral("audioqualities"), ui.abitrates_list->text()); } QString speeds_list_str = ui.speeds_list->toPlainText(); if (!speeds_list_str.isEmpty()) { profileElement.setAttribute(QStringLiteral("speeds"), speeds_list_str.replace('\n', ';').simplified()); } doc.appendChild(profileElement); saveProfile(doc.documentElement()); parseProfiles(); } delete d; } bool RenderWidget::saveProfile(QDomElement newprofile) { QDir dir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/export/")); if (!dir.exists()) { dir.mkpath(QStringLiteral(".")); } QDomDocument doc; QFile file(dir.absoluteFilePath(QStringLiteral("customprofiles.xml"))); doc.setContent(&file, false); file.close(); QDomElement documentElement; QDomElement profiles = doc.documentElement(); if (profiles.isNull() || profiles.tagName() != QLatin1String("profiles")) { doc.clear(); profiles = doc.createElement(QStringLiteral("profiles")); profiles.setAttribute(QStringLiteral("version"), 1); doc.appendChild(profiles); } int version = profiles.attribute(QStringLiteral("version"), nullptr).toInt(); if (version < 1) { doc.clear(); profiles = doc.createElement(QStringLiteral("profiles")); profiles.setAttribute(QStringLiteral("version"), 1); doc.appendChild(profiles); } QDomNodeList profilelist = doc.elementsByTagName(QStringLiteral("profile")); QString newProfileName = newprofile.attribute(QStringLiteral("name")); // Check existing profiles QStringList existingProfileNames; int i = 0; while (!profilelist.item(i).isNull()) { documentElement = profilelist.item(i).toElement(); QString profileName = documentElement.attribute(QStringLiteral("name")); existingProfileNames << profileName; i++; } // Check if a profile with that same name already exists bool ok; while (existingProfileNames.contains(newProfileName)) { QString updatedProfileName = QInputDialog::getText(this, i18n("Profile already exists"), i18n("This profile name already exists. Change the name if you don't want to overwrite it."), QLineEdit::Normal, newProfileName, &ok); if (!ok) { return false; } if (updatedProfileName == newProfileName) { // remove previous profile profiles.removeChild(profilelist.item(existingProfileNames.indexOf(newProfileName))); break; } else { newProfileName = updatedProfileName; newprofile.setAttribute(QStringLiteral("name"), newProfileName); } } profiles.appendChild(newprofile); // QCString save = doc.toString().utf8(); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { KMessageBox::sorry(this, i18n("Unable to write to file %1", dir.absoluteFilePath("customprofiles.xml"))); return false; } QTextStream out(&file); out << doc.toString(); if (file.error() != QFile::NoError) { KMessageBox::error(this, i18n("Cannot write to file %1", dir.absoluteFilePath("customprofiles.xml"))); file.close(); return false; } file.close(); return true; } void RenderWidget::slotCopyToFavorites() { QTreeWidgetItem *item = m_view.formats->currentItem(); if ((item == nullptr) || (item->parent() == nullptr)) { return; } QString params = item->data(0, ParamsRole).toString(); QString extension = item->data(0, ExtensionRole).toString(); QString currentProfile = item->text(0); QDomDocument doc; QDomElement profileElement = doc.createElement(QStringLiteral("profile")); profileElement.setAttribute(QStringLiteral("name"), currentProfile); profileElement.setAttribute(QStringLiteral("category"), i18nc("Category Name", "Custom")); profileElement.setAttribute(QStringLiteral("destinationid"), QStringLiteral("favorites")); profileElement.setAttribute(QStringLiteral("extension"), extension); profileElement.setAttribute(QStringLiteral("args"), params); if (params.contains(QStringLiteral("%bitrate"))) { // profile has a variable bitrate profileElement.setAttribute(QStringLiteral("defaultbitrate"), item->data(0, DefaultBitrateRole).toString()); profileElement.setAttribute(QStringLiteral("bitrates"), item->data(0, BitratesRole).toStringList().join(QLatin1Char(','))); } else if (params.contains(QStringLiteral("%quality"))) { profileElement.setAttribute(QStringLiteral("defaultquality"), item->data(0, DefaultBitrateRole).toString()); profileElement.setAttribute(QStringLiteral("qualities"), item->data(0, BitratesRole).toStringList().join(QLatin1Char(','))); } if (params.contains(QStringLiteral("%audiobitrate"))) { // profile has a variable bitrate profileElement.setAttribute(QStringLiteral("defaultaudiobitrate"), item->data(0, DefaultAudioBitrateRole).toString()); profileElement.setAttribute(QStringLiteral("audiobitrates"), item->data(0, AudioBitratesRole).toStringList().join(QLatin1Char(','))); } else if (params.contains(QStringLiteral("%audioquality"))) { // profile has a variable bitrate profileElement.setAttribute(QStringLiteral("defaultaudioquality"), item->data(0, DefaultAudioBitrateRole).toString()); profileElement.setAttribute(QStringLiteral("audioqualities"), item->data(0, AudioBitratesRole).toStringList().join(QLatin1Char(','))); } if (item->data(0, SpeedsRole).canConvert(QVariant::StringList) && (item->data(0, SpeedsRole).toStringList().count() != 0)) { // profile has a variable speed profileElement.setAttribute(QStringLiteral("speeds"), item->data(0, SpeedsRole).toStringList().join(QLatin1Char(';'))); } doc.appendChild(profileElement); if (saveProfile(doc.documentElement())) { parseProfiles(profileElement.attribute(QStringLiteral("name"))); } } void RenderWidget::slotDownloadNewRenderProfiles() { if (getNewStuff(QStringLiteral(":data/kdenlive_renderprofiles.knsrc")) > 0) { reloadProfiles(); } } int RenderWidget::getNewStuff(const QString &configFile) { KNS3::Entry::List entries; QPointer dialog = new KNS3::DownloadDialog(configFile); if (dialog->exec() != 0) { entries = dialog->changedEntries(); } for (const KNS3::Entry &entry : entries) { if (entry.status() == KNS3::Entry::Installed) { qCDebug(KDENLIVE_LOG) << "// Installed files: " << entry.installedFiles(); } } delete dialog; return entries.size(); } void RenderWidget::slotEditProfile() { QTreeWidgetItem *item = m_view.formats->currentItem(); if ((item == nullptr) || (item->parent() == nullptr)) { return; } QString params = item->data(0, ParamsRole).toString(); Ui::SaveProfile_UI ui; QPointer d = new QDialog(this); ui.setupUi(d); QString customGroup = item->parent()->text(0); if (customGroup.isEmpty()) { customGroup = i18nc("Group Name", "Custom"); } ui.group_name->setText(customGroup); ui.profile_name->setText(item->text(0)); ui.extension->setText(item->data(0, ExtensionRole).toString()); ui.parameters->setText(params); ui.profile_name->setFocus(); if (params.contains(QStringLiteral("%bitrate")) || ui.parameters->toPlainText().contains(QStringLiteral("%quality"))) { if (params.contains(QStringLiteral("%quality"))) { ui.vbitrates_label->setText(i18n("Qualities")); ui.default_vbitrate_label->setText(i18n("Default quality")); } else { ui.vbitrates_label->setText(i18n("Bitrates")); ui.default_vbitrate_label->setText(i18n("Default bitrate")); } if (item->data(0, BitratesRole).canConvert(QVariant::StringList) && (item->data(0, BitratesRole).toStringList().count() != 0)) { QStringList bitrates = item->data(0, BitratesRole).toStringList(); ui.vbitrates_list->setText(bitrates.join(QLatin1Char(','))); if (item->data(0, DefaultBitrateRole).canConvert(QVariant::String)) { ui.default_vbitrate->setValue(item->data(0, DefaultBitrateRole).toInt()); } } } else { ui.vbitrates->setHidden(true); } if (params.contains(QStringLiteral("%audiobitrate")) || ui.parameters->toPlainText().contains(QStringLiteral("%audioquality"))) { if (params.contains(QStringLiteral("%audioquality"))) { ui.abitrates_label->setText(i18n("Qualities")); ui.default_abitrate_label->setText(i18n("Default quality")); } else { ui.abitrates_label->setText(i18n("Bitrates")); ui.default_abitrate_label->setText(i18n("Default bitrate")); } if (item->data(0, AudioBitratesRole).canConvert(QVariant::StringList) && (item->data(0, AudioBitratesRole).toStringList().count() != 0)) { QStringList bitrates = item->data(0, AudioBitratesRole).toStringList(); ui.abitrates_list->setText(bitrates.join(QLatin1Char(','))); if (item->data(0, DefaultAudioBitrateRole).canConvert(QVariant::String)) { ui.default_abitrate->setValue(item->data(0, DefaultAudioBitrateRole).toInt()); } } } else { ui.abitrates->setHidden(true); } if (item->data(0, SpeedsRole).canConvert(QVariant::StringList) && (item->data(0, SpeedsRole).toStringList().count() != 0)) { QStringList speeds = item->data(0, SpeedsRole).toStringList(); ui.speeds_list->setText(speeds.join('\n')); } d->setWindowTitle(i18n("Edit Profile")); if (d->exec() == QDialog::Accepted) { slotDeleteProfile(true); QString exportFile = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/export/customprofiles.xml"); QDomDocument doc; QFile file(exportFile); doc.setContent(&file, false); file.close(); QDomElement documentElement; QDomElement profiles = doc.documentElement(); if (profiles.isNull() || profiles.tagName() != QLatin1String("profiles")) { doc.clear(); profiles = doc.createElement(QStringLiteral("profiles")); profiles.setAttribute(QStringLiteral("version"), 1); doc.appendChild(profiles); } int version = profiles.attribute(QStringLiteral("version"), nullptr).toInt(); if (version < 1) { doc.clear(); profiles = doc.createElement(QStringLiteral("profiles")); profiles.setAttribute(QStringLiteral("version"), 1); doc.appendChild(profiles); } QString newProfileName = ui.profile_name->text().simplified(); QString newGroupName = ui.group_name->text().simplified(); if (newGroupName.isEmpty()) { newGroupName = i18nc("Group Name", "Custom"); } QDomNodeList profilelist = doc.elementsByTagName(QStringLiteral("profile")); int i = 0; while (!profilelist.item(i).isNull()) { // make sure a profile with same name doesn't exist documentElement = profilelist.item(i).toElement(); QString profileName = documentElement.attribute(QStringLiteral("name")); if (profileName == newProfileName) { // a profile with that same name already exists bool ok; newProfileName = QInputDialog::getText(this, i18n("Profile already exists"), i18n("This profile name already exists. Change the name if you don't want to overwrite it."), QLineEdit::Normal, newProfileName, &ok); if (!ok) { return; } if (profileName == newProfileName) { profiles.removeChild(profilelist.item(i)); break; } } ++i; } QDomElement profileElement = doc.createElement(QStringLiteral("profile")); profileElement.setAttribute(QStringLiteral("name"), newProfileName); profileElement.setAttribute(QStringLiteral("category"), newGroupName); profileElement.setAttribute(QStringLiteral("extension"), ui.extension->text().simplified()); QString args = ui.parameters->toPlainText().simplified(); profileElement.setAttribute(QStringLiteral("args"), args); if (args.contains(QStringLiteral("%bitrate"))) { // profile has a variable bitrate profileElement.setAttribute(QStringLiteral("defaultbitrate"), QString::number(ui.default_vbitrate->value())); profileElement.setAttribute(QStringLiteral("bitrates"), ui.vbitrates_list->text()); } else if (args.contains(QStringLiteral("%quality"))) { profileElement.setAttribute(QStringLiteral("defaultquality"), QString::number(ui.default_vbitrate->value())); profileElement.setAttribute(QStringLiteral("qualities"), ui.vbitrates_list->text()); } if (args.contains(QStringLiteral("%audiobitrate"))) { // profile has a variable bitrate profileElement.setAttribute(QStringLiteral("defaultaudiobitrate"), QString::number(ui.default_abitrate->value())); profileElement.setAttribute(QStringLiteral("audiobitrates"), ui.abitrates_list->text()); } else if (args.contains(QStringLiteral("%audioquality"))) { profileElement.setAttribute(QStringLiteral("defaultaudioquality"), QString::number(ui.default_abitrate->value())); profileElement.setAttribute(QStringLiteral("audioqualities"), ui.abitrates_list->text()); } QString speeds_list_str = ui.speeds_list->toPlainText(); if (!speeds_list_str.isEmpty()) { // profile has a variable speed profileElement.setAttribute(QStringLiteral("speeds"), speeds_list_str.replace('\n', ';').simplified()); } profiles.appendChild(profileElement); // QCString save = doc.toString().utf8(); delete d; if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { KMessageBox::error(this, i18n("Cannot write to file %1", exportFile)); return; } QTextStream out(&file); out << doc.toString(); if (file.error() != QFile::NoError) { KMessageBox::error(this, i18n("Cannot write to file %1", exportFile)); file.close(); return; } file.close(); parseProfiles(); } else { delete d; } } void RenderWidget::slotDeleteProfile(bool dontRefresh) { // TODO: delete a profile installed by KNewStuff the easy way /* QString edit = m_view.formats->currentItem()->data(EditableRole).toString(); if (!edit.endsWith(QLatin1String("customprofiles.xml"))) { // This is a KNewStuff installed file, process through KNS KNS::Engine engine(0); if (engine.init("kdenlive_render.knsrc")) { KNS::Entry::List entries; } return; }*/ QTreeWidgetItem *item = m_view.formats->currentItem(); if ((item == nullptr) || (item->parent() == nullptr)) { return; } QString currentProfile = item->text(0); QString exportFile = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/export/customprofiles.xml"); QDomDocument doc; QFile file(exportFile); doc.setContent(&file, false); file.close(); QDomElement documentElement; QDomNodeList profiles = doc.elementsByTagName(QStringLiteral("profile")); int i = 0; QString profileName; while (!profiles.item(i).isNull()) { documentElement = profiles.item(i).toElement(); profileName = documentElement.attribute(QStringLiteral("name")); if (profileName == currentProfile) { doc.documentElement().removeChild(profiles.item(i)); break; } ++i; } if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { KMessageBox::sorry(this, i18n("Unable to write to file %1", exportFile)); return; } QTextStream out(&file); out << doc.toString(); if (file.error() != QFile::NoError) { KMessageBox::error(this, i18n("Cannot write to file %1", exportFile)); file.close(); return; } file.close(); if (dontRefresh) { return; } parseProfiles(); focusFirstVisibleItem(); } void RenderWidget::updateButtons() { if ((m_view.formats->currentItem() == nullptr) || m_view.formats->currentItem()->isHidden()) { m_view.buttonSave->setEnabled(false); m_view.buttonDelete->setEnabled(false); m_view.buttonEdit->setEnabled(false); m_view.buttonRender->setEnabled(false); m_view.buttonGenerateScript->setEnabled(false); } else { m_view.buttonSave->setEnabled(true); m_view.buttonRender->setEnabled(m_view.formats->currentItem()->data(0, ErrorRole).isNull()); m_view.buttonGenerateScript->setEnabled(m_view.formats->currentItem()->data(0, ErrorRole).isNull()); QString edit = m_view.formats->currentItem()->data(0, EditableRole).toString(); if (edit.isEmpty() || !edit.endsWith(QLatin1String("customprofiles.xml"))) { m_view.buttonDelete->setEnabled(false); m_view.buttonEdit->setEnabled(false); } else { m_view.buttonDelete->setEnabled(true); m_view.buttonEdit->setEnabled(true); } } } void RenderWidget::focusFirstVisibleItem(const QString &profile) { QTreeWidgetItem *item = nullptr; if (!profile.isEmpty()) { QList items = m_view.formats->findItems(profile, Qt::MatchExactly | Qt::MatchRecursive); if (!items.isEmpty()) { item = items.constFirst(); } } if (!item) { // searched profile not found in any category, select 1st available profile for (int i = 0; i < m_view.formats->topLevelItemCount(); ++i) { item = m_view.formats->topLevelItem(i); if (item->childCount() > 0) { item = item->child(0); break; } } } if (item) { m_view.formats->setCurrentItem(item); item->parent()->setExpanded(true); refreshParams(); } updateButtons(); } void RenderWidget::slotPrepareExport(bool delayedRendering, const QString &scriptPath) { Q_UNUSED(scriptPath); if (!QFile::exists(KdenliveSettings::rendererpath())) { KMessageBox::sorry(this, i18n("Cannot find the melt program required for rendering (part of Mlt)")); return; } if (QFile::exists(m_view.out_file->url().toLocalFile())) { if (KMessageBox::warningYesNo(this, i18n("Output file already exists. Do you want to overwrite it?")) != KMessageBox::Yes) { return; } } QString chapterFile; if (m_view.create_chapter->isChecked()) { chapterFile = m_view.out_file->url().toLocalFile() + QStringLiteral(".dvdchapter"); } // mantisbt 1051 QDir dir(m_view.out_file->url().adjusted(QUrl::RemoveFilename).toLocalFile()); if (!dir.exists() && !dir.mkpath(QStringLiteral("."))) { KMessageBox::sorry(this, i18n("The directory %1, could not be created.\nPlease make sure you have the required permissions.", m_view.out_file->url().adjusted(QUrl::RemoveFilename).toLocalFile())); return; } prepareRendering(delayedRendering, chapterFile); } void RenderWidget::prepareRendering(bool delayedRendering, const QString &chapterFile) { KdenliveDoc *project = pCore->currentDoc(); QString playlistPath; QString mltSuffix(QStringLiteral(".mlt")); QList playlistPaths; QList trackNames; QString renderName; if (delayedRendering) { bool ok; renderName = QFileInfo(pCore->currentDoc()->url().toLocalFile()).fileName(); if (renderName.isEmpty()) { renderName = i18n("export") + QStringLiteral(".mlt"); } QDir projectFolder(pCore->currentDoc()->projectDataFolder()); projectFolder.mkpath(QStringLiteral("kdenlive-renderqueue")); projectFolder.cd(QStringLiteral("kdenlive-renderqueue")); if (projectFolder.exists(renderName)) { int ix = 1; while (projectFolder.exists(renderName)) { if (renderName.contains(QLatin1Char('-'))) { renderName = renderName.section(QLatin1Char('-'), 0, -2); } else { renderName = renderName.section(QLatin1Char('.'), 0, -2); } renderName.append(QString("-%1.mlt").arg(ix)); ix++; } } renderName = renderName.section(QLatin1Char('.'), 0, -2); renderName = QInputDialog::getText(this, i18n("Delayed rendering"), i18n("Select a name for this rendering."), QLineEdit::Normal, renderName, &ok); if (!ok) { return; } if (!renderName.endsWith(QStringLiteral(".mlt"))) { renderName.append(QStringLiteral(".mlt")); } if (projectFolder.exists(renderName)) { if (KMessageBox::questionYesNo(this, i18n("File %1 already exists.\nDo you want to overwrite it?", renderName)) == KMessageBox::No) { return; } } playlistPath = projectFolder.absoluteFilePath(renderName); } else { QTemporaryFile tmp(QDir::tempPath() + "/kdenlive-XXXXXX.mlt"); if (!tmp.open()) { // Something went wrong return; } tmp.close(); playlistPath = tmp.fileName(); } int in = 0; int out; Monitor *pMon = pCore->getMonitor(Kdenlive::ProjectMonitor); bool zoneOnly = m_view.render_zone->isChecked(); if (zoneOnly) { in = pMon->getZoneStart(); out = pMon->getZoneEnd() - 1; } else { out = pCore->projectDuration() - 2; } QString playlistContent = pCore->projectManager()->projectSceneList(project->url().adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).toLocalFile()); if (!chapterFile.isEmpty()) { QDomDocument doc; QDomElement chapters = doc.createElement(QStringLiteral("chapters")); chapters.setAttribute(QStringLiteral("fps"), pCore->getCurrentFps()); doc.appendChild(chapters); const QList guidesList = project->getGuideModel()->getAllMarkers(); for (int i = 0; i < guidesList.count(); i++) { - CommentedTime c = guidesList.at(i); + const CommentedTime &c = guidesList.at(i); int time = c.time().frames(pCore->getCurrentFps()); if (time >= in && time < out) { if (zoneOnly) { time = time - in; } } QDomElement chapter = doc.createElement(QStringLiteral("chapter")); chapters.appendChild(chapter); chapter.setAttribute(QStringLiteral("title"), c.comment()); chapter.setAttribute(QStringLiteral("time"), time); } if (!chapters.childNodes().isEmpty()) { if (!project->getGuideModel()->hasMarker(out)) { // Always insert a guide in pos 0 QDomElement chapter = doc.createElement(QStringLiteral("chapter")); chapters.insertBefore(chapter, QDomNode()); chapter.setAttribute(QStringLiteral("title"), i18nc("the first in a list of chapters", "Start")); chapter.setAttribute(QStringLiteral("time"), QStringLiteral("0")); } // save chapters file QFile file(chapterFile); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { qCWarning(KDENLIVE_LOG) << "////// ERROR writing DVD CHAPTER file: " << chapterFile; } else { file.write(doc.toString().toUtf8()); if (file.error() != QFile::NoError) { qCWarning(KDENLIVE_LOG) << "////// ERROR writing DVD CHAPTER file: " << chapterFile; } file.close(); } } } // Set playlist audio volume to 100% QDomDocument doc; doc.setContent(playlistContent); QDomElement tractor = doc.documentElement().firstChildElement(QStringLiteral("tractor")); if (!tractor.isNull()) { QDomNodeList props = tractor.elementsByTagName(QStringLiteral("property")); for (int i = 0; i < props.count(); ++i) { if (props.at(i).toElement().attribute(QStringLiteral("name")) == QLatin1String("meta.volume")) { props.at(i).firstChild().setNodeValue(QStringLiteral("1")); break; } } } // Add autoclose to playlists. QDomNodeList playlists = doc.elementsByTagName(QStringLiteral("playlist")); for (int i = 0; i < playlists.length(); ++i) { playlists.item(i).toElement().setAttribute(QStringLiteral("autoclose"), 1); } // Do we want proxy rendering if (project->useProxy() && !proxyRendering()) { QString root = doc.documentElement().attribute(QStringLiteral("root")); if (!root.isEmpty() && !root.endsWith(QLatin1Char('/'))) { root.append(QLatin1Char('/')); } // replace proxy clips with originals QMap proxies = pCore->projectItemModel()->getProxies(pCore->currentDoc()->documentRoot()); QDomNodeList producers = doc.elementsByTagName(QStringLiteral("producer")); QString producerResource; QString producerService; QString suffix; QString prefix; for (int n = 0; n < producers.length(); ++n) { QDomElement e = producers.item(n).toElement(); producerResource = Xml::getXmlProperty(e, QStringLiteral("resource")); producerService = Xml::getXmlProperty(e, QStringLiteral("mlt_service")); if (producerResource.isEmpty() || producerService == QLatin1String("color")) { continue; } if (producerService == QLatin1String("timewarp")) { // slowmotion producer prefix = producerResource.section(QLatin1Char(':'), 0, 0) + QLatin1Char(':'); producerResource = producerResource.section(QLatin1Char(':'), 1); } else { prefix.clear(); } if (producerService == QLatin1String("framebuffer")) { // slowmotion producer suffix = QLatin1Char('?') + producerResource.section(QLatin1Char('?'), 1); producerResource = producerResource.section(QLatin1Char('?'), 0, 0); } else { suffix.clear(); } if (!producerResource.isEmpty()) { if (QFileInfo(producerResource).isRelative()) { producerResource.prepend(root); } if (proxies.contains(producerResource)) { QString replacementResource = proxies.value(producerResource); Xml::setXmlProperty(e, QStringLiteral("resource"), prefix + replacementResource + suffix); if (producerService == QLatin1String("timewarp")) { Xml::setXmlProperty(e, QStringLiteral("warp_resource"), replacementResource); } // We need to delete the "aspect_ratio" property because proxy clips // sometimes have different ratio than original clips Xml::removeXmlProperty(e, QStringLiteral("aspect_ratio")); Xml::removeMetaProperties(e); } } } } generateRenderFiles(doc, playlistPath, in, out, delayedRendering); } void RenderWidget::generateRenderFiles(QDomDocument doc, const QString &playlistPath, int in, int out, bool delayedRendering) { QDomDocument clone; KdenliveDoc *project = pCore->currentDoc(); int passes = m_view.checkTwoPass->isChecked() ? 2 : 1; QString renderArgs = m_view.advanced_params->toPlainText().simplified(); QDomElement consumer = doc.createElement(QStringLiteral("consumer")); QDomNodeList profiles = doc.elementsByTagName(QStringLiteral("profile")); if (profiles.isEmpty()) { doc.documentElement().insertAfter(consumer, doc.documentElement()); } else { doc.documentElement().insertAfter(consumer, profiles.at(profiles.length() - 1)); } // Check for fps change double forcedfps = 0; if (renderArgs.startsWith(QLatin1String("r="))) { QString sub = renderArgs.section(QLatin1Char(' '), 0, 0).toLower(); sub = sub.section(QLatin1Char('='), 1, 1); forcedfps = sub.toDouble(); } else if (renderArgs.contains(QStringLiteral(" r="))) { QString sub = renderArgs.section(QStringLiteral(" r="), 1, 1); sub = sub.section(QLatin1Char(' '), 0, 0).toLower(); forcedfps = sub.toDouble(); } else if (renderArgs.contains(QStringLiteral("mlt_profile="))) { QString sub = renderArgs.section(QStringLiteral("mlt_profile="), 1, 1); sub = sub.section(QLatin1Char(' '), 0, 0).toLower(); forcedfps = ProfileRepository::get()->getProfile(sub)->fps(); } bool resizeProfile = false; std::unique_ptr &profile = pCore->getCurrentProfile(); if (renderArgs.contains(QLatin1String("%dv_standard"))) { QString dvstd; if (fmod((double)profile->frame_rate_num() / profile->frame_rate_den(), 30.01) > 27) { dvstd = QStringLiteral("ntsc"); if (!(profile->frame_rate_num() == 30000 && profile->frame_rate_den() == 1001)) { forcedfps = 30000.0 / 1001; } if (!(profile->width() == 720 && profile->height() == 480)) { resizeProfile = true; } } else { dvstd = QStringLiteral("pal"); if (!(profile->frame_rate_num() == 25 && profile->frame_rate_den() == 1)) { forcedfps = 25; } if (!(profile->width() == 720 && profile->height() == 576)) { resizeProfile = true; } } if ((double)profile->display_aspect_num() / profile->display_aspect_den() > 1.5) { dvstd += QLatin1String("_wide"); } renderArgs.replace(QLatin1String("%dv_standard"), dvstd); } QStringList args = renderArgs.split(QLatin1Char(' ')); for (auto ¶m : args) { if (param.contains(QLatin1Char('='))) { QString paramValue = param.section(QLatin1Char('='), 1); if (paramValue.startsWith(QLatin1Char('%'))) { if (paramValue.startsWith(QStringLiteral("%bitrate")) || paramValue == QStringLiteral("%quality")) { if (paramValue.contains("+'k'")) paramValue = QString::number(m_view.video->value()) + 'k'; else paramValue = QString::number(m_view.video->value()); } if (paramValue.startsWith(QStringLiteral("%audiobitrate")) || paramValue == QStringLiteral("%audioquality")) { if (paramValue.contains("+'k'")) paramValue = QString::number(m_view.audio->value()) + 'k'; else paramValue = QString::number(m_view.audio->value()); } if (paramValue == QStringLiteral("%dar")) paramValue = '@' + QString::number(profile->display_aspect_num()) + QLatin1Char('/') + QString::number(profile->display_aspect_den()); if (paramValue == QStringLiteral("%passes")) paramValue = QString::number(static_cast(m_view.checkTwoPass->isChecked()) + 1); } consumer.setAttribute(param.section(QLatin1Char('='), 0, 0), paramValue); } } // Check for movit if (KdenliveSettings::gpu_accel()) { consumer.setAttribute(QStringLiteral("glsl."), 1); } // in/out points if (m_view.render_guide->isChecked()) { double fps = profile->fps(); double guideStart = m_view.guide_start->itemData(m_view.guide_start->currentIndex()).toDouble(); double guideEnd = m_view.guide_end->itemData(m_view.guide_end->currentIndex()).toDouble(); consumer.setAttribute(QStringLiteral("in"), (int)GenTime(guideStart).frames(fps)); consumer.setAttribute(QStringLiteral("out"), (int)GenTime(guideEnd).frames(fps)); } else { consumer.setAttribute(QStringLiteral("in"), in); consumer.setAttribute(QStringLiteral("out"), out); } // Check if the rendering profile is different from project profile, // in which case we need to use the producer_comsumer from MLT QString subsize; if (renderArgs.startsWith(QLatin1String("s="))) { subsize = renderArgs.section(QLatin1Char(' '), 0, 0).toLower(); subsize = subsize.section(QLatin1Char('='), 1, 1); } else if (renderArgs.contains(QStringLiteral(" s="))) { subsize = renderArgs.section(QStringLiteral(" s="), 1, 1); subsize = subsize.section(QLatin1Char(' '), 0, 0).toLower(); } else if (m_view.rescale->isChecked() && m_view.rescale->isEnabled()) { subsize = QStringLiteral("%1x%2").arg(m_view.rescale_width->value()).arg(m_view.rescale_height->value()); } if (!subsize.isEmpty()) { consumer.setAttribute(QStringLiteral("s"), subsize); } // Check if we need to embed the playlist into the producer consumer // That is required if PAR != 1 if (profile->sample_aspect_num() != profile->sample_aspect_den() && subsize.isEmpty()) { resizeProfile = true; } // Project metadata if (m_view.export_meta->isChecked()) { QMap metadata = project->metadata(); QMap::const_iterator i = metadata.constBegin(); while (i != metadata.constEnd()) { consumer.setAttribute(i.key(), QString(QUrl::toPercentEncoding(i.value()))); ++i; } } // Adjust scanning switch (m_view.scanning_list->currentIndex()) { case 1: consumer.setAttribute(QStringLiteral("progressive"), 1); break; case 2: // Interlaced rendering consumer.setAttribute(QStringLiteral("progressive"), 0); // Adjust field order consumer.setAttribute(QStringLiteral("top_field_first"), m_view.field_order->currentIndex()); break; default: // leave as is break; } // check if audio export is selected bool exportAudio; if (automaticAudioExport()) { // TODO check if projact contains audio // exportAudio = pCore->projectManager()->currentTimeline()->checkProjectAudio(); exportAudio = true; } else { exportAudio = selectedAudioExport(); } // disable audio if requested if (!exportAudio) { consumer.setAttribute(QStringLiteral("an"), 1); } int threadCount = QThread::idealThreadCount(); if (threadCount > 2 && m_view.parallel_process->isChecked()) { threadCount = qMin(threadCount - 1, 4); } else { threadCount = 1; } // Set the thread counts if (!renderArgs.contains(QStringLiteral("threads="))) { consumer.setAttribute(QStringLiteral("threads"), KdenliveSettings::encodethreads()); } consumer.setAttribute(QStringLiteral("real_time"), -threadCount); // check which audio tracks have to be exported /*if (stemExport) { // TODO refac //TODO port to new timeline model Timeline *ct = pCore->projectManager()->currentTimeline(); int allTracksCount = ct->tracksCount(); // reset tracks count (tracks to be rendered) tracksCount = 0; // begin with track 1 (track zero is a hidden black track) for (int i = 1; i < allTracksCount; i++) { Track *track = ct->track(i); // add only tracks to render list that are not muted and have audio if ((track != nullptr) && !track->info().isMute && track->hasAudio()) { QDomDocument docCopy = doc.cloneNode(true).toDocument(); QString trackName = track->info().trackName; // save track name trackNames << trackName; qCDebug(KDENLIVE_LOG) << "Track-Name: " << trackName; // create stem export doc content QDomNodeList tracks = docCopy.elementsByTagName(QStringLiteral("track")); for (int j = 0; j < allTracksCount; j++) { if (j != i) { // mute other tracks tracks.at(j).toElement().setAttribute(QStringLiteral("hide"), QStringLiteral("both")); } } docList << docCopy; tracksCount++; } } }*/ if (m_view.checkTwoPass->isChecked()) { // We will generate 2 files, one for each pass. clone = doc.cloneNode(true).toDocument(); } QStringList playlists; QString renderedFile = m_view.out_file->url().toLocalFile(); for (int i = 0; i < passes; i++) { // Append consumer settings QDomDocument final = i > 0 ? clone : doc; QDomNodeList cons = final.elementsByTagName(QStringLiteral("consumer")); QDomElement myConsumer = cons.at(0).toElement(); QString mytarget = renderedFile; QString playlistName = playlistPath; myConsumer.setAttribute(QStringLiteral("mlt_service"), QStringLiteral("avformat")); if (passes == 2 && i == 1) { playlistName = playlistName.section(QLatin1Char('.'), 0, -2) + QString("-pass%1.").arg(i + 1) + playlistName.section(QLatin1Char('.'), -1); } playlists << playlistName; myConsumer.setAttribute(QStringLiteral("target"), mytarget); // Prepare rendering args int pass = passes == 2 ? i + 1 : 0; if (renderArgs.contains(QStringLiteral("libx265"))) { if (pass == 1 || pass == 2) { QString x265params = myConsumer.attribute("x265-params"); x265params = QString("pass=%1:stats=%2:%3").arg(pass).arg(mytarget.replace(":", "\\:") + "_2pass.log").arg(x265params); myConsumer.setAttribute("x265-params", x265params); } } else { if (pass == 1 || pass == 2) { myConsumer.setAttribute("pass", pass); myConsumer.setAttribute("passlogfile", mytarget + "_2pass.log"); } if (pass == 1) { myConsumer.setAttribute("fastfirstpass", 1); myConsumer.removeAttribute("acodec"); myConsumer.setAttribute("an", 1); } else { myConsumer.removeAttribute("fastfirstpass"); } } QFile file(playlistName); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { pCore->displayMessage(i18n("Cannot write to file %1", playlistName), ErrorMessage); return; } file.write(final.toString().toUtf8()); if (file.error() != QFile::NoError) { pCore->displayMessage(i18n("Cannot write to file %1", playlistName), ErrorMessage); file.close(); return; } file.close(); } // Create job RenderJobItem *renderItem = nullptr; QList existing = m_view.running_jobs->findItems(renderedFile, Qt::MatchExactly, 1); if (!existing.isEmpty()) { renderItem = static_cast(existing.at(0)); if (renderItem->status() == RUNNINGJOB || renderItem->status() == WAITINGJOB || renderItem->status() == STARTINGJOB) { KMessageBox::information( this, i18n("There is already a job writing file:
%1
Abort the job if you want to overwrite it...", renderedFile), i18n("Already running")); return; } if (delayedRendering || playlists.size() > 1) { delete renderItem; renderItem = nullptr; } else { renderItem->setData(1, ProgressRole, 0); renderItem->setStatus(WAITINGJOB); renderItem->setIcon(0, QIcon::fromTheme(QStringLiteral("media-playback-pause"))); renderItem->setData(1, Qt::UserRole, i18n("Waiting...")); QStringList argsJob = {KdenliveSettings::rendererpath(), playlistPath, renderedFile, QStringLiteral("-pid:%1").arg(QCoreApplication::applicationPid())}; renderItem->setData(1, ParametersRole, argsJob); renderItem->setData(1, TimeRole, QDateTime::currentDateTime()); if (!exportAudio) { renderItem->setData(1, ExtraInfoRole, i18n("Video without audio track")); } else { renderItem->setData(1, ExtraInfoRole, QString()); } m_view.running_jobs->setCurrentItem(renderItem); m_view.tabWidget->setCurrentIndex(1); checkRenderStatus(); return; } } if (delayedRendering) { parseScriptFiles(); return; } QList jobList; for (const QString &pl : playlists) { renderItem = new RenderJobItem(m_view.running_jobs, QStringList() << QString() << renderedFile); renderItem->setData(1, TimeRole, QDateTime::currentDateTime()); QStringList argsJob = {KdenliveSettings::rendererpath(), pl, renderedFile, QStringLiteral("-pid:%1").arg(QCoreApplication::applicationPid())}; renderItem->setData(1, ParametersRole, argsJob); qDebug() << "* CREATED JOB WITH ARGS: " << argsJob; if (!exportAudio) { renderItem->setData(1, ExtraInfoRole, i18n("Video without audio track")); } else { renderItem->setData(1, ExtraInfoRole, QString()); } jobList << renderItem; } m_view.running_jobs->setCurrentItem(jobList.at(0)); m_view.tabWidget->setCurrentIndex(1); // check render status checkRenderStatus(); // create full playlistPaths /*for (int i = 0; i < tracksCount; i++) { QString plPath(playlistPath); // add track number to path name if (stemExport) { plPath = plPath + QLatin1Char('_') + QString(trackNames.at(i)).replace(QLatin1Char(' '), QLatin1Char('_')); } // add mlt suffix if (!plPath.endsWith(mltSuffix)) { plPath += mltSuffix; } playlistPaths << plPath; qCDebug(KDENLIVE_LOG) << "playlistPath: " << plPath << endl; // Do save scenelist QFile file(plPath); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { pCore->displayMessage(i18n("Cannot write to file %1", plPath), ErrorMessage); return; } file.write(docList.at(i).toString().toUtf8()); if (file.error() != QFile::NoError) { pCore->displayMessage(i18n("Cannot write to file %1", plPath), ErrorMessage); file.close(); return; } file.close(); }*/ // slotExport(delayedRendering, in, out, project->metadata(), playlistPaths, trackNames, renderName, exportAudio); } void RenderWidget::slotExport(bool scriptExport, int zoneIn, int zoneOut, const QMap &metadata, const QList &playlistPaths, const QList &trackNames, const QString &scriptPath, bool exportAudio) { // DEPRECATED QTreeWidgetItem *item = m_view.formats->currentItem(); if (!item) { return; } QString destBase = m_view.out_file->url().toLocalFile().trimmed(); if (destBase.isEmpty()) { return; } // script file QFile file(scriptPath); int stemCount = playlistPaths.count(); bool stemExport = (!trackNames.isEmpty()); for (int stemIdx = 0; stemIdx < stemCount; stemIdx++) { QString dest(destBase); // on stem export append track name to each filename if (stemExport) { QFileInfo dfi(dest); QStringList filePath; // construct the full file path filePath << dfi.absolutePath() << QDir::separator() << dfi.completeBaseName() + QLatin1Char('_') << QString(trackNames.at(stemIdx)).replace(QLatin1Char(' '), QLatin1Char('_')) << QStringLiteral(".") << dfi.suffix(); dest = filePath.join(QString()); } // Check whether target file has an extension. // If not, ask whether extension should be added or not. QString extension = item->data(0, ExtensionRole).toString(); if (!dest.endsWith(extension, Qt::CaseInsensitive)) { if (KMessageBox::questionYesNo(this, i18n("File has no extension. Add extension (%1)?", extension)) == KMessageBox::Yes) { dest.append('.' + extension); } } // Checks for image sequence QStringList imageSequences; imageSequences << QStringLiteral("jpg") << QStringLiteral("png") << QStringLiteral("bmp") << QStringLiteral("dpx") << QStringLiteral("ppm") << QStringLiteral("tga") << QStringLiteral("tif"); if (imageSequences.contains(extension)) { // format string for counter? if (!QRegExp(QStringLiteral(".*%[0-9]*d.*")).exactMatch(dest)) { dest = dest.section(QLatin1Char('.'), 0, -2) + QStringLiteral("_%05d.") + extension; } } if (QFile::exists(dest)) { if (KMessageBox::warningYesNo(this, i18n("Output file already exists. Do you want to overwrite it?")) != KMessageBox::Yes) { for (const QString &playlistFilePath : playlistPaths) { QFile playlistFile(playlistFilePath); if (playlistFile.exists()) { playlistFile.remove(); } } return; } } // Generate script file QStringList overlayargs; if (m_view.tc_overlay->isChecked()) { QString filterFile = QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("metadata.properties")); overlayargs << QStringLiteral("meta.attr.timecode=1") << "meta.attr.timecode.markup=#" + QString(m_view.tc_type->currentIndex() != 0 ? "frame" : "timecode"); overlayargs << QStringLiteral("-attach") << QStringLiteral("data_feed:attr_check") << QStringLiteral("-attach"); overlayargs << "data_show:" + filterFile << QStringLiteral("_loader=1") << QStringLiteral("dynamic=1"); } QStringList render_process_args; if (!scriptExport) { render_process_args << QStringLiteral("-erase"); } #ifndef Q_OS_WIN if (KdenliveSettings::usekuiserver()) { render_process_args << QStringLiteral("-kuiserver"); } // get process id render_process_args << QStringLiteral("-pid:%1").arg(QCoreApplication::applicationPid()); #endif // Set locale for render process if required if (QLocale().decimalPoint() != QLocale::system().decimalPoint()) { ; #ifndef Q_OS_MAC const QString currentLocale = setlocale(LC_NUMERIC, nullptr); #else const QString currentLocale = setlocale(LC_NUMERIC_MASK, nullptr); #endif render_process_args << QStringLiteral("-locale:%1").arg(currentLocale); } QString renderArgs = m_view.advanced_params->toPlainText().simplified(); QString std = renderArgs; // Check for fps change double forcedfps = 0; if (std.startsWith(QLatin1String("r="))) { QString sub = std.section(QLatin1Char(' '), 0, 0).toLower(); sub = sub.section(QLatin1Char('='), 1, 1); forcedfps = sub.toDouble(); } else if (std.contains(QStringLiteral(" r="))) { QString sub = std.section(QStringLiteral(" r="), 1, 1); sub = sub.section(QLatin1Char(' '), 0, 0).toLower(); forcedfps = sub.toDouble(); } else if (std.contains(QStringLiteral("mlt_profile="))) { QString sub = std.section(QStringLiteral("mlt_profile="), 1, 1); sub = sub.section(QLatin1Char(' '), 0, 0).toLower(); forcedfps = ProfileRepository::get()->getProfile(sub)->fps(); } bool resizeProfile = false; std::unique_ptr &profile = pCore->getCurrentProfile(); if (renderArgs.contains(QLatin1String("%dv_standard"))) { QString dvstd; if (fmod((double)profile->frame_rate_num() / profile->frame_rate_den(), 30.01) > 27) { dvstd = QStringLiteral("ntsc"); if (!(profile->frame_rate_num() == 30000 && profile->frame_rate_den() == 1001)) { forcedfps = 30000.0 / 1001; } if (!(profile->width() == 720 && profile->height() == 480)) { resizeProfile = true; } } else { dvstd = QStringLiteral("pal"); if (!(profile->frame_rate_num() == 25 && profile->frame_rate_den() == 1)) { forcedfps = 25; } if (!(profile->width() == 720 && profile->height() == 576)) { resizeProfile = true; } } if ((double)profile->display_aspect_num() / profile->display_aspect_den() > 1.5) { dvstd += QLatin1String("_wide"); } renderArgs.replace(QLatin1String("%dv_standard"), dvstd); } // If there is an fps change, we need to use the producer consumer AND update the in/out points if (forcedfps > 0 && qAbs((int)100 * forcedfps - ((int)100 * profile->frame_rate_num() / profile->frame_rate_den())) > 2) { resizeProfile = true; double ratio = profile->frame_rate_num() / profile->frame_rate_den() / forcedfps; if (ratio > 0) { zoneIn /= ratio; zoneOut /= ratio; } } if (m_view.render_guide->isChecked()) { double fps = profile->fps(); double guideStart = m_view.guide_start->itemData(m_view.guide_start->currentIndex()).toDouble(); double guideEnd = m_view.guide_end->itemData(m_view.guide_end->currentIndex()).toDouble(); render_process_args << "in=" + QString::number((int)GenTime(guideStart).frames(fps)) << "out=" + QString::number((int)GenTime(guideEnd).frames(fps)); } else { render_process_args << "in=" + QString::number(zoneIn) << "out=" + QString::number(zoneOut); } if (!overlayargs.isEmpty()) { render_process_args << "preargs=" + overlayargs.join(QLatin1Char(' ')); } render_process_args << profile->path() << item->data(0, RenderRole).toString(); if (!scriptExport && m_view.play_after->isChecked()) { QMimeDatabase db; QMimeType mime = db.mimeTypeForFile(dest); KService::Ptr serv = KMimeTypeTrader::self()->preferredService(mime.name()); if (serv) { KIO::DesktopExecParser parser(*serv, QList() << QUrl::fromLocalFile(QUrl::toPercentEncoding(dest))); render_process_args << parser.resultingArguments().join(QLatin1Char(' ')); } else { // no service found to play MIME type // TODO: inform user // errorMessage(PlaybackError, i18n("No service found to play %1", mime.name())); render_process_args << QStringLiteral("-"); } } else { render_process_args << QStringLiteral("-"); } if (m_view.speed->isEnabled()) { renderArgs.append(QChar(' ') + item->data(0, SpeedsRole).toStringList().at(m_view.speed->value())); } // Project metadata if (m_view.export_meta->isChecked()) { QMap::const_iterator i = metadata.constBegin(); while (i != metadata.constEnd()) { renderArgs.append(QStringLiteral(" %1=%2").arg(i.key(), QString(QUrl::toPercentEncoding(i.value())))); ++i; } } // Adjust frame scale int width; int height; if (m_view.rescale->isChecked() && m_view.rescale->isEnabled()) { width = m_view.rescale_width->value(); height = m_view.rescale_height->value(); } else { width = profile->width(); height = profile->height(); } // Adjust scanning if (m_view.scanning_list->currentIndex() == 1) { renderArgs.append(QStringLiteral(" progressive=1")); } else if (m_view.scanning_list->currentIndex() == 2) { renderArgs.append(QStringLiteral(" progressive=0")); } // disable audio if requested if (!exportAudio) { renderArgs.append(QStringLiteral(" an=1 ")); } int threadCount = QThread::idealThreadCount(); if (threadCount > 2 && m_view.parallel_process->isChecked()) { threadCount = qMin(threadCount - 1, 4); } else { threadCount = 1; } // Set the thread counts if (!renderArgs.contains(QStringLiteral("threads="))) { renderArgs.append(QStringLiteral(" threads=%1").arg(KdenliveSettings::encodethreads())); } renderArgs.append(QStringLiteral(" real_time=-%1").arg(threadCount)); // Check if the rendering profile is different from project profile, // in which case we need to use the producer_consumer from MLT QString subsize; if (std.startsWith(QLatin1String("s="))) { subsize = std.section(QLatin1Char(' '), 0, 0).toLower(); subsize = subsize.section(QLatin1Char('='), 1, 1); } else if (std.contains(QStringLiteral(" s="))) { subsize = std.section(QStringLiteral(" s="), 1, 1); subsize = subsize.section(QLatin1Char(' '), 0, 0).toLower(); } else if (m_view.rescale->isChecked() && m_view.rescale->isEnabled()) { subsize = QStringLiteral(" s=%1x%2").arg(width).arg(height); // Add current size parameter renderArgs.append(subsize); } // Check if we need to embed the playlist into the producer consumer // That is required if PAR != 1 if (profile->sample_aspect_num() != profile->sample_aspect_den() && subsize.isEmpty()) { resizeProfile = true; } QStringList paramsList = renderArgs.split(' ', QString::SkipEmptyParts); for (int i = 0; i < paramsList.count(); ++i) { QString paramName = paramsList.at(i).section(QLatin1Char('='), 0, -2); QString paramValue = paramsList.at(i).section(QLatin1Char('='), -1); // If the profiles do not match we need to use the consumer tag if (paramName == QLatin1String("mlt_profile") && paramValue != profile->path()) { resizeProfile = true; } // evaluate expression if (paramValue.startsWith(QLatin1Char('%'))) { if (paramValue.startsWith(QStringLiteral("%bitrate")) || paramValue == QStringLiteral("%quality")) { if (paramValue.contains("+'k'")) paramValue = QString::number(m_view.video->value()) + 'k'; else paramValue = QString::number(m_view.video->value()); } if (paramValue.startsWith(QStringLiteral("%audiobitrate")) || paramValue == QStringLiteral("%audioquality")) { if (paramValue.contains("+'k'")) paramValue = QString::number(m_view.audio->value()) + 'k'; else paramValue = QString::number(m_view.audio->value()); } if (paramValue == QStringLiteral("%dar")) paramValue = '@' + QString::number(profile->display_aspect_num()) + QLatin1Char('/') + QString::number(profile->display_aspect_den()); if (paramValue == QStringLiteral("%passes")) paramValue = QString::number(static_cast(m_view.checkTwoPass->isChecked()) + 1); paramsList[i] = paramName + QLatin1Char('=') + paramValue; } } /*if (resizeProfile && !KdenliveSettings::gpu_accel()) { render_process_args << "consumer:" + (scriptExport ? ScriptGetVar("SOURCE_" + QString::number(stemIdx)) : QUrl::fromLocalFile(playlistPaths.at(stemIdx)).toEncoded()); } else { render_process_args << (scriptExport ? ScriptGetVar("SOURCE_" + QString::number(stemIdx)) : QUrl::fromLocalFile(playlistPaths.at(stemIdx)).toEncoded()); } render_process_args << (scriptExport ? ScriptGetVar("TARGET_" + QString::number(stemIdx)) : QUrl::fromLocalFile(dest).toEncoded());*/ if (KdenliveSettings::gpu_accel()) { render_process_args << QStringLiteral("glsl.=1"); } render_process_args << paramsList; if (scriptExport) { QTextStream outStream(&file); QString stemIdxStr(QString::number(stemIdx)); /*outStream << ScriptSetVar("SOURCE_" + stemIdxStr, QUrl::fromLocalFile(playlistPaths.at(stemIdx)).toEncoded()) << '\n'; outStream << ScriptSetVar("TARGET_" + stemIdxStr, QUrl::fromLocalFile(dest).toEncoded()) << '\n'; outStream << ScriptSetVar("PARAMETERS_" + stemIdxStr, render_process_args.join(QLatin1Char(' '))) << '\n'; outStream << ScriptGetVar("RENDERER") + " " + ScriptGetVar("PARAMETERS_" + stemIdxStr) << "\n";*/ if (stemIdx == (stemCount - 1)) { if (file.error() != QFile::NoError) { KMessageBox::error(this, i18n("Cannot write to file %1", scriptPath)); file.close(); return; } file.close(); QFile::setPermissions(scriptPath, file.permissions() | QFile::ExeUser); QTimer::singleShot(400, this, &RenderWidget::parseScriptFiles); m_view.tabWidget->setCurrentIndex(2); return; } continue; } // Save rendering profile to document QMap renderProps; renderProps.insert(QStringLiteral("rendercategory"), m_view.formats->currentItem()->parent()->text(0)); renderProps.insert(QStringLiteral("renderprofile"), m_view.formats->currentItem()->text(0)); renderProps.insert(QStringLiteral("renderurl"), destBase); renderProps.insert(QStringLiteral("renderzone"), QString::number(static_cast(m_view.render_zone->isChecked()))); renderProps.insert(QStringLiteral("renderguide"), QString::number(static_cast(m_view.render_guide->isChecked()))); renderProps.insert(QStringLiteral("renderstartguide"), QString::number(m_view.guide_start->currentIndex())); renderProps.insert(QStringLiteral("renderendguide"), QString::number(m_view.guide_end->currentIndex())); renderProps.insert(QStringLiteral("renderscanning"), QString::number(m_view.scanning_list->currentIndex())); renderProps.insert(QStringLiteral("renderfield"), QString::number(m_view.field_order->currentIndex())); int export_audio = 0; if (m_view.export_audio->checkState() == Qt::Checked) { export_audio = 2; } else if (m_view.export_audio->checkState() == Qt::Unchecked) { export_audio = 1; } renderProps.insert(QStringLiteral("renderexportaudio"), QString::number(export_audio)); renderProps.insert(QStringLiteral("renderrescale"), QString::number(static_cast(m_view.rescale->isChecked()))); renderProps.insert(QStringLiteral("renderrescalewidth"), QString::number(m_view.rescale_width->value())); renderProps.insert(QStringLiteral("renderrescaleheight"), QString::number(m_view.rescale_height->value())); renderProps.insert(QStringLiteral("rendertcoverlay"), QString::number(static_cast(m_view.tc_overlay->isChecked()))); renderProps.insert(QStringLiteral("rendertctype"), QString::number(m_view.tc_type->currentIndex())); renderProps.insert(QStringLiteral("renderratio"), QString::number(static_cast(m_view.rescale_keep->isChecked()))); renderProps.insert(QStringLiteral("renderplay"), QString::number(static_cast(m_view.play_after->isChecked()))); renderProps.insert(QStringLiteral("rendertwopass"), QString::number(static_cast(m_view.checkTwoPass->isChecked()))); renderProps.insert(QStringLiteral("renderquality"), QString::number(m_view.video->value())); renderProps.insert(QStringLiteral("renderaudioquality"), QString::number(m_view.audio->value())); renderProps.insert(QStringLiteral("renderspeed"), QString::number(m_view.speed->value())); emit selectedRenderProfile(renderProps); // insert item in running jobs list RenderJobItem *renderItem = nullptr; QList existing = m_view.running_jobs->findItems(dest, Qt::MatchExactly, 1); if (!existing.isEmpty()) { renderItem = static_cast(existing.at(0)); if (renderItem->status() == RUNNINGJOB || renderItem->status() == WAITINGJOB || renderItem->status() == STARTINGJOB) { KMessageBox::information(this, i18n("There is already a job writing file:
%1
Abort the job if you want to overwrite it...", dest), i18n("Already running")); return; } /*if (renderItem->type() != DirectRenderType) { delete renderItem; renderItem = nullptr; } else { renderItem->setData(1, ProgressRole, 0); renderItem->setStatus(WAITINGJOB); renderItem->setIcon(0, QIcon::fromTheme(QStringLiteral("media-playback-pause"))); renderItem->setData(1, Qt::UserRole, i18n("Waiting...")); renderItem->setData(1, ParametersRole, dest); }*/ } if (!renderItem) { renderItem = new RenderJobItem(m_view.running_jobs, QStringList() << QString() << dest); } renderItem->setData(1, TimeRole, QDateTime::currentDateTime()); // Set rendering type /*if (group == QLatin1String("dvd")) { if (m_view.open_dvd->isChecked()) { renderItem->setData(0, Qt::UserRole, group); if (renderArgs.contains(QStringLiteral("mlt_profile="))) { //TODO: probably not valid anymore (no more MLT profiles in args) // rendering profile contains an MLT profile, so pass it to the running jog item, useful for dvd QString prof = renderArgs.section(QStringLiteral("mlt_profile="), 1, 1); prof = prof.section(QLatin1Char(' '), 0, 0); qCDebug(KDENLIVE_LOG) << "// render profile: " << prof; renderItem->setMetadata(prof); } } } else { if (group == QLatin1String("websites") && m_view.open_browser->isChecked()) { renderItem->setData(0, Qt::UserRole, group); // pass the url QString url = m_view.formats->currentItem()->data(ExtraRole).toString(); renderItem->setMetadata(url); } }*/ renderItem->setData(1, ParametersRole, render_process_args); if (!exportAudio) { renderItem->setData(1, ExtraInfoRole, i18n("Video without audio track")); } else { renderItem->setData(1, ExtraInfoRole, QString()); } m_view.running_jobs->setCurrentItem(renderItem); m_view.tabWidget->setCurrentIndex(1); // check render status checkRenderStatus(); } // end loop } void RenderWidget::checkRenderStatus() { // check if we have a job waiting to render if (m_blockProcessing) { return; } RenderJobItem *item = static_cast(m_view.running_jobs->topLevelItem(0)); // Make sure no other rendering is running while (item != nullptr) { if (item->status() == RUNNINGJOB) { return; } item = static_cast(m_view.running_jobs->itemBelow(item)); } item = static_cast(m_view.running_jobs->topLevelItem(0)); bool waitingJob = false; // Find first waiting job while (item != nullptr) { if (item->status() == WAITINGJOB) { item->setData(1, TimeRole, QDateTime::currentDateTime()); waitingJob = true; startRendering(item); // Check for 2 pass encoding QStringList jobData = item->data(1, ParametersRole).toStringList(); if (jobData.size() > 2 && jobData.at(1).endsWith(QStringLiteral("-pass2.mlt"))) { // Find and remove 1st pass job QTreeWidgetItem *above = m_view.running_jobs->itemAbove(item); QString firstPassName = jobData.at(1).section(QLatin1Char('-'), 0, -2) + QStringLiteral(".mlt"); while (above) { QStringList aboveData = above->data(1, ParametersRole).toStringList(); qDebug() << "// GOT JOB: " << aboveData.at(1); if (aboveData.size() > 2 && aboveData.at(1) == firstPassName) { delete above; break; } above = m_view.running_jobs->itemAbove(above); } } item->setStatus(STARTINGJOB); break; } item = static_cast(m_view.running_jobs->itemBelow(item)); } if (!waitingJob && m_view.shutdown->isChecked()) { emit shutdown(); } } void RenderWidget::startRendering(RenderJobItem *item) { auto rendererArgs = item->data(1, ParametersRole).toStringList(); qDebug() << "starting kdenlive_render process using: " << m_renderer; if (!QProcess::startDetached(m_renderer, rendererArgs)) { item->setStatus(FAILEDJOB); } else { KNotification::event(QStringLiteral("RenderStarted"), i18n("Rendering %1 started", item->text(1)), QPixmap(), this); } } int RenderWidget::waitingJobsCount() const { int count = 0; RenderJobItem *item = static_cast(m_view.running_jobs->topLevelItem(0)); while (item != nullptr) { if (item->status() == WAITINGJOB || item->status() == STARTINGJOB) { count++; } item = static_cast(m_view.running_jobs->itemBelow(item)); } return count; } void RenderWidget::adjustViewToProfile() { m_view.scanning_list->setCurrentIndex(0); m_view.rescale_width->setValue(KdenliveSettings::defaultrescalewidth()); if (!m_view.rescale_keep->isChecked()) { m_view.rescale_height->blockSignals(true); m_view.rescale_height->setValue(KdenliveSettings::defaultrescaleheight()); m_view.rescale_height->blockSignals(false); } refreshView(); } void RenderWidget::refreshView() { m_view.formats->blockSignals(true); QIcon brokenIcon = QIcon::fromTheme(QStringLiteral("dialog-close")); QIcon warningIcon = QIcon::fromTheme(QStringLiteral("dialog-warning")); KColorScheme scheme(palette().currentColorGroup(), KColorScheme::Window); const QColor disabled = scheme.foreground(KColorScheme::InactiveText).color(); const QColor disabledbg = scheme.background(KColorScheme::NegativeBackground).color(); // We borrow a reference to the profile's pointer to query it more easily std::unique_ptr &profile = pCore->getCurrentProfile(); double project_framerate = (double)profile->frame_rate_num() / profile->frame_rate_den(); for (int i = 0; i < m_view.formats->topLevelItemCount(); ++i) { QTreeWidgetItem *group = m_view.formats->topLevelItem(i); for (int j = 0; j < group->childCount(); ++j) { QTreeWidgetItem *item = group->child(j); QString std = item->data(0, StandardRole).toString(); if (std.isEmpty() || (std.contains(QStringLiteral("PAL"), Qt::CaseInsensitive) && profile->frame_rate_num() == 25 && profile->frame_rate_den() == 1) || (std.contains(QStringLiteral("NTSC"), Qt::CaseInsensitive) && profile->frame_rate_num() == 30000 && profile->frame_rate_den() == 1001)) { // Standard OK } else { item->setData(0, ErrorRole, i18n("Standard (%1) not compatible with project profile (%2)", std, project_framerate)); item->setIcon(0, brokenIcon); item->setForeground(0, disabled); continue; } QString params = item->data(0, ParamsRole).toString(); // Make sure the selected profile uses the same frame rate as project profile if (params.contains(QStringLiteral("mlt_profile="))) { QString profile_str = params.section(QStringLiteral("mlt_profile="), 1, 1).section(QLatin1Char(' '), 0, 0); std::unique_ptr &target_profile = ProfileRepository::get()->getProfile(profile_str); if (target_profile->frame_rate_den() > 0) { double profile_rate = (double)target_profile->frame_rate_num() / target_profile->frame_rate_den(); if ((int)(1000.0 * profile_rate) != (int)(1000.0 * project_framerate)) { item->setData(0, ErrorRole, i18n("Frame rate (%1) not compatible with project profile (%2)", profile_rate, project_framerate)); item->setIcon(0, brokenIcon); item->setForeground(0, disabled); continue; } } } // Make sure the selected profile uses an installed avformat codec / format if (!supportedFormats.isEmpty()) { QString format; if (params.startsWith(QLatin1String("f="))) { format = params.section(QStringLiteral("f="), 1, 1); } else if (params.contains(QStringLiteral(" f="))) { format = params.section(QStringLiteral(" f="), 1, 1); } if (!format.isEmpty()) { format = format.section(QLatin1Char(' '), 0, 0).toLower(); if (!supportedFormats.contains(format)) { item->setData(0, ErrorRole, i18n("Unsupported video format: %1", format)); item->setIcon(0, brokenIcon); item->setForeground(0, disabled); continue; } } } if (!acodecsList.isEmpty()) { QString format; if (params.startsWith(QLatin1String("acodec="))) { format = params.section(QStringLiteral("acodec="), 1, 1); } else if (params.contains(QStringLiteral(" acodec="))) { format = params.section(QStringLiteral(" acodec="), 1, 1); } if (!format.isEmpty()) { format = format.section(QLatin1Char(' '), 0, 0).toLower(); if (!acodecsList.contains(format)) { item->setData(0, ErrorRole, i18n("Unsupported audio codec: %1", format)); item->setIcon(0, brokenIcon); item->setForeground(0, disabled); item->setBackground(0, disabledbg); } } } if (!vcodecsList.isEmpty()) { QString format; if (params.startsWith(QLatin1String("vcodec="))) { format = params.section(QStringLiteral("vcodec="), 1, 1); } else if (params.contains(QStringLiteral(" vcodec="))) { format = params.section(QStringLiteral(" vcodec="), 1, 1); } if (!format.isEmpty()) { format = format.section(QLatin1Char(' '), 0, 0).toLower(); if (!vcodecsList.contains(format)) { item->setData(0, ErrorRole, i18n("Unsupported video codec: %1", format)); item->setIcon(0, brokenIcon); item->setForeground(0, disabled); continue; } } } if (params.contains(QStringLiteral(" profile=")) || params.startsWith(QLatin1String("profile="))) { // changed in MLT commit d8a3a5c9190646aae72048f71a39ee7446a3bd45 // (http://www.mltframework.org/gitweb/mlt.git?p=mltframework.org/mlt.git;a=commit;h=d8a3a5c9190646aae72048f71a39ee7446a3bd45) item->setData(0, ErrorRole, i18n("This render profile uses a 'profile' parameter.
Unless you know what you are doing you will probably " "have to change it to 'mlt_profile'.")); item->setIcon(0, warningIcon); continue; } } } focusFirstVisibleItem(); m_view.formats->blockSignals(false); refreshParams(); } QUrl RenderWidget::filenameWithExtension(QUrl url, const QString &extension) { if (!url.isValid()) { url = QUrl::fromLocalFile(pCore->currentDoc()->projectDataFolder() + QDir::separator()); } QString directory = url.adjusted(QUrl::RemoveFilename).toLocalFile(); QString filename = url.fileName(); QString ext; if (extension.at(0) == '.') { ext = extension; } else { ext = '.' + extension; } if (filename.isEmpty()) { filename = i18n("untitled"); } int pos = filename.lastIndexOf('.'); if (pos == 0) { filename.append(ext); } else { if (!filename.endsWith(ext, Qt::CaseInsensitive)) { filename = filename.left(pos) + ext; } } return QUrl::fromLocalFile(directory + filename); } void RenderWidget::refreshParams() { // Format not available (e.g. codec not installed); Disable start button QTreeWidgetItem *item = m_view.formats->currentItem(); if ((item == nullptr) || item->parent() == nullptr) { // This is a category item, not a real profile m_view.buttonBox->setEnabled(false); } else { m_view.buttonBox->setEnabled(true); } QString extension; if (item) { extension = item->data(0, ExtensionRole).toString(); } if ((item == nullptr) || item->isHidden() || extension.isEmpty()) { if (!item) { errorMessage(ProfileError, i18n("No matching profile")); } else if (!item->parent()) // category ; else if (extension.isEmpty()) { errorMessage(ProfileError, i18n("Invalid profile")); } m_view.advanced_params->clear(); m_view.buttonRender->setEnabled(false); m_view.buttonGenerateScript->setEnabled(false); return; } QString params = item->data(0, ParamsRole).toString(); errorMessage(ProfileError, item->data(0, ErrorRole).toString()); m_view.advanced_params->setPlainText(params); if (params.contains(QStringLiteral(" s=")) || params.startsWith(QLatin1String("s=")) || params.contains(QLatin1String("%dv_standard"))) { // profile has a fixed size, do not allow resize m_view.rescale->setEnabled(false); setRescaleEnabled(false); } else { m_view.rescale->setEnabled(true); setRescaleEnabled(m_view.rescale->isChecked()); } QUrl url = filenameWithExtension(m_view.out_file->url(), extension); m_view.out_file->setUrl(url); // if (!url.isEmpty()) { // QString path = url.path(); // int pos = path.lastIndexOf('.') + 1; // if (pos == 0) path.append('.' + extension); // else path = path.left(pos) + extension; // m_view.out_file->setUrl(QUrl(path)); // } else { // m_view.out_file->setUrl(QUrl(QDir::homePath() + QStringLiteral("/untitled.") + extension)); // } m_view.out_file->setFilter("*." + extension); QString edit = item->data(0, EditableRole).toString(); if (edit.isEmpty() || !edit.endsWith(QLatin1String("customprofiles.xml"))) { m_view.buttonDelete->setEnabled(false); m_view.buttonEdit->setEnabled(false); } else { m_view.buttonDelete->setEnabled(true); m_view.buttonEdit->setEnabled(true); } // video quality control m_view.video->blockSignals(true); bool quality = false; if ((params.contains(QStringLiteral("%quality")) || params.contains(QStringLiteral("%bitrate"))) && item->data(0, BitratesRole).canConvert(QVariant::StringList)) { // bitrates or quantizers list QStringList qs = item->data(0, BitratesRole).toStringList(); if (qs.count() > 1) { quality = true; int qmax = qs.constFirst().toInt(); int qmin = qs.last().toInt(); if (qmax < qmin) { // always show best quality on right m_view.video->setRange(qmax, qmin); m_view.video->setProperty("decreasing", true); } else { m_view.video->setRange(qmin, qmax); m_view.video->setProperty("decreasing", false); } } } m_view.video->setEnabled(quality); m_view.quality->setEnabled(quality); m_view.qualityLabel->setEnabled(quality); m_view.video->blockSignals(false); // audio quality control quality = false; m_view.audio->blockSignals(true); if ((params.contains(QStringLiteral("%audioquality")) || params.contains(QStringLiteral("%audiobitrate"))) && item->data(0, AudioBitratesRole).canConvert(QVariant::StringList)) { // bitrates or quantizers list QStringList qs = item->data(0, AudioBitratesRole).toStringList(); if (qs.count() > 1) { quality = true; int qmax = qs.constFirst().toInt(); int qmin = qs.last().toInt(); if (qmax < qmin) { m_view.audio->setRange(qmax, qmin); m_view.audio->setProperty("decreasing", true); } else { m_view.audio->setRange(qmin, qmax); m_view.audio->setProperty("decreasing", false); } if (params.contains(QStringLiteral("%audiobitrate"))) { m_view.audio->setSingleStep(32); // 32kbps step } else { m_view.audio->setSingleStep(1); } } } m_view.audio->setEnabled(quality); m_view.audio->blockSignals(false); if (m_view.quality->isEnabled()) { adjustAVQualities(m_view.quality->value()); } if (item->data(0, SpeedsRole).canConvert(QVariant::StringList) && (item->data(0, SpeedsRole).toStringList().count() != 0)) { int speed = item->data(0, SpeedsRole).toStringList().count() - 1; m_view.speed->setEnabled(true); m_view.speed->setMaximum(speed); m_view.speed->setValue(speed * 3 / 4); // default to intermediate speed } else { m_view.speed->setEnabled(false); } if (!item->data(0, FieldRole).isNull()) { m_view.field_order->setCurrentIndex(item->data(0, FieldRole).toInt()); } adjustSpeed(m_view.speed->value()); bool passes = params.contains(QStringLiteral("passes")); m_view.checkTwoPass->setEnabled(passes); m_view.checkTwoPass->setChecked(passes && params.contains(QStringLiteral("passes=2"))); m_view.encoder_threads->setEnabled(!params.contains(QStringLiteral("threads="))); m_view.buttonRender->setEnabled(m_view.formats->currentItem()->data(0, ErrorRole).isNull()); m_view.buttonGenerateScript->setEnabled(m_view.formats->currentItem()->data(0, ErrorRole).isNull()); } void RenderWidget::reloadProfiles() { parseProfiles(); } void RenderWidget::parseProfiles(const QString &selectedProfile) { m_view.formats->clear(); // Parse our xml profile QString exportFile = QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("export/profiles.xml")); parseFile(exportFile, false); // Parse some MLT's profiles parseMltPresets(); QString exportFolder = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/export/"); QDir directory(exportFolder); QStringList filter; filter << QStringLiteral("*.xml"); QStringList fileList = directory.entryList(filter, QDir::Files); // We should parse customprofiles.xml in last position, so that user profiles // can also override profiles installed by KNewStuff fileList.removeAll(QStringLiteral("customprofiles.xml")); for (const QString &filename : fileList) { parseFile(directory.absoluteFilePath(filename), true); } if (QFile::exists(exportFolder + QStringLiteral("customprofiles.xml"))) { parseFile(exportFolder + QStringLiteral("customprofiles.xml"), true); } focusFirstVisibleItem(selectedProfile); } void RenderWidget::parseMltPresets() { QDir root(KdenliveSettings::mltpath()); if (!root.cd(QStringLiteral("../presets/consumer/avformat"))) { // Cannot find MLT's presets directory qCWarning(KDENLIVE_LOG) << " / / / WARNING, cannot find MLT's preset folder"; return; } if (root.cd(QStringLiteral("lossless"))) { QString groupName = i18n("Lossless/HQ"); QList foundGroup = m_view.formats->findItems(groupName, Qt::MatchExactly); QTreeWidgetItem *groupItem; if (!foundGroup.isEmpty()) { groupItem = foundGroup.takeFirst(); } else { groupItem = new QTreeWidgetItem(QStringList(groupName)); m_view.formats->addTopLevelItem(groupItem); groupItem->setExpanded(true); } const QStringList profiles = root.entryList(QDir::Files, QDir::Name); for (const QString &prof : profiles) { KConfig config(root.absoluteFilePath(prof), KConfig::SimpleConfig); KConfigGroup group = config.group(QByteArray()); QString vcodec = group.readEntry("vcodec"); QString acodec = group.readEntry("acodec"); QString extension = group.readEntry("meta.preset.extension"); QString note = group.readEntry("meta.preset.note"); QString profileName = prof; if (!vcodec.isEmpty() || !acodec.isEmpty()) { profileName.append(" ("); if (!vcodec.isEmpty()) { profileName.append(vcodec); if (!acodec.isEmpty()) { profileName.append("+" + acodec); } } else if (!acodec.isEmpty()) { profileName.append(acodec); } profileName.append(QLatin1Char(')')); } QTreeWidgetItem *item = new QTreeWidgetItem(QStringList(profileName)); item->setData(0, ExtensionRole, extension); item->setData(0, RenderRole, "avformat"); item->setData(0, ParamsRole, QString("properties=lossless/" + prof)); if (!note.isEmpty()) { item->setToolTip(0, note); } groupItem->addChild(item); } } if (root.cd(QStringLiteral("../stills"))) { QString groupName = i18nc("Category Name", "Images sequence"); QList foundGroup = m_view.formats->findItems(groupName, Qt::MatchExactly); QTreeWidgetItem *groupItem; if (!foundGroup.isEmpty()) { groupItem = foundGroup.takeFirst(); } else { groupItem = new QTreeWidgetItem(QStringList(groupName)); m_view.formats->addTopLevelItem(groupItem); groupItem->setExpanded(true); } QStringList profiles = root.entryList(QDir::Files, QDir::Name); for (const QString &prof : profiles) { QTreeWidgetItem *item = loadFromMltPreset(groupName, root.absoluteFilePath(prof), prof); if (!item) { continue; } item->setData(0, ParamsRole, QString("properties=stills/" + prof)); groupItem->addChild(item); } // Add GIF as image sequence root.cdUp(); QTreeWidgetItem *item = loadFromMltPreset(groupName, root.absoluteFilePath(QStringLiteral("GIF")), QStringLiteral("GIF")); if (item) { item->setData(0, ParamsRole, QStringLiteral("properties=GIF")); groupItem->addChild(item); } } } QTreeWidgetItem *RenderWidget::loadFromMltPreset(const QString &groupName, const QString &path, const QString &profileName) { KConfig config(path, KConfig::SimpleConfig); KConfigGroup group = config.group(QByteArray()); QString extension = group.readEntry("meta.preset.extension"); QString note = group.readEntry("meta.preset.note"); if (extension.isEmpty()) { return nullptr; } QTreeWidgetItem *item = new QTreeWidgetItem(QStringList(profileName)); item->setData(0, GroupRole, groupName); item->setData(0, ExtensionRole, extension); item->setData(0, RenderRole, "avformat"); if (!note.isEmpty()) { item->setToolTip(0, note); } return item; } void RenderWidget::parseFile(const QString &exportFile, bool editable) { QDomDocument doc; QFile file(exportFile); doc.setContent(&file, false); file.close(); QDomElement documentElement; QDomElement profileElement; QString extension; QDomNodeList groups = doc.elementsByTagName(QStringLiteral("group")); QTreeWidgetItem *item = nullptr; bool replaceVorbisCodec = false; if (acodecsList.contains(QStringLiteral("libvorbis"))) { replaceVorbisCodec = true; } bool replaceLibfaacCodec = false; if (acodecsList.contains(QStringLiteral("libfaac"))) { replaceLibfaacCodec = true; } if (editable || groups.isEmpty()) { QDomElement profiles = doc.documentElement(); if (editable && profiles.attribute(QStringLiteral("version"), nullptr).toInt() < 1) { // this is an old profile version, update it QDomDocument newdoc; QDomElement newprofiles = newdoc.createElement(QStringLiteral("profiles")); newprofiles.setAttribute(QStringLiteral("version"), 1); newdoc.appendChild(newprofiles); QDomNodeList profilelist = doc.elementsByTagName(QStringLiteral("profile")); for (int i = 0; i < profilelist.count(); ++i) { QString category = i18nc("Category Name", "Custom"); QString ext; QDomNode parent = profilelist.at(i).parentNode(); if (!parent.isNull()) { QDomElement parentNode = parent.toElement(); if (parentNode.hasAttribute(QStringLiteral("name"))) { category = parentNode.attribute(QStringLiteral("name")); } ext = parentNode.attribute(QStringLiteral("extension")); } if (!profilelist.at(i).toElement().hasAttribute(QStringLiteral("category"))) { profilelist.at(i).toElement().setAttribute(QStringLiteral("category"), category); } if (!ext.isEmpty()) { profilelist.at(i).toElement().setAttribute(QStringLiteral("extension"), ext); } QDomNode n = profilelist.at(i).cloneNode(); newprofiles.appendChild(newdoc.importNode(n, true)); } if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { KMessageBox::sorry(this, i18n("Unable to write to file %1", exportFile)); return; } QTextStream out(&file); out << newdoc.toString(); file.close(); parseFile(exportFile, editable); return; } QDomNode node = doc.elementsByTagName(QStringLiteral("profile")).at(0); if (node.isNull()) { return; } int count = 1; while (!node.isNull()) { QDomElement profile = node.toElement(); QString profileName = profile.attribute(QStringLiteral("name")); QString standard = profile.attribute(QStringLiteral("standard")); QTextDocument docConvert; docConvert.setHtml(profile.attribute(QStringLiteral("args"))); QString params = docConvert.toPlainText().simplified(); if (replaceVorbisCodec && params.contains(QStringLiteral("acodec=vorbis"))) { // replace vorbis with libvorbis params = params.replace(QLatin1String("=vorbis"), QLatin1String("=libvorbis")); } if (replaceLibfaacCodec && params.contains(QStringLiteral("acodec=aac"))) { // replace libfaac with aac params = params.replace(QLatin1String("aac"), QLatin1String("libfaac")); } QString prof_extension = profile.attribute(QStringLiteral("extension")); if (!prof_extension.isEmpty()) { extension = prof_extension; } QString groupName = profile.attribute(QStringLiteral("category"), i18nc("Category Name", "Custom")); QList foundGroup = m_view.formats->findItems(groupName, Qt::MatchExactly); QTreeWidgetItem *groupItem; if (!foundGroup.isEmpty()) { groupItem = foundGroup.takeFirst(); } else { groupItem = new QTreeWidgetItem(QStringList(groupName)); if (editable) { m_view.formats->insertTopLevelItem(0, groupItem); } else { m_view.formats->addTopLevelItem(groupItem); groupItem->setExpanded(true); } } // Check if item with same name already exists and replace it, // allowing to override default profiles QTreeWidgetItem *childitem = nullptr; for (int j = 0; j < groupItem->childCount(); ++j) { if (groupItem->child(j)->text(0) == profileName) { childitem = groupItem->child(j); break; } } if (!childitem) { childitem = new QTreeWidgetItem(QStringList(profileName)); } childitem->setData(0, GroupRole, groupName); childitem->setData(0, ExtensionRole, extension); childitem->setData(0, RenderRole, "avformat"); childitem->setData(0, StandardRole, standard); childitem->setData(0, ParamsRole, params); if (params.contains(QLatin1String("%quality"))) { childitem->setData(0, BitratesRole, profile.attribute(QStringLiteral("qualities")).split(QLatin1Char(','), QString::SkipEmptyParts)); } else if (params.contains(QLatin1String("%bitrate"))) { childitem->setData(0, BitratesRole, profile.attribute(QStringLiteral("bitrates")).split(QLatin1Char(','), QString::SkipEmptyParts)); } if (params.contains(QLatin1String("%audioquality"))) { childitem->setData(0, AudioBitratesRole, profile.attribute(QStringLiteral("audioqualities")).split(QLatin1Char(','), QString::SkipEmptyParts)); } else if (params.contains(QLatin1String("%audiobitrate"))) { childitem->setData(0, AudioBitratesRole, profile.attribute(QStringLiteral("audiobitrates")).split(QLatin1Char(','), QString::SkipEmptyParts)); } if (profile.hasAttribute(QStringLiteral("speeds"))) { childitem->setData(0, SpeedsRole, profile.attribute(QStringLiteral("speeds")).split(QLatin1Char(';'), QString::SkipEmptyParts)); } if (profile.hasAttribute(QStringLiteral("url"))) { childitem->setData(0, ExtraRole, profile.attribute(QStringLiteral("url"))); } if (profile.hasAttribute(QStringLiteral("top_field_first"))) { childitem->setData(0, FieldRole, profile.attribute(QStringLiteral("top_field_first"))); } if (editable) { childitem->setData(0, EditableRole, exportFile); if (exportFile.endsWith(QLatin1String("customprofiles.xml"))) { childitem->setIcon(0, QIcon::fromTheme(QStringLiteral("favorite"))); } else { childitem->setIcon(0, QIcon::fromTheme(QStringLiteral("applications-internet"))); } } groupItem->addChild(childitem); node = doc.elementsByTagName(QStringLiteral("profile")).at(count); count++; } return; } int i = 0; QString groupName; QString profileName; QString prof_extension; QString renderer; QString params; QString standard; while (!groups.item(i).isNull()) { documentElement = groups.item(i).toElement(); QDomNode gname = documentElement.elementsByTagName(QStringLiteral("groupname")).at(0); groupName = documentElement.attribute(QStringLiteral("name"), i18nc("Attribute Name", "Custom")); extension = documentElement.attribute(QStringLiteral("extension"), QString()); renderer = documentElement.attribute(QStringLiteral("renderer"), QString()); QList foundGroup = m_view.formats->findItems(groupName, Qt::MatchExactly); QTreeWidgetItem *groupItem; if (!foundGroup.isEmpty()) { groupItem = foundGroup.takeFirst(); } else { groupItem = new QTreeWidgetItem(QStringList(groupName)); m_view.formats->addTopLevelItem(groupItem); groupItem->setExpanded(true); } QDomNode n = groups.item(i).firstChild(); while (!n.isNull()) { if (n.toElement().tagName() != QLatin1String("profile")) { n = n.nextSibling(); continue; } profileElement = n.toElement(); profileName = profileElement.attribute(QStringLiteral("name")); standard = profileElement.attribute(QStringLiteral("standard")); params = profileElement.attribute(QStringLiteral("args")).simplified(); if (replaceVorbisCodec && params.contains(QStringLiteral("acodec=vorbis"))) { // replace vorbis with libvorbis params = params.replace(QLatin1String("=vorbis"), QLatin1String("=libvorbis")); } if (replaceLibfaacCodec && params.contains(QStringLiteral("acodec=aac"))) { // replace libfaac with aac params = params.replace(QLatin1String("aac"), QLatin1String("libfaac")); } prof_extension = profileElement.attribute(QStringLiteral("extension")); if (!prof_extension.isEmpty()) { extension = prof_extension; } item = new QTreeWidgetItem(QStringList(profileName)); item->setData(0, GroupRole, groupName); item->setData(0, ExtensionRole, extension); item->setData(0, RenderRole, renderer); item->setData(0, StandardRole, standard); item->setData(0, ParamsRole, params); if (params.contains(QLatin1String("%quality"))) { item->setData(0, BitratesRole, profileElement.attribute(QStringLiteral("qualities")).split(QLatin1Char(','), QString::SkipEmptyParts)); } else if (params.contains(QLatin1String("%bitrate"))) { item->setData(0, BitratesRole, profileElement.attribute(QStringLiteral("bitrates")).split(QLatin1Char(','), QString::SkipEmptyParts)); } if (params.contains(QLatin1String("%audioquality"))) { item->setData(0, AudioBitratesRole, profileElement.attribute(QStringLiteral("audioqualities")).split(QLatin1Char(','), QString::SkipEmptyParts)); } else if (params.contains(QLatin1String("%audiobitrate"))) { item->setData(0, AudioBitratesRole, profileElement.attribute(QStringLiteral("audiobitrates")).split(QLatin1Char(','), QString::SkipEmptyParts)); } if (profileElement.hasAttribute(QStringLiteral("speeds"))) { item->setData(0, SpeedsRole, profileElement.attribute(QStringLiteral("speeds")).split(QLatin1Char(';'), QString::SkipEmptyParts)); } if (profileElement.hasAttribute(QStringLiteral("url"))) { item->setData(0, ExtraRole, profileElement.attribute(QStringLiteral("url"))); } groupItem->addChild(item); n = n.nextSibling(); } ++i; } } void RenderWidget::setRenderJob(const QString &dest, int progress) { RenderJobItem *item; QList existing = m_view.running_jobs->findItems(dest, Qt::MatchExactly, 1); if (!existing.isEmpty()) { item = static_cast(existing.at(0)); } else { item = new RenderJobItem(m_view.running_jobs, QStringList() << QString() << dest); if (progress == 0) { item->setStatus(WAITINGJOB); } } item->setData(1, ProgressRole, progress); item->setStatus(RUNNINGJOB); if (progress == 0) { item->setIcon(0, QIcon::fromTheme(QStringLiteral("media-record"))); item->setData(1, TimeRole, QDateTime::currentDateTime()); slotCheckJob(); } else { QDateTime startTime = item->data(1, TimeRole).toDateTime(); qint64 elapsedTime = startTime.secsTo(QDateTime::currentDateTime()); qint64 remaining = elapsedTime * (100 - progress) / progress; int days = static_cast(remaining / 86400); int remainingSecs = static_cast(remaining % 86400); QTime when = QTime(0, 0, 0, 0); when = when.addSecs(remainingSecs); QString est = (days > 0) ? i18np("%1 day ", "%1 days ", days) : QString(); est.append(when.toString(QStringLiteral("hh:mm:ss"))); QString t = i18n("Remaining time %1", est); item->setData(1, Qt::UserRole, t); } } void RenderWidget::setRenderStatus(const QString &dest, int status, const QString &error) { RenderJobItem *item; QList existing = m_view.running_jobs->findItems(dest, Qt::MatchExactly, 1); if (!existing.isEmpty()) { item = static_cast(existing.at(0)); } else { item = new RenderJobItem(m_view.running_jobs, QStringList() << QString() << dest); } if (!item) { return; } if (status == -1) { // Job finished successfully item->setStatus(FINISHEDJOB); QDateTime startTime = item->data(1, TimeRole).toDateTime(); qint64 elapsedTime = startTime.secsTo(QDateTime::currentDateTime()); int days = static_cast(elapsedTime / 86400); int secs = static_cast(elapsedTime % 86400); QTime when = QTime(0, 0, 0, 0); when = when.addSecs(secs); QString est = (days > 0) ? i18np("%1 day ", "%1 days ", days) : QString(); est.append(when.toString(QStringLiteral("hh:mm:ss"))); QString t = i18n("Rendering finished in %1", est); item->setData(1, Qt::UserRole, t); #ifdef KF5_USE_PURPOSE m_shareMenu->model()->setInputData(QJsonObject{{QStringLiteral("mimeType"), QMimeDatabase().mimeTypeForFile(item->text(1)).name()}, {QStringLiteral("urls"), QJsonArray({item->text(1)})}}); m_shareMenu->model()->setPluginType(QStringLiteral("Export")); m_shareMenu->reload(); #endif QString notif = i18n("Rendering of %1 finished in %2", item->text(1), est); KNotification *notify = new KNotification(QStringLiteral("RenderFinished")); notify->setText(notif); #if KNOTIFICATIONS_VERSION >= QT_VERSION_CHECK(5, 29, 0) notify->setUrls({QUrl::fromLocalFile(dest)}); #endif notify->sendEvent(); QString itemGroup = item->data(0, Qt::UserRole).toString(); if (itemGroup == QLatin1String("dvd")) { emit openDvdWizard(item->text(1)); } else if (itemGroup == QLatin1String("websites")) { QString url = item->metadata(); if (!url.isEmpty()) { new KRun(QUrl::fromLocalFile(url), this); } } } else if (status == -2) { // Rendering crashed item->setStatus(FAILEDJOB); m_view.error_log->append(i18n("Rendering of %1 crashed
", dest)); m_view.error_log->append(error); m_view.error_log->append(QStringLiteral("
")); m_view.error_box->setVisible(true); } else if (status == -3) { // User aborted job item->setStatus(ABORTEDJOB); } else { delete item; } slotCheckJob(); checkRenderStatus(); } void RenderWidget::slotAbortCurrentJob() { RenderJobItem *current = static_cast(m_view.running_jobs->currentItem()); if (current) { if (current->status() == RUNNINGJOB) { emit abortProcess(current->text(1)); } else { delete current; slotCheckJob(); checkRenderStatus(); } } } void RenderWidget::slotStartCurrentJob() { RenderJobItem *current = static_cast(m_view.running_jobs->currentItem()); if ((current != nullptr) && current->status() == WAITINGJOB) { startRendering(current); } m_view.start_job->setEnabled(false); } void RenderWidget::slotCheckJob() { bool activate = false; RenderJobItem *current = static_cast(m_view.running_jobs->currentItem()); if (current) { if (current->status() == RUNNINGJOB || current->status() == STARTINGJOB) { m_view.abort_job->setText(i18n("Abort Job")); m_view.start_job->setEnabled(false); } else { m_view.abort_job->setText(i18n("Remove Job")); m_view.start_job->setEnabled(current->status() == WAITINGJOB); } activate = true; #ifdef KF5_USE_PURPOSE if (current->status() == FINISHEDJOB) { m_shareMenu->model()->setInputData(QJsonObject{{QStringLiteral("mimeType"), QMimeDatabase().mimeTypeForFile(current->text(1)).name()}, {QStringLiteral("urls"), QJsonArray({current->text(1)})}}); m_shareMenu->model()->setPluginType(QStringLiteral("Export")); m_shareMenu->reload(); m_view.shareButton->setEnabled(true); } else { m_view.shareButton->setEnabled(false); } #endif } m_view.abort_job->setEnabled(activate); /* for (int i = 0; i < m_view.running_jobs->topLevelItemCount(); ++i) { current = static_cast(m_view.running_jobs->topLevelItem(i)); if (current == static_cast (m_view.running_jobs->currentItem())) { current->setSizeHint(1, QSize(m_view.running_jobs->columnWidth(1), fontMetrics().height() * 3)); } else current->setSizeHint(1, QSize(m_view.running_jobs->columnWidth(1), fontMetrics().height() * 2)); }*/ } void RenderWidget::slotCLeanUpJobs() { int ix = 0; RenderJobItem *current = static_cast(m_view.running_jobs->topLevelItem(ix)); while (current != nullptr) { if (current->status() == FINISHEDJOB || current->status() == ABORTEDJOB) { delete current; } else { ix++; } current = static_cast(m_view.running_jobs->topLevelItem(ix)); } slotCheckJob(); } void RenderWidget::parseScriptFiles() { QStringList scriptsFilter; scriptsFilter << QStringLiteral("*.mlt"); m_view.scripts_list->clear(); QTreeWidgetItem *item; // List the project scripts QDir projectFolder(pCore->currentDoc()->projectDataFolder()); projectFolder.mkpath(QStringLiteral("kdenlive-renderqueue")); projectFolder.cd(QStringLiteral("kdenlive-renderqueue")); QStringList scriptFiles = projectFolder.entryList(scriptsFilter, QDir::Files); for (int i = 0; i < scriptFiles.size(); ++i) { QUrl scriptpath = QUrl::fromLocalFile(projectFolder.absoluteFilePath(scriptFiles.at(i))); QFile f(scriptpath.toLocalFile()); QDomDocument doc; doc.setContent(&f, false); f.close(); QDomElement consumer = doc.documentElement().firstChildElement(QStringLiteral("consumer")); if (consumer.isNull()) { continue; } QString target = consumer.attribute(QStringLiteral("target")); if (target.isEmpty()) { continue; } item = new QTreeWidgetItem(m_view.scripts_list, QStringList() << QString() << scriptpath.fileName()); auto icon = QFileIconProvider().icon(QFileInfo(f)); item->setIcon(0, icon.isNull() ? QIcon::fromTheme(QStringLiteral("application-x-executable-script")) : icon); item->setSizeHint(0, QSize(m_view.scripts_list->columnWidth(0), fontMetrics().height() * 2)); item->setData(1, Qt::UserRole, QUrl(QUrl::fromEncoded(target.toUtf8())).url(QUrl::PreferLocalFile)); item->setData(1, Qt::UserRole + 1, scriptpath.toLocalFile()); } QTreeWidgetItem *script = m_view.scripts_list->topLevelItem(0); if (script) { m_view.scripts_list->setCurrentItem(script); script->setSelected(true); } } void RenderWidget::slotCheckScript() { QTreeWidgetItem *current = m_view.scripts_list->currentItem(); if (current == nullptr) { return; } m_view.start_script->setEnabled(current->data(0, Qt::UserRole).toString().isEmpty()); m_view.delete_script->setEnabled(true); for (int i = 0; i < m_view.scripts_list->topLevelItemCount(); ++i) { current = m_view.scripts_list->topLevelItem(i); if (current == m_view.scripts_list->currentItem()) { current->setSizeHint(1, QSize(m_view.scripts_list->columnWidth(1), fontMetrics().height() * 3)); } else { current->setSizeHint(1, QSize(m_view.scripts_list->columnWidth(1), fontMetrics().height() * 2)); } } } void RenderWidget::slotStartScript() { RenderJobItem *item = static_cast(m_view.scripts_list->currentItem()); if (item) { QString destination = item->data(1, Qt::UserRole).toString(); if (QFile::exists(destination)) { if (KMessageBox::warningYesNo(this, i18n("Output file already exists. Do you want to overwrite it?")) != KMessageBox::Yes) { return; } } QString path = item->data(1, Qt::UserRole + 1).toString(); // Insert new job in queue RenderJobItem *renderItem = nullptr; QList existing = m_view.running_jobs->findItems(destination, Qt::MatchExactly, 1); if (!existing.isEmpty()) { renderItem = static_cast(existing.at(0)); if (renderItem->status() == RUNNINGJOB || renderItem->status() == WAITINGJOB || renderItem->status() == STARTINGJOB) { KMessageBox::information( this, i18n("There is already a job writing file:
%1
Abort the job if you want to overwrite it...", destination), i18n("Already running")); return; } delete renderItem; renderItem = nullptr; } if (!renderItem) { renderItem = new RenderJobItem(m_view.running_jobs, QStringList() << QString() << destination); } renderItem->setData(1, ProgressRole, 0); renderItem->setStatus(WAITINGJOB); renderItem->setIcon(0, QIcon::fromTheme(QStringLiteral("media-playback-pause"))); renderItem->setData(1, Qt::UserRole, i18n("Waiting...")); renderItem->setData(1, TimeRole, QDateTime::currentDateTime()); QStringList argsJob = {KdenliveSettings::rendererpath(), path, destination, QStringLiteral("-pid:%1").arg(QCoreApplication::applicationPid())}; renderItem->setData(1, ParametersRole, argsJob); checkRenderStatus(); m_view.tabWidget->setCurrentIndex(1); } } void RenderWidget::slotDeleteScript() { QTreeWidgetItem *item = m_view.scripts_list->currentItem(); if (item) { QString path = item->data(1, Qt::UserRole + 1).toString(); bool success = true; success &= static_cast(QFile::remove(path)); if (!success) { qCWarning(KDENLIVE_LOG) << "// Error removing script or playlist: " << path << ", " << path << ".mlt"; } parseScriptFiles(); } } void RenderWidget::slotGenerateScript() { slotPrepareExport(true); } void RenderWidget::slotHideLog() { m_view.error_box->setVisible(false); } void RenderWidget::setRenderProfile(const QMap &props) { m_view.scanning_list->setCurrentIndex(props.value(QStringLiteral("renderscanning")).toInt()); m_view.field_order->setCurrentIndex(props.value(QStringLiteral("renderfield")).toInt()); int exportAudio = props.value(QStringLiteral("renderexportaudio")).toInt(); switch (exportAudio) { case 1: m_view.export_audio->setCheckState(Qt::Unchecked); break; case 2: m_view.export_audio->setCheckState(Qt::Checked); break; default: m_view.export_audio->setCheckState(Qt::PartiallyChecked); } if (props.contains(QStringLiteral("renderrescale"))) { m_view.rescale->setChecked(props.value(QStringLiteral("renderrescale")).toInt() != 0); } if (props.contains(QStringLiteral("renderrescalewidth"))) { m_view.rescale_width->setValue(props.value(QStringLiteral("renderrescalewidth")).toInt()); } if (props.contains(QStringLiteral("renderrescaleheight"))) { m_view.rescale_height->setValue(props.value(QStringLiteral("renderrescaleheight")).toInt()); } if (props.contains(QStringLiteral("rendertcoverlay"))) { m_view.tc_overlay->setChecked(props.value(QStringLiteral("rendertcoverlay")).toInt() != 0); } if (props.contains(QStringLiteral("rendertctype"))) { m_view.tc_type->setCurrentIndex(props.value(QStringLiteral("rendertctype")).toInt()); } if (props.contains(QStringLiteral("renderratio"))) { m_view.rescale_keep->setChecked(props.value(QStringLiteral("renderratio")).toInt() != 0); } if (props.contains(QStringLiteral("renderplay"))) { m_view.play_after->setChecked(props.value(QStringLiteral("renderplay")).toInt() != 0); } if (props.contains(QStringLiteral("rendertwopass"))) { m_view.checkTwoPass->setChecked(props.value(QStringLiteral("rendertwopass")).toInt() != 0); } if (props.value(QStringLiteral("renderzone")) == QLatin1String("1")) { m_view.render_zone->setChecked(true); } else if (props.value(QStringLiteral("renderguide")) == QLatin1String("1")) { m_view.render_guide->setChecked(true); m_view.guide_start->setCurrentIndex(props.value(QStringLiteral("renderstartguide")).toInt()); m_view.guide_end->setCurrentIndex(props.value(QStringLiteral("renderendguide")).toInt()); } else { m_view.render_full->setChecked(true); } slotUpdateGuideBox(); QString url = props.value(QStringLiteral("renderurl")); if (!url.isEmpty()) { m_view.out_file->setUrl(QUrl::fromLocalFile(url)); } if (props.contains(QStringLiteral("renderprofile")) || props.contains(QStringLiteral("rendercategory"))) { focusFirstVisibleItem(props.value(QStringLiteral("renderprofile"))); } if (props.contains(QStringLiteral("renderquality"))) { m_view.video->setValue(props.value(QStringLiteral("renderquality")).toInt()); } else if (props.contains(QStringLiteral("renderbitrate"))) { m_view.video->setValue(props.value(QStringLiteral("renderbitrate")).toInt()); } else { m_view.quality->setValue(m_view.quality->maximum() * 3 / 4); } if (props.contains(QStringLiteral("renderaudioquality"))) { m_view.audio->setValue(props.value(QStringLiteral("renderaudioquality")).toInt()); } if (props.contains(QStringLiteral("renderaudiobitrate"))) { m_view.audio->setValue(props.value(QStringLiteral("renderaudiobitrate")).toInt()); } if (props.contains(QStringLiteral("renderspeed"))) { m_view.speed->setValue(props.value(QStringLiteral("renderspeed")).toInt()); } } bool RenderWidget::startWaitingRenderJobs() { m_blockProcessing = true; #ifdef Q_OS_WIN const QLatin1String ScriptFormat(".bat"); #else const QLatin1String ScriptFormat(".sh"); #endif QTemporaryFile tmp(QDir::tempPath() + QStringLiteral("/kdenlive-XXXXXX") + ScriptFormat); if (!tmp.open()) { // Something went wrong return false; } tmp.close(); QString autoscriptFile = tmp.fileName(); QFile file(autoscriptFile); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { qCWarning(KDENLIVE_LOG) << "////// ERROR writing to file: " << autoscriptFile; KMessageBox::error(nullptr, i18n("Cannot write to file %1", autoscriptFile)); return false; } QTextStream outStream(&file); #ifndef Q_OS_WIN outStream << "#! /bin/sh" << '\n' << '\n'; #endif RenderJobItem *item = static_cast(m_view.running_jobs->topLevelItem(0)); while (item != nullptr) { if (item->status() == WAITINGJOB) { // Add render process for item const QString params = item->data(1, ParametersRole).toStringList().join(QLatin1Char(' ')); outStream << '\"' << m_renderer << "\" " << params << '\n'; } item = static_cast(m_view.running_jobs->itemBelow(item)); } // erase itself when rendering is finished #ifndef Q_OS_WIN outStream << "rm \"" << autoscriptFile << "\"\n"; #else outStream << "del \"" << autoscriptFile << "\"\n"; #endif if (file.error() != QFile::NoError) { KMessageBox::error(nullptr, i18n("Cannot write to file %1", autoscriptFile)); file.close(); m_blockProcessing = false; return false; } file.close(); QFile::setPermissions(autoscriptFile, file.permissions() | QFile::ExeUser); QProcess::startDetached(autoscriptFile, QStringList()); return true; } void RenderWidget::slotPlayRendering(QTreeWidgetItem *item, int) { RenderJobItem *renderItem = static_cast(item); if (renderItem->status() != FINISHEDJOB) { return; } new KRun(QUrl::fromLocalFile(item->text(1)), this); } void RenderWidget::errorMessage(RenderError type, const QString &message) { QString fullMessage; m_errorMessages.insert(type, message); QMapIterator i(m_errorMessages); while (i.hasNext()) { i.next(); if (!i.value().isEmpty()) { if (!fullMessage.isEmpty()) { fullMessage.append(QLatin1Char('\n')); } fullMessage.append(i.value()); } } if (!fullMessage.isEmpty()) { m_infoMessage->setMessageType(KMessageWidget::Warning); m_infoMessage->setText(fullMessage); m_infoMessage->show(); } else { m_infoMessage->hide(); } } void RenderWidget::slotUpdateEncodeThreads(int val) { KdenliveSettings::setEncodethreads(val); } void RenderWidget::slotUpdateRescaleWidth(int val) { KdenliveSettings::setDefaultrescalewidth(val); if (!m_view.rescale_keep->isChecked()) { return; } m_view.rescale_height->blockSignals(true); std::unique_ptr &profile = pCore->getCurrentProfile(); m_view.rescale_height->setValue(val * profile->height() / profile->width()); KdenliveSettings::setDefaultrescaleheight(m_view.rescale_height->value()); m_view.rescale_height->blockSignals(false); } void RenderWidget::slotUpdateRescaleHeight(int val) { KdenliveSettings::setDefaultrescaleheight(val); if (!m_view.rescale_keep->isChecked()) { return; } m_view.rescale_width->blockSignals(true); std::unique_ptr &profile = pCore->getCurrentProfile(); m_view.rescale_width->setValue(val * profile->width() / profile->height()); KdenliveSettings::setDefaultrescaleheight(m_view.rescale_width->value()); m_view.rescale_width->blockSignals(false); } void RenderWidget::slotSwitchAspectRatio() { KdenliveSettings::setRescalekeepratio(m_view.rescale_keep->isChecked()); if (m_view.rescale_keep->isChecked()) { slotUpdateRescaleWidth(m_view.rescale_width->value()); } } void RenderWidget::slotUpdateAudioLabel(int ix) { if (ix == Qt::PartiallyChecked) { m_view.export_audio->setText(i18n("Export audio (automatic)")); } else { m_view.export_audio->setText(i18n("Export audio")); } m_view.stemAudioExport->setEnabled(ix != Qt::Unchecked); } bool RenderWidget::automaticAudioExport() const { return (m_view.export_audio->checkState() == Qt::PartiallyChecked); } bool RenderWidget::selectedAudioExport() const { return (m_view.export_audio->checkState() != Qt::Unchecked); } void RenderWidget::updateProxyConfig(bool enable) { m_view.proxy_render->setHidden(!enable); } bool RenderWidget::proxyRendering() { return m_view.proxy_render->isChecked(); } bool RenderWidget::isStemAudioExportEnabled() const { return (m_view.stemAudioExport->isChecked() && m_view.stemAudioExport->isVisible() && m_view.stemAudioExport->isEnabled()); } void RenderWidget::setRescaleEnabled(bool enable) { for (int i = 0; i < m_view.rescale_box->layout()->count(); ++i) { if (m_view.rescale_box->itemAt(i)->widget()) { m_view.rescale_box->itemAt(i)->widget()->setEnabled(enable); } } } void RenderWidget::keyPressEvent(QKeyEvent *e) { if (e->key() == Qt::Key_Return || e->key() == Qt::Key_Enter) { switch (m_view.tabWidget->currentIndex()) { case 1: if (m_view.start_job->isEnabled()) { slotStartCurrentJob(); } break; case 2: if (m_view.start_script->isEnabled()) { slotStartScript(); } break; default: if (m_view.buttonRender->isEnabled()) { slotPrepareExport(); } break; } } else { QDialog::keyPressEvent(e); } } void RenderWidget::adjustAVQualities(int quality) { // calculate video/audio quality indexes from the general quality cursor // taking into account decreasing/increasing video/audio quality parameter double q = (double)quality / m_view.quality->maximum(); int dq = q * (m_view.video->maximum() - m_view.video->minimum()); // prevent video spinbox to update quality cursor (loop) m_view.video->blockSignals(true); m_view.video->setValue(m_view.video->property("decreasing").toBool() ? m_view.video->maximum() - dq : m_view.video->minimum() + dq); m_view.video->blockSignals(false); dq = q * (m_view.audio->maximum() - m_view.audio->minimum()); dq -= dq % m_view.audio->singleStep(); // keep a 32 pitch for bitrates m_view.audio->setValue(m_view.audio->property("decreasing").toBool() ? m_view.audio->maximum() - dq : m_view.audio->minimum() + dq); } void RenderWidget::adjustQuality(int videoQuality) { int dq = videoQuality * m_view.quality->maximum() / (m_view.video->maximum() - m_view.video->minimum()); m_view.quality->blockSignals(true); m_view.quality->setValue(m_view.video->property("decreasing").toBool() ? m_view.video->maximum() - dq : m_view.video->minimum() + dq); m_view.quality->blockSignals(false); } void RenderWidget::adjustSpeed(int speedIndex) { if (m_view.formats->currentItem()) { QStringList speeds = m_view.formats->currentItem()->data(0, SpeedsRole).toStringList(); if (speedIndex < speeds.count()) { m_view.speed->setToolTip(i18n("Codec speed parameters:\n") + speeds.at(speedIndex)); } } } void RenderWidget::checkCodecs() { Mlt::Profile p; auto *consumer = new Mlt::Consumer(p, "avformat"); if (consumer) { consumer->set("vcodec", "list"); consumer->set("acodec", "list"); consumer->set("f", "list"); consumer->start(); vcodecsList.clear(); Mlt::Properties vcodecs((mlt_properties)consumer->get_data("vcodec")); vcodecsList.reserve(vcodecs.count()); for (int i = 0; i < vcodecs.count(); ++i) { vcodecsList << QString(vcodecs.get(i)); } acodecsList.clear(); Mlt::Properties acodecs((mlt_properties)consumer->get_data("acodec")); acodecsList.reserve(acodecs.count()); for (int i = 0; i < acodecs.count(); ++i) { acodecsList << QString(acodecs.get(i)); } supportedFormats.clear(); Mlt::Properties formats((mlt_properties)consumer->get_data("f")); supportedFormats.reserve(formats.count()); for (int i = 0; i < formats.count(); ++i) { supportedFormats << QString(formats.get(i)); } delete consumer; } } void RenderWidget::slotProxyWarn(bool enableProxy) { errorMessage(ProxyWarning, enableProxy ? i18n("Rendering using low quality proxy") : QString()); } diff --git a/src/doc/kdenlivedoc.cpp b/src/doc/kdenlivedoc.cpp index 4bf836ae6..200c5092b 100644 --- a/src/doc/kdenlivedoc.cpp +++ b/src/doc/kdenlivedoc.cpp @@ -1,1713 +1,1713 @@ /*************************************************************************** * Copyright (C) 2007 by Jean-Baptiste Mardelle (jb@kdenlive.org) * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "kdenlivedoc.h" #include "bin/bin.h" #include "bin/bincommands.h" #include "bin/binplaylist.hpp" #include "bin/clipcreator.hpp" #include "bin/model/markerlistmodel.hpp" #include "bin/projectclip.h" #include "bin/projectitemmodel.h" #include "core.h" #include "dialogs/profilesdialog.h" #include "documentchecker.h" #include "documentvalidator.h" #include "docundostack.hpp" #include "effects/effectsrepository.hpp" #include "jobs/jobmanager.h" #include "kdenlivesettings.h" #include "mainwindow.h" #include "mltcontroller/clipcontroller.h" #include "profiles/profilemodel.hpp" #include "profiles/profilerepository.hpp" #include "project/projectcommands.h" #include "titler/titlewidget.h" #include "transitions/transitionsrepository.hpp" #include #include #include #include #include #include #include #include #include "kdenlive_debug.h" #include #include #include #include #include #include #include #include #include #include #include #ifdef Q_OS_MAC #include #endif const double DOCUMENTVERSION = 0.98; KdenliveDoc::KdenliveDoc(const QUrl &url, const QString &projectFolder, QUndoGroup *undoGroup, const QString &profileName, const QMap &properties, const QMap &metadata, const QPoint &tracks, bool *openBackup, MainWindow *parent) : QObject(parent) , m_autosave(nullptr) , m_url(url) , m_commandStack(std::make_shared(undoGroup)) , m_modified(false) , m_documentOpenStatus(CleanProject) , m_projectFolder(projectFolder) { m_guideModel.reset(new MarkerListModel(m_commandStack, this)); connect(m_guideModel.get(), &MarkerListModel::modelChanged, this, &KdenliveDoc::guidesChanged); connect(this, SIGNAL(updateCompositionMode(int)), parent, SLOT(slotUpdateCompositeAction(int))); bool success = false; connect(m_commandStack.get(), &QUndoStack::indexChanged, this, &KdenliveDoc::slotModified); connect(m_commandStack.get(), &DocUndoStack::invalidate, this, &KdenliveDoc::checkPreviewStack); // connect(m_commandStack, SIGNAL(cleanChanged(bool)), this, SLOT(setModified(bool))); // init default document properties m_documentProperties[QStringLiteral("zoom")] = QLatin1Char('8'); m_documentProperties[QStringLiteral("verticalzoom")] = QLatin1Char('1'); m_documentProperties[QStringLiteral("zonein")] = QLatin1Char('0'); m_documentProperties[QStringLiteral("zoneout")] = QStringLiteral("-1"); m_documentProperties[QStringLiteral("enableproxy")] = QString::number((int)KdenliveSettings::enableproxy()); m_documentProperties[QStringLiteral("proxyparams")] = KdenliveSettings::proxyparams(); m_documentProperties[QStringLiteral("proxyextension")] = KdenliveSettings::proxyextension(); m_documentProperties[QStringLiteral("previewparameters")] = KdenliveSettings::previewparams(); m_documentProperties[QStringLiteral("previewextension")] = KdenliveSettings::previewextension(); m_documentProperties[QStringLiteral("externalproxyparams")] = KdenliveSettings::externalProxyProfile(); m_documentProperties[QStringLiteral("enableexternalproxy")] = QString::number((int)KdenliveSettings::externalproxy()); m_documentProperties[QStringLiteral("generateproxy")] = QString::number((int)KdenliveSettings::generateproxy()); m_documentProperties[QStringLiteral("proxyminsize")] = QString::number(KdenliveSettings::proxyminsize()); m_documentProperties[QStringLiteral("generateimageproxy")] = QString::number((int)KdenliveSettings::generateimageproxy()); m_documentProperties[QStringLiteral("proxyimageminsize")] = QString::number(KdenliveSettings::proxyimageminsize()); m_documentProperties[QStringLiteral("proxyimagesize")] = QString::number(KdenliveSettings::proxyimagesize()); m_documentProperties[QStringLiteral("videoTarget")] = QString::number(tracks.y()); m_documentProperties[QStringLiteral("audioTarget")] = QString::number(tracks.y() - 1); m_documentProperties[QStringLiteral("activeTrack")] = QString::number(tracks.y()); m_documentProperties[QStringLiteral("enableTimelineZone")] = QLatin1Char('0'); // Load properties QMapIterator i(properties); while (i.hasNext()) { i.next(); m_documentProperties[i.key()] = i.value(); } // Load metadata QMapIterator j(metadata); while (j.hasNext()) { j.next(); m_documentMetadata[j.key()] = j.value(); } /*if (QLocale().decimalPoint() != QLocale::system().decimalPoint()) { qDebug()<<"* * ** AARCH DOCUMENT PROBLEM;"; exit(1); setlocale(LC_NUMERIC, ""); QLocale systemLocale = QLocale::system(); systemLocale.setNumberOptions(QLocale::OmitGroupSeparator); QLocale::setDefault(systemLocale); // locale conversion might need to be redone ///TODO: how to reset repositories... //EffectsRepository::get()->init(); //TransitionsRepository::get()->init(); //initEffects::parseEffectFiles(pCore->getMltRepository(), QString::fromLatin1(setlocale(LC_NUMERIC, nullptr))); }*/ *openBackup = false; if (url.isValid()) { QFile file(url.toLocalFile()); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { // The file cannot be opened if (KMessageBox::warningContinueCancel(parent, i18n("Cannot open the project file,\nDo you want to open a backup file?"), i18n("Error opening file"), KGuiItem(i18n("Open Backup"))) == KMessageBox::Continue) { *openBackup = true; } // KMessageBox::error(parent, KIO::NetAccess::lastErrorString()); } else { qCDebug(KDENLIVE_LOG) << " // / processing file open"; QString errorMsg; int line; int col; QDomImplementation::setInvalidDataPolicy(QDomImplementation::DropInvalidChars); success = m_document.setContent(&file, false, &errorMsg, &line, &col); file.close(); if (!success) { // It is corrupted int answer = KMessageBox::warningYesNoCancel( parent, i18n("Cannot open the project file, error is:\n%1 (line %2, col %3)\nDo you want to open a backup file?", errorMsg, line, col), i18n("Error opening file"), KGuiItem(i18n("Open Backup")), KGuiItem(i18n("Recover"))); if (answer == KMessageBox::Yes) { *openBackup = true; } else if (answer == KMessageBox::No) { // Try to recover broken file produced by Kdenlive 0.9.4 if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { int correction = 0; QString playlist = QString::fromUtf8(file.readAll()); while (!success && correction < 2) { int errorPos = 0; line--; col = col - 2; for (int k = 0; k < line && errorPos < playlist.length(); ++k) { errorPos = playlist.indexOf(QLatin1Char('\n'), errorPos); errorPos++; } errorPos += col; if (errorPos >= playlist.length()) { break; } playlist.remove(errorPos, 1); line = 0; col = 0; success = m_document.setContent(playlist, false, &errorMsg, &line, &col); correction++; } if (!success) { KMessageBox::sorry(parent, i18n("Cannot recover this project file")); } else { // Document was modified, ask for backup QDomElement mlt = m_document.documentElement(); mlt.setAttribute(QStringLiteral("modified"), 1); } } } } else { qCDebug(KDENLIVE_LOG) << " // / processing file open: validate"; parent->slotGotProgressInfo(i18n("Validating"), 100); qApp->processEvents(); DocumentValidator validator(m_document, url); success = validator.isProject(); if (!success) { // It is not a project file parent->slotGotProgressInfo(i18n("File %1 is not a Kdenlive project file", m_url.toLocalFile()), 100); if (KMessageBox::warningContinueCancel( parent, i18n("File %1 is not a valid project file.\nDo you want to open a backup file?", m_url.toLocalFile()), i18n("Error opening file"), KGuiItem(i18n("Open Backup"))) == KMessageBox::Continue) { *openBackup = true; } } else { /* * Validate the file against the current version (upgrade * and recover it if needed). It is NOT a passive operation */ // TODO: backup the document or alert the user? success = validator.validate(DOCUMENTVERSION); if (success && !KdenliveSettings::gpu_accel()) { success = validator.checkMovit(); } if (success) { // Let the validator handle error messages qCDebug(KDENLIVE_LOG) << " // / processing file validate ok"; pCore->displayMessage(i18n("Check missing clips"), InformationMessage, 300); qApp->processEvents(); DocumentChecker d(m_url, m_document); success = !d.hasErrorInClips(); if (success) { loadDocumentProperties(); if (m_document.documentElement().hasAttribute(QStringLiteral("upgraded"))) { m_documentOpenStatus = UpgradedProject; pCore->displayMessage(i18n("Your project was upgraded, a backup will be created on next save"), ErrorMessage); } else if (m_document.documentElement().hasAttribute(QStringLiteral("modified")) || validator.isModified()) { m_documentOpenStatus = ModifiedProject; pCore->displayMessage(i18n("Your project was modified on opening, a backup will be created on next save"), ErrorMessage); setModified(true); } pCore->displayMessage(QString(), OperationCompletedMessage); } } } } } } // Something went wrong, or a new file was requested: create a new project if (!success) { m_url.clear(); pCore->setCurrentProfile(profileName); m_document = createEmptyDocument(tracks.x(), tracks.y()); updateProjectProfile(false); } if (!m_projectFolder.isEmpty()) { // Ask to create the project directory if it does not exist QDir folder(m_projectFolder); if (!folder.mkpath(QStringLiteral("."))) { // Project folder is not writable m_projectFolder = m_url.toString(QUrl::RemoveFilename | QUrl::RemoveScheme); folder.setPath(m_projectFolder); if (folder.exists()) { KMessageBox::sorry( parent, i18n("The project directory %1, could not be created.\nPlease make sure you have the required permissions.\nDefaulting to system folders", m_projectFolder)); } else { KMessageBox::information(parent, i18n("Document project folder is invalid, using system default folders")); } m_projectFolder.clear(); } } initCacheDirs(); updateProjectFolderPlacesEntry(); } KdenliveDoc::~KdenliveDoc() { if (m_url.isEmpty()) { // Document was never saved, delete cache folder QString documentId = QDir::cleanPath(getDocumentProperty(QStringLiteral("documentid"))); bool ok; documentId.toLongLong(&ok, 10); if (ok && !documentId.isEmpty()) { QDir baseCache = getCacheDir(CacheBase, &ok); if (baseCache.dirName() == documentId && baseCache.entryList(QDir::Files).isEmpty()) { baseCache.removeRecursively(); } } } // qCDebug(KDENLIVE_LOG) << "// DEL CLP MAN"; // Clean up guide model m_guideModel.reset(); // qCDebug(KDENLIVE_LOG) << "// DEL CLP MAN done"; if (m_autosave) { if (!m_autosave->fileName().isEmpty()) { m_autosave->remove(); } delete m_autosave; } } const QByteArray KdenliveDoc::getProjectXml() { return m_document.toString().toUtf8(); } QDomDocument KdenliveDoc::createEmptyDocument(int videotracks, int audiotracks) { QList tracks; // Tracks are added «backwards», so we need to reverse the track numbering // mbt 331: http://www.kdenlive.org/mantis/view.php?id=331 // Better default names for tracks: Audio 1 etc. instead of blank numbers tracks.reserve(audiotracks + videotracks); for (int i = 0; i < audiotracks; ++i) { TrackInfo audioTrack; audioTrack.type = AudioTrack; audioTrack.isMute = false; audioTrack.isBlind = true; audioTrack.isLocked = false; // audioTrack.trackName = i18n("Audio %1", audiotracks - i); audioTrack.duration = 0; tracks.append(audioTrack); } for (int i = 0; i < videotracks; ++i) { TrackInfo videoTrack; videoTrack.type = VideoTrack; videoTrack.isMute = false; videoTrack.isBlind = false; videoTrack.isLocked = false; // videoTrack.trackName = i18n("Video %1", i + 1); videoTrack.duration = 0; tracks.append(videoTrack); } return createEmptyDocument(tracks); } QDomDocument KdenliveDoc::createEmptyDocument(const QList &tracks) { // Creating new document QDomDocument doc; Mlt::Profile docProfile; Mlt::Consumer xmlConsumer(docProfile, "xml:kdenlive_playlist"); xmlConsumer.set("no_profile", 1); xmlConsumer.set("terminate_on_pause", 1); xmlConsumer.set("store", "kdenlive"); Mlt::Tractor tractor(docProfile); Mlt::Producer bk(docProfile, "color:black"); tractor.insert_track(bk, 0); for (int i = 0; i < tracks.count(); ++i) { Mlt::Tractor track(docProfile); track.set("kdenlive:track_name", tracks.at(i).trackName.toUtf8().constData()); track.set("kdenlive:trackheight", KdenliveSettings::trackheight()); if (tracks.at(i).type == AudioTrack) { track.set("kdenlive:audio_track", 1); } if (tracks.at(i).isLocked) { track.set("kdenlive:locked_track", 1); } if (tracks.at(i).isMute) { if (tracks.at(i).isBlind) { track.set("hide", 3); } else { track.set("hide", 2); } } else if (tracks.at(i).isBlind) { track.set("hide", 1); } Mlt::Playlist playlist1(docProfile); Mlt::Playlist playlist2(docProfile); track.insert_track(playlist1, 0); track.insert_track(playlist2, 1); tractor.insert_track(track, i + 1); } QScopedPointer field(tractor.field()); QString compositeService = TransitionsRepository::get()->getCompositingTransition(); if (!compositeService.isEmpty()) { for (int i = 0; i <= tracks.count(); i++) { if (i > 0 && tracks.at(i - 1).type == AudioTrack) { Mlt::Transition tr(docProfile, "mix"); tr.set("a_track", 0); tr.set("b_track", i); tr.set("always_active", 1); tr.set("sum", 1); tr.set("internal_added", 237); field->plant_transition(tr, 0, i); } if (i > 0 && tracks.at(i - 1).type == VideoTrack) { Mlt::Transition tr(docProfile, compositeService.toUtf8().constData()); tr.set("a_track", 0); tr.set("b_track", i); tr.set("always_active", 1); tr.set("internal_added", 237); field->plant_transition(tr, 0, i); } } } Mlt::Producer prod(tractor.get_producer()); xmlConsumer.connect(prod); xmlConsumer.run(); QString playlist = QString::fromUtf8(xmlConsumer.get("kdenlive_playlist")); doc.setContent(playlist); return doc; } bool KdenliveDoc::useProxy() const { return m_documentProperties.value(QStringLiteral("enableproxy")).toInt() != 0; } bool KdenliveDoc::useExternalProxy() const { return m_documentProperties.value(QStringLiteral("enableexternalproxy")).toInt() != 0; } bool KdenliveDoc::autoGenerateProxy(int width) const { return (m_documentProperties.value(QStringLiteral("generateproxy")).toInt() != 0) && width > m_documentProperties.value(QStringLiteral("proxyminsize")).toInt(); } bool KdenliveDoc::autoGenerateImageProxy(int width) const { return (m_documentProperties.value(QStringLiteral("generateimageproxy")).toInt() != 0) && width > m_documentProperties.value(QStringLiteral("proxyimageminsize")).toInt(); } void KdenliveDoc::slotAutoSave(const QString &scene) { if (m_autosave != nullptr) { if (!m_autosave->isOpen() && !m_autosave->open(QIODevice::ReadWrite)) { // show error: could not open the autosave file qCDebug(KDENLIVE_LOG) << "ERROR; CANNOT CREATE AUTOSAVE FILE"; } if (scene.isEmpty()) { // Make sure we don't save if scenelist is corrupted KMessageBox::error(QApplication::activeWindow(), i18n("Cannot write to file %1, scene list is corrupted.", m_autosave->fileName())); return; } m_autosave->resize(0); m_autosave->write(scene.toUtf8()); m_autosave->flush(); } } void KdenliveDoc::setZoom(int horizontal, int vertical) { m_documentProperties[QStringLiteral("zoom")] = QString::number(horizontal); if (vertical > -1) { m_documentProperties[QStringLiteral("verticalzoom")] = QString::number(vertical); } } QPoint KdenliveDoc::zoom() const { return QPoint(m_documentProperties.value(QStringLiteral("zoom")).toInt(), m_documentProperties.value(QStringLiteral("verticalzoom")).toInt()); } void KdenliveDoc::setZone(int start, int end) { m_documentProperties[QStringLiteral("zonein")] = QString::number(start); m_documentProperties[QStringLiteral("zoneout")] = QString::number(end); } QPoint KdenliveDoc::zone() const { return QPoint(m_documentProperties.value(QStringLiteral("zonein")).toInt(), m_documentProperties.value(QStringLiteral("zoneout")).toInt()); } QPair KdenliveDoc::targetTracks() const { return {m_documentProperties.value(QStringLiteral("videoTarget")).toInt(), m_documentProperties.value(QStringLiteral("audioTarget")).toInt()}; } QDomDocument KdenliveDoc::xmlSceneList(const QString &scene) { QDomDocument sceneList; sceneList.setContent(scene, true); QDomElement mlt = sceneList.firstChildElement(QStringLiteral("mlt")); if (mlt.isNull() || !mlt.hasChildNodes()) { // scenelist is corrupted return sceneList; } // Set playlist audio volume to 100% QDomElement tractor = mlt.firstChildElement(QStringLiteral("tractor")); if (!tractor.isNull()) { QDomNodeList props = tractor.elementsByTagName(QStringLiteral("property")); for (int i = 0; i < props.count(); ++i) { if (props.at(i).toElement().attribute(QStringLiteral("name")) == QLatin1String("meta.volume")) { props.at(i).firstChild().setNodeValue(QStringLiteral("1")); break; } } } QDomNodeList pls = mlt.elementsByTagName(QStringLiteral("playlist")); QDomElement mainPlaylist; for (int i = 0; i < pls.count(); ++i) { if (pls.at(i).toElement().attribute(QStringLiteral("id")) == BinPlaylist::binPlaylistId) { mainPlaylist = pls.at(i).toElement(); break; } } // check if project contains custom effects to embed them in project file QDomNodeList effects = mlt.elementsByTagName(QStringLiteral("filter")); int maxEffects = effects.count(); // qCDebug(KDENLIVE_LOG) << "// FOUD " << maxEffects << " EFFECTS+++++++++++++++++++++"; QMap effectIds; for (int i = 0; i < maxEffects; ++i) { QDomNode m = effects.at(i); QDomNodeList params = m.childNodes(); QString id; QString tag; for (int j = 0; j < params.count(); ++j) { QDomElement e = params.item(j).toElement(); if (e.attribute(QStringLiteral("name")) == QLatin1String("kdenlive_id")) { id = e.firstChild().nodeValue(); } if (e.attribute(QStringLiteral("name")) == QLatin1String("tag")) { tag = e.firstChild().nodeValue(); } if (!id.isEmpty() && !tag.isEmpty()) { effectIds.insert(id, tag); } } } // TODO: find a way to process this before rendering MLT scenelist to xml /*QDomDocument customeffects = initEffects::getUsedCustomEffects(effectIds); if (!customeffects.documentElement().childNodes().isEmpty()) { Xml::setXmlProperty(mainPlaylist, QStringLiteral("kdenlive:customeffects"), customeffects.toString()); }*/ // addedXml.appendChild(sceneList.importNode(customeffects.documentElement(), true)); // TODO: move metadata to previous step in saving process QDomElement docmetadata = sceneList.createElement(QStringLiteral("documentmetadata")); QMapIterator j(m_documentMetadata); while (j.hasNext()) { j.next(); docmetadata.setAttribute(j.key(), j.value()); } // addedXml.appendChild(docmetadata); return sceneList; } bool KdenliveDoc::saveSceneList(const QString &path, const QString &scene) { QDomDocument sceneList = xmlSceneList(scene); if (sceneList.isNull()) { // Make sure we don't save if scenelist is corrupted KMessageBox::error(QApplication::activeWindow(), i18n("Cannot write to file %1, scene list is corrupted.", path)); return false; } // Backup current version backupLastSavedVersion(path); if (m_documentOpenStatus != CleanProject) { // create visible backup file and warn user QString baseFile = path.section(QStringLiteral(".kdenlive"), 0, 0); int ct = 0; QString backupFile = baseFile + QStringLiteral("_backup") + QString::number(ct) + QStringLiteral(".kdenlive"); while (QFile::exists(backupFile)) { ct++; backupFile = baseFile + QStringLiteral("_backup") + QString::number(ct) + QStringLiteral(".kdenlive"); } QString message; if (m_documentOpenStatus == UpgradedProject) { message = i18n("Your project file was upgraded to the latest Kdenlive document version.\nTo make sure you don't lose data, a backup copy called %1 " "was created.", backupFile); } else { message = i18n("Your project file was modified by Kdenlive.\nTo make sure you don't lose data, a backup copy called %1 was created.", backupFile); } KIO::FileCopyJob *copyjob = KIO::file_copy(QUrl::fromLocalFile(path), QUrl::fromLocalFile(backupFile)); if (copyjob->exec()) { KMessageBox::information(QApplication::activeWindow(), message); m_documentOpenStatus = CleanProject; } else { KMessageBox::information( QApplication::activeWindow(), i18n("Your project file was upgraded to the latest Kdenlive document version, but it was not possible to create the backup copy %1.", backupFile)); } } QFile file(path); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { qCWarning(KDENLIVE_LOG) << "////// ERROR writing to file: " << path; KMessageBox::error(QApplication::activeWindow(), i18n("Cannot write to file %1", path)); return false; } file.write(sceneList.toString().toUtf8()); if (file.error() != QFile::NoError) { KMessageBox::error(QApplication::activeWindow(), i18n("Cannot write to file %1", path)); file.close(); return false; } file.close(); cleanupBackupFiles(); QFileInfo info(file); QString fileName = QUrl::fromLocalFile(path).fileName().section(QLatin1Char('.'), 0, -2); fileName.append(QLatin1Char('-') + m_documentProperties.value(QStringLiteral("documentid"))); fileName.append(info.lastModified().toString(QStringLiteral("-yyyy-MM-dd-hh-mm"))); fileName.append(QStringLiteral(".kdenlive.png")); QDir backupFolder(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/.backup")); emit saveTimelinePreview(backupFolder.absoluteFilePath(fileName)); return true; } QString KdenliveDoc::projectTempFolder() const { if (m_projectFolder.isEmpty()) { return QStandardPaths::writableLocation(QStandardPaths::CacheLocation); } return m_projectFolder; } QString KdenliveDoc::projectDataFolder() const { if (m_projectFolder.isEmpty()) { if (KdenliveSettings::customprojectfolder()) { return KdenliveSettings::defaultprojectfolder(); } return QStandardPaths::writableLocation(QStandardPaths::MoviesLocation); } return m_projectFolder; } void KdenliveDoc::setProjectFolder(const QUrl &url) { if (url == QUrl::fromLocalFile(m_projectFolder)) { return; } setModified(true); QDir dir(url.toLocalFile()); if (!dir.exists()) { dir.mkpath(dir.absolutePath()); } dir.mkdir(QStringLiteral("titles")); /*if (KMessageBox::questionYesNo(QApplication::activeWindow(), i18n("You have changed the project folder. Do you want to copy the cached data from %1 to the * new folder %2?", m_projectFolder, url.path())) == KMessageBox::Yes) moveProjectData(url);*/ m_projectFolder = url.toLocalFile(); updateProjectFolderPlacesEntry(); } void KdenliveDoc::moveProjectData(const QString & /*src*/, const QString &dest) { // Move proxies QList cacheUrls; auto binClips = pCore->projectItemModel()->getAllClipIds(); // First step: all clips referenced by the bin model exist and are inserted for (const auto &binClip : binClips) { auto projClip = pCore->projectItemModel()->getClipByBinID(binClip); if (projClip->clipType() == ClipType::Text) { // the image for title clip must be moved QUrl oldUrl = QUrl::fromLocalFile(projClip->clipUrl()); if (!oldUrl.isEmpty()) { QUrl newUrl = QUrl::fromLocalFile(dest + QStringLiteral("/titles/") + oldUrl.fileName()); KIO::Job *job = KIO::copy(oldUrl, newUrl); if (job->exec()) { projClip->setProducerProperty(QStringLiteral("resource"), newUrl.toLocalFile()); } } continue; } QString proxy = projClip->getProducerProperty(QStringLiteral("kdenlive:proxy")); if (proxy.length() > 2 && QFile::exists(proxy)) { QUrl pUrl = QUrl::fromLocalFile(proxy); if (!cacheUrls.contains(pUrl)) { cacheUrls << pUrl; } } } if (!cacheUrls.isEmpty()) { QDir proxyDir(dest + QStringLiteral("/proxy/")); if (proxyDir.mkpath(QStringLiteral("."))) { KIO::CopyJob *job = KIO::move(cacheUrls, QUrl::fromLocalFile(proxyDir.absolutePath())); KJobWidgets::setWindow(job, QApplication::activeWindow()); if (static_cast(job->exec()) > 0) { KMessageBox::sorry(QApplication::activeWindow(), i18n("Moving proxy clips failed: %1", job->errorText())); } } } } bool KdenliveDoc::profileChanged(const QString &profile) const { return pCore->getCurrentProfile() != ProfileRepository::get()->getProfile(profile); } Render *KdenliveDoc::renderer() { return nullptr; } std::shared_ptr KdenliveDoc::commandStack() { return m_commandStack; } int KdenliveDoc::getFramePos(const QString &duration) { return m_timecode.getFrameCount(duration); } QDomDocument KdenliveDoc::toXml() { return m_document; } Timecode KdenliveDoc::timecode() const { return m_timecode; } QDomNodeList KdenliveDoc::producersList() { return m_document.elementsByTagName(QStringLiteral("producer")); } int KdenliveDoc::width() const { return pCore->getCurrentProfile()->width(); } int KdenliveDoc::height() const { return pCore->getCurrentProfile()->height(); } QUrl KdenliveDoc::url() const { return m_url; } void KdenliveDoc::setUrl(const QUrl &url) { m_url = url; } void KdenliveDoc::slotModified() { setModified(!m_commandStack->isClean()); } void KdenliveDoc::setModified(bool mod) { // fix mantis#3160: The document may have an empty URL if not saved yet, but should have a m_autosave in any case if ((m_autosave != nullptr) && mod && KdenliveSettings::crashrecovery()) { emit startAutoSave(); } if (mod == m_modified) { return; } m_modified = mod; emit docModified(m_modified); } bool KdenliveDoc::isModified() const { return m_modified; } const QString KdenliveDoc::description() const { if (!m_url.isValid()) { return i18n("Untitled") + QStringLiteral("[*] / ") + pCore->getCurrentProfile()->description(); } return m_url.fileName() + QStringLiteral(" [*]/ ") + pCore->getCurrentProfile()->description(); } QString KdenliveDoc::searchFileRecursively(const QDir &dir, const QString &matchSize, const QString &matchHash) const { QString foundFileName; QByteArray fileData; QByteArray fileHash; QStringList filesAndDirs = dir.entryList(QDir::Files | QDir::Readable); for (int i = 0; i < filesAndDirs.size() && foundFileName.isEmpty(); ++i) { QFile file(dir.absoluteFilePath(filesAndDirs.at(i))); if (file.open(QIODevice::ReadOnly)) { if (QString::number(file.size()) == matchSize) { /* * 1 MB = 1 second per 450 files (or faster) * 10 MB = 9 seconds per 450 files (or faster) */ if (file.size() > 1000000 * 2) { fileData = file.read(1000000); if (file.seek(file.size() - 1000000)) { fileData.append(file.readAll()); } } else { fileData = file.readAll(); } file.close(); fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5); if (QString::fromLatin1(fileHash.toHex()) == matchHash) { return file.fileName(); } qCDebug(KDENLIVE_LOG) << filesAndDirs.at(i) << "size match but not hash"; } } ////qCDebug(KDENLIVE_LOG) << filesAndDirs.at(i) << file.size() << fileHash.toHex(); } filesAndDirs = dir.entryList(QDir::Dirs | QDir::Readable | QDir::Executable | QDir::NoDotAndDotDot); for (int i = 0; i < filesAndDirs.size() && foundFileName.isEmpty(); ++i) { foundFileName = searchFileRecursively(dir.absoluteFilePath(filesAndDirs.at(i)), matchSize, matchHash); if (!foundFileName.isEmpty()) { break; } } return foundFileName; } // TODO refac : delete std::shared_ptr KdenliveDoc::getBinClip(const QString &clipId) { return pCore->bin()->getBinClip(clipId); } QStringList KdenliveDoc::getBinFolderClipIds(const QString &folderId) const { return pCore->bin()->getBinFolderClipIds(folderId); } void KdenliveDoc::slotCreateTextTemplateClip(const QString &group, const QString &groupId, QUrl path) { Q_UNUSED(group) // TODO refac: this seem to be a duplicate of ClipCreationDialog::createTitleTemplateClip. See if we can merge QString titlesFolder = QDir::cleanPath(m_projectFolder + QStringLiteral("/titles/")); if (path.isEmpty()) { QPointer d = new QFileDialog(QApplication::activeWindow(), i18n("Enter Template Path"), titlesFolder); d->setMimeTypeFilters(QStringList() << QStringLiteral("application/x-kdenlivetitle")); d->setFileMode(QFileDialog::ExistingFile); if (d->exec() == QDialog::Accepted && !d->selectedUrls().isEmpty()) { path = d->selectedUrls().first(); } delete d; } if (path.isEmpty()) { return; } // TODO: rewrite with new title system (just set resource) QString id = ClipCreator::createTitleTemplate(path.toString(), QString(), i18n("Template title clip"), groupId, pCore->projectItemModel()); emit selectLastAddedClip(id); } void KdenliveDoc::cacheImage(const QString &fileId, const QImage &img) const { bool ok = false; QDir dir = getCacheDir(CacheThumbs, &ok); if (ok) { img.save(dir.absoluteFilePath(fileId + QStringLiteral(".png"))); } } void KdenliveDoc::setDocumentProperty(const QString &name, const QString &value) { if (value.isEmpty()) { m_documentProperties.remove(name); return; } m_documentProperties[name] = value; } const QString KdenliveDoc::getDocumentProperty(const QString &name, const QString &defaultValue) const { return m_documentProperties.value(name, defaultValue); } QMap KdenliveDoc::getRenderProperties() const { QMap renderProperties; QMapIterator i(m_documentProperties); while (i.hasNext()) { i.next(); if (i.key().startsWith(QLatin1String("render"))) { if (i.key() == QLatin1String("renderurl")) { // Check that we have a full path QString value = i.value(); if (QFileInfo(value).isRelative()) { value.prepend(m_documentRoot); } renderProperties.insert(i.key(), value); } else { renderProperties.insert(i.key(), i.value()); } } } return renderProperties; } void KdenliveDoc::saveCustomEffects(const QDomNodeList &customeffects) { QDomElement e; QStringList importedEffects; int maxchild = customeffects.count(); QStringList newPaths; for (int i = 0; i < maxchild; ++i) { e = customeffects.at(i).toElement(); const QString id = e.attribute(QStringLiteral("id")); const QString tag = e.attribute(QStringLiteral("tag")); if (!id.isEmpty()) { // Check if effect exists or save it if (EffectsRepository::get()->exists(id)) { QDomDocument doc; doc.appendChild(doc.importNode(e, true)); QString path = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/effects"); path += id + QStringLiteral(".xml"); if (!QFile::exists(path)) { importedEffects << id; newPaths << path; QFile file(path); if (file.open(QFile::WriteOnly | QFile::Truncate)) { QTextStream out(&file); out << doc.toString(); } } } } } if (!importedEffects.isEmpty()) { KMessageBox::informationList(QApplication::activeWindow(), i18n("The following effects were imported from the project:"), importedEffects); } if (!importedEffects.isEmpty()) { emit reloadEffects(newPaths); } } void KdenliveDoc::updateProjectFolderPlacesEntry() { /* * For similar and more code have a look at kfileplacesmodel.cpp and the included files: * http://websvn.kde.org/trunk/KDE/kdelibs/kfile/kfileplacesmodel.cpp?view=markup */ const QString file = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/user-places.xbel"); KBookmarkManager *bookmarkManager = KBookmarkManager::managerForExternalFile(file); if (!bookmarkManager) { return; } KBookmarkGroup root = bookmarkManager->root(); KBookmark bookmark = root.first(); QString kdenliveName = QCoreApplication::applicationName(); QUrl documentLocation = QUrl::fromLocalFile(m_projectFolder); bool exists = false; while (!bookmark.isNull()) { // UDI not empty indicates a device QString udi = bookmark.metaDataItem(QStringLiteral("UDI")); QString appName = bookmark.metaDataItem(QStringLiteral("OnlyInApp")); if (udi.isEmpty() && appName == kdenliveName && bookmark.text() == i18n("Project Folder")) { if (bookmark.url() != documentLocation) { bookmark.setUrl(documentLocation); bookmarkManager->emitChanged(root); } exists = true; break; } bookmark = root.next(bookmark); } // if entry does not exist yet (was not found), well, create it then if (!exists) { bookmark = root.addBookmark(i18n("Project Folder"), documentLocation, QStringLiteral("folder-favorites")); // Make this user selectable ? bookmark.setMetaDataItem(QStringLiteral("OnlyInApp"), kdenliveName); bookmarkManager->emitChanged(root); } } // static double KdenliveDoc::getDisplayRatio(const QString &path) { QFile file(path); QDomDocument doc; if (!file.open(QIODevice::ReadOnly)) { qCWarning(KDENLIVE_LOG) << "ERROR, CANNOT READ: " << path; return 0; } if (!doc.setContent(&file)) { qCWarning(KDENLIVE_LOG) << "ERROR, CANNOT READ: " << path; file.close(); return 0; } file.close(); QDomNodeList list = doc.elementsByTagName(QStringLiteral("profile")); if (list.isEmpty()) { return 0; } QDomElement profile = list.at(0).toElement(); double den = profile.attribute(QStringLiteral("display_aspect_den")).toDouble(); if (den > 0) { return profile.attribute(QStringLiteral("display_aspect_num")).toDouble() / den; } return 0; } void KdenliveDoc::backupLastSavedVersion(const QString &path) { // Ensure backup folder exists if (path.isEmpty()) { return; } QFile file(path); QDir backupFolder(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/.backup")); QString fileName = QUrl::fromLocalFile(path).fileName().section(QLatin1Char('.'), 0, -2); QFileInfo info(file); fileName.append(QLatin1Char('-') + m_documentProperties.value(QStringLiteral("documentid"))); fileName.append(info.lastModified().toString(QStringLiteral("-yyyy-MM-dd-hh-mm"))); fileName.append(QStringLiteral(".kdenlive")); QString backupFile = backupFolder.absoluteFilePath(fileName); if (file.exists()) { // delete previous backup if it was done less than 60 seconds ago QFile::remove(backupFile); if (!QFile::copy(path, backupFile)) { KMessageBox::information(QApplication::activeWindow(), i18n("Cannot create backup copy:\n%1", backupFile)); } } } void KdenliveDoc::cleanupBackupFiles() { QDir backupFolder(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/.backup")); QString projectFile = url().fileName().section(QLatin1Char('.'), 0, -2); projectFile.append(QLatin1Char('-') + m_documentProperties.value(QStringLiteral("documentid"))); projectFile.append(QStringLiteral("-??")); projectFile.append(QStringLiteral("??")); projectFile.append(QStringLiteral("-??")); projectFile.append(QStringLiteral("-??")); projectFile.append(QStringLiteral("-??")); projectFile.append(QStringLiteral("-??.kdenlive")); QStringList filter; filter << projectFile; backupFolder.setNameFilters(filter); QFileInfoList resultList = backupFolder.entryInfoList(QDir::Files, QDir::Time); QDateTime d = QDateTime::currentDateTime(); QStringList hourList; QStringList dayList; QStringList weekList; QStringList oldList; for (int i = 0; i < resultList.count(); ++i) { if (d.secsTo(resultList.at(i).lastModified()) < 3600) { // files created in the last hour hourList.append(resultList.at(i).absoluteFilePath()); } else if (d.secsTo(resultList.at(i).lastModified()) < 43200) { // files created in the day dayList.append(resultList.at(i).absoluteFilePath()); } else if (d.daysTo(resultList.at(i).lastModified()) < 8) { // files created in the week weekList.append(resultList.at(i).absoluteFilePath()); } else { // older files oldList.append(resultList.at(i).absoluteFilePath()); } } if (hourList.count() > 20) { int step = hourList.count() / 10; for (int i = 0; i < hourList.count(); i += step) { // qCDebug(KDENLIVE_LOG)<<"REMOVE AT: "< 20) { int step = dayList.count() / 10; for (int i = 0; i < dayList.count(); i += step) { dayList.removeAt(i); --i; } } else { dayList.clear(); } if (weekList.count() > 20) { int step = weekList.count() / 10; for (int i = 0; i < weekList.count(); i += step) { weekList.removeAt(i); --i; } } else { weekList.clear(); } if (oldList.count() > 20) { int step = oldList.count() / 10; for (int i = 0; i < oldList.count(); i += step) { oldList.removeAt(i); --i; } } else { oldList.clear(); } QString f; while (hourList.count() > 0) { f = hourList.takeFirst(); QFile::remove(f); QFile::remove(f + QStringLiteral(".png")); } while (dayList.count() > 0) { f = dayList.takeFirst(); QFile::remove(f); QFile::remove(f + QStringLiteral(".png")); } while (weekList.count() > 0) { f = weekList.takeFirst(); QFile::remove(f); QFile::remove(f + QStringLiteral(".png")); } while (oldList.count() > 0) { f = oldList.takeFirst(); QFile::remove(f); QFile::remove(f + QStringLiteral(".png")); } } const QMap KdenliveDoc::metadata() const { return m_documentMetadata; } void KdenliveDoc::setMetadata(const QMap &meta) { setModified(true); m_documentMetadata = meta; } void KdenliveDoc::slotProxyCurrentItem(bool doProxy, QList> clipList, bool force, QUndoCommand *masterCommand) { if (clipList.isEmpty()) { clipList = pCore->bin()->selectedClips(); } bool hasParent = true; if (masterCommand == nullptr) { masterCommand = new QUndoCommand(); if (doProxy) { masterCommand->setText(i18np("Add proxy clip", "Add proxy clips", clipList.count())); } else { masterCommand->setText(i18np("Remove proxy clip", "Remove proxy clips", clipList.count())); } hasParent = false; } // Make sure the proxy folder exists bool ok = false; QDir dir = getCacheDir(CacheProxy, &ok); if (!ok) { // Error return; } if (m_proxyExtension.isEmpty()) { initProxySettings(); } QString extension = QLatin1Char('.') + m_proxyExtension; // getDocumentProperty(QStringLiteral("proxyextension")); /*QString params = getDocumentProperty(QStringLiteral("proxyparams")); if (params.contains(QStringLiteral("-s "))) { QString proxySize = params.section(QStringLiteral("-s "), 1).section(QStringLiteral("x"), 0, 0); extension.prepend(QStringLiteral("-") + proxySize); }*/ // Prepare updated properties QMap newProps; QMap oldProps; if (!doProxy) { newProps.insert(QStringLiteral("kdenlive:proxy"), QStringLiteral("-")); } // Parse clips QStringList externalProxyParams = m_documentProperties.value(QStringLiteral("externalproxyparams")).split(QLatin1Char(';')); for (int i = 0; i < clipList.count(); ++i) { - std::shared_ptr item = clipList.at(i); + const std::shared_ptr &item = clipList.at(i); ClipType::ProducerType t = item->clipType(); // Only allow proxy on some clip types if ((t == ClipType::Video || t == ClipType::AV || t == ClipType::Unknown || t == ClipType::Image || t == ClipType::Playlist || t == ClipType::SlideShow) && item->isReady()) { if ((doProxy && !force && item->hasProxy()) || (!doProxy && !item->hasProxy() && pCore->projectItemModel()->hasClip(item->AbstractProjectItem::clipId()))) { continue; } if (doProxy) { newProps.clear(); QString path; if (useExternalProxy() && item->hasLimitedDuration()) { if (externalProxyParams.count() >= 3) { QFileInfo info(item->url()); QDir clipDir = info.absoluteDir(); if (clipDir.cd(externalProxyParams.at(0))) { // Find correct file QString fileName = info.fileName(); if (!externalProxyParams.at(1).isEmpty()) { fileName.prepend(externalProxyParams.at(1)); } if (!externalProxyParams.at(2).isEmpty()) { fileName = fileName.section(QLatin1Char('.'), 0, -2); fileName.append(externalProxyParams.at(2)); } if (clipDir.exists(fileName)) { path = clipDir.absoluteFilePath(fileName); } } } } if (path.isEmpty()) { path = dir.absoluteFilePath(item->hash() + (t == ClipType::Image ? QStringLiteral(".png") : extension)); } newProps.insert(QStringLiteral("kdenlive:proxy"), path); // We need to insert empty proxy so that undo will work // TODO: how to handle clip properties // oldProps = clip->currentProperties(newProps); oldProps.insert(QStringLiteral("kdenlive:proxy"), QStringLiteral("-")); } else { if (t == ClipType::SlideShow) { // Revert to picture aspect ratio newProps.insert(QStringLiteral("aspect_ratio"), QStringLiteral("1")); } // Reset to original url newProps.insert(QStringLiteral("resource"), item->url()); } new EditClipCommand(pCore->bin(), item->AbstractProjectItem::clipId(), oldProps, newProps, true, masterCommand); } else { // Cannot proxy this clip type pCore->bin()->doDisplayMessage(i18n("Clip type does not support proxies"), KMessageWidget::Information); } } if (!hasParent) { if (masterCommand->childCount() > 0) { m_commandStack->push(masterCommand); } else { delete masterCommand; } } } QMap KdenliveDoc::documentProperties() { m_documentProperties.insert(QStringLiteral("version"), QString::number(DOCUMENTVERSION)); m_documentProperties.insert(QStringLiteral("kdenliveversion"), QStringLiteral(KDENLIVE_VERSION)); if (!m_projectFolder.isEmpty()) { m_documentProperties.insert(QStringLiteral("storagefolder"), m_projectFolder + QLatin1Char('/') + m_documentProperties.value(QStringLiteral("documentid"))); } m_documentProperties.insert(QStringLiteral("profile"), pCore->getCurrentProfile()->path()); ; if (!m_documentProperties.contains(QStringLiteral("decimalPoint"))) { m_documentProperties.insert(QStringLiteral("decimalPoint"), QLocale().decimalPoint()); } return m_documentProperties; } void KdenliveDoc::loadDocumentProperties() { QDomNodeList list = m_document.elementsByTagName(QStringLiteral("playlist")); QDomElement baseElement = m_document.documentElement(); m_documentRoot = baseElement.attribute(QStringLiteral("root")); if (!m_documentRoot.isEmpty()) { m_documentRoot = QDir::cleanPath(m_documentRoot) + QDir::separator(); } if (!list.isEmpty()) { QDomElement pl = list.at(0).toElement(); if (pl.isNull()) { return; } QDomNodeList props = pl.elementsByTagName(QStringLiteral("property")); QString name; QDomElement e; for (int i = 0; i < props.count(); i++) { e = props.at(i).toElement(); name = e.attribute(QStringLiteral("name")); if (name.startsWith(QLatin1String("kdenlive:docproperties."))) { name = name.section(QLatin1Char('.'), 1); if (name == QStringLiteral("storagefolder")) { // Make sure we have an absolute path QString value = e.firstChild().nodeValue(); if (QFileInfo(value).isRelative()) { value.prepend(m_documentRoot); } m_documentProperties.insert(name, value); } else if (name == QStringLiteral("guides")) { QString guides = e.firstChild().nodeValue(); if (!guides.isEmpty()) { QMetaObject::invokeMethod(m_guideModel.get(), "importFromJson", Qt::QueuedConnection, Q_ARG(const QString &, guides), Q_ARG(bool, true), Q_ARG(bool, false)); } } else { m_documentProperties.insert(name, e.firstChild().nodeValue()); } } else if (name.startsWith(QLatin1String("kdenlive:docmetadata."))) { name = name.section(QLatin1Char('.'), 1); m_documentMetadata.insert(name, e.firstChild().nodeValue()); } } } QString path = m_documentProperties.value(QStringLiteral("storagefolder")); if (!path.isEmpty()) { QDir dir(path); dir.cdUp(); m_projectFolder = dir.absolutePath(); } QString profile = m_documentProperties.value(QStringLiteral("profile")); bool profileFound = pCore->setCurrentProfile(profile); if (!profileFound) { // try to find matching profile from MLT profile properties list = m_document.elementsByTagName(QStringLiteral("profile")); if (!list.isEmpty()) { std::unique_ptr xmlProfile(new ProfileParam(list.at(0).toElement())); QString profilePath = ProfileRepository::get()->findMatchingProfile(xmlProfile.get()); // Document profile does not exist, create it as custom profile if (profilePath.isEmpty()) { profilePath = ProfileRepository::get()->saveProfile(xmlProfile.get()); } profileFound = pCore->setCurrentProfile(profilePath); } } if (!profileFound) { qDebug() << "ERROR, no matching profile found"; } updateProjectProfile(false); } void KdenliveDoc::updateProjectProfile(bool reloadProducers) { pCore->jobManager()->slotCancelJobs(); double fps = pCore->getCurrentFps(); double fpsChanged = m_timecode.fps() / fps; m_timecode.setFormat(fps); pCore->monitorManager()->resetProfiles(m_timecode); if (!reloadProducers) { return; } emit updateFps(fpsChanged); if (!qFuzzyCompare(fpsChanged, 1.0)) { pCore->bin()->reloadAllProducers(); } } void KdenliveDoc::resetProfile() { updateProjectProfile(true); emit docModified(true); } void KdenliveDoc::slotSwitchProfile(const QString &profile_path) { pCore->setCurrentProfile(profile_path); updateProjectProfile(true); emit docModified(true); } void KdenliveDoc::switchProfile(std::unique_ptr &profile, const QString &id, const QDomElement &xml) { Q_UNUSED(id) Q_UNUSED(xml) // Request profile update QString matchingProfile = ProfileRepository::get()->findMatchingProfile(profile.get()); if (matchingProfile.isEmpty() && (profile->width() % 8 != 0)) { // Make sure profile width is a multiple of 8, required by some parts of mlt profile->adjustDimensions(); matchingProfile = ProfileRepository::get()->findMatchingProfile(profile.get()); } if (!matchingProfile.isEmpty()) { // We found a known matching profile, switch and inform user profile->m_path = matchingProfile; profile->m_description = ProfileRepository::get()->getProfile(matchingProfile)->description(); if (KdenliveSettings::default_profile().isEmpty()) { // Default project format not yet confirmed, propose QString currentProfileDesc = pCore->getCurrentProfile()->description(); KMessageBox::ButtonCode answer = KMessageBox::questionYesNoCancel( QApplication::activeWindow(), i18n("Your default project profile is %1, but your clip's profile is %2.\nDo you want to change default profile for future projects ?", currentProfileDesc, profile->description()), i18n("Change default project profile"), KGuiItem(i18n("Change default to %1", profile->description())), KGuiItem(i18n("Keep current default %1", currentProfileDesc)), KGuiItem(i18n("Ask me later"))); switch (answer) { case KMessageBox::Yes: KdenliveSettings::setDefault_profile(profile->path()); pCore->setCurrentProfile(profile->path()); updateProjectProfile(true); emit docModified(true); return; break; case KMessageBox::No: return; break; default: break; } } // Build actions for the info message (switch / cancel) QList list; const QString profilePath = profile->path(); QAction *ac = new QAction(QIcon::fromTheme(QStringLiteral("dialog-ok")), i18n("Switch"), this); connect(ac, &QAction::triggered, [this, profilePath]() { this->slotSwitchProfile(profilePath); }); QAction *ac2 = new QAction(QIcon::fromTheme(QStringLiteral("dialog-cancel")), i18n("Cancel"), this); list << ac << ac2; pCore->displayBinMessage(i18n("Switch to clip profile %1?", profile->descriptiveString()), KMessageWidget::Information, list); } else { // No known profile, ask user if he wants to use clip profile anyway // Check profile fps so that we don't end up with an fps = 30.003 which would mess things up QString adjustMessage; double fps = (double)profile->frame_rate_num() / profile->frame_rate_den(); double fps_int; double fps_frac = std::modf(fps, &fps_int); if (fps_frac < 0.4) { profile->m_frame_rate_num = (int)fps_int; profile->m_frame_rate_den = 1; } else { // Check for 23.98, 29.97, 59.94 if (qFuzzyCompare(fps_int, 23.0)) { if (qFuzzyCompare(fps, 23.98)) { profile->m_frame_rate_num = 24000; profile->m_frame_rate_den = 1001; } } else if (qFuzzyCompare(fps_int, 29.0)) { if (qFuzzyCompare(fps, 29.97)) { profile->m_frame_rate_num = 30000; profile->m_frame_rate_den = 1001; } } else if (qFuzzyCompare(fps_int, 59.0)) { if (qFuzzyCompare(fps, 59.94)) { profile->m_frame_rate_num = 60000; profile->m_frame_rate_den = 1001; } } else { // Unknown profile fps, warn user adjustMessage = i18n("\nWarning: unknown non integer fps, might cause incorrect duration display."); } } if (qFuzzyCompare((double)profile->m_frame_rate_num / profile->m_frame_rate_den, fps)) { adjustMessage = i18n("\nProfile fps adjusted from original %1", QString::number(fps, 'f', 4)); } if (KMessageBox::warningContinueCancel(QApplication::activeWindow(), i18n("No profile found for your clip.\nCreate and switch to new profile (%1x%2, %3fps)?%4", profile->m_width, profile->m_height, QString::number((double)profile->m_frame_rate_num / profile->m_frame_rate_den, 'f', 2), adjustMessage)) == KMessageBox::Continue) { profile->m_description = QStringLiteral("%1x%2 %3fps") .arg(profile->m_width) .arg(profile->m_height) .arg(QString::number((double)profile->m_frame_rate_num / profile->m_frame_rate_den, 'f', 2)); QString profilePath = ProfileRepository::get()->saveProfile(profile.get()); pCore->setCurrentProfile(profilePath); updateProjectProfile(true); emit docModified(true); } } } void KdenliveDoc::doAddAction(const QString &name, QAction *a, const QKeySequence &shortcut) { pCore->window()->actionCollection()->addAction(name, a); a->setShortcut(shortcut); pCore->window()->actionCollection()->setDefaultShortcut(a, a->shortcut()); } QAction *KdenliveDoc::getAction(const QString &name) { return pCore->window()->actionCollection()->action(name); } void KdenliveDoc::previewProgress(int p) { pCore->window()->setPreviewProgress(p); } void KdenliveDoc::displayMessage(const QString &text, MessageType type, int timeOut) { pCore->window()->displayMessage(text, type, timeOut); } void KdenliveDoc::selectPreviewProfile() { // Read preview profiles and find the best match if (!KdenliveSettings::previewparams().isEmpty()) { setDocumentProperty(QStringLiteral("previewparameters"), KdenliveSettings::previewparams()); setDocumentProperty(QStringLiteral("previewextension"), KdenliveSettings::previewextension()); return; } KConfig conf(QStringLiteral("encodingprofiles.rc"), KConfig::CascadeConfig, QStandardPaths::AppDataLocation); KConfigGroup group(&conf, "timelinepreview"); QMap values = group.entryMap(); if (KdenliveSettings::nvencEnabled() && values.contains(QStringLiteral("x264-nvenc"))) { const QString bestMatch = values.value(QStringLiteral("x264-nvenc")); setDocumentProperty(QStringLiteral("previewparameters"), bestMatch.section(QLatin1Char(';'), 0, 0)); setDocumentProperty(QStringLiteral("previewextension"), bestMatch.section(QLatin1Char(';'), 1, 1)); return; } if (KdenliveSettings::vaapiEnabled() && values.contains(QStringLiteral("x264-vaapi"))) { const QString bestMatch = values.value(QStringLiteral("x264-vaapi")); setDocumentProperty(QStringLiteral("previewparameters"), bestMatch.section(QLatin1Char(';'), 0, 0)); setDocumentProperty(QStringLiteral("previewextension"), bestMatch.section(QLatin1Char(';'), 1, 1)); return; } QMapIterator i(values); QStringList matchingProfiles; QStringList fallBackProfiles; QSize pSize = pCore->getCurrentFrameDisplaySize(); QString profileSize = QStringLiteral("%1x%2").arg(pSize.width()).arg(pSize.height()); while (i.hasNext()) { i.next(); // Check for frame rate QString params = i.value(); QStringList data = i.value().split(QLatin1Char(' ')); // Check for size mismatch if (params.contains(QStringLiteral("s="))) { QString paramSize = params.section(QStringLiteral("s="), 1).section(QLatin1Char(' '), 0, 0); if (paramSize != profileSize) { continue; } } bool rateFound = false; for (const QString &arg : data) { if (arg.startsWith(QStringLiteral("r="))) { rateFound = true; double fps = arg.section(QLatin1Char('='), 1).toDouble(); if (fps > 0) { if (qAbs((int)(pCore->getCurrentFps() * 100) - (fps * 100)) <= 1) { matchingProfiles << i.value(); break; } } } } if (!rateFound) { // Profile without fps, can be used as fallBack fallBackProfiles << i.value(); } } QString bestMatch; if (!matchingProfiles.isEmpty()) { bestMatch = matchingProfiles.first(); } else if (!fallBackProfiles.isEmpty()) { bestMatch = fallBackProfiles.first(); } if (!bestMatch.isEmpty()) { setDocumentProperty(QStringLiteral("previewparameters"), bestMatch.section(QLatin1Char(';'), 0, 0)); setDocumentProperty(QStringLiteral("previewextension"), bestMatch.section(QLatin1Char(';'), 1, 1)); } else { setDocumentProperty(QStringLiteral("previewparameters"), QString()); setDocumentProperty(QStringLiteral("previewextension"), QString()); } } QString KdenliveDoc::getAutoProxyProfile() { if (m_proxyExtension.isEmpty() || m_proxyParams.isEmpty()) { initProxySettings(); } return m_proxyParams; } void KdenliveDoc::initProxySettings() { // Read preview profiles and find the best match KConfig conf(QStringLiteral("encodingprofiles.rc"), KConfig::CascadeConfig, QStandardPaths::AppDataLocation); KConfigGroup group(&conf, "proxy"); QString params; QMap values = group.entryMap(); // Select best proxy profile depending on hw encoder support if (KdenliveSettings::nvencEnabled() && values.contains(QStringLiteral("x264-nvenc"))) { params = values.value(QStringLiteral("x264-nvenc")); } else if (KdenliveSettings::vaapiEnabled() && values.contains(QStringLiteral("x264-vaapi"))) { params = values.value(QStringLiteral("x264-vaapi")); } else { params = values.value(QStringLiteral("MJPEG")); } m_proxyParams = params.section(QLatin1Char(';'), 0, 0); m_proxyExtension = params.section(QLatin1Char(';'), 1); } void KdenliveDoc::checkPreviewStack() { // A command was pushed in the middle of the stack, remove all cached data from last undos emit removeInvalidUndo(m_commandStack->count()); } void KdenliveDoc::saveMltPlaylist(const QString &fileName) { Q_UNUSED(fileName) // TODO REFAC // m_render->preparePreviewRendering(fileName); } void KdenliveDoc::initCacheDirs() { bool ok = false; QString kdenliveCacheDir; QString documentId = QDir::cleanPath(getDocumentProperty(QStringLiteral("documentid"))); documentId.toLongLong(&ok, 10); if (m_projectFolder.isEmpty()) { kdenliveCacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation); } else { kdenliveCacheDir = m_projectFolder; } if (!ok || documentId.isEmpty() || kdenliveCacheDir.isEmpty()) { return; } QString basePath = kdenliveCacheDir + QLatin1Char('/') + documentId; QDir dir(basePath); dir.mkpath(QStringLiteral(".")); dir.mkdir(QStringLiteral("preview")); dir.mkdir(QStringLiteral("audiothumbs")); dir.mkdir(QStringLiteral("videothumbs")); QDir cacheDir(kdenliveCacheDir); cacheDir.mkdir(QStringLiteral("proxy")); } QDir KdenliveDoc::getCacheDir(CacheType type, bool *ok) const { QString basePath; QString kdenliveCacheDir; QString documentId = QDir::cleanPath(getDocumentProperty(QStringLiteral("documentid"))); documentId.toLongLong(ok, 10); if (m_projectFolder.isEmpty()) { kdenliveCacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation); if (!*ok || documentId.isEmpty() || kdenliveCacheDir.isEmpty()) { *ok = false; return QDir(kdenliveCacheDir); } } else { // Use specified folder to store all files kdenliveCacheDir = m_projectFolder; } basePath = kdenliveCacheDir + QLatin1Char('/') + documentId; switch (type) { case SystemCacheRoot: return QStandardPaths::writableLocation(QStandardPaths::CacheLocation); case CacheRoot: basePath = kdenliveCacheDir; break; case CachePreview: basePath.append(QStringLiteral("/preview")); break; case CacheProxy: basePath = kdenliveCacheDir; basePath.append(QStringLiteral("/proxy")); break; case CacheAudio: basePath.append(QStringLiteral("/audiothumbs")); break; case CacheThumbs: basePath.append(QStringLiteral("/videothumbs")); break; default: break; } QDir dir(basePath); if (!dir.exists()) { *ok = false; } return dir; } QStringList KdenliveDoc::getProxyHashList() { return pCore->bin()->getProxyHashList(); } std::shared_ptr KdenliveDoc::getGuideModel() const { return m_guideModel; } void KdenliveDoc::guidesChanged() { m_documentProperties[QStringLiteral("guides")] = m_guideModel->toJson(); } void KdenliveDoc::groupsChanged(const QString &groups) { m_documentProperties[QStringLiteral("groups")] = groups; } const QString KdenliveDoc::documentRoot() const { return m_documentRoot; } bool KdenliveDoc::updatePreviewSettings(const QString &profile) { if (profile.isEmpty()) { return false; } QString params = profile.section(QLatin1Char(';'), 0, 0); QString ext = profile.section(QLatin1Char(';'), 1, 1); if (params != getDocumentProperty(QStringLiteral("previewparameters")) || ext != getDocumentProperty(QStringLiteral("previewextension"))) { // Timeline preview params changed, delete all existing previews. setDocumentProperty(QStringLiteral("previewparameters"), params); setDocumentProperty(QStringLiteral("previewextension"), ext); return true; } return false; } diff --git a/src/effects/effectstack/model/effectgroupmodel.cpp b/src/effects/effectstack/model/effectgroupmodel.cpp index ae0db48c6..62f0a3b67 100644 --- a/src/effects/effectstack/model/effectgroupmodel.cpp +++ b/src/effects/effectstack/model/effectgroupmodel.cpp @@ -1,85 +1,85 @@ /*************************************************************************** * 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 "effectgroupmodel.hpp" #include "effectstackmodel.hpp" #include EffectGroupModel::EffectGroupModel(const QList &data, const QString &name, const std::shared_ptr &stack, bool isRoot) : AbstractEffectItem(EffectItemType::Group, data, stack, isRoot) , m_name(name) { } // static std::shared_ptr EffectGroupModel::construct(const QString &name, std::shared_ptr stack, bool isRoot) { QList data; data << name << name; - std::shared_ptr self(new EffectGroupModel(data, name, std::move(stack), isRoot)); + std::shared_ptr self(new EffectGroupModel(data, name, stack, isRoot)); baseFinishConstruct(self); return self; } void EffectGroupModel::updateEnable() { for (int i = 0; i < childCount(); ++i) { std::static_pointer_cast(child(i))->updateEnable(); } } bool EffectGroupModel::isAudio() const { bool result = false; for (int i = 0; i < childCount() && !result; ++i) { result = result || std::static_pointer_cast(child(i))->isAudio(); } return result; } void EffectGroupModel::plant(const std::weak_ptr &service) { for (int i = 0; i < childCount(); ++i) { std::static_pointer_cast(child(i))->plant(service); } } void EffectGroupModel::plantClone(const std::weak_ptr &service) { for (int i = 0; i < childCount(); ++i) { std::static_pointer_cast(child(i))->plantClone(service); } } void EffectGroupModel::unplant(const std::weak_ptr &service) { for (int i = 0; i < childCount(); ++i) { std::static_pointer_cast(child(i))->unplant(service); } } void EffectGroupModel::unplantClone(const std::weak_ptr &service) { for (int i = 0; i < childCount(); ++i) { std::static_pointer_cast(child(i))->unplantClone(service); } } diff --git a/src/effects/effectstack/model/effectitemmodel.cpp b/src/effects/effectstack/model/effectitemmodel.cpp index 295329de8..33ad2ab65 100644 --- a/src/effects/effectstack/model/effectitemmodel.cpp +++ b/src/effects/effectstack/model/effectitemmodel.cpp @@ -1,220 +1,220 @@ /*************************************************************************** * 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 "effectitemmodel.hpp" #include "core.h" #include "effects/effectsrepository.hpp" #include "effectstackmodel.hpp" #include EffectItemModel::EffectItemModel(const QList &effectData, std::unique_ptr effect, const QDomElement &xml, const QString &effectId, const std::shared_ptr &stack, bool isEnabled) : AbstractEffectItem(EffectItemType::Effect, effectData, stack, false, isEnabled) , AssetParameterModel(std::move(effect), xml, effectId, std::static_pointer_cast(stack)->getOwnerId()) , m_childId(0) { connect(this, &AssetParameterModel::updateChildren, [&](const QString &name) { if (m_childEffects.size() == 0) { return; } qDebug() << "* * *SETTING EFFECT PARAM: " << name << " = " << m_asset->get(name.toUtf8().constData()); QMapIterator> i(m_childEffects); while (i.hasNext()) { i.next(); i.value()->filter().set(name.toUtf8().constData(), m_asset->get(name.toUtf8().constData())); } }); } // static std::shared_ptr EffectItemModel::construct(const QString &effectId, std::shared_ptr stack) { Q_ASSERT(EffectsRepository::get()->exists(effectId)); QDomElement xml = EffectsRepository::get()->getXml(effectId); std::unique_ptr effect = EffectsRepository::get()->getEffect(effectId); effect->set("kdenlive_id", effectId.toUtf8().constData()); QList data; data << EffectsRepository::get()->getName(effectId) << effectId; - std::shared_ptr self(new EffectItemModel(data, std::move(effect), xml, effectId, std::move(stack), true)); + std::shared_ptr self(new EffectItemModel(data, std::move(effect), xml, effectId, stack, true)); baseFinishConstruct(self); return self; } // static std::shared_ptr EffectItemModel::construct(std::unique_ptr effect, std::shared_ptr stack) { QString effectId = effect->get("kdenlive_id"); if (effectId.isEmpty()) { effectId = effect->get("mlt_service"); } Q_ASSERT(EffectsRepository::get()->exists(effectId)); QDomElement xml = EffectsRepository::get()->getXml(effectId); QDomNodeList params = xml.elementsByTagName(QStringLiteral("parameter")); for (int i = 0; i < params.count(); ++i) { QDomElement currentParameter = params.item(i).toElement(); QString paramName = currentParameter.attribute(QStringLiteral("name")); QString paramValue = effect->get(paramName.toUtf8().constData()); currentParameter.setAttribute(QStringLiteral("value"), paramValue); } QList data; data << EffectsRepository::get()->getName(effectId) << effectId; bool disable = effect->get_int("disable") == 0; - std::shared_ptr self(new EffectItemModel(data, std::move(effect), xml, effectId, std::move(stack), disable)); + std::shared_ptr self(new EffectItemModel(data, std::move(effect), xml, effectId, stack, disable)); baseFinishConstruct(self); return self; } void EffectItemModel::plant(const std::weak_ptr &service) { if (auto ptr = service.lock()) { int ret = ptr->attach(filter()); Q_ASSERT(ret == 0); } else { qDebug() << "Error : Cannot plant effect because parent service is not available anymore"; Q_ASSERT(false); } } void EffectItemModel::loadClone(const std::weak_ptr &service) { if (auto ptr = service.lock()) { const QString effectId = getAssetId(); std::shared_ptr effect = nullptr; for (int i = 0; i < ptr->filter_count(); i++) { std::unique_ptr filt(ptr->filter(i)); QString effName = filt->get("kdenlive_id"); if (effName == effectId && filt->get_int("_kdenlive_processed") == 0) { if (auto ptr2 = m_model.lock()) { effect = EffectItemModel::construct(std::move(filt), ptr2); int childId = ptr->get_int("_childid"); if (childId == 0) { childId = m_childId++; ptr->set("_childid", childId); } m_childEffects.insert(childId, effect); } break; } filt->set("_kdenlive_processed", 1); } return; } qDebug() << "Error : Cannot plant effect because parent service is not available anymore"; Q_ASSERT(false); } void EffectItemModel::plantClone(const std::weak_ptr &service) { if (auto ptr = service.lock()) { const QString effectId = getAssetId(); std::shared_ptr effect = nullptr; if (auto ptr2 = m_model.lock()) { effect = EffectItemModel::construct(effectId, ptr2); effect->setParameters(getAllParameters()); int childId = ptr->get_int("_childid"); if (childId == 0) { childId = m_childId++; ptr->set("_childid", childId); } m_childEffects.insert(childId, effect); int ret = ptr->attach(effect->filter()); Q_ASSERT(ret == 0); return; } } qDebug() << "Error : Cannot plant effect because parent service is not available anymore"; Q_ASSERT(false); } void EffectItemModel::unplant(const std::weak_ptr &service) { if (auto ptr = service.lock()) { int ret = ptr->detach(filter()); Q_ASSERT(ret == 0); } else { qDebug() << "Error : Cannot plant effect because parent service is not available anymore"; Q_ASSERT(false); } } void EffectItemModel::unplantClone(const std::weak_ptr &service) { if (m_childEffects.size() == 0) { return; } if (auto ptr = service.lock()) { int ret = ptr->detach(filter()); Q_ASSERT(ret == 0); int childId = ptr->get_int("_childid"); auto effect = m_childEffects.take(childId); if (effect && effect->isValid()) { ptr->detach(effect->filter()); effect.reset(); } } else { qDebug() << "Error : Cannot plant effect because parent service is not available anymore"; Q_ASSERT(false); } } Mlt::Filter &EffectItemModel::filter() const { return *static_cast(m_asset.get()); } bool EffectItemModel::isValid() const { return m_asset && m_asset->is_valid(); } void EffectItemModel::updateEnable() { filter().set("disable", isEnabled() ? 0 : 1); pCore->refreshProjectItem(m_ownerId); const QModelIndex start = AssetParameterModel::index(0, 0); const QModelIndex end = AssetParameterModel::index(rowCount() - 1, 0); emit dataChanged(start, end, QVector()); emit enabledChange(!isEnabled()); // Update timeline child producers AssetParameterModel::updateChildren(QStringLiteral("disable")); } void EffectItemModel::setCollapsed(bool collapsed) { filter().set("kdenlive:collapsed", collapsed ? 1 : 0); } bool EffectItemModel::isCollapsed() { return filter().get_int("kdenlive:collapsed") == 1; } bool EffectItemModel::isAudio() const { return EffectsRepository::get()->getType(getAssetId()) == EffectType::Audio; } diff --git a/src/effects/effectstack/model/effectstackmodel.cpp b/src/effects/effectstack/model/effectstackmodel.cpp index d1686e555..28d8e760e 100644 --- a/src/effects/effectstack/model/effectstackmodel.cpp +++ b/src/effects/effectstack/model/effectstackmodel.cpp @@ -1,1081 +1,1081 @@ /*************************************************************************** * 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 "effectstackmodel.hpp" #include "assets/keyframes/model/keyframemodellist.hpp" #include "core.h" #include "doc/docundostack.hpp" #include "effectgroupmodel.hpp" #include "effectitemmodel.hpp" #include "effects/effectsrepository.hpp" #include "macros.hpp" #include "timeline2/model/timelinemodel.hpp" #include #include #include #include EffectStackModel::EffectStackModel(std::weak_ptr service, ObjectId ownerId, std::weak_ptr undo_stack) : AbstractTreeModel() , m_effectStackEnabled(true) , m_ownerId(ownerId) - , m_undoStack(undo_stack) + , m_undoStack(std::move(undo_stack)) , m_lock(QReadWriteLock::Recursive) , m_loadingExisting(false) { m_masterService = std::move(service); } std::shared_ptr EffectStackModel::construct(std::weak_ptr service, ObjectId ownerId, std::weak_ptr undo_stack) { - std::shared_ptr self(new EffectStackModel(std::move(service), ownerId, undo_stack)); + std::shared_ptr self(new EffectStackModel(std::move(service), ownerId, std::move(undo_stack))); self->rootItem = EffectGroupModel::construct(QStringLiteral("root"), self, true); return self; } void EffectStackModel::resetService(std::weak_ptr service) { QWriteLocker locker(&m_lock); m_masterService = std::move(service); m_childServices.clear(); // replant all effects in new service for (int i = 0; i < rootItem->childCount(); ++i) { std::static_pointer_cast(rootItem->child(i))->plant(m_masterService); } } void EffectStackModel::addService(std::weak_ptr service) { QWriteLocker locker(&m_lock); m_childServices.emplace_back(std::move(service)); for (int i = 0; i < rootItem->childCount(); ++i) { std::static_pointer_cast(rootItem->child(i))->plantClone(m_childServices.back()); } } void EffectStackModel::loadService(std::weak_ptr service) { QWriteLocker locker(&m_lock); m_childServices.emplace_back(std::move(service)); for (int i = 0; i < rootItem->childCount(); ++i) { std::static_pointer_cast(rootItem->child(i))->loadClone(m_childServices.back()); } } -void EffectStackModel::removeService(std::shared_ptr service) +void EffectStackModel::removeService(const std::shared_ptr &service) { QWriteLocker locker(&m_lock); std::vector to_delete; for (int i = int(m_childServices.size()) - 1; i >= 0; --i) { auto ptr = m_childServices[uint(i)].lock(); if (service->get_int("_childid") == ptr->get_int("_childid")) { for (int j = 0; j < rootItem->childCount(); ++j) { std::static_pointer_cast(rootItem->child(j))->unplantClone(ptr); } to_delete.push_back(i); } } for (int i : to_delete) { m_childServices.erase(m_childServices.begin() + i); } } void EffectStackModel::removeCurrentEffect() { int ix = 0; if (auto ptr = m_masterService.lock()) { ix = ptr->get_int("kdenlive:activeeffect"); } if (ix < 0) { return; } std::shared_ptr effect = std::static_pointer_cast(rootItem->child(ix)); if (effect) { removeEffect(effect); } } -void EffectStackModel::removeEffect(std::shared_ptr effect) +void EffectStackModel::removeEffect(const std::shared_ptr &effect) { qDebug() << "* * ** REMOVING EFFECT FROM STACK!!!\n!!!!!!!!!"; QWriteLocker locker(&m_lock); Q_ASSERT(m_allItems.count(effect->getId()) > 0); int parentId = -1; if (auto ptr = effect->parentItem().lock()) parentId = ptr->getId(); int current = 0; if (auto srv = m_masterService.lock()) { current = srv->get_int("kdenlive:activeeffect"); if (current >= rootItem->childCount() - 1) { srv->set("kdenlive:activeeffect", --current); } } int currentRow = effect->row(); Fun undo = addItem_lambda(effect, parentId); if (currentRow != rowCount() - 1) { Fun move = moveItem_lambda(effect->getId(), currentRow, true); PUSH_LAMBDA(move, undo); } Fun redo = removeItem_lambda(effect->getId()); bool res = redo(); if (res) { int inFades = int(fadeIns.size()); int outFades = int(fadeOuts.size()); fadeIns.erase(effect->getId()); fadeOuts.erase(effect->getId()); inFades = int(fadeIns.size()) - inFades; outFades = int(fadeOuts.size()) - outFades; QString effectName = EffectsRepository::get()->getName(effect->getAssetId()); Fun update = [this, current, inFades, outFades]() { // Required to build the effect view if (current < 0 || rowCount() == 0) { // Stack is now empty emit dataChanged(QModelIndex(), QModelIndex(), {}); } else { QVector roles = {TimelineModel::EffectNamesRole}; if (inFades < 0) { roles << TimelineModel::FadeInRole; } if (outFades < 0) { roles << TimelineModel::FadeOutRole; } qDebug() << "// EMITTING UNDO DATA CHANGE: " << roles; emit dataChanged(QModelIndex(), QModelIndex(), roles); } // TODO: only update if effect is fade or keyframe /*if (inFades < 0) { pCore->updateItemModel(m_ownerId, QStringLiteral("fadein")); } else if (outFades < 0) { pCore->updateItemModel(m_ownerId, QStringLiteral("fadeout")); }*/ pCore->updateItemKeyframes(m_ownerId); return true; }; Fun update2 = [this, inFades, outFades]() { // Required to build the effect view QVector roles = {TimelineModel::EffectNamesRole}; // TODO: only update if effect is fade or keyframe if (inFades < 0) { roles << TimelineModel::FadeInRole; } else if (outFades < 0) { roles << TimelineModel::FadeOutRole; } qDebug() << "// EMITTING REDO DATA CHANGE: " << roles; emit dataChanged(QModelIndex(), QModelIndex(), roles); pCore->updateItemKeyframes(m_ownerId); return true; }; update(); PUSH_LAMBDA(update, redo); PUSH_LAMBDA(update2, undo); PUSH_UNDO(undo, redo, i18n("Delete effect %1", effectName)); } else { qDebug() << "..........FAILED EFFECT DELETION"; } } -bool EffectStackModel::copyEffect(std::shared_ptr sourceItem, PlaylistState::ClipState state, bool logUndo) +bool EffectStackModel::copyEffect(const std::shared_ptr &sourceItem, PlaylistState::ClipState state, bool logUndo) { std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool result = copyEffect(sourceItem, state, undo, redo); if (result && logUndo) { std::shared_ptr sourceEffect = std::static_pointer_cast(sourceItem); QString effectName = EffectsRepository::get()->getName(sourceEffect->getAssetId()); PUSH_UNDO(undo, redo, i18n("copy effect %1", effectName)); } return result; } QDomElement EffectStackModel::toXml(QDomDocument &document) { QDomElement container = document.createElement(QStringLiteral("effects")); for (int i = 0; i < rootItem->childCount(); ++i) { std::shared_ptr sourceEffect = std::static_pointer_cast(rootItem->child(i)); QDomElement sub = document.createElement(QStringLiteral("effect")); sub.setAttribute(QStringLiteral("id"), sourceEffect->getAssetId()); sub.setAttribute(QStringLiteral("in"), sourceEffect->filter().get_int("in")); sub.setAttribute(QStringLiteral("out"), sourceEffect->filter().get_int("out")); QVector> params = sourceEffect->getAllParameters(); QLocale locale; - for (auto param : params) { + for (const auto ¶m : params) { if (param.second.type() == QVariant::Double) { Xml::setXmlProperty(sub, param.first, locale.toString(param.second.toDouble())); } else { Xml::setXmlProperty(sub, param.first, param.second.toString()); } } container.appendChild(sub); } return container; } void EffectStackModel::fromXml(const QDomElement &effectsXml, Fun &undo, Fun &redo) { QDomNodeList nodeList = effectsXml.elementsByTagName(QStringLiteral("effect")); for (int i = 0; i < nodeList.count(); ++i) { QDomElement node = nodeList.item(i).toElement(); const QString effectId = node.attribute(QStringLiteral("id")); auto effect = EffectItemModel::construct(effectId, shared_from_this()); int in = node.attribute(QStringLiteral("in")).toInt(); int out = node.attribute(QStringLiteral("out")).toInt(); if (out > 0) { effect->filter().set("in", in); effect->filter().set("out", out); } QVector> parameters; QDomNodeList params = node.elementsByTagName(QStringLiteral("property")); for (int j = 0; j < params.count(); j++) { QDomElement pnode = params.item(j).toElement(); parameters.append(QPair(pnode.attribute(QStringLiteral("name")), QVariant(pnode.text()))); } effect->setParameters(parameters); Fun local_undo = removeItem_lambda(effect->getId()); // TODO the parent should probably not always be the root Fun local_redo = addItem_lambda(effect, rootItem->getId()); effect->prepareKeyframes(); connect(effect.get(), &AssetParameterModel::modelChanged, this, &EffectStackModel::modelChanged); connect(effect.get(), &AssetParameterModel::replugEffect, this, &EffectStackModel::replugEffect, Qt::DirectConnection); if (effectId == QLatin1String("fadein") || effectId == QLatin1String("fade_from_black")) { fadeIns.insert(effect->getId()); } else if (effectId == QLatin1String("fadeout") || effectId == QLatin1String("fade_to_black")) { fadeOuts.insert(effect->getId()); } local_redo(); UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); } if (true) { Fun update = [this]() { emit dataChanged(QModelIndex(), QModelIndex(), {}); return true; }; update(); PUSH_LAMBDA(update, redo); PUSH_LAMBDA(update, undo); } } -bool EffectStackModel::copyEffect(std::shared_ptr sourceItem, PlaylistState::ClipState state, Fun &undo, Fun &redo) +bool EffectStackModel::copyEffect(const std::shared_ptr &sourceItem, PlaylistState::ClipState state, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); if (sourceItem->childCount() > 0) { // TODO: group return false; } bool audioEffect = sourceItem->isAudio(); if (audioEffect) { if (state == PlaylistState::VideoOnly) { // This effect cannot be used return false; } } else if (state == PlaylistState::AudioOnly) { return false; } std::shared_ptr sourceEffect = std::static_pointer_cast(sourceItem); const QString effectId = sourceEffect->getAssetId(); auto effect = EffectItemModel::construct(effectId, shared_from_this()); effect->setParameters(sourceEffect->getAllParameters()); effect->filter().set("in", sourceEffect->filter().get_int("in")); effect->filter().set("out", sourceEffect->filter().get_int("out")); Fun local_undo = removeItem_lambda(effect->getId()); // TODO the parent should probably not always be the root Fun local_redo = addItem_lambda(effect, rootItem->getId()); effect->prepareKeyframes(); connect(effect.get(), &AssetParameterModel::modelChanged, this, &EffectStackModel::modelChanged); connect(effect.get(), &AssetParameterModel::replugEffect, this, &EffectStackModel::replugEffect, Qt::DirectConnection); QVector roles = {TimelineModel::EffectNamesRole}; if (effectId == QLatin1String("fadein") || effectId == QLatin1String("fade_from_black")) { fadeIns.insert(effect->getId()); roles << TimelineModel::FadeInRole; } else if (effectId == QLatin1String("fadeout") || effectId == QLatin1String("fade_to_black")) { fadeOuts.insert(effect->getId()); roles << TimelineModel::FadeOutRole; } bool res = local_redo(); if (res) { Fun update = [this, roles]() { emit dataChanged(QModelIndex(), QModelIndex(), roles); return true; }; update(); PUSH_LAMBDA(update, local_redo); PUSH_LAMBDA(update, local_undo); UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); } return res; } bool EffectStackModel::appendEffect(const QString &effectId, bool makeCurrent) { QWriteLocker locker(&m_lock); auto effect = EffectItemModel::construct(effectId, shared_from_this()); PlaylistState::ClipState state = pCore->getItemState(m_ownerId); if (effect->isAudio()) { if (state == PlaylistState::VideoOnly) { // Cannot add effect to this clip return false; } } else if (state == PlaylistState::AudioOnly) { // Cannot add effect to this clip return false; } Fun undo = removeItem_lambda(effect->getId()); // TODO the parent should probably not always be the root Fun redo = addItem_lambda(effect, rootItem->getId()); effect->prepareKeyframes(); connect(effect.get(), &AssetParameterModel::modelChanged, this, &EffectStackModel::modelChanged); connect(effect.get(), &AssetParameterModel::replugEffect, this, &EffectStackModel::replugEffect, Qt::DirectConnection); int currentActive = getActiveEffect(); if (makeCurrent) { if (auto srvPtr = m_masterService.lock()) { srvPtr->set("kdenlive:activeeffect", rowCount()); } } bool res = redo(); if (res) { int inFades = 0; int outFades = 0; if (effectId == QLatin1String("fadein") || effectId == QLatin1String("fade_from_black")) { inFades++; } else if (effectId == QLatin1String("fadeout") || effectId == QLatin1String("fade_to_black")) { outFades++; } QString effectName = EffectsRepository::get()->getName(effectId); Fun update = [this, inFades, outFades]() { // TODO: only update if effect is fade or keyframe QVector roles = {TimelineModel::EffectNamesRole}; if (inFades > 0) { roles << TimelineModel::FadeInRole; } else if (outFades > 0) { roles << TimelineModel::FadeOutRole; } pCore->updateItemKeyframes(m_ownerId); emit dataChanged(QModelIndex(), QModelIndex(), roles); return true; }; update(); PUSH_LAMBDA(update, redo); PUSH_LAMBDA(update, undo); PUSH_UNDO(undo, redo, i18n("Add effect %1", effectName)); } else if (makeCurrent) { if (auto srvPtr = m_masterService.lock()) { srvPtr->set("kdenlive:activeeffect", currentActive); } } return res; } bool EffectStackModel::adjustStackLength(bool adjustFromEnd, int oldIn, int oldDuration, int newIn, int duration, int offset, Fun &undo, Fun &redo, bool logUndo) { QWriteLocker locker(&m_lock); const int fadeInDuration = getFadePosition(true); const int fadeOutDuration = getFadePosition(false); int out = newIn + duration; for (const auto &leaf : rootItem->getLeaves()) { std::shared_ptr item = std::static_pointer_cast(leaf); if (item->effectItemType() == EffectItemType::Group) { // probably an empty group, ignore continue; } std::shared_ptr effect = std::static_pointer_cast(leaf); if (fadeInDuration > 0 && fadeIns.count(leaf->getId()) > 0) { int oldEffectIn = qMax(0, effect->filter().get_in()); int oldEffectOut = effect->filter().get_out(); qDebug() << "--previous effect: " << oldEffectIn << "-" << oldEffectOut; int effectDuration = qMin(effect->filter().get_length() - 1, duration); if (!adjustFromEnd && (oldIn != newIn || duration != oldDuration)) { // Clip start was resized, adjust effect in / out Fun operation = [effect, newIn, effectDuration, logUndo]() { effect->setParameter(QStringLiteral("in"), newIn, false); effect->setParameter(QStringLiteral("out"), newIn + effectDuration, logUndo); qDebug() << "--new effect: " << newIn << "-" << newIn + effectDuration; return true; }; bool res = operation(); if (!res) { return false; } Fun reverse = [effect, oldEffectIn, oldEffectOut, logUndo]() { effect->setParameter(QStringLiteral("in"), oldEffectIn, false); effect->setParameter(QStringLiteral("out"), oldEffectOut, logUndo); return true; }; PUSH_LAMBDA(operation, redo); PUSH_LAMBDA(reverse, undo); } else if (effectDuration < oldEffectOut - oldEffectIn || (logUndo && effect->filter().get_int("_refout") > 0)) { // Clip length changed, shorter than effect length so resize int referenceEffectOut = effect->filter().get_int("_refout"); if (referenceEffectOut <= 0) { referenceEffectOut = oldEffectOut; effect->filter().set("_refout", referenceEffectOut); } Fun operation = [effect, oldEffectIn, effectDuration, logUndo]() { effect->setParameter(QStringLiteral("out"), oldEffectIn + effectDuration, logUndo); return true; }; bool res = operation(); if (!res) { return false; } if (logUndo) { Fun reverse = [effect, referenceEffectOut]() { effect->setParameter(QStringLiteral("out"), referenceEffectOut, true); effect->filter().set("_refout", (char *)nullptr); return true; }; PUSH_LAMBDA(operation, redo); PUSH_LAMBDA(reverse, undo); } } } else if (fadeOutDuration > 0 && fadeOuts.count(leaf->getId()) > 0) { int effectDuration = qMin(fadeOutDuration, duration); int newFadeIn = out - effectDuration; int oldFadeIn = effect->filter().get_int("in"); int oldOut = effect->filter().get_int("out"); int referenceEffectIn = effect->filter().get_int("_refin"); if (referenceEffectIn <= 0) { referenceEffectIn = oldFadeIn; effect->filter().set("_refin", referenceEffectIn); } Fun operation = [effect, newFadeIn, out, logUndo]() { effect->setParameter(QStringLiteral("in"), newFadeIn, false); effect->setParameter(QStringLiteral("out"), out, logUndo); return true; }; bool res = operation(); if (!res) { return false; } if (logUndo) { Fun reverse = [effect, referenceEffectIn, oldOut]() { effect->setParameter(QStringLiteral("in"), referenceEffectIn, false); effect->setParameter(QStringLiteral("out"), oldOut, true); effect->filter().set("_refin", (char *)nullptr); return true; }; PUSH_LAMBDA(operation, redo); PUSH_LAMBDA(reverse, undo); } } else { // Not a fade effect, check for keyframes std::shared_ptr keyframes = effect->getKeyframeModel(); if (keyframes != nullptr) { // Effect has keyframes, update these keyframes->resizeKeyframes(oldIn, oldIn + oldDuration - 1, newIn, out - 1, offset, adjustFromEnd, undo, redo); QModelIndex index = getIndexFromItem(effect); Fun refresh = [effect, index]() { effect->dataChanged(index, index, QVector()); return true; }; refresh(); PUSH_LAMBDA(refresh, redo); PUSH_LAMBDA(refresh, undo); } else { qDebug() << "// NULL Keyframes---------"; } } } return true; } bool EffectStackModel::adjustFadeLength(int duration, bool fromStart, bool audioFade, bool videoFade, bool logUndo) { QWriteLocker locker(&m_lock); if (fromStart) { // Fade in if (fadeIns.empty()) { if (audioFade) { appendEffect(QStringLiteral("fadein")); } if (videoFade) { appendEffect(QStringLiteral("fade_from_black")); } } QList indexes; auto ptr = m_masterService.lock(); int in = 0; if (ptr) { in = ptr->get_int("in"); } qDebug() << "//// SETTING CLIP FADIN: " << duration; int oldDuration = -1; for (int i = 0; i < rootItem->childCount(); ++i) { if (fadeIns.count(std::static_pointer_cast(rootItem->child(i))->getId()) > 0) { std::shared_ptr effect = std::static_pointer_cast(rootItem->child(i)); if (oldDuration == -1) { oldDuration = effect->filter().get_length(); } effect->filter().set("in", in); duration = qMin(pCore->getItemDuration(m_ownerId), duration); effect->filter().set("out", in + duration); indexes << getIndexFromItem(effect); } } if (!indexes.isEmpty()) { emit dataChanged(indexes.first(), indexes.last(), QVector()); pCore->updateItemModel(m_ownerId, QStringLiteral("fadein")); if (videoFade) { int min = pCore->getItemPosition(m_ownerId); QSize range(min, min + qMax(duration, oldDuration)); pCore->refreshProjectRange(range); if (logUndo) { pCore->invalidateRange(range); } } } } else { // Fade out if (fadeOuts.empty()) { if (audioFade) { appendEffect(QStringLiteral("fadeout")); } if (videoFade) { appendEffect(QStringLiteral("fade_to_black")); } } int in = 0; auto ptr = m_masterService.lock(); if (ptr) { in = ptr->get_int("in"); } int itemDuration = pCore->getItemDuration(m_ownerId); int out = in + itemDuration; int oldDuration = -1; QList indexes; for (int i = 0; i < rootItem->childCount(); ++i) { if (fadeOuts.count(std::static_pointer_cast(rootItem->child(i))->getId()) > 0) { std::shared_ptr effect = std::static_pointer_cast(rootItem->child(i)); if (oldDuration == -1) { oldDuration = effect->filter().get_length(); } effect->filter().set("out", out); duration = qMin(itemDuration, duration); effect->filter().set("in", out - duration); indexes << getIndexFromItem(effect); } } if (!indexes.isEmpty()) { emit dataChanged(indexes.first(), indexes.last(), QVector()); pCore->updateItemModel(m_ownerId, QStringLiteral("fadeout")); if (videoFade) { int min = pCore->getItemPosition(m_ownerId); QSize range(min + itemDuration - qMax(duration, oldDuration), min + itemDuration); pCore->refreshProjectRange(range); if (logUndo) { pCore->invalidateRange(range); } } } } return true; } int EffectStackModel::getFadePosition(bool fromStart) { QWriteLocker locker(&m_lock); if (fromStart) { if (fadeIns.empty()) { return 0; } for (int i = 0; i < rootItem->childCount(); ++i) { if (*(fadeIns.begin()) == std::static_pointer_cast(rootItem->child(i))->getId()) { std::shared_ptr effect = std::static_pointer_cast(rootItem->child(i)); return effect->filter().get_length(); } } } else { if (fadeOuts.empty()) { return 0; } for (int i = 0; i < rootItem->childCount(); ++i) { if (*(fadeOuts.begin()) == std::static_pointer_cast(rootItem->child(i))->getId()) { std::shared_ptr effect = std::static_pointer_cast(rootItem->child(i)); return effect->filter().get_length(); } } } return 0; } bool EffectStackModel::removeFade(bool fromStart) { QWriteLocker locker(&m_lock); std::vector toRemove; for (int i = 0; i < rootItem->childCount(); ++i) { if ((fromStart && fadeIns.count(std::static_pointer_cast(rootItem->child(i))->getId()) > 0) || (!fromStart && fadeOuts.count(std::static_pointer_cast(rootItem->child(i))->getId()) > 0)) { toRemove.push_back(i); } } for (int i : toRemove) { std::shared_ptr effect = std::static_pointer_cast(rootItem->child(i)); removeEffect(effect); } return true; } -void EffectStackModel::moveEffect(int destRow, std::shared_ptr item) +void EffectStackModel::moveEffect(int destRow, const std::shared_ptr &item) { QWriteLocker locker(&m_lock); Q_ASSERT(m_allItems.count(item->getId()) > 0); int oldRow = item->row(); Fun undo = moveItem_lambda(item->getId(), oldRow); Fun redo = moveItem_lambda(item->getId(), destRow); bool res = redo(); if (res) { Fun update = [this]() { this->dataChanged(QModelIndex(), QModelIndex(), {TimelineModel::EffectNamesRole}); return true; }; update(); UPDATE_UNDO_REDO(update, update, undo, redo); auto effectId = std::static_pointer_cast(item)->getAssetId(); QString effectName = EffectsRepository::get()->getName(effectId); PUSH_UNDO(undo, redo, i18n("Move effect %1", effectName)); } } void EffectStackModel::registerItem(const std::shared_ptr &item) { QWriteLocker locker(&m_lock); // qDebug() << "$$$$$$$$$$$$$$$$$$$$$ Planting effect"; QModelIndex ix; if (!item->isRoot()) { auto effectItem = std::static_pointer_cast(item); if (!m_loadingExisting) { // qDebug() << "$$$$$$$$$$$$$$$$$$$$$ Planting effect in " << m_childServices.size(); effectItem->plant(m_masterService); for (const auto &service : m_childServices) { // qDebug() << "$$$$$$$$$$$$$$$$$$$$$ Planting CLONE effect in " << (void *)service.lock().get(); effectItem->plantClone(service); } } effectItem->setEffectStackEnabled(m_effectStackEnabled); const QString &effectId = effectItem->getAssetId(); if (effectId == QLatin1String("fadein") || effectId == QLatin1String("fade_from_black")) { fadeIns.insert(effectItem->getId()); } else if (effectId == QLatin1String("fadeout") || effectId == QLatin1String("fade_to_black")) { fadeOuts.insert(effectItem->getId()); } ix = getIndexFromItem(effectItem); if (!effectItem->isAudio() && !m_loadingExisting) { pCore->refreshProjectItem(m_ownerId); pCore->invalidateItem(m_ownerId); } } AbstractTreeModel::registerItem(item); } void EffectStackModel::deregisterItem(int id, TreeItem *item) { QWriteLocker locker(&m_lock); if (!item->isRoot()) { auto effectItem = static_cast(item); effectItem->unplant(m_masterService); for (const auto &service : m_childServices) { effectItem->unplantClone(service); } if (!effectItem->isAudio()) { pCore->refreshProjectItem(m_ownerId); pCore->invalidateItem(m_ownerId); } } AbstractTreeModel::deregisterItem(id, item); } void EffectStackModel::setEffectStackEnabled(bool enabled) { QWriteLocker locker(&m_lock); m_effectStackEnabled = enabled; // Recursively updates children states for (int i = 0; i < rootItem->childCount(); ++i) { std::static_pointer_cast(rootItem->child(i))->setEffectStackEnabled(enabled); } emit dataChanged(QModelIndex(), QModelIndex(), {TimelineModel::EffectsEnabledRole}); emit enabledStateChanged(); } -std::shared_ptr EffectStackModel::getEffectStackRow(int row, std::shared_ptr parentItem) +std::shared_ptr EffectStackModel::getEffectStackRow(int row, const std::shared_ptr &parentItem) { return std::static_pointer_cast(parentItem ? rootItem->child(row) : rootItem->child(row)); } -bool EffectStackModel::importEffects(std::shared_ptr sourceStack, PlaylistState::ClipState state) +bool EffectStackModel::importEffects(const std::shared_ptr &sourceStack, PlaylistState::ClipState state) { QWriteLocker locker(&m_lock); // TODO: manage fades, keyframes if clips don't have same size / in point bool found = false; for (int i = 0; i < sourceStack->rowCount(); i++) { auto item = sourceStack->getEffectStackRow(i); // NO undo. this should only be used on project opening if (copyEffect(item, state, false)) { found = true; } } if (found) { modelChanged(); } return found; } -bool EffectStackModel::importEffects(std::shared_ptr sourceStack, PlaylistState::ClipState state, Fun &undo, Fun &redo) +bool EffectStackModel::importEffects(const std::shared_ptr &sourceStack, PlaylistState::ClipState state, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); // TODO: manage fades, keyframes if clips don't have same size / in point bool found = false; for (int i = 0; i < sourceStack->rowCount(); i++) { auto item = sourceStack->getEffectStackRow(i); if (copyEffect(item, state, undo, redo)) { found = true; } } if (found) { modelChanged(); } return found; } -void EffectStackModel::importEffects(std::weak_ptr service, PlaylistState::ClipState state, bool alreadyExist) +void EffectStackModel::importEffects(const std::weak_ptr &service, PlaylistState::ClipState state, bool alreadyExist) { QWriteLocker locker(&m_lock); m_loadingExisting = alreadyExist; if (auto ptr = service.lock()) { for (int i = 0; i < ptr->filter_count(); i++) { std::unique_ptr filter(ptr->filter(i)); if (filter->get("kdenlive_id") == nullptr) { // don't consider internal MLT stuff continue; } const QString effectId = qstrdup(filter->get("kdenlive_id")); // The MLT filter already exists, use it directly to create the effect std::shared_ptr effect; if (alreadyExist) { // effect is already plugged in the service effect = EffectItemModel::construct(std::move(filter), shared_from_this()); } else { // duplicate effect std::unique_ptr asset = EffectsRepository::get()->getEffect(effectId); asset->inherit(*(filter)); effect = EffectItemModel::construct(std::move(asset), shared_from_this()); } if (effect->isAudio()) { if (state == PlaylistState::VideoOnly) { // Don't import effect continue; } } else if (state == PlaylistState::AudioOnly) { // Don't import effect continue; } connect(effect.get(), &AssetParameterModel::modelChanged, this, &EffectStackModel::modelChanged); connect(effect.get(), &AssetParameterModel::replugEffect, this, &EffectStackModel::replugEffect, Qt::DirectConnection); Fun redo = addItem_lambda(effect, rootItem->getId()); effect->prepareKeyframes(); if (redo()) { if (effectId == QLatin1String("fadein") || effectId == QLatin1String("fade_from_black")) { fadeIns.insert(effect->getId()); } else if (effectId == QLatin1String("fadeout") || effectId == QLatin1String("fade_to_black")) { fadeOuts.insert(effect->getId()); } } } } m_loadingExisting = false; modelChanged(); } void EffectStackModel::setActiveEffect(int ix) { QWriteLocker locker(&m_lock); if (auto ptr = m_masterService.lock()) { ptr->set("kdenlive:activeeffect", ix); } pCore->updateItemKeyframes(m_ownerId); } int EffectStackModel::getActiveEffect() const { QWriteLocker locker(&m_lock); if (auto ptr = m_masterService.lock()) { return ptr->get_int("kdenlive:activeeffect"); } return 0; } -void EffectStackModel::slotCreateGroup(std::shared_ptr childEffect) +void EffectStackModel::slotCreateGroup(const std::shared_ptr &childEffect) { QWriteLocker locker(&m_lock); auto groupItem = EffectGroupModel::construct(QStringLiteral("group"), shared_from_this()); rootItem->appendChild(groupItem); groupItem->appendChild(childEffect); } ObjectId EffectStackModel::getOwnerId() const { return m_ownerId; } bool EffectStackModel::checkConsistency() { if (!AbstractTreeModel::checkConsistency()) { return false; } std::vector> allFilters; // We do a DFS on the tree to retrieve all the filters std::stack> stck; stck.push(std::static_pointer_cast(rootItem)); while (!stck.empty()) { auto current = stck.top(); stck.pop(); if (current->effectItemType() == EffectItemType::Effect) { if (current->childCount() > 0) { qDebug() << "ERROR: Found an effect with children"; return false; } allFilters.push_back(std::static_pointer_cast(current)); continue; } for (int i = current->childCount() - 1; i >= 0; --i) { stck.push(std::static_pointer_cast(current->child(i))); } } for (const auto &service : m_childServices) { auto ptr = service.lock(); if (!ptr) { qDebug() << "ERROR: unavailable service"; return false; } // MLT inserts some default normalizer filters that are not managed by Kdenlive, which explains why the filter count is not equal int kdenliveFilterCount = 0; for (int i = 0; i < ptr->filter_count(); i++) { std::shared_ptr filt(ptr->filter(i)); if (filt->get("kdenlive_id") != NULL) { kdenliveFilterCount++; } // qDebug() << "FILTER: "<filter(i)->get("mlt_service"); } if (kdenliveFilterCount != (int)allFilters.size()) { qDebug() << "ERROR: Wrong filter count: " << kdenliveFilterCount << " = " << allFilters.size(); return false; } int ct = 0; for (uint i = 0; i < allFilters.size(); ++i) { while (ptr->filter(ct)->get("kdenlive_id") == NULL && ct < ptr->filter_count()) { ct++; } auto mltFilter = ptr->filter(ct); auto currentFilter = allFilters[i]->filter(); if (QString(currentFilter.get("mlt_service")) != QLatin1String(mltFilter->get("mlt_service"))) { qDebug() << "ERROR: filter " << i << "differ: " << ct << ", " << currentFilter.get("mlt_service") << " = " << mltFilter->get("mlt_service"); return false; } QVector> params = allFilters[i]->getAllParameters(); - for (auto val : params) { + for (const auto &val : params) { // Check parameters values if (val.second != QVariant(mltFilter->get(val.first.toUtf8().constData()))) { qDebug() << "ERROR: filter " << i << "PARAMETER MISMATCH: " << val.first << " = " << val.second << " != " << mltFilter->get(val.first.toUtf8().constData()); return false; } } ct++; } } return true; } void EffectStackModel::adjust(const QString &effectId, const QString &effectName, double value) { QWriteLocker locker(&m_lock); for (int i = 0; i < rootItem->childCount(); ++i) { std::shared_ptr sourceEffect = std::static_pointer_cast(rootItem->child(i)); if (effectId == sourceEffect->getAssetId()) { sourceEffect->setParameter(effectName, QString::number(value)); return; } } } bool EffectStackModel::hasFilter(const QString &effectId) const { READ_LOCK(); return rootItem->accumulate_const(false, [effectId](bool b, std::shared_ptr it) { if (b) return true; auto item = std::static_pointer_cast(it); if (item->effectItemType() == EffectItemType::Group) { return false; } auto sourceEffect = std::static_pointer_cast(it); return effectId == sourceEffect->getAssetId(); }); } double EffectStackModel::getFilterParam(const QString &effectId, const QString ¶mName) { READ_LOCK(); for (int i = 0; i < rootItem->childCount(); ++i) { std::shared_ptr sourceEffect = std::static_pointer_cast(rootItem->child(i)); if (effectId == sourceEffect->getAssetId()) { return sourceEffect->filter().get_double(paramName.toUtf8().constData()); } } return 0.0; } KeyframeModel *EffectStackModel::getEffectKeyframeModel() { if (rootItem->childCount() == 0) return nullptr; int ix = 0; if (auto ptr = m_masterService.lock()) { ix = ptr->get_int("kdenlive:activeeffect"); } if (ix < 0) { return nullptr; } std::shared_ptr sourceEffect = std::static_pointer_cast(rootItem->child(ix)); std::shared_ptr listModel = sourceEffect->getKeyframeModel(); if (listModel) { return listModel->getKeyModel(); } return nullptr; } -void EffectStackModel::replugEffect(std::shared_ptr asset) +void EffectStackModel::replugEffect(const std::shared_ptr &asset) { QWriteLocker locker(&m_lock); auto effectItem = std::static_pointer_cast(asset); int oldRow = effectItem->row(); int count = rowCount(); for (int ix = oldRow; ix < count; ix++) { auto item = std::static_pointer_cast(rootItem->child(ix)); item->unplant(m_masterService); for (const auto &service : m_childServices) { item->unplantClone(service); } } std::unique_ptr effect = EffectsRepository::get()->getEffect(effectItem->getAssetId()); effect->inherit(effectItem->filter()); effectItem->resetAsset(std::move(effect)); for (int ix = oldRow; ix < count; ix++) { auto item = std::static_pointer_cast(rootItem->child(ix)); item->unplant(m_masterService); for (const auto &service : m_childServices) { item->plantClone(service); } } } void EffectStackModel::cleanFadeEffects(bool outEffects, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); const auto &toDelete = outEffects ? fadeOuts : fadeIns; for (int id : toDelete) { auto effect = std::static_pointer_cast(getItemById(id)); Fun operation = removeItem_lambda(id); if (operation()) { Fun reverse = addItem_lambda(effect, rootItem->getId()); UPDATE_UNDO_REDO(operation, reverse, undo, redo); } } if (!toDelete.empty()) { Fun updateRedo = [this, toDelete, outEffects]() { for (int id : toDelete) { if (outEffects) { fadeOuts.erase(id); } else { fadeIns.erase(id); } } QVector roles = {TimelineModel::EffectNamesRole}; roles << (outEffects ? TimelineModel::FadeOutRole : TimelineModel::FadeInRole); emit dataChanged(QModelIndex(), QModelIndex(), roles); pCore->updateItemKeyframes(m_ownerId); return true; }; updateRedo(); PUSH_LAMBDA(updateRedo, redo); } } const QString EffectStackModel::effectNames() const { QStringList effects; for (int i = 0; i < rootItem->childCount(); ++i) { effects.append(EffectsRepository::get()->getName(std::static_pointer_cast(rootItem->child(i))->getAssetId())); } return effects.join(QLatin1Char('/')); } bool EffectStackModel::isStackEnabled() const { return m_effectStackEnabled; } bool EffectStackModel::addEffectKeyFrame(int frame, double normalisedVal) { if (rootItem->childCount() == 0) return false; int ix = 0; if (auto ptr = m_masterService.lock()) { ix = ptr->get_int("kdenlive:activeeffect"); } if (ix < 0) { return false; } std::shared_ptr sourceEffect = std::static_pointer_cast(rootItem->child(ix)); std::shared_ptr listModel = sourceEffect->getKeyframeModel(); return listModel->addKeyframe(frame, normalisedVal); } bool EffectStackModel::removeKeyFrame(int frame) { if (rootItem->childCount() == 0) return false; int ix = 0; if (auto ptr = m_masterService.lock()) { ix = ptr->get_int("kdenlive:activeeffect"); } if (ix < 0) { return false; } std::shared_ptr sourceEffect = std::static_pointer_cast(rootItem->child(ix)); std::shared_ptr listModel = sourceEffect->getKeyframeModel(); return listModel->removeKeyframe(GenTime(frame, pCore->getCurrentFps())); } bool EffectStackModel::updateKeyFrame(int oldFrame, int newFrame, QVariant normalisedVal) { if (rootItem->childCount() == 0) return false; int ix = 0; if (auto ptr = m_masterService.lock()) { ix = ptr->get_int("kdenlive:activeeffect"); } if (ix < 0) { return false; } std::shared_ptr sourceEffect = std::static_pointer_cast(rootItem->child(ix)); std::shared_ptr listModel = sourceEffect->getKeyframeModel(); - return listModel->updateKeyframe(GenTime(oldFrame, pCore->getCurrentFps()), GenTime(newFrame, pCore->getCurrentFps()), normalisedVal); + return listModel->updateKeyframe(GenTime(oldFrame, pCore->getCurrentFps()), GenTime(newFrame, pCore->getCurrentFps()), std::move(normalisedVal)); } diff --git a/src/effects/effectstack/model/effectstackmodel.hpp b/src/effects/effectstack/model/effectstackmodel.hpp index 830daf719..7a26acfea 100644 --- a/src/effects/effectstack/model/effectstackmodel.hpp +++ b/src/effects/effectstack/model/effectstackmodel.hpp @@ -1,186 +1,186 @@ /*************************************************************************** * Copyright (C) 2017 by Nicolas Carion * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #ifndef EFFECTSTACKMODEL_H #define EFFECTSTACKMODEL_H #include "abstractmodel/abstracttreemodel.hpp" #include "definitions.h" #include "undohelper.hpp" #include #include #include #include /* @brief This class an effect stack as viewed by the back-end. It is responsible for planting and managing effects into the list of producer it holds a pointer to. It can contains more than one producer for example if it represents the effect stack of a projectClip: this clips contains several producers (audio, video, ...) */ class AbstractEffectItem; class AssetParameterModel; class DocUndoStack; class EffectItemModel; class TreeItem; class KeyframeModel; class EffectStackModel : public AbstractTreeModel { Q_OBJECT public: /* @brief Constructs an effect stack and returns a shared ptr to the constructed object @param service is the mlt object on which we will plant the effects @param ownerId is some information about the actual object to which the effects are applied */ static std::shared_ptr construct(std::weak_ptr service, ObjectId ownerId, std::weak_ptr undo_stack); protected: EffectStackModel(std::weak_ptr service, ObjectId ownerId, std::weak_ptr undo_stack); public: /* @brief Add an effect at the bottom of the stack */ bool appendEffect(const QString &effectId, bool makeCurrent = false); /* @brief Copy an existing effect and append it at the bottom of the stack @param logUndo: if true, an undo/redo is created */ - bool copyEffect(std::shared_ptr sourceItem, PlaylistState::ClipState state, bool logUndo = true); - bool copyEffect(std::shared_ptr sourceItem, PlaylistState::ClipState state, Fun &undo, Fun &redo); + bool copyEffect(const std::shared_ptr &sourceItem, PlaylistState::ClipState state, bool logUndo = true); + bool copyEffect(const std::shared_ptr &sourceItem, PlaylistState::ClipState state, Fun &undo, Fun &redo); /* @brief Import all effects from the given effect stack */ - bool importEffects(std::shared_ptr sourceStack, PlaylistState::ClipState state); + bool importEffects(const std::shared_ptr &sourceStack, PlaylistState::ClipState state); /* @brief Import all effects attached to a given service @param alreadyExist: if true, the effect should be already attached to the service owned by this effectstack (it means we are in the process of loading). In that case, we need to build the stack but not replant the effects */ - bool importEffects(std::shared_ptr sourceStack, PlaylistState::ClipState state, Fun &undo, Fun &redo); - void importEffects(std::weak_ptr service, PlaylistState::ClipState state, bool alreadyExist = false); + bool importEffects(const std::shared_ptr &sourceStack, PlaylistState::ClipState state, Fun &undo, Fun &redo); + void importEffects(const std::weak_ptr &service, PlaylistState::ClipState state, bool alreadyExist = false); bool removeFade(bool fromStart); /* @brief This function change the global (timeline-wise) enabled state of the effects */ void setEffectStackEnabled(bool enabled); /* @brief Returns an effect or group from the stack (at the given row) */ - std::shared_ptr getEffectStackRow(int row, std::shared_ptr parentItem = nullptr); + std::shared_ptr getEffectStackRow(int row, const std::shared_ptr &parentItem = nullptr); /* @brief Move an effect in the stack */ - void moveEffect(int destRow, std::shared_ptr item); + void moveEffect(int destRow, const std::shared_ptr &item); /* @brief Set effect in row as current one */ void setActiveEffect(int ix); /* @brief Get currently active effect row */ int getActiveEffect() const; /* @brief Adjust an effect duration (useful for fades) */ bool adjustFadeLength(int duration, bool fromStart, bool audioFade, bool videoFade, bool logUndo); bool adjustStackLength(bool adjustFromEnd, int oldIn, int oldDuration, int newIn, int duration, int offset, Fun &undo, Fun &redo, bool logUndo); - void slotCreateGroup(std::shared_ptr childEffect); + void slotCreateGroup(const std::shared_ptr &childEffect); /* @brief Returns the id of the owner of the stack */ ObjectId getOwnerId() const; int getFadePosition(bool fromStart); Q_INVOKABLE void adjust(const QString &effectId, const QString &effectName, double value); /* @brief Returns true if the stack contains an effect with the given Id */ Q_INVOKABLE bool hasFilter(const QString &effectId) const; // TODO: this break the encapsulation, remove Q_INVOKABLE double getFilterParam(const QString &effectId, const QString ¶mName); /** get the active effect's keyframe model */ Q_INVOKABLE KeyframeModel *getEffectKeyframeModel(); /** Add a keyframe in all model parameters */ bool addEffectKeyFrame(int frame, double normalisedVal); /** Remove a keyframe in all model parameters */ bool removeKeyFrame(int frame); /** Update a keyframe in all model parameters (with value updated only in first parameter)*/ bool updateKeyFrame(int oldFrame, int newFrame, QVariant normalisedVal); /** Remove unwanted fade effects, mostly after a cut operation */ void cleanFadeEffects(bool outEffects, Fun &undo, Fun &redo); /* Remove all the services associated with this stack and replace them with the given one */ void resetService(std::weak_ptr service); /* @brief Append a new service to be managed by this stack */ void addService(std::weak_ptr service); /* @brief Append an existing service to be managed by this stack (on document load)*/ void loadService(std::weak_ptr service); /* @brief Remove a service from those managed by this stack */ - void removeService(std::shared_ptr service); + void removeService(const std::shared_ptr &service); /* @brief Returns a comma separated list of effect names */ const QString effectNames() const; bool isStackEnabled() const; /* @brief Returns an XML representation of the effect stack with all parameters */ QDomElement toXml(QDomDocument &document); /* @brief Load an effect stack from an XML representation */ void fromXml(const QDomElement &effectsXml, Fun &undo, Fun &redo); /* @brief Delete active effect from stack */ void removeCurrentEffect(); /* @brief This is a convenience function that helps check if the tree is in a valid state */ bool checkConsistency() override; public slots: /* @brief Delete an effect from the stack */ - void removeEffect(std::shared_ptr effect); + void removeEffect(const std::shared_ptr &effect); protected: /* @brief Register the existence of a new element */ void registerItem(const std::shared_ptr &item) override; /* @brief Deregister the existence of a new element*/ void deregisterItem(int id, TreeItem *item) override; std::weak_ptr m_masterService; std::vector> m_childServices; bool m_effectStackEnabled; ObjectId m_ownerId; std::weak_ptr m_undoStack; private: mutable QReadWriteLock m_lock; std::unordered_set fadeIns; std::unordered_set fadeOuts; /** @brief: When loading a project, we load filters/effects that are already planted * in the producer, so we shouldn't plant them again. Setting this value to * true will prevent planting in the producer */ bool m_loadingExisting; private slots: /** @brief: Some effects do not support dynamic changes like sox, and need to be unplugged / replugged on each param change */ - void replugEffect(std::shared_ptr asset); + void replugEffect(const std::shared_ptr &asset); signals: /** @brief: This signal is connected to the project clip for bin clips and activates the reload of effects on child (timeline) producers */ void modelChanged(); void enabledStateChanged(); }; #endif diff --git a/src/effects/effectstack/view/builtstack.cpp b/src/effects/effectstack/view/builtstack.cpp index 2ba325b3a..c2e4febee 100644 --- a/src/effects/effectstack/view/builtstack.cpp +++ b/src/effects/effectstack/view/builtstack.cpp @@ -1,64 +1,64 @@ /*************************************************************************** * Copyright (C) 2017 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 "builtstack.hpp" #include "assets/assetpanel.hpp" #include "core.h" #include "effects/effectstack/model/effectstackmodel.hpp" //#include "qml/colorwheelitem.h" #include #include #include BuiltStack::BuiltStack(AssetPanel *parent) : QQuickWidget(parent) , m_model(nullptr) { KDeclarative::KDeclarative kdeclarative; QQmlEngine *eng = engine(); kdeclarative.setDeclarativeEngine(eng); kdeclarative.setupContext(); kdeclarative.setupEngine(eng); // qmlRegisterType("Kdenlive.Controls", 1, 0, "ColorWheelItem"); setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); setMinimumHeight(300); // setClearColor(palette().base().color()); // setSource(QUrl(QStringLiteral("qrc:/qml/BuiltStack.qml"))); setFocusPolicy(Qt::StrongFocus); QQuickItem *root = rootObject(); QObject::connect(root, SIGNAL(valueChanged(QString, int)), parent, SLOT(parameterChanged(QString, int))); setResizeMode(QQuickWidget::SizeRootObjectToView); } BuiltStack::~BuiltStack() {} -void BuiltStack::setModel(std::shared_ptr model, ObjectId ownerId) +void BuiltStack::setModel(const std::shared_ptr &model, ObjectId ownerId) { m_model = model; if (ownerId.first == ObjectType::TimelineClip) { QVariant current_speed((int)(100.0 * pCore->getClipSpeed(ownerId.second))); qDebug() << " CLIP SPEED OFR: " << ownerId.second << " = " << current_speed; QMetaObject::invokeMethod(rootObject(), "setSpeed", Qt::QueuedConnection, Q_ARG(QVariant, current_speed)); } rootContext()->setContextProperty("effectstackmodel", model.get()); QMetaObject::invokeMethod(rootObject(), "resetStack", Qt::QueuedConnection); } diff --git a/src/effects/effectstack/view/builtstack.hpp b/src/effects/effectstack/view/builtstack.hpp index e066705bc..e6ab0acb5 100644 --- a/src/effects/effectstack/view/builtstack.hpp +++ b/src/effects/effectstack/view/builtstack.hpp @@ -1,45 +1,45 @@ /*************************************************************************** * Copyright (C) 2017 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 . * ***************************************************************************/ #ifndef BUILTSTACK_H #define BUILTSTACK_H #include "definitions.h" #include #include class AssetPanel; class EffectStackModel; class BuiltStack : public QQuickWidget { Q_OBJECT public: BuiltStack(AssetPanel *parent); virtual ~BuiltStack(); - void setModel(std::shared_ptr model, ObjectId ownerId); + void setModel(const std::shared_ptr &model, ObjectId ownerId); private: std::shared_ptr m_model; }; #endif diff --git a/src/effects/effectstack/view/collapsibleeffectview.cpp b/src/effects/effectstack/view/collapsibleeffectview.cpp index 6e53f466f..f19275543 100644 --- a/src/effects/effectstack/view/collapsibleeffectview.cpp +++ b/src/effects/effectstack/view/collapsibleeffectview.cpp @@ -1,807 +1,807 @@ /*************************************************************************** * Copyright (C) 2017 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 "collapsibleeffectview.hpp" #include "assets/view/assetparameterview.hpp" #include "core.h" #include "dialogs/clipcreationdialog.h" #include "effects/effectsrepository.hpp" #include "effects/effectstack/model/effectitemmodel.hpp" #include "kdenlivesettings.h" #include "monitor/monitor.h" #include "kdenlive_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include -CollapsibleEffectView::CollapsibleEffectView(std::shared_ptr effectModel, QSize frameSize, QImage icon, QWidget *parent) +CollapsibleEffectView::CollapsibleEffectView(const std::shared_ptr &effectModel, QSize frameSize, const QImage &icon, QWidget *parent) : AbstractCollapsibleWidget(parent) , m_view(nullptr) , m_model(effectModel) , m_regionEffect(false) { QString effectId = effectModel->getAssetId(); QString effectName = EffectsRepository::get()->getName(effectId); if (effectId == QLatin1String("region")) { m_regionEffect = true; decoframe->setObjectName(QStringLiteral("decoframegroup")); } filterWheelEvent = true; setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Maximum); // decoframe->setProperty("active", true); // m_info.fromString(effect.attribute(QStringLiteral("kdenlive_info"))); // setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)); buttonUp->setIcon(QIcon::fromTheme(QStringLiteral("kdenlive-up"))); buttonUp->setToolTip(i18n("Move effect up")); buttonDown->setIcon(QIcon::fromTheme(QStringLiteral("kdenlive-down"))); buttonDown->setToolTip(i18n("Move effect down")); buttonDel->setIcon(QIcon::fromTheme(QStringLiteral("kdenlive-deleffect"))); buttonDel->setToolTip(i18n("Delete effect")); // buttonUp->setEnabled(canMoveUp); // buttonDown->setEnabled(!lastEffect); if (effectId == QLatin1String("speed")) { // Speed effect is a "pseudo" effect, cannot be moved buttonUp->setVisible(false); buttonDown->setVisible(false); m_isMovable = false; setAcceptDrops(false); } else { setAcceptDrops(true); } // checkAll->setToolTip(i18n("Enable/Disable all effects")); // buttonShowComments->setIcon(QIcon::fromTheme("help-about")); // buttonShowComments->setToolTip(i18n("Show additional information for the parameters")); m_collapse = new KDualAction(i18n("Collapse Effect"), i18n("Expand Effect"), this); m_collapse->setActiveIcon(QIcon::fromTheme(QStringLiteral("arrow-right"))); collapseButton->setDefaultAction(m_collapse); connect(m_collapse, &KDualAction::activeChanged, this, &CollapsibleEffectView::slotSwitch); if (effectModel->rowCount() == 0) { // Effect has no paramerter m_collapse->setInactiveIcon(QIcon::fromTheme(QStringLiteral("tools-wizard"))); collapseButton->setEnabled(false); } else { m_collapse->setInactiveIcon(QIcon::fromTheme(QStringLiteral("arrow-down"))); } QHBoxLayout *l = static_cast(frame->layout()); m_colorIcon = new QLabel(this); l->insertWidget(0, m_colorIcon); m_colorIcon->setFixedSize(icon.size()); title = new QLabel(this); l->insertWidget(2, title); m_keyframesButton = new QToolButton(this); m_keyframesButton->setIcon(QIcon::fromTheme(QStringLiteral("adjustcurves"))); m_keyframesButton->setAutoRaise(true); m_keyframesButton->setCheckable(true); m_keyframesButton->setToolTip(i18n("Enable Keyframes")); l->insertWidget(3, m_keyframesButton); // Enable button m_enabledButton = new KDualAction(i18n("Disable Effect"), i18n("Enable Effect"), this); m_enabledButton->setActiveIcon(QIcon::fromTheme(QStringLiteral("hint"))); m_enabledButton->setInactiveIcon(QIcon::fromTheme(QStringLiteral("visibility"))); enabledButton->setDefaultAction(m_enabledButton); connect(m_model.get(), &AssetParameterModel::enabledChange, this, &CollapsibleEffectView::enableView); m_groupAction = new QAction(QIcon::fromTheme(QStringLiteral("folder-new")), i18n("Create Group"), this); connect(m_groupAction, &QAction::triggered, this, &CollapsibleEffectView::slotCreateGroup); if (m_regionEffect) { effectName.append(':' + QUrl(Xml::getXmlParameter(m_effect, QStringLiteral("resource"))).fileName()); } // Color thumb m_colorIcon->setPixmap(QPixmap::fromImage(icon)); title->setText(effectName); m_view = new AssetParameterView(this); const std::shared_ptr effectParamModel = std::static_pointer_cast(effectModel); m_view->setModel(effectParamModel, frameSize); connect(m_view, &AssetParameterView::seekToPos, this, &AbstractCollapsibleWidget::seekToPos); connect(this, &CollapsibleEffectView::refresh, m_view, &AssetParameterView::slotRefresh); m_keyframesButton->setVisible(m_view->keyframesAllowed()); QVBoxLayout *lay = new QVBoxLayout(widgetFrame); lay->setContentsMargins(0, 0, 0, 2); lay->setSpacing(0); connect(m_keyframesButton, &QToolButton::toggled, [this](bool toggle) { m_view->toggleKeyframes(toggle); // We need to switch twice to get a correct resize slotSwitch(!m_model->isCollapsed()); slotSwitch(!m_model->isCollapsed()); }); lay->addWidget(m_view); if (!effectParamModel->hasMoreThanOneKeyframe()) { // No keyframe or only one, allow hiding bool hideByDefault = effectParamModel->data(effectParamModel->index(0, 0), AssetParameterModel::HideKeyframesFirstRole).toBool(); if (hideByDefault) { m_view->toggleKeyframes(false); } else { m_keyframesButton->setChecked(true); } } else { m_keyframesButton->setChecked(true); } // Presets presetButton->setIcon(QIcon::fromTheme(QStringLiteral("document-new-from-template"))); presetButton->setMenu(m_view->presetMenu()); // Main menu m_menu = new QMenu(this); if (effectModel->rowCount() == 0) { collapseButton->setEnabled(false); m_view->setVisible(false); } m_menu->addAction(QIcon::fromTheme(QStringLiteral("document-save")), i18n("Save Effect"), this, SLOT(slotSaveEffect())); if (!m_regionEffect) { /*if (m_info.groupIndex == -1) { m_menu->addAction(m_groupAction); }*/ m_menu->addAction(QIcon::fromTheme(QStringLiteral("folder-new")), i18n("Create Region"), this, SLOT(slotCreateRegion())); } // setupWidget(info, metaInfo); menuButton->setIcon(QIcon::fromTheme(QStringLiteral("kdenlive-menu"))); menuButton->setMenu(m_menu); if (!effectModel->isEnabled()) { title->setEnabled(false); m_colorIcon->setEnabled(false); if (KdenliveSettings::disable_effect_parameters()) { widgetFrame->setEnabled(false); } m_enabledButton->setActive(true); } else { m_enabledButton->setActive(false); } connect(m_enabledButton, &KDualAction::activeChangedByUser, this, &CollapsibleEffectView::slotDisable); connect(buttonUp, &QAbstractButton::clicked, this, &CollapsibleEffectView::slotEffectUp); connect(buttonDown, &QAbstractButton::clicked, this, &CollapsibleEffectView::slotEffectDown); connect(buttonDel, &QAbstractButton::clicked, this, &CollapsibleEffectView::slotDeleteEffect); Q_FOREACH (QSpinBox *sp, findChildren()) { sp->installEventFilter(this); sp->setFocusPolicy(Qt::StrongFocus); } Q_FOREACH (KComboBox *cb, findChildren()) { cb->installEventFilter(this); cb->setFocusPolicy(Qt::StrongFocus); } Q_FOREACH (QProgressBar *cb, findChildren()) { cb->installEventFilter(this); cb->setFocusPolicy(Qt::StrongFocus); } m_collapse->setActive(m_model->isCollapsed()); slotSwitch(m_model->isCollapsed()); } CollapsibleEffectView::~CollapsibleEffectView() { qDebug() << "deleting collapsibleeffectview"; } void CollapsibleEffectView::setWidgetHeight(qreal value) { widgetFrame->setFixedHeight(m_view->contentHeight() * value); } void CollapsibleEffectView::slotCreateGroup() { emit createGroup(m_model); } void CollapsibleEffectView::slotCreateRegion() { QString allExtensions = ClipCreationDialog::getExtensions().join(QLatin1Char(' ')); const QString dialogFilter = allExtensions + QLatin1Char(' ') + QLatin1Char('|') + i18n("All Supported Files") + QStringLiteral("\n* ") + QLatin1Char('|') + i18n("All Files"); QString clipFolder = KRecentDirs::dir(QStringLiteral(":KdenliveClipFolder")); if (clipFolder.isEmpty()) { clipFolder = QDir::homePath(); } QPointer d = new QFileDialog(QApplication::activeWindow(), QString(), clipFolder, dialogFilter); d->setFileMode(QFileDialog::ExistingFile); if (d->exec() == QDialog::Accepted && !d->selectedUrls().isEmpty()) { KRecentDirs::add(QStringLiteral(":KdenliveClipFolder"), d->selectedUrls().first().adjusted(QUrl::RemoveFilename).toLocalFile()); emit createRegion(effectIndex(), d->selectedUrls().first()); } delete d; } void CollapsibleEffectView::slotUnGroup() { emit unGroup(this); } bool CollapsibleEffectView::eventFilter(QObject *o, QEvent *e) { if (e->type() == QEvent::Enter) { frame->setProperty("mouseover", true); frame->setStyleSheet(frame->styleSheet()); return QWidget::eventFilter(o, e); } if (e->type() == QEvent::Wheel) { QWheelEvent *we = static_cast(e); if (!filterWheelEvent || we->modifiers() != Qt::NoModifier) { e->accept(); return false; } if (qobject_cast(o)) { // if (qobject_cast(o)->focusPolicy() == Qt::WheelFocus) { e->accept(); return false; } if (qobject_cast(o)) { if (qobject_cast(o)->focusPolicy() == Qt::WheelFocus) { e->accept(); return false; } e->ignore(); return true; } if (qobject_cast(o)) { // if (qobject_cast(o)->focusPolicy() == Qt::WheelFocus)*/ { e->accept(); return false; } } return QWidget::eventFilter(o, e); } QDomElement CollapsibleEffectView::effect() const { return m_effect; } QDomElement CollapsibleEffectView::effectForSave() const { QDomElement effect = m_effect.cloneNode().toElement(); effect.removeAttribute(QStringLiteral("kdenlive_ix")); /* if (m_paramWidget) { int in = m_paramWidget->range().x(); EffectsController::offsetKeyframes(in, effect); } */ return effect; } bool CollapsibleEffectView::isActive() const { return decoframe->property("active").toBool(); } bool CollapsibleEffectView::isEnabled() const { return m_enabledButton->isActive(); } void CollapsibleEffectView::slotActivateEffect(QModelIndex ix) { // m_colorIcon->setEnabled(active); bool active = ix.row() == m_model->row(); decoframe->setProperty("active", active); decoframe->setStyleSheet(decoframe->styleSheet()); if (active) { pCore->getMonitor(m_model->monitorId)->slotShowEffectScene(needsMonitorEffectScene()); } m_view->initKeyframeView(active); } void CollapsibleEffectView::mousePressEvent(QMouseEvent *e) { m_dragStart = e->globalPos(); emit activateEffect(m_model); QWidget::mousePressEvent(e); } void CollapsibleEffectView::mouseMoveEvent(QMouseEvent *e) { if ((e->globalPos() - m_dragStart).manhattanLength() < QApplication::startDragDistance()) { QPixmap pix = frame->grab(); emit startDrag(pix, m_model); } QWidget::mouseMoveEvent(e); } void CollapsibleEffectView::mouseDoubleClickEvent(QMouseEvent *event) { if (frame->underMouse() && collapseButton->isEnabled()) { event->accept(); m_collapse->setActive(!m_collapse->isActive()); } else { event->ignore(); } } void CollapsibleEffectView::mouseReleaseEvent(QMouseEvent *event) { m_dragStart = QPoint(); if (!decoframe->property("active").toBool()) { // emit activateEffect(effectIndex()); } QWidget::mouseReleaseEvent(event); } void CollapsibleEffectView::slotDisable(bool disable) { QString effectId = m_model->getAssetId(); QString effectName = EffectsRepository::get()->getName(effectId); std::static_pointer_cast(m_model)->markEnabled(effectName, !disable); } void CollapsibleEffectView::slotDeleteEffect() { emit deleteEffect(m_model); } void CollapsibleEffectView::slotEffectUp() { emit moveEffect(qMax(0, m_model->row() - 1), m_model); } void CollapsibleEffectView::slotEffectDown() { emit moveEffect(m_model->row() + 2, m_model); } void CollapsibleEffectView::slotSaveEffect() { QString name = QInputDialog::getText(this, i18n("Save Effect"), i18n("Name for saved effect: ")); if (name.trimmed().isEmpty()) { return; } QDir dir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/effects/")); if (!dir.exists()) { dir.mkpath(QStringLiteral(".")); } if (dir.exists(name + QStringLiteral(".xml"))) if (KMessageBox::questionYesNo(this, i18n("File %1 already exists.\nDo you want to overwrite it?", name + QStringLiteral(".xml"))) == KMessageBox::No) { return; } QDomDocument doc; // Get base effect xml QString effectId = m_model->getAssetId(); QDomElement effect = EffectsRepository::get()->getXml(effectId); // Adjust param values QVector> currentValues = m_model->getAllParameters(); QMap values; QLocale locale; for (const auto ¶m : currentValues) { if (param.second.type() == QVariant::Double) { values.insert(param.first, locale.toString(param.second.toDouble())); } else { values.insert(param.first, param.second.toString()); } } QDomNodeList params = effect.elementsByTagName("parameter"); for (int i = 0; i < params.count(); ++i) { const QString paramName = params.item(i).toElement().attribute("name"); const QString paramType = params.item(i).toElement().attribute("type"); if (paramType == QLatin1String("fixed") || !values.contains(paramName)) { continue; } params.item(i).toElement().setAttribute(QStringLiteral("value"), values.value(paramName)); } doc.appendChild(doc.importNode(effect, true)); effect = doc.firstChild().toElement(); effect.removeAttribute(QStringLiteral("kdenlive_ix")); effect.setAttribute(QStringLiteral("id"), name); effect.setAttribute(QStringLiteral("type"), QStringLiteral("custom")); /* if (m_paramWidget) { int in = m_paramWidget->range().x(); EffectsController::offsetKeyframes(in, effect); } */ QDomElement effectname = effect.firstChildElement(QStringLiteral("name")); effect.removeChild(effectname); effectname = doc.createElement(QStringLiteral("name")); QDomText nametext = doc.createTextNode(name); effectname.appendChild(nametext); effect.insertBefore(effectname, QDomNode()); QDomElement effectprops = effect.firstChildElement(QStringLiteral("properties")); effectprops.setAttribute(QStringLiteral("id"), name); effectprops.setAttribute(QStringLiteral("type"), QStringLiteral("custom")); QFile file(dir.absoluteFilePath(name + QStringLiteral(".xml"))); if (file.open(QFile::WriteOnly | QFile::Truncate)) { QTextStream out(&file); out << doc.toString(); } file.close(); emit reloadEffect(dir.absoluteFilePath(name + QStringLiteral(".xml"))); } void CollapsibleEffectView::slotResetEffect() { m_view->resetValues(); } void CollapsibleEffectView::slotSwitch(bool collapse) { widgetFrame->setFixedHeight(collapse ? 0 : m_view->sizeHint().height()); setFixedHeight(widgetFrame->height() + frame->height() + (2 * decoframe->lineWidth())); // m_view->setVisible(!collapse); emit switchHeight(m_model, height()); m_model->setCollapsed(collapse); } void CollapsibleEffectView::animationChanged(const QVariant &geom) { parentWidget()->setFixedHeight(geom.toRect().height()); } void CollapsibleEffectView::animationFinished() { if (m_collapse->isActive()) { widgetFrame->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); } else { widgetFrame->setFixedHeight(m_view->contentHeight()); } } void CollapsibleEffectView::setGroupIndex(int ix) { Q_UNUSED(ix) /*if (m_info.groupIndex == -1 && ix != -1) { m_menu->removeAction(m_groupAction); } else if (m_info.groupIndex != -1 && ix == -1) { m_menu->addAction(m_groupAction); } m_info.groupIndex = ix; m_effect.setAttribute(QStringLiteral("kdenlive_info"), m_info.toString());*/ } void CollapsibleEffectView::setGroupName(const QString &groupName){ Q_UNUSED(groupName) /*m_info.groupName = groupName; m_effect.setAttribute(QStringLiteral("kdenlive_info"), m_info.toString());*/ } QString CollapsibleEffectView::infoString() const { return QString(); // m_info.toString(); } void CollapsibleEffectView::removeFromGroup() { /*if (m_info.groupIndex != -1) { m_menu->addAction(m_groupAction); } m_info.groupIndex = -1; m_info.groupName.clear(); m_effect.setAttribute(QStringLiteral("kdenlive_info"), m_info.toString()); emit parameterChanged(m_original_effect, m_effect, effectIndex());*/ } int CollapsibleEffectView::groupIndex() const { return -1; // m_info.groupIndex; } int CollapsibleEffectView::effectIndex() const { if (m_effect.isNull()) { return -1; } return m_effect.attribute(QStringLiteral("kdenlive_ix")).toInt(); } void CollapsibleEffectView::updateWidget(const ItemInfo &info, const QDomElement &effect) { // cleanup /* delete m_paramWidget; m_paramWidget = nullptr; */ m_effect = effect; setupWidget(info); } void CollapsibleEffectView::updateFrameInfo() { /* if (m_paramWidget) { m_paramWidget->refreshFrameInfo(); } */ } void CollapsibleEffectView::setActiveKeyframe(int kf) { Q_UNUSED(kf) /* if (m_paramWidget) { m_paramWidget->setActiveKeyframe(kf); } */ } void CollapsibleEffectView::setupWidget(const ItemInfo &info) { Q_UNUSED(info) /* if (m_effect.isNull()) { // //qCDebug(KDENLIVE_LOG) << "// EMPTY EFFECT STACK"; return; } delete m_paramWidget; m_paramWidget = nullptr; if (m_effect.attribute(QStringLiteral("tag")) == QLatin1String("region")) { m_regionEffect = true; QDomNodeList effects = m_effect.elementsByTagName(QStringLiteral("effect")); QDomNodeList origin_effects = m_original_effect.elementsByTagName(QStringLiteral("effect")); m_paramWidget = new ParameterContainer(m_effect, info, metaInfo, widgetFrame); QWidget *container = new QWidget(widgetFrame); QVBoxLayout *vbox = static_cast(widgetFrame->layout()); vbox->addWidget(container); // m_paramWidget = new ParameterContainer(m_effect.toElement(), info, metaInfo, container); for (int i = 0; i < effects.count(); ++i) { bool canMoveUp = true; if (i == 0 || effects.at(i - 1).toElement().attribute(QStringLiteral("id")) == QLatin1String("speed")) { canMoveUp = false; } CollapsibleEffectView *coll = new CollapsibleEffectView(effects.at(i).toElement(), origin_effects.at(i).toElement(), info, metaInfo, canMoveUp, i == effects.count() - 1, container); m_subParamWidgets.append(coll); connect(coll, &CollapsibleEffectView::parameterChanged, this, &CollapsibleEffectView::slotUpdateRegionEffectParams); // container = new QWidget(widgetFrame); vbox->addWidget(coll); // p = new ParameterContainer(effects.at(i).toElement(), info, isEffect, container); } } else { m_paramWidget = new ParameterContainer(m_effect, info, metaInfo, widgetFrame); connect(m_paramWidget, &ParameterContainer::disableCurrentFilter, this, &CollapsibleEffectView::slotDisable); connect(m_paramWidget, &ParameterContainer::importKeyframes, this, &CollapsibleEffectView::importKeyframes); if (m_effect.firstChildElement(QStringLiteral("parameter")).isNull()) { // Effect has no parameter, don't allow expand collapseButton->setEnabled(false); collapseButton->setVisible(false); widgetFrame->setVisible(false); } } if (collapseButton->isEnabled() && m_info.isCollapsed) { widgetFrame->setVisible(false); collapseButton->setArrowType(Qt::RightArrow); } connect(m_paramWidget, &ParameterContainer::parameterChanged, this, &CollapsibleEffectView::parameterChanged); connect(m_paramWidget, &ParameterContainer::startFilterJob, this, &CollapsibleEffectView::startFilterJob); connect(this, &CollapsibleEffectView::syncEffectsPos, m_paramWidget, &ParameterContainer::syncEffectsPos); connect(m_paramWidget, &ParameterContainer::checkMonitorPosition, this, &CollapsibleEffectView::checkMonitorPosition); connect(m_paramWidget, &ParameterContainer::seekTimeline, this, &CollapsibleEffectView::seekTimeline); connect(m_paramWidget, &ParameterContainer::importClipKeyframes, this, &CollapsibleEffectView::prepareImportClipKeyframes); */ } bool CollapsibleEffectView::isGroup() const { return false; } void CollapsibleEffectView::updateTimecodeFormat() { /* m_paramWidget->updateTimecodeFormat(); if (!m_subParamWidgets.isEmpty()) { // we have a group for (int i = 0; i < m_subParamWidgets.count(); ++i) { m_subParamWidgets.at(i)->updateTimecodeFormat(); } } */ } void CollapsibleEffectView::slotUpdateRegionEffectParams(const QDomElement & /*old*/, const QDomElement & /*e*/, int /*ix*/) { // qCDebug(KDENLIVE_LOG)<<"// EMIT CHANGE SUBEFFECT.....:"; emit parameterChanged(m_original_effect, m_effect, effectIndex()); } void CollapsibleEffectView::slotSyncEffectsPos(int pos) { emit syncEffectsPos(pos); } void CollapsibleEffectView::dragEnterEvent(QDragEnterEvent *event) { Q_UNUSED(event) /* if (event->mimeData()->hasFormat(QStringLiteral("kdenlive/effectslist"))) { frame->setProperty("target", true); frame->setStyleSheet(frame->styleSheet()); event->acceptProposedAction(); } else if (m_paramWidget->doesAcceptDrops() && event->mimeData()->hasFormat(QStringLiteral("kdenlive/geometry")) && event->source()->objectName() != QStringLiteral("ParameterContainer")) { event->setDropAction(Qt::CopyAction); event->setAccepted(true); } else { QWidget::dragEnterEvent(event); } */ } void CollapsibleEffectView::dragLeaveEvent(QDragLeaveEvent * /*event*/) { frame->setProperty("target", false); frame->setStyleSheet(frame->styleSheet()); } void CollapsibleEffectView::importKeyframes(const QString &kf) { QMap keyframes; if (kf.contains(QLatin1Char('\n'))) { const QStringList params = kf.split(QLatin1Char('\n'), QString::SkipEmptyParts); for (const QString ¶m : params) { keyframes.insert(param.section(QLatin1Char('='), 0, 0), param.section(QLatin1Char('='), 1)); } } else { keyframes.insert(kf.section(QLatin1Char('='), 0, 0), kf.section(QLatin1Char('='), 1)); } emit importClipKeyframes(AVWidget, m_itemInfo, m_effect.cloneNode().toElement(), keyframes); } void CollapsibleEffectView::dropEvent(QDropEvent *event) { if (event->mimeData()->hasFormat(QStringLiteral("kdenlive/geometry"))) { if (event->source()->objectName() == QStringLiteral("ParameterContainer")) { return; } // emit activateEffect(effectIndex()); QString itemData = event->mimeData()->data(QStringLiteral("kdenlive/geometry")); importKeyframes(itemData); return; } frame->setProperty("target", false); frame->setStyleSheet(frame->styleSheet()); const QString effects = QString::fromUtf8(event->mimeData()->data(QStringLiteral("kdenlive/effectslist"))); // event->acceptProposedAction(); QDomDocument doc; doc.setContent(effects, true); QDomElement e = doc.documentElement(); int ix = e.attribute(QStringLiteral("kdenlive_ix")).toInt(); int currentEffectIx = effectIndex(); if (ix == currentEffectIx || e.attribute(QStringLiteral("id")) == QLatin1String("speed")) { // effect dropped on itself, or unmovable speed dropped, reject event->ignore(); return; } if (ix == 0 || e.tagName() == QLatin1String("effectgroup")) { if (e.tagName() == QLatin1String("effectgroup")) { // moving a group QDomNodeList subeffects = e.elementsByTagName(QStringLiteral("effect")); if (subeffects.isEmpty()) { event->ignore(); return; } event->setDropAction(Qt::MoveAction); event->accept(); /* EffectInfo info; info.fromString(subeffects.at(0).toElement().attribute(QStringLiteral("kdenlive_info"))); if (info.groupIndex >= 0) { // Moving group QList effectsIds; // Collect moved effects ids for (int i = 0; i < subeffects.count(); ++i) { QDomElement effect = subeffects.at(i).toElement(); effectsIds << effect.attribute(QStringLiteral("kdenlive_ix")).toInt(); } // emit moveEffect(effectsIds, currentEffectIx, info.groupIndex, info.groupName); } else { // group effect dropped from effect list if (m_info.groupIndex > -1) { // TODO: Should we merge groups?? } emit addEffect(e); }*/ emit addEffect(e); return; } // effect dropped from effects list, add it e.setAttribute(QStringLiteral("kdenlive_ix"), ix); /*if (m_info.groupIndex > -1) { // Dropped on a group e.setAttribute(QStringLiteral("kdenlive_info"), m_info.toString()); }*/ event->setDropAction(Qt::CopyAction); event->accept(); emit addEffect(e); return; } // emit moveEffect(QList() << ix, currentEffectIx, m_info.groupIndex, m_info.groupName); event->setDropAction(Qt::MoveAction); event->accept(); } void CollapsibleEffectView::adjustButtons(int ix, int max) { buttonUp->setEnabled(ix > 0); buttonDown->setEnabled(ix < max - 1); } MonitorSceneType CollapsibleEffectView::needsMonitorEffectScene() const { if (!m_model->isEnabled() || !m_view) { return MonitorSceneDefault; } return m_view->needsMonitorEffectScene(); } void CollapsibleEffectView::setKeyframes(const QString &tag, const QString &keyframes) { Q_UNUSED(tag) Q_UNUSED(keyframes) /* m_paramWidget->setKeyframes(tag, keyframes); */ } bool CollapsibleEffectView::isMovable() const { return m_isMovable; } void CollapsibleEffectView::prepareImportClipKeyframes() { emit importClipKeyframes(AVWidget, m_itemInfo, m_effect.cloneNode().toElement(), QMap()); } void CollapsibleEffectView::enableView(bool enabled) { m_enabledButton->setActive(enabled); title->setEnabled(!enabled); m_colorIcon->setEnabled(!enabled); if (enabled) { if (KdenliveSettings::disable_effect_parameters()) { widgetFrame->setEnabled(false); } } else { widgetFrame->setEnabled(true); } } diff --git a/src/effects/effectstack/view/collapsibleeffectview.hpp b/src/effects/effectstack/view/collapsibleeffectview.hpp index d84b6ce6f..ae030245a 100644 --- a/src/effects/effectstack/view/collapsibleeffectview.hpp +++ b/src/effects/effectstack/view/collapsibleeffectview.hpp @@ -1,167 +1,167 @@ /*************************************************************************** * Copyright (C) 2017 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 . * ***************************************************************************/ #ifndef COLLAPSIBLEEFFECTVIEW_H #define COLLAPSIBLEEFFECTVIEW_H #include "abstractcollapsiblewidget.h" #include "definitions.h" #include "timecode.h" #include #include class QLabel; class KDualAction; class EffectItemModel; class AssetParameterView; /**) * @class CollapsibleEffectView * @brief A container for the parameters of an effect * @author Jean-Baptiste Mardelle */ class CollapsibleEffectView : public AbstractCollapsibleWidget { Q_OBJECT public: - explicit CollapsibleEffectView(std::shared_ptr effectModel, QSize frameSize, QImage icon, QWidget *parent = nullptr); + explicit CollapsibleEffectView(const std::shared_ptr &effectModel, QSize frameSize, const QImage &icon, QWidget *parent = nullptr); ~CollapsibleEffectView(); QLabel *title; void setupWidget(const ItemInfo &info); void updateTimecodeFormat(); /** @brief Install event filter so that scrolling with mouse wheel does not change parameter value. */ bool eventFilter(QObject *o, QEvent *e) override; /** @brief Update effect GUI to reflect parameted changes. */ void updateWidget(const ItemInfo &info, const QDomElement &effect); /** @brief Returns effect xml. */ QDomElement effect() const; /** @brief Returns effect xml with keyframe offset for saving. */ QDomElement effectForSave() const; int groupIndex() const; bool isGroup() const override; int effectIndex() const; void setGroupIndex(int ix); void setGroupName(const QString &groupName); /** @brief Remove this effect from its group. */ void removeFromGroup(); QString infoString() const; bool isActive() const; bool isEnabled() const; /** @brief Should the wheel event be sent to parent widget for scrolling. */ bool filterWheelEvent; /** @brief Show / hide up / down buttons. */ void adjustButtons(int ix, int max); /** @brief Returns this effect's monitor scene type if any is needed. */ MonitorSceneType needsMonitorEffectScene() const; /** @brief Import keyframes from a clip's data. */ void setKeyframes(const QString &tag, const QString &keyframes); /** @brief Pass frame size info (dar, etc). */ void updateFrameInfo(); /** @brief Select active keyframe. */ void setActiveKeyframe(int kf); /** @brief Returns true if effect can be moved (false for speed effect). */ bool isMovable() const; public slots: void slotSyncEffectsPos(int pos); void slotDisable(bool disable); void slotResetEffect(); void importKeyframes(const QString &keyframes); void slotActivateEffect(QModelIndex ix); private slots: void setWidgetHeight(qreal value); void animationFinished(); void enableView(bool enabled); private slots: void slotSwitch(bool expand); void slotDeleteEffect(); void slotEffectUp(); void slotEffectDown(); void slotSaveEffect(); void slotCreateGroup(); void slotCreateRegion(); void slotUnGroup(); /** @brief A sub effect parameter was changed */ void slotUpdateRegionEffectParams(const QDomElement & /*old*/, const QDomElement & /*e*/, int /*ix*/); void prepareImportClipKeyframes(); void animationChanged(const QVariant &geom); private: AssetParameterView *m_view; std::shared_ptr m_model; KDualAction *m_collapse; QToolButton *m_keyframesButton; QList m_subParamWidgets; QDomElement m_effect; ItemInfo m_itemInfo; QDomElement m_original_effect; QList m_subEffects; QMenu *m_menu; bool m_isMovable; /** @brief True if this is a region effect, which behaves in a special way, like a group. */ bool m_regionEffect; /** @brief The add group action. */ QAction *m_groupAction; KDualAction *m_enabledButton; QLabel *m_colorIcon; QPixmap m_iconPix; QPoint m_dragStart; protected: void mouseDoubleClickEvent(QMouseEvent *event) override; void mousePressEvent(QMouseEvent *event) override; void mouseMoveEvent(QMouseEvent *e) override; void mouseReleaseEvent(QMouseEvent *event) override; void dragEnterEvent(QDragEnterEvent *event) override; void dragLeaveEvent(QDragLeaveEvent *event) override; void dropEvent(QDropEvent *event) override; signals: void parameterChanged(const QDomElement &, const QDomElement &, int); void syncEffectsPos(int); void effectStateChanged(bool, int ix, MonitorSceneType effectNeedsMonitorScene); void deleteEffect(std::shared_ptr effect); void moveEffect(int destRow, std::shared_ptr effect); void checkMonitorPosition(int); void seekTimeline(int); /** @brief Start an MLT filter job on this clip. */ void startFilterJob(QMap &, QMap &, QMap &); /** @brief An effect was reset, trigger param reload. */ void resetEffect(int ix); /** @brief Ask for creation of a group. */ void createGroup(std::shared_ptr effectModel); void unGroup(CollapsibleEffectView *); void createRegion(int, const QUrl &); void deleteGroup(const QDomDocument &); void importClipKeyframes(GraphicsRectItem, ItemInfo, QDomElement, const QMap &keyframes = QMap()); void switchHeight(std::shared_ptr model, int height); void startDrag(QPixmap, std::shared_ptr effectModel); void activateEffect(std::shared_ptr effectModel); void refresh(); }; #endif diff --git a/src/effects/effectstack/view/effectstackview.cpp b/src/effects/effectstack/view/effectstackview.cpp index 8bdcde876..3497217e0 100644 --- a/src/effects/effectstack/view/effectstackview.cpp +++ b/src/effects/effectstack/view/effectstackview.cpp @@ -1,378 +1,378 @@ /*************************************************************************** * Copyright (C) 2017 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 "effectstackview.hpp" #include "assets/assetlist/view/qmltypes/asseticonprovider.hpp" #include "assets/assetpanel.hpp" #include "assets/view/assetparameterview.hpp" #include "builtstack.hpp" #include "collapsibleeffectview.hpp" #include "core.h" #include "effects/effectstack/model/effectitemmodel.hpp" #include "effects/effectstack/model/effectstackmodel.hpp" #include "kdenlivesettings.h" #include "monitor/monitor.h" #include #include #include #include #include #include #include - +#include WidgetDelegate::WidgetDelegate(QObject *parent) : QStyledItemDelegate(parent) { } QSize WidgetDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const { QSize s = QStyledItemDelegate::sizeHint(option, index); if (m_height.contains(index)) { s.setHeight(m_height.value(index)); } return s; } void WidgetDelegate::setHeight(const QModelIndex &index, int height) { m_height[index] = height; emit sizeHintChanged(index); } void WidgetDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { QStyleOptionViewItem opt(option); initStyleOption(&opt, index); QStyle *style = opt.widget ? opt.widget->style() : QApplication::style(); style->drawPrimitive(QStyle::PE_PanelItemViewItem, &opt, painter, opt.widget); } EffectStackView::EffectStackView(AssetPanel *parent) : QWidget(parent) , m_model(nullptr) , m_thumbnailer(new AssetIconProvider(true)) { setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); m_lay = new QVBoxLayout(this); m_lay->setContentsMargins(0, 0, 0, 0); m_lay->setSpacing(0); setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)); setAcceptDrops(true); /*m_builtStack = new BuiltStack(parent); m_builtStack->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); m_lay->addWidget(m_builtStack); m_builtStack->setVisible(KdenliveSettings::showbuiltstack());*/ m_effectsTree = new QTreeView(this); m_effectsTree->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); m_effectsTree->setHeaderHidden(true); m_effectsTree->setRootIsDecorated(false); QString style = QStringLiteral("QTreeView {border: none;}"); // m_effectsTree->viewport()->setAutoFillBackground(false); m_effectsTree->setStyleSheet(style); m_effectsTree->setVisible(!KdenliveSettings::showbuiltstack()); m_lay->addWidget(m_effectsTree); m_lay->setStretch(1, 10); } EffectStackView::~EffectStackView() { delete m_thumbnailer; } void EffectStackView::dragEnterEvent(QDragEnterEvent *event) { if (event->mimeData()->hasFormat(QStringLiteral("kdenlive/effect"))) { if (event->source() == this) { event->setDropAction(Qt::MoveAction); } else { event->setDropAction(Qt::CopyAction); } event->setAccepted(true); } else { event->setAccepted(false); } } void EffectStackView::dropEvent(QDropEvent *event) { event->accept(); QString effectId = event->mimeData()->data(QStringLiteral("kdenlive/effect")); int row = m_model->rowCount(); for (int i = 0; i < m_model->rowCount(); i++) { auto item = m_model->getEffectStackRow(i); if (item->childCount() > 0) { // TODO: group continue; } std::shared_ptr eff = std::static_pointer_cast(item); QModelIndex ix = m_model->getIndexFromItem(eff); QWidget *w = m_effectsTree->indexWidget(ix); if (w && w->geometry().contains(event->pos())) { qDebug() << "// DROPPED ON EFF: " << eff->getAssetId(); row = i; break; } } if (event->source() == this) { QString sourceData = event->mimeData()->data(QStringLiteral("kdenlive/effectsource")); int oldRow = sourceData.section(QLatin1Char('-'), 2, 2).toInt(); qDebug() << "// MOVING EFFECT FROM : " << oldRow << " TO " << row; if (row == oldRow || (row == m_model->rowCount() && oldRow == row - 1)) { return; } m_model->moveEffect(row, m_model->getEffectStackRow(oldRow)); } else { bool added = false; if (row < m_model->rowCount()) { if (m_model->appendEffect(effectId)) { added = true; m_model->moveEffect(row, m_model->getEffectStackRow(m_model->rowCount() - 1)); } } else { if (m_model->appendEffect(effectId)) { added = true; std::shared_ptr item = m_model->getEffectStackRow(m_model->rowCount() - 1); if (item) { slotActivateEffect(std::static_pointer_cast(item)); } } } if (!added) { pCore->displayMessage(i18n("Cannot add effect to clip"), InformationMessage); } } } void EffectStackView::setModel(std::shared_ptr model, const QSize frameSize) { qDebug() << "MUTEX LOCK!!!!!!!!!!!! setmodel"; m_mutex.lock(); unsetModel(false); - m_model = model; + m_model = std::move(model); m_sourceFrameSize = frameSize; m_effectsTree->setModel(m_model.get()); m_effectsTree->setItemDelegateForColumn(0, new WidgetDelegate(this)); m_effectsTree->setColumnHidden(1, true); m_effectsTree->setAcceptDrops(true); m_effectsTree->setDragDropMode(QAbstractItemView::DragDrop); m_effectsTree->setDragEnabled(true); m_effectsTree->setUniformRowHeights(false); m_mutex.unlock(); qDebug() << "MUTEX UNLOCK!!!!!!!!!!!! setmodel"; loadEffects(); connect(m_model.get(), &EffectStackModel::dataChanged, this, &EffectStackView::refresh); connect(m_model.get(), &EffectStackModel::enabledStateChanged, this, &EffectStackView::updateEnabledState); connect(this, &EffectStackView::removeCurrentEffect, m_model.get(), &EffectStackModel::removeCurrentEffect); // m_builtStack->setModel(model, stackOwner()); } void EffectStackView::loadEffects() { qDebug() << "MUTEX LOCK!!!!!!!!!!!! loadEffects: "; QMutexLocker lock(&m_mutex); int max = m_model->rowCount(); if (max == 0) { // blank stack ObjectId item = m_model->getOwnerId(); pCore->getMonitor(item.first == ObjectType::BinClip ? Kdenlive::ClipMonitor : Kdenlive::ProjectMonitor)->slotShowEffectScene(MonitorSceneDefault); return; } int active = qBound(0, m_model->getActiveEffect(), max - 1); for (int i = 0; i < max; i++) { std::shared_ptr item = m_model->getEffectStackRow(i); QSize size; if (item->childCount() > 0) { // group, create sub stack continue; } std::shared_ptr effectModel = std::static_pointer_cast(item); CollapsibleEffectView *view = nullptr; // We need to rebuild the effect view QImage effectIcon = m_thumbnailer->requestImage(effectModel->getAssetId(), &size, QSize(QStyle::PM_SmallIconSize, QStyle::PM_SmallIconSize)); view = new CollapsibleEffectView(effectModel, m_sourceFrameSize, effectIcon, this); connect(view, &CollapsibleEffectView::deleteEffect, m_model.get(), &EffectStackModel::removeEffect); connect(view, &CollapsibleEffectView::moveEffect, m_model.get(), &EffectStackModel::moveEffect); connect(view, &CollapsibleEffectView::reloadEffect, this, &EffectStackView::reloadEffect); connect(view, &CollapsibleEffectView::switchHeight, this, &EffectStackView::slotAdjustDelegate, Qt::DirectConnection); connect(view, &CollapsibleEffectView::startDrag, this, &EffectStackView::slotStartDrag); connect(view, &CollapsibleEffectView::createGroup, m_model.get(), &EffectStackModel::slotCreateGroup); connect(view, &CollapsibleEffectView::activateEffect, this, &EffectStackView::slotActivateEffect); connect(view, &CollapsibleEffectView::seekToPos, [this](int pos) { // at this point, the effects returns a pos relative to the clip. We need to convert it to a global time int clipIn = pCore->getItemPosition(m_model->getOwnerId()); emit seekToPos(pos + clipIn); }); connect(this, &EffectStackView::doActivateEffect, view, &CollapsibleEffectView::slotActivateEffect); QModelIndex ix = m_model->getIndexFromItem(effectModel); m_effectsTree->setIndexWidget(ix, view); WidgetDelegate *del = static_cast(m_effectsTree->itemDelegate(ix)); del->setHeight(ix, view->height()); view->buttonUp->setEnabled(i > 0); view->buttonDown->setEnabled(i < max - 1); if (i == active) { m_model->setActiveEffect(i); emit doActivateEffect(ix); } } updateTreeHeight(); qDebug() << "MUTEX UNLOCK!!!!!!!!!!!! loadEffects"; } void EffectStackView::updateTreeHeight() { // For some reason, the treeview height does not update correctly, so enforce it int totalHeight = 0; for (int j = 0; j < m_model->rowCount(); j++) { std::shared_ptr item2 = m_model->getEffectStackRow(j); std::shared_ptr eff = std::static_pointer_cast(item2); QModelIndex idx = m_model->getIndexFromItem(eff); auto w = m_effectsTree->indexWidget(idx); totalHeight += w->height(); } setMinimumHeight(totalHeight); } -void EffectStackView::slotActivateEffect(std::shared_ptr effectModel) +void EffectStackView::slotActivateEffect(const std::shared_ptr &effectModel) { qDebug() << "MUTEX LOCK!!!!!!!!!!!! slotactivateeffect: " << effectModel->row(); QMutexLocker lock(&m_mutex); m_model->setActiveEffect(effectModel->row()); QModelIndex activeIx = m_model->getIndexFromItem(effectModel); emit doActivateEffect(activeIx); qDebug() << "MUTEX UNLOCK!!!!!!!!!!!! slotactivateeffect"; } -void EffectStackView::slotStartDrag(QPixmap pix, std::shared_ptr effectModel) +void EffectStackView::slotStartDrag(const QPixmap &pix, const std::shared_ptr &effectModel) { auto *drag = new QDrag(this); drag->setPixmap(pix); auto *mime = new QMimeData; mime->setData(QStringLiteral("kdenlive/effect"), effectModel->getAssetId().toUtf8()); // TODO this will break if source effect is not on the stack of a timeline clip QByteArray effectSource; effectSource += QString::number((int)effectModel->getOwnerId().first).toUtf8(); effectSource += '-'; effectSource += QString::number((int)effectModel->getOwnerId().second).toUtf8(); effectSource += '-'; effectSource += QString::number(effectModel->row()).toUtf8(); mime->setData(QStringLiteral("kdenlive/effectsource"), effectSource); // mime->setData(QStringLiteral("kdenlive/effectrow"), QString::number(effectModel->row()).toUtf8()); // Assign ownership of the QMimeData object to the QDrag object. drag->setMimeData(mime); // Start the drag and drop operation drag->exec(Qt::CopyAction | Qt::MoveAction, Qt::CopyAction); } -void EffectStackView::slotAdjustDelegate(std::shared_ptr effectModel, int height) +void EffectStackView::slotAdjustDelegate(const std::shared_ptr &effectModel, int height) { qDebug() << "MUTEX LOCK!!!!!!!!!!!! adjustdelegate: " << height; QMutexLocker lock(&m_mutex); QModelIndex ix = m_model->getIndexFromItem(effectModel); WidgetDelegate *del = static_cast(m_effectsTree->itemDelegate(ix)); del->setHeight(ix, height); updateTreeHeight(); qDebug() << "MUTEX UNLOCK!!!!!!!!!!!! adjustdelegate"; } void EffectStackView::refresh(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles) { Q_UNUSED(roles) if (!topLeft.isValid() || !bottomRight.isValid()) { loadEffects(); return; } for (int i = topLeft.row(); i <= bottomRight.row(); ++i) { for (int j = topLeft.column(); j <= bottomRight.column(); ++j) { CollapsibleEffectView *w = static_cast(m_effectsTree->indexWidget(m_model->index(i, j, topLeft.parent()))); if (w) { w->refresh(); } } } } void EffectStackView::unsetModel(bool reset) { // Release ownership of smart pointer Kdenlive::MonitorId id = Kdenlive::NoMonitor; if (m_model) { ObjectId item = m_model->getOwnerId(); id = item.first == ObjectType::BinClip ? Kdenlive::ClipMonitor : Kdenlive::ProjectMonitor; disconnect(m_model.get(), &EffectStackModel::dataChanged, this, &EffectStackView::refresh); disconnect(this, &EffectStackView::removeCurrentEffect, m_model.get(), &EffectStackModel::removeCurrentEffect); } if (reset) { QMutexLocker lock(&m_mutex); m_model.reset(); m_effectsTree->setModel(nullptr); } if (id != Kdenlive::NoMonitor) { pCore->getMonitor(id)->slotShowEffectScene(MonitorSceneDefault); } } ObjectId EffectStackView::stackOwner() const { if (m_model) { return m_model->getOwnerId(); } return ObjectId(ObjectType::NoItem, -1); } bool EffectStackView::addEffect(const QString &effectId) { if (m_model) { return m_model->appendEffect(effectId); } return false; } bool EffectStackView::isEmpty() const { return m_model == nullptr ? true : m_model->rowCount() == 0; } void EffectStackView::enableStack(bool enable) { if (m_model) { m_model->setEffectStackEnabled(enable); } } bool EffectStackView::isStackEnabled() const { if (m_model) { return m_model->isStackEnabled(); } return false; } /* void EffectStackView::switchBuiltStack(bool show) { m_builtStack->setVisible(show); m_effectsTree->setVisible(!show); KdenliveSettings::setShowbuiltstack(show); } */ diff --git a/src/effects/effectstack/view/effectstackview.hpp b/src/effects/effectstack/view/effectstackview.hpp index 4d21115ac..d83c4d7ef 100644 --- a/src/effects/effectstack/view/effectstackview.hpp +++ b/src/effects/effectstack/view/effectstackview.hpp @@ -1,111 +1,111 @@ /*************************************************************************** * Copyright (C) 2017 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 . * ***************************************************************************/ #ifndef EFFECTSTACKVIEW_H #define EFFECTSTACKVIEW_H #include "definitions.h" #include #include #include #include class QVBoxLayout; class QTreeView; class CollapsibleEffectView; class AssetParameterModel; class EffectStackModel; class EffectItemModel; class AssetIconProvider; class BuiltStack; class AssetPanel; class WidgetDelegate : public QStyledItemDelegate { Q_OBJECT public: explicit WidgetDelegate(QObject *parent = nullptr); void setHeight(const QModelIndex &index, int height); QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; private: QMap m_height; }; class EffectStackView : public QWidget { Q_OBJECT public: EffectStackView(AssetPanel *parent); virtual ~EffectStackView(); void setModel(std::shared_ptr model, const QSize frameSize); void unsetModel(bool reset = true); ObjectId stackOwner() const; /** @brief Add an effect to the current stack */ bool addEffect(const QString &effectId); /** @brief Returns true if effectstack is empty */ bool isEmpty() const; /** @brief Enables / disables the stack */ void enableStack(bool enable); bool isStackEnabled() const; protected: void dragEnterEvent(QDragEnterEvent *event) override; void dropEvent(QDropEvent *event) override; private: QMutex m_mutex; QVBoxLayout *m_lay; // BuiltStack *m_builtStack; QTreeView *m_effectsTree; std::shared_ptr m_model; std::vector m_widgets; AssetIconProvider *m_thumbnailer; /** @brief the frame size of the original clip this effect is applied on */ QSize m_sourceFrameSize; const QString getStyleSheet(); void updateTreeHeight(); private slots: void refresh(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles); - void slotAdjustDelegate(std::shared_ptr effectModel, int height); - void slotStartDrag(QPixmap pix, std::shared_ptr effectModel); - void slotActivateEffect(std::shared_ptr effectModel); + void slotAdjustDelegate(const std::shared_ptr &effectModel, int height); + void slotStartDrag(const QPixmap &pix, const std::shared_ptr &effectModel); + void slotActivateEffect(const std::shared_ptr &effectModel); void loadEffects(); // void switchBuiltStack(bool show); signals: void doActivateEffect(QModelIndex); void seekToPos(int); void reloadEffect(const QString &path); void updateEnabledState(); void removeCurrentEffect(); }; #endif diff --git a/src/jobs/abstractclipjob.cpp b/src/jobs/abstractclipjob.cpp index 305bb3f65..5c1aacc0f 100644 --- a/src/jobs/abstractclipjob.cpp +++ b/src/jobs/abstractclipjob.cpp @@ -1,58 +1,58 @@ /*************************************************************************** * * * 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 "abstractclipjob.h" #include "doc/kdenlivedoc.h" #include "kdenlivesettings.h" AbstractClipJob::AbstractClipJob(JOBTYPE type, const QString &id, QObject *parent) : QObject(parent) , m_clipId(id) , m_jobType(type) { } AbstractClipJob::~AbstractClipJob() {} const QString AbstractClipJob::clipId() const { return m_clipId; } const QString AbstractClipJob::getErrorMessage() const { return m_errorMessage; } const QString AbstractClipJob::getLogDetails() const { return m_logDetails; } // static -bool AbstractClipJob::execute(std::shared_ptr job) +bool AbstractClipJob::execute(const std::shared_ptr &job) { return job->startJob(); } AbstractClipJob::JOBTYPE AbstractClipJob::jobType() const { return m_jobType; } diff --git a/src/jobs/abstractclipjob.h b/src/jobs/abstractclipjob.h index 98fe8b50c..88ec3e2fa 100644 --- a/src/jobs/abstractclipjob.h +++ b/src/jobs/abstractclipjob.h @@ -1,100 +1,100 @@ /*************************************************************************** * * * 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 * ***************************************************************************/ #ifndef ABSTRACTCLIPJOB #define ABSTRACTCLIPJOB #include #include #include "definitions.h" #include "undohelper.hpp" #include /** * @class AbstractClipJob * @brief This is the base class for all Kdenlive clip jobs. * */ struct Job_t; class AbstractClipJob : public QObject { Q_OBJECT public: enum JOBTYPE { NOJOBTYPE = 0, PROXYJOB = 1, CUTJOB = 2, STABILIZEJOB = 3, TRANSCODEJOB = 4, FILTERCLIPJOB = 5, THUMBJOB = 6, ANALYSECLIPJOB = 7, LOADJOB = 8, AUDIOTHUMBJOB = 9, SPEEDJOB = 10 }; AbstractClipJob(JOBTYPE type, const QString &id, QObject *parent = nullptr); virtual ~AbstractClipJob(); template static std::shared_ptr make(const QString &binId, Args &&... args) { auto m = std::make_shared(binId, std::forward(args)...); return m; } /** @brief Returns the id of the bin clip that this job is working on. */ const QString clipId() const; const QString getErrorMessage() const; const QString getLogDetails() const; virtual const QString getDescription() const = 0; virtual bool startJob() = 0; /** @brief This is to be called after the job finished. By design, the job should store the result of the computation but not share it with the rest of the code. This happens when we call commitResult This methods return true on success */ virtual bool commitResult(Fun &undo, Fun &redo) = 0; // brief run a given job - static bool execute(std::shared_ptr job); + static bool execute(const std::shared_ptr &job); /* @brief return the type of this job */ JOBTYPE jobType() const; protected: QString m_clipId; QString m_errorMessage; QString m_logDetails; int m_addClipToProject; JOBTYPE m_jobType; bool m_resultConsumed{false}; signals: // send an int between 0 and 100 to reflect computation progress void jobProgress(int); void jobCanceled(); }; #endif diff --git a/src/jobs/jobmanager.cpp b/src/jobs/jobmanager.cpp index 7197cf9c5..ca64df7f0 100644 --- a/src/jobs/jobmanager.cpp +++ b/src/jobs/jobmanager.cpp @@ -1,451 +1,451 @@ /* Copyright (C) 2014 Jean-Baptiste Mardelle Copyright (C) 2017 Nicolas Carion This file is part of Kdenlive. See www.kdenlive.org. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "jobmanager.h" #include "bin/abstractprojectitem.h" #include "bin/projectclip.h" #include "bin/projectitemmodel.h" #include "core.h" #include "macros.hpp" #include "undohelper.hpp" #include #include #include #include int JobManager::m_currentId = 0; JobManager::JobManager(QObject *parent) : QAbstractListModel(parent) , m_lock(QReadWriteLock::Recursive) { } JobManager::~JobManager() { slotCancelJobs(); } int JobManager::getBlockingJobId(const QString &id, AbstractClipJob::JOBTYPE type) { READ_LOCK(); std::vector result; if (m_jobsByClip.count(id) > 0) { for (int jobId : m_jobsByClip.at(id)) { if (!m_jobs.at(jobId)->m_future.isFinished() && !m_jobs.at(jobId)->m_future.isCanceled()) { if (type == AbstractClipJob::NOJOBTYPE || m_jobs.at(jobId)->m_type == type) { return jobId; } } } } return -1; } std::vector JobManager::getPendingJobsIds(const QString &id, AbstractClipJob::JOBTYPE type) { READ_LOCK(); std::vector result; if (m_jobsByClip.count(id) > 0) { for (int jobId : m_jobsByClip.at(id)) { if (!m_jobs.at(jobId)->m_future.isFinished() && !m_jobs.at(jobId)->m_future.isCanceled()) { if (type == AbstractClipJob::NOJOBTYPE || m_jobs.at(jobId)->m_type == type) { result.push_back(jobId); } } } } return result; } std::vector JobManager::getFinishedJobsIds(const QString &id, AbstractClipJob::JOBTYPE type) { READ_LOCK(); std::vector result; if (m_jobsByClip.count(id) > 0) { for (int jobId : m_jobsByClip.at(id)) { if (m_jobs.at(jobId)->m_future.isFinished() || m_jobs.at(jobId)->m_future.isCanceled()) { if (type == AbstractClipJob::NOJOBTYPE || m_jobs.at(jobId)->m_type == type) { result.push_back(jobId); } } } } return result; } void JobManager::discardJobs(const QString &binId, AbstractClipJob::JOBTYPE type) { QWriteLocker locker(&m_lock); if (m_jobsByClip.count(binId) == 0) { return; } for (int jobId : m_jobsByClip.at(binId)) { if (type == AbstractClipJob::NOJOBTYPE || m_jobs.at(jobId)->m_type == type) { - for (std::shared_ptr job : m_jobs.at(jobId)->m_job) { + for (const std::shared_ptr &job : m_jobs.at(jobId)->m_job) { job->jobCanceled(); } m_jobs.at(jobId)->m_future.cancel(); } } } bool JobManager::hasPendingJob(const QString &clipId, AbstractClipJob::JOBTYPE type, int *foundId) { READ_LOCK(); if (m_jobsByClip.count(clipId) > 0) { for (int jobId : m_jobsByClip.at(clipId)) { if ((type == AbstractClipJob::NOJOBTYPE || m_jobs.at(jobId)->m_type == type) && !m_jobs.at(jobId)->m_future.isFinished() && !m_jobs.at(jobId)->m_future.isCanceled()) { if (foundId) { *foundId = jobId; } return true; } } if (foundId) { *foundId = -1; } } return false; } void JobManager::updateJobCount() { READ_LOCK(); int count = 0; for (const auto &j : m_jobs) { if (!j.second->m_future.isFinished() && !j.second->m_future.isCanceled()) { count++; /*for (int i = 0; i < j.second->m_future.future().resultCount(); ++i) { if (j.second->m_future.future().isResultReadyAt(i)) { count++; } }*/ } } // Set jobs count emit jobCount(count); } /* void JobManager::prepareJobs(const QList &clips, double fps, AbstractClipJob::JOBTYPE jobType, const QStringList ¶ms) { // TODO filter clips QList matching = filterClips(clips, jobType, params); if (matching.isEmpty()) { m_bin->doDisplayMessage(i18n("No valid clip to process"), KMessageWidget::Information); return; } QHash jobs; if (jobType == AbstractClipJob::TRANSCODEJOB) { jobs = CutClipJob::prepareTranscodeJob(fps, matching, params); } else if (jobType == AbstractClipJob::CUTJOB) { ProjectClip *clip = matching.constFirst(); double originalFps = clip->getOriginalFps(); jobs = CutClipJob::prepareCutClipJob(fps, originalFps, clip); } else if (jobType == AbstractClipJob::ANALYSECLIPJOB) { jobs = CutClipJob::prepareAnalyseJob(fps, matching, params); } else if (jobType == AbstractClipJob::FILTERCLIPJOB) { jobs = FilterJob::prepareJob(matching, params); } else if (jobType == AbstractClipJob::PROXYJOB) { jobs = ProxyJob::prepareJob(m_bin, matching); } if (!jobs.isEmpty()) { QHashIterator i(jobs); while (i.hasNext()) { i.next(); launchJob(i.key(), i.value(), false); } slotCheckJobProcess(); } } */ void JobManager::slotDiscardClipJobs(const QString &binId) { QWriteLocker locker(&m_lock); if (m_jobsByClip.count(binId) > 0) { for (int jobId : m_jobsByClip.at(binId)) { Q_ASSERT(m_jobs.count(jobId) > 0); - for (std::shared_ptr job : m_jobs.at(jobId)->m_job) { + for (const std::shared_ptr &job : m_jobs.at(jobId)->m_job) { job->jobCanceled(); } m_jobs[jobId]->m_future.cancel(); } } } void JobManager::slotCancelPendingJobs() { QWriteLocker locker(&m_lock); for (const auto &j : m_jobs) { if (!j.second->m_future.isStarted()) { - for (std::shared_ptr job : j.second->m_job) { + for (const std::shared_ptr &job : j.second->m_job) { job->jobCanceled(); } j.second->m_future.cancel(); } } } void JobManager::slotCancelJobs() { QWriteLocker locker(&m_lock); for (const auto &j : m_jobs) { - for (std::shared_ptr job : j.second->m_job) { + for (const std::shared_ptr &job : j.second->m_job) { job->jobCanceled(); } j.second->m_future.cancel(); } } -void JobManager::createJob(std::shared_ptr job) +void JobManager::createJob(const std::shared_ptr &job) { /* // This thread wait mechanism was broken and caused a race condition locking the application // so I switched to a simpler model bool ok = false; // wait for parents to finish while (!ok) { ok = true; for (int p : parents) { if (!m_jobs[p]->m_completionMutex.tryLock()) { ok = false; qDebug()<<"********\nWAITING FOR JOB COMPLETION MUTEX!!: "<m_id<<" : "<m_id<<"="<m_type; break; } else { qDebug()<<">>>>>>>>>>\nJOB COMPLETION MUTEX DONE: "<m_id; m_jobs[p]->m_completionMutex.unlock(); } } if (!ok) { QThread::msleep(10); } }*/ // connect progress signals QReadLocker locker(&m_lock); for (const auto &it : job->m_indices) { size_t i = it.second; auto binId = it.first; connect(job->m_job[i].get(), &AbstractClipJob::jobProgress, [job, i, binId](int p) { job->m_progress[i] = std::max(job->m_progress[i], p); pCore->projectItemModel()->onItemUpdated(binId, AbstractProjectItem::JobProgress); }); } connect(&job->m_future, &QFutureWatcher::started, this, &JobManager::updateJobCount); connect(&job->m_future, &QFutureWatcher::finished, [this, id = job->m_id]() { slotManageFinishedJob(id); }); connect(&job->m_future, &QFutureWatcher::canceled, [this, id = job->m_id]() { slotManageCanceledJob(id); }); job->m_actualFuture = QtConcurrent::mapped(job->m_job, AbstractClipJob::execute); job->m_future.setFuture(job->m_actualFuture); // In the unlikely event that the job finished before the signal connection was made, we check manually for finish and cancel /*if (job->m_future.isFinished()) { //emit job->m_future.finished(); slotManageFinishedJob(job->m_id); } if (job->m_future.isCanceled()) { //emit job->m_future.canceled(); slotManageCanceledJob(job->m_id); }*/ } void JobManager::slotManageCanceledJob(int id) { QReadLocker locker(&m_lock); Q_ASSERT(m_jobs.count(id) > 0); if (m_jobs[id]->m_processed) return; m_jobs[id]->m_processed = true; m_jobs[id]->m_completionMutex.unlock(); // send notification to refresh view for (const auto &it : m_jobs[id]->m_indices) { pCore->projectItemModel()->onItemUpdated(it.first, AbstractProjectItem::JobStatus); } // TODO: delete child jobs updateJobCount(); } void JobManager::slotManageFinishedJob(int id) { qDebug() << "################### JOB finished" << id; QReadLocker locker(&m_lock); Q_ASSERT(m_jobs.count(id) > 0); if (m_jobs[id]->m_processed) return; // send notification to refresh view for (const auto &it : m_jobs[id]->m_indices) { pCore->projectItemModel()->onItemUpdated(it.first, AbstractProjectItem::JobStatus); } bool ok = true; for (bool res : m_jobs[id]->m_future.future()) { ok = ok && res; } Fun undo = []() { return true; }; Fun redo = []() { return true; }; if (!ok) { qDebug() << " * * * ** * * *\nWARNING + + +\nJOB NOT CORRECT FINISH: " << id << "\n------------------------"; // TODO: delete child jobs m_jobs[id]->m_completionMutex.unlock(); locker.unlock(); if (m_jobs.at(id)->m_type == AbstractClipJob::LOADJOB) { // loading failed, remove clip for (const auto &it : m_jobs[id]->m_indices) { std::shared_ptr item = pCore->projectItemModel()->getItemByBinId(it.first); if (item && item->itemType() == AbstractProjectItem::ClipItem) { auto clipItem = std::static_pointer_cast(item); if (!clipItem->isReady()) { // We were trying to load a new clip, delete it pCore->projectItemModel()->requestBinClipDeletion(item, undo, redo); } } } } else { QString bid; for (const auto &it : m_jobs.at(id)->m_indices) { bid = it.first; break; } QPair message = getJobMessageForClip(id, bid); if (!message.first.isEmpty()) { if (!message.second.isEmpty()) { pCore->displayBinLogMessage(message.first, KMessageWidget::Warning, message.second); } else { pCore->displayBinMessage(message.first, KMessageWidget::Warning); } } } updateJobCount(); return; } // unlock mutex to allow further processing // TODO: the lock mechanism should handle this better! locker.unlock(); for (const auto &j : m_jobs[id]->m_job) { ok = ok && j->commitResult(undo, redo); } m_jobs[id]->m_processed = true; if (!ok) { m_jobs[id]->m_failed = true; QString bid; for (const auto &it : m_jobs.at(id)->m_indices) { bid = it.first; break; } qDebug() << "ERROR: Job " << id << " failed, BID: " << bid; QPair message = getJobMessageForClip(id, bid); if (!message.first.isEmpty()) { if (!message.second.isEmpty()) { pCore->displayBinLogMessage(message.first, KMessageWidget::Warning, message.second); } else { pCore->displayBinMessage(message.first, KMessageWidget::Warning); } } } m_jobs[id]->m_completionMutex.unlock(); if (ok && !m_jobs[id]->m_undoString.isEmpty()) { pCore->pushUndo(undo, redo, m_jobs[id]->m_undoString); } if (m_jobsByParents.count(id) > 0) { std::vector children = m_jobsByParents[id]; for (int cid : children) { QtConcurrent::run(this, &JobManager::createJob, m_jobs[cid]); } m_jobsByParents.erase(id); } updateJobCount(); } AbstractClipJob::JOBTYPE JobManager::getJobType(int jobId) const { READ_LOCK(); Q_ASSERT(m_jobs.count(jobId) > 0); return m_jobs.at(jobId)->m_type; } JobManagerStatus JobManager::getJobStatus(int jobId) const { READ_LOCK(); Q_ASSERT(m_jobs.count(jobId) > 0); auto job = m_jobs.at(jobId); if (job->m_future.isFinished()) { return JobManagerStatus::Finished; } if (job->m_future.isCanceled()) { return JobManagerStatus::Canceled; } if (job->m_future.isRunning()) { return JobManagerStatus::Running; } return JobManagerStatus::Pending; } bool JobManager::jobSucceded(int jobId) const { READ_LOCK(); Q_ASSERT(m_jobs.count(jobId) > 0); auto job = m_jobs.at(jobId); return !job->m_failed; } int JobManager::getJobProgressForClip(int jobId, const QString &binId) const { READ_LOCK(); Q_ASSERT(m_jobs.count(jobId) > 0); auto job = m_jobs.at(jobId); Q_ASSERT(job->m_indices.count(binId) > 0); size_t ind = job->m_indices.at(binId); return job->m_progress[ind]; } QPair JobManager::getJobMessageForClip(int jobId, const QString &binId) const { READ_LOCK(); Q_ASSERT(m_jobs.count(jobId) > 0); auto job = m_jobs.at(jobId); Q_ASSERT(job->m_indices.count(binId) > 0); size_t ind = job->m_indices.at(binId); return {job->m_job[ind]->getErrorMessage(), job->m_job[ind]->getLogDetails()}; } QVariant JobManager::data(const QModelIndex &index, int role) const { if (!index.isValid()) { return QVariant(); } int row = index.row(); if (row >= int(m_jobs.size()) || row < 0) { return QVariant(); } auto it = m_jobs.begin(); std::advance(it, row); switch (role) { case Qt::DisplayRole: return QVariant(it->second->m_job.front()->getDescription()); break; } return QVariant(); } int JobManager::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent); return int(m_jobs.size()); } diff --git a/src/jobs/jobmanager.h b/src/jobs/jobmanager.h index 564729934..a93b59a80 100644 --- a/src/jobs/jobmanager.h +++ b/src/jobs/jobmanager.h @@ -1,173 +1,173 @@ /* Copyright (C) 2014 Jean-Baptiste Mardelle Copyright (C) 2017 Nicolas Carion This file is part of Kdenlive. See www.kdenlive.org. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef JOBMANAGER #define JOBMANAGER #include "abstractclipjob.h" #include "definitions.h" #include #include #include #include #include #include #include #include class AbstractClipJob; /** * @class JobManager * @brief This class is responsible for clip jobs management. * */ enum class JobManagerStatus { NoJob, Pending, Running, Finished, Canceled }; Q_DECLARE_METATYPE(JobManagerStatus) struct Job_t { std::vector> m_job; // List of the jobs std::vector m_progress; // progress of the job, for each clip std::unordered_map m_indices; // keys are binIds, value are ids in the vectors m_job and m_progress; QFutureWatcher m_future; // future of the job QFuture m_actualFuture; QMutex m_completionMutex; // mutex that is locked during execution of the process AbstractClipJob::JOBTYPE m_type; QString m_undoString; int m_id; bool m_processed = false; // flag that we set to true when we are done with this job bool m_failed = false; // flag that we set to true when a problem occurred }; class AudioThumbJob; class LoadJob; class SceneSplitJob; class StabilizeJob; class ThumbJob; class JobManager : public QAbstractListModel, public enable_shared_from_this_virtual { Q_OBJECT public: explicit JobManager(QObject *parent); virtual ~JobManager(); /** @brief Start a job This function calls the prepareJob function of the job if it provides one. @param T is the type of job (must inherit from AbstractClipJob) @param binIds is the list of clips to which we apply the job @param parents is the list of the ids of the job that must terminate before this one can start @param args are the arguments to construct the job @param return the id of the created job */ template int startJob(const std::vector &binIds, int parentId, QString undoString, Args &&... args); // Same function, but we specify the function used to create a new job template int startJob(const std::vector &binIds, int parentId, QString undoString, std::function(const QString &, Args...)> createFn, Args &&... args); // Same function, but do not call prepareJob template int startJob_noprepare(const std::vector &binIds, int parentId, QString undoString, Args &&... args); /** @brief Discard specific job type for a clip. * @param binId the clip id * @param type The type of job that you want to abort, leave to NOJOBTYPE to abort all jobs */ void discardJobs(const QString &binId, AbstractClipJob::JOBTYPE type = AbstractClipJob::NOJOBTYPE); /** @brief Check if there is a pending / running job a clip. * @param binId the clip id * @param type The type of job that you want to query * @param foundId : if a valid ptr is passed, we store the id of the first matching job found (-1 if not found) */ bool hasPendingJob(const QString &binId, AbstractClipJob::JOBTYPE type, int *foundId = nullptr); /** @brief Get the list of pending or running job ids for given clip. * @param binId the clip id * @param type The type of job that you want to query. Leave to NOJOBTYPE to match all */ std::vector getPendingJobsIds(const QString &binId, AbstractClipJob::JOBTYPE type = AbstractClipJob::NOJOBTYPE); int getBlockingJobId(const QString &id, AbstractClipJob::JOBTYPE type); /** @brief Get the list of finished or cancelled job ids for given clip. * @param binId the clip id * @param type The type of job that you want to query. Leave to NOJOBTYPE to match all */ std::vector getFinishedJobsIds(const QString &binId, AbstractClipJob::JOBTYPE type = AbstractClipJob::NOJOBTYPE); /** @brief return the type of a given job */ AbstractClipJob::JOBTYPE getJobType(int jobId) const; /** @brief return the type of a given job */ JobManagerStatus getJobStatus(int jobId) const; /** @brief returns false if job failed */ bool jobSucceded(int jobId) const; /** @brief return the progress of a given job on a given clip */ int getJobProgressForClip(int jobId, const QString &binId) const; /** @brief return the message of a given job on a given clip (message, detailed log)*/ QPair getJobMessageForClip(int jobId, const QString &binId) const; // Mandatory overloads QVariant data(const QModelIndex &index, int role) const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override; protected: // Helper function to launch a given job. // This has to be launched asynchrnously since it blocks until all parents are finished - void createJob(std::shared_ptr job); + void createJob(const std::shared_ptr &job); void updateJobCount(); void slotManageCanceledJob(int id); void slotManageFinishedJob(int id); public slots: /** @brief Discard jobs running on a given clip */ void slotDiscardClipJobs(const QString &binId); /** @brief Discard all running jobs. */ void slotCancelJobs(); /** @brief Discard all pending jobs. */ void slotCancelPendingJobs(); private: /** @brief This is a lock that ensures safety in case of concurrent access */ mutable QReadWriteLock m_lock; /** @brief This is the id of the last created job */ static int m_currentId; /** @brief This is the list of all jobs, ordered by id. A job is represented by a pointer to the job class and a future to the result */ std::map> m_jobs; /** @brief List of all the jobs by clip. */ std::unordered_map> m_jobsByClip; std::unordered_map> m_jobsByParents; signals: void jobCount(int); }; #include "jobmanager.ipp" #endif diff --git a/src/jobs/jobmanager.ipp b/src/jobs/jobmanager.ipp index b5a6ca1be..0c3ac16fb 100644 --- a/src/jobs/jobmanager.ipp +++ b/src/jobs/jobmanager.ipp @@ -1,120 +1,120 @@ /* Copyright (C) 2014 Jean-Baptiste Mardelle Copyright (C) 2017 Nicolas Carion This file is part of Kdenlive. See www.kdenlive.org. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include #include template int JobManager::startJob(const std::vector &binIds, int parentId, QString undoString, std::function(const QString &, Args...)> createFn, Args &&... args) { static_assert(std::is_base_of::value, "Your job must inherit from AbstractClipJob"); // QWriteLocker locker(&m_lock); int jobId = m_currentId++; std::shared_ptr job(new Job_t()); job->m_completionMutex.lock(); job->m_undoString = std::move(undoString); job->m_id = jobId; for (const auto &id : binIds) { job->m_job.push_back(createFn(id, args...)); job->m_progress.push_back(0); job->m_indices[id] = size_t(int(job->m_job.size()) - 1); job->m_type = job->m_job.back()->jobType(); m_jobsByClip[id].push_back(jobId); } m_lock.lockForWrite(); int insertionRow = static_cast(m_jobs.size()); beginInsertRows(QModelIndex(), insertionRow, insertionRow); Q_ASSERT(m_jobs.count(jobId) == 0); m_jobs[jobId] = job; endInsertRows(); m_lock.unlock(); if (parentId == -1 || m_jobs[parentId]->m_completionMutex.tryLock()) { if (parentId != -1) { m_jobs[parentId]->m_completionMutex.unlock(); } QtConcurrent::run(this, &JobManager::createJob, job); } else { m_jobsByParents[parentId].push_back(jobId); } return jobId; } // we must specialize the second version of startjob depending on the type (some types requires to use a prepareJob method). Because we cannot use partial // specialization for functions, we resort to a static method of a class in this impl namespace we must specialize the second version of startjob depending on // the type (some types requires to use a prepareJob method). Because we cannot use partial specialization for functions, we resort to a static method of a // dummy struct in a namespace namespace impl { // This is a simple member detector borrowed from https://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Member_Detector template class Detect_prepareJob { // clang-format off struct Fallback {int prepareJob;}; // add member name "prepareJob" struct Derived : T, Fallback {}; // clang-format on template struct Check; typedef char ArrayOfOne[1]; // typedef for an array of size one. typedef char ArrayOfTwo[2]; // typedef for an array of size two. template static ArrayOfOne &func(Check *); template static ArrayOfTwo &func(...); public: typedef Detect_prepareJob type; enum { value = sizeof(func(0)) == 2 }; }; struct dummy { template static typename std::enable_if::value || Noprepare, int>::type - exec(std::shared_ptr ptr, const std::vector &binIds, int parentId, QString undoString, Args &&... args) + exec(const std::shared_ptr& ptr, const std::vector &binIds, int parentId, QString undoString, Args &&... args) { auto defaultCreate = [](const QString &id, Args... local_args) { return AbstractClipJob::make(id, std::forward(local_args)...); }; using local_createFn_t = std::function(const QString &, Args...)>; return ptr->startJob(binIds, parentId, std::move(undoString), local_createFn_t(std::move(defaultCreate)), std::forward(args)...); } template static typename std::enable_if::value && !Noprepare, int>::type exec(std::shared_ptr ptr, const std::vector &binIds, int parentId, QString undoString, Args &&... args) { // For job stabilization, there is a custom preparation function return T::prepareJob(ptr, binIds, parentId, std::move(undoString), std::forward(args)...); } }; } // namespace impl template int JobManager::startJob(const std::vector &binIds, int parentId, QString undoString, Args &&... args) { return impl::dummy::exec(shared_from_this(), binIds, parentId, std::move(undoString), std::forward(args)...); } template int JobManager::startJob_noprepare(const std::vector &binIds, int parentId, QString undoString, Args &&... args) { return impl::dummy::exec(shared_from_this(), binIds, parentId, std::move(undoString), std::forward(args)...); } diff --git a/src/jobs/loadjob.cpp b/src/jobs/loadjob.cpp index e202bdaa7..2438a2597 100644 --- a/src/jobs/loadjob.cpp +++ b/src/jobs/loadjob.cpp @@ -1,562 +1,562 @@ /*************************************************************************** * Copyright (C) 2017 by Nicolas Carion * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #include "loadjob.hpp" #include "bin/projectclip.h" #include "bin/projectfolder.h" #include "bin/projectitemmodel.h" #include "core.h" #include "doc/kdenlivedoc.h" #include "doc/kthumb.h" #include "kdenlivesettings.h" #include "klocalizedstring.h" #include "macros.hpp" #include "profiles/profilemodel.hpp" #include "project/dialogs/slideshowclip.h" #include "xml/xml.hpp" #include #include #include #include #include LoadJob::LoadJob(const QString &binId, const QDomElement &xml) : AbstractClipJob(LOADJOB, binId) , m_xml(xml) { } const QString LoadJob::getDescription() const { return i18n("Loading clip %1", m_clipId); } namespace { ClipType::ProducerType getTypeForService(const QString &id, const QString &path) { if (id.isEmpty()) { QString ext = path.section(QLatin1Char('.'), -1); if (ext == QLatin1String("mlt") || ext == QLatin1String("kdenlive")) { return ClipType::Playlist; } return ClipType::Unknown; } if (id == QLatin1String("color") || id == QLatin1String("colour")) { return ClipType::Color; } if (id == QLatin1String("kdenlivetitle")) { return ClipType::Text; } if (id == QLatin1String("qtext")) { return ClipType::QText; } if (id == QLatin1String("xml") || id == QLatin1String("consumer")) { return ClipType::Playlist; } if (id == QLatin1String("webvfx")) { return ClipType::WebVfx; } return ClipType::Unknown; } // Read the properties of the xml and pass them to the producer. Note that some properties like resource are ignored -void processProducerProperties(std::shared_ptr prod, const QDomElement &xml) +void processProducerProperties(const std::shared_ptr &prod, const QDomElement &xml) { // TODO: there is some duplication with clipcontroller > updateproducer that also copies properties QString value; QStringList internalProperties; internalProperties << QStringLiteral("bypassDuplicate") << QStringLiteral("resource") << QStringLiteral("mlt_service") << QStringLiteral("audio_index") << QStringLiteral("video_index") << QStringLiteral("mlt_type"); QDomNodeList props; if (xml.tagName() == QLatin1String("producer")) { props = xml.childNodes(); } else { props = xml.firstChildElement(QStringLiteral("producer")).childNodes(); } for (int i = 0; i < props.count(); ++i) { if (props.at(i).toElement().tagName() != QStringLiteral("property")) { continue; } QString propertyName = props.at(i).toElement().attribute(QStringLiteral("name")); if (!internalProperties.contains(propertyName) && !propertyName.startsWith(QLatin1Char('_'))) { value = props.at(i).firstChild().nodeValue(); if (propertyName.startsWith(QLatin1String("kdenlive-force."))) { // this is a special forced property, pass it propertyName.remove(0, 15); } prod->set(propertyName.toUtf8().constData(), value.toUtf8().constData()); } } } } // namespace // static std::shared_ptr LoadJob::loadResource(QString &resource, const QString &type) { if (!resource.startsWith(type)) { resource.prepend(type); } return std::make_shared(pCore->getCurrentProfile()->profile(), nullptr, resource.toUtf8().constData()); } std::shared_ptr LoadJob::loadPlaylist(QString &resource) { std::unique_ptr xmlProfile(new Mlt::Profile()); xmlProfile->set_explicit(0); std::unique_ptr producer(new Mlt::Producer(*xmlProfile, "xml", resource.toUtf8().constData())); if (!producer->is_valid()) { qDebug() << "////// ERROR, CANNOT LOAD SELECTED PLAYLIST: " << resource; return nullptr; } if (pCore->getCurrentProfile()->isCompatible(xmlProfile.get())) { // We can use the "xml" producer since profile is the same (using it with different profiles corrupts the project. // Beware that "consumer" currently crashes on audio mixes! resource.prepend(QStringLiteral("xml:")); } else { // This is currently crashing so I guess we'd better reject it for now qDebug() << "////// ERROR, INCOMPATIBLE PROFILE: " << resource; return nullptr; // path.prepend(QStringLiteral("consumer:")); } pCore->getCurrentProfile()->set_explicit(1); return std::make_shared(pCore->getCurrentProfile()->profile(), nullptr, resource.toUtf8().constData()); } -void LoadJob::checkProfile(const QString clipId, QDomElement xml, std::shared_ptr producer) +void LoadJob::checkProfile(const QString &clipId, const QDomElement &xml, const std::shared_ptr &producer) { // Check if clip profile matches QString service = producer->get("mlt_service"); // Check for image producer if (service == QLatin1String("qimage") || service == QLatin1String("pixbuf")) { // This is an image, create profile from image size int width = producer->get_int("meta.media.width"); if (width % 8 > 0) { width += 8 - width % 8; } int height = producer->get_int("meta.media.height"); height += height % 2; if (width > 100 && height > 100) { std::unique_ptr projectProfile(new ProfileParam(pCore->getCurrentProfile().get())); projectProfile->m_width = width; projectProfile->m_height = height; projectProfile->m_sample_aspect_num = 1; projectProfile->m_sample_aspect_den = 1; projectProfile->m_display_aspect_num = width; projectProfile->m_display_aspect_den = height; projectProfile->m_description.clear(); pCore->currentDoc()->switchProfile(projectProfile, clipId, xml); } else { // Very small image, we probably don't want to use this as profile } } else if (service.contains(QStringLiteral("avformat"))) { std::unique_ptr blankProfile(new Mlt::Profile()); blankProfile->set_explicit(0); blankProfile->from_producer(*producer); std::unique_ptr clipProfile(new ProfileParam(blankProfile.get())); std::unique_ptr projectProfile(new ProfileParam(pCore->getCurrentProfile().get())); clipProfile->adjustDimensions(); if (*clipProfile.get() == *projectProfile.get()) { if (KdenliveSettings::default_profile().isEmpty()) { // Confirm default project format KdenliveSettings::setDefault_profile(pCore->getCurrentProfile()->path()); } } else { // Profiles do not match, propose profile adjustment pCore->currentDoc()->switchProfile(clipProfile, clipId, xml); } } } void LoadJob::processSlideShow() { int ttl = Xml::getXmlProperty(m_xml, QStringLiteral("ttl")).toInt(); QString anim = Xml::getXmlProperty(m_xml, QStringLiteral("animation")); if (!anim.isEmpty()) { auto *filter = new Mlt::Filter(pCore->getCurrentProfile()->profile(), "affine"); if ((filter != nullptr) && filter->is_valid()) { int cycle = ttl; QString geometry = SlideshowClip::animationToGeometry(anim, cycle); if (!geometry.isEmpty()) { if (anim.contains(QStringLiteral("low-pass"))) { auto *blur = new Mlt::Filter(pCore->getCurrentProfile()->profile(), "boxblur"); if ((blur != nullptr) && blur->is_valid()) { m_producer->attach(*blur); } } filter->set("transition.geometry", geometry.toUtf8().data()); filter->set("transition.cycle", cycle); m_producer->attach(*filter); } } } QString fade = Xml::getXmlProperty(m_xml, QStringLiteral("fade")); if (fade == QLatin1String("1")) { // user wants a fade effect to slideshow auto *filter = new Mlt::Filter(pCore->getCurrentProfile()->profile(), "luma"); if ((filter != nullptr) && filter->is_valid()) { if (ttl != 0) { filter->set("cycle", ttl); } QString luma_duration = Xml::getXmlProperty(m_xml, QStringLiteral("luma_duration")); QString luma_file = Xml::getXmlProperty(m_xml, QStringLiteral("luma_file")); if (!luma_duration.isEmpty()) { filter->set("duration", luma_duration.toInt()); } if (!luma_file.isEmpty()) { filter->set("luma.resource", luma_file.toUtf8().constData()); QString softness = Xml::getXmlProperty(m_xml, QStringLiteral("softness")); if (!softness.isEmpty()) { int soft = softness.toInt(); filter->set("luma.softness", (double)soft / 100.0); } } m_producer->attach(*filter); } } QString crop = Xml::getXmlProperty(m_xml, QStringLiteral("crop")); if (crop == QLatin1String("1")) { // user wants to center crop the slides auto *filter = new Mlt::Filter(pCore->getCurrentProfile()->profile(), "crop"); if ((filter != nullptr) && filter->is_valid()) { filter->set("center", 1); m_producer->attach(*filter); } } } bool LoadJob::startJob() { if (m_done) { return true; } m_resource = Xml::getXmlProperty(m_xml, QStringLiteral("resource")); ClipType::ProducerType type = static_cast(m_xml.attribute(QStringLiteral("type")).toInt()); QString service = Xml::getXmlProperty(m_xml, QStringLiteral("mlt_service")); if (type == ClipType::Unknown) { type = getTypeForService(service, m_resource); } switch (type) { case ClipType::Color: m_producer = loadResource(m_resource, QStringLiteral("color:")); break; case ClipType::Text: case ClipType::TextTemplate: m_producer = loadResource(m_resource, QStringLiteral("kdenlivetitle:")); break; case ClipType::QText: m_producer = loadResource(m_resource, QStringLiteral("qtext:")); break; case ClipType::Playlist: m_producer = loadPlaylist(m_resource); break; case ClipType::SlideShow: default: if (!service.isEmpty()) { service.append(QChar(':')); m_producer = loadResource(m_resource, service); } else { m_producer = std::make_shared(pCore->getCurrentProfile()->profile(), nullptr, m_resource.toUtf8().constData()); } break; } if (!m_producer || m_producer->is_blank() || !m_producer->is_valid()) { qCDebug(KDENLIVE_LOG) << " / / / / / / / / ERROR / / / / // CANNOT LOAD PRODUCER: " << m_resource; m_done = true; m_successful = false; if (m_producer) { m_producer.reset(); } QMetaObject::invokeMethod(pCore.get(), "displayBinMessage", Qt::QueuedConnection, Q_ARG(const QString &, i18n("Cannot open file %1", m_resource)), Q_ARG(int, (int)KMessageWidget::Warning)); m_errorMessage.append(i18n("ERROR: Could not load clip %1: producer is invalid", m_resource)); return false; } processProducerProperties(m_producer, m_xml); QString clipName = Xml::getXmlProperty(m_xml, QStringLiteral("kdenlive:clipname")); if (clipName.isEmpty()) { clipName = QFileInfo(Xml::getXmlProperty(m_xml, QStringLiteral("kdenlive:originalurl"))).fileName(); } m_producer->set("kdenlive:clipname", clipName.toUtf8().constData()); QString groupId = Xml::getXmlProperty(m_xml, QStringLiteral("kdenlive:folderid")); if (!groupId.isEmpty()) { m_producer->set("kdenlive:folderid", groupId.toUtf8().constData()); } int clipOut = 0, duration = 0; if (m_xml.hasAttribute(QStringLiteral("out"))) { clipOut = m_xml.attribute(QStringLiteral("out")).toInt(); } // setup length here as otherwise default length (currently 15000 frames in MLT) will be taken even if outpoint is larger if (type == ClipType::Color || type == ClipType::Text || type == ClipType::TextTemplate || type == ClipType::QText || type == ClipType::Image || type == ClipType::SlideShow) { int length; if (m_xml.hasAttribute(QStringLiteral("length"))) { length = m_xml.attribute(QStringLiteral("length")).toInt(); clipOut = qMax(1, length - 1); } else { length = Xml::getXmlProperty(m_xml, QStringLiteral("length")).toInt(); clipOut -= m_xml.attribute(QStringLiteral("in")).toInt(); if (length < clipOut) { length = clipOut == 1 ? 1 : clipOut + 1; } } // Pass duration if it was forced if (m_xml.hasAttribute(QStringLiteral("duration"))) { duration = m_xml.attribute(QStringLiteral("duration")).toInt(); if (length < duration) { length = duration; if (clipOut > 0) { clipOut = length - 1; } } } if (duration == 0) { duration = length; } m_producer->set("length", m_producer->frames_to_time(length, mlt_time_clock)); int kdenlive_duration = m_producer->time_to_frames(Xml::getXmlProperty(m_xml, QStringLiteral("kdenlive:duration")).toUtf8().constData()); if (kdenlive_duration > 0) { m_producer->set("kdenlive:duration", m_producer->frames_to_time(kdenlive_duration , mlt_time_clock)); } else { m_producer->set("kdenlive:duration", m_producer->get("length")); } } if (clipOut > 0) { m_producer->set_in_and_out(m_xml.attribute(QStringLiteral("in")).toInt(), clipOut); } if (m_xml.hasAttribute(QStringLiteral("templatetext"))) { m_producer->set("templatetext", m_xml.attribute(QStringLiteral("templatetext")).toUtf8().constData()); } duration = duration > 0 ? duration : m_producer->get_playtime(); if (type == ClipType::SlideShow) { processSlideShow(); } int vindex = -1; double fps = -1; const QString mltService = m_producer->get("mlt_service"); if (mltService == QLatin1String("xml") || mltService == QLatin1String("consumer")) { // MLT playlist, create producer with blank profile to get real profile info QString tmpPath = m_resource; if (tmpPath.startsWith(QLatin1String("consumer:"))) { tmpPath = "xml:" + tmpPath.section(QLatin1Char(':'), 1); } Mlt::Profile original_profile; std::unique_ptr tmpProd(new Mlt::Producer(original_profile, nullptr, tmpPath.toUtf8().constData())); original_profile.set_explicit(1); double originalFps = original_profile.fps(); fps = originalFps; if (originalFps > 0 && !qFuzzyCompare(originalFps, pCore->getCurrentFps())) { int originalLength = tmpProd->get_length(); int fixedLength = (int)(originalLength * pCore->getCurrentFps() / originalFps); m_producer->set("length", fixedLength); m_producer->set("out", fixedLength - 1); } } else if (mltService == QLatin1String("avformat")) { // check if there are multiple streams vindex = m_producer->get_int("video_index"); // List streams int streams = m_producer->get_int("meta.media.nb_streams"); m_audio_list.clear(); m_video_list.clear(); for (int i = 0; i < streams; ++i) { QByteArray propertyName = QStringLiteral("meta.media.%1.stream.type").arg(i).toLocal8Bit(); QString stype = m_producer->get(propertyName.data()); if (stype == QLatin1String("audio")) { m_audio_list.append(i); } else if (stype == QLatin1String("video")) { m_video_list.append(i); } } if (vindex > -1) { char property[200]; snprintf(property, sizeof(property), "meta.media.%d.stream.frame_rate", vindex); fps = m_producer->get_double(property); } if (fps <= 0) { if (m_producer->get_double("meta.media.frame_rate_den") > 0) { fps = m_producer->get_double("meta.media.frame_rate_num") / m_producer->get_double("meta.media.frame_rate_den"); } else { fps = m_producer->get_double("source_fps"); } } } if (fps <= 0 && type == ClipType::Unknown) { // something wrong, maybe audio file with embedded image QMimeDatabase db; QString mime = db.mimeTypeForFile(m_resource).name(); if (mime.startsWith(QLatin1String("audio"))) { m_producer->set("video_index", -1); vindex = -1; } } m_done = m_successful = true; return true; } void LoadJob::processMultiStream() { auto m_binClip = pCore->projectItemModel()->getClipByBinID(m_clipId); // We retrieve the folder containing our clip, because we will set the other streams in the same auto parent = pCore->projectItemModel()->getRootFolder()->clipId(); if (auto ptr = m_binClip->parentItem().lock()) { parent = std::static_pointer_cast(ptr)->clipId(); } else { qDebug() << "Warning, something went wrong while accessing parent of bin clip"; } // This helper lambda request addition of a given stream auto addStream = [this, parentId = std::move(parent)](int vindex, int aindex, Fun &undo, Fun &redo) { auto clone = ProjectClip::cloneProducer(m_producer); clone->set("video_index", vindex); clone->set("audio_index", aindex); QString id; pCore->projectItemModel()->requestAddBinClip(id, clone, parentId, undo, redo); }; Fun undo = []() { return true; }; Fun redo = []() { return true; }; if (KdenliveSettings::automultistreams()) { for (int i = 1; i < m_video_list.count(); ++i) { int vindex = m_video_list.at(i); int aindex = 0; if (i <= m_audio_list.count() - 1) { aindex = m_audio_list.at(i); } addStream(vindex, aindex, undo, redo); } return; } int width = 60.0 * pCore->getCurrentDar(); if (width % 2 == 1) { width++; } QScopedPointer dialog(new QDialog(qApp->activeWindow())); dialog->setWindowTitle(QStringLiteral("Multi Stream Clip")); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); QWidget *mainWidget = new QWidget(dialog.data()); auto *mainLayout = new QVBoxLayout; dialog->setLayout(mainLayout); mainLayout->addWidget(mainWidget); QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); dialog->connect(buttonBox, &QDialogButtonBox::accepted, dialog.data(), &QDialog::accept); dialog->connect(buttonBox, &QDialogButtonBox::rejected, dialog.data(), &QDialog::reject); okButton->setText(i18n("Import selected clips")); QLabel *lab1 = new QLabel(i18n("Additional streams for clip\n %1", m_resource), mainWidget); mainLayout->addWidget(lab1); QList groupList; QList comboList; // We start loading the list at 1, video index 0 should already be loaded for (int j = 1; j < m_video_list.count(); ++j) { m_producer->set("video_index", m_video_list.at(j)); // TODO this keyframe should be cached QImage thumb = KThumb::getFrame(m_producer.get(), 0, width, 60); QGroupBox *streamFrame = new QGroupBox(i18n("Video stream %1", m_video_list.at(j)), mainWidget); mainLayout->addWidget(streamFrame); streamFrame->setProperty("vindex", m_video_list.at(j)); groupList << streamFrame; streamFrame->setCheckable(true); streamFrame->setChecked(true); auto *vh = new QVBoxLayout(streamFrame); QLabel *iconLabel = new QLabel(mainWidget); mainLayout->addWidget(iconLabel); iconLabel->setPixmap(QPixmap::fromImage(thumb)); vh->addWidget(iconLabel); if (m_audio_list.count() > 1) { auto *cb = new KComboBox(mainWidget); mainLayout->addWidget(cb); for (int k = 0; k < m_audio_list.count(); ++k) { cb->addItem(i18n("Audio stream %1", m_audio_list.at(k)), m_audio_list.at(k)); } comboList << cb; cb->setCurrentIndex(qMin(j, m_audio_list.count() - 1)); vh->addWidget(cb); } mainLayout->addWidget(streamFrame); } mainLayout->addWidget(buttonBox); if (dialog->exec() == QDialog::Accepted) { // import selected streams for (int i = 0; i < groupList.count(); ++i) { if (groupList.at(i)->isChecked()) { int vindex = groupList.at(i)->property("vindex").toInt(); int ax = qMin(i, comboList.size() - 1); int aindex = -1; if (ax >= 0) { // only check audio index if we have several audio streams aindex = comboList.at(ax)->itemData(comboList.at(ax)->currentIndex()).toInt(); } addStream(vindex, aindex, undo, redo); } } } pCore->pushUndo(undo, redo, i18n("Add additional streams for clip")); } bool LoadJob::commitResult(Fun &undo, Fun &redo) { qDebug() << "################### loadjob COMMIT"; Q_ASSERT(!m_resultConsumed); if (!m_done) { qDebug() << "ERROR: Trying to consume invalid results"; return false; } m_resultConsumed = true; auto m_binClip = pCore->projectItemModel()->getClipByBinID(m_clipId); if (!m_successful) { // TODO: Deleting cannot happen at this stage or we endup in a mutex lock pCore->projectItemModel()->requestBinClipDeletion(m_binClip, undo, redo); return false; } if (m_xml.hasAttribute(QStringLiteral("_checkProfile")) && m_producer->get_int("video_index") > -1) { checkProfile(m_clipId, m_xml, m_producer); } if (m_video_list.size() > 1) { processMultiStream(); } // note that the image is moved into lambda, it won't be available from this class anymore auto operation = [clip = m_binClip, prod = std::move(m_producer)]() { clip->setProducer(prod, true); return true; }; auto reverse = []() { // This is probably not invertible, leave as is. return true; }; bool ok = operation(); if (ok) { if (pCore->projectItemModel()->clipsCount() == 1) { // Always select first added clip pCore->selectBinClip(m_clipId); } UPDATE_UNDO_REDO_NOLOCK(operation, reverse, undo, redo); } return ok; } diff --git a/src/jobs/loadjob.hpp b/src/jobs/loadjob.hpp index 1fc96232a..cd5c04e2f 100644 --- a/src/jobs/loadjob.hpp +++ b/src/jobs/loadjob.hpp @@ -1,80 +1,80 @@ /*************************************************************************** * Copyright (C) 2017 by Nicolas Carion * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #pragma once #include "abstractclipjob.h" #include #include /* @brief This class represents the job that corresponds to loading a clip from xml */ class ProjectClip; namespace Mlt { class Producer; } class LoadJob : public AbstractClipJob { Q_OBJECT public: /* @brief Extract a thumb for given clip. @param frameNumber is the frame to extract. Leave to -1 for default @param persistent: if true, we will use the persistent cache (for query and saving) */ LoadJob(const QString &binId, const QDomElement &xml); const QString getDescription() const override; bool startJob() override; /** @brief This is to be called after the job finished. By design, the job should store the result of the computation but not share it with the rest of the code. This happens when we call commitResult */ bool commitResult(Fun &undo, Fun &redo) override; // Do some checks on the profile - static void checkProfile(const QString clipId, QDomElement xml, std::shared_ptr producer); + static void checkProfile(const QString &clipId, const QDomElement &xml, const std::shared_ptr &producer); protected: // helper to load some kind of resources such as color. This will modify resource if needs be (for eg., in the case of color, it will prepend "color:" if // needed) static std::shared_ptr loadResource(QString &resource, const QString &type); std::shared_ptr loadPlaylist(QString &resource); // Create the required filter for a slideshow void processSlideShow(); // This should be called from commitResult (that is, from the GUI thread) to deal with multi stream videos void processMultiStream(); private: QDomElement m_xml; bool m_done{false}, m_successful{false}; std::shared_ptr m_producer; QList m_audio_list, m_video_list; QString m_resource; }; diff --git a/src/jobs/scenesplitjob.cpp b/src/jobs/scenesplitjob.cpp index 3cc63b65f..dcc402e67 100644 --- a/src/jobs/scenesplitjob.cpp +++ b/src/jobs/scenesplitjob.cpp @@ -1,173 +1,173 @@ /*************************************************************************** * Copyright (C) 2011 by Jean-Baptiste Mardelle (jb@kdenlive.org) * * 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 "scenesplitjob.hpp" #include "bin/clipcreator.hpp" #include "bin/model/markerlistmodel.hpp" #include "bin/projectclip.h" #include "bin/projectfolder.h" #include "bin/projectitemmodel.h" #include "core.h" #include "jobmanager.h" #include "kdenlivesettings.h" #include "project/clipstabilize.h" #include "ui_scenecutdialog_ui.h" #include #include #include SceneSplitJob::SceneSplitJob(const QString &binId, bool subClips, int markersType, int minInterval) : MeltJob(binId, STABILIZEJOB, true, -1, -1) , m_subClips(subClips) , m_markersType(markersType) , m_minInterval(minInterval) { } const QString SceneSplitJob::getDescription() const { return i18n("Scene split"); } void SceneSplitJob::configureConsumer() { m_consumer.reset(new Mlt::Consumer(*m_profile.get(), "null")); m_consumer->set("all", 1); m_consumer->set("terminate_on_pause", 1); m_consumer->set("real_time", -KdenliveSettings::mltthreads()); // We just want to find scene change, set all methods to the fastests m_consumer->set("rescale", "nearest"); m_consumer->set("deinterlace_method", "onefield"); m_consumer->set("top_field_first", -1); } void SceneSplitJob::configureFilter() { m_filter.reset(new Mlt::Filter(*m_profile.get(), "motion_est")); if ((m_filter == nullptr) || !m_filter->is_valid()) { m_errorMessage.append(i18n("Cannot create filter motion_est. Cannot split scenes")); return; } m_filter->set("shot_change_list", 0); m_filter->set("denoise", 0); } void SceneSplitJob::configureProfile() { m_profile->set_height(160); m_profile->set_width(m_profile->height() * m_profile->sar()); } // static -int SceneSplitJob::prepareJob(std::shared_ptr ptr, const std::vector &binIds, int parentId, QString undoString) +int SceneSplitJob::prepareJob(const std::shared_ptr &ptr, const std::vector &binIds, int parentId, QString undoString) { // Show config dialog QScopedPointer d(new QDialog(QApplication::activeWindow())); Ui::SceneCutDialog_UI ui; ui.setupUi(d.data()); // Set up categories for (size_t i = 0; i < MarkerListModel::markerTypes.size(); ++i) { ui.marker_type->insertItem((int)i, i18n("Category %1", i)); ui.marker_type->setItemData((int)i, MarkerListModel::markerTypes[i], Qt::DecorationRole); } ui.marker_type->setCurrentIndex(KdenliveSettings::default_marker_type()); ui.zone_only->setEnabled(false); // not implemented ui.store_data->setEnabled(false); // not implemented if (d->exec() != QDialog::Accepted) { return -1; } int markersType = ui.add_markers->isChecked() ? ui.marker_type->currentIndex() : -1; bool subclips = ui.cut_scenes->isChecked(); int minInterval = ui.minDuration->value(); return ptr->startJob_noprepare(binIds, parentId, std::move(undoString), subclips, markersType, minInterval); } bool SceneSplitJob::commitResult(Fun &undo, Fun &redo) { Q_UNUSED(undo) Q_UNUSED(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; } QString result = QString::fromLatin1(m_filter->get("shot_change_list")); if (result.isEmpty()) { m_errorMessage.append(i18n("No data returned from clip analysis")); return false; } auto binClip = pCore->projectItemModel()->getClipByBinID(m_clipId); QStringList markerData = result.split(QLatin1Char(';')); if (m_markersType >= 0) { // Build json data for markers QJsonArray list; int ix = 1; int lastCut = 0; - for (const QString marker : markerData) { + for (const QString &marker : markerData) { int pos = marker.section(QLatin1Char('='), 0, 0).toInt(); if (m_minInterval > 0 && ix > 1 && pos - lastCut < m_minInterval) { continue; } lastCut = pos; QJsonObject currentMarker; currentMarker.insert(QLatin1String("pos"), QJsonValue(pos)); currentMarker.insert(QLatin1String("comment"), QJsonValue(i18n("Scene %1", ix))); currentMarker.insert(QLatin1String("type"), QJsonValue(m_markersType)); list.push_back(currentMarker); ix++; } QJsonDocument json(list); binClip->getMarkerModel()->importFromJson(QString(json.toJson()), true, undo, redo); } if (m_subClips) { // Create zones int ix = 1; int lastCut = 0; QMap zoneData; - for (const QString marker : markerData) { + for (const QString &marker : markerData) { int pos = marker.section(QLatin1Char('='), 0, 0).toInt(); if (pos <= lastCut + 1 || pos - lastCut < m_minInterval) { continue; } zoneData.insert(i18n("Scene %1", ix), QString("%1;%2").arg(lastCut).arg(pos - 1)); lastCut = pos; ix++; } if (!zoneData.isEmpty()) { pCore->projectItemModel()->loadSubClips(m_clipId, zoneData, undo, redo); } } qDebug() << "RESULT of the SCENESPLIT filter:" << result; // TODO refac: reimplement add markers and subclips return true; } diff --git a/src/jobs/scenesplitjob.hpp b/src/jobs/scenesplitjob.hpp index f0e692016..0d180489a 100644 --- a/src/jobs/scenesplitjob.hpp +++ b/src/jobs/scenesplitjob.hpp @@ -1,70 +1,70 @@ /*************************************************************************** * Copyright (C) 2011 by Jean-Baptiste Mardelle (jb@kdenlive.org) * * Copyright (C) 2017 by Nicolas Carion * * * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #pragma once #include "meltjob.h" #include #include /** * @class SceneSplitJob * @brief Detects the scenes of a clip using a mlt filter * */ class JobManager; class SceneSplitJob : public MeltJob { Q_OBJECT public: /** @brief Creates a scenesplit job for the given bin clip @param subClips if true, we create a subclip per found scene @param markersType The type of markers that will be created to denote scene. Leave -1 for no markers */ SceneSplitJob(const QString &binId, bool subClips, int markersType = -1, int minInterval = 0); // This is a special function that prepares the stabilize job for a given list of clips. // Namely, it displays the required UI to configure the job and call startJob with the right set of parameters // Then the job is automatically put in queue. Its id is returned - static int prepareJob(std::shared_ptr ptr, const std::vector &binIds, int parentId, QString undoString); + static int prepareJob(const std::shared_ptr &ptr, const std::vector &binIds, int parentId, QString undoString); bool commitResult(Fun &undo, Fun &redo) override; const QString getDescription() const override; protected: // @brief create and configure consumer void configureConsumer() override; // @brief create and configure filter void configureFilter() override; // @brief extra configuration of the profile (eg: resize the profile) void configureProfile() override; bool m_subClips; int m_markersType; // @brief minimum scene duration. int m_minInterval; }; diff --git a/src/jobs/speedjob.cpp b/src/jobs/speedjob.cpp index c278dc1ab..3f1180a12 100644 --- a/src/jobs/speedjob.cpp +++ b/src/jobs/speedjob.cpp @@ -1,136 +1,136 @@ /*************************************************************************** * Copyright (C) 2018 by Jean-Baptiste Mardelle (jb@kdenlive.org) * * 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 "speedjob.hpp" #include "bin/clipcreator.hpp" #include "bin/projectclip.h" #include "bin/projectfolder.h" #include "bin/projectitemmodel.h" #include "core.h" #include "jobmanager.h" #include "kdenlivesettings.h" #include "project/clipstabilize.h" #include "ui_scenecutdialog_ui.h" #include #include #include #include SpeedJob::SpeedJob(const QString &binId, double speed, const QString &destUrl) : MeltJob(binId, SPEEDJOB, false, -1, -1) , m_speed(speed) , m_destUrl(destUrl) { m_requiresFilter = false; } const QString SpeedJob::getDescription() const { return i18n("Change clip speed"); } void SpeedJob::configureConsumer() { m_consumer.reset(new Mlt::Consumer(*m_profile.get(), "xml", m_destUrl.toUtf8().constData())); m_consumer->set("terminate_on_pause", 1); m_consumer->set("title", "Speed Change"); m_consumer->set("real_time", -KdenliveSettings::mltthreads()); } void SpeedJob::configureProducer() { if (!qFuzzyCompare(m_speed, 1.0)) { QString resource = m_producer->get("resource"); m_producer.reset(new Mlt::Producer(*m_profile.get(), "timewarp", QStringLiteral("%1:%2").arg(m_speed).arg(resource).toUtf8().constData())); } } void SpeedJob::configureFilter() {} // static -int SpeedJob::prepareJob(std::shared_ptr ptr, const std::vector &binIds, int parentId, QString undoString) +int SpeedJob::prepareJob(const std::shared_ptr &ptr, const std::vector &binIds, int parentId, QString undoString) { // Show config dialog bool ok; int speed = QInputDialog::getInt(QApplication::activeWindow(), i18n("Clip Speed"), i18n("Percentage"), 100, -100000, 100000, 1, &ok); if (!ok) { return -1; } std::unordered_map destinations; // keys are binIds, values are path to target files for (const auto &binId : binIds) { auto binClip = pCore->projectItemModel()->getClipByBinID(binId); // Filter several clips, destination points to a folder QString mltfile = QFileInfo(binClip->url()).absoluteFilePath() + QStringLiteral(".mlt"); destinations[binId] = mltfile; } // Now we have to create the jobs objects. This is trickier than usual, since the parameters are different for each job (each clip has its own // destination). We have to construct a lambda that does that. auto createFn = [dest = std::move(destinations), fSpeed = speed / 100.0](const QString &id) { return std::make_shared(id, fSpeed, dest.at(id)); }; // We are now all set to create the job. Note that we pass all the parameters directly through the lambda, hence there are no extra parameters to the // function using local_createFn_t = std::function(const QString &)>; return ptr->startJob(binIds, parentId, std::move(undoString), local_createFn_t(std::move(createFn))); } bool SpeedJob::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; } auto binClip = pCore->projectItemModel()->getClipByBinID(m_clipId); // We store the stabilized clips in a sub folder with this name const QString folderName(i18n("Speed Change")); QString folderId = QStringLiteral("-1"); bool found = false; // We first try to see if it exists auto containingFolder = std::static_pointer_cast(binClip->parent()); for (int i = 0; i < containingFolder->childCount(); ++i) { auto currentItem = std::static_pointer_cast(containingFolder->child(i)); if (currentItem->itemType() == AbstractProjectItem::FolderItem && currentItem->name() == folderName) { found = true; folderId = currentItem->clipId(); break; } } if (!found) { // if it was not found, we create it pCore->projectItemModel()->requestAddFolder(folderId, folderName, binClip->parent()->clipId(), undo, redo); } auto id = ClipCreator::createClipFromFile(m_destUrl, folderId, pCore->projectItemModel(), undo, redo); return id != QStringLiteral("-1"); } diff --git a/src/jobs/speedjob.hpp b/src/jobs/speedjob.hpp index d79588850..a408acfef 100644 --- a/src/jobs/speedjob.hpp +++ b/src/jobs/speedjob.hpp @@ -1,67 +1,67 @@ /*************************************************************************** * Copyright (C) 2018 by Jean-Baptiste Mardelle (jb@kdenlive.org) * * Copyright (C) 2017 by Nicolas Carion * * * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #pragma once #include "meltjob.h" #include #include /** * @class SpeedJob * @brief Create a timewarp producer to change speed of a producer * */ class JobManager; class SpeedJob : public MeltJob { Q_OBJECT public: /** @brief Creates a timewarp producer @param speed The speed value */ SpeedJob(const QString &binId, double speed, const QString &destUrl); // This is a special function that prepares the stabilize job for a given list of clips. // Namely, it displays the required UI to configure the job and call startJob with the right set of parameters // Then the job is automatically put in queue. Its id is returned - static int prepareJob(std::shared_ptr ptr, const std::vector &binIds, int parentId, QString undoString); + static int prepareJob(const std::shared_ptr &ptr, const std::vector &binIds, int parentId, QString undoString); bool commitResult(Fun &undo, Fun &redo) override; const QString getDescription() const override; protected: // @brief create and configure consumer void configureConsumer() override; // @brief create and configure producer void configureProducer() override; // @brief create and configure filter void configureFilter() override; double m_speed; QString m_destUrl; }; diff --git a/src/jobs/stabilizejob.cpp b/src/jobs/stabilizejob.cpp index 98b384269..578484ad7 100644 --- a/src/jobs/stabilizejob.cpp +++ b/src/jobs/stabilizejob.cpp @@ -1,157 +1,158 @@ /*************************************************************************** * Copyright (C) 2011 by Jean-Baptiste Mardelle (jb@kdenlive.org) * * 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 "stabilizejob.hpp" #include "bin/clipcreator.hpp" #include "bin/projectclip.h" #include "bin/projectfolder.h" #include "bin/projectitemmodel.h" #include "core.h" #include "jobmanager.h" #include "kdenlivesettings.h" #include "project/clipstabilize.h" #include #include StabilizeJob::StabilizeJob(const QString &binId, const QString &filterName, const QString &destUrl, const std::unordered_map &filterParams) : MeltJob(binId, STABILIZEJOB, true, -1, -1) , m_filterName(filterName) , m_destUrl(destUrl) , m_filterParams(filterParams) { Q_ASSERT(supportedFilters().count(filterName) > 0); } const QString StabilizeJob::getDescription() const { return i18n("Stabilize clips"); } void StabilizeJob::configureConsumer() { m_consumer.reset(new Mlt::Consumer(*m_profile.get(), "xml", m_destUrl.toUtf8().constData())); m_consumer->set("all", 1); m_consumer->set("title", "Stabilized"); m_consumer->set("real_time", -KdenliveSettings::mltthreads()); } void StabilizeJob::configureFilter() { m_filter.reset(new Mlt::Filter(*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 for (const auto &it : m_filterParams) { m_filter->set(it.first.toUtf8().constData(), it.second.toUtf8().constData()); } QString targetFile = m_destUrl + QStringLiteral(".trf"); m_filter->set("filename", targetFile.toUtf8().constData()); } // static std::unordered_set StabilizeJob::supportedFilters() { return {QLatin1String("vidstab"), QLatin1String("videostab2"), QLatin1String("videostab")}; } // static -int StabilizeJob::prepareJob(std::shared_ptr ptr, const std::vector &binIds, int parentId, QString undoString, const QString &filterName) +int StabilizeJob::prepareJob(const std::shared_ptr &ptr, const std::vector &binIds, int parentId, QString undoString, + const QString &filterName) { Q_ASSERT(supportedFilters().count(filterName) > 0); if (filterName == QLatin1String("vidstab") || filterName == QLatin1String("videostab2") || filterName == QLatin1String("videostab")) { // vidstab QScopedPointer d(new ClipStabilize(binIds, filterName, 100000)); if (d->exec() == QDialog::Accepted) { std::unordered_map filterParams = d->filterParams(); QString destination = d->destination(); std::unordered_map destinations; // keys are binIds, values are path to target files for (const auto &binId : binIds) { auto binClip = pCore->projectItemModel()->getClipByBinID(binId); if (binIds.size() == 1) { // We only have one clip, destination points to the final url destinations[binId] = destination; } else { // Filter several clips, destination points to a folder QString mltfile = destination + QFileInfo(binClip->url()).fileName() + QStringLiteral(".mlt"); destinations[binId] = mltfile; } } // Now we have to create the jobs objects. This is trickier than usual, since the parameters are different for each job (each clip has its own // destination). We have to construct a lambda that does that. - auto createFn = [dest = std::move(destinations), fName = std::move(filterName), fParams = std::move(filterParams)](const QString &id) { + auto createFn = [dest = std::move(destinations), fName = filterName, fParams = std::move(filterParams)](const QString &id) { return std::make_shared(id, fName, dest.at(id), fParams); }; // We are now all set to create the job. Note that we pass all the parameters directly through the lambda, hence there are no extra parameters to // the function using local_createFn_t = std::function(const QString &)>; return ptr->startJob(binIds, parentId, std::move(undoString), local_createFn_t(std::move(createFn))); } } return -1; } bool StabilizeJob::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; } auto binClip = pCore->projectItemModel()->getClipByBinID(m_clipId); // We store the stabilized clips in a sub folder with this name const QString folderName(i18n("Stabilized")); QString folderId = QStringLiteral("-1"); bool found = false; // We first try to see if it exists auto containingFolder = std::static_pointer_cast(binClip->parent()); for (int i = 0; i < containingFolder->childCount(); ++i) { auto currentItem = std::static_pointer_cast(containingFolder->child(i)); if (currentItem->itemType() == AbstractProjectItem::FolderItem && currentItem->name() == folderName) { found = true; folderId = currentItem->clipId(); break; } } if (!found) { // if it was not found, we create it pCore->projectItemModel()->requestAddFolder(folderId, folderName, binClip->parent()->clipId(), undo, redo); } auto id = ClipCreator::createClipFromFile(m_destUrl, folderId, pCore->projectItemModel(), undo, redo); return id != QStringLiteral("-1"); } diff --git a/src/jobs/stabilizejob.hpp b/src/jobs/stabilizejob.hpp index 5c2120195..257bb2122 100644 --- a/src/jobs/stabilizejob.hpp +++ b/src/jobs/stabilizejob.hpp @@ -1,71 +1,72 @@ /*************************************************************************** * Copyright (C) 2011 by Jean-Baptiste Mardelle (jb@kdenlive.org) * * Copyright (C) 2017 by Nicolas Carion * * * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #pragma once #include "meltjob.h" #include #include /** * @class StabilizeJob * @brief Stabilize a clip using a mlt filter * */ class JobManager; class StabilizeJob : public MeltJob { Q_OBJECT public: /** @brief Creates a stabilize job job for the given bin clip @brief filterName is the name of the actual melt filter to use @brief destUrl is the path to the file we are going to produce @brief filterParams is a map containing the xml parameters of the filter */ StabilizeJob(const QString &binId, const QString &filterName, const QString &destUrl, const std::unordered_map &filterparams); // This is a special function that prepares the stabilize job for a given list of clips. // Namely, it displays the required UI to configure the job and call startJob with the right set of parameters // Then the job is automatically put in queue. Its id is returned - static int prepareJob(std::shared_ptr ptr, const std::vector &binIds, int parentId, QString undoString, const QString &filterName); + static int prepareJob(const std::shared_ptr &ptr, const std::vector &binIds, int parentId, QString undoString, + const QString &filterName); // Return the list of stabilization filters that we support static std::unordered_set supportedFilters(); bool commitResult(Fun &undo, Fun &redo) override; const QString getDescription() const override; protected: // @brief create and configure consumer void configureConsumer() override; // @brief create and configure filter void configureFilter() override; protected: QString m_filterName; QString m_destUrl; std::unordered_map m_filterParams; }; diff --git a/src/lib/audio/audioInfo.cpp b/src/lib/audio/audioInfo.cpp index af7578865..02ea7f9cf 100644 --- a/src/lib/audio/audioInfo.cpp +++ b/src/lib/audio/audioInfo.cpp @@ -1,58 +1,58 @@ /*************************************************************************** * Copyright (C) 2012 by Simon Andreas Eugster (simon.eu@gmail.com) * * 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) any later version. * ***************************************************************************/ #include "audioInfo.h" #include "audioStreamInfo.h" #include #include -AudioInfo::AudioInfo(std::shared_ptr producer) +AudioInfo::AudioInfo(const std::shared_ptr &producer) { // Since we already receive an MLT producer, we do not need to initialize MLT: // Mlt::Factory::init(nullptr); // Get the number of streams and add the information of each of them if it is an audio stream. int streams = producer->get_int("meta.media.nb_streams"); for (int i = 0; i < streams; ++i) { QByteArray propertyName = QStringLiteral("meta.media.%1.stream.type").arg(i).toLocal8Bit(); const char *streamtype = producer->get(propertyName.data()); if ((streamtype != nullptr) && strcmp("audio", streamtype) == 0) { m_list << new AudioStreamInfo(producer, i); } } } AudioInfo::~AudioInfo() { for (AudioStreamInfo *info : m_list) { delete info; } } int AudioInfo::size() const { return m_list.size(); } AudioStreamInfo const *AudioInfo::info(int pos) const { Q_ASSERT(pos >= 0); Q_ASSERT(pos <= m_list.size()); return m_list.at(pos); } void AudioInfo::dumpInfo() const { for (AudioStreamInfo *info : m_list) { info->dumpInfo(); } } diff --git a/src/lib/audio/audioInfo.h b/src/lib/audio/audioInfo.h index e53ec262c..3f5ce7dca 100644 --- a/src/lib/audio/audioInfo.h +++ b/src/lib/audio/audioInfo.h @@ -1,34 +1,34 @@ /* Copyright (C) 2012 Simon A. Eugster (Granjow) 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 3 of the License, or (at your option) any later version. */ #ifndef AUDIOINFO_H #define AUDIOINFO_H #include #include #include class AudioStreamInfo; class AudioInfo { public: - explicit AudioInfo(std::shared_ptr producer); + explicit AudioInfo(const std::shared_ptr &producer); ~AudioInfo(); int size() const; AudioStreamInfo const *info(int pos) const; void dumpInfo() const; private: QList m_list; }; #endif // AUDIOINFO_H diff --git a/src/lib/audio/audioStreamInfo.cpp b/src/lib/audio/audioStreamInfo.cpp index f1486c9b5..8f0f8c4e7 100644 --- a/src/lib/audio/audioStreamInfo.cpp +++ b/src/lib/audio/audioStreamInfo.cpp @@ -1,84 +1,84 @@ /* Copyright (C) 2012 Simon A. Eugster (Granjow) 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 3 of the License, or (at your option) any later version. */ #include "audioStreamInfo.h" #include "kdenlive_debug.h" #include #include -AudioStreamInfo::AudioStreamInfo(std::shared_ptr producer, int audioStreamIndex) +AudioStreamInfo::AudioStreamInfo(const std::shared_ptr &producer, int audioStreamIndex) : m_audioStreamIndex(audioStreamIndex) , m_ffmpegAudioIndex(0) , m_samplingRate(48000) , m_channels(2) , m_bitRate(0) { if (audioStreamIndex > -1) { QByteArray key; key = QStringLiteral("meta.media.%1.codec.sample_fmt").arg(audioStreamIndex).toLocal8Bit(); m_samplingFormat = QString::fromLatin1(producer->get(key.data())); key = QStringLiteral("meta.media.%1.codec.sample_rate").arg(audioStreamIndex).toLocal8Bit(); m_samplingRate = producer->get_int(key.data()); key = QStringLiteral("meta.media.%1.codec.bit_rate").arg(audioStreamIndex).toLocal8Bit(); m_bitRate = producer->get_int(key.data()); key = QStringLiteral("meta.media.%1.codec.channels").arg(audioStreamIndex).toLocal8Bit(); m_channels = producer->get_int(key.data()); int streams = producer->get_int("meta.media.nb_streams"); QList audioStreams; for (int i = 0; i < streams; ++i) { QByteArray propertyName = QStringLiteral("meta.media.%1.stream.type").arg(i).toLocal8Bit(); QString type = producer->get(propertyName.data()); if (type == QLatin1String("audio")) { audioStreams << i; } } if (audioStreams.count() > 1) { m_ffmpegAudioIndex = audioStreams.indexOf(m_audioStreamIndex); } } } AudioStreamInfo::~AudioStreamInfo() = default; int AudioStreamInfo::samplingRate() const { return m_samplingRate; } int AudioStreamInfo::channels() const { return m_channels; } int AudioStreamInfo::bitrate() const { return m_bitRate; } int AudioStreamInfo::audio_index() const { return m_audioStreamIndex; } int AudioStreamInfo::ffmpeg_audio_index() const { return m_ffmpegAudioIndex; } void AudioStreamInfo::dumpInfo() const { qCDebug(KDENLIVE_LOG) << "Info for audio stream " << m_audioStreamIndex << "\n\tChannels: " << m_channels << "\n\tSampling rate: " << m_samplingRate << "\n\tBit rate: " << m_bitRate; } diff --git a/src/lib/audio/audioStreamInfo.h b/src/lib/audio/audioStreamInfo.h index 3b1764fe9..4e5b2f428 100644 --- a/src/lib/audio/audioStreamInfo.h +++ b/src/lib/audio/audioStreamInfo.h @@ -1,45 +1,45 @@ /* Copyright (C) 2012 Simon A. Eugster (Granjow) 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 3 of the License, or (at your option) any later version. */ #ifndef AUDIOSTREAMINFO_H #define AUDIOSTREAMINFO_H #include #include #include /** Provides easy access to properties of an audio stream. */ class AudioStreamInfo { public: // TODO make that access a shared ptr instead of raw - AudioStreamInfo(std::shared_ptr producer, int audioStreamIndex); + AudioStreamInfo(const std::shared_ptr &producer, int audioStreamIndex); ~AudioStreamInfo(); int samplingRate() const; int channels() const; int bitrate() const; const QString &samplingFormat() const; int audio_index() const; int ffmpeg_audio_index() const; void dumpInfo() const; private: int m_audioStreamIndex; int m_ffmpegAudioIndex; int m_samplingRate; int m_channels; int m_bitRate; QString m_samplingFormat; }; #endif // AUDIOSTREAMINFO_H diff --git a/src/logger.cpp b/src/logger.cpp index c7412b5ca..945e6a387 100644 --- a/src/logger.cpp +++ b/src/logger.cpp @@ -1,380 +1,380 @@ /*************************************************************************** * Copyright (C) 2019 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 "logger.hpp" #include "bin/projectitemmodel.h" #include "timeline2/model/timelinemodel.hpp" #include #include #include #include #include #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-parameter" #pragma GCC diagnostic ignored "-Wsign-conversion" #pragma GCC diagnostic ignored "-Wfloat-equal" #pragma GCC diagnostic ignored "-Wshadow" #pragma GCC diagnostic ignored "-Wpedantic" #include #pragma GCC diagnostic pop thread_local bool Logger::is_executing = false; std::mutex Logger::mut; std::vector Logger::operations; std::vector Logger::invoks; std::unordered_map> Logger::constr; std::unordered_map Logger::translation_table; std::unordered_map Logger::back_translation_table; int Logger::dump_count = 0; thread_local size_t Logger::result_awaiting = INT_MAX; void Logger::init() { std::string cur_ind = "a"; auto incr_ind = [&](auto &&self, size_t i = 0) { if (i >= cur_ind.size()) { cur_ind += "a"; return; } if (cur_ind[i] == 'z') { cur_ind[i] = 'A'; } else if (cur_ind[i] == 'Z') { cur_ind[i] = 'a'; self(self, i + 1); } else { cur_ind[i]++; } }; for (const auto &o : {"TimelineModel", "TrackModel", "test_producer", "test_producer_sound"}) { translation_table[std::string("constr_") + o] = cur_ind; incr_ind(incr_ind); } for (const auto &m : rttr::type::get().get_methods()) { translation_table[m.get_name().to_string()] = cur_ind; incr_ind(incr_ind); } for (const auto &i : translation_table) { back_translation_table[i.second] = i.first; } } bool Logger::start_logging() { std::unique_lock lk(mut); if (is_executing) { return false; } is_executing = true; return true; } void Logger::stop_logging() { std::unique_lock lk(mut); is_executing = false; } -std::string Logger::get_ptr_name(rttr::variant ptr) +std::string Logger::get_ptr_name(const rttr::variant &ptr) { if (ptr.can_convert()) { return "timeline_" + std::to_string(get_id_from_ptr(ptr.convert())); } else if (ptr.can_convert()) { return "binModel"; } else { std::cout << "Error: unhandled ptr type " << ptr.get_type().get_name().to_string() << std::endl; } return "unknown"; } void Logger::log_res(rttr::variant result) { std::unique_lock lk(mut); Q_ASSERT(result_awaiting < invoks.size()); invoks[result_awaiting].res = std::move(result); } void Logger::log_create_producer(const std::string &type, std::vector args) { std::unique_lock lk(mut); for (auto &a : args) { // this will rewove shared/weak/unique ptrs if (a.get_type().is_wrapper()) { a = a.extract_wrapped_value(); } const std::string class_name = a.get_type().get_name().to_string(); } constr[type].push_back({type, std::move(args)}); operations.push_back(ConstrId{type, constr[type].size() - 1}); } namespace { bool isIthParamARef(const rttr::method &method, size_t i) { QString sig = QString::fromStdString(method.get_signature().to_string()); int deb = sig.indexOf("("); int end = sig.lastIndexOf(")"); sig = sig.mid(deb + 1, deb - end - 1); QStringList args = sig.split(QStringLiteral(",")); return args[(int)i].contains("&") && !args[(int)i].contains("const &"); } std::string quoted(const std::string &input) { #if __cpp_lib_quoted_string_io std::stringstream ss; ss << std::quoted(input); return ss.str(); #else // very incomplete implem return "\"" + input + "\""; #endif } } // namespace void Logger::print_trace() { dump_count++; auto process_args = [&](const std::vector &args, const std::unordered_set &refs = {}) { std::stringstream ss; bool deb = true; size_t i = 0; for (const auto &a : args) { if (deb) { deb = false; i = 0; } else { ss << ", "; ++i; } if (refs.count(i) > 0) { ss << "dummy_" << i; } else if (a.get_type() == rttr::type::get()) { ss << a.convert(); } else if (a.get_type() == rttr::type::get()) { ss << (a.convert() ? "true" : "false"); } else if (a.get_type().is_enumeration()) { auto e = a.get_type().get_enumeration(); ss << e.get_name().to_string() << "::" << a.convert(); } else if (a.can_convert()) { ss << quoted(a.convert().toStdString()); } else if (a.can_convert()) { ss << quoted(a.convert()); } else if (a.can_convert>()) { auto set = a.convert>(); ss << "{"; bool beg = true; for (int s : set) { if (beg) beg = false; else ss << ", "; ss << s; } ss << "}"; } else if (a.get_type().is_pointer()) { ss << get_ptr_name(a); } else { std::cout << "Error: unhandled arg type " << a.get_type().get_name().to_string() << std::endl; } } return ss.str(); }; auto process_args_fuzz = [&](const std::vector &args, const std::unordered_set &refs = {}) { std::stringstream ss; bool deb = true; size_t i = 0; for (const auto &a : args) { if (deb) { deb = false; i = 0; } else { ss << " "; ++i; } if (refs.count(i) > 0) { continue; } else if (a.get_type() == rttr::type::get()) { ss << a.convert(); } else if (a.get_type() == rttr::type::get()) { ss << (a.convert() ? "1" : "0"); } else if (a.get_type().is_enumeration()) { ss << a.convert(); } else if (a.can_convert()) { std::string out = a.convert().toStdString(); if (out.empty()) { out = "$$"; } ss << out; } else if (a.can_convert()) { std::string out = a.convert(); if (out.empty()) { out = "$$"; } ss << out; } else if (a.can_convert>()) { auto set = a.convert>(); ss << set.size() << " "; bool beg = true; for (int s : set) { if (beg) beg = false; else ss << " "; ss << s; } } else if (a.get_type().is_pointer()) { if (a.can_convert()) { ss << get_id_from_ptr(a.convert()); } else if (a.can_convert()) { // only one binModel, we skip the parameter since it's unambiguous } else { std::cout << "Error: unhandled ptr type " << a.get_type().get_name().to_string() << std::endl; } } else { std::cout << "Error: unhandled arg type " << a.get_type().get_name().to_string() << std::endl; } } return ss.str(); }; std::ofstream fuzz_file; fuzz_file.open("fuzz_case_" + std::to_string(dump_count) + ".txt"); std::ofstream test_file; test_file.open("test_case_" + std::to_string(dump_count) + ".cpp"); test_file << "TEST_CASE(\"Regression\") {" << std::endl; test_file << "auto binModel = pCore->projectItemModel();" << std::endl; test_file << "std::shared_ptr undoStack = std::make_shared(nullptr);" << std::endl; test_file << "std::shared_ptr guideModel = std::make_shared(undoStack);" << std::endl; test_file << "TimelineModel::next_id = 0;" << std::endl; test_file << "{" << std::endl; test_file << "Mock pmMock;" << std::endl; test_file << "When(Method(pmMock, undoStack)).AlwaysReturn(undoStack);" << std::endl; test_file << "ProjectManager &mocked = pmMock.get();" << std::endl; test_file << "pCore->m_projectManager = &mocked;" << std::endl; auto check_consistancy = [&]() { if (constr.count("TimelineModel") > 0) { for (size_t i = 0; i < constr["TimelineModel"].size(); ++i) { test_file << "REQUIRE(timeline_" << i << "->checkConsistency());" << std::endl; } } }; for (const auto &o : operations) { if (o.can_convert()) { InvokId id = o.convert(); Invok &invok = invoks[id.id]; std::unordered_set refs; rttr::method m = invok.ptr.get_type().get_method(invok.method); test_file << "{" << std::endl; for (const auto &a : m.get_parameter_infos()) { if (isIthParamARef(m, a.get_index())) { refs.insert(a.get_index()); test_file << a.get_type().get_name().to_string() << " dummy_" << std::to_string(a.get_index()) << ";" << std::endl; } } if (m.get_return_type() != rttr::type::get()) { test_file << m.get_return_type().get_name().to_string() << " res = "; } test_file << get_ptr_name(invok.ptr) << "->" << invok.method << "(" << process_args(invok.args, refs) << ");" << std::endl; if (m.get_return_type() != rttr::type::get() && invok.res.is_valid()) { test_file << "REQUIRE( res == " << invok.res.to_string() << ");" << std::endl; } test_file << "}" << std::endl; std::string invok_name = invok.method; if (translation_table.count(invok_name) > 0) { auto args = invok.args; if (rttr::type::get().get_method(invok_name).is_valid()) { args.insert(args.begin(), invok.ptr); // adding an arg just messed up the references std::unordered_set new_refs; for (const size_t &r : refs) { new_refs.insert(r + 1); } std::swap(refs, new_refs); } fuzz_file << translation_table[invok_name] << " " << process_args_fuzz(args, refs) << std::endl; } else { std::cout << "ERROR: unknown method " << invok_name << std::endl; } } else if (o.can_convert()) { ConstrId id = o.convert(); std::string constr_name = std::string("constr_") + id.type; if (translation_table.count(constr_name) > 0) { fuzz_file << translation_table[constr_name] << " " << process_args_fuzz(constr[id.type][id.id].second) << std::endl; } else { std::cout << "ERROR: unknown constructor " << constr_name << std::endl; } if (id.type == "TimelineModel") { test_file << "TimelineItemModel tim_" << id.id << "(®_profile, undoStack);" << std::endl; test_file << "Mock timMock_" << id.id << "(tim_" << id.id << ");" << std::endl; test_file << "auto timeline_" << id.id << " = std::shared_ptr(&timMock_" << id.id << ".get(), [](...) {});" << std::endl; test_file << "TimelineItemModel::finishConstruct(timeline_" << id.id << ", guideModel);" << std::endl; test_file << "Fake(Method(timMock_" << id.id << ", adjustAssetRange));" << std::endl; } else if (id.type == "TrackModel") { std::string params = process_args(constr[id.type][id.id].second); test_file << "TrackModel::construct(" << params << ");" << std::endl; } else if (id.type == "test_producer") { std::string params = process_args(constr[id.type][id.id].second); test_file << "createProducer(reg_profile, " << params << ");" << std::endl; } else if (id.type == "test_producer_sound") { std::string params = process_args(constr[id.type][id.id].second); test_file << "createProducerWithSound(reg_profile, " << params << ");" << std::endl; } else { std::cout << "Error: unknown constructor " << id.type << std::endl; } } else { std::cout << "Error: unknown operation" << std::endl; } check_consistancy(); test_file << "undoStack->undo();" << std::endl; check_consistancy(); test_file << "undoStack->redo();" << std::endl; check_consistancy(); } test_file << "}" << std::endl; test_file << "pCore->m_projectManager = nullptr;" << std::endl; test_file << "}" << std::endl; } void Logger::clear() { is_executing = false; invoks.clear(); operations.clear(); } LogGuard::LogGuard() { m_hasGuard = Logger::start_logging(); } LogGuard::~LogGuard() { if (m_hasGuard) { Logger::stop_logging(); } } bool LogGuard::hasGuard() const { return m_hasGuard; } diff --git a/src/logger.hpp b/src/logger.hpp index 556e32f2b..8720e51fd 100644 --- a/src/logger.hpp +++ b/src/logger.hpp @@ -1,180 +1,180 @@ /*************************************************************************** * Copyright (C) 2019 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 stdd::it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #pragma once #include #include #include #include #include #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-parameter" #pragma GCC diagnostic ignored "-Wsign-conversion" #pragma GCC diagnostic ignored "-Wfloat-equal" #pragma GCC diagnostic ignored "-Wshadow" #pragma GCC diagnostic ignored "-Wpedantic" #include #pragma GCC diagnostic pop /** @brief This class is meant to provide an easy way to reproduce bugs involving the model. * The idea is to log any modifier function involving a model class, and trace the parameters that were passed, to be able to generate a test-case producing the * same behaviour. Note that many modifier functions of the models are nested. We are only interested in the top-most call, and we must ignore bottom calls. */ class Logger { public: /// @brief Inits the logger. Must be called at startup static void init(); /** @brief Notify the logger that the current thread wants to start logging. * This function returns true if this is a top-level call, meaning that we indeed want to log it. If the function returns false, the caller must not log. */ static bool start_logging(); /** @brief This logs the construction of an object of type T, whose new instance is passed. The instance will be kept around in case future calls refer to * it. The arguments should more or less match the constructor arguments. In general, it's better to call the corresponding macro TRACE_CONSTR */ template static void log_constr(T *inst, std::vector args); /** @brief Logs the call to a member function on a given instance of class T. The string contains the method name, and then the vector contains all the * parameters. In general, the method should be registered in RTTR. It's better to call the corresponding macro TRACE() if appropriate */ template static void log(T *inst, std::string str, std::vector args); static void log_create_producer(const std::string &type, std::vector args); /** @brief When the last function logged has a return value, you can log it through this function, by passing the corresponding value. In general, it's * better to call the macro TRACE_RES */ static void log_res(rttr::variant result); /// @brief Notify that we are done with our function. Must not be called if start_logging returned false. static void stop_logging(); static void print_trace(); /// @brief Resets the current log static void clear(); static std::unordered_map translation_table; static std::unordered_map back_translation_table; protected: /** @brief Look amongst the known instances to get the name of a given pointer */ - static std::string get_ptr_name(rttr::variant ptr); + static std::string get_ptr_name(const rttr::variant &ptr); template static size_t get_id_from_ptr(T *ptr); struct InvokId { size_t id; }; struct ConstrId { std::string type; size_t id; }; // a construction log contains the pointer as first parameter, and the vector of parameters using Constr = std::pair>; struct Invok { rttr::variant ptr; std::string method; std::vector args; rttr::variant res; }; thread_local static bool is_executing; thread_local static size_t result_awaiting; static std::mutex mut; static std::vector operations; static std::unordered_map> constr; static std::vector invoks; static int dump_count; }; /** @brief This class provides a RAII mechanism to log the execution of a function */ class LogGuard { public: LogGuard(); ~LogGuard(); // @brief Returns true if we are the top-level caller. bool hasGuard() const; protected: bool m_hasGuard = false; }; /// See Logger::log_constr. Note that the macro fills in the ptr instance for you. #define TRACE_CONSTR(ptr, ...) \ LogGuard __guard; \ if (__guard.hasGuard()) { \ Logger::log_constr((ptr), {__VA_ARGS__}); \ } /// See Logger::log. Note that the macro fills the ptr instance and the method name for you. #define TRACE(...) \ LogGuard __guard; \ if (__guard.hasGuard()) { \ Logger::log(this, __FUNCTION__, {__VA_ARGS__}); \ } /// See Logger::log_res #define TRACE_RES(res) \ if (__guard.hasGuard()) { \ Logger::log_res(res); \ } /******* Implementations ***********/ template void Logger::log_constr(T *inst, std::vector args) { std::unique_lock lk(mut); for (auto &a : args) { // this will rewove shared/weak/unique ptrs if (a.get_type().is_wrapper()) { a = a.extract_wrapped_value(); } } std::string class_name = rttr::type::get().get_name().to_string(); constr[class_name].push_back({inst, std::move(args)}); operations.push_back(ConstrId{class_name, constr[class_name].size() - 1}); } template void Logger::log(T *inst, std::string fctName, std::vector args) { std::unique_lock lk(mut); for (auto &a : args) { // this will rewove shared/weak/unique ptrs if (a.get_type().is_wrapper()) { a = a.extract_wrapped_value(); } } std::string class_name = rttr::type::get().get_name().to_string(); invoks.push_back({inst, std::move(fctName), std::move(args), rttr::variant()}); operations.push_back(InvokId{invoks.size() - 1}); result_awaiting = invoks.size() - 1; } template size_t Logger::get_id_from_ptr(T *ptr) { const std::string class_name = rttr::type::get().get_name().to_string(); for (size_t i = 0; i < constr.at(class_name).size(); ++i) { if (constr.at(class_name)[i].first.convert() == ptr) { return i; } } std::cerr << "Error: ptr of type " << class_name << " not found" << std::endl; return INT_MAX; } diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index df7b0f6b3..986f152ba 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1,3798 +1,3798 @@ /*************************************************************************** * Copyright (C) 2007 by Jean-Baptiste Mardelle (jb@kdenlive.org) * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "mainwindow.h" #include "assets/assetpanel.hpp" #include "bin/clipcreator.hpp" #include "bin/generators/generators.h" #include "bin/projectclip.h" #include "bin/projectfolder.h" #include "bin/projectitemmodel.h" #include "core.h" #include "dialogs/clipcreationdialog.h" #include "dialogs/kdenlivesettingsdialog.h" #include "dialogs/renderwidget.h" #include "dialogs/wizard.h" #include "doc/docundostack.hpp" #include "doc/kdenlivedoc.h" #include "effects/effectlist/view/effectlistwidget.hpp" #include "effectslist/effectbasket.h" #include "hidetitlebars.h" #include "jobs/jobmanager.h" #include "jobs/scenesplitjob.hpp" #include "jobs/speedjob.hpp" #include "jobs/stabilizejob.hpp" #include "kdenlivesettings.h" #include "layoutmanagement.h" #include "library/librarywidget.h" #include "mainwindowadaptor.h" #include "mltconnection.h" #include "mltcontroller/clipcontroller.h" #include "monitor/monitor.h" #include "monitor/monitormanager.h" #include "monitor/scopes/audiographspectrum.h" #include "profiles/profilemodel.hpp" #include "project/cliptranscode.h" #include "project/dialogs/archivewidget.h" #include "project/dialogs/projectsettings.h" #include "project/projectcommands.h" #include "project/projectmanager.h" #include "scopes/scopemanager.h" #include "timeline2/view/timelinecontroller.h" #include "timeline2/view/timelinetabs.hpp" #include "timeline2/view/timelinewidget.h" #include "titler/titlewidget.h" #include "transitions/transitionlist/view/transitionlistwidget.hpp" #include "transitions/transitionsrepository.hpp" #include "utils/resourcewidget.h" #include "utils/thememanager.h" #include "profiles/profilerepository.hpp" #include "widgets/progressbutton.h" #include #include "project/dialogs/temporarydata.h" #ifdef USE_JOGSHUTTLE #include "jogshuttle/jogmanager.h" #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kdenlive_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static const char version[] = KDENLIVE_VERSION; namespace Mlt { class Producer; } QMap MainWindow::m_lumacache; QMap MainWindow::m_lumaFiles; /*static bool sortByNames(const QPair &a, const QPair &b) { return a.first < b.first; }*/ // determine the default KDE style as defined BY THE USER // (as opposed to whatever style KDE considers default) static QString defaultStyle(const char *fallback = nullptr) { KSharedConfigPtr kdeGlobals = KSharedConfig::openConfig(QStringLiteral("kdeglobals"), KConfig::NoGlobals); KConfigGroup cg(kdeGlobals, "KDE"); return cg.readEntry("widgetStyle", fallback); } MainWindow::MainWindow(QWidget *parent) : KXmlGuiWindow(parent) , m_exitCode(EXIT_SUCCESS) , m_assetPanel(nullptr) , m_clipMonitor(nullptr) , m_projectMonitor(nullptr) , m_timelineTabs(nullptr) , m_renderWidget(nullptr) , m_messageLabel(nullptr) , m_themeInitialized(false) , m_isDarkTheme(false) { } void MainWindow::init() { QString desktopStyle = QApplication::style()->objectName(); // Load themes auto themeManager = new ThemeManager(actionCollection()); actionCollection()->addAction(QStringLiteral("themes_menu"), themeManager); connect(themeManager, &ThemeManager::themeChanged, this, &MainWindow::slotThemeChanged); if (!KdenliveSettings::widgetstyle().isEmpty() && QString::compare(desktopStyle, KdenliveSettings::widgetstyle(), Qt::CaseInsensitive) != 0) { // User wants a custom widget style, init doChangeStyle(); } // Widget themes for non KDE users KActionMenu *stylesAction = new KActionMenu(i18n("Style"), this); auto *stylesGroup = new QActionGroup(stylesAction); // GTK theme does not work well with Kdenlive, and does not support color theming, so avoid it QStringList availableStyles = QStyleFactory::keys(); if (KdenliveSettings::widgetstyle().isEmpty()) { // First run QStringList incompatibleStyles = {QStringLiteral("GTK+"), QStringLiteral("windowsvista"), QStringLiteral("windowsxp")}; if (incompatibleStyles.contains(desktopStyle, Qt::CaseInsensitive)) { if (availableStyles.contains(QStringLiteral("breeze"), Qt::CaseInsensitive)) { // Auto switch to Breeze theme KdenliveSettings::setWidgetstyle(QStringLiteral("Breeze")); } else if (availableStyles.contains(QStringLiteral("fusion"), Qt::CaseInsensitive)) { KdenliveSettings::setWidgetstyle(QStringLiteral("Fusion")); } } else { KdenliveSettings::setWidgetstyle(QStringLiteral("Default")); } } // Add default style action QAction *defaultStyle = new QAction(i18n("Default"), stylesGroup); defaultStyle->setData(QStringLiteral("Default")); defaultStyle->setCheckable(true); stylesAction->addAction(defaultStyle); if (KdenliveSettings::widgetstyle() == QLatin1String("Default") || KdenliveSettings::widgetstyle().isEmpty()) { defaultStyle->setChecked(true); } for (const QString &style : availableStyles) { auto *a = new QAction(style, stylesGroup); a->setCheckable(true); a->setData(style); if (KdenliveSettings::widgetstyle() == style) { a->setChecked(true); } stylesAction->addAction(a); } connect(stylesGroup, &QActionGroup::triggered, this, &MainWindow::slotChangeStyle); // QIcon::setThemeSearchPaths(QStringList() <setCurrentProfile(defaultProfile.isEmpty() ? ProjectManager::getDefaultProjectFormat() : defaultProfile); m_commandStack = new QUndoGroup(); // If using a custom profile, make sure the file exists or fallback to default QString currentProfilePath = pCore->getCurrentProfile()->path(); if (currentProfilePath.startsWith(QLatin1Char('/')) && !QFile::exists(currentProfilePath)) { KMessageBox::sorry(this, i18n("Cannot find your default profile, switching to ATSC 1080p 25")); pCore->setCurrentProfile(QStringLiteral("atsc_1080p_25")); KdenliveSettings::setDefault_profile(QStringLiteral("atsc_1080p_25")); } m_gpuAllowed = EffectsRepository::get()->hasInternalEffect(QStringLiteral("glsl.manager")); m_shortcutRemoveFocus = new QShortcut(QKeySequence(QStringLiteral("Esc")), this); connect(m_shortcutRemoveFocus, &QShortcut::activated, this, &MainWindow::slotRemoveFocus); /// Add Widgets setDockOptions(dockOptions() | QMainWindow::AllowNestedDocks | QMainWindow::AllowTabbedDocks); setDockOptions(dockOptions() | QMainWindow::GroupedDragging); setTabPosition(Qt::AllDockWidgetAreas, (QTabWidget::TabPosition)KdenliveSettings::tabposition()); m_timelineToolBar = toolBar(QStringLiteral("timelineToolBar")); m_timelineToolBarContainer = new QWidget(this); auto *ctnLay = new QVBoxLayout; ctnLay->setSpacing(0); ctnLay->setContentsMargins(0, 0, 0, 0); m_timelineToolBarContainer->setLayout(ctnLay); ctnLay->addWidget(m_timelineToolBar); KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup mainConfig(config, QStringLiteral("MainWindow")); KConfigGroup tbGroup(&mainConfig, QStringLiteral("Toolbar timelineToolBar")); m_timelineToolBar->applySettings(tbGroup); QFrame *fr = new QFrame(this); fr->setFrameShape(QFrame::HLine); fr->setMaximumHeight(1); fr->setLineWidth(1); ctnLay->addWidget(fr); setCentralWidget(m_timelineToolBarContainer); setupActions(); QDockWidget *libraryDock = addDock(i18n("Library"), QStringLiteral("library"), pCore->library()); m_clipMonitor = new Monitor(Kdenlive::ClipMonitor, pCore->monitorManager(), this); pCore->bin()->setMonitor(m_clipMonitor); connect(m_clipMonitor, &Monitor::showConfigDialog, this, &MainWindow::slotPreferences); connect(m_clipMonitor, &Monitor::addMarker, this, &MainWindow::slotAddMarkerGuideQuickly); connect(m_clipMonitor, &Monitor::deleteMarker, this, &MainWindow::slotDeleteClipMarker); connect(m_clipMonitor, &Monitor::seekToPreviousSnap, this, &MainWindow::slotSnapRewind); connect(m_clipMonitor, &Monitor::seekToNextSnap, this, &MainWindow::slotSnapForward); connect(pCore->bin(), &Bin::findInTimeline, this, &MainWindow::slotClipInTimeline); // TODO deprecated, replace with Bin methods if necessary /*connect(m_projectList, SIGNAL(loadingIsOver()), this, SLOT(slotElapsedTime())); connect(m_projectList, SIGNAL(updateRenderStatus()), this, SLOT(slotCheckRenderStatus())); connect(m_projectList, SIGNAL(updateProfile(QString)), this, SLOT(slotUpdateProjectProfile(QString))); connect(m_projectList, SIGNAL(refreshClip(QString,bool)), pCore->monitorManager(), SLOT(slotRefreshCurrentMonitor(QString))); connect(m_clipMonitor, SIGNAL(zoneUpdated(QPoint)), m_projectList, SLOT(slotUpdateClipCut(QPoint)));*/ // TODO refac : reimplement ? // connect(m_clipMonitor, &Monitor::extractZone, pCore->bin(), &Bin::slotStartCutJob); connect(m_clipMonitor, &Monitor::passKeyPress, this, &MainWindow::triggerKey); m_projectMonitor = new Monitor(Kdenlive::ProjectMonitor, pCore->monitorManager(), this); connect(m_projectMonitor, &Monitor::passKeyPress, this, &MainWindow::triggerKey); connect(m_projectMonitor, &Monitor::addMarker, this, &MainWindow::slotAddMarkerGuideQuickly); connect(m_projectMonitor, &Monitor::deleteMarker, this, &MainWindow::slotDeleteGuide); connect(m_projectMonitor, &Monitor::seekToPreviousSnap, this, &MainWindow::slotSnapRewind); connect(m_projectMonitor, &Monitor::seekToNextSnap, this, &MainWindow::slotSnapForward); connect(m_loopClip, &QAction::triggered, m_projectMonitor, &Monitor::slotLoopClip); pCore->monitorManager()->initMonitors(m_clipMonitor, m_projectMonitor); connect(m_clipMonitor, &Monitor::addMasterEffect, pCore->bin(), &Bin::slotAddEffect); m_timelineTabs = new TimelineTabs(this); ctnLay->addWidget(m_timelineTabs); // Audio spectrum scope m_audioSpectrum = new AudioGraphSpectrum(pCore->monitorManager()); QDockWidget *spectrumDock = addDock(i18n("Audio Spectrum"), QStringLiteral("audiospectrum"), m_audioSpectrum); // Close library and audiospectrum on first run libraryDock->close(); spectrumDock->close(); m_projectBinDock = addDock(i18n("Project Bin"), QStringLiteral("project_bin"), pCore->bin()); m_assetPanel = new AssetPanel(this); connect(m_assetPanel, &AssetPanel::doSplitEffect, m_projectMonitor, &Monitor::slotSwitchCompare); connect(m_assetPanel, &AssetPanel::doSplitBinEffect, m_clipMonitor, &Monitor::slotSwitchCompare); connect(m_assetPanel, &AssetPanel::changeSpeed, this, &MainWindow::slotChangeSpeed); connect(m_timelineTabs, &TimelineTabs::showTransitionModel, m_assetPanel, &AssetPanel::showTransition); connect(m_timelineTabs, &TimelineTabs::showItemEffectStack, m_assetPanel, &AssetPanel::showEffectStack); connect(m_timelineTabs, &TimelineTabs::updateZoom, this, &MainWindow::updateZoomSlider); connect(pCore->bin(), &Bin::requestShowEffectStack, m_assetPanel, &AssetPanel::showEffectStack); connect(this, &MainWindow::clearAssetPanel, m_assetPanel, &AssetPanel::clearAssetPanel); connect(m_assetPanel, &AssetPanel::seekToPos, [this](int pos) { ObjectId oId = m_assetPanel->effectStackOwner(); switch (oId.first) { case ObjectType::TimelineTrack: case ObjectType::TimelineClip: case ObjectType::TimelineComposition: getCurrentTimeline()->controller()->setPosition(pos); break; case ObjectType::BinClip: m_clipMonitor->requestSeek(pos); break; default: qDebug() << "ERROR unhandled object type"; break; } }); m_effectStackDock = addDock(i18n("Properties"), QStringLiteral("effect_stack"), m_assetPanel); m_effectList2 = new EffectListWidget(this); connect(m_effectList2, &EffectListWidget::activateAsset, pCore->projectManager(), &ProjectManager::activateAsset); connect(m_assetPanel, &AssetPanel::reloadEffect, m_effectList2, &EffectListWidget::reloadCustomEffect); m_effectListDock = addDock(i18n("Effects"), QStringLiteral("effect_list"), m_effectList2); m_transitionList2 = new TransitionListWidget(this); m_transitionListDock = addDock(i18n("Transitions"), QStringLiteral("transition_list"), m_transitionList2); // Add monitors here to keep them at the right of the window m_clipMonitorDock = addDock(i18n("Clip Monitor"), QStringLiteral("clip_monitor"), m_clipMonitor); m_projectMonitorDock = addDock(i18n("Project Monitor"), QStringLiteral("project_monitor"), m_projectMonitor); m_undoView = new QUndoView(); m_undoView->setCleanIcon(QIcon::fromTheme(QStringLiteral("edit-clear"))); m_undoView->setEmptyLabel(i18n("Clean")); m_undoView->setGroup(m_commandStack); m_undoViewDock = addDock(i18n("Undo History"), QStringLiteral("undo_history"), m_undoView); // Color and icon theme stuff connect(m_commandStack, &QUndoGroup::cleanChanged, m_saveAction, &QAction::setDisabled); addAction(QStringLiteral("styles_menu"), stylesAction); QAction *iconAction = new QAction(i18n("Force Breeze Icon Theme"), this); iconAction->setCheckable(true); iconAction->setChecked(KdenliveSettings::force_breeze()); addAction(QStringLiteral("force_icon_theme"), iconAction); connect(iconAction, &QAction::triggered, this, &MainWindow::forceIconSet); // Close non-general docks for the initial layout // only show important ones m_undoViewDock->close(); /// Tabify Widgets tabifyDockWidget(m_transitionListDock, m_effectListDock); tabifyDockWidget(m_effectStackDock, pCore->bin()->clipPropertiesDock()); // tabifyDockWidget(m_effectListDock, m_effectStackDock); tabifyDockWidget(m_clipMonitorDock, m_projectMonitorDock); bool firstRun = readOptions(); // Monitor Record action addAction(QStringLiteral("switch_monitor_rec"), m_clipMonitor->recAction()); // Build effects menu m_effectsMenu = new QMenu(i18n("Add Effect"), this); m_effectActions = new KActionCategory(i18n("Effects"), actionCollection()); m_effectList2->reloadEffectMenu(m_effectsMenu, m_effectActions); m_transitionsMenu = new QMenu(i18n("Add Transition"), this); m_transitionActions = new KActionCategory(i18n("Transitions"), actionCollection()); auto *scmanager = new ScopeManager(this); new LayoutManagement(this); new HideTitleBars(this); m_extraFactory = new KXMLGUIClient(this); buildDynamicActions(); // Create Effect Basket (dropdown list of favorites) m_effectBasket = new EffectBasket(this); connect(m_effectBasket, &EffectBasket::activateAsset, pCore->projectManager(), &ProjectManager::activateAsset); connect(m_effectList2, &EffectListWidget::reloadFavorites, m_effectBasket, &EffectBasket::slotReloadBasket); auto *widgetlist = new QWidgetAction(this); widgetlist->setDefaultWidget(m_effectBasket); // widgetlist->setText(i18n("Favorite Effects")); widgetlist->setToolTip(i18n("Favorite Effects")); widgetlist->setIcon(QIcon::fromTheme(QStringLiteral("favorite"))); auto *menu = new QMenu(this); menu->addAction(widgetlist); auto *basketButton = new QToolButton(this); basketButton->setMenu(menu); basketButton->setToolButtonStyle(toolBar()->toolButtonStyle()); basketButton->setDefaultAction(widgetlist); basketButton->setPopupMode(QToolButton::InstantPopup); // basketButton->setText(i18n("Favorite Effects")); basketButton->setToolTip(i18n("Favorite Effects")); basketButton->setIcon(QIcon::fromTheme(QStringLiteral("favorite"))); auto *toolButtonAction = new QWidgetAction(this); toolButtonAction->setText(i18n("Favorite Effects")); toolButtonAction->setIcon(QIcon::fromTheme(QStringLiteral("favorite"))); toolButtonAction->setDefaultWidget(basketButton); addAction(QStringLiteral("favorite_effects"), toolButtonAction); connect(toolButtonAction, &QAction::triggered, basketButton, &QToolButton::showMenu); // Render button ProgressButton *timelineRender = new ProgressButton(i18n("Render"), 100, this); auto *tlrMenu = new QMenu(this); timelineRender->setMenu(tlrMenu); connect(this, &MainWindow::setRenderProgress, timelineRender, &ProgressButton::setProgress); auto *renderButtonAction = new QWidgetAction(this); renderButtonAction->setText(i18n("Render Button")); renderButtonAction->setIcon(QIcon::fromTheme(QStringLiteral("media-record"))); renderButtonAction->setDefaultWidget(timelineRender); addAction(QStringLiteral("project_render_button"), renderButtonAction); // Timeline preview button ProgressButton *timelinePreview = new ProgressButton(i18n("Rendering preview"), 1000, this); auto *tlMenu = new QMenu(this); timelinePreview->setMenu(tlMenu); connect(this, &MainWindow::setPreviewProgress, timelinePreview, &ProgressButton::setProgress); auto *previewButtonAction = new QWidgetAction(this); previewButtonAction->setText(i18n("Timeline Preview")); previewButtonAction->setIcon(QIcon::fromTheme(QStringLiteral("preview-render-on"))); previewButtonAction->setDefaultWidget(timelinePreview); addAction(QStringLiteral("timeline_preview_button"), previewButtonAction); setupGUI(KXmlGuiWindow::ToolBar | KXmlGuiWindow::StatusBar | KXmlGuiWindow::Save | KXmlGuiWindow::Create); if (firstRun) { if (QScreen *current = QApplication::primaryScreen()) { if (current->availableSize().height() < 1000) { resize(current->availableSize()); } else { resize(current->availableSize() / 1.5); } } } updateActionsToolTip(); m_timelineToolBar->setToolButtonStyle(Qt::ToolButtonFollowStyle); m_timelineToolBar->setProperty("otherToolbar", true); timelinePreview->setToolButtonStyle(m_timelineToolBar->toolButtonStyle()); connect(m_timelineToolBar, &QToolBar::toolButtonStyleChanged, timelinePreview, &ProgressButton::setToolButtonStyle); timelineRender->setToolButtonStyle(toolBar()->toolButtonStyle()); /*ScriptingPart* sp = new ScriptingPart(this, QStringList()); guiFactory()->addClient(sp);*/ loadGenerators(); loadDockActions(); loadClipActions(); // Connect monitor overlay info menu. QMenu *monitorOverlay = static_cast(factory()->container(QStringLiteral("monitor_config_overlay"), this)); connect(monitorOverlay, &QMenu::triggered, this, &MainWindow::slotSwitchMonitorOverlay); m_projectMonitor->setupMenu(static_cast(factory()->container(QStringLiteral("monitor_go"), this)), monitorOverlay, m_playZone, m_loopZone, nullptr, m_loopClip); m_clipMonitor->setupMenu(static_cast(factory()->container(QStringLiteral("monitor_go"), this)), monitorOverlay, m_playZone, m_loopZone, static_cast(factory()->container(QStringLiteral("marker_menu"), this))); QMenu *clipInTimeline = static_cast(factory()->container(QStringLiteral("clip_in_timeline"), this)); clipInTimeline->setIcon(QIcon::fromTheme(QStringLiteral("go-jump"))); pCore->bin()->setupGeneratorMenu(); connect(pCore->monitorManager(), &MonitorManager::updateOverlayInfos, this, &MainWindow::slotUpdateMonitorOverlays); // Setup and fill effects and transitions menus. QMenu *m = static_cast(factory()->container(QStringLiteral("video_effects_menu"), this)); connect(m, &QMenu::triggered, this, &MainWindow::slotAddEffect); connect(m_effectsMenu, &QMenu::triggered, this, &MainWindow::slotAddEffect); connect(m_transitionsMenu, &QMenu::triggered, this, &MainWindow::slotAddTransition); m_timelineContextMenu = new QMenu(this); m_timelineContextMenu->addAction(actionCollection()->action(QStringLiteral("insert_space"))); m_timelineContextMenu->addAction(actionCollection()->action(QStringLiteral("delete_space"))); m_timelineContextMenu->addAction(actionCollection()->action(QStringLiteral("delete_space_all_tracks"))); m_timelineContextMenu->addAction(actionCollection()->action(KStandardAction::name(KStandardAction::Paste))); // QMenu *markersMenu = static_cast(factory()->container(QStringLiteral("marker_menu"), this)); /*m_timelineClipActions->addMenu(markersMenu); m_timelineClipActions->addSeparator(); m_timelineClipActions->addMenu(m_transitionsMenu); m_timelineClipActions->addMenu(m_effectsMenu);*/ slotConnectMonitors(); m_timelineToolBar->setToolButtonStyle(Qt::ToolButtonIconOnly); // TODO: let user select timeline toolbar toolbutton style // connect(toolBar(), &QToolBar::iconSizeChanged, m_timelineToolBar, &QToolBar::setToolButtonStyle); m_timelineToolBar->setContextMenuPolicy(Qt::CustomContextMenu); connect(m_timelineToolBar, &QWidget::customContextMenuRequested, this, &MainWindow::showTimelineToolbarMenu); QAction *prevRender = actionCollection()->action(QStringLiteral("prerender_timeline_zone")); QAction *stopPrevRender = actionCollection()->action(QStringLiteral("stop_prerender_timeline")); tlMenu->addAction(stopPrevRender); tlMenu->addAction(actionCollection()->action(QStringLiteral("set_render_timeline_zone"))); tlMenu->addAction(actionCollection()->action(QStringLiteral("unset_render_timeline_zone"))); tlMenu->addAction(actionCollection()->action(QStringLiteral("clear_render_timeline_zone"))); // Automatic timeline preview action QAction *autoRender = new QAction(QIcon::fromTheme(QStringLiteral("view-refresh")), i18n("Automatic Preview"), this); autoRender->setCheckable(true); autoRender->setChecked(KdenliveSettings::autopreview()); connect(autoRender, &QAction::triggered, this, &MainWindow::slotToggleAutoPreview); tlMenu->addAction(autoRender); tlMenu->addSeparator(); tlMenu->addAction(actionCollection()->action(QStringLiteral("disable_preview"))); tlMenu->addAction(actionCollection()->action(QStringLiteral("manage_cache"))); timelinePreview->defineDefaultAction(prevRender, stopPrevRender); timelinePreview->setAutoRaise(true); QAction *showRender = actionCollection()->action(QStringLiteral("project_render")); tlrMenu->addAction(showRender); tlrMenu->addAction(actionCollection()->action(QStringLiteral("stop_project_render"))); timelineRender->defineDefaultAction(showRender, showRender); timelineRender->setAutoRaise(true); // Populate encoding profiles KConfig conf(QStringLiteral("encodingprofiles.rc"), KConfig::CascadeConfig, QStandardPaths::AppDataLocation); /*KConfig conf(QStringLiteral("encodingprofiles.rc"), KConfig::CascadeConfig, QStandardPaths::AppDataLocation); if (KdenliveSettings::proxyparams().isEmpty() || KdenliveSettings::proxyextension().isEmpty()) { KConfigGroup group(&conf, "proxy"); QMap values = group.entryMap(); QMapIterator i(values); if (i.hasNext()) { i.next(); QString proxystring = i.value(); KdenliveSettings::setProxyparams(proxystring.section(QLatin1Char(';'), 0, 0)); KdenliveSettings::setProxyextension(proxystring.section(QLatin1Char(';'), 1, 1)); } }*/ if (KdenliveSettings::v4l_parameters().isEmpty() || KdenliveSettings::v4l_extension().isEmpty()) { KConfigGroup group(&conf, "video4linux"); QMap values = group.entryMap(); QMapIterator i(values); if (i.hasNext()) { i.next(); QString v4lstring = i.value(); KdenliveSettings::setV4l_parameters(v4lstring.section(QLatin1Char(';'), 0, 0)); KdenliveSettings::setV4l_extension(v4lstring.section(QLatin1Char(';'), 1, 1)); } } if (KdenliveSettings::grab_parameters().isEmpty() || KdenliveSettings::grab_extension().isEmpty()) { KConfigGroup group(&conf, "screengrab"); QMap values = group.entryMap(); QMapIterator i(values); if (i.hasNext()) { i.next(); QString grabstring = i.value(); KdenliveSettings::setGrab_parameters(grabstring.section(QLatin1Char(';'), 0, 0)); KdenliveSettings::setGrab_extension(grabstring.section(QLatin1Char(';'), 1, 1)); } } if (KdenliveSettings::decklink_parameters().isEmpty() || KdenliveSettings::decklink_extension().isEmpty()) { KConfigGroup group(&conf, "decklink"); QMap values = group.entryMap(); QMapIterator i(values); if (i.hasNext()) { i.next(); QString decklinkstring = i.value(); KdenliveSettings::setDecklink_parameters(decklinkstring.section(QLatin1Char(';'), 0, 0)); KdenliveSettings::setDecklink_extension(decklinkstring.section(QLatin1Char(';'), 1, 1)); } } if (!QDir(KdenliveSettings::currenttmpfolder()).isReadable()) KdenliveSettings::setCurrenttmpfolder(QStandardPaths::writableLocation(QStandardPaths::TempLocation)); QTimer::singleShot(0, this, &MainWindow::GUISetupDone); #ifdef USE_JOGSHUTTLE new JogManager(this); #endif scmanager->slotCheckActiveScopes(); // m_messageLabel->setMessage(QStringLiteral("This is a beta version. Always backup your data"), MltError); } void MainWindow::slotThemeChanged(const QString &name) { KSharedConfigPtr config = KSharedConfig::openConfig(name); QPalette plt = KColorScheme::createApplicationPalette(config); // qApp->setPalette(plt); // Required for qml palette change QGuiApplication::setPalette(plt); QColor background = plt.window().color(); bool useDarkIcons = background.value() < 100; if (m_assetPanel) { m_assetPanel->updatePalette(); } if (m_effectList2) { // Trigger a repaint to have icons adapted m_effectList2->reset(); } if (m_transitionList2) { // Trigger a repaint to have icons adapted m_transitionList2->reset(); } if (m_clipMonitor) { m_clipMonitor->setPalette(plt); } if (m_projectMonitor) { m_projectMonitor->setPalette(plt); } if (m_timelineTabs) { m_timelineTabs->setPalette(plt); getMainTimeline()->controller()->resetView(); } if (m_audioSpectrum) { m_audioSpectrum->refreshPixmap(); } KSharedConfigPtr kconfig = KSharedConfig::openConfig(); KConfigGroup initialGroup(kconfig, "version"); if (initialGroup.exists() && KdenliveSettings::force_breeze() && useDarkIcons != KdenliveSettings::use_dark_breeze()) { // We need to reload icon theme QIcon::setThemeName(useDarkIcons ? QStringLiteral("breeze-dark") : QStringLiteral("breeze")); KdenliveSettings::setUse_dark_breeze(useDarkIcons); } #if KXMLGUI_VERSION_MINOR < 23 && KXMLGUI_VERSION_MAJOR == 5 // Not required anymore with auto colored icons since KF5 5.23 QColor background = plt.window().color(); bool useDarkIcons = background.value() < 100; if (m_themeInitialized && useDarkIcons != m_isDarkTheme) { if (pCore->bin()) { pCore->bin()->refreshIcons(); } if (m_clipMonitor) { m_clipMonitor->refreshIcons(); } if (m_projectMonitor) { m_projectMonitor->refreshIcons(); } if (pCore->monitorManager()) { pCore->monitorManager()->refreshIcons(); } for (QAction *action : actionCollection()->actions()) { QIcon icon = action->icon(); if (icon.isNull()) { continue; } QString iconName = icon.name(); QIcon newIcon = QIcon::fromTheme(iconName); if (newIcon.isNull()) { continue; } action->setIcon(newIcon); } } m_themeInitialized = true; m_isDarkTheme = useDarkIcons; #endif } void MainWindow::updateActionsToolTip() { // Add shortcut to action tooltips QList collections = KActionCollection::allCollections(); for (int i = 0; i < collections.count(); ++i) { KActionCollection *coll = collections.at(i); for (QAction *tempAction : coll->actions()) { // find the shortcut pattern and delete (note the preceding space in the RegEx) QString strippedTooltip = tempAction->toolTip().remove(QRegExp(QStringLiteral("\\s\\(.*\\)"))); // append shortcut if it exists for action if (tempAction->shortcut() == QKeySequence(0)) { tempAction->setToolTip(strippedTooltip); } else { tempAction->setToolTip(strippedTooltip + QStringLiteral(" (") + tempAction->shortcut().toString() + QLatin1Char(')')); } connect(tempAction, &QAction::changed, this, &MainWindow::updateAction); } } } void MainWindow::updateAction() { QAction *action = qobject_cast(sender()); QString toolTip = KLocalizedString::removeAcceleratorMarker(action->toolTip()); QString strippedTooltip = toolTip.remove(QRegExp(QStringLiteral("\\s\\(.*\\)"))); action->setToolTip(i18nc("@info:tooltip Tooltip of toolbar button", "%1 (%2)", strippedTooltip, action->shortcut().toString())); } MainWindow::~MainWindow() { pCore->prepareShutdown(); m_timelineTabs->disconnectTimeline(getMainTimeline()); delete m_audioSpectrum; if (m_projectMonitor) { m_projectMonitor->stop(); } if (m_clipMonitor) { m_clipMonitor->stop(); } ClipController::mediaUnavailable.reset(); delete m_projectMonitor; delete m_clipMonitor; delete m_shortcutRemoveFocus; delete m_effectList2; delete m_transitionList2; qDeleteAll(m_transitions); // Mlt::Factory::close(); } // virtual bool MainWindow::queryClose() { if (m_renderWidget) { int waitingJobs = m_renderWidget->waitingJobsCount(); if (waitingJobs > 0) { switch ( KMessageBox::warningYesNoCancel(this, i18np("You have 1 rendering job waiting in the queue.\nWhat do you want to do with this job?", "You have %1 rendering jobs waiting in the queue.\nWhat do you want to do with these jobs?", waitingJobs), QString(), KGuiItem(i18n("Start them now")), KGuiItem(i18n("Delete them")))) { case KMessageBox::Yes: // create script with waiting jobs and start it if (!m_renderWidget->startWaitingRenderJobs()) { return false; } break; case KMessageBox::No: // Don't do anything, jobs will be deleted break; default: return false; } } } saveOptions(); // WARNING: According to KMainWindow::queryClose documentation we are not supposed to close the document here? return pCore->projectManager()->closeCurrentDocument(true, true); } void MainWindow::loadGenerators() { QMenu *addMenu = static_cast(factory()->container(QStringLiteral("generators"), this)); Generators::getGenerators(KdenliveSettings::producerslist(), addMenu); connect(addMenu, &QMenu::triggered, this, &MainWindow::buildGenerator); } void MainWindow::buildGenerator(QAction *action) { Generators gen(m_clipMonitor, action->data().toString(), this); if (gen.exec() == QDialog::Accepted) { pCore->bin()->slotAddClipToProject(gen.getSavedClip()); } } void MainWindow::saveProperties(KConfigGroup &config) { // save properties here KXmlGuiWindow::saveProperties(config); // TODO: fix session management if (qApp->isSavingSession() && pCore->projectManager()) { if (pCore->currentDoc() && !pCore->currentDoc()->url().isEmpty()) { config.writeEntry("kdenlive_lastUrl", pCore->currentDoc()->url().toLocalFile()); } } } void MainWindow::readProperties(const KConfigGroup &config) { // read properties here KXmlGuiWindow::readProperties(config); // TODO: fix session management /*if (qApp->isSessionRestored()) { pCore->projectManager()->openFile(QUrl::fromLocalFile(config.readEntry("kdenlive_lastUrl", QString()))); }*/ } void MainWindow::saveNewToolbarConfig() { KXmlGuiWindow::saveNewToolbarConfig(); // TODO for some reason all dynamically inserted actions are removed by the save toolbar // So we currently re-add them manually.... loadDockActions(); loadClipActions(); pCore->bin()->rebuildMenu(); QMenu *monitorOverlay = static_cast(factory()->container(QStringLiteral("monitor_config_overlay"), this)); if (monitorOverlay) { m_projectMonitor->setupMenu(static_cast(factory()->container(QStringLiteral("monitor_go"), this)), monitorOverlay, m_playZone, m_loopZone, nullptr, m_loopClip); m_clipMonitor->setupMenu(static_cast(factory()->container(QStringLiteral("monitor_go"), this)), monitorOverlay, m_playZone, m_loopZone, static_cast(factory()->container(QStringLiteral("marker_menu"), this))); } } void MainWindow::slotReloadEffects(const QStringList &paths) { - for (const QString p : paths) { + for (const QString &p : paths) { EffectsRepository::get()->reloadCustom(p); } m_effectList2->reloadEffectMenu(m_effectsMenu, m_effectActions); } void MainWindow::configureNotifications() { KNotifyConfigWidget::configure(this); } void MainWindow::slotFullScreen() { KToggleFullScreenAction::setFullScreen(this, actionCollection()->action(QStringLiteral("fullscreen"))->isChecked()); } void MainWindow::slotConnectMonitors() { // connect(m_projectList, SIGNAL(deleteProjectClips(QStringList,QMap)), this, // SLOT(slotDeleteProjectClips(QStringList,QMap))); connect(m_clipMonitor, &Monitor::refreshClipThumbnail, pCore->bin(), &Bin::slotRefreshClipThumbnail); connect(m_projectMonitor, &Monitor::requestFrameForAnalysis, this, &MainWindow::slotMonitorRequestRenderFrame); connect(m_projectMonitor, &Monitor::createSplitOverlay, this, &MainWindow::createSplitOverlay, Qt::DirectConnection); connect(m_projectMonitor, &Monitor::removeSplitOverlay, this, &MainWindow::removeSplitOverlay, Qt::DirectConnection); } void MainWindow::createSplitOverlay(Mlt::Filter *filter) { getMainTimeline()->controller()->createSplitOverlay(filter); m_projectMonitor->activateSplit(); } void MainWindow::removeSplitOverlay() { getMainTimeline()->controller()->removeSplitOverlay(); } void MainWindow::addAction(const QString &name, QAction *action, const QKeySequence &shortcut, KActionCategory *category) { m_actionNames.append(name); if (category) { category->addAction(name, action); } else { actionCollection()->addAction(name, action); } actionCollection()->setDefaultShortcut(action, shortcut); } QAction *MainWindow::addAction(const QString &name, const QString &text, const QObject *receiver, const char *member, const QIcon &icon, const QKeySequence &shortcut, KActionCategory *category) { auto *action = new QAction(text, this); if (!icon.isNull()) { action->setIcon(icon); } addAction(name, action, shortcut, category); connect(action, SIGNAL(triggered(bool)), receiver, member); return action; } void MainWindow::setupActions() { // create edit mode buttons m_normalEditTool = new QAction(QIcon::fromTheme(QStringLiteral("kdenlive-normal-edit")), i18n("Normal mode"), this); m_normalEditTool->setCheckable(true); m_normalEditTool->setChecked(true); m_overwriteEditTool = new QAction(QIcon::fromTheme(QStringLiteral("kdenlive-overwrite-edit")), i18n("Overwrite mode"), this); m_overwriteEditTool->setCheckable(true); m_overwriteEditTool->setChecked(false); m_insertEditTool = new QAction(QIcon::fromTheme(QStringLiteral("kdenlive-insert-edit")), i18n("Insert mode"), this); m_insertEditTool->setCheckable(true); m_insertEditTool->setChecked(false); KSelectAction *sceneMode = new KSelectAction(i18n("Timeline Edit Mode"), this); sceneMode->addAction(m_normalEditTool); sceneMode->addAction(m_overwriteEditTool); sceneMode->addAction(m_insertEditTool); sceneMode->setCurrentItem(0); connect(sceneMode, static_cast(&KSelectAction::triggered), this, &MainWindow::slotChangeEdit); addAction(QStringLiteral("timeline_mode"), sceneMode); m_useTimelineZone = new KDualAction(i18n("Don't Use Timeline Zone for Insert"), i18n("Use Timeline Zone for Insert"), this); m_useTimelineZone->setActiveIcon(QIcon::fromTheme(QStringLiteral("timeline-use-zone-on"))); m_useTimelineZone->setInactiveIcon(QIcon::fromTheme(QStringLiteral("timeline-use-zone-off"))); m_useTimelineZone->setAutoToggle(true); connect(m_useTimelineZone, &KDualAction::activeChangedByUser, this, &MainWindow::slotSwitchTimelineZone); addAction(QStringLiteral("use_timeline_zone_in_edit"), m_useTimelineZone, Qt::Key_G); m_compositeAction = new KSelectAction(QIcon::fromTheme(QStringLiteral("composite-track-off")), i18n("Track compositing"), this); m_compositeAction->setToolTip(i18n("Track compositing")); QAction *noComposite = new QAction(QIcon::fromTheme(QStringLiteral("composite-track-off")), i18n("None"), this); noComposite->setCheckable(true); noComposite->setData(0); m_compositeAction->addAction(noComposite); QString compose = TransitionsRepository::get()->getCompositingTransition(); if (compose == QStringLiteral("movit.overlay")) { // Movit, do not show "preview" option since movit is faster QAction *hqComposite = new QAction(QIcon::fromTheme(QStringLiteral("composite-track-on")), i18n("High Quality"), this); hqComposite->setCheckable(true); hqComposite->setData(2); m_compositeAction->addAction(hqComposite); m_compositeAction->setCurrentAction(hqComposite); } else { QAction *previewComposite = new QAction(QIcon::fromTheme(QStringLiteral("composite-track-preview")), i18n("Preview"), this); previewComposite->setCheckable(true); previewComposite->setData(1); m_compositeAction->addAction(previewComposite); if (compose != QStringLiteral("composite")) { QAction *hqComposite = new QAction(QIcon::fromTheme(QStringLiteral("composite-track-on")), i18n("High Quality"), this); hqComposite->setData(2); hqComposite->setCheckable(true); m_compositeAction->addAction(hqComposite); m_compositeAction->setCurrentAction(hqComposite); } else { m_compositeAction->setCurrentAction(previewComposite); } } connect(m_compositeAction, static_cast(&KSelectAction::triggered), this, &MainWindow::slotUpdateCompositing); addAction(QStringLiteral("timeline_compositing"), m_compositeAction); QAction *splitView = new QAction(QIcon::fromTheme(QStringLiteral("view-split-top-bottom")), i18n("Split Audio Tracks"), this); addAction(QStringLiteral("timeline_view_split"), splitView); splitView->setData(QVariant::fromValue(1)); splitView->setCheckable(true); splitView->setChecked(KdenliveSettings::audiotracksbelow()); QAction *mixedView = new QAction(QIcon::fromTheme(QStringLiteral("document-new")), i18n("Mixed Audio tracks"), this); addAction(QStringLiteral("timeline_mixed_view"), mixedView); mixedView->setData(QVariant::fromValue(0)); mixedView->setCheckable(true); mixedView->setChecked(!KdenliveSettings::audiotracksbelow()); QActionGroup *clipTypeGroup = new QActionGroup(this); clipTypeGroup->addAction(mixedView); clipTypeGroup->addAction(splitView); connect(clipTypeGroup, &QActionGroup::triggered, this, &MainWindow::slotUpdateTimelineView); auto tlsettings = new QMenu(this); tlsettings->setIcon(QIcon::fromTheme(QStringLiteral("configure"))); tlsettings->addAction(m_compositeAction); tlsettings->addAction(mixedView); tlsettings->addAction(splitView); addAction(QStringLiteral("timeline_settings"), tlsettings->menuAction()); m_timeFormatButton = new KSelectAction(QStringLiteral("00:00:00:00 / 00:00:00:00"), this); m_timeFormatButton->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); m_timeFormatButton->addAction(i18n("hh:mm:ss:ff")); m_timeFormatButton->addAction(i18n("Frames")); if (KdenliveSettings::frametimecode()) { m_timeFormatButton->setCurrentItem(1); } else { m_timeFormatButton->setCurrentItem(0); } connect(m_timeFormatButton, static_cast(&KSelectAction::triggered), this, &MainWindow::slotUpdateTimecodeFormat); m_timeFormatButton->setToolBarMode(KSelectAction::MenuMode); m_timeFormatButton->setToolButtonPopupMode(QToolButton::InstantPopup); addAction(QStringLiteral("timeline_timecode"), m_timeFormatButton); // create tools buttons m_buttonSelectTool = new QAction(QIcon::fromTheme(QStringLiteral("cursor-arrow")), i18n("Selection tool"), this); // toolbar->addAction(m_buttonSelectTool); m_buttonSelectTool->setCheckable(true); m_buttonSelectTool->setChecked(true); m_buttonRazorTool = new QAction(QIcon::fromTheme(QStringLiteral("edit-cut")), i18n("Razor tool"), this); // toolbar->addAction(m_buttonRazorTool); m_buttonRazorTool->setCheckable(true); m_buttonRazorTool->setChecked(false); m_buttonSpacerTool = new QAction(QIcon::fromTheme(QStringLiteral("distribute-horizontal-x")), i18n("Spacer tool"), this); // toolbar->addAction(m_buttonSpacerTool); m_buttonSpacerTool->setCheckable(true); m_buttonSpacerTool->setChecked(false); auto *toolGroup = new QActionGroup(this); toolGroup->addAction(m_buttonSelectTool); toolGroup->addAction(m_buttonRazorTool); toolGroup->addAction(m_buttonSpacerTool); toolGroup->setExclusive(true); // toolbar->setToolButtonStyle(Qt::ToolButtonIconOnly); /*QWidget * actionWidget; int max = toolbar->iconSizeDefault() + 2; actionWidget = toolbar->widgetForAction(m_normalEditTool); actionWidget->setMaximumWidth(max); actionWidget->setMaximumHeight(max - 4); actionWidget = toolbar->widgetForAction(m_insertEditTool); actionWidget->setMaximumWidth(max); actionWidget->setMaximumHeight(max - 4); actionWidget = toolbar->widgetForAction(m_overwriteEditTool); actionWidget->setMaximumWidth(max); actionWidget->setMaximumHeight(max - 4); actionWidget = toolbar->widgetForAction(m_buttonSelectTool); actionWidget->setMaximumWidth(max); actionWidget->setMaximumHeight(max - 4); actionWidget = toolbar->widgetForAction(m_buttonRazorTool); actionWidget->setMaximumWidth(max); actionWidget->setMaximumHeight(max - 4); actionWidget = toolbar->widgetForAction(m_buttonSpacerTool); actionWidget->setMaximumWidth(max); actionWidget->setMaximumHeight(max - 4);*/ connect(toolGroup, &QActionGroup::triggered, this, &MainWindow::slotChangeTool); m_buttonVideoThumbs = new QAction(QIcon::fromTheme(QStringLiteral("kdenlive-show-videothumb")), i18n("Show video thumbnails"), this); m_buttonVideoThumbs->setCheckable(true); m_buttonVideoThumbs->setChecked(KdenliveSettings::videothumbnails()); connect(m_buttonVideoThumbs, &QAction::triggered, this, &MainWindow::slotSwitchVideoThumbs); m_buttonAudioThumbs = new QAction(QIcon::fromTheme(QStringLiteral("kdenlive-show-audiothumb")), i18n("Show audio thumbnails"), this); m_buttonAudioThumbs->setCheckable(true); m_buttonAudioThumbs->setChecked(KdenliveSettings::audiothumbnails()); connect(m_buttonAudioThumbs, &QAction::triggered, this, &MainWindow::slotSwitchAudioThumbs); m_buttonShowMarkers = new QAction(QIcon::fromTheme(QStringLiteral("kdenlive-show-markers")), i18n("Show markers comments"), this); m_buttonShowMarkers->setCheckable(true); m_buttonShowMarkers->setChecked(KdenliveSettings::showmarkers()); connect(m_buttonShowMarkers, &QAction::triggered, this, &MainWindow::slotSwitchMarkersComments); m_buttonSnap = new QAction(QIcon::fromTheme(QStringLiteral("kdenlive-snap")), i18n("Snap"), this); m_buttonSnap->setCheckable(true); m_buttonSnap->setChecked(KdenliveSettings::snaptopoints()); connect(m_buttonSnap, &QAction::triggered, this, &MainWindow::slotSwitchSnap); m_buttonAutomaticTransition = new QAction(QIcon::fromTheme(QStringLiteral("auto-transition")), i18n("Automatic transitions"), this); m_buttonAutomaticTransition->setCheckable(true); m_buttonAutomaticTransition->setChecked(KdenliveSettings::automatictransitions()); connect(m_buttonAutomaticTransition, &QAction::triggered, this, &MainWindow::slotSwitchAutomaticTransition); m_buttonFitZoom = new QAction(QIcon::fromTheme(QStringLiteral("zoom-fit-best")), i18n("Fit zoom to project"), this); m_buttonFitZoom->setCheckable(false); m_zoomSlider = new QSlider(Qt::Horizontal, this); m_zoomSlider->setRange(0, 20); m_zoomSlider->setPageStep(1); m_zoomSlider->setInvertedAppearance(true); m_zoomSlider->setInvertedControls(true); m_zoomSlider->setMaximumWidth(150); m_zoomSlider->setMinimumWidth(100); m_zoomIn = KStandardAction::zoomIn(this, SLOT(slotZoomIn()), actionCollection()); m_zoomOut = KStandardAction::zoomOut(this, SLOT(slotZoomOut()), actionCollection()); connect(m_zoomSlider, SIGNAL(valueChanged(int)), this, SLOT(slotSetZoom(int))); connect(m_zoomSlider, &QAbstractSlider::sliderMoved, this, &MainWindow::slotShowZoomSliderToolTip); connect(m_buttonFitZoom, &QAction::triggered, this, &MainWindow::slotFitZoom); m_trimLabel = new QLabel(QStringLiteral(" "), this); m_trimLabel->setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)); // m_trimLabel->setAutoFillBackground(true); m_trimLabel->setAlignment(Qt::AlignHCenter); m_trimLabel->setStyleSheet(QStringLiteral("QLabel { background-color :red; }")); KToolBar *toolbar = new KToolBar(QStringLiteral("statusToolBar"), this, Qt::BottomToolBarArea); toolbar->setMovable(false); toolbar->setToolButtonStyle(Qt::ToolButtonIconOnly); /*QString styleBorderless = QStringLiteral("QToolButton { border-width: 0px;margin: 1px 3px 0px;padding: 0px;}");*/ toolbar->addWidget(m_trimLabel); toolbar->addAction(m_buttonAutomaticTransition); toolbar->addAction(m_buttonVideoThumbs); toolbar->addAction(m_buttonAudioThumbs); toolbar->addAction(m_buttonShowMarkers); toolbar->addAction(m_buttonSnap); toolbar->addSeparator(); toolbar->addAction(m_buttonFitZoom); toolbar->addAction(m_zoomOut); toolbar->addWidget(m_zoomSlider); toolbar->addAction(m_zoomIn); int small = style()->pixelMetric(QStyle::PM_SmallIconSize); statusBar()->setMaximumHeight(2 * small); m_messageLabel = new StatusBarMessageLabel(this); m_messageLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::MinimumExpanding); connect(this, &MainWindow::displayMessage, m_messageLabel, &StatusBarMessageLabel::setMessage); statusBar()->addWidget(m_messageLabel, 0); QWidget *spacer = new QWidget(this); spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); statusBar()->addWidget(spacer, 1); statusBar()->addPermanentWidget(toolbar); toolbar->setIconSize(QSize(small, small)); toolbar->layout()->setContentsMargins(0, 0, 0, 0); statusBar()->setContentsMargins(0, 0, 0, 0); addAction(QStringLiteral("normal_mode"), m_normalEditTool); addAction(QStringLiteral("overwrite_mode"), m_overwriteEditTool); addAction(QStringLiteral("insert_mode"), m_insertEditTool); addAction(QStringLiteral("select_tool"), m_buttonSelectTool, Qt::Key_S); addAction(QStringLiteral("razor_tool"), m_buttonRazorTool, Qt::Key_X); addAction(QStringLiteral("spacer_tool"), m_buttonSpacerTool, Qt::Key_M); addAction(QStringLiteral("automatic_transition"), m_buttonAutomaticTransition); addAction(QStringLiteral("show_video_thumbs"), m_buttonVideoThumbs); addAction(QStringLiteral("show_audio_thumbs"), m_buttonAudioThumbs); addAction(QStringLiteral("show_markers"), m_buttonShowMarkers); addAction(QStringLiteral("snap"), m_buttonSnap); addAction(QStringLiteral("zoom_fit"), m_buttonFitZoom); addAction(QStringLiteral("run_wizard"), i18n("Run Config Wizard"), this, SLOT(slotRunWizard()), QIcon::fromTheme(QStringLiteral("tools-wizard"))); addAction(QStringLiteral("project_settings"), i18n("Project Settings"), this, SLOT(slotEditProjectSettings()), QIcon::fromTheme(QStringLiteral("configure"))); addAction(QStringLiteral("project_render"), i18n("Render"), this, SLOT(slotRenderProject()), QIcon::fromTheme(QStringLiteral("media-record")), Qt::CTRL + Qt::Key_Return); addAction(QStringLiteral("stop_project_render"), i18n("Stop Render"), this, SLOT(slotStopRenderProject()), QIcon::fromTheme(QStringLiteral("media-record"))); addAction(QStringLiteral("project_clean"), i18n("Clean Project"), this, SLOT(slotCleanProject()), QIcon::fromTheme(QStringLiteral("edit-clear"))); addAction("project_adjust_profile", i18n("Adjust Profile to Current Clip"), pCore->bin(), SLOT(adjustProjectProfileToItem())); m_playZone = addAction(QStringLiteral("monitor_play_zone"), i18n("Play Zone"), pCore->monitorManager(), SLOT(slotPlayZone()), QIcon::fromTheme(QStringLiteral("media-playback-start")), Qt::CTRL + Qt::Key_Space); m_loopZone = addAction(QStringLiteral("monitor_loop_zone"), i18n("Loop Zone"), pCore->monitorManager(), SLOT(slotLoopZone()), QIcon::fromTheme(QStringLiteral("media-playback-start")), Qt::ALT + Qt::Key_Space); m_loopClip = new QAction(QIcon::fromTheme(QStringLiteral("media-playback-start")), i18n("Loop selected clip"), this); addAction(QStringLiteral("monitor_loop_clip"), m_loopClip); m_loopClip->setEnabled(false); addAction(QStringLiteral("dvd_wizard"), i18n("DVD Wizard"), this, SLOT(slotDvdWizard()), QIcon::fromTheme(QStringLiteral("media-optical"))); addAction(QStringLiteral("transcode_clip"), i18n("Transcode Clips"), this, SLOT(slotTranscodeClip()), QIcon::fromTheme(QStringLiteral("edit-copy"))); addAction(QStringLiteral("archive_project"), i18n("Archive Project"), this, SLOT(slotArchiveProject()), QIcon::fromTheme(QStringLiteral("document-save-all"))); addAction(QStringLiteral("switch_monitor"), i18n("Switch monitor"), this, SLOT(slotSwitchMonitors()), QIcon(), Qt::Key_T); addAction(QStringLiteral("expand_timeline_clip"), i18n("Expand Clip"), pCore->projectManager(), SLOT(slotExpandClip()), QIcon::fromTheme(QStringLiteral("document-open"))); QAction *overlayInfo = new QAction(QIcon::fromTheme(QStringLiteral("help-hint")), i18n("Monitor Info Overlay"), this); addAction(QStringLiteral("monitor_overlay"), overlayInfo); overlayInfo->setCheckable(true); overlayInfo->setData(0x01); QAction *overlayTCInfo = new QAction(QIcon::fromTheme(QStringLiteral("help-hint")), i18n("Monitor Overlay Timecode"), this); addAction(QStringLiteral("monitor_overlay_tc"), overlayTCInfo); overlayTCInfo->setCheckable(true); overlayTCInfo->setData(0x02); QAction *overlayFpsInfo = new QAction(QIcon::fromTheme(QStringLiteral("help-hint")), i18n("Monitor Overlay Playback Fps"), this); addAction(QStringLiteral("monitor_overlay_fps"), overlayFpsInfo); overlayFpsInfo->setCheckable(true); overlayFpsInfo->setData(0x20); QAction *overlayMarkerInfo = new QAction(QIcon::fromTheme(QStringLiteral("help-hint")), i18n("Monitor Overlay Markers"), this); addAction(QStringLiteral("monitor_overlay_markers"), overlayMarkerInfo); overlayMarkerInfo->setCheckable(true); overlayMarkerInfo->setData(0x04); QAction *overlayAudioInfo = new QAction(QIcon::fromTheme(QStringLiteral("help-hint")), i18n("Monitor Overlay Audio Waveform"), this); addAction(QStringLiteral("monitor_overlay_audiothumb"), overlayAudioInfo); overlayAudioInfo->setCheckable(true); overlayAudioInfo->setData(0x10); QAction *dropFrames = new QAction(QIcon(), i18n("Real Time (drop frames)"), this); dropFrames->setCheckable(true); dropFrames->setChecked(KdenliveSettings::monitor_dropframes()); addAction(QStringLiteral("mlt_realtime"), dropFrames); connect(dropFrames, &QAction::toggled, this, &MainWindow::slotSwitchDropFrames); KSelectAction *monitorGamma = new KSelectAction(i18n("Monitor Gamma"), this); monitorGamma->addAction(i18n("sRGB (computer)")); monitorGamma->addAction(i18n("Rec. 709 (TV)")); addAction(QStringLiteral("mlt_gamma"), monitorGamma); monitorGamma->setCurrentItem(KdenliveSettings::monitor_gamma()); connect(monitorGamma, static_cast(&KSelectAction::triggered), this, &MainWindow::slotSetMonitorGamma); addAction(QStringLiteral("switch_trim"), i18n("Trim Mode"), this, SLOT(slotSwitchTrimMode()), QIcon::fromTheme(QStringLiteral("cursor-arrow"))); // disable shortcut until fully working, Qt::CTRL + Qt::Key_T); addAction(QStringLiteral("insert_project_tree"), i18n("Insert Zone in Project Bin"), this, SLOT(slotInsertZoneToTree()), QIcon::fromTheme(QStringLiteral("kdenlive-add-clip")), Qt::CTRL + Qt::Key_I); addAction(QStringLiteral("monitor_seek_snap_backward"), i18n("Go to Previous Snap Point"), this, SLOT(slotSnapRewind()), QIcon::fromTheme(QStringLiteral("media-seek-backward")), Qt::ALT + Qt::Key_Left); addAction(QStringLiteral("seek_clip_start"), i18n("Go to Clip Start"), this, SLOT(slotClipStart()), QIcon::fromTheme(QStringLiteral("media-seek-backward")), Qt::Key_Home); addAction(QStringLiteral("seek_clip_end"), i18n("Go to Clip End"), this, SLOT(slotClipEnd()), QIcon::fromTheme(QStringLiteral("media-seek-forward")), Qt::Key_End); addAction(QStringLiteral("monitor_seek_snap_forward"), i18n("Go to Next Snap Point"), this, SLOT(slotSnapForward()), QIcon::fromTheme(QStringLiteral("media-seek-forward")), Qt::ALT + Qt::Key_Right); addAction(QStringLiteral("align_playhead"), i18n("Align Playhead to Mouse Position"), this, SLOT(slotAlignPlayheadToMousePos()), QIcon(), Qt::Key_P); addAction(QStringLiteral("grab_item"), i18n("Grab Current Item"), this, SLOT(slotGrabItem()), QIcon::fromTheme(QStringLiteral("transform-move")), Qt::SHIFT + Qt::Key_G); QAction *stickTransition = new QAction(i18n("Automatic Transition"), this); stickTransition->setData(QStringLiteral("auto")); stickTransition->setCheckable(true); stickTransition->setEnabled(false); addAction(QStringLiteral("auto_transition"), stickTransition); connect(stickTransition, &QAction::triggered, this, &MainWindow::slotAutoTransition); addAction(QStringLiteral("overwrite_to_in_point"), i18n("Overwrite Clip Zone in Timeline"), this, SLOT(slotInsertClipOverwrite()), QIcon::fromTheme(QStringLiteral("timeline-overwrite")), Qt::Key_B); addAction(QStringLiteral("insert_to_in_point"), i18n("Insert Clip Zone in Timeline"), this, SLOT(slotInsertClipInsert()), QIcon::fromTheme(QStringLiteral("timeline-insert")), Qt::Key_V); addAction(QStringLiteral("remove_extract"), i18n("Extract Timeline Zone"), this, SLOT(slotExtractZone()), QIcon::fromTheme(QStringLiteral("timeline-extract")), Qt::SHIFT + Qt::Key_X); addAction(QStringLiteral("remove_lift"), i18n("Lift Timeline Zone"), this, SLOT(slotLiftZone()), QIcon::fromTheme(QStringLiteral("timeline-lift")), Qt::Key_Z); addAction(QStringLiteral("set_render_timeline_zone"), i18n("Add Preview Zone"), this, SLOT(slotDefinePreviewRender()), QIcon::fromTheme(QStringLiteral("preview-add-zone"))); addAction(QStringLiteral("unset_render_timeline_zone"), i18n("Remove Preview Zone"), this, SLOT(slotRemovePreviewRender()), QIcon::fromTheme(QStringLiteral("preview-remove-zone"))); addAction(QStringLiteral("clear_render_timeline_zone"), i18n("Remove All Preview Zones"), this, SLOT(slotClearPreviewRender()), QIcon::fromTheme(QStringLiteral("preview-remove-all"))); addAction(QStringLiteral("prerender_timeline_zone"), i18n("Start Preview Render"), this, SLOT(slotPreviewRender()), QIcon::fromTheme(QStringLiteral("preview-render-on")), QKeySequence(Qt::SHIFT + Qt::Key_Return)); addAction(QStringLiteral("stop_prerender_timeline"), i18n("Stop Preview Render"), this, SLOT(slotStopPreviewRender()), QIcon::fromTheme(QStringLiteral("preview-render-off"))); addAction(QStringLiteral("select_timeline_clip"), i18n("Select Clip"), this, SLOT(slotSelectTimelineClip()), QIcon::fromTheme(QStringLiteral("edit-select")), Qt::Key_Plus); addAction(QStringLiteral("deselect_timeline_clip"), i18n("Deselect Clip"), this, SLOT(slotDeselectTimelineClip()), QIcon::fromTheme(QStringLiteral("edit-select")), Qt::Key_Minus); addAction(QStringLiteral("select_add_timeline_clip"), i18n("Add Clip To Selection"), this, SLOT(slotSelectAddTimelineClip()), QIcon::fromTheme(QStringLiteral("edit-select")), Qt::ALT + Qt::Key_Plus); addAction(QStringLiteral("select_timeline_transition"), i18n("Select Transition"), this, SLOT(slotSelectTimelineTransition()), QIcon::fromTheme(QStringLiteral("edit-select")), Qt::SHIFT + Qt::Key_Plus); addAction(QStringLiteral("deselect_timeline_transition"), i18n("Deselect Transition"), this, SLOT(slotDeselectTimelineTransition()), QIcon::fromTheme(QStringLiteral("edit-select")), Qt::SHIFT + Qt::Key_Minus); addAction(QStringLiteral("select_add_timeline_transition"), i18n("Add Transition To Selection"), this, SLOT(slotSelectAddTimelineTransition()), QIcon::fromTheme(QStringLiteral("edit-select")), Qt::ALT + Qt::SHIFT + Qt::Key_Plus); addAction(QStringLiteral("add_clip_marker"), i18n("Add Marker"), this, SLOT(slotAddClipMarker()), QIcon::fromTheme(QStringLiteral("bookmark-new"))); addAction(QStringLiteral("delete_clip_marker"), i18n("Delete Marker"), this, SLOT(slotDeleteClipMarker()), QIcon::fromTheme(QStringLiteral("edit-delete"))); addAction(QStringLiteral("delete_all_clip_markers"), i18n("Delete All Markers"), this, SLOT(slotDeleteAllClipMarkers()), QIcon::fromTheme(QStringLiteral("edit-delete"))); QAction *editClipMarker = addAction(QStringLiteral("edit_clip_marker"), i18n("Edit Marker"), this, SLOT(slotEditClipMarker()), QIcon::fromTheme(QStringLiteral("document-properties"))); editClipMarker->setData(QStringLiteral("edit_marker")); addAction(QStringLiteral("add_marker_guide_quickly"), i18n("Add Marker/Guide quickly"), this, SLOT(slotAddMarkerGuideQuickly()), QIcon::fromTheme(QStringLiteral("bookmark-new")), Qt::Key_Asterisk); // Clip actions. We set some category info on the action data to enable/disable it contextually in timelinecontroller KActionCategory *clipActionCategory = new KActionCategory(i18n("Current Selection"), actionCollection()); QAction *splitAudio = addAction(QStringLiteral("clip_split"), i18n("Split Audio"), this, SLOT(slotSplitAV()), QIcon::fromTheme(QStringLiteral("document-new")), QKeySequence(), clipActionCategory); // "S" will be handled specifically to change the action name depending on current selection splitAudio->setData('S'); splitAudio->setEnabled(false); QAction *setAudioAlignReference = addAction(QStringLiteral("set_audio_align_ref"), i18n("Set Audio Reference"), this, SLOT(slotSetAudioAlignReference()), QIcon(), QKeySequence(), clipActionCategory); // "A" as data means this action should only be available for clips with audio setAudioAlignReference->setData('A'); setAudioAlignReference->setEnabled(false); QAction *alignAudio = addAction(QStringLiteral("align_audio"), i18n("Align Audio to Reference"), this, SLOT(slotAlignAudio()), QIcon(), QKeySequence(), clipActionCategory); // "A" as data means this action should only be available for clips with audio alignAudio->setData('A'); alignAudio->setEnabled(false); QAction *act = addAction(QStringLiteral("edit_item_duration"), i18n("Edit Duration"), this, SLOT(slotEditItemDuration()), QIcon::fromTheme(QStringLiteral("measure")), QKeySequence(), clipActionCategory); act->setEnabled(false); act = addAction(QStringLiteral("clip_in_project_tree"), i18n("Clip in Project Bin"), this, SLOT(slotClipInProjectTree()), QIcon::fromTheme(QStringLiteral("go-jump-definition")), QKeySequence(), clipActionCategory); act->setEnabled(false); // "C" as data means this action should only be available for clips - not for compositions act->setData('C'); act = addAction(QStringLiteral("cut_timeline_clip"), i18n("Cut Clip"), this, SLOT(slotCutTimelineClip()), QIcon::fromTheme(QStringLiteral("edit-cut")), Qt::SHIFT + Qt::Key_R); act = addAction(QStringLiteral("delete_timeline_clip"), i18n("Delete Selected Item"), this, SLOT(slotDeleteItem()), QIcon::fromTheme(QStringLiteral("edit-delete")), Qt::Key_Delete); QAction *resizeStart = new QAction(QIcon(), i18n("Resize Item Start"), this); addAction(QStringLiteral("resize_timeline_clip_start"), resizeStart, Qt::Key_1, clipActionCategory); resizeStart->setEnabled(false); connect(resizeStart, &QAction::triggered, this, &MainWindow::slotResizeItemStart); QAction *resizeEnd = new QAction(QIcon(), i18n("Resize Item End"), this); addAction(QStringLiteral("resize_timeline_clip_end"), resizeEnd, Qt::Key_2, clipActionCategory); resizeEnd->setEnabled(false); connect(resizeEnd, &QAction::triggered, this, &MainWindow::slotResizeItemEnd); QAction *pasteEffects = addAction(QStringLiteral("paste_effects"), i18n("Paste Effects"), this, SLOT(slotPasteEffects()), QIcon::fromTheme(QStringLiteral("edit-paste")), QKeySequence(), clipActionCategory); pasteEffects->setEnabled(false); // "C" as data means this action should only be available for clips - not for compositions pasteEffects->setData('C'); QAction *groupClip = addAction(QStringLiteral("group_clip"), i18n("Group Clips"), this, SLOT(slotGroupClips()), QIcon::fromTheme(QStringLiteral("object-group")), Qt::CTRL + Qt::Key_G, clipActionCategory); // "G" as data means this action should only be available for multiple items selection groupClip->setData('G'); groupClip->setEnabled(false); QAction *ungroupClip = addAction(QStringLiteral("ungroup_clip"), i18n("Ungroup Clips"), this, SLOT(slotUnGroupClips()), QIcon::fromTheme(QStringLiteral("object-ungroup")), Qt::CTRL + Qt::SHIFT + Qt::Key_G, clipActionCategory); // "U" as data means this action should only be available if selection is a group ungroupClip->setData('U'); ungroupClip->setEnabled(false); act = clipActionCategory->addAction(KStandardAction::Copy, this, SLOT(slotCopy())); act->setEnabled(false); KStandardAction::paste(this, SLOT(slotPaste()), actionCollection()); /*act = KStandardAction::copy(this, SLOT(slotCopy()), actionCollection()); clipActionCategory->addAction(KStandardAction::name(KStandardAction::Copy), act); act->setEnabled(false); act = KStandardAction::paste(this, SLOT(slotPaste()), actionCollection()); clipActionCategory->addAction(KStandardAction::name(KStandardAction::Paste), act); act->setEnabled(false);*/ kdenliveCategoryMap.insert(QStringLiteral("timelineselection"), clipActionCategory); addAction(QStringLiteral("insert_space"), i18n("Insert Space"), this, SLOT(slotInsertSpace())); addAction(QStringLiteral("delete_space"), i18n("Remove Space"), this, SLOT(slotRemoveSpace())); addAction(QStringLiteral("delete_space_all_tracks"), i18n("Remove Space In All Tracks"), this, SLOT(slotRemoveAllSpace())); KActionCategory *timelineActions = new KActionCategory(i18n("Tracks"), actionCollection()); QAction *insertTrack = new QAction(QIcon(), i18n("Insert Track"), this); connect(insertTrack, &QAction::triggered, this, &MainWindow::slotInsertTrack); timelineActions->addAction(QStringLiteral("insert_track"), insertTrack); QAction *deleteTrack = new QAction(QIcon(), i18n("Delete Track"), this); connect(deleteTrack, &QAction::triggered, this, &MainWindow::slotDeleteTrack); timelineActions->addAction(QStringLiteral("delete_track"), deleteTrack); deleteTrack->setData("delete_track"); QAction *selectTrack = new QAction(QIcon(), i18n("Select All in Current Track"), this); connect(selectTrack, &QAction::triggered, this, &MainWindow::slotSelectTrack); timelineActions->addAction(QStringLiteral("select_track"), selectTrack); QAction *selectAll = KStandardAction::selectAll(this, SLOT(slotSelectAllTracks()), this); selectAll->setIcon(QIcon::fromTheme(QStringLiteral("kdenlive-select-all"))); selectAll->setShortcutContext(Qt::WidgetWithChildrenShortcut); timelineActions->addAction(QStringLiteral("select_all_tracks"), selectAll); QAction *unselectAll = KStandardAction::deselect(this, SLOT(slotUnselectAllTracks()), this); unselectAll->setIcon(QIcon::fromTheme(QStringLiteral("kdenlive-unselect-all"))); unselectAll->setShortcutContext(Qt::WidgetWithChildrenShortcut); timelineActions->addAction(QStringLiteral("unselect_all_tracks"), unselectAll); kdenliveCategoryMap.insert(QStringLiteral("timeline"), timelineActions); // Cached data management addAction(QStringLiteral("manage_cache"), i18n("Manage Cached Data"), this, SLOT(slotManageCache()), QIcon::fromTheme(QStringLiteral("network-server-database"))); QAction *disablePreview = new QAction(i18n("Disable Timeline Preview"), this); disablePreview->setCheckable(true); addAction(QStringLiteral("disable_preview"), disablePreview); addAction(QStringLiteral("add_guide"), i18n("Add Guide"), this, SLOT(slotAddGuide()), QIcon::fromTheme(QStringLiteral("list-add"))); addAction(QStringLiteral("delete_guide"), i18n("Delete Guide"), this, SLOT(slotDeleteGuide()), QIcon::fromTheme(QStringLiteral("edit-delete"))); addAction(QStringLiteral("edit_guide"), i18n("Edit Guide"), this, SLOT(slotEditGuide()), QIcon::fromTheme(QStringLiteral("document-properties"))); addAction(QStringLiteral("delete_all_guides"), i18n("Delete All Guides"), this, SLOT(slotDeleteAllGuides()), QIcon::fromTheme(QStringLiteral("edit-delete"))); m_saveAction = KStandardAction::save(pCore->projectManager(), SLOT(saveFile()), actionCollection()); m_saveAction->setIcon(QIcon::fromTheme(QStringLiteral("document-save"))); addAction(QStringLiteral("save_selection"), i18n("Save Selection"), pCore->projectManager(), SLOT(slotSaveSelection()), QIcon::fromTheme(QStringLiteral("document-save"))); QAction *sentToLibrary = addAction(QStringLiteral("send_library"), i18n("Add Timeline Selection to Library"), pCore->library(), SLOT(slotAddToLibrary()), QIcon::fromTheme(QStringLiteral("bookmark-new"))); sentToLibrary->setEnabled(false); pCore->library()->setupActions(QList() << sentToLibrary); KStandardAction::showMenubar(this, SLOT(showMenuBar(bool)), actionCollection()); act = KStandardAction::quit(this, SLOT(close()), actionCollection()); // act->setIcon(QIcon::fromTheme(QStringLiteral("application-exit"))); KStandardAction::keyBindings(this, SLOT(slotEditKeys()), actionCollection()); KStandardAction::preferences(this, SLOT(slotPreferences()), actionCollection()); KStandardAction::configureNotifications(this, SLOT(configureNotifications()), actionCollection()); KStandardAction::fullScreen(this, SLOT(slotFullScreen()), this, actionCollection()); QAction *undo = KStandardAction::undo(m_commandStack, SLOT(undo()), actionCollection()); undo->setEnabled(false); connect(m_commandStack, &QUndoGroup::canUndoChanged, undo, &QAction::setEnabled); QAction *redo = KStandardAction::redo(m_commandStack, SLOT(redo()), actionCollection()); redo->setEnabled(false); connect(m_commandStack, &QUndoGroup::canRedoChanged, redo, &QAction::setEnabled); auto *addClips = new QMenu(this); QAction *addClip = addAction(QStringLiteral("add_clip"), i18n("Add Clip"), pCore->bin(), SLOT(slotAddClip()), QIcon::fromTheme(QStringLiteral("kdenlive-add-clip"))); addClips->addAction(addClip); QAction *action = addAction(QStringLiteral("add_color_clip"), i18n("Add Color Clip"), pCore->bin(), SLOT(slotCreateProjectClip()), QIcon::fromTheme(QStringLiteral("kdenlive-add-color-clip"))); action->setData((int)ClipType::Color); addClips->addAction(action); action = addAction(QStringLiteral("add_slide_clip"), i18n("Add Slideshow Clip"), pCore->bin(), SLOT(slotCreateProjectClip()), QIcon::fromTheme(QStringLiteral("kdenlive-add-slide-clip"))); action->setData((int)ClipType::SlideShow); addClips->addAction(action); action = addAction(QStringLiteral("add_text_clip"), i18n("Add Title Clip"), pCore->bin(), SLOT(slotCreateProjectClip()), QIcon::fromTheme(QStringLiteral("kdenlive-add-text-clip"))); action->setData((int)ClipType::Text); addClips->addAction(action); action = addAction(QStringLiteral("add_text_template_clip"), i18n("Add Template Title"), pCore->bin(), SLOT(slotCreateProjectClip()), QIcon::fromTheme(QStringLiteral("kdenlive-add-text-clip"))); action->setData((int)ClipType::TextTemplate); addClips->addAction(action); /*action = addAction(QStringLiteral("add_qtext_clip"), i18n("Add Simple Text Clip"), pCore->bin(), SLOT(slotCreateProjectClip()), QIcon::fromTheme(QStringLiteral("kdenlive-add-text-clip"))); action->setData((int) QText); addClips->addAction(action);*/ QAction *addFolder = addAction(QStringLiteral("add_folder"), i18n("Create Folder"), pCore->bin(), SLOT(slotAddFolder()), QIcon::fromTheme(QStringLiteral("folder-new"))); addClips->addAction(addAction(QStringLiteral("download_resource"), i18n("Online Resources"), this, SLOT(slotDownloadResources()), QIcon::fromTheme(QStringLiteral("edit-download")))); QAction *clipProperties = addAction(QStringLiteral("clip_properties"), i18n("Clip Properties"), pCore->bin(), SLOT(slotSwitchClipProperties()), QIcon::fromTheme(QStringLiteral("document-edit"))); clipProperties->setData("clip_properties"); QAction *openClip = addAction(QStringLiteral("edit_clip"), i18n("Edit Clip"), pCore->bin(), SLOT(slotOpenClip()), QIcon::fromTheme(QStringLiteral("document-open"))); openClip->setData("edit_clip"); openClip->setEnabled(false); QAction *deleteClip = addAction(QStringLiteral("delete_clip"), i18n("Delete Clip"), pCore->bin(), SLOT(slotDeleteClip()), QIcon::fromTheme(QStringLiteral("edit-delete"))); deleteClip->setData("delete_clip"); deleteClip->setEnabled(false); QAction *reloadClip = addAction(QStringLiteral("reload_clip"), i18n("Reload Clip"), pCore->bin(), SLOT(slotReloadClip()), QIcon::fromTheme(QStringLiteral("view-refresh"))); reloadClip->setData("reload_clip"); reloadClip->setEnabled(false); QAction *disableEffects = addAction(QStringLiteral("disable_timeline_effects"), i18n("Disable Timeline Effects"), pCore->projectManager(), SLOT(slotDisableTimelineEffects(bool)), QIcon::fromTheme(QStringLiteral("favorite"))); disableEffects->setData("disable_timeline_effects"); disableEffects->setCheckable(true); disableEffects->setChecked(false); QAction *locateClip = addAction(QStringLiteral("locate_clip"), i18n("Locate Clip..."), pCore->bin(), SLOT(slotLocateClip()), QIcon::fromTheme(QStringLiteral("edit-file"))); locateClip->setData("locate_clip"); locateClip->setEnabled(false); QAction *duplicateClip = addAction(QStringLiteral("duplicate_clip"), i18n("Duplicate Clip"), pCore->bin(), SLOT(slotDuplicateClip()), QIcon::fromTheme(QStringLiteral("edit-copy"))); duplicateClip->setData("duplicate_clip"); duplicateClip->setEnabled(false); QAction *proxyClip = new QAction(i18n("Proxy Clip"), this); addAction(QStringLiteral("proxy_clip"), proxyClip); proxyClip->setData(QStringList() << QString::number((int)AbstractClipJob::PROXYJOB)); proxyClip->setCheckable(true); proxyClip->setChecked(false); addAction(QStringLiteral("switch_track_lock"), i18n("Toggle Track Lock"), pCore->projectManager(), SLOT(slotSwitchTrackLock()), QIcon(), Qt::SHIFT + Qt::Key_L); addAction(QStringLiteral("switch_all_track_lock"), i18n("Toggle All Track Lock"), pCore->projectManager(), SLOT(slotSwitchAllTrackLock()), QIcon(), Qt::CTRL + Qt::SHIFT + Qt::Key_L); addAction(QStringLiteral("switch_track_target"), i18n("Toggle Track Target"), pCore->projectManager(), SLOT(slotSwitchTrackTarget()), QIcon(), Qt::SHIFT + Qt::Key_T); addAction(QStringLiteral("add_project_note"), i18n("Add Project Note"), pCore->projectManager(), SLOT(slotAddProjectNote()), QIcon::fromTheme(QStringLiteral("bookmark"))); QHash actions({{QStringLiteral("locate"), locateClip}, {QStringLiteral("reload"), reloadClip}, {QStringLiteral("duplicate"), duplicateClip}, {QStringLiteral("proxy"), proxyClip}, {QStringLiteral("properties"), clipProperties}, {QStringLiteral("open"), openClip}, {QStringLiteral("delete"), deleteClip}, {QStringLiteral("folder"), addFolder}}); pCore->bin()->setupMenu(addClips, addClip, actions); // Setup effects and transitions actions. KActionCategory *transitionActions = new KActionCategory(i18n("Transitions"), actionCollection()); // m_transitions = new QAction*[transitions.count()]; auto allTransitions = TransitionsRepository::get()->getNames(); for (const auto &transition : allTransitions) { auto *transAction = new QAction(transition.first, this); transAction->setData(transition.second); transAction->setIconVisibleInMenu(false); transitionActions->addAction("transition_" + transition.second, transAction); } // monitor actions addAction(QStringLiteral("extract_frame"), i18n("Extract frame..."), pCore->monitorManager(), SLOT(slotExtractCurrentFrame()), QIcon::fromTheme(QStringLiteral("insert-image"))); addAction(QStringLiteral("extract_frame_to_project"), i18n("Extract frame to project..."), pCore->monitorManager(), SLOT(slotExtractCurrentFrameToProject()), QIcon::fromTheme(QStringLiteral("insert-image"))); } void MainWindow::saveOptions() { KdenliveSettings::self()->save(); } bool MainWindow::readOptions() { KSharedConfigPtr config = KSharedConfig::openConfig(); pCore->projectManager()->recentFilesAction()->loadEntries(KConfigGroup(config, "Recent Files")); if (KdenliveSettings::defaultprojectfolder().isEmpty()) { QDir dir(QStandardPaths::writableLocation(QStandardPaths::MoviesLocation)); dir.mkpath(QStringLiteral(".")); KdenliveSettings::setDefaultprojectfolder(dir.absolutePath()); } if (KdenliveSettings::trackheight() == 0) { QFontMetrics metrics(font()); int trackHeight = 2 * metrics.height(); QStyle *style = qApp->style(); trackHeight += style->pixelMetric(QStyle::PM_ToolBarIconSize) + 2 * style->pixelMetric(QStyle::PM_ToolBarItemMargin) + style->pixelMetric(QStyle::PM_ToolBarItemSpacing) + 2; KdenliveSettings::setTrackheight(trackHeight); } if (KdenliveSettings::trackheight() == 0) { KdenliveSettings::setTrackheight(50); } bool firstRun = false; KConfigGroup initialGroup(config, "version"); if (!initialGroup.exists() || KdenliveSettings::sdlAudioBackend().isEmpty()) { // First run, check if user is on a KDE Desktop firstRun = true; // this is our first run, show Wizard QPointer w = new Wizard(true, false); if (w->exec() == QDialog::Accepted && w->isOk()) { w->adjustSettings(); delete w; } else { delete w; ::exit(1); } } else if (!KdenliveSettings::ffmpegpath().isEmpty() && !QFile::exists(KdenliveSettings::ffmpegpath())) { // Invalid entry for FFmpeg, check system QPointer w = new Wizard(true, config->name().contains(QLatin1String("appimage"))); if (w->exec() == QDialog::Accepted && w->isOk()) { w->adjustSettings(); } delete w; } initialGroup.writeEntry("version", version); return firstRun; } void MainWindow::slotRunWizard() { QPointer w = new Wizard(false, false, this); if (w->exec() == QDialog::Accepted && w->isOk()) { w->adjustSettings(); } delete w; } void MainWindow::slotRefreshProfiles() { KdenliveSettingsDialog *d = static_cast(KConfigDialog::exists(QStringLiteral("settings"))); if (d) { d->checkProfile(); } } void MainWindow::slotEditProjectSettings() { KdenliveDoc *project = pCore->currentDoc(); QPoint p = getMainTimeline()->getTracksCount(); ProjectSettings *w = new ProjectSettings(project, project->metadata(), getMainTimeline()->controller()->extractCompositionLumas(), p.x(), p.y(), project->projectTempFolder(), true, !project->isModified(), this); connect(w, &ProjectSettings::disableProxies, this, &MainWindow::slotDisableProxies); // connect(w, SIGNAL(disablePreview()), pCore->projectManager()->currentTimeline(), SLOT(invalidateRange())); connect(w, &ProjectSettings::refreshProfiles, this, &MainWindow::slotRefreshProfiles); if (w->exec() == QDialog::Accepted) { QString profile = w->selectedProfile(); // project->setProjectFolder(w->selectedFolder()); bool modified = false; if (m_renderWidget) { m_renderWidget->updateDocumentPath(); } if (KdenliveSettings::videothumbnails() != w->enableVideoThumbs()) { slotSwitchVideoThumbs(); } if (KdenliveSettings::audiothumbnails() != w->enableAudioThumbs()) { slotSwitchAudioThumbs(); } if (project->getDocumentProperty(QStringLiteral("proxyparams")) != w->proxyParams() || project->getDocumentProperty(QStringLiteral("proxyextension")) != w->proxyExtension()) { modified = true; project->setDocumentProperty(QStringLiteral("proxyparams"), w->proxyParams()); project->setDocumentProperty(QStringLiteral("proxyextension"), w->proxyExtension()); if (pCore->projectItemModel()->clipsCount() > 0 && KMessageBox::questionYesNo(this, i18n("You have changed the proxy parameters. Do you want to recreate all proxy clips for this project?")) == KMessageBox::Yes) { pCore->bin()->rebuildProxies(); } } if (project->getDocumentProperty(QStringLiteral("externalproxyparams")) != w->externalProxyParams()) { modified = true; project->setDocumentProperty(QStringLiteral("externalproxyparams"), w->externalProxyParams()); if (pCore->projectItemModel()->clipsCount() > 0 && KMessageBox::questionYesNo(this, i18n("You have changed the proxy parameters. Do you want to recreate all proxy clips for this project?")) == KMessageBox::Yes) { pCore->bin()->rebuildProxies(); } } if (project->getDocumentProperty(QStringLiteral("generateproxy")) != QString::number((int)w->generateProxy())) { modified = true; project->setDocumentProperty(QStringLiteral("generateproxy"), QString::number((int)w->generateProxy())); } if (project->getDocumentProperty(QStringLiteral("proxyminsize")) != QString::number(w->proxyMinSize())) { modified = true; project->setDocumentProperty(QStringLiteral("proxyminsize"), QString::number(w->proxyMinSize())); } if (project->getDocumentProperty(QStringLiteral("generateimageproxy")) != QString::number((int)w->generateImageProxy())) { modified = true; project->setDocumentProperty(QStringLiteral("generateimageproxy"), QString::number((int)w->generateImageProxy())); } if (project->getDocumentProperty(QStringLiteral("proxyimageminsize")) != QString::number(w->proxyImageMinSize())) { modified = true; project->setDocumentProperty(QStringLiteral("proxyimageminsize"), QString::number(w->proxyImageMinSize())); } if (project->getDocumentProperty(QStringLiteral("proxyimagesize")) != QString::number(w->proxyImageSize())) { modified = true; project->setDocumentProperty(QStringLiteral("proxyimagesize"), QString::number(w->proxyImageSize())); } if (QString::number((int)w->useProxy()) != project->getDocumentProperty(QStringLiteral("enableproxy"))) { project->setDocumentProperty(QStringLiteral("enableproxy"), QString::number((int)w->useProxy())); modified = true; slotUpdateProxySettings(); } if (QString::number((int)w->useExternalProxy()) != project->getDocumentProperty(QStringLiteral("enableexternalproxy"))) { project->setDocumentProperty(QStringLiteral("enableexternalproxy"), QString::number((int)w->useExternalProxy())); modified = true; } if (w->metadata() != project->metadata()) { project->setMetadata(w->metadata()); } QString newProjectFolder = w->storageFolder(); if (newProjectFolder.isEmpty()) { newProjectFolder = QStandardPaths::writableLocation(QStandardPaths::CacheLocation); } if (newProjectFolder != project->projectTempFolder()) { KMessageBox::ButtonCode answer; // Project folder changed: if (project->isModified()) { answer = KMessageBox::warningContinueCancel(this, i18n("The current project has not been saved. This will first save the project, then move " "all temporary files from %1 to %2, and the project file will be reloaded", project->projectTempFolder(), newProjectFolder)); if (answer == KMessageBox::Continue) { pCore->projectManager()->saveFile(); } } else { answer = KMessageBox::warningContinueCancel( this, i18n("This will move all temporary files from %1 to %2, the project file will then be reloaded", project->projectTempFolder(), newProjectFolder)); } if (answer == KMessageBox::Continue) { // Proceed with move QString documentId = QDir::cleanPath(project->getDocumentProperty(QStringLiteral("documentid"))); bool ok; documentId.toLongLong(&ok, 10); if (!ok || documentId.isEmpty()) { KMessageBox::sorry(this, i18n("Cannot perform operation, invalid document id: %1", documentId)); } else { QDir newDir(newProjectFolder); QDir oldDir(project->projectTempFolder()); if (newDir.exists(documentId)) { KMessageBox::sorry(this, i18n("Cannot perform operation, target directory already exists: %1", newDir.absoluteFilePath(documentId))); } else { // Proceed with the move pCore->projectManager()->moveProjectData(oldDir.absoluteFilePath(documentId), newDir.absolutePath()); } } } } if (pCore->getCurrentProfile()->path() != profile || project->profileChanged(profile)) { if (!qFuzzyCompare(pCore->getCurrentProfile()->fps() - ProfileRepository::get()->getProfile(profile)->fps(), 0.)) { // Fps was changed, we save the project to an xml file with updated profile and reload project // Check if blank project if (project->url().fileName().isEmpty() && !project->isModified()) { // Trying to switch project profile from an empty project pCore->setCurrentProfile(profile); pCore->projectManager()->newFile(profile, false); return; } pCore->projectManager()->saveWithUpdatedProfile(profile); } else { pCore->setCurrentProfile(profile); pCore->projectManager()->slotResetProfiles(); slotUpdateDocumentState(true); } } if (modified) { project->setModified(); } } delete w; } void MainWindow::slotDisableProxies() { pCore->currentDoc()->setDocumentProperty(QStringLiteral("enableproxy"), QString::number((int)false)); pCore->currentDoc()->setModified(); slotUpdateProxySettings(); } void MainWindow::slotStopRenderProject() { if (m_renderWidget) { m_renderWidget->slotAbortCurrentJob(); } } void MainWindow::slotRenderProject() { KdenliveDoc *project = pCore->currentDoc(); if (!m_renderWidget) { QString projectfolder = project ? project->projectDataFolder() + QDir::separator() : KdenliveSettings::defaultprojectfolder(); if (project) { m_renderWidget = new RenderWidget(project->useProxy(), this); connect(m_renderWidget, &RenderWidget::shutdown, this, &MainWindow::slotShutdown); connect(m_renderWidget, &RenderWidget::selectedRenderProfile, this, &MainWindow::slotSetDocumentRenderProfile); connect(m_renderWidget, &RenderWidget::abortProcess, this, &MainWindow::abortRenderJob); connect(m_renderWidget, &RenderWidget::openDvdWizard, this, &MainWindow::slotDvdWizard); connect(this, &MainWindow::updateRenderWidgetProfile, m_renderWidget, &RenderWidget::adjustViewToProfile); double projectDuration = GenTime(getMainTimeline()->controller()->duration(), pCore->getCurrentFps()).ms() / 1000; m_renderWidget->setGuides(project->getGuideModel()->getAllMarkers(), projectDuration); m_renderWidget->updateDocumentPath(); m_renderWidget->setRenderProfile(project->getRenderProperties()); } if (m_compositeAction->currentAction()) { m_renderWidget->errorMessage(RenderWidget::CompositeError, m_compositeAction->currentAction()->data().toInt() == 1 ? i18n("Rendering using low quality track compositing") : QString()); } } slotCheckRenderStatus(); m_renderWidget->show(); // m_renderWidget->showNormal(); // What are the following lines supposed to do? // m_renderWidget->enableAudio(false); // m_renderWidget->export_audio; } void MainWindow::slotCheckRenderStatus() { // Make sure there are no missing clips // TODO /*if (m_renderWidget) m_renderWidget->missingClips(pCore->bin()->hasMissingClips());*/ } void MainWindow::setRenderingProgress(const QString &url, int progress) { emit setRenderProgress(progress); if (m_renderWidget) { m_renderWidget->setRenderJob(url, progress); } } void MainWindow::setRenderingFinished(const QString &url, int status, const QString &error) { emit setRenderProgress(100); if (m_renderWidget) { m_renderWidget->setRenderStatus(url, status, error); } } void MainWindow::addProjectClip(const QString &url) { if (pCore->currentDoc()) { QStringList ids = pCore->projectItemModel()->getClipByUrl(QFileInfo(url)); if (!ids.isEmpty()) { // Clip is already in project bin, abort return; } ClipCreator::createClipFromFile(url, pCore->projectItemModel()->getRootFolder()->clipId(), pCore->projectItemModel()); } } void MainWindow::addTimelineClip(const QString &url) { if (pCore->currentDoc()) { QStringList ids = pCore->projectItemModel()->getClipByUrl(QFileInfo(url)); if (!ids.isEmpty()) { pCore->selectBinClip(ids.constFirst()); slotInsertClipInsert(); } } } void MainWindow::scriptRender(const QString &url) { slotRenderProject(); m_renderWidget->slotPrepareExport(true, url); } void MainWindow::exitApp() { QApplication::exit(0); } void MainWindow::slotCleanProject() { if (KMessageBox::warningContinueCancel(this, i18n("This will remove all unused clips from your project."), i18n("Clean up project")) == KMessageBox::Cancel) { return; } pCore->bin()->cleanup(); } void MainWindow::slotUpdateMousePosition(int pos) { if (pCore->currentDoc()) { switch (m_timeFormatButton->currentItem()) { case 0: m_timeFormatButton->setText(pCore->currentDoc()->timecode().getTimecodeFromFrames(pos) + QStringLiteral(" / ") + pCore->currentDoc()->timecode().getTimecodeFromFrames(getMainTimeline()->controller()->duration())); break; default: m_timeFormatButton->setText( QStringLiteral("%1 / %2").arg(pos, 6, 10, QLatin1Char('0')).arg(getMainTimeline()->controller()->duration(), 6, 10, QLatin1Char('0'))); } } } void MainWindow::slotUpdateProjectDuration(int pos) { Q_UNUSED(pos) if (pCore->currentDoc()) { slotUpdateMousePosition(getMainTimeline()->controller()->getMousePos()); } } void MainWindow::slotUpdateDocumentState(bool modified) { setWindowTitle(pCore->currentDoc()->description()); setWindowModified(modified); m_saveAction->setEnabled(modified); } void MainWindow::connectDocument() { KdenliveDoc *project = pCore->currentDoc(); connect(project, &KdenliveDoc::startAutoSave, pCore->projectManager(), &ProjectManager::slotStartAutoSave); connect(project, &KdenliveDoc::reloadEffects, this, &MainWindow::slotReloadEffects); KdenliveSettings::setProject_fps(pCore->getCurrentFps()); m_projectMonitor->slotLoadClipZone(project->zone()); connect(m_projectMonitor, &Monitor::multitrackView, getMainTimeline()->controller(), &TimelineController::slotMultitrackView, Qt::UniqueConnection); connect(getMainTimeline()->controller(), &TimelineController::timelineClipSelected, pCore->library(), &LibraryWidget::enableAddSelection, Qt::UniqueConnection); connect(pCore->library(), &LibraryWidget::saveTimelineSelection, getMainTimeline()->controller(), &TimelineController::saveTimelineSelection, Qt::UniqueConnection); // TODO REFAC: reconnect to new timeline /* Timeline *trackView = pCore->projectManager()->currentTimeline(); connect(trackView, &Timeline::configTrack, this, &MainWindow::slotConfigTrack); connect(trackView, &Timeline::updateTracksInfo, this, &MainWindow::slotUpdateTrackInfo); connect(trackView, &Timeline::mousePosition, this, &MainWindow::slotUpdateMousePosition); connect(pCore->producerQueue(), &ProducerQueue::infoProcessingFinished, trackView->projectView(), &CustomTrackView::slotInfoProcessingFinished, Qt::DirectConnection); connect(trackView->projectView(), &CustomTrackView::importKeyframes, this, &MainWindow::slotProcessImportKeyframes); connect(trackView->projectView(), &CustomTrackView::updateTrimMode, this, &MainWindow::setTrimMode); connect(m_projectMonitor, SIGNAL(renderPosition(int)), trackView, SLOT(moveCursorPos(int))); connect(m_projectMonitor, SIGNAL(zoneUpdated(QPoint)), trackView, SLOT(slotSetZone(QPoint))); connect(trackView->projectView(), &CustomTrackView::guidesUpdated, this, &MainWindow::slotGuidesUpdated); connect(trackView->projectView(), &CustomTrackView::loadMonitorScene, m_projectMonitor, &Monitor::slotShowEffectScene); connect(trackView->projectView(), &CustomTrackView::setQmlProperty, m_projectMonitor, &Monitor::setQmlProperty); connect(m_projectMonitor, SIGNAL(acceptRipple(bool)), trackView->projectView(), SLOT(slotAcceptRipple(bool))); connect(m_projectMonitor, SIGNAL(switchTrimMode(int)), trackView->projectView(), SLOT(switchTrimMode(int))); connect(project, &KdenliveDoc::saveTimelinePreview, trackView, &Timeline::slotSaveTimelinePreview); connect(trackView, SIGNAL(showTrackEffects(int, TrackInfo)), this, SLOT(slotTrackSelected(int, TrackInfo))); connect(trackView->projectView(), &CustomTrackView::clipItemSelected, this, &MainWindow::slotTimelineClipSelected, Qt::DirectConnection); connect(trackView->projectView(), &CustomTrackView::setActiveKeyframe, m_effectStack, &EffectStackView2::setActiveKeyframe); connect(trackView->projectView(), SIGNAL(transitionItemSelected(Transition *, int, QPoint, bool)), m_effectStack, SLOT(slotTransitionItemSelected(Transition *, int, QPoint, bool)), Qt::DirectConnection); connect(trackView->projectView(), SIGNAL(transitionItemSelected(Transition *, int, QPoint, bool)), this, SLOT(slotActivateTransitionView(Transition *))); connect(trackView->projectView(), &CustomTrackView::zoomIn, this, &MainWindow::slotZoomIn); connect(trackView->projectView(), &CustomTrackView::zoomOut, this, &MainWindow::slotZoomOut); connect(trackView, SIGNAL(setZoom(int)), this, SLOT(slotSetZoom(int))); connect(trackView, SIGNAL(displayMessage(QString, MessageType)), m_messageLabel, SLOT(setMessage(QString, MessageType))); connect(trackView->projectView(), SIGNAL(displayMessage(QString, MessageType)), m_messageLabel, SLOT(setMessage(QString, MessageType))); connect(pCore->bin(), &Bin::clipNameChanged, trackView->projectView(), &CustomTrackView::clipNameChanged); connect(trackView->projectView(), SIGNAL(showClipFrame(QString, int)), pCore->bin(), SLOT(selectClipById(QString, int))); connect(trackView->projectView(), SIGNAL(playMonitor()), m_projectMonitor, SLOT(slotPlay())); connect(trackView->projectView(), &CustomTrackView::pauseMonitor, m_projectMonitor, &Monitor::pause, Qt::DirectConnection); connect(m_projectMonitor, &Monitor::addEffect, trackView->projectView(), &CustomTrackView::slotAddEffectToCurrentItem); connect(trackView->projectView(), SIGNAL(transitionItemSelected(Transition *, int, QPoint, bool)), m_projectMonitor, SLOT(slotSetSelectedClip(Transition *))); connect(pCore->bin(), SIGNAL(gotFilterJobResults(QString, int, int, stringMap, stringMap)), trackView->projectView(), SLOT(slotGotFilterJobResults(QString, int, int, stringMap, stringMap))); //TODO //connect(m_projectList, SIGNAL(addMarkers(QString,QList)), trackView->projectView(), SLOT(slotAddClipMarker(QString,QList))); // Effect stack signals connect(m_effectStack, &EffectStackView2::updateEffect, trackView->projectView(), &CustomTrackView::slotUpdateClipEffect); connect(m_effectStack, &EffectStackView2::updateClipRegion, trackView->projectView(), &CustomTrackView::slotUpdateClipRegion); connect(m_effectStack, SIGNAL(removeEffect(ClipItem *, int, QDomElement)), trackView->projectView(), SLOT(slotDeleteEffect(ClipItem *, int, QDomElement))); connect(m_effectStack, SIGNAL(removeEffectGroup(ClipItem *, int, QDomDocument)), trackView->projectView(), SLOT(slotDeleteEffectGroup(ClipItem *, int, QDomDocument))); connect(m_effectStack, SIGNAL(addEffect(ClipItem *, QDomElement, int)), trackView->projectView(), SLOT(slotAddEffect(ClipItem *, QDomElement, int))); connect(m_effectStack, SIGNAL(changeEffectState(ClipItem *, int, QList, bool)), trackView->projectView(), SLOT(slotChangeEffectState(ClipItem *, int, QList, bool))); connect(m_effectStack, SIGNAL(changeEffectPosition(ClipItem *, int, QList, int)), trackView->projectView(), SLOT(slotChangeEffectPosition(ClipItem *, int, QList, int))); connect(m_effectStack, &EffectStackView2::refreshEffectStack, trackView->projectView(), &CustomTrackView::slotRefreshEffects); connect(m_effectStack, &EffectStackView2::seekTimeline, trackView->projectView(), &CustomTrackView::seekCursorPos); connect(m_effectStack, SIGNAL(importClipKeyframes(GraphicsRectItem, ItemInfo, QDomElement, QMap)), trackView->projectView(), SLOT(slotImportClipKeyframes(GraphicsRectItem, ItemInfo, QDomElement, QMap))); // Transition config signals connect(m_effectStack->transitionConfig(), SIGNAL(transitionUpdated(Transition *, QDomElement)), trackView->projectView(), SLOT(slotTransitionUpdated(Transition *, QDomElement))); connect(m_effectStack->transitionConfig(), &TransitionSettings::seekTimeline, trackView->projectView(), &CustomTrackView::seekCursorPos); connect(trackView->projectView(), SIGNAL(activateDocumentMonitor()), m_projectMonitor, SLOT(slotActivateMonitor()), Qt::DirectConnection); connect(project, &KdenliveDoc::updateFps, this, [this](double changed) { if (changed == 0.0) { slotUpdateProfile(false); } else { slotUpdateProfile(true); } }, Qt::DirectConnection); connect(trackView, &Timeline::zoneMoved, this, &MainWindow::slotZoneMoved); trackView->projectView()->setContextMenu(m_timelineContextMenu, m_timelineClipActions, m_timelineContextTransitionMenu, m_clipTypeGroup, static_cast(factory()->container(QStringLiteral("marker_menu"), this))); */ getMainTimeline()->controller()->clipActions = kdenliveCategoryMap.value(QStringLiteral("timelineselection"))->actions(); connect(m_projectMonitor, SIGNAL(zoneUpdated(QPoint)), project, SLOT(setModified())); connect(m_clipMonitor, SIGNAL(zoneUpdated(QPoint)), project, SLOT(setModified())); connect(project, &KdenliveDoc::docModified, this, &MainWindow::slotUpdateDocumentState); connect(pCore->bin(), SIGNAL(displayMessage(QString, int, MessageType)), m_messageLabel, SLOT(setProgressMessage(QString, int, MessageType))); if (m_renderWidget) { slotCheckRenderStatus(); // m_renderWidget->setGuides(pCore->projectManager()->currentTimeline()->projectView()->guidesData(), project->projectDuration()); m_renderWidget->updateDocumentPath(); m_renderWidget->setRenderProfile(project->getRenderProperties()); } m_zoomSlider->setValue(project->zoom().x()); m_commandStack->setActiveStack(project->commandStack().get()); setWindowTitle(project->description()); setWindowModified(project->isModified()); m_saveAction->setEnabled(project->isModified()); m_normalEditTool->setChecked(true); connect(m_projectMonitor, &Monitor::durationChanged, this, &MainWindow::slotUpdateProjectDuration); pCore->monitorManager()->setDocument(project); connect(m_effectList2, &EffectListWidget::reloadFavorites, getMainTimeline(), &TimelineWidget::updateEffectFavorites); connect(m_transitionList2, &TransitionListWidget::reloadFavorites, getMainTimeline(), &TimelineWidget::updateTransitionFavorites); // TODO REFAC: fix // trackView->updateProfile(1.0); // Init document zone // m_projectMonitor->slotZoneMoved(trackView->inPoint(), trackView->outPoint()); // Update the mouse position display so it will display in DF/NDF format by default based on the project setting. // slotUpdateMousePosition(0); // Update guides info in render widget // slotGuidesUpdated(); // set tool to select tool setTrimMode(QString()); m_buttonSelectTool->setChecked(true); connect(m_projectMonitorDock, &QDockWidget::visibilityChanged, m_projectMonitor, &Monitor::slotRefreshMonitor, Qt::UniqueConnection); connect(m_clipMonitorDock, &QDockWidget::visibilityChanged, m_clipMonitor, &Monitor::slotRefreshMonitor, Qt::UniqueConnection); } void MainWindow::slotZoneMoved(int start, int end) { pCore->currentDoc()->setZone(start, end); QPoint zone(start, end); m_projectMonitor->slotLoadClipZone(zone); } void MainWindow::slotGuidesUpdated() { if (m_renderWidget) { double projectDuration = GenTime(getMainTimeline()->controller()->duration() - TimelineModel::seekDuration - 2, pCore->getCurrentFps()).ms() / 1000; m_renderWidget->setGuides(pCore->currentDoc()->getGuideModel()->getAllMarkers(), projectDuration); } } void MainWindow::slotEditKeys() { KShortcutsDialog dialog(KShortcutsEditor::AllActions, KShortcutsEditor::LetterShortcutsAllowed, this); // Find the combobox inside KShortcutsDialog for choosing keyboard scheme QComboBox *schemesList = nullptr; foreach (QLabel *label, dialog.findChildren()) { if (label->text() == i18n("Current scheme:")) { schemesList = qobject_cast(label->buddy()); break; } } // If scheme choosing combobox was found, find the "More Actions" button in the same // dialog that provides a dropdown menu with additional actions, and add // "Download New Keyboard Schemes..." button into that menu if (schemesList) { foreach (QPushButton *button, dialog.findChildren()) { if (button->text() == i18n("More Actions")) { QMenu *moreActionsMenu = button->menu(); moreActionsMenu->addAction(i18n("Download New Keyboard Schemes..."), this, [this, schemesList] { slotGetNewKeyboardStuff(schemesList); }); break; } } } else { qWarning() << "Could not get list of schemes. Downloading new schemes is not available."; } dialog.addCollection(actionCollection(), i18nc("general keyboard shortcuts", "General")); dialog.configure(); } void MainWindow::slotPreferences(int page, int option) { /* * An instance of your dialog could be already created and could be * cached, in which case you want to display the cached dialog * instead of creating another one */ if (KConfigDialog::showDialog(QStringLiteral("settings"))) { KdenliveSettingsDialog *d = static_cast(KConfigDialog::exists(QStringLiteral("settings"))); if (page != -1) { d->showPage(page, option); } return; } // KConfigDialog didn't find an instance of this dialog, so lets // create it : // Get the mappable actions in localized form QMap actions; KActionCollection *collection = actionCollection(); QRegExp ampEx("&{1,1}"); for (const QString &action_name : m_actionNames) { QString action_text = collection->action(action_name)->text(); action_text.remove(ampEx); actions[action_text] = action_name; } KdenliveSettingsDialog *dialog = new KdenliveSettingsDialog(actions, m_gpuAllowed, this); connect(dialog, &KConfigDialog::settingsChanged, this, &MainWindow::updateConfiguration); connect(dialog, &KConfigDialog::settingsChanged, this, &MainWindow::configurationChanged); connect(dialog, &KdenliveSettingsDialog::doResetProfile, pCore->projectManager(), &ProjectManager::slotResetProfiles); connect(dialog, &KdenliveSettingsDialog::doResetConsumer, pCore->projectManager(), &ProjectManager::slotResetConsumers); connect(dialog, &KdenliveSettingsDialog::checkTabPosition, this, &MainWindow::slotCheckTabPosition); connect(dialog, &KdenliveSettingsDialog::restartKdenlive, this, &MainWindow::slotRestart); connect(dialog, &KdenliveSettingsDialog::updateLibraryFolder, pCore.get(), &Core::updateLibraryPath); connect(dialog, &KdenliveSettingsDialog::audioThumbFormatChanged, m_timelineTabs, &TimelineTabs::audioThumbFormatChanged); connect(dialog, &KdenliveSettingsDialog::resetView, this, &MainWindow::resetTimelineTracks); dialog->show(); if (page != -1) { dialog->showPage(page, option); } } void MainWindow::slotCheckTabPosition() { int pos = tabPosition(Qt::LeftDockWidgetArea); if (KdenliveSettings::tabposition() != pos) { setTabPosition(Qt::AllDockWidgetAreas, (QTabWidget::TabPosition)KdenliveSettings::tabposition()); } } void MainWindow::slotRestart() { m_exitCode = EXIT_RESTART; QApplication::closeAllWindows(); } void MainWindow::closeEvent(QCloseEvent *event) { KXmlGuiWindow::closeEvent(event); if (event->isAccepted()) { QApplication::exit(m_exitCode); return; } } void MainWindow::updateConfiguration() { // TODO: we should apply settings to all projects, not only the current one m_buttonAudioThumbs->setChecked(KdenliveSettings::audiothumbnails()); m_buttonVideoThumbs->setChecked(KdenliveSettings::videothumbnails()); m_buttonShowMarkers->setChecked(KdenliveSettings::showmarkers()); slotSwitchAutomaticTransition(); // Update list of transcoding profiles buildDynamicActions(); loadClipActions(); } void MainWindow::slotSwitchVideoThumbs() { KdenliveSettings::setVideothumbnails(!KdenliveSettings::videothumbnails()); m_timelineTabs->showThumbnailsChanged(); m_buttonVideoThumbs->setChecked(KdenliveSettings::videothumbnails()); } void MainWindow::slotSwitchAudioThumbs() { KdenliveSettings::setAudiothumbnails(!KdenliveSettings::audiothumbnails()); m_timelineTabs->showAudioThumbnailsChanged(); m_buttonAudioThumbs->setChecked(KdenliveSettings::audiothumbnails()); } void MainWindow::slotSwitchMarkersComments() { KdenliveSettings::setShowmarkers(!KdenliveSettings::showmarkers()); getMainTimeline()->controller()->showMarkersChanged(); m_buttonShowMarkers->setChecked(KdenliveSettings::showmarkers()); } void MainWindow::slotSwitchSnap() { KdenliveSettings::setSnaptopoints(!KdenliveSettings::snaptopoints()); m_buttonSnap->setChecked(KdenliveSettings::snaptopoints()); getMainTimeline()->controller()->snapChanged(KdenliveSettings::snaptopoints()); } void MainWindow::slotSwitchAutomaticTransition() { KdenliveSettings::setAutomatictransitions(!KdenliveSettings::automatictransitions()); m_buttonAutomaticTransition->setChecked(KdenliveSettings::automatictransitions()); } void MainWindow::slotDeleteItem() { if ((QApplication::focusWidget() != nullptr) && (QApplication::focusWidget()->parentWidget() != nullptr) && QApplication::focusWidget()->parentWidget() == pCore->bin()) { pCore->bin()->slotDeleteClip(); } else { QWidget *widget = QApplication::focusWidget(); while ((widget != nullptr) && widget != this) { if (widget == m_effectStackDock) { m_assetPanel->deleteCurrentEffect(); return; } widget = widget->parentWidget(); } // effect stack has no focus getMainTimeline()->controller()->deleteSelectedClips(); } } void MainWindow::slotAddClipMarker() { std::shared_ptr clip(nullptr); GenTime pos; if (m_projectMonitor->isActive()) { return; } else { clip = m_clipMonitor->currentController(); pos = GenTime(m_clipMonitor->position(), pCore->getCurrentFps()); } if (!clip) { m_messageLabel->setMessage(i18n("Cannot find clip to add marker"), ErrorMessage); return; } QString id = clip->AbstractProjectItem::clipId(); clip->getMarkerModel()->editMarkerGui(pos, this, true, clip.get()); } void MainWindow::slotDeleteClipMarker(bool allowGuideDeletion) { std::shared_ptr clip(nullptr); GenTime pos; if (m_projectMonitor->isActive()) { // TODO refac retrieve active clip /* if (pCore->projectManager()->currentTimeline()) { ClipItem *item = pCore->projectManager()->currentTimeline()->projectView()->getActiveClipUnderCursor(); if (item) { pos = (GenTime(m_projectMonitor->position(), pCore->getCurrentFps()) - item->startPos() + item->cropStart()) / item->speed(); clip = pCore->bin()->getBinClip(item->getBinId()); } } */ } else { clip = m_clipMonitor->currentController(); pos = GenTime(m_clipMonitor->position(), pCore->getCurrentFps()); } if (!clip) { m_messageLabel->setMessage(i18n("Cannot find clip to remove marker"), ErrorMessage); return; } QString id = clip->AbstractProjectItem::clipId(); bool markerFound = false; CommentedTime marker = clip->getMarkerModel()->getMarker(pos, &markerFound); if (!markerFound) { if (allowGuideDeletion && m_projectMonitor->isActive()) { slotDeleteGuide(); } else { m_messageLabel->setMessage(i18n("No marker found at cursor time"), ErrorMessage); } return; } clip->getMarkerModel()->removeMarker(pos); } void MainWindow::slotDeleteAllClipMarkers() { std::shared_ptr clip(nullptr); if (m_projectMonitor->isActive()) { // TODO refac /* if (pCore->projectManager()->currentTimeline()) { ClipItem *item = pCore->projectManager()->currentTimeline()->projectView()->getActiveClipUnderCursor(); if (item) { clip = pCore->bin()->getBinClip(item->getBinId()); } } */ } else { clip = m_clipMonitor->currentController(); } if (!clip) { m_messageLabel->setMessage(i18n("Cannot find clip to remove marker"), ErrorMessage); return; } bool ok = clip->getMarkerModel()->removeAllMarkers(); if (!ok) { m_messageLabel->setMessage(i18n("An error occurred while deleting markers"), ErrorMessage); return; } } void MainWindow::slotEditClipMarker() { std::shared_ptr clip(nullptr); GenTime pos; if (m_projectMonitor->isActive()) { // TODO refac /* if (pCore->projectManager()->currentTimeline()) { ClipItem *item = pCore->projectManager()->currentTimeline()->projectView()->getActiveClipUnderCursor(); if (item) { pos = (GenTime(m_projectMonitor->position(), pCore->getCurrentFps()) - item->startPos() + item->cropStart()) / item->speed(); clip = pCore->bin()->getBinClip(item->getBinId()); } } */ } else { clip = m_clipMonitor->currentController(); pos = GenTime(m_clipMonitor->position(), pCore->getCurrentFps()); } if (!clip) { m_messageLabel->setMessage(i18n("Cannot find clip to edit marker"), ErrorMessage); return; } QString id = clip->AbstractProjectItem::clipId(); bool markerFound = false; CommentedTime oldMarker = clip->getMarkerModel()->getMarker(pos, &markerFound); if (!markerFound) { m_messageLabel->setMessage(i18n("No marker found at cursor time"), ErrorMessage); return; } clip->getMarkerModel()->editMarkerGui(pos, this, false, clip.get()); } void MainWindow::slotAddMarkerGuideQuickly() { if (!getMainTimeline() || !pCore->currentDoc()) { return; } if (m_clipMonitor->isActive()) { std::shared_ptr clip(m_clipMonitor->currentController()); GenTime pos(m_clipMonitor->position(), pCore->getCurrentFps()); if (!clip) { m_messageLabel->setMessage(i18n("Cannot find clip to add marker"), ErrorMessage); return; } CommentedTime marker(pos, pCore->currentDoc()->timecode().getDisplayTimecode(pos, false), KdenliveSettings::default_marker_type()); clip->getMarkerModel()->addMarker(marker.time(), marker.comment(), marker.markerType()); } else { getMainTimeline()->controller()->switchGuide(); } } void MainWindow::slotAddGuide() { getMainTimeline()->controller()->switchGuide(); } void MainWindow::slotInsertSpace() { getMainTimeline()->controller()->insertSpace(); } void MainWindow::slotRemoveSpace() { getMainTimeline()->controller()->removeSpace(-1, -1, false); } void MainWindow::slotRemoveAllSpace() { getMainTimeline()->controller()->removeSpace(-1, -1, true); } void MainWindow::slotInsertTrack() { pCore->monitorManager()->activateMonitor(Kdenlive::ProjectMonitor); getMainTimeline()->controller()->addTrack(-1); } void MainWindow::slotDeleteTrack() { pCore->monitorManager()->activateMonitor(Kdenlive::ProjectMonitor); getMainTimeline()->controller()->deleteTrack(-1); } void MainWindow::slotSelectTrack() { getMainTimeline()->controller()->selectCurrentTrack(); } void MainWindow::slotSelectAllTracks() { getMainTimeline()->controller()->selectAll(); } void MainWindow::slotUnselectAllTracks() { getMainTimeline()->controller()->clearSelection(); } void MainWindow::slotEditGuide() { getMainTimeline()->controller()->editGuide(); } void MainWindow::slotDeleteGuide() { getMainTimeline()->controller()->switchGuide(-1, true); } void MainWindow::slotDeleteAllGuides() { pCore->currentDoc()->getGuideModel()->removeAllMarkers(); } void MainWindow::slotCutTimelineClip() { getMainTimeline()->controller()->cutClipUnderCursor(); } void MainWindow::slotInsertClipOverwrite() { const QString &binId = m_clipMonitor->activeClipId(); if (binId.isEmpty()) { // No clip in monitor return; } int pos = getMainTimeline()->controller()->insertZone(binId, m_clipMonitor->getZoneInfo(), true); if (pos > 0) { pCore->monitorManager()->activateMonitor(Kdenlive::ProjectMonitor); m_projectMonitor->refreshMonitorIfActive(true); getCurrentTimeline()->controller()->setPosition(pos); pCore->monitorManager()->activateMonitor(Kdenlive::ClipMonitor); } } void MainWindow::slotInsertClipInsert() { const QString &binId = m_clipMonitor->activeClipId(); if (binId.isEmpty()) { // No clip in monitor return; } int pos = getMainTimeline()->controller()->insertZone(binId, m_clipMonitor->getZoneInfo(), false); if (pos > 0) { pCore->monitorManager()->activateMonitor(Kdenlive::ProjectMonitor); m_projectMonitor->refreshMonitorIfActive(true); getCurrentTimeline()->controller()->setPosition(pos); pCore->monitorManager()->activateMonitor(Kdenlive::ClipMonitor); } } void MainWindow::slotExtractZone() { getMainTimeline()->controller()->extractZone(m_clipMonitor->getZoneInfo()); } void MainWindow::slotLiftZone() { getMainTimeline()->controller()->extractZone(m_clipMonitor->getZoneInfo(), true); } void MainWindow::slotPreviewRender() { if (pCore->currentDoc()) { getCurrentTimeline()->controller()->startPreviewRender(); } } void MainWindow::slotStopPreviewRender() { if (pCore->currentDoc()) { getCurrentTimeline()->controller()->stopPreviewRender(); } } void MainWindow::slotDefinePreviewRender() { if (pCore->currentDoc()) { getCurrentTimeline()->controller()->addPreviewRange(true); } } void MainWindow::slotRemovePreviewRender() { if (pCore->currentDoc()) { getCurrentTimeline()->controller()->addPreviewRange(false); } } void MainWindow::slotClearPreviewRender() { if (pCore->currentDoc()) { getCurrentTimeline()->controller()->clearPreviewRange(); } } void MainWindow::slotSelectTimelineClip() { getCurrentTimeline()->controller()->selectCurrentItem(ObjectType::TimelineClip, true); } void MainWindow::slotSelectTimelineTransition() { getCurrentTimeline()->controller()->selectCurrentItem(ObjectType::TimelineComposition, true); } void MainWindow::slotDeselectTimelineClip() { getCurrentTimeline()->controller()->selectCurrentItem(ObjectType::TimelineClip, false); } void MainWindow::slotDeselectTimelineTransition() { getCurrentTimeline()->controller()->selectCurrentItem(ObjectType::TimelineComposition, false); } void MainWindow::slotSelectAddTimelineClip() { getCurrentTimeline()->controller()->selectCurrentItem(ObjectType::TimelineClip, true, true); } void MainWindow::slotSelectAddTimelineTransition() { getCurrentTimeline()->controller()->selectCurrentItem(ObjectType::TimelineComposition, true, true); } void MainWindow::slotGroupClips() { getCurrentTimeline()->controller()->groupSelection(); } void MainWindow::slotUnGroupClips() { getCurrentTimeline()->controller()->unGroupSelection(); } void MainWindow::slotEditItemDuration() { // TODO refac /* if (pCore->projectManager()->currentTimeline()) { pCore->projectManager()->currentTimeline()->projectView()->editItemDuration(); } */ } void MainWindow::slotAddProjectClip(const QUrl &url, const QStringList &folderInfo) { pCore->bin()->droppedUrls(QList() << url, folderInfo); } void MainWindow::slotAddProjectClipList(const QList &urls) { pCore->bin()->droppedUrls(urls); } void MainWindow::slotAddTransition(QAction *result) { if (!result) { return; } // TODO refac /* QStringList info = result->data().toStringList(); if (info.isEmpty() || info.count() < 2) { return; } QDomElement transition = transitions.getEffectByTag(info.at(0), info.at(1)); if (pCore->projectManager()->currentTimeline() && !transition.isNull()) { pCore->projectManager()->currentTimeline()->projectView()->slotAddTransitionToSelectedClips(transition.cloneNode().toElement()); } */ } void MainWindow::slotAddEffect(QAction *result) { qDebug() << "// EFFECTS MENU TRIGGERED: " << result->data().toString(); if (!result) { return; } QString effectId = result->data().toString(); addEffect(effectId); } void MainWindow::addEffect(const QString &effectId) { if (m_assetPanel->effectStackOwner().first == ObjectType::TimelineClip) { // Add effect to the current timeline selection QVariantMap effectData; effectData.insert(QStringLiteral("kdenlive/effect"), effectId); pCore->window()->getMainTimeline()->controller()->addAsset(effectData); } else if (m_assetPanel->effectStackOwner().first == ObjectType::TimelineTrack || m_assetPanel->effectStackOwner().first == ObjectType::BinClip) { if (!m_assetPanel->addEffect(effectId)) { pCore->displayMessage(i18n("Cannot add effect to clip"), InformationMessage); } } else { pCore->displayMessage(i18n("Select an item to add effect"), InformationMessage); } } void MainWindow::slotZoomIn(bool zoomOnMouse) { slotSetZoom(m_zoomSlider->value() - 1, zoomOnMouse); slotShowZoomSliderToolTip(); } void MainWindow::slotZoomOut(bool zoomOnMouse) { slotSetZoom(m_zoomSlider->value() + 1, zoomOnMouse); slotShowZoomSliderToolTip(); } void MainWindow::slotFitZoom() { /* if (pCore->projectManager()->currentTimeline()) { m_zoomSlider->setValue(pCore->projectManager()->currentTimeline()->fitZoom()); // Make sure to reset scroll bar to start pCore->projectManager()->currentTimeline()->projectView()->scrollToStart(); } */ } void MainWindow::slotSetZoom(int value, bool zoomOnMouse) { value = qBound(m_zoomSlider->minimum(), value, m_zoomSlider->maximum()); m_timelineTabs->changeZoom(value, zoomOnMouse); updateZoomSlider(value); } void MainWindow::updateZoomSlider(int value) { slotUpdateZoomSliderToolTip(value); KdenliveDoc *project = pCore->currentDoc(); if (project) { project->setZoom(value); } m_zoomOut->setEnabled(value < m_zoomSlider->maximum()); m_zoomIn->setEnabled(value > m_zoomSlider->minimum()); QSignalBlocker blocker(m_zoomSlider); m_zoomSlider->setValue(value); } void MainWindow::slotShowZoomSliderToolTip(int zoomlevel) { if (zoomlevel != -1) { slotUpdateZoomSliderToolTip(zoomlevel); } QPoint global = m_zoomSlider->rect().topLeft(); global.ry() += m_zoomSlider->height() / 2; QHelpEvent toolTipEvent(QEvent::ToolTip, QPoint(0, 0), m_zoomSlider->mapToGlobal(global)); QApplication::sendEvent(m_zoomSlider, &toolTipEvent); } void MainWindow::slotUpdateZoomSliderToolTip(int zoomlevel) { int max = m_zoomSlider->maximum() + 1; m_zoomSlider->setToolTip(i18n("Zoom Level: %1/%2", max - zoomlevel, max)); } void MainWindow::slotGotProgressInfo(const QString &message, int progress, MessageType type) { m_messageLabel->setProgressMessage(message, progress, type); } void MainWindow::customEvent(QEvent *e) { if (e->type() == QEvent::User) { m_messageLabel->setMessage(static_cast(e)->message(), MltError); } } void MainWindow::slotSnapRewind() { if (m_projectMonitor->isActive()) { getMainTimeline()->controller()->gotoPreviousSnap(); } else { m_clipMonitor->slotSeekToPreviousSnap(); } } void MainWindow::slotSnapForward() { if (m_projectMonitor->isActive()) { getMainTimeline()->controller()->gotoNextSnap(); } else { m_clipMonitor->slotSeekToNextSnap(); } } void MainWindow::slotClipStart() { if (m_projectMonitor->isActive()) { getMainTimeline()->controller()->seekCurrentClip(false); } else { m_clipMonitor->slotStart(); } } void MainWindow::slotClipEnd() { if (m_projectMonitor->isActive()) { getMainTimeline()->controller()->seekCurrentClip(true); } else { m_clipMonitor->slotEnd(); } } void MainWindow::slotChangeTool(QAction *action) { if (action == m_buttonSelectTool) { slotSetTool(SelectTool); } else if (action == m_buttonRazorTool) { slotSetTool(RazorTool); } else if (action == m_buttonSpacerTool) { slotSetTool(SpacerTool); } } void MainWindow::slotChangeEdit(QAction *action) { TimelineMode::EditMode mode = TimelineMode::NormalEdit; if (action == m_overwriteEditTool) { mode = TimelineMode::OverwriteEdit; } else if (action == m_insertEditTool) { mode = TimelineMode::InsertEdit; } getMainTimeline()->controller()->getModel()->setEditMode(mode); } void MainWindow::slotSetTool(ProjectTool tool) { if (pCore->currentDoc()) { // pCore->currentDoc()->setTool(tool); QString message; switch (tool) { case SpacerTool: message = i18n("Ctrl + click to use spacer on current track only"); break; case RazorTool: message = i18n("Click on a clip to cut it, Shift + move to preview cut frame"); break; default: message = i18n("Shift + click to create a selection rectangle, Ctrl + click to add an item to selection"); break; } m_messageLabel->setMessage(message, InformationMessage); getMainTimeline()->setTool(tool); } } void MainWindow::slotCopy() { getMainTimeline()->controller()->copyItem(); } void MainWindow::slotPaste() { getMainTimeline()->controller()->pasteItem(); } void MainWindow::slotPasteEffects() { getMainTimeline()->controller()->pasteEffects(); } -void MainWindow::slotClipInTimeline(const QString &clipId, QList ids) +void MainWindow::slotClipInTimeline(const QString &clipId, const QList &ids) { Q_UNUSED(clipId) QMenu *inTimelineMenu = static_cast(factory()->container(QStringLiteral("clip_in_timeline"), this)); QList actionList; for (int i = 0; i < ids.count(); ++i) { QString track = getMainTimeline()->controller()->getTrackNameFromIndex(pCore->getItemTrack(ObjectId(ObjectType::TimelineClip, ids.at(i)))); QString start = pCore->currentDoc()->timecode().getTimecodeFromFrames(pCore->getItemPosition(ObjectId(ObjectType::TimelineClip, ids.at(i)))); int j = 0; QAction *a = new QAction(track + QStringLiteral(": ") + start, inTimelineMenu); a->setData(ids.at(i)); connect(a, &QAction::triggered, this, &MainWindow::slotSelectClipInTimeline); while (j < actionList.count()) { if (actionList.at(j)->text() > a->text()) { break; } j++; } actionList.insert(j, a); } QList list = inTimelineMenu->actions(); unplugActionList(QStringLiteral("timeline_occurences")); qDeleteAll(list); plugActionList(QStringLiteral("timeline_occurences"), actionList); if (actionList.isEmpty()) { inTimelineMenu->setEnabled(false); } else { inTimelineMenu->setEnabled(true); } } void MainWindow::slotClipInProjectTree() { QList ids = getMainTimeline()->controller()->selection(); if (!ids.isEmpty()) { m_projectBinDock->raise(); ObjectId id(ObjectType::TimelineClip, ids.constFirst()); int start = pCore->getItemIn(id); int duration = pCore->getItemDuration(id); QPoint zone(start, start + duration); qDebug() << " - - selecting clip on monitor, zone: " << zone; if (m_projectMonitor->isActive()) { slotSwitchMonitors(); } int pos = m_projectMonitor->position(); int itemPos = pCore->getItemPosition(id); if (pos >= itemPos && pos < itemPos + duration) { pos -= (itemPos - start); } else { pos = -1; } pCore->selectBinClip(getMainTimeline()->controller()->getClipBinId(ids.constFirst()), pos, zone); } } void MainWindow::slotSelectClipInTimeline() { pCore->monitorManager()->activateMonitor(Kdenlive::ProjectMonitor); QAction *action = qobject_cast(sender()); int clipId = action->data().toInt(); getMainTimeline()->controller()->focusItem(clipId); } /** Gets called when the window gets hidden */ void MainWindow::hideEvent(QHideEvent * /*event*/) { if (isMinimized() && pCore->monitorManager()) { pCore->monitorManager()->pauseActiveMonitor(); } } /*void MainWindow::slotSaveZone(Render *render, const QPoint &zone, DocClipBase *baseClip, QUrl path) { QPointer dialog = new QDialog(this); dialog->setWindowTitle("Save clip zone"); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel); QVBoxLayout *mainLayout = new QVBoxLayout; dialog->setLayout(mainLayout); QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); dialog->connect(buttonBox, SIGNAL(accepted()), dialog, SLOT(accept())); dialog->connect(buttonBox, SIGNAL(rejected()), dialog, SLOT(reject())); QLabel *label1 = new QLabel(i18n("Save clip zone as:"), this); if (path.isEmpty()) { QString tmppath = pCore->currentDoc()->projectFolder().path() + QDir::separator(); if (baseClip == nullptr) { tmppath.append("untitled.mlt"); } else { tmppath.append((baseClip->name().isEmpty() ? baseClip->fileURL().fileName() : baseClip->name()) + '-' + QString::number(zone.x()).rightJustified(4, '0') + QStringLiteral(".mlt")); } path = QUrl(tmppath); } KUrlRequester *url = new KUrlRequester(path, this); url->setFilter("video/mlt-playlist"); QLabel *label2 = new QLabel(i18n("Description:"), this); QLineEdit *edit = new QLineEdit(this); mainLayout->addWidget(label1); mainLayout->addWidget(url); mainLayout->addWidget(label2); mainLayout->addWidget(edit); mainLayout->addWidget(buttonBox); if (dialog->exec() == QDialog::Accepted) { if (QFile::exists(url->url().path())) { if (KMessageBox::questionYesNo(this, i18n("File %1 already exists.\nDo you want to overwrite it?", url->url().path())) == KMessageBox::No) { slotSaveZone(render, zone, baseClip, url->url()); delete dialog; return; } } if (baseClip && !baseClip->fileURL().isEmpty()) { // create zone from clip url, so that we don't have problems with proxy clips QProcess p; QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); env.remove("MLT_PROFILE"); p.setProcessEnvironment(env); p.start(KdenliveSettings::rendererpath(), QStringList() << baseClip->fileURL().toLocalFile() << "in=" + QString::number(zone.x()) << "out=" + QString::number(zone.y()) << "-consumer" << "xml:" + url->url().path()); if (!p.waitForStarted(3000)) { KMessageBox::sorry(this, i18n("Cannot start MLT's renderer:\n%1", KdenliveSettings::rendererpath())); } else if (!p.waitForFinished(5000)) { KMessageBox::sorry(this, i18n("Timeout while creating xml output")); } } else render->saveZone(url->url(), edit->text(), zone); } delete dialog; }*/ void MainWindow::slotResizeItemStart() { getMainTimeline()->controller()->setInPoint(); } void MainWindow::slotResizeItemEnd() { getMainTimeline()->controller()->setOutPoint(); } int MainWindow::getNewStuff(const QString &configFile) { KNS3::Entry::List entries; QPointer dialog = new KNS3::DownloadDialog(configFile); if (dialog->exec() != 0) { entries = dialog->changedEntries(); } for (const KNS3::Entry &entry : entries) { if (entry.status() == KNS3::Entry::Installed) { qCDebug(KDENLIVE_LOG) << "// Installed files: " << entry.installedFiles(); } } delete dialog; return entries.size(); } void MainWindow::slotGetNewKeyboardStuff(QComboBox *schemesList) { if (getNewStuff(QStringLiteral(":data/kdenlive_keyboardschemes.knsrc")) > 0) { // Refresh keyboard schemes list (schemes list creation code copied from KShortcutSchemesEditor) QStringList schemes; schemes << QStringLiteral("Default"); // List files in the shortcuts subdir, each one is a scheme. See KShortcutSchemesHelper::{shortcutSchemeFileName,exportActionCollection} const QStringList shortcutsDirs = QStandardPaths::locateAll( QStandardPaths::GenericDataLocation, QCoreApplication::applicationName() + QStringLiteral("/shortcuts"), QStandardPaths::LocateDirectory); qCDebug(KDENLIVE_LOG) << "shortcut scheme dirs:" << shortcutsDirs; Q_FOREACH (const QString &dir, shortcutsDirs) { Q_FOREACH (const QString &file, QDir(dir).entryList(QDir::Files | QDir::NoDotAndDotDot)) { qCDebug(KDENLIVE_LOG) << "shortcut scheme file:" << file; schemes << file; } } schemesList->clear(); schemesList->addItems(schemes); } } void MainWindow::slotAutoTransition() { // TODO refac /* if (pCore->projectManager()->currentTimeline()) { pCore->projectManager()->currentTimeline()->projectView()->autoTransition(); } */ } void MainWindow::slotSplitAV() { getMainTimeline()->controller()->splitAV(); } void MainWindow::slotSetAudioAlignReference() { // TODO refac /* if (pCore->projectManager()->currentTimeline()) { pCore->projectManager()->currentTimeline()->projectView()->setAudioAlignReference(); } */ } void MainWindow::slotAlignAudio() { // TODO refac /* if (pCore->projectManager()->currentTimeline()) { pCore->projectManager()->currentTimeline()->projectView()->alignAudio(); } */ } void MainWindow::slotUpdateClipType(QAction *action) { Q_UNUSED(action) // TODO refac /* if (pCore->projectManager()->currentTimeline()) { PlaylistState::ClipState state = (PlaylistState::ClipState)action->data().toInt(); pCore->projectManager()->currentTimeline()->projectView()->setClipType(state); } */ } void MainWindow::slotUpdateTimelineView(QAction *action) { int viewMode = action->data().toInt(); KdenliveSettings::setAudiotracksbelow(viewMode == 1); getMainTimeline()->controller()->getModel()->_resetView(); } void MainWindow::slotDvdWizard(const QString &url) { // We must stop the monitors since we create a new on in the dvd wizard QPointer w = new DvdWizard(pCore->monitorManager(), url, this); w->exec(); delete w; pCore->monitorManager()->activateMonitor(Kdenlive::ClipMonitor); } void MainWindow::slotShowTimeline(bool show) { if (!show) { m_timelineState = saveState(); centralWidget()->setHidden(true); } else { centralWidget()->setHidden(false); restoreState(m_timelineState); } } void MainWindow::loadClipActions() { unplugActionList(QStringLiteral("add_effect")); plugActionList(QStringLiteral("add_effect"), m_effectsMenu->actions()); QList clipJobActions = getExtraActions(QStringLiteral("clipjobs")); unplugActionList(QStringLiteral("clip_jobs")); plugActionList(QStringLiteral("clip_jobs"), clipJobActions); QList atcActions = getExtraActions(QStringLiteral("audiotranscoderslist")); unplugActionList(QStringLiteral("audio_transcoders_list")); plugActionList(QStringLiteral("audio_transcoders_list"), atcActions); QList tcActions = getExtraActions(QStringLiteral("transcoderslist")); unplugActionList(QStringLiteral("transcoders_list")); plugActionList(QStringLiteral("transcoders_list"), tcActions); } void MainWindow::loadDockActions() { QList list = kdenliveCategoryMap.value(QStringLiteral("interface"))->actions(); // Sort actions QMap sorted; QStringList sortedList; for (QAction *a : list) { sorted.insert(a->text(), a); sortedList << a->text(); } QList orderedList; sortedList.sort(Qt::CaseInsensitive); for (const QString &text : sortedList) { orderedList << sorted.value(text); } unplugActionList(QStringLiteral("dock_actions")); plugActionList(QStringLiteral("dock_actions"), orderedList); } void MainWindow::buildDynamicActions() { KActionCategory *ts = nullptr; if (kdenliveCategoryMap.contains(QStringLiteral("clipjobs"))) { ts = kdenliveCategoryMap.take(QStringLiteral("clipjobs")); delete ts; } ts = new KActionCategory(i18n("Clip Jobs"), m_extraFactory->actionCollection()); Mlt::Profile profile; std::unique_ptr filter; for (const QString &stab : {QStringLiteral("vidstab"), QStringLiteral("videostab2"), QStringLiteral("videostab")}) { filter.reset(new Mlt::Filter(profile, stab.toUtf8().constData())); if ((filter != nullptr) && filter->is_valid()) { QAction *action = new QAction(i18n("Stabilize") + QStringLiteral(" (") + stab + QLatin1Char(')'), m_extraFactory->actionCollection()); ts->addAction(action->text(), action); connect(action, &QAction::triggered, [stab]() { pCore->jobManager()->startJob(pCore->bin()->selectedClipsIds(), {}, i18np("Stabilize clip", "Stabilize clips", pCore->bin()->selectedClipsIds().size()), stab); }); break; } } filter.reset(new Mlt::Filter(profile, "motion_est")); if (filter) { if (filter->is_valid()) { QAction *action = new QAction(i18n("Automatic scene split"), m_extraFactory->actionCollection()); ts->addAction(action->text(), action); connect(action, &QAction::triggered, [&]() { pCore->jobManager()->startJob(pCore->bin()->selectedClipsIds(), {}, i18n("Scene detection")); }); } } if (true /* TODO: check if timewarp producer is available */) { QAction *action = new QAction(i18n("Duplicate clip with speed change"), m_extraFactory->actionCollection()); ts->addAction(action->text(), action); connect(action, &QAction::triggered, [&]() { pCore->jobManager()->startJob(pCore->bin()->selectedClipsIds(), {}, i18n("Change clip speed")); }); } // TODO refac reimplement analyseclipjob /* QAction *action = new QAction(i18n("Analyse keyframes"), m_extraFactory->actionCollection()); QStringList stabJob(QString::number((int)AbstractClipJob::ANALYSECLIPJOB)); action->setData(stabJob); ts->addAction(action->text(), action); connect(action, &QAction::triggered, pCore->bin(), &Bin::slotStartClipJob); */ kdenliveCategoryMap.insert(QStringLiteral("clipjobs"), ts); if (kdenliveCategoryMap.contains(QStringLiteral("transcoderslist"))) { ts = kdenliveCategoryMap.take(QStringLiteral("transcoderslist")); delete ts; } if (kdenliveCategoryMap.contains(QStringLiteral("audiotranscoderslist"))) { ts = kdenliveCategoryMap.take(QStringLiteral("audiotranscoderslist")); delete ts; } // TODO refac : reimplement transcode /* ts = new KActionCategory(i18n("Transcoders"), m_extraFactory->actionCollection()); KActionCategory *ats = new KActionCategory(i18n("Extract Audio"), m_extraFactory->actionCollection()); KSharedConfigPtr config = KSharedConfig::openConfig(QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("kdenlivetranscodingrc")), KConfig::CascadeConfig); KConfigGroup transConfig(config, "Transcoding"); // read the entries QMap profiles = transConfig.entryMap(); QMapIterator i(profiles); while (i.hasNext()) { i.next(); QStringList transList; transList << QString::number((int)AbstractClipJob::TRANSCODEJOB); transList << i.value().split(QLatin1Char(';')); auto *a = new QAction(i.key(), m_extraFactory->actionCollection()); a->setData(transList); if (transList.count() > 1) { a->setToolTip(transList.at(1)); } // slottranscode connect(a, &QAction::triggered, pCore->bin(), &Bin::slotStartClipJob); if (transList.count() > 3 && transList.at(3) == QLatin1String("audio")) { // This is an audio transcoding action ats->addAction(i.key(), a); } else { ts->addAction(i.key(), a); } } kdenliveCategoryMap.insert(QStringLiteral("transcoderslist"), ts); kdenliveCategoryMap.insert(QStringLiteral("audiotranscoderslist"), ats); */ // Populate View menu with show / hide actions for dock widgets KActionCategory *guiActions = nullptr; if (kdenliveCategoryMap.contains(QStringLiteral("interface"))) { guiActions = kdenliveCategoryMap.take(QStringLiteral("interface")); delete guiActions; } guiActions = new KActionCategory(i18n("Interface"), actionCollection()); QAction *showTimeline = new QAction(i18n("Timeline"), this); showTimeline->setCheckable(true); showTimeline->setChecked(true); connect(showTimeline, &QAction::triggered, this, &MainWindow::slotShowTimeline); guiActions->addAction(showTimeline->text(), showTimeline); actionCollection()->addAction(showTimeline->text(), showTimeline); QList docks = findChildren(); for (int j = 0; j < docks.count(); ++j) { QDockWidget *dock = docks.at(j); QAction *dockInformations = dock->toggleViewAction(); if (!dockInformations) { continue; } dockInformations->setChecked(!dock->isHidden()); guiActions->addAction(dockInformations->text(), dockInformations); } kdenliveCategoryMap.insert(QStringLiteral("interface"), guiActions); } QList MainWindow::getExtraActions(const QString &name) { if (!kdenliveCategoryMap.contains(name)) { return QList(); } return kdenliveCategoryMap.value(name)->actions(); } void MainWindow::slotTranscode(const QStringList &urls) { Q_UNUSED(urls) // TODO refac : remove or reimplement transcoding /* QString params; QString desc; if (urls.isEmpty()) { QAction *action = qobject_cast(sender()); QStringList transList = action->data().toStringList(); pCore->bin()->startClipJob(transList); return; } if (urls.isEmpty()) { m_messageLabel->setMessage(i18n("No clip to transcode"), ErrorMessage); return; } qCDebug(KDENLIVE_LOG) << "// TRANSODING FOLDER: " << pCore->bin()->getFolderInfo(); ClipTranscode *d = new ClipTranscode(urls, params, QStringList(), desc, pCore->bin()->getFolderInfo()); connect(d, &ClipTranscode::addClip, this, &MainWindow::slotAddProjectClip); d->show(); */ } void MainWindow::slotTranscodeClip() { // TODO refac : remove or reimplement transcoding /* QString allExtensions = ClipCreationDialog::getExtensions().join(QLatin1Char(' ')); const QString dialogFilter = i18n("All Supported Files") + QLatin1Char('(') + allExtensions + QStringLiteral(");;") + i18n("All Files") + QStringLiteral("(*)"); QString clipFolder = KRecentDirs::dir(QStringLiteral(":KdenliveClipFolder")); QStringList urls = QFileDialog::getOpenFileNames(this, i18n("Files to transcode"), clipFolder, dialogFilter); if (urls.isEmpty()) { return; } slotTranscode(urls); */ } void MainWindow::slotSetDocumentRenderProfile(const QMap &props) { KdenliveDoc *project = pCore->currentDoc(); bool modified = false; QMapIterator i(props); while (i.hasNext()) { i.next(); if (project->getDocumentProperty(i.key()) == i.value()) { continue; } project->setDocumentProperty(i.key(), i.value()); modified = true; } if (modified) { project->setModified(); } } void MainWindow::slotUpdateTimecodeFormat(int ix) { KdenliveSettings::setFrametimecode(ix == 1); m_clipMonitor->updateTimecodeFormat(); m_projectMonitor->updateTimecodeFormat(); // TODO refac: reimplement ? // m_effectStack->transitionConfig()->updateTimecodeFormat(); // m_effectStack->updateTimecodeFormat(); pCore->bin()->updateTimecodeFormat(); getMainTimeline()->controller()->frameFormatChanged(); m_timeFormatButton->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); } void MainWindow::slotRemoveFocus() { getMainTimeline()->setFocus(); } void MainWindow::slotShutdown() { pCore->currentDoc()->setModified(false); // Call shutdown QDBusConnectionInterface *interface = QDBusConnection::sessionBus().interface(); if ((interface != nullptr) && interface->isServiceRegistered(QStringLiteral("org.kde.ksmserver"))) { QDBusInterface smserver(QStringLiteral("org.kde.ksmserver"), QStringLiteral("/KSMServer"), QStringLiteral("org.kde.KSMServerInterface")); smserver.call(QStringLiteral("logout"), 1, 2, 2); } else if ((interface != nullptr) && interface->isServiceRegistered(QStringLiteral("org.gnome.SessionManager"))) { QDBusInterface smserver(QStringLiteral("org.gnome.SessionManager"), QStringLiteral("/org/gnome/SessionManager"), QStringLiteral("org.gnome.SessionManager")); smserver.call(QStringLiteral("Shutdown")); } } void MainWindow::slotSwitchMonitors() { pCore->monitorManager()->slotSwitchMonitors(!m_clipMonitor->isActive()); if (m_projectMonitor->isActive()) { getMainTimeline()->setFocus(); } else { pCore->bin()->focusBinView(); } } void MainWindow::slotSwitchMonitorOverlay(QAction *action) { if (pCore->monitorManager()->isActive(Kdenlive::ClipMonitor)) { m_clipMonitor->switchMonitorInfo(action->data().toInt()); } else { m_projectMonitor->switchMonitorInfo(action->data().toInt()); } } void MainWindow::slotSwitchDropFrames(bool drop) { m_clipMonitor->switchDropFrames(drop); m_projectMonitor->switchDropFrames(drop); } void MainWindow::slotSetMonitorGamma(int gamma) { KdenliveSettings::setMonitor_gamma(gamma); m_clipMonitor->updateMonitorGamma(); m_projectMonitor->updateMonitorGamma(); } void MainWindow::slotInsertZoneToTree() { if (!m_clipMonitor->isActive() || m_clipMonitor->currentController() == nullptr) { return; } QPoint info = m_clipMonitor->getZoneInfo(); QString id; pCore->projectItemModel()->requestAddBinSubClip(id, info.x(), info.y(), QString(), m_clipMonitor->activeClipId()); } void MainWindow::slotMonitorRequestRenderFrame(bool request) { if (request) { m_projectMonitor->sendFrameForAnalysis(true); return; } for (int i = 0; i < m_gfxScopesList.count(); ++i) { if (m_gfxScopesList.at(i)->isVisible() && tabifiedDockWidgets(m_gfxScopesList.at(i)).isEmpty() && static_cast(m_gfxScopesList.at(i)->widget())->autoRefreshEnabled()) { request = true; break; } } #ifdef DEBUG_MAINW qCDebug(KDENLIVE_LOG) << "Any scope accepting new frames? " << request; #endif if (!request) { m_projectMonitor->sendFrameForAnalysis(false); } } void MainWindow::slotUpdateProxySettings() { KdenliveDoc *project = pCore->currentDoc(); if (m_renderWidget) { m_renderWidget->updateProxyConfig(project->useProxy()); } pCore->bin()->refreshProxySettings(); } void MainWindow::slotArchiveProject() { KdenliveDoc *doc = pCore->currentDoc(); QDomDocument xmlDoc = doc->xmlSceneList(m_projectMonitor->sceneList(doc->url().adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).toLocalFile())); QPointer d(new ArchiveWidget(doc->url().fileName(), xmlDoc, getMainTimeline()->controller()->extractCompositionLumas(), this)); if (d->exec() != 0) { m_messageLabel->setMessage(i18n("Archiving project"), OperationCompletedMessage); } } void MainWindow::slotDownloadResources() { QString currentFolder; if (pCore->currentDoc()) { currentFolder = pCore->currentDoc()->projectDataFolder(); } else { currentFolder = KdenliveSettings::defaultprojectfolder(); } auto *d = new ResourceWidget(currentFolder); connect(d, &ResourceWidget::addClip, this, &MainWindow::slotAddProjectClip); d->show(); } void MainWindow::slotProcessImportKeyframes(GraphicsRectItem type, const QString &tag, const QString &keyframes) { Q_UNUSED(keyframes) Q_UNUSED(tag) if (type == AVWidget) { // This data should be sent to the effect stack // TODO REFAC reimplement // m_effectStack->setKeyframes(tag, data); } else if (type == TransitionWidget) { // This data should be sent to the transition stack // TODO REFAC reimplement // m_effectStack->transitionConfig()->setKeyframes(tag, data); } else { // Error } } void MainWindow::slotAlignPlayheadToMousePos() { pCore->monitorManager()->activateMonitor(Kdenlive::ProjectMonitor); getMainTimeline()->controller()->seekToMouse(); } void MainWindow::triggerKey(QKeyEvent *ev) { // Hack: The QQuickWindow that displays fullscreen monitor does not integrate quith QActions. // so on keypress events we parse keys and check for shortcuts in all existing actions QKeySequence seq; // Remove the Num modifier or some shortcuts like "*" will not work if (ev->modifiers() != Qt::KeypadModifier) { seq = QKeySequence(ev->key() + static_cast(ev->modifiers())); } else { seq = QKeySequence(ev->key()); } QList collections = KActionCollection::allCollections(); for (int i = 0; i < collections.count(); ++i) { KActionCollection *coll = collections.at(i); for (QAction *tempAction : coll->actions()) { if (tempAction->shortcuts().contains(seq)) { // Trigger action tempAction->trigger(); ev->accept(); return; } } } } QDockWidget *MainWindow::addDock(const QString &title, const QString &objectName, QWidget *widget, Qt::DockWidgetArea area) { QDockWidget *dockWidget = new QDockWidget(title, this); dockWidget->setObjectName(objectName); dockWidget->setWidget(widget); addDockWidget(area, dockWidget); connect(dockWidget, &QDockWidget::dockLocationChanged, this, [this](Qt::DockWidgetArea dockLocationArea) { if (dockLocationArea == Qt::NoDockWidgetArea) { updateDockTitleBars(false); } else { updateDockTitleBars(true); } }); connect(dockWidget, &QDockWidget::topLevelChanged, this, &MainWindow::updateDockTitleBars); return dockWidget; } void MainWindow::slotUpdateMonitorOverlays(int id, int code) { QMenu *monitorOverlay = static_cast(factory()->container(QStringLiteral("monitor_config_overlay"), this)); if (!monitorOverlay) { return; } QList actions = monitorOverlay->actions(); for (QAction *ac : actions) { int mid = ac->data().toInt(); if (mid == 0x010) { ac->setEnabled(id == Kdenlive::ClipMonitor); } ac->setChecked(code & mid); } } void MainWindow::slotChangeStyle(QAction *a) { QString style = a->data().toString(); KdenliveSettings::setWidgetstyle(style); doChangeStyle(); } void MainWindow::doChangeStyle() { QString newStyle = KdenliveSettings::widgetstyle(); if (newStyle.isEmpty() || newStyle == QStringLiteral("Default")) { newStyle = defaultStyle("Breeze"); } QApplication::setStyle(QStyleFactory::create(newStyle)); } bool MainWindow::isTabbedWith(QDockWidget *widget, const QString &otherWidget) { QList tabbed = tabifiedDockWidgets(widget); for (int i = 0; i < tabbed.count(); i++) { if (tabbed.at(i)->objectName() == otherWidget) { return true; } } return false; } void MainWindow::updateDockTitleBars(bool isTopLevel) { if (!KdenliveSettings::showtitlebars() || !isTopLevel) { return; } QList docks = pCore->window()->findChildren(); for (int i = 0; i < docks.count(); ++i) { QDockWidget *dock = docks.at(i); QWidget *bar = dock->titleBarWidget(); if (dock->isFloating()) { if (bar) { dock->setTitleBarWidget(nullptr); delete bar; } continue; } QList docked = pCore->window()->tabifiedDockWidgets(dock); if (docked.isEmpty()) { if (bar) { dock->setTitleBarWidget(nullptr); delete bar; } continue; } bool hasVisibleDockSibling = false; for (QDockWidget *sub : docked) { if (sub->toggleViewAction()->isChecked()) { // we have another docked widget, so tabs are visible and can be used instead of title bars hasVisibleDockSibling = true; break; } } if (!hasVisibleDockSibling) { if (bar) { dock->setTitleBarWidget(nullptr); delete bar; } continue; } if (!bar) { dock->setTitleBarWidget(new QWidget); } } } void MainWindow::slotToggleAutoPreview(bool enable) { KdenliveSettings::setAutopreview(enable); if (enable && getMainTimeline()) { getMainTimeline()->controller()->startPreviewRender(); } } void MainWindow::configureToolbars() { // Since our timeline toolbar is a non-standard toolbar (as it is docked in a custom widget, not // in a QToolBarDockArea, we have to hack KXmlGuiWindow to avoid a crash when saving toolbar config. // This is why we hijack the configureToolbars() and temporarily move the toolbar to a standard location QVBoxLayout *ctnLay = (QVBoxLayout *)m_timelineToolBarContainer->layout(); ctnLay->removeWidget(m_timelineToolBar); addToolBar(Qt::BottomToolBarArea, m_timelineToolBar); auto *toolBarEditor = new KEditToolBar(guiFactory(), this); toolBarEditor->setAttribute(Qt::WA_DeleteOnClose); connect(toolBarEditor, SIGNAL(newToolBarConfig()), SLOT(saveNewToolbarConfig())); connect(toolBarEditor, &QDialog::finished, this, &MainWindow::rebuildTimlineToolBar); toolBarEditor->show(); } void MainWindow::rebuildTimlineToolBar() { // Timeline toolbar settings changed, we can now re-add our toolbar to custom location m_timelineToolBar = toolBar(QStringLiteral("timelineToolBar")); removeToolBar(m_timelineToolBar); m_timelineToolBar->setToolButtonStyle(Qt::ToolButtonIconOnly); QVBoxLayout *ctnLay = (QVBoxLayout *)m_timelineToolBarContainer->layout(); if (ctnLay) { ctnLay->insertWidget(0, m_timelineToolBar); } m_timelineToolBar->setContextMenuPolicy(Qt::CustomContextMenu); connect(m_timelineToolBar, &QWidget::customContextMenuRequested, this, &MainWindow::showTimelineToolbarMenu); m_timelineToolBar->setVisible(true); } void MainWindow::showTimelineToolbarMenu(const QPoint &pos) { QMenu menu; menu.addAction(actionCollection()->action(KStandardAction::name(KStandardAction::ConfigureToolbars))); QMenu *contextSize = new QMenu(i18n("Icon Size")); menu.addMenu(contextSize); auto *sizeGroup = new QActionGroup(contextSize); int currentSize = m_timelineToolBar->iconSize().width(); QAction *a = new QAction(i18nc("@item:inmenu Icon size", "Default"), contextSize); a->setData(m_timelineToolBar->iconSizeDefault()); a->setCheckable(true); if (m_timelineToolBar->iconSizeDefault() == currentSize) { a->setChecked(true); } a->setActionGroup(sizeGroup); contextSize->addAction(a); KIconTheme *theme = KIconLoader::global()->theme(); QList avSizes; if (theme) { avSizes = theme->querySizes(KIconLoader::Toolbar); } qSort(avSizes); if (avSizes.count() < 10) { // Fixed or threshold type icons Q_FOREACH (int it, avSizes) { QString text; if (it < 19) { text = i18n("Small (%1x%2)", it, it); } else if (it < 25) { text = i18n("Medium (%1x%2)", it, it); } else if (it < 35) { text = i18n("Large (%1x%2)", it, it); } else { text = i18n("Huge (%1x%2)", it, it); } // save the size in the contextIconSizes map auto *sizeAction = new QAction(text, contextSize); sizeAction->setData(it); sizeAction->setCheckable(true); sizeAction->setActionGroup(sizeGroup); if (it == currentSize) { sizeAction->setChecked(true); } contextSize->addAction(sizeAction); } } else { // Scalable icons. const int progression[] = {16, 22, 32, 48, 64, 96, 128, 192, 256}; for (uint i = 0; i < 9; i++) { Q_FOREACH (int it, avSizes) { if (it >= progression[i]) { QString text; if (it < 19) { text = i18n("Small (%1x%2)", it, it); } else if (it < 25) { text = i18n("Medium (%1x%2)", it, it); } else if (it < 35) { text = i18n("Large (%1x%2)", it, it); } else { text = i18n("Huge (%1x%2)", it, it); } // save the size in the contextIconSizes map auto *sizeAction = new QAction(text, contextSize); sizeAction->setData(it); sizeAction->setCheckable(true); sizeAction->setActionGroup(sizeGroup); if (it == currentSize) { sizeAction->setChecked(true); } contextSize->addAction(sizeAction); break; } } } } connect(contextSize, &QMenu::triggered, this, &MainWindow::setTimelineToolbarIconSize); menu.exec(m_timelineToolBar->mapToGlobal(pos)); contextSize->deleteLater(); } void MainWindow::setTimelineToolbarIconSize(QAction *a) { if (!a) { return; } int size = a->data().toInt(); m_timelineToolBar->setIconDimensions(size); KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup mainConfig(config, QStringLiteral("MainWindow")); KConfigGroup tbGroup(&mainConfig, QStringLiteral("Toolbar timelineToolBar")); m_timelineToolBar->saveSettings(tbGroup); } void MainWindow::slotManageCache() { QDialog d(this); d.setWindowTitle(i18n("Manage Cache Data")); auto *lay = new QVBoxLayout; TemporaryData tmp(pCore->currentDoc(), false, this); connect(&tmp, &TemporaryData::disableProxies, this, &MainWindow::slotDisableProxies); // TODO refac /* connect(&tmp, SIGNAL(disablePreview()), pCore->projectManager()->currentTimeline(), SLOT(invalidateRange())); */ QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Close); connect(buttonBox, &QDialogButtonBox::rejected, &d, &QDialog::reject); lay->addWidget(&tmp); lay->addWidget(buttonBox); d.setLayout(lay); d.exec(); } void MainWindow::slotUpdateCompositing(QAction *compose) { int mode = compose->data().toInt(); getMainTimeline()->controller()->switchCompositing(mode); if (m_renderWidget) { m_renderWidget->errorMessage(RenderWidget::CompositeError, mode == 1 ? i18n("Rendering using low quality track compositing") : QString()); } } void MainWindow::slotUpdateCompositeAction(int mode) { QList actions = m_compositeAction->actions(); for (int i = 0; i < actions.count(); i++) { if (actions.at(i)->data().toInt() == mode) { m_compositeAction->setCurrentAction(actions.at(i)); break; } } if (m_renderWidget) { m_renderWidget->errorMessage(RenderWidget::CompositeError, mode == 1 ? i18n("Rendering using low quality track compositing") : QString()); } } void MainWindow::showMenuBar(bool show) { if (!show) { KMessageBox::information(this, i18n("This will hide the menu bar completely. You can show it again by typing Ctrl+M."), i18n("Hide menu bar"), QStringLiteral("show-menubar-warning")); } menuBar()->setVisible(show); } void MainWindow::forceIconSet(bool force) { KdenliveSettings::setForce_breeze(force); if (force) { // Check current color theme QColor background = qApp->palette().window().color(); bool useDarkIcons = background.value() < 100; KdenliveSettings::setUse_dark_breeze(useDarkIcons); } if (KMessageBox::warningContinueCancel(this, i18n("Kdenlive needs to be restarted to apply icon theme change. Restart now ?")) == KMessageBox::Continue) { slotRestart(); } } void MainWindow::slotSwitchTrimMode() { // TODO refac /* if (pCore->projectManager()->currentTimeline()) { pCore->projectManager()->currentTimeline()->projectView()->switchTrimMode(); } */ } void MainWindow::setTrimMode(const QString &mode){ Q_UNUSED(mode) // TODO refac /* if (pCore->projectManager()->currentTimeline()) { m_trimLabel->setText(mode); m_trimLabel->setVisible(!mode.isEmpty()); } */ } TimelineWidget *MainWindow::getMainTimeline() const { return m_timelineTabs->getMainTimeline(); } TimelineWidget *MainWindow::getCurrentTimeline() const { return m_timelineTabs->getCurrentTimeline(); } void MainWindow::resetTimelineTracks() { TimelineWidget *current = getCurrentTimeline(); if (current) { current->controller()->resetTrackHeight(); } } void MainWindow::slotChangeSpeed(int speed) { ObjectId owner = m_assetPanel->effectStackOwner(); // TODO: manage bin clips / tracks if (owner.first == ObjectType::TimelineClip) { getCurrentTimeline()->controller()->changeItemSpeed(owner.second, speed); } } void MainWindow::slotSwitchTimelineZone(bool active) { pCore->currentDoc()->setDocumentProperty(QStringLiteral("enableTimelineZone"), active ? QStringLiteral("1") : QStringLiteral("0")); getCurrentTimeline()->controller()->useRulerChanged(); QSignalBlocker blocker(m_useTimelineZone); m_useTimelineZone->setActive(active); } void MainWindow::slotGrabItem() { getCurrentTimeline()->controller()->grabCurrent(); } // static void MainWindow::refreshLumas() { // Check for Kdenlive installed luma files, add empty string at start for no luma QStringList imagefiles; QStringList fileFilters; MainWindow::m_lumaFiles.clear(); fileFilters << QStringLiteral("*.png") << QStringLiteral("*.pgm"); QStringList customLumas = QStandardPaths::locateAll(QStandardPaths::AppDataLocation, QStringLiteral("lumas"), QStandardPaths::LocateDirectory); customLumas.append(QString(mlt_environment("MLT_DATA")) + QStringLiteral("/lumas")); for (const QString &folder : customLumas) { QDir topDir(folder); QStringList folders = topDir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot); for (const QString &f : folders) { QDir dir(topDir.absoluteFilePath(f)); QStringList filesnames = dir.entryList(fileFilters, QDir::Files); if (MainWindow::m_lumaFiles.contains(f)) { imagefiles = MainWindow::m_lumaFiles.value(f); } for (const QString &fname : filesnames) { imagefiles.append(dir.absoluteFilePath(fname)); } MainWindow::m_lumaFiles.insert(f, imagefiles); } } } #ifdef DEBUG_MAINW #undef DEBUG_MAINW #endif diff --git a/src/mainwindow.h b/src/mainwindow.h index 703f19de2..7dede2cbe 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -1,490 +1,490 @@ /*************************************************************************** * Copyright (C) 2007 by Jean-Baptiste Mardelle (jb@kdenlive.org) * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #ifndef MAINWINDOW_H #define MAINWINDOW_H #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "bin/bin.h" #include "definitions.h" #include "dvdwizard/dvdwizard.h" #include "gentime.h" #include "kdenlive_debug.h" #include "kdenlivecore_export.h" #include "statusbarmessagelabel.h" class AssetPanel; class AudioGraphSpectrum; class EffectStackView2; class EffectBasket; class EffectListWidget; class TransitionListWidget; class EffectStackView; class KIconLoader; class KdenliveDoc; class Monitor; class Render; class RenderWidget; class TimelineTabs; class TimelineWidget; class Transition; class MltErrorEvent : public QEvent { public: explicit MltErrorEvent(const QString &message) : QEvent(QEvent::User) , m_message(message) { } QString message() const { return m_message; } private: QString m_message; }; class /*KDENLIVECORE_EXPORT*/ MainWindow : public KXmlGuiWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent = nullptr); /** @brief Initialises the main window. * @param MltPath (optional) path to MLT environment * @param Url (optional) file to open * @param clipsToLoad (optional) a comma separated list of clips to import in project * * If Url is present, it will be opened, otherwise, if openlastproject is * set, latest project will be opened. If no file is open after trying this, * a default new file will be created. */ void init(); virtual ~MainWindow(); /** @brief Cache for luma files thumbnails. */ static QMap m_lumacache; static QMap m_lumaFiles; /** @brief Adds an action to the action collection and stores the name. */ void addAction(const QString &name, QAction *action, const QKeySequence &shortcut = QKeySequence(), KActionCategory *category = nullptr); /** @brief Adds an action to the action collection and stores the name. */ QAction *addAction(const QString &name, const QString &text, const QObject *receiver, const char *member, const QIcon &icon = QIcon(), const QKeySequence &shortcut = QKeySequence(), KActionCategory *category = nullptr); /** * @brief Adds a new dock widget to this window. * @param title title of the dock widget * @param objectName objectName of the dock widget (required for storing layouts) * @param widget widget to use in the dock * @param area area to which the dock should be added to * @returns the created dock widget */ QDockWidget *addDock(const QString &title, const QString &objectName, QWidget *widget, Qt::DockWidgetArea area = Qt::TopDockWidgetArea); QUndoGroup *m_commandStack; QUndoView *m_undoView; /** @brief holds info about whether movit is available on this system */ bool m_gpuAllowed; int m_exitCode; QMap kdenliveCategoryMap; QList getExtraActions(const QString &name); /** @brief Returns true if docked widget is tabbed with another widget from its object name */ bool isTabbedWith(QDockWidget *widget, const QString &otherWidget); /** @brief Returns a ptr to the main timeline widget of the project */ TimelineWidget *getMainTimeline() const; /** @brief Returns a pointer to the current timeline */ TimelineWidget *getCurrentTimeline() const; /** @brief Reload luma files */ static void refreshLumas(); protected: /** @brief Closes the window. * @return false if the user presses "Cancel" on a confirmation dialog or * the operation requested (starting waiting jobs or saving file) fails, * true otherwise */ bool queryClose() override; void closeEvent(QCloseEvent *) override; /** @brief Reports a message in the status bar when an error occurs. */ void customEvent(QEvent *e) override; /** @brief Stops the active monitor when the window gets hidden. */ void hideEvent(QHideEvent *e) override; /** @brief Saves the file and the window properties when saving the session. */ void saveProperties(KConfigGroup &config) override; /** @brief Restores the window and the file when a session is loaded. */ void readProperties(const KConfigGroup &config) override; void saveNewToolbarConfig() override; private: /** @brief Sets up all the actions and attaches them to the collection. */ void setupActions(); KColorSchemeManager *m_colorschemes; QDockWidget *m_projectBinDock; QDockWidget *m_effectListDock; QDockWidget *m_transitionListDock; TransitionListWidget *m_transitionList2; EffectListWidget *m_effectList2; AssetPanel *m_assetPanel; QDockWidget *m_effectStackDock; QDockWidget *m_clipMonitorDock; Monitor *m_clipMonitor; QDockWidget *m_projectMonitorDock; Monitor *m_projectMonitor; AudioGraphSpectrum *m_audioSpectrum; QDockWidget *m_undoViewDock; KSelectAction *m_timeFormatButton; KSelectAction *m_compositeAction; TimelineTabs *m_timelineTabs; /** This list holds all the scopes used in Kdenlive, allowing to manage some global settings */ QList m_gfxScopesList; KActionCategory *m_effectActions; KActionCategory *m_transitionActions; QMenu *m_effectsMenu; QMenu *m_transitionsMenu; QMenu *m_timelineContextMenu; QList m_timelineClipActions; KDualAction *m_useTimelineZone; /** Action names that can be used in the slotDoAction() slot, with their i18n() names */ QStringList m_actionNames; /** @brief Shortcut to remove the focus from any element. * * It allows to get out of e.g. text input fields and to press another * shortcut. */ QShortcut *m_shortcutRemoveFocus; RenderWidget *m_renderWidget; StatusBarMessageLabel *m_messageLabel; QList m_transitions; QAction *m_buttonAudioThumbs; QAction *m_buttonVideoThumbs; QAction *m_buttonShowMarkers; QAction *m_buttonFitZoom; QAction *m_buttonAutomaticTransition; QAction *m_normalEditTool; QAction *m_overwriteEditTool; QAction *m_insertEditTool; QAction *m_buttonSelectTool; QAction *m_buttonRazorTool; QAction *m_buttonSpacerTool; QAction *m_buttonSnap; QAction *m_saveAction; QSlider *m_zoomSlider; QAction *m_zoomIn; QAction *m_zoomOut; QAction *m_loopZone; QAction *m_playZone; QAction *m_loopClip; QAction *m_proxyClip; QString m_theme; KIconLoader *m_iconLoader; KToolBar *m_timelineToolBar; QWidget *m_timelineToolBarContainer; QLabel *m_trimLabel; /** @brief initialize startup values, return true if first run. */ bool readOptions(); void saveOptions(); void loadGenerators(); /** @brief Instantiates a "Get Hot New Stuff" dialog. * @param configFile configuration file for KNewStuff * @return number of installed items */ int getNewStuff(const QString &configFile = QString()); QStringList m_pluginFileNames; QByteArray m_timelineState; void buildDynamicActions(); void loadClipActions(); QTime m_timer; KXMLGUIClient *m_extraFactory; bool m_themeInitialized; bool m_isDarkTheme; EffectBasket *m_effectBasket; /** @brief Update widget style. */ void doChangeStyle(); void updateActionsToolTip(); public slots: void slotGotProgressInfo(const QString &message, int progress, MessageType type = DefaultMessage); void slotReloadEffects(const QStringList &paths); Q_SCRIPTABLE void setRenderingProgress(const QString &url, int progress); Q_SCRIPTABLE void setRenderingFinished(const QString &url, int status, const QString &error); Q_SCRIPTABLE void addProjectClip(const QString &url); Q_SCRIPTABLE void addTimelineClip(const QString &url); Q_SCRIPTABLE void addEffect(const QString &effectId); Q_SCRIPTABLE void scriptRender(const QString &url); Q_NOREPLY void exitApp(); void slotSwitchVideoThumbs(); void slotSwitchAudioThumbs(); void slotPreferences(int page = -1, int option = -1); void connectDocument(); /** @brief Reload project profile in config dialog if changed. */ void slotRefreshProfiles(); void updateDockTitleBars(bool isTopLevel = true); void configureToolbars() override; /** @brief Decreases the timeline zoom level by 1. */ void slotZoomIn(bool zoomOnMouse = false); /** @brief Increases the timeline zoom level by 1. */ void slotZoomOut(bool zoomOnMouse = false); /** @brief Enable or disable the use of timeline zone for edits. */ void slotSwitchTimelineZone(bool toggled); private slots: /** @brief Shows the shortcut dialog. */ void slotEditKeys(); void loadDockActions(); /** @brief Reflects setting changes to the GUI. */ void updateConfiguration(); void slotConnectMonitors(); void slotUpdateMousePosition(int pos); void slotUpdateProjectDuration(int pos); void slotEditProjectSettings(); void slotSwitchMarkersComments(); void slotSwitchSnap(); void slotSwitchAutomaticTransition(); void slotRenderProject(); void slotStopRenderProject(); void slotFullScreen(); /** @brief if modified is true adds "modified" to the caption and enables the save button. * (triggered by KdenliveDoc::setModified()) */ void slotUpdateDocumentState(bool modified); /** @brief Sets the timeline zoom slider to @param value. * * Also disables zoomIn and zoomOut actions if they cannot be used at the moment. */ void slotSetZoom(int value, bool zoomOnMouse = false); /** @brief Makes the timeline zoom level fit the timeline content. */ void slotFitZoom(); /** @brief Updates the zoom slider tooltip to fit @param zoomlevel. */ void slotUpdateZoomSliderToolTip(int zoomlevel); /** @brief Timeline was zoom, update slider to reflect that */ void updateZoomSlider(int value); /** @brief Displays the zoom slider tooltip. * @param zoomlevel (optional) The zoom level to show in the tooltip. * * Adopted from Dolphin (src/statusbar/dolphinstatusbar.cpp) */ void slotShowZoomSliderToolTip(int zoomlevel = -1); /** @brief Deletes item in timeline, project tree or effect stack depending on focus. */ void slotDeleteItem(); void slotAddClipMarker(); void slotDeleteClipMarker(bool allowGuideDeletion = false); void slotDeleteAllClipMarkers(); void slotEditClipMarker(); /** @brief Adds marker or guide at the current position without showing the marker dialog. * * Adds a marker if clip monitor is active, otherwise a guide. * The comment is set to the current position (therefore not dialog). * This can be useful to mark something during playback. */ void slotAddMarkerGuideQuickly(); void slotCutTimelineClip(); void slotInsertClipOverwrite(); void slotInsertClipInsert(); void slotExtractZone(); void slotLiftZone(); void slotPreviewRender(); void slotStopPreviewRender(); void slotDefinePreviewRender(); void slotRemovePreviewRender(); void slotClearPreviewRender(); void slotSelectTimelineClip(); void slotSelectTimelineTransition(); void slotDeselectTimelineClip(); void slotDeselectTimelineTransition(); void slotSelectAddTimelineClip(); void slotSelectAddTimelineTransition(); void slotAddEffect(QAction *result); void slotAddTransition(QAction *result); void slotAddProjectClip(const QUrl &url, const QStringList &folderInfo); void slotAddProjectClipList(const QList &urls); void slotChangeTool(QAction *action); void slotChangeEdit(QAction *action); void slotSetTool(ProjectTool tool); void slotSnapForward(); void slotSnapRewind(); void slotClipStart(); void slotClipEnd(); void slotSelectClipInTimeline(); - void slotClipInTimeline(const QString &clipId, QList ids); + void slotClipInTimeline(const QString &clipId, const QList &ids); void slotInsertSpace(); void slotRemoveSpace(); void slotRemoveAllSpace(); void slotAddGuide(); void slotEditGuide(); void slotDeleteGuide(); void slotDeleteAllGuides(); void slotGuidesUpdated(); void slotCopy(); void slotPaste(); void slotPasteEffects(); void slotResizeItemStart(); void slotResizeItemEnd(); void configureNotifications(); void slotInsertTrack(); void slotDeleteTrack(); /** @brief Select all clips in active track. */ void slotSelectTrack(); /** @brief Select all clips in timeline. */ void slotSelectAllTracks(); void slotUnselectAllTracks(); void slotGetNewKeyboardStuff(QComboBox *schemesList); void slotAutoTransition(); void slotRunWizard(); void slotZoneMoved(int start, int end); void slotDvdWizard(const QString &url = QString()); void slotGroupClips(); void slotUnGroupClips(); void slotEditItemDuration(); void slotClipInProjectTree(); // void slotClipToProjectTree(); void slotSplitAV(); void slotSetAudioAlignReference(); void slotAlignAudio(); void slotUpdateClipType(QAction *action); void slotUpdateTimelineView(QAction *action); void slotShowTimeline(bool show); void slotTranscode(const QStringList &urls = QStringList()); void slotTranscodeClip(); /** @brief Archive project: creates a copy of the project file with all clips in a new folder. */ void slotArchiveProject(); void slotSetDocumentRenderProfile(const QMap &props); /** @brief Switches between displaying frames or timecode. * @param ix 0 = display timecode, 1 = display frames. */ void slotUpdateTimecodeFormat(int ix); /** @brief Removes the focus of anything. */ void slotRemoveFocus(); void slotCleanProject(); void slotShutdown(); void slotSwitchMonitors(); void slotSwitchMonitorOverlay(QAction *); void slotSwitchDropFrames(bool drop); void slotSetMonitorGamma(int gamma); void slotCheckRenderStatus(); void slotInsertZoneToTree(); /** @brief The monitor informs that it needs (or not) to have frames sent by the renderer. */ void slotMonitorRequestRenderFrame(bool request); /** @brief Update project because the use of proxy clips was enabled / disabled. */ void slotUpdateProxySettings(); /** @brief Disable proxies for this project. */ void slotDisableProxies(); /** @brief Open the online services search dialog. */ void slotDownloadResources(); /** @brief Process keyframe data sent from a clip to effect / transition stack. */ void slotProcessImportKeyframes(GraphicsRectItem type, const QString &tag, const QString &keyframes); /** @brief Move playhead to mouse cursor position if defined key is pressed */ void slotAlignPlayheadToMousePos(); void slotThemeChanged(const QString &name); /** @brief Close Kdenlive and try to restart it */ void slotRestart(); void triggerKey(QKeyEvent *ev); /** @brief Update monitor overlay actions on monitor switch */ void slotUpdateMonitorOverlays(int id, int code); /** @brief Update widget style */ void slotChangeStyle(QAction *a); /** @brief Create temporary top track to preview an effect */ void createSplitOverlay(Mlt::Filter *filter); void removeSplitOverlay(); /** @brief Create a generator's setup dialog */ void buildGenerator(QAction *action); void slotCheckTabPosition(); /** @brief Toggle automatic timeline preview on/off */ void slotToggleAutoPreview(bool enable); /** @brief Rebuild/reload timeline toolbar. */ void rebuildTimlineToolBar(); void showTimelineToolbarMenu(const QPoint &pos); /** @brief Open Cached Data management dialog. */ void slotManageCache(); void showMenuBar(bool show); /** @brief Change forced icon theme setting (asks for app restart). */ void forceIconSet(bool force); /** @brief Toggle current project's compositing mode. */ void slotUpdateCompositing(QAction *compose); /** @brief Update compositing action to display current project setting. */ void slotUpdateCompositeAction(int mode); /** @brief Cycle through the different timeline trim modes. */ void slotSwitchTrimMode(); void setTrimMode(const QString &mode); /** @brief Set timeline toolbar icon size. */ void setTimelineToolbarIconSize(QAction *a); void slotChangeSpeed(int speed); void updateAction(); /** @brief Request adjust of timeline track height */ void resetTimelineTracks(); /** @brief Set keyboard grabbing on current timeline item */ void slotGrabItem(); signals: Q_SCRIPTABLE void abortRenderJob(const QString &url); void configurationChanged(); void GUISetupDone(); void setPreviewProgress(int); void setRenderProgress(int); void displayMessage(const QString &, MessageType, int); /** @brief Project profile changed, update render widget accordingly. */ void updateRenderWidgetProfile(); /** @brief Clear asset view if itemId is displayed. */ void clearAssetPanel(int itemId = -1); void adjustAssetPanelRange(int itemId, int in, int out); }; #endif diff --git a/src/mltcontroller/clipcontroller.cpp b/src/mltcontroller/clipcontroller.cpp index 7045390bb..a4be629ad 100644 --- a/src/mltcontroller/clipcontroller.cpp +++ b/src/mltcontroller/clipcontroller.cpp @@ -1,879 +1,879 @@ /* Copyright (C) 2012 Till Theato Copyright (C) 2014 Jean-Baptiste Mardelle This file is part of Kdenlive. See www.kdenlive.org. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "clipcontroller.h" #include "bin/model/markerlistmodel.hpp" #include "doc/docundostack.hpp" #include "doc/kdenlivedoc.h" #include "effects/effectstack/model/effectstackmodel.hpp" #include "kdenlivesettings.h" #include "lib/audio/audioStreamInfo.h" #include "profiles/profilemodel.hpp" #include "core.h" #include "kdenlive_debug.h" #include #include #include #include std::shared_ptr ClipController::mediaUnavailable; -ClipController::ClipController(const QString clipId, std::shared_ptr producer) +ClipController::ClipController(const QString &clipId, const std::shared_ptr &producer) : selectedEffectIndex(1) , m_audioThumbCreated(false) , m_masterProducer(producer) , m_properties(producer ? new Mlt::Properties(producer->get_properties()) : nullptr) , m_usesProxy(false) , m_audioInfo(nullptr) , m_audioIndex(0) , m_videoIndex(0) , m_clipType(ClipType::Unknown) , m_hasLimitedDuration(true) , m_effectStack(producer ? EffectStackModel::construct(producer, {ObjectType::BinClip, clipId.toInt()}, pCore->undoStack()) : nullptr) , m_hasAudio(false) , m_hasVideo(false) , m_controllerBinId(clipId) { if (m_masterProducer && !m_masterProducer->is_valid()) { qCDebug(KDENLIVE_LOG) << "// WARNING, USING INVALID PRODUCER"; return; } if (m_masterProducer) { checkAudioVideo(); } if (m_properties) { setProducerProperty(QStringLiteral("kdenlive:id"), m_controllerBinId); m_service = m_properties->get("mlt_service"); QString proxy = m_properties->get("kdenlive:proxy"); QString path = m_properties->get("resource"); if (proxy.length() > 2) { // This is a proxy producer, read original url from kdenlive property path = m_properties->get("kdenlive:originalurl"); if (QFileInfo(path).isRelative()) { path.prepend(pCore->currentDoc()->documentRoot()); } m_usesProxy = true; } else if (m_service != QLatin1String("color") && m_service != QLatin1String("colour") && !path.isEmpty() && QFileInfo(path).isRelative() && path != QLatin1String("")) { path.prepend(pCore->currentDoc()->documentRoot()); } m_path = path.isEmpty() ? QString() : QFileInfo(path).absoluteFilePath(); getInfoForProducer(); } else { m_producerLock.lock(); } } ClipController::~ClipController() { delete m_properties; m_masterProducer.reset(); } const QString ClipController::binId() const { return m_controllerBinId; } const std::unique_ptr &ClipController::audioInfo() const { return m_audioInfo; } void ClipController::addMasterProducer(const std::shared_ptr &producer) { qDebug() << "################### ClipController::addmasterproducer"; QString documentRoot = pCore->currentDoc()->documentRoot(); m_masterProducer = producer; m_properties = new Mlt::Properties(m_masterProducer->get_properties()); int id = m_controllerBinId.toInt(); m_effectStack = EffectStackModel::construct(producer, {ObjectType::BinClip, id}, pCore->undoStack()); if (!m_masterProducer->is_valid()) { m_masterProducer = ClipController::mediaUnavailable; m_producerLock.unlock(); qCDebug(KDENLIVE_LOG) << "// WARNING, USING INVALID PRODUCER"; } else { checkAudioVideo(); m_producerLock.unlock(); QString proxy = m_properties->get("kdenlive:proxy"); m_service = m_properties->get("mlt_service"); QString path = m_properties->get("resource"); m_usesProxy = false; if (proxy.length() > 2) { // This is a proxy producer, read original url from kdenlive property path = m_properties->get("kdenlive:originalurl"); if (QFileInfo(path).isRelative()) { path.prepend(documentRoot); } m_usesProxy = true; } else if (m_service != QLatin1String("color") && m_service != QLatin1String("colour") && !path.isEmpty() && QFileInfo(path).isRelative()) { path.prepend(documentRoot); } m_path = path.isEmpty() ? QString() : QFileInfo(path).absoluteFilePath(); getInfoForProducer(); emitProducerChanged(m_controllerBinId, producer); setProducerProperty(QStringLiteral("kdenlive:id"), m_controllerBinId); } connectEffectStack(); } namespace { QString producerXml(const std::shared_ptr &producer, bool includeMeta) { Mlt::Consumer c(*producer->profile(), "xml", "string"); Mlt::Service s(producer->get_service()); if (!s.is_valid()) { return QString(); } int ignore = s.get_int("ignore_points"); if (ignore != 0) { s.set("ignore_points", 0); } c.set("time_format", "frames"); if (!includeMeta) { c.set("no_meta", 1); } c.set("store", "kdenlive"); c.set("no_root", 1); c.set("root", "/"); c.connect(s); c.start(); if (ignore != 0) { s.set("ignore_points", ignore); } return QString::fromUtf8(c.get("string")); } } // namespace void ClipController::getProducerXML(QDomDocument &document, bool includeMeta) { // TODO refac this is a probable duplicate with Clip::xml if (m_masterProducer) { QString xml = producerXml(m_masterProducer, includeMeta); document.setContent(xml); } else { qCDebug(KDENLIVE_LOG) << " + + ++ NO MASTER PROD"; } } void ClipController::getInfoForProducer() { date = QFileInfo(m_path).lastModified(); m_audioIndex = -1; m_videoIndex = -1; // special case: playlist with a proxy clip have to be detected separately if (m_usesProxy && m_path.endsWith(QStringLiteral(".mlt"))) { m_clipType = ClipType::Playlist; } else if (m_service == QLatin1String("avformat") || m_service == QLatin1String("avformat-novalidate")) { m_audioIndex = getProducerIntProperty(QStringLiteral("audio_index")); m_videoIndex = getProducerIntProperty(QStringLiteral("video_index")); if (m_audioIndex == -1) { m_clipType = ClipType::Video; } else if (m_videoIndex == -1) { m_clipType = ClipType::Audio; } else { m_clipType = ClipType::AV; } } else if (m_service == QLatin1String("qimage") || m_service == QLatin1String("pixbuf")) { if (m_path.contains(QLatin1Char('%')) || m_path.contains(QStringLiteral("/.all."))) { m_clipType = ClipType::SlideShow; m_hasLimitedDuration = true; } else { m_clipType = ClipType::Image; m_hasLimitedDuration = false; } } else if (m_service == QLatin1String("colour") || m_service == QLatin1String("color")) { m_clipType = ClipType::Color; m_hasLimitedDuration = false; } else if (m_service == QLatin1String("kdenlivetitle")) { if (!m_path.isEmpty()) { m_clipType = ClipType::TextTemplate; } else { m_clipType = ClipType::Text; } m_hasLimitedDuration = false; } else if (m_service == QLatin1String("xml") || m_service == QLatin1String("consumer")) { m_clipType = ClipType::Playlist; } else if (m_service == QLatin1String("webvfx")) { m_clipType = ClipType::WebVfx; } else if (m_service == QLatin1String("qtext")) { m_clipType = ClipType::QText; } else if (m_service == QLatin1String("blipflash")) { // Mostly used for testing m_clipType = ClipType::AV; m_hasLimitedDuration = true; } else { m_clipType = ClipType::Unknown; } if (m_audioIndex > -1 || m_clipType == ClipType::Playlist) { m_audioInfo.reset(new AudioStreamInfo(m_masterProducer, m_audioIndex)); } if (!m_hasLimitedDuration) { int playtime = m_masterProducer->time_to_frames(m_masterProducer->get("kdenlive:duration")); if (playtime <= 0) { // Fix clips having missing kdenlive:duration m_masterProducer->set("kdenlive:duration", m_masterProducer->frames_to_time(m_masterProducer->get_playtime(), mlt_time_clock)); m_masterProducer->set("out", m_masterProducer->frames_to_time(m_masterProducer->get_length() - 1, mlt_time_clock)); } } } bool ClipController::hasLimitedDuration() const { return m_hasLimitedDuration; } void ClipController::forceLimitedDuration() { m_hasLimitedDuration = true; } std::shared_ptr ClipController::originalProducer() { QMutexLocker lock(&m_producerLock); return m_masterProducer; } Mlt::Producer *ClipController::masterProducer() { return new Mlt::Producer(*m_masterProducer); } bool ClipController::isValid() { if (m_masterProducer == nullptr) { return false; } return m_masterProducer->is_valid(); } // static const char *ClipController::getPassPropertiesList(bool passLength) { if (!passLength) { return "kdenlive:proxy,kdenlive:originalurl,force_aspect_num,force_aspect_den,force_aspect_ratio,force_fps,force_progressive,force_tff,threads,force_" "colorspace,set.force_full_luma,file_hash,autorotate,xmldata,video_index,audio_index,set.test_image,set.test_audio"; } return "kdenlive:proxy,kdenlive:originalurl,force_aspect_num,force_aspect_den,force_aspect_ratio,force_fps,force_progressive,force_tff,threads,force_" "colorspace,set.force_full_luma,templatetext,file_hash,autorotate,xmldata,length,video_index,audio_index,set.test_image,set.test_audio"; } QMap ClipController::getPropertiesFromPrefix(const QString &prefix, bool withPrefix) { Mlt::Properties subProperties; subProperties.pass_values(*m_properties, prefix.toUtf8().constData()); QMap subclipsData; for (int i = 0; i < subProperties.count(); i++) { subclipsData.insert(withPrefix ? QString(prefix + subProperties.get_name(i)) : subProperties.get_name(i), subProperties.get(i)); } return subclipsData; } void ClipController::updateProducer(const std::shared_ptr &producer) { qDebug() << "################### ClipController::updateProducer"; // TODO replace all track producers if (!m_properties) { // producer has not been initialized return addMasterProducer(producer); } Mlt::Properties passProperties; // Keep track of necessary properties QString proxy = producer->get("kdenlive:proxy"); if (proxy.length() > 2) { // This is a proxy producer, read original url from kdenlive property m_usesProxy = true; } else { m_usesProxy = false; } // This is necessary as some properties like set.test_audio are reset on producer creation const char *passList = getPassPropertiesList(m_usesProxy); passProperties.pass_list(*m_properties, passList); delete m_properties; *m_masterProducer = producer.get(); checkAudioVideo(); m_properties = new Mlt::Properties(m_masterProducer->get_properties()); // Pass properties from previous producer m_properties->pass_list(passProperties, passList); if (!m_masterProducer->is_valid()) { qCDebug(KDENLIVE_LOG) << "// WARNING, USING INVALID PRODUCER"; } else { m_effectStack->resetService(m_masterProducer); emitProducerChanged(m_controllerBinId, producer); // URL and name should not be updated otherwise when proxying a clip we cannot find back the original url /*m_url = QUrl::fromLocalFile(m_masterProducer->get("resource")); if (m_url.isValid()) { m_name = m_url.fileName(); } */ } m_producerLock.unlock(); qDebug() << "// replace finished: " << binId() << " : " << m_masterProducer->get("resource"); } const QString ClipController::getStringDuration() { if (m_masterProducer) { int playtime = m_masterProducer->time_to_frames(m_masterProducer->get("kdenlive:duration")); if (playtime > 0) { return QString(m_properties->frames_to_time(playtime, mlt_time_smpte_df)); } return m_masterProducer->get_length_time(mlt_time_smpte_df); } return i18n("Unknown"); } int ClipController::getProducerDuration() const { if (m_masterProducer) { int playtime = m_masterProducer->time_to_frames(m_masterProducer->get("kdenlive:duration")); if (playtime <= 0) { return playtime = m_masterProducer->get_length(); } return playtime; } return -1; } char *ClipController::framesToTime(int frames) const { if (m_masterProducer) { return m_masterProducer->frames_to_time(frames, mlt_time_clock); } return nullptr; } GenTime ClipController::getPlaytime() const { if (!m_masterProducer || !m_masterProducer->is_valid()) { return GenTime(); } double fps = pCore->getCurrentFps(); if (!m_hasLimitedDuration) { int playtime = m_masterProducer->time_to_frames(m_masterProducer->get("kdenlive:duration")); return GenTime(playtime == 0 ? m_masterProducer->get_playtime() : playtime, fps); } return GenTime(m_masterProducer->get_playtime(), fps); } int ClipController::getFramePlaytime() const { if (!m_masterProducer || !m_masterProducer->is_valid()) { return 0; } if (!m_hasLimitedDuration) { int playtime = m_masterProducer->time_to_frames(m_masterProducer->get("kdenlive:duration")); return playtime == 0 ? m_masterProducer->get_playtime() : playtime; } return m_masterProducer->get_playtime(); } QString ClipController::getProducerProperty(const QString &name) const { if (!m_properties) { return QString(); } if (m_usesProxy && name.startsWith(QLatin1String("meta."))) { QString correctedName = QStringLiteral("kdenlive:") + name; return m_properties->get(correctedName.toUtf8().constData()); } return QString(m_properties->get(name.toUtf8().constData())); } int ClipController::getProducerIntProperty(const QString &name) const { if (!m_properties) { return 0; } if (m_usesProxy && name.startsWith(QLatin1String("meta."))) { QString correctedName = QStringLiteral("kdenlive:") + name; return m_properties->get_int(correctedName.toUtf8().constData()); } return m_properties->get_int(name.toUtf8().constData()); } qint64 ClipController::getProducerInt64Property(const QString &name) const { if (!m_properties) { return 0; } return m_properties->get_int64(name.toUtf8().constData()); } double ClipController::getProducerDoubleProperty(const QString &name) const { if (!m_properties) { return 0; } return m_properties->get_double(name.toUtf8().constData()); } QColor ClipController::getProducerColorProperty(const QString &name) const { if (!m_properties) { return QColor(); } mlt_color color = m_properties->get_color(name.toUtf8().constData()); return QColor::fromRgb(color.r, color.g, color.b); } QMap ClipController::currentProperties(const QMap &props) { QMap currentProps; QMap::const_iterator i = props.constBegin(); while (i != props.constEnd()) { currentProps.insert(i.key(), getProducerProperty(i.key())); ++i; } return currentProps; } double ClipController::originalFps() const { if (!m_properties) { return 0; } QString propertyName = QStringLiteral("meta.media.%1.stream.frame_rate").arg(m_videoIndex); return m_properties->get_double(propertyName.toUtf8().constData()); } QString ClipController::videoCodecProperty(const QString &property) const { if (!m_properties) { return QString(); } QString propertyName = QStringLiteral("meta.media.%1.codec.%2").arg(m_videoIndex).arg(property); return m_properties->get(propertyName.toUtf8().constData()); } const QString ClipController::codec(bool audioCodec) const { if ((m_properties == nullptr) || (m_clipType != ClipType::AV && m_clipType != ClipType::Video && m_clipType != ClipType::Audio)) { return QString(); } QString propertyName = QStringLiteral("meta.media.%1.codec.name").arg(audioCodec ? m_audioIndex : m_videoIndex); return m_properties->get(propertyName.toUtf8().constData()); } const QString ClipController::clipUrl() const { return m_path; } QString ClipController::clipName() const { QString name = getProducerProperty(QStringLiteral("kdenlive:clipname")); if (!name.isEmpty()) { return name; } return QFileInfo(m_path).fileName(); } QString ClipController::description() const { if (m_clipType == ClipType::TextTemplate) { QString name = getProducerProperty(QStringLiteral("templatetext")); return name; } QString name = getProducerProperty(QStringLiteral("kdenlive:description")); if (!name.isEmpty()) { return name; } return getProducerProperty(QStringLiteral("meta.attr.comment.markup")); } QString ClipController::serviceName() const { return m_service; } void ClipController::setProducerProperty(const QString &name, int value) { if (!m_masterProducer) return; // TODO: also set property on all track producers m_masterProducer->parent().set(name.toUtf8().constData(), value); } void ClipController::setProducerProperty(const QString &name, double value) { if (!m_masterProducer) return; // TODO: also set property on all track producers m_masterProducer->parent().set(name.toUtf8().constData(), value); } void ClipController::setProducerProperty(const QString &name, const QString &value) { if (!m_masterProducer) return; // TODO: also set property on all track producers if (value.isEmpty()) { m_masterProducer->parent().set(name.toUtf8().constData(), (char *)nullptr); } else { m_masterProducer->parent().set(name.toUtf8().constData(), value.toUtf8().constData()); } } void ClipController::resetProducerProperty(const QString &name) { // TODO: also set property on all track producers m_masterProducer->parent().set(name.toUtf8().constData(), (char *)nullptr); } ClipType::ProducerType ClipController::clipType() const { return m_clipType; } const QSize ClipController::getFrameSize() const { if (m_masterProducer == nullptr) { return QSize(); } int width = m_masterProducer->get_int("meta.media.width"); if (width == 0) { width = m_masterProducer->get_int("width"); } int height = m_masterProducer->get_int("meta.media.height"); if (height == 0) { height = m_masterProducer->get_int("height"); } return QSize(width, height); } bool ClipController::hasAudio() const { return m_hasAudio; } void ClipController::checkAudioVideo() { m_masterProducer->seek(0); if (m_masterProducer->get_int("_placeholder") == 1 || m_masterProducer->get("text") == QLatin1String("INVALID")) { // This is a placeholder file, try to guess from its properties QString orig_service = m_masterProducer->get("kdenlive:orig_service"); if (orig_service.startsWith(QStringLiteral("avformat")) || (m_masterProducer->get_int("audio_index") + m_masterProducer->get_int("video_index") > 0)) { m_hasAudio = m_masterProducer->get_int("audio_index") >= 0; m_hasVideo = m_masterProducer->get_int("video_index") >= 0; } else { // Assume image or text producer m_hasAudio = false; m_hasVideo = true; } return; } QScopedPointer frame(m_masterProducer->get_frame()); // test_audio returns 1 if there is NO audio (strange but true at the time this code is written) m_hasAudio = frame->get_int("test_audio") == 0; m_hasVideo = frame->get_int("test_image") == 0; } bool ClipController::hasVideo() const { return m_hasVideo; } PlaylistState::ClipState ClipController::defaultState() const { if (hasVideo()) { return PlaylistState::VideoOnly; } if (hasAudio()) { return PlaylistState::AudioOnly; } return PlaylistState::Disabled; } QPixmap ClipController::pixmap(int framePosition, int width, int height) { // TODO refac this should use the new thumb infrastructure m_masterProducer->seek(framePosition); Mlt::Frame *frame = m_masterProducer->get_frame(); if (frame == nullptr || !frame->is_valid()) { QPixmap p(width, height); p.fill(QColor(Qt::red).rgb()); return p; } frame->set("rescale.interp", "bilinear"); frame->set("deinterlace_method", "onefield"); frame->set("top_field_first", -1); if (width == 0) { width = m_masterProducer->get_int("meta.media.width"); if (width == 0) { width = m_masterProducer->get_int("width"); } } if (height == 0) { height = m_masterProducer->get_int("meta.media.height"); if (height == 0) { height = m_masterProducer->get_int("height"); } } // int ow = frameWidth; // int oh = height; mlt_image_format format = mlt_image_rgb24a; width += width % 2; height += height % 2; const uchar *imagedata = frame->get_image(format, width, height); QImage image(imagedata, width, height, QImage::Format_RGBA8888); QPixmap pixmap; pixmap.convertFromImage(image); delete frame; return pixmap; } void ClipController::setZone(const QPoint &zone) { setProducerProperty(QStringLiteral("kdenlive:zone_in"), zone.x()); setProducerProperty(QStringLiteral("kdenlive:zone_out"), zone.y()); } QPoint ClipController::zone() const { int in = getProducerIntProperty(QStringLiteral("kdenlive:zone_in")); int max = getFramePlaytime() - 1; int out = qMin(getProducerIntProperty(QStringLiteral("kdenlive:zone_out")), max); if (out <= in) { out = max; } QPoint zone(in, out); return zone; } const QString ClipController::getClipHash() const { return getProducerProperty(QStringLiteral("kdenlive:file_hash")); } Mlt::Properties &ClipController::properties() { return *m_properties; } void ClipController::mirrorOriginalProperties(Mlt::Properties &props) { if (m_usesProxy && QFileInfo(m_properties->get("resource")).fileName() == QFileInfo(m_properties->get("kdenlive:proxy")).fileName()) { // We have a proxy clip, load original source producer std::shared_ptr prod = std::make_shared(pCore->getCurrentProfile()->profile(), nullptr, m_path.toUtf8().constData()); // Get frame to make sure we retrieve all original props std::shared_ptr fr(prod->get_frame()); if (!prod->is_valid()) { return; } Mlt::Properties sourceProps(prod->get_properties()); props.inherit(sourceProps); } else { if (m_clipType == ClipType::AV || m_clipType == ClipType::Video || m_clipType == ClipType::Audio) { // Make sure that a frame / image was fetched to initialize all meta properties QString progressive = m_properties->get("meta.media.progressive"); if (progressive.isEmpty()) { // Fetch a frame to initialize required properties QScopedPointer tmpProd(nullptr); if (KdenliveSettings::gpu_accel()) { QString service = m_masterProducer->get("mlt_service"); tmpProd.reset(new Mlt::Producer(pCore->getCurrentProfile()->profile(), service.toUtf8().constData(), m_masterProducer->get("resource"))); } std::shared_ptr fr(tmpProd ? tmpProd->get_frame() : m_masterProducer->get_frame()); mlt_image_format format = mlt_image_none; int width = 0; int height = 0; fr->get_image(format, width, height); } } props.inherit(*m_properties); } } void ClipController::addEffect(QDomElement &xml) { Q_UNUSED(xml) // TODO refac: this must be rewritten /* QMutexLocker lock(&m_effectMutex); Mlt::Service service = m_masterProducer->parent(); ItemInfo info; info.cropStart = GenTime(); info.cropDuration = getPlaytime(); EffectsList eff = effectList(); EffectsController::initEffect(info, eff, getProducerProperty(QStringLiteral("kdenlive:proxy")), xml); // Add effect to list and setup a kdenlive_ix value int kdenlive_ix = 0; for (int i = 0; i < service.filter_count(); ++i) { QScopedPointer effect(service.filter(i)); int ix = effect->get_int("kdenlive_ix"); if (ix > kdenlive_ix) { kdenlive_ix = ix; } } kdenlive_ix++; xml.setAttribute(QStringLiteral("kdenlive_ix"), kdenlive_ix); EffectsParameterList params = EffectsController::getEffectArgs(xml); EffectManager effect(service); effect.addEffect(params, getPlaytime().frames(pCore->getCurrentFps())); if (auto ptr = m_binController.lock()) ptr->updateTrackProducer(m_controllerBinId); */ } void ClipController::removeEffect(int effectIndex, bool delayRefresh) { Q_UNUSED(effectIndex) Q_UNUSED(delayRefresh) // TODO refac: this must be rewritten /* QMutexLocker lock(&m_effectMutex); Mlt::Service service(m_masterProducer->parent()); EffectManager effect(service); effect.removeEffect(effectIndex, true); if (!delayRefresh) { if (auto ptr = m_binController.lock()) ptr->updateTrackProducer(m_controllerBinId); } */ } void ClipController::moveEffect(int oldPos, int newPos) { Q_UNUSED(oldPos) Q_UNUSED(newPos) // TODO refac: this must be rewritten /* QMutexLocker lock(&m_effectMutex); Mlt::Service service(m_masterProducer->parent()); EffectManager effect(service); effect.moveEffect(oldPos, newPos); */ } int ClipController::effectsCount() { int count = 0; Mlt::Service service(m_masterProducer->parent()); for (int ix = 0; ix < service.filter_count(); ++ix) { QScopedPointer effect(service.filter(ix)); QString id = effect->get("kdenlive_id"); if (!id.isEmpty()) { count++; } } return count; } void ClipController::changeEffectState(const QList &indexes, bool disable) { Q_UNUSED(indexes) Q_UNUSED(disable) // TODO refac : this must be rewritten /* Mlt::Service service = m_masterProducer->parent(); for (int i = 0; i < service.filter_count(); ++i) { QScopedPointer effect(service.filter(i)); if ((effect != nullptr) && effect->is_valid() && indexes.contains(effect->get_int("kdenlive_ix"))) { effect->set("disable", (int)disable); } } if (auto ptr = m_binController.lock()) ptr->updateTrackProducer(m_controllerBinId); */ } void ClipController::updateEffect(const QDomElement &e, int ix) { Q_UNUSED(e) Q_UNUSED(ix) // TODO refac : this must be rewritten /* QString tag = e.attribute(QStringLiteral("id")); if (tag == QLatin1String("autotrack_rectangle") || tag.startsWith(QLatin1String("ladspa")) || tag == QLatin1String("sox")) { // this filters cannot be edited, remove and re-add it removeEffect(ix, true); QDomElement clone = e.cloneNode().toElement(); addEffect(clone); return; } EffectsParameterList params = EffectsController::getEffectArgs(e); Mlt::Service service = m_masterProducer->parent(); for (int i = 0; i < service.filter_count(); ++i) { QScopedPointer effect(service.filter(i)); if (!effect || !effect->is_valid() || effect->get_int("kdenlive_ix") != ix) { continue; } service.lock(); QString prefix; QString ser = effect->get("mlt_service"); if (ser == QLatin1String("region")) { prefix = QStringLiteral("filter0."); } for (int j = 0; j < params.count(); ++j) { effect->set((prefix + params.at(j).name()).toUtf8().constData(), params.at(j).value().toUtf8().constData()); // qCDebug(KDENLIVE_LOG)<updateTrackProducer(m_controllerBinId); // slotRefreshTracks(); */ } bool ClipController::hasEffects() const { return m_effectStack->rowCount() > 0; } void ClipController::setBinEffectsEnabled(bool enabled) { m_effectStack->setEffectStackEnabled(enabled); } void ClipController::saveZone(QPoint zone, const QDir &dir) { QString path = QString(clipName() + QLatin1Char('_') + QString::number(zone.x()) + QStringLiteral(".mlt")); if (dir.exists(path)) { // TODO ask for overwrite } Mlt::Consumer xmlConsumer(pCore->getCurrentProfile()->profile(), ("xml:" + dir.absoluteFilePath(path)).toUtf8().constData()); xmlConsumer.set("terminate_on_pause", 1); Mlt::Producer prod(m_masterProducer->get_producer()); Mlt::Producer *prod2 = prod.cut(zone.x(), zone.y()); Mlt::Playlist list(pCore->getCurrentProfile()->profile()); list.insert_at(0, *prod2, 0); // list.set("title", desc.toUtf8().constData()); xmlConsumer.connect(list); xmlConsumer.run(); delete prod2; } std::shared_ptr ClipController::getEffectStack() const { return m_effectStack; } void ClipController::addEffect(const QString &effectId) { m_effectStack->appendEffect(effectId, true); } -bool ClipController::copyEffect(std::shared_ptr stackModel, int rowId) +bool ClipController::copyEffect(const std::shared_ptr &stackModel, int rowId) { m_effectStack->copyEffect(stackModel->getEffectStackRow(rowId), !m_hasAudio ? PlaylistState::VideoOnly : !m_hasVideo ? PlaylistState::AudioOnly : PlaylistState::Disabled); return true; } std::shared_ptr ClipController::getMarkerModel() const { return m_markerModel; } diff --git a/src/mltcontroller/clipcontroller.h b/src/mltcontroller/clipcontroller.h index d68ab0db3..5a192e887 100644 --- a/src/mltcontroller/clipcontroller.h +++ b/src/mltcontroller/clipcontroller.h @@ -1,234 +1,234 @@ /* Copyright (C) 2012 Till Theato Copyright (C) 2014 Jean-Baptiste Mardelle This file is part of Kdenlive. See www.kdenlive.org. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef CLIPCONTROLLER_H #define CLIPCONTROLLER_H #include "definitions.h" #include #include #include #include #include #include #include class QPixmap; class Bin; class AudioStreamInfo; class EffectStackModel; class MarkerListModel; /** * @class ClipController * @brief Provides a convenience wrapper around the project Bin clip producers. * It also holds a QList of track producers for the 'master' producer in case we * need to update or replace them */ class ClipController { public: friend class Bin; /** * @brief Constructor. The constructor is protected because you should call the static Construct instead * @param bincontroller reference to the bincontroller * @param producer producer to create reference to */ - explicit ClipController(const QString id, std::shared_ptr producer = nullptr); + explicit ClipController(const QString &id, const std::shared_ptr &producer = nullptr); public: virtual ~ClipController(); QMutex producerMutex; /** @brief Returns true if the master producer is valid */ bool isValid(); /** @brief Stores the file's creation time */ QDateTime date; /** @brief Replaces the master producer and (TODO) the track producers with an updated producer, for example a proxy */ void updateProducer(const std::shared_ptr &producer); void getProducerXML(QDomDocument &document, bool includeMeta = false); /** @brief Returns a clone of our master producer. Delete after use! */ Mlt::Producer *masterProducer(); /** @brief Returns the clip name (usually file name) */ QString clipName() const; /** @brief Returns the clip's description or metadata comment */ QString description() const; /** @brief Returns the clip's MLT resource */ const QString clipUrl() const; /** @brief Returns the clip's type as defined in definitions.h */ ClipType::ProducerType clipType() const; /** @brief Returns the MLT's producer id */ const QString binId() const; /** @brief Returns the clip's duration */ GenTime getPlaytime() const; int getFramePlaytime() const; /** * @brief Sets a property. * @param name name of the property * @param value the new value */ void setProducerProperty(const QString &name, const QString &value); void setProducerProperty(const QString &name, int value); void setProducerProperty(const QString &name, double value); /** @brief Reset a property on the MLT producer (=delete the property). */ void resetProducerProperty(const QString &name); /** * @brief Returns the list of all properties starting with prefix. For subclips, the list is of this type: * { subclip name , subclip in/out } where the subclip in/ou value is a semi-colon separated in/out value, like "25;220" */ QMap getPropertiesFromPrefix(const QString &prefix, bool withPrefix = false); /** * @brief Returns the value of a property. * @param name name o the property */ QMap currentProperties(const QMap &props); QString getProducerProperty(const QString &key) const; int getProducerIntProperty(const QString &key) const; qint64 getProducerInt64Property(const QString &key) const; QColor getProducerColorProperty(const QString &key) const; double getProducerDoubleProperty(const QString &key) const; double originalFps() const; QString videoCodecProperty(const QString &property) const; const QString codec(bool audioCodec) const; const QString getClipHash() const; const QSize getFrameSize() const; /** @brief Returns the clip duration as a string like 00:00:02:01. */ const QString getStringDuration(); int getProducerDuration() const; char *framesToTime(int frames) const; /** * @brief Returns a pixmap created from a frame of the producer. * @param position frame position * @param width width of the pixmap (only a guidance) * @param height height of the pixmap (only a guidance) */ QPixmap pixmap(int position = 0, int width = 0, int height = 0); /** @brief Returns the MLT producer's service. */ QString serviceName() const; /** @brief Returns the original master producer. */ std::shared_ptr originalProducer(); /** @brief Holds index of currently selected master clip effect. */ int selectedEffectIndex; /** @brief Sets the master producer for this clip when we build the controller without master clip. */ void addMasterProducer(const std::shared_ptr &producer); /* @brief Returns the marker model associated with this clip */ std::shared_ptr getMarkerModel() const; void setZone(const QPoint &zone); QPoint zone() const; bool hasLimitedDuration() const; void forceLimitedDuration(); Mlt::Properties &properties(); void mirrorOriginalProperties(Mlt::Properties &props); void addEffect(QDomElement &xml); - bool copyEffect(std::shared_ptr stackModel, int rowId); + bool copyEffect(const std::shared_ptr &stackModel, int rowId); void removeEffect(int effectIndex, bool delayRefresh = false); /** @brief Enable/disable an effect. */ void changeEffectState(const QList &indexes, bool disable); void updateEffect(const QDomElement &e, int ix); /** @brief Returns true if the bin clip has effects */ bool hasEffects() const; /** @brief Returns true if the clip contains at least one audio stream */ bool hasAudio() const; /** @brief Returns true if the clip contains at least one video stream */ bool hasVideo() const; /** @brief Returns the default state a clip should be in. If the clips contains both video and audio, this defaults to video */ PlaylistState::ClipState defaultState() const; /** @brief Returns info about clip audio */ const std::unique_ptr &audioInfo() const; /** @brief Returns true if audio thumbnails for this clip are cached */ bool m_audioThumbCreated; /** @brief When replacing a producer, it is important that we keep some properties, for example force_ stuff and url for proxies * this method returns a list of properties that we want to keep when replacing a producer . */ static const char *getPassPropertiesList(bool passLength = true); /** @brief Disable all Kdenlive effects on this clip */ void setBinEffectsEnabled(bool enabled); /** @brief Returns the number of Kdenlive added effects for this bin clip */ int effectsCount(); /** @brief Move an effect in stack for this bin clip */ void moveEffect(int oldPos, int newPos); /** @brief Save an xml playlist of current clip with in/out points as zone.x()/y() */ void saveZone(QPoint zone, const QDir &dir); /* @brief This is the producer that serves as a placeholder while a clip is being loaded. It is created in Core at startup */ static std::shared_ptr mediaUnavailable; /** @brief Returns a ptr to the effetstack associated with this element */ std::shared_ptr getEffectStack() const; /** @brief Append an effect to this producer's effect list */ void addEffect(const QString &effectId); protected: virtual void emitProducerChanged(const QString &, const std::shared_ptr &){}; virtual void connectEffectStack(){}; // This is the helper function that checks if the clip has audio and video and stores the result void checkAudioVideo(); std::shared_ptr m_masterProducer; Mlt::Properties *m_properties; bool m_usesProxy; std::unique_ptr m_audioInfo; QString m_service; QString m_path; int m_audioIndex; int m_videoIndex; ClipType::ProducerType m_clipType; bool m_hasLimitedDuration; QMutex m_effectMutex; void getInfoForProducer(); // void rebuildEffectList(ProfileInfo info); std::shared_ptr m_effectStack; std::shared_ptr m_markerModel; bool m_hasAudio; bool m_hasVideo; private: QMutex m_producerLock; QString m_controllerBinId; }; #endif diff --git a/src/mltcontroller/clippropertiescontroller.cpp b/src/mltcontroller/clippropertiescontroller.cpp index 3111b8cb0..0b302a1f6 100644 --- a/src/mltcontroller/clippropertiescontroller.cpp +++ b/src/mltcontroller/clippropertiescontroller.cpp @@ -1,1337 +1,1337 @@ /* Copyright (C) 2015 Jean-Baptiste Mardelle This file is part of Kdenlive. See www.kdenlive.org. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "clippropertiescontroller.h" #include "bin/model/markerlistmodel.hpp" #include "clipcontroller.h" #include "core.h" #include "dialogs/profilesdialog.h" #include "doc/kdenlivedoc.h" #include "kdenlivesettings.h" #include "profiles/profilerepository.hpp" #include "project/projectmanager.h" #include "timecodedisplay.h" #include "widgets/choosecolorwidget.h" #include #include #ifdef KF5_USE_FILEMETADATA #include #include #include #include #endif #include #include "kdenlive_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include AnalysisTree::AnalysisTree(QWidget *parent) : QTreeWidget(parent) { setRootIsDecorated(false); setColumnCount(2); setAlternatingRowColors(true); setHeaderHidden(true); setDragEnabled(true); } // virtual QMimeData *AnalysisTree::mimeData(const QList list) const { QString mimeData; for (QTreeWidgetItem *item : list) { if ((item->flags() & Qt::ItemIsDragEnabled) != 0) { mimeData.append(item->text(1)); } } auto *mime = new QMimeData; mime->setData(QStringLiteral("kdenlive/geometry"), mimeData.toUtf8()); return mime; } #ifdef KF5_USE_FILEMETADATA class ExtractionResult : public KFileMetaData::ExtractionResult { public: ExtractionResult(const QString &filename, const QString &mimetype, QTreeWidget *tree) : KFileMetaData::ExtractionResult(filename, mimetype, KFileMetaData::ExtractionResult::ExtractMetaData) , m_tree(tree) { } void append(const QString & /*text*/) override {} void addType(KFileMetaData::Type::Type /*type*/) override {} void add(KFileMetaData::Property::Property property, const QVariant &value) override { bool decode = false; switch (property) { case KFileMetaData::Property::ImageMake: case KFileMetaData::Property::ImageModel: case KFileMetaData::Property::ImageDateTime: case KFileMetaData::Property::BitRate: case KFileMetaData::Property::TrackNumber: case KFileMetaData::Property::ReleaseYear: case KFileMetaData::Property::Composer: case KFileMetaData::Property::Genre: case KFileMetaData::Property::Artist: case KFileMetaData::Property::Album: case KFileMetaData::Property::Title: case KFileMetaData::Property::Comment: case KFileMetaData::Property::Copyright: case KFileMetaData::Property::PhotoFocalLength: case KFileMetaData::Property::PhotoExposureTime: case KFileMetaData::Property::PhotoFNumber: case KFileMetaData::Property::PhotoApertureValue: case KFileMetaData::Property::PhotoWhiteBalance: case KFileMetaData::Property::PhotoGpsLatitude: case KFileMetaData::Property::PhotoGpsLongitude: decode = true; break; default: break; } if (decode) { KFileMetaData::PropertyInfo info(property); if (info.valueType() == QVariant::DateTime) { new QTreeWidgetItem(m_tree, QStringList() << info.displayName() << value.toDateTime().toString(Qt::DefaultLocaleShortDate)); } else if (info.valueType() == QVariant::Int) { int val = value.toInt(); if (property == KFileMetaData::Property::BitRate) { // Adjust unit for bitrate new QTreeWidgetItem(m_tree, QStringList() << info.displayName() << QString::number(val / 1000) + QLatin1Char(' ') + i18nc("Kilobytes per seconds", "kb/s")); } else { new QTreeWidgetItem(m_tree, QStringList() << info.displayName() << QString::number(val)); } } else if (info.valueType() == QVariant::Double) { new QTreeWidgetItem(m_tree, QStringList() << info.displayName() << QString::number(value.toDouble())); } else { new QTreeWidgetItem(m_tree, QStringList() << info.displayName() << value.toString()); } } } private: QTreeWidget *m_tree; }; #endif ClipPropertiesController::ClipPropertiesController(ClipController *controller, QWidget *parent) : QWidget(parent) , m_controller(controller) , m_tc(Timecode(Timecode::HH_MM_SS_HH, pCore->getCurrentFps())) , m_id(controller->binId()) , m_type(controller->clipType()) , m_properties(new Mlt::Properties(controller->properties())) , m_textEdit(nullptr) { m_controller->mirrorOriginalProperties(m_sourceProperties); setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)); setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); auto *lay = new QVBoxLayout; lay->setContentsMargins(0, 0, 0, 0); m_clipLabel = new QLabel(controller->clipName()); lay->addWidget(m_clipLabel); m_tabWidget = new QTabWidget(this); lay->addWidget(m_tabWidget); setLayout(lay); m_tabWidget->setDocumentMode(true); m_tabWidget->setTabPosition(QTabWidget::East); QScrollArea *forcePage = new QScrollArea(this); m_propertiesPage = new QWidget(this); m_markersPage = new QWidget(this); m_metaPage = new QWidget(this); m_analysisPage = new QWidget(this); // Clip properties auto *propsBox = new QVBoxLayout; m_propertiesTree = new QTreeWidget(this); m_propertiesTree->setRootIsDecorated(false); m_propertiesTree->setColumnCount(2); m_propertiesTree->setAlternatingRowColors(true); m_propertiesTree->sortByColumn(0, Qt::AscendingOrder); m_propertiesTree->setHeaderHidden(true); propsBox->addWidget(m_propertiesTree); fillProperties(); m_propertiesPage->setLayout(propsBox); // Clip markers auto *mBox = new QVBoxLayout; m_markerTree = new QTreeView; m_markerTree->setRootIsDecorated(false); m_markerTree->setAlternatingRowColors(true); m_markerTree->setHeaderHidden(true); m_markerTree->setSelectionMode(QAbstractItemView::ExtendedSelection); m_markerTree->setModel(controller->getMarkerModel().get()); mBox->addWidget(m_markerTree); auto *bar = new QToolBar; bar->addAction(QIcon::fromTheme(QStringLiteral("document-new")), i18n("Add marker"), this, SLOT(slotAddMarker())); bar->addAction(QIcon::fromTheme(QStringLiteral("trash-empty")), i18n("Delete marker"), this, SLOT(slotDeleteMarker())); bar->addAction(QIcon::fromTheme(QStringLiteral("document-edit")), i18n("Edit marker"), this, SLOT(slotEditMarker())); bar->addAction(QIcon::fromTheme(QStringLiteral("document-save-as")), i18n("Export markers"), this, SLOT(slotSaveMarkers())); bar->addAction(QIcon::fromTheme(QStringLiteral("document-open")), i18n("Import markers"), this, SLOT(slotLoadMarkers())); mBox->addWidget(bar); m_markersPage->setLayout(mBox); connect(m_markerTree, &QAbstractItemView::doubleClicked, this, &ClipPropertiesController::slotSeekToMarker); // metadata auto *m2Box = new QVBoxLayout; auto *metaTree = new QTreeWidget; metaTree->setRootIsDecorated(true); metaTree->setColumnCount(2); metaTree->setAlternatingRowColors(true); metaTree->setHeaderHidden(true); m2Box->addWidget(metaTree); slotFillMeta(metaTree); m_metaPage->setLayout(m2Box); // Clip analysis auto *aBox = new QVBoxLayout; m_analysisTree = new AnalysisTree(this); aBox->addWidget(new QLabel(i18n("Analysis data"))); aBox->addWidget(m_analysisTree); auto *bar2 = new QToolBar; bar2->addAction(QIcon::fromTheme(QStringLiteral("trash-empty")), i18n("Delete analysis"), this, SLOT(slotDeleteAnalysis())); bar2->addAction(QIcon::fromTheme(QStringLiteral("document-save-as")), i18n("Export analysis"), this, SLOT(slotSaveAnalysis())); bar2->addAction(QIcon::fromTheme(QStringLiteral("document-open")), i18n("Import analysis"), this, SLOT(slotLoadAnalysis())); aBox->addWidget(bar2); slotFillAnalysisData(); m_analysisPage->setLayout(aBox); // Force properties auto *vbox = new QVBoxLayout; vbox->setSpacing(0); if (m_type == ClipType::Text || m_type == ClipType::SlideShow || m_type == ClipType::TextTemplate) { QPushButton *editButton = new QPushButton(i18n("Edit Clip"), this); connect(editButton, &QAbstractButton::clicked, this, &ClipPropertiesController::editClip); vbox->addWidget(editButton); } if (m_type == ClipType::Color || m_type == ClipType::Image || m_type == ClipType::AV || m_type == ClipType::Video || m_type == ClipType::TextTemplate) { // Edit duration widget m_originalProperties.insert(QStringLiteral("out"), m_properties->get("out")); int kdenlive_length = m_properties->time_to_frames(m_properties->get("kdenlive:duration")); if (kdenlive_length > 0) { m_originalProperties.insert(QStringLiteral("kdenlive:duration"), m_properties->get("kdenlive:duration")); } m_originalProperties.insert(QStringLiteral("length"), m_properties->get("length")); auto *hlay = new QHBoxLayout; QCheckBox *box = new QCheckBox(i18n("Duration"), this); box->setObjectName(QStringLiteral("force_duration")); hlay->addWidget(box); auto *timePos = new TimecodeDisplay(m_tc, this); timePos->setObjectName(QStringLiteral("force_duration_value")); timePos->setValue(kdenlive_length > 0 ? kdenlive_length : m_properties->get_int("length")); int original_length = m_properties->get_int("kdenlive:original_length"); if (original_length > 0) { box->setChecked(true); } else { timePos->setEnabled(false); } hlay->addWidget(timePos); vbox->addLayout(hlay); connect(box, &QAbstractButton::toggled, timePos, &QWidget::setEnabled); connect(box, &QCheckBox::stateChanged, this, &ClipPropertiesController::slotEnableForce); connect(timePos, &TimecodeDisplay::timeCodeEditingFinished, this, &ClipPropertiesController::slotDurationChanged); connect(this, &ClipPropertiesController::updateTimeCodeFormat, timePos, &TimecodeDisplay::slotUpdateTimeCodeFormat); connect(this, SIGNAL(modified(int)), timePos, SLOT(setValue(int))); // connect(this, static_cast(&ClipPropertiesController::modified), timePos, &TimecodeDisplay::setValue); } if (m_type == ClipType::TextTemplate) { // Edit text widget QString currentText = m_properties->get("templatetext"); m_originalProperties.insert(QStringLiteral("templatetext"), currentText); m_textEdit = new QTextEdit(this); m_textEdit->setAcceptRichText(false); m_textEdit->setPlainText(currentText); m_textEdit->setPlaceholderText(i18n("Enter template text here")); vbox->addWidget(m_textEdit); QPushButton *button = new QPushButton(i18n("Apply"), this); vbox->addWidget(button); connect(button, &QPushButton::clicked, this, &ClipPropertiesController::slotTextChanged); } else if (m_type == ClipType::Color) { // Edit color widget m_originalProperties.insert(QStringLiteral("resource"), m_properties->get("resource")); mlt_color color = m_properties->get_color("resource"); ChooseColorWidget *choosecolor = new ChooseColorWidget(i18n("Color"), QColor::fromRgb(color.r, color.g, color.b).name(), "", false, this); vbox->addWidget(choosecolor); // connect(choosecolor, SIGNAL(displayMessage(QString,int)), this, SIGNAL(displayMessage(QString,int))); connect(choosecolor, &ChooseColorWidget::modified, this, &ClipPropertiesController::slotColorModified); connect(this, static_cast(&ClipPropertiesController::modified), choosecolor, &ChooseColorWidget::slotColorModified); } if (m_type == ClipType::AV || m_type == ClipType::Video || m_type == ClipType::Image) { // Aspect ratio int force_ar_num = m_properties->get_int("force_aspect_num"); int force_ar_den = m_properties->get_int("force_aspect_den"); m_originalProperties.insert(QStringLiteral("force_aspect_den"), (force_ar_den == 0) ? QString() : QString::number(force_ar_den)); m_originalProperties.insert(QStringLiteral("force_aspect_num"), (force_ar_num == 0) ? QString() : QString::number(force_ar_num)); auto *hlay = new QHBoxLayout; QCheckBox *box = new QCheckBox(i18n("Aspect Ratio"), this); box->setObjectName(QStringLiteral("force_ar")); vbox->addWidget(box); connect(box, &QCheckBox::stateChanged, this, &ClipPropertiesController::slotEnableForce); auto *spin1 = new QSpinBox(this); spin1->setMaximum(8000); spin1->setObjectName(QStringLiteral("force_aspect_num_value")); hlay->addWidget(spin1); hlay->addWidget(new QLabel(QStringLiteral(":"))); auto *spin2 = new QSpinBox(this); spin2->setMinimum(1); spin2->setMaximum(8000); spin2->setObjectName(QStringLiteral("force_aspect_den_value")); hlay->addWidget(spin2); if (force_ar_num == 0) { // use current ratio int num = m_properties->get_int("meta.media.sample_aspect_num"); int den = m_properties->get_int("meta.media.sample_aspect_den"); if (den == 0) { num = 1; den = 1; } spin1->setEnabled(false); spin2->setEnabled(false); spin1->setValue(num); spin2->setValue(den); } else { box->setChecked(true); spin1->setEnabled(true); spin2->setEnabled(true); spin1->setValue(force_ar_num); spin2->setValue(force_ar_den); } connect(spin2, static_cast(&QSpinBox::valueChanged), this, &ClipPropertiesController::slotAspectValueChanged); connect(spin1, static_cast(&QSpinBox::valueChanged), this, &ClipPropertiesController::slotAspectValueChanged); connect(box, &QAbstractButton::toggled, spin1, &QWidget::setEnabled); connect(box, &QAbstractButton::toggled, spin2, &QWidget::setEnabled); vbox->addLayout(hlay); // Proxy QString proxy = m_properties->get("kdenlive:proxy"); m_originalProperties.insert(QStringLiteral("kdenlive:proxy"), proxy); hlay = new QHBoxLayout; QGroupBox *bg = new QGroupBox(this); bg->setCheckable(false); bg->setFlat(true); QHBoxLayout *groupLay = new QHBoxLayout; groupLay->setContentsMargins(0, 0, 0, 0); auto *pbox = new QCheckBox(i18n("Proxy clip"), this); pbox->setTristate(true); // Proxy codec label QLabel *lab = new QLabel(this); pbox->setObjectName(QStringLiteral("kdenlive:proxy")); bool hasProxy = proxy.length() > 2; if (hasProxy) { bg->setToolTip(proxy); bool proxyReady = (QFileInfo(proxy).fileName() == QFileInfo(m_properties->get("resource")).fileName()); if (proxyReady) { pbox->setCheckState(Qt::Checked); lab->setText(m_properties->get(QString("meta.media.%1.codec.name").arg(m_properties->get_int("video_index")).toUtf8().constData())); } else { pbox->setCheckState(Qt::PartiallyChecked); } } else { pbox->setCheckState(Qt::Unchecked); } pbox->setEnabled(pCore->projectManager()->current()->getDocumentProperty(QStringLiteral("enableproxy")).toInt() != 0); - connect(pbox, &QCheckBox::stateChanged, [this, pbox, bg](int state) { + connect(pbox, &QCheckBox::stateChanged, [this, pbox](int state) { emit requestProxy(state == Qt::PartiallyChecked); if (state == Qt::Checked) { QSignalBlocker bk(pbox); pbox->setCheckState(Qt::Unchecked); } }); connect(this, &ClipPropertiesController::enableProxy, pbox, &QCheckBox::setEnabled); connect(this, &ClipPropertiesController::proxyModified, [this, pbox, bg, lab](const QString &pxy) { bool hasProxyClip = pxy.length() > 2; QSignalBlocker bk(pbox); pbox->setCheckState(hasProxyClip ? Qt::Checked : Qt::Unchecked); bg->setEnabled(pbox->isChecked()); bg->setToolTip(pxy); lab->setText(hasProxyClip ? m_properties->get(QString("meta.media.%1.codec.name").arg(m_properties->get_int("video_index")).toUtf8().constData()) : QString()); }); hlay->addWidget(pbox); bg->setEnabled(pbox->checkState() == Qt::Checked); groupLay->addWidget(lab); // Delete button QToolButton *tb = new QToolButton(this); tb->setIcon(QIcon::fromTheme(QStringLiteral("edit-delete"))); tb->setAutoRaise(true); connect(tb, &QToolButton::clicked, [this, proxy]() { emit deleteProxy(); }); tb->setToolTip(i18n("Delete proxy file")); groupLay->addWidget(tb); // Folder button tb = new QToolButton(this); QMenu *pMenu = new QMenu(this); tb->setIcon(QIcon::fromTheme(QStringLiteral("kdenlive-menu"))); tb->setToolTip(i18n("Proxy options")); tb->setMenu(pMenu); tb->setAutoRaise(true); tb->setPopupMode(QToolButton::InstantPopup); QAction *ac = new QAction(QIcon::fromTheme(QStringLiteral("document-open")), i18n("Open folder"), this); connect(ac, &QAction::triggered, [this]() { QString pxy = m_properties->get("kdenlive:proxy"); QDesktopServices::openUrl(QUrl::fromLocalFile(QFileInfo(pxy).path())); }); pMenu->addAction(ac); ac = new QAction(QIcon::fromTheme(QStringLiteral("media-playback-start")), i18n("Play proxy clip"), this); connect(ac, &QAction::triggered, [this]() { QString pxy = m_properties->get("kdenlive:proxy"); QDesktopServices::openUrl(QUrl::fromLocalFile(pxy)); }); pMenu->addAction(ac); ac = new QAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Copy file location to clipboard"), this); connect(ac, &QAction::triggered, [this]() { QString pxy = m_properties->get("kdenlive:proxy"); QGuiApplication::clipboard()->setText(pxy); }); pMenu->addAction(ac); groupLay->addWidget(tb); bg->setLayout(groupLay); hlay->addWidget(bg); vbox->addLayout(hlay); } if (m_type == ClipType::AV || m_type == ClipType::Video) { QLocale locale; // Fps QString force_fps = m_properties->get("force_fps"); m_originalProperties.insert(QStringLiteral("force_fps"), force_fps.isEmpty() ? QStringLiteral("-") : force_fps); auto *hlay = new QHBoxLayout; QCheckBox *box = new QCheckBox(i18n("Frame rate"), this); box->setObjectName(QStringLiteral("force_fps")); connect(box, &QCheckBox::stateChanged, this, &ClipPropertiesController::slotEnableForce); auto *spin = new QDoubleSpinBox(this); spin->setMaximum(1000); connect(spin, SIGNAL(valueChanged(double)), this, SLOT(slotValueChanged(double))); // connect(spin, static_cast(&QDoubleSpinBox::valueChanged), this, &ClipPropertiesController::slotValueChanged); spin->setObjectName(QStringLiteral("force_fps_value")); if (force_fps.isEmpty()) { spin->setValue(controller->originalFps()); } else { spin->setValue(locale.toDouble(force_fps)); } connect(box, &QAbstractButton::toggled, spin, &QWidget::setEnabled); box->setChecked(!force_fps.isEmpty()); spin->setEnabled(!force_fps.isEmpty()); hlay->addWidget(box); hlay->addWidget(spin); vbox->addLayout(hlay); // Scanning QString force_prog = m_properties->get("force_progressive"); m_originalProperties.insert(QStringLiteral("force_progressive"), force_prog.isEmpty() ? QStringLiteral("-") : force_prog); hlay = new QHBoxLayout; box = new QCheckBox(i18n("Scanning"), this); connect(box, &QCheckBox::stateChanged, this, &ClipPropertiesController::slotEnableForce); box->setObjectName(QStringLiteral("force_progressive")); auto *combo = new QComboBox(this); combo->addItem(i18n("Interlaced"), 0); combo->addItem(i18n("Progressive"), 1); connect(combo, static_cast(&QComboBox::currentIndexChanged), this, &ClipPropertiesController::slotComboValueChanged); combo->setObjectName(QStringLiteral("force_progressive_value")); if (!force_prog.isEmpty()) { combo->setCurrentIndex(force_prog.toInt()); } connect(box, &QAbstractButton::toggled, combo, &QWidget::setEnabled); box->setChecked(!force_prog.isEmpty()); combo->setEnabled(!force_prog.isEmpty()); hlay->addWidget(box); hlay->addWidget(combo); vbox->addLayout(hlay); // Field order QString force_tff = m_properties->get("force_tff"); m_originalProperties.insert(QStringLiteral("force_tff"), force_tff.isEmpty() ? QStringLiteral("-") : force_tff); hlay = new QHBoxLayout; box = new QCheckBox(i18n("Field order"), this); connect(box, &QCheckBox::stateChanged, this, &ClipPropertiesController::slotEnableForce); box->setObjectName(QStringLiteral("force_tff")); combo = new QComboBox(this); combo->addItem(i18n("Bottom first"), 0); combo->addItem(i18n("Top first"), 1); connect(combo, static_cast(&QComboBox::currentIndexChanged), this, &ClipPropertiesController::slotComboValueChanged); combo->setObjectName(QStringLiteral("force_tff_value")); if (!force_tff.isEmpty()) { combo->setCurrentIndex(force_tff.toInt()); } connect(box, &QAbstractButton::toggled, combo, &QWidget::setEnabled); box->setChecked(!force_tff.isEmpty()); combo->setEnabled(!force_tff.isEmpty()); hlay->addWidget(box); hlay->addWidget(combo); vbox->addLayout(hlay); // Autorotate QString autorotate = m_properties->get("autorotate"); m_originalProperties.insert(QStringLiteral("autorotate"), autorotate); hlay = new QHBoxLayout; box = new QCheckBox(i18n("Disable autorotate"), this); connect(box, &QCheckBox::stateChanged, this, &ClipPropertiesController::slotEnableForce); box->setObjectName(QStringLiteral("autorotate")); box->setChecked(autorotate == QLatin1String("0")); hlay->addWidget(box); vbox->addLayout(hlay); // Decoding threads QString threads = m_properties->get("threads"); m_originalProperties.insert(QStringLiteral("threads"), threads); hlay = new QHBoxLayout; hlay->addWidget(new QLabel(i18n("Threads"))); auto *spinI = new QSpinBox(this); spinI->setMaximum(4); spinI->setObjectName(QStringLiteral("threads_value")); if (!threads.isEmpty()) { spinI->setValue(threads.toInt()); } else { spinI->setValue(1); } connect(spinI, static_cast(&QSpinBox::valueChanged), this, static_cast(&ClipPropertiesController::slotValueChanged)); hlay->addWidget(spinI); vbox->addLayout(hlay); // Video index if (!m_videoStreams.isEmpty()) { QString vix = m_sourceProperties.get("video_index"); m_originalProperties.insert(QStringLiteral("video_index"), vix); hlay = new QHBoxLayout; KDualAction *ac = new KDualAction(i18n("Disable video"), i18n("Enable video"), this); ac->setInactiveIcon(QIcon::fromTheme(QStringLiteral("kdenlive-show-video"))); ac->setActiveIcon(QIcon::fromTheme(QStringLiteral("kdenlive-hide-video"))); QToolButton *tbv = new QToolButton(this); tbv->setToolButtonStyle(Qt::ToolButtonIconOnly); tbv->setDefaultAction(ac); tbv->setAutoRaise(true); hlay->addWidget(tbv); hlay->addWidget(new QLabel(i18n("Video stream"))); QComboBox *videoStream = new QComboBox(this); int ix = 1; for (int stream : m_videoStreams) { videoStream->addItem(i18n("Video stream %1", ix), stream); ix++; } if (!vix.isEmpty() && vix.toInt() > -1) { videoStream->setCurrentIndex(videoStream->findData(QVariant(vix))); } ac->setActive(vix.toInt() == -1); videoStream->setEnabled(vix.toInt() > -1); videoStream->setVisible(m_videoStreams.size() > 1); - connect(ac, &KDualAction::activeChanged, [this, ac, videoStream](bool activated) { + connect(ac, &KDualAction::activeChanged, [this, videoStream](bool activated) { QMap properties; int vindx = -1; if (activated) { videoStream->setEnabled(false); } else { videoStream->setEnabled(true); vindx = videoStream->currentData().toInt(); } properties.insert(QStringLiteral("video_index"), QString::number(vindx)); properties.insert(QStringLiteral("set.test_image"), vindx > -1 ? QStringLiteral("0") : QStringLiteral("1")); emit updateClipProperties(m_id, m_originalProperties, properties); m_originalProperties = properties; }); QObject::connect(videoStream, static_cast(&QComboBox::currentIndexChanged), [this, videoStream]() { QMap properties; properties.insert(QStringLiteral("video_index"), QString::number(videoStream->currentData().toInt())); emit updateClipProperties(m_id, m_originalProperties, properties); m_originalProperties = properties; }); hlay->addWidget(videoStream); vbox->addLayout(hlay); } // Audio index if (!m_audioStreams.isEmpty()) { QString vix = m_sourceProperties.get("audio_index"); m_originalProperties.insert(QStringLiteral("audio_index"), vix); hlay = new QHBoxLayout; KDualAction *ac = new KDualAction(i18n("Disable audio"), i18n("Enable audio"), this); ac->setInactiveIcon(QIcon::fromTheme(QStringLiteral("kdenlive-show-audio"))); ac->setActiveIcon(QIcon::fromTheme(QStringLiteral("kdenlive-hide-audio"))); QToolButton *tbv = new QToolButton(this); tbv->setToolButtonStyle(Qt::ToolButtonIconOnly); tbv->setDefaultAction(ac); tbv->setAutoRaise(true); hlay->addWidget(tbv); hlay->addWidget(new QLabel(i18n("Audio stream"))); QComboBox *audioStream = new QComboBox(this); int ix = 1; for (int stream : m_audioStreams) { audioStream->addItem(i18n("Audio stream %1", ix), stream); ix++; } if (!vix.isEmpty() && vix.toInt() > -1) { audioStream->setCurrentIndex(audioStream->findData(QVariant(vix))); } ac->setActive(vix.toInt() == -1); audioStream->setEnabled(vix.toInt() > -1); audioStream->setVisible(m_audioStreams.size() > 1); - connect(ac, &KDualAction::activeChanged, [this, ac, audioStream](bool activated) { + connect(ac, &KDualAction::activeChanged, [this, audioStream](bool activated) { QMap properties; int vindx = -1; if (activated) { audioStream->setEnabled(false); } else { audioStream->setEnabled(true); vindx = audioStream->currentData().toInt(); } properties.insert(QStringLiteral("audio_index"), QString::number(vindx)); properties.insert(QStringLiteral("set.test_audio"), vindx > -1 ? QStringLiteral("0") : QStringLiteral("1")); emit updateClipProperties(m_id, m_originalProperties, properties); m_originalProperties = properties; }); QObject::connect(audioStream, static_cast(&QComboBox::currentIndexChanged), [this, audioStream]() { QMap properties; properties.insert(QStringLiteral("audio_index"), QString::number(audioStream->currentData().toInt())); emit updateClipProperties(m_id, m_originalProperties, properties); m_originalProperties = properties; }); hlay->addWidget(audioStream); vbox->addLayout(hlay); } // Colorspace hlay = new QHBoxLayout; box = new QCheckBox(i18n("Colorspace"), this); box->setObjectName(QStringLiteral("force_colorspace")); connect(box, &QCheckBox::stateChanged, this, &ClipPropertiesController::slotEnableForce); combo = new QComboBox(this); combo->setObjectName(QStringLiteral("force_colorspace_value")); combo->addItem(ProfileRepository::getColorspaceDescription(601), 601); combo->addItem(ProfileRepository::getColorspaceDescription(709), 709); combo->addItem(ProfileRepository::getColorspaceDescription(240), 240); int force_colorspace = m_properties->get_int("force_colorspace"); m_originalProperties.insert(QStringLiteral("force_colorspace"), force_colorspace == 0 ? QStringLiteral("-") : QString::number(force_colorspace)); int colorspace = controller->videoCodecProperty(QStringLiteral("colorspace")).toInt(); if (force_colorspace > 0) { box->setChecked(true); combo->setEnabled(true); combo->setCurrentIndex(combo->findData(force_colorspace)); } else if (colorspace > 0) { combo->setEnabled(false); combo->setCurrentIndex(combo->findData(colorspace)); } else { combo->setEnabled(false); } connect(box, &QAbstractButton::toggled, combo, &QWidget::setEnabled); connect(combo, static_cast(&QComboBox::currentIndexChanged), this, &ClipPropertiesController::slotComboValueChanged); hlay->addWidget(box); hlay->addWidget(combo); vbox->addLayout(hlay); // Full luma QString force_luma = m_properties->get("set.force_full_luma"); m_originalProperties.insert(QStringLiteral("set.force_full_luma"), force_luma); hlay = new QHBoxLayout; box = new QCheckBox(i18n("Full luma range"), this); connect(box, &QCheckBox::stateChanged, this, &ClipPropertiesController::slotEnableForce); box->setObjectName(QStringLiteral("set.force_full_luma")); box->setChecked(!force_luma.isEmpty()); hlay->addWidget(box); vbox->addLayout(hlay); hlay->addStretch(10); } QWidget *forceProp = new QWidget(this); forceProp->setLayout(vbox); forcePage->setWidget(forceProp); forcePage->setWidgetResizable(true); vbox->addStretch(10); m_tabWidget->addTab(m_propertiesPage, QString()); m_tabWidget->addTab(forcePage, QString()); m_tabWidget->addTab(m_markersPage, QString()); m_tabWidget->addTab(m_metaPage, QString()); m_tabWidget->addTab(m_analysisPage, QString()); m_tabWidget->setTabIcon(0, QIcon::fromTheme(QStringLiteral("edit-find"))); m_tabWidget->setTabToolTip(0, i18n("File info")); m_tabWidget->setTabIcon(1, QIcon::fromTheme(QStringLiteral("document-edit"))); m_tabWidget->setTabToolTip(1, i18n("Properties")); m_tabWidget->setTabIcon(2, QIcon::fromTheme(QStringLiteral("bookmark-new"))); m_tabWidget->setTabToolTip(2, i18n("Markers")); m_tabWidget->setTabIcon(3, QIcon::fromTheme(QStringLiteral("view-grid"))); m_tabWidget->setTabToolTip(3, i18n("Metadata")); m_tabWidget->setTabIcon(4, QIcon::fromTheme(QStringLiteral("visibility"))); m_tabWidget->setTabToolTip(4, i18n("Analysis")); m_tabWidget->setCurrentIndex(KdenliveSettings::properties_panel_page()); if (m_type == ClipType::Color) { m_tabWidget->setTabEnabled(0, false); } connect(m_tabWidget, &QTabWidget::currentChanged, this, &ClipPropertiesController::updateTab); } ClipPropertiesController::~ClipPropertiesController() {} void ClipPropertiesController::updateTab(int ix) { KdenliveSettings::setProperties_panel_page(ix); } void ClipPropertiesController::slotRefreshTimeCode() { emit updateTimeCodeFormat(); } void ClipPropertiesController::slotReloadProperties() { mlt_color color; m_properties.reset(new Mlt::Properties(m_controller->properties())); m_clipLabel->setText(m_properties->get("kdenlive:clipname")); switch (m_type) { case ClipType::Color: m_originalProperties.insert(QStringLiteral("resource"), m_properties->get("resource")); m_originalProperties.insert(QStringLiteral("out"), m_properties->get("out")); m_originalProperties.insert(QStringLiteral("length"), m_properties->get("length")); emit modified(m_properties->get_int("length")); color = m_properties->get_color("resource"); emit modified(QColor::fromRgb(color.r, color.g, color.b)); break; case ClipType::TextTemplate: m_textEdit->setPlainText(m_properties->get("templatetext")); break; case ClipType::Image: case ClipType::AV: case ClipType::Video: { QString proxy = m_properties->get("kdenlive:proxy"); if (proxy != m_originalProperties.value(QStringLiteral("kdenlive:proxy"))) { m_originalProperties.insert(QStringLiteral("kdenlive:proxy"), proxy); emit proxyModified(proxy); } break; } default: break; } } void ClipPropertiesController::slotColorModified(const QColor &newcolor) { QMap properties; properties.insert(QStringLiteral("resource"), newcolor.name(QColor::HexArgb)); QMap oldProperties; oldProperties.insert(QStringLiteral("resource"), m_properties->get("resource")); emit updateClipProperties(m_id, oldProperties, properties); } void ClipPropertiesController::slotDurationChanged(int duration) { QMap properties; // kdenlive_length is the default duration for image / title clips int kdenlive_length = m_properties->time_to_frames(m_properties->get("kdenlive:duration")); int current_length = m_properties->get_int("length"); if (kdenlive_length > 0) { // special case, image/title clips store default duration in kdenlive:duration property properties.insert(QStringLiteral("kdenlive:duration"), m_properties->frames_to_time(duration)); if (duration > current_length) { properties.insert(QStringLiteral("length"), m_properties->frames_to_time(duration)); properties.insert(QStringLiteral("out"), m_properties->frames_to_time(duration - 1)); } } else { properties.insert(QStringLiteral("length"), m_properties->frames_to_time(duration)); properties.insert(QStringLiteral("out"), m_properties->frames_to_time(duration - 1)); } emit updateClipProperties(m_id, m_originalProperties, properties); m_originalProperties = properties; } void ClipPropertiesController::slotEnableForce(int state) { QCheckBox *box = qobject_cast(sender()); if (!box) { return; } QString param = box->objectName(); QMap properties; QLocale locale; if (state == Qt::Unchecked) { // The force property was disable, remove it / reset default if necessary if (param == QLatin1String("force_duration")) { // special case, reset original duration TimecodeDisplay *timePos = findChild(param + QStringLiteral("_value")); timePos->setValue(m_properties->get_int("kdenlive:original_length")); int original = m_properties->get_int("kdenlive:original_length"); m_properties->set("kdenlive:original_length", (char *)nullptr); slotDurationChanged(original); return; } if (param == QLatin1String("kdenlive:transparency")) { properties.insert(param, QString()); } else if (param == QLatin1String("force_ar")) { properties.insert(QStringLiteral("force_aspect_den"), QString()); properties.insert(QStringLiteral("force_aspect_num"), QString()); properties.insert(QStringLiteral("force_aspect_ratio"), QString()); } else if (param == QLatin1String("autorotate")) { properties.insert(QStringLiteral("autorotate"), QString()); } else { properties.insert(param, QString()); } } else { // A force property was set if (param == QLatin1String("force_duration")) { int original_length = m_properties->get_int("kdenlive:original_length"); if (original_length == 0) { int kdenlive_length = m_properties->time_to_frames(m_properties->get("kdenlive:duration")); m_properties->set("kdenlive:original_length", kdenlive_length > 0 ? m_properties->get("kdenlive:duration") : m_properties->get("length")); } } else if (param == QLatin1String("force_fps")) { QDoubleSpinBox *spin = findChild(param + QStringLiteral("_value")); if (!spin) { return; } properties.insert(param, locale.toString(spin->value())); } else if (param == QLatin1String("threads")) { QSpinBox *spin = findChild(param + QStringLiteral("_value")); if (!spin) { return; } properties.insert(param, QString::number(spin->value())); } else if (param == QLatin1String("force_colorspace") || param == QLatin1String("force_progressive") || param == QLatin1String("force_tff")) { QComboBox *combo = findChild(param + QStringLiteral("_value")); if (!combo) { return; } properties.insert(param, QString::number(combo->currentData().toInt())); } else if (param == QLatin1String("set.force_full_luma")) { properties.insert(param, QStringLiteral("1")); } else if (param == QLatin1String("autorotate")) { properties.insert(QStringLiteral("autorotate"), QStringLiteral("0")); } else if (param == QLatin1String("force_ar")) { QSpinBox *spin = findChild(QStringLiteral("force_aspect_num_value")); QSpinBox *spin2 = findChild(QStringLiteral("force_aspect_den_value")); if ((spin == nullptr) || (spin2 == nullptr)) { return; } properties.insert(QStringLiteral("force_aspect_den"), QString::number(spin2->value())); properties.insert(QStringLiteral("force_aspect_num"), QString::number(spin->value())); properties.insert(QStringLiteral("force_aspect_ratio"), locale.toString((double)spin->value() / spin2->value())); } } if (properties.isEmpty()) { return; } emit updateClipProperties(m_id, m_originalProperties, properties); m_originalProperties = properties; } void ClipPropertiesController::slotValueChanged(double value) { QDoubleSpinBox *box = qobject_cast(sender()); if (!box) { return; } QString param = box->objectName().section(QLatin1Char('_'), 0, -2); QMap properties; QLocale locale; properties.insert(param, locale.toString(value)); emit updateClipProperties(m_id, m_originalProperties, properties); m_originalProperties = properties; } void ClipPropertiesController::slotValueChanged(int value) { QSpinBox *box = qobject_cast(sender()); if (!box) { return; } QString param = box->objectName().section(QLatin1Char('_'), 0, -2); QMap properties; properties.insert(param, QString::number(value)); emit updateClipProperties(m_id, m_originalProperties, properties); m_originalProperties = properties; } void ClipPropertiesController::slotAspectValueChanged(int) { QSpinBox *spin = findChild(QStringLiteral("force_aspect_num_value")); QSpinBox *spin2 = findChild(QStringLiteral("force_aspect_den_value")); if ((spin == nullptr) || (spin2 == nullptr)) { return; } QMap properties; properties.insert(QStringLiteral("force_aspect_den"), QString::number(spin2->value())); properties.insert(QStringLiteral("force_aspect_num"), QString::number(spin->value())); QLocale locale; properties.insert(QStringLiteral("force_aspect_ratio"), locale.toString((double)spin->value() / spin2->value())); emit updateClipProperties(m_id, m_originalProperties, properties); m_originalProperties = properties; } void ClipPropertiesController::slotComboValueChanged() { QComboBox *box = qobject_cast(sender()); if (!box) { return; } QString param = box->objectName().section(QLatin1Char('_'), 0, -2); QMap properties; properties.insert(param, QString::number(box->currentData().toInt())); emit updateClipProperties(m_id, m_originalProperties, properties); m_originalProperties = properties; } void ClipPropertiesController::fillProperties() { m_clipProperties.clear(); QList propertyMap; m_propertiesTree->setSortingEnabled(false); #ifdef KF5_USE_FILEMETADATA // Read File Metadata through KDE's metadata system KFileMetaData::ExtractorCollection metaDataCollection; QMimeDatabase mimeDatabase; QMimeType mimeType; mimeType = mimeDatabase.mimeTypeForFile(m_controller->clipUrl()); for (KFileMetaData::Extractor *plugin : metaDataCollection.fetchExtractors(mimeType.name())) { ExtractionResult extractionResult(m_controller->clipUrl(), mimeType.name(), m_propertiesTree); plugin->extract(&extractionResult); } #endif // Get MLT's metadata if (m_type == ClipType::Image) { int width = m_sourceProperties.get_int("meta.media.width"); int height = m_sourceProperties.get_int("meta.media.height"); propertyMap.append(QStringList() << i18n("Image size") << QString::number(width) + QLatin1Char('x') + QString::number(height)); } if (m_type == ClipType::AV || m_type == ClipType::Video || m_type == ClipType::Audio) { int vindex = m_sourceProperties.get_int("video_index"); int default_audio = m_sourceProperties.get_int("audio_index"); // Find maximum stream index values m_videoStreams.clear(); m_audioStreams.clear(); for (int ix = 0; ix < m_sourceProperties.get_int("meta.media.nb_streams"); ++ix) { char property[200]; snprintf(property, sizeof(property), "meta.media.%d.stream.type", ix); QString type = m_sourceProperties.get(property); if (type == QLatin1String("video")) { m_videoStreams << ix; } else if (type == QLatin1String("audio")) { m_audioStreams << ix; } } m_clipProperties.insert(QStringLiteral("default_video"), QString::number(vindex)); m_clipProperties.insert(QStringLiteral("default_audio"), QString::number(default_audio)); if (vindex > -1) { // We have a video stream QString codecInfo = QString("meta.media.%1.codec.").arg(vindex); QString streamInfo = QString("meta.media.%1.stream.").arg(vindex); QString property = codecInfo + QStringLiteral("long_name"); QString codec = m_sourceProperties.get(property.toUtf8().constData()); if (!codec.isEmpty()) { propertyMap.append({i18n("Video codec"), codec}); } int width = m_sourceProperties.get_int("meta.media.width"); int height = m_sourceProperties.get_int("meta.media.height"); propertyMap.append({i18n("Frame size"), QString::number(width) + QLatin1Char('x') + QString::number(height)}); property = streamInfo + QStringLiteral("frame_rate"); QString fpsValue = m_sourceProperties.get(property.toUtf8().constData()); if (!fpsValue.isEmpty()) { propertyMap.append({i18n("Frame rate"), fpsValue}); } else { int rate_den = m_sourceProperties.get_int("meta.media.frame_rate_den"); if (rate_den > 0) { double fps = ((double)m_sourceProperties.get_int("meta.media.frame_rate_num")) / rate_den; propertyMap.append({i18n("Frame rate"), QString::number(fps, 'f', 2)}); } } property = codecInfo + QStringLiteral("bit_rate"); int bitrate = m_sourceProperties.get_int(property.toUtf8().constData()) / 1000; if (bitrate > 0) { propertyMap.append({i18n("Video bitrate"), QString::number(bitrate) + QLatin1Char(' ') + i18nc("Kilobytes per seconds", "kb/s")}); } int scan = m_sourceProperties.get_int("meta.media.progressive"); propertyMap.append({i18n("Scanning"), (scan == 1 ? i18n("Progressive") : i18n("Interlaced"))}); property = codecInfo + QStringLiteral("sample_aspect_ratio"); double par = m_sourceProperties.get_double(property.toUtf8().constData()); if (qFuzzyIsNull(par)) { // Read media aspect ratio par = m_sourceProperties.get_double("aspect_ratio"); } propertyMap.append({i18n("Pixel aspect ratio"), QString::number(par, 'f', 3)}); property = codecInfo + QStringLiteral("pix_fmt"); propertyMap.append({i18n("Pixel format"), m_sourceProperties.get(property.toUtf8().constData())}); property = codecInfo + QStringLiteral("colorspace"); int colorspace = m_sourceProperties.get_int(property.toUtf8().constData()); propertyMap.append({i18n("Colorspace"), ProfileRepository::getColorspaceDescription(colorspace)}); } if (default_audio > -1) { QString codecInfo = QString("meta.media.%1.codec.").arg(default_audio); QString property = codecInfo + QStringLiteral("long_name"); QString codec = m_sourceProperties.get(property.toUtf8().constData()); if (!codec.isEmpty()) { propertyMap.append({i18n("Audio codec"), codec}); } property = codecInfo + QStringLiteral("channels"); int channels = m_sourceProperties.get_int(property.toUtf8().constData()); propertyMap.append({i18n("Audio channels"), QString::number(channels)}); property = codecInfo + QStringLiteral("sample_rate"); int srate = m_sourceProperties.get_int(property.toUtf8().constData()); propertyMap.append({i18n("Audio frequency"), QString::number(srate) + QLatin1Char(' ') + i18nc("Herz", "Hz")}); property = codecInfo + QStringLiteral("bit_rate"); int bitrate = m_sourceProperties.get_int(property.toUtf8().constData()) / 1000; if (bitrate > 0) { propertyMap.append({i18n("Audio bitrate"), QString::number(bitrate) + QLatin1Char(' ') + i18nc("Kilobytes per seconds", "kb/s")}); } } } qint64 filesize = m_sourceProperties.get_int64("kdenlive:file_size"); if (filesize > 0) { QLocale locale(QLocale::system()); // use the user's locale for getting proper separators! propertyMap.append({i18n("File size"), KIO::convertSize((size_t)filesize) + QStringLiteral(" (") + locale.toString(filesize) + QLatin1Char(')')}); } for (int i = 0; i < propertyMap.count(); i++) { QTreeWidgetItem *item = new QTreeWidgetItem(m_propertiesTree, propertyMap.at(i)); item->setToolTip(1, propertyMap.at(i).at(1)); } m_propertiesTree->setSortingEnabled(true); m_propertiesTree->resizeColumnToContents(0); } void ClipPropertiesController::slotSeekToMarker() { auto markerModel = m_controller->getMarkerModel(); auto current = m_markerTree->currentIndex(); if (!current.isValid()) return; GenTime pos(markerModel->data(current, MarkerListModel::PosRole).toDouble()); emit seekToFrame(pos.frames(pCore->getCurrentFps())); } void ClipPropertiesController::slotEditMarker() { auto markerModel = m_controller->getMarkerModel(); auto current = m_markerTree->currentIndex(); if (!current.isValid()) return; GenTime pos(markerModel->data(current, MarkerListModel::PosRole).toDouble()); markerModel->editMarkerGui(pos, this, false, m_controller); } void ClipPropertiesController::slotDeleteMarker() { auto markerModel = m_controller->getMarkerModel(); auto current = m_markerTree->currentIndex(); if (!current.isValid()) return; GenTime pos(markerModel->data(current, MarkerListModel::PosRole).toDouble()); markerModel->removeMarker(pos); } void ClipPropertiesController::slotAddMarker() { auto markerModel = m_controller->getMarkerModel(); GenTime pos(m_controller->originalProducer()->position(), m_tc.fps()); markerModel->editMarkerGui(pos, this, true, m_controller); } void ClipPropertiesController::slotSaveMarkers() { QScopedPointer fd(new QFileDialog(this, i18n("Save Clip Markers"), pCore->projectManager()->current()->projectDataFolder())); fd->setMimeTypeFilters(QStringList() << QStringLiteral("text/plain")); fd->setFileMode(QFileDialog::AnyFile); fd->setAcceptMode(QFileDialog::AcceptSave); if (fd->exec() != QDialog::Accepted) { return; } QStringList selection = fd->selectedFiles(); QString url; if (!selection.isEmpty()) { url = selection.first(); } if (url.isEmpty()) { return; } QFile file(url); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { KMessageBox::error(this, i18n("Cannot open file %1", QUrl::fromLocalFile(url).fileName())); return; } file.write(m_controller->getMarkerModel()->toJson().toUtf8()); file.close(); } void ClipPropertiesController::slotLoadMarkers() { QScopedPointer fd(new QFileDialog(this, i18n("Load Clip Markers"), pCore->projectManager()->current()->projectDataFolder())); fd->setMimeTypeFilters(QStringList() << QStringLiteral("text/plain")); fd->setFileMode(QFileDialog::ExistingFile); if (fd->exec() != QDialog::Accepted) { return; } QStringList selection = fd->selectedFiles(); QString url; if (!selection.isEmpty()) { url = selection.first(); } if (url.isEmpty()) { return; } QFile file(url); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { KMessageBox::error(this, i18n("Cannot open file %1", QUrl::fromLocalFile(url).fileName())); return; } QString fileContent = QString::fromUtf8(file.readAll()); file.close(); bool res = m_controller->getMarkerModel()->importFromJson(fileContent, false); if (!res) { KMessageBox::error(this, i18n("An error occurred while parsing the marker file")); } } void ClipPropertiesController::slotFillMeta(QTreeWidget *tree) { tree->clear(); if (m_type != ClipType::AV && m_type != ClipType::Video && m_type != ClipType::Image) { // Currently, we only use exiftool on video files return; } int exifUsed = m_controller->getProducerIntProperty(QStringLiteral("kdenlive:exiftool")); if (exifUsed == 1) { Mlt::Properties subProperties; subProperties.pass_values(*m_properties, "kdenlive:meta.exiftool."); if (subProperties.count() > 0) { QTreeWidgetItem *exif = new QTreeWidgetItem(tree, QStringList() << i18n("Exif") << QString()); exif->setExpanded(true); for (int i = 0; i < subProperties.count(); i++) { new QTreeWidgetItem(exif, QStringList() << subProperties.get_name(i) << subProperties.get(i)); } } } else if (KdenliveSettings::use_exiftool()) { QString url = m_controller->clipUrl(); // Check for Canon THM file url = url.section(QLatin1Char('.'), 0, -2) + QStringLiteral(".THM"); if (QFile::exists(url)) { // Read the exif metadata embedded in the THM file QProcess p; QStringList args; args << QStringLiteral("-g") << QStringLiteral("-args") << url; p.start(QStringLiteral("exiftool"), args); p.waitForFinished(); QString res = p.readAllStandardOutput(); m_controller->setProducerProperty(QStringLiteral("kdenlive:exiftool"), 1); QTreeWidgetItem *exif = nullptr; QStringList list = res.split(QLatin1Char('\n')); for (const QString &tagline : list) { if (tagline.startsWith(QLatin1String("-File")) || tagline.startsWith(QLatin1String("-ExifTool"))) { continue; } QString tag = tagline.section(QLatin1Char(':'), 1).simplified(); if (tag.startsWith(QLatin1String("ImageWidth")) || tag.startsWith(QLatin1String("ImageHeight"))) { continue; } if (!tag.section(QLatin1Char('='), 0, 0).isEmpty() && !tag.section(QLatin1Char('='), 1).simplified().isEmpty()) { if (!exif) { exif = new QTreeWidgetItem(tree, QStringList() << i18n("Exif") << QString()); exif->setExpanded(true); } m_controller->setProducerProperty("kdenlive:meta.exiftool." + tag.section(QLatin1Char('='), 0, 0), tag.section(QLatin1Char('='), 1).simplified()); new QTreeWidgetItem(exif, QStringList() << tag.section(QLatin1Char('='), 0, 0) << tag.section(QLatin1Char('='), 1).simplified()); } } } else { if (m_type == ClipType::Image || m_controller->codec(false) == QLatin1String("h264")) { QProcess p; QStringList args; args << QStringLiteral("-g") << QStringLiteral("-args") << m_controller->clipUrl(); p.start(QStringLiteral("exiftool"), args); p.waitForFinished(); QString res = p.readAllStandardOutput(); if (m_type != ClipType::Image) { m_controller->setProducerProperty(QStringLiteral("kdenlive:exiftool"), 1); } QTreeWidgetItem *exif = nullptr; QStringList list = res.split(QLatin1Char('\n')); for (const QString &tagline : list) { if (m_type != ClipType::Image && !tagline.startsWith(QLatin1String("-H264"))) { continue; } QString tag = tagline.section(QLatin1Char(':'), 1); if (tag.startsWith(QLatin1String("ImageWidth")) || tag.startsWith(QLatin1String("ImageHeight"))) { continue; } if (!exif) { exif = new QTreeWidgetItem(tree, QStringList() << i18n("Exif") << QString()); exif->setExpanded(true); } if (m_type != ClipType::Image) { // Do not store image exif metadata in project file, would be too much noise m_controller->setProducerProperty("kdenlive:meta.exiftool." + tag.section(QLatin1Char('='), 0, 0), tag.section(QLatin1Char('='), 1).simplified()); } new QTreeWidgetItem(exif, QStringList() << tag.section(QLatin1Char('='), 0, 0) << tag.section(QLatin1Char('='), 1).simplified()); } } } } int magic = m_controller->getProducerIntProperty(QStringLiteral("kdenlive:magiclantern")); if (magic == 1) { Mlt::Properties subProperties; subProperties.pass_values(*m_properties, "kdenlive:meta.magiclantern."); QTreeWidgetItem *magicL = nullptr; for (int i = 0; i < subProperties.count(); i++) { if (!magicL) { magicL = new QTreeWidgetItem(tree, QStringList() << i18n("Magic Lantern") << QString()); QIcon icon(QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("meta_magiclantern.png"))); magicL->setIcon(0, icon); magicL->setExpanded(true); } new QTreeWidgetItem(magicL, QStringList() << subProperties.get_name(i) << subProperties.get(i)); } } else if (m_type != ClipType::Image && KdenliveSettings::use_magicLantern()) { QString url = m_controller->clipUrl(); url = url.section(QLatin1Char('.'), 0, -2) + QStringLiteral(".LOG"); if (QFile::exists(url)) { QFile file(url); if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { m_controller->setProducerProperty(QStringLiteral("kdenlive:magiclantern"), 1); QTreeWidgetItem *magicL = nullptr; while (!file.atEnd()) { QString line = file.readLine().simplified(); if (line.startsWith('#') || line.isEmpty() || !line.contains(QLatin1Char(':'))) { continue; } if (line.startsWith(QLatin1String("CSV data"))) { break; } m_controller->setProducerProperty("kdenlive:meta.magiclantern." + line.section(QLatin1Char(':'), 0, 0).simplified(), line.section(QLatin1Char(':'), 1).simplified()); if (!magicL) { magicL = new QTreeWidgetItem(tree, QStringList() << i18n("Magic Lantern") << QString()); QIcon icon(QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("meta_magiclantern.png"))); magicL->setIcon(0, icon); magicL->setExpanded(true); } new QTreeWidgetItem(magicL, QStringList() << line.section(QLatin1Char(':'), 0, 0).simplified() << line.section(QLatin1Char(':'), 1).simplified()); } } } // if (!meta.isEmpty()) // clip->setMetadata(meta, "Magic Lantern"); // clip->setProperty("magiclantern", "1"); } tree->resizeColumnToContents(0); } void ClipPropertiesController::slotFillAnalysisData() { m_analysisTree->clear(); Mlt::Properties subProperties; subProperties.pass_values(*m_properties, "kdenlive:clipanalysis."); if (subProperties.count() > 0) { for (int i = 0; i < subProperties.count(); i++) { new QTreeWidgetItem(m_analysisTree, QStringList() << subProperties.get_name(i) << subProperties.get(i)); } } m_analysisTree->resizeColumnToContents(0); } void ClipPropertiesController::slotDeleteAnalysis() { QTreeWidgetItem *current = m_analysisTree->currentItem(); if (!current) { return; } emit editAnalysis(m_id, "kdenlive:clipanalysis." + current->text(0), QString()); } void ClipPropertiesController::slotSaveAnalysis() { const QString url = QFileDialog::getSaveFileName(this, i18n("Save Analysis Data"), QFileInfo(m_controller->clipUrl()).absolutePath(), i18n("Text File (*.txt)")); if (url.isEmpty()) { return; } KSharedConfigPtr config = KSharedConfig::openConfig(url, KConfig::SimpleConfig); KConfigGroup analysisConfig(config, "Analysis"); QTreeWidgetItem *current = m_analysisTree->currentItem(); analysisConfig.writeEntry(current->text(0), current->text(1)); } void ClipPropertiesController::slotLoadAnalysis() { const QString url = QFileDialog::getOpenFileName(this, i18n("Open Analysis Data"), QFileInfo(m_controller->clipUrl()).absolutePath(), i18n("Text File (*.txt)")); if (url.isEmpty()) { return; } KSharedConfigPtr config = KSharedConfig::openConfig(url, KConfig::SimpleConfig); KConfigGroup transConfig(config, "Analysis"); // read the entries QMap profiles = transConfig.entryMap(); QMapIterator i(profiles); while (i.hasNext()) { i.next(); emit editAnalysis(m_id, "kdenlive:clipanalysis." + i.key(), i.value()); } } void ClipPropertiesController::slotTextChanged() { QMap properties; properties.insert(QStringLiteral("templatetext"), m_textEdit->toPlainText()); emit updateClipProperties(m_id, m_originalProperties, properties); m_originalProperties = properties; } diff --git a/src/monitor/glwidget.cpp b/src/monitor/glwidget.cpp index fb424b8b6..64f26a5a0 100644 --- a/src/monitor/glwidget.cpp +++ b/src/monitor/glwidget.cpp @@ -1,1988 +1,1988 @@ /* * Copyright (c) 2011-2016 Meltytech, LLC * Original author: Dan Dennedy * Modified for Kdenlive: Jean-Baptiste Mardelle * * GL shader based on BSD licensed code from Peter Bengtsson: * http://www.fourcc.org/source/YUV420P-OpenGL-GLSLang.c * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include #include #include #include #include #include #include "core.h" #include "glwidget.h" #include "kdenlivesettings.h" #include "monitorproxy.h" #include "profiles/profilemodel.hpp" #include "qml/qmlaudiothumb.h" #include "timeline2/view/qml/timelineitems.h" #include #ifndef GL_UNPACK_ROW_LENGTH #ifdef GL_UNPACK_ROW_LENGTH_EXT #define GL_UNPACK_ROW_LENGTH GL_UNPACK_ROW_LENGTH_EXT #else #error GL_UNPACK_ROW_LENGTH undefined #endif #endif #ifdef QT_NO_DEBUG #define check_error(fn) \ { \ } #else #define check_error(fn) \ { \ uint err = fn->glGetError(); \ if (err != GL_NO_ERROR) { \ qCCritical(KDENLIVE_LOG) << "GL error" << hex << err << dec << "at" << __FILE__ << ":" << __LINE__; \ } \ } #endif #ifndef GL_TIMEOUT_IGNORED #define GL_TIMEOUT_IGNORED 0xFFFFFFFFFFFFFFFFull #endif using namespace Mlt; GLWidget::GLWidget(int id, QObject *parent) : QQuickView((QWindow *)parent) , sendFrameForAnalysis(false) , m_glslManager(nullptr) , m_consumer(nullptr) , m_producer(nullptr) , m_id(id) , m_rulerHeight(QFontMetrics(QApplication::font()).lineSpacing() * 0.7) , m_shader(nullptr) , m_initSem(0) , m_analyseSem(1) , m_isInitialized(false) , m_threadStartEvent(nullptr) , m_threadStopEvent(nullptr) , m_threadCreateEvent(nullptr) , m_threadJoinEvent(nullptr) , m_displayEvent(nullptr) , m_frameRenderer(nullptr) , m_projectionLocation(0) , m_modelViewLocation(0) , m_vertexLocation(0) , m_texCoordLocation(0) , m_colorspaceLocation(0) , m_zoom(1.0f) , m_sendFrame(false) , m_isZoneMode(false) , m_isLoopMode(false) , m_offset(QPoint(0, 0)) , m_audioWaveDisplayed(false) , m_fbo(nullptr) , m_shareContext(nullptr) , m_openGLSync(false) , m_ClientWaitSync(nullptr) { KDeclarative::KDeclarative kdeclarative; kdeclarative.setDeclarativeEngine(engine()); #if KDECLARATIVE_VERSION >= QT_VERSION_CHECK(5, 45, 0) kdeclarative.setupEngine(engine()); kdeclarative.setupContext(); #else kdeclarative.setupBindings(); #endif m_texture[0] = m_texture[1] = m_texture[2] = 0; qRegisterMetaType("Mlt::Frame"); qRegisterMetaType("SharedFrame"); qmlRegisterType("AudioThumb", 1, 0, "QmlAudioThumb"); setPersistentOpenGLContext(true); setPersistentSceneGraph(true); setClearBeforeRendering(false); setResizeMode(QQuickView::SizeRootObjectToView); m_offscreenSurface.setFormat(QWindow::format()); m_offscreenSurface.create(); m_monitorProfile = new Mlt::Profile(); m_refreshTimer.setSingleShot(true); m_refreshTimer.setInterval(50); m_blackClip.reset(new Mlt::Producer(*m_monitorProfile, "color:black")); m_blackClip->set("kdenlive:id", "black"); m_blackClip->set("out", 3); connect(&m_refreshTimer, &QTimer::timeout, this, &GLWidget::refresh); m_producer = m_blackClip; if (!initGPUAccel()) { disableGPUAccel(); } connect(this, &QQuickWindow::sceneGraphInitialized, this, &GLWidget::initializeGL, Qt::DirectConnection); connect(this, &QQuickWindow::beforeRendering, this, &GLWidget::paintGL, Qt::DirectConnection); registerTimelineItems(); m_proxy = new MonitorProxy(this); connect(m_proxy, &MonitorProxy::seekRequestChanged, this, &GLWidget::requestSeek); rootContext()->setContextProperty("controller", m_proxy); } GLWidget::~GLWidget() { // C & D delete m_glslManager; delete m_threadStartEvent; delete m_threadStopEvent; delete m_threadCreateEvent; delete m_threadJoinEvent; delete m_displayEvent; if (m_frameRenderer) { if (m_frameRenderer->isRunning()) { QMetaObject::invokeMethod(m_frameRenderer, "cleanup"); m_frameRenderer->quit(); m_frameRenderer->wait(); m_frameRenderer->deleteLater(); } else { delete m_frameRenderer; } } m_blackClip.reset(); delete m_shareContext; delete m_shader; // delete m_monitorProfile; } void GLWidget::updateAudioForAnalysis() { if (m_frameRenderer) { m_frameRenderer->sendAudioForAnalysis = KdenliveSettings::monitor_audio(); } } void GLWidget::initializeGL() { if (m_isInitialized || !isVisible() || (openglContext() == nullptr)) return; openglContext()->makeCurrent(&m_offscreenSurface); initializeOpenGLFunctions(); qCDebug(KDENLIVE_LOG) << "OpenGL vendor: " << QString::fromUtf8((const char *)glGetString(GL_VENDOR)); qCDebug(KDENLIVE_LOG) << "OpenGL renderer: " << QString::fromUtf8((const char *)glGetString(GL_RENDERER)); qCDebug(KDENLIVE_LOG) << "OpenGL Threaded: " << openglContext()->supportsThreadedOpenGL(); qCDebug(KDENLIVE_LOG) << "OpenGL ARG_SYNC: " << openglContext()->hasExtension("GL_ARB_sync"); qCDebug(KDENLIVE_LOG) << "OpenGL OpenGLES: " << openglContext()->isOpenGLES(); // C & D if (onlyGLESGPUAccel()) { disableGPUAccel(); } createShader(); m_openGLSync = initGPUAccelSync(); // C & D if (m_glslManager) { // Create a context sharing with this context for the RenderThread context. // This is needed because openglContext() is active in another thread // at the time that RenderThread is created. // See this Qt bug for more info: https://bugreports.qt.io/browse/QTBUG-44677 // TODO: QTBUG-44677 is closed. still applicable? m_shareContext = new QOpenGLContext; m_shareContext->setFormat(openglContext()->format()); m_shareContext->setShareContext(openglContext()); m_shareContext->create(); } m_frameRenderer = new FrameRenderer(openglContext(), &m_offscreenSurface, m_ClientWaitSync); m_frameRenderer->sendAudioForAnalysis = KdenliveSettings::monitor_audio(); openglContext()->makeCurrent(this); // openglContext()->blockSignals(false); connect(m_frameRenderer, &FrameRenderer::frameDisplayed, this, &GLWidget::frameDisplayed, Qt::QueuedConnection); connect(m_frameRenderer, &FrameRenderer::textureReady, this, &GLWidget::updateTexture, Qt::DirectConnection); connect(m_frameRenderer, &FrameRenderer::frameDisplayed, this, &GLWidget::onFrameDisplayed, Qt::QueuedConnection); connect(m_frameRenderer, &FrameRenderer::audioSamplesSignal, this, &GLWidget::audioSamplesSignal, Qt::QueuedConnection); m_initSem.release(); m_isInitialized = true; reconfigure(); } void GLWidget::resizeGL(int width, int height) { int x, y, w, h; height -= m_rulerHeight; double this_aspect = (double)width / height; double video_aspect = m_monitorProfile->dar(); // Special case optimization to negate odd effect of sample aspect ratio // not corresponding exactly with image resolution. if ((int)(this_aspect * 1000) == (int)(video_aspect * 1000)) { w = width; h = height; } // Use OpenGL to normalise sample aspect ratio else if (height * video_aspect > width) { w = width; h = width / video_aspect; } else { w = height * video_aspect; h = height; } x = (width - w) / 2; y = (height - h) / 2; m_rect.setRect(x, y, w, h); double scalex = (double)m_rect.width() / m_monitorProfile->width() * m_zoom; double scaley = (double)m_rect.width() / ((double)m_monitorProfile->height() * m_monitorProfile->dar() / m_monitorProfile->width()) / m_monitorProfile->width() * m_zoom; QPoint center = m_rect.center(); QQuickItem *rootQml = rootObject(); if (rootQml) { rootQml->setProperty("center", center); rootQml->setProperty("scalex", scalex); rootQml->setProperty("scaley", scaley); if (rootQml->objectName() == QLatin1String("rootsplit")) { // Adjust splitter pos rootQml->setProperty("splitterPos", x + (rootQml->property("realpercent").toDouble() * w)); } } emit rectChanged(); } void GLWidget::resizeEvent(QResizeEvent *event) { resizeGL(event->size().width(), event->size().height()); QQuickView::resizeEvent(event); } void GLWidget::createGPUAccelFragmentProg() { m_shader->addShaderFromSourceCode(QOpenGLShader::Fragment, "uniform sampler2D tex;" "varying highp vec2 coordinates;" "void main(void) {" " gl_FragColor = texture2D(tex, coordinates);" "}"); m_shader->link(); m_textureLocation[0] = m_shader->uniformLocation("tex"); } void GLWidget::createShader() { m_shader = new QOpenGLShaderProgram; m_shader->addShaderFromSourceCode(QOpenGLShader::Vertex, "uniform highp mat4 projection;" "uniform highp mat4 modelView;" "attribute highp vec4 vertex;" "attribute highp vec2 texCoord;" "varying highp vec2 coordinates;" "void main(void) {" " gl_Position = projection * modelView * vertex;" " coordinates = texCoord;" "}"); // C & D if (m_glslManager) { createGPUAccelFragmentProg(); } else { // A & B createYUVTextureProjectFragmentProg(); } m_projectionLocation = m_shader->uniformLocation("projection"); m_modelViewLocation = m_shader->uniformLocation("modelView"); m_vertexLocation = m_shader->attributeLocation("vertex"); m_texCoordLocation = m_shader->attributeLocation("texCoord"); } void GLWidget::createYUVTextureProjectFragmentProg() { m_shader->addShaderFromSourceCode(QOpenGLShader::Fragment, "uniform sampler2D Ytex, Utex, Vtex;" "uniform lowp int colorspace;" "varying highp vec2 coordinates;" "void main(void) {" " mediump vec3 texel;" " texel.r = texture2D(Ytex, coordinates).r - 0.0625;" // Y " texel.g = texture2D(Utex, coordinates).r - 0.5;" // U " texel.b = texture2D(Vtex, coordinates).r - 0.5;" // V " mediump mat3 coefficients;" " if (colorspace == 601) {" " coefficients = mat3(" " 1.1643, 1.1643, 1.1643," // column 1 " 0.0, -0.39173, 2.017," // column 2 " 1.5958, -0.8129, 0.0);" // column 3 " } else {" // ITU-R 709 " coefficients = mat3(" " 1.1643, 1.1643, 1.1643," // column 1 " 0.0, -0.213, 2.112," // column 2 " 1.793, -0.533, 0.0);" // column 3 " }" " gl_FragColor = vec4(coefficients * texel, 1.0);" "}"); m_shader->link(); m_textureLocation[0] = m_shader->uniformLocation("Ytex"); m_textureLocation[1] = m_shader->uniformLocation("Utex"); m_textureLocation[2] = m_shader->uniformLocation("Vtex"); m_colorspaceLocation = m_shader->uniformLocation("colorspace"); } static void uploadTextures(QOpenGLContext *context, const SharedFrame &frame, GLuint texture[]) { int width = frame.get_image_width(); int height = frame.get_image_height(); const uint8_t *image = frame.get_image(); QOpenGLFunctions *f = context->functions(); // The planes of pixel data may not be a multiple of the default 4 bytes. f->glPixelStorei(GL_UNPACK_ALIGNMENT, 1); // Upload each plane of YUV to a texture. if (texture[0] != 0u) { f->glDeleteTextures(3, texture); } check_error(f); f->glGenTextures(3, texture); check_error(f); f->glBindTexture(GL_TEXTURE_2D, texture[0]); check_error(f); f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); check_error(f); f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); check_error(f); f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); check_error(f); f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); check_error(f); f->glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, width, height, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, image); check_error(f); f->glBindTexture(GL_TEXTURE_2D, texture[1]); check_error(f); f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); check_error(f); f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); check_error(f); f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); check_error(f); f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); check_error(f); f->glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, width / 2, height / 2, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, image + width * height); check_error(f); f->glBindTexture(GL_TEXTURE_2D, texture[2]); check_error(f); f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); check_error(f); f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); check_error(f); f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); check_error(f); f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); check_error(f); f->glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, width / 2, height / 2, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, image + width * height + width / 2 * height / 2); check_error(f); } void GLWidget::clear() { stopGlsl(); update(); } void GLWidget::releaseAnalyse() { m_analyseSem.release(); } bool GLWidget::acquireSharedFrameTextures() { // A if ((m_glslManager == nullptr) && !openglContext()->supportsThreadedOpenGL()) { QMutexLocker locker(&m_contextSharedAccess); if (!m_sharedFrame.is_valid()) { return false; } uploadTextures(openglContext(), m_sharedFrame, m_texture); } else if (m_glslManager) { // C & D m_contextSharedAccess.lock(); if (m_sharedFrame.is_valid()) { m_texture[0] = *((const GLuint *)m_sharedFrame.get_image()); } } if (!m_texture[0]) { // C & D if (m_glslManager) m_contextSharedAccess.unlock(); return false; } return true; } void GLWidget::bindShaderProgram() { m_shader->bind(); // C & D if (m_glslManager) { m_shader->setUniformValue(m_textureLocation[0], 0); } else { // A & B m_shader->setUniformValue(m_textureLocation[0], 0); m_shader->setUniformValue(m_textureLocation[1], 1); m_shader->setUniformValue(m_textureLocation[2], 2); m_shader->setUniformValue(m_colorspaceLocation, m_monitorProfile->colorspace()); } } void GLWidget::releaseSharedFrameTextures() { // C & D if (m_glslManager) { glFinish(); m_contextSharedAccess.unlock(); } } bool GLWidget::initGPUAccel() { if (!KdenliveSettings::gpu_accel()) return false; m_glslManager = new Mlt::Filter(*m_monitorProfile, "glsl.manager"); return m_glslManager->is_valid(); } // C & D // TODO: insure safe, idempotent on all pipelines. void GLWidget::disableGPUAccel() { delete m_glslManager; m_glslManager = nullptr; KdenliveSettings::setGpu_accel(false); // Need to destroy MLT global reference to prevent filters from trying to use GPU. mlt_properties_set_data(mlt_global_properties(), "glslManager", nullptr, 0, nullptr, nullptr); emit gpuNotSupported(); } bool GLWidget::onlyGLESGPUAccel() const { return (m_glslManager != nullptr) && openglContext()->isOpenGLES(); } #if defined(Q_OS_WIN) bool GLWidget::initGPUAccelSync() { // no-op // TODO: getProcAddress is not working on Windows? return false; } #else bool GLWidget::initGPUAccelSync() { if (!KdenliveSettings::gpu_accel()) return false; if (m_glslManager == nullptr) return false; if (!openglContext()->hasExtension("GL_ARB_sync")) return false; m_ClientWaitSync = (ClientWaitSync_fp)openglContext()->getProcAddress("glClientWaitSync"); if (m_ClientWaitSync) { return true; } else { qCDebug(KDENLIVE_LOG) << " / / // NO GL SYNC, ERROR"; // fallback on A || B // TODO: fallback on A || B || C? disableGPUAccel(); return false; } } #endif void GLWidget::paintGL() { QOpenGLFunctions *f = openglContext()->functions(); int width = this->width() * devicePixelRatio(); int height = this->height() * devicePixelRatio(); f->glDisable(GL_BLEND); f->glDisable(GL_DEPTH_TEST); f->glDepthMask(GL_FALSE); f->glViewport(0, (m_rulerHeight * devicePixelRatio() * 0.5 + 0.5), width, height); check_error(f); QColor color(KdenliveSettings::window_background()); f->glClearColor(color.redF(), color.greenF(), color.blueF(), color.alphaF()); f->glClear(GL_COLOR_BUFFER_BIT); check_error(f); if (!acquireSharedFrameTextures()) return; // Bind textures. for (uint i = 0; i < 3; ++i) { if (m_texture[i] != 0u) { f->glActiveTexture(GL_TEXTURE0 + i); f->glBindTexture(GL_TEXTURE_2D, m_texture[i]); check_error(f); } } bindShaderProgram(); check_error(f); // Setup an orthographic projection. QMatrix4x4 projection; projection.scale(2.0f / (float)width, 2.0f / (float)height); m_shader->setUniformValue(m_projectionLocation, projection); check_error(f); // Set model view. QMatrix4x4 modelView; if (!qFuzzyCompare(m_zoom, 1.0f)) { if ((offset().x() != 0) || (offset().y() != 0)) modelView.translate(-offset().x() * devicePixelRatio(), offset().y() * devicePixelRatio()); modelView.scale(zoom(), zoom()); } m_shader->setUniformValue(m_modelViewLocation, modelView); check_error(f); // Provide vertices of triangle strip. QVector vertices; width = m_rect.width() * devicePixelRatio(); height = m_rect.height() * devicePixelRatio(); vertices << QVector2D(float(-width) / 2.0f, float(-height) / 2.0f); vertices << QVector2D(float(-width) / 2.0f, float(height) / 2.0f); vertices << QVector2D(float(width) / 2.0f, float(-height) / 2.0f); vertices << QVector2D(float(width) / 2.0f, float(height) / 2.0f); m_shader->enableAttributeArray(m_vertexLocation); check_error(f); m_shader->setAttributeArray(m_vertexLocation, vertices.constData()); check_error(f); // Provide texture coordinates. QVector texCoord; texCoord << QVector2D(0.0f, 1.0f); texCoord << QVector2D(0.0f, 0.0f); texCoord << QVector2D(1.0f, 1.0f); texCoord << QVector2D(1.0f, 0.0f); m_shader->enableAttributeArray(m_texCoordLocation); check_error(f); m_shader->setAttributeArray(m_texCoordLocation, texCoord.constData()); check_error(f); // Render glDrawArrays(GL_TRIANGLE_STRIP, 0, vertices.size()); check_error(f); if (m_sendFrame && m_analyseSem.tryAcquire(1)) { // Render RGB frame for analysis int fullWidth = m_monitorProfile->width(); int fullHeight = m_monitorProfile->height(); if ((m_fbo == nullptr) || m_fbo->size() != QSize(fullWidth, fullHeight)) { delete m_fbo; QOpenGLFramebufferObjectFormat fmt; fmt.setSamples(1); fmt.setInternalTextureFormat(GL_RGB); // GL_RGBA32F); // which one is the fastest ? m_fbo = new QOpenGLFramebufferObject(fullWidth, fullHeight, fmt); // GL_TEXTURE_2D); } m_fbo->bind(); glViewport(0, 0, fullWidth, fullHeight); QMatrix4x4 projection2; projection2.scale(2.0f / (float)width, 2.0f / (float)height); m_shader->setUniformValue(m_projectionLocation, projection2); glDrawArrays(GL_TRIANGLE_STRIP, 0, vertices.size()); check_error(f); m_fbo->release(); emit analyseFrame(m_fbo->toImage()); m_sendFrame = false; } // Cleanup m_shader->disableAttributeArray(m_vertexLocation); m_shader->disableAttributeArray(m_texCoordLocation); m_shader->release(); for (uint i = 0; i < 3; ++i) { if (m_texture[i] != 0u) { f->glActiveTexture(GL_TEXTURE0 + i); f->glBindTexture(GL_TEXTURE_2D, 0); check_error(f); } } glActiveTexture(GL_TEXTURE0); check_error(f); releaseSharedFrameTextures(); check_error(f); } void GLWidget::slotZoom(bool zoomIn) { if (zoomIn) { if (qFuzzyCompare(m_zoom, 1.0f)) { setZoom(2.0f); } else if (qFuzzyCompare(m_zoom, 2.0f)) { setZoom(3.0f); } else if (m_zoom < 1.0f) { setZoom(m_zoom * 2); } } else { if (qFuzzyCompare(m_zoom, 3.0f)) { setZoom(2.0); } else if (qFuzzyCompare(m_zoom, 2.0f)) { setZoom(1.0); } else if (m_zoom > 0.2) { setZoom(m_zoom / 2); } } } void GLWidget::wheelEvent(QWheelEvent *event) { if (((event->modifiers() & Qt::ControlModifier) != 0u) && ((event->modifiers() & Qt::ShiftModifier) != 0u)) { slotZoom(event->delta() > 0); return; } emit mouseSeek(event->delta(), (uint)event->modifiers()); event->accept(); } void GLWidget::requestSeek() { if (!m_producer) { return; } if (m_proxy->seeking()) { m_producer->seek(m_proxy->seekPosition()); if (m_consumer->is_stopped()) { m_consumer->start(); } else { m_consumer->purge(); m_consumer->set("refresh", 1); } } } void GLWidget::seek(int pos) { if (!m_proxy->seeking()) { m_proxy->setSeekPosition(pos); m_producer->seek(pos); if (m_consumer->is_stopped()) { m_consumer->start(); } else { m_consumer->purge(); m_consumer->set("refresh", 1); } } else { m_proxy->setSeekPosition(pos); } } void GLWidget::requestRefresh() { if (m_proxy->seeking()) { return; } if (m_producer && qFuzzyIsNull(m_producer->get_speed())) { m_refreshTimer.start(); } } QString GLWidget::frameToTime(int frames) const { return m_consumer ? m_consumer->frames_to_time(frames, mlt_time_smpte_df) : QStringLiteral("-"); } void GLWidget::refresh() { m_refreshTimer.stop(); if (m_proxy->seeking()) { return; } QMutexLocker locker(&m_mltMutex); if (m_consumer->is_stopped()) { m_consumer->start(); } m_consumer->set("refresh", 1); } bool GLWidget::checkFrameNumber(int pos, int offset) { emit consumerPosition(pos); if (!m_proxy->setPosition(pos)) { emit seekPosition(m_proxy->seekOrCurrentPosition()); } const double speed = m_producer->get_speed(); if (m_proxy->seeking()) { m_producer->set_speed(0); m_producer->seek(m_proxy->seekPosition()); if (qFuzzyIsNull(speed)) { m_consumer->set("refresh", 1); } else { m_producer->set_speed(speed); } } else if (qFuzzyIsNull(speed)) { if (m_isLoopMode) { if (pos >= m_producer->get_int("out") - offset) { m_consumer->purge(); m_producer->seek(m_proxy->zoneIn()); m_producer->set_speed(1.0); m_consumer->set("refresh", 1); } return true; } else { if (pos >= m_producer->get_int("out") - offset) { return false; } return true; } } else if (speed < 0. && pos <= 0) { m_producer->set_speed(0); return false; } return true; } void GLWidget::mousePressEvent(QMouseEvent *event) { if ((rootObject() != nullptr) && rootObject()->objectName() != QLatin1String("root") && !(event->modifiers() & Qt::ControlModifier) && !(event->buttons() & Qt::MiddleButton)) { event->ignore(); QQuickView::mousePressEvent(event); return; } if ((event->button() & Qt::LeftButton) != 0u) { if ((event->modifiers() & Qt::ControlModifier) != 0u) { // Pan view m_panStart = event->pos(); setCursor(Qt::ClosedHandCursor); } else { m_dragStart = event->pos(); } } else if ((event->button() & Qt::RightButton) != 0u) { emit showContextMenu(event->globalPos()); } else if ((event->button() & Qt::MiddleButton) != 0u) { m_panStart = event->pos(); setCursor(Qt::ClosedHandCursor); } event->accept(); QQuickView::mousePressEvent(event); } void GLWidget::mouseMoveEvent(QMouseEvent *event) { if ((rootObject() != nullptr) && rootObject()->objectName() != QLatin1String("root") && !(event->modifiers() & Qt::ControlModifier) && !(event->buttons() & Qt::MiddleButton)) { event->ignore(); QQuickView::mouseMoveEvent(event); return; } /* if (event->modifiers() == Qt::ShiftModifier && m_producer) { emit seekTo(m_producer->get_length() * event->x() / width()); return; }*/ QQuickView::mouseMoveEvent(event); if (!m_panStart.isNull()) { emit panView(m_panStart - event->pos()); m_panStart = event->pos(); event->accept(); QQuickView::mouseMoveEvent(event); return; } if (!(event->buttons() & Qt::LeftButton)) { QQuickView::mouseMoveEvent(event); return; } if (!event->isAccepted() && !m_dragStart.isNull() && (event->pos() - m_dragStart).manhattanLength() >= QApplication::startDragDistance()) { m_dragStart = QPoint(); emit startDrag(); } } void GLWidget::keyPressEvent(QKeyEvent *event) { QQuickView::keyPressEvent(event); if (!event->isAccepted()) { emit passKeyEvent(event); } } void GLWidget::createThread(RenderThread **thread, thread_function_t function, void *data) { #ifdef Q_OS_WIN // On Windows, MLT event consumer-thread-create is fired from the Qt main thread. while (!m_isInitialized) { qApp->processEvents(); } #else if (!m_isInitialized) { m_initSem.acquire(); } #endif (*thread) = new RenderThread(function, data, m_shareContext, &m_offscreenSurface); (*thread)->start(); } static void onThreadCreate(mlt_properties owner, GLWidget *self, RenderThread **thread, int *priority, thread_function_t function, void *data) { Q_UNUSED(owner) Q_UNUSED(priority) // self->clearFrameRenderer(); self->createThread(thread, function, data); self->lockMonitor(); } static void onThreadJoin(mlt_properties owner, GLWidget *self, RenderThread *thread) { Q_UNUSED(owner) if (thread) { thread->quit(); thread->wait(); delete thread; // self->clearFrameRenderer(); self->releaseMonitor(); } } void GLWidget::startGlsl() { // C & D if (m_glslManager) { // clearFrameRenderer(); m_glslManager->fire_event("init glsl"); if (m_glslManager->get_int("glsl_supported") == 0) { disableGPUAccel(); } else { emit started(); } } } static void onThreadStarted(mlt_properties owner, GLWidget *self) { Q_UNUSED(owner) self->startGlsl(); } void GLWidget::releaseMonitor() { emit lockMonitor(false); } void GLWidget::lockMonitor() { emit lockMonitor(true); } void GLWidget::stopGlsl() { if (m_consumer) { m_consumer->purge(); } // C & D // TODO This is commented out for now because it is causing crashes. // Technically, this should be the correct thing to do, but it appears // some changes have created regression (see shotcut) // with respect to restarting the consumer in GPU mode. // m_glslManager->fire_event("close glsl"); m_texture[0] = 0; } static void onThreadStopped(mlt_properties owner, GLWidget *self) { Q_UNUSED(owner) self->stopGlsl(); } void GLWidget::slotSwitchAudioOverlay(bool enable) { KdenliveSettings::setDisplayAudioOverlay(enable); if (m_audioWaveDisplayed && !enable) { if (m_producer && m_producer->get_int("video_index") != -1) { // We have a video producer, disable filter removeAudioOverlay(); } } if (enable && !m_audioWaveDisplayed && m_producer) { createAudioOverlay(m_producer->get_int("video_index") == -1); } } -int GLWidget::setProducer(std::shared_ptr producer, bool isActive, int position) +int GLWidget::setProducer(const std::shared_ptr &producer, bool isActive, int position) { int error = 0; QString currentId; int consumerPosition = 0; currentId = m_producer->parent().get("kdenlive:id"); if (producer) { m_producer = producer; } else { if (currentId == QLatin1String("black")) { return 0; } if (m_audioWaveDisplayed) { removeAudioOverlay(); } m_producer = m_blackClip; } // redundant check. postcondition of above is m_producer != null if (m_producer) { m_producer->set_speed(0); if (m_consumer) { consumerPosition = m_consumer->position(); m_consumer->stop(); if (!m_consumer->is_stopped()) { m_consumer->stop(); } } error = reconfigure(); if (error == 0) { // The profile display aspect ratio may have changed. resizeGL(width(), height()); } } else { return error; } if (!m_consumer) { return error; } consumerPosition = m_consumer->position(); if (m_producer->get_int("video_index") == -1) { // This is an audio only clip, attach visualization filter. Currently, the filter crashes MLT when Movit accel is used if (!m_audioWaveDisplayed) { createAudioOverlay(true); } else if (m_consumer) { if (KdenliveSettings::gpu_accel()) { removeAudioOverlay(); } else { adjustAudioOverlay(true); } } } else if (m_audioWaveDisplayed && (m_consumer != nullptr)) { // This is not an audio clip, hide wave if (KdenliveSettings::displayAudioOverlay()) { adjustAudioOverlay(m_producer->get_int("video_index") == -1); } else { removeAudioOverlay(); } } else if (KdenliveSettings::displayAudioOverlay()) { createAudioOverlay(false); } if (position == -1 && m_producer->parent().get("kdenlive:id") == currentId) { position = consumerPosition; } if (isActive) { startConsumer(); } m_proxy->requestSeekPosition(position > 0 ? position : m_producer->position()); return error; } int GLWidget::droppedFrames() const { return (m_consumer ? m_consumer->get_int("drop_count") : 0); } void GLWidget::resetDrops() { if (m_consumer) { m_consumer->set("drop_count", 0); } } void GLWidget::createAudioOverlay(bool isAudio) { if (!m_consumer) { return; } if (isAudio && KdenliveSettings::gpu_accel()) { // Audiowaveform filter crashes on Movit + audio clips) return; } Mlt::Filter f(*m_monitorProfile, "audiowaveform"); if (f.is_valid()) { // f.set("show_channel", 1); f.set("color.1", "0xffff0099"); f.set("fill", 1); if (isAudio) { // Fill screen f.set("rect", "0,0,100%,100%"); } else { // Overlay on lower part of the screen f.set("rect", "0,80%,100%,20%"); } m_consumer->attach(f); m_audioWaveDisplayed = true; } } void GLWidget::removeAudioOverlay() { Mlt::Service sourceService(m_consumer->get_service()); // move all effects to the correct producer int ct = 0; Mlt::Filter *filter = sourceService.filter(ct); while (filter != nullptr) { QString srv = filter->get("mlt_service"); if (srv == QLatin1String("audiowaveform")) { sourceService.detach(*filter); delete filter; break; } else { ct++; } filter = sourceService.filter(ct); } m_audioWaveDisplayed = false; } void GLWidget::adjustAudioOverlay(bool isAudio) { Mlt::Service sourceService(m_consumer->get_service()); // move all effects to the correct producer int ct = 0; Mlt::Filter *filter = sourceService.filter(ct); while (filter != nullptr) { QString srv = filter->get("mlt_service"); if (srv == QLatin1String("audiowaveform")) { if (isAudio) { filter->set("rect", "0,0,100%,100%"); } else { filter->set("rect", "0,80%,100%,20%"); } break; } else { ct++; } filter = sourceService.filter(ct); } } void GLWidget::stopCapture() { if (strcmp(m_consumer->get("mlt_service"), "multi") == 0) { m_consumer->set("refresh", 0); m_consumer->purge(); m_consumer->stop(); } } int GLWidget::reconfigureMulti(const QString ¶ms, const QString &path, Mlt::Profile *profile) { QString serviceName = property("mlt_service").toString(); if ((m_consumer == nullptr) || !m_consumer->is_valid() || strcmp(m_consumer->get("mlt_service"), "multi") != 0) { if (m_consumer) { m_consumer->purge(); m_consumer->stop(); m_consumer.reset(); } m_consumer.reset(new Mlt::FilteredConsumer(*profile, "multi")); delete m_threadStartEvent; m_threadStartEvent = nullptr; delete m_threadStopEvent; m_threadStopEvent = nullptr; delete m_threadCreateEvent; delete m_threadJoinEvent; if (m_consumer) { m_threadCreateEvent = m_consumer->listen("consumer-thread-create", this, (mlt_listener)onThreadCreate); m_threadJoinEvent = m_consumer->listen("consumer-thread-join", this, (mlt_listener)onThreadJoin); } } if (m_consumer->is_valid()) { // build sub consumers // m_consumer->set("mlt_image_format", "yuv422"); reloadProfile(); int volume = KdenliveSettings::volume(); m_consumer->set("0", serviceName.toUtf8().constData()); m_consumer->set("0.mlt_image_format", "yuv422"); m_consumer->set("0.terminate_on_pause", 0); // m_consumer->set("0.preview_off", 1); m_consumer->set("0.real_time", 0); m_consumer->set("0.volume", (double)volume / 100); if (serviceName.startsWith(QLatin1String("sdl_audio"))) { #ifdef Q_OS_WIN m_consumer->set("0.audio_buffer", 2048); #else m_consumer->set("0.audio_buffer", 512); #endif QString audioDevice = KdenliveSettings::audiodevicename(); if (!audioDevice.isEmpty()) { m_consumer->set("audio_device", audioDevice.toUtf8().constData()); } QString audioDriver = KdenliveSettings::audiodrivername(); if (!audioDriver.isEmpty()) { m_consumer->set("audio_driver", audioDriver.toUtf8().constData()); } } m_consumer->set("1", "avformat"); m_consumer->set("1.target", path.toUtf8().constData()); // m_consumer->set("1.real_time", -KdenliveSettings::mltthreads()); m_consumer->set("terminate_on_pause", 0); m_consumer->set("1.terminate_on_pause", 0); // m_consumer->set("1.terminate_on_pause", 0);// was commented out. restoring it fixes mantis#3415 - FFmpeg recording freezes QStringList paramList = params.split(' ', QString::SkipEmptyParts); for (int i = 0; i < paramList.count(); ++i) { QString key = "1." + paramList.at(i).section(QLatin1Char('='), 0, 0); QString value = paramList.at(i).section(QLatin1Char('='), 1, 1); if (value == QLatin1String("%threads")) { value = QString::number(QThread::idealThreadCount()); } m_consumer->set(key.toUtf8().constData(), value.toUtf8().constData()); } // Connect the producer to the consumer - tell it to "run" later delete m_displayEvent; // C & D if (m_glslManager) { // D if (m_openGLSync) { m_displayEvent = m_consumer->listen("consumer-frame-show", this, (mlt_listener)on_gl_frame_show); } else { // C m_displayEvent = m_consumer->listen("consumer-frame-show", this, (mlt_listener)on_gl_nosync_frame_show); } } else { // A & B m_displayEvent = m_consumer->listen("consumer-frame-show", this, (mlt_listener)on_frame_show); } m_consumer->connect(*m_producer.get()); m_consumer->start(); return 0; } return -1; } int GLWidget::reconfigure(Mlt::Profile *profile) { int error = 0; // use SDL for audio, OpenGL for video QString serviceName = property("mlt_service").toString(); if (profile) { reloadProfile(); m_blackClip.reset(new Mlt::Producer(*profile, "color:black")); m_blackClip->set("kdenlive:id", "black"); } if ((m_consumer == nullptr) || !m_consumer->is_valid() || strcmp(m_consumer->get("mlt_service"), "multi") == 0) { if (m_consumer) { m_consumer->purge(); m_consumer->stop(); m_consumer.reset(); } QString audioBackend = (KdenliveSettings::external_display()) ? QString("decklink:%1").arg(KdenliveSettings::blackmagic_output_device()) : KdenliveSettings::audiobackend(); if (serviceName.isEmpty() || serviceName != audioBackend) { m_consumer.reset(new Mlt::FilteredConsumer(*m_monitorProfile, audioBackend.toLatin1().constData())); if (m_consumer->is_valid()) { serviceName = audioBackend; setProperty("mlt_service", serviceName); if (KdenliveSettings::external_display()) { m_consumer->set("terminate_on_pause", 0); } } else { // Warning, audio backend unavailable on system m_consumer.reset(); QStringList backends = {"sdl2_audio", "sdl_audio", "rtaudio"}; for (const QString &bk : backends) { if (bk == audioBackend) { // Already tested continue; } m_consumer.reset(new Mlt::FilteredConsumer(*m_monitorProfile, bk.toLatin1().constData())); if (m_consumer->is_valid()) { if (audioBackend == KdenliveSettings::sdlAudioBackend()) { // switch sdl audio backend KdenliveSettings::setSdlAudioBackend(bk); } qDebug() << "++++++++\nSwitching audio backend to: " << bk << "\n++++++++++"; KdenliveSettings::setAudiobackend(bk); serviceName = bk; setProperty("mlt_service", serviceName); break; } else { m_consumer.reset(); } } if (!m_consumer) { qWarning() << "WARNING, NO AUDIO BACKEND FOUND"; return -1; } } } delete m_threadStartEvent; m_threadStartEvent = nullptr; delete m_threadStopEvent; m_threadStopEvent = nullptr; delete m_threadCreateEvent; delete m_threadJoinEvent; if (m_consumer) { m_threadCreateEvent = m_consumer->listen("consumer-thread-create", this, (mlt_listener)onThreadCreate); m_threadJoinEvent = m_consumer->listen("consumer-thread-join", this, (mlt_listener)onThreadJoin); } } if (m_consumer->is_valid()) { // Connect the producer to the consumer - tell it to "run" later if (m_producer) { m_consumer->connect(*m_producer.get()); // m_producer->set_speed(0.0); } int dropFrames = realTime(); if (!KdenliveSettings::monitor_dropframes()) { dropFrames = -dropFrames; } m_consumer->set("real_time", dropFrames); // C & D if (m_glslManager) { if (!m_threadStartEvent) { m_threadStartEvent = m_consumer->listen("consumer-thread-started", this, (mlt_listener)onThreadStarted); } if (!m_threadStopEvent) { m_threadStopEvent = m_consumer->listen("consumer-thread-stopped", this, (mlt_listener)onThreadStopped); } if (!serviceName.startsWith(QLatin1String("decklink"))) { m_consumer->set("mlt_image_format", "glsl"); } } else { // A & B m_consumer->set("mlt_image_format", "yuv422"); } delete m_displayEvent; // C & D if (m_glslManager) { m_displayEvent = m_consumer->listen("consumer-frame-show", this, (mlt_listener)on_gl_frame_show); } else { // A & B m_displayEvent = m_consumer->listen("consumer-frame-show", this, (mlt_listener)on_frame_show); } int volume = KdenliveSettings::volume(); if (serviceName.startsWith(QLatin1String("sdl_audio"))) { QString audioDevice = KdenliveSettings::audiodevicename(); if (!audioDevice.isEmpty()) { m_consumer->set("audio_device", audioDevice.toUtf8().constData()); } QString audioDriver = KdenliveSettings::audiodrivername(); if (!audioDriver.isEmpty()) { m_consumer->set("audio_driver", audioDriver.toUtf8().constData()); } } /*if (!m_monitorProfile->progressive()) m_consumer->set("progressive", property("progressive").toBool());*/ m_consumer->set("volume", volume / 100.0); // m_consumer->set("progressive", 1); m_consumer->set("rescale", KdenliveSettings::mltinterpolation().toUtf8().constData()); m_consumer->set("deinterlace_method", KdenliveSettings::mltdeinterlacer().toUtf8().constData()); /* #ifdef Q_OS_WIN m_consumer->set("audio_buffer", 2048); #else m_consumer->set("audio_buffer", 512); #endif */ m_consumer->set("buffer", 25); m_consumer->set("prefill", 1); m_consumer->set("scrub_audio", 1); if (KdenliveSettings::monitor_gamma() == 0) { m_consumer->set("color_trc", "iec61966_2_1"); } else { m_consumer->set("color_trc", "bt709"); } } else { // Cleanup on error error = 2; } return error; } float GLWidget::zoom() const { return m_zoom; } float GLWidget::scale() const { return (double)m_rect.width() / m_monitorProfile->width() * m_zoom; } Mlt::Profile *GLWidget::profile() { return m_monitorProfile; } void GLWidget::reloadProfile() { auto &profile = pCore->getCurrentProfile(); m_monitorProfile->get_profile()->description = strdup(profile->description().toUtf8().constData()); m_monitorProfile->set_colorspace(profile->colorspace()); m_monitorProfile->set_frame_rate(profile->frame_rate_num(), profile->frame_rate_den()); m_monitorProfile->set_height(profile->height()); m_monitorProfile->set_width(profile->width()); m_monitorProfile->set_progressive(static_cast(profile->progressive())); m_monitorProfile->set_sample_aspect(profile->sample_aspect_num(), profile->sample_aspect_den()); m_monitorProfile->set_display_aspect(profile->display_aspect_num(), profile->display_aspect_den()); m_monitorProfile->set_explicit(1); // The profile display aspect ratio may have changed. resizeGL(width(), height()); refreshSceneLayout(); } QSize GLWidget::profileSize() const { return QSize(m_monitorProfile->width(), m_monitorProfile->height()); } QRect GLWidget::displayRect() const { return m_rect; } QPoint GLWidget::offset() const { return QPoint(m_offset.x() - ((int)((float)m_monitorProfile->width() * m_zoom) - width()) / 2, m_offset.y() - ((int)((float)m_monitorProfile->height() * m_zoom) - height()) / 2); } void GLWidget::setZoom(float zoom) { double zoomRatio = zoom / m_zoom; m_zoom = zoom; emit zoomChanged(); if (rootObject()) { rootObject()->setProperty("zoom", m_zoom); double scalex = rootObject()->property("scalex").toDouble() * zoomRatio; rootObject()->setProperty("scalex", scalex); double scaley = rootObject()->property("scaley").toDouble() * zoomRatio; rootObject()->setProperty("scaley", scaley); } update(); } void GLWidget::onFrameDisplayed(const SharedFrame &frame) { m_contextSharedAccess.lock(); m_sharedFrame = frame; m_sendFrame = sendFrameForAnalysis; m_contextSharedAccess.unlock(); update(); } void GLWidget::mouseReleaseEvent(QMouseEvent *event) { QQuickView::mouseReleaseEvent(event); if (m_dragStart.isNull() && m_panStart.isNull() && (rootObject() != nullptr) && rootObject()->objectName() != QLatin1String("root") && !(event->modifiers() & Qt::ControlModifier)) { event->ignore(); return; } if (!m_dragStart.isNull() && m_panStart.isNull() && ((event->button() & Qt::LeftButton) != 0u) && !event->isAccepted()) { emit monitorPlay(); } m_dragStart = QPoint(); m_panStart = QPoint(); setCursor(Qt::ArrowCursor); } void GLWidget::mouseDoubleClickEvent(QMouseEvent *event) { QQuickView::mouseDoubleClickEvent(event); if (event->isAccepted()) { return; } if ((rootObject() == nullptr) || rootObject()->objectName() != QLatin1String("rooteffectscene")) { emit switchFullScreen(); } event->accept(); } void GLWidget::setOffsetX(int x, int max) { m_offset.setX(x); emit offsetChanged(); if (rootObject()) { rootObject()->setProperty("offsetx", m_zoom > 1.0f ? x - max / 2.0 - 10 : 0); } update(); } void GLWidget::setOffsetY(int y, int max) { m_offset.setY(y); if (rootObject()) { rootObject()->setProperty("offsety", m_zoom > 1.0f ? y - max / 2.0 - 10 : 0); } update(); } int GLWidget::realTime() const { // C & D if (m_glslManager) { return 1; } return KdenliveSettings::mltthreads(); } std::shared_ptr GLWidget::consumer() { return m_consumer; } void GLWidget::updateGamma() { reconfigure(); } void GLWidget::resetConsumer(bool fullReset) { if (fullReset && m_consumer) { m_consumer->purge(); m_consumer->stop(); m_consumer.reset(); } reconfigure(); } const QString GLWidget::sceneList(const QString &root, const QString &fullPath) { QString playlist; qCDebug(KDENLIVE_LOG) << " * * *Setting document xml root: " << root; Mlt::Consumer xmlConsumer(*m_monitorProfile, "xml", fullPath.isEmpty() ? "kdenlive_playlist" : fullPath.toUtf8().constData()); if (!root.isEmpty()) { xmlConsumer.set("root", root.toUtf8().constData()); } if (!xmlConsumer.is_valid()) { return QString(); } m_producer->optimise(); xmlConsumer.set("terminate_on_pause", 1); xmlConsumer.set("store", "kdenlive"); xmlConsumer.set("time_format", "clock"); // Disabling meta creates cleaner files, but then we don't have access to metadata on the fly (meta channels, etc) // And we must use "avformat" instead of "avformat-novalidate" on project loading which causes a big delay on project opening // xmlConsumer.set("no_meta", 1); Mlt::Producer prod(m_producer->get_producer()); if (!prod.is_valid()) { return QString(); } xmlConsumer.connect(prod); xmlConsumer.run(); playlist = fullPath.isEmpty() ? QString::fromUtf8(xmlConsumer.get("kdenlive_playlist")) : fullPath; return playlist; } void GLWidget::updateTexture(GLuint yName, GLuint uName, GLuint vName) { m_texture[0] = yName; m_texture[1] = uName; m_texture[2] = vName; m_sendFrame = sendFrameForAnalysis; // update(); } // MLT consumer-frame-show event handler void GLWidget::on_frame_show(mlt_consumer, void *self, mlt_frame frame_ptr) { Mlt::Frame frame(frame_ptr); if (frame.get_int("rendered") != 0) { GLWidget *widget = static_cast(self); int timeout = (widget->consumer()->get_int("real_time") > 0) ? 0 : 1000; if ((widget->m_frameRenderer != nullptr) && widget->m_frameRenderer->semaphore()->tryAcquire(1, timeout)) { QMetaObject::invokeMethod(widget->m_frameRenderer, "showFrame", Qt::QueuedConnection, Q_ARG(Mlt::Frame, frame)); } } } void GLWidget::on_gl_nosync_frame_show(mlt_consumer, void *self, mlt_frame frame_ptr) { Mlt::Frame frame(frame_ptr); if (frame.get_int("rendered") != 0) { GLWidget *widget = static_cast(self); int timeout = (widget->consumer()->get_int("real_time") > 0) ? 0 : 1000; if ((widget->m_frameRenderer != nullptr) && widget->m_frameRenderer->semaphore()->tryAcquire(1, timeout)) { QMetaObject::invokeMethod(widget->m_frameRenderer, "showGLNoSyncFrame", Qt::QueuedConnection, Q_ARG(Mlt::Frame, frame)); } } } void GLWidget::on_gl_frame_show(mlt_consumer, void *self, mlt_frame frame_ptr) { Mlt::Frame frame(frame_ptr); if (frame.get_int("rendered") != 0) { GLWidget *widget = static_cast(self); int timeout = (widget->consumer()->get_int("real_time") > 0) ? 0 : 1000; if ((widget->m_frameRenderer != nullptr) && widget->m_frameRenderer->semaphore()->tryAcquire(1, timeout)) { QMetaObject::invokeMethod(widget->m_frameRenderer, "showGLFrame", Qt::QueuedConnection, Q_ARG(Mlt::Frame, frame)); } } } RenderThread::RenderThread(thread_function_t function, void *data, QOpenGLContext *context, QSurface *surface) : QThread(nullptr) , m_function(function) , m_data(data) , m_context(nullptr) , m_surface(surface) { if (context) { m_context = new QOpenGLContext; m_context->setFormat(context->format()); m_context->setShareContext(context); m_context->create(); m_context->moveToThread(this); } } RenderThread::~RenderThread() { // would otherwise leak if RenderThread is allocated with a context but not run. // safe post-run delete m_context; } // TODO: missing some exception handling? void RenderThread::run() { if (m_context) { m_context->makeCurrent(m_surface); } m_function(m_data); if (m_context) { m_context->doneCurrent(); delete m_context; m_context = nullptr; } } FrameRenderer::FrameRenderer(QOpenGLContext *shareContext, QSurface *surface, GLWidget::ClientWaitSync_fp clientWaitSync) : QThread(nullptr) , m_semaphore(3) , m_context(nullptr) , m_surface(surface) , m_ClientWaitSync(clientWaitSync) , m_gl32(nullptr) , sendAudioForAnalysis(false) { Q_ASSERT(shareContext); m_renderTexture[0] = m_renderTexture[1] = m_renderTexture[2] = 0; m_displayTexture[0] = m_displayTexture[1] = m_displayTexture[2] = 0; // B & C & D if (KdenliveSettings::gpu_accel() || shareContext->supportsThreadedOpenGL()) { m_context = new QOpenGLContext; m_context->setFormat(shareContext->format()); m_context->setShareContext(shareContext); m_context->create(); m_context->moveToThread(this); } setObjectName(QStringLiteral("FrameRenderer")); moveToThread(this); start(); } FrameRenderer::~FrameRenderer() { delete m_context; delete m_gl32; } void FrameRenderer::showFrame(Mlt::Frame frame) { int width = 0; int height = 0; mlt_image_format format = mlt_image_yuv420p; frame.get_image(format, width, height); // Save this frame for future use and to keep a reference to the GL Texture. m_displayFrame = SharedFrame(frame); if ((m_context != nullptr) && m_context->isValid()) { m_context->makeCurrent(m_surface); // Upload each plane of YUV to a texture. QOpenGLFunctions *f = m_context->functions(); uploadTextures(m_context, m_displayFrame, m_renderTexture); f->glBindTexture(GL_TEXTURE_2D, 0); check_error(f); f->glFinish(); for (int i = 0; i < 3; ++i) { std::swap(m_renderTexture[i], m_displayTexture[i]); } emit textureReady(m_displayTexture[0], m_displayTexture[1], m_displayTexture[2]); m_context->doneCurrent(); } // The frame is now done being modified and can be shared with the rest // of the application. emit frameDisplayed(m_displayFrame); m_semaphore.release(); } void FrameRenderer::showGLFrame(Mlt::Frame frame) { if ((m_context != nullptr) && m_context->isValid()) { int width = 0; int height = 0; frame.set("movit.convert.use_texture", 1); mlt_image_format format = mlt_image_glsl_texture; frame.get_image(format, width, height); m_context->makeCurrent(m_surface); pipelineSyncToFrame(frame); m_context->functions()->glFinish(); m_context->doneCurrent(); // Save this frame for future use and to keep a reference to the GL Texture. m_displayFrame = SharedFrame(frame); } // The frame is now done being modified and can be shared with the rest // of the application. emit frameDisplayed(m_displayFrame); m_semaphore.release(); } void FrameRenderer::showGLNoSyncFrame(Mlt::Frame frame) { if ((m_context != nullptr) && m_context->isValid()) { int width = 0; int height = 0; frame.set("movit.convert.use_texture", 1); mlt_image_format format = mlt_image_glsl_texture; frame.get_image(format, width, height); m_context->makeCurrent(m_surface); m_context->functions()->glFinish(); m_context->doneCurrent(); // Save this frame for future use and to keep a reference to the GL Texture. m_displayFrame = SharedFrame(frame); } // The frame is now done being modified and can be shared with the rest // of the application. emit frameDisplayed(m_displayFrame); m_semaphore.release(); } void FrameRenderer::cleanup() { if ((m_renderTexture[0] != 0u) && (m_renderTexture[1] != 0u) && (m_renderTexture[2] != 0u)) { m_context->makeCurrent(m_surface); m_context->functions()->glDeleteTextures(3, m_renderTexture); if ((m_displayTexture[0] != 0u) && (m_displayTexture[1] != 0u) && (m_displayTexture[2] != 0u)) { m_context->functions()->glDeleteTextures(3, m_displayTexture); } m_context->doneCurrent(); m_renderTexture[0] = m_renderTexture[1] = m_renderTexture[2] = 0; m_displayTexture[0] = m_displayTexture[1] = m_displayTexture[2] = 0; } } // D void FrameRenderer::pipelineSyncToFrame(Mlt::Frame &frame) { GLsync sync = (GLsync)frame.get_data("movit.convert.fence"); if (!sync) return; #ifdef Q_OS_WIN // On Windows, use QOpenGLFunctions_3_2_Core instead of getProcAddress. // TODO: move to initialization of m_ClientWaitSync if (!m_gl32) { m_gl32 = m_context->versionFunctions(); if (m_gl32) { m_gl32->initializeOpenGLFunctions(); } } if (m_gl32) { m_gl32->glClientWaitSync(sync, 0, GL_TIMEOUT_IGNORED); check_error(m_context->functions()); } #else if (m_ClientWaitSync) { m_ClientWaitSync(sync, 0, GL_TIMEOUT_IGNORED); check_error(m_context->functions()); } #endif // Q_OS_WIN } void GLWidget::setAudioThumb(int channels, const QVariantList &audioCache) { if (!rootObject()) return; QmlAudioThumb *audioThumbDisplay = rootObject()->findChild(QStringLiteral("audiothumb")); if (!audioThumbDisplay) return; QImage img(width(), height() / 6, QImage::Format_ARGB32_Premultiplied); img.fill(Qt::transparent); if (!audioCache.isEmpty() && channels > 0) { int audioLevelCount = audioCache.count() - 1; // simplified audio QPainter painter(&img); QRectF mappedRect(0, 0, img.width(), img.height()); int channelHeight = mappedRect.height(); double value; double scale = (double)width() / (audioLevelCount / channels); if (scale < 1) { painter.setPen(QColor(80, 80, 150, 200)); for (int i = 0; i < img.width(); i++) { int framePos = i / scale; value = audioCache.at(qMin(framePos * channels, audioLevelCount)).toDouble() / 256; for (int channel = 1; channel < channels; channel++) { value = qMax(value, audioCache.at(qMin(framePos * channels + channel, audioLevelCount)).toDouble() / 256); } painter.drawLine(i, mappedRect.bottom() - (value * channelHeight), i, mappedRect.bottom()); } } else { QPainterPath positiveChannelPath; positiveChannelPath.moveTo(0, mappedRect.bottom()); for (int i = 0; i < audioLevelCount / channels; i++) { value = audioCache.at(qMin(i * channels, audioLevelCount)).toDouble() / 256; for (int channel = 1; channel < channels; channel++) { value = qMax(value, audioCache.at(qMin(i * channels + channel, audioLevelCount)).toDouble() / 256); } positiveChannelPath.lineTo(i * scale, mappedRect.bottom() - (value * channelHeight)); } positiveChannelPath.lineTo(mappedRect.right(), mappedRect.bottom()); painter.setPen(Qt::NoPen); painter.setBrush(QBrush(QColor(80, 80, 150, 200))); painter.drawPath(positiveChannelPath); } painter.end(); } audioThumbDisplay->setImage(img); } void GLWidget::refreshSceneLayout() { if (!rootObject()) { return; } rootObject()->setProperty("profile", QPoint(m_monitorProfile->width(), m_monitorProfile->height())); rootObject()->setProperty("scalex", (double)m_rect.width() / m_monitorProfile->width() * m_zoom); rootObject()->setProperty("scaley", (double)m_rect.width() / (((double)m_monitorProfile->height() * m_monitorProfile->dar() / m_monitorProfile->width())) / m_monitorProfile->width() * m_zoom); } void GLWidget::switchPlay(bool play, double speed) { m_proxy->setSeekPosition(-1); if (!m_producer || !m_consumer) { return; } if (m_isZoneMode) { resetZoneMode(); } if (play) { if (m_id == Kdenlive::ClipMonitor && m_consumer->position() == m_producer->get_out()) { m_producer->seek(0); } m_producer->set_speed(speed); m_consumer->start(); m_consumer->set("refresh", 1); } else { m_producer->set_speed(0); m_producer->seek(m_consumer->position() + 1); m_consumer->purge(); m_consumer->start(); } } bool GLWidget::playZone(bool loop) { if (!m_producer || m_proxy->zoneOut() <= m_proxy->zoneIn()) { pCore->displayMessage(i18n("Select a zone to play"), InformationMessage, 500); return false; } m_proxy->setSeekPosition(-1); m_producer->seek(m_proxy->zoneIn()); m_producer->set_speed(0); m_consumer->purge(); m_producer->set("out", m_proxy->zoneOut()); m_producer->set_speed(1.0); if (m_consumer->is_stopped()) { m_consumer->start(); } m_consumer->set("refresh", 1); m_isZoneMode = true; m_isLoopMode = loop; return true; } bool GLWidget::loopClip() { if (!m_producer || m_proxy->zoneOut() <= m_proxy->zoneIn()) { pCore->displayMessage(i18n("Select a zone to play"), InformationMessage, 500); return false; } m_proxy->setSeekPosition(-1); m_producer->seek(0); m_producer->set_speed(0); m_consumer->purge(); m_producer->set("out", m_producer->get_playtime()); m_producer->set_speed(1.0); if (m_consumer->is_stopped()) { m_consumer->start(); } m_consumer->set("refresh", 1); m_isZoneMode = true; m_isLoopMode = true; return true; } void GLWidget::resetZoneMode() { if (!m_isZoneMode && !m_isLoopMode) { return; } m_producer->set("out", m_producer->get_length()); m_isZoneMode = false; m_isLoopMode = false; } MonitorProxy *GLWidget::getControllerProxy() { return m_proxy; } int GLWidget::getCurrentPos() const { return m_proxy->seeking() ? m_proxy->seekPosition() : m_consumer->position(); } -void GLWidget::setRulerInfo(int duration, std::shared_ptr model) +void GLWidget::setRulerInfo(int duration, const std::shared_ptr &model) { rootObject()->setProperty("duration", duration); if (model != nullptr) { // we are resetting marker/snap model, reset zone rootContext()->setContextProperty("markersModel", model.get()); } } void GLWidget::startConsumer() { if (m_consumer == nullptr) { return; } if (m_consumer->is_stopped() && m_consumer->start() == -1) { // ARGH CONSUMER BROKEN!!!! KMessageBox::error( qApp->activeWindow(), i18n("Could not create the video preview window.\nThere is something wrong with your Kdenlive install or your driver settings, please fix it.")); if (m_displayEvent) { delete m_displayEvent; } m_displayEvent = nullptr; m_consumer.reset(); return; } m_consumer->set("refresh", 1); } void GLWidget::stop() { m_refreshTimer.stop(); m_proxy->setSeekPosition(-1); // why this lock? QMutexLocker locker(&m_mltMutex); if (m_producer) { if (m_isZoneMode) { resetZoneMode(); } m_producer->set_speed(0.0); } if (m_consumer) { m_consumer->purge(); if (!m_consumer->is_stopped()) { m_consumer->stop(); } } } double GLWidget::playSpeed() const { if (m_producer) { return m_producer->get_speed(); } return 0.0; } void GLWidget::setDropFrames(bool drop) { // why this lock? QMutexLocker locker(&m_mltMutex); if (m_consumer) { int dropFrames = realTime(); if (!drop) { dropFrames = -dropFrames; } m_consumer->stop(); m_consumer->set("real_time", dropFrames); if (m_consumer->start() == -1) { qCWarning(KDENLIVE_LOG) << "ERROR, Cannot start monitor"; } } } int GLWidget::volume() const { if ((!m_consumer) || (!m_producer)) { return -1; } if (m_consumer->get("mlt_service") == QStringLiteral("multi")) { return ((int)100 * m_consumer->get_double("0.volume")); } return ((int)100 * m_consumer->get_double("volume")); } void GLWidget::setVolume(double volume) { if (m_consumer) { if (m_consumer->get("mlt_service") == QStringLiteral("multi")) { m_consumer->set("0.volume", volume); } else { m_consumer->set("volume", volume); } } } int GLWidget::duration() const { if (!m_producer) { return 0; } return m_producer->get_playtime(); } void GLWidget::setConsumerProperty(const QString &name, const QString &value) { QMutexLocker locker(&m_mltMutex); if (m_consumer) { m_consumer->set(name.toUtf8().constData(), value.toUtf8().constData()); if (m_consumer->start() == -1) { qCWarning(KDENLIVE_LOG) << "ERROR, Cannot start monitor"; } } } diff --git a/src/monitor/glwidget.h b/src/monitor/glwidget.h index 4a40b3a69..f0f420f6f 100644 --- a/src/monitor/glwidget.h +++ b/src/monitor/glwidget.h @@ -1,342 +1,342 @@ /* * Copyright (c) 2011-2014 Meltytech, LLC * Author: Dan Dennedy * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef GLWIDGET_H #define GLWIDGET_H #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "bin/model/markerlistmodel.hpp" #include "definitions.h" #include "kdenlivesettings.h" #include "scopes/sharedframe.h" class QOpenGLFunctions_3_2_Core; namespace Mlt { class Filter; class Producer; class Consumer; class Profile; } // namespace Mlt class RenderThread; class FrameRenderer; class MonitorProxy; typedef void *(*thread_function_t)(void *); /* QQuickView that renders an . * * Creates an MLT consumer and renders a GL view from the consumer. This pipeline is one of: * * A. YUV gl texture w/o GPU filter acceleration * B. YUV gl texture multithreaded w/o GPU filter acceleration * C. RGB gl texture multithreaded w/ GPU filter acceleration and no sync * D. RGB gl texture multithreaded w/ GPU filter acceleration and sync */ class GLWidget : public QQuickView, protected QOpenGLFunctions { Q_OBJECT Q_PROPERTY(QRect rect READ rect NOTIFY rectChanged) Q_PROPERTY(float zoom READ zoom NOTIFY zoomChanged) Q_PROPERTY(QPoint offset READ offset NOTIFY offsetChanged) public: friend class MonitorController; friend class Monitor; friend class MonitorProxy; using ClientWaitSync_fp = GLenum (*)(GLsync, GLbitfield, GLuint64); GLWidget(int id, QObject *parent = nullptr); ~GLWidget(); int requestedSeekPosition; void createThread(RenderThread **thread, thread_function_t function, void *data); void startGlsl(); void stopGlsl(); void clear(); // TODO: currently unused int reconfigureMulti(const QString ¶ms, const QString &path, Mlt::Profile *profile); void stopCapture(); int reconfigure(Mlt::Profile *profile = nullptr); /** @brief Get the current MLT producer playlist. * @return A string describing the playlist */ const QString sceneList(const QString &root, const QString &fullPath = QString()); int displayWidth() const { return m_rect.width(); } void updateAudioForAnalysis(); int displayHeight() const { return m_rect.height(); } QObject *videoWidget() { return this; } Mlt::Filter *glslManager() const { return m_glslManager; } QRect rect() const { return m_rect; } QRect effectRect() const { return m_effectRect; } float zoom() const; float scale() const; QPoint offset() const; std::shared_ptr consumer(); Mlt::Producer *producer(); QSize profileSize() const; QRect displayRect() const; /** @brief set to true if we want to emit a QImage of the frame for analysis */ bool sendFrameForAnalysis; void updateGamma(); /** @brief delete and rebuild consumer, for example when external display is switched */ void resetConsumer(bool fullReset); Mlt::Profile *profile(); void reloadProfile(); void lockMonitor(); void releaseMonitor(); int realTime() const; void setAudioThumb(int channels = 0, const QVariantList &audioCache = QList()); int droppedFrames() const; void resetDrops(); bool checkFrameNumber(int pos, int offset); /** @brief Return current timeline position */ int getCurrentPos() const; /** @brief Requests a monitor refresh */ void requestRefresh(); - void setRulerInfo(int duration, std::shared_ptr model = nullptr); + void setRulerInfo(int duration, const std::shared_ptr &model = nullptr); MonitorProxy *getControllerProxy(); bool playZone(bool loop = false); bool loopClip(); void startConsumer(); void stop(); int rulerHeight() const; /** @brief return current play producer's playing speed */ double playSpeed() const; /** @brief Turn drop frame feature on/off */ void setDropFrames(bool drop); /** @brief Returns current audio volume */ int volume() const; /** @brief Set audio volume on consumer */ void setVolume(double volume); /** @brief Returns current producer's duration in frames */ int duration() const; /** @brief Set a property on the MLT consumer */ void setConsumerProperty(const QString &name, const QString &value); protected: void mouseReleaseEvent(QMouseEvent *event) override; void mouseDoubleClickEvent(QMouseEvent *event) override; void wheelEvent(QWheelEvent *event) override; /** @brief Update producer, should ONLY be called from monitor */ - int setProducer(std::shared_ptr producer, bool isActive, int position = -1); + int setProducer(const std::shared_ptr &producer, bool isActive, int position = -1); QString frameToTime(int frames) const; public slots: void seek(int pos); void requestSeek(); void setZoom(float zoom); void setOffsetX(int x, int max); void setOffsetY(int y, int max); void slotSwitchAudioOverlay(bool enable); void slotZoom(bool zoomIn); void initializeGL(); void releaseAnalyse(); void switchPlay(bool play, double speed = 1.0); signals: void frameDisplayed(const SharedFrame &frame); void dragStarted(); void seekTo(int x); void gpuNotSupported(); void started(); void paused(); void playing(); void rectChanged(); void zoomChanged(); void offsetChanged(); void monitorPlay(); void switchFullScreen(bool minimizeOnly = false); void mouseSeek(int eventDelta, uint modifiers); void startDrag(); void analyseFrame(const QImage &); void audioSamplesSignal(const audioShortVector &, int, int, int); void showContextMenu(const QPoint &); void lockMonitor(bool); void passKeyEvent(QKeyEvent *); void panView(const QPoint &diff); void seekPosition(int); void consumerPosition(int); void activateMonitor(); protected: Mlt::Filter *m_glslManager; // TODO: MTL has lock/unlock of individual nodes. Use those. // keeping this for refactoring ease. QMutex m_mltMutex; std::shared_ptr m_consumer; std::shared_ptr m_producer; Mlt::Profile *m_monitorProfile; int m_id; int m_rulerHeight; private: QRect m_rect; QRect m_effectRect; GLuint m_texture[3]; QOpenGLShaderProgram *m_shader; QPoint m_panStart; QPoint m_dragStart; QSemaphore m_initSem; QSemaphore m_analyseSem; bool m_isInitialized; Mlt::Event *m_threadStartEvent; Mlt::Event *m_threadStopEvent; Mlt::Event *m_threadCreateEvent; Mlt::Event *m_threadJoinEvent; Mlt::Event *m_displayEvent; FrameRenderer *m_frameRenderer; int m_projectionLocation; int m_modelViewLocation; int m_vertexLocation; int m_texCoordLocation; int m_colorspaceLocation; int m_textureLocation[3]; QTimer m_refreshTimer; float m_zoom; bool m_sendFrame; bool m_isZoneMode; bool m_isLoopMode; QPoint m_offset; bool m_audioWaveDisplayed; MonitorProxy *m_proxy; std::shared_ptr m_blackClip; static void on_frame_show(mlt_consumer, void *self, mlt_frame frame); static void on_gl_frame_show(mlt_consumer, void *self, mlt_frame frame_ptr); static void on_gl_nosync_frame_show(mlt_consumer, void *self, mlt_frame frame_ptr); void createAudioOverlay(bool isAudio); void removeAudioOverlay(); void adjustAudioOverlay(bool isAudio); QOpenGLFramebufferObject *m_fbo; void refreshSceneLayout(); void resetZoneMode(); /* OpenGL context management. Interfaces to MLT according to the configured render pipeline. */ private slots: void resizeGL(int width, int height); void updateTexture(GLuint yName, GLuint uName, GLuint vName); void paintGL(); void onFrameDisplayed(const SharedFrame &frame); void refresh(); protected: QMutex m_contextSharedAccess; QOffscreenSurface m_offscreenSurface; SharedFrame m_sharedFrame; QOpenGLContext *m_shareContext; bool acquireSharedFrameTextures(); void bindShaderProgram(); void createGPUAccelFragmentProg(); void createShader(); void createYUVTextureProjectFragmentProg(); void disableGPUAccel(); void releaseSharedFrameTextures(); // pipeline A - YUV gl texture w/o GPU filter acceleration // pipeline B - YUV gl texture multithreaded w/o GPU filter acceleration // pipeline C - RGB gl texture multithreaded w/ GPU filter acceleration and no sync // pipeline D - RGB gl texture multithreaded w/ GPU filter acceleration and sync bool m_openGLSync; bool initGPUAccelSync(); // pipeline C & D bool initGPUAccel(); bool onlyGLESGPUAccel() const; // pipeline A & B & C & D // not null iff D ClientWaitSync_fp m_ClientWaitSync; protected: void resizeEvent(QResizeEvent *event) override; void mousePressEvent(QMouseEvent *) override; void mouseMoveEvent(QMouseEvent *) override; void keyPressEvent(QKeyEvent *event) override; }; class RenderThread : public QThread { Q_OBJECT public: RenderThread(thread_function_t function, void *data, QOpenGLContext *context, QSurface *surface); ~RenderThread(); protected: void run() override; private: thread_function_t m_function; void *m_data; QOpenGLContext *m_context; QSurface *m_surface; }; class FrameRenderer : public QThread { Q_OBJECT public: explicit FrameRenderer(QOpenGLContext *shareContext, QSurface *surface, GLWidget::ClientWaitSync_fp clientWaitSync); ~FrameRenderer(); QSemaphore *semaphore() { return &m_semaphore; } QOpenGLContext *context() const { return m_context; } Q_INVOKABLE void showFrame(Mlt::Frame frame); Q_INVOKABLE void showGLFrame(Mlt::Frame frame); Q_INVOKABLE void showGLNoSyncFrame(Mlt::Frame frame); public slots: void cleanup(); signals: void textureReady(GLuint yName, GLuint uName = 0, GLuint vName = 0); void frameDisplayed(const SharedFrame &frame); void audioSamplesSignal(const audioShortVector &, int, int, int); private: QSemaphore m_semaphore; SharedFrame m_displayFrame; QOpenGLContext *m_context; QSurface *m_surface; GLWidget::ClientWaitSync_fp m_ClientWaitSync; void pipelineSyncToFrame(Mlt::Frame &); public: GLuint m_renderTexture[3]; GLuint m_displayTexture[3]; QOpenGLFunctions_3_2_Core *m_gl32; bool sendAudioForAnalysis; }; #endif diff --git a/src/monitor/monitor.cpp b/src/monitor/monitor.cpp index a6f0f53fd..937fd5b3f 100644 --- a/src/monitor/monitor.cpp +++ b/src/monitor/monitor.cpp @@ -1,2142 +1,2142 @@ /*************************************************************************** * Copyright (C) 2007 by Jean-Baptiste Mardelle (jb@kdenlive.org) * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "monitor.h" #include "bin/bin.h" #include "bin/projectclip.h" #include "core.h" #include "dialogs/profilesdialog.h" #include "doc/kdenlivedoc.h" #include "doc/kthumb.h" #include "glwidget.h" #include "kdenlivesettings.h" #include "lib/audio/audioStreamInfo.h" #include "mainwindow.h" #include "mltcontroller/clipcontroller.h" #include "monitorproxy.h" #include "project/projectmanager.h" #include "qmlmanager.h" #include "recmanager.h" #include "scopes/monitoraudiolevel.h" #include "timeline2/model/snapmodel.hpp" #include "transitions/transitionsrepository.hpp" #include "klocalizedstring.h" #include #include #include #include #include #include #include #include #include "kdenlive_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include - +#include #define SEEK_INACTIVE (-1) QuickEventEater::QuickEventEater(QObject *parent) : QObject(parent) { } bool QuickEventEater::eventFilter(QObject *obj, QEvent *event) { switch (event->type()) { case QEvent::DragEnter: { QDragEnterEvent *ev = reinterpret_cast(event); if (ev->mimeData()->hasFormat(QStringLiteral("kdenlive/effect"))) { ev->acceptProposedAction(); return true; } break; } case QEvent::DragMove: { QDragEnterEvent *ev = reinterpret_cast(event); if (ev->mimeData()->hasFormat(QStringLiteral("kdenlive/effect"))) { ev->acceptProposedAction(); return true; } break; } case QEvent::Drop: { QDropEvent *ev = static_cast(event); if (ev) { QStringList effectData; effectData << QString::fromUtf8(ev->mimeData()->data(QStringLiteral("kdenlive/effect"))); QStringList source = QString::fromUtf8(ev->mimeData()->data(QStringLiteral("kdenlive/effectsource"))).split(QLatin1Char('-')); effectData << source; emit addEffect(effectData); ev->accept(); return true; } break; } default: break; } return QObject::eventFilter(obj, event); } QuickMonitorEventEater::QuickMonitorEventEater(QWidget *parent) : QObject(parent) { } bool QuickMonitorEventEater::eventFilter(QObject *obj, QEvent *event) { if (event->type() == QEvent::KeyPress) { QKeyEvent *ev = static_cast(event); if (ev) { emit doKeyPressEvent(ev); return true; } } return QObject::eventFilter(obj, event); } Monitor::Monitor(Kdenlive::MonitorId id, MonitorManager *manager, QWidget *parent) : AbstractMonitor(id, manager, parent) , m_controller(nullptr) , m_glMonitor(nullptr) , m_snaps(new SnapModel()) , m_splitEffect(nullptr) , m_splitProducer(nullptr) , m_dragStarted(false) , m_recManager(nullptr) , m_loopClipAction(nullptr) , m_sceneVisibilityAction(nullptr) , m_multitrackView(nullptr) , m_contextMenu(nullptr) , m_loopClipTransition(true) , m_editMarker(nullptr) , m_forceSizeFactor(0) , m_lastMonitorSceneType(MonitorSceneDefault) { auto *layout = new QVBoxLayout; layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(0); // Create container widget m_glWidget = new QWidget; auto *glayout = new QGridLayout(m_glWidget); glayout->setSpacing(0); glayout->setContentsMargins(0, 0, 0, 0); // Create QML OpenGL widget m_glMonitor = new GLWidget((int)id); connect(m_glMonitor, &GLWidget::passKeyEvent, this, &Monitor::doKeyPressEvent); connect(m_glMonitor, &GLWidget::panView, this, &Monitor::panView); connect(m_glMonitor, &GLWidget::seekPosition, this, &Monitor::seekPosition, Qt::DirectConnection); connect(m_glMonitor, &GLWidget::consumerPosition, this, &Monitor::slotSeekPosition, Qt::DirectConnection); connect(m_glMonitor, &GLWidget::activateMonitor, this, &AbstractMonitor::slotActivateMonitor, Qt::DirectConnection); m_videoWidget = QWidget::createWindowContainer(qobject_cast(m_glMonitor)); m_videoWidget->setAcceptDrops(true); auto *leventEater = new QuickEventEater(this); m_videoWidget->installEventFilter(leventEater); connect(leventEater, &QuickEventEater::addEffect, this, &Monitor::slotAddEffect); m_qmlManager = new QmlManager(m_glMonitor); connect(m_qmlManager, &QmlManager::effectChanged, this, &Monitor::effectChanged); connect(m_qmlManager, &QmlManager::effectPointsChanged, this, &Monitor::effectPointsChanged); auto *monitorEventEater = new QuickMonitorEventEater(this); m_glWidget->installEventFilter(monitorEventEater); connect(monitorEventEater, &QuickMonitorEventEater::doKeyPressEvent, this, &Monitor::doKeyPressEvent); glayout->addWidget(m_videoWidget, 0, 0); m_verticalScroll = new QScrollBar(Qt::Vertical); glayout->addWidget(m_verticalScroll, 0, 1); m_verticalScroll->hide(); m_horizontalScroll = new QScrollBar(Qt::Horizontal); glayout->addWidget(m_horizontalScroll, 1, 0); m_horizontalScroll->hide(); connect(m_horizontalScroll, &QAbstractSlider::valueChanged, this, &Monitor::setOffsetX); connect(m_verticalScroll, &QAbstractSlider::valueChanged, this, &Monitor::setOffsetY); connect(m_glMonitor, &GLWidget::frameDisplayed, this, &Monitor::onFrameDisplayed); connect(m_glMonitor, &GLWidget::mouseSeek, this, &Monitor::slotMouseSeek); connect(m_glMonitor, &GLWidget::monitorPlay, this, &Monitor::slotPlay); connect(m_glMonitor, &GLWidget::startDrag, this, &Monitor::slotStartDrag); connect(m_glMonitor, &GLWidget::switchFullScreen, this, &Monitor::slotSwitchFullScreen); connect(m_glMonitor, &GLWidget::zoomChanged, this, &Monitor::setZoom); connect(m_glMonitor, SIGNAL(lockMonitor(bool)), this, SLOT(slotLockMonitor(bool)), Qt::DirectConnection); connect(m_glMonitor, &GLWidget::showContextMenu, this, &Monitor::slotShowMenu); connect(m_glMonitor, &GLWidget::gpuNotSupported, this, &Monitor::gpuError); m_glWidget->setMinimumSize(QSize(320, 180)); layout->addWidget(m_glWidget, 10); layout->addStretch(); // Tool bar buttons m_toolbar = new QToolBar(this); QWidget *sp1 = new QWidget(this); sp1->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); m_toolbar->addWidget(sp1); if (id == Kdenlive::ClipMonitor) { // Add options for recording m_recManager = new RecManager(this); connect(m_recManager, &RecManager::warningMessage, this, &Monitor::warningMessage); connect(m_recManager, &RecManager::addClipToProject, this, &Monitor::addClipToProject); m_toolbar->addAction(manager->getAction(QStringLiteral("insert_project_tree"))); m_toolbar->setToolTip(i18n("Insert Zone to Project Bin")); m_toolbar->addSeparator(); } if (id != Kdenlive::DvdMonitor) { m_toolbar->addAction(manager->getAction(QStringLiteral("mark_in"))); m_toolbar->addAction(manager->getAction(QStringLiteral("mark_out"))); } m_toolbar->addAction(manager->getAction(QStringLiteral("monitor_seek_backward"))); auto *playButton = new QToolButton(m_toolbar); m_playMenu = new QMenu(i18n("Play..."), this); QAction *originalPlayAction = static_cast(manager->getAction(QStringLiteral("monitor_play"))); m_playAction = new KDualAction(i18n("Play"), i18n("Pause"), this); m_playAction->setInactiveIcon(QIcon::fromTheme(QStringLiteral("media-playback-start"))); m_playAction->setActiveIcon(QIcon::fromTheme(QStringLiteral("media-playback-pause"))); QString strippedTooltip = m_playAction->toolTip().remove(QRegExp(QStringLiteral("\\s\\(.*\\)"))); // append shortcut if it exists for action if (originalPlayAction->shortcut() == QKeySequence(0)) { m_playAction->setToolTip(strippedTooltip); } else { m_playAction->setToolTip(strippedTooltip + QStringLiteral(" (") + originalPlayAction->shortcut().toString() + QLatin1Char(')')); } m_playMenu->addAction(m_playAction); connect(m_playAction, &QAction::triggered, this, &Monitor::slotSwitchPlay); playButton->setMenu(m_playMenu); playButton->setPopupMode(QToolButton::MenuButtonPopup); m_toolbar->addWidget(playButton); m_toolbar->addAction(manager->getAction(QStringLiteral("monitor_seek_forward"))); playButton->setDefaultAction(m_playAction); m_configMenu = new QMenu(i18n("Misc..."), this); if (id != Kdenlive::DvdMonitor) { if (id == Kdenlive::ClipMonitor) { m_markerMenu = new QMenu(i18n("Go to marker..."), this); } else { m_markerMenu = new QMenu(i18n("Go to guide..."), this); } m_markerMenu->setEnabled(false); m_configMenu->addMenu(m_markerMenu); connect(m_markerMenu, &QMenu::triggered, this, &Monitor::slotGoToMarker); m_forceSize = new KSelectAction(QIcon::fromTheme(QStringLiteral("transform-scale")), i18n("Force Monitor Size"), this); QAction *fullAction = m_forceSize->addAction(QIcon(), i18n("Force 100%")); fullAction->setData(100); QAction *halfAction = m_forceSize->addAction(QIcon(), i18n("Force 50%")); halfAction->setData(50); QAction *freeAction = m_forceSize->addAction(QIcon(), i18n("Free Resize")); freeAction->setData(0); m_configMenu->addAction(m_forceSize); m_forceSize->setCurrentAction(freeAction); connect(m_forceSize, static_cast(&KSelectAction::triggered), this, &Monitor::slotForceSize); } // Create Volume slider popup m_audioSlider = new QSlider(Qt::Vertical); m_audioSlider->setRange(0, 100); m_audioSlider->setValue(100); connect(m_audioSlider, &QSlider::valueChanged, this, &Monitor::slotSetVolume); auto *widgetslider = new QWidgetAction(this); widgetslider->setText(i18n("Audio volume")); widgetslider->setDefaultWidget(m_audioSlider); auto *menu = new QMenu(this); menu->addAction(widgetslider); m_audioButton = new QToolButton(this); m_audioButton->setMenu(menu); m_audioButton->setToolTip(i18n("Volume")); m_audioButton->setPopupMode(QToolButton::InstantPopup); QIcon icon; if (KdenliveSettings::volume() == 0) { icon = QIcon::fromTheme(QStringLiteral("audio-volume-muted")); } else { icon = QIcon::fromTheme(QStringLiteral("audio-volume-medium")); } m_audioButton->setIcon(icon); m_toolbar->addWidget(m_audioButton); setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); setLayout(layout); setMinimumHeight(200); connect(this, &Monitor::scopesClear, m_glMonitor, &GLWidget::releaseAnalyse, Qt::DirectConnection); connect(m_glMonitor, &GLWidget::analyseFrame, this, &Monitor::frameUpdated); connect(m_glMonitor, &GLWidget::audioSamplesSignal, this, &Monitor::audioSamplesSignal); if (id != Kdenlive::ClipMonitor) { // TODO: reimplement // connect(render, &Render::durationChanged, this, &Monitor::durationChanged); connect(m_glMonitor->getControllerProxy(), &MonitorProxy::saveZone, this, &Monitor::updateTimelineClipZone); } else { connect(m_glMonitor->getControllerProxy(), &MonitorProxy::saveZone, this, &Monitor::updateClipZone); } connect(m_glMonitor->getControllerProxy(), &MonitorProxy::triggerAction, pCore.get(), &Core::triggerAction); connect(m_glMonitor->getControllerProxy(), &MonitorProxy::seekNextKeyframe, this, &Monitor::seekToNextKeyframe); connect(m_glMonitor->getControllerProxy(), &MonitorProxy::seekPreviousKeyframe, this, &Monitor::seekToPreviousKeyframe); connect(m_glMonitor->getControllerProxy(), &MonitorProxy::addRemoveKeyframe, this, &Monitor::addRemoveKeyframe); connect(m_glMonitor->getControllerProxy(), &MonitorProxy::seekToKeyframe, this, &Monitor::slotSeekToKeyFrame); m_sceneVisibilityAction = new QAction(QIcon::fromTheme(QStringLiteral("transform-crop")), i18n("Show/Hide edit mode"), this); m_sceneVisibilityAction->setCheckable(true); m_sceneVisibilityAction->setChecked(KdenliveSettings::showOnMonitorScene()); connect(m_sceneVisibilityAction, &QAction::triggered, this, &Monitor::slotEnableEffectScene); m_toolbar->addAction(m_sceneVisibilityAction); m_toolbar->addSeparator(); m_timePos = new TimecodeDisplay(m_monitorManager->timecode(), this); m_toolbar->addWidget(m_timePos); auto *configButton = new QToolButton(m_toolbar); configButton->setIcon(QIcon::fromTheme(QStringLiteral("kdenlive-menu"))); configButton->setToolTip(i18n("Options")); configButton->setMenu(m_configMenu); configButton->setPopupMode(QToolButton::InstantPopup); m_toolbar->addWidget(configButton); if (m_recManager) { m_toolbar->addAction(m_recManager->switchAction()); } /*QWidget *spacer = new QWidget(this); spacer->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); m_toolbar->addWidget(spacer);*/ m_toolbar->addSeparator(); int tm = 0; int bm = 0; m_toolbar->getContentsMargins(nullptr, &tm, nullptr, &bm); m_audioMeterWidget = new MonitorAudioLevel(m_glMonitor->profile(), m_toolbar->height() - tm - bm, this); m_toolbar->addWidget(m_audioMeterWidget); if (!m_audioMeterWidget->isValid) { KdenliveSettings::setMonitoraudio(0x01); m_audioMeterWidget->setVisibility(false); } else { m_audioMeterWidget->setVisibility((KdenliveSettings::monitoraudio() & m_id) != 0); } connect(m_timePos, SIGNAL(timeCodeEditingFinished()), this, SLOT(slotSeek())); layout->addWidget(m_toolbar); if (m_recManager) { layout->addWidget(m_recManager->toolbar()); } // Load monitor overlay qml loadQmlScene(MonitorSceneDefault); // Info message widget m_infoMessage = new KMessageWidget(this); layout->addWidget(m_infoMessage); m_infoMessage->hide(); } Monitor::~Monitor() { delete m_splitEffect; delete m_audioMeterWidget; delete m_glMonitor; delete m_videoWidget; delete m_glWidget; delete m_timePos; } void Monitor::setOffsetX(int x) { m_glMonitor->setOffsetX(x, m_horizontalScroll->maximum()); } void Monitor::setOffsetY(int y) { m_glMonitor->setOffsetY(y, m_verticalScroll->maximum()); } void Monitor::slotGetCurrentImage(bool request) { m_glMonitor->sendFrameForAnalysis = request; m_monitorManager->activateMonitor(m_id); refreshMonitorIfActive(); if (request) { // Update analysis state QTimer::singleShot(500, m_monitorManager, &MonitorManager::checkScopes); } else { m_glMonitor->releaseAnalyse(); } } void Monitor::slotAddEffect(const QStringList &effect) { if (m_id == Kdenlive::ClipMonitor) { if (m_controller) { emit addMasterEffect(m_controller->AbstractProjectItem::clipId(), effect); } } else { emit addEffect(effect); } } void Monitor::refreshIcons() { QList allMenus = this->findChildren(); for (int i = 0; i < allMenus.count(); i++) { QAction *m = allMenus.at(i); QIcon ic = m->icon(); if (ic.isNull() || ic.name().isEmpty()) { continue; } QIcon newIcon = QIcon::fromTheme(ic.name()); m->setIcon(newIcon); } QList allButtons = this->findChildren(); for (int i = 0; i < allButtons.count(); i++) { KDualAction *m = allButtons.at(i); QIcon ic = m->activeIcon(); if (ic.isNull() || ic.name().isEmpty()) { continue; } QIcon newIcon = QIcon::fromTheme(ic.name()); m->setActiveIcon(newIcon); ic = m->inactiveIcon(); if (ic.isNull() || ic.name().isEmpty()) { continue; } newIcon = QIcon::fromTheme(ic.name()); m->setInactiveIcon(newIcon); } } QAction *Monitor::recAction() { if (m_recManager) { return m_recManager->switchAction(); } return nullptr; } void Monitor::slotLockMonitor(bool lock) { m_monitorManager->lockMonitor(m_id, lock); } void Monitor::setupMenu(QMenu *goMenu, QMenu *overlayMenu, QAction *playZone, QAction *loopZone, QMenu *markerMenu, QAction *loopClip) { delete m_contextMenu; m_contextMenu = new QMenu(this); m_contextMenu->addMenu(m_playMenu); if (goMenu) { m_contextMenu->addMenu(goMenu); } if (markerMenu) { m_contextMenu->addMenu(markerMenu); QList list = markerMenu->actions(); for (int i = 0; i < list.count(); ++i) { if (list.at(i)->data().toString() == QLatin1String("edit_marker")) { m_editMarker = list.at(i); break; } } } m_playMenu->addAction(playZone); m_playMenu->addAction(loopZone); if (loopClip) { m_loopClipAction = loopClip; m_playMenu->addAction(loopClip); } // TODO: add save zone to timeline monitor when fixed m_contextMenu->addMenu(m_markerMenu); if (m_id == Kdenlive::ClipMonitor) { m_contextMenu->addAction(QIcon::fromTheme(QStringLiteral("document-save")), i18n("Save zone"), this, SLOT(slotSaveZone())); QAction *extractZone = m_configMenu->addAction(QIcon::fromTheme(QStringLiteral("document-new")), i18n("Extract Zone"), this, SLOT(slotExtractCurrentZone())); m_contextMenu->addAction(extractZone); } m_contextMenu->addAction(m_monitorManager->getAction(QStringLiteral("extract_frame"))); m_contextMenu->addAction(m_monitorManager->getAction(QStringLiteral("extract_frame_to_project"))); if (m_id == Kdenlive::ProjectMonitor) { m_multitrackView = m_contextMenu->addAction(QIcon::fromTheme(QStringLiteral("view-split-left-right")), i18n("Multitrack view"), this, SIGNAL(multitrackView(bool))); m_multitrackView->setCheckable(true); m_configMenu->addAction(m_multitrackView); } else if (m_id == Kdenlive::ClipMonitor) { QAction *setThumbFrame = m_contextMenu->addAction(QIcon::fromTheme(QStringLiteral("document-new")), i18n("Set current image as thumbnail"), this, SLOT(slotSetThumbFrame())); m_configMenu->addAction(setThumbFrame); } if (overlayMenu) { m_contextMenu->addMenu(overlayMenu); } QAction *overlayAudio = m_contextMenu->addAction(QIcon(), i18n("Overlay audio waveform")); overlayAudio->setCheckable(true); connect(overlayAudio, &QAction::toggled, m_glMonitor, &GLWidget::slotSwitchAudioOverlay); overlayAudio->setChecked(KdenliveSettings::displayAudioOverlay()); m_configMenu->addAction(overlayAudio); QAction *switchAudioMonitor = m_configMenu->addAction(i18n("Show Audio Levels"), this, SLOT(slotSwitchAudioMonitor())); switchAudioMonitor->setCheckable(true); switchAudioMonitor->setChecked((KdenliveSettings::monitoraudio() & m_id) != 0); // For some reason, the frame in QAbstracSpinBox (base class of TimeCodeDisplay) needs to be displayed once, then hidden // or it will never appear (supposed to appear on hover). m_timePos->setFrame(false); } void Monitor::slotGoToMarker(QAction *action) { int pos = action->data().toInt(); slotSeek(pos); } void Monitor::slotForceSize(QAction *a) { int resizeType = a->data().toInt(); int profileWidth = 320; int profileHeight = 200; if (resizeType > 0) { // calculate size QRect r = QApplication::desktop()->screenGeometry(); profileHeight = m_glMonitor->profileSize().height() * resizeType / 100; profileWidth = m_glMonitor->profile()->dar() * profileHeight; if (profileWidth > r.width() * 0.8 || profileHeight > r.height() * 0.7) { // reset action to free resize const QList list = m_forceSize->actions(); for (QAction *ac : list) { if (ac->data().toInt() == m_forceSizeFactor) { m_forceSize->setCurrentAction(ac); break; } } warningMessage(i18n("Your screen resolution is not sufficient for this action")); return; } } switch (resizeType) { case 100: case 50: // resize full size setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); m_videoWidget->setMinimumSize(profileWidth, profileHeight); m_videoWidget->setMaximumSize(profileWidth, profileHeight); setMinimumSize(QSize(profileWidth, profileHeight + m_toolbar->height() + m_glMonitor->getControllerProxy()->rulerHeight())); break; default: // Free resize m_videoWidget->setMinimumSize(profileWidth, profileHeight); m_videoWidget->setMaximumSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX); setMinimumSize(QSize(profileWidth, profileHeight + m_toolbar->height() + m_glMonitor->getControllerProxy()->rulerHeight())); setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); break; } m_forceSizeFactor = resizeType; updateGeometry(); } QString Monitor::getTimecodeFromFrames(int pos) { return m_monitorManager->timecode().getTimecodeFromFrames(pos); } double Monitor::fps() const { return m_monitorManager->timecode().fps(); } Timecode Monitor::timecode() const { return m_monitorManager->timecode(); } void Monitor::updateMarkers() { if (m_controller) { m_markerMenu->clear(); QList markers = m_controller->getMarkerModel()->getAllMarkers(); if (!markers.isEmpty()) { for (int i = 0; i < markers.count(); ++i) { int pos = (int)markers.at(i).time().frames(m_monitorManager->timecode().fps()); QString position = m_monitorManager->timecode().getTimecode(markers.at(i).time()) + QLatin1Char(' ') + markers.at(i).comment(); QAction *go = m_markerMenu->addAction(position); go->setData(pos); } } m_markerMenu->setEnabled(!m_markerMenu->isEmpty()); } } void Monitor::setGuides(const QMap &guides) { // TODO: load guides model m_markerMenu->clear(); QMapIterator i(guides); QList guidesList; while (i.hasNext()) { i.next(); CommentedTime timeGuide(GenTime(i.key()), i.value()); guidesList << timeGuide; int pos = (int)timeGuide.time().frames(m_monitorManager->timecode().fps()); QString position = m_monitorManager->timecode().getTimecode(timeGuide.time()) + QLatin1Char(' ') + timeGuide.comment(); QAction *go = m_markerMenu->addAction(position); go->setData(pos); } // m_ruler->setMarkers(guidesList); m_markerMenu->setEnabled(!m_markerMenu->isEmpty()); checkOverlay(); } void Monitor::slotSeekToPreviousSnap() { if (m_controller) { m_glMonitor->seek(getSnapForPos(true).frames(m_monitorManager->timecode().fps())); } } void Monitor::slotSeekToNextSnap() { if (m_controller) { m_glMonitor->seek(getSnapForPos(false).frames(m_monitorManager->timecode().fps())); } } int Monitor::position() { return m_glMonitor->getCurrentPos(); } GenTime Monitor::getSnapForPos(bool previous) { int frame = previous ? m_snaps->getPreviousPoint(m_glMonitor->getCurrentPos()) : m_snaps->getNextPoint(m_glMonitor->getCurrentPos()); return GenTime(frame, pCore->getCurrentFps()); } void Monitor::slotLoadClipZone(const QPoint &zone) { m_glMonitor->getControllerProxy()->setZone(zone.x(), zone.y()); checkOverlay(); } void Monitor::slotSetZoneStart() { m_glMonitor->getControllerProxy()->setZoneIn(m_glMonitor->getCurrentPos()); if (m_controller) { m_controller->setZone(m_glMonitor->getControllerProxy()->zone()); } else { // timeline emit timelineZoneChanged(); } checkOverlay(); } void Monitor::slotSetZoneEnd(bool discardLastFrame) { Q_UNUSED(discardLastFrame); int pos = m_glMonitor->getCurrentPos(); if (m_controller) { if (pos < (int)m_controller->frameDuration() - 1) { pos++; } } else pos++; m_glMonitor->getControllerProxy()->setZoneOut(pos); if (m_controller) { m_controller->setZone(m_glMonitor->getControllerProxy()->zone()); } checkOverlay(); } // virtual void Monitor::mousePressEvent(QMouseEvent *event) { m_monitorManager->activateMonitor(m_id); if ((event->button() & Qt::RightButton) == 0u) { if (m_glWidget->geometry().contains(event->pos())) { m_DragStartPosition = event->pos(); event->accept(); } } else if (m_contextMenu) { slotActivateMonitor(); m_contextMenu->popup(event->globalPos()); event->accept(); } QWidget::mousePressEvent(event); } void Monitor::slotShowMenu(const QPoint pos) { slotActivateMonitor(); if (m_contextMenu) { m_contextMenu->popup(pos); } } void Monitor::resizeEvent(QResizeEvent *event) { Q_UNUSED(event) if (m_glMonitor->zoom() > 0.0f) { float horizontal = float(m_horizontalScroll->value()) / float(m_horizontalScroll->maximum()); float vertical = float(m_verticalScroll->value()) / float(m_verticalScroll->maximum()); adjustScrollBars(horizontal, vertical); } else { m_horizontalScroll->hide(); m_verticalScroll->hide(); } } void Monitor::adjustScrollBars(float horizontal, float vertical) { if (m_glMonitor->zoom() > 1.0f) { m_horizontalScroll->setPageStep(m_glWidget->width()); m_horizontalScroll->setMaximum((int)((float)m_glMonitor->profileSize().width() * m_glMonitor->zoom()) - m_horizontalScroll->pageStep()); m_horizontalScroll->setValue(qRound(horizontal * float(m_horizontalScroll->maximum()))); emit m_horizontalScroll->valueChanged(m_horizontalScroll->value()); m_horizontalScroll->show(); } else { int max = (int)((float)m_glMonitor->profileSize().width() * m_glMonitor->zoom()) - m_glWidget->width(); emit m_horizontalScroll->valueChanged(qRound(0.5 * max)); m_horizontalScroll->hide(); } if (m_glMonitor->zoom() > 1.0f) { m_verticalScroll->setPageStep(m_glWidget->height()); m_verticalScroll->setMaximum((int)((float)m_glMonitor->profileSize().height() * m_glMonitor->zoom()) - m_verticalScroll->pageStep()); m_verticalScroll->setValue((int)((float)m_verticalScroll->maximum() * vertical)); emit m_verticalScroll->valueChanged(m_verticalScroll->value()); m_verticalScroll->show(); } else { int max = (int)((float)m_glMonitor->profileSize().height() * m_glMonitor->zoom()) - m_glWidget->height(); emit m_verticalScroll->valueChanged(qRound(0.5 * max)); m_verticalScroll->hide(); } } void Monitor::setZoom() { if (qFuzzyCompare(m_glMonitor->zoom(), 1.0f)) { m_horizontalScroll->hide(); m_verticalScroll->hide(); m_glMonitor->setOffsetX(m_horizontalScroll->value(), m_horizontalScroll->maximum()); m_glMonitor->setOffsetY(m_verticalScroll->value(), m_verticalScroll->maximum()); } else { adjustScrollBars(0.5f, 0.5f); } } void Monitor::slotSwitchFullScreen(bool minimizeOnly) { // TODO: disable screensaver? if (!m_glWidget->isFullScreen() && !minimizeOnly) { // Check if we have a multiple monitor setup int monitors = QApplication::desktop()->screenCount(); int screen = -1; if (monitors > 1) { QRect screenres; // Move monitor widget to the second screen (one screen for Kdenlive, the other one for the Monitor widget // int currentScreen = QApplication::desktop()->screenNumber(this); for (int i = 0; screen == -1 && i < QApplication::desktop()->screenCount(); i++) { if (i != QApplication::desktop()->screenNumber(this->parentWidget()->parentWidget())) { screen = i; } } } m_qmlManager->enableAudioThumbs(false); m_glWidget->setParent(QApplication::desktop()->screen(screen)); m_glWidget->move(QApplication::desktop()->screenGeometry(screen).bottomLeft()); m_glWidget->showFullScreen(); } else { m_glWidget->showNormal(); m_qmlManager->enableAudioThumbs(true); QVBoxLayout *lay = (QVBoxLayout *)layout(); lay->insertWidget(0, m_glWidget, 10); } } void Monitor::reparent() { m_glWidget->setParent(nullptr); m_glWidget->showMinimized(); m_glWidget->showNormal(); QVBoxLayout *lay = (QVBoxLayout *)layout(); lay->insertWidget(0, m_glWidget, 10); } // virtual void Monitor::mouseReleaseEvent(QMouseEvent *event) { if (m_dragStarted) { event->ignore(); return; } if (event->button() != Qt::RightButton) { if (m_glMonitor->geometry().contains(event->pos())) { if (isActive()) { slotPlay(); } else { slotActivateMonitor(); } } // else event->ignore(); //QWidget::mouseReleaseEvent(event); } m_dragStarted = false; event->accept(); QWidget::mouseReleaseEvent(event); } void Monitor::slotStartDrag() { if (m_id == Kdenlive::ProjectMonitor || m_controller == nullptr) { // dragging is only allowed for clip monitor return; } auto *drag = new QDrag(this); auto *mimeData = new QMimeData; // Get drag state QQuickItem *root = m_glMonitor->rootObject(); int dragType = 0; if (root) { dragType = root->property("dragType").toInt(); root->setProperty("dragType", 0); } QByteArray prodData; QPoint p = m_glMonitor->getControllerProxy()->zone(); if (p.x() == -1 || p.y() == -1) { prodData = m_controller->AbstractProjectItem::clipId().toUtf8(); } else { QStringList list; list.append(m_controller->AbstractProjectItem::clipId()); list.append(QString::number(p.x())); list.append(QString::number(p.y() - 1)); prodData.append(list.join(QLatin1Char('/')).toUtf8()); } switch (dragType) { case 1: // Audio only drag prodData.prepend('A'); break; case 2: // Audio only drag prodData.prepend('V'); break; default: break; } mimeData->setData(QStringLiteral("kdenlive/producerslist"), prodData); drag->setMimeData(mimeData); /*QPixmap pix = m_currentClip->thumbnail(); drag->setPixmap(pix); drag->setHotSpot(QPoint(0, 50));*/ drag->start(Qt::MoveAction); } void Monitor::enterEvent(QEvent *event) { m_qmlManager->enableAudioThumbs(true); QWidget::enterEvent(event); } void Monitor::leaveEvent(QEvent *event) { m_qmlManager->enableAudioThumbs(false); QWidget::leaveEvent(event); } // virtual void Monitor::mouseMoveEvent(QMouseEvent *event) { if (m_dragStarted || m_controller == nullptr) { return; } if ((event->pos() - m_DragStartPosition).manhattanLength() < QApplication::startDragDistance()) { return; } { auto *drag = new QDrag(this); auto *mimeData = new QMimeData; m_dragStarted = true; QStringList list; list.append(m_controller->AbstractProjectItem::clipId()); QPoint p = m_glMonitor->getControllerProxy()->zone(); list.append(QString::number(p.x())); list.append(QString::number(p.y())); QByteArray clipData; clipData.append(list.join(QLatin1Char(';')).toUtf8()); mimeData->setData(QStringLiteral("kdenlive/clip"), clipData); drag->setMimeData(mimeData); drag->start(Qt::MoveAction); } event->accept(); } /*void Monitor::dragMoveEvent(QDragMoveEvent * event) { event->setDropAction(Qt::IgnoreAction); event->setDropAction(Qt::MoveAction); if (event->mimeData()->hasText()) { event->acceptProposedAction(); } } Qt::DropActions Monitor::supportedDropActions() const { // returns what actions are supported when dropping return Qt::MoveAction; }*/ QStringList Monitor::mimeTypes() const { QStringList qstrList; // list of accepted MIME types for drop qstrList.append(QStringLiteral("kdenlive/clip")); return qstrList; } // virtual void Monitor::wheelEvent(QWheelEvent *event) { slotMouseSeek(event->delta(), event->modifiers()); event->accept(); } void Monitor::mouseDoubleClickEvent(QMouseEvent *event) { slotSwitchFullScreen(); event->accept(); } void Monitor::keyPressEvent(QKeyEvent *event) { if (event->key() == Qt::Key_Escape) { slotSwitchFullScreen(); event->accept(); return; } if (m_glWidget->isFullScreen()) { event->ignore(); emit passKeyPress(event); return; } QWidget::keyPressEvent(event); } void Monitor::slotMouseSeek(int eventDelta, uint modifiers) { if ((modifiers & Qt::ControlModifier) != 0u) { int delta = m_monitorManager->timecode().fps(); if (eventDelta > 0) { delta = 0 - delta; } m_glMonitor->seek(m_glMonitor->getCurrentPos() - delta); } else if ((modifiers & Qt::AltModifier) != 0u) { if (eventDelta >= 0) { emit seekToPreviousSnap(); } else { emit seekToNextSnap(); } } else { if (eventDelta >= 0) { slotRewindOneFrame(); } else { slotForwardOneFrame(); } } } void Monitor::slotSetThumbFrame() { if (m_controller == nullptr) { return; } m_controller->setProducerProperty(QStringLiteral("kdenlive:thumbnailFrame"), m_glMonitor->getCurrentPos()); emit refreshClipThumbnail(m_controller->AbstractProjectItem::clipId()); } void Monitor::slotExtractCurrentZone() { if (m_controller == nullptr) { return; } emit extractZone(m_controller->AbstractProjectItem::clipId()); } std::shared_ptr Monitor::currentController() const { return m_controller; } void Monitor::slotExtractCurrentFrame(QString frameName, bool addToProject) { if (QFileInfo(frameName).fileName().isEmpty()) { // convenience: when extracting an image to be added to the project, // suggest a suitable image file name. In the project monitor, this // suggestion bases on the project file name; in the clip monitor, // the suggestion bases on the clip file name currently shown. // Finally, the frame number is added to this suggestion, prefixed // with "-f", so we get something like clip-f#.png. QString suggestedImageName = QFileInfo(currentController() ? currentController()->clipName() : pCore->currentDoc()->url().isValid() ? pCore->currentDoc()->url().fileName() : i18n("untitled")) .completeBaseName() + QStringLiteral("-f") + QString::number(m_glMonitor->getCurrentPos()).rightJustified(6, QLatin1Char('0')) + QStringLiteral(".png"); frameName = QFileInfo(frameName, suggestedImageName).fileName(); } QString framesFolder = KRecentDirs::dir(QStringLiteral(":KdenliveFramesFolder")); if (framesFolder.isEmpty()) { framesFolder = QDir::homePath(); } QScopedPointer dlg(new QDialog(this)); QScopedPointer fileWidget(new KFileWidget(QUrl::fromLocalFile(framesFolder), dlg.data())); dlg->setWindowTitle(addToProject ? i18n("Save Image") : i18n("Save Image to Project")); auto *layout = new QVBoxLayout; layout->addWidget(fileWidget.data()); QCheckBox *b = nullptr; if (m_id == Kdenlive::ClipMonitor) { b = new QCheckBox(i18n("Export image using source resolution"), dlg.data()); b->setChecked(KdenliveSettings::exportframe_usingsourceres()); fileWidget->setCustomWidget(b); } fileWidget->setConfirmOverwrite(true); fileWidget->okButton()->show(); fileWidget->cancelButton()->show(); QObject::connect(fileWidget->okButton(), &QPushButton::clicked, fileWidget.data(), &KFileWidget::slotOk); QObject::connect(fileWidget.data(), &KFileWidget::accepted, fileWidget.data(), &KFileWidget::accept); QObject::connect(fileWidget.data(), &KFileWidget::accepted, dlg.data(), &QDialog::accept); QObject::connect(fileWidget->cancelButton(), &QPushButton::clicked, dlg.data(), &QDialog::reject); dlg->setLayout(layout); fileWidget->setMimeFilter(QStringList() << QStringLiteral("image/png")); fileWidget->setMode(KFile::File | KFile::LocalOnly); fileWidget->setOperationMode(KFileWidget::Saving); QUrl relativeUrl; relativeUrl.setPath(frameName); #if KIO_VERSION >= QT_VERSION_CHECK(5, 33, 0) fileWidget->setSelectedUrl(relativeUrl); #else fileWidget->setSelection(relativeUrl.toString()); #endif KSharedConfig::Ptr conf = KSharedConfig::openConfig(); QWindow *handle = dlg->windowHandle(); if ((handle != nullptr) && conf->hasGroup("FileDialogSize")) { KWindowConfig::restoreWindowSize(handle, conf->group("FileDialogSize")); dlg->resize(handle->size()); } if (dlg->exec() == QDialog::Accepted) { QString selectedFile = fileWidget->selectedFile(); if (!selectedFile.isEmpty()) { // Create Qimage with frame QImage frame; // check if we are using a proxy if ((m_controller != nullptr) && !m_controller->getProducerProperty(QStringLiteral("kdenlive:proxy")).isEmpty() && m_controller->getProducerProperty(QStringLiteral("kdenlive:proxy")) != QLatin1String("-")) { // using proxy, use original clip url to get frame frame = m_glMonitor->getControllerProxy()->extractFrame(m_glMonitor->getCurrentPos(), m_controller->getProducerProperty(QStringLiteral("kdenlive:originalurl")), -1, -1, b != nullptr ? b->isChecked() : false); } else { frame = m_glMonitor->getControllerProxy()->extractFrame(m_glMonitor->getCurrentPos(), QString(), -1, -1, b != nullptr ? b->isChecked() : false); } frame.save(selectedFile); if (b != nullptr) { KdenliveSettings::setExportframe_usingsourceres(b->isChecked()); } KRecentDirs::add(QStringLiteral(":KdenliveFramesFolder"), QUrl::fromLocalFile(selectedFile).adjusted(QUrl::RemoveFilename).toLocalFile()); if (addToProject) { QStringList folderInfo = pCore->bin()->getFolderInfo(); pCore->bin()->droppedUrls(QList() << QUrl::fromLocalFile(selectedFile), folderInfo); } } } } void Monitor::setTimePos(const QString &pos) { m_timePos->setValue(pos); slotSeek(); } void Monitor::slotSeek() { slotSeek(m_timePos->getValue()); } void Monitor::slotSeek(int pos) { slotActivateMonitor(); m_glMonitor->seek(pos); } void Monitor::checkOverlay(int pos) { if (m_qmlManager->sceneType() != MonitorSceneDefault) { // we are not in main view, ignore return; } QString overlayText; if (pos == -1) { pos = m_timePos->getValue(); } QPoint zone = m_glMonitor->getControllerProxy()->zone(); std::shared_ptr model; if (m_id == Kdenlive::ClipMonitor && m_controller) { model = m_controller->getMarkerModel(); } else if (m_id == Kdenlive::ProjectMonitor && pCore->currentDoc()) { model = pCore->currentDoc()->getGuideModel(); } if (model) { bool found = false; CommentedTime marker = model->getMarker(GenTime(pos, m_monitorManager->timecode().fps()), &found); if (!found) { if (pos == zone.x()) { overlayText = i18n("In Point"); } else if (pos == zone.y() - 1) { overlayText = i18n("Out Point"); } } else { overlayText = marker.comment(); } } m_glMonitor->getControllerProxy()->setMarkerComment(overlayText); } int Monitor::getZoneStart() { return m_glMonitor->getControllerProxy()->zoneIn(); } int Monitor::getZoneEnd() { return m_glMonitor->getControllerProxy()->zoneOut(); } void Monitor::slotZoneStart() { slotActivateMonitor(); m_glMonitor->getControllerProxy()->pauseAndSeek(m_glMonitor->getControllerProxy()->zoneIn()); } void Monitor::slotZoneEnd() { slotActivateMonitor(); m_glMonitor->getControllerProxy()->pauseAndSeek(m_glMonitor->getControllerProxy()->zoneOut() - 1); } void Monitor::slotRewind(double speed) { slotActivateMonitor(); if (qFuzzyIsNull(speed)) { double currentspeed = m_glMonitor->playSpeed(); if (currentspeed > -1) { speed = -1; } else { speed = currentspeed * 1.5; } } m_glMonitor->switchPlay(true, speed); m_playAction->setActive(true); } void Monitor::slotForward(double speed) { slotActivateMonitor(); if (qFuzzyIsNull(speed)) { double currentspeed = m_glMonitor->playSpeed(); if (currentspeed < 1) { speed = 1; } else { speed = currentspeed * 1.2; } } m_glMonitor->switchPlay(true, speed); m_playAction->setActive(true); } void Monitor::slotRewindOneFrame(int diff) { slotActivateMonitor(); m_glMonitor->seek(m_glMonitor->getCurrentPos() - diff); } void Monitor::slotForwardOneFrame(int diff) { slotActivateMonitor(); m_glMonitor->seek(m_glMonitor->getCurrentPos() + diff); } void Monitor::seekCursor(int pos) { Q_UNUSED(pos) // Deprecated should not be used, instead requestSeek /*if (m_ruler->slotNewValue(pos)) { m_timePos->setValue(pos); checkOverlay(pos); if (m_id != Kdenlive::ClipMonitor) { emit renderPosition(pos); } }*/ } -void Monitor::adjustRulerSize(int length, std::shared_ptr markerModel) +void Monitor::adjustRulerSize(int length, const std::shared_ptr &markerModel) { if (m_controller != nullptr) { m_glMonitor->setRulerInfo(length); } else { m_glMonitor->setRulerInfo(length, markerModel); } m_timePos->setRange(0, length); if (markerModel) { connect(markerModel.get(), SIGNAL(dataChanged(const QModelIndex &, const QModelIndex &, const QVector &)), this, SLOT(checkOverlay())); connect(markerModel.get(), SIGNAL(rowsInserted(const QModelIndex &, int, int)), this, SLOT(checkOverlay())); connect(markerModel.get(), SIGNAL(rowsRemoved(const QModelIndex &, int, int)), this, SLOT(checkOverlay())); } } void Monitor::stop() { m_playAction->setActive(false); m_glMonitor->stop(); } void Monitor::mute(bool mute, bool updateIconOnly) { // TODO: we should set the "audio_off" property to 1 to mute the consumer instead of changing volume QIcon icon; if (mute || KdenliveSettings::volume() == 0) { icon = QIcon::fromTheme(QStringLiteral("audio-volume-muted")); } else { icon = QIcon::fromTheme(QStringLiteral("audio-volume-medium")); } m_audioButton->setIcon(icon); if (!updateIconOnly) { m_glMonitor->setVolume(mute ? 0 : (double)KdenliveSettings::volume() / 100.0); } } void Monitor::start() { if (!isVisible() || !isActive()) { return; } m_glMonitor->startConsumer(); } void Monitor::slotRefreshMonitor(bool visible) { if (visible) { if (slotActivateMonitor()) { start(); } } } void Monitor::refreshMonitorIfActive(bool directUpdate) { if (isActive()) { if (directUpdate) { m_glMonitor->refresh(); } else { m_glMonitor->requestRefresh(); } } } void Monitor::pause() { if (!m_playAction->isActive()) { return; } slotActivateMonitor(); m_glMonitor->switchPlay(false); m_playAction->setActive(false); } void Monitor::switchPlay(bool play) { m_playAction->setActive(play); m_glMonitor->switchPlay(play); } void Monitor::slotSwitchPlay() { slotActivateMonitor(); m_glMonitor->switchPlay(m_playAction->isActive()); } void Monitor::slotPlay() { m_playAction->trigger(); } void Monitor::slotPlayZone() { slotActivateMonitor(); bool ok = m_glMonitor->playZone(); if (ok) { m_playAction->setActive(true); } } void Monitor::slotLoopZone() { slotActivateMonitor(); bool ok = m_glMonitor->playZone(true); if (ok) { m_playAction->setActive(true); } } void Monitor::slotLoopClip() { slotActivateMonitor(); bool ok = m_glMonitor->loopClip(); if (ok) { m_playAction->setActive(true); } } -void Monitor::updateClipProducer(std::shared_ptr prod) +void Monitor::updateClipProducer(const std::shared_ptr &prod) { if (m_glMonitor->setProducer(prod, isActive(), -1)) { prod->set_speed(1.0); } } void Monitor::updateClipProducer(const QString &playlist) { Q_UNUSED(playlist) // TODO // Mlt::Producer *prod = new Mlt::Producer(*m_glMonitor->profile(), playlist.toUtf8().constData()); // m_glMonitor->setProducer(prod, isActive(), render->seekFramePosition()); m_glMonitor->switchPlay(true); } -void Monitor::slotOpenClip(std::shared_ptr controller, int in, int out) +void Monitor::slotOpenClip(const std::shared_ptr &controller, int in, int out) { if (m_controller) { disconnect(m_controller->getMarkerModel().get(), SIGNAL(dataChanged(const QModelIndex &, const QModelIndex &, const QVector &)), this, SLOT(checkOverlay())); disconnect(m_controller->getMarkerModel().get(), SIGNAL(rowsInserted(const QModelIndex &, int, int)), this, SLOT(checkOverlay())); disconnect(m_controller->getMarkerModel().get(), SIGNAL(rowsRemoved(const QModelIndex &, int, int)), this, SLOT(checkOverlay())); } m_controller = controller; loadQmlScene(MonitorSceneDefault); m_snaps.reset(new SnapModel()); m_glMonitor->getControllerProxy()->resetZone(); if (controller) { connect(m_controller->getMarkerModel().get(), SIGNAL(dataChanged(const QModelIndex &, const QModelIndex &, const QVector &)), this, SLOT(checkOverlay())); connect(m_controller->getMarkerModel().get(), SIGNAL(rowsInserted(const QModelIndex &, int, int)), this, SLOT(checkOverlay())); connect(m_controller->getMarkerModel().get(), SIGNAL(rowsRemoved(const QModelIndex &, int, int)), this, SLOT(checkOverlay())); if (m_recManager->toolbar()->isVisible()) { // we are in record mode, don't display clip return; } m_glMonitor->setRulerInfo((int)m_controller->frameDuration(), controller->getMarkerModel()); m_timePos->setRange(0, (int)m_controller->frameDuration()); updateMarkers(); connect(m_glMonitor->getControllerProxy(), &MonitorProxy::addSnap, this, &Monitor::addSnapPoint, Qt::DirectConnection); connect(m_glMonitor->getControllerProxy(), &MonitorProxy::removeSnap, this, &Monitor::removeSnapPoint, Qt::DirectConnection); if (out == -1) { m_glMonitor->getControllerProxy()->setZone(m_controller->zone(), false); qDebug() << m_controller->zone(); } else { m_glMonitor->getControllerProxy()->setZone(in, out, false); } m_snaps->addPoint((int)m_controller->frameDuration()); // Loading new clip / zone, stop if playing if (m_playAction->isActive()) { m_playAction->setActive(false); } m_glMonitor->setProducer(m_controller->originalProducer(), isActive(), in); m_audioMeterWidget->audioChannels = controller->audioInfo() ? controller->audioInfo()->channels() : 0; m_glMonitor->setAudioThumb(controller->audioChannels(), controller->audioFrameCache); m_controller->getMarkerModel()->registerSnapModel(m_snaps); // hasEffects = controller->hasEffects(); } else { m_glMonitor->setProducer(nullptr, isActive()); m_glMonitor->setAudioThumb(); m_audioMeterWidget->audioChannels = 0; } if (slotActivateMonitor()) { start(); } checkOverlay(); } const QString Monitor::activeClipId() { if (m_controller) { return m_controller->AbstractProjectItem::clipId(); } return QString(); } void Monitor::slotOpenDvdFile(const QString &file) { // TODO Q_UNUSED(file) m_glMonitor->initializeGL(); // render->loadUrl(file); } void Monitor::slotSaveZone() { // TODO? or deprecate // render->saveZone(pCore->currentDoc()->projectDataFolder(), m_ruler->zone()); } void Monitor::setCustomProfile(const QString &profile, const Timecode &tc) { // TODO or deprecate Q_UNUSED(profile) m_timePos->updateTimeCode(tc); if (true) { return; } slotActivateMonitor(); // render->prepareProfileReset(tc.fps()); if (m_multitrackView) { m_multitrackView->setChecked(false); } // TODO: this is a temporary profile for DVD preview, it should not alter project profile // pCore->setCurrentProfile(profile); m_glMonitor->reloadProfile(); } void Monitor::resetProfile() { m_timePos->updateTimeCode(m_monitorManager->timecode()); m_glMonitor->reloadProfile(); m_glMonitor->rootObject()->setProperty("framesize", QRect(0, 0, m_glMonitor->profileSize().width(), m_glMonitor->profileSize().height())); double fps = m_monitorManager->timecode().fps(); // Update drop frame info m_qmlManager->setProperty(QStringLiteral("dropped"), false); m_qmlManager->setProperty(QStringLiteral("fps"), QString::number(fps, 'g', 2)); } void Monitor::resetConsumer(bool fullReset) { m_glMonitor->resetConsumer(fullReset); } const QString Monitor::sceneList(const QString &root, const QString &fullPath) { return m_glMonitor->sceneList(root, fullPath); } void Monitor::updateClipZone() { if (m_controller == nullptr) { return; } m_controller->setZone(m_glMonitor->getControllerProxy()->zone()); } void Monitor::updateTimelineClipZone() { emit zoneUpdated(m_glMonitor->getControllerProxy()->zone()); } void Monitor::switchDropFrames(bool drop) { m_glMonitor->setDropFrames(drop); } void Monitor::switchMonitorInfo(int code) { int currentOverlay; if (m_id == Kdenlive::ClipMonitor) { currentOverlay = KdenliveSettings::displayClipMonitorInfo(); currentOverlay ^= code; KdenliveSettings::setDisplayClipMonitorInfo(currentOverlay); } else { currentOverlay = KdenliveSettings::displayProjectMonitorInfo(); currentOverlay ^= code; KdenliveSettings::setDisplayProjectMonitorInfo(currentOverlay); } updateQmlDisplay(currentOverlay); } void Monitor::updateMonitorGamma() { if (isActive()) { stop(); m_glMonitor->updateGamma(); start(); } else { m_glMonitor->updateGamma(); } } void Monitor::slotEditMarker() { if (m_editMarker) { m_editMarker->trigger(); } } void Monitor::updateTimecodeFormat() { m_timePos->slotUpdateTimeCodeFormat(); m_glMonitor->rootObject()->setProperty("timecode", m_timePos->displayText()); } QPoint Monitor::getZoneInfo() const { if (m_controller == nullptr) { return QPoint(); } return m_controller->zone(); } void Monitor::slotEnableEffectScene(bool enable) { KdenliveSettings::setShowOnMonitorScene(enable); MonitorSceneType sceneType = enable ? m_lastMonitorSceneType : MonitorSceneDefault; slotShowEffectScene(sceneType, true); if (enable) { emit seekPosition(m_glMonitor->getCurrentPos()); } } void Monitor::slotShowEffectScene(MonitorSceneType sceneType, bool temporary) { if (sceneType == MonitorSceneNone) { // We just want to revert to normal scene if (m_qmlManager->sceneType() == MonitorSceneSplit || m_qmlManager->sceneType() == MonitorSceneDefault) { // Ok, nothing to do return; } sceneType = MonitorSceneDefault; } if (!temporary) { m_lastMonitorSceneType = sceneType; } loadQmlScene(sceneType); } void Monitor::slotSeekToKeyFrame() { if (m_qmlManager->sceneType() == MonitorSceneGeometry) { // Adjust splitter pos int kfr = m_glMonitor->rootObject()->property("requestedKeyFrame").toInt(); emit seekToKeyframe(kfr); } } void Monitor::setUpEffectGeometry(const QRect &r, const QVariantList &list, const QVariantList &types) { QQuickItem *root = m_glMonitor->rootObject(); if (!root) { return; } if (!list.isEmpty()) { root->setProperty("centerPointsTypes", types); root->setProperty("centerPoints", list); } if (!r.isEmpty()) { root->setProperty("framesize", r); } } void Monitor::setEffectSceneProperty(const QString &name, const QVariant &value) { QQuickItem *root = m_glMonitor->rootObject(); if (!root) { return; } root->setProperty(name.toUtf8().constData(), value); } QRect Monitor::effectRect() const { QQuickItem *root = m_glMonitor->rootObject(); if (!root) { return QRect(); } return root->property("framesize").toRect(); } QVariantList Monitor::effectPolygon() const { QQuickItem *root = m_glMonitor->rootObject(); if (!root) { return QVariantList(); } return root->property("centerPoints").toList(); } QVariantList Monitor::effectRoto() const { QQuickItem *root = m_glMonitor->rootObject(); if (!root) { return QVariantList(); } QVariantList points = root->property("centerPoints").toList(); QVariantList controlPoints = root->property("centerPointsTypes").toList(); // rotoscoping effect needs a list of QVariantList mix; mix.reserve(points.count() * 3); for (int i = 0; i < points.count(); i++) { mix << controlPoints.at(2 * i); mix << points.at(i); mix << controlPoints.at(2 * i + 1); } return mix; } void Monitor::setEffectKeyframe(bool enable) { QQuickItem *root = m_glMonitor->rootObject(); if (root) { root->setProperty("iskeyframe", enable); } } bool Monitor::effectSceneDisplayed(MonitorSceneType effectType) { return m_qmlManager->sceneType() == effectType; } void Monitor::slotSetVolume(int volume) { KdenliveSettings::setVolume(volume); QIcon icon; double renderVolume = m_glMonitor->volume(); m_glMonitor->setVolume((double)volume / 100.0); if (renderVolume > 0 && volume > 0) { return; } if (volume == 0) { icon = QIcon::fromTheme(QStringLiteral("audio-volume-muted")); } else { icon = QIcon::fromTheme(QStringLiteral("audio-volume-medium")); } m_audioButton->setIcon(icon); } void Monitor::sendFrameForAnalysis(bool analyse) { m_glMonitor->sendFrameForAnalysis = analyse; } void Monitor::updateAudioForAnalysis() { m_glMonitor->updateAudioForAnalysis(); } void Monitor::onFrameDisplayed(const SharedFrame &frame) { m_monitorManager->frameDisplayed(frame); int position = frame.get_position(); if (!m_glMonitor->checkFrameNumber(position, m_id == Kdenlive::ClipMonitor ? 0 : TimelineModel::seekDuration + 1)) { m_playAction->setActive(false); } checkDrops(m_glMonitor->droppedFrames()); } void Monitor::checkDrops(int dropped) { if (m_droppedTimer.isValid()) { if (m_droppedTimer.hasExpired(1000)) { m_droppedTimer.invalidate(); double fps = m_monitorManager->timecode().fps(); if (dropped == 0) { // No dropped frames since last check m_qmlManager->setProperty(QStringLiteral("dropped"), false); m_qmlManager->setProperty(QStringLiteral("fps"), QString::number(fps, 'g', 2)); } else { m_glMonitor->resetDrops(); fps -= dropped; m_qmlManager->setProperty(QStringLiteral("dropped"), true); m_qmlManager->setProperty(QStringLiteral("fps"), QString::number(fps, 'g', 2)); m_droppedTimer.start(); } } } else if (dropped > 0) { // Start m_dropTimer m_glMonitor->resetDrops(); m_droppedTimer.start(); } } void Monitor::reloadProducer(const QString &id) { if (!m_controller) { return; } if (m_controller->AbstractProjectItem::clipId() == id) { slotOpenClip(m_controller); } } QString Monitor::getMarkerThumb(GenTime pos) { if (!m_controller) { return QString(); } if (!m_controller->getClipHash().isEmpty()) { QString url = m_monitorManager->getCacheFolder(CacheThumbs) .absoluteFilePath(m_controller->getClipHash() + QLatin1Char('#') + QString::number((int)pos.frames(m_monitorManager->timecode().fps())) + QStringLiteral(".png")); if (QFile::exists(url)) { return url; } } return QString(); } const QString Monitor::projectFolder() const { return m_monitorManager->getProjectFolder(); } void Monitor::setPalette(const QPalette &p) { QWidget::setPalette(p); QList allButtons = this->findChildren(); for (int i = 0; i < allButtons.count(); i++) { QToolButton *m = allButtons.at(i); QIcon ic = m->icon(); if (ic.isNull() || ic.name().isEmpty()) { continue; } QIcon newIcon = QIcon::fromTheme(ic.name()); m->setIcon(newIcon); } QQuickItem *root = m_glMonitor->rootObject(); if (root) { QMetaObject::invokeMethod(root, "updatePalette"); } m_audioMeterWidget->refreshPixmap(); } void Monitor::gpuError() { qCWarning(KDENLIVE_LOG) << " + + + + Error initializing Movit GLSL manager"; warningMessage(i18n("Cannot initialize Movit's GLSL manager, please disable Movit"), -1); } void Monitor::warningMessage(const QString &text, int timeout, const QList &actions) { m_infoMessage->setMessageType(KMessageWidget::Warning); m_infoMessage->setText(text); for (QAction *action : actions) { m_infoMessage->addAction(action); } m_infoMessage->setCloseButtonVisible(true); m_infoMessage->animatedShow(); if (timeout > 0) { QTimer::singleShot(timeout, m_infoMessage, &KMessageWidget::animatedHide); } } void Monitor::activateSplit() { loadQmlScene(MonitorSceneSplit); if (isActive()) { m_glMonitor->requestRefresh(); } else if (slotActivateMonitor()) { start(); } } void Monitor::slotSwitchCompare(bool enable) { if (m_id == Kdenlive::ProjectMonitor) { if (enable) { if (m_qmlManager->sceneType() == MonitorSceneSplit) { // Split scene is already active return; } m_splitEffect = new Mlt::Filter(*profile(), "frei0r.alphagrad"); if ((m_splitEffect != nullptr) && m_splitEffect->is_valid()) { m_splitEffect->set("0", 0.5); // 0 is the Clip left parameter m_splitEffect->set("1", 0); // 1 is gradient width m_splitEffect->set("2", -0.747); // 2 is tilt } else { // frei0r.scal0tilt is not available warningMessage(i18n("The alphagrad filter is required for that feature, please install frei0r and restart Kdenlive")); return; } emit createSplitOverlay(m_splitEffect); return; } // Delete temp track emit removeSplitOverlay(); delete m_splitEffect; m_splitEffect = nullptr; loadQmlScene(MonitorSceneDefault); if (isActive()) { m_glMonitor->requestRefresh(); } else if (slotActivateMonitor()) { start(); } return; } if (m_controller == nullptr || !m_controller->hasEffects()) { // disable split effect if (m_controller) { pCore->displayMessage(i18n("Clip has no effects"), InformationMessage); } else { pCore->displayMessage(i18n("Select a clip in project bin to compare effect"), InformationMessage); } return; } if (enable) { if (m_qmlManager->sceneType() == MonitorSceneSplit) { // Split scene is already active qDebug() << " . . . .. ALREADY ACTIVE"; return; } buildSplitEffect(m_controller->masterProducer()); } else if (m_splitEffect) { // TODO m_glMonitor->setProducer(m_controller->originalProducer(), isActive(), position()); delete m_splitEffect; m_splitProducer.reset(); m_splitEffect = nullptr; loadQmlScene(MonitorSceneDefault); } slotActivateMonitor(); } void Monitor::buildSplitEffect(Mlt::Producer *original) { m_splitEffect = new Mlt::Filter(*profile(), "frei0r.alphagrad"); if ((m_splitEffect != nullptr) && m_splitEffect->is_valid()) { m_splitEffect->set("0", 0.5); // 0 is the Clip left parameter m_splitEffect->set("1", 0); // 1 is gradient width m_splitEffect->set("2", -0.747); // 2 is tilt } else { // frei0r.scal0tilt is not available pCore->displayMessage(i18n("The alphagrad filter is required for that feature, please install frei0r and restart Kdenlive"), ErrorMessage); return; } QString splitTransition = TransitionsRepository::get()->getCompositingTransition(); Mlt::Transition t(*profile(), splitTransition.toUtf8().constData()); if (!t.is_valid()) { delete m_splitEffect; pCore->displayMessage(i18n("The cairoblend transition is required for that feature, please install frei0r and restart Kdenlive"), ErrorMessage); return; } Mlt::Tractor trac(*profile()); std::shared_ptr clone = ProjectClip::cloneProducer(std::make_shared(original)); // Delete all effects int ct = 0; Mlt::Filter *filter = clone->filter(ct); while (filter != nullptr) { QString ix = QString::fromLatin1(filter->get("kdenlive_id")); if (!ix.isEmpty()) { if (clone->detach(*filter) == 0) { } else { ct++; } } else { ct++; } delete filter; filter = clone->filter(ct); } trac.set_track(*original, 0); trac.set_track(*clone.get(), 1); clone.get()->attach(*m_splitEffect); t.set("always_active", 1); trac.plant_transition(t, 0, 1); delete original; m_splitProducer = std::make_shared(trac.get_producer()); m_glMonitor->setProducer(m_splitProducer, isActive(), position()); m_glMonitor->setRulerInfo((int)m_controller->frameDuration(), m_controller->getMarkerModel()); loadQmlScene(MonitorSceneSplit); } QSize Monitor::profileSize() const { return m_glMonitor->profileSize(); } void Monitor::loadQmlScene(MonitorSceneType type) { if (m_id == Kdenlive::DvdMonitor || type == m_qmlManager->sceneType()) { return; } bool sceneWithEdit = type == MonitorSceneGeometry || type == MonitorSceneCorners || type == MonitorSceneRoto; if ((m_sceneVisibilityAction != nullptr) && !m_sceneVisibilityAction->isChecked() && sceneWithEdit) { // User doesn't want effect scenes pCore->displayMessage(i18n("Enable edit mode in monitor to edit effect"), InformationMessage, 500); type = MonitorSceneDefault; } double ratio = (double)m_glMonitor->profileSize().width() / (int)(m_glMonitor->profileSize().height() * m_glMonitor->profile()->dar() + 0.5); m_qmlManager->setScene(m_id, type, m_glMonitor->profileSize(), ratio, m_glMonitor->displayRect(), m_glMonitor->zoom(), m_timePos->maximum()); QQuickItem *root = m_glMonitor->rootObject(); switch (type) { case MonitorSceneSplit: QObject::connect(root, SIGNAL(qmlMoveSplit()), this, SLOT(slotAdjustEffectCompare()), Qt::UniqueConnection); break; case MonitorSceneGeometry: case MonitorSceneCorners: case MonitorSceneRoto: break; case MonitorSceneRipple: QObject::connect(root, SIGNAL(doAcceptRipple(bool)), this, SIGNAL(acceptRipple(bool)), Qt::UniqueConnection); QObject::connect(root, SIGNAL(switchTrimMode(int)), this, SIGNAL(switchTrimMode(int)), Qt::UniqueConnection); break; case MonitorSceneDefault: QObject::connect(root, SIGNAL(editCurrentMarker()), this, SLOT(slotEditInlineMarker()), Qt::UniqueConnection); m_qmlManager->setProperty(QStringLiteral("timecode"), m_timePos->displayText()); if (m_id == Kdenlive::ClipMonitor) { updateQmlDisplay(KdenliveSettings::displayClipMonitorInfo()); } else if (m_id == Kdenlive::ProjectMonitor) { updateQmlDisplay(KdenliveSettings::displayProjectMonitorInfo()); } break; default: break; } m_qmlManager->setProperty(QStringLiteral("fps"), QString::number(m_monitorManager->timecode().fps(), 'g', 2)); } void Monitor::setQmlProperty(const QString &name, const QVariant &value) { m_qmlManager->setProperty(name, value); } void Monitor::slotAdjustEffectCompare() { QRect r = m_glMonitor->rect(); double percent = 0.5; if (m_qmlManager->sceneType() == MonitorSceneSplit) { // Adjust splitter pos QQuickItem *root = m_glMonitor->rootObject(); percent = 0.5 - ((root->property("splitterPos").toInt() - r.left() - r.width() / 2.0) / (double)r.width() / 2.0) / 0.75; // Store real frame percentage for resize events root->setProperty("realpercent", percent); } if (m_splitEffect) { m_splitEffect->set("0", percent); } m_glMonitor->refresh(); } Mlt::Profile *Monitor::profile() { return m_glMonitor->profile(); } void Monitor::slotSwitchRec(bool enable) { if (!m_recManager) { return; } if (enable) { m_toolbar->setVisible(false); m_recManager->toolbar()->setVisible(true); } else if (m_recManager->toolbar()->isVisible()) { m_recManager->stop(); m_toolbar->setVisible(true); emit refreshCurrentClip(); } } bool Monitor::startCapture(const QString ¶ms, const QString &path, Mlt::Producer *p) { // TODO m_controller = nullptr; if (false) { // render->updateProducer(p)) { m_glMonitor->reconfigureMulti(params, path, p->profile()); return true; } return false; } bool Monitor::stopCapture() { m_glMonitor->stopCapture(); slotOpenClip(nullptr); m_glMonitor->reconfigure(profile()); return true; } void Monitor::doKeyPressEvent(QKeyEvent *ev) { keyPressEvent(ev); } void Monitor::slotEditInlineMarker() { QQuickItem *root = m_glMonitor->rootObject(); if (root) { std::shared_ptr model; if (m_controller) { // We are editing a clip marker model = m_controller->getMarkerModel(); } else { model = pCore->currentDoc()->getGuideModel(); } QString newComment = root->property("markerText").toString(); bool found = false; CommentedTime oldMarker = model->getMarker(m_timePos->gentime(), &found); if (!found || newComment == oldMarker.comment()) { // No change return; } oldMarker.setComment(newComment); model->addMarker(oldMarker.time(), oldMarker.comment(), oldMarker.markerType()); } } void Monitor::prepareAudioThumb(int channels, QVariantList &audioCache) { m_glMonitor->setAudioThumb(channels, audioCache); } void Monitor::slotSwitchAudioMonitor() { if (!m_audioMeterWidget->isValid) { KdenliveSettings::setMonitoraudio(0x01); m_audioMeterWidget->setVisibility(false); return; } int currentOverlay = KdenliveSettings::monitoraudio(); currentOverlay ^= m_id; KdenliveSettings::setMonitoraudio(currentOverlay); if ((KdenliveSettings::monitoraudio() & m_id) != 0) { // We want to enable this audio monitor, so make monitor active slotActivateMonitor(); } displayAudioMonitor(isActive()); } void Monitor::displayAudioMonitor(bool isActive) { bool enable = isActive && ((KdenliveSettings::monitoraudio() & m_id) != 0); if (enable) { connect(m_monitorManager, &MonitorManager::frameDisplayed, m_audioMeterWidget, &ScopeWidget::onNewFrame, Qt::UniqueConnection); } else { disconnect(m_monitorManager, &MonitorManager::frameDisplayed, m_audioMeterWidget, &ScopeWidget::onNewFrame); } m_audioMeterWidget->setVisibility((KdenliveSettings::monitoraudio() & m_id) != 0); } void Monitor::updateQmlDisplay(int currentOverlay) { m_glMonitor->rootObject()->setVisible((currentOverlay & 0x01) != 0); m_glMonitor->rootObject()->setProperty("showMarkers", currentOverlay & 0x04); m_glMonitor->rootObject()->setProperty("showFps", currentOverlay & 0x20); m_glMonitor->rootObject()->setProperty("showTimecode", currentOverlay & 0x02); m_glMonitor->rootObject()->setProperty("showAudiothumb", currentOverlay & 0x10); } void Monitor::clearDisplay() { m_glMonitor->clear(); } void Monitor::panView(QPoint diff) { // Only pan if scrollbars are visible if (m_horizontalScroll->isVisible()) { m_horizontalScroll->setValue(m_horizontalScroll->value() + diff.x()); } if (m_verticalScroll->isVisible()) { m_verticalScroll->setValue(m_verticalScroll->value() + diff.y()); } } void Monitor::requestSeek(int pos) { m_glMonitor->seek(pos); } void Monitor::setProducer(std::shared_ptr producer, int pos) { - m_glMonitor->setProducer(producer, isActive(), pos); + m_glMonitor->setProducer(std::move(producer), isActive(), pos); } void Monitor::reconfigure() { m_glMonitor->reconfigure(); } void Monitor::slotSeekPosition(int pos) { m_timePos->setValue(pos); checkOverlay(); } void Monitor::slotStart() { slotActivateMonitor(); m_glMonitor->switchPlay(false); m_glMonitor->seek(0); } void Monitor::slotEnd() { slotActivateMonitor(); m_glMonitor->switchPlay(false); if (m_id == Kdenlive::ClipMonitor) { m_glMonitor->seek(m_glMonitor->duration()); } else { m_glMonitor->seek(pCore->projectDuration()); } } void Monitor::addSnapPoint(int pos) { m_snaps->addPoint(pos); } void Monitor::removeSnapPoint(int pos) { m_snaps->removePoint(pos); } void Monitor::slotZoomIn() { m_glMonitor->slotZoom(true); } void Monitor::slotZoomOut() { m_glMonitor->slotZoom(false); } void Monitor::setConsumerProperty(const QString &name, const QString &value) { m_glMonitor->setConsumerProperty(name, value); } diff --git a/src/monitor/monitor.h b/src/monitor/monitor.h index 137f73d8b..e4c9d7e37 100644 --- a/src/monitor/monitor.h +++ b/src/monitor/monitor.h @@ -1,371 +1,371 @@ /*************************************************************************** * Copyright (C) 2007 by Jean-Baptiste Mardelle (jb@kdenlive.org) * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #ifndef MONITOR_H #define MONITOR_H #include "abstractmonitor.h" #include "bin/model/markerlistmodel.hpp" #include "definitions.h" #include "gentime.h" #include "scopes/sharedframe.h" #include "timecodedisplay.h" #include #include #include #include #include #include class SnapModel; class ProjectClip; class MonitorManager; class QSlider; class KDualAction; class KSelectAction; class KMessageWidget; class QQuickItem; class QScrollBar; class RecManager; class QToolButton; class QmlManager; class GLWidget; class MonitorAudioLevel; namespace Mlt { class Profile; class Filter; } // namespace Mlt class QuickEventEater : public QObject { Q_OBJECT public: explicit QuickEventEater(QObject *parent = nullptr); protected: bool eventFilter(QObject *obj, QEvent *event) override; signals: void addEffect(const QStringList &); }; class QuickMonitorEventEater : public QObject { Q_OBJECT public: explicit QuickMonitorEventEater(QWidget *parent); protected: bool eventFilter(QObject *obj, QEvent *event) override; signals: void doKeyPressEvent(QKeyEvent *); }; class Monitor : public AbstractMonitor { Q_OBJECT public: friend class MonitorManager; Monitor(Kdenlive::MonitorId id, MonitorManager *manager, QWidget *parent = nullptr); ~Monitor(); void resetProfile(); /** @brief Rebuild consumers after a property change */ void resetConsumer(bool fullReset); void setCustomProfile(const QString &profile, const Timecode &tc); void setupMenu(QMenu *goMenu, QMenu *overlayMenu, QAction *playZone, QAction *loopZone, QMenu *markerMenu = nullptr, QAction *loopClip = nullptr); const QString sceneList(const QString &root, const QString &fullPath = QString()); const QString activeClipId(); int position(); void updateTimecodeFormat(); void updateMarkers(); /** @brief Controller for the clip currently displayed (only valid for clip monitor). */ std::shared_ptr currentController() const; /** @brief Add timeline guides to the ruler and context menu */ void setGuides(const QMap &guides); void reloadProducer(const QString &id); /** @brief Reimplemented from QWidget, updates the palette colors. */ void setPalette(const QPalette &p); /** @brief Returns a hh:mm:ss timecode from a frame number. */ QString getTimecodeFromFrames(int pos); /** @brief Returns current project's fps. */ double fps() const; /** @brief Returns current project's timecode. */ Timecode timecode() const; /** @brief Get url for the clip's thumbnail */ QString getMarkerThumb(GenTime pos); /** @brief Get current project's folder */ const QString projectFolder() const; /** @brief Get the project's Mlt profile */ Mlt::Profile *profile(); int getZoneStart(); int getZoneEnd(); void setUpEffectGeometry(const QRect &r, const QVariantList &list = QVariantList(), const QVariantList &types = QVariantList()); /** @brief Set a property on the effect scene */ void setEffectSceneProperty(const QString &name, const QVariant &value); /** @brief Returns effective display size */ QSize profileSize() const; QRect effectRect() const; QVariantList effectPolygon() const; QVariantList effectRoto() const; void setEffectKeyframe(bool enable); void sendFrameForAnalysis(bool analyse); void updateAudioForAnalysis(); void switchMonitorInfo(int code); void switchDropFrames(bool drop); void updateMonitorGamma(); void mute(bool, bool updateIconOnly = false) override; bool startCapture(const QString ¶ms, const QString &path, Mlt::Producer *p); bool stopCapture(); void reparent(); /** @brief Returns the action displaying record toolbar */ QAction *recAction(); void refreshIcons(); /** @brief Send audio thumb data to qml for on monitor display */ void prepareAudioThumb(int channels, QVariantList &audioCache); void connectAudioSpectrum(bool activate); /** @brief Set a property on the Qml scene **/ void setQmlProperty(const QString &name, const QVariant &value); void displayAudioMonitor(bool isActive); /** @brief Prepare split effect from timeline clip producer **/ void activateSplit(); /** @brief Clear monitor display **/ void clearDisplay(); void setProducer(std::shared_ptr producer, int pos = -1); void reconfigure(); /** @brief Saves current monitor frame to an image file, and add it to project if addToProject is set to true **/ void slotExtractCurrentFrame(QString frameName = QString(), bool addToProject = false); /** @brief Zoom in active monitor */ void slotZoomIn(); /** @brief Zoom out active monitor */ void slotZoomOut(); /** @brief Set a property on the MLT consumer */ void setConsumerProperty(const QString &name, const QString &value); protected: void mousePressEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; void mouseDoubleClickEvent(QMouseEvent *event) override; void resizeEvent(QResizeEvent *event) override; void keyPressEvent(QKeyEvent *event) override; /** @brief Move to another position on mouse wheel event. * * Moves towards the end of the clip/timeline on mouse wheel down/back, the * opposite on mouse wheel up/forward. * Ctrl + wheel moves by a second, without Ctrl it moves by a single frame. */ void wheelEvent(QWheelEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override; void enterEvent(QEvent *event) override; void leaveEvent(QEvent *event) override; virtual QStringList mimeTypes() const; private: std::shared_ptr m_controller; /** @brief The QQuickView that handles our monitor display (video and qml overlay) **/ GLWidget *m_glMonitor; /** @brief Container for our QQuickView monitor display (QQuickView needs to be embedded) **/ QWidget *m_glWidget; /** @brief Scrollbar for our monitor view, used when zooming the monitor **/ QScrollBar *m_verticalScroll; /** @brief Scrollbar for our monitor view, used when zooming the monitor **/ QScrollBar *m_horizontalScroll; /** @brief Widget holding the window for the QQuickView **/ QWidget *m_videoWidget; /** @brief Manager for qml overlay for the QQuickView **/ QmlManager *m_qmlManager; std::shared_ptr m_snaps; Mlt::Filter *m_splitEffect; std::shared_ptr m_splitProducer; int m_length; bool m_dragStarted; // TODO: Move capture stuff in own class RecManager *m_recManager; /** @brief The widget showing current time position **/ TimecodeDisplay *m_timePos; KDualAction *m_playAction; KSelectAction *m_forceSize; /** Has to be available so we can enable and disable it. */ QAction *m_loopClipAction; QAction *m_sceneVisibilityAction; QAction *m_zoomVisibilityAction; QAction *m_multitrackView; QMenu *m_contextMenu; QMenu *m_configMenu; QMenu *m_playMenu; QMenu *m_markerMenu; QPoint m_DragStartPosition; /** true if selected clip is transition, false = selected clip is clip. * Necessary because sometimes we get two signals, e.g. we get a clip and we get selected transition = nullptr. */ bool m_loopClipTransition; GenTime getSnapForPos(bool previous); QToolBar *m_toolbar; QToolButton *m_audioButton; QSlider *m_audioSlider; QAction *m_editMarker; KMessageWidget *m_infoMessage; int m_forceSizeFactor; MonitorSceneType m_lastMonitorSceneType; MonitorAudioLevel *m_audioMeterWidget; QElapsedTimer m_droppedTimer; double m_displayedFps; void adjustScrollBars(float horizontal, float vertical); void loadQmlScene(MonitorSceneType type); void updateQmlDisplay(int currentOverlay); /** @brief Check and display dropped frames */ void checkDrops(int dropped); /** @brief Create temporary Mlt::Tractor holding a clip and it's effectless clone */ void buildSplitEffect(Mlt::Producer *original); private slots: Q_DECL_DEPRECATED void seekCursor(int pos); void slotSetThumbFrame(); void slotSaveZone(); void slotSeek(); void updateClipZone(); void slotGoToMarker(QAction *action); void slotSetVolume(int volume); void slotEditMarker(); void slotExtractCurrentZone(); void onFrameDisplayed(const SharedFrame &frame); void slotStartDrag(); void setZoom(); void slotEnableEffectScene(bool enable); void slotAdjustEffectCompare(); void slotShowMenu(const QPoint pos); void slotForceSize(QAction *a); void slotSeekToKeyFrame(); /** @brief Display a non blocking error message to user **/ void warningMessage(const QString &text, int timeout = 5000, const QList &actions = QList()); void slotLockMonitor(bool lock); void slotAddEffect(const QStringList &effect); void slotSwitchPlay(); void slotEditInlineMarker(); /** @brief Pass keypress event to mainwindow */ void doKeyPressEvent(QKeyEvent *); /** @brief There was an error initializing Movit */ void gpuError(); void setOffsetX(int x); void setOffsetY(int y); /** @brief Pan monitor view */ void panView(QPoint diff); /** @brief Project monitor zone changed, inform timeline */ void updateTimelineClipZone(); void slotSeekPosition(int); void addSnapPoint(int pos); void removeSnapPoint(int pos); public slots: void slotOpenDvdFile(const QString &); // void slotSetClipProducer(DocClipBase *clip, QPoint zone = QPoint(), bool forceUpdate = false, int position = -1); - void updateClipProducer(std::shared_ptr prod); + void updateClipProducer(const std::shared_ptr &prod); void updateClipProducer(const QString &playlist); - void slotOpenClip(std::shared_ptr controller, int in = -1, int out = -1); + void slotOpenClip(const std::shared_ptr &controller, int in = -1, int out = -1); void slotRefreshMonitor(bool visible); void slotSeek(int pos); void stop() override; void start() override; void switchPlay(bool play); void slotPlay() override; void pause(); void slotPlayZone(); void slotLoopZone(); /** @brief Loops the selected item (clip or transition). */ void slotLoopClip(); void slotForward(double speed = 0); void slotRewind(double speed = 0); void slotRewindOneFrame(int diff = 1); void slotForwardOneFrame(int diff = 1); void slotStart(); void slotEnd(); void slotSetZoneStart(); void slotSetZoneEnd(bool discardLastFrame = false); void slotZoneStart(); void slotZoneEnd(); void slotLoadClipZone(const QPoint &zone); void slotSeekToNextSnap(); void slotSeekToPreviousSnap(); - void adjustRulerSize(int length, std::shared_ptr markerModel = nullptr); + void adjustRulerSize(int length, const std::shared_ptr &markerModel = nullptr); void setTimePos(const QString &pos); QPoint getZoneInfo() const; /** @brief Display the on monitor effect scene (to adjust geometry over monitor). */ void slotShowEffectScene(MonitorSceneType sceneType, bool temporary = false); bool effectSceneDisplayed(MonitorSceneType effectType); /** @brief split screen to compare clip with and without effect */ void slotSwitchCompare(bool enable); void slotMouseSeek(int eventDelta, uint modifiers) override; void slotSwitchFullScreen(bool minimizeOnly = false) override; /** @brief Display or hide the record toolbar */ void slotSwitchRec(bool enable); /** @brief Request QImage of current frame */ void slotGetCurrentImage(bool request); /** @brief Enable/disable display of monitor's audio levels widget */ void slotSwitchAudioMonitor(); /** @brief Request seeking */ void requestSeek(int pos); /** @brief Check current position to show relevant infos in qml view (markers, zone in/out, etc). */ void checkOverlay(int pos = -1); void refreshMonitorIfActive(bool directUpdate = false) override; signals: void seekPosition(int); /** @brief Request a timeline seeking if diff is true, position is a relative offset, otherwise an absolute position */ void seekTimeline(int position); void durationChanged(int); void refreshClipThumbnail(const QString &); void zoneUpdated(const QPoint &); void timelineZoneChanged(); /** @brief Editing transitions / effects over the monitor requires the renderer to send frames as QImage. * This causes a major slowdown, so we only enable it if required */ void requestFrameForAnalysis(bool); /** @brief Request a zone extraction (ffmpeg transcoding). */ void extractZone(const QString &id); void effectChanged(const QRect &); void effectPointsChanged(const QVariantList &); void addRemoveKeyframe(); void seekToNextKeyframe(); void seekToPreviousKeyframe(); void seekToKeyframe(int); void addClipToProject(const QUrl &); void showConfigDialog(int, int); /** @brief Request display of current bin clip. */ void refreshCurrentClip(); void addEffect(const QStringList &); void addMasterEffect(QString, const QStringList &); void passKeyPress(QKeyEvent *); /** @brief Enable / disable project monitor multitrack view (split view with one track in each quarter). */ void multitrackView(bool); void timeCodeUpdated(const QString &); void addMarker(); void deleteMarker(bool deleteGuide = true); void seekToPreviousSnap(); void seekToNextSnap(); void createSplitOverlay(Mlt::Filter *); void removeSplitOverlay(); void acceptRipple(bool); void switchTrimMode(int); }; #endif diff --git a/src/project/dialogs/archivewidget.cpp b/src/project/dialogs/archivewidget.cpp index caf8b2725..d471a2684 100644 --- a/src/project/dialogs/archivewidget.cpp +++ b/src/project/dialogs/archivewidget.cpp @@ -1,1100 +1,1100 @@ /*************************************************************************** * 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 "archivewidget.h" #include "bin/bin.h" #include "bin/projectclip.h" #include "bin/projectfolder.h" #include "bin/projectitemmodel.h" #include "core.h" #include "projectsettings.h" #include "titler/titlewidget.h" #include "xml/xml.hpp" #include "kdenlive_debug.h" #include #include #include #include #include #include #include #include #include ArchiveWidget::ArchiveWidget(const QString &projectName, const QDomDocument &doc, const QStringList &luma_list, QWidget *parent) : QDialog(parent) , m_requestedSize(0) , m_copyJob(nullptr) , m_name(projectName.section(QLatin1Char('.'), 0, -2)) , m_doc(doc) , m_temp(nullptr) , m_abortArchive(false) , m_extractMode(false) , m_progressTimer(nullptr) , m_extractArchive(nullptr) , m_missingClips(0) { setAttribute(Qt::WA_DeleteOnClose); setupUi(this); setWindowTitle(i18n("Archive Project")); archive_url->setUrl(QUrl::fromLocalFile(QDir::homePath())); connect(archive_url, &KUrlRequester::textChanged, this, &ArchiveWidget::slotCheckSpace); connect(this, SIGNAL(archivingFinished(bool)), this, SLOT(slotArchivingFinished(bool))); connect(this, SIGNAL(archiveProgress(int)), this, SLOT(slotArchivingProgress(int))); connect(proxy_only, &QCheckBox::stateChanged, this, &ArchiveWidget::slotProxyOnly); // Setup categories QTreeWidgetItem *videos = new QTreeWidgetItem(files_list, QStringList() << i18n("Video clips")); videos->setIcon(0, QIcon::fromTheme(QStringLiteral("video-x-generic"))); videos->setData(0, Qt::UserRole, QStringLiteral("videos")); videos->setExpanded(false); QTreeWidgetItem *sounds = new QTreeWidgetItem(files_list, QStringList() << i18n("Audio clips")); sounds->setIcon(0, QIcon::fromTheme(QStringLiteral("audio-x-generic"))); sounds->setData(0, Qt::UserRole, QStringLiteral("sounds")); sounds->setExpanded(false); QTreeWidgetItem *images = new QTreeWidgetItem(files_list, QStringList() << i18n("Image clips")); images->setIcon(0, QIcon::fromTheme(QStringLiteral("image-x-generic"))); images->setData(0, Qt::UserRole, QStringLiteral("images")); images->setExpanded(false); QTreeWidgetItem *slideshows = new QTreeWidgetItem(files_list, QStringList() << i18n("Slideshow clips")); slideshows->setIcon(0, QIcon::fromTheme(QStringLiteral("image-x-generic"))); slideshows->setData(0, Qt::UserRole, QStringLiteral("slideshows")); slideshows->setExpanded(false); QTreeWidgetItem *texts = new QTreeWidgetItem(files_list, QStringList() << i18n("Text clips")); texts->setIcon(0, QIcon::fromTheme(QStringLiteral("text-plain"))); texts->setData(0, Qt::UserRole, QStringLiteral("texts")); texts->setExpanded(false); QTreeWidgetItem *playlists = new QTreeWidgetItem(files_list, QStringList() << i18n("Playlist clips")); playlists->setIcon(0, QIcon::fromTheme(QStringLiteral("video-mlt-playlist"))); playlists->setData(0, Qt::UserRole, QStringLiteral("playlist")); playlists->setExpanded(false); QTreeWidgetItem *others = new QTreeWidgetItem(files_list, QStringList() << i18n("Other clips")); others->setIcon(0, QIcon::fromTheme(QStringLiteral("unknown"))); others->setData(0, Qt::UserRole, QStringLiteral("others")); others->setExpanded(false); QTreeWidgetItem *lumas = new QTreeWidgetItem(files_list, QStringList() << i18n("Luma files")); lumas->setIcon(0, QIcon::fromTheme(QStringLiteral("image-x-generic"))); lumas->setData(0, Qt::UserRole, QStringLiteral("lumas")); lumas->setExpanded(false); QTreeWidgetItem *proxies = new QTreeWidgetItem(files_list, QStringList() << i18n("Proxy clips")); proxies->setIcon(0, QIcon::fromTheme(QStringLiteral("video-x-generic"))); proxies->setData(0, Qt::UserRole, QStringLiteral("proxy")); proxies->setExpanded(false); // process all files QStringList allFonts; QStringList extraImageUrls; QStringList otherUrls; generateItems(lumas, luma_list); QMap slideUrls; QMap audioUrls; QMap videoUrls; QMap imageUrls; QMap playlistUrls; QMap proxyUrls; QList> clipList = pCore->projectItemModel()->getRootFolder()->childClips(); - for (std::shared_ptr clip : clipList) { + for (const std::shared_ptr &clip : clipList) { ClipType::ProducerType t = clip->clipType(); QString id = clip->binId(); if (t == ClipType::Color) { continue; } if (t == ClipType::SlideShow) { // TODO: Slideshow files slideUrls.insert(id, clip->clipUrl()); } else if (t == ClipType::Image) { imageUrls.insert(id, clip->clipUrl()); } else if (t == ClipType::QText) { allFonts << clip->getProducerProperty(QStringLiteral("family")); } else if (t == ClipType::Text) { QStringList imagefiles = TitleWidget::extractImageList(clip->getProducerProperty(QStringLiteral("xmldata"))); QStringList fonts = TitleWidget::extractFontList(clip->getProducerProperty(QStringLiteral("xmldata"))); extraImageUrls << imagefiles; allFonts << fonts; } else if (t == ClipType::Playlist) { playlistUrls.insert(id, clip->clipUrl()); QStringList files = ProjectSettings::extractPlaylistUrls(clip->clipUrl()); otherUrls << files; } else if (!clip->clipUrl().isEmpty()) { if (t == ClipType::Audio) { audioUrls.insert(id, clip->clipUrl()); } else { videoUrls.insert(id, clip->clipUrl()); // Check if we have a proxy QString proxy = clip->getProducerProperty(QStringLiteral("kdenlive:proxy")); if (!proxy.isEmpty() && proxy != QLatin1String("-") && QFile::exists(proxy)) { proxyUrls.insert(id, proxy); } } } } generateItems(images, extraImageUrls); generateItems(sounds, audioUrls); generateItems(videos, videoUrls); generateItems(images, imageUrls); generateItems(slideshows, slideUrls); generateItems(playlists, playlistUrls); generateItems(others, otherUrls); generateItems(proxies, proxyUrls); allFonts.removeDuplicates(); m_infoMessage = new KMessageWidget(this); QVBoxLayout *s = static_cast(layout()); s->insertWidget(5, m_infoMessage); m_infoMessage->setCloseButtonVisible(false); m_infoMessage->setWordWrap(true); m_infoMessage->hide(); // missing clips, warn user if (m_missingClips > 0) { QString infoText = i18np("You have %1 missing clip in your project.", "You have %1 missing clips in your project.", m_missingClips); m_infoMessage->setMessageType(KMessageWidget::Warning); m_infoMessage->setText(infoText); m_infoMessage->animatedShow(); } // TODO: fonts // Hide unused categories, add item count int total = 0; for (int i = 0; i < files_list->topLevelItemCount(); ++i) { QTreeWidgetItem *parentItem = files_list->topLevelItem(i); int items = parentItem->childCount(); if (items == 0) { files_list->topLevelItem(i)->setHidden(true); } else { if (parentItem->data(0, Qt::UserRole).toString() == QLatin1String("slideshows")) { // Special case: slideshows contain several files for (int j = 0; j < items; ++j) { total += parentItem->child(j)->data(0, Qt::UserRole + 1).toStringList().count(); } } else { total += items; } parentItem->setText(0, files_list->topLevelItem(i)->text(0) + QLatin1Char(' ') + i18np("(%1 item)", "(%1 items)", items)); } } if (m_name.isEmpty()) { m_name = i18n("Untitled"); } compressed_archive->setText(compressed_archive->text() + QStringLiteral(" (") + m_name + QStringLiteral(".tar.gz)")); project_files->setText(i18np("%1 file to archive, requires %2", "%1 files to archive, requires %2", total, KIO::convertSize(m_requestedSize))); buttonBox->button(QDialogButtonBox::Apply)->setText(i18n("Archive")); connect(buttonBox->button(QDialogButtonBox::Apply), &QAbstractButton::clicked, this, &ArchiveWidget::slotStartArchiving); buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false); slotCheckSpace(); } // Constructor for extract widget ArchiveWidget::ArchiveWidget(const QUrl &url, QWidget *parent) : QDialog(parent) , m_requestedSize(0) , m_copyJob(nullptr) , m_temp(nullptr) , m_abortArchive(false) , m_extractMode(true) , m_extractUrl(url) , m_extractArchive(nullptr) , m_missingClips(0) , m_infoMessage(nullptr) { // setAttribute(Qt::WA_DeleteOnClose); setupUi(this); m_progressTimer = new QTimer; m_progressTimer->setInterval(800); m_progressTimer->setSingleShot(false); connect(m_progressTimer, &QTimer::timeout, this, &ArchiveWidget::slotExtractProgress); connect(this, &ArchiveWidget::extractingFinished, this, &ArchiveWidget::slotExtractingFinished); connect(this, &ArchiveWidget::showMessage, this, &ArchiveWidget::slotDisplayMessage); compressed_archive->setHidden(true); proxy_only->setHidden(true); project_files->setHidden(true); files_list->setHidden(true); label->setText(i18n("Extract to")); setWindowTitle(i18n("Open Archived Project")); archive_url->setUrl(QUrl::fromLocalFile(QDir::homePath())); buttonBox->button(QDialogButtonBox::Apply)->setText(i18n("Extract")); connect(buttonBox->button(QDialogButtonBox::Apply), &QAbstractButton::clicked, this, &ArchiveWidget::slotStartExtracting); buttonBox->button(QDialogButtonBox::Apply)->setEnabled(true); adjustSize(); m_archiveThread = QtConcurrent::run(this, &ArchiveWidget::openArchiveForExtraction); } ArchiveWidget::~ArchiveWidget() { delete m_extractArchive; delete m_progressTimer; } void ArchiveWidget::slotDisplayMessage(const QString &icon, const QString &text) { icon_info->setPixmap(QIcon::fromTheme(icon).pixmap(16, 16)); text_info->setText(text); } void ArchiveWidget::slotJobResult(bool success, const QString &text) { m_infoMessage->setMessageType(success ? KMessageWidget::Positive : KMessageWidget::Warning); m_infoMessage->setText(text); m_infoMessage->animatedShow(); } void ArchiveWidget::openArchiveForExtraction() { emit showMessage(QStringLiteral("system-run"), i18n("Opening archive...")); m_extractArchive = new KTar(m_extractUrl.toLocalFile()); if (!m_extractArchive->isOpen() && !m_extractArchive->open(QIODevice::ReadOnly)) { emit showMessage(QStringLiteral("dialog-close"), i18n("Cannot open archive file:\n %1", m_extractUrl.toLocalFile())); groupBox->setEnabled(false); return; } // Check that it is a kdenlive project archive bool isProjectArchive = false; QStringList files = m_extractArchive->directory()->entries(); for (int i = 0; i < files.count(); ++i) { if (files.at(i).endsWith(QLatin1String(".kdenlive"))) { m_projectName = files.at(i); isProjectArchive = true; break; } } if (!isProjectArchive) { emit showMessage(QStringLiteral("dialog-close"), i18n("File %1\n is not an archived Kdenlive project", m_extractUrl.toLocalFile())); groupBox->setEnabled(false); buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false); return; } buttonBox->button(QDialogButtonBox::Apply)->setEnabled(true); emit showMessage(QStringLiteral("dialog-ok"), i18n("Ready")); } void ArchiveWidget::done(int r) { if (closeAccepted()) { QDialog::done(r); } } void ArchiveWidget::closeEvent(QCloseEvent *e) { if (closeAccepted()) { e->accept(); } else { e->ignore(); } } bool ArchiveWidget::closeAccepted() { if (!m_extractMode && !archive_url->isEnabled()) { // Archiving in progress, should we stop? if (KMessageBox::warningContinueCancel(this, i18n("Archiving in progress, do you want to stop it?"), i18n("Stop Archiving"), KGuiItem(i18n("Stop Archiving"))) != KMessageBox::Continue) { return false; } if (m_copyJob) { m_copyJob->kill(); } } return true; } void ArchiveWidget::generateItems(QTreeWidgetItem *parentItem, const QStringList &items) { QStringList filesList; QString fileName; int ix = 0; bool isSlideshow = parentItem->data(0, Qt::UserRole).toString() == QLatin1String("slideshows"); for (const QString &file : items) { QTreeWidgetItem *item = new QTreeWidgetItem(parentItem, QStringList() << file); fileName = QUrl::fromLocalFile(file).fileName(); if (isSlideshow) { // we store each slideshow in a separate subdirectory item->setData(0, Qt::UserRole, ix); ix++; QUrl slideUrl = QUrl::fromLocalFile(file); QDir dir(slideUrl.adjusted(QUrl::RemoveFilename).toLocalFile()); if (slideUrl.fileName().startsWith(QLatin1String(".all."))) { // MIME type slideshow (for example *.png) QStringList filters; // TODO: improve jpeg image detection with extension like jpeg, requires change in MLT image producers filters << QStringLiteral("*.") + slideUrl.fileName().section(QLatin1Char('.'), -1); dir.setNameFilters(filters); QFileInfoList resultList = dir.entryInfoList(QDir::Files); QStringList slideImages; qint64 totalSize = 0; for (int i = 0; i < resultList.count(); ++i) { totalSize += resultList.at(i).size(); slideImages << resultList.at(i).absoluteFilePath(); } item->setData(0, Qt::UserRole + 1, slideImages); item->setData(0, Qt::UserRole + 3, totalSize); m_requestedSize += static_cast(totalSize); } else { // pattern url (like clip%.3d.png) QStringList result = dir.entryList(QDir::Files); QString filter = slideUrl.fileName(); QString ext = filter.section(QLatin1Char('.'), -1); filter = filter.section(QLatin1Char('%'), 0, -2); QString regexp = QLatin1Char('^') + filter + QStringLiteral("\\d+\\.") + ext + QLatin1Char('$'); QRegExp rx(regexp); QStringList slideImages; QString directory = dir.absolutePath(); if (!directory.endsWith(QLatin1Char('/'))) { directory.append(QLatin1Char('/')); } qint64 totalSize = 0; for (const QString &path : result) { if (rx.exactMatch(path)) { totalSize += QFileInfo(directory + path).size(); slideImages << directory + path; } } item->setData(0, Qt::UserRole + 1, slideImages); item->setData(0, Qt::UserRole + 3, totalSize); m_requestedSize += static_cast(totalSize); } } else if (filesList.contains(fileName)) { // we have 2 files with same name int i = 0; QString newFileName = fileName.section(QLatin1Char('.'), 0, -2) + QLatin1Char('_') + QString::number(i) + QLatin1Char('.') + fileName.section(QLatin1Char('.'), -1); while (filesList.contains(newFileName)) { i++; newFileName = fileName.section(QLatin1Char('.'), 0, -2) + QLatin1Char('_') + QString::number(i) + QLatin1Char('.') + fileName.section(QLatin1Char('.'), -1); } fileName = newFileName; item->setData(0, Qt::UserRole, fileName); } if (!isSlideshow) { qint64 fileSize = QFileInfo(file).size(); if (fileSize <= 0) { item->setIcon(0, QIcon::fromTheme(QStringLiteral("edit-delete"))); m_missingClips++; } else { m_requestedSize += static_cast(fileSize); item->setData(0, Qt::UserRole + 3, fileSize); } filesList << fileName; } } } void ArchiveWidget::generateItems(QTreeWidgetItem *parentItem, const QMap &items) { QStringList filesList; QString fileName; int ix = 0; bool isSlideshow = parentItem->data(0, Qt::UserRole).toString() == QLatin1String("slideshows"); QMap::const_iterator it = items.constBegin(); while (it != items.constEnd()) { QString file = it.value(); QTreeWidgetItem *item = new QTreeWidgetItem(parentItem, QStringList() << file); // Store the clip's id item->setData(0, Qt::UserRole + 2, it.key()); fileName = QUrl::fromLocalFile(file).fileName(); if (isSlideshow) { // we store each slideshow in a separate subdirectory item->setData(0, Qt::UserRole, ix); ix++; QUrl slideUrl = QUrl::fromLocalFile(file); QDir dir(slideUrl.adjusted(QUrl::RemoveFilename).toLocalFile()); if (slideUrl.fileName().startsWith(QLatin1String(".all."))) { // MIME type slideshow (for example *.png) QStringList filters; // TODO: improve jpeg image detection with extension like jpeg, requires change in MLT image producers filters << QStringLiteral("*.") + slideUrl.fileName().section(QLatin1Char('.'), -1); dir.setNameFilters(filters); QFileInfoList resultList = dir.entryInfoList(QDir::Files); QStringList slideImages; qint64 totalSize = 0; for (int i = 0; i < resultList.count(); ++i) { totalSize += resultList.at(i).size(); slideImages << resultList.at(i).absoluteFilePath(); } item->setData(0, Qt::UserRole + 1, slideImages); item->setData(0, Qt::UserRole + 3, totalSize); m_requestedSize += static_cast(totalSize); } else { // pattern url (like clip%.3d.png) QStringList result = dir.entryList(QDir::Files); QString filter = slideUrl.fileName(); QString ext = filter.section(QLatin1Char('.'), -1).section(QLatin1Char('?'), 0, 0); filter = filter.section(QLatin1Char('%'), 0, -2); QString regexp = QLatin1Char('^') + filter + QStringLiteral("\\d+\\.") + ext + QLatin1Char('$'); QRegExp rx(regexp); QStringList slideImages; qint64 totalSize = 0; for (const QString &path : result) { if (rx.exactMatch(path)) { totalSize += QFileInfo(dir.absoluteFilePath(path)).size(); slideImages << dir.absoluteFilePath(path); } } item->setData(0, Qt::UserRole + 1, slideImages); item->setData(0, Qt::UserRole + 3, totalSize); m_requestedSize += static_cast(totalSize); } } else if (filesList.contains(fileName)) { // we have 2 files with same name int index2 = 0; QString newFileName = fileName.section(QLatin1Char('.'), 0, -2) + QLatin1Char('_') + QString::number(index2) + QLatin1Char('.') + fileName.section(QLatin1Char('.'), -1); while (filesList.contains(newFileName)) { index2++; newFileName = fileName.section(QLatin1Char('.'), 0, -2) + QLatin1Char('_') + QString::number(index2) + QLatin1Char('.') + fileName.section(QLatin1Char('.'), -1); } fileName = newFileName; item->setData(0, Qt::UserRole, fileName); } if (!isSlideshow) { qint64 fileSize = QFileInfo(file).size(); if (fileSize <= 0) { item->setIcon(0, QIcon::fromTheme(QStringLiteral("edit-delete"))); m_missingClips++; } else { m_requestedSize += static_cast(fileSize); item->setData(0, Qt::UserRole + 3, fileSize); } filesList << fileName; } ++it; } } void ArchiveWidget::slotCheckSpace() { KDiskFreeSpaceInfo inf = KDiskFreeSpaceInfo::freeSpaceInfo(archive_url->url().toLocalFile()); KIO::filesize_t freeSize = inf.available(); if (freeSize > m_requestedSize) { // everything is ok buttonBox->button(QDialogButtonBox::Apply)->setEnabled(true); slotDisplayMessage(QStringLiteral("dialog-ok"), i18n("Available space on drive: %1", KIO::convertSize(freeSize))); } else { buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false); slotDisplayMessage(QStringLiteral("dialog-close"), i18n("Not enough space on drive, free space: %1", KIO::convertSize(freeSize))); } } bool ArchiveWidget::slotStartArchiving(bool firstPass) { if (firstPass && ((m_copyJob != nullptr) || m_archiveThread.isRunning())) { // archiving in progress, abort if (m_copyJob) { m_copyJob->kill(KJob::EmitResult); } m_abortArchive = true; return true; } bool isArchive = compressed_archive->isChecked(); if (!firstPass) { m_copyJob = nullptr; } else { // starting archiving m_abortArchive = false; m_duplicateFiles.clear(); m_replacementList.clear(); m_foldersList.clear(); m_filesList.clear(); slotDisplayMessage(QStringLiteral("system-run"), i18n("Archiving...")); repaint(); archive_url->setEnabled(false); proxy_only->setEnabled(false); compressed_archive->setEnabled(false); } QList files; QUrl destUrl; QString destPath; QTreeWidgetItem *parentItem; bool isSlideshow = false; int items = 0; // We parse all files going into one folder, then start the copy job for (int i = 0; i < files_list->topLevelItemCount(); ++i) { parentItem = files_list->topLevelItem(i); if (parentItem->isDisabled()) { parentItem->setExpanded(false); continue; } if (parentItem->childCount() > 0) { if (parentItem->data(0, Qt::UserRole).toString() == QLatin1String("slideshows")) { QUrl slideFolder = QUrl::fromLocalFile(archive_url->url().toLocalFile() + QStringLiteral("/slideshows")); if (isArchive) { m_foldersList.append(QStringLiteral("slideshows")); } else { QDir dir(slideFolder.toLocalFile()); if (!dir.mkpath(QStringLiteral("."))) { KMessageBox::sorry(this, i18n("Cannot create directory %1", slideFolder.toLocalFile())); } } isSlideshow = true; } else { isSlideshow = false; } files_list->setCurrentItem(parentItem); parentItem->setExpanded(true); destPath = parentItem->data(0, Qt::UserRole).toString() + QLatin1Char('/'); destUrl = QUrl::fromLocalFile(archive_url->url().toLocalFile() + QLatin1Char('/') + destPath); QTreeWidgetItem *item; for (int j = 0; j < parentItem->childCount(); ++j) { item = parentItem->child(j); if (item->isDisabled()) { continue; } // Special case: slideshows items++; if (isSlideshow) { destPath += item->data(0, Qt::UserRole).toString() + QLatin1Char('/'); destUrl = QUrl::fromLocalFile(archive_url->url().toLocalFile() + QDir::separator() + destPath); QStringList srcFiles = item->data(0, Qt::UserRole + 1).toStringList(); for (int k = 0; k < srcFiles.count(); ++k) { files << QUrl::fromLocalFile(srcFiles.at(k)); } item->setDisabled(true); if (parentItem->indexOfChild(item) == parentItem->childCount() - 1) { // We have processed all slideshows parentItem->setDisabled(true); } break; } else if (item->data(0, Qt::UserRole).isNull()) { files << QUrl::fromLocalFile(item->text(0)); } else { // We must rename the destination file, since another file with same name exists // TODO: monitor progress if (isArchive) { m_filesList.insert(item->text(0), destPath + item->data(0, Qt::UserRole).toString()); } else { m_duplicateFiles.insert(QUrl::fromLocalFile(item->text(0)), QUrl::fromLocalFile(destUrl.toLocalFile() + QLatin1Char('/') + item->data(0, Qt::UserRole).toString())); } } } if (!isSlideshow) { parentItem->setDisabled(true); } break; } } if (items == 0) { // No clips to archive slotArchivingFinished(nullptr, true); return true; } if (destPath.isEmpty()) { if (m_duplicateFiles.isEmpty()) { return false; } QMapIterator i(m_duplicateFiles); if (i.hasNext()) { i.next(); QUrl startJobSrc = i.key(); QUrl startJobDst = i.value(); m_duplicateFiles.remove(startJobSrc); KIO::CopyJob *job = KIO::copyAs(startJobSrc, startJobDst, KIO::HideProgressInfo); connect(job, SIGNAL(result(KJob *)), this, SLOT(slotArchivingFinished(KJob *))); connect(job, SIGNAL(processedSize(KJob *, KIO::filesize_t)), this, SLOT(slotArchivingProgress(KJob *, KIO::filesize_t))); } return true; } if (isArchive) { m_foldersList.append(destPath); for (int i = 0; i < files.count(); ++i) { m_filesList.insert(files.at(i).toLocalFile(), destPath + files.at(i).fileName()); } slotArchivingFinished(); } else if (files.isEmpty()) { slotStartArchiving(false); } else { QDir dir(destUrl.toLocalFile()); if (!dir.mkpath(QStringLiteral("."))) { KMessageBox::sorry(this, i18n("Cannot create directory %1", destUrl.toLocalFile())); } m_copyJob = KIO::copy(files, destUrl, KIO::HideProgressInfo); connect(m_copyJob, SIGNAL(result(KJob *)), this, SLOT(slotArchivingFinished(KJob *))); connect(m_copyJob, SIGNAL(processedSize(KJob *, KIO::filesize_t)), this, SLOT(slotArchivingProgress(KJob *, KIO::filesize_t))); } if (firstPass) { progressBar->setValue(0); buttonBox->button(QDialogButtonBox::Apply)->setText(i18n("Abort")); } return true; } void ArchiveWidget::slotArchivingFinished(KJob *job, bool finished) { if (job == nullptr || job->error() == 0) { if (!finished && slotStartArchiving(false)) { // We still have files to archive return; } if (!compressed_archive->isChecked()) { // Archiving finished progressBar->setValue(100); if (processProjectFile()) { slotJobResult(true, i18n("Project was successfully archived.")); } else { slotJobResult(false, i18n("There was an error processing project file")); } } else { processProjectFile(); } } else { m_copyJob = nullptr; slotJobResult(false, i18n("There was an error while copying the files: %1", job->errorString())); } if (!compressed_archive->isChecked()) { buttonBox->button(QDialogButtonBox::Apply)->setText(i18n("Archive")); archive_url->setEnabled(true); proxy_only->setEnabled(true); compressed_archive->setEnabled(true); for (int i = 0; i < files_list->topLevelItemCount(); ++i) { files_list->topLevelItem(i)->setDisabled(false); for (int j = 0; j < files_list->topLevelItem(i)->childCount(); ++j) { files_list->topLevelItem(i)->child(j)->setDisabled(false); } } } } void ArchiveWidget::slotArchivingProgress(KJob *, KIO::filesize_t size) { progressBar->setValue(static_cast(100 * size / m_requestedSize)); } bool ArchiveWidget::processProjectFile() { QTreeWidgetItem *item; bool isArchive = compressed_archive->isChecked(); for (int i = 0; i < files_list->topLevelItemCount(); ++i) { QTreeWidgetItem *parentItem = files_list->topLevelItem(i); if (parentItem->childCount() > 0) { QDir destFolder(archive_url->url().toLocalFile() + QDir::separator() + parentItem->data(0, Qt::UserRole).toString()); bool isSlideshow = parentItem->data(0, Qt::UserRole).toString() == QLatin1String("slideshows"); for (int j = 0; j < parentItem->childCount(); ++j) { item = parentItem->child(j); QUrl src = QUrl::fromLocalFile(item->text(0)); QUrl dest = QUrl::fromLocalFile(destFolder.absolutePath()); if (isSlideshow) { dest = QUrl::fromLocalFile(parentItem->data(0, Qt::UserRole).toString() + QLatin1Char('/') + item->data(0, Qt::UserRole).toString() + QLatin1Char('/') + src.fileName()); } else if (item->data(0, Qt::UserRole).isNull()) { dest = QUrl::fromLocalFile(parentItem->data(0, Qt::UserRole).toString() + QLatin1Char('/') + src.fileName()); } else { dest = QUrl::fromLocalFile(parentItem->data(0, Qt::UserRole).toString() + QLatin1Char('/') + item->data(0, Qt::UserRole).toString()); } m_replacementList.insert(src, dest); } } } QDomElement mlt = m_doc.documentElement(); QString root = mlt.attribute(QStringLiteral("root")); if (!root.isEmpty() && !root.endsWith(QLatin1Char('/'))) { root.append(QLatin1Char('/')); } // Adjust global settings QString basePath; if (isArchive) { basePath = QStringLiteral("$CURRENTPATH"); } else { basePath = archive_url->url().adjusted(QUrl::StripTrailingSlash | QUrl::StripTrailingSlash).toLocalFile(); } // Switch to relative path mlt.removeAttribute(QStringLiteral("root")); // process kdenlive producers QDomNodeList prods = mlt.elementsByTagName(QStringLiteral("kdenlive_producer")); for (int i = 0; i < prods.count(); ++i) { QDomElement e = prods.item(i).toElement(); if (e.isNull()) { continue; } if (e.hasAttribute(QStringLiteral("resource"))) { QUrl src = QUrl::fromLocalFile(e.attribute(QStringLiteral("resource"))); QUrl dest = m_replacementList.value(src); if (!dest.isEmpty()) { e.setAttribute(QStringLiteral("resource"), dest.toLocalFile()); } } if (e.hasAttribute(QStringLiteral("kdenlive:proxy")) && e.attribute(QStringLiteral("kdenlive:proxy")) != QLatin1String("-")) { QUrl src = QUrl::fromLocalFile(e.attribute(QStringLiteral("kdenlive:proxy"))); QUrl dest = m_replacementList.value(src); if (!dest.isEmpty()) { e.setAttribute(QStringLiteral("kdenlive:proxy"), dest.toLocalFile()); } } } // process mlt producers prods = mlt.elementsByTagName(QStringLiteral("producer")); for (int i = 0; i < prods.count(); ++i) { QDomElement e = prods.item(i).toElement(); if (e.isNull()) { continue; } QString src = Xml::getXmlProperty(e, QStringLiteral("resource")); if (!src.isEmpty()) { if (QFileInfo(src).isRelative()) { src.prepend(root); } QUrl srcUrl = QUrl::fromLocalFile(src); QUrl dest = m_replacementList.value(srcUrl); if (!dest.isEmpty()) { Xml::setXmlProperty(e, QStringLiteral("resource"), dest.toLocalFile()); } } src = Xml::getXmlProperty(e, QStringLiteral("xmldata")); bool found = false; if (!src.isEmpty() && (src.contains(QLatin1String("QGraphicsPixmapItem")) || src.contains(QLatin1String("QGraphicsSvgItem")))) { // Title with images, replace paths QDomDocument titleXML; titleXML.setContent(src); QDomNodeList images = titleXML.documentElement().elementsByTagName(QLatin1String("item")); for (int j = 0; j < images.count(); ++j) { QDomNode n = images.at(j); QDomElement url = n.firstChildElement(QLatin1String("content")); if (!url.isNull() && url.hasAttribute(QLatin1String("url"))) { QUrl srcUrl = QUrl::fromLocalFile(url.attribute(QLatin1String("url"))); QUrl dest = m_replacementList.value(srcUrl); if (dest.isValid()) { url.setAttribute(QLatin1String("url"), dest.toLocalFile()); found = true; } } } if (found) { // replace content Xml::setXmlProperty(e, QStringLiteral("xmldata"), titleXML.toString()); } } } // process mlt transitions (for luma files) prods = mlt.elementsByTagName(QStringLiteral("transition")); QString attribute; for (int i = 0; i < prods.count(); ++i) { QDomElement e = prods.item(i).toElement(); if (e.isNull()) { continue; } attribute = QStringLiteral("resource"); QString src = Xml::getXmlProperty(e, attribute); if (src.isEmpty()) { attribute = QStringLiteral("luma"); } src = Xml::getXmlProperty(e, attribute); if (!src.isEmpty()) { if (QFileInfo(src).isRelative()) { src.prepend(root); } QUrl srcUrl = QUrl::fromLocalFile(src); QUrl dest = m_replacementList.value(srcUrl); if (!dest.isEmpty()) { Xml::setXmlProperty(e, attribute, dest.toLocalFile()); } } } QString playList = m_doc.toString(); if (isArchive) { QString startString(QStringLiteral("\"")); startString.append(archive_url->url().adjusted(QUrl::StripTrailingSlash).toLocalFile()); QString endString(QStringLiteral("\"")); endString.append(basePath); playList.replace(startString, endString); startString = QLatin1Char('>') + archive_url->url().adjusted(QUrl::StripTrailingSlash).toLocalFile(); endString = QLatin1Char('>') + basePath; playList.replace(startString, endString); } if (isArchive) { m_temp = new QTemporaryFile; if (!m_temp->open()) { KMessageBox::error(this, i18n("Cannot create temporary file")); } m_temp->write(playList.toUtf8()); m_temp->close(); m_archiveThread = QtConcurrent::run(this, &ArchiveWidget::createArchive); return true; } QString path = archive_url->url().toLocalFile() + QDir::separator() + m_name + QStringLiteral(".kdenlive"); QFile file(path); if (file.exists() && KMessageBox::warningYesNo(this, i18n("Output file already exists. Do you want to overwrite it?")) != KMessageBox::Yes) { return false; } if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { qCWarning(KDENLIVE_LOG) << "////// ERROR writing to file: " << path; KMessageBox::error(this, i18n("Cannot write to file %1", path)); return false; } file.write(m_doc.toString().toUtf8()); if (file.error() != QFile::NoError) { KMessageBox::error(this, i18n("Cannot write to file %1", path)); file.close(); return false; } file.close(); return true; } void ArchiveWidget::createArchive() { QString archiveName(archive_url->url().toLocalFile() + QDir::separator() + m_name + QStringLiteral(".tar.gz")); if (QFile::exists(archiveName) && KMessageBox::questionYesNo(this, i18n("File %1 already exists.\nDo you want to overwrite it?", archiveName)) == KMessageBox::No) { return; } QFileInfo dirInfo(archive_url->url().toLocalFile()); QString user = dirInfo.owner(); QString group = dirInfo.group(); KTar archive(archiveName, QStringLiteral("application/x-gzip")); archive.open(QIODevice::WriteOnly); // Create folders for (const QString &path : m_foldersList) { archive.writeDir(path, user, group); } // Add files int ix = 0; QMapIterator i(m_filesList); while (i.hasNext()) { i.next(); archive.addLocalFile(i.key(), i.value()); emit archiveProgress((int)100 * ix / m_filesList.count()); ix++; } // Add project file bool result = false; if (m_temp) { archive.addLocalFile(m_temp->fileName(), m_name + QStringLiteral(".kdenlive")); result = archive.close(); delete m_temp; m_temp = nullptr; } emit archivingFinished(result); } void ArchiveWidget::slotArchivingFinished(bool result) { if (result) { slotJobResult(true, i18n("Project was successfully archived.")); buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false); } else { slotJobResult(false, i18n("There was an error processing project file")); } progressBar->setValue(100); buttonBox->button(QDialogButtonBox::Apply)->setText(i18n("Archive")); archive_url->setEnabled(true); proxy_only->setEnabled(true); compressed_archive->setEnabled(true); for (int i = 0; i < files_list->topLevelItemCount(); ++i) { files_list->topLevelItem(i)->setDisabled(false); for (int j = 0; j < files_list->topLevelItem(i)->childCount(); ++j) { files_list->topLevelItem(i)->child(j)->setDisabled(false); } } } void ArchiveWidget::slotArchivingProgress(int p) { progressBar->setValue(p); } void ArchiveWidget::slotStartExtracting() { if (m_archiveThread.isRunning()) { // TODO: abort extracting return; } QFileInfo f(m_extractUrl.toLocalFile()); m_requestedSize = static_cast(f.size()); QDir dir(archive_url->url().toLocalFile()); if (!dir.mkpath(QStringLiteral("."))) { KMessageBox::sorry(this, i18n("Cannot create directory %1", archive_url->url().toLocalFile())); } slotDisplayMessage(QStringLiteral("system-run"), i18n("Extracting...")); buttonBox->button(QDialogButtonBox::Apply)->setText(i18n("Abort")); m_archiveThread = QtConcurrent::run(this, &ArchiveWidget::doExtracting); m_progressTimer->start(); } void ArchiveWidget::slotExtractProgress() { KIO::DirectorySizeJob *job = KIO::directorySize(archive_url->url()); connect(job, &KJob::result, this, &ArchiveWidget::slotGotProgress); } void ArchiveWidget::slotGotProgress(KJob *job) { if (!job->error()) { KIO::DirectorySizeJob *j = static_cast(job); progressBar->setValue(static_cast(100 * j->totalSize() / m_requestedSize)); } job->deleteLater(); } void ArchiveWidget::doExtracting() { m_extractArchive->directory()->copyTo(archive_url->url().toLocalFile() + QDir::separator()); m_extractArchive->close(); emit extractingFinished(); } QString ArchiveWidget::extractedProjectFile() const { return archive_url->url().toLocalFile() + QDir::separator() + m_projectName; } void ArchiveWidget::slotExtractingFinished() { m_progressTimer->stop(); // Process project file QFile file(extractedProjectFile()); bool error = false; if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { error = true; } else { QString playList = QString::fromUtf8(file.readAll()); file.close(); if (playList.isEmpty()) { error = true; } else { playList.replace(QLatin1String("$CURRENTPATH"), archive_url->url().adjusted(QUrl::StripTrailingSlash).toLocalFile()); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { qCWarning(KDENLIVE_LOG) << "////// ERROR writing to file: "; error = true; } else { file.write(playList.toUtf8()); if (file.error() != QFile::NoError) { error = true; } file.close(); } } } if (error) { KMessageBox::sorry(QApplication::activeWindow(), i18n("Cannot open project file %1", extractedProjectFile()), i18n("Cannot open file")); reject(); } else { accept(); } } void ArchiveWidget::slotProxyOnly(int onlyProxy) { m_requestedSize = 0; if (onlyProxy == Qt::Checked) { // Archive proxy clips QStringList proxyIdList; QTreeWidgetItem *parentItem = nullptr; // Build list of existing proxy ids for (int i = 0; i < files_list->topLevelItemCount(); ++i) { parentItem = files_list->topLevelItem(i); if (parentItem->data(0, Qt::UserRole).toString() == QLatin1String("proxy")) { break; } } if (!parentItem) { return; } int items = parentItem->childCount(); for (int j = 0; j < items; ++j) { proxyIdList << parentItem->child(j)->data(0, Qt::UserRole + 2).toString(); } // Parse all items to disable original clips for existing proxies for (int i = 0; i < proxyIdList.count(); ++i) { const QString &id = proxyIdList.at(i); if (id.isEmpty()) { continue; } for (int j = 0; j < files_list->topLevelItemCount(); ++j) { parentItem = files_list->topLevelItem(j); if (parentItem->data(0, Qt::UserRole).toString() == QLatin1String("proxy")) { continue; } items = parentItem->childCount(); for (int k = 0; k < items; ++k) { if (parentItem->child(k)->data(0, Qt::UserRole + 2).toString() == id) { // This item has a proxy, do not archive it parentItem->child(k)->setFlags(Qt::ItemIsSelectable); break; } } } } } else { // Archive all clips for (int i = 0; i < files_list->topLevelItemCount(); ++i) { QTreeWidgetItem *parentItem = files_list->topLevelItem(i); int items = parentItem->childCount(); for (int j = 0; j < items; ++j) { parentItem->child(j)->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable); } } } // Calculate requested size int total = 0; for (int i = 0; i < files_list->topLevelItemCount(); ++i) { QTreeWidgetItem *parentItem = files_list->topLevelItem(i); int items = parentItem->childCount(); int itemsCount = 0; bool isSlideshow = parentItem->data(0, Qt::UserRole).toString() == QLatin1String("slideshows"); for (int j = 0; j < items; ++j) { if (!parentItem->child(j)->isDisabled()) { m_requestedSize += static_cast(parentItem->child(j)->data(0, Qt::UserRole + 3).toInt()); if (isSlideshow) { total += parentItem->child(j)->data(0, Qt::UserRole + 1).toStringList().count(); } else { total++; } itemsCount++; } } parentItem->setText(0, parentItem->text(0).section(QLatin1Char('('), 0, 0) + i18np("(%1 item)", "(%1 items)", itemsCount)); } project_files->setText(i18np("%1 file to archive, requires %2", "%1 files to archive, requires %2", total, KIO::convertSize(m_requestedSize))); slotCheckSpace(); } diff --git a/src/project/dialogs/projectsettings.cpp b/src/project/dialogs/projectsettings.cpp index 4fbb95e5f..da9a84dc6 100644 --- a/src/project/dialogs/projectsettings.cpp +++ b/src/project/dialogs/projectsettings.cpp @@ -1,865 +1,865 @@ /*************************************************************************** * Copyright (C) 2016 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 "projectsettings.h" #include "bin/bin.h" #include "bin/projectclip.h" #include "bin/projectfolder.h" #include "bin/projectitemmodel.h" #include "core.h" #include "dialogs/encodingprofilesdialog.h" #include "dialogs/profilesdialog.h" #include "doc/kdenlivedoc.h" #include "kdenlivesettings.h" #include "mltcontroller/clipcontroller.h" #include "profiles/profilemodel.hpp" #include "project/dialogs/profilewidget.h" #include "project/dialogs/temporarydata.h" #include "titler/titlewidget.h" #include "xml/xml.hpp" #include "kdenlive_debug.h" #include #include #include #include #include #include #include #include #include class NoEditDelegate : public QStyledItemDelegate { public: NoEditDelegate(QObject *parent = nullptr) : QStyledItemDelegate(parent) { } QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override { Q_UNUSED(parent); Q_UNUSED(option); Q_UNUSED(index); return nullptr; } }; ProjectSettings::ProjectSettings(KdenliveDoc *doc, QMap metadata, const QStringList &lumas, int videotracks, int audiotracks, const QString & /*projectPath*/, bool readOnlyTracks, bool savedProject, QWidget *parent) : QDialog(parent) , m_savedProject(savedProject) , m_lumas(lumas) { setupUi(this); tabWidget->setTabBarAutoHide(true); auto *vbox = new QVBoxLayout; vbox->setContentsMargins(0, 0, 0, 0); m_pw = new ProfileWidget(this); vbox->addWidget(m_pw); profile_box->setLayout(vbox); profile_box->setTitle(i18n("Select the profile (preset) of the project")); list_search->setTreeWidget(files_list); project_folder->setMode(KFile::Directory); m_buttonOk = buttonBox->button(QDialogButtonBox::Ok); // buttonOk->setEnabled(false); audio_thumbs->setChecked(KdenliveSettings::audiothumbnails()); video_thumbs->setChecked(KdenliveSettings::videothumbnails()); audio_tracks->setValue(audiotracks); video_tracks->setValue(videotracks); connect(generate_proxy, &QAbstractButton::toggled, proxy_minsize, &QWidget::setEnabled); connect(generate_imageproxy, &QAbstractButton::toggled, proxy_imageminsize, &QWidget::setEnabled); connect(generate_imageproxy, &QAbstractButton::toggled, image_label, &QWidget::setEnabled); connect(generate_imageproxy, &QAbstractButton::toggled, proxy_imagesize, &QWidget::setEnabled); QString currentProf; if (doc) { currentProf = pCore->getCurrentProfile()->path(); enable_proxy->setChecked(doc->getDocumentProperty(QStringLiteral("enableproxy")).toInt() != 0); generate_proxy->setChecked(doc->getDocumentProperty(QStringLiteral("generateproxy")).toInt() != 0); proxy_minsize->setValue(doc->getDocumentProperty(QStringLiteral("proxyminsize")).toInt()); m_proxyparameters = doc->getDocumentProperty(QStringLiteral("proxyparams")); m_initialExternalProxyProfile = doc->getDocumentProperty(QStringLiteral("externalproxyparams")); generate_imageproxy->setChecked(doc->getDocumentProperty(QStringLiteral("generateimageproxy")).toInt() != 0); proxy_imageminsize->setValue(doc->getDocumentProperty(QStringLiteral("proxyimageminsize")).toInt()); proxy_imagesize->setValue(doc->getDocumentProperty(QStringLiteral("proxyimagesize")).toInt()); m_proxyextension = doc->getDocumentProperty(QStringLiteral("proxyextension")); external_proxy->setChecked(doc->getDocumentProperty(QStringLiteral("enableexternalproxy")).toInt() != 0); m_previewparams = doc->getDocumentProperty(QStringLiteral("previewparameters")); m_previewextension = doc->getDocumentProperty(QStringLiteral("previewextension")); QString storageFolder = doc->getDocumentProperty(QStringLiteral("storagefolder")); if (!storageFolder.isEmpty()) { custom_folder->setChecked(true); } project_folder->setUrl(QUrl::fromLocalFile(doc->projectTempFolder())); auto *cacheWidget = new TemporaryData(doc, true, this); connect(cacheWidget, &TemporaryData::disableProxies, this, &ProjectSettings::disableProxies); connect(cacheWidget, &TemporaryData::disablePreview, this, &ProjectSettings::disablePreview); tabWidget->addTab(cacheWidget, i18n("Cache Data")); } else { currentProf = KdenliveSettings::default_profile(); enable_proxy->setChecked(KdenliveSettings::enableproxy()); external_proxy->setChecked(KdenliveSettings::externalproxy()); qDebug() << "//// INITIAL REPORT; ENABLE EXT PROCY: " << KdenliveSettings::externalproxy() << "\n++++++++"; m_initialExternalProxyProfile = KdenliveSettings::externalProxyProfile(); generate_proxy->setChecked(KdenliveSettings::generateproxy()); proxy_minsize->setValue(KdenliveSettings::proxyminsize()); m_proxyparameters = KdenliveSettings::proxyparams(); generate_imageproxy->setChecked(KdenliveSettings::generateimageproxy()); proxy_imageminsize->setValue(KdenliveSettings::proxyimageminsize()); m_proxyextension = KdenliveSettings::proxyextension(); m_previewparams = KdenliveSettings::previewparams(); m_previewextension = KdenliveSettings::previewextension(); custom_folder->setChecked(KdenliveSettings::customprojectfolder()); project_folder->setUrl(QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::CacheLocation))); } // Select profile m_pw->loadProfile(currentProf); proxy_minsize->setEnabled(generate_proxy->isChecked()); proxy_imageminsize->setEnabled(generate_imageproxy->isChecked()); loadProxyProfiles(); loadPreviewProfiles(); loadExternalProxyProfiles(); // Proxy GUI stuff proxy_showprofileinfo->setIcon(QIcon::fromTheme(QStringLiteral("help-about"))); proxy_showprofileinfo->setToolTip(i18n("Show default profile parameters")); proxy_manageprofile->setIcon(QIcon::fromTheme(QStringLiteral("configure"))); proxy_manageprofile->setToolTip(i18n("Manage proxy profiles")); connect(proxy_manageprofile, &QAbstractButton::clicked, this, &ProjectSettings::slotManageEncodingProfile); proxy_profile->setToolTip(i18n("Select default proxy profile")); connect(proxy_profile, QOverload::of(&QComboBox::currentIndexChanged), this, &ProjectSettings::slotUpdateProxyParams); proxyparams->setVisible(false); proxyparams->setMaximumHeight(QFontMetrics(font()).lineSpacing() * 5); connect(proxy_showprofileinfo, &QAbstractButton::clicked, proxyparams, &QWidget::setVisible); external_proxy_profile->setToolTip(i18n("Select camcorder profile")); // Preview GUI stuff preview_showprofileinfo->setIcon(QIcon::fromTheme(QStringLiteral("help-about"))); preview_showprofileinfo->setToolTip(i18n("Show default profile parameters")); preview_manageprofile->setIcon(QIcon::fromTheme(QStringLiteral("configure"))); preview_manageprofile->setToolTip(i18n("Manage timeline preview profiles")); connect(preview_manageprofile, &QAbstractButton::clicked, this, &ProjectSettings::slotManagePreviewProfile); preview_profile->setToolTip(i18n("Select default preview profile")); connect(preview_profile, QOverload::of(&QComboBox::currentIndexChanged), this, &ProjectSettings::slotUpdatePreviewParams); previewparams->setVisible(false); previewparams->setMaximumHeight(QFontMetrics(font()).lineSpacing() * 5); connect(preview_showprofileinfo, &QAbstractButton::clicked, previewparams, &QWidget::setVisible); if (readOnlyTracks) { video_tracks->setEnabled(false); audio_tracks->setEnabled(false); } metadata_list->setItemDelegateForColumn(0, new NoEditDelegate(this)); connect(metadata_list, &QTreeWidget::itemDoubleClicked, this, &ProjectSettings::slotEditMetadata); // Metadata list QTreeWidgetItem *item = new QTreeWidgetItem(metadata_list, QStringList() << i18n("Title")); item->setData(0, Qt::UserRole, QStringLiteral("meta.attr.title.markup")); if (metadata.contains(QStringLiteral("meta.attr.title.markup"))) { item->setText(1, metadata.value(QStringLiteral("meta.attr.title.markup"))); metadata.remove(QStringLiteral("meta.attr.title.markup")); } item->setFlags(Qt::ItemIsEditable | Qt::ItemIsSelectable | Qt::ItemIsEnabled); item = new QTreeWidgetItem(metadata_list, QStringList() << i18n("Author")); item->setData(0, Qt::UserRole, QStringLiteral("meta.attr.author.markup")); if (metadata.contains(QStringLiteral("meta.attr.author.markup"))) { item->setText(1, metadata.value(QStringLiteral("meta.attr.author.markup"))); metadata.remove(QStringLiteral("meta.attr.author.markup")); } else if (metadata.contains(QStringLiteral("meta.attr.artist.markup"))) { item->setText(0, i18n("Artist")); item->setData(0, Qt::UserRole, QStringLiteral("meta.attr.artist.markup")); item->setText(1, metadata.value(QStringLiteral("meta.attr.artist.markup"))); metadata.remove(QStringLiteral("meta.attr.artist.markup")); } item->setFlags(Qt::ItemIsEditable | Qt::ItemIsSelectable | Qt::ItemIsEnabled); item = new QTreeWidgetItem(metadata_list, QStringList() << i18n("Copyright")); item->setData(0, Qt::UserRole, QStringLiteral("meta.attr.copyright.markup")); if (metadata.contains(QStringLiteral("meta.attr.copyright.markup"))) { item->setText(1, metadata.value(QStringLiteral("meta.attr.copyright.markup"))); metadata.remove(QStringLiteral("meta.attr.copyright.markup")); } item->setFlags(Qt::ItemIsEditable | Qt::ItemIsSelectable | Qt::ItemIsEnabled); item = new QTreeWidgetItem(metadata_list, QStringList() << i18n("Year")); item->setData(0, Qt::UserRole, QStringLiteral("meta.attr.year.markup")); if (metadata.contains(QStringLiteral("meta.attr.year.markup"))) { item->setText(1, metadata.value(QStringLiteral("meta.attr.year.markup"))); metadata.remove(QStringLiteral("meta.attr.year.markup")); } else if (metadata.contains(QStringLiteral("meta.attr.date.markup"))) { item->setText(0, i18n("Date")); item->setData(0, Qt::UserRole, QStringLiteral("meta.attr.date.markup")); item->setText(1, metadata.value(QStringLiteral("meta.attr.date.markup"))); metadata.remove(QStringLiteral("meta.attr.date.markup")); } item->setFlags(Qt::ItemIsEditable | Qt::ItemIsSelectable | Qt::ItemIsEnabled); QMap::const_iterator meta = metadata.constBegin(); while (meta != metadata.constEnd()) { item = new QTreeWidgetItem(metadata_list, QStringList() << meta.key().section(QLatin1Char('.'), 2, 2)); item->setData(0, Qt::UserRole, meta.key()); item->setText(1, meta.value()); item->setFlags(Qt::ItemIsEditable | Qt::ItemIsSelectable | Qt::ItemIsEnabled); ++meta; } connect(add_metadata, &QAbstractButton::clicked, this, &ProjectSettings::slotAddMetadataField); connect(delete_metadata, &QAbstractButton::clicked, this, &ProjectSettings::slotDeleteMetadataField); add_metadata->setIcon(QIcon::fromTheme(QStringLiteral("list-add"))); delete_metadata->setIcon(QIcon::fromTheme(QStringLiteral("list-remove"))); if (doc != nullptr) { slotUpdateFiles(); connect(delete_unused, &QAbstractButton::clicked, this, &ProjectSettings::slotDeleteUnused); } else { tabWidget->removeTab(2); tabWidget->removeTab(1); } connect(project_folder, &KUrlRequester::textChanged, this, &ProjectSettings::slotUpdateButton); connect(button_export, &QAbstractButton::clicked, this, &ProjectSettings::slotExportToText); // Delete unused files is not implemented delete_unused->setVisible(false); } void ProjectSettings::slotEditMetadata(QTreeWidgetItem *item, int) { metadata_list->editItem(item, 1); } void ProjectSettings::slotDeleteUnused() { QStringList toDelete; // TODO /* QList list = m_projectList->documentClipList(); for (int i = 0; i < list.count(); ++i) { DocClipBase *clip = list.at(i); if (clip->numReferences() == 0 && clip->clipType() != SlideShow) { QUrl url = clip->fileURL(); if (url.isValid() && !toDelete.contains(url.path())) toDelete << url.path(); } } // make sure our urls are not used in another clip for (int i = 0; i < list.count(); ++i) { DocClipBase *clip = list.at(i); if (clip->numReferences() > 0) { QUrl url = clip->fileURL(); if (url.isValid() && toDelete.contains(url.path())) toDelete.removeAll(url.path()); } } if (toDelete.count() == 0) { // No physical url to delete, we only remove unused clips from project (color clips for example have no physical url) if (KMessageBox::warningContinueCancel(this, i18n("This will remove all unused clips from your project."), i18n("Clean up project")) == KMessageBox::Cancel) return; m_projectList->cleanup(); slotUpdateFiles(); return; } if (KMessageBox::warningYesNoList(this, i18n("This will remove the following files from your hard drive.\nThis action cannot be undone, only use if you know what you are doing.\nAre you sure you want to continue?"), toDelete, i18n("Delete unused clips")) != KMessageBox::Yes) return; m_projectList->trashUnusedClips(); slotUpdateFiles(); */ } void ProjectSettings::slotUpdateFiles(bool cacheOnly) { qDebug() << "// UPDATING PROJECT FILES\n----------\n-----------"; m_projectProxies.clear(); m_projectThumbs.clear(); if (cacheOnly) { return; } files_list->clear(); // List all files that are used in the project. That also means: // images included in slideshow and titles, files in playlist clips // TODO: images used in luma transitions? // Setup categories QTreeWidgetItem *videos = new QTreeWidgetItem(files_list, QStringList() << i18n("Video clips")); videos->setIcon(0, QIcon::fromTheme(QStringLiteral("video-x-generic"))); videos->setExpanded(true); QTreeWidgetItem *sounds = new QTreeWidgetItem(files_list, QStringList() << i18n("Audio clips")); sounds->setIcon(0, QIcon::fromTheme(QStringLiteral("audio-x-generic"))); sounds->setExpanded(true); QTreeWidgetItem *images = new QTreeWidgetItem(files_list, QStringList() << i18n("Image clips")); images->setIcon(0, QIcon::fromTheme(QStringLiteral("image-x-generic"))); images->setExpanded(true); QTreeWidgetItem *slideshows = new QTreeWidgetItem(files_list, QStringList() << i18n("Slideshow clips")); slideshows->setIcon(0, QIcon::fromTheme(QStringLiteral("image-x-generic"))); slideshows->setExpanded(true); QTreeWidgetItem *texts = new QTreeWidgetItem(files_list, QStringList() << i18n("Text clips")); texts->setIcon(0, QIcon::fromTheme(QStringLiteral("text-plain"))); texts->setExpanded(true); QTreeWidgetItem *playlists = new QTreeWidgetItem(files_list, QStringList() << i18n("Playlist clips")); playlists->setIcon(0, QIcon::fromTheme(QStringLiteral("video-mlt-playlist"))); playlists->setExpanded(true); QTreeWidgetItem *others = new QTreeWidgetItem(files_list, QStringList() << i18n("Other clips")); others->setIcon(0, QIcon::fromTheme(QStringLiteral("unknown"))); others->setExpanded(true); int count = 0; QStringList allFonts; for (const QString &file : m_lumas) { count++; new QTreeWidgetItem(images, QStringList() << file); } QList> clipList = pCore->projectItemModel()->getRootFolder()->childClips(); - for (std::shared_ptr clip : clipList) { + for (const std::shared_ptr &clip : clipList) { switch (clip->clipType()) { case ClipType::Color: // ignore color clips in list, there is no real file break; case ClipType::SlideShow: { const QStringList subfiles = extractSlideshowUrls(clip->clipUrl()); for (const QString &file : subfiles) { count++; new QTreeWidgetItem(slideshows, QStringList() << file); } break; } case ClipType::Text: { new QTreeWidgetItem(texts, QStringList() << clip->clipUrl()); const QStringList imagefiles = TitleWidget::extractImageList(clip->getProducerProperty(QStringLiteral("xmldata"))); const QStringList fonts = TitleWidget::extractFontList(clip->getProducerProperty(QStringLiteral("xmldata"))); for (const QString &file : imagefiles) { new QTreeWidgetItem(images, QStringList() << file); } allFonts << fonts; break; } case ClipType::Audio: new QTreeWidgetItem(sounds, QStringList() << clip->clipUrl()); break; case ClipType::Image: new QTreeWidgetItem(images, QStringList() << clip->clipUrl()); break; case ClipType::Playlist: { new QTreeWidgetItem(playlists, QStringList() << clip->clipUrl()); const QStringList files = extractPlaylistUrls(clip->clipUrl()); for (const QString &file : files) { new QTreeWidgetItem(others, QStringList() << file); } break; } case ClipType::Unknown: new QTreeWidgetItem(others, QStringList() << clip->clipUrl()); break; default: new QTreeWidgetItem(videos, QStringList() << clip->clipUrl()); break; } } uint used = 0; uint unUsed = 0; qint64 usedSize = 0; qint64 unUsedSize = 0; pCore->bin()->getBinStats(&used, &unUsed, &usedSize, &unUsedSize); allFonts.removeDuplicates(); // Hide unused categories for (int j = 0; j < files_list->topLevelItemCount(); ++j) { if (files_list->topLevelItem(j)->childCount() == 0) { files_list->topLevelItem(j)->setHidden(true); } } files_count->setText(QString::number(count)); fonts_list->addItems(allFonts); if (allFonts.isEmpty()) { fonts_list->setHidden(true); label_fonts->setHidden(true); } used_count->setText(QString::number(used)); used_size->setText(KIO::convertSize(static_cast(usedSize))); unused_count->setText(QString::number(unUsed)); unused_size->setText(KIO::convertSize(static_cast(unUsedSize))); delete_unused->setEnabled(unUsed > 0); } const QString ProjectSettings::selectedPreview() const { return preview_profile->itemData(preview_profile->currentIndex()).toString(); } void ProjectSettings::accept() { if (selectedProfile().isEmpty()) { KMessageBox::error(this, i18n("Please select a video profile")); return; } QString params = preview_profile->itemData(preview_profile->currentIndex()).toString(); if (!params.isEmpty()) { if (params.section(QLatin1Char(';'), 0, 0) != m_previewparams || params.section(QLatin1Char(';'), 1, 1) != m_previewextension) { // Timeline preview settings changed, warn if there are existing previews if (pCore->hasTimelinePreview() && KMessageBox::warningContinueCancel(this, i18n("You changed the timeline preview profile. This will remove all existing timeline previews for " "this project.\n Are you sure you want to proceed?"), i18n("Confirm profile change")) == KMessageBox::Cancel) { return; } } } if (selectedProfile() != pCore->getCurrentProfile()->path()) { if (KMessageBox::warningContinueCancel( this, i18n("Changing the profile of your project cannot be undone.\nIt is recommended to save your project before attempting this operation " "that might cause some corruption in transitions.\n Are you sure you want to proceed?"), i18n("Confirm profile change")) == KMessageBox::Cancel) { return; } } QDialog::accept(); } void ProjectSettings::slotUpdateButton(const QString &path) { if (path.isEmpty()) { m_buttonOk->setEnabled(false); } else { m_buttonOk->setEnabled(true); slotUpdateFiles(true); } } QString ProjectSettings::selectedProfile() const { return m_pw->selectedProfile(); } QUrl ProjectSettings::selectedFolder() const { return project_folder->url(); } QPoint ProjectSettings::tracks() const { QPoint p; p.setX(video_tracks->value()); p.setY(audio_tracks->value()); return p; } bool ProjectSettings::enableVideoThumbs() const { return video_thumbs->isChecked(); } bool ProjectSettings::enableAudioThumbs() const { return audio_thumbs->isChecked(); } bool ProjectSettings::useProxy() const { return enable_proxy->isChecked(); } bool ProjectSettings::useExternalProxy() const { return external_proxy->isChecked(); } bool ProjectSettings::generateProxy() const { return generate_proxy->isChecked(); } bool ProjectSettings::generateImageProxy() const { return generate_imageproxy->isChecked(); } int ProjectSettings::proxyMinSize() const { return proxy_minsize->value(); } int ProjectSettings::proxyImageMinSize() const { return proxy_imageminsize->value(); } int ProjectSettings::proxyImageSize() const { return proxy_imagesize->value(); } QString ProjectSettings::externalProxyParams() const { return external_proxy_profile->currentData().toString(); } QString ProjectSettings::proxyParams() const { QString params = proxy_profile->currentData().toString(); return params.section(QLatin1Char(';'), 0, 0); } QString ProjectSettings::proxyExtension() const { QString params = proxy_profile->currentData().toString(); return params.section(QLatin1Char(';'), 1, 1); } // static QStringList ProjectSettings::extractPlaylistUrls(const QString &path) { QStringList urls; QDomDocument doc; QFile file(path); if (!file.open(QIODevice::ReadOnly)) { return urls; } if (!doc.setContent(&file)) { file.close(); return urls; } file.close(); QString root = doc.documentElement().attribute(QStringLiteral("root")); if (!root.isEmpty() && !root.endsWith(QLatin1Char('/'))) { root.append(QLatin1Char('/')); } QDomNodeList files = doc.elementsByTagName(QStringLiteral("producer")); for (int i = 0; i < files.count(); ++i) { QDomElement e = files.at(i).toElement(); QString type = Xml::getXmlProperty(e, QStringLiteral("mlt_service")); if (type != QLatin1String("colour")) { QString url = Xml::getXmlProperty(e, QStringLiteral("resource")); if (type == QLatin1String("timewarp")) { url = Xml::getXmlProperty(e, QStringLiteral("warp_resource")); } else if (type == QLatin1String("framebuffer")) { url = url.section(QLatin1Char('?'), 0, 0); } if (!url.isEmpty()) { if (QFileInfo(url).isRelative()) { url.prepend(root); } if (url.section(QLatin1Char('.'), 0, -2).endsWith(QLatin1String("/.all"))) { // slideshow clip, extract image urls urls << extractSlideshowUrls(url); } else { urls << url; } if (url.endsWith(QLatin1String(".mlt")) || url.endsWith(QLatin1String(".kdenlive"))) { // TODO: Do something to avoid infinite loops if 2 files reference themselves... urls << extractPlaylistUrls(url); } } } } // luma files for transitions files = doc.elementsByTagName(QStringLiteral("transition")); for (int i = 0; i < files.count(); ++i) { QDomElement e = files.at(i).toElement(); QString url = Xml::getXmlProperty(e, QStringLiteral("resource")); if (!url.isEmpty()) { if (QFileInfo(url).isRelative()) { url.prepend(root); } urls << url; } } return urls; } // static QStringList ProjectSettings::extractSlideshowUrls(const QString &url) { QStringList urls; QString path = QFileInfo(url).absolutePath(); QDir dir(path); if (url.contains(QStringLiteral(".all."))) { // this is a MIME slideshow, like *.jpeg QString ext = url.section(QLatin1Char('.'), -1); QStringList filters; filters << QStringLiteral("*.") + ext; dir.setNameFilters(filters); QStringList result = dir.entryList(QDir::Files); urls.append(path + filters.at(0) + QStringLiteral(" (") + i18np("1 image found", "%1 images found", result.count()) + QLatin1Char(')')); } else { // this is a pattern slideshow, like sequence%4d.jpg QString filter = QFileInfo(url).fileName(); QString ext = filter.section(QLatin1Char('.'), -1); filter = filter.section(QLatin1Char('%'), 0, -2); QString regexp = QLatin1Char('^') + filter + QStringLiteral("\\d+\\.") + ext + QLatin1Char('$'); QRegExp rx(regexp); int count = 0; const QStringList result = dir.entryList(QDir::Files); for (const QString &p : result) { if (rx.exactMatch(p)) { count++; } } urls.append(url + QStringLiteral(" (") + i18np("1 image found", "%1 images found", count) + QLatin1Char(')')); } return urls; } void ProjectSettings::slotExportToText() { const QString savePath = QFileDialog::getSaveFileName(this, QString(), project_folder->url().toLocalFile(), QStringLiteral("text/plain")); if (savePath.isEmpty()) { return; } QString text; text.append(i18n("Project folder: %1", project_folder->url().toLocalFile()) + '\n'); text.append(i18n("Project profile: %1", m_pw->selectedProfile()) + '\n'); text.append(i18n("Total clips: %1 (%2 used in timeline).", files_count->text(), used_count->text()) + "\n\n"); for (int i = 0; i < files_list->topLevelItemCount(); ++i) { if (files_list->topLevelItem(i)->childCount() > 0) { text.append('\n' + files_list->topLevelItem(i)->text(0) + ":\n\n"); for (int j = 0; j < files_list->topLevelItem(i)->childCount(); ++j) { text.append(files_list->topLevelItem(i)->child(j)->text(0) + '\n'); } } } QTemporaryFile tmpfile; if (!tmpfile.open()) { qCWarning(KDENLIVE_LOG) << "///// CANNOT CREATE TMP FILE in: " << tmpfile.fileName(); return; } QFile xmlf(tmpfile.fileName()); if (!xmlf.open(QIODevice::WriteOnly)) { return; } xmlf.write(text.toUtf8()); if (xmlf.error() != QFile::NoError) { xmlf.close(); return; } xmlf.close(); KIO::FileCopyJob *copyjob = KIO::file_copy(QUrl::fromLocalFile(tmpfile.fileName()), QUrl::fromLocalFile(savePath)); copyjob->exec(); } void ProjectSettings::slotUpdateProxyParams() { QString params = proxy_profile->currentData().toString(); proxyparams->setPlainText(params.section(QLatin1Char(';'), 0, 0)); } void ProjectSettings::slotUpdatePreviewParams() { QString params = preview_profile->currentData().toString(); previewparams->setPlainText(params.section(QLatin1Char(';'), 0, 0)); } const QMap ProjectSettings::metadata() const { QMap metadata; for (int i = 0; i < metadata_list->topLevelItemCount(); ++i) { QTreeWidgetItem *item = metadata_list->topLevelItem(i); if (!item->text(1).simplified().isEmpty()) { // Insert metadata entry QString key = item->data(0, Qt::UserRole).toString(); if (key.isEmpty()) { key = QStringLiteral("meta.attr.") + item->text(0).simplified() + QStringLiteral(".markup"); } QString value = item->text(1); metadata.insert(key, value); } } return metadata; } void ProjectSettings::slotAddMetadataField() { QString metaField = QInputDialog::getText(this, i18n("Metadata"), i18n("Metadata")); if (metaField.isEmpty()) { return; } QTreeWidgetItem *item = new QTreeWidgetItem(metadata_list, QStringList() << metaField); item->setFlags(Qt::ItemIsEditable | Qt::ItemIsSelectable | Qt::ItemIsEnabled); } void ProjectSettings::slotDeleteMetadataField() { QTreeWidgetItem *item = metadata_list->currentItem(); if (item) { delete item; } } void ProjectSettings::slotManageEncodingProfile() { QPointer d = new EncodingProfilesDialog(0); d->exec(); delete d; loadProxyProfiles(); } void ProjectSettings::slotManagePreviewProfile() { QPointer d = new EncodingProfilesDialog(1); d->exec(); delete d; loadPreviewProfiles(); } void ProjectSettings::loadProxyProfiles() { // load proxy profiles KConfig conf(QStringLiteral("encodingprofiles.rc"), KConfig::CascadeConfig, QStandardPaths::AppDataLocation); KConfigGroup group(&conf, "proxy"); QMap values = group.entryMap(); QMapIterator k(values); int ix = -1; proxy_profile->clear(); if (KdenliveSettings::vaapiEnabled() || KdenliveSettings::nvencEnabled()) { proxy_profile->addItem(QIcon::fromTheme(QStringLiteral("speedometer")), i18n("Automatic")); } else { proxy_profile->addItem(i18n("Automatic")); } while (k.hasNext()) { k.next(); if (!k.key().isEmpty()) { QString params = k.value().section(QLatin1Char(';'), 0, 0); QString extension = k.value().section(QLatin1Char(';'), 1, 1); if (ix == -1 && ((params == m_proxyparameters && extension == m_proxyextension))) { // this is the current profile ix = proxy_profile->count(); } if (params.contains(QLatin1String("vaapi"))) { proxy_profile->addItem(KdenliveSettings::vaapiEnabled() ? QIcon::fromTheme(QStringLiteral("speedometer")) : QIcon::fromTheme(QStringLiteral("dialog-cancel")), k.key(), k.value()); } else if (params.contains(QLatin1String("nvenc"))) { proxy_profile->addItem(KdenliveSettings::nvencEnabled() ? QIcon::fromTheme(QStringLiteral("speedometer")) : QIcon::fromTheme(QStringLiteral("dialog-cancel")), k.key(), k.value()); } else { proxy_profile->addItem(k.key(), k.value()); } } } if (ix == -1) { // Current project proxy settings not found if (m_proxyparameters.isEmpty() && m_proxyextension.isEmpty()) { ix = 0; } else { ix = proxy_profile->count(); proxy_profile->addItem(i18n("Current Settings"), QString(m_proxyparameters + QLatin1Char(';') + m_proxyextension)); } } proxy_profile->setCurrentIndex(ix); slotUpdateProxyParams(); } void ProjectSettings::loadExternalProxyProfiles() { // load proxy profiles KConfig conf(QStringLiteral("externalproxies.rc"), KConfig::CascadeConfig, QStandardPaths::AppDataLocation); KConfigGroup group(&conf, "proxy"); QMap values = group.entryMap(); QMapIterator k(values); int ix = -1; external_proxy_profile->clear(); while (k.hasNext()) { k.next(); if (!k.key().isEmpty()) { if (ix == -1 && k.value() == m_initialExternalProxyProfile) { // this is the current profile ix = external_proxy_profile->count(); } if (k.value().contains(QLatin1Char(';'))) { external_proxy_profile->addItem(k.key(), k.value()); } } } if (ix == -1 && !m_initialExternalProxyProfile.isEmpty()) { // Current project proxy settings not found ix = external_proxy_profile->count(); external_proxy_profile->addItem(i18n("Current Settings"), m_initialExternalProxyProfile); } external_proxy_profile->setCurrentIndex(ix); } void ProjectSettings::loadPreviewProfiles() { // load proxy profiles KConfig conf(QStringLiteral("encodingprofiles.rc"), KConfig::CascadeConfig, QStandardPaths::AppDataLocation); KConfigGroup group(&conf, "timelinepreview"); QMap values = group.entryMap(); QMapIterator k(values); int ix = -1; preview_profile->clear(); while (k.hasNext()) { k.next(); if (!k.key().isEmpty()) { QString params = k.value().section(QLatin1Char(';'), 0, 0); QString extension = k.value().section(QLatin1Char(';'), 1, 1); if (ix == -1 && (params == m_previewparams && extension == m_previewextension)) { // this is the current profile ix = preview_profile->count(); } if (params.contains(QLatin1String("nvenc"))) { preview_profile->addItem(KdenliveSettings::nvencEnabled() ? QIcon::fromTheme(QStringLiteral("speedometer")) : QIcon::fromTheme(QStringLiteral("dialog-cancel")), k.key(), k.value()); } else { preview_profile->addItem(k.key(), k.value()); } } } if (ix == -1) { // Current project proxy settings not found ix = preview_profile->count(); if (m_previewparams.isEmpty() && m_previewextension.isEmpty()) { // Leave empty, will be automatically detected if (KdenliveSettings::nvencEnabled()) { preview_profile->addItem(QIcon::fromTheme(QStringLiteral("speedometer")), i18n("Automatic")); } else { preview_profile->addItem(i18n("Automatic")); } } else { if (m_previewparams.contains(QLatin1String("nvenc"))) { preview_profile->addItem(QIcon::fromTheme(QStringLiteral("speedometer")), i18n("Current Settings"), QString(m_previewparams + QLatin1Char(';') + m_previewextension)); } else { preview_profile->addItem(i18n("Current Settings"), QString(m_previewparams + QLatin1Char(';') + m_previewextension)); } } } preview_profile->setCurrentIndex(ix); slotUpdatePreviewParams(); } const QString ProjectSettings::storageFolder() const { if (custom_folder->isChecked()) { return project_folder->url().toLocalFile(); } return QString(); } diff --git a/src/project/projectmanager.cpp b/src/project/projectmanager.cpp index 442a97b46..bffa89dea 100644 --- a/src/project/projectmanager.cpp +++ b/src/project/projectmanager.cpp @@ -1,958 +1,958 @@ /* Copyright (C) 2014 Till Theato 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 3 of the License, or (at your option) any later version. */ #include "projectmanager.h" #include "bin/bin.h" #include "bin/projectitemmodel.h" #include "core.h" #include "doc/kdenlivedoc.h" #include "jobs/jobmanager.h" #include "kdenlivesettings.h" #include "mainwindow.h" #include "monitor/monitormanager.h" #include "profiles/profilemodel.hpp" #include "project/dialogs/archivewidget.h" #include "project/dialogs/backupwidget.h" #include "project/dialogs/noteswidget.h" #include "project/dialogs/projectsettings.h" #include "utils/thumbnailcache.hpp" #include "xml/xml.hpp" // Temporary for testing #include "bin/model/markerlistmodel.hpp" #include "profiles/profilerepository.hpp" #include "project/notesplugin.h" #include "timeline2/model/builders/meltBuilder.hpp" #include "timeline2/view/timelinecontroller.h" #include "timeline2/view/timelinewidget.h" #include #include #include #include #include #include "kdenlive_debug.h" #include #include #include #include #include #include #include #include #include ProjectManager::ProjectManager(QObject *parent) : QObject(parent) , m_project(nullptr) , m_progressDialog(nullptr) { m_fileRevert = KStandardAction::revert(this, SLOT(slotRevert()), pCore->window()->actionCollection()); m_fileRevert->setIcon(QIcon::fromTheme(QStringLiteral("document-revert"))); m_fileRevert->setEnabled(false); QAction *a = KStandardAction::open(this, SLOT(openFile()), pCore->window()->actionCollection()); a->setIcon(QIcon::fromTheme(QStringLiteral("document-open"))); a = KStandardAction::saveAs(this, SLOT(saveFileAs()), pCore->window()->actionCollection()); a->setIcon(QIcon::fromTheme(QStringLiteral("document-save-as"))); a = KStandardAction::openNew(this, SLOT(newFile()), pCore->window()->actionCollection()); a->setIcon(QIcon::fromTheme(QStringLiteral("document-new"))); m_recentFilesAction = KStandardAction::openRecent(this, SLOT(openFile(QUrl)), pCore->window()->actionCollection()); QAction *backupAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-undo")), i18n("Open Backup File"), this); pCore->window()->addAction(QStringLiteral("open_backup"), backupAction); connect(backupAction, SIGNAL(triggered(bool)), SLOT(slotOpenBackup())); m_notesPlugin = new NotesPlugin(this); m_autoSaveTimer.setSingleShot(true); connect(&m_autoSaveTimer, &QTimer::timeout, this, &ProjectManager::slotAutoSave); // Ensure the default data folder exist QDir dir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)); dir.mkpath(QStringLiteral(".backup")); dir.mkdir(QStringLiteral("titles")); } ProjectManager::~ProjectManager() {} void ProjectManager::slotLoadOnOpen() { if (m_startUrl.isValid()) { openFile(); } else if (KdenliveSettings::openlastproject()) { openLastFile(); } else { newFile(false); } if (!m_loadClipsOnOpen.isEmpty() && (m_project != nullptr)) { const QStringList list = m_loadClipsOnOpen.split(QLatin1Char(',')); QList urls; urls.reserve(list.count()); for (const QString &path : list) { // qCDebug(KDENLIVE_LOG) << QDir::current().absoluteFilePath(path); urls << QUrl::fromLocalFile(QDir::current().absoluteFilePath(path)); } pCore->bin()->droppedUrls(urls); } m_loadClipsOnOpen.clear(); } void ProjectManager::init(const QUrl &projectUrl, const QString &clipList) { m_startUrl = projectUrl; m_loadClipsOnOpen = clipList; } void ProjectManager::newFile(bool showProjectSettings) { QString profileName = KdenliveSettings::default_profile(); if (profileName.isEmpty()) { profileName = pCore->getCurrentProfile()->path(); } newFile(profileName, showProjectSettings); } void ProjectManager::newFile(QString profileName, bool showProjectSettings) { // fix mantis#3160 QUrl startFile = QUrl::fromLocalFile(KdenliveSettings::defaultprojectfolder() + QStringLiteral("/_untitled.kdenlive")); if (checkForBackupFile(startFile, true)) { return; } m_fileRevert->setEnabled(false); QString projectFolder; QMap documentProperties; QMap documentMetadata; QPoint projectTracks(KdenliveSettings::videotracks(), KdenliveSettings::audiotracks()); pCore->monitorManager()->resetDisplay(); QString documentId = QString::number(QDateTime::currentMSecsSinceEpoch()); documentProperties.insert(QStringLiteral("documentid"), documentId); if (!showProjectSettings) { if (!closeCurrentDocument()) { return; } if (KdenliveSettings::customprojectfolder()) { projectFolder = KdenliveSettings::defaultprojectfolder(); if (!projectFolder.endsWith(QLatin1Char('/'))) { projectFolder.append(QLatin1Char('/')); } documentProperties.insert(QStringLiteral("storagefolder"), projectFolder + documentId); } } else { QPointer w = new ProjectSettings(nullptr, QMap(), QStringList(), projectTracks.x(), projectTracks.y(), KdenliveSettings::defaultprojectfolder(), false, true, pCore->window()); connect(w.data(), &ProjectSettings::refreshProfiles, pCore->window(), &MainWindow::slotRefreshProfiles); if (w->exec() != QDialog::Accepted) { delete w; return; } if (!closeCurrentDocument()) { delete w; return; } if (KdenliveSettings::videothumbnails() != w->enableVideoThumbs()) { pCore->window()->slotSwitchVideoThumbs(); } if (KdenliveSettings::audiothumbnails() != w->enableAudioThumbs()) { pCore->window()->slotSwitchAudioThumbs(); } profileName = w->selectedProfile(); projectFolder = w->storageFolder(); projectTracks = w->tracks(); documentProperties.insert(QStringLiteral("enableproxy"), QString::number((int)w->useProxy())); documentProperties.insert(QStringLiteral("generateproxy"), QString::number((int)w->generateProxy())); documentProperties.insert(QStringLiteral("proxyminsize"), QString::number(w->proxyMinSize())); documentProperties.insert(QStringLiteral("proxyparams"), w->proxyParams()); documentProperties.insert(QStringLiteral("proxyextension"), w->proxyExtension()); documentProperties.insert(QStringLiteral("generateimageproxy"), QString::number((int)w->generateImageProxy())); QString preview = w->selectedPreview(); if (!preview.isEmpty()) { documentProperties.insert(QStringLiteral("previewparameters"), preview.section(QLatin1Char(';'), 0, 0)); documentProperties.insert(QStringLiteral("previewextension"), preview.section(QLatin1Char(';'), 1, 1)); } documentProperties.insert(QStringLiteral("proxyimageminsize"), QString::number(w->proxyImageMinSize())); if (!projectFolder.isEmpty()) { if (!projectFolder.endsWith(QLatin1Char('/'))) { projectFolder.append(QLatin1Char('/')); } documentProperties.insert(QStringLiteral("storagefolder"), projectFolder + documentId); } documentMetadata = w->metadata(); delete w; } bool openBackup; m_notesPlugin->clear(); documentProperties.insert(QStringLiteral("decimalPoint"), QLocale().decimalPoint()); KdenliveDoc *doc = new KdenliveDoc(QUrl(), projectFolder, pCore->window()->m_commandStack, profileName, documentProperties, documentMetadata, projectTracks, &openBackup, pCore->window()); doc->m_autosave = new KAutoSaveFile(startFile, doc); pCore->bin()->setDocument(doc); m_project = doc; pCore->monitorManager()->activateMonitor(Kdenlive::ProjectMonitor); updateTimeline(0); pCore->window()->connectDocument(); bool disabled = m_project->getDocumentProperty(QStringLiteral("disabletimelineeffects")) == QLatin1String("1"); QAction *disableEffects = pCore->window()->actionCollection()->action(QStringLiteral("disable_timeline_effects")); if (disableEffects) { if (disabled != disableEffects->isChecked()) { disableEffects->blockSignals(true); disableEffects->setChecked(disabled); disableEffects->blockSignals(false); } } emit docOpened(m_project); m_lastSave.start(); } bool ProjectManager::closeCurrentDocument(bool saveChanges, bool quit) { if ((m_project != nullptr) && m_project->isModified() && saveChanges) { QString message; if (m_project->url().fileName().isEmpty()) { message = i18n("Save changes to document?"); } else { message = i18n("The project \"%1\" has been changed.\nDo you want to save your changes?", m_project->url().fileName()); } switch (KMessageBox::warningYesNoCancel(pCore->window(), message)) { case KMessageBox::Yes: // save document here. If saving fails, return false; if (!saveFile()) { return false; } break; case KMessageBox::Cancel: return false; break; default: break; } } pCore->window()->getMainTimeline()->controller()->clipActions.clear(); if (!quit && !qApp->isSavingSession()) { m_autoSaveTimer.stop(); if (m_project) { pCore->jobManager()->slotCancelJobs(); pCore->bin()->abortOperations(); pCore->monitorManager()->clipMonitor()->slotOpenClip(nullptr); pCore->window()->clearAssetPanel(); delete m_project; m_project = nullptr; } pCore->monitorManager()->setDocument(m_project); } /* // Make sure to reset locale to system's default QString requestedLocale = QLocale::system().name(); QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); if (env.contains(QStringLiteral("LC_NUMERIC"))) { requestedLocale = env.value(QStringLiteral("LC_NUMERIC")); } qDebug()<<"//////////// RESETTING LOCALE TO: "<decimal_point; if (QString::fromUtf8(separator) != QString(newLocale.decimalPoint())) { pCore->displayBinMessage(i18n("There is a locale conflict on your system, project might get corrupt"), KMessageWidget::Warning); } setlocale(LC_NUMERIC, requestedLocale.toUtf8().constData()); #endif QLocale::setDefault(newLocale);*/ return true; } bool ProjectManager::saveFileAs(const QString &outputFileName) { pCore->monitorManager()->pauseActiveMonitor(); // Sync document properties prepareSave(); QString saveFolder = QFileInfo(outputFileName).absolutePath(); QString scene = projectSceneList(saveFolder); if (!m_replacementPattern.isEmpty()) { QMapIterator i(m_replacementPattern); while (i.hasNext()) { i.next(); scene.replace(i.key(), i.value()); } } if (!m_project->saveSceneList(outputFileName, scene)) { return false; } QUrl url = QUrl::fromLocalFile(outputFileName); // Save timeline thumbnails QStringList thumbKeys = pCore->window()->getMainTimeline()->controller()->getThumbKeys(); ThumbnailCache::get()->saveCachedThumbs(thumbKeys); m_project->setUrl(url); // setting up autosave file in ~/.kde/data/stalefiles/kdenlive/ // saved under file name // actual saving by KdenliveDoc::slotAutoSave() called by a timer 3 seconds after the document has been edited // This timer is set by KdenliveDoc::setModified() const QString projectId = QCryptographicHash::hash(url.fileName().toUtf8(), QCryptographicHash::Md5).toHex(); QUrl autosaveUrl = QUrl::fromLocalFile(QFileInfo(outputFileName).absoluteDir().absoluteFilePath(projectId + QStringLiteral(".kdenlive"))); if (m_project->m_autosave == nullptr) { // The temporary file is not opened or created until actually needed. // The file filename does not have to exist for KAutoSaveFile to be constructed (if it exists, it will not be touched). m_project->m_autosave = new KAutoSaveFile(autosaveUrl, m_project); } else { m_project->m_autosave->setManagedFile(autosaveUrl); } pCore->window()->setWindowTitle(m_project->description()); m_project->setModified(false); m_recentFilesAction->addUrl(url); // remember folder for next project opening KRecentDirs::add(QStringLiteral(":KdenliveProjectsFolder"), saveFolder); saveRecentFiles(); m_fileRevert->setEnabled(true); pCore->window()->m_undoView->stack()->setClean(); return true; } void ProjectManager::saveRecentFiles() { KSharedConfigPtr config = KSharedConfig::openConfig(); m_recentFilesAction->saveEntries(KConfigGroup(config, "Recent Files")); config->sync(); } bool ProjectManager::saveFileAs() { QFileDialog fd(pCore->window()); fd.setDirectory(m_project->url().isValid() ? m_project->url().adjusted(QUrl::RemoveFilename).toLocalFile() : KdenliveSettings::defaultprojectfolder()); fd.setMimeTypeFilters(QStringList() << QStringLiteral("application/x-kdenlive")); fd.setAcceptMode(QFileDialog::AcceptSave); fd.setFileMode(QFileDialog::AnyFile); fd.setDefaultSuffix(QStringLiteral("kdenlive")); if (fd.exec() != QDialog::Accepted || fd.selectedFiles().isEmpty()) { return false; } QString outputFile = fd.selectedFiles().constFirst(); #if KXMLGUI_VERSION_MINOR < 23 && KXMLGUI_VERSION_MAJOR == 5 // Since Plasma 5.7 (release at same time as KF 5.23, // the file dialog manages the overwrite check if (QFile::exists(outputFile)) { // Show the file dialog again if the user does not want to overwrite the file if (KMessageBox::questionYesNo(pCore->window(), i18n("File %1 already exists.\nDo you want to overwrite it?", outputFile)) == KMessageBox::No) { return saveFileAs(); } } #endif bool ok = false; QDir cacheDir = m_project->getCacheDir(CacheBase, &ok); if (ok) { QFile file(cacheDir.absoluteFilePath(QString::fromLatin1(QUrl::toPercentEncoding(QStringLiteral(".") + outputFile)))); file.open(QIODevice::ReadWrite | QIODevice::Text); file.close(); } return saveFileAs(outputFile); } bool ProjectManager::saveFile() { if (!m_project) { // Calling saveFile before a project was created, something is wrong qCDebug(KDENLIVE_LOG) << "SaveFile called without project"; return false; } if (m_project->url().isEmpty()) { return saveFileAs(); } bool result = saveFileAs(m_project->url().toLocalFile()); m_project->m_autosave->resize(0); return result; } void ProjectManager::openFile() { if (m_startUrl.isValid()) { openFile(m_startUrl); m_startUrl.clear(); return; } QUrl url = QFileDialog::getOpenFileUrl(pCore->window(), QString(), QUrl::fromLocalFile(KRecentDirs::dir(QStringLiteral(":KdenliveProjectsFolder"))), getMimeType()); if (!url.isValid()) { return; } KRecentDirs::add(QStringLiteral(":KdenliveProjectsFolder"), url.adjusted(QUrl::RemoveFilename).toLocalFile()); m_recentFilesAction->addUrl(url); saveRecentFiles(); openFile(url); } void ProjectManager::openLastFile() { if (m_recentFilesAction->selectableActionGroup()->actions().isEmpty()) { // No files in history newFile(false); return; } QAction *firstUrlAction = m_recentFilesAction->selectableActionGroup()->actions().last(); if (firstUrlAction) { firstUrlAction->trigger(); } else { newFile(false); } } // fix mantis#3160 separate check from openFile() so we can call it from newFile() // to find autosaved files (in ~/.local/share/stalefiles/kdenlive) and recover it bool ProjectManager::checkForBackupFile(const QUrl &url, bool newFile) { // Check for autosave file that belong to the url we passed in. const QString projectId = QCryptographicHash::hash(url.fileName().toUtf8(), QCryptographicHash::Md5).toHex(); QUrl autosaveUrl = newFile ? url : QUrl::fromLocalFile(QFileInfo(url.path()).absoluteDir().absoluteFilePath(projectId + QStringLiteral(".kdenlive"))); QList staleFiles = KAutoSaveFile::staleFiles(autosaveUrl); KAutoSaveFile *orphanedFile = nullptr; // Check if we can have a lock on one of the file, // meaning it is not handled by any Kdenlive instance if (!staleFiles.isEmpty()) { for (KAutoSaveFile *stale : staleFiles) { if (stale->open(QIODevice::QIODevice::ReadWrite)) { // Found orphaned autosave file orphanedFile = stale; break; } else { // Another Kdenlive instance is probably handling this autosave file staleFiles.removeAll(stale); delete stale; continue; } } } if (orphanedFile) { if (KMessageBox::questionYesNo(nullptr, i18n("Auto-saved files exist. Do you want to recover them now?"), i18n("File Recovery"), KGuiItem(i18n("Recover")), KGuiItem(i18n("Don't recover"))) == KMessageBox::Yes) { doOpenFile(url, orphanedFile); return true; } // remove the stale files for (KAutoSaveFile *stale : staleFiles) { stale->open(QIODevice::ReadWrite); delete stale; } return false; } return false; } void ProjectManager::openFile(const QUrl &url) { QMimeDatabase db; // Make sure the url is a Kdenlive project file QMimeType mime = db.mimeTypeForUrl(url); if (mime.inherits(QStringLiteral("application/x-compressed-tar"))) { // Opening a compressed project file, we need to process it // qCDebug(KDENLIVE_LOG)<<"Opening archive, processing"; QPointer ar = new ArchiveWidget(url); if (ar->exec() == QDialog::Accepted) { openFile(QUrl::fromLocalFile(ar->extractedProjectFile())); } else if (m_startUrl.isValid()) { // we tried to open an invalid file from command line, init new project newFile(false); } delete ar; return; } /*if (!url.fileName().endsWith(".kdenlive")) { // This is not a Kdenlive project file, abort loading KMessageBox::sorry(pCore->window(), i18n("File %1 is not a Kdenlive project file", url.toLocalFile())); if (m_startUrl.isValid()) { // we tried to open an invalid file from command line, init new project newFile(false); } return; }*/ if ((m_project != nullptr) && m_project->url() == url) { return; } if (!closeCurrentDocument()) { return; } if (checkForBackupFile(url)) { return; } pCore->window()->slotGotProgressInfo(i18n("Opening file %1", url.toLocalFile()), 100, InformationMessage); doOpenFile(url, nullptr); } void ProjectManager::doOpenFile(const QUrl &url, KAutoSaveFile *stale) { Q_ASSERT(m_project == nullptr); m_fileRevert->setEnabled(true); delete m_progressDialog; pCore->monitorManager()->resetDisplay(); pCore->monitorManager()->activateMonitor(Kdenlive::ProjectMonitor); m_progressDialog = new QProgressDialog(pCore->window()); m_progressDialog->setWindowTitle(i18n("Loading project")); m_progressDialog->setCancelButton(nullptr); m_progressDialog->setLabelText(i18n("Loading project")); m_progressDialog->setMaximum(0); m_progressDialog->show(); bool openBackup; m_notesPlugin->clear(); KdenliveDoc *doc = new KdenliveDoc(stale ? QUrl::fromLocalFile(stale->fileName()) : url, QString(), pCore->window()->m_commandStack, KdenliveSettings::default_profile().isEmpty() ? pCore->getCurrentProfile()->path() : KdenliveSettings::default_profile(), QMap(), QMap(), QPoint(KdenliveSettings::videotracks(), KdenliveSettings::audiotracks()), &openBackup, pCore->window()); if (stale == nullptr) { const QString projectId = QCryptographicHash::hash(url.fileName().toUtf8(), QCryptographicHash::Md5).toHex(); QUrl autosaveUrl = QUrl::fromLocalFile(QFileInfo(url.path()).absoluteDir().absoluteFilePath(projectId + QStringLiteral(".kdenlive"))); stale = new KAutoSaveFile(autosaveUrl, doc); doc->m_autosave = stale; } else { doc->m_autosave = stale; stale->setParent(doc); // if loading from an autosave of unnamed file then keep unnamed if (url.fileName().contains(QStringLiteral("_untitled.kdenlive"))) { doc->setUrl(QUrl()); } else { doc->setUrl(url); } doc->setModified(true); stale->setParent(doc); } m_progressDialog->setLabelText(i18n("Loading clips")); // TODO refac delete this pCore->bin()->setDocument(doc); QList rulerActions; rulerActions << pCore->window()->actionCollection()->action(QStringLiteral("set_render_timeline_zone")); rulerActions << pCore->window()->actionCollection()->action(QStringLiteral("unset_render_timeline_zone")); rulerActions << pCore->window()->actionCollection()->action(QStringLiteral("clear_render_timeline_zone")); // Set default target tracks to upper audio / lower video tracks m_project = doc; updateTimeline(m_project->getDocumentProperty(QStringLiteral("position")).toInt()); pCore->window()->connectDocument(); QDateTime documentDate = QFileInfo(m_project->url().toLocalFile()).lastModified(); pCore->window()->getMainTimeline()->controller()->loadPreview(m_project->getDocumentProperty(QStringLiteral("previewchunks")), m_project->getDocumentProperty(QStringLiteral("dirtypreviewchunks")), documentDate, m_project->getDocumentProperty(QStringLiteral("disablepreview")).toInt()); emit docOpened(m_project); pCore->window()->slotGotProgressInfo(QString(), 100); if (openBackup) { slotOpenBackup(url); } m_lastSave.start(); delete m_progressDialog; m_progressDialog = nullptr; } void ProjectManager::slotRevert() { if (m_project->isModified() && KMessageBox::warningContinueCancel(pCore->window(), i18n("This will delete all changes made since you last saved your project. Are you sure you want to continue?"), i18n("Revert to last saved version")) == KMessageBox::Cancel) { return; } QUrl url = m_project->url(); if (closeCurrentDocument(false)) { doOpenFile(url, nullptr); } } QString ProjectManager::getMimeType(bool open) { QString mimetype = i18n("Kdenlive project (*.kdenlive)"); if (open) { mimetype.append(QStringLiteral(";;") + i18n("Archived project (*.tar.gz)")); } return mimetype; } KdenliveDoc *ProjectManager::current() { return m_project; } void ProjectManager::slotOpenBackup(const QUrl &url) { QUrl projectFile; QUrl projectFolder; QString projectId; if (url.isValid()) { // we could not open the project file, guess where the backups are projectFolder = QUrl::fromLocalFile(KdenliveSettings::defaultprojectfolder()); projectFile = url; } else { projectFolder = QUrl::fromLocalFile(m_project->projectTempFolder()); projectFile = m_project->url(); projectId = m_project->getDocumentProperty(QStringLiteral("documentid")); } QPointer dia = new BackupWidget(projectFile, projectFolder, projectId, pCore->window()); if (dia->exec() == QDialog::Accepted) { QString requestedBackup = dia->selectedFile(); m_project->backupLastSavedVersion(projectFile.toLocalFile()); closeCurrentDocument(false); doOpenFile(QUrl::fromLocalFile(requestedBackup), nullptr); if (m_project) { m_project->setUrl(projectFile); m_project->setModified(true); pCore->window()->setWindowTitle(m_project->description()); } } delete dia; } KRecentFilesAction *ProjectManager::recentFilesAction() { return m_recentFilesAction; } void ProjectManager::slotStartAutoSave() { if (m_lastSave.elapsed() > 300000) { // If the project was not saved in the last 5 minute, force save m_autoSaveTimer.stop(); slotAutoSave(); } else { m_autoSaveTimer.start(3000); // will trigger slotAutoSave() in 3 seconds } } void ProjectManager::slotAutoSave() { prepareSave(); QString saveFolder = m_project->url().adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).toLocalFile(); QString scene = projectSceneList(saveFolder); if (!m_replacementPattern.isEmpty()) { QMapIterator i(m_replacementPattern); while (i.hasNext()) { i.next(); scene.replace(i.key(), i.value()); } } m_project->slotAutoSave(scene); m_lastSave.start(); } QString ProjectManager::projectSceneList(const QString &outputFolder) { // TODO: re-implement overlay and all // TODO refac: repair this return pCore->monitorManager()->projectMonitor()->sceneList(outputFolder); /*bool multitrackEnabled = m_trackView->multitrackView; if (multitrackEnabled) { // Multitrack view was enabled, disable for auto save m_trackView->slotMultitrackView(false); } m_trackView->connectOverlayTrack(false); QString scene = pCore->monitorManager()->projectMonitor()->sceneList(outputFolder); m_trackView->connectOverlayTrack(true); if (multitrackEnabled) { // Multitrack view was enabled, re-enable for auto save m_trackView->slotMultitrackView(true); } return scene; */ } void ProjectManager::setDocumentNotes(const QString ¬es) { m_notesPlugin->widget()->setHtml(notes); } QString ProjectManager::documentNotes() const { QString text = m_notesPlugin->widget()->toPlainText().simplified(); if (text.isEmpty()) { return QString(); } return m_notesPlugin->widget()->toHtml(); } void ProjectManager::slotAddProjectNote() { m_notesPlugin->widget()->raise(); m_notesPlugin->widget()->setFocus(); m_notesPlugin->widget()->addProjectNote(); } void ProjectManager::prepareSave() { pCore->projectItemModel()->saveDocumentProperties(pCore->window()->getMainTimeline()->controller()->documentProperties(), m_project->metadata(), m_project->getGuideModel()); pCore->projectItemModel()->saveProperty(QStringLiteral("kdenlive:documentnotes"), documentNotes()); pCore->projectItemModel()->saveProperty(QStringLiteral("kdenlive:docproperties.groups"), m_mainTimelineModel->groupsData()); } void ProjectManager::slotResetProfiles() { m_project->resetProfile(); pCore->monitorManager()->resetProfiles(m_project->timecode()); pCore->monitorManager()->updateScopeSource(); } void ProjectManager::slotResetConsumers(bool fullReset) { pCore->monitorManager()->resetConsumers(fullReset); } void ProjectManager::slotExpandClip() { // TODO refac // m_trackView->projectView()->expandActiveClip(); } void ProjectManager::disableBinEffects(bool disable) { if (m_project) { if (disable) { m_project->setDocumentProperty(QStringLiteral("disablebineffects"), QString::number((int)true)); } else { m_project->setDocumentProperty(QStringLiteral("disablebineffects"), QString()); } } pCore->monitorManager()->refreshProjectMonitor(); pCore->monitorManager()->refreshClipMonitor(); } void ProjectManager::slotDisableTimelineEffects(bool disable) { if (disable) { m_project->setDocumentProperty(QStringLiteral("disabletimelineeffects"), QString::number((int)true)); } else { m_project->setDocumentProperty(QStringLiteral("disabletimelineeffects"), QString()); } m_mainTimelineModel->setTimelineEffectsEnabled(!disable); pCore->monitorManager()->refreshProjectMonitor(); } void ProjectManager::slotSwitchTrackLock() { pCore->window()->getMainTimeline()->controller()->switchTrackLock(); } void ProjectManager::slotSwitchAllTrackLock() { pCore->window()->getMainTimeline()->controller()->switchTrackLock(true); } void ProjectManager::slotSwitchTrackTarget() { pCore->window()->getMainTimeline()->controller()->switchTargetTrack(); } QString ProjectManager::getDefaultProjectFormat() { // On first run, lets use an HD1080p profile with fps related to timezone country. Then, when the first video is added to a project, if it does not match // our profile, propose a new default. QTimeZone zone; zone = QTimeZone::systemTimeZone(); QList ntscCountries; ntscCountries << QLocale::Canada << QLocale::Chile << QLocale::CostaRica << QLocale::Cuba << QLocale::DominicanRepublic << QLocale::Ecuador; ntscCountries << QLocale::Japan << QLocale::Mexico << QLocale::Nicaragua << QLocale::Panama << QLocale::Peru << QLocale::Philippines; ntscCountries << QLocale::PuertoRico << QLocale::SouthKorea << QLocale::Taiwan << QLocale::UnitedStates; bool ntscProject = ntscCountries.contains(zone.country()); if (!ntscProject) { return QStringLiteral("atsc_1080p_25"); } return QStringLiteral("atsc_1080p_2997"); } void ProjectManager::saveZone(const QStringList &info, const QDir &dir) { pCore->bin()->saveZone(info, dir); } void ProjectManager::moveProjectData(const QString &src, const QString &dest) { // Move tmp folder (thumbnails, timeline preview) KIO::CopyJob *copyJob = KIO::move(QUrl::fromLocalFile(src), QUrl::fromLocalFile(dest)); connect(copyJob, &KJob::result, this, &ProjectManager::slotMoveFinished); connect(copyJob, SIGNAL(percent(KJob *, ulong)), this, SLOT(slotMoveProgress(KJob *, ulong))); m_project->moveProjectData(src, dest); } void ProjectManager::slotMoveProgress(KJob *, unsigned long progress) { pCore->window()->slotGotProgressInfo(i18n("Moving project folder"), static_cast(progress), ProcessingJobMessage); } void ProjectManager::slotMoveFinished(KJob *job) { if (job->error() == 0) { pCore->window()->slotGotProgressInfo(QString(), 100, InformationMessage); KIO::CopyJob *copyJob = static_cast(job); QString newFolder = copyJob->destUrl().toLocalFile(); // Check if project folder is inside document folder, in which case, paths will be relative QDir projectDir(m_project->url().toString(QUrl::RemoveFilename | QUrl::RemoveScheme)); QDir srcDir(m_project->projectTempFolder()); if (srcDir.absolutePath().startsWith(projectDir.absolutePath())) { m_replacementPattern.insert(QStringLiteral(">proxy/"), QStringLiteral(">") + newFolder + QStringLiteral("/proxy/")); } else { m_replacementPattern.insert(m_project->projectTempFolder() + QStringLiteral("/proxy/"), newFolder + QStringLiteral("/proxy/")); } m_project->setProjectFolder(QUrl::fromLocalFile(newFolder)); saveFile(); m_replacementPattern.clear(); slotRevert(); } else { KMessageBox::sorry(pCore->window(), i18n("Error moving project folder: %1", job->errorText())); } } void ProjectManager::updateTimeline(int pos, int scrollPos) { Q_UNUSED(scrollPos); pCore->jobManager()->slotCancelJobs(); /*qDebug() << "Loading xml"<getProjectXml().constData(); QFile file("/tmp/data.xml"); if (file.open(QIODevice::ReadWrite)) { QTextStream stream(&file); stream << m_project->getProjectXml() << endl; }*/ pCore->window()->getMainTimeline()->loading = true; pCore->window()->slotSwitchTimelineZone(m_project->getDocumentProperty(QStringLiteral("enableTimelineZone")).toInt() == 1); QScopedPointer xmlProd(new Mlt::Producer(pCore->getCurrentProfile()->profile(), "xml-string", m_project->getProjectXml().constData())); Mlt::Service s(*xmlProd); Mlt::Tractor tractor(s); m_mainTimelineModel = TimelineItemModel::construct(&pCore->getCurrentProfile()->profile(), m_project->getGuideModel(), m_project->commandStack()); constructTimelineFromMelt(m_mainTimelineModel, tractor); const QString groupsData = m_project->getDocumentProperty(QStringLiteral("groups")); if (!groupsData.isEmpty()) { m_mainTimelineModel->loadGroups(groupsData); } pCore->monitorManager()->projectMonitor()->setProducer(m_mainTimelineModel->producer(), pos); pCore->window()->getMainTimeline()->setModel(m_mainTimelineModel); pCore->monitorManager()->projectMonitor()->adjustRulerSize(m_mainTimelineModel->duration() - 1, m_project->getGuideModel()); pCore->window()->getMainTimeline()->controller()->setZone(m_project->zone()); pCore->window()->getMainTimeline()->controller()->setTargetTracks(m_project->targetTracks()); pCore->window()->getMainTimeline()->controller()->setScrollPos(m_project->getDocumentProperty(QStringLiteral("scrollPos")).toInt()); int activeTrackPosition = m_project->getDocumentProperty(QStringLiteral("activeTrack")).toInt(); if (activeTrackPosition > -1) { pCore->window()->getMainTimeline()->controller()->setActiveTrack(m_mainTimelineModel->getTrackIndexFromPosition(activeTrackPosition)); } m_mainTimelineModel->setUndoStack(m_project->commandStack()); } void ProjectManager::adjustProjectDuration() { pCore->monitorManager()->projectMonitor()->adjustRulerSize(m_mainTimelineModel->duration() - 1, nullptr); } -void ProjectManager::activateAsset(const QVariantMap effectData) +void ProjectManager::activateAsset(const QVariantMap &effectData) { if (effectData.contains(QStringLiteral("kdenlive/effect"))) { pCore->window()->addEffect(effectData.value(QStringLiteral("kdenlive/effect")).toString()); } else { pCore->window()->getMainTimeline()->controller()->addAsset(effectData); } } std::shared_ptr ProjectManager::getGuideModel() { return current()->getGuideModel(); } std::shared_ptr ProjectManager::undoStack() { return current()->commandStack(); } -void ProjectManager::saveWithUpdatedProfile(const QString updatedProfile) +void ProjectManager::saveWithUpdatedProfile(const QString &updatedProfile) { // First backup current project with fps appended QString message; if (m_project && m_project->isModified()) { switch ( KMessageBox::warningYesNoCancel(pCore->window(), i18n("The project \"%1\" has been changed.\nDo you want to save your changes?", m_project->url().fileName().isEmpty() ? i18n("Untitled") : m_project->url().fileName()))) { case KMessageBox::Yes: // save document here. If saving fails, return false; if (!saveFile()) { pCore->displayBinMessage(i18n("Project profile change aborted"), KMessageWidget::Information); return; } break; default: pCore->displayBinMessage(i18n("Project profile change aborted"), KMessageWidget::Information); return; break; } } if (!m_project || m_project->isModified()) { pCore->displayBinMessage(i18n("Project profile change aborted"), KMessageWidget::Information); return; } const QString currentFile = m_project->url().toLocalFile(); // Now update to new profile auto &newProfile = ProfileRepository::get()->getProfile(updatedProfile); QString convertedFile = currentFile.section(QLatin1Char('.'), 0, -2); convertedFile.append(QString("-%1.kdenlive").arg((int)(newProfile->fps() * 100))); QFile f(currentFile); QDomDocument doc; doc.setContent(&f, false); f.close(); QDomElement mltProfile = doc.documentElement().firstChildElement(QStringLiteral("profile")); if (!mltProfile.isNull()) { mltProfile.setAttribute(QStringLiteral("frame_rate_num"), newProfile->frame_rate_num()); mltProfile.setAttribute(QStringLiteral("frame_rate_den"), newProfile->frame_rate_den()); mltProfile.setAttribute(QStringLiteral("display_aspect_num"), newProfile->display_aspect_num()); mltProfile.setAttribute(QStringLiteral("display_aspect_den"), newProfile->display_aspect_den()); mltProfile.setAttribute(QStringLiteral("sample_aspect_num"), newProfile->sample_aspect_num()); mltProfile.setAttribute(QStringLiteral("sample_aspect_den"), newProfile->sample_aspect_den()); mltProfile.setAttribute(QStringLiteral("colorspace"), newProfile->colorspace()); mltProfile.setAttribute(QStringLiteral("progressive"), newProfile->progressive()); mltProfile.setAttribute(QStringLiteral("description"), newProfile->description()); mltProfile.setAttribute(QStringLiteral("width"), newProfile->width()); mltProfile.setAttribute(QStringLiteral("height"), newProfile->height()); } QDomNodeList playlists = doc.documentElement().elementsByTagName(QStringLiteral("playlist")); for (int i = 0; i < playlists.count(); ++i) { QDomElement e = playlists.at(i).toElement(); if (e.attribute(QStringLiteral("id")) == QLatin1String("main_bin")) { Xml::setXmlProperty(e, QStringLiteral("kdenlive:docproperties.profile"), updatedProfile); break; } } QFile file(convertedFile); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { return; } QTextStream out(&file); out << doc.toString(); if (file.error() != QFile::NoError) { KMessageBox::error(qApp->activeWindow(), i18n("Cannot write to file %1", convertedFile)); file.close(); return; } file.close(); openFile(QUrl::fromLocalFile(convertedFile)); pCore->displayBinMessage(i18n("Project profile changed"), KMessageWidget::Information); } diff --git a/src/project/projectmanager.h b/src/project/projectmanager.h index 512adceff..d535a2ec5 100644 --- a/src/project/projectmanager.h +++ b/src/project/projectmanager.h @@ -1,192 +1,192 @@ /* Copyright (C) 2014 Till Theato 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 3 of the License, or (at your option) any later version. */ #ifndef PROJECTMANAGER_H #define PROJECTMANAGER_H #include "kdenlivecore_export.h" #include #include #include #include #include #include #include "timeline2/model/timelineitemmodel.hpp" #include #include #include class KAutoSaveFile; class KJob; class KdenliveDoc; class MarkerListModel; class NotesPlugin; class Project; class QAction; class QProgressDialog; class QUrl; class DocUndoStack; /** * @class ProjectManager * @brief Takes care of interaction with projects. */ class /*KDENLIVECORE_EXPORT*/ ProjectManager : public QObject { Q_OBJECT public: /** @brief Sets up actions to interact for project interaction (undo, redo, open, save, ...) and creates an empty project. */ explicit ProjectManager(QObject *parent = nullptr); virtual ~ProjectManager(); /** @brief Returns a pointer to the currently opened project. A project should always be open. */ KdenliveDoc *current(); /** @brief Store command line args for later opening. */ void init(const QUrl &projectUrl, const QString &clipList); void doOpenFile(const QUrl &url, KAutoSaveFile *stale); KRecentFilesAction *recentFilesAction(); void prepareSave(); /** @brief Disable all bin effects in current project */ void disableBinEffects(bool disable); /** @brief Returns current project's xml scene */ QString projectSceneList(const QString &outputFolder); /** @brief returns a default hd profile depending on timezone*/ static QString getDefaultProjectFormat(); void saveZone(const QStringList &info, const QDir &dir); /** @brief Move project data files to new url */ void moveProjectData(const QString &src, const QString &dest); /** @brief Retrieve current project's notes */ QString documentNotes() const; /** @brief Retrieve the current Guide Model The method is virtual to allow mocking */ virtual std::shared_ptr getGuideModel(); /** @brief Return the current undo stack The method is virtual to allow mocking */ virtual std::shared_ptr undoStack(); /** @brief This will create a backup file with fps appended to project name, * and save the project with an updated profile info, then reopen it. */ - void saveWithUpdatedProfile(const QString updatedProfile); + void saveWithUpdatedProfile(const QString &updatedProfile); public slots: void newFile(QString profileName, bool showProjectSettings = true); void newFile(bool showProjectSettings = true); /** @brief Shows file open dialog. */ void openFile(); void openLastFile(); /** @brief Load files / clips passed on the command line. */ void slotLoadOnOpen(); /** @brief Checks whether a URL is available to save to. * @return Whether the file was saved. */ bool saveFile(); /** @brief Shows a save file dialog for saving the project. * @return Whether the file was saved. */ bool saveFileAs(); /** @brief Set properties to match outputFileName and save the document. * Creates an autosave version of the output file too, at * ~/.kde/data/stalefiles/kdenlive/ \n * that will be actually written in KdenliveDoc::slotAutoSave() * @param outputFileName The URL to save to / The document's URL. * @return Whether we had success. */ bool saveFileAs(const QString &outputFileName); /** @brief Close currently opened document. Returns false if something went wrong (cannot save modifications, ...). */ bool closeCurrentDocument(bool saveChanges = true, bool quit = false); /** @brief Prepares opening @param url. * * Checks if already open and whether backup exists */ void openFile(const QUrl &url); /** @brief Start autosave timer */ void slotStartAutoSave(); /** @brief Update project and monitors profiles */ void slotResetProfiles(); /** @brief Rebuild consumers after a property change */ void slotResetConsumers(bool fullReset); /** @brief Expand current timeline clip (recover clips and tracks from an MLT playlist) */ void slotExpandClip(); /** @brief Dis/enable all timeline effects */ void slotDisableTimelineEffects(bool disable); /** @brief Un/Lock current timeline track */ void slotSwitchTrackLock(); void slotSwitchAllTrackLock(); /** @brief Un/Set current track as target */ void slotSwitchTrackTarget(); /** @brief Set the text for current project's notes */ void setDocumentNotes(const QString ¬es); /** @brief Project's duration changed, adjust monitor, etc. */ void adjustProjectDuration(); /** @brief Add an asset in timeline (effect, transition). */ - void activateAsset(const QVariantMap effectData); + void activateAsset(const QVariantMap &effectData); /** @brief insert current timeline timecode in notes widget and focus widget to allow entering quick note */ void slotAddProjectNote(); private slots: void slotRevert(); /** @brief Open the project's backupdialog. */ void slotOpenBackup(const QUrl &url = QUrl()); /** @brief Start autosaving the document. */ void slotAutoSave(); /** @brief Report progress of folder move operation. */ void slotMoveProgress(KJob *, unsigned long progress); void slotMoveFinished(KJob *job); signals: void docOpened(KdenliveDoc *document); // void projectOpened(Project *project); protected: void updateTimeline(int pos = -1, int scrollPos = -1); private: /** @brief Checks that the Kdenlive MIME type is correctly installed. * @param open If set to true, this will return the MIME type allowed for file opening (adds .tar.gz format) * @return The MIME type */ QString getMimeType(bool open = true); /** @brief checks if autoback files exists, recovers from it if user says yes, returns true if files were recovered. */ bool checkForBackupFile(const QUrl &url, bool newFile = false); KdenliveDoc *m_project; std::shared_ptr m_mainTimelineModel; QTime m_lastSave; QTimer m_autoSaveTimer; QUrl m_startUrl; QString m_loadClipsOnOpen; QMap m_replacementPattern; QAction *m_fileRevert; KRecentFilesAction *m_recentFilesAction; NotesPlugin *m_notesPlugin; QProgressDialog *m_progressDialog; void saveRecentFiles(); }; #endif diff --git a/src/scopes/abstractscopewidget.cpp b/src/scopes/abstractscopewidget.cpp index d83a18490..ba5d56de7 100644 --- a/src/scopes/abstractscopewidget.cpp +++ b/src/scopes/abstractscopewidget.cpp @@ -1,555 +1,555 @@ /*************************************************************************** * Copyright (C) 2010 by Simon Andreas Eugster (simon.eu@gmail.com) * * 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) any later version. * ***************************************************************************/ #include "abstractscopewidget.h" #include "monitor/monitor.h" #include #include #include #include #include #include "klocalizedstring.h" #include #include - +#include // Uncomment for Scope debugging. //#define DEBUG_ASW #ifdef DEBUG_ASW #include "kdenlive_debug.h" #endif const int REALTIME_FPS = 30; const QColor light(250, 238, 226, 255); const QColor dark(40, 40, 39, 255); const QColor dark2(25, 25, 23, 255); const QColor AbstractScopeWidget::colHighlightLight(18, 130, 255, 255); const QColor AbstractScopeWidget::colHighlightDark(255, 64, 19, 255); const QColor AbstractScopeWidget::colDarkWhite(250, 250, 250); const QPen AbstractScopeWidget::penThick(QBrush(AbstractScopeWidget::colDarkWhite.rgb()), 2, Qt::SolidLine); const QPen AbstractScopeWidget::penThin(QBrush(AbstractScopeWidget::colDarkWhite.rgb()), 1, Qt::SolidLine); const QPen AbstractScopeWidget::penLight(QBrush(QColor(200, 200, 250, 150)), 1, Qt::SolidLine); const QPen AbstractScopeWidget::penLightDots(QBrush(QColor(200, 200, 250, 150)), 1, Qt::DotLine); const QPen AbstractScopeWidget::penLighter(QBrush(QColor(225, 225, 250, 225)), 1, Qt::SolidLine); const QPen AbstractScopeWidget::penDark(QBrush(QColor(0, 0, 20, 250)), 1, Qt::SolidLine); const QPen AbstractScopeWidget::penDarkDots(QBrush(QColor(0, 0, 20, 250)), 1, Qt::DotLine); const QPen AbstractScopeWidget::penBackground(QBrush(dark2), 1, Qt::SolidLine); const QString AbstractScopeWidget::directions[] = {QStringLiteral("North"), QStringLiteral("Northeast"), QStringLiteral("East"), QStringLiteral("Southeast")}; AbstractScopeWidget::AbstractScopeWidget(bool trackMouse, QWidget *parent) : QWidget(parent) , m_mousePos(0, 0) , m_mouseWithinWidget(false) , offset(5) , m_accelFactorHUD(1) , m_accelFactorScope(1) , m_accelFactorBackground(1) , m_semaphoreHUD(1) , m_semaphoreScope(1) , m_semaphoreBackground(1) , initialDimensionUpdateDone(false) , m_requestForcedUpdate(false) , m_rescaleMinDist(4) , m_rescaleVerticalThreshold(2.0f) , m_rescaleActive(false) , m_rescalePropertiesLocked(false) , m_rescaleFirstRescaleDone(true) , m_rescaleDirection(North) { m_scopePalette = QPalette(); m_scopePalette.setBrush(QPalette::Window, QBrush(dark2)); m_scopePalette.setBrush(QPalette::Base, QBrush(dark)); m_scopePalette.setBrush(QPalette::Button, QBrush(dark)); m_scopePalette.setBrush(QPalette::Text, QBrush(light)); m_scopePalette.setBrush(QPalette::WindowText, QBrush(light)); m_scopePalette.setBrush(QPalette::ButtonText, QBrush(light)); setPalette(m_scopePalette); setAutoFillBackground(true); m_aAutoRefresh = new QAction(i18n("Auto Refresh"), this); m_aAutoRefresh->setCheckable(true); m_aRealtime = new QAction(i18n("Realtime (with precision loss)"), this); m_aRealtime->setCheckable(true); m_menu = new QMenu(); // Disabled dark palette on menus since it breaks up with some themes: kdenlive issue #2950 // m_menu->setPalette(m_scopePalette); m_menu->addAction(m_aAutoRefresh); m_menu->addAction(m_aRealtime); setContextMenuPolicy(Qt::CustomContextMenu); connect(this, &AbstractScopeWidget::customContextMenuRequested, this, &AbstractScopeWidget::slotContextMenuRequested); connect(this, &AbstractScopeWidget::signalHUDRenderingFinished, this, &AbstractScopeWidget::slotHUDRenderingFinished); connect(this, &AbstractScopeWidget::signalScopeRenderingFinished, this, &AbstractScopeWidget::slotScopeRenderingFinished); connect(this, &AbstractScopeWidget::signalBackgroundRenderingFinished, this, &AbstractScopeWidget::slotBackgroundRenderingFinished); connect(m_aRealtime, &QAction::toggled, this, &AbstractScopeWidget::slotResetRealtimeFactor); connect(m_aAutoRefresh, &QAction::toggled, this, &AbstractScopeWidget::slotAutoRefreshToggled); // Enable mouse tracking if desired. // Causes the mouseMoved signal to be emitted when the mouse moves inside the // widget, even when no mouse button is pressed. this->setMouseTracking(trackMouse); } AbstractScopeWidget::~AbstractScopeWidget() { writeConfig(); delete m_menu; delete m_aAutoRefresh; delete m_aRealtime; } void AbstractScopeWidget::init() { m_widgetName = widgetName(); readConfig(); } void AbstractScopeWidget::readConfig() { KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup scopeConfig(config, configName()); m_aAutoRefresh->setChecked(scopeConfig.readEntry("autoRefresh", true)); m_aRealtime->setChecked(scopeConfig.readEntry("realtime", false)); scopeConfig.sync(); } void AbstractScopeWidget::writeConfig() { KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup scopeConfig(config, configName()); scopeConfig.writeEntry("autoRefresh", m_aAutoRefresh->isChecked()); scopeConfig.writeEntry("realtime", m_aRealtime->isChecked()); scopeConfig.sync(); } QString AbstractScopeWidget::configName() { return "Scope_" + m_widgetName; } void AbstractScopeWidget::prodHUDThread() { if (this->visibleRegion().isEmpty()) { #ifdef DEBUG_ASW qCDebug(KDENLIVE_LOG) << "Scope " << m_widgetName << " is not visible. Not calculating HUD."; #endif } else { if (m_semaphoreHUD.tryAcquire(1)) { Q_ASSERT(!m_threadHUD.isRunning()); m_newHUDFrames.fetchAndStoreRelaxed(0); m_newHUDUpdates.fetchAndStoreRelaxed(0); m_threadHUD = QtConcurrent::run(this, &AbstractScopeWidget::renderHUD, m_accelFactorHUD); #ifdef DEBUG_ASW qCDebug(KDENLIVE_LOG) << "HUD thread started in " << m_widgetName; #endif } #ifdef DEBUG_ASW else { qCDebug(KDENLIVE_LOG) << "HUD semaphore locked, not prodding in " << m_widgetName << ". Thread running: " << m_threadHUD.isRunning(); } #endif } } void AbstractScopeWidget::prodScopeThread() { // Only start a new thread if the scope is actually visible // and not hidden by another widget on the stack and if user want the scope to update. if (this->visibleRegion().isEmpty() || (!m_aAutoRefresh->isChecked() && !m_requestForcedUpdate)) { #ifdef DEBUG_ASW qCDebug(KDENLIVE_LOG) << "Scope " << m_widgetName << " is not visible. Not calculating scope."; #endif } else { // Try to acquire the semaphore. This must only succeed if m_threadScope is not running // anymore. Therefore the semaphore must NOT be released before m_threadScope ends. // If acquiring the semaphore fails, the thread is still running. if (m_semaphoreScope.tryAcquire(1)) { Q_ASSERT(!m_threadScope.isRunning()); m_newScopeFrames.fetchAndStoreRelaxed(0); m_newScopeUpdates.fetchAndStoreRelaxed(0); Q_ASSERT(m_accelFactorScope > 0); // See http://doc.qt.nokia.com/latest/qtconcurrentrun.html#run about // running member functions in a thread m_threadScope = QtConcurrent::run(this, &AbstractScopeWidget::renderScope, m_accelFactorScope); m_requestForcedUpdate = false; #ifdef DEBUG_ASW qCDebug(KDENLIVE_LOG) << "Scope thread started in " << m_widgetName; #endif } else { #ifdef DEBUG_ASW qCDebug(KDENLIVE_LOG) << "Scope semaphore locked, not prodding in " << m_widgetName << ". Thread running: " << m_threadScope.isRunning(); #endif } } } void AbstractScopeWidget::prodBackgroundThread() { if (this->visibleRegion().isEmpty()) { #ifdef DEBUG_ASW qCDebug(KDENLIVE_LOG) << "Scope " << m_widgetName << " is not visible. Not calculating background."; #endif } else { if (m_semaphoreBackground.tryAcquire(1)) { Q_ASSERT(!m_threadBackground.isRunning()); m_newBackgroundFrames.fetchAndStoreRelaxed(0); m_newBackgroundUpdates.fetchAndStoreRelaxed(0); m_threadBackground = QtConcurrent::run(this, &AbstractScopeWidget::renderBackground, m_accelFactorBackground); #ifdef DEBUG_ASW qCDebug(KDENLIVE_LOG) << "Background thread started in " << m_widgetName; #endif } else { #ifdef DEBUG_ASW qCDebug(KDENLIVE_LOG) << "Background semaphore locked, not prodding in " << m_widgetName << ". Thread running: " << m_threadBackground.isRunning(); #endif } } } void AbstractScopeWidget::forceUpdate(bool doUpdate) { #ifdef DEBUG_ASW qCDebug(KDENLIVE_LOG) << "Forced update called in " << widgetName() << ". Arg: " << doUpdate; #endif if (!doUpdate) { return; } m_requestForcedUpdate = true; m_newHUDUpdates.fetchAndAddRelaxed(1); m_newScopeUpdates.fetchAndAddRelaxed(1); m_newBackgroundUpdates.fetchAndAddRelaxed(1); prodHUDThread(); prodScopeThread(); prodBackgroundThread(); } void AbstractScopeWidget::forceUpdateHUD() { m_newHUDUpdates.fetchAndAddRelaxed(1); prodHUDThread(); } void AbstractScopeWidget::forceUpdateScope() { m_newScopeUpdates.fetchAndAddRelaxed(1); m_requestForcedUpdate = true; prodScopeThread(); } void AbstractScopeWidget::forceUpdateBackground() { m_newBackgroundUpdates.fetchAndAddRelaxed(1); prodBackgroundThread(); } ///// Events ///// void AbstractScopeWidget::resizeEvent(QResizeEvent *event) { // Update the dimension of the available rect for painting m_scopeRect = scopeRect(); forceUpdate(); QWidget::resizeEvent(event); } void AbstractScopeWidget::showEvent(QShowEvent *event) { QWidget::showEvent(event); m_scopeRect = scopeRect(); } void AbstractScopeWidget::paintEvent(QPaintEvent *) { QPainter davinci(this); davinci.drawImage(m_scopeRect.topLeft(), m_imgBackground); davinci.drawImage(m_scopeRect.topLeft(), m_imgScope); davinci.drawImage(m_scopeRect.topLeft(), m_imgHUD); } void AbstractScopeWidget::mousePressEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) { // Rescaling mode starts m_rescaleActive = true; m_rescalePropertiesLocked = false; m_rescaleFirstRescaleDone = false; m_rescaleStartPoint = event->pos(); m_rescaleModifiers = event->modifiers(); } } void AbstractScopeWidget::mouseReleaseEvent(QMouseEvent *event) { m_rescaleActive = false; m_rescalePropertiesLocked = false; if (!m_aAutoRefresh->isChecked()) { m_requestForcedUpdate = true; } prodHUDThread(); prodScopeThread(); prodBackgroundThread(); QWidget::mouseReleaseEvent(event); } void AbstractScopeWidget::mouseMoveEvent(QMouseEvent *event) { m_mousePos = event->pos(); m_mouseWithinWidget = true; emit signalMousePositionChanged(); QPoint movement = event->pos() - m_rescaleStartPoint; if (m_rescaleActive) { if (m_rescalePropertiesLocked) { // Direction is known, now adjust parameters // Reset the starting point to make the next moveEvent relative to the current one m_rescaleStartPoint = event->pos(); if (!m_rescaleFirstRescaleDone) { // We have just learned the desired direction; Normalize the movement to one pixel // to avoid a jump by m_rescaleMinDist if (movement.x() != 0) { movement.setX(movement.x() / abs(movement.x())); } if (movement.y() != 0) { movement.setY(movement.y() / abs(movement.y())); } m_rescaleFirstRescaleDone = true; } handleMouseDrag(movement, m_rescaleDirection, m_rescaleModifiers); } else { // Detect the movement direction here. // This algorithm relies on the aspect ratio of dy/dx (size and signum). if (movement.manhattanLength() > m_rescaleMinDist) { float diff = ((float)movement.y()) / (float)movement.x(); - if (fabs(diff) > m_rescaleVerticalThreshold || movement.x() == 0) { + if (std::fabs(diff) > m_rescaleVerticalThreshold || movement.x() == 0) { m_rescaleDirection = North; - } else if (fabs(diff) < 1 / m_rescaleVerticalThreshold) { + } else if (std::fabs(diff) < 1 / m_rescaleVerticalThreshold) { m_rescaleDirection = East; } else if (diff < 0) { m_rescaleDirection = Northeast; } else { m_rescaleDirection = Southeast; } #ifdef DEBUG_ASW qCDebug(KDENLIVE_LOG) << "Diff is " << diff << "; chose " << directions[m_rescaleDirection] << " as direction"; #endif m_rescalePropertiesLocked = true; } } } } void AbstractScopeWidget::leaveEvent(QEvent *) { m_mouseWithinWidget = false; emit signalMousePositionChanged(); } void AbstractScopeWidget::slotContextMenuRequested(const QPoint &pos) { m_menu->exec(this->mapToGlobal(pos)); } uint AbstractScopeWidget::calculateAccelFactorHUD(uint oldMseconds, uint) { - return ceil((float)oldMseconds * REALTIME_FPS / 1000); + return std::ceil((float)oldMseconds * REALTIME_FPS / 1000); } uint AbstractScopeWidget::calculateAccelFactorScope(uint oldMseconds, uint) { - return ceil((float)oldMseconds * REALTIME_FPS / 1000); + return std::ceil((float)oldMseconds * REALTIME_FPS / 1000); } uint AbstractScopeWidget::calculateAccelFactorBackground(uint oldMseconds, uint) { - return ceil((float)oldMseconds * REALTIME_FPS / 1000); + return std::ceil((float)oldMseconds * REALTIME_FPS / 1000); } ///// Slots ///// void AbstractScopeWidget::slotHUDRenderingFinished(uint mseconds, uint oldFactor) { #ifdef DEBUG_ASW qCDebug(KDENLIVE_LOG) << "HUD rendering has finished in " << mseconds << " ms, waiting for termination in " << m_widgetName; #endif m_threadHUD.waitForFinished(); m_imgHUD = m_threadHUD.result(); m_semaphoreHUD.release(1); this->update(); if (m_aRealtime->isChecked()) { int accel; accel = (int)calculateAccelFactorHUD(mseconds, oldFactor); if (m_accelFactorHUD < 1) { accel = 1; } m_accelFactorHUD = accel; } if ((m_newHUDFrames > 0 && m_aAutoRefresh->isChecked()) || m_newHUDUpdates > 0) { #ifdef DEBUG_ASW qCDebug(KDENLIVE_LOG) << "Trying to start a new HUD thread for " << m_widgetName << ". New frames/updates: " << m_newHUDFrames << '/' << m_newHUDUpdates; #endif prodHUDThread(); } } void AbstractScopeWidget::slotScopeRenderingFinished(uint mseconds, uint oldFactor) { // The signal can be received before the thread has really finished. So we // need to wait until it has really finished before starting a new thread. #ifdef DEBUG_ASW qCDebug(KDENLIVE_LOG) << "Scope rendering has finished in " << mseconds << " ms, waiting for termination in " << m_widgetName; #endif m_threadScope.waitForFinished(); m_imgScope = m_threadScope.result(); // The scope thread has finished. Now we can release the semaphore, allowing a new thread. // See prodScopeThread where the semaphore is acquired again. m_semaphoreScope.release(1); this->update(); // Calculate the acceleration factor hint to get «realtime» updates. if (m_aRealtime->isChecked()) { int accel; accel = (int)calculateAccelFactorScope(mseconds, oldFactor); if (accel < 1) { // If mseconds happens to be 0. accel = 1; } // Don't directly calculate with m_accelFactorScope as we are dealing with concurrency. // If m_accelFactorScope is set to 0 at the wrong moment, who knows what might happen // then :) Therefore use a local variable. m_accelFactorScope = accel; } if ((m_newScopeFrames > 0 && m_aAutoRefresh->isChecked()) || m_newScopeUpdates > 0) { #ifdef DEBUG_ASW qCDebug(KDENLIVE_LOG) << "Trying to start a new scope thread for " << m_widgetName << ". New frames/updates: " << m_newScopeFrames << '/' << m_newScopeUpdates; #endif prodScopeThread(); } } void AbstractScopeWidget::slotBackgroundRenderingFinished(uint mseconds, uint oldFactor) { #ifdef DEBUG_ASW qCDebug(KDENLIVE_LOG) << "Background rendering has finished in " << mseconds << " ms, waiting for termination in " << m_widgetName; #endif m_threadBackground.waitForFinished(); m_imgBackground = m_threadBackground.result(); m_semaphoreBackground.release(1); this->update(); if (m_aRealtime->isChecked()) { int accel; accel = (int)calculateAccelFactorBackground(mseconds, oldFactor); if (m_accelFactorBackground < 1) { accel = 1; } m_accelFactorBackground = accel; } if ((m_newBackgroundFrames > 0 && m_aAutoRefresh->isChecked()) || m_newBackgroundUpdates > 0) { #ifdef DEBUG_ASW qCDebug(KDENLIVE_LOG) << "Trying to start a new background thread for " << m_widgetName << ". New frames/updates: " << m_newBackgroundFrames << '/' << m_newBackgroundUpdates; #endif prodBackgroundThread(); } } void AbstractScopeWidget::slotRenderZoneUpdated() { m_newHUDFrames.fetchAndAddRelaxed(1); m_newScopeFrames.fetchAndAddRelaxed(1); m_newBackgroundFrames.fetchAndAddRelaxed(1); #ifdef DEBUG_ASW qCDebug(KDENLIVE_LOG) << "Data incoming at " << widgetName() << ". New frames total HUD/Scope/Background: " << m_newHUDFrames << '/' << m_newScopeFrames << '/' << m_newBackgroundFrames; #endif if (this->visibleRegion().isEmpty()) { #ifdef DEBUG_ASW qCDebug(KDENLIVE_LOG) << "Scope of widget " << m_widgetName << " is not at the top, not rendering."; #endif } else { if (m_aAutoRefresh->isChecked()) { prodHUDThread(); prodScopeThread(); prodBackgroundThread(); } } } void AbstractScopeWidget::slotResetRealtimeFactor(bool realtimeChecked) { if (!realtimeChecked) { m_accelFactorHUD = 1; m_accelFactorScope = 1; m_accelFactorBackground = 1; } } bool AbstractScopeWidget::autoRefreshEnabled() const { return m_aAutoRefresh->isChecked(); } void AbstractScopeWidget::slotAutoRefreshToggled(bool autoRefresh) { #ifdef DEBUG_ASW qCDebug(KDENLIVE_LOG) << "Auto-refresh switched to " << autoRefresh << " in " << widgetName() << " (Visible: " << isVisible() << '/' << this->visibleRegion().isEmpty() << ')'; #endif if (isVisible()) { // Notify listeners whether we accept new frames now emit requestAutoRefresh(autoRefresh); } // TODO only if depends on input if (autoRefresh) { // forceUpdate(); m_requestForcedUpdate = true; } } void AbstractScopeWidget::handleMouseDrag(const QPoint &, const RescaleDirection, const Qt::KeyboardModifiers) {} #ifdef DEBUG_ASW #undef DEBUG_ASW #endif diff --git a/src/scopes/colorscopes/vectorscope.cpp b/src/scopes/colorscopes/vectorscope.cpp index a8ff0baff..06fd34aa4 100644 --- a/src/scopes/colorscopes/vectorscope.cpp +++ b/src/scopes/colorscopes/vectorscope.cpp @@ -1,553 +1,553 @@ /*************************************************************************** * Copyright (C) 2010 by Simon Andreas Eugster (simon.eu@gmail.com) * * 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) any later version. * ***************************************************************************/ #include "vectorscope.h" #include "colorplaneexport.h" #include "colortools.h" #include "vectorscopegenerator.h" #include "kdenlive_debug.h" #include "klocalizedstring.h" #include #include #include #include #include - +#include const float P75 = .75; const QPointF YUV_R(-.147, .615); const QPointF YUV_G(-.289, -.515); const QPointF YUV_B(.437, -.100); const QPointF YUV_Cy(.147, -.615); const QPointF YUV_Mg(.289, .515); const QPointF YUV_Yl(-.437, .100); const QPointF YPbPr_R(-.169, .5); const QPointF YPbPr_G(-.331, -.419); const QPointF YPbPr_B(.5, -.081); const QPointF YPbPr_Cy(.169, -.5); const QPointF YPbPr_Mg(.331, .419); const QPointF YPbPr_Yl(-.5, .081); Vectorscope::Vectorscope(QWidget *parent) : AbstractGfxScopeWidget(true, parent) , m_gain(1) { ui = new Ui::Vectorscope_UI(); ui->setupUi(this); m_colorTools = new ColorTools(); m_vectorscopeGenerator = new VectorscopeGenerator(); ui->paintMode->addItem(i18n("Green 2"), QVariant(VectorscopeGenerator::PaintMode_Green2)); ui->paintMode->addItem(i18n("Green"), QVariant(VectorscopeGenerator::PaintMode_Green)); ui->paintMode->addItem(i18n("Black"), QVariant(VectorscopeGenerator::PaintMode_Black)); ui->paintMode->addItem(i18n("Modified YUV (Chroma)"), QVariant(VectorscopeGenerator::PaintMode_Chroma)); ui->paintMode->addItem(i18n("YUV"), QVariant(VectorscopeGenerator::PaintMode_YUV)); ui->paintMode->addItem(i18n("Original Color"), QVariant(VectorscopeGenerator::PaintMode_Original)); ui->backgroundMode->addItem(i18n("None"), QVariant(BG_NONE)); ui->backgroundMode->addItem(i18n("YUV"), QVariant(BG_YUV)); ui->backgroundMode->addItem(i18n("Modified YUV (Chroma)"), QVariant(BG_CHROMA)); ui->backgroundMode->addItem(i18n("YPbPr"), QVariant(BG_YPbPr)); ui->sliderGain->setMinimum(0); ui->sliderGain->setMaximum(40); connect(ui->backgroundMode, SIGNAL(currentIndexChanged(int)), this, SLOT(slotBackgroundChanged())); connect(ui->sliderGain, &QAbstractSlider::valueChanged, this, &Vectorscope::slotGainChanged); connect(ui->paintMode, SIGNAL(currentIndexChanged(int)), this, SLOT(forceUpdateScope())); connect(this, &Vectorscope::signalMousePositionChanged, this, &Vectorscope::forceUpdateHUD); ui->sliderGain->setValue(0); ///// Build context menu ///// m_menu->addSeparator()->setText(i18n("Tools")); m_aExportBackground = new QAction(i18n("Export background"), this); m_menu->addAction(m_aExportBackground); connect(m_aExportBackground, &QAction::triggered, this, &Vectorscope::slotExportBackground); m_menu->addSeparator()->setText(i18n("Drawing options")); m_a75PBox = new QAction(i18n("75% box"), this); m_a75PBox->setCheckable(true); m_menu->addAction(m_a75PBox); connect(m_a75PBox, &QAction::changed, this, &Vectorscope::forceUpdateBackground); m_aAxisEnabled = new QAction(i18n("Draw axis"), this); m_aAxisEnabled->setCheckable(true); m_menu->addAction(m_aAxisEnabled); connect(m_aAxisEnabled, &QAction::changed, this, &Vectorscope::forceUpdateBackground); m_aIQLines = new QAction(i18n("Draw I/Q lines"), this); m_aIQLines->setCheckable(true); m_menu->addAction(m_aIQLines); connect(m_aIQLines, &QAction::changed, this, &Vectorscope::forceUpdateBackground); m_menu->addSeparator()->setText(i18n("Color Space")); m_aColorSpace_YPbPr = new QAction(i18n("YPbPr"), this); m_aColorSpace_YPbPr->setCheckable(true); m_aColorSpace_YUV = new QAction(i18n("YUV"), this); m_aColorSpace_YUV->setCheckable(true); m_agColorSpace = new QActionGroup(this); m_agColorSpace->addAction(m_aColorSpace_YPbPr); m_agColorSpace->addAction(m_aColorSpace_YUV); m_menu->addAction(m_aColorSpace_YPbPr); m_menu->addAction(m_aColorSpace_YUV); connect(m_aColorSpace_YPbPr, &QAction::toggled, this, &Vectorscope::slotColorSpaceChanged); connect(m_aColorSpace_YUV, &QAction::toggled, this, &Vectorscope::slotColorSpaceChanged); // To make the 1.0x text show slotGainChanged(ui->sliderGain->value()); init(); } Vectorscope::~Vectorscope() { writeConfig(); delete m_colorTools; delete m_vectorscopeGenerator; delete m_aColorSpace_YPbPr; delete m_aColorSpace_YUV; delete m_aExportBackground; delete m_aAxisEnabled; delete m_a75PBox; delete m_agColorSpace; delete ui; } QString Vectorscope::widgetName() const { return QStringLiteral("Vectorscope"); } void Vectorscope::readConfig() { AbstractGfxScopeWidget::readConfig(); KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup scopeConfig(config, configName()); m_a75PBox->setChecked(scopeConfig.readEntry("75PBox", false)); m_aAxisEnabled->setChecked(scopeConfig.readEntry("axis", false)); m_aIQLines->setChecked(scopeConfig.readEntry("iqlines", false)); ui->backgroundMode->setCurrentIndex(scopeConfig.readEntry("backgroundmode").toInt()); ui->paintMode->setCurrentIndex(scopeConfig.readEntry("paintmode").toInt()); ui->sliderGain->setValue(scopeConfig.readEntry("gain", 1)); m_aColorSpace_YPbPr->setChecked(scopeConfig.readEntry("colorspace_ypbpr", false)); m_aColorSpace_YUV->setChecked(!m_aColorSpace_YPbPr->isChecked()); } void Vectorscope::writeConfig() { KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup scopeConfig(config, configName()); scopeConfig.writeEntry("75PBox", m_a75PBox->isChecked()); scopeConfig.writeEntry("axis", m_aAxisEnabled->isChecked()); scopeConfig.writeEntry("iqlines", m_aIQLines->isChecked()); scopeConfig.writeEntry("backgroundmode", ui->backgroundMode->currentIndex()); scopeConfig.writeEntry("paintmode", ui->paintMode->currentIndex()); scopeConfig.writeEntry("gain", ui->sliderGain->value()); scopeConfig.writeEntry("colorspace_ypbpr", m_aColorSpace_YPbPr->isChecked()); scopeConfig.sync(); } QRect Vectorscope::scopeRect() { // Distance from top/left/right int border = 6; // We want to paint below the controls area. The line is the lowest element. QPoint topleft(border, ui->verticalSpacer->geometry().y() + border); QPoint bottomright(ui->horizontalSpacer->geometry().right() - border, this->size().height() - border); m_visibleRect = QRect(topleft, bottomright); QRect scopeRect(topleft, bottomright); // Circle Width: min of width and height cw = (scopeRect.height() < scopeRect.width()) ? scopeRect.height() : scopeRect.width(); scopeRect.setWidth(cw); scopeRect.setHeight(cw); m_centerPoint = m_vectorscopeGenerator->mapToCircle(scopeRect.size(), QPointF(0, 0)); pR75 = m_vectorscopeGenerator->mapToCircle(scopeRect.size(), P75 * VectorscopeGenerator::scaling * YUV_R); pG75 = m_vectorscopeGenerator->mapToCircle(scopeRect.size(), P75 * VectorscopeGenerator::scaling * YUV_G); pB75 = m_vectorscopeGenerator->mapToCircle(scopeRect.size(), P75 * VectorscopeGenerator::scaling * YUV_B); pCy75 = m_vectorscopeGenerator->mapToCircle(scopeRect.size(), P75 * VectorscopeGenerator::scaling * YUV_Cy); pMg75 = m_vectorscopeGenerator->mapToCircle(scopeRect.size(), P75 * VectorscopeGenerator::scaling * YUV_Mg); pYl75 = m_vectorscopeGenerator->mapToCircle(scopeRect.size(), P75 * VectorscopeGenerator::scaling * YUV_Yl); qR75 = m_vectorscopeGenerator->mapToCircle(scopeRect.size(), P75 * VectorscopeGenerator::scaling * YPbPr_R); qG75 = m_vectorscopeGenerator->mapToCircle(scopeRect.size(), P75 * VectorscopeGenerator::scaling * YPbPr_G); qB75 = m_vectorscopeGenerator->mapToCircle(scopeRect.size(), P75 * VectorscopeGenerator::scaling * YPbPr_B); qCy75 = m_vectorscopeGenerator->mapToCircle(scopeRect.size(), P75 * VectorscopeGenerator::scaling * YPbPr_Cy); qMg75 = m_vectorscopeGenerator->mapToCircle(scopeRect.size(), P75 * VectorscopeGenerator::scaling * YPbPr_Mg); qYl75 = m_vectorscopeGenerator->mapToCircle(scopeRect.size(), P75 * VectorscopeGenerator::scaling * YPbPr_Yl); return scopeRect; } bool Vectorscope::isHUDDependingOnInput() const { return false; } bool Vectorscope::isScopeDependingOnInput() const { return true; } bool Vectorscope::isBackgroundDependingOnInput() const { return false; } QImage Vectorscope::renderHUD(uint) { QImage hud; QLocale locale; locale.setNumberOptions(QLocale::OmitGroupSeparator); if (m_mouseWithinWidget) { // Mouse moved: Draw a circle over the scope hud = QImage(m_visibleRect.size(), QImage::Format_ARGB32); hud.fill(qRgba(0, 0, 0, 0)); QPainter davinci(&hud); QPoint widgetCenterPoint = m_scopeRect.topLeft() + m_centerPoint; int dx = -widgetCenterPoint.x() + m_mousePos.x(); int dy = widgetCenterPoint.y() - m_mousePos.y(); QPoint reference = m_vectorscopeGenerator->mapToCircle(m_scopeRect.size(), QPointF(1, 0)); float r = sqrt(dx * dx + dy * dy); float percent = (float)100 * r / (float)VectorscopeGenerator::scaling / (float)m_gain / float(reference.x() - widgetCenterPoint.x()); switch (ui->backgroundMode->itemData(ui->backgroundMode->currentIndex()).toInt()) { case BG_NONE: davinci.setPen(penLight); break; default: if (r > cw / 2.0) { davinci.setPen(penLight); } else { davinci.setPen(penDark); } break; } davinci.drawEllipse(m_centerPoint, (int)r, (int)r); davinci.setPen(penThin); davinci.drawText(QPoint(m_scopeRect.width() - 40, m_scopeRect.height()), i18n("%1 %%", locale.toString(percent, 'f', 0))); - float angle = copysign(acos((float)dx / (float)r) * 180. / M_PI, dy); + float angle = copysign(std::acos((float)dx / (float)r) * 180. / M_PI, dy); davinci.drawText(QPoint(10, m_scopeRect.height()), i18n("%1°", locale.toString(angle, 'f', 1))); // m_circleEnabled = false; } else { hud = QImage(0, 0, QImage::Format_ARGB32); } emit signalHUDRenderingFinished(0, 1); return hud; } QImage Vectorscope::renderGfxScope(uint accelerationFactor, const QImage &qimage) { QTime start = QTime::currentTime(); QImage scope; if (cw <= 0) { qCDebug(KDENLIVE_LOG) << "Scope size not known yet. Aborting."; } else { VectorscopeGenerator::ColorSpace colorSpace = m_aColorSpace_YPbPr->isChecked() ? VectorscopeGenerator::ColorSpace_YPbPr : VectorscopeGenerator::ColorSpace_YUV; VectorscopeGenerator::PaintMode paintMode = (VectorscopeGenerator::PaintMode)ui->paintMode->itemData(ui->paintMode->currentIndex()).toInt(); scope = m_vectorscopeGenerator->calculateVectorscope(m_scopeRect.size(), qimage, m_gain, paintMode, colorSpace, m_aAxisEnabled->isChecked(), accelerationFactor); } unsigned int mseconds = (uint)start.msecsTo(QTime::currentTime()); emit signalScopeRenderingFinished(mseconds, accelerationFactor); return scope; } QImage Vectorscope::renderBackground(uint) { QTime start = QTime::currentTime(); start.start(); QImage bg(m_visibleRect.size(), QImage::Format_ARGB32); bg.fill(qRgba(0, 0, 0, 0)); // Set up tools QPainter davinci(&bg); davinci.setRenderHint(QPainter::Antialiasing, true); QPoint vinciPoint; QPoint vinciPoint2; // Draw the color plane (if selected) QImage colorPlane; switch (ui->backgroundMode->itemData(ui->backgroundMode->currentIndex()).toInt()) { case BG_YUV: colorPlane = m_colorTools->yuvColorWheel(m_scopeRect.size(), (unsigned char)128, 1 / VectorscopeGenerator::scaling, false, true); davinci.drawImage(0, 0, colorPlane); break; case BG_CHROMA: colorPlane = m_colorTools->yuvColorWheel(m_scopeRect.size(), (unsigned char)255, 1 / VectorscopeGenerator::scaling, true, true); davinci.drawImage(0, 0, colorPlane); break; case BG_YPbPr: colorPlane = m_colorTools->yPbPrColorWheel(m_scopeRect.size(), (unsigned char)128, 1 / VectorscopeGenerator::scaling, true); davinci.drawImage(0, 0, colorPlane); break; } // Draw I/Q lines (from the YIQ color space; Skin tones lie on the I line) // Positions are calculated by transforming YIQ:[0 1 0] or YIQ:[0 0 1] to YUV/YPbPr. if (m_aIQLines->isChecked()) { switch (ui->backgroundMode->itemData(ui->backgroundMode->currentIndex()).toInt()) { case BG_NONE: davinci.setPen(penLightDots); break; default: davinci.setPen(penDarkDots); break; } if (m_aColorSpace_YUV->isChecked()) { vinciPoint = m_vectorscopeGenerator->mapToCircle(m_scopeRect.size(), QPointF(-.544, .838)); vinciPoint2 = m_vectorscopeGenerator->mapToCircle(m_scopeRect.size(), QPointF(.544, -.838)); } else { vinciPoint = m_vectorscopeGenerator->mapToCircle(m_scopeRect.size(), QPointF(-.675, .737)); vinciPoint2 = m_vectorscopeGenerator->mapToCircle(m_scopeRect.size(), QPointF(.675, -.737)); } davinci.drawLine(vinciPoint, vinciPoint2); davinci.setPen(penThick); davinci.drawText(vinciPoint - QPoint(11, 5), QStringLiteral("I")); switch (ui->backgroundMode->itemData(ui->backgroundMode->currentIndex()).toInt()) { case BG_NONE: davinci.setPen(penLightDots); break; default: davinci.setPen(penDarkDots); break; } if (m_aColorSpace_YUV->isChecked()) { vinciPoint = m_vectorscopeGenerator->mapToCircle(m_scopeRect.size(), QPointF(.838, .544)); vinciPoint2 = m_vectorscopeGenerator->mapToCircle(m_scopeRect.size(), QPointF(-.838, -.544)); } else { vinciPoint = m_vectorscopeGenerator->mapToCircle(m_scopeRect.size(), QPointF(.908, .443)); vinciPoint2 = m_vectorscopeGenerator->mapToCircle(m_scopeRect.size(), QPointF(-.908, -.443)); } davinci.drawLine(vinciPoint, vinciPoint2); davinci.setPen(penThick); davinci.drawText(vinciPoint - QPoint(-7, 2), QStringLiteral("Q")); } // Draw the vectorscope circle davinci.setPen(penThick); davinci.drawEllipse(0, 0, cw, cw); // Draw RGB/CMY points with 100% chroma if (m_aColorSpace_YUV->isChecked()) { vinciPoint = m_vectorscopeGenerator->mapToCircle(m_scopeRect.size(), VectorscopeGenerator::scaling * YUV_R); davinci.drawEllipse(vinciPoint, 4, 4); davinci.drawText(vinciPoint - QPoint(20, -10), QStringLiteral("R")); vinciPoint = m_vectorscopeGenerator->mapToCircle(m_scopeRect.size(), VectorscopeGenerator::scaling * YUV_G); davinci.drawEllipse(vinciPoint, 4, 4); davinci.drawText(vinciPoint - QPoint(20, 0), QStringLiteral("G")); vinciPoint = m_vectorscopeGenerator->mapToCircle(m_scopeRect.size(), VectorscopeGenerator::scaling * YUV_B); davinci.drawEllipse(vinciPoint, 4, 4); davinci.drawText(vinciPoint + QPoint(15, 10), QStringLiteral("B")); vinciPoint = m_vectorscopeGenerator->mapToCircle(m_scopeRect.size(), VectorscopeGenerator::scaling * YUV_Cy); davinci.drawEllipse(vinciPoint, 4, 4); davinci.drawText(vinciPoint + QPoint(15, -5), QStringLiteral("Cy")); vinciPoint = m_vectorscopeGenerator->mapToCircle(m_scopeRect.size(), VectorscopeGenerator::scaling * YUV_Mg); davinci.drawEllipse(vinciPoint, 4, 4); davinci.drawText(vinciPoint + QPoint(15, 10), QStringLiteral("Mg")); vinciPoint = m_vectorscopeGenerator->mapToCircle(m_scopeRect.size(), VectorscopeGenerator::scaling * YUV_Yl); davinci.drawEllipse(vinciPoint, 4, 4); davinci.drawText(vinciPoint - QPoint(25, 0), QStringLiteral("Yl")); } else { vinciPoint = m_vectorscopeGenerator->mapToCircle(m_scopeRect.size(), VectorscopeGenerator::scaling * YPbPr_R); davinci.drawEllipse(vinciPoint, 4, 4); davinci.drawText(vinciPoint - QPoint(20, -10), QStringLiteral("R")); vinciPoint = m_vectorscopeGenerator->mapToCircle(m_scopeRect.size(), VectorscopeGenerator::scaling * YPbPr_G); davinci.drawEllipse(vinciPoint, 4, 4); davinci.drawText(vinciPoint - QPoint(20, 0), QStringLiteral("G")); vinciPoint = m_vectorscopeGenerator->mapToCircle(m_scopeRect.size(), VectorscopeGenerator::scaling * YPbPr_B); davinci.drawEllipse(vinciPoint, 4, 4); davinci.drawText(vinciPoint + QPoint(15, 10), QStringLiteral("B")); vinciPoint = m_vectorscopeGenerator->mapToCircle(m_scopeRect.size(), VectorscopeGenerator::scaling * YPbPr_Cy); davinci.drawEllipse(vinciPoint, 4, 4); davinci.drawText(vinciPoint + QPoint(15, -5), QStringLiteral("Cy")); vinciPoint = m_vectorscopeGenerator->mapToCircle(m_scopeRect.size(), VectorscopeGenerator::scaling * YPbPr_Mg); davinci.drawEllipse(vinciPoint, 4, 4); davinci.drawText(vinciPoint + QPoint(15, 10), QStringLiteral("Mg")); vinciPoint = m_vectorscopeGenerator->mapToCircle(m_scopeRect.size(), VectorscopeGenerator::scaling * YPbPr_Yl); davinci.drawEllipse(vinciPoint, 4, 4); davinci.drawText(vinciPoint - QPoint(25, 0), QStringLiteral("Yl")); } switch (ui->backgroundMode->itemData(ui->backgroundMode->currentIndex()).toInt()) { case BG_NONE: davinci.setPen(penLight); break; default: davinci.setPen(penDark); break; } // Draw axis if (m_aAxisEnabled->isChecked()) { davinci.drawLine(m_vectorscopeGenerator->mapToCircle(m_scopeRect.size(), QPointF(0, -.9)), m_vectorscopeGenerator->mapToCircle(m_scopeRect.size(), QPointF(0, .9))); davinci.drawLine(m_vectorscopeGenerator->mapToCircle(m_scopeRect.size(), QPointF(-.9, 0)), m_vectorscopeGenerator->mapToCircle(m_scopeRect.size(), QPointF(.9, 0))); } // Draw center point switch (ui->backgroundMode->itemData(ui->backgroundMode->currentIndex()).toInt()) { case BG_CHROMA: davinci.setPen(penDark); break; default: davinci.setPen(penThin); break; } davinci.drawEllipse(m_centerPoint, 5, 5); // Draw 75% box if (m_a75PBox->isChecked()) { if (m_aColorSpace_YUV->isChecked()) { davinci.drawLine(pR75, pYl75); davinci.drawLine(pYl75, pG75); davinci.drawLine(pG75, pCy75); davinci.drawLine(pCy75, pB75); davinci.drawLine(pB75, pMg75); davinci.drawLine(pMg75, pR75); } else { davinci.drawLine(qR75, qYl75); davinci.drawLine(qYl75, qG75); davinci.drawLine(qG75, qCy75); davinci.drawLine(qCy75, qB75); davinci.drawLine(qB75, qMg75); davinci.drawLine(qMg75, qR75); } } // Draw RGB/CMY points with 75% chroma (for NTSC) davinci.setPen(penThin); if (m_aColorSpace_YUV->isChecked()) { davinci.drawEllipse(pR75, 3, 3); davinci.drawEllipse(pG75, 3, 3); davinci.drawEllipse(pB75, 3, 3); davinci.drawEllipse(pCy75, 3, 3); davinci.drawEllipse(pMg75, 3, 3); davinci.drawEllipse(pYl75, 3, 3); } else { davinci.drawEllipse(qR75, 3, 3); davinci.drawEllipse(qG75, 3, 3); davinci.drawEllipse(qB75, 3, 3); davinci.drawEllipse(qCy75, 3, 3); davinci.drawEllipse(qMg75, 3, 3); davinci.drawEllipse(qYl75, 3, 3); } // Draw realtime factor (number of skipped pixels) if (m_aRealtime->isChecked()) { davinci.setPen(penThin); davinci.drawText(QPoint(m_scopeRect.width() - 40, m_scopeRect.height() - 15), QVariant(m_accelFactorScope).toString().append(QStringLiteral("x"))); } emit signalBackgroundRenderingFinished((uint)start.elapsed(), 1); return bg; } ///// Slots ///// void Vectorscope::slotGainChanged(int newval) { QLocale locale; locale.setNumberOptions(QLocale::OmitGroupSeparator); m_gain = 1 + (float)newval / 10; ui->lblGain->setText(locale.toString(m_gain, 'f', 1) + QLatin1Char('x')); forceUpdateScope(); } void Vectorscope::slotExportBackground() { QPointer colorPlaneExportDialog = new ColorPlaneExport(this); colorPlaneExportDialog->exec(); delete colorPlaneExportDialog; } void Vectorscope::slotBackgroundChanged() { // Background changed, switch to a suitable color mode now int index; switch (ui->backgroundMode->itemData(ui->backgroundMode->currentIndex()).toInt()) { case BG_YUV: index = ui->paintMode->findData(QVariant(VectorscopeGenerator::PaintMode_Black)); if (index >= 0) { ui->paintMode->setCurrentIndex(index); } break; case BG_NONE: if (ui->paintMode->itemData(ui->paintMode->currentIndex()).toInt() == VectorscopeGenerator::PaintMode_Black) { index = ui->paintMode->findData(QVariant(VectorscopeGenerator::PaintMode_Green2)); ui->paintMode->setCurrentIndex(index); } break; } forceUpdateBackground(); } void Vectorscope::slotColorSpaceChanged() { int index; if (m_aColorSpace_YPbPr->isChecked()) { if (ui->backgroundMode->itemData(ui->backgroundMode->currentIndex()).toInt() == BG_YUV) { index = ui->backgroundMode->findData(QVariant(BG_YPbPr)); if (index >= 0) { ui->backgroundMode->setCurrentIndex(index); } } } else { if (ui->backgroundMode->itemData(ui->backgroundMode->currentIndex()).toInt() == BG_YPbPr) { index = ui->backgroundMode->findData(QVariant(BG_YUV)); if (index >= 0) { ui->backgroundMode->setCurrentIndex(index); } } } forceUpdate(); } diff --git a/src/scopes/colorscopes/waveformgenerator.cpp b/src/scopes/colorscopes/waveformgenerator.cpp index fd1941bce..902105880 100644 --- a/src/scopes/colorscopes/waveformgenerator.cpp +++ b/src/scopes/colorscopes/waveformgenerator.cpp @@ -1,145 +1,145 @@ /*************************************************************************** * Copyright (C) 2010 by Simon Andreas Eugster (simon.eu@gmail.com) * * 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) any later version. * ***************************************************************************/ #include "waveformgenerator.h" #include #include #include #include #include #include #define CHOP255(a) ((255) < (a) ? (255) : (a)) WaveformGenerator::WaveformGenerator() = default; WaveformGenerator::~WaveformGenerator() = default; QImage WaveformGenerator::calculateWaveform(const QSize &waveformSize, const QImage &image, WaveformGenerator::PaintMode paintMode, bool drawAxis, WaveformGenerator::Rec rec, uint accelFactor) { Q_ASSERT(accelFactor >= 1); // QTime time; // time.start(); QImage wave(waveformSize, QImage::Format_ARGB32); if (waveformSize.width() <= 0 || waveformSize.height() <= 0 || image.width() <= 0 || image.height() <= 0) { return QImage(); } // Fill with transparent color wave.fill(qRgba(0, 0, 0, 0)); const uint ww = (uint)waveformSize.width(); const uint wh = (uint)waveformSize.height(); const uint iw = (uint)image.bytesPerLine(); const uint ih = (uint)image.height(); const uint byteCount = iw * ih; std::vector> waveValues((size_t)waveformSize.width(), std::vector((size_t)waveformSize.height(), 0)); // Number of input pixels that will fall on one scope pixel. // Must be a float because the acceleration factor can be high, leading to <1 expected px per px. const float pixelDepth = (float)((byteCount >> 2) / accelFactor) / float(ww * wh); const float gain = 255. / (8. * pixelDepth); // qCDebug(KDENLIVE_LOG) << "Pixel depth: expected " << pixelDepth << "; Gain: using " << gain << " (acceleration: " << accelFactor << "x)"; // Subtract 1 from sizes because we start counting from 0. // Not doing it would result in attempts to paint outside of the image. const float hPrediv = (float)(wh - 1) / 255.; const float wPrediv = (float)(ww - 1) / float(iw - 1); const uchar *bits = image.bits(); const int bpp = image.depth() / 8; for (uint i = 0, x = 0; i < byteCount; i += (uint)bpp) { Q_ASSERT(bits < image.bits() + byteCount); double dY, dx, dy; auto *col = (const QRgb *)bits; if (rec == WaveformGenerator::Rec_601) { // CIE 601 Luminance dY = .299 * qRed(*col) + .587 * qGreen(*col) + .114 * qBlue(*col); } else { // CIE 709 Luminance dY = .2125 * qRed(*col) + .7154 * qGreen(*col) + .0721 * qBlue(*col); } // dY is on [0,255] now. dy = dY * hPrediv; dx = (float)x * wPrediv; waveValues[(size_t)dx][(size_t)dy]++; bits += bpp; x += (uint)bpp; if (x > iw) { x -= iw; if (accelFactor > 1) { bits += bpp * (int)iw * ((int)accelFactor - 1); i += (uint)bpp * iw * (accelFactor - 1); } } } switch (paintMode) { case PaintMode_Green: for (int i = 0; i < waveformSize.width(); ++i) { for (int j = 0; j < waveformSize.height(); ++j) { // Logarithmic scale. Needs fine tuning by hand, but looks great. wave.setPixel(i, waveformSize.height() - j - 1, qRgba(CHOP255(52 * log(0.1 * gain * (float)waveValues[(size_t)i][(size_t)j])), - CHOP255(52 * log(gain * (float)waveValues[(size_t)i][(size_t)j])), + CHOP255(52 * std::log(gain * (float)waveValues[(size_t)i][(size_t)j])), CHOP255(52 * log(.25 * gain * (float)waveValues[(size_t)i][(size_t)j])), - CHOP255(64 * log(gain * (float)waveValues[(size_t)i][(size_t)j])))); + CHOP255(64 * std::log(gain * (float)waveValues[(size_t)i][(size_t)j])))); } } break; case PaintMode_Yellow: for (int i = 0; i < waveformSize.width(); ++i) { for (int j = 0; j < waveformSize.height(); ++j) { wave.setPixel(i, waveformSize.height() - j - 1, qRgba(255, 242, 0, CHOP255(gain * (float)waveValues[(size_t)i][(size_t)j]))); } } break; default: for (int i = 0; i < waveformSize.width(); ++i) { for (int j = 0; j < waveformSize.height(); ++j) { wave.setPixel(i, waveformSize.height() - j - 1, qRgba(255, 255, 255, CHOP255(2. * gain * (float)waveValues[(size_t)i][(size_t)j]))); } } break; } if (drawAxis) { QPainter davinci(&wave); QRgb opx; davinci.setPen(qRgba(150, 255, 200, 32)); davinci.setCompositionMode(QPainter::CompositionMode_Overlay); for (int i = 0; i <= 10; ++i) { float dy = (float)i / 10. * ((int)wh - 1); for (int x = 0; x < (int)ww; ++x) { opx = wave.pixel(x, dy); wave.setPixel(x, dy, qRgba(CHOP255(150 + qRed(opx)), 255, CHOP255(200 + qBlue(opx)), CHOP255(32 + qAlpha(opx)))); } } } // uint diff = time.elapsed(); // emit signalCalculationFinished(wave, diff); return wave; } #undef CHOP255 diff --git a/src/timeline2/model/builders/meltBuilder.cpp b/src/timeline2/model/builders/meltBuilder.cpp index 05b717598..7bbafa6a0 100644 --- a/src/timeline2/model/builders/meltBuilder.cpp +++ b/src/timeline2/model/builders/meltBuilder.cpp @@ -1,311 +1,311 @@ /*************************************************************************** * 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 "meltBuilder.hpp" #include "../clipmodel.hpp" #include "../timelineitemmodel.hpp" #include "../timelinemodel.hpp" #include "../trackmodel.hpp" #include "../undohelper.hpp" #include "bin/bin.h" #include "bin/projectitemmodel.h" #include "core.h" #include "kdenlivesettings.h" #include #include #include #include #include #include #include static QStringList m_errorMessage; bool constructTrackFromMelt(const std::shared_ptr &timeline, int tid, Mlt::Tractor &track, const std::unordered_map &binIdCorresp, Fun &undo, Fun &redo, bool audioTrack); bool constructTrackFromMelt(const std::shared_ptr &timeline, int tid, Mlt::Playlist &track, const std::unordered_map &binIdCorresp, Fun &undo, Fun &redo, bool audioTrack); bool constructTimelineFromMelt(const std::shared_ptr &timeline, Mlt::Tractor tractor) { Fun undo = []() { return true; }; Fun redo = []() { return true; }; // First, we destruct the previous tracks timeline->requestReset(undo, redo); m_errorMessage.clear(); std::unordered_map binIdCorresp; pCore->projectItemModel()->loadBinPlaylist(&tractor, timeline->tractor(), binIdCorresp); QSet reserved_names{QLatin1String("playlistmain"), QLatin1String("timeline_preview"), QLatin1String("timeline_overlay"), QLatin1String("black_track")}; bool ok = true; qDebug() << "//////////////////////\nTrying to construct" << tractor.count() << "tracks.\n////////////////////////////////"; for (int i = 0; i < tractor.count() && ok; i++) { std::unique_ptr track(tractor.track(i)); QString playlist_name = track->get("id"); if (reserved_names.contains(playlist_name)) { continue; } switch (track->type()) { case producer_type: // TODO check that it is the black track, and otherwise log an error qDebug() << "SUSPICIOUS: we weren't expecting a producer when parsing the timeline"; break; case tractor_type: { // that is a double track int tid; bool audioTrack = track->get_int("kdenlive:audio_track") == 1; ok = timeline->requestTrackInsertion(-1, tid, QString(), audioTrack, undo, redo, false); int lockState = track->get_int("kdenlive:locked_track"); Mlt::Tractor local_tractor(*track); ok = ok && constructTrackFromMelt(timeline, tid, local_tractor, binIdCorresp, undo, redo, audioTrack); timeline->setTrackProperty(tid, QStringLiteral("kdenlive:thumbs_format"), track->get("kdenlive:thumbs_format")); if (lockState > 0) { timeline->setTrackProperty(tid, QStringLiteral("kdenlive:locked_track"), QString::number(lockState)); } break; } case playlist_type: { // that is a single track qDebug() << "Adding track: " << track->get("id"); int tid; Mlt::Playlist local_playlist(*track); const QString trackName = local_playlist.get("kdenlive:track_name"); bool audioTrack = local_playlist.get_int("kdenlive:audio_track") == 1; ok = timeline->requestTrackInsertion(-1, tid, trackName, audioTrack, undo, redo, false); int muteState = track->get_int("hide"); if (muteState > 0 && (!audioTrack || (audioTrack && muteState != 1))) { timeline->setTrackProperty(tid, QStringLiteral("hide"), QString::number(muteState)); } int lockState = local_playlist.get_int("kdenlive:locked_track"); ok = ok && constructTrackFromMelt(timeline, tid, local_playlist, binIdCorresp, undo, redo, audioTrack); timeline->setTrackProperty(tid, QStringLiteral("kdenlive:thumbs_format"), local_playlist.get("kdenlive:thumbs_format")); if (lockState > 0) { timeline->setTrackProperty(tid, QStringLiteral("kdenlive:locked_track"), QString::number(lockState)); } break; } default: qDebug() << "ERROR: Unexpected item in the timeline"; } } // Loading compositions QScopedPointer service(tractor.producer()); QList compositions; while ((service != nullptr) && service->is_valid()) { if (service->type() == transition_type) { Mlt::Transition t((mlt_transition)service->get_service()); QString id(t.get("kdenlive_id")); QString internal(t.get("internal_added")); if (internal.isEmpty()) { compositions << new Mlt::Transition(t); if (id.isEmpty()) { qDebug() << "// Warning, this should not happen, transition without id: " << t.get("id") << " = " << t.get("mlt_service"); t.set("kdenlive_id", t.get("mlt_service")); } } } service.reset(service->producer()); } // Sort compositions and insert if (!compositions.isEmpty()) { std::sort(compositions.begin(), compositions.end(), [](Mlt::Transition *a, Mlt::Transition *b) { return a->get_b_track() < b->get_b_track(); }); while (!compositions.isEmpty()) { QScopedPointer t(compositions.takeFirst()); auto transProps = std::make_unique(t->get_properties()); QString id(t->get("kdenlive_id")); int compoId; int aTrack = t->get_a_track(); if (aTrack > tractor.count()) { m_errorMessage << i18n("Invalid composition %1 found on track %2 at %3, compositing with track %4.", t->get("id"), t->get_b_track(), t->get_in(), t->get_a_track()); continue; } ok = timeline->requestCompositionInsertion(id, timeline->getTrackIndexFromPosition(t->get_b_track() - 1), t->get_a_track(), t->get_in(), t->get_length(), std::move(transProps), compoId, undo, redo); if (!ok) { qDebug() << "ERROR : failed to insert composition in track " << t->get_b_track() << ", position" << t->get_in() << ", ID: " << id << ", MLT ID: " << t->get("id"); // timeline->requestItemDeletion(compoId, false); m_errorMessage << i18n("Invalid composition %1 found on track %2 at %3.", t->get("id"), t->get_b_track(), t->get_in()); continue; } qDebug() << "Inserted composition in track " << t->get_b_track() << ", position" << t->get_in() << "/" << t->get_out(); } } // build internal track compositing timeline->buildTrackCompositing(); timeline->updateDuration(); if (!ok) { // TODO log error undo(); return false; } if (!m_errorMessage.isEmpty()) { KMessageBox::sorry(qApp->activeWindow(), m_errorMessage.join("\n"), i18n("Problems found in your project file")); } return true; } bool constructTrackFromMelt(const std::shared_ptr &timeline, int tid, Mlt::Tractor &track, const std::unordered_map &binIdCorresp, Fun &undo, Fun &redo, bool audioTrack) { if (track.count() != 2) { // we expect a tractor with two tracks (a "fake" track) qDebug() << "ERROR : wrong number of subtracks"; return false; } for (int i = 0; i < track.count(); i++) { std::unique_ptr sub_track(track.track(i)); if (sub_track->type() != playlist_type) { qDebug() << "ERROR : SubTracks must be MLT::Playlist"; return false; } Mlt::Playlist playlist(*sub_track); constructTrackFromMelt(timeline, tid, playlist, binIdCorresp, undo, redo, audioTrack); if (i == 0) { // Pass track properties int height = track.get_int("kdenlive:trackheight"); timeline->setTrackProperty(tid, "kdenlive:trackheight", height == 0 ? "100" : QString::number(height)); timeline->setTrackProperty(tid, "kdenlive:collapsed", QString::number(track.get_int("kdenlive:collapsed"))); QString trackName = track.get("kdenlive:track_name"); if (!trackName.isEmpty()) { timeline->setTrackProperty(tid, QStringLiteral("kdenlive:track_name"), trackName.toUtf8().constData()); } if (audioTrack) { // This is an audio track timeline->setTrackProperty(tid, QStringLiteral("kdenlive:audio_track"), QStringLiteral("1")); timeline->setTrackProperty(tid, QStringLiteral("hide"), QStringLiteral("1")); } else { // video track, hide audio timeline->setTrackProperty(tid, QStringLiteral("hide"), QStringLiteral("2")); } int muteState = playlist.get_int("hide"); if (muteState > 0 && (!audioTrack || (audioTrack && muteState != 1))) { timeline->setTrackProperty(tid, QStringLiteral("hide"), QString::number(muteState)); } } } std::shared_ptr serv = std::make_shared(track.get_service()); timeline->importTrackEffects(tid, serv); return true; } namespace { // This function tries to recover the state of the producer (audio or video or both) -PlaylistState::ClipState inferState(std::shared_ptr prod, bool audioTrack) +PlaylistState::ClipState inferState(const std::shared_ptr &prod, bool audioTrack) { auto getProperty = [prod](const QString &name) { if (prod->parent().is_valid()) { return QString::fromUtf8(prod->parent().get(name.toUtf8().constData())); } return QString::fromUtf8(prod->get(name.toUtf8().constData())); }; auto getIntProperty = [prod](const QString &name) { if (prod->parent().is_valid()) { return prod->parent().get_int(name.toUtf8().constData()); } return prod->get_int(name.toUtf8().constData()); }; QString service = getProperty("mlt_service"); std::pair VidAud{true, true}; VidAud.first = getIntProperty("set.test_image") == 0; VidAud.second = getIntProperty("set.test_audio") == 0; if (audioTrack || ((service.contains(QStringLiteral("avformat")) && getIntProperty(QStringLiteral("video_index")) == -1))) { VidAud.first = false; } if (!audioTrack || ((service.contains(QStringLiteral("avformat")) && getIntProperty(QStringLiteral("audio_index")) == -1))) { VidAud.second = false; } return stateFromBool(VidAud); } } // namespace bool constructTrackFromMelt(const std::shared_ptr &timeline, int tid, Mlt::Playlist &track, const std::unordered_map &binIdCorresp, Fun &undo, Fun &redo, bool audioTrack) { for (int i = 0; i < track.count(); i++) { if (track.is_blank(i)) { continue; } std::shared_ptr clip(track.get_clip(i)); int position = track.clip_start(i); switch (clip->type()) { case unknown_type: case producer_type: { qDebug() << "Looking for clip clip "<< clip->parent().get("kdenlive:id")<<" = "<parent().get("kdenlive:clipname"); QString binId; if (clip->parent().get_int("_kdenlive_processed") == 1) { // This is a bin clip, already processed no need to change id binId = QString(clip->parent().get("kdenlive:id")); } else { QString clipId = clip->parent().get("kdenlive:id"); if (clipId.startsWith(QStringLiteral("slowmotion"))) { clipId = clipId.section(QLatin1Char(':'), 1, 1); } if (clipId.isEmpty()) { clipId = clip->get("kdenlive:id"); } Q_ASSERT(!clipId.isEmpty() && binIdCorresp.count(clipId) > 0); binId = binIdCorresp.at(clipId); clip->parent().set("kdenlive:id", binId.toUtf8().constData()); clip->parent().set("_kdenlive_processed", 1); } bool ok = false; int cid = -1; if (pCore->bin()->getBinClip(binId)) { PlaylistState::ClipState st = inferState(clip, audioTrack); cid = ClipModel::construct(timeline, binId, clip, st); ok = timeline->requestClipMove(cid, tid, position, true, false, undo, redo); } else { qDebug() << "// Cannot find bin clip: " << binId << " - " << clip->get("id"); } if (!ok && cid > -1) { qDebug() << "ERROR : failed to insert clip in track" << tid << "position" << position; timeline->requestItemDeletion(cid, false); m_errorMessage << i18n("Invalid clip %1 found on track %2 at %3.", clip->parent().get("id"), track.get("id"), position); break; } qDebug() << "Inserted clip in track" << tid << "at " << position; break; } case tractor_type: { // TODO This is a nested timeline qDebug() << "NOT_IMPLEMENTED: code for parsing nested timeline is not there yet."; break; } default: qDebug() << "ERROR : unexpected object found on playlist"; return false; break; } } std::shared_ptr serv = std::make_shared(track.get_service()); timeline->importTrackEffects(tid, serv); return true; } diff --git a/src/timeline2/model/clipmodel.cpp b/src/timeline2/model/clipmodel.cpp index 99638816c..dd576025d 100644 --- a/src/timeline2/model/clipmodel.cpp +++ b/src/timeline2/model/clipmodel.cpp @@ -1,669 +1,669 @@ /*************************************************************************** * 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 "clipmodel.hpp" #include "bin/projectclip.h" #include "bin/projectitemmodel.h" #include "core.h" #include "effects/effectstack/model/effectstackmodel.hpp" #include "macros.hpp" #include "timelinemodel.hpp" #include "trackmodel.hpp" #include #include #include #include -ClipModel::ClipModel(std::shared_ptr parent, std::shared_ptr prod, const QString &binClipId, int id, +ClipModel::ClipModel(const std::shared_ptr &parent, std::shared_ptr prod, const QString &binClipId, int id, PlaylistState::ClipState state, double speed) : MoveableItem(parent, id) , m_producer(std::move(prod)) , m_effectStack(EffectStackModel::construct(m_producer, {ObjectType::TimelineClip, m_id}, parent->m_undoStack)) , m_binClipId(binClipId) , forceThumbReload(false) , m_currentState(state) , m_speed(speed) , m_fakeTrack(-1) { m_producer->set("kdenlive:id", binClipId.toUtf8().constData()); m_producer->set("_kdenlive_cid", m_id); std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(m_binClipId); m_canBeVideo = binClip->hasVideo(); m_canBeAudio = binClip->hasAudio(); m_clipType = binClip->clipType(); if (binClip) { m_endlessResize = !binClip->hasLimitedDuration(); } else { m_endlessResize = false; } QObject::connect(m_effectStack.get(), &EffectStackModel::dataChanged, [&](const QModelIndex &, const QModelIndex &, QVector roles) { qDebug() << "// GOT CLIP STACK DATA CHANGE: " << roles; if (m_currentTrackId != -1) { if (auto ptr = m_parent.lock()) { QModelIndex ix = ptr->makeClipIndexFromID(m_id); qDebug() << "// GOT CLIP STACK DATA CHANGE DONE: " << ix << " = " << roles; ptr->dataChanged(ix, ix, roles); } } }); } int ClipModel::construct(const std::shared_ptr &parent, const QString &binClipId, int id, PlaylistState::ClipState state, double speed) { id = (id == -1 ? TimelineModel::getNextId() : id); std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(binClipId); // We refine the state according to what the clip can actually produce std::pair videoAudio = stateToBool(state); videoAudio.first = videoAudio.first && binClip->hasVideo(); videoAudio.second = videoAudio.second && binClip->hasAudio(); state = stateFromBool(videoAudio); std::shared_ptr cutProducer = binClip->getTimelineProducer(id, state, speed); std::shared_ptr clip(new ClipModel(parent, cutProducer, binClipId, id, state, speed)); clip->setClipState_lambda(state)(); parent->registerClip(clip); return id; } -int ClipModel::construct(const std::shared_ptr &parent, const QString &binClipId, std::shared_ptr producer, +int ClipModel::construct(const std::shared_ptr &parent, const QString &binClipId, const std::shared_ptr &producer, PlaylistState::ClipState state) { // we hand the producer to the bin clip, and in return we get a cut to a good master producer // We might not be able to use directly the producer that we receive as an argument, because it cannot share the same master producer with any other // clipModel (due to a mlt limitation, see ProjectClip doc) int id = TimelineModel::getNextId(); std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(binClipId); // We refine the state according to what the clip can actually produce std::pair videoAudio = stateToBool(state); videoAudio.first = videoAudio.first && binClip->hasVideo(); videoAudio.second = videoAudio.second && binClip->hasAudio(); state = stateFromBool(videoAudio); double speed = 1.0; if (QString::fromUtf8(producer->parent().get("mlt_service")) == QLatin1String("timewarp")) { speed = producer->parent().get_double("warp_speed"); } auto result = binClip->giveMasterAndGetTimelineProducer(id, producer, state); std::shared_ptr clip(new ClipModel(parent, result.first, binClipId, id, state, speed)); clip->setClipState_lambda(state)(); clip->m_effectStack->importEffects(producer, state, result.second); parent->registerClip(clip); return id; } void ClipModel::registerClipToBin(std::shared_ptr service, bool registerProducer) { std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(m_binClipId); if (!binClip) { qDebug() << "Error : Bin clip for id: " << m_binClipId << " NOT AVAILABLE!!!"; } qDebug() << "REGISTRATION " << m_id << "ptr count" << m_parent.use_count(); - binClip->registerService(m_parent, m_id, service, registerProducer); + binClip->registerService(m_parent, m_id, std::move(service), registerProducer); } void ClipModel::deregisterClipToBin() { std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(m_binClipId); binClip->deregisterTimelineClip(m_id); pCore->removeFromSelection(m_id); } ClipModel::~ClipModel() {} bool ClipModel::requestResize(int size, bool right, Fun &undo, Fun &redo, bool logUndo) { QWriteLocker locker(&m_lock); // qDebug() << "RESIZE CLIP" << m_id << "target size=" << size << "right=" << right << "endless=" << m_endlessResize << "length" << // m_producer->get_length(); if (!m_endlessResize && (size <= 0 || size > m_producer->get_length())) { return false; } int delta = getPlaytime() - size; if (delta == 0) { return true; } int in = m_producer->get_in(); int out = m_producer->get_out(); int old_in = in, old_out = out; // check if there is enough space on the chosen side if (!right && in + delta < 0 && !m_endlessResize) { return false; } if (!m_endlessResize && right && (out - delta >= m_producer->get_length())) { return false; } if (right) { out -= delta; } else { in += delta; } // qDebug() << "Resize facts delta =" << delta << "old in" << old_in << "old_out" << old_out << "in" << in << "out" << out; std::function track_operation = []() { return true; }; std::function track_reverse = []() { return true; }; int outPoint = out; int inPoint = in; int offset = 0; if (m_endlessResize) { offset = inPoint; outPoint = out - in; inPoint = 0; } if (m_currentTrackId != -1) { if (auto ptr = m_parent.lock()) { track_operation = ptr->getTrackById(m_currentTrackId)->requestClipResize_lambda(m_id, inPoint, outPoint, right); } else { qDebug() << "Error : Moving clip failed because parent timeline is not available anymore"; Q_ASSERT(false); } } else { // Ensure producer is long enough if (m_endlessResize && outPoint > m_producer->parent().get_length()) { m_producer->set("length", outPoint + 1); } } Fun operation = [this, inPoint, outPoint, track_operation]() { if (track_operation()) { m_producer->set_in_and_out(inPoint, outPoint); return true; } return false; }; if (operation()) { // Now, we are in the state in which the timeline should be when we try to revert current action. So we can build the reverse action from here if (m_currentTrackId != -1) { QVector roles{TimelineModel::DurationRole}; if (!right) { roles.push_back(TimelineModel::StartRole); roles.push_back(TimelineModel::InPointRole); } else { roles.push_back(TimelineModel::OutPointRole); } if (auto ptr = m_parent.lock()) { QModelIndex ix = ptr->makeClipIndexFromID(m_id); // TODO: integrate in undo ptr->dataChanged(ix, ix, roles); track_reverse = ptr->getTrackById(m_currentTrackId)->requestClipResize_lambda(m_id, old_in, old_out, right); } } Fun reverse = [this, old_in, old_out, track_reverse]() { if (track_reverse()) { m_producer->set_in_and_out(old_in, old_out); return true; } return false; }; qDebug() << "----------\n-----------\n// ADJUSTING EFFECT LENGTH, LOGUNDO " << logUndo << ", " << old_in << "/" << inPoint << ", " << m_producer->get_playtime(); if (logUndo) { adjustEffectLength(right, old_in, inPoint, old_out - old_in, m_producer->get_playtime(), offset, reverse, operation, logUndo); } UPDATE_UNDO_REDO(operation, reverse, undo, redo); return true; } return false; } const QString ClipModel::getProperty(const QString &name) const { READ_LOCK(); if (service()->parent().is_valid()) { return QString::fromUtf8(service()->parent().get(name.toUtf8().constData())); } return QString::fromUtf8(service()->get(name.toUtf8().constData())); } int ClipModel::getIntProperty(const QString &name) const { READ_LOCK(); if (service()->parent().is_valid()) { return service()->parent().get_int(name.toUtf8().constData()); } return service()->get_int(name.toUtf8().constData()); } QSize ClipModel::getFrameSize() const { READ_LOCK(); if (service()->parent().is_valid()) { return QSize(service()->parent().get_int("meta.media.width"), service()->parent().get_int("meta.media.height")); } return QSize(service()->get_int("meta.media.width"), service()->get_int("meta.media.height")); } double ClipModel::getDoubleProperty(const QString &name) const { READ_LOCK(); if (service()->parent().is_valid()) { return service()->parent().get_double(name.toUtf8().constData()); } return service()->get_double(name.toUtf8().constData()); } Mlt::Producer *ClipModel::service() const { READ_LOCK(); return m_producer.get(); } std::shared_ptr ClipModel::getProducer() { READ_LOCK(); return m_producer; } int ClipModel::getPlaytime() const { READ_LOCK(); return m_producer->get_playtime(); } void ClipModel::setTimelineEffectsEnabled(bool enabled) { QWriteLocker locker(&m_lock); m_effectStack->setEffectStackEnabled(enabled); } bool ClipModel::addEffect(const QString &effectId) { QWriteLocker locker(&m_lock); if (EffectsRepository::get()->getType(effectId) == EffectType::Audio) { if (m_currentState == PlaylistState::VideoOnly) { return false; } } else if (m_currentState == PlaylistState::AudioOnly) { return false; } m_effectStack->appendEffect(effectId); return true; } -bool ClipModel::copyEffect(std::shared_ptr stackModel, int rowId) +bool ClipModel::copyEffect(const std::shared_ptr &stackModel, int rowId) { QWriteLocker locker(&m_lock); m_effectStack->copyEffect(stackModel->getEffectStackRow(rowId), m_currentState); return true; } bool ClipModel::importEffects(std::shared_ptr stackModel) { QWriteLocker locker(&m_lock); - m_effectStack->importEffects(stackModel, m_currentState); + m_effectStack->importEffects(std::move(stackModel), m_currentState); return true; } bool ClipModel::importEffects(std::weak_ptr service) { QWriteLocker locker(&m_lock); - m_effectStack->importEffects(service, m_currentState); + m_effectStack->importEffects(std::move(service), m_currentState); return true; } bool ClipModel::removeFade(bool fromStart) { QWriteLocker locker(&m_lock); m_effectStack->removeFade(fromStart); return true; } bool ClipModel::adjustEffectLength(bool adjustFromEnd, int oldIn, int newIn, int oldDuration, int duration, int offset, Fun &undo, Fun &redo, bool logUndo) { QWriteLocker locker(&m_lock); return m_effectStack->adjustStackLength(adjustFromEnd, oldIn, oldDuration, newIn, duration, offset, undo, redo, logUndo); } bool ClipModel::adjustEffectLength(const QString &effectName, int duration, int originalDuration, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); qDebug() << ".... ADJUSTING FADE LENGTH: " << duration << " / " << effectName; Fun operation = [this, duration, effectName, originalDuration]() { return m_effectStack->adjustFadeLength(duration, effectName == QLatin1String("fadein") || effectName == QLatin1String("fade_to_black"), audioEnabled(), !isAudioOnly(), originalDuration > 0); }; if (operation() && originalDuration > 0) { Fun reverse = [this, originalDuration, effectName]() { return m_effectStack->adjustFadeLength(originalDuration, effectName == QLatin1String("fadein") || effectName == QLatin1String("fade_to_black"), audioEnabled(), !isAudioOnly(), true); }; UPDATE_UNDO_REDO(operation, reverse, undo, redo); } return true; } bool ClipModel::audioEnabled() const { READ_LOCK(); return stateToBool(m_currentState).second; } bool ClipModel::isAudioOnly() const { READ_LOCK(); return m_currentState == PlaylistState::AudioOnly; } void ClipModel::refreshProducerFromBin(PlaylistState::ClipState state, double speed) { // We require that the producer is not in the track when we refresh the producer, because otherwise the modification will not be propagated. Remove the clip // first, refresh, and then replant. Q_ASSERT(m_currentTrackId == -1); QWriteLocker locker(&m_lock); int in = getIn(); int out = getOut(); qDebug() << "refresh " << speed << m_speed << in << out; if (!qFuzzyCompare(speed, m_speed) && !qFuzzyCompare(speed, 0.)) { in = in * m_speed / speed; out = in + getPlaytime() - 1; // prevent going out of the clip's range out = std::min(out, int(double(m_producer->get_length()) * m_speed / speed) - 1); m_speed = speed; qDebug() << "changing speed" << in << out << m_speed; } std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(m_binClipId); std::shared_ptr binProducer = binClip->getTimelineProducer(m_id, state, m_speed); m_producer = std::move(binProducer); m_producer->set_in_and_out(in, out); // replant effect stack in updated service m_effectStack->resetService(m_producer); m_producer->set("kdenlive:id", binClip->clipId().toUtf8().constData()); m_producer->set("_kdenlive_cid", m_id); m_endlessResize = !binClip->hasLimitedDuration(); } void ClipModel::refreshProducerFromBin() { refreshProducerFromBin(m_currentState); } bool ClipModel::useTimewarpProducer(double speed, Fun &undo, Fun &redo) { if (m_endlessResize) { // no timewarp for endless producers return false; } if (qFuzzyCompare(speed, m_speed)) { // nothing to do return true; } std::function local_undo = []() { return true; }; std::function local_redo = []() { return true; }; double previousSpeed = getSpeed(); int oldDuration = getPlaytime(); int newDuration = int(double(oldDuration) * previousSpeed / speed); int oldOut = getOut(); int oldIn = getIn(); auto operation = useTimewarpProducer_lambda(speed); auto reverse = useTimewarpProducer_lambda(previousSpeed); if (oldOut >= newDuration) { // in that case, we are going to shrink the clip when changing the producer. We must undo that when reloading the old producer reverse = [reverse, oldIn, oldOut, this]() { bool res = reverse(); if (res) { setInOut(oldIn, oldOut); } return res; }; } if (operation()) { UPDATE_UNDO_REDO(operation, reverse, local_undo, local_redo); bool res = requestResize(newDuration, true, local_undo, local_redo, true); if (!res) { local_undo(); return false; } UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } qDebug() << "tw: operation fail"; return false; } Fun ClipModel::useTimewarpProducer_lambda(double speed) { QWriteLocker locker(&m_lock); return [speed, this]() { qDebug() << "timeWarp producer" << speed; refreshProducerFromBin(m_currentState, speed); if (auto ptr = m_parent.lock()) { QModelIndex ix = ptr->makeClipIndexFromID(m_id); ptr->notifyChange(ix, ix, TimelineModel::SpeedRole); } return true; }; } QVariant ClipModel::getAudioWaveform() { READ_LOCK(); std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(m_binClipId); if (binClip) { return QVariant::fromValue(binClip->audioFrameCache); } return QVariant(); } const QString &ClipModel::binId() const { return m_binClipId; } std::shared_ptr ClipModel::getMarkerModel() const { READ_LOCK(); return pCore->projectItemModel()->getClipByBinID(m_binClipId)->getMarkerModel(); } int ClipModel::audioChannels() const { READ_LOCK(); return pCore->projectItemModel()->getClipByBinID(m_binClipId)->audioChannels(); } int ClipModel::fadeIn() const { return m_effectStack->getFadePosition(true); } int ClipModel::fadeOut() const { return m_effectStack->getFadePosition(false); } double ClipModel::getSpeed() const { return m_speed; } KeyframeModel *ClipModel::getKeyframeModel() { return m_effectStack->getEffectKeyframeModel(); } bool ClipModel::showKeyframes() const { READ_LOCK(); return !service()->get_int("kdenlive:hide_keyframes"); } void ClipModel::setShowKeyframes(bool show) { QWriteLocker locker(&m_lock); service()->set("kdenlive:hide_keyframes", (int)!show); } Fun ClipModel::setClipState_lambda(PlaylistState::ClipState state) { QWriteLocker locker(&m_lock); return [this, state]() { if (auto ptr = m_parent.lock()) { switch (state) { case PlaylistState::Disabled: m_producer->set("set.test_audio", 1); m_producer->set("set.test_image", 1); break; case PlaylistState::VideoOnly: m_producer->set("set.test_image", 0); break; case PlaylistState::AudioOnly: m_producer->set("set.test_audio", 0); break; default: // error break; } m_currentState = state; if (m_currentTrackId != -1 && ptr->isClip(m_id)) { // if this is false, the clip is being created. Don't update model in that case QModelIndex ix = ptr->makeClipIndexFromID(m_id); ptr->dataChanged(ix, ix, {TimelineModel::StatusRole}); } return true; } return false; }; } bool ClipModel::setClipState(PlaylistState::ClipState state, Fun &undo, Fun &redo) { if (state == PlaylistState::VideoOnly && !canBeVideo()) { return false; } if (state == PlaylistState::AudioOnly && !canBeAudio()) { return false; } if (state == m_currentState) { return true; } auto old_state = m_currentState; auto operation = setClipState_lambda(state); if (operation()) { auto reverse = setClipState_lambda(old_state); UPDATE_UNDO_REDO(operation, reverse, undo, redo); return true; } return false; } PlaylistState::ClipState ClipModel::clipState() const { READ_LOCK(); return m_currentState; } ClipType::ProducerType ClipModel::clipType() const { READ_LOCK(); return m_clipType; } -void ClipModel::passTimelineProperties(std::shared_ptr other) +void ClipModel::passTimelineProperties(const std::shared_ptr &other) { READ_LOCK(); Mlt::Properties source(m_producer->get_properties()); Mlt::Properties dest(other->service()->get_properties()); dest.pass_list(source, "kdenlive:hide_keyframes,kdenlive:activeeffect"); } bool ClipModel::canBeVideo() const { return m_canBeVideo; } bool ClipModel::canBeAudio() const { return m_canBeAudio; } const QString ClipModel::effectNames() const { READ_LOCK(); return m_effectStack->effectNames(); } int ClipModel::getFakeTrackId() const { return m_fakeTrack; } void ClipModel::setFakeTrackId(int fid) { m_fakeTrack = fid; } int ClipModel::getFakePosition() const { return m_fakePosition; } void ClipModel::setFakePosition(int fid) { m_fakePosition = fid; } QDomElement ClipModel::toXml(QDomDocument &document) { QDomElement container = document.createElement(QStringLiteral("clip")); container.setAttribute(QStringLiteral("binid"), m_binClipId); container.setAttribute(QStringLiteral("id"), m_id); container.setAttribute(QStringLiteral("in"), getIn()); container.setAttribute(QStringLiteral("out"), getOut()); container.setAttribute(QStringLiteral("position"), getPosition()); if (auto ptr = m_parent.lock()) { int trackId = ptr->getTrackPosition(getCurrentTrackId()); container.setAttribute(QStringLiteral("track"), trackId); } container.setAttribute(QStringLiteral("speed"), m_speed); container.appendChild(m_effectStack->toXml(document)); return container; } bool ClipModel::checkConsistency() { if (!m_effectStack->checkConsistency()) { qDebug() << "Consistency check failed for effecstack"; return false; } std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(m_binClipId); auto instances = binClip->timelineInstances(); bool found = false; for (const auto &i : instances) { if (i == m_id) { found = true; break; } } if (!found) { qDebug() << "ERROR: binClip doesn't acknowledge timeline clip existence"; return false; } if (m_currentState == PlaylistState::VideoOnly && !m_canBeVideo) { qDebug() << "ERROR: clip is in video state but doesn't have video"; return false; } if (m_currentState == PlaylistState::AudioOnly && !m_canBeAudio) { qDebug() << "ERROR: clip is in video state but doesn't have video"; return false; } // TODO: check speed return true; } diff --git a/src/timeline2/model/clipmodel.hpp b/src/timeline2/model/clipmodel.hpp index 238ecf08d..6426aabd9 100644 --- a/src/timeline2/model/clipmodel.hpp +++ b/src/timeline2/model/clipmodel.hpp @@ -1,215 +1,215 @@ /*************************************************************************** * Copyright (C) 2017 by Nicolas Carion * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #ifndef CLIPMODEL_H #define CLIPMODEL_H #include "moveableItem.hpp" #include "undohelper.hpp" #include #include namespace Mlt { class Producer; } class EffectStackModel; class MarkerListModel; class TimelineModel; class TrackModel; class KeyframeModel; /* @brief This class represents a Clip object, as viewed by the backend. In general, the Gui associated with it will send modification queries (such as resize or move), and this class authorize them or not depending on the validity of the modifications */ class ClipModel : public MoveableItem { ClipModel() = delete; protected: /* This constructor is not meant to be called, call the static construct instead */ - ClipModel(std::shared_ptr parent, std::shared_ptr prod, const QString &binClipId, int id, PlaylistState::ClipState state, - double speed = 1.); + ClipModel(const std::shared_ptr &parent, std::shared_ptr prod, const QString &binClipId, int id, + PlaylistState::ClipState state, double speed = 1.); public: ~ClipModel(); /* @brief Creates a clip, which references itself to the parent timeline Returns the (unique) id of the created clip @param parent is a pointer to the timeline @param binClip is the id of the bin clip associated @param id Requested id of the clip. Automatic if -1 */ static int construct(const std::shared_ptr &parent, const QString &binClipId, int id, PlaylistState::ClipState state, double speed = 1.); /* @brief Creates a clip, which references itself to the parent timeline Returns the (unique) id of the created clip This variants assumes a producer is already known, which should typically happen only at loading time. Note that there is no guarantee that this producer is actually going to be used. It might be discarded. */ - static int construct(const std::shared_ptr &parent, const QString &binClipId, std::shared_ptr producer, + static int construct(const std::shared_ptr &parent, const QString &binClipId, const std::shared_ptr &producer, PlaylistState::ClipState state); /* @brief returns a property of the clip, or from it's parent if it's a cut */ const QString getProperty(const QString &name) const override; int getIntProperty(const QString &name) const; double getDoubleProperty(const QString &name) const; QSize getFrameSize() const; Q_INVOKABLE bool showKeyframes() const; Q_INVOKABLE void setShowKeyframes(bool show); /* @brief Returns true if the clip can be converted to a video clip */ bool canBeVideo() const; /* @brief Returns true if the clip can be converted to an audio clip */ bool canBeAudio() const; /* @brief Returns a comma separated list of effect names */ const QString effectNames() const; /** @brief Returns the timeline clip status (video / audio only) */ PlaylistState::ClipState clipState() const; /** @brief Returns the bin clip type (image, color, AV, ...) */ ClipType::ProducerType clipType() const; /** @brief Sets the timeline clip status (video / audio only) */ bool setClipState(PlaylistState::ClipState state, Fun &undo, Fun &redo); /** @brief The fake track is used in insrt/overwrote mode. * in this case, dragging a clip is always accepted, but the change is not applied to the model. * so we use a 'fake' track id to pass to the qml view */ int getFakeTrackId() const; void setFakeTrackId(int fid); int getFakePosition() const; void setFakePosition(int fid); /* @brief Returns an XML representation of the clip with its effects */ QDomElement toXml(QDomDocument &document); protected: // helper functions that creates the lambda Fun setClipState_lambda(PlaylistState::ClipState state); public: /* @brief returns the length of the item on the timeline */ int getPlaytime() const override; /** @brief Returns audio cache data from bin clip to display audio thumbs */ QVariant getAudioWaveform(); /** @brief Returns the bin clip's id */ const QString &binId() const; void registerClipToBin(std::shared_ptr service, bool registerProducer); void deregisterClipToBin(); bool addEffect(const QString &effectId); - bool copyEffect(std::shared_ptr stackModel, int rowId); + bool copyEffect(const std::shared_ptr &stackModel, int rowId); /* @brief Import effects from a different stackModel */ bool importEffects(std::shared_ptr stackModel); /* @brief Import effects from a service that contains some (another clip?) */ bool importEffects(std::weak_ptr service); bool removeFade(bool fromStart); /** @brief Adjust effects duration. Should be called after each resize / cut operation */ bool adjustEffectLength(bool adjustFromEnd, int oldIn, int newIn, int oldDuration, int duration, int offset, Fun &undo, Fun &redo, bool logUndo); bool adjustEffectLength(const QString &effectName, int duration, int originalDuration, Fun &undo, Fun &redo); - void passTimelineProperties(std::shared_ptr other); + void passTimelineProperties(const std::shared_ptr &other); KeyframeModel *getKeyframeModel(); int fadeIn() const; int fadeOut() const; friend class TrackModel; friend class TimelineModel; friend class TimelineItemModel; friend class TimelineController; friend struct TimelineFunctions; protected: Mlt::Producer *service() const override; /* @brief Performs a resize of the given clip. Returns true if the operation succeeded, and otherwise nothing is modified This method is protected because it shouldn't be called directly. Call the function in the timeline instead. If a snap point is within reach, the operation will be coerced to use it. @param size is the new size of the clip @param right is true if we change the right side of the clip, false otherwise @param undo Lambda function containing the current undo stack. Will be updated with current operation @param redo Lambda function containing the current redo queue. Will be updated with current operation */ bool requestResize(int size, bool right, Fun &undo, Fun &redo, bool logUndo = true) override; /* @brief This function change the global (timeline-wise) enabled state of the effects */ void setTimelineEffectsEnabled(bool enabled); /* @brief This functions should be called when the producer of the binClip changes, to allow refresh * @param state corresponds to the state of the clip we want (audio or video) * @param speed corresponds to the speed we need. Leave to 0 to keep current speed. Warning: this function doesn't notify the model. Unless you know what * you are doing, better use useTimewarProducer to change the speed */ void refreshProducerFromBin(PlaylistState::ClipState state, double speed = 0); void refreshProducerFromBin(); /* @brief This functions replaces the current producer with a slowmotion one It also resizes the producer so that set of frames contained in the clip is the same */ bool useTimewarpProducer(double speed, Fun &undo, Fun &redo); // @brief Lambda that merely changes the speed (in and out are untouched) Fun useTimewarpProducer_lambda(double speed); /** @brief Returns the marker model associated with this clip */ std::shared_ptr getMarkerModel() const; /** @brief Returns the number of audio channels for this clip */ int audioChannels() const; bool audioEnabled() const; bool isAudioOnly() const; double getSpeed() const; /*@brief This is a debug function to ensure the clip is in a valid state */ bool checkConsistency(); protected: std::shared_ptr m_producer; std::shared_ptr getProducer(); std::shared_ptr m_effectStack; QString m_binClipId; // This is the Id of the bin clip this clip corresponds to. bool m_endlessResize; // Whether this clip can be freely resized bool forceThumbReload; // Used to trigger a forced thumb reload, when producer changes PlaylistState::ClipState m_currentState; ClipType::ProducerType m_clipType; double m_speed = -1; // Speed of the clip bool m_canBeVideo, m_canBeAudio; // Fake track id, used when dragging in insert/overwrite mode int m_fakeTrack; int m_fakePosition; }; #endif diff --git a/src/timeline2/model/compositionmodel.cpp b/src/timeline2/model/compositionmodel.cpp index e8e6c1c41..7a1c86aff 100644 --- a/src/timeline2/model/compositionmodel.cpp +++ b/src/timeline2/model/compositionmodel.cpp @@ -1,284 +1,284 @@ /*************************************************************************** * Copyright (C) 2017 by Jean-Baptiste Mardelle * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #include "compositionmodel.hpp" #include "assets/keyframes/model/keyframemodellist.hpp" #include "timelinemodel.hpp" #include "trackmodel.hpp" #include "transitions/transitionsrepository.hpp" #include "undohelper.hpp" #include #include #include CompositionModel::CompositionModel(std::weak_ptr parent, std::unique_ptr transition, int id, const QDomElement &transitionXml, const QString &transitionId) : MoveableItem(std::move(parent), id) , AssetParameterModel(std::move(transition), transitionXml, transitionId, {ObjectType::TimelineComposition, m_id}) , a_track(-1) , m_duration(0) { m_compositionName = TransitionsRepository::get()->getName(transitionId); } int CompositionModel::construct(const std::weak_ptr &parent, const QString &transitionId, int id, std::unique_ptr sourceProperties) { std::unique_ptr transition = TransitionsRepository::get()->getTransition(transitionId); transition->set_in_and_out(0, 0); auto xml = TransitionsRepository::get()->getXml(transitionId); if (sourceProperties) { // Paste parameters from existing source composition QStringList sourceProps; for (int i = 0; i < sourceProperties->count(); i++) { sourceProps << sourceProperties->get_name(i); } QDomNodeList params = xml.elementsByTagName(QStringLiteral("parameter")); for (int i = 0; i < params.count(); ++i) { QDomElement currentParameter = params.item(i).toElement(); QString paramName = currentParameter.attribute(QStringLiteral("name")); if (!sourceProps.contains(paramName)) { continue; } QString paramValue = sourceProperties->get(paramName.toUtf8().constData()); currentParameter.setAttribute(QStringLiteral("value"), paramValue); } } std::shared_ptr composition(new CompositionModel(parent, std::move(transition), id, xml, transitionId)); id = composition->m_id; if (auto ptr = parent.lock()) { ptr->registerComposition(composition); } else { qDebug() << "Error : construction of composition failed because parent timeline is not available anymore"; Q_ASSERT(false); } return id; } bool CompositionModel::requestResize(int size, bool right, Fun &undo, Fun &redo, bool logUndo) { QWriteLocker locker(&m_lock); if (size <= 0) { return false; } int delta = getPlaytime() - size; qDebug() << "compo request resize to " << size << ", ACTUAL SZ: " << getPlaytime() << ", " << right << delta; int in = getIn(); int out = in + getPlaytime() - 1; int oldDuration = out - in; int old_in = in, old_out = out; if (right) { out -= delta; } else { in += delta; } // if the in becomes negative, we add the necessary length in out. if (in < 0) { out = out - in; in = 0; } std::function track_operation = []() { return true; }; std::function track_reverse = []() { return true; }; if (m_currentTrackId != -1) { if (auto ptr = m_parent.lock()) { track_operation = ptr->getTrackById(m_currentTrackId)->requestCompositionResize_lambda(m_id, in, out, logUndo); } else { qDebug() << "Error : Moving composition failed because parent timeline is not available anymore"; Q_ASSERT(false); } } else { // Perform resize only setInOut(in, out); } - Fun operation = [in, out, track_operation, this]() { + Fun operation = [track_operation]() { if (track_operation()) { return true; } return false; }; if (operation()) { // Now, we are in the state in which the timeline should be when we try to revert current action. So we can build the reverse action from here auto ptr = m_parent.lock(); // we send a list of roles to be updated QVector roles{TimelineModel::DurationRole}; if (!right) { roles.push_back(TimelineModel::StartRole); } if (m_currentTrackId != -1 && ptr) { QModelIndex ix = ptr->makeCompositionIndexFromID(m_id); // TODO: integrate in undo ptr->dataChanged(ix, ix, roles); track_reverse = ptr->getTrackById(m_currentTrackId)->requestCompositionResize_lambda(m_id, old_in, old_out, logUndo); } - Fun reverse = [old_in, old_out, track_reverse, this]() { + Fun reverse = [track_reverse]() { if (track_reverse()) { return true; } return false; }; auto kfr = getKeyframeModel(); if (kfr) { // Adjust keyframe length if (oldDuration > 0) { kfr->resizeKeyframes(0, oldDuration, 0, out - in, 0, right, undo, redo); } - Fun refresh = [kfr, this]() { + Fun refresh = [kfr]() { kfr->modelChanged(); return true; }; refresh(); UPDATE_UNDO_REDO(refresh, refresh, undo, redo); } UPDATE_UNDO_REDO(operation, reverse, undo, redo); return true; } return false; } const QString CompositionModel::getProperty(const QString &name) const { READ_LOCK(); return QString::fromUtf8(service()->get(name.toUtf8().constData())); } Mlt::Transition *CompositionModel::service() const { READ_LOCK(); return static_cast(m_asset.get()); } Mlt::Properties *CompositionModel::properties() { READ_LOCK(); return new Mlt::Properties(m_asset.get()->get_properties()); } int CompositionModel::getPlaytime() const { READ_LOCK(); return m_duration + 1; } int CompositionModel::getATrack() const { READ_LOCK(); return a_track == -1 ? -1 : service()->get_int("a_track"); } void CompositionModel::setForceTrack(bool force) { READ_LOCK(); service()->set("force_track", force ? 1 : 0); } int CompositionModel::getForcedTrack() const { QWriteLocker locker(&m_lock); return (service()->get_int("force_track") == 0 || a_track == -1) ? -1 : service()->get_int("a_track"); } void CompositionModel::setATrack(int trackMltPosition, int trackId) { QWriteLocker locker(&m_lock); Q_ASSERT(trackId != getCurrentTrackId()); // can't compose with same track a_track = trackMltPosition; if (a_track >= 0) { service()->set("a_track", trackMltPosition); } if (m_currentTrackId != -1) { emit compositionTrackChanged(); } } KeyframeModel *CompositionModel::getEffectKeyframeModel() { prepareKeyframes(); if (getKeyframeModel()) { return getKeyframeModel()->getKeyModel(); } return nullptr; } bool CompositionModel::showKeyframes() const { READ_LOCK(); return !service()->get_int("kdenlive:hide_keyframes"); } void CompositionModel::setShowKeyframes(bool show) { QWriteLocker locker(&m_lock); service()->set("kdenlive:hide_keyframes", (int)!show); } const QString &CompositionModel::displayName() const { return m_compositionName; } void CompositionModel::setInOut(int in, int out) { MoveableItem::setInOut(in, out); m_duration = out - in; setPosition(in); } void CompositionModel::setCurrentTrackId(int tid) { MoveableItem::setCurrentTrackId(tid); } int CompositionModel::getOut() const { return getPosition() + m_duration; } int CompositionModel::getIn() const { return getPosition(); } QDomElement CompositionModel::toXml(QDomDocument &document) { QDomElement container = document.createElement(QStringLiteral("composition")); container.setAttribute(QStringLiteral("id"), m_id); container.setAttribute(QStringLiteral("composition"), m_assetId); container.setAttribute(QStringLiteral("in"), getIn()); container.setAttribute(QStringLiteral("out"), getOut()); container.setAttribute(QStringLiteral("position"), getPosition()); if (auto ptr = m_parent.lock()) { int trackId = ptr->getTrackPosition(getCurrentTrackId()); container.setAttribute(QStringLiteral("track"), trackId); } container.setAttribute(QStringLiteral("a_track"), getATrack()); QScopedPointer props(properties()); for (int i = 0; i < props->count(); i++) { QString name = props->get_name(i); if (name.startsWith(QLatin1Char('_'))) { continue; } Xml::setXmlProperty(container, name, props->get(i)); } return container; } diff --git a/src/timeline2/model/groupsmodel.cpp b/src/timeline2/model/groupsmodel.cpp index eee501aa5..f18b496b3 100644 --- a/src/timeline2/model/groupsmodel.cpp +++ b/src/timeline2/model/groupsmodel.cpp @@ -1,1020 +1,1020 @@ /*************************************************************************** * 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 "groupsmodel.hpp" #include "macros.hpp" #include "timelineitemmodel.hpp" #include "trackmodel.hpp" #include #include #include #include #include #include #include #include GroupsModel::GroupsModel(std::weak_ptr parent) : m_parent(std::move(parent)) , m_lock(QReadWriteLock::Recursive) { } void GroupsModel::promoteToGroup(int gid, GroupType type) { Q_ASSERT(type != GroupType::Leaf); Q_ASSERT(m_groupIds.count(gid) == 0); m_groupIds.insert({gid, type}); auto ptr = m_parent.lock(); if (ptr) { // qDebug() << "Registering group" << gid << "of type" << groupTypeToStr(getType(gid)); ptr->registerGroup(gid); } else { qDebug() << "Impossible to create group because the timeline is not available anymore"; Q_ASSERT(false); } } void GroupsModel::downgradeToLeaf(int gid) { Q_ASSERT(m_groupIds.count(gid) != 0); Q_ASSERT(m_downLink.at(gid).size() == 0); auto ptr = m_parent.lock(); if (ptr) { // qDebug() << "Deregistering group" << gid << "of type" << groupTypeToStr(getType(gid)); ptr->deregisterGroup(gid); m_groupIds.erase(gid); } else { qDebug() << "Impossible to ungroup item because the timeline is not available anymore"; Q_ASSERT(false); } } Fun GroupsModel::groupItems_lambda(int gid, const std::unordered_set &ids, GroupType type, int parent) { QWriteLocker locker(&m_lock); Q_ASSERT(ids.size() == 0 || type != GroupType::Leaf); return [gid, ids, parent, type, this]() { createGroupItem(gid); if (parent != -1) { setGroup(gid, parent); } if (ids.size() > 0) { promoteToGroup(gid, type); std::unordered_set roots; std::transform(ids.begin(), ids.end(), std::inserter(roots, roots.begin()), [&](int id) { return getRootId(id); }); auto ptr = m_parent.lock(); if (!ptr) Q_ASSERT(false); for (int id : roots) { setGroup(getRootId(id), gid, type != GroupType::Selection); } } return true; }; } int GroupsModel::groupItems(const std::unordered_set &ids, Fun &undo, Fun &redo, GroupType type, bool force) { QWriteLocker locker(&m_lock); Q_ASSERT(type != GroupType::Leaf); Q_ASSERT(!ids.empty()); std::unordered_set roots; std::transform(ids.begin(), ids.end(), std::inserter(roots, roots.begin()), [&](int id) { return getRootId(id); }); if (roots.size() == 1 && !force) { // We do not create a group with only one element. Instead, we return the id of that element return *(roots.begin()); } int gid = TimelineModel::getNextId(); auto operation = groupItems_lambda(gid, roots, type); if (operation()) { auto reverse = destructGroupItem_lambda(gid); UPDATE_UNDO_REDO(operation, reverse, undo, redo); return gid; } return -1; } bool GroupsModel::ungroupItem(int id, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); int gid = getRootId(id); if (m_groupIds.count(gid) == 0) { // element is not part of a group return false; } return destructGroupItem(gid, true, undo, redo); } void GroupsModel::createGroupItem(int id) { QWriteLocker locker(&m_lock); Q_ASSERT(m_upLink.count(id) == 0); Q_ASSERT(m_downLink.count(id) == 0); m_upLink[id] = -1; m_downLink[id] = std::unordered_set(); } Fun GroupsModel::destructGroupItem_lambda(int id) { QWriteLocker locker(&m_lock); return [this, id]() { removeFromGroup(id); auto ptr = m_parent.lock(); if (!ptr) Q_ASSERT(false); for (int child : m_downLink[id]) { m_upLink[child] = -1; QModelIndex ix; if (ptr->isClip(child)) { ix = ptr->makeClipIndexFromID(child); } else if (ptr->isComposition(child)) { ix = ptr->makeCompositionIndexFromID(child); } if (ix.isValid()) { ptr->dataChanged(ix, ix, {TimelineModel::GroupedRole}); } } m_downLink[id].clear(); if (getType(id) != GroupType::Leaf) { downgradeToLeaf(id); } m_downLink.erase(id); m_upLink.erase(id); return true; }; } bool GroupsModel::destructGroupItem(int id, bool deleteOrphan, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); Q_ASSERT(m_upLink.count(id) > 0); int parent = m_upLink[id]; auto old_children = m_downLink[id]; auto old_type = getType(id); auto old_parent_type = GroupType::Normal; if (parent != -1) { old_parent_type = getType(parent); } auto operation = destructGroupItem_lambda(id); if (operation()) { auto reverse = groupItems_lambda(id, old_children, old_type, parent); // we may need to reset the group of the parent if (parent != -1) { auto setParent = [&, old_parent_type, parent]() { setType(parent, old_parent_type); return true; }; PUSH_LAMBDA(setParent, reverse); } UPDATE_UNDO_REDO(operation, reverse, undo, redo); if (parent != -1 && m_downLink[parent].empty() && deleteOrphan) { return destructGroupItem(parent, true, undo, redo); } return true; } return false; } bool GroupsModel::destructGroupItem(int id) { QWriteLocker locker(&m_lock); return destructGroupItem_lambda(id)(); } int GroupsModel::getRootId(int id) const { READ_LOCK(); std::unordered_set seen; // we store visited ids to detect cycles int father = -1; do { Q_ASSERT(m_upLink.count(id) > 0); Q_ASSERT(seen.count(id) == 0); seen.insert(id); father = m_upLink.at(id); if (father != -1) { id = father; } } while (father != -1); return id; } bool GroupsModel::isLeaf(int id) const { READ_LOCK(); Q_ASSERT(m_downLink.count(id) > 0); return m_downLink.at(id).empty(); } bool GroupsModel::isInGroup(int id) const { READ_LOCK(); Q_ASSERT(m_downLink.count(id) > 0); return getRootId(id) != id; } int GroupsModel::getSplitPartner(int id) const { READ_LOCK(); Q_ASSERT(m_downLink.count(id) > 0); int groupId = m_upLink.at(id); if (groupId == -1 || getType(groupId) != GroupType::AVSplit) { // clip does not have an AV split partner return -1; } std::unordered_set leaves = getDirectChildren(groupId); if (leaves.size() != 2) { // clip does not have an AV split partner qDebug() << "WRONG SPLIT GROUP SIZE: " << leaves.size(); return -1; } for (const int &child : leaves) { if (child != id) { return child; } } return -1; } std::unordered_set GroupsModel::getSubtree(int id) const { READ_LOCK(); std::unordered_set result; result.insert(id); std::queue queue; queue.push(id); while (!queue.empty()) { int current = queue.front(); queue.pop(); for (const int &child : m_downLink.at(current)) { result.insert(child); queue.push(child); } } return result; } std::unordered_set GroupsModel::getLeaves(int id) const { READ_LOCK(); std::unordered_set result; std::queue queue; queue.push(id); while (!queue.empty()) { int current = queue.front(); queue.pop(); for (const int &child : m_downLink.at(current)) { queue.push(child); } if (m_downLink.at(current).empty()) { result.insert(current); } } return result; } std::unordered_set GroupsModel::getDirectChildren(int id) const { READ_LOCK(); Q_ASSERT(m_downLink.count(id) > 0); return m_downLink.at(id); } int GroupsModel::getDirectAncestor(int id) const { READ_LOCK(); Q_ASSERT(m_upLink.count(id) > 0); return m_upLink.at(id); } void GroupsModel::setGroup(int id, int groupId, bool changeState) { QWriteLocker locker(&m_lock); Q_ASSERT(m_upLink.count(id) > 0); Q_ASSERT(groupId == -1 || m_downLink.count(groupId) > 0); Q_ASSERT(id != groupId); removeFromGroup(id); m_upLink[id] = groupId; if (groupId != -1) { m_downLink[groupId].insert(id); auto ptr = m_parent.lock(); if (changeState && ptr) { QModelIndex ix; if (ptr->isClip(id)) { ix = ptr->makeClipIndexFromID(id); } else if (ptr->isComposition(id)) { ix = ptr->makeCompositionIndexFromID(id); } if (ix.isValid()) { ptr->dataChanged(ix, ix, {TimelineModel::GroupedRole}); } } if (getType(groupId) == GroupType::Leaf) { promoteToGroup(groupId, GroupType::Normal); } } } void GroupsModel::removeFromGroup(int id) { QWriteLocker locker(&m_lock); Q_ASSERT(m_upLink.count(id) > 0); Q_ASSERT(m_downLink.count(id) > 0); int parent = m_upLink[id]; if (parent != -1) { Q_ASSERT(getType(parent) != GroupType::Leaf); m_downLink[parent].erase(id); QModelIndex ix; auto ptr = m_parent.lock(); if (!ptr) Q_ASSERT(false); if (ptr->isClip(id)) { ix = ptr->makeClipIndexFromID(id); } else if (ptr->isComposition(id)) { ix = ptr->makeCompositionIndexFromID(id); } if (ix.isValid()) { ptr->dataChanged(ix, ix, {TimelineModel::GroupedRole}); } if (m_downLink[parent].size() == 0) { downgradeToLeaf(parent); } } m_upLink[id] = -1; } bool GroupsModel::mergeSingleGroups(int id, Fun &undo, Fun &redo) { // The idea is as follow: we start from the leaves, and go up to the root. // In the process, if we find a node with only one children, we flag it for deletion QWriteLocker locker(&m_lock); Q_ASSERT(m_upLink.count(id) > 0); auto leaves = getLeaves(id); std::unordered_map old_parents, new_parents; std::vector to_delete; std::unordered_set processed; // to avoid going twice along the same branch for (int leaf : leaves) { int current = m_upLink[leaf]; int start = leaf; while (current != m_upLink[id] && processed.count(current) == 0) { processed.insert(current); if (m_downLink[current].size() == 1) { to_delete.push_back(current); } else { if (current != m_upLink[start]) { old_parents[start] = m_upLink[start]; new_parents[start] = current; } start = current; } current = m_upLink[current]; } if (current != m_upLink[start]) { old_parents[start] = m_upLink[start]; new_parents[start] = current; } } auto parent_changer = [this](const std::unordered_map &parents) { auto ptr = m_parent.lock(); if (!ptr) { qDebug() << "Impossible to create group because the timeline is not available anymore"; return false; } for (const auto &group : parents) { setGroup(group.first, group.second); } return true; }; Fun reverse = [old_parents, parent_changer]() { return parent_changer(old_parents); }; Fun operation = [new_parents, parent_changer]() { return parent_changer(new_parents); }; bool res = operation(); if (!res) { bool undone = reverse(); Q_ASSERT(undone); return res; } UPDATE_UNDO_REDO(operation, reverse, undo, redo); for (int gid : to_delete) { Q_ASSERT(m_downLink[gid].size() == 0); if (getType(gid) == GroupType::Selection) { continue; } res = destructGroupItem(gid, false, undo, redo); if (!res) { bool undone = undo(); Q_ASSERT(undone); return res; } } return true; } bool GroupsModel::split(int id, const std::function &criterion, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); if (isLeaf(id)) { return true; } // This function is valid only for roots (otherwise it is not clear what should be the new parent of the created tree) Q_ASSERT(m_upLink[id] == -1); Q_ASSERT(m_groupIds[id] != GroupType::Selection); bool regroup = true; // we don't support splitting if selection group is active // We do a BFS on the tree to copy it // We store corresponding nodes std::unordered_map corresp; // keys are id in the original tree, values are temporary negative id assigned for creation of the new tree corresp[-1] = -1; // These are the nodes to be moved to new tree std::vector to_move; // We store the groups (ie the nodes) that are going to be part of the new tree // Keys are temporary id (negative) and values are the set of children (true ids in the case of leaves and temporary ids for other nodes) std::unordered_map> new_groups; // We store also the target type of the new groups std::unordered_map new_types; std::queue queue; queue.push(id); int tempId = -10; while (!queue.empty()) { int current = queue.front(); queue.pop(); if (!isLeaf(current) || criterion(current)) { if (isLeaf(current)) { to_move.push_back(current); new_groups[corresp[m_upLink[current]]].insert(current); } else { corresp[current] = tempId; new_types[tempId] = getType(current); if (m_upLink[current] != -1) new_groups[corresp[m_upLink[current]]].insert(tempId); tempId--; } } for (const int &child : m_downLink.at(current)) { queue.push(child); } } // First, we simulate deletion of elements that we have to remove from the original tree // A side effect of this is that empty groups will be removed for (const auto &leaf : to_move) { destructGroupItem(leaf, true, undo, redo); } // we artificially recreate the leaves Fun operation = [this, to_move]() { for (const auto &leaf : to_move) { createGroupItem(leaf); } return true; }; Fun reverse = [this, to_move]() { for (const auto &group : to_move) { destructGroupItem(group); } return true; }; bool res = operation(); if (!res) { return false; } UPDATE_UNDO_REDO(operation, reverse, undo, redo); // We prune the new_groups to remove empty ones bool finished = false; while (!finished) { finished = true; int selected = INT_MAX; for (const auto &it : new_groups) { if (it.second.size() == 0) { // empty group finished = false; selected = it.first; break; } for (int it2 : it.second) { if (it2 < -1 && new_groups.count(it2) == 0) { // group that has no reference, it is empty too finished = false; selected = it2; break; } } if (!finished) break; } if (!finished) { new_groups.erase(selected); for (auto it = new_groups.begin(); it != new_groups.end(); ++it) { (*it).second.erase(selected); } } } // We now regroup the items of the new tree to recreate hierarchy. // This is equivalent to creating the tree bottom up (starting from the leaves) // At each iteration, we create a new node by grouping together elements that are either leaves or already created nodes. std::unordered_map created_id; // to keep track of node that we create while (!new_groups.empty()) { int selected = INT_MAX; for (const auto &group : new_groups) { // we check that all children are already created bool ok = true; for (int elem : group.second) { if (elem < -1 && created_id.count(elem) == 0) { ok = false; break; } } if (ok) { selected = group.first; break; } } Q_ASSERT(selected != INT_MAX); std::unordered_set group; for (int elem : new_groups[selected]) { group.insert(elem < -1 ? created_id[elem] : elem); } Q_ASSERT(new_types.count(selected) != 0); int gid = groupItems(group, undo, redo, new_types[selected], true); created_id[selected] = gid; new_groups.erase(selected); } if (regroup) { if (m_groupIds.count(id) > 0) { mergeSingleGroups(id, undo, redo); } if (created_id[corresp[id]]) { mergeSingleGroups(created_id[corresp[id]], undo, redo); } } return res; } void GroupsModel::setInGroupOf(int id, int targetId, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); Q_ASSERT(m_upLink.count(targetId) > 0); Fun operation = [this, id, group = m_upLink[targetId]]() { setGroup(id, group); return true; }; Fun reverse = [this, id, group = m_upLink[id]]() { setGroup(id, group); return true; }; operation(); UPDATE_UNDO_REDO(operation, reverse, undo, redo); } bool GroupsModel::createGroupAtSameLevel(int id, std::unordered_set to_add, GroupType type, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); Q_ASSERT(m_upLink.count(id) > 0); Q_ASSERT(isLeaf(id)); if (to_add.size() == 0) { return true; } int gid = TimelineModel::getNextId(); std::unordered_map old_parents; to_add.insert(id); for (int g : to_add) { Q_ASSERT(m_upLink.count(g) > 0); old_parents[g] = m_upLink[g]; } Fun operation = [this, gid, type, to_add, parent = m_upLink.at(id)]() { createGroupItem(gid); setGroup(gid, parent); for (const auto &g : to_add) { setGroup(g, gid); } setType(gid, type); return true; }; Fun reverse = [this, old_parents, gid]() { for (const auto &g : old_parents) { setGroup(g.first, g.second); } setGroup(gid, -1); destructGroupItem_lambda(gid)(); return true; }; bool success = operation(); if (success) { UPDATE_UNDO_REDO(operation, reverse, undo, redo); } return success; } bool GroupsModel::processCopy(int gid, std::unordered_map &mapping, Fun &undo, Fun &redo) { qDebug() << "processCopy" << gid; if (isLeaf(gid)) { qDebug() << "it is a leaf"; return true; } bool ok = true; std::unordered_set targetGroup; for (int child : m_downLink.at(gid)) { ok = ok && processCopy(child, mapping, undo, redo); if (!ok) { break; } targetGroup.insert(mapping.at(child)); } qDebug() << "processCopy" << gid << "success of child" << ok; if (ok && m_groupIds[gid] != GroupType::Selection) { int id = groupItems(targetGroup, undo, redo); qDebug() << "processCopy" << gid << "created id" << id; if (id != -1) { mapping[gid] = id; return true; } } return ok; } bool GroupsModel::copyGroups(std::unordered_map &mapping, Fun &undo, Fun &redo) { Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; // destruct old groups for the targets items for (const auto &corresp : mapping) { ungroupItem(corresp.second, local_undo, local_redo); } std::unordered_set roots; std::transform(mapping.begin(), mapping.end(), std::inserter(roots, roots.begin()), [&](decltype(*mapping.begin()) corresp) { return getRootId(corresp.first); }); bool res = true; qDebug() << "found" << roots.size() << "roots"; for (int r : roots) { qDebug() << "processing copy for root " << r; res = res && processCopy(r, mapping, local_undo, local_redo); if (!res) { bool undone = local_undo(); Q_ASSERT(undone); return false; } } UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } GroupType GroupsModel::getType(int id) const { if (m_groupIds.count(id) > 0) { return m_groupIds.at(id); } return GroupType::Leaf; } QJsonObject GroupsModel::toJson(int gid) const { QJsonObject currentGroup; currentGroup.insert(QLatin1String("type"), QJsonValue(groupTypeToStr(getType(gid)))); if (m_groupIds.count(gid) > 0) { // in that case, we have a proper group QJsonArray array; Q_ASSERT(m_downLink.count(gid) > 0); for (int c : m_downLink.at(gid)) { array.push_back(toJson(c)); } currentGroup.insert(QLatin1String("children"), array); } else { // in that case we have a clip or composition if (auto ptr = m_parent.lock()) { Q_ASSERT(ptr->isClip(gid) || ptr->isComposition(gid)); currentGroup.insert(QLatin1String("leaf"), QJsonValue(QLatin1String(ptr->isClip(gid) ? "clip" : "composition"))); int track = ptr->getTrackPosition(ptr->getItemTrackId(gid)); int pos = ptr->getItemPosition(gid); currentGroup.insert(QLatin1String("data"), QJsonValue(QString("%1:%2").arg(track).arg(pos))); } else { qDebug() << "Impossible to create group because the timeline is not available anymore"; Q_ASSERT(false); } } return currentGroup; } const QString GroupsModel::toJson() const { std::unordered_set roots; std::transform(m_groupIds.begin(), m_groupIds.end(), std::inserter(roots, roots.begin()), [&](decltype(*m_groupIds.begin()) g) { return getRootId(g.first); }); QJsonArray list; for (int r : roots) { if (getType(r) != GroupType::Selection) list.push_back(toJson(r)); } QJsonDocument json(list); return QString(json.toJson()); } const QString GroupsModel::toJson(std::unordered_set roots) const { QJsonArray list; for (int r : roots) { if (getType(r) != GroupType::Selection) list.push_back(toJson(r)); } QJsonDocument json(list); return QString(json.toJson()); } int GroupsModel::fromJson(const QJsonObject &o, Fun &undo, Fun &redo) { if (!o.contains(QLatin1String("type"))) { qDebug() << "CANNOT PARSE GROUP DATA"; return -1; } auto type = groupTypeFromStr(o.value(QLatin1String("type")).toString()); if (type == GroupType::Leaf) { if (auto ptr = m_parent.lock()) { if (!o.contains(QLatin1String("data")) || !o.contains(QLatin1String("leaf"))) { qDebug() << "Error: missing info in the group structure while parsing json"; return -1; } QString data = o.value(QLatin1String("data")).toString(); QString leaf = o.value(QLatin1String("leaf")).toString(); int trackId = ptr->getTrackIndexFromPosition(data.section(":", 0, 0).toInt()); int pos = data.section(":", 1, 1).toInt(); int id = -1; if (leaf == QLatin1String("clip")) { id = ptr->getClipByPosition(trackId, pos); } else if (leaf == QLatin1String("composition")) { id = ptr->getCompositionByPosition(trackId, pos); } else { qDebug() << " * * *UNKNOWN ITEM: " << leaf; } return id; } else { qDebug() << "Impossible to create group because the timeline is not available anymore"; Q_ASSERT(false); } } else { if (!o.contains(QLatin1String("children"))) { qDebug() << "Error: missing info in the group structure while parsing json"; return -1; } auto value = o.value(QLatin1String("children")); if (!value.isArray()) { qDebug() << "Error : Expected json array of children while parsing groups"; return -1; } const auto children = value.toArray(); std::unordered_set ids; for (const auto &c : children) { if (!c.isObject()) { qDebug() << "Error : Expected json object while parsing groups"; return -1; } ids.insert(fromJson(c.toObject(), undo, redo)); } if (ids.count(-1) > 0) { return -1; } return groupItems(ids, undo, redo, type); } return -1; } bool GroupsModel::fromJson(const QString &data) { Fun undo = []() { return true; }; Fun redo = []() { return true; }; auto json = QJsonDocument::fromJson(data.toUtf8()); if (!json.isArray()) { qDebug() << "Error : Json file should be an array"; return false; } const auto list = json.array(); bool ok = true; for (const auto &elem : list) { if (!elem.isObject()) { qDebug() << "Error : Expected json object while parsing groups"; undo(); return false; } ok = ok && fromJson(elem.toObject(), undo, redo); } return ok; } -bool GroupsModel::fromJsonWithOffset(const QString &data, QMap trackMap, int offset, Fun &undo, Fun &redo) +bool GroupsModel::fromJsonWithOffset(const QString &data, const QMap &trackMap, int offset, Fun &undo, Fun &redo) { Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; auto json = QJsonDocument::fromJson(data.toUtf8()); if (!json.isArray()) { qDebug() << "Error : Json file should be an array"; return false; } auto list = json.array(); bool ok = true; for (auto elem : list) { if (!elem.isObject()) { qDebug() << "Error : Expected json object while parsing groups"; local_undo(); return false; } QJsonObject obj = elem.toObject(); auto value = obj.value(QLatin1String("children")); if (!value.isArray()) { qDebug() << "Error : Expected json array of children while parsing groups"; continue; } QJsonArray updatedNodes; auto children = value.toArray(); std::unordered_set ids; for (auto c : children) { if (!c.isObject()) { continue; } QJsonObject child = c.toObject(); if (child.contains(QLatin1String("data"))) { if (auto ptr = m_parent.lock()) { QString cur_data = child.value(QLatin1String("data")).toString(); int trackId = ptr->getTrackIndexFromPosition(cur_data.section(":", 0, 0).toInt()); int pos = cur_data.section(":", 1, 1).toInt(); int trackPos = ptr->getTrackPosition(trackMap.value(trackId)); pos += offset; child.insert(QLatin1String("data"), QJsonValue(QString("%1:%2").arg(trackPos).arg(pos))); } updatedNodes.append(QJsonValue(child)); } } qDebug() << "* ** * UPDATED JSON NODES: " << updatedNodes; obj.insert(QLatin1String("children"), QJsonValue(updatedNodes)); qDebug() << "* ** * UPDATED JSON NODES: " << obj; ok = (fromJson(obj, local_undo, local_redo) > 0); if (ok) { UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); } } return ok; } void GroupsModel::setType(int gid, GroupType type) { Q_ASSERT(m_groupIds.count(gid) != 0); if (type == GroupType::Leaf) { Q_ASSERT(m_downLink[gid].size() == 0); if (m_groupIds.count(gid) > 0) { m_groupIds.erase(gid); } } else { m_groupIds[gid] = type; } } bool GroupsModel::checkConsistency(bool failOnSingleGroups, bool checkTimelineConsistency) { // check that all element with up link have a down link for (const auto &elem : m_upLink) { if (m_downLink.count(elem.first) == 0) { qDebug() << "ERROR: Group model has missing up/down links"; return false; } } // check that all element with down link have a up link for (const auto &elem : m_downLink) { if (m_upLink.count(elem.first) == 0) { qDebug() << "ERROR: Group model has missing up/down links"; return false; } } int selectionCount = 0; for (const auto &elem : m_upLink) { // iterate through children to check links for (const auto &child : m_downLink[elem.first]) { if (m_upLink[child] != elem.first) { qDebug() << "ERROR: Group model has inconsistent up/down links"; return false; } } bool isLeaf = m_downLink[elem.first].empty(); if (isLeaf) { if (m_groupIds.count(elem.first) > 0) { qDebug() << "ERROR: Group model has wrong tracking of non-leaf groups"; return false; } } else { if (m_groupIds.count(elem.first) == 0) { qDebug() << "ERROR: Group model has wrong tracking of non-leaf groups"; return false; } if (m_downLink[elem.first].size() == 1 && failOnSingleGroups) { qDebug() << "ERROR: Group model contains groups with single element"; return false; } if (getType(elem.first) == GroupType::Selection) { selectionCount++; } if (elem.second != -1 && getType(elem.first) == GroupType::Selection) { qDebug() << "ERROR: Group model contains inner groups of selection type"; return false; } if (getType(elem.first) == GroupType::Leaf) { qDebug() << "ERROR: Group model contains groups of Leaf type"; return false; } } } if (selectionCount > 1) { qDebug() << "ERROR: Found too many selections: " << selectionCount; return false; } // Finally, we do a depth first visit of the tree to check for loops std::unordered_set visited; for (const auto &elem : m_upLink) { if (elem.second == -1) { // this is a root, traverse the tree from here std::stack stack; stack.push(elem.first); while (!stack.empty()) { int cur = stack.top(); stack.pop(); if (visited.count(cur) > 0) { qDebug() << "ERROR: Group model contains a cycle"; return false; } visited.insert(cur); for (int child : m_downLink[cur]) { stack.push(child); } } } } // Do a last pass to check everybody was visited for (const auto &elem : m_upLink) { if (visited.count(elem.first) == 0) { qDebug() << "ERROR: Group model contains unreachable elements"; return false; } } if (checkTimelineConsistency) { if (auto ptr = m_parent.lock()) { auto isTimelineObject = [&](int cid) { return ptr->isClip(cid) || ptr->isComposition(cid); }; for (int g : ptr->m_allGroups) { if (m_upLink.count(g) == 0 || getType(g) == GroupType::Leaf) { qDebug() << "ERROR: Timeline contains inconsistent group data"; return false; } } for (const auto &elem : m_upLink) { if (getType(elem.first) == GroupType::Leaf) { if (!isTimelineObject(elem.first)) { qDebug() << "ERROR: Group model contains leaf element that is not a clip nor a composition"; return false; } } else { if (ptr->m_allGroups.count(elem.first) == 0) { qDebug() << "ERROR: Group model contains group element that is not registered on timeline"; Q_ASSERT(false); return false; } if (getType(elem.first) == GroupType::AVSplit) { if (m_downLink[elem.first].size() != 2) { qDebug() << "ERROR: Group model contains a AVSplit group with a children count != 2"; return false; } auto it = m_downLink[elem.first].begin(); int cid1 = (*it); ++it; int cid2 = (*it); if (!isTimelineObject(cid1) || !isTimelineObject(cid2)) { qDebug() << "ERROR: Group model contains an AVSplit group with invalid members"; return false; } int tid1 = ptr->getClipTrackId(cid1); bool isAudio1 = ptr->getTrackById(tid1)->isAudioTrack(); int tid2 = ptr->getClipTrackId(cid2); bool isAudio2 = ptr->getTrackById(tid2)->isAudioTrack(); if (isAudio1 == isAudio2) { qDebug() << "ERROR: Group model contains an AVSplit formed with members that are both on an audio track or on a video track"; return false; } } } } } } return true; } diff --git a/src/timeline2/model/groupsmodel.hpp b/src/timeline2/model/groupsmodel.hpp index 400ab6f32..9f09ddd95 100644 --- a/src/timeline2/model/groupsmodel.hpp +++ b/src/timeline2/model/groupsmodel.hpp @@ -1,233 +1,233 @@ /*************************************************************************** * Copyright (C) 2017 by Nicolas Carion * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #ifndef GROUPMODEL_H #define GROUPMODEL_H #include "definitions.h" #include "undohelper.hpp" #include #include #include #include class TimelineItemModel; /* @brief This class represents the group hierarchy. This is basically a tree structure In this class, we consider that a groupItem is either a clip or a group */ class GroupsModel { public: GroupsModel() = delete; GroupsModel(std::weak_ptr parent); /* @brief Create a group that contains all the given items and returns the id of the created group. Note that if an item is already part of a group, its topmost group will be considered instead and added in the newly created group. If only one id is provided, no group is created, unless force = true. @param ids set containing the items to group. @param undo Lambda function containing the current undo stack. Will be updated with current operation @param redo Lambda function containing the current redo queue. Will be updated with current operation @param type indicates the type of group we create Returns the id of the new group, or -1 on error. */ int groupItems(const std::unordered_set &ids, Fun &undo, Fun &redo, GroupType type = GroupType::Normal, bool force = false); protected: /* Lambda version */ Fun groupItems_lambda(int gid, const std::unordered_set &ids, GroupType type = GroupType::Normal, int parent = -1); public: /* Deletes the topmost group containing given element Note that if the element is not in a group, then it will not be touched. Return true on success @param id id of the groupitem @param undo Lambda function containing the current undo stack. Will be updated with current operation @param redo Lambda function containing the current redo queue. Will be updated with current operation */ bool ungroupItem(int id, Fun &undo, Fun &redo); /* @brief Create a groupItem in the hierarchy. Initially it is not part of a group @param id id of the groupItem */ void createGroupItem(int id); /* @brief Destruct a group item Note that this public function expects that the given id is an orphan element. @param id id of the groupItem */ bool destructGroupItem(int id); /* @brief Merges group with only one child to parent Ex: . . / \ / \ . . becomes a b / \ a b @param id id of the tree to consider */ bool mergeSingleGroups(int id, Fun &undo, Fun &redo); /* @brief Split the group tree according to a given criterion All the leaves satisfying the criterion are moved to the new tree, the other stay Both tree are subsequently simplified to avoid weird structure. @param id is the root of the tree */ bool split(int id, const std::function &criterion, Fun &undo, Fun &redo); /* @brief Copy a group hierarchy. @param mapping describes the correspondence between the ids of the items in the source group hierarchy, and their counterpart in the hierarchy that we create. It will also be used as a return parameter, by adding the mapping between the groups of the hierarchy Note that if the target items should not belong to a group. */ bool copyGroups(std::unordered_map &mapping, Fun &undo, Fun &redo); /* @brief Get the overall father of a given groupItem If the element has no father, it is returned as is. @param id id of the groupitem */ int getRootId(int id) const; /* @brief Returns true if the groupItem has no descendant @param id of the groupItem */ bool isLeaf(int id) const; /* @brief Returns true if the element is in a non-trivial group @param id of the groupItem */ bool isInGroup(int id) const; /* @brief Move element id in the same group as targetId */ void setInGroupOf(int id, int targetId, Fun &undo, Fun &redo); /* @brief We replace the leaf node given by id with a group that contains the leaf plus all the clips in to_add. * The created group type is given in parameter * Returns true on success */ bool createGroupAtSameLevel(int id, std::unordered_set to_add, GroupType type, Fun &undo, Fun &redo); /* @brief Returns the id of all the descendant of given item (including item) @param id of the groupItem */ std::unordered_set getSubtree(int id) const; /* @brief Returns the id of all the leaves in the subtree of the given item This should correspond to the ids of the clips, since they should be the only items with no descendants @param id of the groupItem */ std::unordered_set getLeaves(int id) const; /* @brief Gets direct children of a given group item @param id of the groupItem */ std::unordered_set getDirectChildren(int id) const; /* @brief Gets direct ancestor of a given group item. Returns -1 if not in a group @param id of the groupItem */ int getDirectAncestor(int id) const; /* @brief Get the type of the group @param id of the groupItem. Must be a proper group, not a leaf */ GroupType getType(int id) const; /* @brief Convert the group hierarchy to json. Note that we cannot expect clipId nor groupId to be the same on project reopening, thus we cannot rely on them for saving. To workaround that, we currently identify clips by their position + track */ const QString toJson() const; const QString toJson(std::unordered_set roots) const; bool fromJson(const QString &data); - bool fromJsonWithOffset(const QString &data, QMap trackMap, int offset, Fun &undo, Fun &redo); + bool fromJsonWithOffset(const QString &data, const QMap &trackMap, int offset, Fun &undo, Fun &redo); /* @brief if the clip belongs to a AVSplit group, then return the id of the other corresponding clip. Otherwise, returns -1 */ int getSplitPartner(int id) const; /* @brief Check the internal consistency of the model. Returns false if something is wrong @param failOnSingleGroups: if true, we make sure that a non-leaf node has at least two children @param checkTimelineConsistency: if true, we make sure that the group data of the parent timeline are consistent */ bool checkConsistency(bool failOnSingleGroups = true, bool checkTimelineConsistency = false); protected: /* @brief Destruct a groupItem in the hierarchy. All its children will become their own roots Return true on success @param id id of the groupitem @param deleteOrphan If this parameter is true, we recursively delete any group that become empty following the destruction @param undo Lambda function containing the current undo stack. Will be updated with current operation @param redo Lambda function containing the current redo queue. Will be updated with current operation */ bool destructGroupItem(int id, bool deleteOrphan, Fun &undo, Fun &redo); /* Lambda version */ Fun destructGroupItem_lambda(int id); /* @brief change the group of a given item @param id of the groupItem @param groupId id of the group to assign it to @param changeState when false, the grouped role for item won't be updated (for selection) */ void setGroup(int id, int groupId, bool changeState = true); /* @brief Remove an item from all the groups it belongs to. @param id of the groupItem */ void removeFromGroup(int id); /* @brief This is the actual recursive implementation of the copy function. */ bool processCopy(int gid, std::unordered_map &mapping, Fun &undo, Fun &redo); /* @brief This is the actual recursive implementation of the conversion to json */ QJsonObject toJson(int gid) const; /* @brief This is the actual recursive implementation of the parsing from json Returns the id of the created group */ int fromJson(const QJsonObject &o, Fun &undo, Fun &redo); /* @brief Transform a leaf node into a group node of given type. This implies doing the registration to the timeline */ void promoteToGroup(int gid, GroupType type); /* @brief Transform a group node with no children into a leaf. This implies doing the deregistration to the timeline */ void downgradeToLeaf(int gid); /* @Brief helper function to change the type of a group. @param id of the groupItem @param type: new type of the group */ void setType(int gid, GroupType type); private: std::weak_ptr m_parent; std::unordered_map m_upLink; // edges toward parent std::unordered_map> m_downLink; // edges toward children std::unordered_map m_groupIds; // this keeps track of "real" groups (non-leaf elements), and their types mutable QReadWriteLock m_lock; // This is a lock that ensures safety in case of concurrent access }; #endif diff --git a/src/timeline2/model/moveableItem.ipp b/src/timeline2/model/moveableItem.ipp index a58e25d80..cefb4e4cd 100644 --- a/src/timeline2/model/moveableItem.ipp +++ b/src/timeline2/model/moveableItem.ipp @@ -1,104 +1,106 @@ /*************************************************************************** * 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 "macros.hpp" +#include + template MoveableItem::MoveableItem(std::weak_ptr parent, int id) - : m_parent(parent) + : m_parent(std::move(parent)) , m_id(id == -1 ? TimelineModel::getNextId() : id) , m_position(-1) , m_currentTrackId(-1) , m_grabbed(false) , m_lock(QReadWriteLock::Recursive) { } template int MoveableItem::getId() const { READ_LOCK(); return m_id; } template int MoveableItem::getCurrentTrackId() const { READ_LOCK(); return m_currentTrackId; } template int MoveableItem::getPosition() const { READ_LOCK(); return m_position; } template std::pair MoveableItem::getInOut() const { READ_LOCK(); return {getIn(), getOut()}; } template int MoveableItem::getIn() const { READ_LOCK(); return service()->get_in(); } template int MoveableItem::getOut() const { READ_LOCK(); return service()->get_out(); } template bool MoveableItem::isValid() { READ_LOCK(); return service()->is_valid(); } template void MoveableItem::setPosition(int pos) { QWriteLocker locker(&m_lock); m_position = pos; } template void MoveableItem::setCurrentTrackId(int tid) { QWriteLocker locker(&m_lock); m_currentTrackId = tid; } template void MoveableItem::setInOut(int in, int out) { QWriteLocker locker(&m_lock); service()->set_in_and_out(in, out); } template bool MoveableItem::isGrabbed() const { READ_LOCK(); return m_grabbed; } template void MoveableItem::setGrab(bool grab) { QWriteLocker locker(&m_lock); m_grabbed = grab; } diff --git a/src/timeline2/model/timelinefunctions.cpp b/src/timeline2/model/timelinefunctions.cpp index cf3213e6d..63eaf9ab3 100644 --- a/src/timeline2/model/timelinefunctions.cpp +++ b/src/timeline2/model/timelinefunctions.cpp @@ -1,987 +1,988 @@ /* Copyright (C) 2017 Jean-Baptiste Mardelle This file is part of Kdenlive. See www.kdenlive.org. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "timelinefunctions.hpp" #include "clipmodel.hpp" #include "compositionmodel.hpp" #include "core.h" #include "effects/effectstack/model/effectstackmodel.hpp" #include "groupsmodel.hpp" #include "timelineitemmodel.hpp" #include "trackmodel.hpp" #include "transitions/transitionsrepository.hpp" #include #include #include #include -bool TimelineFunctions::copyClip(std::shared_ptr timeline, int clipId, int &newId, PlaylistState::ClipState state, Fun &undo, Fun &redo) +bool TimelineFunctions::copyClip(const std::shared_ptr &timeline, int clipId, int &newId, PlaylistState::ClipState state, Fun &undo, + Fun &redo) { // Special case: slowmotion clips double clipSpeed = timeline->m_allClips[clipId]->getSpeed(); bool res = timeline->requestClipCreation(timeline->getClipBinId(clipId), newId, state, clipSpeed, undo, redo); timeline->m_allClips[newId]->m_endlessResize = timeline->m_allClips[clipId]->m_endlessResize; // copy useful timeline properties timeline->m_allClips[clipId]->passTimelineProperties(timeline->m_allClips[newId]); int duration = timeline->getClipPlaytime(clipId); int init_duration = timeline->getClipPlaytime(newId); if (duration != init_duration) { int in = timeline->m_allClips[clipId]->getIn(); res = res && timeline->requestItemResize(newId, init_duration - in, false, true, undo, redo); res = res && timeline->requestItemResize(newId, duration, true, true, undo, redo); } if (!res) { return false; } std::shared_ptr sourceStack = timeline->getClipEffectStackModel(clipId); std::shared_ptr destStack = timeline->getClipEffectStackModel(newId); destStack->importEffects(sourceStack, state); return res; } -bool TimelineFunctions::requestMultipleClipsInsertion(std::shared_ptr timeline, const QStringList &binIds, int trackId, int position, +bool TimelineFunctions::requestMultipleClipsInsertion(const std::shared_ptr &timeline, const QStringList &binIds, int trackId, int position, QList &clipIds, bool logUndo, bool refreshView) { std::function undo = []() { return true; }; std::function redo = []() { return true; }; for (const QString &binId : binIds) { int clipId; if (timeline->requestClipInsertion(binId, trackId, position, clipId, logUndo, refreshView, true, undo, redo)) { clipIds.append(clipId); position += timeline->getItemPlaytime(clipId); } else { undo(); clipIds.clear(); return false; } } if (logUndo) { pCore->pushUndo(undo, redo, i18n("Insert Clips")); } return true; } -bool TimelineFunctions::processClipCut(std::shared_ptr timeline, int clipId, int position, int &newId, Fun &undo, Fun &redo) +bool TimelineFunctions::processClipCut(const std::shared_ptr &timeline, int clipId, int position, int &newId, Fun &undo, Fun &redo) { int trackId = timeline->getClipTrackId(clipId); int trackDuration = timeline->getTrackById_const(trackId)->trackDuration(); int start = timeline->getClipPosition(clipId); int duration = timeline->getClipPlaytime(clipId); if (start > position || (start + duration) < position) { return false; } PlaylistState::ClipState state = timeline->m_allClips[clipId]->clipState(); bool res = copyClip(timeline, clipId, newId, state, undo, redo); timeline->m_blockRefresh = true; res = res && timeline->requestItemResize(clipId, position - start, true, true, undo, redo); int newDuration = timeline->getClipPlaytime(clipId); // parse effects std::shared_ptr sourceStack = timeline->getClipEffectStackModel(clipId); sourceStack->cleanFadeEffects(true, undo, redo); std::shared_ptr destStack = timeline->getClipEffectStackModel(newId); destStack->cleanFadeEffects(false, undo, redo); res = res && timeline->requestItemResize(newId, duration - newDuration, false, true, undo, redo); // The next requestclipmove does not check for duration change since we don't invalidate timeline, so check duration change now bool durationChanged = trackDuration != timeline->getTrackById_const(trackId)->trackDuration(); res = res && timeline->requestClipMove(newId, trackId, position, true, false, undo, redo); if (durationChanged) { // Track length changed, check project duration Fun updateDuration = [timeline]() { timeline->updateDuration(); return true; }; updateDuration(); PUSH_LAMBDA(updateDuration, redo); } timeline->m_blockRefresh = false; return res; } bool TimelineFunctions::requestClipCut(std::shared_ptr timeline, int clipId, int position) { std::function undo = []() { return true; }; std::function redo = []() { return true; }; - bool result = TimelineFunctions::requestClipCut(timeline, clipId, position, undo, redo); + bool result = TimelineFunctions::requestClipCut(std::move(timeline), clipId, position, undo, redo); if (result) { pCore->pushUndo(undo, redo, i18n("Cut clip")); } return result; } -bool TimelineFunctions::requestClipCut(std::shared_ptr timeline, int clipId, int position, Fun &undo, Fun &redo) +bool TimelineFunctions::requestClipCut(const std::shared_ptr &timeline, int clipId, int position, Fun &undo, Fun &redo) { const std::unordered_set clips = timeline->getGroupElements(clipId); int root = timeline->m_groups->getRootId(clipId); std::unordered_set topElements; if (timeline->m_temporarySelectionGroup == root) { topElements = timeline->m_groups->getDirectChildren(root); } else { topElements.insert(root); } // We need to call clearSelection before attempting the split or the group split will be corrupted by the selection group (no undo support) bool processClearSelection = false; int count = 0; QList newIds; int mainId = -1; QList clipsToCut; for (int cid : clips) { int start = timeline->getClipPosition(cid); int duration = timeline->getClipPlaytime(cid); if (start < position && (start + duration) > position) { clipsToCut << cid; if (!processClearSelection && pCore->isSelected(cid)) { processClearSelection = true; } } } if (processClearSelection) { pCore->clearSelection(); } for (int cid : clipsToCut) { count++; int newId; bool res = processClipCut(timeline, cid, position, newId, undo, redo); if (!res) { bool undone = undo(); Q_ASSERT(undone); return false; } else if (cid == clipId) { mainId = newId; } // splitted elements go temporarily in the same group as original ones. timeline->m_groups->setInGroupOf(newId, cid, undo, redo); newIds << newId; } if (count > 0 && timeline->m_groups->isInGroup(clipId)) { // we now split the group hierarchy. // As a splitting criterion, we compare start point with split position auto criterion = [timeline, position](int cid) { return timeline->getClipPosition(cid) < position; }; bool res = true; for (const int topId : topElements) { res = res & timeline->m_groups->split(topId, criterion, undo, redo); } if (!res) { bool undone = undo(); Q_ASSERT(undone); return false; } } if (processClearSelection) { if (mainId >= 0) { pCore->selectItem(mainId); } else if (!newIds.isEmpty()) { pCore->selectItem(newIds.first()); } } return count > 0; } -int TimelineFunctions::requestSpacerStartOperation(std::shared_ptr timeline, int trackId, int position) +int TimelineFunctions::requestSpacerStartOperation(const std::shared_ptr &timeline, int trackId, int position) { std::unordered_set clips = timeline->getItemsInRange(trackId, position, -1); if (clips.size() > 0) { timeline->requestClipsGroup(clips, false, GroupType::Selection); return (*clips.cbegin()); } return -1; } -bool TimelineFunctions::requestSpacerEndOperation(std::shared_ptr timeline, int itemId, int startPosition, int endPosition) +bool TimelineFunctions::requestSpacerEndOperation(const std::shared_ptr &timeline, int itemId, int startPosition, int endPosition) { // Move group back to original position int track = timeline->getItemTrackId(itemId); bool isClip = timeline->isClip(itemId); if (isClip) { timeline->requestClipMove(itemId, track, startPosition, false, false); } else { timeline->requestCompositionMove(itemId, track, startPosition, false, false); } std::unordered_set clips = timeline->getGroupElements(itemId); // break group pCore->clearSelection(); // Start undoable command std::function undo = []() { return true; }; std::function redo = []() { return true; }; int res = timeline->requestClipsGroup(clips, undo, redo); bool final = false; if (res > -1) { if (clips.size() > 1) { final = timeline->requestGroupMove(itemId, res, 0, endPosition - startPosition, true, true, undo, redo); } else { // only 1 clip to be moved if (isClip) { final = timeline->requestClipMove(itemId, track, endPosition, true, true, undo, redo); } else { final = timeline->requestCompositionMove(itemId, track, -1, endPosition, true, true, undo, redo); } } } if (final && clips.size() > 1) { final = timeline->requestClipUngroup(itemId, undo, redo); } if (final) { pCore->pushUndo(undo, redo, i18n("Insert space")); return true; } return false; } -bool TimelineFunctions::extractZone(std::shared_ptr timeline, QVector tracks, QPoint zone, bool liftOnly) +bool TimelineFunctions::extractZone(const std::shared_ptr &timeline, QVector tracks, QPoint zone, bool liftOnly) { // Start undoable command std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool result = true; for (int trackId : tracks) { result = result && TimelineFunctions::liftZone(timeline, trackId, zone, undo, redo); } if (result && !liftOnly) { result = TimelineFunctions::removeSpace(timeline, -1, zone, undo, redo); } pCore->pushUndo(undo, redo, liftOnly ? i18n("Lift zone") : i18n("Extract zone")); return result; } -bool TimelineFunctions::insertZone(std::shared_ptr timeline, QList trackIds, const QString &binId, int insertFrame, QPoint zone, +bool TimelineFunctions::insertZone(const std::shared_ptr &timeline, QList trackIds, const QString &binId, int insertFrame, QPoint zone, bool overwrite) { // Start undoable command std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool result = false; int trackId = trackIds.takeFirst(); if (overwrite) { result = TimelineFunctions::liftZone(timeline, trackId, QPoint(insertFrame, insertFrame + (zone.y() - zone.x())), undo, redo); if (!trackIds.isEmpty()) { result = result && TimelineFunctions::liftZone(timeline, trackIds.takeFirst(), QPoint(insertFrame, insertFrame + (zone.y() - zone.x())), undo, redo); } } else { // Cut all tracks auto it = timeline->m_allTracks.cbegin(); while (it != timeline->m_allTracks.cend()) { int target_track = (*it)->getId(); if (timeline->getTrackById_const(target_track)->isLocked()) { ++it; continue; } int startClipId = timeline->getClipByPosition(target_track, insertFrame); if (startClipId > -1) { // There is a clip, cut it TimelineFunctions::requestClipCut(timeline, startClipId, insertFrame, undo, redo); } ++it; } result = TimelineFunctions::insertSpace(timeline, trackId, QPoint(insertFrame, insertFrame + (zone.y() - zone.x())), undo, redo); } if (result) { int newId = -1; QString binClipId = QString("%1/%2/%3").arg(binId).arg(zone.x()).arg(zone.y() - 1); result = timeline->requestClipInsertion(binClipId, trackId, insertFrame, newId, true, true, true, undo, redo); if (result) { pCore->pushUndo(undo, redo, overwrite ? i18n("Overwrite zone") : i18n("Insert zone")); } } if (!result) { undo(); } return result; } -bool TimelineFunctions::liftZone(std::shared_ptr timeline, int trackId, QPoint zone, Fun &undo, Fun &redo) +bool TimelineFunctions::liftZone(const std::shared_ptr &timeline, int trackId, QPoint zone, Fun &undo, Fun &redo) { // Check if there is a clip at start point int startClipId = timeline->getClipByPosition(trackId, zone.x()); if (startClipId > -1) { // There is a clip, cut it if (timeline->getClipPosition(startClipId) < zone.x()) { qDebug() << "/// CUTTING AT START: " << zone.x() << ", ID: " << startClipId; TimelineFunctions::requestClipCut(timeline, startClipId, zone.x(), undo, redo); qDebug() << "/// CUTTING AT START DONE"; } } int endClipId = timeline->getClipByPosition(trackId, zone.y()); if (endClipId > -1) { // There is a clip, cut it if (timeline->getClipPosition(endClipId) + timeline->getClipPlaytime(endClipId) > zone.y()) { qDebug() << "/// CUTTING AT END: " << zone.y() << ", ID: " << endClipId; TimelineFunctions::requestClipCut(timeline, endClipId, zone.y(), undo, redo); qDebug() << "/// CUTTING AT END DONE"; } } std::unordered_set clips = timeline->getItemsInRange(trackId, zone.x(), zone.y()); for (const auto &clipId : clips) { timeline->requestItemDeletion(clipId, undo, redo); } return true; } -bool TimelineFunctions::removeSpace(std::shared_ptr timeline, int trackId, QPoint zone, Fun &undo, Fun &redo) +bool TimelineFunctions::removeSpace(const std::shared_ptr &timeline, int trackId, QPoint zone, Fun &undo, Fun &redo) { Q_UNUSED(trackId) std::unordered_set clips = timeline->getItemsInRange(-1, zone.y() - 1, -1, true); bool result = false; if (clips.size() > 0) { int clipId = *clips.begin(); if (clips.size() > 1) { int res = timeline->requestClipsGroup(clips, undo, redo); if (res > -1) { result = timeline->requestGroupMove(clipId, res, 0, zone.x() - zone.y(), true, true, undo, redo); if (result) { result = timeline->requestClipUngroup(clipId, undo, redo); } if (!result) { undo(); } } } else { // only 1 clip to be moved int clipStart = timeline->getItemPosition(clipId); result = timeline->requestClipMove(clipId, timeline->getItemTrackId(clipId), clipStart - (zone.y() - zone.x()), true, true, undo, redo); } } return result; } -bool TimelineFunctions::insertSpace(std::shared_ptr timeline, int trackId, QPoint zone, Fun &undo, Fun &redo) +bool TimelineFunctions::insertSpace(const std::shared_ptr &timeline, int trackId, QPoint zone, Fun &undo, Fun &redo) { Q_UNUSED(trackId) std::unordered_set clips = timeline->getItemsInRange(-1, zone.x(), -1, true); bool result = true; if (clips.size() > 0) { int clipId = *clips.begin(); if (clips.size() > 1) { int res = timeline->requestClipsGroup(clips, undo, redo); if (res > -1) { result = timeline->requestGroupMove(clipId, res, 0, zone.y() - zone.x(), true, true, undo, redo); if (result) { result = timeline->requestClipUngroup(clipId, undo, redo); } else { pCore->displayMessage(i18n("Cannot move selected group"), ErrorMessage); } } } else { // only 1 clip to be moved int clipStart = timeline->getItemPosition(clipId); result = timeline->requestClipMove(clipId, timeline->getItemTrackId(clipId), clipStart + (zone.y() - zone.x()), true, true, undo, redo); } } return result; } -bool TimelineFunctions::requestItemCopy(std::shared_ptr timeline, int clipId, int trackId, int position) +bool TimelineFunctions::requestItemCopy(const std::shared_ptr &timeline, int clipId, int trackId, int position) { Q_ASSERT(timeline->isClip(clipId) || timeline->isComposition(clipId)); Fun undo = []() { return true; }; Fun redo = []() { return true; }; int deltaTrack = timeline->getTrackPosition(trackId) - timeline->getTrackPosition(timeline->getItemTrackId(clipId)); int deltaPos = position - timeline->getItemPosition(clipId); std::unordered_set allIds = timeline->getGroupElements(clipId); std::unordered_map mapping; // keys are ids of the source clips, values are ids of the copied clips bool res = true; for (int id : allIds) { int newId = -1; if (timeline->isClip(id)) { PlaylistState::ClipState state = timeline->m_allClips[id]->clipState(); res = copyClip(timeline, id, newId, state, undo, redo); res = res && (newId != -1); } int target_position = timeline->getItemPosition(id) + deltaPos; int target_track_position = timeline->getTrackPosition(timeline->getItemTrackId(id)) + deltaTrack; if (target_track_position >= 0 && target_track_position < timeline->getTracksCount()) { auto it = timeline->m_allTracks.cbegin(); std::advance(it, target_track_position); int target_track = (*it)->getId(); if (timeline->isClip(id)) { res = res && timeline->requestClipMove(newId, target_track, target_position, true, true, undo, redo); } else { const QString &transitionId = timeline->m_allCompositions[id]->getAssetId(); std::unique_ptr transProps(timeline->m_allCompositions[id]->properties()); res = res & timeline->requestCompositionInsertion(transitionId, target_track, -1, target_position, timeline->m_allCompositions[id]->getPlaytime(), std::move(transProps), newId, undo, redo); } } else { res = false; } if (!res) { bool undone = undo(); Q_ASSERT(undone); return false; } mapping[id] = newId; } qDebug() << "Successful copy, coping groups..."; res = timeline->m_groups->copyGroups(mapping, undo, redo); if (!res) { bool undone = undo(); Q_ASSERT(undone); return false; } return true; } -void TimelineFunctions::showClipKeyframes(std::shared_ptr timeline, int clipId, bool value) +void TimelineFunctions::showClipKeyframes(const std::shared_ptr &timeline, int clipId, bool value) { timeline->m_allClips[clipId]->setShowKeyframes(value); QModelIndex modelIndex = timeline->makeClipIndexFromID(clipId); timeline->dataChanged(modelIndex, modelIndex, {TimelineModel::ShowKeyframesRole}); } -void TimelineFunctions::showCompositionKeyframes(std::shared_ptr timeline, int compoId, bool value) +void TimelineFunctions::showCompositionKeyframes(const std::shared_ptr &timeline, int compoId, bool value) { timeline->m_allCompositions[compoId]->setShowKeyframes(value); QModelIndex modelIndex = timeline->makeCompositionIndexFromID(compoId); timeline->dataChanged(modelIndex, modelIndex, {TimelineModel::ShowKeyframesRole}); } -bool TimelineFunctions::switchEnableState(std::shared_ptr timeline, int clipId) +bool TimelineFunctions::switchEnableState(const std::shared_ptr &timeline, int clipId) { PlaylistState::ClipState oldState = timeline->getClipPtr(clipId)->clipState(); PlaylistState::ClipState state = PlaylistState::Disabled; bool disable = true; if (oldState == PlaylistState::Disabled) { state = timeline->getTrackById_const(timeline->getClipTrackId(clipId))->trackType(); disable = false; } Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool result = changeClipState(timeline, clipId, state, undo, redo); if (result) { pCore->pushUndo(undo, redo, disable ? i18n("Disable clip") : i18n("Enable clip")); } return result; } -bool TimelineFunctions::changeClipState(std::shared_ptr timeline, int clipId, PlaylistState::ClipState status, Fun &undo, Fun &redo) +bool TimelineFunctions::changeClipState(const std::shared_ptr &timeline, int clipId, PlaylistState::ClipState status, Fun &undo, Fun &redo) { int track = timeline->getClipTrackId(clipId); int start = -1; int end = -1; if (track > -1) { if (!timeline->getTrackById_const(track)->isAudioTrack()) { start = timeline->getItemPosition(clipId); end = start + timeline->getItemPlaytime(clipId); } } Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; bool result = timeline->m_allClips[clipId]->setClipState(status, local_undo, local_redo); Fun local_update = [start, end, timeline]() { if (start > -1) { timeline->invalidateZone(start, end); timeline->checkRefresh(start, end); } return true; }; if (start > -1) { local_update(); PUSH_LAMBDA(local_update, local_redo); PUSH_LAMBDA(local_update, local_undo); } UPDATE_UNDO_REDO_NOLOCK(local_redo, local_undo, undo, redo); return result; } -bool TimelineFunctions::requestSplitAudio(std::shared_ptr timeline, int clipId, int audioTarget) +bool TimelineFunctions::requestSplitAudio(const std::shared_ptr &timeline, int clipId, int audioTarget) { std::function undo = []() { return true; }; std::function redo = []() { return true; }; const std::unordered_set clips = timeline->getGroupElements(clipId); bool done = false; // Now clear selection so we don't mess with groups pCore->clearSelection(); for (int cid : clips) { if (!timeline->getClipPtr(cid)->canBeAudio() || timeline->getClipPtr(cid)->clipState() == PlaylistState::AudioOnly) { // clip without audio or audio only, skip pCore->displayMessage(i18n("One or more clips don't have audio, or are already audio"), ErrorMessage); return false; } int position = timeline->getClipPosition(cid); int track = timeline->getClipTrackId(cid); QList possibleTracks = audioTarget >= 0 ? QList() << audioTarget : timeline->getLowerTracksId(track, TrackType::AudioTrack); if (possibleTracks.isEmpty()) { // No available audio track for splitting, abort undo(); pCore->displayMessage(i18n("No available audio track for split operation"), ErrorMessage); return false; } int newId; bool res = copyClip(timeline, cid, newId, PlaylistState::AudioOnly, undo, redo); if (!res) { bool undone = undo(); Q_ASSERT(undone); pCore->displayMessage(i18n("Audio split failed"), ErrorMessage); return false; } bool success = false; while (!success && !possibleTracks.isEmpty()) { int newTrack = possibleTracks.takeFirst(); success = timeline->requestClipMove(newId, newTrack, position, true, false, undo, redo); } TimelineFunctions::changeClipState(timeline, cid, PlaylistState::VideoOnly, undo, redo); success = success && timeline->m_groups->createGroupAtSameLevel(cid, std::unordered_set{newId}, GroupType::AVSplit, undo, redo); if (!success) { bool undone = undo(); Q_ASSERT(undone); pCore->displayMessage(i18n("Audio split failed"), ErrorMessage); return false; } done = true; } if (done) { pCore->pushUndo(undo, redo, i18n("Split Audio")); } return done; } -bool TimelineFunctions::requestSplitVideo(std::shared_ptr timeline, int clipId, int videoTarget) +bool TimelineFunctions::requestSplitVideo(const std::shared_ptr &timeline, int clipId, int videoTarget) { std::function undo = []() { return true; }; std::function redo = []() { return true; }; const std::unordered_set clips = timeline->getGroupElements(clipId); bool done = false; // Now clear selection so we don't mess with groups pCore->clearSelection(); for (int cid : clips) { if (!timeline->getClipPtr(cid)->canBeVideo() || timeline->getClipPtr(cid)->clipState() == PlaylistState::VideoOnly) { // clip without audio or audio only, skip continue; } int position = timeline->getClipPosition(cid); QList possibleTracks = QList() << videoTarget; if (possibleTracks.isEmpty()) { // No available audio track for splitting, abort undo(); pCore->displayMessage(i18n("No available video track for split operation"), ErrorMessage); return false; } int newId; bool res = copyClip(timeline, cid, newId, PlaylistState::VideoOnly, undo, redo); if (!res) { bool undone = undo(); Q_ASSERT(undone); pCore->displayMessage(i18n("Video split failed"), ErrorMessage); return false; } bool success = false; while (!success && !possibleTracks.isEmpty()) { int newTrack = possibleTracks.takeFirst(); success = timeline->requestClipMove(newId, newTrack, position, true, false, undo, redo); } TimelineFunctions::changeClipState(timeline, cid, PlaylistState::AudioOnly, undo, redo); success = success && timeline->m_groups->createGroupAtSameLevel(cid, std::unordered_set{newId}, GroupType::AVSplit, undo, redo); if (!success) { bool undone = undo(); Q_ASSERT(undone); pCore->displayMessage(i18n("Video split failed"), ErrorMessage); return false; } done = true; } if (done) { pCore->pushUndo(undo, redo, i18n("Split Video")); } return done; } -void TimelineFunctions::setCompositionATrack(std::shared_ptr timeline, int cid, int aTrack) +void TimelineFunctions::setCompositionATrack(const std::shared_ptr &timeline, int cid, int aTrack) { std::function undo = []() { return true; }; std::function redo = []() { return true; }; std::shared_ptr compo = timeline->getCompositionPtr(cid); int previousATrack = compo->getATrack(); int previousAutoTrack = compo->getForcedTrack() == -1; bool autoTrack = aTrack < 0; if (autoTrack) { // Automatic track compositing, find lower video track aTrack = timeline->getPreviousVideoTrackPos(compo->getCurrentTrackId()); } int start = timeline->getItemPosition(cid); int end = start + timeline->getItemPlaytime(cid); Fun local_redo = [timeline, cid, aTrack, autoTrack, start, end]() { QScopedPointer field(timeline->m_tractor->field()); field->lock(); timeline->getCompositionPtr(cid)->setForceTrack(!autoTrack); timeline->getCompositionPtr(cid)->setATrack(aTrack, aTrack <= 0 ? -1 : timeline->getTrackIndexFromPosition(aTrack - 1)); field->unlock(); QModelIndex modelIndex = timeline->makeCompositionIndexFromID(cid); timeline->dataChanged(modelIndex, modelIndex, {TimelineModel::ItemATrack}); timeline->invalidateZone(start, end); timeline->checkRefresh(start, end); return true; }; Fun local_undo = [timeline, cid, previousATrack, previousAutoTrack, start, end]() { QScopedPointer field(timeline->m_tractor->field()); field->lock(); timeline->getCompositionPtr(cid)->setForceTrack(!previousAutoTrack); timeline->getCompositionPtr(cid)->setATrack(previousATrack, previousATrack <= 0 ? -1 : timeline->getTrackIndexFromPosition(previousATrack - 1)); field->unlock(); QModelIndex modelIndex = timeline->makeCompositionIndexFromID(cid); timeline->dataChanged(modelIndex, modelIndex, {TimelineModel::ItemATrack}); timeline->invalidateZone(start, end); timeline->checkRefresh(start, end); return true; }; if (local_redo()) { PUSH_LAMBDA(local_undo, undo); PUSH_LAMBDA(local_redo, redo); } pCore->pushUndo(undo, redo, i18n("Change Composition Track")); } -void TimelineFunctions::enableMultitrackView(std::shared_ptr timeline, bool enable) +void TimelineFunctions::enableMultitrackView(const std::shared_ptr &timeline, bool enable) { QList videoTracks; for (const auto &track : timeline->m_iteratorTable) { if (timeline->getTrackById_const(track.first)->isAudioTrack() || timeline->getTrackById_const(track.first)->isHidden()) { continue; } videoTracks << track.first; } if (videoTracks.size() < 2) { pCore->displayMessage(i18n("Cannot enable multitrack view on a single track"), InformationMessage); } // First, dis/enable track compositing QScopedPointer service(timeline->m_tractor->field()); Mlt::Field *field = timeline->m_tractor->field(); field->lock(); while ((service != nullptr) && service->is_valid()) { if (service->type() == transition_type) { Mlt::Transition t((mlt_transition)service->get_service()); QString serviceName = t.get("mlt_service"); int added = t.get_int("internal_added"); if (added == 237 && serviceName != QLatin1String("mix")) { // remove all compositing transitions t.set("disable", enable ? "1" : nullptr); } else if (!enable && added == 200) { field->disconnect_service(t); } } service.reset(service->producer()); } if (enable) { for (int i = 0; i < videoTracks.size(); ++i) { Mlt::Transition transition(*timeline->m_tractor->profile(), "composite"); transition.set("mlt_service", "composite"); transition.set("a_track", 0); transition.set("b_track", timeline->getTrackMltIndex(videoTracks.at(i))); transition.set("distort", 0); transition.set("aligned", 0); // 200 is an arbitrary number so we can easily remove these transition later transition.set("internal_added", 200); QString geometry; switch (i) { case 0: switch (videoTracks.size()) { case 2: geometry = QStringLiteral("0 0 50% 100%"); break; case 3: geometry = QStringLiteral("0 0 33% 100%"); break; case 4: geometry = QStringLiteral("0 0 50% 50%"); break; case 5: case 6: geometry = QStringLiteral("0 0 33% 50%"); break; default: geometry = QStringLiteral("0 0 33% 33%"); break; } break; case 1: switch (videoTracks.size()) { case 2: geometry = QStringLiteral("50% 0 50% 100%"); break; case 3: geometry = QStringLiteral("33% 0 33% 100%"); break; case 4: geometry = QStringLiteral("50% 0 50% 50%"); break; case 5: case 6: geometry = QStringLiteral("33% 0 33% 50%"); break; default: geometry = QStringLiteral("33% 0 33% 33%"); break; } break; case 2: switch (videoTracks.size()) { case 3: geometry = QStringLiteral("66% 0 33% 100%"); break; case 4: geometry = QStringLiteral("0 50% 50% 50%"); break; case 5: case 6: geometry = QStringLiteral("66% 0 33% 50%"); break; default: geometry = QStringLiteral("66% 0 33% 33%"); break; } break; case 3: switch (videoTracks.size()) { case 4: geometry = QStringLiteral("50% 50% 50% 50%"); break; case 5: case 6: geometry = QStringLiteral("0 50% 33% 50%"); break; default: geometry = QStringLiteral("0 33% 33% 33%"); break; } break; case 4: switch (videoTracks.size()) { case 5: case 6: geometry = QStringLiteral("33% 50% 33% 50%"); break; default: geometry = QStringLiteral("33% 33% 33% 33%"); break; } break; case 5: switch (videoTracks.size()) { case 6: geometry = QStringLiteral("66% 50% 33% 50%"); break; default: geometry = QStringLiteral("66% 33% 33% 33%"); break; } break; case 6: geometry = QStringLiteral("0 66% 33% 33%"); break; case 7: geometry = QStringLiteral("33% 66% 33% 33%"); break; default: geometry = QStringLiteral("66% 66% 33% 33%"); break; } // Add transition to track: transition.set("geometry", geometry.toUtf8().constData()); transition.set("always_active", 1); field->plant_transition(transition, 0, timeline->getTrackMltIndex(videoTracks.at(i))); } } field->unlock(); timeline->requestMonitorRefresh(); } -void TimelineFunctions::saveTimelineSelection(std::shared_ptr timeline, QList selection, QDir targetDir) +void TimelineFunctions::saveTimelineSelection(const std::shared_ptr &timeline, QList selection, const QDir &targetDir) { bool ok; QString name = QInputDialog::getText(qApp->activeWindow(), i18n("Add Clip to Library"), i18n("Enter a name for the clip in Library"), QLineEdit::Normal, QString(), &ok); if (name.isEmpty() || !ok) { return; } if (targetDir.exists(name + QStringLiteral(".mlt"))) { // TODO: warn and ask for overwrite / rename } int offset = -1; int lowerAudioTrack = -1; int lowerVideoTrack = -1; QString fullPath = targetDir.absoluteFilePath(name + QStringLiteral(".mlt")); // Build a copy of selected tracks. QMap sourceTracks; for (int i : selection) { int sourceTrack = timeline->getItemTrackId(i); int clipPos = timeline->getItemPosition(i); if (offset < 0 || clipPos < offset) { offset = clipPos; } int trackPos = timeline->getTrackMltIndex(sourceTrack); if (!sourceTracks.contains(trackPos)) { sourceTracks.insert(trackPos, sourceTrack); } } // Build target timeline Mlt::Tractor newTractor(*timeline->m_tractor->profile()); QScopedPointer field(newTractor.field()); int ix = 0; QString composite = TransitionsRepository::get()->getCompositingTransition(); QMapIterator i(sourceTracks); QList compositions; while (i.hasNext()) { i.next(); QScopedPointer newTrackPlaylist(new Mlt::Playlist(*newTractor.profile())); newTractor.set_track(*newTrackPlaylist, ix); // QScopedPointer trackProducer(newTractor.track(ix)); int trackId = i.value(); sourceTracks.insert(timeline->getTrackMltIndex(trackId), ix); std::shared_ptr track = timeline->getTrackById_const(trackId); bool isAudio = track->isAudioTrack(); if (isAudio) { newTrackPlaylist->set("hide", 1); if (lowerAudioTrack < 0) { lowerAudioTrack = ix; } } else { newTrackPlaylist->set("hide", 2); if (lowerVideoTrack < 0) { lowerVideoTrack = ix; } } for (int itemId : selection) { if (timeline->getItemTrackId(itemId) == trackId) { // Copy clip on the destination track if (timeline->isClip(itemId)) { int clip_position = timeline->m_allClips[itemId]->getPosition(); auto clip_loc = track->getClipIndexAt(clip_position); int target_clip = clip_loc.second; QSharedPointer clip = track->getClipProducer(target_clip); newTrackPlaylist->insert_at(clip_position - offset, clip.data(), 1); } else if (timeline->isComposition(itemId)) { // Composition Mlt::Transition *t = new Mlt::Transition(*timeline->m_allCompositions[itemId].get()); QString id(t->get("kdenlive_id")); QString internal(t->get("internal_added")); if (internal.isEmpty()) { compositions << t; if (id.isEmpty()) { qDebug() << "// Warning, this should not happen, transition without id: " << t->get("id") << " = " << t->get("mlt_service"); t->set("kdenlive_id", t->get("mlt_service")); } } } } } ix++; } // Sort compositions and insert if (!compositions.isEmpty()) { std::sort(compositions.begin(), compositions.end(), [](Mlt::Transition *a, Mlt::Transition *b) { return a->get_b_track() < b->get_b_track(); }); while (!compositions.isEmpty()) { QScopedPointer t(compositions.takeFirst()); if (sourceTracks.contains(t->get_a_track()) && sourceTracks.contains(t->get_b_track())) { Mlt::Transition newComposition(*newTractor.profile(), t->get("mlt_service")); Mlt::Properties sourceProps(t->get_properties()); newComposition.inherit(sourceProps); QString id(t->get("kdenlive_id")); int in = qMax(0, t->get_in() - offset); int out = t->get_out() - offset; newComposition.set_in_and_out(in, out); int a_track = sourceTracks.value(t->get_a_track()); int b_track = sourceTracks.value(t->get_b_track()); field->plant_transition(newComposition, a_track, b_track); } } } // Track compositing i.toFront(); ix = 0; while (i.hasNext()) { i.next(); int trackId = i.value(); std::shared_ptr track = timeline->getTrackById_const(trackId); bool isAudio = track->isAudioTrack(); if ((isAudio && ix > lowerAudioTrack) || (!isAudio && ix > lowerVideoTrack)) { // add track compositing / mix Mlt::Transition t(*newTractor.profile(), isAudio ? "mix" : composite.toUtf8().constData()); if (isAudio) { t.set("sum", 1); } t.set("always_active", 1); t.set("internal_added", 237); field->plant_transition(t, isAudio ? lowerAudioTrack : lowerVideoTrack, ix); } ix++; } Mlt::Consumer xmlConsumer(*newTractor.profile(), ("xml:" + fullPath).toUtf8().constData()); xmlConsumer.set("terminate_on_pause", 1); xmlConsumer.connect(newTractor); xmlConsumer.run(); } -int TimelineFunctions::getTrackOffset(std::shared_ptr timeline, int startTrack, int destTrack) +int TimelineFunctions::getTrackOffset(const std::shared_ptr &timeline, int startTrack, int destTrack) { qDebug() << "+++++++\nGET TRACK OFFSET: " << startTrack << " - " << destTrack; int masterTrackMltIndex = timeline->getTrackMltIndex(startTrack); int destTrackMltIndex = timeline->getTrackMltIndex(destTrack); int offset = 0; qDebug() << "+++++++\nGET TRACK MLT: " << masterTrackMltIndex << " - " << destTrackMltIndex; if (masterTrackMltIndex == destTrackMltIndex) { return offset; } int step = masterTrackMltIndex > destTrackMltIndex ? -1 : 1; bool isAudio = timeline->isAudioTrack(startTrack); int track = masterTrackMltIndex; while (track != destTrackMltIndex) { track += step; qDebug() << "+ + +TESTING TRACK: " << track; int trackId = timeline->getTrackIndexFromPosition(track - 1); if (isAudio == timeline->isAudioTrack(trackId)) { offset += step; } } return offset; } -int TimelineFunctions::getOffsetTrackId(std::shared_ptr timeline, int startTrack, int offset, bool audioOffset) +int TimelineFunctions::getOffsetTrackId(const std::shared_ptr &timeline, int startTrack, int offset, bool audioOffset) { int masterTrackMltIndex = timeline->getTrackMltIndex(startTrack); bool isAudio = timeline->isAudioTrack(startTrack); if (isAudio != audioOffset) { offset = -offset; } qDebug() << "* ** * MASTER INDEX: " << masterTrackMltIndex << ", OFFSET: " << offset; while (offset != 0) { masterTrackMltIndex += offset > 0 ? 1 : -1; qDebug() << "#### TESTING TRACK: " << masterTrackMltIndex; if (masterTrackMltIndex < 0) { masterTrackMltIndex = 0; break; } else if (masterTrackMltIndex > (int)timeline->m_allTracks.size()) { masterTrackMltIndex = (int)timeline->m_allTracks.size(); break; } int trackId = timeline->getTrackIndexFromPosition(masterTrackMltIndex - 1); if (timeline->isAudioTrack(trackId) == isAudio) { offset += offset > 0 ? -1 : 1; } } return timeline->getTrackIndexFromPosition(masterTrackMltIndex - 1); } diff --git a/src/timeline2/model/timelinefunctions.hpp b/src/timeline2/model/timelinefunctions.hpp index da632160d..62f5b9a65 100644 --- a/src/timeline2/model/timelinefunctions.hpp +++ b/src/timeline2/model/timelinefunctions.hpp @@ -1,105 +1,105 @@ /* Copyright (C) 2017 Jean-Baptiste Mardelle This file is part of Kdenlive. See www.kdenlive.org. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef TIMELINEFUNCTIONS_H #define TIMELINEFUNCTIONS_H #include "definitions.h" #include "undohelper.hpp" #include #include #include /** * @namespace TimelineFunction * @brief This namespace contains a list of static methods for advanced timeline editing features * based on timelinemodel methods */ class TimelineItemModel; struct TimelineFunctions { /* @brief Cuts a clip at given position If the clip is part of the group, all clips of the groups are cut at the same position. The group structure is then preserved for clips on both sides Returns true on success @param timeline : ptr to the timeline model @param clipId: Id of the clip to split @param position: position (in frames from the beginning of the timeline) where to cut */ static bool requestClipCut(std::shared_ptr timeline, int clipId, int position); /* This is the same function, except that it accumulates undo/redo */ - static bool requestClipCut(std::shared_ptr timeline, int clipId, int position, Fun &undo, Fun &redo); + static bool requestClipCut(const std::shared_ptr &timeline, int clipId, int position, Fun &undo, Fun &redo); /* This is the same function, except that it accumulates undo/redo and do not deal with groups. Do not call directly */ - static bool processClipCut(std::shared_ptr timeline, int clipId, int position, int &newId, Fun &undo, Fun &redo); + static bool processClipCut(const std::shared_ptr &timeline, int clipId, int position, int &newId, Fun &undo, Fun &redo); /* @brief Makes a perfect copy of a given clip, but do not insert it */ - static bool copyClip(std::shared_ptr timeline, int clipId, int &newId, PlaylistState::ClipState state, Fun &undo, Fun &redo); + static bool copyClip(const std::shared_ptr &timeline, int clipId, int &newId, PlaylistState::ClipState state, Fun &undo, Fun &redo); /* @brief Request the addition of multiple clips to the timeline * If the addition of any of the clips fails, the entire operation is undone. * @returns true on success, false otherwise. * @param binIds the list of bin ids to be inserted * @param trackId the track where the insertion should happen * @param position the position at which the clips should be inserted * @param clipIds a return parameter with the ids assigned to the clips if success, empty otherwise */ - static bool requestMultipleClipsInsertion(std::shared_ptr timeline, const QStringList &binIds, int trackId, int position, + static bool requestMultipleClipsInsertion(const std::shared_ptr &timeline, const QStringList &binIds, int trackId, int position, QList &clipIds, bool logUndo, bool refreshView); - static int requestSpacerStartOperation(std::shared_ptr timeline, int trackId, int position); - static bool requestSpacerEndOperation(std::shared_ptr timeline, int itemId, int startPosition, int endPosition); - static bool extractZone(std::shared_ptr timeline, QVector tracks, QPoint zone, bool liftOnly); - static bool liftZone(std::shared_ptr timeline, int trackId, QPoint zone, Fun &undo, Fun &redo); - static bool removeSpace(std::shared_ptr timeline, int trackId, QPoint zone, Fun &undo, Fun &redo); - static bool insertSpace(std::shared_ptr timeline, int trackId, QPoint zone, Fun &undo, Fun &redo); - static bool insertZone(std::shared_ptr timeline, QList trackIds, const QString &binId, int insertFrame, QPoint zone, + static int requestSpacerStartOperation(const std::shared_ptr &timeline, int trackId, int position); + static bool requestSpacerEndOperation(const std::shared_ptr &timeline, int itemId, int startPosition, int endPosition); + static bool extractZone(const std::shared_ptr &timeline, QVector tracks, QPoint zone, bool liftOnly); + static bool liftZone(const std::shared_ptr &timeline, int trackId, QPoint zone, Fun &undo, Fun &redo); + static bool removeSpace(const std::shared_ptr &timeline, int trackId, QPoint zone, Fun &undo, Fun &redo); + static bool insertSpace(const std::shared_ptr &timeline, int trackId, QPoint zone, Fun &undo, Fun &redo); + static bool insertZone(const std::shared_ptr &timeline, QList trackIds, const QString &binId, int insertFrame, QPoint zone, bool overwrite); - static bool requestItemCopy(std::shared_ptr timeline, int clipId, int trackId, int position); - static void showClipKeyframes(std::shared_ptr timeline, int clipId, bool value); - static void showCompositionKeyframes(std::shared_ptr timeline, int compoId, bool value); + static bool requestItemCopy(const std::shared_ptr &timeline, int clipId, int trackId, int position); + static void showClipKeyframes(const std::shared_ptr &timeline, int clipId, bool value); + static void showCompositionKeyframes(const std::shared_ptr &timeline, int compoId, bool value); /* @brief If the clip is activated, disable, otherwise enable * @param timeline: pointer to the timeline that we modify * @param clipId: Id of the clip to modify * @param status: target status of the clip This function creates an undo object and returns true on success */ - static bool switchEnableState(std::shared_ptr timeline, int clipId); + static bool switchEnableState(const std::shared_ptr &timeline, int clipId); /* @brief change the clip state and accumulates for undo/redo */ - static bool changeClipState(std::shared_ptr timeline, int clipId, PlaylistState::ClipState status, Fun &undo, Fun &redo); + static bool changeClipState(const std::shared_ptr &timeline, int clipId, PlaylistState::ClipState status, Fun &undo, Fun &redo); - static bool requestSplitAudio(std::shared_ptr timeline, int clipId, int audioTarget); - static bool requestSplitVideo(std::shared_ptr timeline, int clipId, int videoTarget); - static void setCompositionATrack(std::shared_ptr timeline, int cid, int aTrack); - static void enableMultitrackView(std::shared_ptr timeline, bool enable); - static void saveTimelineSelection(std::shared_ptr timeline, QList selection, QDir targetDir); + static bool requestSplitAudio(const std::shared_ptr &timeline, int clipId, int audioTarget); + static bool requestSplitVideo(const std::shared_ptr &timeline, int clipId, int videoTarget); + static void setCompositionATrack(const std::shared_ptr &timeline, int cid, int aTrack); + static void enableMultitrackView(const std::shared_ptr &timeline, bool enable); + static void saveTimelineSelection(const std::shared_ptr &timeline, QList selection, const QDir &targetDir); /** @brief returns the number of same type tracks between 2 tracks */ - static int getTrackOffset(std::shared_ptr timeline, int startTrack, int destTrack); + static int getTrackOffset(const std::shared_ptr &timeline, int startTrack, int destTrack); /** @brief returns an offset track id */ - static int getOffsetTrackId(std::shared_ptr timeline, int startTrack, int offset, bool audioOffset); + static int getOffsetTrackId(const std::shared_ptr &timeline, int startTrack, int offset, bool audioOffset); }; #endif diff --git a/src/timeline2/model/timelineitemmodel.cpp b/src/timeline2/model/timelineitemmodel.cpp index aac9df5d2..35e0249b2 100644 --- a/src/timeline2/model/timelineitemmodel.cpp +++ b/src/timeline2/model/timelineitemmodel.cpp @@ -1,610 +1,610 @@ /*************************************************************************** * Copyright (C) 2017 by Nicolas Carion * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #include "timelineitemmodel.hpp" #include "assets/keyframes/model/keyframemodel.hpp" #include "bin/model/markerlistmodel.hpp" #include "clipmodel.hpp" #include "compositionmodel.hpp" #include "core.h" #include "doc/docundostack.hpp" #include "groupsmodel.hpp" #include "kdenlivesettings.h" #include "macros.hpp" #include "trackmodel.hpp" #include "transitions/transitionsrepository.hpp" #include #include #include #include #include #include #include TimelineItemModel::TimelineItemModel(Mlt::Profile *profile, std::weak_ptr undo_stack) - : TimelineModel(profile, undo_stack) + : TimelineModel(profile, std::move(undo_stack)) { } -void TimelineItemModel::finishConstruct(std::shared_ptr ptr, std::shared_ptr guideModel) +void TimelineItemModel::finishConstruct(const std::shared_ptr &ptr, const std::shared_ptr &guideModel) { ptr->weak_this_ = ptr; ptr->m_groups = std::unique_ptr(new GroupsModel(ptr)); guideModel->registerSnapModel(ptr->m_snaps); } std::shared_ptr TimelineItemModel::construct(Mlt::Profile *profile, std::shared_ptr guideModel, std::weak_ptr undo_stack) { std::shared_ptr ptr(new TimelineItemModel(profile, std::move(undo_stack))); finishConstruct(ptr, std::move(guideModel)); return ptr; } TimelineItemModel::~TimelineItemModel() = default; QModelIndex TimelineItemModel::index(int row, int column, const QModelIndex &parent) const { READ_LOCK(); QModelIndex result; if (parent.isValid()) { auto trackId = int(parent.internalId()); Q_ASSERT(isTrack(trackId)); int clipId = getTrackById_const(trackId)->getClipByRow(row); if (clipId != -1) { result = createIndex(row, 0, quintptr(clipId)); } else if (row < getTrackClipsCount(trackId) + getTrackCompositionsCount(trackId)) { int compoId = getTrackById_const(trackId)->getCompositionByRow(row); if (compoId != -1) { result = createIndex(row, 0, quintptr(compoId)); } } else { // Invalid index requested Q_ASSERT(false); } } else if (row < getTracksCount() && row >= 0) { // Get sort order // row = getTracksCount() - 1 - row; auto it = m_allTracks.cbegin(); std::advance(it, row); int trackId = (*it)->getId(); result = createIndex(row, column, quintptr(trackId)); } return result; } /*QModelIndex TimelineItemModel::makeIndex(int trackIndex, int clipIndex) const { return index(clipIndex, 0, index(trackIndex)); }*/ QModelIndex TimelineItemModel::makeClipIndexFromID(int clipId) const { Q_ASSERT(m_allClips.count(clipId) > 0); int trackId = m_allClips.at(clipId)->getCurrentTrackId(); if (trackId == -1) { // Clip is not inserted in a track qDebug() << "/// WARNING; INVALID CLIP INDEX REQUESTED\n________________"; return QModelIndex(); } int row = getTrackById_const(trackId)->getRowfromClip(clipId); return index(row, 0, makeTrackIndexFromID(trackId)); } QModelIndex TimelineItemModel::makeCompositionIndexFromID(int compoId) const { Q_ASSERT(m_allCompositions.count(compoId) > 0); int trackId = m_allCompositions.at(compoId)->getCurrentTrackId(); return index(getTrackById_const(trackId)->getRowfromComposition(compoId), 0, makeTrackIndexFromID(trackId)); } QModelIndex TimelineItemModel::makeTrackIndexFromID(int trackId) const { // we retrieve iterator Q_ASSERT(m_iteratorTable.count(trackId) > 0); auto it = m_iteratorTable.at(trackId); int ind = (int)std::distance(m_allTracks.begin(), it); // Get sort order // ind = getTracksCount() - 1 - ind; return index(ind); } QModelIndex TimelineItemModel::parent(const QModelIndex &index) const { READ_LOCK(); // qDebug() << "TimelineItemModel::parent"<< index; if (index == QModelIndex()) { return index; } const int id = static_cast(index.internalId()); if (!index.isValid() || isTrack(id)) { return QModelIndex(); } if (isClip(id)) { const int trackId = getClipTrackId(id); return makeTrackIndexFromID(trackId); } if (isComposition(id)) { const int trackId = getCompositionTrackId(id); return makeTrackIndexFromID(trackId); } return QModelIndex(); } int TimelineItemModel::rowCount(const QModelIndex &parent) const { READ_LOCK(); if (parent.isValid()) { const int id = (int)parent.internalId(); if (!isTrack(id)) { // clips don't have children // if it is not a track, it is something invalid return 0; } return getTrackClipsCount(id) + getTrackCompositionsCount(id); } return getTracksCount(); } int TimelineItemModel::columnCount(const QModelIndex &parent) const { Q_UNUSED(parent); return 1; } QHash TimelineItemModel::roleNames() const { QHash roles; roles[NameRole] = "name"; roles[ResourceRole] = "resource"; roles[ServiceRole] = "mlt_service"; roles[BinIdRole] = "binId"; roles[TrackIdRole] = "trackId"; roles[FakeTrackIdRole] = "fakeTrackId"; roles[FakePositionRole] = "fakePosition"; roles[StartRole] = "start"; roles[DurationRole] = "duration"; roles[MarkersRole] = "markers"; roles[KeyframesRole] = "keyframeModel"; roles[ShowKeyframesRole] = "showKeyframes"; roles[StatusRole] = "clipStatus"; roles[TypeRole] = "clipType"; roles[InPointRole] = "in"; roles[OutPointRole] = "out"; roles[FramerateRole] = "fps"; roles[GroupedRole] = "grouped"; roles[IsDisabledRole] = "disabled"; roles[IsAudioRole] = "audio"; roles[AudioLevelsRole] = "audioLevels"; roles[AudioChannelsRole] = "audioChannels"; roles[IsCompositeRole] = "composite"; roles[IsLockedRole] = "locked"; roles[FadeInRole] = "fadeIn"; roles[FadeOutRole] = "fadeOut"; roles[FileHashRole] = "hash"; roles[SpeedRole] = "speed"; roles[HeightRole] = "trackHeight"; roles[TrackTagRole] = "trackTag"; roles[ItemIdRole] = "item"; roles[ItemATrack] = "a_track"; roles[HasAudio] = "hasAudio"; roles[CanBeAudioRole] = "canBeAudio"; roles[CanBeVideoRole] = "canBeVideo"; roles[ReloadThumbRole] = "reloadThumb"; roles[ThumbsFormatRole] = "thumbsFormat"; roles[EffectNamesRole] = "effectNames"; roles[EffectsEnabledRole] = "isStackEnabled"; roles[GrabbedRole] = "isGrabbed"; return roles; } QVariant TimelineItemModel::data(const QModelIndex &index, int role) const { READ_LOCK(); if (!m_tractor || !index.isValid()) { // qDebug() << "DATA abort. Index validity="< clip = m_allClips.at(id); // Get data for a clip switch (role) { // TODO case NameRole: case Qt::DisplayRole: { QString result = clip->getProperty("kdenlive:clipname"); if (result.isEmpty()) { result = clip->getProperty("kdenlive:originalurl"); if (result.isEmpty()) { result = clip->getProperty("resource"); } if (!result.isEmpty()) { result = QFileInfo(result).fileName(); } else { result = clip->getProperty("mlt_service"); } } return result; } case ResourceRole: { QString result = clip->getProperty("resource"); if (result == QLatin1String("")) { result = clip->getProperty("mlt_service"); } return result; } case FakeTrackIdRole: return clip->getFakeTrackId(); case FakePositionRole: return clip->getFakePosition(); case BinIdRole: return clip->binId(); case TrackIdRole: return clip->getCurrentTrackId(); case ServiceRole: return clip->getProperty("mlt_service"); break; case AudioLevelsRole: return clip->getAudioWaveform(); case AudioChannelsRole: return clip->audioChannels(); case HasAudio: return clip->audioEnabled(); case IsAudioRole: return clip->isAudioOnly(); case CanBeAudioRole: return clip->canBeAudio(); case CanBeVideoRole: return clip->canBeVideo(); case MarkersRole: { return QVariant::fromValue(clip->getMarkerModel().get()); } case KeyframesRole: { return QVariant::fromValue(clip->getKeyframeModel()); } case StatusRole: return QVariant::fromValue(clip->clipState()); case TypeRole: return QVariant::fromValue(clip->clipType()); case StartRole: return clip->getPosition(); case DurationRole: return clip->getPlaytime(); case GroupedRole: { int parentId = m_groups->getDirectAncestor(id); return parentId != -1 && parentId != m_temporarySelectionGroup; } case EffectNamesRole: return clip->effectNames(); case InPointRole: return clip->getIn(); case OutPointRole: return clip->getOut(); case ShowKeyframesRole: return clip->showKeyframes(); case FadeInRole: return clip->fadeIn(); case FadeOutRole: return clip->fadeOut(); case ReloadThumbRole: return clip->forceThumbReload; case SpeedRole: return clip->getSpeed(); case GrabbedRole: return clip->isGrabbed(); default: break; } } else if (isTrack(id)) { // qDebug() << "DATA REQUESTED FOR TRACK "<< id; switch (role) { case NameRole: case Qt::DisplayRole: { return getTrackById_const(id)->getProperty("kdenlive:track_name").toString(); } case TypeRole: return QVariant::fromValue(ClipType::ProducerType::Track); case DurationRole: // qDebug() << "DATA yielding duration" << m_tractor->get_playtime(); return getTrackById_const(id)->trackDuration(); case IsDisabledRole: // qDebug() << "DATA yielding mute" << 0; return getTrackById_const(id)->isAudioTrack() ? getTrackById_const(id)->isMute() : getTrackById_const(id)->isHidden(); case IsAudioRole: return getTrackById_const(id)->isAudioTrack(); case TrackTagRole: return getTrackTagById(id); case IsLockedRole: return getTrackById_const(id)->getProperty("kdenlive:locked_track").toInt() == 1; case HeightRole: { int collapsed = getTrackById_const(id)->getProperty("kdenlive:collapsed").toInt(); if (collapsed > 0) { return collapsed; } int height = getTrackById_const(id)->getProperty("kdenlive:trackheight").toInt(); // qDebug() << "DATA yielding height" << height; return (height > 0 ? height : 60); } case ThumbsFormatRole: return getTrackById_const(id)->getProperty("kdenlive:thumbs_format").toInt(); case IsCompositeRole: { return Qt::Unchecked; } case EffectNamesRole: { return getTrackById_const(id)->effectNames(); } case EffectsEnabledRole: { return getTrackById_const(id)->stackEnabled(); } default: break; } } else if (isComposition(id)) { std::shared_ptr compo = m_allCompositions.at(id); switch (role) { case NameRole: case Qt::DisplayRole: case ResourceRole: case ServiceRole: return compo->displayName(); break; case TypeRole: return QVariant::fromValue(ClipType::ProducerType::Composition); case StartRole: return compo->getPosition(); case TrackIdRole: return compo->getCurrentTrackId(); case DurationRole: return compo->getPlaytime(); case GroupedRole: return m_groups->isInGroup(id); case InPointRole: return 0; case OutPointRole: return 100; case BinIdRole: return 5; case KeyframesRole: { return QVariant::fromValue(compo->getEffectKeyframeModel()); } case ShowKeyframesRole: return compo->showKeyframes(); case ItemATrack: return compo->getForcedTrack(); case MarkersRole: { QVariantList markersList; return markersList; } case GrabbedRole: return compo->isGrabbed(); default: break; } } else { qDebug() << "UNKNOWN DATA requested " << index << roleNames()[role]; } return QVariant(); } void TimelineItemModel::setTrackProperty(int trackId, const QString &name, const QString &value) { std::shared_ptr track = getTrackById(trackId); track->setProperty(name, value); QVector roles; if (name == QLatin1String("kdenlive:track_name")) { roles.push_back(NameRole); } else if (name == QLatin1String("kdenlive:locked_track")) { roles.push_back(IsLockedRole); } else if (name == QLatin1String("hide")) { roles.push_back(IsDisabledRole); if (!track->isAudioTrack()) { pCore->requestMonitorRefresh(); } } else if (name == QLatin1String("kdenlive:thumbs_format")) { roles.push_back(ThumbsFormatRole); } if (!roles.isEmpty()) { QModelIndex ix = makeTrackIndexFromID(trackId); emit dataChanged(ix, ix, roles); } } void TimelineItemModel::setTrackStackEnabled(int tid, bool enable) { std::shared_ptr track = getTrackById(tid); track->setEffectStackEnabled(enable); QModelIndex ix = makeTrackIndexFromID(tid); emit dataChanged(ix, ix, {TimelineModel::EffectsEnabledRole}); } void TimelineItemModel::importTrackEffects(int tid, std::weak_ptr service) { std::shared_ptr track = getTrackById(tid); - track->importEffects(service); + track->importEffects(std::move(service)); } QVariant TimelineItemModel::getTrackProperty(int tid, const QString &name) const { return getTrackById_const(tid)->getProperty(name); } int TimelineItemModel::getFirstVideoTrackIndex() const { int trackId = -1; auto it = m_allTracks.cbegin(); while (it != m_allTracks.cend()) { trackId = (*it)->getId(); if (!(*it)->isAudioTrack()) { break; } ++it; } return trackId; } const QString TimelineItemModel::getTrackFullName(int tid) const { QString tag = getTrackTagById(tid); QString trackName = getTrackById_const(tid)->getProperty(QStringLiteral("kdenlive:track_name")).toString(); return trackName.isEmpty() ? tag : tag + QStringLiteral(" - ") + trackName; } const QString TimelineItemModel::groupsData() { return m_groups->toJson(); } bool TimelineItemModel::loadGroups(const QString &groupsData) { return m_groups->fromJson(groupsData); } bool TimelineItemModel::isInMultiSelection(int cid) const { if (m_temporarySelectionGroup == -1) { return false; } bool res = (m_groups->getRootId(cid) == m_temporarySelectionGroup) && (m_groups->getDirectChildren(m_temporarySelectionGroup).size() != 1); return res; } bool TimelineItemModel::isSelected(int cid) const { if (m_temporarySelectionGroup == -1) { return false; } return m_groups->getRootId(cid) == m_temporarySelectionGroup; } void TimelineItemModel::notifyChange(const QModelIndex &topleft, const QModelIndex &bottomright, bool start, bool duration, bool updateThumb) { QVector roles; if (start) { roles.push_back(TimelineModel::StartRole); if (updateThumb) { roles.push_back(TimelineModel::InPointRole); } } if (duration) { roles.push_back(TimelineModel::DurationRole); if (updateThumb) { roles.push_back(TimelineModel::OutPointRole); } } emit dataChanged(topleft, bottomright, roles); } void TimelineItemModel::notifyChange(const QModelIndex &topleft, const QModelIndex &bottomright, const QVector &roles) { emit dataChanged(topleft, bottomright, roles); } void TimelineItemModel::buildTrackCompositing(bool rebuild) { auto it = m_allTracks.cbegin(); QScopedPointer field(m_tractor->field()); field->lock(); // Make sure all previous track compositing is removed if (rebuild) { QScopedPointer service(new Mlt::Service(field->get_service())); while ((service != nullptr) && service->is_valid()) { if (service->type() == transition_type) { Mlt::Transition t((mlt_transition)service->get_service()); QString serviceName = t.get("mlt_service"); if (t.get_int("internal_added") == 237) { // remove all compositing transitions field->disconnect_service(t); } } service.reset(service->producer()); } } QString composite = TransitionsRepository::get()->getCompositingTransition(); while (it != m_allTracks.cend()) { int trackId = getTrackMltIndex((*it)->getId()); if (!composite.isEmpty() && !(*it)->isAudioTrack()) { // video track, add composition std::unique_ptr transition = TransitionsRepository::get()->getTransition(composite); transition->set("internal_added", 237); transition->set("always_active", 1); field->plant_transition(*transition, 0, trackId); transition->set_tracks(0, trackId); } else if ((*it)->isAudioTrack()) { // audio mix std::unique_ptr transition = TransitionsRepository::get()->getTransition(QStringLiteral("mix")); transition->set("internal_added", 237); transition->set("always_active", 1); transition->set("sum", 1); field->plant_transition(*transition, 0, trackId); transition->set_tracks(0, trackId); } ++it; } field->unlock(); if (composite.isEmpty()) { pCore->displayMessage(i18n("Could not setup track compositing, check your install"), MessageType::ErrorMessage); } } void TimelineItemModel::notifyChange(const QModelIndex &topleft, const QModelIndex &bottomright, int role) { emit dataChanged(topleft, bottomright, {role}); } void TimelineItemModel::_beginRemoveRows(const QModelIndex &i, int j, int k) { // qDebug()<<"FORWARDING beginRemoveRows"<. * ***************************************************************************/ #ifndef TIMELINEITEMMODEL_H #define TIMELINEITEMMODEL_H #include "timelinemodel.hpp" #include "undohelper.hpp" /* @brief This class is the thin wrapper around the TimelineModel that provides interface for the QML. It derives from AbstractItemModel to provide the model to the QML interface. An itemModel is organized with row and columns that contain the data. It can be hierarchical, meaning that a given index (row,column) can contain another level of rows and column. Our organization is as follows: at the top level, each row contains a track. These rows are in the same order as in the actual timeline. Then each of this row contains itself sub-rows that correspond to the clips. Here the order of these sub-rows is unrelated to the chronological order of the clips, but correspond to their Id order. For example, if you have three clips, with ids 12, 45 and 150, they will receive row index 0,1 and 2. This is because the order actually doesn't matter since the clips are rendered based on their positions rather than their row order. The id order has been chosen because it is consistent with a valid ordering of the clips. The columns are never used, so the data is always in column 0 An ModelIndex in the ItemModel consists of a row number, a column number, and a parent index. In our case, tracks have always an empty parent, and the clip have a track index as parent. A ModelIndex can also store one additional integer, and we exploit this feature to store the unique ID of the object it corresponds to. */ class MarkerListModel; class TimelineItemModel : public TimelineModel { Q_OBJECT public: /* @brief construct a timeline object and returns a pointer to the created object @param undo_stack is a weak pointer to the undo stack of the project @param guideModel ptr to the guide model of the project */ static std::shared_ptr construct(Mlt::Profile *profile, std::shared_ptr guideModel, std::weak_ptr undo_stack); friend bool constructTimelineFromMelt(const std::shared_ptr &timeline, Mlt::Tractor tractor); protected: /* @brief this constructor should not be called. Call the static construct instead */ TimelineItemModel(Mlt::Profile *profile, std::weak_ptr undo_stack); public: ~TimelineItemModel(); int rowCount(const QModelIndex &parent = QModelIndex()) const override; int columnCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QHash roleNames() const override; QModelIndex index(int row, int column = 0, const QModelIndex &parent = QModelIndex()) const override; // QModelIndex makeIndex(int trackIndex, int clipIndex) const; /* @brief Creates an index based on the ID of the clip*/ QModelIndex makeClipIndexFromID(int clipId) const override; /* @brief Creates an index based on the ID of the compoition*/ QModelIndex makeCompositionIndexFromID(int compoId) const override; /* @brief Creates an index based on the ID of the track*/ QModelIndex makeTrackIndexFromID(int trackId) const override; QModelIndex parent(const QModelIndex &index) const override; Q_INVOKABLE void setTrackProperty(int tid, const QString &name, const QString &value); /* @brief Enabled/disabled a track's effect stack */ Q_INVOKABLE void setTrackStackEnabled(int tid, bool enable); Q_INVOKABLE QVariant getTrackProperty(int tid, const QString &name) const; /** @brief returns the lower video track index in timeline. **/ int getFirstVideoTrackIndex() const; const QString getTrackFullName(int tid) const; void notifyChange(const QModelIndex &topleft, const QModelIndex &bottomright, bool start, bool duration, bool updateThumb) override; void notifyChange(const QModelIndex &topleft, const QModelIndex &bottomright, const QVector &roles) override; void notifyChange(const QModelIndex &topleft, const QModelIndex &bottomright, int role) override; /** @brief Rebuild track compositing */ void buildTrackCompositing(bool rebuild = false); /** @brief Import track effects */ void importTrackEffects(int tid, std::weak_ptr service); const QString groupsData(); bool loadGroups(const QString &groupsData); /* @brief returns true if clip is in temporary selection group. */ bool isInMultiSelection(int cid) const; bool isSelected(int cid) const; virtual void _beginRemoveRows(const QModelIndex &, int, int) override; virtual void _beginInsertRows(const QModelIndex &, int, int) override; virtual void _endRemoveRows() override; virtual void _endInsertRows() override; virtual void _resetView() override; protected: // This is an helper function that finishes a construction of a freshly created TimelineItemModel - static void finishConstruct(std::shared_ptr ptr, std::shared_ptr guideModel); + static void finishConstruct(const std::shared_ptr &ptr, const std::shared_ptr &guideModel); }; #endif diff --git a/src/timeline2/model/timelinemodel.cpp b/src/timeline2/model/timelinemodel.cpp index 9aaf2240e..e286eee89 100644 --- a/src/timeline2/model/timelinemodel.cpp +++ b/src/timeline2/model/timelinemodel.cpp @@ -1,2790 +1,2790 @@ /*************************************************************************** * 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 "timelinemodel.hpp" #include "assets/model/assetparametermodel.hpp" #include "bin/projectclip.h" #include "bin/projectitemmodel.h" #include "clipmodel.hpp" #include "compositionmodel.hpp" #include "core.h" #include "doc/docundostack.hpp" #include "effects/effectsrepository.hpp" #include "groupsmodel.hpp" #include "kdenlivesettings.h" #include "logger.hpp" #include "snapmodel.hpp" #include "timelinefunctions.hpp" #include "trackmodel.hpp" #include #include #include #include #include #include #include #include #include #include "macros.hpp" #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-parameter" #pragma GCC diagnostic ignored "-Wsign-conversion" #pragma GCC diagnostic ignored "-Wfloat-equal" #pragma GCC diagnostic ignored "-Wshadow" #pragma GCC diagnostic ignored "-Wpedantic" #include #pragma GCC diagnostic pop RTTR_REGISTRATION { using namespace rttr; registration::class_("TimelineModel") .method("requestClipMove", select_overload(&TimelineModel::requestClipMove))( parameter_names("clipId", "trackId", "position", "updateView", "logUndo", "invalidateTimeline")) .method("requestCompositionMove", select_overload(&TimelineModel::requestCompositionMove))( parameter_names("compoId", "trackId", "position", "updateView", "logUndo")) .method("requestClipInsertion", select_overload(&TimelineModel::requestClipInsertion))( parameter_names("binClipId", "trackId", "position", "id", "logUndo", "refreshView", "useTargets")) .method("requestItemDeletion", select_overload(&TimelineModel::requestItemDeletion))(parameter_names("clipId", "logUndo")) .method("requestGroupMove", select_overload(&TimelineModel::requestGroupMove))( parameter_names("clipId", "groupId", "delta_track", "delta_pos", "updateView", "logUndo")) .method("requestGroupDeletion", select_overload(&TimelineModel::requestGroupDeletion))(parameter_names("clipId", "logUndo")) .method("requestItemResize", select_overload(&TimelineModel::requestItemResize))( parameter_names("itemId", "size", "right", "logUndo", "snapDistance", "allowSingleResize")) .method("requestClipsGroup", select_overload &, bool, GroupType)>(&TimelineModel::requestClipsGroup))( parameter_names("ids", "logUndo", "type")) .method("requestClipUngroup", select_overload(&TimelineModel::requestClipUngroup))(parameter_names("itemId", "logUndo")) .method("requestTrackInsertion", select_overload(&TimelineModel::requestTrackInsertion))( parameter_names("pos", "id", "trackName", "audioTrack")) .method("requestTrackDeletion", select_overload(&TimelineModel::requestTrackDeletion))(parameter_names("trackId")); } int TimelineModel::next_id = 0; int TimelineModel::seekDuration = 30000; TimelineModel::TimelineModel(Mlt::Profile *profile, std::weak_ptr undo_stack) : QAbstractItemModel_shared_from_this() , m_tractor(new Mlt::Tractor(*profile)) , m_snaps(new SnapModel()) - , m_undoStack(undo_stack) + , m_undoStack(std::move(undo_stack)) , m_profile(profile) , m_blackClip(new Mlt::Producer(*profile, "color:black")) , m_lock(QReadWriteLock::Recursive) , m_timelineEffectsEnabled(true) , m_id(getNextId()) , m_temporarySelectionGroup(-1) , m_overlayTrackCount(-1) , m_audioTarget(-1) , m_videoTarget(-1) , m_editMode(TimelineMode::NormalEdit) , m_blockRefresh(false) { // Create black background track m_blackClip->set("id", "black_track"); m_blackClip->set("mlt_type", "producer"); m_blackClip->set("aspect_ratio", 1); m_blackClip->set("length", INT_MAX); m_blackClip->set("set.test_audio", 0); m_blackClip->set("length", INT_MAX); m_blackClip->set_in_and_out(0, TimelineModel::seekDuration); m_tractor->insert_track(*m_blackClip, 0); TRACE_CONSTR(this); } TimelineModel::~TimelineModel() { std::vector all_ids; for (auto tracks : m_iteratorTable) { all_ids.push_back(tracks.first); } for (auto tracks : all_ids) { deregisterTrack_lambda(tracks, false)(); } for (const auto &clip : m_allClips) { clip.second->deregisterClipToBin(); } } int TimelineModel::getTracksCount() const { READ_LOCK(); int count = m_tractor->count(); if (m_overlayTrackCount > -1) { count -= m_overlayTrackCount; } Q_ASSERT(count >= 0); // don't count the black background track Q_ASSERT(count - 1 == static_cast(m_allTracks.size())); return count - 1; } int TimelineModel::getTrackIndexFromPosition(int pos) const { Q_ASSERT(pos >= 0 && pos < (int)m_allTracks.size()); READ_LOCK(); auto it = m_allTracks.begin(); while (pos > 0) { it++; pos--; } return (*it)->getId(); } int TimelineModel::getClipsCount() const { READ_LOCK(); int size = int(m_allClips.size()); return size; } int TimelineModel::getCompositionsCount() const { READ_LOCK(); int size = int(m_allCompositions.size()); return size; } int TimelineModel::getClipTrackId(int clipId) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); const auto clip = m_allClips.at(clipId); return clip->getCurrentTrackId(); } int TimelineModel::getCompositionTrackId(int compoId) const { Q_ASSERT(m_allCompositions.count(compoId) > 0); const auto trans = m_allCompositions.at(compoId); return trans->getCurrentTrackId(); } int TimelineModel::getItemTrackId(int itemId) const { READ_LOCK(); Q_ASSERT(isClip(itemId) || isComposition(itemId)); if (isComposition(itemId)) { return getCompositionTrackId(itemId); } return getClipTrackId(itemId); } int TimelineModel::getClipPosition(int clipId) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); const auto clip = m_allClips.at(clipId); int pos = clip->getPosition(); return pos; } double TimelineModel::getClipSpeed(int clipId) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); return m_allClips.at(clipId)->getSpeed(); } int TimelineModel::getClipSplitPartner(int clipId) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); return m_groups->getSplitPartner(clipId); } int TimelineModel::getClipIn(int clipId) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); const auto clip = m_allClips.at(clipId); return clip->getIn(); } PlaylistState::ClipState TimelineModel::getClipState(int clipId) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); const auto clip = m_allClips.at(clipId); return clip->clipState(); } const QString TimelineModel::getClipBinId(int clipId) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); const auto clip = m_allClips.at(clipId); QString id = clip->binId(); return id; } int TimelineModel::getClipPlaytime(int clipId) const { READ_LOCK(); Q_ASSERT(isClip(clipId)); const auto clip = m_allClips.at(clipId); int playtime = clip->getPlaytime(); return playtime; } QSize TimelineModel::getClipFrameSize(int clipId) const { READ_LOCK(); Q_ASSERT(isClip(clipId)); const auto clip = m_allClips.at(clipId); return clip->getFrameSize(); } int TimelineModel::getTrackClipsCount(int trackId) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); int count = getTrackById_const(trackId)->getClipsCount(); return count; } int TimelineModel::getClipByPosition(int trackId, int position) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); return getTrackById_const(trackId)->getClipByPosition(position); } int TimelineModel::getCompositionByPosition(int trackId, int position) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); return getTrackById_const(trackId)->getCompositionByPosition(position); } int TimelineModel::getTrackPosition(int trackId) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); auto it = m_allTracks.begin(); int pos = (int)std::distance(it, (decltype(it))m_iteratorTable.at(trackId)); return pos; } int TimelineModel::getTrackMltIndex(int trackId) const { READ_LOCK(); // Because of the black track that we insert in first position, the mlt index is the position + 1 return getTrackPosition(trackId) + 1; } int TimelineModel::getTrackSortValue(int trackId, bool separated) const { if (separated) { return getTrackPosition(trackId) + 1; } auto it = m_allTracks.end(); int aCount = 0; int vCount = 0; bool isAudio = false; int trackPos = 0; while (it != m_allTracks.begin()) { --it; bool audioTrack = (*it)->isAudioTrack(); if (audioTrack) { aCount++; } else { vCount++; } if (trackId == (*it)->getId()) { isAudio = audioTrack; trackPos = audioTrack ? aCount : vCount; } } int trackDiff = aCount - vCount; if (trackDiff > 0) { // more audio tracks if (!isAudio) { trackPos -= trackDiff; } else if (trackPos > vCount) { return -trackPos; } } return isAudio ? ((aCount * trackPos) - 1) : (vCount + 1 - trackPos) * 2; } QList TimelineModel::getLowerTracksId(int trackId, TrackType type) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); QList results; auto it = m_iteratorTable.at(trackId); while (it != m_allTracks.begin()) { --it; if (type == TrackType::AnyTrack) { results << (*it)->getId(); continue; } bool audioTrack = (*it)->isAudioTrack(); if (type == TrackType::AudioTrack && audioTrack) { results << (*it)->getId(); } else if (type == TrackType::VideoTrack && !audioTrack) { results << (*it)->getId(); } } return results; } int TimelineModel::getPreviousVideoTrackIndex(int trackId) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); auto it = m_iteratorTable.at(trackId); while (it != m_allTracks.begin()) { --it; if (it != m_allTracks.begin() && !(*it)->isAudioTrack()) { break; } } return it == m_allTracks.begin() ? 0 : (*it)->getId(); } int TimelineModel::getPreviousVideoTrackPos(int trackId) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); auto it = m_iteratorTable.at(trackId); while (it != m_allTracks.begin()) { --it; if (it != m_allTracks.begin() && !(*it)->isAudioTrack()) { break; } } return it == m_allTracks.begin() ? 0 : getTrackMltIndex((*it)->getId()); } int TimelineModel::getMirrorVideoTrackId(int trackId) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); auto it = m_iteratorTable.at(trackId); if (!(*it)->isAudioTrack()) { // we expected an audio track... return -1; } int count = 0; if (it != m_allTracks.end()) { ++it; } while (it != m_allTracks.end()) { if ((*it)->isAudioTrack()) { count++; } else { if (count == 0) { return (*it)->getId(); } count--; } ++it; } if (!(*it)->isAudioTrack() && count == 0) { return (*it)->getId(); } return -1; } int TimelineModel::getMirrorTrackId(int trackId) const { if (isAudioTrack(trackId)) { return getMirrorVideoTrackId(trackId); } return getMirrorAudioTrackId(trackId); } int TimelineModel::getMirrorAudioTrackId(int trackId) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); auto it = m_iteratorTable.at(trackId); if ((*it)->isAudioTrack()) { // we expected a video track... return -1; } int count = 0; if (it != m_allTracks.begin()) { --it; } while (it != m_allTracks.begin()) { if (!(*it)->isAudioTrack()) { count++; } else { if (count == 0) { return (*it)->getId(); } count--; } --it; } if ((*it)->isAudioTrack() && count == 0) { return (*it)->getId(); } return -1; } void TimelineModel::setEditMode(TimelineMode::EditMode mode) { m_editMode = mode; } bool TimelineModel::normalEdit() const { return m_editMode == TimelineMode::NormalEdit; } bool TimelineModel::fakeClipMove(int clipId, int trackId, int position, bool updateView, bool invalidateTimeline, Fun &undo, Fun &redo) { Q_UNUSED(updateView); Q_UNUSED(invalidateTimeline); Q_UNUSED(undo); Q_UNUSED(redo); Q_ASSERT(isClip(clipId)); m_allClips[clipId]->setFakePosition(position); bool trackChanged = false; if (trackId > -1) { if (trackId != m_allClips[clipId]->getFakeTrackId()) { if (getTrackById_const(trackId)->trackType() == m_allClips[clipId]->clipState()) { m_allClips[clipId]->setFakeTrackId(trackId); trackChanged = true; } } } QModelIndex modelIndex = makeClipIndexFromID(clipId); if (modelIndex.isValid()) { QVector roles{FakePositionRole}; if (trackChanged) { roles << FakeTrackIdRole; } notifyChange(modelIndex, modelIndex, roles); return true; } return false; } bool TimelineModel::requestClipMove(int clipId, int trackId, int position, bool updateView, bool invalidateTimeline, Fun &undo, Fun &redo) { // qDebug() << "// FINAL MOVE: " << invalidateTimeline << ", UPDATE VIEW: " << updateView; if (trackId == -1) { return false; } Q_ASSERT(isClip(clipId)); if (m_allClips[clipId]->clipState() == PlaylistState::Disabled) { if (getTrackById_const(trackId)->trackType() == PlaylistState::AudioOnly && !m_allClips[clipId]->canBeAudio()) { return false; } if (getTrackById_const(trackId)->trackType() == PlaylistState::VideoOnly && !m_allClips[clipId]->canBeVideo()) { return false; } } else if (getTrackById_const(trackId)->trackType() != m_allClips[clipId]->clipState()) { // Move not allowed (audio / video mismatch) qDebug() << "// CLIP MISMATCH: " << getTrackById_const(trackId)->trackType() << " == " << m_allClips[clipId]->clipState(); return false; } std::function local_undo = []() { return true; }; std::function local_redo = []() { return true; }; bool ok = true; int old_trackId = getClipTrackId(clipId); bool notifyViewOnly = false; bool localUpdateView = updateView; // qDebug()<<"MOVING CLIP FROM: "< 0); if (m_allClips[clipId]->getPosition() == position && getClipTrackId(clipId) == trackId) { return true; } if (m_groups->isInGroup(clipId)) { // element is in a group. int groupId = m_groups->getRootId(clipId); int current_trackId = getClipTrackId(clipId); int track_pos1 = getTrackPosition(trackId); int track_pos2 = getTrackPosition(current_trackId); int delta_track = track_pos1 - track_pos2; int delta_pos = position - m_allClips[clipId]->getPosition(); return requestFakeGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo); } std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool res = fakeClipMove(clipId, trackId, position, updateView, invalidateTimeline, undo, redo); if (res && logUndo) { PUSH_UNDO(undo, redo, i18n("Move clip")); } return res; } bool TimelineModel::requestClipMove(int clipId, int trackId, int position, bool updateView, bool logUndo, bool invalidateTimeline) { QWriteLocker locker(&m_lock); TRACE(clipId, trackId, position, updateView, logUndo, invalidateTimeline); Q_ASSERT(m_allClips.count(clipId) > 0); if (m_allClips[clipId]->getPosition() == position && getClipTrackId(clipId) == trackId) { TRACE_RES(true); return true; } if (m_groups->isInGroup(clipId)) { // element is in a group. int groupId = m_groups->getRootId(clipId); int current_trackId = getClipTrackId(clipId); int track_pos1 = getTrackPosition(trackId); int track_pos2 = getTrackPosition(current_trackId); int delta_track = track_pos1 - track_pos2; int delta_pos = position - m_allClips[clipId]->getPosition(); return requestGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo); } std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool res = requestClipMove(clipId, trackId, position, updateView, invalidateTimeline, undo, redo); if (res && logUndo) { PUSH_UNDO(undo, redo, i18n("Move clip")); } TRACE_RES(res); return res; } bool TimelineModel::requestClipMoveAttempt(int clipId, int trackId, int position) { QWriteLocker locker(&m_lock); Q_ASSERT(m_allClips.count(clipId) > 0); if (m_allClips[clipId]->getPosition() == position && getClipTrackId(clipId) == trackId) { return true; } std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool res = true; if (m_groups->isInGroup(clipId)) { // element is in a group. int groupId = m_groups->getRootId(clipId); int current_trackId = getClipTrackId(clipId); int track_pos1 = getTrackPosition(trackId); int track_pos2 = getTrackPosition(current_trackId); int delta_track = track_pos1 - track_pos2; int delta_pos = position - m_allClips[clipId]->getPosition(); res = requestGroupMove(clipId, groupId, delta_track, delta_pos, false, false, undo, redo, false); } else { res = requestClipMove(clipId, trackId, position, false, false, undo, redo); } if (res) { undo(); } return res; } int TimelineModel::suggestItemMove(int itemId, int trackId, int position, int cursorPosition, int snapDistance) { if (isClip(itemId)) { return suggestClipMove(itemId, trackId, position, cursorPosition, snapDistance); } return suggestCompositionMove(itemId, trackId, position, cursorPosition, snapDistance); } int TimelineModel::suggestClipMove(int clipId, int trackId, int position, int cursorPosition, int snapDistance, bool allowViewUpdate) { QWriteLocker locker(&m_lock); Q_ASSERT(isClip(clipId)); Q_ASSERT(isTrack(trackId)); int currentPos = getClipPosition(clipId); int sourceTrackId = getClipTrackId(clipId); if (sourceTrackId > -1 && getTrackById_const(trackId)->isAudioTrack() != getTrackById_const(sourceTrackId)->isAudioTrack()) { // Trying move on incompatible track type, stay on same track trackId = sourceTrackId; } if (currentPos == position && sourceTrackId == trackId) { return position; } bool after = position > currentPos; if (snapDistance > 0) { // For snapping, we must ignore all in/outs of the clips of the group being moved std::vector ignored_pts; std::unordered_set all_items = {clipId}; if (m_groups->isInGroup(clipId)) { int groupId = m_groups->getRootId(clipId); all_items = m_groups->getLeaves(groupId); } for (int current_clipId : all_items) { if (getItemTrackId(current_clipId) != -1) { int in = getItemPosition(current_clipId); int out = in + getItemPlaytime(current_clipId); ignored_pts.push_back(in); ignored_pts.push_back(out); } } int snapped = requestBestSnapPos(position, m_allClips[clipId]->getPlaytime(), m_editMode == TimelineMode::NormalEdit ? ignored_pts : std::vector(), cursorPosition, snapDistance); // qDebug() << "Starting suggestion " << clipId << position << currentPos << "snapped to " << snapped; if (snapped >= 0) { position = snapped; } } // we check if move is possible bool possible = m_editMode == TimelineMode::NormalEdit ? requestClipMove(clipId, trackId, position, true, false, false) : requestFakeClipMove(clipId, trackId, position, true, false, false); /*} else { possible = requestClipMoveAttempt(clipId, trackId, position); }*/ if (possible) { return position; } // Find best possible move if (!m_groups->isInGroup(clipId)) { // Try same track move if (trackId != sourceTrackId) { qDebug() << "// TESTING SAME TRACVK MOVE: " << trackId << " = " << sourceTrackId; trackId = sourceTrackId; possible = requestClipMove(clipId, trackId, position, true, false, false); if (!possible) { qDebug() << "CANNOT MOVE CLIP : " << clipId << " ON TK: " << trackId << ", AT POS: " << position; } else { return position; } } int blank_length = getTrackById(trackId)->getBlankSizeNearClip(clipId, after); qDebug() << "Found blank" << blank_length; if (blank_length < INT_MAX) { if (after) { position = currentPos + blank_length; } else { position = currentPos - blank_length; } } else { return currentPos; } possible = requestClipMove(clipId, trackId, position, true, false, false); return possible ? position : currentPos; } // find best pos for groups int groupId = m_groups->getRootId(clipId); std::unordered_set all_items = m_groups->getLeaves(groupId); QMap trackPosition; // First pass, sort clips by track and keep only the first / last depending on move direction for (int current_clipId : all_items) { int clipTrack = getItemTrackId(current_clipId); if (clipTrack == -1) { continue; } int in = getItemPosition(current_clipId); if (trackPosition.contains(clipTrack)) { if (after) { // keep only last clip position for track int out = in + getItemPlaytime(current_clipId); if (trackPosition.value(clipTrack) < out) { trackPosition.insert(clipTrack, out); } } else { // keep only first clip position for track if (trackPosition.value(clipTrack) > in) { trackPosition.insert(clipTrack, in); } } } else { trackPosition.insert(clipTrack, after ? in + getItemPlaytime(current_clipId) : in); } } // Now check space on each track QMapIterator i(trackPosition); int blank_length = -1; while (i.hasNext()) { i.next(); int track_space; if (!after) { // Check space before the position track_space = i.value() - getTrackById(i.key())->getBlankStart(i.value() - 1); if (blank_length == -1 || blank_length > track_space) { blank_length = track_space; } } else { // Check space after the position track_space = getTrackById(i.key())->getBlankEnd(i.value() + 1) - i.value() - 1; if (blank_length == -1 || blank_length > track_space) { blank_length = track_space; } } } if (blank_length != 0) { int updatedPos = currentPos + (after ? blank_length : -blank_length); possible = requestClipMove(clipId, trackId, updatedPos, true, false, false); if (possible) { return updatedPos; } } return currentPos; } int TimelineModel::suggestCompositionMove(int compoId, int trackId, int position, int cursorPosition, int snapDistance) { QWriteLocker locker(&m_lock); Q_ASSERT(isComposition(compoId)); Q_ASSERT(isTrack(trackId)); int currentPos = getCompositionPosition(compoId); int currentTrack = getCompositionTrackId(compoId); if (getTrackById_const(trackId)->isAudioTrack()) { // Trying move on incompatible track type, stay on same track trackId = currentTrack; } if (currentPos == position && currentTrack == trackId) { return position; } if (snapDistance > 0) { // For snapping, we must ignore all in/outs of the clips of the group being moved std::vector ignored_pts; if (m_groups->isInGroup(compoId)) { int groupId = m_groups->getRootId(compoId); auto all_items = m_groups->getLeaves(groupId); for (int current_compoId : all_items) { // TODO: fix for composition int in = getItemPosition(current_compoId); int out = in + getItemPlaytime(current_compoId); ignored_pts.push_back(in); ignored_pts.push_back(out); } } else { int in = currentPos; int out = in + getCompositionPlaytime(compoId); qDebug() << " * ** IGNORING SNAP PTS: " << in << "-" << out; ignored_pts.push_back(in); ignored_pts.push_back(out); } int snapped = requestBestSnapPos(position, m_allCompositions[compoId]->getPlaytime(), ignored_pts, cursorPosition, snapDistance); qDebug() << "Starting suggestion " << compoId << position << currentPos << "snapped to " << snapped; if (snapped >= 0) { position = snapped; } } // we check if move is possible bool possible = requestCompositionMove(compoId, trackId, position, true, false); qDebug() << "Original move success" << possible; if (possible) { return position; } /*bool after = position > currentPos; int blank_length = getTrackById(trackId)->getBlankSizeNearComposition(compoId, after); qDebug() << "Found blank" << blank_length; if (blank_length < INT_MAX) { if (after) { return currentPos + blank_length; } return currentPos - blank_length; } return position;*/ return currentPos; } bool TimelineModel::requestClipCreation(const QString &binClipId, int &id, PlaylistState::ClipState state, double speed, Fun &undo, Fun &redo) { qDebug() << "requestClipCreation " << binClipId; QString bid = binClipId; if (binClipId.contains(QLatin1Char('/'))) { bid = binClipId.section(QLatin1Char('/'), 0, 0); } if (!pCore->projectItemModel()->hasClip(bid)) { qDebug() << " / / / /MASTER CLIP NOT FOUND"; return false; } std::shared_ptr master = pCore->projectItemModel()->getClipByBinID(bid); if (!master->isReady() || !master->isCompatible(state)) { return false; } int clipId = TimelineModel::getNextId(); id = clipId; Fun local_undo = deregisterClip_lambda(clipId); ClipModel::construct(shared_from_this(), bid, clipId, state, speed); auto clip = m_allClips[clipId]; Fun local_redo = [clip, this, state]() { // We capture a shared_ptr to the clip, which means that as long as this undo object lives, the clip object is not deleted. To insert it back it is // sufficient to register it. registerClip(clip, true); clip->refreshProducerFromBin(state); return true; }; if (binClipId.contains(QLatin1Char('/'))) { int in = binClipId.section(QLatin1Char('/'), 1, 1).toInt(); int out = binClipId.section(QLatin1Char('/'), 2, 2).toInt(); int initLength = m_allClips[clipId]->getPlaytime(); bool res = true; if (in != 0) { res = requestItemResize(clipId, initLength - in, false, true, local_undo, local_redo); } res = res && requestItemResize(clipId, out - in + 1, true, true, local_undo, local_redo); if (!res) { bool undone = local_undo(); Q_ASSERT(undone); return false; } } UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } bool TimelineModel::requestClipInsertion(const QString &binClipId, int trackId, int position, int &id, bool logUndo, bool refreshView, bool useTargets) { QWriteLocker locker(&m_lock); TRACE(binClipId, trackId, position, id, logUndo, refreshView, useTargets); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool result = requestClipInsertion(binClipId, trackId, position, id, logUndo, refreshView, useTargets, undo, redo); if (result && logUndo) { PUSH_UNDO(undo, redo, i18n("Insert Clip")); } TRACE_RES(result); return result; } bool TimelineModel::requestClipInsertion(const QString &binClipId, int trackId, int position, int &id, bool logUndo, bool refreshView, bool useTargets, Fun &undo, Fun &redo) { std::function local_undo = []() { return true; }; std::function local_redo = []() { return true; }; qDebug() << "requestClipInsertion " << binClipId << " " << " " << trackId << " " << position; bool res = false; ClipType::ProducerType type = ClipType::Unknown; QString bid = binClipId.section(QLatin1Char('/'), 0, 0); // dropType indicates if we want a normal drop (disabled), audio only or video only drop PlaylistState::ClipState dropType = PlaylistState::Disabled; if (bid.startsWith(QLatin1Char('A'))) { dropType = PlaylistState::AudioOnly; bid = bid.remove(0, 1); } else if (bid.startsWith(QLatin1Char('V'))) { dropType = PlaylistState::VideoOnly; bid = bid.remove(0, 1); } if (!pCore->projectItemModel()->hasClip(bid)) { return false; } std::shared_ptr master = pCore->projectItemModel()->getClipByBinID(bid); type = master->clipType(); if (useTargets && m_audioTarget == -1 && m_videoTarget == -1) { useTargets = false; } if (dropType == PlaylistState::Disabled && (type == ClipType::AV || type == ClipType::Playlist)) { if (m_audioTarget >= 0 && m_videoTarget == -1 && useTargets) { // If audio target is set but no video target, only insert audio trackId = m_audioTarget; } bool audioDrop = getTrackById_const(trackId)->isAudioTrack(); res = requestClipCreation(binClipId, id, getTrackById_const(trackId)->trackType(), 1.0, local_undo, local_redo); res = res && requestClipMove(id, trackId, position, refreshView, logUndo, local_undo, local_redo); int target_track = audioDrop ? m_videoTarget : m_audioTarget; qDebug() << "CLIP HAS A+V: " << master->hasAudioAndVideo(); if (res && (!useTargets || target_track > -1) && master->hasAudioAndVideo()) { if (!useTargets) { target_track = audioDrop ? getMirrorVideoTrackId(trackId) : getMirrorAudioTrackId(trackId); } // QList possibleTracks = m_audioTarget >= 0 ? QList() << m_audioTarget : getLowerTracksId(trackId, TrackType::AudioTrack); QList possibleTracks; qDebug() << "CREATING SPLIT " << target_track << " usetargets" << useTargets; if (target_track >= 0 && !getTrackById_const(target_track)->isLocked()) { possibleTracks << target_track; } if (possibleTracks.isEmpty()) { // No available audio track for splitting, abort pCore->displayMessage(i18n("No available track for split operation"), ErrorMessage); res = false; } else { std::function audio_undo = []() { return true; }; std::function audio_redo = []() { return true; }; int newId; res = requestClipCreation(binClipId, newId, audioDrop ? PlaylistState::VideoOnly : PlaylistState::AudioOnly, 1.0, audio_undo, audio_redo); if (res) { bool move = false; while (!move && !possibleTracks.isEmpty()) { int newTrack = possibleTracks.takeFirst(); move = requestClipMove(newId, newTrack, position, true, false, audio_undo, audio_redo); } // use lazy evaluation to group only if move was successful res = res && move && requestClipsGroup({id, newId}, audio_undo, audio_redo, GroupType::AVSplit); if (!res || !move) { pCore->displayMessage(i18n("Audio split failed: no viable track"), ErrorMessage); bool undone = audio_undo(); Q_ASSERT(undone); } else { UPDATE_UNDO_REDO(audio_redo, audio_undo, local_undo, local_redo); } } else { pCore->displayMessage(i18n("Audio split failed: impossible to create audio clip"), ErrorMessage); bool undone = audio_undo(); Q_ASSERT(undone); } } } } else { std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(bid); if (dropType == PlaylistState::Disabled) { dropType = getTrackById_const(trackId)->trackType(); } else if (dropType != getTrackById_const(trackId)->trackType()) { qDebug() << "// INCORRECT DRAG, ABORTING"; return false; } QString normalisedBinId = binClipId; if (normalisedBinId.startsWith(QLatin1Char('A')) || normalisedBinId.startsWith(QLatin1Char('V'))) { normalisedBinId.remove(0, 1); } res = requestClipCreation(normalisedBinId, id, dropType, 1.0, local_undo, local_redo); res = res && requestClipMove(id, trackId, position, refreshView, logUndo, local_undo, local_redo); } if (!res) { bool undone = local_undo(); Q_ASSERT(undone); id = -1; return false; } UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } bool TimelineModel::requestItemDeletion(int clipId, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); if (m_groups->isInGroup(clipId)) { return requestGroupDeletion(clipId, undo, redo); } return requestClipDeletion(clipId, undo, redo); } bool TimelineModel::requestItemDeletion(int itemId, bool logUndo) { QWriteLocker locker(&m_lock); TRACE(itemId, logUndo); Q_ASSERT(isClip(itemId) || isComposition(itemId)); if (m_groups->isInGroup(itemId)) { bool res = requestGroupDeletion(itemId, logUndo); TRACE_RES(res); return res; } Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool res = false; QString actionLabel; if (isClip(itemId)) { actionLabel = i18n("Delete Clip"); res = requestClipDeletion(itemId, undo, redo); } else { actionLabel = i18n("Delete Composition"); res = requestCompositionDeletion(itemId, undo, redo); } if (res && logUndo) { PUSH_UNDO(undo, redo, actionLabel); } TRACE_RES(res); return res; } bool TimelineModel::requestClipDeletion(int clipId, Fun &undo, Fun &redo) { int trackId = getClipTrackId(clipId); if (trackId != -1) { bool res = getTrackById(trackId)->requestClipDeletion(clipId, true, true, undo, redo); if (!res) { undo(); return false; } } auto operation = deregisterClip_lambda(clipId); auto clip = m_allClips[clipId]; Fun reverse = [this, clip]() { // We capture a shared_ptr to the clip, which means that as long as this undo object lives, the clip object is not deleted. To insert it back it is // sufficient to register it. registerClip(clip, true); return true; }; if (operation()) { emit removeFromSelection(clipId); UPDATE_UNDO_REDO(operation, reverse, undo, redo); return true; } undo(); return false; } bool TimelineModel::requestCompositionDeletion(int compositionId, Fun &undo, Fun &redo) { int trackId = getCompositionTrackId(compositionId); if (trackId != -1) { bool res = getTrackById(trackId)->requestCompositionDeletion(compositionId, true, true, undo, redo); if (!res) { undo(); return false; } else { unplantComposition(compositionId); } } Fun operation = deregisterComposition_lambda(compositionId); auto composition = m_allCompositions[compositionId]; Fun reverse = [this, composition]() { // We capture a shared_ptr to the composition, which means that as long as this undo object lives, the composition object is not deleted. To insert it // back it is sufficient to register it. registerComposition(composition); return true; }; if (operation()) { emit removeFromSelection(compositionId); UPDATE_UNDO_REDO(operation, reverse, undo, redo); return true; } undo(); return false; } std::unordered_set TimelineModel::getItemsInRange(int trackId, int start, int end, bool listCompositions) { Q_UNUSED(listCompositions) std::unordered_set allClips; if (trackId == -1) { for (const auto &track : m_allTracks) { std::unordered_set clipTracks = getItemsInRange(track->getId(), start, end, listCompositions); allClips.insert(clipTracks.begin(), clipTracks.end()); } } else { std::unordered_set clipTracks = getTrackById(trackId)->getClipsInRange(start, end); allClips.insert(clipTracks.begin(), clipTracks.end()); if (listCompositions) { std::unordered_set compoTracks = getTrackById(trackId)->getCompositionsInRange(start, end); allClips.insert(compoTracks.begin(), compoTracks.end()); } } return allClips; } bool TimelineModel::requestFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool logUndo) { std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool res = requestFakeGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo, undo, redo); if (res && logUndo) { PUSH_UNDO(undo, redo, i18n("Move group")); } return res; } bool TimelineModel::requestFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool finalMove, Fun &undo, Fun &redo, bool allowViewRefresh) { Q_UNUSED(updateView); Q_UNUSED(finalMove); Q_UNUSED(undo); Q_UNUSED(redo); Q_UNUSED(allowViewRefresh); QWriteLocker locker(&m_lock); Q_ASSERT(m_allGroups.count(groupId) > 0); bool ok = true; auto all_items = m_groups->getLeaves(groupId); Q_ASSERT(all_items.size() > 1); Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; // Moving groups is a two stage process: first we remove the clips from the tracks, and then try to insert them back at their calculated new positions. // This way, we ensure that no conflict will arise with clips inside the group being moved Fun update_model = []() { return true; }; // Check if there is a track move // First, remove clips std::unordered_map old_track_ids, old_position, old_forced_track; for (int item : all_items) { int old_trackId = getItemTrackId(item); old_track_ids[item] = old_trackId; if (old_trackId != -1) { if (isClip(item)) { old_position[item] = m_allClips[item]->getPosition(); } else { old_position[item] = m_allCompositions[item]->getPosition(); old_forced_track[item] = m_allCompositions[item]->getForcedTrack(); } } } // Second step, calculate delta int audio_delta, video_delta; audio_delta = video_delta = delta_track; if (getTrackById(old_track_ids[clipId])->isAudioTrack()) { // Master clip is audio, so reverse delta for video clips video_delta = -delta_track; } else { audio_delta = -delta_track; } bool trackChanged = false; // Reverse sort. We need to insert from left to right to avoid confusing the view for (int item : all_items) { int current_track_id = old_track_ids[item]; int current_track_position = getTrackPosition(current_track_id); int d = getTrackById(current_track_id)->isAudioTrack() ? audio_delta : video_delta; int target_track_position = current_track_position + d; if (target_track_position >= 0 && target_track_position < getTracksCount()) { auto it = m_allTracks.cbegin(); std::advance(it, target_track_position); int target_track = (*it)->getId(); int target_position = old_position[item] + delta_pos; if (isClip(item)) { qDebug() << "/// SETTING FAKE CLIP: " << target_track << ", POSITION: " << target_position; m_allClips[item]->setFakePosition(target_position); if (m_allClips[item]->getFakeTrackId() != target_track) { trackChanged = true; } m_allClips[item]->setFakeTrackId(target_track); } else { } } else { qDebug() << "// ABORTING; MOVE TRIED ON TRACK: " << target_track_position << "..\n..\n.."; ok = false; } if (!ok) { bool undone = local_undo(); Q_ASSERT(undone); return false; } } QModelIndex modelIndex; QVector roles{FakePositionRole}; if (trackChanged) { roles << FakeTrackIdRole; } for (int item : all_items) { if (isClip(item)) { modelIndex = makeClipIndexFromID(item); } else { modelIndex = makeCompositionIndexFromID(item); } notifyChange(modelIndex, modelIndex, roles); } return true; } bool TimelineModel::requestGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool logUndo) { QWriteLocker locker(&m_lock); TRACE(clipId, groupId, delta_track, delta_pos, updateView, logUndo); std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool res = requestGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo, undo, redo); if (res && logUndo) { PUSH_UNDO(undo, redo, i18n("Move group")); } TRACE_RES(res); return res; } bool TimelineModel::requestGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool finalMove, Fun &undo, Fun &redo, bool allowViewRefresh) { QWriteLocker locker(&m_lock); Q_ASSERT(m_allGroups.count(groupId) > 0); bool ok = true; auto all_items = m_groups->getLeaves(groupId); Q_ASSERT(all_items.size() > 1); Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; // Sort clips. We need to delete from right to left to avoid confusing the view, and compositions from top to bottom std::vector sorted_clips(all_items.begin(), all_items.end()); std::sort(sorted_clips.begin(), sorted_clips.end(), [this, delta_track](int clipId1, int clipId2) { int p1 = isClip(clipId1) ? m_allClips[clipId1]->getPosition() : delta_track < 0 ? getTrackMltIndex(m_allCompositions[clipId1]->getCurrentTrackId()) : delta_track > 0 ? -getTrackMltIndex(m_allCompositions[clipId1]->getCurrentTrackId()) : m_allCompositions[clipId1]->getPosition(); int p2 = isClip(clipId2) ? m_allClips[clipId2]->getPosition() : delta_track < 0 ? getTrackMltIndex(m_allCompositions[clipId2]->getCurrentTrackId()) : delta_track > 0 ? -getTrackMltIndex(m_allCompositions[clipId2]->getCurrentTrackId()) : m_allCompositions[clipId2]->getPosition(); return p2 <= p1; }); // Moving groups is a two stage process: first we remove the clips from the tracks, and then try to insert them back at their calculated new positions. // This way, we ensure that no conflict will arise with clips inside the group being moved Fun update_model = []() { return true; }; // Check if there is a track move bool updatePositionOnly = false; if (delta_track == 0 && updateView) { updateView = false; allowViewRefresh = false; updatePositionOnly = true; update_model = [sorted_clips, this]() { QModelIndex modelIndex; QVector roles{StartRole}; for (int item : sorted_clips) { if (isClip(item)) { modelIndex = makeClipIndexFromID(item); } else { modelIndex = makeCompositionIndexFromID(item); } notifyChange(modelIndex, modelIndex, roles); } return true; }; } // First, remove clips std::unordered_map old_track_ids, old_position, old_forced_track; for (int item : sorted_clips) { int old_trackId = getItemTrackId(item); old_track_ids[item] = old_trackId; if (old_trackId != -1) { bool updateThisView = allowViewRefresh; if (isClip(item)) { ok = ok && getTrackById(old_trackId)->requestClipDeletion(item, updateThisView, finalMove, local_undo, local_redo); old_position[item] = m_allClips[item]->getPosition(); } else { // ok = ok && getTrackById(old_trackId)->requestCompositionDeletion(item, updateThisView, finalMove, local_undo, local_redo); old_position[item] = m_allCompositions[item]->getPosition(); old_forced_track[item] = m_allCompositions[item]->getForcedTrack(); } if (!ok) { bool undone = local_undo(); Q_ASSERT(undone); return false; } } } // Second step, reinsert clips at correct positions int audio_delta, video_delta; audio_delta = video_delta = delta_track; if (getTrackById(old_track_ids[clipId])->isAudioTrack()) { // Master clip is audio, so reverse delta for video clips video_delta = -delta_track; } else { audio_delta = -delta_track; } // Reverse sort. We need to insert from left to right to avoid confusing the view std::reverse(std::begin(sorted_clips), std::end(sorted_clips)); for (int item : sorted_clips) { int current_track_id = old_track_ids[item]; int current_track_position = getTrackPosition(current_track_id); int d = getTrackById(current_track_id)->isAudioTrack() ? audio_delta : video_delta; int target_track_position = current_track_position + d; bool updateThisView = allowViewRefresh; if (target_track_position >= 0 && target_track_position < getTracksCount()) { auto it = m_allTracks.cbegin(); std::advance(it, target_track_position); int target_track = (*it)->getId(); int target_position = old_position[item] + delta_pos; if (isClip(item)) { ok = ok && requestClipMove(item, target_track, target_position, updateThisView, finalMove, local_undo, local_redo); } else { ok = ok && requestCompositionMove(item, target_track, old_forced_track[item], target_position, updateThisView, finalMove, local_undo, local_redo); } } else { qDebug() << "// ABORTING; MOVE TRIED ON TRACK: " << target_track_position << "..\n..\n.."; ok = false; } if (!ok) { bool undone = local_undo(); Q_ASSERT(undone); return false; } } if (updatePositionOnly) { update_model(); PUSH_LAMBDA(update_model, local_redo); PUSH_LAMBDA(update_model, local_undo); } UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } bool TimelineModel::requestGroupDeletion(int clipId, bool logUndo) { QWriteLocker locker(&m_lock); TRACE(clipId, logUndo); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool res = requestGroupDeletion(clipId, undo, redo); if (res && logUndo) { PUSH_UNDO(undo, redo, i18n("Remove group")); } TRACE_RES(res); return res; } bool TimelineModel::requestGroupDeletion(int clipId, Fun &undo, Fun &redo) { // we do a breadth first exploration of the group tree, ungroup (delete) every inner node, and then delete all the leaves. std::queue group_queue; group_queue.push(m_groups->getRootId(clipId)); std::unordered_set all_items; std::unordered_set all_compositions; while (!group_queue.empty()) { int current_group = group_queue.front(); if (m_temporarySelectionGroup == current_group) { m_temporarySelectionGroup = -1; } group_queue.pop(); Q_ASSERT(isGroup(current_group)); auto children = m_groups->getDirectChildren(current_group); int one_child = -1; // we need the id on any of the indices of the elements of the group for (int c : children) { if (isClip(c)) { all_items.insert(c); one_child = c; } else if (isComposition(c)) { all_compositions.insert(c); one_child = c; } else { Q_ASSERT(isGroup(c)); one_child = c; group_queue.push(c); } } if (one_child != -1) { bool res = m_groups->ungroupItem(one_child, undo, redo); if (!res) { undo(); return false; } } } for (int clip : all_items) { bool res = requestClipDeletion(clip, undo, redo); if (!res) { undo(); return false; } } for (int compo : all_compositions) { bool res = requestCompositionDeletion(compo, undo, redo); if (!res) { undo(); return false; } } return true; } int TimelineModel::requestItemResize(int itemId, int size, bool right, bool logUndo, int snapDistance, bool allowSingleResize) { if (logUndo) { qDebug() << "---------------------\n---------------------\nRESIZE W/UNDO CALLED\n++++++++++++++++\n++++"; } QWriteLocker locker(&m_lock); TRACE(itemId, size, right, logUndo, snapDistance, allowSingleResize); Q_ASSERT(isClip(itemId) || isComposition(itemId)); if (size <= 0) { TRACE_RES(-1); return -1; } int in = getItemPosition(itemId); int out = in + getItemPlaytime(itemId); if (snapDistance > 0) { Fun temp_undo = []() { return true; }; Fun temp_redo = []() { return true; }; int proposed_size = m_snaps->proposeSize(in, out, size, right, snapDistance); if (proposed_size > 0) { // only test move if proposed_size is valid bool success = false; if (isClip(itemId)) { success = m_allClips[itemId]->requestResize(proposed_size, right, temp_undo, temp_redo, false); } else { success = m_allCompositions[itemId]->requestResize(proposed_size, right, temp_undo, temp_redo, false); } if (success) { temp_undo(); // undo temp move size = proposed_size; } } } Fun undo = []() { return true; }; Fun redo = []() { return true; }; std::unordered_set all_items; if (!allowSingleResize && m_groups->isInGroup(itemId)) { int groupId = m_groups->getRootId(itemId); auto items = m_groups->getLeaves(groupId); for (int id : items) { if (id == itemId) { all_items.insert(id); continue; } int start = getItemPosition(id); int end = in + getItemPlaytime(id); if (right) { if (out == end) { all_items.insert(id); } } else if (start == in) { all_items.insert(id); } } } else { all_items.insert(itemId); } bool result = true; for (int id : all_items) { result = result && requestItemResize(id, size, right, logUndo, undo, redo); } if (!result) { bool undone = undo(); Q_ASSERT(undone); TRACE_RES(-1); return -1; } if (result && logUndo) { if (isClip(itemId)) { PUSH_UNDO(undo, redo, i18n("Resize clip")); } else { PUSH_UNDO(undo, redo, i18n("Resize composition")); } } int res = result ? size : -1; TRACE_RES(res); return res; } bool TimelineModel::requestItemResize(int itemId, int size, bool right, bool logUndo, Fun &undo, Fun &redo, bool blockUndo) { Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; Fun update_model = [itemId, right, logUndo, this]() { Q_ASSERT(isClip(itemId) || isComposition(itemId)); if (getItemTrackId(itemId) != -1) { qDebug() << "++++++++++\nRESIZING ITEM: " << itemId << "\n+++++++"; QModelIndex modelIndex = isClip(itemId) ? makeClipIndexFromID(itemId) : makeCompositionIndexFromID(itemId); notifyChange(modelIndex, modelIndex, !right, true, logUndo); } return true; }; bool result = false; if (isClip(itemId)) { result = m_allClips[itemId]->requestResize(size, right, local_undo, local_redo, logUndo); } else { Q_ASSERT(isComposition(itemId)); result = m_allCompositions[itemId]->requestResize(size, right, local_undo, local_redo, logUndo); } if (result) { if (!blockUndo) { PUSH_LAMBDA(update_model, local_undo); } PUSH_LAMBDA(update_model, local_redo); update_model(); UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); } return result; } int TimelineModel::requestClipsGroup(const std::unordered_set &ids, bool logUndo, GroupType type) { QWriteLocker locker(&m_lock); TRACE(ids, logUndo, type); Fun undo = []() { return true; }; Fun redo = []() { return true; }; if (m_temporarySelectionGroup > -1) { m_groups->destructGroupItem(m_temporarySelectionGroup); // We don't log in undo the selection changes // int firstChild = *m_groups->getDirectChildren(m_temporarySelectionGroup).begin(); // requestClipUngroup(firstChild, undo, redo); m_temporarySelectionGroup = -1; } int result = requestClipsGroup(ids, undo, redo, type); if (type == GroupType::Selection) { m_temporarySelectionGroup = result; } if (result > -1 && logUndo && type != GroupType::Selection) { PUSH_UNDO(undo, redo, i18n("Group clips")); } TRACE_RES(result); return result; } int TimelineModel::requestClipsGroup(const std::unordered_set &ids, Fun &undo, Fun &redo, GroupType type) { QWriteLocker locker(&m_lock); for (int id : ids) { if (isClip(id)) { if (getClipTrackId(id) == -1) { return -1; } } else if (isComposition(id)) { if (getCompositionTrackId(id) == -1) { return -1; } } else if (!isGroup(id)) { return -1; } } if (type == GroupType::Selection && ids.size() == 1) { // only one element selected, no group created return -1; } int groupId = m_groups->groupItems(ids, undo, redo, type); return groupId; } bool TimelineModel::requestClipUngroup(int itemId, bool logUndo) { QWriteLocker locker(&m_lock); TRACE(itemId, logUndo); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool result = true; if (itemId == m_temporarySelectionGroup) { // Delete selection group without undo Fun tmp_undo = []() { return true; }; Fun tmp_redo = []() { return true; }; requestClipUngroup(itemId, tmp_undo, tmp_redo); m_temporarySelectionGroup = -1; } else { result = requestClipUngroup(itemId, undo, redo); } if (result && logUndo) { PUSH_UNDO(undo, redo, i18n("Ungroup clips")); } TRACE_RES(result); return result; } bool TimelineModel::requestClipUngroup(int itemId, Fun &undo, Fun &redo) { return m_groups->ungroupItem(itemId, undo, redo); } bool TimelineModel::requestTrackInsertion(int position, int &id, const QString &trackName, bool audioTrack) { QWriteLocker locker(&m_lock); TRACE(position, id, trackName, audioTrack); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool result = requestTrackInsertion(position, id, trackName, audioTrack, undo, redo); if (result) { PUSH_UNDO(undo, redo, i18n("Insert Track")); } TRACE_RES(result); return result; } bool TimelineModel::requestTrackInsertion(int position, int &id, const QString &trackName, bool audioTrack, Fun &undo, Fun &redo, bool updateView) { // TODO: make sure we disable overlayTrack before inserting a track if (position == -1) { position = (int)(m_allTracks.size()); } if (position < 0 || position > (int)m_allTracks.size()) { return false; } int trackId = TimelineModel::getNextId(); id = trackId; Fun local_undo = deregisterTrack_lambda(trackId, true); TrackModel::construct(shared_from_this(), trackId, position, trackName, audioTrack); auto track = getTrackById(trackId); Fun local_redo = [track, position, updateView, this]() { // We capture a shared_ptr to the track, which means that as long as this undo object lives, the track object is not deleted. To insert it back it is // sufficient to register it. registerTrack(track, position, updateView); return true; }; UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } bool TimelineModel::requestTrackDeletion(int trackId) { // TODO: make sure we disable overlayTrack before deleting a track QWriteLocker locker(&m_lock); TRACE(trackId); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool result = requestTrackDeletion(trackId, undo, redo); if (result) { if (m_videoTarget == trackId) { m_videoTarget = -1; } if (m_audioTarget == trackId) { m_audioTarget = -1; } PUSH_UNDO(undo, redo, i18n("Delete Track")); } TRACE_RES(result); return result; } bool TimelineModel::requestTrackDeletion(int trackId, Fun &undo, Fun &redo) { Q_ASSERT(isTrack(trackId)); std::vector clips_to_delete; for (const auto &it : getTrackById(trackId)->m_allClips) { clips_to_delete.push_back(it.first); } Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; for (int clip : clips_to_delete) { bool res = true; while (res && m_groups->isInGroup(clip)) { res = requestClipUngroup(clip, local_undo, local_redo); } if (res) { res = requestClipDeletion(clip, local_undo, local_redo); } if (!res) { bool u = local_undo(); Q_ASSERT(u); return false; } } int old_position = getTrackPosition(trackId); auto operation = deregisterTrack_lambda(trackId, true); std::shared_ptr track = getTrackById(trackId); Fun reverse = [this, track, old_position]() { // We capture a shared_ptr to the track, which means that as long as this undo object lives, the track object is not deleted. To insert it back it is // sufficient to register it. registerTrack(track, old_position); return true; }; if (operation()) { UPDATE_UNDO_REDO(operation, reverse, local_undo, local_redo); UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } local_undo(); return false; } void TimelineModel::registerTrack(std::shared_ptr track, int pos, bool doInsert, bool reloadView) { // qDebug() << "REGISTER TRACK" << track->getId() << pos; int id = track->getId(); if (pos == -1) { pos = static_cast(m_allTracks.size()); } Q_ASSERT(pos >= 0); Q_ASSERT(pos <= static_cast(m_allTracks.size())); // effective insertion (MLT operation), add 1 to account for black background track if (doInsert) { int error = m_tractor->insert_track(*track, pos + 1); Q_ASSERT(error == 0); // we might need better error handling... } // we now insert in the list auto posIt = m_allTracks.begin(); std::advance(posIt, pos); auto it = m_allTracks.insert(posIt, std::move(track)); // it now contains the iterator to the inserted element, we store it Q_ASSERT(m_iteratorTable.count(id) == 0); // check that id is not used (shouldn't happen) m_iteratorTable[id] = it; if (reloadView) { // don't reload view on each track load on project opening _resetView(); } } void TimelineModel::registerClip(const std::shared_ptr &clip, bool registerProducer) { int id = clip->getId(); qDebug() << " // /REQUEST TL CLP REGSTR: " << id << "\n--------\nCLIPS COUNT: " << m_allClips.size(); Q_ASSERT(m_allClips.count(id) == 0); m_allClips[id] = clip; clip->registerClipToBin(clip->getProducer(), registerProducer); m_groups->createGroupItem(id); clip->setTimelineEffectsEnabled(m_timelineEffectsEnabled); } void TimelineModel::registerGroup(int groupId) { Q_ASSERT(m_allGroups.count(groupId) == 0); m_allGroups.insert(groupId); } Fun TimelineModel::deregisterTrack_lambda(int id, bool updateView) { return [this, id, updateView]() { // qDebug() << "DEREGISTER TRACK" << id; auto it = m_iteratorTable[id]; // iterator to the element int index = getTrackPosition(id); // compute index in list m_tractor->remove_track(static_cast(index + 1)); // melt operation, add 1 to account for black background track // send update to the model m_allTracks.erase(it); // actual deletion of object m_iteratorTable.erase(id); // clean table if (updateView) { _resetView(); } return true; }; } Fun TimelineModel::deregisterClip_lambda(int clipId) { return [this, clipId]() { // qDebug() << " // /REQUEST TL CLP DELETION: " << clipId << "\n--------\nCLIPS COUNT: " << m_allClips.size(); clearAssetView(clipId); Q_ASSERT(m_allClips.count(clipId) > 0); Q_ASSERT(getClipTrackId(clipId) == -1); // clip must be deleted from its track at this point Q_ASSERT(!m_groups->isInGroup(clipId)); // clip must be ungrouped at this point auto clip = m_allClips[clipId]; m_allClips.erase(clipId); clip->deregisterClipToBin(); m_groups->destructGroupItem(clipId); return true; }; } void TimelineModel::deregisterGroup(int id) { Q_ASSERT(m_allGroups.count(id) > 0); m_allGroups.erase(id); } std::shared_ptr TimelineModel::getTrackById(int trackId) { Q_ASSERT(m_iteratorTable.count(trackId) > 0); return *m_iteratorTable[trackId]; } const std::shared_ptr TimelineModel::getTrackById_const(int trackId) const { Q_ASSERT(m_iteratorTable.count(trackId) > 0); return *m_iteratorTable.at(trackId); } bool TimelineModel::addTrackEffect(int trackId, const QString &effectId) { Q_ASSERT(m_iteratorTable.count(trackId) > 0); if ((*m_iteratorTable.at(trackId))->addEffect(effectId) == false) { QString effectName = EffectsRepository::get()->getName(effectId); pCore->displayMessage(i18n("Cannot add effect %1 to selected track", effectName), InformationMessage, 500); return false; } return true; } bool TimelineModel::copyTrackEffect(int trackId, const QString &sourceId) { QStringList source = sourceId.split(QLatin1Char('-')); Q_ASSERT(m_iteratorTable.count(trackId) > 0 && source.count() == 3); int itemType = source.at(0).toInt(); int itemId = source.at(1).toInt(); int itemRow = source.at(2).toInt(); std::shared_ptr effectStack = pCore->getItemEffectStack(itemType, itemId); if ((*m_iteratorTable.at(trackId))->copyEffect(effectStack, itemRow) == false) { pCore->displayMessage(i18n("Cannot paste effect to selected track"), InformationMessage, 500); return false; } return true; } std::shared_ptr TimelineModel::getClipPtr(int clipId) const { Q_ASSERT(m_allClips.count(clipId) > 0); return m_allClips.at(clipId); } bool TimelineModel::addClipEffect(int clipId, const QString &effectId, bool notify) { Q_ASSERT(m_allClips.count(clipId) > 0); bool result = m_allClips.at(clipId)->addEffect(effectId); if (!result && notify) { QString effectName = EffectsRepository::get()->getName(effectId); pCore->displayMessage(i18n("Cannot add effect %1 to selected clip", effectName), InformationMessage, 500); } return result; } bool TimelineModel::removeFade(int clipId, bool fromStart) { Q_ASSERT(m_allClips.count(clipId) > 0); return m_allClips.at(clipId)->removeFade(fromStart); } std::shared_ptr TimelineModel::getClipEffectStack(int itemId) { Q_ASSERT(m_allClips.count(itemId)); return m_allClips.at(itemId)->m_effectStack; } bool TimelineModel::copyClipEffect(int clipId, const QString &sourceId) { QStringList source = sourceId.split(QLatin1Char('-')); Q_ASSERT(m_allClips.count(clipId) && source.count() == 3); int itemType = source.at(0).toInt(); int itemId = source.at(1).toInt(); int itemRow = source.at(2).toInt(); std::shared_ptr effectStack = pCore->getItemEffectStack(itemType, itemId); return m_allClips.at(clipId)->copyEffect(effectStack, itemRow); } bool TimelineModel::adjustEffectLength(int clipId, const QString &effectId, int duration, int initialDuration) { Q_ASSERT(m_allClips.count(clipId)); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool res = m_allClips.at(clipId)->adjustEffectLength(effectId, duration, initialDuration, undo, redo); if (res && initialDuration > 0) { PUSH_UNDO(undo, redo, i18n("Adjust Fade")); } return res; } std::shared_ptr TimelineModel::getCompositionPtr(int compoId) const { Q_ASSERT(m_allCompositions.count(compoId) > 0); return m_allCompositions.at(compoId); } int TimelineModel::getNextId() { return TimelineModel::next_id++; } bool TimelineModel::isClip(int id) const { return m_allClips.count(id) > 0; } bool TimelineModel::isComposition(int id) const { return m_allCompositions.count(id) > 0; } bool TimelineModel::isTrack(int id) const { return m_iteratorTable.count(id) > 0; } bool TimelineModel::isGroup(int id) const { return m_allGroups.count(id) > 0; } void TimelineModel::updateDuration() { int current = m_blackClip->get_playtime() - TimelineModel::seekDuration; int duration = 0; for (const auto &tck : m_iteratorTable) { auto track = (*tck.second); duration = qMax(duration, track->trackDuration()); } if (duration != current) { // update black track length m_blackClip->set_in_and_out(0, duration + TimelineModel::seekDuration); emit durationUpdated(); } } int TimelineModel::duration() const { return m_tractor->get_playtime() - TimelineModel::seekDuration; } std::unordered_set TimelineModel::getGroupElements(int clipId) { int groupId = m_groups->getRootId(clipId); return m_groups->getLeaves(groupId); } Mlt::Profile *TimelineModel::getProfile() { return m_profile; } bool TimelineModel::requestReset(Fun &undo, Fun &redo) { std::vector all_ids; for (const auto &track : m_iteratorTable) { all_ids.push_back(track.first); } bool ok = true; for (int trackId : all_ids) { ok = ok && requestTrackDeletion(trackId, undo, redo); } return ok; } void TimelineModel::setUndoStack(std::weak_ptr undo_stack) { m_undoStack = std::move(undo_stack); } int TimelineModel::suggestSnapPoint(int pos, int snapDistance) { int snapped = m_snaps->getClosestPoint(pos); return (qAbs(snapped - pos) < snapDistance ? snapped : pos); } int TimelineModel::requestBestSnapPos(int pos, int length, const std::vector &pts, int cursorPosition, int snapDistance) { if (!pts.empty()) { m_snaps->ignore(pts); } m_snaps->addPoint(cursorPosition); int snapped_start = m_snaps->getClosestPoint(pos); int snapped_end = m_snaps->getClosestPoint(pos + length); m_snaps->unIgnore(); m_snaps->removePoint(cursorPosition); int startDiff = qAbs(pos - snapped_start); int endDiff = qAbs(pos + length - snapped_end); if (startDiff < endDiff && startDiff <= snapDistance) { // snap to start return snapped_start; } if (endDiff <= snapDistance) { // snap to end return snapped_end - length; } return -1; } int TimelineModel::requestNextSnapPos(int pos) { return m_snaps->getNextPoint(pos); } int TimelineModel::requestPreviousSnapPos(int pos) { return m_snaps->getPreviousPoint(pos); } void TimelineModel::addSnap(int pos) { return m_snaps->addPoint(pos); } void TimelineModel::removeSnap(int pos) { return m_snaps->removePoint(pos); } void TimelineModel::registerComposition(const std::shared_ptr &composition) { int id = composition->getId(); Q_ASSERT(m_allCompositions.count(id) == 0); m_allCompositions[id] = composition; m_groups->createGroupItem(id); } bool TimelineModel::requestCompositionInsertion(const QString &transitionId, int trackId, int position, int length, std::unique_ptr transProps, int &id, bool logUndo) { QWriteLocker locker(&m_lock); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool result = requestCompositionInsertion(transitionId, trackId, -1, position, length, std::move(transProps), id, undo, redo, logUndo); if (result && logUndo) { PUSH_UNDO(undo, redo, i18n("Insert Composition")); } return result; } bool TimelineModel::requestCompositionInsertion(const QString &transitionId, int trackId, int compositionTrack, int position, int length, std::unique_ptr transProps, int &id, Fun &undo, Fun &redo, bool finalMove) { qDebug() << "Inserting compo track" << trackId << "pos" << position << "length" << length; int compositionId = TimelineModel::getNextId(); id = compositionId; Fun local_undo = deregisterComposition_lambda(compositionId); CompositionModel::construct(shared_from_this(), transitionId, compositionId, std::move(transProps)); auto composition = m_allCompositions[compositionId]; Fun local_redo = [composition, this]() { // We capture a shared_ptr to the composition, which means that as long as this undo object lives, the composition object is not deleted. To insert it // back it is sufficient to register it. registerComposition(composition); return true; }; bool res = requestCompositionMove(compositionId, trackId, compositionTrack, position, true, finalMove, local_undo, local_redo); qDebug() << "trying to move" << trackId << "pos" << position << "success " << res; if (res) { res = requestItemResize(compositionId, length, true, true, local_undo, local_redo, true); qDebug() << "trying to resize" << compositionId << "length" << length << "success " << res; } if (!res) { bool undone = local_undo(); Q_ASSERT(undone); id = -1; return false; } UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } Fun TimelineModel::deregisterComposition_lambda(int compoId) { return [this, compoId]() { Q_ASSERT(m_allCompositions.count(compoId) > 0); Q_ASSERT(!m_groups->isInGroup(compoId)); // composition must be ungrouped at this point clearAssetView(compoId); m_allCompositions.erase(compoId); m_groups->destructGroupItem(compoId); return true; }; } int TimelineModel::getCompositionPosition(int compoId) const { Q_ASSERT(m_allCompositions.count(compoId) > 0); const auto trans = m_allCompositions.at(compoId); return trans->getPosition(); } int TimelineModel::getCompositionPlaytime(int compoId) const { READ_LOCK(); Q_ASSERT(m_allCompositions.count(compoId) > 0); const auto trans = m_allCompositions.at(compoId); int playtime = trans->getPlaytime(); return playtime; } int TimelineModel::getItemPosition(int itemId) const { if (isClip(itemId)) { return getClipPosition(itemId); } return getCompositionPosition(itemId); } int TimelineModel::getItemPlaytime(int itemId) const { if (isClip(itemId)) { return getClipPlaytime(itemId); } return getCompositionPlaytime(itemId); } int TimelineModel::getTrackCompositionsCount(int trackId) const { Q_ASSERT(isTrack(trackId)); return getTrackById_const(trackId)->getCompositionsCount(); } bool TimelineModel::requestCompositionMove(int compoId, int trackId, int position, bool updateView, bool logUndo) { QWriteLocker locker(&m_lock); Q_ASSERT(isComposition(compoId)); if (m_allCompositions[compoId]->getPosition() == position && getCompositionTrackId(compoId) == trackId) { return true; } if (m_groups->isInGroup(compoId)) { // element is in a group. int groupId = m_groups->getRootId(compoId); int current_trackId = getCompositionTrackId(compoId); int track_pos1 = getTrackPosition(trackId); int track_pos2 = getTrackPosition(current_trackId); int delta_track = track_pos1 - track_pos2; int delta_pos = position - m_allCompositions[compoId]->getPosition(); return requestGroupMove(compoId, groupId, delta_track, delta_pos, updateView, logUndo); } std::function undo = []() { return true; }; std::function redo = []() { return true; }; int min = getCompositionPosition(compoId); int max = min + getCompositionPlaytime(compoId); int tk = getCompositionTrackId(compoId); bool res = requestCompositionMove(compoId, trackId, m_allCompositions[compoId]->getForcedTrack(), position, updateView, logUndo, undo, redo); if (tk > -1) { min = qMin(min, getCompositionPosition(compoId)); max = qMax(max, getCompositionPosition(compoId)); } else { min = getCompositionPosition(compoId); max = min + getCompositionPlaytime(compoId); } if (res && logUndo) { PUSH_UNDO(undo, redo, i18n("Move composition")); checkRefresh(min, max); } return res; } bool TimelineModel::isAudioTrack(int trackId) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); auto it = m_iteratorTable.at(trackId); return (*it)->isAudioTrack(); } bool TimelineModel::requestCompositionMove(int compoId, int trackId, int compositionTrack, int position, bool updateView, bool finalMove, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); Q_ASSERT(isComposition(compoId)); Q_ASSERT(isTrack(trackId)); if (compositionTrack == -1 || (compositionTrack > 0 && trackId == getTrackIndexFromPosition(compositionTrack - 1))) { // qDebug() << "// compo track: " << trackId << ", PREVIOUS TK: " << getPreviousVideoTrackPos(trackId); compositionTrack = getPreviousVideoTrackPos(trackId); } if (compositionTrack == -1) { // it doesn't make sense to insert a composition on the last track qDebug() << "Move failed because of last track"; return false; } qDebug() << "Requesting composition move" << trackId << "," << position << " ( " << compositionTrack << " / " << (compositionTrack > 0 ? getTrackIndexFromPosition(compositionTrack - 1) : 0); Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; bool ok = true; int old_trackId = getCompositionTrackId(compoId); bool notifyViewOnly = false; Fun update_model = []() { return true; }; if (updateView && old_trackId == trackId) { // Move on same track, only send view update updateView = false; notifyViewOnly = true; update_model = [compoId, this]() { QModelIndex modelIndex = makeCompositionIndexFromID(compoId); - notifyChange(modelIndex, modelIndex, {StartRole}); + notifyChange(modelIndex, modelIndex, StartRole); return true; }; } if (old_trackId != -1) { Fun delete_operation = []() { return true; }; Fun delete_reverse = []() { return true; }; if (old_trackId != trackId) { delete_operation = [this, compoId]() { bool res = unplantComposition(compoId); if (res) m_allCompositions[compoId]->setATrack(-1, -1); return res; }; int oldAtrack = m_allCompositions[compoId]->getATrack(); delete_reverse = [this, compoId, oldAtrack, updateView]() { m_allCompositions[compoId]->setATrack(oldAtrack, oldAtrack <= 0 ? -1 : getTrackIndexFromPosition(oldAtrack - 1)); return replantCompositions(compoId, updateView); }; } ok = delete_operation(); if (!ok) qDebug() << "Move failed because of first delete operation"; if (ok) { if (notifyViewOnly) { PUSH_LAMBDA(update_model, local_undo); } UPDATE_UNDO_REDO(delete_operation, delete_reverse, local_undo, local_redo); ok = getTrackById(old_trackId)->requestCompositionDeletion(compoId, updateView, finalMove, local_undo, local_redo); } if (!ok) { qDebug() << "Move failed because of first deletion request"; bool undone = local_undo(); Q_ASSERT(undone); return false; } } ok = getTrackById(trackId)->requestCompositionInsertion(compoId, position, updateView, finalMove, local_undo, local_redo); if (!ok) qDebug() << "Move failed because of second insertion request"; if (ok) { Fun insert_operation = []() { return true; }; Fun insert_reverse = []() { return true; }; if (old_trackId != trackId) { insert_operation = [this, compoId, compositionTrack, updateView]() { qDebug() << "-------------- ATRACK ----------------\n" << compositionTrack << " = " << getTrackIndexFromPosition(compositionTrack); m_allCompositions[compoId]->setATrack(compositionTrack, compositionTrack <= 0 ? -1 : getTrackIndexFromPosition(compositionTrack - 1)); return replantCompositions(compoId, updateView); }; insert_reverse = [this, compoId]() { bool res = unplantComposition(compoId); if (res) m_allCompositions[compoId]->setATrack(-1, -1); return res; }; } ok = insert_operation(); if (!ok) qDebug() << "Move failed because of second insert operation"; if (ok) { if (notifyViewOnly) { PUSH_LAMBDA(update_model, local_redo); } UPDATE_UNDO_REDO(insert_operation, insert_reverse, local_undo, local_redo); } } if (!ok) { bool undone = local_undo(); Q_ASSERT(undone); return false; } update_model(); UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } bool TimelineModel::replantCompositions(int currentCompo, bool updateView) { // We ensure that the compositions are planted in a decreasing order of b_track. // For that, there is no better option than to disconnect every composition and then reinsert everything in the correct order. std::vector> compos; for (const auto &compo : m_allCompositions) { int trackId = compo.second->getCurrentTrackId(); if (trackId == -1 || compo.second->getATrack() == -1) { continue; } // Note: we need to retrieve the position of the track, that is its melt index. int trackPos = getTrackMltIndex(trackId); compos.push_back({trackPos, compo.first}); if (compo.first != currentCompo) { unplantComposition(compo.first); } } // sort by decreasing b_track std::sort(compos.begin(), compos.end(), [](const std::pair &a, const std::pair &b) { return a.first > b.first; }); // replant QScopedPointer field(m_tractor->field()); field->lock(); // Unplant track compositing mlt_service nextservice = mlt_service_get_producer(field->get_service()); mlt_properties properties = MLT_SERVICE_PROPERTIES(nextservice); QString resource = mlt_properties_get(properties, "mlt_service"); mlt_service_type mlt_type = mlt_service_identify(nextservice); QList trackCompositions; while (mlt_type == transition_type) { Mlt::Transition transition((mlt_transition)nextservice); nextservice = mlt_service_producer(nextservice); int internal = transition.get_int("internal_added"); if (internal > 0 && resource != QLatin1String("mix")) { trackCompositions << new Mlt::Transition(transition); field->disconnect_service(transition); transition.disconnect_all_producers(); } if (nextservice == nullptr) { break; } mlt_type = mlt_service_identify(nextservice); properties = MLT_SERVICE_PROPERTIES(nextservice); resource = mlt_properties_get(properties, "mlt_service"); } // Sort track compositing std::sort(trackCompositions.begin(), trackCompositions.end(), [](Mlt::Transition *a, Mlt::Transition *b) { return a->get_b_track() < b->get_b_track(); }); for (const auto &compo : compos) { int aTrack = m_allCompositions[compo.second]->getATrack(); Q_ASSERT(aTrack != -1 && aTrack < m_tractor->count()); int ret = field->plant_transition(*m_allCompositions[compo.second].get(), aTrack, compo.first); qDebug() << "Planting composition " << compo.second << "in " << aTrack << "/" << compo.first << "IN = " << m_allCompositions[compo.second]->getIn() << "OUT = " << m_allCompositions[compo.second]->getOut() << "ret=" << ret; Mlt::Transition &transition = *m_allCompositions[compo.second].get(); transition.set_tracks(aTrack, compo.first); mlt_service consumer = mlt_service_consumer(transition.get_service()); Q_ASSERT(consumer != nullptr); if (ret != 0) { field->unlock(); return false; } } // Replant last tracks compositing while (!trackCompositions.isEmpty()) { Mlt::Transition *firstTr = trackCompositions.takeFirst(); field->plant_transition(*firstTr, firstTr->get_a_track(), firstTr->get_b_track()); } field->unlock(); if (updateView) { QModelIndex modelIndex = makeCompositionIndexFromID(currentCompo); - notifyChange(modelIndex, modelIndex, {ItemATrack}); + notifyChange(modelIndex, modelIndex, ItemATrack); } return true; } bool TimelineModel::unplantComposition(int compoId) { qDebug() << "Unplanting" << compoId; Mlt::Transition &transition = *m_allCompositions[compoId].get(); mlt_service consumer = mlt_service_consumer(transition.get_service()); Q_ASSERT(consumer != nullptr); QScopedPointer field(m_tractor->field()); field->lock(); field->disconnect_service(transition); int ret = transition.disconnect_all_producers(); mlt_service nextservice = mlt_service_get_producer(transition.get_service()); // mlt_service consumer = mlt_service_consumer(transition.get_service()); Q_ASSERT(nextservice == nullptr); // Q_ASSERT(consumer == nullptr); field->unlock(); return ret != 0; } bool TimelineModel::checkConsistency() { for (const auto &tck : m_iteratorTable) { auto track = (*tck.second); // Check parent/children link for tracks if (auto ptr = track->m_parent.lock()) { if (ptr.get() != this) { qDebug() << "Wrong parent for track" << tck.first; return false; } } else { qDebug() << "NULL parent for track" << tck.first; return false; } // check consistency of track if (!track->checkConsistency()) { qDebug() << "Consistency check failed for track" << tck.first; return false; } } // We store all in/outs of clips to check snap points std::map snaps; // Check parent/children link for clips for (const auto &cp : m_allClips) { auto clip = (cp.second); // Check parent/children link for tracks if (auto ptr = clip->m_parent.lock()) { if (ptr.get() != this) { qDebug() << "Wrong parent for clip" << cp.first; return false; } } else { qDebug() << "NULL parent for clip" << cp.first; return false; } if (getClipTrackId(cp.first) != -1) { snaps[clip->getPosition()] += 1; snaps[clip->getPosition() + clip->getPlaytime()] += 1; } if (!clip->checkConsistency()) { qDebug() << "Consistency check failed for clip" << cp.first; return false; } } for (const auto &cp : m_allCompositions) { auto clip = (cp.second); // Check parent/children link for tracks if (auto ptr = clip->m_parent.lock()) { if (ptr.get() != this) { qDebug() << "Wrong parent for compo" << cp.first; return false; } } else { qDebug() << "NULL parent for compo" << cp.first; return false; } if (getCompositionTrackId(cp.first) != -1) { snaps[clip->getPosition()] += 1; snaps[clip->getPosition() + clip->getPlaytime()] += 1; } } // Check snaps auto stored_snaps = m_snaps->_snaps(); if (snaps.size() != stored_snaps.size()) { qDebug() << "Wrong number of snaps: " << snaps.size() << " == " << stored_snaps.size(); return false; } for (auto i = snaps.begin(), j = stored_snaps.begin(); i != snaps.end(); ++i, ++j) { if (*i != *j) { qDebug() << "Wrong snap info at point" << (*i).first; return false; } } // We check consistency with bin model auto binClips = pCore->projectItemModel()->getAllClipIds(); // First step: all clips referenced by the bin model exist and are inserted for (const auto &binClip : binClips) { auto projClip = pCore->projectItemModel()->getClipByBinID(binClip); for (const auto &insertedClip : projClip->m_registeredClips) { if (auto ptr = insertedClip.second.lock()) { if (ptr.get() == this) { // check we are talking of this timeline if (!isClip(insertedClip.first)) { qDebug() << "Bin model registers a bad clip ID" << insertedClip.first; return false; } } } else { qDebug() << "Bin model registers a clip in a NULL timeline" << insertedClip.first; return false; } } } // Second step: all clips are referenced for (const auto &clip : m_allClips) { auto binId = clip.second->m_binClipId; auto projClip = pCore->projectItemModel()->getClipByBinID(binId); if (projClip->m_registeredClips.count(clip.first) == 0) { qDebug() << "Clip " << clip.first << "not registered in bin"; return false; } } // We now check consistency of the compositions. For that, we list all compositions of the tractor, and see if we have a matching one in our // m_allCompositions std::unordered_set remaining_compo; for (const auto &compo : m_allCompositions) { if (getCompositionTrackId(compo.first) != -1 && m_allCompositions[compo.first]->getATrack() != -1) { remaining_compo.insert(compo.first); // check validity of the consumer Mlt::Transition &transition = *m_allCompositions[compo.first].get(); mlt_service consumer = mlt_service_consumer(transition.get_service()); Q_ASSERT(consumer != nullptr); } } QScopedPointer field(m_tractor->field()); field->lock(); mlt_service nextservice = mlt_service_get_producer(field->get_service()); mlt_service_type mlt_type = mlt_service_identify(nextservice); while (nextservice != nullptr) { if (mlt_type == transition_type) { mlt_transition tr = (mlt_transition)nextservice; int currentTrack = mlt_transition_get_b_track(tr); int currentATrack = mlt_transition_get_a_track(tr); int currentIn = (int)mlt_transition_get_in(tr); int currentOut = (int)mlt_transition_get_out(tr); qDebug() << "looking composition IN: " << currentIn << ", OUT: " << currentOut << ", TRACK: " << currentTrack << " / " << currentATrack; int foundId = -1; // we iterate to try to find a matching compo for (int compoId : remaining_compo) { if (getTrackMltIndex(getCompositionTrackId(compoId)) == currentTrack && m_allCompositions[compoId]->getATrack() == currentATrack && m_allCompositions[compoId]->getIn() == currentIn && m_allCompositions[compoId]->getOut() == currentOut) { foundId = compoId; break; } } if (foundId == -1) { qDebug() << "Error, we didn't find matching composition IN: " << currentIn << ", OUT: " << currentOut << ", TRACK: " << currentTrack << " / " << currentATrack; field->unlock(); return false; } qDebug() << "Found"; remaining_compo.erase(foundId); } nextservice = mlt_service_producer(nextservice); if (nextservice == nullptr) { break; } mlt_type = mlt_service_identify(nextservice); } field->unlock(); if (!remaining_compo.empty()) { qDebug() << "Error: We found less compositions than expected. Compositions that have not been found:"; for (int compoId : remaining_compo) { qDebug() << compoId; } return false; } // We check consistency of groups if (!m_groups->checkConsistency(true, true)) { qDebug() << "== ERROR IN GROUP CONSISTENCY"; return false; } return true; } void TimelineModel::setTimelineEffectsEnabled(bool enabled) { m_timelineEffectsEnabled = enabled; // propagate info to clips for (const auto &clip : m_allClips) { clip.second->setTimelineEffectsEnabled(enabled); } // TODO if we support track effects, they should be disabled here too } std::shared_ptr TimelineModel::producer() { return std::make_shared(tractor()); } void TimelineModel::checkRefresh(int start, int end) { if (m_blockRefresh) { return; } int currentPos = tractor()->position(); if (currentPos >= start && currentPos < end) { emit requestMonitorRefresh(); } } void TimelineModel::clearAssetView(int itemId) { emit requestClearAssetView(itemId); } std::shared_ptr TimelineModel::getCompositionParameterModel(int compoId) const { READ_LOCK(); Q_ASSERT(isComposition(compoId)); return std::static_pointer_cast(m_allCompositions.at(compoId)); } std::shared_ptr TimelineModel::getClipEffectStackModel(int clipId) const { READ_LOCK(); Q_ASSERT(isClip(clipId)); return std::static_pointer_cast(m_allClips.at(clipId)->m_effectStack); } std::shared_ptr TimelineModel::getTrackEffectStackModel(int trackId) { READ_LOCK(); Q_ASSERT(isTrack(trackId)); return getTrackById(trackId)->m_effectStack; } QStringList TimelineModel::extractCompositionLumas() const { QStringList urls; for (const auto &compo : m_allCompositions) { QString luma = compo.second->getProperty(QStringLiteral("resource")); if (!luma.isEmpty()) { urls << QUrl::fromLocalFile(luma).toLocalFile(); } } urls.removeDuplicates(); return urls; } void TimelineModel::adjustAssetRange(int clipId, int in, int out) { Q_UNUSED(clipId) Q_UNUSED(in) Q_UNUSED(out) // pCore->adjustAssetRange(clipId, in, out); } void TimelineModel::requestClipReload(int clipId) { std::function local_undo = []() { return true; }; std::function local_redo = []() { return true; }; // in order to make the producer change effective, we need to unplant / replant the clip in int track int old_trackId = getClipTrackId(clipId); int oldPos = getClipPosition(clipId); int oldOut = getClipIn(clipId) + getClipPlaytime(clipId); // Check if clip out is longer than actual producer duration (if user forced duration) std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(getClipBinId(clipId)); bool refreshView = oldOut > (int)binClip->frameDuration(); if (old_trackId != -1) { getTrackById(old_trackId)->requestClipDeletion(clipId, refreshView, true, local_undo, local_redo); } m_allClips[clipId]->refreshProducerFromBin(); if (old_trackId != -1) { getTrackById(old_trackId)->requestClipInsertion(clipId, oldPos, refreshView, true, local_undo, local_redo); } } void TimelineModel::replugClip(int clipId) { int old_trackId = getClipTrackId(clipId); if (old_trackId != -1) { getTrackById(old_trackId)->replugClip(clipId); } } void TimelineModel::requestClipUpdate(int clipId, const QVector &roles) { QModelIndex modelIndex = makeClipIndexFromID(clipId); if (roles.contains(TimelineModel::ReloadThumbRole)) { m_allClips[clipId]->forceThumbReload = !m_allClips[clipId]->forceThumbReload; } notifyChange(modelIndex, modelIndex, roles); } bool TimelineModel::requestClipTimeWarp(int clipId, double speed, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); if (qFuzzyCompare(speed, m_allClips[clipId]->getSpeed())) { return true; } std::function local_undo = []() { return true; }; std::function local_redo = []() { return true; }; int oldPos = getClipPosition(clipId); // in order to make the producer change effective, we need to unplant / replant the clip in int track bool success = true; int trackId = getClipTrackId(clipId); if (trackId != -1) { success = success && getTrackById(trackId)->requestClipDeletion(clipId, true, true, local_undo, local_redo); } if (success) { success = m_allClips[clipId]->useTimewarpProducer(speed, local_undo, local_redo); } if (trackId != -1) { success = success && getTrackById(trackId)->requestClipInsertion(clipId, oldPos, true, true, local_undo, local_redo); } if (!success) { local_undo(); return false; } UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return success; } bool TimelineModel::requestClipTimeWarp(int clipId, double speed) { Fun undo = []() { return true; }; Fun redo = []() { return true; }; // Get main clip info int trackId = getClipTrackId(clipId); bool result = true; if (trackId != -1) { // Check if clip has a split partner int splitId = m_groups->getSplitPartner(clipId); if (splitId > -1) { result = requestClipTimeWarp(splitId, speed / 100.0, undo, redo); } if (result) { result = requestClipTimeWarp(clipId, speed / 100.0, undo, redo); } else { pCore->displayMessage(i18n("Change speed failed"), ErrorMessage); undo(); return false; } } else { // If clip is not inserted on a track, we just change the producer m_allClips[clipId]->useTimewarpProducer(speed, undo, redo); } if (result) { PUSH_UNDO(undo, redo, i18n("Change clip speed")); return true; } return false; } const QString TimelineModel::getTrackTagById(int trackId) const { READ_LOCK(); Q_ASSERT(isTrack(trackId)); bool isAudio = getTrackById_const(trackId)->isAudioTrack(); int count = 1; int totalAudio = 2; auto it = m_allTracks.begin(); bool found = false; while ((isAudio || !found) && it != m_allTracks.end()) { if ((*it)->isAudioTrack()) { totalAudio++; if (isAudio && !found) { count++; } } else if (!isAudio) { count++; } if ((*it)->getId() == trackId) { found = true; } it++; } return isAudio ? QStringLiteral("A%1").arg(totalAudio - count) : QStringLiteral("V%1").arg(count - 1); } void TimelineModel::updateProfile(Mlt::Profile *profile) { m_profile = profile; m_tractor->set_profile(*m_profile); } int TimelineModel::getBlankSizeNearClip(int clipId, bool after) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); int trackId = getClipTrackId(clipId); if (trackId != -1) { return getTrackById_const(trackId)->getBlankSizeNearClip(clipId, after); } return 0; } int TimelineModel::getPreviousTrackId(int trackId) { READ_LOCK(); Q_ASSERT(isTrack(trackId)); auto it = m_iteratorTable.at(trackId); bool audioWanted = (*it)->isAudioTrack(); while (it != m_allTracks.begin()) { --it; if (it != m_allTracks.begin() && (*it)->isAudioTrack() == audioWanted) { break; } } return it == m_allTracks.begin() ? trackId : (*it)->getId(); } int TimelineModel::getNextTrackId(int trackId) { READ_LOCK(); Q_ASSERT(isTrack(trackId)); auto it = m_iteratorTable.at(trackId); bool audioWanted = (*it)->isAudioTrack(); while (it != m_allTracks.end()) { ++it; if (it != m_allTracks.end() && (*it)->isAudioTrack() == audioWanted) { break; } } return it == m_allTracks.end() ? trackId : (*it)->getId(); } diff --git a/src/timeline2/model/trackmodel.cpp b/src/timeline2/model/trackmodel.cpp index e56483a83..daba59d54 100644 --- a/src/timeline2/model/trackmodel.cpp +++ b/src/timeline2/model/trackmodel.cpp @@ -1,1141 +1,1141 @@ /*************************************************************************** * 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 "trackmodel.hpp" #include "clipmodel.hpp" #include "compositionmodel.hpp" #include "effects/effectstack/model/effectstackmodel.hpp" #include "kdenlivesettings.h" #include "logger.hpp" #include "snapmodel.hpp" #include "timelinemodel.hpp" #include #include #include #include TrackModel::TrackModel(const std::weak_ptr &parent, int id, const QString &trackName, bool audioTrack) : m_parent(parent) , m_id(id == -1 ? TimelineModel::getNextId() : id) , m_lock(QReadWriteLock::Recursive) { if (auto ptr = parent.lock()) { m_track = std::shared_ptr(new Mlt::Tractor(*ptr->getProfile())); m_playlists[0].set_profile(*ptr->getProfile()); m_playlists[1].set_profile(*ptr->getProfile()); m_track->insert_track(m_playlists[0], 0); m_track->insert_track(m_playlists[1], 1); if (!trackName.isEmpty()) { m_track->set("kdenlive:track_name", trackName.toUtf8().constData()); } if (audioTrack) { m_track->set("kdenlive:audio_track", 1); for (int i = 0; i < 2; i++) { m_playlists[i].set("hide", 1); } } m_track->set("kdenlive:trackheight", KdenliveSettings::trackheight()); m_effectStack = EffectStackModel::construct(m_track, {ObjectType::TimelineTrack, m_id}, ptr->m_undoStack); QObject::connect(m_effectStack.get(), &EffectStackModel::dataChanged, [&](const QModelIndex &, const QModelIndex &, QVector roles) { if (auto ptr2 = m_parent.lock()) { QModelIndex ix = ptr2->makeTrackIndexFromID(m_id); ptr2->dataChanged(ix, ix, roles); } }); } else { qDebug() << "Error : construction of track failed because parent timeline is not available anymore"; Q_ASSERT(false); } } TrackModel::TrackModel(const std::weak_ptr &parent, Mlt::Tractor mltTrack, int id) : m_parent(parent) , m_id(id == -1 ? TimelineModel::getNextId() : id) { if (auto ptr = parent.lock()) { m_track = std::shared_ptr(new Mlt::Tractor(mltTrack)); m_playlists[0] = *m_track->track(0); m_playlists[1] = *m_track->track(1); m_effectStack = EffectStackModel::construct(m_track, {ObjectType::TimelineTrack, m_id}, ptr->m_undoStack); } else { qDebug() << "Error : construction of track failed because parent timeline is not available anymore"; Q_ASSERT(false); } } TrackModel::~TrackModel() { m_track->remove_track(1); m_track->remove_track(0); } int TrackModel::construct(const std::weak_ptr &parent, int id, int pos, const QString &trackName, bool audioTrack) { std::shared_ptr track(new TrackModel(parent, id, trackName, audioTrack)); TRACE_CONSTR(track.get(), parent, id, pos, trackName, audioTrack); id = track->m_id; if (auto ptr = parent.lock()) { ptr->registerTrack(std::move(track), pos); } else { qDebug() << "Error : construction of track failed because parent timeline is not available anymore"; Q_ASSERT(false); } return id; } int TrackModel::getClipsCount() { READ_LOCK(); #ifdef QT_DEBUG int count = 0; for (int j = 0; j < 2; j++) { for (int i = 0; i < m_playlists[j].count(); i++) { if (!m_playlists[j].is_blank(i)) { count++; } } } Q_ASSERT(count == static_cast(m_allClips.size())); #else int count = (int)m_allClips.size(); #endif return count; } Fun TrackModel::requestClipInsertion_lambda(int clipId, int position, bool updateView, bool finalMove) { QWriteLocker locker(&m_lock); // By default, insertion occurs in topmost track // Find out the clip id at position int target_clip = m_playlists[0].get_clip_index_at(position); int count = m_playlists[0].count(); // we create the function that has to be executed after the melt order. This is essentially book-keeping auto end_function = [clipId, this, position, updateView, finalMove]() { if (auto ptr = m_parent.lock()) { std::shared_ptr clip = ptr->getClipPtr(clipId); m_allClips[clip->getId()] = clip; // store clip // update clip position and track clip->setPosition(position); clip->setCurrentTrackId(m_id); int new_in = clip->getPosition(); int new_out = new_in + clip->getPlaytime(); ptr->m_snaps->addPoint(new_in); ptr->m_snaps->addPoint(new_out); if (updateView) { int clip_index = getRowfromClip(clipId); ptr->_beginInsertRows(ptr->makeTrackIndexFromID(m_id), clip_index, clip_index); ptr->_endInsertRows(); bool audioOnly = clip->isAudioOnly(); if (!audioOnly && !isHidden() && !isAudioTrack()) { // only refresh monitor if not an audio track and not hidden ptr->checkRefresh(new_in, new_out); } if (!audioOnly && finalMove && !isAudioTrack()) { ptr->invalidateZone(new_in, new_out); } } return true; } qDebug() << "Error : Clip Insertion failed because timeline is not available anymore"; return false; }; if (target_clip >= count && isBlankAt(position)) { // In that case, we append after, in the first playlist return [this, position, clipId, end_function, finalMove]() { if (auto ptr = m_parent.lock()) { // Lock MLT playlist so that we don't end up with an invalid frame being displayed m_playlists[0].lock(); std::shared_ptr clip = ptr->getClipPtr(clipId); int index = m_playlists[0].insert_at(position, *clip, 1); m_playlists[0].consolidate_blanks(); m_playlists[0].unlock(); if (finalMove) { ptr->updateDuration(); } return index != -1 && end_function(); } qDebug() << "Error : Clip Insertion failed because timeline is not available anymore"; return false; }; } if (isBlankAt(position)) { int blank_end = getBlankEnd(position); int length = -1; if (auto ptr = m_parent.lock()) { std::shared_ptr clip = ptr->getClipPtr(clipId); length = clip->getPlaytime(); } if (blank_end >= position + length) { return [this, position, clipId, end_function]() { if (auto ptr = m_parent.lock()) { // Lock MLT playlist so that we don't end up with an invalid frame being displayed m_playlists[0].lock(); std::shared_ptr clip = ptr->getClipPtr(clipId); int index = m_playlists[0].insert_at(position, *clip, 1); m_playlists[0].consolidate_blanks(); m_playlists[0].unlock(); return index != -1 && end_function(); } qDebug() << "Error : Clip Insertion failed because timeline is not available anymore"; return false; }; } } return []() { return false; }; } bool TrackModel::requestClipInsertion(int clipId, int position, bool updateView, bool finalMove, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); if (isLocked()) { return false; } if (auto ptr = m_parent.lock()) { if (isAudioTrack() && !ptr->getClipPtr(clipId)->canBeAudio()) { qDebug() << "// ATTEMPTING TO INSERT NON AUDIO CLIP ON AUDIO TRACK"; return false; } if (!isAudioTrack() && !ptr->getClipPtr(clipId)->canBeVideo()) { qDebug() << "// ATTEMPTING TO INSERT NON VIDEO CLIP ON VIDEO TRACK"; return false; } Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; bool res = true; if (ptr->getClipPtr(clipId)->clipState() != PlaylistState::Disabled) { res = res && ptr->getClipPtr(clipId)->setClipState(isAudioTrack() ? PlaylistState::AudioOnly : PlaylistState::VideoOnly, local_undo, local_redo); } auto operation = requestClipInsertion_lambda(clipId, position, updateView, finalMove); res = res && operation(); if (res) { auto reverse = requestClipDeletion_lambda(clipId, updateView, finalMove); UPDATE_UNDO_REDO(operation, reverse, local_undo, local_redo); UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); return true; } bool undone = local_undo(); Q_ASSERT(undone); return false; } return false; } void TrackModel::replugClip(int clipId) { QWriteLocker locker(&m_lock); int clip_position = m_allClips[clipId]->getPosition(); auto clip_loc = getClipIndexAt(clip_position); int target_track = clip_loc.first; int target_clip = clip_loc.second; // lock MLT playlist so that we don't end up with invalid frames in monitor m_playlists[target_track].lock(); Q_ASSERT(target_clip < m_playlists[target_track].count()); Q_ASSERT(!m_playlists[target_track].is_blank(target_clip)); std::unique_ptr prod(m_playlists[target_track].replace_with_blank(target_clip)); if (auto ptr = m_parent.lock()) { std::shared_ptr clip = ptr->getClipPtr(clipId); m_playlists[target_track].insert_at(clip_position, *clip, 1); if (!clip->isAudioOnly() && !isAudioTrack()) { ptr->invalidateZone(clip->getIn(), clip->getOut()); } if (!clip->isAudioOnly() && !isHidden() && !isAudioTrack()) { // only refresh monitor if not an audio track and not hidden ptr->checkRefresh(clip->getIn(), clip->getOut()); } } m_playlists[target_track].consolidate_blanks(); m_playlists[target_track].unlock(); } Fun TrackModel::requestClipDeletion_lambda(int clipId, bool updateView, bool finalMove) { QWriteLocker locker(&m_lock); // Find index of clip int clip_position = m_allClips[clipId]->getPosition(); bool audioOnly = m_allClips[clipId]->isAudioOnly(); int old_in = clip_position; int old_out = old_in + m_allClips[clipId]->getPlaytime(); return [clip_position, clipId, old_in, old_out, updateView, audioOnly, finalMove, this]() { auto clip_loc = getClipIndexAt(clip_position); if (updateView) { int old_clip_index = getRowfromClip(clipId); auto ptr = m_parent.lock(); ptr->_beginRemoveRows(ptr->makeTrackIndexFromID(getId()), old_clip_index, old_clip_index); ptr->_endRemoveRows(); } int target_track = clip_loc.first; int target_clip = clip_loc.second; // lock MLT playlist so that we don't end up with invalid frames in monitor m_playlists[target_track].lock(); Q_ASSERT(target_clip < m_playlists[target_track].count()); Q_ASSERT(!m_playlists[target_track].is_blank(target_clip)); auto prod = m_playlists[target_track].replace_with_blank(target_clip); if (prod != nullptr) { m_playlists[target_track].consolidate_blanks(); m_allClips[clipId]->setCurrentTrackId(-1); m_allClips.erase(clipId); delete prod; m_playlists[target_track].unlock(); if (auto ptr = m_parent.lock()) { ptr->m_snaps->removePoint(old_in); ptr->m_snaps->removePoint(old_out); if (finalMove) { if (!audioOnly && !isAudioTrack()) { ptr->invalidateZone(old_in, old_out); } if (target_clip >= m_playlists[target_track].count()) { // deleted last clip in playlist ptr->updateDuration(); } } if (!audioOnly && !isHidden() && !isAudioTrack()) { // only refresh monitor if not an audio track and not hidden ptr->checkRefresh(old_in, old_out); } } return true; } m_playlists[target_track].unlock(); return false; }; } bool TrackModel::requestClipDeletion(int clipId, bool updateView, bool finalMove, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); Q_ASSERT(m_allClips.count(clipId) > 0); if (isLocked()) { return false; } auto old_clip = m_allClips[clipId]; int old_position = old_clip->getPosition(); // qDebug() << "/// REQUESTOING CLIP DELETION_: " << updateView; auto operation = requestClipDeletion_lambda(clipId, updateView, finalMove); if (operation()) { auto reverse = requestClipInsertion_lambda(clipId, old_position, updateView, finalMove); UPDATE_UNDO_REDO(operation, reverse, undo, redo); return true; } return false; } int TrackModel::getBlankSizeAtPos(int frame) { READ_LOCK(); int min_length = 0; for (int i = 0; i < 2; ++i) { int ix = m_playlists[i].get_clip_index_at(frame); if (m_playlists[i].is_blank(ix)) { int blank_length = m_playlists[i].clip_length(ix); if (min_length == 0 || (blank_length > 0 && blank_length < min_length)) { min_length = blank_length; } } } return min_length; } int TrackModel::suggestCompositionLength(int position) { READ_LOCK(); if (m_playlists[0].is_blank_at(position) && m_playlists[1].is_blank_at(position)) { return -1; } auto clip_loc = getClipIndexAt(position); int track = clip_loc.first; int index = clip_loc.second; int other_index; // index in the other track int other_track = (track + 1) % 2; int end_pos = m_playlists[track].clip_start(index) + m_playlists[track].clip_length(index); other_index = m_playlists[other_track].get_clip_index_at(end_pos); if (other_index < m_playlists[other_track].count()) { end_pos = std::min(end_pos, m_playlists[other_track].clip_start(other_index) + m_playlists[other_track].clip_length(other_index)); } int min = -1; std::unordered_set existing = getCompositionsInRange(position, end_pos); if (existing.size() > 0) { for (int id : existing) { if (min < 0) { min = m_allCompositions[id]->getPosition(); } else { min = qMin(min, m_allCompositions[id]->getPosition()); } } } if (min >= 0) { // An existing composition is limiting the space end_pos = min; } return end_pos - position; } int TrackModel::getBlankSizeNearClip(int clipId, bool after) { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); int clip_position = m_allClips[clipId]->getPosition(); auto clip_loc = getClipIndexAt(clip_position); int track = clip_loc.first; int index = clip_loc.second; int other_index; // index in the other track int other_track = (track + 1) % 2; if (after) { int first_pos = m_playlists[track].clip_start(index) + m_playlists[track].clip_length(index); other_index = m_playlists[other_track].get_clip_index_at(first_pos); index++; } else { int last_pos = m_playlists[track].clip_start(index) - 1; other_index = m_playlists[other_track].get_clip_index_at(last_pos); index--; } if (index < 0) return 0; int length = INT_MAX; if (index < m_playlists[track].count()) { if (!m_playlists[track].is_blank(index)) { return 0; } length = std::min(length, m_playlists[track].clip_length(index)); } if (other_index < m_playlists[other_track].count()) { if (!m_playlists[other_track].is_blank(other_index)) { return 0; } length = std::min(length, m_playlists[other_track].clip_length(other_index)); } return length; } int TrackModel::getBlankSizeNearComposition(int compoId, bool after) { READ_LOCK(); Q_ASSERT(m_allCompositions.count(compoId) > 0); int clip_position = m_allCompositions[compoId]->getPosition(); Q_ASSERT(m_compoPos.count(clip_position) > 0); Q_ASSERT(m_compoPos[clip_position] == compoId); auto it = m_compoPos.find(clip_position); int clip_length = m_allCompositions[compoId]->getPlaytime(); int length = INT_MAX; if (after) { ++it; if (it != m_compoPos.end()) { return it->first - clip_position - clip_length; } } else { if (it != m_compoPos.begin()) { --it; return clip_position - it->first - m_allCompositions[it->second]->getPlaytime(); } return clip_position; } return length; } Fun TrackModel::requestClipResize_lambda(int clipId, int in, int out, bool right) { QWriteLocker locker(&m_lock); int clip_position = m_allClips[clipId]->getPosition(); int old_in = clip_position; int old_out = old_in + m_allClips[clipId]->getPlaytime(); auto clip_loc = getClipIndexAt(clip_position); int target_track = clip_loc.first; int target_clip = clip_loc.second; Q_ASSERT(target_clip < m_playlists[target_track].count()); int size = out - in + 1; bool checkRefresh = false; if (!isHidden() && !isAudioTrack()) { checkRefresh = true; } - auto update_snaps = [clipId, old_in, old_out, checkRefresh, this](int new_in, int new_out) { + auto update_snaps = [old_in, old_out, checkRefresh, this](int new_in, int new_out) { if (auto ptr = m_parent.lock()) { ptr->m_snaps->removePoint(old_in); ptr->m_snaps->removePoint(old_out); ptr->m_snaps->addPoint(new_in); ptr->m_snaps->addPoint(new_out); if (checkRefresh) { ptr->checkRefresh(old_in, old_out); ptr->checkRefresh(new_in, new_out); // ptr->adjustAssetRange(clipId, m_allClips[clipId]->getIn(), m_allClips[clipId]->getOut()); } } else { qDebug() << "Error : clip resize failed because parent timeline is not available anymore"; Q_ASSERT(false); } }; int delta = m_allClips[clipId]->getPlaytime() - size; if (delta == 0) { return []() { return true; }; } // qDebug() << "RESIZING CLIP: " << clipId << " FROM: " << delta; if (delta > 0) { // we shrink clip return [right, target_clip, target_track, clip_position, delta, in, out, clipId, update_snaps, this]() { int target_clip_mutable = target_clip; int blank_index = right ? (target_clip_mutable + 1) : target_clip_mutable; // insert blank to space that is going to be empty // The second is parameter is delta - 1 because this function expects an out time, which is basically size - 1 m_playlists[target_track].insert_blank(blank_index, delta - 1); if (!right) { m_allClips[clipId]->setPosition(clip_position + delta); // Because we inserted blank before, the index of our clip has increased target_clip_mutable++; } int err = m_playlists[target_track].resize_clip(target_clip_mutable, in, out); // make sure to do this after, to avoid messing the indexes m_playlists[target_track].consolidate_blanks(); if (err == 0) { update_snaps(m_allClips[clipId]->getPosition(), m_allClips[clipId]->getPosition() + out - in + 1); if (right && m_playlists[target_track].count() - 1 == target_clip_mutable) { // deleted last clip in playlist if (auto ptr = m_parent.lock()) { ptr->updateDuration(); } } } return err == 0; }; } int blank = -1; int other_blank_end = getBlankEnd(clip_position, (target_track + 1) % 2); if (right) { if (target_clip == m_playlists[target_track].count() - 1 && other_blank_end >= out) { // clip is last, it can always be extended return [this, target_clip, target_track, in, out, update_snaps, clipId]() { // color, image and title clips can have unlimited resize QScopedPointer clip(m_playlists[target_track].get_clip(target_clip)); if (out >= clip->get_length()) { clip->parent().set("length", out + 1); clip->parent().set("out", out); clip->set("length", out + 1); } int err = m_playlists[target_track].resize_clip(target_clip, in, out); if (err == 0) { update_snaps(m_allClips[clipId]->getPosition(), m_allClips[clipId]->getPosition() + out - in + 1); } m_playlists[target_track].consolidate_blanks(); if (m_playlists[target_track].count() - 1 == target_clip) { // deleted last clip in playlist if (auto ptr = m_parent.lock()) { ptr->updateDuration(); } } return err == 0; }; } blank = target_clip + 1; } else { if (target_clip == 0) { // clip is first, it can never be extended on the left return []() { return false; }; } blank = target_clip - 1; } if (m_playlists[target_track].is_blank(blank)) { int blank_length = m_playlists[target_track].clip_length(blank); if (blank_length + delta >= 0 && other_blank_end >= out) { return [blank_length, blank, right, clipId, delta, update_snaps, this, in, out, target_clip, target_track]() { int target_clip_mutable = target_clip; int err = 0; if (blank_length + delta == 0) { err = m_playlists[target_track].remove(blank); if (!right) { target_clip_mutable--; } } else { err = m_playlists[target_track].resize_clip(blank, 0, blank_length + delta - 1); } if (err == 0) { QScopedPointer clip(m_playlists[target_track].get_clip(target_clip_mutable)); if (out >= clip->get_length()) { clip->parent().set("length", out + 1); clip->parent().set("out", out); clip->set("length", out + 1); } err = m_playlists[target_track].resize_clip(target_clip_mutable, in, out); } if (!right && err == 0) { m_allClips[clipId]->setPosition(m_playlists[target_track].clip_start(target_clip_mutable)); } if (err == 0) { update_snaps(m_allClips[clipId]->getPosition(), m_allClips[clipId]->getPosition() + out - in + 1); } m_playlists[target_track].consolidate_blanks(); return err == 0; }; } } return []() { return false; }; } int TrackModel::getId() const { return m_id; } int TrackModel::getClipByPosition(int position) { READ_LOCK(); QSharedPointer prod(nullptr); if (m_playlists[0].count() > 0) { prod = QSharedPointer(m_playlists[0].get_clip_at(position)); } if ((!prod || prod->is_blank()) && m_playlists[1].count() > 0) { prod = QSharedPointer(m_playlists[1].get_clip_at(position)); } if (!prod || prod->is_blank()) { return -1; } return prod->get_int("_kdenlive_cid"); } QSharedPointer TrackModel::getClipProducer(int clipId) { READ_LOCK(); QSharedPointer prod(nullptr); if (m_playlists[0].count() > 0) { prod = QSharedPointer(m_playlists[0].get_clip(clipId)); } if ((!prod || prod->is_blank()) && m_playlists[1].count() > 0) { prod = QSharedPointer(m_playlists[1].get_clip(clipId)); } return prod; } int TrackModel::getCompositionByPosition(int position) { READ_LOCK(); for (const auto &comp : m_compoPos) { if (comp.first == position) { return comp.second; } else if (comp.first < position) { if (comp.first + m_allCompositions[comp.second]->getPlaytime() >= position) { return comp.second; } } } return -1; } int TrackModel::getClipByRow(int row) const { READ_LOCK(); if (row >= static_cast(m_allClips.size())) { return -1; } auto it = m_allClips.cbegin(); std::advance(it, row); return (*it).first; } std::unordered_set TrackModel::getClipsInRange(int position, int end) { READ_LOCK(); std::unordered_set ids; for (const auto &clp : m_allClips) { int pos = clp.second->getPosition(); int length = clp.second->getPlaytime(); if (end > -1 && pos >= end) { continue; } if (pos >= position || pos + length - 1 >= position) { ids.insert(clp.first); } } return ids; } int TrackModel::getRowfromClip(int clipId) const { READ_LOCK(); Q_ASSERT(m_allClips.count(clipId) > 0); return (int)std::distance(m_allClips.begin(), m_allClips.find(clipId)); } std::unordered_set TrackModel::getCompositionsInRange(int position, int end) { READ_LOCK(); // TODO: this function doesn't take into accounts the fact that there are two tracks std::unordered_set ids; for (const auto &compo : m_allCompositions) { int pos = compo.second->getPosition(); int length = compo.second->getPlaytime(); if (end > -1 && pos >= end) { continue; } if (pos >= position || pos + length - 1 >= position) { ids.insert(compo.first); } } return ids; } int TrackModel::getRowfromComposition(int tid) const { READ_LOCK(); Q_ASSERT(m_allCompositions.count(tid) > 0); return (int)m_allClips.size() + (int)std::distance(m_allCompositions.begin(), m_allCompositions.find(tid)); } QVariant TrackModel::getProperty(const QString &name) const { READ_LOCK(); return QVariant(m_track->get(name.toUtf8().constData())); } void TrackModel::setProperty(const QString &name, const QString &value) { QWriteLocker locker(&m_lock); m_track->set(name.toUtf8().constData(), value.toUtf8().constData()); // Hide property mus be defined at playlist level or it won't be saved if (name == QLatin1String("kdenlive:audio_track") || name == QLatin1String("hide")) { for (int i = 0; i < 2; i++) { m_playlists[i].set(name.toUtf8().constData(), value.toInt()); } } } bool TrackModel::checkConsistency() { auto ptr = m_parent.lock(); if (!ptr) { return false; } std::vector> clips; // clips stored by (position, id) for (const auto &c : m_allClips) { Q_ASSERT(c.second); Q_ASSERT(c.second.get() == ptr->getClipPtr(c.first).get()); clips.push_back({c.second->getPosition(), c.first}); } std::sort(clips.begin(), clips.end()); size_t current_clip = 0; int playtime = std::max(m_playlists[0].get_playtime(), m_playlists[1].get_playtime()); for (int i = 0; i < playtime; i++) { int track, index; if (isBlankAt(i)) { track = 0; index = m_playlists[0].get_clip_index_at(i); } else { auto clip_loc = getClipIndexAt(i); track = clip_loc.first; index = clip_loc.second; } Q_ASSERT(m_playlists[(track + 1) % 2].is_blank_at(i)); if (current_clip < clips.size() && i >= clips[current_clip].first) { auto clip = m_allClips[clips[current_clip].second]; if (i >= clips[current_clip].first + clip->getPlaytime()) { current_clip++; i--; continue; } if (isBlankAt(i)) { qDebug() << "ERROR: Found blank when clip was required at position " << i; return false; } auto pr = m_playlists[track].get_clip(index); Mlt::Producer prod(pr); if (!prod.same_clip(*clip)) { qDebug() << "ERROR: Wrong clip at position " << i; delete pr; return false; } delete pr; } else { if (!isBlankAt(i)) { qDebug() << "ERROR: Found clip when blank was required at position " << i; return false; } } } // We now check compositions positions if (m_allCompositions.size() != m_compoPos.size()) { qDebug() << "Error: the number of compositions position doesn't match number of compositions"; return false; } for (const auto &compo : m_allCompositions) { int pos = compo.second->getPosition(); if (m_compoPos.count(pos) == 0) { qDebug() << "Error: the position of composition " << compo.first << " is not properly stored"; return false; } if (m_compoPos[pos] != compo.first) { qDebug() << "Error: found composition" << m_compoPos[pos] << "instead of " << compo.first << "at position" << pos; return false; } } for (auto it = m_compoPos.begin(); it != m_compoPos.end(); ++it) { int compoId = it->second; int cur_in = m_allCompositions[compoId]->getPosition(); Q_ASSERT(cur_in == it->first); int cur_out = cur_in + m_allCompositions[compoId]->getPlaytime() - 1; ++it; if (it != m_compoPos.end()) { int next_compoId = it->second; int next_in = m_allCompositions[next_compoId]->getPosition(); int next_out = next_in + m_allCompositions[next_compoId]->getPlaytime() - 1; if (next_in <= cur_out) { qDebug() << "Error: found collision between composition " << compoId << "[ " << cur_in << ", " << cur_out << "] and " << next_compoId << "[ " << next_in << ", " << next_out << "]"; return false; } } --it; } return true; } std::pair TrackModel::getClipIndexAt(int position) { READ_LOCK(); for (int j = 0; j < 2; j++) { if (!m_playlists[j].is_blank_at(position)) { return {j, m_playlists[j].get_clip_index_at(position)}; } } Q_ASSERT(false); return {-1, -1}; } bool TrackModel::isBlankAt(int position) { READ_LOCK(); return m_playlists[0].is_blank_at(position) && m_playlists[1].is_blank_at(position); } int TrackModel::getBlankStart(int position) { READ_LOCK(); int result = 0; for (int j = 0; j < 2; j++) { if (m_playlists[j].count() == 0) { break; } if (!m_playlists[j].is_blank_at(position)) { result = position; break; } int clip_index = m_playlists[j].get_clip_index_at(position); int start = m_playlists[j].clip_start(clip_index); if (start > result) { result = start; } } return result; } int TrackModel::getBlankEnd(int position, int track) { READ_LOCK(); // Q_ASSERT(m_playlists[track].is_blank_at(position)); if (!m_playlists[track].is_blank_at(position)) { return position; } int clip_index = m_playlists[track].get_clip_index_at(position); int count = m_playlists[track].count(); if (clip_index < count) { int blank_start = m_playlists[track].clip_start(clip_index); int blank_length = m_playlists[track].clip_length(clip_index); return blank_start + blank_length; } return INT_MAX; } int TrackModel::getBlankEnd(int position) { READ_LOCK(); int end = INT_MAX; for (int j = 0; j < 2; j++) { end = std::min(getBlankEnd(position, j), end); } return end; } Fun TrackModel::requestCompositionResize_lambda(int compoId, int in, int out, bool logUndo) { QWriteLocker locker(&m_lock); int compo_position = m_allCompositions[compoId]->getPosition(); Q_ASSERT(m_compoPos.count(compo_position) > 0); Q_ASSERT(m_compoPos[compo_position] == compoId); int old_in = compo_position; int old_out = old_in + m_allCompositions[compoId]->getPlaytime() - 1; qDebug() << "compo resize " << compoId << in << "-" << out << " / " << old_in << "-" << old_out; if (out == -1) { out = in + old_out - old_in; } - auto update_snaps = [compoId, old_in, old_out, logUndo, this](int new_in, int new_out) { + auto update_snaps = [old_in, old_out, logUndo, this](int new_in, int new_out) { if (auto ptr = m_parent.lock()) { ptr->m_snaps->removePoint(old_in); ptr->m_snaps->removePoint(old_out + 1); ptr->m_snaps->addPoint(new_in); ptr->m_snaps->addPoint(new_out); ptr->checkRefresh(old_in, old_out); ptr->checkRefresh(new_in, new_out); if (logUndo) { ptr->invalidateZone(old_in, old_out); ptr->invalidateZone(new_in, new_out); } // ptr->adjustAssetRange(compoId, new_in, new_out); } else { qDebug() << "Error : Composition resize failed because parent timeline is not available anymore"; Q_ASSERT(false); } }; if (in == compo_position && (out == -1 || out == old_out)) { return []() { qDebug() << "//// NO MOVE PERFORMED\n!!!!!!!!!!!!!!!!!!!!!!!!!!"; return true; }; } // temporary remove of current compo to check collisions qDebug() << "// CURRENT COMPOSITIONS ----\n" << m_compoPos << "\n--------------"; m_compoPos.erase(compo_position); bool intersecting = hasIntersectingComposition(in, out); // put it back m_compoPos[compo_position] = compoId; if (intersecting) { return []() { qDebug() << "//// FALSE MOVE PERFORMED\n!!!!!!!!!!!!!!!!!!!!!!!!!!"; return false; }; } return [in, out, compoId, update_snaps, this]() { m_compoPos.erase(m_allCompositions[compoId]->getPosition()); m_allCompositions[compoId]->setInOut(in, out); update_snaps(in, out + 1); m_compoPos[m_allCompositions[compoId]->getPosition()] = compoId; return true; }; } bool TrackModel::requestCompositionInsertion(int compoId, int position, bool updateView, bool finalMove, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); if (isLocked()) { return false; } auto operation = requestCompositionInsertion_lambda(compoId, position, updateView, finalMove); if (operation()) { auto reverse = requestCompositionDeletion_lambda(compoId, updateView, finalMove); UPDATE_UNDO_REDO(operation, reverse, undo, redo); return true; } return false; } bool TrackModel::requestCompositionDeletion(int compoId, bool updateView, bool finalMove, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); if (isLocked()) { return false; } Q_ASSERT(m_allCompositions.count(compoId) > 0); auto old_composition = m_allCompositions[compoId]; int old_position = old_composition->getPosition(); Q_ASSERT(m_compoPos.count(old_position) > 0); Q_ASSERT(m_compoPos[old_position] == compoId); auto operation = requestCompositionDeletion_lambda(compoId, updateView, finalMove); if (operation()) { auto reverse = requestCompositionInsertion_lambda(compoId, old_position, updateView, finalMove); UPDATE_UNDO_REDO(operation, reverse, undo, redo); return true; } return false; } Fun TrackModel::requestCompositionDeletion_lambda(int compoId, bool updateView, bool finalMove) { QWriteLocker locker(&m_lock); // Find index of clip int clip_position = m_allCompositions[compoId]->getPosition(); int old_in = clip_position; int old_out = old_in + m_allCompositions[compoId]->getPlaytime(); - return [clip_position, compoId, old_in, old_out, updateView, finalMove, this]() { + return [compoId, old_in, old_out, updateView, finalMove, this]() { int old_clip_index = getRowfromComposition(compoId); auto ptr = m_parent.lock(); if (updateView) { ptr->_beginRemoveRows(ptr->makeTrackIndexFromID(getId()), old_clip_index, old_clip_index); ptr->_endRemoveRows(); } m_allCompositions[compoId]->setCurrentTrackId(-1); m_allCompositions.erase(compoId); m_compoPos.erase(old_in); ptr->m_snaps->removePoint(old_in); ptr->m_snaps->removePoint(old_out); if (finalMove) { ptr->invalidateZone(old_in, old_out); } return true; }; } int TrackModel::getCompositionByRow(int row) const { READ_LOCK(); if (row < (int)m_allClips.size()) { return -1; } Q_ASSERT(row <= (int)m_allClips.size() + (int)m_allCompositions.size()); auto it = m_allCompositions.cbegin(); std::advance(it, row - (int)m_allClips.size()); return (*it).first; } int TrackModel::getCompositionsCount() const { READ_LOCK(); return (int)m_allCompositions.size(); } Fun TrackModel::requestCompositionInsertion_lambda(int compoId, int position, bool updateView, bool finalMove) { QWriteLocker locker(&m_lock); bool intersecting = true; if (auto ptr = m_parent.lock()) { intersecting = hasIntersectingComposition(position, position + ptr->getCompositionPlaytime(compoId) - 1); } else { qDebug() << "Error : Composition Insertion failed because timeline is not available anymore"; } if (!intersecting) { return [compoId, this, position, updateView, finalMove]() { if (auto ptr = m_parent.lock()) { std::shared_ptr composition = ptr->getCompositionPtr(compoId); m_allCompositions[composition->getId()] = composition; // store clip // update clip position and track composition->setCurrentTrackId(getId()); int new_in = position; int new_out = new_in + composition->getPlaytime(); composition->setInOut(new_in, new_out - 1); if (updateView) { int composition_index = getRowfromComposition(composition->getId()); ptr->_beginInsertRows(ptr->makeTrackIndexFromID(composition->getCurrentTrackId()), composition_index, composition_index); ptr->_endInsertRows(); } ptr->m_snaps->addPoint(new_in); ptr->m_snaps->addPoint(new_out); m_compoPos[new_in] = composition->getId(); if (finalMove) { ptr->invalidateZone(new_in, new_out); } return true; } qDebug() << "Error : Composition Insertion failed because timeline is not available anymore"; return false; }; } return []() { return false; }; } bool TrackModel::hasIntersectingComposition(int in, int out) const { READ_LOCK(); auto it = m_compoPos.lower_bound(in); if (m_compoPos.empty()) { return false; } if (it != m_compoPos.end() && it->first <= out) { // compo at it intersects return true; } if (it == m_compoPos.begin()) { return false; } --it; int end = it->first + m_allCompositions.at(it->second)->getPlaytime() - 1; return end >= in; return false; } bool TrackModel::addEffect(const QString &effectId) { READ_LOCK(); return m_effectStack->appendEffect(effectId); } const QString TrackModel::effectNames() const { READ_LOCK(); return m_effectStack->effectNames(); } bool TrackModel::stackEnabled() const { READ_LOCK(); return m_effectStack->isStackEnabled(); } void TrackModel::setEffectStackEnabled(bool enable) { m_effectStack->setEffectStackEnabled(enable); } int TrackModel::trackDuration() { return m_track->get_length(); } bool TrackModel::isLocked() const { READ_LOCK(); return m_track->get_int("kdenlive:locked_track"); } bool TrackModel::isAudioTrack() const { return m_track->get_int("kdenlive:audio_track") == 1; } PlaylistState::ClipState TrackModel::trackType() const { return (m_track->get_int("kdenlive:audio_track") == 1 ? PlaylistState::AudioOnly : PlaylistState::VideoOnly); } bool TrackModel::isHidden() const { return m_track->get_int("hide") & 1; } bool TrackModel::isMute() const { return m_track->get_int("hide") & 2; } bool TrackModel::importEffects(std::weak_ptr service) { QWriteLocker locker(&m_lock); - m_effectStack->importEffects(service, trackType()); + m_effectStack->importEffects(std::move(service), trackType()); return true; } -bool TrackModel::copyEffect(std::shared_ptr stackModel, int rowId) +bool TrackModel::copyEffect(const std::shared_ptr &stackModel, int rowId) { QWriteLocker locker(&m_lock); return m_effectStack->copyEffect(stackModel->getEffectStackRow(rowId), isAudioTrack() ? PlaylistState::AudioOnly : PlaylistState::VideoOnly); } diff --git a/src/timeline2/model/trackmodel.hpp b/src/timeline2/model/trackmodel.hpp index 1b8337166..2f5fd7e99 100644 --- a/src/timeline2/model/trackmodel.hpp +++ b/src/timeline2/model/trackmodel.hpp @@ -1,274 +1,274 @@ /*************************************************************************** * Copyright (C) 2017 by Nicolas Carion * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #ifndef TRACKMODEL_H #define TRACKMODEL_H #include "definitions.h" #include "undohelper.hpp" #include #include #include #include #include #include #include class TimelineModel; class ClipModel; class CompositionModel; class EffectStackModel; /* @brief This class represents a Track object, as viewed by the backend. To allow same track transitions, a Track object corresponds to two Mlt::Playlist, between which we can switch when required by the transitions. In general, the Gui associated with it will send modification queries (such as resize or move), and this class authorize them or not depending on the validity of the modifications */ class TrackModel { public: TrackModel() = delete; ~TrackModel(); friend class ClipModel; friend class CompositionModel; friend class TimelineController; friend struct TimelineFunctions; friend class TimelineItemModel; friend class TimelineModel; private: /* This constructor is private, call the static construct instead */ TrackModel(const std::weak_ptr &parent, int id = -1, const QString &trackName = QString(), bool audioTrack = false); TrackModel(const std::weak_ptr &parent, Mlt::Tractor mltTrack, int id = -1); public: /* @brief Creates a track, which references itself to the parent Returns the (unique) id of the created track @param id Requested id of the track. Automatic if id = -1 @param pos is the optional position of the track. If left to -1, it will be added at the end */ static int construct(const std::weak_ptr &parent, int id = -1, int pos = -1, const QString &trackName = QString(), bool audioTrack = false); /* @brief returns the number of clips */ int getClipsCount(); /* @brief returns the number of compositions */ int getCompositionsCount() const; /* Perform a split at the requested position */ bool splitClip(QSharedPointer caller, int position); /* Implicit conversion operator to access the underlying producer */ operator Mlt::Producer &() { return *m_track.get(); } /* @brief Returns true if track is in locked state */ bool isLocked() const; /* @brief Returns true if track is an audio track */ bool isAudioTrack() const; /* @brief Returns the track type (audio / video) */ PlaylistState::ClipState trackType() const; /* @brief Returns true if track is disabled */ bool isHidden() const; /* @brief Returns true if track is disabled */ bool isMute() const; // TODO make protected QVariant getProperty(const QString &name) const; void setProperty(const QString &name, const QString &value); protected: /* @brief Returns a lambda that performs a resize of the given clip. The lamda returns true if the operation succeeded, and otherwise nothing is modified This method is protected because it shouldn't be called directly. Call the function in the timeline instead. @param clipId is the id of the clip @param in is the new starting on the clip @param out is the new ending on the clip @param right is true if we change the right side of the clip, false otherwise */ Fun requestClipResize_lambda(int clipId, int in, int out, bool right); /* @brief Performs an insertion of the given clip. Returns true if the operation succeeded, and otherwise, the track is not modified. This method is protected because it shouldn't be called directly. Call the function in the timeline instead. @param clip is the id of the clip @param position is the position where to insert the clip @param updateView whether we send update to the view @param finalMove if the move is finished (not while dragging), so we invalidate timeline preview / check project duration @param undo Lambda function containing the current undo stack. Will be updated with current operation @param redo Lambda function containing the current redo queue. Will be updated with current operation */ bool requestClipInsertion(int clipId, int position, bool updateView, bool finalMove, Fun &undo, Fun &redo); /* @brief This function returns a lambda that performs the requested operation */ Fun requestClipInsertion_lambda(int clipId, int position, bool updateView, bool finalMove); /* @brief Performs an deletion of the given clip. Returns true if the operation succeeded, and otherwise, the track is not modified. This method is protected because it shouldn't be called directly. Call the function in the timeline instead. @param clipId is the id of the clip @param updateView whether we send update to the view @param finalMove if the move is finished (not while dragging), so we invalidate timeline preview / check project duration @param undo Lambda function containing the current undo stack. Will be updated with current operation @param redo Lambda function containing the current redo queue. Will be updated with current operation */ bool requestClipDeletion(int clipId, bool updateView, bool finalMove, Fun &undo, Fun &redo); /* @brief This function returns a lambda that performs the requested operation */ Fun requestClipDeletion_lambda(int clipId, bool updateView, bool finalMove); /* @brief Performs an insertion of the given composition. Returns true if the operation succeeded, and otherwise, the track is not modified. This method is protected because it shouldn't be called directly. Call the function in the timeline instead. Note that in Mlt, the composition insertion logic is not really at the track level, but we use that level to do collision checking @param compoId is the id of the composition @param position is the position where to insert the composition @param updateView whether we send update to the view @param undo Lambda function containing the current undo stack. Will be updated with current operation @param redo Lambda function containing the current redo queue. Will be updated with current operation */ bool requestCompositionInsertion(int compoId, int position, bool updateView, bool finalMove, Fun &undo, Fun &redo); /* @brief This function returns a lambda that performs the requested operation */ Fun requestCompositionInsertion_lambda(int compoId, int position, bool updateView, bool finalMove = false); bool requestCompositionDeletion(int compoId, bool updateView, bool finalMove, Fun &undo, Fun &redo); Fun requestCompositionDeletion_lambda(int compoId, bool updateView, bool finalMove = false); Fun requestCompositionResize_lambda(int compoId, int in, int out = -1, bool logUndo = false); /* @brief Returns the size of the blank before or after the given clip @param clipId is the id of the clip @param after is true if we query the blank after, false otherwise */ int getBlankSizeNearClip(int clipId, bool after); int getBlankSizeNearComposition(int compoId, bool after); int getBlankStart(int position); int getBlankSizeAtPos(int frame); /*@brief Returns the best composition duration depending on clips on the track */ int suggestCompositionLength(int position); /*@brief Returns the (unique) construction id of the track*/ int getId() const; /*@brief This function is used only by the QAbstractItemModel Given a row in the model, retrieves the corresponding clip id. If it does not exist, returns -1 */ int getClipByRow(int row) const; /*@brief This function is used only by the QAbstractItemModel Given a row in the model, retrieves the corresponding composition id. If it does not exist, returns -1 */ int getCompositionByRow(int row) const; /*@brief This function is used only by the QAbstractItemModel Given a clip ID, returns the row of the clip. */ int getRowfromClip(int clipId) const; /*@brief This function is used only by the QAbstractItemModel Given a composition ID, returns the row of the composition. */ int getRowfromComposition(int compoId) const; /*@brief This is an helper function that test frame level consistency with the MLT structures */ bool checkConsistency(); /* @brief Returns true if we have a composition intersecting with the range [in,out]*/ bool hasIntersectingComposition(int in, int out) const; /* @brief This is an helper function that returns the sub-playlist in which the clip is inserted, along with its index in the playlist @param position the position of the target clip*/ std::pair getClipIndexAt(int position); QSharedPointer getClipProducer(int clipId); /* @brief This is an helper function that checks in all playlists if the given position is a blank */ bool isBlankAt(int position); /* @brief This is an helper function that returns the end of the blank that covers given position */ int getBlankEnd(int position); /* Same, but we restrict to a specific track*/ int getBlankEnd(int position, int track); /* @brief Returns the clip id on this track at position requested, or -1 if no clip */ int getClipByPosition(int position); /* @brief Returns the composition id on this track starting position requested, or -1 if not found */ int getCompositionByPosition(int position); /* @brief Add a track effect */ bool addEffect(const QString &effectId); /* @brief Returns a comma separated list of effect names */ const QString effectNames() const; /* @brief Returns true if effect stack is enabled */ bool stackEnabled() const; /* @brief Enable / disable the track's effect stack */ void setEffectStackEnabled(bool enable); /* @brief This function removes the clip from the mlt object, and then insert it back in the same spot again. * This is used when some properties of the clip have changed, and we need this to refresh it */ void replugClip(int clipId); int trackDuration(); /* @brief Returns the list of the ids of the clips that intersect the given range */ std::unordered_set getClipsInRange(int position, int end = -1); /* @brief Returns the list of the ids of the compositions that intersect the given range */ std::unordered_set getCompositionsInRange(int position, int end); /* @brief Import effects from a service that contains some (another track) */ bool importEffects(std::weak_ptr service); /* @brief Copy effects from anoter effect stack */ - bool copyEffect(std::shared_ptr stackModel, int rowId); + bool copyEffect(const std::shared_ptr &stackModel, int rowId); public slots: /*Delete the current track and all its associated clips */ void slotDelete(); private: std::weak_ptr m_parent; int m_id; // this is the creation id of the track, used for book-keeping // We fake two playlists to allow same track transitions. std::shared_ptr m_track; Mlt::Playlist m_playlists[2]; std::map> m_allClips; /*this is important to keep an ordered structure to store the clips, since we use their ids order as row order*/ std::map> m_allCompositions; /*this is important to keep an ordered structure to store the clips, since we use their ids order as row order*/ std::map m_compoPos; // We store the positions of the compositions. In Melt, the compositions are not inserted at the track level, but we keep // those positions here to check for moves and resize mutable QReadWriteLock m_lock; // This is a lock that ensures safety in case of concurrent access protected: std::shared_ptr m_effectStack; }; #endif diff --git a/src/timeline2/view/dialogs/trackdialog.cpp b/src/timeline2/view/dialogs/trackdialog.cpp index 1a9224f10..c1c0bcab8 100644 --- a/src/timeline2/view/dialogs/trackdialog.cpp +++ b/src/timeline2/view/dialogs/trackdialog.cpp @@ -1,103 +1,103 @@ /*************************************************************************** * Copyright (C) 2017 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 "trackdialog.h" #include "kdenlivesettings.h" #include -TrackDialog::TrackDialog(std::shared_ptr model, int trackIndex, QWidget *parent, bool deleteMode) +TrackDialog::TrackDialog(const std::shared_ptr &model, int trackIndex, QWidget *parent, bool deleteMode) : QDialog(parent) , m_audioCount(1) , m_videoCount(1) { setWindowTitle(deleteMode ? i18n("Delete Track") : i18n("Add Track")); // setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)); QIcon videoIcon = QIcon::fromTheme(QStringLiteral("kdenlive-show-video")); QIcon audioIcon = QIcon::fromTheme(QStringLiteral("kdenlive-show-audio")); setupUi(this); QStringList existingTrackNames; for (int i = model->getTracksCount() - 1; i >= 0; i--) { int tid = model->getTrackIndexFromPosition(i); bool audioTrack = model->isAudioTrack(tid); if (audioTrack) { m_audioCount++; } else { m_videoCount++; } const QString trackName = model->getTrackFullName(tid); existingTrackNames << trackName; comboTracks->addItem(audioTrack ? audioIcon : videoIcon, trackName.isEmpty() ? QString::number(i) : trackName, tid); // Track index in in MLT, so add + 1 to compensate black track m_positionByIndex.insert(tid, i + 1); } if (trackIndex > -1) { int ix = comboTracks->findData(trackIndex); comboTracks->setCurrentIndex(ix); if (model->isAudioTrack(trackIndex)) { audio_track->setChecked(true); } } trackIndex--; if (deleteMode) { track_name->setVisible(false); video_track->setVisible(false); audio_track->setVisible(false); name_label->setVisible(false); before_select->setVisible(false); label->setText(i18n("Delete Track")); } else { // No default name since we now use tags /*QString proposedName = i18n("Video %1", trackIndex); while (existingTrackNames.contains(proposedName)) { proposedName = i18n("Video %1", ++trackIndex); } track_name->setText(proposedName);*/ } } int TrackDialog::selectedTrackPosition() const { if (comboTracks->count() > 0) { int position = m_positionByIndex.value(comboTracks->currentData().toInt()); if (before_select->currentIndex() == 1) { position--; } return position; } return -1; } int TrackDialog::selectedTrackId() const { if (comboTracks->count() > 0) { return comboTracks->currentData().toInt(); } return -1; } bool TrackDialog::addAudioTrack() const { return !video_track->isChecked(); } const QString TrackDialog::trackName() const { return track_name->text(); } diff --git a/src/timeline2/view/dialogs/trackdialog.h b/src/timeline2/view/dialogs/trackdialog.h index b62ec2234..49a26118e 100644 --- a/src/timeline2/view/dialogs/trackdialog.h +++ b/src/timeline2/view/dialogs/trackdialog.h @@ -1,52 +1,52 @@ /*************************************************************************** * Copyright (C) 2008 by Jean-Baptiste Mardelle (jb@kdenlive.org) * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #ifndef TRACKDIALOG2_H #define TRACKDIALOG2_H #include "timeline2/model/timelineitemmodel.hpp" #include "ui_addtrack_ui.h" class TrackDialog : public QDialog, public Ui::AddTrack_UI { Q_OBJECT public: - explicit TrackDialog(std::shared_ptr model, int trackIndex = -1, QWidget *parent = nullptr, bool deleteMode = false); + explicit TrackDialog(const std::shared_ptr &model, int trackIndex = -1, QWidget *parent = nullptr, bool deleteMode = false); /** @brief: returns the selected position in MLT */ int selectedTrackPosition() const; /** @brief: returns the selected track's trackId */ int selectedTrackId() const; /** @brief: returns true if we want to insert an audio track */ bool addAudioTrack() const; /** @brief: returns the newly created track name */ const QString trackName() const; private: int m_audioCount; int m_videoCount; QMap m_positionByIndex; }; #endif diff --git a/src/timeline2/view/qmltypes/thumbnailprovider.cpp b/src/timeline2/view/qmltypes/thumbnailprovider.cpp index 74af02e01..8b5ca5651 100644 --- a/src/timeline2/view/qmltypes/thumbnailprovider.cpp +++ b/src/timeline2/view/qmltypes/thumbnailprovider.cpp @@ -1,145 +1,145 @@ /* * Copyright (c) 2013-2016 Meltytech, LLC * Author: Dan Dennedy * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "thumbnailprovider.h" #include "bin/projectclip.h" #include "bin/projectitemmodel.h" #include "core.h" #include "utils/thumbnailcache.hpp" #include #include #include #include #include ThumbnailProvider::ThumbnailProvider() : QQuickImageProvider(QQmlImageProviderBase::Image, QQmlImageProviderBase::ForceAsynchronousImageLoading) //, m_profile(pCore->getCurrentProfilePath().toUtf8().constData()) { } ThumbnailProvider::~ThumbnailProvider() {} void ThumbnailProvider::resetProject() { // m_producers.clear(); } QImage ThumbnailProvider::requestImage(const QString &id, QSize *size, const QSize &requestedSize) { QImage result; // id is binID/#frameNumber QString binId = id.section('/', 0, 0); bool ok; int frameNumber = id.section('#', -1).toInt(&ok); if (ok) { if (ThumbnailCache::get()->hasThumbnail(binId, frameNumber, false)) { result = ThumbnailCache::get()->getThumbnail(binId, frameNumber); *size = result.size(); return result; } std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(binId); if (binClip) { std::shared_ptr prod = binClip->thumbProducer(); if (prod && prod->is_valid()) { result = makeThumbnail(prod, frameNumber, requestedSize); ThumbnailCache::get()->storeThumbnail(binId, frameNumber, result, false); } } /*if (m_producers.contains(binId.toInt())) { producer = m_producers.object(binId.toInt()); } else { m_binClip->thumbProducer(); if (!resource.isEmpty()) { producer = new Mlt::Producer(m_profile, service.toUtf8().constData(), resource.toUtf8().constData()); } else { producer = new Mlt::Producer(m_profile, service.toUtf8().constData()); } std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(binId); if (binClip) { std::shared_ptr projectProducer = binClip->originalProducer(); Mlt::Properties original(projectProducer->get_properties()); Mlt::Properties cloneProps(producer->get_properties()); cloneProps.pass_list(original, "video_index,force_aspect_num,force_aspect_den,force_aspect_ratio,force_fps,force_progressive,force_tff," "force_colorspace,set.force_full_luma,templatetext,autorotate,xmldata"); } Mlt::Filter scaler(m_profile, "swscale"); Mlt::Filter padder(m_profile, "resize"); Mlt::Filter converter(m_profile, "avcolor_space"); producer->attach(scaler); producer->attach(padder); producer->attach(converter); m_producers.insert(binId.toInt(), producer); } if ((producer != nullptr) && producer->is_valid()) { // result = KThumb::getFrame(producer, frameNumber, 0, 0); result = makeThumbnail(producer, frameNumber, requestedSize); ThumbnailCache::get()->storeThumbnail(binId, frameNumber, result, false); //m_cache->insertImage(key, result); } else { qDebug() << "INVALID PRODUCER; " << service << " / " << resource; }*/ } if (size) *size = result.size(); return result; } QString ThumbnailProvider::cacheKey(Mlt::Properties &properties, const QString &service, const QString &resource, const QString &hash, int frameNumber) { QString time = properties.frames_to_time(frameNumber, mlt_time_clock); // Reduce the precision to centiseconds to increase chance for cache hit // without much loss of accuracy. time = time.left(time.size() - 1); QString key; if (hash.isEmpty()) { key = QString("%1 %2 %3").arg(service).arg(resource).arg(time); QCryptographicHash hash2(QCryptographicHash::Sha1); hash2.addData(key.toUtf8()); key = hash2.result().toHex(); } else { key = QString("%1 %2").arg(hash).arg(time); } return key; } -QImage ThumbnailProvider::makeThumbnail(std::shared_ptr producer, int frameNumber, const QSize &requestedSize) +QImage ThumbnailProvider::makeThumbnail(const std::shared_ptr &producer, int frameNumber, const QSize &requestedSize) { Q_UNUSED(requestedSize) producer->seek(frameNumber); QScopedPointer frame(producer->get_frame()); if (frame == nullptr || !frame->is_valid()) { return QImage(); } int ow = 0; // requestedSize.width(); int oh = 0; // requestedSize.height(); /*if (ow > 0 && oh > 0) { frame->set("rescale.interp", "fastest"); frame->set("deinterlace_method", "onefield"); frame->set("top_field_first", -1); }*/ mlt_image_format format = mlt_image_rgb24a; const uchar *image = frame->get_image(format, ow, oh); if (image) { QImage temp(ow, oh, QImage::Format_ARGB32); memcpy(temp.scanLine(0), image, (unsigned)(ow * oh * 4)); return temp.rgbSwapped(); } return QImage(); } diff --git a/src/timeline2/view/qmltypes/thumbnailprovider.h b/src/timeline2/view/qmltypes/thumbnailprovider.h index fceafc654..24e9feb24 100644 --- a/src/timeline2/view/qmltypes/thumbnailprovider.h +++ b/src/timeline2/view/qmltypes/thumbnailprovider.h @@ -1,43 +1,43 @@ /* * Copyright (c) 2013-2016 Meltytech, LLC * Author: Dan Dennedy * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef THUMBNAILPROVIDER_H #define THUMBNAILPROVIDER_H #include #include #include #include #include #include class ThumbnailProvider : public QQuickImageProvider { public: explicit ThumbnailProvider(); virtual ~ThumbnailProvider(); QImage requestImage(const QString &id, QSize *size, const QSize &requestedSize) override; void resetProject(); private: QString cacheKey(Mlt::Properties &properties, const QString &service, const QString &resource, const QString &hash, int frameNumber); - QImage makeThumbnail(std::shared_ptr producer, int frameNumber, const QSize &requestedSize); + QImage makeThumbnail(const std::shared_ptr &producer, int frameNumber, const QSize &requestedSize); QCache m_producers; }; #endif // THUMBNAILPROVIDER_H diff --git a/src/timeline2/view/timelinecontroller.cpp b/src/timeline2/view/timelinecontroller.cpp index e0218977a..841d094e4 100644 --- a/src/timeline2/view/timelinecontroller.cpp +++ b/src/timeline2/view/timelinecontroller.cpp @@ -1,2483 +1,2483 @@ /*************************************************************************** * Copyright (C) 2017 by Jean-Baptiste Mardelle * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #include "timelinecontroller.h" #include "../model/timelinefunctions.hpp" #include "assets/keyframes/model/keyframemodellist.hpp" #include "bin/bin.h" #include "bin/projectfolder.h" #include "bin/model/markerlistmodel.hpp" #include "bin/projectclip.h" #include "bin/projectitemmodel.h" #include "core.h" #include "dialogs/spacerdialog.h" #include "doc/kdenlivedoc.h" #include "effects/effectsrepository.hpp" #include "effects/effectstack/model/effectstackmodel.hpp" #include "kdenlivesettings.h" #include "lib/audio/audioEnvelope.h" #include "previewmanager.h" #include "project/projectmanager.h" #include "timeline2/model/clipmodel.hpp" #include "timeline2/model/compositionmodel.hpp" #include "timeline2/model/groupsmodel.hpp" #include "timeline2/model/timelineitemmodel.hpp" #include "timeline2/model/trackmodel.hpp" #include "timeline2/view/dialogs/clipdurationdialog.h" #include "timeline2/view/dialogs/trackdialog.h" #include "transitions/transitionsrepository.hpp" #include #include #include #include #include #include #include int TimelineController::m_duration = 0; TimelineController::TimelineController(QObject *parent) : QObject(parent) , m_root(nullptr) , m_usePreview(false) , m_position(0) , m_seekPosition(-1) , m_activeTrack(0) , m_audioRef(-1) , m_zone(-1, -1) , m_scale(QFontMetrics(QApplication::font()).maxWidth() / 250) , m_timelinePreview(nullptr) { m_disablePreview = pCore->currentDoc()->getAction(QStringLiteral("disable_preview")); connect(m_disablePreview, &QAction::triggered, this, &TimelineController::disablePreview); connect(this, &TimelineController::selectionChanged, this, &TimelineController::updateClipActions); m_disablePreview->setEnabled(false); } TimelineController::~TimelineController() { delete m_timelinePreview; m_timelinePreview = nullptr; } void TimelineController::setModel(std::shared_ptr model) { delete m_timelinePreview; m_zone = QPoint(-1, -1); m_timelinePreview = nullptr; m_model = std::move(model); m_selection.selectedItems.clear(); m_selection.selectedTrack = -1; connect(m_model.get(), &TimelineItemModel::requestClearAssetView, [&](int id) { pCore->clearAssetPanel(id); }); connect(m_model.get(), &TimelineItemModel::requestMonitorRefresh, [&]() { pCore->requestMonitorRefresh(); }); connect(m_model.get(), &TimelineModel::invalidateZone, this, &TimelineController::invalidateZone, Qt::DirectConnection); connect(m_model.get(), &TimelineModel::durationUpdated, this, &TimelineController::checkDuration); connect(m_model.get(), &TimelineModel::removeFromSelection, this, &TimelineController::slotUpdateSelection); } void TimelineController::setTargetTracks(QPair targets) { setVideoTarget(targets.first >= 0 && targets.first < m_model->getTracksCount() ? m_model->getTrackIndexFromPosition(targets.first) : -1); setAudioTarget(targets.second >= 0 && targets.second < m_model->getTracksCount() ? m_model->getTrackIndexFromPosition(targets.second) : -1); } std::shared_ptr TimelineController::getModel() const { return m_model; } void TimelineController::setRoot(QQuickItem *root) { m_root = root; } Mlt::Tractor *TimelineController::tractor() { return m_model->tractor(); } void TimelineController::removeSelection(int newSelection) { if (!m_selection.selectedItems.contains(newSelection)) { return; } m_selection.selectedItems.removeAll(newSelection); std::unordered_set ids; ids.insert(m_selection.selectedItems.cbegin(), m_selection.selectedItems.cend()); if (ids.size() > 1) { m_model->m_temporarySelectionGroup = m_model->requestClipsGroup(ids, true, GroupType::Selection); } else if (m_model->m_temporarySelectionGroup > -1) { m_model->m_groups->destructGroupItem(m_model->m_temporarySelectionGroup); m_model->m_temporarySelectionGroup = -1; } std::unordered_set newIds; if (m_model->m_temporarySelectionGroup >= 0) { // new items were selected, inform model to prepare for group drag newIds = m_model->getGroupElements(m_selection.selectedItems.constFirst()); } emit selectionChanged(); if (!m_selection.selectedItems.isEmpty()) emitSelectedFromSelection(); else emit selected(nullptr); } void TimelineController::addSelection(int newSelection, bool clear) { if (m_selection.selectedItems.contains(newSelection)) { return; } if (clear) { if (m_model->m_temporarySelectionGroup >= 0) { m_model->m_groups->destructGroupItem(m_model->m_temporarySelectionGroup); m_model->m_temporarySelectionGroup = -1; } m_selection.selectedItems.clear(); } m_selection.selectedItems << newSelection; std::unordered_set ids; ids.insert(m_selection.selectedItems.cbegin(), m_selection.selectedItems.cend()); m_model->m_temporarySelectionGroup = m_model->requestClipsGroup(ids, true, GroupType::Selection); std::unordered_set newIds; if (m_model->m_temporarySelectionGroup >= 0) { // new items were selected, inform model to prepare for group drag newIds = m_model->getGroupElements(m_selection.selectedItems.constFirst()); } emit selectionChanged(); if (!m_selection.selectedItems.isEmpty()) emitSelectedFromSelection(); else emit selected(nullptr); } int TimelineController::getCurrentItem() { // TODO: if selection is empty, return topmost clip under timeline cursor if (m_selection.selectedItems.isEmpty()) { return -1; } // TODO: if selection contains more than 1 clip, return topmost clip under timeline cursor in selection return m_selection.selectedItems.constFirst(); } double TimelineController::scaleFactor() const { return m_scale; } const QString TimelineController::getTrackNameFromMltIndex(int trackPos) { if (trackPos == -1) { return i18n("unknown"); } if (trackPos == 0) { return i18n("Black"); } return m_model->getTrackTagById(m_model->getTrackIndexFromPosition(trackPos - 1)); } const QString TimelineController::getTrackNameFromIndex(int trackIndex) { QString trackName = m_model->getTrackFullName(trackIndex); return trackName.isEmpty() ? m_model->getTrackTagById(trackIndex) : trackName; } QMap TimelineController::getTrackNames(bool videoOnly) { QMap names; for (const auto &track : m_model->m_iteratorTable) { if (videoOnly && m_model->getTrackById_const(track.first)->isAudioTrack()) { continue; } QString trackName = m_model->getTrackFullName(track.first); names[m_model->getTrackMltIndex(track.first)] = trackName; } return names; } void TimelineController::setScaleFactorOnMouse(double scale, bool zoomOnMouse) { /*if (m_duration * scale < width() - 160) { // Don't allow scaling less than full project's width scale = (width() - 160.0) / m_duration; }*/ if (m_root) { m_root->setProperty("zoomOnMouse", zoomOnMouse ? qMin(getMousePos(), duration()) : -1); m_scale = scale; emit scaleFactorChanged(); } else { qWarning("Timeline root not created, impossible to zoom in"); } } void TimelineController::setScaleFactor(double scale) { m_scale = scale; // Update mainwindow's zoom slider emit updateZoom(scale); // inform qml emit scaleFactorChanged(); } int TimelineController::duration() const { return m_duration; } int TimelineController::fullDuration() const { return m_duration + TimelineModel::seekDuration; } void TimelineController::checkDuration() { int currentLength = m_model->duration(); if (currentLength != m_duration) { m_duration = currentLength; emit durationChanged(); } } std::unordered_set TimelineController::getCurrentSelectionIds() const { std::unordered_set selection; if (m_model->m_temporarySelectionGroup >= 0 || (!m_selection.selectedItems.isEmpty() && m_model->m_groups->isInGroup(m_selection.selectedItems.constFirst()))) { selection = m_model->getGroupElements(m_selection.selectedItems.constFirst()); } else { for (int i : m_selection.selectedItems) { selection.insert(i); } } return selection; } void TimelineController::selectCurrentItem(ObjectType type, bool select, bool addToCurrent) { QList toSelect; int currentClip = type == ObjectType::TimelineClip ? m_model->getClipByPosition(m_activeTrack, timelinePosition()) : m_model->getCompositionByPosition(m_activeTrack, timelinePosition()); if (currentClip == -1) { pCore->displayMessage(i18n("No item under timeline cursor in active track"), InformationMessage, 500); return; } if (addToCurrent || !select) { toSelect = m_selection.selectedItems; } if (select) { if (!toSelect.contains(currentClip)) { toSelect << currentClip; setSelection(toSelect); } } else if (toSelect.contains(currentClip)) { toSelect.removeAll(currentClip); setSelection(toSelect); } } void TimelineController::setSelection(const QList &newSelection, int trackIndex, bool isMultitrack) { qDebug() << "Changing selection to" << newSelection << " trackIndex" << trackIndex << "isMultitrack" << isMultitrack; if (newSelection != selection() || trackIndex != m_selection.selectedTrack || isMultitrack != m_selection.isMultitrackSelected) { m_selection.selectedItems = newSelection; m_selection.selectedTrack = trackIndex; m_selection.isMultitrackSelected = isMultitrack; if (m_model->m_temporarySelectionGroup > -1) { // Clear current selection m_model->requestClipUngroup(m_model->m_temporarySelectionGroup, false); } std::unordered_set newIds; if (m_selection.selectedItems.size() > 0) { std::unordered_set ids; ids.insert(m_selection.selectedItems.cbegin(), m_selection.selectedItems.cend()); m_model->m_temporarySelectionGroup = m_model->requestClipsGroup(ids, true, GroupType::Selection); if (m_model->m_temporarySelectionGroup >= 0 || (!m_selection.selectedItems.isEmpty() && m_model->m_groups->isInGroup(m_selection.selectedItems.constFirst()))) { newIds = m_model->getGroupElements(m_selection.selectedItems.constFirst()); } else { qDebug() << "// NON GROUPED SELCTUIIN: " << m_selection.selectedItems << " !!!!!!"; } emitSelectedFromSelection(); } else { // Empty selection emit selected(nullptr); emit showItemEffectStack(QString(), nullptr, QSize(), false); } emit selectionChanged(); } } void TimelineController::emitSelectedFromSelection() { /*if (!m_model.trackList().count()) { if (m_model.tractor()) selectMultitrack(); else emit selected(0); return; } int trackIndex = currentTrack(); int clipIndex = selection().isEmpty()? 0 : selection().first(); Mlt::ClipInfo* info = getClipInfo(trackIndex, clipIndex); if (info && info->producer && info->producer->is_valid()) { delete m_updateCommand; m_updateCommand = new Timeline::UpdateCommand(*this, trackIndex, clipIndex, info->start); // We need to set these special properties so time-based filters // can get information about the cut while still applying filters // to the cut parent. info->producer->set(kFilterInProperty, info->frame_in); info->producer->set(kFilterOutProperty, info->frame_out); if (MLT.isImageProducer(info->producer)) info->producer->set("out", info->cut->get_int("out")); info->producer->set(kMultitrackItemProperty, 1); m_ignoreNextPositionChange = true; emit selected(info->producer); delete info; }*/ } QList TimelineController::selection() const { if (!m_root) return QList(); return m_selection.selectedItems; } void TimelineController::setScrollPos(int pos) { if (pos > 0 && m_root) { QMetaObject::invokeMethod(m_root, "setScrollPos", Qt::QueuedConnection, Q_ARG(QVariant, pos)); } } void TimelineController::selectMultitrack() { setSelection(QList(), -1, true); QMetaObject::invokeMethod(m_root, "selectMultitrack"); // emit selected(m_model.tractor()); } void TimelineController::resetView() { m_model->_resetView(); if (m_root) { QMetaObject::invokeMethod(m_root, "updatePalette"); } emit colorsChanged(); } bool TimelineController::snap() { return KdenliveSettings::snaptopoints(); } void TimelineController::snapChanged(bool snap) { m_root->setProperty("snapping", snap ? 10 / std::sqrt(m_scale) : -1); } bool TimelineController::ripple() { return false; } bool TimelineController::scrub() { return false; } int TimelineController::insertClip(int tid, int position, const QString &data_str, bool logUndo, bool refreshView, bool useTargets) { int id; if (tid == -1) { tid = m_activeTrack; } if (position == -1) { position = timelinePosition(); } if (!m_model->requestClipInsertion(data_str, tid, position, id, logUndo, refreshView, useTargets)) { id = -1; } return id; } QList TimelineController::insertClips(int tid, int position, const QStringList &binIds, bool logUndo, bool refreshView) { QList clipIds; if (tid == -1) { tid = m_activeTrack; } if (position == -1) { position = timelinePosition(); } TimelineFunctions::requestMultipleClipsInsertion(m_model, binIds, tid, position, clipIds, logUndo, refreshView); // we don't need to check the return value of the above function, in case of failure it will return an empty list of ids. return clipIds; } int TimelineController::insertNewComposition(int tid, int position, const QString &transitionId, bool logUndo) { int clipId = m_model->getTrackById_const(tid)->getClipByPosition(position); if (clipId > 0) { int minimum = m_model->getClipPosition(clipId); return insertNewComposition(tid, clipId, position - minimum, transitionId, logUndo); } return insertComposition(tid, position, transitionId, logUndo); } int TimelineController::insertNewComposition(int tid, int clipId, int offset, const QString &transitionId, bool logUndo) { int id; int minimum = m_model->getClipPosition(clipId); int clip_duration = m_model->getClipPlaytime(clipId); int position = minimum; if (offset > clip_duration / 2) { position += offset; } int duration = m_model->getTrackById_const(tid)->suggestCompositionLength(position); int lowerVideoTrackId = m_model->getPreviousVideoTrackIndex(tid); bool revert = false; if (lowerVideoTrackId > 0) { int bottomId = m_model->getTrackById_const(lowerVideoTrackId)->getClipByPosition(position); if (bottomId > 0) { QPair bottom(m_model->m_allClips[bottomId]->getPosition(), m_model->m_allClips[bottomId]->getPlaytime()); if (bottom.first > minimum && position > bottom.first) { int test_duration = m_model->getTrackById_const(tid)->suggestCompositionLength(bottom.first); if (test_duration > 0) { position = bottom.first; duration = test_duration; revert = true; } } } int duration2 = m_model->getTrackById_const(lowerVideoTrackId)->suggestCompositionLength(position); if (duration2 > 0) { duration = (duration > 0) ? qMin(duration, duration2) : duration2; } } if (duration <= 4) { // if suggested composition duration is lower than 4 frames, use default duration = pCore->currentDoc()->getFramePos(KdenliveSettings::transition_duration()); } std::unique_ptr props(nullptr); if (revert) { props.reset(new Mlt::Properties()); if (transitionId == QLatin1String("dissolve")) { props->set("reverse", 1); } else if (transitionId == QLatin1String("composite") || transitionId == QLatin1String("slide")) { props->set("invert", 1); } else if (transitionId == QLatin1String("wipe")) { props->set("geometry", "0%/0%:100%x100%:100;-1=0%/0%:100%x100%:0"); } } if (!m_model->requestCompositionInsertion(transitionId, tid, position, duration, std::move(props), id, logUndo)) { id = -1; pCore->displayMessage(i18n("Could not add composition at selected position"), InformationMessage, 500); } return id; } int TimelineController::insertComposition(int tid, int position, const QString &transitionId, bool logUndo) { int id; int duration = pCore->currentDoc()->getFramePos(KdenliveSettings::transition_duration()); if (!m_model->requestCompositionInsertion(transitionId, tid, position, duration, nullptr, id, logUndo)) { id = -1; } return id; } void TimelineController::deleteSelectedClips() { if (m_selection.selectedItems.isEmpty()) { return; } if (m_model->m_temporarySelectionGroup != -1) { // selection is grouped, delete group only m_selection.selectedItems.clear(); emit selectionChanged(); m_model->requestGroupDeletion(m_model->m_temporarySelectionGroup); return; } else { for (int cid : m_selection.selectedItems) { m_model->requestItemDeletion(cid); } } m_selection.selectedItems.clear(); emit selectionChanged(); } void TimelineController::slotUpdateSelection(int itemId) { if (m_selection.selectedItems.contains(itemId)) { m_selection.selectedItems.removeAll(itemId); emit selectionChanged(); } } void TimelineController::copyItem() { int clipId = -1; int masterTrack = -1; if (!m_selection.selectedItems.isEmpty()) { clipId = m_selection.selectedItems.first(); // Check grouped clips QList extraClips = m_selection.selectedItems; masterTrack = m_model->getTrackPosition(m_model->getItemTrackId(m_selection.selectedItems.first())); std::unordered_set groupRoots; for (int id : m_selection.selectedItems) { if (m_model->m_groups->isInGroup(id)) { int gid = m_model->m_groups->getRootId(id); qDebug() << " * ** ITEM " << id << " IS IN GROUP: " << gid; if (gid != m_model->m_temporarySelectionGroup) { qDebug() << " * ** TRYING TO INSERT GP: " << gid; if (groupRoots.find(gid) == groupRoots.end()) { groupRoots.insert(gid); } } else { qDebug() << " * ** TRYING TO INSERT SELECTION CHILD"; std::unordered_set selection = m_model->m_groups->getDirectChildren(gid); for (int j : selection) { if (groupRoots.find(j) == groupRoots.end()) { groupRoots.insert(j); } } } std::unordered_set selection = m_model->getGroupElements(id); for (int j : selection) { if (!extraClips.contains(j)) { extraClips << j; } } } } qDebug() << "==============\n GROUP ROOTS: "; for (int gp : groupRoots) { qDebug() << "GROUP: " << gp; } qDebug() << "\n======="; QDomDocument copiedItems; int offset = -1; QDomElement container = copiedItems.createElement(QStringLiteral("kdenlive-scene")); copiedItems.appendChild(container); QStringList binIds; for (int id : extraClips) { if (offset == -1 || m_model->getItemPosition(id) < offset) { offset = m_model->getItemPosition(id); } if (m_model->isClip(id)) { container.appendChild(m_model->m_allClips[id]->toXml(copiedItems)); const QString bid = m_model->m_allClips[id]->binId(); if (!binIds.contains(bid)) { binIds << bid; } } else if (m_model->isComposition(id)) { container.appendChild(m_model->m_allCompositions[id]->toXml(copiedItems)); } } QDomElement container2 = copiedItems.createElement(QStringLiteral("bin")); container.appendChild(container2); for (const QString &id : binIds) { std::shared_ptr clip = pCore->bin()->getBinClip(id); QDomDocument tmp; container2.appendChild(clip->toXml(tmp)); } container.setAttribute(QStringLiteral("offset"), offset); container.setAttribute(QStringLiteral("masterTrack"), masterTrack); container.setAttribute(QStringLiteral("documentid"), pCore->currentDoc()->getDocumentProperty(QStringLiteral("documentid"))); QDomElement grp = copiedItems.createElement(QStringLiteral("groups")); container.appendChild(grp); grp.appendChild(copiedItems.createTextNode(m_model->m_groups->toJson(groupRoots))); // TODO: groups qDebug() << " / // / PASTED DOC: \n\n" << copiedItems.toString() << "\n\n------------"; QClipboard *clipboard = QApplication::clipboard(); clipboard->setText(copiedItems.toString()); } else { return; } m_root->setProperty("copiedClip", clipId); } bool TimelineController::pasteItem() { QClipboard *clipboard = QApplication::clipboard(); QString txt = clipboard->text(); QDomDocument copiedItems; copiedItems.setContent(txt); if (copiedItems.documentElement().tagName() == QLatin1String("kdenlive-scene")) { qDebug()<<" / / READING CLIPS FROM CLIPBOARD"; } else { return false; } int tid = getMouseTrack(); int position = getMousePos(); if (tid == -1) { tid = m_activeTrack; } if (position == -1) { position = timelinePosition(); } std::function undo = []() { return true; }; std::function redo = []() { return true; }; const QString docId = copiedItems.documentElement().attribute(QStringLiteral("documentid")); QMap mappedIds; if (!docId.isEmpty() && docId != pCore->currentDoc()->getDocumentProperty(QStringLiteral("documentid"))) { // paste from another document, import bin clips QString folderId = pCore->projectItemModel()->getFolderIdByName(i18n("Pasted clips")); if (folderId.isEmpty()) { // Folder doe not exist const QString rootId = pCore->projectItemModel()->getRootFolder()->clipId(); folderId = QString::number(pCore->projectItemModel()->getFreeFolderId()); pCore->projectItemModel()->requestAddFolder(folderId, i18n("Pasted clips"), rootId, undo, redo); } QDomNodeList binClips = copiedItems.documentElement().elementsByTagName(QStringLiteral("producer")); for (int i = 0; i < binClips.count(); ++i) { QDomElement currentProd = binClips.item(i).toElement(); QString clipId = Xml::getXmlProperty(currentProd, QStringLiteral("kdenlive:id")); if (!pCore->projectItemModel()->isIdFree(clipId)) { QString updatedId = QString::number(pCore->projectItemModel()->getFreeClipId()); Xml::setXmlProperty(currentProd, QStringLiteral("kdenlive:id"), updatedId); mappedIds.insert(clipId, updatedId); clipId = updatedId; } pCore->projectItemModel()->requestAddBinClip(clipId, currentProd, folderId, undo, redo); } } QDomNodeList clips = copiedItems.documentElement().elementsByTagName(QStringLiteral("clip")); QDomNodeList compositions = copiedItems.documentElement().elementsByTagName(QStringLiteral("composition")); int offset = copiedItems.documentElement().attribute(QStringLiteral("offset")).toInt(); int masterTrack = m_model->getTrackIndexFromPosition(copiedItems.documentElement().attribute(QStringLiteral("masterTrack")).toInt()); int trackOffset = TimelineFunctions::getTrackOffset(m_model, masterTrack, tid); bool masterIsAudio = m_model->isAudioTrack(masterTrack); // find paste tracks QMap tracksMap; for (int i = 0; i < clips.count(); i++) { QDomElement prod = clips.at(i).toElement(); int trackId = m_model->getTrackIndexFromPosition(prod.attribute(QStringLiteral("track")).toInt()); if (tracksMap.contains(trackId)) { // Track already processed, skip continue; } if (trackOffset == 0) { tracksMap.insert(trackId, trackId); continue; } tracksMap.insert(trackId, TimelineFunctions::getOffsetTrackId(m_model, trackId, trackOffset, masterIsAudio)); } for (int i = 0; i < compositions.count(); i++) { QDomElement prod = compositions.at(i).toElement(); int trackId = m_model->getTrackIndexFromPosition(prod.attribute(QStringLiteral("track")).toInt()); if (!tracksMap.contains(trackId)) { tracksMap.insert(trackId, TimelineFunctions::getOffsetTrackId(m_model, trackId, trackOffset, masterIsAudio)); } int atrackId = prod.attribute(QStringLiteral("a_track")).toInt(); if (atrackId == 0) { continue; } atrackId = m_model->getTrackIndexFromPosition(atrackId); if (!tracksMap.contains(atrackId)) { tracksMap.insert(atrackId, TimelineFunctions::getOffsetTrackId(m_model, atrackId, trackOffset, masterIsAudio)); } } bool res = true; QLocale locale; QMap correspondingIds; QList waitingIds; for (int i = 0; i < clips.count(); i++) { waitingIds << i; } for (int i = 0; res && !waitingIds.isEmpty();) { if (i >= waitingIds.size()) { i = 0; } QDomElement prod = clips.at(waitingIds.at(i)).toElement(); QString originalId = prod.attribute(QStringLiteral("binid")); if (mappedIds.contains(originalId)) { // Map id originalId = mappedIds.value(originalId); } int in = prod.attribute(QStringLiteral("in")).toInt(); int out = prod.attribute(QStringLiteral("out")).toInt(); int trackId = m_model->getTrackIndexFromPosition(prod.attribute(QStringLiteral("track")).toInt()); int pos = prod.attribute(QStringLiteral("position")).toInt() - offset; double speed = locale.toDouble(prod.attribute(QStringLiteral("speed"))); int newId; bool created = m_model->requestClipCreation(originalId, newId, m_model->getTrackById_const(trackId)->trackType(), speed, undo, redo); if (created) { // Master producer is ready //ids.removeAll(originalId); waitingIds.removeAt(i); } else { i++; qApp->processEvents(); continue; } if (m_model->m_allClips[newId]->m_endlessResize) { out = out - in; in = 0; m_model->m_allClips[newId]->m_producer->set("length", out + 1); } m_model->m_allClips[newId]->setInOut(in, out); correspondingIds.insert(prod.attribute(QStringLiteral("id")).toInt(), newId); res = res & m_model->getTrackById(tracksMap.value(trackId))->requestClipInsertion(newId, position + pos, true, true, undo, redo); // paste effects if (res) { std::shared_ptr destStack = m_model->getClipEffectStackModel(newId); destStack->fromXml(prod.firstChildElement(QStringLiteral("effects")), undo, redo); } } // Compositions for (int i = 0; res && i < compositions.count(); i++) { QDomElement prod = compositions.at(i).toElement(); QString originalId = prod.attribute(QStringLiteral("composition")); int in = prod.attribute(QStringLiteral("in")).toInt(); int out = prod.attribute(QStringLiteral("out")).toInt(); int trackId = m_model->getTrackIndexFromPosition(prod.attribute(QStringLiteral("track")).toInt()); int aTrackId = prod.attribute(QStringLiteral("a_track")).toInt(); if (aTrackId > 0) { aTrackId = tracksMap.value(m_model->getTrackIndexFromPosition(aTrackId - 1)); } int pos = prod.attribute(QStringLiteral("position")).toInt() - offset; int newId; auto transProps = std::make_unique(); QDomNodeList props = prod.elementsByTagName(QStringLiteral("property")); for (int j = 0; j < props.count(); j++) { transProps->set(props.at(j).toElement().attribute(QStringLiteral("name")).toUtf8().constData(), props.at(j).toElement().text().toUtf8().constData()); } res = m_model->requestCompositionInsertion(originalId, tracksMap.value(trackId), aTrackId, position + pos, out - in, std::move(transProps), newId, undo, redo); } if (!res) { undo(); return false; } const QString groupsData = copiedItems.documentElement().firstChildElement(QStringLiteral("groups")).text(); qDebug() << "************** GRP DATA ********\n" << groupsData << "\n******"; m_model->m_groups->fromJsonWithOffset(groupsData, tracksMap, position - offset, undo, redo); pCore->pushUndo(undo, redo, i18n("Paste clips")); return true; } void TimelineController::triggerAction(const QString &name) { pCore->triggerAction(name); } QString TimelineController::timecode(int frames) { return KdenliveSettings::frametimecode() ? QString::number(frames) : m_model->tractor()->frames_to_time(frames, mlt_time_smpte_df); } bool TimelineController::showThumbnails() const { return KdenliveSettings::videothumbnails(); } bool TimelineController::showAudioThumbnails() const { return KdenliveSettings::audiothumbnails(); } bool TimelineController::showMarkers() const { return KdenliveSettings::showmarkers(); } bool TimelineController::audioThumbFormat() const { return KdenliveSettings::displayallchannels(); } bool TimelineController::showWaveforms() const { return KdenliveSettings::audiothumbnails(); } void TimelineController::addTrack(int tid) { if (tid == -1) { tid = m_activeTrack; } QPointer d = new TrackDialog(m_model, tid, qApp->activeWindow()); if (d->exec() == QDialog::Accepted) { int newTid; m_model->requestTrackInsertion(d->selectedTrackPosition(), newTid, d->trackName(), d->addAudioTrack()); m_model->buildTrackCompositing(true); m_model->_resetView(); } } void TimelineController::deleteTrack(int tid) { if (tid == -1) { tid = m_activeTrack; } QPointer d = new TrackDialog(m_model, tid, qApp->activeWindow(), true); if (d->exec() == QDialog::Accepted) { int selectedTrackIx = d->selectedTrackId(); m_model->requestTrackDeletion(selectedTrackIx); m_model->buildTrackCompositing(true); if (m_activeTrack == selectedTrackIx) { setActiveTrack(m_model->getTrackIndexFromPosition(m_model->getTracksCount() - 1)); } } } void TimelineController::gotoNextSnap() { setPosition(m_model->requestNextSnapPos(timelinePosition())); } void TimelineController::gotoPreviousSnap() { setPosition(m_model->requestPreviousSnapPos(timelinePosition())); } void TimelineController::groupSelection() { if (m_selection.selectedItems.size() < 2) { pCore->displayMessage(i18n("Select at least 2 items to group"), InformationMessage, 500); return; } std::unordered_set clips; for (int id : m_selection.selectedItems) { clips.insert(id); } m_model->requestClipsGroup(clips); emit selectionChanged(); } void TimelineController::unGroupSelection(int cid) { if (cid == -1 && m_selection.selectedItems.isEmpty()) { pCore->displayMessage(i18n("Select at least 1 item to ungroup"), InformationMessage, 500); return; } if (cid == -1) { if (m_model->m_temporarySelectionGroup >= 0) { cid = m_model->m_temporarySelectionGroup; } else { for (int id : m_selection.selectedItems) { if (m_model->m_groups->getRootId(id)) { cid = id; break; } } } } int tmpGroup = m_model->m_temporarySelectionGroup; if (tmpGroup >= 0) { m_model->requestClipUngroup(m_model->m_temporarySelectionGroup, false); } if (cid > -1) { if (cid != tmpGroup) { cid = m_model->m_groups->getDirectAncestor(cid); } else { cid = -1; for (int id : m_selection.selectedItems) { if (m_model->m_groups->getRootId(id)) { cid = id; break; } } } if (cid > -1) { m_model->requestClipUngroup(cid); } } m_selection.selectedItems.clear(); emit selectionChanged(); } void TimelineController::setInPoint() { int cursorPos = timelinePosition(); if (!m_selection.selectedItems.isEmpty()) { for (int id : m_selection.selectedItems) { int start = m_model->getItemPosition(id); if (start == cursorPos) { continue; } int size = start + m_model->getItemPlaytime(id) - cursorPos; m_model->requestItemResize(id, size, false, true, 0, false); } } } int TimelineController::timelinePosition() const { return m_seekPosition >= 0 ? m_seekPosition : m_position; } void TimelineController::setOutPoint() { int cursorPos = timelinePosition(); if (!m_selection.selectedItems.isEmpty()) { for (int id : m_selection.selectedItems) { int start = m_model->getItemPosition(id); if (start + m_model->getItemPlaytime(id) == cursorPos) { continue; } int size = cursorPos - start; m_model->requestItemResize(id, size, true, true, 0, false); } } } void TimelineController::editMarker(const QString &cid, int frame) { std::shared_ptr clip = pCore->bin()->getBinClip(cid); GenTime pos(frame, pCore->getCurrentFps()); clip->getMarkerModel()->editMarkerGui(pos, qApp->activeWindow(), false, clip.get()); } void TimelineController::editGuide(int frame) { if (frame == -1) { frame = timelinePosition(); } auto guideModel = pCore->projectManager()->current()->getGuideModel(); GenTime pos(frame, pCore->getCurrentFps()); guideModel->editMarkerGui(pos, qApp->activeWindow(), false); } void TimelineController::moveGuide(int frame, int newFrame) { auto guideModel = pCore->projectManager()->current()->getGuideModel(); GenTime pos(frame, pCore->getCurrentFps()); GenTime newPos(newFrame, pCore->getCurrentFps()); guideModel->editMarker(pos, newPos); } void TimelineController::switchGuide(int frame, bool deleteOnly) { bool markerFound = false; if (frame == -1) { frame = timelinePosition(); } CommentedTime marker = pCore->projectManager()->current()->getGuideModel()->getMarker(GenTime(frame, pCore->getCurrentFps()), &markerFound); if (!markerFound) { if (deleteOnly) { pCore->displayMessage(i18n("No guide found at current position"), InformationMessage, 500); return; } GenTime pos(frame, pCore->getCurrentFps()); pCore->projectManager()->current()->getGuideModel()->addMarker(pos, i18n("guide")); } else { pCore->projectManager()->current()->getGuideModel()->removeMarker(marker.time()); } } -void TimelineController::addAsset(const QVariantMap data) +void TimelineController::addAsset(const QVariantMap &data) { QString effect = data.value(QStringLiteral("kdenlive/effect")).toString(); if (!m_selection.selectedItems.isEmpty()) { QList effectSelection; for (int id : m_selection.selectedItems) { if (m_model->isClip(id)) { effectSelection << id; int partner = m_model->getClipSplitPartner(id); if (partner > -1 && !effectSelection.contains(partner)) { effectSelection << partner; } } } bool foundMatch = false; for (int id : effectSelection) { if (m_model->addClipEffect(id, effect, false)) { foundMatch = true; } } if (!foundMatch) { QString effectName = EffectsRepository::get()->getName(effect); pCore->displayMessage(i18n("Cannot add effect %1 to selected clip", effectName), InformationMessage, 500); } } else { pCore->displayMessage(i18n("Select a clip to apply an effect"), InformationMessage, 500); } } void TimelineController::requestRefresh() { pCore->requestMonitorRefresh(); } void TimelineController::showAsset(int id) { if (m_model->isComposition(id)) { emit showTransitionModel(id, m_model->getCompositionParameterModel(id)); } else if (m_model->isClip(id)) { QModelIndex clipIx = m_model->makeClipIndexFromID(id); QString clipName = m_model->data(clipIx, Qt::DisplayRole).toString(); bool showKeyframes = m_model->data(clipIx, TimelineModel::ShowKeyframesRole).toInt(); qDebug() << "-----\n// SHOW KEYFRAMES: " << showKeyframes; emit showItemEffectStack(clipName, m_model->getClipEffectStackModel(id), m_model->getClipFrameSize(id), showKeyframes); } } void TimelineController::showTrackAsset(int trackId) { emit showItemEffectStack(getTrackNameFromIndex(trackId), m_model->getTrackEffectStackModel(trackId), pCore->getCurrentFrameSize(), false); } void TimelineController::setPosition(int position) { setSeekPosition(position); emit seeked(position); } void TimelineController::setAudioTarget(int track) { m_model->m_audioTarget = track; emit audioTargetChanged(); } void TimelineController::setVideoTarget(int track) { m_model->m_videoTarget = track; emit videoTargetChanged(); } void TimelineController::setActiveTrack(int track) { m_activeTrack = track; emit activeTrackChanged(); } void TimelineController::setSeekPosition(int position) { m_seekPosition = position; emit seekPositionChanged(); } void TimelineController::onSeeked(int position) { m_position = position; emit positionChanged(); if (m_seekPosition > -1 && position == m_seekPosition) { m_seekPosition = -1; emit seekPositionChanged(); } } void TimelineController::setZone(const QPoint &zone) { if (m_zone.x() > 0) { m_model->removeSnap(m_zone.x()); } if (m_zone.y() > 0) { m_model->removeSnap(m_zone.y() - 1); } if (zone.x() > 0) { m_model->addSnap(zone.x()); } if (zone.y() > 0) { m_model->addSnap(zone.y() - 1); } m_zone = zone; emit zoneChanged(); } void TimelineController::setZoneIn(int inPoint) { if (m_zone.x() > 0) { m_model->removeSnap(m_zone.x()); } if (inPoint > 0) { m_model->addSnap(inPoint); } m_zone.setX(inPoint); emit zoneMoved(m_zone); } void TimelineController::setZoneOut(int outPoint) { if (m_zone.y() > 0) { m_model->removeSnap(m_zone.y() - 1); } if (outPoint > 0) { m_model->addSnap(outPoint - 1); } m_zone.setY(outPoint); emit zoneMoved(m_zone); } -void TimelineController::selectItems(QVariantList arg, int startFrame, int endFrame, bool addToSelect) +void TimelineController::selectItems(const QVariantList &arg, int startFrame, int endFrame, bool addToSelect) { std::unordered_set previousSelection = getCurrentSelectionIds(); std::unordered_set itemsToSelect; if (addToSelect) { for (int cid : m_selection.selectedItems) { itemsToSelect.insert(cid); } } m_selection.selectedItems.clear(); for (int i = 0; i < arg.count(); i++) { auto currentClips = m_model->getItemsInRange(arg.at(i).toInt(), startFrame, endFrame, true); itemsToSelect.insert(currentClips.begin(), currentClips.end()); } if (itemsToSelect.size() > 0) { for (int x : itemsToSelect) { m_selection.selectedItems << x; } qDebug() << "// GROUPING ITEMS: " << m_selection.selectedItems; m_model->m_temporarySelectionGroup = m_model->requestClipsGroup(itemsToSelect, true, GroupType::Selection); qDebug() << "// GROUPING ITEMS DONE"; } else if (m_model->m_temporarySelectionGroup > -1) { m_model->requestClipUngroup(m_model->m_temporarySelectionGroup, false); } std::unordered_set newIds; if (m_model->m_temporarySelectionGroup >= 0) { newIds = m_model->getGroupElements(m_selection.selectedItems.constFirst()); for (int child : newIds) { QModelIndex ix; if (m_model->isClip(child)) { ix = m_model->makeClipIndexFromID(child); } else if (m_model->isComposition(child)) { ix = m_model->makeCompositionIndexFromID(child); } if (ix.isValid()) { m_model->dataChanged(ix, ix, {TimelineModel::GroupedRole}); } } } emit selectionChanged(); } void TimelineController::requestClipCut(int clipId, int position) { if (position == -1) { position = timelinePosition(); } TimelineFunctions::requestClipCut(m_model, clipId, position); } void TimelineController::cutClipUnderCursor(int position, int track) { if (position == -1) { position = timelinePosition(); } QMutexLocker lk(&m_metaMutex); bool foundClip = false; for (int cid : m_selection.selectedItems) { if (m_model->isClip(cid)) { if (TimelineFunctions::requestClipCut(m_model, cid, position)) { foundClip = true; // Cutting clips in the selection group is handled in TimelineFunctions break; } } else { qDebug() << "//// TODO: COMPOSITION CUT!!!"; } } if (!foundClip) { if (track == -1) { track = m_activeTrack; } if (track >= 0) { int cid = m_model->getClipByPosition(track, position); if (cid >= 0 && TimelineFunctions::requestClipCut(m_model, cid, position)) { foundClip = true; } } } if (!foundClip) { pCore->displayMessage(i18n("No clip to cut"), InformationMessage, 500); } } int TimelineController::requestSpacerStartOperation(int trackId, int position) { return TimelineFunctions::requestSpacerStartOperation(m_model, trackId, position); } bool TimelineController::requestSpacerEndOperation(int clipId, int startPosition, int endPosition) { return TimelineFunctions::requestSpacerEndOperation(m_model, clipId, startPosition, endPosition); } void TimelineController::seekCurrentClip(bool seekToEnd) { for (int cid : m_selection.selectedItems) { int start = m_model->getItemPosition(cid); if (seekToEnd) { start += m_model->getItemPlaytime(cid); } setPosition(start); break; } } void TimelineController::seekToClip(int cid, bool seekToEnd) { int start = m_model->getItemPosition(cid); if (seekToEnd) { start += m_model->getItemPlaytime(cid); } setPosition(start); } void TimelineController::seekToMouse() { QVariant returnedValue; QMetaObject::invokeMethod(m_root, "getMousePos", Q_RETURN_ARG(QVariant, returnedValue)); int mousePos = returnedValue.toInt(); setPosition(mousePos); } int TimelineController::getMousePos() { QVariant returnedValue; QMetaObject::invokeMethod(m_root, "getMousePos", Q_RETURN_ARG(QVariant, returnedValue)); return returnedValue.toInt(); } int TimelineController::getMouseTrack() { QVariant returnedValue; QMetaObject::invokeMethod(m_root, "getMouseTrack", Q_RETURN_ARG(QVariant, returnedValue)); return returnedValue.toInt(); } void TimelineController::refreshItem(int id) { int in = m_model->getItemPosition(id); if (in > m_position || (m_model->isClip(id) && m_model->m_allClips[id]->isAudioOnly())) { return; } if (m_position <= in + m_model->getItemPlaytime(id)) { pCore->requestMonitorRefresh(); } } QPoint TimelineController::getTracksCount() const { QVariant returnedValue; QMetaObject::invokeMethod(m_root, "getTracksCount", Q_RETURN_ARG(QVariant, returnedValue)); QVariantList tracks = returnedValue.toList(); QPoint p(tracks.at(0).toInt(), tracks.at(1).toInt()); return p; } QStringList TimelineController::extractCompositionLumas() const { return m_model->extractCompositionLumas(); } void TimelineController::addEffectToCurrentClip(const QStringList &effectData) { QList activeClips; for (int track = m_model->getTracksCount() - 1; track >= 0; track--) { int trackIx = m_model->getTrackIndexFromPosition(track); int cid = m_model->getClipByPosition(trackIx, timelinePosition()); if (cid > -1) { activeClips << cid; } } if (!activeClips.isEmpty()) { if (effectData.count() == 4) { QString effectString = effectData.at(1) + QStringLiteral("-") + effectData.at(2) + QStringLiteral("-") + effectData.at(3); m_model->copyClipEffect(activeClips.first(), effectString); } else { m_model->addClipEffect(activeClips.first(), effectData.constFirst()); } } } void TimelineController::adjustFade(int cid, const QString &effectId, int duration, int initialDuration) { if (duration <= 0) { // remove fade m_model->removeFade(cid, effectId == QLatin1String("fadein")); } else { m_model->adjustEffectLength(cid, effectId, duration, initialDuration); } } QPair TimelineController::getCompositionATrack(int cid) const { QPair result; std::shared_ptr compo = m_model->getCompositionPtr(cid); if (compo) { result = QPair(compo->getATrack(), m_model->getTrackMltIndex(compo->getCurrentTrackId())); } return result; } void TimelineController::setCompositionATrack(int cid, int aTrack) { TimelineFunctions::setCompositionATrack(m_model, cid, aTrack); } bool TimelineController::compositionAutoTrack(int cid) const { std::shared_ptr compo = m_model->getCompositionPtr(cid); return compo && compo->getForcedTrack() == -1; } const QString TimelineController::getClipBinId(int clipId) const { return m_model->getClipBinId(clipId); } void TimelineController::focusItem(int itemId) { int start = m_model->getItemPosition(itemId); setPosition(start); } int TimelineController::headerWidth() const { return qMax(10, KdenliveSettings::headerwidth()); } void TimelineController::setHeaderWidth(int width) { KdenliveSettings::setHeaderwidth(width); } bool TimelineController::createSplitOverlay(Mlt::Filter *filter) { if (m_timelinePreview && m_timelinePreview->hasOverlayTrack()) { return true; } int clipId = getCurrentItem(); if (clipId == -1) { pCore->displayMessage(i18n("Select a clip to compare effect"), InformationMessage, 500); return false; } std::shared_ptr clip = m_model->getClipPtr(clipId); const QString binId = clip->binId(); // Get clean bin copy of the clip std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(binId); std::shared_ptr binProd(binClip->masterProducer()->cut(clip->getIn(), clip->getOut())); // Get copy of timeline producer std::shared_ptr clipProducer(new Mlt::Producer(*clip)); // Built tractor and compositing Mlt::Tractor trac(*m_model->m_tractor->profile()); Mlt::Playlist play(*m_model->m_tractor->profile()); Mlt::Playlist play2(*m_model->m_tractor->profile()); play.append(*clipProducer.get()); play2.append(*binProd); trac.set_track(play, 0); trac.set_track(play2, 1); play2.attach(*filter); QString splitTransition = TransitionsRepository::get()->getCompositingTransition(); Mlt::Transition t(*m_model->m_tractor->profile(), splitTransition.toUtf8().constData()); t.set("always_active", 1); trac.plant_transition(t, 0, 1); int startPos = m_model->getClipPosition(clipId); // plug in overlay playlist Mlt::Playlist *overlay = new Mlt::Playlist(*m_model->m_tractor->profile()); overlay->insert_blank(0, startPos); Mlt::Producer split(trac.get_producer()); overlay->insert_at(startPos, &split, 1); // insert in tractor if (!m_timelinePreview) { initializePreview(); } m_timelinePreview->setOverlayTrack(overlay); m_model->m_overlayTrackCount = m_timelinePreview->addedTracks(); return true; } void TimelineController::removeSplitOverlay() { if (m_timelinePreview && !m_timelinePreview->hasOverlayTrack()) { return; } // disconnect m_timelinePreview->removeOverlayTrack(); m_model->m_overlayTrackCount = m_timelinePreview->addedTracks(); } void TimelineController::addPreviewRange(bool add) { if (m_zone.isNull()) { return; } if (!m_timelinePreview) { initializePreview(); } if (m_timelinePreview) { m_timelinePreview->addPreviewRange(m_zone, add); } } void TimelineController::clearPreviewRange() { if (m_timelinePreview) { m_timelinePreview->clearPreviewRange(); } } void TimelineController::startPreviewRender() { // Timeline preview stuff if (!m_timelinePreview) { initializePreview(); } else if (m_disablePreview->isChecked()) { m_disablePreview->setChecked(false); disablePreview(false); } if (m_timelinePreview) { if (!m_usePreview) { m_timelinePreview->buildPreviewTrack(); m_usePreview = true; m_model->m_overlayTrackCount = m_timelinePreview->addedTracks(); } m_timelinePreview->startPreviewRender(); } } void TimelineController::stopPreviewRender() { if (m_timelinePreview) { m_timelinePreview->abortRendering(); } } void TimelineController::initializePreview() { if (m_timelinePreview) { // Update parameters if (!m_timelinePreview->loadParams()) { if (m_usePreview) { // Disconnect preview track m_timelinePreview->disconnectTrack(); m_usePreview = false; } delete m_timelinePreview; m_timelinePreview = nullptr; } } else { m_timelinePreview = new PreviewManager(this, m_model->m_tractor.get()); if (!m_timelinePreview->initialize()) { // TODO warn user delete m_timelinePreview; m_timelinePreview = nullptr; } else { } } QAction *previewRender = pCore->currentDoc()->getAction(QStringLiteral("prerender_timeline_zone")); if (previewRender) { previewRender->setEnabled(m_timelinePreview != nullptr); } m_disablePreview->setEnabled(m_timelinePreview != nullptr); m_disablePreview->blockSignals(true); m_disablePreview->setChecked(false); m_disablePreview->blockSignals(false); } void TimelineController::disablePreview(bool disable) { if (disable) { m_timelinePreview->deletePreviewTrack(); m_usePreview = false; m_model->m_overlayTrackCount = m_timelinePreview->addedTracks(); } else { if (!m_usePreview) { if (!m_timelinePreview->buildPreviewTrack()) { // preview track already exists, reconnect m_model->m_tractor->lock(); m_timelinePreview->reconnectTrack(); m_model->m_tractor->unlock(); } m_timelinePreview->loadChunks(QVariantList(), QVariantList(), QDateTime()); m_usePreview = true; } } m_model->m_overlayTrackCount = m_timelinePreview->addedTracks(); } QVariantList TimelineController::dirtyChunks() const { return m_timelinePreview ? m_timelinePreview->m_dirtyChunks : QVariantList(); } QVariantList TimelineController::renderedChunks() const { return m_timelinePreview ? m_timelinePreview->m_renderedChunks : QVariantList(); } int TimelineController::workingPreview() const { return m_timelinePreview ? m_timelinePreview->workingPreview : -1; } bool TimelineController::useRuler() const { return pCore->currentDoc()->getDocumentProperty(QStringLiteral("enableTimelineZone")).toInt() == 1; } void TimelineController::resetPreview() { if (m_timelinePreview) { m_timelinePreview->clearPreviewRange(); initializePreview(); } } -void TimelineController::loadPreview(QString chunks, QString dirty, const QDateTime &documentDate, int enable) +void TimelineController::loadPreview(const QString &chunks, const QString &dirty, const QDateTime &documentDate, int enable) { if (chunks.isEmpty() && dirty.isEmpty()) { return; } if (!m_timelinePreview) { initializePreview(); } QVariantList renderedChunks; QVariantList dirtyChunks; QStringList chunksList = chunks.split(QLatin1Char(','), QString::SkipEmptyParts); QStringList dirtyList = dirty.split(QLatin1Char(','), QString::SkipEmptyParts); for (const QString &frame : chunksList) { renderedChunks << frame.toInt(); } for (const QString &frame : dirtyList) { dirtyChunks << frame.toInt(); } m_disablePreview->blockSignals(true); m_disablePreview->setChecked(enable); m_disablePreview->blockSignals(false); if (!enable) { m_timelinePreview->buildPreviewTrack(); m_usePreview = true; m_model->m_overlayTrackCount = m_timelinePreview->addedTracks(); } m_timelinePreview->loadChunks(renderedChunks, dirtyChunks, documentDate); } QMap TimelineController::documentProperties() { QMap props = pCore->currentDoc()->documentProperties(); int audioTarget = m_model->m_audioTarget == -1 ? -1 : m_model->getTrackPosition(m_model->m_audioTarget); int videoTarget = m_model->m_videoTarget == -1 ? -1 : m_model->getTrackPosition(m_model->m_videoTarget); int activeTrack = m_activeTrack == -1 ? -1 : m_model->getTrackPosition(m_activeTrack); props.insert(QStringLiteral("audioTarget"), QString::number(audioTarget)); props.insert(QStringLiteral("videoTarget"), QString::number(videoTarget)); props.insert(QStringLiteral("activeTrack"), QString::number(activeTrack)); props.insert(QStringLiteral("position"), QString::number(timelinePosition())); QVariant returnedValue; QMetaObject::invokeMethod(m_root, "getScrollPos", Q_RETURN_ARG(QVariant, returnedValue)); int scrollPos = returnedValue.toInt(); props.insert(QStringLiteral("scrollPos"), QString::number(scrollPos)); props.insert(QStringLiteral("zonein"), QString::number(m_zone.x())); props.insert(QStringLiteral("zoneout"), QString::number(m_zone.y())); if (m_timelinePreview) { QPair chunks = m_timelinePreview->previewChunks(); props.insert(QStringLiteral("previewchunks"), chunks.first.join(QLatin1Char(','))); props.insert(QStringLiteral("dirtypreviewchunks"), chunks.second.join(QLatin1Char(','))); } props.insert(QStringLiteral("disablepreview"), QString::number((int)m_disablePreview->isChecked())); return props; } void TimelineController::insertSpace(int trackId, int frame) { if (frame == -1) { frame = timelinePosition(); } if (trackId == -1) { trackId = m_activeTrack; } QPointer d = new SpacerDialog(GenTime(65, pCore->getCurrentFps()), pCore->currentDoc()->timecode(), qApp->activeWindow()); if (d->exec() != QDialog::Accepted) { delete d; return; } int cid = requestSpacerStartOperation(d->affectAllTracks() ? -1 : trackId, frame); int spaceDuration = d->selectedDuration().frames(pCore->getCurrentFps()); delete d; if (cid == -1) { pCore->displayMessage(i18n("No clips found to insert space"), InformationMessage, 500); return; } int start = m_model->getItemPosition(cid); requestSpacerEndOperation(cid, start, start + spaceDuration); } void TimelineController::removeSpace(int trackId, int frame, bool affectAllTracks) { if (frame == -1) { frame = timelinePosition(); } if (trackId == -1) { trackId = m_activeTrack; } // find blank duration int spaceDuration = m_model->getTrackById_const(trackId)->getBlankSizeAtPos(frame); int cid = requestSpacerStartOperation(affectAllTracks ? -1 : trackId, frame); if (cid == -1) { pCore->displayMessage(i18n("No clips found to insert space"), InformationMessage, 500); return; } int start = m_model->getItemPosition(cid); requestSpacerEndOperation(cid, start, start - spaceDuration); } void TimelineController::invalidateItem(int cid) { if (!m_timelinePreview || m_model->getItemTrackId(cid) == -1) { return; } int start = m_model->getItemPosition(cid); int end = start + m_model->getItemPlaytime(cid); m_timelinePreview->invalidatePreview(start, end); } void TimelineController::invalidateZone(int in, int out) { if (!m_timelinePreview) { return; } m_timelinePreview->invalidatePreview(in, out); } void TimelineController::changeItemSpeed(int clipId, double speed) { if (qFuzzyCompare(speed, -1)) { speed = 100 * m_model->getClipSpeed(clipId); bool ok = false; double duration = m_model->getItemPlaytime(clipId); // this is the max speed so that the clip is at least one frame long double maxSpeed = 100. * duration * qAbs(m_model->getClipSpeed(clipId)); // this is the min speed so that the clip doesn't bump into the next one on track double minSpeed = 100. * duration * qAbs(m_model->getClipSpeed(clipId)) / (duration + double(m_model->getBlankSizeNearClip(clipId, true)) - 1); // if there is a split partner, we must also take it into account int partner = m_model->getClipSplitPartner(clipId); if (partner != -1) { double duration2 = m_model->getItemPlaytime(partner); double maxSpeed2 = 100. * duration2 * qAbs(m_model->getClipSpeed(partner)); double minSpeed2 = 100. * duration2 * qAbs(m_model->getClipSpeed(partner)) / (duration2 + double(m_model->getBlankSizeNearClip(partner, true)) - 1); minSpeed = std::max(minSpeed, minSpeed2); maxSpeed = std::min(maxSpeed, maxSpeed2); } speed = QInputDialog::getDouble(QApplication::activeWindow(), i18n("Clip Speed"), i18n("Percentage"), speed, minSpeed, maxSpeed, 2, &ok); if (!ok) { return; } } m_model->requestClipTimeWarp(clipId, speed); } void TimelineController::switchCompositing(int mode) { // m_model->m_tractor->lock(); QScopedPointer service(m_model->m_tractor->field()); Mlt::Field *field = m_model->m_tractor->field(); field->lock(); while ((service != nullptr) && service->is_valid()) { if (service->type() == transition_type) { Mlt::Transition t((mlt_transition)service->get_service()); QString serviceName = t.get("mlt_service"); if (t.get_int("internal_added") == 237 && serviceName != QLatin1String("mix")) { // remove all compositing transitions field->disconnect_service(t); } } service.reset(service->producer()); } if (mode > 0) { const QString compositeGeometry = QStringLiteral("0=0/0:%1x%2").arg(m_model->m_tractor->profile()->width()).arg(m_model->m_tractor->profile()->height()); // Loop through tracks for (int track = 1; track < m_model->getTracksCount(); track++) { if (m_model->getTrackById(m_model->getTrackIndexFromPosition(track))->getProperty("kdenlive:audio_track").toInt() == 0) { // This is a video track Mlt::Transition t(*m_model->m_tractor->profile(), mode == 1 ? "composite" : TransitionsRepository::get()->getCompositingTransition().toUtf8().constData()); t.set("always_active", 1); t.set("a_track", 0); t.set("b_track", track + 1); if (mode == 1) { t.set("valign", "middle"); t.set("halign", "centre"); t.set("fill", 1); t.set("geometry", compositeGeometry.toUtf8().constData()); } t.set("internal_added", 237); field->plant_transition(t, 0, track + 1); } } } field->unlock(); delete field; pCore->requestMonitorRefresh(); } void TimelineController::extractZone(QPoint zone, bool liftOnly) { QVector tracks; if (audioTarget() >= 0) { tracks << audioTarget(); } if (videoTarget() >= 0) { tracks << videoTarget(); } if (tracks.isEmpty()) { tracks << m_activeTrack; } if (m_zone == QPoint()) { // Use current timeline position and clip zone length zone.setY(timelinePosition() + zone.y() - zone.x()); zone.setX(timelinePosition()); } TimelineFunctions::extractZone(m_model, tracks, m_zone == QPoint() ? zone : m_zone, liftOnly); } void TimelineController::extract(int clipId) { // TODO: grouped clips? int in = m_model->getClipPosition(clipId); QPoint zone(in, in + m_model->getClipPlaytime(clipId)); int track = m_model->getClipTrackId(clipId); TimelineFunctions::extractZone(m_model, QVector() << track, zone, false); } int TimelineController::insertZone(const QString &binId, QPoint zone, bool overwrite) { std::shared_ptr clip = pCore->bin()->getBinClip(binId); int aTrack = -1; int vTrack = -1; if (clip->hasAudio()) { aTrack = audioTarget(); } if (clip->hasVideo()) { vTrack = videoTarget(); } if (aTrack == -1 && vTrack == -1) { // No target tracks defined, use active track if (m_model->getTrackById_const(m_activeTrack)->isAudioTrack()) { aTrack = m_activeTrack; vTrack = m_model->getMirrorVideoTrackId(aTrack); } else { vTrack = m_activeTrack; aTrack = m_model->getMirrorAudioTrackId(vTrack); } } int insertPoint; QPoint sourceZone; if (useRuler() && m_zone != QPoint()) { // We want to use timeline zone for in/out insert points insertPoint = m_zone.x(); sourceZone = QPoint(zone.x(), zone.x() + m_zone.y() - m_zone.x()); } else { // Use current timeline pos and clip zone for in/out insertPoint = timelinePosition(); sourceZone = zone; } QList target_tracks; if (vTrack > -1) { target_tracks << vTrack; } if (aTrack > -1) { target_tracks << aTrack; } return TimelineFunctions::insertZone(m_model, target_tracks, binId, insertPoint, sourceZone, overwrite) ? insertPoint + (sourceZone.y() - sourceZone.x()) : -1; } -void TimelineController::updateClip(int clipId, QVector roles) +void TimelineController::updateClip(int clipId, const QVector &roles) { QModelIndex ix = m_model->makeClipIndexFromID(clipId); if (ix.isValid()) { m_model->dataChanged(ix, ix, roles); } } void TimelineController::showClipKeyframes(int clipId, bool value) { TimelineFunctions::showClipKeyframes(m_model, clipId, value); } void TimelineController::showCompositionKeyframes(int clipId, bool value) { TimelineFunctions::showCompositionKeyframes(m_model, clipId, value); } void TimelineController::switchEnableState(int clipId) { TimelineFunctions::switchEnableState(m_model, clipId); } void TimelineController::addCompositionToClip(const QString &assetId, int clipId, int offset) { int track = m_model->getClipTrackId(clipId); insertNewComposition(track, clipId, offset, assetId, true); } void TimelineController::addEffectToClip(const QString &assetId, int clipId) { m_model->addClipEffect(clipId, assetId); } bool TimelineController::splitAV() { int cid = m_selection.selectedItems.first(); if (m_model->isClip(cid)) { std::shared_ptr clip = m_model->getClipPtr(cid); if (clip->clipState() == PlaylistState::AudioOnly) { return TimelineFunctions::requestSplitVideo(m_model, cid, videoTarget()); } else { return TimelineFunctions::requestSplitAudio(m_model, cid, audioTarget()); } } pCore->displayMessage(i18n("No clip found to perform AV split operation"), InformationMessage, 500); return false; } void TimelineController::splitAudio(int clipId) { TimelineFunctions::requestSplitAudio(m_model, clipId, audioTarget()); } void TimelineController::splitVideo(int clipId) { TimelineFunctions::requestSplitVideo(m_model, clipId, videoTarget()); } void TimelineController::setAudioRef(int clipId) { m_audioRef = clipId; std::unique_ptr envelope(new AudioEnvelope(getClipBinId(clipId), clipId)); m_audioCorrelator.reset(new AudioCorrelation(std::move(envelope))); connect(m_audioCorrelator.get(), &AudioCorrelation::gotAudioAlignData, [&](int cid, int shift) { int pos = m_model->getClipPosition(m_audioRef) + shift + m_model->getClipIn(m_audioRef); bool result = m_model->requestClipMove(cid, m_model->getClipTrackId(cid), pos, true, true); if (!result) { pCore->displayMessage(i18n("Cannot move clip to frame %1.", (pos + shift)), InformationMessage, 500); } }); connect(m_audioCorrelator.get(), &AudioCorrelation::displayMessage, pCore.get(), &Core::displayMessage); } void TimelineController::alignAudio(int clipId) { // find other clip if (m_audioRef == -1 || m_audioRef == clipId) { pCore->displayMessage(i18n("Set audio reference before attempting to align"), InformationMessage, 500); return; } const QString masterBinClipId = getClipBinId(m_audioRef); if (m_model->m_groups->isInGroup(clipId)) { std::unordered_set groupIds = m_model->getGroupElements(clipId); // Check that no item is grouped with our audioRef item // TODO clearSelection(); } const QString otherBinId = getClipBinId(clipId); if (otherBinId == masterBinClipId) { // easy, same clip. int newPos = m_model->getClipPosition(m_audioRef) - m_model->getClipIn(m_audioRef) + m_model->getClipIn(clipId); if (newPos) { bool result = m_model->requestClipMove(clipId, m_model->getClipTrackId(clipId), newPos, true, true); if (!result) { pCore->displayMessage(i18n("Cannot move clip to frame %1.", newPos), InformationMessage, 500); } return; } } // Perform audio calculation AudioEnvelope *envelope = new AudioEnvelope(getClipBinId(clipId), clipId, (size_t)m_model->getClipIn(clipId), (size_t)m_model->getClipPlaytime(clipId), (size_t)m_model->getClipPosition(clipId)); m_audioCorrelator->addChild(envelope); } void TimelineController::switchTrackLock(bool applyToAll) { if (!applyToAll) { // apply to active track only bool locked = m_model->getTrackById_const(m_activeTrack)->getProperty("kdenlive:locked_track").toInt() == 1; m_model->setTrackProperty(m_activeTrack, QStringLiteral("kdenlive:locked_track"), locked ? QStringLiteral("0") : QStringLiteral("1")); } else { // Invert track lock // Get track states first QMap trackLockState; int unlockedTracksCount = 0; int tracksCount = m_model->getTracksCount(); for (int track = tracksCount - 1; track >= 0; track--) { int trackIx = m_model->getTrackIndexFromPosition(track); bool isLocked = m_model->getTrackById_const(trackIx)->getProperty("kdenlive:locked_track").toInt() == 1; if (!isLocked) { unlockedTracksCount++; } trackLockState.insert(trackIx, isLocked); } if (unlockedTracksCount == tracksCount) { // do not lock all tracks, leave active track unlocked trackLockState.insert(m_activeTrack, true); } QMapIterator i(trackLockState); while (i.hasNext()) { i.next(); m_model->setTrackProperty(i.key(), QStringLiteral("kdenlive:locked_track"), i.value() ? QStringLiteral("0") : QStringLiteral("1")); } } } void TimelineController::switchTargetTrack() { bool isAudio = m_model->getTrackById_const(m_activeTrack)->getProperty("kdenlive:audio_track").toInt() == 1; if (isAudio) { setAudioTarget(audioTarget() == m_activeTrack ? -1 : m_activeTrack); } else { setVideoTarget(videoTarget() == m_activeTrack ? -1 : m_activeTrack); } } int TimelineController::audioTarget() const { return m_model->m_audioTarget; } int TimelineController::videoTarget() const { return m_model->m_videoTarget; } void TimelineController::resetTrackHeight() { int tracksCount = m_model->getTracksCount(); for (int track = tracksCount - 1; track >= 0; track--) { int trackIx = m_model->getTrackIndexFromPosition(track); m_model->getTrackById(trackIx)->setProperty(QStringLiteral("kdenlive:trackheight"), QString::number(KdenliveSettings::trackheight())); } QModelIndex modelStart = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(0)); QModelIndex modelEnd = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(tracksCount - 1)); m_model->dataChanged(modelStart, modelEnd, {TimelineModel::HeightRole}); } int TimelineController::groupClips(const QList &clipIds) { std::unordered_set theSet(clipIds.begin(), clipIds.end()); return m_model->requestClipsGroup(theSet, false, GroupType::Selection); } bool TimelineController::ungroupClips(int clipId) { return m_model->requestClipUngroup(clipId); } void TimelineController::clearSelection() { if (m_model->m_temporarySelectionGroup >= 0) { m_model->m_groups->destructGroupItem(m_model->m_temporarySelectionGroup); m_model->m_temporarySelectionGroup = -1; } m_selection.selectedItems.clear(); emit selectionChanged(); } void TimelineController::selectAll() { QList ids; - for (auto clp : m_model->m_allClips) { + for (const auto &clp : m_model->m_allClips) { ids << clp.first; } - for (auto clp : m_model->m_allCompositions) { + for (const auto &clp : m_model->m_allCompositions) { ids << clp.first; } setSelection(ids); } void TimelineController::selectCurrentTrack() { QList ids; - for (auto clp : m_model->getTrackById_const(m_activeTrack)->m_allClips) { + for (const auto &clp : m_model->getTrackById_const(m_activeTrack)->m_allClips) { ids << clp.first; } - for (auto clp : m_model->getTrackById_const(m_activeTrack)->m_allCompositions) { + for (const auto &clp : m_model->getTrackById_const(m_activeTrack)->m_allCompositions) { ids << clp.first; } setSelection(ids); } void TimelineController::pasteEffects(int targetId) { QVariant returnedValue; QMetaObject::invokeMethod(m_root, "getCopiedItemId", Q_RETURN_ARG(QVariant, returnedValue)); int sourceId = returnedValue.toInt(); if (targetId == -1 && !m_selection.selectedItems.isEmpty()) { targetId = m_selection.selectedItems.constFirst(); } if (!m_model->isClip(targetId) || !m_model->isClip(sourceId)) { return; } std::function undo = []() { return true; }; std::function redo = []() { return true; }; std::shared_ptr sourceStack = m_model->getClipEffectStackModel(sourceId); std::shared_ptr destStack = m_model->getClipEffectStackModel(targetId); bool result = destStack->importEffects(sourceStack, m_model->m_allClips[targetId]->clipState(), undo, redo); if (result) { pCore->pushUndo(undo, redo, i18n("Paste effects")); } else { pCore->displayMessage(i18n("Cannot paste effect on selected clip"), InformationMessage, 500); undo(); } } double TimelineController::fps() const { return pCore->getCurrentFps(); } void TimelineController::editItemDuration(int id) { int start = m_model->getItemPosition(id); int in = 0; int duration = m_model->getItemPlaytime(id); int maxLength = -1; bool isComposition = false; if (m_model->isClip(id)) { in = m_model->getClipIn(id); std::shared_ptr clip = pCore->bin()->getBinClip(getClipBinId(id)); if (clip && clip->hasLimitedDuration()) { maxLength = clip->getProducerDuration(); } } else if (m_model->isComposition(id)) { // nothing to do isComposition = true; } else { pCore->displayMessage(i18n("No item to edit"), InformationMessage, 500); return; } int trackId = m_model->getItemTrackId(id); int maxFrame = qMax(0, start + duration + (isComposition ? m_model->getTrackById(trackId)->getBlankSizeNearComposition(id, true) : m_model->getTrackById(trackId)->getBlankSizeNearClip(id, true))); int minFrame = qMax(0, in - (isComposition ? m_model->getTrackById(trackId)->getBlankSizeNearComposition(id, false) : m_model->getTrackById(trackId)->getBlankSizeNearClip(id, false))); int partner = isComposition ? -1 : m_model->getClipSplitPartner(id); QPointer dialog = new ClipDurationDialog(id, pCore->currentDoc()->timecode(), start, minFrame, in, in + duration, maxLength, maxFrame, qApp->activeWindow()); if (dialog->exec() == QDialog::Accepted) { std::function undo = []() { return true; }; std::function redo = []() { return true; }; int newPos = dialog->startPos().frames(pCore->getCurrentFps()); int newIn = dialog->cropStart().frames(pCore->getCurrentFps()); int newDuration = dialog->duration().frames(pCore->getCurrentFps()); bool result = true; if (newPos < start) { if (!isComposition) { result = m_model->requestClipMove(id, trackId, newPos, true, true, undo, redo); if (result && partner > -1) { result = m_model->requestClipMove(partner, m_model->getItemTrackId(partner), newPos, true, true, undo, redo); } } else { result = m_model->requestCompositionMove(id, trackId, newPos, m_model->m_allCompositions[id]->getForcedTrack(), true, true, undo, redo); } if (result && newIn != in) { m_model->requestItemResize(id, duration + (in - newIn), false, true, undo, redo); if (result && partner > -1) { result = m_model->requestItemResize(partner, duration + (in - newIn), false, true, undo, redo); } } if (newDuration != duration + (in - newIn)) { result = result && m_model->requestItemResize(id, newDuration, true, true, undo, redo); if (result && partner > -1) { result = m_model->requestItemResize(partner, newDuration, false, true, undo, redo); } } } else { // perform resize first if (newIn != in) { result = m_model->requestItemResize(id, duration + (in - newIn), false, true, undo, redo); if (result && partner > -1) { result = m_model->requestItemResize(partner, duration + (in - newIn), false, true, undo, redo); } } if (newDuration != duration + (in - newIn)) { result = result && m_model->requestItemResize(id, newDuration, start == newPos, true, undo, redo); if (result && partner > -1) { result = m_model->requestItemResize(partner, newDuration, start == newPos, true, undo, redo); } } if (start != newPos || newIn != in) { if (!isComposition) { result = result && m_model->requestClipMove(id, trackId, newPos, true, true, undo, redo); if (result && partner > -1) { result = m_model->requestClipMove(partner, m_model->getItemTrackId(partner), newPos, true, true, undo, redo); } } else { result = result && m_model->requestCompositionMove(id, trackId, newPos, m_model->m_allCompositions[id]->getForcedTrack(), true, true, undo, redo); } } } if (result) { pCore->pushUndo(undo, redo, i18n("Edit item")); } else { undo(); } } } void TimelineController::updateClipActions() { if (m_selection.selectedItems.isEmpty()) { for (QAction *act : clipActions) { act->setEnabled(false); } emit timelineClipSelected(false); return; } std::shared_ptr clip(nullptr); int item = m_selection.selectedItems.first(); if (m_model->isClip(item)) { clip = m_model->getClipPtr(item); } for (QAction *act : clipActions) { bool enableAction = true; const QChar actionData = act->data().toChar(); if (actionData == QLatin1Char('G')) { enableAction = m_model->isInMultiSelection(item); } else if (actionData == QLatin1Char('U')) { enableAction = m_model->m_groups->isInGroup(item) && !m_model->isInMultiSelection(item); } else if (actionData == QLatin1Char('A')) { enableAction = clip && clip->clipState() == PlaylistState::AudioOnly; } else if (actionData == QLatin1Char('V')) { enableAction = clip && clip->clipState() == PlaylistState::VideoOnly; } else if (actionData == QLatin1Char('D')) { enableAction = clip && clip->clipState() == PlaylistState::Disabled; } else if (actionData == QLatin1Char('E')) { enableAction = clip && clip->clipState() != PlaylistState::Disabled; } else if (actionData == QLatin1Char('X') || actionData == QLatin1Char('S')) { enableAction = clip && clip->canBeVideo() && clip->canBeAudio(); if (enableAction && actionData == QLatin1Char('S')) { act->setText(clip->clipState() == PlaylistState::AudioOnly ? i18n("Split video") : i18n("Split audio")); } } else if (actionData == QLatin1Char('C') && clip == nullptr) { enableAction = false; } act->setEnabled(enableAction); } emit timelineClipSelected(clip != nullptr); } const QString TimelineController::getAssetName(const QString &assetId, bool isTransition) { return isTransition ? TransitionsRepository::get()->getName(assetId) : EffectsRepository::get()->getName(assetId); } void TimelineController::grabCurrent() { if (m_selection.selectedItems.isEmpty()) { // TODO: error displayMessage return; } int id = m_selection.selectedItems.constFirst(); if (m_model->isClip(id)) { std::shared_ptr clip = m_model->getClipPtr(id); clip->setGrab(!clip->isGrabbed()); QModelIndex ix = m_model->makeClipIndexFromID(id); if (ix.isValid()) { m_model->dataChanged(ix, ix, {TimelineItemModel::GrabbedRole}); } } else if (m_model->isComposition(id)) { std::shared_ptr clip = m_model->getCompositionPtr(id); clip->setGrab(!clip->isGrabbed()); QModelIndex ix = m_model->makeCompositionIndexFromID(id); if (ix.isValid()) { m_model->dataChanged(ix, ix, {TimelineItemModel::GrabbedRole}); } } } int TimelineController::getItemMovingTrack(int itemId) const { if (m_model->isClip(itemId)) { int trackId = m_model->m_allClips[itemId]->getFakeTrackId(); return trackId < 0 ? m_model->m_allClips[itemId]->getCurrentTrackId() : trackId; } return m_model->m_allCompositions[itemId]->getCurrentTrackId(); } bool TimelineController::endFakeMove(int clipId, int position, bool updateView, bool logUndo, bool invalidateTimeline) { Q_ASSERT(m_model->m_allClips.count(clipId) > 0); int trackId = m_model->m_allClips[clipId]->getFakeTrackId(); if (m_model->getClipPosition(clipId) == position && m_model->getClipTrackId(clipId) == trackId) { qDebug() << "* * ** END FAKE; NO MOVE RQSTED"; return true; } if (m_model->m_groups->isInGroup(clipId)) { // element is in a group. int groupId = m_model->m_groups->getRootId(clipId); int current_trackId = m_model->getClipTrackId(clipId); int track_pos1 = m_model->getTrackPosition(trackId); int track_pos2 = m_model->getTrackPosition(current_trackId); int delta_track = track_pos1 - track_pos2; int delta_pos = position - m_model->m_allClips[clipId]->getPosition(); return endFakeGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo); } qDebug() << "//////\n//////\nENDING FAKE MNOVE: " << trackId << ", POS: " << position; std::function undo = []() { return true; }; std::function redo = []() { return true; }; int duration = m_model->getClipPlaytime(clipId); int currentTrack = m_model->m_allClips[clipId]->getCurrentTrackId(); bool res = true; if (currentTrack > -1) { res = res & m_model->getTrackById(currentTrack)->requestClipDeletion(clipId, updateView, invalidateTimeline, undo, redo); } if (m_model->m_editMode == TimelineMode::OverwriteEdit) { res = res & TimelineFunctions::liftZone(m_model, trackId, QPoint(position, position + duration), undo, redo); } else if (m_model->m_editMode == TimelineMode::InsertEdit) { int startClipId = m_model->getClipByPosition(trackId, position); if (startClipId > -1) { // There is a clip, cut res = res & TimelineFunctions::requestClipCut(m_model, startClipId, position, undo, redo); } res = res & TimelineFunctions::insertSpace(m_model, trackId, QPoint(position, position + duration), undo, redo); } res = res & m_model->getTrackById(trackId)->requestClipInsertion(clipId, position, updateView, invalidateTimeline, undo, redo); if (res) { if (logUndo) { pCore->pushUndo(undo, redo, i18n("Move item")); } } else { qDebug() << "//// FAKE FAILED"; undo(); } return res; } bool TimelineController::endFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool logUndo) { std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool res = endFakeGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo, undo, redo); if (res && logUndo) { pCore->pushUndo(undo, redo, i18n("Move group")); } return res; } bool TimelineController::endFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool finalMove, Fun &undo, Fun &redo) { Q_ASSERT(m_model->m_allGroups.count(groupId) > 0); bool ok = true; auto all_items = m_model->m_groups->getLeaves(groupId); Q_ASSERT(all_items.size() > 1); Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; // Sort clips. We need to delete from right to left to avoid confusing the view std::vector sorted_clips(all_items.begin(), all_items.end()); std::sort(sorted_clips.begin(), sorted_clips.end(), [this](int clipId1, int clipId2) { int p1 = m_model->isClip(clipId1) ? m_model->m_allClips[clipId1]->getPosition() : m_model->m_allCompositions[clipId1]->getPosition(); int p2 = m_model->isClip(clipId2) ? m_model->m_allClips[clipId2]->getPosition() : m_model->m_allCompositions[clipId2]->getPosition(); return p2 <= p1; }); // Moving groups is a two stage process: first we remove the clips from the tracks, and then try to insert them back at their calculated new positions. // This way, we ensure that no conflict will arise with clips inside the group being moved // First, remove clips int audio_delta, video_delta; audio_delta = video_delta = delta_track; int master_trackId = m_model->getItemTrackId(clipId); if (m_model->getTrackById_const(master_trackId)->isAudioTrack()) { // Master clip is audio, so reverse delta for video clips video_delta = -delta_track; } else { audio_delta = -delta_track; } int min = -1; int max = -1; std::unordered_map old_track_ids, old_position, old_forced_track, new_track_ids; for (int item : sorted_clips) { int old_trackId = m_model->getItemTrackId(item); old_track_ids[item] = old_trackId; if (old_trackId != -1) { bool updateThisView = true; if (m_model->isClip(item)) { int current_track_position = m_model->getTrackPosition(old_trackId); int d = m_model->getTrackById_const(old_trackId)->isAudioTrack() ? audio_delta : video_delta; int target_track_position = current_track_position + d; auto it = m_model->m_allTracks.cbegin(); std::advance(it, target_track_position); int target_track = (*it)->getId(); new_track_ids[item] = target_track; old_position[item] = m_model->m_allClips[item]->getPosition(); int duration = m_model->m_allClips[item]->getPlaytime(); min = min < 0 ? old_position[item] + delta_pos : qMin(min, old_position[item] + delta_pos); max = max < 0 ? old_position[item] + delta_pos + duration : qMax(max, old_position[item] + delta_pos + duration); ok = ok && m_model->getTrackById(old_trackId)->requestClipDeletion(item, updateThisView, finalMove, undo, redo); } else { // ok = ok && getTrackById(old_trackId)->requestCompositionDeletion(item, updateThisView, local_undo, local_redo); old_position[item] = m_model->m_allCompositions[item]->getPosition(); old_forced_track[item] = m_model->m_allCompositions[item]->getForcedTrack(); } if (!ok) { bool undone = undo(); Q_ASSERT(undone); return false; } } } bool res = true; if (m_model->m_editMode == TimelineMode::OverwriteEdit) { for (int item : sorted_clips) { if (m_model->isClip(item) && new_track_ids.count(item) > 0) { int target_track = new_track_ids[item]; int target_position = old_position[item] + delta_pos; int duration = m_model->m_allClips[item]->getPlaytime(); res = res & TimelineFunctions::liftZone(m_model, target_track, QPoint(target_position, target_position + duration), undo, redo); } } } else if (m_model->m_editMode == TimelineMode::InsertEdit) { QList processedTracks; for (int item : sorted_clips) { int target_track = new_track_ids[item]; if (processedTracks.contains(target_track)) { // already processed continue; } processedTracks << target_track; int target_position = min; int startClipId = m_model->getClipByPosition(target_track, target_position); if (startClipId > -1) { // There is a clip, cut res = res & TimelineFunctions::requestClipCut(m_model, startClipId, target_position, undo, redo); } } res = res & TimelineFunctions::insertSpace(m_model, -1, QPoint(min, max), undo, redo); } for (int item : sorted_clips) { if (m_model->isClip(item)) { int target_track = new_track_ids[item]; int target_position = old_position[item] + delta_pos; ok = ok && m_model->requestClipMove(item, target_track, target_position, updateView, finalMove, undo, redo); } else { // ok = ok && requestCompositionMove(item, target_track, old_forced_track[item], target_position, updateThisView, local_undo, local_redo); } if (!ok) { bool undone = undo(); Q_ASSERT(undone); return false; } } return true; } QStringList TimelineController::getThumbKeys() { QStringList result; - for (auto clp : m_model->m_allClips) { + for (const auto &clp : m_model->m_allClips) { const QString binId = getClipBinId(clp.first); std::shared_ptr binClip = pCore->bin()->getBinClip(binId); result << binClip->hash() + QLatin1Char('#') + QString::number(clp.second->getIn()) + QStringLiteral(".png"); result << binClip->hash() + QLatin1Char('#') + QString::number(clp.second->getOut()) + QStringLiteral(".png"); } result.removeDuplicates(); return result; } bool TimelineController::isInSelection(int itemId) { return m_model->isInMultiSelection(itemId); } bool TimelineController::exists(int itemId) { return m_model->isClip(itemId) || m_model->isComposition(itemId); } void TimelineController::slotMultitrackView(bool enable) { TimelineFunctions::enableMultitrackView(m_model, enable); } -void TimelineController::saveTimelineSelection(QDir targetDir) +void TimelineController::saveTimelineSelection(const QDir &targetDir) { TimelineFunctions::saveTimelineSelection(m_model, m_selection.selectedItems, targetDir); } void TimelineController::addEffectKeyframe(int cid, int frame, double val) { if (m_model->isClip(cid)) { std::shared_ptr destStack = m_model->getClipEffectStackModel(cid); destStack->addEffectKeyFrame(frame, val); } else if (m_model->isComposition(cid)) { std::shared_ptr listModel = m_model->m_allCompositions[cid]->getKeyframeModel(); listModel->addKeyframe(frame, val); } } void TimelineController::removeEffectKeyframe(int cid, int frame) { if (m_model->isClip(cid)) { std::shared_ptr destStack = m_model->getClipEffectStackModel(cid); destStack->removeKeyFrame(frame); } else if (m_model->isComposition(cid)) { std::shared_ptr listModel = m_model->m_allCompositions[cid]->getKeyframeModel(); listModel->removeKeyframe(GenTime(frame, pCore->getCurrentFps())); } } -void TimelineController::updateEffectKeyframe(int cid, int oldFrame, int newFrame, QVariant normalizedValue) +void TimelineController::updateEffectKeyframe(int cid, int oldFrame, int newFrame, const QVariant &normalizedValue) { if (m_model->isClip(cid)) { std::shared_ptr destStack = m_model->getClipEffectStackModel(cid); destStack->updateKeyFrame(oldFrame, newFrame, normalizedValue); } else if (m_model->isComposition(cid)) { std::shared_ptr listModel = m_model->m_allCompositions[cid]->getKeyframeModel(); listModel->updateKeyframe(GenTime(oldFrame, pCore->getCurrentFps()), GenTime(newFrame, pCore->getCurrentFps()), normalizedValue); } } QColor TimelineController::videoColor() const { KColorScheme scheme(QApplication::palette().currentColorGroup(), KColorScheme::View); return scheme.background(KColorScheme::LinkBackground).color().darker(); } QColor TimelineController::audioColor() const { KColorScheme scheme(QApplication::palette().currentColorGroup(), KColorScheme::View); return scheme.background(KColorScheme::NegativeBackground).color(); } QColor TimelineController::neutralColor() const { KColorScheme scheme(QApplication::palette().currentColorGroup(), KColorScheme::View); return scheme.background(KColorScheme::VisitedBackground).color(); } QColor TimelineController::groupColor() const { KColorScheme scheme(QApplication::palette().currentColorGroup(), KColorScheme::Complementary); return scheme.background(KColorScheme::NegativeBackground).color(); } diff --git a/src/timeline2/view/timelinecontroller.h b/src/timeline2/view/timelinecontroller.h index 7d4ca9931..f87ea8c02 100644 --- a/src/timeline2/view/timelinecontroller.h +++ b/src/timeline2/view/timelinecontroller.h @@ -1,530 +1,530 @@ /*************************************************************************** * Copyright (C) 2017 by Jean-Baptiste Mardelle * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #ifndef TIMELINECONTROLLER_H #define TIMELINECONTROLLER_H #include "definitions.h" #include "lib/audio/audioCorrelation.h" #include "timeline2/model/timelineitemmodel.hpp" #include #include class PreviewManager; class QAction; class QQuickItem; // see https://bugreports.qt.io/browse/QTBUG-57714, don't expose a QWidget as a context property class TimelineController : public QObject { Q_OBJECT /* @brief holds a list of currently selected clips (list of clipId's) */ Q_PROPERTY(QList selection READ selection WRITE setSelection NOTIFY selectionChanged) /* @brief holds the timeline zoom factor */ Q_PROPERTY(double scaleFactor READ scaleFactor WRITE setScaleFactor NOTIFY scaleFactorChanged) /* @brief holds the current project duration */ Q_PROPERTY(int duration READ duration NOTIFY durationChanged) Q_PROPERTY(int fullDuration READ fullDuration NOTIFY durationChanged) Q_PROPERTY(bool audioThumbFormat READ audioThumbFormat NOTIFY audioThumbFormatChanged) /* @brief holds the current timeline position */ Q_PROPERTY(int position READ position WRITE setPosition NOTIFY positionChanged) Q_PROPERTY(int zoneIn READ zoneIn WRITE setZoneIn NOTIFY zoneChanged) Q_PROPERTY(int zoneOut READ zoneOut WRITE setZoneOut NOTIFY zoneChanged) Q_PROPERTY(int seekPosition READ seekPosition WRITE setSeekPosition NOTIFY seekPositionChanged) Q_PROPERTY(bool ripple READ ripple NOTIFY rippleChanged) Q_PROPERTY(bool scrub READ scrub NOTIFY scrubChanged) Q_PROPERTY(bool showThumbnails READ showThumbnails NOTIFY showThumbnailsChanged) Q_PROPERTY(bool showMarkers READ showMarkers NOTIFY showMarkersChanged) Q_PROPERTY(bool showAudioThumbnails READ showAudioThumbnails NOTIFY showAudioThumbnailsChanged) Q_PROPERTY(QVariantList dirtyChunks READ dirtyChunks NOTIFY dirtyChunksChanged) Q_PROPERTY(QVariantList renderedChunks READ renderedChunks NOTIFY renderedChunksChanged) Q_PROPERTY(int workingPreview READ workingPreview NOTIFY workingPreviewChanged) Q_PROPERTY(bool useRuler READ useRuler NOTIFY useRulerChanged) Q_PROPERTY(int activeTrack READ activeTrack WRITE setActiveTrack NOTIFY activeTrackChanged) Q_PROPERTY(int audioTarget READ audioTarget WRITE setAudioTarget NOTIFY audioTargetChanged) Q_PROPERTY(int videoTarget READ videoTarget WRITE setVideoTarget NOTIFY videoTargetChanged) Q_PROPERTY(QColor videoColor READ videoColor NOTIFY colorsChanged) Q_PROPERTY(QColor audioColor READ audioColor NOTIFY colorsChanged) Q_PROPERTY(QColor neutralColor READ neutralColor NOTIFY colorsChanged) Q_PROPERTY(QColor groupColor READ groupColor NOTIFY colorsChanged) public: TimelineController(QObject *parent); virtual ~TimelineController(); /** @brief Sets the model that this widgets displays */ void setModel(std::shared_ptr model); std::shared_ptr getModel() const; void setRoot(QQuickItem *root); Q_INVOKABLE bool isMultitrackSelected() const { return m_selection.isMultitrackSelected; } Q_INVOKABLE int selectedTrack() const { return m_selection.selectedTrack; } /** @brief Remove a clip id from current selection */ Q_INVOKABLE void removeSelection(int newSelection); /** @brief Add a clip id to current selection */ Q_INVOKABLE void addSelection(int newSelection, bool clear = false); /** @brief Edit an item's in/out points with a dialog */ Q_INVOKABLE void editItemDuration(int itemId); /** @brief Clear current selection and inform the view */ void clearSelection(); /** @brief Select all timeline items */ void selectAll(); /* @brief Select all items in one track */ void selectCurrentTrack(); /* @brief returns current timeline's zoom factor */ Q_INVOKABLE double scaleFactor() const; /* @brief set current timeline's zoom factor */ void setScaleFactorOnMouse(double scale, bool zoomOnMouse); void setScaleFactor(double scale); /* @brief Returns the project's duration (tractor) */ Q_INVOKABLE int duration() const; Q_INVOKABLE int fullDuration() const; /* @brief Returns the current cursor position (frame currently displayed by MLT) */ Q_INVOKABLE int position() const { return m_position; } /* @brief Returns the seek request position (-1 = no seek pending) */ Q_INVOKABLE int seekPosition() const { return m_seekPosition; } Q_INVOKABLE int audioTarget() const; Q_INVOKABLE int videoTarget() const; Q_INVOKABLE int activeTrack() const { return m_activeTrack; } Q_INVOKABLE QColor videoColor() const; Q_INVOKABLE QColor audioColor() const; Q_INVOKABLE QColor neutralColor() const; Q_INVOKABLE QColor groupColor() const; /* @brief Request a seek operation @param position is the desired new timeline position */ Q_INVOKABLE int zoneIn() const { return m_zone.x(); } Q_INVOKABLE int zoneOut() const { return m_zone.y(); } Q_INVOKABLE void setZoneIn(int inPoint); Q_INVOKABLE void setZoneOut(int outPoint); void setZone(const QPoint &zone); /* @brief Request a seek operation @param position is the desired new timeline position */ Q_INVOKABLE void setPosition(int position); Q_INVOKABLE bool snap(); Q_INVOKABLE bool ripple(); Q_INVOKABLE bool scrub(); Q_INVOKABLE QString timecode(int frames); /* @brief Request inserting a new clip in timeline (dragged from bin or monitor) @param tid is the destination track @param position is the timeline position @param xml is the data describing the dropped clip @param logUndo if set to false, no undo object is stored @return the id of the inserted clip */ Q_INVOKABLE int insertClip(int tid, int position, const QString &xml, bool logUndo, bool refreshView, bool useTargets); /* @brief Request inserting multiple clips into the timeline (dragged from bin or monitor) * @param tid is the destination track * @param position is the timeline position * @param binIds the IDs of the bins being dropped * @param logUndo if set to false, no undo object is stored * @return the ids of the inserted clips */ Q_INVOKABLE QList insertClips(int tid, int position, const QStringList &binIds, bool logUndo, bool refreshView); /* @brief Request the grouping of the given clips * @param clipIds the ids to be grouped * @return the group id or -1 in case of faiure */ Q_INVOKABLE int groupClips(const QList &clipIds); /* @brief Request the ungrouping of clips * @param clipId the id of a clip belonging to the group * @return true in case of success, false otherwise */ Q_INVOKABLE bool ungroupClips(int clipId); Q_INVOKABLE void copyItem(); Q_INVOKABLE bool pasteItem(); /* @brief Request inserting a new composition in timeline (dragged from compositions list) @param tid is the destination track @param position is the timeline position @param transitionId is the data describing the dropped composition @param logUndo if set to false, no undo object is stored @return the id of the inserted composition */ Q_INVOKABLE int insertComposition(int tid, int position, const QString &transitionId, bool logUndo); /* @brief Request inserting a new composition in timeline (dragged from compositions list) this function will check if there is a clip at insert point and adjust the composition length accordingly @param tid is the destination track @param position is the timeline position @param transitionId is the data describing the dropped composition @param logUndo if set to false, no undo object is stored @return the id of the inserted composition */ Q_INVOKABLE int insertNewComposition(int tid, int position, const QString &transitionId, bool logUndo); Q_INVOKABLE int insertNewComposition(int tid, int clipId, int offset, const QString &transitionId, bool logUndo); /* @brief Request deletion of the currently selected clips */ Q_INVOKABLE void deleteSelectedClips(); Q_INVOKABLE void triggerAction(const QString &name); /* @brief Do we want to display video thumbnails */ bool showThumbnails() const; bool showAudioThumbnails() const; bool showMarkers() const; bool audioThumbFormat() const; /* @brief Do we want to display audio thumbnails */ Q_INVOKABLE bool showWaveforms() const; /* @brief Insert a timeline track */ Q_INVOKABLE void addTrack(int tid); /* @brief Remove a timeline track */ Q_INVOKABLE void deleteTrack(int tid); /* @brief Group selected items in timeline */ Q_INVOKABLE void groupSelection(); /* @brief Ungroup selected items in timeline */ Q_INVOKABLE void unGroupSelection(int cid = -1); /* @brief Ask for edit marker dialog */ Q_INVOKABLE void editMarker(const QString &cid, int frame); /* @brief Ask for edit timeline guide dialog */ Q_INVOKABLE void editGuide(int frame = -1); Q_INVOKABLE void moveGuide(int frame, int newFrame); /* @brief Add a timeline guide */ Q_INVOKABLE void switchGuide(int frame = -1, bool deleteOnly = false); /* @brief Request monitor refresh */ Q_INVOKABLE void requestRefresh(); /* @brief Show the asset of the given item in the AssetPanel If the id corresponds to a clip, we show the corresponding effect stack If the id corresponds to a composition, we show its properties */ Q_INVOKABLE void showAsset(int id); Q_INVOKABLE void showTrackAsset(int trackId); - Q_INVOKABLE void selectItems(QVariantList arg, int startFrame, int endFrame, bool addToSelect); + Q_INVOKABLE void selectItems(const QVariantList &arg, int startFrame, int endFrame, bool addToSelect); /* @brief Returns true is item is selected as well as other items */ Q_INVOKABLE bool isInSelection(int itemId); Q_INVOKABLE bool exists(int itemId); Q_INVOKABLE int headerWidth() const; Q_INVOKABLE void setHeaderWidth(int width); /* @brief Seek to next snap point */ void gotoNextSnap(); /* @brief Seek to previous snap point */ void gotoPreviousSnap(); /* @brief Set current item's start point to cursor position */ void setInPoint(); /* @brief Set current item's end point to cursor position */ void setOutPoint(); /* @brief Return the project's tractor */ Mlt::Tractor *tractor(); /* @brief Sets the list of currently selected clips @param selection is the list of id's @param trackIndex is current clip's track @param isMultitrack is true if we want to select the whole tractor (currently unused) */ void setSelection(const QList &selection = QList(), int trackIndex = -1, bool isMultitrack = false); /* @brief Get the list of currently selected clip id's */ QList selection() const; /* @brief Add an asset (effect, composition) */ - void addAsset(const QVariantMap data); + void addAsset(const QVariantMap &data); /* @brief Cuts the clip on current track at timeline position */ Q_INVOKABLE void cutClipUnderCursor(int position = -1, int track = -1); /* @brief Request a spacer operation */ Q_INVOKABLE int requestSpacerStartOperation(int trackId, int position); /* @brief Request a spacer operation */ Q_INVOKABLE bool requestSpacerEndOperation(int clipId, int startPosition, int endPosition); /* @brief Request a Fade in effect for clip */ Q_INVOKABLE void adjustFade(int cid, const QString &effectId, int duration, int initialDuration); Q_INVOKABLE const QString getTrackNameFromMltIndex(int trackPos); /* @brief Request inserting space in a track */ Q_INVOKABLE void insertSpace(int trackId = -1, int frame = -1); Q_INVOKABLE void removeSpace(int trackId = -1, int frame = -1, bool affectAllTracks = false); /* @brief If clip is enabled, disable, otherwise enable */ Q_INVOKABLE void switchEnableState(int clipId); Q_INVOKABLE void addCompositionToClip(const QString &assetId, int clipId, int offset); Q_INVOKABLE void addEffectToClip(const QString &assetId, int clipId); Q_INVOKABLE void requestClipCut(int clipId, int position); Q_INVOKABLE void extract(int clipId); Q_INVOKABLE void splitAudio(int clipId); Q_INVOKABLE void splitVideo(int clipId); Q_INVOKABLE void setAudioRef(int clipId); Q_INVOKABLE void alignAudio(int clipId); Q_INVOKABLE bool endFakeMove(int clipId, int position, bool updateView, bool logUndo, bool invalidateTimeline); Q_INVOKABLE int getItemMovingTrack(int itemId) const; bool endFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool logUndo); bool endFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool finalMove, Fun &undo, Fun &redo); bool splitAV(); /* @brief Seeks to selected clip start / end */ Q_INVOKABLE void pasteEffects(int targetId = -1); Q_INVOKABLE double fps() const; Q_INVOKABLE void addEffectKeyframe(int cid, int frame, double val); Q_INVOKABLE void removeEffectKeyframe(int cid, int frame); - Q_INVOKABLE void updateEffectKeyframe(int cid, int oldFrame, int newFrame, QVariant normalizedValue = QVariant()); + Q_INVOKABLE void updateEffectKeyframe(int cid, int oldFrame, int newFrame, const QVariant &normalizedValue = QVariant()); void switchTrackLock(bool applyToAll = false); void switchTargetTrack(); const QString getTrackNameFromIndex(int trackIndex); /* @brief Seeks to selected clip start / end */ void seekCurrentClip(bool seekToEnd = false); /* @brief Seeks to a clip start (or end) based on it's clip id */ void seekToClip(int cid, bool seekToEnd); /* @brief Returns the number of tracks (audioTrakcs, videoTracks) */ QPoint getTracksCount() const; /* @brief Request monitor refresh if item (clip or composition) is under timeline cursor */ void refreshItem(int id); /* @brief Seek timeline to mouse position */ void seekToMouse(); /* @brief User enabled / disabled snapping, update timeline behavior */ void snapChanged(bool snap); /* @brief Returns a list of all luma files used in the project */ QStringList extractCompositionLumas() const; /* @brief Get the frame where mouse is positioned */ int getMousePos(); /* @brief Get the frame where mouse is positioned */ int getMouseTrack(); /* @brief Returns a map of track ids/track names */ QMap getTrackNames(bool videoOnly); /* @brief Returns the transition a track index for a composition (MLT index / Track id) */ QPair getCompositionATrack(int cid) const; void setCompositionATrack(int cid, int aTrack); /* @brief Return true if composition's a_track is automatic (no forced track) */ bool compositionAutoTrack(int cid) const; const QString getClipBinId(int clipId) const; void focusItem(int itemId); /* @brief Create and display a split clip view to compare effect */ bool createSplitOverlay(Mlt::Filter *filter); /* @brief Delete the split clip view to compare effect */ void removeSplitOverlay(); /* @brief Add current timeline zone to preview rendering */ void addPreviewRange(bool add); /* @brief Clear current timeline zone from preview rendering */ void clearPreviewRange(); void startPreviewRender(); void stopPreviewRender(); QVariantList dirtyChunks() const; QVariantList renderedChunks() const; /* @brief returns the frame currently processed by timeline preview, -1 if none */ int workingPreview() const; /** @brief Return true if we want to use timeline ruler zone for editing */ bool useRuler() const; /* @brief Load timeline preview from saved doc */ - void loadPreview(QString chunks, QString dirty, const QDateTime &documentDate, int enable); + void loadPreview(const QString &chunks, const QString &dirty, const QDateTime &documentDate, int enable); /* @brief Return document properties with added settings from timeline */ QMap documentProperties(); /** @brief Change track compsiting mode */ void switchCompositing(int mode); /** @brief Change a clip item's speed in timeline */ Q_INVOKABLE void changeItemSpeed(int clipId, double speed); /** @brief Delete selected zone and fill gap by moving following clips * @param lift if true, the zone will simply be deleted but clips won't be moved */ void extractZone(QPoint zone, bool liftOnly = false); /** @brief Insert clip monitor into timeline * @returns the zone end position or -1 on fail */ int insertZone(const QString &binId, QPoint zone, bool overwrite); - void updateClip(int clipId, QVector roles); + void updateClip(int clipId, const QVector &roles); void showClipKeyframes(int clipId, bool value); void showCompositionKeyframes(int clipId, bool value); /** @brief Returns last usable timeline position (seek request or current pos) */ int timelinePosition() const; /** @brief Adjust all timeline tracks height */ void resetTrackHeight(); /** @brief timeline preview params changed, reset */ void resetPreview(); /** @brief Select the clip in active track under cursor */ void selectCurrentItem(ObjectType type, bool select, bool addToCurrent = false); /** @brief Set target tracks (video, audio) */ void setTargetTracks(QPair targets); /** @brief Return asset's display name from it's id (effect or composition) */ Q_INVOKABLE const QString getAssetName(const QString &assetId, bool isTransition); /** @brief Set keyboard grabbing on current selection */ void grabCurrent(); /** @brief Returns keys for all used thumbnails */ QStringList getThumbKeys(); public slots: void selectMultitrack(); void resetView(); Q_INVOKABLE void setSeekPosition(int position); Q_INVOKABLE void setAudioTarget(int track); Q_INVOKABLE void setVideoTarget(int track); Q_INVOKABLE void setActiveTrack(int track); void onSeeked(int position); void addEffectToCurrentClip(const QStringList &effectData); /** @brief Dis / enable timeline preview. */ void disablePreview(bool disable); void invalidateItem(int cid); void invalidateZone(int in, int out); void checkDuration(); /** @brief Dis / enable multi track view. */ void slotMultitrackView(bool enable); /** @brief Save timeline selected clips to target folder. */ - void saveTimelineSelection(QDir targetDir); + void saveTimelineSelection(const QDir &targetDir); /** @brief Restore timeline scroll pos on open. */ void setScrollPos(int pos); private slots: void slotUpdateSelection(int itemId); void updateClipActions(); public: /** @brief a list of actions that have to be enabled/disabled depending on the timeline selection */ QList clipActions; private: QQuickItem *m_root; KActionCollection *m_actionCollection; std::shared_ptr m_model; bool m_usePreview; struct Selection { QList selectedItems; int selectedTrack; bool isMultitrackSelected; }; int m_position; int m_seekPosition; int m_audioTarget; int m_videoTarget; int m_activeTrack; int m_audioRef; QPoint m_zone; double m_scale; static int m_duration; Selection m_selection; Selection m_savedSelection; PreviewManager *m_timelinePreview; QAction *m_disablePreview; std::shared_ptr m_audioCorrelator; QMutex m_metaMutex; void emitSelectedFromSelection(); int getCurrentItem(); void initializePreview(); // Get a list of currently selected items, including clips grouped with selection std::unordered_set getCurrentSelectionIds() const; signals: void selected(Mlt::Producer *producer); void selectionChanged(); void frameFormatChanged(); void trackHeightChanged(); void scaleFactorChanged(); void audioThumbFormatChanged(); void durationChanged(); void positionChanged(); void seekPositionChanged(); void audioTargetChanged(); void videoTargetChanged(); void activeTrackChanged(); void colorsChanged(); void showThumbnailsChanged(); void showAudioThumbnailsChanged(); void showMarkersChanged(); void rippleChanged(); void scrubChanged(); void seeked(int position); void zoneChanged(); void zoneMoved(const QPoint &zone); /* @brief Requests that a given parameter model is displayed in the asset panel */ void showTransitionModel(int tid, std::shared_ptr); void showItemEffectStack(const QString &clipName, std::shared_ptr, QSize frameSize, bool showKeyframes); /* @brief notify of chunks change */ void dirtyChunksChanged(); void renderedChunksChanged(); void workingPreviewChanged(); void useRulerChanged(); void updateZoom(double); /* @brief emitted when timeline selection changes, true if a clip is selected */ void timelineClipSelected(bool); Q_INVOKABLE void ungrabHack(); }; #endif diff --git a/src/timeline2/view/timelinewidget.cpp b/src/timeline2/view/timelinewidget.cpp index d30c8c5f3..86fdba328 100644 --- a/src/timeline2/view/timelinewidget.cpp +++ b/src/timeline2/view/timelinewidget.cpp @@ -1,199 +1,199 @@ /*************************************************************************** * Copyright (C) 2017 by Jean-Baptiste Mardelle * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #include "timelinewidget.h" #include "../model/builders/meltBuilder.hpp" #include "assets/keyframes/model/keyframemodel.hpp" #include "assets/model/assetparametermodel.hpp" #include "core.h" #include "doc/docundostack.hpp" #include "doc/kdenlivedoc.h" #include "effects/effectlist/model/effectfilter.hpp" #include "effects/effectlist/model/effecttreemodel.hpp" #include "kdenlivesettings.h" #include "mainwindow.h" #include "profiles/profilemodel.hpp" #include "project/projectmanager.h" #include "qml/timelineitems.h" #include "qmltypes/thumbnailprovider.h" #include "timelinecontroller.h" #include "transitions/transitionlist/model/transitionfilter.hpp" #include "transitions/transitionlist/model/transitiontreemodel.hpp" #include "utils/clipboardproxy.hpp" #include #include // #include #include #include #include #include #include #include const int TimelineWidget::comboScale[] = {1, 2, 4, 8, 15, 30, 50, 75, 100, 150, 200, 300, 500, 800, 1000, 1500, 2000, 3000, 6000, 15000, 30000}; TimelineWidget::TimelineWidget(QWidget *parent) : QQuickWidget(parent) { KDeclarative::KDeclarative kdeclarative; kdeclarative.setDeclarativeEngine(engine()); #if KDECLARATIVE_VERSION >= QT_VERSION_CHECK(5, 45, 0) kdeclarative.setupEngine(engine()); kdeclarative.setupContext(); #else kdeclarative.setupBindings(); #endif setClearColor(palette().window().color()); registerTimelineItems(); // Build transition model for context menu m_transitionModel = TransitionTreeModel::construct(true, this); m_transitionProxyModel.reset(new TransitionFilter(this)); static_cast(m_transitionProxyModel.get())->setFilterType(true, TransitionType::Favorites); m_transitionProxyModel->setSourceModel(m_transitionModel.get()); m_transitionProxyModel->setSortRole(AssetTreeModel::NameRole); m_transitionProxyModel->sort(0, Qt::AscendingOrder); // Build effects model for context menu m_effectsModel = EffectTreeModel::construct(QStringLiteral(), this); m_effectsProxyModel.reset(new EffectFilter(this)); static_cast(m_effectsProxyModel.get())->setFilterType(true, EffectType::Favorites); m_effectsProxyModel->setSourceModel(m_effectsModel.get()); m_effectsProxyModel->setSortRole(AssetTreeModel::NameRole); m_effectsProxyModel->sort(0, Qt::AscendingOrder); m_proxy = new TimelineController(this); connect(m_proxy, &TimelineController::zoneMoved, this, &TimelineWidget::zoneMoved); connect(m_proxy, &TimelineController::ungrabHack, this, &TimelineWidget::slotUngrabHack); setResizeMode(QQuickWidget::SizeRootObjectToView); m_thumbnailer = new ThumbnailProvider; engine()->addImageProvider(QStringLiteral("thumbnail"), m_thumbnailer); setVisible(false); setFocusPolicy(Qt::StrongFocus); // connect(&*m_model, SIGNAL(seeked(int)), this, SLOT(onSeeked(int))); } TimelineWidget::~TimelineWidget() { delete m_proxy; } void TimelineWidget::updateEffectFavorites() { rootContext()->setContextProperty("effectModel", sortedItems(KdenliveSettings::favorite_effects(), false)); } void TimelineWidget::updateTransitionFavorites() { rootContext()->setContextProperty("transitionModel", sortedItems(KdenliveSettings::favorite_transitions(), true)); } const QStringList TimelineWidget::sortedItems(const QStringList &items, bool isTransition) { QMap sortedItems; for (const QString &effect : items) { sortedItems.insert(m_proxy->getAssetName(effect, isTransition), effect); } return sortedItems.values(); } -void TimelineWidget::setModel(std::shared_ptr model) +void TimelineWidget::setModel(const std::shared_ptr &model) { m_thumbnailer->resetProject(); m_sortModel.reset(new QSortFilterProxyModel(this)); m_sortModel->setSourceModel(model.get()); m_sortModel->setSortRole(TimelineItemModel::SortRole); m_sortModel->sort(0, Qt::DescendingOrder); m_proxy->setModel(model); rootContext()->setContextProperty("multitrack", m_sortModel.get()); rootContext()->setContextProperty("controller", model.get()); rootContext()->setContextProperty("timeline", m_proxy); rootContext()->setContextProperty("transitionModel", sortedItems(KdenliveSettings::favorite_transitions(), true)); // m_transitionProxyModel.get()); // rootContext()->setContextProperty("effectModel", m_effectsProxyModel.get()); rootContext()->setContextProperty("effectModel", sortedItems(KdenliveSettings::favorite_effects(), false)); rootContext()->setContextProperty("guidesModel", pCore->projectManager()->current()->getGuideModel().get()); rootContext()->setContextProperty("clipboard", new ClipboardProxy(this)); setSource(QUrl(QStringLiteral("qrc:/qml/timeline.qml"))); connect(rootObject(), SIGNAL(mousePosChanged(int)), pCore->window(), SLOT(slotUpdateMousePosition(int))); connect(rootObject(), SIGNAL(zoomIn(bool)), pCore->window(), SLOT(slotZoomIn(bool))); connect(rootObject(), SIGNAL(zoomOut(bool)), pCore->window(), SLOT(slotZoomOut(bool))); m_proxy->setRoot(rootObject()); setVisible(true); loading = false; m_proxy->checkDuration(); m_proxy->positionChanged(); } void TimelineWidget::mousePressEvent(QMouseEvent *event) { emit focusProjectMonitor(); QQuickWidget::mousePressEvent(event); } void TimelineWidget::slotChangeZoom(int value, bool zoomOnMouse) { double pixelScale = QFontMetrics(font()).maxWidth() * 2; m_proxy->setScaleFactorOnMouse(pixelScale / comboScale[value], zoomOnMouse); } Mlt::Tractor *TimelineWidget::tractor() { return m_proxy->tractor(); } TimelineController *TimelineWidget::controller() { return m_proxy; } void TimelineWidget::zoneUpdated(const QPoint &zone) { m_proxy->setZone(zone); } void TimelineWidget::setTool(ProjectTool tool) { rootObject()->setProperty("activeTool", (int)tool); } QPoint TimelineWidget::getTracksCount() const { return m_proxy->getTracksCount(); } void TimelineWidget::slotUngrabHack() { // Workaround bug: https://bugreports.qt.io/browse/QTBUG-59044 // https://phabricator.kde.org/D5515 if (quickWindow() && quickWindow()->mouseGrabberItem()) { quickWindow()->mouseGrabberItem()->ungrabMouse(); } } int TimelineWidget::zoomForScale(double value) const { int scale = 100.0 / value; int ix = 13; while (comboScale[ix] > scale && ix > 0) { ix--; } return ix; } diff --git a/src/timeline2/view/timelinewidget.h b/src/timeline2/view/timelinewidget.h index 40b168e4b..25a1116d3 100644 --- a/src/timeline2/view/timelinewidget.h +++ b/src/timeline2/view/timelinewidget.h @@ -1,87 +1,87 @@ /*************************************************************************** * Copyright (C) 2017 by Jean-Baptiste Mardelle * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #ifndef TIMELINEWIDGET_H #define TIMELINEWIDGET_H #include "assets/assetlist/model/assetfilter.hpp" #include "assets/assetlist/model/assettreemodel.hpp" #include "timeline2/model/timelineitemmodel.hpp" #include class ThumbnailProvider; class KActionCollection; class AssetParameterModel; class TimelineController; class QSortFilterProxyModel; class TimelineWidget : public QQuickWidget { Q_OBJECT public: TimelineWidget(QWidget *parent = Q_NULLPTR); ~TimelineWidget(); /* @brief Sets the model shown by this widget */ - void setModel(std::shared_ptr model); + void setModel(const std::shared_ptr &model); /* @brief Return the project's tractor */ Mlt::Tractor *tractor(); TimelineController *controller(); void setTool(ProjectTool tool); QPoint getTracksCount() const; /* @brief calculate zoom level for a scale */ int zoomForScale(double value) const; bool loading; protected: void mousePressEvent(QMouseEvent *event) override; public slots: void slotChangeZoom(int value, bool zoomOnMouse); void zoneUpdated(const QPoint &zone); /* @brief Favorite effects have changed, reload model for context menu */ void updateEffectFavorites(); /* @brief Favorite transitions have changed, reload model for context menu */ void updateTransitionFavorites(); private slots: void slotUngrabHack(); private: ThumbnailProvider *m_thumbnailer; TimelineController *m_proxy; static const int comboScale[]; std::shared_ptr m_transitionModel; std::unique_ptr m_transitionProxyModel; std::shared_ptr m_effectsModel; std::unique_ptr m_effectsProxyModel; std::unique_ptr m_sortModel; /* @brief Returns an alphabetically sorted list of favorite effects or transitions */ const QStringList sortedItems(const QStringList &items, bool isTransition); signals: void focusProjectMonitor(); void zoneMoved(const QPoint &zone); }; #endif diff --git a/tests/keyframetest.cpp b/tests/keyframetest.cpp index 3f83c1fc1..112b1e0f6 100644 --- a/tests/keyframetest.cpp +++ b/tests/keyframetest.cpp @@ -1,248 +1,248 @@ #include "test_utils.hpp" using namespace fakeit; -bool test_model_equality(std::shared_ptr m1, std::shared_ptr m2) +bool test_model_equality(const std::shared_ptr &m1, const std::shared_ptr &m2) { // we cheat a bit by simply comparing the underlying map qDebug() << "Equality test" << m1->m_keyframeList.size() << m2->m_keyframeList.size(); QList model1; QList model2; for (const auto &m : m1->m_keyframeList) { model1 << m.first.frames(25) << (int)m.second.first << m.second.second; } for (const auto &m : m2->m_keyframeList) { model2 << m.first.frames(25) << (int)m.second.first << m.second.second; } return model1 == model2; } -bool check_anim_identity(std::shared_ptr m) +bool check_anim_identity(const std::shared_ptr &m) { auto m2 = std::shared_ptr(new KeyframeModel(m->m_model, m->m_index, m->m_undoStack)); m2->parseAnimProperty(m->getAnimProperty()); return test_model_equality(m, m2); } TEST_CASE("Keyframe model", "[KeyframeModel]") { std::shared_ptr undoStack = std::make_shared(nullptr); std::shared_ptr guideModel = std::make_shared(undoStack); // Here we do some trickery to enable testing. // We mock the project class so that the undoStack function returns our undoStack Mock pmMock; When(Method(pmMock, undoStack)).AlwaysReturn(undoStack); ProjectManager &mocked = pmMock.get(); pCore->m_projectManager = &mocked; Mlt::Profile pr; std::shared_ptr producer = std::make_shared(pr, "color", "red"); auto effectstack = EffectStackModel::construct(producer, {ObjectType::TimelineClip, 0}, undoStack); effectstack->appendEffect(QStringLiteral("audiobalance")); REQUIRE(effectstack->checkConsistency()); REQUIRE(effectstack->rowCount() == 1); auto effect = std::dynamic_pointer_cast(effectstack->getEffectStackRow(0)); effect->prepareKeyframes(); qDebug() << effect->getAssetId() << effect->getAllParameters(); REQUIRE(effect->rowCount() == 1); QModelIndex index = effect->index(0, 0); auto model = std::shared_ptr(new KeyframeModel(effect, index, undoStack)); SECTION("Add/remove + undo") { auto state0 = [&]() { REQUIRE(model->rowCount() == 1); REQUIRE(check_anim_identity(model)); }; state0(); REQUIRE(model->addKeyframe(GenTime(1.1), KeyframeType::Linear, 42)); auto state1 = [&]() { REQUIRE(model->rowCount() == 2); REQUIRE(check_anim_identity(model)); REQUIRE(model->hasKeyframe(GenTime(1.1))); bool ok; auto k = model->getKeyframe(GenTime(1.1), &ok); REQUIRE(ok); auto k0 = model->getKeyframe(GenTime(0), &ok); REQUIRE(ok); auto k1 = model->getClosestKeyframe(GenTime(0.655555), &ok); REQUIRE(ok); REQUIRE(k1 == k); auto k2 = model->getNextKeyframe(GenTime(0.5), &ok); REQUIRE(ok); REQUIRE(k2 == k); auto k3 = model->getPrevKeyframe(GenTime(0.5), &ok); REQUIRE(ok); REQUIRE(k3 == k0); auto k4 = model->getPrevKeyframe(GenTime(10), &ok); REQUIRE(ok); REQUIRE(k4 == k); model->getNextKeyframe(GenTime(10), &ok); REQUIRE_FALSE(ok); }; state1(); undoStack->undo(); state0(); undoStack->redo(); state1(); REQUIRE(model->addKeyframe(GenTime(12.6), KeyframeType::Discrete, 33)); auto state2 = [&]() { REQUIRE(model->rowCount() == 3); REQUIRE(check_anim_identity(model)); REQUIRE(model->hasKeyframe(GenTime(1.1))); REQUIRE(model->hasKeyframe(GenTime(12.6))); bool ok; auto k = model->getKeyframe(GenTime(1.1), &ok); REQUIRE(ok); auto k0 = model->getKeyframe(GenTime(0), &ok); REQUIRE(ok); auto kk = model->getKeyframe(GenTime(12.6), &ok); REQUIRE(ok); auto k1 = model->getClosestKeyframe(GenTime(0.655555), &ok); REQUIRE(ok); REQUIRE(k1 == k); auto k2 = model->getNextKeyframe(GenTime(0.5), &ok); REQUIRE(ok); REQUIRE(k2 == k); auto k3 = model->getPrevKeyframe(GenTime(0.5), &ok); REQUIRE(ok); REQUIRE(k3 == k0); auto k4 = model->getPrevKeyframe(GenTime(10), &ok); REQUIRE(ok); REQUIRE(k4 == k); auto k5 = model->getNextKeyframe(GenTime(10), &ok); REQUIRE(ok); REQUIRE(k5 == kk); }; state2(); undoStack->undo(); state1(); undoStack->undo(); state0(); undoStack->redo(); state1(); undoStack->redo(); state2(); REQUIRE(model->removeKeyframe(GenTime(1.1))); auto state3 = [&]() { REQUIRE(model->rowCount() == 2); REQUIRE(check_anim_identity(model)); REQUIRE(model->hasKeyframe(GenTime(12.6))); bool ok; model->getKeyframe(GenTime(1.1), &ok); REQUIRE_FALSE(ok); auto k0 = model->getKeyframe(GenTime(0), &ok); REQUIRE(ok); auto kk = model->getKeyframe(GenTime(12.6), &ok); REQUIRE(ok); auto k1 = model->getClosestKeyframe(GenTime(0.655555), &ok); REQUIRE(ok); REQUIRE(k1 == k0); auto k2 = model->getNextKeyframe(GenTime(0.5), &ok); REQUIRE(ok); REQUIRE(k2 == kk); auto k3 = model->getPrevKeyframe(GenTime(0.5), &ok); REQUIRE(ok); REQUIRE(k3 == k0); auto k4 = model->getPrevKeyframe(GenTime(10), &ok); REQUIRE(ok); REQUIRE(k4 == k0); auto k5 = model->getNextKeyframe(GenTime(10), &ok); REQUIRE(ok); REQUIRE(k5 == kk); }; state3(); undoStack->undo(); state2(); undoStack->undo(); state1(); undoStack->undo(); state0(); undoStack->redo(); state1(); undoStack->redo(); state2(); undoStack->redo(); state3(); REQUIRE(model->removeAllKeyframes()); state0(); undoStack->undo(); state3(); undoStack->redo(); state0(); } SECTION("Move keyframes + undo") { auto state0 = [&]() { REQUIRE(model->rowCount() == 1); REQUIRE(check_anim_identity(model)); }; state0(); REQUIRE(model->addKeyframe(GenTime(1.1), KeyframeType::Linear, 42)); auto state1 = [&](double pos) { REQUIRE(model->rowCount() == 2); REQUIRE(check_anim_identity(model)); REQUIRE(model->hasKeyframe(GenTime(pos))); bool ok; auto k = model->getKeyframe(GenTime(pos), &ok); REQUIRE(ok); auto k0 = model->getKeyframe(GenTime(0), &ok); REQUIRE(ok); auto k1 = model->getClosestKeyframe(GenTime(pos + 10), &ok); REQUIRE(ok); REQUIRE(k1 == k); auto k2 = model->getNextKeyframe(GenTime(pos - 0.3), &ok); REQUIRE(ok); REQUIRE(k2 == k); auto k3 = model->getPrevKeyframe(GenTime(pos - 0.3), &ok); REQUIRE(ok); REQUIRE(k3 == k0); auto k4 = model->getPrevKeyframe(GenTime(pos + 0.3), &ok); REQUIRE(ok); REQUIRE(k4 == k); model->getNextKeyframe(GenTime(pos + 0.3), &ok); REQUIRE_FALSE(ok); }; state1(1.1); REQUIRE(model->moveKeyframe(GenTime(1.1), GenTime(2.6), -1, true)); state1(2.6); undoStack->undo(); state1(1.1); undoStack->redo(); state1(2.6); REQUIRE(model->moveKeyframe(GenTime(2.6), GenTime(6.1), -1, true)); state1(6.1); undoStack->undo(); state1(2.6); undoStack->undo(); state1(1.1); undoStack->redo(); state1(2.6); undoStack->redo(); state1(6.1); REQUIRE(model->addKeyframe(GenTime(12.6), KeyframeType::Discrete, 33)); REQUIRE_FALSE(model->moveKeyframe(GenTime(6.1), GenTime(12.6), -1, true)); undoStack->undo(); state1(6.1); } pCore->m_projectManager = nullptr; } diff --git a/tests/markertest.cpp b/tests/markertest.cpp index 8219a78b7..75d96f847 100644 --- a/tests/markertest.cpp +++ b/tests/markertest.cpp @@ -1,193 +1,193 @@ #include "catch.hpp" #pragma GCC diagnostic ignored "-Wnon-virtual-dtor" #pragma GCC diagnostic push #include "fakeit.hpp" #include #include #include #include #include #include #include #define private public #define protected public #include "bin/model/markerlistmodel.hpp" #include "core.h" #include "doc/docundostack.hpp" #include "gentime.h" #include "project/projectmanager.h" #include "timeline2/model/snapmodel.hpp" using namespace fakeit; using Marker = std::tuple; double fps; -void checkMarkerList(std::shared_ptr model, const std::vector &l, std::shared_ptr snaps) +void checkMarkerList(const std::shared_ptr &model, const std::vector &l, const std::shared_ptr &snaps) { auto list = l; std::sort(list.begin(), list.end(), [](const Marker &a, const Marker &b) { return std::get<0>(a) < std::get<0>(b); }); REQUIRE(model->rowCount() == (int)list.size()); if (model->rowCount() == 0) { REQUIRE(snaps->getClosestPoint(0) == -1); } for (int i = 0; i < model->rowCount(); ++i) { REQUIRE(qAbs(std::get<0>(list[i]).seconds() - model->data(model->index(i), MarkerListModel::PosRole).toDouble()) < 0.9 / fps); REQUIRE(std::get<1>(list[i]) == model->data(model->index(i), MarkerListModel::CommentRole).toString()); REQUIRE(std::get<2>(list[i]) == model->data(model->index(i), MarkerListModel::TypeRole).toInt()); REQUIRE(MarkerListModel::markerTypes[std::get<2>(list[i])] == model->data(model->index(i), MarkerListModel::ColorRole).value()); // check for marker existence int frame = std::get<0>(list[i]).frames(fps); REQUIRE(model->hasMarker(frame)); // cheap way to check for snap REQUIRE(snaps->getClosestPoint(frame) == frame); } } -void checkStates(std::shared_ptr undoStack, std::shared_ptr model, const std::vector> &states, - std::shared_ptr snaps) +void checkStates(const std::shared_ptr &undoStack, const std::shared_ptr &model, const std::vector> &states, + const std::shared_ptr &snaps) { for (size_t i = 0; i < states.size(); ++i) { checkMarkerList(model, states[states.size() - 1 - i], snaps); if (i < states.size() - 1) { undoStack->undo(); } } for (size_t i = 1; i < states.size(); ++i) { undoStack->redo(); checkMarkerList(model, states[i], snaps); } } TEST_CASE("Marker model", "[MarkerListModel]") { fps = pCore->getCurrentFps(); GenTime::setFps(fps); std::shared_ptr undoStack = std::make_shared(nullptr); std::shared_ptr model = std::make_shared(undoStack, nullptr); std::shared_ptr snaps = std::make_shared(); model->registerSnapModel(snaps); // Here we do some trickery to enable testing. // We mock the project class so that the getGuideModel function returns this model Mock pmMock; When(Method(pmMock, getGuideModel)).AlwaysReturn(model); ProjectManager &mocked = pmMock.get(); pCore->m_projectManager = &mocked; SECTION("Basic Manipulation") { std::vector list; checkMarkerList(model, list, snaps); // add markers list.push_back(Marker(GenTime(1.3), QLatin1String("test marker"), 3)); model->addMarker(GenTime(1.3), QLatin1String("test marker"), 3); checkMarkerList(model, list, snaps); auto state1 = list; checkStates(undoStack, model, {{}, state1}, snaps); list.push_back(Marker(GenTime(0.3), QLatin1String("test marker2"), 0)); model->addMarker(GenTime(0.3), QLatin1String("test marker2"), 0); checkMarkerList(model, list, snaps); auto state2 = list; checkStates(undoStack, model, {{}, state1, state2}, snaps); // rename markers std::get<1>(list[0]) = QLatin1String("new comment"); std::get<2>(list[0]) = 1; model->addMarker(GenTime(1.3), QLatin1String("new comment"), 1); checkMarkerList(model, list, snaps); auto state3 = list; checkStates(undoStack, model, {{}, state1, state2, state3}, snaps); // delete markers std::swap(list[0], list[1]); list.pop_back(); model->removeMarker(GenTime(1.3)); checkMarkerList(model, list, snaps); auto state4 = list; checkStates(undoStack, model, {{}, state1, state2, state3, state4}, snaps); list.pop_back(); model->removeMarker(GenTime(0.3)); checkMarkerList(model, list, snaps); auto state5 = list; checkStates(undoStack, model, {{}, state1, state2, state3, state4, state5}, snaps); } SECTION("Json identity test") { std::vector list; checkMarkerList(model, list, snaps); // add markers list.push_back(Marker(GenTime(1.3), QLatin1String("test marker"), 3)); model->addMarker(GenTime(1.3), QLatin1String("test marker"), 3); list.push_back(Marker(GenTime(0.3), QLatin1String("test marker2"), 0)); model->addMarker(GenTime(0.3), QLatin1String("test marker2"), 0); list.push_back(Marker(GenTime(3), QLatin1String("test marker3"), 0)); model->addMarker(GenTime(3), QLatin1String("test marker3"), 0); checkMarkerList(model, list, snaps); // export QString json = model->toJson(); // clean model->removeMarker(GenTime(0.3)); model->removeMarker(GenTime(3)); model->removeMarker(GenTime(1.3)); checkMarkerList(model, {}, snaps); // Reimport REQUIRE(model->importFromJson(json, false)); checkMarkerList(model, list, snaps); // undo/redo undoStack->undo(); checkMarkerList(model, {}, snaps); undoStack->redo(); checkMarkerList(model, list, snaps); // now we try the same thing with non-empty model undoStack->undo(); checkMarkerList(model, {}, snaps); // non - conflicting marker list.push_back(Marker(GenTime(5), QLatin1String("non conflicting"), 0)); std::vector otherMarkers; otherMarkers.push_back(Marker(GenTime(5), QLatin1String("non conflicting"), 0)); model->addMarker(GenTime(5), QLatin1String("non conflicting"), 0); REQUIRE(model->importFromJson(json, false)); checkMarkerList(model, list, snaps); undoStack->undo(); checkMarkerList(model, otherMarkers, snaps); undoStack->redo(); checkMarkerList(model, list, snaps); undoStack->undo(); // conflicting marker otherMarkers.push_back(Marker(GenTime(1.3), QLatin1String("conflicting"), 1)); model->addMarker(GenTime(1.3), QLatin1String("conflicting"), 1); checkMarkerList(model, otherMarkers, snaps); REQUIRE_FALSE(model->importFromJson(json, false)); checkMarkerList(model, otherMarkers, snaps); REQUIRE(model->importFromJson(json, true)); checkMarkerList(model, list, snaps); undoStack->undo(); checkMarkerList(model, otherMarkers, snaps); undoStack->redo(); checkMarkerList(model, list, snaps); } pCore->m_projectManager = nullptr; }