übernahme Code Shortcut

This commit is contained in:
georg0480
2026-01-31 15:28:10 +01:00
parent 6f4d6b9301
commit ef46c21291
1787 changed files with 1126465 additions and 0 deletions

454
src/commands/undohelper.cpp Normal file
View File

@@ -0,0 +1,454 @@
/*
* Copyright (c) 2015-2024 Meltytech, LLC
*
* This program is free software: you can 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 <http://www.gnu.org/licenses/>.
*/
#include "undohelper.h"
#include "Logger.h"
#include "mltcontroller.h"
#include "models/audiolevelstask.h"
#include "shotcut_mlt_properties.h"
#include <QScopedPointer>
#include <QUuid>
#ifdef UNDOHELPER_DEBUG
#define UNDOLOG LOG_DEBUG()
#else
#define UNDOLOG \
if (false) \
LOG_DEBUG()
#endif
UndoHelper::UndoHelper(MultitrackModel &model)
: m_model(model)
, m_hints(NoHints)
{}
void UndoHelper::recordBeforeState()
{
#ifdef UNDOHELPER_DEBUG
debugPrintState("Before state");
#endif
m_state.clear();
m_clipsAdded.clear();
m_insertedOrder.clear();
for (int i = 0; i < m_model.trackList().count(); ++i) {
int mltIndex = m_model.trackList()[i].mlt_index;
QScopedPointer<Mlt::Producer> trackProducer(m_model.tractor()->track(mltIndex));
Mlt::Playlist playlist(*trackProducer);
for (int j = 0; j < playlist.count(); ++j) {
QScopedPointer<Mlt::Producer> clip(playlist.get_clip(j));
QUuid uid = MLT.ensureHasUuid(clip->parent());
if (clip->is_blank()) {
uid = MLT.ensureHasUuid(*clip);
}
m_insertedOrder << uid;
Info &info = m_state[uid];
if (!(m_hints & SkipXML))
info.xml = MLT.XML(&clip->parent());
Mlt::ClipInfo clipInfo;
playlist.clip_info(j, &clipInfo);
info.frame_in = clipInfo.frame_in;
info.frame_out = clipInfo.frame_out;
info.oldTrackIndex = i;
info.oldClipIndex = j;
info.isBlank = playlist.is_blank(j);
if (clipInfo.cut && clipInfo.cut->property_exists(kShotcutGroupProperty)) {
info.group = clipInfo.cut->get_int(kShotcutGroupProperty);
}
}
}
}
void UndoHelper::recordAfterState()
{
#ifdef UNDOHELPER_DEBUG
debugPrintState("After state");
#endif
QList<QUuid> clipsRemoved = m_state.keys();
m_clipsAdded.clear();
for (int i = 0; i < m_model.trackList().count(); ++i) {
int mltIndex = m_model.trackList()[i].mlt_index;
QScopedPointer<Mlt::Producer> trackProducer(m_model.tractor()->track(mltIndex));
Mlt::Playlist playlist(*trackProducer);
for (int j = 0; j < playlist.count(); ++j) {
QScopedPointer<Mlt::Producer> clip(playlist.get_clip(j));
QUuid uid = MLT.ensureHasUuid(clip->parent());
if (clip->is_blank()) {
uid = MLT.ensureHasUuid(*clip);
}
/* Clips not previously in m_state are new */
if (!m_state.contains(uid)) {
UNDOLOG << "New clip at" << i << j;
m_clipsAdded << uid;
m_affectedTracks << i;
} else {
Info &info = m_state[uid];
info.changes = 0;
info.newTrackIndex = i;
info.newClipIndex = j;
/* Indices have changed; these are moved */
if (info.oldTrackIndex != info.newTrackIndex
|| info.oldClipIndex != info.newClipIndex) {
UNDOLOG << "Clip" << uid << "moved from" << info.oldTrackIndex
<< info.oldClipIndex << "to" << info.newTrackIndex << info.newClipIndex;
info.changes |= Moved;
m_affectedTracks << info.oldTrackIndex;
m_affectedTracks << info.newTrackIndex;
}
if (!(m_hints & SkipXML) && !info.isBlank) {
QString newXml = MLT.XML(&clip->parent());
if (info.xml != newXml) {
UNDOLOG << "Modified xml:" << uid;
info.changes |= XMLModified;
m_affectedTracks << i;
}
}
Mlt::ClipInfo newInfo;
playlist.clip_info(j, &newInfo);
/* Only in/out point changes are handled at this time. */
if (info.frame_in != newInfo.frame_in || info.frame_out != newInfo.frame_out) {
UNDOLOG << "In/out changed:" << uid;
info.changes |= ClipInfoModified;
info.in_delta = info.frame_in - newInfo.frame_in;
info.out_delta = newInfo.frame_out - info.frame_out;
m_affectedTracks << i;
}
}
clipsRemoved.removeOne(uid);
}
}
/* Clips that did not show up are removed from the timeline */
foreach (QUuid uid, clipsRemoved) {
UNDOLOG << "Clip removed:" << uid;
auto &info = m_state[uid];
info.changes = Removed;
m_affectedTracks << info.oldTrackIndex;
}
}
void UndoHelper::undoChanges()
{
#ifdef UNDOHELPER_DEBUG
debugPrintState("Before undo");
#endif
if (m_hints & RestoreTracks) {
restoreAffectedTracks();
emit m_model.modified();
#ifdef UNDOHELPER_DEBUG
debugPrintState("After undo");
#endif
return;
}
QMap<int, int> indexAdjustment;
/* We're walking through the list in the order of uids, which is the order in which the
* clips were laid out originally. As we go through the clips we make sure the clips behind
* the current index are as they were originally before we move on to the next one */
foreach (QUuid uid, m_insertedOrder) {
const Info &info = m_state[uid];
UNDOLOG << "Handling uid" << uid << "on track" << info.oldTrackIndex << "index"
<< info.oldClipIndex;
int trackIndex = m_model.trackList()[info.oldTrackIndex].mlt_index;
QScopedPointer<Mlt::Producer> trackProducer(m_model.tractor()->track(trackIndex));
Mlt::Playlist playlist(*trackProducer);
/* This is the index in the track we're currently restoring */
int currentIndex = qMin(info.oldClipIndex + indexAdjustment[trackIndex],
playlist.count() - 1);
/* Clips that were moved are simply searched for using the uid, and moved in place. We
* do not use the indices directly because they become invalid once the playlist is
* modified. */
if (info.changes & Moved) {
Q_ASSERT(info.newTrackIndex == info.oldTrackIndex
&& "cross-track moves are unsupported so far");
int clipCurrentlyAt = -1;
for (int i = 0; i < playlist.count(); ++i) {
QScopedPointer<Mlt::Producer> clip(playlist.get_clip(i));
if (MLT.uuid(clip->parent()) == uid || MLT.uuid(*clip) == uid) {
clipCurrentlyAt = i;
break;
}
}
Q_ASSERT(clipCurrentlyAt != -1 && "Moved clip could not be found");
UNDOLOG << "Found clip with uid" << uid << "at index" << clipCurrentlyAt;
if (clipCurrentlyAt != info.oldClipIndex
&& (currentIndex < clipCurrentlyAt || currentIndex > clipCurrentlyAt + 1)) {
UNDOLOG << "moving from" << clipCurrentlyAt << "to" << currentIndex;
QModelIndex modelIndex = m_model.createIndex(clipCurrentlyAt, 0, info.oldTrackIndex);
m_model.beginMoveRows(modelIndex.parent(),
clipCurrentlyAt,
clipCurrentlyAt,
modelIndex.parent(),
currentIndex);
playlist.move(clipCurrentlyAt, currentIndex);
m_model.endMoveRows();
}
}
/* Removed clips are reinserted using their stored XML */
if (info.changes & Removed) {
QModelIndex modelIndex = m_model.createIndex(currentIndex, 0, info.oldTrackIndex);
m_model.beginInsertRows(modelIndex.parent(), currentIndex, currentIndex);
if (info.isBlank) {
playlist.insert_blank(currentIndex, info.frame_out - info.frame_in);
UNDOLOG << "inserting isBlank at " << currentIndex;
} else {
UNDOLOG << "inserting clip at " << currentIndex << uid;
Q_ASSERT(!(m_hints & SkipXML) && "Cannot restore clip without stored XML");
Q_ASSERT(!info.xml.isEmpty());
Mlt::Producer restoredClip(MLT.profile(),
"xml-string",
info.xml.toUtf8().constData());
if (restoredClip.type() == mlt_service_tractor_type) { // transition
restoredClip.set("mlt_type", "mlt_producer");
} else {
fixTransitions(playlist, currentIndex, restoredClip);
}
playlist.insert(restoredClip, currentIndex, info.frame_in, info.frame_out);
}
m_model.endInsertRows();
QScopedPointer<Mlt::Producer> clip(playlist.get_clip(currentIndex));
Q_ASSERT(currentIndex < playlist.count());
Q_ASSERT(!clip.isNull());
if (info.isBlank) {
MLT.setUuid(*clip, uid);
} else {
MLT.setUuid(clip->parent(), uid);
}
if (info.group >= 0) {
clip->set(kShotcutGroupProperty, info.group);
}
AudioLevelsTask::start(clip->parent(), &m_model, modelIndex);
indexAdjustment[trackIndex]++;
}
/* Only in/out points handled so far */
if (info.changes & ClipInfoModified) {
int filterIn = MLT.filterIn(playlist, currentIndex);
int filterOut = MLT.filterOut(playlist, currentIndex);
QScopedPointer<Mlt::Producer> clip(playlist.get_clip(currentIndex));
if (clip && clip->is_valid()) {
UNDOLOG << "resizing clip at" << currentIndex << "in" << info.frame_in << "out"
<< info.frame_out;
if (clip->parent().get_data("mlt_mix"))
clip->parent().set("mlt_mix", nullptr, 0);
if (clip->get_data("mix_in"))
clip->set("mix_in", nullptr, 0);
if (clip->get_data("mix_out"))
clip->set("mix_out", nullptr, 0);
playlist.resize_clip(currentIndex, info.frame_in, info.frame_out);
MLT.adjustClipFilters(clip->parent(),
filterIn,
filterOut,
info.in_delta,
info.out_delta,
info.in_delta);
}
QModelIndex modelIndex = m_model.createIndex(currentIndex, 0, info.oldTrackIndex);
QVector<int> roles;
roles << MultitrackModel::InPointRole;
roles << MultitrackModel::OutPointRole;
roles << MultitrackModel::DurationRole;
emit m_model.dataChanged(modelIndex, modelIndex, roles);
if (clip && clip->is_valid())
AudioLevelsTask::start(clip->parent(), &m_model, modelIndex);
}
}
/* Finally we walk through the tracks once more, removing clips that
* were added, and clearing the temporarily used uid property */
int trackIndex = 0;
foreach (const Track &track, m_model.trackList()) {
QScopedPointer<Mlt::Producer> trackProducer(m_model.tractor()->track(track.mlt_index));
Mlt::Playlist playlist(*trackProducer);
for (int i = playlist.count() - 1; i >= 0; --i) {
QScopedPointer<Mlt::Producer> clip(playlist.get_clip(i));
QUuid uid = MLT.uuid(clip->parent());
if (clip->is_blank()) {
uid = MLT.uuid(*clip);
}
if (m_clipsAdded.removeOne(uid)) {
UNDOLOG << "Removing clip at" << i;
m_model.beginRemoveRows(m_model.index(trackIndex), i, i);
if (clip->parent().get_data("mlt_mix"))
clip->parent().set("mlt_mix", NULL, 0);
if (clip->get_data("mix_in"))
clip->set("mix_in", NULL, 0);
if (clip->get_data("mix_out"))
clip->set("mix_out", NULL, 0);
playlist.remove(i);
m_model.endRemoveRows();
}
}
trackIndex++;
}
emit m_model.modified();
#ifdef UNDOHELPER_DEBUG
debugPrintState("After undo");
#endif
}
void UndoHelper::setHints(OptimizationHints hints)
{
m_hints = hints;
}
void UndoHelper::debugPrintState(const QString &title)
{
LOG_DEBUG() << "timeline state:" << title << "{";
for (int i = 0; i < m_model.trackList().count(); ++i) {
int mltIndex = m_model.trackList()[i].mlt_index;
QString trackStr = QStringLiteral(" track %1 (mlt-idx %2):").arg(i).arg(mltIndex);
QScopedPointer<Mlt::Producer> trackProducer(m_model.tractor()->track(mltIndex));
Mlt::Playlist playlist(*trackProducer);
for (int j = 0; j < playlist.count(); ++j) {
Mlt::ClipInfo info;
playlist.clip_info(j, &info);
QUuid uid = MLT.uuid(*info.producer);
if (info.producer->is_blank() && info.cut) {
uid = MLT.uuid(*info.cut);
}
trackStr += QStringLiteral(" [ %5 %1 -> %2 (%3 frames) %4]")
.arg(info.frame_in)
.arg(info.frame_out)
.arg(info.frame_count)
.arg(info.cut->is_blank() ? "blank " : "clip")
.arg(uid.toString());
}
LOG_DEBUG() << qPrintable(trackStr);
}
LOG_DEBUG() << "}";
}
void UndoHelper::restoreAffectedTracks()
{
// Remove everything in the affected tracks.
for (const auto &trackIndex : std::as_const(m_affectedTracks)) {
if (trackIndex >= 0 && trackIndex < m_model.trackList().size()) {
auto mlt_index = m_model.trackList().at(trackIndex).mlt_index;
QScopedPointer<Mlt::Producer> producer(m_model.tractor()->track(mlt_index));
if (producer->is_valid()) {
Mlt::Playlist playlist(*producer.data());
m_model.beginRemoveRows(m_model.index(trackIndex), 0, playlist.count() - 1);
UNDOLOG << "clearing track" << trackIndex;
playlist.clear();
m_model.endRemoveRows();
}
}
}
for (const auto &uid : std::as_const(m_insertedOrder)) {
const Info &info = m_state[uid];
if (m_affectedTracks.contains(info.oldTrackIndex)) {
UNDOLOG << "Handling uid" << uid << "on track" << info.oldTrackIndex << "index"
<< info.oldClipIndex;
// Clips are restored using their stored XML.
int mltIndex = m_model.trackList()[info.oldTrackIndex].mlt_index;
QScopedPointer<Mlt::Producer> trackProducer(m_model.tractor()->track(mltIndex));
Mlt::Playlist playlist(*trackProducer);
auto currentIndex = playlist.count();
QModelIndex modelIndex = m_model.createIndex(currentIndex, 0, info.oldTrackIndex);
m_model.beginInsertRows(modelIndex.parent(), currentIndex, currentIndex);
if (info.isBlank) {
playlist.blank(info.frame_out - info.frame_in);
UNDOLOG << "appending blank at" << currentIndex << info.frame_out << info.frame_in;
} else {
UNDOLOG << "appending clip at" << currentIndex;
Q_ASSERT(!(m_hints & SkipXML) && "Cannot restore clip without stored XML");
Q_ASSERT(!info.xml.isEmpty());
Mlt::Producer restoredClip(MLT.profile(),
"xml-string",
info.xml.toUtf8().constData());
if (restoredClip.type() == mlt_service_tractor_type) { // transition
restoredClip.set("mlt_type", "mlt_producer");
}
playlist.append(restoredClip, info.frame_in, info.frame_out);
if (info.group >= 0) {
QScopedPointer<Mlt::Producer> clip(playlist.get_clip(currentIndex));
clip->set(kShotcutGroupProperty, info.group);
}
}
m_model.endInsertRows();
QScopedPointer<Mlt::Producer> clip(playlist.get_clip(currentIndex));
Q_ASSERT(currentIndex < playlist.count());
Q_ASSERT(!clip.isNull());
if (info.isBlank) {
MLT.setUuid(*clip, uid);
} else {
MLT.setUuid(clip->parent(), uid);
}
AudioLevelsTask::start(clip->parent(), &m_model, modelIndex);
}
}
for (const auto &trackIndex : std::as_const(m_affectedTracks)) {
if (trackIndex >= 0 && trackIndex < m_model.trackList().size()) {
auto mlt_index = m_model.trackList().at(trackIndex).mlt_index;
QScopedPointer<Mlt::Producer> producer(m_model.tractor()->track(mlt_index));
if (producer->is_valid()) {
Mlt::Playlist playlist(*producer.data());
for (auto currentIndex = 0; currentIndex < playlist.count(); currentIndex++) {
Mlt::Producer clip = playlist.get_clip(currentIndex);
fixTransitions(playlist, currentIndex, clip);
}
}
}
}
}
void UndoHelper::fixTransitions(Mlt::Playlist playlist, int clipIndex, Mlt::Producer clip)
{
if (clip.is_blank()) {
return;
}
int transitionIndex = 0;
for (auto currentIndex : {clipIndex + 1, clipIndex - 1}) {
// Connect a transition on the right/left to the new producer.
Mlt::Producer producer(playlist.get_clip(currentIndex));
if (producer.is_valid() && producer.parent().get(kShotcutTransitionProperty)) {
Mlt::Tractor transition(producer.parent());
if (transition.is_valid()) {
QScopedPointer<Mlt::Producer> transitionClip(transition.track(transitionIndex));
if (transitionClip->is_valid()
&& transitionClip->parent().get_service() != clip.parent().get_service()) {
UNDOLOG << "Fixing transition at clip index" << currentIndex
<< "transition index" << transitionIndex;
transitionClip.reset(
clip.cut(transitionClip->get_in(), transitionClip->get_out()));
transition.set_track(*transitionClip.data(), transitionIndex);
}
}
}
transitionIndex++;
}
}