diff --git a/fuzzer/fuzzing.cpp b/fuzzer/fuzzing.cpp index 6da6b9c1b..089039001 100644 --- a/fuzzer/fuzzing.cpp +++ b/fuzzer/fuzzing.cpp @@ -1,423 +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); + arguments.emplace_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); + arguments.emplace_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)); + arguments.emplace_back(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); + arguments.emplace_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); + arguments.emplace_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); + arguments.emplace_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); + arguments.emplace_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)); + arguments.emplace_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); + arguments.emplace_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(); + for (auto &all_timeline : all_timelines) { + all_timeline.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 d9b568a28..576d238b4 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 +#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) { 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); + auto *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/renderer/renderjob.cpp b/renderer/renderjob.cpp index e23985242..45b71c123 100644 --- a/renderer/renderjob.cpp +++ b/renderer/renderjob.cpp @@ -1,312 +1,312 @@ /*************************************************************************** * 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 "renderjob.h" #include #include #include #include - +#include // Can't believe I need to do this to sleep. class SleepThread : QThread { public: void run() override {} static void msleep(unsigned long msecs) { QThread::msleep(msecs); } }; RenderJob::RenderJob(const QString &render, const QString &scenelist, const QString &target, int pid, int in, int out) : QObject() , m_scenelist(scenelist) - , m_dest(target) + , m_dest(std::move(target)) , m_progress(0) - , m_prog(render) + , m_prog(std::move(render)) , m_player() , m_jobUiserver(nullptr) , m_kdenliveinterface(nullptr) , m_usekuiserver(true) , m_logfile(scenelist + QStringLiteral(".txt")) , m_erase(scenelist.startsWith(QDir::tempPath())) , m_seconds(0) , m_frame(0) , m_pid(pid) , m_dualpass(false) { m_renderProcess = new QProcess; m_renderProcess->setReadChannel(QProcess::StandardError); connect(m_renderProcess, &QProcess::stateChanged, this, &RenderJob::slotCheckProcess); // Disable VDPAU so that rendering will work even if there is a Kdenlive instance using VDPAU qputenv("MLT_NO_VDPAU", "1"); m_args << "-progress" << scenelist; if (in != -1) { m_args << QStringLiteral("in=") + QString::number(in); } if (out != -1) { m_args << QStringLiteral("out=") + QString::number(out); } // Create a log of every render process. if (!m_logfile.open(QIODevice::WriteOnly | QIODevice::Text)) { qWarning() << "Unable to log to " << m_logfile.fileName(); } else { m_logstream.setDevice(&m_logfile); } } RenderJob::~RenderJob() { delete m_renderProcess; m_logfile.close(); } void RenderJob::setLocale(const QString &locale) { qputenv("LC_NUMERIC", locale.toUtf8().constData()); } void RenderJob::slotAbort(const QString &url) { if (m_dest == url) { slotAbort(); } } void RenderJob::slotAbort() { qWarning() << "Job aborted by user..."; m_renderProcess->kill(); if (m_kdenliveinterface) { m_dbusargs[1] = -3; m_dbusargs.append(QString()); m_kdenliveinterface->callWithArgumentList(QDBus::NoBlock, QStringLiteral("setRenderingFinished"), m_dbusargs); } if (m_jobUiserver) { m_jobUiserver->call(QStringLiteral("terminate"), QString()); } if (m_erase) { QFile(m_scenelist).remove(); } QFile(m_dest).remove(); m_logstream << "Job aborted by user" << endl; m_logstream.flush(); m_logfile.close(); qApp->quit(); } void RenderJob::receivedStderr() { QString result = QString::fromLocal8Bit(m_renderProcess->readAllStandardError()).simplified(); if (!result.startsWith(QLatin1String("Current Frame"))) { m_errorMessage.append(result + QStringLiteral("
")); } else { m_logstream << "melt: " << result << endl; int pro = result.section(QLatin1Char(' '), -1).toInt(); if (pro <= m_progress || pro <= 0 || pro > 100) { return; } m_progress = pro; if (m_args.contains(QStringLiteral("pass=1"))) { m_progress /= 2.0; } else if (m_args.contains(QStringLiteral("pass=2"))) { m_progress = 50 + m_progress / 2.0; } int frame = result.section(QLatin1Char(','), 1).section(QLatin1Char(' '), -1).toInt(); if ((m_kdenliveinterface != nullptr) && m_kdenliveinterface->isValid()) { m_dbusargs[1] = m_progress; m_kdenliveinterface->callWithArgumentList(QDBus::NoBlock, QStringLiteral("setRenderingProgress"), m_dbusargs); } if (m_jobUiserver) { m_jobUiserver->call(QStringLiteral("setPercent"), (uint)m_progress); int seconds = m_startTime.secsTo(QTime::currentTime()); if (seconds < 0) { // 1 day offset, add seconds in a day seconds += 86400; } seconds = (int)(seconds * (100 - m_progress) / m_progress); if (seconds == m_seconds) { return; } m_jobUiserver->call(QStringLiteral("setDescriptionField"), (uint)0, QString(), tr("Remaining time: ") + QTime(0, 0, 0).addSecs(seconds).toString(QStringLiteral("hh:mm:ss"))); // m_jobUiserver->call(QStringLiteral("setSpeed"), (frame - m_frame) / (seconds - m_seconds)); // m_jobUiserver->call("setSpeed", (frame - m_frame) / (seconds - m_seconds)); m_frame = frame; m_seconds = seconds; } } } void RenderJob::start() { QDBusConnectionInterface *interface = QDBusConnection::sessionBus().interface(); if ((interface != nullptr) && m_usekuiserver) { if (!interface->isServiceRegistered(QStringLiteral("org.kde.JobViewServer"))) { qWarning() << "No org.kde.JobViewServer registered, trying to start kuiserver"; if (QProcess::startDetached(QStringLiteral("kuiserver"))) { // Give it a couple of seconds to start QTime t; t.start(); while (!interface->isServiceRegistered(QStringLiteral("org.kde.JobViewServer")) && t.elapsed() < 3000) { SleepThread::msleep(100); // Sleep 100 ms } } else { qWarning() << "Failed to start kuiserver"; } } if (interface->isServiceRegistered(QStringLiteral("org.kde.JobViewServer"))) { QDBusInterface kuiserver(QStringLiteral("org.kde.JobViewServer"), QStringLiteral("/JobViewServer"), QStringLiteral("org.kde.JobViewServer")); QDBusReply objectPath = kuiserver.asyncCall(QStringLiteral("requestView"), QLatin1String("kdenlive"), QLatin1String("kdenlive"), 0x0001); QString reply = ((QDBusObjectPath)objectPath).path(); // Use of the KDE JobViewServer is an ugly hack, it is not reliable QString dbusView = QStringLiteral("org.kde.JobViewV2"); m_jobUiserver = new QDBusInterface(QStringLiteral("org.kde.JobViewServer"), reply, dbusView); if ((m_jobUiserver != nullptr) && m_jobUiserver->isValid()) { m_startTime = QTime::currentTime(); if (!m_args.contains(QStringLiteral("pass=2"))) { m_jobUiserver->call(QStringLiteral("setPercent"), (uint)0); } m_jobUiserver->call(QStringLiteral("setInfoMessage"), tr("Rendering %1").arg(QFileInfo(m_dest).fileName())); QDBusConnection::sessionBus().connect(QStringLiteral("org.kde.JobViewServer"), reply, dbusView, QStringLiteral("cancelRequested"), this, SLOT(slotAbort())); } } } if (m_pid > -1) { initKdenliveDbusInterface(); } // Make sure the destination directory is writable /*QFileInfo checkDestination(QFileInfo(m_dest).absolutePath()); if (!checkDestination.isWritable()) { slotIsOver(QProcess::NormalExit, false); }*/ // Because of the logging, we connect to stderr in all cases. connect(m_renderProcess, &QProcess::readyReadStandardError, this, &RenderJob::receivedStderr); m_renderProcess->start(m_prog, m_args); qDebug() << "Started render process: " << m_prog << ' ' << m_args.join(QLatin1Char(' ')); m_logstream << "Started render process: " << m_prog << ' ' << m_args.join(QLatin1Char(' ')) << endl; } void RenderJob::initKdenliveDbusInterface() { QString kdenliveId; QDBusConnection connection = QDBusConnection::sessionBus(); QDBusConnectionInterface *ibus = connection.interface(); kdenliveId = QStringLiteral("org.kde.kdenlive-%1").arg(m_pid); if (!ibus->isServiceRegistered(kdenliveId)) { kdenliveId.clear(); const QStringList services = ibus->registeredServiceNames(); for (const QString &service : services) { if (!service.startsWith(QLatin1String("org.kde.kdenlive"))) { continue; } kdenliveId = service; break; } } m_dbusargs.clear(); if (kdenliveId.isEmpty()) { return; } m_kdenliveinterface = new QDBusInterface(kdenliveId, QStringLiteral("/kdenlive/MainWindow_1"), QStringLiteral("org.kde.kdenlive.rendering"), connection, this); if (m_kdenliveinterface) { m_dbusargs.append(m_dest); m_dbusargs.append((int)0); if (!m_args.contains(QStringLiteral("pass=2"))) { m_kdenliveinterface->callWithArgumentList(QDBus::NoBlock, QStringLiteral("setRenderingProgress"), m_dbusargs); } connect(m_kdenliveinterface, SIGNAL(abortRenderJob(QString)), this, SLOT(slotAbort(QString))); } } void RenderJob::slotCheckProcess(QProcess::ProcessState state) { if (state == QProcess::NotRunning) { slotIsOver(m_renderProcess->exitStatus()); } } void RenderJob::slotIsOver(QProcess::ExitStatus status, bool isWritable) { if (m_jobUiserver) { m_jobUiserver->call(QStringLiteral("setDescriptionField"), (uint)1, tr("Rendered file"), m_dest); // m_jobUiserver->call(QStringLiteral("terminate"), QString()); } if (!isWritable) { QString error = tr("Cannot write to %1, check permissions.").arg(m_dest); if (m_kdenliveinterface) { m_dbusargs[1] = (int)-2; m_dbusargs.append(error); m_kdenliveinterface->callWithArgumentList(QDBus::NoBlock, QStringLiteral("setRenderingFinished"), m_dbusargs); } QProcess::startDetached(QStringLiteral("kdialog"), QStringList() << QStringLiteral("--error") << error); m_logstream << error << endl; qApp->quit(); } if (m_erase) { QFile(m_scenelist).remove(); } if (status == QProcess::CrashExit || m_renderProcess->error() != QProcess::UnknownError || m_renderProcess->exitCode() != 0) { // rendering crashed if (m_kdenliveinterface) { m_dbusargs[1] = (int)-2; m_dbusargs.append(m_errorMessage); m_kdenliveinterface->callWithArgumentList(QDBus::NoBlock, QStringLiteral("setRenderingFinished"), m_dbusargs); } QStringList args; QString error = tr("Rendering of %1 aborted, resulting video will probably be corrupted.").arg(m_dest); args << QStringLiteral("--error") << error; m_logstream << error << endl; QProcess::startDetached(QStringLiteral("kdialog"), args); qApp->quit(); } else { if (!m_dualpass && (m_kdenliveinterface != nullptr)) { m_dbusargs[1] = (int)-1; m_dbusargs.append(QString()); m_kdenliveinterface->callWithArgumentList(QDBus::NoBlock, QStringLiteral("setRenderingFinished"), m_dbusargs); } m_logstream << "Rendering of " << m_dest << " finished" << endl; if (!m_dualpass && m_player.length() > 3 && m_player.contains(QLatin1Char(' '))) { QStringList args = m_player.split(QLatin1Char(' ')); QString exec = args.takeFirst(); // Decode url QString url = QUrl::fromEncoded(args.takeLast().toUtf8()).toLocalFile(); args << url; QProcess::startDetached(exec, args); } m_logstream.flush(); if (m_dualpass) { emit renderingFinished(); deleteLater(); } else { m_logfile.remove(); qApp->quit(); } } } diff --git a/src/abstractmodel/abstracttreemodel.cpp b/src/abstractmodel/abstracttreemodel.cpp index 299c00b95..df060e1a1 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(); + return {}; } QModelIndex AbstractTreeModel::parent(const QModelIndex &index) const { - if (!index.isValid()) return QModelIndex(); + if (!index.isValid()) return {}; 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(); + return {}; } 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(); + return {}; } 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(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(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 24fec46ed..7f66e4706 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(); + ~AbstractTreeModel() override; /* @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(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 b0d97f9f2..51d6b63fe 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) +TreeItem::TreeItem(QList data, const std::shared_ptr &model, bool isRoot, int id) + : m_itemData(std::move(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, 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(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, 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) { 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(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 889c3fe13..1f86eaa3e 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); + explicit TreeItem(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(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); /* @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(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/abstractassetsrepository.hpp b/src/assets/abstractassetsrepository.hpp index 397701586..87d3c53a0 100644 --- a/src/assets/abstractassetsrepository.hpp +++ b/src/assets/abstractassetsrepository.hpp @@ -1,121 +1,122 @@ /*************************************************************************** * 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 ASSETSREPOSITORY_H #define ASSETSREPOSITORY_H #include "definitions.h" #include #include #include #include #include #include /** @brief This class is the base class for assets (transitions or effets) repositories */ template class AbstractAssetsRepository { public: AbstractAssetsRepository(); - virtual ~AbstractAssetsRepository(){}; + virtual ~AbstractAssetsRepository() = default; + ; /* @brief Returns true if a given asset exists */ bool exists(const QString &assetId) const; /* @brief Returns a vector of pair (asset id, asset name) */ QVector> getNames() const; /* @brief Return type of asset */ AssetType getType(const QString &assetId) const; /* @brief Return name of asset */ Q_INVOKABLE QString getName(const QString &assetId) const; /* @brief Return description of asset */ QString getDescription(const QString &assetId) const; /* @brief Set an asset as favorite (or not)*/ virtual void setFavorite(const QString &assetId, bool favorite) = 0; /* @brief Returns a DomElement representing the asset's properties */ QDomElement getXml(const QString &assetId) const; protected: struct Info { QString id; // identifier of the asset QString mltId; //"tag" of the asset, that is the name of the mlt service QString name, description, author, version_str; int version; QDomElement xml; AssetType type; }; // Reads the blacklist file and populate appropriate structure void parseBlackList(const QString &path); void init(); virtual Mlt::Properties *retrieveListFromMlt() const = 0; virtual void parseFavorites() = 0; /* @brief Parse some info from a mlt structure @param res Datastructure to fill @return true on success */ bool parseInfoFromMlt(const QString &effectId, Info &res); /* @brief Returns the metadata associated with the given asset*/ virtual Mlt::Properties *getMetadata(const QString &assetId) = 0; /* @brief Parse one asset from its XML content @param res data structure to fill @return true of success */ bool parseInfoFromXml(const QDomElement ¤tAsset, Info &res) const; /* @brief Figure what is the type of the asset based on its metadata and store it in res*/ virtual void parseType(QScopedPointer &metadata, Info &res) = 0; /* @brief Retrieves additional info about asset from a custom XML file The resulting assets are stored in customAssets */ virtual void parseCustomAssetFile(const QString &file_name, std::unordered_map &customAssets) const = 0; /* @brief Returns the path to custom XML description of the assets*/ virtual QStringList assetDirs() const = 0; /* @brief Returns the path to the assets' blacklist*/ virtual QString assetBlackListPath() const = 0; std::unordered_map m_assets; QSet m_blacklist; QSet m_favorites; }; #include "abstractassetsrepository.ipp" #endif diff --git a/src/assets/abstractassetsrepository.ipp b/src/assets/abstractassetsrepository.ipp index 50f16d7fb..bcde738a6 100644 --- a/src/assets/abstractassetsrepository.ipp +++ b/src/assets/abstractassetsrepository.ipp @@ -1,315 +1,315 @@ /*************************************************************************** * 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 "xml/xml.hpp" #include #include #include #include #include #include #ifdef Q_OS_MAC #include #endif -template AbstractAssetsRepository::AbstractAssetsRepository() {} +template AbstractAssetsRepository::AbstractAssetsRepository() = default; template void AbstractAssetsRepository::init() { // Warning: Mlt::Factory::init() resets the locale to the default system value, make sure we keep correct locale #ifndef Q_OS_MAC setlocale(LC_NUMERIC, nullptr); #else setlocale(LC_NUMERIC_MASK, nullptr); #endif // Parse effects blacklist parseBlackList(assetBlackListPath()); parseFavorites(); // Retrieve the list of MLT's available assets. QScopedPointer assets(retrieveListFromMlt()); int max = assets->count(); QString sox = QStringLiteral("sox."); for (int i = 0; i < max; ++i) { Info info; QString name = assets->get_name(i); info.id = name; if (name.startsWith(sox)) { // sox effects are not usage directly (parameters not available) continue; } // qDebug() << "trying to parse " < customAssets; for (const auto &dir : asset_dirs) { QDir current_dir(dir); QStringList filter; filter << QStringLiteral("*.xml"); QStringList fileList = current_dir.entryList(filter, QDir::Files); for (const auto &file : fileList) { QString path = current_dir.absoluteFilePath(file); parseCustomAssetFile(path, customAssets); } } // We add the custom assets for (const auto &custom : customAssets) { // Custom assets should override default ones m_assets[custom.first] = custom.second; /*if (m_assets.count(custom.second.mltId) > 0) { m_assets.erase(custom.second.mltId); } if (m_assets.count(custom.first) == 0) { m_assets[custom.first] = custom.second; } else { qDebug() << "Error: conflicting asset name " << custom.first; }*/ } } template void AbstractAssetsRepository::parseBlackList(const QString &path) { QFile blacklist_file(path); if (blacklist_file.open(QIODevice::ReadOnly)) { QTextStream stream(&blacklist_file); QString line; while (stream.readLineInto(&line)) { line = line.simplified(); if (!line.isEmpty() && !line.startsWith('#')) { m_blacklist.insert(line); } } blacklist_file.close(); } } template bool AbstractAssetsRepository::parseInfoFromMlt(const QString &assetId, Info &res) { QScopedPointer metadata(getMetadata(assetId)); if (metadata && metadata->is_valid()) { if (metadata->get("title") && metadata->get("identifier") && strlen(metadata->get("title")) > 0) { res.name = metadata->get("title"); res.name[0] = res.name[0].toUpper(); res.description = metadata->get("description"); res.author = metadata->get("creator"); res.version_str = metadata->get("version"); res.version = ceil(100 * metadata->get_double("version")); res.id = res.mltId = assetId; parseType(metadata, res); // Create params QDomDocument doc; QDomElement eff = doc.createElement(QStringLiteral("effect")); QString id = metadata->get("identifier"); eff.setAttribute(QStringLiteral("tag"), id); eff.setAttribute(QStringLiteral("id"), id); ////qCDebug(KDENLIVE_LOG)<<"Effect: "<get_data("parameters")); for (int j = 0; param_props.is_valid() && j < param_props.count(); ++j) { QDomElement params = doc.createElement(QStringLiteral("parameter")); Mlt::Properties paramdesc((mlt_properties) param_props.get_data(param_props.get_name(j))); params.setAttribute(QStringLiteral("name"), paramdesc.get("identifier")); if (params.attribute(QStringLiteral("name")) == QLatin1String("argument")) { // This parameter has to be given as attribute when using command line, do not show it in Kdenlive continue; } if (paramdesc.get("readonly") && !strcmp(paramdesc.get("readonly"), "yes")) { // Do not expose readonly parameters continue; } if (paramdesc.get("maximum")) { params.setAttribute(QStringLiteral("max"), paramdesc.get("maximum")); } if (paramdesc.get("minimum")) { params.setAttribute(QStringLiteral("min"), paramdesc.get("minimum")); } QString paramType = paramdesc.get("type"); if (paramType == QLatin1String("integer")) { if (params.attribute(QStringLiteral("min")) == QLatin1String("0") && params.attribute(QStringLiteral("max")) == QLatin1String("1")) { params.setAttribute(QStringLiteral("type"), QStringLiteral("bool")); } else { params.setAttribute(QStringLiteral("type"), QStringLiteral("constant")); } } else if (paramType == QLatin1String("float")) { params.setAttribute(QStringLiteral("type"), QStringLiteral("constant")); // param type is float, set default decimals to 3 params.setAttribute(QStringLiteral("decimals"), QStringLiteral("3")); } else if (paramType == QLatin1String("boolean")) { params.setAttribute(QStringLiteral("type"), QStringLiteral("bool")); } else if (paramType == QLatin1String("geometry")) { params.setAttribute(QStringLiteral("type"), QStringLiteral("geometry")); } else if (paramType == QLatin1String("string")) { // string parameter are not really supported, so if we have a default value, enforce it params.setAttribute(QStringLiteral("type"), QStringLiteral("fixed")); if (paramdesc.get("default")) { QString stringDefault = paramdesc.get("default"); stringDefault.remove(QLatin1Char('\'')); params.setAttribute(QStringLiteral("value"), stringDefault); } else { // String parameter without default, skip it completely continue; } } else { params.setAttribute(QStringLiteral("type"), paramType); if (!QString(paramdesc.get("format")).isEmpty()) { params.setAttribute(QStringLiteral("format"), paramdesc.get("format")); } } if (!params.hasAttribute(QStringLiteral("value"))) { if (paramdesc.get("default")) { params.setAttribute(QStringLiteral("default"), paramdesc.get("default")); } if (paramdesc.get("value")) { params.setAttribute(QStringLiteral("value"), paramdesc.get("value")); } else { params.setAttribute(QStringLiteral("value"), paramdesc.get("default")); } } QString paramName = paramdesc.get("title"); if (!paramName.isEmpty()) { QDomElement pname = doc.createElement(QStringLiteral("name")); pname.appendChild(doc.createTextNode(paramName)); params.appendChild(pname); } if (paramdesc.get("description")) { QDomElement comment = doc.createElement(QStringLiteral("comment")); comment.appendChild(doc.createTextNode(paramdesc.get("description"))); params.appendChild(comment); } eff.appendChild(params); } doc.appendChild(eff); res.xml = eff; return true; } } return false; } template bool AbstractAssetsRepository::exists(const QString &assetId) const { return m_assets.count(assetId) > 0; } template QVector> AbstractAssetsRepository::getNames() const { QVector> res; res.reserve((int)m_assets.size()); for (const auto &asset : m_assets) { res.push_back({asset.first, asset.second.name}); } std::sort(res.begin(), res.end(), [](const QPair &a, const QPair &b) { return a.second < b.second; }); return res; } template AssetType AbstractAssetsRepository::getType(const QString &assetId) const { Q_ASSERT(m_assets.count(assetId) > 0); return m_assets.at(assetId).type; } template QString AbstractAssetsRepository::getName(const QString &assetId) const { Q_ASSERT(m_assets.count(assetId) > 0); return m_assets.at(assetId).name; } template QString AbstractAssetsRepository::getDescription(const QString &assetId) const { Q_ASSERT(m_assets.count(assetId) > 0); return m_assets.at(assetId).description; } template bool AbstractAssetsRepository::parseInfoFromXml(const QDomElement ¤tAsset, Info &res) const { QString tag = currentAsset.attribute(QStringLiteral("tag"), QString()); QString id = currentAsset.attribute(QStringLiteral("id"), QString()); if (id.isEmpty()) { id = tag; } if (!exists(tag)) { qDebug() << "++++++ Unknown asset : " << tag; return false; } // Check if there is a maximal version set if (currentAsset.hasAttribute(QStringLiteral("version"))) { // a specific version of the filter is required if (m_assets.at(tag).version < (int)(100 * currentAsset.attribute(QStringLiteral("version")).toDouble())) { return false; } } res = m_assets.at(tag); res.id = id; res.mltId = tag; // Update description if the xml provide one QString description = Xml::getSubTagContent(currentAsset, QStringLiteral("description")); if (!description.isEmpty()) { res.description = description; } // Update name if the xml provide one QString name = Xml::getSubTagContent(currentAsset, QStringLiteral("name")); if (!name.isEmpty()) { res.name = name; } return true; } template QDomElement AbstractAssetsRepository::getXml(const QString &assetId) const { if (m_assets.count(assetId) == 0) { qDebug() << "Error : Requesting info on unknown transition " << assetId; return QDomElement(); } return m_assets.at(assetId).xml.cloneNode().toElement(); } diff --git a/src/assets/assetlist/model/assetfilter.cpp b/src/assets/assetlist/model/assetfilter.cpp index 86a4341fa..52bb300a7 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(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/assetlist/model/assetfilter.hpp b/src/assets/assetlist/model/assetfilter.hpp index 11172e5a4..9c556a202 100644 --- a/src/assets/assetlist/model/assetfilter.hpp +++ b/src/assets/assetlist/model/assetfilter.hpp @@ -1,70 +1,70 @@ /*************************************************************************** * 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 ASSETFILTER_H #define ASSETFILTER_H #include #include /* @brief This class is used as a proxy model to filter an asset list based on given criterion (name, ...) */ class TreeItem; class AssetFilter : public QSortFilterProxyModel { Q_OBJECT public: AssetFilter(QObject *parent = nullptr); /* @brief Manage the name filter @param enabled whether to enable this filter @param pattern to match against effects' names */ void setFilterName(bool enabled, const QString &pattern); /** @brief Returns true if the ModelIndex in the source model is visible after filtering */ bool isVisible(const QModelIndex &sourceIndex); /** @brief If we are in favorite view, invalidate filter to refresh. Call this after a favorite has changed */ virtual void reloadFilterOnFavorite() = 0; QVariantList getCategories(); Q_INVOKABLE QModelIndex getNextChild(const QModelIndex ¤t); Q_INVOKABLE QModelIndex getPreviousChild(const QModelIndex ¤t); Q_INVOKABLE QModelIndex firstVisibleItem(const QModelIndex ¤t); Q_INVOKABLE QModelIndex getCategory(int catRow); Q_INVOKABLE QModelIndex getModelIndex(QModelIndex current); Q_INVOKABLE QModelIndex getProxyIndex(QModelIndex current); protected: bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; bool filterName(const std::shared_ptr &item) const; /* @brief Apply all filter and returns true if the object should be kept after filtering */ virtual bool applyAll(std::shared_ptr item) const; - bool m_name_enabled; + bool m_name_enabled{false}; QString m_name_value; }; #endif diff --git a/src/assets/assetlist/model/assettreemodel.hpp b/src/assets/assetlist/model/assettreemodel.hpp index db6af30ca..42a4137c5 100644 --- a/src/assets/assetlist/model/assettreemodel.hpp +++ b/src/assets/assetlist/model/assettreemodel.hpp @@ -1,58 +1,58 @@ /*************************************************************************** * 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 ASSETTREEMODEL_H #define ASSETTREEMODEL_H #include "abstractmodel/abstracttreemodel.hpp" /* @brief This class represents an effect hierarchy to be displayed as a tree */ class TreeItem; class QMenu; class KActionCategory; class AssetTreeModel : public AbstractTreeModel { public: - explicit AssetTreeModel(QObject *parent = 0); + explicit AssetTreeModel(QObject *parent = nullptr); enum { IdRole = Qt::UserRole + 1, NameRole, FavoriteRole }; // Helper function to retrieve name QString getName(const QModelIndex &index) const; // Helper function to retrieve description QString getDescription(const QModelIndex &index) const; // Helper function to retrieve if an effect is categorized as favorite bool isFavorite(const QModelIndex &index) const; void setFavorite(const QModelIndex &index, bool favorite, bool isEffect); QHash roleNames() const override; QVariant data(const QModelIndex &index, int role) const override; virtual void reloadAssetMenu(QMenu *effectsMenu, KActionCategory *effectActions) = 0; // for convenience, we store the column of each data field static int nameCol, idCol, favCol, typeCol; protected: }; #endif diff --git a/src/assets/assetlist/view/assetlistwidget.cpp b/src/assets/assetlist/view/assetlistwidget.cpp index 980c0a152..918bacc90 100644 --- a/src/assets/assetlist/view/assetlistwidget.cpp +++ b/src/assets/assetlist/view/assetlistwidget.cpp @@ -1,109 +1,109 @@ /*************************************************************************** * 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 "assetlistwidget.hpp" #include "assets/assetlist/model/assetfilter.hpp" #include "assets/assetlist/model/assettreemodel.hpp" #include "assets/assetlist/view/qmltypes/asseticonprovider.hpp" #include #include #include #include #include AssetListWidget::AssetListWidget(QWidget *parent) : QQuickWidget(parent) - , m_assetIconProvider(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 } AssetListWidget::~AssetListWidget() { // clear source setSource(QUrl()); } void AssetListWidget::setup() { setResizeMode(QQuickWidget::SizeRootObjectToView); engine()->addImageProvider(QStringLiteral("asseticon"), m_assetIconProvider); setSource(QUrl(QStringLiteral("qrc:/qml/assetList.qml"))); setFocusPolicy(Qt::StrongFocus); } void AssetListWidget::reset() { setSource(QUrl(QStringLiteral("qrc:/qml/assetList.qml"))); } QString AssetListWidget::getName(const QModelIndex &index) const { return m_model->getName(m_proxyModel->mapToSource(index)); } bool AssetListWidget::isFavorite(const QModelIndex &index) const { return m_model->isFavorite(m_proxyModel->mapToSource(index)); } void AssetListWidget::setFavorite(const QModelIndex &index, bool favorite, bool isEffect) { m_model->setFavorite(m_proxyModel->mapToSource(index), favorite, isEffect); } QString AssetListWidget::getDescription(const QModelIndex &index) const { return m_model->getDescription(m_proxyModel->mapToSource(index)); } void AssetListWidget::setFilterName(const QString &pattern) { m_proxyModel->setFilterName(!pattern.isEmpty(), pattern); if (!pattern.isEmpty()) { QVariantList mapped = m_proxyModel->getCategories(); QMetaObject::invokeMethod(rootObject(), "expandNodes", Qt::DirectConnection, Q_ARG(QVariant, mapped)); } } QVariantMap AssetListWidget::getMimeData(const QString &assetId) const { QVariantMap mimeData; mimeData.insert(getMimeType(assetId), assetId); return mimeData; } void AssetListWidget::activate(const QModelIndex &ix) { if (!ix.isValid()) { return; } const QString assetId = m_model->data(m_proxyModel->mapToSource(ix), AssetTreeModel::IdRole).toString(); emit activateAsset(getMimeData(assetId)); } diff --git a/src/assets/assetlist/view/assetlistwidget.hpp b/src/assets/assetlist/view/assetlistwidget.hpp index 61a90039d..463fd1b08 100644 --- a/src/assets/assetlist/view/assetlistwidget.hpp +++ b/src/assets/assetlist/view/assetlistwidget.hpp @@ -1,83 +1,83 @@ /*************************************************************************** * 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 ASSETLISTWIDGET_H #define ASSETLISTWIDGET_H #include "effects/effectsrepository.hpp" #include #include /* @brief This class is a generic widget that display the list of available assets */ class AssetIconProvider; class AssetFilter; class AssetTreeModel; class AssetListWidget : public QQuickWidget { Q_OBJECT /* @brief Should the descriptive info box be displayed */ public: AssetListWidget(QWidget *parent = Q_NULLPTR); - virtual ~AssetListWidget(); + ~AssetListWidget() override; /* @brief Returns the name of the asset given its model index */ QString getName(const QModelIndex &index) const; /* @brief Returns true if this effect belongs to favorites */ bool isFavorite(const QModelIndex &index) const; /* @brief Returns true if this effect belongs to favorites */ void setFavorite(const QModelIndex &index, bool favorite = true, bool isEffect = true); /* @brief Returns the description of the asset given its model index */ QString getDescription(const QModelIndex &index) const; /* @brief Sets the pattern against which the assets' names are filtered */ void setFilterName(const QString &pattern); /*@brief Return mime type used for drag and drop. It can be kdenlive/effect, kdenlive/composition or kdenlive/transition*/ virtual QString getMimeType(const QString &assetId) const = 0; QVariantMap getMimeData(const QString &assetId) const; void activate(const QModelIndex &ix); /* @brief Rebuild the view by resetting the source. Is there a better way? */ void reset(); protected: void setup(); std::shared_ptr m_model; std::unique_ptr m_proxyModel; // the QmlEngine takes ownership of the image provider - AssetIconProvider *m_assetIconProvider; + AssetIconProvider *m_assetIconProvider{nullptr}; signals: void activateAsset(const QVariantMap data); }; #endif diff --git a/src/assets/assetpanel.cpp b/src/assets/assetpanel.cpp index 6ea3f0269..c6ff3ecd3 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); + auto *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); + auto *lay = new QVBoxLayout(m_container); lay->setContentsMargins(0, 0, 0, 0); lay->addWidget(m_transitionWidget); lay->addWidget(m_effectStackWidget); - QScrollArea *sc = new QScrollArea; + auto *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, 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, 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/keyframes/model/corners/cornershelper.cpp b/src/assets/keyframes/model/corners/cornershelper.cpp index 83ec196f4..875b2bccd 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, 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(); + for (const auto &point : points) { + QPointF pt = point.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(); + auto 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/keyframemodellist.cpp b/src/assets/keyframes/model/keyframemodellist.cpp index e8be994f5..b73e275ad 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(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(); + return {}; } void KeyframeModelList::addParameter(const QModelIndex &index) { std::shared_ptr parameter(new KeyframeModel(m_model, index, m_undoStack)); m_parameters.insert({index, std::move(parameter)}); } bool KeyframeModelList::applyOperation(const std::function, Fun &, Fun &)> &op, const QString &undoString) { QWriteLocker locker(&m_lock); Q_ASSERT(m_parameters.size() > 0); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool res = true; for (const auto ¶m : m_parameters) { res = op(param.second, undo, redo); if (!res) { bool undone = undo(); Q_ASSERT(undone); return res; } } if (res && !undoString.isEmpty()) { PUSH_UNDO(undo, redo, undoString); } return res; } bool KeyframeModelList::addKeyframe(GenTime pos, KeyframeType type) { QWriteLocker locker(&m_lock); Q_ASSERT(m_parameters.size() > 0); bool update = (m_parameters.begin()->second->hasKeyframe(pos) > 0); auto op = [pos, type](std::shared_ptr param, Fun &undo, Fun &redo) { QVariant value = param->getInterpolatedValue(pos); return param->addKeyframe(pos, type, value, true, undo, redo); }; return applyOperation(op, update ? i18n("Change keyframe type") : i18n("Add keyframe")); } bool KeyframeModelList::addKeyframe(int frame, double val) { QWriteLocker locker(&m_lock); GenTime pos(frame, pCore->getCurrentFps()); Q_ASSERT(m_parameters.size() > 0); bool update = (m_parameters.begin()->second->hasKeyframe(pos) > 0); bool isRectParam = false; if (m_inTimelineIndex.isValid()) { if (auto ptr = m_model.lock()) { - ParamType tp = ptr->data(m_inTimelineIndex, AssetParameterModel::TypeRole).value(); + auto tp = ptr->data(m_inTimelineIndex, AssetParameterModel::TypeRole).value(); if (tp == ParamType::AnimatedRect) { isRectParam = true; } } } auto op = [this, pos, val, isRectParam](std::shared_ptr param, Fun &undo, Fun &redo) { QVariant value; if (m_inTimelineIndex.isValid()) { if (m_parameters.at(m_inTimelineIndex) == param) { if (isRectParam) { value = param->getInterpolatedValue(pos); value = param->updateInterpolated(value, val); } else { value = param->getNormalizedValue(val); } } else { value = param->getInterpolatedValue(pos); } } else if (m_parameters.begin()->second == param) { value = param->getNormalizedValue(val); } else { value = param->getInterpolatedValue(pos); } return param->addKeyframe(pos, (KeyframeType)KdenliveSettings::defaultkeyframeinterp(), value, true, undo, redo); }; return applyOperation(op, update ? i18n("Change keyframe type") : i18n("Add keyframe")); } bool KeyframeModelList::removeKeyframe(GenTime pos) { QWriteLocker locker(&m_lock); Q_ASSERT(m_parameters.size() > 0); auto op = [pos](std::shared_ptr param, Fun &undo, Fun &redo) { return param->removeKeyframe(pos, undo, redo); }; return applyOperation(op, i18n("Delete keyframe")); } bool KeyframeModelList::removeAllKeyframes() { QWriteLocker locker(&m_lock); Q_ASSERT(m_parameters.size() > 0); auto op = [](std::shared_ptr param, Fun &undo, Fun &redo) { return param->removeAllKeyframes(undo, redo); }; return applyOperation(op, i18n("Delete all keyframes")); } bool KeyframeModelList::removeNextKeyframes(GenTime pos) { QWriteLocker locker(&m_lock); Q_ASSERT(m_parameters.size() > 0); auto op = [pos](std::shared_ptr param, Fun &undo, Fun &redo) { return param->removeNextKeyframes(pos, undo, redo); }; return applyOperation(op, i18n("Delete keyframes")); } bool KeyframeModelList::moveKeyframe(GenTime oldPos, GenTime pos, bool logUndo) { QWriteLocker locker(&m_lock); Q_ASSERT(m_parameters.size() > 0); auto op = [oldPos, pos](std::shared_ptr param, Fun &undo, Fun &redo) { return param->moveKeyframe(oldPos, pos, QVariant(), undo, redo); }; return applyOperation(op, logUndo ? i18n("Move keyframe") : QString()); } bool KeyframeModelList::updateKeyframe(GenTime oldPos, GenTime pos, const QVariant &normalizedVal, bool logUndo) { QWriteLocker locker(&m_lock); Q_ASSERT(m_parameters.size() > 0); bool isRectParam = false; if (m_inTimelineIndex.isValid()) { if (auto ptr = m_model.lock()) { - ParamType tp = ptr->data(m_inTimelineIndex, AssetParameterModel::TypeRole).value(); + auto tp = ptr->data(m_inTimelineIndex, AssetParameterModel::TypeRole).value(); if (tp == ParamType::AnimatedRect) { isRectParam = true; } } } auto op = [this, oldPos, pos, normalizedVal, isRectParam](std::shared_ptr param, Fun &undo, Fun &redo) { QVariant value; if (m_inTimelineIndex.isValid()) { if (m_parameters.at(m_inTimelineIndex) == param) { if (isRectParam) { if (normalizedVal.isValid()) { value = param->getInterpolatedValue(oldPos); value = param->updateInterpolated(value, normalizedVal.toDouble()); } } else { value = normalizedVal; } } } else if (m_parameters.begin()->second == param) { value = normalizedVal; } return param->moveKeyframe(oldPos, pos, value, undo, redo); }; return applyOperation(op, logUndo ? i18n("Move keyframe") : QString()); } bool KeyframeModelList::updateKeyframe(GenTime pos, const QVariant &value, const QPersistentModelIndex &index) { if (singleKeyframe()) { bool ok = false; Keyframe kf = m_parameters.begin()->second->getNextKeyframe(GenTime(-1), &ok); pos = kf.first; } if (auto ptr = m_model.lock()) { - AssetKeyframeCommand *command = new AssetKeyframeCommand(ptr, index, value, pos); + auto *command = new AssetKeyframeCommand(ptr, index, value, pos); pCore->pushUndo(command); } return true; QWriteLocker locker(&m_lock); Q_ASSERT(m_parameters.count(index) > 0); Fun undo = []() { return true; }; Fun redo = []() { return true; }; if (singleKeyframe()) { bool ok = false; Keyframe kf = m_parameters.begin()->second->getNextKeyframe(GenTime(-1), &ok); pos = kf.first; } bool res = m_parameters.at(index)->updateKeyframe(pos, value, undo, redo); if (res) { PUSH_UNDO(undo, redo, i18n("Update keyframe")); } return res; } bool KeyframeModelList::updateKeyframeType(GenTime pos, int type, const QPersistentModelIndex &index) { QWriteLocker locker(&m_lock); Q_ASSERT(m_parameters.count(index) > 0); Fun undo = []() { return true; }; Fun redo = []() { return true; }; if (singleKeyframe()) { bool ok = false; Keyframe kf = m_parameters.begin()->second->getNextKeyframe(GenTime(-1), &ok); pos = kf.first; } // Update kf type in all parameters bool res = true; for (const auto ¶m : m_parameters) { res = res && param.second->updateKeyframeType(pos, type, undo, redo); } if (res) { PUSH_UNDO(undo, redo, i18n("Update keyframe")); } return res; } KeyframeType KeyframeModelList::keyframeType(GenTime pos) const { QWriteLocker locker(&m_lock); if (singleKeyframe()) { bool ok = false; Keyframe kf = m_parameters.begin()->second->getNextKeyframe(GenTime(-1), &ok); return kf.second; } bool ok = false; Keyframe kf = m_parameters.begin()->second->getKeyframe(pos, &ok); return kf.second; } Keyframe KeyframeModelList::getKeyframe(const GenTime &pos, bool *ok) const { READ_LOCK(); Q_ASSERT(m_parameters.size() > 0); return m_parameters.begin()->second->getKeyframe(pos, ok); } bool KeyframeModelList::singleKeyframe() const { READ_LOCK(); Q_ASSERT(m_parameters.size() > 0); return m_parameters.begin()->second->singleKeyframe(); } bool KeyframeModelList::isEmpty() const { READ_LOCK(); return (m_parameters.size() == 0 || m_parameters.begin()->second->rowCount() == 0); } Keyframe KeyframeModelList::getNextKeyframe(const GenTime &pos, bool *ok) const { READ_LOCK(); Q_ASSERT(m_parameters.size() > 0); return m_parameters.begin()->second->getNextKeyframe(pos, ok); } Keyframe KeyframeModelList::getPrevKeyframe(const GenTime &pos, bool *ok) const { READ_LOCK(); Q_ASSERT(m_parameters.size() > 0); return m_parameters.begin()->second->getPrevKeyframe(pos, ok); } Keyframe KeyframeModelList::getClosestKeyframe(const GenTime &pos, bool *ok) const { READ_LOCK(); Q_ASSERT(m_parameters.size() > 0); return m_parameters.begin()->second->getClosestKeyframe(pos, ok); } bool KeyframeModelList::hasKeyframe(int frame) const { READ_LOCK(); Q_ASSERT(m_parameters.size() > 0); return m_parameters.begin()->second->hasKeyframe(frame); } void KeyframeModelList::refresh() { QWriteLocker locker(&m_lock); for (const auto ¶m : m_parameters) { param.second->refresh(); } } void KeyframeModelList::reset() { QWriteLocker locker(&m_lock); for (const auto ¶m : m_parameters) { param.second->reset(); } } QVariant KeyframeModelList::getInterpolatedValue(int pos, const QPersistentModelIndex &index) const { READ_LOCK(); Q_ASSERT(m_parameters.count(index) > 0); return m_parameters.at(index)->getInterpolatedValue(pos); } KeyframeModel *KeyframeModelList::getKeyModel() { if (m_inTimelineIndex.isValid()) { return m_parameters.at(m_inTimelineIndex).get(); } if (auto ptr = m_model.lock()) { for (const auto ¶m : m_parameters) { if (ptr->data(param.first, AssetParameterModel::ShowInTimelineRole) == true) { m_inTimelineIndex = param.first; return param.second.get(); } } } return nullptr; } KeyframeModel *KeyframeModelList::getKeyModel(const QPersistentModelIndex &index) { if (m_parameters.size() > 0) { 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/rotoscoping/bpoint.cpp b/src/assets/keyframes/model/rotoscoping/bpoint.cpp index 9baddd164..7a09cbd1f 100644 --- a/src/assets/keyframes/model/rotoscoping/bpoint.cpp +++ b/src/assets/keyframes/model/rotoscoping/bpoint.cpp @@ -1,101 +1,101 @@ /*************************************************************************** * Copyright (C) 2011 by Till Theato (root@ttill.de) * * 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 "bpoint.h" #include BPoint::BPoint() : h1(-1, -1) , p(-1, -1) , h2(-1, -1) - , handlesLinked(true) + { } BPoint::BPoint(const QPointF &handle1, const QPointF &point, const QPointF &handle2) : h1(handle1) , p(point) , h2(handle2) { autoSetLinked(); } QPointF &BPoint::operator[](int i) { return i == 0 ? h1 : (i == 1 ? p : h2); } const QPointF &BPoint::operator[](int i) const { return i == 0 ? h1 : (i == 1 ? p : h2); } bool BPoint::operator==(const BPoint &point) const { return point.h1 == h1 && point.p == p && point.h2 == h2; } void BPoint::setP(const QPointF &point, bool updateHandles) { QPointF offset = point - p; p = point; if (updateHandles) { h1 += offset; h2 += offset; } } void BPoint::setH1(const QPointF &handle1) { h1 = handle1; if (handlesLinked) { qreal angle = QLineF(h1, p).angle(); QLineF l = QLineF(p, h2); l.setAngle(angle); h2 = l.p2(); } } void BPoint::setH2(const QPointF &handle2) { h2 = handle2; if (handlesLinked) { qreal angle = QLineF(h2, p).angle(); QLineF l = QLineF(p, h1); l.setAngle(angle); h1 = l.p2(); } } void BPoint::autoSetLinked() { // sometimes the angle is returned as 360° // due to rounding problems the angle is sometimes not quite 0 qreal angle = QLineF(h1, p).angleTo(QLineF(p, h2)); handlesLinked = angle < 1e-3 || qRound(angle) == 360; } void BPoint::setHandlesLinked(bool linked) { handlesLinked = linked; if (linked) { // we force recomputing one of the handles setH1(h1); } } diff --git a/src/assets/keyframes/model/rotoscoping/bpoint.h b/src/assets/keyframes/model/rotoscoping/bpoint.h index eefaf2f5a..8df6d6cac 100644 --- a/src/assets/keyframes/model/rotoscoping/bpoint.h +++ b/src/assets/keyframes/model/rotoscoping/bpoint.h @@ -1,72 +1,72 @@ /*************************************************************************** * Copyright (C) 2011 by Till Theato (root@ttill.de) * * 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 BPOINT_H #define BPOINT_H #include /** * @brief Represents a point in a cubic Bézier spline. */ class BPoint { public: enum class PointType { H1 = 0, P = 1, H2 = 2 }; /** @brief Sets the point to -1, -1 to mark it as unusable (until point + handles have proper values) */ BPoint(); /** @brief Sets up according to the params. Linking detecting is done using autoSetLinked(). */ BPoint(const QPointF &handle1, const QPointF &point, const QPointF &handle2); bool operator==(const BPoint &point) const; /** @brief Returns h1 if i = 0, p if i = 1, h2 if i = 2. */ QPointF &operator[](int i); /** @brief Returns h1 if i = 0, p if i = 1, h2 if i = 2. */ const QPointF &operator[](int i) const; /** @brief Sets p to @param point. * @param updateHandles (default = true) Whether to make sure the handles keep their position relative to p. */ void setP(const QPointF &point, bool updateHandles = true); /** @brief Sets h1 to @param handle1. * * If handlesLinked is true h2 is updated. */ void setH1(const QPointF &handle1); /** @brief Sets h2 to @param handle2. * If handlesLinked is true h1 is updated. */ void setH2(const QPointF &handle2); /** @brief Sets handlesLinked to true if the handles are in a linked state (line through h1, p, h2) otherwise to false. */ void autoSetLinked(); /** @brief Toggles the link of the handles to @param linked*/ void setHandlesLinked(bool linked); /** handle 1 */ QPointF h1; /** point */ QPointF p; /** handle 2 */ QPointF h2; /** handles are linked to achieve a natural locking spline => PH1 = -r*PH2 ; a line can be drawn through h1, p, h2 */ - bool handlesLinked; + bool handlesLinked{true}; }; #endif diff --git a/src/assets/keyframes/model/rotoscoping/rotohelper.cpp b/src/assets/keyframes/model/rotoscoping/rotohelper.cpp index ca302e7bc..11fb3ff97 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, 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(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); + for (const auto &i : p) { + centerPoints << QVariant(i.p); + controlPoints << QVariant(i.h1); + controlPoints << QVariant(i.h2); } if (m_monitor) { m_monitor->setUpEffectGeometry(QRect(), centerPoints, controlPoints); } } } 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/model/assetcommand.cpp b/src/assets/model/assetcommand.cpp index 0273b06b3..4328a80c4 100644 --- a/src/assets/model/assetcommand.cpp +++ b/src/assets/model/assetcommand.cpp @@ -1,152 +1,151 @@ /*************************************************************************** * 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(const std::shared_ptr &model, const QModelIndex &index, const QString &value, QUndoCommand *parent) +#include +AssetCommand::AssetCommand(const std::shared_ptr &model, const QModelIndex &index, QString value, QUndoCommand *parent) : QUndoCommand(parent) , m_model(model) , m_index(index) - , m_value(value) + , m_value(std::move(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(const std::shared_ptr &model, const QModelIndex &index, const QVariant &value, GenTime pos, +AssetKeyframeCommand::AssetKeyframeCommand(const std::shared_ptr &model, const QModelIndex &index, QVariant value, GenTime pos, QUndoCommand *parent) : QUndoCommand(parent) , m_model(model) , m_index(index) - , m_value(value) + , m_value(std::move(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(const std::shared_ptr &model, const QVector> ¶meters, - QUndoCommand *parent) +AssetUpdateCommand::AssetUpdateCommand(const std::shared_ptr &model, QVector> parameters, QUndoCommand *parent) : QUndoCommand(parent) , m_model(model) - , m_value(parameters) + , m_value(std::move(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 b579f1d86..f24b62538 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(const std::shared_ptr &model, const QModelIndex &index, const QString &value, QUndoCommand *parent = nullptr); + AssetCommand(const std::shared_ptr &model, const QModelIndex &index, 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(const std::shared_ptr &model, const QModelIndex &index, const QVariant &value, GenTime pos, + AssetKeyframeCommand(const std::shared_ptr &model, const QModelIndex &index, 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(const std::shared_ptr &model, const QVector> ¶meters, QUndoCommand *parent = nullptr); + AssetUpdateCommand(const std::shared_ptr &model, QVector> parameters, 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 3c59dff5a..e41f73105 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++) { 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); + for (auto &&i : array) { + QJsonValue val = i; if (val.isObject()) { result << val.toObject().keys(); } } return result; } } return QStringList(); } const QVector> AssetParameterModel::loadPreset(const QString &presetFile, const QString &presetName) { QFile loadFile(presetFile); QVector> params; if (loadFile.exists() && loadFile.open(QIODevice::ReadOnly)) { QByteArray saveData = loadFile.readAll(); QJsonDocument loadDoc(QJsonDocument::fromJson(saveData)); if (loadDoc.isObject() && loadDoc.object().contains(presetName)) { qDebug() << "..........\n..........\nLOADING OBJECT JSON"; QJsonValue val = loadDoc.object().value(presetName); if (val.isObject()) { QVariantMap map = val.toObject().toVariantMap(); QMap::const_iterator i = map.constBegin(); while (i != map.constEnd()) { params.append({i.key(), i.value()}); ++i; } } } else if (loadDoc.isArray()) { QJsonArray array = loadDoc.array(); - for (int i = 0; i < array.size(); i++) { - QJsonValue val = array.at(i); + for (auto &&i : array) { + QJsonValue val = i; if (val.isObject() && val.toObject().contains(presetName)) { QJsonValue preset = val.toObject().value(presetName); if (preset.isArray()) { QJsonArray paramArray = preset.toArray(); - for (int j = 0; j < paramArray.size(); j++) { - QJsonValue v1 = paramArray.at(j); + for (auto &&j : paramArray) { + QJsonValue v1 = j; if (v1.isObject()) { QJsonObject ob = v1.toObject(); params.append({ob.value("name").toString(), ob.value("value").toVariant()}); } } } qDebug() << "// LOADED PRESET: " << presetName << "\n" << params; break; } } } } return params; } void AssetParameterModel::setParameters(const QVector> ¶ms) { QLocale locale; for (const auto ¶m : params) { if (param.second.type() == QVariant::Double) { setParameter(param.first, locale.toString(param.second.toDouble()), false); } else { setParameter(param.first, param.second.toString(), false); } } if (m_keyframes) { m_keyframes->refresh(); } // emit 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) { 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 2aff2f604..5f78e6e5b 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(); + ~AssetParameterModel() override; 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); 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/assetparameterview.cpp b/src/assets/view/assetparameterview.cpp index adf435985..224d73905 100644 --- a/src/assets/view/assetparameterview.cpp +++ b/src/assets/view/assetparameterview.cpp @@ -1,334 +1,334 @@ /*************************************************************************** * 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 "assetparameterview.hpp" #include "assets/model/assetcommand.hpp" #include "assets/model/assetparametermodel.hpp" #include "assets/view/widgets/abstractparamwidget.hpp" #include "assets/view/widgets/keyframewidget.hpp" #include "core.h" #include #include #include #include #include #include #include #include #include #include AssetParameterView::AssetParameterView(QWidget *parent) : QWidget(parent) - , m_mainKeyframeWidget(nullptr) + { m_lay = new QVBoxLayout(this); m_lay->setContentsMargins(0, 0, 0, 2); m_lay->setSpacing(0); setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)); // Presets Combo m_presetMenu = new QMenu(this); m_presetMenu->setToolTip(i18n("Presets")); } void AssetParameterView::setModel(const std::shared_ptr &model, QSize frameSize, bool addSpacer) { unsetModel(); QMutexLocker lock(&m_lock); m_model = model; const QString paramTag = model->getAssetId(); QDir dir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/effects/presets/")); const QString presetFile = dir.absoluteFilePath(QString("%1.json").arg(paramTag)); connect(this, &AssetParameterView::updatePresets, [this, presetFile](const QString &presetName) { m_presetMenu->clear(); m_presetGroup.reset(new QActionGroup(this)); m_presetGroup->setExclusive(true); m_presetMenu->addAction(QIcon::fromTheme(QStringLiteral("view-refresh")), i18n("Reset Effect"), this, SLOT(resetValues())); // Save preset m_presetMenu->addAction(QIcon::fromTheme(QStringLiteral("document-save-as-template")), i18n("Save preset"), this, SLOT(slotSavePreset())); m_presetMenu->addAction(QIcon::fromTheme(QStringLiteral("document-save-as-template")), i18n("Update current preset"), this, SLOT(slotUpdatePreset())); m_presetMenu->addAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18n("Delete preset"), this, SLOT(slotDeletePreset())); m_presetMenu->addSeparator(); QStringList presets = m_model->getPresetList(presetFile); for (const QString &pName : presets) { QAction *ac = m_presetMenu->addAction(pName, this, SLOT(slotLoadPreset())); m_presetGroup->addAction(ac); ac->setData(pName); ac->setCheckable(true); if (pName == presetName) { ac->setChecked(true); } } }); emit updatePresets(); connect(m_model.get(), &AssetParameterModel::dataChanged, this, &AssetParameterView::refresh); if (paramTag.endsWith(QStringLiteral("lift_gamma_gain"))) { // Special case, the colorwheel widget manages several parameters QModelIndex index = model->index(0, 0); auto w = AbstractParamWidget::construct(model, index, frameSize, this); connect(w, &AbstractParamWidget::valueChanged, this, &AssetParameterView::commitChanges); m_lay->addWidget(w); m_widgets.push_back(w); } else { for (int i = 0; i < model->rowCount(); ++i) { QModelIndex index = model->index(i, 0); auto type = model->data(index, AssetParameterModel::TypeRole).value(); if (m_mainKeyframeWidget && (type == ParamType::Geometry || type == ParamType::Animated || type == ParamType::RestrictedAnim || type == ParamType::KeyframeParam)) { // Keyframe widget can have some extra params that shouldn't build a new widget qDebug() << "// FOUND ADDED PARAM"; m_mainKeyframeWidget->addParameter(index); } else { auto w = AbstractParamWidget::construct(model, index, frameSize, this); connect(this, &AssetParameterView::initKeyframeView, w, &AbstractParamWidget::slotInitMonitor); if (type == ParamType::KeyframeParam || type == ParamType::AnimatedRect || type == ParamType::Roto_spline) { m_mainKeyframeWidget = static_cast(w); } connect(w, &AbstractParamWidget::valueChanged, this, &AssetParameterView::commitChanges); connect(w, &AbstractParamWidget::seekToPos, this, &AssetParameterView::seekToPos); m_lay->addWidget(w); m_widgets.push_back(w); } } } if (addSpacer) { m_lay->addStretch(); } } QVector> AssetParameterView::getDefaultValues() const { QLocale locale; QVector> values; for (int i = 0; i < m_model->rowCount(); ++i) { QModelIndex index = m_model->index(i, 0); QString name = m_model->data(index, AssetParameterModel::NameRole).toString(); - ParamType type = m_model->data(index, AssetParameterModel::TypeRole).value(); + auto type = m_model->data(index, AssetParameterModel::TypeRole).value(); QVariant defaultValue = m_model->data(index, AssetParameterModel::DefaultRole); if (type == ParamType::KeyframeParam || type == ParamType::AnimatedRect) { QString val = type == ParamType::KeyframeParam ? locale.toString(defaultValue.toDouble()) : defaultValue.toString(); if (!val.contains(QLatin1Char('='))) { val.prepend(QStringLiteral("%1=").arg(m_model->data(index, AssetParameterModel::ParentInRole).toInt())); defaultValue = QVariant(val); } } values.append({name, defaultValue}); } return values; } void AssetParameterView::resetValues() { const QVector> values = getDefaultValues(); - AssetUpdateCommand *command = new AssetUpdateCommand(m_model, values); + auto *command = new AssetUpdateCommand(m_model, values); pCore->pushUndo(command); /*if (m_mainKeyframeWidget) { m_mainKeyframeWidget->resetKeyframes(); }*/ } void AssetParameterView::commitChanges(const QModelIndex &index, const QString &value, bool storeUndo) { // Warning: please note that some widgets (for example keyframes) do NOT send the valueChanged signal and do modifications on their own - AssetCommand *command = new AssetCommand(m_model, index, value); + auto *command = new AssetCommand(m_model, index, value); if (storeUndo) { pCore->pushUndo(command); } else { command->redo(); delete command; } } void AssetParameterView::unsetModel() { QMutexLocker lock(&m_lock); if (m_model) { // if a model is already there, we have to disconnect signals first disconnect(m_model.get(), &AssetParameterModel::dataChanged, this, &AssetParameterView::refresh); } m_mainKeyframeWidget = nullptr; // clear layout m_widgets.clear(); QLayoutItem *child; while ((child = m_lay->takeAt(0)) != nullptr) { if (child->layout()) { QLayoutItem *subchild; while ((subchild = child->layout()->takeAt(0)) != nullptr) { delete subchild->widget(); delete subchild->spacerItem(); } } delete child->widget(); delete child->spacerItem(); } // Release ownership of smart pointer m_model.reset(); } void AssetParameterView::refresh(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles) { QMutexLocker lock(&m_lock); if (m_widgets.size() == 0) { // no visible param for this asset, abort return; } Q_UNUSED(roles); // We are expecting indexes that are children of the root index, which is "invalid" Q_ASSERT(!topLeft.parent().isValid()); // We make sure the range is valid if (m_mainKeyframeWidget) { m_mainKeyframeWidget->slotRefresh(); } else { auto type = m_model->data(m_model->index(topLeft.row(), 0), AssetParameterModel::TypeRole).value(); if (type == ParamType::ColorWheel) { // Some special widgets, like colorwheel handle multiple params so we can have cases where param index row is greater than the number of widgets. // Should be better managed m_widgets[0]->slotRefresh(); return; } Q_ASSERT(bottomRight.row() < (int)m_widgets.size()); - for (size_t i = (size_t)topLeft.row(); i <= (size_t)bottomRight.row(); ++i) { + for (auto i = (size_t)topLeft.row(); i <= (size_t)bottomRight.row(); ++i) { if (m_widgets.size() > i) { m_widgets[i]->slotRefresh(); } } } } int AssetParameterView::contentHeight() const { return m_lay->minimumSize().height(); } MonitorSceneType AssetParameterView::needsMonitorEffectScene() const { if (m_mainKeyframeWidget) { return m_mainKeyframeWidget->requiredScene(); } for (int i = 0; i < m_model->rowCount(); ++i) { QModelIndex index = m_model->index(i, 0); auto type = m_model->data(index, AssetParameterModel::TypeRole).value(); if (type == ParamType::Geometry) { return MonitorSceneGeometry; } } return MonitorSceneDefault; } /*void AssetParameterView::initKeyframeView() { if (m_mainKeyframeWidget) { m_mainKeyframeWidget->initMonitor(); } else { for (int i = 0; i < m_model->rowCount(); ++i) { QModelIndex index = m_model->index(i, 0); auto type = m_model->data(index, AssetParameterModel::TypeRole).value(); if (type == ParamType::Geometry) { return MonitorSceneGeometry; } } } }*/ void AssetParameterView::slotRefresh() { refresh(m_model->index(0, 0), m_model->index(m_model->rowCount() - 1, 0), {}); } bool AssetParameterView::keyframesAllowed() const { return m_mainKeyframeWidget != nullptr; } bool AssetParameterView::modelHideKeyframes() const { return m_mainKeyframeWidget != nullptr && !m_mainKeyframeWidget->keyframesVisible(); } void AssetParameterView::toggleKeyframes(bool enable) { if (m_mainKeyframeWidget) { m_mainKeyframeWidget->showKeyframes(enable); } } void AssetParameterView::slotDeletePreset() { QAction *ac = m_presetGroup->checkedAction(); if (!ac) { return; } QDir dir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/effects/presets/")); if (!dir.exists()) { dir.mkpath(QStringLiteral(".")); } const QString presetFile = dir.absoluteFilePath(QString("%1.json").arg(m_model->getAssetId())); m_model->deletePreset(presetFile, ac->data().toString()); emit updatePresets(); } void AssetParameterView::slotUpdatePreset() { QAction *ac = m_presetGroup->checkedAction(); if (!ac) { return; } slotSavePreset(ac->data().toString()); } void AssetParameterView::slotSavePreset(QString presetName) { if (presetName.isEmpty()) { bool ok; presetName = QInputDialog::getText(this, i18n("Enter preset name"), i18n("Enter the name of this preset"), QLineEdit::Normal, QString(), &ok); if (!ok) return; } QDir dir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/effects/presets/")); if (!dir.exists()) { dir.mkpath(QStringLiteral(".")); } const QString presetFile = dir.absoluteFilePath(QString("%1.json").arg(m_model->getAssetId())); m_model->savePreset(presetFile, presetName); emit updatePresets(presetName); } void AssetParameterView::slotLoadPreset() { - QAction *action = qobject_cast(sender()); + auto *action = qobject_cast(sender()); if (!action) { return; } const QString presetName = action->data().toString(); QDir dir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/effects/presets/")); const QString presetFile = dir.absoluteFilePath(QString("%1.json").arg(m_model->getAssetId())); const QVector> params = m_model->loadPreset(presetFile, presetName); - AssetUpdateCommand *command = new AssetUpdateCommand(m_model, params); + auto *command = new AssetUpdateCommand(m_model, params); pCore->pushUndo(command); } QMenu *AssetParameterView::presetMenu() { return m_presetMenu; } diff --git a/src/assets/view/assetparameterview.hpp b/src/assets/view/assetparameterview.hpp index a28516c7d..1a3e4fb55 100644 --- a/src/assets/view/assetparameterview.hpp +++ b/src/assets/view/assetparameterview.hpp @@ -1,112 +1,112 @@ /*************************************************************************** * 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 ASSETPARAMETERVIEW_H #define ASSETPARAMETERVIEW_H #include "definitions.h" #include #include #include #include #include /* @brief This class is the view for a list of parameters. */ class QVBoxLayout; class QMenu; class QActionGroup; class AbstractParamWidget; class AssetParameterModel; class KeyframeWidget; class AssetParameterView : public QWidget { Q_OBJECT public: AssetParameterView(QWidget *parent = nullptr); /** Sets the model to be displayed by current view */ virtual void setModel(const std::shared_ptr &model, QSize frameSize, bool addSpacer = false); /** Set the widget to display no model (this yield ownership on the smart-ptr)*/ void unsetModel(); /** Returns the preferred widget height */ int contentHeight() const; /** Returns the type of monitor overlay required by this effect */ MonitorSceneType needsMonitorEffectScene() const; /** Returns true is the effect can use keyframes */ bool keyframesAllowed() const; /** Returns true is the keyframes should be hidden on first opening*/ bool modelHideKeyframes() const; /** Returns the preset menu to be embedded in toolbars */ QMenu *presetMenu(); public slots: void slotRefresh(); void toggleKeyframes(bool enable); /** Reset all parameter values to default */ void resetValues(); /** Save all parameters to a preset */ void slotSavePreset(QString presetName = QString()); /** Save all parameters to a preset */ void slotLoadPreset(); void slotUpdatePreset(); void slotDeletePreset(); protected: /** @brief This is a handler for the dataChanged slot of the model. It basically instructs the widgets in the given range to be refreshed */ void refresh(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles); QVBoxLayout *m_lay; /** @brief Protect from concurrent operations **/ QMutex m_lock; std::shared_ptr m_model; std::vector m_widgets; - KeyframeWidget *m_mainKeyframeWidget; + KeyframeWidget *m_mainKeyframeWidget{nullptr}; QMenu *m_presetMenu; std::shared_ptr m_presetGroup; private slots: /** @brief Apply a change of parameter sent by the view @param index is the index corresponding to the modified param @param value is the new value of the parameter @param storeUndo: if true, an undo object is created */ void commitChanges(const QModelIndex &index, const QString &value, bool storeUndo); QVector> getDefaultValues() const; signals: void seekToPos(int); void initKeyframeView(bool active); /** @brief clear and refill the effect presets */ void updatePresets(const QString &presetName = QString()); }; #endif diff --git a/src/assets/view/widgets/abstractparamwidget.hpp b/src/assets/view/widgets/abstractparamwidget.hpp index aef819681..59a316f66 100644 --- a/src/assets/view/widgets/abstractparamwidget.hpp +++ b/src/assets/view/widgets/abstractparamwidget.hpp @@ -1,83 +1,84 @@ /*************************************************************************** * Copyright (C) 2016 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 ABSTRACTPARAMWIDGET_H #define ABSTRACTPARAMWIDGET_H #include #include #include #include /** @brief Base class of all the widgets representing a parameter of an asset (effect or transition) */ class AssetParameterModel; class AbstractParamWidget : public QWidget { Q_OBJECT public: AbstractParamWidget() = delete; AbstractParamWidget(std::shared_ptr model, QModelIndex index, QWidget *parent); - virtual ~AbstractParamWidget(){}; + ~AbstractParamWidget() override = default; + ; /** @brief Factory method to construct a parameter widget. @param model Parameter model this parameter belongs to @param index Index of the parameter in the given model @param parent parent widget */ static AbstractParamWidget *construct(const std::shared_ptr &model, QModelIndex index, QSize frameSize, QWidget *parent); signals: /** @brief Signal sent when the parameters hold by the widgets are modified The index corresponds which parameter is changed The string is the new value The bool allows to decide whether an undo object should be created */ void valueChanged(QModelIndex, QString, bool); /* @brief Signal sent when the filter needs to be deactivated or reactivated. This happens for example when the user has to pick a color. */ void disableCurrentFilter(bool); void seekToPos(int); public slots: /** @brief Toggle the comments on or off */ virtual void slotShowComment(bool) { qDebug() << "DEBUG: show_comment not correctly overridden"; } /** @brief refresh the properties to reflect changes in the model */ virtual void slotRefresh() = 0; /** @brief initialize qml keyframe view after creating it */ virtual void slotInitMonitor(bool /*active*/) {} protected: std::shared_ptr m_model; QPersistentModelIndex m_index; }; #endif diff --git a/src/assets/view/widgets/colorwheel.cpp b/src/assets/view/widgets/colorwheel.cpp index b632d5684..cd40823f9 100644 --- a/src/assets/view/widgets/colorwheel.cpp +++ b/src/assets/view/widgets/colorwheel.cpp @@ -1,341 +1,341 @@ /* * Copyright (c) 2013 Meltytech, LLC * Author: Dan Dennedy * Some ideas came from Qt-Plus: https://github.com/liuyanghejerry/Qt-Plus * and Steinar Gunderson's Movit demo app. * * 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 "colorwheel.h" #include - +#include NegQColor NegQColor::fromHsvF(qreal h, qreal s, qreal l, qreal a) { NegQColor color; color.qcolor = QColor::fromHsvF(h, s, l < 0 ? -l : l, a); color.sign_r = l < 0 ? -1 : 1; color.sign_g = l < 0 ? -1 : 1; color.sign_b = l < 0 ? -1 : 1; return color; } NegQColor NegQColor::fromRgbF(qreal r, qreal g, qreal b, qreal a) { NegQColor color; color.qcolor = QColor::fromRgbF(r < 0 ? -r : r, g < 0 ? -g : g, b < 0 ? -b : b, a); color.sign_r = r < 0 ? -1 : 1; color.sign_g = g < 0 ? -1 : 1; color.sign_b = b < 0 ? -1 : 1; return color; } qreal NegQColor::redF() { return qcolor.redF() * sign_r; } qreal NegQColor::greenF() { return qcolor.greenF() * sign_g; } qreal NegQColor::blueF() { return qcolor.blueF() * sign_b; } qreal NegQColor::valueF() { return qcolor.valueF() * sign_g; } int NegQColor::hue() { return qcolor.hue(); } qreal NegQColor::hueF() { return qcolor.hueF(); } qreal NegQColor::saturationF() { return qcolor.saturationF(); } -ColorWheel::ColorWheel(const QString &id, const QString &name, const NegQColor &color, QWidget *parent) +ColorWheel::ColorWheel(QString id, QString name, NegQColor color, QWidget *parent) : QWidget(parent) - , m_id(id) + , m_id(std::move(id)) , m_isMouseDown(false) , m_margin(5) - , m_color(color) + , m_color(std::move(color)) , m_isInWheel(false) , m_isInSquare(false) - , m_name(name) + , m_name(std::move(name)) { QFontInfo info(font()); m_unitSize = info.pixelSize(); m_initialSize = QSize(m_unitSize * 11.5, m_unitSize * 11); m_sliderWidth = m_unitSize * 1.5; resize(m_initialSize); setMinimumSize(100, 100); setMaximumSize(m_initialSize); setCursor(Qt::CrossCursor); } void ColorWheel::setFactorDefaultZero(qreal factor, qreal defvalue, qreal zero) { m_sizeFactor = factor; m_defaultValue = defvalue; m_zeroShift = zero; } NegQColor ColorWheel::color() const { return m_color; } void ColorWheel::setColor(const NegQColor &color) { m_color = color; } int ColorWheel::wheelSize() const { return qMin(width() - m_sliderWidth, height() - m_unitSize); } NegQColor ColorWheel::colorForPoint(const QPoint &point) { if (!m_image.valid(point)) { return NegQColor(); } if (m_isInWheel) { qreal w = wheelSize(); qreal xf = qreal(point.x()) / w; qreal yf = 1.0 - qreal(point.y()) / w; qreal xp = 2.0 * xf - 1.0; qreal yp = 2.0 * yf - 1.0; qreal rad = qMin(hypot(xp, yp), 1.0); qreal theta = qAtan2(yp, xp); theta -= 105.0 / 360.0 * 2.0 * M_PI; if (theta < 0.0) { theta += 2.0 * M_PI; } qreal hue = (theta * 180.0 / M_PI) / 360.0; return NegQColor::fromHsvF(hue, rad, m_color.valueF()); } if (m_isInSquare) { qreal value = 1.0 - qreal(point.y() - m_margin) / (wheelSize() - m_margin * 2); if (!qFuzzyCompare(m_zeroShift, 0.)) { value = value - m_zeroShift; } return NegQColor::fromHsvF(m_color.hueF(), m_color.saturationF(), value); } - return NegQColor(); + return {}; } QSize ColorWheel::sizeHint() const { - return QSize(width(), height()); + return {width(), height()}; } QSize ColorWheel::minimumSizeHint() const { - return QSize(100, 100); + return {100, 100}; } void ColorWheel::mousePressEvent(QMouseEvent *event) { m_lastPoint = event->pos(); if (m_wheelRegion.contains(m_lastPoint)) { m_isInWheel = true; m_isInSquare = false; if (event->button() == Qt::LeftButton) { changeColor(colorForPoint(m_lastPoint)); } else { // reset to default on middle button qreal r = m_color.redF(); qreal b = m_color.blueF(); qreal g = m_color.greenF(); qreal max = qMax(r, b); max = qMax(max, g); changeColor(NegQColor::fromRgbF(max, max, max)); } } else if (m_sliderRegion.contains(m_lastPoint)) { m_isInWheel = false; m_isInSquare = true; if (event->button() != Qt::MidButton) { changeColor(colorForPoint(m_lastPoint)); } else { NegQColor c; c = NegQColor::fromHsvF(m_color.hueF(), m_color.saturationF(), m_defaultValue / m_sizeFactor); changeColor(c); } } m_isMouseDown = true; } void ColorWheel::mouseMoveEvent(QMouseEvent *event) { m_lastPoint = event->pos(); if (!m_isMouseDown) { return; } if (m_wheelRegion.contains(m_lastPoint) && m_isInWheel) { const NegQColor color = colorForPoint(m_lastPoint); changeColor(color); } else if (m_sliderRegion.contains(m_lastPoint) && m_isInSquare) { const NegQColor color = colorForPoint(m_lastPoint); changeColor(color); } } void ColorWheel::mouseReleaseEvent(QMouseEvent *event) { Q_UNUSED(event) m_isMouseDown = false; m_isInWheel = false; m_isInSquare = false; } void ColorWheel::resizeEvent(QResizeEvent *event) { m_image = QImage(event->size(), QImage::Format_ARGB32_Premultiplied); m_image.fill(palette().background().color().rgb()); drawWheel(); drawSlider(); update(); } QString ColorWheel::getParamValues() { return QString::number(m_color.redF() * m_sizeFactor, 'g', 2) + QLatin1Char(',') + QString::number(m_color.greenF() * m_sizeFactor, 'g', 2) + QLatin1Char(',') + QString::number(m_color.blueF() * m_sizeFactor, 'g', 2); } void ColorWheel::paintEvent(QPaintEvent *event) { Q_UNUSED(event) QPainter painter(this); // QStyleOption opt; // opt.init(this); painter.setRenderHint(QPainter::Antialiasing); painter.drawImage(0, 0, m_image); // painter.drawRect(0, 0, width(), height()); painter.drawText(m_margin, wheelSize() + m_unitSize - m_margin, m_name + QLatin1Char(' ') + getParamValues()); drawWheelDot(painter); drawSliderBar(painter); // style()->drawPrimitive(QStyle::PE_Widget, &opt, &painter, this); } void ColorWheel::drawWheel() { int r = wheelSize(); QPainter painter(&m_image); painter.setRenderHint(QPainter::Antialiasing); m_image.fill(0); // transparent QConicalGradient conicalGradient; conicalGradient.setColorAt(0.0, Qt::red); conicalGradient.setColorAt(60.0 / 360.0, Qt::yellow); conicalGradient.setColorAt(135.0 / 360.0, Qt::green); conicalGradient.setColorAt(180.0 / 360.0, Qt::cyan); conicalGradient.setColorAt(240.0 / 360.0, Qt::blue); conicalGradient.setColorAt(315.0 / 360.0, Qt::magenta); conicalGradient.setColorAt(1.0, Qt::red); QRadialGradient radialGradient(0.0, 0.0, r / 2); radialGradient.setColorAt(0.0, Qt::white); radialGradient.setColorAt(1.0, Qt::transparent); painter.translate(r / 2, r / 2); painter.rotate(-105); QBrush hueBrush(conicalGradient); painter.setPen(Qt::NoPen); painter.setBrush(hueBrush); painter.drawEllipse(QPoint(0, 0), r / 2 - m_margin, r / 2 - m_margin); QBrush saturationBrush(radialGradient); painter.setBrush(saturationBrush); painter.drawEllipse(QPoint(0, 0), r / 2 - m_margin, r / 2 - m_margin); m_wheelRegion = QRegion(r / 2, r / 2, r - 2 * m_margin, r - 2 * m_margin, QRegion::Ellipse); m_wheelRegion.translate(-(r - 2 * m_margin) / 2, -(r - 2 * m_margin) / 2); } void ColorWheel::drawSlider() { QPainter painter(&m_image); painter.setRenderHint(QPainter::Antialiasing); int ws = wheelSize(); qreal scale = qreal(ws + m_sliderWidth) / maximumWidth(); int w = m_sliderWidth * scale; int h = ws - m_margin * 2; QLinearGradient gradient(0, 0, w, h); gradient.setColorAt(0.0, Qt::white); gradient.setColorAt(1.0, Qt::black); QBrush brush(gradient); painter.setPen(Qt::NoPen); painter.setBrush(brush); painter.translate(ws, m_margin); painter.drawRect(0, 0, w, h); m_sliderRegion = QRegion(ws, m_margin, w, h); } void ColorWheel::drawWheelDot(QPainter &painter) { int r = wheelSize() / 2; QPen pen(Qt::white); pen.setWidth(2); painter.setPen(pen); painter.setBrush(Qt::black); painter.translate(r, r); painter.rotate(360.0 - m_color.hue()); painter.rotate(-105); // r -= margin; painter.drawEllipse(QPointF(m_color.saturationF() * r, 0.0), 4, 4); painter.resetTransform(); } void ColorWheel::drawSliderBar(QPainter &painter) { qreal value = 1.0 - m_color.valueF(); if (m_id == QLatin1String("lift")) { value -= m_zeroShift; } int ws = wheelSize(); qreal scale = qreal(ws + m_sliderWidth) / maximumWidth(); int w = m_sliderWidth * scale; int h = ws - m_margin * 2; QPen pen(Qt::white); pen.setWidth(2); painter.setPen(pen); painter.setBrush(Qt::black); painter.translate(ws, m_margin + value * h); painter.drawRect(0, 0, w, 4); painter.resetTransform(); } void ColorWheel::changeColor(const NegQColor &color) { m_color = color; drawWheel(); drawSlider(); update(); emit colorChange(m_color); } diff --git a/src/assets/view/widgets/colorwheel.h b/src/assets/view/widgets/colorwheel.h index e978c7d56..4f767d703 100644 --- a/src/assets/view/widgets/colorwheel.h +++ b/src/assets/view/widgets/colorwheel.h @@ -1,98 +1,98 @@ /* * Copyright (c) 2013 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 COLORWHEELPARAM_H #define COLORWHEELPARAM_H #include #include #include class NegQColor { public: int8_t sign_r = 1; int8_t sign_g = 1; int8_t sign_b = 1; QColor qcolor; static NegQColor fromHsvF(qreal h, qreal s, qreal l, qreal a = 1.0); static NegQColor fromRgbF(qreal r, qreal g, qreal b, qreal a = 1.0); qreal redF(); qreal greenF(); qreal blueF(); qreal valueF(); int hue(); qreal hueF(); qreal saturationF(); }; class ColorWheel : public QWidget { Q_OBJECT public: - explicit ColorWheel(const QString &id, const QString &name, const NegQColor &color, QWidget *parent = nullptr); + explicit ColorWheel(QString id, QString name, NegQColor color, QWidget *parent = nullptr); QSize sizeHint() const override; QSize minimumSizeHint() const override; NegQColor color() const; void setColor(const NegQColor &color); void setFactorDefaultZero(qreal factor, qreal defvalue, qreal zero); signals: void colorChange(const NegQColor &color); public slots: void changeColor(const NegQColor &color); protected: void mousePressEvent(QMouseEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; void resizeEvent(QResizeEvent *event) override; void paintEvent(QPaintEvent *event) override; private: QString m_id; QSize m_initialSize; QImage m_image; bool m_isMouseDown; QPoint m_lastPoint; int m_margin; int m_sliderWidth; QRegion m_wheelRegion; QRegion m_sliderRegion; NegQColor m_color; bool m_isInWheel; bool m_isInSquare; int m_unitSize; QString m_name; qreal m_sizeFactor = 1; qreal m_defaultValue = 1; qreal m_zeroShift = 0; int wheelSize() const; NegQColor colorForPoint(const QPoint &point); void drawWheel(); void drawWheelDot(QPainter &painter); void drawSliderBar(QPainter &painter); void drawSlider(); QString getParamValues(); }; #endif // COLORWHEEL_H diff --git a/src/assets/view/widgets/curves/abstractcurvewidget.h b/src/assets/view/widgets/curves/abstractcurvewidget.h index de47b3aec..20c694843 100644 --- a/src/assets/view/widgets/curves/abstractcurvewidget.h +++ b/src/assets/view/widgets/curves/abstractcurvewidget.h @@ -1,161 +1,161 @@ /*************************************************************************** * Copyright (C) 2016 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 ABSTRACTCURVEWIDGET_H #define ABSTRACTCURVEWIDGET_H #include "bezier/bpoint.h" #include #include #include /** State of a point being moved */ enum class State_t { NORMAL, DRAG }; /** This class is a workaround to be able to use templating in the actual class Note that Qt doesn't support templated signals, so we have to define a signal for all possible Point type */ class __dummy_AbstractCurveWidget : public QWidget { Q_OBJECT public: __dummy_AbstractCurveWidget(QWidget *parent) : QWidget(parent) { } signals: /** * Emitted whenever a control point has changed position. */ void modified(); /** Signal sent when the current point changes. The point is returned, as well as a flag that determines if the point is the first or last. */ void currentPoint(const QPointF &p, bool extremal); void currentPoint(const BPoint &p, bool extremal); void resized(const QSize &s); public slots: /** @brief Delete current spline point if it is not a extremal point (first or last) */ virtual void slotDeleteCurrentPoint() = 0; virtual void slotZoomIn() = 0; virtual void slotZoomOut() = 0; virtual void reset() = 0; }; /** @brief Base class of all the widgets representing a curve of points */ template class AbstractCurveWidget : public __dummy_AbstractCurveWidget { public: - typedef typename Curve_t::Point_t Point_t; - virtual ~AbstractCurveWidget(){}; + typedef typename Curve_t::Point_t Point_t; // NOLINT + ~AbstractCurveWidget() override = default; /** @param parent Optional parent of the widget */ AbstractCurveWidget(QWidget *parent = nullptr); /** @brief Returns whether the points are controlled with additional handles */ virtual bool hasHandles() { return false; } /** @brief Sets the maximal number of points of the curve */ void setMaxPoints(int max); /** @brief Sets the background pixmap to @param pixmap. The background pixmap will be drawn under the grid and the curve*/ void setPixmap(const QPixmap &pixmap); /** @brief Number of lines used in grid. */ int gridLines() const; /** @brief Sets the number of grid lines to draw (in one direction) to @param lines. */ void setGridLines(int lines); /** @brief Constructs the curve from @param string */ void setFromString(const QString &string); /** @brief Resets the curve to an empty one */ void reset() override; /** @brief Returns a string corresponding to the curve */ QString toString(); /** @brief Replaces current point with @param p (index stays the same). * @param final (default = true) emit signal modified? */ void updateCurrentPoint(const Point_t &p, bool final = true); /** @brief Returns the selected point or else empty point. */ Point_t getCurrentPoint(); /** @brief Returns the list of all the points. */ virtual QList getPoints() const = 0; public: /** @brief Delete current spline point if it is not a extremal point (first or last) */ void slotDeleteCurrentPoint() override; void slotZoomIn() override; void slotZoomOut() override; protected: void paintBackground(QPainter *p); int heightForWidth(int w) const override; void resizeEvent(QResizeEvent *event) override; void leaveEvent(QEvent *) override; void mouseReleaseEvent(QMouseEvent *e) override; void keyPressEvent(QKeyEvent *) override; /** Utility function to check if current selected point is the first or the last */ bool isCurrentPointExtremal(); - int m_zoomLevel; - int m_gridLines; + int m_zoomLevel{0}; + int m_gridLines{3}; /** Background */ QPixmap m_pixmap; /** A copy of m_pixmap but scaled to fit the size of the edit region */ std::shared_ptr m_pixmapCache; /** Whether we have to regenerate the pixmap cache because the pixmap or the size of the edit region changed. */ - bool m_pixmapIsDirty; + bool m_pixmapIsDirty{true}; - int m_currentPointIndex; - int m_maxPoints; + int m_currentPointIndex{-1}; + int m_maxPoints{1000000}; int m_wWidth, m_wHeight; - State_t m_state; + State_t m_state{State_t::NORMAL}; Curve_t m_curve; - double m_grabRadius; + double m_grabRadius{10}; }; #include "abstractcurvewidget.ipp" #endif diff --git a/src/assets/view/widgets/curves/abstractcurvewidget.ipp b/src/assets/view/widgets/curves/abstractcurvewidget.ipp index 1ab0d8bbb..97ad5c4ef 100644 --- a/src/assets/view/widgets/curves/abstractcurvewidget.ipp +++ b/src/assets/view/widgets/curves/abstractcurvewidget.ipp @@ -1,248 +1,243 @@ /*************************************************************************** * Copyright (C) 2016 by Nicolas Carion * * * * 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 #include #include template AbstractCurveWidget::AbstractCurveWidget(QWidget *parent) : __dummy_AbstractCurveWidget(parent) - , m_zoomLevel(0) - , m_gridLines(3) - , m_pixmapCache(nullptr) - , m_pixmapIsDirty(true) - , m_currentPointIndex(-1) - , m_maxPoints(1000000) - , m_state(State_t::NORMAL) - , m_grabRadius(10) + , + m_pixmapCache(nullptr) + { setMouseTracking(true); setAutoFillBackground(false); setAttribute(Qt::WA_OpaquePaintEvent); setMinimumSize(150, 150); QSizePolicy sp(QSizePolicy::Preferred, QSizePolicy::MinimumExpanding); sp.setHeightForWidth(true); // force widget to have a height dependent on width; setSizePolicy(sp); setFocusPolicy(Qt::StrongFocus); } template void AbstractCurveWidget::paintBackground(QPainter *p) { /* * Zoom */ m_wWidth = width() - 1; m_wHeight = height() - 1; int offsetX = 1 / 8. * m_zoomLevel * m_wWidth; int offsetY = 1 / 8. * m_zoomLevel * m_wHeight; m_wWidth -= 2 * offsetX; m_wHeight -= 2 * offsetY; p->translate(offsetX, offsetY); /* * Background */ p->fillRect(rect().translated(-offsetX, -offsetY), palette().background()); if (!m_pixmap.isNull()) { if (m_pixmapIsDirty || !m_pixmapCache) { m_pixmapCache = std::make_shared(m_wWidth + 1, m_wHeight + 1); QPainter cachePainter(m_pixmapCache.get()); cachePainter.scale(1.0 * (m_wWidth + 1) / m_pixmap.width(), 1.0 * (m_wHeight + 1) / m_pixmap.height()); cachePainter.drawPixmap(0, 0, m_pixmap); m_pixmapIsDirty = false; } p->drawPixmap(0, 0, *m_pixmapCache); } // select color of the grid, depending on whether we have a palette or not if (!m_pixmap.isNull()) { p->setPen(QPen(palette().mid().color(), 1, Qt::SolidLine)); } else { int h, s, l, a; auto bg = palette().color(QPalette::Window); bg.getHsl(&h, &s, &l, &a); l = (l > 128) ? l - 30 : l + 30; bg.setHsl(h, s, l, a); p->setPen(QPen(bg, 1, Qt::SolidLine)); } /* * Borders */ p->drawRect(0, 0, m_wWidth, m_wHeight); /* * Grid */ if (m_gridLines != 0) { double stepH = m_wWidth / (double)(m_gridLines + 1); double stepV = m_wHeight / (double)(m_gridLines + 1); for (int i = 1; i <= m_gridLines; ++i) { p->drawLine(QLineF(i * stepH, 0, i * stepH, m_wHeight)); p->drawLine(QLineF(0, i * stepV, m_wWidth, i * stepV)); } } p->setRenderHint(QPainter::Antialiasing); /* * Standard line */ p->drawLine(QLineF(0, m_wHeight, m_wWidth, 0)); } template void AbstractCurveWidget::setMaxPoints(int max) { Q_ASSERT(max >= 2); m_maxPoints = max; } template void AbstractCurveWidget::setPixmap(const QPixmap &pix) { m_pixmap = pix; m_pixmapIsDirty = true; update(); } template int AbstractCurveWidget::gridLines() const { return m_gridLines; } template void AbstractCurveWidget::setGridLines(int lines) { m_gridLines = qBound(0, lines, 8); update(); } template void AbstractCurveWidget::slotZoomIn() { m_zoomLevel = qMax(m_zoomLevel - 1, 0); m_pixmapIsDirty = true; update(); } template void AbstractCurveWidget::slotZoomOut() { m_zoomLevel = qMin(m_zoomLevel + 1, 3); m_pixmapIsDirty = true; update(); } template void AbstractCurveWidget::setFromString(const QString &str) { m_curve.fromString(str); m_currentPointIndex = -1; emit currentPoint(Point_t(), true); update(); } template void AbstractCurveWidget::reset() { setFromString(Curve_t().toString()); m_currentPointIndex = -1; emit currentPoint(Point_t(), true); emit modified(); update(); } template QString AbstractCurveWidget::toString() { return m_curve.toString(); } template void AbstractCurveWidget::updateCurrentPoint(const Point_t &p, bool final) { if (m_currentPointIndex >= 0) { m_curve.setPoint(m_currentPointIndex, p); // during validation the point might have changed emit currentPoint(m_curve.getPoint(m_currentPointIndex), isCurrentPointExtremal()); if (final) { emit modified(); } update(); } } template typename AbstractCurveWidget::Point_t AbstractCurveWidget::getCurrentPoint() { if (m_currentPointIndex >= 0) { return m_curve.getPoint(m_currentPointIndex); } else { return Point_t(); } } template bool AbstractCurveWidget::isCurrentPointExtremal() { return m_currentPointIndex == 0 || m_currentPointIndex == m_curve.points().size() - 1; } template void AbstractCurveWidget::slotDeleteCurrentPoint() { if (m_currentPointIndex > 0 && m_currentPointIndex < m_curve.points().size() - 1) { m_curve.removePoint(m_currentPointIndex); m_currentPointIndex--; emit currentPoint(m_curve.getPoint(m_currentPointIndex), isCurrentPointExtremal()); update(); emit modified(); setCursor(Qt::ArrowCursor); m_state = State_t::NORMAL; } } template void AbstractCurveWidget::resizeEvent(QResizeEvent *e) { m_pixmapIsDirty = true; QWidget::resizeEvent(e); } template void AbstractCurveWidget::leaveEvent(QEvent *event) { QWidget::leaveEvent(event); } template void AbstractCurveWidget::mouseReleaseEvent(QMouseEvent *event) { if (event->button() != Qt::LeftButton) { return; } setCursor(Qt::ArrowCursor); m_state = State_t::NORMAL; emit modified(); } template void AbstractCurveWidget::keyPressEvent(QKeyEvent *e) { if (e->key() == Qt::Key_Delete || e->key() == Qt::Key_Backspace) { slotDeleteCurrentPoint(); } else { QWidget::keyPressEvent(e); } } template int AbstractCurveWidget::heightForWidth(int w) const { return w; } diff --git a/src/assets/view/widgets/curves/bezier/beziersplineeditor.cpp b/src/assets/view/widgets/curves/bezier/beziersplineeditor.cpp index c753e9378..91641c60c 100644 --- a/src/assets/view/widgets/curves/bezier/beziersplineeditor.cpp +++ b/src/assets/view/widgets/curves/bezier/beziersplineeditor.cpp @@ -1,323 +1,320 @@ /*************************************************************************** * Copyright (C) 2010 by Till Theato (root@ttill.de) * * 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 "beziersplineeditor.h" #include "cubicbezierspline.h" #include "kdenlivesettings.h" #include "complex" #include #include BezierSplineEditor::BezierSplineEditor(QWidget *parent) : AbstractCurveWidget(parent) - , m_showAllHandles(true) - , m_currentPointType(BPoint::PointType::P) - , m_grabOffsetX(0) - , m_grabOffsetY(0) + { m_curve = CubicBezierSpline(); } -BezierSplineEditor::~BezierSplineEditor() {} +BezierSplineEditor::~BezierSplineEditor() = default; void BezierSplineEditor::paintEvent(QPaintEvent *event) { Q_UNUSED(event) QPainter p(this); paintBackground(&p); /* * Prepare Spline, Points */ int max = m_curve.count() - 1; if (max < 1) { return; } BPoint point(m_curve.getPoint(0, m_wWidth, m_wHeight, true)); /* * Spline */ BPoint next; QPainterPath splinePath(QPointF(point.p.x(), point.p.y())); for (int i = 0; i < max; ++i) { point = m_curve.getPoint(i, m_wWidth, m_wHeight, true); next = m_curve.getPoint(i + 1, m_wWidth, m_wHeight, true); splinePath.cubicTo(point.h2, next.h1, next.p); } p.setPen(QPen(palette().text().color(), 1, Qt::SolidLine)); p.drawPath(splinePath); /* * Points + Handles */ p.setPen(QPen(Qt::red, 1, Qt::SolidLine)); QPolygonF handle = QPolygonF() << QPointF(0, -3) << QPointF(3, 0) << QPointF(0, 3) << QPointF(-3, 0); for (int i = 0; i <= max; ++i) { point = m_curve.getPoint(i, m_wWidth, m_wHeight, true); if (i == m_currentPointIndex) { // selected point: fill p and handles p.setBrush(QBrush(QColor(Qt::red), Qt::SolidPattern)); // connect p and handles with lines if (i != 0) { p.drawLine(QLineF(point.h1.x(), point.h1.y(), point.p.x(), point.p.y())); } if (i != max) { p.drawLine(QLineF(point.p.x(), point.p.y(), point.h2.x(), point.h2.y())); } } p.drawEllipse(QRectF(point.p.x() - 3, point.p.y() - 3, 6, 6)); if (i != 0 && (i == m_currentPointIndex || m_showAllHandles)) { p.drawConvexPolygon(handle.translated(point.h1.x(), point.h1.y())); } if (i != max && (i == m_currentPointIndex || m_showAllHandles)) { p.drawConvexPolygon(handle.translated(point.h2.x(), point.h2.y())); } if (i == m_currentPointIndex) { p.setBrush(QBrush(Qt::NoBrush)); } } } void BezierSplineEditor::mousePressEvent(QMouseEvent *event) { 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 = (event->pos().x() - offsetX) / (double)(wWidth); double y = 1.0 - (event->pos().y() - offsetY) / (double)(wHeight); BPoint::PointType selectedPoint; int closestPointIndex = nearestPointInRange(QPointF(x, y), wWidth, wHeight, &selectedPoint); if (event->button() == Qt::RightButton && closestPointIndex > 0 && closestPointIndex < m_curve.count() - 1 && selectedPoint == BPoint::PointType::P) { m_currentPointIndex = closestPointIndex; slotDeleteCurrentPoint(); return; } if (event->button() != Qt::LeftButton) { return; } if (closestPointIndex < 0) { if (m_curve.count() < m_maxPoints) { m_currentPointIndex = m_curve.addPoint(QPointF(x, y)); m_currentPointType = BPoint::PointType::P; } } else { m_currentPointIndex = closestPointIndex; m_currentPointType = selectedPoint; } BPoint point = m_curve.getPoint(m_currentPointIndex); m_grabPOriginal = point; if (m_currentPointIndex > 0) { m_grabPPrevious = m_curve.getPoint(m_currentPointIndex - 1); } if (m_currentPointIndex < m_curve.count() - 1) { m_grabPNext = m_curve.getPoint(m_currentPointIndex + 1); } m_grabOffsetX = point[(int)m_currentPointType].x() - x; m_grabOffsetY = point[(int)m_currentPointType].y() - y; point[(int)m_currentPointType] = QPointF(x + m_grabOffsetX, y + m_grabOffsetY); m_curve.setPoint(m_currentPointIndex, point); m_state = State_t::DRAG; emit currentPoint(point, isCurrentPointExtremal()); emit modified(); update(); } void BezierSplineEditor::mouseMoveEvent(QMouseEvent *event) { 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 = (event->pos().x() - offsetX) / (double)(wWidth); double y = 1.0 - (event->pos().y() - offsetY) / (double)(wHeight); if (m_state == State_t::NORMAL) { // If no point is selected set the cursor shape if on top BPoint::PointType type; int nearestPointIndex = nearestPointInRange(QPointF(x, y), wWidth, wHeight, &type); if (nearestPointIndex < 0) { setCursor(Qt::ArrowCursor); } else { setCursor(Qt::CrossCursor); } } else { // Else, drag the selected point setCursor(Qt::CrossCursor); x += m_grabOffsetX; y += m_grabOffsetY; double leftX = 0.; double rightX = 1.; BPoint point = m_curve.getPoint(m_currentPointIndex); switch (m_currentPointType) { case BPoint::PointType::H1: rightX = point.p.x(); if (m_currentPointIndex == 0) { leftX = -4; } else { leftX = m_curve.getPoint(m_currentPointIndex - 1).p.x(); } x = qBound(leftX, x, rightX); point.setH1(QPointF(x, y)); break; case BPoint::PointType::P: if (m_currentPointIndex == 0) { rightX = 0.0; } else if (m_currentPointIndex == m_curve.count() - 1) { leftX = 1.0; } x = qBound(leftX, x, rightX); y = qBound(0., y, 1.); // handles might have changed because we neared another point // try to restore point.h1 = m_grabPOriginal.h1; point.h2 = m_grabPOriginal.h2; // and move by same offset // (using update handle in point.setP won't work because the offset between new and old point is very small) point.h1 += QPointF(x, y) - m_grabPOriginal.p; point.h2 += QPointF(x, y) - m_grabPOriginal.p; point.setP(QPointF(x, y), false); break; case BPoint::PointType::H2: leftX = point.p.x(); if (m_currentPointIndex == m_curve.count() - 1) { rightX = 5; } else { rightX = m_curve.getPoint(m_currentPointIndex + 1).p.x(); } x = qBound(leftX, x, rightX); point.setH2(QPointF(x, y)); }; int index = m_currentPointIndex; m_currentPointIndex = m_curve.setPoint(m_currentPointIndex, point); if (m_currentPointType == BPoint::PointType::P) { // we might have changed the handles of other points // try to restore if (index == m_currentPointIndex) { if (m_currentPointIndex > 0) { m_curve.setPoint(m_currentPointIndex - 1, m_grabPPrevious); } if (m_currentPointIndex < m_curve.count() - 1) { m_curve.setPoint(m_currentPointIndex + 1, m_grabPNext); } } else { if (m_currentPointIndex < index) { m_curve.setPoint(index, m_grabPPrevious); m_grabPNext = m_grabPPrevious; if (m_currentPointIndex > 0) { m_grabPPrevious = m_curve.getPoint(m_currentPointIndex - 1); } } else { m_curve.setPoint(index, m_grabPNext); m_grabPPrevious = m_grabPNext; if (m_currentPointIndex < m_curve.count() - 1) { m_grabPNext = m_curve.getPoint(m_currentPointIndex + 1); } } } } emit currentPoint(point, isCurrentPointExtremal()); if (KdenliveSettings::dragvalue_directupdate()) { emit modified(); } update(); } } void BezierSplineEditor::mouseDoubleClickEvent(QMouseEvent * /*event*/) { if (m_currentPointIndex >= 0) { BPoint p = m_curve.getPoint(m_currentPointIndex); p.handlesLinked = !p.handlesLinked; m_curve.setPoint(m_currentPointIndex, p); emit currentPoint(p, isCurrentPointExtremal()); } } int BezierSplineEditor::nearestPointInRange(const QPointF &p, int wWidth, int wHeight, BPoint::PointType *sel) { auto nearest = m_curve.closestPoint(p); int nearestIndex = nearest.first; BPoint::PointType pointType = nearest.second; if (nearestIndex >= 0 && (nearestIndex == m_currentPointIndex || pointType == BPoint::PointType::P || m_showAllHandles)) { // a point was found and it is not a hidden handle BPoint point = m_curve.getPoint(nearestIndex); double dx = (p.x() - point[(int)pointType].x()) * wWidth; double dy = (p.y() - point[(int)pointType].y()) * wHeight; if (dx * dx + dy * dy <= m_grabRadius * m_grabRadius) { *sel = pointType; return nearestIndex; } } return -1; } void BezierSplineEditor::setShowAllHandles(bool show) { if (m_showAllHandles != show) { m_showAllHandles = show; update(); } } QList BezierSplineEditor::getPoints() const { return m_curve.getPoints(); } diff --git a/src/assets/view/widgets/curves/bezier/beziersplineeditor.h b/src/assets/view/widgets/curves/bezier/beziersplineeditor.h index af41515c2..b99ec7357 100644 --- a/src/assets/view/widgets/curves/bezier/beziersplineeditor.h +++ b/src/assets/view/widgets/curves/bezier/beziersplineeditor.h @@ -1,74 +1,74 @@ /*************************************************************************** * Copyright (C) 2010 by Till Theato (root@ttill.de) * * 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 BEZIERSPLINEEDITOR_H #define BEZIERSPLINEEDITOR_H #include "../abstractcurvewidget.h" #include "bpoint.h" #include "colortools.h" #include "cubicbezierspline.h" #include class BezierSplineEditor : public AbstractCurveWidget { Q_OBJECT public: - typedef BPoint Point_t; + using Point_t = BPoint; explicit BezierSplineEditor(QWidget *parent = nullptr); - ~BezierSplineEditor(); + ~BezierSplineEditor() override; /** @brief Sets the property showAllHandles to @param show. * * showAllHandles: Whether to show only handles for the selected point for all points. */ void setShowAllHandles(bool show); QList getPoints() const override; public slots: protected: void paintEvent(QPaintEvent *event) override; void mousePressEvent(QMouseEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override; void mouseDoubleClickEvent(QMouseEvent *event) override; private: /** Whether to show handles for all points or only for the selected one. */ - bool m_showAllHandles; + bool m_showAllHandles{true}; - BPoint::PointType m_currentPointType; - double m_grabOffsetX; - double m_grabOffsetY; + BPoint::PointType m_currentPointType{BPoint::PointType::P}; + double m_grabOffsetX{0}; + double m_grabOffsetY{0}; /** selected point before it was modified by dragging (at the time of the mouse press) */ BPoint m_grabPOriginal; /** point with the index currentPointIndex + 1 at the time of the mouse press */ BPoint m_grabPNext; /** point with the index currentPointIndex - 1 at the time of the mouse press */ BPoint m_grabPPrevious; /** @brief Finds the point nearest to @param p and returns it's index. * @param sel Is filled with the type of the closest point (h1, p, h2) * * If no point is near enough -1 is returned. */ int nearestPointInRange(const QPointF &p, int wWidth, int wHeight, BPoint::PointType *sel); }; #endif diff --git a/src/assets/view/widgets/curves/bezier/bpoint.cpp b/src/assets/view/widgets/curves/bezier/bpoint.cpp index 9baddd164..7a09cbd1f 100644 --- a/src/assets/view/widgets/curves/bezier/bpoint.cpp +++ b/src/assets/view/widgets/curves/bezier/bpoint.cpp @@ -1,101 +1,101 @@ /*************************************************************************** * Copyright (C) 2011 by Till Theato (root@ttill.de) * * 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 "bpoint.h" #include BPoint::BPoint() : h1(-1, -1) , p(-1, -1) , h2(-1, -1) - , handlesLinked(true) + { } BPoint::BPoint(const QPointF &handle1, const QPointF &point, const QPointF &handle2) : h1(handle1) , p(point) , h2(handle2) { autoSetLinked(); } QPointF &BPoint::operator[](int i) { return i == 0 ? h1 : (i == 1 ? p : h2); } const QPointF &BPoint::operator[](int i) const { return i == 0 ? h1 : (i == 1 ? p : h2); } bool BPoint::operator==(const BPoint &point) const { return point.h1 == h1 && point.p == p && point.h2 == h2; } void BPoint::setP(const QPointF &point, bool updateHandles) { QPointF offset = point - p; p = point; if (updateHandles) { h1 += offset; h2 += offset; } } void BPoint::setH1(const QPointF &handle1) { h1 = handle1; if (handlesLinked) { qreal angle = QLineF(h1, p).angle(); QLineF l = QLineF(p, h2); l.setAngle(angle); h2 = l.p2(); } } void BPoint::setH2(const QPointF &handle2) { h2 = handle2; if (handlesLinked) { qreal angle = QLineF(h2, p).angle(); QLineF l = QLineF(p, h1); l.setAngle(angle); h1 = l.p2(); } } void BPoint::autoSetLinked() { // sometimes the angle is returned as 360° // due to rounding problems the angle is sometimes not quite 0 qreal angle = QLineF(h1, p).angleTo(QLineF(p, h2)); handlesLinked = angle < 1e-3 || qRound(angle) == 360; } void BPoint::setHandlesLinked(bool linked) { handlesLinked = linked; if (linked) { // we force recomputing one of the handles setH1(h1); } } diff --git a/src/assets/view/widgets/curves/bezier/bpoint.h b/src/assets/view/widgets/curves/bezier/bpoint.h index eefaf2f5a..8df6d6cac 100644 --- a/src/assets/view/widgets/curves/bezier/bpoint.h +++ b/src/assets/view/widgets/curves/bezier/bpoint.h @@ -1,72 +1,72 @@ /*************************************************************************** * Copyright (C) 2011 by Till Theato (root@ttill.de) * * 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 BPOINT_H #define BPOINT_H #include /** * @brief Represents a point in a cubic Bézier spline. */ class BPoint { public: enum class PointType { H1 = 0, P = 1, H2 = 2 }; /** @brief Sets the point to -1, -1 to mark it as unusable (until point + handles have proper values) */ BPoint(); /** @brief Sets up according to the params. Linking detecting is done using autoSetLinked(). */ BPoint(const QPointF &handle1, const QPointF &point, const QPointF &handle2); bool operator==(const BPoint &point) const; /** @brief Returns h1 if i = 0, p if i = 1, h2 if i = 2. */ QPointF &operator[](int i); /** @brief Returns h1 if i = 0, p if i = 1, h2 if i = 2. */ const QPointF &operator[](int i) const; /** @brief Sets p to @param point. * @param updateHandles (default = true) Whether to make sure the handles keep their position relative to p. */ void setP(const QPointF &point, bool updateHandles = true); /** @brief Sets h1 to @param handle1. * * If handlesLinked is true h2 is updated. */ void setH1(const QPointF &handle1); /** @brief Sets h2 to @param handle2. * If handlesLinked is true h1 is updated. */ void setH2(const QPointF &handle2); /** @brief Sets handlesLinked to true if the handles are in a linked state (line through h1, p, h2) otherwise to false. */ void autoSetLinked(); /** @brief Toggles the link of the handles to @param linked*/ void setHandlesLinked(bool linked); /** handle 1 */ QPointF h1; /** point */ QPointF p; /** handle 2 */ QPointF h2; /** handles are linked to achieve a natural locking spline => PH1 = -r*PH2 ; a line can be drawn through h1, p, h2 */ - bool handlesLinked; + bool handlesLinked{true}; }; #endif diff --git a/src/assets/view/widgets/curves/bezier/cubicbezierspline.cpp b/src/assets/view/widgets/curves/bezier/cubicbezierspline.cpp index 6230c5f7d..9a2d901fa 100644 --- a/src/assets/view/widgets/curves/bezier/cubicbezierspline.cpp +++ b/src/assets/view/widgets/curves/bezier/cubicbezierspline.cpp @@ -1,210 +1,210 @@ /*************************************************************************** * Copyright (C) 2010 by Till Theato (root@ttill.de) * * 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 "cubicbezierspline.h" #include #include #include -#include +#include /** @brief For sorting a Bezier spline. Whether a is before b. */ static bool pointLessThan(const BPoint &a, const BPoint &b) { return a.p.x() < b.p.x(); } CubicBezierSpline::CubicBezierSpline() { m_points.append(BPoint(QPointF(0, 0), QPointF(0, 0), QPointF(.1, .1))); m_points.append(BPoint(QPointF(.9, .9), QPointF(1, 1), QPointF(1, 1))); } CubicBezierSpline::CubicBezierSpline(const CubicBezierSpline &spline) { m_points = spline.m_points; } CubicBezierSpline &CubicBezierSpline::operator=(const CubicBezierSpline &spline) = default; void CubicBezierSpline::fromString(const QString &spline) { m_points.clear(); QLocale locale; const QStringList bpoints = spline.split(QLatin1Char('|')); for (const QString &bpoint : bpoints) { const QStringList points = bpoint.split(QLatin1Char('#')); QVector values; for (const QString &point : points) { const QStringList xy = point.split(QLatin1Char(';')); if (xy.count() == 2) { values.append(QPointF(locale.toDouble(xy.at(0)), locale.toDouble(xy.at(1)))); } } if (values.count() == 3) { m_points.append(BPoint(values.at(0), values.at(1), values.at(2))); } } keepSorted(); validatePoints(); } QString CubicBezierSpline::toString() const { QStringList spline; QLocale locale; locale.setNumberOptions(QLocale::OmitGroupSeparator); for (const BPoint &p : m_points) { spline << QStringLiteral("%1;%2#%3;%4#%5;%6") .arg(locale.toString(p.h1.x()), locale.toString(p.h1.y()), locale.toString(p.p.x()), locale.toString(p.p.y()), locale.toString(p.h2.x()), locale.toString(p.h2.y())); } return spline.join(QLatin1Char('|')); } int CubicBezierSpline::setPoint(int ix, const BPoint &point) { m_points[ix] = point; keepSorted(); validatePoints(); return indexOf(point); // in case it changed } QList CubicBezierSpline::points() const { return m_points; } void CubicBezierSpline::removePoint(int ix) { m_points.removeAt(ix); } int CubicBezierSpline::addPoint(const BPoint &point) { m_points.append(point); keepSorted(); validatePoints(); return indexOf(point); } int CubicBezierSpline::addPoint(const QPointF &point) { // Check if point is in range if (point.x() < m_points[0].p.x() || point.x() > m_points.back().p.x()) { return -1; } // first we find by dichotomy the previous and next points on the curve int prev = 0, next = m_points.size() - 1; while (prev < next - 1) { int mid = (prev + next) / 2; if (point.x() < m_points[mid].p.x()) { next = mid; } else { prev = mid; } } // compute vector between adjacent points QPointF vec = m_points[next].p - m_points[prev].p; // normalize vec /= 10. * sqrt(vec.x() * vec.x() + vec.y() * vec.y()); // add resulting point return addPoint(BPoint(point - vec, point, point + vec)); } BPoint CubicBezierSpline::getPoint(int ix, int normalisedWidth, int normalisedHeight, bool invertHeight) { BPoint p = m_points.at(ix); for (int i = 0; i < 3; ++i) { p[i].rx() *= normalisedWidth; p[i].ry() *= normalisedHeight; if (invertHeight) { p[i].ry() = normalisedHeight - p[i].y(); } } return p; } void CubicBezierSpline::validatePoints() { BPoint p1, p2; for (int i = 0; i < m_points.count() - 1; ++i) { p1 = m_points.at(i); p2 = m_points.at(i + 1); p1.h2.setX(qBound(p1.p.x(), p1.h2.x(), p2.p.x())); p2.h1.setX(qBound(p1.p.x(), p2.h1.x(), p2.p.x())); m_points[i] = p1; m_points[i + 1] = p2; } } void CubicBezierSpline::keepSorted() { qSort(m_points.begin(), m_points.end(), pointLessThan); } int CubicBezierSpline::indexOf(const BPoint &p) { if (m_points.indexOf(p) == -1) { // point changed during validation process for (int i = 0; i < m_points.count(); ++i) { // this condition should be sufficient, too if (m_points.at(i).p == p.p) { return i; } } } else { return m_points.indexOf(p); } return -1; } int CubicBezierSpline::count() const { return m_points.size(); } QList CubicBezierSpline::getPoints() const { return m_points; } std::pair CubicBezierSpline::closestPoint(const QPointF &p) const { double nearestDistanceSquared = 1e100; BPoint::PointType selectedPoint = BPoint::PointType::P; int nearestIndex = -1; int i = 0; // find out distance using the Pythagorean theorem for (const auto &point : m_points) { for (int j = 0; j < 3; ++j) { double dx = point[j].x() - p.x(); double dy = point[j].y() - p.y(); double distanceSquared = dx * dx + dy * dy; if (distanceSquared < nearestDistanceSquared) { nearestIndex = i; nearestDistanceSquared = distanceSquared; selectedPoint = (BPoint::PointType)j; } } ++i; } return {nearestIndex, selectedPoint}; } diff --git a/src/assets/view/widgets/curves/bezier/cubicbezierspline.h b/src/assets/view/widgets/curves/bezier/cubicbezierspline.h index adce1a1f9..d85440228 100644 --- a/src/assets/view/widgets/curves/bezier/cubicbezierspline.h +++ b/src/assets/view/widgets/curves/bezier/cubicbezierspline.h @@ -1,92 +1,92 @@ /*************************************************************************** * Copyright (C) 2010 by Till Theato (root@ttill.de) * * 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 CUBICBEZIERSPLINE_H #define CUBICBEZIERSPLINE_H #include "bpoint.h" #include #include #include class CubicBezierSpline { public: - typedef BPoint Point_t; + using Point_t = BPoint; explicit CubicBezierSpline(); CubicBezierSpline(const CubicBezierSpline &spline); CubicBezierSpline &operator=(const CubicBezierSpline &spline); /** @brief Loads the points from the string @param spline. * * x, y values have to be separated with a ';' * handles and points with a '#' * and the nodes with a '|' * So that you get: h1x;h1y#px;py#h2x;h2y|h1x;h1y#... */ void fromString(const QString &spline); /** @brief Returns the points stored in a string. * * x, y values have are separated with a ';' * handles and points with a '#' * and the nodes with a '|' * So that you get: h1x;h1y#px;py#h2x;h2y|h1x;h1y#... */ QString toString() const; /** @brief Returns a list of the points defining the spline. */ QList points() const; /** @brief Returns the number of points in the spline.*/ int count() const; /** @brief Sets the point at index @param ix to @param point and returns its index (it might have changed during validation). */ int setPoint(int ix, const BPoint &point); /** @brief Adds @param point and returns its index. */ int addPoint(const BPoint &point); /** @brief Add a point based on a position @param point only This will try to compute relevant handles based on neihbouring points Return the index of the added point. */ int addPoint(const QPointF &point); /** @brief Removes the point at @param ix. */ void removePoint(int ix); /** @brief Returns the point at @param ix. * @param ix Index of the point * @param normalisedWidth (default = 1) Will be multiplied will all x values to change the range from 0-1 into another one * @param normalisedHeight (default = 1) Will be multiplied will all y values to change the range from 0-1 into another one * @param invertHeight (default = false) true => y = 0 is at the very top */ BPoint getPoint(int ix, int normalisedWidth = 1, int normalisedHeight = 1, bool invertHeight = false); /** @brief Returns the closest point to @param p, represented by its index and type (center or handle) */ std::pair closestPoint(const QPointF &p) const; QList getPoints() const; private: void validatePoints(); void keepSorted(); int indexOf(const BPoint &p); QList m_points; }; #endif diff --git a/src/assets/view/widgets/curves/cubic/kis_cubic_curve.cpp b/src/assets/view/widgets/curves/cubic/kis_cubic_curve.cpp index 8300f9f77..9dedf7e02 100644 --- a/src/assets/view/widgets/curves/cubic/kis_cubic_curve.cpp +++ b/src/assets/view/widgets/curves/cubic/kis_cubic_curve.cpp @@ -1,446 +1,446 @@ /* * Copyright (c) 2005 Casper Boemann * Copyright (c) 2009 Dmitry Kazakov * Copyright (c) 2010 Cyrille Berger * * 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 "kis_cubic_curve.h" #include #include #include #include template class KisTridiagonalSystem { /* * e.g. * |b0 c0 0 0 0| |x0| |f0| * |a0 b1 c1 0 0| |x1| |f1| * |0 a1 b2 c2 0|*|x2|=|f2| * |0 0 a2 b3 c3| |x3| |f3| * |0 0 0 a3 b4| |x4| |f4| */ public: /** * @return - vector that is storing x[] */ static QVector calculate(QList &a, QList &b, QList &c, QList &f) { QVector x; QVector alpha; QVector beta; int i; int size = b.size(); Q_ASSERT(a.size() == size - 1 && c.size() == size - 1 && f.size() == size); x.resize(size); /** * Check for special case when * order of the matrix is equal to 1 */ if (size == 1) { x[0] = f[0] / b[0]; return x; } /** * Common case */ alpha.resize(size); beta.resize(size); alpha[1] = -c[0] / b[0]; beta[1] = f[0] / b[0]; for (i = 1; i < size - 1; ++i) { alpha[i + 1] = -c[i] / (a[i - 1] * alpha[i] + b[i]); beta[i + 1] = (f[i] - a[i - 1] * beta[i]) / (a[i - 1] * alpha[i] + b[i]); } x.last() = (f.last() - a.last() * beta.last()) / (b.last() + a.last() * alpha.last()); for (i = size - 2; i >= 0; --i) { x[i] = alpha[i + 1] * x[i + 1] + beta[i + 1]; } return x; } }; template class KisCubicSpline { /** * s[i](x)=a[i] + * b[i] * (x-x[i]) + * 1/2 * c[i] * (x-x[i])^2 + * 1/6 * d[i] * (x-x[i])^3 * * h[i]=x[i+1]-x[i] * */ protected: QList m_a; QVector m_b; QVector m_c; QVector m_d; QVector m_h; T m_begin; T m_end; - int m_intervals; + int m_intervals{0}; public: KisCubicSpline() : m_begin(0) , m_end(0) - , m_intervals(0) + { } explicit KisCubicSpline(const QList &a) : m_begin(0) , m_end(0) , m_intervals(0) { createSpline(a); } /** * Create new spline and precalculate some values * for future * * @a - base points of the spline */ void createSpline(const QList &a) { int intervals = m_intervals = a.size() - 1; int i; m_begin = a.constFirst().x(); m_end = a.last().x(); m_a.clear(); m_b.resize(intervals); m_c.clear(); m_d.resize(intervals); m_h.resize(intervals); for (i = 0; i < intervals; ++i) { m_h[i] = a[i + 1].x() - a[i].x(); m_a.append(a[i].y()); } m_a.append(a.last().y()); QList tri_b; QList tri_f; QList tri_a; /* equals to @tri_c */ for (i = 0; i < intervals - 1; ++i) { tri_b.append(2. * (m_h[i] + m_h[i + 1])); tri_f.append(6. * ((m_a[i + 2] - m_a[i + 1]) / m_h[i + 1] - (m_a[i + 1] - m_a[i]) / m_h[i])); } for (i = 1; i < intervals - 1; ++i) { tri_a.append(m_h[i]); } if (intervals > 1) { KisTridiagonalSystem tridia; m_c = tridia.calculate(tri_a, tri_b, tri_a, tri_f); } m_c.prepend(0); m_c.append(0); for (i = 0; i < intervals; ++i) { m_d[i] = (m_c[i + 1] - m_c[i]) / m_h[i]; } for (i = 0; i < intervals; ++i) { m_b[i] = -0.5 * (m_c[i] * m_h[i]) - (1 / 6.0) * (m_d[i] * m_h[i] * m_h[i]) + (m_a[i + 1] - m_a[i]) / m_h[i]; } } /** * Get value of precalculated spline in the point @x */ T getValue(T x) const { T x0; int i = findRegion(x, x0); /* TODO: check for asm equivalent */ return m_a[i] + m_b[i] * (x - x0) + 0.5 * m_c[i] * (x - x0) * (x - x0) + (1 / 6.0) * m_d[i] * (x - x0) * (x - x0) * (x - x0); } T begin() const { return m_begin; } T end() const { return m_end; } protected: /** * findRegion - Searches for the region containing @x * @x0 - out parameter, containing beginning of the region * @return - index of the region */ int findRegion(T x, T &x0) const { int i; x0 = m_begin; for (i = 0; i < m_intervals; ++i) { if (x >= x0 && x < x0 + m_h[i]) { return i; } x0 += m_h[i]; } if (x >= x0) { x0 -= m_h[m_intervals - 1]; return m_intervals - 1; } qDebug("X value: %f\n", x); qDebug("m_begin: %f\n", m_begin); qDebug("m_end : %f\n", m_end); Q_ASSERT_X(0, "findRegion", "X value is outside regions"); /* **never reached** */ return -1; } }; static bool pointLessThan(const QPointF &a, const QPointF &b) { return a.x() < b.x(); } struct KisCubicCurve::Data : public QSharedData { Data() { init(); } Data(const Data &data) : QSharedData() { init(); points = data.points; } void init() { validSpline = false; validU16Transfer = false; validFTransfer = false; } ~Data() = default; mutable KisCubicSpline spline; QList points; mutable bool validSpline; mutable QVector u16Transfer; mutable bool validU16Transfer; mutable QVector fTransfer; mutable bool validFTransfer; void updateSpline(); void keepSorted(); qreal value(qreal x); void invalidate(); template void updateTransfer(QVector<_T_> *transfer, bool &valid, _T2_ min, _T2_ max, int size); }; void KisCubicCurve::Data::updateSpline() { if (validSpline) { return; } validSpline = true; spline.createSpline(points); } void KisCubicCurve::Data::invalidate() { validSpline = false; validFTransfer = false; validU16Transfer = false; } void KisCubicCurve::Data::keepSorted() { qSort(points.begin(), points.end(), pointLessThan); } qreal KisCubicCurve::Data::value(qreal x) { updateSpline(); /* Automatically extend non-existing parts of the curve * (e.g. before the first point) and cut off big y-values */ x = qBound(spline.begin(), x, spline.end()); qreal y = spline.getValue(x); return qBound((qreal)0.0, y, (qreal)1.0); } template void KisCubicCurve::Data::updateTransfer(QVector<_T_> *transfer, bool &valid, _T2_ min, _T2_ max, int size) { if (!valid || transfer->size() != size) { if (transfer->size() != size) { transfer->resize(size); } qreal end = 1.0 / (size - 1); for (int i = 0; i < size; ++i) { /* Direct uncached version */ _T2_ val = value(i * end) * max; val = qBound(min, val, max); (*transfer)[i] = val; } valid = true; } } struct KisCubicCurve::Private { QSharedDataPointer data; }; KisCubicCurve::KisCubicCurve() : d(new Private) { d->data = new Data; QPointF p; p.rx() = 0.0; p.ry() = 0.0; d->data->points.append(p); p.rx() = 1.0; p.ry() = 1.0; d->data->points.append(p); } KisCubicCurve::KisCubicCurve(const QList &points) : d(new Private) { d->data = new Data; d->data->points = points; d->data->keepSorted(); } KisCubicCurve::KisCubicCurve(const KisCubicCurve &curve) : d(new Private(*curve.d)) { } KisCubicCurve::~KisCubicCurve() { delete d; } KisCubicCurve &KisCubicCurve::operator=(const KisCubicCurve &curve) { *d = *curve.d; return *this; } bool KisCubicCurve::operator==(const KisCubicCurve &curve) const { if (d->data == curve.d->data) { return true; } return d->data->points == curve.d->data->points; } qreal KisCubicCurve::value(qreal x) const { return d->data->value(x); } QList KisCubicCurve::points() const { return d->data->points; } void KisCubicCurve::setPoints(const QList &points) { d->data.detach(); d->data->points = points; d->data->invalidate(); } int KisCubicCurve::setPoint(int idx, const QPointF &point) { d->data.detach(); d->data->points[idx] = point; d->data->keepSorted(); d->data->invalidate(); return idx; } int KisCubicCurve::addPoint(const QPointF &point) { d->data.detach(); d->data->points.append(point); d->data->keepSorted(); d->data->invalidate(); return d->data->points.indexOf(point); } void KisCubicCurve::removePoint(int idx) { d->data.detach(); d->data->points.removeAt(idx); d->data->invalidate(); } const QString KisCubicCurve::toString() const { QString sCurve; for (const QPointF &pair : d->data->points) { sCurve += QString::number(pair.x()); sCurve += QStringLiteral("/"); sCurve += QString::number(pair.y()); sCurve += QStringLiteral(";"); } return sCurve; } void KisCubicCurve::fromString(const QString &string) { const QStringList data = string.split(QLatin1Char(';')); QList points; for (const QString &pair : data) { if (pair.indexOf('/') > -1) { QPointF p; p.rx() = pair.section(QLatin1Char('/'), 0, 0).toDouble(); p.ry() = pair.section(QLatin1Char('/'), 1, 1).toDouble(); points.append(p); } } setPoints(points); } int KisCubicCurve::count() const { return d->data->points.size(); } QPointF KisCubicCurve::getPoint(int ix, int normalisedWidth, int normalisedHeight, bool invertHeight) { QPointF p = d->data->points.at(ix); p.rx() *= normalisedWidth; p.ry() *= normalisedHeight; if (invertHeight) { p.ry() = normalisedHeight - p.y(); } return p; } diff --git a/src/assets/view/widgets/curves/cubic/kis_cubic_curve.h b/src/assets/view/widgets/curves/cubic/kis_cubic_curve.h index 1ddb88692..8c1dbdded 100644 --- a/src/assets/view/widgets/curves/cubic/kis_cubic_curve.h +++ b/src/assets/view/widgets/curves/cubic/kis_cubic_curve.h @@ -1,78 +1,78 @@ /* * Copyright (c) 2010 Cyrille Berger * * 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 KIS_CUBIC_CURVE_H #define KIS_CUBIC_CURVE_H #include #include class QPointF; /** * Hold the data for a cubic curve. */ class KisCubicCurve { public: - typedef QPointF Point_t; + using Point_t = QPointF; KisCubicCurve(); explicit KisCubicCurve(const QList &points); explicit KisCubicCurve(const QVector &points); KisCubicCurve(const KisCubicCurve &curve); ~KisCubicCurve(); KisCubicCurve &operator=(const KisCubicCurve &curve); bool operator==(const KisCubicCurve &curve) const; public: qreal value(qreal x) const; QList points() const; void setPoints(const QList &points); int setPoint(int idx, const QPointF &point); /** * Add a point to the curve, the list of point is always sorted. * @return the index of the inserted point */ int addPoint(const QPointF &point); void removePoint(int idx); /** @brief Returns the number of points on the curve */ int count() const; /** @brief Returns the point at @param ix. * @param ix Index of the point * @param normalisedWidth (default = 1) Will be multiplied will all x values to change the range from 0-1 into another one * @param normalisedHeight (default = 1) Will be multiplied will all y values to change the range from 0-1 into another one * @param invertHeight (default = false) true => y = 0 is at the very top */ QPointF getPoint(int ix, int normalisedWidth = 1, int normalisedHeight = 1, bool invertHeight = false); public: QVector uint16Transfer(int size = 256) const; QVector floatTransfer(int size = 256) const; public: const QString toString() const; void fromString(const QString &); private: struct Data; struct Private; Private *const d; // NOLINT }; #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 ee89eee0c..167e80834 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_pixmapIsDirty = false; m_pixmapCache = nullptr; m_maxPoints = 0; m_curve = KisCubicCurve(); update(); } -KisCurveWidget::~KisCurveWidget() {} +KisCurveWidget::~KisCurveWidget() = default; QSize KisCurveWidget::sizeHint() const { - return QSize(500, 500); + return {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 = curve; } QList KisCurveWidget::getPoints() const { return m_curve.points(); } diff --git a/src/assets/view/widgets/curves/cubic/kis_curve_widget.h b/src/assets/view/widgets/curves/cubic/kis_curve_widget.h index 85387b9da..32f3ca9da 100644 --- a/src/assets/view/widgets/curves/cubic/kis_curve_widget.h +++ b/src/assets/view/widgets/curves/cubic/kis_curve_widget.h @@ -1,106 +1,106 @@ /* * Copyright (c) 2005 Casper 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. */ #ifndef KIS_CURVE_WIDGET_H #define KIS_CURVE_WIDGET_H // Qt includes. #include #include "../abstractcurvewidget.h" #include "colortools.h" #include "kis_cubic_curve.h" class QEvent; class QMouseEvent; class QObject; class QPaintEvent; class QPixmap; class QSpinBox; /** * KisCurveWidget is a widget that shows a single curve that can be edited * by the user. The user can grab the curve and move it; this creates * a new control point. Control points can be deleted by selecting a point * and pressing the delete key. * * (From: http://techbase.kde.org/Projects/Widgets_and_Classes#KisCurveWidget) * KisCurveWidget allows editing of spline based y=f(x) curves. Handy for cases * where you want the user to control such things as tablet pressure * response, color transformations, acceleration by time, aeroplane lift *by angle of attack. */ class KisCurveWidget : public AbstractCurveWidget { Q_OBJECT public: - typedef QPointF Point_t; + using Point_t = QPointF; /** * Create a new curve widget with a default curve, that is a straight * line from bottom-left to top-right. */ explicit KisCurveWidget(QWidget *parent = nullptr); - virtual ~KisCurveWidget(); + ~KisCurveWidget() override; QSize sizeHint() const override; protected: void paintEvent(QPaintEvent *) override; void mousePressEvent(QMouseEvent *e) override; void mouseMoveEvent(QMouseEvent *e) override; public: /** * Handy function that creates new point in the middle * of the curve and sets focus on the m_intIn field, * so the user can move this point anywhere in a moment */ void addPointInTheMiddle(); void setCurve(KisCubicCurve &&curve); QList getPoints() const override; private: double io2sp(int x) const; int sp2io(double x) const; bool jumpOverExistingPoints(QPointF &pt, int skipIndex); int nearestPointInRange(QPointF pt, int wWidth, int wHeight) const; /* Dragging variables */ double m_grabOffsetX; double m_grabOffsetY; double m_grabOriginalX; double m_grabOriginalY; QPointF m_draggedAwayPoint; int m_draggedAwayPointIndex; bool m_guideVisible; QColor m_colorGuide; /* Working range of them */ int m_inOutMin; int m_inOutMax; }; #endif /* KIS_CURVE_WIDGET_H */ diff --git a/src/assets/view/widgets/curves/curveparamwidget.h b/src/assets/view/widgets/curves/curveparamwidget.h index 45e5d74f2..62f2a3429 100644 --- a/src/assets/view/widgets/curves/curveparamwidget.h +++ b/src/assets/view/widgets/curves/curveparamwidget.h @@ -1,105 +1,106 @@ /*************************************************************************** * Copyright (C) 2016 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 CURVEPARAMWIDGET_H #define CURVEPARAMWIDGET_H #include "../abstractparamwidget.hpp" #include "bezier/beziersplineeditor.h" #include "cubic/kis_curve_widget.h" #include "ui_bezierspline_ui.h" #include "widgets/dragvalue.h" template class ValueLabel; /** @brief Class representing a curve and some additional controls */ template class CurveParamWidget : public AbstractParamWidget { public: - virtual ~CurveParamWidget(){}; + ~CurveParamWidget() override = default; + ; CurveParamWidget(std::shared_ptr model, QModelIndex index, QWidget *parent); enum class CurveModes { Red = 0, Green = 1, Blue = 2, Luma = 3, Alpha = 4, RGB = 5, Hue = 6, Saturation = 7 }; /** @brief sets the mode of the curve. This affects the background that is displayed. * The list of available modes depends on the CurveWidget that we have */ void setMode(CurveModes mode); /** @brief Stringify the content of the curve */ QString toString() const; using Point_t = typename CurveWidget_t::Point_t; /** @brief returns the list of points on the curve */ QList getPoints() { return m_edit->getPoints(); } /** @brief Set the maximal number of points on the curve. This function is only available for KisCurveWidget. */ void setMaxPoints(int max); /** @brief Helper function to convert a mode to the corresponding ColorsRGB value. This avoids using potentially non consistent intermediate cast to int */ static ColorTools::ColorsRGB modeToColorsRGB(CurveModes mode); protected: void deleteIrrelevantItems(); void setupLayoutPoint(); void setupLayoutHandles(); void slotGridChange(); void slotShowPixmap(bool show); void slotUpdatePointEntries(const BPoint &p, bool extremal); void slotUpdatePointEntries(const QPointF &p, bool extremal); void slotUpdatePointP(double /*value*/, bool final); void slotUpdatePointH1(double /*value*/, bool final); void slotUpdatePointH2(double /*value*/, bool final); void slotSetHandlesLinked(bool linked); void slotShowAllHandles(bool show); public slots: /** @brief Toggle the comments on or off */ void slotShowComment(bool show) override; /** @brief refresh the properties to reflect changes in the model */ void slotRefresh() override; private: Ui::BezierSpline_UI m_ui; DragValue *m_pX; DragValue *m_pY; DragValue *m_h1X; DragValue *m_h1Y; DragValue *m_h2X; DragValue *m_h2Y; CurveWidget_t *m_edit; CurveModes m_mode; bool m_showPixmap; ValueLabel *m_leftParam, *m_bottomParam; }; #include "curveparamwidget.ipp" #endif diff --git a/src/assets/view/widgets/curves/curveparamwidget.ipp b/src/assets/view/widgets/curves/curveparamwidget.ipp index 76b25b21c..2f40d5273 100644 --- a/src/assets/view/widgets/curves/curveparamwidget.ipp +++ b/src/assets/view/widgets/curves/curveparamwidget.ipp @@ -1,428 +1,428 @@ /*************************************************************************** * Copyright (C) 2016 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 "bezier/beziersplineeditor.h" #include "colortools.h" #include "cubic/kis_curve_widget.h" #include "kdenlivesettings.h" #include "widgets/dragvalue.h" #include /*@brief this label is a pixmap corresponding to a legend of the axis*/ template class ValueLabel : public QLabel { public: /**@brief Creates the widget @param isVert This parameter is true if the widget is vertical @param mode This is the original mode @param parent Parent of the widget */ ValueLabel(bool isVert, typename CurveParamWidget::CurveModes mode, QWidget *parent) : QLabel(parent) , m_mode(mode) , m_isVert(isVert) { if (m_isVert) { setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred); setFixedWidth(10); } else { setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); setFixedHeight(10); } setScaledContents(true); } public slots: void setMode(typename CurveParamWidget::CurveModes m) { m_mode = m; createPixmap(); } private: using CurveModes = typename CurveParamWidget::CurveModes; void createPixmap() { QTransform t; QSize s = size(); if (!m_isVert) { t.rotate(90); s.setHeight(size().width()); s.setWidth(size().height()); } if (m_mode == CurveModes::Hue) { setPixmap(QPixmap::fromImage(ColorTools::hsvCurvePlane(s, QColor::fromHsv(200, 200, 200), ColorTools::COM_H, ColorTools::COM_H)).transformed(t)); } else if (m_mode == CurveModes::Saturation) { setPixmap(QPixmap()); } else { auto color = CurveParamWidget::modeToColorsRGB(m_mode); setPixmap(QPixmap::fromImage(ColorTools::rgbCurveLine(s, color, palette().background().color().rgb())).transformed(t)); } } typename CurveParamWidget::CurveModes m_mode; bool m_isVert; }; template <> void CurveParamWidget::slotUpdatePointP(double, bool final) { m_edit->updateCurrentPoint(QPointF(m_pX->value(), m_pY->value()), final); } template <> void CurveParamWidget::slotUpdatePointP(double, bool final) { BPoint p = m_edit->getCurrentPoint(); p.setP(QPointF(m_pX->value(), m_pY->value())); m_edit->updateCurrentPoint(p, final); } template <> void CurveParamWidget::slotUpdatePointH1(double /*value*/, bool final) { BPoint p = m_edit->getCurrentPoint(); p.setH1(QPointF(m_h1X->value(), m_h1Y->value())); m_edit->updateCurrentPoint(p, final); } template void CurveParamWidget::slotUpdatePointH1(double /*value*/, bool /*final*/) {} template <> void CurveParamWidget::slotUpdatePointH2(double /*value*/, bool final) { BPoint p = m_edit->getCurrentPoint(); p.setH2(QPointF(m_h2X->value(), m_h2Y->value())); m_edit->updateCurrentPoint(p, final); } template void CurveParamWidget::slotUpdatePointH2(double /*value*/, bool /*final*/) {} template <> void CurveParamWidget::slotSetHandlesLinked(bool linked) { BPoint p = m_edit->getCurrentPoint(); p.setHandlesLinked(linked); m_edit->updateCurrentPoint(p); } template void CurveParamWidget::slotSetHandlesLinked(bool /*linked*/) {} template <> void CurveParamWidget::slotShowAllHandles(bool show) { m_edit->setShowAllHandles(show); KdenliveSettings::setBezier_showallhandles(show); } template void CurveParamWidget::slotShowAllHandles(bool /*show*/) {} template CurveParamWidget::CurveParamWidget(std::shared_ptr model, QModelIndex index, QWidget *parent) : AbstractParamWidget(std::move(model), index, parent) , m_mode(CurveModes::Luma) , m_showPixmap(KdenliveSettings::bezier_showpixmap()) { // construct curve editor m_edit = new CurveWidget_t(this); connect(m_edit, static_cast(&CurveWidget_t::currentPoint), this, static_cast::*)(const Point_t &, bool)>(&CurveParamWidget::slotUpdatePointEntries)); // construct and fill layout - QVBoxLayout *layout = new QVBoxLayout(this); + auto *layout = new QVBoxLayout(this); layout->setSpacing(0); setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); layout->addWidget(m_edit); m_leftParam = new ValueLabel(true, m_mode, this); m_leftParam->setFrameStyle(QFrame::StyledPanel | QFrame::Plain); m_leftParam->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); m_bottomParam = new ValueLabel(false, m_mode, this); m_bottomParam->setFrameStyle(QFrame::StyledPanel | QFrame::Plain); m_bottomParam->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed); // horizontal layout to make sure that everything is centered - QHBoxLayout *horiz_layout = new QHBoxLayout; + auto *horiz_layout = new QHBoxLayout; horiz_layout->addWidget(m_leftParam); horiz_layout->addWidget(m_bottomParam); layout->addLayout(horiz_layout); - QWidget *widget = new QWidget(this); + auto *widget = new QWidget(this); widget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); m_ui.setupUi(widget); layout->addWidget(widget); // set up icons and initial button states m_ui.buttonLinkHandles->setIcon(QIcon::fromTheme(QStringLiteral("edit-link"))); m_ui.buttonDeletePoint->setIcon(QIcon::fromTheme(QStringLiteral("list-remove"))); m_ui.buttonZoomIn->setIcon(QIcon::fromTheme(QStringLiteral("zoom-in"))); m_ui.buttonZoomOut->setIcon(QIcon::fromTheme(QStringLiteral("zoom-out"))); m_ui.buttonGridChange->setIcon(QIcon::fromTheme(QStringLiteral("view-grid"))); m_ui.buttonShowPixmap->setIcon(QIcon(QPixmap::fromImage(ColorTools::rgbCurvePlane(QSize(16, 16), ColorTools::ColorsRGB::Luma, (float)0.8)))); m_ui.buttonResetSpline->setIcon(QIcon::fromTheme(QStringLiteral("view-refresh"))); m_ui.buttonShowAllHandles->setIcon(QIcon::fromTheme(QStringLiteral("draw-bezier-curves"))); m_ui.widgetPoint->setEnabled(false); m_edit->setGridLines(KdenliveSettings::bezier_gridlines()); m_ui.buttonShowPixmap->setChecked(KdenliveSettings::bezier_showpixmap()); m_ui.buttonShowAllHandles->setChecked(KdenliveSettings::bezier_showallhandles()); slotShowAllHandles(KdenliveSettings::bezier_showallhandles()); // connect buttons to their slots connect(m_ui.buttonLinkHandles, &QAbstractButton::toggled, this, &CurveParamWidget::slotSetHandlesLinked); connect(m_ui.buttonDeletePoint, &QAbstractButton::clicked, m_edit, &CurveWidget_t::slotDeleteCurrentPoint); connect(m_ui.buttonZoomIn, &QAbstractButton::clicked, m_edit, &CurveWidget_t::slotZoomIn); connect(m_ui.buttonZoomOut, &QAbstractButton::clicked, m_edit, &CurveWidget_t::slotZoomOut); connect(m_ui.buttonGridChange, &QAbstractButton::clicked, this, &CurveParamWidget::slotGridChange); connect(m_ui.buttonShowPixmap, &QAbstractButton::toggled, this, &CurveParamWidget::slotShowPixmap); connect(m_ui.buttonResetSpline, &QAbstractButton::clicked, m_edit, &CurveWidget_t::reset); connect(m_ui.buttonShowAllHandles, &QAbstractButton::toggled, this, &CurveParamWidget::slotShowAllHandles); setupLayoutPoint(); setupLayoutHandles(); slotRefresh(); deleteIrrelevantItems(); // emit the signal of the base class when appropriate connect(m_edit, &CurveWidget_t::modified, [this]() { emit valueChanged(m_index, m_edit->toString(), true); }); } template <> void CurveParamWidget::deleteIrrelevantItems() { m_ui.gridLayout->removeWidget(m_ui.buttonShowAllHandles); delete m_ui.buttonShowAllHandles; } template void CurveParamWidget::deleteIrrelevantItems() { // Nothing to do in general } template void CurveParamWidget::setupLayoutPoint() { m_pX = new DragValue(i18n("In"), 0, 3, 0, 1, -1, QString(), false, this); m_pX->setStep(0.001); m_pY = new DragValue(i18n("Out"), 0, 3, 0, 1, -1, QString(), false, this); m_pY->setStep(0.001); m_ui.layoutP->addWidget(m_pX); m_ui.layoutP->addWidget(m_pY); connect(m_pX, &DragValue::valueChanged, this, &CurveParamWidget::slotUpdatePointP); connect(m_pY, &DragValue::valueChanged, this, &CurveParamWidget::slotUpdatePointP); } template <> void CurveParamWidget::setupLayoutHandles() { m_h1X = new DragValue(i18n("X"), 0, 3, -2, 2, -1, QString(), false, this); m_h1X->setStep(0.001); m_h1Y = new DragValue(i18n("Y"), 0, 3, -2, 2, -1, QString(), false, this); m_h1Y->setStep(0.001); m_h2X = new DragValue(i18n("X"), 0, 3, -2, 2, -1, QString(), false, this); m_h2X->setStep(0.001); m_h2Y = new DragValue(i18n("Y"), 0, 3, -2, 2, -1, QString(), false, this); m_h2Y->setStep(0.001); m_ui.layoutH1->addWidget(new QLabel(i18n("Handle 1:"))); m_ui.layoutH1->addWidget(m_h1X); m_ui.layoutH1->addWidget(m_h1Y); m_ui.layoutH2->addWidget(new QLabel(i18n("Handle 2:"))); m_ui.layoutH2->addWidget(m_h2X); m_ui.layoutH2->addWidget(m_h2Y); connect(m_h1X, &DragValue::valueChanged, this, &CurveParamWidget::slotUpdatePointH1); connect(m_h1Y, &DragValue::valueChanged, this, &CurveParamWidget::slotUpdatePointH1); connect(m_h2X, &DragValue::valueChanged, this, &CurveParamWidget::slotUpdatePointH2); connect(m_h2Y, &DragValue::valueChanged, this, &CurveParamWidget::slotUpdatePointH2); } template void CurveParamWidget::setupLayoutHandles() { // Nothing to do in general } template QString CurveParamWidget::toString() const { return m_edit->toString(); } template void CurveParamWidget::setMode(CurveModes mode) { if (m_mode != mode) { m_mode = mode; if (m_showPixmap) { slotShowPixmap(true); } m_leftParam->setMode(mode); m_bottomParam->setMode(mode); } } template void CurveParamWidget::slotGridChange() { m_edit->setGridLines((m_edit->gridLines() + 1) % 9); KdenliveSettings::setBezier_gridlines(m_edit->gridLines()); } template ColorTools::ColorsRGB CurveParamWidget::modeToColorsRGB(CurveModes mode) { switch (mode) { case CurveModes::Red: return ColorTools::ColorsRGB::R; case CurveModes::Green: return ColorTools::ColorsRGB::G; case CurveModes::Blue: return ColorTools::ColorsRGB::B; case CurveModes::Luma: return ColorTools::ColorsRGB::Luma; case CurveModes::Alpha: return ColorTools::ColorsRGB::A; case CurveModes::RGB: case CurveModes::Hue: case CurveModes::Saturation: default: return ColorTools::ColorsRGB::RGB; } return ColorTools::ColorsRGB::RGB; } template void CurveParamWidget::slotShowPixmap(bool show) { m_showPixmap = show; KdenliveSettings::setBezier_showpixmap(show); if (show) { if (m_mode == CurveModes::Hue) { m_edit->setPixmap( QPixmap::fromImage(ColorTools::hsvCurvePlane(m_edit->size(), QColor::fromHsv(200, 200, 200), ColorTools::COM_H, ColorTools::COM_H))); } else if (m_mode == CurveModes::Saturation) { m_edit->setPixmap(QPixmap()); } else { auto color = modeToColorsRGB(m_mode); m_edit->setPixmap(QPixmap::fromImage(ColorTools::rgbCurvePlane(m_edit->size(), color, 1, palette().background().color().rgb()))); } } else { m_edit->setPixmap(QPixmap()); } } template <> void CurveParamWidget::slotUpdatePointEntries(const BPoint &p, bool extremal) { blockSignals(true); if (p == BPoint()) { m_ui.widgetPoint->setEnabled(false); } else { m_ui.widgetPoint->setEnabled(true); // disable irrelevant buttons if the point is extremal m_pX->setEnabled(!extremal); m_ui.buttonDeletePoint->setEnabled(!extremal); m_ui.buttonLinkHandles->setEnabled(!extremal); if (extremal && p.p.x() + 1e-4 >= 1.00) { // last point m_h2X->setEnabled(false); m_h2Y->setEnabled(false); } else { m_h2X->setEnabled(true); m_h2Y->setEnabled(true); } if (extremal && p.p.x() <= 0.01) { // first point m_h1X->setEnabled(false); m_h1Y->setEnabled(false); } else { m_h1X->setEnabled(true); m_h1Y->setEnabled(true); } for (auto elem : {m_pX, m_pY, m_h1X, m_h1Y, m_h2X, m_h2Y}) { elem->blockSignals(true); } m_pX->setValue(p.p.x()); m_pY->setValue(p.p.y()); m_h1X->setValue(p.h1.x()); m_h1Y->setValue(p.h1.y()); m_h2X->setValue(p.h2.x()); m_h2Y->setValue(p.h2.y()); for (auto elem : {m_pX, m_pY, m_h1X, m_h1Y, m_h2X, m_h2Y}) { elem->blockSignals(false); } m_ui.buttonLinkHandles->setChecked(p.handlesLinked); } blockSignals(false); } template void CurveParamWidget::slotUpdatePointEntries(const BPoint &p, bool extremal) { Q_UNUSED(p); Q_UNUSED(extremal); // Wrong slot called in curve widget Q_ASSERT(false); } template <> void CurveParamWidget::slotUpdatePointEntries(const QPointF &p, bool extremal) { blockSignals(true); if (p == QPointF()) { m_ui.widgetPoint->setEnabled(false); } else { m_ui.widgetPoint->setEnabled(true); // disable irrelevant buttons if the point is extremal m_pX->setEnabled(!extremal); m_ui.buttonDeletePoint->setEnabled(!extremal); for (auto elem : {m_pX, m_pY}) { elem->blockSignals(true); } m_pX->setValue(p.x()); m_pY->setValue(p.y()); for (auto elem : {m_pX, m_pY}) { elem->blockSignals(false); } } blockSignals(false); } template void CurveParamWidget::slotUpdatePointEntries(const QPointF &p, bool extremal) { Q_UNUSED(p); Q_UNUSED(extremal); // Wrong slot called in curve widget Q_ASSERT(false); } template void CurveParamWidget::slotShowComment(bool show) { Q_UNUSED(show); } template void CurveParamWidget::slotRefresh() { if (m_model->data(m_index, AssetParameterModel::TypeRole).template value() == ParamType::Curve) { QList points; QLocale locale; // Rounding gives really weird results. (int) (10 * 0.3) gives 2! So for now, add 0.5 to get correct result int number = m_model->data(m_index, AssetParameterModel::Enum3Role).toDouble() * 10 + 0.5; int start = m_model->data(m_index, AssetParameterModel::MinRole).toInt(); // for the curve, inpoints are numbered: 6, 8, 10, 12, 14 // outpoints, 7, 9, 11, 13,15 so we need to deduce these enums int inRef = (int)AssetParameterModel::Enum6Role + 2 * (start - 1); int outRef = (int)AssetParameterModel::Enum7Role + 2 * (start - 1); for (int j = start; j <= number; ++j) { double inVal = m_model->data(m_index, (AssetParameterModel::DataRoles)inRef).toDouble(); double outVal = m_model->data(m_index, (AssetParameterModel::DataRoles)outRef).toDouble(); points << QPointF(inVal, outVal); inRef += 2; outRef += 2; } if (!points.isEmpty()) { m_edit->setFromString(KisCubicCurve(points).toString()); } } else { m_edit->setFromString(m_model->data(m_index, AssetParameterModel::ValueRole).toString()); } } template void CurveParamWidget::setMaxPoints(int max) { m_edit->setMaxPoints(max); } diff --git a/src/assets/view/widgets/geometryeditwidget.cpp b/src/assets/view/widgets/geometryeditwidget.cpp index d30bb9a5e..de063fde4 100644 --- a/src/assets/view/widgets/geometryeditwidget.cpp +++ b/src/assets/view/widgets/geometryeditwidget.cpp @@ -1,109 +1,109 @@ /*************************************************************************** * 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 "geometryeditwidget.hpp" #include "assets/model/assetparametermodel.hpp" #include "core.h" #include "kdenlivesettings.h" #include "monitor/monitor.h" #include "monitor/monitormanager.h" #include "timecodedisplay.h" #include "widgets/geometrywidget.h" #include #include #include #include GeometryEditWidget::GeometryEditWidget(std::shared_ptr model, QModelIndex index, QSize frameSize, QWidget *parent) : AbstractParamWidget(std::move(model), index, parent) { auto *layout = new QVBoxLayout(this); QString comment = m_model->data(m_index, AssetParameterModel::CommentRole).toString(); const QString value = m_model->data(m_index, AssetParameterModel::ValueRole).toString().simplified(); int start = m_model->data(m_index, AssetParameterModel::ParentInRole).toInt(); int end = start + m_model->data(m_index, AssetParameterModel::ParentDurationRole).toInt(); Mlt::Geometry geometry(value.toUtf8().data(), end, frameSize.width(), frameSize.height()); Mlt::GeometryItem item; QRect rect; if (geometry.fetch(&item, 0) == 0) { rect = QRect(item.x(), item.y(), item.w(), item.h()); } else { // Cannot read value, use random default rect = QRect(50, 50, 200, 200); } Monitor *monitor = pCore->getMonitor(m_model->monitorId); m_geom = new GeometryWidget(monitor, QPair(start, end), rect, 100, frameSize, false, m_model->data(m_index, AssetParameterModel::OpacityRole).toBool(), true, this); m_geom->setSizePolicy(QSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred)); layout->addWidget(m_geom); // emit the signal of the base class when appropriate connect(this->m_geom, &GeometryWidget::valueChanged, [this](const QString val) { emit valueChanged(m_index, val, true); }); setToolTip(comment); } -GeometryEditWidget::~GeometryEditWidget() {} +GeometryEditWidget::~GeometryEditWidget() = default; void GeometryEditWidget::slotRefresh() { const QString value = m_model->data(m_index, AssetParameterModel::ValueRole).toString().simplified(); QRect rect; QStringList vals = value.split(QLatin1Char(' ')); int start = m_model->data(m_index, AssetParameterModel::ParentInRole).toInt(); int end = start + m_model->data(m_index, AssetParameterModel::ParentDurationRole).toInt(); m_geom->slotSetRange(QPair(start, end)); if (vals.count() >= 4) { rect = QRect(vals.at(0).toInt(), vals.at(1).toInt(), vals.at(2).toInt(), vals.at(3).toInt()); m_geom->setValue(rect); } } void GeometryEditWidget::slotShowComment(bool show) { Q_UNUSED(show); } void GeometryEditWidget::monitorSeek(int pos) { // Update monitor scene for geometry params int start = m_model->data(m_index, AssetParameterModel::ParentInRole).toInt(); int end = start + m_model->data(m_index, AssetParameterModel::ParentDurationRole).toInt(); if (pos >= start && pos < end) { m_geom->connectMonitor(true); pCore->getMonitor(m_model->monitorId)->setEffectKeyframe(true); } else { m_geom->connectMonitor(false); } } void GeometryEditWidget::slotInitMonitor(bool active) { m_geom->connectMonitor(active); Monitor *monitor = pCore->getMonitor(m_model->monitorId); if (active) { monitor->setEffectKeyframe(true); connect(monitor, &Monitor::seekPosition, this, &GeometryEditWidget::monitorSeek, Qt::UniqueConnection); } else { disconnect(monitor, &Monitor::seekPosition, this, &GeometryEditWidget::monitorSeek); } } diff --git a/src/assets/view/widgets/geometryeditwidget.hpp b/src/assets/view/widgets/geometryeditwidget.hpp index 58a29d187..b5a62f86a 100644 --- a/src/assets/view/widgets/geometryeditwidget.hpp +++ b/src/assets/view/widgets/geometryeditwidget.hpp @@ -1,64 +1,64 @@ /*************************************************************************** * 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 GEOMEDITWIDGET_H #define GEOMEDITWIDGET_H #include "timecode.h" #include #include #include "abstractparamwidget.hpp" class QSlider; class GeometryWidget; /*@brief This class is used to display a parameter with time value */ class GeometryEditWidget : public AbstractParamWidget { Q_OBJECT public: /** @brief Sets up the parameter's GUI.*/ explicit GeometryEditWidget(std::shared_ptr model, QModelIndex index, QSize frameSize, QWidget *parent = nullptr); - ~GeometryEditWidget(); + ~GeometryEditWidget() override; public slots: /** @brief Toggle the comments on or off */ void slotShowComment(bool show) override; /** @brief refresh the properties to reflect changes in the model */ void slotRefresh() override; /** @brief initialize qml overlay */ void slotInitMonitor(bool active) override; private slots: /** @brief monitor seek pos changed. */ void monitorSeek(int pos); private: GeometryWidget *m_geom; }; #endif diff --git a/src/assets/view/widgets/keyframeimport.cpp b/src/assets/view/widgets/keyframeimport.cpp index fa828e113..9dadf52e4 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 #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(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(); + auto 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() {} +KeyframeImport::~KeyframeImport() = default; 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(); + auto 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/keyframeimport.h b/src/assets/view/widgets/keyframeimport.h index 883cb3dc2..717c31626 100644 --- a/src/assets/view/widgets/keyframeimport.h +++ b/src/assets/view/widgets/keyframeimport.h @@ -1,87 +1,87 @@ /*************************************************************************** * 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 . * ***************************************************************************/ #ifndef KEYFRAMEIMPORT_H #define KEYFRAMEIMPORT_H #include #include #include #include "assets/model/assetparametermodel.hpp" #include "definitions.h" #include "timecode.h" class PositionWidget; class QComboBox; class QCheckBox; class QSpinBox; class KeyframeView; namespace Mlt { class Properties; } class KeyframeImport : public QDialog { Q_OBJECT public: explicit KeyframeImport(int in, int out, const QString &animData, std::shared_ptr model, QList indexes, QWidget *parent = nullptr); - virtual ~KeyframeImport(); + ~KeyframeImport() override; QString selectedData() const; QString selectedTarget() const; private: std::shared_ptr m_model; bool m_supportsAnim; QComboBox *m_dataCombo; QLabel *m_previewLabel; PositionWidget *m_inPoint; PositionWidget *m_outPoint; PositionWidget *m_offsetPoint; QCheckBox *m_limitRange; QCheckBox *m_limitKeyframes; QSpinBox *m_limitNumber; QComboBox *m_sourceCombo; QComboBox *m_targetCombo; QComboBox *m_alignCombo; QLabel *m_sourceRangeLabel; QList m_maximas; QDoubleSpinBox m_destMin; QDoubleSpinBox m_destMax; /** @brief Contains the 4 dimensional (x,y,w,h) target parameter names / tag **/ QMap m_geometryTargets; /** @brief Contains the 1 dimensional target parameter names / tag **/ QMap m_simpleTargets; void drawKeyFrameChannels(QPixmap &pix, int in, int out, int limitKeyframes, const QColor &textColor); protected: void resizeEvent(QResizeEvent *ev) override; private slots: void updateDataDisplay(); void updateDisplay(); void updateRange(); void updateDestinationRange(); }; #endif diff --git a/src/assets/view/widgets/keyframewidget.cpp b/src/assets/view/widgets/keyframewidget.cpp index 342915db9..31cd6c87c 100644 --- a/src/assets/view/widgets/keyframewidget.cpp +++ b/src/assets/view/widgets/keyframewidget.cpp @@ -1,506 +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(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(); + auto 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(); + auto 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(); + auto type = m_model->data(w.first, AssetParameterModel::TypeRole).value(); if (type == ParamType::AnimatedRect) { ((GeometryWidget *)w.second)->connectMonitor(active); break; } } } 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); + auto *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 d2880c0a8..f030db72c 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(); + ~KeyframeWidget() override; /* @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(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/assets/view/widgets/positioneditwidget.cpp b/src/assets/view/widgets/positioneditwidget.cpp index 8d61ea17d..e5e9de026 100644 --- a/src/assets/view/widgets/positioneditwidget.cpp +++ b/src/assets/view/widgets/positioneditwidget.cpp @@ -1,135 +1,135 @@ /*************************************************************************** positionedit.cpp - description ------------------- begin : 03 Aug 2008 copyright : (C) 2008 by Marco Gittler email : g.marco@freenet.de ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "positioneditwidget.hpp" #include "assets/model/assetparametermodel.hpp" #include "core.h" #include "kdenlivesettings.h" #include "monitor/monitormanager.h" #include "timecodedisplay.h" #include #include #include PositionEditWidget::PositionEditWidget(std::shared_ptr model, QModelIndex index, QWidget *parent) : AbstractParamWidget(std::move(model), index, parent) { auto *layout = new QHBoxLayout(this); QString name = m_model->data(m_index, Qt::DisplayRole).toString(); QString comment = m_model->data(m_index, AssetParameterModel::CommentRole).toString(); QLabel *label = new QLabel(name, this); m_slider = new QSlider(Qt::Horizontal, this); m_slider->setSizePolicy(QSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred)); m_display = new TimecodeDisplay(pCore->monitorManager()->timecode(), this); m_display->setSizePolicy(QSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred)); layout->addWidget(label); layout->addWidget(m_slider); layout->addWidget(m_display); m_inverted = m_model->data(m_index, AssetParameterModel::DefaultRole).toInt() < 0; slotRefresh(); connect(m_slider, &QAbstractSlider::valueChanged, m_display, static_cast(&TimecodeDisplay::setValue)); connect(m_display, &TimecodeDisplay::timeCodeEditingFinished, m_slider, &QAbstractSlider::setValue); connect(m_slider, &QAbstractSlider::valueChanged, this, &PositionEditWidget::valueChanged); // emit the signal of the base class when appropriate connect(this->m_slider, &QAbstractSlider::valueChanged, [this](int val) { if (m_inverted) { val = m_model->data(m_index, AssetParameterModel::ParentInRole).toInt() + m_model->data(m_index, AssetParameterModel::ParentDurationRole).toInt() - val; } else if (!m_model->data(m_index, AssetParameterModel::RelativePosRole).toBool()) { val += m_model->data(m_index, AssetParameterModel::ParentInRole).toInt(); } emit AbstractParamWidget::valueChanged(m_index, QString::number(val), true); }); setToolTip(comment); } -PositionEditWidget::~PositionEditWidget() {} +PositionEditWidget::~PositionEditWidget() = default; void PositionEditWidget::updateTimecodeFormat() { m_display->slotUpdateTimeCodeFormat(); } int PositionEditWidget::getPosition() const { return m_slider->value(); } void PositionEditWidget::setPosition(int pos) { m_slider->setValue(pos); } void PositionEditWidget::slotUpdatePosition() { m_slider->blockSignals(true); m_slider->setValue(m_display->getValue()); m_slider->blockSignals(false); emit valueChanged(); } void PositionEditWidget::slotRefresh() { int min = m_model->data(m_index, AssetParameterModel::ParentInRole).toInt(); int max = min + m_model->data(m_index, AssetParameterModel::ParentDurationRole).toInt(); const QSignalBlocker blocker(m_slider); const QSignalBlocker blocker2(m_display); QVariant value = m_model->data(m_index, AssetParameterModel::ValueRole); int val; if (value.isNull()) { val = m_model->data(m_index, AssetParameterModel::DefaultRole).toInt(); if (m_inverted) { val = -val; } } else { if (value.userType() == QMetaType::QString) { val = m_model->time_to_frames(value.toString()); } else { val = value.toInt(); } if (m_inverted) { if (val < 0) { val = -val; } else { val = max - value.toInt(); } } } m_slider->setRange(0, max - min); m_display->setRange(0, max - min); if (!m_inverted && !m_model->data(m_index, AssetParameterModel::RelativePosRole).toBool()) { val -= min; } m_slider->setValue(val); m_display->setValue(val); } bool PositionEditWidget::isValid() const { return m_slider->minimum() != m_slider->maximum(); } void PositionEditWidget::slotShowComment(bool show) { Q_UNUSED(show); } diff --git a/src/assets/view/widgets/positioneditwidget.hpp b/src/assets/view/widgets/positioneditwidget.hpp index b88ecbe53..04efdac92 100644 --- a/src/assets/view/widgets/positioneditwidget.hpp +++ b/src/assets/view/widgets/positioneditwidget.hpp @@ -1,71 +1,71 @@ /*************************************************************************** positionedit.h - description ------------------- begin : 03 Aug 2008 copyright : (C) 2008 by Marco Gittler email : g.marco@freenet.de ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #ifndef POSITIONWIDGET_H #define POSITIONWIDGET_H #include "abstractparamwidget.hpp" #include "timecode.h" #include #include class QSlider; class TimecodeDisplay; /*@brief This class is used to display a parameter with time value */ class PositionEditWidget : public AbstractParamWidget { Q_OBJECT public: /** @brief Sets up the parameter's GUI.*/ explicit PositionEditWidget(std::shared_ptr model, QModelIndex index, QWidget *parent = nullptr); - ~PositionEditWidget(); + ~PositionEditWidget() override; /** @brief get current position */ int getPosition() const; /** @brief set position */ void setPosition(int pos); /** @brief Call this when the timecode has been changed project-wise */ void updateTimecodeFormat(); /** @brief checks that the allowed time interval is valid */ bool isValid() const; public slots: /** @brief Toggle the comments on or off */ void slotShowComment(bool show) override; /** @brief refresh the properties to reflect changes in the model */ void slotRefresh() override; private: TimecodeDisplay *m_display; QSlider *m_slider; bool m_inverted; private slots: void slotUpdatePosition(); signals: void valueChanged(); }; #endif diff --git a/src/bin/abstractprojectitem.cpp b/src/bin/abstractprojectitem.cpp index 7d07af14e..3c4438791 100644 --- a/src/bin/abstractprojectitem.cpp +++ b/src/bin/abstractprojectitem.cpp @@ -1,300 +1,300 @@ /* 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 "abstractprojectitem.h" #include "bin.h" #include "core.h" #include "jobs/jobmanager.h" #include "macros.hpp" #include "projectitemmodel.h" #include "jobs/audiothumbjob.hpp" #include "jobs/loadjob.hpp" #include "jobs/thumbjob.hpp" #include #include #include - -AbstractProjectItem::AbstractProjectItem(PROJECTITEMTYPE type, const QString &id, const std::shared_ptr &model, bool isRoot) +#include +AbstractProjectItem::AbstractProjectItem(PROJECTITEMTYPE type, QString id, const std::shared_ptr &model, bool isRoot) : TreeItem(QList(), std::static_pointer_cast(model), isRoot) , m_name() , m_description() , m_thumbnail(QIcon()) , m_date() - , m_binId(id) + , m_binId(std::move(id)) , m_usage(0) , m_clipStatus(StatusReady) , m_itemType(type) , m_lock(QReadWriteLock::Recursive) , m_isCurrent(false) { Q_ASSERT(!isRoot || type == FolderItem); } bool AbstractProjectItem::operator==(const std::shared_ptr &projectItem) const { // FIXME: only works for folders bool equal = this->m_childItems == projectItem->m_childItems; // equal = equal && (m_parentItem == projectItem->m_parentItem); return equal; } std::shared_ptr AbstractProjectItem::parent() const { return std::static_pointer_cast(m_parentItem.lock()); } void AbstractProjectItem::setRefCount(uint count) { m_usage = count; if (auto ptr = m_model.lock()) std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this()), AbstractProjectItem::UsageCount); } uint AbstractProjectItem::refCount() const { return m_usage; } void AbstractProjectItem::addRef() { m_usage++; if (auto ptr = m_model.lock()) std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this()), AbstractProjectItem::UsageCount); } void AbstractProjectItem::removeRef() { m_usage--; if (auto ptr = m_model.lock()) std::static_pointer_cast(ptr)->onItemUpdated(std::static_pointer_cast(shared_from_this()), AbstractProjectItem::UsageCount); } const QString &AbstractProjectItem::clipId() const { return m_binId; } QPixmap AbstractProjectItem::roundedPixmap(const QPixmap &source) { QPixmap pix(source.size()); pix.fill(Qt::transparent); QPainter p(&pix); p.setRenderHint(QPainter::Antialiasing, true); QPainterPath path; path.addRoundedRect(0.5, 0.5, pix.width() - 1, pix.height() - 1, 4, 4); p.setClipPath(path); p.drawPixmap(0, 0, source); p.end(); return pix; } AbstractProjectItem::PROJECTITEMTYPE AbstractProjectItem::itemType() const { return m_itemType; } QVariant AbstractProjectItem::getData(DataType type) const { QVariant data; switch (type) { case DataName: data = QVariant(m_name); break; case DataDescription: data = QVariant(m_description); break; case DataThumbnail: data = QVariant(m_thumbnail); break; case DataId: data = QVariant(m_id); break; case DataDuration: data = QVariant(m_duration); break; case DataInPoint: data = QVariant(m_inPoint); break; case DataDate: data = QVariant(m_date); break; case UsageCount: data = QVariant(m_usage); break; case ItemTypeRole: data = QVariant(m_itemType); break; case ClipType: data = clipType(); break; case ClipHasAudioAndVideo: data = hasAudioAndVideo(); break; case JobType: if (itemType() == ClipItem) { auto jobIds = pCore->jobManager()->getPendingJobsIds(clipId()); if (jobIds.empty()) { jobIds = pCore->jobManager()->getFinishedJobsIds(clipId()); } if (jobIds.size() > 0) { data = QVariant(pCore->jobManager()->getJobType(jobIds[0])); } } break; case JobStatus: if (itemType() == ClipItem) { auto jobIds = pCore->jobManager()->getPendingJobsIds(clipId()); if (jobIds.empty()) { jobIds = pCore->jobManager()->getFinishedJobsIds(clipId()); } if (jobIds.size() > 0) { data = QVariant::fromValue(pCore->jobManager()->getJobStatus(jobIds[0])); } else { data = QVariant::fromValue(JobManagerStatus::NoJob); } } break; case JobProgress: if (itemType() == ClipItem) { auto jobIds = pCore->jobManager()->getPendingJobsIds(clipId()); if (jobIds.size() > 0) { data = QVariant(pCore->jobManager()->getJobProgressForClip(jobIds[0], clipId())); } else { data = QVariant(0); } } break; case JobSuccess: if (itemType() == ClipItem) { auto jobIds = pCore->jobManager()->getFinishedJobsIds(clipId()); if (jobIds.size() > 0) { // Check the last job status data = QVariant(pCore->jobManager()->jobSucceded(jobIds[jobIds.size() - 1])); } else { data = QVariant(true); } } break; case ClipStatus: data = QVariant(m_clipStatus); break; case ClipToolTip: data = QVariant(getToolTip()); break; default: break; } return data; } int AbstractProjectItem::supportedDataCount() const { return 3; } QString AbstractProjectItem::name() const { return m_name; } void AbstractProjectItem::setName(const QString &name) { m_name = name; } QString AbstractProjectItem::description() const { return m_description; } void AbstractProjectItem::setDescription(const QString &description) { m_description = description; } QPoint AbstractProjectItem::zone() const { - return QPoint(); + return {}; } void AbstractProjectItem::setClipStatus(CLIPSTATUS status) { m_clipStatus = status; } bool AbstractProjectItem::statusReady() const { return m_clipStatus == StatusReady; } AbstractProjectItem::CLIPSTATUS AbstractProjectItem::clipStatus() const { return m_clipStatus; } std::shared_ptr AbstractProjectItem::getEnclosingFolder(bool strict) { if (!strict && itemType() == AbstractProjectItem::FolderItem) { return std::static_pointer_cast(shared_from_this()); } if (auto ptr = m_parentItem.lock()) { return std::static_pointer_cast(ptr)->getEnclosingFolder(false); } return std::shared_ptr(); } bool AbstractProjectItem::selfSoftDelete(Fun &undo, Fun &redo) { pCore->jobManager()->slotDiscardClipJobs(clipId()); Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; for (const auto &child : m_childItems) { bool res = std::static_pointer_cast(child)->selfSoftDelete(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; } QString AbstractProjectItem::lastParentId() const { return m_lastParentId; } void AbstractProjectItem::updateParent(std::shared_ptr newParent) { // bool reload = !m_lastParentId.isEmpty(); m_lastParentId.clear(); if (newParent) { m_lastParentId = std::static_pointer_cast(newParent)->clipId(); } TreeItem::updateParent(newParent); } diff --git a/src/bin/abstractprojectitem.h b/src/bin/abstractprojectitem.h index a07a77808..a73583930 100644 --- a/src/bin/abstractprojectitem.h +++ b/src/bin/abstractprojectitem.h @@ -1,223 +1,223 @@ /* 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 ABSTRACTPROJECTITEM_H #define ABSTRACTPROJECTITEM_H #include "abstractmodel/treeitem.hpp" #include "undohelper.hpp" #include #include #include #include class ProjectClip; class ProjectFolder; class Bin; class QDomElement; class QDomDocument; class ProjectItemModel; /** * @class AbstractProjectItem * @brief Base class for all project items (clips, folders, ...). * * Project items are stored in a tree like structure ... */ class AbstractProjectItem : public QObject, public TreeItem { Q_OBJECT public: enum PROJECTITEMTYPE { FolderUpItem = 0, FolderItem = 1, ClipItem = 2, SubClipItem = 3 }; /** * @brief Constructor. * @param type is the type of the bin item * @param id is the binId * @param model is the ptr to the item model * @param isRoot is true if this is the topmost folder */ - AbstractProjectItem(PROJECTITEMTYPE type, const QString &id, const std::shared_ptr &model, bool isRoot = false); + AbstractProjectItem(PROJECTITEMTYPE type, QString id, const std::shared_ptr &model, bool isRoot = false); bool operator==(const std::shared_ptr &projectItem) const; /** @brief Returns a pointer to the parent item (or NULL). */ std::shared_ptr parent() const; /** @brief Returns the type of this item (folder, clip, subclip, etc). */ PROJECTITEMTYPE itemType() const; /** @brief Used to search for a clip with a specific id. */ virtual std::shared_ptr clip(const QString &id) = 0; /** @brief Used to search for a folder with a specific id. */ virtual std::shared_ptr folder(const QString &id) = 0; virtual std::shared_ptr clipAt(int ix) = 0; /** @brief Recursively disable/enable bin effects. */ virtual void setBinEffectsEnabled(bool enabled) = 0; /** @brief Returns true if item has both audio and video enabled. */ virtual bool hasAudioAndVideo() const = 0; /** @brief This function executes what should be done when the item is deleted but without deleting effectively. For example, the item will deregister itself from the model and delete the clips from the timeline. However, the object is NOT actually deleted, and the tree structure is preserved. @param Undo,Redo are the lambdas accumulating the update. */ virtual bool selfSoftDelete(Fun &undo, Fun &redo); /** @brief Returns the clip's id. */ const QString &clipId() const; virtual QPoint zone() const; // TODO refac : these ref counting are probably deprecated by smart ptrs /** @brief Set current usage count. */ void setRefCount(uint count); /** @brief Returns clip's current usage count in timeline. */ uint refCount() const; /** @brief Increase usage count. */ void addRef(); /** @brief Decrease usage count. */ void removeRef(); enum DataType { // display name of item DataName = Qt::DisplayRole, // image thumbnail DataThumbnail = Qt::DecorationRole, // Tooltip text,usually full path ClipToolTip = Qt::ToolTipRole, // unique id of the project clip / folder DataId = Qt::UserRole, // creation date DataDate, // Description for item (user editable) DataDescription, // Number of occurrences used in timeline UsageCount, // Empty if clip has no effect, icon otherwise IconOverlay, // item type (clip, subclip, folder) ItemTypeRole, // Duration of the clip DataDuration, // Inpoint of the subclip (0 for clips) DataInPoint, // If there is a running job, which type JobType, // Current progress of the job JobProgress, // error message if job crashes (not fully implemented) JobSuccess, JobStatus, // Item status (ready or not, missing, waiting, ...) ClipStatus, ClipType, ClipHasAudioAndVideo }; enum CLIPSTATUS { StatusReady = 0, StatusMissing, StatusWaiting, StatusDeleting }; void setClipStatus(AbstractProjectItem::CLIPSTATUS status); AbstractProjectItem::CLIPSTATUS clipStatus() const; bool statusReady() const; /** @brief Returns the data that describes this item. * @param type type of data to return * * This function is necessary for interaction with ProjectItemModel. */ virtual QVariant getData(DataType type) const; /** * @brief Returns the amount of different types of data this item supports. * * This base class supports only DataName and DataDescription, so the return value is always 2. * This function is necessary for interaction with ProjectItemModel. */ virtual int supportedDataCount() const; /** @brief Returns the (displayable) name of this item. */ QString name() const; /** @brief Sets a new (displayable) name. */ virtual void setName(const QString &name); /** @brief Returns the (displayable) description of this item. */ QString description() const; /** @brief Sets a new description. */ virtual void setDescription(const QString &description); virtual QDomElement toXml(QDomDocument &document, bool includeMeta = false) = 0; virtual QString getToolTip() const = 0; virtual bool rename(const QString &name, int column) = 0; /* @brief Return the bin id of the last parent that this element got, even if this parent has already been destroyed. Return the empty string if the element was parentless */ QString lastParentId() const; /* @brief This is an overload of TreeItem::updateParent that tracks the id of the id of the parent */ void updateParent(std::shared_ptr newParent) override; /* Returns a ptr to the enclosing dir, and nullptr if none is found. @param strict if set to false, the enclosing dir of a dir is itself, otherwise we try to find a "true" parent */ std::shared_ptr getEnclosingFolder(bool strict = false); /** @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. */ virtual bool isIncludedInTimeline() { return false; } virtual ClipType::ProducerType clipType() const = 0; signals: void childAdded(AbstractProjectItem *child); void aboutToRemoveChild(AbstractProjectItem *child); protected: QString m_name; QString m_description; QIcon m_thumbnail; QString m_duration; int m_inPoint; QDateTime m_date; QString m_binId; uint m_usage; CLIPSTATUS m_clipStatus; PROJECTITEMTYPE m_itemType; QString m_lastParentId; /** @brief Returns a rounded border pixmap from the @param source pixmap. */ QPixmap roundedPixmap(const QPixmap &source); mutable QReadWriteLock m_lock; // This is a lock that ensures safety in case of concurrent access private: bool m_isCurrent; }; #endif diff --git a/src/bin/bin.cpp b/src/bin/bin.cpp index c99cc9a83..bb96db8f6 100644 --- a/src/bin/bin.cpp +++ b/src/bin/bin.cpp @@ -1,3117 +1,3115 @@ /* 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; + auto *me = (QMouseEvent *)event; if (m_audioDragRect.contains(me->pos())) { dragType = PlaylistState::AudioOnly; } else if (m_videoDragRect.contains(me->pos())) { dragType = PlaylistState::VideoOnly; } else { dragType = PlaylistState::Disabled; } } event->ignore(); return false; } void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const override { if (index.column() != 0) { QStyledItemDelegate::updateEditorGeometry(editor, option, index); return; } QStyleOptionViewItem opt = option; initStyleOption(&opt, index); QRect r1 = option.rect; int type = index.data(AbstractProjectItem::ItemTypeRole).toInt(); int decoWidth = 0; if (opt.decorationSize.height() > 0) { decoWidth += r1.height() * m_dar; } int mid = 0; if (type == AbstractProjectItem::ClipItem || type == AbstractProjectItem::SubClipItem) { mid = (int)((r1.height() / 2)); } r1.adjust(decoWidth, 0, 0, -mid); QFont ft = option.font; ft.setBold(true); QFontMetricsF fm(ft); QRect r2 = fm.boundingRect(r1, Qt::AlignLeft | Qt::AlignTop, index.data(AbstractProjectItem::DataName).toString()).toRect(); editor->setGeometry(r2); } QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override { QSize hint = QStyledItemDelegate::sizeHint(option, index); QString text = index.data(AbstractProjectItem::DataName).toString(); QRectF r = option.rect; QFont ft = option.font; ft.setBold(true); QFontMetricsF fm(ft); QStyle *style = option.widget ? option.widget->style() : QApplication::style(); const int textMargin = style->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1; int width = fm.boundingRect(r, Qt::AlignLeft | Qt::AlignTop, text).width() + option.decorationSize.width() + 2 * textMargin; hint.setWidth(width); int type = index.data(AbstractProjectItem::ItemTypeRole).toInt(); if (type == AbstractProjectItem::FolderItem || type == AbstractProjectItem::FolderUpItem) { return QSize(hint.width(), qMin(option.fontMetrics.lineSpacing() + 4, hint.height())); } if (type == AbstractProjectItem::ClipItem) { return QSize(hint.width(), qMax(option.fontMetrics.lineSpacing() * 2 + 4, qMax(hint.height(), option.decorationSize.height()))); } if (type == AbstractProjectItem::SubClipItem) { return QSize(hint.width(), qMax(option.fontMetrics.lineSpacing() * 2 + 4, qMin(hint.height(), (int)(option.decorationSize.height() / 1.5)))); } QIcon icon = qvariant_cast(index.data(Qt::DecorationRole)); QString line1 = index.data(Qt::DisplayRole).toString(); QString line2 = index.data(Qt::UserRole).toString(); int textW = qMax(option.fontMetrics.width(line1), option.fontMetrics.width(line2)); QSize iconSize = icon.actualSize(option.decorationSize); - return QSize(qMax(textW, iconSize.width()) + 4, option.fontMetrics.lineSpacing() * 2 + 4); + return {qMax(textW, iconSize.width()) + 4, option.fontMetrics.lineSpacing() * 2 + 4}; } void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override { if (index.column() == 0 && !index.data().isNull()) { QRect r1 = option.rect; painter->save(); painter->setClipRect(r1); QStyleOptionViewItem opt(option); initStyleOption(&opt, index); int type = index.data(AbstractProjectItem::ItemTypeRole).toInt(); QStyle *style = opt.widget ? opt.widget->style() : QApplication::style(); const int textMargin = style->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1; // QRect r = QStyle::alignedRect(opt.direction, Qt::AlignVCenter | Qt::AlignLeft, opt.decorationSize, r1); style->drawPrimitive(QStyle::PE_PanelItemViewItem, &opt, painter, opt.widget); if ((option.state & static_cast(QStyle::State_Selected)) != 0) { painter->setPen(option.palette.highlightedText().color()); } else { painter->setPen(option.palette.text().color()); } QRect r = r1; QFont font = painter->font(); font.setBold(true); painter->setFont(font); if (type == AbstractProjectItem::ClipItem || type == AbstractProjectItem::SubClipItem) { int decoWidth = 0; if (opt.decorationSize.height() > 0) { r.setWidth(r.height() * m_dar); QPixmap pix = opt.icon.pixmap(opt.icon.actualSize(r.size())); // Draw icon decoWidth += r.width() + textMargin; r.setWidth(r.height() * pix.width() / pix.height()); painter->drawPixmap(r, pix, QRect(0, 0, pix.width(), pix.height())); } int mid = (int)((r1.height() / 2)); r1.adjust(decoWidth, 0, 0, -mid); QRect r2 = option.rect; r2.adjust(decoWidth, mid, 0, 0); QRectF bounding; painter->drawText(r1, Qt::AlignLeft | Qt::AlignTop, index.data(AbstractProjectItem::DataName).toString(), &bounding); font.setBold(false); painter->setFont(font); QString subText = index.data(AbstractProjectItem::DataDuration).toString(); if (!subText.isEmpty()) { r2.adjust(0, bounding.bottom() - r2.top(), 0, 0); QColor subTextColor = painter->pen().color(); subTextColor.setAlphaF(.5); painter->setPen(subTextColor); // Draw usage counter int usage = index.data(AbstractProjectItem::UsageCount).toInt(); if (usage > 0) { subText.append(QString().sprintf(" [%d]", usage)); } painter->drawText(r2, Qt::AlignLeft | Qt::AlignTop, subText, &bounding); // Add audio/video icons for selective drag int cType = index.data(AbstractProjectItem::ClipType).toInt(); bool hasAudioAndVideo = index.data(AbstractProjectItem::ClipHasAudioAndVideo).toBool(); if (hasAudioAndVideo && (cType == ClipType::AV || cType == ClipType::Playlist) && (opt.state & QStyle::State_MouseOver)) { bounding.moveLeft(bounding.right() + (2 * textMargin)); bounding.adjust(0, textMargin, 0, -textMargin); QIcon aDrag = QIcon::fromTheme(QStringLiteral("audio-volume-medium")); m_audioDragRect = bounding.toRect(); m_audioDragRect.setWidth(m_audioDragRect.height()); aDrag.paint(painter, m_audioDragRect, Qt::AlignLeft); m_videoDragRect = m_audioDragRect; m_videoDragRect.moveLeft(m_audioDragRect.right()); QIcon vDrag = QIcon::fromTheme(QStringLiteral("kdenlive-show-video")); vDrag.paint(painter, m_videoDragRect, Qt::AlignLeft); } else { m_audioDragRect = QRect(); m_videoDragRect = QRect(); } } if (type == AbstractProjectItem::ClipItem) { // Overlay icon if necessary QVariant v = index.data(AbstractProjectItem::IconOverlay); if (!v.isNull()) { QIcon reload = QIcon::fromTheme(v.toString()); r.setTop(r.bottom() - bounding.height()); r.setWidth(bounding.height()); reload.paint(painter, r); } int jobProgress = index.data(AbstractProjectItem::JobProgress).toInt(); - JobManagerStatus status = index.data(AbstractProjectItem::JobStatus).value(); + auto status = index.data(AbstractProjectItem::JobStatus).value(); if (status == JobManagerStatus::Pending || status == JobManagerStatus::Running) { // Draw job progress bar int progressWidth = option.fontMetrics.averageCharWidth() * 8; int progressHeight = option.fontMetrics.ascent() / 4; QRect progress(r1.x() + 1, opt.rect.bottom() - progressHeight - 2, progressWidth, progressHeight); painter->setPen(Qt::NoPen); painter->setBrush(Qt::darkGray); if (status == JobManagerStatus::Running) { painter->drawRoundedRect(progress, 2, 2); painter->setBrush((option.state & static_cast((QStyle::State_Selected) != 0)) != 0 ? option.palette.text() : option.palette.highlight()); progress.setWidth((progressWidth - 2) * jobProgress / 100); painter->drawRoundedRect(progress, 2, 2); } else { // Draw kind of a pause icon progress.setWidth(3); painter->drawRect(progress); progress.moveLeft(progress.right() + 3); painter->drawRect(progress); } } bool jobsucceeded = index.data(AbstractProjectItem::JobSuccess).toBool(); if (!jobsucceeded) { QIcon warning = QIcon::fromTheme(QStringLiteral("process-stop")); warning.paint(painter, r2); } } } else { // Folder or Folder Up items int decoWidth = 0; if (opt.decorationSize.height() > 0) { r.setWidth(r.height() * m_dar); QPixmap pix = opt.icon.pixmap(opt.icon.actualSize(r.size())); // Draw icon decoWidth += r.width() + textMargin; r.setWidth(r.height() * pix.width() / pix.height()); painter->drawPixmap(r, pix, QRect(0, 0, pix.width(), pix.height())); } r1.adjust(decoWidth, 0, 0, 0); QRectF bounding; painter->drawText(r1, Qt::AlignLeft | Qt::AlignTop, index.data(AbstractProjectItem::DataName).toString(), &bounding); } painter->restore(); } else { QStyledItemDelegate::paint(painter, option, index); } } private: - mutable bool m_editorOpen; + mutable bool m_editorOpen{false}; mutable QRect m_audioDragRect; mutable QRect m_videoDragRect; - double m_dar; + double m_dar{1.778}; public: - PlaylistState::ClipState dragType; + PlaylistState::ClipState dragType{PlaylistState::Disabled}; }; MyListView::MyListView(QWidget *parent) : QListView(parent) { setViewMode(QListView::IconMode); setMovement(QListView::Static); setResizeMode(QListView::Adjust); setUniformItemSizes(true); setDragDropMode(QAbstractItemView::DragDrop); setAcceptDrops(true); setDragEnabled(true); viewport()->setAcceptDrops(true); } void MyListView::focusInEvent(QFocusEvent *event) { QListView::focusInEvent(event); if (event->reason() == Qt::MouseFocusReason) { emit focusView(); } } MyTreeView::MyTreeView(QWidget *parent) : QTreeView(parent) { setEditing(false); } void MyTreeView::mousePressEvent(QMouseEvent *event) { QTreeView::mousePressEvent(event); if (event->button() == Qt::LeftButton) { m_startPos = event->pos(); QModelIndex ix = indexAt(m_startPos); if (ix.isValid()) { QAbstractItemDelegate *del = itemDelegate(ix); m_dragType = static_cast(del)->dragType; } else { m_dragType = PlaylistState::Disabled; } } } void MyTreeView::focusInEvent(QFocusEvent *event) { QTreeView::focusInEvent(event); if (event->reason() == Qt::MouseFocusReason) { emit focusView(); } } void MyTreeView::mouseMoveEvent(QMouseEvent *event) { bool dragged = false; if ((event->buttons() & Qt::LeftButton) != 0u) { int distance = (event->pos() - m_startPos).manhattanLength(); if (distance >= QApplication::startDragDistance()) { dragged = performDrag(); } } if (!dragged) { QTreeView::mouseMoveEvent(event); } } void MyTreeView::closeEditor(QWidget *editor, QAbstractItemDelegate::EndEditHint hint) { QAbstractItemView::closeEditor(editor, hint); setEditing(false); } void MyTreeView::editorDestroyed(QObject *editor) { QAbstractItemView::editorDestroyed(editor); setEditing(false); } bool MyTreeView::isEditing() const { return state() == QAbstractItemView::EditingState; } void MyTreeView::setEditing(bool edit) { setState(edit ? QAbstractItemView::EditingState : QAbstractItemView::NoState); } bool MyTreeView::performDrag() { QModelIndexList bases = selectedIndexes(); QModelIndexList indexes; for (int i = 0; i < bases.count(); i++) { if (bases.at(i).column() == 0) { indexes << bases.at(i); } } if (indexes.isEmpty()) { return false; } // Check if we want audio or video only emit updateDragMode(m_dragType); auto *drag = new QDrag(this); drag->setMimeData(model()->mimeData(indexes)); QModelIndex ix = indexes.constFirst(); if (ix.isValid()) { QIcon icon = ix.data(AbstractProjectItem::DataThumbnail).value(); QPixmap pix = icon.pixmap(iconSize()); QSize size = pix.size(); QImage image(size, QImage::Format_ARGB32_Premultiplied); image.fill(Qt::transparent); QPainter p(&image); p.setOpacity(0.7); p.drawPixmap(0, 0, pix); p.setOpacity(1); if (indexes.count() > 1) { QPalette palette; int radius = size.height() / 3; p.setBrush(palette.highlight()); p.setPen(palette.highlightedText().color()); p.drawEllipse(QPoint(size.width() / 2, size.height() / 2), radius, radius); p.drawText(size.width() / 2 - radius, size.height() / 2 - radius, 2 * radius, 2 * radius, Qt::AlignCenter, QString::number(indexes.count())); } p.end(); drag->setPixmap(QPixmap::fromImage(image)); } drag->exec(); return true; } SmallJobLabel::SmallJobLabel(QWidget *parent) : QPushButton(parent) - , 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) +Bin::Bin(std::shared_ptr model, QWidget *parent) : QWidget(parent) , isLoading(false) - , m_itemModel(model) + , m_itemModel(std::move(model)) , m_itemView(nullptr) , m_doc(nullptr) , m_extractAudioAction(nullptr) , m_transcodeAction(nullptr) , m_clipsActionsMenu(nullptr) , m_inTimelineAction(nullptr) , m_listType((BinViewType)KdenliveSettings::binMode()) , m_iconSize(160, 90) , m_propertiesPanel(nullptr) , m_blankThumb() , m_invalidClipDialog(nullptr) , m_gainedFocus(false) , m_audioDuration(0) , m_processedAudio(0) { m_layout = new QVBoxLayout(this); // Create toolbar for buttons m_toolbar = new QToolBar(this); int size = style()->pixelMetric(QStyle::PM_SmallIconSize); QSize iconSize(size, size); m_toolbar->setIconSize(iconSize); m_toolbar->setToolButtonStyle(Qt::ToolButtonIconOnly); m_layout->addWidget(m_toolbar); m_layout->setSpacing(0); m_layout->setContentsMargins(0, 0, 0, 0); // Search line m_proxyModel = new ProjectSortProxyModel(this); m_proxyModel->setDynamicSortFilter(true); m_searchLine = new QLineEdit(this); m_searchLine->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred); // m_searchLine->setClearButtonEnabled(true); m_searchLine->setPlaceholderText(i18n("Search")); m_searchLine->setFocusPolicy(Qt::ClickFocus); connect(m_searchLine, &QLineEdit::textChanged, m_proxyModel, &ProjectSortProxyModel::slotSetSearchString); auto *leventEater = new LineEventEater(this); m_searchLine->installEventFilter(leventEater); connect(leventEater, &LineEventEater::clearSearchLine, m_searchLine, &QLineEdit::clear); connect(leventEater, &LineEventEater::showClearButton, this, &Bin::showClearButton); setFocusPolicy(Qt::ClickFocus); connect(m_itemModel.get(), &ProjectItemModel::refreshPanel, this, &Bin::refreshPanel); connect(m_itemModel.get(), &ProjectItemModel::refreshAudioThumbs, this, &Bin::doRefreshAudioThumbs); connect(m_itemModel.get(), &ProjectItemModel::refreshClip, this, &Bin::refreshClip); connect(m_itemModel.get(), &ProjectItemModel::updateTimelineProducers, this, &Bin::updateTimelineProducers); connect(m_itemModel.get(), &ProjectItemModel::emitMessage, this, &Bin::emitMessage); // Connect models m_proxyModel->setSourceModel(m_itemModel.get()); connect(m_itemModel.get(), &QAbstractItemModel::dataChanged, m_proxyModel, &ProjectSortProxyModel::slotDataChanged); connect(m_proxyModel, &ProjectSortProxyModel::selectModel, this, &Bin::selectProxyModel); connect(m_itemModel.get(), static_cast(&ProjectItemModel::itemDropped), this, static_cast(&Bin::slotItemDropped)); connect(m_itemModel.get(), static_cast &, const QModelIndex &)>(&ProjectItemModel::itemDropped), this, static_cast &, const QModelIndex &)>(&Bin::slotItemDropped)); connect(m_itemModel.get(), &ProjectItemModel::effectDropped, this, &Bin::slotEffectDropped); connect(m_itemModel.get(), &QAbstractItemModel::dataChanged, this, &Bin::slotItemEdited); connect(this, &Bin::refreshPanel, this, &Bin::doRefreshPanel); // Zoom slider QWidget *container = new QWidget(this); - QHBoxLayout *lay = new QHBoxLayout; + auto *lay = new QHBoxLayout; m_slider = new QSlider(Qt::Horizontal, this); m_slider->setMaximumWidth(100); m_slider->setMinimumWidth(40); m_slider->setRange(0, 10); m_slider->setValue(KdenliveSettings::bin_zoom()); connect(m_slider, &QAbstractSlider::valueChanged, this, &Bin::slotSetIconSize); - QToolButton *tb1 = new QToolButton(this); + auto *tb1 = new QToolButton(this); tb1->setIcon(QIcon::fromTheme(QStringLiteral("zoom-in"))); connect(tb1, &QToolButton::clicked, [&]() { m_slider->setValue(qMin(m_slider->value() + 1, m_slider->maximum())); }); - QToolButton *tb2 = new QToolButton(this); + auto *tb2 = new QToolButton(this); tb2->setIcon(QIcon::fromTheme(QStringLiteral("zoom-out"))); connect(tb2, &QToolButton::clicked, [&]() { m_slider->setValue(qMax(m_slider->value() - 1, m_slider->minimum())); }); lay->addWidget(tb2); lay->addWidget(m_slider); lay->addWidget(tb1); container->setLayout(lay); auto *widgetslider = new QWidgetAction(this); widgetslider->setDefaultWidget(container); // View type KSelectAction *listType = new KSelectAction(QIcon::fromTheme(QStringLiteral("view-list-tree")), i18n("View Mode"), this); pCore->window()->actionCollection()->addAction(QStringLiteral("bin_view_mode"), listType); QAction *treeViewAction = listType->addAction(QIcon::fromTheme(QStringLiteral("view-list-tree")), i18n("Tree View")); listType->addAction(treeViewAction); treeViewAction->setData(BinTreeView); if (m_listType == treeViewAction->data().toInt()) { listType->setCurrentAction(treeViewAction); } pCore->window()->actionCollection()->addAction(QStringLiteral("bin_view_mode_tree"), treeViewAction); QAction *iconViewAction = listType->addAction(QIcon::fromTheme(QStringLiteral("view-list-icons")), i18n("Icon View")); iconViewAction->setData(BinIconView); if (m_listType == iconViewAction->data().toInt()) { listType->setCurrentAction(iconViewAction); } pCore->window()->actionCollection()->addAction(QStringLiteral("bin_view_mode_icon"), iconViewAction); QAction *disableEffects = new QAction(i18n("Disable Bin Effects"), this); connect(disableEffects, &QAction::triggered, [this](bool disable) { this->setBinEffectsEnabled(!disable); }); disableEffects->setIcon(QIcon::fromTheme(QStringLiteral("favorite"))); disableEffects->setData("disable_bin_effects"); disableEffects->setCheckable(true); disableEffects->setChecked(false); pCore->window()->actionCollection()->addAction(QStringLiteral("disable_bin_effects"), disableEffects); 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()); + auto *mouseEvent = static_cast(event); + auto *view = qobject_cast(obj->parent()); if (view) { QModelIndex idx = view->indexAt(mouseEvent->pos()); m_gainedFocus = false; if (idx.isValid()) { std::shared_ptr item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(idx)); editMasterEffect(item); } else { editMasterEffect(nullptr); } } // make sure we discard the focus indicator m_gainedFocus = false; } return success; } if (event->type() == QEvent::MouseButtonDblClick) { - QMouseEvent *mouseEvent = static_cast(event); - QAbstractItemView *view = qobject_cast(obj->parent()); + auto *mouseEvent = static_cast(event); + auto *view = qobject_cast(obj->parent()); if (view) { QModelIndex idx = view->indexAt(mouseEvent->pos()); if (!idx.isValid()) { // User double clicked on empty area slotAddClip(); } else { slotItemDoubleClicked(idx, mouseEvent->pos()); } } else { qCDebug(KDENLIVE_LOG) << " +++++++ NO VIEW-------!!"; } return true; } if (event->type() == QEvent::Wheel) { - QWheelEvent *e = static_cast(event); + auto *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); + auto *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()); + auto *currentItem = static_cast(items.at(i).internalPointer()); AbstractProjectItem::PROJECTITEMTYPE type = currentItem->itemType(); if (folderWanted && type == AbstractProjectItem::FolderItem) { // We found our folder return items.at(i); } if (!folderWanted && type == AbstractProjectItem::ClipItem) { // We found our clip return items.at(i); } } - return QModelIndex(); + return {}; } void Bin::selectClipById(const QString &clipId, int frame, const QPoint &zone) { if (m_monitor->activeClipId() == clipId) { if (frame > -1) { m_monitor->slotSeek(frame); } if (!zone.isNull()) { m_monitor->slotLoadClipZone(zone); } return; } m_proxyModel->selectionModel()->clearSelection(); std::shared_ptr clip = getBinClip(clipId); if (clip) { selectClip(clip); if (frame > -1) { m_monitor->slotSeek(frame); } if (!zone.isNull()) { m_monitor->slotLoadClipZone(zone); } } } void Bin::selectProxyModel(const QModelIndex &id) { if (isLoading) { // return; } if (id.isValid()) { if (id.column() != 0) { return; } std::shared_ptr currentItem = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(id)); if (currentItem) { // Set item as current so that it displays its content in clip monitor setCurrent(currentItem); if (currentItem->itemType() == AbstractProjectItem::ClipItem) { m_reloadAction->setEnabled(true); m_locateAction->setEnabled(true); m_duplicateAction->setEnabled(true); std::shared_ptr clip = std::static_pointer_cast(currentItem); ClipType::ProducerType type = clip->clipType(); m_openAction->setEnabled(type == ClipType::Image || type == ClipType::Audio || type == ClipType::Text || type == ClipType::TextTemplate); showClipProperties(clip, false); m_deleteAction->setText(i18n("Delete Clip")); m_proxyAction->setText(i18n("Proxy Clip")); emit findInTimeline(clip->clipId(), clip->timelineInstances()); } else if (currentItem->itemType() == AbstractProjectItem::FolderItem) { // A folder was selected, disable editing clip m_openAction->setEnabled(false); m_reloadAction->setEnabled(false); m_locateAction->setEnabled(false); m_duplicateAction->setEnabled(false); m_deleteAction->setText(i18n("Delete Folder")); m_proxyAction->setText(i18n("Proxy Folder")); } else if (currentItem->itemType() == AbstractProjectItem::SubClipItem) { showClipProperties(std::static_pointer_cast(currentItem->parent()), false); m_openAction->setEnabled(false); m_reloadAction->setEnabled(false); m_locateAction->setEnabled(false); m_duplicateAction->setEnabled(false); m_deleteAction->setText(i18n("Delete Clip")); m_proxyAction->setText(i18n("Proxy Clip")); } m_deleteAction->setEnabled(true); } else { emit findInTimeline(QString()); m_reloadAction->setEnabled(false); m_locateAction->setEnabled(false); m_duplicateAction->setEnabled(false); m_openAction->setEnabled(false); m_deleteAction->setEnabled(false); } } else { // No item selected in bin m_openAction->setEnabled(false); m_deleteAction->setEnabled(false); showClipProperties(nullptr); emit findInTimeline(QString()); emit requestClipShow(nullptr); // clear effect stack emit requestShowEffectStack(QString(), nullptr, QSize(), false); // Display black bg in clip monitor emit openClip(std::shared_ptr()); } } std::vector Bin::selectedClipsIds(bool excludeFolders) { const QModelIndexList indexes = m_proxyModel->selectionModel()->selectedIndexes(); std::vector ids; // We define the lambda that will be executed on each item of the subset of nodes of the tree that are selected auto itemAdder = [excludeFolders, &ids](std::vector &ids_vec, std::shared_ptr item) { auto binItem = std::static_pointer_cast(item); if (!excludeFolders || (binItem->itemType() != AbstractProjectItem::FolderItem && binItem->itemType() != AbstractProjectItem::FolderUpItem)) { ids.push_back(binItem->clipId()); } return ids_vec; }; for (const QModelIndex &ix : indexes) { if (!ix.isValid() || ix.column() != 0) { continue; } std::shared_ptr item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(ix)); item->accumulate(ids, itemAdder); } return ids; } QList> Bin::selectedClips() { auto ids = selectedClipsIds(true); QList> ret; for (const auto &id : ids) { ret.push_back(m_itemModel->getClipByBinID(id)); } return ret; } void Bin::slotInitView(QAction *action) { if (action) { m_proxyModel->selectionModel()->clearSelection(); int viewType = action->data().toInt(); KdenliveSettings::setBinMode(viewType); if (viewType == m_listType) { return; } if (m_listType == BinTreeView) { // save current treeview state (column width) - QTreeView *view = static_cast(m_itemView); + auto *view = static_cast(m_itemView); m_headerInfo = view->header()->saveState(); m_showDate->setEnabled(true); m_showDesc->setEnabled(true); } else { // remove the current folderUp item if any if (m_folderUp) { if (m_folderUp->parent()) { m_folderUp->parent()->removeChild(m_folderUp); } m_folderUp.reset(); } } m_listType = static_cast(viewType); } if (m_itemView) { delete m_itemView; } switch (m_listType) { case BinIconView: m_itemView = new MyListView(this); m_folderUp = ProjectFolderUp::construct(m_itemModel); m_showDate->setEnabled(false); m_showDesc->setEnabled(false); break; default: m_itemView = new MyTreeView(this); m_showDate->setEnabled(true); m_showDesc->setEnabled(true); break; } m_itemView->setMouseTracking(true); m_itemView->viewport()->installEventFilter(this); QSize zoom = m_iconSize * (m_slider->value() / 4.0); m_itemView->setIconSize(zoom); QPixmap pix(zoom); pix.fill(Qt::lightGray); m_blankThumb.addPixmap(pix); m_binTreeViewDelegate->setDar(pCore->getCurrentDar()); m_itemView->setModel(m_proxyModel); m_itemView->setSelectionModel(m_proxyModel->selectionModel()); m_layout->insertWidget(1, m_itemView); // setup some default view specific parameters if (m_listType == BinTreeView) { m_itemView->setItemDelegate(m_binTreeViewDelegate); - MyTreeView *view = static_cast(m_itemView); + auto *view = static_cast(m_itemView); view->setSortingEnabled(true); view->setWordWrap(true); connect(m_proxyModel, &QAbstractItemModel::layoutAboutToBeChanged, this, &Bin::slotSetSorting); connect(view, &MyTreeView::updateDragMode, m_itemModel.get(), &ProjectItemModel::setDragType, Qt::DirectConnection); m_proxyModel->setDynamicSortFilter(true); if (!m_headerInfo.isEmpty()) { view->header()->restoreState(m_headerInfo); } else { view->header()->resizeSections(QHeaderView::ResizeToContents); view->resizeColumnToContents(0); view->setColumnHidden(1, true); view->setColumnHidden(2, true); } m_showDate->setChecked(!view->isColumnHidden(1)); m_showDesc->setChecked(!view->isColumnHidden(2)); connect(view->header(), &QHeaderView::sectionResized, this, &Bin::slotSaveHeaders); connect(view->header(), &QHeaderView::sectionClicked, this, &Bin::slotSaveHeaders); connect(view, &MyTreeView::focusView, this, &Bin::slotGotFocus); } else if (m_listType == BinIconView) { - MyListView *view = static_cast(m_itemView); + auto *view = static_cast(m_itemView); connect(view, &MyListView::focusView, this, &Bin::slotGotFocus); } m_itemView->setEditTriggers(QAbstractItemView::NoEditTriggers); // DoubleClicked); m_itemView->setSelectionMode(QAbstractItemView::ExtendedSelection); m_itemView->setDragDropMode(QAbstractItemView::DragDrop); m_itemView->setAlternatingRowColors(true); m_itemView->setAcceptDrops(true); m_itemView->setFocus(); } void Bin::slotSetIconSize(int size) { if (!m_itemView) { return; } KdenliveSettings::setBin_zoom(size); QSize zoom = m_iconSize; zoom = zoom * (size / 4.0); m_itemView->setIconSize(zoom); QPixmap pix(zoom); pix.fill(Qt::lightGray); m_blankThumb.addPixmap(pix); } void Bin::rebuildMenu() { m_transcodeAction = static_cast(pCore->window()->factory()->container(QStringLiteral("transcoders"), pCore->window())); m_extractAudioAction = static_cast(pCore->window()->factory()->container(QStringLiteral("extract_audio"), pCore->window())); m_clipsActionsMenu = static_cast(pCore->window()->factory()->container(QStringLiteral("clip_actions"), pCore->window())); m_menu->insertMenu(m_reloadAction, m_extractAudioAction); m_menu->insertMenu(m_reloadAction, m_transcodeAction); m_menu->insertMenu(m_reloadAction, m_clipsActionsMenu); m_inTimelineAction = m_menu->insertMenu(m_reloadAction, static_cast(pCore->window()->factory()->container(QStringLiteral("clip_in_timeline"), pCore->window()))); } void Bin::contextMenuEvent(QContextMenuEvent *event) { bool enableClipActions = false; ClipType::ProducerType type = ClipType::Unknown; bool isFolder = false; bool isImported = false; AbstractProjectItem::PROJECTITEMTYPE itemType = AbstractProjectItem::FolderItem; QString clipService; QString audioCodec; if (m_itemView) { QModelIndex idx = m_itemView->indexAt(m_itemView->viewport()->mapFromGlobal(event->globalPos())); if (idx.isValid()) { // User right clicked on a clip std::shared_ptr currentItem = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(idx)); itemType = currentItem->itemType(); if (currentItem) { enableClipActions = true; if (itemType == AbstractProjectItem::ClipItem) { auto clip = std::static_pointer_cast(currentItem); if (clip) { m_proxyAction->blockSignals(true); emit findInTimeline(clip->clipId(), clip->timelineInstances()); clipService = clip->getProducerProperty(QStringLiteral("mlt_service")); m_proxyAction->setChecked(clip->hasProxy()); QList transcodeActions; if (m_transcodeAction) { transcodeActions = m_transcodeAction->actions(); } QStringList dataList; QString condition; audioCodec = clip->codec(true); QString videoCodec = clip->codec(false); type = clip->clipType(); if (clip->hasUrl()) { isImported = true; } bool noCodecInfo = false; if (audioCodec.isEmpty() && videoCodec.isEmpty()) { noCodecInfo = true; } for (int i = 0; i < transcodeActions.count(); ++i) { dataList = transcodeActions.at(i)->data().toStringList(); if (dataList.count() > 4) { condition = dataList.at(4); if (condition.isEmpty()) { transcodeActions.at(i)->setEnabled(true); continue; } if (noCodecInfo) { // No audio / video codec, this is an MLT clip, disable conditionnal transcoding transcodeActions.at(i)->setEnabled(false); continue; } if (condition.startsWith(QLatin1String("vcodec"))) { transcodeActions.at(i)->setEnabled(condition.section(QLatin1Char('='), 1, 1) == videoCodec); } else if (condition.startsWith(QLatin1String("acodec"))) { transcodeActions.at(i)->setEnabled(condition.section(QLatin1Char('='), 1, 1) == audioCodec); } } } } m_proxyAction->blockSignals(false); } else if (itemType == AbstractProjectItem::SubClipItem) { } } } } // Enable / disable clip actions m_proxyAction->setEnabled((m_doc->getDocumentProperty(QStringLiteral("enableproxy")).toInt() != 0) && enableClipActions); m_openAction->setEnabled(type == ClipType::Image || type == ClipType::Audio || type == ClipType::TextTemplate || type == ClipType::Text); m_reloadAction->setEnabled(enableClipActions); m_locateAction->setEnabled(enableClipActions); m_duplicateAction->setEnabled(enableClipActions); m_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); + auto *view = static_cast(m_itemView); view->setExpanded(ix, !view->isExpanded(ix)); return; } } } if (ix.isValid()) { QRect IconRect = m_itemView->visualRect(ix); IconRect.setWidth((double)IconRect.height() / m_itemView->iconSize().height() * m_itemView->iconSize().width()); if (!pos.isNull() && ((ix.column() == 2 && item->itemType() == AbstractProjectItem::ClipItem) || (!IconRect.contains(pos) && pos.y() < (IconRect.y() + IconRect.height() / 2)))) { // User clicked outside icon, trigger rename m_itemView->edit(ix); return; } if (item->itemType() == AbstractProjectItem::ClipItem) { std::shared_ptr clip = std::static_pointer_cast(item); if (clip) { if (clip->clipType() == ClipType::Text || clip->clipType() == ClipType::TextTemplate) { // m_propertiesPanel->setEnabled(false); showTitleWidget(clip); } else { slotSwitchClipProperties(clip); } } } } } void Bin::slotEditClip() { QString panelId = m_propertiesPanel->property("clipId").toString(); QModelIndex current = m_proxyModel->selectionModel()->currentIndex(); std::shared_ptr item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(current)); if (item->clipId() != panelId) { // wrong clip return; } auto clip = std::static_pointer_cast(item); QString parentFolder = getCurrentFolder(); switch (clip->clipType()) { case ClipType::Text: case ClipType::TextTemplate: showTitleWidget(clip); break; case ClipType::SlideShow: showSlideshowWidget(clip); break; case ClipType::QText: ClipCreationDialog::createQTextClip(m_doc, parentFolder, this, clip.get()); break; default: break; } } void Bin::slotSwitchClipProperties() { QModelIndex current = m_proxyModel->selectionModel()->currentIndex(); if (current.isValid()) { // User clicked in the icon, open clip properties std::shared_ptr item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(current)); std::shared_ptr currentItem = nullptr; if (item->itemType() == AbstractProjectItem::ClipItem) { currentItem = std::static_pointer_cast(item); } else if (item->itemType() == AbstractProjectItem::SubClipItem) { currentItem = std::static_pointer_cast(item)->getMasterClip(); } if (currentItem) { slotSwitchClipProperties(currentItem); return; } } slotSwitchClipProperties(nullptr); } void Bin::slotSwitchClipProperties(const std::shared_ptr &clip) { if (clip == nullptr) { m_propertiesPanel->setEnabled(false); return; } if (clip->clipType() == ClipType::SlideShow) { m_propertiesPanel->setEnabled(false); showSlideshowWidget(clip); } else if (clip->clipType() == ClipType::QText) { m_propertiesPanel->setEnabled(false); QString parentFolder = getCurrentFolder(); ClipCreationDialog::createQTextClip(m_doc, parentFolder, this, clip.get()); } else { m_propertiesPanel->setEnabled(true); showClipProperties(clip); m_propertiesDock->show(); m_propertiesDock->raise(); } // Check if properties panel is not tabbed under Bin // if (!pCore->window()->isTabbedWith(m_propertiesDock, QStringLiteral("project_bin"))) { } void Bin::doRefreshPanel(const QString &id) { std::shared_ptr currentItem = getFirstSelectedClip(); if ((currentItem != nullptr) && currentItem->AbstractProjectItem::clipId() == id) { showClipProperties(currentItem, true); } } 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()); + auto *lay = static_cast(m_propertiesPanel->layout()); if (lay == nullptr) { lay = new QVBoxLayout(m_propertiesPanel); m_propertiesPanel->setLayout(lay); } ClipPropertiesController *panel = clip->buildProperties(m_propertiesPanel); connect(this, &Bin::refreshTimeCode, panel, &ClipPropertiesController::slotRefreshTimeCode); connect(panel, &ClipPropertiesController::updateClipProperties, this, &Bin::slotEditClipCommand); connect(panel, &ClipPropertiesController::seekToFrame, m_monitor, static_cast(&Monitor::slotSeek)); connect(panel, &ClipPropertiesController::editClip, this, &Bin::slotEditClip); connect(panel, SIGNAL(editAnalysis(QString, QString, QString)), this, SLOT(slotAddClipExtraData(QString, QString, QString))); lay->addWidget(panel); } void Bin::slotEditClipCommand(const QString &id, const QMap &oldProps, const QMap &newProps) { auto *command = new EditClipCommand(this, id, oldProps, newProps, true); m_doc->commandStack()->push(command); } void Bin::reloadClip(const QString &id) { std::shared_ptr clip = m_itemModel->getClipByBinID(id); if (!clip) { return; } clip->reloadProducer(); } void Bin::reloadMonitorIfActive(const QString &id) { if (m_monitor->activeClipId() == id) { slotOpenCurrent(); } } QStringList Bin::getBinFolderClipIds(const QString &id) const { QStringList ids; std::shared_ptr folder = m_itemModel->getFolderByBinId(id); if (folder) { for (int i = 0; i < folder->childCount(); i++) { std::shared_ptr child = std::static_pointer_cast(folder->child(i)); if (child->itemType() == AbstractProjectItem::ClipItem) { ids << child->clipId(); } } } return ids; } std::shared_ptr Bin::getBinClip(const QString &id) { std::shared_ptr clip = nullptr; if (id.contains(QLatin1Char('_'))) { clip = m_itemModel->getClipByBinID(id.section(QLatin1Char('_'), 0, 0)); } else if (!id.isEmpty()) { clip = m_itemModel->getClipByBinID(id); } return clip; } void Bin::setWaitingStatus(const QString &id) { std::shared_ptr clip = m_itemModel->getClipByBinID(id); if (clip) { clip->setClipStatus(AbstractProjectItem::StatusWaiting); } } void Bin::slotRemoveInvalidClip(const QString &id, bool replace, const QString &errorMessage) { Q_UNUSED(replace); std::shared_ptr clip = m_itemModel->getClipByBinID(id); if (!clip) { return; } emit requesteInvalidRemoval(id, clip->url(), errorMessage); } void Bin::selectClip(const std::shared_ptr &clip) { QModelIndex ix = m_itemModel->getIndexFromItem(clip); int row = ix.row(); const QModelIndex id = m_itemModel->index(row, 0, ix.parent()); const QModelIndex id2 = m_itemModel->index(row, m_itemModel->columnCount() - 1, ix.parent()); if (id.isValid() && id2.isValid()) { m_proxyModel->selectionModel()->select(QItemSelection(m_proxyModel->mapFromSource(id), m_proxyModel->mapFromSource(id2)), QItemSelectionModel::Select); } m_itemView->scrollTo(m_proxyModel->mapFromSource(ix)); } void Bin::slotOpenCurrent() { std::shared_ptr currentItem = getFirstSelectedClip(); if (currentItem) { emit openClip(currentItem); } } void Bin::openProducer(std::shared_ptr controller) { emit openClip(std::move(controller)); } void Bin::openProducer(std::shared_ptr controller, int in, int out) { emit openClip(std::move(controller), in, out); } void Bin::emitItemUpdated(std::shared_ptr item) { emit itemUpdated(std::move(item)); } void Bin::emitRefreshPanel(const QString &id) { emit refreshPanel(id); } void Bin::setupGeneratorMenu() { if (!m_menu) { qCDebug(KDENLIVE_LOG) << "Warning, menu was not created, something is wrong"; return; } - QMenu *addMenu = qobject_cast(pCore->window()->factory()->container(QStringLiteral("generators"), pCore->window())); + auto *addMenu = qobject_cast(pCore->window()->factory()->container(QStringLiteral("generators"), pCore->window())); if (addMenu) { QMenu *menu = m_addButton->menu(); menu->addMenu(addMenu); addMenu->setEnabled(!addMenu->isEmpty()); m_addButton->setMenu(menu); } addMenu = qobject_cast(pCore->window()->factory()->container(QStringLiteral("extract_audio"), pCore->window())); if (addMenu) { m_menu->addMenu(addMenu); addMenu->setEnabled(!addMenu->isEmpty()); m_extractAudioAction = addMenu; } addMenu = qobject_cast(pCore->window()->factory()->container(QStringLiteral("transcoders"), pCore->window())); if (addMenu) { m_menu->addMenu(addMenu); addMenu->setEnabled(!addMenu->isEmpty()); m_transcodeAction = addMenu; } addMenu = qobject_cast(pCore->window()->factory()->container(QStringLiteral("clip_actions"), pCore->window())); if (addMenu) { m_menu->addMenu(addMenu); addMenu->setEnabled(!addMenu->isEmpty()); m_clipsActionsMenu = addMenu; } addMenu = qobject_cast(pCore->window()->factory()->container(QStringLiteral("clip_in_timeline"), pCore->window())); if (addMenu) { m_inTimelineAction = m_menu->addMenu(addMenu); m_inTimelineAction->setEnabled(!addMenu->isEmpty()); } if (m_locateAction) { m_menu->addAction(m_locateAction); } if (m_reloadAction) { m_menu->addAction(m_reloadAction); } if (m_duplicateAction) { m_menu->addAction(m_duplicateAction); } if (m_proxyAction) { m_menu->addAction(m_proxyAction); } addMenu = qobject_cast(pCore->window()->factory()->container(QStringLiteral("clip_timeline"), pCore->window())); if (addMenu) { m_menu->addMenu(addMenu); addMenu->setEnabled(false); } m_menu->addAction(m_editAction); m_menu->addAction(m_openAction); m_menu->addAction(m_renameAction); m_menu->addAction(m_deleteAction); m_menu->insertSeparator(m_deleteAction); } void Bin::setupMenu(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()); + auto *act = qobject_cast(sender()); if (act == nullptr) { // Cannot access triggering action, something is wrong qCDebug(KDENLIVE_LOG) << "// Error in clip creation action"; return; } ClipType::ProducerType type = (ClipType::ProducerType)act->data().toInt(); 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(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(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); + auto *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); + auto *view = qobject_cast(m_itemView); if (view) { view->setColumnHidden(1, !show); } } void Bin::slotShowDescColumn(bool show) { - QTreeView *view = qobject_cast(m_itemView); + auto *view = qobject_cast(m_itemView); if (view) { view->setColumnHidden(2, !show); } } void Bin::slotQueryRemoval(const QString &id, const QString &url, const QString &errorMessage) { if (m_invalidClipDialog) { if (!url.isEmpty()) { m_invalidClipDialog->addClip(id, url); } return; } QString message = i18n("Clip is invalid, will be removed from project."); if (!errorMessage.isEmpty()) { message.append("\n" + errorMessage); } m_invalidClipDialog = new InvalidDialog(i18n("Invalid clip"), message, true, this); m_invalidClipDialog->addClip(id, url); int result = m_invalidClipDialog->exec(); if (result == QDialog::Accepted) { const QStringList ids = m_invalidClipDialog->getIds(); Fun undo = []() { return true; }; Fun redo = []() { return true; }; for (const QString &i : ids) { auto item = m_itemModel->getClipByBinID(i); m_itemModel->requestBinClipDeletion(item, undo, redo); } } delete m_invalidClipDialog; m_invalidClipDialog = nullptr; } void Bin::slotRefreshClipThumbnail(const QString &id) { std::shared_ptr clip = m_itemModel->getClipByBinID(id); if (!clip) { return; } clip->reloadProducer(true); } void Bin::slotAddClipExtraData(const QString &id, const QString &key, const QString &clipData, QUndoCommand *groupCommand) { std::shared_ptr clip = m_itemModel->getClipByBinID(id); if (!clip) { return; } QString oldValue = clip->getProducerProperty(key); QMap oldProps; oldProps.insert(key, oldValue); QMap newProps; newProps.insert(key, clipData); auto *command = new EditClipCommand(this, id, oldProps, newProps, true, groupCommand); if (!groupCommand) { m_doc->commandStack()->push(command); } } void Bin::slotUpdateClipProperties(const QString &id, const QMap &properties, bool refreshPropertiesPanel) { std::shared_ptr clip = m_itemModel->getClipByBinID(id); if (clip) { clip->setProperties(properties, refreshPropertiesPanel); } } void Bin::updateTimelineProducers(const QString &id, const QMap &passProperties) { Q_UNUSED(id) Q_UNUSED(passProperties) // TODO REFAC // pCore->projectManager()->currentTimeline()->updateClipProperties(id, passProperties); // m_doc->renderer()->updateSlowMotionProducers(id, passProperties); } void Bin::showSlideshowWidget(const std::shared_ptr &clip) { QString folder = QFileInfo(clip->url()).absolutePath(); qCDebug(KDENLIVE_LOG) << " ** * CLIP ABS PATH: " << clip->url() << " = " << folder; SlideshowClip *dia = new SlideshowClip(m_doc->timecode(), folder, clip.get(), this); if (dia->exec() == QDialog::Accepted) { // edit clip properties QMap properties; properties.insert(QStringLiteral("out"), clip->framesToTime(m_doc->getFramePos(dia->clipDuration()) * dia->imageCount() - 1)); properties.insert(QStringLiteral("kdenlive:duration"), clip->framesToTime(m_doc->getFramePos(dia->clipDuration()) * dia->imageCount())); properties.insert(QStringLiteral("kdenlive:clipname"), dia->clipName()); properties.insert(QStringLiteral("ttl"), QString::number(m_doc->getFramePos(dia->clipDuration()))); properties.insert(QStringLiteral("loop"), QString::number(static_cast(dia->loop()))); properties.insert(QStringLiteral("crop"), QString::number(static_cast(dia->crop()))); properties.insert(QStringLiteral("fade"), QString::number(static_cast(dia->fade()))); properties.insert(QStringLiteral("luma_duration"), QString::number(m_doc->getFramePos(dia->lumaDuration()))); properties.insert(QStringLiteral("luma_file"), dia->lumaFile()); properties.insert(QStringLiteral("softness"), QString::number(dia->softness())); properties.insert(QStringLiteral("animation"), dia->animation()); QMap oldProperties; oldProperties.insert(QStringLiteral("out"), clip->getProducerProperty(QStringLiteral("out"))); oldProperties.insert(QStringLiteral("kdenlive:duration"), clip->getProducerProperty(QStringLiteral("kdenlive:duration"))); oldProperties.insert(QStringLiteral("kdenlive:clipname"), clip->name()); oldProperties.insert(QStringLiteral("ttl"), clip->getProducerProperty(QStringLiteral("ttl"))); oldProperties.insert(QStringLiteral("loop"), clip->getProducerProperty(QStringLiteral("loop"))); oldProperties.insert(QStringLiteral("crop"), clip->getProducerProperty(QStringLiteral("crop"))); oldProperties.insert(QStringLiteral("fade"), clip->getProducerProperty(QStringLiteral("fade"))); oldProperties.insert(QStringLiteral("luma_duration"), clip->getProducerProperty(QStringLiteral("luma_duration"))); oldProperties.insert(QStringLiteral("luma_file"), clip->getProducerProperty(QStringLiteral("luma_file"))); oldProperties.insert(QStringLiteral("softness"), clip->getProducerProperty(QStringLiteral("softness"))); oldProperties.insert(QStringLiteral("animation"), clip->getProducerProperty(QStringLiteral("animation"))); slotEditClipCommand(clip->AbstractProjectItem::clipId(), oldProperties, properties); } delete dia; } void Bin::setBinEffectsEnabled(bool enabled) { QAction *disableEffects = pCore->window()->actionCollection()->action(QStringLiteral("disable_bin_effects")); if (disableEffects) { if (enabled == disableEffects->isChecked()) { return; } disableEffects->blockSignals(true); disableEffects->setChecked(!enabled); disableEffects->blockSignals(false); } m_itemModel->setBinEffectsEnabled(enabled); pCore->projectManager()->disableBinEffects(!enabled); } void Bin::slotRenameItem() { const QModelIndexList indexes = m_proxyModel->selectionModel()->selectedRows(0); for (const QModelIndex &ix : indexes) { if (!ix.isValid()) { continue; } m_itemView->setCurrentIndex(ix); m_itemView->edit(ix); return; } } void Bin::refreshProxySettings() { QList> clipList = m_itemModel->getRootFolder()->childClips(); auto *masterCommand = new QUndoCommand(); masterCommand->setText(m_doc->useProxy() ? i18n("Enable proxies") : i18n("Disable proxies")); // en/disable proxy option in clip properties for (QWidget *w : m_propertiesPanel->findChildren()) { static_cast(w)->enableProxy(m_doc->useProxy()); } if (!m_doc->useProxy()) { // Disable all proxies m_doc->slotProxyCurrentItem(false, clipList, false, masterCommand); } else { QList> toProxy; for (const std::shared_ptr &clp : clipList) { ClipType::ProducerType t = clp->clipType(); if (t == ClipType::Playlist) { toProxy << clp; continue; } else if ((t == ClipType::AV || t == ClipType::Video) && m_doc->autoGenerateProxy(clp->getProducerIntProperty(QStringLiteral("meta.media.width")))) { // Start proxy toProxy << clp; continue; } else if (t == ClipType::Image && m_doc->autoGenerateImageProxy(clp->getProducerIntProperty(QStringLiteral("meta.media.width")))) { // Start proxy toProxy << clp; continue; } } if (!toProxy.isEmpty()) { m_doc->slotProxyCurrentItem(true, toProxy, false, masterCommand); } } if (masterCommand->childCount() > 0) { m_doc->commandStack()->push(masterCommand); } else { delete masterCommand; } } QStringList Bin::getProxyHashList() { QStringList list; QList> clipList = m_itemModel->getRootFolder()->childClips(); for (const std::shared_ptr &clp : clipList) { if (clp->clipType() == ClipType::AV || clp->clipType() == ClipType::Video || clp->clipType() == ClipType::Playlist) { list << clp->hash(); } } return list; } void Bin::slotSendAudioThumb(const QString &id) { std::shared_ptr clip = m_itemModel->getClipByBinID(id); if ((clip != nullptr) && clip->audioThumbCreated()) { m_monitor->prepareAudioThumb(clip->audioChannels(), clip->audioFrameCache); } else { 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 (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 (const std::shared_ptr &clip : clipList) { clip->setRefCount(0); } } void Bin::getBinStats(uint *used, uint *unused, qint64 *usedSize, qint64 *unusedSize) { QList> clipList = m_itemModel->getRootFolder()->childClips(); for (const std::shared_ptr &clip : clipList) { if (clip->refCount() == 0) { *unused += 1; *unusedSize += clip->getProducerInt64Property(QStringLiteral("kdenlive:file_size")); } else { *used += 1; *usedSize += clip->getProducerInt64Property(QStringLiteral("kdenlive:file_size")); } } } QDir Bin::getCacheDir(CacheType type, bool *ok) const { return m_doc->getCacheDir(type, ok); } void Bin::rebuildProxies() { QList> clipList = m_itemModel->getRootFolder()->childClips(); QList> toProxy; for (const std::shared_ptr &clp : clipList) { if (clp->hasProxy()) { toProxy << clp; // Abort all pending jobs pCore->jobManager()->discardJobs(clp->clipId(), AbstractClipJob::PROXYJOB); clp->deleteProxy(); } } if (toProxy.isEmpty()) { return; } auto *masterCommand = new QUndoCommand(); masterCommand->setText(i18n("Rebuild proxies")); m_doc->slotProxyCurrentItem(true, toProxy, true, masterCommand); if (masterCommand->childCount() > 0) { m_doc->commandStack()->push(masterCommand); } else { delete masterCommand; } } void Bin::showClearButton(bool show) { m_searchLine->setClearButtonEnabled(show); } void Bin::saveZone(const QStringList &info, const QDir &dir) { if (info.size() != 3) { return; } std::shared_ptr clip = getBinClip(info.constFirst()); if (clip) { QPoint zone(info.at(1).toInt(), info.at(2).toInt()); clip->saveZone(zone, dir); } } void Bin::setCurrent(const std::shared_ptr &item) { switch (item->itemType()) { case AbstractProjectItem::ClipItem: { openProducer(std::static_pointer_cast(item)); std::shared_ptr clp = std::static_pointer_cast(item); emit requestShowEffectStack(clp->clipName(), clp->m_effectStack, clp->getFrameSize(), false); break; } case AbstractProjectItem::SubClipItem: { auto subClip = std::static_pointer_cast(item); QPoint zone = subClip->zone(); openProducer(subClip->getMasterClip(), zone.x(), zone.y()); break; } case AbstractProjectItem::FolderUpItem: case AbstractProjectItem::FolderItem: default: break; } } void Bin::cleanup() { m_itemModel->requestCleanup(); } std::shared_ptr Bin::getClipEffectStack(int itemId) { std::shared_ptr clip = m_itemModel->getClipByBinID(QString::number(itemId)); Q_ASSERT(clip != nullptr); std::shared_ptr effectStack = std::static_pointer_cast(clip)->m_effectStack; return effectStack; } size_t Bin::getClipDuration(int itemId) const { std::shared_ptr clip = m_itemModel->getClipByBinID(QString::number(itemId)); Q_ASSERT(clip != nullptr); return clip->frameDuration(); } PlaylistState::ClipState Bin::getClipState(int itemId) const { std::shared_ptr clip = m_itemModel->getClipByBinID(QString::number(itemId)); Q_ASSERT(clip != nullptr); bool audio = clip->hasAudio(); bool video = clip->hasVideo(); return audio ? (video ? PlaylistState::Disabled : PlaylistState::AudioOnly) : PlaylistState::VideoOnly; } QString Bin::getCurrentFolder() { // Check parent item QModelIndex ix = m_proxyModel->selectionModel()->currentIndex(); std::shared_ptr parentFolder = m_itemModel->getRootFolder(); if (ix.isValid() && m_proxyModel->selectionModel()->isSelected(ix)) { std::shared_ptr currentItem = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(ix)); parentFolder = std::static_pointer_cast(currentItem->getEnclosingFolder()); } return parentFolder->clipId(); } void Bin::adjustProjectProfileToItem() { QModelIndex current = m_proxyModel->selectionModel()->currentIndex(); if (current.isValid()) { // User clicked in the icon, open clip properties std::shared_ptr item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(current)); auto clip = std::static_pointer_cast(item); if (clip) { QDomDocument doc; LoadJob::checkProfile(clip->clipId(), clip->toXml(doc, false), clip->originalProducer()); } } } diff --git a/src/bin/bin.h b/src/bin/bin.h index 0dedf0145..28f452d52 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; + QAction *m_action{nullptr}; 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(); + explicit Bin(std::shared_ptr model, QWidget *parent = nullptr); + ~Bin() override; 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(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(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(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(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(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/bincommands.cpp b/src/bin/bincommands.cpp index 051cc31c3..ebb556560 100644 --- a/src/bin/bincommands.cpp +++ b/src/bin/bincommands.cpp @@ -1,114 +1,112 @@ /*************************************************************************** * Copyright (C) 2015 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 "bincommands.h" #include "bin.h" #include - -MoveBinClipCommand::MoveBinClipCommand(Bin *bin, const QString &clipId, const QString &oldParentId, const QString &newParentId, QUndoCommand *parent) +#include +MoveBinClipCommand::MoveBinClipCommand(Bin *bin, QString clipId, QString oldParentId, QString newParentId, QUndoCommand *parent) : QUndoCommand(parent) , m_bin(bin) - , m_clipId(clipId) - , m_oldParentId(oldParentId) - , m_newParentId(newParentId) + , m_clipId(std::move(clipId)) + , m_oldParentId(std::move(oldParentId)) + , m_newParentId(std::move(newParentId)) { setText(i18n("Move Clip")); } // virtual void MoveBinClipCommand::undo() { m_bin->doMoveClip(m_clipId, m_oldParentId); } // virtual void MoveBinClipCommand::redo() { m_bin->doMoveClip(m_clipId, m_newParentId); } -MoveBinFolderCommand::MoveBinFolderCommand(Bin *bin, const QString &clipId, const QString &oldParentId, const QString &newParentId, QUndoCommand *parent) +MoveBinFolderCommand::MoveBinFolderCommand(Bin *bin, QString clipId, QString oldParentId, QString newParentId, QUndoCommand *parent) : QUndoCommand(parent) , m_bin(bin) - , m_clipId(clipId) - , m_oldParentId(oldParentId) - , m_newParentId(newParentId) + , m_clipId(std::move(clipId)) + , m_oldParentId(std::move(oldParentId)) + , m_newParentId(std::move(newParentId)) { setText(i18n("Move Clip")); } // virtual void MoveBinFolderCommand::undo() { m_bin->doMoveFolder(m_clipId, m_oldParentId); } // virtual void MoveBinFolderCommand::redo() { m_bin->doMoveFolder(m_clipId, m_newParentId); } -RenameBinSubClipCommand::RenameBinSubClipCommand(Bin *bin, const QString &clipId, const QString &newName, const QString &oldName, int in, int out, - QUndoCommand *parent) +RenameBinSubClipCommand::RenameBinSubClipCommand(Bin *bin, QString clipId, QString newName, QString oldName, int in, int out, QUndoCommand *parent) : QUndoCommand(parent) , m_bin(bin) - , m_clipId(clipId) - , m_oldName(oldName) - , m_newName(newName) + , m_clipId(std::move(clipId)) + , m_oldName(std::move(oldName)) + , m_newName(std::move(newName)) , m_in(in) , m_out(out) { setText(i18n("Rename Zone")); } // virtual void RenameBinSubClipCommand::undo() { m_bin->renameSubClip(m_clipId, m_oldName, m_newName, m_in, m_out); } // virtual void RenameBinSubClipCommand::redo() { m_bin->renameSubClip(m_clipId, m_newName, m_oldName, m_in, m_out); } -EditClipCommand::EditClipCommand(Bin *bin, const QString &id, const QMap &oldparams, const QMap &newparams, bool doIt, - QUndoCommand *parent) +EditClipCommand::EditClipCommand(Bin *bin, QString id, QMap oldparams, QMap newparams, bool doIt, QUndoCommand *parent) : QUndoCommand(parent) , m_bin(bin) - , m_oldparams(oldparams) - , m_newparams(newparams) - , m_id(id) + , m_oldparams(std::move(oldparams)) + , m_newparams(std::move(newparams)) + , m_id(std::move(id)) , m_doIt(doIt) , m_firstExec(true) { setText(i18n("Edit clip")); } // virtual void EditClipCommand::undo() { m_bin->slotUpdateClipProperties(m_id, m_oldparams, true); } // virtual void EditClipCommand::redo() { if (m_doIt) { m_bin->slotUpdateClipProperties(m_id, m_newparams, !m_firstExec); } m_doIt = true; m_firstExec = false; } diff --git a/src/bin/bincommands.h b/src/bin/bincommands.h index 39b3666e7..84bb869d5 100644 --- a/src/bin/bincommands.h +++ b/src/bin/bincommands.h @@ -1,96 +1,94 @@ /*************************************************************************** * Copyright (C) 2015 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 BINCOMMANDS_H #define BINCOMMANDS_H #include #include #include class Bin; class MoveBinClipCommand : public QUndoCommand { public: - explicit MoveBinClipCommand(Bin *bin, const QString &clipId, const QString &oldParentId, const QString &newParentId, QUndoCommand *parent = nullptr); + explicit MoveBinClipCommand(Bin *bin, QString clipId, QString oldParentId, QString newParentId, QUndoCommand *parent = nullptr); void undo() override; void redo() override; private: Bin *m_bin; QString m_clipId; QString m_oldParentId; QString m_newParentId; }; class MoveBinFolderCommand : public QUndoCommand { public: - explicit MoveBinFolderCommand(Bin *bin, const QString &clipId, const QString &oldParentId, const QString &newParentId, QUndoCommand *parent = nullptr); + explicit MoveBinFolderCommand(Bin *bin, QString clipId, QString oldParentId, QString newParentId, QUndoCommand *parent = nullptr); void undo() override; void redo() override; private: Bin *m_bin; QString m_clipId; QString m_oldParentId; QString m_newParentId; }; class RenameBinSubClipCommand : public QUndoCommand { public: - explicit RenameBinSubClipCommand(Bin *bin, const QString &clipId, const QString &newName, const QString &oldName, int in, int out, - QUndoCommand *parent = nullptr); + explicit RenameBinSubClipCommand(Bin *bin, QString clipId, QString newName, QString oldName, int in, int out, QUndoCommand *parent = nullptr); void undo() override; void redo() override; private: Bin *m_bin; QString m_clipId; QString m_oldName; QString m_newName; int m_in; int m_out; }; class EditClipCommand : public QUndoCommand { public: - EditClipCommand(Bin *bin, const QString &id, const QMap &oldparams, const QMap &newparams, bool doIt, - QUndoCommand *parent = nullptr); + EditClipCommand(Bin *bin, QString id, QMap oldparams, QMap newparams, bool doIt, QUndoCommand *parent = nullptr); void undo() override; void redo() override; private: Bin *m_bin; QMap m_oldparams; QMap m_newparams; QString m_id; /** @brief Should this command be executed on first redo ? TODO: we should refactor the code to get rid of this and always execute actions through the *command system. *. */ bool m_doIt; /** @brief This value is true is this is the first time we execute the command, false otherwise. This allows us to refresh the properties panel * only on the later executions of the command, since on the first execution, the properties panel already contains the correct info. */ bool m_firstExec; }; #endif diff --git a/src/bin/generators/generators.cpp b/src/bin/generators/generators.cpp index 2f3ec92d5..7f4897490 100644 --- a/src/bin/generators/generators.cpp +++ b/src/bin/generators/generators.cpp @@ -1,200 +1,200 @@ /*************************************************************************** * 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 "generators.h" #include "assets/abstractassetsrepository.hpp" #include "doc/kthumb.h" #include "effects/effectsrepository.hpp" #include "kdenlivesettings.h" #include "monitor/monitor.h" #include #include #include #include #include #include #include #include "klocalizedstring.h" #include "kxmlgui_version.h" #include #include - +#include #include #include #include #include Generators::Generators(Monitor *monitor, const QString &path, QWidget *parent) : QDialog(parent) , m_producer(nullptr) , m_timePos(nullptr) , m_container(nullptr) , m_preview(nullptr) { QFile file(path); QDomDocument doc; doc.setContent(&file, false); file.close(); QDomElement base = doc.documentElement(); if (base.tagName() == QLatin1String("generator")) { QString generatorTag = base.attribute(QStringLiteral("tag")); setWindowTitle(base.firstChildElement(QStringLiteral("name")).text()); auto *lay = new QVBoxLayout(this); m_preview = new QLabel; m_preview->setMinimumSize(1, 1); lay->addWidget(m_preview); m_producer = new Mlt::Producer(*monitor->profile(), generatorTag.toUtf8().constData()); m_pixmap = QPixmap::fromImage(KThumb::getFrame(m_producer, 0, monitor->profile()->width(), monitor->profile()->height())); m_preview->setPixmap(m_pixmap.scaledToWidth(m_preview->width())); auto *hlay = new QHBoxLayout; hlay->addWidget(new QLabel(i18n("Duration"))); m_timePos = new TimecodeDisplay(monitor->timecode(), this); if (base.hasAttribute(QStringLiteral("updateonduration"))) { connect(m_timePos, &TimecodeDisplay::timeCodeEditingFinished, this, &Generators::updateDuration); } hlay->addWidget(m_timePos); lay->addLayout(hlay); QWidget *frameWidget = new QWidget; lay->addWidget(frameWidget); m_view = new AssetParameterView(frameWidget); lay->addWidget(m_view); QString tag = base.attribute(QStringLiteral("tag"), QString()); QString id = base.hasAttribute(QStringLiteral("id")) ? base.attribute(QStringLiteral("id")) : tag; auto prop = std::make_unique(m_producer->get_properties()); - m_assetModel = std::shared_ptr(new AssetParameterModel(std::move(prop), base, tag, {ObjectType::NoItem, -1})); + m_assetModel.reset(new AssetParameterModel(std::move(prop), base, tag, {ObjectType::NoItem, -1})); // NOLINT m_view->setModel(m_assetModel, QSize(1920, 1080), false); connect(m_assetModel.get(), &AssetParameterModel::modelChanged, [this]() { updateProducer(); }); lay->addStretch(10); 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); m_timePos->setValue(KdenliveSettings::color_duration()); } } void Generators::updateProducer() { int w = m_pixmap.width(); int h = m_pixmap.height(); m_pixmap = QPixmap::fromImage(KThumb::getFrame(m_producer, 0, w, h)); m_preview->setPixmap(m_pixmap.scaledToWidth(m_preview->width())); } void Generators::resizeEvent(QResizeEvent *event) { QDialog::resizeEvent(event); m_preview->setPixmap(m_pixmap.scaledToWidth(m_preview->width())); } Generators::~Generators() { delete m_timePos; } // static void Generators::getGenerators(const QStringList &producers, QMenu *menu) { const QStringList generatorFolders = QStandardPaths::locateAll(QStandardPaths::AppDataLocation, QStringLiteral("generators"), QStandardPaths::LocateDirectory); const QStringList filters = QStringList() << QStringLiteral("*.xml"); for (const QString &folder : generatorFolders) { QDir directory(folder); const QStringList filesnames = directory.entryList(filters, QDir::Files); for (const QString &fname : filesnames) { QPair result = parseGenerator(directory.absoluteFilePath(fname), producers); if (!result.first.isEmpty()) { QAction *action = menu->addAction(result.first); action->setData(result.second); } } } } // static QPair Generators::parseGenerator(const QString &path, const QStringList &producers) { QPair result; QDomDocument doc; QFile file(path); doc.setContent(&file, false); file.close(); QDomElement base = doc.documentElement(); if (base.tagName() == QLatin1String("generator")) { QString generatorTag = base.attribute(QStringLiteral("tag")); if (producers.contains(generatorTag)) { result.first = base.firstChildElement(QStringLiteral("name")).text(); result.second = path; } } return result; } void Generators::updateDuration(int duration) { m_producer->set("length", duration); m_producer->set_in_and_out(0, duration - 1); updateProducer(); } QUrl Generators::getSavedClip(QString clipFolder) { if (clipFolder.isEmpty()) { clipFolder = KRecentDirs::dir(QStringLiteral(":KdenliveClipFolder")); } if (clipFolder.isEmpty()) { clipFolder = QDir::homePath(); } QFileDialog fd(this); fd.setDirectory(clipFolder); fd.setNameFilter(i18n("MLT playlist (*.mlt)")); fd.setAcceptMode(QFileDialog::AcceptSave); fd.setFileMode(QFileDialog::AnyFile); fd.setDefaultSuffix(QStringLiteral("mlt")); if (fd.exec() != QDialog::Accepted || fd.selectedUrls().isEmpty()) { return QUrl(); } QUrl url = fd.selectedUrls().constFirst(); if (url.isValid()) { #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(url.toLocalFile())) { if (KMessageBox::warningYesNo(this, i18n("Output file already exists. Do you want to overwrite it?")) != KMessageBox::Yes) { return getSavedClip(url.toLocalFile()); } } #endif Mlt::Tractor trac(*m_producer->profile()); m_producer->set("length", m_timePos->getValue()); m_producer->set_in_and_out(0, m_timePos->getValue() - 1); trac.set_track(*m_producer, 0); Mlt::Consumer c(*m_producer->profile(), "xml", url.toLocalFile().toUtf8().constData()); c.connect(trac); c.run(); return url; } return QUrl(); } diff --git a/src/bin/generators/generators.h b/src/bin/generators/generators.h index 520164d21..01b80f58f 100644 --- a/src/bin/generators/generators.h +++ b/src/bin/generators/generators.h @@ -1,75 +1,75 @@ /*************************************************************************** * 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 * ***************************************************************************/ #ifndef GENERATORS_H #define GENERATORS_H #include "assets/model/assetparametermodel.hpp" #include "assets/view/assetparameterview.hpp" #include #include #include #include #include /** * @class Generators * @brief Base class for clip generators. * */ namespace Mlt { class Producer; } class QLabel; class QResizeEvent; class ParameterContainer; class Monitor; class TimecodeDisplay; class Generators : public QDialog { Q_OBJECT public: explicit Generators(Monitor *monitor, const QString &path, QWidget *parent = nullptr); - virtual ~Generators(); + ~Generators() override; static void getGenerators(const QStringList &producers, QMenu *menu); static QPair parseGenerator(const QString &path, const QStringList &producers); QUrl getSavedClip(QString path = QString()); void resizeEvent(QResizeEvent *event) override; private: Mlt::Producer *m_producer; TimecodeDisplay *m_timePos; ParameterContainer *m_container; AssetParameterView *m_view; std::shared_ptr m_assetModel; QLabel *m_preview; QPixmap m_pixmap; private slots: void updateProducer(); void updateDuration(int duration); }; #endif diff --git a/src/bin/model/markerlistmodel.cpp b/src/bin/model/markerlistmodel.cpp index 5116401e8..5c0c99519 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 +#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) +MarkerListModel::MarkerListModel(QString clipId, std::weak_ptr undo_stack, QObject *parent) : QAbstractListModel(parent) , m_undoStack(std::move(undo_stack)) , m_guide(false) - , m_clipId(clipId) + , m_clipId(std::move(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(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 f1daa955e..74ee6806d 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); + explicit MarkerListModel(QString clipId, std::weak_ptr undo_stack, QObject *parent = nullptr); /* @brief Construct a guide list (bound to the timeline) */ MarkerListModel(std::weak_ptr undo_stack, QObject *parent = nullptr); enum { CommentRole = Qt::UserRole + 1, PosRole, FrameRole, ColorRole, TypeRole }; /* @brief Adds a marker at the given position. If there is already one, the comment will be overridden @param pos defines the position of the marker, relative to the clip @param comment is the text associated with the marker @param type is the type (color) associated with the marker. If -1 is passed, then the value is pulled from kdenlive's defaults */ bool addMarker(GenTime pos, const QString &comment, int type = -1); protected: /* @brief Same function but accumulates undo/redo */ bool addMarker(GenTime pos, const QString &comment, int type, Fun &undo, Fun &redo); public: /* @brief Removes the marker at the given position. */ 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(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 6761f692d..58629c128 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 +#include #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-parameter" #pragma GCC diagnostic ignored "-Wsign-conversion" #pragma GCC diagnostic ignored "-Wfloat-equal" #pragma GCC diagnostic ignored "-Wshadow" #pragma GCC diagnostic ignored "-Wpedantic" #include #pragma GCC diagnostic pop RTTR_REGISTRATION { using namespace rttr; registration::class_("ProjectClip"); } ProjectClip::ProjectClip(const QString &id, const QIcon &thumb, const std::shared_ptr &model, std::shared_ptr producer) : AbstractProjectItem(AbstractProjectItem::ClipItem, id, model) , ClipController(id, std::move(producer)) , m_thumbsProducer(nullptr) { m_markerModel = std::make_shared(id, pCore->projectManager()->undoStack()); m_clipStatus = StatusReady; m_name = clipName(); m_duration = getStringDuration(); m_inPoint = 0; m_date = date; m_description = ClipController::description(); if (m_clipType == ClipType::Audio) { m_thumbnail = QIcon::fromTheme(QStringLiteral("audio-x-generic")); } else { m_thumbnail = thumb; } // Make sure we have a hash for this clip hash(); connect(m_markerModel.get(), &MarkerListModel::modelChanged, [&]() { setProducerProperty(QStringLiteral("kdenlive:markers"), m_markerModel->toJson()); }); QString markers = getProducerProperty(QStringLiteral("kdenlive:markers")); if (!markers.isEmpty()) { QMetaObject::invokeMethod(m_markerModel.get(), "importFromJson", Qt::QueuedConnection, Q_ARG(const QString &, markers), Q_ARG(bool, true), Q_ARG(bool, false)); } connectEffectStack(); } // static std::shared_ptr ProjectClip::construct(const QString &id, const QIcon &thumb, const std::shared_ptr &model, const std::shared_ptr &producer) { std::shared_ptr self(new ProjectClip(id, thumb, model, producer)); baseFinishConstruct(self); 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, const std::shared_ptr &model) : AbstractProjectItem(AbstractProjectItem::ClipItem, id, model) , ClipController(id) , m_thumbsProducer(nullptr) { m_clipStatus = StatusWaiting; m_thumbnail = thumb; m_markerModel = std::make_shared(m_binId, pCore->projectManager()->undoStack()); if (description.hasAttribute(QStringLiteral("type"))) { m_clipType = (ClipType::ProducerType)description.attribute(QStringLiteral("type")).toInt(); if (m_clipType == ClipType::Audio) { m_thumbnail = QIcon::fromTheme(QStringLiteral("audio-x-generic")); } } m_temporaryUrl = getXmlProperty(description, QStringLiteral("resource")); QString clipName = getXmlProperty(description, QStringLiteral("kdenlive:clipname")); if (!clipName.isEmpty()) { m_name = clipName; } else if (!m_temporaryUrl.isEmpty()) { m_name = QFileInfo(m_temporaryUrl).fileName(); } else { m_name = i18n("Untitled"); } connect(m_markerModel.get(), &MarkerListModel::modelChanged, [&]() { setProducerProperty(QStringLiteral("kdenlive:markers"), m_markerModel->toJson()); }); } std::shared_ptr ProjectClip::construct(const QString &id, const QDomElement &description, const QIcon &thumb, std::shared_ptr model) { std::shared_ptr self(new ProjectClip(id, description, thumb, std::move(model))); baseFinishConstruct(self); return self; } ProjectClip::~ProjectClip() { // controller is deleted in bincontroller m_thumbMutex.lock(); m_requestedThumbs.clear(); m_thumbMutex.unlock(); m_thumbThread.waitForFinished(); audioFrameCache.clear(); } void ProjectClip::connectEffectStack() { connect(m_effectStack.get(), &EffectStackModel::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(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_timewarpProducers[clipId] = std::make_shared(&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_audioProducers[clipId] = std::make_shared(&master->parent()); m_effectStack->loadService(m_audioProducers[clipId]); return {master, true}; } if (state == PlaylistState::VideoOnly) { // good, we found a master video producer, and we didn't have any - m_videoProducers[clipId] = std::shared_ptr(new Mlt::Producer(&master->parent())); + m_videoProducers[clipId] = std::make_shared(&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(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); + auto *panel = new ClipPropertiesController(static_cast(this), parent); connect(this, &ProjectClip::refreshPropertiesPanel, panel, &ClipPropertiesController::slotReloadProperties); connect(this, &ProjectClip::refreshAnalysisPanel, panel, &ClipPropertiesController::slotFillAnalysisData); connect(panel, &ClipPropertiesController::requestProxy, [this](bool doProxy) { QList> clipList{std::static_pointer_cast(shared_from_this())}; pCore->currentDoc()->slotProxyCurrentItem(doProxy, clipList); }); connect(panel, &ClipPropertiesController::deleteProxy, this, &ProjectClip::deleteProxy); return panel; } void ProjectClip::deleteProxy() { // Disable proxy file QString proxy = getProducerProperty(QStringLiteral("kdenlive:proxy")); QList> clipList{std::static_pointer_cast(shared_from_this())}; pCore->currentDoc()->slotProxyCurrentItem(false, clipList); // Delete bool ok; QDir dir = pCore->currentDoc()->getCacheDir(CacheProxy, &ok); if (ok && proxy.length() > 2) { proxy = QFileInfo(proxy).fileName(); if (dir.exists(proxy)) { dir.remove(proxy); } } } void ProjectClip::updateParent(std::shared_ptr parent) { if (parent) { auto item = std::static_pointer_cast(parent); ClipController::setProducerProperty(QStringLiteral("kdenlive:folderid"), item->clipId()); } AbstractProjectItem::updateParent(parent); } bool ProjectClip::matches(const QString &condition) { // TODO Q_UNUSED(condition) return true; } bool ProjectClip::rename(const QString &name, int column) { QMap newProperites; QMap oldProperites; bool edited = false; switch (column) { case 0: if (m_name == name) { return false; } // Rename clip oldProperites.insert(QStringLiteral("kdenlive:clipname"), m_name); newProperites.insert(QStringLiteral("kdenlive:clipname"), name); m_name = name; edited = true; break; case 2: if (m_description == name) { return false; } // Rename clip if (m_clipType == ClipType::TextTemplate) { oldProperites.insert(QStringLiteral("templatetext"), m_description); newProperites.insert(QStringLiteral("templatetext"), name); } else { oldProperites.insert(QStringLiteral("kdenlive:description"), m_description); newProperites.insert(QStringLiteral("kdenlive:description"), name); } m_description = name; edited = true; break; } if (edited) { pCore->bin()->slotEditClipCommand(m_binId, oldProperites, newProperites); } return edited; } QVariant ProjectClip::getData(DataType type) const { switch (type) { case AbstractProjectItem::IconOverlay: return m_effectStack && m_effectStack->rowCount() > 0 ? QVariant("kdenlive-track_has_effect") : QVariant(); default: return AbstractProjectItem::getData(type); } } 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, const std::shared_ptr &service, bool forceRegister) { if (!service->is_cut() || forceRegister) { int hasAudio = service->get_int("set.test_audio") == 0; int hasVideo = service->get_int("set.test_image") == 0; if (hasVideo && m_videoProducers.count(clipId) == 0) { // This is an undo producer, register it! m_videoProducers[clipId] = service; m_effectStack->addService(m_videoProducers[clipId]); } else if (hasAudio && m_audioProducers.count(clipId) == 0) { // This is an undo producer, register it! m_audioProducers[clipId] = service; m_effectStack->addService(m_audioProducers[clipId]); } } registerTimelineClip(std::move(timeline), clipId); } void ProjectClip::registerTimelineClip(std::weak_ptr timeline, int clipId) { Q_ASSERT(m_registeredClips.count(clipId) == 0); Q_ASSERT(!timeline.expired()); m_registeredClips[clipId] = std::move(timeline); setRefCount((uint)m_registeredClips.size()); } void ProjectClip::deregisterTimelineClip(int clipId) { qDebug() << " ** * DEREGISTERING TIMELINE CLIP: " << clipId; Q_ASSERT(m_registeredClips.count(clipId) > 0); m_registeredClips.erase(clipId); if (m_videoProducers.count(clipId) > 0) { m_effectStack->removeService(m_videoProducers[clipId]); m_videoProducers.erase(clipId); } if (m_audioProducers.count(clipId) > 0) { m_effectStack->removeService(m_audioProducers[clipId]); m_audioProducers.erase(clipId); } setRefCount((uint)m_registeredClips.size()); } QList ProjectClip::timelineInstances() const { QList ids; - for (std::map>::const_iterator it = m_registeredClips.begin(); it != m_registeredClips.end(); ++it) { - ids.push_back(it->first); + for (const auto &m_registeredClip : m_registeredClips) { + ids.push_back(m_registeredClip.first); } return ids; } bool ProjectClip::selfSoftDelete(Fun &undo, Fun &redo) { auto toDelete = m_registeredClips; // we cannot use m_registeredClips directly, because it will be modified during loop for (const auto &clip : toDelete) { if (m_registeredClips.count(clip.first) == 0) { // clip already deleted, was probably grouped with another one continue; } if (auto timeline = clip.second.lock()) { timeline->requestItemDeletion(clip.first, undo, redo); } else { qDebug() << "Error while deleting clip: timeline unavailable"; Q_ASSERT(false); return false; } } return AbstractProjectItem::selfSoftDelete(undo, redo); } bool ProjectClip::isIncludedInTimeline() { return m_registeredClips.size() > 0; } void ProjectClip::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(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 b9c4e2a8a..a261e2e16 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, 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, 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(); + ~ProjectClip() override; 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(const std::shared_ptr &producer); std::shared_ptr softClone(const char *list); 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, 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 47d1f277e..bb92c79a7 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 #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, std::move(model))); baseFinishConstruct(self); return self; } 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(std::move(model))); baseFinishConstruct(self); return self; } -ProjectFolder::~ProjectFolder() {} +ProjectFolder::~ProjectFolder() = default; 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 fd5816094..5036fd609 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, const std::shared_ptr &model); explicit ProjectFolder(const std::shared_ptr &model); public: - ~ProjectFolder(); + ~ProjectFolder() override; /** * @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 1a8d38e65..f0fe8bd0c 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 #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(std::move(model))); baseFinishConstruct(self); return self; } -ProjectFolderUp::~ProjectFolderUp() {} +ProjectFolderUp::~ProjectFolderUp() = default; 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 e6ae5fc4c..314a615c5 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(const std::shared_ptr &model); public: - ~ProjectFolderUp(); + ~ProjectFolderUp() override; /** * @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 84601a961..4ce46557f 100644 --- a/src/bin/projectitemmodel.cpp +++ b/src/bin/projectitemmodel.cpp @@ -1,952 +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() {} +ProjectItemModel::~ProjectItemModel() = default; 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(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(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(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, 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(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(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(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, 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(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 c16061b94..5bfc6a710 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(); + ~ProjectItemModel() override; 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(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, 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(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(const std::shared_ptr &folder, const QString &newName); /* @brief Helper function to add a given item to the tree */ 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(const std::shared_ptr &item); public slots: /** @brief An item in the list was modified, notify */ 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 b04461fd8..6d70c1c13 100644 --- a/src/bin/projectsubclip.cpp +++ b/src/bin/projectsubclip.cpp @@ -1,175 +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, 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(); + return {}; } QPoint ProjectSubClip::zone() const { - return QPoint(m_inPoint, m_out); + return {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 59a0f2cc5..7b2762f4b 100644 --- a/src/bin/projectsubclip.h +++ b/src/bin/projectsubclip.h @@ -1,97 +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, 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(); + ~ProjectSubClip() override; 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/managecapturesdialog.cpp b/src/capture/managecapturesdialog.cpp index 6081631f4..86f3f1942 100644 --- a/src/capture/managecapturesdialog.cpp +++ b/src/capture/managecapturesdialog.cpp @@ -1,147 +1,147 @@ /*************************************************************************** * 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 "managecapturesdialog.h" #include "doc/kthumb.h" #include "kdenlive_debug.h" #include "klocalizedstring.h" #include #include #include #include #include #include #include ManageCapturesDialog::ManageCapturesDialog(const QList &files, QWidget *parent) : QDialog(parent) { setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)); m_view.setupUi(this); m_importButton = m_view.buttonBox->button(QDialogButtonBox::Ok); m_importButton->setText(i18n("import")); m_view.treeWidget->setIconSize(QSize(70, 50)); for (const QUrl &url : files) { QStringList text; text << url.fileName(); KFileItem file(url); file.setDelayedMimeTypes(true); text << KIO::convertSize(file.size()); auto *item = new QTreeWidgetItem(m_view.treeWidget, text); item->setData(0, Qt::UserRole, url.toLocalFile()); item->setToolTip(0, url.toLocalFile()); item->setFlags(Qt::ItemIsUserCheckable | Qt::ItemIsSelectable | Qt::ItemIsEnabled); item->setCheckState(0, Qt::Checked); } connect(m_view.treeWidget, &QTreeWidget::itemChanged, this, &ManageCapturesDialog::slotRefreshButtons); connect(m_view.deleteButton, &QAbstractButton::pressed, this, &ManageCapturesDialog::slotDeleteCurrent); connect(m_view.toggleButton, &QAbstractButton::pressed, this, &ManageCapturesDialog::slotToggle); QTreeWidgetItem *item = m_view.treeWidget->topLevelItem(0); if (item) { m_view.treeWidget->setCurrentItem(item); } connect(m_view.treeWidget, &QTreeWidget::itemSelectionChanged, this, &ManageCapturesDialog::slotCheckItemIcon); QTimer::singleShot(500, this, &ManageCapturesDialog::slotCheckItemIcon); m_view.treeWidget->resizeColumnToContents(0); m_view.treeWidget->setEnabled(false); adjustSize(); } -ManageCapturesDialog::~ManageCapturesDialog() {} +ManageCapturesDialog::~ManageCapturesDialog() = default; void ManageCapturesDialog::slotCheckItemIcon() { int ct = 0; const int count = m_view.treeWidget->topLevelItemCount(); while (ct < count) { QTreeWidgetItem *item = m_view.treeWidget->topLevelItem(ct); // QTreeWidgetItem *item = m_view.treeWidget->currentItem(); if (item->icon(0).isNull()) { QPixmap p = KThumb::getImage(QUrl(item->data(0, Qt::UserRole).toString()), 0, 70, 50); item->setIcon(0, QIcon(p)); m_view.treeWidget->resizeColumnToContents(0); repaint(); // QTimer::singleShot(400, this, SLOT(slotCheckItemIcon())); } ct++; } m_view.treeWidget->setEnabled(true); } void ManageCapturesDialog::slotRefreshButtons() { const int count = m_view.treeWidget->topLevelItemCount(); bool enabled = false; for (int i = 0; i < count; ++i) { QTreeWidgetItem *item = m_view.treeWidget->topLevelItem(i); if ((item != nullptr) && item->checkState(0) == Qt::Checked) { enabled = true; break; } } m_importButton->setEnabled(enabled); } void ManageCapturesDialog::slotDeleteCurrent() { QTreeWidgetItem *item = m_view.treeWidget->currentItem(); if (!item) { return; } const int i = m_view.treeWidget->indexOfTopLevelItem(item); m_view.treeWidget->takeTopLevelItem(i); // qCDebug(KDENLIVE_LOG) << "DELETING FILE: " << item->text(0); // KIO::NetAccess::del(QUrl(item->text(0)), this); if (!QFile::remove(item->data(0, Qt::UserRole).toString())) { qCDebug(KDENLIVE_LOG) << "// ERRor removing file " << item->data(0, Qt::UserRole).toString(); } delete item; item = nullptr; } void ManageCapturesDialog::slotToggle() { const int count = m_view.treeWidget->topLevelItemCount(); for (int i = 0; i < count; ++i) { QTreeWidgetItem *item = m_view.treeWidget->topLevelItem(i); if (item) { if (item->checkState(0) == Qt::Checked) { item->setCheckState(0, Qt::Unchecked); } else { item->setCheckState(0, Qt::Checked); } } } } QList ManageCapturesDialog::importFiles() const { QList result; const int count = m_view.treeWidget->topLevelItemCount(); for (int i = 0; i < count; ++i) { QTreeWidgetItem *item = m_view.treeWidget->topLevelItem(i); if ((item != nullptr) && item->checkState(0) == Qt::Checked) { result.append(QUrl(item->data(0, Qt::UserRole).toString())); } } return result; } diff --git a/src/capture/managecapturesdialog.h b/src/capture/managecapturesdialog.h index 4e07f54a7..f7ca599bb 100644 --- a/src/capture/managecapturesdialog.h +++ b/src/capture/managecapturesdialog.h @@ -1,49 +1,49 @@ /*************************************************************************** * 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 MANAGECAPTURESDIALOG_H #define MANAGECAPTURESDIALOG_H #include #include #include "ui_managecaptures_ui.h" class ManageCapturesDialog : public QDialog { Q_OBJECT public: explicit ManageCapturesDialog(const QList &files, QWidget *parent = nullptr); - ~ManageCapturesDialog(); + ~ManageCapturesDialog() override; QList importFiles() const; private slots: void slotRefreshButtons(); void slotDeleteCurrent(); void slotToggle(); void slotCheckItemIcon(); private: Ui::ManageCaptures_UI m_view; QPushButton *m_importButton; }; #endif diff --git a/src/capture/mltdevicecapture.cpp b/src/capture/mltdevicecapture.cpp index fd0b565c6..b83892705 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(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); + auto *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; + return false; } m_droppedFramesTimer.start(); - return 1; + return true; } 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 ed227d2fd..0140dccee 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(const QString &profile, /*VideoSurface *surface,*/ QWidget *parent = nullptr); /** @brief Destroy the MLT Renderer. */ - ~MltDeviceCapture(); + ~MltDeviceCapture() override; 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/capture/v4lcapture.cpp b/src/capture/v4lcapture.cpp index cf3e97fe2..5be14d999 100644 --- a/src/capture/v4lcapture.cpp +++ b/src/capture/v4lcapture.cpp @@ -1,133 +1,133 @@ /*************************************************************************** * Copyright (C) 2010 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 "v4lcapture.h" #include "kdenlivesettings.h" +#include +#include +#include #include #include -#include -#include -#include #include #include #include -V4lCaptureHandler::V4lCaptureHandler() {} +V4lCaptureHandler::V4lCaptureHandler() = default; // static QStringList V4lCaptureHandler::getDeviceName(const QString &input) { char *src = strdup(input.toUtf8().constData()); QString pixelformatdescription; int fd = open(src, O_RDWR | O_NONBLOCK); if (fd < 0) { free(src); return QStringList(); } struct v4l2_capability cap; char *devName = nullptr; int captureEnabled = 1; if (ioctl(fd, VIDIOC_QUERYCAP, &cap) < 0) { fprintf(stderr, "Cannot get capabilities."); // return nullptr; } else { devName = strdup((char *)cap.card); if ((cap.capabilities & V4L2_CAP_VIDEO_CAPTURE) == 0u) { // Device cannot capture captureEnabled = 0; } } if (captureEnabled != 0) { struct v4l2_format format; memset(&format, 0, sizeof(format)); format.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; struct v4l2_fmtdesc fmt; memset(&fmt, 0, sizeof(fmt)); fmt.index = 0; fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; struct v4l2_frmsizeenum sizes; memset(&sizes, 0, sizeof(sizes)); struct v4l2_frmivalenum rates; memset(&rates, 0, sizeof(rates)); char value[200]; while (ioctl(fd, VIDIOC_ENUM_FMT, &fmt) != -1) { if (pixelformatdescription.length() > 2000) { break; } if (snprintf(value, sizeof(value), ">%c%c%c%c", fmt.pixelformat >> 0, fmt.pixelformat >> 8, fmt.pixelformat >> 16, fmt.pixelformat >> 24) > 0) { pixelformatdescription.append(value); } fprintf(stderr, "detected format: %s: %c%c%c%c\n", fmt.description, fmt.pixelformat >> 0, fmt.pixelformat >> 8, fmt.pixelformat >> 16, fmt.pixelformat >> 24); sizes.pixel_format = fmt.pixelformat; sizes.index = 0; // Query supported frame size while (ioctl(fd, VIDIOC_ENUM_FRAMESIZES, &sizes) != -1) { struct v4l2_frmsize_discrete image_size = sizes.discrete; // Query supported frame rates rates.index = 0; rates.pixel_format = fmt.pixelformat; rates.width = image_size.width; rates.height = image_size.height; if (pixelformatdescription.length() > 2000) { break; } if (snprintf(value, sizeof(value), ":%dx%d=", image_size.width, image_size.height) > 0) { pixelformatdescription.append(value); } fprintf(stderr, "Size: %dx%d: ", image_size.width, image_size.height); while (ioctl(fd, VIDIOC_ENUM_FRAMEINTERVALS, &rates) != -1) { if (pixelformatdescription.length() > 2000) { break; } if (snprintf(value, sizeof(value), "%d/%d,", rates.discrete.denominator, rates.discrete.numerator) > 0) { pixelformatdescription.append(value); } fprintf(stderr, "%d/%d, ", rates.discrete.numerator, rates.discrete.denominator); rates.index++; } fprintf(stderr, "\n"); sizes.index++; } fmt.index++; } } close(fd); free(src); QStringList result; if (devName == nullptr) { return result; } QString deviceName(devName); free(devName); result << (deviceName.isEmpty() ? input : deviceName) << pixelformatdescription; return result; } diff --git a/src/colortools.cpp b/src/colortools.cpp index bbfb976f0..e43b33950 100644 --- a/src/colortools.cpp +++ b/src/colortools.cpp @@ -1,377 +1,377 @@ /*************************************************************************** * 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 "colortools.h" #include -#include +#include //#define DEBUG_CT #ifdef DEBUG_CT #include "kdenlive_debug.h" #endif namespace { double preventOverflow(double value) { return value < 0 ? 0 : value > 255 ? 255 : value; } } // namespace ColorTools::ColorTools(QObject *parent) : QObject(parent) { } QImage ColorTools::yuvColorWheel(const QSize &size, int Y, float scaling, bool modifiedVersion, bool circleOnly) { QImage wheel(size, QImage::Format_ARGB32); if (size.width() == 0 || size.height() == 0) { qCritical("ERROR: Size of the color wheel must not be 0!"); return wheel; } if (circleOnly) { wheel.fill(qRgba(0, 0, 0, 0)); } double dr, dg, db, dv; double ru, rv, rr; const int w = size.width(); const int h = size.height(); const float w2 = (float)w / 2; const float h2 = (float)h / 2; for (int u = 0; u < w; ++u) { // Transform u from {0,...,w} to [-1,1] double du = (double)2 * u / (w - 1) - 1; du = scaling * du; for (int v = 0; v < h; ++v) { dv = (double)2 * v / (h - 1) - 1; dv = scaling * dv; if (circleOnly) { // Ellipsis equation: x²/a² + y²/b² = 1 // Here: x=ru, y=rv, a=w/2, b=h/2, 1=rr // For rr > 1, the point lies outside. Don't draw it. ru = u - double(w2); rv = v - double(h2); rr = ru * ru / (w2 * w2) + rv * rv / (h2 * h2); if (rr > 1) { continue; } } // Calculate the RGB values from YUV dr = Y + 290.8 * dv; dg = Y - 100.6 * du - 148 * dv; db = Y + 517.2 * du; if (modifiedVersion) { // Scale the RGB values down, or up, to max 255 const double dmax = 255 / std::max({fabs(dr), fabs(dg), fabs(db)}); dr *= dmax; dg *= dmax; db *= dmax; } // Avoid overflows (which would generate intersecting patterns). // Note that not all possible (y,u,v) values with u,v \in [-1,1] // have a correct RGB representation, therefore some RGB values // may exceed {0,...,255}. dr = preventOverflow(dr); dg = preventOverflow(dg); db = preventOverflow(db); wheel.setPixel(u, (h - v - 1), qRgba(dr, dg, db, 255)); } } emit signalYuvWheelCalculationFinished(); return wheel; } QImage ColorTools::yuvVerticalPlane(const QSize &size, int angle, float scaling) { QImage plane(size, QImage::Format_ARGB32); if (size.width() == 0 || size.height() == 0) { qCritical("ERROR: Size of the color plane must not be 0!"); return plane; } double dr, dg, db, Y; const int w = size.width(); const int h = size.height(); const double uscaling = scaling * cos(M_PI * angle / 180); const double vscaling = scaling * sin(M_PI * angle / 180); for (int uv = 0; uv < w; ++uv) { double du = uscaling * ((double)2 * uv / w - 1); //(double)? double dv = vscaling * ((double)2 * uv / w - 1); for (int y = 0; y < h; ++y) { Y = (double)255 * y / h; // See yuv2rgb, yuvColorWheel dr = preventOverflow(Y + 290.8 * dv); dg = preventOverflow(Y - 100.6 * du - 148 * dv); db = preventOverflow(Y + 517.2 * du); plane.setPixel(uv, (h - y - 1), qRgba(dr, dg, db, 255)); } } return plane; } QImage ColorTools::rgbCurvePlane(const QSize &size, const ColorTools::ColorsRGB &color, float scaling, const QRgb &background) { Q_ASSERT(scaling > 0 && scaling <= 1); QImage plane(size, QImage::Format_ARGB32); if (size.width() == 0 || size.height() == 0) { qCritical("ERROR: Size of the color plane must not be 0!"); return plane; } const int w = size.width(); const int h = size.height(); double dcol; double dx, dy; for (int x = 0; x < w; ++x) { double dval = (double)255 * x / (w - 1); for (int y = 0; y < h; ++y) { dy = (double)y / (h - 1); dx = (double)x / (w - 1); if (1 - scaling < 0.0001) { dcol = (double)255 * dy; } else { dcol = (double)255 * (dy - (dy - dx) * (1 - scaling)); } if (color == ColorTools::ColorsRGB::R) { plane.setPixel(x, (h - y - 1), qRgb(dcol, dval, dval)); } else if (color == ColorTools::ColorsRGB::G) { plane.setPixel(x, (h - y - 1), qRgb(dval, dcol, dval)); } else if (color == ColorTools::ColorsRGB::B) { plane.setPixel(x, (h - y - 1), qRgb(dval, dval, dcol)); } else if (color == ColorTools::ColorsRGB::A) { plane.setPixel(x, (h - y - 1), qRgb(dcol / 255. * qRed(background), dcol / 255. * qGreen(background), dcol / 255. * qBlue(background))); } else { plane.setPixel(x, (h - y - 1), qRgb(dcol, dcol, dcol)); } } } return plane; } QImage ColorTools::rgbCurveLine(const QSize &size, const ColorTools::ColorsRGB &color, const QRgb &background) { QImage plane(size, QImage::Format_ARGB32); if (size.width() == 0 || size.height() == 0) { qCritical("ERROR: Size of the color line must not be 0!"); return plane; } const int w = size.width(); const int h = size.height(); double dcol; double dy; for (int x = 0; x < w; ++x) { for (int y = 0; y < h; ++y) { dy = (double)y / (h - 1); dcol = (double)255 * dy; if (color == ColorTools::ColorsRGB::R) { plane.setPixel(x, (h - y - 1), qRgb(dcol, 0, 0)); } else if (color == ColorTools::ColorsRGB::G) { plane.setPixel(x, (h - y - 1), qRgb(0, dcol, 0)); } else if (color == ColorTools::ColorsRGB::B) { plane.setPixel(x, (h - y - 1), qRgb(0, 0, dcol)); } else if (color == ColorTools::ColorsRGB::A) { plane.setPixel(x, (h - y - 1), qRgb(dcol / 255. * qRed(background), dcol / 255. * qGreen(background), dcol / 255. * qBlue(background))); } else { plane.setPixel(x, (h - y - 1), qRgb(dcol, dcol, dcol)); } } } return plane; } QImage ColorTools::yPbPrColorWheel(const QSize &size, int Y, float scaling, bool circleOnly) { QImage wheel(size, QImage::Format_ARGB32); if (size.width() == 0 || size.height() == 0) { qCritical("ERROR: Size of the color wheel must not be 0!"); return wheel; } if (circleOnly) { wheel.fill(qRgba(0, 0, 0, 0)); } double dr, dg, db, dpR; double rB, rR, rr; const int w = size.width(); const int h = size.height(); const float w2 = (float)w / 2; const float h2 = (float)h / 2; for (int b = 0; b < w; ++b) { // Transform pB from {0,...,w} to [-0.5,0.5] double dpB = (double)b / (w - 1) - .5; dpB = scaling * dpB; for (int r = 0; r < h; ++r) { dpR = (double)r / (h - 1) - .5; dpR = scaling * dpR; if (circleOnly) { // see yuvColorWheel rB = b - double(w2); rR = r - double(h2); rr = rB * rB / (w2 * w2) + rR * rR / (h2 * h2); if (rr > 1) { continue; } } // Calculate the RGB values from YPbPr dr = preventOverflow(Y + 357.5 * dpR); dg = preventOverflow(Y - 87.75 * dpB - 182.1 * dpR); db = preventOverflow(Y + 451.86 * dpB); wheel.setPixel(b, (h - r - 1), qRgba(dr, dg, db, 255)); } } return wheel; } QImage ColorTools::hsvHueShiftPlane(const QSize &size, int S, int V, int MIN, int MAX) { Q_ASSERT(size.width() > 0); Q_ASSERT(size.height() > 0); Q_ASSERT(MAX > MIN); QImage plane(size, QImage::Format_ARGB32); #ifdef DEBUG_CT qCDebug(KDENLIVE_LOG) << "Requested: Saturation " << S << ", Value " << V; QColor colTest(-1, 256, 257); qCDebug(KDENLIVE_LOG) << "-1 mapped to " << colTest.red() << ", 256 to " << colTest.green() << ", 257 to " << colTest.blue(); #endif QColor col(0, 0, 0); const int hueValues = MAX - MIN; float huediff; int newhue; for (int x = 0; x < size.width(); ++x) { float hue = x / (size.width() - 1.0) * 359; for (int y = 0; y < size.height(); ++y) { huediff = (1.0f - y / (size.height() - 1.0)) * hueValues + MIN; // qCDebug(KDENLIVE_LOG) << "hue: " << hue << ", huediff: " << huediff; newhue = hue + huediff + 360; // Avoid negative numbers. Rest (>360) will be mapped correctly. col.setHsv(newhue, S, V); plane.setPixel(x, y, col.rgba()); } } return plane; } QImage ColorTools::hsvCurvePlane(const QSize &size, const QColor &baseColor, const ComponentsHSV &xVariant, const ComponentsHSV &yVariant, bool shear, const float offsetY) { Q_ASSERT(size.width() > 0); Q_ASSERT(size.height() > 0); /*int xMax, yMax; switch(xVariant) { case COM_H: xMax = 360; break; case COM_S: case COM_V: xMax = 256; break; } switch (yVariant) { case COM_H: yMax = 360; break; case COM_S: case COM_V: yMax = 256; break; }*/ QImage plane(size, QImage::Format_ARGB32); QColor col(0, 0, 0); float hue, sat, val; hue = baseColor.hueF(); sat = baseColor.saturationF(); val = baseColor.valueF(); for (int x = 0; x < size.width(); ++x) { switch (xVariant) { case COM_H: hue = x / (size.width() - 1.0); break; case COM_S: sat = x / (size.width() - 1.0); break; case COM_V: val = x / (size.width() - 1.0); break; } for (int y = 0; y < size.height(); ++y) { switch (yVariant) { case COM_H: hue = 1.0 - y / (size.height() - 1.0); break; case COM_S: sat = 1.0 - y / (size.height() - 1.0); break; case COM_V: val = 1.0 - y / (size.height() - 1.0); break; } col.setHsvF(hue, sat, val); if (!shear) { plane.setPixel(x, y, col.rgba()); } else { plane.setPixel(x, (2 * size.height() + y - x * size.width() / size.height() - int(offsetY) * size.height()) % size.height(), col.rgba()); } } } return plane; } diff --git a/src/core.cpp b/src/core.cpp index 2621dfdf5..2ee1c90fc 100644 --- a/src/core.cpp +++ b/src/core.cpp @@ -1,706 +1,701 @@ /* 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 "core.h" #include "bin/bin.h" #include "bin/projectitemmodel.h" #include "doc/docundostack.hpp" #include "doc/kdenlivedoc.h" #include "jobs/jobmanager.h" #include "kdenlive_debug.h" #include "kdenlivesettings.h" #include "library/librarywidget.h" #include "mainwindow.h" #include "mltconnection.h" #include "mltcontroller/clipcontroller.h" #include "monitor/monitormanager.h" #include "profiles/profilemodel.hpp" #include "profiles/profilerepository.hpp" #include "project/projectmanager.h" #include "timeline2/model/timelineitemmodel.hpp" #include "timeline2/view/timelinecontroller.h" #include "timeline2/view/timelinewidget.h" #include #include #include #include #include #include #ifdef Q_OS_MAC #include #endif std::unique_ptr Core::m_self; Core::Core() - : m_mainWindow(nullptr) - , m_projectManager(nullptr) - , m_monitorManager(nullptr) - , m_binWidget(nullptr) - , m_library(nullptr) - , m_thumbProfile(nullptr) + : m_thumbProfile(nullptr) { } void Core::prepareShutdown() { m_guiConstructed = false; } Core::~Core() { if (m_monitorManager) { delete m_monitorManager; } // delete m_binWidget; if (m_projectManager) { delete m_projectManager; } ClipController::mediaUnavailable.reset(); } void Core::build(const QString &MltPath) { if (m_self) { return; } m_self.reset(new Core()); m_self->initLocale(); qRegisterMetaType("audioShortVector"); qRegisterMetaType>("QVector"); qRegisterMetaType("MessageType"); qRegisterMetaType("stringMap"); qRegisterMetaType("audioByteArray"); qRegisterMetaType>("QList"); qRegisterMetaType>("std::shared_ptr"); qRegisterMetaType>(); qRegisterMetaType("QDomElement"); qRegisterMetaType("requestClipInfo"); // Open connection with Mlt MltConnection::construct(MltPath); // load the profile from disk ProfileRepository::get()->refresh(); // load default profile m_self->m_profile = KdenliveSettings::default_profile(); if (m_self->m_profile.isEmpty()) { m_self->m_profile = ProjectManager::getDefaultProjectFormat(); KdenliveSettings::setDefault_profile(m_self->m_profile); } // Init producer shown for unavailable media // TODO make it a more proper image, it currently causes a crash on exit ClipController::mediaUnavailable = std::make_shared(ProfileRepository::get()->getProfile(m_self->m_profile)->profile(), "color:blue"); ClipController::mediaUnavailable->set("length", 99999999); m_self->m_projectItemModel = ProjectItemModel::construct(); // Job manager must be created before bin to correctly connect m_self->m_jobManager.reset(new JobManager(m_self.get())); } void Core::initGUI(const QUrl &Url) { m_guiConstructed = true; m_profile = KdenliveSettings::default_profile(); m_currentProfile = m_profile; profileChanged(); m_mainWindow = new MainWindow(); // load default profile and ask user to select one if not found. if (m_profile.isEmpty()) { m_profile = ProjectManager::getDefaultProjectFormat(); profileChanged(); KdenliveSettings::setDefault_profile(m_profile); } if (!ProfileRepository::get()->profileExists(m_profile)) { KMessageBox::sorry(m_mainWindow, i18n("The default profile of Kdenlive is not set or invalid, press OK to set it to a correct value.")); // TODO this simple widget should be improved and probably use profileWidget // we get the list of profiles QVector> all_profiles = ProfileRepository::get()->getAllProfiles(); QStringList all_descriptions; for (const auto &profile : all_profiles) { all_descriptions << profile.first; } // ask the user bool ok; QString item = QInputDialog::getItem(m_mainWindow, i18n("Select Default Profile"), i18n("Profile:"), all_descriptions, 0, false, &ok); if (ok) { ok = false; for (const auto &profile : all_profiles) { if (profile.first == item) { m_profile = profile.second; ok = true; } } } if (!ok) { KMessageBox::error( m_mainWindow, i18n("The given profile is invalid. We default to the profile \"dv_pal\", but you can change this from Kdenlive's settings panel")); m_profile = QStringLiteral("dv_pal"); } KdenliveSettings::setDefault_profile(m_profile); profileChanged(); } m_projectManager = new ProjectManager(this); m_binWidget = new Bin(m_projectItemModel, m_mainWindow); m_library = new LibraryWidget(m_projectManager, m_mainWindow); connect(m_library, SIGNAL(addProjectClips(QList)), m_binWidget, SLOT(droppedUrls(QList))); connect(this, &Core::updateLibraryPath, m_library, &LibraryWidget::slotUpdateLibraryPath); m_monitorManager = new MonitorManager(this); // Producer queue, creating MLT::Producers on request /* m_producerQueue = new ProducerQueue(m_binController); connect(m_producerQueue, &ProducerQueue::gotFileProperties, m_binWidget, &Bin::slotProducerReady); connect(m_producerQueue, &ProducerQueue::replyGetImage, m_binWidget, &Bin::slotThumbnailReady); connect(m_producerQueue, &ProducerQueue::requestProxy, [this](const QString &id){ m_binWidget->startJob(id, AbstractClipJob::PROXYJOB);}); connect(m_producerQueue, &ProducerQueue::removeInvalidClip, m_binWidget, &Bin::slotRemoveInvalidClip, Qt::DirectConnection); connect(m_producerQueue, SIGNAL(addClip(QString, QMap)), m_binWidget, SLOT(slotAddUrl(QString, QMap))); connect(m_binController.get(), SIGNAL(createThumb(QDomElement, QString, int)), m_producerQueue, SLOT(getFileProperties(QDomElement, QString, int))); connect(m_binWidget, &Bin::producerReady, m_producerQueue, &ProducerQueue::slotProcessingDone, Qt::DirectConnection); // TODO connect(m_producerQueue, SIGNAL(removeInvalidProxy(QString,bool)), m_binWidget, SLOT(slotRemoveInvalidProxy(QString,bool)));*/ m_mainWindow->init(); projectManager()->init(Url, QString()); if (qApp->isSessionRestored()) { // NOTE: we are restoring only one window, because Kdenlive only uses one MainWindow m_mainWindow->restore(1, false); } QMetaObject::invokeMethod(pCore->projectManager(), "slotLoadOnOpen", Qt::QueuedConnection); m_mainWindow->show(); } std::unique_ptr &Core::self() { if (!m_self) { qDebug() << "Error : Core has not been created"; } return m_self; } MainWindow *Core::window() { return m_mainWindow; } ProjectManager *Core::projectManager() { return m_projectManager; } MonitorManager *Core::monitorManager() { return m_monitorManager; } Monitor *Core::getMonitor(int id) { if (id == Kdenlive::ClipMonitor) { return m_monitorManager->clipMonitor(); } return m_monitorManager->projectMonitor(); } Bin *Core::bin() { return m_binWidget; } void Core::selectBinClip(const QString &clipId, int frame, const QPoint &zone) { m_binWidget->selectClipById(clipId, frame, zone); } std::shared_ptr Core::jobManager() { return m_jobManager; } LibraryWidget *Core::library() { return m_library; } void Core::initLocale() { QLocale systemLocale = QLocale(); #ifndef Q_OS_MAC setlocale(LC_NUMERIC, nullptr); #else setlocale(LC_NUMERIC_MASK, nullptr); #endif // localeconv()->decimal_point does not give reliable results on Windows #ifndef Q_OS_WIN char *separator = localeconv()->decimal_point; if (QString::fromUtf8(separator) != QChar(systemLocale.decimalPoint())) { // qCDebug(KDENLIVE_LOG)<<"------\n!!! system locale is not similar to Qt's locale... be prepared for bugs!!!\n------"; // HACK: There is a locale conflict, so set locale to C // Make sure to override exported values or it won't work qputenv("LANG", "C"); #ifndef Q_OS_MAC setlocale(LC_NUMERIC, "C"); #else setlocale(LC_NUMERIC_MASK, "C"); #endif systemLocale = QLocale::c(); } #endif systemLocale.setNumberOptions(QLocale::OmitGroupSeparator); QLocale::setDefault(systemLocale); } std::unique_ptr &Core::getMltRepository() { return MltConnection::self()->getMltRepository(); } std::unique_ptr &Core::getCurrentProfile() const { return ProfileRepository::get()->getProfile(m_currentProfile); } const QString &Core::getCurrentProfilePath() const { return m_currentProfile; } bool Core::setCurrentProfile(const QString &profilePath) { if (m_currentProfile == profilePath) { // no change required return true; } if (ProfileRepository::get()->profileExists(profilePath)) { m_currentProfile = profilePath; m_thumbProfile.reset(); // inform render widget m_mainWindow->updateRenderWidgetProfile(); if (m_guiConstructed && m_mainWindow->getCurrentTimeline()->controller()->getModel()) { m_mainWindow->getCurrentTimeline()->controller()->getModel()->updateProfile(&getCurrentProfile()->profile()); checkProfileValidity(); } return true; } return false; } void Core::checkProfileValidity() { int offset = (getCurrentProfile()->profile().width() % 8) + (getCurrentProfile()->profile().height() % 2); if (offset > 0) { // Profile is broken, warn user if (m_binWidget) { m_binWidget->displayBinMessage(i18n("Your project profile is invalid, rendering might fail."), KMessageWidget::Warning); } } } double Core::getCurrentSar() const { return getCurrentProfile()->sar(); } double Core::getCurrentDar() const { return getCurrentProfile()->dar(); } double Core::getCurrentFps() const { return getCurrentProfile()->fps(); } QSize Core::getCurrentFrameDisplaySize() const { - return QSize((int)(getCurrentProfile()->height() * getCurrentDar() + 0.5), getCurrentProfile()->height()); + return {(int)(getCurrentProfile()->height() * getCurrentDar() + 0.5), getCurrentProfile()->height()}; } QSize Core::getCurrentFrameSize() const { - return QSize(getCurrentProfile()->width(), getCurrentProfile()->height()); + return {getCurrentProfile()->width(), getCurrentProfile()->height()}; } void Core::requestMonitorRefresh() { if (!m_guiConstructed) return; m_monitorManager->refreshProjectMonitor(); } void Core::refreshProjectRange(QSize range) { if (!m_guiConstructed) return; m_monitorManager->refreshProjectRange(range); } int Core::getItemPosition(const ObjectId &id) { if (!m_guiConstructed) return 0; switch (id.first) { case ObjectType::TimelineClip: if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isClip(id.second)) { return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getClipPosition(id.second); } break; case ObjectType::TimelineComposition: if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isComposition(id.second)) { return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getCompositionPosition(id.second); } break; case ObjectType::BinClip: return 0; break; default: qDebug() << "ERROR: unhandled object type"; } return 0; } int Core::getItemIn(const ObjectId &id) { if (!m_guiConstructed) { qDebug() << "/ / // QUERYING ITEM IN BUT GUI NOT BUILD!!"; return 0; } switch (id.first) { case ObjectType::TimelineClip: if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isClip(id.second)) { return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getClipIn(id.second); } break; case ObjectType::TimelineComposition: return 0; break; case ObjectType::BinClip: return 0; break; default: qDebug() << "ERROR: unhandled object type"; } return 0; } PlaylistState::ClipState Core::getItemState(const ObjectId &id) { if (!m_guiConstructed) return PlaylistState::Disabled; switch (id.first) { case ObjectType::TimelineClip: if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isClip(id.second)) { return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getClipState(id.second); } break; case ObjectType::TimelineComposition: return PlaylistState::VideoOnly; break; case ObjectType::BinClip: return m_binWidget->getClipState(id.second); break; case ObjectType::TimelineTrack: return m_mainWindow->getCurrentTimeline()->controller()->getModel()->isAudioTrack(id.second) ? PlaylistState::AudioOnly : PlaylistState::VideoOnly; default: qDebug() << "ERROR: unhandled object type"; break; } return PlaylistState::Disabled; } int Core::getItemDuration(const ObjectId &id) { if (!m_guiConstructed) return 0; switch (id.first) { case ObjectType::TimelineClip: if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isClip(id.second)) { return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getClipPlaytime(id.second); } break; case ObjectType::TimelineComposition: if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isComposition(id.second)) { return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getCompositionPlaytime(id.second); } break; case ObjectType::BinClip: return (int)m_binWidget->getClipDuration(id.second); break; default: qDebug() << "ERROR: unhandled object type"; } return 0; } int Core::getItemTrack(const ObjectId &id) { if (!m_guiConstructed) return 0; switch (id.first) { case ObjectType::TimelineClip: case ObjectType::TimelineComposition: return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getItemTrackId(id.second); break; default: qDebug() << "ERROR: unhandled object type"; } return 0; } void Core::refreshProjectItem(const ObjectId &id) { if (!m_guiConstructed || m_mainWindow->getCurrentTimeline()->loading) return; switch (id.first) { case ObjectType::TimelineClip: if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isClip(id.second)) { m_mainWindow->getCurrentTimeline()->controller()->refreshItem(id.second); } break; case ObjectType::TimelineComposition: if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isComposition(id.second)) { m_mainWindow->getCurrentTimeline()->controller()->refreshItem(id.second); } break; case ObjectType::TimelineTrack: if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isTrack(id.second)) { requestMonitorRefresh(); } break; case ObjectType::BinClip: m_monitorManager->refreshClipMonitor(); break; default: qDebug() << "ERROR: unhandled object type"; } } bool Core::hasTimelinePreview() const { if (!m_guiConstructed) { return false; } return m_mainWindow->getCurrentTimeline()->controller()->renderedChunks().size() > 0; } KdenliveDoc *Core::currentDoc() { return m_projectManager->current(); } int Core::projectDuration() const { if (!m_guiConstructed) { return 0; } return m_mainWindow->getCurrentTimeline()->controller()->duration(); } void Core::profileChanged() { GenTime::setFps(getCurrentFps()); } void Core::pushUndo(const Fun &undo, const Fun &redo, const QString &text) { undoStack()->push(new FunctionalUndoCommand(undo, redo, text)); } void Core::pushUndo(QUndoCommand *command) { undoStack()->push(command); } void Core::displayMessage(const QString &message, MessageType type, int timeout) { if (m_mainWindow) { m_mainWindow->displayMessage(message, type, timeout); } else { qDebug() << message; } } void Core::displayBinMessage(const QString &text, int type, const QList &actions) { m_binWidget->doDisplayMessage(text, (KMessageWidget::MessageType)type, actions); } void Core::displayBinLogMessage(const QString &text, int type, const QString &logInfo) { m_binWidget->doDisplayMessage(text, (KMessageWidget::MessageType)type, logInfo); } void Core::clearAssetPanel(int itemId) { if (m_guiConstructed) m_mainWindow->clearAssetPanel(itemId); } std::shared_ptr Core::getItemEffectStack(int itemType, int itemId) { if (!m_guiConstructed) return nullptr; switch (itemType) { case (int)ObjectType::TimelineClip: return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getClipEffectStack(itemId); case (int)ObjectType::TimelineTrack: // TODO return nullptr; break; case (int)ObjectType::BinClip: return m_binWidget->getClipEffectStack(itemId); default: return nullptr; } } std::shared_ptr Core::undoStack() { return projectManager()->undoStack(); } QMap Core::getVideoTrackNames() { if (!m_guiConstructed) return QMap(); return m_mainWindow->getCurrentTimeline()->controller()->getTrackNames(true); } QPair Core::getCompositionATrack(int cid) const { - if (!m_guiConstructed) return QPair(); + if (!m_guiConstructed) return {}; return m_mainWindow->getCurrentTimeline()->controller()->getCompositionATrack(cid); } bool Core::compositionAutoTrack(int cid) const { return m_mainWindow->getCurrentTimeline()->controller()->compositionAutoTrack(cid); } void Core::setCompositionATrack(int cid, int aTrack) { if (!m_guiConstructed) return; m_mainWindow->getCurrentTimeline()->controller()->setCompositionATrack(cid, aTrack); } std::shared_ptr Core::projectItemModel() { return m_projectItemModel; } void Core::invalidateRange(QSize range) { if (!m_mainWindow || m_mainWindow->getCurrentTimeline()->loading) return; m_mainWindow->getCurrentTimeline()->controller()->invalidateZone(range.width(), range.height()); } void Core::invalidateItem(ObjectId itemId) { if (!m_mainWindow || m_mainWindow->getCurrentTimeline()->loading) return; switch (itemId.first) { case ObjectType::TimelineClip: case ObjectType::TimelineComposition: m_mainWindow->getCurrentTimeline()->controller()->invalidateItem(itemId.second); break; case ObjectType::TimelineTrack: // TODO: invalidate all clips in track break; default: // bin clip should automatically be reloaded, compositions should not have effects break; } } double Core::getClipSpeed(int id) const { return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getClipSpeed(id); } void Core::updateItemKeyframes(ObjectId id) { if (id.first == ObjectType::TimelineClip && m_mainWindow) { m_mainWindow->getCurrentTimeline()->controller()->updateClip(id.second, {TimelineModel::KeyframesRole}); } } void Core::updateItemModel(ObjectId id, const QString &service) { if (m_mainWindow && !m_mainWindow->getCurrentTimeline()->loading && service.startsWith(QLatin1String("fade")) && id.first == ObjectType::TimelineClip) { bool startFade = service == QLatin1String("fadein") || service == QLatin1String("fade_from_black"); m_mainWindow->getCurrentTimeline()->controller()->updateClip(id.second, {startFade ? TimelineModel::FadeInRole : TimelineModel::FadeOutRole}); } } void Core::showClipKeyframes(ObjectId id, bool enable) { if (id.first == ObjectType::TimelineClip) { m_mainWindow->getCurrentTimeline()->controller()->showClipKeyframes(id.second, enable); } else if (id.first == ObjectType::TimelineComposition) { m_mainWindow->getCurrentTimeline()->controller()->showCompositionKeyframes(id.second, enable); } } Mlt::Profile *Core::thumbProfile() { if (!m_thumbProfile) { - m_thumbProfile = std::unique_ptr(new Mlt::Profile(m_currentProfile.toStdString().c_str())); + m_thumbProfile = std::make_unique(m_currentProfile.toStdString().c_str()); m_thumbProfile->set_height(200); int width = 200 * m_thumbProfile->dar(); if (width % 8 > 0) { width += 8 - width % 8; } m_thumbProfile->set_width(width); } return m_thumbProfile.get(); } void Core::clearSelection() { if (m_mainWindow && m_guiConstructed) { m_mainWindow->getCurrentTimeline()->controller()->clearSelection(); } } void Core::selectItem(int itemId) { if (m_mainWindow && m_guiConstructed) { m_mainWindow->getCurrentTimeline()->controller()->addSelection(itemId, true); } } bool Core::isSelected(int itemId) const { if (m_mainWindow && m_guiConstructed) { return m_mainWindow->getCurrentTimeline()->controller()->selection().contains(itemId); } return false; } void Core::removeFromSelection(int itemId) { if (m_mainWindow && m_guiConstructed) { m_mainWindow->getCurrentTimeline()->controller()->removeSelection(itemId); } } void Core::triggerAction(const QString &name) { QAction *action = m_mainWindow->actionCollection()->action(name); if (action) { action->trigger(); } } void Core::clean() { m_self.reset(); } diff --git a/src/core.h b/src/core.h index f7c64c7e6..50175b8f3 100644 --- a/src/core.h +++ b/src/core.h @@ -1,219 +1,219 @@ /* 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 CORE_H #define CORE_H #include "definitions.h" #include "kdenlivecore_export.h" #include "undohelper.hpp" #include #include #include #include class Bin; class DocUndoStack; class EffectStackModel; class JobManager; class KdenliveDoc; class LibraryWidget; class MainWindow; class Monitor; class MonitorManager; class ProfileModel; class ProjectItemModel; class ProjectManager; namespace Mlt { class Repository; class Profile; } // namespace Mlt #define EXIT_RESTART (42) #define pCore Core::self() /** * @class Core * @brief Singleton that provides access to the different parts of Kdenlive * * Needs to be initialize before any widgets are created in MainWindow. * Plugins should be loaded after the widget setup. */ class /*KDENLIVECORE_EXPORT*/ Core : public QObject { Q_OBJECT public: Core(const Core &) = delete; Core &operator=(const Core &) = delete; Core(Core &&) = delete; Core &operator=(Core &&) = delete; - virtual ~Core(); + ~Core() override; /** * @brief Setup the basics of the application, in particular the connection * with Mlt * @param MltPath (optional) path to MLT environment */ static void build(const QString &MltPath = QString()); /** * @brief Init the GUI part of the app and show the main window * @param Url (optional) file to open * 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 initGUI(const QUrl &Url); /** @brief Returns a pointer to the singleton object. */ static std::unique_ptr &self(); /** @brief Delete the global core instance */ static void clean(); /** @brief Returns a pointer to the main window. */ MainWindow *window(); /** @brief Returns a pointer to the project manager. */ ProjectManager *projectManager(); /** @brief Returns a pointer to the current project. */ KdenliveDoc *currentDoc(); /** @brief Returns a pointer to the monitor manager. */ MonitorManager *monitorManager(); /** @brief Returns a pointer to the view of the project bin. */ Bin *bin(); /** @brief Select a clip in the Bin from its id. */ void selectBinClip(const QString &id, int frame = -1, const QPoint &zone = QPoint()); /** @brief Returns a pointer to the model of the project bin. */ std::shared_ptr projectItemModel(); /** @brief Returns a pointer to the job manager. Please do not store it. */ std::shared_ptr jobManager(); /** @brief Returns a pointer to the library. */ LibraryWidget *library(); /** @brief Returns a pointer to MLT's repository */ std::unique_ptr &getMltRepository(); /** @brief Returns a pointer to the current profile */ std::unique_ptr &getCurrentProfile() const; const QString &getCurrentProfilePath() const; /** @brief Define the active profile * @returns true if profile exists, false if not found */ bool setCurrentProfile(const QString &profilePath); /** @brief Returns Sample Aspect Ratio of current profile */ double getCurrentSar() const; /** @brief Returns Display Aspect Ratio of current profile */ double getCurrentDar() const; /** @brief Returns frame rate of current profile */ double getCurrentFps() const; /** @brief Returns the frame size (width x height) of current profile */ QSize getCurrentFrameSize() const; /** @brief Returns the frame display size (width x height) of current profile */ QSize getCurrentFrameDisplaySize() const; /** @brief Request project monitor refresh */ void requestMonitorRefresh(); /** @brief Request project monitor refresh if current position is inside range*/ void refreshProjectRange(QSize range); /** @brief Request project monitor refresh if referenced item is under cursor */ void refreshProjectItem(const ObjectId &id); /** @brief Returns a reference to a monitor (clip or project monitor) */ Monitor *getMonitor(int id); /** @brief This function must be called whenever the profile used changes */ void profileChanged(); /** @brief Create and push and undo object based on the corresponding functions Note that if you class permits and requires it, you should use the macro PUSH_UNDO instead*/ void pushUndo(const Fun &undo, const Fun &redo, const QString &text); void pushUndo(QUndoCommand *command); /** @brief display a user info/warning message in statusbar */ void displayMessage(const QString &message, MessageType type, int timeout = -1); /** @brief Clear asset view if itemId is displayed. */ void clearAssetPanel(int itemId); /** @brief Returns the effectstack of a given bin clip. */ std::shared_ptr getItemEffectStack(int itemType, int itemId); int getItemPosition(const ObjectId &id); int getItemIn(const ObjectId &id); int getItemTrack(const ObjectId &id); int getItemDuration(const ObjectId &id); /** @brief Returns the capabilities of a clip: AudioOnly, VideoOnly or Disabled if both are allowed */ PlaylistState::ClipState getItemState(const ObjectId &id); /** @brief Get a list of video track names with indexes */ QMap getVideoTrackNames(); /** @brief Returns the composition A track (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; std::shared_ptr undoStack(); double getClipSpeed(int id) const; /** @brief Mark an item as invalid for timeline preview */ void invalidateItem(ObjectId itemId); void invalidateRange(QSize range); void prepareShutdown(); /** the keyframe model changed (effect added, deleted, active effect changed), inform timeline */ void updateItemKeyframes(ObjectId id); /** A fade for clip id changed, update timeline */ void updateItemModel(ObjectId id, const QString &service); /** Show / hide keyframes for a timeline clip */ void showClipKeyframes(ObjectId id, bool enable); Mlt::Profile *thumbProfile(); void clearSelection(); void selectItem(int itemId); bool isSelected(int itemId) const; void removeFromSelection(int itemId); /** @brief Returns the current project duration */ int projectDuration() const; /** @brief Returns true if current project has some rendered timeline preview */ bool hasTimelinePreview() const; private: explicit Core(); static std::unique_ptr m_self; /** @brief Makes sure Qt's locale and system locale settings match. */ void initLocale(); - MainWindow *m_mainWindow; - ProjectManager *m_projectManager; - MonitorManager *m_monitorManager; + MainWindow *m_mainWindow{nullptr}; + ProjectManager *m_projectManager{nullptr}; + MonitorManager *m_monitorManager{nullptr}; std::shared_ptr m_projectItemModel; std::shared_ptr m_jobManager; - Bin *m_binWidget; - LibraryWidget *m_library; + Bin *m_binWidget{nullptr}; + LibraryWidget *m_library{nullptr}; /** @brief Current project's profile path */ QString m_currentProfile; QString m_profile; std::unique_ptr m_thumbProfile; bool m_guiConstructed = false; /** @brief Check that the profile is valid (width is a multiple of 8 and height a multiple of 2 */ void checkProfileValidity(); public slots: void triggerAction(const QString &name); /** @brief display a user info/warning message in the project bin */ void displayBinMessage(const QString &text, int type, const QList &actions = QList()); void displayBinLogMessage(const QString &text, int type, const QString &logInfo); signals: void coreIsReady(); void updateLibraryPath(); }; #endif diff --git a/src/definitions.cpp b/src/definitions.cpp index c248e8076..f46f52edb 100644 --- a/src/definitions.cpp +++ b/src/definitions.cpp @@ -1,199 +1,200 @@ /*************************************************************************** * 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 "definitions.h" #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; // clang-format off registration::enumeration("GroupType")( value("Normal", GroupType::Normal), value("Selection", GroupType::Selection), value("AVSplit", GroupType::AVSplit), value("Leaf", GroupType::Leaf) ); // clang-format on } QDebug operator<<(QDebug qd, const ItemInfo &info) { qd << "ItemInfo " << &info; qd << "\tTrack" << info.track; qd << "\tStart pos: " << info.startPos.toString(); qd << "\tEnd pos: " << info.endPos.toString(); qd << "\tCrop start: " << info.cropStart.toString(); qd << "\tCrop duration: " << info.cropDuration.toString(); return qd.maybeSpace(); } CommentedTime::CommentedTime() : m_time(GenTime(0)) - , m_type(0) + { } -CommentedTime::CommentedTime(const GenTime &time, const QString &comment, int markerType) +CommentedTime::CommentedTime(const GenTime &time, QString comment, int markerType) : m_time(time) - , m_comment(comment) + , m_comment(std::move(comment)) , m_type(markerType) { } CommentedTime::CommentedTime(const QString &hash, const GenTime &time) : m_time(time) , m_comment(hash.section(QLatin1Char(':'), 1)) , m_type(hash.section(QLatin1Char(':'), 0, 0).toInt()) { } QString CommentedTime::comment() const { return (m_comment.isEmpty() ? i18n("Marker") : m_comment); } GenTime CommentedTime::time() const { return m_time; } void CommentedTime::setComment(const QString &comm) { m_comment = comm; } void CommentedTime::setMarkerType(int newtype) { m_type = newtype; } QString CommentedTime::hash() const { return QString::number(m_type) + QLatin1Char(':') + (m_comment.isEmpty() ? i18n("Marker") : m_comment); } int CommentedTime::markerType() const { return m_type; } QColor CommentedTime::markerColor(int type) { switch (type) { case 0: return Qt::red; break; case 1: return Qt::blue; break; case 2: return Qt::green; break; case 3: return Qt::yellow; break; default: return Qt::cyan; break; } } bool CommentedTime::operator>(const CommentedTime &op) const { return m_time > op.time(); } bool CommentedTime::operator<(const CommentedTime &op) const { return m_time < op.time(); } bool CommentedTime::operator>=(const CommentedTime &op) const { return m_time >= op.time(); } bool CommentedTime::operator<=(const CommentedTime &op) const { return m_time <= op.time(); } bool CommentedTime::operator==(const CommentedTime &op) const { return m_time == op.time(); } bool CommentedTime::operator!=(const CommentedTime &op) const { return m_time != op.time(); } const QString groupTypeToStr(GroupType t) { switch (t) { case GroupType::Normal: return QStringLiteral("Normal"); case GroupType::Selection: return QStringLiteral("Selection"); case GroupType::AVSplit: return QStringLiteral("AVSplit"); case GroupType::Leaf: return QStringLiteral("Leaf"); } Q_ASSERT(false); return QString(); } GroupType groupTypeFromStr(const QString &s) { std::vector types{GroupType::Selection, GroupType::Normal, GroupType::AVSplit, GroupType::Leaf}; for (const auto &t : types) { if (s == groupTypeToStr(t)) { return t; } } Q_ASSERT(false); return GroupType::Normal; } std::pair stateToBool(PlaylistState::ClipState state) { return {state == PlaylistState::VideoOnly, state == PlaylistState::AudioOnly}; } PlaylistState::ClipState stateFromBool(std::pair av) { assert(!av.first || !av.second); if (av.first) { return PlaylistState::VideoOnly; } else if (av.second) { return PlaylistState::AudioOnly; } else { return PlaylistState::Disabled; } } diff --git a/src/definitions.h b/src/definitions.h index 0060cb886..21a29e0c8 100644 --- a/src/definitions.h +++ b/src/definitions.h @@ -1,332 +1,330 @@ /*************************************************************************** * 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 DEFINITIONS_H #define DEFINITIONS_H #include "gentime.h" #include "kdenlive_debug.h" #include #include #include #include #include #include const int MAXCLIPDURATION = 15000; namespace Kdenlive { enum MonitorId { NoMonitor = 0x01, ClipMonitor = 0x02, ProjectMonitor = 0x04, RecordMonitor = 0x08, StopMotionMonitor = 0x10, DvdMonitor = 0x20 }; const int DefaultThumbHeight = 100; } // namespace Kdenlive enum class GroupType { Normal, Selection, // in that case, the group is used to emulate a selection AVSplit, // in that case, the group links the audio and video of the same clip Leaf // This is a leaf (clip or composition) }; const QString groupTypeToStr(GroupType t); GroupType groupTypeFromStr(const QString &s); enum class ObjectType { TimelineClip, TimelineComposition, TimelineTrack, BinClip, NoItem }; using ObjectId = std::pair; enum OperationType { None = 0, WaitingForConfirm, MoveOperation, ResizeStart, ResizeEnd, RollingStart, RollingEnd, RippleStart, RippleEnd, FadeIn, FadeOut, TransitionStart, TransitionEnd, MoveGuide, KeyFrame, Seek, Spacer, RubberSelection, ScrollTimeline, ZoomTimeline }; namespace PlaylistState { Q_NAMESPACE enum ClipState { VideoOnly = 1, AudioOnly = 2, Disabled = 3 }; Q_ENUM_NS(ClipState) } // namespace PlaylistState // returns a pair corresponding to (video, audio) std::pair stateToBool(PlaylistState::ClipState state); PlaylistState::ClipState stateFromBool(std::pair av); namespace TimelineMode { enum EditMode { NormalEdit = 0, OverwriteEdit = 1, InsertEdit = 2 }; } namespace ClipType { Q_NAMESPACE enum ProducerType { Unknown = 0, Audio = 1, Video = 2, AV = 3, Color = 4, Image = 5, Text = 6, SlideShow = 7, Virtual = 8, Playlist = 9, WebVfx = 10, TextTemplate = 11, QText = 12, Composition = 13, Track = 14 }; Q_ENUM_NS(ProducerType) } // namespace ClipType enum ProjectItemType { ProjectClipType = 0, ProjectFolderType, ProjectSubclipType }; enum GraphicsRectItem { AVWidget = 70000, LabelWidget, TransitionWidget, GroupWidget }; enum ProjectTool { SelectTool = 0, RazorTool = 1, SpacerTool = 2 }; enum MonitorSceneType { MonitorSceneNone = 0, MonitorSceneDefault, MonitorSceneGeometry, MonitorSceneCorners, MonitorSceneRoto, MonitorSceneSplit, MonitorSceneRipple }; enum MessageType { DefaultMessage, ProcessingJobMessage, OperationCompletedMessage, InformationMessage, ErrorMessage, MltError }; enum TrackType { AudioTrack = 0, VideoTrack = 1, AnyTrack = 2 }; enum CacheType { SystemCacheRoot = -1, CacheRoot = 0, CacheBase = 1, CachePreview = 2, CacheProxy = 3, CacheAudio = 4, CacheThumbs = 5 }; enum TrimMode { NormalTrim, RippleTrim, RollingTrim, SlipTrim, SlideTrim }; class TrackInfo { public: TrackType type; QString trackName; bool isMute; bool isBlind; bool isLocked; int duration; /*EffectsList effectsList; TrackInfo() : type(VideoTrack) , isMute(0) , isBlind(0) , isLocked(0) , duration(0) , effectsList(true) { }*/ }; struct requestClipInfo { QDomElement xml; QString clipId; int imageHeight; bool replaceProducer; bool operator==(const requestClipInfo &a) const { return clipId == a.clipId; } }; typedef QMap stringMap; typedef QMap> audioByteArray; -typedef QVector audioShortVector; +using audioShortVector = QVector; class ItemInfo { public: /** startPos is the position where the clip starts on the track */ GenTime startPos; /** endPos is the duration where the clip ends on the track */ GenTime endPos; /** cropStart is the position where the sub-clip starts, relative to the clip's 0 position */ GenTime cropStart; /** cropDuration is the duration of the clip */ GenTime cropDuration; /** Track number */ - int track; + int track{0}; ItemInfo() - : track(0) + { } bool isValid() const { return startPos != endPos; } bool contains(GenTime pos) const { if (startPos == endPos) { return true; } return (pos <= endPos && pos >= startPos); } bool operator==(const ItemInfo &a) const { return startPos == a.startPos && endPos == a.endPos && track == a.track && cropStart == a.cropStart; } }; class TransitionInfo { public: /** startPos is the position where the clip starts on the track */ GenTime startPos; /** endPos is the duration where the clip ends on the track */ GenTime endPos; /** the track on which the transition is (b_track)*/ - int b_track; + int b_track{0}; /** the track on which the transition is applied (a_track)*/ - int a_track; + int a_track{0}; /** Does the user request for a special a_track */ - bool forceTrack; + bool forceTrack{0}; TransitionInfo() - : b_track(0) - , a_track(0) - , forceTrack(0) + { } }; class CommentedTime { public: CommentedTime(); - CommentedTime(const GenTime &time, const QString &comment, int markerType = 0); + CommentedTime(const GenTime &time, QString comment, int markerType = 0); CommentedTime(const QString &hash, const GenTime &time); QString comment() const; GenTime time() const; /** @brief Returns a string containing infos needed to store marker info. string equals marker type + QLatin1Char(':') + marker comment */ QString hash() const; void setComment(const QString &comm); void setMarkerType(int t); int markerType() const; static QColor markerColor(int type); /* Implementation of > operator; Works identically as with basic types. */ bool operator>(const CommentedTime &op) const; /* Implementation of < operator; Works identically as with basic types. */ bool operator<(const CommentedTime &op) const; /* Implementation of >= operator; Works identically as with basic types. */ bool operator>=(const CommentedTime &op) const; /* Implementation of <= operator; Works identically as with basic types. */ bool operator<=(const CommentedTime &op) const; /* Implementation of == operator; Works identically as with basic types. */ bool operator==(const CommentedTime &op) const; /* Implementation of != operator; Works identically as with basic types. */ bool operator!=(const CommentedTime &op) const; private: GenTime m_time; QString m_comment; - int m_type; + int m_type{0}; }; QDebug operator<<(QDebug qd, const ItemInfo &info); // we provide hash function for qstring and QPersistentModelIndex namespace std { template <> struct hash { std::size_t operator()(const QString &k) const { return qHash(k); } }; template <> struct hash { std::size_t operator()(const QPersistentModelIndex &k) const { return qHash(k); } }; } // namespace std // The following is a hack that allows to use shared_from_this in the case of a multiple inheritance. // Credit: https://stackoverflow.com/questions/14939190/boost-shared-from-this-and-multiple-inheritance template struct enable_shared_from_this_virtual; class enable_shared_from_this_virtual_base : public std::enable_shared_from_this { - typedef std::enable_shared_from_this base_type; + using base_type = std::enable_shared_from_this; template friend struct enable_shared_from_this_virtual; std::shared_ptr shared_from_this() { return base_type::shared_from_this(); } std::shared_ptr shared_from_this() const { return base_type::shared_from_this(); } }; template struct enable_shared_from_this_virtual : virtual enable_shared_from_this_virtual_base { - typedef enable_shared_from_this_virtual_base base_type; + using base_type = enable_shared_from_this_virtual_base; public: std::shared_ptr shared_from_this() { std::shared_ptr result(base_type::shared_from_this(), static_cast(this)); return result; } std::shared_ptr shared_from_this() const { std::shared_ptr result(base_type::shared_from_this(), static_cast(this)); return result; } }; // This is a small trick to have a QAbstractItemModel with shared_from_this enabled without multiple inheritance // Be careful, if you use this class, you have to make sure to init weak_this_ when you construct a shared_ptr to your object template class QAbstractItemModel_shared_from_this : public QAbstractItemModel { protected: QAbstractItemModel_shared_from_this() : QAbstractItemModel() { } public: std::shared_ptr shared_from_this() { std::shared_ptr p(weak_this_); assert(p.get() == this); return p; } std::shared_ptr shared_from_this() const { std::shared_ptr p(weak_this_); assert(p.get() == this); return p; } public: // actually private, but avoids compiler template friendship issues mutable std::weak_ptr weak_this_; }; #endif diff --git a/src/dialogs/encodingprofilesdialog.h b/src/dialogs/encodingprofilesdialog.h index 8c5f86c4f..db4d7c4b7 100644 --- a/src/dialogs/encodingprofilesdialog.h +++ b/src/dialogs/encodingprofilesdialog.h @@ -1,48 +1,48 @@ /*************************************************************************** * 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 ENCODINGPROFILESDIALOG_H #define ENCODINGPROFILESDIALOG_H #include #include "definitions.h" #include "ui_manageencodingprofile_ui.h" class EncodingProfilesDialog : public QDialog, Ui::ManageEncodingProfile_UI { Q_OBJECT public: explicit EncodingProfilesDialog(int profileType, QWidget *parent = nullptr); - ~EncodingProfilesDialog(); + ~EncodingProfilesDialog() override; private slots: void slotLoadProfiles(); void slotShowParams(); void slotDeleteProfile(); void slotAddProfile(); void slotEditProfile(); private: KConfig *m_configFile; KConfigGroup *m_configGroup; }; #endif diff --git a/src/dialogs/kdenlivesettingsdialog.cpp b/src/dialogs/kdenlivesettingsdialog.cpp index 0d716ec0c..6da257c9f 100644 --- a/src/dialogs/kdenlivesettingsdialog.cpp +++ b/src/dialogs/kdenlivesettingsdialog.cpp @@ -1,1564 +1,1564 @@ /*************************************************************************** * 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 "kdenlivesettingsdialog.h" #include "clipcreationdialog.h" #include "core.h" #include "dialogs/profilesdialog.h" #include "encodingprofilesdialog.h" #include "kdenlivesettings.h" #include "profiles/profilemodel.hpp" #include "profiles/profilerepository.hpp" #include "profilesdialog.h" #include "project/dialogs/profilewidget.h" #ifdef USE_V4L #include "capture/v4lcapture.h" #endif #include "kdenlive_debug.h" #include "klocalizedstring.h" #include #include #include #include #include #include #include #include #include #include #include #include +#include +#include #include -#include -#include #include #ifdef USE_JOGSHUTTLE #include "jogshuttle/jogaction.h" #include "jogshuttle/jogshuttleconfig.h" #include #include #endif -KdenliveSettingsDialog::KdenliveSettingsDialog(const QMap &mappable_actions, bool gpuAllowed, QWidget *parent) +KdenliveSettingsDialog::KdenliveSettingsDialog(QMap mappable_actions, bool gpuAllowed, QWidget *parent) : KConfigDialog(parent, QStringLiteral("settings"), KdenliveSettings::self()) , m_modified(false) , m_shuttleModified(false) - , m_mappable_actions(mappable_actions) + , m_mappable_actions(std::move(mappable_actions)) { KdenliveSettings::setV4l_format(0); QWidget *p1 = new QWidget; QFontInfo ftInfo(font()); m_configMisc.setupUi(p1); m_page1 = addPage(p1, i18n("Misc")); m_page1->setIcon(QIcon::fromTheme(QStringLiteral("configure"))); m_configMisc.kcfg_use_exiftool->setEnabled(!QStandardPaths::findExecutable(QStringLiteral("exiftool")).isEmpty()); QWidget *p8 = new QWidget; m_configProject.setupUi(p8); m_page8 = addPage(p8, i18n("Project Defaults")); auto *vbox = new QVBoxLayout; m_pw = new ProfileWidget(this); vbox->addWidget(m_pw); m_configProject.profile_box->setLayout(vbox); m_configProject.profile_box->setTitle(i18n("Select the default profile (preset)")); // Select profile m_pw->loadProfile(KdenliveSettings::default_profile().isEmpty() ? pCore->getCurrentProfile()->path() : KdenliveSettings::default_profile()); connect(m_pw, &ProfileWidget::profileChanged, this, &KdenliveSettingsDialog::slotDialogModified); m_page8->setIcon(QIcon::fromTheme(QStringLiteral("project-defaults"))); m_configProject.projecturl->setMode(KFile::Directory); m_configProject.projecturl->setUrl(QUrl::fromLocalFile(KdenliveSettings::defaultprojectfolder())); QWidget *p9 = new QWidget; m_configProxy.setupUi(p9); KPageWidgetItem *page9 = addPage(p9, i18n("Proxy Clips")); page9->setIcon(QIcon::fromTheme(QStringLiteral("zoom-out"))); connect(m_configProxy.kcfg_generateproxy, &QAbstractButton::toggled, m_configProxy.kcfg_proxyminsize, &QWidget::setEnabled); m_configProxy.kcfg_proxyminsize->setEnabled(KdenliveSettings::generateproxy()); connect(m_configProxy.kcfg_generateimageproxy, &QAbstractButton::toggled, m_configProxy.kcfg_proxyimageminsize, &QWidget::setEnabled); m_configProxy.kcfg_proxyimageminsize->setEnabled(KdenliveSettings::generateimageproxy()); loadExternalProxyProfiles(); QWidget *p3 = new QWidget; m_configTimeline.setupUi(p3); m_page3 = addPage(p3, i18n("Timeline")); m_page3->setIcon(QIcon::fromTheme(QStringLiteral("video-display"))); m_configTimeline.kcfg_trackheight->setMinimum(ftInfo.pixelSize() * 1.5); QWidget *p2 = new QWidget; m_configEnv.setupUi(p2); m_configEnv.mltpathurl->setMode(KFile::Directory); m_configEnv.mltpathurl->lineEdit()->setObjectName(QStringLiteral("kcfg_mltpath")); m_configEnv.rendererpathurl->lineEdit()->setObjectName(QStringLiteral("kcfg_rendererpath")); m_configEnv.ffmpegurl->lineEdit()->setObjectName(QStringLiteral("kcfg_ffmpegpath")); m_configEnv.ffplayurl->lineEdit()->setObjectName(QStringLiteral("kcfg_ffplaypath")); m_configEnv.ffprobeurl->lineEdit()->setObjectName(QStringLiteral("kcfg_ffprobepath")); int maxThreads = QThread::idealThreadCount(); m_configEnv.kcfg_mltthreads->setMaximum(maxThreads > 2 ? maxThreads : 8); m_configEnv.tmppathurl->setMode(KFile::Directory); m_configEnv.tmppathurl->lineEdit()->setObjectName(QStringLiteral("kcfg_currenttmpfolder")); m_configEnv.capturefolderurl->setMode(KFile::Directory); m_configEnv.capturefolderurl->lineEdit()->setObjectName(QStringLiteral("kcfg_capturefolder")); m_configEnv.capturefolderurl->setEnabled(!KdenliveSettings::capturetoprojectfolder()); connect(m_configEnv.kcfg_capturetoprojectfolder, &QAbstractButton::clicked, this, &KdenliveSettingsDialog::slotEnableCaptureFolder); // Library folder m_configEnv.libraryfolderurl->setMode(KFile::Directory); m_configEnv.libraryfolderurl->lineEdit()->setObjectName(QStringLiteral("kcfg_libraryfolder")); m_configEnv.libraryfolderurl->setEnabled(!KdenliveSettings::librarytodefaultfolder()); m_configEnv.kcfg_librarytodefaultfolder->setToolTip(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/library")); connect(m_configEnv.kcfg_librarytodefaultfolder, &QAbstractButton::clicked, this, &KdenliveSettingsDialog::slotEnableLibraryFolder); // Mime types QStringList mimes = ClipCreationDialog::getExtensions(); qSort(mimes); m_configEnv.supportedmimes->setPlainText(mimes.join(QLatin1Char(' '))); m_page2 = addPage(p2, i18n("Environment")); m_page2->setIcon(QIcon::fromTheme(QStringLiteral("application-x-executable-script"))); QWidget *p4 = new QWidget; m_configCapture.setupUi(p4); m_configCapture.tabWidget->removeTab(0); m_configCapture.tabWidget->removeTab(2); #ifdef USE_V4L // Video 4 Linux device detection for (int i = 0; i < 10; ++i) { QString path = QStringLiteral("/dev/video") + QString::number(i); if (QFile::exists(path)) { QStringList deviceInfo = V4lCaptureHandler::getDeviceName(path); if (!deviceInfo.isEmpty()) { m_configCapture.kcfg_detectedv4ldevices->addItem(deviceInfo.at(0), path); m_configCapture.kcfg_detectedv4ldevices->setItemData(m_configCapture.kcfg_detectedv4ldevices->count() - 1, deviceInfo.at(1), Qt::UserRole + 1); } } } connect(m_configCapture.kcfg_detectedv4ldevices, static_cast(&KComboBox::currentIndexChanged), this, &KdenliveSettingsDialog::slotUpdatev4lDevice); connect(m_configCapture.kcfg_v4l_format, static_cast(&KComboBox::currentIndexChanged), this, &KdenliveSettingsDialog::slotUpdatev4lCaptureProfile); connect(m_configCapture.config_v4l, &QAbstractButton::clicked, this, &KdenliveSettingsDialog::slotEditVideo4LinuxProfile); slotUpdatev4lDevice(); #endif m_page4 = addPage(p4, i18n("Capture")); m_page4->setIcon(QIcon::fromTheme(QStringLiteral("media-record"))); m_configCapture.tabWidget->setCurrentIndex(KdenliveSettings::defaultcapture()); #ifdef Q_WS_MAC m_configCapture.tabWidget->setEnabled(false); m_configCapture.kcfg_defaultcapture->setEnabled(false); m_configCapture.label->setText(i18n("Capture is not yet available on Mac OS X.")); #endif QWidget *p5 = new QWidget; m_configShuttle.setupUi(p5); #ifdef USE_JOGSHUTTLE m_configShuttle.toolBtnReload->setIcon(QIcon::fromTheme(QStringLiteral("view-refresh"))); connect(m_configShuttle.kcfg_enableshuttle, &QCheckBox::stateChanged, this, &KdenliveSettingsDialog::slotCheckShuttle); connect(m_configShuttle.shuttledevicelist, SIGNAL(activated(int)), this, SLOT(slotUpdateShuttleDevice(int))); connect(m_configShuttle.toolBtnReload, &QAbstractButton::clicked, this, &KdenliveSettingsDialog::slotReloadShuttleDevices); slotCheckShuttle(static_cast(KdenliveSettings::enableshuttle())); m_configShuttle.shuttledisabled->hide(); // Store the button pointers into an array for easier handling them in the other functions. // TODO: impl enumerator or live with cut and paste :-))) setupJogshuttleBtns(KdenliveSettings::shuttledevice()); #if 0 m_shuttle_buttons.push_back(m_configShuttle.shuttle1); m_shuttle_buttons.push_back(m_configShuttle.shuttle2); m_shuttle_buttons.push_back(m_configShuttle.shuttle3); m_shuttle_buttons.push_back(m_configShuttle.shuttle4); m_shuttle_buttons.push_back(m_configShuttle.shuttle5); m_shuttle_buttons.push_back(m_configShuttle.shuttle6); m_shuttle_buttons.push_back(m_configShuttle.shuttle7); m_shuttle_buttons.push_back(m_configShuttle.shuttle8); m_shuttle_buttons.push_back(m_configShuttle.shuttle9); m_shuttle_buttons.push_back(m_configShuttle.shuttle10); m_shuttle_buttons.push_back(m_configShuttle.shuttle11); m_shuttle_buttons.push_back(m_configShuttle.shuttle12); m_shuttle_buttons.push_back(m_configShuttle.shuttle13); m_shuttle_buttons.push_back(m_configShuttle.shuttle14); m_shuttle_buttons.push_back(m_configShuttle.shuttle15); #endif #else /* ! USE_JOGSHUTTLE */ m_configShuttle.kcfg_enableshuttle->hide(); m_configShuttle.kcfg_enableshuttle->setDisabled(true); #endif /* USE_JOGSHUTTLE */ m_page5 = addPage(p5, i18n("JogShuttle")); m_page5->setIcon(QIcon::fromTheme(QStringLiteral("jog-dial"))); QWidget *p6 = new QWidget; m_configSdl.setupUi(p6); m_configSdl.reload_blackmagic->setIcon(QIcon::fromTheme(QStringLiteral("view-refresh"))); connect(m_configSdl.reload_blackmagic, &QAbstractButton::clicked, this, &KdenliveSettingsDialog::slotReloadBlackMagic); // m_configSdl.kcfg_openglmonitors->setHidden(true); m_page6 = addPage(p6, i18n("Playback")); m_page6->setIcon(QIcon::fromTheme(QStringLiteral("media-playback-start"))); QWidget *p7 = new QWidget; m_configTranscode.setupUi(p7); m_page7 = addPage(p7, i18n("Transcode")); m_page7->setIcon(QIcon::fromTheme(QStringLiteral("edit-copy"))); connect(m_configTranscode.button_add, &QAbstractButton::clicked, this, &KdenliveSettingsDialog::slotAddTranscode); connect(m_configTranscode.button_delete, &QAbstractButton::clicked, this, &KdenliveSettingsDialog::slotDeleteTranscode); connect(m_configTranscode.profiles_list, &QListWidget::itemChanged, this, &KdenliveSettingsDialog::slotDialogModified); connect(m_configTranscode.profiles_list, &QListWidget::currentRowChanged, this, &KdenliveSettingsDialog::slotSetTranscodeProfile); connect(m_configTranscode.profile_name, &QLineEdit::textChanged, this, &KdenliveSettingsDialog::slotEnableTranscodeUpdate); connect(m_configTranscode.profile_description, &QLineEdit::textChanged, this, &KdenliveSettingsDialog::slotEnableTranscodeUpdate); connect(m_configTranscode.profile_extension, &QLineEdit::textChanged, this, &KdenliveSettingsDialog::slotEnableTranscodeUpdate); connect(m_configTranscode.profile_parameters, &QPlainTextEdit::textChanged, this, &KdenliveSettingsDialog::slotEnableTranscodeUpdate); connect(m_configTranscode.profile_audioonly, &QCheckBox::stateChanged, this, &KdenliveSettingsDialog::slotEnableTranscodeUpdate); connect(m_configTranscode.button_update, &QAbstractButton::pressed, this, &KdenliveSettingsDialog::slotUpdateTranscodingProfile); m_configTranscode.profile_parameters->setMaximumHeight(QFontMetrics(font()).lineSpacing() * 5); connect(m_configEnv.kp_image, &QAbstractButton::clicked, this, &KdenliveSettingsDialog::slotEditImageApplication); connect(m_configEnv.kp_audio, &QAbstractButton::clicked, this, &KdenliveSettingsDialog::slotEditAudioApplication); loadEncodingProfiles(); connect(m_configSdl.kcfg_audio_driver, static_cast(&KComboBox::currentIndexChanged), this, &KdenliveSettingsDialog::slotCheckAlsaDriver); connect(m_configSdl.kcfg_audio_backend, static_cast(&KComboBox::currentIndexChanged), this, &KdenliveSettingsDialog::slotCheckAudioBackend); initDevices(); connect(m_configCapture.kcfg_grab_capture_type, static_cast(&KComboBox::currentIndexChanged), this, &KdenliveSettingsDialog::slotUpdateGrabRegionStatus); slotUpdateGrabRegionStatus(); loadTranscodeProfiles(); // decklink profile QAction *act = new QAction(QIcon::fromTheme(QStringLiteral("configure")), i18n("Configure profiles"), this); act->setData(4); connect(act, &QAction::triggered, this, &KdenliveSettingsDialog::slotManageEncodingProfile); m_configCapture.decklink_manageprofile->setDefaultAction(act); m_configCapture.decklink_showprofileinfo->setIcon(QIcon::fromTheme(QStringLiteral("help-about"))); m_configCapture.decklink_parameters->setVisible(false); m_configCapture.decklink_parameters->setMaximumHeight(QFontMetrics(font()).lineSpacing() * 4); m_configCapture.decklink_parameters->setPlainText(KdenliveSettings::decklink_parameters()); connect(m_configCapture.kcfg_decklink_profile, static_cast(&KComboBox::currentIndexChanged), this, &KdenliveSettingsDialog::slotUpdateDecklinkProfile); connect(m_configCapture.decklink_showprofileinfo, &QAbstractButton::clicked, m_configCapture.decklink_parameters, &QWidget::setVisible); // ffmpeg profile m_configCapture.v4l_showprofileinfo->setIcon(QIcon::fromTheme(QStringLiteral("help-about"))); m_configCapture.v4l_parameters->setVisible(false); m_configCapture.v4l_parameters->setMaximumHeight(QFontMetrics(font()).lineSpacing() * 4); m_configCapture.v4l_parameters->setPlainText(KdenliveSettings::v4l_parameters()); act = new QAction(QIcon::fromTheme(QStringLiteral("configure")), i18n("Configure profiles"), this); act->setData(2); connect(act, &QAction::triggered, this, &KdenliveSettingsDialog::slotManageEncodingProfile); m_configCapture.v4l_manageprofile->setDefaultAction(act); connect(m_configCapture.kcfg_v4l_profile, static_cast(&KComboBox::currentIndexChanged), this, &KdenliveSettingsDialog::slotUpdateV4lProfile); connect(m_configCapture.v4l_showprofileinfo, &QAbstractButton::clicked, m_configCapture.v4l_parameters, &QWidget::setVisible); // screen grab profile m_configCapture.grab_showprofileinfo->setIcon(QIcon::fromTheme(QStringLiteral("help-about"))); m_configCapture.grab_parameters->setVisible(false); m_configCapture.grab_parameters->setMaximumHeight(QFontMetrics(font()).lineSpacing() * 4); m_configCapture.grab_parameters->setPlainText(KdenliveSettings::grab_parameters()); act = new QAction(QIcon::fromTheme(QStringLiteral("configure")), i18n("Configure profiles"), this); act->setData(3); connect(act, &QAction::triggered, this, &KdenliveSettingsDialog::slotManageEncodingProfile); m_configCapture.grab_manageprofile->setDefaultAction(act); connect(m_configCapture.kcfg_grab_profile, static_cast(&KComboBox::currentIndexChanged), this, &KdenliveSettingsDialog::slotUpdateGrabProfile); connect(m_configCapture.grab_showprofileinfo, &QAbstractButton::clicked, m_configCapture.grab_parameters, &QWidget::setVisible); // Timeline preview act = new QAction(QIcon::fromTheme(QStringLiteral("configure")), i18n("Configure profiles"), this); act->setData(1); connect(act, &QAction::triggered, this, &KdenliveSettingsDialog::slotManageEncodingProfile); m_configProject.preview_manageprofile->setDefaultAction(act); connect(m_configProject.kcfg_preview_profile, static_cast(&KComboBox::currentIndexChanged), this, &KdenliveSettingsDialog::slotUpdatePreviewProfile); connect(m_configProject.preview_showprofileinfo, &QAbstractButton::clicked, m_configProject.previewparams, &QWidget::setVisible); m_configProject.previewparams->setVisible(false); m_configProject.previewparams->setMaximumHeight(QFontMetrics(font()).lineSpacing() * 3); m_configProject.previewparams->setPlainText(KdenliveSettings::previewparams()); m_configProject.preview_showprofileinfo->setIcon(QIcon::fromTheme(QStringLiteral("help-about"))); m_configProject.preview_showprofileinfo->setToolTip(i18n("Show default timeline preview parameters")); m_configProject.preview_manageprofile->setIcon(QIcon::fromTheme(QStringLiteral("configure"))); m_configProject.preview_manageprofile->setToolTip(i18n("Manage timeline preview profiles")); m_configProject.kcfg_preview_profile->setToolTip(i18n("Select default timeline preview profile")); // proxy profile stuff m_configProxy.proxy_showprofileinfo->setIcon(QIcon::fromTheme(QStringLiteral("help-about"))); m_configProxy.proxy_showprofileinfo->setToolTip(i18n("Show default profile parameters")); m_configProxy.proxy_manageprofile->setIcon(QIcon::fromTheme(QStringLiteral("configure"))); m_configProxy.proxy_manageprofile->setToolTip(i18n("Manage proxy profiles")); m_configProxy.kcfg_proxy_profile->setToolTip(i18n("Select default proxy profile")); m_configProxy.proxyparams->setVisible(false); m_configProxy.proxyparams->setMaximumHeight(QFontMetrics(font()).lineSpacing() * 3); m_configProxy.proxyparams->setPlainText(KdenliveSettings::proxyparams()); act = new QAction(QIcon::fromTheme(QStringLiteral("configure")), i18n("Configure profiles"), this); act->setData(0); connect(act, &QAction::triggered, this, &KdenliveSettingsDialog::slotManageEncodingProfile); m_configProxy.proxy_manageprofile->setDefaultAction(act); connect(m_configProxy.proxy_showprofileinfo, &QAbstractButton::clicked, m_configProxy.proxyparams, &QWidget::setVisible); connect(m_configProxy.kcfg_proxy_profile, static_cast(&KComboBox::currentIndexChanged), this, &KdenliveSettingsDialog::slotUpdateProxyProfile); slotUpdateProxyProfile(-1); slotUpdateV4lProfile(-1); slotUpdateGrabProfile(-1); slotUpdateDecklinkProfile(-1); // enable GPU accel only if Movit is found m_configSdl.kcfg_gpu_accel->setEnabled(gpuAllowed); m_configSdl.kcfg_gpu_accel->setToolTip(i18n("GPU processing needs MLT compiled with Movit and Rtaudio modules")); getBlackMagicDeviceList(m_configCapture.kcfg_decklink_capturedevice); if (!getBlackMagicOutputDeviceList(m_configSdl.kcfg_blackmagic_output_device)) { // No blackmagic card found m_configSdl.kcfg_external_display->setEnabled(false); } // Config dialog size KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup settingsGroup(config, "settings"); QSize optimalSize; if (!settingsGroup.exists() || !settingsGroup.hasKey("dialogSize")) { const QSize screenSize = (QGuiApplication::primaryScreen()->availableSize() * 0.9); const QSize targetSize = QSize(1024, 700); optimalSize = targetSize.boundedTo(screenSize); } else { optimalSize = settingsGroup.readEntry("dialogSize", QVariant(size())).toSize(); } resize(optimalSize); } // static bool KdenliveSettingsDialog::getBlackMagicDeviceList(KComboBox *devicelist, bool force) { if (!force && !KdenliveSettings::decklink_device_found()) { return false; } Mlt::Profile profile; Mlt::Producer bm(profile, "decklink"); int found_devices = 0; if (bm.is_valid()) { bm.set("list_devices", 1); found_devices = bm.get_int("devices"); } else { KdenliveSettings::setDecklink_device_found(false); } if (found_devices <= 0) { devicelist->setEnabled(false); return false; } KdenliveSettings::setDecklink_device_found(true); for (int i = 0; i < found_devices; ++i) { char *tmp = qstrdup(QStringLiteral("device.%1").arg(i).toUtf8().constData()); devicelist->addItem(bm.get(tmp)); delete[] tmp; } return true; } bool KdenliveSettingsDialog::getBlackMagicOutputDeviceList(KComboBox *devicelist, bool force) { if (!force && !KdenliveSettings::decklink_device_found()) { return false; } Mlt::Profile profile; Mlt::Consumer bm(profile, "decklink"); int found_devices = 0; if (bm.is_valid()) { bm.set("list_devices", 1); found_devices = bm.get_int("devices"); } else { KdenliveSettings::setDecklink_device_found(false); } if (found_devices <= 0) { devicelist->setEnabled(false); return false; } KdenliveSettings::setDecklink_device_found(true); for (int i = 0; i < found_devices; ++i) { char *tmp = qstrdup(QStringLiteral("device.%1").arg(i).toUtf8().constData()); devicelist->addItem(bm.get(tmp)); delete[] tmp; } devicelist->addItem(QStringLiteral("test")); return true; } void KdenliveSettingsDialog::setupJogshuttleBtns(const QString &device) { QList list; QList list1; list << m_configShuttle.shuttle1; list << m_configShuttle.shuttle2; list << m_configShuttle.shuttle3; list << m_configShuttle.shuttle4; list << m_configShuttle.shuttle5; list << m_configShuttle.shuttle6; list << m_configShuttle.shuttle7; list << m_configShuttle.shuttle8; list << m_configShuttle.shuttle9; list << m_configShuttle.shuttle10; list << m_configShuttle.shuttle11; list << m_configShuttle.shuttle12; list << m_configShuttle.shuttle13; list << m_configShuttle.shuttle14; list << m_configShuttle.shuttle15; list1 << m_configShuttle.label_2; // #1 list1 << m_configShuttle.label_4; // #2 list1 << m_configShuttle.label_3; // #3 list1 << m_configShuttle.label_7; // #4 list1 << m_configShuttle.label_5; // #5 list1 << m_configShuttle.label_6; // #6 list1 << m_configShuttle.label_8; // #7 list1 << m_configShuttle.label_9; // #8 list1 << m_configShuttle.label_10; // #9 list1 << m_configShuttle.label_11; // #10 list1 << m_configShuttle.label_12; // #11 list1 << m_configShuttle.label_13; // #12 list1 << m_configShuttle.label_14; // #13 list1 << m_configShuttle.label_15; // #14 list1 << m_configShuttle.label_16; // #15 for (int i = 0; i < list.count(); ++i) { list[i]->hide(); list1[i]->hide(); } #ifdef USE_JOGSHUTTLE if (!m_configShuttle.kcfg_enableshuttle->isChecked()) { return; } int keysCount = JogShuttle::keysCount(device); for (int i = 0; i < keysCount; ++i) { m_shuttle_buttons.push_back(list[i]); list[i]->show(); list1[i]->show(); } // populate the buttons with the current configuration. The items are sorted // according to the user-selected language, so they do not appear in random order. QMap mappable_actions(m_mappable_actions); QList action_names = mappable_actions.keys(); QList::Iterator iter = action_names.begin(); // qCDebug(KDENLIVE_LOG) << "::::::::::::::::"; while (iter != action_names.end()) { // qCDebug(KDENLIVE_LOG) << *iter; ++iter; } // qCDebug(KDENLIVE_LOG) << "::::::::::::::::"; qSort(action_names); iter = action_names.begin(); while (iter != action_names.end()) { // qCDebug(KDENLIVE_LOG) << *iter; ++iter; } // qCDebug(KDENLIVE_LOG) << "::::::::::::::::"; // Here we need to compute the action_id -> index-in-action_names. We iterate over the // action_names, as the sorting may depend on the user-language. QStringList actions_map = JogShuttleConfig::actionMap(KdenliveSettings::shuttlebuttons()); QMap action_pos; for (const QString &action_id : actions_map) { // This loop find out at what index is the string that would map to the action_id. for (int i = 0; i < action_names.size(); ++i) { if (mappable_actions[action_names.at(i)] == action_id) { action_pos[action_id] = i; break; } } } int i = 0; for (KComboBox *button : m_shuttle_buttons) { button->addItems(action_names); connect(button, SIGNAL(activated(int)), this, SLOT(slotShuttleModified())); ++i; if (i < actions_map.size()) { button->setCurrentIndex(action_pos[actions_map[i]]); } } #endif } -KdenliveSettingsDialog::~KdenliveSettingsDialog() {} +KdenliveSettingsDialog::~KdenliveSettingsDialog() = default; void KdenliveSettingsDialog::slotUpdateGrabRegionStatus() { m_configCapture.region_group->setHidden(m_configCapture.kcfg_grab_capture_type->currentIndex() != 1); } void KdenliveSettingsDialog::slotEnableCaptureFolder() { m_configEnv.capturefolderurl->setEnabled(!m_configEnv.kcfg_capturetoprojectfolder->isChecked()); } void KdenliveSettingsDialog::slotEnableLibraryFolder() { m_configEnv.libraryfolderurl->setEnabled(!m_configEnv.kcfg_librarytodefaultfolder->isChecked()); } void KdenliveSettingsDialog::initDevices() { // Fill audio drivers m_configSdl.kcfg_audio_driver->addItem(i18n("Automatic"), QString()); #ifndef Q_WS_MAC m_configSdl.kcfg_audio_driver->addItem(i18n("OSS"), "dsp"); m_configSdl.kcfg_audio_driver->addItem(i18n("ALSA"), "alsa"); m_configSdl.kcfg_audio_driver->addItem(i18n("PulseAudio"), "pulse"); m_configSdl.kcfg_audio_driver->addItem(i18n("OSS with DMA access"), "dma"); m_configSdl.kcfg_audio_driver->addItem(i18n("Esound daemon"), "esd"); m_configSdl.kcfg_audio_driver->addItem(i18n("ARTS daemon"), "artsc"); #endif if (!KdenliveSettings::audiodrivername().isEmpty()) for (int i = 1; i < m_configSdl.kcfg_audio_driver->count(); ++i) { if (m_configSdl.kcfg_audio_driver->itemData(i).toString() == KdenliveSettings::audiodrivername()) { m_configSdl.kcfg_audio_driver->setCurrentIndex(i); KdenliveSettings::setAudio_driver((uint)i); } } // Fill the list of audio playback / recording devices m_configSdl.kcfg_audio_device->addItem(i18n("Default"), QString()); m_configCapture.kcfg_v4l_alsadevice->addItem(i18n("Default"), "default"); if (!QStandardPaths::findExecutable(QStringLiteral("aplay")).isEmpty()) { m_readProcess.setOutputChannelMode(KProcess::OnlyStdoutChannel); m_readProcess.setProgram(QStringLiteral("aplay"), QStringList() << QStringLiteral("-l")); connect(&m_readProcess, &KProcess::readyReadStandardOutput, this, &KdenliveSettingsDialog::slotReadAudioDevices); m_readProcess.execute(5000); } else { // If aplay is not installed on the system, parse the /proc/asound/pcm file QFile file(QStringLiteral("/proc/asound/pcm")); if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { QTextStream stream(&file); QString line = stream.readLine(); QString deviceId; while (!line.isNull()) { if (line.contains(QStringLiteral("playback"))) { deviceId = line.section(QLatin1Char(':'), 0, 0); m_configSdl.kcfg_audio_device->addItem(line.section(QLatin1Char(':'), 1, 1), QStringLiteral("plughw:%1,%2") .arg(deviceId.section(QLatin1Char('-'), 0, 0).toInt()) .arg(deviceId.section(QLatin1Char('-'), 1, 1).toInt())); } if (line.contains(QStringLiteral("capture"))) { deviceId = line.section(QLatin1Char(':'), 0, 0); m_configCapture.kcfg_v4l_alsadevice->addItem( line.section(QLatin1Char(':'), 1, 1).simplified(), QStringLiteral("hw:%1,%2").arg(deviceId.section(QLatin1Char('-'), 0, 0).toInt()).arg(deviceId.section(QLatin1Char('-'), 1, 1).toInt())); } line = stream.readLine(); } file.close(); } else { qCDebug(KDENLIVE_LOG) << " / / / /CANNOT READ PCM"; } } // Add pulseaudio capture option m_configCapture.kcfg_v4l_alsadevice->addItem(i18n("PulseAudio"), "pulse"); if (!KdenliveSettings::audiodevicename().isEmpty()) { // Select correct alsa device int ix = m_configSdl.kcfg_audio_device->findData(KdenliveSettings::audiodevicename()); m_configSdl.kcfg_audio_device->setCurrentIndex(ix); KdenliveSettings::setAudio_device(ix); } if (!KdenliveSettings::v4l_alsadevicename().isEmpty()) { // Select correct alsa device int ix = m_configCapture.kcfg_v4l_alsadevice->findData(KdenliveSettings::v4l_alsadevicename()); m_configCapture.kcfg_v4l_alsadevice->setCurrentIndex(ix); KdenliveSettings::setV4l_alsadevice(ix); } m_configSdl.kcfg_audio_backend->addItem(i18n("SDL"), KdenliveSettings::sdlAudioBackend()); m_configSdl.kcfg_audio_backend->addItem(i18n("RtAudio"), "rtaudio"); if (!KdenliveSettings::audiobackend().isEmpty()) { int ix = m_configSdl.kcfg_audio_backend->findData(KdenliveSettings::audiobackend()); m_configSdl.kcfg_audio_backend->setCurrentIndex(ix); KdenliveSettings::setAudio_backend(ix); } m_configSdl.group_sdl->setEnabled(KdenliveSettings::audiobackend().startsWith(QLatin1String("sdl_audio"))); loadCurrentV4lProfileInfo(); } void KdenliveSettingsDialog::slotReadAudioDevices() { QString result = QString(m_readProcess.readAllStandardOutput()); // qCDebug(KDENLIVE_LOG) << "// / / / / / READING APLAY: "; // qCDebug(KDENLIVE_LOG) << result; const QStringList lines = result.split(QLatin1Char('\n')); for (const QString &devicestr : lines) { ////qCDebug(KDENLIVE_LOG) << "// READING LINE: " << data; if (!devicestr.startsWith(QLatin1Char(' ')) && devicestr.count(QLatin1Char(':')) > 1) { QString card = devicestr.section(QLatin1Char(':'), 0, 0).section(QLatin1Char(' '), -1); QString device = devicestr.section(QLatin1Char(':'), 1, 1).section(QLatin1Char(' '), -1); m_configSdl.kcfg_audio_device->addItem(devicestr.section(QLatin1Char(':'), -1).simplified(), QStringLiteral("plughw:%1,%2").arg(card).arg(device)); m_configCapture.kcfg_v4l_alsadevice->addItem(devicestr.section(QLatin1Char(':'), -1).simplified(), QStringLiteral("hw:%1,%2").arg(card).arg(device)); } } } void KdenliveSettingsDialog::showPage(int page, int option) { switch (page) { case 1: setCurrentPage(m_page1); break; case 2: setCurrentPage(m_page2); break; case 3: setCurrentPage(m_page3); break; case 4: setCurrentPage(m_page4); m_configCapture.tabWidget->setCurrentIndex(option); break; case 5: setCurrentPage(m_page5); break; case 6: setCurrentPage(m_page6); break; case 7: setCurrentPage(m_page7); break; default: setCurrentPage(m_page1); } } void KdenliveSettingsDialog::slotEditAudioApplication() { KService::Ptr service; QPointer dlg = new KOpenWithDialog(QList(), i18n("Select default audio editor"), m_configEnv.kcfg_defaultaudioapp->text(), this); if (dlg->exec() == QDialog::Accepted) { service = dlg->service(); m_configEnv.kcfg_defaultaudioapp->setText(KRun::binaryName(service->exec(), false)); } delete dlg; } void KdenliveSettingsDialog::slotEditImageApplication() { QPointer dlg = new KOpenWithDialog(QList(), i18n("Select default image editor"), m_configEnv.kcfg_defaultimageapp->text(), this); if (dlg->exec() == QDialog::Accepted) { KService::Ptr service = dlg->service(); m_configEnv.kcfg_defaultimageapp->setText(KRun::binaryName(service->exec(), false)); } delete dlg; } void KdenliveSettingsDialog::slotCheckShuttle(int state) { #ifdef USE_JOGSHUTTLE m_configShuttle.config_group->setEnabled(state != 0); m_configShuttle.shuttledevicelist->clear(); QStringList devNames = KdenliveSettings::shuttledevicenames(); QStringList devPaths = KdenliveSettings::shuttledevicepaths(); if (devNames.count() != devPaths.count()) { return; } for (int i = 0; i < devNames.count(); ++i) { m_configShuttle.shuttledevicelist->addItem(devNames.at(i), devPaths.at(i)); } if (state != 0) { setupJogshuttleBtns(m_configShuttle.shuttledevicelist->itemData(m_configShuttle.shuttledevicelist->currentIndex()).toString()); } #endif /* USE_JOGSHUTTLE */ } void KdenliveSettingsDialog::slotUpdateShuttleDevice(int ix) { #ifdef USE_JOGSHUTTLE QString device = m_configShuttle.shuttledevicelist->itemData(ix).toString(); // KdenliveSettings::setShuttledevice(device); setupJogshuttleBtns(device); m_configShuttle.kcfg_shuttledevice->setText(device); #endif /* USE_JOGSHUTTLE */ } void KdenliveSettingsDialog::updateWidgets() { // Revert widgets to last saved state (for example when user pressed "Cancel") // //qCDebug(KDENLIVE_LOG) << "// // // KCONFIG Revert called"; #ifdef USE_JOGSHUTTLE // revert jog shuttle device if (m_configShuttle.shuttledevicelist->count() > 0) { for (int i = 0; i < m_configShuttle.shuttledevicelist->count(); ++i) { if (m_configShuttle.shuttledevicelist->itemData(i) == KdenliveSettings::shuttledevice()) { m_configShuttle.shuttledevicelist->setCurrentIndex(i); break; } } } // Revert jog shuttle buttons QList action_names = m_mappable_actions.keys(); qSort(action_names); QStringList actions_map = JogShuttleConfig::actionMap(KdenliveSettings::shuttlebuttons()); QMap action_pos; for (const QString &action_id : actions_map) { // This loop find out at what index is the string that would map to the action_id. for (int i = 0; i < action_names.size(); ++i) { if (m_mappable_actions[action_names[i]] == action_id) { action_pos[action_id] = i; break; } } } int i = 0; for (KComboBox *button : m_shuttle_buttons) { ++i; if (i < actions_map.size()) { button->setCurrentIndex(action_pos[actions_map[i]]); } } #endif /* USE_JOGSHUTTLE */ } void KdenliveSettingsDialog::accept() { if (m_pw->selectedProfile().isEmpty()) { KMessageBox::error(this, i18n("Please select a video profile")); return; } KConfigDialog::accept(); } void KdenliveSettingsDialog::updateSettings() { // Save changes to settings (for example when user pressed "Apply" or "Ok") // //qCDebug(KDENLIVE_LOG) << "// // // KCONFIG UPDATE called"; if (m_pw->selectedProfile().isEmpty()) { KMessageBox::error(this, i18n("Please select a video profile")); return; } KdenliveSettings::setDefault_profile(m_pw->selectedProfile()); bool resetProfile = false; bool resetConsumer = false; bool fullReset = false; bool updateCapturePath = false; bool updateLibrary = false; /*if (m_configShuttle.shuttledevicelist->count() > 0) { QString device = m_configShuttle.shuttledevicelist->itemData(m_configShuttle.shuttledevicelist->currentIndex()).toString(); if (device != KdenliveSettings::shuttledevice()) KdenliveSettings::setShuttledevice(device); }*/ // Capture default folder if (m_configEnv.kcfg_capturetoprojectfolder->isChecked() != KdenliveSettings::capturetoprojectfolder()) { KdenliveSettings::setCapturetoprojectfolder(m_configEnv.kcfg_capturetoprojectfolder->isChecked()); updateCapturePath = true; } if (m_configProject.projecturl->url().toLocalFile() != KdenliveSettings::defaultprojectfolder()) { KdenliveSettings::setDefaultprojectfolder(m_configProject.projecturl->url().toLocalFile()); } if (m_configEnv.capturefolderurl->url().toLocalFile() != KdenliveSettings::capturefolder()) { KdenliveSettings::setCapturefolder(m_configEnv.capturefolderurl->url().toLocalFile()); updateCapturePath = true; } // Library default folder if (m_configEnv.kcfg_librarytodefaultfolder->isChecked() != KdenliveSettings::librarytodefaultfolder()) { KdenliveSettings::setLibrarytodefaultfolder(m_configEnv.kcfg_librarytodefaultfolder->isChecked()); updateLibrary = true; } if (m_configEnv.libraryfolderurl->url().toLocalFile() != KdenliveSettings::libraryfolder()) { KdenliveSettings::setLibraryfolder(m_configEnv.libraryfolderurl->url().toLocalFile()); if (!KdenliveSettings::librarytodefaultfolder()) { updateLibrary = true; } } if (m_configCapture.kcfg_v4l_format->currentIndex() != (int)KdenliveSettings::v4l_format()) { saveCurrentV4lProfile(); KdenliveSettings::setV4l_format(0); } // Check if screengrab is fullscreen if (m_configCapture.kcfg_grab_capture_type->currentIndex() != KdenliveSettings::grab_capture_type()) { KdenliveSettings::setGrab_capture_type(m_configCapture.kcfg_grab_capture_type->currentIndex()); emit updateFullScreenGrab(); } // Check encoding profiles // FFmpeg QString profilestr = m_configCapture.kcfg_v4l_profile->currentData().toString(); if (!profilestr.isEmpty() && (profilestr.section(QLatin1Char(';'), 0, 0) != KdenliveSettings::v4l_parameters() || profilestr.section(QLatin1Char(';'), 1, 1) != KdenliveSettings::v4l_extension())) { KdenliveSettings::setV4l_parameters(profilestr.section(QLatin1Char(';'), 0, 0)); KdenliveSettings::setV4l_extension(profilestr.section(QLatin1Char(';'), 1, 1)); } // screengrab profilestr = m_configCapture.kcfg_grab_profile->currentData().toString(); if (!profilestr.isEmpty() && (profilestr.section(QLatin1Char(';'), 0, 0) != KdenliveSettings::grab_parameters() || profilestr.section(QLatin1Char(';'), 1, 1) != KdenliveSettings::grab_extension())) { KdenliveSettings::setGrab_parameters(profilestr.section(QLatin1Char(';'), 0, 0)); KdenliveSettings::setGrab_extension(profilestr.section(QLatin1Char(';'), 1, 1)); } // decklink profilestr = m_configCapture.kcfg_decklink_profile->currentData().toString(); if (!profilestr.isEmpty() && (profilestr.section(QLatin1Char(';'), 0, 0) != KdenliveSettings::decklink_parameters() || profilestr.section(QLatin1Char(';'), 1, 1) != KdenliveSettings::decklink_extension())) { KdenliveSettings::setDecklink_parameters(profilestr.section(QLatin1Char(';'), 0, 0)); KdenliveSettings::setDecklink_extension(profilestr.section(QLatin1Char(';'), 1, 1)); } // proxies profilestr = m_configProxy.kcfg_proxy_profile->currentData().toString(); if (!profilestr.isEmpty() && (profilestr.section(QLatin1Char(';'), 0, 0) != KdenliveSettings::proxyparams() || profilestr.section(QLatin1Char(';'), 1, 1) != KdenliveSettings::proxyextension())) { KdenliveSettings::setProxyparams(profilestr.section(QLatin1Char(';'), 0, 0)); KdenliveSettings::setProxyextension(profilestr.section(QLatin1Char(';'), 1, 1)); } // external proxies profilestr = m_configProxy.kcfg_external_proxy_profile->currentData().toString(); if (!profilestr.isEmpty() && (profilestr != KdenliveSettings::externalProxyProfile())) { KdenliveSettings::setExternalProxyProfile(profilestr); } // timeline preview profilestr = m_configProject.kcfg_preview_profile->currentData().toString(); if (!profilestr.isEmpty() && (profilestr.section(QLatin1Char(';'), 0, 0) != KdenliveSettings::previewparams() || profilestr.section(QLatin1Char(';'), 1, 1) != KdenliveSettings::previewextension())) { KdenliveSettings::setPreviewparams(profilestr.section(QLatin1Char(';'), 0, 0)); KdenliveSettings::setPreviewextension(profilestr.section(QLatin1Char(';'), 1, 1)); } if (updateCapturePath) { emit updateCaptureFolder(); } if (updateLibrary) { emit updateLibraryFolder(); } QString value = m_configCapture.kcfg_v4l_alsadevice->currentData().toString(); if (value != KdenliveSettings::v4l_alsadevicename()) { KdenliveSettings::setV4l_alsadevicename(value); } if (m_configSdl.kcfg_external_display->isChecked() != KdenliveSettings::external_display()) { KdenliveSettings::setExternal_display(m_configSdl.kcfg_external_display->isChecked()); resetConsumer = true; fullReset = true; } else if (KdenliveSettings::external_display() && KdenliveSettings::blackmagic_output_device() != m_configSdl.kcfg_blackmagic_output_device->currentIndex()) { resetConsumer = true; fullReset = true; } value = m_configSdl.kcfg_audio_driver->currentData().toString(); if (value != KdenliveSettings::audiodrivername()) { KdenliveSettings::setAudiodrivername(value); resetConsumer = true; } if (value == QLatin1String("alsa")) { // Audio device setting is only valid for alsa driver value = m_configSdl.kcfg_audio_device->currentData().toString(); if (value != KdenliveSettings::audiodevicename()) { KdenliveSettings::setAudiodevicename(value); resetConsumer = true; } } else if (!KdenliveSettings::audiodevicename().isEmpty()) { KdenliveSettings::setAudiodevicename(QString()); resetConsumer = true; } value = m_configSdl.kcfg_audio_backend->currentData().toString(); if (value != KdenliveSettings::audiobackend()) { KdenliveSettings::setAudiobackend(value); resetConsumer = true; fullReset = true; } if (m_configSdl.kcfg_window_background->color() != KdenliveSettings::window_background()) { KdenliveSettings::setWindow_background(m_configSdl.kcfg_window_background->color()); resetProfile = true; } if (m_configSdl.kcfg_volume->value() != KdenliveSettings::volume()) { KdenliveSettings::setVolume(m_configSdl.kcfg_volume->value()); resetConsumer = true; } if (m_configMisc.kcfg_tabposition->currentIndex() != KdenliveSettings::tabposition()) { KdenliveSettings::setTabposition(m_configMisc.kcfg_tabposition->currentIndex()); } if (m_configTimeline.kcfg_displayallchannels->isChecked() != KdenliveSettings::displayallchannels()) { KdenliveSettings::setDisplayallchannels(m_configTimeline.kcfg_displayallchannels->isChecked()); emit audioThumbFormatChanged(); } if (m_modified) { // The transcoding profiles were modified, save. m_modified = false; saveTranscodeProfiles(); } #ifdef USE_JOGSHUTTLE m_shuttleModified = false; QStringList actions; actions << QStringLiteral("monitor_pause"); // the Job rest position action. for (KComboBox *button : m_shuttle_buttons) { actions << m_mappable_actions[button->currentText()]; } QString maps = JogShuttleConfig::actionMap(actions); // fprintf(stderr, "Shuttle config: %s\n", JogShuttleConfig::actionMap(actions).toLatin1().constData()); if (KdenliveSettings::shuttlebuttons() != maps) { KdenliveSettings::setShuttlebuttons(maps); } #endif bool restart = false; if (m_configSdl.kcfg_gpu_accel->isChecked() != KdenliveSettings::gpu_accel()) { // GPU setting was changed, we need to restart Kdenlive or everything will be corrupted if (KMessageBox::warningContinueCancel(this, i18n("Kdenlive must be restarted to change this setting")) == KMessageBox::Continue) { restart = true; } else { m_configSdl.kcfg_gpu_accel->setChecked(KdenliveSettings::gpu_accel()); } } if (m_configTimeline.kcfg_trackheight->value() != KdenliveSettings::trackheight()) { KdenliveSettings::setTrackheight(m_configTimeline.kcfg_trackheight->value()); emit resetView(); } // Mimes if (m_configEnv.kcfg_addedExtensions->text() != KdenliveSettings::addedExtensions()) { // Update list KdenliveSettings::setAddedExtensions(m_configEnv.kcfg_addedExtensions->text()); QStringList mimes = ClipCreationDialog::getExtensions(); qSort(mimes); m_configEnv.supportedmimes->setPlainText(mimes.join(QLatin1Char(' '))); } KConfigDialog::settingsChangedSlot(); // KConfigDialog::updateSettings(); if (resetConsumer) { emit doResetConsumer(fullReset); } if (resetProfile) { emit doResetProfile(); } if (restart) { emit restartKdenlive(); } emit checkTabPosition(); // remembering Config dialog size KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup settingsGroup(config, "settings"); settingsGroup.writeEntry("dialogSize", QVariant(size())); } void KdenliveSettingsDialog::slotCheckAlsaDriver() { QString value = m_configSdl.kcfg_audio_driver->itemData(m_configSdl.kcfg_audio_driver->currentIndex()).toString(); m_configSdl.kcfg_audio_device->setEnabled(value == QLatin1String("alsa")); } void KdenliveSettingsDialog::slotCheckAudioBackend() { QString value = m_configSdl.kcfg_audio_backend->itemData(m_configSdl.kcfg_audio_backend->currentIndex()).toString(); m_configSdl.group_sdl->setEnabled(value.startsWith(QLatin1String("sdl_audio"))); } void KdenliveSettingsDialog::loadTranscodeProfiles() { KSharedConfigPtr config = KSharedConfig::openConfig(QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("kdenlivetranscodingrc")), KConfig::CascadeConfig); KConfigGroup transConfig(config, "Transcoding"); // read the entries m_configTranscode.profiles_list->blockSignals(true); m_configTranscode.profiles_list->clear(); QMap profiles = transConfig.entryMap(); QMapIterator i(profiles); while (i.hasNext()) { i.next(); auto *item = new QListWidgetItem(i.key()); QString profilestr = i.value(); if (profilestr.contains(QLatin1Char(';'))) { item->setToolTip(profilestr.section(QLatin1Char(';'), 1, 1)); } item->setData(Qt::UserRole, profilestr); m_configTranscode.profiles_list->addItem(item); // item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsEditable); } m_configTranscode.profiles_list->blockSignals(false); m_configTranscode.profiles_list->setCurrentRow(0); } void KdenliveSettingsDialog::saveTranscodeProfiles() { QString transcodeFile = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/kdenlivetranscodingrc"); KSharedConfigPtr config = KSharedConfig::openConfig(transcodeFile); KConfigGroup transConfig(config, "Transcoding"); // read the entries transConfig.deleteGroup(); int max = m_configTranscode.profiles_list->count(); for (int i = 0; i < max; ++i) { QListWidgetItem *item = m_configTranscode.profiles_list->item(i); transConfig.writeEntry(item->text(), item->data(Qt::UserRole).toString()); } config->sync(); } void KdenliveSettingsDialog::slotAddTranscode() { if (!m_configTranscode.profiles_list->findItems(m_configTranscode.profile_name->text(), Qt::MatchExactly).isEmpty()) { KMessageBox::sorry(this, i18n("A profile with that name already exists")); return; } QListWidgetItem *item = new QListWidgetItem(m_configTranscode.profile_name->text()); QString profilestr = m_configTranscode.profile_parameters->toPlainText(); profilestr.append(" %1." + m_configTranscode.profile_extension->text()); profilestr.append(';'); if (!m_configTranscode.profile_description->text().isEmpty()) { profilestr.append(m_configTranscode.profile_description->text()); } if (m_configTranscode.profile_audioonly->isChecked()) { profilestr.append(";audio"); } item->setData(Qt::UserRole, profilestr); m_configTranscode.profiles_list->addItem(item); m_configTranscode.profiles_list->setCurrentItem(item); slotDialogModified(); } void KdenliveSettingsDialog::slotUpdateTranscodingProfile() { QListWidgetItem *item = m_configTranscode.profiles_list->currentItem(); if (!item) { return; } m_configTranscode.button_update->setEnabled(false); item->setText(m_configTranscode.profile_name->text()); QString profilestr = m_configTranscode.profile_parameters->toPlainText(); profilestr.append(" %1." + m_configTranscode.profile_extension->text()); profilestr.append(';'); if (!m_configTranscode.profile_description->text().isEmpty()) { profilestr.append(m_configTranscode.profile_description->text()); } if (m_configTranscode.profile_audioonly->isChecked()) { profilestr.append(QStringLiteral(";audio")); } item->setData(Qt::UserRole, profilestr); slotDialogModified(); } void KdenliveSettingsDialog::slotDeleteTranscode() { QListWidgetItem *item = m_configTranscode.profiles_list->currentItem(); if (item == nullptr) { return; } delete item; slotDialogModified(); } void KdenliveSettingsDialog::slotEnableTranscodeUpdate() { if (!m_configTranscode.profile_box->isEnabled()) { return; } bool allow = true; if (m_configTranscode.profile_name->text().isEmpty() || m_configTranscode.profile_extension->text().isEmpty()) { allow = false; } m_configTranscode.button_update->setEnabled(allow); } void KdenliveSettingsDialog::slotSetTranscodeProfile() { m_configTranscode.profile_box->setEnabled(false); m_configTranscode.button_update->setEnabled(false); m_configTranscode.profile_name->clear(); m_configTranscode.profile_description->clear(); m_configTranscode.profile_extension->clear(); m_configTranscode.profile_parameters->clear(); m_configTranscode.profile_audioonly->setChecked(false); QListWidgetItem *item = m_configTranscode.profiles_list->currentItem(); if (!item) { return; } m_configTranscode.profile_name->setText(item->text()); QString profilestr = item->data(Qt::UserRole).toString(); if (profilestr.contains(QLatin1Char(';'))) { m_configTranscode.profile_description->setText(profilestr.section(QLatin1Char(';'), 1, 1)); if (profilestr.section(QLatin1Char(';'), 2, 2) == QLatin1String("audio")) { m_configTranscode.profile_audioonly->setChecked(true); } profilestr = profilestr.section(QLatin1Char(';'), 0, 0).simplified(); } m_configTranscode.profile_extension->setText(profilestr.section(QLatin1Char('.'), -1)); m_configTranscode.profile_parameters->setPlainText(profilestr.section(QLatin1Char(' '), 0, -2)); m_configTranscode.profile_box->setEnabled(true); } void KdenliveSettingsDialog::slotShuttleModified() { #ifdef USE_JOGSHUTTLE QStringList actions; actions << QStringLiteral("monitor_pause"); // the Job rest position action. for (KComboBox *button : m_shuttle_buttons) { actions << m_mappable_actions[button->currentText()]; } QString maps = JogShuttleConfig::actionMap(actions); m_shuttleModified = KdenliveSettings::shuttlebuttons() != maps; #endif KConfigDialog::updateButtons(); } void KdenliveSettingsDialog::slotDialogModified() { m_modified = true; KConfigDialog::updateButtons(); } // virtual bool KdenliveSettingsDialog::hasChanged() { if (m_modified || m_shuttleModified) { return true; } return KConfigDialog::hasChanged(); } void KdenliveSettingsDialog::slotUpdatev4lDevice() { QString device = m_configCapture.kcfg_detectedv4ldevices->itemData(m_configCapture.kcfg_detectedv4ldevices->currentIndex()).toString(); if (!device.isEmpty()) { m_configCapture.kcfg_video4vdevice->setText(device); } QString info = m_configCapture.kcfg_detectedv4ldevices->itemData(m_configCapture.kcfg_detectedv4ldevices->currentIndex(), Qt::UserRole + 1).toString(); m_configCapture.kcfg_v4l_format->blockSignals(true); m_configCapture.kcfg_v4l_format->clear(); QString vl4ProfilePath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/profiles/video4linux"); if (QFile::exists(vl4ProfilePath)) { m_configCapture.kcfg_v4l_format->addItem(i18n("Current settings")); } QStringList pixelformats = info.split('>', QString::SkipEmptyParts); QString itemSize; QString pixelFormat; QStringList itemRates; for (int i = 0; i < pixelformats.count(); ++i) { QString format = pixelformats.at(i).section(QLatin1Char(':'), 0, 0); QStringList sizes = pixelformats.at(i).split(':', QString::SkipEmptyParts); pixelFormat = sizes.takeFirst(); for (int j = 0; j < sizes.count(); ++j) { itemSize = sizes.at(j).section(QLatin1Char('='), 0, 0); itemRates = sizes.at(j).section(QLatin1Char('='), 1, 1).split(QLatin1Char(','), QString::SkipEmptyParts); for (int k = 0; k < itemRates.count(); ++k) { m_configCapture.kcfg_v4l_format->addItem( QLatin1Char('[') + format + QStringLiteral("] ") + itemSize + QStringLiteral(" (") + itemRates.at(k) + QLatin1Char(')'), QStringList() << format << itemSize.section('x', 0, 0) << itemSize.section('x', 1, 1) << itemRates.at(k).section(QLatin1Char('/'), 0, 0) << itemRates.at(k).section(QLatin1Char('/'), 1, 1)); } } } m_configCapture.kcfg_v4l_format->blockSignals(false); slotUpdatev4lCaptureProfile(); } void KdenliveSettingsDialog::slotUpdatev4lCaptureProfile() { QStringList info = m_configCapture.kcfg_v4l_format->itemData(m_configCapture.kcfg_v4l_format->currentIndex(), Qt::UserRole).toStringList(); if (info.isEmpty()) { // No auto info, display the current ones loadCurrentV4lProfileInfo(); return; } m_configCapture.p_size->setText(info.at(1) + QLatin1Char('x') + info.at(2)); m_configCapture.p_fps->setText(info.at(3) + QLatin1Char('/') + info.at(4)); m_configCapture.p_aspect->setText(QStringLiteral("1/1")); m_configCapture.p_display->setText(info.at(1) + QLatin1Char('/') + info.at(2)); m_configCapture.p_colorspace->setText(ProfileRepository::getColorspaceDescription(601)); m_configCapture.p_progressive->setText(i18n("Progressive")); QDir dir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/profiles/")); if (!dir.exists() || !dir.exists(QStringLiteral("video4linux"))) { saveCurrentV4lProfile(); } } void KdenliveSettingsDialog::loadCurrentV4lProfileInfo() { QDir dir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/profiles/")); if (!dir.exists()) { dir.mkpath(QStringLiteral(".")); } if (!ProfileRepository::get()->profileExists(dir.absoluteFilePath(QStringLiteral("video4linux")))) { // No default formats found, build one std::unique_ptr prof(new ProfileParam(pCore->getCurrentProfile().get())); prof->m_width = 320; prof->m_height = 200; prof->m_frame_rate_num = 15; prof->m_frame_rate_den = 1; prof->m_display_aspect_num = 4; prof->m_display_aspect_den = 3; prof->m_sample_aspect_num = 1; prof->m_sample_aspect_den = 1; - prof->m_progressive = 1; + prof->m_progressive = true; prof->m_colorspace = 601; ProfileRepository::get()->saveProfile(prof.get(), dir.absoluteFilePath(QStringLiteral("video4linux"))); } auto &prof = ProfileRepository::get()->getProfile(dir.absoluteFilePath(QStringLiteral("video4linux"))); m_configCapture.p_size->setText(QString::number(prof->width()) + QLatin1Char('x') + QString::number(prof->height())); m_configCapture.p_fps->setText(QString::number(prof->frame_rate_num()) + QLatin1Char('/') + QString::number(prof->frame_rate_den())); m_configCapture.p_aspect->setText(QString::number(prof->sample_aspect_num()) + QLatin1Char('/') + QString::number(prof->sample_aspect_den())); m_configCapture.p_display->setText(QString::number(prof->display_aspect_num()) + QLatin1Char('/') + QString::number(prof->display_aspect_den())); m_configCapture.p_colorspace->setText(ProfileRepository::getColorspaceDescription(prof->colorspace())); if (prof->progressive()) { m_configCapture.p_progressive->setText(i18n("Progressive")); } } void KdenliveSettingsDialog::saveCurrentV4lProfile() { std::unique_ptr profile(new ProfileParam(pCore->getCurrentProfile().get())); profile->m_description = QStringLiteral("Video4Linux capture"); profile->m_colorspace = ProfileRepository::getColorspaceFromDescription(m_configCapture.p_colorspace->text()); profile->m_width = m_configCapture.p_size->text().section('x', 0, 0).toInt(); profile->m_height = m_configCapture.p_size->text().section('x', 1, 1).toInt(); profile->m_sample_aspect_num = m_configCapture.p_aspect->text().section(QLatin1Char('/'), 0, 0).toInt(); profile->m_sample_aspect_den = m_configCapture.p_aspect->text().section(QLatin1Char('/'), 1, 1).toInt(); profile->m_display_aspect_num = m_configCapture.p_display->text().section(QLatin1Char('/'), 0, 0).toInt(); profile->m_display_aspect_den = m_configCapture.p_display->text().section(QLatin1Char('/'), 1, 1).toInt(); profile->m_frame_rate_num = m_configCapture.p_fps->text().section(QLatin1Char('/'), 0, 0).toInt(); profile->m_frame_rate_den = m_configCapture.p_fps->text().section(QLatin1Char('/'), 1, 1).toInt(); profile->m_progressive = m_configCapture.p_progressive->text() == i18n("Progressive"); QDir dir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/profiles/")); if (!dir.exists()) { dir.mkpath(QStringLiteral(".")); } ProfileRepository::get()->saveProfile(profile.get(), dir.absoluteFilePath(QStringLiteral("video4linux"))); } void KdenliveSettingsDialog::slotManageEncodingProfile() { - QAction *act = qobject_cast(sender()); + auto *act = qobject_cast(sender()); int type = 0; if (act) { type = act->data().toInt(); } QPointer dia = new EncodingProfilesDialog(type); dia->exec(); delete dia; loadEncodingProfiles(); } void KdenliveSettingsDialog::loadExternalProxyProfiles() { // load proxy profiles KConfig conf(QStringLiteral("externalproxies.rc"), KConfig::CascadeConfig, QStandardPaths::AppDataLocation); KConfigGroup group(&conf, "proxy"); QMap values = group.entryMap(); QMapIterator k(values); QString currentItem = KdenliveSettings::externalProxyProfile(); m_configProxy.kcfg_external_proxy_profile->blockSignals(true); m_configProxy.kcfg_external_proxy_profile->clear(); while (k.hasNext()) { k.next(); if (!k.key().isEmpty()) { if (k.value().contains(QLatin1Char(';'))) { m_configProxy.kcfg_external_proxy_profile->addItem(k.key(), k.value()); } } } if (!currentItem.isEmpty()) { m_configProxy.kcfg_external_proxy_profile->setCurrentIndex(m_configProxy.kcfg_external_proxy_profile->findText(currentItem)); } m_configProxy.kcfg_external_proxy_profile->blockSignals(false); } void KdenliveSettingsDialog::loadEncodingProfiles() { KConfig conf(QStringLiteral("encodingprofiles.rc"), KConfig::CascadeConfig, QStandardPaths::AppDataLocation); // Load v4l profiles m_configCapture.kcfg_v4l_profile->blockSignals(true); QString currentItem = m_configCapture.kcfg_v4l_profile->currentText(); m_configCapture.kcfg_v4l_profile->clear(); KConfigGroup group(&conf, "video4linux"); QMap values = group.entryMap(); QMapIterator i(values); while (i.hasNext()) { i.next(); if (!i.key().isEmpty()) { m_configCapture.kcfg_v4l_profile->addItem(i.key(), i.value()); } } m_configCapture.kcfg_v4l_profile->blockSignals(false); if (!currentItem.isEmpty()) { m_configCapture.kcfg_v4l_profile->setCurrentIndex(m_configCapture.kcfg_v4l_profile->findText(currentItem)); } // Load Screen Grab profiles m_configCapture.kcfg_grab_profile->blockSignals(true); currentItem = m_configCapture.kcfg_grab_profile->currentText(); m_configCapture.kcfg_grab_profile->clear(); KConfigGroup group2(&conf, "screengrab"); values = group2.entryMap(); QMapIterator j(values); while (j.hasNext()) { j.next(); if (!j.key().isEmpty()) { m_configCapture.kcfg_grab_profile->addItem(j.key(), j.value()); } } m_configCapture.kcfg_grab_profile->blockSignals(false); if (!currentItem.isEmpty()) { m_configCapture.kcfg_grab_profile->setCurrentIndex(m_configCapture.kcfg_grab_profile->findText(currentItem)); } // Load Decklink profiles m_configCapture.kcfg_decklink_profile->blockSignals(true); currentItem = m_configCapture.kcfg_decklink_profile->currentText(); m_configCapture.kcfg_decklink_profile->clear(); KConfigGroup group3(&conf, "decklink"); values = group3.entryMap(); QMapIterator k(values); while (k.hasNext()) { k.next(); if (!k.key().isEmpty()) { m_configCapture.kcfg_decklink_profile->addItem(k.key(), k.value()); } } m_configCapture.kcfg_decklink_profile->blockSignals(false); if (!currentItem.isEmpty()) { m_configCapture.kcfg_decklink_profile->setCurrentIndex(m_configCapture.kcfg_decklink_profile->findText(currentItem)); } // Load Timeline Preview profiles m_configProject.kcfg_preview_profile->blockSignals(true); currentItem = m_configProject.kcfg_preview_profile->currentText(); m_configProject.kcfg_preview_profile->clear(); KConfigGroup group5(&conf, "timelinepreview"); values = group5.entryMap(); m_configProject.kcfg_preview_profile->addItem(i18n("Automatic")); QMapIterator l(values); while (l.hasNext()) { l.next(); if (!l.key().isEmpty()) { m_configProject.kcfg_preview_profile->addItem(l.key(), l.value()); } } if (!currentItem.isEmpty()) { m_configProject.kcfg_preview_profile->setCurrentIndex(m_configProject.kcfg_preview_profile->findText(currentItem)); } m_configProject.kcfg_preview_profile->blockSignals(false); QString profilestr = m_configProject.kcfg_preview_profile->itemData(m_configProject.kcfg_preview_profile->currentIndex()).toString(); if (profilestr.isEmpty()) { m_configProject.previewparams->clear(); } else { m_configProject.previewparams->setPlainText(profilestr.section(QLatin1Char(';'), 0, 0)); } // Load Proxy profiles m_configProxy.kcfg_proxy_profile->blockSignals(true); currentItem = m_configProxy.kcfg_proxy_profile->currentText(); m_configProxy.kcfg_proxy_profile->clear(); KConfigGroup group4(&conf, "proxy"); values = group4.entryMap(); m_configProxy.kcfg_proxy_profile->addItem(i18n("Automatic")); QMapIterator m(values); while (m.hasNext()) { m.next(); if (!m.key().isEmpty()) { m_configProxy.kcfg_proxy_profile->addItem(m.key(), m.value()); } } if (!currentItem.isEmpty()) { m_configProxy.kcfg_proxy_profile->setCurrentIndex(m_configProxy.kcfg_proxy_profile->findText(currentItem)); } m_configProxy.kcfg_proxy_profile->blockSignals(false); profilestr = m_configProxy.kcfg_proxy_profile->itemData(m_configProxy.kcfg_proxy_profile->currentIndex()).toString(); if (profilestr.isEmpty()) { m_configProxy.proxyparams->clear(); } else { m_configProxy.proxyparams->setPlainText(profilestr.section(QLatin1Char(';'), 0, 0)); } } void KdenliveSettingsDialog::slotUpdateDecklinkProfile(int ix) { if (ix == -1) { ix = KdenliveSettings::decklink_profile(); } else { ix = m_configCapture.kcfg_decklink_profile->currentIndex(); } QString profilestr = m_configCapture.kcfg_decklink_profile->itemData(ix).toString(); if (profilestr.isEmpty()) { return; } m_configCapture.decklink_parameters->setPlainText(profilestr.section(QLatin1Char(';'), 0, 0)); // } void KdenliveSettingsDialog::slotUpdateV4lProfile(int ix) { if (ix == -1) { ix = KdenliveSettings::v4l_profile(); } else { ix = m_configCapture.kcfg_v4l_profile->currentIndex(); } QString profilestr = m_configCapture.kcfg_v4l_profile->itemData(ix).toString(); if (profilestr.isEmpty()) { return; } m_configCapture.v4l_parameters->setPlainText(profilestr.section(QLatin1Char(';'), 0, 0)); // } void KdenliveSettingsDialog::slotUpdateGrabProfile(int ix) { if (ix == -1) { ix = KdenliveSettings::grab_profile(); } else { ix = m_configCapture.kcfg_grab_profile->currentIndex(); } QString profilestr = m_configCapture.kcfg_grab_profile->itemData(ix).toString(); if (profilestr.isEmpty()) { return; } m_configCapture.grab_parameters->setPlainText(profilestr.section(QLatin1Char(';'), 0, 0)); // } void KdenliveSettingsDialog::slotUpdateProxyProfile(int ix) { if (ix == -1) { ix = KdenliveSettings::proxy_profile(); } else { ix = m_configProxy.kcfg_proxy_profile->currentIndex(); } QString profilestr = m_configProxy.kcfg_proxy_profile->itemData(ix).toString(); if (profilestr.isEmpty()) { return; } m_configProxy.proxyparams->setPlainText(profilestr.section(QLatin1Char(';'), 0, 0)); } void KdenliveSettingsDialog::slotUpdatePreviewProfile(int ix) { if (ix == -1) { ix = KdenliveSettings::preview_profile(); } else { ix = m_configProject.kcfg_preview_profile->currentIndex(); } QString profilestr = m_configProject.kcfg_preview_profile->itemData(ix).toString(); if (profilestr.isEmpty()) { return; } m_configProject.previewparams->setPlainText(profilestr.section(QLatin1Char(';'), 0, 0)); } void KdenliveSettingsDialog::slotEditVideo4LinuxProfile() { QString vl4ProfilePath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/profiles/video4linux"); QPointer w = new ProfilesDialog(vl4ProfilePath, true); if (w->exec() == QDialog::Accepted) { // save and update profile loadCurrentV4lProfileInfo(); } delete w; } void KdenliveSettingsDialog::slotReloadBlackMagic() { getBlackMagicDeviceList(m_configCapture.kcfg_decklink_capturedevice, true); if (!getBlackMagicOutputDeviceList(m_configSdl.kcfg_blackmagic_output_device, true)) { // No blackmagic card found m_configSdl.kcfg_external_display->setEnabled(false); } m_configSdl.kcfg_external_display->setEnabled(KdenliveSettings::decklink_device_found()); } void KdenliveSettingsDialog::checkProfile() { m_pw->loadProfile(KdenliveSettings::default_profile().isEmpty() ? pCore->getCurrentProfile()->path() : KdenliveSettings::default_profile()); } void KdenliveSettingsDialog::slotReloadShuttleDevices() { #ifdef USE_JOGSHUTTLE QString devDirStr = QStringLiteral("/dev/input/by-id"); QDir devDir(devDirStr); if (!devDir.exists()) { devDirStr = QStringLiteral("/dev/input"); } QStringList devNamesList; QStringList devPathList; m_configShuttle.shuttledevicelist->clear(); DeviceMap devMap = JogShuttle::enumerateDevices(devDirStr); DeviceMapIter iter = devMap.begin(); while (iter != devMap.end()) { m_configShuttle.shuttledevicelist->addItem(iter.key(), iter.value()); devNamesList << iter.key(); devPathList << iter.value(); ++iter; } KdenliveSettings::setShuttledevicenames(devNamesList); KdenliveSettings::setShuttledevicepaths(devPathList); QTimer::singleShot(200, this, SLOT(slotUpdateShuttleDevice())); #endif // USE_JOGSHUTTLE } diff --git a/src/dialogs/kdenlivesettingsdialog.h b/src/dialogs/kdenlivesettingsdialog.h index eb1273bd5..3a06fc09b 100644 --- a/src/dialogs/kdenlivesettingsdialog.h +++ b/src/dialogs/kdenlivesettingsdialog.h @@ -1,140 +1,140 @@ /*************************************************************************** * 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 KDENLIVESETTINGSDIALOG_H #define KDENLIVESETTINGSDIALOG_H #include #include #include #include "ui_configcapture_ui.h" #include "ui_configenv_ui.h" #include "ui_configjogshuttle_ui.h" #include "ui_configmisc_ui.h" #include "ui_configproject_ui.h" #include "ui_configproxy_ui.h" #include "ui_configsdl_ui.h" #include "ui_configtimeline_ui.h" #include "ui_configtranscode_ui.h" class ProfileWidget; class KdenliveSettingsDialog : public KConfigDialog { Q_OBJECT public: - KdenliveSettingsDialog(const QMap &mappable_actions, bool gpuAllowed, QWidget *parent = nullptr); - ~KdenliveSettingsDialog(); + KdenliveSettingsDialog(QMap mappable_actions, bool gpuAllowed, QWidget *parent = nullptr); + ~KdenliveSettingsDialog() override; void showPage(int page, int option); void checkProfile(); protected slots: void updateSettings() override; void updateWidgets() override; bool hasChanged() override; void accept() override; private slots: void slotCheckShuttle(int state = 0); void slotUpdateShuttleDevice(int ix = 0); void slotEditImageApplication(); void slotEditAudioApplication(); void slotReadAudioDevices(); void slotUpdateGrabRegionStatus(); void slotCheckAlsaDriver(); void slotCheckAudioBackend(); void slotAddTranscode(); void slotDeleteTranscode(); /** @brief Update current transcoding profile. */ void slotUpdateTranscodingProfile(); /** @brief Enable / disable the update profile button. */ void slotEnableTranscodeUpdate(); /** @brief Update display of current transcoding profile parameters. */ void slotSetTranscodeProfile(); void slotShuttleModified(); void slotDialogModified(); void slotEnableCaptureFolder(); void slotEnableLibraryFolder(); void slotUpdatev4lDevice(); void slotUpdatev4lCaptureProfile(); void slotManageEncodingProfile(); void slotUpdateDecklinkProfile(int ix = 0); void slotUpdateProxyProfile(int ix = 0); void slotUpdatePreviewProfile(int ix = 0); void slotUpdateV4lProfile(int ix = 0); void slotUpdateGrabProfile(int ix = 0); void slotEditVideo4LinuxProfile(); void slotReloadBlackMagic(); void slotReloadShuttleDevices(); void loadExternalProxyProfiles(); private: KPageWidgetItem *m_page1; KPageWidgetItem *m_page2; KPageWidgetItem *m_page3; KPageWidgetItem *m_page4; KPageWidgetItem *m_page5; KPageWidgetItem *m_page6; KPageWidgetItem *m_page7; KPageWidgetItem *m_page8; Ui::ConfigEnv_UI m_configEnv; Ui::ConfigMisc_UI m_configMisc; Ui::ConfigTimeline_UI m_configTimeline; Ui::ConfigCapture_UI m_configCapture; Ui::ConfigJogShuttle_UI m_configShuttle; Ui::ConfigSdl_UI m_configSdl; Ui::ConfigTranscode_UI m_configTranscode; Ui::ConfigProject_UI m_configProject; Ui::ConfigProxy_UI m_configProxy; ProfileWidget *m_pw; KProcess m_readProcess; bool m_modified; bool m_shuttleModified; QMap m_mappable_actions; QVector m_shuttle_buttons; void initDevices(); void loadTranscodeProfiles(); void saveTranscodeProfiles(); void loadCurrentV4lProfileInfo(); void saveCurrentV4lProfile(); void loadEncodingProfiles(); void setupJogshuttleBtns(const QString &device); /** @brief Fill a combobox with the found blackmagic devices */ static bool getBlackMagicDeviceList(KComboBox *devicelist, bool force = false); static bool getBlackMagicOutputDeviceList(KComboBox *devicelist, bool force = false); signals: void customChanged(); void doResetProfile(); void doResetConsumer(bool fullReset); void updateCaptureFolder(); void updateLibraryFolder(); // Screengrab method changed between fullsceen and region, update rec monitor void updateFullScreenGrab(); /** @brief A settings changed that requires a Kdenlive restart, trigger it */ void restartKdenlive(); void checkTabPosition(); /** @brief Switch between merged / separate channels for audio thumbs */ void audioThumbFormatChanged(); /** @brief An important timeline property changed, prepare for a reset */ void resetView(); }; #endif diff --git a/src/dialogs/markerdialog.h b/src/dialogs/markerdialog.h index 852099515..412718bb4 100644 --- a/src/dialogs/markerdialog.h +++ b/src/dialogs/markerdialog.h @@ -1,64 +1,64 @@ /*************************************************************************** * 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 MARKERDIALOG_H #define MARKERDIALOG_H #include "ui_markerdialog_ui.h" #include "definitions.h" #include "timecode.h" #include "timecodedisplay.h" class ClipController; namespace Mlt { } /** * @class MarkerDialog * @brief A dialog for editing markers and guides. * @author Jean-Baptiste Mardelle */ class MarkerDialog : public QDialog, public Ui::MarkerDialog_UI { Q_OBJECT public: explicit MarkerDialog(ClipController *clip, const CommentedTime &t, const Timecode &tc, const QString &caption, QWidget *parent = nullptr); - ~MarkerDialog(); + ~MarkerDialog() override; CommentedTime newMarker(); QImage markerImage() const; private slots: void slotUpdateThumb(); private: ClipController *m_clip; TimecodeDisplay *m_in; double m_dar; QTimer *m_previewTimer; signals: void updateThumb(); }; #endif diff --git a/src/dialogs/profilesdialog.cpp b/src/dialogs/profilesdialog.cpp index 6d31678d7..56eecfde7 100644 --- a/src/dialogs/profilesdialog.cpp +++ b/src/dialogs/profilesdialog.cpp @@ -1,374 +1,372 @@ /*************************************************************************** * 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 "profilesdialog.h" #include "core.h" #include "kdenlivesettings.h" #include "profiles/profilemodel.hpp" #include "profiles/profilerepository.hpp" #include "klocalizedstring.h" #include #include #include "kdenlive_debug.h" #include #include #include ProfilesDialog::ProfilesDialog(const QString &profileDescription, QWidget *parent) : QDialog(parent) - , m_profileIsModified(false) - , m_isCustomProfile(false) - , m_profilesChanged(false) + { // ask profile repository for a refresh ProfileRepository::get()->refresh(); m_view.setupUi(this); // Add message widget - QGridLayout *lay = (QGridLayout *)layout(); + auto *lay = (QGridLayout *)layout(); m_infoMessage = new KMessageWidget; lay->addWidget(m_infoMessage, 2, 0, 1, -1); m_infoMessage->setCloseButtonVisible(true); m_infoMessage->hide(); // Fill colorspace list (see mlt_profile.h) m_view.colorspace->addItem(ProfileRepository::getColorspaceDescription(601), 601); m_view.colorspace->addItem(ProfileRepository::getColorspaceDescription(709), 709); m_view.colorspace->addItem(ProfileRepository::getColorspaceDescription(240), 240); m_view.colorspace->addItem(ProfileRepository::getColorspaceDescription(0), 0); QStringList profilesFilter; profilesFilter << QStringLiteral("*"); m_view.button_delete->setIcon(QIcon::fromTheme(QStringLiteral("trash-empty"))); m_view.button_delete->setToolTip(i18n("Delete profile")); m_view.button_save->setIcon(QIcon::fromTheme(QStringLiteral("document-save"))); m_view.button_save->setToolTip(i18n("Save profile")); m_view.button_create->setIcon(QIcon::fromTheme(QStringLiteral("document-new"))); m_view.button_create->setToolTip(i18n("Create new profile")); fillList(profileDescription); slotUpdateDisplay(); connectDialog(); } void ProfilesDialog::connectDialog() { connect(m_view.profiles_list, static_cast(&QComboBox::currentIndexChanged), [&]() { slotUpdateDisplay(); }); connect(m_view.button_create, &QAbstractButton::clicked, this, &ProfilesDialog::slotCreateProfile); connect(m_view.button_save, &QAbstractButton::clicked, this, &ProfilesDialog::slotSaveProfile); connect(m_view.button_delete, &QAbstractButton::clicked, this, &ProfilesDialog::slotDeleteProfile); connect(m_view.button_default, &QAbstractButton::clicked, this, &ProfilesDialog::slotSetDefaultProfile); connect(m_view.description, &QLineEdit::textChanged, this, &ProfilesDialog::slotProfileEdited); connect(m_view.frame_num, static_cast(&QSpinBox::valueChanged), this, &ProfilesDialog::slotProfileEdited); connect(m_view.frame_den, static_cast(&QSpinBox::valueChanged), this, &ProfilesDialog::slotProfileEdited); connect(m_view.aspect_num, static_cast(&QSpinBox::valueChanged), this, &ProfilesDialog::slotProfileEdited); connect(m_view.aspect_den, static_cast(&QSpinBox::valueChanged), this, &ProfilesDialog::slotProfileEdited); connect(m_view.display_num, static_cast(&QSpinBox::valueChanged), this, &ProfilesDialog::slotProfileEdited); connect(m_view.display_den, static_cast(&QSpinBox::valueChanged), this, &ProfilesDialog::slotProfileEdited); connect(m_view.progressive, &QCheckBox::stateChanged, this, &ProfilesDialog::slotProfileEdited); connect(m_view.size_h, static_cast(&QSpinBox::valueChanged), this, &ProfilesDialog::slotProfileEdited); connect(m_view.size_h, &QAbstractSpinBox::editingFinished, this, &ProfilesDialog::slotAdjustHeight); m_view.size_h->setSingleStep(2); connect(m_view.size_w, static_cast(&QSpinBox::valueChanged), this, &ProfilesDialog::slotProfileEdited); connect(m_view.size_w, &QAbstractSpinBox::editingFinished, this, &ProfilesDialog::slotAdjustWidth); m_view.size_w->setSingleStep(8); } ProfilesDialog::ProfilesDialog(const QString &profilePath, bool, QWidget *parent) : QDialog(parent) , m_profileIsModified(false) , m_isCustomProfile(true) , m_customProfilePath(profilePath) , m_profilesChanged(false) { m_view.setupUi(this); // Add message widget - QGridLayout *lay = (QGridLayout *)layout(); + auto *lay = (QGridLayout *)layout(); m_infoMessage = new KMessageWidget; lay->addWidget(m_infoMessage, 2, 0, 1, -1); m_infoMessage->setCloseButtonVisible(true); m_infoMessage->hide(); // Fill colorspace list (see mlt_profile.h) m_view.colorspace->addItem(ProfileRepository::getColorspaceDescription(601), 601); m_view.colorspace->addItem(ProfileRepository::getColorspaceDescription(709), 709); m_view.colorspace->addItem(ProfileRepository::getColorspaceDescription(240), 240); m_view.colorspace->addItem(ProfileRepository::getColorspaceDescription(0), 0); QStringList profilesFilter; profilesFilter << QStringLiteral("*"); m_view.button_save->setIcon(QIcon::fromTheme(QStringLiteral("document-save"))); m_view.button_save->setToolTip(i18n("Save profile")); m_view.button_create->setHidden(true); m_view.profiles_list->setHidden(true); m_view.button_delete->setHidden(true); m_view.button_default->setHidden(true); m_view.description->setEnabled(false); slotUpdateDisplay(profilePath); connectDialog(); } void ProfilesDialog::slotAdjustWidth() { // A profile's width should always be a multiple of 8 QSignalBlocker blk(m_view.size_w); int val = m_view.size_w->value(); int correctedWidth = (val + 7) / 8 * 8; if (val == correctedWidth) { // Ok, no action required, width is a multiple of 8 m_infoMessage->animatedHide(); } else { m_view.size_w->setValue(correctedWidth); m_infoMessage->setText(i18n("Profile width must be a multiple of 8. It was adjusted to %1", correctedWidth)); m_infoMessage->setWordWrap(m_infoMessage->text().length() > 35); m_infoMessage->setMessageType(KMessageWidget::Warning); m_infoMessage->animatedShow(); } } void ProfilesDialog::slotAdjustHeight() { // A profile's height should always be a multiple of 2 QSignalBlocker blk(m_view.size_h); int val = m_view.size_h->value(); int correctedHeight = val + (val % 2); if (val == correctedHeight) { // Ok, no action required, width is a multiple of 8 m_infoMessage->animatedHide(); } else { m_view.size_h->setValue(correctedHeight); m_infoMessage->setText(i18n("Profile height must be a multiple of 2. It was adjusted to %1", correctedHeight)); m_infoMessage->setWordWrap(m_infoMessage->text().length() > 35); m_infoMessage->setMessageType(KMessageWidget::Warning); m_infoMessage->animatedShow(); } } void ProfilesDialog::slotProfileEdited() { m_profileIsModified = true; } void ProfilesDialog::fillList(const QString &selectedProfile) { m_view.profiles_list->clear(); // Retrieve the list from the repository QVector> profiles = ProfileRepository::get()->getAllProfiles(); for (const auto &p : profiles) { m_view.profiles_list->addItem(p.first, p.second); } if (!KdenliveSettings::default_profile().isEmpty()) { int ix = m_view.profiles_list->findData(KdenliveSettings::default_profile()); if (ix > -1) { m_view.profiles_list->setCurrentIndex(ix); } else { // Error, profile not found qCWarning(KDENLIVE_LOG) << "Project profile not found, disable editing"; } } int ix = m_view.profiles_list->findText(selectedProfile); if (ix != -1) { m_view.profiles_list->setCurrentIndex(ix); } m_selectedProfileIndex = m_view.profiles_list->currentIndex(); } void ProfilesDialog::accept() { if (askForSave()) { QDialog::accept(); } } void ProfilesDialog::reject() { if (askForSave()) { QDialog::reject(); } } void ProfilesDialog::closeEvent(QCloseEvent *event) { if (askForSave()) { event->accept(); } else { event->ignore(); } } bool ProfilesDialog::askForSave() { if (!m_profileIsModified) { return true; } if (KMessageBox::questionYesNo(this, i18n("The custom profile was modified, do you want to save it?")) != KMessageBox::Yes) { return true; } return slotSaveProfile(); } void ProfilesDialog::slotCreateProfile() { m_view.button_delete->setEnabled(false); m_view.button_create->setEnabled(false); m_view.button_save->setEnabled(true); m_view.properties->setEnabled(true); } void ProfilesDialog::slotSetDefaultProfile() { if (m_profileIsModified) { m_infoMessage->setText(i18n("Save your profile before setting it to default")); m_infoMessage->setWordWrap(m_infoMessage->text().length() > 35); m_infoMessage->setMessageType(KMessageWidget::Warning); m_infoMessage->animatedShow(); return; } int ix = m_view.profiles_list->currentIndex(); QString path = m_view.profiles_list->itemData(ix).toString(); if (!path.isEmpty()) { KdenliveSettings::setDefault_profile(path); } } bool ProfilesDialog::slotSaveProfile() { slotAdjustWidth(); if (!m_customProfilePath.isEmpty()) { saveProfile(m_customProfilePath); return true; } const QString profileDesc = m_view.description->text(); int ix = m_view.profiles_list->findText(profileDesc); if (ix != -1) { // this profile name already exists const QString path = m_view.profiles_list->itemData(ix).toString(); if (!path.contains(QLatin1Char('/'))) { KMessageBox::sorry( this, i18n("A profile with same name already exists in MLT's default profiles, please choose another description for your custom profile.")); return false; } saveProfile(path); } else { int i = 0; QDir dir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/profiles/")); if (!dir.exists()) { dir.mkpath(QStringLiteral(".")); } QString customName = QStringLiteral("customprofile"); QString profilePath = dir.absoluteFilePath(customName + QString::number(i)); while (QFile::exists(profilePath)) { ++i; profilePath = dir.absoluteFilePath(customName + QString::number(i)); } saveProfile(profilePath); } m_profileIsModified = false; fillList(profileDesc); m_view.button_create->setEnabled(true); m_profilesChanged = true; return true; } void ProfilesDialog::saveProfile(const QString &path) { std::unique_ptr profile(new ProfileParam(pCore->getCurrentProfile().get())); profile->m_description = m_view.description->text(); profile->m_frame_rate_num = m_view.frame_num->value(); profile->m_frame_rate_den = m_view.frame_den->value(); profile->m_width = m_view.size_w->value(); profile->m_height = m_view.size_h->value(); profile->m_progressive = static_cast(m_view.progressive->isChecked()); profile->m_sample_aspect_num = m_view.aspect_num->value(); profile->m_sample_aspect_den = m_view.aspect_den->value(); profile->m_display_aspect_num = m_view.display_num->value(); profile->m_display_aspect_den = m_view.display_den->value(); profile->m_colorspace = m_view.colorspace->itemData(m_view.colorspace->currentIndex()).toInt(); ProfileRepository::get()->saveProfile(profile.get(), path); } void ProfilesDialog::slotDeleteProfile() { const QString path = m_view.profiles_list->itemData(m_view.profiles_list->currentIndex()).toString(); bool success = ProfileRepository::get()->deleteProfile(path); if (success) { m_profilesChanged = true; fillList(); } } void ProfilesDialog::slotUpdateDisplay(QString currentProfilePath) { qDebug() << "/ / / /UPDATING DISPLAY FOR PROFILE: " << currentProfilePath; if (!askForSave()) { m_view.profiles_list->blockSignals(true); m_view.profiles_list->setCurrentIndex(m_selectedProfileIndex); m_view.profiles_list->blockSignals(false); return; } QLocale locale; locale.setNumberOptions(QLocale::OmitGroupSeparator); m_selectedProfileIndex = m_view.profiles_list->currentIndex(); if (currentProfilePath.isEmpty()) { currentProfilePath = m_view.profiles_list->itemData(m_view.profiles_list->currentIndex()).toString(); } m_isCustomProfile = currentProfilePath.contains(QLatin1Char('/')); m_view.button_create->setEnabled(true); m_view.button_delete->setEnabled(m_isCustomProfile); m_view.properties->setEnabled(m_isCustomProfile); m_view.button_save->setEnabled(m_isCustomProfile); std::unique_ptr &curProfile = ProfileRepository::get()->getProfile(currentProfilePath); m_view.description->setText(curProfile->description()); m_view.size_w->setValue(curProfile->width()); m_view.size_h->setValue(curProfile->height()); m_view.aspect_num->setValue(curProfile->sample_aspect_num()); m_view.aspect_den->setValue(curProfile->sample_aspect_den()); m_view.display_num->setValue(curProfile->display_aspect_num()); m_view.display_den->setValue(curProfile->display_aspect_den()); m_view.frame_num->setValue(curProfile->frame_rate_num()); m_view.frame_den->setValue(curProfile->frame_rate_den()); m_view.progressive->setChecked(curProfile->progressive() != 0); if (curProfile->progressive() != 0) { m_view.fields->setText(locale.toString((double)curProfile->frame_rate_num() / curProfile->frame_rate_den(), 'f', 2)); } else { m_view.fields->setText(locale.toString((double)2 * curProfile->frame_rate_num() / curProfile->frame_rate_den(), 'f', 2)); } int colorix = m_view.colorspace->findData(curProfile->colorspace()); if (colorix > -1) { m_view.colorspace->setCurrentIndex(colorix); } m_profileIsModified = false; } bool ProfilesDialog::profileTreeChanged() const { return m_profilesChanged; } diff --git a/src/dialogs/profilesdialog.h b/src/dialogs/profilesdialog.h index 4ed9e82a8..4b9727830 100644 --- a/src/dialogs/profilesdialog.h +++ b/src/dialogs/profilesdialog.h @@ -1,72 +1,72 @@ /*************************************************************************** * 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 PROFILESDIALOG_H #define PROFILESDIALOG_H #include "definitions.h" #include "ui_profiledialog_ui.h" #include class KMessageWidget; class ProfilesDialog : public QDialog { Q_OBJECT public: explicit ProfilesDialog(const QString &profileDescription = QString(), QWidget *parent = nullptr); /** @brief Using this constructor, the dialog only allows editing one profile. */ explicit ProfilesDialog(const QString &profilePath, bool, QWidget *parent = nullptr); void fillList(const QString &selectedProfile = QString()); bool profileTreeChanged() const; protected: void closeEvent(QCloseEvent *event) override; private slots: void slotUpdateDisplay(QString currentProfilePath = QString()); void slotCreateProfile(); bool slotSaveProfile(); void slotDeleteProfile(); void slotSetDefaultProfile(); void slotProfileEdited(); /** @brief Make sure the profile's width is always a multiple of 8 */ void slotAdjustWidth(); /** @brief Make sure the profile's height is always a multiple of 2 */ void slotAdjustHeight(); void accept() override; void reject() override; private: Ui::ProfilesDialog_UI m_view; int m_selectedProfileIndex; - bool m_profileIsModified; - bool m_isCustomProfile; + bool m_profileIsModified{false}; + bool m_isCustomProfile{false}; /** @brief If we are in single profile editing, should contain the path for this profile. */ QString m_customProfilePath; /** @brief True if a profile was saved / deleted and profile tree requires a reload. */ - bool m_profilesChanged; + bool m_profilesChanged{false}; KMessageWidget *m_infoMessage; void saveProfile(const QString &path); bool askForSave(); void connectDialog(); }; #endif diff --git a/src/dialogs/renderwidget.cpp b/src/dialogs/renderwidget.cpp index 538feb6da..8a3dc1615 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); + return {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++) { 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++) { 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)); + auto *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)); + auto *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()); + auto *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()); + auto *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()); + auto *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)); + auto *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()); + auto *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)); + auto *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); + auto *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/dialogs/renderwidget.h b/src/dialogs/renderwidget.h index a25fda94e..61c32f53a 100644 --- a/src/dialogs/renderwidget.h +++ b/src/dialogs/renderwidget.h @@ -1,245 +1,245 @@ /*************************************************************************** * 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 RENDERWIDGET_H #define RENDERWIDGET_H #include #include #include #include #ifdef KF5_USE_PURPOSE namespace Purpose { class Menu; } #endif #include "definitions.h" #include "ui_renderwidget_ui.h" class QDomElement; class QKeyEvent; // RenderViewDelegate is used to draw the progress bars. class RenderViewDelegate : public QStyledItemDelegate { Q_OBJECT public: explicit RenderViewDelegate(QWidget *parent) : QStyledItemDelegate(parent) { } void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override { if (index.column() == 1) { painter->save(); QStyleOptionViewItem opt(option); QStyle *style = opt.widget ? opt.widget->style() : QApplication::style(); const int textMargin = style->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1; style->drawPrimitive(QStyle::PE_PanelItemViewItem, &opt, painter, opt.widget); QFont font = painter->font(); font.setBold(true); painter->setFont(font); QRect r1 = option.rect; r1.adjust(0, textMargin, 0, -textMargin); int mid = (int)((r1.height() / 2)); r1.setBottom(r1.y() + mid); QRect bounding; painter->drawText(r1, Qt::AlignLeft | Qt::AlignTop, index.data().toString(), &bounding); r1.moveTop(r1.bottom() - textMargin); font.setBold(false); painter->setFont(font); painter->drawText(r1, Qt::AlignLeft | Qt::AlignTop, index.data(Qt::UserRole).toString()); int progress = index.data(Qt::UserRole + 3).toInt(); if (progress > 0 && progress < 100) { // draw progress bar QColor color = option.palette.alternateBase().color(); QColor fgColor = option.palette.text().color(); color.setAlpha(150); fgColor.setAlpha(150); painter->setBrush(QBrush(color)); painter->setPen(QPen(fgColor)); int width = qMin(200, r1.width() - 4); QRect bgrect(r1.left() + 2, option.rect.bottom() - 6 - textMargin, width, 6); painter->drawRoundedRect(bgrect, 3, 3); painter->setBrush(QBrush(fgColor)); bgrect.adjust(2, 2, 0, -1); painter->setPen(Qt::NoPen); bgrect.setWidth((width - 2) * progress / 100); painter->drawRect(bgrect); } else { r1.setBottom(opt.rect.bottom()); r1.setTop(r1.bottom() - mid); painter->drawText(r1, Qt::AlignLeft | Qt::AlignBottom, index.data(Qt::UserRole + 5).toString()); } painter->restore(); } else { QStyledItemDelegate::paint(painter, option, index); } } }; class RenderJobItem : public QTreeWidgetItem { public: explicit RenderJobItem(QTreeWidget *parent, const QStringList &strings, int type = QTreeWidgetItem::Type); void setStatus(int status); int status() const; void setMetadata(const QString &data); const QString metadata() const; void render(); private: int m_status; QString m_data; }; class RenderWidget : public QDialog { Q_OBJECT public: explicit RenderWidget(bool enableProxy, QWidget *parent = nullptr); - virtual ~RenderWidget(); + ~RenderWidget() override; void setGuides(const QList &guidesList, double duration); void focusFirstVisibleItem(const QString &profile = QString()); void setRenderJob(const QString &dest, int progress = 0); void setRenderStatus(const QString &dest, int status, const QString &error); void updateDocumentPath(); void reloadProfiles(); void setRenderProfile(const QMap &props); int waitingJobsCount() const; QString getFreeScriptName(const QUrl &projectName = QUrl(), const QString &prefix = QString()); bool startWaitingRenderJobs(); /** @brief Returns true if the export audio checkbox is set to automatic. */ bool automaticAudioExport() const; /** @brief Returns true if user wants audio export. */ bool selectedAudioExport() const; /** @brief Show / hide proxy settings. */ void updateProxyConfig(bool enable); /** @brief Should we render using proxy clips. */ bool proxyRendering(); /** @brief Returns true if the stem audio export checkbox is set. */ bool isStemAudioExportEnabled() const; enum RenderError { CompositeError = 0, ProfileError = 1, ProxyWarning = 2, PlaybackError = 3 }; /** @brief Display warning message in render widget. */ void errorMessage(RenderError type, const QString &message); protected: QSize sizeHint() const override; void keyPressEvent(QKeyEvent *e) override; public slots: Q_DECL_DEPRECATED void slotExport(bool scriptExport, int zoneIn, int zoneOut, const QMap &metadata, const QList &playlistPaths, const QList &trackNames, const QString &scriptPath, bool exportAudio); void slotAbortCurrentJob(); void slotPrepareExport(bool scriptExport = false, const QString &scriptPath = QString()); void adjustViewToProfile(); private slots: void slotUpdateButtons(const QUrl &url); void slotUpdateButtons(); void refreshView(); /** @brief Updates available options when a new format has been selected. */ void refreshParams(); void slotSaveProfile(); void slotEditProfile(); void slotDeleteProfile(bool dontRefresh = false); void slotUpdateGuideBox(); void slotCheckStartGuidePosition(); void slotCheckEndGuidePosition(); void showInfoPanel(); void slotStartScript(); void slotDeleteScript(); void slotGenerateScript(); void parseScriptFiles(); void slotCheckScript(); void slotCheckJob(); void slotEditItem(QTreeWidgetItem *item); void slotCLeanUpJobs(); void slotHideLog(); void slotPlayRendering(QTreeWidgetItem *item, int); void slotStartCurrentJob(); void slotCopyToFavorites(); void slotDownloadNewRenderProfiles(); void slotUpdateEncodeThreads(int); void slotUpdateRescaleHeight(int); void slotUpdateRescaleWidth(int); void slotSwitchAspectRatio(); /** @brief Update export audio label depending on current settings. */ void slotUpdateAudioLabel(int ix); /** @brief Enable / disable the rescale options. */ void setRescaleEnabled(bool enable); /** @brief Adjust video/audio quality spinboxes from quality slider. */ void adjustAVQualities(int quality); /** @brief Adjust quality slider from video spinbox. */ void adjustQuality(int videoQuality); /** @brief Show updated command parameter in tooltip. */ void adjustSpeed(int videoQuality); /** @brief Display warning on proxy rendering. */ void slotProxyWarn(bool enableProxy); /** @brief User shared a rendered file, give feedback. */ void slotShareActionFinished(const QJsonObject &output, int error, const QString &message); private: Ui::RenderWidget_UI m_view; QString m_projectFolder; RenderViewDelegate *m_scriptsDelegate; RenderViewDelegate *m_jobsDelegate; bool m_blockProcessing; QString m_renderer; KMessageWidget *m_infoMessage; KMessageWidget *m_jobInfoMessage; QMap m_errorMessages; #ifdef KF5_USE_PURPOSE Purpose::Menu *m_shareMenu; #endif void parseMltPresets(); void parseProfiles(const QString &selectedProfile = QString()); void parseFile(const QString &exportFile, bool editable); void updateButtons(); QUrl filenameWithExtension(QUrl url, const QString &extension); /** @brief Check if a job needs to be started. */ void checkRenderStatus(); void startRendering(RenderJobItem *item); bool saveProfile(QDomElement newprofile); /** @brief Create a rendering profile from MLT preset. */ QTreeWidgetItem *loadFromMltPreset(const QString &groupName, const QString &path, const QString &profileName); void checkCodecs(); int getNewStuff(const QString &configFile); void prepareRendering(bool delayedRendering, const QString &chapterFile); void generateRenderFiles(QDomDocument doc, const QString &playlistPath, int in, int out, bool delayedRendering); signals: void abortProcess(const QString &url); void openDvdWizard(const QString &url); /** Send the info about rendering that will be saved in the document: (profile destination, profile name and url of rendered file */ void selectedRenderProfile(const QMap &renderProps); void shutdown(); }; #endif diff --git a/src/dialogs/wizard.cpp b/src/dialogs/wizard.cpp index da89859a1..7784cbae2 100644 --- a/src/dialogs/wizard.cpp +++ b/src/dialogs/wizard.cpp @@ -1,1026 +1,1026 @@ /*************************************************************************** * 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 "wizard.h" #include "kdenlivesettings.h" #include "profiles/profilemodel.hpp" #include "profiles/profilerepository.hpp" #include "profilesdialog.h" #include "utils/thememanager.h" #ifdef USE_V4L #include "capture/v4lcapture.h" #endif #include "core.h" #include #include #include #include #include #include #include #include "kdenlive_debug.h" #include #include #include #include #include #include #include #include #include #include // Recommended MLT version const int mltVersionMajor = MLT_MIN_MAJOR_VERSION; const int mltVersionMinor = MLT_MIN_MINOR_VERSION; const int mltVersionRevision = MLT_MIN_PATCH_VERSION; static const char kdenlive_version[] = KDENLIVE_VERSION; static QStringList acodecsList; static QStringList vcodecsList; MyWizardPage::MyWizardPage(QWidget *parent) : QWizardPage(parent) - , m_isComplete(false) + { } void MyWizardPage::setComplete(bool complete) { m_isComplete = complete; } bool MyWizardPage::isComplete() const { return m_isComplete; } Wizard::Wizard(bool autoClose, bool appImageCheck, QWidget *parent) : QWizard(parent) , m_systemCheckIsOk(false) , m_brokenModule(false) { setWindowTitle(i18n("Welcome to Kdenlive")); int logoHeight = fontMetrics().height() * 2.5; setWizardStyle(QWizard::ModernStyle); setOption(QWizard::NoBackButtonOnLastPage, true); // setOption(QWizard::ExtendedWatermarkPixmap, false); m_page = new MyWizardPage(this); m_page->setTitle(i18n("Welcome to Kdenlive %1", QString(kdenlive_version))); m_page->setSubTitle(i18n("Using MLT %1", mlt_version_get_string())); setPixmap(QWizard::LogoPixmap, QIcon::fromTheme(QStringLiteral(":/pics/kdenlive.png")).pixmap(logoHeight, logoHeight)); m_startLayout = new QVBoxLayout; m_errorWidget = new KMessageWidget(this); m_startLayout->addWidget(m_errorWidget); m_errorWidget->setCloseButtonVisible(false); m_errorWidget->hide(); m_page->setLayout(m_startLayout); addPage(m_page); /*QWizardPage *page2 = new QWizardPage; page2->setTitle(i18n("Video Standard")); m_standard.setupUi(page2);*/ setButtonText(QWizard::CancelButton, i18n("Abort")); setButtonText(QWizard::FinishButton, i18n("OK")); slotCheckMlt(); if (autoClose) { // This is a first run instance, check HW encoders testHwEncoders(); } if (!m_errors.isEmpty() || !m_warnings.isEmpty() || (!m_infos.isEmpty() && !appImageCheck)) { QLabel *lab = new QLabel(this); lab->setText(i18n("Startup error or warning, check our online manual.")); connect(lab, &QLabel::linkActivated, this, &Wizard::slotOpenManual); m_startLayout->addWidget(lab); } else { // Everything is ok, auto close the wizard m_page->setComplete(true); if (autoClose) { QTimer::singleShot(0, this, &QDialog::accept); return; } auto *lab = new KMessageWidget(this); lab->setText(i18n("Codecs have been updated, everything seems fine.")); lab->setMessageType(KMessageWidget::Positive); lab->setCloseButtonVisible(false); m_startLayout->addWidget(lab); // HW accel QCheckBox *cb = new QCheckBox(i18n("VAAPI hardware acceleration"), this); m_startLayout->addWidget(cb); cb->setChecked(KdenliveSettings::vaapiEnabled()); QCheckBox *cbn = new QCheckBox(i18n("NVIDIA hardware acceleration"), this); m_startLayout->addWidget(cbn); cbn->setChecked(KdenliveSettings::nvencEnabled()); QPushButton *pb = new QPushButton(i18n("Check hardware acceleration"), this); connect(pb, &QPushButton::clicked, [&, cb, cbn, pb]() { testHwEncoders(); pb->setEnabled(false); cb->setChecked(KdenliveSettings::vaapiEnabled()); cbn->setChecked(KdenliveSettings::nvencEnabled()); updateHwStatus(); pb->setEnabled(true); }); m_startLayout->addWidget(pb); setOption(QWizard::NoCancelButton, true); return; } if (!m_errors.isEmpty()) { auto *errorLabel = new KMessageWidget(this); errorLabel->setText(QStringLiteral("
    ") + m_errors + QStringLiteral("
")); errorLabel->setMessageType(KMessageWidget::Error); errorLabel->setWordWrap(true); errorLabel->setCloseButtonVisible(false); m_startLayout->addWidget(errorLabel); m_page->setComplete(false); errorLabel->show(); if (!autoClose) { setButtonText(QWizard::CancelButton, i18n("Close")); } } else { m_page->setComplete(true); if (!autoClose) { setOption(QWizard::NoCancelButton, true); } } if (!m_warnings.isEmpty()) { auto *errorLabel = new KMessageWidget(this); errorLabel->setText(QStringLiteral("
    ") + m_warnings + QStringLiteral("
")); errorLabel->setMessageType(KMessageWidget::Warning); errorLabel->setWordWrap(true); errorLabel->setCloseButtonVisible(false); m_startLayout->addWidget(errorLabel); errorLabel->show(); } if (!m_infos.isEmpty()) { auto *errorLabel = new KMessageWidget(this); errorLabel->setText(QStringLiteral("
    ") + m_infos + QStringLiteral("
")); errorLabel->setMessageType(KMessageWidget::Information); errorLabel->setWordWrap(true); errorLabel->setCloseButtonVisible(false); m_startLayout->addWidget(errorLabel); errorLabel->show(); } // build profiles lists /*QMap profilesInfo = ProfilesDialog::getProfilesInfo(); QMap::const_iterator i = profilesInfo.constBegin(); while (i != profilesInfo.constEnd()) { QMap< QString, QString > profileData = ProfilesDialog::getSettingsFromFile(i.key()); if (profileData.value(QStringLiteral("width")) == QLatin1String("720")) m_dvProfiles.insert(i.value(), i.key()); else if (profileData.value(QStringLiteral("width")).toInt() >= 1080) m_hdvProfiles.insert(i.value(), i.key()); else m_otherProfiles.insert(i.value(), i.key()); ++i; } m_standard.button_all->setChecked(true); connect(m_standard.button_all, SIGNAL(toggled(bool)), this, SLOT(slotCheckStandard())); connect(m_standard.button_hdv, SIGNAL(toggled(bool)), this, SLOT(slotCheckStandard())); connect(m_standard.button_dv, SIGNAL(toggled(bool)), this, SLOT(slotCheckStandard())); slotCheckStandard(); connect(m_standard.profiles_list, SIGNAL(itemSelectionChanged()), this, SLOT(slotCheckSelectedItem())); // select default profile if (!KdenliveSettings::default_profile().isEmpty()) { for (int i = 0; i < m_standard.profiles_list->count(); ++i) { if (m_standard.profiles_list->item(i)->data(Qt::UserRole).toString() == KdenliveSettings::default_profile()) { m_standard.profiles_list->setCurrentRow(i); m_standard.profiles_list->scrollToItem(m_standard.profiles_list->currentItem()); break; } } } setPage(2, page2); QWizardPage *page3 = new QWizardPage; page3->setTitle(i18n("Additional Settings")); m_extra.setupUi(page3); m_extra.projectfolder->setMode(KFile::Directory); m_extra.projectfolder->setUrl(QUrl(KdenliveSettings::defaultprojectfolder())); m_extra.videothumbs->setChecked(KdenliveSettings::videothumbnails()); m_extra.audiothumbs->setChecked(KdenliveSettings::audiothumbnails()); m_extra.autosave->setChecked(KdenliveSettings::crashrecovery()); connect(m_extra.videothumbs, SIGNAL(stateChanged(int)), this, SLOT(slotCheckThumbs())); connect(m_extra.audiothumbs, SIGNAL(stateChanged(int)), this, SLOT(slotCheckThumbs())); slotCheckThumbs(); addPage(page3);*/ #ifndef Q_WS_MAC /*QWizardPage *page6 = new QWizardPage; page6->setTitle(i18n("Capture device")); m_capture.setupUi(page6); bool found_decklink = Render::getBlackMagicDeviceList(m_capture.decklink_devices); KdenliveSettings::setDecklink_device_found(found_decklink); if (found_decklink) m_capture.decklink_status->setText(i18n("Default Blackmagic Decklink card:")); else m_capture.decklink_status->setText(i18n("No Blackmagic Decklink device found")); connect(m_capture.decklink_devices, SIGNAL(currentIndexChanged(int)), this, SLOT(slotUpdateDecklinkDevice(int))); connect(m_capture.button_reload, SIGNAL(clicked()), this, SLOT(slotDetectWebcam())); connect(m_capture.v4l_devices, SIGNAL(currentIndexChanged(int)), this, SLOT(slotUpdateCaptureParameters())); connect(m_capture.v4l_formats, SIGNAL(currentIndexChanged(int)), this, SLOT(slotSaveCaptureFormat())); m_capture.button_reload->setIcon(QIcon::fromTheme(QStringLiteral("view-refresh")));*/ #endif // listViewDelegate = new WizardDelegate(treeWidget); // m_check.programList->setItemDelegate(listViewDelegate); // slotDetectWebcam(); // QTimer::singleShot(500, this, SLOT(slotCheckMlt())); } void Wizard::slotDetectWebcam() { #ifdef USE_V4L m_capture.v4l_devices->blockSignals(true); m_capture.v4l_devices->clear(); // Video 4 Linux device detection for (int i = 0; i < 10; ++i) { QString path = "/dev/video" + QString::number(i); if (QFile::exists(path)) { QStringList deviceInfo = V4lCaptureHandler::getDeviceName(path.toUtf8().constData()); if (!deviceInfo.isEmpty()) { m_capture.v4l_devices->addItem(deviceInfo.at(0), path); m_capture.v4l_devices->setItemData(m_capture.v4l_devices->count() - 1, deviceInfo.at(1), Qt::UserRole + 1); } } } if (m_capture.v4l_devices->count() > 0) { m_capture.v4l_status->setText(i18n("Default video4linux device:")); // select default device bool found = false; for (int i = 0; i < m_capture.v4l_devices->count(); ++i) { QString device = m_capture.v4l_devices->itemData(i).toString(); if (device == KdenliveSettings::video4vdevice()) { m_capture.v4l_devices->setCurrentIndex(i); found = true; break; } } slotUpdateCaptureParameters(); if (!found) { m_capture.v4l_devices->setCurrentIndex(0); } } else { m_capture.v4l_status->setText(i18n("No device found, plug your webcam and refresh.")); } m_capture.v4l_devices->blockSignals(false); #endif /* USE_V4L */ } void Wizard::slotUpdateCaptureParameters() { QString device = m_capture.v4l_devices->itemData(m_capture.v4l_devices->currentIndex()).toString(); if (!device.isEmpty()) { KdenliveSettings::setVideo4vdevice(device); } QString formats = m_capture.v4l_devices->itemData(m_capture.v4l_devices->currentIndex(), Qt::UserRole + 1).toString(); m_capture.v4l_formats->blockSignals(true); m_capture.v4l_formats->clear(); QDir dir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/profiles/")); if (!dir.exists()) { dir.mkpath(QStringLiteral(".")); } if (ProfileRepository::get()->profileExists(dir.absoluteFilePath(QStringLiteral("video4linux")))) { auto &profileInfo = ProfileRepository::get()->getProfile(dir.absoluteFilePath(QStringLiteral("video4linux"))); m_capture.v4l_formats->addItem(i18n("Current settings (%1x%2, %3/%4fps)", profileInfo->width(), profileInfo->height(), profileInfo->frame_rate_num(), profileInfo->frame_rate_den()), QStringList() << QStringLiteral("unknown") << QString::number(profileInfo->width()) << QString::number(profileInfo->height()) << QString::number(profileInfo->frame_rate_num()) << QString::number(profileInfo->frame_rate_den())); } QStringList pixelformats = formats.split('>', QString::SkipEmptyParts); QString itemSize; QString pixelFormat; QStringList itemRates; for (int i = 0; i < pixelformats.count(); ++i) { QString format = pixelformats.at(i).section(QLatin1Char(':'), 0, 0); QStringList sizes = pixelformats.at(i).split(':', QString::SkipEmptyParts); pixelFormat = sizes.takeFirst(); for (int j = 0; j < sizes.count(); ++j) { itemSize = sizes.at(j).section(QLatin1Char('='), 0, 0); itemRates = sizes.at(j).section(QLatin1Char('='), 1, 1).split(QLatin1Char(','), QString::SkipEmptyParts); for (int k = 0; k < itemRates.count(); ++k) { QString formatDescription = QLatin1Char('[') + format + QStringLiteral("] ") + itemSize + QStringLiteral(" (") + itemRates.at(k) + QLatin1Char(')'); if (m_capture.v4l_formats->findText(formatDescription) == -1) { m_capture.v4l_formats->addItem(formatDescription, QStringList() << format << itemSize.section('x', 0, 0) << itemSize.section('x', 1, 1) << itemRates.at(k).section(QLatin1Char('/'), 0, 0) << itemRates.at(k).section(QLatin1Char('/'), 1, 1)); } } } } if (!dir.exists(QStringLiteral("video4linux"))) { if (m_capture.v4l_formats->count() > 9) { slotSaveCaptureFormat(); } else { // No existing profile and no autodetected profiles std::unique_ptr profileInfo(new ProfileParam(pCore->getCurrentProfile().get())); profileInfo->m_width = 320; profileInfo->m_height = 200; profileInfo->m_frame_rate_num = 15; profileInfo->m_frame_rate_den = 1; profileInfo->m_display_aspect_num = 4; profileInfo->m_display_aspect_den = 3; profileInfo->m_sample_aspect_num = 1; profileInfo->m_sample_aspect_den = 1; - profileInfo->m_progressive = 1; + profileInfo->m_progressive = true; profileInfo->m_colorspace = 601; ProfileRepository::get()->saveProfile(profileInfo.get(), dir.absoluteFilePath(QStringLiteral("video4linux"))); m_capture.v4l_formats->addItem(i18n("Default settings (%1x%2, %3/%4fps)", profileInfo->width(), profileInfo->height(), profileInfo->frame_rate_num(), profileInfo->frame_rate_den()), QStringList() << QStringLiteral("unknown") << QString::number(profileInfo->width()) << QString::number(profileInfo->height()) << QString::number(profileInfo->frame_rate_num()) << QString::number(profileInfo->frame_rate_den())); } } m_capture.v4l_formats->blockSignals(false); } void Wizard::checkMltComponents() { m_brokenModule = false; Mlt::Repository *repository = Mlt::Factory::init(); if (!repository) { m_errors.append(i18n("
  • Cannot start MLT backend, check your installation
  • ")); m_systemCheckIsOk = false; } else { int mltVersion = (mltVersionMajor << 16) + (mltVersionMinor << 8) + mltVersionRevision; int runningVersion = mlt_version_get_int(); if (runningVersion < mltVersion) { m_errors.append( i18n("
  • Unsupported MLT version
    Please upgrade to %1.%2.%3
  • ", mltVersionMajor, mltVersionMinor, mltVersionRevision)); m_systemCheckIsOk = false; } // Retrieve the list of available transitions. Mlt::Properties *producers = repository->producers(); QStringList producersItemList; producersItemList.reserve(producers->count()); for (int i = 0; i < producers->count(); ++i) { producersItemList << producers->get_name(i); } delete producers; // Check that we have the frei0r effects installed Mlt::Properties *filters = repository->filters(); bool hasFrei0r = false; QString filterName; for (int i = 0; i < filters->count(); ++i) { filterName = filters->get_name(i); if (filterName.startsWith(QStringLiteral("frei0r."))) { hasFrei0r = true; break; } } delete filters; if (!hasFrei0r) { // Frei0r effects not found qDebug() << "Missing Frei0r module"; m_warnings.append( i18n("
  • Missing package: Frei0r effects (frei0r-plugins)
    provides many effects and transitions. Install recommended
  • ")); } #ifndef Q_OS_WIN // Check that we have the breeze icon theme installed const QStringList iconPaths = QIcon::themeSearchPaths(); bool hasBreeze = false; for (const QString &path : iconPaths) { QDir dir(path); if (dir.exists(QStringLiteral("breeze"))) { hasBreeze = true; break; } } if (!hasBreeze) { // Breeze icons not found qDebug() << "Missing Breeze icons"; m_warnings.append( i18n("
  • Missing package: Breeze icons (breeze-icon-theme)
    provides many icons used in Kdenlive. Install recommended
  • ")); } #endif Mlt::Properties *consumers = repository->consumers(); QStringList consumersItemList; consumersItemList.reserve(consumers->count()); for (int i = 0; i < consumers->count(); ++i) { consumersItemList << consumers->get_name(i); } delete consumers; if (consumersItemList.contains(QStringLiteral("sdl2"))) { // MLT >= 6.6.0 and SDL2 module KdenliveSettings::setSdlAudioBackend(QStringLiteral("sdl2_audio")); KdenliveSettings::setAudiobackend(QStringLiteral("sdl2_audio")); } else if (consumersItemList.contains(QStringLiteral("sdl"))) { // MLT < 6.6.0 KdenliveSettings::setSdlAudioBackend(QStringLiteral("sdl_audio")); KdenliveSettings::setAudiobackend(QStringLiteral("sdl_audio")); } else if (consumersItemList.contains(QStringLiteral("rtaudio"))) { KdenliveSettings::setSdlAudioBackend(QStringLiteral("sdl2_audio")); KdenliveSettings::setAudiobackend(QStringLiteral("rtaudio")); } else { // SDL module m_errors.append(i18n("
  • Missing MLT module: sdl or rtaudio
    required for audio output
  • ")); m_systemCheckIsOk = false; } // AVformat module Mlt::Consumer *consumer = nullptr; Mlt::Profile p; if (consumersItemList.contains(QStringLiteral("avformat"))) { consumer = new Mlt::Consumer(p, "avformat"); } if (consumer == nullptr || !consumer->is_valid()) { qDebug() << "Missing AVFORMAT MLT module"; m_warnings.append(i18n("
  • Missing MLT module: avformat (FFmpeg)
    required for audio/video
  • ")); m_brokenModule = true; } else { consumer->set("vcodec", "list"); consumer->set("acodec", "list"); consumer->set("f", "list"); consumer->start(); Mlt::Properties vcodecs((mlt_properties)consumer->get_data("vcodec")); for (int i = 0; i < vcodecs.count(); ++i) { vcodecsList << QString(vcodecs.get(i)); } Mlt::Properties acodecs((mlt_properties)consumer->get_data("acodec")); for (int i = 0; i < acodecs.count(); ++i) { acodecsList << QString(acodecs.get(i)); } checkMissingCodecs(); delete consumer; } // Image module if (!producersItemList.contains(QStringLiteral("qimage")) && !producersItemList.contains(QStringLiteral("pixbuf"))) { qDebug() << "Missing image MLT module"; m_warnings.append(i18n("
  • Missing MLT module: qimage or pixbuf
    required for images and titles
  • ")); m_brokenModule = true; } // Titler module if (!producersItemList.contains(QStringLiteral("kdenlivetitle"))) { qDebug() << "Missing TITLER MLT module"; m_warnings.append(i18n("
  • Missing MLT module: kdenlivetitle
    required to create titles
  • ")); KdenliveSettings::setHastitleproducer(false); m_brokenModule = true; } else { KdenliveSettings::setHastitleproducer(true); } } if (m_systemCheckIsOk && !m_brokenModule) { // everything is ok return; } if (!m_systemCheckIsOk || m_brokenModule) { // Something is wrong with install if (!m_systemCheckIsOk) { // WARN } } else { // OK } } void Wizard::checkMissingCodecs() { bool replaceVorbisCodec = false; if (acodecsList.contains(QStringLiteral("libvorbis"))) { replaceVorbisCodec = true; } bool replaceLibfaacCodec = false; if (!acodecsList.contains(QStringLiteral("aac")) && acodecsList.contains(QStringLiteral("libfaac"))) { replaceLibfaacCodec = true; } QStringList profilesList; profilesList << QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("export/profiles.xml")); QDir directory = QDir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/export/")); QStringList filter; filter << QStringLiteral("*.xml"); const QStringList fileList = directory.entryList(filter, QDir::Files); for (const QString &filename : fileList) { profilesList << directory.absoluteFilePath(filename); } // We should parse customprofiles.xml in last position, so that user profiles // can also override profiles installed by KNewStuff QStringList requiredACodecs; QStringList requiredVCodecs; for (const QString &filename : profilesList) { QDomDocument doc; QFile file(filename); doc.setContent(&file, false); file.close(); QString std; QString format; QDomNodeList profiles = doc.elementsByTagName(QStringLiteral("profile")); for (int i = 0; i < profiles.count(); ++i) { std = profiles.at(i).toElement().attribute(QStringLiteral("args")); format.clear(); if (std.startsWith(QLatin1String("acodec="))) { format = std.section(QStringLiteral("acodec="), 1, 1); } else if (std.contains(QStringLiteral(" acodec="))) { format = std.section(QStringLiteral(" acodec="), 1, 1); } if (!format.isEmpty()) { requiredACodecs << format.section(QLatin1Char(' '), 0, 0).toLower(); } format.clear(); if (std.startsWith(QLatin1String("vcodec="))) { format = std.section(QStringLiteral("vcodec="), 1, 1); } else if (std.contains(QStringLiteral(" vcodec="))) { format = std.section(QStringLiteral(" vcodec="), 1, 1); } if (!format.isEmpty()) { requiredVCodecs << format.section(QLatin1Char(' '), 0, 0).toLower(); } } } requiredACodecs.removeDuplicates(); requiredVCodecs.removeDuplicates(); if (replaceVorbisCodec) { int ix = requiredACodecs.indexOf(QStringLiteral("vorbis")); if (ix > -1) { requiredACodecs.replace(ix, QStringLiteral("libvorbis")); } } if (replaceLibfaacCodec) { int ix = requiredACodecs.indexOf(QStringLiteral("aac")); if (ix > -1) { requiredACodecs.replace(ix, QStringLiteral("libfaac")); } } for (int i = 0; i < acodecsList.count(); ++i) { requiredACodecs.removeAll(acodecsList.at(i)); } for (int i = 0; i < vcodecsList.count(); ++i) { requiredVCodecs.removeAll(vcodecsList.at(i)); } /* * Info about missing codecs is given in render widget, no need to put this at first start * if (!requiredACodecs.isEmpty() || !requiredVCodecs.isEmpty()) { QString missing = requiredACodecs.join(QLatin1Char(',')); if (!missing.isEmpty() && !requiredVCodecs.isEmpty()) { missing.append(','); } missing.append(requiredVCodecs.join(QLatin1Char(','))); missing.prepend(i18n("The following codecs were not found on your system. Check our online manual if you need them: ")); m_infos.append(QString("
  • %1
  • ").arg(missing)); }*/ } void Wizard::slotCheckPrograms() { bool allIsOk = true; // Check first in same folder as melt exec const QStringList mltpath = QStringList() << QFileInfo(KdenliveSettings::rendererpath()).canonicalPath(); QString exepath; if (KdenliveSettings::ffmpegpath().isEmpty() || !QFileInfo::exists(KdenliveSettings::ffmpegpath())) { exepath = QStandardPaths::findExecutable(QStringLiteral("ffmpeg%1").arg(FFMPEG_SUFFIX), mltpath); if (exepath.isEmpty()) { exepath = QStandardPaths::findExecutable(QStringLiteral("ffmpeg%1").arg(FFMPEG_SUFFIX)); } qDebug() << "Unable to find FFMpeg binary..."; if (exepath.isEmpty()) { // Check for libav version exepath = QStandardPaths::findExecutable(QStringLiteral("avconv")); if (exepath.isEmpty()) { m_warnings.append(i18n("
  • Missing app: ffmpeg
    required for proxy clips and transcoding
  • ")); allIsOk = false; } } } QString playpath; if (KdenliveSettings::ffplaypath().isEmpty() || !QFileInfo::exists(KdenliveSettings::ffplaypath())) { playpath = QStandardPaths::findExecutable(QStringLiteral("ffplay%1").arg(FFMPEG_SUFFIX), mltpath); if (playpath.isEmpty()) { playpath = QStandardPaths::findExecutable(QStringLiteral("ffplay%1").arg(FFMPEG_SUFFIX)); } if (playpath.isEmpty()) { // Check for libav version playpath = QStandardPaths::findExecutable(QStringLiteral("avplay")); if (playpath.isEmpty()) { m_infos.append(i18n("
  • Missing app: ffplay
    recommended for some preview jobs
  • ")); } } } QString probepath; if (KdenliveSettings::ffprobepath().isEmpty() || !QFileInfo::exists(KdenliveSettings::ffprobepath())) { probepath = QStandardPaths::findExecutable(QStringLiteral("ffprobe%1").arg(FFMPEG_SUFFIX), mltpath); if (probepath.isEmpty()) { probepath = QStandardPaths::findExecutable(QStringLiteral("ffprobe%1").arg(FFMPEG_SUFFIX)); } if (probepath.isEmpty()) { // Check for libav version probepath = QStandardPaths::findExecutable(QStringLiteral("avprobe")); if (probepath.isEmpty()) { m_infos.append(i18n("
  • Missing app: ffprobe
    recommended for extra clip analysis
  • ")); } } } if (!exepath.isEmpty()) { KdenliveSettings::setFfmpegpath(exepath); } if (!playpath.isEmpty()) { KdenliveSettings::setFfplaypath(playpath); } if (!probepath.isEmpty()) { KdenliveSettings::setFfprobepath(probepath); } // Deprecated /* #ifndef Q_WS_MAC item = new QTreeWidgetItem(m_treeWidget, QStringList() << QString() << i18n("dvgrab")); item->setData(1, Qt::UserRole, i18n("Required for firewire capture")); item->setSizeHint(0, m_itemSize); if (QStandardPaths::findExecutable(QStringLiteral("dvgrab")).isEmpty()) item->setIcon(0, m_badIcon); else item->setIcon(0, m_okIcon); #endif */ // set up some default applications QString program; if (KdenliveSettings::defaultimageapp().isEmpty()) { program = QStandardPaths::findExecutable(QStringLiteral("gimp")); if (program.isEmpty()) { program = QStandardPaths::findExecutable(QStringLiteral("krita")); } if (!program.isEmpty()) { KdenliveSettings::setDefaultimageapp(program); } } if (KdenliveSettings::defaultaudioapp().isEmpty()) { program = QStandardPaths::findExecutable(QStringLiteral("audacity")); if (program.isEmpty()) { program = QStandardPaths::findExecutable(QStringLiteral("traverso")); } if (!program.isEmpty()) { KdenliveSettings::setDefaultaudioapp(program); } } if (allIsOk) { // OK } else { // WRONG } } void Wizard::installExtraMimes(const QString &baseName, const QStringList &globs) { QMimeDatabase db; QString mimefile = baseName; mimefile.replace('/', '-'); QMimeType mime = db.mimeTypeForName(baseName); QStringList missingGlobs; for (const QString &glob : globs) { QMimeType type = db.mimeTypeForFile(glob, QMimeDatabase::MatchExtension); QString mimeName = type.name(); if (!mimeName.contains(QStringLiteral("audio")) && !mimeName.contains(QStringLiteral("video"))) { missingGlobs << glob; } } if (missingGlobs.isEmpty()) { return; } if (!mime.isValid() || mime.isDefault()) { qCDebug(KDENLIVE_LOG) << "MIME type " << baseName << " not found"; } else { QStringList extensions = mime.globPatterns(); QString comment = mime.comment(); for (const QString &glob : missingGlobs) { if (!extensions.contains(glob)) { extensions << glob; } } // qCDebug(KDENLIVE_LOG) << "EXTS: " << extensions; QDir mimeDir(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/mime/packages/")); if (!mimeDir.exists()) { mimeDir.mkpath(QStringLiteral(".")); } QString packageFileName = mimeDir.absoluteFilePath(mimefile + QStringLiteral(".xml")); // qCDebug(KDENLIVE_LOG) << "INSTALLING NEW MIME TO: " << packageFileName; QFile packageFile(packageFileName); if (!packageFile.open(QIODevice::WriteOnly)) { qCCritical(KDENLIVE_LOG) << "Couldn't open" << packageFileName << "for writing"; return; } QXmlStreamWriter writer(&packageFile); writer.setAutoFormatting(true); writer.writeStartDocument(); const QString nsUri = QStringLiteral("http://www.freedesktop.org/standards/shared-mime-info"); writer.writeDefaultNamespace(nsUri); writer.writeStartElement(QStringLiteral("mime-info")); writer.writeStartElement(nsUri, QStringLiteral("mime-type")); writer.writeAttribute(QStringLiteral("type"), baseName); if (!comment.isEmpty()) { writer.writeStartElement(nsUri, QStringLiteral("comment")); writer.writeCharacters(comment); writer.writeEndElement(); // comment } for (const QString &pattern : extensions) { writer.writeStartElement(nsUri, QStringLiteral("glob")); writer.writeAttribute(QStringLiteral("pattern"), pattern); writer.writeEndElement(); // glob } writer.writeEndElement(); // mime-info writer.writeEndElement(); // mime-type writer.writeEndDocument(); } } void Wizard::runUpdateMimeDatabase() { const QString localPackageDir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/mime/"); // Q_ASSERT(!localPackageDir.isEmpty()); KProcess proc; proc << QStringLiteral("update-mime-database"); proc << localPackageDir; const int exitCode = proc.execute(); if (exitCode != 0) { qCWarning(KDENLIVE_LOG) << proc.program() << "exited with error code" << exitCode; } } void Wizard::slotCheckStandard() { m_standard.profiles_list->clear(); QStringList profiles; if (!m_standard.button_hdv->isChecked()) { // DV standard QMapIterator i(m_dvProfiles); while (i.hasNext()) { i.next(); auto *item = new QListWidgetItem(i.key(), m_standard.profiles_list); item->setData(Qt::UserRole, i.value()); } } if (!m_standard.button_dv->isChecked()) { // HDV standard QMapIterator i(m_hdvProfiles); while (i.hasNext()) { i.next(); auto *item = new QListWidgetItem(i.key(), m_standard.profiles_list); item->setData(Qt::UserRole, i.value()); } } if (m_standard.button_all->isChecked()) { QMapIterator i(m_otherProfiles); while (i.hasNext()) { i.next(); auto *item = new QListWidgetItem(i.key(), m_standard.profiles_list); item->setData(Qt::UserRole, i.value()); } // m_standard.profiles_list->sortItems(); } for (int i = 0; i < m_standard.profiles_list->count(); ++i) { QListWidgetItem *item = m_standard.profiles_list->item(i); std::unique_ptr &curProfile = ProfileRepository::get()->getProfile(item->data(Qt::UserRole).toString()); const QString infoString = QStringLiteral("") + i18n("Frame size:") + QStringLiteral(" %1x%2
    ").arg(curProfile->width()).arg(curProfile->height()) + i18n("Frame rate:") + QStringLiteral(" %1/%2
    ").arg(curProfile->frame_rate_num()).arg(curProfile->frame_rate_den()) + i18n("Pixel aspect ratio:") + QStringLiteral("%1/%2
    ").arg(curProfile->sample_aspect_num()).arg(curProfile->sample_aspect_den()) + i18n("Display aspect ratio:") + QStringLiteral(" %1/%2").arg(curProfile->display_aspect_num()).arg(curProfile->display_aspect_den()); /*const QString infoString = QStringLiteral("" + i18n("Frame size:") + QStringLiteral(" %1x%2
    ") + i18n("Frame rate:") + QStringLiteral(" %3/%4
    ") + i18n("Pixel aspect ratio:") + QStringLiteral("%5/%6
    ") + i18n("Display aspect ratio:") + QStringLiteral(" %7/%8")) .arg(QString::number(curProfile->width()), QString::number(curProfile->height()), QString::number(curProfile->frame_rate_num()), QString::number(curProfile->frame_rate_den()), QString::number(curProfile->sample_aspect_num()), QString::number(curProfile->sample_aspect_den()), QString::number(curProfile->display_aspect_num()), QString::number(curProfile->display_aspect_den()));*/ item->setToolTip(infoString); } m_standard.profiles_list->setSortingEnabled(true); m_standard.profiles_list->setCurrentRow(0); } void Wizard::slotCheckSelectedItem() { // Make sure we always have an item highlighted m_standard.profiles_list->setCurrentRow(m_standard.profiles_list->currentRow()); } void Wizard::adjustSettings() { // if (m_extra.installmimes->isChecked()) { { QStringList globs; globs << QStringLiteral("*.mts") << QStringLiteral("*.m2t") << QStringLiteral("*.mod") << QStringLiteral("*.ts") << QStringLiteral("*.m2ts") << QStringLiteral("*.m2v"); installExtraMimes(QStringLiteral("video/mpeg"), globs); globs.clear(); globs << QStringLiteral("*.dv"); installExtraMimes(QStringLiteral("video/dv"), globs); runUpdateMimeDatabase(); } } void Wizard::slotCheckMlt() { QString errorMessage; if (KdenliveSettings::rendererpath().isEmpty()) { errorMessage.append(i18n("Your MLT installation cannot be found. Install MLT and restart Kdenlive.\n")); } if (!errorMessage.isEmpty()) { errorMessage.prepend(QStringLiteral("%1
    ").arg(i18n("Fatal Error"))); QLabel *pix = new QLabel(); pix->setPixmap(QIcon::fromTheme(QStringLiteral("dialog-error")).pixmap(30)); QLabel *label = new QLabel(errorMessage); label->setWordWrap(true); m_startLayout->addSpacing(40); m_startLayout->addWidget(pix); m_startLayout->addWidget(label); m_systemCheckIsOk = false; // Warn } else { m_systemCheckIsOk = true; } if (m_systemCheckIsOk) { checkMltComponents(); } slotCheckPrograms(); } bool Wizard::isOk() const { return m_systemCheckIsOk; } void Wizard::slotOpenManual() { KRun::runUrl(QUrl(QStringLiteral("https://kdenlive.org/troubleshooting")), QStringLiteral("text/html"), this, KRun::RunFlags()); } void Wizard::slotShowWebInfos() { KRun::runUrl(QUrl("http://kdenlive.org/discover/" + QString(kdenlive_version).section(QLatin1Char(' '), 0, 0)), QStringLiteral("text/html"), this, KRun::RunFlags()); } void Wizard::slotSaveCaptureFormat() { QStringList format = m_capture.v4l_formats->itemData(m_capture.v4l_formats->currentIndex()).toStringList(); if (format.isEmpty()) { return; } std::unique_ptr profile(new ProfileParam(pCore->getCurrentProfile().get())); profile->m_description = QStringLiteral("Video4Linux capture"); profile->m_colorspace = 601; profile->m_width = format.at(1).toInt(); profile->m_height = format.at(2).toInt(); profile->m_sample_aspect_num = 1; profile->m_sample_aspect_den = 1; profile->m_display_aspect_num = format.at(1).toInt(); profile->m_display_aspect_den = format.at(2).toInt(); profile->m_frame_rate_num = format.at(3).toInt(); profile->m_frame_rate_den = format.at(4).toInt(); - profile->m_progressive = 1; + profile->m_progressive = true; QDir dir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/profiles/")); if (!dir.exists()) { dir.mkpath(QStringLiteral(".")); } ProfileRepository::get()->saveProfile(profile.get(), dir.absoluteFilePath(QStringLiteral("video4linux"))); } void Wizard::slotUpdateDecklinkDevice(uint captureCard) { KdenliveSettings::setDecklink_capturedevice(captureCard); } void Wizard::testHwEncoders() { QProcess hwEncoders; // Testing vaapi support QTemporaryFile tmp(QDir::tempPath() + "/XXXXXX.mp4"); if (!tmp.open()) { // Something went wrong return; } tmp.close(); // VAAPI testing QStringList args{"-y", "-vaapi_device", "/dev/dri/renderD128", "-f", "lavfi", "-i", "smptebars=duration=5:size=1280x720:rate=25", "-vf", "format=nv12,hwupload", "-c:v", "h264_vaapi", "-an", "-f", "mp4", tmp.fileName()}; qDebug() << "// FFMPEG ARGS: " << args; hwEncoders.start(KdenliveSettings::ffmpegpath(), args); bool vaapiSupported = false; if (hwEncoders.waitForFinished()) { if (hwEncoders.exitStatus() == QProcess::CrashExit) { qDebug() << "/// ++ VAAPI NOT SUPPORTED"; } else { if (tmp.exists() && tmp.size() > 0) { qDebug() << "/// ++ VAAPI YES SUPPORTED ::::::"; // vaapi support enabled vaapiSupported = true; } else { qDebug() << "/// ++ VAAPI FAILED ::::::"; // vaapi support not enabled } } } KdenliveSettings::setVaapiEnabled(vaapiSupported); // NVIDIA testing QTemporaryFile tmp2(QDir::tempPath() + "/XXXXXX.mp4"); if (!tmp2.open()) { // Something went wrong return; } tmp2.close(); QStringList args2{"-y", "-hwaccel", "cuvid", "-f", "lavfi", "-i", "smptebars=duration=5:size=1280x720:rate=25", "-c:v", "h264_nvenc", "-an", "-f", "mp4", tmp2.fileName()}; qDebug() << "// FFMPEG ARGS: " << args2; hwEncoders.start(KdenliveSettings::ffmpegpath(), args2); bool nvencSupported = false; if (hwEncoders.waitForFinished()) { if (hwEncoders.exitStatus() == QProcess::CrashExit) { qDebug() << "/// ++ NVENC NOT SUPPORTED"; } else { if (tmp2.exists() && tmp2.size() > 0) { qDebug() << "/// ++ NVENC YES SUPPORTED ::::::"; // vaapi support enabled nvencSupported = true; } else { qDebug() << "/// ++ NVENC FAILED ::::::"; // vaapi support not enabled } } } KdenliveSettings::setNvencEnabled(nvencSupported); } void Wizard::updateHwStatus() { auto *statusLabel = new KMessageWidget(this); bool hwEnabled = KdenliveSettings::vaapiEnabled() || KdenliveSettings::nvencEnabled(); statusLabel->setMessageType(hwEnabled ? KMessageWidget::Positive : KMessageWidget::Information); statusLabel->setWordWrap(true); QString statusMessage; if (!hwEnabled) { statusMessage = i18n("No hardware encoders found."); } else { if (KdenliveSettings::nvencEnabled()) { statusMessage += i18n("NVIDIA hardware encoders found and enabled."); } if (KdenliveSettings::vaapiEnabled()) { statusMessage += i18n("VAAPI hardware encoders found and enabled."); } } statusLabel->setText(statusMessage); statusLabel->setCloseButtonVisible(false); // errorLabel->setTimeout(); m_startLayout->addWidget(statusLabel); statusLabel->animatedShow(); QTimer::singleShot(3000, statusLabel, &KMessageWidget::animatedHide); } diff --git a/src/dialogs/wizard.h b/src/dialogs/wizard.h index 5c0bfd3a3..bec0adb95 100644 --- a/src/dialogs/wizard.h +++ b/src/dialogs/wizard.h @@ -1,89 +1,89 @@ /*************************************************************************** * 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 WIZARD_H #define WIZARD_H #include #include #include #include "ui_wizardcapture_ui.h" #include "ui_wizardcheck_ui.h" #include "ui_wizardextra_ui.h" #include "ui_wizardmltcheck_ui.h" #include "ui_wizardstandard_ui.h" class KMessageWidget; class MyWizardPage : public QWizardPage { public: explicit MyWizardPage(QWidget *parent = nullptr); void setComplete(bool complete); bool isComplete() const override; - bool m_isComplete; + bool m_isComplete{false}; }; class Wizard : public QWizard { Q_OBJECT public: explicit Wizard(bool autoClose, bool appImageCheck, QWidget *parent = nullptr); void installExtraMimes(const QString &baseName, const QStringList &globs); void runUpdateMimeDatabase(); void adjustSettings(); bool isOk() const; static void testHwEncoders(); private: Ui::WizardStandard_UI m_standard; Ui::WizardExtra_UI m_extra; Ui::WizardMltCheck_UI m_mltCheck; Ui::WizardCapture_UI m_capture; Ui::WizardCheck_UI m_check; QVBoxLayout *m_startLayout; MyWizardPage *m_page; KMessageWidget *m_errorWidget; bool m_systemCheckIsOk; bool m_brokenModule; QString m_errors; QString m_warnings; QString m_infos; QMap m_dvProfiles; QMap m_hdvProfiles; QMap m_otherProfiles; void slotCheckPrograms(); void checkMltComponents(); void checkMissingCodecs(); void updateHwStatus(); private slots: void slotCheckStandard(); void slotCheckSelectedItem(); void slotCheckMlt(); void slotShowWebInfos(); void slotDetectWebcam(); void slotUpdateCaptureParameters(); void slotSaveCaptureFormat(); void slotUpdateDecklinkDevice(uint captureCard); void slotOpenManual(); }; #endif diff --git a/src/doc/documentchecker.cpp b/src/doc/documentchecker.cpp index 75cd331da..bf62b5008 100644 --- a/src/doc/documentchecker.cpp +++ b/src/doc/documentchecker.cpp @@ -1,1230 +1,1230 @@ /*************************************************************************** * 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 "documentchecker.h" #include "bin/binplaylist.hpp" #include "effects/effectsrepository.hpp" #include "kdenlivesettings.h" #include "kthumb.h" #include "titler/titlewidget.h" #include #include #include #include #include "kdenlive_debug.h" #include #include #include #include #include #include - +#include const int hashRole = Qt::UserRole; const int sizeRole = Qt::UserRole + 1; const int idRole = Qt::UserRole + 2; const int statusRole = Qt::UserRole + 3; const int typeRole = Qt::UserRole + 4; const int typeOriginalResource = Qt::UserRole + 5; const int clipTypeRole = Qt::UserRole + 6; const int CLIPMISSING = 0; const int CLIPOK = 1; const int CLIPPLACEHOLDER = 2; const int PROXYMISSING = 4; const int SOURCEMISSING = 5; const int LUMAMISSING = 10; const int LUMAOK = 11; const int LUMAPLACEHOLDER = 12; enum MISSINGTYPE { TITLE_IMAGE_ELEMENT = 20, TITLE_FONT_ELEMENT = 21 }; -DocumentChecker::DocumentChecker(const QUrl &url, const QDomDocument &doc) - : m_url(url) +DocumentChecker::DocumentChecker(QUrl url, const QDomDocument &doc) + : m_url(std::move(url)) , m_doc(doc) , m_dialog(nullptr) { } QMap DocumentChecker::getLumaPairs() const { QMap lumaSearchPairs; lumaSearchPairs.insert(QStringLiteral("luma"), QStringLiteral("resource")); lumaSearchPairs.insert(QStringLiteral("movit.luma_mix"), QStringLiteral("resource")); lumaSearchPairs.insert(QStringLiteral("composite"), QStringLiteral("luma")); lumaSearchPairs.insert(QStringLiteral("region"), QStringLiteral("composite.luma")); return lumaSearchPairs; } bool DocumentChecker::hasErrorInClips() { int max; QDomElement baseElement = m_doc.documentElement(); QString root = baseElement.attribute(QStringLiteral("root")); if (!root.isEmpty()) { QDir dir(root); if (!dir.exists()) { // Looks like project was moved, try recovering root from current project url m_rootReplacement.first = dir.absolutePath() + QDir::separator(); root = m_url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).toLocalFile(); baseElement.setAttribute(QStringLiteral("root"), root); root = QDir::cleanPath(root) + QDir::separator(); m_rootReplacement.second = root; } else { root = QDir::cleanPath(root) + QDir::separator(); } } // Check if strorage folder for temp files exists QString storageFolder; QDir projectDir(m_url.adjusted(QUrl::RemoveFilename).toLocalFile()); QDomNodeList playlists = m_doc.elementsByTagName(QStringLiteral("playlist")); for (int i = 0; i < playlists.count(); ++i) { if (playlists.at(i).toElement().attribute(QStringLiteral("id")) == BinPlaylist::binPlaylistId) { QString documentid = Xml::getXmlProperty(playlists.at(i).toElement(), QStringLiteral("kdenlive:docproperties.documentid")); if (documentid.isEmpty()) { // invalid document id, recreate one documentid = QString::number(QDateTime::currentMSecsSinceEpoch()); // TODO: Warn on invalid doc id Xml::setXmlProperty(playlists.at(i).toElement(), QStringLiteral("kdenlive:docproperties.documentid"), documentid); } storageFolder = Xml::getXmlProperty(playlists.at(i).toElement(), QStringLiteral("kdenlive:docproperties.storagefolder")); if (!storageFolder.isEmpty() && QFileInfo(storageFolder).isRelative()) { storageFolder.prepend(root); } if (!storageFolder.isEmpty() && !QFile::exists(storageFolder) && projectDir.exists(documentid)) { storageFolder = projectDir.absolutePath(); Xml::setXmlProperty(playlists.at(i).toElement(), QStringLiteral("kdenlive:docproperties.storagefolder"), projectDir.absoluteFilePath(documentid)); m_doc.documentElement().setAttribute(QStringLiteral("modified"), 1); } break; } } QDomNodeList documentProducers = m_doc.elementsByTagName(QStringLiteral("producer")); QDomElement profile = baseElement.firstChildElement(QStringLiteral("profile")); bool hdProfile = true; if (!profile.isNull()) { if (profile.attribute(QStringLiteral("width")).toInt() < 1000) { hdProfile = false; } } // List clips whose proxy is missing QList missingProxies; // List clips who have a working proxy but no source clip QList missingSources; m_safeImages.clear(); m_safeFonts.clear(); m_missingFonts.clear(); max = documentProducers.count(); QStringList verifiedPaths; QStringList missingPaths; QStringList serviceToCheck; serviceToCheck << QStringLiteral("kdenlivetitle") << QStringLiteral("qimage") << QStringLiteral("pixbuf") << QStringLiteral("timewarp") << QStringLiteral("framebuffer") << QStringLiteral("xml"); for (int i = 0; i < max; ++i) { QDomElement e = documentProducers.item(i).toElement(); QString service = Xml::getXmlProperty(e, QStringLiteral("mlt_service")); if (!service.startsWith(QLatin1String("avformat")) && !serviceToCheck.contains(service)) { continue; } if (service == QLatin1String("qtext")) { checkMissingImagesAndFonts(QStringList(), QStringList(Xml::getXmlProperty(e, QStringLiteral("family"))), e.attribute(QStringLiteral("id")), e.attribute(QStringLiteral("name"))); continue; } if (service == QLatin1String("kdenlivetitle")) { // TODO: Check is clip template is missing (xmltemplate) or hash changed QString xml = Xml::getXmlProperty(e, QStringLiteral("xmldata")); QStringList images = TitleWidget::extractImageList(xml); QStringList fonts = TitleWidget::extractFontList(xml); checkMissingImagesAndFonts(images, fonts, e.attribute(QStringLiteral("id")), e.attribute(QStringLiteral("name"))); continue; } QString resource = Xml::getXmlProperty(e, QStringLiteral("resource")); if (resource.isEmpty()) { continue; } if (service == QLatin1String("timewarp")) { // slowmotion clip, trim speed info resource = Xml::getXmlProperty(e, QStringLiteral("warp_resource")); } else if (service == QLatin1String("framebuffer")) { // slowmotion clip, trim speed info resource = resource.section(QLatin1Char('?'), 0, 0); } // Make sure to have absolute paths if (QFileInfo(resource).isRelative()) { resource.prepend(root); } if (verifiedPaths.contains(resource)) { // Don't check same url twice (for example track producers) if (missingPaths.contains(resource)) { m_missingClips.append(e); } continue; } QString proxy = Xml::getXmlProperty(e, QStringLiteral("kdenlive:proxy")); if (proxy.length() > 1) { if (QFileInfo(proxy).isRelative()) { proxy.prepend(root); } if (!QFile::exists(proxy)) { // Missing clip found // Check if proxy exists in current storage folder bool fixed = false; if (!storageFolder.isEmpty()) { QDir dir(storageFolder + QStringLiteral("/proxy/")); if (dir.exists(QFileInfo(proxy).fileName())) { QString updatedPath = dir.absoluteFilePath(QFileInfo(proxy).fileName()); fixProxyClip(e.attribute(QStringLiteral("id")), Xml::getXmlProperty(e, QStringLiteral("kdenlive:proxy")), updatedPath, documentProducers); fixed = true; } } if (!fixed) { missingProxies.append(e); } } QString original = Xml::getXmlProperty(e, QStringLiteral("kdenlive:originalurl")); if (QFileInfo(original).isRelative()) { original.prepend(root); } // Check for slideshows bool slideshow = original.contains(QStringLiteral("/.all.")) || original.contains(QLatin1Char('?')) || original.contains(QLatin1Char('%')); if (slideshow && !Xml::getXmlProperty(e, QStringLiteral("ttl")).isEmpty()) { original = QFileInfo(original).absolutePath(); } if (!QFile::exists(original)) { // clip has proxy but original clip is missing missingSources.append(e); missingPaths.append(resource); } verifiedPaths.append(resource); continue; } // Check for slideshows bool slideshow = resource.contains(QStringLiteral("/.all.")) || resource.contains(QLatin1Char('?')) || resource.contains(QLatin1Char('%')); if ((service == QLatin1String("qimage") || service == QLatin1String("pixbuf")) && slideshow) { resource = QFileInfo(resource).absolutePath(); } if (!QFile::exists(resource)) { // Missing clip found m_missingClips.append(e); missingPaths.append(resource); } // Make sure we don't query same path twice verifiedPaths.append(resource); } // Get list of used Luma files QStringList missingLumas; QStringList filesToCheck; QString filePath; QMap lumaSearchPairs = getLumaPairs(); QDomNodeList trans = m_doc.elementsByTagName(QStringLiteral("transition")); max = trans.count(); for (int i = 0; i < max; ++i) { QDomElement transition = trans.at(i).toElement(); QString service = getProperty(transition, QStringLiteral("mlt_service")); QString luma; if (lumaSearchPairs.contains(service)) { luma = getProperty(transition, lumaSearchPairs.value(service)); } if (!luma.isEmpty() && !filesToCheck.contains(luma)) { filesToCheck.append(luma); } } QMap autoFixLuma; QString lumaPath; // Check existence of luma files for (const QString &lumafile : filesToCheck) { filePath = lumafile; if (QFileInfo(filePath).isRelative()) { filePath.prepend(root); } if (!QFile::exists(filePath)) { QString lumaName = filePath.section(QLatin1Char('/'), -1); // check if this was an old format luma, not in correct folder QString fixedLuma = filePath.section(QLatin1Char('/'), 0, -2); lumaName.prepend(hdProfile ? QStringLiteral("/HD/") : QStringLiteral("/PAL/")); fixedLuma.append(lumaName); if (QFile::exists(fixedLuma)) { // Auto replace pgm with png for lumas autoFixLuma.insert(filePath, fixedLuma); continue; } // Check MLT folder if (lumaPath.isEmpty()) { QDir dir(QCoreApplication::applicationDirPath()); dir.cdUp(); dir.cd(QStringLiteral("share/kdenlive/lumas/")); lumaPath = dir.absolutePath(); } lumaName.prepend(lumaPath); if (QFile::exists(lumaName)) { autoFixLuma.insert(filePath, lumaName); continue; } if (filePath.endsWith(QLatin1String(".pgm"))) { fixedLuma = filePath.section(QLatin1Char('.'), 0, -2) + QStringLiteral(".png"); } else if (filePath.endsWith(QLatin1String(".png"))) { fixedLuma = filePath.section(QLatin1Char('.'), 0, -2) + QStringLiteral(".pgm"); } if (!fixedLuma.isEmpty() && QFile::exists(fixedLuma)) { // Auto replace pgm with png for lumas autoFixLuma.insert(filePath, fixedLuma); } else { missingLumas.append(lumafile); } } } if (!autoFixLuma.isEmpty()) { for (int i = 0; i < max; ++i) { QDomElement transition = trans.at(i).toElement(); QString service = getProperty(transition, QStringLiteral("mlt_service")); QString luma; if (lumaSearchPairs.contains(service)) { luma = getProperty(transition, lumaSearchPairs.value(service)); } if (!luma.isEmpty() && autoFixLuma.contains(luma)) { updateProperty(transition, lumaSearchPairs.value(service), autoFixLuma.value(luma)); } } } // Check for missing effects QDomNodeList effs = m_doc.elementsByTagName(QStringLiteral("filter")); max = effs.count(); QStringList filters; for (int i = 0; i < max; ++i) { QDomElement transition = effs.at(i).toElement(); QString service = getProperty(transition, QStringLiteral("kdenlive_id")); filters << service; } QStringList processed; for (const QString &id : filters) { if (!processed.contains(id) && !EffectsRepository::get()->exists(id)) { m_missingFilters << id; } processed << id; } if (!m_missingFilters.isEmpty()) { // Delete missing effects for (int i = 0; i < effs.count(); ++i) { QDomElement e = effs.item(i).toElement(); if (m_missingFilters.contains(getProperty(e, QStringLiteral("kdenlive_id")))) { // Remove clip e.parentNode().removeChild(e); --i; } } } if (m_missingClips.isEmpty() && missingLumas.isEmpty() && missingProxies.isEmpty() && missingSources.isEmpty() && m_missingFonts.isEmpty() && m_missingFilters.isEmpty()) { return false; } m_dialog = new QDialog(); m_dialog->setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)); m_ui.setupUi(m_dialog); for (const QString &l : missingLumas) { QTreeWidgetItem *item = new QTreeWidgetItem(m_ui.treeWidget, QStringList() << i18n("Luma file") << l); item->setIcon(0, QIcon::fromTheme(QStringLiteral("dialog-close"))); item->setData(0, idRole, l); item->setData(0, statusRole, LUMAMISSING); } m_ui.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(m_missingClips.isEmpty() && missingProxies.isEmpty() && missingSources.isEmpty()); max = m_missingClips.count(); m_missingProxyIds.clear(); for (int i = 0; i < max; ++i) { QDomElement e = m_missingClips.at(i).toElement(); QString clipType; ClipType::ProducerType type; int status = CLIPMISSING; const QString service = Xml::getXmlProperty(e, QStringLiteral("mlt_service")); QString resource = service == QLatin1String("timewarp") ? Xml::getXmlProperty(e, QStringLiteral("warp_resource")) : Xml::getXmlProperty(e, QStringLiteral("resource")); bool slideshow = resource.contains(QStringLiteral("/.all.")) || resource.contains(QLatin1Char('?')) || resource.contains(QLatin1Char('%')); if (service == QLatin1String("avformat") || service == QLatin1String("avformat-novalidate") || service == QLatin1String("framebuffer") || service == QLatin1String("timewarp")) { clipType = i18n("Video clip"); type = ClipType::AV; } else if (service == QLatin1String("qimage") || service == QLatin1String("pixbuf")) { if (slideshow) { clipType = i18n("Slideshow clip"); type = ClipType::SlideShow; } else { clipType = i18n("Image clip"); type = ClipType::Image; } } else if (service == QLatin1String("mlt") || service == QLatin1String("xml")) { clipType = i18n("Playlist clip"); type = ClipType::Playlist; } else if (e.tagName() == QLatin1String("missingtitle")) { clipType = i18n("Title Image"); status = TITLE_IMAGE_ELEMENT; type = ClipType::Text; } else { clipType = i18n("Unknown"); type = ClipType::Unknown; } QTreeWidgetItem *item = new QTreeWidgetItem(m_ui.treeWidget, QStringList() << clipType); item->setData(0, statusRole, CLIPMISSING); item->setData(0, clipTypeRole, (int)type); item->setData(0, idRole, e.attribute(QStringLiteral("id"))); item->setToolTip(0, i18n("Missing item")); if (status == TITLE_IMAGE_ELEMENT) { item->setIcon(0, QIcon::fromTheme(QStringLiteral("dialog-warning"))); item->setToolTip(1, e.attribute(QStringLiteral("name"))); QString imageResource = e.attribute(QStringLiteral("resource")); item->setData(0, typeRole, status); item->setData(0, typeOriginalResource, e.attribute(QStringLiteral("resource"))); if (!m_rootReplacement.first.isEmpty()) { if (imageResource.startsWith(m_rootReplacement.first)) { imageResource.replace(m_rootReplacement.first, m_rootReplacement.second); if (QFile::exists(imageResource)) { item->setData(0, statusRole, CLIPOK); item->setToolTip(0, i18n("Relocated item")); } } } item->setText(1, imageResource); } else { item->setIcon(0, QIcon::fromTheme(QStringLiteral("dialog-close"))); if (QFileInfo(resource).isRelative()) { resource.prepend(root); } item->setData(0, hashRole, Xml::getXmlProperty(e, QStringLiteral("kdenlive:file_hash"))); item->setData(0, sizeRole, Xml::getXmlProperty(e, QStringLiteral("kdenlive:file_size"))); if (!m_rootReplacement.first.isEmpty()) { if (resource.startsWith(m_rootReplacement.first)) { resource.replace(m_rootReplacement.first, m_rootReplacement.second); if (QFile::exists(resource)) { item->setData(0, statusRole, CLIPOK); item->setToolTip(0, i18n("Relocated item")); } } } item->setText(1, resource); } } for (const QString &font : m_missingFonts) { QString clipType = i18n("Title Font"); QTreeWidgetItem *item = new QTreeWidgetItem(m_ui.treeWidget, QStringList() << clipType); item->setData(0, statusRole, CLIPPLACEHOLDER); item->setIcon(0, QIcon::fromTheme(QStringLiteral("dialog-warning"))); QString newft = QFontInfo(QFont(font)).family(); item->setText(1, i18n("%1 will be replaced by %2", font, newft)); item->setData(0, typeRole, TITLE_FONT_ELEMENT); } QString infoLabel; if (!m_missingClips.isEmpty()) { infoLabel = i18n("The project file contains missing clips or files."); } if (!m_missingFilters.isEmpty()) { if (!infoLabel.isEmpty()) { infoLabel.append(QStringLiteral("\n")); } infoLabel.append(i18np("Missing effect: %2 will be removed from project.", "Missing effects: %2 will be removed from project.", m_missingFilters.count(), m_missingFilters.join(","))); } if (!missingProxies.isEmpty()) { if (!infoLabel.isEmpty()) { infoLabel.append(QStringLiteral("\n")); } infoLabel.append(i18n("Missing proxies will be recreated after opening.")); } if (!missingSources.isEmpty()) { if (!infoLabel.isEmpty()) { infoLabel.append(QStringLiteral("\n")); } infoLabel.append(i18np("The project file contains a missing clip, you can still work with its proxy.", "The project file contains %1 missing clips, you can still work with their proxies.", missingSources.count())); } if (!infoLabel.isEmpty()) { m_ui.infoLabel->setText(infoLabel); } m_ui.removeSelected->setEnabled(!m_missingClips.isEmpty()); m_ui.recursiveSearch->setEnabled(!m_missingClips.isEmpty() || !missingLumas.isEmpty() || !missingSources.isEmpty()); m_ui.usePlaceholders->setEnabled(!m_missingClips.isEmpty()); // Check missing proxies max = missingProxies.count(); if (max > 0) { QTreeWidgetItem *item = new QTreeWidgetItem(m_ui.treeWidget, QStringList() << i18n("Proxy clip")); item->setIcon(0, QIcon::fromTheme(QStringLiteral("dialog-warning"))); item->setText( 1, i18np("%1 missing proxy clip, will be recreated on project opening", "%1 missing proxy clips, will be recreated on project opening", max)); // item->setData(0, hashRole, e.attribute("file_hash")); item->setData(0, statusRole, PROXYMISSING); item->setToolTip(0, i18n("Missing proxy")); } for (int i = 0; i < max; ++i) { QDomElement e = missingProxies.at(i).toElement(); QString realPath = Xml::getXmlProperty(e, QStringLiteral("kdenlive:originalurl")); QString id = e.attribute(QStringLiteral("id")); m_missingProxyIds << id; // Tell Kdenlive to recreate proxy e.setAttribute(QStringLiteral("_replaceproxy"), QStringLiteral("1")); // Replace proxy url with real clip in MLT producers QDomElement mltProd; int prodsCount = documentProducers.count(); for (int j = 0; j < prodsCount; ++j) { mltProd = documentProducers.at(j).toElement(); QString prodId = mltProd.attribute(QStringLiteral("id")); QString parentId = prodId; bool slowmotion = false; if (parentId.startsWith(QLatin1String("slowmotion"))) { slowmotion = true; parentId = parentId.section(QLatin1Char(':'), 1, 1); } if (parentId.contains(QLatin1Char('_'))) { parentId = parentId.section(QLatin1Char('_'), 0, 0); } if (parentId == id) { // Hit, we must replace url QString suffix; QString resource = Xml::getXmlProperty(mltProd, QStringLiteral("resource")); if (slowmotion) { suffix = QLatin1Char('?') + resource.section(QLatin1Char('?'), -1); } Xml::setXmlProperty(mltProd, QStringLiteral("resource"), realPath + suffix); if (prodId == id) { // Only set proxy property on master producer Xml::setXmlProperty(mltProd, QStringLiteral("kdenlive:proxy"), QStringLiteral("-")); } } } } if (max > 0) { // original doc was modified m_doc.documentElement().setAttribute(QStringLiteral("modified"), 1); } // Check clips with available proxies but missing original source clips max = missingSources.count(); if (max > 0) { QTreeWidgetItem *item = new QTreeWidgetItem(m_ui.treeWidget, QStringList() << i18n("Source clip")); item->setIcon(0, QIcon::fromTheme(QStringLiteral("dialog-warning"))); item->setText(1, i18n("%1 missing source clips, you can only use the proxies", max)); // item->setData(0, hashRole, e.attribute("file_hash")); item->setData(0, statusRole, SOURCEMISSING); item->setToolTip(0, i18n("Missing source clip")); for (int i = 0; i < max; ++i) { QDomElement e = missingSources.at(i).toElement(); QString realPath = Xml::getXmlProperty(e, QStringLiteral("kdenlive:originalurl")); QString id = e.attribute(QStringLiteral("id")); // Tell Kdenlive the source is missing e.setAttribute(QStringLiteral("_missingsource"), QStringLiteral("1")); QTreeWidgetItem *subitem = new QTreeWidgetItem(item, QStringList() << i18n("Source clip")); // qCDebug(KDENLIVE_LOG)<<"// Adding missing source clip: "<setIcon(0, QIcon::fromTheme(QStringLiteral("dialog-close"))); subitem->setText(1, realPath); subitem->setData(0, hashRole, Xml::getXmlProperty(e, QStringLiteral("kdenlive:file_hash"))); subitem->setData(0, sizeRole, Xml::getXmlProperty(e, QStringLiteral("kdenlive:file_size"))); subitem->setData(0, statusRole, CLIPMISSING); // int t = e.attribute("type").toInt(); subitem->setData(0, typeRole, Xml::getXmlProperty(e, QStringLiteral("mlt_service"))); subitem->setData(0, idRole, id); } } if (max > 0) { // original doc was modified m_doc.documentElement().setAttribute(QStringLiteral("modified"), 1); } m_ui.treeWidget->resizeColumnToContents(0); connect(m_ui.recursiveSearch, &QAbstractButton::pressed, this, &DocumentChecker::slotSearchClips); connect(m_ui.usePlaceholders, &QAbstractButton::pressed, this, &DocumentChecker::slotPlaceholders); connect(m_ui.removeSelected, &QAbstractButton::pressed, this, &DocumentChecker::slotDeleteSelected); connect(m_ui.treeWidget, &QTreeWidget::itemDoubleClicked, this, &DocumentChecker::slotEditItem); connect(m_ui.treeWidget, &QTreeWidget::itemSelectionChanged, this, &DocumentChecker::slotCheckButtons); // adjustSize(); if (m_ui.treeWidget->topLevelItem(0)) { m_ui.treeWidget->setCurrentItem(m_ui.treeWidget->topLevelItem(0)); } checkStatus(); int acceptMissing = m_dialog->exec(); if (acceptMissing == QDialog::Accepted) { acceptDialog(); } return (acceptMissing != QDialog::Accepted); } DocumentChecker::~DocumentChecker() { delete m_dialog; } QString DocumentChecker::getProperty(const QDomElement &effect, const QString &name) { QDomNodeList params = effect.elementsByTagName(QStringLiteral("property")); for (int i = 0; i < params.count(); ++i) { QDomElement e = params.item(i).toElement(); if (e.attribute(QStringLiteral("name")) == name) { return e.firstChild().nodeValue(); } } return QString(); } void DocumentChecker::updateProperty(const QDomElement &effect, const QString &name, const QString &value) { QDomNodeList params = effect.elementsByTagName(QStringLiteral("property")); for (int i = 0; i < params.count(); ++i) { QDomElement e = params.item(i).toElement(); if (e.attribute(QStringLiteral("name")) == name) { e.firstChild().setNodeValue(value); break; } } } void DocumentChecker::setProperty(QDomElement &effect, const QString &name, const QString &value) { QDomNodeList params = effect.elementsByTagName(QStringLiteral("property")); bool found = false; for (int i = 0; i < params.count(); ++i) { QDomElement e = params.item(i).toElement(); if (e.attribute(QStringLiteral("name")) == name) { e.firstChild().setNodeValue(value); found = true; break; } } if (!found) { // create property QDomDocument doc = effect.ownerDocument(); QDomElement e = doc.createElement(QStringLiteral("property")); e.setAttribute(QStringLiteral("name"), name); QDomText val = doc.createTextNode(value); e.appendChild(val); effect.appendChild(e); } } void DocumentChecker::slotSearchClips() { // QString clipFolder = KRecentDirs::dir(QStringLiteral(":KdenliveClipFolder")); QString clipFolder = m_url.adjusted(QUrl::RemoveFilename).toLocalFile(); QString newpath = QFileDialog::getExistingDirectory(qApp->activeWindow(), i18n("Clips folder"), clipFolder); if (newpath.isEmpty()) { return; } int ix = 0; bool fixed = false; m_ui.recursiveSearch->setChecked(true); // TODO: make non modal QTreeWidgetItem *child = m_ui.treeWidget->topLevelItem(ix); QDir searchDir(newpath); while (child != nullptr) { if (child->data(0, statusRole).toInt() == SOURCEMISSING) { for (int j = 0; j < child->childCount(); ++j) { QTreeWidgetItem *subchild = child->child(j); QString clipPath = searchFileRecursively(searchDir, subchild->data(0, sizeRole).toString(), subchild->data(0, hashRole).toString(), subchild->text(1)); if (!clipPath.isEmpty()) { fixed = true; subchild->setText(1, clipPath); subchild->setIcon(0, QIcon::fromTheme(QStringLiteral("dialog-ok"))); subchild->setData(0, statusRole, CLIPOK); } } } else if (child->data(0, statusRole).toInt() == CLIPMISSING) { bool perfectMatch = true; ClipType::ProducerType type = (ClipType::ProducerType)child->data(0, clipTypeRole).toInt(); QString clipPath; if (type != ClipType::SlideShow) { // Slideshows cannot be found with hash / size clipPath = searchFileRecursively(searchDir, child->data(0, sizeRole).toString(), child->data(0, hashRole).toString(), child->text(1)); } if (clipPath.isEmpty()) { clipPath = searchPathRecursively(searchDir, QUrl::fromLocalFile(child->text(1)).fileName(), type); perfectMatch = false; } if (!clipPath.isEmpty()) { fixed = true; child->setText(1, clipPath); child->setIcon(0, perfectMatch ? QIcon::fromTheme(QStringLiteral("dialog-ok")) : QIcon::fromTheme(QStringLiteral("dialog-warning"))); child->setData(0, statusRole, CLIPOK); } } else if (child->data(0, statusRole).toInt() == LUMAMISSING) { QString fileName = searchLuma(searchDir, child->data(0, idRole).toString()); if (!fileName.isEmpty()) { fixed = true; child->setText(1, fileName); child->setIcon(0, QIcon::fromTheme(QStringLiteral("dialog-ok"))); child->setData(0, statusRole, LUMAOK); } } else if (child->data(0, typeRole).toInt() == TITLE_IMAGE_ELEMENT && child->data(0, statusRole).toInt() == CLIPPLACEHOLDER) { // Search missing title images QString missingFileName = QUrl::fromLocalFile(child->text(1)).fileName(); QString newPath = searchPathRecursively(searchDir, missingFileName); if (!newPath.isEmpty()) { // File found fixed = true; child->setText(1, newPath); child->setIcon(0, QIcon::fromTheme(QStringLiteral("dialog-ok"))); child->setData(0, statusRole, CLIPOK); } } ix++; child = m_ui.treeWidget->topLevelItem(ix); } m_ui.recursiveSearch->setChecked(false); m_ui.recursiveSearch->setEnabled(true); if (fixed) { // original doc was modified m_doc.documentElement().setAttribute(QStringLiteral("modified"), 1); } checkStatus(); } QString DocumentChecker::searchLuma(const QDir &dir, const QString &file) const { QDir searchPath(KdenliveSettings::mltpath()); QString fname = QUrl::fromLocalFile(file).fileName(); if (file.contains(QStringLiteral("PAL"))) { searchPath.cd(QStringLiteral("../lumas/PAL")); } else { searchPath.cd(QStringLiteral("../lumas/NTSC")); } QFileInfo result(searchPath, fname); if (result.exists()) { return result.filePath(); } // try to find luma in application path searchPath.setPath(QCoreApplication::applicationDirPath()); #ifdef Q_OS_WIN searchPath.cd(QStringLiteral("data/lumas")); #else searchPath.cd(QStringLiteral("../share/apps/kdenlive/lumas")); #endif result.setFile(searchPath, fname); if (result.exists()) { return result.filePath(); } // Try in Kdenlive's standard KDE path QString res = QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("lumas/") + fname); if (!res.isEmpty()) { return res; } // Try in user's chosen folder return searchPathRecursively(dir, fname); } QString DocumentChecker::searchPathRecursively(const QDir &dir, const QString &fileName, ClipType::ProducerType type) const { QString foundFileName; QStringList filters; if (type == ClipType::SlideShow) { if (fileName.contains(QLatin1Char('%'))) { filters << fileName.section(QLatin1Char('%'), 0, -2) + QLatin1Char('*'); } else { return QString(); } } else { filters << fileName; } QDir searchDir(dir); searchDir.setNameFilters(filters); QStringList filesAndDirs = searchDir.entryList(QDir::Files | QDir::Readable); if (!filesAndDirs.isEmpty()) { // File Found if (type == ClipType::SlideShow) { return searchDir.absoluteFilePath(fileName); } return searchDir.absoluteFilePath(filesAndDirs.at(0)); } searchDir.setNameFilters(QStringList()); filesAndDirs = searchDir.entryList(QDir::Dirs | QDir::Readable | QDir::Executable | QDir::NoDotAndDotDot); for (int i = 0; i < filesAndDirs.size() && foundFileName.isEmpty(); ++i) { foundFileName = searchPathRecursively(searchDir.absoluteFilePath(filesAndDirs.at(i)), fileName, type); if (!foundFileName.isEmpty()) { break; } } return foundFileName; } QString DocumentChecker::searchFileRecursively(const QDir &dir, const QString &matchSize, const QString &matchHash, const QString &fileName) const { if (matchSize.isEmpty() && matchHash.isEmpty()) { return searchPathRecursively(dir, QUrl::fromLocalFile(fileName).fileName()); } 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 (QString::number(file.size()) == matchSize) { if (file.open(QIODevice::ReadOnly)) { /* * 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) << 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, fileName); if (!foundFileName.isEmpty()) { break; } } return foundFileName; } void DocumentChecker::slotEditItem(QTreeWidgetItem *item, int) { int t = item->data(0, typeRole).toInt(); if (t == TITLE_FONT_ELEMENT) { return; } //|| t == TITLE_IMAGE_ELEMENT) { QUrl url = KUrlRequesterDialog::getUrl(QUrl::fromLocalFile(item->text(1)), m_dialog, i18n("Enter new location for file")); if (!url.isValid()) { return; } item->setText(1, url.toLocalFile()); ClipType::ProducerType type = (ClipType::ProducerType)item->data(0, clipTypeRole).toInt(); bool fixed = false; if (type == ClipType::SlideShow && QFile::exists(url.adjusted(QUrl::RemoveFilename).toLocalFile())) { fixed = true; } if (fixed || QFile::exists(url.toLocalFile())) { item->setIcon(0, QIcon::fromTheme(QStringLiteral("dialog-ok"))); int id = item->data(0, statusRole).toInt(); if (id < 10) { item->setData(0, statusRole, CLIPOK); } else { item->setData(0, statusRole, LUMAOK); } checkStatus(); } else { item->setIcon(0, QIcon::fromTheme(QStringLiteral("dialog-close"))); int id = item->data(0, statusRole).toInt(); if (id < 10) { item->setData(0, statusRole, CLIPMISSING); } else { item->setData(0, statusRole, LUMAMISSING); } checkStatus(); } } void DocumentChecker::acceptDialog() { QDomNodeList producers = m_doc.elementsByTagName(QStringLiteral("producer")); int ix = 0; // prepare transitions QDomNodeList trans = m_doc.elementsByTagName(QStringLiteral("transition")); // prepare filters QDomNodeList filters = m_doc.elementsByTagName(QStringLiteral("filter")); // Mark document as modified m_doc.documentElement().setAttribute(QStringLiteral("modified"), 1); QTreeWidgetItem *child = m_ui.treeWidget->topLevelItem(ix); while (child != nullptr) { if (child->data(0, statusRole).toInt() == SOURCEMISSING) { for (int j = 0; j < child->childCount(); ++j) { fixSourceClipItem(child->child(j), producers); } } else { fixClipItem(child, producers, trans); } ix++; child = m_ui.treeWidget->topLevelItem(ix); } // QDialog::accept(); } void DocumentChecker::fixProxyClip(const QString &id, const QString &oldUrl, const QString &newUrl, const QDomNodeList &producers) { QDomElement e, property; QDomNodeList properties; for (int i = 0; i < producers.count(); ++i) { e = producers.item(i).toElement(); QString parentId = Xml::getXmlProperty(e, QStringLiteral("kdenlive:id")); if (parentId.isEmpty()) { // This is probably an old project file QString sourceId = e.attribute(QStringLiteral("id")); parentId = sourceId.section(QLatin1Char('_'), 0, 0); if (parentId.startsWith(QLatin1String("slowmotion"))) { parentId = parentId.section(QLatin1Char(':'), 1, 1); } } if (parentId == id) { // Fix clip QString resource = Xml::getXmlProperty(e, QStringLiteral("resource")); // TODO: Slowmmotion clips if (resource.contains(QRegExp(QStringLiteral("\\?[0-9]+\\.[0-9]+(&strobe=[0-9]+)?$")))) { // fixedResource.append(QLatin1Char('?') + resource.section(QLatin1Char('?'), -1)); } if (resource == oldUrl) { Xml::setXmlProperty(e, QStringLiteral("resource"), newUrl); } if (!Xml::getXmlProperty(e, QStringLiteral("kdenlive:proxy")).isEmpty()) { // Only set originalurl on master producer Xml::setXmlProperty(e, QStringLiteral("kdenlive:proxy"), newUrl); } } } } void DocumentChecker::fixSourceClipItem(QTreeWidgetItem *child, const QDomNodeList &producers) { QDomElement e, property; QDomNodeList properties; // int t = child->data(0, typeRole).toInt(); if (child->data(0, statusRole).toInt() == CLIPOK) { QString id = child->data(0, idRole).toString(); for (int i = 0; i < producers.count(); ++i) { e = producers.item(i).toElement(); QString parentId = Xml::getXmlProperty(e, QStringLiteral("kdenlive:id")); if (parentId.isEmpty()) { // This is probably an old project file QString sourceId = e.attribute(QStringLiteral("id")); parentId = sourceId.section(QLatin1Char('_'), 0, 0); if (parentId.startsWith(QLatin1String("slowmotion"))) { parentId = parentId.section(QLatin1Char(':'), 1, 1); } } if (parentId == id) { // Fix clip QString resource = Xml::getXmlProperty(e, QStringLiteral("resource")); QString fixedResource = child->text(1); if (resource.contains(QRegExp(QStringLiteral("\\?[0-9]+\\.[0-9]+(&strobe=[0-9]+)?$")))) { fixedResource.append(QLatin1Char('?') + resource.section(QLatin1Char('?'), -1)); } if (!Xml::getXmlProperty(e, QStringLiteral("kdenlive:originalurl")).isEmpty()) { // Only set originalurl on master producer Xml::setXmlProperty(e, QStringLiteral("kdenlive:originalurl"), fixedResource); } if (m_missingProxyIds.contains(parentId)) { // Proxy is also missing, replace resource Xml::setXmlProperty(e, QStringLiteral("resource"), fixedResource); } } } } } void DocumentChecker::fixClipItem(QTreeWidgetItem *child, const QDomNodeList &producers, const QDomNodeList &trans) { QDomElement e, property; QDomNodeList properties; int t = child->data(0, typeRole).toInt(); QString id = child->data(0, idRole).toString(); if (child->data(0, statusRole).toInt() == CLIPOK) { QString fixedResource = child->text(1); if (t == TITLE_IMAGE_ELEMENT) { // edit images embedded in titles for (int i = 0; i < producers.count(); ++i) { e = producers.item(i).toElement(); QString parentId = Xml::getXmlProperty(e, QStringLiteral("kdenlive:id")); if (parentId.isEmpty()) { // This is probably an old project file QString sourceId = e.attribute(QStringLiteral("id")); parentId = sourceId.section(QLatin1Char('_'), 0, 0); if (parentId.startsWith(QLatin1String("slowmotion"))) { parentId = parentId.section(QLatin1Char(':'), 1, 1); } } if (parentId == id) { // Fix clip properties = e.childNodes(); for (int j = 0; j < properties.count(); ++j) { property = properties.item(j).toElement(); if (property.attribute(QStringLiteral("name")) == QLatin1String("xmldata")) { QString xml = property.firstChild().nodeValue(); xml.replace(child->data(0, typeOriginalResource).toString(), fixedResource); property.firstChild().setNodeValue(xml); break; } } } } } else { // edit clip url /*for (int i = 0; i < infoproducers.count(); ++i) { e = infoproducers.item(i).toElement(); if (e.attribute("id") == id) { // Fix clip e.setAttribute("resource", child->text(1)); e.setAttribute("name", QUrl(child->text(1)).fileName()); e.removeAttribute("_missingsource"); break; } }*/ for (int i = 0; i < producers.count(); ++i) { e = producers.item(i).toElement(); if (e.attribute(QStringLiteral("id")).section(QLatin1Char('_'), 0, 0) == id || e.attribute(QStringLiteral("id")).section(QLatin1Char(':'), 1, 1) == id || e.attribute(QStringLiteral("id")) == id) { // Fix clip QString resource = getProperty(e, QStringLiteral("resource")); QString service = getProperty(e, QStringLiteral("mlt_service")); QString updatedResource = fixedResource; if (resource.contains(QRegExp(QStringLiteral("\\?[0-9]+\\.[0-9]+(&strobe=[0-9]+)?$")))) { updatedResource.append(QLatin1Char('?') + resource.section(QLatin1Char('?'), -1)); } if (service == QLatin1String("timewarp")) { updateProperty(e, QStringLiteral("warp_resource"), updatedResource); updatedResource.prepend(getProperty(e, QStringLiteral("warp_speed")) + QLatin1Char(':')); } updateProperty(e, QStringLiteral("resource"), updatedResource); } } } } else if (child->data(0, statusRole).toInt() == CLIPPLACEHOLDER && t != TITLE_FONT_ELEMENT && t != TITLE_IMAGE_ELEMENT) { // QString id = child->data(0, idRole).toString(); for (int i = 0; i < producers.count(); ++i) { e = producers.item(i).toElement(); if (e.attribute("id") == id) { // Fix clip setProperty(e, QStringLiteral("_placeholder"), QStringLiteral("1")); setProperty(e, QStringLiteral("kdenlive:orig_service"), getProperty(e, QStringLiteral("mlt_service"))); break; } } } else if (child->data(0, statusRole).toInt() == LUMAOK) { QMap lumaSearchPairs = getLumaPairs(); for (int i = 0; i < trans.count(); ++i) { QString service = getProperty(trans.at(i).toElement(), QStringLiteral("mlt_service")); QString luma; if (lumaSearchPairs.contains(service)) { luma = getProperty(trans.at(i).toElement(), lumaSearchPairs.value(service)); } if (!luma.isEmpty() && luma == child->data(0, idRole).toString()) { updateProperty(trans.at(i).toElement(), lumaSearchPairs.value(service), child->text(1)); // qCDebug(KDENLIVE_LOG) << "replace with; " << child->text(1); } } } else if (child->data(0, statusRole).toInt() == LUMAMISSING) { QMap lumaSearchPairs = getLumaPairs(); for (int i = 0; i < trans.count(); ++i) { QString service = getProperty(trans.at(i).toElement(), QStringLiteral("mlt_service")); QString luma; if (lumaSearchPairs.contains(service)) { luma = getProperty(trans.at(i).toElement(), lumaSearchPairs.value(service)); } if (!luma.isEmpty() && luma == child->data(0, idRole).toString()) { updateProperty(trans.at(i).toElement(), lumaSearchPairs.value(service), QString()); } } } } void DocumentChecker::slotPlaceholders() { int ix = 0; QTreeWidgetItem *child = m_ui.treeWidget->topLevelItem(ix); while (child != nullptr) { if (child->data(0, statusRole).toInt() == CLIPMISSING) { child->setData(0, statusRole, CLIPPLACEHOLDER); child->setIcon(0, QIcon::fromTheme(QStringLiteral("dialog-ok"))); } else if (child->data(0, statusRole).toInt() == LUMAMISSING) { child->setData(0, statusRole, LUMAPLACEHOLDER); child->setIcon(0, QIcon::fromTheme(QStringLiteral("dialog-ok"))); } ix++; child = m_ui.treeWidget->topLevelItem(ix); } checkStatus(); } void DocumentChecker::checkStatus() { bool status = true; int ix = 0; QTreeWidgetItem *child = m_ui.treeWidget->topLevelItem(ix); while (child != nullptr) { int childStatus = child->data(0, statusRole).toInt(); if (childStatus == CLIPMISSING) { status = false; break; } ix++; child = m_ui.treeWidget->topLevelItem(ix); } m_ui.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(status); } void DocumentChecker::slotDeleteSelected() { if (KMessageBox::warningContinueCancel(m_dialog, i18np("This will remove the selected clip from this project", "This will remove the selected clips from this project", m_ui.treeWidget->selectedItems().count()), i18n("Remove clips")) == KMessageBox::Cancel) { return; } QStringList deletedIds; QStringList deletedLumas; QDomNodeList playlists = m_doc.elementsByTagName(QStringLiteral("playlist")); for (QTreeWidgetItem *child : m_ui.treeWidget->selectedItems()) { int id = child->data(0, statusRole).toInt(); if (id == CLIPMISSING) { deletedIds.append(child->data(0, idRole).toString()); delete child; } else if (id == LUMAMISSING) { deletedLumas.append(child->data(0, idRole).toString()); delete child; } } if (!deletedLumas.isEmpty()) { QDomElement e; QDomNodeList transitions = m_doc.elementsByTagName(QStringLiteral("transition")); QMap lumaSearchPairs = getLumaPairs(); for (const QString &lumaPath : deletedLumas) { for (int i = 0; i < transitions.count(); ++i) { e = transitions.item(i).toElement(); QString service = Xml::getXmlProperty(e, QStringLiteral("mlt_service")); QString resource; if (lumaSearchPairs.contains(service)) { resource = getProperty(e, lumaSearchPairs.value(service)); } if (!resource.isEmpty() && resource == lumaPath) { Xml::removeXmlProperty(e, lumaSearchPairs.value(service)); } } } } if (!deletedIds.isEmpty()) { QDomElement e; QDomNodeList producers = m_doc.elementsByTagName(QStringLiteral("producer")); QDomNode mlt = m_doc.elementsByTagName(QStringLiteral("mlt")).at(0); QDomNode kdenlivedoc = m_doc.elementsByTagName(QStringLiteral("kdenlivedoc")).at(0); for (int i = 0; i < producers.count(); ++i) { e = producers.item(i).toElement(); if (deletedIds.contains(e.attribute(QStringLiteral("id")).section(QLatin1Char('_'), 0, 0)) || deletedIds.contains(e.attribute(QStringLiteral("id")).section(QLatin1Char(':'), 1, 1).section(QLatin1Char('_'), 0, 0))) { // Remove clip mlt.removeChild(e); --i; } } for (int i = 0; i < playlists.count(); ++i) { QDomNodeList entries = playlists.at(i).toElement().elementsByTagName(QStringLiteral("entry")); for (int j = 0; j < entries.count(); ++j) { e = entries.item(j).toElement(); if (deletedIds.contains(e.attribute(QStringLiteral("producer")).section(QLatin1Char('_'), 0, 0)) || deletedIds.contains(e.attribute(QStringLiteral("producer")).section(QLatin1Char(':'), 1, 1).section(QLatin1Char('_'), 0, 0))) { // Replace clip with blank while (e.childNodes().count() > 0) { e.removeChild(e.firstChild()); } e.setTagName(QStringLiteral("blank")); e.removeAttribute(QStringLiteral("producer")); int length = e.attribute(QStringLiteral("out")).toInt() - e.attribute(QStringLiteral("in")).toInt(); e.setAttribute(QStringLiteral("length"), length); j--; } } } m_doc.documentElement().setAttribute(QStringLiteral("modified"), 1); checkStatus(); } } void DocumentChecker::checkMissingImagesAndFonts(const QStringList &images, const QStringList &fonts, const QString &id, const QString &baseClip) { QDomDocument doc; for (const QString &img : images) { if (m_safeImages.contains(img)) { continue; } if (!QFile::exists(img)) { QDomElement e = doc.createElement(QStringLiteral("missingtitle")); e.setAttribute(QStringLiteral("type"), TITLE_IMAGE_ELEMENT); e.setAttribute(QStringLiteral("resource"), img); e.setAttribute(QStringLiteral("id"), id); e.setAttribute(QStringLiteral("name"), baseClip); m_missingClips.append(e); } else { m_safeImages.append(img); } } for (const QString &fontelement : fonts) { if (m_safeFonts.contains(fontelement)) { continue; } QFont f(fontelement); ////qCDebug(KDENLIVE_LOG) << "/ / / CHK FONTS: " << fontelement << " = " << QFontInfo(f).family(); if (fontelement != QFontInfo(f).family()) { m_missingFonts << fontelement; } else { m_safeFonts.append(fontelement); } } } void DocumentChecker::slotCheckButtons() { if (m_ui.treeWidget->currentItem()) { QTreeWidgetItem *item = m_ui.treeWidget->currentItem(); int t = item->data(0, typeRole).toInt(); int s = item->data(0, statusRole).toInt(); if (t == TITLE_FONT_ELEMENT || t == TITLE_IMAGE_ELEMENT || s == PROXYMISSING) { m_ui.removeSelected->setEnabled(false); } else { m_ui.removeSelected->setEnabled(true); } } } diff --git a/src/doc/documentchecker.h b/src/doc/documentchecker.h index a57575a68..5d9435276 100644 --- a/src/doc/documentchecker.h +++ b/src/doc/documentchecker.h @@ -1,85 +1,85 @@ /*************************************************************************** * 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 DOCUMENTCHECKER_H #define DOCUMENTCHECKER_H #include "definitions.h" #include "ui_missingclips_ui.h" #include #include #include class DocumentChecker : public QObject { Q_OBJECT public: - explicit DocumentChecker(const QUrl &url, const QDomDocument &doc); - ~DocumentChecker(); + explicit DocumentChecker(QUrl url, const QDomDocument &doc); + ~DocumentChecker() override; /** * @brief checks for problems with the clips in the project * Checks for missing proxies, wrong duration clips, missing fonts, missing images, missing source clips * Calls DocumentChecker::checkMissingImagesAndFonts () /n * Called by KdenliveDoc::checkDocumentClips () /n * @return */ bool hasErrorInClips(); private slots: void acceptDialog(); void slotSearchClips(); void slotEditItem(QTreeWidgetItem *item, int); void slotPlaceholders(); void slotDeleteSelected(); QString getProperty(const QDomElement &effect, const QString &name); void updateProperty(const QDomElement &effect, const QString &name, const QString &value); void setProperty(QDomElement &effect, const QString &name, const QString &value); QString searchLuma(const QDir &dir, const QString &file) const; /** @brief Check if images and fonts in this clip exists, returns a list of images that do exist so we don't check twice. */ void checkMissingImagesAndFonts(const QStringList &images, const QStringList &fonts, const QString &id, const QString &baseClip); void slotCheckButtons(); private: QUrl m_url; QDomDocument m_doc; Ui::MissingClips_UI m_ui; QDialog *m_dialog; QPair m_rootReplacement; QString searchPathRecursively(const QDir &dir, const QString &fileName, ClipType::ProducerType type = ClipType::Unknown) const; QString searchFileRecursively(const QDir &dir, const QString &matchSize, const QString &matchHash, const QString &fileName) const; void checkStatus(); QMap m_missingTitleImages; QMap m_missingTitleFonts; QList m_missingClips; QStringList m_missingFilters; QStringList m_missingFonts; QStringList m_safeImages; QStringList m_safeFonts; QStringList m_missingProxyIds; void fixClipItem(QTreeWidgetItem *child, const QDomNodeList &producers, const QDomNodeList &trans); void fixSourceClipItem(QTreeWidgetItem *child, const QDomNodeList &producers); void fixProxyClip(const QString &id, const QString &oldUrl, const QString &newUrl, const QDomNodeList &producers); /** @brief Returns list of transitions containing luma files */ QMap getLumaPairs() const; }; #endif diff --git a/src/doc/documentvalidator.cpp b/src/doc/documentvalidator.cpp index 5cb860543..554cec1df 100644 --- a/src/doc/documentvalidator.cpp +++ b/src/doc/documentvalidator.cpp @@ -1,2361 +1,2361 @@ /*************************************************************************** * 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 "documentvalidator.h" #include "bin/binplaylist.hpp" #include "core.h" #include "definitions.h" #include "effects/effectsrepository.hpp" #include "mainwindow.h" #include "transitions/transitionsrepository.hpp" #include "xml/xml.hpp" #include "kdenlive_debug.h" #include #include #include #include #include #include #include #include #include #ifdef Q_OS_MAC #include #endif #include - -DocumentValidator::DocumentValidator(const QDomDocument &doc, const QUrl &documentUrl) +#include +DocumentValidator::DocumentValidator(const QDomDocument &doc, QUrl documentUrl) : m_doc(doc) - , m_url(documentUrl) + , m_url(std::move(documentUrl)) , m_modified(false) { } bool DocumentValidator::validate(const double currentVersion) { QDomElement mlt = m_doc.firstChildElement(QStringLiteral("mlt")); // At least the root element must be there if (mlt.isNull()) { return false; } QDomElement kdenliveDoc = mlt.firstChildElement(QStringLiteral("kdenlivedoc")); QString rootDir = mlt.attribute(QStringLiteral("root")); if (rootDir == QLatin1String("$CURRENTPATH")) { // The document was extracted from a Kdenlive archived project, fix root directory QString playlist = m_doc.toString(); playlist.replace(QLatin1String("$CURRENTPATH"), m_url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).toLocalFile()); m_doc.setContent(playlist); mlt = m_doc.firstChildElement(QStringLiteral("mlt")); kdenliveDoc = mlt.firstChildElement(QStringLiteral("kdenlivedoc")); } else if (rootDir.isEmpty()) { mlt.setAttribute(QStringLiteral("root"), m_url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).toLocalFile()); } // Previous MLT / Kdenlive versions used C locale by default QLocale documentLocale = QLocale::c(); if (mlt.hasAttribute(QStringLiteral("LC_NUMERIC"))) { // Check document numeric separator (added in Kdenlive 16.12.1 QDomElement main_playlist = mlt.firstChildElement(QStringLiteral("playlist")); QDomNodeList props = main_playlist.elementsByTagName(QStringLiteral("property")); QChar numericalSeparator; for (int i = 0; i < props.count(); ++i) { QDomNode n = props.at(i); if (n.toElement().attribute(QStringLiteral("name")) == QLatin1String("kdenlive:docproperties.decimalPoint")) { QString sep = n.firstChild().nodeValue(); if (!sep.isEmpty()) { numericalSeparator = sep.at(0); } break; } } bool error = false; if (!numericalSeparator.isNull() && numericalSeparator != QLocale().decimalPoint()) { qCDebug(KDENLIVE_LOG) << " * ** LOCALE CHANGE REQUIRED: " << numericalSeparator << "!=" << QLocale().decimalPoint() << " / " << QLocale::system().decimalPoint(); // Change locale to match document QString requestedLocale = mlt.attribute(QStringLiteral("LC_NUMERIC")); documentLocale = QLocale(requestedLocale); #ifdef Q_OS_WIN // Most locales don't work on windows, so use C whenever possible if (numericalSeparator == QLatin1Char('.')) { #else if (numericalSeparator != documentLocale.decimalPoint() && numericalSeparator == QLatin1Char('.')) { #endif requestedLocale = QStringLiteral("C"); documentLocale = QLocale::c(); } #ifdef Q_OS_MAC setlocale(LC_NUMERIC_MASK, requestedLocale.toUtf8().constData()); #elif defined(Q_OS_WIN) std::locale::global(std::locale(requestedLocale.toUtf8().constData())); #else setlocale(LC_NUMERIC, requestedLocale.toUtf8().constData()); #endif if (numericalSeparator != documentLocale.decimalPoint()) { // Parse installed locales to find one matching const QList list = QLocale::matchingLocales(QLocale::AnyLanguage, QLocale().script(), QLocale::AnyCountry); QLocale matching; for (const QLocale &loc : list) { if (loc.decimalPoint() == numericalSeparator) { matching = loc; qCDebug(KDENLIVE_LOG) << "Warning, document locale: " << mlt.attribute(QStringLiteral("LC_NUMERIC")) << " is not available, using: " << loc.name(); #ifndef Q_OS_MAC setlocale(LC_NUMERIC, loc.name().toUtf8().constData()); #else setlocale(LC_NUMERIC_MASK, loc.name().toUtf8().constData()); #endif documentLocale = matching; break; } } error = numericalSeparator != documentLocale.decimalPoint(); } } else if (numericalSeparator.isNull()) { // Change locale to match document #ifndef Q_OS_MAC const QString newloc = QString::fromLatin1(setlocale(LC_NUMERIC, mlt.attribute(QStringLiteral("LC_NUMERIC")).toUtf8().constData())); #else const QString newloc = setlocale(LC_NUMERIC_MASK, mlt.attribute("LC_NUMERIC").toUtf8().constData()); #endif documentLocale = QLocale(mlt.attribute(QStringLiteral("LC_NUMERIC"))); error = newloc.isEmpty(); } else { // Document separator matching system separator documentLocale = QLocale(); } if (error) { // Requested locale not available, ask for install KMessageBox::sorry(QApplication::activeWindow(), i18n("The document was created in \"%1\" locale, which is not installed on your system. Please install that language pack. " "Until then, Kdenlive might not be able to correctly open the document.", mlt.attribute(QStringLiteral("LC_NUMERIC")))); } // Make sure Qt locale and C++ locale have the same numeric separator, might not be the case // With some locales since C++ and Qt use a different database for locales // localeconv()->decimal_point does not give reliable results on Windows #ifndef Q_OS_WIN char *separator = localeconv()->decimal_point; if (QString::fromUtf8(separator) != QString(documentLocale.decimalPoint())) { KMessageBox::sorry(QApplication::activeWindow(), i18n("There is a locale conflict on your system. The document uses locale %1 which uses a \"%2\" as numeric separator (in " "system libraries) but Qt expects \"%3\". You might not be able to correctly open the project.", mlt.attribute(QStringLiteral("LC_NUMERIC")), documentLocale.decimalPoint(), separator)); // qDebug()<<"------\n!!! system locale is not similar to Qt's locale... be prepared for bugs!!!\n------"; // HACK: There is a locale conflict, so set locale to at least have correct decimal point if (strncmp(separator, ".", 1) == 0) { documentLocale = QLocale::c(); } else if (strncmp(separator, ",", 1) == 0) { documentLocale = QLocale(QStringLiteral("fr_FR.UTF-8")); } } #endif } documentLocale.setNumberOptions(QLocale::OmitGroupSeparator); if (documentLocale.decimalPoint() != QLocale().decimalPoint()) { // If loading an older MLT file without LC_NUMERIC, set locale to C which was previously the default if (!mlt.hasAttribute(QStringLiteral("LC_NUMERIC"))) { #ifndef Q_OS_MAC setlocale(LC_NUMERIC, "C"); #else setlocale(LC_NUMERIC_MASK, "C"); #endif } QLocale::setDefault(documentLocale); if (documentLocale.decimalPoint() != QLocale().decimalPoint()) { KMessageBox::sorry(QApplication::activeWindow(), i18n("There is a locale conflict. The document uses a \"%1\" as numeric separator, but your computer is configured to use " "\"%2\". Change your computer settings or you might not be able to correctly open the project.", documentLocale.decimalPoint(), QLocale().decimalPoint())); } // locale conversion might need to be redone // TODO reload repositories /*#ifndef Q_OS_MAC initEffects::parseEffectFiles(pCore->getMltRepository(), QString::fromLatin1(setlocale(LC_NUMERIC, nullptr))); #else initEffects::parseEffectFiles(pCore->getMltRepository(), QString::fromLatin1(setlocale(LC_NUMERIC_MASK, nullptr))); #endif */ } double version = -1; if (kdenliveDoc.isNull() || !kdenliveDoc.hasAttribute(QStringLiteral("version"))) { // Newer Kdenlive document version QDomElement main = mlt.firstChildElement(QStringLiteral("playlist")); version = Xml::getXmlProperty(main, QStringLiteral("kdenlive:docproperties.version")).toDouble(); } else { bool ok; version = documentLocale.toDouble(kdenliveDoc.attribute(QStringLiteral("version")), &ok); if (!ok) { // Could not parse version number, there is probably a conflict in decimal separator QLocale tempLocale = QLocale(mlt.attribute(QStringLiteral("LC_NUMERIC"))); version = tempLocale.toDouble(kdenliveDoc.attribute(QStringLiteral("version")), &ok); if (!ok) { version = kdenliveDoc.attribute(QStringLiteral("version")).toDouble(&ok); } if (!ok) { // Last try: replace comma with a dot QString versionString = kdenliveDoc.attribute(QStringLiteral("version")); if (versionString.contains(QLatin1Char(','))) { versionString.replace(QLatin1Char(','), QLatin1Char('.')); } version = versionString.toDouble(&ok); if (!ok) { qCDebug(KDENLIVE_LOG) << "// CANNOT PARSE VERSION NUMBER, ERROR!"; } } } } // Upgrade the document to the latest version if (!upgrade(version, currentVersion)) { return false; } if (version < 0.97) { checkOrphanedProducers(); } return true; /* // Check the syntax (this will be replaced by XSD validation with Qt 4.6) // and correct some errors { // Return (or create) the tractor QDomElement tractor = mlt.firstChildElement("tractor"); if (tractor.isNull()) { m_modified = true; tractor = m_doc.createElement("tractor"); tractor.setAttribute("global_feed", "1"); tractor.setAttribute("in", "0"); tractor.setAttribute("out", "-1"); tractor.setAttribute("id", "maintractor"); mlt.appendChild(tractor); } // Make sure at least one track exists, and they're equal in number to // to the maximum between MLT and Kdenlive playlists and tracks // // In older Kdenlive project files, one playlist is not a real track (the black track), we have: track count = playlist count- 1 // In newer Qt5 Kdenlive, the Bin playlist should not appear as a track. So we should have: track count = playlist count- 2 int trackOffset = 1; QDomNodeList playlists = m_doc.elementsByTagName("playlist"); // Remove "main bin" playlist that simply holds the bin's clips and is not a real playlist for (int i = 0; i < playlists.count(); ++i) { QString playlistId = playlists.at(i).toElement().attribute("id"); if (playlistId == BinController::binPlaylistId()) { // remove pseudo-playlist //playlists.at(i).parentNode().removeChild(playlists.at(i)); trackOffset = 2; break; } } int tracksMax = playlists.count() - trackOffset; // Remove the black track and bin track QDomNodeList tracks = tractor.elementsByTagName("track"); tracksMax = qMax(tracks.count() - 1, tracksMax); QDomNodeList tracksinfo = kdenliveDoc.elementsByTagName("trackinfo"); tracksMax = qMax(tracksinfo.count(), tracksMax); tracksMax = qMax(1, tracksMax); // Force existence of one track if (playlists.count() - trackOffset < tracksMax || tracks.count() < tracksMax || tracksinfo.count() < tracksMax) { qCDebug(KDENLIVE_LOG) << "//// WARNING, PROJECT IS CORRUPTED, MISSING TRACK"; m_modified = true; int difference; // use the MLT tracks as reference if (tracks.count() - 1 < tracksMax) { // Looks like one MLT track is missing, remove the extra Kdenlive track if there is one. if (tracksinfo.count() != tracks.count() - 1) { // The Kdenlive tracks are not ok, clear and rebuild them QDomNode tinfo = kdenliveDoc.firstChildElement("tracksinfo"); QDomNode tnode = tinfo.firstChild(); while (!tnode.isNull()) { tinfo.removeChild(tnode); tnode = tinfo.firstChild(); } for (int i = 1; i < tracks.count(); ++i) { QString hide = tracks.at(i).toElement().attribute("hide"); QDomElement newTrack = m_doc.createElement("trackinfo"); if (hide == "video") { // audio track; newTrack.setAttribute("type", "audio"); newTrack.setAttribute("blind", 1); newTrack.setAttribute("mute", 0); newTrack.setAttribute("lock", 0); } else { newTrack.setAttribute("blind", 0); newTrack.setAttribute("mute", 0); newTrack.setAttribute("lock", 0); } tinfo.appendChild(newTrack); } } } if (playlists.count() - 1 < tracksMax) { difference = tracksMax - (playlists.count() - 1); for (int i = 0; i < difference; ++i) { QDomElement playlist = m_doc.createElement("playlist"); mlt.appendChild(playlist); } } if (tracks.count() - 1 < tracksMax) { difference = tracksMax - (tracks.count() - 1); for (int i = 0; i < difference; ++i) { QDomElement track = m_doc.createElement("track"); tractor.appendChild(track); } } if (tracksinfo.count() < tracksMax) { QDomElement tracksinfoElm = kdenliveDoc.firstChildElement("tracksinfo"); if (tracksinfoElm.isNull()) { tracksinfoElm = m_doc.createElement("tracksinfo"); kdenliveDoc.appendChild(tracksinfoElm); } difference = tracksMax - tracksinfo.count(); for (int i = 0; i < difference; ++i) { QDomElement trackinfo = m_doc.createElement("trackinfo"); trackinfo.setAttribute("mute", "0"); trackinfo.setAttribute("locked", "0"); tracksinfoElm.appendChild(trackinfo); } } } // TODO: check the tracks references // TODO: check internal mix transitions } updateEffects(); return true; */ } bool DocumentValidator::upgrade(double version, const double currentVersion) { qCDebug(KDENLIVE_LOG) << "Opening a document with version " << version << " / " << currentVersion; // No conversion needed if (qFuzzyCompare(version, currentVersion)) { return true; } // The document is too new if (version > currentVersion) { // qCDebug(KDENLIVE_LOG) << "Unable to open document with version " << version; KMessageBox::sorry( QApplication::activeWindow(), i18n("This project type is unsupported (version %1) and can't be loaded.\nPlease consider upgrading your Kdenlive version.", version), i18n("Unable to open project")); return false; } // Unsupported document versions if (qFuzzyCompare(version, 0.5) || qFuzzyCompare(version, 0.7)) { // 0.7 is unsupported // qCDebug(KDENLIVE_LOG) << "Unable to open document with version " << version; KMessageBox::sorry(QApplication::activeWindow(), i18n("This project type is unsupported (version %1) and can't be loaded.", version), i18n("Unable to open project")); return false; } // QDomNode infoXmlNode; QDomElement infoXml; QDomNodeList docs = m_doc.elementsByTagName(QStringLiteral("kdenlivedoc")); if (!docs.isEmpty()) { infoXmlNode = m_doc.elementsByTagName(QStringLiteral("kdenlivedoc")).at(0); infoXml = infoXmlNode.toElement(); infoXml.setAttribute(QStringLiteral("upgraded"), 1); } m_doc.documentElement().setAttribute(QStringLiteral("upgraded"), 1); if (version <= 0.6) { QDomElement infoXml_old = infoXmlNode.cloneNode(true).toElement(); // Needed for folders QDomNode westley = m_doc.elementsByTagName(QStringLiteral("westley")).at(1); QDomNode tractor = m_doc.elementsByTagName(QStringLiteral("tractor")).at(0); QDomNode multitrack = m_doc.elementsByTagName(QStringLiteral("multitrack")).at(0); QDomNodeList playlists = m_doc.elementsByTagName(QStringLiteral("playlist")); QDomNode props = m_doc.elementsByTagName(QStringLiteral("properties")).at(0).toElement(); QString profile = props.toElement().attribute(QStringLiteral("videoprofile")); int startPos = props.toElement().attribute(QStringLiteral("timeline_position")).toInt(); if (profile == QLatin1String("dv_wide")) { profile = QStringLiteral("dv_pal_wide"); } // move playlists outside of tractor and add the tracks instead int max = playlists.count(); if (westley.isNull()) { westley = m_doc.createElement(QStringLiteral("westley")); m_doc.documentElement().appendChild(westley); } if (tractor.isNull()) { // qCDebug(KDENLIVE_LOG) << "// NO MLT PLAYLIST, building empty one"; QDomElement blank_tractor = m_doc.createElement(QStringLiteral("tractor")); westley.appendChild(blank_tractor); QDomElement blank_playlist = m_doc.createElement(QStringLiteral("playlist")); blank_playlist.setAttribute(QStringLiteral("id"), QStringLiteral("black_track")); westley.insertBefore(blank_playlist, QDomNode()); QDomElement blank_track = m_doc.createElement(QStringLiteral("track")); blank_track.setAttribute(QStringLiteral("producer"), QStringLiteral("black_track")); blank_tractor.appendChild(blank_track); QDomNodeList kdenlivetracks = m_doc.elementsByTagName(QStringLiteral("kdenlivetrack")); for (int i = 0; i < kdenlivetracks.count(); ++i) { blank_playlist = m_doc.createElement(QStringLiteral("playlist")); blank_playlist.setAttribute(QStringLiteral("id"), QStringLiteral("playlist") + QString::number(i)); westley.insertBefore(blank_playlist, QDomNode()); blank_track = m_doc.createElement(QStringLiteral("track")); blank_track.setAttribute(QStringLiteral("producer"), QStringLiteral("playlist") + QString::number(i)); blank_tractor.appendChild(blank_track); if (kdenlivetracks.at(i).toElement().attribute(QStringLiteral("cliptype")) == QLatin1String("Sound")) { blank_playlist.setAttribute(QStringLiteral("hide"), QStringLiteral("video")); blank_track.setAttribute(QStringLiteral("hide"), QStringLiteral("video")); } } } else for (int i = 0; i < max; ++i) { QDomNode n = playlists.at(i); westley.insertBefore(n, QDomNode()); QDomElement pl = n.toElement(); QDomElement track = m_doc.createElement(QStringLiteral("track")); QString trackType = pl.attribute(QStringLiteral("hide")); if (!trackType.isEmpty()) { track.setAttribute(QStringLiteral("hide"), trackType); } QString playlist_id = pl.attribute(QStringLiteral("id")); if (playlist_id.isEmpty()) { playlist_id = QStringLiteral("black_track"); pl.setAttribute(QStringLiteral("id"), playlist_id); } track.setAttribute(QStringLiteral("producer"), playlist_id); // tractor.appendChild(track); #define KEEP_TRACK_ORDER 1 #ifdef KEEP_TRACK_ORDER tractor.insertAfter(track, QDomNode()); #else // Insert the new track in an order that hopefully matches the 3 video, then 2 audio tracks of Kdenlive 0.7.0 // insertion sort - O( tracks*tracks ) // Note, this breaks _all_ transitions - but you can move them up and down afterwards. QDomElement tractor_elem = tractor.toElement(); if (!tractor_elem.isNull()) { QDomNodeList tracks = tractor_elem.elementsByTagName("track"); int size = tracks.size(); if (size == 0) { tractor.insertAfter(track, QDomNode()); } else { bool inserted = false; for (int i = 0; i < size; ++i) { QDomElement track_elem = tracks.at(i).toElement(); if (track_elem.isNull()) { tractor.insertAfter(track, QDomNode()); inserted = true; break; } else { // qCDebug(KDENLIVE_LOG) << "playlist_id: " << playlist_id << " producer:" << track_elem.attribute("producer"); if (playlist_id < track_elem.attribute("producer")) { tractor.insertBefore(track, track_elem); inserted = true; break; } } } // Reach here, no insertion, insert last if (!inserted) { tractor.insertAfter(track, QDomNode()); } } } else { qCWarning(KDENLIVE_LOG) << "tractor was not a QDomElement"; tractor.insertAfter(track, QDomNode()); } #endif } tractor.removeChild(multitrack); // audio track mixing transitions should not be added to track view, so add required attribute QDomNodeList transitions = m_doc.elementsByTagName(QStringLiteral("transition")); max = transitions.count(); for (int i = 0; i < max; ++i) { QDomElement tr = transitions.at(i).toElement(); if (tr.attribute(QStringLiteral("combine")) == QLatin1String("1") && tr.attribute(QStringLiteral("mlt_service")) == QLatin1String("mix")) { QDomElement property = m_doc.createElement(QStringLiteral("property")); property.setAttribute(QStringLiteral("name"), QStringLiteral("internal_added")); QDomText value = m_doc.createTextNode(QStringLiteral("237")); property.appendChild(value); tr.appendChild(property); property = m_doc.createElement(QStringLiteral("property")); property.setAttribute(QStringLiteral("name"), QStringLiteral("mlt_service")); value = m_doc.createTextNode(QStringLiteral("mix")); property.appendChild(value); tr.appendChild(property); } else { // convert transition QDomNamedNodeMap attrs = tr.attributes(); for (int j = 0; j < attrs.count(); ++j) { QString attrName = attrs.item(j).nodeName(); if (attrName != QLatin1String("in") && attrName != QLatin1String("out") && attrName != QLatin1String("id")) { QDomElement property = m_doc.createElement(QStringLiteral("property")); property.setAttribute(QStringLiteral("name"), attrName); QDomText value = m_doc.createTextNode(attrs.item(j).nodeValue()); property.appendChild(value); tr.appendChild(property); } } } } // move transitions after tracks for (int i = 0; i < max; ++i) { tractor.insertAfter(transitions.at(0), QDomNode()); } // Fix filters format QDomNodeList entries = m_doc.elementsByTagName(QStringLiteral("entry")); max = entries.count(); for (int i = 0; i < max; ++i) { QString last_id; int effectix = 0; QDomNode m = entries.at(i).firstChild(); while (!m.isNull()) { if (m.toElement().tagName() == QLatin1String("filter")) { QDomElement filt = m.toElement(); QDomNamedNodeMap attrs = filt.attributes(); QString current_id = filt.attribute(QStringLiteral("kdenlive_id")); if (current_id != last_id) { effectix++; last_id = current_id; } QDomElement e = m_doc.createElement(QStringLiteral("property")); e.setAttribute(QStringLiteral("name"), QStringLiteral("kdenlive_ix")); QDomText value = m_doc.createTextNode(QString::number(effectix)); e.appendChild(value); filt.appendChild(e); for (int j = 0; j < attrs.count(); ++j) { QDomAttr a = attrs.item(j).toAttr(); if (!a.isNull()) { // qCDebug(KDENLIVE_LOG) << " FILTER; adding :" << a.name() << ':' << a.value(); auto property = m_doc.createElement(QStringLiteral("property")); property.setAttribute(QStringLiteral("name"), a.name()); auto property_value = m_doc.createTextNode(a.value()); property.appendChild(property_value); filt.appendChild(property); } } } m = m.nextSibling(); } } /* QDomNodeList filters = m_doc.elementsByTagName("filter"); max = filters.count(); QString last_id; int effectix = 0; for (int i = 0; i < max; ++i) { QDomElement filt = filters.at(i).toElement(); QDomNamedNodeMap attrs = filt.attributes(); QString current_id = filt.attribute("kdenlive_id"); if (current_id != last_id) { effectix++; last_id = current_id; } QDomElement e = m_doc.createElement("property"); e.setAttribute("name", "kdenlive_ix"); QDomText value = m_doc.createTextNode(QString::number(1)); e.appendChild(value); filt.appendChild(e); for (int j = 0; j < attrs.count(); ++j) { QDomAttr a = attrs.item(j).toAttr(); if (!a.isNull()) { //qCDebug(KDENLIVE_LOG) << " FILTER; adding :" << a.name() << ':' << a.value(); QDomElement e = m_doc.createElement("property"); e.setAttribute("name", a.name()); QDomText value = m_doc.createTextNode(a.value()); e.appendChild(value); filt.appendChild(e); } } }*/ // fix slowmotion QDomNodeList producers = westley.toElement().elementsByTagName(QStringLiteral("producer")); max = producers.count(); for (int i = 0; i < max; ++i) { QDomElement prod = producers.at(i).toElement(); if (prod.attribute(QStringLiteral("mlt_service")) == QLatin1String("framebuffer")) { QString slowmotionprod = prod.attribute(QStringLiteral("resource")); slowmotionprod.replace(QLatin1Char(':'), QLatin1Char('?')); // qCDebug(KDENLIVE_LOG) << "// FOUND WRONG SLOWMO, new: " << slowmotionprod; prod.setAttribute(QStringLiteral("resource"), slowmotionprod); } } // move producers to correct place, markers to a global list, fix clip descriptions QDomElement markers = m_doc.createElement(QStringLiteral("markers")); // This will get the xml producers: producers = m_doc.elementsByTagName(QStringLiteral("producer")); max = producers.count(); for (int i = 0; i < max; ++i) { QDomElement prod = producers.at(0).toElement(); // add resource also as a property (to allow path correction in setNewResource()) // TODO: will it work with slowmotion? needs testing /*if (!prod.attribute("resource").isEmpty()) { QDomElement prop_resource = m_doc.createElement("property"); prop_resource.setAttribute("name", "resource"); QDomText resource = m_doc.createTextNode(prod.attribute("resource")); prop_resource.appendChild(resource); prod.appendChild(prop_resource); }*/ QDomNode m = prod.firstChild(); if (!m.isNull()) { if (m.toElement().tagName() == QLatin1String("markers")) { QDomNodeList prodchilds = m.childNodes(); int maxchild = prodchilds.count(); for (int k = 0; k < maxchild; ++k) { QDomElement mark = prodchilds.at(0).toElement(); mark.setAttribute(QStringLiteral("id"), prod.attribute(QStringLiteral("id"))); markers.insertAfter(mark, QDomNode()); } prod.removeChild(m); } else if (prod.attribute(QStringLiteral("type")).toInt() == (int)ClipType::Text) { // convert title clip if (m.toElement().tagName() == QLatin1String("textclip")) { QDomDocument tdoc; QDomElement titleclip = m.toElement(); QDomElement title = tdoc.createElement(QStringLiteral("kdenlivetitle")); tdoc.appendChild(title); QDomNodeList objects = titleclip.childNodes(); int maxchild = objects.count(); for (int k = 0; k < maxchild; ++k) { QDomElement ob = objects.at(k).toElement(); if (ob.attribute(QStringLiteral("type")) == QLatin1String("3")) { // text object - all of this goes into "xmldata"... QDomElement item = tdoc.createElement(QStringLiteral("item")); item.setAttribute(QStringLiteral("z-index"), ob.attribute(QStringLiteral("z"))); item.setAttribute(QStringLiteral("type"), QStringLiteral("QGraphicsTextItem")); QDomElement position = tdoc.createElement(QStringLiteral("position")); position.setAttribute(QStringLiteral("x"), ob.attribute(QStringLiteral("x"))); position.setAttribute(QStringLiteral("y"), ob.attribute(QStringLiteral("y"))); QDomElement content = tdoc.createElement(QStringLiteral("content")); content.setAttribute(QStringLiteral("font"), ob.attribute(QStringLiteral("font_family"))); content.setAttribute(QStringLiteral("font-size"), ob.attribute(QStringLiteral("font_size"))); content.setAttribute(QStringLiteral("font-bold"), ob.attribute(QStringLiteral("bold"))); content.setAttribute(QStringLiteral("font-italic"), ob.attribute(QStringLiteral("italic"))); content.setAttribute(QStringLiteral("font-underline"), ob.attribute(QStringLiteral("underline"))); QString col = ob.attribute(QStringLiteral("color")); QColor c(col); content.setAttribute(QStringLiteral("font-color"), colorToString(c)); // todo: These fields are missing from the newly generated xmldata: // transform, startviewport, endviewport, background QDomText conttxt = tdoc.createTextNode(ob.attribute(QStringLiteral("text"))); content.appendChild(conttxt); item.appendChild(position); item.appendChild(content); title.appendChild(item); } else if (ob.attribute(QStringLiteral("type")) == QLatin1String("5")) { // rectangle object QDomElement item = tdoc.createElement(QStringLiteral("item")); item.setAttribute(QStringLiteral("z-index"), ob.attribute(QStringLiteral("z"))); item.setAttribute(QStringLiteral("type"), QStringLiteral("QGraphicsRectItem")); QDomElement position = tdoc.createElement(QStringLiteral("position")); position.setAttribute(QStringLiteral("x"), ob.attribute(QStringLiteral("x"))); position.setAttribute(QStringLiteral("y"), ob.attribute(QStringLiteral("y"))); QDomElement content = tdoc.createElement(QStringLiteral("content")); QString col = ob.attribute(QStringLiteral("color")); QColor c(col); content.setAttribute(QStringLiteral("brushcolor"), colorToString(c)); QString rect = QStringLiteral("0,0,"); rect.append(ob.attribute(QStringLiteral("width"))); rect.append(QLatin1String(",")); rect.append(ob.attribute(QStringLiteral("height"))); content.setAttribute(QStringLiteral("rect"), rect); item.appendChild(position); item.appendChild(content); title.appendChild(item); } } prod.setAttribute(QStringLiteral("xmldata"), tdoc.toString()); // mbd todo: This clearly does not work, as every title gets the same name - trying to leave it empty // QStringList titleInfo = TitleWidget::getFreeTitleInfo(projectFolder()); // prod.setAttribute("titlename", titleInfo.at(0)); // prod.setAttribute("resource", titleInfo.at(1)); ////qCDebug(KDENLIVE_LOG)<<"TITLE DATA:\n"< 0) { prod.setAttribute(QStringLiteral("out"), QString::number(duration)); } // The clip goes back in, but text clips should not go back in, at least not modified westley.insertBefore(prod, QDomNode()); } QDomNode westley0 = m_doc.elementsByTagName(QStringLiteral("westley")).at(0); if (!markers.firstChild().isNull()) { westley0.appendChild(markers); } /* * Convert as much of the kdenlivedoc as possible. Use the producer in * westley. First, remove the old stuff from westley, and add a new * empty one. Also, track the max id in order to use it for the adding * of groups/folders */ int max_kproducer_id = 0; westley0.removeChild(infoXmlNode); QDomElement infoXml_new = m_doc.createElement(QStringLiteral("kdenlivedoc")); infoXml_new.setAttribute(QStringLiteral("profile"), profile); infoXml.setAttribute(QStringLiteral("position"), startPos); // Add all the producers that has a resource in westley QDomElement westley_element = westley0.toElement(); if (westley_element.isNull()) { qCWarning(KDENLIVE_LOG) << "westley0 element in document was not a QDomElement - unable to add producers to new kdenlivedoc"; } else { QDomNodeList wproducers = westley_element.elementsByTagName(QStringLiteral("producer")); int kmax = wproducers.count(); for (int i = 0; i < kmax; ++i) { QDomElement wproducer = wproducers.at(i).toElement(); if (wproducer.isNull()) { qCWarning(KDENLIVE_LOG) << "Found producer in westley0, that was not a QDomElement"; continue; } if (wproducer.attribute(QStringLiteral("id")) == QLatin1String("black")) { continue; } // We have to do slightly different things, depending on the type // qCDebug(KDENLIVE_LOG) << "Converting producer element with type" << wproducer.attribute("type"); if (wproducer.attribute(QStringLiteral("type")).toInt() == (int)ClipType::Text) { // qCDebug(KDENLIVE_LOG) << "Found TEXT element in producer" << endl; QDomElement kproducer = wproducer.cloneNode(true).toElement(); kproducer.setTagName(QStringLiteral("kdenlive_producer")); infoXml_new.appendChild(kproducer); /* * TODO: Perhaps needs some more changes here to * "frequency", aspect ratio as a float, frame_size, * channels, and later, resource and title name */ } else { QDomElement kproducer = m_doc.createElement(QStringLiteral("kdenlive_producer")); kproducer.setAttribute(QStringLiteral("id"), wproducer.attribute(QStringLiteral("id"))); if (!wproducer.attribute(QStringLiteral("description")).isEmpty()) { kproducer.setAttribute(QStringLiteral("description"), wproducer.attribute(QStringLiteral("description"))); } kproducer.setAttribute(QStringLiteral("resource"), wproducer.attribute(QStringLiteral("resource"))); kproducer.setAttribute(QStringLiteral("type"), wproducer.attribute(QStringLiteral("type"))); // Testing fix for 358 if (!wproducer.attribute(QStringLiteral("aspect_ratio")).isEmpty()) { kproducer.setAttribute(QStringLiteral("aspect_ratio"), wproducer.attribute(QStringLiteral("aspect_ratio"))); } if (!wproducer.attribute(QStringLiteral("source_fps")).isEmpty()) { kproducer.setAttribute(QStringLiteral("fps"), wproducer.attribute(QStringLiteral("source_fps"))); } if (!wproducer.attribute(QStringLiteral("length")).isEmpty()) { kproducer.setAttribute(QStringLiteral("duration"), wproducer.attribute(QStringLiteral("length"))); } infoXml_new.appendChild(kproducer); } if (wproducer.attribute(QStringLiteral("id")).toInt() > max_kproducer_id) { max_kproducer_id = wproducer.attribute(QStringLiteral("id")).toInt(); } } } #define LOOKUP_FOLDER 1 #ifdef LOOKUP_FOLDER /* * Look through all the folder elements of the old doc, for each folder, * for each producer, get the id, look it up in the new doc, set the * groupname and groupid. Note, this does not work at the moment - at * least one folder shows up missing, and clips with no folder does not * show up. */ // QDomElement infoXml_old = infoXmlNode.toElement(); if (!infoXml_old.isNull()) { QDomNodeList folders = infoXml_old.elementsByTagName(QStringLiteral("folder")); int fsize = folders.size(); int groupId = max_kproducer_id + 1; // Start at +1 of max id of the kdenlive_producers for (int i = 0; i < fsize; ++i) { QDomElement folder = folders.at(i).toElement(); if (!folder.isNull()) { QString groupName = folder.attribute(QStringLiteral("name")); // qCDebug(KDENLIVE_LOG) << "groupName: " << groupName << " with groupId: " << groupId; QDomNodeList fproducers = folder.elementsByTagName(QStringLiteral("producer")); int psize = fproducers.size(); for (int j = 0; j < psize; ++j) { QDomElement fproducer = fproducers.at(j).toElement(); if (!fproducer.isNull()) { QString id = fproducer.attribute(QStringLiteral("id")); // This is not very effective, but compared to loading the clips, its a breeze QDomNodeList kdenlive_producers = infoXml_new.elementsByTagName(QStringLiteral("kdenlive_producer")); int kpsize = kdenlive_producers.size(); for (int k = 0; k < kpsize; ++k) { QDomElement kproducer = kdenlive_producers.at(k).toElement(); // Its an element for sure if (id == kproducer.attribute(QStringLiteral("id"))) { // We do not check that it already is part of a folder kproducer.setAttribute(QStringLiteral("groupid"), groupId); kproducer.setAttribute(QStringLiteral("groupname"), groupName); break; } } } } ++groupId; } } } #endif QDomNodeList elements = westley.childNodes(); max = elements.count(); for (int i = 0; i < max; ++i) { QDomElement prod = elements.at(0).toElement(); westley0.insertAfter(prod, QDomNode()); } westley0.appendChild(infoXml_new); westley0.removeChild(westley); // adds information to QDomNodeList kproducers = m_doc.elementsByTagName(QStringLiteral("kdenlive_producer")); QDomNodeList avfiles = infoXml_old.elementsByTagName(QStringLiteral("avfile")); // qCDebug(KDENLIVE_LOG) << "found" << avfiles.count() << "s and" << kproducers.count() << "s"; for (int i = 0; i < avfiles.count(); ++i) { QDomElement avfile = avfiles.at(i).toElement(); QDomElement kproducer; if (avfile.isNull()) { qCWarning(KDENLIVE_LOG) << "found an that is not a QDomElement"; } else { QString id = avfile.attribute(QStringLiteral("id")); // this is horrible, must be rewritten, it's just for test for (int j = 0; j < kproducers.count(); ++j) { ////qCDebug(KDENLIVE_LOG) << "checking with id" << kproducers.at(j).toElement().attribute("id"); if (kproducers.at(j).toElement().attribute(QStringLiteral("id")) == id) { kproducer = kproducers.at(j).toElement(); break; } } if (kproducer == QDomElement()) { qCWarning(KDENLIVE_LOG) << "no match for with id =" << id; } else { ////qCDebug(KDENLIVE_LOG) << "ready to set additional 's attributes (id =" << id << ')'; kproducer.setAttribute(QStringLiteral("channels"), avfile.attribute(QStringLiteral("channels"))); kproducer.setAttribute(QStringLiteral("duration"), avfile.attribute(QStringLiteral("duration"))); kproducer.setAttribute(QStringLiteral("frame_size"), avfile.attribute(QStringLiteral("width")) + QLatin1Char('x') + avfile.attribute(QStringLiteral("height"))); kproducer.setAttribute(QStringLiteral("frequency"), avfile.attribute(QStringLiteral("frequency"))); if (kproducer.attribute(QStringLiteral("description")).isEmpty() && !avfile.attribute(QStringLiteral("description")).isEmpty()) { kproducer.setAttribute(QStringLiteral("description"), avfile.attribute(QStringLiteral("description"))); } } } } infoXml = infoXml_new; } if (version <= 0.81) { // Add the tracks information QString tracksOrder = infoXml.attribute(QStringLiteral("tracks")); if (tracksOrder.isEmpty()) { QDomNodeList tracks = m_doc.elementsByTagName(QStringLiteral("track")); for (int i = 0; i < tracks.count(); ++i) { QDomElement track = tracks.at(i).toElement(); if (track.attribute(QStringLiteral("producer")) != QLatin1String("black_track")) { if (track.attribute(QStringLiteral("hide")) == QLatin1String("video")) { tracksOrder.append(QLatin1Char('a')); } else { tracksOrder.append(QLatin1Char('v')); } } } } QDomElement tracksinfo = m_doc.createElement(QStringLiteral("tracksinfo")); for (int i = 0; i < tracksOrder.size(); ++i) { QDomElement trackinfo = m_doc.createElement(QStringLiteral("trackinfo")); if (tracksOrder.data()[i] == QLatin1Char('a')) { trackinfo.setAttribute(QStringLiteral("type"), QStringLiteral("audio")); trackinfo.setAttribute(QStringLiteral("blind"), 1); } else { trackinfo.setAttribute(QStringLiteral("blind"), 0); } trackinfo.setAttribute(QStringLiteral("mute"), 0); trackinfo.setAttribute(QStringLiteral("locked"), 0); tracksinfo.appendChild(trackinfo); } infoXml.appendChild(tracksinfo); } if (version <= 0.82) { // Convert s in s (MLT extreme makeover) QDomNodeList westleyNodes = m_doc.elementsByTagName(QStringLiteral("westley")); for (int i = 0; i < westleyNodes.count(); ++i) { QDomElement westley = westleyNodes.at(i).toElement(); westley.setTagName(QStringLiteral("mlt")); } } if (version <= 0.83) { // Replace point size with pixel size in text titles if (m_doc.toString().contains(QStringLiteral("font-size"))) { KMessageBox::ButtonCode convert = KMessageBox::Continue; QDomNodeList kproducerNodes = m_doc.elementsByTagName(QStringLiteral("kdenlive_producer")); for (int i = 0; i < kproducerNodes.count() && convert != KMessageBox::No; ++i) { QDomElement kproducer = kproducerNodes.at(i).toElement(); if (kproducer.attribute(QStringLiteral("type")).toInt() == (int)ClipType::Text) { QDomDocument data; data.setContent(kproducer.attribute(QStringLiteral("xmldata"))); QDomNodeList items = data.firstChild().childNodes(); for (int j = 0; j < items.count() && convert != KMessageBox::No; ++j) { if (items.at(j).attributes().namedItem(QStringLiteral("type")).nodeValue() == QLatin1String("QGraphicsTextItem")) { QDomNamedNodeMap textProperties = items.at(j).namedItem(QStringLiteral("content")).attributes(); if (textProperties.namedItem(QStringLiteral("font-pixel-size")).isNull() && !textProperties.namedItem(QStringLiteral("font-size")).isNull()) { // Ask the user if he wants to convert if (convert != KMessageBox::Yes && convert != KMessageBox::No) { convert = (KMessageBox::ButtonCode)KMessageBox::warningYesNo( QApplication::activeWindow(), i18n("Some of your text clips were saved with size in points, which means different sizes on different displays. Do " "you want to convert them to pixel size, making them portable? It is recommended you do this on the computer they " "were first created on, or you could have to adjust their size."), i18n("Update Text Clips")); } if (convert == KMessageBox::Yes) { QFont font; font.setPointSize(textProperties.namedItem(QStringLiteral("font-size")).nodeValue().toInt()); QDomElement content = items.at(j).namedItem(QStringLiteral("content")).toElement(); content.setAttribute(QStringLiteral("font-pixel-size"), QFontInfo(font).pixelSize()); content.removeAttribute(QStringLiteral("font-size")); kproducer.setAttribute(QStringLiteral("xmldata"), data.toString()); /* * You may be tempted to delete the preview file * to force its recreation: bad idea (see * http://www.kdenlive.org/mantis/view.php?id=749) */ } } } } } } } // Fill the element QDomElement docProperties = infoXml.firstChildElement(QStringLiteral("documentproperties")); if (docProperties.isNull()) { docProperties = m_doc.createElement(QStringLiteral("documentproperties")); docProperties.setAttribute(QStringLiteral("zonein"), infoXml.attribute(QStringLiteral("zonein"))); docProperties.setAttribute(QStringLiteral("zoneout"), infoXml.attribute(QStringLiteral("zoneout"))); docProperties.setAttribute(QStringLiteral("zoom"), infoXml.attribute(QStringLiteral("zoom"))); docProperties.setAttribute(QStringLiteral("position"), infoXml.attribute(QStringLiteral("position"))); infoXml.appendChild(docProperties); } } if (version <= 0.84) { // update the title clips to use the new MLT kdenlivetitle producer QDomNodeList kproducerNodes = m_doc.elementsByTagName(QStringLiteral("kdenlive_producer")); for (int i = 0; i < kproducerNodes.count(); ++i) { QDomElement kproducer = kproducerNodes.at(i).toElement(); if (kproducer.attribute(QStringLiteral("type")).toInt() == (int)ClipType::Text) { QString data = kproducer.attribute(QStringLiteral("xmldata")); QString datafile = kproducer.attribute(QStringLiteral("resource")); if (!datafile.endsWith(QLatin1String(".kdenlivetitle"))) { datafile = QString(); kproducer.setAttribute(QStringLiteral("resource"), QString()); } QString id = kproducer.attribute(QStringLiteral("id")); QDomNodeList mltproducers = m_doc.elementsByTagName(QStringLiteral("producer")); bool foundData = false; bool foundResource = false; bool foundService = false; for (int j = 0; j < mltproducers.count(); ++j) { QDomElement wproducer = mltproducers.at(j).toElement(); if (wproducer.attribute(QStringLiteral("id")) == id) { QDomNodeList props = wproducer.childNodes(); for (int k = 0; k < props.count(); ++k) { if (props.at(k).toElement().attribute(QStringLiteral("name")) == QLatin1String("xmldata")) { props.at(k).firstChild().setNodeValue(data); foundData = true; } else if (props.at(k).toElement().attribute(QStringLiteral("name")) == QLatin1String("mlt_service")) { props.at(k).firstChild().setNodeValue(QStringLiteral("kdenlivetitle")); foundService = true; } else if (props.at(k).toElement().attribute(QStringLiteral("name")) == QLatin1String("resource")) { props.at(k).firstChild().setNodeValue(datafile); foundResource = true; } } if (!foundData) { QDomElement e = m_doc.createElement(QStringLiteral("property")); e.setAttribute(QStringLiteral("name"), QStringLiteral("xmldata")); QDomText value = m_doc.createTextNode(data); e.appendChild(value); wproducer.appendChild(e); } if (!foundService) { QDomElement e = m_doc.createElement(QStringLiteral("property")); e.setAttribute(QStringLiteral("name"), QStringLiteral("mlt_service")); QDomText value = m_doc.createTextNode(QStringLiteral("kdenlivetitle")); e.appendChild(value); wproducer.appendChild(e); } if (!foundResource) { QDomElement e = m_doc.createElement(QStringLiteral("property")); e.setAttribute(QStringLiteral("name"), QStringLiteral("resource")); QDomText value = m_doc.createTextNode(datafile); e.appendChild(value); wproducer.appendChild(e); } break; } } } } } if (version <= 0.85) { // update the LADSPA effects to use the new ladspa.id format instead of external xml file QDomNodeList effectNodes = m_doc.elementsByTagName(QStringLiteral("filter")); for (int i = 0; i < effectNodes.count(); ++i) { QDomElement effect = effectNodes.at(i).toElement(); if (Xml::getXmlProperty(effect, QStringLiteral("mlt_service")) == QLatin1String("ladspa")) { // Needs to be converted QStringList info = getInfoFromEffectName(Xml::getXmlProperty(effect, QStringLiteral("kdenlive_id"))); if (info.isEmpty()) { continue; } // info contains the correct ladspa.id from kdenlive effect name, and a list of parameter's old and new names Xml::setXmlProperty(effect, QStringLiteral("kdenlive_id"), info.at(0)); Xml::setXmlProperty(effect, QStringLiteral("tag"), info.at(0)); Xml::setXmlProperty(effect, QStringLiteral("mlt_service"), info.at(0)); Xml::removeXmlProperty(effect, QStringLiteral("src")); for (int j = 1; j < info.size(); ++j) { QString value = Xml::getXmlProperty(effect, info.at(j).section(QLatin1Char('='), 0, 0)); if (!value.isEmpty()) { // update parameter name Xml::renameXmlProperty(effect, info.at(j).section(QLatin1Char('='), 0, 0), info.at(j).section(QLatin1Char('='), 1, 1)); } } } } } if (version <= 0.86) { // Make sure we don't have avformat-novalidate producers, since it caused crashes QDomNodeList producers = m_doc.elementsByTagName(QStringLiteral("producer")); int max = producers.count(); for (int i = 0; i < max; ++i) { QDomElement prod = producers.at(i).toElement(); if (Xml::getXmlProperty(prod, QStringLiteral("mlt_service")) == QLatin1String("avformat-novalidate")) { Xml::setXmlProperty(prod, QStringLiteral("mlt_service"), QStringLiteral("avformat")); } } // There was a mistake in Geometry transitions where the last keyframe was created one frame after the end of transition, so fix it and move last // keyframe to real end of transition // Get profile info (width / height) int profileWidth; int profileHeight; QDomElement profile = m_doc.firstChildElement(QStringLiteral("profile")); if (profile.isNull()) { profile = infoXml.firstChildElement(QStringLiteral("profileinfo")); if (!profile.isNull()) { // old MLT format, we need to add profile QDomNode mlt = m_doc.firstChildElement(QStringLiteral("mlt")); QDomNode firstProd = m_doc.firstChildElement(QStringLiteral("producer")); QDomElement pr = profile.cloneNode().toElement(); pr.setTagName(QStringLiteral("profile")); mlt.insertBefore(pr, firstProd); } } if (profile.isNull()) { // could not find profile info, set PAL profileWidth = 720; profileHeight = 576; } else { profileWidth = profile.attribute(QStringLiteral("width")).toInt(); profileHeight = profile.attribute(QStringLiteral("height")).toInt(); } QDomNodeList transitions = m_doc.elementsByTagName(QStringLiteral("transition")); max = transitions.count(); for (int i = 0; i < max; ++i) { QDomElement trans = transitions.at(i).toElement(); int out = trans.attribute(QStringLiteral("out")).toInt() - trans.attribute(QStringLiteral("in")).toInt(); QString geom = Xml::getXmlProperty(trans, QStringLiteral("geometry")); Mlt::Geometry *g = new Mlt::Geometry(geom.toUtf8().data(), out, profileWidth, profileHeight); Mlt::GeometryItem item; if (g->next_key(&item, out) == 0) { // We have a keyframe just after last frame, try to move it to last frame if (item.frame() == out + 1) { item.frame(out); g->insert(item); g->remove(out + 1); Xml::setXmlProperty(trans, QStringLiteral("geometry"), QString::fromLatin1(g->serialise())); } } delete g; } } if (version <= 0.87) { if (!m_doc.firstChildElement(QStringLiteral("mlt")).hasAttribute(QStringLiteral("LC_NUMERIC"))) { m_doc.firstChildElement(QStringLiteral("mlt")).setAttribute(QStringLiteral("LC_NUMERIC"), QStringLiteral("C")); } } if (version <= 0.88) { // convert to new MLT-only format QDomNodeList producers = m_doc.elementsByTagName(QStringLiteral("producer")); QDomDocumentFragment frag = m_doc.createDocumentFragment(); // Create Bin Playlist QDomElement main_playlist = m_doc.createElement(QStringLiteral("playlist")); QDomElement prop = m_doc.createElement(QStringLiteral("property")); prop.setAttribute(QStringLiteral("name"), QStringLiteral("xml_retain")); QDomText val = m_doc.createTextNode(QStringLiteral("1")); prop.appendChild(val); main_playlist.appendChild(prop); // Move markers QDomNodeList markers = m_doc.elementsByTagName(QStringLiteral("marker")); for (int i = 0; i < markers.count(); ++i) { QDomElement marker = markers.at(i).toElement(); QDomElement property = m_doc.createElement(QStringLiteral("property")); property.setAttribute(QStringLiteral("name"), QStringLiteral("kdenlive:marker.") + marker.attribute(QStringLiteral("id")) + QLatin1Char(':') + marker.attribute(QStringLiteral("time"))); QDomText val_node = m_doc.createTextNode(marker.attribute(QStringLiteral("type")) + QLatin1Char(':') + marker.attribute(QStringLiteral("comment"))); property.appendChild(val_node); main_playlist.appendChild(property); } // Move guides QDomNodeList guides = m_doc.elementsByTagName(QStringLiteral("guide")); for (int i = 0; i < guides.count(); ++i) { QDomElement guide = guides.at(i).toElement(); QDomElement property = m_doc.createElement(QStringLiteral("property")); property.setAttribute(QStringLiteral("name"), QStringLiteral("kdenlive:guide.") + guide.attribute(QStringLiteral("time"))); QDomText val_node = m_doc.createTextNode(guide.attribute(QStringLiteral("comment"))); property.appendChild(val_node); main_playlist.appendChild(property); } // Move folders QDomNodeList folders = m_doc.elementsByTagName(QStringLiteral("folder")); for (int i = 0; i < folders.count(); ++i) { QDomElement folder = folders.at(i).toElement(); QDomElement property = m_doc.createElement(QStringLiteral("property")); property.setAttribute(QStringLiteral("name"), QStringLiteral("kdenlive:folder.-1.") + folder.attribute(QStringLiteral("id"))); QDomText val_node = m_doc.createTextNode(folder.attribute(QStringLiteral("name"))); property.appendChild(val_node); main_playlist.appendChild(property); } QDomNode mlt = m_doc.firstChildElement(QStringLiteral("mlt")); main_playlist.setAttribute(QStringLiteral("id"), BinPlaylist::binPlaylistId); mlt.toElement().setAttribute(QStringLiteral("producer"), BinPlaylist::binPlaylistId); QStringList ids; QStringList slowmotionIds; QDomNode firstProd = m_doc.firstChildElement(QStringLiteral("producer")); QDomNodeList kdenlive_producers = m_doc.elementsByTagName(QStringLiteral("kdenlive_producer")); // Rename all track producers to correct name: "id_playlistName" instead of "id_trackNumber" QMap trackRenaming; // Create a list of which producers / track on which the producer is QMap playlistForId; QDomNodeList entries = m_doc.elementsByTagName(QStringLiteral("entry")); for (int i = 0; i < entries.count(); i++) { QDomElement entry = entries.at(i).toElement(); QString entryId = entry.attribute(QStringLiteral("producer")); if (entryId == QLatin1String("black")) { continue; } bool audioOnlyProducer = false; if (trackRenaming.contains(entryId)) { // rename entry.setAttribute(QStringLiteral("producer"), trackRenaming.value(entryId)); continue; } if (entryId.endsWith(QLatin1String("_video"))) { // Video only producers are not track aware continue; } if (entryId.endsWith(QLatin1String("_audio"))) { // Audio only producer audioOnlyProducer = true; entryId = entryId.section(QLatin1Char('_'), 0, -2); } if (!entryId.contains(QLatin1Char('_'))) { // not a track producer playlistForId.insert(entryId, entry.parentNode().toElement().attribute(QStringLiteral("id"))); continue; } if (entryId.startsWith(QLatin1String("slowmotion:"))) { // Check broken slowmotion producers (they should not be track aware) QString newId = QStringLiteral("slowmotion:") + entryId.section(QLatin1Char(':'), 1, 1).section(QLatin1Char('_'), 0, 0) + QLatin1Char(':') + entryId.section(QLatin1Char(':'), 2); trackRenaming.insert(entryId, newId); entry.setAttribute(QStringLiteral("producer"), newId); continue; } QString track = entryId.section(QLatin1Char('_'), 1, 1); QString playlistId = entry.parentNode().toElement().attribute(QStringLiteral("id")); if (track == playlistId) { continue; } QString newId = entryId.section(QLatin1Char('_'), 0, 0) + QLatin1Char('_') + playlistId; if (audioOnlyProducer) { newId.append(QStringLiteral("_audio")); trackRenaming.insert(entryId + QStringLiteral("_audio"), newId); } else { trackRenaming.insert(entryId, newId); } entry.setAttribute(QStringLiteral("producer"), newId); } if (!trackRenaming.isEmpty()) { for (int i = 0; i < producers.count(); ++i) { QDomElement prod = producers.at(i).toElement(); QString id = prod.attribute(QStringLiteral("id")); if (trackRenaming.contains(id)) { prod.setAttribute(QStringLiteral("id"), trackRenaming.value(id)); } } } // Create easily searchable index of original producers QMap m_source_producers; for (int j = 0; j < kdenlive_producers.count(); j++) { QDomElement prod = kdenlive_producers.at(j).toElement(); QString id = prod.attribute(QStringLiteral("id")); m_source_producers.insert(id, prod); } for (int i = 0; i < producers.count(); ++i) { QDomElement prod = producers.at(i).toElement(); QString id = prod.attribute(QStringLiteral("id")); if (id == QLatin1String("black")) { continue; } if (id.startsWith(QLatin1String("slowmotion"))) { // No need to process slowmotion producers QString slowmo = id.section(QLatin1Char(':'), 1, 1).section(QLatin1Char('_'), 0, 0); if (!slowmotionIds.contains(slowmo)) { slowmotionIds << slowmo; } continue; } QString prodId = id.section(QLatin1Char('_'), 0, 0); if (ids.contains(prodId)) { // Make sure we didn't create a duplicate if (ids.contains(id)) { // we have a duplicate, check if this needs to be a track producer QString service = Xml::getXmlProperty(prod, QStringLiteral("mlt_service")); int a_ix = Xml::getXmlProperty(prod, QStringLiteral("audio_index")).toInt(); if (service == QLatin1String("xml") || service == QLatin1String("consumer") || (service.contains(QStringLiteral("avformat")) && a_ix != -1)) { // This should be a track producer, rename QString newId = id + QLatin1Char('_') + playlistForId.value(id); prod.setAttribute(QStringLiteral("id"), newId); for (int j = 0; j < entries.count(); j++) { QDomElement entry = entries.at(j).toElement(); QString entryId = entry.attribute(QStringLiteral("producer")); if (entryId == id) { entry.setAttribute(QStringLiteral("producer"), newId); } } } else { // This is a duplicate, remove mlt.removeChild(prod); i--; } } // Already processed, continue continue; } if (id == prodId) { // This is an original producer, move it to the main playlist QDomElement entry = m_doc.createElement(QStringLiteral("entry")); entry.setAttribute(QStringLiteral("producer"), id); main_playlist.appendChild(entry); QString service = Xml::getXmlProperty(prod, QStringLiteral("mlt_service")); if (service == QLatin1String("kdenlivetitle")) { fixTitleProducerLocale(prod); } QDomElement source = m_source_producers.value(id); if (!source.isNull()) { updateProducerInfo(prod, source); entry.setAttribute(QStringLiteral("in"), QStringLiteral("0")); entry.setAttribute(QStringLiteral("out"), QString::number(source.attribute(QStringLiteral("duration")).toInt() - 1)); } frag.appendChild(prod); // Changing prod parent removes it from list, so rewind index i--; } else { QDomElement originalProd = prod.cloneNode().toElement(); originalProd.setAttribute(QStringLiteral("id"), prodId); if (id.endsWith(QLatin1String("_audio"))) { Xml::removeXmlProperty(originalProd, QStringLiteral("video_index")); } else if (id.endsWith(QLatin1String("_video"))) { Xml::removeXmlProperty(originalProd, QStringLiteral("audio_index")); } QDomElement source = m_source_producers.value(prodId); QDomElement entry = m_doc.createElement(QStringLiteral("entry")); if (!source.isNull()) { updateProducerInfo(originalProd, source); entry.setAttribute(QStringLiteral("in"), QStringLiteral("0")); entry.setAttribute(QStringLiteral("out"), QString::number(source.attribute(QStringLiteral("duration")).toInt() - 1)); } frag.appendChild(originalProd); entry.setAttribute(QStringLiteral("producer"), prodId); main_playlist.appendChild(entry); } ids.append(prodId); } // Make sure to include producers that were not in timeline for (int j = 0; j < kdenlive_producers.count(); j++) { QDomElement prod = kdenlive_producers.at(j).toElement(); QString id = prod.attribute(QStringLiteral("id")); if (!ids.contains(id)) { // Clip was not in timeline, create it QDomElement originalProd = prod.cloneNode().toElement(); originalProd.setTagName(QStringLiteral("producer")); Xml::setXmlProperty(originalProd, QStringLiteral("resource"), originalProd.attribute(QStringLiteral("resource"))); updateProducerInfo(originalProd, prod); originalProd.removeAttribute(QStringLiteral("proxy")); originalProd.removeAttribute(QStringLiteral("type")); originalProd.removeAttribute(QStringLiteral("file_hash")); originalProd.removeAttribute(QStringLiteral("file_size")); originalProd.removeAttribute(QStringLiteral("frame_size")); originalProd.removeAttribute(QStringLiteral("zone_out")); originalProd.removeAttribute(QStringLiteral("zone_in")); originalProd.removeAttribute(QStringLiteral("name")); originalProd.removeAttribute(QStringLiteral("type")); originalProd.removeAttribute(QStringLiteral("duration")); originalProd.removeAttribute(QStringLiteral("cutzones")); int type = prod.attribute(QStringLiteral("type")).toInt(); QString mltService; switch (type) { case 4: mltService = QStringLiteral("colour"); break; case 5: case 7: mltService = QStringLiteral("qimage"); break; case 6: mltService = QStringLiteral("kdenlivetitle"); break; case 9: mltService = QStringLiteral("xml"); break; default: mltService = QStringLiteral("avformat"); break; } Xml::setXmlProperty(originalProd, QStringLiteral("mlt_service"), mltService); Xml::setXmlProperty(originalProd, QStringLiteral("mlt_type"), QStringLiteral("producer")); QDomElement entry = m_doc.createElement(QStringLiteral("entry")); entry.setAttribute(QStringLiteral("in"), QStringLiteral("0")); entry.setAttribute(QStringLiteral("out"), QString::number(prod.attribute(QStringLiteral("duration")).toInt() - 1)); entry.setAttribute(QStringLiteral("producer"), id); main_playlist.appendChild(entry); if (type == 6) { fixTitleProducerLocale(originalProd); } frag.appendChild(originalProd); ids << id; } } // Set clip folders for (int j = 0; j < kdenlive_producers.count(); j++) { QDomElement prod = kdenlive_producers.at(j).toElement(); QString id = prod.attribute(QStringLiteral("id")); QString folder = prod.attribute(QStringLiteral("groupid")); QDomNodeList mlt_producers = frag.childNodes(); for (int k = 0; k < mlt_producers.count(); k++) { QDomElement mltprod = mlt_producers.at(k).toElement(); if (mltprod.tagName() != QLatin1String("producer")) { continue; } if (mltprod.attribute(QStringLiteral("id")) == id) { if (!folder.isEmpty()) { // We have found our producer, set folder info QDomElement property = m_doc.createElement(QStringLiteral("property")); property.setAttribute(QStringLiteral("name"), QStringLiteral("kdenlive:folderid")); QDomText val_node = m_doc.createTextNode(folder); property.appendChild(val_node); mltprod.appendChild(property); } break; } } } // Make sure all slowmotion producers have a master clip for (int i = 0; i < slowmotionIds.count(); i++) { const QString &slo = slowmotionIds.at(i); if (!ids.contains(slo)) { // rebuild producer from Kdenlive's old xml format for (int j = 0; j < kdenlive_producers.count(); j++) { QDomElement prod = kdenlive_producers.at(j).toElement(); QString id = prod.attribute(QStringLiteral("id")); if (id == slo) { // We found the kdenlive_producer, build MLT producer QDomElement original = m_doc.createElement(QStringLiteral("producer")); original.setAttribute(QStringLiteral("in"), 0); original.setAttribute(QStringLiteral("out"), prod.attribute(QStringLiteral("duration")).toInt() - 1); original.setAttribute(QStringLiteral("id"), id); QDomElement property = m_doc.createElement(QStringLiteral("property")); property.setAttribute(QStringLiteral("name"), QStringLiteral("resource")); QDomText val_node = m_doc.createTextNode(prod.attribute(QStringLiteral("resource"))); property.appendChild(val_node); original.appendChild(property); QDomElement prop2 = m_doc.createElement(QStringLiteral("property")); prop2.setAttribute(QStringLiteral("name"), QStringLiteral("mlt_service")); QDomText val2 = m_doc.createTextNode(QStringLiteral("avformat")); prop2.appendChild(val2); original.appendChild(prop2); QDomElement prop3 = m_doc.createElement(QStringLiteral("property")); prop3.setAttribute(QStringLiteral("name"), QStringLiteral("length")); QDomText val3 = m_doc.createTextNode(prod.attribute(QStringLiteral("duration"))); prop3.appendChild(val3); original.appendChild(prop3); QDomElement entry = m_doc.createElement(QStringLiteral("entry")); entry.setAttribute(QStringLiteral("in"), original.attribute(QStringLiteral("in"))); entry.setAttribute(QStringLiteral("out"), original.attribute(QStringLiteral("out"))); entry.setAttribute(QStringLiteral("producer"), id); main_playlist.appendChild(entry); frag.appendChild(original); ids << slo; break; } } } } frag.appendChild(main_playlist); mlt.insertBefore(frag, firstProd); } if (version < 0.91) { // Migrate track properties QDomNode mlt = m_doc.firstChildElement(QStringLiteral("mlt")); QDomNodeList old_tracks = m_doc.elementsByTagName(QStringLiteral("trackinfo")); QDomNodeList tracks = m_doc.elementsByTagName(QStringLiteral("track")); QDomNodeList playlists = m_doc.elementsByTagName(QStringLiteral("playlist")); for (int i = 0; i < old_tracks.count(); i++) { QString playlistName = tracks.at(i + 1).toElement().attribute(QStringLiteral("producer")); // find playlist for track QDomElement trackPlaylist; for (int j = 0; j < playlists.count(); j++) { if (playlists.at(j).toElement().attribute(QStringLiteral("id")) == playlistName) { trackPlaylist = playlists.at(j).toElement(); break; } } if (!trackPlaylist.isNull()) { QDomElement kdenliveTrack = old_tracks.at(i).toElement(); if (kdenliveTrack.attribute(QStringLiteral("type")) == QLatin1String("audio")) { Xml::setXmlProperty(trackPlaylist, QStringLiteral("kdenlive:audio_track"), QStringLiteral("1")); } if (kdenliveTrack.attribute(QStringLiteral("locked")) == QLatin1String("1")) { Xml::setXmlProperty(trackPlaylist, QStringLiteral("kdenlive:locked_track"), QStringLiteral("1")); } Xml::setXmlProperty(trackPlaylist, QStringLiteral("kdenlive:track_name"), kdenliveTrack.attribute(QStringLiteral("trackname"))); } } // Find bin playlist playlists = m_doc.elementsByTagName(QStringLiteral("playlist")); QDomElement playlist; for (int i = 0; i < playlists.count(); i++) { if (playlists.at(i).toElement().attribute(QStringLiteral("id")) == BinPlaylist::binPlaylistId) { playlist = playlists.at(i).toElement(); break; } } // Migrate document notes QDomNodeList notesList = m_doc.elementsByTagName(QStringLiteral("documentnotes")); if (!notesList.isEmpty()) { QDomElement notes_elem = notesList.at(0).toElement(); QString notes = notes_elem.firstChild().nodeValue(); Xml::setXmlProperty(playlist, QStringLiteral("kdenlive:documentnotes"), notes); } // Migrate clip groups QDomNodeList groupElement = m_doc.elementsByTagName(QStringLiteral("groups")); if (!groupElement.isEmpty()) { QDomElement groups = groupElement.at(0).toElement(); QDomDocument d2; d2.importNode(groups, true); Xml::setXmlProperty(playlist, QStringLiteral("kdenlive:clipgroups"), d2.toString()); } // Migrate custom effects QDomNodeList effectsElement = m_doc.elementsByTagName(QStringLiteral("customeffects")); if (!effectsElement.isEmpty()) { QDomElement effects = effectsElement.at(0).toElement(); QDomDocument d2; d2.importNode(effects, true); Xml::setXmlProperty(playlist, QStringLiteral("kdenlive:customeffects"), d2.toString()); } Xml::setXmlProperty(playlist, QStringLiteral("kdenlive:docproperties.version"), QString::number(currentVersion)); if (!infoXml.isNull()) { Xml::setXmlProperty(playlist, QStringLiteral("kdenlive:docproperties.projectfolder"), infoXml.attribute(QStringLiteral("projectfolder"))); } // Remove deprecated Kdenlive extra info from xml doc before sending it to MLT QDomElement docXml = mlt.firstChildElement(QStringLiteral("kdenlivedoc")); if (!docXml.isNull()) { mlt.removeChild(docXml); } } if (version < 0.92) { // Luma transition used for wipe is deprecated, we now use a composite, convert QDomNodeList transitionList = m_doc.elementsByTagName(QStringLiteral("transition")); QDomElement trans; for (int i = 0; i < transitionList.count(); i++) { trans = transitionList.at(i).toElement(); QString id = Xml::getXmlProperty(trans, QStringLiteral("kdenlive_id")); if (id == QLatin1String("luma")) { Xml::setXmlProperty(trans, QStringLiteral("kdenlive_id"), QStringLiteral("wipe")); Xml::setXmlProperty(trans, QStringLiteral("mlt_service"), QStringLiteral("composite")); bool reverse = Xml::getXmlProperty(trans, QStringLiteral("reverse")).toInt() != 0; Xml::setXmlProperty(trans, QStringLiteral("luma_invert"), Xml::getXmlProperty(trans, QStringLiteral("invert"))); Xml::setXmlProperty(trans, QStringLiteral("luma"), Xml::getXmlProperty(trans, QStringLiteral("resource"))); Xml::removeXmlProperty(trans, QStringLiteral("invert")); Xml::removeXmlProperty(trans, QStringLiteral("reverse")); Xml::removeXmlProperty(trans, QStringLiteral("resource")); if (reverse) { Xml::setXmlProperty(trans, QStringLiteral("geometry"), QStringLiteral("0%/0%:100%x100%:100;-1=0%/0%:100%x100%:0")); } else { Xml::setXmlProperty(trans, QStringLiteral("geometry"), QStringLiteral("0%/0%:100%x100%:0;-1=0%/0%:100%x100%:100")); } Xml::setXmlProperty(trans, QStringLiteral("aligned"), QStringLiteral("0")); Xml::setXmlProperty(trans, QStringLiteral("fill"), QStringLiteral("1")); } } } if (version < 0.93) { // convert old keyframe filters to animated // these filters were "animated" by adding several instance of the filter, each one having a start and end tag. // We convert by parsing the start and end tags vor values and adding all to the new animated parameter QMap keyframeFilterToConvert; keyframeFilterToConvert.insert(QStringLiteral("volume"), QStringList() << QStringLiteral("gain") << QStringLiteral("end") << QStringLiteral("level")); keyframeFilterToConvert.insert(QStringLiteral("brightness"), QStringList() << QStringLiteral("start") << QStringLiteral("end") << QStringLiteral("level")); QDomNodeList entries = m_doc.elementsByTagName(QStringLiteral("entry")); for (int i = 0; i < entries.count(); i++) { QDomNode entry = entries.at(i); QDomNodeList effects = entry.toElement().elementsByTagName(QStringLiteral("filter")); QStringList parsedIds; for (int j = 0; j < effects.count(); j++) { QDomElement eff = effects.at(j).toElement(); QString id = Xml::getXmlProperty(eff, QStringLiteral("kdenlive_id")); if (keyframeFilterToConvert.contains(id) && !parsedIds.contains(id)) { parsedIds << id; QMap values; QStringList conversionParams = keyframeFilterToConvert.value(id); int offset = eff.attribute(QStringLiteral("in")).toInt(); int out = eff.attribute(QStringLiteral("out")).toInt(); convertKeyframeEffect(eff, conversionParams, values, offset); Xml::removeXmlProperty(eff, conversionParams.at(0)); Xml::removeXmlProperty(eff, conversionParams.at(1)); for (int k = j + 1; k < effects.count(); k++) { QDomElement subEffect = effects.at(k).toElement(); QString subId = Xml::getXmlProperty(subEffect, QStringLiteral("kdenlive_id")); if (subId == id) { convertKeyframeEffect(subEffect, conversionParams, values, offset); out = subEffect.attribute(QStringLiteral("out")).toInt(); entry.removeChild(subEffect); k--; } } QStringList parsedValues; QLocale locale; QMapIterator l(values); if (id == QLatin1String("volume")) { // convert old volume range (0-300) to new dB values (-60-60) while (l.hasNext()) { l.next(); double v = l.value(); if (v <= 0) { v = -60; } else { v = log10(v) * 20; } parsedValues << QString::number(l.key()) + QLatin1Char('=') + locale.toString(v); } } else { while (l.hasNext()) { l.next(); parsedValues << QString::number(l.key()) + QLatin1Char('=') + locale.toString(l.value()); } } Xml::setXmlProperty(eff, conversionParams.at(2), parsedValues.join(QLatin1Char(';'))); // Xml::setXmlProperty(eff, QStringLiteral("kdenlive:sync_in_out"), QStringLiteral("1")); eff.setAttribute(QStringLiteral("out"), out); } } } } if (version < 0.94) { // convert slowmotion effects/producers QDomNodeList producers = m_doc.elementsByTagName(QStringLiteral("producer")); int max = producers.count(); QStringList slowmoIds; for (int i = 0; i < max; ++i) { QDomElement prod = producers.at(i).toElement(); QString id = prod.attribute(QStringLiteral("id")); if (id.startsWith(QLatin1String("slowmotion"))) { QString service = Xml::getXmlProperty(prod, QStringLiteral("mlt_service")); if (service == QLatin1String("framebuffer")) { // convert to new timewarp producer prod.setAttribute(QStringLiteral("id"), id + QStringLiteral(":1")); slowmoIds << id; Xml::setXmlProperty(prod, QStringLiteral("mlt_service"), QStringLiteral("timewarp")); QString resource = Xml::getXmlProperty(prod, QStringLiteral("resource")); Xml::setXmlProperty(prod, QStringLiteral("warp_resource"), resource.section(QLatin1Char('?'), 0, 0)); Xml::setXmlProperty(prod, QStringLiteral("warp_speed"), resource.section(QLatin1Char('?'), 1).section(QLatin1Char(':'), 0, 0)); Xml::setXmlProperty(prod, QStringLiteral("resource"), resource.section(QLatin1Char('?'), 1) + QLatin1Char(':') + resource.section(QLatin1Char('?'), 0, 0)); Xml::setXmlProperty(prod, QStringLiteral("audio_index"), QStringLiteral("-1")); } } } if (!slowmoIds.isEmpty()) { producers = m_doc.elementsByTagName(QStringLiteral("entry")); max = producers.count(); for (int i = 0; i < max; ++i) { QDomElement prod = producers.at(i).toElement(); QString entryId = prod.attribute(QStringLiteral("producer")); if (slowmoIds.contains(entryId)) { prod.setAttribute(QStringLiteral("producer"), entryId + QStringLiteral(":1")); } } } // qCDebug(KDENLIVE_LOG)<<"------------------------\n"< markersList; for (int i = 0; i < props.count(); ++i) { QDomNode n = props.at(i); QString prop = n.toElement().attribute(QStringLiteral("name")); if (prop.startsWith(QLatin1String("kdenlive:guide."))) { // Process guide double guidePos = prop.section(QLatin1Char('.'), 1).toDouble(); QJsonObject currentGuide; currentGuide.insert(QStringLiteral("pos"), QJsonValue(GenTime(guidePos).frames(pCore->getCurrentFps()))); currentGuide.insert(QStringLiteral("comment"), QJsonValue(n.firstChild().nodeValue())); currentGuide.insert(QStringLiteral("type"), QJsonValue(0)); // Clear entry in old format n.toElement().setAttribute(QStringLiteral("name"), QStringLiteral("_")); guidesList.push_back(currentGuide); } else if (prop.startsWith(QLatin1String("kdenlive:marker."))) { // Process marker double markerPos = prop.section(QLatin1Char(':'), -1).toDouble(); QString markerBinClip = prop.section(QLatin1Char('.'), 1).section(QLatin1Char(':'), 0, 0); QString markerData = n.firstChild().nodeValue(); int markerType = markerData.section(QLatin1Char(':'), 0, 0).toInt(); QString markerComment = markerData.section(QLatin1Char(':'), 1); QJsonObject currentMarker; currentMarker.insert(QStringLiteral("pos"), QJsonValue(GenTime(markerPos).frames(pCore->getCurrentFps()))); currentMarker.insert(QStringLiteral("comment"), QJsonValue(markerComment)); currentMarker.insert(QStringLiteral("type"), QJsonValue(markerType)); // Clear entry in old format n.toElement().setAttribute(QStringLiteral("name"), QStringLiteral("_")); if (markersList.contains(markerBinClip)) { // we already have a marker list for this clip QJsonArray markerList = markersList.value(markerBinClip); markerList.push_back(currentMarker); markersList.insert(markerBinClip, markerList); } else { QJsonArray markerList; markerList.push_back(currentMarker); markersList.insert(markerBinClip, markerList); } } } if (!guidesList.isEmpty()) { QJsonDocument json(guidesList); Xml::setXmlProperty(main_playlist, QStringLiteral("kdenlive:docproperties.guides"), json.toJson()); } // Update producers QDomNodeList producers = m_doc.elementsByTagName(QStringLiteral("producer")); int max = producers.count(); for (int i = 0; i < max; ++i) { QDomElement prod = producers.at(i).toElement(); if (prod.isNull()) continue; // Move to new kdenlive:id format const QString id = prod.attribute(QStringLiteral("id")).section(QLatin1Char('_'), 0, 0); Xml::setXmlProperty(prod, QStringLiteral("kdenlive:id"), id); if (markersList.contains(id)) { QJsonDocument json(markersList.value(id)); Xml::setXmlProperty(prod, QStringLiteral("kdenlive:markers"), json.toJson()); } // Check image sequences with buggy begin frame number const QString service = Xml::getXmlProperty(prod, QStringLiteral("mlt_service")); if (service == QLatin1String("pixbuf") || service == QLatin1String("qimage")) { QString resource = Xml::getXmlProperty(prod, QStringLiteral("resource")); if (resource.contains(QStringLiteral("?begin:"))) { resource.replace(QStringLiteral("?begin:"), QStringLiteral("?begin=")); Xml::setXmlProperty(prod, QStringLiteral("resource"), resource); } } } } if (version < 0.98) { // rename main bin playlist, create extra tracks for old type AV clips, port groups to JSon QJsonArray newGroups; QDomNodeList playlists = m_doc.elementsByTagName(QStringLiteral("playlist")); QDomNodeList masterProducers = m_doc.elementsByTagName(QStringLiteral("producer")); QDomElement playlist; QDomNode mainplaylist; QDomNode mlt = m_doc.firstChildElement(QStringLiteral("mlt")); QDomNode tractor = mlt.firstChildElement(QStringLiteral("tractor")); // Build start trackIndex QMap trackIndex; QDomNodeList tracks = tractor.toElement().elementsByTagName(QStringLiteral("track")); for (int i = 0; i < tracks.count(); i++) { trackIndex.insert(QString::number(i), tracks.at(i).toElement().attribute(QStringLiteral("producer"))); } int trackOffset = 0; // AV clips are not supported anymore. Check if we have some and add extra audio tracks if necessary // Update the main bin name as well to be xml compliant for (int i = 0; i < playlists.count(); i++) { if (playlists.at(i).toElement().attribute(QStringLiteral("id")) == QLatin1String("main bin")) { playlists.at(i).toElement().setAttribute(QStringLiteral("id"), BinPlaylist::binPlaylistId); mainplaylist = playlists.at(i); QString oldGroups = Xml::getXmlProperty(mainplaylist.toElement(), QStringLiteral("kdenlive:clipgroups")); QDomDocument groupsDoc; groupsDoc.setContent(oldGroups); QDomNodeList groups = groupsDoc.elementsByTagName(QStringLiteral("group")); for (int g = 0; g < groups.count(); g++) { QDomNodeList elements = groups.at(g).childNodes(); QJsonArray array; for (int h = 0; h < elements.count(); h++) { QJsonObject item; item.insert(QLatin1String("type"), QJsonValue(QStringLiteral("Leaf"))); item.insert(QLatin1String("leaf"), QJsonValue(QLatin1String("clip"))); QString pos = elements.at(h).toElement().attribute(QStringLiteral("position")); QString track = trackIndex.value(elements.at(h).toElement().attribute(QStringLiteral("track"))); item.insert(QLatin1String("data"), QJsonValue(QString("%1:%2").arg(track).arg(pos))); array.push_back(item); } QJsonObject currentGroup; currentGroup.insert(QLatin1String("type"), QJsonValue(QStringLiteral("Normal"))); currentGroup.insert(QLatin1String("children"), array); newGroups.push_back(currentGroup); } } else { if (Xml::getXmlProperty(playlists.at(i).toElement(), QStringLiteral("kdenlive:audio_track")) == QLatin1String("1")) { // Audio track, no need to process continue; } const QString playlistName = playlists.at(i).toElement().attribute(QStringLiteral("id")); QDomElement duplicate_playlist = m_doc.createElement(QStringLiteral("playlist")); duplicate_playlist.setAttribute(QStringLiteral("id"), QString("%1_duplicate").arg(playlistName)); QDomElement pltype = m_doc.createElement(QStringLiteral("property")); pltype.setAttribute(QStringLiteral("name"), QStringLiteral("kdenlive:audio_track")); pltype.setNodeValue(QStringLiteral("1")); QDomText value1 = m_doc.createTextNode(QStringLiteral("1")); pltype.appendChild(value1); duplicate_playlist.appendChild(pltype); QDomElement plname = m_doc.createElement(QStringLiteral("property")); plname.setAttribute(QStringLiteral("name"), QStringLiteral("kdenlive:track_name")); QDomText value = m_doc.createTextNode(i18n("extra audio")); plname.appendChild(value); duplicate_playlist.appendChild(plname); QDomNodeList producers = playlists.at(i).childNodes(); bool duplicationRequested = false; int pos = 0; for (int j = 0; j < producers.count(); j++) { if (producers.at(j).nodeName() == QLatin1String("blank")) { // blank, duplicate duplicate_playlist.appendChild(producers.at(j).cloneNode()); pos += producers.at(j).toElement().attribute(QStringLiteral("length")).toInt(); } else if (producers.at(j).nodeName() == QLatin1String("filter")) { // effect, duplicate duplicate_playlist.appendChild(producers.at(j).cloneNode()); } else if (producers.at(j).nodeName() != QLatin1String("entry")) { // property node, pass continue; } else if (producers.at(j).toElement().attribute(QStringLiteral("producer")).endsWith(playlistName)) { // This is an AV clip // Check master properties bool hasAudio = true; bool hasVideo = true; const QString currentId = producers.at(j).toElement().attribute(QStringLiteral("producer")); int in = producers.at(j).toElement().attribute(QStringLiteral("in")).toInt(); int out = producers.at(j).toElement().attribute(QStringLiteral("out")).toInt(); for (int k = 0; k < masterProducers.count(); k++) { if (masterProducers.at(k).toElement().attribute(QStringLiteral("id")) == currentId) { hasVideo = Xml::getXmlProperty(masterProducers.at(k).toElement(), QStringLiteral("video_index")) != QLatin1String("-1"); hasAudio = Xml::getXmlProperty(masterProducers.at(k).toElement(), QStringLiteral("audio_index")) != QLatin1String("-1"); break; } } if (!hasAudio) { // no duplication needed, replace with blank QDomElement duplicate = m_doc.createElement(QStringLiteral("blank")); duplicate.setAttribute(QStringLiteral("length"), QString::number(out - in + 1)); duplicate_playlist.appendChild(duplicate); pos += out - in + 1; continue; } QDomNode prod = producers.at(j).cloneNode(); Xml::setXmlProperty(prod.toElement(), QStringLiteral("set.test_video"), QStringLiteral("1")); duplicate_playlist.appendChild(prod); // Check if that is an audio clip on a video track if (!hasVideo) { // Audio clip on a video track, replace with blank and duplicate producers.at(j).toElement().setTagName("blank"); producers.at(j).toElement().setAttribute("length", QString::number(out - in + 1)); } else { // group newly created AVSplit group // We temporarily store track with their playlist name since track index will change // as we insert the duplicate tracks QJsonArray array; QJsonObject items; items.insert(QLatin1String("type"), QJsonValue(QStringLiteral("Leaf"))); items.insert(QLatin1String("leaf"), QJsonValue(QLatin1String("clip"))); items.insert(QLatin1String("data"), QJsonValue(QString("%1:%2").arg(playlistName).arg(pos))); array.push_back(items); QJsonObject itemb; itemb.insert(QLatin1String("type"), QJsonValue(QStringLiteral("Leaf"))); itemb.insert(QLatin1String("leaf"), QJsonValue(QLatin1String("clip"))); itemb.insert(QLatin1String("data"), QJsonValue(QString("%1:%2").arg(duplicate_playlist.attribute(QStringLiteral("id"))).arg(pos))); array.push_back(itemb); QJsonObject currentGroup; currentGroup.insert(QLatin1String("type"), QJsonValue(QStringLiteral("AVSplit"))); currentGroup.insert(QLatin1String("children"), array); newGroups.push_back(currentGroup); } duplicationRequested = true; pos += out - in + 1; } else { // no duplication needed, replace with blank QDomElement duplicate = m_doc.createElement(QStringLiteral("blank")); int in = producers.at(j).toElement().attribute(QStringLiteral("in")).toInt(); int out = producers.at(j).toElement().attribute(QStringLiteral("out")).toInt(); duplicate.setAttribute(QStringLiteral("length"), QString::number(out - in + 1)); duplicate_playlist.appendChild(duplicate); pos += out - in + 1; } } if (duplicationRequested) { // Plant the playlist at the end mlt.insertBefore(duplicate_playlist, tractor); QDomNode lastTrack = tractor.firstChildElement(QStringLiteral("track")); QDomElement duplicate = m_doc.createElement(QStringLiteral("track")); duplicate.setAttribute(QStringLiteral("producer"), QString("%1_duplicate").arg(playlistName)); duplicate.setAttribute(QStringLiteral("hide"), QStringLiteral("video")); tractor.insertAfter(duplicate, lastTrack); trackOffset++; } } } if (trackOffset > 0) { // Some tracks were added, adjust compositions QDomNodeList transitions = m_doc.elementsByTagName(QStringLiteral("transition")); int max = transitions.count(); for (int i = 0; i < max; ++i) { QDomElement t = transitions.at(i).toElement(); if (Xml::getXmlProperty(t, QStringLiteral("internal_added")).toInt() > 0) { // internal transitions will be rebuilt, no need to correct continue; } int a_track = Xml::getXmlProperty(t, QStringLiteral("a_track")).toInt(); int b_track = Xml::getXmlProperty(t, QStringLiteral("b_track")).toInt(); if (a_track > 0) { Xml::setXmlProperty(t, QStringLiteral("a_track"), QString::number(a_track + trackOffset)); } if (b_track > 0) { Xml::setXmlProperty(t, QStringLiteral("b_track"), QString::number(b_track + trackOffset)); } } } // Process groups data QJsonDocument json(newGroups); QString groupsData = QString(json.toJson()); tracks = tractor.toElement().elementsByTagName(QStringLiteral("track")); for (int i = 0; i < tracks.count(); i++) { // Replace track names with their current index in our view const QString trackId = QString("%1:").arg(tracks.at(i).toElement().attribute(QStringLiteral("producer"))); groupsData.replace(trackId, QString("%1:").arg(i - 1)); } Xml::setXmlProperty(mainplaylist.toElement(), QStringLiteral("kdenlive:docproperties.groups"), groupsData); } m_modified = true; return true; } void DocumentValidator::convertKeyframeEffect(const QDomElement &effect, const QStringList ¶ms, QMap &values, int offset) { QLocale locale; int in = effect.attribute(QStringLiteral("in")).toInt() - offset; values.insert(in, locale.toDouble(Xml::getXmlProperty(effect, params.at(0)))); QString endValue = Xml::getXmlProperty(effect, params.at(1)); if (!endValue.isEmpty()) { int out = effect.attribute(QStringLiteral("out")).toInt() - offset; values.insert(out, locale.toDouble(endValue)); } } void DocumentValidator::updateProducerInfo(const QDomElement &prod, const QDomElement &source) { QString pxy = source.attribute(QStringLiteral("proxy")); if (pxy.length() > 1) { Xml::setXmlProperty(prod, QStringLiteral("kdenlive:proxy"), pxy); Xml::setXmlProperty(prod, QStringLiteral("kdenlive:originalurl"), source.attribute(QStringLiteral("resource"))); } if (source.hasAttribute(QStringLiteral("file_hash"))) { Xml::setXmlProperty(prod, QStringLiteral("kdenlive:file_hash"), source.attribute(QStringLiteral("file_hash"))); } if (source.hasAttribute(QStringLiteral("file_size"))) { Xml::setXmlProperty(prod, QStringLiteral("kdenlive:file_size"), source.attribute(QStringLiteral("file_size"))); } if (source.hasAttribute(QStringLiteral("name"))) { Xml::setXmlProperty(prod, QStringLiteral("kdenlive:clipname"), source.attribute(QStringLiteral("name"))); } if (source.hasAttribute(QStringLiteral("zone_out"))) { Xml::setXmlProperty(prod, QStringLiteral("kdenlive:zone_out"), source.attribute(QStringLiteral("zone_out"))); } if (source.hasAttribute(QStringLiteral("zone_in"))) { Xml::setXmlProperty(prod, QStringLiteral("kdenlive:zone_in"), source.attribute(QStringLiteral("zone_in"))); } if (source.hasAttribute(QStringLiteral("cutzones"))) { QString zoneData = source.attribute(QStringLiteral("cutzones")); const QStringList zoneList = zoneData.split(QLatin1Char(';')); int ct = 1; for (const QString &data : zoneList) { QString zoneName = data.section(QLatin1Char('-'), 2); if (zoneName.isEmpty()) { zoneName = i18n("Zone %1", ct++); } Xml::setXmlProperty(prod, QStringLiteral("kdenlive:clipzone.") + zoneName, data.section(QLatin1Char('-'), 0, 0) + QLatin1Char(';') + data.section(QLatin1Char('-'), 1, 1)); } } } QStringList DocumentValidator::getInfoFromEffectName(const QString &oldName) { QStringList info; // Returns a list to convert old Kdenlive ladspa effects if (oldName == QLatin1String("pitch_shift")) { info << QStringLiteral("ladspa.1433"); info << QStringLiteral("pitch=0"); } else if (oldName == QLatin1String("vinyl")) { info << QStringLiteral("ladspa.1905"); info << QStringLiteral("year=0"); info << QStringLiteral("rpm=1"); info << QStringLiteral("warping=2"); info << QStringLiteral("crackle=3"); info << QStringLiteral("wear=4"); } else if (oldName == QLatin1String("room_reverb")) { info << QStringLiteral("ladspa.1216"); info << QStringLiteral("room=0"); info << QStringLiteral("delay=1"); info << QStringLiteral("damp=2"); } else if (oldName == QLatin1String("reverb")) { info << QStringLiteral("ladspa.1423"); info << QStringLiteral("room=0"); info << QStringLiteral("damp=1"); } else if (oldName == QLatin1String("rate_scale")) { info << QStringLiteral("ladspa.1417"); info << QStringLiteral("rate=0"); } else if (oldName == QLatin1String("pitch_scale")) { info << QStringLiteral("ladspa.1193"); info << QStringLiteral("coef=0"); } else if (oldName == QLatin1String("phaser")) { info << QStringLiteral("ladspa.1217"); info << QStringLiteral("rate=0"); info << QStringLiteral("depth=1"); info << QStringLiteral("feedback=2"); info << QStringLiteral("spread=3"); } else if (oldName == QLatin1String("limiter")) { info << QStringLiteral("ladspa.1913"); info << QStringLiteral("gain=0"); info << QStringLiteral("limit=1"); info << QStringLiteral("release=2"); } else if (oldName == QLatin1String("equalizer_15")) { info << QStringLiteral("ladspa.1197"); info << QStringLiteral("1=0"); info << QStringLiteral("2=1"); info << QStringLiteral("3=2"); info << QStringLiteral("4=3"); info << QStringLiteral("5=4"); info << QStringLiteral("6=5"); info << QStringLiteral("7=6"); info << QStringLiteral("8=7"); info << QStringLiteral("9=8"); info << QStringLiteral("10=9"); info << QStringLiteral("11=10"); info << QStringLiteral("12=11"); info << QStringLiteral("13=12"); info << QStringLiteral("14=13"); info << QStringLiteral("15=14"); } else if (oldName == QLatin1String("equalizer")) { info << QStringLiteral("ladspa.1901"); info << QStringLiteral("logain=0"); info << QStringLiteral("midgain=1"); info << QStringLiteral("higain=2"); } else if (oldName == QLatin1String("declipper")) { info << QStringLiteral("ladspa.1195"); } return info; } QString DocumentValidator::colorToString(const QColor &c) { QString ret = QStringLiteral("%1,%2,%3,%4"); ret = ret.arg(c.red()).arg(c.green()).arg(c.blue()).arg(c.alpha()); return ret; } bool DocumentValidator::isProject() const { return m_doc.documentElement().tagName() == QLatin1String("mlt"); } bool DocumentValidator::isModified() const { return m_modified; } bool DocumentValidator::checkMovit() { QString playlist = m_doc.toString(); if (!playlist.contains(QStringLiteral("movit."))) { // Project does not use Movit GLSL effects, we can load it return true; } if (KMessageBox::questionYesNo(QApplication::activeWindow(), i18n("The project file uses some GPU effects. GPU acceleration is not currently enabled.\nDo you want to convert the " "project to a non-GPU version ?\nThis might result in data loss.")) != KMessageBox::Yes) { return false; } // Try to convert Movit filters to their non GPU equivalent QStringList convertedFilters; QStringList discardedFilters; bool hasWB = EffectsRepository::get()->exists(QStringLiteral("frei0r.colgate")); bool hasBlur = EffectsRepository::get()->exists(QStringLiteral("frei0r.IIRblur")); QString compositeTrans; if (TransitionsRepository::get()->exists(QStringLiteral("qtblend"))) { compositeTrans = QStringLiteral("qtblend"); } else if (TransitionsRepository::get()->exists(QStringLiteral("frei0r.cairoblend"))) { compositeTrans = QStringLiteral("frei0r.cairoblend"); } // Parse all effects in document QDomNodeList filters = m_doc.elementsByTagName(QStringLiteral("filter")); int max = filters.count(); for (int i = 0; i < max; ++i) { QDomElement filt = filters.at(i).toElement(); QString filterId = filt.attribute(QStringLiteral("id")); if (!filterId.startsWith(QLatin1String("movit."))) { continue; } if (filterId == QLatin1String("movit.white_balance") && hasWB) { // Convert to frei0r.colgate filt.setAttribute(QStringLiteral("id"), QStringLiteral("frei0r.colgate")); Xml::setXmlProperty(filt, QStringLiteral("kdenlive_id"), QStringLiteral("frei0r.colgate")); Xml::setXmlProperty(filt, QStringLiteral("tag"), QStringLiteral("frei0r.colgate")); Xml::setXmlProperty(filt, QStringLiteral("mlt_service"), QStringLiteral("frei0r.colgate")); Xml::renameXmlProperty(filt, QStringLiteral("neutral_color"), QStringLiteral("Neutral Color")); QString value = Xml::getXmlProperty(filt, QStringLiteral("color_temperature")); value = factorizeGeomValue(value, 15000.0); Xml::setXmlProperty(filt, QStringLiteral("color_temperature"), value); Xml::renameXmlProperty(filt, QStringLiteral("color_temperature"), QStringLiteral("Color Temperature")); convertedFilters << filterId; continue; } if (filterId == QLatin1String("movit.blur") && hasBlur) { // Convert to frei0r.IIRblur filt.setAttribute(QStringLiteral("id"), QStringLiteral("frei0r.IIRblur")); Xml::setXmlProperty(filt, QStringLiteral("kdenlive_id"), QStringLiteral("frei0r.IIRblur")); Xml::setXmlProperty(filt, QStringLiteral("tag"), QStringLiteral("frei0r.IIRblur")); Xml::setXmlProperty(filt, QStringLiteral("mlt_service"), QStringLiteral("frei0r.IIRblur")); Xml::renameXmlProperty(filt, QStringLiteral("radius"), QStringLiteral("Amount")); QString value = Xml::getXmlProperty(filt, QStringLiteral("Amount")); value = factorizeGeomValue(value, 14.0); Xml::setXmlProperty(filt, QStringLiteral("Amount"), value); convertedFilters << filterId; continue; } if (filterId == QLatin1String("movit.mirror")) { // Convert to MLT's mirror filt.setAttribute(QStringLiteral("id"), QStringLiteral("mirror")); Xml::setXmlProperty(filt, QStringLiteral("kdenlive_id"), QStringLiteral("mirror")); Xml::setXmlProperty(filt, QStringLiteral("tag"), QStringLiteral("mirror")); Xml::setXmlProperty(filt, QStringLiteral("mlt_service"), QStringLiteral("mirror")); Xml::setXmlProperty(filt, QStringLiteral("mirror"), QStringLiteral("flip")); convertedFilters << filterId; continue; } if (filterId.startsWith(QLatin1String("movit."))) { // TODO: implement conversion for more filters discardedFilters << filterId; } } // Parse all transitions in document QDomNodeList transitions = m_doc.elementsByTagName(QStringLiteral("transition")); max = transitions.count(); for (int i = 0; i < max; ++i) { QDomElement t = transitions.at(i).toElement(); QString transId = Xml::getXmlProperty(t, QStringLiteral("mlt_service")); if (!transId.startsWith(QLatin1String("movit."))) { continue; } if (transId == QLatin1String("movit.overlay") && !compositeTrans.isEmpty()) { // Convert to frei0r.cairoblend Xml::setXmlProperty(t, QStringLiteral("mlt_service"), compositeTrans); convertedFilters << transId; continue; } if (transId.startsWith(QLatin1String("movit."))) { // TODO: implement conversion for more filters discardedFilters << transId; } } convertedFilters.removeDuplicates(); discardedFilters.removeDuplicates(); if (discardedFilters.isEmpty()) { KMessageBox::informationList(QApplication::activeWindow(), i18n("The following filters/transitions were converted to non GPU versions:"), convertedFilters); } else { KMessageBox::informationList(QApplication::activeWindow(), i18n("The following filters/transitions were deleted from the project:"), discardedFilters); } m_modified = true; QString scene = m_doc.toString(); scene.replace(QLatin1String("movit."), QString()); m_doc.setContent(scene); return true; } QString DocumentValidator::factorizeGeomValue(const QString &value, double factor) { const QStringList vals = value.split(QLatin1Char(';')); QString result; QLocale locale; for (int i = 0; i < vals.count(); i++) { const QString &s = vals.at(i); QString key = s.section(QLatin1Char('='), 0, 0); QString val = s.section(QLatin1Char('='), 1, 1); double v = locale.toDouble(val) / factor; result.append(key + QLatin1Char('=') + locale.toString(v)); if (i + 1 < vals.count()) { result.append(QLatin1Char(';')); } } return result; } void DocumentValidator::checkOrphanedProducers() { QDomElement mlt = m_doc.firstChildElement(QStringLiteral("mlt")); QDomElement main = mlt.firstChildElement(QStringLiteral("playlist")); QDomNodeList bin_producers = main.childNodes(); QStringList binProducers; for (int k = 0; k < bin_producers.count(); k++) { QDomElement mltprod = bin_producers.at(k).toElement(); if (mltprod.tagName() != QLatin1String("entry")) { continue; } binProducers << mltprod.attribute(QStringLiteral("producer")); } QDomNodeList producers = m_doc.elementsByTagName(QStringLiteral("producer")); int max = producers.count(); QStringList allProducers; for (int i = 0; i < max; ++i) { QDomElement prod = producers.at(i).toElement(); if (prod.isNull()) { continue; } allProducers << prod.attribute(QStringLiteral("id")); } QDomDocumentFragment frag = m_doc.createDocumentFragment(); QDomDocumentFragment trackProds = m_doc.createDocumentFragment(); for (int i = 0; i < max; ++i) { QDomElement prod = producers.at(i).toElement(); if (prod.isNull()) { continue; } QString id = prod.attribute(QStringLiteral("id")).section(QLatin1Char('_'), 0, 0); if (id.startsWith(QLatin1String("slowmotion")) || id == QLatin1String("black")) { continue; } if (!binProducers.contains(id)) { QString binId = Xml::getXmlProperty(prod, QStringLiteral("kdenlive:binid")); if (!binId.isEmpty() && binProducers.contains(binId)) { continue; } qCWarning(KDENLIVE_LOG) << " ///////// WARNING, FOUND UNKNOWN PRODUDER: " << id << " ----------------"; // This producer is unknown to Bin QString service = Xml::getXmlProperty(prod, QStringLiteral("mlt_service")); QString distinctiveTag(QStringLiteral("resource")); if (service == QLatin1String("kdenlivetitle")) { distinctiveTag = QStringLiteral("xmldata"); } QString orphanValue = Xml::getXmlProperty(prod, distinctiveTag); for (int j = 0; j < max; j++) { // Search for a similar producer QDomElement binProd = producers.at(j).toElement(); binId = binProd.attribute(QStringLiteral("id")).section(QLatin1Char('_'), 0, 0); if (service != QLatin1String("timewarp") && (binId.startsWith(QLatin1String("slowmotion")) || !binProducers.contains(binId))) { continue; } QString binService = Xml::getXmlProperty(binProd, QStringLiteral("mlt_service")); qCDebug(KDENLIVE_LOG) << " / /LKNG FOR: " << service << " / " << orphanValue << ", checking: " << binProd.attribute(QStringLiteral("id")); if (service != binService) { continue; } QString binValue = Xml::getXmlProperty(binProd, distinctiveTag); if (binValue == orphanValue) { // Found probable source producer, replace frag.appendChild(prod); i--; QDomNodeList entries = m_doc.elementsByTagName(QStringLiteral("entry")); for (int k = 0; k < entries.count(); k++) { QDomElement entry = entries.at(k).toElement(); if (entry.attribute(QStringLiteral("producer")) == id) { QString entryId = binId; if (service.contains(QStringLiteral("avformat")) || service == QLatin1String("xml") || service == QLatin1String("consumer")) { // We must use track producer, find track for this entry QString trackPlaylist = entry.parentNode().toElement().attribute(QStringLiteral("id")); entryId.append(QLatin1Char('_') + trackPlaylist); } if (!allProducers.contains(entryId)) { // The track producer does not exist, create a clone for it QDomElement cloned = binProd.cloneNode(true).toElement(); cloned.setAttribute(QStringLiteral("id"), entryId); trackProds.appendChild(cloned); allProducers << entryId; } entry.setAttribute(QStringLiteral("producer"), entryId); m_modified = true; } } continue; } } } } if (!trackProds.isNull()) { QDomNode firstProd = m_doc.firstChildElement(QStringLiteral("producer")); mlt.insertBefore(trackProds, firstProd); } } void DocumentValidator::fixTitleProducerLocale(QDomElement &producer) { QString data = Xml::getXmlProperty(producer, QStringLiteral("xmldata")); QDomDocument doc; doc.setContent(data); QDomNodeList nodes = doc.elementsByTagName(QStringLiteral("position")); bool fixed = false; for (int i = 0; i < nodes.count(); i++) { QDomElement pos = nodes.at(i).toElement(); QString x = pos.attribute(QStringLiteral("x")); QString y = pos.attribute(QStringLiteral("y")); if (x.contains(QLatin1Char(','))) { // x pos was saved in locale format, fix x = x.section(QLatin1Char(','), 0, 0); pos.setAttribute(QStringLiteral("x"), x); fixed = true; } if (y.contains(QLatin1Char(','))) { // x pos was saved in locale format, fix y = y.section(QLatin1Char(','), 0, 0); pos.setAttribute(QStringLiteral("y"), y); fixed = true; } } nodes = doc.elementsByTagName(QStringLiteral("content")); for (int i = 0; i < nodes.count(); i++) { QDomElement pos = nodes.at(i).toElement(); QString x = pos.attribute(QStringLiteral("font-outline")); QString y = pos.attribute(QStringLiteral("textwidth")); if (x.contains(QLatin1Char(','))) { // x pos was saved in locale format, fix x = x.section(QLatin1Char(','), 0, 0); pos.setAttribute(QStringLiteral("font-outline"), x); fixed = true; } if (y.contains(QLatin1Char(','))) { // x pos was saved in locale format, fix y = y.section(QLatin1Char(','), 0, 0); pos.setAttribute(QStringLiteral("textwidth"), y); fixed = true; } } if (fixed) { Xml::setXmlProperty(producer, QStringLiteral("xmldata"), doc.toString()); } } diff --git a/src/doc/documentvalidator.h b/src/doc/documentvalidator.h index c2e9d6cd3..fa23ce255 100644 --- a/src/doc/documentvalidator.h +++ b/src/doc/documentvalidator.h @@ -1,58 +1,58 @@ /*************************************************************************** * Copyright (C) 2009 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 DOCUMENTVALIDATOR_H #define DOCUMENTVALIDATOR_H #include #include #include #include class DocumentValidator { public: - DocumentValidator(const QDomDocument &doc, const QUrl &documentUrl); + DocumentValidator(const QDomDocument &doc, QUrl documentUrl); bool isProject() const; bool validate(const double currentVersion); bool isModified() const; /** @brief Check if the project contains references to Movit stuff (GLSL), and try to convert if wanted. */ bool checkMovit(); private: QDomDocument m_doc; QUrl m_url; bool m_modified; /** @brief Upgrade from a previous Kdenlive document version. */ bool upgrade(double version, const double currentVersion); /** @brief Pass producer properties from previous Kdenlive versions. */ void updateProducerInfo(const QDomElement &prod, const QDomElement &source); /** @brief Make sur we don't have orphaned producers (that are not in Bin). */ void checkOrphanedProducers(); QStringList getInfoFromEffectName(const QString &oldName); QString colorToString(const QColor &c); QString factorizeGeomValue(const QString &value, double factor); /** @brief Kdenlive <= 0.9.10 saved title clip item position/opacity with locale which was wrong, fix. */ void fixTitleProducerLocale(QDomElement &producer); void convertKeyframeEffect(const QDomElement &effect, const QStringList ¶ms, QMap &values, int offset); }; #endif diff --git a/src/doc/kdenlivedoc.cpp b/src/doc/kdenlivedoc.cpp index 200c5092b..43986fad5 100644 --- a/src/doc/kdenlivedoc.cpp +++ b/src/doc/kdenlivedoc.cpp @@ -1,1713 +1,1712 @@ /*************************************************************************** * 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) +KdenliveDoc::KdenliveDoc(const QUrl &url, 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_projectFolder(std::move(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) { 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/doc/kdenlivedoc.h b/src/doc/kdenlivedoc.h index 86b0fa90c..a781005a4 100644 --- a/src/doc/kdenlivedoc.h +++ b/src/doc/kdenlivedoc.h @@ -1,256 +1,256 @@ /*************************************************************************** * 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 * ***************************************************************************/ /*! \class KdenliveDoc \brief Represents a kdenlive project file Instances of KdeliveDoc classes are created by void MainWindow::newFile(bool showProjectSettings, bool force) */ #ifndef KDENLIVEDOC_H #define KDENLIVEDOC_H #include #include #include #include #include #include #include #include "definitions.h" #include "gentime.h" #include "timecode.h" class MainWindow; class TrackInfo; class ProjectClip; class ClipController; class MarkerListModel; class Render; class ProfileParam; class QTextEdit; class QUndoGroup; class QUndoCommand; class DocUndoStack; namespace Mlt { class Profile; } class KdenliveDoc : public QObject { Q_OBJECT public: - KdenliveDoc(const QUrl &url, const QString &projectFolder, QUndoGroup *undoGroup, const QString &profileName, const QMap &properties, + KdenliveDoc(const QUrl &url, QString projectFolder, QUndoGroup *undoGroup, const QString &profileName, const QMap &properties, const QMap &metadata, const QPoint &tracks, bool *openBackup, MainWindow *parent = nullptr); - ~KdenliveDoc(); + ~KdenliveDoc() override; friend class LoadJob; /** @brief Get current document's producer. */ const QByteArray getProjectXml(); QDomNodeList producersList(); double fps() const; int width() const; int height() const; QUrl url() const; KAutoSaveFile *m_autosave; Timecode timecode() const; QDomDocument toXml(); std::shared_ptr commandStack(); int getFramePos(const QString &duration); /** @brief Get a bin's clip from its id. */ std::shared_ptr getBinClip(const QString &clipId); /** @brief Get a list of all clip ids that are inside a folder. */ QStringList getBinFolderClipIds(const QString &folderId) const; const QString description() const; void setUrl(const QUrl &url); /** @brief Defines whether the document needs to be saved. */ bool isModified() const; /** @brief Returns the project folder, used to store project temporary files. */ QString projectTempFolder() const; /** @brief Returns the folder used to store project data files (titles, etc). */ QString projectDataFolder() const; void setZoom(int horizontal, int vertical = -1); QPoint zoom() const; double dar() const; /** @brief Returns the project file xml. */ QDomDocument xmlSceneList(const QString &scene); /** @brief Saves the project file xml to a file. */ bool saveSceneList(const QString &path, const QString &scene); /** @brief Saves only the MLT xml to a file for preview rendering. */ void saveMltPlaylist(const QString &fileName); void cacheImage(const QString &fileId, const QImage &img) const; void setProjectFolder(const QUrl &url); void setZone(int start, int end); QPoint zone() const; /** @brief Returns target tracks (video, audio). */ QPair targetTracks() const; void setDocumentProperty(const QString &name, const QString &value); const QString getDocumentProperty(const QString &name, const QString &defaultValue = QString()) const; /** @brief Gets the list of renderer properties saved into the document. */ QMap getRenderProperties() const; /** @brief Read the display ratio from an xml project file. */ static double getDisplayRatio(const QString &path); /** @brief Backup the project file */ void backupLastSavedVersion(const QString &path); /** @brief Returns the document metadata (author, copyright, ...) */ const QMap metadata() const; /** @brief Set the document metadata (author, copyright, ...) */ void setMetadata(const QMap &meta); /** @brief Get all document properties that need to be saved */ QMap documentProperties(); bool useProxy() const; bool useExternalProxy() const; bool autoGenerateProxy(int width) const; bool autoGenerateImageProxy(int width) const; /** @brief Saves effects embedded in project file. */ void saveCustomEffects(const QDomNodeList &customeffects); void resetProfile(); /** @brief Returns true if the profile file has changed. */ bool profileChanged(const QString &profile) const; /** @brief Get an action from main actioncollection. */ QAction *getAction(const QString &name); /** @brief Add an action to main actioncollection. */ void doAddAction(const QString &name, QAction *a, const QKeySequence &shortcut); void invalidatePreviews(QList chunks); void previewProgress(int p); /** @brief Select most appropriate rendering profile for timeline preview based on fps / size. */ void selectPreviewProfile(); void displayMessage(const QString &text, MessageType type = DefaultMessage, int timeOut = 0); /** @brief Get a cache directory for this project. */ QDir getCacheDir(CacheType type, bool *ok) const; /** @brief Create standard cache dirs for the project */ void initCacheDirs(); /** @brief Get a list of all proxy hash used in this project */ QStringList getProxyHashList(); /** @brief Move project data files to new url */ void moveProjectData(const QString &src, const QString &dest); /** @brief Returns a pointer to the guide model */ std::shared_ptr getGuideModel() const; // TODO REFAC: delete */ Render *renderer(); /** @brief Returns MLT's root (base path) */ const QString documentRoot() const; /** @brief Returns true if timeline preview settings changed*/ bool updatePreviewSettings(const QString &profile); /** @brief Returns the recommended proxy profile parameters */ QString getAutoProxyProfile(); private: QUrl m_url; QDomDocument m_document; /** @brief MLT's root (base path) that is stripped from urls in saved xml */ QString m_documentRoot; Timecode m_timecode; std::shared_ptr m_commandStack; QString m_searchFolder; /** @brief Tells whether the current document has been changed after being saved. */ bool m_modified; /** @brief The default recommended proxy extension */ QString m_proxyExtension; /** @brief The default recommended proxy params */ QString m_proxyParams; /** @brief Tells whether the current document was modified by Kdenlive on opening, and a backup should be created on save. */ enum DOCSTATUS { CleanProject, ModifiedProject, UpgradedProject }; DOCSTATUS m_documentOpenStatus; /** @brief The project folder, used to store project files (titles, effects...). */ QString m_projectFolder; QList m_undoChunks; QMap m_documentProperties; QMap m_documentMetadata; std::shared_ptr m_guideModel; QString searchFileRecursively(const QDir &dir, const QString &matchSize, const QString &matchHash) const; /** @brief Creates a new project. */ QDomDocument createEmptyDocument(int videotracks, int audiotracks); QDomDocument createEmptyDocument(const QList &tracks); /** @brief Updates the project folder location entry in the kdenlive file dialogs to point to the current project folder. */ void updateProjectFolderPlacesEntry(); /** @brief Only keep some backup files, delete some */ void cleanupBackupFiles(); /** @brief Load document properties from the xml file */ void loadDocumentProperties(); /** @brief update document properties to reflect a change in the current profile */ void updateProjectProfile(bool reloadProducers = false); /** @brief initialize proxy settings based on hw status */ void initProxySettings(); public slots: void slotCreateTextTemplateClip(const QString &group, const QString &groupId, QUrl path); /** @brief Sets the document as modified or up to date. * @description If crash recovery is turned on, a timer calls KdenliveDoc::slotAutoSave() \n * Emits docModified connected to MainWindow::slotUpdateDocumentState \n * @param mod (optional) true if the document has to be saved */ void setModified(bool mod = true); void slotProxyCurrentItem(bool doProxy, QList> clipList = QList>(), bool force = false, QUndoCommand *masterCommand = nullptr); /** @brief Saves the current project at the autosave location. * @description The autosave files are in ~/.kde/data/stalefiles/kdenlive/ */ void slotAutoSave(const QString &scene); /** @brief Groups were changed, save to MLT. */ void groupsChanged(const QString &groups); private slots: void slotModified(); void switchProfile(std::unique_ptr &profile, const QString &id, const QDomElement &xml); void slotSwitchProfile(const QString &profile_path); /** @brief Check if we did a new action invalidating more recent undo items. */ void checkPreviewStack(); /** @brief Guides were changed, save to MLT. */ void guidesChanged(); signals: void resetProjectList(); /** @brief Informs that the document status has been changed. * * If the document has been modified, it's called with true as an argument. */ void docModified(bool); void selectLastAddedClip(const QString &); /** @brief When creating a backup file, also save a thumbnail of current timeline */ void saveTimelinePreview(const QString &path); /** @brief Trigger the autosave timer start */ void startAutoSave(); /** @brief Current doc created effects, reload list */ void reloadEffects(const QStringList &paths); /** @brief Fps was changed, update timeline (changed = 1 means no change) */ void updateFps(double changed); /** @brief If a command is pushed when we are in the middle of undo stack, invalidate further undo history */ void removeInvalidUndo(int ix); /** @brief Update compositing info */ void updateCompositionMode(int); }; #endif diff --git a/src/dvdwizard/dvdwizard.cpp b/src/dvdwizard/dvdwizard.cpp index c127970b0..393ab41b6 100644 --- a/src/dvdwizard/dvdwizard.cpp +++ b/src/dvdwizard/dvdwizard.cpp @@ -1,1044 +1,1044 @@ /*************************************************************************** * 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 "dvdwizard.h" #include "dvdwizardvob.h" #include "dialogs/profilesdialog.h" #include "kdenlivesettings.h" #include "monitor/monitormanager.h" #include "timecode.h" #include #include #include #include "kdenlive_debug.h" #include #include #include #include #include #include #include DvdWizard::DvdWizard(MonitorManager *manager, const QString &url, QWidget *parent) : QWizard(parent) , m_authorFile(QStringLiteral("XXXXXX.xml")) , m_menuFile(QStringLiteral("XXXXXX.xml")) , m_menuVobFile(QStringLiteral("XXXXXX.mpg")) , m_letterboxMovie(QStringLiteral("XXXXXX.mpg")) , m_dvdauthor(nullptr) , m_mkiso(nullptr) , m_vobitem(nullptr) , m_selectedImage(QStringLiteral("XXXXXX.png")) , m_selectedLetterImage(QStringLiteral("XXXXXX.png")) , m_highlightedImage(QStringLiteral("XXXXXX.png")) , m_highlightedLetterImage(QStringLiteral("XXXXXX.png")) , m_menuVideo(QStringLiteral("XXXXXX.vob")) , m_menuFinalVideo(QStringLiteral("XXXXXX.vob")) , m_menuImageBackground(QStringLiteral("XXXXXX.png")) , m_burnMenu(new QMenu(parent)) , m_previousPage(0) { setWindowTitle(i18n("DVD Wizard")); // setPixmap(QWizard::WatermarkPixmap, QPixmap(QStandardPaths::locate(QStandardPaths::AppDataLocation, "banner.png"))); m_pageVob = new DvdWizardVob(this); m_pageVob->setTitle(i18n("Select Files For Your DVD")); addPage(m_pageVob); m_pageChapters = new DvdWizardChapters(manager, m_pageVob->dvdFormat(), this); m_pageChapters->setTitle(i18n("DVD Chapters")); addPage(m_pageChapters); if (!url.isEmpty()) { m_pageVob->setUrl(url); } m_pageVob->setMinimumSize(m_pageChapters->size()); m_pageMenu = new DvdWizardMenu(m_pageVob->dvdFormat(), this); m_pageMenu->setTitle(i18n("Create DVD Menu")); addPage(m_pageMenu); auto *page4 = new QWizardPage; page4->setTitle(i18n("Creating DVD Image")); m_status.setupUi(page4); m_status.error_box->setHidden(true); m_status.tmp_folder->setUrl(QUrl::fromLocalFile(KdenliveSettings::currenttmpfolder())); m_status.tmp_folder->setMode(KFile::Directory | KFile::ExistingOnly); m_status.iso_image->setUrl(QUrl::fromLocalFile(QDir::homePath() + QStringLiteral("/untitled.iso"))); m_status.iso_image->setFilter(QStringLiteral("*.iso")); m_status.iso_image->setMode(KFile::File); m_isoMessage = new KMessageWidget; - QGridLayout *s = static_cast(page4->layout()); + auto *s = static_cast(page4->layout()); s->addWidget(m_isoMessage, 5, 0, 1, -1); m_isoMessage->hide(); addPage(page4); connect(this, &QWizard::currentIdChanged, this, &DvdWizard::slotPageChanged); connect(m_status.button_start, &QAbstractButton::clicked, this, &DvdWizard::slotGenerate); connect(m_status.button_abort, &QAbstractButton::clicked, this, &DvdWizard::slotAbort); connect(m_status.button_preview, &QAbstractButton::clicked, this, &DvdWizard::slotPreview); QString programName(QStringLiteral("k3b")); QString exec = QStandardPaths::findExecutable(programName); if (!exec.isEmpty()) { // Add K3b action QAction *k3b = m_burnMenu->addAction(QIcon::fromTheme(programName), i18n("Burn with %1", programName), this, SLOT(slotBurn())); k3b->setData(exec); } programName = QStringLiteral("brasero"); exec = QStandardPaths::findExecutable(programName); if (!exec.isEmpty()) { // Add Brasero action QAction *brasero = m_burnMenu->addAction(QIcon::fromTheme(programName), i18n("Burn with %1", programName), this, SLOT(slotBurn())); brasero->setData(exec); } if (m_burnMenu->isEmpty()) { m_burnMenu->addAction(i18n("No burning program found (K3b, Brasero)")); } m_status.button_burn->setMenu(m_burnMenu); m_status.button_burn->setIcon(QIcon::fromTheme(QStringLiteral("tools-media-optical-burn"))); m_status.button_burn->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); m_status.button_preview->setIcon(QIcon::fromTheme(QStringLiteral("media-playback-start"))); setButtonText(QWizard::CustomButton1, i18n("Load")); setButtonText(QWizard::CustomButton2, i18n("Save")); button(QWizard::CustomButton1)->setIcon(QIcon::fromTheme(QStringLiteral("document-open"))); button(QWizard::CustomButton2)->setIcon(QIcon::fromTheme(QStringLiteral("document-save"))); connect(button(QWizard::CustomButton1), &QAbstractButton::clicked, this, &DvdWizard::slotLoad); connect(button(QWizard::CustomButton2), &QAbstractButton::clicked, this, &DvdWizard::slotSave); setOption(QWizard::HaveCustomButton1, true); setOption(QWizard::HaveCustomButton2, true); QList layout; layout << QWizard::CustomButton1 << QWizard::CustomButton2 << QWizard::Stretch << QWizard::BackButton << QWizard::NextButton << QWizard::CancelButton << QWizard::FinishButton; setButtonLayout(layout); } DvdWizard::~DvdWizard() { m_authorFile.remove(); m_menuFile.remove(); m_menuVobFile.remove(); m_letterboxMovie.remove(); m_menuImageBackground.remove(); blockSignals(true); delete m_burnMenu; if (m_dvdauthor) { m_dvdauthor->blockSignals(true); m_dvdauthor->close(); delete m_dvdauthor; } if (m_mkiso) { m_mkiso->blockSignals(true); m_mkiso->close(); delete m_mkiso; } } void DvdWizard::slotPageChanged(int page) { if (page == 0) { // Update chapters that were modified in page 1 m_pageVob->updateChapters(m_pageChapters->chaptersData()); m_pageChapters->stopMonitor(); m_previousPage = 0; } else if (page == 1) { if (m_previousPage == 0) { m_pageChapters->setVobFiles(m_pageVob->dvdFormat(), m_pageVob->selectedUrls(), m_pageVob->durations(), m_pageVob->chapters()); } else { // For some reason, when coming from page 2, we need to trick the monitor or it disappears m_pageChapters->createMonitor(m_pageVob->dvdFormat()); } m_previousPage = 1; } else if (page == 2) { m_pageChapters->stopMonitor(); m_pageVob->updateChapters(m_pageChapters->chaptersData()); m_pageMenu->setTargets(m_pageChapters->selectedTitles(), m_pageChapters->selectedTargets()); m_pageMenu->changeProfile(m_pageVob->dvdFormat()); m_previousPage = 2; } } void DvdWizard::generateDvd() { m_isoMessage->animatedHide(); QDir dir(m_status.tmp_folder->url().toLocalFile() + QStringLiteral("DVD/")); if (!dir.exists()) { dir.mkpath(dir.absolutePath()); } if (!dir.exists()) { // We failed creating tmp DVD directory KMessageBox::sorry(this, i18n("Cannot create temporary directory %1", m_status.tmp_folder->url().toLocalFile() + QStringLiteral("DVD"))); return; } m_status.error_box->setHidden(true); m_status.error_box->setCurrentIndex(0); m_status.menu_file->clear(); m_status.dvd_file->clear(); m_selectedImage.setFileTemplate(m_status.tmp_folder->url().toLocalFile() + QStringLiteral("XXXXXX.png")); m_selectedLetterImage.setFileTemplate(m_status.tmp_folder->url().toLocalFile() + QStringLiteral("XXXXXX.png")); m_highlightedImage.setFileTemplate(m_status.tmp_folder->url().toLocalFile() + QStringLiteral("XXXXXX.png")); m_highlightedLetterImage.setFileTemplate(m_status.tmp_folder->url().toLocalFile() + QStringLiteral("XXXXXX.png")); m_selectedImage.open(); m_selectedLetterImage.open(); m_highlightedImage.open(); m_highlightedLetterImage.open(); m_menuImageBackground.setFileTemplate(m_status.tmp_folder->url().toLocalFile() + QStringLiteral("XXXXXX.png")); m_menuImageBackground.setAutoRemove(false); m_menuImageBackground.open(); m_menuVideo.setFileTemplate(m_status.tmp_folder->url().toLocalFile() + QStringLiteral("XXXXXX.vob")); m_menuVideo.open(); m_menuFinalVideo.setFileTemplate(m_status.tmp_folder->url().toLocalFile() + QStringLiteral("XXXXXX.vob")); m_menuFinalVideo.open(); m_letterboxMovie.close(); m_letterboxMovie.setFileTemplate(m_status.tmp_folder->url().toLocalFile() + QStringLiteral("XXXXXX.mpg")); m_letterboxMovie.setAutoRemove(false); m_letterboxMovie.open(); m_menuFile.close(); m_menuFile.setFileTemplate(m_status.tmp_folder->url().toLocalFile() + QStringLiteral("XXXXXX.xml")); m_menuFile.setAutoRemove(false); m_menuFile.open(); m_menuVobFile.close(); m_menuVobFile.setFileTemplate(m_status.tmp_folder->url().toLocalFile() + QStringLiteral("XXXXXX.mpg")); m_menuVobFile.setAutoRemove(false); m_menuVobFile.open(); m_authorFile.close(); m_authorFile.setFileTemplate(m_status.tmp_folder->url().toLocalFile() + QStringLiteral("XXXXXX.xml")); m_authorFile.setAutoRemove(false); m_authorFile.open(); QListWidgetItem *images = m_status.job_progress->item(0); m_status.job_progress->setCurrentRow(0); images->setIcon(QIcon::fromTheme(QStringLiteral("system-run"))); m_status.error_log->clear(); // initialize html content m_status.error_log->setText(QStringLiteral("")); if (m_pageMenu->createMenu()) { m_pageMenu->createButtonImages(m_selectedImage.fileName(), m_highlightedImage.fileName(), false); m_pageMenu->createBackgroundImage(m_menuImageBackground.fileName(), false); images->setIcon(QIcon::fromTheme(QStringLiteral("dialog-ok"))); connect(&m_menuJob, static_cast(&QProcess::finished), this, &DvdWizard::slotProcessMenuStatus); // qCDebug(KDENLIVE_LOG) << "/// STARTING MLT VOB CREATION: "<menuMovie()) { // create menu vob file m_vobitem = m_status.job_progress->item(1); m_status.job_progress->setCurrentRow(1); m_vobitem->setIcon(QIcon::fromTheme(QStringLiteral("system-run"))); QStringList args; args << QStringLiteral("-profile") << m_pageVob->dvdProfile(); args.append(m_menuImageBackground.fileName()); args.append(QStringLiteral("in=0")); args.append(QStringLiteral("out=100")); args << QStringLiteral("-consumer") << "avformat:" + m_menuVideo.fileName() << QStringLiteral("properties=DVD"); m_menuJob.start(KdenliveSettings::rendererpath(), args); } else { // Movie as menu background, do the compositing m_vobitem = m_status.job_progress->item(1); m_status.job_progress->setCurrentRow(1); m_vobitem->setIcon(QIcon::fromTheme(QStringLiteral("system-run"))); int menuLength = m_pageMenu->menuMovieLength(); if (menuLength == -1) { // menu movie is invalid errorMessage(i18n("Menu movie is invalid")); m_status.button_start->setEnabled(true); m_status.button_abort->setEnabled(false); return; } QStringList args; args.append(QStringLiteral("-profile")); args.append(m_pageVob->dvdProfile()); args.append(m_pageMenu->menuMoviePath()); args << QStringLiteral("-track") << m_menuImageBackground.fileName(); args << "out=" + QString::number(menuLength); args << QStringLiteral("-transition") << QStringLiteral("composite") << QStringLiteral("always_active=1"); args << QStringLiteral("-consumer") << "avformat:" + m_menuFinalVideo.fileName() << QStringLiteral("properties=DVD"); m_menuJob.start(KdenliveSettings::rendererpath(), args); // qCDebug(KDENLIVE_LOG)<<"// STARTING MENU JOB, image: "< buttons = m_pageMenu->buttonsInfo(); QStringList buttonsTarget; // create xml spumux file QListWidgetItem *spuitem = m_status.job_progress->item(2); m_status.job_progress->setCurrentRow(2); spuitem->setIcon(QIcon::fromTheme(QStringLiteral("system-run"))); QDomDocument doc; QDomElement sub = doc.createElement(QStringLiteral("subpictures")); doc.appendChild(sub); QDomElement stream = doc.createElement(QStringLiteral("stream")); sub.appendChild(stream); QDomElement spu = doc.createElement(QStringLiteral("spu")); stream.appendChild(spu); spu.setAttribute(QStringLiteral("force"), QStringLiteral("yes")); spu.setAttribute(QStringLiteral("start"), QStringLiteral("00:00:00.00")); // spu.setAttribute("image", m_menuImage.fileName()); spu.setAttribute(QStringLiteral("select"), m_selectedImage.fileName()); spu.setAttribute(QStringLiteral("highlight"), m_highlightedImage.fileName()); /*spu.setAttribute("autoorder", "rows");*/ int max = buttons.count() - 1; int i = 0; QMapIterator it(buttons); while (it.hasNext()) { it.next(); QDomElement but = doc.createElement(QStringLiteral("button")); but.setAttribute(QStringLiteral("name"), 'b' + QString::number(i)); if (i < max) { but.setAttribute(QStringLiteral("down"), 'b' + QString::number(i + 1)); } else { but.setAttribute(QStringLiteral("down"), QStringLiteral("b0")); } if (i > 0) { but.setAttribute(QStringLiteral("up"), 'b' + QString::number(i - 1)); } else { but.setAttribute(QStringLiteral("up"), 'b' + QString::number(max)); } QRect r = it.value(); // int target = it.key(); // TODO: solve play all button // if (target == 0) target = 1; // We need to make sure that the y coordinate is a multiple of 2, otherwise button may not be displayed buttonsTarget.append(it.key()); int y0 = r.y(); if (y0 % 2 == 1) { y0++; } int y1 = r.bottom(); if (y1 % 2 == 1) { y1--; } but.setAttribute(QStringLiteral("x0"), QString::number(r.x())); but.setAttribute(QStringLiteral("y0"), QString::number(y0)); but.setAttribute(QStringLiteral("x1"), QString::number(r.right())); but.setAttribute(QStringLiteral("y1"), QString::number(y1)); spu.appendChild(but); ++i; } QFile menuFile(m_menuFile.fileName()); if (menuFile.open(QFile::WriteOnly)) { menuFile.write(doc.toString().toUtf8()); } menuFile.close(); // qCDebug(KDENLIVE_LOG) << " SPUMUX DATA: " << doc.toString(); QStringList args; args << QStringLiteral("-s") << QStringLiteral("0") << m_menuFile.fileName(); // qCDebug(KDENLIVE_LOG) << "SPM ARGS: " << args << m_menuVideo.fileName() << m_menuVobFile.fileName(); QProcess spumux; QString menuMovieUrl; QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); env.insert(QStringLiteral("VIDEO_FORMAT"), m_pageVob->dvdFormat() == PAL || m_pageVob->dvdFormat() == PAL_WIDE ? "PAL" : "NTSC"); spumux.setProcessEnvironment(env); if (m_pageMenu->menuMovie()) { spumux.setStandardInputFile(m_menuFinalVideo.fileName()); } else { spumux.setStandardInputFile(m_menuVideo.fileName()); } spumux.setStandardOutputFile(m_menuVobFile.fileName()); spumux.start(QStringLiteral("spumux"), args); if (spumux.waitForFinished()) { m_status.error_log->append(spumux.readAllStandardError()); if (spumux.exitStatus() == QProcess::CrashExit) { // TODO: inform user via messagewidget after string freeze QByteArray result = spumux.readAllStandardError(); spuitem->setIcon(QIcon::fromTheme(QStringLiteral("dialog-close"))); m_status.error_log->append(result); m_status.error_box->setHidden(false); m_status.menu_file->setPlainText(m_menuFile.readAll()); m_status.dvd_file->setPlainText(m_authorFile.readAll()); m_status.button_start->setEnabled(true); // qCDebug(KDENLIVE_LOG) << "/// RENDERING SPUMUX MENU crashed"; return; } } else { // qCDebug(KDENLIVE_LOG) << "/// RENDERING SPUMUX MENU timed out"; errorMessage(i18n("Rendering job timed out")); spuitem->setIcon(QIcon::fromTheme(QStringLiteral("dialog-close"))); m_status.error_log->append(QStringLiteral("
    ") + i18n("Menu job timed out")); m_status.error_log->scrollToAnchor(QStringLiteral("result")); m_status.error_box->setHidden(false); m_status.menu_file->setPlainText(m_menuFile.readAll()); m_status.dvd_file->setPlainText(m_authorFile.readAll()); m_status.button_start->setEnabled(true); return; } if (m_pageVob->dvdFormat() == PAL_WIDE || m_pageVob->dvdFormat() == NTSC_WIDE) { // Second step processing for 16:9 DVD, add letterbox stream m_pageMenu->createButtonImages(m_selectedLetterImage.fileName(), m_highlightedLetterImage.fileName(), true); buttons = m_pageMenu->buttonsInfo(true); QDomDocument docLetter; QDomElement subLetter = docLetter.createElement(QStringLiteral("subpictures")); docLetter.appendChild(subLetter); QDomElement streamLetter = docLetter.createElement(QStringLiteral("stream")); subLetter.appendChild(streamLetter); QDomElement spuLetter = docLetter.createElement(QStringLiteral("spu")); streamLetter.appendChild(spuLetter); spuLetter.setAttribute(QStringLiteral("force"), QStringLiteral("yes")); spuLetter.setAttribute(QStringLiteral("start"), QStringLiteral("00:00:00.00")); spuLetter.setAttribute(QStringLiteral("select"), m_selectedLetterImage.fileName()); spuLetter.setAttribute(QStringLiteral("highlight"), m_highlightedLetterImage.fileName()); max = buttons.count() - 1; i = 0; QMapIterator it2(buttons); while (it2.hasNext()) { it2.next(); QDomElement but = docLetter.createElement(QStringLiteral("button")); but.setAttribute(QStringLiteral("name"), 'b' + QString::number(i)); if (i < max) { but.setAttribute(QStringLiteral("down"), 'b' + QString::number(i + 1)); } else { but.setAttribute(QStringLiteral("down"), QStringLiteral("b0")); } if (i > 0) { but.setAttribute(QStringLiteral("up"), 'b' + QString::number(i - 1)); } else { but.setAttribute(QStringLiteral("up"), 'b' + QString::number(max)); } QRect r = it2.value(); // We need to make sure that the y coordinate is a multiple of 2, otherwise button may not be displayed buttonsTarget.append(it2.key()); int y0 = r.y(); if (y0 % 2 == 1) { y0++; } int y1 = r.bottom(); if (y1 % 2 == 1) { y1--; } but.setAttribute(QStringLiteral("x0"), QString::number(r.x())); but.setAttribute(QStringLiteral("y0"), QString::number(y0)); but.setAttribute(QStringLiteral("x1"), QString::number(r.right())); but.setAttribute(QStringLiteral("y1"), QString::number(y1)); spuLetter.appendChild(but); ++i; } ////qCDebug(KDENLIVE_LOG) << " SPUMUX DATA: " << doc.toString(); if (menuFile.open(QFile::WriteOnly)) { menuFile.write(docLetter.toString().toUtf8()); } menuFile.close(); spumux.setStandardInputFile(m_menuVobFile.fileName()); spumux.setStandardOutputFile(m_letterboxMovie.fileName()); args.clear(); args << QStringLiteral("-s") << QStringLiteral("1") << m_menuFile.fileName(); spumux.start(QStringLiteral("spumux"), args); // qCDebug(KDENLIVE_LOG) << "SPM ARGS LETTERBOX: " << args << m_menuVideo.fileName() << m_letterboxMovie.fileName(); if (spumux.waitForFinished()) { m_status.error_log->append(spumux.readAllStandardError()); if (spumux.exitStatus() == QProcess::CrashExit) { // TODO: inform user via messagewidget after string freeze QByteArray result = spumux.readAllStandardError(); spuitem->setIcon(QIcon::fromTheme(QStringLiteral("dialog-close"))); m_status.error_log->append(result); m_status.error_box->setHidden(false); m_status.menu_file->setPlainText(m_menuFile.readAll()); m_status.dvd_file->setPlainText(m_authorFile.readAll()); m_status.button_start->setEnabled(true); // qCDebug(KDENLIVE_LOG) << "/// RENDERING SPUMUX MENU crashed"; return; } } else { // qCDebug(KDENLIVE_LOG) << "/// RENDERING SPUMUX MENU timed out"; errorMessage(i18n("Rendering job timed out")); spuitem->setIcon(QIcon::fromTheme(QStringLiteral("dialog-close"))); m_status.error_log->append(QStringLiteral("

    ") + i18n("Menu job timed out")); m_status.error_log->scrollToAnchor(QStringLiteral("result")); m_status.error_box->setHidden(false); m_status.menu_file->setPlainText(m_menuFile.readAll()); m_status.dvd_file->setPlainText(m_authorFile.readAll()); m_status.button_start->setEnabled(true); return; } menuMovieUrl = m_letterboxMovie.fileName(); } else { menuMovieUrl = m_menuVobFile.fileName(); } spuitem->setIcon(QIcon::fromTheme(QStringLiteral("dialog-ok"))); // qCDebug(KDENLIVE_LOG) << "/// DONE: " << menuMovieUrl; processDvdauthor(menuMovieUrl, buttons, buttonsTarget); } void DvdWizard::processDvdauthor(const QString &menuMovieUrl, const QMap &buttons, const QStringList &buttonsTarget) { // create dvdauthor xml QListWidgetItem *authitem = m_status.job_progress->item(3); m_status.job_progress->setCurrentRow(3); authitem->setIcon(QIcon::fromTheme(QStringLiteral("system-run"))); QDomDocument dvddoc; QDomElement auth = dvddoc.createElement(QStringLiteral("dvdauthor")); auth.setAttribute(QStringLiteral("dest"), m_status.tmp_folder->url().toLocalFile() + QStringLiteral("DVD")); dvddoc.appendChild(auth); QDomElement vmgm = dvddoc.createElement(QStringLiteral("vmgm")); auth.appendChild(vmgm); if (m_pageMenu->createMenu() && !m_pageVob->introMovie().isEmpty()) { // Use first movie in list as intro movie QDomElement menus = dvddoc.createElement(QStringLiteral("menus")); vmgm.appendChild(menus); QDomElement pgc = dvddoc.createElement(QStringLiteral("pgc")); pgc.setAttribute(QStringLiteral("entry"), QStringLiteral("title")); menus.appendChild(pgc); QDomElement menuvob = dvddoc.createElement(QStringLiteral("vob")); menuvob.setAttribute(QStringLiteral("file"), m_pageVob->introMovie()); pgc.appendChild(menuvob); QDomElement post = dvddoc.createElement(QStringLiteral("post")); QDomText call = dvddoc.createTextNode(QStringLiteral("jump titleset 1 menu;")); post.appendChild(call); pgc.appendChild(post); } QDomElement titleset = dvddoc.createElement(QStringLiteral("titleset")); auth.appendChild(titleset); if (m_pageMenu->createMenu()) { // DVD main menu QDomElement menus = dvddoc.createElement(QStringLiteral("menus")); titleset.appendChild(menus); QDomElement menuvideo = dvddoc.createElement(QStringLiteral("video")); menus.appendChild(menuvideo); switch (m_pageVob->dvdFormat()) { case PAL_WIDE: menuvideo.setAttribute(QStringLiteral("format"), QStringLiteral("pal")); menuvideo.setAttribute(QStringLiteral("aspect"), QStringLiteral("16:9")); menuvideo.setAttribute(QStringLiteral("widescreen"), QStringLiteral("nopanscan")); break; case NTSC_WIDE: menuvideo.setAttribute(QStringLiteral("format"), QStringLiteral("ntsc")); menuvideo.setAttribute(QStringLiteral("aspect"), QStringLiteral("16:9")); menuvideo.setAttribute(QStringLiteral("widescreen"), QStringLiteral("nopanscan")); break; case NTSC: menuvideo.setAttribute(QStringLiteral("format"), QStringLiteral("ntsc")); menuvideo.setAttribute(QStringLiteral("aspect"), QStringLiteral("4:3")); break; default: menuvideo.setAttribute(QStringLiteral("format"), QStringLiteral("pal")); menuvideo.setAttribute(QStringLiteral("aspect"), QStringLiteral("4:3")); break; } if (m_pageVob->dvdFormat() == PAL_WIDE || m_pageVob->dvdFormat() == NTSC_WIDE) { // Add letterbox stream info QDomElement subpict = dvddoc.createElement(QStringLiteral("subpicture")); QDomElement stream = dvddoc.createElement(QStringLiteral("stream")); stream.setAttribute(QStringLiteral("id"), QStringLiteral("0")); stream.setAttribute(QStringLiteral("mode"), QStringLiteral("widescreen")); subpict.appendChild(stream); QDomElement stream2 = dvddoc.createElement(QStringLiteral("stream")); stream2.setAttribute(QStringLiteral("id"), QStringLiteral("1")); stream2.setAttribute(QStringLiteral("mode"), QStringLiteral("letterbox")); subpict.appendChild(stream2); menus.appendChild(subpict); } QDomElement pgc = dvddoc.createElement(QStringLiteral("pgc")); pgc.setAttribute(QStringLiteral("entry"), QStringLiteral("root")); menus.appendChild(pgc); QDomElement pre = dvddoc.createElement(QStringLiteral("pre")); pgc.appendChild(pre); QDomText nametext = dvddoc.createTextNode(QStringLiteral("{g1 = 0;}")); pre.appendChild(nametext); QDomElement menuvob = dvddoc.createElement(QStringLiteral("vob")); menuvob.setAttribute(QStringLiteral("file"), menuMovieUrl); pgc.appendChild(menuvob); for (int i = 0; i < buttons.count(); ++i) { QDomElement button = dvddoc.createElement(QStringLiteral("button")); button.setAttribute(QStringLiteral("name"), 'b' + QString::number(i)); nametext = dvddoc.createTextNode(QLatin1Char('{') + buttonsTarget.at(i) + QStringLiteral(";}")); button.appendChild(nametext); pgc.appendChild(button); } if (m_pageMenu->loopMovie()) { QDomElement menuloop = dvddoc.createElement(QStringLiteral("post")); nametext = dvddoc.createTextNode(QStringLiteral("jump titleset 1 menu;")); menuloop.appendChild(nametext); pgc.appendChild(menuloop); } else { menuvob.setAttribute(QStringLiteral("pause"), QStringLiteral("inf")); } } QDomElement titles = dvddoc.createElement(QStringLiteral("titles")); titleset.appendChild(titles); QDomElement video = dvddoc.createElement(QStringLiteral("video")); titles.appendChild(video); switch (m_pageVob->dvdFormat()) { case PAL_WIDE: video.setAttribute(QStringLiteral("format"), QStringLiteral("pal")); video.setAttribute(QStringLiteral("aspect"), QStringLiteral("16:9")); break; case NTSC_WIDE: video.setAttribute(QStringLiteral("format"), QStringLiteral("ntsc")); video.setAttribute(QStringLiteral("aspect"), QStringLiteral("16:9")); break; case NTSC: video.setAttribute(QStringLiteral("format"), QStringLiteral("ntsc")); video.setAttribute(QStringLiteral("aspect"), QStringLiteral("4:3")); break; default: video.setAttribute(QStringLiteral("format"), QStringLiteral("pal")); video.setAttribute(QStringLiteral("aspect"), QStringLiteral("4:3")); break; } QDomElement pgc2; // Get list of clips QStringList voburls = m_pageVob->selectedUrls(); for (int i = 0; i < voburls.count(); ++i) { if (!voburls.at(i).isEmpty()) { // Add vob entry pgc2 = dvddoc.createElement(QStringLiteral("pgc")); pgc2.setAttribute(QStringLiteral("pause"), 0); titles.appendChild(pgc2); QDomElement vob = dvddoc.createElement(QStringLiteral("vob")); vob.setAttribute(QStringLiteral("file"), voburls.at(i)); // Add chapters QStringList chaptersList = m_pageChapters->chapters(i); if (!chaptersList.isEmpty()) { vob.setAttribute(QStringLiteral("chapters"), chaptersList.join(QLatin1Char(','))); } pgc2.appendChild(vob); if (m_pageMenu->createMenu()) { QDomElement post = dvddoc.createElement(QStringLiteral("post")); QDomText call; if (i == voburls.count() - 1) { call = dvddoc.createTextNode(QStringLiteral("{g1 = 0; call menu;}")); } else { call = dvddoc.createTextNode("{if ( g1 eq 999 ) { call menu; } jump title " + QString::number(i + 2).rightJustified(2, '0') + QStringLiteral(";}")); } post.appendChild(call); pgc2.appendChild(post); } } } QFile data2(m_authorFile.fileName()); if (data2.open(QFile::WriteOnly)) { data2.write(dvddoc.toString().toUtf8()); } data2.close(); /*//qCDebug(KDENLIVE_LOG) << "------------------"; //qCDebug(KDENLIVE_LOG) << dvddoc.toString(); //qCDebug(KDENLIVE_LOG) << "------------------";*/ QStringList args; args << QStringLiteral("-x") << m_authorFile.fileName(); // qCDebug(KDENLIVE_LOG) << "// DVDAUTH ARGS: " << args; if (m_dvdauthor) { m_dvdauthor->blockSignals(true); m_dvdauthor->close(); delete m_dvdauthor; m_dvdauthor = nullptr; } m_creationLog.clear(); m_dvdauthor = new QProcess(this); // Set VIDEO_FORMAT variable (required by dvdauthor 0.7) QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); env.insert(QStringLiteral("VIDEO_FORMAT"), m_pageVob->dvdFormat() == PAL || m_pageVob->dvdFormat() == PAL_WIDE ? "PAL" : "NTSC"); m_dvdauthor->setProcessEnvironment(env); connect(m_dvdauthor, static_cast(&QProcess::finished), this, &DvdWizard::slotRenderFinished); connect(m_dvdauthor, &QProcess::readyReadStandardOutput, this, &DvdWizard::slotShowRenderInfo); m_dvdauthor->setProcessChannelMode(QProcess::MergedChannels); m_dvdauthor->start(QStringLiteral("dvdauthor"), args); m_status.button_abort->setEnabled(true); button(QWizard::FinishButton)->setEnabled(false); } void DvdWizard::slotProcessMenuStatus(int, QProcess::ExitStatus status) { if (status == QProcess::CrashExit) { // qCDebug(KDENLIVE_LOG) << "/// RENDERING MENU vob crashed"; errorMessage(i18n("Rendering menu crashed")); QByteArray result = m_menuJob.readAllStandardError(); if (m_vobitem) { m_vobitem->setIcon(QIcon::fromTheme(QStringLiteral("dialog-close"))); } m_status.error_log->append(result); m_status.error_box->setHidden(false); m_status.button_start->setEnabled(true); m_status.button_abort->setEnabled(false); return; } if (m_vobitem) { m_vobitem->setIcon(QIcon::fromTheme(QStringLiteral("dialog-ok"))); } processSpumux(); } void DvdWizard::slotShowRenderInfo() { QString log = QString(m_dvdauthor->readAll()); m_status.error_log->append(log); m_status.error_box->setHidden(false); } void DvdWizard::errorMessage(const QString &text) { m_isoMessage->setText(text); m_isoMessage->setMessageType(KMessageWidget::Error); m_isoMessage->animatedShow(); } void DvdWizard::infoMessage(const QString &text) { m_isoMessage->setText(text); m_isoMessage->setMessageType(KMessageWidget::Positive); m_isoMessage->animatedShow(); } void DvdWizard::slotRenderFinished(int exitCode, QProcess::ExitStatus status) { QListWidgetItem *authitem = m_status.job_progress->item(3); if (status == QProcess::CrashExit || exitCode != 0) { errorMessage(i18n("DVDAuthor process crashed")); QString result(m_dvdauthor->readAllStandardError()); result.append(QStringLiteral("

    ")); result.append(i18n("DVDAuthor process crashed.
    ")); m_status.error_log->append(result); m_status.error_log->scrollToAnchor(QStringLiteral("result")); m_status.error_box->setHidden(false); m_status.menu_file->setPlainText(m_menuFile.readAll()); m_status.dvd_file->setPlainText(m_authorFile.readAll()); // qCDebug(KDENLIVE_LOG) << "DVDAuthor process crashed"; authitem->setIcon(QIcon::fromTheme(QStringLiteral("dialog-close"))); m_dvdauthor->close(); delete m_dvdauthor; m_dvdauthor = nullptr; m_status.button_start->setEnabled(true); m_status.button_abort->setEnabled(false); cleanup(); button(QWizard::FinishButton)->setEnabled(true); return; } m_creationLog.append(m_dvdauthor->readAllStandardError()); m_dvdauthor->close(); delete m_dvdauthor; m_dvdauthor = nullptr; // Check if DVD structure has the necessary info if (!QFile::exists(m_status.tmp_folder->url().toLocalFile() + QStringLiteral("/DVD/VIDEO_TS/VIDEO_TS.IFO"))) { errorMessage(i18n("DVD structure broken")); m_status.error_log->append(m_creationLog + QStringLiteral("

    ") + i18n("DVD structure broken")); m_status.error_log->scrollToAnchor(QStringLiteral("result")); m_status.error_box->setHidden(false); m_status.menu_file->setPlainText(m_menuFile.readAll()); m_status.dvd_file->setPlainText(m_authorFile.readAll()); // qCDebug(KDENLIVE_LOG) << "DVDAuthor process crashed"; authitem->setIcon(QIcon::fromTheme(QStringLiteral("dialog-close"))); m_status.button_start->setEnabled(true); m_status.button_abort->setEnabled(false); cleanup(); button(QWizard::FinishButton)->setEnabled(true); return; } authitem->setIcon(QIcon::fromTheme(QStringLiteral("dialog-ok"))); QStringList args; args << QStringLiteral("-dvd-video") << QStringLiteral("-v") << QStringLiteral("-o") << m_status.iso_image->url().toLocalFile() << m_status.tmp_folder->url().toLocalFile() + QDir::separator() + QStringLiteral("DVD"); if (m_mkiso) { m_mkiso->blockSignals(true); m_mkiso->close(); delete m_mkiso; m_mkiso = nullptr; } m_mkiso = new QProcess(this); connect(m_mkiso, static_cast(&QProcess::finished), this, &DvdWizard::slotIsoFinished); connect(m_mkiso, &QProcess::readyReadStandardOutput, this, &DvdWizard::slotShowIsoInfo); m_mkiso->setProcessChannelMode(QProcess::MergedChannels); QListWidgetItem *isoitem = m_status.job_progress->item(4); m_status.job_progress->setCurrentRow(4); isoitem->setIcon(QIcon::fromTheme(QStringLiteral("system-run"))); if (!QStandardPaths::findExecutable(QStringLiteral("genisoimage")).isEmpty()) { m_mkiso->start(QStringLiteral("genisoimage"), args); } else { m_mkiso->start(QStringLiteral("mkisofs"), args); } } void DvdWizard::slotShowIsoInfo() { QString log = QString(m_mkiso->readAll()); m_status.error_log->append(log); m_status.error_box->setHidden(false); } void DvdWizard::slotIsoFinished(int exitCode, QProcess::ExitStatus status) { button(QWizard::FinishButton)->setEnabled(true); QListWidgetItem *isoitem = m_status.job_progress->item(4); if (status == QProcess::CrashExit || exitCode != 0) { errorMessage(i18n("ISO creation process crashed.")); QString result(m_mkiso->readAllStandardError()); result.append(QStringLiteral("

    ")); result.append(i18n("ISO creation process crashed.")); m_status.error_log->append(result); m_status.error_log->scrollToAnchor(QStringLiteral("result")); m_status.error_box->setHidden(false); m_status.menu_file->setPlainText(m_menuFile.readAll()); m_status.dvd_file->setPlainText(m_authorFile.readAll()); m_mkiso->close(); delete m_mkiso; m_mkiso = nullptr; cleanup(); // qCDebug(KDENLIVE_LOG) << "Iso process crashed"; isoitem->setIcon(QIcon::fromTheme(QStringLiteral("dialog-close"))); m_status.button_start->setEnabled(true); m_status.button_abort->setEnabled(false); return; } m_creationLog.append(m_mkiso->readAllStandardError()); delete m_mkiso; m_mkiso = nullptr; m_status.button_start->setEnabled(true); m_status.button_abort->setEnabled(false); // Check if DVD iso is ok QFile iso(m_status.iso_image->url().toLocalFile()); if (!iso.exists() || iso.size() == 0) { if (iso.exists()) { iso.remove(); } errorMessage(i18n("DVD ISO is broken")); m_status.error_log->append(m_creationLog + QStringLiteral("
    ") + i18n("DVD ISO is broken") + QStringLiteral("")); m_status.error_log->scrollToAnchor(QStringLiteral("result")); m_status.error_box->setHidden(false); m_status.menu_file->setPlainText(m_menuFile.readAll()); m_status.dvd_file->setPlainText(m_authorFile.readAll()); isoitem->setIcon(QIcon::fromTheme(QStringLiteral("dialog-close"))); cleanup(); return; } isoitem->setIcon(QIcon::fromTheme(QStringLiteral("dialog-ok"))); // qCDebug(KDENLIVE_LOG) << "ISO IMAGE " << m_status.iso_image->url().toLocalFile() << " Successfully created"; cleanup(); // qCDebug(KDENLIVE_LOG) << m_creationLog; infoMessage(i18n("DVD ISO image %1 successfully created.", m_status.iso_image->url().toLocalFile())); m_status.error_log->append(QStringLiteral("") + i18n("DVD ISO image %1 successfully created.", m_status.iso_image->url().toLocalFile()) + QStringLiteral("")); m_status.error_log->scrollToAnchor(QStringLiteral("result")); m_status.button_preview->setEnabled(true); m_status.button_burn->setEnabled(true); m_status.error_box->setHidden(false); // KMessageBox::information(this, i18n("DVD ISO image %1 successfully created.", m_status.iso_image->url().toLocalFile())); } void DvdWizard::cleanup() { QDir dir(m_status.tmp_folder->url().toLocalFile() + QDir::separator() + QStringLiteral("DVD")); // Try to make sure we delete the correct directory if (dir.exists() && dir.dirName() == QLatin1String("DVD")) { dir.removeRecursively(); } } void DvdWizard::slotPreview() { const QStringList programNames = {QStringLiteral("xine"), QStringLiteral("vlc")}; QString exec; for (const QString &prog : programNames) { exec = QStandardPaths::findExecutable(prog); if (!exec.isEmpty()) { break; } } if (exec.isEmpty()) { KMessageBox::sorry(this, i18n("Previewing requires one of these applications (%1)", programNames.join(","))); } else { QProcess::startDetached(exec, QStringList() << "dvd://" + m_status.iso_image->url().toLocalFile()); } } void DvdWizard::slotBurn() { - QAction *action = qobject_cast(sender()); + auto *action = qobject_cast(sender()); QString exec = action->data().toString(); QStringList args; if (exec.endsWith(QLatin1String("k3b"))) { args << QStringLiteral("--image") << m_status.iso_image->url().toLocalFile(); } else { args << "--image=" + m_status.iso_image->url().toLocalFile(); } QProcess::startDetached(exec, args); } void DvdWizard::slotGenerate() { // clear job icons if (((m_dvdauthor != nullptr) && m_dvdauthor->state() != QProcess::NotRunning) || ((m_mkiso != nullptr) && m_mkiso->state() != QProcess::NotRunning)) { return; } for (int i = 0; i < m_status.job_progress->count(); ++i) { m_status.job_progress->item(i)->setIcon(QIcon()); } QString warnMessage; if (QFile::exists(m_status.tmp_folder->url().toLocalFile() + QStringLiteral("DVD"))) { warnMessage.append(i18n("Folder %1 already exists. Overwrite?\n", m_status.tmp_folder->url().toLocalFile() + QStringLiteral("DVD"))); } if (QFile::exists(m_status.iso_image->url().toLocalFile())) { warnMessage.append(i18n("Image file %1 already exists. Overwrite?", m_status.iso_image->url().toLocalFile())); } if (warnMessage.isEmpty() || KMessageBox::questionYesNo(this, warnMessage) == KMessageBox::Yes) { cleanup(); QTimer::singleShot(300, this, &DvdWizard::generateDvd); m_status.button_preview->setEnabled(false); m_status.button_burn->setEnabled(false); m_status.job_progress->setEnabled(true); m_status.button_start->setEnabled(false); } } void DvdWizard::slotAbort() { // clear job icons if ((m_dvdauthor != nullptr) && m_dvdauthor->state() != QProcess::NotRunning) { m_dvdauthor->terminate(); } else if ((m_mkiso != nullptr) && m_mkiso->state() != QProcess::NotRunning) { m_mkiso->terminate(); } } void DvdWizard::slotSave() { QString projectFolder = KRecentDirs::dir(QStringLiteral(":KdenliveDvdFolder")); if (projectFolder.isEmpty()) { projectFolder = QDir::homePath(); } QUrl url = QFileDialog::getSaveFileUrl(this, i18n("Save DVD Project"), QUrl::fromLocalFile(projectFolder), i18n("DVD project (*.kdvd)")); if (!url.isValid()) { return; } KRecentDirs::add(QStringLiteral(":KdenliveDvdFolder"), url.adjusted(QUrl::RemoveFilename).toLocalFile()); if (currentId() == 0) { m_pageChapters->setVobFiles(m_pageVob->dvdFormat(), m_pageVob->selectedUrls(), m_pageVob->durations(), m_pageVob->chapters()); } QDomDocument doc; QDomElement dvdproject = doc.createElement(QStringLiteral("dvdproject")); dvdproject.setAttribute(QStringLiteral("profile"), m_pageVob->dvdProfile()); dvdproject.setAttribute(QStringLiteral("tmp_folder"), m_status.tmp_folder->url().toLocalFile()); dvdproject.setAttribute(QStringLiteral("iso_image"), m_status.iso_image->url().toLocalFile()); dvdproject.setAttribute(QStringLiteral("intro_movie"), m_pageVob->introMovie()); doc.appendChild(dvdproject); QDomElement menu = m_pageMenu->toXml(); if (!menu.isNull()) { dvdproject.appendChild(doc.importNode(menu, true)); } QDomElement chaps = m_pageChapters->toXml(); if (!chaps.isNull()) { dvdproject.appendChild(doc.importNode(chaps, true)); } QFile file(url.toLocalFile()); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { qCWarning(KDENLIVE_LOG) << "////// ERROR writing to file: " << url.toLocalFile(); KMessageBox::error(this, i18n("Cannot write to file %1", url.toLocalFile())); return; } file.write(doc.toString().toUtf8()); if (file.error() != QFile::NoError) { KMessageBox::error(this, i18n("Cannot write to file %1", url.toLocalFile())); } file.close(); } void DvdWizard::slotLoad() { QString projectFolder = KRecentDirs::dir(QStringLiteral(":KdenliveDvdFolder")); if (projectFolder.isEmpty()) { projectFolder = QDir::homePath(); } const QUrl url = QFileDialog::getOpenFileUrl(this, QString(), QUrl::fromLocalFile(projectFolder), i18n("DVD project (*.kdvd)")); if (!url.isValid()) { return; } KRecentDirs::add(QStringLiteral(":KdenliveDvdFolder"), url.adjusted(QUrl::RemoveFilename).toLocalFile()); QDomDocument doc; QFile file(url.toLocalFile()); doc.setContent(&file, false); file.close(); QDomElement dvdproject = doc.documentElement(); if (dvdproject.tagName() != QLatin1String("dvdproject")) { KMessageBox::error(this, i18n("File %1 is not a Kdenlive project file.", url.toLocalFile())); return; } QString profile = dvdproject.attribute(QStringLiteral("profile")); m_pageVob->setProfile(profile); m_pageVob->clear(); m_status.tmp_folder->setUrl(QUrl::fromLocalFile(dvdproject.attribute(QStringLiteral("tmp_folder")))); m_status.iso_image->setUrl(QUrl::fromLocalFile(dvdproject.attribute(QStringLiteral("iso_image")))); QString intro = dvdproject.attribute(QStringLiteral("intro_movie")); if (!intro.isEmpty()) { m_pageVob->slotAddVobFile(QUrl::fromLocalFile(intro)); m_pageVob->setUseIntroMovie(true); } QDomNodeList vobs = doc.elementsByTagName(QStringLiteral("vob")); for (int i = 0; i < vobs.count(); ++i) { QDomElement e = vobs.at(i).toElement(); m_pageVob->slotAddVobFile(QUrl::fromLocalFile(e.attribute(QStringLiteral("file"))), e.attribute(QStringLiteral("chapters"))); } m_pageMenu->loadXml(m_pageVob->dvdFormat(), dvdproject.firstChildElement(QStringLiteral("menu"))); } diff --git a/src/dvdwizard/dvdwizard.h b/src/dvdwizard/dvdwizard.h index b16374701..5dbd7fe29 100644 --- a/src/dvdwizard/dvdwizard.h +++ b/src/dvdwizard/dvdwizard.h @@ -1,91 +1,91 @@ /*************************************************************************** * 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 DVDWIZARD_H #define DVDWIZARD_H #include "dvdwizard/dvdwizardchapters.h" #include "dvdwizard/dvdwizardmenu.h" #include "dvdwizard/dvdwizardvob.h" #include "ui_dvdwizardchapters_ui.h" #include "ui_dvdwizardstatus_ui.h" #include #include #include typedef QMap stringRectMap; class DvdWizard : public QWizard { Q_OBJECT public: explicit DvdWizard(MonitorManager *manager, const QString &url = QString(), QWidget *parent = nullptr); - virtual ~DvdWizard(); + ~DvdWizard() override; void processSpumux(); private: DvdWizardVob *m_pageVob; DvdWizardMenu *m_pageMenu; Ui::DvdWizardStatus_UI m_status; KMessageWidget *m_isoMessage; DvdWizardChapters *m_pageChapters; QTemporaryFile m_authorFile; QTemporaryFile m_menuFile; QTemporaryFile m_menuVobFile; QTemporaryFile m_letterboxMovie; QProcess *m_dvdauthor; QProcess *m_mkiso; QProcess m_menuJob; QString m_creationLog; QListWidgetItem *m_vobitem; QTemporaryFile m_selectedImage; QTemporaryFile m_selectedLetterImage; QTemporaryFile m_highlightedImage; QTemporaryFile m_highlightedLetterImage; QTemporaryFile m_menuVideo; QTemporaryFile m_menuFinalVideo; QTemporaryFile m_menuImageBackground; QMenu *m_burnMenu; int m_previousPage; void cleanup(); void errorMessage(const QString &text); void infoMessage(const QString &text); void processDvdauthor(const QString &menuMovieUrl = QString(), const stringRectMap &buttons = stringRectMap(), const QStringList &buttonsTarget = QStringList()); private slots: void slotPageChanged(int page); void slotRenderFinished(int exitCode, QProcess::ExitStatus status); void slotIsoFinished(int exitCode, QProcess::ExitStatus status); void generateDvd(); void slotPreview(); void slotBurn(); void slotGenerate(); void slotAbort(); void slotLoad(); void slotSave(); void slotShowRenderInfo(); void slotShowIsoInfo(); void slotProcessMenuStatus(int, QProcess::ExitStatus status); }; #endif diff --git a/src/dvdwizard/dvdwizardchapters.h b/src/dvdwizard/dvdwizardchapters.h index 7150788f2..d9bdf62a5 100644 --- a/src/dvdwizard/dvdwizardchapters.h +++ b/src/dvdwizard/dvdwizardchapters.h @@ -1,64 +1,64 @@ /*************************************************************************** * Copyright (C) 2009 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 DVDWIZARDCHAPTERS_H #define DVDWIZARDCHAPTERS_H #include "dvdwizardvob.h" #include "monitor/monitor.h" #include "monitor/monitormanager.h" #include "ui_dvdwizardchapters_ui.h" #include class DvdWizardChapters : public QWizardPage { Q_OBJECT public: explicit DvdWizardChapters(MonitorManager *manager, DVDFORMAT format, QWidget *parent = nullptr); - virtual ~DvdWizardChapters(); + ~DvdWizardChapters() override; void changeProfile(DVDFORMAT format); void setPal(bool isPal); void setVobFiles(DVDFORMAT format, const QStringList &movies, const QStringList &durations, const QStringList &chapters); QStringList selectedTitles() const; QStringList selectedTargets() const; QStringList chapters(int ix) const; QDomElement toXml() const; QMap chaptersData() const; void stopMonitor(); void createMonitor(DVDFORMAT format); private: Ui::DvdWizardChapters_UI m_view; DVDFORMAT m_format; Monitor *m_monitor; MonitorManager *m_manager; Timecode m_tc; void updateMonitorMarkers(); private slots: void slotUpdateChaptersList(); void slotAddChapter(); void slotRemoveChapter(); void slotGoToChapter(); void slotEnableChapters(int state); }; #endif diff --git a/src/dvdwizard/dvdwizardmenu.cpp b/src/dvdwizard/dvdwizardmenu.cpp index 920e0e4b7..859700c90 100644 --- a/src/dvdwizard/dvdwizardmenu.cpp +++ b/src/dvdwizard/dvdwizardmenu.cpp @@ -1,914 +1,912 @@ /*************************************************************************** * Copyright (C) 2009 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 "dvdwizardmenu.h" #include "kdenlivesettings.h" #include "doc/kthumb.h" #include "kdenlive_debug.h" #include "klocalizedstring.h" #include #include #include #include enum { DvdButtonItem = QGraphicsItem::UserType + 1, DvdButtonUnderlineItem = QGraphicsItem::UserType + 2 }; DvdScene::DvdScene(QObject *parent) : QGraphicsScene(parent) - , m_width(0) - , m_height(0) - , m_gridSize(1) + { } void DvdScene::setProfile(int width, int height) { m_width = width; m_height = height; setSceneRect(0, 0, m_width, m_height); } int DvdScene::gridSize() const { return m_gridSize; } void DvdScene::setGridSize(int gridSize) { m_gridSize = gridSize; } void DvdScene::mouseReleaseEvent(QGraphicsSceneMouseEvent *mouseEvent) { QGraphicsScene::mouseReleaseEvent(mouseEvent); emit sceneChanged(); } void DvdScene::drawForeground(QPainter *painter, const QRectF &rect) { // draw the grid if needed if (gridSize() <= 1) { return; } QPen pen(QColor(255, 0, 0, 100)); painter->setPen(pen); qreal left = int(rect.left()) - (int(rect.left()) % m_gridSize); qreal top = int(rect.top()) - (int(rect.top()) % m_gridSize); QVector points; for (qreal x = left; x < rect.right(); x += m_gridSize) { for (qreal y = top; y < rect.bottom(); y += m_gridSize) { points.append(QPointF(x, y)); } } painter->drawPoints(points.data(), points.size()); } DvdButton::DvdButton(const QString &text) : QGraphicsTextItem(text) , m_target(0) , m_command(QStringLiteral("jump title 1")) , m_backToMenu(false) { setFlags(QGraphicsItem::ItemIsMovable | QGraphicsItem::ItemIsSelectable); setFlag(QGraphicsItem::ItemSendsGeometryChanges, true); } void DvdButton::setTarget(int t, const QString &c) { m_target = t; m_command = c; } int DvdButton::target() const { return m_target; } QString DvdButton::command() const { return m_command; } bool DvdButton::backMenu() const { return m_backToMenu; } int DvdButton::type() const { // Enable the use of qgraphicsitem_cast with this item. return UserType + 1; } void DvdButton::setBackMenu(bool back) { m_backToMenu = back; } QVariant DvdButton::itemChange(GraphicsItemChange change, const QVariant &value) { if (change == ItemPositionChange && (scene() != nullptr)) { QPoint newPos = value.toPoint(); if (QApplication::mouseButtons() == Qt::LeftButton && (qobject_cast(scene()) != nullptr)) { - DvdScene *customScene = qobject_cast(scene()); + auto *customScene = qobject_cast(scene()); int gridSize = customScene->gridSize(); int xV = (newPos.x() / gridSize) * gridSize; int yV = (newPos.y() / gridSize) * gridSize; newPos = QPoint(xV, yV); } QRectF sceneShape = sceneBoundingRect(); - DvdScene *sc = static_cast(scene()); + auto *sc = static_cast(scene()); newPos.setX(qMax(newPos.x(), 0)); newPos.setY(qMax(newPos.y(), 0)); if (newPos.x() + sceneShape.width() > sc->width()) { newPos.setX(sc->width() - sceneShape.width()); } if (newPos.y() + sceneShape.height() > sc->height()) { newPos.setY(sc->height() - sceneShape.height()); } sceneShape.translate(newPos - pos()); QList list = scene()->items(sceneShape, Qt::IntersectsItemShape); list.removeAll(this); if (!list.isEmpty()) { for (int i = 0; i < list.count(); ++i) { if (list.at(i)->type() == Type) { return pos(); } } } return newPos; } return QGraphicsItem::itemChange(change, value); } DvdWizardMenu::DvdWizardMenu(DVDFORMAT format, QWidget *parent) : QWizardPage(parent) , m_color(nullptr) , m_safeRect(nullptr) , m_finalSize(720, 576) , m_movieLength(-1) { m_view.setupUi(this); m_view.play_text->setText(i18n("Play")); m_scene = new DvdScene(this); m_view.menu_preview->setScene(m_scene); m_view.menu_preview->setMouseTracking(true); connect(m_view.create_menu, &QAbstractButton::toggled, m_view.menu_box, &QWidget::setEnabled); connect(m_view.create_menu, &QAbstractButton::toggled, this, &QWizardPage::completeChanged); m_view.add_button->setIcon(QIcon::fromTheme(QStringLiteral("document-new"))); m_view.delete_button->setIcon(QIcon::fromTheme(QStringLiteral("trash-empty"))); m_view.zoom_button->setIcon(QIcon::fromTheme(QStringLiteral("zoom-in"))); m_view.unzoom_button->setIcon(QIcon::fromTheme(QStringLiteral("zoom-out"))); m_view.add_button->setToolTip(i18n("Add new button")); m_view.delete_button->setToolTip(i18n("Delete current button")); changeProfile(format); // Create color background m_color = new QGraphicsRectItem(0, 0, m_width, m_height); m_color->setBrush(m_view.background_color->color()); m_color->setZValue(2); m_scene->addItem(m_color); // create background image m_background = new QGraphicsPixmapItem(); m_background->setZValue(3); // m_scene->addItem(m_background); // create safe zone rect int safeW = m_width / 20; int safeH = m_height / 20; m_safeRect = new QGraphicsRectItem(safeW, safeH, m_width - 2 * safeW, m_height - 2 * safeH); QPen pen(Qt::red); pen.setStyle(Qt::DashLine); pen.setWidth(3); m_safeRect->setPen(pen); m_safeRect->setZValue(5); m_scene->addItem(m_safeRect); checkBackgroundType(0); // create menu button DvdButton *button = new DvdButton(m_view.play_text->text()); QFont font = m_view.font_family->currentFont(); font.setPixelSize(m_view.font_size->value()); // font.setStyleStrategy(QFont::NoAntialias); if (m_view.use_shadow->isChecked()) { auto *shadow = new QGraphicsDropShadowEffect(this); shadow->setBlurRadius(7); shadow->setOffset(4, 4); button->setGraphicsEffect(shadow); } connect(m_view.use_shadow, &QCheckBox::stateChanged, this, &DvdWizardMenu::slotEnableShadows); button->setFont(font); button->setDefaultTextColor(m_view.text_color->color()); button->setZValue(4); QRectF r = button->sceneBoundingRect(); m_scene->addItem(button); button->setPos((m_width - r.width()) / 2, (m_height - r.height()) / 2); button->setSelected(true); if (m_view.use_grid->isChecked()) { m_scene->setGridSize(10); } connect(m_view.use_grid, &QAbstractButton::toggled, this, &DvdWizardMenu::slotUseGrid); // m_view.menu_preview->resizefitInView(0, 0, m_width, m_height); connect(m_view.play_text, &QLineEdit::textChanged, this, &DvdWizardMenu::buildButton); connect(m_view.text_color, &KColorButton::changed, this, static_cast(&DvdWizardMenu::updateColor)); connect(m_view.font_size, static_cast(&QSpinBox::valueChanged), this, &DvdWizardMenu::buildButton); connect(m_view.font_family, &QFontComboBox::currentFontChanged, this, &DvdWizardMenu::buildButton); connect(m_view.background_image, &KUrlRequester::textChanged, this, &DvdWizardMenu::buildImage); connect(m_view.background_color, &KColorButton::changed, this, &DvdWizardMenu::buildColor); connect(m_view.background_list, static_cast(&KComboBox::currentIndexChanged), this, &DvdWizardMenu::checkBackgroundType); connect(m_view.target_list, static_cast(&KComboBox::activated), this, &DvdWizardMenu::setButtonTarget); connect(m_view.back_to_menu, &QAbstractButton::toggled, this, &DvdWizardMenu::setBackToMenu); connect(m_view.add_button, &QAbstractButton::pressed, this, &DvdWizardMenu::addButton); connect(m_view.delete_button, &QAbstractButton::pressed, this, &DvdWizardMenu::deleteButton); connect(m_view.zoom_button, &QAbstractButton::pressed, this, &DvdWizardMenu::slotZoom); connect(m_view.unzoom_button, &QAbstractButton::pressed, this, &DvdWizardMenu::slotUnZoom); connect(m_scene, &QGraphicsScene::selectionChanged, this, &DvdWizardMenu::buttonChanged); connect(m_scene, &DvdScene::sceneChanged, this, &QWizardPage::completeChanged); // red background for error message KColorScheme scheme(palette().currentColorGroup(), KColorScheme::Window); QPalette p = m_view.error_message->palette(); p.setColor(QPalette::Background, scheme.background(KColorScheme::NegativeBackground).color()); m_view.error_message->setAutoFillBackground(true); m_view.error_message->setPalette(p); m_view.menu_box->setEnabled(false); m_menuMessage = new KMessageWidget; - QGridLayout *s = static_cast(layout()); + auto *s = static_cast(layout()); s->addWidget(m_menuMessage, 7, 0, 1, -1); m_menuMessage->hide(); m_view.error_message->hide(); } DvdWizardMenu::~DvdWizardMenu() { delete m_color; delete m_safeRect; delete m_background; delete m_scene; } void DvdWizardMenu::slotEnableShadows(int enable) { QList list = m_scene->items(); for (int i = 0; i < list.count(); ++i) { if (list.at(i)->type() == DvdButtonItem) { if (enable != 0) { auto *shadow = new QGraphicsDropShadowEffect(this); shadow->setBlurRadius(7); shadow->setOffset(4, 4); list.at(i)->setGraphicsEffect(shadow); } else { list.at(i)->setGraphicsEffect(nullptr); } } } } void DvdWizardMenu::setButtonTarget(int ix) { QList list = m_scene->selectedItems(); for (int i = 0; i < list.count(); ++i) { if (list.at(i)->type() == DvdButtonItem) { - DvdButton *button = static_cast(list.at(i)); + auto *button = static_cast(list.at(i)); button->setTarget(ix, m_view.target_list->itemData(ix).toString()); break; } } emit completeChanged(); } void DvdWizardMenu::setBackToMenu(bool backToMenu) { QList list = m_scene->selectedItems(); for (int i = 0; i < list.count(); ++i) { if (list.at(i)->type() == DvdButtonItem) { - DvdButton *button = static_cast(list.at(i)); + auto *button = static_cast(list.at(i)); button->setBackMenu(backToMenu); break; } } emit completeChanged(); } void DvdWizardMenu::buttonChanged() { QList list = m_scene->selectedItems(); bool foundButton = false; for (int i = 0; i < list.count(); ++i) { if (list.at(i)->type() == DvdButtonItem) { m_view.play_text->blockSignals(true); m_view.font_size->blockSignals(true); m_view.font_family->blockSignals(true); m_view.target_list->blockSignals(true); m_view.back_to_menu->blockSignals(true); foundButton = true; m_view.tabWidget->widget(0)->setEnabled(true); - DvdButton *button = static_cast(list.at(i)); + auto *button = static_cast(list.at(i)); m_view.target_list->setCurrentIndex(button->target()); m_view.play_text->setText(button->toPlainText()); m_view.back_to_menu->setChecked(button->backMenu()); QFont font = button->font(); m_view.font_size->setValue(font.pixelSize()); m_view.font_family->setCurrentFont(font); m_view.play_text->blockSignals(false); m_view.font_size->blockSignals(false); m_view.font_family->blockSignals(false); m_view.target_list->blockSignals(false); m_view.back_to_menu->blockSignals(false); break; } } if (!foundButton) { m_view.tabWidget->widget(0)->setEnabled(false); } } void DvdWizardMenu::addButton() { m_scene->clearSelection(); DvdButton *button = new DvdButton(m_view.play_text->text()); QFont font = m_view.font_family->currentFont(); font.setPixelSize(m_view.font_size->value()); if (m_view.use_shadow->isChecked()) { auto *shadow = new QGraphicsDropShadowEffect(this); shadow->setBlurRadius(7); shadow->setOffset(4, 4); button->setGraphicsEffect(shadow); } // font.setStyleStrategy(QFont::NoAntialias); button->setFont(font); button->setDefaultTextColor(m_view.text_color->color()); button->setZValue(4); QRectF r = button->sceneBoundingRect(); m_scene->addItem(button); button->setPos((m_width - r.width()) / 2, (m_height - r.height()) / 2); button->setSelected(true); } void DvdWizardMenu::deleteButton() { QList list = m_scene->selectedItems(); for (int i = 0; i < list.count(); ++i) { if (list.at(i)->type() == DvdButtonItem) { delete list.at(i); break; } } } void DvdWizardMenu::changeProfile(DVDFORMAT format) { m_format = format; switch (m_format) { case PAL_WIDE: m_finalSize = QSize(720, 576); m_width = 1024; m_height = 576; break; case NTSC_WIDE: m_finalSize = QSize(720, 480); m_width = 853; m_height = 480; break; case NTSC: m_finalSize = QSize(720, 480); m_width = 640; m_height = 480; break; default: m_finalSize = QSize(720, 576); m_width = 768; m_height = 576; } updatePreview(); } void DvdWizardMenu::updatePreview() { m_scene->setProfile(m_width, m_height); QMatrix matrix; matrix.scale(0.5, 0.5); m_view.menu_preview->setMatrix(matrix); m_view.menu_preview->setMinimumSize(m_width / 2 + 4, m_height / 2 + 8); if (m_color) { m_color->setRect(0, 0, m_width, m_height); } int safeW = m_width / 20; int safeH = m_height / 20; if (m_safeRect) { m_safeRect->setRect(safeW, safeH, m_width - 2 * safeW, m_height - 2 * safeH); } } void DvdWizardMenu::setTargets(const QStringList &list, const QStringList &targetlist) { m_view.target_list->clear(); m_view.target_list->addItem(i18n("Play All"), "jump title 1"); int movieCount = 0; for (int i = 0; i < list.count(); ++i) { if (targetlist.at(i).contains(QStringLiteral("chapter"))) { m_view.target_list->addItem(list.at(i), targetlist.at(i)); } else { m_view.target_list->addItem(QIcon::fromTheme(QStringLiteral("video-x-generic")), list.at(i), targetlist.at(i)); movieCount++; } } m_view.back_to_menu->setHidden(movieCount == 1); } void DvdWizardMenu::checkBackgroundType(int ix) { if (ix == 0) { m_view.background_color->setVisible(true); m_view.background_image->setVisible(false); m_view.loop_movie->setVisible(false); if (m_background->scene() != nullptr) { m_scene->removeItem(m_background); } } else { m_view.background_color->setVisible(false); m_view.background_image->setVisible(true); if (ix == 1) { m_view.background_image->clear(); m_view.background_image->setFilter(QStringLiteral("*")); if (m_background->scene() != nullptr) { m_scene->removeItem(m_background); } m_view.loop_movie->setVisible(false); } else { if (m_background->scene() != nullptr) { m_scene->removeItem(m_background); } m_view.background_image->clear(); m_view.background_image->setFilter(QStringLiteral("video/mpeg")); m_view.loop_movie->setVisible(true); } } emit completeChanged(); } void DvdWizardMenu::buildColor() { m_color->setBrush(m_view.background_color->color()); } void DvdWizardMenu::slotUseGrid(bool useGrid) { if (useGrid) { m_scene->setGridSize(10); } else { m_scene->setGridSize(1); } m_scene->update(); } void DvdWizardMenu::buildImage() { emit completeChanged(); if (m_view.background_image->url().isEmpty()) { if (m_background->scene() != nullptr) { m_scene->removeItem(m_background); } return; } QPixmap pix; if (m_view.background_list->currentIndex() == 1) { // image background if (!pix.load(m_view.background_image->url().toLocalFile())) { if (m_background->scene() != nullptr) { m_scene->removeItem(m_background); } return; } pix = pix.scaled(m_width, m_height); } else if (m_view.background_list->currentIndex() == 2) { // video background m_movieLength = -1; QString profileName = DvdWizardVob::getDvdProfile(m_format); Mlt::Profile profile(profileName.toUtf8().constData()); profile.set_explicit(1); Mlt::Producer *producer = new Mlt::Producer(profile, m_view.background_image->url().toLocalFile().toUtf8().constData()); if ((producer != nullptr) && producer->is_valid()) { pix = QPixmap::fromImage(KThumb::getFrame(producer, 0, m_width, m_height)); m_movieLength = producer->get_length(); } delete producer; } m_background->setPixmap(pix); m_scene->addItem(m_background); } void DvdWizardMenu::buildButton() { DvdButton *button = nullptr; QList list = m_scene->selectedItems(); for (int i = 0; i < list.count(); ++i) { if (list.at(i)->type() == DvdButtonItem) { button = static_cast(list.at(i)); break; } } if (button == nullptr) { return; } button->setPlainText(m_view.play_text->text()); QFont font = m_view.font_family->currentFont(); font.setPixelSize(m_view.font_size->value()); // font.setStyleStrategy(QFont::NoAntialias); button->setFont(font); button->setDefaultTextColor(m_view.text_color->color()); // Check for button overlapping in case we changed text / size emit completeChanged(); } void DvdWizardMenu::updateColor() { updateColor(m_view.text_color->color()); m_view.menu_preview->viewport()->update(); } void DvdWizardMenu::prepareUnderLines() { QList list = m_scene->items(); for (int i = 0; i < list.count(); ++i) { if (list.at(i)->type() == DvdButtonItem) { QRectF r = list.at(i)->sceneBoundingRect(); int bottom = r.bottom() - 1; if (bottom % 2 == 1) { bottom = bottom - 1; } int underlineHeight = r.height() / 10; if (underlineHeight % 2 == 1) { underlineHeight = underlineHeight - 1; } underlineHeight = qMin(underlineHeight, 10); underlineHeight = qMax(underlineHeight, 2); r.setTop(bottom - underlineHeight); r.setBottom(bottom); r.adjust(2, 0, -2, 0); auto *underline = new DvdButtonUnderline(r); m_scene->addItem(underline); list.at(i)->setVisible(false); } } } void DvdWizardMenu::resetUnderLines() { QList list = m_scene->items(); QList toDelete; for (int i = 0; i < list.count(); ++i) { if (list.at(i)->type() == DvdButtonUnderlineItem) { toDelete.append(list.at(i)); } if (list.at(i)->type() == DvdButtonItem) { list.at(i)->setVisible(true); } } while (!toDelete.isEmpty()) { QGraphicsItem *item = toDelete.takeFirst(); delete item; } } void DvdWizardMenu::updateUnderlineColor(QColor c) { QList list = m_scene->items(); for (int i = 0; i < list.count(); ++i) { if (list.at(i)->type() == DvdButtonUnderlineItem) { - DvdButtonUnderline *underline = static_cast(list.at(i)); + auto *underline = static_cast(list.at(i)); underline->setPen(Qt::NoPen); c.setAlpha(150); underline->setBrush(c); } } } void DvdWizardMenu::updateColor(const QColor &c) { DvdButton *button = nullptr; QList list = m_scene->items(); for (int i = 0; i < list.count(); ++i) { if (list.at(i)->type() == DvdButtonItem) { button = static_cast(list.at(i)); button->setDefaultTextColor(c); } } } void DvdWizardMenu::createButtonImages(const QString &selected_image, const QString &highlighted_image, bool letterbox) { if (m_view.create_menu->isChecked()) { m_scene->clearSelection(); QRectF source(0, 0, m_width, m_height); QRectF target; if (!letterbox) { target = QRectF(0, 0, m_finalSize.width(), m_finalSize.height()); } else { // Scale the button images to fit a letterbox image double factor = (double)m_width / m_finalSize.width(); int letterboxHeight = m_height / factor; target = QRectF(0, (m_finalSize.height() - letterboxHeight) / 2, m_finalSize.width(), letterboxHeight); } if (m_safeRect->scene() != nullptr) { m_scene->removeItem(m_safeRect); } if (m_color->scene() != nullptr) { m_scene->removeItem(m_color); } if (m_background->scene() != nullptr) { m_scene->removeItem(m_background); } prepareUnderLines(); QImage img(m_finalSize.width(), m_finalSize.height(), QImage::Format_ARGB32); img.fill(Qt::transparent); updateUnderlineColor(m_view.highlighted_color->color()); QPainter p; p.begin(&img); // p.setRenderHints(QPainter::Antialiasing, false); // p.setRenderHints(QPainter::TextAntialiasing, false); m_scene->render(&p, target, source, Qt::IgnoreAspectRatio); p.end(); img.setColor(0, m_view.highlighted_color->color().rgb()); img.setColor(1, qRgba(0, 0, 0, 0)); img.save(highlighted_image); img.fill(Qt::transparent); updateUnderlineColor(m_view.selected_color->color()); p.begin(&img); // p.setRenderHints(QPainter::Antialiasing, false); // p.setRenderHints(QPainter::TextAntialiasing, false); m_scene->render(&p, target, source, Qt::IgnoreAspectRatio); p.end(); img.setColor(0, m_view.selected_color->color().rgb()); img.setColor(1, qRgba(0, 0, 0, 0)); img.save(selected_image); resetUnderLines(); m_scene->addItem(m_safeRect); m_scene->addItem(m_color); if (m_view.background_list->currentIndex() > 0) { m_scene->addItem(m_background); } } } void DvdWizardMenu::createBackgroundImage(const QString &img1, bool letterbox) { Q_UNUSED(letterbox) m_scene->clearSelection(); if (m_safeRect->scene() != nullptr) { m_scene->removeItem(m_safeRect); } bool showBg = false; QImage img(m_width, m_height, QImage::Format_ARGB32); // TODO: Should the image be scaled when letterboxing? /* QRectF source(0, 0, m_width, m_height); QRectF target; if (!letterbox) target = QRectF(0, 0, m_finalSize.width(), m_finalSize.height()); else { // Scale the button images to fit a letterbox image double factor = (double) m_width / m_finalSize.width(); int letterboxHeight = m_height / factor; target = QRectF(0, (m_finalSize.height() - letterboxHeight) / 2, m_finalSize.width(), letterboxHeight); }*/ if (menuMovie()) { showBg = true; if (m_background->scene() != nullptr) { m_scene->removeItem(m_background); } if (m_color->scene() != nullptr) { m_scene->removeItem(m_color); } img.fill(Qt::transparent); } updateColor(m_view.text_color->color()); QPainter p(&img); p.setRenderHints(QPainter::Antialiasing, true); p.setRenderHints(QPainter::TextAntialiasing, true); // set image grid to "1" to ensure we don't display dots all over // the image when rendering int oldSize = m_scene->gridSize(); m_scene->setGridSize(1); m_scene->render(&p, QRectF(0, 0, img.width(), img.height())); m_scene->setGridSize(oldSize); // m_scene->render(&p, target, source, Qt::IgnoreAspectRatio); p.end(); img.save(img1); m_scene->addItem(m_safeRect); if (showBg) { m_scene->addItem(m_background); m_scene->addItem(m_color); } } bool DvdWizardMenu::createMenu() const { return m_view.create_menu->isChecked(); } bool DvdWizardMenu::loopMovie() const { return m_view.loop_movie->isChecked(); } bool DvdWizardMenu::menuMovie() const { return m_view.background_list->currentIndex() == 2; } QString DvdWizardMenu::menuMoviePath() const { return m_view.background_image->url().toLocalFile(); } int DvdWizardMenu::menuMovieLength() const { return m_movieLength; } QMap DvdWizardMenu::buttonsInfo(bool letterbox) { QMap info; QList list = m_scene->items(); double ratiox = (double)m_finalSize.width() / m_width; double ratioy = 1; int offset = 0; if (letterbox) { int letterboxHeight = m_height * ratiox; ratioy = (double)letterboxHeight / m_finalSize.height(); offset = (m_finalSize.height() - letterboxHeight) / 2; } for (int i = 0; i < list.count(); ++i) { if (list.at(i)->type() == DvdButtonItem) { - DvdButton *button = static_cast(list.at(i)); + auto *button = static_cast(list.at(i)); QRectF r = button->sceneBoundingRect(); QRect adjustedRect(r.x() * ratiox, offset + r.y() * ratioy, r.width() * ratiox, r.height() * ratioy); // Make sure y1 is not odd (requested by spumux) if (adjustedRect.height() % 2 == 1) { adjustedRect.setHeight(adjustedRect.height() + 1); } if (adjustedRect.y() % 2 == 1) { adjustedRect.setY(adjustedRect.y() - 1); } QString command = button->command(); if (button->backMenu()) { command.prepend(QStringLiteral("g1 = 999;")); } info.insertMulti(command, adjustedRect); } } return info; } QDomElement DvdWizardMenu::toXml() const { QDomDocument doc; QDomElement xml = doc.createElement(QStringLiteral("menu")); doc.appendChild(xml); xml.setAttribute(QStringLiteral("enabled"), static_cast(m_view.create_menu->isChecked())); if (m_view.background_list->currentIndex() == 0) { // Color bg xml.setAttribute(QStringLiteral("background_color"), m_view.background_color->color().name()); } else if (m_view.background_list->currentIndex() == 1) { // Image bg xml.setAttribute(QStringLiteral("background_image"), m_view.background_image->url().toLocalFile()); } else { // Video bg xml.setAttribute(QStringLiteral("background_video"), m_view.background_image->url().toLocalFile()); } xml.setAttribute(QStringLiteral("text_color"), m_view.text_color->color().name()); xml.setAttribute(QStringLiteral("selected_color"), m_view.selected_color->color().name()); xml.setAttribute(QStringLiteral("highlighted_color"), m_view.highlighted_color->color().name()); xml.setAttribute(QStringLiteral("text_shadow"), (int)m_view.use_shadow->isChecked()); QList list = m_scene->items(); int buttonCount = 0; for (int i = 0; i < list.count(); ++i) { if (list.at(i)->type() == DvdButtonItem) { buttonCount++; - DvdButton *button = static_cast(list.at(i)); + auto *button = static_cast(list.at(i)); QDomElement xmlbutton = doc.createElement(QStringLiteral("button")); xmlbutton.setAttribute(QStringLiteral("target"), button->target()); xmlbutton.setAttribute(QStringLiteral("command"), button->command()); xmlbutton.setAttribute(QStringLiteral("backtomenu"), static_cast(button->backMenu())); xmlbutton.setAttribute(QStringLiteral("posx"), (int)button->pos().x()); xmlbutton.setAttribute(QStringLiteral("posy"), (int)button->pos().y()); xmlbutton.setAttribute(QStringLiteral("text"), button->toPlainText()); QFont font = button->font(); xmlbutton.setAttribute(QStringLiteral("font_size"), font.pixelSize()); xmlbutton.setAttribute(QStringLiteral("font_family"), font.family()); xml.appendChild(xmlbutton); } } return doc.documentElement(); } void DvdWizardMenu::loadXml(DVDFORMAT format, const QDomElement &xml) { // qCDebug(KDENLIVE_LOG) << "// LOADING MENU"; if (xml.isNull()) { return; } // qCDebug(KDENLIVE_LOG) << "// LOADING MENU 1"; changeProfile(format); m_view.create_menu->setChecked(xml.attribute(QStringLiteral("enabled")).toInt() != 0); if (xml.hasAttribute(QStringLiteral("background_color"))) { m_view.background_list->setCurrentIndex(0); m_view.background_color->setColor(xml.attribute(QStringLiteral("background_color"))); } else if (xml.hasAttribute(QStringLiteral("background_image"))) { m_view.background_list->setCurrentIndex(1); m_view.background_image->setUrl(QUrl(xml.attribute(QStringLiteral("background_image")))); } else if (xml.hasAttribute(QStringLiteral("background_video"))) { m_view.background_list->setCurrentIndex(2); m_view.background_image->setUrl(QUrl(xml.attribute(QStringLiteral("background_video")))); } m_view.text_color->setColor(xml.attribute(QStringLiteral("text_color"))); m_view.selected_color->setColor(xml.attribute(QStringLiteral("selected_color"))); m_view.highlighted_color->setColor(xml.attribute(QStringLiteral("highlighted_color"))); m_view.use_shadow->setChecked(xml.attribute(QStringLiteral("text_shadow")).toInt() != 0); QDomNodeList buttons = xml.elementsByTagName(QStringLiteral("button")); // qCDebug(KDENLIVE_LOG) << "// LOADING MENU 2" << buttons.count(); if (!buttons.isEmpty()) { // Clear existing buttons for (QGraphicsItem *item : m_scene->items()) { if (item->type() == DvdButtonItem) { m_scene->removeItem(item); delete item; } } } for (int i = 0; i < buttons.count(); ++i) { QDomElement e = buttons.at(i).toElement(); // create menu button DvdButton *button = new DvdButton(e.attribute(QStringLiteral("text"))); QFont font(e.attribute(QStringLiteral("font_family"))); font.setPixelSize(e.attribute(QStringLiteral("font_size")).toInt()); if (m_view.use_shadow->isChecked()) { auto *shadow = new QGraphicsDropShadowEffect(this); shadow->setBlurRadius(7); shadow->setOffset(4, 4); button->setGraphicsEffect(shadow); } // font.setStyleStrategy(QFont::NoAntialias); button->setFont(font); button->setTarget(e.attribute(QStringLiteral("target")).toInt(), e.attribute(QStringLiteral("command"))); button->setBackMenu(e.attribute(QStringLiteral("backtomenu")).toInt() != 0); button->setDefaultTextColor(m_view.text_color->color()); button->setZValue(4); m_scene->addItem(button); button->setPos(e.attribute(QStringLiteral("posx")).toInt(), e.attribute(QStringLiteral("posy")).toInt()); } } void DvdWizardMenu::slotZoom() { m_view.menu_preview->scale(2.0, 2.0); } void DvdWizardMenu::slotUnZoom() { m_view.menu_preview->scale(0.5, 0.5); } diff --git a/src/dvdwizard/dvdwizardmenu.h b/src/dvdwizard/dvdwizardmenu.h index 71fa5da9e..a4d047aa4 100644 --- a/src/dvdwizard/dvdwizardmenu.h +++ b/src/dvdwizard/dvdwizardmenu.h @@ -1,147 +1,147 @@ /*************************************************************************** * Copyright (C) 2009 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 DVDWIZARDMENU_H #define DVDWIZARDMENU_H #include "dvdwizardvob.h" #include "ui_dvdwizardmenu_ui.h" #include #include #include #include #include #include #include class DvdScene : public QGraphicsScene { Q_OBJECT public: explicit DvdScene(QObject *parent = nullptr); void setProfile(int width, int height); int gridSize() const; void setGridSize(int gridSize); private: - int m_width; - int m_height; - int m_gridSize; + int m_width{0}; + int m_height{0}; + int m_gridSize{1}; protected: void mouseReleaseEvent(QGraphicsSceneMouseEvent *mouseEvent) override; void drawForeground(QPainter *painter, const QRectF &rect) override; signals: void sceneChanged(); }; class DvdButtonUnderline : public QGraphicsRectItem { public: explicit DvdButtonUnderline(const QRectF &rect, QGraphicsItem *parent = nullptr) : QGraphicsRectItem(rect, parent) { } int type() const override { // Enable the use of qgraphicsitem_cast with this item. return UserType + 2; } }; class DvdButton : public QGraphicsTextItem { public: explicit DvdButton(const QString &text); void setTarget(int t, const QString &c); int target() const; QString command() const; bool backMenu() const; int type() const override; void setBackMenu(bool back); private: int m_target; QString m_command; bool m_backToMenu; protected: QVariant itemChange(GraphicsItemChange change, const QVariant &value) override; }; class DvdWizardMenu : public QWizardPage { Q_OBJECT public: explicit DvdWizardMenu(DVDFORMAT format, QWidget *parent = nullptr); - virtual ~DvdWizardMenu(); + ~DvdWizardMenu() override; bool createMenu() const; void createBackgroundImage(const QString &img1, bool letterbox); void createButtonImages(const QString &selected_image, const QString &highlighted_image, bool letterbox); void setTargets(const QStringList &list, const QStringList &targetlist); QMap buttonsInfo(bool letterbox = false); bool loopMovie() const; bool menuMovie() const; QString menuMoviePath() const; int menuMovieLength() const; void changeProfile(DVDFORMAT format); QDomElement toXml() const; void loadXml(DVDFORMAT format, const QDomElement &xml); void prepareUnderLines(); void resetUnderLines(); private: Ui::DvdWizardMenu_UI m_view; DVDFORMAT m_format; DvdScene *m_scene; QGraphicsPixmapItem *m_background; QGraphicsRectItem *m_color; QGraphicsRectItem *m_safeRect; int m_width; int m_height; QSize m_finalSize; int m_movieLength; KMessageWidget *m_menuMessage; private slots: void buildButton(); void buildColor(); void buildImage(); void checkBackgroundType(int ix); void updatePreview(); void buttonChanged(); void addButton(); void setButtonTarget(int ix); void deleteButton(); void updateColor(); void updateColor(const QColor &c); void updateUnderlineColor(QColor c); void setBackToMenu(bool backToMenu); void slotZoom(); void slotUnZoom(); void slotEnableShadows(int enable); void slotUseGrid(bool useGrid); }; #endif diff --git a/src/dvdwizard/dvdwizardvob.cpp b/src/dvdwizard/dvdwizardvob.cpp index 9f743cfd0..8aec8e2d6 100644 --- a/src/dvdwizard/dvdwizardvob.cpp +++ b/src/dvdwizard/dvdwizardvob.cpp @@ -1,875 +1,874 @@ /*************************************************************************** * Copyright (C) 2009 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 "dvdwizardvob.h" #include "dialogs/clipcreationdialog.h" #include "doc/kthumb.h" #include "kdenlivesettings.h" #include "timecode.h" #include #include "kdenlive_debug.h" #include "klocalizedstring.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include DvdTreeWidget::DvdTreeWidget(QWidget *parent) : QTreeWidget(parent) { setAcceptDrops(true); } void DvdTreeWidget::dragEnterEvent(QDragEnterEvent *event) { if (event->mimeData()->hasUrls()) { event->setDropAction(Qt::CopyAction); event->setAccepted(true); } else { QTreeWidget::dragEnterEvent(event); } } void DvdTreeWidget::dragMoveEvent(QDragMoveEvent *event) { event->acceptProposedAction(); } void DvdTreeWidget::mouseDoubleClickEvent(QMouseEvent *) { emit addNewClip(); } void DvdTreeWidget::dropEvent(QDropEvent *event) { QList clips = event->mimeData()->urls(); event->accept(); emit addClips(clips); } DvdWizardVob::DvdWizardVob(QWidget *parent) : QWizardPage(parent) - , m_installCheck(true) - , m_duration(0) + { m_view.setupUi(this); m_view.button_add->setIcon(QIcon::fromTheme(QStringLiteral("list-add"))); m_view.button_delete->setIcon(QIcon::fromTheme(QStringLiteral("list-remove"))); m_view.button_up->setIcon(QIcon::fromTheme(QStringLiteral("go-up"))); m_view.button_down->setIcon(QIcon::fromTheme(QStringLiteral("go-down"))); m_vobList = new DvdTreeWidget(this); auto *lay1 = new QVBoxLayout; lay1->setMargin(0); lay1->addWidget(m_vobList); m_view.list_frame->setLayout(lay1); m_vobList->setColumnCount(3); m_vobList->setHeaderHidden(true); m_view.convert_box->setVisible(false); connect(m_vobList, SIGNAL(addClips(QList)), this, SLOT(slotAddVobList(QList))); connect(m_vobList, SIGNAL(addNewClip()), this, SLOT(slotAddVobList())); connect(m_view.button_add, SIGNAL(clicked()), this, SLOT(slotAddVobList())); connect(m_view.button_delete, &QAbstractButton::clicked, this, &DvdWizardVob::slotDeleteVobFile); connect(m_view.button_up, &QAbstractButton::clicked, this, &DvdWizardVob::slotItemUp); connect(m_view.button_down, &QAbstractButton::clicked, this, &DvdWizardVob::slotItemDown); connect(m_view.convert_abort, &QAbstractButton::clicked, this, &DvdWizardVob::slotAbortTranscode); connect(m_vobList, &QTreeWidget::itemSelectionChanged, this, &DvdWizardVob::slotCheckVobList); m_vobList->setIconSize(QSize(60, 45)); QString errorMessage; if (QStandardPaths::findExecutable(QStringLiteral("dvdauthor")).isEmpty()) { errorMessage.append(i18n("Program %1 is required for the DVD wizard.", i18n("dvdauthor"))); } if (QStandardPaths::findExecutable(QStringLiteral("mkisofs")).isEmpty() && QStandardPaths::findExecutable(QStringLiteral("genisoimage")).isEmpty()) { errorMessage.append(i18n("Program %1 or %2 is required for the DVD wizard.", i18n("mkisofs"), i18n("genisoimage"))); } if (!errorMessage.isEmpty()) { m_view.button_add->setEnabled(false); m_view.dvd_profile->setEnabled(false); } m_view.dvd_profile->addItems(QStringList() << i18n("PAL 4:3") << i18n("PAL 16:9") << i18n("NTSC 4:3") << i18n("NTSC 16:9")); connect(m_view.dvd_profile, static_cast(&KComboBox::activated), this, &DvdWizardVob::slotCheckProfiles); m_vobList->header()->setStretchLastSection(false); m_vobList->header()->setSectionResizeMode(0, QHeaderView::Stretch); m_vobList->header()->setSectionResizeMode(1, QHeaderView::Custom); m_vobList->header()->setSectionResizeMode(2, QHeaderView::Custom); m_capacityBar = new KCapacityBar(KCapacityBar::DrawTextInline, this); auto *lay = new QHBoxLayout; lay->addWidget(m_capacityBar); m_view.size_box->setLayout(lay); m_vobList->setItemDelegate(new DvdViewDelegate(m_vobList)); m_transcodeAction = new QAction(i18n("Transcode"), this); connect(m_transcodeAction, &QAction::triggered, this, &DvdWizardVob::slotTranscodeFiles); m_warnMessage = new KMessageWidget; m_warnMessage->setCloseButtonVisible(false); - QGridLayout *s = static_cast(layout()); + auto *s = static_cast(layout()); s->addWidget(m_warnMessage, 2, 0, 1, -1); if (!errorMessage.isEmpty()) { m_warnMessage->setMessageType(KMessageWidget::Error); m_warnMessage->setText(errorMessage); m_installCheck = false; } else { m_warnMessage->setMessageType(KMessageWidget::Warning); m_warnMessage->setText(i18n("Your clips do not match selected DVD format, transcoding required.")); m_warnMessage->addAction(m_transcodeAction); m_warnMessage->hide(); } m_view.button_transcode->setHidden(true); slotCheckVobList(); m_transcodeProcess.setProcessChannelMode(QProcess::MergedChannels); connect(&m_transcodeProcess, &QProcess::readyReadStandardOutput, this, &DvdWizardVob::slotShowTranscodeInfo); connect(&m_transcodeProcess, static_cast(&QProcess::finished), this, &DvdWizardVob::slotTranscodeFinished); } DvdWizardVob::~DvdWizardVob() { delete m_capacityBar; // Abort running transcoding if (m_transcodeProcess.state() != QProcess::NotRunning) { disconnect(&m_transcodeProcess, static_cast(&QProcess::finished), this, &DvdWizardVob::slotTranscodeFinished); m_transcodeProcess.close(); m_transcodeProcess.waitForFinished(); } } bool DvdWizardVob::isComplete() const { return m_vobList->topLevelItemCount() > 0; } void DvdWizardVob::slotShowTranscodeInfo() { QString log = QString(m_transcodeProcess.readAll()); if (m_duration == 0) { if (log.contains(QStringLiteral("Duration:"))) { QString durationstr = log.section(QStringLiteral("Duration:"), 1, 1).section(QLatin1Char(','), 0, 0).simplified(); const QStringList numbers = durationstr.split(QLatin1Char(':')); if (numbers.size() < 3) { return; } m_duration = numbers.at(0).toInt() * 3600 + numbers.at(1).toInt() * 60 + numbers.at(2).toDouble(); // log_text->setHidden(true); // job_progress->setHidden(false); } else { // log_text->setHidden(false); // job_progress->setHidden(true); } } else if (log.contains(QStringLiteral("time="))) { int progress; QString time = log.section(QStringLiteral("time="), 1, 1).simplified().section(QLatin1Char(' '), 0, 0); if (time.contains(QLatin1Char(':'))) { const QStringList numbers = time.split(QLatin1Char(':')); if (numbers.size() < 3) { return; } progress = numbers.at(0).toInt() * 3600 + numbers.at(1).toInt() * 60 + numbers.at(2).toDouble(); } else { progress = (int)time.toDouble(); } m_view.convert_progress->setValue((int)(100.0 * progress / m_duration)); } // log_text->setPlainText(log); } void DvdWizardVob::slotAbortTranscode() { if (m_transcodeProcess.state() != QProcess::NotRunning) { m_transcodeProcess.close(); m_transcodeProcess.waitForFinished(); } m_transcodeQueue.clear(); m_view.convert_box->hide(); slotCheckProfiles(); } void DvdWizardVob::slotTranscodeFinished(int exitCode, QProcess::ExitStatus exitStatus) { if (exitCode == 0 && exitStatus == QProcess::NormalExit) { slotTranscodedClip(m_currentTranscoding.filename, m_currentTranscoding.filename + m_currentTranscoding.params.section(QStringLiteral("%1"), 1, 1).section(QLatin1Char(' '), 0, 0)); if (!m_transcodeQueue.isEmpty()) { m_transcodeProcess.close(); processTranscoding(); return; } } else { // Something failed // TODO show log m_warnMessage->setMessageType(KMessageWidget::Warning); m_warnMessage->setText(i18n("Transcoding failed!")); m_warnMessage->animatedShow(); m_transcodeQueue.clear(); } m_duration = 0; m_transcodeProcess.close(); if (m_transcodeQueue.isEmpty()) { m_view.convert_box->setHidden(true); slotCheckProfiles(); } } void DvdWizardVob::slotCheckProfiles() { bool conflict = false; int comboProfile = m_view.dvd_profile->currentIndex(); for (int i = 0; i < m_vobList->topLevelItemCount(); ++i) { QTreeWidgetItem *item = m_vobList->topLevelItem(i); if (item->data(0, Qt::UserRole + 1).toInt() != comboProfile) { conflict = true; break; } } m_transcodeAction->setEnabled(conflict); if (conflict) { showProfileError(); } else { m_warnMessage->animatedHide(); } } void DvdWizardVob::slotAddVobList(QList list) { if (list.isEmpty()) { QString allExtensions = ClipCreationDialog::getExtensions().join(QLatin1Char(' ')); QString dialogFilter = i18n("All Supported Files") + QStringLiteral(" (") + allExtensions + QStringLiteral(");; ") + i18n("MPEG Files") + QStringLiteral(" (*.mpeg *.mpg *.vob);; ") + i18n("All Files") + QStringLiteral(" (*.*)"); list = QFileDialog::getOpenFileUrls(this, i18n("Add new video file"), QUrl::fromLocalFile(KRecentDirs::dir(QStringLiteral(":KdenliveDvdFolder"))), dialogFilter); if (!list.isEmpty()) { KRecentDirs::add(QStringLiteral(":KdenliveDvdFolder"), list.at(0).adjusted(QUrl::RemoveFilename).toLocalFile()); } } for (const QUrl &url : list) { slotAddVobFile(url, QString(), false); } slotCheckVobList(); slotCheckProfiles(); } void DvdWizardVob::slotAddVobFile(const QUrl &url, const QString &chapters, bool checkFormats) { if (!url.isValid()) { return; } QFile f(url.toLocalFile()); qint64 fileSize = f.size(); Mlt::Profile profile; profile.set_explicit(false); QTreeWidgetItem *item = new QTreeWidgetItem(m_vobList, QStringList() << url.toLocalFile() << QString() << QString::number(static_cast(fileSize))); item->setData(2, Qt::UserRole, fileSize); item->setData(0, Qt::DecorationRole, QIcon::fromTheme(QStringLiteral("video-x-generic")).pixmap(60, 45)); item->setToolTip(0, url.toLocalFile()); QString resource = url.toLocalFile(); resource.prepend(QStringLiteral("avformat:")); Mlt::Producer *producer = new Mlt::Producer(profile, resource.toUtf8().data()); if ((producer != nullptr) && producer->is_valid() && !producer->is_blank()) { double fps = profile.fps(); profile.from_producer(*producer); profile.set_explicit(1); if (!qFuzzyCompare(profile.fps(), fps)) { // fps changed, rebuild producer delete producer; producer = new Mlt::Producer(profile, resource.toUtf8().data()); } } if ((producer != nullptr) && producer->is_valid() && !producer->is_blank()) { int width = 45.0 * profile.dar(); if (width % 2 == 1) { width++; } item->setData(0, Qt::DecorationRole, QPixmap::fromImage(KThumb::getFrame(producer, 0, width, 45))); int playTime = producer->get_playtime(); item->setText(1, Timecode::getStringTimecode(playTime, profile.fps())); item->setData(1, Qt::UserRole, playTime); int standard = -1; int aspect = profile.dar() * 100; if (profile.height() == 576 && qFuzzyCompare(profile.fps(), 25.0)) { if (aspect > 150) { standard = 1; } else { standard = 0; } } else if (profile.height() == 480 && qAbs(profile.fps() - 30000.0 / 1001) < 0.2) { if (aspect > 150) { standard = 3; } else { standard = 2; } } QString standardName; switch (standard) { case 3: standardName = i18n("NTSC 16:9"); break; case 2: standardName = i18n("NTSC 4:3"); break; case 1: standardName = i18n("PAL 16:9"); break; case 0: standardName = i18n("PAL 4:3"); break; default: standardName = i18n("Unknown"); } standardName.append(QStringLiteral(" | %1x%2, %3fps").arg(profile.width()).arg(profile.height()).arg(profile.fps())); item->setData(0, Qt::UserRole, standardName); item->setData(0, Qt::UserRole + 1, standard); item->setData(0, Qt::UserRole + 2, QSize(profile.dar() * profile.height(), profile.height())); if (m_vobList->topLevelItemCount() == 1) { // This is the first added movie, auto select DVD format if (standard >= 0) { m_view.dvd_profile->blockSignals(true); m_view.dvd_profile->setCurrentIndex(standard); m_view.dvd_profile->blockSignals(false); } } } else { // Cannot load movie, reject showError(i18n("The clip %1 is invalid.", url.fileName())); } delete producer; if (!chapters.isEmpty()) { item->setData(1, Qt::UserRole + 1, chapters); } else if (QFile::exists(url.toLocalFile() + QStringLiteral(".dvdchapter"))) { // insert chapters as children QFile file(url.toLocalFile() + QStringLiteral(".dvdchapter")); if (file.open(QIODevice::ReadOnly)) { QDomDocument doc; if (!doc.setContent(&file)) { file.close(); return; } file.close(); QDomNodeList chapterNodes = doc.elementsByTagName(QStringLiteral("chapter")); QStringList chaptersList; for (int j = 0; j < chapterNodes.count(); ++j) { chaptersList.append(QString::number(chapterNodes.at(j).toElement().attribute(QStringLiteral("time")).toInt())); } item->setData(1, Qt::UserRole + 1, chaptersList.join(QLatin1Char(';'))); } } else { // Explicitly add a chapter at 00:00:00:00 item->setData(1, Qt::UserRole + 1, "0"); } if (checkFormats) { slotCheckVobList(); slotCheckProfiles(); } } void DvdWizardVob::slotDeleteVobFile() { QTreeWidgetItem *item = m_vobList->currentItem(); if (item == nullptr) { return; } delete item; slotCheckVobList(); slotCheckProfiles(); } void DvdWizardVob::setUrl(const QString &url) { slotAddVobFile(QUrl::fromLocalFile(url)); } QStringList DvdWizardVob::selectedUrls() const { QStringList result; int max = m_vobList->topLevelItemCount(); int i = 0; if (m_view.use_intro->isChecked()) { // First movie is only for intro i = 1; } for (; i < max; ++i) { QTreeWidgetItem *item = m_vobList->topLevelItem(i); if (item) { result.append(item->text(0)); } } return result; } QStringList DvdWizardVob::durations() const { QStringList result; int max = m_vobList->topLevelItemCount(); int i = 0; if (m_view.use_intro->isChecked()) { // First movie is only for intro i = 1; } for (; i < max; ++i) { QTreeWidgetItem *item = m_vobList->topLevelItem(i); if (item) { result.append(QString::number(item->data(1, Qt::UserRole).toInt())); } } return result; } QStringList DvdWizardVob::chapters() const { QStringList result; int max = m_vobList->topLevelItemCount(); int i = 0; if (m_view.use_intro->isChecked()) { // First movie is only for intro i = 1; } for (; i < max; ++i) { QTreeWidgetItem *item = m_vobList->topLevelItem(i); if (item) { result.append(item->data(1, Qt::UserRole + 1).toString()); } } return result; } void DvdWizardVob::updateChapters(const QMap &chaptersdata) { int max = m_vobList->topLevelItemCount(); int i = 0; if (m_view.use_intro->isChecked()) { // First movie is only for intro i = 1; } for (; i < max; ++i) { QTreeWidgetItem *item = m_vobList->topLevelItem(i); if (chaptersdata.contains(item->text(0))) { item->setData(1, Qt::UserRole + 1, chaptersdata.value(item->text(0))); } } } int DvdWizardVob::duration(int ix) const { int result = -1; QTreeWidgetItem *item = m_vobList->topLevelItem(ix); if (item) { result = item->data(1, Qt::UserRole).toInt(); } return result; } const QString DvdWizardVob::introMovie() const { QString url; if (m_view.use_intro->isChecked() && m_vobList->topLevelItemCount() > 0) { url = m_vobList->topLevelItem(0)->text(0); } return url; } void DvdWizardVob::setUseIntroMovie(bool use) { m_view.use_intro->setChecked(use); } void DvdWizardVob::slotCheckVobList() { emit completeChanged(); int max = m_vobList->topLevelItemCount(); QTreeWidgetItem *item = m_vobList->currentItem(); bool hasItem = true; if (item == nullptr) { hasItem = false; } m_view.button_delete->setEnabled(hasItem); if (hasItem && m_vobList->indexOfTopLevelItem(item) == 0) { m_view.button_up->setEnabled(false); } else { m_view.button_up->setEnabled(hasItem); } if (hasItem && m_vobList->indexOfTopLevelItem(item) == max - 1) { m_view.button_down->setEnabled(false); } else { m_view.button_down->setEnabled(hasItem); } qint64 totalSize = 0; for (int i = 0; i < max; ++i) { item = m_vobList->topLevelItem(i); if (item) { totalSize += (qint64)item->data(2, Qt::UserRole).toInt(); } } qint64 maxSize = (qint64)47000 * 100000; m_capacityBar->setValue(static_cast(100 * totalSize / maxSize)); m_capacityBar->setText(KIO::convertSize(static_cast(totalSize))); } void DvdWizardVob::slotItemUp() { QTreeWidgetItem *item = m_vobList->currentItem(); if (item == nullptr) { return; } int index = m_vobList->indexOfTopLevelItem(item); if (index == 0) { return; } m_vobList->insertTopLevelItem(index - 1, m_vobList->takeTopLevelItem(index)); } void DvdWizardVob::slotItemDown() { int max = m_vobList->topLevelItemCount(); QTreeWidgetItem *item = m_vobList->currentItem(); if (item == nullptr) { return; } int index = m_vobList->indexOfTopLevelItem(item); if (index == max - 1) { return; } m_vobList->insertTopLevelItem(index + 1, m_vobList->takeTopLevelItem(index)); } DVDFORMAT DvdWizardVob::dvdFormat() const { return (DVDFORMAT)m_view.dvd_profile->currentIndex(); } const QString DvdWizardVob::dvdProfile() const { QString profile; switch (m_view.dvd_profile->currentIndex()) { case PAL_WIDE: profile = QStringLiteral("dv_pal_wide"); break; case NTSC: profile = QStringLiteral("dv_ntsc"); break; case NTSC_WIDE: profile = QStringLiteral("dv_ntsc_wide"); break; default: profile = QStringLiteral("dv_pal"); } return profile; } // static QString DvdWizardVob::getDvdProfile(DVDFORMAT format) { QString profile; switch (format) { case PAL_WIDE: profile = QStringLiteral("dv_pal_wide"); break; case NTSC: profile = QStringLiteral("dv_ntsc"); break; case NTSC_WIDE: profile = QStringLiteral("dv_ntsc_wide"); break; default: profile = QStringLiteral("dv_pal"); } return profile; } void DvdWizardVob::setProfile(const QString &profile) { if (profile == QLatin1String("dv_pal_wide")) { m_view.dvd_profile->setCurrentIndex(PAL_WIDE); } else if (profile == QLatin1String("dv_ntsc")) { m_view.dvd_profile->setCurrentIndex(NTSC); } else if (profile == QLatin1String("dv_ntsc_wide")) { m_view.dvd_profile->setCurrentIndex(NTSC_WIDE); } else { m_view.dvd_profile->setCurrentIndex(PAL); } } void DvdWizardVob::clear() { m_vobList->clear(); } void DvdWizardVob::slotTranscodeFiles() { m_warnMessage->animatedHide(); // Find transcoding info related to selected DVD profile KSharedConfigPtr config = KSharedConfig::openConfig(QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("kdenlivetranscodingrc")), KConfig::CascadeConfig); KConfigGroup transConfig(config, "Transcoding"); // read the entries QString profileEasyName; QSize destSize; QSize finalSize; switch (m_view.dvd_profile->currentIndex()) { case PAL_WIDE: profileEasyName = QStringLiteral("DVD PAL 16:9"); destSize = QSize(1024, 576); finalSize = QSize(720, 576); break; case NTSC: profileEasyName = QStringLiteral("DVD NTSC 4:3"); destSize = QSize(640, 480); finalSize = QSize(720, 480); break; case NTSC_WIDE: profileEasyName = QStringLiteral("DVD NTSC 16:9"); destSize = QSize(853, 480); finalSize = QSize(720, 480); break; default: profileEasyName = QStringLiteral("DVD PAL 4:3"); destSize = QSize(768, 576); finalSize = QSize(720, 576); } QString params = transConfig.readEntry(profileEasyName); m_transcodeQueue.clear(); m_view.convert_progress->setValue(0); m_duration = 0; // Transcode files that do not match selected profile int max = m_vobList->topLevelItemCount(); int format = m_view.dvd_profile->currentIndex(); m_view.convert_box->setVisible(true); for (int i = 0; i < max; ++i) { QTreeWidgetItem *item = m_vobList->topLevelItem(i); if (item->data(0, Qt::UserRole + 1).toInt() != format) { // File needs to be transcoded m_transcodeAction->setEnabled(false); QSize original = item->data(0, Qt::UserRole + 2).toSize(); double input_aspect = (double)original.width() / original.height(); QStringList postParams; if (input_aspect > (double)destSize.width() / destSize.height()) { // letterboxing int conv_height = (int)(destSize.width() / input_aspect); int conv_pad = (int)(((double)(destSize.height() - conv_height)) / 2.0); if (conv_pad % 2 == 1) { conv_pad--; } postParams << QStringLiteral("-vf") << QStringLiteral("scale=%1:%2,pad=%3:%4:0:%5,setdar=%6") .arg(finalSize.width()) .arg(destSize.height() - 2 * conv_pad) .arg(finalSize.width()) .arg(finalSize.height()) .arg(conv_pad) .arg(input_aspect); } else { // pillarboxing int conv_width = (int)(destSize.height() * input_aspect); int conv_pad = (int)(((double)(destSize.width() - conv_width)) / destSize.width() * finalSize.width() / 2.0); if (conv_pad % 2 == 1) { conv_pad--; } postParams << QStringLiteral("-vf") << QStringLiteral("scale=%1:%2,pad=%3:%4:%5:0,setdar=%6") .arg(finalSize.width() - 2 * conv_pad) .arg(destSize.height()) .arg(finalSize.width()) .arg(finalSize.height()) .arg(conv_pad) .arg(input_aspect); } TranscodeJobInfo jobInfo; jobInfo.filename = item->text(0); jobInfo.params = params.section(QLatin1Char(';'), 0, 0); jobInfo.postParams = postParams; m_transcodeQueue << jobInfo; } } processTranscoding(); } void DvdWizardVob::processTranscoding() { if (m_transcodeQueue.isEmpty()) { return; } m_currentTranscoding = m_transcodeQueue.takeFirst(); QStringList parameters; QStringList postParams = m_currentTranscoding.postParams; QString params = m_currentTranscoding.params; QString extension = params.section(QStringLiteral("%1"), 1, 1).section(QLatin1Char(' '), 0, 0); parameters << QStringLiteral("-i") << m_currentTranscoding.filename; if (QFile::exists(m_currentTranscoding.filename + extension)) { if (KMessageBox::questionYesNo(this, i18n("File %1 already exists.\nDo you want to overwrite it?", m_currentTranscoding.filename + extension)) == KMessageBox::No) { // TODO inform about abortion m_transcodeQueue.clear(); return; } parameters << QStringLiteral("-y"); } bool replaceVfParams = false; const QStringList splitted = params.split(QLatin1Char(' ')); for (QString s : splitted) { if (replaceVfParams) { parameters << postParams.at(1); replaceVfParams = false; } else if (s.startsWith(QLatin1String("%1"))) { parameters << s.replace(0, 2, m_currentTranscoding.filename); } else if (!postParams.isEmpty() && s == QLatin1String("-vf")) { replaceVfParams = true; parameters << s; } else { parameters << s; } } qCDebug(KDENLIVE_LOG) << " / / /STARTING TCODE JB: \n" << KdenliveSettings::ffmpegpath() << " = " << parameters; m_transcodeProcess.start(KdenliveSettings::ffmpegpath(), parameters); m_view.convert_label->setText(i18n("Transcoding: %1", QUrl::fromLocalFile(m_currentTranscoding.filename).fileName())); } void DvdWizardVob::slotTranscodedClip(const QString &src, const QString &transcoded) { if (transcoded.isEmpty()) { // Transcoding canceled or failed m_transcodeAction->setEnabled(true); return; } int max = m_vobList->topLevelItemCount(); for (int i = 0; i < max; ++i) { QTreeWidgetItem *item = m_vobList->topLevelItem(i); if (QUrl::fromLocalFile(item->text(0)).toLocalFile() == src) { // Replace movie with transcoded version item->setText(0, transcoded); QFile f(transcoded); qint64 fileSize = f.size(); Mlt::Profile profile; profile.set_explicit(false); item->setText(2, KIO::convertSize(static_cast(fileSize))); item->setData(2, Qt::UserRole, fileSize); item->setData(0, Qt::DecorationRole, QIcon::fromTheme(QStringLiteral("video-x-generic")).pixmap(60, 45)); item->setToolTip(0, transcoded); QString resource = transcoded; resource.prepend(QStringLiteral("avformat:")); Mlt::Producer *producer = new Mlt::Producer(profile, resource.toUtf8().data()); if ((producer != nullptr) && producer->is_valid() && !producer->is_blank()) { double fps = profile.fps(); profile.from_producer(*producer); profile.set_explicit(1); if (!qFuzzyCompare(profile.fps(), fps)) { // fps changed, rebuild producer delete producer; producer = new Mlt::Producer(profile, resource.toUtf8().data()); } } if ((producer != nullptr) && producer->is_valid() && !producer->is_blank()) { int width = 45.0 * profile.dar(); if (width % 2 == 1) { width++; } item->setData(0, Qt::DecorationRole, QPixmap::fromImage(KThumb::getFrame(producer, 0, width, 45))); int playTime = producer->get_playtime(); item->setText(1, Timecode::getStringTimecode(playTime, profile.fps())); item->setData(1, Qt::UserRole, playTime); int standard = -1; int aspect = profile.dar() * 100; if (profile.height() == 576) { if (aspect > 150) { standard = 1; } else { standard = 0; } } else if (profile.height() == 480) { if (aspect > 150) { standard = 3; } else { standard = 2; } } QString standardName; switch (standard) { case 3: standardName = i18n("NTSC 16:9"); break; case 2: standardName = i18n("NTSC 4:3"); break; case 1: standardName = i18n("PAL 16:9"); break; case 0: standardName = i18n("PAL 4:3"); break; default: standardName = i18n("Unknown"); } item->setData(0, Qt::UserRole, standardName); item->setData(0, Qt::UserRole + 1, standard); item->setData(0, Qt::UserRole + 2, QSize(profile.dar() * profile.height(), profile.height())); } else { // Cannot load movie, reject showError(i18n("The clip %1 is invalid.", transcoded)); } delete producer; slotCheckVobList(); if (m_transcodeQueue.isEmpty()) { slotCheckProfiles(); } break; } } } void DvdWizardVob::showProfileError() { m_warnMessage->setText(i18n("Your clips do not match selected DVD format, transcoding required.")); m_warnMessage->setCloseButtonVisible(false); m_warnMessage->addAction(m_transcodeAction); m_warnMessage->animatedShow(); } void DvdWizardVob::showError(const QString &error) { m_warnMessage->setText(error); m_warnMessage->setCloseButtonVisible(true); m_warnMessage->removeAction(m_transcodeAction); m_warnMessage->animatedShow(); } diff --git a/src/dvdwizard/dvdwizardvob.h b/src/dvdwizard/dvdwizardvob.h index e2dd7e97a..ad70ca095 100644 --- a/src/dvdwizard/dvdwizardvob.h +++ b/src/dvdwizard/dvdwizardvob.h @@ -1,161 +1,161 @@ /*************************************************************************** * Copyright (C) 2009 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 DVDWIZARDVOB_H #define DVDWIZARDVOB_H #include "ui_dvdwizardvob_ui.h" #include #include #include #include #include #include #include #include #include #include enum DVDFORMAT { PAL, PAL_WIDE, NTSC, NTSC_WIDE }; struct TranscodeJobInfo { QString filename; QString params; QStringList postParams; }; class DvdTreeWidget : public QTreeWidget { Q_OBJECT public: explicit DvdTreeWidget(QWidget *parent); protected: void dragEnterEvent(QDragEnterEvent *event) override; void dropEvent(QDropEvent *event) override; void mouseDoubleClickEvent(QMouseEvent *) override; void dragMoveEvent(QDragMoveEvent *event) override; signals: void addNewClip(); void addClips(const QList &); }; class DvdViewDelegate : public QStyledItemDelegate { Q_OBJECT public: explicit DvdViewDelegate(QWidget *parent) : QStyledItemDelegate(parent) { } void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override { if (index.column() == 0) { painter->save(); QStyleOptionViewItem opt(option); QRect r1 = option.rect; QStyle *style = opt.widget ? opt.widget->style() : QApplication::style(); const int textMargin = style->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1; style->drawPrimitive(QStyle::PE_PanelItemViewItem, &opt, painter, opt.widget); QPixmap pixmap = index.data(Qt::DecorationRole).value(); QPoint pixmapPoint(r1.left() + textMargin, r1.top() + (r1.height() - pixmap.height()) / 2); painter->drawPixmap(pixmapPoint, pixmap); int decoWidth = pixmap.width() + 2 * textMargin; QFont font = painter->font(); font.setBold(true); painter->setFont(font); int mid = (int)((r1.height() / 2)); r1.adjust(decoWidth, 0, 0, -mid); QRect r2 = option.rect; r2.adjust(decoWidth, mid, 0, 0); painter->drawText(r1, Qt::AlignLeft | Qt::AlignBottom, QUrl(index.data().toString()).fileName()); font.setBold(false); painter->setFont(font); QString subText = index.data(Qt::UserRole).toString(); QRectF bounding; painter->drawText(r2, Qt::AlignLeft | Qt::AlignVCenter, subText, &bounding); painter->restore(); } else { QStyledItemDelegate::paint(painter, option, index); } } }; class DvdWizardVob : public QWizardPage { Q_OBJECT public: explicit DvdWizardVob(QWidget *parent = nullptr); - virtual ~DvdWizardVob(); + ~DvdWizardVob() override; QStringList selectedUrls() const; void setUrl(const QString &url); DVDFORMAT dvdFormat() const; const QString dvdProfile() const; int duration(int ix) const; QStringList durations() const; QStringList chapters() const; void setProfile(const QString &profile); void clear(); const QString introMovie() const; void setUseIntroMovie(bool use); void updateChapters(const QMap &chaptersdata); static QString getDvdProfile(DVDFORMAT format); bool isComplete() const override; private: Ui::DvdWizardVob_UI m_view; DvdTreeWidget *m_vobList; KCapacityBar *m_capacityBar; QAction *m_transcodeAction; - bool m_installCheck; + bool m_installCheck{true}; KMessageWidget *m_warnMessage; - int m_duration; + int m_duration{0}; QProcess m_transcodeProcess; QList m_transcodeQueue; TranscodeJobInfo m_currentTranscoding; void showProfileError(); void showError(const QString &error); void processTranscoding(); public slots: void slotAddVobFile(const QUrl &url = QUrl(), const QString &chapters = QString(), bool checkFormats = true); void slotAddVobList(QList list = QList()); void slotCheckProfiles(); private slots: void slotCheckVobList(); void slotDeleteVobFile(); void slotItemUp(); void slotItemDown(); void slotTranscodeFiles(); void slotTranscodedClip(const QString &, const QString &); void slotShowTranscodeInfo(); void slotTranscodeFinished(int exitCode, QProcess::ExitStatus exitStatus); void slotAbortTranscode(); }; #endif diff --git a/src/effects/effectlist/view/effectlistwidget.cpp b/src/effects/effectlist/view/effectlistwidget.cpp index b151f1412..efbaefa7c 100644 --- a/src/effects/effectlist/view/effectlistwidget.cpp +++ b/src/effects/effectlist/view/effectlistwidget.cpp @@ -1,94 +1,94 @@ /*************************************************************************** * 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 "effectlistwidget.hpp" #include "../model/effectfilter.hpp" #include "../model/effecttreemodel.hpp" #include "assets/assetlist/view/qmltypes/asseticonprovider.hpp" #include #include #include #include - +#include EffectListWidget::EffectListWidget(QWidget *parent) : AssetListWidget(parent) { QString effectCategory = QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("kdenliveeffectscategory.rc")); m_model = EffectTreeModel::construct(effectCategory, this); - m_proxyModel.reset(new EffectFilter(this)); + m_proxyModel = std::make_unique(this); m_proxyModel->setSourceModel(m_model.get()); m_proxyModel->setSortRole(EffectTreeModel::NameRole); m_proxyModel->sort(0, Qt::AscendingOrder); m_proxy = new EffectListWidgetProxy(this); rootContext()->setContextProperty("assetlist", m_proxy); rootContext()->setContextProperty("assetListModel", m_proxyModel.get()); rootContext()->setContextProperty("isEffectList", true); m_assetIconProvider = new AssetIconProvider(true); setup(); } void EffectListWidget::updateFavorite(const QModelIndex &index) { m_proxyModel->dataChanged(index, index, QVector()); m_proxyModel->reloadFilterOnFavorite(); emit reloadFavorites(); } EffectListWidget::~EffectListWidget() { delete m_proxy; qDebug() << " - - -Deleting effect list widget"; } void EffectListWidget::setFilterType(const QString &type) { if (type == "video") { static_cast(m_proxyModel.get())->setFilterType(true, EffectType::Video); } else if (type == "audio") { static_cast(m_proxyModel.get())->setFilterType(true, EffectType::Audio); } else if (type == "custom") { static_cast(m_proxyModel.get())->setFilterType(true, EffectType::Custom); } else if (type == "favorites") { static_cast(m_proxyModel.get())->setFilterType(true, EffectType::Favorites); } else { static_cast(m_proxyModel.get())->setFilterType(false, EffectType::Video); } } QString EffectListWidget::getMimeType(const QString &assetId) const { Q_UNUSED(assetId); return QStringLiteral("kdenlive/effect"); } void EffectListWidget::reloadCustomEffect(const QString &path) { static_cast(m_model.get())->reloadEffect(path); } void EffectListWidget::reloadEffectMenu(QMenu *effectsMenu, KActionCategory *effectActions) { m_model->reloadAssetMenu(effectsMenu, effectActions); } diff --git a/src/effects/effectlist/view/effectlistwidget.hpp b/src/effects/effectlist/view/effectlistwidget.hpp index e429d0645..2fc64db88 100644 --- a/src/effects/effectlist/view/effectlistwidget.hpp +++ b/src/effects/effectlist/view/effectlistwidget.hpp @@ -1,101 +1,101 @@ /*************************************************************************** * 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 EFFECTLISTWIDGET_H #define EFFECTLISTWIDGET_H #include "assets/assetlist/view/assetlistwidget.hpp" #include "kdenlivesettings.h" /* @brief This class is a widget that display the list of available effects */ class EffectFilter; class EffectTreeModel; class EffectListWidgetProxy; class KActionCategory; class QMenu; class EffectListWidget : public AssetListWidget { Q_OBJECT public: EffectListWidget(QWidget *parent = Q_NULLPTR); - ~EffectListWidget(); + ~EffectListWidget() override; void setFilterType(const QString &type); /*@brief Return mime type used for drag and drop. It will be kdenlive/effect*/ QString getMimeType(const QString &assetId) const override; void updateFavorite(const QModelIndex &index); void reloadEffectMenu(QMenu *effectsMenu, KActionCategory *effectActions); public slots: void reloadCustomEffect(const QString &path); private: EffectListWidgetProxy *m_proxy; signals: void reloadFavorites(); }; // see https://bugreports.qt.io/browse/QTBUG-57714, don't expose a QWidget as a context property class EffectListWidgetProxy : public QObject { Q_OBJECT Q_PROPERTY(bool showDescription READ showDescription WRITE setShowDescription NOTIFY showDescriptionChanged) public: EffectListWidgetProxy(EffectListWidget *parent) : QObject(parent) , q(parent) { } Q_INVOKABLE QString getName(const QModelIndex &index) const { return q->getName(index); } Q_INVOKABLE bool isFavorite(const QModelIndex &index) const { return q->isFavorite(index); } Q_INVOKABLE void setFavorite(const QModelIndex &index, bool favorite) const { q->setFavorite(index, favorite, true); q->updateFavorite(index); } Q_INVOKABLE QString getDescription(const QModelIndex &index) const { return q->getDescription(index); } Q_INVOKABLE QVariantMap getMimeData(const QString &assetId) const { return q->getMimeData(assetId); } Q_INVOKABLE void activate(const QModelIndex &ix) { q->activate(ix); } Q_INVOKABLE void setFilterType(const QString &type) { q->setFilterType(type); } Q_INVOKABLE void setFilterName(const QString &pattern) { q->setFilterName(pattern); } Q_INVOKABLE QString getMimeType(const QString &assetId) const { return q->getMimeType(assetId); } bool showDescription() const { return KdenliveSettings::showeffectinfo(); } void setShowDescription(bool show) { KdenliveSettings::setShoweffectinfo(show); emit showDescriptionChanged(); } signals: void showDescriptionChanged(); private: EffectListWidget *q; // NOLINT }; #endif diff --git a/src/effects/effectstack/model/effectgroupmodel.cpp b/src/effects/effectstack/model/effectgroupmodel.cpp index 62f0a3b67..85699900a 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) +EffectGroupModel::EffectGroupModel(const QList &data, QString name, const std::shared_ptr &stack, bool isRoot) : AbstractEffectItem(EffectItemType::Group, data, stack, isRoot) - , m_name(name) + , m_name(std::move(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, 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/effectgroupmodel.hpp b/src/effects/effectstack/model/effectgroupmodel.hpp index c3c6e94ce..a10f9c6ba 100644 --- a/src/effects/effectstack/model/effectgroupmodel.hpp +++ b/src/effects/effectstack/model/effectgroupmodel.hpp @@ -1,60 +1,60 @@ /*************************************************************************** * 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 EFFECTGROUPMODEL_H #define EFFECTGROUPMODEL_H #include "abstracteffectitem.hpp" #include "abstractmodel/treeitem.hpp" class EffectStackModel; /* @brief This represents a group of effects of the effectstack */ class EffectGroupModel : public AbstractEffectItem { public: /* This construct an effect of the given id @param is a ptr to the model this item belongs to. This is required to send update signals */ static std::shared_ptr construct(const QString &name, std::shared_ptr stack, bool isRoot = false); /* @brief Return true if the effect applies only to audio */ bool isAudio() const override; /* @brief This function plants the effect into the given service in last position */ void plant(const std::weak_ptr &service) override; void plantClone(const std::weak_ptr &service) override; /* @brief This function unplants (removes) the effect from the given service */ void unplant(const std::weak_ptr &service) override; void unplantClone(const std::weak_ptr &service) override; protected: - EffectGroupModel(const QList &data, const QString &name, const std::shared_ptr &stack, bool isRoot = false); + EffectGroupModel(const QList &data, QString name, const std::shared_ptr &stack, bool isRoot = false); void updateEnable() override; QString m_name; }; #endif diff --git a/src/effects/effectstack/model/effectstackmodel.cpp b/src/effects/effectstack/model/effectstackmodel.cpp index 5e7fe8906..1fd35bb5e 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_ownerId(std::move(ownerId)) , 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, 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(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(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(m_fadeIns.size()); int outFades = int(m_fadeOuts.size()); m_fadeIns.erase(effect->getId()); m_fadeOuts.erase(effect->getId()); inFades = int(m_fadeIns.size()) - inFades; outFades = int(m_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(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 (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")) { m_fadeIns.insert(effect->getId()); } else if (effectId == QLatin1String("fadeout") || effectId == QLatin1String("fade_to_black")) { m_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(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")) { m_fadeIns.insert(effect->getId()); roles << TimelineModel::FadeInRole; } else if (effectId == QLatin1String("fadeout") || effectId == QLatin1String("fade_to_black")) { m_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 && m_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 && m_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 (m_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 (m_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 (m_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 (m_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 (m_fadeIns.empty()) { return 0; } for (int i = 0; i < rootItem->childCount(); ++i) { if (*(m_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 (m_fadeOuts.empty()) { return 0; } for (int i = 0; i < rootItem->childCount(); ++i) { if (*(m_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 && m_fadeIns.count(std::static_pointer_cast(rootItem->child(i))->getId()) > 0) || (!fromStart && m_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, 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")) { m_fadeIns.insert(effectItem->getId()); } else if (effectId == QLatin1String("fadeout") || effectId == QLatin1String("fade_to_black")) { m_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, const std::shared_ptr &parentItem) { return std::static_pointer_cast(parentItem ? rootItem->child(row) : rootItem->child(row)); } 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(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(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")) { m_fadeIns.insert(effect->getId()); } else if (effectId == QLatin1String("fadeout") || effectId == QLatin1String("fade_to_black")) { m_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(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) { + if (filt->get("kdenlive_id") != nullptr) { 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()) { + while (ptr->filter(ct)->get("kdenlive_id") == nullptr && 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 (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(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 ? m_fadeOuts : m_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) { m_fadeOuts.erase(id); } else { m_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()), std::move(normalisedVal)); } diff --git a/src/effects/effectstack/view/builtstack.cpp b/src/effects/effectstack/view/builtstack.cpp index c2e4febee..f2fe841f7 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() {} +BuiltStack::~BuiltStack() = default; 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 e6ab0acb5..b1dd71bb9 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(); + ~BuiltStack() override; 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 f19275543..e903c09a8 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(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()); + auto *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); + auto *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); + auto *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 ae030245a..1526ae294 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(const std::shared_ptr &effectModel, QSize frameSize, const QImage &icon, QWidget *parent = nullptr); - ~CollapsibleEffectView(); + ~CollapsibleEffectView() override; 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 3497217e0..55514959a 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 = 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)); + auto *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(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(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(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)); + auto *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 d83c4d7ef..b0dcdcd04 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(); + ~EffectStackView() override; 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(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 5c1aacc0f..1e7cddf17 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) +AbstractClipJob::AbstractClipJob(JOBTYPE type, QString id, QObject *parent) : QObject(parent) - , m_clipId(id) + , m_clipId(std::move(id)) , m_jobType(type) { } -AbstractClipJob::~AbstractClipJob() {} +AbstractClipJob::~AbstractClipJob() = default; 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(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 88ec3e2fa..525e7027c 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(); + AbstractClipJob(JOBTYPE type, QString id, QObject *parent = nullptr); + ~AbstractClipJob() override; 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(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/audiothumbjob.cpp b/src/jobs/audiothumbjob.cpp index ab371a066..23d4388be 100644 --- a/src/jobs/audiothumbjob.cpp +++ b/src/jobs/audiothumbjob.cpp @@ -1,342 +1,342 @@ /*************************************************************************** * 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 "audiothumbjob.hpp" #include "bin/projectclip.h" #include "bin/projectitemmodel.h" #include "core.h" #include "doc/kdenlivedoc.h" #include "doc/kthumb.h" #include "kdenlivesettings.h" #include "klocalizedstring.h" #include "lib/audio/audioStreamInfo.h" #include "macros.hpp" #include "utils/thumbnailcache.hpp" #include #include #include #include #include #include AudioThumbJob::AudioThumbJob(const QString &binId) : AbstractClipJob(AUDIOTHUMBJOB, binId) , m_ffmpegProcess(nullptr) { } const QString AudioThumbJob::getDescription() const { return i18n("Extracting audio thumb from clip %1", m_clipId); } bool AudioThumbJob::computeWithMlt() { m_audioLevels.clear(); m_errorMessage.clear(); // MLT audio thumbs: slower but safer QString service = m_prod->get("mlt_service"); if (service == QLatin1String("avformat-novalidate")) { service = QStringLiteral("avformat"); } else if (service.startsWith(QLatin1String("xml"))) { service = QStringLiteral("xml-nogl"); } QScopedPointer audioProducer(new Mlt::Producer(*m_prod->profile(), service.toUtf8().constData(), m_prod->get("resource"))); if (!audioProducer->is_valid()) { m_errorMessage.append(i18n("Audio thumbs: cannot open file %1", m_prod->get("resource"))); return false; } audioProducer->set("video_index", "-1"); Mlt::Filter chans(*m_prod->profile(), "audiochannels"); Mlt::Filter converter(*m_prod->profile(), "audioconvert"); Mlt::Filter levels(*m_prod->profile(), "audiolevel"); audioProducer->attach(chans); audioProducer->attach(converter); audioProducer->attach(levels); int last_val = 0; double framesPerSecond = audioProducer->get_fps(); mlt_audio_format audioFormat = mlt_audio_s16; QStringList keys; keys.reserve(m_channels); for (int i = 0; i < m_channels; i++) { keys << "meta.media.audio_level." + QString::number(i); } for (int z = 0; z < m_lengthInFrames; ++z) { int val = (int)(100.0 * z / m_lengthInFrames); if (last_val != val) { emit jobProgress(val); last_val = val; } QScopedPointer mltFrame(audioProducer->get_frame()); if ((mltFrame != nullptr) && mltFrame->is_valid() && (mltFrame->get_int("test_audio") == 0)) { int samples = mlt_sample_calculator(float(framesPerSecond), m_frequency, z); mltFrame->get_audio(audioFormat, m_frequency, m_channels, samples); for (int channel = 0; channel < m_channels; ++channel) { double level = 256 * qMin(mltFrame->get_double(keys.at(channel).toUtf8().constData()) * 0.9, 1.0); m_audioLevels << level; } } else if (!m_audioLevels.isEmpty()) { for (int channel = 0; channel < m_channels; channel++) { m_audioLevels << m_audioLevels.last(); } } } m_done = true; return true; } bool AudioThumbJob::computeWithFFMPEG() { m_audioLevels.clear(); QStringList args; std::vector> channelFiles; for (int i = 0; i < m_channels; i++) { std::unique_ptr channelTmpfile(new QTemporaryFile()); if (!channelTmpfile->open()) { m_errorMessage.append(i18n("Audio thumbs: cannot create temporary file, check disk space and permissions\n")); return false; } channelTmpfile->close(); channelFiles.emplace_back(std::move(channelTmpfile)); } args << QStringLiteral("-i") << QUrl::fromLocalFile(m_prod->get("resource")).toLocalFile(); // Output progress info args << QStringLiteral("-progress"); #ifdef Q_OS_WIN args << QStringLiteral("-"); #else args << QStringLiteral("/dev/stdout"); #endif bool isFFmpeg = KdenliveSettings::ffmpegpath().contains(QLatin1String("ffmpeg")); args << QStringLiteral("-filter_complex:a"); if (m_channels == 1) { args << QStringLiteral("aformat=channel_layouts=mono,%1=100").arg(isFFmpeg ? "aresample=async" : "sample_rates"); args << QStringLiteral("-map") << QStringLiteral("0:a%1").arg(m_audioStream > 0 ? ":" + QString::number(m_audioStream) : QString()) << QStringLiteral("-c:a") << QStringLiteral("pcm_s16le") << QStringLiteral("-y") << QStringLiteral("-f") << QStringLiteral("data") << channelFiles[0]->fileName(); } else { QString aformat = QStringLiteral("[0:a%1]%2=100,channelsplit=channel_layout=%3") .arg(m_audioStream > 0 ? ":" + QString::number(m_audioStream) : QString()) .arg(isFFmpeg ? "aresample=async" : "aformat=sample_rates=") .arg(m_channels > 2 ? "5.1" : "stereo"); for (int i = 0; i < m_channels; ++i) { aformat.append(QStringLiteral("[0:%1]").arg(i)); } args << aformat; for (int i = 0; i < m_channels; i++) { // Channel 1 args << QStringLiteral("-map") << QStringLiteral("[0:%1]").arg(i) << QStringLiteral("-c:a") << QStringLiteral("pcm_s16le") << QStringLiteral("-y") << QStringLiteral("-f") << QStringLiteral("data") << channelFiles[size_t(i)]->fileName(); } } m_ffmpegProcess = new QProcess; m_ffmpegProcess->start(KdenliveSettings::ffmpegpath(), args); connect(m_ffmpegProcess, &QProcess::readyReadStandardOutput, this, &AudioThumbJob::updateFfmpegProgress); m_ffmpegProcess->waitForFinished(-1); if (m_ffmpegProcess->exitStatus() != QProcess::CrashExit) { int dataSize = 0; std::vector rawChannels; std::vector sourceChannels; - for (size_t i = 0; i < channelFiles.size(); i++) { - channelFiles[i]->open(); - sourceChannels.emplace_back(channelFiles[i]->readAll()); + for (auto &channelFile : channelFiles) { + channelFile->open(); + sourceChannels.emplace_back(channelFile->readAll()); QByteArray &res = sourceChannels.back(); - channelFiles[i]->close(); + channelFile->close(); if (dataSize == 0) { dataSize = res.size(); } if (res.isEmpty() || res.size() != dataSize) { // Something went wrong, abort m_errorMessage.append(i18n("Audio thumbs: error reading audio thumbnail created with FFmpeg\n")); return false; } rawChannels.emplace_back((const qint16 *)res.constData()); } int progress = 0; std::vector channelsData; double offset = (double)dataSize / (2.0 * m_lengthInFrames); int intraOffset = 1; if (offset > 1000) { intraOffset = offset / 60; } else if (offset > 250) { intraOffset = offset / 10; } double factor = 800.0 / 32768; for (int i = 0; i < m_lengthInFrames; i++) { channelsData.resize((size_t)rawChannels.size()); std::fill(channelsData.begin(), channelsData.end(), 0); int pos = (int)(i * offset); int steps = 0; for (int j = 0; j < (int)offset && (pos + j < dataSize); j += intraOffset) { steps++; for (size_t k = 0; k < rawChannels.size(); k++) { channelsData[k] += abs(rawChannels[k][pos + j]); } } - for (size_t k = 0; k < channelsData.size(); k++) { + for (long &k : channelsData) { if (steps != 0) { - channelsData[k] /= steps; + k /= steps; } - m_audioLevels << (int)((double)channelsData[k] * factor); + m_audioLevels << (int)((double)k * factor); } int p = 80 + (i * 20 / m_lengthInFrames); if (p != progress) { emit jobProgress(p); progress = p; } } m_done = true; return true; } QString err = m_ffmpegProcess->readAllStandardError(); delete m_ffmpegProcess; // m_errorMessage += err; // m_errorMessage.append(i18n("Failed to create FFmpeg audio thumbnails, we now try to use MLT")); qWarning() << "Failed to create FFmpeg audio thumbs:\n" << err << "\n---------------------"; return false; } void AudioThumbJob::updateFfmpegProgress() { QString result = m_ffmpegProcess->readAllStandardOutput(); const QStringList lines = result.split(QLatin1Char('\n')); for (const QString &data : lines) { if (data.startsWith(QStringLiteral("out_time_ms"))) { double ms = data.section(QLatin1Char('='), 1).toDouble(); emit jobProgress((int)(ms / m_binClip->duration().ms() / 10)); } else { m_logDetails += data + QStringLiteral("\n"); } } } bool AudioThumbJob::startJob() { if (m_done) { return true; } m_binClip = pCore->projectItemModel()->getClipByBinID(m_clipId); if (m_binClip->audioChannels() == 0 || m_binClip->audioThumbCreated()) { // nothing to do m_done = true; m_successful = true; return true; } m_prod = m_binClip->originalProducer(); m_frequency = m_binClip->audioInfo()->samplingRate(); m_frequency = m_frequency <= 0 ? 48000 : m_frequency; m_channels = m_binClip->audioInfo()->channels(); m_channels = m_channels <= 0 ? 2 : m_channels; m_lengthInFrames = m_prod->get_length(); m_audioStream = m_binClip->audioInfo()->ffmpeg_audio_index(); if ((m_prod == nullptr) || !m_prod->is_valid()) { m_errorMessage.append(i18n("Audio thumbs: cannot open project file %1", m_binClip->url())); m_done = true; m_successful = false; return false; } m_cachePath = m_binClip->getAudioThumbPath(); // checking for cached thumbs QImage image(m_cachePath); if (!image.isNull()) { // convert cached image int n = image.width() * image.height(); for (int i = 0; i < n; i++) { QRgb p = image.pixel(i / m_channels, i % m_channels); m_audioLevels << qRed(p); m_audioLevels << qGreen(p); m_audioLevels << qBlue(p); m_audioLevels << qAlpha(p); } } if (!m_audioLevels.isEmpty()) { m_done = true; m_successful = true; return true; } bool ok = m_binClip->clipType() == ClipType::Playlist ? false : computeWithFFMPEG(); ok = ok ? ok : computeWithMlt(); Q_ASSERT(ok == m_done); if (ok && m_done && !m_audioLevels.isEmpty()) { // Put into an image for caching. int count = m_audioLevels.size(); image = QImage((int)lrint((count + 3) / 4.0 / m_channels), m_channels, QImage::Format_ARGB32); int n = image.width() * image.height(); for (int i = 0; i < n; i++) { QRgb p; if ((4 * i + 3) < count) { p = qRgba(m_audioLevels.at(4 * i).toInt(), m_audioLevels.at(4 * i + 1).toInt(), m_audioLevels.at(4 * i + 2).toInt(), m_audioLevels.at(4 * i + 3).toInt()); } else { int last = m_audioLevels.last().toInt(); int r = (4 * i + 0) < count ? m_audioLevels.at(4 * i + 0).toInt() : last; int g = (4 * i + 1) < count ? m_audioLevels.at(4 * i + 1).toInt() : last; int b = (4 * i + 2) < count ? m_audioLevels.at(4 * i + 2).toInt() : last; int a = last; p = qRgba(r, g, b, a); } image.setPixel(i / m_channels, i % m_channels, p); } image.save(m_cachePath); m_successful = true; return true; } m_done = true; m_successful = false; return false; } bool AudioThumbJob::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; } QVariantList old = m_binClip->audioFrameCache; // note that the image is moved into lambda, it won't be available from this class anymore auto operation = [clip = m_binClip, audio = std::move(m_audioLevels)]() { clip->updateAudioThumbnail(audio); return true; }; auto reverse = [clip = m_binClip, audio = std::move(old)]() { clip->updateAudioThumbnail(audio); return true; }; bool ok = operation(); if (ok) { UPDATE_UNDO_REDO_NOLOCK(operation, reverse, undo, redo); } return ok; } diff --git a/src/jobs/jobmanager.h b/src/jobs/jobmanager.h index a93b59a80..1660122d4 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(); + ~JobManager() override; /** @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(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 0c3ac16fb..704dbd106 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 }; + typedef Detect_prepareJob type; // NOLINT + enum { value = sizeof(func(nullptr)) == 2 }; }; struct dummy { template static typename std::enable_if::value || Noprepare, int>::type 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/meltjob.cpp b/src/jobs/meltjob.cpp index ad27038d0..0d405230e 100644 --- a/src/jobs/meltjob.cpp +++ b/src/jobs/meltjob.cpp @@ -1,275 +1,275 @@ /*************************************************************************** * 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 "meltjob.h" #include "bin/projectclip.h" #include "bin/projectitemmodel.h" #include "core.h" #include "kdenlivesettings.h" #include "profiles/profilemodel.hpp" #include +#include #include - static void consumer_frame_render(mlt_consumer, MeltJob *self, mlt_frame frame_ptr) { Mlt::Frame frame(frame_ptr); self->mltFrameCallback((int)frame.get_position()); } MeltJob::MeltJob(const QString &binId, JOBTYPE type, bool useProducerProfile, int in, int out) : AbstractClipJob(type, binId) , m_useProducerProfile(useProducerProfile) , m_in(in) , m_out(out) , m_requiresFilter(true) { } bool MeltJob::startJob() { auto binClip = pCore->projectItemModel()->getClipByBinID(m_clipId); m_url = binClip->url(); if (m_url.isEmpty()) { m_errorMessage.append(i18n("No producer for this clip.")); m_successful = false; m_done = true; return false; } /* QString consumerName = m_consumerParams.value(QStringLiteral("consumer")); // safety check, make sure we don't overwrite a source clip if (!m_dest.isEmpty() && !m_dest.endsWith(QStringLiteral(".mlt"))) { m_errorMessage.append(i18n("Invalid destination: %1.", consumerName)); setStatus(JobCrashed); return; } int in = m_producerParams.value(QStringLiteral("in")).toInt(); if (in > 0 && !m_extra.contains(QStringLiteral("offset"))) { m_extra.insert(QStringLiteral("offset"), QString::number(in)); } int out = m_producerParams.value(QStringLiteral("out")).toInt(); QString filterName = m_filterParams.value(QStringLiteral("filter")); // optional params int startPos = -1; int track = -1; // used when triggering a job from an effect if (m_extra.contains(QStringLiteral("clipStartPos"))) { startPos = m_extra.value(QStringLiteral("clipStartPos")).toInt(); } if (m_extra.contains(QStringLiteral("clipTrack"))) { track = m_extra.value(QStringLiteral("clipTrack")).toInt(); } if (!m_extra.contains(QStringLiteral("finalfilter"))) { m_extra.insert(QStringLiteral("finalfilter"), filterName); } if (out != -1 && out <= in) { m_errorMessage.append(i18n("Clip zone undefined (%1 - %2).", in, out)); setStatus(JobCrashed); return; } */ auto &projectProfile = pCore->getCurrentProfile(); // bool producerProfile = m_extra.contains(QStringLiteral("producer_profile")); if (m_useProducerProfile) { m_profile.reset(new Mlt::Profile()); m_profile->set_explicit(0); } else { m_profile.reset(&projectProfile->profile()); } double fps = projectProfile->fps(); int fps_num = projectProfile->frame_rate_num(); int fps_den = projectProfile->frame_rate_den(); - m_producer.reset(new Mlt::Producer(*m_profile.get(), m_url.toUtf8().constData())); + m_producer = std::make_unique(*m_profile.get(), m_url.toUtf8().constData()); if (m_producer && m_useProducerProfile) { m_profile->from_producer(*m_producer.get()); m_profile->set_explicit(1); } if (m_useProducerProfile) { configureProfile(); } if (!qFuzzyCompare(m_profile->fps(), fps) && m_useProducerProfile) { // Reload producer // Force same fps as projec profile or the resulting .mlt will not load in our project m_profile->set_frame_rate(fps_num, fps_den); - m_producer.reset(new Mlt::Producer(*m_profile.get(), m_url.toUtf8().constData())); + m_producer = std::make_unique(*m_profile.get(), m_url.toUtf8().constData()); } if ((m_producer == nullptr) || !m_producer->is_valid()) { // Clip was removed or something went wrong, Notify user? m_errorMessage.append(i18n("Invalid clip")); m_successful = false; m_done = true; return false; } /* // Process producer params QMapIterator i(m_producerParams); QStringList ignoredProps; ignoredProps << QStringLiteral("producer") << QStringLiteral("in") << QStringLiteral("out"); while (i.hasNext()) { i.next(); QString key = i.key(); if (!ignoredProps.contains(key)) { producer->set(i.key().toUtf8().constData(), i.value().toUtf8().constData()); } } */ if (m_out == -1) { m_out = m_producer->get_playtime() - 1; } if (m_in == -1) { m_in = 0; } if (m_out != m_producer->get_playtime() - 1 || m_in != 0) { std::swap(m_wholeProducer, m_producer); m_producer.reset(m_wholeProducer->cut(m_in, m_out)); } configureProducer(); if ((m_producer == nullptr) || !m_producer->is_valid()) { // Clip was removed or something went wrong, Notify user? m_errorMessage.append(i18n("Invalid clip")); m_successful = false; m_done = true; return false; } // Build consumer configureConsumer(); /* if (m_consumerName.contains(QLatin1String(":"))) { m_consumer.reset(new Mlt::Consumer(*m_profile, consumerName.section(QLatin1Char(':'), 0, 0).toUtf8().constData(), m_dest.toUtf8().constData())); } else { m_consumer = new Mlt::Consumer(*m_profile, consumerName.toUtf8().constData()); }*/ if ((m_consumer == nullptr) || !m_consumer->is_valid()) { m_errorMessage.append(i18n("Cannot create consumer.")); m_successful = false; m_done = true; return false; } /* if (!m_consumerParams.contains(QStringLiteral("real_time"))) { m_consumer->set("real_time", -KdenliveSettings::mltthreads()); } */ // Process consumer params /* QMapIterator j(m_consumerParams); ignoredProps.clear(); ignoredProps << QStringLiteral("consumer"); while (j.hasNext()) { j.next(); QString key = j.key(); if (!ignoredProps.contains(key)) { m_consumer->set(j.key().toUtf8().constData(), j.value().toUtf8().constData()); } } if (consumerName.startsWith(QStringLiteral("xml:"))) { // Use relative path in xml m_consumer->set("root", QFileInfo(m_dest).absolutePath().toUtf8().constData()); } */ // Build filter configureFilter(); /* if (!filterName.isEmpty()) { m_filter = new Mlt::Filter(*m_profile, filterName.toUtf8().data()); if ((m_filter == nullptr) || !m_filter->is_valid()) { m_errorMessage = i18n("Filter %1 crashed", filterName); setStatus(JobCrashed); return; } // Process filter params QMapIterator k(m_filterParams); ignoredProps.clear(); ignoredProps << QStringLiteral("filter"); while (k.hasNext()) { k.next(); QString key = k.key(); if (!ignoredProps.contains(key)) { m_filter->set(k.key().toUtf8().constData(), k.value().toUtf8().constData()); } } } */ if (m_requiresFilter && (m_filter == nullptr || !m_filter->is_valid())) { m_errorMessage.append(i18n("Cannot create filter.")); m_successful = false; m_done = true; return false; } Mlt::Tractor tractor(*m_profile.get()); Mlt::Playlist playlist; playlist.append(*m_producer.get()); tractor.set_track(playlist, 0); m_consumer->connect(tractor); m_producer->set_speed(0); m_producer->seek(0); m_length = m_producer->get_playtime(); if (m_length == 0) { m_length = m_producer->get_length(); } if (m_filter) { m_producer->attach(*m_filter.get()); } m_showFrameEvent.reset(m_consumer->listen("consumer-frame-render", this, (mlt_listener)consumer_frame_render)); m_producer->set_speed(1); m_consumer->run(); /* QMap jobResults; if (m_jobStatus != JobAborted && m_extra.contains(QStringLiteral("key"))) { QString result = QString::fromLatin1(m_filter->get(m_extra.value(QStringLiteral("key")).toUtf8().constData())); jobResults.insert(m_extra.value(QStringLiteral("key")), result); } if (!jobResults.isEmpty() && m_jobStatus != JobAborted) { emit gotFilterJobResults(m_clipId, startPos, track, jobResults, m_extra); } if (m_jobStatus == JobWorking) { m_jobStatus = JobDone; } */ m_successful = m_done = true; return true; } void MeltJob::mltFrameCallback(int pos) { if (m_length > 0) { emit jobProgress((int)(100 * pos / m_length)); } } diff --git a/src/jobs/scenesplitjob.cpp b/src/jobs/scenesplitjob.cpp index dcc402e67..e8aac7616 100644 --- a/src/jobs/scenesplitjob.cpp +++ b/src/jobs/scenesplitjob.cpp @@ -1,173 +1,172 @@ /*************************************************************************** * 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 = std::make_unique(*m_profile.get(), "null"); m_consumer->set("all", 1); m_consumer->set("terminate_on_pause", 1); m_consumer->set("real_time", -KdenliveSettings::mltthreads()); // 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")); + m_filter = std::make_unique(*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(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) { 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) { 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/speedjob.cpp b/src/jobs/speedjob.cpp index 3f1180a12..400a6f516 100644 --- a/src/jobs/speedjob.cpp +++ b/src/jobs/speedjob.cpp @@ -1,136 +1,135 @@ /*************************************************************************** * 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) +SpeedJob::SpeedJob(const QString &binId, double speed, QString destUrl) : MeltJob(binId, SPEEDJOB, false, -1, -1) , m_speed(speed) - , m_destUrl(destUrl) + , m_destUrl(std::move(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 = std::make_unique(*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())); + m_producer = std::make_unique(*m_profile.get(), "timewarp", QStringLiteral("%1:%2").arg(m_speed).arg(resource).toUtf8().constData()); } } void SpeedJob::configureFilter() {} // static 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 a408acfef..a77406d0e 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); + SpeedJob(const QString &binId, double speed, 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(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 578484ad7..fba736fef 100644 --- a/src/jobs/stabilizejob.cpp +++ b/src/jobs/stabilizejob.cpp @@ -1,158 +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 #include - -StabilizeJob::StabilizeJob(const QString &binId, const QString &filterName, const QString &destUrl, const std::unordered_map &filterParams) +StabilizeJob::StabilizeJob(const QString &binId, const QString &filterName, QString destUrl, std::unordered_map filterParams) : MeltJob(binId, STABILIZEJOB, true, -1, -1) , m_filterName(filterName) - , m_destUrl(destUrl) - , m_filterParams(filterParams) + , m_destUrl(std::move(destUrl)) + , m_filterParams(std::move(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 = std::make_unique(*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())); + m_filter = std::make_unique(*m_profile.get(), m_filterName.toUtf8().data()); if ((m_filter == nullptr) || !m_filter->is_valid()) { m_errorMessage.append(i18n("Cannot create filter %1", m_filterName)); return; } // Process filter params 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(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 = 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 257bb2122..d7d270f8b 100644 --- a/src/jobs/stabilizejob.hpp +++ b/src/jobs/stabilizejob.hpp @@ -1,72 +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); + StabilizeJob(const QString &binId, const QString &filterName, QString destUrl, 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(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/jobs/thumbjob.cpp b/src/jobs/thumbjob.cpp index 2632dd23b..8f2337237 100644 --- a/src/jobs/thumbjob.cpp +++ b/src/jobs/thumbjob.cpp @@ -1,183 +1,183 @@ /*************************************************************************** * 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 "thumbjob.hpp" #include "bin/projectclip.h" #include "bin/projectitemmodel.h" #include "bin/projectsubclip.h" #include "core.h" #include "doc/kthumb.h" #include "klocalizedstring.h" #include "macros.hpp" #include "utils/thumbnailcache.hpp" #include #include #include #include ThumbJob::ThumbJob(const QString &binId, int imageHeight, int frameNumber, bool persistent, bool reloadAllThumbs) : AbstractClipJob(THUMBJOB, binId) , m_frameNumber(frameNumber) , m_fullWidth(imageHeight * pCore->getCurrentDar() + 0.5) , m_imageHeight(imageHeight) , m_persistent(persistent) , m_reloadAll(reloadAllThumbs) - , m_subClip(false) + { if (m_fullWidth % 8 > 0) { m_fullWidth += 8 - m_fullWidth % 8; } m_imageHeight += m_imageHeight % 2; auto item = pCore->projectItemModel()->getItemByBinId(binId); Q_ASSERT(item->itemType() == AbstractProjectItem::ClipItem || item->itemType() == AbstractProjectItem::SubClipItem); if (item->itemType() == AbstractProjectItem::ClipItem) { m_binClip = pCore->projectItemModel()->getClipByBinID(binId); } else if (item->itemType() == AbstractProjectItem::SubClipItem) { m_subClip = true; m_binClip = pCore->projectItemModel()->getClipByBinID(item->parent()->clipId()); m_frameNumber = std::max(m_frameNumber, std::static_pointer_cast(item)->zone().x()); } } const QString ThumbJob::getDescription() const { return i18n("Extracting thumb at frame %1 from clip %2", m_frameNumber, m_clipId); } bool ThumbJob::startJob() { if (m_done) { return true; } // We reload here, because things may have changed since creation of this job if (m_subClip) { auto item = pCore->projectItemModel()->getItemByBinId(m_clipId); m_binClip = std::static_pointer_cast(item->parent()); m_frameNumber = item->zone().x(); } else { m_binClip = pCore->projectItemModel()->getClipByBinID(m_clipId); if (m_frameNumber < 0) { m_frameNumber = qMax(0, m_binClip->getProducerIntProperty(QStringLiteral("kdenlive:thumbnailFrame"))); } } if (m_binClip->clipType() == ClipType::Audio) { // Don't create thumbnail for audio clips m_done = false; return true; } m_inCache = false; if (ThumbnailCache::get()->hasThumbnail(m_binClip->clipId(), m_frameNumber, !m_persistent)) { m_done = true; m_result = ThumbnailCache::get()->getThumbnail(m_binClip->clipId(), m_frameNumber); m_inCache = true; return true; } m_prod = m_binClip->thumbProducer(); if ((m_prod == nullptr) || !m_prod->is_valid()) { qDebug() << "********\nCOULD NOT READ THUMB PRODUCER\n********"; return false; } int max = m_prod->get_length(); m_frameNumber = m_binClip->clipType() == ClipType::Image ? 0 : qMin(m_frameNumber, max - 1); if (m_frameNumber > 0) { m_prod->seek(m_frameNumber); } QScopedPointer frame(m_prod->get_frame()); frame->set("deinterlace_method", "onefield"); frame->set("top_field_first", -1); frame->set("rescale.interp", "nearest"); if ((frame != nullptr) && frame->is_valid()) { m_result = KThumb::getFrame(frame.data()); m_done = true; } return m_done; } bool ThumbJob::commitResult(Fun &undo, Fun &redo) { Q_ASSERT(!m_resultConsumed); if (!m_done) { if (m_binClip->clipType() == ClipType::Audio) { // audio files get standard audio icon, ok return true; } qDebug() << "ERROR: Trying to consume invalid results"; return false; } if (!m_inCache) { if (m_result.isNull()) { qDebug() << "+++++\nINVALID RESULT IMAGE\n++++++++++++++"; m_result = QImage(m_fullWidth, m_imageHeight, QImage::Format_ARGB32_Premultiplied); m_result.fill(Qt::red); QPainter p(&m_result); p.setPen(Qt::white); p.drawText(0, 0, m_fullWidth, m_imageHeight, Qt::AlignCenter, i18n("Invalid")); } else { ThumbnailCache::get()->storeThumbnail(m_binClip->clipId(), m_frameNumber, m_result, m_persistent); } } m_resultConsumed = true; // TODO a refactor of ProjectClip and ProjectSubClip should make that possible without branching (both classes implement setThumbnail) bool ok = false; if (m_subClip) { auto subClip = std::static_pointer_cast(pCore->projectItemModel()->getItemByBinId(m_clipId)); QImage old = subClip->thumbnail(m_result.width(), m_result.height()).toImage(); // note that the image is moved into lambda, it won't be available from this class anymore auto operation = [clip = subClip, image = std::move(m_result)]() { clip->setThumbnail(image); return true; }; auto reverse = [clip = subClip, image = std::move(old)]() { clip->setThumbnail(image); return true; }; ok = operation(); if (ok) { UPDATE_UNDO_REDO_NOLOCK(operation, reverse, undo, redo); } } else { QImage old = m_binClip->thumbnail(m_result.width(), m_result.height()).toImage(); // note that the image is moved into lambda, it won't be available from this class anymore auto operation = [clip = m_binClip, image = std::move(m_result), this]() { clip->setThumbnail(image); if (m_reloadAll) { clip->updateTimelineClips({TimelineModel::ReloadThumbRole}); } return true; }; auto reverse = [clip = m_binClip, image = std::move(old), this]() { clip->setThumbnail(image); if (m_reloadAll) { clip->updateTimelineClips({TimelineModel::ReloadThumbRole}); } return true; }; ok = operation(); if (ok) { UPDATE_UNDO_REDO_NOLOCK(operation, reverse, undo, redo); } } return ok; } diff --git a/src/jogshuttle/jogaction.cpp b/src/jogshuttle/jogaction.cpp index 120266b5a..2be965758 100644 --- a/src/jogshuttle/jogaction.cpp +++ b/src/jogshuttle/jogaction.cpp @@ -1,79 +1,79 @@ /*************************************************************************** * Copyright (C) 2010 by Pascal Fleury (fleury@users.sourceforge.net) * * * * 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 "jogaction.h" #include "core.h" #include "monitor/monitormanager.h" #include "kdenlive_debug.h" +#include +#include #include -#include -#include - +#include // TODO(fleury): this should probably be a user configuration parameter (at least the max speed). // const double SPEEDS[] = {0.0, 0.5, 1.0, 2.0, 4.0, 8.0, 16.0, 32.0}; const double SPEEDS[] = {0.0, 1.0, 2.0, 4.0, 5.0, 8.0, 16.0, 60.0}; const size_t SPEEDS_SIZE = sizeof(SPEEDS) / sizeof(double); -JogShuttleAction::JogShuttleAction(const JogShuttle *jogShuttle, const QStringList &actionMap, QObject *parent) +JogShuttleAction::JogShuttleAction(const JogShuttle *jogShuttle, QStringList actionMap, QObject *parent) : QObject(parent) , m_jogShuttle(jogShuttle) - , m_actionMap(actionMap) + , m_actionMap(std::move(actionMap)) { // Add action map 0 used for stopping the monitor when the shuttle is in neutral position. if (m_actionMap.isEmpty()) { m_actionMap.append(QStringLiteral("monitor_pause")); } connect(m_jogShuttle, &JogShuttle::jogBack, pCore->monitorManager(), &MonitorManager::slotRewindOneFrame); connect(m_jogShuttle, &JogShuttle::jogForward, pCore->monitorManager(), &MonitorManager::slotForwardOneFrame); connect(m_jogShuttle, &JogShuttle::shuttlePos, this, &JogShuttleAction::slotShuttlePos); connect(m_jogShuttle, &JogShuttle::button, this, &JogShuttleAction::slotButton); connect(this, &JogShuttleAction::rewind, pCore->monitorManager(), &MonitorManager::slotRewind); connect(this, &JogShuttleAction::forward, pCore->monitorManager(), &MonitorManager::slotForward); } JogShuttleAction::~JogShuttleAction() = default; void JogShuttleAction::slotShuttlePos(int shuttle_pos) { size_t magnitude = (unsigned)abs(shuttle_pos); if (magnitude < SPEEDS_SIZE) { if (shuttle_pos < 0) { emit rewind(-SPEEDS[magnitude]); } else if (shuttle_pos == 0) { ////qCDebug(KDENLIVE_LOG) << "Shuttle pos0 action: " << m_actionMap[0]; emit action(m_actionMap[0]); } else if (shuttle_pos > 0) { emit forward(SPEEDS[magnitude]); } } } void JogShuttleAction::slotButton(int button_id) { if (button_id >= m_actionMap.size() || m_actionMap[button_id].isEmpty()) { // TODO(fleury): Should this go to the status bar to inform the user ? // qCDebug(KDENLIVE_LOG) << "No action applied for button: " << button_id; return; } ////qCDebug(KDENLIVE_LOG) << "Shuttle button =" << button_id << ": action=" << m_actionMap[button_id]; emit action(m_actionMap[button_id]); } diff --git a/src/jogshuttle/jogaction.h b/src/jogshuttle/jogaction.h index 12d8b4c38..06c33672a 100644 --- a/src/jogshuttle/jogaction.h +++ b/src/jogshuttle/jogaction.h @@ -1,47 +1,47 @@ /*************************************************************************** * Copyright (C) 2010 by Pascal Fleury (fleury@users.sourceforge.net) * * * * 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 JOGACTION_H #define JOGACTION_H #include "jogshuttle.h" #include #include class JogShuttleAction : public QObject { - Q_OBJECT public : explicit JogShuttleAction(const JogShuttle *jogShuttle, const QStringList &actionMap, QObject *parent = nullptr); - ~JogShuttleAction(); + Q_OBJECT public : explicit JogShuttleAction(const JogShuttle *jogShuttle, QStringList actionMap, QObject *parent = nullptr); + ~JogShuttleAction() override; private: const JogShuttle *m_jogShuttle; // this is indexed by button ID, having QString() for any non-used ones. QStringList m_actionMap; public slots: void slotShuttlePos(int); void slotButton(int); signals: void rewind(double); void forward(double); void action(const QString &); }; #endif diff --git a/src/jogshuttle/jogmanager.cpp b/src/jogshuttle/jogmanager.cpp index 5f3676961..7fb4f838a 100644 --- a/src/jogshuttle/jogmanager.cpp +++ b/src/jogshuttle/jogmanager.cpp @@ -1,52 +1,51 @@ /* 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 "jogmanager.h" #include "core.h" #include "jogaction.h" #include "jogshuttle.h" #include "jogshuttleconfig.h" #include "kdenlivesettings.h" #include "mainwindow.h" JogManager::JogManager(QObject *parent) : QObject(parent) - , m_shuttle(nullptr) - , m_shuttleAction(nullptr) + { slotConfigurationChanged(); connect(pCore->window(), &MainWindow::configurationChanged, this, &JogManager::slotConfigurationChanged); } void JogManager::slotConfigurationChanged() { delete m_shuttleAction; m_shuttleAction = nullptr; delete m_shuttle; m_shuttle = nullptr; if (KdenliveSettings::enableshuttle()) { m_shuttle = new JogShuttle(JogShuttle::canonicalDevice(KdenliveSettings::shuttledevice())); m_shuttleAction = new JogShuttleAction(m_shuttle, JogShuttleConfig::actionMap(KdenliveSettings::shuttlebuttons())); connect(m_shuttleAction, &JogShuttleAction::action, this, &JogManager::slotDoAction); } } void JogManager::slotDoAction(const QString &actionName) { QAction *action = pCore->window()->actionCollection()->action(actionName); if (!action) { fprintf(stderr, "%s", QStringLiteral("shuttle action '%1' unknown\n").arg(actionName).toLatin1().constData()); return; } action->trigger(); } diff --git a/src/jogshuttle/jogmanager.h b/src/jogshuttle/jogmanager.h index 01e7874b4..30e0dafaa 100644 --- a/src/jogshuttle/jogmanager.h +++ b/src/jogshuttle/jogmanager.h @@ -1,40 +1,40 @@ /* 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 JOGMANAGER_H #define JOGMANAGER_H #include class JogShuttle; class JogShuttleAction; /** * @class JogManager * @brief Turns JogShuttle support on/off according to KdenliveSettings and connects between JogShuttleAction and the actual actions. */ class JogManager : public QObject { Q_OBJECT public: explicit JogManager(QObject *parent = nullptr); private slots: void slotDoAction(const QString &actionName); void slotConfigurationChanged(); private: - JogShuttle *m_shuttle; - JogShuttleAction *m_shuttleAction; + JogShuttle *m_shuttle{nullptr}; + JogShuttleAction *m_shuttleAction{nullptr}; }; #endif diff --git a/src/jogshuttle/jogshuttle.cpp b/src/jogshuttle/jogshuttle.cpp index 99d346db4..8c3401879 100644 --- a/src/jogshuttle/jogshuttle.cpp +++ b/src/jogshuttle/jogshuttle.cpp @@ -1,238 +1,238 @@ /*************************************************************************** * Copyright (C) 2008 by Jean-Baptiste Mardelle (jb@kdenlive.org) * * Based on code by Arendt David * * * * 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 "jogshuttle.h" #include "kdenlive_debug.h" #include #include #include - -#include -#include +#include +#include #include +#include // according to earlier standards #include #include #include // init media event type constants const QEvent::Type MediaCtrlEvent::Key = (QEvent::Type)QEvent::registerEventType(); const QEvent::Type MediaCtrlEvent::Jog = (QEvent::Type)QEvent::registerEventType(); const QEvent::Type MediaCtrlEvent::Shuttle = (QEvent::Type)QEvent::registerEventType(); -ShuttleThread::ShuttleThread(const QString &device, QObject *parent) - : m_device(device) +ShuttleThread::ShuttleThread(QString device, QObject *parent) + : m_device(std::move(device)) , m_parent(parent) , m_isRunning(false) { } ShuttleThread::~ShuttleThread() = default; QString ShuttleThread::device() { return m_device; } void ShuttleThread::stop() { m_isRunning = false; } void ShuttleThread::run() { media_ctrl mc; media_ctrl_open_dev(&mc, m_device.toUtf8().data()); if (mc.fd < 0) { return; } fd_set readset; struct timeval timeout; // enter thread loop m_isRunning = true; while (m_isRunning) { // reset the read set FD_ZERO(&readset); FD_SET(mc.fd, &readset); // reinit the timeout structure timeout.tv_sec = 0; timeout.tv_usec = 400000; // do select in blocked mode and wake up after timeout // for stop_me evaluation int result = select(mc.fd + 1, &readset, nullptr, nullptr, &timeout); // see if there was an error or timeout else process event if (result < 0 && errno == EINTR) { // EINTR event caught. This is not a problem - continue processing continue; } if (result < 0) { // stop thread m_isRunning = false; } else if (result > 0) { // we have input if (FD_ISSET(mc.fd, &readset)) { media_ctrl_event mev; mev.type = MEDIA_CTRL_EVENT_NONE; // read input media_ctrl_read_event(&mc, &mev); // process event handleEvent(mev); } } else if (result == 0) { // on timeout } } // close the handle and return thread media_ctrl_close(&mc); } void ShuttleThread::handleEvent(const media_ctrl_event &ev) { if (ev.type == MEDIA_CTRL_EVENT_KEY) { key(ev); } else if (ev.type == MEDIA_CTRL_EVENT_JOG) { jog(ev); } else if (ev.type == MEDIA_CTRL_EVENT_SHUTTLE) { shuttle(ev); } } void ShuttleThread::key(const media_ctrl_event &ev) { if (ev.value == KEY_PRESS) { QApplication::postEvent(m_parent, new MediaCtrlEvent(MediaCtrlEvent::Key, ev.index + 1)); } } void ShuttleThread::shuttle(const media_ctrl_event &ev) { int value = ev.value / 2; if (value > MaxShuttleRange || value < -MaxShuttleRange) { // qCDebug(KDENLIVE_LOG) << "Jog shuttle value is out of range: " << MaxShuttleRange; return; } QApplication::postEvent(m_parent, new MediaCtrlEvent(MediaCtrlEvent::Shuttle, value)); } void ShuttleThread::jog(const media_ctrl_event &ev) { QApplication::postEvent(m_parent, new MediaCtrlEvent(MediaCtrlEvent::Jog, ev.value)); } JogShuttle::JogShuttle(const QString &device, QObject *parent) : QObject(parent) , m_shuttleProcess(device, this) { m_shuttleProcess.start(); } JogShuttle::~JogShuttle() { stopDevice(); } void JogShuttle::stopDevice() { if (m_shuttleProcess.isRunning()) { // tell thread to stop m_shuttleProcess.stop(); m_shuttleProcess.quit(); // give the thread some time (ms) to shutdown m_shuttleProcess.wait(600); // if still running - do it in the hardcore way if (m_shuttleProcess.isRunning()) { m_shuttleProcess.terminate(); qCWarning(KDENLIVE_LOG) << "Needed to force jogshuttle process termination"; } } } void JogShuttle::customEvent(QEvent *e) { QEvent::Type type = e->type(); if (type == MediaCtrlEvent::Key) { auto *mev = static_cast(e); emit button(mev->value()); } else if (type == MediaCtrlEvent::Jog) { auto *mev = static_cast(e); int value = mev->value(); if (value < 0) { emit jogBack(); } else if (value > 0) { emit jogForward(); } } else if (type == MediaCtrlEvent::Shuttle) { auto *mev = static_cast(e); emit shuttlePos(mev->value()); } } QString JogShuttle::canonicalDevice(const QString &device) { return QDir(device).canonicalPath(); } DeviceMap JogShuttle::enumerateDevices(const QString &devPath) { DeviceMap devs; QDir devDir(devPath); if (!devDir.exists()) { return devs; } const QStringList fileList = devDir.entryList(QDir::System | QDir::Files); for (const QString &fileName : fileList) { QString devFullPath = devDir.absoluteFilePath(fileName); QString fileLink = JogShuttle::canonicalDevice(devFullPath); // qCDebug(KDENLIVE_LOG) << QString(" [%1] ").arg(fileName); // qCDebug(KDENLIVE_LOG) << QString(" [%1] ").arg(fileLink); media_ctrl mc; media_ctrl_open_dev(&mc, (char *)fileLink.toUtf8().data()); if (mc.fd > 0 && (mc.device != nullptr)) { devs.insert(QString(mc.device->name), devFullPath); qCDebug(KDENLIVE_LOG) << QStringLiteral(" [keys-count=%1] ").arg(media_ctrl_get_keys_count(&mc)); } media_ctrl_close(&mc); } return devs; } int JogShuttle::keysCount(const QString &devPath) { media_ctrl mc; int keysCount = 0; QString fileLink = canonicalDevice(devPath); media_ctrl_open_dev(&mc, (char *)fileLink.toUtf8().data()); if (mc.fd > 0 && (mc.device != nullptr)) { keysCount = media_ctrl_get_keys_count(&mc); } return keysCount; } diff --git a/src/jogshuttle/jogshuttle.h b/src/jogshuttle/jogshuttle.h index 04b2b5918..649bd2ab2 100644 --- a/src/jogshuttle/jogshuttle.h +++ b/src/jogshuttle/jogshuttle.h @@ -1,101 +1,101 @@ /*************************************************************************** * Copyright (C) 2008 by Jean-Baptiste Mardelle (jb@kdenlive.org) * * Based on code by Arendt David * * * * 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 SHUTTLE_H #define SHUTTLE_H #include #include #include #include #include class MediaCtrlEvent : public QEvent { public: MediaCtrlEvent(QEvent::Type type, int value) : QEvent(type) , m_value(value) { } int value() { return m_value; } static const QEvent::Type Key; static const QEvent::Type Jog; static const QEvent::Type Shuttle; private: int m_value; }; class ShuttleThread : public QThread { public: - ShuttleThread(const QString &device, QObject *parent); - ~ShuttleThread(); + ShuttleThread(QString device, QObject *parent); + ~ShuttleThread() override; void run() override; QString device(); void stop(); private: enum { MaxShuttleRange = 7 }; void handleEvent(const media_ctrl_event &ev); void jog(const media_ctrl_event &ev); void shuttle(const media_ctrl_event &ev); void key(const media_ctrl_event &ev); QString m_device; QObject *m_parent; volatile bool m_isRunning; }; typedef QMap DeviceMap; typedef QMap::iterator DeviceMapIter; class JogShuttle : public QObject { Q_OBJECT public: explicit JogShuttle(const QString &device, QObject *parent = nullptr); - ~JogShuttle(); + ~JogShuttle() override; void stopDevice(); void initDevice(const QString &device); static QString canonicalDevice(const QString &device); static DeviceMap enumerateDevices(const QString &devPath); static int keysCount(const QString &devPath); protected: void customEvent(QEvent *e) override; private: ShuttleThread m_shuttleProcess; signals: void jogBack(); void jogForward(); void shuttlePos(int); void button(int); }; #endif diff --git a/src/jogshuttle/jogshuttleconfig.cpp b/src/jogshuttle/jogshuttleconfig.cpp index 2e7c6e388..9f0d0fc51 100644 --- a/src/jogshuttle/jogshuttleconfig.cpp +++ b/src/jogshuttle/jogshuttleconfig.cpp @@ -1,75 +1,75 @@ /*************************************************************************** * Copyright (C) 2010 by Pascal Fleury (fleury@users.sourceforge.net) * * * * 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 "jogshuttleconfig.h" #include #include #include #include -#include +#include using std::string; using std::stringstream; using std::vector; // these 2 functions will convert the action maps to and from a string representation not unlike this: // button1=rewind_one_frame;button2=forward_one_frame;button15=play static const QChar DELIMITER = ';'; static const QChar KEY_VALUE_SEP = '='; static const QString BUTTON_PREFIX(QStringLiteral("button")); QStringList JogShuttleConfig::actionMap(const QString &actionsConfig) { QStringList actionMap; const QStringList mappings = actionsConfig.split(DELIMITER); for (const QString &mapping : mappings) { QStringList parts = mapping.split(KEY_VALUE_SEP); if (parts.size() != 2) { fprintf(stderr, "Invalid button configuration: %s", mapping.toLatin1().constData()); continue; } // skip the 'button' prefix int button_id = parts[0].midRef(BUTTON_PREFIX.length()).toInt(); // fprintf(stderr, " - Handling map key='%s' (ID=%d), value='%s'\n", parts[0].data().toLatin1(), button_id, parts[1].data().toLatin1()); // DBG while (actionMap.size() <= button_id) { actionMap << QString(); } actionMap[button_id] = parts[1]; } // for (int i = 0; i < actionMap.size(); ++i) fprintf(stderr, "button #%d -> action '%s'\n", i, actionMap[i].data().toLatin1()); //DBG return actionMap; } QString JogShuttleConfig::actionMap(const QStringList &actionMap) { QStringList mappings; for (int i = 0; i < actionMap.size(); ++i) { if (actionMap[i].isEmpty()) { continue; } mappings << QStringLiteral("%1%2%3%4").arg(BUTTON_PREFIX).arg(i).arg(KEY_VALUE_SEP).arg(actionMap[i]); } return mappings.join(DELIMITER); } diff --git a/src/lib/audio/audioCorrelation.cpp b/src/lib/audio/audioCorrelation.cpp index 688ad104c..35dc60184 100644 --- a/src/lib/audio/audioCorrelation.cpp +++ b/src/lib/audio/audioCorrelation.cpp @@ -1,161 +1,161 @@ /* 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 "audioCorrelation.h" #include "fftCorrelation.h" #include "kdenlive_debug.h" #include "klocalizedstring.h" #include #include #include AudioCorrelation::AudioCorrelation(std::unique_ptr mainTrackEnvelope) : m_mainTrackEnvelope(std::move(mainTrackEnvelope)) { // Q_ASSERT(!mainTrackEnvelope->hasComputationStarted()); connect(m_mainTrackEnvelope.get(), &AudioEnvelope::envelopeReady, this, &AudioCorrelation::slotAnnounceEnvelope); m_mainTrackEnvelope->startComputeEnvelope(); } AudioCorrelation::~AudioCorrelation() { for (AudioEnvelope *envelope : m_children) { delete envelope; } for (AudioCorrelationInfo *info : m_correlations) { delete info; } qCDebug(KDENLIVE_LOG) << "Envelope deleted."; } void AudioCorrelation::slotAnnounceEnvelope() { emit displayMessage(i18n("Audio analysis finished"), OperationCompletedMessage, 500); } void AudioCorrelation::addChild(AudioEnvelope *envelope) { // We need to connect before starting the computation, to make sure // there is no race condition where the signal 'envelopeReady' is // lost. Q_ASSERT(!envelope->hasComputationStarted()); connect(envelope, &AudioEnvelope::envelopeReady, this, &AudioCorrelation::slotProcessChild); envelope->startComputeEnvelope(); } void AudioCorrelation::slotProcessChild(AudioEnvelope *envelope) { // Note that at this point the computation of the envelope of the // main track might not be finished. envelope() will block until // the computation is done. const size_t sizeMain = m_mainTrackEnvelope->envelope().size(); const size_t sizeSub = envelope->envelope().size(); - AudioCorrelationInfo *info = new AudioCorrelationInfo(sizeMain, sizeSub); + auto *info = new AudioCorrelationInfo(sizeMain, sizeSub); qint64 *correlation = info->correlationVector(); const std::vector &envMain = m_mainTrackEnvelope->envelope(); const std::vector &envSub = envelope->envelope(); qint64 max = 0; if (sizeSub > 200) { FFTCorrelation::correlate(&envMain[0], sizeMain, &envSub[0], sizeSub, correlation); } else { correlate(&envMain[0], sizeMain, &envSub[0], sizeSub, correlation, &max); info->setMax(max); } m_children.append(envelope); m_correlations.append(info); Q_ASSERT(m_correlations.size() == m_children.size()); int index = m_children.indexOf(envelope); int shift = getShift(index); emit gotAudioAlignData(envelope->clipId(), shift); } int AudioCorrelation::getShift(int childIndex) const { Q_ASSERT(childIndex >= 0); Q_ASSERT(childIndex < m_correlations.size()); int indexOffset = (int)m_correlations.at(childIndex)->maxIndex(); indexOffset -= (int)m_children.at(childIndex)->envelope().size(); return indexOffset; } AudioCorrelationInfo const *AudioCorrelation::info(int childIndex) const { Q_ASSERT(childIndex >= 0); Q_ASSERT(childIndex < m_correlations.size()); return m_correlations.at(childIndex); } void AudioCorrelation::correlate(const qint64 *envMain, size_t sizeMain, const qint64 *envSub, size_t sizeSub, qint64 *correlation, qint64 *out_max) { Q_ASSERT(correlation != nullptr); qint64 const *left; qint64 const *right; int size; qint64 sum; qint64 max = 0; /* Correlation: SHIFT \in [-sS..sM] <--sS---- [ sub ]----sM--->[ sub ] [ main ] ^ correlation vector index = SHIFT + sS main is fixed, sub is shifted along main. */ QTime t; t.start(); for (int shift = -(int)sizeSub; shift <= (int)sizeMain; ++shift) { if (shift <= 0) { left = envSub - shift; right = envMain; size = std::min((int)sizeSub + shift, (int)sizeMain); } else { left = envSub; right = envMain + shift; size = std::min((int)sizeSub, (int)sizeMain - shift); } sum = 0; for (int i = 0; i < size; ++i) { sum += (*left) * (*right); left++; right++; } correlation[sizeSub + (size_t)shift] = (qint64)qAbs(sum); if (sum > max) { max = sum; } } qCDebug(KDENLIVE_LOG) << "Correlation calculated. Time taken: " << t.elapsed() << " ms."; if (out_max != nullptr) { *out_max = max; } } diff --git a/src/lib/audio/audioCorrelation.h b/src/lib/audio/audioCorrelation.h index d4c53de1c..fbd2d3f56 100644 --- a/src/lib/audio/audioCorrelation.h +++ b/src/lib/audio/audioCorrelation.h @@ -1,87 +1,87 @@ /*************************************************************************** * 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. * ***************************************************************************/ #ifndef AUDIOCORRELATION_H #define AUDIOCORRELATION_H #include "audioCorrelationInfo.h" #include "audioEnvelope.h" #include "definitions.h" #include /** This class does the correlation between two tracks in order to synchronize (align) them. It uses one main track (used in the initializer); further tracks will be aligned relative to this main track. */ class AudioCorrelation : public QObject { Q_OBJECT public: /** @param mainTrackEnvelope Envelope of the reference track. Its actual computation will be started in this constructor (i.e. mainTrackEnvelope->StartComputeEnvelope() will be called). The computation of the envelop must not be started when passed to this constructor (i.e. mainTrackEnvelope->HasComputationStarted() must return false). */ explicit AudioCorrelation(std::unique_ptr mainTrackEnvelope); - ~AudioCorrelation(); + ~AudioCorrelation() override; /** Adds a child envelope that will be aligned to the reference envelope. This function returns immediately, the alignment computation is done asynchronously. When done, the signal gotAudioAlignData will be emitted. Similarly to the main envelope, the computation of the envelope must not be started when it is passed to this object. This object will take ownership of the passed envelope. */ void addChild(AudioEnvelope *envelope); const AudioCorrelationInfo *info(int childIndex) const; int getShift(int childIndex) const; /** Correlates the two vectors envMain and envSub. \c correlation must be a pre-allocated vector of size sizeMain+sizeSub+1. */ static void correlate(const qint64 *envMain, size_t sizeMain, const qint64 *envSub, size_t sizeSub, qint64 *correlation, qint64 *out_max = nullptr); private: std::unique_ptr m_mainTrackEnvelope; QList m_children; QList m_correlations; private slots: /** This is invoked when the child envelope is computed. This triggers the actual computations of the cross-correlation for aligning the envelope to the reference envelope. Takes ownership of @p envelope. */ void slotProcessChild(AudioEnvelope *envelope); void slotAnnounceEnvelope(); signals: void gotAudioAlignData(int, int); void displayMessage(const QString &, MessageType, int); }; #endif // AUDIOCORRELATION_H diff --git a/src/lib/audio/audioEnvelope.cpp b/src/lib/audio/audioEnvelope.cpp index 39ebb331e..be6889dda 100644 --- a/src/lib/audio/audioEnvelope.cpp +++ b/src/lib/audio/audioEnvelope.cpp @@ -1,163 +1,163 @@ /*************************************************************************** * 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 "audioEnvelope.h" #include "audioStreamInfo.h" #include "bin/bin.h" #include "bin/projectclip.h" #include "core.h" #include "kdenlive_debug.h" #include #include #include #include #include - +#include AudioEnvelope::AudioEnvelope(const QString &binId, int clipId, size_t offset, size_t length, size_t startPos) : m_offset(offset) , m_clipId(clipId) , m_startpos(startPos) , m_length(length) { std::shared_ptr clip = pCore->bin()->getBinClip(binId); m_envelopeSize = clip->frameDuration(); m_producer = clip->cloneProducer(); connect(&m_watcher, &QFutureWatcherBase::finished, this, [this] { envelopeReady(this); }); if (!m_producer || !m_producer->is_valid()) { qCDebug(KDENLIVE_LOG) << "// Cannot create envelope for producer: " << binId; } - m_info.reset(new AudioInfo(m_producer)); + m_info = std::make_unique(m_producer); if (length > 0) { Q_ASSERT(length + m_offset <= m_envelopeSize); m_envelopeSize = length; } } AudioEnvelope::~AudioEnvelope() { if (hasComputationStarted()) { // This is better than nothing, but does not seem enough to // guarantee safe deletion of the AudioEnvelope while the // computations are running: if the computations have just // finished, m_watcher might be finished, but the signal // 'envelopeReady' might still be pending while AudioEnvelope is // being deleted, which can cause a crash according to // http://doc.qt.io/qt-5/qobject.html#dtor.QObject. m_audioSummary.waitForFinished(); m_watcher.waitForFinished(); } } void AudioEnvelope::startComputeEnvelope() { m_audioSummary = QtConcurrent::run(this, &AudioEnvelope::loadAndNormalizeEnvelope); m_watcher.setFuture(m_audioSummary); } bool AudioEnvelope::hasComputationStarted() const { // An empty qFuture is canceled. QtConcurrent::run() returns a // future that does not support cancelation, so this is a good way // to check whether the computations have started. return !m_audioSummary.isCanceled(); } const AudioEnvelope::AudioSummary &AudioEnvelope::audioSummary() { Q_ASSERT(hasComputationStarted()); m_audioSummary.waitForFinished(); Q_ASSERT(m_audioSummary.constBegin() != m_audioSummary.constEnd()); // We use this instead of m_audioSummary.result() in order to return // a const reference instead of a copy. return *m_audioSummary.constBegin(); } const std::vector &AudioEnvelope::envelope() { // Blocks until the summary is available. return audioSummary().audioAmplitudes; } AudioEnvelope::AudioSummary AudioEnvelope::loadAndNormalizeEnvelope() const { qCDebug(KDENLIVE_LOG) << "Loading envelope ..."; AudioSummary summary(m_envelopeSize); int samplingRate = m_info->info(0)->samplingRate(); mlt_audio_format format_s16 = mlt_audio_s16; int channels = 1; QTime t; t.start(); m_producer->seek((int)m_offset); for (size_t i = 0; i < summary.audioAmplitudes.size(); ++i) { std::unique_ptr frame(m_producer->get_frame((int)i)); qint64 position = mlt_frame_get_position(frame->get_frame()); int samples = mlt_sample_calculator(m_producer->get_fps(), samplingRate, position); auto *data = static_cast(frame->get_audio(format_s16, samplingRate, channels, samples)); summary.audioAmplitudes[i] = 0; for (int k = 0; k < samples; ++k) { summary.audioAmplitudes[i] += abs(data[k]); } } qCDebug(KDENLIVE_LOG) << "Calculating the envelope (" << m_envelopeSize << " frames) took " << t.elapsed() << " ms."; qCDebug(KDENLIVE_LOG) << "Normalizing envelope ..."; const qint64 meanBeforeNormalization = std::accumulate(summary.audioAmplitudes.begin(), summary.audioAmplitudes.end(), 0LL) / (qint64)summary.audioAmplitudes.size(); // Normalize the envelope. summary.amplitudeMax = 0; for (size_t i = 0; i < summary.audioAmplitudes.size(); ++i) { summary.audioAmplitudes[i] -= meanBeforeNormalization; summary.amplitudeMax = std::max(summary.amplitudeMax, qAbs(summary.audioAmplitudes[i])); } return summary; } int AudioEnvelope::clipId() const { return m_clipId; } size_t AudioEnvelope::startPos() const { return m_startpos; } QImage AudioEnvelope::drawEnvelope() { const AudioSummary &summary = audioSummary(); QImage img((int)m_envelopeSize, 400, QImage::Format_ARGB32); img.fill(qRgb(255, 255, 255)); if (summary.amplitudeMax == 0) { return img; } for (int x = 0; x < img.width(); ++x) { double fy = (double)summary.audioAmplitudes[(size_t)x] / double(summary.amplitudeMax) * (double)img.height(); for (int y = img.height() - 1; y > img.height() - 1 - fy; --y) { img.setPixel(x, y, qRgb(50, 50, 50)); } } return img; } void AudioEnvelope::dumpInfo() { if (!m_audioSummary.isFinished()) { qCDebug(KDENLIVE_LOG) << "Envelope not yet generated, no information available."; } else { const AudioSummary &summary = audioSummary(); qCDebug(KDENLIVE_LOG) << "Envelope info" << "\n* size = " << summary.audioAmplitudes.size() << "\n* max = " << summary.amplitudeMax; } } diff --git a/src/lib/audio/audioEnvelope.h b/src/lib/audio/audioEnvelope.h index f73df22ab..4e4ff5be5 100644 --- a/src/lib/audio/audioEnvelope.h +++ b/src/lib/audio/audioEnvelope.h @@ -1,105 +1,105 @@ /*************************************************************************** * 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. * ***************************************************************************/ #ifndef AUDIOENVELOPE_H #define AUDIOENVELOPE_H #include "audioInfo.h" #include #include #include #include #include class QImage; /** The audio envelope is a simplified version of an audio track with frame resolution. One entry is calculated by the sum of the absolute values of all samples in the current frame. See also: http://bemasc.net/wordpress/2011/07/26/an-auto-aligner-for-pitivi/ */ class AudioEnvelope : public QObject { Q_OBJECT public: explicit AudioEnvelope(const QString &binId, int clipId, size_t offset = 0, size_t length = 0, size_t startPos = 0); - virtual ~AudioEnvelope(); + ~AudioEnvelope() override; /** Starts the asynchronous computation that computes the envelope. When the computations are done, the signal 'envelopeReady' will be emitted. */ void startComputeEnvelope(); /** Returns whether startComputeEnvelope() has been called. */ bool hasComputationStarted() const; /** Returns the envelope data. Blocks until the computation of the envelope is done. REQUIRES: startComputeEnvelope() has been called. */ const std::vector &envelope(); QImage drawEnvelope(); void dumpInfo(); int clipId() const; size_t startPos() const; private: struct AudioSummary { explicit AudioSummary(size_t size) : audioAmplitudes(size) { } - AudioSummary() {} + AudioSummary() = default; // This is the envelope data. There is one element for each // frame, which contains the sum of the absolute amplitudes of // the audio signal for that frame. std::vector audioAmplitudes; // Maximum absolute value of the elements in 'audioAmplitudes'. qint64 amplitudeMax = 0; }; /** Blocks until the AudioSummary has been computed. REQUIRES: startComputeEnvelope() has been called. */ const AudioSummary &audioSummary(); /** Actually computes the envelope data, synchronously. */ AudioSummary loadAndNormalizeEnvelope() const; std::shared_ptr m_producer; std::unique_ptr m_info; QFutureWatcher m_watcher; QFuture m_audioSummary; const size_t m_offset; const int m_clipId; const size_t m_startpos; const size_t m_length; size_t m_envelopeSize; signals: void envelopeReady(AudioEnvelope *envelope); }; #endif // AUDIOENVELOPE_H diff --git a/src/lib/audio/fftCorrelation.cpp b/src/lib/audio/fftCorrelation.cpp index 581c7530d..dc2b6b973 100644 --- a/src/lib/audio/fftCorrelation.cpp +++ b/src/lib/audio/fftCorrelation.cpp @@ -1,134 +1,134 @@ /* 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 "fftCorrelation.h" extern "C" { #include "../external/kiss_fft/tools/kiss_fftr.h" } #include "kdenlive_debug.h" #include #include #include void FFTCorrelation::correlate(const qint64 *left, const size_t leftSize, const qint64 *right, const size_t rightSize, qint64 *out_correlated) { - float *correlatedFloat = new float[leftSize + rightSize + 1]; + auto *correlatedFloat = new float[leftSize + rightSize + 1]; correlate(left, leftSize, right, rightSize, correlatedFloat); // The correlation vector will have entries up to N (number of entries // of the vector), so converting to integers will not lose that much // of precision. for (size_t i = 0; i < leftSize + rightSize + 1; ++i) { out_correlated[i] = correlatedFloat[i]; } delete[] correlatedFloat; } void FFTCorrelation::correlate(const qint64 *left, const size_t leftSize, const qint64 *right, const size_t rightSize, float *out_correlated) { QTime t; t.start(); - float *leftF = new float[leftSize]; - float *rightF = new float[rightSize]; + auto *leftF = new float[leftSize]; + auto *rightF = new float[rightSize]; // First the qint64 values need to be normalized to floats // Dividing by the max value is maybe not the best solution, but the // maximum value after correlation should not be larger than the longest // vector since each value should be at most 1 qint64 maxLeft = 1; qint64 maxRight = 1; for (size_t i = 0; i < leftSize; ++i) { if (labs(left[i]) > maxLeft) { maxLeft = labs(left[i]); } } for (size_t i = 0; i < rightSize; ++i) { if (labs(right[i]) > maxRight) { maxRight = labs(right[i]); } } // One side needs to be reversed, since multiplication in frequency domain (fourier space) // calculates the convolution: \sum l[x]r[N-x] and not the correlation: \sum l[x]r[x] for (size_t i = 0; i < leftSize; ++i) { leftF[i] = double(left[i]) / (double)maxLeft; } for (size_t i = 0; i < rightSize; ++i) { rightF[rightSize - 1 - i] = double(right[i]) / (double)maxRight; } // Now we can convolve to get the correlation convolve(leftF, leftSize, rightF, rightSize, out_correlated); qCDebug(KDENLIVE_LOG) << "Correlation (FFT based) computed in " << t.elapsed() << " ms."; delete[] leftF; delete[] rightF; } void FFTCorrelation::convolve(const float *left, const size_t leftSize, const float *right, const size_t rightSize, float *out_convolved) { QTime time; time.start(); // To avoid issues with repetition (we are dealing with cosine waves // in the fourier domain) we need to pad the vectors to at least twice their size, // otherwise convolution would convolve with the repeated pattern as well size_t largestSize = std::max(leftSize, rightSize); // The vectors must have the same size (same frequency resolution!) and should // be a power of 2 (for FFT). size_t size = 64; while (size / 2 < largestSize) { size = size << 1; } const size_t fft_size = size / 2 + 1; kiss_fftr_cfg fftConfig = kiss_fftr_alloc((int)size, 0, nullptr, nullptr); kiss_fftr_cfg ifftConfig = kiss_fftr_alloc((int)size, 1, nullptr, nullptr); std::vector leftFFT(fft_size); std::vector rightFFT(fft_size); std::vector correlatedFFT(fft_size); // Fill in the data into our new vectors with padding std::vector leftData(size, 0); std::vector rightData(size, 0); std::vector convolved(size); std::copy(left, left + leftSize, leftData.begin()); std::copy(right, right + rightSize, rightData.begin()); // Fourier transformation of the vectors kiss_fftr(fftConfig, &leftData[0], &leftFFT[0]); kiss_fftr(fftConfig, &rightData[0], &rightFFT[0]); // Convolution in spacial domain is a multiplication in fourier domain. O(n). for (size_t i = 0; i < correlatedFFT.size(); ++i) { correlatedFFT[i].r = leftFFT[i].r * rightFFT[i].r - leftFFT[i].i * rightFFT[i].i; correlatedFFT[i].i = leftFFT[i].r * rightFFT[i].i + leftFFT[i].i * rightFFT[i].r; } // Inverse fourier transformation to get the convolved data. // Insert one element at the beginning to obtain the same result // that we also get with the nested for loop correlation. *out_convolved = 0; size_t out_size = leftSize + rightSize + 1; kiss_fftri(ifftConfig, &correlatedFFT[0], &convolved[0]); std::copy(convolved.begin(), convolved.begin() + (int)out_size - 1, out_convolved + 1); // Finally some cleanup. kiss_fftr_free(fftConfig); kiss_fftr_free(ifftConfig); qCDebug(KDENLIVE_LOG) << "FFT convolution computed. Time taken: " << time.elapsed() << " ms"; } diff --git a/src/lib/audio/fftTools.cpp b/src/lib/audio/fftTools.cpp index 3221b0b7e..5f003952d 100644 --- a/src/lib/audio/fftTools.cpp +++ b/src/lib/audio/fftTools.cpp @@ -1,328 +1,328 @@ /*************************************************************************** * 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 "fftTools.h" +#include #include -#include #include // Uncomment for debugging, like writing a GNU Octave .m file to /tmp //#define DEBUG_FFTTOOLS #ifdef DEBUG_FFTTOOLS #include "kdenlive_debug.h" #include #include #endif FFTTools::FFTTools() : m_fftCfgs() , m_windowFunctions() { } FFTTools::~FFTTools() { QHash::iterator i; for (i = m_fftCfgs.begin(); i != m_fftCfgs.end(); ++i) { free(*i); } } const QString FFTTools::windowSignature(const WindowType windowType, const int size, const float param) { return QStringLiteral("s%1_t%2_p%3").arg(size).arg(windowType).arg(param, 0, 'f', 3); } const QString FFTTools::cfgSignature(const int size) { return QStringLiteral("s%1").arg(size); } // http://cplusplus.syntaxerrors.info/index.php?title=Cannot_declare_member_function_%E2%80%98static_int_Foo::bar%28%29%E2%80%99_to_have_static_linkage const QVector FFTTools::window(const WindowType windowType, const int size, const float param) { Q_ASSERT(size > 0); // Deliberately avoid converting size to a float // to keep mid an integer. int mid = (int(size - 1) / 2); int max = (size - 1); QVector window; switch (windowType) { case Window_Rect: return QVector(size + 1, 1); break; case Window_Triangle: window = QVector(size + 1); for (int x = 0; x < mid; ++x) { window[x] = (float)x / (float)mid + (float)(mid - x) / (float)mid * param; } for (int x = mid; x < size; ++x) { window[x] = float(x - mid) / (float)(max - mid) * param + (float)(max - x) / (float)(max - mid); } window[size] = .5 + param / 2; #ifdef DEBUG_FFTTOOLS qCDebug(KDENLIVE_LOG) << "Triangle window (factor " << window[size] << "):"; for (int i = 0; i < size; ++i) { qCDebug(KDENLIVE_LOG) << window[i]; } qCDebug(KDENLIVE_LOG) << "Triangle window end."; #endif return window; break; case Window_Hamming: // Use a quick version of the Hamming window here: Instead of // interpolating values between (-max/2) and (max/2) // we use integer values instead, ranging from -mid to (max-mid). window = QVector(size + 1); for (int x = 0; x < size; ++x) { window[x] = .54 + .46 * cos(2. * M_PI * ((float)x - (float)mid) / (float)size); } // Integrating the cosine over the window function results in // an area of 0; So only the constant factor 0.54 counts. window[size] = .54; #ifdef DEBUG_FFTTOOLS qCDebug(KDENLIVE_LOG) << "Hanning window (factor " << window[size] << "):"; for (int i = 0; i < size; ++i) { qCDebug(KDENLIVE_LOG) << window[i]; } qCDebug(KDENLIVE_LOG) << "Hanning window end."; #endif return window; break; } Q_ASSERT(false); return QVector(); } void FFTTools::fftNormalized(const audioShortVector &audioFrame, const uint channel, const uint numChannels, float *freqSpectrum, const WindowType windowType, const uint windowSize, const float param) { #ifdef DEBUG_FFTTOOLS QTime start = QTime::currentTime(); #endif const uint numSamples = (uint)audioFrame.size() / numChannels; if (((windowSize & 1) != 0u) || windowSize < 2) { return; } const QString cfgSig = cfgSignature((int)windowSize); const QString winSig = windowSignature(windowType, (int)windowSize, param); // Get the kiss_fft configuration from the config cache // or build a new configuration if the requested one is not available. kiss_fftr_cfg myCfg; if (m_fftCfgs.contains(cfgSig)) { #ifdef DEBUG_FFTTOOLS qCDebug(KDENLIVE_LOG) << "Re-using FFT configuration with size " << windowSize; #endif myCfg = m_fftCfgs.value(cfgSig); } else { #ifdef DEBUG_FFTTOOLS qCDebug(KDENLIVE_LOG) << "Creating FFT configuration with size " << windowSize; #endif myCfg = kiss_fftr_alloc((int)windowSize, 0, nullptr, nullptr); m_fftCfgs.insert(cfgSig, myCfg); } // Get the window function from the cache // (except for a rectangular window; nothing to do there). QVector window; float windowScaleFactor = 1; if (windowType != FFTTools::Window_Rect) { if (m_windowFunctions.contains(winSig)) { #ifdef DEBUG_FFTTOOLS qCDebug(KDENLIVE_LOG) << "Re-using window function with signature " << winSig; #endif window = m_windowFunctions.value(winSig); } else { #ifdef DEBUG_FFTTOOLS qCDebug(KDENLIVE_LOG) << "Building new window function with signature " << winSig; #endif window = FFTTools::window(windowType, (int)windowSize, 0); m_windowFunctions.insert(winSig, window); } windowScaleFactor = 1.0 / window[(int)windowSize]; } // Prepare frequency space vector. The resulting FFT vector is only half as long. - kiss_fft_cpx *freqData = new kiss_fft_cpx[int(windowSize) / 2]; - float *data = new float[(int)windowSize]; + auto *freqData = new kiss_fft_cpx[int(windowSize) / 2]; + auto *data = new float[(int)windowSize]; // Copy the first channel's audio into a vector for the FFT display; // Fill the data vector indices that cannot be covered with sample data with 0 if (numSamples < windowSize) { std::fill(&data[numSamples], &data[windowSize - 1], 0); } // Normalize signals to [0,1] to get correct dB values later on for (uint i = 0; i < numSamples && i < windowSize; ++i) { // Performance note: Benchmarking has shown that using the if/else inside the loop // does not do noticeable worse than keeping it outside (perhaps the branch predictor // is good enough), so it remains in there for better readability. if (windowType != FFTTools::Window_Rect) { data[i] = (float)audioFrame.data()[i * numChannels + channel] / 32767.0f * window[(int)i]; } else { data[i] = (float)audioFrame.data()[i * numChannels + channel] / 32767.0f; } } // Calculate the Fast Fourier Transform for the input data kiss_fftr(myCfg, data, freqData); // Logarithmic scale: 20 * log ( 2 * magnitude / N ) with magnitude = sqrt(r² + i²) // with N = FFT size (after FFT, 1/2 window size) for (uint i = 0; i < windowSize / 2; ++i) { // Logarithmic scale: 20 * log ( 2 * magnitude / N ) with magnitude = sqrt(r² + i²) // with N = FFT size (after FFT, 1/2 window size) freqSpectrum[i] = 20 * log(pow(pow(fabs(freqData[i].r * windowScaleFactor), 2) + pow(fabs(freqData[i].i * windowScaleFactor), 2), .5) / ((float)windowSize / 2.0f)) / log(10); ; } #ifdef DEBUG_FFTTOOLS std::ofstream mFile; mFile.open("/tmp/freq.m"); if (!mFile) { qCDebug(KDENLIVE_LOG) << "Opening file failed."; } else { mFile << "val = [ "; for (int sample = 0; sample < 256; ++sample) { mFile << data[sample] << ' '; } mFile << " ];\n"; mFile << "freq = [ "; for (int sample = 0; sample < 256; ++sample) { mFile << freqData[sample].r << '+' << freqData[sample].i << "*i "; } mFile << " ];\n"; mFile.close(); qCDebug(KDENLIVE_LOG) << "File written."; } #endif #ifdef DEBUG_FFTTOOLS qCDebug(KDENLIVE_LOG) << "Calculated FFT in " << start.elapsed() << " ms."; #endif delete[] freqData; delete[] data; } const QVector FFTTools::interpolatePeakPreserving(const QVector &in, const uint targetSize, uint left, uint right, float fill) { #ifdef DEBUG_FFTTOOLS QTime start = QTime::currentTime(); #endif if (right == 0) { Q_ASSERT(in.size() > 0); right = (uint)in.size() - 1; } Q_ASSERT(targetSize > 0); Q_ASSERT(left < right); QVector out((int)targetSize); float x; int xi; int i; if (((float)(right - left)) / (float)targetSize < 2.) { float x_prev = 0; for (i = 0; i < (int)targetSize; ++i) { // i: Target index // x: Interpolated source index (float!) // xi: floor(x) // Transform [0,targetSize-1] to [left,right] x = ((float)i) / float(targetSize - 1) * float(right - left) + (float)left; xi = (int)floor(x); if (x > float(in.size() - 1)) { // This may happen if right > in.size()-1; Fill the rest of the vector // with the default value now. break; } // Use linear interpolation in order to get smoother display if (xi == 0 || xi == (int)in.size() - 1) { // ... except if we are at the left or right border of the input sigal. // Special case here since we consider previous and future values as well for // the actual interpolation (not possible here). out[i] = in[xi]; } else { if (in[xi] > in[xi + 1] && x_prev < (float)xi) { // This is a hack to preserve peaks. // Consider f = {0, 100, 0} // x = {0.5, 1.5} // Then x is 50 both times, and the 100 peak is lost. // Get it back here for the first x after the peak (which is at xi). // (x is the first after the peak if the previous x was smaller than floor(x).) out[i] = in[xi]; } else { out[i] = ((float)xi + 1. - x) * in[xi] + (x - (float)xi) * in[xi + 1]; } } x_prev = x; } } else { // If there are more than 2 samples per pixel in average, then use the maximum of them // since by only looking at the left sample we might miss some maxima. int src = (int)left; for (i = 0; i < (int)targetSize; ++i) { // x: right bound // xi: floor(x) x = ((float)(i + 1)) / float(targetSize - 1) * float(right - left) + (float)left; xi = (int)floor(x); int points = 0; out[i] = fill; for (; src < xi && src < (int)in.size(); ++src) { if (out[i] < in[src]) { out[i] = in[src]; } points++; } // xi_prev = xi; } } // Fill the rest of the vector if the right border exceeds the input vector. for (; i < (int)targetSize; ++i) { out[i] = fill; } #ifdef DEBUG_FFTTOOLS qCDebug(KDENLIVE_LOG) << "Interpolated " << targetSize << " nodes from " << in.size() << " input points in " << start.elapsed() << " ms"; #endif return out; } #ifdef DEBUG_FFTTOOLS #undef DEBUG_FFTTOOLS #endif diff --git a/src/library/librarywidget.h b/src/library/librarywidget.h index 28d9cfcb1..0910e702b 100644 --- a/src/library/librarywidget.h +++ b/src/library/librarywidget.h @@ -1,216 +1,216 @@ /*************************************************************************** * 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 . * ***************************************************************************/ /*! * @class LibraryWidget * @brief A "library" that contains a list of clips to be used across projects * @author Jean-Baptiste Mardelle */ #ifndef LIBRARYWIDGET_H #define LIBRARYWIDGET_H #include "definitions.h" #include #include #include #include #include #include #include #include #include #include #include #include class ProjectManager; class KJob; class QProgressBar; class QToolBar; /** * @class BinItemDelegate * @brief This class is responsible for drawing items in the QTreeView. */ class LibraryItemDelegate : public QStyledItemDelegate { public: explicit LibraryItemDelegate(QObject *parent = nullptr) : QStyledItemDelegate(parent) { } void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const override { QStyleOptionViewItem opt = option; initStyleOption(&opt, index); QRect r1 = option.rect; QStyle *style = opt.widget ? opt.widget->style() : QApplication::style(); const int textMargin = style->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1; int decoWidth = 2 * textMargin + r1.height() * 1.8; int 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(Qt::DisplayRole).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(Qt::UserRole + 1).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); - return QSize(hint.width(), qMax(option.fontMetrics.lineSpacing() * 2 + 4, qMax(hint.height(), option.decorationSize.height()))); + return {hint.width(), qMax(option.fontMetrics.lineSpacing() * 2 + 4, qMax(hint.height(), option.decorationSize.height()))}; } void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override { if (index.column() == 0) { QRect r1 = option.rect; painter->save(); painter->setClipRect(r1); QStyleOptionViewItem opt(option); initStyleOption(&opt, index); 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 & QStyle::State_Selected) { 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); int decoWidth = 2 * textMargin + r1.height() * 1.8; r.setWidth(r1.height() * 1.8); // Draw thumbnail opt.icon.paint(painter, r); 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(Qt::DisplayRole).toString(), &bounding); font.setBold(false); painter->setFont(font); QString subText = index.data(Qt::UserRole + 1).toString(); r2.adjust(0, bounding.bottom() - r2.top(), 0, 0); QColor subTextColor = painter->pen().color(); subTextColor.setAlphaF(.5); painter->setPen(subTextColor); painter->drawText(r2, Qt::AlignLeft | Qt::AlignTop, subText, &bounding); painter->restore(); } else { QStyledItemDelegate::paint(painter, option, index); } } }; class LibraryTree : public QTreeWidget { Q_OBJECT public: explicit LibraryTree(QWidget *parent = nullptr); protected: QStringList mimeTypes() const override; QMimeData *mimeData(const QList list) const override; void dropEvent(QDropEvent *event) override; void mousePressEvent(QMouseEvent *event) override; public slots: void slotUpdateThumb(const QString &path, const QString &iconPath); void slotUpdateThumb(const QString &path, const QPixmap &pix); signals: void moveData(const QList &, const QString &); void importSequence(const QStringList &, const QString &); }; class LibraryWidget : public QWidget { Q_OBJECT public: explicit LibraryWidget(ProjectManager *m_manager, QWidget *parent = nullptr); void setupActions(const QList &list); public slots: void slotAddToLibrary(); void slotUpdateLibraryPath(); private slots: void slotAddToProject(); void slotDeleteFromLibrary(); void updateActions(); void slotAddFolder(); void slotRenameItem(); void slotMoveData(const QList &, QString); void slotSaveSequence(const QStringList &info, QString dest); void slotItemEdited(QTreeWidgetItem *item, int column); void slotDownloadFinished(KJob *); void slotDownloadProgress(KJob *, int); void slotGotPreview(const KFileItem &item, const QPixmap &pix); void slotItemsAdded(const QUrl &url, const KFileItemList &list); void slotItemsDeleted(const KFileItemList &list); void slotClearAll(); private: LibraryTree *m_libraryTree; QToolBar *m_toolBar; QProgressBar *m_progressBar; QAction *m_addAction; QAction *m_deleteAction; QTimer m_timer; KMessageWidget *m_infoWidget; ProjectManager *m_manager; QList m_folders; KIO::PreviewJob *m_previewJob; KCoreDirLister *m_coreLister; QMutex m_treeMutex; QDir m_directory; void showMessage(const QString &text, KMessageWidget::MessageType type = KMessageWidget::Warning); signals: void addProjectClips(const QList &); void thumbReady(const QString &, const QString &); void enableAddSelection(bool); void saveTimelineSelection(QDir); }; #endif diff --git a/src/logger.cpp b/src/logger.cpp index 945e6a387..f16e28e84 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(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}); + operations.emplace_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 8720e51fd..9e4931b56 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(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}); + operations.emplace_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}); + operations.emplace_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 986f152ba..128c50cb1 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1,3798 +1,3789 @@ /*************************************************************************** * 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()); + auto *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) { 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); + auto *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); + auto *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, 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()); + auto *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())); + filter = std::make_unique(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")); + filter = std::make_unique(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(); + auto *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(); + auto *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++) { + for (int i : progression) { Q_FOREACH (int it, avSizes) { - if (it >= progression[i]) { + if (it >= 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 7dede2cbe..59f6020fa 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -1,490 +1,491 @@ /*************************************************************************** * 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 #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) + explicit MltErrorEvent(QString message) : QEvent(QEvent::User) - , m_message(message) + , m_message(std::move(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(); + ~MainWindow() override; /** @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; + int m_exitCode{EXIT_SUCCESS}; 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; + AssetPanel *m_assetPanel{nullptr}; QDockWidget *m_effectStackDock; QDockWidget *m_clipMonitorDock; - Monitor *m_clipMonitor; + Monitor *m_clipMonitor{nullptr}; QDockWidget *m_projectMonitorDock; - Monitor *m_projectMonitor; + Monitor *m_projectMonitor{nullptr}; AudioGraphSpectrum *m_audioSpectrum; QDockWidget *m_undoViewDock; KSelectAction *m_timeFormatButton; KSelectAction *m_compositeAction; - TimelineTabs *m_timelineTabs; + TimelineTabs *m_timelineTabs{nullptr}; /** 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; + RenderWidget *m_renderWidget{nullptr}; + StatusBarMessageLabel *m_messageLabel{nullptr}; 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; + bool m_themeInitialized{false}; + bool m_isDarkTheme{false}; 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, 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 a4be629ad..c14ea16d5 100644 --- a/src/mltcontroller/clipcontroller.cpp +++ b/src/mltcontroller/clipcontroller.cpp @@ -1,879 +1,878 @@ /* 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, 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)); + m_audioInfo = std::make_unique(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); + return {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(); + return {}; } 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(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/clippropertiescontroller.cpp b/src/mltcontroller/clippropertiescontroller.cpp index 0b302a1f6..522dd4602 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); + auto *forcePage = new QScrollArea(this); m_propertiesPage = new QWidget(this); m_markersPage = new QWidget(this); m_metaPage = new QWidget(this); m_analysisPage = new QWidget(this); // Clip properties auto *propsBox = new QVBoxLayout; m_propertiesTree = new QTreeWidget(this); m_propertiesTree->setRootIsDecorated(false); m_propertiesTree->setColumnCount(2); m_propertiesTree->setAlternatingRowColors(true); m_propertiesTree->sortByColumn(0, Qt::AscendingOrder); m_propertiesTree->setHeaderHidden(true); propsBox->addWidget(m_propertiesTree); fillProperties(); m_propertiesPage->setLayout(propsBox); // Clip markers auto *mBox = new QVBoxLayout; m_markerTree = new QTreeView; m_markerTree->setRootIsDecorated(false); m_markerTree->setAlternatingRowColors(true); m_markerTree->setHeaderHidden(true); m_markerTree->setSelectionMode(QAbstractItemView::ExtendedSelection); m_markerTree->setModel(controller->getMarkerModel().get()); mBox->addWidget(m_markerTree); auto *bar = new QToolBar; bar->addAction(QIcon::fromTheme(QStringLiteral("document-new")), i18n("Add marker"), this, SLOT(slotAddMarker())); bar->addAction(QIcon::fromTheme(QStringLiteral("trash-empty")), i18n("Delete marker"), this, SLOT(slotDeleteMarker())); bar->addAction(QIcon::fromTheme(QStringLiteral("document-edit")), i18n("Edit marker"), this, SLOT(slotEditMarker())); bar->addAction(QIcon::fromTheme(QStringLiteral("document-save-as")), i18n("Export markers"), this, SLOT(slotSaveMarkers())); bar->addAction(QIcon::fromTheme(QStringLiteral("document-open")), i18n("Import markers"), this, SLOT(slotLoadMarkers())); mBox->addWidget(bar); m_markersPage->setLayout(mBox); connect(m_markerTree, &QAbstractItemView::doubleClicked, this, &ClipPropertiesController::slotSeekToMarker); // metadata auto *m2Box = new QVBoxLayout; auto *metaTree = new QTreeWidget; metaTree->setRootIsDecorated(true); metaTree->setColumnCount(2); metaTree->setAlternatingRowColors(true); metaTree->setHeaderHidden(true); m2Box->addWidget(metaTree); slotFillMeta(metaTree); m_metaPage->setLayout(m2Box); // Clip analysis auto *aBox = new QVBoxLayout; m_analysisTree = new AnalysisTree(this); aBox->addWidget(new QLabel(i18n("Analysis data"))); aBox->addWidget(m_analysisTree); auto *bar2 = new QToolBar; bar2->addAction(QIcon::fromTheme(QStringLiteral("trash-empty")), i18n("Delete analysis"), this, SLOT(slotDeleteAnalysis())); bar2->addAction(QIcon::fromTheme(QStringLiteral("document-save-as")), i18n("Export analysis"), this, SLOT(slotSaveAnalysis())); bar2->addAction(QIcon::fromTheme(QStringLiteral("document-open")), i18n("Import analysis"), this, SLOT(slotLoadAnalysis())); aBox->addWidget(bar2); slotFillAnalysisData(); m_analysisPage->setLayout(aBox); // Force properties auto *vbox = new QVBoxLayout; vbox->setSpacing(0); if (m_type == ClipType::Text || m_type == ClipType::SlideShow || m_type == ClipType::TextTemplate) { QPushButton *editButton = new QPushButton(i18n("Edit Clip"), this); connect(editButton, &QAbstractButton::clicked, this, &ClipPropertiesController::editClip); vbox->addWidget(editButton); } if (m_type == ClipType::Color || m_type == ClipType::Image || m_type == ClipType::AV || m_type == ClipType::Video || m_type == ClipType::TextTemplate) { // Edit duration widget m_originalProperties.insert(QStringLiteral("out"), m_properties->get("out")); int kdenlive_length = m_properties->time_to_frames(m_properties->get("kdenlive:duration")); if (kdenlive_length > 0) { m_originalProperties.insert(QStringLiteral("kdenlive:duration"), m_properties->get("kdenlive:duration")); } m_originalProperties.insert(QStringLiteral("length"), m_properties->get("length")); auto *hlay = new QHBoxLayout; QCheckBox *box = new QCheckBox(i18n("Duration"), this); box->setObjectName(QStringLiteral("force_duration")); hlay->addWidget(box); auto *timePos = new TimecodeDisplay(m_tc, this); timePos->setObjectName(QStringLiteral("force_duration_value")); timePos->setValue(kdenlive_length > 0 ? kdenlive_length : m_properties->get_int("length")); int original_length = m_properties->get_int("kdenlive:original_length"); if (original_length > 0) { box->setChecked(true); } else { timePos->setEnabled(false); } hlay->addWidget(timePos); vbox->addLayout(hlay); connect(box, &QAbstractButton::toggled, timePos, &QWidget::setEnabled); connect(box, &QCheckBox::stateChanged, this, &ClipPropertiesController::slotEnableForce); connect(timePos, &TimecodeDisplay::timeCodeEditingFinished, this, &ClipPropertiesController::slotDurationChanged); connect(this, &ClipPropertiesController::updateTimeCodeFormat, timePos, &TimecodeDisplay::slotUpdateTimeCodeFormat); connect(this, SIGNAL(modified(int)), timePos, SLOT(setValue(int))); // connect(this, static_cast(&ClipPropertiesController::modified), timePos, &TimecodeDisplay::setValue); } if (m_type == ClipType::TextTemplate) { // Edit text widget QString currentText = m_properties->get("templatetext"); m_originalProperties.insert(QStringLiteral("templatetext"), currentText); m_textEdit = new QTextEdit(this); m_textEdit->setAcceptRichText(false); m_textEdit->setPlainText(currentText); m_textEdit->setPlaceholderText(i18n("Enter template text here")); vbox->addWidget(m_textEdit); QPushButton *button = new QPushButton(i18n("Apply"), this); vbox->addWidget(button); connect(button, &QPushButton::clicked, this, &ClipPropertiesController::slotTextChanged); } else if (m_type == ClipType::Color) { // Edit color widget m_originalProperties.insert(QStringLiteral("resource"), m_properties->get("resource")); mlt_color color = m_properties->get_color("resource"); ChooseColorWidget *choosecolor = new ChooseColorWidget(i18n("Color"), QColor::fromRgb(color.r, color.g, color.b).name(), "", false, this); vbox->addWidget(choosecolor); // connect(choosecolor, SIGNAL(displayMessage(QString,int)), this, SIGNAL(displayMessage(QString,int))); connect(choosecolor, &ChooseColorWidget::modified, this, &ClipPropertiesController::slotColorModified); connect(this, static_cast(&ClipPropertiesController::modified), choosecolor, &ChooseColorWidget::slotColorModified); } if (m_type == ClipType::AV || m_type == ClipType::Video || m_type == ClipType::Image) { // Aspect ratio int force_ar_num = m_properties->get_int("force_aspect_num"); int force_ar_den = m_properties->get_int("force_aspect_den"); m_originalProperties.insert(QStringLiteral("force_aspect_den"), (force_ar_den == 0) ? QString() : QString::number(force_ar_den)); m_originalProperties.insert(QStringLiteral("force_aspect_num"), (force_ar_num == 0) ? QString() : QString::number(force_ar_num)); auto *hlay = new QHBoxLayout; QCheckBox *box = new QCheckBox(i18n("Aspect Ratio"), this); box->setObjectName(QStringLiteral("force_ar")); vbox->addWidget(box); connect(box, &QCheckBox::stateChanged, this, &ClipPropertiesController::slotEnableForce); auto *spin1 = new QSpinBox(this); spin1->setMaximum(8000); spin1->setObjectName(QStringLiteral("force_aspect_num_value")); hlay->addWidget(spin1); hlay->addWidget(new QLabel(QStringLiteral(":"))); auto *spin2 = new QSpinBox(this); spin2->setMinimum(1); spin2->setMaximum(8000); spin2->setObjectName(QStringLiteral("force_aspect_den_value")); hlay->addWidget(spin2); if (force_ar_num == 0) { // use current ratio int num = m_properties->get_int("meta.media.sample_aspect_num"); int den = m_properties->get_int("meta.media.sample_aspect_den"); if (den == 0) { num = 1; den = 1; } spin1->setEnabled(false); spin2->setEnabled(false); spin1->setValue(num); spin2->setValue(den); } else { box->setChecked(true); spin1->setEnabled(true); spin2->setEnabled(true); spin1->setValue(force_ar_num); spin2->setValue(force_ar_den); } connect(spin2, static_cast(&QSpinBox::valueChanged), this, &ClipPropertiesController::slotAspectValueChanged); connect(spin1, static_cast(&QSpinBox::valueChanged), this, &ClipPropertiesController::slotAspectValueChanged); connect(box, &QAbstractButton::toggled, spin1, &QWidget::setEnabled); connect(box, &QAbstractButton::toggled, spin2, &QWidget::setEnabled); vbox->addLayout(hlay); // Proxy QString proxy = m_properties->get("kdenlive:proxy"); m_originalProperties.insert(QStringLiteral("kdenlive:proxy"), proxy); hlay = new QHBoxLayout; - QGroupBox *bg = new QGroupBox(this); + auto *bg = new QGroupBox(this); bg->setCheckable(false); bg->setFlat(true); - QHBoxLayout *groupLay = new QHBoxLayout; + auto *groupLay = new QHBoxLayout; groupLay->setContentsMargins(0, 0, 0, 0); auto *pbox = new QCheckBox(i18n("Proxy clip"), this); pbox->setTristate(true); // Proxy codec label QLabel *lab = new QLabel(this); pbox->setObjectName(QStringLiteral("kdenlive:proxy")); bool hasProxy = proxy.length() > 2; if (hasProxy) { bg->setToolTip(proxy); bool proxyReady = (QFileInfo(proxy).fileName() == QFileInfo(m_properties->get("resource")).fileName()); if (proxyReady) { pbox->setCheckState(Qt::Checked); lab->setText(m_properties->get(QString("meta.media.%1.codec.name").arg(m_properties->get_int("video_index")).toUtf8().constData())); } else { pbox->setCheckState(Qt::PartiallyChecked); } } else { pbox->setCheckState(Qt::Unchecked); } pbox->setEnabled(pCore->projectManager()->current()->getDocumentProperty(QStringLiteral("enableproxy")).toInt() != 0); connect(pbox, &QCheckBox::stateChanged, [this, pbox](int state) { emit requestProxy(state == Qt::PartiallyChecked); if (state == Qt::Checked) { QSignalBlocker bk(pbox); pbox->setCheckState(Qt::Unchecked); } }); connect(this, &ClipPropertiesController::enableProxy, pbox, &QCheckBox::setEnabled); connect(this, &ClipPropertiesController::proxyModified, [this, pbox, bg, lab](const QString &pxy) { bool hasProxyClip = pxy.length() > 2; QSignalBlocker bk(pbox); pbox->setCheckState(hasProxyClip ? Qt::Checked : Qt::Unchecked); bg->setEnabled(pbox->isChecked()); bg->setToolTip(pxy); lab->setText(hasProxyClip ? m_properties->get(QString("meta.media.%1.codec.name").arg(m_properties->get_int("video_index")).toUtf8().constData()) : QString()); }); hlay->addWidget(pbox); bg->setEnabled(pbox->checkState() == Qt::Checked); groupLay->addWidget(lab); // Delete button - QToolButton *tb = new QToolButton(this); + auto *tb = new QToolButton(this); tb->setIcon(QIcon::fromTheme(QStringLiteral("edit-delete"))); tb->setAutoRaise(true); connect(tb, &QToolButton::clicked, [this, proxy]() { emit deleteProxy(); }); tb->setToolTip(i18n("Delete proxy file")); groupLay->addWidget(tb); // Folder button tb = new QToolButton(this); - QMenu *pMenu = new QMenu(this); + auto *pMenu = new QMenu(this); tb->setIcon(QIcon::fromTheme(QStringLiteral("kdenlive-menu"))); tb->setToolTip(i18n("Proxy options")); tb->setMenu(pMenu); tb->setAutoRaise(true); tb->setPopupMode(QToolButton::InstantPopup); QAction *ac = new QAction(QIcon::fromTheme(QStringLiteral("document-open")), i18n("Open folder"), this); connect(ac, &QAction::triggered, [this]() { QString pxy = m_properties->get("kdenlive:proxy"); QDesktopServices::openUrl(QUrl::fromLocalFile(QFileInfo(pxy).path())); }); pMenu->addAction(ac); ac = new QAction(QIcon::fromTheme(QStringLiteral("media-playback-start")), i18n("Play proxy clip"), this); connect(ac, &QAction::triggered, [this]() { QString pxy = m_properties->get("kdenlive:proxy"); QDesktopServices::openUrl(QUrl::fromLocalFile(pxy)); }); pMenu->addAction(ac); ac = new QAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Copy file location to clipboard"), this); connect(ac, &QAction::triggered, [this]() { QString pxy = m_properties->get("kdenlive:proxy"); QGuiApplication::clipboard()->setText(pxy); }); pMenu->addAction(ac); groupLay->addWidget(tb); bg->setLayout(groupLay); hlay->addWidget(bg); vbox->addLayout(hlay); } if (m_type == ClipType::AV || m_type == ClipType::Video) { QLocale locale; // 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); + auto *tbv = new QToolButton(this); tbv->setToolButtonStyle(Qt::ToolButtonIconOnly); tbv->setDefaultAction(ac); tbv->setAutoRaise(true); hlay->addWidget(tbv); hlay->addWidget(new QLabel(i18n("Video stream"))); - QComboBox *videoStream = new QComboBox(this); + auto *videoStream = new QComboBox(this); int ix = 1; for (int stream : m_videoStreams) { videoStream->addItem(i18n("Video stream %1", ix), stream); ix++; } if (!vix.isEmpty() && vix.toInt() > -1) { videoStream->setCurrentIndex(videoStream->findData(QVariant(vix))); } ac->setActive(vix.toInt() == -1); videoStream->setEnabled(vix.toInt() > -1); videoStream->setVisible(m_videoStreams.size() > 1); connect(ac, &KDualAction::activeChanged, [this, videoStream](bool activated) { QMap properties; int vindx = -1; if (activated) { videoStream->setEnabled(false); } else { videoStream->setEnabled(true); vindx = videoStream->currentData().toInt(); } properties.insert(QStringLiteral("video_index"), QString::number(vindx)); properties.insert(QStringLiteral("set.test_image"), vindx > -1 ? QStringLiteral("0") : QStringLiteral("1")); emit updateClipProperties(m_id, m_originalProperties, properties); m_originalProperties = properties; }); QObject::connect(videoStream, static_cast(&QComboBox::currentIndexChanged), [this, videoStream]() { QMap properties; properties.insert(QStringLiteral("video_index"), QString::number(videoStream->currentData().toInt())); emit updateClipProperties(m_id, m_originalProperties, properties); m_originalProperties = properties; }); hlay->addWidget(videoStream); vbox->addLayout(hlay); } // Audio index 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); + auto *tbv = new QToolButton(this); tbv->setToolButtonStyle(Qt::ToolButtonIconOnly); tbv->setDefaultAction(ac); tbv->setAutoRaise(true); hlay->addWidget(tbv); hlay->addWidget(new QLabel(i18n("Audio stream"))); - QComboBox *audioStream = new QComboBox(this); + auto *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, 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() {} +ClipPropertiesController::~ClipPropertiesController() = default; void ClipPropertiesController::updateTab(int ix) { KdenliveSettings::setProperties_panel_page(ix); } void ClipPropertiesController::slotRefreshTimeCode() { emit updateTimeCodeFormat(); } void ClipPropertiesController::slotReloadProperties() { mlt_color color; m_properties.reset(new Mlt::Properties(m_controller->properties())); m_clipLabel->setText(m_properties->get("kdenlive:clipname")); switch (m_type) { case ClipType::Color: m_originalProperties.insert(QStringLiteral("resource"), m_properties->get("resource")); m_originalProperties.insert(QStringLiteral("out"), m_properties->get("out")); m_originalProperties.insert(QStringLiteral("length"), m_properties->get("length")); emit modified(m_properties->get_int("length")); color = m_properties->get_color("resource"); emit modified(QColor::fromRgb(color.r, color.g, color.b)); break; case ClipType::TextTemplate: m_textEdit->setPlainText(m_properties->get("templatetext")); break; case ClipType::Image: case ClipType::AV: case ClipType::Video: { QString proxy = m_properties->get("kdenlive:proxy"); if (proxy != m_originalProperties.value(QStringLiteral("kdenlive:proxy"))) { m_originalProperties.insert(QStringLiteral("kdenlive:proxy"), proxy); emit proxyModified(proxy); } break; } default: break; } } void ClipPropertiesController::slotColorModified(const QColor &newcolor) { QMap properties; properties.insert(QStringLiteral("resource"), newcolor.name(QColor::HexArgb)); QMap oldProperties; oldProperties.insert(QStringLiteral("resource"), m_properties->get("resource")); emit updateClipProperties(m_id, oldProperties, properties); } void ClipPropertiesController::slotDurationChanged(int duration) { QMap properties; // kdenlive_length is the default duration for image / title clips int kdenlive_length = m_properties->time_to_frames(m_properties->get("kdenlive:duration")); int current_length = m_properties->get_int("length"); if (kdenlive_length > 0) { // special case, image/title clips store default duration in kdenlive:duration property properties.insert(QStringLiteral("kdenlive:duration"), m_properties->frames_to_time(duration)); if (duration > current_length) { properties.insert(QStringLiteral("length"), m_properties->frames_to_time(duration)); properties.insert(QStringLiteral("out"), m_properties->frames_to_time(duration - 1)); } } else { properties.insert(QStringLiteral("length"), m_properties->frames_to_time(duration)); properties.insert(QStringLiteral("out"), m_properties->frames_to_time(duration - 1)); } emit updateClipProperties(m_id, m_originalProperties, properties); m_originalProperties = properties; } void ClipPropertiesController::slotEnableForce(int state) { - QCheckBox *box = qobject_cast(sender()); + auto *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")); + auto *timePos = findChild(param + QStringLiteral("_value")); timePos->setValue(m_properties->get_int("kdenlive:original_length")); int original = m_properties->get_int("kdenlive:original_length"); m_properties->set("kdenlive:original_length", (char *)nullptr); slotDurationChanged(original); return; } if (param == QLatin1String("kdenlive:transparency")) { properties.insert(param, QString()); } else if (param == QLatin1String("force_ar")) { properties.insert(QStringLiteral("force_aspect_den"), QString()); properties.insert(QStringLiteral("force_aspect_num"), QString()); properties.insert(QStringLiteral("force_aspect_ratio"), QString()); } else if (param == QLatin1String("autorotate")) { properties.insert(QStringLiteral("autorotate"), QString()); } else { properties.insert(param, QString()); } } else { // A force property was set if (param == QLatin1String("force_duration")) { int original_length = m_properties->get_int("kdenlive:original_length"); if (original_length == 0) { int kdenlive_length = m_properties->time_to_frames(m_properties->get("kdenlive:duration")); m_properties->set("kdenlive:original_length", kdenlive_length > 0 ? m_properties->get("kdenlive:duration") : m_properties->get("length")); } } else if (param == QLatin1String("force_fps")) { - QDoubleSpinBox *spin = findChild(param + QStringLiteral("_value")); + auto *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")); + auto *spin = findChild(param + QStringLiteral("_value")); if (!spin) { return; } properties.insert(param, QString::number(spin->value())); } else if (param == QLatin1String("force_colorspace") || param == QLatin1String("force_progressive") || param == QLatin1String("force_tff")) { - QComboBox *combo = findChild(param + QStringLiteral("_value")); + auto *combo = findChild(param + QStringLiteral("_value")); if (!combo) { return; } properties.insert(param, QString::number(combo->currentData().toInt())); } else if (param == QLatin1String("set.force_full_luma")) { properties.insert(param, QStringLiteral("1")); } else if (param == QLatin1String("autorotate")) { properties.insert(QStringLiteral("autorotate"), QStringLiteral("0")); } else if (param == QLatin1String("force_ar")) { - QSpinBox *spin = findChild(QStringLiteral("force_aspect_num_value")); - QSpinBox *spin2 = findChild(QStringLiteral("force_aspect_den_value")); + auto *spin = findChild(QStringLiteral("force_aspect_num_value")); + auto *spin2 = findChild(QStringLiteral("force_aspect_den_value")); if ((spin == nullptr) || (spin2 == nullptr)) { return; } properties.insert(QStringLiteral("force_aspect_den"), QString::number(spin2->value())); properties.insert(QStringLiteral("force_aspect_num"), QString::number(spin->value())); properties.insert(QStringLiteral("force_aspect_ratio"), locale.toString((double)spin->value() / spin2->value())); } } if (properties.isEmpty()) { return; } emit updateClipProperties(m_id, m_originalProperties, properties); m_originalProperties = properties; } void ClipPropertiesController::slotValueChanged(double value) { - QDoubleSpinBox *box = qobject_cast(sender()); + auto *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()); + auto *box = qobject_cast(sender()); if (!box) { return; } QString param = box->objectName().section(QLatin1Char('_'), 0, -2); QMap properties; properties.insert(param, QString::number(value)); emit updateClipProperties(m_id, m_originalProperties, properties); m_originalProperties = properties; } void ClipPropertiesController::slotAspectValueChanged(int) { - QSpinBox *spin = findChild(QStringLiteral("force_aspect_num_value")); - QSpinBox *spin2 = findChild(QStringLiteral("force_aspect_den_value")); + auto *spin = findChild(QStringLiteral("force_aspect_num_value")); + auto *spin2 = findChild(QStringLiteral("force_aspect_den_value")); if ((spin == nullptr) || (spin2 == nullptr)) { return; } QMap properties; properties.insert(QStringLiteral("force_aspect_den"), QString::number(spin2->value())); properties.insert(QStringLiteral("force_aspect_num"), QString::number(spin->value())); QLocale locale; 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()); + auto *box = qobject_cast(sender()); if (!box) { return; } QString param = box->objectName().section(QLatin1Char('_'), 0, -2); QMap properties; properties.insert(param, QString::number(box->currentData().toInt())); emit updateClipProperties(m_id, m_originalProperties, properties); m_originalProperties = properties; } void ClipPropertiesController::fillProperties() { m_clipProperties.clear(); QList propertyMap; m_propertiesTree->setSortingEnabled(false); #ifdef KF5_USE_FILEMETADATA // Read File Metadata through KDE's metadata system KFileMetaData::ExtractorCollection metaDataCollection; QMimeDatabase mimeDatabase; QMimeType mimeType; mimeType = mimeDatabase.mimeTypeForFile(m_controller->clipUrl()); for (KFileMetaData::Extractor *plugin : metaDataCollection.fetchExtractors(mimeType.name())) { ExtractionResult extractionResult(m_controller->clipUrl(), mimeType.name(), m_propertiesTree); plugin->extract(&extractionResult); } #endif // Get MLT's metadata if (m_type == ClipType::Image) { int width = m_sourceProperties.get_int("meta.media.width"); int height = m_sourceProperties.get_int("meta.media.height"); propertyMap.append(QStringList() << i18n("Image size") << QString::number(width) + QLatin1Char('x') + QString::number(height)); } if (m_type == ClipType::AV || m_type == ClipType::Video || m_type == ClipType::Audio) { int vindex = m_sourceProperties.get_int("video_index"); int default_audio = m_sourceProperties.get_int("audio_index"); // Find maximum stream index values m_videoStreams.clear(); 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)); + auto *item = new QTreeWidgetItem(m_propertiesTree, propertyMap.at(i)); item->setToolTip(1, propertyMap.at(i).at(1)); } m_propertiesTree->setSortingEnabled(true); m_propertiesTree->resizeColumnToContents(0); } void ClipPropertiesController::slotSeekToMarker() { auto markerModel = m_controller->getMarkerModel(); auto current = m_markerTree->currentIndex(); if (!current.isValid()) return; GenTime pos(markerModel->data(current, MarkerListModel::PosRole).toDouble()); emit seekToFrame(pos.frames(pCore->getCurrentFps())); } void ClipPropertiesController::slotEditMarker() { auto markerModel = m_controller->getMarkerModel(); auto current = m_markerTree->currentIndex(); if (!current.isValid()) return; GenTime pos(markerModel->data(current, MarkerListModel::PosRole).toDouble()); markerModel->editMarkerGui(pos, this, false, m_controller); } void ClipPropertiesController::slotDeleteMarker() { auto markerModel = m_controller->getMarkerModel(); auto current = m_markerTree->currentIndex(); if (!current.isValid()) return; GenTime pos(markerModel->data(current, MarkerListModel::PosRole).toDouble()); markerModel->removeMarker(pos); } 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/mltcontroller/clippropertiescontroller.h b/src/mltcontroller/clippropertiescontroller.h index 8986a5f8c..ead4ad805 100644 --- a/src/mltcontroller/clippropertiescontroller.h +++ b/src/mltcontroller/clippropertiescontroller.h @@ -1,131 +1,131 @@ /* 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 CLIPPROPERTIESCONTROLLER_H #define CLIPPROPERTIESCONTROLLER_H #include "definitions.h" #include "timecode.h" #include #include #include class ClipController; class QMimeData; class QTextEdit; class QLabel; class AnalysisTree : public QTreeWidget { public: explicit AnalysisTree(QWidget *parent = nullptr); protected: QMimeData *mimeData(const QList list) const override; }; /** * @class ClipPropertiesController * @brief This class creates the widgets allowing to edit clip properties */ class ClipPropertiesController : public QWidget { Q_OBJECT public: /** * @brief Constructor. * @param id The clip's id * @param properties The clip's properties * @param parent The widget where our infos will be displayed */ explicit ClipPropertiesController(ClipController *controller, QWidget *parent); - virtual ~ClipPropertiesController(); + ~ClipPropertiesController() override; public slots: void slotReloadProperties(); void slotRefreshTimeCode(); void slotFillMeta(QTreeWidget *tree); void slotFillAnalysisData(); private slots: void slotColorModified(const QColor &newcolor); void slotDurationChanged(int duration); void slotEnableForce(int state); void slotValueChanged(double); void slotSeekToMarker(); void slotEditMarker(); void slotDeleteMarker(); void slotAddMarker(); void slotLoadMarkers(); void slotSaveMarkers(); void slotDeleteAnalysis(); void slotSaveAnalysis(); void slotLoadAnalysis(); void slotAspectValueChanged(int); void slotComboValueChanged(); void slotValueChanged(int value); void slotTextChanged(); void updateTab(int ix); private: ClipController *m_controller; QTabWidget *m_tabWidget; QLabel *m_clipLabel; Timecode m_tc; QString m_id; ClipType::ProducerType m_type; /** @brief: the properties of the active producer (can be a proxy) */ std::shared_ptr m_properties; /** @brief: the properties of the original source producer (cannot be a proxy) */ Mlt::Properties m_sourceProperties; QMap m_originalProperties; QMap m_clipProperties; QList m_videoStreams; QList m_audioStreams; QTreeWidget *m_propertiesTree; QWidget *m_propertiesPage; QWidget *m_markersPage; QWidget *m_metaPage; QWidget *m_analysisPage; QTreeView *m_markerTree; AnalysisTree *m_analysisTree; QTextEdit *m_textEdit; void fillProperties(); signals: void updateClipProperties(const QString &, const QMap &, const QMap &); void modified(const QColor &); void modified(int); void updateTimeCodeFormat(); /** @brief Seek clip monitor to a frame. */ void seekToFrame(int); void editAnalysis(const QString &id, const QString &name, const QString &value); void editClip(); void requestProxy(bool doProxy); void proxyModified(const QString &); void deleteProxy(); void enableProxy(bool); }; #endif diff --git a/src/monitor/abstractmonitor.cpp b/src/monitor/abstractmonitor.cpp index b5ccfe7df..4a15e2163 100644 --- a/src/monitor/abstractmonitor.cpp +++ b/src/monitor/abstractmonitor.cpp @@ -1,42 +1,42 @@ /*************************************************************************** * 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 "abstractmonitor.h" #include "monitormanager.h" #include "kdenlivesettings.h" AbstractMonitor::AbstractMonitor(Kdenlive::MonitorId id, MonitorManager *manager, QWidget *parent) : QWidget(parent) , m_id(id) , m_monitorManager(manager) { } -AbstractMonitor::~AbstractMonitor() {} +AbstractMonitor::~AbstractMonitor() = default; bool AbstractMonitor::isActive() const { return m_monitorManager->isActive(m_id); } bool AbstractMonitor::slotActivateMonitor() { return m_monitorManager->activateMonitor(m_id); } diff --git a/src/monitor/abstractmonitor.h b/src/monitor/abstractmonitor.h index 913b2e2b0..811709bcc 100644 --- a/src/monitor/abstractmonitor.h +++ b/src/monitor/abstractmonitor.h @@ -1,108 +1,108 @@ /*************************************************************************** * 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 ABSTRACTMONITOR_H #define ABSTRACTMONITOR_H #include "definitions.h" -#include +#include #include #include #include class MonitorManager; class AbstractRender : public QObject { Q_OBJECT public : /** @brief Build an abstract MLT Renderer * @param name A unique identifier for this 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 renderer (default one will be used if empty). */ explicit AbstractRender(Kdenlive::MonitorId name, QWidget *parent = nullptr) : QObject(parent) , sendFrameForAnalysis(false) , analyseAudio(false) , m_id(name) { } /** @brief Destroy the MLT Renderer. */ - virtual ~AbstractRender() {} + ~AbstractRender() override = default; /** @brief This property is used to decide if the renderer should convert it's frames to QImage for use in other Kdenlive widgets. */ bool sendFrameForAnalysis; /** @brief This property is used to decide if the renderer should send audio data for monitoring. */ bool analyseAudio; Kdenlive::MonitorId id() const { return m_id; } /** @brief Someone needs us to send again a frame. */ virtual void sendFrameUpdate() = 0; private: Kdenlive::MonitorId m_id; signals: /** @brief The renderer refreshed the current frame. */ void frameUpdated(const QImage &); /** @brief This signal contains the audio of the current frame. */ void audioSamplesSignal(const audioShortVector &, int, int, int); /** @brief Scopes are ready to receive a new frame. */ void scopesClear(); }; class AbstractMonitor : public QWidget { Q_OBJECT public: AbstractMonitor(Kdenlive::MonitorId id, MonitorManager *manager, QWidget *parent = nullptr); Kdenlive::MonitorId id() { return m_id; } - virtual ~AbstractMonitor(); + ~AbstractMonitor() override; bool isActive() const; virtual void mute(bool mute, bool updateIconOnly = false) = 0; public slots: virtual void stop() = 0; virtual void start() = 0; virtual void slotPlay() = 0; virtual void refreshMonitorIfActive(bool directUpdate = false) = 0; virtual void slotMouseSeek(int eventDelta, uint modifiers) = 0; bool slotActivateMonitor(); virtual void slotSwitchFullScreen(bool minimizeOnly = false) = 0; protected: Kdenlive::MonitorId m_id; MonitorManager *m_monitorManager; signals: /** @brief Send a frame for analysis or title background display. */ void frameUpdated(const QImage &); /** @brief This signal contains the audio of the current frame. */ void audioSamplesSignal(const audioShortVector &, int, int, int); /** @brief Scopes are ready to receive a new frame. */ void scopesClear(); }; #endif diff --git a/src/monitor/glwidget.cpp b/src/monitor/glwidget.cpp index 64f26a5a0..68417bcba 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(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()); + return {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); + return {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); + auto *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); + auto *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); + auto *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"); + auto 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")); + auto *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, 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 f0f420f6f..199ff7685 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 *); +using thread_function_t = void *(*)(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(); + ~GLWidget() override; 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, 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(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(); + ~RenderThread() override; 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(); + ~FrameRenderer() override; 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 9cf877ca4..03319f02b 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); + auto *ev = reinterpret_cast(event); if (ev->mimeData()->hasFormat(QStringLiteral("kdenlive/effect"))) { ev->acceptProposedAction(); return true; } break; } case QEvent::DragMove: { - QDragEnterEvent *ev = reinterpret_cast(event); + auto *ev = reinterpret_cast(event); if (ev->mimeData()->hasFormat(QStringLiteral("kdenlive/effect"))) { ev->acceptProposedAction(); return true; } break; } case QEvent::Drop: { - QDropEvent *ev = static_cast(event); + auto *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); + auto *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()); + return {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(); + auto *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(); + auto *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, 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(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(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 (/* DISABLES CODE */ (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 {}; } 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 {}; } 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 (/* DISABLES CODE */ (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(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 e4c9d7e37..53d4f712b 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(); + ~Monitor() override; 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(const std::shared_ptr &prod); void updateClipProducer(const QString &playlist); 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, 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/monitor/monitormanager.cpp b/src/monitor/monitormanager.cpp index 7b9127fd3..9e151a61e 100644 --- a/src/monitor/monitormanager.cpp +++ b/src/monitor/monitormanager.cpp @@ -1,626 +1,623 @@ /*************************************************************************** * 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 "monitormanager.h" #include "core.h" #include "doc/kdenlivedoc.h" #include "kdenlivesettings.h" #include "mainwindow.h" #include #include "klocalizedstring.h" #include #include "kdenlive_debug.h" #include MonitorManager::MonitorManager(QObject *parent) : QObject(parent) - , m_document(nullptr) - , m_clipMonitor(nullptr) - , m_projectMonitor(nullptr) - , m_activeMonitor(nullptr) + { setupActions(); } Timecode MonitorManager::timecode() const { return m_timecode; } void MonitorManager::setDocument(KdenliveDoc *doc) { m_document = doc; } QAction *MonitorManager::getAction(const QString &name) { return pCore->window()->action(name.toUtf8().constData()); } void MonitorManager::initMonitors(Monitor *clipMonitor, Monitor *projectMonitor) { m_clipMonitor = clipMonitor; m_projectMonitor = projectMonitor; m_monitorsList.append(clipMonitor); m_monitorsList.append(projectMonitor); } void MonitorManager::appendMonitor(AbstractMonitor *monitor) { if (!m_monitorsList.contains(monitor)) { m_monitorsList.append(monitor); } } void MonitorManager::removeMonitor(AbstractMonitor *monitor) { m_monitorsList.removeAll(monitor); } AbstractMonitor *MonitorManager::monitor(Kdenlive::MonitorId monitorName) { AbstractMonitor *monitor = nullptr; - for (int i = 0; i < m_monitorsList.size(); ++i) { - if (m_monitorsList[i]->id() == monitorName) { - monitor = m_monitorsList.at(i); + for (auto &i : m_monitorsList) { + if (i->id() == monitorName) { + monitor = i; } } return monitor; } void MonitorManager::setConsumerProperty(const QString &name, const QString &value) { if (m_clipMonitor) { m_clipMonitor->setConsumerProperty(name, value); } if (m_projectMonitor) { m_projectMonitor->setConsumerProperty(name, value); } } void MonitorManager::lockMonitor(Kdenlive::MonitorId name, bool lock) { Q_UNUSED(name) if (lock) { m_refreshMutex.lock(); } else { m_refreshMutex.unlock(); } } void MonitorManager::focusProjectMonitor() { activateMonitor(Kdenlive::ProjectMonitor); } void MonitorManager::refreshProjectRange(QSize range) { if (m_projectMonitor->position() >= range.width() && m_projectMonitor->position() <= range.height()) { m_projectMonitor->refreshMonitorIfActive(); } } void MonitorManager::refreshProjectMonitor() { m_projectMonitor->refreshMonitorIfActive(); } void MonitorManager::refreshClipMonitor() { m_clipMonitor->refreshMonitorIfActive(); } bool MonitorManager::activateMonitor(Kdenlive::MonitorId name) { if (m_clipMonitor == nullptr || m_projectMonitor == nullptr) { return false; } if ((m_activeMonitor != nullptr) && m_activeMonitor->id() == name) { return false; } QMutexLocker locker(&m_switchMutex); bool stopCurrent = m_activeMonitor != nullptr; for (int i = 0; i < m_monitorsList.count(); ++i) { if (m_monitorsList.at(i)->id() == name) { m_activeMonitor = m_monitorsList.at(i); } else if (stopCurrent) { m_monitorsList.at(i)->stop(); } } if (m_activeMonitor) { m_activeMonitor->parentWidget()->raise(); if (name == Kdenlive::ClipMonitor) { emit updateOverlayInfos(name, KdenliveSettings::displayClipMonitorInfo()); m_projectMonitor->displayAudioMonitor(false); m_clipMonitor->displayAudioMonitor(true); } else if (name == Kdenlive::ProjectMonitor) { emit updateOverlayInfos(name, KdenliveSettings::displayProjectMonitorInfo()); m_clipMonitor->displayAudioMonitor(false); m_projectMonitor->displayAudioMonitor(true); } } emit checkColorScopes(); return (m_activeMonitor != nullptr); } void MonitorManager::resetDisplay() { m_projectMonitor->clearDisplay(); m_clipMonitor->clearDisplay(); } bool MonitorManager::isActive(Kdenlive::MonitorId id) const { return m_activeMonitor ? m_activeMonitor->id() == id : false; } void MonitorManager::slotSwitchMonitors(bool activateClip) { if (activateClip) { activateMonitor(Kdenlive::ClipMonitor); } else { activateMonitor(Kdenlive::ProjectMonitor); } } void MonitorManager::stopActiveMonitor() { if (m_activeMonitor) { m_activeMonitor->stop(); } } void MonitorManager::pauseActiveMonitor() { if (m_activeMonitor == m_clipMonitor) { m_clipMonitor->pause(); } else if (m_activeMonitor == m_projectMonitor) { m_projectMonitor->pause(); } } void MonitorManager::slotPlay() { if (m_activeMonitor) { m_activeMonitor->slotPlay(); } } void MonitorManager::slotPause() { pauseActiveMonitor(); } void MonitorManager::slotPlayZone() { if (m_activeMonitor == m_clipMonitor) { m_clipMonitor->slotPlayZone(); } else if (m_activeMonitor == m_projectMonitor) { m_projectMonitor->slotPlayZone(); } } void MonitorManager::slotLoopZone() { if (m_activeMonitor == m_clipMonitor) { m_clipMonitor->slotLoopZone(); } else { m_projectMonitor->slotLoopZone(); } } void MonitorManager::slotRewind(double speed) { if (m_activeMonitor == m_clipMonitor) { m_clipMonitor->slotRewind(speed); } else if (m_activeMonitor == m_projectMonitor) { m_projectMonitor->slotRewind(speed); } } void MonitorManager::slotForward(double speed) { if (m_activeMonitor == m_clipMonitor) { m_clipMonitor->slotForward(speed); } else if (m_activeMonitor == m_projectMonitor) { m_projectMonitor->slotForward(speed); } } void MonitorManager::slotRewindOneFrame() { if (m_activeMonitor == m_clipMonitor) { m_clipMonitor->slotRewindOneFrame(); } else if (m_activeMonitor == m_projectMonitor) { m_projectMonitor->slotRewindOneFrame(); } } void MonitorManager::slotForwardOneFrame() { if (m_activeMonitor == m_clipMonitor) { m_clipMonitor->slotForwardOneFrame(); } else if (m_activeMonitor == m_projectMonitor) { m_projectMonitor->slotForwardOneFrame(); } } void MonitorManager::slotRewindOneSecond() { if (m_activeMonitor == m_clipMonitor) { m_clipMonitor->slotRewindOneFrame(m_timecode.fps()); } else if (m_activeMonitor == m_projectMonitor) { m_projectMonitor->slotRewindOneFrame(m_timecode.fps()); } } void MonitorManager::slotForwardOneSecond() { if (m_activeMonitor == m_clipMonitor) { m_clipMonitor->slotForwardOneFrame(m_timecode.fps()); } else if (m_activeMonitor == m_projectMonitor) { m_projectMonitor->slotForwardOneFrame(m_timecode.fps()); } } void MonitorManager::slotStart() { if (m_activeMonitor == m_clipMonitor) { m_clipMonitor->slotStart(); } else if (m_activeMonitor == m_projectMonitor) { m_projectMonitor->slotStart(); } } void MonitorManager::slotEnd() { if (m_activeMonitor == m_clipMonitor) { m_clipMonitor->slotEnd(); } else if (m_activeMonitor == m_projectMonitor) { m_projectMonitor->slotEnd(); } } void MonitorManager::resetProfiles(const Timecode &tc) { m_timecode = tc; m_clipMonitor->resetProfile(); m_projectMonitor->resetProfile(); } void MonitorManager::resetConsumers(bool fullReset) { bool clipMonitorActive = m_clipMonitor->isActive(); m_clipMonitor->resetConsumer(fullReset); m_projectMonitor->resetConsumer(fullReset); if (clipMonitorActive) { refreshClipMonitor(); } else { refreshProjectMonitor(); } } void MonitorManager::slotUpdateAudioMonitoring() { if (m_clipMonitor) { m_clipMonitor->updateAudioForAnalysis(); } if (m_projectMonitor) { m_projectMonitor->updateAudioForAnalysis(); } } void MonitorManager::clearScopeSource() { emit clearScopes(); } void MonitorManager::updateScopeSource() { emit checkColorScopes(); } AbstractMonitor *MonitorManager::activeMonitor() { if (m_activeMonitor) { return m_activeMonitor; } return nullptr; } void MonitorManager::slotSwitchFullscreen() { if (m_activeMonitor) { m_activeMonitor->slotSwitchFullScreen(); } } QString MonitorManager::getProjectFolder() const { if (m_document == nullptr) { // qCDebug(KDENLIVE_LOG)<<" + + +nullptr DOC!!"; return QString(); } return m_document->projectDataFolder() + QDir::separator(); } void MonitorManager::setupActions() { KDualAction *playAction = new KDualAction(i18n("Play"), i18n("Pause"), this); playAction->setInactiveIcon(QIcon::fromTheme(QStringLiteral("media-playback-start"))); playAction->setActiveIcon(QIcon::fromTheme(QStringLiteral("media-playback-pause"))); connect(playAction, &KDualAction::activeChangedByUser, this, &MonitorManager::slotPlay); pCore->window()->addAction(QStringLiteral("monitor_play"), playAction, Qt::Key_Space); QAction *monitorPause = new QAction(QIcon::fromTheme(QStringLiteral("media-playback-stop")), i18n("Pause"), this); connect(monitorPause, &QAction::triggered, this, &MonitorManager::slotPause); pCore->window()->addAction(QStringLiteral("monitor_pause"), monitorPause, Qt::Key_K); QAction *fullMonitor = new QAction(i18n("Switch monitor fullscreen"), this); fullMonitor->setIcon(QIcon::fromTheme(QStringLiteral("view-fullscreen"))); connect(fullMonitor, &QAction::triggered, this, &MonitorManager::slotSwitchFullscreen); pCore->window()->addAction(QStringLiteral("monitor_fullscreen"), fullMonitor); QAction *monitorZoomIn = new QAction(i18n("Zoom in monitor"), this); monitorZoomIn->setIcon(QIcon::fromTheme(QStringLiteral("zoom-in"))); connect(monitorZoomIn, &QAction::triggered, this, &MonitorManager::slotZoomIn); pCore->window()->addAction(QStringLiteral("monitor_zoomin"), monitorZoomIn); QAction *monitorZoomOut = new QAction(i18n("Zoom out monitor"), this); monitorZoomOut->setIcon(QIcon::fromTheme(QStringLiteral("zoom-out"))); connect(monitorZoomOut, &QAction::triggered, this, &MonitorManager::slotZoomOut); pCore->window()->addAction(QStringLiteral("monitor_zoomout"), monitorZoomOut); QAction *monitorSeekBackward = new QAction(QIcon::fromTheme(QStringLiteral("media-seek-backward")), i18n("Rewind"), this); connect(monitorSeekBackward, SIGNAL(triggered(bool)), SLOT(slotRewind())); pCore->window()->addAction(QStringLiteral("monitor_seek_backward"), monitorSeekBackward, Qt::Key_J); QAction *monitorSeekBackwardOneFrame = new QAction(QIcon::fromTheme(QStringLiteral("media-skip-backward")), i18n("Rewind 1 Frame"), this); connect(monitorSeekBackwardOneFrame, &QAction::triggered, this, &MonitorManager::slotRewindOneFrame); pCore->window()->addAction(QStringLiteral("monitor_seek_backward-one-frame"), monitorSeekBackwardOneFrame, Qt::Key_Left); QAction *monitorSeekBackwardOneSecond = new QAction(QIcon::fromTheme(QStringLiteral("media-skip-backward")), i18n("Rewind 1 Second"), this); connect(monitorSeekBackwardOneSecond, &QAction::triggered, this, &MonitorManager::slotRewindOneSecond); pCore->window()->addAction(QStringLiteral("monitor_seek_backward-one-second"), monitorSeekBackwardOneSecond, Qt::SHIFT + Qt::Key_Left); QAction *monitorSeekForward = new QAction(QIcon::fromTheme(QStringLiteral("media-seek-forward")), i18n("Forward"), this); connect(monitorSeekForward, SIGNAL(triggered(bool)), SLOT(slotForward())); pCore->window()->addAction(QStringLiteral("monitor_seek_forward"), monitorSeekForward, Qt::Key_L); QAction *projectStart = new QAction(QIcon::fromTheme(QStringLiteral("go-first")), i18n("Go to Project Start"), this); connect(projectStart, &QAction::triggered, this, &MonitorManager::slotStart); pCore->window()->addAction(QStringLiteral("seek_start"), projectStart, Qt::CTRL + Qt::Key_Home); QAction *projectEnd = new QAction(QIcon::fromTheme(QStringLiteral("go-last")), i18n("Go to Project End"), this); connect(projectEnd, &QAction::triggered, this, &MonitorManager::slotEnd); pCore->window()->addAction(QStringLiteral("seek_end"), projectEnd, Qt::CTRL + Qt::Key_End); QAction *monitorSeekForwardOneFrame = new QAction(QIcon::fromTheme(QStringLiteral("media-skip-forward")), i18n("Forward 1 Frame"), this); connect(monitorSeekForwardOneFrame, &QAction::triggered, this, &MonitorManager::slotForwardOneFrame); pCore->window()->addAction(QStringLiteral("monitor_seek_forward-one-frame"), monitorSeekForwardOneFrame, Qt::Key_Right); QAction *monitorSeekForwardOneSecond = new QAction(QIcon::fromTheme(QStringLiteral("media-skip-forward")), i18n("Forward 1 Second"), this); connect(monitorSeekForwardOneSecond, &QAction::triggered, this, &MonitorManager::slotForwardOneSecond); pCore->window()->addAction(QStringLiteral("monitor_seek_forward-one-second"), monitorSeekForwardOneSecond, Qt::SHIFT + Qt::Key_Right); KSelectAction *interlace = new KSelectAction(i18n("Deinterlacer"), this); interlace->addAction(i18n("One Field (fast)")); interlace->addAction(i18n("Linear Blend (fast)")); interlace->addAction(i18n("YADIF - temporal only (good)")); interlace->addAction(i18n("YADIF - temporal + spacial (best)")); if (KdenliveSettings::mltdeinterlacer() == QLatin1String("linearblend")) { interlace->setCurrentItem(1); } else if (KdenliveSettings::mltdeinterlacer() == QLatin1String("yadif-temporal")) { interlace->setCurrentItem(2); } else if (KdenliveSettings::mltdeinterlacer() == QLatin1String("yadif")) { interlace->setCurrentItem(3); } else { interlace->setCurrentItem(0); } connect(interlace, static_cast(&KSelectAction::triggered), this, &MonitorManager::slotSetDeinterlacer); pCore->window()->addAction(QStringLiteral("mlt_interlace"), interlace); KSelectAction *interpol = new KSelectAction(i18n("Interpolation"), this); interpol->addAction(i18n("Nearest Neighbor (fast)")); interpol->addAction(i18n("Bilinear (good)")); interpol->addAction(i18n("Bicubic (better)")); interpol->addAction(i18n("Hyper/Lanczos (best)")); if (KdenliveSettings::mltinterpolation() == QLatin1String("bilinear")) { interpol->setCurrentItem(1); } else if (KdenliveSettings::mltinterpolation() == QLatin1String("bicubic")) { interpol->setCurrentItem(2); } else if (KdenliveSettings::mltinterpolation() == QLatin1String("hyper")) { interpol->setCurrentItem(3); } else { interpol->setCurrentItem(0); } connect(interpol, static_cast(&KSelectAction::triggered), this, &MonitorManager::slotSetInterpolation); pCore->window()->addAction(QStringLiteral("mlt_interpolation"), interpol); QAction *zoneStart = new QAction(QIcon::fromTheme(QStringLiteral("media-seek-backward")), i18n("Go to Zone Start"), this); connect(zoneStart, &QAction::triggered, this, &MonitorManager::slotZoneStart); pCore->window()->addAction(QStringLiteral("seek_zone_start"), zoneStart, Qt::SHIFT + Qt::Key_I); m_muteAction = new KDualAction(i18n("Mute monitor"), i18n("Unmute monitor"), this); m_muteAction->setActiveIcon(QIcon::fromTheme(QStringLiteral("audio-volume-medium"))); m_muteAction->setInactiveIcon(QIcon::fromTheme(QStringLiteral("audio-volume-muted"))); connect(m_muteAction, &KDualAction::activeChangedByUser, this, &MonitorManager::slotMuteCurrentMonitor); pCore->window()->addAction(QStringLiteral("mlt_mute"), m_muteAction); QAction *zoneEnd = new QAction(QIcon::fromTheme(QStringLiteral("media-seek-forward")), i18n("Go to Zone End"), this); connect(zoneEnd, &QAction::triggered, this, &MonitorManager::slotZoneEnd); pCore->window()->addAction(QStringLiteral("seek_zone_end"), zoneEnd, Qt::SHIFT + Qt::Key_O); QAction *markIn = new QAction(QIcon::fromTheme(QStringLiteral("zone-in")), i18n("Set Zone In"), this); connect(markIn, &QAction::triggered, this, &MonitorManager::slotSetInPoint); pCore->window()->addAction(QStringLiteral("mark_in"), markIn, Qt::Key_I); QAction *markOut = new QAction(QIcon::fromTheme(QStringLiteral("zone-out")), i18n("Set Zone Out"), this); connect(markOut, &QAction::triggered, this, &MonitorManager::slotSetOutPoint); pCore->window()->addAction(QStringLiteral("mark_out"), markOut, Qt::Key_O); } void MonitorManager::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); } } void MonitorManager::slotSetDeinterlacer(int ix) { QString value; switch (ix) { case 1: value = QStringLiteral("linearblend"); break; case 2: value = QStringLiteral("yadif-nospatial"); break; case 3: value = QStringLiteral("yadif"); break; default: value = QStringLiteral("onefield"); } KdenliveSettings::setMltdeinterlacer(value); setConsumerProperty(QStringLiteral("deinterlace_method"), value); } void MonitorManager::slotSetInterpolation(int ix) { QString value; switch (ix) { case 1: value = QStringLiteral("bilinear"); break; case 2: value = QStringLiteral("bicubic"); break; case 3: value = QStringLiteral("hyper"); break; default: value = QStringLiteral("nearest"); } KdenliveSettings::setMltinterpolation(value); setConsumerProperty(QStringLiteral("rescale"), value); } void MonitorManager::slotMuteCurrentMonitor(bool active) { m_activeMonitor->mute(active); } Monitor *MonitorManager::clipMonitor() { return m_clipMonitor; } Monitor *MonitorManager::projectMonitor() { return m_projectMonitor; } void MonitorManager::slotZoneStart() { if (m_activeMonitor == m_clipMonitor) { m_clipMonitor->slotZoneStart(); } else if (m_activeMonitor == m_projectMonitor) { m_projectMonitor->slotZoneStart(); } } void MonitorManager::slotZoneEnd() { if (m_activeMonitor == m_projectMonitor) { m_projectMonitor->slotZoneEnd(); } else if (m_activeMonitor == m_clipMonitor) { m_clipMonitor->slotZoneEnd(); } } void MonitorManager::slotSetInPoint() { if (m_activeMonitor == m_clipMonitor) { m_clipMonitor->slotSetZoneStart(); } else if (m_activeMonitor == m_projectMonitor) { m_projectMonitor->slotSetZoneStart(); } } void MonitorManager::slotSetOutPoint() { if (m_activeMonitor == m_clipMonitor) { m_clipMonitor->slotSetZoneEnd(); } else if (m_activeMonitor == m_projectMonitor) { // NOT anymore: Zone end behaves slightly differently in clip monitor and timeline monitor. // in timeline, set zone end selects the frame before current cursor, but in clip monitor // it selects frame at current cursor position. m_projectMonitor->slotSetZoneEnd(); } } QDir MonitorManager::getCacheFolder(CacheType type) { bool ok = false; if (m_document) { return m_document->getCacheDir(type, &ok); } return QDir(); } void MonitorManager::slotExtractCurrentFrame() { if (m_activeMonitor) { static_cast(m_activeMonitor)->slotExtractCurrentFrame(); } } void MonitorManager::slotExtractCurrentFrameToProject() { if (m_activeMonitor) { static_cast(m_activeMonitor)->slotExtractCurrentFrame(QString(), true); } } void MonitorManager::slotZoomIn() { if (m_activeMonitor) { static_cast(m_activeMonitor)->slotZoomIn(); } } void MonitorManager::slotZoomOut() { if (m_activeMonitor) { static_cast(m_activeMonitor)->slotZoomOut(); } } diff --git a/src/monitor/monitormanager.h b/src/monitor/monitormanager.h index 20637e47d..2f93c4921 100644 --- a/src/monitor/monitormanager.h +++ b/src/monitor/monitormanager.h @@ -1,153 +1,153 @@ /*************************************************************************** * 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 MONITORMANAGER_H #define MONITORMANAGER_H #include "monitor.h" #include "timecode.h" #include class KdenliveDoc; class KDualAction; namespace Mlt { class Profile; } class MonitorManager : public QObject { Q_OBJECT public: explicit MonitorManager(QObject *parent = nullptr); void initMonitors(Monitor *clipMonitor, Monitor *projectMonitor); void appendMonitor(AbstractMonitor *monitor); void removeMonitor(AbstractMonitor *monitor); Timecode timecode() const; void resetProfiles(const Timecode &tc); /** @brief delete and rebuild consumer, for example when external display is switched */ void resetConsumers(bool fullReset); void stopActiveMonitor(); void pauseActiveMonitor(); AbstractMonitor *activeMonitor(); /** Searches for a monitor with the given name. @return nullptr, if no monitor could be found, or the monitor otherwise. */ AbstractMonitor *monitor(Kdenlive::MonitorId monitorName); void updateScopeSource(); void clearScopeSource(); /** @brief Returns current project's folder. */ QString getProjectFolder() const; /** @brief Sets current document for later reference. */ void setDocument(KdenliveDoc *doc); /** @brief Change an MLT consumer property for both monitors. */ void setConsumerProperty(const QString &name, const QString &value); /** @brief Return a mainwindow action from its id name. */ QAction *getAction(const QString &name); Monitor *clipMonitor(); Monitor *projectMonitor(); void lockMonitor(Kdenlive::MonitorId name, bool); void refreshIcons(); void resetDisplay(); QDir getCacheFolder(CacheType type); public slots: /** @brief Activates a monitor. * @param name name of the monitor to activate */ bool activateMonitor(Kdenlive::MonitorId); bool isActive(Kdenlive::MonitorId id) const; void slotPlay(); void slotPause(); void slotPlayZone(); void slotLoopZone(); void slotRewind(double speed = 0); void slotForward(double speed = 0); void slotRewindOneFrame(); void slotForwardOneFrame(); void slotRewindOneSecond(); void slotForwardOneSecond(); void slotStart(); void slotEnd(); void slotZoneStart(); void slotZoneEnd(); void slotSetInPoint(); void slotSetOutPoint(); void focusProjectMonitor(); void refreshProjectMonitor(); /** @brief Refresh project monitor if the timeline cursor is inside the range. */ void refreshProjectRange(QSize range); void refreshClipMonitor(); /** @brief Switch current monitor to fullscreen. */ void slotSwitchFullscreen(); /** @brief Switches between project and clip monitor. * @ref activateMonitor * @param activateClip whether to activate the clip monitor */ void slotSwitchMonitors(bool activateClip); void slotUpdateAudioMonitoring(); /** @brief Export the current monitor's frame to image file. */ void slotExtractCurrentFrame(); /** @brief Export the current monitor's frame to image file and add it to the current project */ void slotExtractCurrentFrameToProject(); private slots: /** @brief Set MLT's consumer deinterlace method */ void slotSetDeinterlacer(int ix); /** @brief Set MLT's consumer interpolation method */ void slotSetInterpolation(int ix); /** @brief Switch muting on/off */ void slotMuteCurrentMonitor(bool active); /** @brief Zoom in active monitor */ void slotZoomIn(); /** @brief Zoom out active monitor */ void slotZoomOut(); private: /** @brief Make sure 2 monitors cannot be activated simultaneously*/ QMutex m_refreshMutex; QMutex m_switchMutex; /** @brief Sets up all the actions and attaches them to the collection of MainWindow. */ void setupActions(); - KdenliveDoc *m_document; - Monitor *m_clipMonitor; - Monitor *m_projectMonitor; + KdenliveDoc *m_document{nullptr}; + Monitor *m_clipMonitor{nullptr}; + Monitor *m_projectMonitor{nullptr}; Timecode m_timecode; - AbstractMonitor *m_activeMonitor; + AbstractMonitor *m_activeMonitor{nullptr}; QList m_monitorsList; KDualAction *m_muteAction; signals: /** @brief When the monitor changed, update the visible color scopes */ void checkColorScopes(); /** @brief When the active monitor renderer was deleted, reset color scopes */ void clearScopes(); /** @brief Check if we still need to send frame for scopes */ void checkScopes(); void addEffect(const QDomElement &); /** @brief Monitor activated, refresh overlay options actions */ void updateOverlayInfos(int, int); /** @brief info is available for audio spectrum widget */ void frameDisplayed(const SharedFrame &); }; #endif diff --git a/src/monitor/monitorproxy.cpp b/src/monitor/monitorproxy.cpp index f48f6ded9..8806d9522 100644 --- a/src/monitor/monitorproxy.cpp +++ b/src/monitor/monitorproxy.cpp @@ -1,279 +1,279 @@ /*************************************************************************** * Copyright (C) 2018 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 "monitorproxy.h" #include "core.h" #include "definitions.h" #include "doc/kthumb.h" #include "glwidget.h" #include "kdenlivesettings.h" #include "monitormanager.h" #include #include #include #include #define SEEK_INACTIVE (-1) MonitorProxy::MonitorProxy(GLWidget *parent) : QObject(parent) , q(parent) , m_position(0) , m_seekPosition(-1) , m_zoneIn(0) , m_zoneOut(-1) { } int MonitorProxy::seekPosition() const { return m_seekPosition; } bool MonitorProxy::seeking() const { return m_seekPosition != SEEK_INACTIVE; } int MonitorProxy::position() const { return m_position; } int MonitorProxy::rulerHeight() const { return q->m_rulerHeight; } int MonitorProxy::overlayType() const { return (q->m_id == (int)Kdenlive::ClipMonitor ? KdenliveSettings::clipMonitorOverlayGuides() : KdenliveSettings::projectMonitorOverlayGuides()); } void MonitorProxy::setOverlayType(int ix) { if (q->m_id == (int)Kdenlive::ClipMonitor) { KdenliveSettings::setClipMonitorOverlayGuides(ix); } else { KdenliveSettings::setProjectMonitorOverlayGuides(ix); } } QString MonitorProxy::markerComment() const { return m_markerComment; } void MonitorProxy::requestSeekPosition(int pos) { q->activateMonitor(); m_seekPosition = pos; emit seekPositionChanged(); emit seekRequestChanged(); } int MonitorProxy::seekOrCurrentPosition() const { return m_seekPosition == SEEK_INACTIVE ? m_position : m_seekPosition; } bool MonitorProxy::setPosition(int pos) { if (m_seekPosition == pos) { m_position = pos; m_seekPosition = SEEK_INACTIVE; emit seekPositionChanged(); } else if (m_position == pos) { return true; } else { m_position = pos; } emit positionChanged(); return false; } void MonitorProxy::setMarkerComment(const QString &comment) { if (m_markerComment == comment) { return; } m_markerComment = comment; emit markerCommentChanged(); } void MonitorProxy::setSeekPosition(int pos) { m_seekPosition = pos; emit seekPositionChanged(); } void MonitorProxy::pauseAndSeek(int pos) { q->switchPlay(false); requestSeekPosition(pos); } int MonitorProxy::zoneIn() const { return m_zoneIn; } int MonitorProxy::zoneOut() const { return m_zoneOut; } void MonitorProxy::setZoneIn(int pos) { if (m_zoneIn > 0) { emit removeSnap(m_zoneIn); } m_zoneIn = pos; if (pos > 0) { emit addSnap(pos); } emit zoneChanged(); emit saveZone(); } void MonitorProxy::setZoneOut(int pos) { if (m_zoneOut > 0) { emit removeSnap(m_zoneOut - 1); } m_zoneOut = pos; if (pos > 0) { emit addSnap(pos - 1); } emit zoneChanged(); emit saveZone(); } void MonitorProxy::setZone(int in, int out, bool sendUpdate) { if (m_zoneIn > 0) { emit removeSnap(m_zoneIn); } if (m_zoneOut > 0) { emit removeSnap(m_zoneOut - 1); } m_zoneIn = in; m_zoneOut = out; if (m_zoneIn > 0) { emit addSnap(m_zoneIn); } if (m_zoneOut > 0) { emit addSnap(m_zoneOut - 1); } emit zoneChanged(); if (sendUpdate) { emit saveZone(); } } void MonitorProxy::setZone(QPoint zone, bool sendUpdate) { setZone(zone.x(), zone.y(), sendUpdate); } void MonitorProxy::resetZone() { m_zoneIn = 0; m_zoneOut = -1; } QPoint MonitorProxy::zone() const { - return QPoint(m_zoneIn, m_zoneOut); + return {m_zoneIn, m_zoneOut}; } QImage MonitorProxy::extractFrame(int frame_position, const QString &path, int width, int height, bool useSourceProfile) { if (width == -1) { width = q->m_monitorProfile->width(); height = q->m_monitorProfile->height(); } else if (width % 2 == 1) { width++; } if (!path.isEmpty()) { QScopedPointer producer(new Mlt::Producer(*q->m_monitorProfile, path.toUtf8().constData())); if (producer && producer->is_valid()) { QImage img = KThumb::getFrame(producer.data(), frame_position, width, height); return img; } } if ((q->m_producer == nullptr) || !path.isEmpty()) { QImage pix(width, height, QImage::Format_RGB32); pix.fill(Qt::black); return pix; } Mlt::Frame *frame = nullptr; QImage img; if (useSourceProfile) { // Our source clip's resolution is higher than current profile, export at full res QScopedPointer tmpProfile(new Mlt::Profile()); QString service = q->m_producer->get("mlt_service"); QScopedPointer tmpProd(new Mlt::Producer(*tmpProfile, service.toUtf8().constData(), q->m_producer->get("resource"))); tmpProfile->from_producer(*tmpProd); width = tmpProfile->width(); height = tmpProfile->height(); if (tmpProd && tmpProd->is_valid()) { Mlt::Filter scaler(*tmpProfile, "swscale"); Mlt::Filter converter(*tmpProfile, "avcolor_space"); tmpProd->attach(scaler); tmpProd->attach(converter); // TODO: paste effects // Clip(*tmpProd).addEffects(*q->m_producer); tmpProd->seek(q->m_producer->position()); frame = tmpProd->get_frame(); img = KThumb::getFrame(frame, width, height); delete frame; } } else if (KdenliveSettings::gpu_accel()) { QString service = q->m_producer->get("mlt_service"); QScopedPointer tmpProd(new Mlt::Producer(*q->m_monitorProfile, service.toUtf8().constData(), q->m_producer->get("resource"))); Mlt::Filter scaler(*q->m_monitorProfile, "swscale"); Mlt::Filter converter(*q->m_monitorProfile, "avcolor_space"); tmpProd->attach(scaler); tmpProd->attach(converter); tmpProd->seek(q->m_producer->position()); frame = tmpProd->get_frame(); img = KThumb::getFrame(frame, width, height); delete frame; } else { frame = q->m_producer->get_frame(); img = KThumb::getFrame(frame, width, height); delete frame; } return img; } void MonitorProxy::activateClipMonitor(bool isClipMonitor) { pCore->monitorManager()->activateMonitor(isClipMonitor ? Kdenlive::ClipMonitor : Kdenlive::ProjectMonitor); } QString MonitorProxy::toTimecode(int frames) const { return KdenliveSettings::frametimecode() ? QString::number(frames) : q->frameToTime(frames); } diff --git a/src/monitor/recmanager.cpp b/src/monitor/recmanager.cpp index 19e728ae8..d16bca699 100644 --- a/src/monitor/recmanager.cpp +++ b/src/monitor/recmanager.cpp @@ -1,445 +1,444 @@ /*************************************************************************** * Copyright (C) 2015 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 "recmanager.h" #include "capture/managecapturesdialog.h" #include "capture/mltdevicecapture.h" #include "dialogs/profilesdialog.h" #include "kdenlivesettings.h" #include "monitor.h" #include "klocalizedstring.h" #include #include #include #include #include #include #include RecManager::RecManager(Monitor *parent) : QObject(parent) , m_monitor(parent) - , m_captureProcess(nullptr) , m_recToolbar(new QToolBar(parent)) - , m_screenCombo(nullptr) + { m_playAction = m_recToolbar->addAction(QIcon::fromTheme(QStringLiteral("media-playback-start")), i18n("Preview")); m_playAction->setCheckable(true); connect(m_playAction, &QAction::toggled, this, &RecManager::slotPreview); m_recAction = m_recToolbar->addAction(QIcon::fromTheme(QStringLiteral("media-record")), i18n("Record")); m_recAction->setCheckable(true); connect(m_recAction, &QAction::toggled, this, &RecManager::slotRecord); m_showLogAction = new QAction(i18n("Show log"), this); connect(m_showLogAction, &QAction::triggered, this, &RecManager::slotShowLog); m_recVideo = new QCheckBox(i18n("Video")); m_recAudio = new QCheckBox(i18n("Audio")); m_recToolbar->addWidget(m_recVideo); m_recToolbar->addWidget(m_recAudio); m_recAudio->setChecked(KdenliveSettings::v4l_captureaudio()); m_recVideo->setChecked(KdenliveSettings::v4l_capturevideo()); // Check number of monitors for FFmpeg screen capture int screens = QApplication::desktop()->screenCount(); if (screens > 1) { m_screenCombo = new QComboBox(parent); for (int ix = 0; ix < screens; ix++) { m_screenCombo->addItem(i18n("Monitor %1", ix)); } m_recToolbar->addWidget(m_screenCombo); // Update screen grab monitor choice in case we changed from fullscreen m_screenCombo->setEnabled(KdenliveSettings::grab_capture_type() == 0); } QWidget *spacer = new QWidget(parent); spacer->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); m_recToolbar->addWidget(spacer); m_device_selector = new QComboBox(parent); // TODO: re-implement firewire / decklink capture // m_device_selector->addItems(QStringList() << i18n("Firewire") << i18n("Webcam") << i18n("Screen Grab") << i18n("Blackmagic Decklink")); m_device_selector->addItem(i18n("Webcam"), Video4Linux); m_device_selector->addItem(i18n("Screen Grab"), ScreenGrab); int selectedCapture = m_device_selector->findData(KdenliveSettings::defaultcapture()); if (selectedCapture > -1) { m_device_selector->setCurrentIndex(selectedCapture); } connect(m_device_selector, static_cast(&QComboBox::currentIndexChanged), this, &RecManager::slotVideoDeviceChanged); m_recToolbar->addWidget(m_device_selector); QAction *configureRec = m_recToolbar->addAction(QIcon::fromTheme(QStringLiteral("configure")), i18n("Configure Recording")); connect(configureRec, &QAction::triggered, this, &RecManager::showRecConfig); m_recToolbar->addSeparator(); m_switchRec = m_recToolbar->addAction(QIcon::fromTheme(QStringLiteral("list-add")), i18n("Show Record Control")); m_switchRec->setCheckable(true); connect(m_switchRec, &QAction::toggled, m_monitor, &Monitor::slotSwitchRec); m_recToolbar->setVisible(false); slotVideoDeviceChanged(); } -RecManager::~RecManager() {} +RecManager::~RecManager() = default; void RecManager::showRecConfig() { m_monitor->showConfigDialog(4, m_device_selector->currentData().toInt()); } QToolBar *RecManager::toolbar() const { return m_recToolbar; } QAction *RecManager::switchAction() const { return m_switchRec; } void RecManager::stopCapture() { if (m_captureProcess) { slotRecord(false); } } void RecManager::stop() { if (m_captureProcess) { // Don't stop screen rec when hiding rec toolbar } else { stopCapture(); m_switchRec->setChecked(false); } toolbar()->setVisible(false); } void RecManager::slotRecord(bool record) { if (m_device_selector->currentData().toInt() == Video4Linux) { if (record) { QDir captureFolder; if (KdenliveSettings::capturetoprojectfolder()) { captureFolder = QDir(m_monitor->projectFolder()); } else { captureFolder = QDir(KdenliveSettings::capturefolder()); } QString extension; if (!m_recVideo->isChecked()) { extension = QStringLiteral("wav"); } else { extension = KdenliveSettings::v4l_extension(); } QString path = captureFolder.absoluteFilePath("capture0000." + extension); int i = 1; while (QFile::exists(path)) { QString num = QString::number(i).rightJustified(4, '0', false); path = captureFolder.absoluteFilePath("capture" + num + QLatin1Char('.') + extension); ++i; } QString v4lparameters = KdenliveSettings::v4l_parameters(); // TODO: when recording audio only, allow param configuration? if (!m_recVideo->isChecked()) { v4lparameters.clear(); } // Add alsa audio capture if (!m_recAudio->isChecked()) { // if we do not want audio, make sure that we don't have audio encoding parameters // this is required otherwise the MLT avformat consumer will not close properly if (v4lparameters.contains(QStringLiteral("acodec"))) { QString endParam = v4lparameters.section(QStringLiteral("acodec"), 1); int vcodec = endParam.indexOf(QStringLiteral(" vcodec")); int format = endParam.indexOf(QStringLiteral(" f=")); int cutPosition = -1; if (vcodec > -1) { if (format > -1) { cutPosition = qMin(vcodec, format); } else { cutPosition = vcodec; } } else if (format > -1) { cutPosition = format; } else { // nothing interesting in end params endParam.clear(); } if (cutPosition > -1) { endParam.remove(0, cutPosition); } v4lparameters = QString(v4lparameters.section(QStringLiteral("acodec"), 0, 0) + QStringLiteral("an=1 ") + endParam).simplified(); } } Mlt::Producer *prod = createV4lProducer(); if ((prod != nullptr) && prod->is_valid()) { m_monitor->startCapture(v4lparameters, path, prod); m_captureFile = QUrl::fromLocalFile(path); } else { m_recAction->blockSignals(true); m_recAction->setChecked(false); m_recAction->blockSignals(false); emit warningMessage(i18n("Capture crashed, please check your parameters")); } } else { m_monitor->stopCapture(); emit addClipToProject(m_captureFile); } return; } if (!record) { if (!m_captureProcess) { return; } m_captureProcess->terminate(); QTimer::singleShot(1500, m_captureProcess, &QProcess::kill); return; } if (m_captureProcess) { return; } m_recError.clear(); QString extension = KdenliveSettings::grab_extension(); QDir captureFolder; if (KdenliveSettings::capturetoprojectfolder()) { captureFolder = QDir(m_monitor->projectFolder()); } else { captureFolder = QDir(KdenliveSettings::capturefolder()); } QFileInfo checkCaptureFolder(captureFolder.absolutePath()); if (!checkCaptureFolder.isWritable()) { emit warningMessage(i18n("The directory %1, could not be created.\nPlease make sure you have the required permissions.", captureFolder.absolutePath())); m_recAction->blockSignals(true); m_recAction->setChecked(false); m_recAction->blockSignals(false); return; } m_captureProcess = new QProcess; connect(m_captureProcess, &QProcess::stateChanged, this, &RecManager::slotProcessStatus); connect(m_captureProcess, &QProcess::readyReadStandardError, this, &RecManager::slotReadProcessInfo); QString path = captureFolder.absoluteFilePath("capture0000." + extension); int i = 1; while (QFile::exists(path)) { QString num = QString::number(i).rightJustified(4, '0', false); path = captureFolder.absoluteFilePath("capture" + num + QLatin1Char('.') + extension); ++i; } m_captureFile = QUrl::fromLocalFile(path); QString captureSize; int screen = -1; if (m_screenCombo) { // Multi monitor setup, capture monitor selected by user screen = m_screenCombo->currentIndex(); } QRect screenSize = QApplication::desktop()->screenGeometry(screen); QStringList captureArgs; captureArgs << QStringLiteral("-f") << QStringLiteral("x11grab"); if (KdenliveSettings::grab_follow_mouse()) { captureArgs << QStringLiteral("-follow_mouse") << QStringLiteral("centered"); } if (!KdenliveSettings::grab_hide_frame()) { captureArgs << QStringLiteral("-show_region") << QStringLiteral("1"); } captureSize = QStringLiteral(":0.0"); if (KdenliveSettings::grab_capture_type() == 0) { // Full screen capture captureArgs << QStringLiteral("-s") << QString::number(screenSize.width()) + QLatin1Char('x') + QString::number(screenSize.height()); captureSize.append(QLatin1Char('+') + QString::number(screenSize.left()) + QLatin1Char('.') + QString::number(screenSize.top())); } else { // Region capture captureArgs << QStringLiteral("-s") << QString::number(KdenliveSettings::grab_width()) + QLatin1Char('x') + QString::number(KdenliveSettings::grab_height()); captureSize.append(QLatin1Char('+') + QString::number(KdenliveSettings::grab_offsetx()) + QLatin1Char(',') + QString::number(KdenliveSettings::grab_offsety())); } // fps captureArgs << QStringLiteral("-r") << QString::number(KdenliveSettings::grab_fps()); if (KdenliveSettings::grab_hide_mouse()) { captureSize.append(QStringLiteral("+nomouse")); } captureArgs << QStringLiteral("-i") << captureSize; if (!KdenliveSettings::grab_parameters().simplified().isEmpty()) { captureArgs << KdenliveSettings::grab_parameters().simplified().split(QLatin1Char(' ')); } captureArgs << path; m_captureProcess->start(KdenliveSettings::ffmpegpath(), captureArgs); if (!m_captureProcess->waitForStarted()) { // Problem launching capture app emit warningMessage(i18n("Failed to start the capture application:\n%1", KdenliveSettings::ffmpegpath())); // delete m_captureProcess; } } void RecManager::slotProcessStatus(QProcess::ProcessState status) { if (status == QProcess::NotRunning) { m_recAction->setEnabled(true); m_recAction->setChecked(false); m_device_selector->setEnabled(true); if (m_captureProcess) { if (m_captureProcess->exitStatus() == QProcess::CrashExit) { emit warningMessage(i18n("Capture crashed, please check your parameters"), -1, QList() << m_showLogAction); } else { if (true) { int code = m_captureProcess->exitCode(); if (code != 0 && code != 255) { emit warningMessage(i18n("Capture crashed, please check your parameters"), -1, QList() << m_showLogAction); } else { // Capture successful, add clip to project emit addClipToProject(m_captureFile); } } } } delete m_captureProcess; m_captureProcess = nullptr; } } void RecManager::slotReadProcessInfo() { QString data = m_captureProcess->readAllStandardError().simplified(); m_recError.append(data + QLatin1Char('\n')); } void RecManager::slotVideoDeviceChanged(int) { int currentItem = m_device_selector->currentData().toInt(); KdenliveSettings::setDefaultcapture(currentItem); switch (currentItem) { case Video4Linux: m_playAction->setEnabled(true); break; case BlackMagic: m_playAction->setEnabled(false); break; default: m_playAction->setEnabled(false); break; } /* m_previewSettings->setEnabled(ix == Video4Linux || ix == BlackMagic); control_frame->setVisible(ix == Video4Linux); monitor_box->setVisible(ix == ScreenBag && monitor_box->count() > 0); m_playAction->setVisible(ix != ScreenBag); m_fwdAction->setVisible(ix == Firewire); m_discAction->setVisible(ix == Firewire); m_rewAction->setVisible(ix == Firewire); m_recAction->setEnabled(ix != Firewire); m_logger.setVisible(ix == BlackMagic); if (m_captureDevice) { // MLT capture still running, abort m_monitorManager->clearScopeSource(); m_captureDevice->stop(); delete m_captureDevice; m_captureDevice = nullptr; } // The m_videoBox container has to be shown once before the MLT consumer is build, or preview will fail switch (ix) { case ScreenBag: } */ } Mlt::Producer *RecManager::createV4lProducer() { QString profilePath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/profiles/video4linux"); Mlt::Profile *vidProfile = new Mlt::Profile(profilePath.toUtf8().constData()); Mlt::Producer *prod = nullptr; if (m_recVideo->isChecked()) { prod = new Mlt::Producer(*vidProfile, QStringLiteral("video4linux2:%1").arg(KdenliveSettings::video4vdevice()).toUtf8().constData()); if ((prod == nullptr) || !prod->is_valid()) { return nullptr; } prod->set("width", vidProfile->width()); prod->set("height", vidProfile->height()); prod->set("framerate", vidProfile->fps()); /*p->set("standard", ui->v4lStandardCombo->currentText().toLatin1().constData()); p->set("channel", ui->v4lChannelSpinBox->value()); p->set("audio_ix", ui->v4lAudioComboBox->currentIndex());*/ prod->set("force_seekable", 0); } if (m_recAudio->isChecked() && (prod != nullptr) && prod->is_valid()) { // Add audio track Mlt::Producer *audio = new Mlt::Producer( *vidProfile, QStringLiteral("alsa:%1?channels=%2").arg(KdenliveSettings::v4l_alsadevicename()).arg(KdenliveSettings::alsachannels()).toUtf8().constData()); audio->set("mlt_service", "avformat-novalidate"); audio->set("audio_index", 0); audio->set("video_index", -1); auto *tractor = new Mlt::Tractor(*vidProfile); tractor->set_track(*prod, 0); delete prod; tractor->set_track(*audio, 1); delete audio; prod = new Mlt::Producer(tractor->get_producer()); delete tractor; } return prod; } void RecManager::slotPreview(bool preview) { if (m_device_selector->currentData().toInt() == Video4Linux) { if (preview) { std::shared_ptr prod(createV4lProducer()); if (prod && prod->is_valid()) { m_monitor->updateClipProducer(prod); } else { emit warningMessage(i18n("Capture crashed, please check your parameters")); } } else { m_monitor->slotOpenClip(nullptr); } } /* buildMltDevice(path); bool isXml; producer = getV4lXmlPlaylist(profile, &isXml); //producer = QString("avformat-novalidate:video4linux2:%1?width:%2&height:%3&frame_rate:%4").arg(KdenliveSettings::video4vdevice()).arg(profile.width).arg(profile.height).arg((double) profile.frame_rate_num / profile.frame_rate_den); if (!m_captureDevice->slotStartPreview(producer, isXml)) { // v4l capture failed to start video_frame->setText(i18n("Failed to start Video4Linux,\ncheck your parameters...")); } else { m_playAction->setEnabled(false); m_stopAction->setEnabled(true); m_isPlaying = true; } }*/ } void RecManager::slotShowLog() { KMessageBox::information(QApplication::activeWindow(), m_recError); } diff --git a/src/monitor/recmanager.h b/src/monitor/recmanager.h index 065388496..995fd503b 100644 --- a/src/monitor/recmanager.h +++ b/src/monitor/recmanager.h @@ -1,98 +1,98 @@ /*************************************************************************** * Copyright (C) 2015 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 . * ***************************************************************************/ /*! * @class RecManager * @brief All recording specific features are gathered here * @author Jean-Baptiste Mardelle */ #ifndef RECMANAGER_H #define RECMANAGER_H #include "definitions.h" #include #include class Monitor; class QAction; class QToolBar; class QComboBox; class QCheckBox; namespace Mlt { class Producer; } class RecManager : public QObject { Q_OBJECT enum CaptureDevice { Video4Linux = 0, ScreenGrab = 1, // Not implemented Firewire = 2, BlackMagic = 3 }; public: explicit RecManager(Monitor *parent = nullptr); - ~RecManager(); + ~RecManager() override; QToolBar *toolbar() const; void stopCapture(); QAction *switchAction() const; /** @brief: stop capture and hide rec panel **/ void stop(); private: Monitor *m_monitor; QAction *m_switchRec; QString m_captureFolder; QUrl m_captureFile; QString m_recError; - QProcess *m_captureProcess; + QProcess *m_captureProcess{nullptr}; QAction *m_recAction; QAction *m_playAction; QAction *m_showLogAction; QToolBar *m_recToolbar; - QComboBox *m_screenCombo; + QComboBox *m_screenCombo{nullptr}; QComboBox *m_device_selector; QCheckBox *m_recVideo; QCheckBox *m_recAudio; Mlt::Producer *createV4lProducer(); private slots: void slotRecord(bool record); void slotPreview(bool record); void slotProcessStatus(QProcess::ProcessState status); void slotReadProcessInfo(); void showRecConfig(); void slotVideoDeviceChanged(int ix = -1); void slotShowLog(); signals: void addClipToProject(const QUrl &); void warningMessage(const QString &, int timeout = 5000, const QList &actions = QList()); }; #endif diff --git a/src/monitor/scopes/audiographspectrum.cpp b/src/monitor/scopes/audiographspectrum.cpp index 14743350c..451c1b877 100644 --- a/src/monitor/scopes/audiographspectrum.cpp +++ b/src/monitor/scopes/audiographspectrum.cpp @@ -1,404 +1,404 @@ /*************************************************************************** * 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 "audiographspectrum.h" #include "../monitormanager.h" #include "kdenlivesettings.h" #include #include #include #include #include #include #include #include -#include +#include #include // Code borrowed from Shotcut's audiospectum by Brian Matherly (GPL) static const int WINDOW_SIZE = 8000; // 6 Hz FFT bins at 48kHz struct band { float low; // Low frequency float center; // Center frequency float high; // High frequency const char *label; }; // Preferred frequencies from ISO R 266-1997 / ANSI S1.6-1984 static const band BAND_TAB[] = { // Low Preferred High Band // Freq Center Freq Label Num {1.12, 1.25, 1.41, "1.25"}, // 1 {1.41, 1.60, 1.78, "1.6"}, // 2 {1.78, 2.00, 2.24, "2.0"}, // 3 {2.24, 2.50, 2.82, "2.5"}, // 4 {2.82, 3.15, 3.55, "3.15"}, // 5 {3.55, 4.00, 4.44, "4.0"}, // 6 {4.44, 5.00, 6.00, "5.0"}, // 7 {6.00, 6.30, 7.00, "6.3"}, // 8 {7.00, 8.00, 9.00, "8.0"}, // 9 {9.00, 10.00, 11.00, "10"}, // 10 {11.00, 12.50, 14.00, "12.5"}, // 11 {14.00, 16.00, 18.00, "16"}, // 12 {18.00, 20.00, 22.00, "20"}, // 13 - First in audible range {22.00, 25.00, 28.00, "25"}, // 14 {28.00, 31.50, 35.00, "31"}, // 15 {35.00, 40.00, 45.00, "40"}, // 16 {45.00, 50.00, 56.00, "50"}, // 17 {56.00, 63.00, 71.00, "63"}, // 18 {71.00, 80.00, 90.00, "80"}, // 19 {90.00, 100.00, 112.00, "100"}, // 20 {112.00, 125.00, 140.00, "125"}, // 21 {140.00, 160.00, 179.00, "160"}, // 22 {179.00, 200.00, 224.00, "200"}, // 23 {224.00, 250.00, 282.00, "250"}, // 24 {282.00, 315.00, 353.00, "315"}, // 25 {353.00, 400.00, 484.00, "400"}, // 26 {484.00, 500.00, 560.00, "500"}, // 27 {560.00, 630.00, 706.00, "630"}, // 28 {706.00, 800.00, 897.00, "800"}, // 29 {897.00, 1000.00, 1121.00, "1k"}, // 30 {1121.00, 1250.00, 1401.00, "1.3k"}, // 31 {1401.00, 1600.00, 1794.00, "1.6k"}, // 32 {1794.00, 2000.00, 2242.00, "2k"}, // 33 {2242.00, 2500.00, 2803.00, "2.5k"}, // 34 {2803.00, 3150.00, 3531.00, "3.2k"}, // 35 {3531.00, 4000.00, 4484.00, "4k"}, // 36 {4484.00, 5000.00, 5605.00, "5k"}, // 37 {5605.00, 6300.00, 7062.00, "6.3k"}, // 38 {7062.00, 8000.00, 8908.00, "8k"}, // 39 {8908.00, 10000.00, 11210.00, "10k"}, // 40 {11210.00, 12500.00, 14012.00, "13k"}, // 41 {14012.00, 16000.00, 17936.00, "16k"}, // 42 {17936.00, 20000.00, 22421.00, "20k"}, // 43 - Last in audible range }; static const int FIRST_AUDIBLE_BAND_INDEX = 12; static const int LAST_AUDIBLE_BAND_INDEX = 42; static const int AUDIBLE_BAND_COUNT = LAST_AUDIBLE_BAND_INDEX - FIRST_AUDIBLE_BAND_INDEX + 1; const double log_factor = 1.0 / log10(1.0 / 127); static inline double levelToDB(double dB) { if (dB <= 0) { return 0; } return (1.0 - log10(dB) * log_factor); } /*EqualizerWidget::EqualizerWidget(QWidget *parent) : QWidget(parent) { QGridLayout *box = new QGridLayout(this); QStringList labels; labels << i18n("Master") << "50Hz" << "100Hz"<<"156Hz"<<"220Hz"<<"311Hz"<<"440Hz"<<"622Hz"<<"880Hz"<<"1.25kHz"<<"1.75kHz"<<"2.5kHz"<<"3.5kHz"<<"5kHz"<<"10kHz"<<"20kHz"; for (int i = 0; i < 16; i++) { QSlider *sl = new QSlider(Qt::Vertical, this); sl->setObjectName(QString::number(i)); box->addWidget(sl, 0, i); QLabel *lab = new QLabel(labels.at(i), this); box->addWidget(lab, 1, i); } setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); }*/ AudioGraphWidget::AudioGraphWidget(QWidget *parent) : QWidget(parent) { m_dbLabels << -45 << -30 << -20 << -15 << -10 << -5 << -2 << 0; for (int i = FIRST_AUDIBLE_BAND_INDEX; i <= LAST_AUDIBLE_BAND_INDEX; i++) { m_freqLabels << BAND_TAB[i].label; } m_maxDb = 0; setMinimumWidth(2 * m_freqLabels.size() + fontMetrics().width(QStringLiteral("888")) + 2); setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)); setMinimumHeight(100); setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); } void AudioGraphWidget::showAudio(const QVector &bands) { m_levels = bands; update(); } void AudioGraphWidget::drawDbLabels(QPainter &p, const QRect &rect) { int dbLabelCount = m_dbLabels.size(); int textHeight = fontMetrics().ascent(); if (dbLabelCount == 0) { return; } int maxWidth = fontMetrics().width(QStringLiteral("-45")); // dB scale is vertical along the left side int prevY = height(); QColor textCol = palette().text().color(); p.setPen(textCol); for (int i = 0; i < dbLabelCount; i++) { QString label = QString::number(m_dbLabels.at(i)); int x = rect.left() + maxWidth - fontMetrics().width(label); int yline = rect.bottom() - pow(10.0, (double)m_dbLabels.at(i) / 50.0) * rect.height() * 40.0 / 42; int y = yline + textHeight / 2; if (y - textHeight < 0) { y = textHeight; } if (prevY - y >= 2) { p.drawText(x, y, label); p.drawLine(rect.left() + maxWidth + 2, yline, rect.width(), yline); prevY = y - textHeight; } } } void AudioGraphWidget::drawChanLabels(QPainter &p, const QRect &rect, int barWidth) { int chanLabelCount = m_freqLabels.size(); int stride = 1; if (chanLabelCount == 0) { return; } p.setPen(palette().text().color().rgb()); // Channel labels are horizontal along the bottom. // Find the widest channel label int chanLabelWidth = 0; for (int i = 0; i < chanLabelCount; i++) { int width = fontMetrics().width(m_freqLabels.at(i)) + 2; chanLabelWidth = width > chanLabelWidth ? width : chanLabelWidth; } int length = rect.width(); while (chanLabelWidth * chanLabelCount / stride > length) { stride++; } int prevX = 0; int y = rect.bottom(); for (int i = 0; i < chanLabelCount; i += stride) { QString label = m_freqLabels.at(i); int x = rect.left() + (2 * i) + i * barWidth + barWidth / 2 - fontMetrics().width(label) / 2; if (x > prevX) { p.drawText(x, y, label); prevX = x + fontMetrics().width(label); } } } void AudioGraphWidget::resizeEvent(QResizeEvent *event) { drawBackground(); QWidget::resizeEvent(event); } void AudioGraphWidget::drawBackground() { QSize s = size(); if (!s.isValid()) { return; } m_pixmap = QPixmap(s); if (m_pixmap.isNull()) { return; } m_pixmap.fill(palette().base().color()); QPainter p(&m_pixmap); QRect rect(0, 0, width() - 3, height()); p.setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)); p.setOpacity(0.6); int offset = fontMetrics().width(QStringLiteral("888")) + 2; if (rect.width() - offset > 10) { drawDbLabels(p, rect); rect.adjust(offset, 0, 0, 0); } int barWidth = (rect.width() - (2 * (AUDIBLE_BAND_COUNT - 1))) / AUDIBLE_BAND_COUNT; drawChanLabels(p, rect, barWidth); rect.adjust(0, 0, 0, -fontMetrics().height()); m_rect = rect; } void AudioGraphWidget::paintEvent(QPaintEvent *pe) { QPainter p(this); p.setClipRect(pe->rect()); p.drawPixmap(0, 0, m_pixmap); if (m_levels.isEmpty()) { return; } int chanCount = m_levels.size(); int height = m_rect.height(); double barWidth = (m_rect.width() - (2.0 * (AUDIBLE_BAND_COUNT - 1))) / AUDIBLE_BAND_COUNT; p.setOpacity(0.6); QRectF rect(m_rect.left(), 0, barWidth, height); for (int i = 0; i < chanCount; i++) { double level = (0.5 + m_levels.at(i)) / 1.5 * height; if (level < 0) { continue; } rect.moveLeft(m_rect.left() + i * barWidth + (2 * i)); rect.setHeight(level); rect.moveBottom(height); p.fillRect(rect, Qt::darkGreen); } } AudioGraphSpectrum::AudioGraphSpectrum(MonitorManager *manager, QWidget *parent) : ScopeWidget(parent) , m_manager(manager) { auto *lay = new QVBoxLayout(this); m_graphWidget = new AudioGraphWidget(this); lay->addWidget(m_graphWidget); /*m_equalizer = new EqualizerWidget(this); lay->addWidget(m_equalizer); lay->setStretchFactor(m_graphWidget, 5); lay->setStretchFactor(m_equalizer, 3);*/ m_filter = new Mlt::Filter(*(m_manager->projectMonitor()->profile()), "fft"); if (!m_filter->is_valid()) { KdenliveSettings::setEnableaudiospectrum(false); auto *mw = new KMessageWidget(this); mw->setCloseButtonVisible(false); mw->setWordWrap(true); mw->setMessageType(KMessageWidget::Information); mw->setText(i18n("MLT must be compiled with libfftw3 to enable Audio Spectrum")); layout()->addWidget(mw); mw->show(); setEnabled(false); return; } m_filter->set("window_size", WINDOW_SIZE); QAction *a = new QAction(i18n("Enable Audio Spectrum"), this); a->setCheckable(true); a->setChecked(KdenliveSettings::enableaudiospectrum()); if (KdenliveSettings::enableaudiospectrum()) { connect(m_manager, &MonitorManager::frameDisplayed, this, &ScopeWidget::onNewFrame, Qt::UniqueConnection); } connect(a, &QAction::triggered, this, &AudioGraphSpectrum::activate); addAction(a); setContextMenuPolicy(Qt::ActionsContextMenu); } AudioGraphSpectrum::~AudioGraphSpectrum() { delete m_graphWidget; delete m_filter; } void AudioGraphSpectrum::activate(bool enable) { if (enable) { connect(m_manager, &MonitorManager::frameDisplayed, this, &ScopeWidget::onNewFrame, Qt::UniqueConnection); } else { disconnect(m_manager, &MonitorManager::frameDisplayed, this, &ScopeWidget::onNewFrame); } KdenliveSettings::setEnableaudiospectrum(enable); } void AudioGraphSpectrum::refreshPixmap() { if (m_graphWidget) { m_graphWidget->drawBackground(); } } void AudioGraphSpectrum::refreshScope(const QSize & /*size*/, bool /*full*/) { SharedFrame sFrame; while (m_queue.count() > 0) { sFrame = m_queue.pop(); if (sFrame.is_valid() && sFrame.get_audio_samples() > 0) { mlt_audio_format format = mlt_audio_s16; int channels = sFrame.get_audio_channels(); int frequency = sFrame.get_audio_frequency(); int samples = sFrame.get_audio_samples(); Mlt::Frame mFrame = sFrame.clone(true, false, false); m_filter->process(mFrame); mFrame.get_audio(format, frequency, channels, samples); if (samples == 0 || format == 0) { // There was an error processing audio from frame continue; } processSpectrum(); } } } void AudioGraphSpectrum::processSpectrum() { QVector bands(AUDIBLE_BAND_COUNT); - float *bins = (float *)m_filter->get_data("bins"); + auto *bins = (float *)m_filter->get_data("bins"); int bin_count = m_filter->get_int("bin_count"); double bin_width = m_filter->get_double("bin_width"); int band = 0; bool firstBandFound = false; for (int bin = 0; bin < bin_count; bin++) { // Loop through all the FFT bins and align bin frequencies with // band frequencies. double F = bin_width * (double)bin; if (!firstBandFound) { // Skip bins that come before the first band. if (BAND_TAB[band + FIRST_AUDIBLE_BAND_INDEX].low > F) { continue; } else { firstBandFound = true; bands[band] = bins[bin]; } } else if (BAND_TAB[band + FIRST_AUDIBLE_BAND_INDEX].high < F) { // This bin is outside of this band - move to the next band. band++; if ((band + FIRST_AUDIBLE_BAND_INDEX) > LAST_AUDIBLE_BAND_INDEX) { // Skip bins that come after the last band. break; } bands[band] = bins[bin]; } else if (bands[band] < bins[bin]) { // Pick the highest bin level within this band to represent the // whole band. bands[band] = bins[bin]; } } // At this point, bands contains the magnitude of the signal for each // band. Convert to dB. for (band = 0; band < bands.size(); band++) { double mag = bands[band]; double dB = mag > 0.0 ? levelToDB(mag) : -100.0; bands[band] = dB; } // Update the audio signal widget QMetaObject::invokeMethod(m_graphWidget, "showAudio", Qt::QueuedConnection, Q_ARG(const QVector &, bands)); } diff --git a/src/monitor/scopes/audiographspectrum.h b/src/monitor/scopes/audiographspectrum.h index 2b9f6f4f1..23003a38d 100644 --- a/src/monitor/scopes/audiographspectrum.h +++ b/src/monitor/scopes/audiographspectrum.h @@ -1,99 +1,99 @@ /*************************************************************************** * 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 . * ***************************************************************************/ /*! * @class AudioGraphSpectrum * @brief An audio spectrum * @author Jean-Baptiste Mardelle */ #ifndef AUDIOGRAPHSPECTRUM_H #define AUDIOGRAPHSPECTRUM_H #include "scopewidget.h" #include "sharedframe.h" #include #include #include namespace Mlt { class Filter; } class MonitorManager; /*class EqualizerWidget : public QWidget { Q_OBJECT public: EqualizerWidget(QWidget *parent = nullptr); };*/ class AudioGraphWidget : public QWidget { Q_OBJECT public: explicit AudioGraphWidget(QWidget *parent = nullptr); void drawBackground(); public slots: void showAudio(const QVector &bands); protected: void paintEvent(QPaintEvent *pe) override; void resizeEvent(QResizeEvent *event) override; private: QVector m_levels; QVector m_dbLabels; QStringList m_freqLabels; QPixmap m_pixmap; QRect m_rect; int m_maxDb; void drawDbLabels(QPainter &p, const QRect &rect); void drawChanLabels(QPainter &p, const QRect &rect, int barWidth); }; class AudioGraphSpectrum : public ScopeWidget { Q_OBJECT public: AudioGraphSpectrum(MonitorManager *manager, QWidget *parent = nullptr); - virtual ~AudioGraphSpectrum(); + ~AudioGraphSpectrum() override; private: MonitorManager *m_manager; Mlt::Filter *m_filter; AudioGraphWidget *m_graphWidget; // EqualizerWidget *m_equalizer; void processSpectrum(); void refreshScope(const QSize &size, bool full) override; public slots: void refreshPixmap(); private slots: void activate(bool enable); }; #endif diff --git a/src/monitor/scopes/dataqueue.h b/src/monitor/scopes/dataqueue.h index c73add9c9..ba72a8109 100644 --- a/src/monitor/scopes/dataqueue.h +++ b/src/monitor/scopes/dataqueue.h @@ -1,155 +1,155 @@ /* * Copyright (c) 2015 Meltytech, LLC * Author: Brian Matherly * * 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 DATAQUEUE_H #define DATAQUEUE_H #include #include #include #include /*! \class DataQueue \brief The DataQueue provides a thread safe container for passing data between objects. \threadsafe DataQueue provides a limited size container for passing data between objects. One object can add data to the queue by calling push() while another object can remove items from the queue by calling pop(). DataQueue provides configurable behavior for handling overflows. It can discard the oldest, discard the newest or block the object calling push() until room has been freed in the queue by another object calling pop(). DataQueue is threadsafe and is therefore most appropriate when passing data between objects operating in different thread contexts. */ template class DataQueue { public: //! Overflow behavior modes. typedef enum { OverflowModeDiscardOldest = 0, //!< Discard oldest items OverflowModeDiscardNewest, //!< Discard newest items OverflowModeWait //!< Wait for space to be free } OverflowMode; /*! Constructs a DataQueue. The \a size will be the maximum queue size and the \a mode will dictate overflow behavior. */ explicit DataQueue(int maxSize, OverflowMode mode); //! Destructs a DataQueue. virtual ~DataQueue(); /*! Pushes an item into the queue. If the queue is full and overflow mode is OverflowModeWait then this function will block until pop() is called. */ void push(const T &item); /*! Pops an item from the queue. If the queue is empty then this function will block. If blocking is undesired, then check the return of count() before calling pop(). */ T pop(); //! Returns the number of items in the queue. int count() const; private: QList m_queue; int m_maxSize; OverflowMode m_mode; mutable QMutex m_mutex; QWaitCondition m_notEmptyCondition; QWaitCondition m_notFullCondition; }; template DataQueue::DataQueue(int maxSize, OverflowMode mode) : m_queue() , m_maxSize(maxSize) , m_mode(mode) , m_mutex(QMutex::NonRecursive) , m_notEmptyCondition() , m_notFullCondition() { } -template DataQueue::~DataQueue() {} +template DataQueue::~DataQueue() = default; template void DataQueue::push(const T &item) { m_mutex.lock(); if (m_queue.size() == m_maxSize) { switch (m_mode) { case OverflowModeDiscardOldest: m_queue.removeFirst(); m_queue.append(item); break; case OverflowModeDiscardNewest: // This item is the newest so discard it and exit break; case OverflowModeWait: m_notFullCondition.wait(&m_mutex); m_queue.append(item); break; } } else { m_queue.append(item); if (m_queue.size() == 1) { m_notEmptyCondition.wakeOne(); } } m_mutex.unlock(); } template T DataQueue::pop() { T retVal; m_mutex.lock(); if (m_queue.size() == 0) { m_notEmptyCondition.wait(&m_mutex); } retVal = m_queue.takeFirst(); if (m_mode == OverflowModeWait && m_queue.size() == m_maxSize - 1) { m_notFullCondition.wakeOne(); } m_mutex.unlock(); return retVal; } template int DataQueue::count() const { QMutexLocker locker(&m_mutex); return m_queue.size(); } #endif // DATAQUEUE_H diff --git a/src/monitor/scopes/monitoraudiolevel.cpp b/src/monitor/scopes/monitoraudiolevel.cpp index 7a8cf70b5..31d623bb2 100644 --- a/src/monitor/scopes/monitoraudiolevel.cpp +++ b/src/monitor/scopes/monitoraudiolevel.cpp @@ -1,247 +1,247 @@ /* Copyright (C) 2016 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 "monitoraudiolevel.h" #include "mlt++/Mlt.h" -#include +#include #include #include #include const double log_factor = 1.0 / log10(1.0 / 127); static inline double levelToDB(double level) { if (level <= 0) { return -100; } return 100 * (1.0 - log10(level) * log_factor); } MonitorAudioLevel::MonitorAudioLevel(Mlt::Profile *profile, int height, QWidget *parent) : ScopeWidget(parent) , audioChannels(2) , m_height(height) , m_channelHeight(height / 2) , m_channelDistance(2) , m_channelFillHeight(m_channelHeight) { setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); m_filter = new Mlt::Filter(*profile, "audiolevel"); if (!m_filter->is_valid()) { isValid = false; return; } m_filter->set("iec_scale", 0); isValid = true; } MonitorAudioLevel::~MonitorAudioLevel() { delete m_filter; } void MonitorAudioLevel::refreshScope(const QSize & /*size*/, bool /*full*/) { SharedFrame sFrame; while (m_queue.count() > 0) { sFrame = m_queue.pop(); if (sFrame.is_valid() && sFrame.get_audio_samples() > 0) { mlt_audio_format format = mlt_audio_s16; int channels = sFrame.get_audio_channels(); int frequency = sFrame.get_audio_frequency(); int samples = sFrame.get_audio_samples(); Mlt::Frame mFrame = sFrame.clone(true, false, false); m_filter->process(mFrame); mFrame.get_audio(format, frequency, channels, samples); if (samples == 0) { // There was an error processing audio from frame continue; } QVector levels; for (int i = 0; i < audioChannels; i++) { QString s = QStringLiteral("meta.media.audio_level.%1").arg(i); levels << (int)levelToDB(mFrame.get_double(s.toLatin1().constData())); } QMetaObject::invokeMethod(this, "setAudioValues", Qt::QueuedConnection, Q_ARG(const QVector &, levels)); } } } void MonitorAudioLevel::resizeEvent(QResizeEvent *event) { drawBackground(m_peaks.size()); ScopeWidget::resizeEvent(event); } void MonitorAudioLevel::refreshPixmap() { drawBackground(m_peaks.size()); } void MonitorAudioLevel::drawBackground(int channels) { if (height() == 0) { return; } QSize newSize = QWidget::size(); if (!newSize.isValid()) { return; } QFont ft = font(); ft.setPixelSize(newSize.height() / 3); setFont(ft); int textHeight = fontMetrics().ascent(); newSize.setHeight(newSize.height() - textHeight); QLinearGradient gradient(0, 0, newSize.width(), 0); gradient.setColorAt(0.0, Qt::darkGreen); gradient.setColorAt(0.379, Qt::darkGreen); gradient.setColorAt(0.38, Qt::green); // -20db gradient.setColorAt(0.868, Qt::green); gradient.setColorAt(0.869, Qt::yellow); // -2db gradient.setColorAt(0.95, Qt::yellow); gradient.setColorAt(0.951, Qt::red); // 0db m_pixmap = QPixmap(QWidget::size()); if (m_pixmap.isNull()) { return; } m_pixmap.fill(Qt::transparent); int totalHeight; if (channels < 2) { m_channelHeight = newSize.height() / 2; totalHeight = m_channelHeight; } else { m_channelHeight = (newSize.height() - (channels - 1)) / channels; totalHeight = channels * m_channelHeight + (channels - 1); } QRect rect(0, 0, newSize.width(), totalHeight); QPainter p(&m_pixmap); p.setOpacity(0.4); p.setFont(ft); p.fillRect(rect, QBrush(gradient)); // Channel labels are horizontal along the bottom. QVector dbscale; dbscale << 0 << -2 << -5 << -10 << -15 << -20 << -30 << -45; int dbLabelCount = dbscale.size(); // dB scale is horizontal along the bottom int prevX = m_pixmap.width() * 2; int y = totalHeight + textHeight; for (int i = 0; i < dbLabelCount; i++) { int value = dbscale.at(i); QString label = QString().sprintf("%d", value); int labelWidth = fontMetrics().width(label); double xf = pow(10.0, (double)dbscale.at(i) / 50.0) * m_pixmap.width() * 40.0 / 42; if (xf + labelWidth / 2 > m_pixmap.width()) { xf = width() - labelWidth / 2; } if (prevX - (xf + labelWidth / 2) >= 2) { p.setPen(palette().dark().color()); p.drawLine(xf, 0, xf, totalHeight - 1); xf -= labelWidth / 2; p.setPen(palette().text().color().rgb()); p.drawText((int)xf, y, label); prevX = xf; } } p.setOpacity(1); p.setPen(palette().dark().color()); // Clear space between the 2 channels p.setCompositionMode(QPainter::CompositionMode_Source); if (m_channelHeight < 4) { // too many audio channels, simple line between channels m_channelDistance = 1; m_channelFillHeight = m_channelHeight; for (int i = 0; i < channels; i++) { p.drawLine(0, i * (m_channelHeight + m_channelDistance), rect.width() - 1, i * (m_channelHeight + m_channelDistance)); } } else { m_channelDistance = 2; m_channelFillHeight = m_channelHeight - 2; for (int i = 0; i < channels; i++) { p.drawRect(0, i * (m_channelHeight + m_channelDistance), rect.width() - 1, m_channelHeight - 1); if (i > 0) { p.fillRect(0, i * (m_channelHeight + m_channelDistance) - 2, rect.width(), 2, Qt::transparent); } } } p.end(); } // cppcheck-suppress unusedFunction void MonitorAudioLevel::setAudioValues(const QVector &values) { m_values = values; if (m_peaks.size() != m_values.size()) { m_peaks = values; drawBackground(values.size()); } else { for (int i = 0; i < m_values.size(); i++) { m_peaks[i]--; if (m_values.at(i) > m_peaks.at(i)) { m_peaks[i] = m_values.at(i); } } } update(); } void MonitorAudioLevel::setVisibility(bool enable) { if (enable) { setVisible(true); setFixedHeight(m_height); } else { // set height to 0 so the toolbar layout is not affected setFixedHeight(0); setVisible(false); } } void MonitorAudioLevel::paintEvent(QPaintEvent *pe) { if (!isVisible()) { return; } QPainter p(this); p.setClipRect(pe->rect()); QRect rect(0, 0, width(), height()); if (m_values.isEmpty()) { p.setOpacity(0.2); p.drawPixmap(rect, m_pixmap); return; } p.drawPixmap(rect, m_pixmap); p.setPen(palette().dark().color()); p.setOpacity(0.9); int width = m_channelDistance == 1 ? rect.width() : rect.width() - 1; for (int i = 0; i < m_values.count(); i++) { if (m_values.at(i) >= 100) { continue; } int val = (50 + m_values.at(i)) / 150.0 * rect.width(); p.fillRect(val, i * (m_channelHeight + m_channelDistance) + 1, width - val, m_channelFillHeight, palette().dark()); p.fillRect((50 + m_peaks.at(i)) / 150.0 * rect.width(), i * (m_channelHeight + m_channelDistance) + 1, 1, m_channelFillHeight, palette().text()); } } diff --git a/src/monitor/scopes/monitoraudiolevel.h b/src/monitor/scopes/monitoraudiolevel.h index e14132e5f..a8cb53e96 100644 --- a/src/monitor/scopes/monitoraudiolevel.h +++ b/src/monitor/scopes/monitoraudiolevel.h @@ -1,64 +1,64 @@ /* Copyright (C) 2016 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 MONITORAUDIOLEVEL_H #define MONITORAUDIOLEVEL_H #include "scopewidget.h" #include namespace Mlt { class Profile; class Filter; } // namespace Mlt class MonitorAudioLevel : public ScopeWidget { Q_OBJECT public: explicit MonitorAudioLevel(Mlt::Profile *profile, int height, QWidget *parent = nullptr); - virtual ~MonitorAudioLevel(); + ~MonitorAudioLevel() override; void refreshPixmap(); int audioChannels; bool isValid; void setVisibility(bool enable); protected: void paintEvent(QPaintEvent *) override; void resizeEvent(QResizeEvent *event) override; private: Mlt::Filter *m_filter; int m_height; QPixmap m_pixmap; QVector m_peaks; QVector m_values; int m_channelHeight; int m_channelDistance; int m_channelFillHeight; void drawBackground(int channels = 2); void refreshScope(const QSize &size, bool full) override; private slots: void setAudioValues(const QVector &values); }; #endif diff --git a/src/monitor/scopes/scopewidget.cpp b/src/monitor/scopes/scopewidget.cpp index 9d8661641..b8d4f9cbe 100644 --- a/src/monitor/scopes/scopewidget.cpp +++ b/src/monitor/scopes/scopewidget.cpp @@ -1,93 +1,91 @@ /* * Copyright (c) 2015 Meltytech, LLC * Author: Brian Matherly * * 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 "scopewidget.h" #include "kdenlive_debug.h" #include ScopeWidget::ScopeWidget(QWidget *parent) : QWidget(parent) , m_queue(3, DataQueue::OverflowModeDiscardOldest) , m_future() - , m_refreshPending(false) , m_mutex(QMutex::NonRecursive) - , m_forceRefresh(false) , m_size(0, 0) { // qCDebug(KDENLIVE_LOG) << "begin" << m_future.isFinished(); // qCDebug(KDENLIVE_LOG) << "end"; } ScopeWidget::~ScopeWidget() = default; void ScopeWidget::onNewFrame(const SharedFrame &frame) { m_queue.push(frame); requestRefresh(); } void ScopeWidget::requestRefresh() { if (m_future.isFinished()) { m_future = QtConcurrent::run(this, &ScopeWidget::refreshInThread); } else { m_refreshPending = true; } } void ScopeWidget::refreshInThread() { if (m_size.isEmpty()) { return; } m_mutex.lock(); QSize size = m_size; bool full = m_forceRefresh; m_forceRefresh = false; m_mutex.unlock(); m_refreshPending = false; refreshScope(size, full); // Tell the GUI thread that the refresh is complete. QMetaObject::invokeMethod(this, "onRefreshThreadComplete", Qt::QueuedConnection); } void ScopeWidget::onRefreshThreadComplete() { update(); if (m_refreshPending) { requestRefresh(); } } void ScopeWidget::resizeEvent(QResizeEvent *) { m_mutex.lock(); m_size = size(); m_mutex.unlock(); requestRefresh(); } void ScopeWidget::changeEvent(QEvent *) { m_mutex.lock(); m_forceRefresh = true; m_mutex.unlock(); requestRefresh(); } diff --git a/src/monitor/scopes/scopewidget.h b/src/monitor/scopes/scopewidget.h index 9879cf1d4..3b4595f73 100644 --- a/src/monitor/scopes/scopewidget.h +++ b/src/monitor/scopes/scopewidget.h @@ -1,128 +1,128 @@ /* * Copyright (c) 2015 Meltytech, LLC * Author: Brian Matherly * * 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 SCOPEWIDGET_H #define SCOPEWIDGET_H #include "dataqueue.h" #include "sharedframe.h" #include #include #include #include #include /*! \class ScopeWidget \brief The ScopeWidget provides a common interface for all scopes in Shotcut. ScopeWidget is a QWidget that provides some additional functionality that is common to all scopes. One common function is a queue that can receive and store frames until they can be processed by the scope. Another common function is the ability to trigger the "heavy lifting" to be done in a worker thread. Frames are received by the onNewFrame() slot. The ScopeWidget automatically places new frames in the DataQueue (m_queue). Subclasses shall implement the refreshScope() function and can check for new frames in m_queue. refreshScope() is run from a separate thread. Therefore, any members that are accessed by both the worker thread (refreshScope) and the GUI thread (paintEvent(), resizeEvent(), etc) must be protected by a mutex. After the refreshScope() function returns, the ScopeWidget will automatically request the GUI thread to update(). A well implemented ScopeWidget will be designed such that most of the CPU intensive work will be done in refreshScope() and the paintEvent() implementation will complete quicly to avoid hanging up the GUI thread. Subclasses shall also implement getTitle() so that the application can display an appropriate title for the scope. */ class ScopeWidget : public QWidget { Q_OBJECT public: /*! Constructs an ScopeWidget. The \a name will be set as the objectName and should be initialized by subclasses. */ explicit ScopeWidget(QWidget *parent = nullptr); //! Destructs a ScopeWidget. - virtual ~ScopeWidget(); + ~ScopeWidget() override; /*! Returns the title of the scope to be displayed by the application. This virtual function must be implemented by subclasses. */ // virtual QString getTitle() = 0; /*! Sets the preferred orientation on the scope. This virtual function may be reimplemented by subclasses. */ // virtual void setOrientation(Qt::Orientation) {}; public slots: //! Provides a new frame to the scope. Should be called by the application. void onNewFrame(const SharedFrame &frame); protected: /*! Triggers refreshScope() to be called in a new thread context. Typically requestRefresh would be called from the GUI thread (e.g. in resizeEvent()). onNewFrame() also calls requestRefresh(). */ void requestRefresh(); /*! Performs the main, CPU intensive, scope drawing in a new thread. refreshScope() Shall be implemented by subclasses. Care must be taken to protect any members that may be accessed concurrently by the refresh thread and the GUI thread. */ virtual void refreshScope(const QSize &size, bool full) = 0; /*! Stores frames received by onNewFrame(). Subclasses should check this queue for new frames in the refreshScope() implementation. */ DataQueue m_queue; void resizeEvent(QResizeEvent *) override; void changeEvent(QEvent *) override; private: Q_INVOKABLE void onRefreshThreadComplete(); void refreshInThread(); QFuture m_future; - bool m_refreshPending; + bool m_refreshPending{false}; // Members accessed in multiple threads (mutex protected). QMutex m_mutex; - bool m_forceRefresh; + bool m_forceRefresh{false}; QSize m_size; }; #endif // SCOPEWIDGET_H diff --git a/src/monitor/scopes/sharedframe.h b/src/monitor/scopes/sharedframe.h index 3bea10e9c..a49df00b2 100644 --- a/src/monitor/scopes/sharedframe.h +++ b/src/monitor/scopes/sharedframe.h @@ -1,77 +1,77 @@ /* * Copyright (c) 2015 Meltytech, LLC * Author: Brian Matherly * * 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 SHAREDFRAME_H #define SHAREDFRAME_H #include +#include #include -#include class FrameData; /*! \class SharedFrame \brief The SharedFrame provides thread safe access to Mlt::Frame data. \threadsafe SharedFrame is a wrapper around Mlt::Frame that provides read-only access to the frame data. SharedFrame is a reference counted object having only const functions. Therefore, it is suitable for concurrent access. A SharedFrame can be safely copied. However, all copies will be accessing the same wrapped Mlt::Frame. Therefore, SharedFrame can not provide non-const access to any of the frame data. If it is necessary for an object to modify the frame data (e.g. to resize the image), then the object must call clone() to receive it's own non-const copy of the frame. TODO: Consider providing a similar class in Mlt++. */ class SharedFrame { public: SharedFrame(); explicit SharedFrame(Mlt::Frame &frame); SharedFrame(const SharedFrame &other); ~SharedFrame(); SharedFrame &operator=(const SharedFrame &other); bool is_valid() const; Mlt::Frame clone(bool audio = false, bool image = false, bool alpha = false) const; int get_int(const char *name) const; int64_t get_int64(const char *name) const; double get_double(const char *name) const; char *get(const char *name) const; int get_position() const; mlt_image_format get_image_format() const; int get_image_width() const; int get_image_height() const; const uint8_t *get_image() const; mlt_audio_format get_audio_format() const; int get_audio_channels() const; int get_audio_frequency() const; int get_audio_samples() const; const int16_t *get_audio() const; private: QExplicitlySharedDataPointer d; // NOLINT }; #endif // SHAREDFRAME_H diff --git a/src/profiles/profilemodel.cpp b/src/profiles/profilemodel.cpp index 097c1f213..6a940ea0d 100644 --- a/src/profiles/profilemodel.cpp +++ b/src/profiles/profilemodel.cpp @@ -1,283 +1,283 @@ /*************************************************************************** * 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 "profilemodel.hpp" #include "core.h" #include "kdenlive_debug.h" #include "kdenlivesettings.h" #include #include #include #include - +#include ProfileModel::ProfileModel(const QString &path) : m_path(path) , m_invalid(false) { if (!QFile::exists(path) && path.contains(QLatin1Char('/'))) { qCWarning(KDENLIVE_LOG) << "WARNING, COULD NOT FIND PROFILE " << path << ". We will default to DV_PAL profile"; m_invalid = true; } if (!path.contains(QLatin1Char('/'))) { QDir mltDir(KdenliveSettings::mltpath()); if (!mltDir.exists(path)) { qCWarning(KDENLIVE_LOG) << "WARNING, COULD NOT FIND MLT PROFILE " << path << ". We will default to DV_PAL profile"; m_invalid = true; } } - m_profile = std::unique_ptr(new Mlt::Profile(path.toStdString().c_str())); + m_profile = std::make_unique(path.toStdString().c_str()); m_description = QString(m_profile->description()); } bool ProfileModel::is_valid() const { return (!m_invalid) && m_profile->is_valid(); } QString ProfileModel::description() const { return m_profile->description(); } int ProfileModel::frame_rate_num() const { return m_profile->frame_rate_num(); } int ProfileModel::frame_rate_den() const { return m_profile->frame_rate_den(); } double ProfileModel::fps() const { return m_profile->fps(); } int ProfileModel::width() const { return m_profile->width(); } int ProfileModel::height() const { return m_profile->height(); } bool ProfileModel::progressive() const { return m_profile->progressive(); } int ProfileModel::sample_aspect_num() const { return m_profile->sample_aspect_num(); } int ProfileModel::sample_aspect_den() const { return m_profile->sample_aspect_den(); } double ProfileModel::sar() const { return m_profile->sar(); } int ProfileModel::display_aspect_num() const { return m_profile->display_aspect_num(); } int ProfileModel::display_aspect_den() const { return m_profile->display_aspect_den(); } double ProfileModel::dar() const { return m_profile->dar(); } int ProfileModel::is_explicit() const { return m_profile->is_explicit(); } int ProfileModel::colorspace() const { return m_profile->colorspace(); } QString ProfileModel::path() const { return m_path; } mlt_profile ProfileModel::get_profile() const { return m_profile->get_profile(); } void ProfileModel::set_explicit(int b) { m_profile->set_explicit(b); } ProfileParam::ProfileParam(QDomElement element) : m_description(element.attribute(QStringLiteral("description"))) , m_frame_rate_num(element.attribute(QStringLiteral("frame_rate_num")).toInt()) , m_frame_rate_den(element.attribute(QStringLiteral("frame_rate_den")).toInt()) , m_progressive((element.attribute(QStringLiteral("progressive")).toInt() != 0)) , m_sample_aspect_num(element.attribute(QStringLiteral("sample_aspect_num")).toInt()) , m_sample_aspect_den(element.attribute(QStringLiteral("sample_aspect_den")).toInt()) , m_display_aspect_num(element.attribute(QStringLiteral("display_aspect_num")).toInt()) , m_display_aspect_den(element.attribute(QStringLiteral("display_aspect_den")).toInt()) , m_colorspace(element.attribute(QStringLiteral("colorspace")).toInt()) { // Ensure profile has viable width / height int width = element.attribute(QStringLiteral("width")).toInt(); int height = element.attribute(QStringLiteral("height")).toInt(); if ((width % 8) + (height % 2) > 0) { pCore->displayBinMessage( i18n("The project profile is invalid (%1x%2), it was adjusted to %3x%4.", width, height, width + (width % 8), height + (height % 2)), KMessageWidget::Warning); if (width % 8 > 0) { width += 8 - width % 8; } height += height % 2; element.setAttribute(QStringLiteral("width"), width); element.setAttribute(QStringLiteral("height"), height); } m_width = width; m_height = height; m_fps = m_frame_rate_num / m_frame_rate_den; m_sar = m_sample_aspect_num / m_sample_aspect_den; m_dar = m_display_aspect_num / m_display_aspect_den; } ProfileParam::ProfileParam(ProfileInfo *p) : m_frame_rate_num(p->frame_rate_num()) , m_frame_rate_den(p->frame_rate_den()) , m_width(p->width()) , m_height(p->height()) , m_progressive(p->progressive()) , m_sample_aspect_num(p->sample_aspect_num()) , m_sample_aspect_den(p->sample_aspect_den()) , m_display_aspect_num(p->display_aspect_num()) , m_display_aspect_den(p->display_aspect_den()) , m_colorspace(p->colorspace()) , m_fps(p->fps()) , m_sar(p->sar()) , m_dar(p->dar()) { } ProfileParam::ProfileParam(Mlt::Profile *p) : m_frame_rate_num(p->frame_rate_num()) , m_frame_rate_den(p->frame_rate_den()) , m_width(p->width()) , m_height(p->height()) , m_progressive(p->progressive()) , m_sample_aspect_num(p->sample_aspect_num()) , m_sample_aspect_den(p->sample_aspect_den()) , m_display_aspect_num(p->display_aspect_num()) , m_display_aspect_den(p->display_aspect_den()) , m_colorspace(p->colorspace()) , m_fps(p->fps()) , m_sar(p->sar()) , m_dar(p->dar()) { } QString ProfileParam::path() const { return m_path; } QString ProfileParam::description() const { return m_description; } int ProfileParam::frame_rate_num() const { return m_frame_rate_num; } int ProfileParam::frame_rate_den() const { return m_frame_rate_den; } int ProfileParam::width() const { return m_width; } int ProfileParam::height() const { return m_height; } bool ProfileParam::progressive() const { return m_progressive; } int ProfileParam::sample_aspect_num() const { return m_sample_aspect_num; } int ProfileParam::sample_aspect_den() const { return m_sample_aspect_den; } int ProfileParam::display_aspect_num() const { return m_display_aspect_num; } int ProfileParam::display_aspect_den() const { return m_display_aspect_den; } int ProfileParam::colorspace() const { return m_colorspace; } double ProfileParam::fps() const { return m_fps; } double ProfileParam::dar() const { return m_dar; } double ProfileParam::sar() const { return m_sar; } void ProfileParam::adjustDimensions() { if (m_width % 8 > 0) { m_width += 8 - m_width % 8; } m_height += m_height % 2; } bool ProfileParam::is_valid() const { return (m_frame_rate_den > 0 && m_sample_aspect_den > 0 && m_display_aspect_den > 0 && m_width > 0); } diff --git a/src/profiles/profilemodel.hpp b/src/profiles/profilemodel.hpp index 806c2e57a..58766ce9a 100644 --- a/src/profiles/profilemodel.hpp +++ b/src/profiles/profilemodel.hpp @@ -1,129 +1,129 @@ /*************************************************************************** * 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 PROFILEMODEL_H #define PROFILEMODEL_H #include "profileinfo.hpp" #include #include #include #include /** @brief This is a wrapper around Mlt::Profile to be used by the rest of kdenlive. * It has implicit conversion to Mlt::Profile so you can use it directly in calls to Mlt backend. * */ class ProfileModel : public ProfileInfo { public: ProfileModel() = delete; /* @brief Constructs a profile using the path to the profile description */ ProfileModel(const QString &path); - virtual ~ProfileModel() = default; + ~ProfileModel() override = default; bool is_valid() const override; QString description() const override; int frame_rate_num() const override; int frame_rate_den() const override; double fps() const override; int width() const override; int height() const override; bool progressive() const override; int sample_aspect_num() const override; int sample_aspect_den() const override; double sar() const override; int display_aspect_num() const override; int display_aspect_den() const override; double dar() const override; int is_explicit() const; void set_explicit(int b); int colorspace() const override; mlt_profile get_profile() const; QString path() const override; void adjustDimensions() override{}; /* @brief get underlying profile. Use with caution*/ Mlt::Profile &profile() { return *m_profile.get(); }; protected: QString m_path; bool m_invalid; QString m_description; std::unique_ptr m_profile; }; /* @brief This class serves to describe the parameters of a profile */ class ProfileParam : public ProfileInfo { public: ProfileParam() = delete; ProfileParam(QDomElement element); ProfileParam(ProfileInfo *p); ProfileParam(Mlt::Profile *p); QString path() const override; QString description() const override; int frame_rate_num() const override; int frame_rate_den() const override; int width() const override; int height() const override; bool progressive() const override; int sample_aspect_num() const override; int sample_aspect_den() const override; int display_aspect_num() const override; int display_aspect_den() const override; int colorspace() const override; double fps() const override; double sar() const override; double dar() const override; // A profile's width should always be a multiple of 8 void adjustDimensions() override; bool is_valid() const override; QString m_path; QString m_description; int m_frame_rate_num; int m_frame_rate_den; int m_width; int m_height; bool m_progressive; int m_sample_aspect_num; int m_sample_aspect_den; int m_display_aspect_num; int m_display_aspect_den; int m_colorspace; double m_fps; double m_sar; double m_dar; }; #endif diff --git a/src/profiles/tree/profiletreemodel.cpp b/src/profiles/tree/profiletreemodel.cpp index 5657f1776..4d293bb91 100644 --- a/src/profiles/tree/profiletreemodel.cpp +++ b/src/profiles/tree/profiletreemodel.cpp @@ -1,160 +1,160 @@ /*************************************************************************** * 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 "profiletreemodel.hpp" #include "../profilemodel.hpp" #include "../profilerepository.hpp" #include "abstractmodel/treeitem.hpp" #include #include #include #include #include ProfileTreeModel::ProfileTreeModel(QObject *parent) : AbstractTreeModel(parent) { } std::shared_ptr ProfileTreeModel::construct(QObject *parent) { std::shared_ptr self(new ProfileTreeModel(parent)); QList rootData; rootData << "Description" << "Path" << "Height" << "Width" << "display_aspect_num" << "display_aspect_den" << "sample_aspect_ratio" << "fps" << "colorspace"; self->rootItem = TreeItem::construct(rootData, self, true); ProfileRepository::get()->refresh(); QVector> profiles = ProfileRepository::get()->getAllProfiles(); constexpr size_t nbCrit = 3; // number of criterion we check for profile classification // helper lambda that creates a profile category with the given name auto createCat = [&](const QString &name) { return self->rootItem->appendChild(QList{name}); }; // We define the filters as a vector of pairs. The first element correspond to the tree item holding matching profiles, and the array correspond to the // filter itself std::vector, std::array>> filters{ {createCat(i18n("5K (Wide 2160)")), {{5120, 2160, -1}}}, {createCat(i18n("4K UHD 2160")), {{3840, 2160, -1}}}, {createCat(i18n("4K DCI 2160")), {{4096, 2160, -1}}}, {createCat(i18n("2.5K QHD 1440")), {{-1, 1440, -1}}}, {createCat(i18n("Full HD 1080")), {{1920, 1080, -1}}}, {createCat(i18n("HD 720")), {{-1, 720, -1}}}, {createCat(i18n("SD/DVD")), {{720, QVariant::fromValue(QPair{480, 576}), 4}}}, {createCat(i18n("SD/DVD Widescreen")), {{720, QVariant::fromValue(QPair{480, 576}), 16}}}, }; auto customCat = createCat(i18n("Custom")); // We define lambdas that controls how a given field should be filtered std::array &)>, nbCrit> filtLambdas; filtLambdas[0] = [](QVariant width, std::unique_ptr &ptr) { return width == -1 || ptr->width() == width; }; filtLambdas[1] = [](QVariant height, std::unique_ptr &ptr) { if (height.canConvert()) { return height.toInt() == -1 || ptr->height() == height.toInt(); } QPair valid_values = height.value>(); return ptr->height() == valid_values.first || ptr->height() == valid_values.second; }; filtLambdas[2] = [](QVariant display_aspect_num, std::unique_ptr &ptr) { return display_aspect_num == -1 || ptr->display_aspect_num() == display_aspect_num; }; for (const auto &profile : profiles) { bool foundMatch = false; // we get a pointer to the profilemodel std::unique_ptr &ptr = ProfileRepository::get()->getProfile(profile.second); // we create the data list corresponding to this profile QList data; data << profile.first << profile.second << ptr->height() << ptr->width() << ptr->display_aspect_num() << ptr->display_aspect_den() << ptr->sar() << ptr->fps() << ProfileRepository::getColorspaceDescription(ptr->colorspace()); for (const auto &filter : filters) { bool matching = true; for (size_t i = 0; i < nbCrit && matching; ++i) { matching = filtLambdas[i](filter.second[i], ptr); } if (matching) { foundMatch = true; filter.first->appendChild(data); break; } } if (!foundMatch) { // no filter matched, we default to custom customCat->appendChild(data); } } return self; } QVariant ProfileTreeModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) { return QVariant(); } auto item = getItemById((int)index.internalId()); if (role == Qt::DecorationRole) { if (item->depth() == 1) { return QIcon::fromTheme(QStringLiteral("folder")); } return QIcon::fromTheme(QStringLiteral("file")); } if (role != Qt::DisplayRole) { return QVariant(); } return item->dataColumn(index.column()); } QString ProfileTreeModel::getProfile(const QModelIndex &index) { if (index.isValid()) { auto item = getItemById((int)index.internalId()); if (item->depth() == 2) { return item->dataColumn(1).toString(); } } return QString(); } QModelIndex ProfileTreeModel::findProfile(const QString &profile) { // we iterate over categories for (int i = 0; i < rootItem->childCount(); ++i) { // we iterate over profiles of the category std::shared_ptr category = rootItem->child(i); for (int j = 0; j < category->childCount(); ++j) { // we retrieve profile path std::shared_ptr child = category->child(j); QString path = child->dataColumn(1).toString(); if (path == profile) { return createIndex(j, 0, quintptr(child->getId())); } } } - return QModelIndex(); + return {}; } diff --git a/src/project/clipstabilize.cpp b/src/project/clipstabilize.cpp index 2ec8ea4c1..f37c348c8 100644 --- a/src/project/clipstabilize.cpp +++ b/src/project/clipstabilize.cpp @@ -1,235 +1,234 @@ /*************************************************************************** * Copyright (C) 2008 by Jean-Baptiste Mardelle (jb@kdenlive.org) * * Copyright (C) 2011 by Marco Gittler (marco@gitma.de) * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * 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 "clipstabilize.h" #include "bin/projectclip.h" #include "bin/projectitemmodel.h" #include "core.h" #include "widgets/doublewidget.h" #include "widgets/positionwidget.h" #include "kdenlivesettings.h" #include #include -#include #include -ClipStabilize::ClipStabilize(const std::vector &binIds, const QString &filterName, int out, QWidget *parent) +ClipStabilize::ClipStabilize(const std::vector &binIds, QString filterName, int out, QWidget *parent) : QDialog(parent) - , m_filtername(filterName) + , m_filtername(std::move(filterName)) , m_binIds(binIds) , m_vbox(nullptr) { setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)); setupUi(this); setWindowTitle(i18n("Stabilize Clip")); auto_add->setText(i18np("Add clip to project", "Add clips to project", m_binIds.size())); auto_add->setChecked(KdenliveSettings::add_new_clip()); // QString stylesheet = EffectStackView2::getStyleSheet(); // setStyleSheet(stylesheet); Q_ASSERT(binIds.size() > 0); auto firstBinClip = pCore->projectItemModel()->getClipByBinID(m_binIds.front()); auto firstUrl = firstBinClip->url(); if (m_binIds.size() == 1) { QString newFile = firstUrl; newFile.append(QStringLiteral(".mlt")); dest_url->setMode(KFile::File); dest_url->setUrl(QUrl(newFile)); } else { label_dest->setText(i18n("Destination folder")); dest_url->setMode(KFile::Directory | KFile::ExistingOnly); dest_url->setUrl(QUrl(firstUrl).adjusted(QUrl::RemoveFilename)); } if (m_filtername == QLatin1String("vidstab") || m_filtername == QLatin1String("videostab2")) { m_fixedParams[QStringLiteral("algo")] = QStringLiteral("1"); m_fixedParams[QStringLiteral("relative")] = QStringLiteral("1"); fillParameters( QStringList() << QStringLiteral("accuracy,type,int,value,8,min,1,max,10,tooltip,Accuracy of Shakiness detection") << QStringLiteral("shakiness,type,int,value,4,min,1,max,10,tooltip,How shaky is the Video") << QStringLiteral("stepsize,type,int,value,6,min,0,max,100,tooltip,Stepsize of Detection process minimum around") << QStringLiteral("mincontrast,type,double,value,0.3,min,0,max,1,factor,1,decimals,2,tooltip,Below this Contrast Field is discarded") << QStringLiteral("smoothing,type,int,value,10,min,0,max,100,tooltip,number of frames for lowpass filtering") << QStringLiteral("maxshift,type,int,value,-1,min,-1,max,1000,tooltip,max number of pixels to shift") << QStringLiteral("maxangle,type,double,value,-1,min,-1,max,3.14,decimals,2,tooltip,max angle to rotate (in rad)") << QStringLiteral("crop,type,bool,value,0,min,0,max,1,tooltip,0 = keep border 1 = black background") << QStringLiteral("zoom,type,int,value,0,min,-500,max,500,tooltip,additional zoom during transform") << QStringLiteral("optzoom,type,bool,value,1,min,0,max,1,tooltip,use optimal zoom (calculated from transforms)") << QStringLiteral("sharpen,type,double,value,0.8,min,0,max,1,decimals,1,tooltip,sharpen transformed image") << QStringLiteral("tripod,type,position,value,0,min,0,max,100000,tooltip,reference frame")); } else if (m_filtername == QLatin1String("videostab")) { fillParameters(QStringList(QStringLiteral("shutterangle,type,int,value,0,min,0,max,180,tooltip,Angle that Images could be maximum rotated"))); } connect(buttonBox->button(QDialogButtonBox::Ok), &QPushButton::clicked, this, &ClipStabilize::slotValidate); m_vbox = new QVBoxLayout(optionsbox); QHashIterator> hi(m_ui_params); m_tc.setFormat(KdenliveSettings::project_fps()); while (hi.hasNext()) { hi.next(); QHash val = hi.value(); if (val[QStringLiteral("type")] == QLatin1String("int") || val[QStringLiteral("type")] == QLatin1String("double")) { DoubleWidget *dbl = new DoubleWidget(hi.key() /*name*/, val[QStringLiteral("value")].toDouble(), val[QStringLiteral("min")].toDouble(), val[QStringLiteral("max")].toDouble(), val[QStringLiteral("value")].toDouble(), 1, /*default*/ QString(), /*comment*/ 0 /*id*/, QString(), /*suffix*/ val[QStringLiteral("decimals")] != QString() ? val[QStringLiteral("decimals")].toInt() : 0, this); dbl->setObjectName(hi.key()); dbl->setToolTip(val[QStringLiteral("tooltip")]); connect(dbl, &DoubleWidget::valueChanged, this, &ClipStabilize::slotUpdateParams); m_vbox->addWidget(dbl); } else if (val[QStringLiteral("type")] == QLatin1String("bool")) { auto *ch = new QCheckBox(hi.key(), this); ch->setCheckState(val[QStringLiteral("value")] == QLatin1String("0") ? Qt::Unchecked : Qt::Checked); ch->setObjectName(hi.key()); connect(ch, &QCheckBox::stateChanged, this, &ClipStabilize::slotUpdateParams); ch->setToolTip(val[QStringLiteral("tooltip")]); m_vbox->addWidget(ch); } else if (val[QStringLiteral("type")] == QLatin1String("position")) { PositionWidget *posedit = new PositionWidget(hi.key(), 0, 0, out, m_tc, QString(), this); posedit->setToolTip(val[QStringLiteral("tooltip")]); posedit->setObjectName(hi.key()); m_vbox->addWidget(posedit); connect(posedit, &PositionWidget::valueChanged, this, &ClipStabilize::slotUpdateParams); } } adjustSize(); } ClipStabilize::~ClipStabilize() { /*if (m_stabilizeProcess.state() != QProcess::NotRunning) { m_stabilizeProcess.close(); }*/ KdenliveSettings::setAdd_new_clip(auto_add->isChecked()); } std::unordered_map ClipStabilize::filterParams() const { std::unordered_map params; for (const auto &it : m_fixedParams) { params[it.first] = it.second; } QHashIterator> it(m_ui_params); while (it.hasNext()) { it.next(); params[it.key()] = it.value().value(QStringLiteral("value")); } return params; } QString ClipStabilize::filterName() const { return m_filtername; } QString ClipStabilize::destination() const { QString path = dest_url->url().toLocalFile(); if (m_binIds.size() > 1 && !path.endsWith(QDir::separator())) { path.append(QDir::separator()); } return path; } QString ClipStabilize::desc() const { return i18n("Stabilize clip"); } void ClipStabilize::slotUpdateParams() { for (int i = 0; i < m_vbox->count(); ++i) { QWidget *w = m_vbox->itemAt(i)->widget(); QString name = w->objectName(); if (!name.isEmpty() && m_ui_params.contains(name)) { if (m_ui_params[name][QStringLiteral("type")] == QLatin1String("int") || m_ui_params[name][QStringLiteral("type")] == QLatin1String("double")) { - DoubleWidget *dbl = static_cast(w); + auto *dbl = static_cast(w); m_ui_params[name][QStringLiteral("value")] = QString::number((double)(dbl->getValue())); } else if (m_ui_params[name][QStringLiteral("type")] == QLatin1String("bool")) { - QCheckBox *ch = (QCheckBox *)w; + auto *ch = (QCheckBox *)w; m_ui_params[name][QStringLiteral("value")] = ch->checkState() == Qt::Checked ? QStringLiteral("1") : QStringLiteral("0"); } else if (m_ui_params[name][QStringLiteral("type")] == QLatin1String("position")) { - PositionWidget *pos = (PositionWidget *)w; + auto *pos = (PositionWidget *)w; m_ui_params[name][QStringLiteral("value")] = QString::number(pos->getPosition()); } } } } bool ClipStabilize::autoAddClip() const { return auto_add->isChecked(); } void ClipStabilize::fillParameters(QStringList lst) { m_ui_params.clear(); while (!lst.isEmpty()) { QString vallist = lst.takeFirst(); QStringList cont = vallist.split(QLatin1Char(',')); QString name = cont.takeFirst(); while (!cont.isEmpty()) { QString valname = cont.takeFirst(); QString val; if (!cont.isEmpty()) { val = cont.takeFirst(); } m_ui_params[name][valname] = val; } } } void ClipStabilize::slotValidate() { if (m_binIds.size() == 1) { if (QFile::exists(dest_url->url().toLocalFile())) { if (KMessageBox::questionYesNo(this, i18n("File %1 already exists.\nDo you want to overwrite it?", dest_url->url().toLocalFile())) == KMessageBox::No) { return; } } } else { QDir folder(dest_url->url().toLocalFile()); QStringList existingFiles; for (const QString &binId : m_binIds) { auto binClip = pCore->projectItemModel()->getClipByBinID(binId); auto url = binClip->url(); if (folder.exists(url + QStringLiteral(".mlt"))) { existingFiles.append(folder.absoluteFilePath(url + QStringLiteral(".mlt"))); } } if (!existingFiles.isEmpty()) { if (KMessageBox::warningContinueCancelList(this, i18n("The stabilize job will overwrite the following files:"), existingFiles) == KMessageBox::Cancel) { return; } } } accept(); } diff --git a/src/project/clipstabilize.h b/src/project/clipstabilize.h index 60b6cc959..a4e5331e7 100644 --- a/src/project/clipstabilize.h +++ b/src/project/clipstabilize.h @@ -1,65 +1,65 @@ /*************************************************************************** * Copyright (C) 2008 by Jean-Baptiste Mardelle (jb@kdenlive.org) * * Copyright (C) 2011 by Marco Gittler (marco@gitma.de) * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * 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 CLIPSTABILIZE_H #define CLIPSTABILIZE_H #include "definitions.h" #include "timecode.h" #include "ui_clipstabilize_ui.h" #include #include class ClipStabilize : public QDialog, public Ui::ClipStabilize_UI { Q_OBJECT public: - explicit ClipStabilize(const std::vector &binIds, const QString &filterName, int out, QWidget *parent = nullptr); - ~ClipStabilize(); + explicit ClipStabilize(const std::vector &binIds, QString filterName, int out, QWidget *parent = nullptr); + ~ClipStabilize() override; /** @brief Should the generated clip be added to current project. */ bool autoAddClip() const; /** @brief Return the filter parameters, filter name as value of "filter" entry. */ std::unordered_map filterParams() const; /** @brief Return the destination file or folder. */ QString destination() const; /** @brief Return the job description. */ QString desc() const; /* Return the name of the actual mlt filter used */ QString filterName() const; private slots: void slotUpdateParams(); void slotValidate(); private: QString m_filtername; std::vector m_binIds; QHash> m_ui_params; QVBoxLayout *m_vbox; void fillParameters(QStringList); std::unordered_map m_fixedParams; Timecode m_tc; signals: void addClip(const QUrl &url); }; #endif diff --git a/src/project/cliptranscode.cpp b/src/project/cliptranscode.cpp index 87254b1b3..dfc3b22d1 100644 --- a/src/project/cliptranscode.cpp +++ b/src/project/cliptranscode.cpp @@ -1,315 +1,314 @@ /*************************************************************************** * 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 "cliptranscode.h" #include "kdenlivesettings.h" #include "kxmlgui_version.h" #include #include #include -#include #include -ClipTranscode::ClipTranscode(const QStringList &urls, const QString ¶ms, const QStringList &postParams, const QString &description, - const QStringList &folderInfo, bool automaticMode, QWidget *parent) +ClipTranscode::ClipTranscode(QStringList urls, const QString ¶ms, QStringList postParams, const QString &description, QStringList folderInfo, + bool automaticMode, QWidget *parent) : QDialog(parent) - , m_urls(urls) - , m_folderInfo(folderInfo) + , m_urls(std::move(urls)) + , m_folderInfo(std::move(folderInfo)) , m_duration(0) , m_automaticMode(automaticMode) - , m_postParams(postParams) + , m_postParams(std::move(postParams)) { setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)); setupUi(this); setAttribute(Qt::WA_DeleteOnClose); m_infoMessage = new KMessageWidget; - QGridLayout *s = static_cast(layout()); + auto *s = static_cast(layout()); s->addWidget(m_infoMessage, 10, 0, 1, -1); m_infoMessage->setCloseButtonVisible(false); m_infoMessage->hide(); log_text->setHidden(true); setWindowTitle(i18n("Transcode Clip")); if (m_automaticMode) { auto_add->setHidden(true); } auto_add->setText(i18np("Add clip to project", "Add clips to project", m_urls.count())); auto_add->setChecked(KdenliveSettings::add_new_clip()); if (m_urls.count() == 1) { QString fileName = m_urls.constFirst(); source_url->setUrl(QUrl::fromLocalFile(fileName)); dest_url->setMode(KFile::File); #if KIO_VERSION >= QT_VERSION_CHECK(5, 33, 0) dest_url->setAcceptMode(QFileDialog::AcceptSave); #endif if (!params.isEmpty()) { QString newFile = params.section(QLatin1Char(' '), -1).replace(QLatin1String("%1"), fileName); QUrl dest = QUrl::fromLocalFile(newFile); dest_url->setUrl(dest); } urls_list->setHidden(true); connect(source_url, SIGNAL(textChanged(QString)), this, SLOT(slotUpdateParams())); } else { label_source->setHidden(true); source_url->setHidden(true); label_dest->setText(i18n("Destination folder")); dest_url->setMode(KFile::Directory); dest_url->setUrl(QUrl::fromLocalFile(m_urls.constFirst()).adjusted(QUrl::RemoveFilename)); dest_url->setMode(KFile::Directory | KFile::ExistingOnly); for (int i = 0; i < m_urls.count(); ++i) { urls_list->addItem(m_urls.at(i)); } } if (!params.isEmpty()) { label_profile->setHidden(true); profile_list->setHidden(true); ffmpeg_params->setPlainText(params.simplified()); if (!description.isEmpty()) { transcode_info->setText(description); } else { transcode_info->setHidden(true); } } else { // load Profiles 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 list = i.value().split(QLatin1Char(';')); profile_list->addItem(i.key(), list.at(0)); if (list.count() > 1) { profile_list->setItemData(profile_list->count() - 1, list.at(1), Qt::UserRole + 1); } } connect(profile_list, SIGNAL(currentIndexChanged(int)), this, SLOT(slotUpdateParams(int))); slotUpdateParams(0); } connect(button_start, &QAbstractButton::clicked, this, &ClipTranscode::slotStartTransCode); m_transcodeProcess.setProcessChannelMode(QProcess::MergedChannels); connect(&m_transcodeProcess, &QProcess::readyReadStandardOutput, this, &ClipTranscode::slotShowTranscodeInfo); connect(&m_transcodeProcess, static_cast(&QProcess::finished), this, &ClipTranscode::slotTranscodeFinished); ffmpeg_params->setMaximumHeight(QFontMetrics(font()).lineSpacing() * 5); adjustSize(); } ClipTranscode::~ClipTranscode() { KdenliveSettings::setAdd_new_clip(auto_add->isChecked()); if (m_transcodeProcess.state() != QProcess::NotRunning) { m_transcodeProcess.close(); } delete m_infoMessage; } void ClipTranscode::slotStartTransCode() { if (m_transcodeProcess.state() != QProcess::NotRunning) { return; } if (KdenliveSettings::ffmpegpath().isEmpty()) { // FFmpeg not detected, cannot process the Job log_text->setPlainText(i18n("FFmpeg not found, please set path in Kdenlive's settings Environment")); slotTranscodeFinished(1, QProcess::CrashExit); return; } m_duration = 0; m_destination.clear(); m_infoMessage->animatedHide(); QStringList parameters; QString destination; QString params = ffmpeg_params->toPlainText().simplified(); if (!m_urls.isEmpty() && urls_list->count() > 0) { // We are processing multiple clips source_url->setUrl(QUrl::fromLocalFile(m_urls.takeFirst())); destination = QDir(dest_url->url().toLocalFile()).absoluteFilePath(source_url->url().fileName()); QList matching = urls_list->findItems(source_url->url().toLocalFile(), Qt::MatchExactly); if (!matching.isEmpty()) { matching.at(0)->setFlags(Qt::ItemIsSelectable); urls_list->setCurrentItem(matching.at(0)); } } else { destination = dest_url->url().toLocalFile().section(QLatin1Char('.'), 0, -2); } QString extension = params.section(QStringLiteral("%1"), 1, 1).section(QLatin1Char(' '), 0, 0); QString s_url = source_url->url().toLocalFile(); if (params.contains(QLatin1String("-i "))) { // Filename must be inserted later } else { parameters << QStringLiteral("-i") << s_url; } if (QFile::exists(destination + extension)) { if (KMessageBox::questionYesNo(this, i18n("File %1 already exists.\nDo you want to overwrite it?", destination + extension)) == KMessageBox::No) { // Abort operation if (m_automaticMode) { // inform caller that we aborted emit transcodedClip(source_url->url(), QUrl()); close(); } return; } parameters << QStringLiteral("-y"); } bool replaceVfParams = false; const QStringList splitted = params.split(QLatin1Char(' ')); for (QString s : splitted) { if (replaceVfParams) { parameters << m_postParams.at(1); replaceVfParams = false; } else if (s.startsWith(QLatin1String("%1"))) { parameters << s.replace(0, 2, destination); } else if (!m_postParams.isEmpty() && s == QLatin1String("-vf")) { replaceVfParams = true; parameters << s; } else if (s == QLatin1String("-i")) { parameters << s; parameters << s_url; } else { parameters << s; } } buttonBox->button(QDialogButtonBox::Abort)->setText(i18n("Abort")); m_destination = destination + extension; m_transcodeProcess.start(KdenliveSettings::ffmpegpath(), parameters); source_url->setEnabled(false); dest_url->setEnabled(false); button_start->setEnabled(false); } void ClipTranscode::slotShowTranscodeInfo() { QString log = QString::fromLatin1(m_transcodeProcess.readAll()); if (m_duration == 0) { if (log.contains(QStringLiteral("Duration:"))) { QString duration = log.section(QStringLiteral("Duration:"), 1, 1).section(QLatin1Char(','), 0, 0).simplified(); QStringList numbers = duration.split(QLatin1Char(':')); if (numbers.size() < 3) { return; } m_duration = numbers.at(0).toInt() * 3600 + numbers.at(1).toInt() * 60 + numbers.at(2).toDouble(); log_text->setHidden(true); job_progress->setHidden(false); } else { log_text->setHidden(false); job_progress->setHidden(true); } } else if (log.contains(QStringLiteral("time="))) { int progress; QString time = log.section(QStringLiteral("time="), 1, 1).simplified().section(QLatin1Char(' '), 0, 0); if (time.contains(QLatin1Char(':'))) { QStringList numbers = time.split(QLatin1Char(':')); if (numbers.size() < 3) { return; } progress = numbers.at(0).toInt() * 3600 + numbers.at(1).toInt() * 60 + numbers.at(2).toDouble(); } else { progress = (int)time.toDouble(); } job_progress->setValue((int)(100.0 * progress / m_duration)); } log_text->setPlainText(log); } void ClipTranscode::slotTranscodeFinished(int exitCode, QProcess::ExitStatus exitStatus) { buttonBox->button(QDialogButtonBox::Abort)->setText(i18n("Close")); button_start->setEnabled(true); source_url->setEnabled(true); dest_url->setEnabled(true); m_duration = 0; if (QFileInfo(m_destination).size() <= 0) { // Destination file does not exist, transcoding failed exitCode = 1; } if (exitCode == 0 && exitStatus == QProcess::NormalExit) { log_text->setHtml(log_text->toPlainText() + QStringLiteral("
    ") + i18n("Transcoding finished.")); if (auto_add->isChecked() || m_automaticMode) { QUrl url; if (urls_list->count() > 0) { QString params = ffmpeg_params->toPlainText().simplified(); QString extension = params.section(QStringLiteral("%1"), 1, 1).section(QLatin1Char(' '), 0, 0); url = QUrl::fromLocalFile(dest_url->url().toLocalFile() + QDir::separator() + source_url->url().fileName() + extension); } else { url = dest_url->url(); } if (m_automaticMode) { emit transcodedClip(source_url->url(), url); } else { emit addClip(url, m_folderInfo); } } if (urls_list->count() > 0 && m_urls.count() > 0) { m_transcodeProcess.close(); slotStartTransCode(); return; } if (auto_close->isChecked()) { accept(); } else { m_infoMessage->setMessageType(KMessageWidget::Positive); m_infoMessage->setText(i18n("Transcoding finished.")); m_infoMessage->animatedShow(); } } else { m_infoMessage->setMessageType(KMessageWidget::Warning); m_infoMessage->setText(i18n("Transcoding failed!")); m_infoMessage->animatedShow(); log_text->setVisible(true); } m_transcodeProcess.close(); // Refill url list in case user wants to transcode to another format if (urls_list->count() > 0) { m_urls.clear(); for (int i = 0; i < urls_list->count(); ++i) { m_urls << urls_list->item(i)->text(); } } } void ClipTranscode::slotUpdateParams(int ix) { QString fileName = source_url->url().toLocalFile(); if (ix != -1) { QString params = profile_list->itemData(ix).toString(); ffmpeg_params->setPlainText(params.simplified()); QString desc = profile_list->itemData(ix, Qt::UserRole + 1).toString(); if (!desc.isEmpty()) { transcode_info->setText(desc); transcode_info->setHidden(false); } else { transcode_info->setHidden(true); } } if (urls_list->count() == 0) { QString newFile = ffmpeg_params->toPlainText().simplified().section(QLatin1Char(' '), -1).replace(QLatin1String("%1"), fileName); dest_url->setUrl(QUrl::fromLocalFile(newFile)); } } diff --git a/src/project/cliptranscode.h b/src/project/cliptranscode.h index 6db20a571..6f9343233 100644 --- a/src/project/cliptranscode.h +++ b/src/project/cliptranscode.h @@ -1,63 +1,63 @@ /*************************************************************************** * 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 CLIPTRANSCODE_H #define CLIPTRANSCODE_H #include "ui_cliptranscode_ui.h" #include #include #include class ClipTranscode : public QDialog, public Ui::ClipTranscode_UI { Q_OBJECT public: - ClipTranscode(const QStringList &urls, const QString ¶ms, const QStringList &postParams, const QString &description, - const QStringList &folderInfo = QStringList(), bool automaticMode = false, QWidget *parent = nullptr); - ~ClipTranscode(); + ClipTranscode(QStringList urls, const QString ¶ms, QStringList postParams, const QString &description, QStringList folderInfo = QStringList(), + bool automaticMode = false, QWidget *parent = nullptr); + ~ClipTranscode() override; public slots: void slotStartTransCode(); private slots: void slotShowTranscodeInfo(); void slotTranscodeFinished(int exitCode, QProcess::ExitStatus exitStatus); void slotUpdateParams(int ix = -1); private: QProcess m_transcodeProcess; QStringList m_urls; QStringList m_folderInfo; int m_duration; bool m_automaticMode; /** @brief The path for destination transcoded file. */ QString m_destination; QStringList m_postParams; KMessageWidget *m_infoMessage; signals: void addClip(const QUrl &url, const QStringList &folderInfo = QStringList()); void transcodedClip(const QUrl &source, const QUrl &result); }; #endif diff --git a/src/project/dialogs/archivewidget.cpp b/src/project/dialogs/archivewidget.cpp index d471a2684..73f204ed8 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 - +#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 (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()); + auto *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) +ArchiveWidget::ArchiveWidget(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_extractUrl(std::move(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); + auto *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/archivewidget.h b/src/project/dialogs/archivewidget.h index b67fa8e10..85c9d830c 100644 --- a/src/project/dialogs/archivewidget.h +++ b/src/project/dialogs/archivewidget.h @@ -1,116 +1,116 @@ /*************************************************************************** * 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 ARCHIVEWIDGET_H #define ARCHIVEWIDGET_H #include "ui_archivewidget_ui.h" #include #include #include #include #include #include #include #include class KJob; class KArchive; class ClipController; /** * @class ArchiveWidget * @brief A widget allowing to archive a project (copy all project files to a new location) * @author Jean-Baptiste Mardelle */ class KMessageWidget; class ArchiveWidget : public QDialog, public Ui::ArchiveWidget_UI { Q_OBJECT public: ArchiveWidget(const QString &projectName, const QDomDocument &doc, const QStringList &luma_list, QWidget *parent = nullptr); // Constructor for extracting widget - explicit ArchiveWidget(const QUrl &url, QWidget *parent = nullptr); - ~ArchiveWidget(); + explicit ArchiveWidget(QUrl url, QWidget *parent = nullptr); + ~ArchiveWidget() override; QString extractedProjectFile() const; private slots: void slotCheckSpace(); bool slotStartArchiving(bool firstPass = true); void slotArchivingFinished(KJob *job = nullptr, bool finished = false); void slotArchivingProgress(KJob *, KIO::filesize_t); void done(int r) Q_DECL_OVERRIDE; bool closeAccepted(); void createArchive(); void slotArchivingProgress(int); void slotArchivingFinished(bool result); void slotStartExtracting(); void doExtracting(); void slotExtractingFinished(); void slotExtractProgress(); void slotGotProgress(KJob *); void openArchiveForExtraction(); void slotDisplayMessage(const QString &icon, const QString &text); void slotJobResult(bool success, const QString &text); void slotProxyOnly(int onlyProxy); protected: void closeEvent(QCloseEvent *e) override; private: KIO::filesize_t m_requestedSize; KIO::CopyJob *m_copyJob; QMap m_duplicateFiles; QMap m_replacementList; QString m_name; QDomDocument m_doc; QTemporaryFile *m_temp; bool m_abortArchive; QFuture m_archiveThread; QStringList m_foldersList; QMap m_filesList; bool m_extractMode; QUrl m_extractUrl; QString m_projectName; QTimer *m_progressTimer; KArchive *m_extractArchive; int m_missingClips; KMessageWidget *m_infoMessage; /** @brief Generate tree widget subitems from a string list of urls. */ void generateItems(QTreeWidgetItem *parentItem, const QStringList &items); /** @brief Generate tree widget subitems from a map of clip ids / urls. */ void generateItems(QTreeWidgetItem *parentItem, const QMap &items); /** @brief Replace urls in project file. */ bool processProjectFile(); signals: void archivingFinished(bool); void archiveProgress(int); void extractingFinished(); void showMessage(const QString &, const QString &); }; #endif diff --git a/src/project/dialogs/backupwidget.cpp b/src/project/dialogs/backupwidget.cpp index 80a32a6dc..d8960da74 100644 --- a/src/project/dialogs/backupwidget.cpp +++ b/src/project/dialogs/backupwidget.cpp @@ -1,122 +1,121 @@ /*************************************************************************** * 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 "backupwidget.h" #include "kdenlivesettings.h" #include #include -#include -BackupWidget::BackupWidget(const QUrl &projectUrl, const QUrl &projectFolder, const QString &projectId, QWidget *parent) +BackupWidget::BackupWidget(const QUrl &projectUrl, QUrl projectFolder, const QString &projectId, QWidget *parent) : QDialog(parent) - , m_projectFolder(projectFolder) + , m_projectFolder(std::move(projectFolder)) { setupUi(this); setWindowTitle(i18n("Restore Backup File")); if (!projectUrl.isValid()) { // No url, means we opened the backup dialog from an empty project info_label->setText(i18n("Showing all backup files in folder")); m_projectWildcard = QLatin1Char('*'); } else { info_label->setText(i18n("Showing backup files for %1", projectUrl.fileName())); m_projectWildcard = projectUrl.fileName().section(QLatin1Char('.'), 0, -2); if (!projectId.isEmpty()) { m_projectWildcard.append(QLatin1Char('-') + projectId); } else { // No project id, it was lost, add wildcard m_projectWildcard.append(QLatin1Char('*')); } } m_projectWildcard.append(QStringLiteral("-??")); m_projectWildcard.append(QStringLiteral("??")); m_projectWildcard.append(QStringLiteral("-??")); m_projectWildcard.append(QStringLiteral("-??")); m_projectWildcard.append(QStringLiteral("-??")); m_projectWildcard.append(QStringLiteral("-??.kdenlive")); slotParseBackupFiles(); connect(backup_list, &QListWidget::currentRowChanged, this, &BackupWidget::slotDisplayBackupPreview); backup_list->setCurrentRow(0); backup_list->setMinimumHeight(QFontMetrics(font()).lineSpacing() * 12); slotParseBackupFiles(); } -BackupWidget::~BackupWidget() {} +BackupWidget::~BackupWidget() = default; void BackupWidget::slotParseBackupFiles() { QStringList filter; filter << m_projectWildcard; backup_list->clear(); // Parse new XDG backup folder $HOME/.local/share/kdenlive/.backup QDir backupFolder(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/.backup")); backupFolder.setNameFilters(filter); QFileInfoList resultList = backupFolder.entryInfoList(QDir::Files, QDir::Time); for (int i = 0; i < resultList.count(); ++i) { QString label = resultList.at(i).lastModified().toString(Qt::SystemLocaleLongDate); if (m_projectWildcard.startsWith(QLatin1Char('*'))) { // Displaying all backup files, so add project name in the entries label.prepend(resultList.at(i).fileName().section(QLatin1Char('-'), 0, -7) + QStringLiteral(".kdenlive - ")); } auto *item = new QListWidgetItem(label, backup_list); item->setData(Qt::UserRole, resultList.at(i).absoluteFilePath()); item->setToolTip(resultList.at(i).absoluteFilePath()); } // Parse old $HOME/kdenlive/.backup folder QDir dir(m_projectFolder.toLocalFile() + QStringLiteral("/.backup")); if (dir.exists()) { dir.setNameFilters(filter); QFileInfoList resultList2 = dir.entryInfoList(QDir::Files, QDir::Time); for (int i = 0; i < resultList2.count(); ++i) { QString label = resultList2.at(i).lastModified().toString(Qt::SystemLocaleLongDate); if (m_projectWildcard.startsWith(QLatin1Char('*'))) { // Displaying all backup files, so add project name in the entries label.prepend(resultList2.at(i).fileName().section(QLatin1Char('-'), 0, -7) + QStringLiteral(".kdenlive - ")); } auto *item = new QListWidgetItem(label, backup_list); item->setData(Qt::UserRole, resultList2.at(i).absoluteFilePath()); item->setToolTip(resultList2.at(i).absoluteFilePath()); } } buttonBox->button(QDialogButtonBox::Open)->setEnabled(backup_list->count() > 0); } void BackupWidget::slotDisplayBackupPreview() { if (!backup_list->currentItem()) { backup_preview->setPixmap(QPixmap()); return; } const QString path = backup_list->currentItem()->data(Qt::UserRole).toString(); QPixmap pix(path + QStringLiteral(".png")); backup_preview->setPixmap(pix); } QString BackupWidget::selectedFile() const { if (!backup_list->currentItem()) { return QString(); } return backup_list->currentItem()->data(Qt::UserRole).toString(); } diff --git a/src/project/dialogs/backupwidget.h b/src/project/dialogs/backupwidget.h index 10723b8e6..14c4fae13 100644 --- a/src/project/dialogs/backupwidget.h +++ b/src/project/dialogs/backupwidget.h @@ -1,55 +1,55 @@ /*************************************************************************** * 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 BACKUPWIDGET_H #define BACKUPWIDGET_H #include "ui_backupdialog_ui.h" #include /** * @class BackupWidget * @brief A widget allowing to parse backup project files * @author Jean-Baptiste Mardelle */ class BackupWidget : public QDialog, public Ui::BackupDialog_UI { Q_OBJECT public: - BackupWidget(const QUrl &projectUrl, const QUrl &projectFolder, const QString &projectId, QWidget *parent = nullptr); + BackupWidget(const QUrl &projectUrl, QUrl projectFolder, const QString &projectId, QWidget *parent = nullptr); // Constructor for extracting widget - ~BackupWidget(); + ~BackupWidget() override; /** @brief Return the path for selected backup file. */ QString selectedFile() const; private slots: /** @brief Parse the backup files in project folder. */ void slotParseBackupFiles(); /** @brief Display a thumbnail preview of selected backup. */ void slotDisplayBackupPreview(); private: QString m_projectWildcard; QUrl m_projectFolder; }; #endif diff --git a/src/project/dialogs/noteswidget.h b/src/project/dialogs/noteswidget.h index 81d1b244d..c71d3408c 100644 --- a/src/project/dialogs/noteswidget.h +++ b/src/project/dialogs/noteswidget.h @@ -1,52 +1,52 @@ /*************************************************************************** * 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 NOTESWIDGET_H #define NOTESWIDGET_H #include /** * @class NotesWidget * @brief A small text editor to create project notes. * @author Jean-Baptiste Mardelle */ class NotesWidget : public QTextEdit { Q_OBJECT public: explicit NotesWidget(QWidget *parent = nullptr); - ~NotesWidget(); + ~NotesWidget() override; /** @brief insert current timeline timecode and focus widget to allow entering quick note */ void addProjectNote(); protected: void mouseMoveEvent(QMouseEvent *e) override; void mousePressEvent(QMouseEvent *e) override; private slots: void slotFillNotesMenu(const QPoint &pos); signals: void insertNotesTimecode(); void seekProject(int); }; #endif diff --git a/src/project/dialogs/profilewidget.cpp b/src/project/dialogs/profilewidget.cpp index fdb08f5ac..ade1015a8 100644 --- a/src/project/dialogs/profilewidget.cpp +++ b/src/project/dialogs/profilewidget.cpp @@ -1,269 +1,269 @@ /* Copyright (C) 2016 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 "profilewidget.h" #include "kdenlivesettings.h" #include "kxmlgui_version.h" #include "profiles/profilemodel.hpp" #include "profiles/profilerepository.hpp" #include "profiles/tree/profilefilter.hpp" #include "profiles/tree/profiletreemodel.hpp" #include #include #include #include #include #include #include ProfileWidget::ProfileWidget(QWidget *parent) : QWidget(parent) { m_originalProfile = QStringLiteral("invalid"); setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); auto *lay = new QVBoxLayout; lay->setContentsMargins(0, 0, 0, 0); auto *labelLay = new QHBoxLayout; QLabel *fpsLabel = new QLabel(i18n("Fps"), this); m_fpsFilt = new QComboBox(this); fpsLabel->setBuddy(m_fpsFilt); labelLay->addWidget(fpsLabel); labelLay->addWidget(m_fpsFilt); QLabel *scanningLabel = new QLabel(i18n("Scanning"), this); m_scanningFilt = new QComboBox(this); scanningLabel->setBuddy(m_scanningFilt); labelLay->addWidget(scanningLabel); labelLay->addWidget(m_scanningFilt); labelLay->addStretch(1); auto *manage_profiles = new QToolButton(this); labelLay->addWidget(manage_profiles); manage_profiles->setIcon(QIcon::fromTheme(QStringLiteral("configure"))); manage_profiles->setToolTip(i18n("Manage project profiles")); connect(manage_profiles, &QAbstractButton::clicked, this, &ProfileWidget::slotEditProfiles); lay->addLayout(labelLay); auto *profileSplitter = new QSplitter; m_treeView = new QTreeView(this); m_treeModel = ProfileTreeModel::construct(this); m_filter = new ProfileFilter(this); m_filter->setSourceModel(m_treeModel.get()); m_treeView->setModel(m_filter); for (int i = 1; i < m_treeModel->columnCount(); ++i) { m_treeView->hideColumn(i); } m_treeView->header()->hide(); QItemSelectionModel *selectionModel = m_treeView->selectionModel(); connect(selectionModel, &QItemSelectionModel::currentRowChanged, this, &ProfileWidget::slotChangeSelection); connect(selectionModel, &QItemSelectionModel::selectionChanged, [&](const QItemSelection &selected, const QItemSelection &deselected) { QModelIndex current, old; if (!selected.indexes().isEmpty()) { current = selected.indexes().front(); } if (!deselected.indexes().isEmpty()) { old = deselected.indexes().front(); } slotChangeSelection(current, old); }); int treeViewFontHeight = QFontInfo(m_treeView->font()).pixelSize(); m_treeView->setMinimumHeight(treeViewFontHeight); profileSplitter->addWidget(m_treeView); m_treeView->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); m_descriptionPanel = new QTextEdit(this); m_descriptionPanel->setReadOnly(true); m_descriptionPanel->viewport()->setCursor(Qt::ArrowCursor); m_descriptionPanel->viewport()->setBackgroundRole(QPalette::Mid); m_descriptionPanel->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); m_descriptionPanel->setFrameStyle(QFrame::NoFrame); m_descriptionPanel->setMinimumHeight(treeViewFontHeight); profileSplitter->addWidget(m_descriptionPanel); lay->addWidget(profileSplitter); profileSplitter->setStretchFactor(0, 4); profileSplitter->setStretchFactor(1, 3); refreshFpsCombo(); auto updateFps = [&]() { double current = m_fpsFilt->currentData().toDouble(); KdenliveSettings::setProfile_fps_filter(m_fpsFilt->currentText()); m_filter->setFilterFps(current > 0, current); slotFilterChanged(); }; connect(m_fpsFilt, static_cast(&QComboBox::currentIndexChanged), updateFps); int ix = m_fpsFilt->findText(KdenliveSettings::profile_fps_filter()); if (ix > -1) { m_fpsFilt->setCurrentIndex(ix); } m_scanningFilt->addItem("Any", -1); m_scanningFilt->addItem("Interlaced", 0); m_scanningFilt->addItem("Progressive", 1); auto updateScanning = [&]() { int current = m_scanningFilt->currentData().toInt(); KdenliveSettings::setProfile_scanning_filter(m_scanningFilt->currentText()); m_filter->setFilterInterlaced(current != -1, current == 0); slotFilterChanged(); }; connect(m_scanningFilt, static_cast(&QComboBox::currentIndexChanged), updateScanning); ix = m_scanningFilt->findText(KdenliveSettings::profile_scanning_filter()); if (ix > -1) { m_scanningFilt->setCurrentIndex(ix); } setLayout(lay); } -ProfileWidget::~ProfileWidget() {} +ProfileWidget::~ProfileWidget() = default; void ProfileWidget::refreshFpsCombo() { QLocale locale; QVariant currentValue; if (m_fpsFilt->count() > 1) { // remember last selected value currentValue = m_fpsFilt->currentData(); } m_fpsFilt->clear(); locale.setNumberOptions(QLocale::OmitGroupSeparator); m_fpsFilt->addItem("Any", -1); auto all_fps = ProfileRepository::get()->getAllFps(); for (double fps : all_fps) { m_fpsFilt->addItem(locale.toString(fps), fps); } if (currentValue.isValid()) { int ix = m_fpsFilt->findData(currentValue); if (ix > -1) { m_fpsFilt->setCurrentIndex(ix); } } } void ProfileWidget::loadProfile(const QString &profile) { auto index = m_treeModel->findProfile(profile); if (index.isValid()) { m_originalProfile = m_currentProfile = m_lastValidProfile = profile; if (!trySelectProfile(profile)) { // When loading a profile, ensure it is visible so reset filters if necessary m_fpsFilt->setCurrentIndex(0); m_scanningFilt->setCurrentIndex(0); } } } const QString ProfileWidget::selectedProfile() const { return m_currentProfile; } void ProfileWidget::slotEditProfiles() { auto *w = new ProfilesDialog(m_currentProfile); w->exec(); if (w->profileTreeChanged()) { // Rebuild profiles tree m_treeModel.reset(); m_treeModel = ProfileTreeModel::construct(this); m_filter->setSourceModel(m_treeModel.get()); refreshFpsCombo(); loadProfile(m_currentProfile); } delete w; } void ProfileWidget::fillDescriptionPanel(const QString &profile_path) { QString description; if (profile_path.isEmpty()) { description += i18n("No profile selected"); } else { std::unique_ptr &profile = ProfileRepository::get()->getProfile(profile_path); description += i18n("
    Video Settings
    "); description += i18n("

    Frame size: %1 x %2 (%3:%4)
    ", profile->width(), profile->height(), profile->display_aspect_num(), profile->display_aspect_den()); description += i18n("Frame rate: %1 fps
    ", profile->fps()); description += i18n("Pixel Aspect Ratio: %1
    ", profile->sar()); description += i18n("Color Space: %1
    ", profile->colorspaceDescription()); QString interlaced = i18n("yes"); if (profile->progressive()) { interlaced = i18n("no"); } description += i18n("Interlaced : %1

    ", interlaced); } m_descriptionPanel->setHtml(description); } void ProfileWidget::slotChangeSelection(const QModelIndex ¤t, const QModelIndex &previous) { auto originalIndex = m_filter->mapToSource(current); if (m_treeModel->parent(originalIndex) == QModelIndex()) { // in that case, we have selected a category, which we don't want QItemSelectionModel *selection = m_treeView->selectionModel(); selection->select(previous, QItemSelectionModel::Select); return; } m_currentProfile = m_treeModel->getProfile(originalIndex); if (!m_currentProfile.isEmpty()) { m_lastValidProfile = m_currentProfile; } if (m_originalProfile != m_currentProfile) { emit profileChanged(); } fillDescriptionPanel(m_currentProfile); } bool ProfileWidget::trySelectProfile(const QString &profile) { auto index = m_treeModel->findProfile(profile); if (index.isValid()) { // check if element is visible if (m_filter->isVisible(index)) { // reselect QItemSelectionModel *selection = m_treeView->selectionModel(); selection->select(m_filter->mapFromSource(index), QItemSelectionModel::Select); // expand corresponding category auto parent = m_treeModel->parent(index); m_treeView->expand(m_filter->mapFromSource(parent)); m_treeView->scrollTo(m_filter->mapFromSource(index), QAbstractItemView::PositionAtCenter); return true; } } return false; } void ProfileWidget::slotFilterChanged() { // When filtering change, we must check if the current profile is still visible. if (!trySelectProfile(m_currentProfile)) { // we try to back-up the last valid profile if (!trySelectProfile(m_lastValidProfile)) { // Everything fails, we don't have any profile m_currentProfile = QString(); emit profileChanged(); fillDescriptionPanel(QString()); } } } diff --git a/src/project/dialogs/profilewidget.h b/src/project/dialogs/profilewidget.h index 8adbc8c75..844eaa9e3 100644 --- a/src/project/dialogs/profilewidget.h +++ b/src/project/dialogs/profilewidget.h @@ -1,92 +1,92 @@ /* Copyright (C) 2016 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 PROFILESELECTWIDGET_H #define PROFILESELECTWIDGET_H #include "dialogs/profilesdialog.h" #include #include class KMessageWidget; class QTextEdit; class ProfileTreeModel; class ProfileFilter; class TreeView; class QTreeView; /** * @class ProfileWidget * @brief Provides interface to choose and filter profiles * @author Jean-Baptiste Mardelle, Nicolas Carion */ class ProfileWidget : public QWidget { Q_OBJECT public: explicit ProfileWidget(QWidget *parent = nullptr); - ~ProfileWidget(); + ~ProfileWidget() override; void loadProfile(const QString &profile); const QString selectedProfile() const; private: /** @brief currently selected's profile path */ QString m_currentProfile; QString m_lastValidProfile; QString m_originalProfile; void slotUpdateInfoDisplay(); QComboBox *m_fpsFilt; QComboBox *m_scanningFilt; QTreeView *m_treeView; std::shared_ptr m_treeModel; ProfileFilter *m_filter; QTextEdit *m_descriptionPanel; /** @brief Open project profile management dialog. */ void slotEditProfiles(); /** @brief Manage a change in the selection */ void slotChangeSelection(const QModelIndex ¤t, const QModelIndex &previous); /* @brief Fill the description of the profile. @param profile_path is the path to the profile */ void fillDescriptionPanel(const QString &profile_path); /** @brief Select the profile with given path. Returns true on success */ bool trySelectProfile(const QString &profile); /** @brief Slot to be called whenever filtering changes */ void slotFilterChanged(); /** @brief Reload available fps values */ void refreshFpsCombo(); signals: void profileChanged(); }; #endif diff --git a/src/project/dialogs/projectsettings.cpp b/src/project/dialogs/projectsettings.cpp index da9a84dc6..99bb66d3f 100644 --- a/src/project/dialogs/projectsettings.cpp +++ b/src/project/dialogs/projectsettings.cpp @@ -1,865 +1,864 @@ /*************************************************************************** * 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, +ProjectSettings::ProjectSettings(KdenliveDoc *doc, QMap metadata, QStringList lumas, int videotracks, int audiotracks, const QString & /*projectPath*/, bool readOnlyTracks, bool savedProject, QWidget *parent) : QDialog(parent) , m_savedProject(savedProject) - , m_lumas(lumas) + , m_lumas(std::move(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 (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/dialogs/projectsettings.h b/src/project/dialogs/projectsettings.h index bbe675d23..b628ec5e9 100644 --- a/src/project/dialogs/projectsettings.h +++ b/src/project/dialogs/projectsettings.h @@ -1,110 +1,110 @@ /*************************************************************************** * 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 PROJECTSETTINGS_H #define PROJECTSETTINGS_H #include #include #include "ui_projectsettings_ui.h" class KdenliveDoc; class ProfileWidget; class ProjectSettings : public QDialog, public Ui::ProjectSettings_UI { Q_OBJECT public: - ProjectSettings(KdenliveDoc *doc, QMap metadata, const QStringList &lumas, int videotracks, int audiotracks, const QString &projectPath, + ProjectSettings(KdenliveDoc *doc, QMap metadata, QStringList lumas, int videotracks, int audiotracks, const QString &projectPath, bool readOnlyTracks, bool unsavedProject, QWidget *parent = nullptr); QString selectedProfile() const; QUrl selectedFolder() const; QPoint tracks() const; bool enableVideoThumbs() const; bool enableAudioThumbs() const; bool useProxy() const; bool useExternalProxy() const; bool generateProxy() const; int proxyMinSize() const; bool generateImageProxy() const; int proxyImageMinSize() const; int proxyImageSize() const; QString externalProxyParams() const; QString proxyParams() const; QString proxyExtension() const; const QMap metadata() const; static QStringList extractPlaylistUrls(const QString &path); static QStringList extractSlideshowUrls(const QString &url); const QString selectedPreview() const; const QString storageFolder() const; public slots: void accept() override; private slots: void slotUpdateButton(const QString &path); void slotUpdateFiles(bool cacheOnly = false); void slotDeleteUnused(); /** @brief Export project data to text file. */ void slotExportToText(); /** @brief Update the displayed proxy parameters when user changes selection. */ void slotUpdateProxyParams(); void slotUpdatePreviewParams(); /** @brief Insert a new metadata field. */ void slotAddMetadataField(); /** @brief Delete current metadata field. */ void slotDeleteMetadataField(); /** @brief Display proxy profiles management dialog. */ void slotManageEncodingProfile(); void slotManagePreviewProfile(); /** @brief Open editor for metadata item. */ void slotEditMetadata(QTreeWidgetItem *, int); private: QPushButton *m_buttonOk; ProfileWidget *m_pw; bool m_savedProject; QStringList m_lumas; QString m_proxyparameters; QString m_proxyextension; /** @brief List of all proxies urls in this project. */ QStringList m_projectProxies; /** @brief List of all thumbnails used in this project. */ QStringList m_projectThumbs; QDir m_previewDir; /** @brief Fill the proxy profiles combobox. */ void loadProxyProfiles(); /** @brief Fill the external proxy profiles combobox. */ void loadExternalProxyProfiles(); QString m_previewparams; QString m_previewextension; QString m_initialExternalProxyProfile; /** @brief Fill the proxy profiles combobox. */ void loadPreviewProfiles(); signals: /** @brief User deleted proxies, so disable them in project. */ void disableProxies(); void disablePreview(); void refreshProfiles(); }; #endif diff --git a/src/project/dialogs/slideshowclip.h b/src/project/dialogs/slideshowclip.h index 2af89dc71..5728ae14e 100644 --- a/src/project/dialogs/slideshowclip.h +++ b/src/project/dialogs/slideshowclip.h @@ -1,76 +1,76 @@ /*************************************************************************** * 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 SLIDESHOWCLIP_H #define SLIDESHOWCLIP_H #include "definitions.h" #include "timecode.h" #include "ui_slideshowclip_ui.h" #include class ProjectClip; class SlideshowClip : public QDialog { Q_OBJECT public: explicit SlideshowClip(const Timecode &tc, QString clipFolder, ProjectClip *clip = nullptr, QWidget *parent = nullptr); - virtual ~SlideshowClip(); + ~SlideshowClip() override; /** return selected path for slideshow in MLT format */ QString selectedPath(); QString clipName() const; QString clipDuration() const; QString lumaDuration() const; int imageCount() const; bool loop() const; bool crop() const; bool fade() const; QString lumaFile() const; int softness() const; QString animation() const; /** @brief Get the image frame number from a file path, for example image_047.jpg will return 47. */ static int getFrameNumberFromPath(const QUrl &path); /** @brief return the url pattern for selected slideshow. */ static QString selectedPath(const QUrl &url, bool isMime, QString extension, QStringList *list); /** @brief Convert the selection animation style into an affine geometry string. */ static QString animationToGeometry(const QString &animation, int &ttl); private slots: void parseFolder(); void slotEnableLuma(int state); void slotEnableThumbs(int state); void slotEnableLumaFile(int state); void slotUpdateDurationFormat(int ix); void slotGenerateThumbs(); void slotSetPixmap(const KFileItem &fileItem, const QPixmap &pix); /** @brief Display correct widget depending on user choice (MIME type or pattern method). */ void slotMethodChanged(bool active); private: Ui::SlideshowClip_UI m_view; int m_count; Timecode m_timecode; KIO::PreviewJob *m_thumbJob; }; #endif diff --git a/src/project/dialogs/temporarydata.cpp b/src/project/dialogs/temporarydata.cpp index 16196e8dd..7108311a3 100644 --- a/src/project/dialogs/temporarydata.cpp +++ b/src/project/dialogs/temporarydata.cpp @@ -1,606 +1,606 @@ /* Copyright (C) 2016 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 "temporarydata.h" #include "doc/kdenlivedoc.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include static QList chartColors; ChartWidget::ChartWidget(QWidget *parent) : QWidget(parent) { QFontMetrics ft(font()); int minHeight = ft.height() * 6; setMinimumSize(minHeight, minHeight); setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); m_segments = QList(); } void ChartWidget::setSegments(const QList &segments) { m_segments = segments; update(); } void ChartWidget::paintEvent(QPaintEvent *event) { QPainter painter(this); painter.setRenderHints(QPainter::Antialiasing); const QRectF clipRect = event->rect(); painter.setClipRect(clipRect); int pieWidth = qMin(width(), height()) - 10; const QRectF pieRect(5, 5, pieWidth, pieWidth); int ix = 0; int previous = 0; for (int val : m_segments) { if (val == 0) { ix++; continue; } painter.setBrush(chartColors.at(ix)); painter.drawPie(pieRect, previous, val * 16); previous = val * 16; ix++; } } TemporaryData::TemporaryData(KdenliveDoc *doc, bool currentProjectOnly, QWidget *parent) : QWidget(parent) , m_doc(doc) , m_globalPage(nullptr) , m_globalDelete(nullptr) { chartColors << QColor(Qt::darkRed) << QColor(Qt::darkBlue) << QColor(Qt::darkGreen) << QColor(Qt::darkMagenta); m_currentSizes << 0 << 0 << 0 << 0; auto *lay = new QVBoxLayout; m_currentPage = new QWidget(this); m_currentPage->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Maximum); m_grid = new QGridLayout; m_currentPie = new ChartWidget(this); m_grid->addWidget(m_currentPie, 0, 0, 5, 1); QPalette pal(palette()); QFontMetrics ft(font()); int minHeight = ft.height() / 2; // Timeline preview data QLabel *color = new QLabel(this); color->setFixedSize(minHeight, minHeight); pal.setColor(QPalette::Window, chartColors.at(0)); color->setPalette(pal); color->setAutoFillBackground(true); m_grid->addWidget(color, 0, 1, Qt::AlignCenter); QLabel *preview = new QLabel(i18n("Timeline Preview"), this); m_grid->addWidget(preview, 0, 2); m_previewSize = new QLabel(this); m_grid->addWidget(m_previewSize, 0, 3); auto *del = new QToolButton(this); del->setIcon(QIcon::fromTheme(QStringLiteral("trash-empty"))); connect(del, &QToolButton::clicked, this, &TemporaryData::deletePreview); del->setEnabled(false); m_grid->addWidget(del, 0, 4); // Proxy clips color = new QLabel(this); color->setFixedSize(minHeight, minHeight); pal.setColor(QPalette::Window, chartColors.at(1)); color->setPalette(pal); color->setAutoFillBackground(true); m_grid->addWidget(color, 1, 1, Qt::AlignCenter); preview = new QLabel(i18n("Proxy Clips"), this); m_grid->addWidget(preview, 1, 2); m_proxySize = new QLabel(this); m_grid->addWidget(m_proxySize, 1, 3); del = new QToolButton(this); del->setIcon(QIcon::fromTheme(QStringLiteral("trash-empty"))); connect(del, &QToolButton::clicked, this, &TemporaryData::deleteProxy); del->setEnabled(false); m_grid->addWidget(del, 1, 4); // Audio Thumbs color = new QLabel(this); color->setFixedSize(minHeight, minHeight); pal.setColor(QPalette::Window, chartColors.at(2)); color->setPalette(pal); color->setAutoFillBackground(true); m_grid->addWidget(color, 2, 1, Qt::AlignCenter); preview = new QLabel(i18n("Audio Thumbnails"), this); m_grid->addWidget(preview, 2, 2); m_audioSize = new QLabel(this); m_grid->addWidget(m_audioSize, 2, 3); del = new QToolButton(this); del->setIcon(QIcon::fromTheme(QStringLiteral("trash-empty"))); connect(del, &QToolButton::clicked, this, &TemporaryData::deleteAudio); del->setEnabled(false); m_grid->addWidget(del, 2, 4); // Video Thumbs color = new QLabel(this); color->setFixedSize(minHeight, minHeight); pal.setColor(QPalette::Window, chartColors.at(3)); color->setPalette(pal); color->setAutoFillBackground(true); m_grid->addWidget(color, 3, 1, Qt::AlignCenter); preview = new QLabel(i18n("Video Thumbnails"), this); m_grid->addWidget(preview, 3, 2); m_thumbSize = new QLabel(this); m_grid->addWidget(m_thumbSize, 3, 3); del = new QToolButton(this); del->setIcon(QIcon::fromTheme(QStringLiteral("trash-empty"))); connect(del, &QToolButton::clicked, this, &TemporaryData::deleteThumbs); del->setEnabled(false); m_grid->addWidget(del, 3, 4); m_grid->setRowStretch(4, 10); QFrame *sep = new QFrame(this); sep->setFrameShape(QFrame::HLine); m_grid->addWidget(sep, 5, 0, 1, 5); // Current total preview = new QLabel(i18n("Project total cache data"), this); m_grid->addWidget(preview, 6, 0, 1, 3); bool ok; QDir dir = m_doc->getCacheDir(CacheBase, &ok); preview = new QLabel(QStringLiteral("
    ") + dir.absolutePath() + QStringLiteral(""), this); preview->setToolTip(i18n("Click to open cache folder")); connect(preview, &QLabel::linkActivated, this, &TemporaryData::openCacheFolder); m_grid->addWidget(preview, 7, 0, 1, 5); m_currentSize = new QLabel(this); m_grid->addWidget(m_currentSize, 6, 3); del = new QToolButton(this); del->setIcon(QIcon::fromTheme(QStringLiteral("trash-empty"))); connect(del, &QToolButton::clicked, this, &TemporaryData::deleteCurrentCacheData); del->setEnabled(false); m_grid->addWidget(del, 6, 4); m_currentPage->setLayout(m_grid); m_proxies = m_doc->getProxyHashList(); for (int i = 0; i < m_proxies.count(); i++) { m_proxies[i].append(QLatin1Char('*')); } if (currentProjectOnly) { lay->addWidget(m_currentPage); auto *spacer = new QSpacerItem(1, 1, QSizePolicy::Maximum, QSizePolicy::MinimumExpanding); lay->addSpacerItem(spacer); } else { auto *tab = new QTabWidget(this); tab->addTab(m_currentPage, i18n("Current Project")); m_globalPage = new QWidget(this); buildGlobalCacheDialog(minHeight); tab->addTab(m_globalPage, i18n("All Projects")); lay->addWidget(tab); } setLayout(lay); updateDataInfo(); } void TemporaryData::updateDataInfo() { m_totalCurrent = 0; bool ok = false; QDir preview = m_doc->getCacheDir(CacheBase, &ok); if (!ok) { m_currentPage->setEnabled(false); return; } preview = m_doc->getCacheDir(CachePreview, &ok); if (ok) { KIO::DirectorySizeJob *job = KIO::directorySize(QUrl::fromLocalFile(preview.absolutePath())); connect(job, &KIO::DirectorySizeJob::result, this, &TemporaryData::gotPreviewSize); } preview = m_doc->getCacheDir(CacheProxy, &ok); if (ok) { preview.setNameFilters(m_proxies); const QFileInfoList fList = preview.entryInfoList(); KIO::filesize_t size = 0; for (const QFileInfo &info : fList) { size += (uint)info.size(); } gotProxySize(size); } preview = m_doc->getCacheDir(CacheAudio, &ok); if (ok) { KIO::DirectorySizeJob *job = KIO::directorySize(QUrl::fromLocalFile(preview.absolutePath())); connect(job, &KIO::DirectorySizeJob::result, this, &TemporaryData::gotAudioSize); } preview = m_doc->getCacheDir(CacheThumbs, &ok); if (ok) { KIO::DirectorySizeJob *job = KIO::directorySize(QUrl::fromLocalFile(preview.absolutePath())); connect(job, &KIO::DirectorySizeJob::result, this, &TemporaryData::gotThumbSize); } if (m_globalPage) { updateGlobalInfo(); } } void TemporaryData::gotPreviewSize(KJob *job) { - KIO::DirectorySizeJob *sourceJob = static_cast(job); + auto *sourceJob = static_cast(job); KIO::filesize_t total = sourceJob->totalSize(); if (sourceJob->totalFiles() == 0) { total = 0; } QLayoutItem *button = m_grid->itemAtPosition(0, 4); if ((button != nullptr) && (button->widget() != nullptr)) { button->widget()->setEnabled(total > 0); } m_totalCurrent += total; m_currentSizes[0] = total; m_previewSize->setText(KIO::convertSize(total)); updateTotal(); } void TemporaryData::gotProxySize(KIO::filesize_t total) { QLayoutItem *button = m_grid->itemAtPosition(1, 4); if ((button != nullptr) && (button->widget() != nullptr)) { button->widget()->setEnabled(total > 0); } m_totalCurrent += total; m_currentSizes[1] = total; m_proxySize->setText(KIO::convertSize(total)); updateTotal(); } void TemporaryData::gotAudioSize(KJob *job) { - KIO::DirectorySizeJob *sourceJob = static_cast(job); + auto *sourceJob = static_cast(job); KIO::filesize_t total = sourceJob->totalSize(); if (sourceJob->totalFiles() == 0) { total = 0; } QLayoutItem *button = m_grid->itemAtPosition(2, 4); if ((button != nullptr) && (button->widget() != nullptr)) { button->widget()->setEnabled(total > 0); } m_totalCurrent += total; m_currentSizes[2] = total; m_audioSize->setText(KIO::convertSize(total)); updateTotal(); } void TemporaryData::gotThumbSize(KJob *job) { - KIO::DirectorySizeJob *sourceJob = static_cast(job); + auto *sourceJob = static_cast(job); KIO::filesize_t total = sourceJob->totalSize(); if (sourceJob->totalFiles() == 0) { total = 0; } QLayoutItem *button = m_grid->itemAtPosition(3, 4); if ((button != nullptr) && (button->widget() != nullptr)) { button->widget()->setEnabled(total > 0); } m_totalCurrent += total; m_currentSizes[3] = total; m_thumbSize->setText(KIO::convertSize(total)); updateTotal(); } void TemporaryData::updateTotal() { m_currentSize->setText(KIO::convertSize(m_totalCurrent)); QLayoutItem *button = m_grid->itemAtPosition(5, 4); if ((button != nullptr) && (button->widget() != nullptr)) { button->widget()->setEnabled(m_totalCurrent > 0); } QList segments; for (KIO::filesize_t size : m_currentSizes) { if (m_totalCurrent == 0) { segments << 0; } else { segments << static_cast(size * 360 / m_totalCurrent); } } m_currentPie->setSegments(segments); } void TemporaryData::deletePreview() { bool ok = false; QDir dir = m_doc->getCacheDir(CachePreview, &ok); if (!ok) { return; } if (KMessageBox::warningContinueCancel(this, i18n("Delete all data in the cache folder:\n%1", dir.absolutePath())) != KMessageBox::Continue) { return; } if (dir.dirName() == QLatin1String("preview")) { dir.removeRecursively(); dir.mkpath(QStringLiteral(".")); emit disablePreview(); updateDataInfo(); } } void TemporaryData::deleteProxy() { bool ok = false; QDir dir = m_doc->getCacheDir(CacheProxy, &ok); if (!ok || dir.dirName() != QLatin1String("proxy")) { return; } dir.setNameFilters(m_proxies); QStringList files = dir.entryList(QDir::Files); if (KMessageBox::warningContinueCancelList(this, i18n("Delete all project data in the cache proxy folder:\n%1", dir.absolutePath()), files) != KMessageBox::Continue) { return; } for (const QString &file : files) { dir.remove(file); } emit disableProxies(); updateDataInfo(); } void TemporaryData::deleteAudio() { bool ok = false; QDir dir = m_doc->getCacheDir(CacheAudio, &ok); if (!ok) { return; } if (KMessageBox::warningContinueCancel(this, i18n("Delete all data in the cache audio folder:\n%1", dir.absolutePath())) != KMessageBox::Continue) { return; } if (dir.dirName() == QLatin1String("audiothumbs")) { dir.removeRecursively(); dir.mkpath(QStringLiteral(".")); updateDataInfo(); } } void TemporaryData::deleteThumbs() { bool ok = false; QDir dir = m_doc->getCacheDir(CacheThumbs, &ok); if (!ok) { return; } if (KMessageBox::warningContinueCancel(this, i18n("Delete all data in the cache thumbnail folder:\n%1", dir.absolutePath())) != KMessageBox::Continue) { return; } if (dir.dirName() == QLatin1String("videothumbs")) { dir.removeRecursively(); dir.mkpath(QStringLiteral(".")); updateDataInfo(); } } void TemporaryData::deleteCurrentCacheData() { bool ok = false; QDir dir = m_doc->getCacheDir(CacheBase, &ok); if (!ok) { return; } if (KMessageBox::warningContinueCancel(this, i18n("Delete all data in cache folder:\n%1", dir.absolutePath())) != KMessageBox::Continue) { return; } if (dir.dirName() == m_doc->getDocumentProperty(QStringLiteral("documentid"))) { emit disablePreview(); emit disableProxies(); dir.removeRecursively(); m_doc->initCacheDirs(); updateDataInfo(); } } void TemporaryData::openCacheFolder() { bool ok = false; QDir dir = m_doc->getCacheDir(CacheBase, &ok); if (!ok) { return; } QDesktopServices::openUrl(QUrl::fromLocalFile(dir.absolutePath())); } void TemporaryData::buildGlobalCacheDialog(int minHeight) { auto *lay = new QGridLayout; m_globalPie = new ChartWidget(this); lay->addWidget(m_globalPie, 0, 0, 1, 1); m_listWidget = new QTreeWidget(this); m_listWidget->setColumnCount(3); m_listWidget->setHeaderLabels(QStringList() << i18n("Folder") << i18n("Size") << i18n("Date")); m_listWidget->setRootIsDecorated(false); m_listWidget->setAlternatingRowColors(true); m_listWidget->setSortingEnabled(true); m_listWidget->setSelectionMode(QAbstractItemView::ExtendedSelection); lay->addWidget(m_listWidget, 0, 1, 1, 4); m_globalPage->setLayout(lay); // Total Cache data QPalette pal = palette(); QLabel *color = new QLabel(this); color->setFixedSize(minHeight, minHeight); pal.setColor(QPalette::Window, chartColors.at(0)); color->setPalette(pal); color->setAutoFillBackground(true); lay->addWidget(color, 1, 1, Qt::AlignCenter); QLabel *lab = new QLabel(i18n("Total Cached Data"), this); lay->addWidget(lab, 1, 2, 1, 1); m_globalSize = new QLabel(this); lay->addWidget(m_globalSize, 1, 3, 1, 1); // Selection color = new QLabel(this); color->setFixedSize(minHeight, minHeight); pal.setColor(QPalette::Window, chartColors.at(1)); color->setPalette(pal); color->setAutoFillBackground(true); lay->addWidget(color, 2, 1, Qt::AlignCenter); lab = new QLabel(i18n("Selected Cached Data"), this); lay->addWidget(lab, 2, 2, 1, 1); m_selectedSize = new QLabel(this); lay->addWidget(m_selectedSize, 2, 3, 1, 1); m_globalDelete = new QPushButton(i18n("Delete selected cache"), this); connect(m_globalDelete, &QPushButton::clicked, this, &TemporaryData::deleteSelected); lay->addWidget(m_globalDelete, 2, 4, 1, 1); lay->setColumnStretch(4, 10); lay->setRowStretch(0, 10); connect(m_listWidget, &QTreeWidget::itemSelectionChanged, this, &TemporaryData::refreshGlobalPie); } void TemporaryData::updateGlobalInfo() { m_listWidget->blockSignals(true); m_totalGlobal = 0; m_listWidget->clear(); bool ok = false; QDir preview = m_doc->getCacheDir(SystemCacheRoot, &ok); if (!ok) { m_globalPage->setEnabled(false); return; } m_globalDir = preview; m_globalDirectories.clear(); m_processingDirectory.clear(); m_globalDirectories = m_globalDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); m_globalDelete->setEnabled(!m_globalDirectories.isEmpty()); processglobalDirectories(); m_listWidget->blockSignals(false); } void TemporaryData::processglobalDirectories() { if (m_globalDirectories.isEmpty()) { return; } m_processingDirectory = m_globalDirectories.takeFirst(); KIO::DirectorySizeJob *job = KIO::directorySize(QUrl::fromLocalFile(m_globalDir.absoluteFilePath(m_processingDirectory))); connect(job, &KIO::DirectorySizeJob::result, this, &TemporaryData::gotFolderSize); } void TemporaryData::gotFolderSize(KJob *job) { - KIO::DirectorySizeJob *sourceJob = static_cast(job); + auto *sourceJob = static_cast(job); KIO::filesize_t total = sourceJob->totalSize(); if (sourceJob->totalFiles() == 0) { total = 0; } m_totalGlobal += total; auto *item = new TreeWidgetItem(m_listWidget); // Check last save path for this cache folder QDir dir(m_globalDir.absoluteFilePath(m_processingDirectory)); QStringList filters; filters << QStringLiteral("*.kdenlive"); QStringList str = dir.entryList(filters, QDir::Files | QDir::Hidden, QDir::Time); if (!str.isEmpty()) { QString path = QUrl::fromPercentEncoding(str.at(0).toUtf8()); // Remove leading dot path.remove(0, 1); item->setText(0, m_processingDirectory + QStringLiteral(" (%1)").arg(QUrl::fromLocalFile(path).fileName())); if (QFile::exists(path)) { item->setIcon(0, QIcon::fromTheme(QStringLiteral("kdenlive"))); } else { item->setIcon(0, QIcon::fromTheme(QStringLiteral("dialog-close"))); } } else { item->setText(0, m_processingDirectory); if (m_processingDirectory == QLatin1String("proxy")) { item->setIcon(0, QIcon::fromTheme(QStringLiteral("kdenlive-show-video"))); } } item->setData(0, Qt::UserRole, m_processingDirectory); item->setText(1, KIO::convertSize(total)); QDateTime date = QFileInfo(dir.absolutePath()).lastModified(); item->setText(2, date.toString(Qt::SystemLocaleShortDate)); item->setData(1, Qt::UserRole, total); item->setData(2, Qt::UserRole, date); m_listWidget->addTopLevelItem(item); m_listWidget->resizeColumnToContents(0); m_listWidget->resizeColumnToContents(1); if (m_globalDirectories.isEmpty()) { m_globalSize->setText(KIO::convertSize(m_totalGlobal)); m_listWidget->setCurrentItem(m_listWidget->topLevelItem(0)); } else { processglobalDirectories(); } } void TemporaryData::refreshGlobalPie() { QList list = m_listWidget->selectedItems(); KIO::filesize_t currentSize = 0; for (QTreeWidgetItem *current : list) { if (current) { currentSize += current->data(1, Qt::UserRole).toULongLong(); } } m_selectedSize->setText(KIO::convertSize(currentSize)); int percent = m_totalGlobal <= 0 ? 0 : (int)(currentSize * 360 / m_totalGlobal); m_globalPie->setSegments(QList() << 360 << percent); if (list.size() == 1 && list.at(0)->text(0) == m_doc->getDocumentProperty(QStringLiteral("documentid"))) { m_globalDelete->setText(i18n("Clear current cache")); } else { m_globalDelete->setText(i18n("Delete selected cache")); } } void TemporaryData::deleteSelected() { QList list = m_listWidget->selectedItems(); QStringList folders; for (QTreeWidgetItem *current : list) { if (current) { folders << current->data(0, Qt::UserRole).toString(); } } if (KMessageBox::warningContinueCancelList(this, i18n("Delete the following cache folders from\n%1", m_globalDir.absolutePath()), folders) != KMessageBox::Continue) { return; } const QString currentId = m_doc->getDocumentProperty(QStringLiteral("documentid")); for (const QString &folder : folders) { if (folder == currentId) { // Trying to delete current project's tmp folder. Do not delete, but clear it deleteCurrentCacheData(); continue; } QDir toRemove(m_globalDir.absoluteFilePath(folder)); toRemove.removeRecursively(); if (folder == QLatin1String("proxy")) { // We deleted proxy folder, recreate it toRemove.mkpath(QStringLiteral(".")); } } updateGlobalInfo(); } diff --git a/src/project/invaliddialog.h b/src/project/invaliddialog.h index 7cb8da595..1b1d49556 100644 --- a/src/project/invaliddialog.h +++ b/src/project/invaliddialog.h @@ -1,43 +1,43 @@ /*************************************************************************** * Copyright (C) 2007 by Jean-Baptiste Mardelle (jb@kdenlive.org) * * 2013 by Jean-Nicolas Artaud (jeannicolasartaud@gmail.com) * * * * 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 INVALIDDIALOG_H #define INVALIDDIALOG_H #include class QListWidget; class InvalidDialog : public QDialog { Q_OBJECT public: explicit InvalidDialog(const QString &caption, const QString &message, bool infoOnly, QWidget *parent = nullptr); - ~InvalidDialog(); + ~InvalidDialog() override; void addClip(const QString &id, const QString &path); QStringList getIds() const; private: QListWidget *m_clipList; }; #endif // INVALIDDIALOG_H diff --git a/src/project/projectcommands.cpp b/src/project/projectcommands.cpp index f613da460..b97887e7e 100644 --- a/src/project/projectcommands.cpp +++ b/src/project/projectcommands.cpp @@ -1,155 +1,153 @@ /*************************************************************************** * Copyright (C) 2008 by Jean-Baptiste Mardelle (jb@kdenlive.org) * * Copyright (C) 2014 by Vincent Pinon (vpinon@april.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 "projectcommands.h" #include "bin/projectclip.h" #include "doc/kdenlivedoc.h" #include - -AddClipCutCommand::AddClipCutCommand(ProjectList *list, const QString &id, int in, int out, const QString &desc, bool newItem, bool remove, - QUndoCommand *parent) +#include +AddClipCutCommand::AddClipCutCommand(ProjectList *list, QString id, int in, int out, QString desc, bool newItem, bool remove, QUndoCommand *parent) : QUndoCommand(parent) , m_list(list) - , m_id(id) + , m_id(std::move(id)) , m_in(in) , m_out(out) - , m_desc(desc) + , m_desc(std::move(desc)) , m_newItem(newItem) , m_remove(remove) { setText(i18n("Add clip cut")); } // virtual void AddClipCutCommand::undo() { Q_UNUSED(m_list) Q_UNUSED(m_id) Q_UNUSED(m_in) Q_UNUSED(m_out) Q_UNUSED(m_desc) Q_UNUSED(m_newItem) Q_UNUSED(m_remove) /*if (m_remove) m_list->addClipCut(m_id, m_in, m_out, m_desc, m_newItem); else m_list->removeClipCut(m_id, m_in, m_out);*/ } // virtual void AddClipCutCommand::redo() { /*if (m_remove) m_list->removeClipCut(m_id, m_in, m_out); else m_list->addClipCut(m_id, m_in, m_out, m_desc, m_newItem);*/ } -AddFolderCommand::AddFolderCommand(ProjectList *view, const QString &folderName, const QString &clipId, bool doIt, QUndoCommand *parent) +AddFolderCommand::AddFolderCommand(ProjectList *view, QString folderName, QString clipId, bool doIt, QUndoCommand *parent) : QUndoCommand(parent) , m_view(view) - , m_name(folderName) - , m_id(clipId) + , m_name(std::move(folderName)) + , m_id(std::move(clipId)) , m_doIt(doIt) { if (doIt) { setText(i18n("Add folder")); } else { setText(i18n("Delete folder")); } } // virtual void AddFolderCommand::undo() { Q_UNUSED(m_view) Q_UNUSED(m_name) Q_UNUSED(m_id) Q_UNUSED(m_doIt) /*if (m_doIt) m_view->slotAddFolder(m_name, m_id, true); else m_view->slotAddFolder(m_name, m_id, false);*/ } // virtual void AddFolderCommand::redo() { /*if (m_doIt) m_view->slotAddFolder(m_name, m_id, false); else m_view->slotAddFolder(m_name, m_id, true);*/ } -EditClipCutCommand::EditClipCutCommand(ProjectList *list, const QString &id, const QPoint &oldZone, const QPoint &newZone, const QString &oldComment, - const QString &newComment, bool doIt, QUndoCommand *parent) +EditClipCutCommand::EditClipCutCommand(ProjectList *list, QString id, const QPoint &oldZone, const QPoint &newZone, QString oldComment, QString newComment, + bool doIt, QUndoCommand *parent) : QUndoCommand(parent) , m_list(list) - , m_id(id) + , m_id(std::move(id)) , m_oldZone(oldZone) , m_newZone(newZone) - , m_oldComment(oldComment) - , m_newComment(newComment) + , m_oldComment(std::move(oldComment)) + , m_newComment(std::move(newComment)) , m_doIt(doIt) { setText(i18n("Edit clip cut")); } // virtual void EditClipCutCommand::undo() { Q_UNUSED(m_list) Q_UNUSED(m_id) Q_UNUSED(m_oldZone) Q_UNUSED(m_newZone) Q_UNUSED(m_oldComment) Q_UNUSED(m_newComment) Q_UNUSED(m_doIt) // m_list->doUpdateClipCut(m_id, m_newZone, m_oldZone, m_oldComment); } // virtual void EditClipCutCommand::redo() { /*if (m_doIt) m_list->doUpdateClipCut(m_id, m_oldZone, m_newZone, m_newComment); */ } -EditFolderCommand::EditFolderCommand(ProjectList *view, const QString &newfolderName, const QString &oldfolderName, const QString &clipId, bool doIt, - QUndoCommand *parent) +EditFolderCommand::EditFolderCommand(ProjectList *view, QString newfolderName, QString oldfolderName, QString clipId, bool doIt, QUndoCommand *parent) : QUndoCommand(parent) , m_view(view) - , m_name(newfolderName) - , m_oldname(oldfolderName) - , m_id(clipId) + , m_name(std::move(newfolderName)) + , m_oldname(std::move(oldfolderName)) + , m_id(std::move(clipId)) , m_doIt(doIt) { setText(i18n("Rename folder")); } // virtual void EditFolderCommand::undo() { Q_UNUSED(m_view) Q_UNUSED(m_name) Q_UNUSED(m_oldname) Q_UNUSED(m_id) Q_UNUSED(m_doIt) // m_view->slotAddFolder(m_oldname, m_id, false, true); } // virtual void EditFolderCommand::redo() { // if (m_doIt) m_view->slotAddFolder(m_name, m_id, false, true); } diff --git a/src/project/projectcommands.h b/src/project/projectcommands.h index bdaeccd95..61ce821e7 100644 --- a/src/project/projectcommands.h +++ b/src/project/projectcommands.h @@ -1,96 +1,95 @@ /*************************************************************************** * 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 PROJECTCOMMANDS_H #define PROJECTCOMMANDS_H #include "definitions.h" #include #include class ProjectList; class ProjectClip; class AddClipCutCommand : public QUndoCommand { public: - AddClipCutCommand(ProjectList *list, const QString &id, int in, int out, const QString &desc, bool newItem, bool remove, QUndoCommand *parent = nullptr); + AddClipCutCommand(ProjectList *list, QString id, int in, int out, QString desc, bool newItem, bool remove, QUndoCommand *parent = nullptr); void undo() override; void redo() override; private: ProjectList *m_list; QString m_id; int m_in; int m_out; QString m_desc; bool m_newItem; bool m_remove; }; class AddFolderCommand : public QUndoCommand { public: - AddFolderCommand(ProjectList *view, const QString &folderName, const QString &clipId, bool doIt, QUndoCommand *parent = nullptr); + AddFolderCommand(ProjectList *view, QString folderName, QString clipId, bool doIt, QUndoCommand *parent = nullptr); void undo() override; void redo() override; private: ProjectList *m_view; QString m_name; QString m_id; bool m_doIt; }; class EditClipCutCommand : public QUndoCommand { public: - EditClipCutCommand(ProjectList *list, const QString &id, const QPoint &oldZone, const QPoint &newZone, const QString &oldComment, const QString &newComment, - bool doIt, QUndoCommand *parent = nullptr); + EditClipCutCommand(ProjectList *list, QString id, const QPoint &oldZone, const QPoint &newZone, QString oldComment, QString newComment, bool doIt, + QUndoCommand *parent = nullptr); void undo() override; void redo() override; private: ProjectList *m_list; QString m_id; QPoint m_oldZone; QPoint m_newZone; QString m_oldComment; QString m_newComment; bool m_doIt; }; class EditFolderCommand : public QUndoCommand { public: - EditFolderCommand(ProjectList *view, const QString &newfolderName, const QString &oldfolderName, const QString &clipId, bool doIt, - QUndoCommand *parent = nullptr); + EditFolderCommand(ProjectList *view, QString newfolderName, QString oldfolderName, QString clipId, bool doIt, QUndoCommand *parent = nullptr); void undo() override; void redo() override; private: ProjectList *m_view; QString m_name; QString m_oldname; QString m_id; bool m_doIt; }; #endif diff --git a/src/project/projectmanager.cpp b/src/project/projectmanager.cpp index bffa89dea..928cf328d 100644 --- a/src/project/projectmanager.cpp +++ b/src/project/projectmanager.cpp @@ -1,958 +1,957 @@ /* 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() {} +ProjectManager::~ProjectManager() = default; 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); + auto *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) { 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) { // 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 d535a2ec5..f8661e1a5 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(); + ~ProjectManager() override; /** @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); 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); /** @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; + KdenliveDoc *m_project{nullptr}; 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; + QProgressDialog *m_progressDialog{nullptr}; void saveRecentFiles(); }; #endif diff --git a/src/qt-oauth-lib/logindialog.h b/src/qt-oauth-lib/logindialog.h index fed881b45..963b38ec5 100644 --- a/src/qt-oauth-lib/logindialog.h +++ b/src/qt-oauth-lib/logindialog.h @@ -1,97 +1,97 @@ /******************************************************************************************************** * Copyright (C) 2015 Roger Morton (ttguy1@gmail.com) * * Purpose: implements client access to freesound.org using ver2 of the freesound API. * * Based on code at https://code.google.com/p/qt-oauth-lib/ * Which is Qt Library created by Integrated Computer Solutions, Inc. (ICS) * to provide OAuth2.0 for the Google API. * * Licence: GNU Lesser General Public License * http://www.gnu.org/licenses/lgpl.html * This version of the GNU Lesser General Public License incorporates the terms * and conditions of version 3 of the GNU General Public License http://www.gnu.org/licenses/gpl-3.0-standalone.html * supplemented by the additional permissions listed at http://www.gnu.org/licenses/lgpl.html * * Disclaimer of Warranty. * THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. * EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE * THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. * SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR * OR CORRECTION. * * Limitation of Liability. * IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, * OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO * YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING * OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR * DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF * THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN * ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * *********************************************************************************************************/ #ifndef LOGINDIALOG_H #define LOGINDIALOG_H #include #include #include namespace Ui { class LoginDialog; } /** \brief This is the dialog that is used to login to freesound \details It contains a QWebView object to display the freesound web page. I did try using a QTextBrowser for this purpose but it responds to the URL that is used to connect with "No document for https://www.freesound.org/apiv2/oauth2/authorize/?client_id=3duhagdr874c&redirect_uri=https://www.freesound.org/home/app_permissions/permission_granted/&response_type=code" The use of QWebView adds a dependency on the KF5WebKit to kdenlive. Need install libkf5webkit5-dev package on ubuntu */ class LoginDialog : public QDialog { Q_OBJECT public: explicit LoginDialog(QWidget *parent = nullptr); - ~LoginDialog(); + ~LoginDialog() override; void setLoginUrl(const QUrl &url); QString authCode() const; signals: /** * @brief authCodeObtained - emitted when freesound gives us an Authorisation code \n * Authorisation codes last 10mins and must be exchanged for an access token in that time */ void authCodeObtained(); /** * @brief accessDenied -signal emitted if freesound denies access - eg bad password or user has denied access to Kdenlive app. */ void accessDenied(); /** * @brief canceled - signal emitted when user clicks cancel button in the logon dialog */ void canceled(); /** * @brief useHQPreview - signal emitted when user clicks the "use HQ preview" button in the logon dialog */ void useHQPreview(); private slots: void urlChanged(const QUrl &url); void slotGetHQPreview(); void slotRejected(); private: Ui::LoginDialog *m_ui; QString m_strAuthCode; }; #endif // LOGINDIALOG_H diff --git a/src/scopes/abstractscopewidget.cpp b/src/scopes/abstractscopewidget.cpp index 41fb204e6..85ab63a4f 100644 --- a/src/scopes/abstractscopewidget.cpp +++ b/src/scopes/abstractscopewidget.cpp @@ -1,555 +1,542 @@ /*************************************************************************** * 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) - , m_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 (std::fabs(diff) > m_rescaleVerticalThreshold || movement.x() == 0) { m_rescaleDirection = North; } 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 std::ceil((float)oldMseconds * REALTIME_FPS / 1000); } uint AbstractScopeWidget::calculateAccelFactorScope(uint oldMseconds, uint) { return std::ceil((float)oldMseconds * REALTIME_FPS / 1000); } uint AbstractScopeWidget::calculateAccelFactorBackground(uint oldMseconds, uint) { 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/abstractscopewidget.h b/src/scopes/abstractscopewidget.h index 5611747d1..e437f2e77 100644 --- a/src/scopes/abstractscopewidget.h +++ b/src/scopes/abstractscopewidget.h @@ -1,301 +1,301 @@ /*************************************************************************** * 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. * ***************************************************************************/ #ifndef ABSTRACTSCOPEWIDGET_H #define ABSTRACTSCOPEWIDGET_H #include #include #include #include /** \brief Abstract class for audio/colour scopes (receive data and paint it). This abstract widget is a proof that abstract things sometimes \b *are* useful. The widget expects three layers which \li Will be painted on top of each other on each update \li Are rendered in a separate thread so that the UI is not blocked \li Are rendered only if necessary (e.g., if a layer does not depend on input images, it will not be re-rendered for incoming frames) The layer order is as follows: \verbatim _____________________ / \ / HUD Layer \ / \ --------------------------- _____________________ / \ / Scope Layer \ / \ --------------------------- _____________________ / \ / Background Layer \ / \ --------------------------- \endverbatim Colors of Scope Widgets are defined in here (and thus don't need to be re-defined in the implementation of the widget's .ui file). The custom context menu already contains entries, like for enabling auto- refresh. It can certainly be extended in the implementation of the widget. If you intend to write an own widget inheriting from this one, please read the comments on the unimplemented methods carefully. They are not only here for optical amusement, but also contain important information. */ class AbstractScopeWidget : public QWidget { Q_OBJECT public: /** \param trackMouse enables mouse tracking; The variables m_mousePos and m_mouseWithinWidget will be set if mouse tracking is enabled. \see signalMousePositionChanged(): Emitted when mouse tracking is enabled */ explicit AbstractScopeWidget(bool trackMouse = false, QWidget *parent = nullptr); - virtual ~AbstractScopeWidget(); // Must be virtual because of inheritance, to avoid memory leaks + ~AbstractScopeWidget() override; // Must be virtual because of inheritance, to avoid memory leaks enum RescaleDirection { North, Northeast, East, Southeast }; QPalette m_scopePalette; /** Initializes widget settings (reads configuration). Has to be called in the implementing object. */ virtual void init(); /** Tell whether this scope has auto-refresh enabled. Required for determining whether new data (e.g. an image frame) has to be delivered to this widget. */ bool autoRefreshEnabled() const; bool needsSingleFrame(); ///// Unimplemented ///// virtual QString widgetName() const = 0; ///// Variables ///// static const QColor colHighlightLight; static const QColor colHighlightDark; static const QColor colDarkWhite; static const QPen penThick; static const QPen penThin; static const QPen penLight; static const QPen penLightDots; static const QPen penLighter; static const QPen penDark; static const QPen penDarkDots; static const QPen penBackground; static const QString directions[]; // Mainly for debug output protected: ///// Variables ///// /** The context menu. Feel free to add new entries in your implementation. */ QMenu *m_menu; /** Enables auto refreshing of the scope. This is when fresh data is incoming. Resize events always force a recalculation. */ QAction *m_aAutoRefresh; /** Realtime rendering. Should be disabled if it is not supported. Use the accelerationFactor variable passed to the render functions as a hint of how many times faster the scope should be calculated. */ QAction *m_aRealtime; /** The mouse position; Updated when the mouse enters the widget AND mouse tracking has been enabled. */ QPoint m_mousePos; /** Knows whether the mouse currently lies within the widget or not. Can e.g. be used for drawing a HUD only when the mouse is in the widget. */ - bool m_mouseWithinWidget; + bool m_mouseWithinWidget{false}; /** Offset from the widget's borders */ - const uchar offset; + const uchar offset{5}; /** The rect on the widget we're painting in. Can be used by the implementing widget, e.g. in the render methods. Is updated when necessary (size changes). */ QRect m_scopeRect; /** Images storing the calculated layers. Will be used on repaint events. */ QImage m_imgHUD; QImage m_imgScope; QImage m_imgBackground; /** The acceleration factors can be accessed also by other renderer tasks, e.g. to display the scope's acceleration factor in the HUD renderer. */ - int m_accelFactorHUD; - int m_accelFactorScope; - int m_accelFactorBackground; + int m_accelFactorHUD{1}; + int m_accelFactorScope{1}; + int m_accelFactorBackground{1}; /** Reads the widget's configuration. Can be extended in the implementing subclass (make sure to run readConfig as well). */ virtual void readConfig(); /** Writes the widget configuration. Implementing widgets have to implement an own method and run it in their destructor. */ void writeConfig(); /** Identifier for the widget's configuration. */ QString configName(); ///// Unimplemented Methods ///// /** Where on the widget we can paint in. May also update other variables, like m_scopeRect or custom ones, that have to change together with the widget's size. */ virtual QRect scopeRect() = 0; /** @brief HUD renderer. Must emit signalHUDRenderingFinished(). @see renderScope(uint). */ virtual QImage renderHUD(uint accelerationFactor) = 0; /** @brief Rendering function for the scope layer. This function \b must emit signalScopeRenderingFinished(), otherwise the scope will not attempt to ever call this function again. This signal is required for multi-threading; not emitting it on unused rendering function may increase performance. @param accelerationFactor hints how much faster than usual the calculation should be accomplished, if possible. @see renderHUD(uint) for the upper layer @see renderBackground(uint) for the layer below */ virtual QImage renderScope(uint accelerationFactor) = 0; /** @brief Background renderer. Must emit signalBackgroundRenderingFinished(). @see renderScope(uint) */ virtual QImage renderBackground(uint accelerationFactor) = 0; /** Must return true if the HUD layer depends on the input data. If it does not, then it does not need to be re-calculated when fresh data is incoming. */ virtual bool isHUDDependingOnInput() const = 0; /** @see isHUDDependingOnInput() */ virtual bool isScopeDependingOnInput() const = 0; /** @see isHUDDependingOnInput() */ virtual bool isBackgroundDependingOnInput() const = 0; ///// Can be reimplemented ///// /** Calculates the acceleration factor to be used by the render thread. This method can be refined in the subclass if required. */ virtual uint calculateAccelFactorHUD(uint oldMseconds, uint oldFactor); virtual uint calculateAccelFactorScope(uint oldMseconds, uint oldFactor); virtual uint calculateAccelFactorBackground(uint oldMseconds, uint oldFactor); /** The Abstract Scope will try to detect the movement direction when dragging on the widget with the mouse. As soon as the direction is determined it will execute this method. Can be used e.g. for re-scaling content. This is just a dummy function, re-implement to add functionality. */ virtual void handleMouseDrag(const QPoint &movement, const RescaleDirection rescaleDirection, const Qt::KeyboardModifiers rescaleModifiers); ///// Reimplemented ///// void mouseMoveEvent(QMouseEvent *event) override; void mousePressEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; void leaveEvent(QEvent *) override; void paintEvent(QPaintEvent *) override; void resizeEvent(QResizeEvent *) override; void showEvent(QShowEvent *) override; // Called when the widget is activated via the Menu entry // void raise(); // Called only when manually calling the event -> useless public slots: /** Forces an update of all layers. */ void forceUpdate(bool doUpdate = true); void forceUpdateHUD(); void forceUpdateScope(); void forceUpdateBackground(); protected slots: void slotAutoRefreshToggled(bool); signals: /** \param mseconds represents the time taken for the calculation. \param accelerationFactor is the acceleration factor that has been used for this calculation. */ void signalHUDRenderingFinished(uint mseconds, uint accelerationFactor); void signalScopeRenderingFinished(uint mseconds, uint accelerationFactor); void signalBackgroundRenderingFinished(uint mseconds, uint accelerationFactor); /** For the mouse position itself see m_mousePos. To check whether the mouse has leaved the widget, see m_mouseWithinWidget. This signal is typically connected to forceUpdateHUD(). */ void signalMousePositionChanged(); /** Do we need the renderer to send its frames to us? Emitted when auto-refresh is toggled. */ void requestAutoRefresh(bool); private: /** Counts the number of data frames that have been rendered in the active monitor. The frame number will be reset when the calculation starts for the current data set. */ QAtomicInt m_newHUDFrames; QAtomicInt m_newScopeFrames; QAtomicInt m_newBackgroundFrames; /** Counts the number of updates that, unlike new frames, force a recalculation of the scope, like for example a resize event. */ QAtomicInt m_newHUDUpdates; QAtomicInt m_newScopeUpdates; QAtomicInt m_newBackgroundUpdates; /** The semaphores ensure that the QFutures for the HUD/Scope/Background threads cannot be assigned a new thread while it is still running. (Could cause deadlocks and other nasty things known from parallelism.) */ QSemaphore m_semaphoreHUD; QSemaphore m_semaphoreScope; QSemaphore m_semaphoreBackground; QFuture m_threadHUD; QFuture m_threadScope; QFuture m_threadBackground; - bool m_initialDimensionUpdateDone; - bool m_requestForcedUpdate; + bool m_initialDimensionUpdateDone{false}; + bool m_requestForcedUpdate{false}; QImage m_scopeImage; QString m_widgetName; void prodHUDThread(); void prodScopeThread(); void prodBackgroundThread(); ///// Movement detection ///// - const int m_rescaleMinDist; - const float m_rescaleVerticalThreshold; + const int m_rescaleMinDist{4}; + const float m_rescaleVerticalThreshold{2.0f}; - bool m_rescaleActive; - bool m_rescalePropertiesLocked; - bool m_rescaleFirstRescaleDone; + bool m_rescaleActive{false}; + bool m_rescalePropertiesLocked{false}; + bool m_rescaleFirstRescaleDone{true}; Qt::KeyboardModifiers m_rescaleModifiers; - RescaleDirection m_rescaleDirection; + RescaleDirection m_rescaleDirection{North}; QPoint m_rescaleStartPoint; protected slots: void slotContextMenuRequested(const QPoint &pos); /** To be called when a new frame has been received. The scope then decides whether and when it wants to recalculate the scope, depending on whether it is currently visible and whether a calculation thread is already running. */ void slotRenderZoneUpdated(); /** The following slots are called when rendering of a component has finished. They e.g. update the widget and decide whether to immediately restart the calculation thread. */ void slotHUDRenderingFinished(uint mseconds, uint accelerationFactor); void slotScopeRenderingFinished(uint mseconds, uint accelerationFactor); void slotBackgroundRenderingFinished(uint mseconds, uint accelerationFactor); /** Resets the acceleration factors to 1 when realtime rendering is disabled. */ void slotResetRealtimeFactor(bool realtimeChecked); }; #endif // ABSTRACTSCOPEWIDGET_H diff --git a/src/scopes/audioscopes/abstractaudioscopewidget.cpp b/src/scopes/audioscopes/abstractaudioscopewidget.cpp index 30eafb87a..5cccb757c 100644 --- a/src/scopes/audioscopes/abstractaudioscopewidget.cpp +++ b/src/scopes/audioscopes/abstractaudioscopewidget.cpp @@ -1,58 +1,55 @@ /*************************************************************************** * 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 "abstractaudioscopewidget.h" #include "monitor/monitor.h" // Uncomment for debugging //#define DEBUG_AASW #ifdef DEBUG_AASW #include "kdenlive_debug.h" #endif AbstractAudioScopeWidget::AbstractAudioScopeWidget(bool trackMouse, QWidget *parent) : AbstractScopeWidget(trackMouse, parent) - , m_freq(0) - , m_nChannels(0) - , m_nSamples(0) , m_audioFrame() , m_newData(0) { } void AbstractAudioScopeWidget::slotReceiveAudio(const audioShortVector &sampleData, int freq, int num_channels, int num_samples) { #ifdef DEBUG_AASW qCDebug(KDENLIVE_LOG) << "Received audio for " << widgetName() << '.'; #endif m_audioFrame = sampleData; m_freq = freq; m_nChannels = num_channels; m_nSamples = num_samples; m_newData.fetchAndAddAcquire(1); AbstractScopeWidget::slotRenderZoneUpdated(); } AbstractAudioScopeWidget::~AbstractAudioScopeWidget() = default; QImage AbstractAudioScopeWidget::renderScope(uint accelerationFactor) { const int newData = m_newData.fetchAndStoreAcquire(0); return renderAudioScope(accelerationFactor, m_audioFrame, m_freq, m_nChannels, m_nSamples, newData); } #ifdef DEBUG_AASW #undef DEBUG_AASW #endif diff --git a/src/scopes/audioscopes/abstractaudioscopewidget.h b/src/scopes/audioscopes/abstractaudioscopewidget.h index 209809e17..b9b990dc2 100644 --- a/src/scopes/audioscopes/abstractaudioscopewidget.h +++ b/src/scopes/audioscopes/abstractaudioscopewidget.h @@ -1,56 +1,56 @@ /*************************************************************************** * 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. * ***************************************************************************/ #ifndef ABSTRACTAUDIOSCOPEWIDGET_H #define ABSTRACTAUDIOSCOPEWIDGET_H #include -#include +#include #include "../../definitions.h" #include "../abstractscopewidget.h" class Render; /** \brief Abstract class for scopes analyzing audio samples. */ class AbstractAudioScopeWidget : public AbstractScopeWidget { Q_OBJECT public: explicit AbstractAudioScopeWidget(bool trackMouse = false, QWidget *parent = nullptr); - virtual ~AbstractAudioScopeWidget(); + ~AbstractAudioScopeWidget() override; public slots: void slotReceiveAudio(const audioShortVector &sampleData, int freq, int num_channels, int num_samples); protected: /** @brief This is just a wrapper function, subclasses can use renderAudioScope. */ QImage renderScope(uint accelerationFactor) override; ///// Unimplemented Methods ///// /** @brief Scope renderer. Must emit signalScopeRenderingFinished() when calculation has finished, to allow multi-threading. accelerationFactor hints how much faster than usual the calculation should be accomplished, if possible. */ virtual QImage renderAudioScope(uint accelerationFactor, const audioShortVector &audioFrame, const int freq, const int num_channels, const int num_samples, const int newData) = 0; - int m_freq; - int m_nChannels; - int m_nSamples; + int m_freq{0}; + int m_nChannels{0}; + int m_nSamples{0}; private: audioShortVector m_audioFrame; QAtomicInt m_newData; }; #endif // ABSTRACTAUDIOSCOPEWIDGET_H diff --git a/src/scopes/audioscopes/audiosignal.cpp b/src/scopes/audioscopes/audiosignal.cpp index bb5444ba8..fb5a222c6 100644 --- a/src/scopes/audioscopes/audiosignal.cpp +++ b/src/scopes/audioscopes/audiosignal.cpp @@ -1,225 +1,225 @@ /*************************************************************************** * Copyright (C) 2010 by Marco Gittler (g.marco@freenet.de) * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * 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 "audiosignal.h" #include #include -#include +#include AudioSignal::AudioSignal(QWidget *parent) : AbstractAudioScopeWidget(false, parent) { setMinimumHeight(10); setMinimumWidth(10); m_dbscale << 0 << -1 << -2 << -3 << -4 << -5 << -6 << -8 << -10 << -20 << -40; m_menu->removeAction(m_aRealtime); connect(&m_timer, &QTimer::timeout, this, &AudioSignal::slotNoAudioTimeout); init(); } AudioSignal::~AudioSignal() = default; QImage AudioSignal::renderAudioScope(uint, const audioShortVector &audioFrame, const int, const int num_channels, const int samples, const int) { QTime start = QTime::currentTime(); int num_samples = samples > 200 ? 200 : samples; QByteArray chanAvg; for (int i = 0; i < num_channels; ++i) { long val = 0; for (int s = 0; s < num_samples; s++) { val += abs(audioFrame[i + s * num_channels] / 128); } chanAvg.append(char(val / num_samples)); } if (m_peeks.count() != chanAvg.count()) { m_peeks = QByteArray(chanAvg.count(), 0); m_peekage = QByteArray(chanAvg.count(), 0); } for (int chan = 0; chan < m_peeks.count(); chan++) { m_peekage[chan] = char(m_peekage[chan] + 1); if (m_peeks.at(chan) < chanAvg.at(chan) || m_peekage.at(chan) > 50) { m_peekage[chan] = 0; m_peeks[chan] = chanAvg[chan]; } } QImage image(m_scopeRect.size(), QImage::Format_ARGB32); image.fill(Qt::transparent); QPainter p(&image); p.setPen(Qt::white); p.setRenderHint(QPainter::TextAntialiasing, false); p.setRenderHint(QPainter::Antialiasing, false); int numchan = chanAvg.size(); bool horiz = width() > height(); int dbsize = 20; if (!horiz) { // calculate actual width of lowest=longest db scale mark based on drawing font dbsize = p.fontMetrics().width(QString().sprintf("%d", m_dbscale.at(m_dbscale.size() - 1))); } bool showdb = width() > (dbsize + 40); // valpixel=1.0 for 127, 1.0+(1/40) for 1 short oversample, 1.0+(2/40) for longer oversample for (int i = 0; i < numchan; ++i) { // int maxx= (unsigned char)m_channels[i] * (horiz ? width() : height() ) / 127; double valpixel = valueToPixel((double)(unsigned char)chanAvg[i] / 127.0); int maxx = height() * valpixel; int xdelta = height() / 42; int _y2 = (showdb ? width() - dbsize : width()) / numchan - 1; int _y1 = (showdb ? width() - dbsize : width()) * i / numchan; int _x2 = maxx > xdelta ? xdelta - 3 : maxx - 3; if (horiz) { dbsize = 9; showdb = height() > (dbsize); maxx = width() * valpixel; xdelta = width() / 42; _y2 = (showdb ? height() - dbsize : height()) / numchan - 1; _y1 = (showdb ? height() - dbsize : height()) * i / numchan; _x2 = maxx > xdelta ? xdelta - 1 : maxx - 1; } for (int x = 0; x <= 42; ++x) { int _x1 = x * xdelta; QColor sig = Qt::green; // value of actual painted digit double ival = (double)_x1 / (double)xdelta / 42.0; if (ival > 40.0 / 42.0) { sig = Qt::red; } else if (ival > 37.0 / 42.0) { sig = Qt::darkYellow; } else if (ival > 30.0 / 42.0) { sig = Qt::yellow; } if (maxx > 0) { if (horiz) { p.fillRect(_x1, _y1, _x2, _y2, QBrush(sig, Qt::SolidPattern)); } else { p.fillRect(_y1, height() - _x1, _y2, -_x2, QBrush(sig, Qt::SolidPattern)); } maxx -= xdelta; } } int xp = valueToPixel((double)m_peeks.at(i) / 127.0) * (horiz ? width() : height()) - 2; p.fillRect(horiz ? xp : _y1, horiz ? _y1 : height() - xdelta - xp, horiz ? 3 : _y2, horiz ? _y2 : 3, QBrush(Qt::gray, Qt::SolidPattern)); } if (showdb) { // draw db value at related pixel - for (int l = 0; l < m_dbscale.size(); l++) { + for (int l : m_dbscale) { if (!horiz) { - double xf = pow(10.0, (double)m_dbscale.at(l) / 20.0) * (double)height(); - p.drawText(width() - dbsize, height() - xf * 40.0 / 42.0 + 20, QString().sprintf("%d", m_dbscale.at(l))); + double xf = pow(10.0, (double)l / 20.0) * (double)height(); + p.drawText(width() - dbsize, height() - xf * 40.0 / 42.0 + 20, QString().sprintf("%d", l)); } else { - double xf = pow(10.0, (double)m_dbscale.at(l) / 20.0) * (double)width(); - p.drawText(xf * 40 / 42 - 10, height() - 2, QString().sprintf("%d", m_dbscale.at(l))); + double xf = pow(10.0, (double)l / 20.0) * (double)width(); + p.drawText(xf * 40 / 42 - 10, height() - 2, QString().sprintf("%d", l)); } } } p.end(); emit signalScopeRenderingFinished((uint)start.elapsed(), 1); return image; } QRect AudioSignal::scopeRect() { - return QRect(0, 0, width(), height()); + return {0, 0, width(), height()}; } QImage AudioSignal::renderHUD(uint) { return QImage(); } QImage AudioSignal::renderBackground(uint) { return QImage(); } void AudioSignal::slotReceiveAudio(audioShortVector audioSamples, int, int num_channels, int samples) { int num_samples = samples > 200 ? 200 : samples; QByteArray chanSignal; int num_oversample = 0; for (int i = 0; i < num_channels; ++i) { long val = 0; double over1 = 0.0; double over2 = 0.0; for (int s = 0; s < num_samples; s++) { int sample = abs(audioSamples[i + s * num_channels] / 128); val += sample; if (sample == 128) { num_oversample++; } else { num_oversample = 0; } // if 3 samples over max => 1 peak over 0 db (0db=40.0) if (num_oversample > 3) { over1 = 41.0 / 42.0 * 127; } // 10 samples @max => show max signal if (num_oversample > 10) { over2 = 127; } } // max amplitude = 40/42, 3to10 oversamples=41, more then 10 oversamples=42 if (over2 > 0.0) { chanSignal.append(over2); } else if (over1 > 0.0) { chanSignal.append(over1); } else { chanSignal.append(char((double)val / (double)num_samples * 40.0 / 42.0)); } } showAudio(chanSignal); m_timer.start(1000); } void AudioSignal::slotNoAudioTimeout() { m_peeks.fill(0); showAudio(QByteArray(2, 0)); m_timer.stop(); } void AudioSignal::showAudio(const QByteArray &arr) { m_channels = arr; if (m_peeks.count() != m_channels.count()) { m_peeks = QByteArray(m_channels.count(), 0); m_peekage = QByteArray(m_channels.count(), 0); } for (int chan = 0; chan < m_peeks.count(); chan++) { m_peekage[chan] = char(m_peekage[chan] + 1); if (m_peeks.at(chan) < arr.at(chan) || m_peekage.at(chan) > 50) { m_peekage[chan] = 0; m_peeks[chan] = arr[chan]; } } update(); } double AudioSignal::valueToPixel(double in) { // in=0 -> return 0 (null length from max), in=127/127 return 1 (max length ) return 1.0 - log10(in) / log10(1.0 / 127.0); } diff --git a/src/scopes/audioscopes/audiosignal.h b/src/scopes/audioscopes/audiosignal.h index b3a109adc..f34d16597 100644 --- a/src/scopes/audioscopes/audiosignal.h +++ b/src/scopes/audioscopes/audiosignal.h @@ -1,69 +1,69 @@ /*************************************************************************** * Copyright (C) 2010 by Marco Gittler (g.marco@freenet.de) * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * 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 AUDIOSIGNAL_H #define AUDIOSIGNAL_H #include "abstractaudioscopewidget.h" #include #include #include #include -#include +#include class AudioSignal : public AbstractAudioScopeWidget { Q_OBJECT public: explicit AudioSignal(QWidget *parent = nullptr); - ~AudioSignal(); + ~AudioSignal() override; /** @brief Used for checking whether audio data needs to be delivered */ bool monitoringEnabled() const; QRect scopeRect() override; QImage renderHUD(uint accelerationFactor) override; QImage renderBackground(uint accelerationFactor) override; QImage renderAudioScope(uint accelerationFactor, const audioShortVector &audioFrame, const int, const int num_channels, const int samples, const int) override; QString widgetName() const override { return QStringLiteral("audioSignal"); } bool isHUDDependingOnInput() const override { return false; } bool isScopeDependingOnInput() const override { return true; } bool isBackgroundDependingOnInput() const override { return false; } private: double valueToPixel(double in); QTimer m_timer; QByteArray m_channels, m_peeks, m_peekage; QList m_dbscale; public slots: void showAudio(const QByteArray &); void slotReceiveAudio(audioShortVector audioSamples, int, int num_channels, int samples); private slots: void slotNoAudioTimeout(); signals: void updateAudioMonitoring(); }; #endif diff --git a/src/scopes/audioscopes/audiospectrum.cpp b/src/scopes/audioscopes/audiospectrum.cpp index ac03042da..ffdd77f59 100644 --- a/src/scopes/audioscopes/audiospectrum.cpp +++ b/src/scopes/audioscopes/audiospectrum.cpp @@ -1,539 +1,535 @@ /*************************************************************************** * 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 "audiospectrum.h" #include "lib/audio/fftTools.h" #include "lib/external/kiss_fft/tools/kiss_fftr.h" #include #include #include "klocalizedstring.h" #include #include #include // (defined in the header file) #ifdef DEBUG_AUDIOSPEC #include "kdenlive_debug.h" #endif // (defined in the header file) #ifdef DETECT_OVERMODULATION #include #include #endif // Draw lines instead of single pixels. // This is about 25 % faster, especially when enlarging the scope to e.g. 1680x1050 px. #define AUDIOSPEC_LINES #define MIN_DB_VALUE -120 #define MAX_FREQ_VALUE 96000 #define MIN_FREQ_VALUE 1000 #define ALPHA_MOVING_AVG 0.125 #define MAX_OVM_COLOR 0.7 AudioSpectrum::AudioSpectrum(QWidget *parent) : AbstractAudioScopeWidget(true, parent) , m_fftTools() , m_lastFFT() , m_lastFFTLock(1) , m_peaks() #ifdef DEBUG_AUDIOSPEC , m_timeTotal(0) , m_showTotal(0) #endif - , m_dBmin(-70) - , m_dBmax(0) - , m_freqMax(0) - , m_customFreq(false) - , m_colorizeFactor(0) + { m_ui = new Ui::AudioSpectrum_UI; m_ui->setupUi(this); m_aResetHz = new QAction(i18n("Reset maximum frequency to sampling rate"), this); m_aTrackMouse = new QAction(i18n("Track mouse"), this); m_aTrackMouse->setCheckable(true); m_aShowMax = new QAction(i18n("Show maximum"), this); m_aShowMax->setCheckable(true); m_menu->addSeparator(); m_menu->addAction(m_aResetHz); m_menu->addAction(m_aTrackMouse); m_menu->addAction(m_aShowMax); m_menu->removeAction(m_aRealtime); m_ui->windowSize->addItem(QStringLiteral("256"), QVariant(256)); m_ui->windowSize->addItem(QStringLiteral("512"), QVariant(512)); m_ui->windowSize->addItem(QStringLiteral("1024"), QVariant(1024)); m_ui->windowSize->addItem(QStringLiteral("2048"), QVariant(2048)); m_ui->windowFunction->addItem(i18n("Rectangular window"), FFTTools::Window_Rect); m_ui->windowFunction->addItem(i18n("Triangular window"), FFTTools::Window_Triangle); m_ui->windowFunction->addItem(i18n("Hamming window"), FFTTools::Window_Hamming); connect(m_aResetHz, &QAction::triggered, this, &AudioSpectrum::slotResetMaxFreq); connect(m_ui->windowFunction, SIGNAL(currentIndexChanged(int)), this, SLOT(forceUpdate())); connect(this, &AudioSpectrum::signalMousePositionChanged, this, &AudioSpectrum::forceUpdateHUD); // Note: These strings are used in both Spectogram and AudioSpectrum. Ideally change both (if necessary) to reduce workload on translators m_ui->labelFFTSize->setToolTip(i18n("The maximum window size is limited by the number of samples per frame.")); m_ui->windowSize->setToolTip(i18n("A bigger window improves the accuracy at the cost of computational power.")); m_ui->windowFunction->setToolTip(i18n("The rectangular window function is good for signals with equal signal strength (narrow peak), but creates more " "smearing. See Window function on Wikipedia.")); AbstractScopeWidget::init(); } AudioSpectrum::~AudioSpectrum() { writeConfig(); delete m_aResetHz; delete m_aTrackMouse; delete m_ui; } void AudioSpectrum::readConfig() { AbstractScopeWidget::readConfig(); KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup scopeConfig(config, AbstractScopeWidget::configName()); m_ui->windowSize->setCurrentIndex(scopeConfig.readEntry("windowSize", 0)); m_ui->windowFunction->setCurrentIndex(scopeConfig.readEntry("windowFunction", 0)); m_aTrackMouse->setChecked(scopeConfig.readEntry("trackMouse", true)); m_aShowMax->setChecked(scopeConfig.readEntry("showMax", true)); m_dBmax = scopeConfig.readEntry("dBmax", 0); m_dBmin = scopeConfig.readEntry("dBmin", -70); m_freqMax = scopeConfig.readEntry("freqMax", 0); if (m_freqMax == 0) { m_customFreq = false; m_freqMax = 10000; } else { m_customFreq = true; } } void AudioSpectrum::writeConfig() { KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup scopeConfig(config, AbstractScopeWidget::configName()); scopeConfig.writeEntry("windowSize", m_ui->windowSize->currentIndex()); scopeConfig.writeEntry("windowFunction", m_ui->windowFunction->currentIndex()); scopeConfig.writeEntry("trackMouse", m_aTrackMouse->isChecked()); scopeConfig.writeEntry("showMax", m_aShowMax->isChecked()); scopeConfig.writeEntry("dBmax", m_dBmax); scopeConfig.writeEntry("dBmin", m_dBmin); if (m_customFreq) { scopeConfig.writeEntry("freqMax", m_freqMax); } else { scopeConfig.writeEntry("freqMax", 0); } scopeConfig.sync(); } QString AudioSpectrum::widgetName() const { return QStringLiteral("AudioSpectrum"); } QImage AudioSpectrum::renderBackground(uint) { return QImage(); } QImage AudioSpectrum::renderAudioScope(uint, const audioShortVector &audioFrame, const int freq, const int num_channels, const int num_samples, const int) { if (audioFrame.size() > 63 && m_innerScopeRect.width() > 0 && m_innerScopeRect.height() > 0 // <= 0 if widget is too small (resized by user) ) { if (!m_customFreq) { m_freqMax = freq / 2; } QTime start = QTime::currentTime(); /*******FIXME!!! #ifdef DETECT_OVERMODULATION bool overmodulated = false; int overmodulateCount = 0; for (int i = 0; i < audioFrame.size(); ++i) { if ( audioFrame[i] == std::numeric_limits::max() || audioFrame[i] == std::numeric_limits::min()) { overmodulateCount++; if (overmodulateCount > 3) { overmodulated = true; break; } } } if (overmodulated) { m_colorizeFactor = 1; } else { if (m_colorizeFactor > 0) { m_colorizeFactor -= .08; if (m_colorizeFactor < 0) { m_colorizeFactor = 0; } } } #endif *******/ // Determine the window size to use. It should be // * not bigger than the number of samples actually available // * divisible by 2 int fftWindow = m_ui->windowSize->itemData(m_ui->windowSize->currentIndex()).toInt(); if (fftWindow > num_samples) { fftWindow = num_samples; } if ((fftWindow & 1) == 1) { fftWindow--; } // Show the window size used, for information m_ui->labelFFTSizeNumber->setText(QVariant(fftWindow).toString()); // Get the spectral power distribution of the input samples, // using the given window size and function - float *freqSpectrum = new float[(uint)fftWindow / 2]; + auto *freqSpectrum = new float[(uint)fftWindow / 2]; FFTTools::WindowType windowType = (FFTTools::WindowType)m_ui->windowFunction->itemData(m_ui->windowFunction->currentIndex()).toInt(); m_fftTools.fftNormalized(audioFrame, 0, (uint)num_channels, freqSpectrum, windowType, (uint)fftWindow, 0); // Store the current FFT window (for the HUD) and run the interpolation // for easy pixel-based dB value access QVector dbMap; m_lastFFTLock.acquire(); m_lastFFT = QVector(fftWindow / 2); memcpy(m_lastFFT.data(), &(freqSpectrum[0]), (uint)fftWindow / 2 * sizeof(float)); uint right = uint(((float)m_freqMax) / ((float)m_freq / 2.) * float(m_lastFFT.size() - 1)); dbMap = FFTTools::interpolatePeakPreserving(m_lastFFT, (uint)m_innerScopeRect.width(), 0, right, -180); m_lastFFTLock.release(); #ifdef DEBUG_AUDIOSPEC QTime drawTime = QTime::currentTime(); #endif delete[] freqSpectrum; // Draw the spectrum QImage spectrum(m_scopeRect.size(), QImage::Format_ARGB32); spectrum.fill(qRgba(0, 0, 0, 0)); const int w = m_innerScopeRect.width(); const int h = m_innerScopeRect.height(); const int leftDist = m_innerScopeRect.left() - m_scopeRect.left(); const int topDist = m_innerScopeRect.top() - m_scopeRect.top(); QColor spectrumColor(AbstractScopeWidget::colDarkWhite); int yMax; #ifdef DETECT_OVERMODULATION if (m_colorizeFactor > 0) { QColor col = AbstractScopeWidget::colHighlightDark; QColor spec = spectrumColor; float f = std::sin(M_PI_2 * m_colorizeFactor); spectrumColor = QColor((int)(f * (float)col.red() + (1. - f) * (float)spec.red()), (int)(f * (float)col.green() + (1. - f) * (float)spec.green()), (int)(f * (float)col.blue() + (1. - f) * (float)spec.blue()), spec.alpha()); // Limit the maximum colorization for non-overmodulated frames to better // recognize consecutively overmodulated frames if (m_colorizeFactor > MAX_OVM_COLOR) { m_colorizeFactor = MAX_OVM_COLOR; } } #endif #ifdef AUDIOSPEC_LINES QPainter davinci(&spectrum); davinci.setPen(QPen(QBrush(spectrumColor.rgba()), 1, Qt::SolidLine)); #endif for (int i = 0; i < w; ++i) { yMax = int((dbMap[i] - (float)m_dBmin) / (float)(m_dBmax - m_dBmin) * (float)(h - 1)); if (yMax < 0) { yMax = 0; } else if (yMax >= (int)h) { yMax = h - 1; } #ifdef AUDIOSPEC_LINES davinci.drawLine(leftDist + i, topDist + h - 1, leftDist + i, topDist + h - 1 - yMax); #else for (int y = 0; y < yMax && y < (int)h; ++y) { spectrum.setPixel(leftDist + i, topDist + h - y - 1, spectrumColor.rgba()); } #endif } // Calculate the peak values. Use the new value if it is bigger, otherwise adapt to lower // values using the Moving Average formula if (m_aShowMax->isChecked()) { davinci.setPen(QPen(QBrush(AbstractScopeWidget::colHighlightLight), 2)); if (m_peaks.size() != fftWindow / 2) { m_peaks = QVector(m_lastFFT); } else { for (int i = 0; i < fftWindow / 2; ++i) { if (m_lastFFT[i] > m_peaks[i]) { m_peaks[i] = m_lastFFT[i]; } else { m_peaks[i] = ALPHA_MOVING_AVG * m_lastFFT[i] + (1 - ALPHA_MOVING_AVG) * m_peaks[i]; } } } int prev = 0; m_peakMap = FFTTools::interpolatePeakPreserving(m_peaks, (uint)m_innerScopeRect.width(), 0, right, -180); for (int i = 0; i < w; ++i) { yMax = (m_peakMap[i] - (float)m_dBmin) / (float)(m_dBmax - m_dBmin) * (float)(h - 1); if (yMax < 0) { yMax = 0; } else if (yMax >= (int)h) { yMax = h - 1; } davinci.drawLine(leftDist + i - 1, topDist + h - prev - 1, leftDist + i, topDist + h - yMax - 1); spectrum.setPixel(leftDist + i, topDist + h - yMax - 1, AbstractScopeWidget::colHighlightLight.rgba()); prev = yMax; } } #ifdef DEBUG_AUDIOSPEC m_showTotal++; m_timeTotal += drawTime.elapsed(); qCDebug(KDENLIVE_LOG) << widgetName() << " took " << drawTime.elapsed() << " ms for drawing. Average: " << ((float)m_timeTotal / m_showTotal); #endif emit signalScopeRenderingFinished((uint)start.elapsed(), 1); return spectrum; } emit signalScopeRenderingFinished(0, 1); return QImage(); } QImage AudioSpectrum::renderHUD(uint) { QTime start = QTime::currentTime(); if (m_innerScopeRect.height() > 0 && m_innerScopeRect.width() > 0) { // May be below 0 if widget is too small // Minimum distance between two lines const int minDistY = 30; const int minDistX = 40; const int textDistX = 10; const int textDistY = 25; const int topDist = m_innerScopeRect.top() - m_scopeRect.top(); const int leftDist = m_innerScopeRect.left() - m_scopeRect.left(); const int dbDiff = ceil((float)minDistY / (float)m_innerScopeRect.height() * (float)(m_dBmax - m_dBmin)); const int mouseX = m_mousePos.x() - m_innerScopeRect.left(); const int mouseY = m_mousePos.y() - m_innerScopeRect.top(); QImage hud(m_scopeRect.size(), QImage::Format_ARGB32); hud.fill(qRgba(0, 0, 0, 0)); QPainter davinci(&hud); davinci.setPen(AbstractScopeWidget::penLight); int y; for (int db = -dbDiff; db > m_dBmin; db -= dbDiff) { y = topDist + int(float(m_innerScopeRect.height()) * ((float)db) / float(m_dBmin - m_dBmax)); if (y - topDist > m_innerScopeRect.height() - minDistY + 10) { // Abort here, there is still a line left for min dB to paint which needs some room. break; } davinci.drawLine(leftDist, y, leftDist + m_innerScopeRect.width() - 1, y); davinci.drawText(leftDist + m_innerScopeRect.width() + textDistX, y + 6, i18n("%1 dB", m_dBmax + db)); } davinci.drawLine(leftDist, topDist, leftDist + m_innerScopeRect.width() - 1, topDist); davinci.drawText(leftDist + m_innerScopeRect.width() + textDistX, topDist + 6, i18n("%1 dB", m_dBmax)); davinci.drawLine(leftDist, topDist + m_innerScopeRect.height() - 1, leftDist + m_innerScopeRect.width() - 1, topDist + m_innerScopeRect.height() - 1); davinci.drawText(leftDist + m_innerScopeRect.width() + textDistX, topDist + m_innerScopeRect.height() + 6, i18n("%1 dB", m_dBmin)); const int hzDiff = ceil(((float)minDistX) / (float)m_innerScopeRect.width() * (float)m_freqMax / 1000.) * 1000; int x = 0; const int rightBorder = leftDist + m_innerScopeRect.width() - 1; y = topDist + m_innerScopeRect.height() + textDistY; for (int hz = 0; x <= rightBorder; hz += hzDiff) { davinci.setPen(AbstractScopeWidget::penLighter); x = leftDist + int((float)m_innerScopeRect.width() * ((float)hz) / (float)m_freqMax); if (x <= rightBorder) { davinci.drawLine(x, topDist, x, topDist + m_innerScopeRect.height() + 6); } if (hz < m_freqMax && x + textDistY < leftDist + m_innerScopeRect.width()) { davinci.drawText(x - 4, y, QVariant(hz / 1000).toString()); } else { x = leftDist + m_innerScopeRect.width(); davinci.drawLine(x, topDist, x, topDist + m_innerScopeRect.height() + 6); davinci.drawText(x - 10, y, i18n("%1 kHz", QString("%1").arg((double)m_freqMax / 1000, 0, 'f', 1))); } if (hz > 0) { // Draw finer lines between the main lines davinci.setPen(AbstractScopeWidget::penLightDots); for (uint dHz = 3; dHz > 0; --dHz) { x = leftDist + int((float)m_innerScopeRect.width() * ((float)hz - (float)dHz * (float)hzDiff / 4.0f) / (float)m_freqMax); if (x > rightBorder) { break; } davinci.drawLine(x, topDist, x, topDist + m_innerScopeRect.height() - 1); } } } if (m_aTrackMouse->isChecked() && m_mouseWithinWidget && mouseX < m_innerScopeRect.width() - 1) { davinci.setPen(AbstractScopeWidget::penThin); x = leftDist + mouseX; float db = 0; float freq = ((float)mouseX) / (float)(m_innerScopeRect.width() - 1) * (float)m_freqMax; bool drawDb = false; m_lastFFTLock.acquire(); // We need to test whether the mouse is inside the widget // because the position could already have changed in the meantime (-> crash) if (!m_lastFFT.isEmpty() && mouseX >= 0 && mouseX < m_innerScopeRect.width()) { uint right = uint(((float)m_freqMax) / (m_freq / 2.) * float(m_lastFFT.size() - 1)); QVector dbMap = FFTTools::interpolatePeakPreserving(m_lastFFT, (uint)m_innerScopeRect.width(), 0, right, -120); db = dbMap[mouseX]; y = topDist + m_innerScopeRect.height() - 1 - int((dbMap[mouseX] - (float)m_dBmin) / float(m_dBmax - m_dBmin) * float(m_innerScopeRect.height() - 1)); if (y < (int)topDist + m_innerScopeRect.height() - 1) { drawDb = true; davinci.drawLine(x, y, leftDist + m_innerScopeRect.width() - 1, y); } } else { y = topDist + mouseY; } m_lastFFTLock.release(); if (y > (int)topDist + mouseY) { y = topDist + mouseY; } davinci.drawLine(x, y, x, topDist + m_innerScopeRect.height() - 1); if (drawDb) { QPoint dist(20, -20); QRect rect(leftDist + mouseX + dist.x(), topDist + mouseY + dist.y(), 100, 40); if (rect.right() > (int)leftDist + m_innerScopeRect.width() - 1) { // Mirror the rectangle at the y axis to keep it inside the widget rect = QRect(rect.topLeft() - QPoint(rect.width() + 2 * dist.x(), 0), rect.size()); } QRect textRect(rect.topLeft() + QPoint(12, 4), rect.size()); davinci.fillRect(rect, AbstractScopeWidget::penBackground.brush()); davinci.setPen(AbstractScopeWidget::penLighter); davinci.drawRect(rect); davinci.drawText(textRect, QString(i18n("%1 dB", QString("%1").arg(db, 0, 'f', 2)) + '\n' + i18n("%1 kHz", QString("%1").arg(freq / 1000, 0, 'f', 2)))); } } emit signalHUDRenderingFinished((uint)start.elapsed(), 1); return hud; } #ifdef DEBUG_AUDIOSPEC qCDebug(KDENLIVE_LOG) << "Widget is too small for painting inside. Size of inner scope rect is " << m_innerScopeRect.width() << 'x' << m_innerScopeRect.height() << "."; #endif emit signalHUDRenderingFinished(0, 1); return QImage(); } QRect AudioSpectrum::scopeRect() { m_scopeRect = QRect(QPoint(10, // Left m_ui->verticalSpacer->geometry().top() + 6 // Top ), AbstractAudioScopeWidget::rect().bottomRight()); m_innerScopeRect = QRect(QPoint(m_scopeRect.left() + 6, // Left m_scopeRect.top() + 6 // Top ), QPoint(m_ui->verticalSpacer->geometry().right() - 70, m_ui->verticalSpacer->geometry().bottom() - 40)); return m_scopeRect; } void AudioSpectrum::slotResetMaxFreq() { m_customFreq = false; forceUpdateHUD(); forceUpdateScope(); } ///// EVENTS ///// void AudioSpectrum::handleMouseDrag(const QPoint &movement, const RescaleDirection rescaleDirection, const Qt::KeyboardModifiers rescaleModifiers) { if (rescaleDirection == North) { // Nort-South direction: Adjust the dB scale if ((rescaleModifiers & Qt::ShiftModifier) == 0) { // By default adjust the min dB value m_dBmin += movement.y(); } else { // Adjust max dB value if Shift is pressed. m_dBmax += movement.y(); } // Ensure the dB values lie in [-100, 0] (or rather [MIN_DB_VALUE, 0]) // 0 is the upper bound, everything below -70 dB is most likely noise if (m_dBmax > 0) { m_dBmax = 0; } if (m_dBmin < MIN_DB_VALUE) { m_dBmin = MIN_DB_VALUE; } // Ensure there is at least 6 dB between the minimum and the maximum value; // lower values hardly make sense if (m_dBmax - m_dBmin < 6) { if ((rescaleModifiers & Qt::ShiftModifier) == 0) { // min was adjusted; Try to adjust the max value to maintain the // minimum dB difference of 6 dB m_dBmax = m_dBmin + 6; if (m_dBmax > 0) { m_dBmax = 0; m_dBmin = -6; } } else { // max was adjusted, adjust min m_dBmin = m_dBmax - 6; if (m_dBmin < MIN_DB_VALUE) { m_dBmin = MIN_DB_VALUE; m_dBmax = MIN_DB_VALUE + 6; } } } forceUpdateHUD(); forceUpdateScope(); } else if (rescaleDirection == East) { // East-West direction: Adjust the maximum frequency m_freqMax -= 100 * movement.x(); if (m_freqMax < MIN_FREQ_VALUE) { m_freqMax = MIN_FREQ_VALUE; } if (m_freqMax > MAX_FREQ_VALUE) { m_freqMax = MAX_FREQ_VALUE; } m_customFreq = true; forceUpdateHUD(); forceUpdateScope(); } } diff --git a/src/scopes/audioscopes/audiospectrum.h b/src/scopes/audioscopes/audiospectrum.h index a9edb27b1..6a93d24b0 100644 --- a/src/scopes/audioscopes/audiospectrum.h +++ b/src/scopes/audioscopes/audiospectrum.h @@ -1,99 +1,99 @@ /*************************************************************************** * 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. * ***************************************************************************/ #ifndef AUDIOSPECTRUM_H #define AUDIOSPECTRUM_H #include "abstractaudioscopewidget.h" #include "lib/audio/fftTools.h" #include "lib/external/kiss_fft/tools/kiss_fftr.h" #include "ui_audiospectrum_ui.h" // Enables debugging //#define DEBUG_AUDIOSPEC // Show overmodulation #define DETECT_OVERMODULATION #include #include class AudioSpectrum_UI; /** \brief Displays a spectral power distribution of audio samples. The frequency distribution is calculated by means of a Fast Fourier Transformation. For more information see Wikipedia:FFT and the code comments. \todo Currently only supports one channel. Add support for multiple channels. */ class AudioSpectrum : public AbstractAudioScopeWidget { Q_OBJECT public: explicit AudioSpectrum(QWidget *parent = nullptr); - ~AudioSpectrum(); + ~AudioSpectrum() override; // Implemented virtual methods QString widgetName() const override; protected: ///// Implemented methods ///// QRect scopeRect() override; QImage renderHUD(uint accelerationFactor) override; QImage renderAudioScope(uint accelerationFactor, const audioShortVector &audioFrame, const int freq, const int num_channels, const int num_samples, const int newData) override; QImage renderBackground(uint accelerationFactor) override; void readConfig() override; void writeConfig(); void handleMouseDrag(const QPoint &movement, const RescaleDirection rescaleDirection, const Qt::KeyboardModifiers rescaleModifiers) override; private: Ui::AudioSpectrum_UI *m_ui; QAction *m_aResetHz; QAction *m_aTrackMouse; QAction *m_aShowMax; FFTTools m_fftTools; QVector m_lastFFT; QSemaphore m_lastFFTLock; QVector m_peaks; QVector m_peakMap; /** Contains the plot only; m_scopeRect contains text and widgets as well */ QRect m_innerScopeRect; /** Lower bound for the dB value to display */ - int m_dBmin; + int m_dBmin{-70}; /** Upper bound (max: 0) */ - int m_dBmax; + int m_dBmax{0}; /** Maximum frequency (limited by the sampling rate if determined automatically). Stored for the painters. */ - int m_freqMax; + int m_freqMax{0}; /** The user has chosen a custom frequency. */ - bool m_customFreq; + bool m_customFreq{false}; - float m_colorizeFactor; + float m_colorizeFactor{0}; #ifdef DEBUG_AUDIOSPEC long m_timeTotal; long m_showTotal; #endif private slots: void slotResetMaxFreq(); }; #endif // AUDIOSPECTRUM_H diff --git a/src/scopes/audioscopes/spectrogram.cpp b/src/scopes/audioscopes/spectrogram.cpp index 4bf5b4e6c..c0567537b 100644 --- a/src/scopes/audioscopes/spectrogram.cpp +++ b/src/scopes/audioscopes/spectrogram.cpp @@ -1,538 +1,534 @@ /*************************************************************************** * 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 "spectrogram.h" #include #include #include "klocalizedstring.h" #include #include // Defines the number of FFT samples to store. // Around 4 kB for a window size of 2000. Should be at least as large as the // highest vertical screen resolution available for complete reconstruction. // Can be less as a pre-rendered image is kept in space. #define SPECTROGRAM_HISTORY_SIZE 1000 // Uncomment for debugging //#define DEBUG_SPECTROGRAM #ifdef DEBUG_SPECTROGRAM #include "kdenlive_debug.h" #endif #define MIN_DB_VALUE -120 #define MAX_FREQ_VALUE 96000 #define MIN_FREQ_VALUE 1000 Spectrogram::Spectrogram(QWidget *parent) : AbstractAudioScopeWidget(true, parent) , m_fftTools() , m_fftHistory() , m_fftHistoryImg() - , m_dBmin(-70) - , m_dBmax(0) - , m_freqMax(0) - , m_customFreq(false) - , m_parameterChanged(false) + { m_ui = new Ui::Spectrogram_UI; m_ui->setupUi(this); m_aResetHz = new QAction(i18n("Reset maximum frequency to sampling rate"), this); m_aGrid = new QAction(i18n("Draw grid"), this); m_aGrid->setCheckable(true); m_aTrackMouse = new QAction(i18n("Track mouse"), this); m_aTrackMouse->setCheckable(true); m_aHighlightPeaks = new QAction(i18n("Highlight peaks"), this); m_aHighlightPeaks->setCheckable(true); m_menu->addSeparator(); m_menu->addAction(m_aResetHz); m_menu->addAction(m_aTrackMouse); m_menu->addAction(m_aGrid); m_menu->addAction(m_aHighlightPeaks); m_menu->removeAction(m_aRealtime); m_ui->windowSize->addItem(QStringLiteral("256"), QVariant(256)); m_ui->windowSize->addItem(QStringLiteral("512"), QVariant(512)); m_ui->windowSize->addItem(QStringLiteral("1024"), QVariant(1024)); m_ui->windowSize->addItem(QStringLiteral("2048"), QVariant(2048)); m_ui->windowFunction->addItem(i18n("Rectangular window"), FFTTools::Window_Rect); m_ui->windowFunction->addItem(i18n("Triangular window"), FFTTools::Window_Triangle); m_ui->windowFunction->addItem(i18n("Hamming window"), FFTTools::Window_Hamming); // Note: These strings are used in both Spectogram and AudioSpectrum. Ideally change both (if necessary) to reduce workload on translators m_ui->labelFFTSize->setToolTip(i18n("The maximum window size is limited by the number of samples per frame.")); m_ui->windowSize->setToolTip(i18n("A bigger window improves the accuracy at the cost of computational power.")); m_ui->windowFunction->setToolTip(i18n("The rectangular window function is good for signals with equal signal strength (narrow peak), but creates more " "smearing. See Window function on Wikipedia.")); connect(m_aResetHz, &QAction::triggered, this, &Spectrogram::slotResetMaxFreq); connect(m_ui->windowFunction, SIGNAL(currentIndexChanged(int)), this, SLOT(forceUpdate())); connect(this, &Spectrogram::signalMousePositionChanged, this, &Spectrogram::forceUpdateHUD); AbstractScopeWidget::init(); for (int i = 0; i <= 255 / 5; ++i) { m_colorMap[i + 0 * 255 / 5] = qRgb(0, 0, i * 5); // black to blue m_colorMap[i + 1 * 255 / 5] = qRgb(0, i * 5, 255); // blue to cyan m_colorMap[i + 2 * 255 / 5] = qRgb(0, 255, 255 - i * 5); // cyan to green m_colorMap[i + 3 * 255 / 5] = qRgb(i * 5, 255, 0); // green to yellow m_colorMap[i + 4 * 255 / 5] = qRgb(255, 255 - i * 5, 0); // yellow to red } } Spectrogram::~Spectrogram() { writeConfig(); delete m_aResetHz; delete m_aTrackMouse; delete m_aGrid; delete m_ui; } void Spectrogram::readConfig() { AbstractScopeWidget::readConfig(); KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup scopeConfig(config, AbstractScopeWidget::configName()); m_ui->windowSize->setCurrentIndex(scopeConfig.readEntry("windowSize", 0)); m_ui->windowFunction->setCurrentIndex(scopeConfig.readEntry("windowFunction", 0)); m_aTrackMouse->setChecked(scopeConfig.readEntry("trackMouse", true)); m_aGrid->setChecked(scopeConfig.readEntry("drawGrid", true)); m_aHighlightPeaks->setChecked(scopeConfig.readEntry("highlightPeaks", true)); m_dBmax = scopeConfig.readEntry("dBmax", 0); m_dBmin = scopeConfig.readEntry("dBmin", -70); m_freqMax = scopeConfig.readEntry("freqMax", 0); if (m_freqMax == 0) { m_customFreq = false; m_freqMax = 10000; } else { m_customFreq = true; } } void Spectrogram::writeConfig() { KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup scopeConfig(config, AbstractScopeWidget::configName()); scopeConfig.writeEntry("windowSize", m_ui->windowSize->currentIndex()); scopeConfig.writeEntry("windowFunction", m_ui->windowFunction->currentIndex()); scopeConfig.writeEntry("trackMouse", m_aTrackMouse->isChecked()); scopeConfig.writeEntry("drawGrid", m_aGrid->isChecked()); scopeConfig.writeEntry("highlightPeaks", m_aHighlightPeaks->isChecked()); scopeConfig.writeEntry("dBmax", m_dBmax); scopeConfig.writeEntry("dBmin", m_dBmin); if (m_customFreq) { scopeConfig.writeEntry("freqMax", m_freqMax); } else { scopeConfig.writeEntry("freqMax", 0); } scopeConfig.sync(); } QString Spectrogram::widgetName() const { return QStringLiteral("Spectrogram"); } QRect Spectrogram::scopeRect() { m_scopeRect = QRect(QPoint(10, // Left m_ui->verticalSpacer->geometry().top() + 6 // Top ), AbstractAudioScopeWidget::rect().bottomRight()); m_innerScopeRect = QRect(QPoint(m_scopeRect.left() + 66, // Left m_scopeRect.top() + 6 // Top ), QPoint(m_ui->verticalSpacer->geometry().right() - 70, m_ui->verticalSpacer->geometry().bottom() - 40)); return m_scopeRect; } QImage Spectrogram::renderHUD(uint) { if (m_innerScopeRect.width() > 0 && m_innerScopeRect.height() > 0) { QTime start = QTime::currentTime(); int x, y; const int minDistY = 30; // Minimum distance between two lines const int minDistX = 40; const int textDistX = 10; const int textDistY = 25; const int topDist = m_innerScopeRect.top() - m_scopeRect.top(); const int leftDist = m_innerScopeRect.left() - m_scopeRect.left(); const int mouseX = m_mousePos.x() - m_innerScopeRect.left(); const int mouseY = m_mousePos.y() - m_innerScopeRect.top(); bool hideText; QImage hud(m_scopeRect.size(), QImage::Format_ARGB32); hud.fill(qRgba(0, 0, 0, 0)); QPainter davinci(&hud); davinci.setPen(AbstractScopeWidget::penLight); // Frame display if (m_aGrid->isChecked()) { for (int frameNumber = 0; frameNumber < m_innerScopeRect.height(); frameNumber += minDistY) { y = topDist + m_innerScopeRect.height() - 1 - frameNumber; hideText = m_aTrackMouse->isChecked() && m_mouseWithinWidget && abs(y - mouseY) < (int)textDistY && mouseY < m_innerScopeRect.height() && mouseX < m_innerScopeRect.width() && mouseX >= 0; davinci.drawLine(leftDist, y, leftDist + m_innerScopeRect.width() - 1, y); if (!hideText) { davinci.drawText(leftDist + m_innerScopeRect.width() + textDistX, y + 6, QVariant(frameNumber).toString()); } } } // Draw a line through the mouse position with the correct Frame number if (m_aTrackMouse->isChecked() && m_mouseWithinWidget && mouseY < m_innerScopeRect.height() && mouseX < m_innerScopeRect.width() && mouseX >= 0) { davinci.setPen(AbstractScopeWidget::penLighter); x = leftDist + mouseX; y = topDist + mouseY - 20; if (y < 0) { y = 0; } if (y > (int)topDist + m_innerScopeRect.height() - 1 - 30) { y = topDist + m_innerScopeRect.height() - 1 - 30; } davinci.drawLine(x, topDist + mouseY, leftDist + m_innerScopeRect.width() - 1, topDist + mouseY); davinci.drawText(leftDist + m_innerScopeRect.width() + textDistX, y, m_scopeRect.right() - m_innerScopeRect.right() - textDistX, 40, Qt::AlignLeft, i18n("Frame\n%1", m_innerScopeRect.height() - 1 - mouseY)); } // Frequency grid const uint hzDiff = (uint)ceil(((float)minDistX) / (float)m_innerScopeRect.width() * (float)m_freqMax / 1000.) * 1000; const int rightBorder = leftDist + m_innerScopeRect.width() - 1; x = 0; y = topDist + m_innerScopeRect.height() + textDistY; if (m_aGrid->isChecked()) { for (uint hz = 0; x <= rightBorder; hz += hzDiff) { davinci.setPen(AbstractScopeWidget::penLight); x = int((float)leftDist + (float)(m_innerScopeRect.width() - 1) * ((float)hz) / (float)m_freqMax); // Hide text if it would overlap with the text drawn at the mouse position hideText = m_aTrackMouse->isChecked() && m_mouseWithinWidget && abs(x - (int)(leftDist + mouseX + 20)) < (int)minDistX + 16 && mouseX < m_innerScopeRect.width() && mouseX >= 0; if (x <= rightBorder) { davinci.drawLine(x, topDist, x, topDist + m_innerScopeRect.height() + 6); } if (x + textDistY < leftDist + m_innerScopeRect.width()) { // Only draw the text label if there is still enough room for the final one at the right. if (!hideText) { davinci.drawText(x - 4, y, QVariant(hz / 1000).toString()); } } if (hz > 0) { // Draw finer lines between the main lines davinci.setPen(AbstractScopeWidget::penLightDots); for (uint dHz = 3; dHz > 0; --dHz) { x = int((float)leftDist + (float)m_innerScopeRect.width() * ((float)hz - (float)dHz * (float)hzDiff / 4.0f) / (float)m_freqMax); if (x > rightBorder) { break; } davinci.drawLine(x, topDist, x, topDist + m_innerScopeRect.height() - 1); } } } // Draw the line at the very right (maximum frequency) x = leftDist + m_innerScopeRect.width() - 1; hideText = m_aTrackMouse->isChecked() && m_mouseWithinWidget && qAbs(x - (int)(leftDist + mouseX + 30)) < (int)minDistX && mouseX < m_innerScopeRect.width() && mouseX >= 0; davinci.drawLine(x, topDist, x, topDist + m_innerScopeRect.height() + 6); if (!hideText) { davinci.drawText(x - 10, y, i18n("%1 kHz", QString("%1").arg((double)m_freqMax / 1000, 0, 'f', 1))); } } // Draw a line through the mouse position with the correct frequency label if (m_aTrackMouse->isChecked() && m_mouseWithinWidget && mouseX < m_innerScopeRect.width() && mouseX >= 0) { davinci.setPen(AbstractScopeWidget::penThin); x = leftDist + mouseX; davinci.drawLine(x, topDist, x, topDist + m_innerScopeRect.height() + 6); davinci.drawText( x - 10, y, i18n("%1 kHz", QString("%1").arg((double)(m_mousePos.x() - m_innerScopeRect.left()) / m_innerScopeRect.width() * m_freqMax / 1000, 0, 'f', 2))); } // Draw the dB brightness scale davinci.setPen(AbstractScopeWidget::penLighter); for (y = topDist; y < (int)topDist + m_innerScopeRect.height(); ++y) { float val = 1. - ((float)y - (float)topDist) / ((float)m_innerScopeRect.height() - 1.); uint col = m_colorMap[(int)(val * 255)]; for (x = leftDist - 6; x >= (int)leftDist - 13; --x) { hud.setPixel(x, y, col); } } const int rectWidth = leftDist - m_scopeRect.left() - 22; const int rectHeight = 50; davinci.setFont(QFont(QFont().defaultFamily(), 10)); davinci.drawText(m_scopeRect.left(), topDist, rectWidth, rectHeight, Qt::AlignRight, i18n("%1\ndB", m_dBmax)); davinci.drawText(m_scopeRect.left(), topDist + m_innerScopeRect.height() - 20, rectWidth, rectHeight, Qt::AlignRight, i18n("%1\ndB", m_dBmin)); emit signalHUDRenderingFinished((uint)start.elapsed(), 1); return hud; } emit signalHUDRenderingFinished(0, 1); return QImage(); } QImage Spectrogram::renderAudioScope(uint, const audioShortVector &audioFrame, const int freq, const int num_channels, const int num_samples, const int newData) { if (audioFrame.size() > 63 && m_innerScopeRect.width() > 0 && m_innerScopeRect.height() > 0) { if (!m_customFreq) { m_freqMax = freq / 2; } bool newDataAvailable = newData > 0; #ifdef DEBUG_SPECTROGRAM qCDebug(KDENLIVE_LOG) << "New data for " << widgetName() << ": " << newDataAvailable << QStringLiteral(" (") << newData << " units)"; #endif QTime start = QTime::currentTime(); int fftWindow = m_ui->windowSize->itemData(m_ui->windowSize->currentIndex()).toInt(); if (fftWindow > num_samples) { fftWindow = num_samples; } if ((fftWindow & 1) == 1) { fftWindow--; } // Show the window size used, for information m_ui->labelFFTSizeNumber->setText(QVariant(fftWindow).toString()); if (newDataAvailable) { - float *freqSpectrum = new float[(uint)fftWindow / 2]; + auto *freqSpectrum = new float[(uint)fftWindow / 2]; // Get the spectral power distribution of the input samples, // using the given window size and function FFTTools::WindowType windowType = (FFTTools::WindowType)m_ui->windowFunction->itemData(m_ui->windowFunction->currentIndex()).toInt(); m_fftTools.fftNormalized(audioFrame, 0, (uint)num_channels, freqSpectrum, windowType, (uint)fftWindow, 0); // This method might be called also when a simple refresh is required. // In this case there is no data to append to the history. Only append new data. QVector spectrumVector(fftWindow / 2); memcpy(spectrumVector.data(), &freqSpectrum[0], (uint)fftWindow / 2 * sizeof(float)); m_fftHistory.prepend(spectrumVector); delete[] freqSpectrum; } #ifdef DEBUG_SPECTROGRAM else { qCDebug(KDENLIVE_LOG) << widgetName() << ": Has no new data to Fourier-transform"; } #endif // Limit the maximum history size to avoid wasting space while (m_fftHistory.size() > SPECTROGRAM_HISTORY_SIZE) { m_fftHistory.removeLast(); } // Draw the spectrum QImage spectrum(m_scopeRect.size(), QImage::Format_ARGB32); spectrum.fill(qRgba(0, 0, 0, 0)); QPainter davinci(&spectrum); const int h = m_innerScopeRect.height(); const int leftDist = m_innerScopeRect.left() - m_scopeRect.left(); const int topDist = m_innerScopeRect.top() - m_scopeRect.top(); int windowSize; int y; bool completeRedraw = true; if (m_fftHistoryImg.size() == m_scopeRect.size() && !m_parameterChanged) { // The size of the widget and the parameters (like min/max dB) have not changed since last time, // so we can re-use it, shift it by one pixel, and render the single remaining line. Usually about // 10 times faster for a widget height of around 400 px. if (newDataAvailable) { davinci.drawImage(0, -1, m_fftHistoryImg); } else { // spectrum = m_fftHistoryImg does NOT work, leads to segfaults (anyone knows why, please tell me) davinci.drawImage(0, 0, m_fftHistoryImg); } completeRedraw = false; } y = 0; if ((newData != 0) || m_parameterChanged) { m_parameterChanged = false; bool peak = false; QVector dbMap; uint right; ////////////////FIXME - for (QList>::iterator it = m_fftHistory.begin(); it != m_fftHistory.end(); ++it) { + for (auto &it : m_fftHistory) { - windowSize = (*it).size(); + windowSize = it.size(); // Interpolate the frequency data to match the pixel coordinates right = uint(((float)m_freqMax) / ((float)m_freq / 2.) * float(windowSize - 1)); - dbMap = FFTTools::interpolatePeakPreserving((*it), (uint)m_innerScopeRect.width(), 0, right, -180); + dbMap = FFTTools::interpolatePeakPreserving(it, (uint)m_innerScopeRect.width(), 0, right, -180); for (int i = 0; i < dbMap.size(); ++i) { float val; val = dbMap[i]; peak = val > (float)m_dBmax; // Normalize dB value to [0 1], 1 corresponding to dbMax dB and 0 to dbMin dB val = (val - (float)m_dBmax) / (float)(m_dBmax - m_dBmin) + 1.; if (val < 0) { val = 0; } else if (val > 1) { val = 1; } if (!peak || !m_aHighlightPeaks->isChecked()) { spectrum.setPixel(leftDist + i, topDist + h - 1 - y, m_colorMap[(int)(val * 255)]); } else { spectrum.setPixel(leftDist + i, topDist + h - 1 - y, AbstractScopeWidget::colHighlightDark.rgba()); } } y++; if (y >= topDist + m_innerScopeRect.height()) { break; } if (!completeRedraw) { break; } } } #ifdef DEBUG_SPECTROGRAM qCDebug(KDENLIVE_LOG) << "Rendered " << y - topDist << "lines from " << m_fftHistory.size() << " available samples in " << start.elapsed() << " ms" << (completeRedraw ? "" : " (re-used old image)"); uint storedBytes = 0; for (QList>::iterator it = m_fftHistory.begin(); it != m_fftHistory.end(); ++it) { storedBytes += (*it).size() * sizeof((*it)[0]); } qCDebug(KDENLIVE_LOG) << QString("Total storage used: %1 kB").arg((double)storedBytes / 1000, 0, 'f', 2); #endif m_fftHistoryImg = spectrum; emit signalScopeRenderingFinished((uint)start.elapsed(), 1); return spectrum; } emit signalScopeRenderingFinished(0, 1); return QImage(); } QImage Spectrogram::renderBackground(uint) { return QImage(); } bool Spectrogram::isHUDDependingOnInput() const { return false; } bool Spectrogram::isScopeDependingOnInput() const { return true; } bool Spectrogram::isBackgroundDependingOnInput() const { return false; } void Spectrogram::handleMouseDrag(const QPoint &movement, const RescaleDirection rescaleDirection, const Qt::KeyboardModifiers rescaleModifiers) { if (rescaleDirection == North) { // Nort-South direction: Adjust the dB scale if ((rescaleModifiers & Qt::ShiftModifier) == 0) { // By default adjust the min dB value m_dBmin += movement.y(); } else { // Adjust max dB value if Shift is pressed. m_dBmax += movement.y(); } // Ensure the dB values lie in [-100, 0] (or rather [MIN_DB_VALUE, 0]) // 0 is the upper bound, everything below -70 dB is most likely noise if (m_dBmax > 0) { m_dBmax = 0; } if (m_dBmin < MIN_DB_VALUE) { m_dBmin = MIN_DB_VALUE; } // Ensure there is at least 6 dB between the minimum and the maximum value; // lower values hardly make sense if (m_dBmax - m_dBmin < 6) { if ((rescaleModifiers & Qt::ShiftModifier) == 0) { // min was adjusted; Try to adjust the max value to maintain the // minimum dB difference of 6 dB m_dBmax = m_dBmin + 6; if (m_dBmax > 0) { m_dBmax = 0; m_dBmin = -6; } } else { // max was adjusted, adjust min m_dBmin = m_dBmax - 6; if (m_dBmin < MIN_DB_VALUE) { m_dBmin = MIN_DB_VALUE; m_dBmax = MIN_DB_VALUE + 6; } } } m_parameterChanged = true; forceUpdateHUD(); forceUpdateScope(); } else if (rescaleDirection == East) { // East-West direction: Adjust the maximum frequency m_freqMax -= 100 * movement.x(); if (m_freqMax < MIN_FREQ_VALUE) { m_freqMax = MIN_FREQ_VALUE; } if (m_freqMax > MAX_FREQ_VALUE) { m_freqMax = MAX_FREQ_VALUE; } m_customFreq = true; m_parameterChanged = true; forceUpdateHUD(); forceUpdateScope(); } } void Spectrogram::slotResetMaxFreq() { m_customFreq = false; m_parameterChanged = true; forceUpdateHUD(); forceUpdateScope(); } void Spectrogram::resizeEvent(QResizeEvent *event) { m_parameterChanged = true; AbstractAudioScopeWidget::resizeEvent(event); } #undef SPECTROGRAM_HISTORY_SIZE #ifdef DEBUG_SPECTROGRAM #undef DEBUG_SPECTROGRAM #endif diff --git a/src/scopes/audioscopes/spectrogram.h b/src/scopes/audioscopes/spectrogram.h index edcc86e79..c0b012361 100644 --- a/src/scopes/audioscopes/spectrogram.h +++ b/src/scopes/audioscopes/spectrogram.h @@ -1,84 +1,84 @@ /*************************************************************************** * 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. * ***************************************************************************/ /** This Spectrogram shows the spectral power distribution of incoming audio samples over time. See http://en.wikipedia.org/wiki/Spectrogram. The Spectrogram makes use of two caches: * A cached image where only the most recent line needs to be appended instead of having to recalculate the whole image. A typical speedup factor is 10x. * A FFT cache storing a history of previous spectral power distributions (i.e. the Fourier-transformed audio signals). This is used if the user adjusts parameters like the maximum frequency to display or minimum/maximum signal strength in dB. All required information is preserved in the FFT history, which would not be the case for an image (consider re-sizing the widget to 100x100 px and then back to 800x400 px -- lost is lost). */ #ifndef SPECTROGRAM_H #define SPECTROGRAM_H #include "abstractaudioscopewidget.h" #include "lib/audio/fftTools.h" #include "ui_spectrogram_ui.h" class Spectrogram_UI; class Spectrogram : public AbstractAudioScopeWidget { Q_OBJECT public: explicit Spectrogram(QWidget *parent = nullptr); - ~Spectrogram(); + ~Spectrogram() override; QString widgetName() const override; protected: ///// Implemented methods ///// QRect scopeRect() override; QImage renderHUD(uint accelerationFactor) override; QImage renderAudioScope(uint accelerationFactor, const audioShortVector &audioFrame, const int freq, const int num_channels, const int num_samples, const int newData) override; QImage renderBackground(uint accelerationFactor) override; bool isHUDDependingOnInput() const override; bool isScopeDependingOnInput() const override; bool isBackgroundDependingOnInput() const override; void readConfig() override; void writeConfig(); void handleMouseDrag(const QPoint &movement, const RescaleDirection rescaleDirection, const Qt::KeyboardModifiers rescaleModifiers) override; void resizeEvent(QResizeEvent *event) override; private: Ui::Spectrogram_UI *m_ui; FFTTools m_fftTools; QAction *m_aResetHz; QAction *m_aGrid; QAction *m_aTrackMouse; QAction *m_aHighlightPeaks; QList> m_fftHistory; QImage m_fftHistoryImg; - int m_dBmin; - int m_dBmax; + int m_dBmin{-70}; + int m_dBmax{0}; - int m_freqMax; - bool m_customFreq; + int m_freqMax{0}; + bool m_customFreq{false}; - bool m_parameterChanged; + bool m_parameterChanged{false}; QRect m_innerScopeRect; QRgb m_colorMap[256]; private slots: void slotResetMaxFreq(); }; #endif // SPECTROGRAM_H diff --git a/src/scopes/colorscopes/abstractgfxscopewidget.h b/src/scopes/colorscopes/abstractgfxscopewidget.h index 30a4ada88..6ee662c93 100644 --- a/src/scopes/colorscopes/abstractgfxscopewidget.h +++ b/src/scopes/colorscopes/abstractgfxscopewidget.h @@ -1,60 +1,60 @@ /*************************************************************************** * 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. * ***************************************************************************/ #ifndef ABSTRACTGFXSCOPEWIDGET_H #define ABSTRACTGFXSCOPEWIDGET_H #include #include #include #include "../abstractscopewidget.h" /** \brief Abstract class for scopes analyzing image frames. */ class AbstractGfxScopeWidget : public AbstractScopeWidget { Q_OBJECT public: explicit AbstractGfxScopeWidget(bool trackMouse = false, QWidget *parent = nullptr); - virtual ~AbstractGfxScopeWidget(); // Must be virtual because of inheritance, to avoid memory leaks + ~AbstractGfxScopeWidget() override; // Must be virtual because of inheritance, to avoid memory leaks protected: ///// Variables ///// /** @brief Scope renderer. Must emit signalScopeRenderingFinished() when calculation has finished, to allow multi-threading. accelerationFactor hints how much faster than usual the calculation should be accomplished, if possible. */ virtual QImage renderGfxScope(uint accelerationFactor, const QImage &) = 0; QImage renderScope(uint accelerationFactor) override; void mouseReleaseEvent(QMouseEvent *) override; private: QImage m_scopeImage; QMutex m_mutex; public slots: /** @brief Must be called when the active monitor has shown a new frame. This slot must be connected in the implementing class, it is *not* done in this abstract class. */ void slotRenderZoneUpdated(const QImage &); protected slots: virtual void slotAutoRefreshToggled(bool autoRefresh); signals: void signalFrameRequest(const QString &widgetName); }; #endif // ABSTRACTGFXSCOPEWIDGET_H diff --git a/src/scopes/colorscopes/colorplaneexport.h b/src/scopes/colorscopes/colorplaneexport.h index b0f7f17b2..804d7b5b2 100644 --- a/src/scopes/colorscopes/colorplaneexport.h +++ b/src/scopes/colorscopes/colorplaneexport.h @@ -1,48 +1,48 @@ /*************************************************************************** * 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. * ***************************************************************************/ /** Exports color planes (e.g. YUV-UV-planes) to a file. Basically just for fun, but also for comparing color models. */ #ifndef COLORPLANEEXPORT_H #define COLORPLANEEXPORT_H #include "colortools.h" #include "ui_colorplaneexport_ui.h" #include class ColorPlaneExport_UI; class ColorPlaneExport : public QDialog, public Ui::ColorPlaneExport_UI { Q_OBJECT public: explicit ColorPlaneExport(QWidget *parent = nullptr); - ~ColorPlaneExport(); + ~ColorPlaneExport() override; enum COLOR_EXPORT_MODE { CPE_YUV, CPE_YUV_Y, CPE_YUV_MOD, CPE_RGB_CURVE, CPE_YPbPr, CPE_HSV_HUESHIFT, CPE_HSV_SATURATION }; private: ColorTools *m_colorTools; float m_scaling; void enableSliderScaling(bool enable); void enableSliderColor(bool enable); void enableCbVariant(bool enable); private slots: void slotValidate(); void slotExportPlane(); void slotColormodeChanged(); void slotUpdateDisplays(); }; #endif // COLORPLANEEXPORT_H diff --git a/src/scopes/colorscopes/histogram.h b/src/scopes/colorscopes/histogram.h index 4153ac67a..978092f1c 100644 --- a/src/scopes/colorscopes/histogram.h +++ b/src/scopes/colorscopes/histogram.h @@ -1,52 +1,52 @@ /*************************************************************************** * 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. * ***************************************************************************/ #ifndef HISTOGRAM_H #define HISTOGRAM_H #include "abstractgfxscopewidget.h" #include "ui_histogram_ui.h" class HistogramGenerator; /** * \brief Displays the histogram of frames. */ class Histogram : public AbstractGfxScopeWidget { Q_OBJECT public: explicit Histogram(QWidget *parent = nullptr); - ~Histogram(); + ~Histogram() override; QString widgetName() const override; protected: void readConfig() override; void writeConfig(); private: HistogramGenerator *m_histogramGenerator; QAction *m_aUnscaled; QAction *m_aRec601; QAction *m_aRec709; QActionGroup *m_agRec; QRect scopeRect() override; bool isHUDDependingOnInput() const override; bool isScopeDependingOnInput() const override; bool isBackgroundDependingOnInput() const override; QImage renderHUD(uint accelerationFactor) override; QImage renderGfxScope(uint accelerationFactor, const QImage &) override; QImage renderBackground(uint accelerationFactor) override; Ui::Histogram_UI *m_ui; }; #endif // HISTOGRAM_H diff --git a/src/scopes/colorscopes/histogramgenerator.cpp b/src/scopes/colorscopes/histogramgenerator.cpp index 106b0d91a..1251a5a33 100644 --- a/src/scopes/colorscopes/histogramgenerator.cpp +++ b/src/scopes/colorscopes/histogramgenerator.cpp @@ -1,182 +1,182 @@ /*************************************************************************** * 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 "histogramgenerator.h" #include "klocalizedstring.h" #include #include #include -#include +#include HistogramGenerator::HistogramGenerator() = default; QImage HistogramGenerator::calculateHistogram(const QSize ¶deSize, const QImage &image, const int &components, HistogramGenerator::Rec rec, bool unscaled, uint accelFactor) const { if (paradeSize.height() <= 0 || paradeSize.width() <= 0 || image.width() <= 0 || image.height() <= 0) { return QImage(); } bool drawY = (components & HistogramGenerator::ComponentY) != 0; bool drawR = (components & HistogramGenerator::ComponentR) != 0; bool drawG = (components & HistogramGenerator::ComponentG) != 0; bool drawB = (components & HistogramGenerator::ComponentB) != 0; bool drawSum = (components & HistogramGenerator::ComponentSum) != 0; int r[256], g[256], b[256], y[256], s[766]; // Initialize the values to zero std::fill(r, r + 256, 0); std::fill(g, g + 256, 0); std::fill(b, b + 256, 0); std::fill(y, y + 256, 0); std::fill(s, s + 766, 0); const uint iw = (uint)image.bytesPerLine(); const uint ih = (uint)image.height(); const uint ww = (uint)paradeSize.width(); const uint wh = (uint)paradeSize.height(); const uint byteCount = iw * ih; // Read the stats from the input image for (int Y = 0; Y < image.height(); ++Y) { for (int X = 0; X < image.width(); X += (int)accelFactor) { QRgb col = image.pixel(X, Y); r[qRed(col)]++; g[qGreen(col)]++; b[qBlue(col)]++; if (drawY) { // Use if branch to avoid expensive multiplication if Y disabled if (rec == HistogramGenerator::Rec_601) { y[(int)floor(.299 * qRed(col) + .587 * qGreen(col) + .114 * qBlue(col))]++; } else { y[(int)floor(.2125 * qRed(col) + .7154 * qGreen(col) + .0721 * qBlue(col))]++; } } if (drawSum) { // Use an if branch here because the sum takes more operations than rgb s[qRed(col)]++; s[qGreen(col)]++; s[qBlue(col)]++; } } } const int nParts = (drawY ? 1 : 0) + (drawR ? 1 : 0) + (drawG ? 1 : 0) + (drawB ? 1 : 0) + (drawSum ? 1 : 0); if (nParts == 0) { // Nothing to draw return QImage(); } const int d = 20; // Distance for text const int partH = int((int)wh - nParts * d) / nParts; float scaling = 0; int div = (int)byteCount >> 7; if (div > 0) { scaling = (float)partH / float((int)byteCount >> 7); } const int dist = 40; int wy = 0; // Drawing position QImage histogram(paradeSize, QImage::Format_ARGB32); QPainter davinci(&histogram); davinci.setPen(QColor(220, 220, 220, 255)); histogram.fill(qRgba(0, 0, 0, 0)); if (drawY) { drawComponentFull(&davinci, y, scaling, QRect(0, wy, (int)ww, partH + dist), QColor(220, 220, 210, 255), dist, unscaled, 256); wy += partH + d; } if (drawSum) { drawComponentFull(&davinci, s, scaling / 3, QRect(0, wy, (int)ww, partH + dist), QColor(220, 220, 210, 255), dist, unscaled, 256); wy += partH + d; } if (drawR) { drawComponentFull(&davinci, r, scaling, QRect(0, wy, (int)ww, partH + dist), QColor(255, 128, 0, 255), dist, unscaled, 256); wy += partH + d; } if (drawG) { drawComponentFull(&davinci, g, scaling, QRect(0, wy, (int)ww, partH + dist), QColor(128, 255, 0, 255), dist, unscaled, 256); wy += partH + d; } if (drawB) { drawComponentFull(&davinci, b, scaling, QRect(0, wy, (int)ww, partH + dist), QColor(0, 128, 255, 255), dist, unscaled, 256); } return histogram; } QImage HistogramGenerator::drawComponent(const int *y, const QSize &size, const float &scaling, const QColor &color, bool unscaled, uint max) const { QImage component((int)max, size.height(), QImage::Format_ARGB32); component.fill(qRgba(0, 0, 0, 255)); Q_ASSERT(scaling != INFINITY); const int partH = size.height(); for (uint x = 0; x < max; ++x) { // Calculate the height of the curve at position x int partY = int(scaling * (float)y[x]); // Invert the y axis if (partY > partH - 1) { partY = partH - 1; } partY = partH - 1 - partY; for (int k = partH - 1; k >= partY; --k) { component.setPixel((int)x, k, color.rgba()); } } if (unscaled && size.width() >= component.width()) { return component; } return component.scaled(size, Qt::IgnoreAspectRatio, Qt::FastTransformation); } void HistogramGenerator::drawComponentFull(QPainter *davinci, const int *y, const float &scaling, const QRect &rect, const QColor &color, int textSpace, bool unscaled, uint max) const { QImage component = drawComponent(y, rect.size() - QSize(0, textSpace), scaling, color, unscaled, max); davinci->drawImage(rect.topLeft(), component); uint min = 0; for (uint x = 0; x < max; ++x) { min = x; if (y[x] > 0) { break; } } int maxVal = (int)max - 1; for (int x = (int)max - 1; x >= 0; --x) { maxVal = x; if (y[x] > 0) { break; } } const int textY = rect.bottom() - textSpace + 15; const int dist = 40; const int cw = component.width(); davinci->drawText(0, textY, i18n("min")); davinci->drawText(dist, textY, QString::number(min, 'f', 0)); davinci->drawText(cw - dist - 30, textY, i18n("max")); davinci->drawText(cw - 30, textY, QString::number(maxVal, 'f', 0)); } diff --git a/src/scopes/colorscopes/rgbparade.h b/src/scopes/colorscopes/rgbparade.h index 94451f99b..361182c49 100644 --- a/src/scopes/colorscopes/rgbparade.h +++ b/src/scopes/colorscopes/rgbparade.h @@ -1,53 +1,53 @@ /*************************************************************************** * 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. * ***************************************************************************/ #ifndef RGBPARADE_H #define RGBPARADE_H #include "abstractgfxscopewidget.h" #include "ui_rgbparade_ui.h" class QImage; class RGBParade_UI; class RGBParadeGenerator; /** * \brief Displays the RGB waveform of a frame. * This is the same as the Waveform, but for each colour channel separately. */ class RGBParade : public AbstractGfxScopeWidget { public: explicit RGBParade(QWidget *parent = nullptr); - ~RGBParade(); + ~RGBParade() override; QString widgetName() const override; protected: void readConfig() override; void writeConfig(); QRect scopeRect() override; private: Ui::RGBParade_UI *m_ui; RGBParadeGenerator *m_rgbParadeGenerator; QAction *m_aAxis; QAction *m_aGradRef; bool isHUDDependingOnInput() const override; bool isScopeDependingOnInput() const override; bool isBackgroundDependingOnInput() const override; QImage renderHUD(uint accelerationFactor) override; QImage renderGfxScope(uint accelerationFactor, const QImage &) override; QImage renderBackground(uint accelerationFactor) override; }; #endif // RGBPARADE_H diff --git a/src/scopes/colorscopes/vectorscope.cpp b/src/scopes/colorscopes/vectorscope.cpp index 6a956ad79..6adbdb436 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) + { m_ui = new Ui::Vectorscope_UI(); m_ui->setupUi(this); m_colorTools = new ColorTools(); m_vectorscopeGenerator = new VectorscopeGenerator(); m_ui->paintMode->addItem(i18n("Green 2"), QVariant(VectorscopeGenerator::PaintMode_Green2)); m_ui->paintMode->addItem(i18n("Green"), QVariant(VectorscopeGenerator::PaintMode_Green)); m_ui->paintMode->addItem(i18n("Black"), QVariant(VectorscopeGenerator::PaintMode_Black)); m_ui->paintMode->addItem(i18n("Modified YUV (Chroma)"), QVariant(VectorscopeGenerator::PaintMode_Chroma)); m_ui->paintMode->addItem(i18n("YUV"), QVariant(VectorscopeGenerator::PaintMode_YUV)); m_ui->paintMode->addItem(i18n("Original Color"), QVariant(VectorscopeGenerator::PaintMode_Original)); m_ui->backgroundMode->addItem(i18n("None"), QVariant(BG_NONE)); m_ui->backgroundMode->addItem(i18n("YUV"), QVariant(BG_YUV)); m_ui->backgroundMode->addItem(i18n("Modified YUV (Chroma)"), QVariant(BG_CHROMA)); m_ui->backgroundMode->addItem(i18n("YPbPr"), QVariant(BG_YPbPr)); m_ui->sliderGain->setMinimum(0); m_ui->sliderGain->setMaximum(40); connect(m_ui->backgroundMode, SIGNAL(currentIndexChanged(int)), this, SLOT(slotBackgroundChanged())); connect(m_ui->sliderGain, &QAbstractSlider::valueChanged, this, &Vectorscope::slotGainChanged); connect(m_ui->paintMode, SIGNAL(currentIndexChanged(int)), this, SLOT(forceUpdateScope())); connect(this, &Vectorscope::signalMousePositionChanged, this, &Vectorscope::forceUpdateHUD); m_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(m_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 m_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)); m_ui->backgroundMode->setCurrentIndex(scopeConfig.readEntry("backgroundmode").toInt()); m_ui->paintMode->setCurrentIndex(scopeConfig.readEntry("paintmode").toInt()); m_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", m_ui->backgroundMode->currentIndex()); scopeConfig.writeEntry("paintmode", m_ui->paintMode->currentIndex()); scopeConfig.writeEntry("gain", m_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, m_ui->verticalSpacer->geometry().y() + border); QPoint bottomright(m_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 m_cw = (scopeRect.height() < scopeRect.width()) ? scopeRect.height() : scopeRect.width(); scopeRect.setWidth(m_cw); scopeRect.setHeight(m_cw); m_centerPoint = m_vectorscopeGenerator->mapToCircle(scopeRect.size(), QPointF(0, 0)); m_pR75 = m_vectorscopeGenerator->mapToCircle(scopeRect.size(), P75 * VectorscopeGenerator::scaling * YUV_R); m_pG75 = m_vectorscopeGenerator->mapToCircle(scopeRect.size(), P75 * VectorscopeGenerator::scaling * YUV_G); m_pB75 = m_vectorscopeGenerator->mapToCircle(scopeRect.size(), P75 * VectorscopeGenerator::scaling * YUV_B); m_pCy75 = m_vectorscopeGenerator->mapToCircle(scopeRect.size(), P75 * VectorscopeGenerator::scaling * YUV_Cy); m_pMg75 = m_vectorscopeGenerator->mapToCircle(scopeRect.size(), P75 * VectorscopeGenerator::scaling * YUV_Mg); m_pYl75 = m_vectorscopeGenerator->mapToCircle(scopeRect.size(), P75 * VectorscopeGenerator::scaling * YUV_Yl); m_qR75 = m_vectorscopeGenerator->mapToCircle(scopeRect.size(), P75 * VectorscopeGenerator::scaling * YPbPr_R); m_qG75 = m_vectorscopeGenerator->mapToCircle(scopeRect.size(), P75 * VectorscopeGenerator::scaling * YPbPr_G); m_qB75 = m_vectorscopeGenerator->mapToCircle(scopeRect.size(), P75 * VectorscopeGenerator::scaling * YPbPr_B); m_qCy75 = m_vectorscopeGenerator->mapToCircle(scopeRect.size(), P75 * VectorscopeGenerator::scaling * YPbPr_Cy); m_qMg75 = m_vectorscopeGenerator->mapToCircle(scopeRect.size(), P75 * VectorscopeGenerator::scaling * YPbPr_Mg); m_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 (m_ui->backgroundMode->itemData(m_ui->backgroundMode->currentIndex()).toInt()) { case BG_NONE: davinci.setPen(penLight); break; default: if (r > m_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(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 (m_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)m_ui->paintMode->itemData(m_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 (m_ui->backgroundMode->itemData(m_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 (m_ui->backgroundMode->itemData(m_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 (m_ui->backgroundMode->itemData(m_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, m_cw, m_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 (m_ui->backgroundMode->itemData(m_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 (m_ui->backgroundMode->itemData(m_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(m_pR75, m_pYl75); davinci.drawLine(m_pYl75, m_pG75); davinci.drawLine(m_pG75, m_pCy75); davinci.drawLine(m_pCy75, m_pB75); davinci.drawLine(m_pB75, m_pMg75); davinci.drawLine(m_pMg75, m_pR75); } else { davinci.drawLine(m_qR75, m_qYl75); davinci.drawLine(m_qYl75, m_qG75); davinci.drawLine(m_qG75, m_qCy75); davinci.drawLine(m_qCy75, m_qB75); davinci.drawLine(m_qB75, m_qMg75); davinci.drawLine(m_qMg75, m_qR75); } } // Draw RGB/CMY points with 75% chroma (for NTSC) davinci.setPen(penThin); if (m_aColorSpace_YUV->isChecked()) { davinci.drawEllipse(m_pR75, 3, 3); davinci.drawEllipse(m_pG75, 3, 3); davinci.drawEllipse(m_pB75, 3, 3); davinci.drawEllipse(m_pCy75, 3, 3); davinci.drawEllipse(m_pMg75, 3, 3); davinci.drawEllipse(m_pYl75, 3, 3); } else { davinci.drawEllipse(m_qR75, 3, 3); davinci.drawEllipse(m_qG75, 3, 3); davinci.drawEllipse(m_qB75, 3, 3); davinci.drawEllipse(m_qCy75, 3, 3); davinci.drawEllipse(m_qMg75, 3, 3); davinci.drawEllipse(m_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; m_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 (m_ui->backgroundMode->itemData(m_ui->backgroundMode->currentIndex()).toInt()) { case BG_YUV: index = m_ui->paintMode->findData(QVariant(VectorscopeGenerator::PaintMode_Black)); if (index >= 0) { m_ui->paintMode->setCurrentIndex(index); } break; case BG_NONE: if (m_ui->paintMode->itemData(m_ui->paintMode->currentIndex()).toInt() == VectorscopeGenerator::PaintMode_Black) { index = m_ui->paintMode->findData(QVariant(VectorscopeGenerator::PaintMode_Green2)); m_ui->paintMode->setCurrentIndex(index); } break; } forceUpdateBackground(); } void Vectorscope::slotColorSpaceChanged() { int index; if (m_aColorSpace_YPbPr->isChecked()) { if (m_ui->backgroundMode->itemData(m_ui->backgroundMode->currentIndex()).toInt() == BG_YUV) { index = m_ui->backgroundMode->findData(QVariant(BG_YPbPr)); if (index >= 0) { m_ui->backgroundMode->setCurrentIndex(index); } } } else { if (m_ui->backgroundMode->itemData(m_ui->backgroundMode->currentIndex()).toInt() == BG_YPbPr) { index = m_ui->backgroundMode->findData(QVariant(BG_YUV)); if (index >= 0) { m_ui->backgroundMode->setCurrentIndex(index); } } } forceUpdate(); } diff --git a/src/scopes/colorscopes/vectorscope.h b/src/scopes/colorscopes/vectorscope.h index b6f994fa7..59651611e 100644 --- a/src/scopes/colorscopes/vectorscope.h +++ b/src/scopes/colorscopes/vectorscope.h @@ -1,91 +1,91 @@ /*************************************************************************** * 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. * ***************************************************************************/ #ifndef VECTORSCOPE_H #define VECTORSCOPE_H #include "abstractgfxscopewidget.h" #include "ui_vectorscope_ui.h" class ColorTools; class Vectorscope_UI; class VectorscopeGenerator; enum BACKGROUND_MODE { BG_NONE = 0, BG_YUV = 1, BG_CHROMA = 2, BG_YPbPr = 3 }; /** \brief Displays the vectorscope of a frame. \see VectorscopeGenerator for more details about the vectorscope. */ class Vectorscope : public AbstractGfxScopeWidget { Q_OBJECT public: explicit Vectorscope(QWidget *parent = nullptr); - ~Vectorscope(); + ~Vectorscope() override; QString widgetName() const override; protected: ///// Implemented methods ///// QRect scopeRect() override; QImage renderHUD(uint accelerationFactor) override; QImage renderGfxScope(uint accelerationFactor, const QImage &) override; QImage renderBackground(uint accelerationFactor) override; bool isHUDDependingOnInput() const override; bool isScopeDependingOnInput() const override; bool isBackgroundDependingOnInput() const override; void readConfig() override; ///// Other ///// void writeConfig(); private: Ui::Vectorscope_UI *m_ui; ColorTools *m_colorTools; QActionGroup *m_agColorSpace; QAction *m_aColorSpace_YUV; QAction *m_aColorSpace_YPbPr; QAction *m_aExportBackground; QAction *m_aAxisEnabled; QAction *m_a75PBox; QAction *m_aIQLines; VectorscopeGenerator *m_vectorscopeGenerator; /** How to represent the pixels on the scope (green, original color, ...) */ int m_iPaintMode; /** Custom scaling of the vectorscope */ - float m_gain; + float m_gain{1}; QPoint m_centerPoint, m_pR75, m_pG75, m_pB75, m_pCy75, m_pMg75, m_pYl75; QPoint m_qR75, m_qG75, m_qB75, m_qCy75, m_qMg75, m_qYl75; /** Unlike the scopeRect, this rect represents the overall visible rectangle and not only the square touching the Vectorscope's circle. */ QRect m_visibleRect; /** Updates the dimension. Only necessary when the widget has been resized. */ void updateDimensions(); int m_cw; private slots: void slotGainChanged(int); void slotBackgroundChanged(); void slotExportBackground(); void slotColorSpaceChanged(); }; #endif // VECTORSCOPE_H diff --git a/src/scopes/colorscopes/vectorscopegenerator.cpp b/src/scopes/colorscopes/vectorscopegenerator.cpp index f29e15637..3de98610e 100644 --- a/src/scopes/colorscopes/vectorscopegenerator.cpp +++ b/src/scopes/colorscopes/vectorscopegenerator.cpp @@ -1,268 +1,268 @@ /*************************************************************************** * 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. * ***************************************************************************/ /** Vectorscope. The basis matrix for converting RGB to YUV is (in this example for calculating the YUV value of red): mRgb2Yuv = r = 0.29900 0.58700 0.11400 1.00000 -0.14741 -0.28939 0.43680 x 0.00000 0.61478 -0.51480 -0.09998 0.00000 The resulting YUV value is then drawn on the circle using U and V as coordinate values. The maximum length of such an UV vector is reached for the colors Red and Cyan: 0.632. To make optimal use of space in the circle, this value can be used for scaling. As we are dealing with RGB values in a range of {0,...,255} and the conversion values are made for [0,1], we already divide the conversion values by 255 previously, e.g. in GNU octave. The basis matrix for converting RGB to YPbPr is: mRgb2YPbPr = r = 0.299000 0.587000 0.114000 1.0000 -0.168736 -0.331264 0.500000 x 0.0000 0.500000 -0.418688 -0.081312 0.0000 Note that YUV and YPbPr are not the same. * YUV is used for analog transfer of PAL/NTSC * YPbPr is used for analog transfer of YCbCr sources like DVD/DVB. It does _not_ matter for the Vectorscope what color space the input video is; The used color space is just a tool to visualize the hue. The difference is merely that the scope looks slightly different when choosing the respectively other color space; The way the Vectorscope works stays the same. YCbCr is basically YPbPr for digital use (it is defined on a range of {16,219} for Y and {16,240} for Cb/Cr components). See also: http://www.poynton.com/ColorFAQ.html http://de.wikipedia.org/wiki/Vektorskop http://www.elektroniktutor.de/techno/vektskop.html */ #include "vectorscopegenerator.h" #include -#include +#include // The maximum distance from the center for any RGB color is 0.63, so // no need to make the circle bigger than required. const float SCALING = 1 / .7; const float VectorscopeGenerator::scaling = 1 / .7; /** Input point is on [-1,1]², 0 being at the center, and positive directions are →top/→right. Maps to the coordinates used in QImages with the 0 point at the top left corner. -1 +1 +1+-----------+ | + | | --0++ | | - | -1+-----------+ vvv mapped to v 0 x 0+------+ |0++ | |- | |- | y+------+ With y: 1. Scale from [-1,1] to [0,1] with y01 := (y+1)/2 2. Invert (Orientation of the y axis changes) with y10 := 1-y01 3. Scale from [1,0] to [height-1,0] with yy := (height-1) * y10 x does not need to be inverted. */ QPoint VectorscopeGenerator::mapToCircle(const QSize &targetSize, const QPointF &point) const { - return QPoint((targetSize.width() - 1) * (point.x() + 1) / 2, (targetSize.height() - 1) * (1 - (point.y() + 1) / 2)); + return {int((targetSize.width() - 1) * (point.x() + 1) / 2), int((targetSize.height() - 1) * (1 - (point.y() + 1) / 2))}; } QImage VectorscopeGenerator::calculateVectorscope(const QSize &vectorscopeSize, const QImage &image, const float &gain, const VectorscopeGenerator::PaintMode &paintMode, const VectorscopeGenerator::ColorSpace &colorSpace, bool, uint accelFactor) const { if (vectorscopeSize.width() <= 0 || vectorscopeSize.height() <= 0 || image.width() <= 0 || image.height() <= 0) { // Invalid size return QImage(); } // Prepare the vectorscope data const int cw = (vectorscopeSize.width() < vectorscopeSize.height()) ? vectorscopeSize.width() : vectorscopeSize.height(); QImage scope = QImage(cw, cw, QImage::Format_ARGB32); scope.fill(qRgba(0, 0, 0, 0)); const uchar *bits = image.bits(); double dy, dr, dg, db, dmax; double /*y,*/ u, v; QPoint pt; QRgb px; const int stepsize = int(uint(image.depth() / 8) * accelFactor); // Just an average for the number of image pixels per scope pixel. // NOTE: byteCount() has to be replaced by (img.bytesPerLine()*img.height()) for Qt 4.5 to compile, see: // http://doc.trolltech.org/4.6/qimage.html#bytesPerLine double avgPxPerPx = (double)image.depth() / 8 * (image.bytesPerLine() * image.height()) / scope.size().width() / scope.size().height() / accelFactor; for (int i = 0; i < (image.bytesPerLine() * image.height()); i += stepsize) { auto *col = (const QRgb *)(bits); int r = qRed(*col); int g = qGreen(*col); int b = qBlue(*col); switch (colorSpace) { case VectorscopeGenerator::ColorSpace_YUV: // y = (double) 0.001173 * r +0.002302 * g +0.0004471* b; u = (double)-0.0005781 * r - 0.001135 * g + 0.001713 * b; v = (double)0.002411 * r - 0.002019 * g - 0.0003921 * b; break; case VectorscopeGenerator::ColorSpace_YPbPr: default: // y = (double) 0.001173 * r +0.002302 * g +0.0004471* b; u = (double)-0.0006671 * r - 0.001299 * g + 0.0019608 * b; v = (double)0.001961 * r - 0.001642 * g - 0.0003189 * b; break; } pt = mapToCircle(vectorscopeSize, QPointF(SCALING * gain * u, SCALING * gain * v)); if (pt.x() >= scope.width() || pt.x() < 0 || pt.y() >= scope.height() || pt.y() < 0) { // Point lies outside (because of scaling), don't plot it } else { // Draw the pixel using the chosen draw mode. switch (paintMode) { case PaintMode_YUV: // see yuvColorWheel dy = 128; // Default Y value. Lower = darker. // Calculate the RGB values from YUV/YPbPr switch (colorSpace) { case VectorscopeGenerator::ColorSpace_YUV: dr = dy + 290.8 * v; dg = dy - 100.6 * u - 148 * v; db = dy + 517.2 * u; break; case VectorscopeGenerator::ColorSpace_YPbPr: default: dr = dy + 357.5 * v; dg = dy - 87.75 * u - 182 * v; db = dy + 451.9 * u; break; } if (dr < 0) { dr = 0; } if (dg < 0) { dg = 0; } if (db < 0) { db = 0; } if (dr > 255) { dr = 255; } if (dg > 255) { dg = 255; } if (db > 255) { db = 255; } scope.setPixel(pt, qRgba(dr, dg, db, 255)); break; case PaintMode_Chroma: dy = 200; // Default Y value. Lower = darker. // Calculate the RGB values from YUV/YPbPr switch (colorSpace) { case VectorscopeGenerator::ColorSpace_YUV: dr = dy + 290.8 * v; dg = dy - 100.6 * u - 148 * v; db = dy + 517.2 * u; break; case VectorscopeGenerator::ColorSpace_YPbPr: default: dr = dy + 357.5 * v; dg = dy - 87.75 * u - 182 * v; db = dy + 451.9 * u; break; } // Scale the RGB values back to max 255 dmax = dr; if (dg > dmax) { dmax = dg; } if (db > dmax) { dmax = db; } dmax = 255 / dmax; dr *= dmax; dg *= dmax; db *= dmax; scope.setPixel(pt, qRgba(dr, dg, db, 255)); break; case PaintMode_Original: scope.setPixel(pt, *col); break; case PaintMode_Green: px = scope.pixel(pt); scope.setPixel(pt, qRgba(qRed(px) + (255 - qRed(px)) / (3 * avgPxPerPx), qGreen(px) + 20 * (255 - qGreen(px)) / (avgPxPerPx), qBlue(px) + (255 - qBlue(px)) / (avgPxPerPx), qAlpha(px) + (255 - qAlpha(px)) / (avgPxPerPx))); break; case PaintMode_Green2: px = scope.pixel(pt); scope.setPixel(pt, qRgba(qRed(px) + ceil((255 - (float)qRed(px)) / (4 * avgPxPerPx)), 255, qBlue(px) + ceil((255 - (float)qBlue(px)) / (avgPxPerPx)), qAlpha(px) + ceil((255 - (float)qAlpha(px)) / (avgPxPerPx)))); break; case PaintMode_Black: px = scope.pixel(pt); scope.setPixel(pt, qRgba(0, 0, 0, qAlpha(px) + (255 - qAlpha(px)) / 20)); break; } } bits += stepsize; } return scope; } diff --git a/src/scopes/colorscopes/waveform.cpp b/src/scopes/colorscopes/waveform.cpp index 3752db6eb..6d67d0b1c 100644 --- a/src/scopes/colorscopes/waveform.cpp +++ b/src/scopes/colorscopes/waveform.cpp @@ -1,195 +1,195 @@ /*************************************************************************** * 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 "waveform.h" #include "waveformgenerator.h" // For reading out the project resolution #include "core.h" #include "profiles/profilemodel.hpp" #include "klocalizedstring.h" #include #include #include #include const QSize Waveform::m_textWidth(35, 0); const int Waveform::m_paddingBottom(20); Waveform::Waveform(QWidget *parent) : AbstractGfxScopeWidget(true, parent) - , m_ui(nullptr) + { m_ui = new Ui::Waveform_UI(); m_ui->setupUi(this); m_ui->paintMode->addItem(i18n("Yellow"), QVariant(WaveformGenerator::PaintMode_Yellow)); m_ui->paintMode->addItem(i18n("White"), QVariant(WaveformGenerator::PaintMode_White)); m_ui->paintMode->addItem(i18n("Green"), QVariant(WaveformGenerator::PaintMode_Green)); m_aRec601 = new QAction(i18n("Rec. 601"), this); m_aRec601->setCheckable(true); m_aRec709 = new QAction(i18n("Rec. 709"), this); m_aRec709->setCheckable(true); m_agRec = new QActionGroup(this); m_agRec->addAction(m_aRec601); m_agRec->addAction(m_aRec709); m_menu->addSeparator()->setText(i18n("Luma mode")); m_menu->addAction(m_aRec601); m_menu->addAction(m_aRec709); connect(m_ui->paintMode, SIGNAL(currentIndexChanged(int)), this, SLOT(forceUpdateScope())); connect(this, &Waveform::signalMousePositionChanged, this, &Waveform::forceUpdateHUD); connect(m_aRec601, &QAction::toggled, this, &Waveform::forceUpdateScope); connect(m_aRec709, &QAction::toggled, this, &Waveform::forceUpdateScope); init(); m_waveformGenerator = new WaveformGenerator(); } Waveform::~Waveform() { writeConfig(); delete m_waveformGenerator; delete m_aRec601; delete m_aRec709; delete m_agRec; delete m_ui; } void Waveform::readConfig() { AbstractGfxScopeWidget::readConfig(); KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup scopeConfig(config, configName()); m_ui->paintMode->setCurrentIndex(scopeConfig.readEntry("paintmode", 0)); m_aRec601->setChecked(scopeConfig.readEntry("rec601", false)); m_aRec709->setChecked(!m_aRec601->isChecked()); } void Waveform::writeConfig() { KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup scopeConfig(config, configName()); scopeConfig.writeEntry("paintmode", m_ui->paintMode->currentIndex()); scopeConfig.writeEntry("rec601", m_aRec601->isChecked()); scopeConfig.sync(); } QRect Waveform::scopeRect() { // Distance from top/left/right int border = 6; QPoint topleft(border, m_ui->verticalSpacer->geometry().y() + border); return QRect(topleft, this->size() - QSize(border + topleft.x(), border + topleft.y())); } ///// Implemented methods ///// QString Waveform::widgetName() const { return QStringLiteral("Waveform"); } bool Waveform::isHUDDependingOnInput() const { return false; } bool Waveform::isScopeDependingOnInput() const { return true; } bool Waveform::isBackgroundDependingOnInput() const { return false; } QImage Waveform::renderHUD(uint) { QImage hud(m_scopeRect.size(), QImage::Format_ARGB32); hud.fill(qRgba(0, 0, 0, 0)); QPainter davinci(&hud); davinci.setPen(penLight); // qCDebug(KDENLIVE_LOG) << values.value("width"); const int rightX = scopeRect().width() - m_textWidth.width() + 3; const int x = m_mousePos.x() - scopeRect().x(); const int y = m_mousePos.y() - scopeRect().y(); if (scopeRect().height() > 0 && m_mouseWithinWidget) { int val = int(255. * (1. - (float)y / (float)scopeRect().height())); if (val >= 0 && val <= 255) { // Draw a horizontal line through the current mouse position // and show the value of the waveform there davinci.drawLine(0, y, scopeRect().size().width() - m_textWidth.width(), y); // Make the value stick to the line unless it is at the top/bottom of the scope int valY = y + 5; const int top = 30; const int bottom = 20; if (valY < top) { valY = top; } else if (valY > scopeRect().height() - bottom) { valY = scopeRect().height() - bottom; } davinci.drawText(rightX, valY, QVariant(val).toString()); } if (scopeRect().width() > 0) { // Draw a vertical line and the x position of the source clip const int profileWidth = pCore->getCurrentProfile()->width(); const int clipX = int((float)x / float(scopeRect().width() - m_textWidth.width() - 1) * float(profileWidth - 1)); if (clipX >= 0 && clipX <= profileWidth) { int valX = x - 15; if (valX < 0) { valX = 0; } if (valX > scopeRect().width() - 55 - m_textWidth.width()) { valX = scopeRect().width() - 55 - m_textWidth.width(); } davinci.drawLine(x, y, x, scopeRect().height() - m_paddingBottom); davinci.drawText(valX, scopeRect().height() - 5, QVariant(clipX).toString() + QStringLiteral(" px")); } } } davinci.drawText(rightX, scopeRect().height() - m_paddingBottom, QStringLiteral("0")); davinci.drawText(rightX, 10, QStringLiteral("255")); emit signalHUDRenderingFinished(0, 1); return hud; } QImage Waveform::renderGfxScope(uint accelFactor, const QImage &qimage) { QTime start = QTime::currentTime(); start.start(); const int paintmode = m_ui->paintMode->itemData(m_ui->paintMode->currentIndex()).toInt(); WaveformGenerator::Rec rec = m_aRec601->isChecked() ? WaveformGenerator::Rec_601 : WaveformGenerator::Rec_709; QImage wave = m_waveformGenerator->calculateWaveform(scopeRect().size() - m_textWidth - QSize(0, m_paddingBottom), qimage, (WaveformGenerator::PaintMode)paintmode, true, rec, accelFactor); emit signalScopeRenderingFinished((uint)start.elapsed(), 1); return wave; } QImage Waveform::renderBackground(uint) { emit signalBackgroundRenderingFinished(0, 1); return QImage(); } diff --git a/src/scopes/colorscopes/waveform.h b/src/scopes/colorscopes/waveform.h index cd1877f38..9ca0c8f4d 100644 --- a/src/scopes/colorscopes/waveform.h +++ b/src/scopes/colorscopes/waveform.h @@ -1,63 +1,63 @@ /*************************************************************************** * 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. * ***************************************************************************/ #ifndef WAVEFORM_H #define WAVEFORM_H #include "abstractgfxscopewidget.h" #include "ui_waveform_ui.h" class Waveform_UI; class WaveformGenerator; /** \brief Displays the waveform of a frame. For further explanations of the waveform see the WaveformGenerator class. */ class Waveform : public AbstractGfxScopeWidget { Q_OBJECT public: explicit Waveform(QWidget *parent = nullptr); - ~Waveform(); + ~Waveform() override; QString widgetName() const override; protected: void readConfig() override; void writeConfig(); private: - Ui::Waveform_UI *m_ui; + Ui::Waveform_UI *m_ui{nullptr}; WaveformGenerator *m_waveformGenerator; QAction *m_aRec601; QAction *m_aRec709; QActionGroup *m_agRec; static const QSize m_textWidth; static const int m_paddingBottom; QImage m_waveform; /// Implemented methods /// QRect scopeRect() override; QImage renderHUD(uint) override; QImage renderGfxScope(uint, const QImage &) override; QImage renderBackground(uint) override; bool isHUDDependingOnInput() const override; bool isScopeDependingOnInput() const override; bool isBackgroundDependingOnInput() const override; }; #endif // WAVEFORM_H diff --git a/src/scopes/colorscopes/waveformgenerator.h b/src/scopes/colorscopes/waveformgenerator.h index 1db5c1ff1..46cbb3246 100644 --- a/src/scopes/colorscopes/waveformgenerator.h +++ b/src/scopes/colorscopes/waveformgenerator.h @@ -1,33 +1,33 @@ /*************************************************************************** * 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. * ***************************************************************************/ #ifndef WAVEFORMGENERATOR_H #define WAVEFORMGENERATOR_H #include class QImage; class QSize; class WaveformGenerator : public QObject { Q_OBJECT public: enum PaintMode { PaintMode_Green, PaintMode_Yellow, PaintMode_White }; enum Rec { Rec_601, Rec_709 }; WaveformGenerator(); - ~WaveformGenerator(); + ~WaveformGenerator() override; QImage calculateWaveform(const QSize &waveformSize, const QImage &image, WaveformGenerator::PaintMode paintMode, bool drawAxis, const WaveformGenerator::Rec rec, uint accelFactor = 1); }; #endif // WAVEFORMGENERATOR_H diff --git a/src/scopes/scopemanager.cpp b/src/scopes/scopemanager.cpp index 329e675fc..e068015fc 100644 --- a/src/scopes/scopemanager.cpp +++ b/src/scopes/scopemanager.cpp @@ -1,325 +1,325 @@ /*************************************************************************** * Copyright (C) 2011 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 "scopemanager.h" #include "audioscopes/audiosignal.h" #include "audioscopes/audiospectrum.h" #include "audioscopes/spectrogram.h" #include "colorscopes/histogram.h" #include "colorscopes/rgbparade.h" #include "colorscopes/vectorscope.h" #include "colorscopes/waveform.h" #include "core.h" #include "definitions.h" #include "kdenlivesettings.h" #include "mainwindow.h" #include "monitor/monitormanager.h" #include "klocalizedstring.h" #include //#define DEBUG_SM #ifdef DEBUG_SM #include #endif ScopeManager::ScopeManager(QObject *parent) : QObject(parent) - , m_lastConnectedRenderer(nullptr) + { m_signalMapper = new QSignalMapper(this); connect(pCore->monitorManager(), &MonitorManager::checkColorScopes, this, &ScopeManager::slotUpdateActiveRenderer); connect(pCore->monitorManager(), &MonitorManager::clearScopes, this, &ScopeManager::slotClearColorScopes); connect(pCore->monitorManager(), &MonitorManager::checkScopes, this, &ScopeManager::slotCheckActiveScopes); connect(m_signalMapper, SIGNAL(mapped(QString)), SLOT(slotRequestFrame(QString))); slotUpdateActiveRenderer(); createScopes(); } bool ScopeManager::addScope(AbstractAudioScopeWidget *audioScope, QDockWidget *audioScopeWidget) { bool added = false; int exists = 0; // Only add the scope if it does not exist yet in the list - for (int i = 0; i < m_audioScopes.size(); ++i) { - if (m_audioScopes[i].scope == audioScope) { + for (auto &m_audioScope : m_audioScopes) { + if (m_audioScope.scope == audioScope) { exists = 1; break; } } if (exists == 0) { // Add scope to the list, set up signal/slot connections #ifdef DEBUG_SM qCDebug(KDENLIVE_LOG) << "Adding scope to scope manager: " << audioScope->widgetName(); #endif m_signalMapper->setMapping(audioScopeWidget, QString(audioScope->widgetName())); AudioScopeData asd; asd.scope = audioScope; m_audioScopes.append(asd); connect(audioScope, &AbstractScopeWidget::requestAutoRefresh, this, &ScopeManager::slotCheckActiveScopes); if (audioScopeWidget != nullptr) { connect(audioScopeWidget, &QDockWidget::visibilityChanged, this, &ScopeManager::slotCheckActiveScopes); connect(audioScopeWidget, SIGNAL(visibilityChanged(bool)), m_signalMapper, SLOT(map())); } added = true; } return added; } bool ScopeManager::addScope(AbstractGfxScopeWidget *colorScope, QDockWidget *colorScopeWidget) { bool added = false; int exists = 0; - for (int i = 0; i < m_colorScopes.size(); ++i) { - if (m_colorScopes[i].scope == colorScope) { + for (auto &m_colorScope : m_colorScopes) { + if (m_colorScope.scope == colorScope) { exists = 1; break; } } if (exists == 0) { #ifdef DEBUG_SM qCDebug(KDENLIVE_LOG) << "Adding scope to scope manager: " << colorScope->widgetName(); #endif m_signalMapper->setMapping(colorScopeWidget, QString(colorScope->widgetName())); GfxScopeData gsd; gsd.scope = colorScope; m_colorScopes.append(gsd); connect(colorScope, &AbstractScopeWidget::requestAutoRefresh, this, &ScopeManager::slotCheckActiveScopes); connect(colorScope, &AbstractGfxScopeWidget::signalFrameRequest, this, &ScopeManager::slotRequestFrame); connect(colorScope, &AbstractScopeWidget::signalScopeRenderingFinished, this, &ScopeManager::slotScopeReady); if (colorScopeWidget != nullptr) { connect(colorScopeWidget, &QDockWidget::visibilityChanged, this, &ScopeManager::slotCheckActiveScopes); connect(colorScopeWidget, SIGNAL(visibilityChanged(bool)), m_signalMapper, SLOT(map())); } added = true; } return added; } void ScopeManager::slotDistributeAudio(const audioShortVector &sampleData, int freq, int num_channels, int num_samples) { #ifdef DEBUG_SM qCDebug(KDENLIVE_LOG) << "ScopeManager: Starting to distribute audio."; #endif - for (int i = 0; i < m_audioScopes.size(); ++i) { + for (auto &m_audioScope : m_audioScopes) { // Distribute audio to all scopes that are visible and want to be refreshed - if (!m_audioScopes[i].scope->visibleRegion().isEmpty()) { - if (m_audioScopes[i].scope->autoRefreshEnabled()) { - m_audioScopes[i].scope->slotReceiveAudio(sampleData, freq, num_channels, num_samples); + if (!m_audioScope.scope->visibleRegion().isEmpty()) { + if (m_audioScope.scope->autoRefreshEnabled()) { + m_audioScope.scope->slotReceiveAudio(sampleData, freq, num_channels, num_samples); #ifdef DEBUG_SM qCDebug(KDENLIVE_LOG) << "ScopeManager: Distributed audio to " << m_audioScopes[i].scope->widgetName(); #endif } } } } void ScopeManager::slotDistributeFrame(const QImage &image) { #ifdef DEBUG_SM qCDebug(KDENLIVE_LOG) << "ScopeManager: Starting to distribute frame."; #endif - for (int i = 0; i < m_colorScopes.size(); ++i) { - if (!m_colorScopes[i].scope->visibleRegion().isEmpty()) { - if (m_colorScopes[i].scope->autoRefreshEnabled()) { - m_colorScopes[i].scope->slotRenderZoneUpdated(image); + for (auto &m_colorScope : m_colorScopes) { + if (!m_colorScope.scope->visibleRegion().isEmpty()) { + if (m_colorScope.scope->autoRefreshEnabled()) { + m_colorScope.scope->slotRenderZoneUpdated(image); #ifdef DEBUG_SM qCDebug(KDENLIVE_LOG) << "ScopeManager: Distributed frame to " << m_colorScopes[i].scope->widgetName(); #endif - } else if (m_colorScopes[i].singleFrameRequested) { + } else if (m_colorScope.singleFrameRequested) { // Special case: Auto refresh is disabled, but user requested an update (e.g. by clicking). // Force the scope to update. - m_colorScopes[i].singleFrameRequested = false; - m_colorScopes[i].scope->slotRenderZoneUpdated(image); - m_colorScopes[i].scope->forceUpdateScope(); + m_colorScope.singleFrameRequested = false; + m_colorScope.scope->slotRenderZoneUpdated(image); + m_colorScope.scope->forceUpdateScope(); #ifdef DEBUG_SM qCDebug(KDENLIVE_LOG) << "ScopeManager: Distributed forced frame to " << m_colorScopes[i].scope->widgetName(); #endif } } } // checkActiveColourScopes(); } void ScopeManager::slotScopeReady() { if (m_lastConnectedRenderer) { m_lastConnectedRenderer->scopesClear(); } } void ScopeManager::slotRequestFrame(const QString &widgetName) { #ifdef DEBUG_SM qCDebug(KDENLIVE_LOG) << "ScopeManager: New frame was requested by " << widgetName; #endif // Search for the scope in the lists and tag it to trigger a forced update // in the distribution slots - for (int i = 0; i < m_colorScopes.size(); ++i) { - if (m_colorScopes[i].scope->widgetName() == widgetName) { - m_colorScopes[i].singleFrameRequested = true; + for (auto &m_colorScope : m_colorScopes) { + if (m_colorScope.scope->widgetName() == widgetName) { + m_colorScope.singleFrameRequested = true; break; } } - for (int i = 0; i < m_audioScopes.size(); ++i) { - if (m_audioScopes[i].scope->widgetName() == widgetName) { - m_audioScopes[i].singleFrameRequested = true; + for (auto &m_audioScope : m_audioScopes) { + if (m_audioScope.scope->widgetName() == widgetName) { + m_audioScope.singleFrameRequested = true; break; } } if (m_lastConnectedRenderer) { // TODO: trigger refresh? m_lastConnectedRenderer->refreshMonitorIfActive(); // m_lastConnectedRenderer->sendFrameUpdate(); } } void ScopeManager::slotClearColorScopes() { m_lastConnectedRenderer = nullptr; } void ScopeManager::slotUpdateActiveRenderer() { // Disconnect old connections if (m_lastConnectedRenderer != nullptr) { #ifdef DEBUG_SM qCDebug(KDENLIVE_LOG) << "Disconnected previous renderer: " << m_lastConnectedRenderer->id(); #endif m_lastConnectedRenderer->disconnect(this); } m_lastConnectedRenderer = pCore->monitorManager()->activeMonitor(); // DVD monitor shouldn't be monitored or will cause crash on deletion if (pCore->monitorManager()->isActive(Kdenlive::DvdMonitor)) { m_lastConnectedRenderer = nullptr; } // Connect new renderer if (m_lastConnectedRenderer != nullptr) { connect(m_lastConnectedRenderer, &Monitor::frameUpdated, this, &ScopeManager::slotDistributeFrame, Qt::UniqueConnection); connect(m_lastConnectedRenderer, &Monitor::audioSamplesSignal, this, &ScopeManager::slotDistributeAudio, Qt::UniqueConnection); #ifdef DEBUG_SM qCDebug(KDENLIVE_LOG) << "Renderer connected to ScopeManager: " << m_lastConnectedRenderer->id(); #endif if (imagesAcceptedByScopes()) { #ifdef DEBUG_SM qCDebug(KDENLIVE_LOG) << "Some scopes accept images, triggering frame update."; #endif m_lastConnectedRenderer->refreshMonitorIfActive(); } } } void ScopeManager::slotCheckActiveScopes() { #ifdef DEBUG_SM qCDebug(KDENLIVE_LOG) << "Checking active scopes ..."; #endif // Leave a small delay to make sure that scope widget has been shown or hidden QTimer::singleShot(500, this, &ScopeManager::checkActiveAudioScopes); QTimer::singleShot(500, this, &ScopeManager::checkActiveColourScopes); } bool ScopeManager::audioAcceptedByScopes() const { bool accepted = false; - for (int i = 0; i < m_audioScopes.size(); ++i) { - if (m_audioScopes.at(i).scope->isVisible() && m_audioScopes.at(i).scope->autoRefreshEnabled()) { + for (auto m_audioScope : m_audioScopes) { + if (m_audioScope.scope->isVisible() && m_audioScope.scope->autoRefreshEnabled()) { accepted = true; break; } } #ifdef DEBUG_SM qCDebug(KDENLIVE_LOG) << "Any scope accepting audio? " << accepted; #endif return accepted; } bool ScopeManager::imagesAcceptedByScopes() const { bool accepted = false; - for (int i = 0; i < m_colorScopes.size(); ++i) { - if (!m_colorScopes[i].scope->visibleRegion().isEmpty() && m_colorScopes[i].scope->autoRefreshEnabled()) { + for (auto m_colorScope : m_colorScopes) { + if (!m_colorScope.scope->visibleRegion().isEmpty() && m_colorScope.scope->autoRefreshEnabled()) { accepted = true; break; } } #ifdef DEBUG_SM qCDebug(KDENLIVE_LOG) << "Any scope accepting images? " << accepted; #endif return accepted; } void ScopeManager::checkActiveAudioScopes() { bool audioStillRequested = audioAcceptedByScopes(); #ifdef DEBUG_SM qCDebug(KDENLIVE_LOG) << "ScopeManager: New audio data still requested? " << audioStillRequested; #endif KdenliveSettings::setMonitor_audio(audioStillRequested); pCore->monitorManager()->slotUpdateAudioMonitoring(); } void ScopeManager::checkActiveColourScopes() { bool imageStillRequested = imagesAcceptedByScopes(); #ifdef DEBUG_SM qCDebug(KDENLIVE_LOG) << "ScopeManager: New frames still requested? " << imageStillRequested; #endif // Notify monitors whether frames are still required Monitor *monitor; monitor = static_cast(pCore->monitorManager()->monitor(Kdenlive::ProjectMonitor)); if (monitor != nullptr) { monitor->sendFrameForAnalysis(imageStillRequested); } monitor = static_cast(pCore->monitorManager()->monitor(Kdenlive::ClipMonitor)); if (monitor != nullptr) { monitor->sendFrameForAnalysis(imageStillRequested); } } void ScopeManager::createScopes() { createScopeDock(new Vectorscope(pCore->window()), i18n("Vectorscope"), QStringLiteral("vectorscope")); createScopeDock(new Waveform(pCore->window()), i18n("Waveform"), QStringLiteral("waveform")); createScopeDock(new RGBParade(pCore->window()), i18n("RGB Parade"), QStringLiteral("rgb_parade")); createScopeDock(new Histogram(pCore->window()), i18n("Histogram"), QStringLiteral("histogram")); // Deprecated scopes // createScopeDock(new Spectrogram(pCore->window()), i18n("Spectrogram")); // createScopeDock(new AudioSignal(pCore->window()), i18n("Audio Signal")); // createScopeDock(new AudioSpectrum(pCore->window()), i18n("AudioSpectrum")); } template void ScopeManager::createScopeDock(T *scopeWidget, const QString &title, const QString &name) { QDockWidget *dock = pCore->window()->addDock(title, name, scopeWidget); addScope(scopeWidget, dock); // close for initial layout // actual state will be restored by session management dock->close(); } diff --git a/src/scopes/scopemanager.h b/src/scopes/scopemanager.h index dba117882..6478ccc40 100644 --- a/src/scopes/scopemanager.h +++ b/src/scopes/scopemanager.h @@ -1,139 +1,139 @@ /*************************************************************************** * Copyright (C) 2011 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. * ***************************************************************************/ #ifndef SCOPEMANAGER_H #define SCOPEMANAGER_H #include "audioscopes/abstractaudioscopewidget.h" #include "colorscopes/abstractgfxscopewidget.h" #include class QDockWidget; class AbstractMonitor; /** \brief Manages communication between Scopes and Renderer The scope manager handles the data transfer between the active Renderer and all scopes that have been registered via ScopeManager::addScope(AbstractAudioScopeWidget, QDockWidget) or ScopeManager::addScope(AbstractGfxScopeWidget, QDockWidget). It checks whether the renderer really needs to send data (it does not, for example, if no scopes are visible). */ class ScopeManager : public QObject { Q_OBJECT struct GfxScopeData { AbstractGfxScopeWidget *scope; bool singleFrameRequested; GfxScopeData() { scope = nullptr; singleFrameRequested = false; } }; struct AudioScopeData { AbstractAudioScopeWidget *scope; bool singleFrameRequested; AudioScopeData() { scope = nullptr; singleFrameRequested = false; } }; public: explicit ScopeManager(QObject *parent = nullptr); /** Adds a scope and sets up signal/slot connections to ensure that the scope receives data when required. \see addScope(AbstractGfxScopeWidget, QDockWidget) */ bool addScope(AbstractAudioScopeWidget *audioScope, QDockWidget *audioScopeWidget = nullptr); /** \see addScope(AbstractAudioScopeWidget, QDockWidget) */ bool addScope(AbstractGfxScopeWidget *colorScope, QDockWidget *colorScopeWidget = nullptr); private: QList m_audioScopes; QList m_colorScopes; - AbstractMonitor *m_lastConnectedRenderer; + AbstractMonitor *m_lastConnectedRenderer{nullptr}; QSignalMapper *m_signalMapper; /** Checks whether there is any scope accepting audio data, or if all of them are hidden or if auto refresh is disabled. \see imagesAcceptedByScopes(): Same for image data */ bool audioAcceptedByScopes() const; /** \see audioAcceptedByScopes() */ bool imagesAcceptedByScopes() const; /** Creates all the scopes in audioscopes/ and colorscopes/. New scopes are not detected automatically but have to be added. */ void createScopes(); /** Creates a dock for @param scopeWidget with the title @param title and adds it to the manager. @param scopeWidget has to be of type AbstractAudioScopeWidget or AbstractGfxScopeWidget (@see addScope). */ template void createScopeDock(T *scopeWidget, const QString &title, const QString &name); public slots: void slotCheckActiveScopes(); private slots: /** Updates the signal/slot connection since the active renderer has changed. */ void slotUpdateActiveRenderer(); /** The scope source was deleted, clear it. */ void slotClearColorScopes(); /** \see checkActiveAudioScopes() \see checkActiveColourScopes() */ /** Checks whether audio data is required, and notifies the renderer (enable or disable data sending). \see checkActiveAudioScopes() for image data */ void checkActiveAudioScopes(); /** Checks whether any scope accepts frames, and notifies the renderer. \see checkActiveAudioScopes() for audio data */ void checkActiveColourScopes(); void slotDistributeFrame(const QImage &image); void slotDistributeAudio(const audioShortVector &sampleData, int freq, int num_channels, int num_samples); /** Allows a scope to explicitly request a new frame, even if the scope's autoRefresh is disabled. */ void slotRequestFrame(const QString &widgetName); void slotScopeReady(); }; #endif // SCOPEMANAGER_H diff --git a/src/simplekeyframes/simplekeyframewidget.h b/src/simplekeyframes/simplekeyframewidget.h index ed0d8e75a..66863488e 100644 --- a/src/simplekeyframes/simplekeyframewidget.h +++ b/src/simplekeyframes/simplekeyframewidget.h @@ -1,64 +1,64 @@ /*************************************************************************** * Copyright (C) 2011 by Till Theato (root@ttill.de) * * 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 SIMPLEKEYFRAMEWIDGET_H #define SIMPLEKEYFRAMEWIDGET_H #include "timecode.h" #include class SimpleTimelineWidget; class TimecodeDisplay; class QToolButton; class SimpleKeyframeWidget : public QWidget { Q_OBJECT public: explicit SimpleKeyframeWidget(const Timecode &t, int duration, QWidget *parent = nullptr); - ~SimpleKeyframeWidget(); + ~SimpleKeyframeWidget() override; int getPosition() const; void setKeyframes(const QList &keyframes); void addKeyframe(int pos = -1); void updateTimecodeFormat(); public slots: void slotSetPosition(int pos = -1, bool update = true); private slots: void slotAtKeyframe(bool atKeyframe); signals: void positionChanged(int pos); void keyframeAdded(int pos); void keyframeRemoved(int pos); void keyframeMoved(int oldPos, int newPos); private: SimpleTimelineWidget *m_timeline; QToolButton *m_buttonAddDelete; QToolButton *m_buttonPrevious; QToolButton *m_buttonNext; TimecodeDisplay *m_time; }; #endif diff --git a/src/simplekeyframes/simpletimelinewidget.cpp b/src/simplekeyframes/simpletimelinewidget.cpp index b31c2ea30..cea26e0df 100644 --- a/src/simplekeyframes/simpletimelinewidget.cpp +++ b/src/simplekeyframes/simpletimelinewidget.cpp @@ -1,317 +1,312 @@ /*************************************************************************** * Copyright (C) 2011 by Till Theato (root@ttill.de) * * 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 "simpletimelinewidget.h" #include "kdenlivesettings.h" #include #include #include #include SimpleTimelineWidget::SimpleTimelineWidget(QWidget *parent) : QWidget(parent) - , m_duration(1) - , m_position(0) - , m_currentKeyframe(-1) - , m_currentKeyframeOriginal(-1) - , m_hoverKeyframe(-1) - , m_scale(1) + { setMouseTracking(true); setMinimumSize(QSize(150, 20)); setSizePolicy(QSizePolicy(QSizePolicy::Expanding, 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; setMinimumHeight(m_size); setMaximumHeight(m_size); } void SimpleTimelineWidget::setKeyframes(const QList &keyframes) { m_keyframes = keyframes; qSort(m_keyframes); m_currentKeyframe = m_currentKeyframeOriginal = -1; emit atKeyframe(m_keyframes.contains(m_position)); update(); } void SimpleTimelineWidget::slotSetPosition(int pos) { if (pos != m_position) { m_position = pos; emit atKeyframe(m_keyframes.contains(m_position)); update(); } } void SimpleTimelineWidget::slotAddKeyframe(int pos, int select) { if (pos < 0) { pos = m_position; } m_keyframes.append(pos); qSort(m_keyframes); if (select != 0) { m_currentKeyframe = m_currentKeyframeOriginal = pos; } update(); emit keyframeAdded(pos); if (pos == m_position) { emit atKeyframe(true); } } void SimpleTimelineWidget::slotAddRemove() { if (m_keyframes.contains(m_position)) { slotRemoveKeyframe(m_position); } else { slotAddKeyframe(m_position); } } void SimpleTimelineWidget::slotRemoveKeyframe(int pos) { m_keyframes.removeAll(pos); if (m_currentKeyframe == pos) { m_currentKeyframe = m_currentKeyframeOriginal = -1; } update(); emit keyframeRemoved(pos); if (pos == m_position) { emit atKeyframe(false); } } void SimpleTimelineWidget::setDuration(int dur) { m_duration = dur; } void SimpleTimelineWidget::slotGoToNext() { if (m_position == m_duration) { return; } for (const int &keyframe : m_keyframes) { if (keyframe > m_position) { slotSetPosition(keyframe); emit positionChanged(keyframe); emit atKeyframe(true); return; } } // no keyframe after current position slotSetPosition(m_duration); emit positionChanged(m_duration); emit atKeyframe(false); } void SimpleTimelineWidget::slotGoToPrev() { if (m_position == 0) { return; } for (int i = m_keyframes.count() - 1; i >= 0; --i) { const int framePos(m_keyframes.at(i)); if (framePos < m_position) { slotSetPosition(framePos); emit positionChanged(framePos); emit atKeyframe(true); return; } } // no keyframe before current position slotSetPosition(0); emit positionChanged(0); emit atKeyframe(false); } void SimpleTimelineWidget::mousePressEvent(QMouseEvent *event) { int pos = event->x() / m_scale; if (event->y() < m_lineHeight && event->button() == Qt::LeftButton) { for (const int &keyframe : m_keyframes) { if (qAbs(keyframe - pos) < 5) { m_currentKeyframeOriginal = keyframe; m_keyframes[m_keyframes.indexOf(keyframe)] = pos; m_currentKeyframe = pos; update(); return; } } } // no keyframe next to mouse m_currentKeyframe = m_currentKeyframeOriginal = -1; m_position = pos; emit positionChanged(pos); emit atKeyframe(m_keyframes.contains(pos)); update(); } void SimpleTimelineWidget::mouseMoveEvent(QMouseEvent *event) { int pos = qBound(0, (int)(event->x() / m_scale), m_duration); if ((event->buttons() & Qt::LeftButton) != 0u) { if (m_currentKeyframe >= 0) { if (!m_keyframes.contains(pos)) { // snap to position cursor if (KdenliveSettings::snaptopoints() && qAbs(pos - m_position) < 5 && !m_keyframes.contains(m_position)) { pos = m_position; } // should we maybe sort here? m_keyframes[m_keyframes.indexOf(m_currentKeyframe)] = pos; m_currentKeyframe = pos; emit keyframeMoving(m_currentKeyframeOriginal, m_currentKeyframe); emit atKeyframe(m_keyframes.contains(m_position)); } } else { m_position = pos; emit positionChanged(pos); emit atKeyframe(m_keyframes.contains(pos)); } update(); return; } if (event->y() < m_lineHeight) { for (const int &keyframe : m_keyframes) { if (qAbs(keyframe - pos) < 5) { m_hoverKeyframe = keyframe; setCursor(Qt::PointingHandCursor); update(); return; } } } if (m_hoverKeyframe != -1) { m_hoverKeyframe = -1; setCursor(Qt::ArrowCursor); update(); } } void SimpleTimelineWidget::mouseReleaseEvent(QMouseEvent *event) { Q_UNUSED(event) if (m_currentKeyframe >= 0) { qSort(m_keyframes); emit keyframeMoved(m_currentKeyframeOriginal, m_currentKeyframe); } } void SimpleTimelineWidget::mouseDoubleClickEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton && event->y() < m_lineHeight) { int pos = qBound(0, (int)(event->x() / m_scale), m_duration); for (const int &keyframe : m_keyframes) { if (qAbs(keyframe - pos) < 5) { m_keyframes.removeAll(keyframe); if (keyframe == m_currentKeyframe) { m_currentKeyframe = m_currentKeyframeOriginal = -1; } emit keyframeRemoved(keyframe); if (keyframe == m_position) { emit atKeyframe(false); } return; } } // add new keyframe m_keyframes.append(pos); qSort(m_keyframes); emit keyframeAdded(pos); if (pos == m_position) { emit atKeyframe(true); } } else { QWidget::mouseDoubleClickEvent(event); } } void SimpleTimelineWidget::wheelEvent(QWheelEvent *event) { int change = event->delta() < 0 ? 1 : -1; /*if (m_currentKeyframe > 0) { m_currentKeyframe = qBound(0, m_currentKeyframe + change, m_duration); emit keyframeMoved(m_currentKeyframeOriginal, m_currentKeyframe); } else { */ m_position = qBound(0, m_position + change, m_duration); emit positionChanged(m_position); // } emit atKeyframe(m_keyframes.contains(m_position)); update(); } void SimpleTimelineWidget::paintEvent(QPaintEvent *event) { Q_UNUSED(event) QStylePainter p(this); m_scale = width() / (double)(m_duration); // p.translate(0, m_lineHeight); int headOffset = m_lineHeight / 1.5; /* * keyframes */ for (const int &pos : m_keyframes) { if (pos == m_currentKeyframe || pos == m_hoverKeyframe) { p.setBrush(m_colSelected); } else { p.setBrush(m_colKeyframe); } int scaledPos = pos * m_scale; p.drawLine(scaledPos, headOffset, scaledPos, m_lineHeight + (headOffset / 2)); p.drawEllipse(scaledPos - headOffset / 2, 0, headOffset, headOffset); } p.setPen(palette().dark().color()); /* * Time-"line" */ p.setPen(m_colKeyframe); p.drawLine(0, m_lineHeight + (headOffset / 2), width(), m_lineHeight + (headOffset / 2)); /* * current position */ 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_position * m_scale, 0); p.setBrush(m_colKeyframe); p.drawPolygon(position); } diff --git a/src/simplekeyframes/simpletimelinewidget.h b/src/simplekeyframes/simpletimelinewidget.h index 7674950bc..51720c385 100644 --- a/src/simplekeyframes/simpletimelinewidget.h +++ b/src/simplekeyframes/simpletimelinewidget.h @@ -1,75 +1,75 @@ /*************************************************************************** * Copyright (C) 2011 by Till Theato (root@ttill.de) * * 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 SIMPLETIMELINEWIDGET_H #define SIMPLETIMELINEWIDGET_H #include class SimpleTimelineWidget : public QWidget { Q_OBJECT public: explicit SimpleTimelineWidget(QWidget *parent = nullptr); void setKeyframes(const QList &keyframes); void setDuration(int dur); public slots: void slotSetPosition(int pos); void slotRemoveKeyframe(int pos); void slotAddKeyframe(int pos = -1, int select = false); void slotAddRemove(); void slotGoToNext(); void slotGoToPrev(); protected: void paintEvent(QPaintEvent *event) override; void mousePressEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override; void mouseDoubleClickEvent(QMouseEvent *event) override; void wheelEvent(QWheelEvent *event) override; private: - int m_duration; - int m_position; - int m_currentKeyframe; - int m_currentKeyframeOriginal; - int m_hoverKeyframe; + int m_duration{1}; + int m_position{0}; + int m_currentKeyframe{-1}; + int m_currentKeyframeOriginal{-1}; + int m_hoverKeyframe{-1}; QList m_keyframes; int m_lineHeight; - double m_scale; + double m_scale{1}; int m_size; QColor m_colSelected; QColor m_colKeyframe; QColor m_colKeyframeBg; signals: void positionChanged(int pos); void atKeyframe(bool); void keyframeSelected(); void keyframeMoving(int oldPos, int currentPos); void keyframeMoved(int oldPos, int newPos); void keyframeAdded(int pos); void keyframeRemoved(int pos); }; #endif diff --git a/src/statusbarmessagelabel.cpp b/src/statusbarmessagelabel.cpp index 7926a37b3..b0c7d82d2 100644 --- a/src/statusbarmessagelabel.cpp +++ b/src/statusbarmessagelabel.cpp @@ -1,299 +1,299 @@ /*************************************************************************** * Copyright (C) 2006 by Peter Penz * * 2012 Simon A. Eugster * * peter.penz@gmx.at * * Code borrowed from Dolphin, adapted (2008) to Kdenlive 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 "statusbarmessagelabel.h" #include "kdenlivesettings.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include FlashLabel::FlashLabel(QWidget *parent) : QWidget(parent) { setAutoFillBackground(true); } -FlashLabel::~FlashLabel() {} +FlashLabel::~FlashLabel() = default; void FlashLabel::setColor(const QColor &col) { QPalette pal = palette(); pal.setColor(QPalette::Window, col); setPalette(pal); update(); } QColor FlashLabel::color() const { return palette().window().color(); } StatusBarMessageLabel::StatusBarMessageLabel(QWidget *parent) : FlashLabel(parent) , m_minTextHeight(-1) , m_queueSemaphore(1) { setMinimumHeight(KIconLoader::SizeSmall); auto *lay = new QHBoxLayout(this); m_pixmap = new QLabel(this); m_pixmap->setAlignment(Qt::AlignCenter); m_label = new QLabel(this); m_label->setAlignment(Qt::AlignLeft); m_progress = new QProgressBar(this); lay->addWidget(m_pixmap); lay->addWidget(m_label); lay->addWidget(m_progress); setLayout(lay); m_progress->setVisible(false); lay->setContentsMargins(BorderGap, 0, 2 * BorderGap, 0); m_queueTimer.setSingleShot(true); connect(&m_queueTimer, &QTimer::timeout, this, &StatusBarMessageLabel::slotMessageTimeout); connect(m_label, &QLabel::linkActivated, this, &StatusBarMessageLabel::slotShowJobLog); } -StatusBarMessageLabel::~StatusBarMessageLabel() {} +StatusBarMessageLabel::~StatusBarMessageLabel() = default; void StatusBarMessageLabel::mousePressEvent(QMouseEvent *event) { QWidget::mousePressEvent(event); if (m_pixmap->rect().contains(event->localPos().toPoint()) && m_currentMessage.type == MltError) { confirmErrorMessage(); } } void StatusBarMessageLabel::setProgressMessage(const QString &text, int progress, MessageType type, int timeoutMS) { if (type == ProcessingJobMessage) { m_progress->setValue(progress); m_progress->setVisible(progress < 100); } else if (m_currentMessage.type != ProcessingJobMessage || type == OperationCompletedMessage) { m_progress->setVisible(progress < 100); } if (text == m_currentMessage.text) { return; } setMessage(text, type, timeoutMS); } void StatusBarMessageLabel::setMessage(const QString &text, MessageType type, int timeoutMS) { StatusBarMessageItem item(text, type, timeoutMS); if (type == OperationCompletedMessage) { m_progress->setVisible(false); } if (item.type == ErrorMessage || item.type == MltError) { KNotification::event(QStringLiteral("ErrorMessage"), item.text); } m_queueSemaphore.acquire(); if (!m_messageQueue.contains(item)) { if (item.type == ErrorMessage || item.type == MltError || item.type == ProcessingJobMessage) { qCDebug(KDENLIVE_LOG) << item.text; // Put the new error message at first place and immediately show it if (item.timeoutMillis < 3000) { item.timeoutMillis = 3000; } if (item.type == ProcessingJobMessage) { // This is a job progress info, discard previous ones QList cleanList; for (const StatusBarMessageItem &msg : m_messageQueue) { if (msg.type != ProcessingJobMessage) { cleanList << msg; } } m_messageQueue = cleanList; } else { // Important error message, delete previous queue so they don't appear afterwards out of context m_messageQueue.clear(); } m_messageQueue.push_front(item); // In case we are already displaying an error message, add a little delay int delay = 800 * static_cast(m_currentMessage.type == ErrorMessage || m_currentMessage.type == MltError); m_queueTimer.start(delay); } else { // Message with normal priority m_messageQueue.push_back(item); if (m_queueTimer.elapsed() >= m_currentMessage.timeoutMillis) { m_queueTimer.start(0); } } } m_queueSemaphore.release(); } bool StatusBarMessageLabel::slotMessageTimeout() { m_queueSemaphore.acquire(); bool newMessage = false; // Get the next message from the queue, unless the current one needs to be confirmed if (m_currentMessage.type == ProcessingJobMessage) { // Check if we have a job completed message to cancel this one StatusBarMessageItem item; while (!m_messageQueue.isEmpty()) { item = m_messageQueue.at(0); m_messageQueue.removeFirst(); if (item.type == OperationCompletedMessage || item.type == ErrorMessage || item.type == MltError || item.type == ProcessingJobMessage) { m_currentMessage = item; m_label->setText(m_currentMessage.text); newMessage = true; break; } } } else if (!m_messageQueue.isEmpty()) { if (!m_currentMessage.needsConfirmation()) { m_currentMessage = m_messageQueue.at(0); m_label->setText(m_currentMessage.text); m_messageQueue.removeFirst(); newMessage = true; } } // If the queue is empty, add a default (empty) message if (m_messageQueue.isEmpty() && m_currentMessage.type != DefaultMessage) { m_messageQueue.push_back(StatusBarMessageItem()); } // Start a new timer, unless the current message still needs to be confirmed if (!m_messageQueue.isEmpty()) { if (!m_currentMessage.needsConfirmation()) { // If we only have the default message left to show in the queue, // keep the current one for a little longer. m_queueTimer.start(m_currentMessage.timeoutMillis + 4000 * static_cast(m_messageQueue.at(0).type == DefaultMessage)); } } QColor bgColor = KStatefulBrush(KColorScheme::Window, KColorScheme::NegativeBackground).brush(this).color(); const char *iconName = nullptr; setColor(parentWidget()->palette().window().color()); switch (m_currentMessage.type) { case ProcessingJobMessage: iconName = "chronometer"; m_pixmap->setCursor(Qt::ArrowCursor); break; case OperationCompletedMessage: iconName = "dialog-ok"; m_pixmap->setCursor(Qt::ArrowCursor); break; case InformationMessage: { iconName = "dialog-information"; m_pixmap->setCursor(Qt::ArrowCursor); QPropertyAnimation *anim = new QPropertyAnimation(this, "color", this); anim->setDuration(1500); anim->setEasingCurve(QEasingCurve::InOutQuad); anim->setKeyValueAt(0.2, parentWidget()->palette().highlight().color()); anim->setEndValue(parentWidget()->palette().window().color()); anim->start(QPropertyAnimation::DeleteWhenStopped); break; } case ErrorMessage: { iconName = "dialog-warning"; m_pixmap->setCursor(Qt::ArrowCursor); QPropertyAnimation *anim = new QPropertyAnimation(this, "color", this); anim->setStartValue(bgColor); anim->setKeyValueAt(0.8, bgColor); anim->setEndValue(parentWidget()->palette().window().color()); anim->setEasingCurve(QEasingCurve::OutCubic); anim->setDuration(4000); anim->start(QPropertyAnimation::DeleteWhenStopped); break; } case MltError: { iconName = "dialog-close"; m_pixmap->setCursor(Qt::PointingHandCursor); QPropertyAnimation *anim = new QPropertyAnimation(this, "color", this); anim->setStartValue(bgColor); anim->setEndValue(bgColor); anim->setEasingCurve(QEasingCurve::OutCubic); anim->setDuration(1500); anim->start(QPropertyAnimation::DeleteWhenStopped); break; } case DefaultMessage: m_pixmap->setCursor(Qt::ArrowCursor); default: break; } if (iconName == nullptr) { m_pixmap->setVisible(false); } else { m_pixmap->setPixmap(SmallIcon(iconName)); m_pixmap->setVisible(true); } m_queueSemaphore.release(); return newMessage; } void StatusBarMessageLabel::confirmErrorMessage() { m_currentMessage.confirmed = true; m_queueTimer.start(0); } void StatusBarMessageLabel::resizeEvent(QResizeEvent *event) { QWidget::resizeEvent(event); } void StatusBarMessageLabel::slotShowJobLog(const QString &text) { QDialog d(this); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Close); QWidget *mainWidget = new QWidget(this); auto *l = new QVBoxLayout; QTextEdit t(&d); t.insertPlainText(QUrl::fromPercentEncoding(text.toUtf8())); t.setReadOnly(true); l->addWidget(&t); mainWidget->setLayout(l); auto *mainLayout = new QVBoxLayout; d.setLayout(mainLayout); mainLayout->addWidget(mainWidget); mainLayout->addWidget(buttonBox); d.connect(buttonBox, &QDialogButtonBox::rejected, &d, &QDialog::accept); d.exec(); confirmErrorMessage(); } diff --git a/src/statusbarmessagelabel.h b/src/statusbarmessagelabel.h index 2b973af45..9cb5dd949 100644 --- a/src/statusbarmessagelabel.h +++ b/src/statusbarmessagelabel.h @@ -1,134 +1,133 @@ /*************************************************************************** * Copyright (C) 2006 by Peter Penz * * 2012 Simon A. Eugster * * peter.penz@gmx.at * * Code borrowed from Dolphin, adapted (2008) to Kdenlive 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 STATUSBARMESSAGELABEL_H #define STATUSBARMESSAGELABEL_H #include #include #include #include #include #include - #include +#include #include "lib/qtimerWithTime.h" class QPaintEvent; class QResizeEvent; class QProgressBar; class FlashLabel : public QWidget { Q_PROPERTY(QColor color READ color WRITE setColor) Q_OBJECT public: explicit FlashLabel(QWidget *parent = nullptr); - ~FlashLabel(); + ~FlashLabel() override; QColor color() const; void setColor(const QColor &); }; /** Queue-able message item holding all important information */ struct StatusBarMessageItem { QString text; MessageType type; int timeoutMillis; - bool confirmed; ///< MLT errors need to be confirmed. + bool confirmed{false}; ///< MLT errors need to be confirmed. /// \return true if the error still needs to be confirmed bool needsConfirmation() const { return (type == MltError && !confirmed); } - StatusBarMessageItem(const QString &messageText = QString(), MessageType messageType = DefaultMessage, int timeoutMS = 0) - : text(messageText) + StatusBarMessageItem(QString messageText = QString(), MessageType messageType = DefaultMessage, int timeoutMS = 0) + : text(std::move(messageText)) , type(messageType) , timeoutMillis(timeoutMS) - , confirmed(false) { } bool operator==(const StatusBarMessageItem &other) const { return type == other.type && text == other.text; } }; /** * @brief Represents a message text label as part of the status bar. * * Dependent from the given type automatically a corresponding icon * is shown in front of the text. For message texts having the type * DolphinStatusBar::Error a dynamic color blending is done to get the * attention from the user. */ class StatusBarMessageLabel : public FlashLabel { Q_OBJECT public: explicit StatusBarMessageLabel(QWidget *parent); - virtual ~StatusBarMessageLabel(); + ~StatusBarMessageLabel() override; protected: // void paintEvent(QPaintEvent* event); void mousePressEvent(QMouseEvent *) override; /** @see QWidget::resizeEvent() */ void resizeEvent(QResizeEvent *event) override; public slots: void setProgressMessage(const QString &text, int progress = 100, MessageType type = ProcessingJobMessage, int timeoutMS = 0); void setMessage(const QString &text, MessageType type = DefaultMessage, int timeoutMS = 0); private slots: /** * Closes the currently shown error message and replaces it * by the next pending message. */ void confirmErrorMessage(); /** * Shows the next pending error message. If no pending message * was in the queue, false is returned. */ bool slotMessageTimeout(); void slotShowJobLog(const QString &text); private: enum { GeometryTimeout = 100 }; enum { BorderGap = 2 }; int m_minTextHeight; QLabel *m_pixmap; QLabel *m_label; QProgressBar *m_progress; QTimerWithTime m_queueTimer; QSemaphore m_queueSemaphore; QList m_messageQueue; StatusBarMessageItem m_currentMessage; }; #endif diff --git a/src/timecodedisplay.cpp b/src/timecodedisplay.cpp index 2ba5ecf4d..f61dbe978 100644 --- a/src/timecodedisplay.cpp +++ b/src/timecodedisplay.cpp @@ -1,241 +1,241 @@ /* This file is part of the KDE project Copyright (c) 2010 Jean-Baptiste Mardelle This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "timecodedisplay.h" #include "kdenlivesettings.h" #include #include #include #include #include MyValidator::MyValidator(QObject *parent) : QValidator(parent) { } void MyValidator::fixup(QString &str) const { str.replace(QLatin1Char(' '), QLatin1Char('0')); } QValidator::State MyValidator::validate(QString &str, int &) const { if (str.contains(QLatin1Char(' '))) { fixup(str); } return QValidator::Acceptable; } TimecodeDisplay::TimecodeDisplay(const Timecode &t, QWidget *parent) : QAbstractSpinBox(parent) , m_timecode(t) , m_frametimecode(false) , m_minimum(0) , m_maximum(-1) , m_value(0) { const QFont ft = QFontDatabase::systemFont(QFontDatabase::FixedFont); lineEdit()->setFont(ft); setFont(ft); lineEdit()->setAlignment(Qt::AlignRight | Qt::AlignVCenter); QFontMetrics fm(ft); setFrame(false); QPalette palette; palette.setColor(QPalette::Base, Qt::transparent); // palette.window().color()); setPalette(palette); setTimeCodeFormat(KdenliveSettings::frametimecode(), true); setValue(m_minimum); setMinimumWidth(fm.width(QStringLiteral("88:88:88:88")) + contentsMargins().right() + contentsMargins().left() + frameSize().width() - lineEdit()->contentsRect().width() + (int)QStyle::PM_SpinBoxFrameWidth + 6); setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Maximum); setAccelerated(true); connect(lineEdit(), &QLineEdit::editingFinished, this, &TimecodeDisplay::slotEditingFinished); } // virtual protected QAbstractSpinBox::StepEnabled TimecodeDisplay::stepEnabled() const { QAbstractSpinBox::StepEnabled result = QAbstractSpinBox::StepNone; if (m_value > m_minimum) { result |= QAbstractSpinBox::StepDownEnabled; } if (m_maximum == -1 || m_value < m_maximum) { result |= QAbstractSpinBox::StepUpEnabled; } return result; } // virtual void TimecodeDisplay::stepBy(int steps) { int val = m_value + steps; setValue(val); } void TimecodeDisplay::setTimeCodeFormat(bool frametimecode, bool init) { if (!init && m_frametimecode == frametimecode) { return; } m_frametimecode = frametimecode; lineEdit()->clear(); if (m_frametimecode) { auto *valid = new QIntValidator(lineEdit()); valid->setBottom(0); lineEdit()->setValidator(valid); lineEdit()->setInputMask(QString()); } else { lineEdit()->setInputMask(m_timecode.mask()); auto *valid = new MyValidator(lineEdit()); lineEdit()->setValidator(valid); } setValue(m_value); } void TimecodeDisplay::slotUpdateTimeCodeFormat() { setTimeCodeFormat(KdenliveSettings::frametimecode()); } void TimecodeDisplay::updateTimeCode(const Timecode &t) { m_timecode = t; setTimeCodeFormat(KdenliveSettings::frametimecode()); } void TimecodeDisplay::keyPressEvent(QKeyEvent *e) { if (e->key() == Qt::Key_Return || e->key() == Qt::Key_Enter) { e->setAccepted(true); clearFocus(); } else { QAbstractSpinBox::keyPressEvent(e); } } void TimecodeDisplay::mouseReleaseEvent(QMouseEvent *e) { QAbstractSpinBox::mouseReleaseEvent(e); if (!lineEdit()->underMouse()) { clearFocus(); } } void TimecodeDisplay::wheelEvent(QWheelEvent *e) { QAbstractSpinBox::wheelEvent(e); clearFocus(); } void TimecodeDisplay::enterEvent(QEvent *e) { QAbstractSpinBox::enterEvent(e); setFrame(true); } void TimecodeDisplay::leaveEvent(QEvent *e) { QAbstractSpinBox::leaveEvent(e); setFrame(false); } int TimecodeDisplay::maximum() const { return m_maximum; } int TimecodeDisplay::minimum() const { return m_minimum; } int TimecodeDisplay::getValue() const { return m_value; } GenTime TimecodeDisplay::gentime() const { - return GenTime(m_value, m_timecode.fps()); + return {m_value, m_timecode.fps()}; } Timecode TimecodeDisplay::timecode() const { return m_timecode; } void TimecodeDisplay::setRange(int min, int max) { m_minimum = min; m_maximum = max; } void TimecodeDisplay::setValue(const QString &value) { setValue(m_timecode.getFrameCount(value)); } void TimecodeDisplay::setValue(int value) { if (m_maximum > 0) { value = qBound(m_minimum, value, m_maximum); } else { value = qMax(m_minimum, value); } if (m_frametimecode) { if (value == m_value && !lineEdit()->text().isEmpty()) { return; } m_value = value; lineEdit()->setText(QString::number(value - m_minimum)); } else { if (value == m_value && lineEdit()->text() != QLatin1String(":::")) { return; } m_value = value; QString v = m_timecode.getTimecodeFromFrames(value - m_minimum); lineEdit()->setText(v); } } void TimecodeDisplay::setValue(const GenTime &value) { setValue((int)value.frames(m_timecode.fps())); } void TimecodeDisplay::slotEditingFinished() { lineEdit()->deselect(); if (m_frametimecode) { setValue(lineEdit()->text().toInt() + m_minimum); } else { setValue(m_timecode.getFrameCount(lineEdit()->text()) + m_minimum); } emit timeCodeEditingFinished(m_value); } const QString TimecodeDisplay::displayText() const { return lineEdit()->displayText(); } diff --git a/src/timeline2/model/clipmodel.cpp b/src/timeline2/model/clipmodel.cpp index dd576025d..b14e24036 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(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, 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, 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() {} +ClipModel::~ClipModel() = default; 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")); + return {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(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(std::move(stackModel), m_currentState); return true; } bool ClipModel::importEffects(std::weak_ptr service) { QWriteLocker locker(&m_lock); 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(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 6426aabd9..8302f1994 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(const std::shared_ptr &parent, std::shared_ptr prod, const QString &binClipId, int id, PlaylistState::ClipState state, double speed = 1.); public: - ~ClipModel(); + ~ClipModel() override; /* @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, 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(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(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.hpp b/src/timeline2/model/compositionmodel.hpp index e0ca0e051..0272e645a 100644 --- a/src/timeline2/model/compositionmodel.hpp +++ b/src/timeline2/model/compositionmodel.hpp @@ -1,123 +1,123 @@ /*************************************************************************** * Copyright (C) 2017 by Jean-Baptiste Mardelle * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #ifndef COMPOSITIONMODEL_H #define COMPOSITIONMODEL_H #include "assets/model/assetparametermodel.hpp" #include "moveableItem.hpp" #include "undohelper.hpp" #include #include namespace Mlt { class Transition; } class TimelineModel; class TrackModel; class KeyframeModel; /* @brief This class represents a Composition 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 CompositionModel : public MoveableItem, public AssetParameterModel { CompositionModel() = delete; protected: /* This constructor is not meant to be called, call the static construct instead */ CompositionModel(std::weak_ptr parent, std::unique_ptr transition, int id, const QDomElement &transitionXml, const QString &transitionId); public: /* @brief Creates a composition, which then registers itself to the parent timeline Returns the (unique) id of the created composition @param parent is a pointer to the timeline @param transitionId is the id of the transition to be inserted @param id Requested id of the clip. Automatic if -1 */ static int construct(const std::weak_ptr &parent, const QString &transitionId, int id = -1, std::unique_ptr sourceProperties = nullptr); friend class TrackModel; friend class TimelineModel; /* @brief returns the length of the item on the timeline */ int getPlaytime() const override; /* @brief Returns the id of the second track involved in the composition (a_track in mlt's vocabulary, the b_track being the track where the composition is inserted) */ int getATrack() const; /* @brief Defines the forced_track property. If true, the a_track will not change when composition * is moved to another track. When false, the a_track will automatically change to lower video track */ void setForceTrack(bool force); /* @brief Returns the id of the second track involved in the composition (a_track) or -1 if the a_track should be automatically updated when the composition * changes track */ int getForcedTrack() const; /* @brief Sets the id of the second track involved in the composition*/ void setATrack(int trackMltPosition, int trackId); /* @brief returns a property of the current item */ const QString getProperty(const QString &name) const override; /* @brief returns the active effect's keyframe model */ KeyframeModel *getEffectKeyframeModel(); Q_INVOKABLE bool showKeyframes() const; Q_INVOKABLE void setShowKeyframes(bool show); const QString &displayName() const; Mlt::Properties *properties(); /* @brief Returns an XML representation of the clip with its effects */ QDomElement toXml(QDomDocument &document); protected: Mlt::Transition *service() const override; void setInOut(int in, int out) override; void setCurrentTrackId(int tid) override; - virtual int getOut() const override; - virtual int getIn() const override; + int getOut() const override; + int getIn() const override; /* @brief Performs a resize of the given composition. 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 composition @param right is true if we change the right side of the composition, 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; private: int m_a_track; QString m_compositionName; int m_duration; }; #endif diff --git a/src/timeline2/model/groupsmodel.cpp b/src/timeline2/model/groupsmodel.cpp index f18b496b3..d6a9723b6 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); + for (auto &new_group : new_groups) { + new_group.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, 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/moveableItem.hpp b/src/timeline2/model/moveableItem.hpp index c5f84ccd7..462c98ac9 100644 --- a/src/timeline2/model/moveableItem.hpp +++ b/src/timeline2/model/moveableItem.hpp @@ -1,123 +1,123 @@ /*************************************************************************** * 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 MOVEABLEITEM_H #define MOVEABLEITEM_H #include "timelinemodel.hpp" #include "undohelper.hpp" #include #include #include /* @brief This is the base class for objects that can move, for example clips and compositions */ template class MoveableItem { MoveableItem() = delete; protected: - virtual ~MoveableItem() {} + virtual ~MoveableItem() = default; public: MoveableItem(std::weak_ptr parent, int id = -1); /* @brief returns (unique) id of current item */ int getId() const; /* @brief returns the length of the item on the timeline */ virtual int getPlaytime() const = 0; /* @brief returns the id of the track in which this items is inserted (-1 if none) */ int getCurrentTrackId() const; /* @brief returns the current position of the item (-1 if not inserted) */ int getPosition() const; /* @brief returns the in and out times of the item */ std::pair getInOut() const; virtual int getIn() const; virtual int getOut() const; friend class TrackModel; friend class TimelineModel; /* Implicit conversion operator to access the underlying producer */ operator Service &() { return *service(); } /* Returns true if the underlying producer is valid */ bool isValid(); /* @brief returns a property of the current item */ virtual const QString getProperty(const QString &name) const = 0; /* Set if the item is in grab state */ bool isGrabbed() const; void setGrab(bool grab); protected: /* @brief Returns a pointer to the service. It may be used but do NOT store it*/ virtual Service *service() const = 0; /* @brief Performs a resize of the given item. 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 item @param right is true if we change the right side of the item, 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 */ virtual bool requestResize(int size, bool right, Fun &undo, Fun &redo, bool logUndo = true) = 0; /* Updates the stored position of the item This function is meant to be called by the trackmodel, not directly by the user. If you wish to actually move the item, use the requestMove slot. */ void setPosition(int position); /* Updates the stored track id of the item This function is meant to be called by the timeline, not directly by the user. If you wish to actually change the track the item, use the slot in the timeline slot. */ virtual void setCurrentTrackId(int tid); /* Set in and out of service */ virtual void setInOut(int in, int out); protected: std::weak_ptr m_parent; int m_id; // this is the creation id of the item, used for book-keeping int m_position; int m_currentTrackId; bool m_grabbed; mutable QReadWriteLock m_lock; // This is a lock that ensures safety in case of concurrent access }; #include "moveableItem.ipp" #endif diff --git a/src/timeline2/model/snapmodel.cpp b/src/timeline2/model/snapmodel.cpp index 9858d9caf..384131d0e 100644 --- a/src/timeline2/model/snapmodel.cpp +++ b/src/timeline2/model/snapmodel.cpp @@ -1,129 +1,129 @@ /*************************************************************************** * 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 "snapmodel.hpp" #include +#include #include -#include SnapModel::SnapModel() = default; void SnapModel::addPoint(int position) { if (m_snaps.count(position) == 0) { m_snaps[position] = 1; } else { m_snaps[position]++; } } void SnapModel::removePoint(int position) { Q_ASSERT(m_snaps.count(position) > 0); if (m_snaps[position] == 1) { m_snaps.erase(position); } else { m_snaps[position]--; } } int SnapModel::getClosestPoint(int position) { if (m_snaps.empty()) { return -1; } auto it = m_snaps.lower_bound(position); long long int prev = INT_MIN, next = INT_MAX; if (it != m_snaps.end()) { next = (*it).first; } if (it != m_snaps.begin()) { --it; prev = (*it).first; } if (std::llabs((long long)position - prev) < std::llabs((long long)position - next)) { return (int)prev; } return (int)next; } int SnapModel::getNextPoint(int position) { if (m_snaps.empty()) { return position; } auto it = m_snaps.lower_bound(position + 1); long long int next = position; if (it != m_snaps.end()) { next = (*it).first; } return (int)next; } int SnapModel::getPreviousPoint(int position) { if (m_snaps.empty()) { return 0; } auto it = m_snaps.lower_bound(position); long long int prev = 0; if (it != m_snaps.begin()) { --it; prev = (*it).first; } return (int)prev; } void SnapModel::ignore(const std::vector &pts) { for (int pt : pts) { removePoint(pt); m_ignore.push_back(pt); } } void SnapModel::unIgnore() { for (const auto &pt : m_ignore) { addPoint(pt); } m_ignore.clear(); } int SnapModel::proposeSize(int in, int out, int size, bool right, int maxSnapDist) { ignore({in, out}); int proposed_size = -1; if (right) { int target_pos = in + size - 1; int snapped_pos = getClosestPoint(target_pos); if (snapped_pos != -1 && qAbs(target_pos - snapped_pos) <= maxSnapDist) { proposed_size = snapped_pos - in; } } else { int target_pos = out + 1 - size; int snapped_pos = getClosestPoint(target_pos); if (snapped_pos != -1 && qAbs(target_pos - snapped_pos) <= maxSnapDist) { proposed_size = out - snapped_pos; } } unIgnore(); return proposed_size; } diff --git a/src/timeline2/model/timelinefunctions.cpp b/src/timeline2/model/timelinefunctions.cpp index 63eaf9ab3..70737dbfc 100644 --- a/src/timeline2/model/timelinefunctions.cpp +++ b/src/timeline2/model/timelinefunctions.cpp @@ -1,988 +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(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(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(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(std::move(timeline), clipId, position, undo, redo); if (result) { pCore->pushUndo(undo, redo, i18n("Cut clip")); } return result; } 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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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()); + auto *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(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(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/timelineitemmodel.cpp b/src/timeline2/model/timelineitemmodel.cpp index 35e0249b2..ad3c66997 100644 --- a/src/timeline2/model/timelineitemmodel.cpp +++ b/src/timeline2/model/timelineitemmodel.cpp @@ -1,610 +1,609 @@ /*************************************************************************** * 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, std::move(undo_stack)) { } 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)); + ptr->m_groups = std::make_unique(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(); + return {}; } 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(); + return {}; } 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(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(); + ~TimelineItemModel() override; 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; + void _beginRemoveRows(const QModelIndex &, int, int) override; + void _beginInsertRows(const QModelIndex &, int, int) override; + void _endRemoveRows() override; + void _endInsertRows() override; + void _resetView() override; protected: // This is an helper function that finishes a construction of a freshly created TimelineItemModel 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 e286eee89..40474f483 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(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); 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}); + compos.emplace_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); } 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; + auto 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/timelinemodel.hpp b/src/timeline2/model/timelinemodel.hpp index aaa940832..c6560566f 100644 --- a/src/timeline2/model/timelinemodel.hpp +++ b/src/timeline2/model/timelinemodel.hpp @@ -1,751 +1,751 @@ /*************************************************************************** * 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 TIMELINEMODEL_H #define TIMELINEMODEL_H #include "definitions.h" #include "undohelper.hpp" #include #include -#include +#include #include #include #include #include #include class AssetParameterModel; class EffectStackModel; class ClipModel; class CompositionModel; class DocUndoStack; class GroupsModel; class SnapModel; class TimelineItemModel; class TrackModel; /* @brief This class represents a Timeline 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. This class also serves to keep track of all objects. It holds pointers to all tracks and clips, and gives them unique IDs on creation. These Ids are used in any interactions with the objects and have nothing to do with Melt IDs. This is the entry point for any modifications that has to be made on an element. The dataflow beyond this entry point may vary, for example when the user request a clip resize, the call is deferred to the clip itself, that check if there is enough data to extend by the requested amount, compute the new in and out, and then asks the track if there is enough room for extension. To avoid any confusion on which function to call first, rembember to always call the version in timeline. This is also required to generate the Undo/Redo operators The undo/redo system is designed around lambda functions. Each time a function executes an elementary change to the model, it writes the corresponding operation and its reverse, respectively in the redo and the undo lambdas. This way, if an operation fails for some reason, we can easily cancel the steps that have been done so far without corrupting anything. The other advantage is that operations are easy to compose, and you get a undo/redo pair for free no matter in which way you combine them. Most of the modification functions are named requestObjectAction. Eg, if the object is a clip and we want to move it, we call requestClipMove. These functions always return a bool indicating success, and when they return false they should guarantee than nothing has been modified. Most of the time, these functions come in two versions: the first one is the entry point if you want to perform only the action (and not compose it with other actions). This version will generally automatically push and Undo object on the Application stack, in case the user later wants to cancel the operation. It also generally goes the extra mile to ensure the operation is done in a way that match the user's expectation: for example requestClipMove checks whether the clip belongs to a group and in that case actually mouves the full group. The other version of the function, if it exists, is intended for composition (using the action as part of a complex operation). It takes as input the undo/redo lambda corresponding to the action that is being performed and accumulates on them. Note that this version does the minimal job: in the example of the requestClipMove, it will not move the full group if the clip is in a group. Generally speaking, we don't check ahead of time if an action is going to succeed or not before applying it. We just apply it naively, and if it fails at some point, we use the undo operator that we are constructing on the fly to revert what we have done so far. For example, when we move a group of clips, we apply the move operation to all the clips inside this group (in the right order). If none fails, we are good, otherwise we revert what we've already done. This kind of behaviour frees us from the burden of simulating the actions before actually applying theme. This is a good thing because this simulation step would be very sensitive to corruptions and small discrepancies, which we try to avoid at all cost. It derives from AbstractItemModel (indirectly through TimelineItemModel) 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 TimelineModel : public QAbstractItemModel_shared_from_this { Q_OBJECT protected: /* @brief this constructor should not be called. Call the static construct instead */ TimelineModel(Mlt::Profile *profile, std::weak_ptr undo_stack); public: friend class TrackModel; template friend class MoveableItem; friend class ClipModel; friend class CompositionModel; friend class GroupsModel; friend class TimelineController; friend struct TimelineFunctions; /// Two level model: tracks and clips on track enum { NameRole = Qt::UserRole + 1, ResourceRole, /// clip only ServiceRole, /// clip only StartRole, /// clip only BinIdRole, /// clip only TrackIdRole, FakeTrackIdRole, FakePositionRole, MarkersRole, /// clip only StatusRole, /// clip only TypeRole, /// clip only KeyframesRole, DurationRole, InPointRole, /// clip only OutPointRole, /// clip only FramerateRole, /// clip only GroupedRole, /// clip only HasAudio, /// clip only CanBeAudioRole, /// clip only CanBeVideoRole, /// clip only IsDisabledRole, /// track only IsAudioRole, SortRole, ShowKeyframesRole, AudioLevelsRole, /// clip only AudioChannelsRole, /// clip only IsCompositeRole, /// track only IsLockedRole, /// track only HeightRole, /// track only TrackTagRole, /// track only FadeInRole, /// clip only FadeOutRole, /// clip only FileHashRole, /// clip only SpeedRole, /// clip only ReloadThumbRole, /// clip only ItemATrack, /// composition only ItemIdRole, ThumbsFormatRole, /// track only EffectNamesRole, // track and clip only EffectsEnabledRole, // track and clip only GrabbedRole /// clip+composition only }; - virtual ~TimelineModel(); + ~TimelineModel() override; Mlt::Tractor *tractor() const { return m_tractor.get(); } /* @brief Load tracks from the current tractor, used on project opening */ void loadTractor(); /* @brief Returns the current tractor's producer, useful fo control seeking, playing, etc */ std::shared_ptr producer(); Mlt::Profile *getProfile(); /* @brief returns the number of tracks */ int getTracksCount() const; /* @brief returns the track index (id) from its position */ int getTrackIndexFromPosition(int pos) const; /* @brief returns the track index (id) from its position */ Q_INVOKABLE bool isAudioTrack(int trackId) const; /* @brief returns the number of clips */ int getClipsCount() const; /* @brief returns the number of compositions */ int getCompositionsCount() const; /* @brief Returns the id of the track containing clip (-1 if it is not inserted) @param clipId Id of the clip to test */ Q_INVOKABLE int getClipTrackId(int clipId) const; /* @brief Returns the id of the track containing composition (-1 if it is not inserted) @param clipId Id of the composition to test */ Q_INVOKABLE int getCompositionTrackId(int compoId) const; /* @brief Convenience function that calls either of the previous ones based on item type*/ Q_INVOKABLE int getItemTrackId(int itemId) const; Q_INVOKABLE int getCompositionPosition(int compoId) const; int getCompositionPlaytime(int compoId) const; /* Returns an item position, item can be clip or composition */ Q_INVOKABLE int getItemPosition(int itemId) const; /* Returns an item duration, item can be clip or composition */ int getItemPlaytime(int itemId) const; /* Returns the current speed of a clip */ double getClipSpeed(int clipId) const; /* @brief Helper function to query the amount of free space around a clip * @param clipId: the queried clip. If it is not inserted on a track, this functions returns 0 * @param after: if true, we return the blank after the clip, otherwise, before. */ int getBlankSizeNearClip(int clipId, bool after) const; /* @brief if the clip belongs to a AVSplit group, then return the id of the other corresponding clip. Otherwise, returns -1 */ int getClipSplitPartner(int clipId) const; /* @brief Helper function that returns true if the given ID corresponds to a clip */ bool isClip(int id) const; /* @brief Helper function that returns true if the given ID corresponds to a composition */ bool isComposition(int id) const; /* @brief Helper function that returns true if the given ID corresponds to a track */ bool isTrack(int id) const; /* @brief Helper function that returns true if the given ID corresponds to a track */ bool isGroup(int id) const; /* @brief Given a composition Id, returns its underlying parameter model */ std::shared_ptr getCompositionParameterModel(int compoId) const; /* @brief Given a clip Id, returns its underlying effect stack model */ std::shared_ptr getClipEffectStackModel(int clipId) const; /* @brief Returns the position of clip (-1 if it is not inserted) @param clipId Id of the clip to test */ Q_INVOKABLE int getClipPosition(int clipId) const; Q_INVOKABLE bool addClipEffect(int clipId, const QString &effectId, bool notify = true); Q_INVOKABLE bool addTrackEffect(int trackId, const QString &effectId); bool removeFade(int clipId, bool fromStart); Q_INVOKABLE bool copyClipEffect(int clipId, const QString &sourceId); Q_INVOKABLE bool copyTrackEffect(int trackId, const QString &sourceId); bool adjustEffectLength(int clipId, const QString &effectId, int duration, int initialDuration); /* @brief Returns the closest snap point within snapDistance */ Q_INVOKABLE int suggestSnapPoint(int pos, int snapDistance); /** @brief Return the previous track of same type as source trackId, or trackId if no track found */ Q_INVOKABLE int getPreviousTrackId(int trackId); /** @brief Return the next track of same type as source trackId, or trackId if no track found */ Q_INVOKABLE int getNextTrackId(int trackId); /* @brief Returns the in cut position of a clip @param clipId Id of the clip to test */ int getClipIn(int clipId) const; /* @brief Returns the clip state (audio/video only) */ PlaylistState::ClipState getClipState(int clipId) const; /* @brief Returns the bin id of the clip master @param clipId Id of the clip to test */ const QString getClipBinId(int clipId) const; /* @brief Returns the duration of a clip @param clipId Id of the clip to test */ int getClipPlaytime(int clipId) const; /* @brief Returns the size of the clip's frame (widthxheight) @param clipId Id of the clip to test */ QSize getClipFrameSize(int clipId) const; /* @brief Returns the number of clips in a given track @param trackId Id of the track to test */ int getTrackClipsCount(int trackId) const; /* @brief Returns the number of compositions in a given track @param trackId Id of the track to test */ int getTrackCompositionsCount(int trackId) const; /* @brief Returns the position of the track in the order of the tracks @param trackId Id of the track to test */ int getTrackPosition(int trackId) const; /* @brief Returns the track's index in terms of mlt's internal representation */ int getTrackMltIndex(int trackId) const; /* @brief Returns a sort position for tracks. * @param separated: if true, the tracks will be sorted like: V2,V1,A1,A2 * Otherwise, the tracks will be sorted like V2,A2,V1,A1 */ int getTrackSortValue(int trackId, bool separated) const; /* @brief Returns the ids of the tracks below the given track in the order of the tracks Returns an empty list if no track available @param trackId Id of the track to test */ QList getLowerTracksId(int trackId, TrackType type = TrackType::AnyTrack) const; /* @brief Returns the MLT track index of the video track just below the given track @param trackId Id of the track to test */ int getPreviousVideoTrackPos(int trackId) const; /* @brief Returns the Track id of the video track just below the given track @param trackId Id of the track to test */ int getPreviousVideoTrackIndex(int trackId) const; /* @brief Returns the Id of the corresponding audio track. If trackId corresponds to video1, this will return audio 1 and so on */ int getMirrorAudioTrackId(int trackId) const; int getMirrorVideoTrackId(int trackId) const; int getMirrorTrackId(int trackId) const; /* @brief Move a clip to a specific position This action is undoable Returns true on success. If it fails, nothing is modified. If the clip is not in inserted in a track yet, it gets inserted for the first time. If the clip is in a group, the call is deferred to requestGroupMove @param clipId is the ID of the clip @param trackId is the ID of the target track @param position is the position where we want to move @param updateView if set to false, no signal is sent to qml @param logUndo if set to false, no undo object is stored */ Q_INVOKABLE bool requestClipMove(int clipId, int trackId, int position, bool updateView = true, bool logUndo = true, bool invalidateTimeline = false); /* @brief Move a composition to a specific position This action is undoable Returns true on success. If it fails, nothing is modified. If the clip is not in inserted in a track yet, it gets inserted for the first time. If the clip is in a group, the call is deferred to requestGroupMove @param transid is the ID of the composition @param trackId is the ID of the track */ Q_INVOKABLE bool requestCompositionMove(int compoId, int trackId, int position, bool updateView = true, bool logUndo = true); /* Same function, but accumulates undo and redo, and doesn't check for group*/ bool requestClipMove(int clipId, int trackId, int position, bool updateView, bool invalidateTimeline, Fun &undo, Fun &redo); bool requestCompositionMove(int transid, int trackId, int compositionTrack, int position, bool updateView, bool finalMove, Fun &undo, Fun &redo); /* When timeline edit mode is insert or overwrite, we fake the move (as it will overlap existing clips, and only process the real move on drop */ bool fakeClipMove(int clipId, int trackId, int position, bool updateView, bool invalidateTimeline, Fun &undo, Fun &redo); bool requestFakeClipMove(int clipId, int trackId, int position, bool updateView, bool logUndo, bool invalidateTimeline); bool requestFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView = true, bool logUndo = true); bool requestFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool finalMove, Fun &undo, Fun &redo, bool allowViewRefresh = true); /* @brief Given an intended move, try to suggest a more valid one (accounting for snaps and missing UI calls) @param clipId id of the clip to move @param trackId id of the target track @param position target position @param snapDistance the maximum distance for a snap result, -1 for no snapping of the clip @param dontRefreshMasterClip when false, no view refresh is attempted */ Q_INVOKABLE int suggestItemMove(int itemId, int trackId, int position, int cursorPosition, int snapDistance = -1); Q_INVOKABLE int suggestClipMove(int clipId, int trackId, int position, int cursorPosition, int snapDistance = -1, bool allowViewUpdate = true); Q_INVOKABLE int suggestCompositionMove(int compoId, int trackId, int position, int cursorPosition, int snapDistance = -1); /* @brief Request clip insertion at given position. This action is undoable Returns true on success. If it fails, nothing is modified. @param binClipId id of the clip in the bin @param track Id of the track where to insert @param position Requested position @param ID return parameter of the id of the inserted clip @param logUndo if set to false, no undo object is stored @param refreshView whether the view should be refreshed @param useTargets: if true, the Audio/video split will occur on the set targets. Otherwise, they will be computed as an offset from the middle line */ bool requestClipInsertion(const QString &binClipId, int trackId, int position, int &id, bool logUndo = true, bool refreshView = false, bool useTargets = true); /* Same function, but accumulates undo and redo*/ bool requestClipInsertion(const QString &binClipId, int trackId, int position, int &id, bool logUndo, bool refreshView, bool useTargets, Fun &undo, Fun &redo); /* @brief Creates a new clip instance without inserting it. This action is undoable, returns true on success @param binClipId: Bin id of the clip to insert @param id: return parameter for the id of the newly created clip. @param state: The desired clip state (original, audio/video only). */ bool requestClipCreation(const QString &binClipId, int &id, PlaylistState::ClipState state, double speed, Fun &undo, Fun &redo); /* @brief Deletes the given clip or composition from the timeline This action is undoable Returns true on success. If it fails, nothing is modified. If the clip/composition is in a group, the call is deferred to requestGroupDeletion @param clipId is the ID of the clip/composition @param logUndo if set to false, no undo object is stored */ Q_INVOKABLE bool requestItemDeletion(int clipId, bool logUndo = true); /* Same function, but accumulates undo and redo*/ bool requestItemDeletion(int clipId, Fun &undo, Fun &redo); /* @brief Move a group to a specific position This action is undoable Returns true on success. If it fails, nothing is modified. If the clips in the group are not in inserted in a track yet, they get inserted for the first time. @param clipId is the id of the clip that triggers the group move @param groupId is the id of the group @param delta_track is the delta applied to the track index @param delta_pos is the requested position change @param updateView if set to false, no signal is sent to qml for the clip clipId @param logUndo if set to true, an undo object is created @param allowViewRefresh if false, the view will never get updated (useful for suggestMove) */ bool requestGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView = true, bool logUndo = true); bool requestGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool finalMove, Fun &undo, Fun &redo, bool allowViewRefresh = true); /* @brief Deletes all clips inside the group that contains the given clip. This action is undoable Note that if their is a hierarchy of groups, all of them will be deleted. Returns true on success. If it fails, nothing is modified. @param clipId is the id of the clip that triggers the group deletion */ Q_INVOKABLE bool requestGroupDeletion(int clipId, bool logUndo = true); bool requestGroupDeletion(int clipId, Fun &undo, Fun &redo); /* @brief Change the duration of an item (clip or composition) This action is undoable Returns the real size reached (can be different, if snapping occurs). If it fails, nothing is modified, and -1 is returned @param itemId is the ID of the item @param size is the new size of the item @param right is true if we change the right side of the item, false otherwise @param logUndo if set to true, an undo object is created @param snap if set to true, the resize order will be coerced to use the snapping grid */ Q_INVOKABLE int requestItemResize(int itemId, int size, bool right, bool logUndo = true, int snapDistance = -1, bool allowSingleResize = false); /* Same function, but accumulates undo and redo and doesn't deal with snapping*/ bool requestItemResize(int itemId, int size, bool right, bool logUndo, Fun &undo, Fun &redo, bool blockUndo = false); /* @brief Group together a set of ids The ids are either a group ids or clip ids. The involved clip must already be inserted in a track This action is undoable Returns the group id on success, -1 if it fails and nothing is modified. Typically, ids would be ids of clips, but for convenience, some of them can be ids of groups as well. @param ids Set of ids to group */ int requestClipsGroup(const std::unordered_set &ids, bool logUndo = true, GroupType type = GroupType::Normal); int requestClipsGroup(const std::unordered_set &ids, Fun &undo, Fun &redo, GroupType type = GroupType::Normal); /* @brief Destruct the topmost group containing clip This action is undoable Returns true on success. If it fails, nothing is modified. @param id of the clip to degroup (all clips belonging to the same group will be ungrouped as well) */ bool requestClipUngroup(int itemId, bool logUndo = true); /* Same function, but accumulates undo and redo*/ bool requestClipUngroup(int itemId, Fun &undo, Fun &redo); /* @brief Create a track at given position This action is undoable Returns true on success. If it fails, nothing is modified. @param Requested position (order). If set to -1, the track is inserted last. @param id is a return parameter that holds the id of the resulting track (-1 on failure) */ bool requestTrackInsertion(int pos, int &id, const QString &trackName = QString(), bool audioTrack = false); /* Same function, but accumulates undo and redo*/ bool requestTrackInsertion(int pos, int &id, const QString &trackName, bool audioTrack, Fun &undo, Fun &redo, bool updateView = true); /* @brief Delete track with given id This also deletes all the clips contained in the track. This action is undoable Returns true on success. If it fails, nothing is modified. @param trackId id of the track to delete */ bool requestTrackDeletion(int trackId); /* Same function, but accumulates undo and redo*/ bool requestTrackDeletion(int trackId, Fun &undo, Fun &redo); /* @brief Get project duration Returns the duration in frames */ int duration() const; static int seekDuration; // Duration after project end where seeking is allowed /* @brief Get all the elements of the same group as the given clip. If there is a group hierarchy, only the topmost group is considered. @param clipId id of the clip to test */ std::unordered_set getGroupElements(int clipId); /* @brief Removes all the elements on the timeline (tracks and clips) */ bool requestReset(Fun &undo, Fun &redo); /* @brief Updates the current the pointer to the current undo_stack Must be called for example when the doc change */ void setUndoStack(std::weak_ptr undo_stack); /* @brief Requests the best snapped position for a clip @param pos is the clip's requested position @param length is the clip's duration @param pts snap points to ignore (for example currently moved clip) @param snapDistance the maximum distance for a snap result, -1 for no snapping @returns best snap position or -1 if no snap point is near */ int requestBestSnapPos(int pos, int length, const std::vector &pts = std::vector(), int cursorPosition = 0, int snapDistance = -1); /* @brief Requests the next snapped point @param pos is the current position */ int requestNextSnapPos(int pos); /* @brief Requests the previous snapped point @param pos is the current position */ int requestPreviousSnapPos(int pos); /* @brief Add a new snap point @param pos is the current position */ void addSnap(int pos); /* @brief Remove snap point @param pos is the current position */ void removeSnap(int pos); /* @brief Request composition insertion at given position. This action is undoable Returns true on success. If it fails, nothing is modified. @param transitionId Identifier of the Mlt transition to insert (as given by repository) @param track Id of the track where to insert @param position Requested position @param length Requested initial length. @param id return parameter of the id of the inserted composition @param logUndo if set to false, no undo object is stored */ bool requestCompositionInsertion(const QString &transitionId, int trackId, int position, int length, std::unique_ptr transProps, int &id, bool logUndo = true); /* Same function, but accumulates undo and redo*/ bool requestCompositionInsertion(const QString &transitionId, int trackId, int compositionTrack, int position, int length, std::unique_ptr transProps, int &id, Fun &undo, Fun &redo, bool finalMove = false); /* @brief This function change the global (timeline-wise) enabled state of the effects It disables/enables track and clip effects (recursively) */ void setTimelineEffectsEnabled(bool enabled); /* @brief Get a timeline clip id by its position or -1 if not found */ int getClipByPosition(int trackId, int position) const; /* @brief Get a timeline composition id by its starting position or -1 if not found */ int getCompositionByPosition(int trackId, int position) const; /* @brief Returns a list of all items that are intersect with a given range. * @param trackId is the id of the track for concerned items. Setting trackId to -1 returns items on all tracks * @param start is the position where we the items should start * @param end is the position after which items will not be selected, set to -1 to get all clips on track * @param listCompositions if enabled, the list will also contains composition ids */ std::unordered_set getItemsInRange(int trackId, int start, int end = -1, bool listCompositions = true); /* @brief Returns a list of all luma files used in the project */ QStringList extractCompositionLumas() const; /* @brief Inform asset view of duration change */ virtual void adjustAssetRange(int clipId, int in, int out); void requestClipReload(int clipId); void requestClipUpdate(int clipId, const QVector &roles); /** @brief define current edit mode (normal, insert, overwrite */ void setEditMode(TimelineMode::EditMode mode); Q_INVOKABLE bool normalEdit() const; /** @brief Returns the effectstack of a given clip. */ std::shared_ptr getClipEffectStack(int itemId); std::shared_ptr getTrackEffectStackModel(int trackId); /** @brief Add slowmotion effect to clip in timeline. @param clipId id of the target clip @param speed: speed in percentage. 100 corresponds to original speed, 50 to half the speed This functions create an undo object and also apply the effect to the corresponding audio if there is any. Returns true on success, false otherwise (and nothing is modified) */ bool requestClipTimeWarp(int clipId, double speed); /* @brief Same function as above, but doesn't check for paired audio and accumulate undo/redo */ bool requestClipTimeWarp(int clipId, double speed, Fun &undo, Fun &redo); void replugClip(int clipId); /** @brief Refresh the tractor profile in case a change was requested. */ void updateProfile(Mlt::Profile *profile); protected: /* @brief Register a new track. This is a call-back meant to be called from TrackModel @param pos indicates the number of the track we are adding. If this is -1, then we add at the end. */ void registerTrack(std::shared_ptr track, int pos = -1, bool doInsert = true, bool reloadView = true); /* @brief Register a new clip. This is a call-back meant to be called from ClipModel */ void registerClip(const std::shared_ptr &clip, bool registerProducer = false); /* @brief Register a new composition. This is a call-back meant to be called from CompositionModel */ void registerComposition(const std::shared_ptr &composition); /* @brief Register a new group. This is a call-back meant to be called from GroupsModel */ void registerGroup(int groupId); /* @brief Deregister and destruct the track with given id. @parame updateView Whether to send updates to the model. Must be false when called from a constructor/destructor */ Fun deregisterTrack_lambda(int id, bool updateView = false); /* @brief Return a lambda that deregisters and destructs the clip with given id. Note that the clip must already be deleted from its track and groups. */ Fun deregisterClip_lambda(int id); /* @brief Return a lambda that deregisters and destructs the composition with given id. */ Fun deregisterComposition_lambda(int compoId); /* @brief Deregister a group with given id */ void deregisterGroup(int id); /* @brief Helper function to get a pointer to the track, given its id */ std::shared_ptr getTrackById(int trackId); const std::shared_ptr getTrackById_const(int trackId) const; /*@brief Helper function to get a pointer to a clip, given its id*/ std::shared_ptr getClipPtr(int clipId) const; /*@brief Helper function to get a pointer to a composition, given its id*/ std::shared_ptr getCompositionPtr(int compoId) const; /* @brief Returns next valid unique id to create an object */ static int getNextId(); /* @brief unplant and the replant all the compositions in the correct order @param currentCompo is the id of a compo that have not yet been planted, if any. Otherwise send -1 */ bool replantCompositions(int currentCompo, bool updateView); /* @brief Unplant the composition with given Id */ bool unplantComposition(int compoId); /* Same function but accumulates undo and redo, and doesn't check for group*/ bool requestClipDeletion(int clipId, Fun &undo, Fun &redo); bool requestCompositionDeletion(int compositionId, Fun &undo, Fun &redo); /** @brief Check tracks duration and update black track accordingly */ void updateDuration(); /** @brief Get a track tag (A1, V1, V2,...) through its id */ const QString getTrackTagById(int trackId) const; /** @brief Attempt to make a clip move without ever updating the view */ bool requestClipMoveAttempt(int clipId, int trackId, int position); public: /* @brief Debugging function that checks consistency with Mlt objects */ bool checkConsistency(); protected: /* @brief Refresh project monitor if cursor was inside range */ void checkRefresh(int start, int end); /* @brief Send signal to require clearing effet/composition view */ void clearAssetView(int itemId); bool m_blockRefresh; signals: /* @brief signal triggered by clearAssetView */ void requestClearAssetView(int); void requestMonitorRefresh(); /* @brief signal triggered by track operations */ void invalidateZone(int in, int out); /* @brief signal triggered when a track duration changed (insertion/deletion) */ void durationUpdated(); /* @brief an item was deleted, make sure it is removed from selection */ void removeFromSelection(int id); protected: std::unique_ptr m_tractor; std::list> m_allTracks; std::unordered_map>::iterator> m_iteratorTable; // this logs the iterator associated which each track id. This allows easy access of a track based on its id. std::unordered_map> m_allClips; // the keys are the clip id, and the values are the corresponding pointers std::unordered_map> m_allCompositions; // the keys are the composition id, and the values are the corresponding pointers static int next_id; // next valid id to assign std::unique_ptr m_groups; std::shared_ptr m_snaps; std::unordered_set m_allGroups; // ids of all the groups std::weak_ptr m_undoStack; Mlt::Profile *m_profile; // The black track producer. Its length / out should always be adjusted to the projects's length std::unique_ptr m_blackClip; mutable QReadWriteLock m_lock; // This is a lock that ensures safety in case of concurrent access bool m_timelineEffectsEnabled; bool m_id; // id of the timeline itself // id of the currently selected group in timeline, should be destroyed on each new selection int m_temporarySelectionGroup; // The index of the temporary overlay track in tractor, or -1 if not connected int m_overlayTrackCount; // The preferred audio target for clip insertion or -1 if not defined int m_audioTarget; // The preferred video target for clip insertion or -1 if not defined int m_videoTarget; // Timeline editing mode TimelineMode::EditMode m_editMode; // what follows are some virtual function that corresponds to the QML. They are implemented in TimelineItemModel protected: virtual void _beginRemoveRows(const QModelIndex &, int, int) = 0; virtual void _beginInsertRows(const QModelIndex &, int, int) = 0; virtual void _endRemoveRows() = 0; virtual void _endInsertRows() = 0; virtual void notifyChange(const QModelIndex &topleft, const QModelIndex &bottomright, bool start, bool duration, bool updateThumb) = 0; virtual void notifyChange(const QModelIndex &topleft, const QModelIndex &bottomright, const QVector &roles) = 0; virtual void notifyChange(const QModelIndex &topleft, const QModelIndex &bottomright, int role) = 0; virtual QModelIndex makeClipIndexFromID(int) const = 0; virtual QModelIndex makeCompositionIndexFromID(int) const = 0; virtual QModelIndex makeTrackIndexFromID(int) const = 0; virtual void _resetView() = 0; }; #endif diff --git a/src/timeline2/model/trackmodel.cpp b/src/timeline2/model/trackmodel.cpp index daba59d54..5971f515d 100644 --- a/src/timeline2/model/trackmodel.cpp +++ b/src/timeline2/model/trackmodel.cpp @@ -1,1141 +1,1140 @@ /*************************************************************************** * 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_track = std::make_shared(*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); + for (auto &m_playlist : m_playlists) { + m_playlist.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_track = std::make_shared(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)) { + for (auto &m_playlist : m_playlists) { + for (int i = 0; i < m_playlist.count(); i++) { + if (!m_playlist.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); + for (auto &m_playlist : m_playlists) { + int ix = m_playlist.get_clip_index_at(frame); + if (m_playlist.is_blank(ix)) { + int blank_length = m_playlist.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 = [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()); + for (auto &m_playlist : m_playlists) { + m_playlist.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}); + clips.emplace_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) { + for (auto &m_playlist : m_playlists) { + if (m_playlist.count() == 0) { break; } - if (!m_playlists[j].is_blank_at(position)) { + if (!m_playlist.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); + int clip_index = m_playlist.get_clip_index_at(position); + int start = m_playlist.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 = [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 [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(std::move(service), trackType()); return true; } 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/view/dialogs/clipdurationdialog.h b/src/timeline2/view/dialogs/clipdurationdialog.h index cf804cd6f..454ac8f4b 100644 --- a/src/timeline2/view/dialogs/clipdurationdialog.h +++ b/src/timeline2/view/dialogs/clipdurationdialog.h @@ -1,61 +1,61 @@ /*************************************************************************** * 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 CLIPDURATIONDIALOG_H #define CLIPDURATIONDIALOG_H #include "timecodedisplay.h" #include "ui_clipdurationdialog_ui.h" /** * @class ClipDurationDialog * @brief A dialog for modifying an item's (clip or transition) duration. * @author Jean-Baptiste Mardelle */ class ClipDurationDialog : public QDialog, public Ui::ClipDurationDialog_UI { Q_OBJECT public: explicit ClipDurationDialog(int clipId, const Timecode &tc, int pos, int minpos, int in, int out, int length, int maxpos, QWidget *parent = nullptr); - ~ClipDurationDialog(); + ~ClipDurationDialog() override; GenTime startPos() const; GenTime cropStart() const; GenTime duration() const; private slots: void slotCheckDuration(); void slotCheckStart(); void slotCheckCrop(); void slotCheckEnd(); private: int m_clipId; TimecodeDisplay *m_pos; TimecodeDisplay *m_dur; TimecodeDisplay *m_cropStart; TimecodeDisplay *m_cropEnd; GenTime m_min; GenTime m_max; GenTime m_crop; GenTime m_length; }; #endif diff --git a/src/timeline2/view/previewmanager.h b/src/timeline2/view/previewmanager.h index f2de2d20d..c8302975e 100644 --- a/src/timeline2/view/previewmanager.h +++ b/src/timeline2/view/previewmanager.h @@ -1,159 +1,159 @@ /*************************************************************************** * 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 * ***************************************************************************/ #ifndef PREVIEWMANAGER_H #define PREVIEWMANAGER_H #include "definitions.h" #include #include #include #include #include class TimelineController; namespace Mlt { class Tractor; class Playlist; class Producer; class Profile; } // namespace Mlt /** * @namespace PreviewManager * @brief Handles timeline preview. * This manager creates an additional video track on top of the current timeline and renders * chunks (small video files of 25 frames) that are added on this track when rendered. * This allow us to get a preview with a smooth playback of our project. * Only the preview zone is rendered. Once defined, a preview zone shows as a red line below * the timeline ruler. As chunks are rendered, the zone turns to green. */ class PreviewManager : public QObject { Q_OBJECT public: friend class TimelineController; explicit PreviewManager(TimelineController *controller, Mlt::Tractor *tractor); - virtual ~PreviewManager(); + ~PreviewManager() override; /** @brief: initialize base variables, return false if error. */ bool initialize(); /** @brief: a timeline operation caused changes to frames between startFrame and endFrame. */ void invalidatePreview(int startFrame, int endFrame); /** @brief: after a small delay (some operations trigger several invalidatePreview calls), take care of these invalidated chunks. */ void invalidatePreviews(const QVariantList chunks); /** @brief: user adds current timeline zone to the preview zone. */ void addPreviewRange(const QPoint zone, bool add); /** @brief: Remove all existing previews. */ void clearPreviewRange(); /** @brief: stops current rendering process. */ void abortRendering(); /** @brief: rendering parameters have changed, reload them. */ bool loadParams(); /** @brief: Create the preview track if not existing. */ bool buildPreviewTrack(); /** @brief: Delete the preview track. */ void deletePreviewTrack(); /** @brief: Whenever we save or render our project, we remove the preview track so it is not saved. */ void reconnectTrack(); /** @brief: After project save or render, re-add our preview track. */ void disconnectTrack(); /** @brief: Returns directory currently used to store the preview files. */ const QDir getCacheDir() const; /** @brief: Load existing ruler chunks. */ void loadChunks(QVariantList previewChunks, QVariantList dirtyChunks, const QDateTime &documentDate); int setOverlayTrack(Mlt::Playlist *overlay); /** @brief Remove the effect compare overlay track */ void removeOverlayTrack(); /** @brief The current preview chunk being processed, -1 if none */ int workingPreview; /** @brief Returns the list of existing chunks */ QPair previewChunks() const; bool hasOverlayTrack() const; bool hasPreviewTrack() const; int addedTracks() const; private: TimelineController *m_controller; Mlt::Tractor *m_tractor; Mlt::Playlist *m_previewTrack; Mlt::Playlist *m_overlayTrack; int m_previewTrackIndex; /** @brief: The kdenlive renderer app. */ QString m_renderer; /** @brief: The kdenlive timeline preview process. */ QProcess m_previewProcess; /** @brief: The directory used to store the preview files. */ QDir m_cacheDir; /** @brief: The directory used to store undo history of preview files (child of m_cacheDir). */ QDir m_undoDir; QMutex m_previewMutex; QStringList m_consumerParams; QString m_extension; /** @brief: Timer used to autostart preview rendering. */ QTimer m_previewTimer; /** @brief: Since some timeline operations generate several invalidate calls, use a timer to get them all. */ QTimer m_previewGatherTimer; bool m_initialized; QList m_waitingThumbs; QFuture m_previewThread; /** @brief: The count of chunks to process - to calculate job progress */ int m_chunksToRender; /** @brief: The count of already processed chunks - to calculate job progress */ int m_processedChunks; /** @brief: The render process output, useful in case of failure */ QString m_errorLog; /** @brief: After an undo/redo, if we have preview history, use it. */ void reloadChunks(const QVariantList chunks); /** @brief: A chunk failed to render, abort. */ void corruptedChunk(int workingPreview, const QString &fileName); private slots: /** @brief: To avoid filling the hard drive, remove preview undo history after 5 steps. */ void doCleanupOldPreviews(); /** @brief: Start the real rendering process. */ void doPreviewRender(const QString &scene); // std::shared_ptr sourceProd); /** @brief: If user does an undo, then makes a new timeline operation, delete undo history of more recent stack . */ void slotRemoveInvalidUndo(int ix); /** @brief: When the timer collecting invalid zones is done, process. */ void slotProcessDirtyChunks(); /** @brief: Process preview rendering output. */ void receivedStderr(); public slots: /** @brief: Prepare and start rendering. */ void startPreviewRender(); /** @brief: A chunk has been created, notify ruler. */ void gotPreviewRender(int frame, const QString &file, int progress); protected: QVariantList m_renderedChunks; QVariantList m_dirtyChunks; signals: void abortPreview(); void cleanupOldPreviews(); void previewRender(int frame, const QString &file, int progress); }; #endif diff --git a/src/timeline2/view/qml/timelineitems.cpp b/src/timeline2/view/qml/timelineitems.cpp index 66605e8e5..b62a3535d 100644 --- a/src/timeline2/view/qml/timelineitems.cpp +++ b/src/timeline2/view/qml/timelineitems.cpp @@ -1,200 +1,200 @@ /* Based on Shotcut, Copyright (c) 2015-2016 Meltytech, LLC Copyright (C) 2019 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 "kdenlivesettings.h" #include #include #include #include -#include +#include const QStringList chanelNames{"L", "R", "C", "LFE", "BL", "BR"}; class TimelineTriangle : public QQuickPaintedItem { Q_OBJECT Q_PROPERTY(QColor fillColor MEMBER m_color) public: TimelineTriangle() { setAntialiasing(true); } void paint(QPainter *painter) override { QPainterPath path; path.moveTo(0, 0); path.lineTo(width(), 0); path.lineTo(0, height()); painter->fillPath(path, m_color); painter->setPen(Qt::white); painter->drawLine(width(), 0, 0, height()); } private: QColor m_color; }; class TimelinePlayhead : public QQuickPaintedItem { Q_OBJECT Q_PROPERTY(QColor fillColor MEMBER m_color NOTIFY colorChanged) public: TimelinePlayhead() { connect(this, SIGNAL(colorChanged(QColor)), this, SLOT(update())); } void paint(QPainter *painter) override { QPainterPath path; path.moveTo(width(), 0); path.lineTo(width() / 2.0, height()); path.lineTo(0, 0); painter->fillPath(path, m_color); } signals: void colorChanged(const QColor &); private: QColor m_color; }; class TimelineWaveform : public QQuickPaintedItem { Q_OBJECT Q_PROPERTY(QList levels MEMBER m_audioLevels NOTIFY propertyChanged) Q_PROPERTY(QColor fillColor MEMBER m_color NOTIFY propertyChanged) Q_PROPERTY(int inPoint MEMBER m_inPoint NOTIFY inPointChanged) Q_PROPERTY(int channels MEMBER m_channels NOTIFY audioChannelsChanged) Q_PROPERTY(int outPoint MEMBER m_outPoint NOTIFY outPointChanged) Q_PROPERTY(bool format MEMBER m_format NOTIFY propertyChanged) Q_PROPERTY(bool showItem MEMBER m_showItem) Q_PROPERTY(bool isFirstChunk MEMBER m_firstChunk) public: TimelineWaveform() { setAntialiasing(false); // setClip(true); setEnabled(false); setRenderTarget(QQuickPaintedItem::FramebufferObject); setMipmap(true); setTextureSize(QSize(width(), height())); connect(this, SIGNAL(propertyChanged()), this, SLOT(update())); } void paint(QPainter *painter) override { if (!m_showItem || m_audioLevels.isEmpty()) return; const qreal indicesPrPixel = qreal(m_outPoint - m_inPoint) / width(); QPen pen = painter->pen(); pen.setColor(m_color); pen.setWidthF(0); painter->setBrush(m_color); if (!KdenliveSettings::displayallchannels()) { // Draw merged channels QPainterPath path; path.moveTo(-1, height()); double i = 0; double increment = qMax(1., 1 / indicesPrPixel); int lastIdx = -1; for (; i <= width(); i += increment) { int idx = m_inPoint + int(i * indicesPrPixel); if (lastIdx == idx) { continue; } lastIdx = idx; if (idx + m_channels >= m_audioLevels.length()) break; double level = m_audioLevels.at(idx).toDouble() / 256; for (int j = 1; j < m_channels; j++) { level = qMax(level, m_audioLevels.at(idx + j).toDouble() / 256); } path.lineTo(i, height() - level * height()); } path.lineTo(i, height()); painter->drawPath(path); } else { double channelHeight = height() / (2 * m_channels); QFont font = painter->font(); font.setPixelSize(channelHeight - 1); painter->setFont(font); // Draw separate channels double i = 0; double increment = qMax(1., 1 / indicesPrPixel); QRectF bgRect(0, 0, width(), 2 * channelHeight); QVector channelPaths(m_channels); for (int channel = 0; channel < m_channels; channel++) { double y = height() - (2 * channel * channelHeight) - channelHeight; channelPaths[channel].moveTo(-1, y); painter->setOpacity(0.2); if (channel % 2 == 0) { // Add dark background on odd channels bgRect.moveTo(0, y - channelHeight); painter->fillRect(bgRect, Qt::black); } // Draw channel median line painter->setPen(pen); painter->drawLine(QLineF(0., y, width(), y)); painter->setOpacity(1); int lastIdx = -1; for (i = 0; i <= width(); i += increment) { int idx = m_inPoint + ceil(i * indicesPrPixel); if (lastIdx == idx) { continue; } lastIdx = idx; if (idx + channel >= m_audioLevels.length()) break; qreal level = m_audioLevels.at(idx + channel).toReal() * channelHeight / 256; channelPaths[channel].lineTo(i, y - level); } if (m_firstChunk && m_channels > 1 && m_channels < 7) { painter->drawText(2, y + channelHeight, chanelNames[channel]); } channelPaths[channel].lineTo(i, y); painter->setPen(Qt::NoPen); painter->drawPath(channelPaths.value(channel)); QTransform tr(1, 0, 0, -1, 0, 2 * y); painter->drawPath(tr.map(channelPaths.value(channel))); } } } signals: void propertyChanged(); void inPointChanged(); void outPointChanged(); void audioChannelsChanged(); private: QList m_audioLevels; int m_inPoint; int m_outPoint; QColor m_color; bool m_format; bool m_showItem; int m_channels; bool m_firstChunk; }; void registerTimelineItems() { qmlRegisterType("Kdenlive.Controls", 1, 0, "TimelineTriangle"); qmlRegisterType("Kdenlive.Controls", 1, 0, "TimelinePlayhead"); qmlRegisterType("Kdenlive.Controls", 1, 0, "TimelineWaveform"); } #include "timelineitems.moc" diff --git a/src/timeline2/view/qmltypes/thumbnailprovider.cpp b/src/timeline2/view/qmltypes/thumbnailprovider.cpp index 8b5ca5651..3cbebbe2f 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() {} +ThumbnailProvider::~ThumbnailProvider() = default; 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(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 24e9feb24..360bc129c 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(); + ~ThumbnailProvider() override; 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(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 841d094e4..49e8a2ca9 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 - +#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()); + props = std::make_unique(); 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) { 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(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()); + auto *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(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, 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 (const auto &clp : m_model->m_allClips) { ids << clp.first; } for (const auto &clp : m_model->m_allCompositions) { ids << clp.first; } setSelection(ids); } void TimelineController::selectCurrentTrack() { QList ids; for (const auto &clp : m_model->getTrackById_const(m_activeTrack)->m_allClips) { ids << clp.first; } 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 (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(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, 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 f87ea8c02..b454d312a 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(); + ~TimelineController() override; /** @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(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); /* @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, 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(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, 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(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 86fdba328..6be1d4f80 100644 --- a/src/timeline2/view/timelinewidget.cpp +++ b/src/timeline2/view/timelinewidget.cpp @@ -1,199 +1,198 @@ /*************************************************************************** * 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)); + m_transitionProxyModel = std::make_unique(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)); + m_effectsProxyModel = std::make_unique(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(const std::shared_ptr &model) { m_thumbnailer->resetProject(); - m_sortModel.reset(new QSortFilterProxyModel(this)); + m_sortModel = std::make_unique(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 25a1116d3..9cce09fdf 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(); + ~TimelineWidget() override; /* @brief Sets the model shown by this widget */ 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/src/titler/KoSliderCombo.h b/src/titler/KoSliderCombo.h index e586fd257..25a28978e 100644 --- a/src/titler/KoSliderCombo.h +++ b/src/titler/KoSliderCombo.h @@ -1,138 +1,138 @@ /* This file is part of the KDE project Copyright (c) 2007 Casper Boemann This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KOSLIDERCOMBO_H_ #define KOSLIDERCOMBO_H_ #include /** * @short A widget for qreal values with a popup slider * * KoSliderCombo combines a numerical input and a dropdown slider in a way that takes up as * little screen space as possible. * * It allows the user to either enter a floating point value or quickly set the value using a slider * * One signal is emitted when the value changes. The signal is even emitted when the slider * is moving. The second argument of the signal however tells you if the value is final or not. A * final value is produced by entering a value numerically or by releasing the slider. * * The input of the numerical line edit is constrained to numbers and decimal signs. */ class KoSliderCombo : public QComboBox { Q_OBJECT public: /** * Constructor for the widget, where value is set to 0 * * @param parent parent QWidget */ explicit KoSliderCombo(QWidget *parent = nullptr); /** * Destructor */ - virtual ~KoSliderCombo(); + ~KoSliderCombo() override; /** * The precision of values given as the number of digits after the period. * default is 2 */ qreal decimals() const; /** * The minimum value that can be entered. * default is 0 */ qreal minimum() const; /** * The maximum value that can be entered. * default is 100 */ qreal maximum() const; /** * Sets the precision of the entered values. * @param number the number of digits after the period */ void setDecimals(int number); /** * Sets the minimum value that can be entered. * @param min the minimum value */ void setMinimum(qreal min); /** * Sets the maximum value that can be entered. * @param max the maximum value */ void setMaximum(qreal max); /** * The value shown. */ qreal value() const; QSize minimumSizeHint() const override; ///< reimplemented from KComboBox QSize sizeHint() const override; ///< reimplemented from KComboBox public slots: /** * Sets the value. * The value actually set is forced to be within the legal range: minimum <= value <= maximum * @param value the new value */ void setValue(qreal value); signals: /** * Emitted every time the value changes (by calling setValue() or * by user interaction). * @param value the new value * @param final if the value is final ie not produced during sliding (on slider release it's final) */ void valueChanged(qreal value, bool final); protected: void paintEvent(QPaintEvent *) override; ///< reimplemented from KComboBox void hideEvent(QHideEvent *) override; ///< reimplemented from KComboBox void changeEvent(QEvent *e) override; ///< reimplemented from KComboBox void mousePressEvent(QMouseEvent *e) override; ///< reimplemented from KComboBox void keyPressEvent(QKeyEvent *e) override; ///< reimplemented from KComboBox void wheelEvent(QWheelEvent *e) override; ///< reimplemented from KComboBox private: Q_PRIVATE_SLOT(d, void sliderValueChanged(int value)) Q_PRIVATE_SLOT(d, void sliderReleased()) Q_PRIVATE_SLOT(d, void lineEditFinished()) class KoSliderComboPrivate; KoSliderComboPrivate *const m_d; }; #endif diff --git a/src/titler/graphicsscenerectmove.cpp b/src/titler/graphicsscenerectmove.cpp index 0f1d8b2c6..605ac3fca 100644 --- a/src/titler/graphicsscenerectmove.cpp +++ b/src/titler/graphicsscenerectmove.cpp @@ -1,1079 +1,1070 @@ /*************************************************************************** * copyright (C) 2008 by Marco Gittler (g.marco@freenet.de) * * 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 "graphicsscenerectmove.h" #include "titler/gradientwidget.h" #include "titler/titledocument.h" #include "kdenlive_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include MyQGraphicsEffect::MyQGraphicsEffect(QObject *parent) : QGraphicsEffect(parent) - , m_xOffset(0) - , m_yOffset(0) - , m_blur(0) + { } void MyQGraphicsEffect::setShadow(const QImage &image) { m_shadow = image; } void MyQGraphicsEffect::setOffset(int xOffset, int yOffset, int blur) { m_xOffset = xOffset; m_yOffset = yOffset; m_blur = blur; updateBoundingRect(); } void MyQGraphicsEffect::draw(QPainter *painter) { painter->fillRect(boundingRect(), Qt::transparent); painter->drawImage(-2 * m_blur + m_xOffset, -2 * m_blur + m_yOffset, m_shadow); drawSource(painter); } MyTextItem::MyTextItem(const QString &txt, QGraphicsItem *parent) : QGraphicsTextItem(txt, parent) , m_alignment(Qt::AlignLeft) { setCacheMode(QGraphicsItem::ItemCoordinateCache); setFlag(QGraphicsItem::ItemSendsGeometryChanges, true); document()->setDocumentMargin(0); m_shadowEffect = new MyQGraphicsEffect(this); m_shadowEffect->setEnabled(false); setGraphicsEffect(m_shadowEffect); updateGeometry(); connect(document(), SIGNAL(contentsChange(int, int, int)), this, SLOT(updateGeometry(int, int, int))); } Qt::Alignment MyTextItem::alignment() const { return m_alignment; } void MyTextItem::updateShadow(bool enabled, int blur, int xoffset, int yoffset, QColor color) { m_shadowOffset = QPoint(xoffset, yoffset); m_shadowBlur = blur; m_shadowColor = std::move(color); m_shadowEffect->setEnabled(enabled); m_shadowEffect->setOffset(xoffset, yoffset, blur); if (enabled) { updateShadow(); } update(); } void MyTextItem::setTextColor(const QColor &col) { setDefaultTextColor(col); refreshFormat(); } QStringList MyTextItem::shadowInfo() const { QStringList info; info << QString::number(static_cast(m_shadowEffect->isEnabled())) << m_shadowColor.name(QColor::HexArgb) << QString::number(m_shadowBlur) << QString::number(m_shadowOffset.x()) << QString::number(m_shadowOffset.y()); return info; } void MyTextItem::loadShadow(const QStringList &info) { if (info.count() < 5) { return; } updateShadow((static_cast(info.at(0).toInt())), info.at(2).toInt(), info.at(3).toInt(), info.at(4).toInt(), QColor(info.at(1))); } void MyTextItem::setAlignment(Qt::Alignment alignment) { m_alignment = alignment; QTextBlockFormat format; format.setAlignment(alignment); QTextCursor cursor = textCursor(); // save cursor position int position = textCursor().position(); cursor.select(QTextCursor::Document); cursor.mergeBlockFormat(format); cursor.clearSelection(); cursor.setPosition(position); // restore cursor position setTextCursor(cursor); } void MyTextItem::refreshFormat() { QString gradientData = data(TitleDocument::Gradient).toString(); QTextCursor cursor = textCursor(); QTextCharFormat cformat; cursor.select(QTextCursor::Document); int position = textCursor().position(); // Formatting can be lost on paste, since our QTextCursor gets overwritten, so re-apply all formatting here QColor fgColor = defaultTextColor(); cformat.setForeground(fgColor); cformat.setFont(font()); if (!gradientData.isEmpty()) { QRectF rect = boundingRect(); QLinearGradient gr = GradientWidget::gradientFromString(gradientData, rect.width(), rect.height()); cformat.setForeground(QBrush(gr)); } // Apply cursor.mergeCharFormat(cformat); // restore cursor position cursor.clearSelection(); cursor.setPosition(position); setTextCursor(cursor); } void MyTextItem::updateGeometry(int, int, int) { updateGeometry(); // update gradient if necessary refreshFormat(); QString text = toPlainText(); m_path = QPainterPath(); m_path.setFillRule(Qt::WindingFill); if (text.isEmpty()) { // } else { QFontMetrics metrics(font()); double lineSpacing = data(TitleDocument::LineSpacing).toInt() + metrics.lineSpacing(); // Calculate line width const QStringList lines = text.split(QLatin1Char('\n')); double linePos = metrics.ascent(); QRectF bounding = boundingRect(); /*if (lines.count() > 0) { lineSpacing = bounding.height() / lines.count(); if (lineSpacing != data(TitleDocument::LineSpacing).toInt() + metrics.lineSpacing()) { linePos = 2 * lineSpacing - metrics.descent() - metrics.height(); } }*/ for (const QString &line : lines) { QPainterPath linePath; linePath.addText(0, linePos, font(), line); linePos += lineSpacing; if (m_alignment == Qt::AlignHCenter) { double offset = (bounding.width() - metrics.width(line)) / 2; linePath.translate(offset, 0); } else if (m_alignment == Qt::AlignRight) { double offset = bounding.width() - metrics.width(line); linePath.translate(offset, 0); } m_path.addPath(linePath); } } if (m_shadowEffect->isEnabled()) { updateShadow(); } update(); } void MyTextItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *w) { if ((textInteractionFlags() & static_cast((Qt::TextEditable) != 0)) != 0) { QGraphicsTextItem::paint(painter, option, w); } else { painter->setRenderHint(QPainter::Antialiasing); int outline = data(TitleDocument::OutlineWidth).toInt(); QString gradientData = data(TitleDocument::Gradient).toString(); QTextCursor cursor(document()); cursor.select(QTextCursor::Document); QBrush paintBrush; if (gradientData.isEmpty()) { paintBrush = QBrush(cursor.charFormat().foreground().color()); } else { QRectF rect = boundingRect(); paintBrush = QBrush(GradientWidget::gradientFromString(gradientData, rect.width(), rect.height())); } painter->fillPath(m_path, paintBrush); if (outline > 0) { QVariant variant = data(TitleDocument::OutlineColor); QColor outlineColor = variant.value(); QPen pen(outlineColor); pen.setWidthF(outline); painter->strokePath(m_path, pen); } if (isSelected()) { QPen pen(Qt::red); pen.setStyle(Qt::DashLine); painter->setPen(pen); painter->drawRect(boundingRect()); } } } void MyTextItem::updateShadow() { QString text = toPlainText(); if (text.isEmpty()) { m_shadowEffect->setShadow(QImage()); return; } QRectF bounding = boundingRect(); QPainterPath path = m_path; // Calculate position of text in parent item path.translate(QPointF(2 * m_shadowBlur, 2 * m_shadowBlur)); QRectF fullSize = bounding.united(path.boundingRect()); QImage shadow(fullSize.width() + qAbs(m_shadowOffset.x()) + 4 * m_shadowBlur, fullSize.height() + qAbs(m_shadowOffset.y()) + 4 * m_shadowBlur, QImage::Format_ARGB32_Premultiplied); shadow.fill(Qt::transparent); QPainter painter(&shadow); painter.fillPath(path, QBrush(m_shadowColor)); painter.end(); if (m_shadowBlur > 0) { blurShadow(shadow, m_shadowBlur); } m_shadowEffect->setShadow(shadow); } void MyTextItem::blurShadow(QImage &result, int radius) { int tab[] = {14, 10, 8, 6, 5, 5, 4, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2}; int alpha = (radius < 1) ? 16 : (radius > 17) ? 1 : tab[radius - 1]; int r1 = 0; int r2 = result.height() - 1; int c1 = 0; int c2 = result.width() - 1; int bpl = result.bytesPerLine(); int rgba[4]; unsigned char *p; int i1 = 0; int i2 = 3; for (int col = c1; col <= c2; col++) { p = result.scanLine(r1) + col * 4; for (int i = i1; i <= i2; i++) { rgba[i] = p[i] << 4; } p += bpl; for (int j = r1; j < r2; j++, p += bpl) for (int i = i1; i <= i2; i++) { p[i] = (uchar)((rgba[i] += ((p[i] << 4) - rgba[i]) * alpha / 16) >> 4); } } for (int row = r1; row <= r2; row++) { p = result.scanLine(row) + c1 * 4; for (int i = i1; i <= i2; i++) { rgba[i] = p[i] << 4; } p += 4; for (int j = c1; j < c2; j++, p += 4) for (int i = i1; i <= i2; i++) { p[i] = (uchar)((rgba[i] += ((p[i] << 4) - rgba[i]) * alpha / 16) >> 4); } } for (int col = c1; col <= c2; col++) { p = result.scanLine(r2) + col * 4; for (int i = i1; i <= i2; i++) { rgba[i] = p[i] << 4; } p -= bpl; for (int j = r1; j < r2; j++, p -= bpl) for (int i = i1; i <= i2; i++) { p[i] = (uchar)((rgba[i] += ((p[i] << 4) - rgba[i]) * alpha / 16) >> 4); } } for (int row = r1; row <= r2; row++) { p = result.scanLine(row) + c2 * 4; for (int i = i1; i <= i2; i++) { rgba[i] = p[i] << 4; } p -= 4; for (int j = c1; j < c2; j++, p -= 4) for (int i = i1; i <= i2; i++) { p[i] = (uchar)((rgba[i] += ((p[i] << 4) - rgba[i]) * alpha / 16) >> 4); } } } void MyTextItem::updateGeometry() { QPointF topRightPrev = boundingRect().topRight(); setTextWidth(-1); setTextWidth(boundingRect().width()); setAlignment(m_alignment); QPointF topRight = boundingRect().topRight(); if ((m_alignment & static_cast((Qt::AlignRight) != 0)) != 0) { setPos(pos() + (topRightPrev - topRight)); } } QRectF MyTextItem::baseBoundingRect() const { QRectF base = QGraphicsTextItem::boundingRect(); QTextCursor cur(document()); cur.select(QTextCursor::Document); QTextBlockFormat format = cur.blockFormat(); int lineHeight = format.lineHeight(); int lineHeight2 = QFontMetrics(font()).lineSpacing(); int lines = document()->lineCount(); if (lines > 1) { base.setHeight(lines * lineHeight2 + lineHeight * (lines - 1)); } return base; } QRectF MyTextItem::boundingRect() const { QRectF base = baseBoundingRect(); if (m_shadowEffect->isEnabled() && m_shadowOffset.x() > 0) { base.setRight(base.right() + m_shadowOffset.x()); } if (m_shadowEffect->isEnabled() && m_shadowOffset.y() > 0) { base.setBottom(base.bottom() + m_shadowOffset.y()); } return base; } QVariant MyTextItem::itemChange(GraphicsItemChange change, const QVariant &value) { if (change == ItemPositionChange && (scene() != nullptr)) { QPoint newPos = value.toPoint(); if (QApplication::mouseButtons() == Qt::LeftButton && (qobject_cast(scene()) != nullptr)) { - GraphicsSceneRectMove *customScene = qobject_cast(scene()); + auto *customScene = qobject_cast(scene()); int gridSize = customScene->gridSize(); int xV = (newPos.x() / gridSize) * gridSize; int yV = (newPos.y() / gridSize) * gridSize; newPos = QPoint(xV, yV); } return newPos; } if (change == QGraphicsItem::ItemSelectedHasChanged) { if (!value.toBool()) { // Make sure to deselect text when item loses focus QTextCursor cur(document()); cur.clearSelection(); setTextCursor(cur); } } return QGraphicsItem::itemChange(change, value); } void MyTextItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *evt) { if (textInteractionFlags() == Qt::TextEditorInteraction) { // if editor mode is already on: pass double click events on to the editor: QGraphicsTextItem::mouseDoubleClickEvent(evt); return; } // if editor mode is off: // 1. turn editor mode on and set selected and focused: // SetTextInteraction(true); setTextInteractionFlags(Qt::TextEditorInteraction); setFocus(Qt::MouseFocusReason); // 2. send a single click to this QGraphicsTextItem (this will set the cursor to the mouse position): // create a new mouse event with the same parameters as evt auto *click = new QGraphicsSceneMouseEvent(QEvent::GraphicsSceneMousePress); click->setButton(evt->button()); click->setPos(evt->pos()); QGraphicsTextItem::mousePressEvent(click); delete click; // don't forget to delete the event } MyRectItem::MyRectItem(QGraphicsItem *parent) : QGraphicsRectItem(parent) { setCacheMode(QGraphicsItem::ItemCoordinateCache); setFlag(QGraphicsItem::ItemSendsGeometryChanges, true); } void MyRectItem::setRect(const QRectF &rectangle) { QGraphicsRectItem::setRect(rectangle); if (m_rect != rectangle && !data(TitleDocument::Gradient).isNull()) { m_rect = rectangle; QLinearGradient gr = GradientWidget::gradientFromString(data(TitleDocument::Gradient).toString(), m_rect.width(), m_rect.height()); setBrush(QBrush(gr)); } } QVariant MyRectItem::itemChange(GraphicsItemChange change, const QVariant &value) { if (change == ItemPositionChange && (scene() != nullptr)) { QPoint newPos = value.toPoint(); if (QApplication::mouseButtons() == Qt::LeftButton && (qobject_cast(scene()) != nullptr)) { - GraphicsSceneRectMove *customScene = qobject_cast(scene()); + auto *customScene = qobject_cast(scene()); int gridSize = customScene->gridSize(); int xV = (newPos.x() / gridSize) * gridSize; int yV = (newPos.y() / gridSize) * gridSize; newPos = QPoint(xV, yV); } return newPos; } return QGraphicsItem::itemChange(change, value); } MyPixmapItem::MyPixmapItem(const QPixmap &pixmap, QGraphicsItem *parent) : QGraphicsPixmapItem(pixmap, parent) { setCacheMode(QGraphicsItem::ItemCoordinateCache); setFlag(QGraphicsItem::ItemSendsGeometryChanges, true); } QVariant MyPixmapItem::itemChange(GraphicsItemChange change, const QVariant &value) { if (change == ItemPositionChange && (scene() != nullptr)) { QPoint newPos = value.toPoint(); if (QApplication::mouseButtons() == Qt::LeftButton && (qobject_cast(scene()) != nullptr)) { - GraphicsSceneRectMove *customScene = qobject_cast(scene()); + auto *customScene = qobject_cast(scene()); int gridSize = customScene->gridSize(); int xV = (newPos.x() / gridSize) * gridSize; int yV = (newPos.y() / gridSize) * gridSize; newPos = QPoint(xV, yV); } return newPos; } return QGraphicsItem::itemChange(change, value); } MySvgItem::MySvgItem(const QString &fileName, QGraphicsItem *parent) : QGraphicsSvgItem(fileName, parent) { setCacheMode(QGraphicsItem::ItemCoordinateCache); setFlag(QGraphicsItem::ItemSendsGeometryChanges, true); } QVariant MySvgItem::itemChange(GraphicsItemChange change, const QVariant &value) { if (change == ItemPositionChange && (scene() != nullptr)) { QPoint newPos = value.toPoint(); if (QApplication::mouseButtons() == Qt::LeftButton && (qobject_cast(scene()) != nullptr)) { - GraphicsSceneRectMove *customScene = qobject_cast(scene()); + auto *customScene = qobject_cast(scene()); int gridSize = customScene->gridSize(); int xV = (newPos.x() / gridSize) * gridSize; int yV = (newPos.y() / gridSize) * gridSize; newPos = QPoint(xV, yV); } return newPos; } return QGraphicsItem::itemChange(change, value); } GraphicsSceneRectMove::GraphicsSceneRectMove(QObject *parent) : QGraphicsScene(parent) - , m_selectedItem(nullptr) - , m_resizeMode(NoResize) - , m_possibleAction(NoResize) - , m_tool(TITLE_RECTANGLE) - , m_gridSize(20) - , m_createdText(false) - , m_moveStarted(false) - , m_pan(false) + { // grabMouse(); m_zoom = 1.0; setBackgroundBrush(QBrush(Qt::transparent)); m_fontSize = 0; } void GraphicsSceneRectMove::setSelectedItem(QGraphicsItem *item) { clearSelection(); m_selectedItem = item; item->setSelected(true); update(); } TITLETOOL GraphicsSceneRectMove::tool() const { return m_tool; } void GraphicsSceneRectMove::setTool(TITLETOOL tool) { m_tool = tool; switch (m_tool) { case TITLE_RECTANGLE: setCursor(Qt::CrossCursor); break; case TITLE_TEXT: setCursor(Qt::IBeamCursor); break; default: setCursor(Qt::ArrowCursor); } } void GraphicsSceneRectMove::keyPressEvent(QKeyEvent *keyEvent) { if (m_selectedItem == nullptr || !(m_selectedItem->flags() & QGraphicsItem::ItemIsMovable)) { QGraphicsScene::keyPressEvent(keyEvent); return; } if (m_selectedItem->type() == QGraphicsTextItem::Type) { - MyTextItem *t = static_cast(m_selectedItem); + auto *t = static_cast(m_selectedItem); if ((t->textInteractionFlags() & static_cast((Qt::TextEditorInteraction) != 0)) != 0) { QGraphicsScene::keyPressEvent(keyEvent); return; } } int diff = m_gridSize; if ((keyEvent->modifiers() & Qt::ControlModifier) != 0u) { diff = m_gridSize * 5; } switch (keyEvent->key()) { case Qt::Key_Left: for (QGraphicsItem *qgi : selectedItems()) { qgi->moveBy(-diff, 0); } emit itemMoved(); break; case Qt::Key_Right: for (QGraphicsItem *qgi : selectedItems()) { qgi->moveBy(diff, 0); } emit itemMoved(); break; case Qt::Key_Up: for (QGraphicsItem *qgi : selectedItems()) { qgi->moveBy(0, -diff); } emit itemMoved(); break; case Qt::Key_Down: for (QGraphicsItem *qgi : selectedItems()) { qgi->moveBy(0, diff); } emit itemMoved(); break; case Qt::Key_Delete: case Qt::Key_Backspace: for (QGraphicsItem *qgi : selectedItems()) { if (qgi->data(-1).toInt() == -1) { continue; } removeItem(qgi); delete qgi; } m_selectedItem = nullptr; emit selectionChanged(); break; default: QGraphicsScene::keyPressEvent(keyEvent); } emit actionFinished(); } void GraphicsSceneRectMove::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *e) { QPointF p = e->scenePos(); p += QPoint(-2, -2); m_resizeMode = NoResize; m_selectedItem = nullptr; // http://www.kdenlive.org/mantis/view.php?id=1035 QList i = items(QRectF(p, QSizeF(4, 4)).toRect()); if (i.isEmpty()) { return; } int ix = 1; QGraphicsItem *g = i.constFirst(); while (!(g->flags() & QGraphicsItem::ItemIsSelectable) && ix < i.count()) { g = i.at(ix); ix++; } if ((g != nullptr) && g->type() == QGraphicsTextItem::Type && (((g->flags() & static_cast((QGraphicsItem::ItemIsSelectable) != 0))) != 0)) { m_selectedItem = g; } else { emit doubleClickEvent(); } QGraphicsScene::mouseDoubleClickEvent(e); } void GraphicsSceneRectMove::mouseReleaseEvent(QGraphicsSceneMouseEvent *e) { m_pan = false; if (m_tool == TITLE_RECTANGLE && (m_selectedItem != nullptr)) { setSelectedItem(m_selectedItem); } if (m_createdText) { m_selectedItem->setSelected(true); - MyTextItem *newText = static_cast(m_selectedItem); + auto *newText = static_cast(m_selectedItem); QTextCursor cur(newText->document()); cur.select(QTextCursor::Document); newText->setTextCursor(cur); m_createdText = false; } if ((e->modifiers() & Qt::ShiftModifier) != 0u) { e->accept(); } else { QGraphicsScene::mouseReleaseEvent(e); } QList viewlist = views(); if (!viewlist.isEmpty()) { viewlist.constFirst()->setDragMode(QGraphicsView::RubberBandDrag); } emit actionFinished(); } void GraphicsSceneRectMove::mousePressEvent(QGraphicsSceneMouseEvent *e) { if ((e->buttons() & Qt::MiddleButton) != 0u) { clearTextSelection(); QList viewlist = views(); if (!viewlist.isEmpty()) { viewlist.constFirst()->setDragMode(QGraphicsView::ScrollHandDrag); m_pan = true; e->accept(); QGraphicsScene::mousePressEvent(e); return; } } int xPos = ((int)e->scenePos().x() / m_gridSize) * m_gridSize; int yPos = ((int)e->scenePos().y() / m_gridSize) * m_gridSize; m_moveStarted = false; m_clickPoint = e->scenePos(); m_resizeMode = m_possibleAction; const QList list = items(e->scenePos()); QGraphicsItem *item = nullptr; if (m_tool == TITLE_SELECT) { QList viewlist = views(); if ((e->modifiers() & Qt::ControlModifier) != 0u) { clearTextSelection(); if (!viewlist.isEmpty()) { viewlist.constFirst()->setDragMode(QGraphicsView::ScrollHandDrag); e->ignore(); // QGraphicsScene::mousePressEvent(e); return; } } else { if (!viewlist.isEmpty()) { viewlist.constFirst()->setRubberBandSelectionMode(Qt::IntersectsItemShape); } } bool alreadySelected = false; for (QGraphicsItem *g : list) { // qDebug() << " - - CHECKING ITEM Z:" << g->zValue() << ", TYPE: " << g->type(); // check is there is a selected item in list if (!(g->flags() & QGraphicsItem::ItemIsSelectable)) { continue; } if (g->zValue() > -1000 /* && g->isSelected()*/) { alreadySelected = g->isSelected(); if (!alreadySelected) { g->setSelected(true); } item = g; break; } } if (item == nullptr || (e->modifiers() != Qt::ShiftModifier && !alreadySelected)) { clearTextSelection(); } else if ((e->modifiers() & Qt::ShiftModifier) != 0u) { clearTextSelection(false); } if ((item != nullptr) && ((item->flags() & QGraphicsItem::ItemIsMovable) != 0)) { m_sceneClickPoint = e->scenePos(); m_selectedItem = item; // qCDebug(KDENLIVE_LOG) << "///////// ITEM TYPE: " << item->type(); if (item->type() == QGraphicsTextItem::Type) { - MyTextItem *t = static_cast(item); + auto *t = static_cast(item); if (t->textInteractionFlags() == Qt::TextEditorInteraction) { QGraphicsScene::mousePressEvent(e); return; } t->setTextInteractionFlags(Qt::NoTextInteraction); t->setFlag(QGraphicsItem::ItemSendsGeometryChanges, true); setCursor(Qt::ClosedHandCursor); } else if (item->type() == QGraphicsRectItem::Type || item->type() == QGraphicsSvgItem::Type || item->type() == QGraphicsPixmapItem::Type) { QRectF r1; if (m_selectedItem->type() == QGraphicsRectItem::Type) { r1 = ((QGraphicsRectItem *)m_selectedItem)->rect().normalized(); } else { r1 = m_selectedItem->boundingRect().normalized(); } r1.translate(m_selectedItem->scenePos()); switch (m_resizeMode) { case BottomRight: case Right: case Down: m_clickPoint = r1.topLeft(); e->accept(); break; case TopLeft: case Left: case Up: m_clickPoint = r1.bottomRight(); e->accept(); break; case TopRight: m_clickPoint = r1.bottomLeft(); e->accept(); break; case BottomLeft: m_clickPoint = r1.topRight(); e->accept(); break; default: break; } } } QGraphicsScene::mousePressEvent(e); } else if (m_tool == TITLE_RECTANGLE) { clearTextSelection(); m_sceneClickPoint = QPointF(xPos, yPos); m_selectedItem = nullptr; e->ignore(); } else if (m_tool == TITLE_TEXT) { clearTextSelection(); MyTextItem *textItem = new MyTextItem(i18n("Text"), nullptr); yPos = (((int)e->scenePos().y() - (int)(m_fontSize / 2)) / m_gridSize) * m_gridSize; textItem->setPos(xPos, yPos); addItem(textItem); textItem->setFlags(QGraphicsItem::ItemIsMovable | QGraphicsItem::ItemIsSelectable); textItem->setTextInteractionFlags(Qt::TextEditorInteraction); textItem->setFocus(Qt::MouseFocusReason); emit newText(textItem); m_selectedItem = textItem; m_selectedItem->setSelected(true); m_createdText = true; } // qCDebug(KDENLIVE_LOG) << "////// MOUSE CLICK, RESIZE MODE: " << m_resizeMode; } void GraphicsSceneRectMove::clearTextSelection(bool reset) { if ((m_selectedItem != nullptr) && m_selectedItem->type() == QGraphicsTextItem::Type) { // disable text editing - MyTextItem *t = static_cast(m_selectedItem); + auto *t = static_cast(m_selectedItem); t->textCursor().setPosition(0); QTextBlock cur = t->textCursor().block(); t->setTextCursor(QTextCursor(cur)); t->setTextInteractionFlags(Qt::NoTextInteraction); } if (reset) { m_selectedItem = nullptr; clearSelection(); } } void GraphicsSceneRectMove::mouseMoveEvent(QGraphicsSceneMouseEvent *e) { QList viewlist = views(); if (viewlist.isEmpty()) { e->ignore(); return; } QGraphicsView *view = viewlist.constFirst(); if (m_pan) { QPoint diff = e->lastScreenPos() - e->screenPos(); view->horizontalScrollBar()->setValue(view->horizontalScrollBar()->value() + diff.x()); view->verticalScrollBar()->setValue(view->verticalScrollBar()->value() + diff.y()); e->accept(); QGraphicsScene::mouseMoveEvent(e); return; } if (e->buttons() != Qt::NoButton && !m_moveStarted) { if ((view->mapFromScene(e->scenePos()) - view->mapFromScene(m_clickPoint)).manhattanLength() < QApplication::startDragDistance()) { e->ignore(); return; } m_moveStarted = true; } if ((m_selectedItem != nullptr) && ((e->buttons() & Qt::LeftButton) != 0u)) { if (m_selectedItem->type() == QGraphicsRectItem::Type || m_selectedItem->type() == QGraphicsSvgItem::Type || m_selectedItem->type() == QGraphicsPixmapItem::Type) { QRectF newrect; if (m_selectedItem->type() == QGraphicsRectItem::Type) { newrect = ((QGraphicsRectItem *)m_selectedItem)->rect(); } else { newrect = m_selectedItem->boundingRect(); } int xPos = ((int)e->scenePos().x() / m_gridSize) * m_gridSize; int yPos = ((int)e->scenePos().y() / m_gridSize) * m_gridSize; QPointF newpoint(xPos, yPos); switch (m_resizeMode) { case BottomRight: case BottomLeft: case TopRight: case TopLeft: newrect = QRectF(m_clickPoint, newpoint).normalized(); break; case Up: newrect = QRectF(m_clickPoint, QPointF(m_clickPoint.x() - newrect.width(), newpoint.y())).normalized(); break; case Down: newrect = QRectF(m_clickPoint, QPointF(newrect.width() + m_clickPoint.x(), newpoint.y())).normalized(); break; case Right: newrect = QRectF(m_clickPoint, QPointF(newpoint.x(), m_clickPoint.y() + newrect.height())).normalized(); break; case Left: newrect = QRectF(m_clickPoint, QPointF(newpoint.x(), m_clickPoint.y() - newrect.height())).normalized(); break; default: break; } if (m_selectedItem->type() == QGraphicsRectItem::Type && m_resizeMode != NoResize) { - MyRectItem *gi = static_cast(m_selectedItem); + auto *gi = static_cast(m_selectedItem); // Resize using aspect ratio if (!m_selectedItem->data(0).isNull()) { // we want to keep aspect ratio double hRatio = (double)newrect.width() / m_selectedItem->data(0).toInt(); double vRatio = (double)newrect.height() / m_selectedItem->data(1).toInt(); if (hRatio < vRatio) { newrect.setHeight(m_selectedItem->data(1).toInt() * hRatio); } else { newrect.setWidth(m_selectedItem->data(0).toInt() * vRatio); } } gi->setPos(newrect.topLeft()); gi->setRect(QRectF(QPointF(), newrect.bottomRight() - newrect.topLeft())); return; } QGraphicsScene::mouseMoveEvent(e); } else if (m_selectedItem->type() == QGraphicsTextItem::Type) { - MyTextItem *t = static_cast(m_selectedItem); + auto *t = static_cast(m_selectedItem); if ((t->textInteractionFlags() & static_cast((Qt::TextEditorInteraction) != 0)) != 0) { QGraphicsScene::mouseMoveEvent(e); return; } QGraphicsScene::mouseMoveEvent(e); m_sceneClickPoint = e->scenePos(); } emit itemMoved(); } else if (m_tool == TITLE_SELECT) { QPointF p = e->scenePos(); p += QPoint(-2, -2); m_resizeMode = NoResize; bool itemFound = false; QList list = items(QRectF(p, QSizeF(4, 4)).toRect()); for (const QGraphicsItem *g : list) { if (!(g->flags() & QGraphicsItem::ItemIsSelectable)) { continue; } if ((g->type() == QGraphicsSvgItem::Type || g->type() == QGraphicsPixmapItem::Type) && g->zValue() > -1000) { // image or svg item setCursor(Qt::OpenHandCursor); itemFound = true; break; } else if (g->type() == QGraphicsRectItem::Type && g->zValue() > -1000) { if (view == nullptr) { continue; } QRectF r1 = ((const QGraphicsRectItem *)g)->rect().normalized(); itemFound = true; // Item mapped coordinates QPolygon r = g->deviceTransform(view->viewportTransform()).map(r1).toPolygon(); QPainterPath top(r.point(0)); top.lineTo(r.point(1)); QPainterPath bottom(r.point(2)); bottom.lineTo(r.point(3)); QPainterPath left(r.point(0)); left.lineTo(r.point(3)); QPainterPath right(r.point(1)); right.lineTo(r.point(2)); // The area interested by the mouse pointer QPoint viewPos = view->mapFromScene(e->scenePos()); QPainterPath mouseArea; QFontMetrics metrics(font()); int box = metrics.lineSpacing() / 2; mouseArea.addRect(viewPos.x() - box, viewPos.y() - box, 2 * box, 2 * box); // Check for collisions between the mouse and the borders if (mouseArea.contains(r.point(0))) { m_possibleAction = TopLeft; setCursor(Qt::SizeFDiagCursor); } else if (mouseArea.contains(r.point(2))) { m_possibleAction = BottomRight; setCursor(Qt::SizeFDiagCursor); } else if (mouseArea.contains(r.point(1))) { m_possibleAction = TopRight; setCursor(Qt::SizeBDiagCursor); } else if (mouseArea.contains(r.point(3))) { m_possibleAction = BottomLeft; setCursor(Qt::SizeBDiagCursor); } else if (top.intersects(mouseArea)) { m_possibleAction = Up; setCursor(Qt::SizeVerCursor); } else if (bottom.intersects(mouseArea)) { m_possibleAction = Down; setCursor(Qt::SizeVerCursor); } else if (right.intersects(mouseArea)) { m_possibleAction = Right; setCursor(Qt::SizeHorCursor); } else if (left.intersects(mouseArea)) { m_possibleAction = Left; setCursor(Qt::SizeHorCursor); } else { setCursor(Qt::OpenHandCursor); m_possibleAction = NoResize; } } break; } if (!itemFound) { m_possibleAction = NoResize; setCursor(Qt::ArrowCursor); } QGraphicsScene::mouseMoveEvent(e); } else if (m_tool == TITLE_RECTANGLE && ((e->buttons() & Qt::LeftButton) != 0u)) { if (m_selectedItem == nullptr) { // create new rect item QRectF r(0, 0, e->scenePos().x() - m_sceneClickPoint.x(), e->scenePos().y() - m_sceneClickPoint.y()); r = r.normalized(); auto *rect = new MyRectItem(); rect->setRect(QRectF(0, 0, r.width(), r.height())); addItem(rect); m_selectedItem = rect; m_selectedItem->setPos(m_sceneClickPoint); m_selectedItem->setSelected(true); emit newRect(rect); m_selectedItem->setFlags(QGraphicsItem::ItemIsMovable | QGraphicsItem::ItemIsSelectable | QGraphicsItem::ItemSendsGeometryChanges); m_resizeMode = BottomRight; QGraphicsScene::mouseMoveEvent(e); } } } void GraphicsSceneRectMove::wheelEvent(QGraphicsSceneWheelEvent *wheelEvent) { if (wheelEvent->modifiers() == Qt::ControlModifier) { QList viewlist = views(); ////qCDebug(KDENLIVE_LOG) << wheelEvent->delta() << ' ' << zoom; if (!viewlist.isEmpty()) { if (wheelEvent->delta() > 0) { emit sceneZoom(true); } else { emit sceneZoom(false); } } } else { wheelEvent->setAccepted(false); } } void GraphicsSceneRectMove::setScale(double s) { if (m_zoom < 1.0 / 7.0 && s < 1.0) { return; } if (m_zoom > 10.0 / 7.9 && s > 1.0) { return; } QList viewlist = views(); if (!viewlist.isEmpty()) { viewlist[0]->scale(s, s); m_zoom = m_zoom * s; } ////qCDebug(KDENLIVE_LOG)<<"////////// ZOOM: "< viewlist = views(); if (!viewlist.isEmpty()) { viewlist[0]->resetTransform(); viewlist[0]->scale(s, s); m_zoom = s; } ////qCDebug(KDENLIVE_LOG)<<"////////// ZOOM: "< l = views(); for (QGraphicsView *v : l) { v->setCursor(c); } } void GraphicsSceneRectMove::slotUpdateFontSize(int s) { m_fontSize = s; } void GraphicsSceneRectMove::drawForeground(QPainter *painter, const QRectF &rect) { // draw the grid if needed if (m_gridSize <= 1) { return; } QPen pen(QColor(255, 0, 0, 100)); painter->setPen(pen); qreal left = int(rect.left()) - (int(rect.left()) % m_gridSize); qreal top = int(rect.top()) - (int(rect.top()) % m_gridSize); QVector points; for (qreal x = left; x < rect.right(); x += m_gridSize) { for (qreal y = top; y < rect.bottom(); y += m_gridSize) { points.append(QPointF(x, y)); } } painter->drawPoints(points.data(), points.size()); } int GraphicsSceneRectMove::gridSize() const { return m_gridSize; } void GraphicsSceneRectMove::slotUseGrid(bool enableGrid) { m_gridSize = enableGrid ? 20 : 1; } void GraphicsSceneRectMove::addNewItem(QGraphicsItem *item) { clearSelection(); addItem(item); item->setSelected(true); m_selectedItem = item; } diff --git a/src/titler/graphicsscenerectmove.h b/src/titler/graphicsscenerectmove.h index 8b8625995..b07adf1e0 100644 --- a/src/titler/graphicsscenerectmove.h +++ b/src/titler/graphicsscenerectmove.h @@ -1,170 +1,170 @@ /*************************************************************************** * copyright (C) 2008 by Marco Gittler (g.marco@freenet.de) * * 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 GRAPHICSSCENERECTMOVE_H #define GRAPHICSSCENERECTMOVE_H #include #include #include #include enum resizeModes { NoResize = 0, TopLeft, BottomLeft, TopRight, BottomRight, Left, Right, Up, Down }; enum TITLETOOL { TITLE_SELECT = 0, TITLE_RECTANGLE = 1, TITLE_TEXT = 2, TITLE_IMAGE = 3 }; class MyQGraphicsEffect : public QGraphicsEffect { public: explicit MyQGraphicsEffect(QObject *parent = nullptr); void setOffset(int xOffset, int yOffset, int blur); void setShadow(const QImage &image); protected: void draw(QPainter *painter) override; private: - int m_xOffset; - int m_yOffset; - int m_blur; + int m_xOffset{0}; + int m_yOffset{0}; + int m_blur{0}; QImage m_shadow; }; class MyTextItem : public QGraphicsTextItem { Q_OBJECT public: MyTextItem(const QString &, QGraphicsItem *parent = nullptr); void setAlignment(Qt::Alignment alignment); /** @brief returns an extended bounding containing shadow */ QRectF boundingRect() const override; /** @brief returns the normal bounding rect around text */ QRectF baseBoundingRect() const; Qt::Alignment alignment() const; void updateShadow(bool enabled, int blur, int xoffset, int yoffset, QColor color); QStringList shadowInfo() const; void loadShadow(const QStringList &info); void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *evt) override; void setTextColor(const QColor &col); protected: QVariant itemChange(GraphicsItemChange change, const QVariant &value) override; void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *w) override; private: Qt::Alignment m_alignment; QPoint m_shadowOffset; int m_shadowBlur; QColor m_shadowColor; QPainterPath m_path; MyQGraphicsEffect *m_shadowEffect; void updateShadow(); void blurShadow(QImage &image, int radius); void refreshFormat(); public slots: void updateGeometry(int, int, int); void updateGeometry(); }; class MyRectItem : public QGraphicsRectItem { public: explicit MyRectItem(QGraphicsItem *parent = nullptr); void setRect(const QRectF &rectangle); protected: QVariant itemChange(GraphicsItemChange change, const QVariant &value) override; private: QRectF m_rect; }; class MyPixmapItem : public QGraphicsPixmapItem { public: MyPixmapItem(const QPixmap &pixmap, QGraphicsItem *parent = nullptr); protected: QVariant itemChange(GraphicsItemChange change, const QVariant &value) override; }; class MySvgItem : public QGraphicsSvgItem { public: MySvgItem(const QString &fileName = QString(), QGraphicsItem *parent = nullptr); protected: QVariant itemChange(GraphicsItemChange change, const QVariant &value) override; }; class GraphicsSceneRectMove : public QGraphicsScene { Q_OBJECT public: explicit GraphicsSceneRectMove(QObject *parent = nullptr); void setSelectedItem(QGraphicsItem *item); void setScale(double s); void setZoom(double s); void setTool(TITLETOOL tool); TITLETOOL tool() const; /** @brief Get out of text edit mode. If reset is true, we also unselect all items */ void clearTextSelection(bool reset = true); int gridSize() const; void addNewItem(QGraphicsItem *item); public slots: void slotUpdateFontSize(int s); void slotUseGrid(bool enableGrid); protected: void keyPressEvent(QKeyEvent *keyEvent) override; void mousePressEvent(QGraphicsSceneMouseEvent *) override; void mouseReleaseEvent(QGraphicsSceneMouseEvent *) override; void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *e) override; /** @brief Resizes and moves items */ void mouseMoveEvent(QGraphicsSceneMouseEvent *) override; void wheelEvent(QGraphicsSceneWheelEvent *wheelEvent) override; void drawForeground(QPainter *painter, const QRectF &rect) override; private: void setCursor(const QCursor &); double m_zoom; - QGraphicsItem *m_selectedItem; - resizeModes m_resizeMode; - resizeModes m_possibleAction; + QGraphicsItem *m_selectedItem{nullptr}; + resizeModes m_resizeMode{NoResize}; + resizeModes m_possibleAction{NoResize}; QPointF m_sceneClickPoint; - TITLETOOL m_tool; + TITLETOOL m_tool{TITLE_RECTANGLE}; QPointF m_clickPoint; int m_fontSize; - int m_gridSize; - bool m_createdText; - bool m_moveStarted; - bool m_pan; + int m_gridSize{20}; + bool m_createdText{false}; + bool m_moveStarted{false}; + bool m_pan{false}; signals: void itemMoved(); void sceneZoom(bool); void newRect(QGraphicsRectItem *); void newText(MyTextItem *); void actionFinished(); void doubleClickEvent(); }; #endif diff --git a/src/titler/titledocument.cpp b/src/titler/titledocument.cpp index cf3de6fae..d7266045f 100644 --- a/src/titler/titledocument.cpp +++ b/src/titler/titledocument.cpp @@ -1,742 +1,742 @@ /*************************************************************************** titledocument.h - description ------------------- begin : Feb 28 2008 copyright : (C) 2008 by Marco Gittler email : g.marco@freenet.de ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "titledocument.h" #include "gradientwidget.h" #include "graphicsscenerectmove.h" #include "kdenlivesettings.h" #include "timecode.h" #include #include #include #include "kdenlive_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef Q_OS_MAC #include #endif #include #include #include #include QByteArray fileToByteArray(const QString &filename) { QByteArray ret; QFile file(filename); if (file.open(QIODevice::ReadOnly)) { while (!file.atEnd()) { ret.append(file.readLine()); } } return ret; } TitleDocument::TitleDocument() { m_scene = nullptr; m_width = 0; m_height = 0; m_missingElements = 0; } void TitleDocument::setScene(QGraphicsScene *_scene, int width, int height) { m_scene = _scene; m_width = width; m_height = height; } int TitleDocument::base64ToUrl(QGraphicsItem *item, QDomElement &content, bool embed) { if (embed) { if (!item->data(Qt::UserRole + 1).toString().isEmpty()) { content.setAttribute(QStringLiteral("base64"), item->data(Qt::UserRole + 1).toString()); } else if (!item->data(Qt::UserRole).toString().isEmpty()) { content.setAttribute(QStringLiteral("base64"), fileToByteArray(item->data(Qt::UserRole).toString()).toBase64().data()); } content.removeAttribute(QStringLiteral("url")); } else { // save for project files to disk QString base64 = item->data(Qt::UserRole + 1).toString(); if (!base64.isEmpty()) { QString titlePath; if (!m_projectPath.isEmpty()) { titlePath = m_projectPath; } else { titlePath = QStringLiteral("/tmp/titles"); } QString filename = extractBase64Image(titlePath, base64); if (!filename.isEmpty()) { content.setAttribute(QStringLiteral("url"), filename); content.removeAttribute(QStringLiteral("base64")); } } else { return 1; } } return 0; } // static const QString TitleDocument::extractBase64Image(const QString &titlePath, const QString &data) { QString filename = titlePath + QString(QCryptographicHash::hash(data.toLatin1(), QCryptographicHash::Md5).toHex().append(".titlepart")); QDir dir; dir.mkpath(titlePath); QFile f(filename); if (f.open(QIODevice::WriteOnly)) { f.write(QByteArray::fromBase64(data.toLatin1())); f.close(); return filename; } return QString(); } QDomDocument TitleDocument::xml(QGraphicsRectItem *startv, QGraphicsRectItem *endv, bool embed) { QDomDocument doc; QDomElement main = doc.createElement(QStringLiteral("kdenlivetitle")); main.setAttribute(QStringLiteral("width"), m_width); main.setAttribute(QStringLiteral("height"), m_height); // Save locale #ifndef Q_OS_MAC const char *locale = setlocale(LC_NUMERIC, nullptr); #else const char *locale = setlocale(LC_NUMERIC_MASK, nullptr); #endif main.setAttribute(QStringLiteral("LC_NUMERIC"), locale); doc.appendChild(main); QTextCursor cur; QTextBlockFormat format; for (QGraphicsItem *item : m_scene->items()) { if (!(item->flags() & QGraphicsItem::ItemIsSelectable)) { continue; } QDomElement e = doc.createElement(QStringLiteral("item")); QDomElement content = doc.createElement(QStringLiteral("content")); QFont font; QString gradient; MyTextItem *t; double xPosition = item->pos().x(); switch (item->type()) { case 7: e.setAttribute(QStringLiteral("type"), QStringLiteral("QGraphicsPixmapItem")); content.setAttribute(QStringLiteral("url"), item->data(Qt::UserRole).toString()); base64ToUrl(item, content, embed); break; case 13: e.setAttribute(QStringLiteral("type"), QStringLiteral("QGraphicsSvgItem")); content.setAttribute(QStringLiteral("url"), item->data(Qt::UserRole).toString()); base64ToUrl(item, content, embed); break; case 3: e.setAttribute(QStringLiteral("type"), QStringLiteral("QGraphicsRectItem")); content.setAttribute(QStringLiteral("rect"), rectFToString(static_cast(item)->rect().normalized())); content.setAttribute(QStringLiteral("pencolor"), colorToString(static_cast(item)->pen().color())); if (static_cast(item)->pen() == Qt::NoPen) { content.setAttribute(QStringLiteral("penwidth"), 0); } else { content.setAttribute(QStringLiteral("penwidth"), static_cast(item)->pen().width()); } content.setAttribute(QStringLiteral("brushcolor"), colorToString(static_cast(item)->brush().color())); gradient = item->data(TitleDocument::Gradient).toString(); if (!gradient.isEmpty()) { content.setAttribute(QStringLiteral("gradient"), gradient); } break; case 8: e.setAttribute(QStringLiteral("type"), QStringLiteral("QGraphicsTextItem")); t = static_cast(item); // Don't save empty text nodes if (t->toPlainText().simplified().isEmpty()) { continue; } // content.appendChild(doc.createTextNode(((QGraphicsTextItem*)item)->toHtml())); content.appendChild(doc.createTextNode(t->toPlainText())); font = t->font(); content.setAttribute(QStringLiteral("font"), font.family()); content.setAttribute(QStringLiteral("font-weight"), font.weight()); content.setAttribute(QStringLiteral("font-pixel-size"), font.pixelSize()); content.setAttribute(QStringLiteral("font-italic"), static_cast(font.italic())); content.setAttribute(QStringLiteral("font-underline"), static_cast(font.underline())); content.setAttribute(QStringLiteral("letter-spacing"), QString::number(font.letterSpacing())); gradient = item->data(TitleDocument::Gradient).toString(); if (!gradient.isEmpty()) { content.setAttribute(QStringLiteral("gradient"), gradient); } cur = QTextCursor(t->document()); cur.select(QTextCursor::Document); format = cur.blockFormat(); if (t->toPlainText() == QLatin1String("%s")) { // template text box, adjust size for later remplacement text if (t->alignment() == Qt::AlignHCenter) { // grow dimensions on both sides double xcenter = item->pos().x() + (t->baseBoundingRect().width()) / 2; double offset = qMin(xcenter, m_width - xcenter); xPosition = xcenter - offset; content.setAttribute(QStringLiteral("box-width"), QString::number(2 * offset)); } else if (t->alignment() == Qt::AlignRight) { // grow to the left double offset = item->pos().x() + (t->baseBoundingRect().width()); xPosition = 0; content.setAttribute(QStringLiteral("box-width"), QString::number(offset)); } else { // left align, grow on right side double offset = m_width - item->pos().x(); content.setAttribute(QStringLiteral("box-width"), QString::number(offset)); } } else { content.setAttribute(QStringLiteral("box-width"), QString::number(t->baseBoundingRect().width())); } content.setAttribute(QStringLiteral("box-height"), QString::number(t->baseBoundingRect().height())); if (!t->data(TitleDocument::LineSpacing).isNull()) { content.setAttribute(QStringLiteral("line-spacing"), QString::number(t->data(TitleDocument::LineSpacing).toInt())); } { QTextCursor cursor(t->document()); cursor.select(QTextCursor::Document); QColor fontcolor = cursor.charFormat().foreground().color(); content.setAttribute(QStringLiteral("font-color"), colorToString(fontcolor)); if (!t->data(TitleDocument::OutlineWidth).isNull()) { content.setAttribute(QStringLiteral("font-outline"), QString::number(t->data(TitleDocument::OutlineWidth).toDouble())); } if (!t->data(TitleDocument::OutlineColor).isNull()) { QVariant variant = t->data(TitleDocument::OutlineColor); QColor outlineColor = variant.value(); content.setAttribute(QStringLiteral("font-outline-color"), colorToString(outlineColor)); } } if (!t->data(100).isNull()) { QStringList effectParams = t->data(100).toStringList(); QString effectName = effectParams.takeFirst(); content.setAttribute(QStringLiteral("textwidth"), QString::number(t->sceneBoundingRect().width())); content.setAttribute(effectName, effectParams.join(QLatin1Char(';'))); } // Only save when necessary. if (t->data(OriginXLeft).toInt() == AxisInverted) { content.setAttribute(QStringLiteral("kdenlive-axis-x-inverted"), t->data(OriginXLeft).toInt()); } if (t->data(OriginYTop).toInt() == AxisInverted) { content.setAttribute(QStringLiteral("kdenlive-axis-y-inverted"), t->data(OriginYTop).toInt()); } if (t->textWidth() > 0) { content.setAttribute(QStringLiteral("alignment"), (int)t->alignment()); } content.setAttribute(QStringLiteral("shadow"), t->shadowInfo().join(QLatin1Char(';'))); break; default: continue; } // position QDomElement pos = doc.createElement(QStringLiteral("position")); pos.setAttribute(QStringLiteral("x"), QString::number(xPosition)); pos.setAttribute(QStringLiteral("y"), QString::number(item->pos().y())); QTransform transform = item->transform(); QDomElement tr = doc.createElement(QStringLiteral("transform")); if (!item->data(TitleDocument::ZoomFactor).isNull()) { tr.setAttribute(QStringLiteral("zoom"), QString::number(item->data(TitleDocument::ZoomFactor).toInt())); } if (!item->data(TitleDocument::RotateFactor).isNull()) { QList rotlist = item->data(TitleDocument::RotateFactor).toList(); tr.setAttribute(QStringLiteral("rotation"), QStringLiteral("%1,%2,%3").arg(rotlist[0].toDouble()).arg(rotlist[1].toDouble()).arg(rotlist[2].toDouble())); } tr.appendChild(doc.createTextNode(QStringLiteral("%1,%2,%3,%4,%5,%6,%7,%8,%9") .arg(transform.m11()) .arg(transform.m12()) .arg(transform.m13()) .arg(transform.m21()) .arg(transform.m22()) .arg(transform.m23()) .arg(transform.m31()) .arg(transform.m32()) .arg(transform.m33()))); e.setAttribute(QStringLiteral("z-index"), item->zValue()); pos.appendChild(tr); e.appendChild(pos); e.appendChild(content); if (item->zValue() > -1000) { main.appendChild(e); } } if ((startv != nullptr) && (endv != nullptr)) { QDomElement endport = doc.createElement(QStringLiteral("endviewport")); QDomElement startport = doc.createElement(QStringLiteral("startviewport")); QRectF r(endv->pos().x(), endv->pos().y(), endv->rect().width(), endv->rect().height()); endport.setAttribute(QStringLiteral("rect"), rectFToString(r)); QRectF r2(startv->pos().x(), startv->pos().y(), startv->rect().width(), startv->rect().height()); startport.setAttribute(QStringLiteral("rect"), rectFToString(r2)); main.appendChild(startport); main.appendChild(endport); } QDomElement backgr = doc.createElement(QStringLiteral("background")); QColor color = getBackgroundColor(); backgr.setAttribute(QStringLiteral("color"), colorToString(color)); main.appendChild(backgr); return doc; } /** \brief Get the background color (incl. alpha) from the document, if possibly * \returns The background color of the document, inclusive alpha. If none found, returns (0,0,0,0) */ QColor TitleDocument::getBackgroundColor() const { QColor color(0, 0, 0, 0); if (m_scene) { QList items = m_scene->items(); - for (int i = 0; i < items.size(); ++i) { - if ((int)items.at(i)->zValue() == -1100) { - color = static_cast(items.at(i))->brush().color(); + for (auto item : items) { + if ((int)item->zValue() == -1100) { + color = static_cast(item)->brush().color(); return color; } } } return color; } bool TitleDocument::saveDocument(const QUrl &url, QGraphicsRectItem *startv, QGraphicsRectItem *endv, int duration, bool embed) { if (!m_scene) { return false; } QDomDocument doc = xml(startv, endv, embed); doc.documentElement().setAttribute(QStringLiteral("duration"), duration); // keep some time for backwards compatibility (opening projects with older versions) - 26/12/12 doc.documentElement().setAttribute(QStringLiteral("out"), duration); QTemporaryFile tmpfile; if (!tmpfile.open()) { qCWarning(KDENLIVE_LOG) << "///// CANNOT CREATE TMP FILE in: " << tmpfile.fileName(); return false; } QFile xmlf(tmpfile.fileName()); if (!xmlf.open(QIODevice::WriteOnly)) { return false; } xmlf.write(doc.toString().toUtf8()); if (xmlf.error() != QFile::NoError) { xmlf.close(); return false; } xmlf.close(); KIO::FileCopyJob *copyjob = KIO::file_copy(QUrl::fromLocalFile(tmpfile.fileName()), url, -1, KIO::Overwrite); return copyjob->exec(); } int TitleDocument::loadFromXml(const QDomDocument &doc, QGraphicsRectItem *startv, QGraphicsRectItem *endv, int *duration, const QString &projectpath) { m_projectPath = projectpath; m_missingElements = 0; QDomNodeList titles = doc.elementsByTagName(QStringLiteral("kdenlivetitle")); // TODO: Check if the opened title size is equal to project size, otherwise warn user and rescale if (doc.documentElement().hasAttribute(QStringLiteral("width")) && doc.documentElement().hasAttribute(QStringLiteral("height"))) { int doc_width = doc.documentElement().attribute(QStringLiteral("width")).toInt(); int doc_height = doc.documentElement().attribute(QStringLiteral("height")).toInt(); if (doc_width != m_width || doc_height != m_height) { KMessageBox::information(QApplication::activeWindow(), i18n("This title clip was created with a different frame size."), i18n("Title Profile")); // TODO: convert using QTransform m_width = doc_width; m_height = doc_height; } } else { // Document has no size info, it is likely an old version title, so ignore viewport data QDomNodeList viewportlist = doc.documentElement().elementsByTagName(QStringLiteral("startviewport")); if (!viewportlist.isEmpty()) { doc.documentElement().removeChild(viewportlist.at(0)); } viewportlist = doc.documentElement().elementsByTagName(QStringLiteral("endviewport")); if (!viewportlist.isEmpty()) { doc.documentElement().removeChild(viewportlist.at(0)); } } if (doc.documentElement().hasAttribute(QStringLiteral("duration"))) { *duration = doc.documentElement().attribute(QStringLiteral("duration")).toInt(); } else if (doc.documentElement().hasAttribute(QStringLiteral("out"))) { *duration = doc.documentElement().attribute(QStringLiteral("out")).toInt(); } else { *duration = Timecode().getFrameCount(KdenliveSettings::title_duration()); } int maxZValue = 0; if (!titles.isEmpty()) { QDomNodeList items = titles.item(0).childNodes(); for (int i = 0; i < items.count(); ++i) { QGraphicsItem *gitem = nullptr; QDomNode itemNode = items.item(i); // qCDebug(KDENLIVE_LOG) << items.item(i).attributes().namedItem("type").nodeValue(); int zValue = itemNode.attributes().namedItem(QStringLiteral("z-index")).nodeValue().toInt(); double xPosition = itemNode.namedItem(QStringLiteral("position")).attributes().namedItem(QStringLiteral("x")).nodeValue().toDouble(); if (zValue > -1000) { if (itemNode.attributes().namedItem(QStringLiteral("type")).nodeValue() == QLatin1String("QGraphicsTextItem")) { QDomNamedNodeMap txtProperties = itemNode.namedItem(QStringLiteral("content")).attributes(); QFont font(txtProperties.namedItem(QStringLiteral("font")).nodeValue()); QDomNode node = txtProperties.namedItem(QStringLiteral("font-bold")); if (!node.isNull()) { // Old: Bold/Not bold. font.setBold(node.nodeValue().toInt() != 0); } else { // New: Font weight (QFont::) font.setWeight(txtProperties.namedItem(QStringLiteral("font-weight")).nodeValue().toInt()); } // font.setBold(txtProperties.namedItem("font-bold").nodeValue().toInt()); font.setItalic(txtProperties.namedItem(QStringLiteral("font-italic")).nodeValue().toInt() != 0); font.setUnderline(txtProperties.namedItem(QStringLiteral("font-underline")).nodeValue().toInt() != 0); // Older Kdenlive version did not store pixel size but point size if (txtProperties.namedItem(QStringLiteral("font-pixel-size")).isNull()) { KMessageBox::information(QApplication::activeWindow(), i18n("Some of your text clips were saved with size in points, which means " "different sizes on different displays. They will be converted to pixel " "size, making them portable, but you could have to adjust their size."), i18n("Text Clips Updated")); QFont f2; f2.setPointSize(txtProperties.namedItem(QStringLiteral("font-size")).nodeValue().toInt()); font.setPixelSize(QFontInfo(f2).pixelSize()); } else { font.setPixelSize(txtProperties.namedItem(QStringLiteral("font-pixel-size")).nodeValue().toInt()); } font.setLetterSpacing(QFont::AbsoluteSpacing, txtProperties.namedItem(QStringLiteral("letter-spacing")).nodeValue().toInt()); QColor col(stringToColor(txtProperties.namedItem(QStringLiteral("font-color")).nodeValue())); MyTextItem *txt = new MyTextItem(itemNode.namedItem(QStringLiteral("content")).firstChild().nodeValue(), nullptr); m_scene->addItem(txt); txt->setFont(font); txt->setTextInteractionFlags(Qt::NoTextInteraction); QTextCursor cursor(txt->document()); cursor.select(QTextCursor::Document); QTextCharFormat cformat = cursor.charFormat(); if (txtProperties.namedItem(QStringLiteral("font-outline")).nodeValue().toDouble() > 0.0) { txt->setData(TitleDocument::OutlineWidth, txtProperties.namedItem(QStringLiteral("font-outline")).nodeValue().toDouble()); txt->setData(TitleDocument::OutlineColor, stringToColor(txtProperties.namedItem(QStringLiteral("font-outline-color")).nodeValue())); cformat.setTextOutline(QPen(QColor(stringToColor(txtProperties.namedItem(QStringLiteral("font-outline-color")).nodeValue())), txtProperties.namedItem(QStringLiteral("font-outline")).nodeValue().toDouble(), Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin)); } if (!txtProperties.namedItem(QStringLiteral("line-spacing")).isNull()) { int lineSpacing = txtProperties.namedItem(QStringLiteral("line-spacing")).nodeValue().toInt(); QTextBlockFormat format = cursor.blockFormat(); format.setLineHeight(lineSpacing, QTextBlockFormat::LineDistanceHeight); cursor.setBlockFormat(format); txt->setData(TitleDocument::LineSpacing, lineSpacing); } txt->setTextColor(col); cformat.setForeground(QBrush(col)); cursor.setCharFormat(cformat); if (!txtProperties.namedItem(QStringLiteral("gradient")).isNull()) { // Gradient color QString data = txtProperties.namedItem(QStringLiteral("gradient")).nodeValue(); txt->setData(TitleDocument::Gradient, data); QLinearGradient gr = GradientWidget::gradientFromString(data, txt->boundingRect().width(), txt->boundingRect().height()); cformat.setForeground(QBrush(gr)); cursor.setCharFormat(cformat); } if (!txtProperties.namedItem(QStringLiteral("alignment")).isNull()) { txt->setAlignment((Qt::Alignment)txtProperties.namedItem(QStringLiteral("alignment")).nodeValue().toInt()); } if (!txtProperties.namedItem(QStringLiteral("kdenlive-axis-x-inverted")).isNull()) { txt->setData(OriginXLeft, txtProperties.namedItem(QStringLiteral("kdenlive-axis-x-inverted")).nodeValue().toInt()); } if (!txtProperties.namedItem(QStringLiteral("kdenlive-axis-y-inverted")).isNull()) { txt->setData(OriginYTop, txtProperties.namedItem(QStringLiteral("kdenlive-axis-y-inverted")).nodeValue().toInt()); } if (!txtProperties.namedItem(QStringLiteral("shadow")).isNull()) { QString info = txtProperties.namedItem(QStringLiteral("shadow")).nodeValue(); txt->loadShadow(info.split(QLatin1Char(';'))); } // Effects if (!txtProperties.namedItem(QStringLiteral("typewriter")).isNull()) { QStringList effData = QStringList() << QStringLiteral("typewriter") << txtProperties.namedItem(QStringLiteral("typewriter")).nodeValue(); txt->setData(100, effData); } if (txt->toPlainText() == QLatin1String("%s")) { // template text box, adjust size for later remplacement text if (txt->alignment() == Qt::AlignHCenter) { // grow dimensions on both sides double width = txtProperties.namedItem(QStringLiteral("box-width")).nodeValue().toDouble(); double xcenter = (width - xPosition) / 2.0; xPosition = xcenter - txt->boundingRect().width() / 2; } else if (txt->alignment() == Qt::AlignRight) { // grow to the left xPosition = xPosition + txtProperties.namedItem(QStringLiteral("box-width")).nodeValue().toDouble() - txt->boundingRect().width(); } else { // left align, grow on right side, nothing to do } } gitem = txt; } else if (itemNode.attributes().namedItem(QStringLiteral("type")).nodeValue() == QLatin1String("QGraphicsRectItem")) { QDomNamedNodeMap rectProperties = itemNode.namedItem(QStringLiteral("content")).attributes(); QString rect = rectProperties.namedItem(QStringLiteral("rect")).nodeValue(); QString br_str = rectProperties.namedItem(QStringLiteral("brushcolor")).nodeValue(); QString pen_str = rectProperties.namedItem(QStringLiteral("pencolor")).nodeValue(); double penwidth = rectProperties.namedItem(QStringLiteral("penwidth")).nodeValue().toDouble(); auto *rec = new MyRectItem(); rec->setRect(stringToRect(rect)); if (penwidth > 0) { rec->setPen(QPen(QBrush(stringToColor(pen_str)), penwidth, Qt::SolidLine, Qt::SquareCap, Qt::RoundJoin)); } else { rec->setPen(Qt::NoPen); } if (!rectProperties.namedItem(QStringLiteral("gradient")).isNull()) { // Gradient color QString data = rectProperties.namedItem(QStringLiteral("gradient")).nodeValue(); rec->setData(TitleDocument::Gradient, data); QLinearGradient gr = GradientWidget::gradientFromString(data, rec->rect().width(), rec->rect().height()); rec->setBrush(QBrush(gr)); } else { rec->setBrush(QBrush(stringToColor(br_str))); } m_scene->addItem(rec); gitem = rec; } else if (itemNode.attributes().namedItem(QStringLiteral("type")).nodeValue() == QLatin1String("QGraphicsPixmapItem")) { QString url = itemNode.namedItem(QStringLiteral("content")).attributes().namedItem(QStringLiteral("url")).nodeValue(); QString base64 = itemNode.namedItem(QStringLiteral("content")).attributes().namedItem(QStringLiteral("base64")).nodeValue(); QPixmap pix; bool missing = false; if (base64.isEmpty()) { pix.load(url); if (pix.isNull()) { pix = createInvalidPixmap(url); m_missingElements++; missing = true; } } else { pix.loadFromData(QByteArray::fromBase64(base64.toLatin1())); } auto *rec = new MyPixmapItem(pix); if (missing) { rec->setData(Qt::UserRole + 2, 1); } m_scene->addItem(rec); rec->setShapeMode(QGraphicsPixmapItem::BoundingRectShape); rec->setData(Qt::UserRole, url); if (!base64.isEmpty()) { rec->setData(Qt::UserRole + 1, base64); } gitem = rec; } else if (itemNode.attributes().namedItem(QStringLiteral("type")).nodeValue() == QLatin1String("QGraphicsSvgItem")) { QString url = itemNode.namedItem(QStringLiteral("content")).attributes().namedItem(QStringLiteral("url")).nodeValue(); QString base64 = itemNode.namedItem(QStringLiteral("content")).attributes().namedItem(QStringLiteral("base64")).nodeValue(); QGraphicsSvgItem *rec = nullptr; if (base64.isEmpty()) { if (QFile::exists(url)) { rec = new MySvgItem(url); } } else { rec = new MySvgItem(); QSvgRenderer *renderer = new QSvgRenderer(QByteArray::fromBase64(base64.toLatin1()), rec); rec->setSharedRenderer(renderer); // QString elem=rec->elementId(); // QRectF bounds = renderer->boundsOnElement(elem); } if (rec) { m_scene->addItem(rec); rec->setData(Qt::UserRole, url); if (!base64.isEmpty()) { rec->setData(Qt::UserRole + 1, base64); } gitem = rec; } else { QPixmap pix = createInvalidPixmap(url); m_missingElements++; auto *rec2 = new MyPixmapItem(pix); rec2->setData(Qt::UserRole + 2, 1); m_scene->addItem(rec2); rec2->setShapeMode(QGraphicsPixmapItem::BoundingRectShape); rec2->setData(Qt::UserRole, url); gitem = rec2; } } } // pos and transform if (gitem) { QPointF p(xPosition, itemNode.namedItem(QStringLiteral("position")).attributes().namedItem(QStringLiteral("y")).nodeValue().toDouble()); gitem->setPos(p); QDomElement trans = itemNode.namedItem(QStringLiteral("position")).firstChild().toElement(); gitem->setTransform(stringToTransform(trans.firstChild().nodeValue())); QString rotate = trans.attribute(QStringLiteral("rotation")); if (!rotate.isEmpty()) { gitem->setData(TitleDocument::RotateFactor, stringToList(rotate)); } QString zoom = trans.attribute(QStringLiteral("zoom")); if (!zoom.isEmpty()) { gitem->setData(TitleDocument::ZoomFactor, zoom.toInt()); } if (zValue >= maxZValue) { maxZValue = zValue + 1; } gitem->setZValue(zValue); gitem->setFlags(QGraphicsItem::ItemIsMovable | QGraphicsItem::ItemIsSelectable | QGraphicsItem::ItemSendsGeometryChanges); // effects QDomNode eff = itemNode.namedItem(QStringLiteral("effect")); if (!eff.isNull()) { QDomElement e = eff.toElement(); if (e.attribute(QStringLiteral("type")) == QLatin1String("blur")) { auto *blur = new QGraphicsBlurEffect(); blur->setBlurRadius(e.attribute(QStringLiteral("blurradius")).toInt()); gitem->setGraphicsEffect(blur); } else if (e.attribute(QStringLiteral("type")) == QLatin1String("shadow")) { auto *shadow = new QGraphicsDropShadowEffect(); shadow->setBlurRadius(e.attribute(QStringLiteral("blurradius")).toInt()); shadow->setOffset(e.attribute(QStringLiteral("xoffset")).toInt(), e.attribute(QStringLiteral("yoffset")).toInt()); gitem->setGraphicsEffect(shadow); } } } if (itemNode.nodeName() == QLatin1String("background")) { // qCDebug(KDENLIVE_LOG) << items.item(i).attributes().namedItem("color").nodeValue(); QColor color = QColor(stringToColor(itemNode.attributes().namedItem(QStringLiteral("color")).nodeValue())); // color.setAlpha(itemNode.attributes().namedItem("alpha").nodeValue().toInt()); QList sceneItems = m_scene->items(); - for (int j = 0; j < sceneItems.size(); ++j) { - if ((int)sceneItems.at(j)->zValue() == -1100) { - static_cast(sceneItems.at(j))->setBrush(QBrush(color)); + for (auto sceneItem : sceneItems) { + if ((int)sceneItem->zValue() == -1100) { + static_cast(sceneItem)->setBrush(QBrush(color)); break; } } } else if (itemNode.nodeName() == QLatin1String("startviewport") && (startv != nullptr)) { QString rect = itemNode.attributes().namedItem(QStringLiteral("rect")).nodeValue(); QRectF r = stringToRect(rect); startv->setRect(0, 0, r.width(), r.height()); startv->setPos(r.topLeft()); } else if (itemNode.nodeName() == QLatin1String("endviewport") && (endv != nullptr)) { QString rect = itemNode.attributes().namedItem(QStringLiteral("rect")).nodeValue(); QRectF r = stringToRect(rect); endv->setRect(0, 0, r.width(), r.height()); endv->setPos(r.topLeft()); } } } return maxZValue; } int TitleDocument::invalidCount() const { return m_missingElements; } QPixmap TitleDocument::createInvalidPixmap(const QString &url) { int missingHeight = m_height / 10; QPixmap pix(missingHeight, missingHeight); QIcon icon = QIcon::fromTheme(QStringLiteral("messagebox_warning")); pix.fill(QColor(255, 0, 0, 50)); QPainter ptr(&pix); icon.paint(&ptr, 0, 0, missingHeight / 2, missingHeight / 2); QPen pen(Qt::red); pen.setWidth(3); ptr.setPen(pen); ptr.drawText(QRectF(2, 2, missingHeight - 4, missingHeight - 4), Qt::AlignHCenter | Qt::AlignBottom, QFileInfo(url).fileName()); ptr.drawRect(2, 1, missingHeight - 4, missingHeight - 4); ptr.end(); return pix; } QString TitleDocument::colorToString(const QColor &c) { QString ret = QStringLiteral("%1,%2,%3,%4"); ret = ret.arg(c.red()).arg(c.green()).arg(c.blue()).arg(c.alpha()); return ret; } QString TitleDocument::rectFToString(const QRectF &c) { QString ret = QStringLiteral("%1,%2,%3,%4"); ret = ret.arg(c.left()).arg(c.top()).arg(c.width()).arg(c.height()); return ret; } QRectF TitleDocument::stringToRect(const QString &s) { QStringList l = s.split(QLatin1Char(',')); if (l.size() < 4) { - return QRectF(); + return {}; } return QRectF(l.at(0).toDouble(), l.at(1).toDouble(), l.at(2).toDouble(), l.at(3).toDouble()).normalized(); } QColor TitleDocument::stringToColor(const QString &s) { QStringList l = s.split(QLatin1Char(',')); if (l.size() < 4) { return QColor(); } - return QColor(l.at(0).toInt(), l.at(1).toInt(), l.at(2).toInt(), l.at(3).toInt()); + return {l.at(0).toInt(), l.at(1).toInt(), l.at(2).toInt(), l.at(3).toInt()}; } QTransform TitleDocument::stringToTransform(const QString &s) { QStringList l = s.split(QLatin1Char(',')); if (l.size() < 9) { return QTransform(); } - return QTransform(l.at(0).toDouble(), l.at(1).toDouble(), l.at(2).toDouble(), l.at(3).toDouble(), l.at(4).toDouble(), l.at(5).toDouble(), - l.at(6).toDouble(), l.at(7).toDouble(), l.at(8).toDouble()); + return {l.at(0).toDouble(), l.at(1).toDouble(), l.at(2).toDouble(), l.at(3).toDouble(), l.at(4).toDouble(), + l.at(5).toDouble(), l.at(6).toDouble(), l.at(7).toDouble(), l.at(8).toDouble()}; } QList TitleDocument::stringToList(const QString &s) { QStringList l = s.split(QLatin1Char(',')); if (l.size() < 3) { return QList(); } return QList() << QVariant(l.at(0).toDouble()) << QVariant(l.at(1).toDouble()) << QVariant(l.at(2).toDouble()); } int TitleDocument::frameWidth() const { return m_width; } int TitleDocument::frameHeight() const { return m_height; } diff --git a/src/titler/titlewidget.cpp b/src/titler/titlewidget.cpp index d5243b479..9b57a1505 100644 --- a/src/titler/titlewidget.cpp +++ b/src/titler/titlewidget.cpp @@ -1,3124 +1,3122 @@ /*************************************************************************** titlewidget.cpp - description ------------------- begin : Feb 28 2008 copyright : (C) 2008 by Marco Gittler email : g.marco@freenet.de ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "titlewidget.h" #include "KoSliderCombo.h" #include "doc/kthumb.h" #include "gradientwidget.h" #include "kdenlivesettings.h" #include "monitor/monitor.h" #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 #include #include - #include +#include static QList titletemplates; // What exactly is this variable good for? int settingUp = 0; const int IMAGEITEM = 7; const int RECTITEM = 3; const int TEXTITEM = 8; const int NOEFFECT = 0; const int BLUREFFECT = 1; const int SHADOWEFFECT = 2; const int TYPEWRITEREFFECT = 3; void TitleWidget::refreshTemplateBoxContents() { templateBox->clear(); templateBox->addItem(QString()); for (const TitleTemplate &t : titletemplates) { templateBox->addItem(t.icon, t.name, t.file); } } -TitleWidget::TitleWidget(const QUrl &url, const Timecode &tc, const QString &projectTitlePath, Monitor *monitor, QWidget *parent) +TitleWidget::TitleWidget(const QUrl &url, const Timecode &tc, QString projectTitlePath, Monitor *monitor, QWidget *parent) : QDialog(parent) , Ui::TitleWidget_UI() , m_startViewport(nullptr) , m_endViewport(nullptr) , m_count(0) , m_unicodeDialog(new UnicodeDialog(UnicodeDialog::InputHex)) , m_missingMessage(nullptr) - , m_projectTitlePath(projectTitlePath) + , m_projectTitlePath(std::move(projectTitlePath)) , m_tc(tc) , m_fps(monitor->fps()) , m_guides(QList()) { setupUi(this); setMinimumSize(200, 200); setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)); frame_properties->setEnabled(false); frame_properties->setFixedHeight(frame_toolbar->height()); int size = style()->pixelMetric(QStyle::PM_SmallIconSize); QSize iconSize(size, size); rectBColor->setAlphaChannelEnabled(true); rectFColor->setAlphaChannelEnabled(true); fontColorButton->setAlphaChannelEnabled(true); textOutlineColor->setAlphaChannelEnabled(true); shadowColor->setAlphaChannelEnabled(true); auto *colorGroup = new QButtonGroup(this); colorGroup->addButton(gradient_color); colorGroup->addButton(plain_color); auto *alignGroup = new QButtonGroup(this); alignGroup->addButton(buttonAlignLeft); alignGroup->addButton(buttonAlignCenter); alignGroup->addButton(buttonAlignRight); textOutline->setMinimum(0); textOutline->setMaximum(200); // textOutline->setDecimals(0); textOutline->setValue(0); textOutline->setToolTip(i18n("Outline width")); backgroundAlpha->setMinimum(0); backgroundAlpha->setMaximum(255); bgAlphaSlider->setMinimum(0); bgAlphaSlider->setMaximum(255); backgroundAlpha->setValue(0); backgroundAlpha->setToolTip(i18n("Background color opacity")); itemrotatex->setMinimum(-360); itemrotatex->setMaximum(360); // itemrotatex->setDecimals(0); itemrotatex->setValue(0); itemrotatex->setToolTip(i18n("Rotation around the X axis")); itemrotatey->setMinimum(-360); itemrotatey->setMaximum(360); // itemrotatey->setDecimals(0); itemrotatey->setValue(0); itemrotatey->setToolTip(i18n("Rotation around the Y axis")); itemrotatez->setMinimum(-360); itemrotatez->setMaximum(360); // itemrotatez->setDecimals(0); itemrotatez->setValue(0); itemrotatez->setToolTip(i18n("Rotation around the Z axis")); rectLineWidth->setMinimum(0); rectLineWidth->setMaximum(500); // rectLineWidth->setDecimals(0); rectLineWidth->setValue(0); rectLineWidth->setToolTip(i18n("Border width")); itemzoom->setSuffix(i18n("%")); QSize profileSize = monitor->profileSize(); m_frameWidth = (int)(profileSize.height() * monitor->profile()->dar() + 0.5); m_frameHeight = profileSize.height(); showToolbars(TITLE_SELECT); splitter->setStretchFactor(0, 20); // If project is drop frame, set the input mask as such. title_duration->setInputMask(m_tc.mask()); title_duration->setText(m_tc.reformatSeparators(KdenliveSettings::title_duration())); connect(backgroundColor, &KColorButton::changed, this, &TitleWidget::slotChangeBackground); connect(backgroundAlpha, static_cast(&QSpinBox::valueChanged), this, &TitleWidget::slotChangeBackground); connect(shadowBox, &QGroupBox::toggled, this, &TitleWidget::slotUpdateShadow); connect(shadowColor, &KColorButton::changed, this, &TitleWidget::slotUpdateShadow); connect(blur_radius, static_cast(&QSpinBox::valueChanged), this, &TitleWidget::slotUpdateShadow); connect(shadowX, static_cast(&QSpinBox::valueChanged), this, &TitleWidget::slotUpdateShadow); connect(shadowY, static_cast(&QSpinBox::valueChanged), this, &TitleWidget::slotUpdateShadow); connect(fontColorButton, &KColorButton::changed, this, &TitleWidget::slotUpdateText); connect(plain_color, &QAbstractButton::clicked, this, &TitleWidget::slotUpdateText); connect(gradient_color, &QAbstractButton::clicked, this, &TitleWidget::slotUpdateText); connect(gradients_combo, SIGNAL(currentIndexChanged(int)), this, SLOT(slotUpdateText())); connect(textOutlineColor, &KColorButton::changed, this, &TitleWidget::slotUpdateText); connect(font_family, &QFontComboBox::currentFontChanged, this, &TitleWidget::slotUpdateText); connect(font_size, static_cast(&QSpinBox::valueChanged), this, &TitleWidget::slotUpdateText); connect(letter_spacing, static_cast(&QSpinBox::valueChanged), this, &TitleWidget::slotUpdateText); connect(line_spacing, static_cast(&QSpinBox::valueChanged), this, &TitleWidget::slotUpdateText); connect(textOutline, static_cast(&QSpinBox::valueChanged), this, &TitleWidget::slotUpdateText); connect(font_weight_box, SIGNAL(currentIndexChanged(int)), this, SLOT(slotUpdateText())); connect(rectFColor, &KColorButton::changed, this, &TitleWidget::rectChanged); connect(rectBColor, &KColorButton::changed, this, &TitleWidget::rectChanged); connect(plain_rect, &QAbstractButton::clicked, this, &TitleWidget::rectChanged); connect(gradient_rect, &QAbstractButton::clicked, this, &TitleWidget::rectChanged); connect(gradients_rect_combo, SIGNAL(currentIndexChanged(int)), this, SLOT(rectChanged())); connect(rectLineWidth, static_cast(&QSpinBox::valueChanged), this, &TitleWidget::rectChanged); connect(zValue, static_cast(&QSpinBox::valueChanged), this, &TitleWidget::zIndexChanged); connect(itemzoom, static_cast(&QSpinBox::valueChanged), this, &TitleWidget::itemScaled); connect(itemrotatex, static_cast(&QSpinBox::valueChanged), this, &TitleWidget::itemRotateX); connect(itemrotatey, static_cast(&QSpinBox::valueChanged), this, &TitleWidget::itemRotateY); connect(itemrotatez, static_cast(&QSpinBox::valueChanged), this, &TitleWidget::itemRotateZ); connect(itemhcenter, &QAbstractButton::clicked, this, &TitleWidget::itemHCenter); connect(itemvcenter, &QAbstractButton::clicked, this, &TitleWidget::itemVCenter); connect(itemtop, &QAbstractButton::clicked, this, &TitleWidget::itemTop); connect(itembottom, &QAbstractButton::clicked, this, &TitleWidget::itemBottom); connect(itemleft, &QAbstractButton::clicked, this, &TitleWidget::itemLeft); connect(itemright, &QAbstractButton::clicked, this, &TitleWidget::itemRight); connect(effect_list, SIGNAL(currentIndexChanged(int)), this, SLOT(slotAddEffect(int))); connect(typewriter_delay, static_cast(&QSpinBox::valueChanged), this, &TitleWidget::slotEditTypewriter); connect(typewriter_start, static_cast(&QSpinBox::valueChanged), this, &TitleWidget::slotEditTypewriter); connect(origin_x_left, &QAbstractButton::clicked, this, &TitleWidget::slotOriginXClicked); connect(origin_y_top, &QAbstractButton::clicked, this, &TitleWidget::slotOriginYClicked); connect(monitor, &Monitor::frameUpdated, this, &TitleWidget::slotGotBackground); // Position and size m_signalMapper = new QSignalMapper(this); m_signalMapper->setMapping(value_w, ValueWidth); m_signalMapper->setMapping(value_h, ValueHeight); m_signalMapper->setMapping(value_x, ValueX); m_signalMapper->setMapping(value_y, ValueY); connect(value_w, static_cast(&QSpinBox::valueChanged), m_signalMapper, static_cast(&QSignalMapper::map)); connect(value_h, static_cast(&QSpinBox::valueChanged), m_signalMapper, static_cast(&QSignalMapper::map)); connect(value_x, static_cast(&QSpinBox::valueChanged), m_signalMapper, static_cast(&QSignalMapper::map)); connect(value_y, static_cast(&QSpinBox::valueChanged), m_signalMapper, static_cast(&QSignalMapper::map)); connect(m_signalMapper, SIGNAL(mapped(int)), this, SLOT(slotValueChanged(int))); connect(buttonFitZoom, &QAbstractButton::clicked, this, &TitleWidget::slotAdjustZoom); connect(buttonRealSize, &QAbstractButton::clicked, this, &TitleWidget::slotZoomOneToOne); connect(buttonItalic, &QAbstractButton::clicked, this, &TitleWidget::slotUpdateText); connect(buttonUnder, &QAbstractButton::clicked, this, &TitleWidget::slotUpdateText); connect(buttonAlignLeft, &QAbstractButton::clicked, this, &TitleWidget::slotUpdateText); connect(buttonAlignRight, &QAbstractButton::clicked, this, &TitleWidget::slotUpdateText); connect(buttonAlignCenter, &QAbstractButton::clicked, this, &TitleWidget::slotUpdateText); connect(edit_gradient, &QAbstractButton::clicked, this, &TitleWidget::slotEditGradient); connect(edit_rect_gradient, &QAbstractButton::clicked, this, &TitleWidget::slotEditGradient); connect(displayBg, &QCheckBox::stateChanged, this, &TitleWidget::displayBackgroundFrame); connect(m_unicodeDialog, &UnicodeDialog::charSelected, this, &TitleWidget::slotInsertUnicodeString); // mbd connect(this, &QDialog::accepted, this, &TitleWidget::slotAccepted); font_weight_box->blockSignals(true); font_weight_box->addItem(i18nc("Font style", "Light"), QFont::Light); font_weight_box->addItem(i18nc("Font style", "Normal"), QFont::Normal); font_weight_box->addItem(i18nc("Font style", "Demi-Bold"), QFont::DemiBold); font_weight_box->addItem(i18nc("Font style", "Bold"), QFont::Bold); font_weight_box->addItem(i18nc("Font style", "Black"), QFont::Black); font_weight_box->setToolTip(i18n("Font weight")); font_weight_box->setCurrentIndex(1); font_weight_box->blockSignals(false); buttonFitZoom->setIconSize(iconSize); buttonRealSize->setIconSize(iconSize); buttonItalic->setIconSize(iconSize); buttonUnder->setIconSize(iconSize); buttonAlignCenter->setIconSize(iconSize); buttonAlignLeft->setIconSize(iconSize); buttonAlignRight->setIconSize(iconSize); buttonFitZoom->setIcon(QIcon::fromTheme(QStringLiteral("zoom-fit-best"))); buttonRealSize->setIcon(QIcon::fromTheme(QStringLiteral("zoom-original"))); buttonItalic->setIcon(QIcon::fromTheme(QStringLiteral("format-text-italic"))); buttonUnder->setIcon(QIcon::fromTheme(QStringLiteral("format-text-underline"))); buttonAlignCenter->setIcon(QIcon::fromTheme(QStringLiteral("format-justify-center"))); buttonAlignLeft->setIcon(QIcon::fromTheme(QStringLiteral("format-justify-left"))); buttonAlignRight->setIcon(QIcon::fromTheme(QStringLiteral("format-justify-right"))); edit_gradient->setIcon(QIcon::fromTheme(QStringLiteral("document-edit"))); edit_rect_gradient->setIcon(QIcon::fromTheme(QStringLiteral("document-edit"))); buttonAlignRight->setToolTip(i18n("Align right")); buttonAlignLeft->setToolTip(i18n("Align left")); buttonAlignCenter->setToolTip(i18n("Align center")); buttonAlignLeft->setChecked(true); m_unicodeAction = new QAction(QIcon::fromTheme(QStringLiteral("kdenlive-insert-unicode")), QString(), this); m_unicodeAction->setShortcut(Qt::SHIFT + Qt::CTRL + Qt::Key_U); m_unicodeAction->setToolTip(getTooltipWithShortcut(i18n("Insert Unicode character"), m_unicodeAction)); connect(m_unicodeAction, &QAction::triggered, this, &TitleWidget::slotInsertUnicode); buttonInsertUnicode->setDefaultAction(m_unicodeAction); m_zUp = new QAction(QIcon::fromTheme(QStringLiteral("kdenlive-zindex-up")), QString(), this); m_zUp->setShortcut(Qt::Key_PageUp); m_zUp->setToolTip(i18n("Raise object")); connect(m_zUp, &QAction::triggered, this, &TitleWidget::slotZIndexUp); zUp->setDefaultAction(m_zUp); m_zDown = new QAction(QIcon::fromTheme(QStringLiteral("kdenlive-zindex-down")), QString(), this); m_zDown->setShortcut(Qt::Key_PageDown); m_zDown->setToolTip(i18n("Lower object")); connect(m_zDown, &QAction::triggered, this, &TitleWidget::slotZIndexDown); zDown->setDefaultAction(m_zDown); m_zTop = new QAction(QIcon::fromTheme(QStringLiteral("kdenlive-zindex-top")), QString(), this); // TODO mbt 1414: Shortcut should change z index only if // cursor is NOT in a text field ... // m_zTop->setShortcut(Qt::Key_Home); m_zTop->setToolTip(i18n("Raise object to top")); connect(m_zTop, &QAction::triggered, this, &TitleWidget::slotZIndexTop); zTop->setDefaultAction(m_zTop); m_zBottom = new QAction(QIcon::fromTheme(QStringLiteral("kdenlive-zindex-bottom")), QString(), this); // TODO mbt 1414 // m_zBottom->setShortcut(Qt::Key_End); m_zBottom->setToolTip(i18n("Lower object to bottom")); connect(m_zBottom, &QAction::triggered, this, &TitleWidget::slotZIndexBottom); zBottom->setDefaultAction(m_zBottom); m_selectAll = new QAction(QIcon::fromTheme(QStringLiteral("kdenlive-select-all")), QString(), this); m_selectAll->setShortcut(Qt::CTRL + Qt::Key_A); connect(m_selectAll, &QAction::triggered, this, &TitleWidget::slotSelectAll); buttonSelectAll->setDefaultAction(m_selectAll); m_selectText = new QAction(QIcon::fromTheme(QStringLiteral("kdenlive-select-texts")), QString(), this); m_selectText->setShortcut(Qt::CTRL + Qt::Key_T); connect(m_selectText, &QAction::triggered, this, &TitleWidget::slotSelectText); buttonSelectText->setDefaultAction(m_selectText); buttonSelectText->setEnabled(false); m_selectRects = new QAction(QIcon::fromTheme(QStringLiteral("kdenlive-select-rects")), QString(), this); m_selectRects->setShortcut(Qt::CTRL + Qt::Key_R); connect(m_selectRects, &QAction::triggered, this, &TitleWidget::slotSelectRects); buttonSelectRects->setDefaultAction(m_selectRects); buttonSelectRects->setEnabled(false); m_selectImages = new QAction(QIcon::fromTheme(QStringLiteral("kdenlive-select-images")), QString(), this); m_selectImages->setShortcut(Qt::CTRL + Qt::Key_I); connect(m_selectImages, &QAction::triggered, this, &TitleWidget::slotSelectImages); buttonSelectImages->setDefaultAction(m_selectImages); buttonSelectImages->setEnabled(false); m_unselectAll = new QAction(QIcon::fromTheme(QStringLiteral("kdenlive-unselect-all")), QString(), this); m_unselectAll->setShortcut(Qt::SHIFT + Qt::CTRL + Qt::Key_A); connect(m_unselectAll, &QAction::triggered, this, &TitleWidget::slotSelectNone); buttonUnselectAll->setDefaultAction(m_unselectAll); buttonUnselectAll->setEnabled(false); zDown->setIconSize(iconSize); zTop->setIconSize(iconSize); zBottom->setIconSize(iconSize); zDown->setIcon(QIcon::fromTheme(QStringLiteral("kdenlive-zindex-down"))); zTop->setIcon(QIcon::fromTheme(QStringLiteral("kdenlive-zindex-top"))); zBottom->setIcon(QIcon::fromTheme(QStringLiteral("kdenlive-zindex-bottom"))); connect(zDown, &QAbstractButton::clicked, this, &TitleWidget::slotZIndexDown); connect(zTop, &QAbstractButton::clicked, this, &TitleWidget::slotZIndexTop); connect(zBottom, &QAbstractButton::clicked, this, &TitleWidget::slotZIndexBottom); origin_x_left->setToolTip(i18n("Invert x axis and change 0 point")); origin_y_top->setToolTip(i18n("Invert y axis and change 0 point")); rectBColor->setToolTip(i18n("Select fill color")); rectFColor->setToolTip(i18n("Select border color")); zoom_slider->setToolTip(i18n("Zoom")); buttonRealSize->setToolTip(i18n("Original size (1:1)")); buttonFitZoom->setToolTip(i18n("Fit zoom")); backgroundColor->setToolTip(i18n("Select background color")); backgroundAlpha->setToolTip(i18n("Background opacity")); buttonSelectAll->setToolTip(getTooltipWithShortcut(i18n("Select all"), m_selectAll)); buttonSelectText->setToolTip(getTooltipWithShortcut(i18n("Select text items in current selection"), m_selectText)); buttonSelectRects->setToolTip(getTooltipWithShortcut(i18n("Select rect items in current selection"), m_selectRects)); buttonSelectImages->setToolTip(getTooltipWithShortcut(i18n("Select image items in current selection"), m_selectImages)); buttonUnselectAll->setToolTip(getTooltipWithShortcut(i18n("Unselect all"), m_unselectAll)); itemhcenter->setIconSize(iconSize); itemvcenter->setIconSize(iconSize); itemtop->setIconSize(iconSize); itembottom->setIconSize(iconSize); itemright->setIconSize(iconSize); itemleft->setIconSize(iconSize); itemhcenter->setIcon(QIcon::fromTheme(QStringLiteral("kdenlive-align-hor"))); itemhcenter->setToolTip(i18n("Align item horizontally")); itemvcenter->setIcon(QIcon::fromTheme(QStringLiteral("kdenlive-align-vert"))); itemvcenter->setToolTip(i18n("Align item vertically")); itemtop->setIcon(QIcon::fromTheme(QStringLiteral("kdenlive-align-top"))); itemtop->setToolTip(i18n("Align item to top")); itembottom->setIcon(QIcon::fromTheme(QStringLiteral("kdenlive-align-bottom"))); itembottom->setToolTip(i18n("Align item to bottom")); itemright->setIcon(QIcon::fromTheme(QStringLiteral("kdenlive-align-right"))); itemright->setToolTip(i18n("Align item to right")); itemleft->setIcon(QIcon::fromTheme(QStringLiteral("kdenlive-align-left"))); itemleft->setToolTip(i18n("Align item to left")); auto *layout = new QHBoxLayout; frame_toolbar->setLayout(layout); layout->setContentsMargins(0, 0, 0, 0); QToolBar *m_toolbar = new QToolBar(QStringLiteral("titleToolBar"), this); m_toolbar->setIconSize(iconSize); m_buttonCursor = m_toolbar->addAction(QIcon::fromTheme(QStringLiteral("transform-move")), i18n("Selection Tool")); m_buttonCursor->setCheckable(true); m_buttonCursor->setShortcut(Qt::ALT + Qt::Key_S); m_buttonCursor->setToolTip(i18n("Selection Tool") + QLatin1Char(' ') + m_buttonCursor->shortcut().toString()); connect(m_buttonCursor, &QAction::triggered, this, &TitleWidget::slotSelectTool); m_buttonText = m_toolbar->addAction(QIcon::fromTheme(QStringLiteral("insert-text")), i18n("Add Text")); m_buttonText->setCheckable(true); m_buttonText->setShortcut(Qt::ALT + Qt::Key_T); m_buttonText->setToolTip(i18n("Add Text") + QLatin1Char(' ') + m_buttonText->shortcut().toString()); connect(m_buttonText, &QAction::triggered, this, &TitleWidget::slotTextTool); m_buttonRect = m_toolbar->addAction(QIcon::fromTheme(QStringLiteral("kdenlive-insert-rect")), i18n("Add Rectangle")); m_buttonRect->setCheckable(true); m_buttonRect->setShortcut(Qt::ALT + Qt::Key_R); m_buttonRect->setToolTip(i18n("Add Rectangle") + QLatin1Char(' ') + m_buttonRect->shortcut().toString()); connect(m_buttonRect, &QAction::triggered, this, &TitleWidget::slotRectTool); m_buttonImage = m_toolbar->addAction(QIcon::fromTheme(QStringLiteral("insert-image")), i18n("Add Image")); m_buttonImage->setCheckable(false); m_buttonImage->setShortcut(Qt::ALT + Qt::Key_I); m_buttonImage->setToolTip(i18n("Add Image") + QLatin1Char(' ') + m_buttonImage->shortcut().toString()); connect(m_buttonImage, &QAction::triggered, this, &TitleWidget::slotImageTool); m_toolbar->addSeparator(); m_buttonLoad = m_toolbar->addAction(QIcon::fromTheme(QStringLiteral("document-open")), i18n("Open Document")); m_buttonLoad->setCheckable(false); m_buttonLoad->setShortcut(Qt::CTRL + Qt::Key_O); m_buttonLoad->setToolTip(i18n("Open Document") + QLatin1Char(' ') + m_buttonLoad->shortcut().toString()); connect(m_buttonLoad, SIGNAL(triggered()), this, SLOT(loadTitle())); m_buttonSave = m_toolbar->addAction(QIcon::fromTheme(QStringLiteral("document-save-as")), i18n("Save As")); m_buttonSave->setCheckable(false); m_buttonSave->setShortcut(Qt::CTRL + Qt::Key_S); m_buttonSave->setToolTip(i18n("Save As") + QLatin1Char(' ') + m_buttonSave->shortcut().toString()); connect(m_buttonSave, SIGNAL(triggered()), this, SLOT(saveTitle())); m_buttonDownload = m_toolbar->addAction(QIcon::fromTheme(QStringLiteral("edit-download")), i18n("Download New Title Templates...")); m_buttonDownload->setCheckable(false); m_buttonDownload->setShortcut(Qt::ALT + Qt::Key_D); m_buttonDownload->setToolTip(i18n("Download New Title Templates...") + QLatin1Char(' ') + m_buttonDownload->shortcut().toString()); connect(m_buttonDownload, &QAction::triggered, this, &TitleWidget::downloadTitleTemplates); layout->addWidget(m_toolbar); // initialize graphic scene m_scene = new GraphicsSceneRectMove(this); graphicsView->setScene(m_scene); graphicsView->setMouseTracking(true); graphicsView->setViewportUpdateMode(QGraphicsView::FullViewportUpdate); graphicsView->setDragMode(QGraphicsView::RubberBandDrag); graphicsView->setRubberBandSelectionMode(Qt::ContainsItemBoundingRect); m_titledocument.setScene(m_scene, m_frameWidth, m_frameHeight); connect(m_scene, &QGraphicsScene::changed, this, &TitleWidget::slotChanged); connect(font_size, static_cast(&QSpinBox::valueChanged), m_scene, &GraphicsSceneRectMove::slotUpdateFontSize); connect(use_grid, &QAbstractButton::toggled, m_scene, &GraphicsSceneRectMove::slotUseGrid); // Video frame rect QPen framepen; framepen.setColor(Qt::red); m_frameBorder = new QGraphicsRectItem(QRectF(0, 0, m_frameWidth, m_frameHeight)); m_frameBorder->setPen(framepen); m_frameBorder->setZValue(1000); m_frameBorder->setBrush(Qt::transparent); m_frameBorder->setFlags(nullptr); m_frameBorder->setData(-1, -1); graphicsView->scene()->addItem(m_frameBorder); // Guides connect(show_guides, &QCheckBox::stateChanged, this, &TitleWidget::showGuides); show_guides->setChecked(KdenliveSettings::titlerShowGuides()); hguides->setValue(KdenliveSettings::titlerHGuides()); vguides->setValue(KdenliveSettings::titlerVGuides()); guideColor->setColor(KdenliveSettings::titleGuideColor()); connect(guideColor, &KColorButton::changed, this, &TitleWidget::guideColorChanged); connect(hguides, static_cast(&QSpinBox::valueChanged), this, &TitleWidget::updateGuides); connect(vguides, static_cast(&QSpinBox::valueChanged), this, &TitleWidget::updateGuides); updateGuides(0); // semi transparent safe zones framepen.setColor(QColor(255, 0, 0, 100)); QGraphicsRectItem *safe1 = new QGraphicsRectItem(QRectF(m_frameWidth * 0.05, m_frameHeight * 0.05, m_frameWidth * 0.9, m_frameHeight * 0.9), m_frameBorder); safe1->setBrush(Qt::transparent); safe1->setPen(framepen); safe1->setFlags(nullptr); safe1->setData(-1, -1); QGraphicsRectItem *safe2 = new QGraphicsRectItem(QRectF(m_frameWidth * 0.1, m_frameHeight * 0.1, m_frameWidth * 0.8, m_frameHeight * 0.8), m_frameBorder); safe2->setBrush(Qt::transparent); safe2->setPen(framepen); safe2->setFlags(nullptr); safe2->setData(-1, -1); m_frameBackground = new QGraphicsRectItem(QRectF(0, 0, m_frameWidth, m_frameHeight)); m_frameBackground->setZValue(-1100); m_frameBackground->setBrush(Qt::transparent); m_frameBackground->setFlags(nullptr); graphicsView->scene()->addItem(m_frameBackground); m_frameImage = new QGraphicsPixmapItem(); QTransform qtrans; qtrans.scale(2.0, 2.0); m_frameImage->setTransform(qtrans); m_frameImage->setZValue(-1200); m_frameImage->setFlags(nullptr); displayBackgroundFrame(); graphicsView->scene()->addItem(m_frameImage); connect(m_scene, &QGraphicsScene::selectionChanged, this, &TitleWidget::selectionChanged); connect(m_scene, &GraphicsSceneRectMove::itemMoved, this, &TitleWidget::selectionChanged); connect(m_scene, &GraphicsSceneRectMove::sceneZoom, this, &TitleWidget::slotZoom); connect(m_scene, &GraphicsSceneRectMove::actionFinished, this, &TitleWidget::slotSelectTool); connect(m_scene, &GraphicsSceneRectMove::newRect, this, &TitleWidget::slotNewRect); connect(m_scene, &GraphicsSceneRectMove::newText, this, &TitleWidget::slotNewText); connect(zoom_slider, &QAbstractSlider::valueChanged, this, &TitleWidget::slotUpdateZoom); connect(zoom_spin, static_cast(&QSpinBox::valueChanged), this, &TitleWidget::slotUpdateZoom); // mbd: load saved settings loadGradients(); readChoices(); // Hide effects not implemented tabWidget->removeTab(3); graphicsView->show(); graphicsView->setInteractive(true); // qCDebug(KDENLIVE_LOG) << "// TITLE WIDGWT: " << graphicsView->viewport()->width() << 'x' << graphicsView->viewport()->height(); m_startViewport = new QGraphicsRectItem(QRectF(0, 0, m_frameWidth, m_frameHeight)); // Setting data at -1 so that the item is recognized as undeletable by graphicsscenerectmove m_startViewport->setData(-1, -1); m_endViewport = new QGraphicsRectItem(QRectF(0, 0, m_frameWidth, m_frameHeight)); m_endViewport->setData(-1, -1); m_startViewport->setData(0, m_frameWidth); m_startViewport->setData(1, m_frameHeight); m_endViewport->setData(0, m_frameWidth); m_endViewport->setData(1, m_frameHeight); // scale the view so that the title widget is not too big at startup graphicsView->scale(.5, .5); if (url.isValid()) { loadTitle(url); } else { prepareTools(nullptr); slotTextTool(); QTimer::singleShot(200, this, &TitleWidget::slotAdjustZoom); } initAnimation(); QColor color = backgroundColor->color(); m_scene->setBackgroundBrush(QBrush(color)); color.setAlpha(backgroundAlpha->value()); m_frameBackground->setBrush(color); connect(anim_start, &QAbstractButton::toggled, this, &TitleWidget::slotAnimStart); connect(anim_end, &QAbstractButton::toggled, this, &TitleWidget::slotAnimEnd); connect(templateBox, SIGNAL(currentIndexChanged(int)), this, SLOT(templateIndexChanged(int))); buttonBox->button(QDialogButtonBox::Ok)->setEnabled(KdenliveSettings::hastitleproducer()); if (titletemplates.isEmpty()) { refreshTitleTemplates(m_projectTitlePath); } // templateBox->setIconSize(QSize(60,60)); refreshTemplateBoxContents(); m_lastDocumentHash = QCryptographicHash::hash(xml().toString().toLatin1(), QCryptographicHash::Md5).toHex(); } TitleWidget::~TitleWidget() { m_scene->blockSignals(true); delete m_buttonRect; delete m_buttonText; delete m_buttonImage; delete m_buttonCursor; delete m_buttonSave; delete m_buttonLoad; delete m_unicodeAction; delete m_zUp; delete m_zDown; delete m_zTop; delete m_zBottom; delete m_selectAll; delete m_selectText; delete m_selectRects; delete m_selectImages; delete m_unselectAll; delete m_unicodeDialog; delete m_frameBorder; delete m_frameImage; delete m_startViewport; delete m_endViewport; delete m_scene; delete m_signalMapper; } // static QStringList TitleWidget::extractImageList(const QString &xml) { QStringList result; if (xml.isEmpty()) { return result; } QDomDocument doc; doc.setContent(xml); QDomNodeList images = doc.elementsByTagName(QStringLiteral("content")); for (int i = 0; i < images.count(); ++i) { if (images.at(i).toElement().hasAttribute(QStringLiteral("url"))) { result.append(images.at(i).toElement().attribute(QStringLiteral("url"))); } } return result; } // static QStringList TitleWidget::extractFontList(const QString &xml) { QStringList result; if (xml.isEmpty()) { return result; } QDomDocument doc; doc.setContent(xml); QDomNodeList images = doc.elementsByTagName(QStringLiteral("content")); for (int i = 0; i < images.count(); ++i) { if (images.at(i).toElement().hasAttribute(QStringLiteral("font"))) { result.append(images.at(i).toElement().attribute(QStringLiteral("font"))); } } return result; } // static void TitleWidget::refreshTitleTemplates(const QString &projectPath) { QStringList filters = QStringList() << QStringLiteral("*.kdenlivetitle"); titletemplates.clear(); // project templates QDir dir(projectPath); QStringList templateFiles = dir.entryList(filters, QDir::Files); for (const QString &fname : templateFiles) { TitleTemplate t; t.name = fname; t.file = dir.absoluteFilePath(fname); t.icon = QIcon(KThumb::getImage(QUrl::fromLocalFile(t.file), 0, 60, -1)); titletemplates.append(t); } // system templates QStringList titleTemplates = QStandardPaths::locateAll(QStandardPaths::AppDataLocation, QStringLiteral("titles/"), QStandardPaths::LocateDirectory); for (const QString &folderpath : titleTemplates) { QDir folder(folderpath); QStringList filesnames = folder.entryList(filters, QDir::Files); for (const QString &fname : filesnames) { TitleTemplate t; t.name = fname; t.file = folder.absoluteFilePath(fname); t.icon = QIcon(KThumb::getImage(QUrl::fromLocalFile(t.file), 0, 60, -1)); titletemplates.append(t); } } } void TitleWidget::templateIndexChanged(int index) { QString item = templateBox->itemData(index).toString(); if (!item.isEmpty()) { if (m_lastDocumentHash != QCryptographicHash::hash(xml().toString().toLatin1(), QCryptographicHash::Md5).toHex()) { if (KMessageBox::questionYesNo(this, i18n("Do you really want to load a new template? Changes in this title will be lost!")) == KMessageBox::No) { return; } } loadTitle(QUrl::fromLocalFile(item)); // mbt 1607: Add property to distinguish between unchanged template titles and user titles. // Text of unchanged template titles should be selected when clicked. QList list = graphicsView->scene()->items(); for (QGraphicsItem *qgItem : list) { if (qgItem->type() == TEXTITEM) { - MyTextItem *i = static_cast(qgItem); + auto *i = static_cast(qgItem); i->setProperty("isTemplate", "true"); i->setProperty("templateText", i->toHtml()); } } m_lastDocumentHash = QCryptographicHash::hash(xml().toString().toLatin1(), QCryptographicHash::Md5).toHex(); } } // virtual void TitleWidget::resizeEvent(QResizeEvent * /*event*/) { // slotAdjustZoom(); } // virtual void TitleWidget::keyPressEvent(QKeyEvent *e) { if (e->key() != Qt::Key_Escape && e->key() != Qt::Key_Return && e->key() != Qt::Key_Enter) { QDialog::keyPressEvent(e); } } void TitleWidget::slotTextTool() { m_scene->setTool(TITLE_TEXT); showToolbars(TITLE_TEXT); checkButton(TITLE_TEXT); } void TitleWidget::slotRectTool() { m_scene->setTool(TITLE_RECTANGLE); showToolbars(TITLE_RECTANGLE); checkButton(TITLE_RECTANGLE); // Disable dragging mode, would make dragging a rect impossible otherwise ;) graphicsView->setDragMode(QGraphicsView::NoDrag); } void TitleWidget::slotSelectTool() { m_scene->setTool(TITLE_SELECT); // Enable rubberband selecting mode. graphicsView->setDragMode(QGraphicsView::RubberBandDrag); // Find out which toolbars need to be shown, depending on selected item TITLETOOL t = TITLE_SELECT; QList l = graphicsView->scene()->selectedItems(); if (!l.isEmpty()) { switch (l.at(0)->type()) { case TEXTITEM: t = TITLE_TEXT; break; case RECTITEM: t = TITLE_RECTANGLE; break; case IMAGEITEM: t = TITLE_IMAGE; break; } } enableToolbars(t); if (t == TITLE_RECTANGLE && (l.at(0) == m_endViewport || l.at(0) == m_startViewport)) { // graphicsView->centerOn(l.at(0)); t = TITLE_SELECT; } showToolbars(t); if (!l.isEmpty()) { updateCoordinates(l.at(0)); updateDimension(l.at(0)); updateRotZoom(l.at(0)); } checkButton(TITLE_SELECT); } void TitleWidget::slotImageTool() { QList supported = QImageReader::supportedImageFormats(); QStringList mimeTypeFilters; QString allExtensions = i18n("All Images") + QStringLiteral(" ("); for (const QByteArray &mimeType : supported) { mimeTypeFilters.append(i18n("%1 Image", QString(mimeType)) + QStringLiteral("( *.") + QString(mimeType) + QLatin1Char(')')); allExtensions.append(QStringLiteral("*.") + mimeType + QLatin1Char(' ')); } mimeTypeFilters.sort(); allExtensions.append(QLatin1Char(')')); mimeTypeFilters.prepend(allExtensions); QString clipFolder = KRecentDirs::dir(QStringLiteral(":KdenliveImageFolder")); if (clipFolder.isEmpty()) { clipFolder = QDir::homePath(); } QFileDialog dialog(this, i18n("Add Image"), clipFolder); dialog.setAcceptMode(QFileDialog::AcceptOpen); dialog.setNameFilters(mimeTypeFilters); if (dialog.exec() != QDialog::Accepted) { return; } QUrl url = QUrl::fromLocalFile(dialog.selectedFiles().at(0)); if (url.isValid()) { KRecentDirs::add(QStringLiteral(":KdenliveImageFolder"), url.adjusted(QUrl::RemoveFilename).toLocalFile()); if (url.toLocalFile().endsWith(QLatin1String(".svg"))) { MySvgItem *svg = new MySvgItem(url.toLocalFile()); svg->setFlags(QGraphicsItem::ItemIsMovable | QGraphicsItem::ItemIsSelectable | QGraphicsItem::ItemSendsGeometryChanges); svg->setZValue(m_count++); svg->setData(Qt::UserRole, url.toLocalFile()); m_scene->addNewItem(svg); prepareTools(svg); } else { QPixmap pix(url.toLocalFile()); auto *image = new MyPixmapItem(pix); image->setShapeMode(QGraphicsPixmapItem::BoundingRectShape); image->setFlags(QGraphicsItem::ItemIsMovable | QGraphicsItem::ItemIsSelectable | QGraphicsItem::ItemSendsGeometryChanges); image->setData(Qt::UserRole, url.toLocalFile()); image->setZValue(m_count++); m_scene->addNewItem(image); prepareTools(image); } } m_scene->setTool(TITLE_SELECT); showToolbars(TITLE_SELECT); checkButton(TITLE_SELECT); } void TitleWidget::showToolbars(TITLETOOL toolType) { // toolbar_stack->setEnabled(toolType != TITLE_SELECT); switch (toolType) { case TITLE_IMAGE: toolbar_stack->setCurrentIndex(2); break; case TITLE_RECTANGLE: toolbar_stack->setCurrentIndex(1); break; case TITLE_TEXT: default: toolbar_stack->setCurrentIndex(0); break; } } void TitleWidget::enableToolbars(TITLETOOL toolType) { // TITLETOOL is defined in effectstack/graphicsscenerectmove.h bool enable = false; if (toolType == TITLE_RECTANGLE || toolType == TITLE_IMAGE) { enable = true; } value_w->setEnabled(enable); value_h->setEnabled(enable); } void TitleWidget::checkButton(TITLETOOL toolType) { bool bSelect = false; bool bText = false; bool bRect = false; bool bImage = false; switch (toolType) { case TITLE_SELECT: bSelect = true; break; case TITLE_TEXT: bText = true; break; case TITLE_RECTANGLE: bRect = true; break; case TITLE_IMAGE: bImage = true; break; default: break; } m_buttonCursor->setChecked(bSelect); m_buttonText->setChecked(bText); m_buttonRect->setChecked(bRect); m_buttonImage->setChecked(bImage); } void TitleWidget::displayBackgroundFrame() { QRectF r = m_frameBorder->sceneBoundingRect(); if (!displayBg->isChecked()) { QPixmap pattern(20, 20); pattern.fill(Qt::gray); QColor bgcolor(180, 180, 180); QPainter p(&pattern); p.fillRect(QRect(0, 0, 10, 10), bgcolor); p.fillRect(QRect(10, 10, 20, 20), bgcolor); p.end(); QBrush br(pattern); QPixmap bg((int)(r.width() / 2), (int)(r.height() / 2)); QPainter p2(&bg); p2.fillRect(bg.rect(), br); p2.end(); m_frameImage->setPixmap(bg); } else { emit requestBackgroundFrame(m_clipId, true); } } void TitleWidget::slotGotBackground(const QImage &img) { QRectF r = m_frameBorder->sceneBoundingRect(); m_frameImage->setPixmap(QPixmap::fromImage(img.scaled(r.width() / 2, r.height() / 2))); emit requestBackgroundFrame(m_clipId, false); } void TitleWidget::initAnimation() { align_box->setEnabled(false); QPen startpen(Qt::DotLine); QPen endpen(Qt::DashDotLine); startpen.setColor(QColor(100, 200, 100, 140)); endpen.setColor(QColor(200, 100, 100, 140)); m_startViewport->setPen(startpen); m_endViewport->setPen(endpen); m_startViewport->setZValue(-1000); m_endViewport->setZValue(-1000); m_startViewport->setFlags(nullptr); m_endViewport->setFlags(nullptr); graphicsView->scene()->addItem(m_startViewport); graphicsView->scene()->addItem(m_endViewport); connect(keep_aspect, &QAbstractButton::toggled, this, &TitleWidget::slotKeepAspect); connect(resize50, &QAbstractButton::clicked, this, &TitleWidget::slotResize50); connect(resize100, &QAbstractButton::clicked, this, &TitleWidget::slotResize100); connect(resize200, &QAbstractButton::clicked, this, &TitleWidget::slotResize200); } void TitleWidget::slotUpdateZoom(int pos) { zoom_spin->setValue(pos); zoom_slider->setValue(pos); m_scene->setZoom((double)pos / 100); } void TitleWidget::slotZoom(bool up) { int pos = zoom_slider->value(); if (up) { pos++; } else { pos--; } zoom_slider->setValue(pos); } void TitleWidget::slotAdjustZoom() { /*double scalex = graphicsView->width() / (double)(m_frameWidth * 1.2); double scaley = graphicsView->height() / (double)(m_frameHeight * 1.2); if (scalex > scaley) scalex = scaley; int zoompos = (int)(scalex * 7 + 0.5);*/ graphicsView->fitInView(m_frameBorder, Qt::KeepAspectRatio); int zoompos = graphicsView->matrix().m11() * 100; zoom_slider->setValue(zoompos); graphicsView->centerOn(m_frameBorder); } void TitleWidget::slotZoomOneToOne() { zoom_slider->setValue(100); graphicsView->centerOn(m_frameBorder); } void TitleWidget::slotNewRect(QGraphicsRectItem *rect) { updateAxisButtons(rect); // back to default if (rectLineWidth->value() == 0) { rect->setPen(Qt::NoPen); } else { QPen penf(rectFColor->color()); penf.setWidth(rectLineWidth->value()); penf.setJoinStyle(Qt::RoundJoin); rect->setPen(penf); } if (plain_rect->isChecked()) { rect->setBrush(QBrush(rectBColor->color())); rect->setData(TitleDocument::Gradient, QVariant()); } else { // gradient QString gradientData = gradients_rect_combo->currentData().toString(); rect->setData(TitleDocument::Gradient, gradientData); QLinearGradient gr = GradientWidget::gradientFromString(gradientData, rect->boundingRect().width(), rect->boundingRect().height()); rect->setBrush(QBrush(gr)); } rect->setZValue(m_count++); rect->setData(TitleDocument::ZoomFactor, 100); prepareTools(rect); // setCurrentItem(rect); // graphicsView->setFocus(); } void TitleWidget::slotNewText(MyTextItem *tt) { updateAxisButtons(tt); // back to default letter_spacing->blockSignals(true); line_spacing->blockSignals(true); letter_spacing->setValue(0); line_spacing->setValue(0); letter_spacing->blockSignals(false); line_spacing->blockSignals(false); letter_spacing->setEnabled(true); line_spacing->setEnabled(true); QFont font = font_family->currentFont(); font.setPixelSize(font_size->value()); // mbd: issue 551: font.setWeight(font_weight_box->itemData(font_weight_box->currentIndex()).toInt()); font.setItalic(buttonItalic->isChecked()); font.setUnderline(buttonUnder->isChecked()); tt->setFont(font); QColor color = fontColorButton->color(); QColor outlineColor = textOutlineColor->color(); tt->setTextColor(color); tt->document()->setDocumentMargin(0); QTextCursor cur(tt->document()); cur.select(QTextCursor::Document); QTextBlockFormat format = cur.blockFormat(); QTextCharFormat cformat = cur.charFormat(); double outlineWidth = textOutline->value() / 10.0; tt->setData(TitleDocument::OutlineWidth, outlineWidth); tt->setData(TitleDocument::OutlineColor, outlineColor); if (outlineWidth > 0.0) { cformat.setTextOutline(QPen(outlineColor, outlineWidth)); } tt->updateShadow(shadowBox->isChecked(), blur_radius->value(), shadowX->value(), shadowY->value(), shadowColor->color()); if (gradient_color->isChecked()) { QString gradientData = gradients_combo->currentData().toString(); tt->setData(TitleDocument::Gradient, gradientData); QLinearGradient gr = GradientWidget::gradientFromString(gradientData, tt->boundingRect().width(), tt->boundingRect().height()); cformat.setForeground(QBrush(gr)); } else { cformat.setForeground(QBrush(color)); } cur.setCharFormat(cformat); cur.setBlockFormat(format); tt->setTextCursor(cur); tt->setZValue(m_count++); setCurrentItem(tt); prepareTools(tt); } void TitleWidget::setFontBoxWeight(int weight) { int index = font_weight_box->findData(weight); if (index < 0) { index = font_weight_box->findData(QFont::Normal); } font_weight_box->setCurrentIndex(index); } void TitleWidget::setCurrentItem(QGraphicsItem *item) { m_scene->setSelectedItem(item); } void TitleWidget::zIndexChanged(int v) { QList l = graphicsView->scene()->selectedItems(); - for (int i = 0; i < l.size(); ++i) { - l[i]->setZValue(v); + for (auto &i : l) { + i->setZValue(v); } } void TitleWidget::selectionChanged() { if (m_scene->tool() != TITLE_SELECT) { return; } // qCDebug(KDENLIVE_LOG) << "Number of selected items: " << graphicsView->scene()->selectedItems().length() << '\n'; QList l; // mbt 1607: One text item might have grabbed the keyboard. // Ungrab it for all items that are not selected, otherwise // text input would only work for the text item that grabbed // the keyboard last. l = graphicsView->scene()->items(); for (QGraphicsItem *item : l) { if (item->type() == TEXTITEM && !item->isSelected()) { - MyTextItem *i = static_cast(item); + auto *i = static_cast(item); i->clearFocus(); } } l = graphicsView->scene()->selectedItems(); if (!l.isEmpty()) { buttonUnselectAll->setEnabled(true); // Enable all z index buttons if items selected. // We can selectively disable them later. zUp->setEnabled(true); zDown->setEnabled(true); zTop->setEnabled(true); zBottom->setEnabled(true); } else { buttonUnselectAll->setEnabled(false); } if (l.size() >= 2) { buttonSelectText->setEnabled(true); buttonSelectRects->setEnabled(true); buttonSelectImages->setEnabled(true); } else { buttonSelectText->setEnabled(false); buttonSelectRects->setEnabled(false); buttonSelectImages->setEnabled(false); } if (l.size() == 0) { prepareTools(nullptr); } else if (l.size() == 1) { prepareTools(l.at(0)); } else { /* For multiple selected objects we need to decide which tools to show. */ int firstType = l.at(0)->type(); bool allEqual = true; - for (int i = 0; i < l.size(); ++i) { - if (l.at(i)->type() != firstType) { + for (auto i : l) { + if (i->type() != firstType) { allEqual = false; break; } } // qCDebug(KDENLIVE_LOG) << "All equal? " << allEqual << ".\n"; if (allEqual) { prepareTools(l.at(0)); } else { // Get the default toolset, but enable the property frame (x,y,w,h) prepareTools(nullptr); frame_properties->setEnabled(true); // Enable x/y/w/h if it makes sense. value_x->setEnabled(true); value_y->setEnabled(true); bool containsTextitem = false; - for (int i = 0; i < l.size(); ++i) { - if (l.at(i)->type() == TEXTITEM) { + for (auto i : l) { + if (i->type() == TEXTITEM) { containsTextitem = true; break; } } if (!containsTextitem) { value_w->setEnabled(true); value_h->setEnabled(true); } } // Disable z index buttons if they don't make sense for the current selection int firstZindex = l.at(0)->zValue(); allEqual = true; - for (int i = 0; i < l.size(); ++i) { - if ((int)l[i]->zValue() != firstZindex) { + for (auto &i : l) { + if ((int)i->zValue() != firstZindex) { allEqual = false; break; } } if (!allEqual) { zUp->setEnabled(false); zDown->setEnabled(false); } } } void TitleWidget::slotValueChanged(int type) { /* type tells us which QSpinBox value has changed. */ QList l = graphicsView->scene()->selectedItems(); // qCDebug(KDENLIVE_LOG) << l.size() << " items to be resized\n"; // Get the updated value here already to do less coding afterwards int val = 0; switch (type) { case ValueWidth: val = value_w->value(); break; case ValueHeight: val = value_h->value(); break; case ValueX: val = value_x->value(); break; case ValueY: val = value_y->value(); break; } for (int k = 0; k < l.size(); ++k) { // qCDebug(KDENLIVE_LOG) << "Type of item " << k << ": " << l.at(k)->type() << '\n'; if (l.at(k)->type() == TEXTITEM) { // Just update the position. We don't allow setting width/height for text items yet. switch (type) { case ValueX: updatePosition(l.at(k), val, l.at(k)->pos().y()); break; case ValueY: updatePosition(l.at(k), l.at(k)->pos().x(), val); break; } } else if (l.at(k)->type() == RECTITEM) { - QGraphicsRectItem *rec = static_cast(l.at(k)); + auto *rec = static_cast(l.at(k)); switch (type) { case ValueX: updatePosition(l.at(k), val, l.at(k)->pos().y()); break; case ValueY: updatePosition(l.at(k), l.at(k)->pos().x(), val); break; case ValueWidth: rec->setRect(QRect(0, 0, val, rec->rect().height())); break; case ValueHeight: rec->setRect(QRect(0, 0, rec->rect().width(), val)); break; } } else if (l.at(k)->type() == IMAGEITEM) { if (type == ValueX) { updatePosition(l.at(k), val, l.at(k)->pos().y()); } else if (type == ValueY) { updatePosition(l.at(k), l.at(k)->pos().x(), val); } else { // Width/height has changed. This is more complex. QGraphicsItem *i = l.at(k); Transform t = m_transformations.value(i); // Ratio width:height double phi = (double)i->boundingRect().width() / i->boundingRect().height(); // TODO: proper calculation for rotation around 3 axes double alpha = (double)t.rotatez / 180.0 * M_PI; // New length double length; // Scaling factor double scale = 1; // We want to keep the aspect ratio of the image as the user does not yet have the possibility // to restore the original ratio. You rarely want to change it anyway. switch (type) { case ValueWidth: // Add 0.5 because otherwise incrementing by 1 might have no effect length = val / (cos(alpha) + 1 / phi * sin(alpha)) + 0.5; scale = length / i->boundingRect().width(); break; case ValueHeight: length = val / (phi * sin(alpha) + cos(alpha)) + 0.5; scale = length / i->boundingRect().height(); break; } t.scalex = scale; t.scaley = scale; QTransform qtrans; qtrans.scale(scale, scale); qtrans.rotate(t.rotatex, Qt::XAxis); qtrans.rotate(t.rotatey, Qt::YAxis); qtrans.rotate(t.rotatez, Qt::ZAxis); i->setTransform(qtrans); // qCDebug(KDENLIVE_LOG) << "scale is: " << scale << '\n'; // qCDebug(KDENLIVE_LOG) << i->boundingRect().width() << ": new width\n"; m_transformations[i] = t; if (l.size() == 1) { // Only update the w/h values if the selection contains just one item. // Otherwise, what should we do? ;) // (Use the values of the first item? Of the second? Of the x-th?) updateDimension(i); // Update rotation/zoom values. // These values are not yet able to handle multiple items! updateRotZoom(i); } } } } } void TitleWidget::updateDimension(QGraphicsItem *i) { bool wBlocked = value_w->signalsBlocked(); bool hBlocked = value_h->signalsBlocked(); bool zBlocked = zValue->signalsBlocked(); value_w->blockSignals(true); value_h->blockSignals(true); zValue->blockSignals(true); zValue->setValue((int)i->zValue()); if (i->type() == IMAGEITEM) { // Get multipliers for rotation/scaling /*Transform t = m_transformations.value(i); QRectF r = i->boundingRect(); int width = (int) ( abs(r.width()*t.scalex * cos(t.rotate/180.0*M_PI)) + abs(r.height()*t.scaley * sin(t.rotate/180.0*M_PI)) ); int height = (int) ( abs(r.height()*t.scaley * cos(t.rotate/180*M_PI)) + abs(r.width()*t.scalex * sin(t.rotate/180*M_PI)) );*/ value_w->setValue(i->sceneBoundingRect().width()); value_h->setValue(i->sceneBoundingRect().height()); } else if (i->type() == RECTITEM) { - QGraphicsRectItem *r = static_cast(i); + auto *r = static_cast(i); // qCDebug(KDENLIVE_LOG) << "Rect width is: " << r->rect().width() << ", was: " << value_w->value() << '\n'; value_w->setValue((int)r->rect().width()); value_h->setValue((int)r->rect().height()); } else if (i->type() == TEXTITEM) { - MyTextItem *t = static_cast(i); + auto *t = static_cast(i); value_w->setValue((int)t->boundingRect().width()); value_h->setValue((int)t->boundingRect().height()); } zValue->blockSignals(zBlocked); value_w->blockSignals(wBlocked); value_h->blockSignals(hBlocked); } void TitleWidget::updateCoordinates(QGraphicsItem *i) { // Block signals emitted by this method value_x->blockSignals(true); value_y->blockSignals(true); if (i->type() == TEXTITEM) { - MyTextItem *rec = static_cast(i); + auto *rec = static_cast(i); // Set the correct x coordinate value if (origin_x_left->isChecked()) { // Origin (0 point) is at m_frameWidth, coordinate axis is inverted value_x->setValue((int)(m_frameWidth - rec->pos().x() - rec->boundingRect().width())); } else { // Origin is at 0 (default) value_x->setValue((int)rec->pos().x()); } // Same for y if (origin_y_top->isChecked()) { value_y->setValue((int)(m_frameHeight - rec->pos().y() - rec->boundingRect().height())); } else { value_y->setValue((int)rec->pos().y()); } } else if (i->type() == RECTITEM) { - QGraphicsRectItem *rec = static_cast(i); + auto *rec = static_cast(i); if (origin_x_left->isChecked()) { // Origin (0 point) is at m_frameWidth value_x->setValue((int)(m_frameWidth - rec->pos().x() - rec->rect().width())); } else { // Origin is at 0 (default) value_x->setValue((int)rec->pos().x()); } if (origin_y_top->isChecked()) { value_y->setValue((int)(m_frameHeight - rec->pos().y() - rec->rect().height())); } else { value_y->setValue((int)rec->pos().y()); } } else if (i->type() == IMAGEITEM) { if (origin_x_left->isChecked()) { value_x->setValue((int)(m_frameWidth - i->pos().x() - i->sceneBoundingRect().width())); } else { value_x->setValue((int)i->pos().x()); } if (origin_y_top->isChecked()) { value_y->setValue((int)(m_frameHeight - i->pos().y() - i->sceneBoundingRect().height())); } else { value_y->setValue((int)i->pos().y()); } } // Stop blocking signals now value_x->blockSignals(false); value_y->blockSignals(false); } void TitleWidget::updateRotZoom(QGraphicsItem *i) { itemzoom->blockSignals(true); itemrotatex->blockSignals(true); itemrotatey->blockSignals(true); itemrotatez->blockSignals(true); Transform t = m_transformations.value(i); if (!i->data(TitleDocument::ZoomFactor).isNull()) { itemzoom->setValue(i->data(TitleDocument::ZoomFactor).toInt()); } else { itemzoom->setValue((int)(t.scalex * 100.0 + 0.5)); } itemrotatex->setValue((int)(t.rotatex)); itemrotatey->setValue((int)(t.rotatey)); itemrotatez->setValue((int)(t.rotatez)); itemzoom->blockSignals(false); itemrotatex->blockSignals(false); itemrotatey->blockSignals(false); itemrotatez->blockSignals(false); } void TitleWidget::updatePosition(QGraphicsItem *i) { updatePosition(i, value_x->value(), value_y->value()); } void TitleWidget::updatePosition(QGraphicsItem *i, int x, int y) { if (i->type() == TEXTITEM) { - MyTextItem *rec = static_cast(i); + auto *rec = static_cast(i); int posX; if (origin_x_left->isChecked()) { /* * Origin of the X axis is at m_frameWidth, and distance from right * border of the item to the right border of the frame is taken. See * comment to slotOriginXClicked(). */ posX = m_frameWidth - x - rec->boundingRect().width(); } else { posX = x; } int posY; if (origin_y_top->isChecked()) { /* Same for y axis */ posY = m_frameHeight - y - rec->boundingRect().height(); } else { posY = y; } rec->setPos(posX, posY); } else if (i->type() == RECTITEM) { - QGraphicsRectItem *rec = static_cast(i); + auto *rec = static_cast(i); int posX; if (origin_x_left->isChecked()) { posX = m_frameWidth - x - rec->rect().width(); } else { posX = x; } int posY; if (origin_y_top->isChecked()) { posY = m_frameHeight - y - rec->rect().height(); } else { posY = y; } rec->setPos(posX, posY); } else if (i->type() == IMAGEITEM) { int posX; if (origin_x_left->isChecked()) { // Use the sceneBoundingRect because this also regards transformations like zoom posX = m_frameWidth - x - i->sceneBoundingRect().width(); } else { posX = x; } int posY; if (origin_y_top->isChecked()) { posY = m_frameHeight - y - i->sceneBoundingRect().height(); } else { posY = y; } i->setPos(posX, posY); } } void TitleWidget::updateTextOriginX() { if (origin_x_left->isChecked()) { origin_x_left->setText(i18n("\u2212X")); } else { origin_x_left->setText(i18n("+X")); } } void TitleWidget::slotOriginXClicked() { // Update the text displayed on the button. updateTextOriginX(); QList l = graphicsView->scene()->selectedItems(); if (l.size() >= 1) { updateCoordinates(l.at(0)); // Remember x axis setting l.at(0)->setData(TitleDocument::OriginXLeft, origin_x_left->isChecked() ? TitleDocument::AxisInverted : TitleDocument::AxisDefault); } graphicsView->setFocus(); } void TitleWidget::updateTextOriginY() { if (origin_y_top->isChecked()) { origin_y_top->setText(i18n("\u2212Y")); } else { origin_y_top->setText(i18n("+Y")); } } void TitleWidget::slotOriginYClicked() { // Update the text displayed on the button. updateTextOriginY(); QList l = graphicsView->scene()->selectedItems(); if (l.size() >= 1) { updateCoordinates(l.at(0)); l.at(0)->setData(TitleDocument::OriginYTop, origin_y_top->isChecked() ? TitleDocument::AxisInverted : TitleDocument::AxisDefault); } graphicsView->setFocus(); } void TitleWidget::updateAxisButtons(QGraphicsItem *i) { int xAxis = i->data(TitleDocument::OriginXLeft).toInt(); int yAxis = i->data(TitleDocument::OriginYTop).toInt(); origin_x_left->blockSignals(true); origin_y_top->blockSignals(true); if (xAxis == TitleDocument::AxisInverted) { origin_x_left->setChecked(true); } else { origin_x_left->setChecked(false); } updateTextOriginX(); if (yAxis == TitleDocument::AxisInverted) { origin_y_top->setChecked(true); } else { origin_y_top->setChecked(false); } updateTextOriginY(); origin_x_left->blockSignals(false); origin_y_top->blockSignals(false); } void TitleWidget::slotChangeBackground() { QColor color = backgroundColor->color(); m_scene->setBackgroundBrush(QBrush(color)); color.setAlpha(backgroundAlpha->value()); m_frameBackground->setBrush(QBrush(color)); } void TitleWidget::slotChanged() { QList l = graphicsView->scene()->selectedItems(); if (l.size() >= 1 && l.at(0)->type() == TEXTITEM) { textChanged(static_cast(l.at(0))); } } void TitleWidget::textChanged(MyTextItem *i) { /* * If the user has set origin_x_left (the same for y), we need to look * whether a text element has been selected. If yes, we need to ensure that * the right border of the text field remains fixed also when some text has * been entered. * * This is also known as right-justified, with the difference that it is not * valid for text but for its boundingRect. Text may still be * left-justified. */ updateDimension(i); if (origin_x_left->isChecked() || origin_y_top->isChecked()) { if (!i->document()->isEmpty()) { updatePosition(i); } else { /* * Don't do anything if the string is empty. If the position were * updated here, a newly created text field would be set to the * position of the last selected text field. */ } } // mbt 1607: Template text has changed; don't auto-select content anymore. if (i->property("isTemplate").isValid()) { if (i->property("templateText").isValid()) { if (i->property("templateText") == i->toHtml()) { // Unchanged, do nothing. } else { i->setProperty("isTemplate", QVariant::Invalid); i->setProperty("templateText", QVariant::Invalid); } } } } void TitleWidget::slotInsertUnicode() { m_unicodeDialog->exec(); } void TitleWidget::slotInsertUnicodeString(const QString &string) { const QList l = graphicsView->scene()->selectedItems(); if (!l.isEmpty()) { if (l.at(0)->type() == TEXTITEM) { - MyTextItem *t = static_cast(l.at(0)); + auto *t = static_cast(l.at(0)); t->textCursor().insertText(string); } } } void TitleWidget::slotUpdateText() { QFont font = font_family->currentFont(); font.setPixelSize(font_size->value()); font.setItalic(buttonItalic->isChecked()); font.setUnderline(buttonUnder->isChecked()); font.setWeight(font_weight_box->itemData(font_weight_box->currentIndex()).toInt()); font.setLetterSpacing(QFont::AbsoluteSpacing, letter_spacing->value()); QColor color = fontColorButton->color(); QColor outlineColor = textOutlineColor->color(); QString gradientData; if (gradient_color->isChecked()) { // user wants a gradient gradientData = gradients_combo->currentData().toString(); } double outlineWidth = textOutline->value() / 10.0; int i; QList l = graphicsView->scene()->selectedItems(); for (i = 0; i < l.length(); ++i) { MyTextItem *item = nullptr; if (l.at(i)->type() == TEXTITEM) { item = static_cast(l.at(i)); } if (!item) { // No text item, try next one. continue; } // Set alignment of all text in the text item QTextCursor cur(item->document()); cur.select(QTextCursor::Document); QTextBlockFormat format = cur.blockFormat(); item->setData(TitleDocument::LineSpacing, line_spacing->value()); format.setLineHeight(line_spacing->value(), QTextBlockFormat::LineDistanceHeight); if (buttonAlignLeft->isChecked() || buttonAlignCenter->isChecked() || buttonAlignRight->isChecked()) { if (buttonAlignCenter->isChecked()) { item->setAlignment(Qt::AlignHCenter); } else if (buttonAlignRight->isChecked()) { item->setAlignment(Qt::AlignRight); } else if (buttonAlignLeft->isChecked()) { item->setAlignment(Qt::AlignLeft); } } else { item->setAlignment(Qt::AlignLeft); } // Set font properties item->setFont(font); QTextCharFormat cformat = cur.charFormat(); item->setData(TitleDocument::OutlineWidth, outlineWidth); item->setData(TitleDocument::OutlineColor, outlineColor); if (outlineWidth > 0.0) { cformat.setTextOutline(QPen(outlineColor, outlineWidth, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin)); } if (gradientData.isEmpty()) { cformat.setForeground(QBrush(color)); } else { QLinearGradient gr = GradientWidget::gradientFromString(gradientData, item->boundingRect().width(), item->boundingRect().height()); cformat.setForeground(QBrush(gr)); } // Store gradient in item properties item->setData(TitleDocument::Gradient, gradientData); cur.setCharFormat(cformat); cur.setBlockFormat(format); // item->setTextCursor(cur); cur.clearSelection(); item->setTextCursor(cur); item->setTextColor(color); } } void TitleWidget::rectChanged() { QList l = graphicsView->scene()->selectedItems(); - for (int i = 0; i < l.length(); ++i) { - if (l.at(i)->type() == RECTITEM && (settingUp == 0)) { - QGraphicsRectItem *rec = static_cast(l.at(i)); + for (auto i : l) { + if (i->type() == RECTITEM && (settingUp == 0)) { + auto *rec = static_cast(i); QColor f = rectFColor->color(); if (rectLineWidth->value() == 0) { rec->setPen(Qt::NoPen); } else { QPen penf(f); penf.setWidth(rectLineWidth->value()); penf.setJoinStyle(Qt::RoundJoin); rec->setPen(penf); } if (plain_rect->isChecked()) { rec->setBrush(QBrush(rectBColor->color())); rec->setData(TitleDocument::Gradient, QVariant()); } else { // gradient QString gradientData = gradients_rect_combo->currentData().toString(); rec->setData(TitleDocument::Gradient, gradientData); QLinearGradient gr = GradientWidget::gradientFromString(gradientData, rec->boundingRect().width(), rec->boundingRect().height()); rec->setBrush(QBrush(gr)); } } } } void TitleWidget::itemScaled(int val) { QList l = graphicsView->scene()->selectedItems(); if (l.size() == 1) { Transform x = m_transformations.value(l.at(0)); x.scalex = (double)val / 100.0; x.scaley = (double)val / 100.0; QTransform qtrans; qtrans.scale(x.scalex, x.scaley); qtrans.rotate(x.rotatex, Qt::XAxis); qtrans.rotate(x.rotatey, Qt::YAxis); qtrans.rotate(x.rotatez, Qt::ZAxis); l[0]->setTransform(qtrans); l[0]->setData(TitleDocument::ZoomFactor, val); m_transformations[l.at(0)] = x; updateDimension(l.at(0)); } } void TitleWidget::itemRotateX(int val) { itemRotate(val, 0); } void TitleWidget::itemRotateY(int val) { itemRotate(val, 1); } void TitleWidget::itemRotateZ(int val) { itemRotate(val, 2); } void TitleWidget::itemRotate(int val, int axis) { QList l = graphicsView->scene()->selectedItems(); if (l.size() == 1) { Transform x = m_transformations[l.at(0)]; switch (axis) { case 0: x.rotatex = val; break; case 1: x.rotatey = val; break; case 2: x.rotatez = val; break; } l[0]->setData(TitleDocument::RotateFactor, QList() << QVariant(x.rotatex) << QVariant(x.rotatey) << QVariant(x.rotatez)); QTransform qtrans; qtrans.scale(x.scalex, x.scaley); qtrans.rotate(x.rotatex, Qt::XAxis); qtrans.rotate(x.rotatey, Qt::YAxis); qtrans.rotate(x.rotatez, Qt::ZAxis); l[0]->setTransform(qtrans); m_transformations[l.at(0)] = x; if (l[0]->data(TitleDocument::ZoomFactor).isNull()) { l[0]->setData(TitleDocument::ZoomFactor, 100); } updateDimension(l.at(0)); } } void TitleWidget::itemHCenter() { QList l = graphicsView->scene()->selectedItems(); if (l.size() == 1) { QGraphicsItem *item = l.at(0); QRectF br = item->sceneBoundingRect(); int width = (int)br.width(); int newPos = (int)((m_frameWidth - width) / 2); newPos += item->pos().x() - br.left(); // Check item transformation item->setPos(newPos, item->pos().y()); updateCoordinates(item); } } void TitleWidget::itemVCenter() { QList l = graphicsView->scene()->selectedItems(); if (l.size() == 1) { QGraphicsItem *item = l.at(0); QRectF br = item->sceneBoundingRect(); int height = (int)br.height(); int newPos = (int)((m_frameHeight - height) / 2); newPos += item->pos().y() - br.top(); // Check item transformation item->setPos(item->pos().x(), newPos); updateCoordinates(item); } } void TitleWidget::itemTop() { QList l = graphicsView->scene()->selectedItems(); if (l.size() == 1) { QGraphicsItem *item = l.at(0); QRectF br = item->sceneBoundingRect(); double diff; if (br.top() > 0) { diff = -br.top(); } else { diff = -br.bottom(); } item->moveBy(0, diff); updateCoordinates(item); } } void TitleWidget::itemBottom() { QList l = graphicsView->scene()->selectedItems(); if (l.size() == 1) { QGraphicsItem *item = l.at(0); QRectF br = item->sceneBoundingRect(); double diff; if (br.bottom() > m_frameHeight) { diff = m_frameHeight - br.top(); } else { diff = m_frameHeight - br.bottom(); } item->moveBy(0, diff); updateCoordinates(item); } } void TitleWidget::itemLeft() { QList l = graphicsView->scene()->selectedItems(); if (l.size() == 1) { QGraphicsItem *item = l.at(0); QRectF br = item->sceneBoundingRect(); double diff; if (br.left() > 0) { diff = -br.left(); } else { diff = -br.right(); } item->moveBy(diff, 0); updateCoordinates(item); } } void TitleWidget::itemRight() { QList l = graphicsView->scene()->selectedItems(); if (l.size() == 1) { QGraphicsItem *item = l.at(0); QRectF br = item->sceneBoundingRect(); double diff; if (br.right() < m_frameWidth) { diff = m_frameWidth - br.right(); } else { diff = m_frameWidth - br.left(); } item->moveBy(diff, 0); updateCoordinates(item); } } void TitleWidget::loadTitle(QUrl url) { if (!url.isValid()) { QString startFolder = KRecentDirs::dir(QStringLiteral(":KdenliveProjectsTitles")); url = QFileDialog::getOpenFileUrl(this, i18n("Load Title"), QUrl::fromLocalFile(startFolder.isEmpty() ? m_projectTitlePath : startFolder), i18n("Kdenlive title (*.kdenlivetitle)")); } if (url.isValid()) { // make sure we don't delete the guides qDeleteAll(m_guides); m_guides.clear(); QList items = m_scene->items(); items.removeAll(m_frameBorder); items.removeAll(m_frameBackground); items.removeAll(m_frameImage); - for (int i = 0; i < items.size(); ++i) { - if (items.at(i)->zValue() > -1000) { - delete items.at(i); + for (auto item : items) { + if (item->zValue() > -1000) { + delete item; } } m_scene->clearTextSelection(); QDomDocument doc; QFile file(url.toLocalFile()); doc.setContent(&file, false); file.close(); setXml(doc); updateGuides(0); m_projectTitlePath = QFileInfo(file).dir().absolutePath(); KRecentDirs::add(QStringLiteral(":KdenliveProjectsTitles"), m_projectTitlePath); } } void TitleWidget::saveTitle(QUrl url) { if (anim_start->isChecked()) { slotAnimStart(false); } if (anim_end->isChecked()) { slotAnimEnd(false); } bool embed_image = false; // If we have images in the title, ask for embed QList list = graphicsView->scene()->items(); QGraphicsPixmapItem pix; int pixmapType = pix.type(); for (const QGraphicsItem *item : list) { if (item->type() == pixmapType && item != m_frameImage) { embed_image = true; break; } } if (embed_image && KMessageBox::questionYesNo( this, i18n("Do you want to embed Images into this TitleDocument?\nThis is most needed for sharing Titles.")) != KMessageBox::Yes) { embed_image = false; } if (!url.isValid()) { QPointer fs = new QFileDialog(this, i18n("Save Title"), m_projectTitlePath); fs->setMimeTypeFilters(QStringList() << QStringLiteral("application/x-kdenlivetitle")); fs->setFileMode(QFileDialog::AnyFile); fs->setAcceptMode(QFileDialog::AcceptSave); fs->setDefaultSuffix(QStringLiteral("kdenlivetitle")); // TODO: KF5 porting? // fs->setConfirmOverwrite(true); // fs->setKeepLocation(true); if ((fs->exec() != 0) && !fs->selectedUrls().isEmpty()) { url = fs->selectedUrls().constFirst(); } delete fs; } if (url.isValid()) { if (!m_titledocument.saveDocument(url, m_startViewport, m_endViewport, m_tc.getFrameCount(title_duration->text()), embed_image)) { KMessageBox::error(this, i18n("Cannot write to file %1", url.toLocalFile())); } } } void TitleWidget::downloadTitleTemplates() { if (getNewStuff(QStringLiteral(":data/kdenlive_titles.knsrc")) > 0) { refreshTitleTemplates(m_projectTitlePath); refreshTemplateBoxContents(); } } int TitleWidget::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(); } QDomDocument TitleWidget::xml() { QDomDocument doc = m_titledocument.xml(m_startViewport, m_endViewport); doc.documentElement().setAttribute(QStringLiteral("duration"), m_tc.getFrameCount(title_duration->text())); doc.documentElement().setAttribute(QStringLiteral("out"), m_tc.getFrameCount(title_duration->text())); return doc; } int TitleWidget::duration() const { return m_tc.getFrameCount(title_duration->text()); } void TitleWidget::setXml(const QDomDocument &doc, const QString &id) { m_clipId = id; int duration; if (m_missingMessage) { delete m_missingMessage; m_missingMessage = nullptr; } m_count = m_titledocument.loadFromXml(doc, m_startViewport, m_endViewport, &duration, m_projectTitlePath); adjustFrameSize(); if (m_titledocument.invalidCount() > 0) { m_missingMessage = new KMessageWidget(this); m_missingMessage->setCloseButtonVisible(true); m_missingMessage->setWordWrap(true); m_missingMessage->setMessageType(KMessageWidget::Warning); m_missingMessage->setText(i18n("This title has %1 missing elements", m_titledocument.invalidCount())); QAction *action = new QAction(i18n("Details")); m_missingMessage->addAction(action); connect(action, &QAction::triggered, this, &TitleWidget::showMissingItems); action = new QAction(i18n("Delete missing elements")); m_missingMessage->addAction(action); connect(action, &QAction::triggered, this, &TitleWidget::deleteMissingItems); messageLayout->addWidget(m_missingMessage); m_missingMessage->animatedShow(); } title_duration->setText(m_tc.getTimecode(GenTime(duration, m_fps))); /*if (doc.documentElement().hasAttribute("out")) { GenTime duration = GenTime(doc.documentElement().attribute("out").toDouble() / 1000.0); title_duration->setText(m_tc.getTimecode(duration)); } else title_duration->setText(m_tc.getTimecode(GenTime(5000)));*/ QDomElement e = doc.documentElement(); m_transformations.clear(); QList items = graphicsView->scene()->items(); const double PI = 4.0 * atan(1.0); for (int i = 0; i < items.count(); ++i) { QTransform t = items.at(i)->transform(); Transform x; x.scalex = t.m11(); x.scaley = t.m22(); if (!items.at(i)->data(TitleDocument::RotateFactor).isNull()) { QList rotlist = items.at(i)->data(TitleDocument::RotateFactor).toList(); if (rotlist.count() >= 3) { x.rotatex = rotlist[0].toDouble(); x.rotatey = rotlist[1].toDouble(); x.rotatez = rotlist[2].toDouble(); // Try to adjust zoom t.rotate(x.rotatex * (-1), Qt::XAxis); t.rotate(x.rotatey * (-1), Qt::YAxis); t.rotate(x.rotatez * (-1), Qt::ZAxis); x.scalex = t.m11(); x.scaley = t.m22(); } else { x.rotatex = 0; x.rotatey = 0; x.rotatez = 0; } } else { x.rotatex = 0; x.rotatey = 0; x.rotatez = 180. / PI * atan2(-t.m21(), t.m11()); } m_transformations[items.at(i)] = x; } // mbd: Update the GUI color selectors to match the stuff from the loaded document QColor background_color = m_titledocument.getBackgroundColor(); backgroundAlpha->blockSignals(true); backgroundColor->blockSignals(true); backgroundAlpha->setValue(background_color.alpha()); background_color.setAlpha(255); backgroundColor->setColor(background_color); backgroundAlpha->blockSignals(false); backgroundColor->blockSignals(false); /*startViewportX->setValue(m_startViewport->data(0).toInt()); startViewportY->setValue(m_startViewport->data(1).toInt()); startViewportSize->setValue(m_startViewport->data(2).toInt()); endViewportX->setValue(m_endViewport->data(0).toInt()); endViewportY->setValue(m_endViewport->data(1).toInt()); endViewportSize->setValue(m_endViewport->data(2).toInt());*/ QTimer::singleShot(200, this, &TitleWidget::slotAdjustZoom); slotSelectTool(); selectionChanged(); } void TitleWidget::slotAccepted() { if (anim_start->isChecked()) { slotAnimStart(false); } if (anim_end->isChecked()) { slotAnimEnd(false); } writeChoices(); } void TitleWidget::deleteMissingItems() { m_missingMessage->animatedHide(); QList items = graphicsView->scene()->items(); QList toDelete; for (int i = 0; i < items.count(); ++i) { if (items.at(i)->data(Qt::UserRole + 2).toInt() == 1) { // We found a missing item toDelete << items.at(i); } } if (toDelete.size() != m_titledocument.invalidCount()) { qDebug() << "/// WARNING, INCOHERENT MISSING ELEMENTS in title: " << toDelete.size() << " != " << m_titledocument.invalidCount(); } while (!toDelete.isEmpty()) { QGraphicsItem *item = toDelete.takeFirst(); if (m_scene) { m_scene->removeItem(item); } } m_missingMessage->deleteLater(); } void TitleWidget::showMissingItems() { QList items = graphicsView->scene()->items(); QStringList missingUrls; for (int i = 0; i < items.count(); ++i) { if (items.at(i)->data(Qt::UserRole + 2).toInt() == 1) { // We found a missing item missingUrls << items.at(i)->data(Qt::UserRole).toString(); } } missingUrls.removeDuplicates(); KMessageBox::informationList(QApplication::activeWindow(), i18n("The following files are missing: "), missingUrls); } void TitleWidget::writeChoices() { // Get a pointer to a shared configuration instance, then get the TitleWidget group. KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup titleConfig(config, "TitleWidget"); // Write the entries titleConfig.writeEntry("dialog_geometry", saveGeometry().toBase64()); titleConfig.writeEntry("font_family", font_family->currentFont()); // titleConfig.writeEntry("font_size", font_size->value()); titleConfig.writeEntry("font_pixel_size", font_size->value()); titleConfig.writeEntry("font_color", fontColorButton->color()); titleConfig.writeEntry("font_outline_color", textOutlineColor->color()); titleConfig.writeEntry("font_outline", textOutline->value()); titleConfig.writeEntry("font_weight", font_weight_box->itemData(font_weight_box->currentIndex()).toInt()); titleConfig.writeEntry("font_italic", buttonItalic->isChecked()); titleConfig.writeEntry("font_underlined", buttonUnder->isChecked()); titleConfig.writeEntry("rect_background_color", rectBColor->color()); titleConfig.writeEntry("rect_foreground_color", rectFColor->color()); titleConfig.writeEntry("rect_background_alpha", rectBColor->color().alpha()); titleConfig.writeEntry("rect_foreground_alpha", rectFColor->color().alpha()); titleConfig.writeEntry("rect_line_width", rectLineWidth->value()); titleConfig.writeEntry("background_color", backgroundColor->color()); titleConfig.writeEntry("background_alpha", backgroundAlpha->value()); titleConfig.writeEntry("use_grid", use_grid->isChecked()); //! \todo Not sure if I should sync - it is probably safe to do it config->sync(); } void TitleWidget::readChoices() { // Get a pointer to a shared configuration instance, then get the TitleWidget group. KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup titleConfig(config, "TitleWidget"); // read the entries const QByteArray geometry = titleConfig.readEntry("dialog_geometry", QByteArray()); restoreGeometry(QByteArray::fromBase64(geometry)); font_family->setCurrentFont(titleConfig.readEntry("font_family", font_family->currentFont())); font_size->setValue(titleConfig.readEntry("font_pixel_size", font_size->value())); m_scene->slotUpdateFontSize(font_size->value()); QColor fontColor = QColor(titleConfig.readEntry("font_color", fontColorButton->color())); QColor outlineColor = QColor(titleConfig.readEntry("font_outline_color", textOutlineColor->color())); fontColor.setAlpha(titleConfig.readEntry("font_alpha", fontColor.alpha())); outlineColor.setAlpha(titleConfig.readEntry("font_outline_alpha", outlineColor.alpha())); fontColorButton->setColor(fontColor); textOutlineColor->setColor(outlineColor); textOutline->setValue(titleConfig.readEntry("font_outline", textOutline->value())); int weight; if (titleConfig.readEntry("font_bold", false)) { weight = QFont::Bold; } else { weight = titleConfig.readEntry("font_weight", font_weight_box->itemData(font_weight_box->currentIndex()).toInt()); } setFontBoxWeight(weight); buttonItalic->setChecked(titleConfig.readEntry("font_italic", buttonItalic->isChecked())); buttonUnder->setChecked(titleConfig.readEntry("font_underlined", buttonUnder->isChecked())); QColor fgColor = QColor(titleConfig.readEntry("rect_foreground_color", rectFColor->color())); QColor bgColor = QColor(titleConfig.readEntry("rect_background_color", rectBColor->color())); fgColor.setAlpha(titleConfig.readEntry("rect_foreground_alpha", fgColor.alpha())); bgColor.setAlpha(titleConfig.readEntry("rect_background_alpha", bgColor.alpha())); rectFColor->setColor(fgColor); rectBColor->setColor(bgColor); rectLineWidth->setValue(titleConfig.readEntry("rect_line_width", rectLineWidth->value())); backgroundColor->setColor(titleConfig.readEntry("background_color", backgroundColor->color())); backgroundAlpha->setValue(titleConfig.readEntry("background_alpha", backgroundAlpha->value())); use_grid->setChecked(titleConfig.readEntry("use_grid", false)); m_scene->slotUseGrid(use_grid->isChecked()); } void TitleWidget::adjustFrameSize() { m_frameWidth = m_titledocument.frameWidth(); m_frameHeight = m_titledocument.frameHeight(); m_frameBorder->setRect(0, 0, m_frameWidth, m_frameHeight); displayBackgroundFrame(); } void TitleWidget::slotAnimStart(bool anim) { if (anim && anim_end->isChecked()) { anim_end->setChecked(false); m_endViewport->setZValue(-1000); m_endViewport->setBrush(QBrush()); } slotSelectTool(); QList list = m_scene->items(); for (int i = 0; i < list.count(); ++i) { if (list.at(i)->zValue() > -1000) { if (!list.at(i)->data(-1).isNull()) { continue; } list.at(i)->setFlag(QGraphicsItem::ItemIsMovable, !anim); list.at(i)->setFlag(QGraphicsItem::ItemIsSelectable, !anim); } } align_box->setEnabled(anim); itemzoom->setEnabled(!anim); itemrotatex->setEnabled(!anim); itemrotatey->setEnabled(!anim); itemrotatez->setEnabled(!anim); frame_toolbar->setEnabled(!anim); toolbar_stack->setEnabled(!anim); if (anim) { keep_aspect->setChecked(!m_startViewport->data(0).isNull()); m_startViewport->setZValue(1100); QColor col = m_startViewport->pen().color(); col.setAlpha(100); m_startViewport->setBrush(col); m_startViewport->setFlags(QGraphicsItem::ItemIsMovable | QGraphicsItem::ItemIsSelectable); m_startViewport->setSelected(true); selectionChanged(); slotSelectTool(); if (m_startViewport->childItems().isEmpty()) { addAnimInfoText(); } } else { m_startViewport->setZValue(-1000); m_startViewport->setBrush(QBrush()); m_startViewport->setFlags(nullptr); if (!anim_end->isChecked()) { deleteAnimInfoText(); } } } void TitleWidget::slotAnimEnd(bool anim) { if (anim && anim_start->isChecked()) { anim_start->setChecked(false); m_startViewport->setZValue(-1000); m_startViewport->setBrush(QBrush()); } slotSelectTool(); QList list = m_scene->items(); for (int i = 0; i < list.count(); ++i) { if (list.at(i)->zValue() > -1000) { if (!list.at(i)->data(-1).isNull()) { continue; } list.at(i)->setFlag(QGraphicsItem::ItemIsMovable, !anim); list.at(i)->setFlag(QGraphicsItem::ItemIsSelectable, !anim); } } align_box->setEnabled(anim); itemzoom->setEnabled(!anim); itemrotatex->setEnabled(!anim); itemrotatey->setEnabled(!anim); itemrotatez->setEnabled(!anim); frame_toolbar->setEnabled(!anim); toolbar_stack->setEnabled(!anim); if (anim) { keep_aspect->setChecked(!m_endViewport->data(0).isNull()); m_endViewport->setZValue(1100); QColor col = m_endViewport->pen().color(); col.setAlpha(100); m_endViewport->setBrush(col); m_endViewport->setFlags(QGraphicsItem::ItemIsMovable | QGraphicsItem::ItemIsSelectable); m_endViewport->setSelected(true); selectionChanged(); slotSelectTool(); if (m_endViewport->childItems().isEmpty()) { addAnimInfoText(); } } else { m_endViewport->setZValue(-1000); m_endViewport->setBrush(QBrush()); m_endViewport->setFlags(nullptr); if (!anim_start->isChecked()) { deleteAnimInfoText(); } } } void TitleWidget::addAnimInfoText() { // add text to anim viewport QGraphicsTextItem *t = new QGraphicsTextItem(i18nc("Indicates the start of an animation", "Start"), m_startViewport); QGraphicsTextItem *t2 = new QGraphicsTextItem(i18nc("Indicates the end of an animation", "End"), m_endViewport); QFont font = t->font(); font.setPixelSize(m_startViewport->rect().width() / 10); QColor col = m_startViewport->pen().color(); col.setAlpha(255); t->setDefaultTextColor(col); t->setFont(font); font.setPixelSize(m_endViewport->rect().width() / 10); col = m_endViewport->pen().color(); col.setAlpha(255); t2->setDefaultTextColor(col); t2->setFont(font); } void TitleWidget::updateInfoText() { // update info text font if (!m_startViewport->childItems().isEmpty()) { MyTextItem *item = static_cast(m_startViewport->childItems().at(0)); if (item) { QFont font = item->font(); font.setPixelSize(m_startViewport->rect().width() / 10); item->setFont(font); } } if (!m_endViewport->childItems().isEmpty()) { MyTextItem *item = static_cast(m_endViewport->childItems().at(0)); if (item) { QFont font = item->font(); font.setPixelSize(m_endViewport->rect().width() / 10); item->setFont(font); } } } void TitleWidget::deleteAnimInfoText() { // end animation editing, remove info text while (!m_startViewport->childItems().isEmpty()) { QGraphicsItem *item = m_startViewport->childItems().at(0); if (m_scene) { m_scene->removeItem(item); } } while (!m_endViewport->childItems().isEmpty()) { QGraphicsItem *item = m_endViewport->childItems().at(0); if (m_scene) { m_scene->removeItem(item); } } } void TitleWidget::slotKeepAspect(bool keep) { if ((int)m_endViewport->zValue() == 1100) { m_endViewport->setData(0, keep ? m_frameWidth : QVariant()); m_endViewport->setData(1, keep ? m_frameHeight : QVariant()); } else { m_startViewport->setData(0, keep ? m_frameWidth : QVariant()); m_startViewport->setData(1, keep ? m_frameHeight : QVariant()); } } void TitleWidget::slotResize50() { if ((int)m_endViewport->zValue() == 1100) { m_endViewport->setRect(0, 0, m_frameWidth / 2, m_frameHeight / 2); } else { m_startViewport->setRect(0, 0, m_frameWidth / 2, m_frameHeight / 2); } } void TitleWidget::slotResize100() { if ((int)m_endViewport->zValue() == 1100) { m_endViewport->setRect(0, 0, m_frameWidth, m_frameHeight); } else { m_startViewport->setRect(0, 0, m_frameWidth, m_frameHeight); } } void TitleWidget::slotResize200() { if ((int)m_endViewport->zValue() == 1100) { m_endViewport->setRect(0, 0, m_frameWidth * 2, m_frameHeight * 2); } else { m_startViewport->setRect(0, 0, m_frameWidth * 2, m_frameHeight * 2); } } void TitleWidget::slotAddEffect(int /*ix*/) { QList list = graphicsView->scene()->selectedItems(); /* int effect = effect_list->itemData(ix).toInt(); if (list.size() == 1) { if (effect == NOEFFECT) effect_stack->setHidden(true); else { effect_stack->setCurrentIndex(effect - 1); effect_stack->setHidden(false); } } else // Hide the effects stack when more than one element is selected. effect_stack->setHidden(true); for (QGraphicsItem * item : list) { switch (effect) { case NOEFFECT: item->setData(100, QVariant()); item->setGraphicsEffect(0); break; case TYPEWRITEREFFECT: if (item->type() == TEXTITEM) { QStringList effdata = QStringList() << QStringLiteral("typewriter") << QString::number(typewriter_delay->value()) + QLatin1Char(';') + QString::number(typewriter_start->value()); item->setData(100, effdata); } break; // Do not remove the non-QGraphicsEffects. case BLUREFFECT: item->setGraphicsEffect(new QGraphicsBlurEffect()); break; case SHADOWEFFECT: item->setGraphicsEffect(new QGraphicsDropShadowEffect()); break; } }*/ } void TitleWidget::slotEditTypewriter(int /*ix*/) { QList l = graphicsView->scene()->selectedItems(); if (l.size() == 1) { QStringList effdata = QStringList() << QStringLiteral("typewriter") << QString::number(typewriter_delay->value()) + QLatin1Char(';') + QString::number(typewriter_start->value()); l[0]->setData(100, effdata); } } qreal TitleWidget::zIndexBounds(bool maxBound, bool intersectingOnly) { qreal bound = maxBound ? -99 : 99; const QList l = graphicsView->scene()->selectedItems(); if (!l.isEmpty()) { QList lItems; // Get items (all or intersecting only) if (intersectingOnly) { lItems = graphicsView->scene()->items(l[0]->sceneBoundingRect(), Qt::IntersectsItemShape); } else { lItems = graphicsView->scene()->items(); } if (!lItems.isEmpty()) { int n = lItems.size(); qreal z; if (maxBound) { for (int i = 0; i < n; ++i) { z = lItems[i]->zValue(); if (z > bound && !lItems[i]->isSelected()) { bound = z; } else if (z - 1 > bound) { // To get the maximum index even if it is of an item of the current selection. // Used when updating multiple items, to get all to the same level. // Otherwise, the maximum would stay at -99 if the highest item is in the selection. bound = z - 1; } } } else { // Get minimum z index. for (int i = 0; i < n; ++i) { z = lItems[i]->zValue(); if (z < bound && !lItems[i]->isSelected() && z > -999) { // There are items at the very bottom (background e.g.) with z-index < -1000. bound = z; } else if (z + 1 < bound && z > -999) { bound = z + 1; } } } } } return bound; } void TitleWidget::slotZIndexUp() { QList l = graphicsView->scene()->selectedItems(); if (l.size() >= 1) { qreal currentZ = l[0]->zValue(); qreal max = zIndexBounds(true, true); if (currentZ <= max) { l[0]->setZValue(currentZ + 1); updateDimension(l[0]); } } } void TitleWidget::slotZIndexTop() { QList l = graphicsView->scene()->selectedItems(); qreal max = zIndexBounds(true, false); // qCDebug(KDENLIVE_LOG) << "Max z-index is " << max << ".\n"; - for (int i = 0; i < l.size(); ++i) { - qreal currentZ = l[i]->zValue(); + for (auto &i : l) { + qreal currentZ = i->zValue(); if (currentZ <= max) { // qCDebug(KDENLIVE_LOG) << "Updating item " << i << ", is " << currentZ << ".\n"; - l[i]->setZValue(max + 1); + i->setZValue(max + 1); } else { // qCDebug(KDENLIVE_LOG) << "Not updating " << i << ", is " << currentZ << ".\n"; } } // Update the z index value in the GUI if (!l.isEmpty()) { updateDimension(l[0]); } } void TitleWidget::slotZIndexDown() { QList l = graphicsView->scene()->selectedItems(); if (l.size() >= 1) { qreal currentZ = l[0]->zValue(); qreal min = zIndexBounds(false, true); if (currentZ >= min) { l[0]->setZValue(currentZ - 1); updateDimension(l[0]); } } } void TitleWidget::slotZIndexBottom() { QList l = graphicsView->scene()->selectedItems(); qreal min = zIndexBounds(false, false); - for (int i = 0; i < l.size(); ++i) { - qreal currentZ = l[i]->zValue(); + for (auto &i : l) { + qreal currentZ = i->zValue(); if (currentZ >= min) { - l[i]->setZValue(min - 1); + i->setZValue(min - 1); } } // Update the z index value in the GUI if (!l.isEmpty()) { updateDimension(l[0]); } } void TitleWidget::slotSelectAll() { QList l = graphicsView->scene()->items(); - for (int i = 0; i < l.size(); ++i) { - l.at(i)->setSelected(true); + for (auto i : l) { + i->setSelected(true); } } void TitleWidget::selectItems(int itemType) { QList l; if (!graphicsView->scene()->selectedItems().isEmpty()) { l = graphicsView->scene()->selectedItems(); - for (int i = 0; i < l.size(); ++i) { - if (l.at(i)->type() != itemType) { - l.at(i)->setSelected(false); + for (auto i : l) { + if (i->type() != itemType) { + i->setSelected(false); } } } else { l = graphicsView->scene()->items(); - for (int i = 0; i < l.size(); ++i) { - if (l.at(i)->type() == itemType) { - l.at(i)->setSelected(true); + for (auto i : l) { + if (i->type() == itemType) { + i->setSelected(true); } } } } void TitleWidget::slotSelectText() { selectItems(TEXTITEM); } void TitleWidget::slotSelectRects() { selectItems(RECTITEM); } void TitleWidget::slotSelectImages() { selectItems(IMAGEITEM); } void TitleWidget::slotSelectNone() { graphicsView->blockSignals(true); QList l = graphicsView->scene()->items(); - for (int i = 0; i < l.size(); ++i) { - l.at(i)->setSelected(false); + for (auto i : l) { + i->setSelected(false); } graphicsView->blockSignals(false); selectionChanged(); } QString TitleWidget::getTooltipWithShortcut(const QString &tipText, QAction *button) { return tipText + QStringLiteral(" ") + button->shortcut().toString() + QStringLiteral(""); } void TitleWidget::prepareTools(QGraphicsItem *referenceItem) { // Let some GUI elements block signals. We may want to change their values without any sideeffects. // Additionally, store the previous blocking state to avoid side effects when this function is called from within another one. // Note: Disabling an element also blocks signals. So disabled elements don't need to be set to blocking too. bool blockOX = origin_x_left->signalsBlocked(); bool blockOY = origin_y_top->signalsBlocked(); bool blockEff = effect_list->signalsBlocked(); bool blockRX = itemrotatex->signalsBlocked(); bool blockRY = itemrotatey->signalsBlocked(); bool blockRZ = itemrotatez->signalsBlocked(); bool blockZoom = itemzoom->signalsBlocked(); bool blockX = value_x->signalsBlocked(); bool blockY = value_y->signalsBlocked(); bool blockW = value_w->signalsBlocked(); bool blockH = value_h->signalsBlocked(); origin_x_left->blockSignals(true); origin_y_top->blockSignals(true); effect_list->blockSignals(true); itemrotatex->blockSignals(true); itemrotatey->blockSignals(true); itemrotatez->blockSignals(true); itemzoom->blockSignals(true); value_x->blockSignals(true); value_y->blockSignals(true); value_w->blockSignals(true); value_h->blockSignals(true); if (referenceItem == nullptr) { // qCDebug(KDENLIVE_LOG) << "nullptr item.\n"; effect_list->setCurrentIndex(0); origin_x_left->setChecked(false); origin_y_top->setChecked(false); updateTextOriginX(); updateTextOriginY(); enableToolbars(TITLE_SELECT); showToolbars(TITLE_SELECT); itemzoom->setEnabled(false); itemrotatex->setEnabled(false); itemrotatey->setEnabled(false); itemrotatez->setEnabled(false); frame_properties->setEnabled(false); toolbar_stack->setEnabled(false); /*letter_spacing->setEnabled(false); line_spacing->setEnabled(false); letter_spacing->setValue(0); line_spacing->setValue(0);*/ } else { toolbar_stack->setEnabled(true); frame_properties->setEnabled(true); if (referenceItem != m_startViewport && referenceItem != m_endViewport) { itemzoom->setEnabled(true); itemrotatex->setEnabled(true); itemrotatey->setEnabled(true); itemrotatez->setEnabled(true); } else { itemzoom->setEnabled(false); itemrotatex->setEnabled(false); itemrotatey->setEnabled(false); itemrotatez->setEnabled(false); updateInfoText(); } letter_spacing->setEnabled(referenceItem->type() == TEXTITEM); line_spacing->setEnabled(referenceItem->type() == TEXTITEM); if (referenceItem->type() == TEXTITEM) { showToolbars(TITLE_TEXT); - MyTextItem *i = static_cast(referenceItem); + auto *i = static_cast(referenceItem); if (!i->document()->isEmpty()) { // We have an existing text item selected if (!i->data(100).isNull()) { // Item has an effect QStringList effdata = i->data(100).toStringList(); QString effectName = effdata.takeFirst(); if (effectName == QLatin1String("typewriter")) { QStringList params = effdata.at(0).split(QLatin1Char(';')); typewriter_delay->setValue(params.at(0).toInt()); typewriter_start->setValue(params.at(1).toInt()); effect_list->setCurrentIndex(effect_list->findData((int)TYPEWRITEREFFECT)); } } else { /*if (i->graphicsEffect()) { QGraphicsBlurEffect *blur = static_cast (i->graphicsEffect()); if (blur) { effect_list->setCurrentIndex(effect_list->findData((int) BLUREFFECT)); int rad = (int) blur->blurRadius(); blur_radius->setValue(rad); effect_stack->setHidden(false); } else { QGraphicsDropShadowEffect *shad = static_cast (i->graphicsEffect()); if (shad) { effect_list->setCurrentIndex(effect_list->findData((int) SHADOWEFFECT)); shadow_radius->setValue(shad->blurRadius()); shadow_x->setValue(shad->xOffset()); shadow_y->setValue(shad->yOffset()); effect_stack->setHidden(false); } } } else { effect_list->setCurrentIndex(effect_list->findData((int) NOEFFECT)); effect_stack->setHidden(true); }*/ } font_size->blockSignals(true); font_family->blockSignals(true); font_weight_box->blockSignals(true); buttonItalic->blockSignals(true); buttonUnder->blockSignals(true); fontColorButton->blockSignals(true); buttonAlignLeft->blockSignals(true); buttonAlignRight->blockSignals(true); buttonAlignCenter->blockSignals(true); QFont font = i->font(); font_family->setCurrentFont(font); font_size->setValue(font.pixelSize()); m_scene->slotUpdateFontSize(font.pixelSize()); buttonItalic->setChecked(font.italic()); buttonUnder->setChecked(font.underline()); setFontBoxWeight(font.weight()); QTextCursor cursor(i->document()); cursor.select(QTextCursor::Document); QColor color = cursor.charFormat().foreground().color(); fontColorButton->setColor(color); if (!i->data(TitleDocument::OutlineWidth).isNull()) { textOutline->blockSignals(true); textOutline->setValue(i->data(TitleDocument::OutlineWidth).toDouble() * 10); textOutline->blockSignals(false); } else { textOutline->blockSignals(true); textOutline->setValue(0); textOutline->blockSignals(false); } if (!i->data(TitleDocument::OutlineColor).isNull()) { textOutlineColor->blockSignals(true); QVariant variant = i->data(TitleDocument::OutlineColor); color = variant.value(); textOutlineColor->setColor(color); textOutlineColor->blockSignals(false); } if (!i->data(TitleDocument::Gradient).isNull()) { gradients_combo->blockSignals(true); gradient_color->setChecked(true); QString gradientData = i->data(TitleDocument::Gradient).toString(); int ix = gradients_combo->findData(gradientData); if (ix == -1) { // This gradient does not exist in our settings, store it storeGradient(gradientData); ix = gradients_combo->findData(gradientData); } gradients_combo->setCurrentIndex(ix); gradients_combo->blockSignals(false); } else { plain_color->setChecked(true); } if (i->alignment() == Qt::AlignHCenter) { buttonAlignCenter->setChecked(true); } else if (i->alignment() == Qt::AlignRight) { buttonAlignRight->setChecked(true); } else { buttonAlignLeft->setChecked(true); } QStringList sInfo = i->shadowInfo(); if (sInfo.count() >= 5) { shadowBox->setChecked(static_cast(sInfo.at(0).toInt())); shadowBox->blockSignals(true); shadowColor->setColor(QColor(sInfo.at(1))); blur_radius->setValue(sInfo.at(2).toInt()); shadowX->setValue(sInfo.at(3).toInt()); shadowY->setValue(sInfo.at(4).toInt()); shadowBox->blockSignals(false); } letter_spacing->blockSignals(true); line_spacing->blockSignals(true); QTextCursor cur = i->textCursor(); QTextBlockFormat format = cur.blockFormat(); letter_spacing->setValue(font.letterSpacing()); line_spacing->setValue(format.lineHeight()); letter_spacing->blockSignals(false); line_spacing->blockSignals(false); font_size->blockSignals(false); font_family->blockSignals(false); font_weight_box->blockSignals(false); buttonItalic->blockSignals(false); buttonUnder->blockSignals(false); fontColorButton->blockSignals(false); buttonAlignLeft->blockSignals(false); buttonAlignRight->blockSignals(false); buttonAlignCenter->blockSignals(false); // mbt 1607: Select text if the text item is an unchanged template item. if (i->property("isTemplate").isValid()) { cur.setPosition(0, QTextCursor::MoveAnchor); cur.select(QTextCursor::Document); i->setTextCursor(cur); // Make text editable now. i->grabKeyboard(); i->setTextInteractionFlags(Qt::TextEditorInteraction); } } updateAxisButtons(i); updateCoordinates(i); updateDimension(i); enableToolbars(TITLE_TEXT); } else if ((referenceItem)->type() == RECTITEM) { showToolbars(TITLE_RECTANGLE); settingUp = 1; - QGraphicsRectItem *rec = static_cast(referenceItem); + auto *rec = static_cast(referenceItem); if (rec == m_startViewport || rec == m_endViewport) { enableToolbars(TITLE_SELECT); } else { QColor fcol = rec->pen().color(); QColor bcol = rec->brush().color(); rectFColor->setColor(fcol); QString gradientData = rec->data(TitleDocument::Gradient).toString(); if (gradientData.isEmpty()) { plain_rect->setChecked(true); rectBColor->setColor(bcol); } else { gradient_rect->setChecked(true); gradients_rect_combo->blockSignals(true); int ix = gradients_rect_combo->findData(gradientData); if (ix == -1) { storeGradient(gradientData); ix = gradients_rect_combo->findData(gradientData); } gradients_rect_combo->setCurrentIndex(ix); gradients_rect_combo->blockSignals(false); } settingUp = 0; if (rec->pen() == Qt::NoPen) { rectLineWidth->setValue(0); } else { rectLineWidth->setValue(rec->pen().width()); } enableToolbars(TITLE_RECTANGLE); } updateAxisButtons(referenceItem); updateCoordinates(rec); updateDimension(rec); } else if (referenceItem->type() == IMAGEITEM) { showToolbars(TITLE_IMAGE); updateCoordinates(referenceItem); updateDimension(referenceItem); enableToolbars(TITLE_IMAGE); } else { showToolbars(TITLE_SELECT); enableToolbars(TITLE_SELECT); frame_properties->setEnabled(false); } zValue->setValue((int)referenceItem->zValue()); if (!referenceItem->data(TitleDocument::ZoomFactor).isNull()) { itemzoom->setValue(referenceItem->data(TitleDocument::ZoomFactor).toInt()); } else { itemzoom->setValue((int)(m_transformations.value(referenceItem).scalex * 100.0 + 0.5)); } itemrotatex->setValue((int)(m_transformations.value(referenceItem).rotatex)); itemrotatey->setValue((int)(m_transformations.value(referenceItem).rotatey)); itemrotatez->setValue((int)(m_transformations.value(referenceItem).rotatez)); } effect_list->blockSignals(blockEff); itemrotatex->blockSignals(blockRX); itemrotatey->blockSignals(blockRY); itemrotatez->blockSignals(blockRZ); itemzoom->blockSignals(blockZoom); origin_x_left->blockSignals(blockOX); origin_y_top->blockSignals(blockOY); value_x->blockSignals(blockX); value_y->blockSignals(blockY); value_w->blockSignals(blockW); value_h->blockSignals(blockH); } void TitleWidget::slotEditGradient() { - QToolButton *caller = qobject_cast(QObject::sender()); + auto *caller = qobject_cast(QObject::sender()); if (!caller) { return; } QComboBox *combo = nullptr; if (caller == edit_gradient) { combo = gradients_combo; } else { combo = gradients_rect_combo; } QMap gradients; for (int i = 0; i < combo->count(); i++) { gradients.insert(combo->itemText(i), combo->itemData(i).toString()); } GradientWidget d(gradients, combo->currentIndex()); if (d.exec() == QDialog::Accepted) { // Save current gradients QMap gradMap = d.gradients(); QList icons = d.icons(); QMap::const_iterator i = gradMap.constBegin(); KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup group(config, "TitleGradients"); group.deleteGroup(); combo->clear(); gradients_rect_combo->clear(); int ix = 0; while (i != gradMap.constEnd()) { group.writeEntry(i.key(), i.value()); gradients_combo->addItem(icons.at(ix), i.key(), i.value()); gradients_rect_combo->addItem(icons.at(ix), i.key(), i.value()); ++i; ix++; } group.sync(); combo->setCurrentIndex(d.selectedGradient()); } } void TitleWidget::storeGradient(const QString &gradientData) { KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup group(config, "TitleGradients"); QMap values = group.entryMap(); int ix = qMax(1, values.count()); QString gradName = i18n("Gradient %1", ix); while (values.contains(gradName)) { ix++; gradName = i18n("Gradient %1", ix); } group.writeEntry(gradName, gradientData); group.sync(); QPixmap pix(30, 30); pix.fill(Qt::transparent); QLinearGradient gr = GradientWidget::gradientFromString(gradientData, pix.width(), pix.height()); gr.setStart(0, pix.height() / 2); gr.setFinalStop(pix.width(), pix.height() / 2); QPainter painter(&pix); painter.fillRect(0, 0, pix.width(), pix.height(), QBrush(gr)); painter.end(); QIcon icon(pix); gradients_combo->addItem(icon, gradName, gradientData); gradients_rect_combo->addItem(icon, gradName, gradientData); } void TitleWidget::loadGradients() { QMap gradients; gradients_combo->blockSignals(true); gradients_rect_combo->blockSignals(true); QString grad_data = gradients_combo->currentData().toString(); QString rect_data = gradients_rect_combo->currentData().toString(); gradients_combo->clear(); gradients_rect_combo->clear(); KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup group(config, "TitleGradients"); QMap values = group.entryMap(); if (values.isEmpty()) { // Ensure we at least always have one sample black to white gradient values.insert(i18n("Gradient"), QStringLiteral("#ffffffff;#ff000000;0;100;90")); } QMapIterator k(values); while (k.hasNext()) { k.next(); QPixmap pix(30, 30); pix.fill(Qt::transparent); QLinearGradient gr = GradientWidget::gradientFromString(k.value(), pix.width(), pix.height()); gr.setStart(0, pix.height() / 2); gr.setFinalStop(pix.width(), pix.height() / 2); QPainter painter(&pix); painter.fillRect(0, 0, pix.width(), pix.height(), QBrush(gr)); painter.end(); QIcon icon(pix); gradients_combo->addItem(icon, k.key(), k.value()); gradients_rect_combo->addItem(icon, k.key(), k.value()); } int ix = gradients_combo->findData(grad_data); if (ix >= 0) { gradients_combo->setCurrentIndex(ix); } ix = gradients_rect_combo->findData(rect_data); if (ix >= 0) { gradients_rect_combo->setCurrentIndex(ix); } gradients_combo->blockSignals(false); gradients_rect_combo->blockSignals(false); } void TitleWidget::slotUpdateShadow() { QList l = graphicsView->scene()->selectedItems(); for (int i = 0; i < graphicsView->scene()->selectedItems().length(); ++i) { MyTextItem *item = nullptr; if (l.at(i)->type() == TEXTITEM) { item = static_cast(l.at(i)); } if (!item) { // No text item, try next one. continue; } item->updateShadow(shadowBox->isChecked(), blur_radius->value(), shadowX->value(), shadowY->value(), shadowColor->color()); } } const QString TitleWidget::titleSuggest() { // Find top item to extract title proposal QList list = graphicsView->scene()->items(); int y = m_frameHeight; QString title; for (QGraphicsItem *qgItem : list) { if (qgItem->pos().y() < y && qgItem->type() == TEXTITEM) { - MyTextItem *i = static_cast(qgItem); + auto *i = static_cast(qgItem); QString currentTitle = i->toPlainText().simplified(); if (currentTitle.length() > 2) { title = currentTitle.length() > 12 ? currentTitle.left(12) + QStringLiteral("...") : currentTitle; y = qgItem->pos().y(); } } } return title; } void TitleWidget::showGuides(int state) { for (QGraphicsLineItem *it : m_guides) { it->setVisible(state == Qt::Checked); } KdenliveSettings::setTitlerShowGuides(state == Qt::Checked); } void TitleWidget::updateGuides(int) { KdenliveSettings::setTitlerHGuides(hguides->value()); KdenliveSettings::setTitlerVGuides(vguides->value()); if (!m_guides.isEmpty()) { qDeleteAll(m_guides); m_guides.clear(); } QPen framepen; QColor gColor(KdenliveSettings::titleGuideColor()); framepen.setColor(gColor); // Guides // Horizontal guides int max = hguides->value(); bool guideVisible = show_guides->checkState() == Qt::Checked; for (int i = 0; i < max; i++) { - QGraphicsLineItem *line1 = - new QGraphicsLineItem(0, (i + 1) * m_frameHeight / (max + 1), m_frameWidth, (i + 1) * m_frameHeight / (max + 1), m_frameBorder); + auto *line1 = new QGraphicsLineItem(0, (i + 1) * m_frameHeight / (max + 1), m_frameWidth, (i + 1) * m_frameHeight / (max + 1), m_frameBorder); line1->setPen(framepen); line1->setFlags(nullptr); line1->setData(-1, -1); line1->setVisible(guideVisible); m_guides << line1; } max = vguides->value(); for (int i = 0; i < max; i++) { - QGraphicsLineItem *line1 = - new QGraphicsLineItem((i + 1) * m_frameWidth / (max + 1), 0, (i + 1) * m_frameWidth / (max + 1), m_frameHeight, m_frameBorder); + auto *line1 = new QGraphicsLineItem((i + 1) * m_frameWidth / (max + 1), 0, (i + 1) * m_frameWidth / (max + 1), m_frameHeight, m_frameBorder); line1->setPen(framepen); line1->setFlags(nullptr); line1->setData(-1, -1); line1->setVisible(guideVisible); m_guides << line1; } gColor.setAlpha(160); framepen.setColor(gColor); - QGraphicsLineItem *line6 = new QGraphicsLineItem(0, 0, m_frameWidth, m_frameHeight, m_frameBorder); + auto *line6 = new QGraphicsLineItem(0, 0, m_frameWidth, m_frameHeight, m_frameBorder); line6->setPen(framepen); line6->setFlags(nullptr); line6->setData(-1, -1); line6->setVisible(guideVisible); m_guides << line6; - QGraphicsLineItem *line7 = new QGraphicsLineItem(m_frameWidth, 0, 0, m_frameHeight, m_frameBorder); + auto *line7 = new QGraphicsLineItem(m_frameWidth, 0, 0, m_frameHeight, m_frameBorder); line7->setPen(framepen); line7->setFlags(nullptr); line7->setData(-1, -1); line7->setVisible(guideVisible); m_guides << line7; } void TitleWidget::guideColorChanged(const QColor &col) { KdenliveSettings::setTitleGuideColor(col); QColor guideCol(col); for (QGraphicsLineItem *it : m_guides) { int alpha = it->pen().color().alpha(); guideCol.setAlpha(alpha); QPen framePen(guideCol); it->setPen(framePen); } } diff --git a/src/titler/titlewidget.h b/src/titler/titlewidget.h index f6d29790f..018e29137 100644 --- a/src/titler/titlewidget.h +++ b/src/titler/titlewidget.h @@ -1,382 +1,382 @@ /*************************************************************************** titlewidget.h - description ------------------- begin : Feb 28 2008 copyright : (C) 2008 by Marco Gittler email : g.marco@freenet.de ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #ifndef TITLEWIDGET_H #define TITLEWIDGET_H #include "graphicsscenerectmove.h" #include "timecode.h" #include "titler/titledocument.h" #include "titler/unicodedialog.h" #include "ui_titlewidget_ui.h" #include #include class Monitor; class KMessageWidget; class TitleTemplate { public: QString file; QString name; QIcon icon; }; class Transform { public: Transform() { scalex = 1.0; scaley = 1.0; rotatex = 0.0; rotatey = 0.0; rotatez = 0.0; } double scalex, scaley; int rotatex, rotatey, rotatez; }; /*! \class TitleWidget \brief Title creation dialog Instances of TitleWidget classes are instantiated by KdenliveDoc::slotCreateTextClip () */ class TitleWidget : public QDialog, public Ui::TitleWidget_UI { Q_OBJECT public: /** @brief Draws the dialog and loads a title document (if any). * @param url title document to load * @param tc timecode of the project * @param projectPath default path to save to or load from title documents * @param render project renderer * @param parent (optional) parent widget */ - explicit TitleWidget(const QUrl &url, const Timecode &tc, const QString &projectTitlePath, Monitor *monitor, QWidget *parent = nullptr); - virtual ~TitleWidget(); + explicit TitleWidget(const QUrl &url, const Timecode &tc, QString projectTitlePath, Monitor *monitor, QWidget *parent = nullptr); + ~TitleWidget() override; QDomDocument xml(); void setXml(const QDomDocument &doc, const QString &id = QString()); /** @brief Checks for the images referenced by a title clip. * @param xml XML data representing the title * @return list of the image files */ static QStringList extractImageList(const QString &xml); /** @brief Checks for the fonts referenced by a title clip.\n * Called by DocumentChecker::hasErrorInClips () \n * @param xml XML data representing the title * @return list of the fonts in the title */ static QStringList extractFontList(const QString &xml); /** @brief Returns clip duration. */ int duration() const; /** @brief Retrieves a list of all available title templates. */ static void refreshTitleTemplates(const QString &projectPath); /** @brief Returns a title name suggestion based on content */ const QString titleSuggest(); protected: void resizeEvent(QResizeEvent *event) override; void keyPressEvent(QKeyEvent *e) override; private: /** @brief Rectangle describing the animation start viewport. */ QGraphicsRectItem *m_startViewport; /** @brief Rectangle describing the animation end viewport. */ QGraphicsRectItem *m_endViewport; /** @brief Scene for the titler. */ GraphicsSceneRectMove *m_scene; /** @brief Initialises the animation properties (viewport size, etc.). */ void initAnimation(); QMap m_transformations; TitleDocument m_titledocument; QGraphicsRectItem *m_frameBorder; QGraphicsRectItem *m_frameBackground; QGraphicsPixmapItem *m_frameImage; int m_frameWidth; int m_frameHeight; int m_count; /** @brief Dialog for entering Unicode characters in text fields. */ UnicodeDialog *m_unicodeDialog; KMessageWidget *m_missingMessage; /** @brief Project path for storing title documents. */ QString m_projectTitlePath; Timecode m_tc; /** @brief The project framerate. */ double m_fps; /** @brief The bin id of the clip currently edited, can be empty if this is a new clip. */ QString m_clipId; QAction *m_buttonRect; QAction *m_buttonText; QAction *m_buttonImage; QAction *m_buttonCursor; QAction *m_buttonSave; QAction *m_buttonLoad; QAction *m_buttonDownload; QAction *m_unicodeAction; QAction *m_zUp; QAction *m_zDown; QAction *m_zTop; QAction *m_zBottom; QAction *m_selectAll; QAction *m_selectText; QAction *m_selectRects; QAction *m_selectImages; QAction *m_unselectAll; QString m_lastDocumentHash; QList m_guides; // See http://doc.trolltech.com/4.5/signalsandslots.html#advanced-signals-and-slots-usage. QSignalMapper *m_signalMapper; enum ValueType { ValueWidth = 1, ValueHeight = 2, ValueX = 4, ValueY = 8 }; /** @brief Sets the font weight value in the combo box. (#909) */ void setFontBoxWeight(int weight); /** @brief Stores the choices of font, background and rectangle values. */ void writeChoices(); /** @brief Reads the last stored choices into the dialog. */ void readChoices(); /** @brief Updates the displayed X/Y coordinates. */ void updateCoordinates(QGraphicsItem *i); /** @brief Updates the displayed width/height/zindex values. */ void updateDimension(QGraphicsItem *i); /** @brief Updates the displayed rotation/zoom values. Changes values of rotation/zoom GUI elements. */ void updateRotZoom(QGraphicsItem *i); /** @brief Updates the item position (position read directly from the GUI). Does not change GUI elements. */ void updatePosition(QGraphicsItem *i); /** @brief Updates the item position. Does not change GUI elements. */ void updatePosition(QGraphicsItem *i, int x, int y); void textChanged(MyTextItem *i); void updateAxisButtons(QGraphicsItem *i); void updateTextOriginX(); void updateTextOriginY(); /** @brief Enables the toolbars suiting to toolType. */ void enableToolbars(TITLETOOL toolType); /** @brief Shows the toolbars suiting to toolType. */ void showToolbars(TITLETOOL toolType); /** @brief Set up the tools suiting referenceItem */ void prepareTools(QGraphicsItem *referenceItem); /** @brief Checks a tool button. */ void checkButton(TITLETOOL toolType); void adjustFrameSize(); /** @brief Adds a "start" and "end" info text to the animation viewports. */ void addAnimInfoText(); /** @brief Updates the font for the "start" and "end" info text. */ void updateInfoText(); /** @brief Removes the "start" and "end" info text from animation viewports. */ void deleteAnimInfoText(); /** @brief Refreshes the contents of combobox based on list of title templates. */ void refreshTemplateBoxContents(); qreal maxZIndex(); /** @brief Gets the minimum/maximum Z index of items. * @param maxBound true: use maximum Z index; false: use minimum * @param intersectingOnly if true, consider only the items intersecting * with the currently selected item */ qreal zIndexBounds(bool maxBound, bool intersectingOnly); void itemRotate(int val, int axis); void selectItems(int itemType); /** @brief Appends the shortcut of a QAction to a tooltip text */ QString getTooltipWithShortcut(const QString &tipText, QAction *button); void loadGradients(); void storeGradient(const QString &gradientData); /** Open title download dialog */ void downloadTitleTemplates(); int getNewStuff(const QString &configFile); public slots: void slotNewText(MyTextItem *tt); void slotNewRect(QGraphicsRectItem *rect); void slotChangeBackground(); /** @brief Sets up the tools (toolbars etc.) according to the selected item. */ void selectionChanged(); void rectChanged(); void zIndexChanged(int); void itemScaled(int); void itemRotateX(int); void itemRotateY(int); void itemRotateZ(int); /** Save a title to a title file */ void saveTitle(QUrl url = QUrl()); /** Load a title from a title file */ void loadTitle(QUrl url = QUrl()); void slotGotBackground(const QImage &img); private slots: /** @brief Switches the origin of the X axis between left and right border. * * It's called when the origin of the X coordinate has been changed. The X * origin will either be at the left or at the right side of the frame. * * When the origin of the X axis is at the left side, the user can enter the * distance between an element's left border and the left side of the frame. * * When on the right, the distance from the right border of the frame to the * right border of the element can be entered. This would result in negative * values as long as the element's right border is at the left of the * frame's right border. As that is usually the case, I additionally invert * the X axis. * * Default value is left. * * |----l----->|#######|----r--->| * | |---w-->| | * | |#######| | * | | * |----------m_frameWidth------>| * | | * * Left selected: Value = l * Right selected: Value = r * * To calculate between the two coordinate systems: * l = m_frameWidth - w - r * r = m_frameWidth - w - l */ void slotOriginXClicked(); /** @brief Same as slotOriginXClicked(), but for the Y axis; default is top. * @ref slotOriginXClicked */ void slotOriginYClicked(); /** @brief Updates coordinates of text fields if necessary. * * It's called when something changes in the QGraphicsScene. */ void slotChanged(); /** * Reacts to changes of width/height/x/y QSpinBox values. * @brief Updates width, height, and position of the selected items. * @param valueType of type ValueType */ void slotValueChanged(int valueType); void slotZoom(bool up); void slotUpdateZoom(int pos); void slotAdjustZoom(); void slotZoomOneToOne(); void slotSelectAll(); void slotSelectText(); void slotSelectRects(); void slotSelectImages(); void slotSelectNone(); /** Called whenever text properties change (font e.g.) */ void slotUpdateText(); void slotInsertUnicode(); void slotInsertUnicodeString(const QString &string); void displayBackgroundFrame(); void setCurrentItem(QGraphicsItem *item); void slotTextTool(); void slotRectTool(); void slotSelectTool(); void slotImageTool(); void slotAnimStart(bool); void slotAnimEnd(bool); void slotKeepAspect(bool keep); void itemHCenter(); void itemVCenter(); void itemTop(); void itemBottom(); void itemLeft(); void itemRight(); void slotResize50(); void slotResize100(); void slotResize200(); /** @brief Show hide guides */ void showGuides(int state); /** @brief Build guides */ void updateGuides(int); /** @brief guide color changed, repaint */ void guideColorChanged(const QColor &col); /** @brief Called when accepted, stores user selections for next time use. * @ref writeChoices */ void slotAccepted(); /** @brief Adds an effect to an element. * @param ix index of the effect in the effects menu * * The current implementation allows for one QGraphicsEffect to be added * along with the typewriter effect. This is not clear to the user: the * stack would help, and would permit us to make more QGraphicsEffects * coexist (with different layers of QGraphicsItems). */ void slotAddEffect(int ix); void slotEditTypewriter(int ix); /** @brief Changes the Z index of objects. */ void slotZIndexUp(); void slotZIndexDown(); void slotZIndexTop(); void slotZIndexBottom(); /** Called when the user wants to apply a different template to the title */ void templateIndexChanged(int); void slotEditGradient(); void slotUpdateShadow(); /** @brief Remove missing items from the scene. */ void deleteMissingItems(); /** @brief List missing items from the scene. */ void showMissingItems(); signals: void requestBackgroundFrame(const QString &clipId, bool request); }; #endif diff --git a/src/titler/unicodedialog.cpp b/src/titler/unicodedialog.cpp index a2a700ee3..d6b738280 100644 --- a/src/titler/unicodedialog.cpp +++ b/src/titler/unicodedialog.cpp @@ -1,418 +1,418 @@ /*************************************************************************** * Copyright (C) 2008 by Simon Andreas Eugster (simon.eu@gmail.com) * * * * 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 "unicodedialog.h" #include #include #include #include #include #include #include /// CONSTANTS const int MAX_LENGTH_HEX = 4; const uint MAX_UNICODE_V1 = 65535; UnicodeDialog::UnicodeDialog(InputMethod inputMeth, QWidget *parent) : QDialog(parent) { setWindowTitle(i18n("Details")); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); auto *mainLayout = new QVBoxLayout(this); QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); m_unicodeWidget = new UnicodeWidget(inputMeth); connect(m_unicodeWidget, &UnicodeWidget::charSelected, this, &UnicodeDialog::charSelected); mainLayout->addWidget(m_unicodeWidget); mainLayout->addWidget(buttonBox); connect(okButton, &QAbstractButton::clicked, this, &UnicodeDialog::slotAccept); } -UnicodeDialog::~UnicodeDialog() {} +UnicodeDialog::~UnicodeDialog() = default; void UnicodeDialog::slotAccept() { m_unicodeWidget->slotReturnPressed(); accept(); } /// CONSTRUCTORS/DECONSTRUCTORS UnicodeWidget::UnicodeWidget(UnicodeDialog::InputMethod inputMeth, QWidget *parent) : QWidget(parent) , m_inputMethod(inputMeth) , m_lastCursorPos(0) { setupUi(this); readChoices(); showLastUnicode(); connect(unicodeNumber, &QLineEdit::textChanged, this, &UnicodeWidget::slotTextChanged); connect(unicodeNumber, &QLineEdit::returnPressed, this, &UnicodeWidget::slotReturnPressed); connect(arrowUp, &QAbstractButton::clicked, this, &UnicodeWidget::slotPrevUnicode); connect(arrowDown, &QAbstractButton::clicked, this, &UnicodeWidget::slotNextUnicode); switch (m_inputMethod) { case UnicodeDialog::InputHex: unicodeNumber->setMaxLength(MAX_LENGTH_HEX); break; case UnicodeDialog::InputDec: break; } arrowUp->setShortcut(Qt::Key_Up); arrowDown->setShortcut(Qt::Key_Down); unicode_link->setText(i18n("Information about unicode characters: http://decodeunicode.org")); arrowUp->setToolTip(i18n("Previous Unicode character (Arrow Up)")); arrowDown->setToolTip(i18n("Next Unicode character (Arrow Down)")); unicodeNumber->setToolTip(i18n("Enter your Unicode number here. Allowed characters: [0-9] and [a-f].")); unicodeNumber->selectAll(); // Selection will be reset by setToolTip and similar, so set it here } -UnicodeWidget::~UnicodeWidget() {} +UnicodeWidget::~UnicodeWidget() = default; /// METHODS void UnicodeWidget::showLastUnicode() { unicodeNumber->setText(m_lastUnicodeNumber); unicodeNumber->selectAll(); slotTextChanged(m_lastUnicodeNumber); } bool UnicodeWidget::controlCharacter(const QString &text) { bool isControlCharacter = false; QString t = text.toLower(); switch (m_inputMethod) { case UnicodeDialog::InputHex: if (t.isEmpty() || (t.length() == 1 && !(t == QLatin1String("9") || t == QLatin1String("a") || t == QLatin1String("d"))) || (t.length() == 2 && t.at(0) == QChar('1'))) { isControlCharacter = true; } break; case UnicodeDialog::InputDec: bool ok; isControlCharacter = controlCharacter(text.toUInt(&ok, 16)); break; } return isControlCharacter; } bool UnicodeWidget::controlCharacter(uint value) { bool isControlCharacter = false; if (value < 32 && !(value == 9 || value == 10 || value == 13)) { isControlCharacter = true; } return isControlCharacter; } QString UnicodeWidget::trimmedUnicodeNumber(QString text) { while (!text.isEmpty() && text.at(0) == QChar('0')) { text = text.remove(0, 1); } return text; } QString UnicodeWidget::unicodeInfo(const QString &unicode) { QString infoText(i18n("(no character selected)")); if (unicode.isEmpty()) { return infoText; } QString u = trimmedUnicodeNumber(unicode).toLower(); if (controlCharacter(u)) { infoText = i18n( "Control character. Cannot be inserted/printed. See Wikipedia:Control_character"); } else if (u == QLatin1String("a")) { infoText = i18n("Line Feed (newline character, \\\\n)"); } else if (u == QLatin1String("20")) { infoText = i18n("Standard space character. (Other space characters: U+00a0, U+2000–200b, U+202f)"); } else if (u == QLatin1String("a0")) { infoText = i18n("No-break space. &nbsp; in HTML. See U+2009 and U+0020."); } else if (u == QLatin1String("ab") || u == QLatin1String("bb") || u == QLatin1String("2039") || u == QLatin1String("203a")) { infoText = i18n("

    « (u+00ab, &lfquo; in HTML) and » (u+00bb, &rfquo; in " "HTML) are called Guillemets or angle quotes. Usage in different countries: France (with non-breaking Space 0x00a0), Switzerland, Germany, " "Finland and Sweden.

    and (U+2039/203a, &lsaquo;/&rsaquo;) are " "their single quote equivalents.

    See Wikipedia:Guillemets

    "); } else if (u == QLatin1String("2002")) { infoText = i18n("En Space (width of an n)"); } else if (u == QLatin1String("2003")) { infoText = i18n("Em Space (width of an m)"); } else if (u == QLatin1String("2004")) { infoText = i18n("Three-Per-Em Space. Width: 1/3 of one em"); } else if (u == QLatin1String("2005")) { infoText = i18n("Four-Per-Em Space. Width: 1/4 of one em"); } else if (u == QLatin1String("2006")) { infoText = i18n("Six-Per-Em Space. Width: 1/6 of one em"); } else if (u == QLatin1String("2007")) { infoText = i18n("Figure space (non-breaking). Width of a digit if digits have fixed width in this font."); } else if (u == QLatin1String("2008")) { infoText = i18n("Punctuation Space. Width the same as between a punctuation character and the next character."); } else if (u == QLatin1String("2009")) { infoText = i18n("Thin space, in HTML also &thinsp;. See U+202f and Wikipedia:Space_(punctuation)"); } else if (u == QLatin1String("200a")) { infoText = i18n("Hair Space. Thinner than U+2009."); } else if (u == QLatin1String("2019")) { infoText = i18n("Punctuation Apostrophe. Should be used instead of U+0027. See Wikipedia:Apostrophe"); } else if (u == QLatin1String("2013")) { infoText = i18n("

    An en Dash (dash of the width of an n).

    Usage examples: In English language for value ranges (1878–1903), for " "relationships/connections (Zurich–Dublin). In the German language it is also used (with spaces!) for showing thoughts: " "“Es war – wie immer in den Ferien – ein regnerischer Tag.

    See Wikipedia:Dash

    "); } else if (u == QLatin1String("2014")) { infoText = i18n("

    An em Dash (dash of the width of an m).

    Usage examples: In English language to mark—like here—thoughts. " "Traditionally without spaces.

    See Wikipedia:Dash

    "); } else if (u == QLatin1String("202f")) { infoText = i18n("

    Narrow no-break space. Has the same width as U+2009.

    Usage: For units (spaces are marked with U+2423, ␣): " "230␣V, −21␣°C, 50␣lb, but 90° (no space). In German for abbreviations (like: " "i. d. R. instead of i. d. R. with U+00a0).

    See Wikipedia:de:Schmales_Leerzeichen

    "); } else if (u == QLatin1String("2026")) { infoText = i18n("Ellipsis: If text has been left o… See Wikipedia:Ellipsis"); } else if (u == QLatin1String("2212")) { infoText = i18n("Minus sign. For numbers: −42"); } else if (u == QLatin1String("2423")) { infoText = i18n("Open box; stands for a space."); } else if (u == QLatin1String("2669")) { infoText = i18n("Quarter note (Am.) or crochet (Brit.). See Wikipedia:Quarter_note"); } else if (u == QLatin1String("266a") || u == QLatin1String("266b")) { infoText = i18n("Eighth note (Am.) or quaver (Brit.). Half as long as a quarter note (U+2669). See Wikipedia:Eighth_note"); } else if (u == QLatin1String("266c")) { infoText = i18n("Sixteenth note (Am.) or semiquaver (Brit.). Half as long as an eighth note (U+266a). See Wikipedia:Sixteenth_note"); } else if (u == QLatin1String("1D162")) { infoText = i18n("Thirty-second note (Am.) or demisemiquaver (Brit.). Half as long as a sixteenth note (U+266b). See Wikipedia:Thirty-second_note"); } else { infoText = i18n("No additional information available for this character."); } return infoText; } QString UnicodeWidget::validateText(const QString &text) { QRegExp regex("([0-9]|[a-f])", Qt::CaseInsensitive, QRegExp::RegExp2); QString newText; int pos = 0; switch (m_inputMethod) { case UnicodeDialog::InputHex: // Remove all characters we don't want while ((pos = regex.indexIn(text, pos)) != -1) { newText += regex.cap(1); pos++; } break; case UnicodeDialog::InputDec: // TODO break; } return newText; } void UnicodeWidget::updateOverviewChars(uint unicode) { QString left; QString right; uint i; for (i = 1; i <= 4; ++i) { if (unicode > i && !controlCharacter(unicode - i)) { left = QLatin1Char(' ') + left; left = QChar(unicode - i) + left; } } for (i = 1; i <= 8; ++i) { if (unicode + i <= MAX_UNICODE_V1 && !controlCharacter(unicode + i)) { right += QChar(unicode + i); right += ' '; } } leftChars->setText(left); rightChars->setText(right); } void UnicodeWidget::clearOverviewChars() { leftChars->clear(); rightChars->clear(); } QString UnicodeWidget::nextUnicode(const QString &text, Direction direction) { uint value = 0; QString newText; bool ok; switch (m_inputMethod) { case UnicodeDialog::InputHex: value = text.toUInt(&ok, 16); switch (direction) { case Backward: value--; break; default: value++; break; } // Wrapping if (value == (uint)-1) { value = MAX_UNICODE_V1; } if (value > MAX_UNICODE_V1) { value = 0; } newText.setNum(value, 16); break; case UnicodeDialog::InputDec: break; } return newText; } void UnicodeWidget::readChoices() { // Get a pointer to a shared configuration instance, then get the TitleWidget group. KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup titleConfig(config, "TitleWidget"); // Default is 2013 because there is also (perhaps interesting) information. m_lastUnicodeNumber = titleConfig.readEntry("unicode_number", QStringLiteral("2013")); } void UnicodeWidget::writeChoices() { // Get a pointer to a shared configuration instance, then get the TitleWidget group. KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup titleConfig(config, "TitleWidget"); titleConfig.writeEntry("unicode_number", m_lastUnicodeNumber); } /// SLOTS /** * \brief Validates the entered Unicode number and displays its Unicode character. */ void UnicodeWidget::slotTextChanged(const QString &text) { unicodeNumber->blockSignals(true); QString newText = validateText(text); if (newText.isEmpty()) { unicodeChar->clear(); unicodeNumber->clear(); clearOverviewChars(); m_lastCursorPos = 0; m_lastUnicodeNumber = QString(); labelInfoText->setText(unicodeInfo(QString())); } else { int cursorPos = unicodeNumber->cursorPosition(); unicodeNumber->setText(newText); unicodeNumber->setCursorPosition(cursorPos); // Get the decimal number as uint to create the QChar from bool ok; uint value = 0; switch (m_inputMethod) { case UnicodeDialog::InputHex: value = newText.toUInt(&ok, 16); break; case UnicodeDialog::InputDec: value = newText.toUInt(&ok, 10); break; } updateOverviewChars(value); if (!ok) { // Impossible! validateText never fails! } // If an invalid character has been entered: // Reset the cursor position because the entered char has been deleted. if (text != newText && newText == m_lastUnicodeNumber) { unicodeNumber->setCursorPosition(m_lastCursorPos); } m_lastCursorPos = unicodeNumber->cursorPosition(); m_lastUnicodeNumber = newText; labelInfoText->setText(unicodeInfo(newText)); unicodeChar->setText(QChar(value)); } unicodeNumber->blockSignals(false); } /** * When return pressed, we return the selected unicode character * if it was not a control character. */ void UnicodeWidget::slotReturnPressed() { unicodeNumber->setFocus(); const QString text = trimmedUnicodeNumber(unicodeNumber->text()); if (!controlCharacter(text)) { emit charSelected(unicodeChar->text()); writeChoices(); } } void UnicodeWidget::slotNextUnicode() { const QString text = unicodeNumber->text(); unicodeNumber->setText(nextUnicode(text, Forward)); } void UnicodeWidget::slotPrevUnicode() { const QString text = unicodeNumber->text(); unicodeNumber->setText(nextUnicode(text, Backward)); } void UnicodeWidget::wheelEvent(QWheelEvent *event) { if (frame->underMouse()) { if (event->delta() > 0) { slotNextUnicode(); } else { slotPrevUnicode(); } } } diff --git a/src/titler/unicodedialog.h b/src/titler/unicodedialog.h index aad9022d5..e3e03b599 100644 --- a/src/titler/unicodedialog.h +++ b/src/titler/unicodedialog.h @@ -1,95 +1,95 @@ /*************************************************************************** * Copyright (C) 2008 by Simon Andreas Eugster (simon.eu@gmail.com) * * * * 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. * ***************************************************************************/ #ifndef UNICODEDIALOG_H #define UNICODEDIALOG_H #include "ui_unicodewidget_ui.h" #include class UnicodeWidget; class UnicodeDialog : public QDialog { Q_OBJECT public: /** \brief The input method for the dialog. Atm only InputHex supported. */ enum InputMethod { InputHex, InputDec }; explicit UnicodeDialog(InputMethod inputMeth, QWidget *parent = nullptr); - ~UnicodeDialog(); + ~UnicodeDialog() override; private Q_SLOTS: void slotAccept(); Q_SIGNALS: /** \brief Contains the selected unicode character; emitted when Enter is pressed. */ void charSelected(const QString &); private: UnicodeWidget *m_unicodeWidget; }; class UnicodeWidget : public QWidget, public Ui::UnicodeWidget_UI { Q_OBJECT public: explicit UnicodeWidget(UnicodeDialog::InputMethod inputMeth, QWidget *parent = nullptr); - ~UnicodeWidget(); + ~UnicodeWidget() override; /** \brief Returns infos about a unicode number. Extendable/improvable ;) */ QString unicodeInfo(const QString &unicode); void showLastUnicode(); protected: void wheelEvent(QWheelEvent *event) override; private: enum Direction { Forward, Backward }; /** Selected input method */ UnicodeDialog::InputMethod m_inputMethod; /** \brief Validates text and removes all invalid characters (non-hex e.g.) */ QString validateText(const QString &text); /** \brief Removes all leading zeros */ QString trimmedUnicodeNumber(QString text); /** \brief Checks whether the given string is a control character */ bool controlCharacter(const QString &text); /** \brief Checks whether the given uint is a control character */ bool controlCharacter(uint value); /** \brief Returns the next available unicode. */ QString nextUnicode(const QString &text, Direction direction); /** \brief Paints previous and next characters around current char */ void updateOverviewChars(uint unicode); void clearOverviewChars(); int m_lastCursorPos; QString m_lastUnicodeNumber; /** \brief Reads the last used unicode number from the config file. */ void readChoices(); /** \brief Writes the last used unicode number into the config file. */ void writeChoices(); signals: /** \brief Contains the selected unicode character; emitted when Enter is pressed. */ void charSelected(const QString &); public slots: void slotReturnPressed(); private slots: void slotTextChanged(const QString &text); void slotNextUnicode(); void slotPrevUnicode(); }; #endif diff --git a/src/transitions/transitionlist/view/transitionlistwidget.cpp b/src/transitions/transitionlist/view/transitionlistwidget.cpp index fa7db8a0a..f9885db87 100644 --- a/src/transitions/transitionlist/view/transitionlistwidget.cpp +++ b/src/transitions/transitionlist/view/transitionlistwidget.cpp @@ -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 . * ***************************************************************************/ #include "transitionlistwidget.hpp" #include "../model/transitiontreemodel.hpp" #include "assets/assetlist/view/qmltypes/asseticonprovider.hpp" #include "dialogs/profilesdialog.h" #include "mainwindow.h" #include "transitions/transitionlist/model/transitionfilter.hpp" #include "transitions/transitionsrepository.hpp" #include #include TransitionListWidget::TransitionListWidget(QWidget *parent) : AssetListWidget(parent) { m_model = TransitionTreeModel::construct(true, this); - m_proxyModel.reset(new TransitionFilter(this)); + m_proxyModel = std::make_unique(this); m_proxyModel->setSourceModel(m_model.get()); m_proxyModel->setSortRole(AssetTreeModel::NameRole); m_proxyModel->sort(0, Qt::AscendingOrder); m_proxy = new TransitionListWidgetProxy(this); rootContext()->setContextProperty("assetlist", m_proxy); rootContext()->setContextProperty("assetListModel", m_proxyModel.get()); rootContext()->setContextProperty("isEffectList", false); m_assetIconProvider = new AssetIconProvider(false); setup(); } TransitionListWidget::~TransitionListWidget() { delete m_proxy; qDebug() << " - - -Deleting transition list widget"; } QString TransitionListWidget::getMimeType(const QString &assetId) const { if (TransitionsRepository::get()->isComposition(assetId)) { return QStringLiteral("kdenlive/composition"); } return QStringLiteral("kdenlive/transition"); } void TransitionListWidget::updateFavorite(const QModelIndex &index) { m_proxyModel->dataChanged(index, index, QVector()); m_proxyModel->reloadFilterOnFavorite(); emit reloadFavorites(); } void TransitionListWidget::setFilterType(const QString &type) { if (type == "favorites") { static_cast(m_proxyModel.get())->setFilterType(true, TransitionType::Favorites); } else { static_cast(m_proxyModel.get())->setFilterType(false, TransitionType::Favorites); } } int TransitionListWidget::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 TransitionListWidget::downloadNewLumas() { if (getNewStuff(QStringLiteral(":data/kdenlive_wipes.knsrc")) > 0) { MainWindow::refreshLumas(); // TODO: refresh currently displayd trans ? } } diff --git a/src/transitions/transitionlist/view/transitionlistwidget.hpp b/src/transitions/transitionlist/view/transitionlistwidget.hpp index 9c1be38b8..3f84c80aa 100644 --- a/src/transitions/transitionlist/view/transitionlistwidget.hpp +++ b/src/transitions/transitionlist/view/transitionlistwidget.hpp @@ -1,96 +1,96 @@ /*************************************************************************** * 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 TRANSITIONLISTWIDGET_H #define TRANSITIONLISTWIDGET_H #include "assets/assetlist/view/assetlistwidget.hpp" #include "kdenlivesettings.h" class TransitionListWidgetProxy; /* @brief This class is a widget that display the list of available effects */ class TransitionListWidget : public AssetListWidget { Q_OBJECT public: TransitionListWidget(QWidget *parent = Q_NULLPTR); - ~TransitionListWidget(); + ~TransitionListWidget() override; void setFilterType(const QString &type); /*@brief Return mime type used for drag and drop. It will be kdenlive/composition or kdenlive/transition*/ QString getMimeType(const QString &assetId) const override; void updateFavorite(const QModelIndex &index); void downloadNewLumas(); private: TransitionListWidgetProxy *m_proxy; int getNewStuff(const QString &configFile); signals: void reloadFavorites(); }; // see https://bugreports.qt.io/browse/QTBUG-57714, don't expose a QWidget as a context property class TransitionListWidgetProxy : public QObject { Q_OBJECT Q_PROPERTY(bool showDescription READ showDescription WRITE setShowDescription NOTIFY showDescriptionChanged) public: TransitionListWidgetProxy(TransitionListWidget *parent) : QObject(parent) , q(parent) { } Q_INVOKABLE QString getName(const QModelIndex &index) const { return q->getName(index); } Q_INVOKABLE bool isFavorite(const QModelIndex &index) const { return q->isFavorite(index); } Q_INVOKABLE void setFavorite(const QModelIndex &index, bool favorite) const { q->setFavorite(index, favorite, false); q->updateFavorite(index); } Q_INVOKABLE void setFilterType(const QString &type) { q->setFilterType(type); } Q_INVOKABLE QString getDescription(const QModelIndex &index) const { return q->getDescription(index); } Q_INVOKABLE QVariantMap getMimeData(const QString &assetId) const { return q->getMimeData(assetId); } Q_INVOKABLE void activate(const QModelIndex &ix) { q->activate(ix); } Q_INVOKABLE void setFilterName(const QString &pattern) { q->setFilterName(pattern); } Q_INVOKABLE QString getMimeType(const QString &assetId) const { return q->getMimeType(assetId); } Q_INVOKABLE void downloadNewLumas() { q->downloadNewLumas(); } bool showDescription() const { return KdenliveSettings::showeffectinfo(); } void setShowDescription(bool show) { KdenliveSettings::setShoweffectinfo(show); emit showDescriptionChanged(); } signals: void showDescriptionChanged(); private: TransitionListWidget *q; // NOLINT }; #endif diff --git a/src/transitions/view/transitionstackview.cpp b/src/transitions/view/transitionstackview.cpp index 71d36c561..4491b0340 100644 --- a/src/transitions/view/transitionstackview.cpp +++ b/src/transitions/view/transitionstackview.cpp @@ -1,115 +1,115 @@ /*************************************************************************** * 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 "transitionstackview.hpp" #include "assets/keyframes/model/keyframemodellist.hpp" #include "assets/model/assetparametermodel.hpp" #include "core.h" #include "monitor/monitor.h" #include #include #include #include #include #include TransitionStackView::TransitionStackView(QWidget *parent) : AssetParameterView(parent) { } void TransitionStackView::setModel(const std::shared_ptr &model, QSize frameSize, bool addSpacer) { - QHBoxLayout *lay = new QHBoxLayout; + auto *lay = new QHBoxLayout; m_trackBox = new QComboBox(this); QMapIterator i(pCore->getVideoTrackNames()); QPair aTrack = pCore->getCompositionATrack(model->getOwnerId().second); m_trackBox->addItem(i18n("Automatic"), -1); while (i.hasNext()) { i.next(); if (i.key() < aTrack.second) { m_trackBox->addItem(i.value(), i.key()); } } m_trackBox->addItem(i18n("Background"), 0); AssetParameterView::setModel(model, frameSize, addSpacer); if (!pCore->compositionAutoTrack(model->getOwnerId().second)) { m_trackBox->setCurrentIndex(m_trackBox->findData(aTrack.first)); } QLabel *title = new QLabel(i18n("Composition track: "), this); lay->addWidget(title); lay->addWidget(m_trackBox); m_lay->insertLayout(0, lay); auto kfr = model->getKeyframeModel(); if (kfr) { connect(kfr.get(), &KeyframeModelList::modelChanged, this, &AssetParameterView::slotRefresh); } connect(model.get(), &AssetParameterModel::compositionTrackChanged, this, &TransitionStackView::checkCompoTrack); connect(m_trackBox, SIGNAL(currentIndexChanged(int)), this, SLOT(updateTrack(int))); connect(this, &AssetParameterView::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 seekToTransPos(pos + clipIn); }); initKeyframeView(true); pCore->getMonitor(m_model->monitorId)->slotShowEffectScene(needsMonitorEffectScene()); m_lay->addStretch(10); slotRefresh(); } void TransitionStackView::unsetModel() { if (m_model) { disconnect(m_model.get(), &AssetParameterModel::compositionTrackChanged, this, &TransitionStackView::checkCompoTrack); auto kfr = m_model->getKeyframeModel(); if (kfr) { disconnect(kfr.get(), &KeyframeModelList::modelChanged, this, &AssetParameterView::slotRefresh); } pCore->getMonitor(m_model->monitorId)->slotShowEffectScene(MonitorSceneDefault); } AssetParameterView::unsetModel(); } void TransitionStackView::updateTrack(int newTrack) { Q_UNUSED(newTrack) qDebug() << "// Update transition TRACK to: " << m_trackBox->currentData().toInt(); pCore->setCompositionATrack(m_model->getOwnerId().second, m_trackBox->currentData().toInt()); } ObjectId TransitionStackView::stackOwner() const { if (m_model) { return m_model->getOwnerId(); } return ObjectId(ObjectType::NoItem, -1); } void TransitionStackView::checkCompoTrack() { bool autoTrack = pCore->compositionAutoTrack(m_model->getOwnerId().second); QPair aTrack = autoTrack ? QPair(-1, -1) : pCore->getCompositionATrack(m_model->getOwnerId().second); if (m_trackBox->currentData().toInt() != aTrack.first) { const QSignalBlocker blocker(m_trackBox); m_trackBox->setCurrentIndex(m_trackBox->findData(aTrack.first)); } } diff --git a/src/undohelper.cpp b/src/undohelper.cpp index ae9f8ad59..b537fa9d7 100644 --- a/src/undohelper.cpp +++ b/src/undohelper.cpp @@ -1,49 +1,49 @@ /*************************************************************************** * 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 "undohelper.hpp" #include - -FunctionalUndoCommand::FunctionalUndoCommand(const Fun &undo, const Fun &redo, const QString &text, QUndoCommand *parent) +#include +FunctionalUndoCommand::FunctionalUndoCommand(Fun undo, Fun redo, const QString &text, QUndoCommand *parent) : QUndoCommand(parent) - , m_undo(undo) - , m_redo(redo) + , m_undo(std::move(undo)) + , m_redo(std::move(redo)) , m_undone(false) { setText(text); } void FunctionalUndoCommand::undo() { // qDebug() << "UNDOING " <. * ***************************************************************************/ #ifndef UNDOHELPER_H #define UNDOHELPER_H #include using Fun = std::function; /* @brief this macro executes an operation after a given lambda */ #define PUSH_LAMBDA(operation, lambda) \ lambda = [lambda, operation]() { \ bool v = lambda(); \ return operation() && v; \ }; #include /*@brief this is a generic class that takes fonctors as undo and redo actions. It just executes them when required by Qt Note that QUndoStack actually executes redo() when we push the undoCommand to the stack This is bad for us because we execute the command as we construct the undo Function. So to prevent it to be executed twice, there is a small hack in this command that prevent redoing if it has not been undone before. */ class FunctionalUndoCommand : public QUndoCommand { public: - FunctionalUndoCommand(const Fun &undo, const Fun &redo, const QString &text, QUndoCommand *parent = nullptr); + FunctionalUndoCommand(Fun undo, Fun redo, const QString &text, QUndoCommand *parent = nullptr); void undo() override; void redo() override; private: Fun m_undo, m_redo; bool m_undone; }; #endif diff --git a/src/utils/abstractservice.h b/src/utils/abstractservice.h index 76b11b7fd..1a0cb676c 100644 --- a/src/utils/abstractservice.h +++ b/src/utils/abstractservice.h @@ -1,98 +1,98 @@ /*************************************************************************** * 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 ABSTRACTSERVICE_H #define ABSTRACTSERVICE_H #include const int imageRole = Qt::UserRole; const int urlRole = Qt::UserRole + 1; const int downloadRole = Qt::UserRole + 2; const int durationRole = Qt::UserRole + 3; const int previewRole = Qt::UserRole + 4; const int authorRole = Qt::UserRole + 5; const int authorUrl = Qt::UserRole + 6; const int infoUrl = Qt::UserRole + 7; const int infoData = Qt::UserRole + 8; const int idRole = Qt::UserRole + 9; const int licenseRole = Qt::UserRole + 10; const int descriptionRole = Qt::UserRole + 11; struct OnlineItemInfo { QString itemPreview; QString itemName; QString itemDownload; QString itemId; QString infoUrl; QString license; QString author; QString authorUrl; QString description; QString fileType; QString HQpreview; }; class AbstractService : public QObject { Q_OBJECT public: enum SERVICETYPE { NOSERVICE = 0, FREESOUND = 1, OPENCLIPART = 2, ARCHIVEORG = 3 }; explicit AbstractService(QListWidget *listWidget, QObject *parent = nullptr); - ~AbstractService(); + ~AbstractService() override; /** @brief Get file extension for currently selected item. */ virtual QString getExtension(QListWidgetItem *item); /** @brief Get recommEnded download file name. */ virtual QString getDefaultDownloadName(QListWidgetItem *item); /** @brief Does this service provide a preview (for example preview a sound. */ bool hasPreview; /** @brief Does this service provide meta info about the item. */ bool hasMetadata; /** @brief Should we show the "import" button or does this service provide download urls in info browser. */ bool inlineDownload; /** @brief The type for this service. */ SERVICETYPE serviceType; public slots: virtual void slotStartSearch(const QString &searchText, int page = 0); virtual OnlineItemInfo displayItemDetails(QListWidgetItem *item); virtual bool startItemPreview(QListWidgetItem *item); virtual void stopItemPreview(QListWidgetItem *item); protected: QListWidget *m_listWidget; signals: void searchInfo(const QString &); void maxPages(int); /** @brief Emit meta info for current item in formatted html. */ void gotMetaInfo(const QString &); /** @brief Emit some extra meta info (description, license). */ void gotMetaInfo(const QMap &info); /** @brief We have an url for current item's preview thumbnail. */ void gotThumb(const QString &url); /** @brief The requested search query is finished. */ void searchDone(); }; #endif diff --git a/src/utils/archiveorg.cpp b/src/utils/archiveorg.cpp index 362d20c9f..5a39f8079 100644 --- a/src/utils/archiveorg.cpp +++ b/src/utils/archiveorg.cpp @@ -1,383 +1,384 @@ /*************************************************************************** * 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 "archiveorg.h" #include "kdenlive_debug.h" #include #include #include #include #include #include #include "kdenlivesettings.h" #include #include #include ArchiveOrg::ArchiveOrg(QListWidget *listWidget, QObject *parent) : AbstractService(listWidget, parent) , m_previewProcess(new QProcess) { serviceType = ARCHIVEORG; hasPreview = false; // has no downloadable preview as such. We are automatically displaying the animated gif as a preview. hasMetadata = true; inlineDownload = true; // connect(m_previewProcess, SIGNAL(stateChanged(QProcess::ProcessState)), this, SLOT(slotPreviewStatusChanged(QProcess::ProcessState))); } ArchiveOrg::~ArchiveOrg() { delete m_previewProcess; } /** * @brief ArchiveOrg::slotStartSearch * @param searchText * called by ResourceWidget::slotStartSearch * @param page */ void ArchiveOrg::slotStartSearch(const QString &searchText, int page) { m_listWidget->clear(); QString uri = QStringLiteral("https://www.archive.org/advancedsearch.php?q=%28"); uri.append(searchText); uri.append(QStringLiteral("%29+AND+mediatype%3A%28movies%29")); uri.append(QStringLiteral("&fl%5B%5D=creator&fl%5B%5D=description&fl%5B%5D=identifier&fl%5B%5D=licenseurl&fl%5B%5D=title&sort%5B%5D=&sort%5B%5D=&sort%5B%" "5D=&rows=30")); //&callback=callback&save=yes" uri.append(QStringLiteral("&mediatype=movies")); // MovingImage"); if (page > 1) { uri.append(QStringLiteral("&page=") + QString::number(page)); } uri.append(QStringLiteral("&output=json")); // qCDebug(KDENLIVE_LOG)<<"ArchiveOrg URL: "<< uri; KJob *resolveJob = KIO::storedGet(QUrl(uri), KIO::NoReload, KIO::HideProgressInfo); connect(resolveJob, &KJob::result, this, &ArchiveOrg::slotShowResults); } void ArchiveOrg::slotShowResults(KJob *job) { if (job->error() != 0) { qCDebug(KDENLIVE_LOG) << "ArchiveOrg job error" << job->errorString(); return; } m_listWidget->blockSignals(true); - KIO::StoredTransferJob *storedQueryJob = static_cast(job); + auto *storedQueryJob = static_cast(job); QJsonParseError jsonError; QJsonDocument doc = QJsonDocument::fromJson(storedQueryJob->data(), &jsonError); if (jsonError.error != QJsonParseError::NoError) { // There was an error parsing data KMessageBox::sorry(m_listWidget, jsonError.errorString(), i18n("Error Loading Data")); } QVariant data = doc.toVariant(); QVariant sounds; if (data.canConvert(QVariant::Map)) { QMap map = data.toMap(); QMap::const_iterator i = map.constBegin(); while (i != map.constEnd()) { if (i.key() == QLatin1String("response")) { sounds = i.value(); if (sounds.canConvert(QVariant::Map)) { QMap soundsList = sounds.toMap(); if (soundsList.contains(QStringLiteral("numFound"))) { emit searchInfo(i18np("Found %1 result", "Found %1 results", soundsList.value("numFound").toInt())); } QList resultsList; if (soundsList.contains(QStringLiteral("docs"))) { resultsList = soundsList.value(QStringLiteral("docs")).toList(); /* docs element has a sub element for each search result. And each of the sub elements look like: {"creator":"DoubleACS", < optional "description":"Piano World", < all have this "identifier":"DoubleACS_PianoWorld0110", < all have this "licenseurl":"http:\/\/creativecommons.org\/licenses\/by-nc-nd\/3.0\/", < optional "title":"Piano World"}, < all have this */ } for (int j = 0; j < resultsList.count(); ++j) { if (resultsList.at(j).canConvert(QVariant::Map)) { QMap soundmap = resultsList.at(j).toMap(); if (soundmap.contains(QStringLiteral("title"))) { QListWidgetItem *item = new QListWidgetItem(soundmap.value(QStringLiteral("title")).toString(), m_listWidget); item->setData(descriptionRole, soundmap.value(QStringLiteral("description")).toString()); item->setData(idRole, soundmap.value(QStringLiteral("identifier")).toString()); QString author = soundmap.value(QStringLiteral("creator")).toString(); item->setData(authorRole, author); if (author.startsWith(QLatin1String("http"))) { item->setData(authorUrl, author); } item->setData(infoUrl, "http://archive.org/details/" + soundmap.value(QStringLiteral("identifier")).toString()); item->setData(downloadRole, "http://archive.org/download/" + soundmap.value(QStringLiteral("identifier")).toString()); item->setData(licenseRole, soundmap.value(QStringLiteral("licenseurl")).toString()); } } } } } ++i; } } m_listWidget->blockSignals(false); m_listWidget->setCurrentRow(0); emit searchDone(); } /** * @brief ArchiveOrg::displayItemDetails * @param item * @return * Called by ResourceWidget::slotUpdateCurrentSound() */ OnlineItemInfo ArchiveOrg::displayItemDetails(QListWidgetItem *item) { OnlineItemInfo info; m_metaInfo.clear(); if (!item) { return info; } // info.itemPreview = item->data(previewRole).toString(); info.itemDownload = item->data(downloadRole).toString(); info.itemId = item->data(idRole).toString(); info.itemName = item->text(); info.infoUrl = item->data(infoUrl).toString(); info.author = item->data(authorRole).toString(); info.authorUrl = item->data(authorUrl).toString(); info.license = item->data(licenseRole).toString(); info.description = item->data(descriptionRole).toString(); m_metaInfo.insert(QStringLiteral("url"), info.itemDownload); m_metaInfo.insert(QStringLiteral("id"), info.itemId); QString extraInfoUrl = item->data(infoUrl).toString() + QStringLiteral("&output=json"); if (!extraInfoUrl.isEmpty()) { KJob *resolveJob = KIO::storedGet(QUrl(extraInfoUrl), KIO::NoReload, KIO::HideProgressInfo); resolveJob->setProperty("id", info.itemId); connect(resolveJob, &KJob::result, this, &ArchiveOrg::slotParseResults); } return info; } /** * @brief ArchiveOrg::slotParseResults * @param job * This slot is connected to the download job started by ArchiveOrg::displayItemDetails. The download job is downloading * the extra info associated with the search item the user has clicked on. * This slot parses the extra info from the json document and creates URLs to insert into html that is displayed in the ResourceWidget * The a href url has the tag _import added at the end. This tag is removed by ResourceWidget::slotOpenLink before being used to download the file */ void ArchiveOrg::slotParseResults(KJob *job) { - KIO::StoredTransferJob *storedQueryJob = static_cast(job); + auto *storedQueryJob = static_cast(job); QJsonParseError jsonError; QJsonDocument doc = QJsonDocument::fromJson(storedQueryJob->data(), &jsonError); if (jsonError.error != QJsonParseError::NoError) { // There was an error parsing data KMessageBox::sorry(m_listWidget, jsonError.errorString(), i18n("Error Loading Extra Data")); return; } QVariant data = doc.toVariant(); QVariant files; QVariant fileMetaData; QString html = QStringLiteral(""); + html += QLatin1String(R"(
    )"); if (data.canConvert(QVariant::Map)) { QMap map = data.toMap(); QMap::const_iterator i = map.constBegin(); while (i != map.constEnd()) { // qCDebug(KDENLIVE_LOG)<<"ArchiveOrg::slotParseResults - i.key"<< i.key(); if (i.key() == QLatin1String("files")) { QString format; QString fileSize; QString sDownloadUrl; QString sPreviewUrl; QString sThumbUrl; QString minsLong; files = i.value(); if (files.canConvert(QVariant::Map)) { QMap filesList = files.toMap(); QMap::const_iterator j = filesList.constBegin(); bool bThumbNailFound = false; while (j != filesList.constEnd()) { // qCDebug(KDENLIVE_LOG)<<"ArchiveOrg::slotParseResults - file url "<< "https://archive.org/download/"+ // m_metaInfo.value(QStringLiteral("id")) + j.key(); sDownloadUrl = "https://archive.org/download/" + m_metaInfo.value(QStringLiteral("id")) + j.key(); fileMetaData = j.value(); if (fileMetaData.canConvert(QVariant::Map)) { QMap filesMetaDataList = fileMetaData.toMap(); QMap::const_iterator k = filesMetaDataList.constBegin(); while (k != filesMetaDataList.constEnd()) { // qCDebug(KDENLIVE_LOG)<< k.key()<<": "<").arg(sDownloadUrl + QStringLiteral("_import"), i18n("Import")); } // if (format==QLatin1String("Animated GIF"))// widget does not run through the frames of the animated gif if (format == QLatin1String("Thumbnail") && !bThumbNailFound) { sThumbUrl = QStringLiteral("https://archive.org/download/") + m_metaInfo.value(QStringLiteral("id")) + j.key(); // m_metaInfo.insert(QStringLiteral("preview"), sPreviewUrl); // qCDebug(KDENLIVE_LOG)<<" sPreviewUrl: "<"); if (m_metaInfo.value(QStringLiteral("id")) == job->property("id").toString()) { // qCDebug(KDENLIVE_LOG)<<"ArchiveOrg::slotParseResults html: "<tr.cellone {background-color: %1;}").arg(qApp->palette().alternateBase().color().name()); html += QLatin1String("
    ") + format + QStringLiteral(" (") + fileSize + QStringLiteral("kb ") + minsLong + QStringLiteral("min) ") + QStringLiteral("%2
    "); QString link; int ct = 0; m_thumbsPath.clear(); for (int i = 0; i < links.count(); ++i) { QString href = links.at(i).toElement().attribute(QStringLiteral("href")); qCDebug(KDENLIVE_LOG)<<"ArchiveOrg::slotParseResults"<< href; if (href.endsWith(QLatin1String(".thumbs/"))) { // sub folder contains image thumbs, display one. m_thumbsPath = m_metaInfo.value(QStringLiteral("url")) + QLatin1Char('/') + href; KJob* thumbJob = KIO::storedGet( QUrl(m_thumbsPath), KIO::NoReload, KIO::HideProgressInfo ); thumbJob->setProperty("id", m_metaInfo.value(QStringLiteral("id"))); connect(thumbJob, &KJob::result, this, &ArchiveOrg::slotParseThumbs); } else if (!href.contains(QLatin1Char('/')) && !href.endsWith(QLatin1String(".xml"))) { link = m_metaInfo.value(QStringLiteral("url")) + QLatin1Char('/') + href; ct++; if (ct %2 == 0) { html += QLatin1String(""); } else html += QLatin1String(""); html += QStringLiteral("").arg(link).arg(i18n("Preview")).arg(link + "_import").arg(i18n("Import")); } } html += QLatin1String("
    ") + QUrl(link).fileName() + QStringLiteral("%2%4
    "); if (m_metaInfo.value(QStringLiteral("id")) == job->property("id").toString()) { emit gotMetaInfo(html);// connected ResourceWidget::slotSetMetadata which updates the display with the html we pass // qCDebug(KDENLIVE_LOG)<<"ArchiveOrg::slotParseResults"<data(previewRole).toString(); if (url.isEmpty()) return false; if (m_previewProcess) { if (m_previewProcess->state() != QProcess::NotRunning) { m_previewProcess->close(); } m_previewProcess->start(KdenliveSettings::ffplaypath(), QStringList() << url << QStringLiteral("-nodisp")); } return true; } */ /* void ArchiveOrg::stopItemPreview(QListWidgetItem *item) { if (m_previewProcess && m_previewProcess->state() != QProcess::NotRunning) { m_previewProcess->close(); } } */ QString ArchiveOrg::getExtension(QListWidgetItem *item) { if (!item) { return QString(); } return QStringLiteral("*.") + item->text().section(QLatin1Char('.'), -1); } QString ArchiveOrg::getDefaultDownloadName(QListWidgetItem *item) { if (!item) { return QString(); } return item->text(); } /* void ArchiveOrg::slotParseThumbs(KJob* job) { KIO::StoredTransferJob* storedQueryJob = static_cast( job ); QDomDocument doc; doc.setContent(QString::fromUtf8(storedQueryJob->data())); QDomNodeList links = doc.elementsByTagName(QStringLiteral("a")); if (links.isEmpty()) return; for (int i = 0; i < links.count(); ++i) { QString href = links.at(i).toElement().attribute(QStringLiteral("href")); if (!href.contains(QLatin1Char('/')) && i >= links.count() / 2) { QString thumbUrl = m_thumbsPath + href; if (m_metaInfo.value(QStringLiteral("id")) == job->property("id").toString()) emit gotThumb(thumbUrl); break; } } } */ diff --git a/src/utils/archiveorg.h b/src/utils/archiveorg.h index 4add0b813..2d89a99a1 100644 --- a/src/utils/archiveorg.h +++ b/src/utils/archiveorg.h @@ -1,65 +1,65 @@ /*************************************************************************** * 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 ARCHIVEORG_H #define ARCHIVEORG_H #include "abstractservice.h" #include #include /** \brief search and download videos from archive.org This class is used to search the archive.org libraries and download videos. Is used by ResourceWidget */ class ArchiveOrg : public AbstractService { Q_OBJECT public: explicit ArchiveOrg(QListWidget *listWidget, QObject *parent = nullptr); - virtual ~ArchiveOrg(); + ~ArchiveOrg() override; QString getExtension(QListWidgetItem *item) override; QString getDefaultDownloadName(QListWidgetItem *item) override; public slots: void slotStartSearch(const QString &searchText, int page = 0) override; OnlineItemInfo displayItemDetails(QListWidgetItem *item) override; // bool startItemPreview(QListWidgetItem *item); // void stopItemPreview(QListWidgetItem *item); private slots: void slotShowResults(KJob *job); void slotParseResults(KJob *job); // void slotParseThumbs(KJob* job); private: QMap m_metaInfo; QProcess *m_previewProcess; QString m_thumbsPath; signals: void addClip(const QUrl &, const QString &); void gotPreview(const QString &url); }; #endif diff --git a/src/utils/flowlayout.h b/src/utils/flowlayout.h index 1924eb0a8..2877c0c9f 100644 --- a/src/utils/flowlayout.h +++ b/src/utils/flowlayout.h @@ -1,76 +1,76 @@ /**************************************************************************** ** ** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). ** Contact: http://www.qt-project.org/legal ** ** This file is part of the examples of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:BSD$ ** You may use this file under the terms of the BSD license as follows: ** ** "Redistribution and use in source and binary forms, with or without ** modification, are permitted provided that the following conditions are ** met: ** * Redistributions of source code must retain the above copyright ** notice, this list of conditions and the following disclaimer. ** * Redistributions in binary form must reproduce the above copyright ** notice, this list of conditions and the following disclaimer in ** the documentation and/or other materials provided with the ** distribution. ** * Neither the name of Digia Plc and its Subsidiary(-ies) nor the names ** of its contributors may be used to endorse or promote products derived ** from this software without specific prior written permission. ** ** ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #ifndef FLOWLAYOUT_H #define FLOWLAYOUT_H #include #include #include class FlowLayout : public QLayout { public: explicit FlowLayout(QWidget *parent, int margin = -1, int hSpacing = -1, int vSpacing = -1); explicit FlowLayout(int margin = -1, int hSpacing = -1, int vSpacing = -1); - ~FlowLayout(); + ~FlowLayout() override; void addItem(QLayoutItem *item) override; int horizontalSpacing() const; int verticalSpacing() const; Qt::Orientations expandingDirections() const override; bool hasHeightForWidth() const override; int heightForWidth(int) const override; int count() const override; QLayoutItem *itemAt(int index) const override; QSize minimumSize() const override; void setGeometry(const QRect &rect) override; QSize sizeHint() const override; QLayoutItem *takeAt(int index) override; private: int doLayout(const QRect &rect, bool testOnly) const; int smartSpacing(QStyle::PixelMetric pm) const; QList m_itemList; int m_hSpace; int m_vSpace; }; #endif // FLOWLAYOUT_H diff --git a/src/utils/freesound.cpp b/src/utils/freesound.cpp index c71946318..6f700862d 100644 --- a/src/utils/freesound.cpp +++ b/src/utils/freesound.cpp @@ -1,352 +1,353 @@ /*************************************************************************** * 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 "freesound.h" #include "kdenlive_debug.h" #include #include #include #include #include #include "kdenlivesettings.h" #include "qt-oauth-lib/oauth2.h" #include #include #include /** * @brief FreeSound::FreeSound * @param listWidget * @param parent */ FreeSound::FreeSound(QListWidget *listWidget, QObject *parent) : AbstractService(listWidget, parent) , m_previewProcess(new QProcess) { serviceType = FREESOUND; hasPreview = true; hasMetadata = true; connect(m_previewProcess, SIGNAL(error(QProcess::ProcessError)), this, SLOT(slotPreviewErrored(QProcess::ProcessError))); connect(m_previewProcess, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(slotPreviewFinished(int, QProcess::ExitStatus))); } FreeSound::~FreeSound() { delete m_previewProcess; } /** * @brief FreeSound::slotStartSearch * @param searchText * @param page * Called by ResourceWidget::slotStartSearch */ void FreeSound::slotStartSearch(const QString &searchText, int page) { // ver 2 of freesound API m_listWidget->clear(); QString uri = QStringLiteral("https://www.freesound.org/apiv2/search/text/?format=json&query="); uri.append(searchText); if (page > 1) { uri.append(QStringLiteral("&page=") + QString::number(page)); } uri.append(QStringLiteral("&token=") + OAuth2_strClientSecret); // qCDebug(KDENLIVE_LOG)<error() != 0) { qCDebug(KDENLIVE_LOG) << job->errorString(); } else { m_listWidget->blockSignals(true); // stop the listWidget from emitting signals.Ie clicking on the list while we are busy here will do nothing - KIO::StoredTransferJob *storedQueryJob = static_cast(job); + auto *storedQueryJob = static_cast(job); QJsonParseError jsonError; QJsonDocument doc = QJsonDocument::fromJson(storedQueryJob->data(), &jsonError); if (jsonError.error != QJsonParseError::NoError) { // There was an error parsing data KMessageBox::sorry(m_listWidget, jsonError.errorString(), i18n("Error Loading Data")); } QVariant data = doc.toVariant(); QVariant sounds; if (data.canConvert(QVariant::Map)) { QMap map = data.toMap(); QMap::const_iterator i = map.constBegin(); while (i != map.constEnd()) { if (i.key() == QLatin1String("count")) { emit searchInfo(i18np("Found %1 result", "Found %1 results", i.value().toInt())); } else if (i.key() == QLatin1String("num_pages")) { emit maxPages(i.value().toInt()); } else if (i.key() == QLatin1String("results")) { sounds = i.value(); if (sounds.canConvert(QVariant::List)) { QList soundsList = sounds.toList(); for (int j = 0; j < soundsList.count(); ++j) { if (soundsList.at(j).canConvert(QVariant::Map)) { QMap soundmap = soundsList.at(j).toMap(); if (soundmap.contains(QStringLiteral("name"))) { QListWidgetItem *item = new QListWidgetItem(soundmap.value(QStringLiteral("name")).toString(), m_listWidget); QVariant vid = soundmap.value(QStringLiteral("id")); item->setData(idRole, vid); QVariant authorInfo = soundmap.value(QStringLiteral("username")); item->setData(authorRole, authorInfo); item->setData(authorUrl, QStringLiteral("http://freesound.org/people/") + soundmap.value(QStringLiteral("username")).toString()); item->setData(licenseRole, soundmap.value(QStringLiteral("license"))); item->setData(infoData, QStringLiteral("http://www.freesound.org/apiv2/sounds/") + vid.toString() + QStringLiteral("/?format=json&token=") + OAuth2_strClientSecret); } } } } } ++i; } } m_listWidget->blockSignals(false); // enable listWidget to send signals again. It will register clicks now m_listWidget->setCurrentRow(0); emit searchDone(); } } /** * @brief FreeSound::displayItemDetails * @param item * @return * This is a slot * Called by ResourceWidget::slotUpdateCurrentSound */ OnlineItemInfo FreeSound::displayItemDetails(QListWidgetItem *item) { OnlineItemInfo info; m_metaInfo.clear(); if (!item) { return info; } info.itemPreview = item->data(previewRole).toString(); info.itemId = item->data(idRole).toString(); info.itemName = item->text(); info.infoUrl = item->data(infoUrl).toString(); info.author = item->data(authorRole).toString(); info.authorUrl = item->data(authorUrl).toString(); m_metaInfo.insert(i18n("Duration"), item->data(durationRole).toString()); info.license = item->data(licenseRole).toString(); info.description = item->data(descriptionRole).toString(); QString extraInfoUrl = item->data(infoData).toString(); if (!extraInfoUrl.isEmpty()) { KJob *resolveJob = KIO::storedGet(QUrl(extraInfoUrl), KIO::NoReload, KIO::HideProgressInfo); // connect (obj,signal,obj, slot) // when the KJob resolveJob emits a result signal slotParseResults will be notified connect(resolveJob, &KJob::result, this, &FreeSound::slotParseResults); } return info; } /** * @brief FreeSound::slotParseResults * @param job * emits gotMetaInfo which is connected to slotSetMetadata and to slotDisplayMetaInfo * Fires when displayItemDetails has finished downloading the extra info from freesound */ void FreeSound::slotParseResults(KJob *job) { - KIO::StoredTransferJob *storedQueryJob = static_cast(job); + auto *storedQueryJob = static_cast(job); QJsonParseError jsonError; QJsonDocument doc = QJsonDocument::fromJson(storedQueryJob->data(), &jsonError); if (jsonError.error != QJsonParseError::NoError) { // There was an error parsing data KMessageBox::sorry(m_listWidget, jsonError.errorString(), i18n("Error Loading Data")); } QVariant data = doc.toVariant(); QString html = QStringLiteral("").arg(qApp->palette().alternateBase().color().name()); if (data.canConvert(QVariant::Map)) { QMap info = data.toMap(); - html += QLatin1String(""); + html += QLatin1String(R"(
    )"); if (info.contains(QStringLiteral("duration"))) { html += QLatin1String(""); html += QStringLiteral(""); m_metaInfo.remove(i18n("Duration")); m_metaInfo.insert(i18n("Duration"), info.value(QStringLiteral("duration")).toString()); } if (info.contains(QStringLiteral("samplerate"))) { html += QLatin1String(""); html += QStringLiteral(""); } if (info.contains(QStringLiteral("channels"))) { html += QLatin1String(""); html += QStringLiteral(""); } if (info.contains(QStringLiteral("filesize"))) { html += QLatin1String(""); KIO::filesize_t fSize = info.value(QStringLiteral("filesize")).toDouble(); html += QStringLiteral(""); } if (info.contains(QStringLiteral("license"))) { m_metaInfo.insert(QStringLiteral("license"), info.value(QStringLiteral("license")).toString()); } html += QLatin1String("
    ") + i18n("Duration (s)") + QStringLiteral("") + QString::number(info.value(QStringLiteral("duration")).toDouble()) + QStringLiteral("
    ") + i18n("Samplerate") + QStringLiteral("") + QString::number(info.value(QStringLiteral("samplerate")).toDouble()) + QStringLiteral("
    ") + i18n("Channels") + QStringLiteral("") + QString::number(info.value(QStringLiteral("channels")).toInt()) + QStringLiteral("
    ") + i18n("File size") + QStringLiteral("") + KIO::convertSize(fSize) + QStringLiteral("
    "); if (info.contains(QStringLiteral("description"))) { m_metaInfo.insert(QStringLiteral("description"), info.value(QStringLiteral("description")).toString()); } if (info.contains(QStringLiteral("previews"))) { QMap previews = info.value(QStringLiteral("previews")).toMap(); if (previews.contains(QStringLiteral("preview-lq-mp3"))) { m_metaInfo.insert(QStringLiteral("itemPreview"), previews.value(QStringLiteral("preview-lq-mp3")).toString()); } if (previews.contains(QStringLiteral("preview-hq-mp3"))) { // Can use the HQ preview as alternative download if user does not have a freesound account m_metaInfo.insert(QStringLiteral("HQpreview"), previews.value(QStringLiteral("preview-hq-mp3")).toString()); } } if (info.contains(QStringLiteral("images"))) { QMap images = info.value(QStringLiteral("images")).toMap(); if (images.contains(QStringLiteral("waveform_m"))) { m_metaInfo.insert(QStringLiteral("itemImage"), images.value(QStringLiteral("waveform_m")).toString()); } } if (info.contains( QStringLiteral("download"))) { // this URL will start a download of the actual sound - if used in a browser while logged on to freesound m_metaInfo.insert(QStringLiteral("itemDownload"), info.value(QStringLiteral("download")).toString()); } if (info.contains(QStringLiteral("type"))) { // wav, aif, mp3 etc m_metaInfo.insert(QStringLiteral("fileType"), info.value(QStringLiteral("type")).toString()); } } emit gotMetaInfo(html); emit gotMetaInfo(m_metaInfo); emit gotThumb(m_metaInfo.value(QStringLiteral("itemImage"))); } /** * @brief FreeSound::startItemPreview * Starts playing preview version of the sound using ffplay * @param item * @return */ bool FreeSound::startItemPreview(QListWidgetItem *item) { if (!item) { return false; } const QString url = m_metaInfo.value(QStringLiteral("itemPreview")); if (url.isEmpty()) { return false; } if (m_previewProcess) { if (m_previewProcess->state() != QProcess::NotRunning) { m_previewProcess->close(); } qCDebug(KDENLIVE_LOG) << KdenliveSettings::ffplaypath() + QLatin1Char(' ') + url + QStringLiteral(" -nodisp -autoexit"); m_previewProcess->start(KdenliveSettings::ffplaypath(), QStringList() << url << QStringLiteral("-nodisp") << QStringLiteral("-autoexit")); } return true; } /** * @brief FreeSound::stopItemPreview */ void FreeSound::stopItemPreview(QListWidgetItem * /*item*/) { if ((m_previewProcess != nullptr) && m_previewProcess->state() != QProcess::NotRunning) { m_previewProcess->close(); } } /** * @brief FreeSound::getExtension * @param item * @return */ QString FreeSound::getExtension(QListWidgetItem *item) { if (!item) { return QString(); } QString sItem = item->text(); if (sItem.contains(QLatin1String("."))) { const QString sExt = sItem.section(QLatin1Char('.'), -1); return QStringLiteral("*.") + sExt; } return QString(); // return null if file name has no dots - ie no extension } /** * @brief FreeSound::getDefaultDownloadName * @param item * @return */ QString FreeSound::getDefaultDownloadName(QListWidgetItem *item) { if (!item) { return QString(); } return item->text(); } /** * @brief FreeSound::slotPreviewFinished * @param exitCode * @param exitStatus * Connected to the QProcess m_previewProcess finished signal * emits signal picked up by ResourceWidget that ResouceWidget uses * to set the Preview button back to the text Preview (it will say "Stop" before this.) */ void FreeSound::slotPreviewFinished(int exitCode, QProcess::ExitStatus exitStatus) { Q_UNUSED(exitCode); Q_UNUSED(exitStatus); emit previewFinished(); } void FreeSound::slotPreviewErrored(QProcess::ProcessError error) { qCDebug(KDENLIVE_LOG) << error; } diff --git a/src/utils/freesound.h b/src/utils/freesound.h index bdd773658..c6425644e 100644 --- a/src/utils/freesound.h +++ b/src/utils/freesound.h @@ -1,65 +1,65 @@ /*************************************************************************** * 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 FREESOUND_H #define FREESOUND_H #include "abstractservice.h" #include #include /** \brief search and download sounds from freesound This class is used to search the freesound.org libraries and download sounds. Is used by ResourceWidget */ class FreeSound : public AbstractService { Q_OBJECT public: explicit FreeSound(QListWidget *listWidget, QObject *parent = nullptr); - ~FreeSound(); + ~FreeSound() override; QString getExtension(QListWidgetItem *item) override; QString getDefaultDownloadName(QListWidgetItem *item) override; public slots: void slotStartSearch(const QString &searchText, int page = 0) override; OnlineItemInfo displayItemDetails(QListWidgetItem *item) override; bool startItemPreview(QListWidgetItem *item) override; void stopItemPreview(QListWidgetItem *item) override; private slots: void slotShowResults(KJob *job); void slotParseResults(KJob *job); void slotPreviewFinished(int exitCode, QProcess::ExitStatus exitStatus); void slotPreviewErrored(QProcess::ProcessError error); private: QMap m_metaInfo; QProcess *m_previewProcess; signals: void addClip(const QUrl &, const QString &); void previewFinished(); }; #endif diff --git a/src/utils/openclipart.h b/src/utils/openclipart.h index 45d744eed..ad84822a4 100644 --- a/src/utils/openclipart.h +++ b/src/utils/openclipart.h @@ -1,51 +1,51 @@ /*************************************************************************** * 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 OPENCLIPART_H #define OPENCLIPART_H #include "abstractservice.h" #include /** \brief search and download clip art from openclipart.org This class is used to search the openclipart.org libraries and download clipart. Is used by ResourceWidget */ class OpenClipArt : public AbstractService { Q_OBJECT public: explicit OpenClipArt(QListWidget *listWidget, QObject *parent = nullptr); - ~OpenClipArt(); + ~OpenClipArt() override; QString getExtension(QListWidgetItem *item) override; QString getDefaultDownloadName(QListWidgetItem *item) override; public slots: void slotStartSearch(const QString &searchText, int page = 0) override; OnlineItemInfo displayItemDetails(QListWidgetItem *item) override; private slots: void slotShowResults(KJob *job); }; #endif diff --git a/src/utils/resourcewidget.cpp b/src/utils/resourcewidget.cpp index b78e3271a..8d5454c27 100644 --- a/src/utils/resourcewidget.cpp +++ b/src/utils/resourcewidget.cpp @@ -1,868 +1,867 @@ /*************************************************************************** * 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 "resourcewidget.h" #include "archiveorg.h" #include "freesound.h" #include "kdenlivesettings.h" #include "openclipart.h" #include "kdenlive_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include -#include #include #ifdef QT5_USE_WEBKIT #include "qt-oauth-lib/oauth2.h" #endif -ResourceWidget::ResourceWidget(const QString &folder, QWidget *parent) +ResourceWidget::ResourceWidget(QString folder, QWidget *parent) : QDialog(parent) , m_pOAuth2(nullptr) - , m_folder(folder) + , m_folder(std::move(folder)) , m_currentService(nullptr) , m_movie(nullptr) { setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)); setupUi(this); setAttribute(Qt::WA_DeleteOnClose); m_tmpThumbFile = new QTemporaryFile; service_list->addItem(i18n("Freesound Audio Library"), AbstractService::FREESOUND); service_list->addItem(i18n("Archive.org Video Library"), AbstractService::ARCHIVEORG); service_list->addItem(i18n("Open Clip Art Graphic Library"), AbstractService::OPENCLIPART); setWindowTitle(i18n("Search Online Resources")); QPalette p = palette(); p.setBrush(QPalette::Base, p.window()); info_browser->setPalette(p); connect(button_search, &QAbstractButton::clicked, this, &ResourceWidget::slotStartSearch); connect(search_results, &QListWidget::currentRowChanged, this, &ResourceWidget::slotUpdateCurrentSound); connect(button_preview, &QAbstractButton::clicked, this, &ResourceWidget::slotPlaySound); connect(button_import, SIGNAL(clicked()), this, SLOT(slotSaveItem())); connect(item_license, SIGNAL(leftClickedUrl(QString)), this, SLOT(slotOpenUrl(QString))); connect(service_list, SIGNAL(currentIndexChanged(int)), this, SLOT(slotChangeService())); m_networkManager = new QNetworkConfigurationManager(this); if (!m_networkManager->isOnline()) { slotOnlineChanged(false); } connect(m_networkManager, &QNetworkConfigurationManager::onlineStateChanged, this, &ResourceWidget::slotOnlineChanged); connect(page_next, &QAbstractButton::clicked, this, &ResourceWidget::slotNextPage); connect(page_prev, &QAbstractButton::clicked, this, &ResourceWidget::slotPreviousPage); connect(page_number, static_cast(&QSpinBox::valueChanged), this, &ResourceWidget::slotStartSearch); connect(info_browser, &QTextBrowser::anchorClicked, this, &ResourceWidget::slotOpenLink); m_networkAccessManager = new QNetworkAccessManager(this); m_autoPlay = new QAction(i18n("Auto Play"), this); m_autoPlay->setCheckable(true); auto *resourceMenu = new QMenu; resourceMenu->addAction(m_autoPlay); config_button->setMenu(resourceMenu); config_button->setIcon(QIcon::fromTheme(QStringLiteral("configure"))); sound_box->setEnabled(false); search_text->setFocus(); connect(search_text, SIGNAL(returnPressed()), this, SLOT(slotStartSearch())); #ifdef QT5_USE_WEBKIT m_pOAuth2 = new OAuth2(this); connect(m_pOAuth2, &OAuth2::accessTokenReceived, this, &ResourceWidget::slotAccessTokenReceived); connect(m_pOAuth2, &OAuth2::accessDenied, this, &ResourceWidget::slotFreesoundAccessDenied); connect(m_pOAuth2, &OAuth2::UseHQPreview, this, &ResourceWidget::slotFreesoundUseHQPreview); connect(m_pOAuth2, &OAuth2::Canceled, this, &ResourceWidget::slotFreesoundCanceled); #endif m_currentService = new FreeSound(search_results); m_currentService->slotStartSearch(QStringLiteral("dummy"), 0); // Run a dummy search to initialise the search. // for reasons I (ttguy) can not fathom the first search that gets run fails // with The file or folder http://www.freesound.org/apiv2/search/t does not exist. // but subsequent identical search will work. With this kludge in place we do not have to click the search button // twice to get search to run slotChangeService(); loadConfig(); } /** * @brief ResourceWidget::~ResourceWidget */ ResourceWidget::~ResourceWidget() { delete m_currentService; delete m_tmpThumbFile; delete m_movie; delete m_networkAccessManager; saveConfig(); } /** * @brief ResourceWidget::loadConfig */ void ResourceWidget::loadConfig() { KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup resourceConfig(config, "ResourceWidget"); const QList size = {100, 400}; splitter->setSizes(resourceConfig.readEntry("mainSplitter", size)); } /** * @brief ResourceWidget::saveConfig */ void ResourceWidget::saveConfig() { KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup resourceConfig(config, "ResourceWidget"); resourceConfig.writeEntry(QStringLiteral("mainsplitter"), splitter->size()); config->sync(); } /** * @brief ResourceWidget::slotStartSearch * @param page * connected to the button_search clicked signal in ResourceWidget constructor * also connected to the page_number value changed signal in ResourceWidget constructor * Calls slotStartSearch on the object for the currently selected service. Ie calls * FreeSound::slotStartSearch , ArchiveOrg:slotStartSearch and OpenClipArt:slotStartSearch */ void ResourceWidget::slotStartSearch(int page) { this->setCursor(Qt::WaitCursor); info_browser->clear(); page_number->blockSignals(true); page_number->setValue(page); page_number->blockSignals(false); m_currentService->slotStartSearch(search_text->text(), page); } /** * @brief ResourceWidget::slotUpdateCurrentSound - Fires when user selects a different item in the list of found items * This is not just for sounds. It fires for clip art and videos too. */ void ResourceWidget::slotUpdateCurrentSound() { if (!m_autoPlay->isChecked()) { m_currentService->stopItemPreview(nullptr); button_preview->setText(i18n("Preview")); } item_license->clear(); m_title.clear(); GifLabel->clear(); m_desc.clear(); m_meta.clear(); QListWidgetItem *item = search_results->currentItem(); // get the item the user selected if (!item) { sound_box->setEnabled(false); // sound_box is the group of objects in the Online resources window on the RHS with the // preview and import buttons and the details about the currently selected item. // if nothing is selected then we disable all these return; } m_currentInfo = m_currentService->displayItemDetails(item); // Not so much displaying the items details // This getting the items details into m_currentInfo if (m_autoPlay->isChecked() && m_currentService->hasPreview) { m_currentService->startItemPreview(item); } sound_box->setEnabled(true); // enable the group with the preview and import buttons QString title = "

    " + m_currentInfo.itemName; // title is not just a title. It is a whole lot of HTML for displaying the // the info for the current item. // updateLayout() adds m_image,m_desc and m_meta to the title to make up the html that displays on the widget if (!m_currentInfo.infoUrl.isEmpty()) { title += QStringLiteral(" (%2)").arg(m_currentInfo.infoUrl, i18nc("the url link pointing to a web page", "link")); } title.append(QStringLiteral("

    ")); if (!m_currentInfo.authorUrl.isEmpty()) { title += QStringLiteral("").arg(m_currentInfo.authorUrl); if (!m_currentInfo.author.isEmpty()) { title.append(m_currentInfo.author); } else { title.append(i18n("Author")); } title.append(QStringLiteral("
    ")); } else if (!m_currentInfo.author.isEmpty()) { title.append(m_currentInfo.author + QStringLiteral("
    ")); } else { title.append(QStringLiteral("
    ")); } slotSetTitle(title); // updates the m_title var with the new HTML. Calls updateLayout() // that sets/ updates the html in info_browser if (!m_currentInfo.description.isEmpty()) { slotSetDescription(m_currentInfo.description); } if (!m_currentInfo.license.isEmpty()) { parseLicense(m_currentInfo.license); } } /** * @brief Downloads thumbnail file from url and saves it to tmp directory - temporyFile * * The same temp file is recycled. * Loads the image into the ResourceWidget window. * Connected to signal AbstractService::GotThumb * */ void ResourceWidget::slotLoadThumb(const QString &url) { QUrl img(url); if (img.isEmpty()) { return; } m_tmpThumbFile->close(); if (m_tmpThumbFile->open()) { KIO::FileCopyJob *copyjob = KIO::file_copy(img, QUrl::fromLocalFile(m_tmpThumbFile->fileName()), -1, KIO::HideProgressInfo | KIO::Overwrite); if (copyjob->exec()) { slotSetImage(m_tmpThumbFile->fileName()); } } } /** * @brief ResourceWidget::slotLoadPreview * @param url * Only in use by the ArchiveOrg video search. This slot starts a job to copy down the amimated GIF for use as a preview file * slotLoadAnimatedGif is called when the download finishes. * The clipart search does not have a preview option and the freesound search loads the preview file on demand via slotPlaySound() */ void ResourceWidget::slotLoadPreview(const QString &url) { QUrl gif_url(url); if (gif_url.isEmpty()) { return; } m_tmpThumbFile->close(); if (m_tmpThumbFile->open()) { KIO::FileCopyJob *copyjob = KIO::file_copy(gif_url, QUrl::fromLocalFile(m_tmpThumbFile->fileName()), -1, KIO::HideProgressInfo | KIO::Overwrite); connect(copyjob, &KJob::result, this, &ResourceWidget::slotLoadAnimatedGif); copyjob->start(); } } /** * @brief ResourceWidget::slotLoadAnimatedGif * @param job * Notified when the download of the animated gif is completed. connected via ResourceWidget::slotLoadPreview * Displays this gif in the QLabel */ void ResourceWidget::slotLoadAnimatedGif(KJob *job) { if (job->error() == 0) { delete m_movie; m_movie = new QMovie(m_tmpThumbFile->fileName()); GifLabel->clear(); GifLabel->setMovie(m_movie); // pass a pointer to a QMovie m_movie->start(); } } /** * @brief ResourceWidget::slotDisplayMetaInfo - Fires when meta info has been updated * * connected to gotMetaInfo(QMap) slot in each of the services classes (FreeSound, ArchiveOrg). Copies itemDownload * from metaInfo into m_currentInfo - used by FreeSound case because with new freesound API the * item download data is obtained from a secondary location and is populated into metaInfo */ void ResourceWidget::slotDisplayMetaInfo(const QMap &metaInfo) { if (metaInfo.contains(QStringLiteral("license"))) { parseLicense(metaInfo.value(QStringLiteral("license"))); } if (metaInfo.contains(QStringLiteral("description"))) { slotSetDescription(metaInfo.value(QStringLiteral("description"))); } if (metaInfo.contains(QStringLiteral("itemDownload"))) { m_currentInfo.itemDownload = metaInfo.value(QStringLiteral("itemDownload")); } if (metaInfo.contains(QStringLiteral("itemPreview"))) { if (m_autoPlay->isChecked()) { m_currentService->startItemPreview(search_results->currentItem()); button_preview->setText(i18n("Stop")); } } if (metaInfo.contains(QStringLiteral("fileType"))) { m_currentInfo.fileType = metaInfo.value(QStringLiteral("fileType")); } if (metaInfo.contains(QStringLiteral("HQpreview"))) { m_currentInfo.HQpreview = metaInfo.value(QStringLiteral("HQpreview")); } } /** * @brief ResourceWidget::slotPlaySound * fires when button_preview is clicked. This button is clicked again to stop the preview */ void ResourceWidget::slotPlaySound() { if (!m_currentService) { return; } QString caption; const QString sPreview = i18n("Preview"); caption = button_preview->text(); if (caption.contains(sPreview)) { const bool started = m_currentService->startItemPreview(search_results->currentItem()); if (started) { button_preview->setText(i18n("Stop")); } } else { m_currentService->stopItemPreview(search_results->currentItem()); button_preview->setText(sPreview); } } /** * @brief Fires when import button on the ResourceWidget is clicked and also called by slotOpenLink() * * Opens a dialog for user to choose a save location. * If not freesound Starts a file download job and Connects the job to slotGotFile(). If is freesound creates an OAuth2 object to connect to freesounds oauth2 authentication. The OAuth2 object is connected to a number of slots including ResourceWidget::slotAccessTokenReceived Calls OAuth2::obtainAccessToken to kick off the freesound authentication */ void ResourceWidget::slotSaveItem(const QString &originalUrl) { QUrl saveUrl; QListWidgetItem *item = search_results->currentItem(); if (!item) { return; } QString path = m_folder; QString ext; QString sFileExt; if (!path.endsWith(QLatin1Char('/'))) { path.append(QLatin1Char('/')); } if (!originalUrl.isEmpty()) { path.append(QUrl(originalUrl).fileName()); ext = "*." + QUrl(originalUrl).fileName().section(QLatin1Char('.'), -1); m_currentInfo.itemDownload = originalUrl; } else { path.append(m_currentService->getDefaultDownloadName(item)); if (m_currentService->serviceType == AbstractService::FREESOUND) { #ifdef QT5_USE_WEBKIT sFileExt = m_currentService->getExtension(search_results->currentItem()); #else sFileExt = QStringLiteral("*.") + m_currentInfo.HQpreview.section(QLatin1Char('.'), -1); #endif if (sFileExt.isEmpty()) { sFileExt = QStringLiteral("*.") + m_currentInfo.fileType; // if the file name had no extension then use the file type freesound says it is. } ext = "Audio (" + sFileExt + QStringLiteral(");;All Files(*.*)"); } else if (m_currentService->serviceType == AbstractService::OPENCLIPART) { ext = "Images (" + m_currentService->getExtension(search_results->currentItem()) + QStringLiteral(");;All Files(*.*)"); } else { ext = "Video (" + m_currentService->getExtension(search_results->currentItem()) + QStringLiteral(");;All Files(*.*)"); } } QUrl srcUrl(m_currentInfo.itemDownload); m_saveLocation = GetSaveFileNameAndPathS(path, ext); if (m_saveLocation.isEmpty()) { // user canceled save return; } if (m_currentService->serviceType != AbstractService::FREESOUND) { saveUrl = QUrl::fromLocalFile(m_saveLocation); } slotSetDescription(QString()); button_import->setEnabled(false); // disable buttons while download runs. enabled in slotGotFile #ifdef QT5_USE_WEBKIT if (m_currentService->serviceType == AbstractService::FREESOUND) { // open a dialog to authenticate with free sound and download the file m_pOAuth2->obtainAccessToken(); // when job finished ResourceWidget::slotAccessTokenReceived will be called } else { // not freesound - do file download via a KIO file copy job DoFileDownload(srcUrl, QUrl(saveUrl)); } #else saveUrl = QUrl::fromLocalFile(m_saveLocation); if (m_currentService->serviceType == AbstractService::FREESOUND) { // No OAuth, default to HQ preview srcUrl = QUrl(m_currentInfo.HQpreview); } DoFileDownload(srcUrl, QUrl(saveUrl)); #endif } /** * @brief ResourceWidget::DoFileDownload * @param srcUrl source url * @param saveUrl destination url * Called by ResourceWidget::slotSaveItem() for non freesound downloads. Called by ResourceWidget::slotFreesoundUseHQPreview() * when user chooses to use HQ preview file from freesound * Starts a file copy job to download the file. When file finishes downloading slotGotFile will be called */ void ResourceWidget::DoFileDownload(const QUrl &srcUrl, const QUrl &saveUrl) { KIO::FileCopyJob *getJob = KIO::file_copy(srcUrl, saveUrl, -1, KIO::Overwrite); KFileItem info(srcUrl); getJob->setSourceSize(info.size()); getJob->setProperty("license", item_license->text()); getJob->setProperty("licenseurl", item_license->url()); getJob->setProperty("originurl", m_currentInfo.itemDownload); if (!m_currentInfo.authorUrl.isEmpty()) { getJob->setProperty("author", m_currentInfo.authorUrl); } else if (!m_currentInfo.author.isEmpty()) { getJob->setProperty("author", m_currentInfo.author); } connect(getJob, &KJob::result, this, &ResourceWidget::slotGotFile); getJob->start(); } /** * @brief ResourceWidget::slotFreesoundUseHQPreview * Fires when user clicks the Use HQ preview button on the free sound login dialog */ void ResourceWidget::slotFreesoundUseHQPreview() { m_saveLocation = m_saveLocation + QStringLiteral(".mp3"); // HQ previews are .mp3 files - so append this to file name previously chosen if (QFile::exists(m_saveLocation)) { // check that this newly created file name file does not already exist int ret = QMessageBox::warning(this, i18n("File Exists"), i18n("HQ preview files are all mp3 files. We have added .mp3 as a file extension to the destination file name you chose. " "However, there is an existing file of this name present. \n Do you want to overwrite the existing file?. ") + QStringLiteral("\n") + m_saveLocation, QMessageBox::Yes | QMessageBox::No, QMessageBox::No); if (ret == QMessageBox::No) { button_import->setEnabled(true); return; } } const QUrl saveUrl = QUrl::fromLocalFile(m_saveLocation); QUrl srcUrl(m_currentInfo.HQpreview); DoFileDownload(srcUrl, saveUrl); } /** * @brief ResourceWidget::slotFreesoundCanceled * Fires when user cancels out of the Freesound Login dialog */ void ResourceWidget::slotFreesoundCanceled() { button_import->setEnabled(true); } /** * @brief ResourceWidget::slotGotFile - fires when the file copy job started by ResourceWidget::slotSaveItem() completes * emits addClip which causes clip to be added to the project bin. * Enables the import button * @param job */ void ResourceWidget::slotGotFile(KJob *job) { button_import->setEnabled(true); if (job->error() != 0) { const QString errTxt = job->errorString(); KMessageBox::sorry(this, errTxt, i18n("Error Loading Data")); qCDebug(KDENLIVE_LOG) << "//file import job errored: " << errTxt; return; } - KIO::FileCopyJob *copyJob = static_cast(job); + auto *copyJob = static_cast(job); const QUrl filePath = copyJob->destUrl(); KMessageBox::information(this, i18n("Resource saved to ") + filePath.toLocalFile(), i18n("Data Imported")); emit addClip(filePath, QStringList()); } /** * @brief ResourceWidget::slotOpenUrl. Opens the file on the URL using the associated application via a KRun object * * * called by slotOpenLink() so this will open .html in the users associated browser * @param url */ void ResourceWidget::slotOpenUrl(const QString &url) { new KRun(QUrl(url), this); } /** * @brief ResourceWidget::slotChangeService - fires when user changes what online resource they want to search against via the dropdown list Also fires when widget first opens */ void ResourceWidget::slotChangeService() { info_browser->clear(); delete m_currentService; m_currentService = nullptr; AbstractService::SERVICETYPE service = static_cast(service_list->itemData(service_list->currentIndex()).toInt()); if (service == AbstractService::FREESOUND) { m_currentService = new FreeSound(search_results); } else if (service == AbstractService::OPENCLIPART) { m_currentService = new OpenClipArt(search_results); } else if (service == AbstractService::ARCHIVEORG) { m_currentService = new ArchiveOrg(search_results); connect(m_currentService, SIGNAL(gotPreview(QString)), this, SLOT(slotLoadPreview(QString))); } else { return; } connect(m_currentService, SIGNAL(gotMetaInfo(QString)), this, SLOT(slotSetMetadata(QString))); connect(m_currentService, SIGNAL(gotMetaInfo(QMap)), this, SLOT(slotDisplayMetaInfo(QMap))); connect(m_currentService, &AbstractService::maxPages, this, &ResourceWidget::slotSetMaximum); connect(m_currentService, &AbstractService::searchInfo, search_info, &QLabel::setText); connect(m_currentService, &AbstractService::gotThumb, this, &ResourceWidget::slotLoadThumb); connect(m_currentService, &AbstractService::searchDone, this, &ResourceWidget::slotSearchFinished); if (m_currentService->hasPreview) { connect(m_currentService, SIGNAL(previewFinished()), this, SLOT(slotPreviewFinished())); } button_preview->setVisible(m_currentService->hasPreview); button_import->setVisible(!m_currentService->inlineDownload); search_info->setText(QString()); if (!search_text->text().isEmpty()) { slotStartSearch(); // automatically kick of a search if we have search text and we switch services. } } void ResourceWidget::slotSearchFinished() { this->setCursor(Qt::ArrowCursor); } /** * @brief ResourceWidget::slotSetMaximum * @param max */ void ResourceWidget::slotSetMaximum(int max) { page_number->setMaximum(max); } /** * @brief ResourceWidget::slotOnlineChanged * @param online */ void ResourceWidget::slotOnlineChanged(bool online) { button_search->setEnabled(online); search_info->setText(online ? QString() : i18n("You need to be online\n for searching")); } /** * @brief ResourceWidget::slotNextPage */ void ResourceWidget::slotNextPage() { const int ix = page_number->value(); if (search_results->count() > 0) { page_number->setValue(ix + 1); } } /** * @brief ResourceWidget::slotPreviousPage */ void ResourceWidget::slotPreviousPage() { const int ix = page_number->value(); if (ix > 1) { page_number->setValue(ix - 1); } } /** * @brief ResourceWidget::parseLicense provides a name for the licence based on the license URL * called by ResourceWidget::slotDisplayMetaInfo and by ResourceWidget::slotUpdateCurrentSound * @param licenseUrl */ void ResourceWidget::parseLicense(const QString &licenseUrl) { QString licenseName; // TODO translate them ? if (licenseUrl.contains(QStringLiteral("/sampling+/"))) { licenseName = QStringLiteral("Sampling+"); } else if (licenseUrl.contains(QStringLiteral("/by/"))) { licenseName = QStringLiteral("Attribution"); } else if (licenseUrl.contains(QStringLiteral("/by-nd/"))) { licenseName = QStringLiteral("Attribution-NoDerivs"); } else if (licenseUrl.contains(QStringLiteral("/by-nc-sa/"))) { licenseName = QStringLiteral("Attribution-NonCommercial-ShareAlike"); } else if (licenseUrl.contains(QStringLiteral("/by-sa/"))) { licenseName = QStringLiteral("Attribution-ShareAlike"); } else if (licenseUrl.contains(QStringLiteral("/by-nc/"))) { licenseName = QStringLiteral("Attribution-NonCommercial"); } else if (licenseUrl.contains(QStringLiteral("/by-nc-nd/"))) { licenseName = QStringLiteral("Attribution-NonCommercial-NoDerivs"); } else if (licenseUrl.contains(QLatin1String("/publicdomain/zero/"))) { licenseName = QStringLiteral("Creative Commons 0"); } else if (licenseUrl.endsWith(QLatin1String("/publicdomain")) || licenseUrl.contains(QLatin1String("openclipart.org/share"))) { licenseName = QStringLiteral("Public Domain"); } else { licenseName = i18n("Unknown"); } item_license->setText(licenseName); item_license->setUrl(licenseUrl); } /** * @brief ResourceWidget::slotOpenLink. Fires when Url in the resource wizard is clicked * @param url * Connected to anchorClicked(). If the url ends in _import it downloads the file at the end of the url via slotSaveItem(). * We have created URLs deliberately tagged with _import in ArchiveOrg::slotParseResults. If the URL is tagged in this way we remove the tag * and download the file. * Otherwise it opens the URL via slotOpenUrl() which opens the URL in the systems default web browser */ void ResourceWidget::slotOpenLink(const QUrl &url) { QString path = url.toEncoded(); if (path.endsWith(QLatin1String("_import"))) { // path.chop(7); // import file in Kdenlive slotSaveItem(path); } else { slotOpenUrl(path); } } /** * @brief ResourceWidget::slotSetDescription Updates the display with the description text * @param desc /n * The description is either the detailed description of the file or is progress messages on the download process * */ void ResourceWidget::slotSetDescription(const QString &desc) { if (m_desc != desc) { m_desc = desc; updateLayout(); } } /** * @brief ResourceWidget::slotSetMetadata updates the display with the metadata. * @param desc /n * The meta data is info on the sounds length, samplerate, filesize and number of channels. This is called when gotMetaInfo(Qstring) signal fires. That signal * is passing html in the parameter * This function is updating the html (m_meta) in the ResourceWidget and then calls updateLayout() * which updates actual widget */ void ResourceWidget::slotSetMetadata(const QString &metadata) { if (m_meta != metadata) { m_meta = metadata; updateLayout(); } } /** * @brief ResourceWidget::slotSetImage Sets a thumbnail on the widget * @param desc * called by ResourceWidget::slotLoadThumb \n * This sets a thumb nail image onto a label on the resource widget. If it is a animated .gif it will play */ void ResourceWidget::slotSetImage(const QString &desc) { QPixmap pic(desc); GifLabel->setPixmap(pic); // pass a pointer as a parameter. Display the pic in our label } /** @brief updates the display with information on the selected item. The title consists of the sounds file name and the author * * Called by ResourceWidget::slotUpdateCurrentSound() */ void ResourceWidget::slotSetTitle(const QString &title) { if (m_title != title) { m_title = title; updateLayout(); } } /** * @brief ResourceWidget::updateLayout * This concats the html in m_title, m_desc and m_meta and sets the resulting * html markup into the content of the ResourceWidget \n * Called by slotSetTitle() , slotSetMetadata() ,slotSetDescription() */ void ResourceWidget::updateLayout() { QString content = m_title; if (!m_desc.isEmpty()) { content.append(m_desc); } if (!m_meta.isEmpty()) { content.append(m_meta); } info_browser->setHtml(content); } /** * @brief ResourceWidget::slotPreviewFinished * connected to FreeSound previewFinished signal */ void ResourceWidget::slotPreviewFinished() { button_preview->setText(i18n("Preview")); } /** * @brief ResourceWidget::slotFreesoundAccessDenied * Will fire if freesound denies access - eg wrong password entered. */ void ResourceWidget::slotFreesoundAccessDenied() { button_import->setEnabled(true); info_browser->setHtml(QStringLiteral("") + i18n("Access Denied from Freesound. Have you authorised the Kdenlive application on your freesound account?") + QStringLiteral("")); } /** * @brief ResourceWidget::slotAccessTokenReceived * @param sAccessToken - the access token obtained from freesound website \n * Connected to OAuth2::accessTokenReceived signal in ResourceWidget constructor. * Fires when the OAuth2 object has obtained an access token. This slot then goes ahead * and starts the download of the requested file. ResourceWidget::DownloadRequestFinished will be * notified when that job finishes */ void ResourceWidget::slotAccessTokenReceived(const QString &sAccessToken) { // qCDebug(KDENLIVE_LOG) << "slotAccessTokenReceived: " <") + i18n("Starting File Download") + QStringLiteral("
    "); updateLayout(); QNetworkReply *reply2 = m_networkAccessManager->get(request); connect(reply2, &QIODevice::readyRead, this, &ResourceWidget::slotReadyRead); connect(m_networkAccessManager, &QNetworkAccessManager::finished, this, &ResourceWidget::DownloadRequestFinished); } else { m_meta = QString(); m_desc = QStringLiteral("
    ") + i18n("Error Getting Access Token from Freesound.") + QStringLiteral(""); m_desc.append(QStringLiteral("
    ") + i18n("Try importing again to obtain a new freesound connection") + QStringLiteral("")); updateLayout(); } } /** * @brief ResourceWidget::GetSaveFileNameAndPathS * @param path - starting path where dialog will open on * @param ext - file extension filter to have in play on the dialog * @return QString of the chosen path * Prompts user to choose a file name and path as to where to save a file. * Returns a QString of the path and file name or empty string if user cancels */ QString ResourceWidget::GetSaveFileNameAndPathS(const QString &path, const QString &ext) { QString saveUrlstring = QFileDialog::getSaveFileName(this, QString(), path, ext); if (saveUrlstring.isEmpty()) // only check if the save url is empty (ie if user cancels the save.) // If the URL has no file at the end we trap this error in slotGotFile. { return saveUrlstring; } if (QFile::exists(saveUrlstring)) { int ret = QMessageBox::warning(this, i18n("File Exists"), i18n("Do you want to overwrite the existing file?"), QMessageBox::Yes | QMessageBox::No, QMessageBox::No); if (ret == QMessageBox::No) { return QString(); } } return saveUrlstring; } /** * @brief ResourceWidget::slotReadyRead * Fires each time the download of the freesound file grabs some more data. * Prints dots to the dialog indicating download is progressing. */ void ResourceWidget::slotReadyRead() { m_desc.append(QLatin1Char('.')); updateLayout(); } /** * @brief ResourceWidget::DownloadRequestFinished * @param reply * Fires when the download of the freesound file completes. * If the download was successful this saves the data from memory to the file system. Emits an ResourceWidget::addClip signal. * MainWindow::slotDownloadResources() links this signal to MainWindow::slotAddProjectClip * If the download has failed with AuthenticationRequiredError then it requests a new access token via the refresh token method * and then the download process will retry. * If the download fails for other reasons this reports an error and clears out the access token from memory. * If the user requests the file import again it will request a login to free sound again and the download might succeed on this second try */ void ResourceWidget::DownloadRequestFinished(QNetworkReply *reply) { button_import->setEnabled(true); if (reply->isFinished()) { if (reply->error() == QNetworkReply::NoError) { QByteArray aSoundData = reply->readAll(); QFile file(m_saveLocation); if (file.open(QIODevice::WriteOnly)) { file.write(aSoundData); file.close(); KMessageBox::information(this, i18n("Resource saved to ") + m_saveLocation, i18n("Data Imported")); emit addClip(QUrl(m_saveLocation), QStringList()); // MainWindow::slotDownloadResources() links this signal to MainWindow::slotAddProjectClip m_desc.append(QStringLiteral("
    ") + i18n("Saved file to") + QStringLiteral("
    ")); m_desc.append(m_saveLocation); updateLayout(); } else { #ifdef QT5_USE_WEBKIT m_pOAuth2->ForgetAccessToken(); #endif m_desc.append(QStringLiteral("
    ") + i18n("Error Saving File")); updateLayout(); } } else { if (reply->error() == QNetworkReply::AuthenticationRequiredError) { #ifdef QT5_USE_WEBKIT m_pOAuth2->obtainNewAccessToken(); #endif } else { #ifdef QT5_USE_WEBKIT m_pOAuth2->ForgetAccessToken(); #endif m_desc.append(QStringLiteral("
    ") + i18n("Error Downloading File. Error code: ") + reply->error() + QStringLiteral("
    ")); m_desc.append(QStringLiteral("
    ") + i18n("Try importing again to obtain a new freesound connection") + QStringLiteral("")); updateLayout(); } } } reply->deleteLater(); } diff --git a/src/utils/resourcewidget.h b/src/utils/resourcewidget.h index 8984e8650..e0ebed0e7 100644 --- a/src/utils/resourcewidget.h +++ b/src/utils/resourcewidget.h @@ -1,116 +1,116 @@ /*************************************************************************** * 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 RESOURCEWIDGET_H #define RESOURCEWIDGET_H #include "abstractservice.h" #include "definitions.h" #include "ui_freesound_ui.h" #include #include #include class QAction; class QNetworkConfigurationManager; class QTemporaryFile; class QMovie; class OAuth2; /** \brief This is the window that appears from Project>Online Resources * In the Online Resources window the user can choose from different online resources such as Freesound Audio Library, Archive.org Video * Library and Open clip Art Graphic Library. * Depending on which of these is selected the resourcewidget will instantiate a FreeSound, ArchiveOrg or OpenClipArt * class that will deal with the searching and parsing of the results for the different on line resource libraries */ class ResourceWidget : public QDialog, public Ui::FreeSound_UI { Q_OBJECT public: - explicit ResourceWidget(const QString &folder, QWidget *parent = nullptr); - ~ResourceWidget(); + explicit ResourceWidget(QString folder, QWidget *parent = nullptr); + ~ResourceWidget() override; private slots: void slotStartSearch(int page = 0); /** * @brief Fires when user selects a different item in the list of found items * * This is not just for sounds. It fires for clip art and videos too. */ void slotUpdateCurrentSound(); void slotPlaySound(); void slotDisplayMetaInfo(const QMap &metaInfo); void slotSaveItem(const QString &originalUrl = QString()); void slotOpenUrl(const QString &url); void slotChangeService(); void slotOnlineChanged(bool online); void slotNextPage(); void slotPreviousPage(); void slotOpenLink(const QUrl &url); void slotLoadThumb(const QString &url); /** @brief A file download is finished */ void slotGotFile(KJob *job); void slotSetMetadata(const QString &metadata); void slotSetDescription(const QString &desc); void slotSetImage(const QString &desc); void slotSetTitle(const QString &title); void slotSetMaximum(int max); void slotPreviewFinished(); void slotFreesoundAccessDenied(); void slotReadyRead(); void DownloadRequestFinished(QNetworkReply *reply); void slotAccessTokenReceived(const QString &sAccessToken); void slotFreesoundUseHQPreview(); void slotFreesoundCanceled(); void slotSearchFinished(); void slotLoadPreview(const QString &url); void slotLoadAnimatedGif(KJob *job); private: OAuth2 *m_pOAuth2; QNetworkConfigurationManager *m_networkManager; QNetworkAccessManager *m_networkAccessManager; void loadConfig(); void saveConfig(); void parseLicense(const QString &); QString GetSaveFileNameAndPathS(const QString &path, const QString &ext); QString m_folder; QString m_saveLocation; AbstractService *m_currentService; OnlineItemInfo m_currentInfo; QAction *m_autoPlay; QTemporaryFile *m_tmpThumbFile; QString m_title; QString m_desc; QString m_meta; QMovie *m_movie; void updateLayout(); void DoFileDownload(const QUrl &srcUrl, const QUrl &saveUrl); signals: void addClip(const QUrl &, const QStringList &); }; #endif diff --git a/src/widgets/choosecolorwidget.h b/src/widgets/choosecolorwidget.h index 86eedabce..7e6a706c7 100644 --- a/src/widgets/choosecolorwidget.h +++ b/src/widgets/choosecolorwidget.h @@ -1,69 +1,69 @@ /*************************************************************************** * Copyright (C) 2010 by Till Theato (root@ttill.de) * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * 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 CHOOSECOLORWIDGET_H #define CHOOSECOLORWIDGET_H #include class KColorButton; /** * @class ChooseColorWidget * @brief Provides options to choose a color. Two mechanisms are provided: color-picking directly on the screen and choosing from a list * @author Till Theato */ class ChooseColorWidget : public QWidget { Q_OBJECT public: /** @brief Sets up the widget. * @param name (optional) What the color will be used for (name of the parameter) * @param color (optional) initial color * @param comment (optional) Comment about the parameter * @param alphaEnabled (optional) Should transparent colors be enabled * @param parent(optional) Parent widget */ explicit ChooseColorWidget(const QString &name = QString(), const QString &color = QStringLiteral("0xffffffff"), const QString &comment = QString(), - bool alphaEnabled = false, QWidget *parent = 0); + bool alphaEnabled = false, QWidget *parent = nullptr); /** @brief Gets the chosen color. */ QString getColor() const; private: KColorButton *m_button; public slots: void slotColorModified(const QColor &color); private slots: /** @brief Updates the different color choosing options to have all selected @param color. */ void setColor(const QColor &color); signals: /** @brief Emitted whenever a different color was chosen. */ void modified(QColor = QColor()); void disableCurrentFilter(bool); void valueChanged(); }; #endif diff --git a/src/widgets/colorpickerwidget.cpp b/src/widgets/colorpickerwidget.cpp index 11fdacbad..ba8e45076 100644 --- a/src/widgets/colorpickerwidget.cpp +++ b/src/widgets/colorpickerwidget.cpp @@ -1,253 +1,253 @@ /*************************************************************************** * Copyright (C) 2010 by Till Theato (root@ttill.de) * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * 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 "colorpickerwidget.h" #include #include #include #include #include #include #include #include #include #ifdef Q_WS_X11 #include #include #endif MyFrame::MyFrame(QWidget *parent) : QFrame(parent) { setFrameStyle(QFrame::Box | QFrame::Plain); setWindowOpacity(0.5); setWindowFlags(Qt::FramelessWindowHint); } // virtual void MyFrame::hideEvent(QHideEvent *event) { QFrame::hideEvent(event); // We need a timer here since hiding the frame will trigger a monitor refresh timer that will // repaint the monitor after 70 ms. QTimer::singleShot(250, this, &MyFrame::getColor); } ColorPickerWidget::ColorPickerWidget(QWidget *parent) : QWidget(parent) - , m_filterActive(false) + { #ifdef Q_WS_X11 m_image = nullptr; #endif auto *layout = new QHBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); auto *button = new QToolButton(this); button->setIcon(QIcon::fromTheme(QStringLiteral("color-picker"))); button->setToolTip(QStringLiteral("

    ") + i18n("Pick a color on the screen. By pressing the mouse button and then moving your mouse you can select a " "section of the screen from which to get an average color.") + QStringLiteral("

    ")); button->setAutoRaise(true); connect(button, &QAbstractButton::clicked, this, &ColorPickerWidget::slotSetupEventFilter); layout->addWidget(button); setFocusPolicy(Qt::StrongFocus); m_grabRectFrame = new MyFrame(); m_grabRectFrame->hide(); } ColorPickerWidget::~ColorPickerWidget() { delete m_grabRectFrame; if (m_filterActive) { removeEventFilter(this); } } void ColorPickerWidget::slotGetAverageColor() { disconnect(m_grabRectFrame, SIGNAL(getColor()), this, SLOT(slotGetAverageColor())); m_grabRect = m_grabRect.normalized(); int numPixel = m_grabRect.width() * m_grabRect.height(); int sumR = 0; int sumG = 0; int sumB = 0; /* Only getting the image once for the whole rect results in a vast speed improvement. */ #ifdef Q_WS_X11 Window root = RootWindow(QX11Info::display(), QX11Info::appScreen()); m_image = XGetImage(QX11Info::display(), root, m_grabRect.x(), m_grabRect.y(), m_grabRect.width(), m_grabRect.height(), -1, ZPixmap); #else QScreen *currentScreen = QApplication::primaryScreen(); if (currentScreen) { m_image = currentScreen->grabWindow(0, m_grabRect.x(), m_grabRect.y(), m_grabRect.width(), m_grabRect.height()).toImage(); } #endif for (int x = 0; x < m_grabRect.width(); ++x) { for (int y = 0; y < m_grabRect.height(); ++y) { QColor color = grabColor(QPoint(x, y), false); sumR += color.red(); sumG += color.green(); sumB += color.blue(); } } #ifdef Q_WS_X11 XDestroyImage(m_image); m_image = nullptr; #endif emit colorPicked(QColor(sumR / numPixel, sumG / numPixel, sumB / numPixel)); emit disableCurrentFilter(false); } void ColorPickerWidget::mousePressEvent(QMouseEvent *event) { if (event->button() != Qt::LeftButton) { closeEventFilter(); emit disableCurrentFilter(false); event->accept(); return; } if (m_filterActive) { m_grabRect = QRect(event->globalPos(), QSize(1, 1)); m_grabRectFrame->setGeometry(m_grabRect); m_grabRectFrame->show(); } } void ColorPickerWidget::mouseReleaseEvent(QMouseEvent *event) { if (m_filterActive) { closeEventFilter(); m_grabRect.setWidth(event->globalX() - m_grabRect.x()); m_grabRect.setHeight(event->globalY() - m_grabRect.y()); m_grabRect = m_grabRect.normalized(); if (m_grabRect.width() * m_grabRect.height() == 0) { m_grabRectFrame->hide(); emit colorPicked(grabColor(event->globalPos())); emit disableCurrentFilter(false); } else { // delay because m_grabRectFrame does not hide immediately connect(m_grabRectFrame, SIGNAL(getColor()), this, SLOT(slotGetAverageColor())); m_grabRectFrame->hide(); } return; } QWidget::mouseReleaseEvent(event); } void ColorPickerWidget::mouseMoveEvent(QMouseEvent *event) { if (m_filterActive) { m_grabRect.setWidth(event->globalX() - m_grabRect.x()); m_grabRect.setHeight(event->globalY() - m_grabRect.y()); m_grabRectFrame->setGeometry(m_grabRect.normalized()); } } void ColorPickerWidget::slotSetupEventFilter() { emit disableCurrentFilter(true); m_filterActive = true; setFocus(); installEventFilter(this); grabMouse(QCursor(QIcon::fromTheme(QStringLiteral("color-picker")).pixmap(32, 32), 16, 2)); grabKeyboard(); } void ColorPickerWidget::closeEventFilter() { m_filterActive = false; releaseMouse(); releaseKeyboard(); removeEventFilter(this); } bool ColorPickerWidget::eventFilter(QObject *object, QEvent *event) { // Close color picker on any key press if (event->type() == QEvent::KeyPress || event->type() == QEvent::ShortcutOverride) { closeEventFilter(); emit disableCurrentFilter(false); event->setAccepted(true); return true; } return QObject::eventFilter(object, event); } QColor ColorPickerWidget::grabColor(const QPoint &p, bool destroyImage) { #ifdef Q_WS_X11 /* we use the X11 API directly in this case as we are not getting back a valid return from QPixmap::grabWindow in the case where the application is using an argb visual */ if (!qApp->desktop()->geometry().contains(p)) { return QColor(); } unsigned long xpixel; if (m_image == nullptr) { Window root = RootWindow(QX11Info::display(), QX11Info::appScreen()); m_image = XGetImage(QX11Info::display(), root, p.x(), p.y(), 1, 1, -1, ZPixmap); xpixel = XGetPixel(m_image, 0, 0); } else { xpixel = XGetPixel(m_image, p.x(), p.y()); } if (destroyImage) { XDestroyImage(m_image); m_image = 0; } XColor xcol; xcol.pixel = xpixel; xcol.flags = DoRed | DoGreen | DoBlue; XQueryColor(QX11Info::display(), DefaultColormap(QX11Info::display(), QX11Info::appScreen()), &xcol); return QColor::fromRgbF(xcol.red / 65535.0, xcol.green / 65535.0, xcol.blue / 65535.0); #else Q_UNUSED(destroyImage) if (m_image.isNull()) { QScreen *currentScreen = QApplication::primaryScreen(); if (currentScreen) { QPixmap pm = currentScreen->grabWindow(0, p.x(), p.y(), 1, 1); QImage i = pm.toImage(); return i.pixel(0, 0); } return qRgb(0, 0, 0); } return m_image.pixel(p.x(), p.y()); #endif } diff --git a/src/widgets/colorpickerwidget.h b/src/widgets/colorpickerwidget.h index 77608b47c..d5beab0b1 100644 --- a/src/widgets/colorpickerwidget.h +++ b/src/widgets/colorpickerwidget.h @@ -1,102 +1,102 @@ /*************************************************************************** * Copyright (C) 2010 by Till Theato (root@ttill.de) * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * 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 COLORPICKERWIDGET_H #define COLORPICKERWIDGET_H #include #include class QFrame; #ifdef Q_WS_X11 #include #endif class MyFrame : public QFrame { Q_OBJECT public: explicit MyFrame(QWidget *parent = nullptr); protected: void hideEvent(QHideEvent *event) override; signals: void getColor(); }; /** * @class ColorPickerWidget * @brief A widget to pick a color anywhere on the screen. * @author Till Theato * * The code is partially based on the color picker in KColorDialog. */ class ColorPickerWidget : public QWidget { Q_OBJECT public: /** @brief Sets up the widget. */ explicit ColorPickerWidget(QWidget *parent = nullptr); /** @brief Makes sure the event filter is removed. */ - virtual ~ColorPickerWidget(); + ~ColorPickerWidget() override; protected: void mousePressEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override; bool eventFilter(QObject *object, QEvent *event) override; private: /** @brief Closes the event filter and makes mouse and keyboard work again on other widgets/windows. */ void closeEventFilter(); /** @brief Color of the screen at point @param p. * @param p Position of color requested * @param destroyImage (optional) Whether or not to keep the XImage in m_image (needed for fast processing of rects) */ QColor grabColor(const QPoint &p, bool destroyImage = true); - bool m_filterActive; + bool m_filterActive{false}; QRect m_grabRect; QFrame *m_grabRectFrame; #ifdef Q_WS_X11 XImage *m_image; #else QImage m_image; #endif private slots: /** @brief Sets up an event filter for picking a color. */ void slotSetupEventFilter(); /** @brief Calculates the average color for the pixels in the rect m_grabRect and emits colorPicked. */ void slotGetAverageColor(); signals: /** @brief Signal fired when a new color has been picked */ void colorPicked(const QColor &); /** @brief When user wants to pick a color, it's better to disable filter so we get proper color values. */ void disableCurrentFilter(bool); }; #endif diff --git a/src/widgets/doublewidget.h b/src/widgets/doublewidget.h index bc881de15..3cc3d0b75 100644 --- a/src/widgets/doublewidget.h +++ b/src/widgets/doublewidget.h @@ -1,87 +1,87 @@ /*************************************************************************** * Copyright (C) 2010 by Till Theato (root@ttill.de) * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * 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 DOUBLEWIDGET_H #define DOUBLEWIDGET_H #include class DragValue; /** * @class DoubleWidget * @brief Widget to choose a double parameter (for a effect) with the help of a slider and a spinbox. * @author Till Theato * * The widget does only handle integers, so the parameter has to be converted into the proper double range afterwards. */ class DoubleWidget : public QWidget { Q_OBJECT public: /** @brief Sets up the parameter's GUI. * @param name Name of the parameter * @param value Value of the parameter * @param min Minimum value * @param max maximum value * @param factor value, if we want a range 0-1000 for a 0-1 parameter, we can set a factor of 1000 * @param defaultValue Value used when using reset functionality * @param comment A comment explaining the parameter. Will be shown in a tooltip. * @param suffix (optional) Suffix to display in spinbox * @param parent (optional) Parent Widget */ explicit DoubleWidget(const QString &name, double value, double min, double max, double factor, double defaultValue, const QString &comment, int id, const QString &suffix = QString(), int decimals = 0, QWidget *parent = nullptr); - ~DoubleWidget(); + ~DoubleWidget() override; /** @brief Gets the parameter's value. */ double getValue(); /** @brief Returns minimum size for QSpinBox, used to set all spinboxes to the same width. */ int spinSize(); void setSpinSize(int width); void enableEdit(bool enable); /** @brief Returns true if widget is currently being edited */ bool hasEditFocus() const; public slots: /** @brief Sets the value to @param value. */ void setValue(double value); /** @brief Sets value to m_default. */ void slotReset(); /** @brief Shows/Hides the comment label. */ void slotShowComment(bool show); private slots: void slotSetValue(double value, bool final); private: DragValue *m_dragVal; double m_factor; signals: void valueChanged(double); // same signal as valueChanged, but add an extra boolean to tell if user is dragging value or not void valueChanging(double, bool); }; #endif diff --git a/src/widgets/dragvalue.cpp b/src/widgets/dragvalue.cpp index f6d111420..cf3d65ab9 100644 --- a/src/widgets/dragvalue.cpp +++ b/src/widgets/dragvalue.cpp @@ -1,548 +1,548 @@ /*************************************************************************** * Copyright (C) 2011 by Till Theato (root@ttill.de) * * Copyright (C) 2011 by Jean-Baptiste Mardelle (jb@kdenlive.org) * * 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 "dragvalue.h" #include "kdenlivesettings.h" -#include +#include #include #include #include #include #include #include #include #include #include #include DragValue::DragValue(const QString &label, double defaultValue, int decimals, double min, double max, int id, const QString &suffix, bool showSlider, QWidget *parent) : QWidget(parent) , m_maximum(max) , m_minimum(min) , m_decimals(decimals) , m_default(defaultValue) , m_id(id) , m_intEdit(nullptr) , m_doubleEdit(nullptr) { if (showSlider) { setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); } else { setSizePolicy(QSizePolicy::Maximum, QSizePolicy::MinimumExpanding); } setFocusPolicy(Qt::StrongFocus); setContextMenuPolicy(Qt::CustomContextMenu); setFocusPolicy(Qt::StrongFocus); auto *l = new QHBoxLayout; l->setSpacing(0); l->setContentsMargins(0, 0, 0, 0); m_label = new CustomLabel(label, showSlider, m_maximum - m_minimum, this); l->addWidget(m_label); if (decimals == 0) { m_label->setMaximum(max - min); m_label->setStep(1); m_intEdit = new QSpinBox(this); m_intEdit->setObjectName(QStringLiteral("dragBox")); m_intEdit->setFocusPolicy(Qt::StrongFocus); if (!suffix.isEmpty()) { m_intEdit->setSuffix(suffix); } m_intEdit->setKeyboardTracking(false); m_intEdit->setButtonSymbols(QAbstractSpinBox::NoButtons); m_intEdit->setAlignment(Qt::AlignRight | Qt::AlignVCenter); m_intEdit->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); m_intEdit->setRange((int)m_minimum, (int)m_maximum); m_intEdit->setValue((int)m_default); l->addWidget(m_intEdit); connect(m_intEdit, static_cast(&QSpinBox::valueChanged), this, static_cast(&DragValue::slotSetValue)); connect(m_intEdit, &QAbstractSpinBox::editingFinished, this, &DragValue::slotEditingFinished); } else { m_doubleEdit = new QDoubleSpinBox(this); m_doubleEdit->setDecimals(decimals); m_doubleEdit->setFocusPolicy(Qt::StrongFocus); m_doubleEdit->setObjectName(QStringLiteral("dragBox")); if (!suffix.isEmpty()) { m_doubleEdit->setSuffix(suffix); } m_doubleEdit->setKeyboardTracking(false); m_doubleEdit->setButtonSymbols(QAbstractSpinBox::NoButtons); m_doubleEdit->setAlignment(Qt::AlignRight | Qt::AlignVCenter); m_doubleEdit->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); m_doubleEdit->setRange(m_minimum, m_maximum); double factor = 100; if (m_maximum - m_minimum > 10000) { factor = 1000; } m_label->setStep(1); m_doubleEdit->setSingleStep((m_maximum - m_minimum) / factor); l->addWidget(m_doubleEdit); m_doubleEdit->setValue(m_default); connect(m_doubleEdit, SIGNAL(valueChanged(double)), this, SLOT(slotSetValue(double))); connect(m_doubleEdit, &QAbstractSpinBox::editingFinished, this, &DragValue::slotEditingFinished); } connect(m_label, SIGNAL(valueChanged(double, bool)), this, SLOT(setValueFromProgress(double, bool))); connect(m_label, &CustomLabel::resetValue, this, &DragValue::slotReset); setLayout(l); if (m_intEdit) { m_label->setMaximumHeight(m_intEdit->sizeHint().height()); } else { m_label->setMaximumHeight(m_doubleEdit->sizeHint().height()); } m_menu = new QMenu(this); m_scale = new KSelectAction(i18n("Scaling"), this); m_scale->addAction(i18n("Normal scale")); m_scale->addAction(i18n("Pixel scale")); m_scale->addAction(i18n("Nonlinear scale")); m_scale->setCurrentItem(KdenliveSettings::dragvalue_mode()); m_menu->addAction(m_scale); m_directUpdate = new QAction(i18n("Direct update"), this); m_directUpdate->setCheckable(true); m_directUpdate->setChecked(KdenliveSettings::dragvalue_directupdate()); m_menu->addAction(m_directUpdate); QAction *reset = new QAction(QIcon::fromTheme(QStringLiteral("edit-undo")), i18n("Reset value"), this); connect(reset, &QAction::triggered, this, &DragValue::slotReset); m_menu->addAction(reset); if (m_id > -1) { QAction *timeline = new QAction(QIcon::fromTheme(QStringLiteral("go-jump")), i18n("Show %1 in timeline", label), this); connect(timeline, &QAction::triggered, this, &DragValue::slotSetInTimeline); connect(m_label, &CustomLabel::setInTimeline, this, &DragValue::slotSetInTimeline); m_menu->addAction(timeline); } connect(this, &QWidget::customContextMenuRequested, this, &DragValue::slotShowContextMenu); connect(m_scale, static_cast(&KSelectAction::triggered), this, &DragValue::slotSetScaleMode); connect(m_directUpdate, &QAction::triggered, this, &DragValue::slotSetDirectUpdate); } DragValue::~DragValue() { delete m_intEdit; delete m_doubleEdit; delete m_menu; delete m_label; // delete m_scale; // delete m_directUpdate; } bool DragValue::hasEditFocus() const { QWidget *fWidget = QApplication::focusWidget(); return ((fWidget != nullptr) && fWidget->parentWidget() == this); } int DragValue::spinSize() { if (m_intEdit) { return m_intEdit->sizeHint().width(); } return m_doubleEdit->sizeHint().width(); } void DragValue::setSpinSize(int width) { if (m_intEdit) { m_intEdit->setMinimumWidth(width); } else { m_doubleEdit->setMinimumWidth(width); } } void DragValue::slotSetInTimeline() { emit inTimeline(m_id); } int DragValue::precision() const { return m_decimals; } qreal DragValue::maximum() const { return m_maximum; } qreal DragValue::minimum() const { return m_minimum; } qreal DragValue::value() const { if (m_intEdit) { return m_intEdit->value(); } return m_doubleEdit->value(); } void DragValue::setMaximum(qreal max) { if (!qFuzzyCompare(m_maximum, max)) { m_maximum = max; if (m_intEdit) { m_intEdit->setRange(m_minimum, m_maximum); } else { m_doubleEdit->setRange(m_minimum, m_maximum); } } } void DragValue::setMinimum(qreal min) { if (!qFuzzyCompare(m_minimum, min)) { m_minimum = min; if (m_intEdit) { m_intEdit->setRange(m_minimum, m_maximum); } else { m_doubleEdit->setRange(m_minimum, m_maximum); } } } void DragValue::setRange(qreal min, qreal max) { m_maximum = max; m_minimum = min; if (m_intEdit) { m_intEdit->setRange(m_minimum, m_maximum); } else { m_doubleEdit->setRange(m_minimum, m_maximum); } } void DragValue::setStep(qreal step) { if (m_intEdit) { m_intEdit->setSingleStep(step); } else { m_doubleEdit->setSingleStep(step); } } void DragValue::slotReset() { if (m_intEdit) { m_intEdit->blockSignals(true); m_intEdit->setValue(m_default); m_intEdit->blockSignals(false); emit valueChanged((int)m_default, true); } else { m_doubleEdit->blockSignals(true); m_doubleEdit->setValue(m_default); m_doubleEdit->blockSignals(false); emit valueChanged(m_default, true); } m_label->setProgressValue((m_default - m_minimum) / (m_maximum - m_minimum) * m_label->maximum()); } void DragValue::slotSetValue(int value) { setValue(value, true); } void DragValue::slotSetValue(double value) { setValue(value, true); } void DragValue::setValueFromProgress(double value, bool final) { value = m_minimum + value * (m_maximum - m_minimum) / m_label->maximum(); if (m_decimals == 0) { setValue(qRound(value), final); } else { setValue(value, final); } } void DragValue::setValue(double value, bool final) { value = qBound(m_minimum, value, m_maximum); m_label->setProgressValue((value - m_minimum) / (m_maximum - m_minimum) * m_label->maximum()); if (m_intEdit) { m_intEdit->blockSignals(true); m_intEdit->setValue((int)value); m_intEdit->blockSignals(false); emit valueChanged((int)value, final); } else { m_doubleEdit->blockSignals(true); m_doubleEdit->setValue(value); m_doubleEdit->blockSignals(false); emit valueChanged(value, final); } } void DragValue::focusOutEvent(QFocusEvent *) { if (m_intEdit) { m_intEdit->setFocusPolicy(Qt::StrongFocus); } else { m_doubleEdit->setFocusPolicy(Qt::StrongFocus); } } void DragValue::focusInEvent(QFocusEvent *e) { if (m_intEdit) { m_intEdit->setFocusPolicy(Qt::WheelFocus); } else { m_doubleEdit->setFocusPolicy(Qt::WheelFocus); } if (e->reason() == Qt::TabFocusReason || e->reason() == Qt::BacktabFocusReason) { if (m_intEdit) { m_intEdit->setFocus(e->reason()); } else { m_doubleEdit->setFocus(e->reason()); } } else { QWidget::focusInEvent(e); } } void DragValue::slotEditingFinished() { if (m_intEdit) { int value = m_intEdit->value(); m_intEdit->blockSignals(true); m_intEdit->clearFocus(); m_intEdit->blockSignals(false); if (!KdenliveSettings::dragvalue_directupdate()) { emit valueChanged((double)value, true); } } else { double value = m_doubleEdit->value(); m_doubleEdit->blockSignals(true); m_doubleEdit->clearFocus(); m_doubleEdit->blockSignals(false); if (!KdenliveSettings::dragvalue_directupdate()) { emit valueChanged(value, true); } } } void DragValue::slotShowContextMenu(const QPoint &pos) { // values might have been changed by another object of this class m_scale->setCurrentItem(KdenliveSettings::dragvalue_mode()); m_directUpdate->setChecked(KdenliveSettings::dragvalue_directupdate()); m_menu->exec(mapToGlobal(pos)); } void DragValue::slotSetScaleMode(int mode) { KdenliveSettings::setDragvalue_mode(mode); } void DragValue::slotSetDirectUpdate(bool directUpdate) { KdenliveSettings::setDragvalue_directupdate(directUpdate); } void DragValue::setInTimelineProperty(bool intimeline) { if (m_label->property("inTimeline").toBool() == intimeline) { return; } m_label->setProperty("inTimeline", intimeline); style()->unpolish(m_label); style()->polish(m_label); m_label->update(); if (m_intEdit) { m_intEdit->setProperty("inTimeline", intimeline); style()->unpolish(m_intEdit); style()->polish(m_intEdit); m_intEdit->update(); } else { m_doubleEdit->setProperty("inTimeline", intimeline); style()->unpolish(m_doubleEdit); style()->polish(m_doubleEdit); m_doubleEdit->update(); } } CustomLabel::CustomLabel(const QString &label, bool showSlider, int range, QWidget *parent) : QProgressBar(parent) , m_dragMode(false) , m_showSlider(showSlider) , m_step(10.0) // m_precision(pow(10, precision)), { setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)); setFormat(QLatin1Char(' ') + label); setFocusPolicy(Qt::StrongFocus); setCursor(Qt::PointingHandCursor); setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); if (showSlider) { setRange(0, 1000); } else { setRange(0, range); QSize sh; const QFontMetrics &fm = fontMetrics(); sh.setWidth(fm.width(QLatin1Char(' ') + label + QLatin1Char(' '))); setMaximumWidth(sh.width()); setObjectName(QStringLiteral("dragOnly")); } setValue(0); } void CustomLabel::mousePressEvent(QMouseEvent *e) { if (e->button() == Qt::LeftButton) { m_dragStartPosition = m_dragLastPosition = e->pos(); e->accept(); } else if (e->button() == Qt::MidButton) { emit resetValue(); m_dragStartPosition = QPoint(-1, -1); } else { QWidget::mousePressEvent(e); } } void CustomLabel::mouseMoveEvent(QMouseEvent *e) { if (m_dragStartPosition != QPoint(-1, -1)) { if (!m_dragMode && (e->pos() - m_dragStartPosition).manhattanLength() >= QApplication::startDragDistance()) { m_dragMode = true; m_dragLastPosition = e->pos(); e->accept(); return; } if (m_dragMode) { if (KdenliveSettings::dragvalue_mode() > 0 || !m_showSlider) { int diff = e->x() - m_dragLastPosition.x(); if (e->modifiers() == Qt::ControlModifier) { diff *= 2; } else if (e->modifiers() == Qt::ShiftModifier) { diff /= 2; } if (KdenliveSettings::dragvalue_mode() == 2) { diff = (diff > 0 ? 1 : -1) * pow(diff, 2); } double nv = value() + diff * m_step; if (!qFuzzyCompare(nv, value())) { setNewValue(nv, KdenliveSettings::dragvalue_directupdate()); } } else { double nv = minimum() + ((double)maximum() - minimum()) / width() * e->pos().x(); if (!qFuzzyCompare(nv, value())) { setNewValue(nv, KdenliveSettings::dragvalue_directupdate()); } } m_dragLastPosition = e->pos(); e->accept(); } } else { QWidget::mouseMoveEvent(e); } } void CustomLabel::mouseReleaseEvent(QMouseEvent *e) { if (e->button() == Qt::MidButton) { e->accept(); return; } if (e->modifiers() == Qt::ControlModifier) { emit setInTimeline(); e->accept(); return; } if (m_dragMode) { setNewValue(value(), true); m_dragLastPosition = m_dragStartPosition; e->accept(); } else if (m_showSlider) { setNewValue((double)maximum() * e->pos().x() / width(), true); m_dragLastPosition = m_dragStartPosition; e->accept(); } m_dragMode = false; } void CustomLabel::wheelEvent(QWheelEvent *e) { if (e->delta() > 0) { if (e->modifiers() == Qt::ControlModifier) { slotValueInc(10); } else if (e->modifiers() == Qt::AltModifier) { slotValueInc(0.1); } else { slotValueInc(); } } else { if (e->modifiers() == Qt::ControlModifier) { slotValueDec(10); } else if (e->modifiers() == Qt::AltModifier) { slotValueDec(0.1); } else { slotValueDec(); } } e->accept(); } void CustomLabel::slotValueInc(double factor) { setNewValue(value() + m_step * factor, true); } void CustomLabel::slotValueDec(double factor) { setNewValue(value() - m_step * factor, true); } void CustomLabel::setProgressValue(double value) { setValue(qRound(value)); } void CustomLabel::setNewValue(double value, bool update) { setValue(qRound(value)); emit valueChanged(qRound(value), update); } void CustomLabel::setStep(double step) { m_step = step; } void CustomLabel::focusInEvent(QFocusEvent *) { setFocusPolicy(Qt::WheelFocus); } void CustomLabel::focusOutEvent(QFocusEvent *) { setFocusPolicy(Qt::StrongFocus); } diff --git a/src/widgets/dragvalue.h b/src/widgets/dragvalue.h index 621f8973f..8a3d3dcff 100644 --- a/src/widgets/dragvalue.h +++ b/src/widgets/dragvalue.h @@ -1,169 +1,169 @@ /*************************************************************************** * Copyright (C) 2011 by Till Theato (root@ttill.de) * * 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 DRAGVALUE_H #define DRAGVALUE_H #include #include #include #include #include class QAction; class QMenu; class KSelectAction; class CustomLabel : public QProgressBar { Q_OBJECT public: explicit CustomLabel(const QString &label, bool showSlider = true, int range = 1000, QWidget *parent = nullptr); void setProgressValue(double value); void setStep(double step); protected: // virtual void mouseDoubleClickEvent(QMouseEvent * event); void mousePressEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override; // virtual void paintEvent(QPaintEvent *event); void wheelEvent(QWheelEvent *event) override; void focusInEvent(QFocusEvent *e) override; void focusOutEvent(QFocusEvent *e) override; private: QPoint m_dragStartPosition; QPoint m_dragLastPosition; bool m_dragMode; bool m_showSlider; double m_step; void slotValueInc(double factor = 1); void slotValueDec(double factor = 1); void setNewValue(double, bool); signals: void valueChanged(double, bool); void setInTimeline(); void resetValue(); }; /** * @brief A widget for modifying numbers by dragging, using the mouse wheel or entering them with the keyboard. */ class DragValue : public QWidget { Q_OBJECT public: /** * @brief Default constructor. * @param label The label that will be displayed in the progress bar * @param defaultValue The default value * @param decimals The number of decimals for the parameter. 0 means it is an integer * @param min The minimum value * @param max The maximum value * @param id Used to identify this widget. If this parameter is set, "Show in Timeline" will be available in context menu. * @param suffix The suffix that will be displayed in the spinbox (for example '%') * @param showSlider If disabled, user can still drag on the label but no progress bar is shown */ explicit DragValue(const QString &label, double defaultValue, int decimals, double min = 0, double max = 100, int id = -1, const QString &suffix = QString(), bool showSlider = true, QWidget *parent = nullptr); - virtual ~DragValue(); + ~DragValue() override; /** @brief Returns the precision = number of decimals */ int precision() const; /** @brief Returns the maximum value */ qreal minimum() const; /** @brief Returns the minimum value */ qreal maximum() const; /** @brief Sets the minimum value. */ void setMinimum(qreal min); /** @brief Sets the maximum value. */ void setMaximum(qreal max); /** @brief Sets minimum and maximum value. */ void setRange(qreal min, qreal max); /** @brief Sets the size of a step (when dragging or using the mouse wheel). */ void setStep(qreal step); /** @brief Returns the current value */ qreal value() const; /** @brief Change the "inTimeline" property to paint the intimeline widget differently. */ void setInTimelineProperty(bool intimeline); /** @brief Returns minimum size for QSpinBox, used to set all spinboxes to the same width. */ int spinSize(); /** @brief Sets the minimum size for QSpinBox, used to set all spinboxes to the same width. */ void setSpinSize(int width); /** @brief Returns true if widget is currently being edited */ bool hasEditFocus() const; public slots: /** @brief Sets the value (forced to be in the valid range) and emits valueChanged. */ void setValue(double value, bool final = true); void setValueFromProgress(double value, bool final); /** @brief Resets to default value */ void slotReset(); signals: void valueChanged(double value, bool final = true); void inTimeline(int); /* * Private */ protected: /*virtual void mousePressEvent(QMouseEvent *e); virtual void mouseMoveEvent(QMouseEvent *e); virtual void mouseReleaseEvent(QMouseEvent *e);*/ /** @brief Forwards tab focus to lineedit since it is disabled. */ void focusInEvent(QFocusEvent *e) override; void focusOutEvent(QFocusEvent *e) override; // virtual void keyPressEvent(QKeyEvent *e); // virtual void wheelEvent(QWheelEvent *e); // virtual void paintEvent( QPaintEvent * event ); private slots: void slotEditingFinished(); void slotSetScaleMode(int mode); void slotSetDirectUpdate(bool directUpdate); void slotShowContextMenu(const QPoint &pos); void slotSetValue(int value); void slotSetValue(double value); void slotSetInTimeline(); private: double m_maximum; double m_minimum; int m_decimals; double m_default; int m_id; QSpinBox *m_intEdit; QDoubleSpinBox *m_doubleEdit; QMenu *m_menu; KSelectAction *m_scale; QAction *m_directUpdate; CustomLabel *m_label; }; #endif diff --git a/src/widgets/geometrywidget.cpp b/src/widgets/geometrywidget.cpp index 8238670b2..549a2c63d 100644 --- a/src/widgets/geometrywidget.cpp +++ b/src/widgets/geometrywidget.cpp @@ -1,448 +1,448 @@ /*************************************************************************** * 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 "geometrywidget.h" #include "core.h" #include "doublewidget.h" #include "dragvalue.h" #include "monitor/monitor.h" #include #include GeometryWidget::GeometryWidget(Monitor *monitor, QPair range, const QRect &rect, double opacity, const QSize frameSize, bool useRatioLock, bool useOpacity, bool percentOpacity, QWidget *parent) : QWidget(parent) , m_min(range.first) , m_max(range.second) , m_active(false) , m_monitor(monitor) , m_opacity(nullptr) , m_opacityFactor(percentOpacity ? 1. : 100.) { Q_UNUSED(useRatioLock) setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Maximum); auto *layout = new QVBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(0); m_defaultSize = pCore->getCurrentFrameSize(); m_sourceSize = frameSize.isValid() ? frameSize : m_defaultSize; /*QString paramName = i18n(paramTag.toUtf8().data()); QString comment = m_model->data(ix, AssetParameterModel::CommentRole).toString(); if (!comment.isEmpty()) { comment = i18n(comment.toUtf8().data()); }*/ auto *horLayout = new QHBoxLayout; horLayout->setSpacing(2); m_spinX = new DragValue(i18nc("x axis position", "X"), 0, 0, -99000, 99000, -1, QString(), false, this); connect(m_spinX, &DragValue::valueChanged, this, &GeometryWidget::slotAdjustRectKeyframeValue); horLayout->addWidget(m_spinX); m_spinY = new DragValue(i18nc("y axis position", "Y"), 0, 0, -99000, 99000, -1, QString(), false, this); connect(m_spinY, &DragValue::valueChanged, this, &GeometryWidget::slotAdjustRectKeyframeValue); horLayout->addWidget(m_spinY); m_spinWidth = new DragValue(i18nc("Frame width", "W"), m_defaultSize.width(), 0, 1, 99000, -1, QString(), false, this); connect(m_spinWidth, &DragValue::valueChanged, this, &GeometryWidget::slotAdjustRectWidth); horLayout->addWidget(m_spinWidth); // Lock ratio stuff m_lockRatio = new QAction(QIcon::fromTheme(QStringLiteral("link")), i18n("Lock aspect ratio"), this); m_lockRatio->setCheckable(true); connect(m_lockRatio, &QAction::triggered, this, &GeometryWidget::slotLockRatio); auto *ratioButton = new QToolButton; ratioButton->setDefaultAction(m_lockRatio); horLayout->addWidget(ratioButton); m_spinHeight = new DragValue(i18nc("Frame height", "H"), m_defaultSize.height(), 0, 1, 99000, -1, QString(), false, this); connect(m_spinHeight, &DragValue::valueChanged, this, &GeometryWidget::slotAdjustRectHeight); horLayout->addWidget(m_spinHeight); horLayout->addStretch(10); auto *horLayout2 = new QHBoxLayout; horLayout2->setSpacing(2); m_spinSize = new DragValue(i18n("Size"), 100, 2, 1, 99000, -1, i18n("%"), false, this); m_spinSize->setStep(5); connect(m_spinSize, &DragValue::valueChanged, this, &GeometryWidget::slotResize); horLayout2->addWidget(m_spinSize); if (useOpacity) { m_opacity = new DragValue(i18n("Opacity"), 100, 0, 0, 100, -1, i18n("%"), true, this); m_opacity->setValue((int)(opacity * m_opacityFactor)); connect(m_opacity, &DragValue::valueChanged, [&]() { emit valueChanged(getValue()); }); horLayout2->addWidget(m_opacity); } horLayout2->addStretch(10); // Build buttons m_originalSize = new QAction(QIcon::fromTheme(QStringLiteral("zoom-original")), i18n("Adjust to original size"), this); connect(m_originalSize, &QAction::triggered, this, &GeometryWidget::slotAdjustToSource); m_originalSize->setCheckable(true); QAction *adjustSize = new QAction(QIcon::fromTheme(QStringLiteral("zoom-fit-best")), i18n("Adjust and center in frame"), this); connect(adjustSize, &QAction::triggered, this, &GeometryWidget::slotAdjustToFrameSize); QAction *fitToWidth = new QAction(QIcon::fromTheme(QStringLiteral("zoom-fit-width")), i18n("Fit to width"), this); connect(fitToWidth, &QAction::triggered, this, &GeometryWidget::slotFitToWidth); QAction *fitToHeight = new QAction(QIcon::fromTheme(QStringLiteral("zoom-fit-height")), i18n("Fit to height"), this); connect(fitToHeight, &QAction::triggered, this, &GeometryWidget::slotFitToHeight); QAction *alignleft = new QAction(QIcon::fromTheme(QStringLiteral("kdenlive-align-left")), i18n("Align left"), this); connect(alignleft, &QAction::triggered, this, &GeometryWidget::slotMoveLeft); QAction *alignhcenter = new QAction(QIcon::fromTheme(QStringLiteral("kdenlive-align-hor")), i18n("Center horizontally"), this); connect(alignhcenter, &QAction::triggered, this, &GeometryWidget::slotCenterH); QAction *alignright = new QAction(QIcon::fromTheme(QStringLiteral("kdenlive-align-right")), i18n("Align right"), this); connect(alignright, &QAction::triggered, this, &GeometryWidget::slotMoveRight); QAction *aligntop = new QAction(QIcon::fromTheme(QStringLiteral("kdenlive-align-top")), i18n("Align top"), this); connect(aligntop, &QAction::triggered, this, &GeometryWidget::slotMoveTop); QAction *alignvcenter = new QAction(QIcon::fromTheme(QStringLiteral("kdenlive-align-vert")), i18n("Center vertically"), this); connect(alignvcenter, &QAction::triggered, this, &GeometryWidget::slotCenterV); QAction *alignbottom = new QAction(QIcon::fromTheme(QStringLiteral("kdenlive-align-bottom")), i18n("Align bottom"), this); connect(alignbottom, &QAction::triggered, this, &GeometryWidget::slotMoveBottom); auto *alignLayout = new QHBoxLayout; alignLayout->setSpacing(0); auto *alignButton = new QToolButton; alignButton->setDefaultAction(alignleft); alignButton->setAutoRaise(true); alignLayout->addWidget(alignButton); alignButton = new QToolButton; alignButton->setDefaultAction(alignhcenter); alignButton->setAutoRaise(true); alignLayout->addWidget(alignButton); alignButton = new QToolButton; alignButton->setDefaultAction(alignright); alignButton->setAutoRaise(true); alignLayout->addWidget(alignButton); alignButton = new QToolButton; alignButton->setDefaultAction(aligntop); alignButton->setAutoRaise(true); alignLayout->addWidget(alignButton); alignButton = new QToolButton; alignButton->setDefaultAction(alignvcenter); alignButton->setAutoRaise(true); alignLayout->addWidget(alignButton); alignButton = new QToolButton; alignButton->setDefaultAction(alignbottom); alignButton->setAutoRaise(true); alignLayout->addWidget(alignButton); alignButton = new QToolButton; alignButton->setDefaultAction(m_originalSize); alignButton->setAutoRaise(true); alignLayout->addWidget(alignButton); alignButton = new QToolButton; alignButton->setDefaultAction(adjustSize); alignButton->setAutoRaise(true); alignLayout->addWidget(alignButton); alignButton = new QToolButton; alignButton->setDefaultAction(fitToWidth); alignButton->setAutoRaise(true); alignLayout->addWidget(alignButton); alignButton = new QToolButton; alignButton->setDefaultAction(fitToHeight); alignButton->setAutoRaise(true); alignLayout->addWidget(alignButton); alignLayout->addStretch(10); layout->addLayout(horLayout); layout->addLayout(alignLayout); layout->addLayout(horLayout2); slotUpdateGeometryRect(rect); adjustSizeValue(); } void GeometryWidget::slotAdjustToSource() { m_spinWidth->blockSignals(true); m_spinHeight->blockSignals(true); m_spinWidth->setValue((int)(m_sourceSize.width() / pCore->getCurrentSar() + 0.5), false); m_spinHeight->setValue(m_sourceSize.height(), false); m_spinWidth->blockSignals(false); m_spinHeight->blockSignals(false); slotAdjustRectKeyframeValue(); if (m_lockRatio->isChecked()) { m_monitor->setEffectSceneProperty(QStringLiteral("lockratio"), m_originalSize->isChecked() ? (double)m_sourceSize.width() / m_sourceSize.height() : (double)m_defaultSize.width() / m_defaultSize.height()); } } void GeometryWidget::slotAdjustToFrameSize() { double monitorDar = pCore->getCurrentDar(); double sourceDar = m_sourceSize.width() / m_sourceSize.height(); m_spinWidth->blockSignals(true); m_spinHeight->blockSignals(true); if (sourceDar > monitorDar) { // Fit to width double factor = (double)m_defaultSize.width() / m_sourceSize.width() * pCore->getCurrentSar(); m_spinHeight->setValue((int)(m_sourceSize.height() * factor + 0.5)); m_spinWidth->setValue(m_defaultSize.width()); // Center m_spinY->blockSignals(true); m_spinY->setValue((m_defaultSize.height() - m_spinHeight->value()) / 2); m_spinY->blockSignals(false); } else { // Fit to height double factor = (double)m_defaultSize.height() / m_sourceSize.height(); m_spinHeight->setValue(m_defaultSize.height()); m_spinWidth->setValue((int)(m_sourceSize.width() / pCore->getCurrentSar() * factor + 0.5)); // Center m_spinX->blockSignals(true); m_spinX->setValue((m_defaultSize.width() - m_spinWidth->value()) / 2); m_spinX->blockSignals(false); } m_spinWidth->blockSignals(false); m_spinHeight->blockSignals(false); slotAdjustRectKeyframeValue(); } void GeometryWidget::slotFitToWidth() { double factor = (double)m_defaultSize.width() / m_sourceSize.width() * pCore->getCurrentSar(); m_spinWidth->blockSignals(true); m_spinHeight->blockSignals(true); m_spinHeight->setValue((int)(m_sourceSize.height() * factor + 0.5)); m_spinWidth->setValue(m_defaultSize.width()); m_spinWidth->blockSignals(false); m_spinHeight->blockSignals(false); slotAdjustRectKeyframeValue(); } void GeometryWidget::slotFitToHeight() { double factor = (double)m_defaultSize.height() / m_sourceSize.height(); m_spinWidth->blockSignals(true); m_spinHeight->blockSignals(true); m_spinHeight->setValue(m_defaultSize.height()); m_spinWidth->setValue((int)(m_sourceSize.width() / pCore->getCurrentSar() * factor + 0.5)); m_spinWidth->blockSignals(false); m_spinHeight->blockSignals(false); slotAdjustRectKeyframeValue(); } void GeometryWidget::slotResize(double value) { m_spinWidth->blockSignals(true); m_spinHeight->blockSignals(true); int w = m_originalSize->isChecked() ? m_sourceSize.width() : m_defaultSize.width(); int h = m_originalSize->isChecked() ? m_sourceSize.height() : m_defaultSize.height(); m_spinWidth->setValue(w * value / 100.0); m_spinHeight->setValue(h * value / 100.0); m_spinWidth->blockSignals(false); m_spinHeight->blockSignals(false); slotAdjustRectKeyframeValue(); } /** @brief Moves the rect to the left frame border (x position = 0). */ void GeometryWidget::slotMoveLeft() { m_spinX->setValue(0); } /** @brief Centers the rect horizontally. */ void GeometryWidget::slotCenterH() { m_spinX->setValue((m_defaultSize.width() - m_spinWidth->value()) / 2); } /** @brief Moves the rect to the right frame border (x position = frame width - rect width). */ void GeometryWidget::slotMoveRight() { m_spinX->setValue(m_defaultSize.width() - m_spinWidth->value()); } /** @brief Moves the rect to the top frame border (y position = 0). */ void GeometryWidget::slotMoveTop() { m_spinY->setValue(0); } /** @brief Centers the rect vertically. */ void GeometryWidget::slotCenterV() { m_spinY->setValue((m_defaultSize.height() - m_spinHeight->value()) / 2); } /** @brief Moves the rect to the bottom frame border (y position = frame height - rect height). */ void GeometryWidget::slotMoveBottom() { m_spinY->setValue(m_defaultSize.height() - m_spinHeight->value()); } /** @brief Un/Lock aspect ratio for size in effect parameter. */ void GeometryWidget::slotLockRatio() { - QAction *lockRatio = qobject_cast(QObject::sender()); + auto *lockRatio = qobject_cast(QObject::sender()); if (lockRatio->isChecked()) { m_monitor->setEffectSceneProperty(QStringLiteral("lockratio"), m_originalSize->isChecked() ? (double)m_sourceSize.width() / m_sourceSize.height() : (double)m_defaultSize.width() / m_defaultSize.height()); } else { m_monitor->setEffectSceneProperty(QStringLiteral("lockratio"), -1); } } void GeometryWidget::slotAdjustRectHeight() { if (m_lockRatio->isChecked()) { m_spinWidth->blockSignals(true); if (m_originalSize->isChecked()) { m_spinWidth->setValue((int)(m_spinHeight->value() * m_sourceSize.width() / m_sourceSize.height() + 0.5)); } else { m_spinWidth->setValue((int)(m_spinHeight->value() * m_defaultSize.width() / m_defaultSize.height() + 0.5)); } m_spinWidth->blockSignals(false); } adjustSizeValue(); slotAdjustRectKeyframeValue(); } void GeometryWidget::slotAdjustRectWidth() { if (m_lockRatio->isChecked()) { m_spinHeight->blockSignals(true); if (m_originalSize->isChecked()) { m_spinHeight->setValue((int)(m_spinWidth->value() * m_sourceSize.height() / m_sourceSize.width() + 0.5)); } else { m_spinHeight->setValue((int)(m_spinWidth->value() * m_defaultSize.height() / m_defaultSize.width() + 0.5)); } m_spinHeight->blockSignals(false); } adjustSizeValue(); slotAdjustRectKeyframeValue(); } void GeometryWidget::adjustSizeValue() { double size; if ((double)m_spinWidth->value() / m_spinHeight->value() < pCore->getCurrentDar()) { if (m_originalSize->isChecked()) { size = m_spinWidth->value() * 100.0 / m_sourceSize.width(); } else { size = m_spinWidth->value() * 100.0 / m_defaultSize.width(); } } else { if (m_originalSize->isChecked()) { size = m_spinHeight->value() * 100.0 / m_sourceSize.height(); } else { size = m_spinHeight->value() * 100.0 / m_defaultSize.height(); } } m_spinSize->blockSignals(true); m_spinSize->setValue(size); m_spinSize->blockSignals(false); } void GeometryWidget::slotAdjustRectKeyframeValue() { QRect rect(m_spinX->value(), m_spinY->value(), m_spinWidth->value(), m_spinHeight->value()); m_monitor->setUpEffectGeometry(rect); emit valueChanged(getValue()); } void GeometryWidget::slotUpdateGeometryRect(const QRect r) { if (!r.isValid()) { return; } m_spinX->blockSignals(true); m_spinY->blockSignals(true); m_spinWidth->blockSignals(true); m_spinHeight->blockSignals(true); m_spinX->setValue(r.x()); m_spinY->setValue(r.y()); m_spinWidth->setValue(r.width()); m_spinHeight->setValue(r.height()); m_spinX->blockSignals(false); m_spinY->blockSignals(false); m_spinWidth->blockSignals(false); m_spinHeight->blockSignals(false); m_monitor->setUpEffectGeometry(r); // slotAdjustRectKeyframeValue(); emit valueChanged(getValue()); // setupMonitor(); } void GeometryWidget::setValue(const QRect r, double opacity) { if (!r.isValid()) { return; } m_spinX->blockSignals(true); m_spinY->blockSignals(true); m_spinWidth->blockSignals(true); m_spinHeight->blockSignals(true); m_spinX->setValue(r.x()); m_spinY->setValue(r.y()); m_spinWidth->setValue(r.width()); m_spinHeight->setValue(r.height()); if (m_opacity) { m_opacity->blockSignals(true); if (opacity < 0) { opacity = 100 / m_opacityFactor; } m_opacity->setValue((int)(opacity * m_opacityFactor + 0.5)); m_opacity->blockSignals(false); } m_spinX->blockSignals(false); m_spinY->blockSignals(false); m_spinWidth->blockSignals(false); m_spinHeight->blockSignals(false); adjustSizeValue(); m_monitor->setUpEffectGeometry(r); } const QString GeometryWidget::getValue() const { if (m_opacity) { QLocale locale; return QStringLiteral("%1 %2 %3 %4 %5") .arg(m_spinX->value()) .arg(m_spinY->value()) .arg(m_spinWidth->value()) .arg(m_spinHeight->value()) .arg(locale.toString(m_opacity->value() / m_opacityFactor)); } return QStringLiteral("%1 %2 %3 %4").arg(m_spinX->value()).arg(m_spinY->value()).arg(m_spinWidth->value()).arg(m_spinHeight->value()); } void GeometryWidget::connectMonitor(bool activate) { if (m_active == activate) { return; } m_active = activate; if (activate) { connect(m_monitor, &Monitor::effectChanged, this, &GeometryWidget::slotUpdateGeometryRect, Qt::UniqueConnection); QRect rect(m_spinX->value(), m_spinY->value(), m_spinWidth->value(), m_spinHeight->value()); m_monitor->setUpEffectGeometry(rect); } else { m_monitor->setEffectKeyframe(false); disconnect(m_monitor, &Monitor::effectChanged, this, &GeometryWidget::slotUpdateGeometryRect); } } void GeometryWidget::slotSetRange(QPair range) { m_min = range.first; m_max = range.second; } diff --git a/src/widgets/positionwidget.cpp b/src/widgets/positionwidget.cpp index c9c7c7f67..81f89eaa4 100644 --- a/src/widgets/positionwidget.cpp +++ b/src/widgets/positionwidget.cpp @@ -1,97 +1,97 @@ /*************************************************************************** positionedit.cpp - description ------------------- begin : 03 Aug 2008 copyright : (C) 2008 by Marco Gittler email : g.marco@freenet.de ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "positionwidget.h" #include "kdenlivesettings.h" #include "timecodedisplay.h" #include #include #include PositionWidget::PositionWidget(const QString &name, int pos, int min, int max, const Timecode &tc, const QString &comment, QWidget *parent) : QWidget(parent) { auto *layout = new QHBoxLayout(this); QLabel *label = new QLabel(name, this); m_slider = new QSlider(Qt::Horizontal, this); m_slider->setSizePolicy(QSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred)); m_slider->setRange(min, max); m_display = new TimecodeDisplay(tc, this); m_display->setSizePolicy(QSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred)); m_display->setRange(min, max); layout->addWidget(label); layout->addWidget(m_slider); layout->addWidget(m_display); m_slider->setValue(pos); m_display->setValue(pos); connect(m_slider, SIGNAL(valueChanged(int)), m_display, SLOT(setValue(int))); connect(m_slider, &QAbstractSlider::valueChanged, this, &PositionWidget::valueChanged); connect(m_display, &TimecodeDisplay::timeCodeEditingFinished, this, &PositionWidget::slotUpdatePosition); setToolTip(comment); } -PositionWidget::~PositionWidget() {} +PositionWidget::~PositionWidget() = default; void PositionWidget::updateTimecodeFormat() { m_display->slotUpdateTimeCodeFormat(); } int PositionWidget::getPosition() const { return m_slider->value(); } void PositionWidget::setPosition(int pos) { m_slider->setValue(pos); } void PositionWidget::slotUpdatePosition() { m_slider->blockSignals(true); m_slider->setValue(m_display->getValue()); m_slider->blockSignals(false); emit valueChanged(); } void PositionWidget::setRange(int min, int max, bool absolute) { if (absolute) { m_slider->setRange(min, max); m_display->setRange(min, max); } else { m_slider->setRange(0, max - min); m_display->setRange(0, max - min); } } bool PositionWidget::isValid() const { return m_slider->minimum() != m_slider->maximum(); } void PositionWidget::slotShowComment(bool show) { Q_UNUSED(show); } diff --git a/src/widgets/positionwidget.h b/src/widgets/positionwidget.h index 7dcf3a0b0..3b6c6a9a9 100644 --- a/src/widgets/positionwidget.h +++ b/src/widgets/positionwidget.h @@ -1,77 +1,77 @@ /*************************************************************************** positionedit.h - description ------------------- begin : 03 Aug 2008 copyright : (C) 2008 by Marco Gittler email : g.marco@freenet.de ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #ifndef POSITIONEDIT_H #define POSITIONEDIT_H #include "timecode.h" #include #include class QSlider; class TimecodeDisplay; /*@brief This class is used to display a parameter with time value */ class PositionWidget : public QWidget { Q_OBJECT public: /** @brief Sets up the parameter's GUI. * @param name Name of the parameter * @param pos Initial position (in time) * @param min Minimum time value * @param max maximum time value * @param tc Timecode object to define time format * @param comment (optional) A comment explaining the parameter. Will be shown in a tooltip. * @param parent (optional) Parent Widget */ explicit PositionWidget(const QString &name, int pos, int min, int max, const Timecode &tc, const QString &comment = QString(), QWidget *parent = nullptr); - ~PositionWidget(); + ~PositionWidget() override; /** @brief get current position */ int getPosition() const; /** @brief set position */ void setPosition(int pos); /** @brief Call this when the timecode has been changed project-wise */ void updateTimecodeFormat(); /** @brief checks that the allowed time interval is valid */ bool isValid() const; public slots: /** @brief change the range of the widget @param min New minimum value @param max New maximum value @param absolute If true, the range is shifted to start in 0 */ void setRange(int min, int max, bool absolute = false); void slotShowComment(bool show); private: TimecodeDisplay *m_display; QSlider *m_slider; private slots: void slotUpdatePosition(); signals: void valueChanged(); }; #endif diff --git a/src/widgets/progressbutton.h b/src/widgets/progressbutton.h index 668dd0e11..d8f05203b 100644 --- a/src/widgets/progressbutton.h +++ b/src/widgets/progressbutton.h @@ -1,64 +1,64 @@ /* Copyright (C) 2016 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 PROGRESSBUTTON_H #define PROGRESSBUTTON_H #include #include #include class QAction; /** * @class ProgressButton * @brief A Toolbar button with a small progress bar. * */ class ProgressButton : public QToolButton { Q_PROPERTY(int progress READ progress WRITE setProgress) Q_OBJECT public: explicit ProgressButton(const QString &text, double max = 100, QWidget *parent = nullptr); - ~ProgressButton(); + ~ProgressButton() override; int progress() const; void setProgress(int); void defineDefaultAction(QAction *action, QAction *actionInProgress); protected: void paintEvent(QPaintEvent *event) override; private: QAction *m_defaultAction; int m_max; int m_progress; QElapsedTimer m_timer; QString m_remainingTime; int m_iconSize; QFont m_progressFont; QStyleOptionToolButton m_buttonStyle; /** @brief While rendering, replace real action by a fake on so that rendering is not triggered when clicking again. */ QAction *m_dummyAction; }; #endif diff --git a/tests/keyframetest.cpp b/tests/keyframetest.cpp index 112b1e0f6..fceebb6b2 100644 --- a/tests/keyframetest.cpp +++ b/tests/keyframetest.cpp @@ -1,248 +1,250 @@ +#include + #include "test_utils.hpp" using namespace fakeit; 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(const std::shared_ptr &m) { - auto m2 = std::shared_ptr(new KeyframeModel(m->m_model, m->m_index, m->m_undoStack)); + auto m2 = std::make_shared(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)); + auto model = std::make_shared(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 75d96f847..214e58187 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(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(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)); + list.emplace_back(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)); + list.emplace_back(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)); + list.emplace_back(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)); + list.emplace_back(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)); + list.emplace_back(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)); + list.emplace_back(GenTime(5), QLatin1String("non conflicting"), 0); std::vector otherMarkers; - otherMarkers.push_back(Marker(GenTime(5), QLatin1String("non conflicting"), 0)); + otherMarkers.emplace_back(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)); + otherMarkers.emplace_back(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; }