Skip to content

Commit

Permalink
Add "Apply copied filters" action to timeline clips
Browse files Browse the repository at this point in the history
Any filters that would be duplicated are removed before the filters
are applied. This is why it is not simply called "paste filters".

This allows users to make changes to one or more filters on a clip
and then apply the same changes to many other clips.
  • Loading branch information
bmatherly committed Apr 8, 2024
1 parent cfc9b00 commit 139a251
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 0 deletions.
94 changes: 94 additions & 0 deletions src/commands/timelinecommands.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1975,6 +1975,100 @@ void AlignClipsCommand::undo()
m_undoHelper.undoChanges();
}

ApplyFiltersCommand::ApplyFiltersCommand(MultitrackModel &model, const QString &filterProducerXml,
QUndoCommand *parent)
: QUndoCommand(parent)
, m_model(model)
, m_xml(filterProducerXml)
{
setText(QObject::tr("Apply copied filters"));
}

void ApplyFiltersCommand::addClip(int trackIndex, int clipIndex)
{
auto clipInfo = m_model.getClipInfo(trackIndex, clipIndex);
if (clipInfo && clipInfo->producer && !clipInfo->cut->is_blank() ) {
ClipPosition position(trackIndex, clipIndex);
m_prevFilters.insert(position, MLT.XML(clipInfo->producer));
}
}

void ApplyFiltersCommand::redo()
{
LOG_DEBUG() << "clips:" << m_prevFilters.size();

Mlt::Producer filtersProducer(MLT.profile(), "xml-string", m_xml.toUtf8().constData());
if (!filtersProducer.is_valid() || filtersProducer.filter_count() < 1
|| !filtersProducer.get_int(kShotcutFiltersClipboard)) {
LOG_ERROR() << "Invalid filters producer";
return;
}

// Get the metadata for all the applied filters
QList<QmlMetadata *> m_applyMeta;
for (int i = 0; i < filtersProducer.filter_count(); i++) {
Mlt::Filter *filter = filtersProducer.filter(i);
if (filter && filter->is_valid() && !filter->get_int("_loader")) {
m_applyMeta.append(MAIN.filterController()->metadataForService(filter));
}
delete filter;
}

for (auto &clip : m_prevFilters.keys()) {
auto clipInfo = m_model.getClipInfo(clip.trackIndex, clip.clipIndex);
if (clipInfo && clipInfo->producer) {
// Remove any filters that would be duplicated by the new filters
for (int i = 0; i < clipInfo->producer->filter_count(); i++) {
Mlt::Filter *filter = clipInfo->producer->filter(i);
if (filter && filter->is_valid() && !filter->get_int("_loader")) {
QmlMetadata *currentMeta = MAIN.filterController()->metadataForService(filter);
for (int j = 0; j < m_applyMeta.size(); j++) {
if (m_applyMeta[j] == currentMeta) {
clipInfo->producer->detach(*filter);
i--;
break;
}
}
}
delete filter;
}
// Apply the new filters
MLT.pasteFilters(clipInfo->producer, &filtersProducer);
} else {
LOG_ERROR() << "Unable to find clip" << clip.trackIndex << clip.clipIndex;
}
}
}

void ApplyFiltersCommand::undo()
{
LOG_DEBUG() << "clips:" << m_prevFilters.size();
for (auto &clip : m_prevFilters.keys()) {
auto clipInfo = m_model.getClipInfo(clip.trackIndex, clip.clipIndex);
if (clipInfo && clipInfo->producer) {
// Remove existing filters
for (int i = 0; i < clipInfo->producer->filter_count(); i++) {
Mlt::Filter *filter = clipInfo->producer->filter(i);
if (filter && filter->is_valid() && !filter->get_int("_loader")) {
clipInfo->producer->detach(*filter);
i--;
}
delete filter;
}
// Copy the previous filters
Mlt::Producer previousProducer(MLT.profile(), "xml-string",
m_prevFilters[clip].toUtf8().constData());
if (previousProducer.is_valid()) {
MLT.pasteFilters(clipInfo->producer, &previousProducer);
} else {
LOG_ERROR() << "Unable to restore previous producer";
}
} else {
LOG_ERROR() << "Unable to find clip" << clip.trackIndex << clip.clipIndex;
}
}
}

} // namespace

#include "moc_timelinecommands.cpp"
14 changes: 14 additions & 0 deletions src/commands/timelinecommands.h
Original file line number Diff line number Diff line change
Expand Up @@ -746,6 +746,20 @@ class AlignClipsCommand : public QUndoCommand
QVector<Alignment> m_alignments;
};

class ApplyFiltersCommand : public QUndoCommand
{
public:
ApplyFiltersCommand(MultitrackModel &model, const QString &filterProducerXml,
QUndoCommand *parent = 0);
void addClip(int trackIndex, int clipIndex);
void redo();
void undo();

private:
MultitrackModel &m_model;
QString m_xml;
QMap<ClipPosition, QString> m_prevFilters;
};

} // namespace Timeline

Expand Down
46 changes: 46 additions & 0 deletions src/docks/timelinedock.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ TimelineDock::TimelineDock(QWidget *parent) :
editMenu->addAction(Actions["timelineRippleTrimClipOutAction"]);
editMenu->addAction(Actions["timelineSplitAction"]);
editMenu->addAction(Actions["timelineSplitAllTracksAction"]);
editMenu->addAction(Actions["timelineApplyCopiedFiltersAction"]);
m_mainMenu->addMenu(editMenu);
QMenu *viewMenu = new QMenu(tr("View"), this);
viewMenu->addAction(Actions["timelineZoomOutAction"]);
Expand Down Expand Up @@ -152,6 +153,7 @@ TimelineDock::TimelineDock(QWidget *parent) :
m_clipMenu->addAction(Actions["timelineMergeWithNextAction"]);
m_clipMenu->addAction(Actions["timelineDetachAudioAction"]);
m_clipMenu->addAction(Actions["timelineAlignToReferenceAction"]);
m_clipMenu->addAction(Actions["timelineApplyCopiedFiltersAction"]);
m_clipMenu->addAction(Actions["timelineUpdateThumbnailsAction"]);
m_clipMenu->addAction(Actions["timelineRebuildAudioWaveformAction"]);
m_clipMenu->addAction(Actions["timelinePropertiesAction"]);
Expand Down Expand Up @@ -1287,6 +1289,30 @@ void TimelineDock::setupActions()
});
Actions.add("timelineAlignToReferenceAction", action);

action = new QAction(tr("Apply Copied Filters"), this);
action->setEnabled(false);
connect(action, &QAction::triggered, this, [&](bool checked) {
if (m_selection.selectedClips.length() > 0) {
applyCopiedFiltersToSelectdClips();
}
});
connect(this, &TimelineDock::selectionChanged, action, [ = ]() {
bool enabled = false;
foreach (auto point, selection()) {
// At least one selected item must be a valid clip
if (!isBlank(point.y(), point.x()) && !isTransition(point.y(), point.x())) {
enabled = true;
break;
}
}
if (enabled) {
auto s = QGuiApplication::clipboard()->text();
enabled = s.contains(kShotcutFiltersClipboard);
}
action->setEnabled(enabled);
});
Actions.add("timelineApplyCopiedFiltersAction", action);

action = new QAction(tr("Update Thumbnails"), this);
action->setEnabled(false);
connect(action, &QAction::triggered, this, [&](bool checked) {
Expand Down Expand Up @@ -2063,6 +2089,26 @@ void TimelineDock::alignSelectedClips()
restoreSelection();
}

void TimelineDock::applyCopiedFiltersToSelectdClips()
{
QString xmlToUse = QGuiApplication::clipboard()->text();
if (MLT.isMltXml(xmlToUse) && xmlToUse.contains(kShotcutFiltersClipboard)) {
if (!Settings.proxyEnabled()) {
ProxyManager::filterXML(xmlToUse, "");
}
} else {
LOG_DEBUG() << "Unable to read copied filters" << xmlToUse;
return;
}
Timeline::ApplyFiltersCommand *command = new Timeline::ApplyFiltersCommand(m_model, xmlToUse);
foreach (auto i, m_selection.selectedClips) {
if (!isBlank(i.y(), i.x()) && !isTransition(i.y(), i.x())) {
command->addClip(i.y(), i.x());
}
}
MAIN.undoStack()->push(command);
}

void TimelineDock::onShowFrame(const SharedFrame &frame)
{
if (m_ignoreNextPositionChange) {
Expand Down
1 change: 1 addition & 0 deletions src/docks/timelinedock.h
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ public slots:
return m_model.tractor() && !m_model.trackList().empty();
}
void reportSelectionChange();
void applyCopiedFiltersToSelectdClips();

QQuickWidget m_quickView;
MultitrackModel m_model;
Expand Down

0 comments on commit 139a251

Please sign in to comment.